Delphi - Как вы генерируете событие, когда пользователь нажимает за пределами модального диалога? - PullRequest
10 голосов
/ 25 марта 2012

Можно ли вызвать событие, когда пользователь щелкает за пределами модального диалога?

ОК, Windows предоставляет свои собственные подсказки, когда вы делаете это, издавая «чокнутый» звук или мигая кнопкой на панели задач приложения, но я хотел бы предоставить какую-то дополнительную подсказку для ситуаций, когда звук недоступен и / или пользователь не распознает причину мигания панели задач. Кроме того, я хотел бы попытаться использовать это как способ вывести модальное диалоговое окно на передний план, если оно скрыто за основной формой.

Ответы [ 3 ]

5 голосов
/ 25 марта 2012

Сначала ответим на вопрос:

Вы можете захватить мышь, когда она выходит за пределы диалогового окна или когда уже находится вне диалогового окна при отображении. Затем вы можете поймать 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;

В заключение:

Теперь, это все еще не работает для сообщений или системных диалогов (хотя вы можете использовать эти хорошие диалоги , которые делают), и я должен согласиться с Дэвидом, чтобы выяснить, почему модальный диалог становится запутанным. Если у вас есть формы с 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.

3 голосов
/ 25 марта 2012

То, что вы просите, нелегко достичь. Я создал простой проект с двумя формами: основной формой и модальной формой. Затем я проследил сообщения (используя 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 голосов
/ 25 марта 2012

Я не уверен, как это сделать в 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 () ивозможно Управление подклассами , которое может указать вам правильное направление.

...