TCommonDialog未触发OnModalBegin/改变ModalLevel,如何检测其是否激活?
首先得明确为什么你原来的方法行不通:TCommonDialog(比如TOpenDialog、TSaveDialog)本质是调用Windows原生的系统对话框,并非VCL自身实现的模态窗口。所以VCL的OnModalBegin/OnModalEnd事件不会触发,Application.ModalLevel也不会有变化——因为这些VCL机制只针对VCL自己的模态窗体(比如TForm.ShowModal)。
下面给你几种实用的解决方案,根据你的场景选择:
方法1:利用对话框的OnShow/OnClose事件(最简单,适合自己维护的对话框)
如果这些对话框是你自己代码里创建和调用的,直接用它们的OnShow和OnClose事件来维护一个状态标志即可。
实现步骤:
- 声明一个全局变量或者主窗体的字段来记录状态:
var g_CommonDialogIsActive: Boolean = False;
- 给每个TCommonDialog派生类的
OnShow和OnClose事件绑定处理函数:
procedure TMainForm.OpenDialogShow(Sender: TObject); begin g_CommonDialogIsActive := True; end; procedure TMainForm.OpenDialogClose(Sender: TObject); begin g_CommonDialogIsActive := False; end;
- 在你拦截键盘事件的逻辑里,直接检查这个变量:
if g_CommonDialogIsActive then begin // 对话框处于激活状态,执行对应的键盘事件处理逻辑 end;
优点:代码简单,无额外开销;缺点:需要给每个对话框手动绑定事件,无法覆盖第三方组件或系统内部调用的对话框。
方法2:使用CBT钩子(通用,覆盖所有对话框)
如果需要检测所有在当前进程中弹出的系统对话框(包括第三方组件调用的),可以用Windows的CBT钩子来监听对话框的创建和销毁。
实现代码:
unit DialogHook; interface uses Windows, Messages; type TDialogHook = class private class var FHookHandle: HHOOK; class var FActiveDialogCount: Integer; class function CBTProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; public class procedure StartMonitoring; class procedure StopMonitoring; class function IsAnyDialogActive: Boolean; end; implementation class procedure TDialogHook.StartMonitoring; begin if FHookHandle = 0 then // 只挂钩当前线程,因为对话框都在主线程弹出 FHookHandle := SetWindowsHookEx(WH_CBT, @CBTProc, 0, GetCurrentThreadId); end; class procedure TDialogHook.StopMonitoring; begin if FHookHandle <> 0 then begin UnhookWindowsHookEx(FHookHandle); FHookHandle := 0; FActiveDialogCount := 0; end; end; class function TDialogHook.CBTProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; var ClassName: array[0..255] of Char; begin // 先调用下一个钩子,保证系统正常工作 Result := CallNextHookEx(FHookHandle, nCode, wParam, lParam); if nCode < 0 then Exit; case nCode of // 窗口创建时触发 HCBT_CREATEWND: begin GetClassName(wParam, ClassName, SizeOf(ClassName)); // #32770是Windows通用对话框的类名 if CompareText(ClassName, '#32770') = 0 then Inc(FActiveDialogCount); end; // 窗口销毁时触发 HCBT_DESTROY: begin GetClassName(wParam, ClassName, SizeOf(ClassName)); if CompareText(ClassName, '#32770') = 0 then Dec(FActiveDialogCount); end; end; end; class function TDialogHook.IsAnyDialogActive: Boolean; begin Result := FActiveDialogCount > 0; end; end.
使用方式:
在程序启动时(比如主窗体的OnCreate事件)调用TDialogHook.StartMonitoring,退出时(OnDestroy)调用TDialogHook.StopMonitoring。需要检查状态时,调用TDialogHook.IsAnyDialogActive即可。
优点:自动检测所有系统对话框,无需手动绑定事件;缺点:需要处理钩子的生命周期,代码稍复杂。
方法3:枚举当前线程窗口(按需检查,无持久钩子)
如果不需要实时监听,只是在拦截键盘事件时偶尔检查状态,可以直接枚举当前线程的窗口,判断是否存在激活的系统对话框。
实现代码:
function EnumThreadWindowsProc(hwnd: HWND; lParam: LPARAM): BOOL; stdcall; var ClassName: array[0..255] of Char; WindowStyle: DWORD; begin Result := True; GetClassName(hwnd, ClassName, SizeOf(ClassName)); // 检查是否是通用对话框类,并且是模态窗口 if CompareText(ClassName, '#32770') = 0 then begin WindowStyle := GetWindowLongPtr(hwnd, GWL_STYLE); if (WindowStyle and WS_MODALFRAME) <> 0 then begin PBoolean(lParam)^ := True; Result := False; // 找到后停止枚举 end; end; end; function IsCommonDialogActive: Boolean; var DialogActive: Boolean; begin DialogActive := False; // 枚举当前线程的所有窗口 EnumThreadWindows(GetCurrentThreadId, @EnumThreadWindowsProc, LPARAM(@DialogActive)); Result := DialogActive; end;
使用方式:
直接在键盘事件拦截逻辑里调用IsCommonDialogActive获取状态即可。
优点:无需钩子,代码独立;缺点:每次检查都要枚举窗口,性能略差(但对键盘事件来说完全可以忽略)。
内容的提问来源于stack exchange,提问作者Martin

