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

Posted

技术标签:

【中文标题】Delphi TCanvas 对象从 dll 使用后损坏,如何恢复? [关闭]【英文标题】:Delphi TCanvas object become corrupted after using from dll, how to restore? [closed] 【发布时间】:2016-03-30 23:23:16 【问题描述】:

有一些问题。我有一个带有画布的表单,我需要通过它的句柄从 dll 访问这个画布。我是这样做的:

来自 dll

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

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

有没有办法重置/重新初始化表单的 TCanvas 对象?

【问题讨论】:

canvas.handle := 句柄; // 这里的句柄表示form.canvas.handle传递给dll 您需要提供更多详细信息。在模块之间传递对象听起来很狡猾。或者你画的时间不对。 minimal reproducible example 怎么样。那么没有人需要猜测。 【参考方案1】:

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

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

【讨论】:

我忘了SaveDC() 非常感谢,看来它解决了我的问题。【参考方案2】:

Form 的Canvas 被“损坏”的原因是DLL 的TCanvas 对象正在替换已经分配给HDC 的原始HFONTHBRUSH 和/或HPEN 对象,但随后在其销毁期间分配 stock GDI 对象(来自GetStockObject()),而不是重新分配先前分配的 original 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 函数退出后使表单“重置”其Canvas,您必须欺骗Canvas 知道其 GDI 对象不再分配给 HDC因此它可以从其内部State 成员中清除相关标志,并根据需要重新分配其 GDI 对象。您可以:

    手动触发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;
    

    您可以暂时删除然后重新分配原来的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;
    

    使用SaveDC()RestoreDC(),如Sertac's answer所示。

【讨论】:

请注意,您不需要 dll 来以这种方式丢失 gdi 对象,可执行文件中的辅助画布也会发生这种情况。 感谢您的详细解释,我使用了 SaveDC/RestoreDC,效果很好。【参考方案3】:

标准的 TCanvas 类并不适合在“借来的”画布上绘画。也就是说,由于它管理 GDI 对象的方式(依赖于“拥有”HDC 和正在使用的 DC 中的 GDI 对象)。

在简单的情况下它可以工作,但您遇到的问题并不少见。特别是对于 DLL,可能会出现问题,因为 TCanvas 中的机制依赖于需要的“全局”画布列表 (CanvasList)进行管理并保持同步以响应系统变化。

即在 DLL 中会有一个 CanvasList,它是 DLL 中的画布列表,与宿主应用程序进程中的 CanvasList 分开。应用程序 CanvasList 不会在 DLL 中包含任何 TCanvas 实例,反之亦然。如果一个 DLL 有一个 TCanvas,它实际上是应用程序中一个 TCanvas 的“副本”(使用相同的 HDC),那么问题是如何出现的应该是显而易见的。

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

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

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

另一种可能是使用SaveDC()RestoreDC(),如Sertac's answer所示。

【讨论】:

虽然这是真的,但还有一个更重要的罪魁祸首——当分配了TCanvas.Handle(包括销毁期间),并且已经分配了HDCTCanvas 分配了 stock 来自GetStockObject() 的 GDI 对象,而不是恢复它可能已替换的任何 原始 GDI 对象(字体、钢笔、画笔)。 TCanvas 不需要记住和恢复原始 GDI 对象,这是使用 SelectObject() 时的一般要求。 是的 - 因此“不适合在'借来的'画布上绘画”和“依赖于'拥有' HDC 和 GDI 对象)。但即使你解决了这个问题,全局 CanvasList 是否仍然存在也是一个问题吗?它可能会导致调用 DeselectHandles 并选择 StockObjects 以响应应用程序中的 WM_SYSCOLORCHANE(但不是 DLL)。在那个(可能不太可能但仍然可能)情况下,这两个 TCanvas 是不是又不“同步”了?这是否取决于调用 DLL 绘制的方式和时间? 如果 DLL 绘图包含在一个阻塞函数中,并且该函数在应用程序的主 UI 线程的上下文中调用,则没有问题,因为应用程序不会在 DLL 运行时处理系统更新繁忙的绘图(DLL 本身根本不会处理系统更新,因为它没有自己的消息循环)。应用程序和 DLL 是否具有单独的画布列表无关紧要。 啊,是的,当然。我不确定这是否仍然是一个问题,但(多亏了鼻窦问题)目前所有的气瓶上的灰质都没有燃烧,我无法思考整个事情,只是有一种微不足道的感觉。但是你说的很有道理。琐事消失了。谢谢。 :)

以上是关于Delphi TCanvas 对象从 dll 使用后损坏,如何恢复? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

Delphi 绘图[2] 无Canvas属性,获取 Canvas 对象

如何更改 TCanvas (delphi) 的 textOut 的颜色?

Delphi 绘图TCanvas类[3] TPen类参数及介绍

Delphi控件继承类

Delphi 绘图[1] TCanvas(画布)的类成员 及参数介绍

Delphi中如何从导入的DLL中初始化一个对象?