Delphi - 如何找出当前有焦点的模态对话框并将其置于前台?

8
我有一个 Delphi 2006 应用程序,可以在错误条件下弹出模态对话框。但它似乎陷入一种状态,其中一个这些模态对话框打开并位于主表单前面,但两个表单都不响应消息。单击任何一个会出现“咚”的声音。应用程序正常运行,UI 正在更新主表单,但您无法执行任何操作。我猜主表单下面可能还有另一个模态对话框。无论它是我的还是 Windows 的,我都不知道。

其他要点:

  • the app responds to keyboard shortcuts OK. One of these shortuts shuts down the app gracefully and this worked. I have been unable to reproduce the situation since.
  • the app has a tray icon. This responds to right mouse clicks. If I minimize the app from here the main form minimizes and leaves the modal dialog displayed, still without focus. If I restore the main form, things are as they were, with neither window having focus. Alt-tab has similar results.
  • platform is Windows 7
  • I call DisableProcessWindowsGhosting before any forms are created
  • I open the modal dialogs with

    ModalDialog.PopupParent := MainForm ;
    ModalDialog.ShowModal ;
    
  • I postpone these error dialogs if other modal dialogs are open:

    if (Application.ModalLevel = 0) then
        {open modal dialog}
    
我的问题有两个部分:
1. 是否有办法通过编程找出哪个窗口具有焦点?然后我可以针对这种情况采取一些行动,或者在必要时提供快捷键以将其置于前台或采取某些回避措施(根据对话框的情况),例如将 ModalResult 设置为 mrCancel。
2. 这种情况怎么会发生?通常当我将一个模态对话框放在主窗体后面(我可以通过让模态对话框打开,从托盘图标最小化应用程序,然后再恢复应用程序来做到这一点——应用程序主窗体恢复在对话框前面,对话框仍然保持焦点),我可以通过单击托盘图标将其再次置于前台,或者使用 Esc 键关闭它,但在这种情况下它并没有起作用。
**更新**
Misha 的修复方法有效,除了像 TSaveDialog 这样的非 Delphi 对话框。我能够通过在调用 Execute 之前添加 Application.ModalPopupMode := pmAuto;来使它们也工作。
“让它工作”的意思是,在以下序列之后保存对话框在前面:
1. 打开保存对话框。 2. 从托盘图标最小化应用程序。 3. 使用托盘图标恢复应用程序。
而没有 ModalPopupMode := pmAuto 时,它在主窗体后面。所以我希望这些更改能够解决(尚未重现的)问题。

2
寻找有焦点的窗口并采取补救措施是不够的。你需要从源头解决问题。这意味着要理解窗口所有权(即PopupParent)。 - David Heffernan
谢谢@David。找出哪个窗口具有焦点肯定会有所帮助,因为这可能会向我展示如何重现问题,而目前我无法做到这一点。 - rossmcm
一旦您已经使其表现出来,您应该能够使用类似Spy++的工具来了解所有权关系。我建议您从外部进行调试,至少在您有一个可重现的情况之前。 - David Heffernan
你可以通过在调试版本中添加{$ifdef DEBUG}Sleep(5000);{endif}来经常重现它 - 需要进行一些实验才能找到最容易出现问题的地方。这个z-order混乱的魔法触发器是Windows认为你的应用程序没有响应。 - Warren P
4个回答

5
如果一个有焦点的表单(Form1)响应消息的时间太长,以至于Windows认为Form1无响应,并且Form1显示一个模态表单(Form2),那么在Form2被显示并且应用程序再次处理消息时,Form1将被带到前面,从而潜在地 "覆盖" Form2。
将以下内容放入Application.OnIdle事件中即可解决问题:
  if Assigned(Screen.ActiveForm) then
  begin
    if (fsModal in Screen.ActiveForm.FormState) and
       (Application.DialogHandle <= 0)) then 
    begin
      Screen.ActiveForm.BringToFront;
    end;
  end;

谢谢 @Misha。那似乎有效了,除非有一个非 VCL 对话框打开,例如加载文件对话框,所以我们还没有完全解决。我使用了一个定时器,因为 OnIdle 事件不会触发(例如)如果您打开了上下文菜单。 - rossmcm

4
最后一个活动的弹出窗口(无论是VCL还是非VCL)可以使用GetLastActivePopup查询:
function GetTopWindow: HWND;
begin
  Result := GetLastActivePopup(Application.Handle);
  if (Result = 0) or (Result = Application.Handle) or
      not IsWindowVisible(Result) then
    Result := Screen.ActiveCustomForm.Handle;
end;

这段话有些内容是从TApplication.BringToFront中复制而来。

通过SetForegroundWindow可以将该窗口置于最前端:

SetForegroundWindow(GetTopWindow);

请注意,Application.BringToFront 可能会完全解决问题,但我曾经遇到过它无法正常工作的情况,尽管此后我未能再次复制这种情况。

2
你需要检查 Assigned(Screen.ActiveCustomForm)。我正在使用 Application.ShowMainForm := False 在托盘中运行我的应用程序,如果在没有显示任何窗体的情况下调用 GetTopWindow,它会抛出 AV 异常。 - saastn

0

我使用了Misha的解决方案,并进一步改进(使用NGLN的代码),以解决rossmcm遇到的问题(处理非VCL对话框)。

以下代码在计时器中运行:

type
  TCustomFormAccess = class(TCustomForm);


if Assigned(Screen.ActiveCustomForm) then
begin
  if ((fsModal in Screen.ActiveCustomForm.FormState) and
      (Application.DialogHandle <= 0)) then
  begin
    TopWindow := GetLastActivePopup(Application.Handle);
    TopWindowForm := nil;
    for i := 0 to Screen.CustomFormCount - 1 do
    begin
      CustomFormAccess := TCustomFormAccess(Screen.CustomForms[i]);
      if CustomFormAccess.WindowHandle = TopWindow then TopWindowForm := CustomFormAccess;
    end;
    if Assigned(TopWindowForm) and (Screen.ActiveCustomForm.Handle <> TopWindow) then
    begin
      Screen.ActiveCustomForm.BringToFront;
    end;
  end;
end;

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接