delphi - 如何正确实现在 Canvas 上绘制的线程?

标签 delphi

我想创建一个从TCustomControl派生的自定义控件,该控件将覆盖Paint方法并绘制诸如渐变背景,图形和形状等内容,然后在其上方绘制网格。

我知道所有这些都可能会很慢,因此要优化所有我想使用线程的想法,例如,一个线程绘制背景,一个线程绘制形状,一个线程绘制网格,但是我对此不太自信正确理解和实施所有内容。

通过反复试验并查看一些线程示例(尽管我永远找不到任何好的线程绘制示例),我设法提出了以下内容,这将是我的通用线程类:

type
  TCanvasThread = class(TThread)
  private
    FOnThreadPaint: TNotifyEvent;
    FCanvas: TCanvas;
  protected
    procedure Execute; override;
    procedure Sync;
  public
    constructor Create(Canvas: TCanvas; OnPaint: TNotifyEvent);
    destructor Destroy; override;

    property Canvas: TCanvas read FCanvas;
  end;

constructor TCanvasThread.Create(Canvas: TCanvas; OnPaint: TNotifyEvent);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FCanvas := Canvas;
  FOnThreadPaint := OnPaint;
end;

destructor TCanvasThread.Destroy;
begin
  inherited Destroy;
end;

procedure TCanvasThread.Execute;
begin
  if Assigned(FOnThreadPaint) then
    Synchronize(Sync);
end;

procedure TCanvasThread.Sync;
begin
  FOnThreadPaint(Self);
end;


以上是在自定义控件中实现的,如下所示:

type
  TMyControl = class(TCustomControl)
  private
    procedure OnClientPaint(Sender: TObject); // paint gradient
    procedure OnShapesPaint(Sender: TObject); // paint shapes etc
  protected
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

constructor TMyControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Width := 600;
  Height := 400;
end;

destructor TMyControl.Destroy;
begin
  inherited Destroy;
end;

procedure TMyControl.OnClientPaint(Sender: TObject);
begin
  GradientFillCanvas(TCanvasThread(Sender).Canvas, clSilver, clWhite, ClientRect, gdVertical);
end;

procedure TMyControl.OnShapesPaint(Sender: TObject);
begin
  TCanvasThread(Sender).Canvas.Rectangle(50, 50, 100, 100);
end;

procedure TMyControl.Paint;
begin
  TCanvasThread.Create(Canvas, OnClientPaint);
  TCanvasThread.Create(Canvas, OnShapesPaint);

  // implement other paint threads etc..
  // TCanvasThread.Create(Canvas, OnGridPaint);
  // ...

  // using regular canvas drawing here seems to be blocked too?
end;


通过上面的内容,我可以看到渐变绘制,可以看到绘制了白色矩形形状,但是在调整控件窗口的大小时(例如,与客户端对齐时)有闪烁的负载,我曾想过使用位图进行双重缓冲,但是如果可能更愿意只使用画布。我也无法再使用常规控件画布进行绘制,如TMyControl.Paint中的注释行突出显示的那样。

我是否在这里误解了一些基本知识,但都实施错误呢?我读过诸如关键部分和线程池之类的东西,但是有点让人不知所措。我尝试了Canvas.Lock和Canvas.UnLock,但是无论何时调整大小,所有内容都会闪烁,并且在Paint方法中创建线程后无法在常规画布上绘制。

所以我的问题是如何正确实现在画布上绘制的线程?代码是否全部错误,我需要重新启动并以正确的方式实现它?我现在真的迷失了方向,并感到相当困惑,我什至尝试将在Paint方法中创建线程的位置移到被拦截的WM_SIZE消息方法中,该方法确实减少了闪烁但未完全消除闪烁,我担心我可能在这里错过了一些更大的东西,因此,请您提供一些反馈和指导。

谢谢。

最佳答案

1)您不能以多线程方式使用VCL。它只是不是为此而设计的。普遍使用的多线程锁和锁的开销将是巨大的,其好处-对于99%的应用程序来说,是无法估量的无限。

2)而且您也不要以多线程方式使用Canvas。查看此代码:

procedure TCanvasThread.Execute;
begin
  if Assigned(FOnThreadPaint) then
    Synchronize(Sync);


这是什么意思?这意味着“我不想执行多线程,我想在单个Main VCL线程中运行所有工作”。

Synchronize调用有什么作用?阅读文档,Synchronize的基本形式表示“暂时停止该线程并以单线程方式完成其工作”。现在,如果您的所有后台工作人员线程都在执行“阻止我,并在单个线程中完成我的工作”,那么这就是它的意思。您创建线程只是为了立即停止它们,但是所有工作将被传输到单个Main VCL Thread。您只分配了很多不使用的资源。您制作了单线程应用程序,但负担很重,只能停止创建额外的线程。您还取消了可预测性,并创建了-google这个词! -比赛条件。现在,您有几个“伪后台线程”,但您永远无法分辨出哪个先运行,然后再运行。

那有什么选择呢?

3)首先,仅在没有其他选择时才使用多线程。并且当您有100%孤立的任务块时,而不是单个共享变量。

procedure TMyControl.Paint;
begin
  TCanvasThread.Create(Canvas, OnClientPaint);
  TCanvasThread.Create(Canvas, OnShapesPaint);


违反规则,您将一个和相同的Canvas变量放入两个线程中。你不应该。如果您不能将线程分开以使其完全隔离-那么您很可能没有一项具有多重可读性的任务。

太棒了,我太严格了,有些任务可以共享一些小变量,前提是必须对它们进行任何访问都是明智的,所以永远不会有2个以上的线程立即执行。但是对于任何新手来说,经验法则就像我说的那样:100%隔离或没有多线程,甚至没有99%。

因此,通常您可能希望使用单个Canvas,这意味着您只能有一个线程来做。哎呀尝试使用一些更快的画布而不是标准的VCL画布。可能像http://graphics32.org。在TCanvasDirect 2D上还有GDI+实现-我不知道它们是否更快。或其他来自http://torry.net/和类似目录的2D图形库。

总而言之,在尝试制作缓慢的多线程应用程序之前,请花时间和精力来制作快速的单线程应用程序。

4)有时,您确实可以将图片分成多个图层,例如Photoshop图层。这次您可以希望对其进行多线程处理。您创建了几个不同的位图,每个线程一个。您用透明色填充它们。然后,使线程将所需的部分绘制到自己的位图中。然后在主线程中,看到所有线程何时完成工作,然后在单个主线程中,将许多透明位图一个接一个地融合在目标表单的TPainBox画布上,并以正确的顺序进行。
但是即使那样,您最好还是放弃它们的库存TCanvasTBitmap并使用速度更快的库。如果没有其他问题,我从来没有可靠,快速地处理带有透明图像的普通VCL TBitmap,它们并不是为实现真正的透明而设计的。它以一些意想不到的缺点和反复出现的故障表现出来。

5)关于这些线程,除了种族,您没有任何权利在WM_PAINT事件之外的GDI窗口上绘画,或者按照VCL的条款,您只是在绘画表单时违反了合同(或(TWinControl方法之外的任何Paint)(或在基本OnPaint方法内调用的Paint处理程序)。这只是违反MS Windows法律。您可以将一些数据缓存填充,计算或下载一些不可见数据偏移到后台线程中。甚至在极端情况下,甚至可以将该数据渲染到那些垄断的每线程一个临时位图中。但是只能在Paint / OnPaint内严格完成渲染表单本身,在其画布上绘画的操作,并且不能将其卸载到Paint方法退出后运行的任何实体中。渲染的控制流应全部放在Paint内部,而不能在外部。因此,线程在这里不适用:在Paint方法外部执行时,它们没有触摸表单画布的合法权利。您必须阅读有关MS Windows GDI窗口和消息的一些教程,以及无效增加循环的工作方式。

6)最后,找到OmniThreadingLibrary并阅读所有有关它的教程和说明。您必须得到一个简单的想法-多线程总是昂贵的(如果按每个处理器计算,总效率要比单线程程序低),并且只能将程序的一部分提取到多线程中,而不是整个程序,并且任何作品中只有100%孤立的部分才是真正可以理解的。无论您做什么工作,彼此之间没有任何关系的部分都不会获得100%多线程。
换句话说,请阅读尽可能多的OTL教程和FAQ,以了解简单的想法:您一生中大部分时间都不想使用多线程。多线程是规范的一个例外,仅在某些特定情况下才值得。如果您不确定是否需要多线程-那么就需要单线程。只有在正常和合法手段无效的情况下,才将多线程作为最后的机会。那是在开玩笑,但只有一半。

关于delphi - 如何正确实现在 Canvas 上绘制的线程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44161348/

相关文章:

delphi - Firemonkey 中的 BringToFront 问题

delphi - TidHTTP 错误处理

windows - 如何从字体文件中获取字体名称?

delphi - GlScene 鼠标指向 TGLHeightField

multithreading - DBX错误: Driver could not be properly initialized when use OmniThreadLibrary (but ok otherwise)

delphi - 如何在操作中从 TShortCut 获取快捷方式字符串?

Delphi 7 类助手的先见之明

multithreading - Delphi:Form的线程=“Canvas does not allow drawing”中的ZipForge被卡住

python - 在delphi XE2中访问DLL,从Python转换

delphi - 是否有类似StrToCurr的函数可以处理数千个分隔符?