如何正确实现在画布上绘制的线程?

时间:2017-05-24 14:28:36

标签: 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 个答案:

答案 0 :(得分:11)

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

2)无论如何,你不要以多线程的方式使用你的Canvas。看到你的代码:

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

这是什么意思?这完全意味着"我不想做多线程,我想在单个主VCL线程中运行我的所有工作"。

Synchronize调用了什么?阅读文档,Synchronize的基本形式意味着"暂时停止此线程,并以单线程方式完成工作"。现在,如果所有和每个后台工作线程都在做什么"停止我并在单线程中完成我的工作" - 那就是它所说的。您只创建线程以立即停止它们,但所有工作都将传输到单个主VCL线程。你只分配了许多你不使用的资源。您创建了单线程应用程序,其中包含许多已创建的负载,仅用于停止的额外线程。你额外杀死了可预测性并创造了 - 谷歌这个词! - 竞争条件。你现在有几个"伪背景线程"但你永远不知道哪一个会先工作,哪个会先工作。

那么有什么选择呢?

3)首先,只有在没有其他选项的情况下才进行多线程处理。当你有大量的任务被100%隔离,而不是一个共享变量。

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

违反了规则,你获取一个相同的Canvas变量并将其放入两个线程中。你不应该。如果你不能将线程分开以完全隔离 - 那么你很可能没有一个多线程的任务。

Oookay,我太严谨了,有些任务可以共享一些小数量的变量,前提是任何对它们的访问都是明亮的仲裁,所以永远不会有2个以上的线程同时进行。但对于任何新手来说,拇指规则就像我说的那样:100%隔离或没有多线程,甚至不是99%。

所以,通常你可以想要使用单个Canvas,这意味着你只能有一个线程来做它。 Oookay。尝试使用一些更快的画布而不是标准的VCL画布。可能像http://graphics32.org一样。 TCanvasDirect 2D上还有GDI+次实施 - 我不知道它们是否更快。或者来自http://torry.net/的其他一些2D图形库和类似的目录。

总而言之 - 在尝试制作慢速多线程应用程序之前 - 将时间和精力投入到快速单线程应用程序中。

4)有时你真的可以将你的图片分成几层,就像Photoshop图层一样。这次你可以希望多线程。您创建SEVERAL不同的位图,每个线程一个。你用透明颜色填充它们。然后你让你的线程将他们需要的部分绘制到他们自己的位图中。然后在主线程中你看到所有线程完成它们的工作,然后在单个主线程中你将那些许多透明位图一个接一个地融合到目标形式的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教程和常见问题解答来理解这个简单的想法:你不想在你的大部分时间里多线程。多线程是一种例外,只有在某些特定情况下才值得。当您怀疑是否需要多线程时 - 那么您需要单线程。当没有正常和合法的意思起作用时,你只会将多线程作为最后的机会。那是半开玩笑,但只有一半。