我不明白Delphi中的Application.ProcessMessages正在做什么

use*_*542 23 delphi

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

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

J..*_*... 47

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

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

例如 :

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

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

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

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

问题

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

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

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

  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;
Run Code Online (Sandbox Code Playgroud)

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

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

procedure TApplication.ProcessMessages;
var
  Msg: TMsg;
begin
  while ProcessMessage(Msg) do {loop};
end;
Run Code Online (Sandbox Code Playgroud)

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

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

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


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

彼得下面的钥匙的奥德赛

(Google缓存链接 ......服务器似乎对我很忙)

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

  • @DavidM我认为我对使用它的想法充满消极.问题是"什么是ProcessMessages在做什么?",而不是"为什么这是一个坏主意?". (3认同)
  • @DavidM*"这不是最佳实践,但它会做"*在许多情况下都有效.如果您只是学习Delphi,它肯定是其中之一,并且线程(尤其是来自线程的GUI交互)是一个复杂的主题,许多经验程序员甚至都没有完全掌握.而且当你因任何原因必须快速破解工具时,它可能会帮助你解决多线程实现过度杀伤的问题.我认为J的答案(+1)在Application.ProcessMessages的注意事项上非常清楚,并且说根本不使用它是不合理的. (3认同)
  • @mghie同意.当重新出现的怪物出现时,我总是提起这件事.与"ProcessMessages"相比,线程在很多方面更容易出现棘手的错误."ProcessMessages"的真正问题在于它鼓励编码风格使得糟糕,紧密耦合,难以重构,意大利面/上帝应用程序如果被允许变得太大,最终会成为一个噩梦. (3认同)
  • @DavidM:当叙述`Application.ProcessMessages`的严重危险时,总会提到重入.它也是一个完全的稻草人.将长时间运行的代码放在一个线程中,如果UI没有禁用启动当前无法安全启动的用户操作的控件,则会发现重新发生的相同问题.一旦修复,对"Application.ProcessMessages"的调用通常也不再有问题.(忽略堆栈溢出的可能性,到目前为止还没有回答.) (2认同)

Gol*_*rol 31

消息

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

这些消息将发送到特殊队列.您可以直接发送此类消息(使用SendMessagePostMessageAPI函数),但在设置属性时也会间接发送消息,例如Texta TEdit.这些消息用于通知控件程序中发生了重大事件,以便它们可以正确响应.发送和响应消息的概念是称为事件驱动编程的范例的核心原则.

许多现代操作系统都是事件驱动的,例如Windows.事实上,德尔福VCL封装了很多的Windows功能,很多事情你做将导致控件,用来通知鼠标点击,键盘压力机的这些控件之间发送的消息,Windows的设置更改,申请截止,控制移动,等等.

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

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

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

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

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

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

Application.ProcessMessages来救援

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

Label1.Caption := 'A';
Application.ProcessMessages;
Sleep(5000);
Label1.Caption := 'B';
Run Code Online (Sandbox Code Playgroud)

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

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

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

内幕

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

  repeat

    ProcessMessageFromTheQueue;
    If QueueIsEmpty then
      TellWindowsToWakeMeUpWhenANewMessageArrives;   

  until Terminated;
Run Code Online (Sandbox Code Playgroud)

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

或者是吗?

Application.ProcessMessages 看起来像这样:

  repeat

    ProcessMessageFromTheQueue;

  until QueueIsEmpty;
Run Code Online (Sandbox Code Playgroud)

因此它与应用程序主循环几乎相同:它从队列中选取一条消息并对其进行处理.这意味着,您可以在处理最后一条消息时实际处理下一条消息.一个方便的技巧,但它可以轻松地使您的应用程序混乱.

正确的方法:线程

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

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

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

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

  • 睡眠显然只是许多实际处理的示例占位符. (11认同)