Page 1 of 1

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
by 16587794
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.

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;
I need some guidance. Is it possible to use TChart with GDI+ like this?

Re: How do create a working instance of TChart using GDI+ in runtime, in a non main thread

Posted: Wed Mar 25, 2020 1:58 pm
by Marc
Hello,

Running some tests on this without conclusive results so far. Tests here on a thread (class(TThread)) return inconsistent results (with failures to render to an image file) on x occasions but no failures in execution. Not happy that I have results to report yet so please consider this in-progress.

If you have any more feedback that may be useful, please let us know.

With thanks.
Regards,
Marc Meumann

Re: How do create a working instance of TChart using GDI+ in runtime, in a non main thread

Posted: Thu Apr 02, 2020 12:19 pm
by 16587794
Any updates on this Marc?

Re: How do create a working instance of TChart using GDI+ in runtime, in a non main thread

Posted: Tue May 26, 2020 10:05 pm
by 9236183
To assign a GDI canvas in C++ its :-
graph_->Canvas = new TTeeCanvas3D;

So probably something like in Pascal (just going by memory) :-
graph_.Canvas := TTeeCanvas3D.Create();