65.9K
CodeProject 正在变化。 阅读更多。
Home

KillTT:告别工具提示!

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.76/5 (11投票s)

2005年9月27日

5分钟阅读

viewsIcon

96559

downloadIcon

630

高效地禁用系统范围内的工具提示!

引言

我很久以前写了这个程序,终于决定把它发到这里,看看大家的反响。请大家手下留情,这是我的第一篇文章!:)

我经常在annoyances.org上帮助人们解决Windows相关的问题。那里有很多人想摆脱那些可爱的黄色小框,也就是工具提示。虽然它们的“表亲”,气球提示,可以通过修改注册表来禁用,但目前没有已知的方法可以禁用所有的工具提示。这个程序解决了这个问题。我将给出一个简单的概述,说明它是如何工作的,然后让你们自己去尝试。

MSDN 的 Tooltip 文档,我在此过程中一直使用它。

警告:此代码在Windows 98上无法运行,而且我不知道为什么,也不关心。目前,此代码仅在Windows XP上进行过调试。

背景

为了说明我如何得出目前的实现,让我解释一下最初的实现。首先,我搜索了MSDN,找到了TTM_ACTIVATE消息。这似乎正是我需要的。

  • TTM_ACTIVATE 没有指针参数,因此可以跨进程发送。
  • 在大多数情况下,应用程序无需发送此消息。
  • 此消息似乎没有其他副作用。当控件被禁用时,应用程序会认为用户只是没有将鼠标悬停在正确的位置。

最终,我确实选择了使用此消息来禁用工具提示控件。

问题所在

枚举窗口并禁用所有工具提示控件只是第一步。随着新应用程序的启动,会创建新的工具提示控件。此外,Windows资源管理器在任务栏添加新窗口的按钮时,也会向其控件发送TTM_ACTIVATE消息。因此,问题出现了:拦截窗口的创建和TTM_ACTIVATE消息的发送。我立即排除了轮询作为有效但资源消耗过大的方法。一个系统有数百个窗口,跨进程复制窗口的类名是非常昂贵的。因此,我选择了这种方法:可以通过SetWindowsHookEx注册一个系统范围的钩子函数。此钩子可以拦截许多事件,包括窗口创建以及所有发送或发布的*消息。我设置了两个钩子:

  • 一个WH_CBT钩子,用于拦截窗口创建。
  • 一个WH_CALLWNDPROC钩子,用于拦截TTM_ACTIVATE消息,如果参数为TRUE,则发送另一条参数为FALSE的消息。

这个系统运行得还可以,但也存在性能问题。

解决方案

让我们从头开始逐步了解当前的实现。用户界面是一个系统“托盘”中的图标,点击它可以启用或禁用所有阻塞。应用程序启动时,会枚举所有窗口,并向工具提示窗口发送TTM_ACTIVATEFALSE)。

BOOL CALLBACK KillTT_EnableTooltips(HWND hwnd,LPARAM lParam)
{
    CHAR buf[256];
    GetClassName(hwnd,buf,256);
    if(lstrcmp(buf,TEXT("tooltips_class32"))==0)
    {
        PostMessage(hwnd,TTM_ACTIVATE,lParam,0);
    }
    EnumChildWindows(hwnd,&KillTT_EnableTooltips,lParam);
    return TRUE;
}

// ...


    KillTT_Hook();

// ...


    KillTT_SetBlock(kill);
    if(kill)
        EnumWindows(&KillTT_EnableTooltips,0);

KillTT_Hook只是注册了一个系统范围的WH_CBT钩子。现在,当一个窗口被创建时,钩子DLL会被加载到创建该窗口的进程中,并且钩子函数CBTHook会被调用。但首先,当DLL加载时,

extern "C" BOOL WINAPI _DllMainCRTStartup(
                               HANDLE  hDllHandle,
                               DWORD   dwReason,
                               LPVOID  lpreserved)
{
    if(dwReason == DLL_PROCESS_ATTACH)
    {
        if(hInstance == NULL)
            hInstance = (HINSTANCE)hDllHandle;

        // Allocate a flag that determines if 

        // this DLL is still loaded

        bDLLPresent = (BOOL*)
            HeapAlloc(GetProcessHeap(), 0, sizeof(BOOL));
        *bDLLPresent = TRUE;

        DWORD dwThreadID;

        // Load ourselves.

        HMODULE hmSelf = LoadLibrary("killtt_helper.dll");

        // Create a monitor thread to unload us.

        CreateThread(NULL, 1024, &WaitForUnload, 
                                  hmSelf, 0, &dwThreadID);
    }
    if(dwReason == DLL_PROCESS_DETACH)
    {
        *bDLLPresent = FALSE;
    }

    return TRUE;
}

几点重要说明

  • 当钩子被调用时,DLL会被加载,函数被运行,然后DLL被卸载。正如你所能想象到的,如果DLL中的代码在此时正在运行,这可能会带来一些棘手的问题!因此,我确保DLL在我准备卸载它之前一直保留在内存中。
  • 必须有一个标志来指示DLL是否存在,但它不能是DLL的数据段的一部分,否则它会随着DLL一起被卸载。因此,它被分配在堆上。

当调用钩子函数时,它会检查其参数。如果正在创建一个工具提示窗口,它会使用标准的子类化技术,但有一个诀窍:它不是使用DLL中的函数,而是在堆栈上创建了一个最初用汇编编写的存根,并将窗口过程设置为该存根。该存根确保DLL存在,然后确保阻塞已启用。如果两者都为真,则会检查消息及其参数。如果消息是TTM_ACTIVATETTM_POPUPTTM_TRACKACTIVATETTM_TRACKPOSITIONTTM_RELAYEVENT,则会将其丢弃。否则,会将其传递给原始的Windows过程。(请注意,在整个过程中,我使用了占位符地址。另外请注意,我不是汇编大师。我讨厌汇编,并且写得很差。)

    // lParam
    // wParam
    // uMsg
    // hwnd
    // Return Address
    // Old EBP             <-EBP
        push ebp

        mov eax, 0xFFFFFFFF // DllPresent flag
        cmp dword ptr[eax], 0
        je retold
        mov eax, 0x88888888 // Hooking flag
        cmp dword ptr[eax], 0
        je retold

        mov ebp, esp
        mov eax, dword ptr[ebp + 12]

        cmp eax, TTM_POPUP
        je ret0
        cmp eax, TTM_TRACKACTIVATE
        je ret0
        cmp eax, TTM_TRACKPOSITION
        je ret0
        cmp eax, TTM_RELAYEVENT
        je ret0
        cmp eax, TTM_ACTIVATE
        je ret0

retold:
        pop ebp
        pop eax
        push 0xAAAAAAAA    // Old window proc
        push eax
        mov eax, 0xBBBBBBBB  // CallWindowProc
        jmp eax

ret0:
        xor eax, eax
        pop ebp
        ret 16

我很快意识到一个小缺陷。随着多个工具提示窗口的创建和销毁,堆上会留下少量内存。如果大量工具提示窗口被创建和销毁,这个小的资源泄漏可能会成为一个大问题。因此,ttproc.asm中包含的代码显示了汇编函数的完整源代码,其中包括一个分支,在处理完WM_NCDESTROY(发送给窗口的最后一个消息)后返回时删除该过程。

我学到的东西

  • 如果使用不当,通过SetWindowsHooksEx设置的钩子会对性能产生重大影响。
  • 钩子DLL仅在钩子函数期间加载。不要在其他任何时候依赖它们的存在!
  • 注释!我的代码需要更多注释!
  • 如果你将代码打包到结构中
    • 测试它!在调试器中逐步执行。字节序可能与你预期的不同,从而导致出现乱码。
    • 不要忘记#pragma pack!操作码之间添加的填充会造成很多麻烦。
    • 始终调用FlushInstructionCache,否则可能会发生可怕的事情!一台机器在没有调用此函数的情况下出现了间歇性崩溃。我花了数小时才调试出来。
    • 第一次就把它做好!结构中的代码很难重新处理。

待办事项

  • 修复Win98的代码。
  • 优化汇编。
  • 修复DLL卸载机制。

欢迎评论或提问!这是一个相当不维护的项目,但如果有什么值得改进的地方,我会更新它。

© . All rights reserved.