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