Delphi Dll 动态库窗体中无法显示 Hint 问题的研究和解决

我的 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。当然版本要对应。

有源码就是好!
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dbyoung

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值