我不明白Delphi中的Application.ProcessMessages在做什么[关闭]

问题描述 投票:23回答:2

我是Delphi的noobie,对于任何愚蠢的问题都很抱歉。

我的主管向我解释说Application.ProcessMessages可以防止冻结应用程序并分配一些额外的计算时间。但是在这个命令的文档中总是会解释一个被处理的队列系统?请问有人可以向我解释一下背景吗?

delphi
2个回答
47
投票

没有简短的方法可以正确回答这个问题。

Windows应用程序与操作系统交互的主要方式是通过消息传递系统。在Windows应用程序中发生的一切都是在响应消息时发生的。

例如 :

如果单击屏幕,操作系统将决定单击哪个应用程序并向该应用程序发送消息,指示已收到单击(以及该单击的位置)。

如果窗口被移动并在其下方显示应用程序的一部分,操作系统会发送一条消息,告诉您的应用程序重绘自己。

名单还在继续。发生的一切都是由消息驱动的。

现在,每个应用程序都有一个主用户界面线程(“主”线程),并且该线程有一个主要功能 - 它在无限循环中运行,从操作系统检查这些消息,然后执行必要的代码以响应那些消息。

问题

你过来开始编写一个应用程序。您可能会写一些这样的代码:

procedure TForm1.Button1Click(Sender: TObject);
var i : Integer;
begin
  for i := 0 to 99999999999 do begin
    SolveTheProblemsOfTheWorld(i);
    CalculatePiToABillionPlaces;
  end;
end;

在程序的较大结构中,主线程中的执行如下所示:

  1. 检查消息
  2. 对于每条消息 - 执行关联的处理程序
  3. 返回检查消息(循环)

因此,当突然有一个相关的处理程序(上面的Button1Click)开始花费很长时间才能完成时,这个循环很愉快。理解的关键是一个消息处理程序必须在下一个消息处理程序运行之前完成。例如,如果单击滚动条并拖动它,但是您已将处理程序附加到滚动条的OnClick(需要10秒才能完成),则在该单击处理程序完成之前,应用程序将不会看到拖动操作。与此同时,消息队列正在填满,主线程没有对此做任何事情。

当然,您已经体验过这一点 - 突然您的应用程序无法响应点击。你不能与它交互,你无法移动窗口,如果你拖动另一个窗口,应用程序甚至不会重绘自己 - 它只是填满你留在它上面的任何垃圾。

输入ProcessMessages

不把你长时间运行的代码放入线程的懒惰,可怕的解决方案

你在调用Application.ProcessMessages时正在做的是,在其中一个处理程序的中间,指示主线程休息一下,返回检查消息队列并清空所有堆积的消息;处理任何点击,窗口移动,输入,击键,在需要时重新绘制自己等等。

procedure TForm1.Button1Click(Sender: TObject);
var i : Integer;
begin
  for i := 0 to 99999999999 do begin
    SolveTheProblemsOfTheWorld(i);
    CalculatePiToABillionPlaces;
    Application.ProcessMessages;
  end;
end;

这似乎表面上看起来很明智,因为它允许您在长时间运行的循环中保持应用程序的响应。然而,最终,由于许多非常好的理由,这种编程风格被广泛认为是极差的实践。我只想说,在你想要使用Application.ProcessMessages的任何地方,都可以将这项工作转移到后台线程。

有关更多详细信息,请查看实际代码:

procedure TApplication.ProcessMessages;
var
  Msg: TMsg;
begin
  while ProcessMessage(Msg) do {loop};
end;

因此,当您调用Application.ProcessMessages时,您正在运行一个循环,逐个清空消息队列中的消息(并执行附加到对这些消息做出反应的处理程序的所有代码),直到它为空。当它为空并且没有更多消息要处理时,控制将返回到程序中的下一行。

需要注意的重点是,使用程序时遇到的流畅,流畅的交互是一种完全错觉,它完全取决于正在尽快处理的消息循环。发布到您的应用程序的消息与正在处理的消息之间的时间延迟越小,您的应用程序就越感觉它是活着的和响应的。

因此,连接到用户界面处理程序的所有代码都应该快速运行。长时间运行的操作需要被中断,以便消息处理可以继续(即:Application.ProcessMessages)或者那些操作需要被移动到一个单独的线程,在那里它们可以执行而不需要占用主线程并使其远离其主要职责(是保持用户界面活着)。


有关该主题的非常好的文章,请参阅:

Peter下面的A Key's Odyssey

Internet Archive link ......以防上述死亡)

摘要:本文遵循通过VCL的击键消息的路径。您将学习如何实现密钥处理,OnKey事件如何工作以及在整个过程中可以找到程序员的干预点。此外,还解释了消息处理等内容,您将学习如何在调试器中将消息从消息循环跟踪到最终目标。


31
投票

消息

Delphi VCL应用程序是一个Windows应用程序,Windows组件之间的许多通信(如表单,编辑框,以及计时器等隐藏的东西)都是使用称为消息的东西完成的。

这些消息将发送到特殊队列。您可以直接发送此类消息(使用SendMessagePostMessage API函数),但在设置属性时也会间接发送消息,例如TextTEdit

这些消息用于通知控件程序中发生了重大事件,以便它们可以正确响应。发送和响应消息的概念是称为事件驱动编程的范例的核心原则。

许多现代操作系统都是事件驱动的,例如Windows。事实上,Delphi VCL包含了很多Windows功能,你做的很多事情都会导致在控件之间发送消息,这些消息会通知那些控制鼠标点击,键盘按下,Windows设置更改,应用程序关闭,控制移动等。

执行代码时,Mainthread不处理消息

操作系统还允许程序具有称为线程的东西。线程就像一个程序的小块,似乎同时运行(在某些情况下实际上同时运行)。一个应用程序可以有多个线程,但总有主线程,它也是负责接受传入消息和更新GUI的线程。

当应用程序的主线程空闲(不执行代码)时,它将检查队列以查看是否有消息并处理它们。在执行其他不相关的代码时,这不可能发生。考虑这个例子:

Label1.Caption := 'A';
Sleep(5000); // Sleep is simulating a long, blocking process.
Label1.Caption := 'B';

如果您执行此代码,您会希望标签的标题为“A”,五秒后会变为“B”。但是这是错误的。设置标签的标题会触发通过消息重新绘制控件。由于主线程仍被此代码阻止(即使通过Sleep命令),因此尚未处理该消息。

只有当5秒钟过去并且标题设置为“B”时,主线程才会变为空闲并执行我们通过设置标题触发的重新绘制。另请注意,其他交互(如单击按钮或拖动窗口)将推迟到5秒钟过后。当主线程执行该代码时,整个UI冻结。

Application.ProcessMessages来救援

Application.ProcessMessages将强制应用程序清空其消息队列,因此您可以像这样“修复”代码:

Label1.Caption := 'A';
Application.ProcessMessages;
Sleep(5000);
Label1.Caption := 'B';

通常,代码中不会出现Sleep,而是进行大量实际处理。通过频繁使用Application.ProcessMessage,即使执行代码,也可以保持应用程序的界面响应。

但这有点脏,主线程(和接口)仍然会被阻止,除非你设法挤压很多对Application.ProcessMessages的调用。除了处理绘制消息之外,还会处理其他消息,因此应用程序可以在流程中间处理单击事件。

因此,您阅读的文档是正确的:Application.ProcessMessages将清空消息队列。你的主管不完全正确。它没有为每个说法分配额外的处理时间,但它只是将消息队列的清空集成到执行的代码中,而通常消息将保留在队列中,直到应用程序变为空闲。

内幕

在内部,应用程序本身始终如一。它的主要程序Application.Run看起来像这样(简化的伪代码):

  repeat

    ProcessMessageFromTheQueue;
    If QueueIsEmpty then
      TellWindowsToWakeMeUpWhenANewMessageArrives;   

  until Terminated;

这是执行代码的处理,例如当处理WM_MOUSECLICK消息时,它将触发(通过一些Delphi VCL魔法),你的Button1Click事件处理程序。由于这是一个单独的线程,所以一切都按顺序运行,因此您将理解ProcessMessageFromTheQueue仅在事件处理程序完成时返回。 Delphi一次只处理一条消息,只处理完上一条消息时才执行下一条消息。

或者是吗?

Application.ProcessMessages看起来像这样:

  repeat

    ProcessMessageFromTheQueue;

  until QueueIsEmpty;

因此它与应用程序主循环几乎相同:它从队列中选取一条消息并对其进行处理。这意味着,您可以在处理最后一条消息时实际处理下一条消息。一个方便的技巧,但它可以很容易地使您的应用程序混乱,并导致各种不必要的副作用。

正确的方法:线程

大多数人认为使用单独的线程执行代码并在需要更新标签或进度条时向主线程发送信号是更好的解决方案。这样,主线程一直处于空闲状态,并将继续响应鼠标点击等。

不过,线程对初学者来说很难。要注意的一些事项:

  • 线程(主线程除外)不应直接更新GUI(例如,线程无法设置Label1.Caption。它可能会失败。
  • 变量和其他资源不能同时由多个线程更改,因此您必须采取预防措施来防止(关键部分)。
  • 您必须(在大多数情况下)阻止用户执行已执行的相同操作。
  • 在许多情况下,如果用户想要取消进程或关闭应用程序,则必须找到正确的方法来中断正在运行的线程。

无论如何,这个答案不应该是关于线程的完整课程,但现在至少你知道Application.ProcessMessages的用途。还要注意,它被许多人认为是“邪恶的”,但作为一个初学者,无论如何你都可以尝试自己找出它的优点和缺点。如果你这样做,我希望这些信息至少会激励你在下一章有足够经验的时候尽快转向线程。

© www.soinside.com 2019 - 2024. All rights reserved.