【问题标题】:OnShown event for TForm?TForm 的 OnShown 事件?
【发布时间】:2014-06-17 16:00:16
【问题描述】:

在程序启动时,在OnActivate 事件处理程序中,我需要做一些事情来阻止程序几秒钟。在此期间,窗体的客户区仍未完全绘制,这对用户来说看起来很丑陋。 (在这段阻塞时间内,我不需要程序响应点击或其他用户操作,因此无需将阻塞操作放入线程中 - 我只需要完全绘制表单即可)。所以我使用TForm.UpdateApplication-ProcessMessages在阻塞操作之前更新表单,效果很好:

procedure TForm1.FormActivate(Sender: TObject);
begin
  Form1.Update;
  Application.ProcessMessages;
  Sleep(7000);
end;

但是,我想知道对于这个问题是否没有其他更优雅的解决方案。例如,这可能是在 TForm 的后代中实现的 OnShown 事件,该事件将在表单完全绘制后触发。这样的事件怎么可能实现?

【问题讨论】:

  • 参见例如swissdelphicenter.com/torry/showcode.php?id=1276。但我很确定在 Stack Overflow 上有一个关于它的问题。
  • 发布消息(如您​​提供的链接中)IMO 并不完全可靠,因为消息 (AFAIK) 在某些情况下也可能会丢失。我需要 200% 可靠的东西。
  • 消息不会丢失。但是如果队列已满,PostMessage 可能会在排队时失败,您可以使用 PostMessage 结果参数检测到。
  • 在 .NET 中有一个 Form.Shown 事件(参见 MSDN)。因此,在 Windows 级别,窗体必须知道何时完全绘制。不应该可以使用 Windows API 访问它吗?
  • 你在错误的地方阻止了。只需在 FormShow 事件结束时向自己发送一条消息,然后执行阻止操作以响应该消息。当您在其处理程序中收到消息时,该表单已完全绘制。 (当表单/应用程序失去焦点并重新获得焦点时,可以多次触发 OnActivate;无论如何,这通常是错误的地方。)

标签: delphi delphi-xe2 tform


【解决方案1】:

您真正的问题是您阻塞了 UI 线程。简而言之,你绝不能这样做。将长时间运行的任务移到不同的线程上,从而使 UI 保持响应。

【讨论】:

  • 一般我同意;但在这种特定情况下,出于我的特定目的,这不是问题。
  • 如果您希望您的应用程序无法抽取其 UI 消息队列,并让系统将您的窗口显示为无响应,这显然取决于您。我不会这样做,这就是我写上面的原因。但是,如果您愿意这样做,显然是您的选择。
【解决方案2】:

如果您正在寻找应用程序完成加载/重新绘制时触发的事件,您应该使用 TApplication.OnIdle 事件

http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Forms.TApplication.OnIdle

一旦读取应用程序以接收用户输入,就会触发此事件。注意每次应用程序空闲时都会触发此事件,因此您需要实现一些控制变量,该变量会在 OnIdle 首次触发时通知您。

但正如 David 已经指出的那样,阻止你的 UI(主线程)是不好的。为什么?当您阻塞主线程时,应用程序无法正常处理其消息。这可能会导致操作系统将您的应用程序识别为“挂起”。而且 aou 绝对想避免这种情况,因为它可能会导致用户去强行杀死您的应用程序,这可能会导致数据丢失。此外,如果您想为 Windows 以外的任何其他平台设计应用程序,您的应用程序可能会因此而无法通过认证过程。

【讨论】:

  • 呸。 OnIdle 也不是海报所要寻找的。它触发得太频繁了,甚至比使用 OnActivate 更糟糕。并非所有事情都可以移动到后台线程;例如,在 DBGrid 中显示数据的 CRUD 应用程序必须首先连接到 DB,并且 DB 连接必须属于主线程才能访问。您无法将该连接移至辅助线程,因为您的 UI 无法访问该连接。
【解决方案3】:

过去,一个简单的 PostMessage 就可以解决问题。 本质上,您在基本表单的 DoShow 期间触发它:

procedure TBaseForm.DoShow;
begin
  inherited;
  PostMessage(Handle, APP_AFTERSHOW, 0, 0);
end;

然后捕获消息并为从该基本表单继承的所有表单创建一个 AfterShow 事件。

但这不再有效,如果您正在换肤并且拥有大量 VCL 控件,那就不行了。

我的下一个技巧是在 DoShow 中生成一个简单线程并检查 IsWindowVisible(Handle) 和 IsWindowEnabled(Handle)。这确实加快了速度,因为数据库打开和其他东西已经在 AfterShow 事件中,所以它从加载时间减少了 250 毫秒。

最后我想到了 madHooks,它很容易为我的应用程序挂钩 API ShowWindow 并从中触发 APP_AFTERSHOW。

function ShowWindowCB(hWnd: HWND; nCmdShow: Integer): BOOL; stdcall;
begin
  Result := ShowWindowNext(hWnd, nCmdShow);
  PostMessage(hWnd, APP_AFTERSHOW, 0, 0);
end;

procedure TBaseForm.Loaded;
begin
  inherited;
  if not Assigned(Application.MainForm) then // Must be Mainform it gets assigned after creation completes
    HookAPI(user32, 'ShowWindow', @ShowWindowCB, @ShowWindowNext);
end;

要在 AfterShow 之前完成整个绘制,仍然需要调用 ProcessPaintMessages

procedure TBaseForm.APPAFTERSHOW(var AMessage: TMessage);
begin
  ProcessPaintMessages;
  AfterShow;
end;

procedure ProcessPaintMessages; // << not tested, pulled out of code
var
  msg: TMsg;
begin
    while PeekMessage(msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE) do 
      DispatchMessage(msg);
end; 

我的最后一个测试是在 AfterShow 事件中添加一个 Sleep,并看到由于 AfterShow 事件尚未完成,因此使用空 db 容器完全绘制了表单。

procedure TMainForm.AfterShow;
begin
  inherited;
  Sleep(8*1000);
 ......

【讨论】:

    猜你喜欢
    • 2023-03-06
    • 1970-01-01
    • 2017-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多