Delphi 从dll使用后TCanvas对象损坏,如何恢复?

Delphi TCanvas object become corrupted after using from dll, how to restore?

有问题。我有一个带有 canvas 的表单,我需要通过其句柄从 dll 访问此 canvas。我是这样做的:

来自 dll

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

效果很好,我从dll中绘制了我需要的东西。但是这个技巧有副作用。从 dll 绘制后,表单会丢失字体设置(顺便说一句,从 dll 绘制时我没有使用字体,只有几个矩形),当我从主表单绘制相同的 canvas 时,即使我直接 canvas.font.size := ...; canvas.font.name := ...;在canvas.TextOut之前,字体没有变化。线条、填充等绘画都可以。但是字体会损坏(有时不会,但大多数情况下)。

有没有办法reset/reinit窗体的TCanvas对象?

标准 TCanvas class 不太适合在 "borrowed" canvasses 上绘画。也就是说,由于它管理 GDI 对象的方式(依赖于 "owning" HDC 和它正在使用的那个 DC 中的 GDI 对象的状态。

它可以工作,在简单的情况下,但除此之外,您遇到的问题并不少见。特别是对于 DLL,可能会出现问题,因为 TCanvas 中的机制依赖于 canvases 的 "global" 列表(CanvasList) 需要管理并保持同步以响应系统更改。

即在 DLL 中会有一个 CanvasList,它是 DLL 中 canvases 的列表,与主机应用程序中的 CanvasList 分开过程。应用程序 CanvasList 将不会在 DLL 中包含任何 TCanvas 实例,反之亦然。如果 DLL 有一个 TCanvas,它实际上是应用程序中 TCanvas 的 "duplicate"(使用相同的 HDC),那么它应该很明显会出现问题。

我看到你的情况有两种前进方式,可以单独使用,也可以一起使用。

  1. 您没有提供所有绘画代码的详细信息,因此很难说哪个可能是问题的根源。但是,您可以通过注释掉绘画代码的 all(在 tryfinally 在你的绘画程序中)。这应该可以解决您的字体问题。如果您随后以增量方式(逐行或逐节)重新启用绘画代码,您可以准确地确定导致问题的绘画操作,并从那里(可能)确定解决方案。

  2. 如果你的绘画操作非常简单(就像你说的只是画几个矩形)那么你可以使用简单的 GDI 调用来在问题情况下(或所有情况下)进行绘画,而不是比使用 canvas。在这种情况下,我建议您将 window handle 传递给您的 DLL,而不是设备上下文。然后,您的 DLL 应该通过 GetDC() 获取它自己的设备上下文,并在完成后通过 ReleaseDC() 释放它。当您自己在设备上下文上绘制时,您将需要管理 GDI 对象,但可以确保无论您做什么,都不会干扰由 TCanvas 在设备上绘制的 GDI 对象相同 window.

另一种可能是使用SaveDC()RestoreDC(),如.

您的表单 Canvas 获得 "corrupted" 的原因是因为 DLL 的 TCanvas 对象正在替换原始的 HFONTHBRUSH and/or HPEN 对象已分配给 HDC,但随后在其销毁期间分配 stock GDI 对象(来自 GetStockObject()),而不是重新分配- 分配先前分配的原始 GDI 对象。当 TCanvas.Handle 属性 更改值(包括在销毁期间)时,这发生在 TCanvas.DeselectHandles() 方法中:

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);
  ...

要在 DLL 函数退出后使表单 "reset" 成为其 Canvas,您将不得不 欺骗 Canvas 以了解其GDI 对象不再分配给 HDC 因此它可以从其内部 State 成员中清除相关标志并根据需要重新分配其 GDI 对象。您可以:

  1. 手动触发 Canvas.FontCanvas.BrushCanvas.Pen 属性的 OnChange 事件处理程序:

    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;
    

    或:

    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. 你可以暂时移除然后重新分配原来的HDC,这对State标志有类似的效果:

    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. 使用SaveDC()RestoreDC(),如图.

Canvas 没有任何重置功能,但您可以要求 api 保存 canvas 的设备上下文状态,并在绘制后恢复它。

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;


解释了如何丢失设备上下文的状态。为什么它并不总是发生应该取决于我相信的时间。如果表单在 canvas 使用其字体时进入了新的绘制周期,那么一切都应该很好,因为它在新获取和设置的设备上下文上运行。