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

时间:2012-03-25 00:38:19

标签: delphi delphi-2006

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

好的,Windows通过发出“bonk”声音或闪烁应用程序的任务栏按钮来提供它自己的线索,但我想为声音不可用的情况提供某种额外的线索和/或者用户无法识别任务栏闪烁的原因。此外,我想尝试使用它作为一种方式,如果它隐藏在主窗体后面,将模态对话框放在前面。

3 个答案:

答案 0 :(得分: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;

这很有效。对于可见和混淆的对话框。但正如大卫感激地评论的那样,这对依赖鼠标捕获的控制产生了影响。我所知道的并不多,大多数控件如备忘录或菜单栏都能正常运行。但是拿一个组合框:当一个组合框被放下时,列表框会捕获鼠标。当它丢失鼠标时,列表就会被包裹起来。因此,当用户将鼠标移到对话框外时(请注意,下拉列表可能位于对话框外),组合框将显示非默认行为。

其次,要更多地解决实际问题:

此外,该问题特别指出了在隐藏对话框中对此事件的需求。好吧,上面的鼠标离开和输入代码取决于对话框是否可见,所以让我们忘记所有这些,摆脱缺点并将代码减少到:

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;

结论:

现在,这仍然不适用于消息或系统对话框(尽管你可以使用these nice dialogs),我必须同意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无法处理的异常事情。

答案 1 :(得分: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消息流,但我真的不会尝试这个。

在我看来,你需要更仔细地研究基本问题。你说模态形式有时在主形式之下。在这种情况下,您对window ownership做错了。主要形式应该是你的模态形式的最终所有者,如果是这样,那么它永远不会在主形式之下。在我看来,你只需修复破损的窗口所有权结构,问题就会消失。

答案 2 :(得分: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()Subclassing Controls