Delphi - 当用户点击模态对话框外部时如何生成事件?

12

当用户点击模态对话框外部时,是否可以触发事件?

好的,Windows 提供了一些提示,例如制造“咚”的声音或闪烁应用程序的任务栏按钮,但是在没有声音和/或用户无法理解任务栏闪烁原因的情况下,我想提供某种额外的提示。此外,如果模态对话框被隐藏在主窗体后面,我还想尝试使用这种方式将其放到最前面。


如果您的模态窗体在主窗体后面消失,请使用modalform.PopupMode := pmAutoPopupMode := pmExplicit; Popupparent := MainForm;。不幸的是,在Delphi 2006中,您没有这个选项。但是您可以了解其他内容,例如Delphi 2006中的Application.PopupMode。为什么要使用这样糟糕的Delphi版本呢?(至少升级到2007年吧?) - Warren P
我认为他这样做是因为“愚蠢运算符现象”... - NaN
3个回答

5

首先,回答这个问题:

当鼠标移出对话框时或已经在对话框外部移动时,您可以捕获鼠标。 接着,您可以捕捉 WM_CAPTURECHANGED 事件来触发一个 OnMouseClickOutside 事件:

type
  TDialog = class(TForm)
  private
    FMouseInDialog: Boolean;
    FOnMouseClickOutside: TNotifyEvent;
    procedure WMCaptureChanged(var Message: TMessage);
      message WM_CAPTURECHANGED;
    procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
    procedure CMMouseEnter(var Message: TMessage); message CM_MOUSEENTER;
  protected
    procedure DoShow; override;
  public
    property OnMouseClickOutside: TNotifyEvent read FOnMouseClickOutside
      write FOnMouseClickOutside;
  end;

...

procedure TDialog.CMMouseLeave(var Message: TMessage);
begin
  // CM_MOUSELEAVE is also send to the dialog when the mouse enters a control that
  // is within the dialog:
  if not PtInRect(BoundsRect, Mouse.CursorPos) then
  begin
    // Now the mouse is really outside the dialog. Start capturing it:
    MouseCapture := True;
    FMouseInDialog := False;
  end;
  inherited;
end;

procedure TDialog.CMMouseEnter(var Message: TMessage);
begin
  FMouseInDialog := True;
  // Only release capture when it had, otherwise it might affect another control:
  if MouseCapture then
    MouseCapture := False;
  inherited;
end;

procedure TDialog.DoShow;
begin
  inherited DoShow;
  // When mouse is outside the dialog when it should become visible, CM_MOUSELEAVE
  // isn't send because the mouse hasn't been inside yet. So also capture mouse
  // when the dialog is shown:
  MouseCapture := True;
end;

procedure TDialog.WMCaptureChanged(var Message: TMessage);
begin
 // When the dialog loses mouse capture and the mouse is outside the dialog, fire:
 if (not FMouseInDialog) and Assigned(FOnMouseClickOutside) then
    FOnMouseClickOutside(Self);
  inherited;
end;

这个方法适用于可见和混淆的对话框。但正如David所评论的那样,这会影响依赖鼠标捕获的控件。我不知道有多少这样的控件,大多数控件(如备忘录或菜单栏)将正常工作。但是拿下拉框为例:当下拉框被打开时,列表框会捕获鼠标。当它失去鼠标时,列表会被收起来。因此,当用户将鼠标移出对话框时(请注意,下拉列表可能在对话框外),组合框将表现出非默认行为。

其次,更深入地解决实际问题:

此外,问题明确指出需要在隐藏对话框的情况下使用此事件。好吧,上述鼠标离开和进入代码取决于对话框可见,所以让我们忘记所有这些,摆脱缺点并将代码简化为:

type
  TDialog = class(TForm)
  private
    FOnMouseClickOutside: TNotifyEvent;
    procedure WMCaptureChanged(var Message: TMessage);
      message WM_CAPTURECHANGED;
  protected
    procedure DoShow; override;
  public
    property OnMouseClickOutside: TNotifyEvent read FOnMouseClickOutside
      write FOnMouseClickOutside;
  end;

...

procedure TDialog.DoShow;
begin
  inherited DoShow;
  MouseCapture := True;
end;

procedure TDialog.WMCaptureChanged(var Message: TMessage);
begin
  if Assigned(FOnMouseClickOutside) then
    FOnMouseClickOutside(Self);
  inherited;
end;

现在,如果事件触发了怎么办?对话框仍然隐藏,调用 BringToFront 无效。(相信我,我已经测试过了,尽管重现一个隐藏的对话框非常困难)。你应该使用 SetWindowPos 将对话框置于所有其他窗口之上:
procedure TAnyForm.MouseClickOutsideDialog(Sender: TObject);
begin
  if Sender is TDialog then
    SetWindowPos(TWinControl(Sender).Handle, HWND_TOPMOST, 0, 0, 0, 0,
      SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE or SWP_NOOWNERZORDER);
end;

但是由于对话框应该始终在其他所有窗口的顶部显示,因此您可以完全消除事件并修改代码如下:

type
  TDialog = class(TForm)
  private
    procedure CMShowingChanged(var Message: TMessage);
      message CM_SHOWINGCHANGED;
  end;

...

procedure TDialog.CMShowingChanged(var Message: TMessage);
begin
  if Showing then
    SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE
      or SWP_NOACTIVATE or SWP_NOOWNERZORDER);
  inherited;
end;

总之:

现在,这仍然不能用于消息或系统对话框(虽然您可以使用这些漂亮的对话框),我必须同意David的看法,找出为什么模态对话框会变得模糊不清。如果您有带有FormStyle = fsStayOnTop(或任何具有HWND_TOPMOST作为Z顺序的窗口)的表单,则应使用以下适当的应用程序方法来暂时补偿这些窗口:

procedure TAnyForm.Button1Click(Sender: TObject);
var
  Dialog: TDialog;
begin
  Application.NormalizeAllTopMosts;
  Dialog := TDialog.Create(Application);
  try
    Dialog.ShowModal;
  finally
    Dialog.Free;
    Application.RestoreTopMosts;
  end;
end;

在所有其他情况下,模态对话框的消失意味着您正在做一些超出常规操作的事情,这可能无法由VCL处理。

这样捕获鼠标肯定会有后果吧? - David Heffernan
@David 不是的,鼠标捕获的变化经常发生,在你几乎每次点击时都会发生。这就是为什么备忘录或组合框在鼠标远离它的地方仍然可以滚动的原因。 - NGLN
好的。如果模态对话框上有一个备忘录,它会捕获鼠标,那么这个操作将失败? - David Heffernan
@David 不,它只在鼠标离开对话框时捕获鼠标,当它超出其边界时。 - NGLN
@David 我已经更新了答案,并解释了DoShow的功能。但感谢您的第一条评论:组合框(可能还有其他)确实受到了影响! - NGLN
谢谢。我认为混淆只会在窗口所有者错误的情况下发生。 - David Heffernan

3
你所要求的并不容易实现。我创建了一个简单的项目,包含两个表单,一个主表单和一个模态表单。然后我使用Spy++跟踪了每个表单在主表单被点击时发送的消息,同时模态表单也处于活动状态。请记住,作为显示模态表单协议的一部分,主表单是被禁用的。这意味着Windows知道主表单无法接收焦点,窗口管理器不会将点击转发到任何表单上。发送的消息是为了执行模态表单的闪烁效果。 模态表单消息
S WM_WINDOWPOSCHANGING lpwp:0018EDA8
R WM_WINDOWPOSCHANGING
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:False
R WM_NCACTIVATE fDeactivateOK:True
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE
P message:0x0118 [Unknown] wParam:0000FFF8 lParam:001A9ECC
S WM_NCACTIVATE fActive:True
R WM_NCACTIVATE

主要表单消息

nHittest:FFFE wMouseMsg:WM_LBUTTONDOWN
S WM_WINDOWPOSCHANGING lpwp:0018EDA8
R WM_WINDOWPOSCHANGING
R WM_SETCURSOR fHaltProcessing:False
nHittest:FFFE wMouseMsg:WM_LBUTTONUP
R WM_SETCURSOR fHaltProcessing:False

我认为这里没有任何你可以真正钩住的东西。你最好的希望是尝试检测重复的WM_NCACTIVATE消息流,但我真的不建议这么做。
在我看来,你需要更仔细地研究根本问题。你说模态窗体有时会在主窗体下面。在这种情况下,你在 窗口所有权 上做错了些什么。主窗体应该是模态窗体的最终所有者,如果是这样的话,它就永远不可能在主窗体下面。在我看来,你只需要修复你损坏的窗口所有权结构,问题就会消失。

2
@DavidHeffernan 可能只是我们那些躲在幕后的不满意者之一,他们会对任何不符合他们崇高标准的东西进行负面评价... :-) - Marjan Venema
@rossmcm:因为人们应该自由地进行投票,而不必担心后果,比如报复性的投票或麻烦的攻击。 - Marjan Venema
@marjan:嗯...我认为讽刺的是,人们可以匿名地投反对票(没有提供理由),但不能在提供用户名的情况下提供建设性批评。 - rossmcm
@rossmcm:嗯,不完全是。毕竟你可以自由选择不透露真实姓名和/或使用另一个匿名账户。你需要确保在那个匿名账户上获得一些声誉,这样你才能真正发表评论,但获得50点声望并不太费力:一堆建议和接受的编辑在一个下午就能搞定。 - Marjan Venema

2
我不确定如何在Delphi中实现这一点,但使用C++,您可以执行以下操作:
 // The message loop for our modal dialogbox
 BOOL CALLBACK DialogProc(HWND hwndDlg,
                          UINT uMsg,
                          WPARAM wParam,
                          LPARAM lParam) {
      switch(uMsg) {
        case WM_INITDIALOG:
          return TRUE;
          break;
        case WM_COMMAND:
          switch(wParam) {
            case IDOK:
              EndDialog(hwndDlg, 0);
              return TRUE;
              break;
          }
          break;
        case WM_ACTIVATE:
          // message sent when the window if being activated/deactivated
          if(wParam == WA_INACTIVE) {
            // the window is being inactivated so beep once
            Beep(750, 300);
            // bring dialog to the foreground
            SetForegroundWindow(hwndDlg);
          }
          break;
      }
      return FALSE;
 }

 int main(int argc,char** argv) {
     // create a modal dialog
     DialogBox(GetModuleHandle(NULL),
               MAKEINTRESOURCE(IDD_MYDIALOG),
               HWND_DESKTOP,
               DialogProc);
     return 0;
 }

您还可以查看SetWindowsHookEx()子类化控件,这可能会指引您正确的方向。


模态窗体不会接收WM_ACTIVATE消息,因为应用程序中的所有其他窗体都被禁用了。因此,窗口管理器知道它不需要激活模态窗口。至少这是我的分析告诉我的原因。 - David Heffernan
@DavidHeffernan - 我并没有说上面的方法是正确的解决方案,只是它可以给他一些提示,让他通过处理模态窗口生成的某些消息来解决问题。 - Cyclonecode
你可以从我的答案中看到哪些消息到达了模态窗口。 - David Heffernan
2
在这种情况下,OP遭受了Windows的设计缺陷,而Delphi 2006尚未修改以解决该问题。启用时,在Windows XP及更高版本中,进程窗口虚影会导致应用程序中的窗口丢失Z-Order,除非它们已经正确标记为窗口父级,而在Delphi应用程序中通常不会这样做,直到Windows中的此错误变得至关重要。微软几乎没有将Delphi放在其雷达上,并且这个Windows故障在最近的Delphi版本中早已被解决。 - Warren P
1
@WarrenP 微软不能为错误的窗口所有权负责。 - David Heffernan

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