Introduction
I wrote this program some time back, and finally decided to post it here and see how it is received. Please be gentle, this is my first article! :)
I routinely help people with Windows-related problems on annoyances.org. Several people there wanted to be rid of those wonderful little yellow boxes called tooltips. While their larger cousins, balloon tips, can be disabled via a registry modification, no known way exists to disable all tooltips. This program solves that problem. I will give you a bare-bones overview of how it works and let you play around with it yourself.
MSDN's Tooltip documentation, which I used throughout.
Warning: This code breaks on Windows 98, and I have no clue why, nor do I care at this point. Currently, this code is only debugged on Windows XP.
Background
To help show how I arrived at my current implementation, let me explain the initial implementation. First, I searched MSDN and found the TTM_ACTIVATE
message. This appeared to be exactly what I needed:
TTM_ACTIVATE
has no pointer parameters, and thus may be sent cross-process.
- In most cases, the application will have no need to send this message.
- This message doesn’t appear to have any side effects. When the control is disabled, the application thinks that the user just hasn’t hovered the mouse over the right spot.
In the end, this message was indeed the means I chose to use to disable tooltip controls.
The problem
Enumerating windows and disabling all tooltip controls is only the beginning. As new applications are started, new tooltip controls are created. In addition, Windows Explorer also sends a TTM_ACTIVATE
message to its control whenever a new window’s button is added to the task bar. Hence the problem: Intercept the creation of windows and the sending of TTM_ACTIVATE
messages. I rejected polling immediately as effective, but too resource-hungry. A system has hundreds of windows, and copying the class name of a window across process boundaries is quite expensive. Thus I settled on this approach: A system-wide hook function can be registered via SetWindowsHookEx
. This hook can intercept a number of events, including window creation and every sent or posted message. I set up two hooks:
- A
WH_CBT
hook to intercept window creation.
- A
WH_CALLWNDPROC
hook to intercept TTM_ACTIVATE
and, if the parameter is TRUE
, post another message with a parameter of FALSE
.
This system worked all right, but also suffered from performance problems.
The solution
Let’s walk through the current implementation from the beginning. The user interface is an icon in the system "tray" which, when clicked, either enables or disables all blocking. When the application is started, all windows are enumerated, and tooltip windows are sent a 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
simply registers a system-wide WH_CBT
hook. Now, when a window is created, the hook DLL is loaded into the process that is creating the window, and the hook function CBTHook
is called. First, however, when the DLL is loaded,
extern "C" BOOL WINAPI _DllMainCRTStartup(
HANDLE hDllHandle,
DWORD dwReason,
LPVOID lpreserved)
{
if(dwReason == DLL_PROCESS_ATTACH)
{
if(hInstance == NULL)
hInstance = (HINSTANCE)hDllHandle;
bDLLPresent = (BOOL*)
HeapAlloc(GetProcessHeap(), 0, sizeof(BOOL));
*bDLLPresent = TRUE;
DWORD dwThreadID;
HMODULE hmSelf = LoadLibrary("killtt_helper.dll");
CreateThread(NULL, 1024, &WaitForUnload,
hmSelf, 0, &dwThreadID);
}
if(dwReason == DLL_PROCESS_DETACH)
{
*bDLLPresent = FALSE;
}
return TRUE;
}
Several important notes:
- When a hook is called, the DLL is loaded, the function run, and the DLL is unloaded again. As you might imagine, this can have some nasty consequences if code from the DLL was running at the time! Thus, I make sure the DLL remains in memory until I’m ready to unload it.
- There must be a flag to tell whether the DLL is present, but it cannot be part of the data segment of the DLL, or it will be unloaded with the DLL. Thus, it is allocated on the heap.
When the hook function is called, it checks its parameters. If a tooltip window is being created, it uses standard subclassing techniques with a twist: Rather than using a function from the DLL, a stub, originally written in assembly, is created on the stack and the window procedure is set to that stub. The stub makes sure the DLL is present, and then that blocking is enabled. If both are true, the message and its parameters are checked. If the message is TTM_ACTIVATE
, TTM_POPUP
, TTM_TRACKACTIVATE
, TTM_TRACKPOSITION
, or TTM_RELAYEVENT
, it is discarded. Otherwise, it is passed on to the original Windows procedure. (Note that throughout, I use placeholder values for addresses. Also note that I am not an assembly guru. I hate assembly, and write it poorly.)
push ebp
mov eax, 0xFFFFFFFF
cmp dword ptr[eax], 0
je retold
mov eax, 0x88888888
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
push eax
mov eax, 0xBBBBBBBB
jmp eax
ret0:
xor eax, eax
pop ebp
ret 16
I quickly realized a small flaw. As multiple tooltip Windows are created and destroyed, a small chunk of memory is left behind on the heap. If many tooltip Windows are created and destroyed, this small resource leak could become a big problem. Thus, the code included in ttproc.asm shows the full source of the assembler function, which includes a branch that deletes the procedure upon return from processing of WM_NCDESTROY
, the last message sent to a window.
What I learned
- Hooks set via
SetWindowsHooksEx
have a major performance impact if used incorrectly.
- Hook DLL’s are loaded only for the duration of the hook function. Don’t rely on their presence at any other time!
- Comments! I need more comments in my code!
- If you’re packing code into a structure:
- Test it! Step through in a debugger. Byte order may be different than you expect, resulting in nonsense.
- Don’t forget #pragma pack! Padding added in between opcodes really messes things up.
- Always call
FlushInstructionCache
, or bad things may happen! One machine exhibited sporadic crashes without this call. This took me hours to debug.
- Get it right the first time! Code in structures is hard to rework.
TODO
- Fix code for Win98.
- Optimize assembly.
- Fix DLL unloading mechanism.
Comments or questions appreciated! This is a fairly un-maintained project, but I will update it if something warrants the change.