3

Have some problem. I have a form with a canvas, I need access to this canvas from dll by its handle. I do this in this way:

from dll

canvas := TCanvas.Create;
  try
    canvas.Handle := handle;
    // do some painting on this canvas
  finally
    canvas.free;
  end;

It works well, I paint what I need from dll. But this trick has side effect. After painting from dll, form loses font settings (btw I did not use fonts when painted from dll, just few rects) and when I paint on same canvas from main form, even if I do directly canvas.font.size := ...; canvas.font.name := ...; before canvas.TextOut, the font does not change. Lines, filling and other paintings are ok. But fonts become corrupted (sometimes not, but mostly).

Is there a way to reset/reinit TCanvas object of the form?

Mike K.
  • 145
  • 10
  • canvas.handle := handle; // handle here means form.canvas.handle passed to dll – Mike K. Mar 30 '16 at 23:25
  • 1
    You'll want to provide more details. Passing objects between modules sounds dodgy. Or perhaps you are painting at the wrong time. How about a [mcve]. Then nobody has to guess. – David Heffernan Mar 30 '16 at 23:32

3 Answers3

3

The reason your Form's Canvas gets "corrupted" is because the DLL's TCanvas object is replacing the original HFONT, HBRUSH and/or HPEN objects that were already assigned to the HDC, but is then assigning stock GDI objects (from GetStockObject()) during its destruction, instead of re-assigning the original GDI objects that were previously assigned. This happens in the TCanvas.DeselectHandles() method when the TCanvas.Handle property changes value (which includes during destruction):

var
  ...
  StockPen: HPEN;
  StockBrush: HBRUSH;
  StockFont: HFONT;
  ...

procedure TCanvas.DeselectHandles;
begin
  if (FHandle <> 0) and (State - [csPenValid, csBrushValid, csFontValid] <> State) then
  begin
    SelectObject(FHandle, StockPen);   // <-- STOCK PEN!
    SelectObject(FHandle, StockBrush); // <-- STOCK BRUSH!
    SelectObject(FHandle, StockFont);  // <-- STOCK FONT!
    State := State - [csPenValid, csBrushValid, csFontValid];
  end;
end;

...
initialization
  ...
  StockPen := GetStockObject(BLACK_PEN);
  StockBrush := GetStockObject(HOLLOW_BRUSH);
  StockFont := GetStockObject(SYSTEM_FONT);
  ...

To make the Form "reset" its Canvas after the DLL function exits, you will have to trick the Canvas into knowing its GDI objects are not longer assigned to the HDC so it can clear the relevant flags from its internal State member and reassign its GDI objects as needed. You can either:

  1. manually trigger the OnChange event handlers of the Canvas.Font, Canvas.Brush and Canvas.Pen properties:

    procedure TMyForm.FormPaint(Sender: TObject);
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        Canvas.Font.OnChange(nil);
        Canvas.Brush.OnChange(nil);
        Canvas.Pen.OnChange(nil);
      end;
    end;
    

    Or:

    type
      TGraphicObjectAccess = class(TGraphicObject)
      end;
    
    procedure TMyForm.FormPaint(Sender: TObject);
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        TGraphicObjectAccess(Canvas.Font).Changed;
        TGraphicObjectAccess(Canvas.Brush).Changed;
        TGraphicObjectAccess(Canvas.Pen).Changed;
      end;
    end;
    
  2. you can temporarily remove and then re-assign the original HDC, which has a similar effect on the State flags:

    procedure TMyForm.FormPaint(Sender: TObject);
    var
      DC: HDC;
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        DC := Canvas.Handle;
        Canvas.Handle := 0;
        Canvas.Handle := DC;
      end;
    end;
    
  3. Use SaveDC() and RestoreDC(), as shown in Sertac's answer.

Community
  • 1
  • 1
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
3

Canvas does not have any reset functionality but you can ask the api to save the state of the device context of the canvas, and restore it after your drawing.

var
  SavedDC: Integer;

  ...
  SavedDC := SaveDC(handle);
  try
    canvas := TCanvas.Create;
    try
      canvas.Handle := handle;
      // do some painting on this canvas
    finally
      canvas.free;
    end;
  finally
    RestoreDC(handle, SavedDC);
  end;


Remy's answer explains how you lose the sate of the device context. Why it doesn't always happen should depend on timing I believe. If the form has entered a new paint cycle at the time its canvas uses its font, all should be well since it operates on a newly acquired and setup device context.

Community
  • 1
  • 1
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
2

The standard TCanvas class is not really suited for painting on "borrowed" canvasses. That is, taking a device context (e.g. from some other canvas object) and using it within another, separate TCanvas due to the way it manages GDI objects (relying on "owning" the HDC and the state of GDI objects in that DC that it is working with).

It can work, in simple cases, but otherwise the problems you are experiencing are not uncommon. In particular with a DLL, there could be problems arising from the fact that there are mechanisms within a TCanvas that rely on a "global" list of canvases (CanvasList) that need to be managed and kept in sync in response to system changes.

i.e. In a DLL there will be a CanvasList which is list of canvases in the DLL, separate to the CanvasList in the host application process. The application CanvasList will not include any TCanvas instances in the DLL, and vice versa. If a DLL has a TCanvas which is in fact a "duplicate" of a TCanvas in the application (using the same HDC) then it should be obvious how problems could arise.

I see two ways forward in your case which could be used separately or together.

  1. You haven't provided details of all your painting code so it's difficult to say which is likely to be the source of your problem. However, you can identify this yourself quite easily by commenting out all of your painting code (between the try and finally in your painting routine). This should fix your font problem. If you then re-enable your painting code incrementally (line by line or section by section) you can identify precisely which painting operations are causing the problem and from there (potentially) identify a solution.

  2. If your painting operations are very simple (just painting a few rectangles as you say) then you could use simple GDI calls to do your painting in the problem cases (or all of them), rather than using a canvas. In this case I would suggest that you pass a window handle to your DLL, rather than a device context. Your DLL should then obtain it's own device context via GetDC() and release it via ReleaseDC() when finished. You will need to manage the GDI objects when painting on the device context yourself but can then be sure that whatever you do you are not interfering with the GDI objects being managed by a TCanvas drawing on the same window.

Another possibility is to use SaveDC() and RestoreDC(), as shown in Sertac's answer.

Community
  • 1
  • 1
Deltics
  • 22,162
  • 2
  • 42
  • 70
  • While this is true, there is a more important culprit - when `TCanvas.Handle` is assigned (which includes during destruction), and an `HDC` is already assigned, `TCanvas` assigns **stock** GDI objects from `GetStockObject()` instead of restoring any **original** GDI objects it may have replaced (font, pen, brush). `TCanvas` does not bother to remember and restore original GDI objects, which is a general requirement when using `SelectObject()`. – Remy Lebeau Mar 31 '16 at 01:13
  • Yep - Hence "not suited for painting on 'borrowed' canvases" and "relying on 'owning' the HDC and GDI objects). But even if you solve that problem, would the global CanvasList not still be an issue as well ? It can result in DeselectHandles being called and the StockObjects being selected in in response to a **WM_SYSCOLORCHANGE** in the application (but not the DLL). In that (possibly unlikely but still possible) case, aren't the two *TCanvas*'s out of 'sync' again ? Could it depend on exactly how the DLL painting is invoked and when ? – Deltics Mar 31 '16 at 02:00
  • If the DLL drawing is contained in a blocking function, and that function is called in the context of the app's main UI thread, there is no problem since the app will not process system updates while the DLL is busy drawing (the DLL itself won't process system updates at all, since it does not have its own message loop). It does not matter if the app and DLL have separate canvas lists. – Remy Lebeau Mar 31 '16 at 02:04
  • Ah yes, of course. I wasn't sure it was still a problem but (thanks to a sinus problem) the ol' grey matter isn't firing on all cylinders at the moment and I couldn't think the whole thing thru, just had a niggly feeling. But what you say makes perfect sense. The niggle is gone. Thanks. :) – Deltics Mar 31 '16 at 19:41