How do create a working instance of TChart using GDI+ in runtime, in a non main thread
Posted: Mon Mar 23, 2020 12:12 am
I have this thread that should generate a report and send it as an HTML email with an embedded chart (PNG image).
I am trying to use TChart to generate this chart, and then use VclTee.TeePNG.TPNGExportFormat to generate the PNG stream.
The TChart is created in runtime from within a separate TThread (not the main thread).
The problem is that there are tons of problems related to this.
Even creating a TChart in a separate thread throws exception like "GDI+ error 3 out of memory" or "List index out of bounds" etc.
I imagine that lots of other developers before me must have tried using a TChart component to return a PNG stream in a response from TIdHTTPServer, where each request will run its own context thread?
How did they get TChart to work using GDI+ in a separate thread, or did they not?
I know a lot of people advise NOT to use GDI+ in non-main thread.
However, I am not trying to share the TChart object itself between threads, not even render it on the screen. It will only life for the purpose of generating a PNG stream, and then die. All this will be done in the same thread.
I have tried calling calling VclTee.TeeGDIPOBJ.TeeGDIPlusStartup and VclTee.TeeGDIPOBJ.TeeGDIPlusShutdown (inside the thread) before and after I create the TChart. But that won’t help. Sometimes I don’t get an exception, but instead the image is partially rendered or corrupted.
I have also tried to use the conditional define "TEECANVASLOCKS".
If it’s impossible to use GDI+ in a non-main thread, what’s the best way to fall back on simple GDI?
I have tried putting TeeRenderClasses.Clear; in the Initialization section to get rid of TGDIPlusCanvas, and then add TeeRenderClasses.Add(TCanvas3D);
That seems to work for falling back on GDI, but it’s that the preferred way?
Anyway, this is part of my code, where I create the chart, prepare it, and then generate a PNG stream.
I need some guidance. Is it possible to use TChart with GDI+ like this?
I am trying to use TChart to generate this chart, and then use VclTee.TeePNG.TPNGExportFormat to generate the PNG stream.
The TChart is created in runtime from within a separate TThread (not the main thread).
The problem is that there are tons of problems related to this.
Even creating a TChart in a separate thread throws exception like "GDI+ error 3 out of memory" or "List index out of bounds" etc.
I imagine that lots of other developers before me must have tried using a TChart component to return a PNG stream in a response from TIdHTTPServer, where each request will run its own context thread?
How did they get TChart to work using GDI+ in a separate thread, or did they not?
I know a lot of people advise NOT to use GDI+ in non-main thread.
However, I am not trying to share the TChart object itself between threads, not even render it on the screen. It will only life for the purpose of generating a PNG stream, and then die. All this will be done in the same thread.
I have tried calling calling VclTee.TeeGDIPOBJ.TeeGDIPlusStartup and VclTee.TeeGDIPOBJ.TeeGDIPlusShutdown (inside the thread) before and after I create the TChart. But that won’t help. Sometimes I don’t get an exception, but instead the image is partially rendered or corrupted.
I have also tried to use the conditional define "TEECANVASLOCKS".
If it’s impossible to use GDI+ in a non-main thread, what’s the best way to fall back on simple GDI?
I have tried putting TeeRenderClasses.Clear; in the Initialization section to get rid of TGDIPlusCanvas, and then add TeeRenderClasses.Add(TCanvas3D);
That seems to work for falling back on GDI, but it’s that the preferred way?
Anyway, this is part of my code, where I create the chart, prepare it, and then generate a PNG stream.
Code: Select all
TMailReportBuilder = class(TThread)
// GenerateHourChart called inside the thread, and its job is to fill the aStream with a PNG stream of the chart.
procedure TMailReportBuilder.GenerateHourChart(aStream : TMemoryStream; aForDarkTheme : boolean);
var aChart: TCustomChart;
aExport : TPNGExportFormat;
aCoins : TBarSeries;
aCredits : TBarSeries;
aFreeRuns : TBarSeries;
lp0 : integer;
aBackColor : TColor;
begin
VclTee.TeeGDIPOBJ.TeeGDIPlusStartup; // <- Testing if this helps, it did not
aChart := TCustomChart.Create(nil); // GDI+ exceptions comes already here!!! Switching to GDI makes this code work fine, but its ugly.
aExport := TPNGExportFormat.Create;
try
aStream.Clear;
aChart.AutoRepaint := false; // We dont want updates while we setup and fill the chart
aChart.Width := 1000;
aChart.Height := 600;
aCoins := aChart.AddSeries(TBarSeries) as TBarSeries;
aCredits := aChart.AddSeries(TBarSeries) as TBarSeries;
aFreeRuns := aChart.AddSeries(TBarSeries) as TBarSeries;
if aForDarkTheme then begin
aBackColor := $000000;
end else begin
aBackColor := $FFFFFF;
end;
InitChart(aChart,aBackColor,aForDarkTheme);
InitBarSeries(aCoins,'Coins',$4040A0);
InitBarSeries(aCredits,'Credits',$A04040);
InitBarSeries(aFreeRuns,'Free Runs',$A0A0A0);
for lp0 := 0 to FReport.PeriodHourData.Count-1 do begin
aCoins.AddXY(FReport.PeriodHourData[lp0].StartTime,FReport.PeriodHourData[lp0].Coins);
aCredits.AddXY(FReport.PeriodHourData[lp0].StartTime,FReport.PeriodHourData[lp0].Credits);
aFreeRuns.AddXY(FReport.PeriodHourData[lp0].StartTime,-FReport.PeriodHourData[lp0].FreeRuns);
end;
// TEECANVASLOCKS
aChart.Canvas.ReferenceCanvas.Lock;
try
aExport.PixelFormat := TPixelFormat.pf24bit;
aExport.Panel := aChart;
aExport.Width := 1000;
aExport.Height := 600;
aExport.SaveToStream(aStream);
finally
aChart.Canvas.ReferenceCanvas.Unlock;
end;
finally
aExport.Free;
aChart.Free;
VclTee.TeeGDIPOBJ.TeeGDIPlusShutdown; // <- Testing if this helps, it did not
end;
end;