KillTT:告别工具提示!






3.76/5 (11投票s)
2005年9月27日
5分钟阅读

96559

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_ACTIVATE
(FALSE
)。
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_ACTIVATE
、TTM_POPUP
、TTM_TRACKACTIVATE
、TTM_TRACKPOSITION
或TTM_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卸载机制。
欢迎评论或提问!这是一个相当不维护的项目,但如果有什么值得改进的地方,我会更新它。