views:

405

answers:

3

Normally using GDI+ in Delphi you can use a TPaintBox, and paint during the OnPaint event:

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
   g: TGPGraphics;
begin
   g := TGPGraphics.Create(PaintBox1.Canvas.Handle);
   try
      g.DrawImage(FSomeImage, 0, 0);
   finally
      g.Free;
   end;
end;

The problem with this paradigm is that creating a destroying a Graphics object each time is wasteful and poorly performing. Additionally, there are a few constructs availabe in GDI+ you can only use when you have a persistent Graphics object.

The problem, of course, is when can i create that Graphics object? i need to know when the handle becomes available, and then when it is no longer valid. i need this information so i can create and destroy my Graphics object.


Solution Attempt Nº1

i can solve the creation problem by creating it when it is really needed - on the first time the paint cycle is called:

procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
   if FGraphics = nil then
      FGraphics := TGPGraphics.Create(PaintBox1.Canvas.Handle);

   FGraphics.DrawImage(FSomeImage, 0, 0);
end;

But i have to know when the device context is no longer valid, so i can destroy my FGraphcis object, so that it is re-created the next time it's needed. If for some reason the TPaintBox's device context gets recreated, i would be drawing on an invalid device context the next time OnPaint is called.

What is the intended mechanism in Delphi for me to know when the device context handle of a TPaintBox is created, destroyed, or re-created?

+3  A: 

You can't with the standard TPaintBox because the TPaintBox has a Canvas of type TControlCanvas, for which members relevant to this issue are these:

TControlCanvas = class(TCanvas)
private
  ...
  procedure SetControl(AControl: TControl);
protected
  procedure CreateHandle; override;
public
  procedure FreeHandle;
  ...
  property Control: TControl read FControl write SetControl;
end;

The problem is that FreeHandle and SetControl are not virtual.

But: the TControlCanvas is created and assigned here:

 constructor TGraphicControl.Create(AOwner: TComponent);
 begin
   inherited Create(AOwner);
   FCanvas := TControlCanvas.Create;
   TControlCanvas(FCanvas).Control := Self;
 end;

So what you could do is create a descending TMyControlCanvas that does have virtual methods, and a TMyPaintBox that assigns the Canvas like this:

 constructor TMyPaintBox.Create(AOwner: TComponent);
 begin
   inherited Create(AOwner);
   FCanvas.Free;
   FCanvas := TMyControlCanvas.Create;
   TMyControlCanvas(FCanvas).Control := Self;
 end;

Then you can use the methods in TMyControlCanvas to dynamically create and destroy your TGPGraphics.

That should get you going.

--jeroen

Jeroen Pluimers
Are there issues resizing the control? Do you know if you need to recreate the GDI+ context then, or in any other circumstances?
David M
Just because you add virtual methods to TMyControlCanvas doesn't mean anything in the VCL is going to call them.
Rob Kennedy
@Rob Actually, those methods are only called from within TControlCanvas, so if you make your own TControlCanvas it is going to work.
Jeroen Pluimers
FreeHandle is called from the standalone FreeDeviceContext function.
Rob Kennedy
@Rob: that's why TMyPaintBox now uses the TMyControlCanvas (which might start as a blunt copy of TControlCanvas). That should make the VCL call the methods. Having virtual methods allows you to derive another class from TMyControlCanvas one doing more specific tasks (I'm more of the 'most stuff should be virtual' camp, but I can understand why people are in the 'limit the number of virtual methods' camp).
Jeroen Pluimers
@David: I'm not sure; sorry :-)
Jeroen Pluimers
+1  A: 

Detecting creation is easy. Just override CreateHandle in a descendant TControlCanvas and put yours in place of the default one as Jeroen's answer demonstrates. Detecting destruction is harder.

One way to avoid the issue is to check whether the TGpGraphics handle is equal to the paint-box's handle, so, rather than detect the moment when the device context is freed, you simply check before you need to know.

if not Assigned(FGraphics)
    or (FGraphics.GetHDC <> PaintBox1.Canvas.Handle) then begin
  FGraphics.Free;
  FGraphics := TGpGraphics.Create(PaintBox1.Canvas.Handle);
end;

This probably isn't reliable, though; handle values are liable to be re-used, so although the HDC value might be the same between two checks, there's no guarantee that it still refers to the same OS device-context object.


The TCanvas base class never clears its own Handle property, so anything that invalidates the canvas must occur externally. TControlCanvas clears its Handle property when its Control property gets re-assigned, but that usually only happens when the control is created since TControlCanvas instances are rarely shared. However, TControlCanvas instances work from a pool of device-context handles kept in CanvasList. Whenever one of them needs a DC (in TControlCanvas.CreateHandle), it calls FreeDeviceContext to make room in the canvas cache for the handle it's about to create. That function calls the (non-virtual) FreeHandle method. The cache size is 4 (see CanvasListCacheSize), so if you have several descendants of TCustomControl or TGraphicControl in your program, chances are high that you'll get cache misses whenever more than four of them need to be repainted at once.

TControlCanvas.FreeHandle is not virtual, and it doesn't call any virtual methods. Although you could make a descendant of that class and give it virtual methods, the rest of the VCL is going to continue calling the non-virtual methods, oblivious to any of your additions.


Instead of trying to detect when a device context is released, you might be better off using a different TGpGraphics constructor. Use the one that takes a window handle instead of a DC handle, for instance. Window-handle destruction is much easier to detect. For a one-off solution, assign your own method to the TPaintBox.WindowProc property and watch for wm_Destroy messages. If you're doing this often, then make a descendant class and override DestroyWnd.

Rob Kennedy
A: 

The performance hit you take for creating/destroying the graphics object is minimal. It's far outweighed by the performance hit of using gdi+'s drawing commands in the first place. Neither of which, imo, are worth worrying about when it comes to drawing user interfaces because the user wont notice anyways. And frankly, it can be very inconvenient to try to carry around a graphics object and track changes to the DC handle (especially if you're encapsulating graphics routines inside your own set of classes).

If you need to cache bitmaps, what you may consider doing is creating the bitmap you want to cache with GDI+ (make it the right size & w/ whatever antialias settings you want), saving it to a tmemorystream, and then when you need it, load it from a stream and draw it using good ol' bitblt. It'll be much, much faster than using Graphics.DrawImage. I'm talking orders of magnitude faster.

GrandmasterB
BitBlt doesn't support transparency. Which doesn't matter so much since Image.GetHBITMAP doesn't return support it either.
Ian Boyd
Then use alphablend(). The point is, direct GDI is much faster than using GDI+. Create the bmp with GDI+, cache it in a stream (as a png so that transparency is tracked), and draw it with Alphablend instead of gdi+. it'll be 20 times faster. Just make sure you create it with the proper dimensions you need since AlphaBlend()/BitBlt() dont resample as nicely as the GDI+ functions do.
GrandmasterB
Any btw, the performance hit of creating/destroying the Graphics object is not minimal. And GDI doesn't support all the drawing operations that GDI+ does. Someone is free to come up with their own software based drawing library that internally uses GDI when it can. Call it GDI++.
Ian Boyd