【问题标题】:Get window to refresh (etc) without calling Application.ProcessMessages?在不调用 Application.ProcessMessages 的情况下获取窗口以刷新(等)?
【发布时间】:2009-07-17 09:27:01
【问题描述】:

我这里有一个旧版应用程序,它有一些“耗时”的循环,由于各种用户交互而被触发。耗时的代码会定期使用进度信息(通常是标签)更新屏幕上的某些内容,然后,似乎是为了说服视觉刷新在那里发生,然后代码调用 Application.ProcessMessages(啊!)。

我们现在都知道这会给 GUI 应用程序带来什么样的麻烦(因为是慈善的,那是一个更纯真的时代),我们发现这就像鸡蛋一样,不时让用户实现程序是不可能的,因为他们在程序“忙”时点击控件。

在不处理其他事件/消息等的情况下定期刷新表单的视觉效果的最佳方式是什么?

我的想法是;
- 在执行任何耗时的操作之前禁用所有控件,并保留“...ProcessMessages”调用以“强制”刷新,或
- 找到另一种定期刷新控件的方法

我可以做前者,但这让我想知道 - 有没有更好的解决方案?

旧版示例代码;

我:=0; 而 FJobToBeDone 做 开始 DoStepOfLoop; 公司(一); 如果我 mod 100 = 0 那么 开始 更新标签等; Application.ProcessMessages; 结尾; 结尾;

我已经能听到你们在后面昏倒了。 :-)

【问题讨论】:

    标签: delphi


    【解决方案1】:

    如果您在更改属性后对控件调用 Update(),您将强制它们重绘。另一种方法是调用 Repaint() 而不是 Refresh(),这意味着调用 Update()

    您可能还需要在父控件或框架上调用 Update(),但这可以让您完全消除 ProcessMessages() 调用。

    【讨论】:

    • 在mghie上找到,谢谢!我将它放入几个示例关键位置并进行了快速测试;屏幕更新发生得很好,但是当表单很忙时,控件不会生成“新”事件。如果我在工作开始时将其与明确锁定表单结合起来,我希望这将阻止流氓事件通过。非常感谢!
    • Repaint 和 Update 都不会立即影响 Delphi 6 中设置 TDBGrid 的颜色。在 Update 后调用 Application.ProcessMessages 也无济于事。
    【解决方案2】:

    我用于长时间更新的解决方案是在单独的线程中进行计算。这样,主线程保持非常敏感。一旦线程完成,它会向主线程发送一条 Windows 消息,指示主线程可以处理结果。

    不过,这还有其他一些严重的缺点。首先,当另一个线程处于活动状态时,您必须禁用一些控件,因为它们可能会再次重新启动线程。 第二个缺点是您的代码需要成为线程安全的。有时这可能是一个真正的挑战。如果您使用的是遗留代码,那么您的代码很可能不是线程安全的。 最后,多线程代码更难调试,应该由有经验的开发人员来完成。

    但多线程的一大优势是您的应用程序保持响应,用户可以继续做一些其他事情,直到线程完成。基本上,您是将同步方法转换为异步函数。并且线程可以触发一些消息,指示某些控件刷新自己的数据,这些数据将立即更新。 (并且在您希望更新它们的那一刻。)

    我自己已经多次使用过这种技术,因为我认为它要好得多。 (但也更复杂。)

    【讨论】:

    • 谢谢,Workshop Alex - 我认为接下来这是尝试和做的“正确”事情,特别是在可能长时间不活动的情况下。然而,在这种特殊情况下,mghie 的“更新”似乎完全取代了 ProcessMessages 调用。
    • 虽然 Alex 给出了一个很好的解释,但它缺乏关于如何开始的指针:Delphi 在“添加新”对话框中有一个线程单元模板,但本质上这一切都归结为创建一个从 TThread 派生的类,覆盖 Execute 方法。
    • Stijn 提供了一种在 Delphi 中使用线程的好方法。基本上,TThread 类封装了 Windows API,并提供了一个可以共享数据的基类。就个人而言,我更喜欢使用原始 API,创建一个单独的线程过程,再次调用我的业务逻辑类的特定方法。 (或您的主/子表单的方法。)但由于复杂性,我什至不敢将这种技术用于遗留代码,除非我要从头开始重写它。 (遗留代码通常不是线程安全的。)
    【解决方案3】:

    不要调用 Application.Processmessages,这很慢并且可能会产生无限递归。 例如,要在不滑动的情况下更新 Panel1 中的所有内容,我们可以使用以下方法:

    procedure TForm1.ForceRepaint;
    var
      Cache: TBitmap;
      DC: HDC;
    begin
      Cache := TBitmap.Create;
      Cache.SetSize(Panel1.Width, Panel1.Height);
      Cache.Canvas.Lock;
      DC := GetDC(Panel1.Handle);
      try
        Panel1.PaintTo(Cache.Canvas.Handle, 0, 0);
        BitBlt(DC, 0, 0, Panel1.Width, Panel1.Height, Cache.Canvas.Handle, 0, 0, SRCCOPY);
      finally
        ReleaseDC(Panel1.Handle, DC);
        Cache.Canvas.Unlock;
        Cache.Free;
      end;
    end;
    

    为了获得更好的性能,应该首先创建缓存位图,并在进程完成后释放

    【讨论】:

      【解决方案4】:

      您正在寻找的技术称为threading。这是编程中的一项困难技术。代码应该小心处理,因为它更难调试。无论您是否使用线程,您绝对应该禁用用户应该 搞乱的控件(我的意思是可以影响正在进行的过程的控件)。您应该考虑使用 actions 来启用/禁用按钮等...

      【讨论】:

      • 感谢这个 Codervish。我必须承认我过去根本没有使用过动作列表,但仔细观察后,它似乎确实是一种非常巧妙的方式来控制事物的整个启用/事件方面。干杯!
      • 处理复杂的线程机器,同步和回调,因为一个糟糕的控件不想立即更新?!看在上帝的份上!
      【解决方案5】:

      我们没有禁用控件,而是以 FBusy 的形式有一个布尔变量,然后在用户按下按钮时简单地检查它,我们根据你提到的确切原因引入它,用户点击按钮当他们等待长时间运行的代码运行时(令人恐惧的是您的代码示例有多熟悉)。

      所以你最终会得到类似的东西

      procedure OnClick(Sender:TObejct);
      begin
          if (FBusy) then
          begin
              ShowMessage('Wait for it!!');
              Exit;
          end
          else FBusy := True;
          try
              //long running code
          finally
              FBusy := False;
          end;
      end;
      

      在退出或异常的情况下,记住将长时间运行的代码放在 try-finally 块中是无能为力的,因为您最终会得到一个不起作用的表单。

      正如建议的那样,如果它用于不会影响数据的代码,例如运行报告或数据分析,我们会使用线程,但有些事情这不是一个选项,例如如果我们要更新 20,000 条产品记录,那么我们不会'不希望任何人在飞行途中试图出售或以其他方式更改记录,因此我们必须阻止该应用程序,直到它完成。

      【讨论】:

      • 谢谢你 - 这种方法是我自己在其他应用程序中倾向于使用的方法。我想我会在我编写的下一个应用程序中尝试使用线程来处理长时间运行的代码,因为这似乎是我应该习惯使用的东西。 :-)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-18
      • 2021-03-12
      • 2010-11-24
      • 1970-01-01
      相关资源
      最近更新 更多