我的 PBox 程序中,Dll 动态库窗体无法显示 Hint 提示信息。
只有在鼠标放到主窗体上,并且主窗体成为激活窗体时,才会有 Hint 显示。
我当然希望,无论激活窗体是哪个,都可以显示 Hint 提示信息。
这好像是很多 Dll 窗体的通病。百度、Google,查询了一番,都没有找到现成的答案。
只能自己看 Delphi 源码了。源码基于 Delphi 10.4.2。
一:标准窗体 EXE 程序中的 Hint 流程;
第一阶段:创建 Hint 窗体;
每一个 EXE 窗体程序,程序一创建时,就创建了一个 Hint 窗体。
Vcl.Controls.pas 单元,函数 procedure InitControls 的 16718 行
Application.ShowHint := True;
调用 SetShowHint 函数,创建 Hint 窗体。
THintWindow 类,标准的 win32 窗体。
第二阶段:Hint 窗体的显示;
Delphi 的消息接管。鼠标移动时,程序流动过程:
TControl.WndProc // 消息接管
-------→ WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message) // 鼠标移动消息
-------→ TApplication.StartHintTimer // 开始 Hint 定时器
-------→ SetTimer(0, 0, Value, HintTimerDelegate) // 设置 Hint 定时器
-------→ procedure HintTimerProc(Wnd: HWnd; Msg: UINT; TimerID: UINT_PTR; SysTime: DWORD); stdcall; // Hint 定时器过程
-------→ Application.HintTimerExpired; // Hint 定时器工作
-------→ TApplication.ActivateHint // 激活 Hint 提示
-------→ FHintWindow.ActivateHintData(HintWinRect, HintInfo.HintStr, HintInfo.HintData); // 激活 Hint 提示
-------→ THintWindow.ActiveHint // 显示 Hint 提示
这样,原来已经创建好的 Hint win32 窗体,就被显示出来了。
二:DLL 窗体中的 Hint 显示;
DLL 中,Hint 窗体创建、程序流动都是没有问题的。
直到流动到 TApplication.ActivateHint ,Vcl.Forms.pas 单元,12037 行:
if FShowHint and (FHintControl <> nil) and ForegroundTaskCheck(EnumAllWindowsOnActivateHint) and
(FHintControl = GetHintControl(FindDragTarget(CursorPos, True))) and
(FHintControl.CustomHint = nil) then
条件为假,直接跳过,执行 CancelHint 取消显示了。
我们来看看哪个条件为假,造成的跳过。
这里有5个条件判断。
只有这一个条件 ForegroundTaskCheck(EnumAllWindowsOnActivateHint) 为假,其它条件都是真。
那就查看 ForegroundTaskCheck 函数(函数必须要返回真,才能显示 Hint):
function ForegroundTaskCheck(CheckAll: Boolean): Boolean;
var
Info: TCheckTaskInfo;
begin
Info.FocusWnd := GetActiveWindow;
Info.Found := False;
if (CheckAll) then
EnumWindows(@CheckTaskWindowAll, Winapi.Windows.LPARAM(@Info))
else
EnumThreadWindows(GetCurrentThreadID, @CheckTaskWindow, Winapi.Windows.LPARAM(@Info));
Result := Info.Found;
end;
这里 GetActiveWindow 是我们 Dll 主窗体的句柄(当前 Dll 主窗体处于激活状态)。
默认 CheckAll 是 False, 调用:
EnumThreadWindows(GetCurrentThreadID, @CheckTaskWindow, Winapi.Windows.LPARAM(@Info));
这个 CheckTaskWindow 函数的作用:枚举当前线程中的窗体是否包含上面的 GetActiveWindow 窗体。没有返回假,有则返回真。
EnumThreadWindows 函数,功能是枚举一个线程相关的所有非子窗体。即所有顶级窗体。
而我们的 GetActiveWindow 窗体,不是顶级窗体。它是有父窗体的。所以返回假了。
当我们设置:Application.EnumAllWindowsOnActivateHint := True; 时,
CheckAll 就为 True 了,就会调用:
EnumWindows(@CheckTaskWindowAll, Winapi.Windows.LPARAM(@Info)) 了。
EnumWindows 函数是枚举屏幕上的所有顶层窗体。但我们的 GetActiveWindow 窗体,不是顶级窗体。所以也枚举不到。也返回假。
也就时说,EnumWindows,EnumThreadWindows 函数枚举的都是顶层窗体(区别只是有没有指定要枚举的窗体所属的进程或线程)。
而我们的 GetActiveWindow 窗体不是顶层窗体,所以无论如何都枚举不到。都返回假了。
知道了原因,下面就来着手解决问题了。
我们的 GetActiveWindow 窗体有父,这一点是修改不了的。
看样子,只能修改 在 TApplication.ActivateHint 函数了,
将判断激活窗体的 ForegroundTaskCheck(EnumAllWindowsOnActivateHint) 这个去掉,就可以了。
这样无论激活窗体是哪一个,都会有 Hint 显示了。
而且这样修改也没什么影响,毕竟这个函数(ForegroundTaskCheck)只是句柄激活判断,没有什么实质性的内容。
修改 Vcl.Forms.pas 的 TApplication.ActivateHint 函数,12037 行(注释掉 ForegroundTaskCheck 函数调用):
if FShowHint and (FHintControl <> nil) and // ForegroundTaskCheck(EnumAllWindowsOnActivateHint) and
(FHintControl = GetHintControl(FindDragTarget(CursorPos, True))) and
(FHintControl.CustomHint = nil) then
三:如何修改 Delphi 源码;
譬如修改 Vcl.Forms.pas,先将它复制到本工程中,并把它添加到本工程中。
这样编译工程,就会调用本地的 Vcl.Forms.pas,而不是 Delphi 源码目录中的 Vcl.Forms.pas
(当然,Vcl.Forms.pas 比较特殊,它是标准 EXE 窗体程序的第一个单元,无法添加到本地工程中。所以修改,最好放到 DLL 中修改)。
如果想替换 Delphi 源码目录中的 Vcl.Forms.pas,
则先用本工程中修改好的 Vcl.Forms.pas,替换到源码目录中的 Vcl.Forms.pas。
然后将本地工程编译生成的 Vcl.Forms.dcu,替换 {BDS}\lib\win32(win64)\debug(release) 下的 Vcl.Forms.dcu。当然版本要对应。
有源码就是好!