【问题标题】:How to correctly implement threads for drawing on a canvas?如何正确实现在画布上绘图的线程?
【发布时间】:2017-05-24 14:28:35
【问题描述】:

我想创建一个从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 消息方法,这确实在一定程度上减少了闪烁,但并没有完全减少,我担心我可能在这里遗漏了一些更大的东西,因此请提供一些反馈和指导。

谢谢。

【问题讨论】:

  • 如果准备图形内容需要很长时间,请在线程中准备。但是当这样的准备完成后,将图形结果“传输”到可视化控件并保存在那里。最后,每当控件请求渲染(Paint 方法)时,绘制存储的图形。
  • @VictoriaMarotoSilva 这就是TForm.DoubleBuffered := true 所做的事情:-)(除了明显缺乏对鼠标点击和其他用户行为事件的即时行为类型反应)
  • @Arioch'The,有点像 :) 这是关于自定义控件的。我在地图控件上进行了合作,这是在视觉控制中实际使用线程的一个很好的例子。您正在下载线程中的磁贴,将它们“传递”到控件以在它们“在家”时呈现。
  • @VictoriaMarotoSilva 瓷砖不重叠,因此您不会受到内存消耗和排序问题的影响。但是尝试制作“分层”地图,是否每个线程都渲染不是瓷砖而是所有地图,只是不同类别的对象。你会看到事情是如何变化的:-D
  • @Arioch'The,它是分层的。路由和自定义数据。我们在那里实现了相当复杂的逻辑(优先级)。备份 SQLite DB 中最常用的切片,不可见周围切片的内存缓存。瓷砖渲染和拖动以及其他元素都是动画的。但原则是所有“远程”数据源都是相同的——它们由工作线程获取并分派到控件,在那里它们被缓存以在控件请求时呈现。我们计划今年将此控件作为开源版本发布。

标签: delphi


【解决方案1】:

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

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

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

这是什么意思?它的确切意思是“我不想做多线程,我想在单个主 VCL 线程中运行我的所有工作”。

Synchronize 呼叫有什么作用?阅读文档,Synchronize 的基本形式意味着“暂时停止该线程并以单线程方式完成其工作”。现在,如果您的所有后台工作线程都在“阻止我并在单线程中完成我的工作” - 那么这就是它所说的。您创建线程只是为了立即停止它们,但所有工作都被传输到单个 Main VCL 线程。你只分配了很多你不使用的资源。您制作了具有大量负担的单线程应用程序,该应用程序只创建了要停止的额外线程。您还杀死了可预测性并创建了 - 谷歌这个词! - 比赛条件。您现在有几个“伪后台线程”,但您永远无法判断它们中的哪一个会首先工作,哪些会第二个工作。

那么有哪些选择呢?

3) 首先,只有在没有其他选择的情况下才使用多线程。而且,当您拥有 100% 隔离的任务块时,而不是单个共享变量。

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

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

Oookay,我太严谨了,有些任务可以共享少量变量,前提是对它们的任何访问都是经过仲裁的,因此永远不会有 2 个以上的线程同时执行。但是对于任何新手来说,经验法则就像我说的:100% 隔离或没有多线程,甚至 99% 都没有。

因此,通常您可以使用单个 Canvas,这意味着您只能有一个线程来执行此操作。好吧。尝试使用一些更快的画布而不是标准的 VCL 画布。也许像http://graphics32.org。在Direct 2DGDI+ 上还有TCanvas 实现——我不知道它们是否更快。或来自http://torry.net/ 和类似目录的其他一些二维图形库。

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

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

5)这些线程的另一件事,除了比赛,你没有任何权利在WM_PAINT事件之外的GDI窗口上绘画,或者在VCL条款中,你在绘画时违反了合同在其Paint 方法(或在基本Paint 方法中调用的OnPaint 处理程序)之外形成(或任何TWinControl)。这只是违反了 MS Windows 法律。您可以将一些数据缓存填充、计算或下载一些不可见的数据偏移到后台线程中。也许在极端情况下,甚至将该数据呈现为那些垄断的每线程一个临时位图。但是渲染表单本身,在其画布上绘画 - 只能在 Paint/OnPaint 内完成,并且不能卸载到 Paint 方法退出后运行的任何实体。渲染的控制流应该都在Paint 之内,绝不能在外面。因此,线程在这里不适用:在Paint 方法之外执行,它们没有合法的权利来触摸表单的画布。您必须阅读一些关于 MS Windows GDI 窗口和消息的教程,以及失效-重新创建周期如何工作。

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

【讨论】:

  • 这是一个非常棒的答案,你在哪里找到时间做的? ?
  • @Fritzw,您是如何将 ? 纳入您的评论的?我是新来的,但在这里没有找到关于支持笑脸的任何帮助。对不起,这个超出主题的评论:)
  • Unicode 包含笑脸字母。不过,并非每种字体都涵盖 100% 的 Unicode。在 Android 手机上,它们是“缺少字形”的字母。运行 CharMap.exe,按范围将其设置为 Unicode,然后查看您的字体在笑脸中可以提供什么
  • ....我错过了一次。当我昨天退出 SO 时,我的评分是 11101/11。它只剩下一个赞成票(+10)。但是当我在家里检查我的业力水平时,它已经超过了 11171/11……“它迅速升级”;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-19
  • 2020-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多