Click here to Skip to main content
Click here to Skip to main content

Using Window Messages to Implement Global System Hooks in C#

, 3 May 2007
Rate this:
Please Sign up or sign in to vote.
Explains how to implement any type of global system hook in C# by using an unmanaged C++ DLL and Windows messages.

Screenshot - WilsonGlobalSystemHooks1.jpg

Screenshot - WilsonGlobalSystemHooks1.jpg

Introduction

Global system hooks allow an application to intercept Windows messages intended for other applications. This has always been difficult (impossible, according to MSDN) to implement in C#. This article attempts to implement global system hooks by creating a DLL wrapper in C++ that posts messages to the hooking application's message queue. Put simply, this lets you implement any type of global Windows hook from managed code.

Why Global System Hooks and C# Don't Play Well Together

Global system hooks have always been a problem for .NET developers. To understand why, we first need to discuss how hooks work at a system level and how Windows processes these event notifications.

Normally, an application implements hooks by creating a callback function and informing Windows of this function. When Windows processes a hookable event, such as mouse movement or the creation of a window, it calls this callback function, which the hooking application handles accordingly. With local hooks (when an application hooks other events taking place in the same process), this callback function can be located anywhere, and local hooks can be written pretty easily in C# -- a quick look around CodeProject reveals several examples. Unfortunately, Windows is not so permissive with global system hooks, which allow one process to intercept events from another process. Windows insists that the callback functions for global system hooks be located in DLLs. Neither C# nor VB.NET are able to produce standard Windows DLLs, which means they're also unable to process global system hooks -- the MSDN library even explicitly says global system hooks cannot be handled within managed code applications. How discouraging!

Luckily, C# programmers are a clever and crafty bunch. Michael Kennedy, in his CodeProject article "Global System Hooks in .NET", came up with a workaround. Using C++, he created a DLL that essentially worked as a wrapper to managed code. The C++ DLL registered a callback function with Windows. When a hook notification arrived in that callback, it called a managed code function. Using this technique, he was able to trap several hook notifications, such as low-level keyboard and mouse events. The problem he discovered, though, was that Windows changes the execution context when sending out certain hook notifications. When Windows hooked a low-level mouse or keyboard event, it called any hooking application from within the context of the hooking application. But for most other events, the callback function was called from the context of the hooked application. This essentially meant that his wrapper functions couldn't be used with most Windows hooks.

The Workaround

I've created my own workaround to this problem, although as you'll see below, it comes with some definite caveats. In a nutshell, what I've done is (like Kennedy) to create a C++ DLL which contains the actual callback function. However, the DLL doesn't just serve as a wrapper to a managed code delegate. Instead, when it receives a hook notification, it posts a custom Windows message to the managed application's Windows message queue. This message can then be intercepted in the C# program's WndProc event. So, just as a window regularly receives messages like WM_MOUSEMOVE, your managed application will now also receive messages like WILSON_HOOK_SHELL_WINDOWCREATED.

The Projects

The code consists of three projects. First, there's the GlobalCbtHook, a C++ project which creates the DLL used for the callback functions. Second, there's GlobalHooksTest, a simple application designed to demonstrate this code, which implements WH_SHELL and WH_CBT hooks. The third (and coolest) of the three projects is TransparencyMenu. This is also a demonstration project, showing an actual practical application of global system hooks.

GlobalCbtHook -- the DLL Project

This project is deceptively simple. There are, however, a few quirks to my programming, which have created the potential for some bugs in your application, as described below. One other note: with some minor exceptions, all the similarly-named functions in the DLL do the same thing, just for different types of hooks. That is, InitializeCbtHook and InitializeShellHook work in the same way -- so in my explanations below, I use ABC as a sort of generic hook.

Warning One: SetProp and GetProp

Because Windows injects the callback function into all running processes, there will always be multiple instances of this code loaded. Yet they all need to share at least one piece of information, namely, the handle to the window that needs to receive the hook event notifications. After several frustrating hours reading up on shared memory, I decided I wanted an easier solution. That's when the MSDN documentation threw me a lifesaver in the form of GetProp and SetProp.

GetProp and SetProp are API functions that allow you to associate any type of handle with any window. That is, if you have a window handle (hWnd), you can associate any other handle with it, and in such a way that any application can access that data. Luckily, there's one window handle that's essentially constant in any Windows session, the desktop handle. So, in order to store the handle to the window receiving hook events, I use the desktop window's properties. This is kind of a hack, but works pretty reliably and effectively. The InitializeAbcHook functions checks to see if there is already a handle named WILSON_HOOK_HWND_ABC already associated with the desktop window. Assuming there's not, this handle is set to the handle of the window that will receive the hook events.

This code is in InitializeCbtHook, showing GetProp:

if (GetProp(GetDesktopWindow(), "WILSON_HOOK_HWND_CBT") != NULL)
{
  SendNotifyMessage((HWND)GetProp(GetDesktopWindow(), 
      "WILSON_HOOK_HWND_CBT"), 
      RegisterWindowMessage("WILSON_HOOK_CBT_REPLACED"), 0, 0);
}

SetProp(GetDesktopWindow(), "WILSON_HOOK_HWND_CBT", destination);

And SetProp is used in CbtHookCallback to retreive the window handle:

HWND dstWnd = (HWND)GetProp(GetDesktopWindow(),
               "WILSON_HOOK_HWND_CBT");

There is one major problem to this approach: it means that only one application can be hooking these events at a time. When a second application (that uses the same DLL) starts hooking events, a message (WILSON_HOOK_CBT_REPLACED) is sent to the first application, informing it that it's now out of luck and no longer hooking anything. So, if you use this code in your own software, it'd be a good idea to recompile it, changing the WILSON_HOOK strings to something different, so that your application will not conflict with any other software using this approach.

Warning Two: Return Values

The other major problem with my particular approach to hooking is that it doesn't allow you to return a value when a hook callback is called. Normally, a hook callback function is allowed to return a value, which can affect whatever event is happening that generated the hook. For instance, when the CBT CREATEWND hook is raised, setting a non-zero return value prevents the window from being created. This functionality could be used to prevent certain classes or types of windows from being created. However, in its current form, my solution doesn't allow you to set a return value. There is, theoretically, a fairly easy workaround for this. I use the SendNotifyMessage in the C++ DLL to send the message to the hooking application, which means the DLL just posts a message and then doesn't wait around to see if the hooking application actually processes it. However, you could instead use the SendMessage command, which forces the DLL to wait for the hooking application to process the hook and return the value set in Message.Result in the managed code. The downside to this approach, and the reason I didn't implement it this way, is that it can very easily lead to system crashes. If your managed code encounters an unhandled exception, execution won't return to the calling SendMessage function, which in turn hangs the application being hooked. A dozen hard reboots in the span of about 20 minutes convinced me of the wisdom of SendNotifyMessage.

The RegisterWindowMessage Function

My code uses the RegisterWindowMessage function. This is an API function which generates a unique message ID that can be used among multiple applications. When one application registers a message with a certain name, any other application that registers a message of the same name will get the same message ID in response. All the messages posted by the DLL use message IDs generated by this function. Thus, managed code must also call this function to know what message IDs to watch for in WndProc.

Inside the DLL

The C++ DLL contains code to handle eight different types of hooks. If you are so inclined, you should be able to follow the model and easily create additional code to handle the other hook types. For each type of hook, there are three functions in the DLL. These three functions are more-or-less the same between all the different hook types.

InitializeAbcHook is an exported function that's called from managed code. The first parameter is the ID of the thread to hook. Setting this to zero will hook all processes -- giving you a global hook. You could also use this to only hook events from one thread, which in many scenarios would be far more efficient than hooking events in all threads. The second parameter is the handle of the window that hook events will be broadcast to. This should be the handle of a form or a control. The InitializeAbcHook function checks to see if a message handle has already been stored (a.k.a., another application is already hooking using our DLL), then stores this handle, and finally installs the hook using this line:

hookAbc = SetWindowsHookEx(WH_ABC, (HOOKPROC)AbcHookCallback, 
                                g_appInstance, threadID);

UninitializeAbcHook just removes the hook. Make sure your managed code application calls this before quitting -- if you don't, the DLL will keep hooking, even though your application (that's supposed to receive the hooks) has ended. This can lead to some major problems, as well as needlessly degrades performance.

AbcHookCallback is the actual callback function, and varies a bit depending on the hook type. For the simplest hooks, like mouse and keyboard hooks, this function just sends the necessary message to the queue of the hooking application, and then continues the hook chain by calling CallNextHookEx. For hooks like CBT or Shell, there are multiple types of events that could be occuring -- such as ACTIVATE or CREATEWND. The callback function determines which type of sub-hook is being called, and then raises the appropriate event. For CallWndProc and GetMsg, there is too much data to send to the hooking application using only one message. For these two hooks, after all, there are four pieces of data we want to know in our managed application: the handle of the hooked window, the message that was hooked, and the lParam and wParam values -- however, when sending our own messages to the hooking application, we can only send data in lParam and wParam. So these two callback functions send two messages, one with the handle and intercepted message, the other with the intercepted lParam and wParam. They also have a check to prevent infinite recursion -- GetMsg sends a message to the hooking application, which is in turn intercepted by the same callback function that posted the message.

The Managed Code: GlobalHooks.cs

I've created a class called GlobalHooks which functions as an intermediary between a managed application and the DLL, making it easy to implement global hooks in your own projects. The class contains an abstract internal class called Hook, as well as eight subclasses of Hook, one for each type of hook supported.

To use global system hooks in your own code, the first thing to do is create an instance of the GlobalHooks class:

    private GlobalHooks _GlobalHooks;

When you instantiate this class, you must pass it a window handle:

    _GlobalHooks = new GlobalHooks(this.Handle);

You also need to override the WndProc function to allow the GlobalHooks function to process all incoming messages. This ProcessWindowMessage function checks for messages coming from the DLL, indicating a hook notification has been raised.

    protected override void WndProc(ref Message m)
    {
        if (_GlobalHooks != null)
            _GlobalHooks.ProcessWindowMessage(ref m);

        base.WndProc (ref m);
    }

Next, you need to create some event handlers for whatever you want to hook. For instance, to receive notification every time a top-level window is created, create an event handler for the GlobalHooks.Shell.WindowCreated, as follows:

    _GlobalHooks.Shell.WindowCreated += 
               new GlobalHooks.WindowEventHandler(Shell_WindowCreated);

To begin hooking events, you must call the Start function in the appropriate class.

    _GlobalHooks.Shell.Start();

Just as important are the corresponding Stop functions, which must be called when you want to stop hooking events. Make sure you stop hooking events before your application terminates.

    _GlobalHooks.Shell.Stop();

That's all it takes!

Final Thoughts

Despite the potential pitfalls, the technique described here has worked quite well for me when I've needed to hook global events. However, if you're creating an application that only needs to hook low-level mouse or keyboard events, Michael Kennedy's code might be better suited for you. Also, the CallWndProc and GetMsg hooks are both pretty inefficient, as they essentially duplicate every message generated by Windows (creating a copy to send to the hooking application). If your application only needs to hook one or two messages, you can easily rewrite the DLL to only post notification messages to the hooking application when the hooked program receives those one or two messages you're looking for.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

Share

About the Author

ChrisP1118

Burkina Faso Burkina Faso
I currently live in a small town in Burkina Faso, where I spend some of my free time (during the hours when there's electricity) playing around with C#. For a bit more about my less nerdy activities, check out http://chrisburkina.blogspot.com or http://www.youtube.com/watch?v=Rw1Pb7hlIWw -- neither of them, however, has anything to do with C#.

Comments and Discussions

 
AnswerTo make it work for Windows 7 x86 or x64 PinmemberGeorge Jonsson3-Jul-14 21:19 
QuestionHere is a Shared Memory Version PinmemberCainoStukov11-Feb-14 3:36 
AnswerRe: Here is a Shared Memory Version [modified] Pinmembersamidi28-Mar-14 11:31 
GeneralGreat Article and I've just added several menu items and support for x86 and x64 processes PinmemberAlexanderPro25-Dec-13 4:19 
GeneralMy vote of 4 PinmemberSivaji156526-Sep-13 23:26 
QuestionHook the Windows Key PinmemberBManfred10-Jan-13 23:18 
QuestionMessage has been called twice? PinmemberMember 85635787-Apr-12 20:57 
Questionusing GetDesktopWindow & built-in windows messages PinmemberSzabolcs.Kovacs28-Oct-11 8:19 
AnswerRe: using GetDesktopWindow & built-in windows messages PinmemberSzabolcs.Kovacs28-Oct-11 13:09 
GeneralRe: using GetDesktopWindow & built-in windows messages Pinmemberyousefk16-Feb-12 9:15 
QuestionI have a question... Pinmembermesiasrojo11-Aug-11 0:19 
QuestionGreat Example and I just added support for it to the O2 Platform PinmemberDinis Cruz19-Jun-11 9:49 
GeneralMy vote of 5 Pinmemberbalbi213-Apr-11 19:12 
GeneralMy vote of 5 Pinmemberkoko0721-Feb-11 9:05 
GeneralWM_MENUSELECT messages PinmemberOlivierNice30-Dec-10 3:51 
GeneralMy vote of 5 Pinmembermohammad.abdelaziz.20116-Jul-10 23:46 
GeneralCode does not work on Windows 7 Pinmemberarchitecton2-May-10 1:58 
GeneralRe: Code does not work on Windows 7 [modified] Pinmemberbef29-Jul-11 3:08 
GeneralRe: Code does not work on Windows 7 [modified] PinmemberMember 85260562-Feb-12 1:40 
GeneralRe: Code does not work on Windows 7 [modified] Pinmemberjeho4u11-Jun-12 15:35 
GeneralRe: Code does not work on Windows 7 [modified] [modified] PinmemberBob V.22-Jun-12 4:29 
GeneralRe: Code does not work on Windows 7 [modified] PinmemberGeorge Jonsson3-Jul-14 20:01 
GeneralOpportunity Pinmembergolum190023-Apr-10 9:19 
GeneralDoes not catch when I run the windows calculator [modified] Pinmemberhrynchuk.anatoliy14-Apr-10 4:21 
QuestionHow to get the structures from lparam [modified] Pinmemberknallton10-Feb-10 0:38 
QuestionAfter using hooks my system get slow down Pinmemberpvkalne30-Nov-09 23:51 
AnswerRe: After using hooks my system get slow down PinmemberDonnyGr9-Nov-10 11:47 
GeneralRe: After using hooks my system get slow down Pinmemberpvkalne10-Nov-10 21:26 
QuestionProblem with WndProcEventHandler PinmemberGrzegorzD21-Nov-09 10:08 
GeneralUsing from within service Pinmemberdaubsi29-Oct-09 12:20 
GeneralNot sure if your still monitoring but question Pinmemberdrewdb9-Sep-09 23:05 
GeneralSimilar concept using .NET was published Pinmembertbenami26-Jul-09 8:29 
GeneralA bug in the demo project [modified] Pinmembertestano23-Jul-09 5:17 
GeneralThe demo code doesn't work on vista [modified] Pinmembertestano23-Jul-09 4:28 
QuestionBlocking Keyboard Input PinmemberNirvanaBoy21-May-09 8:20 
QuestionHooking for a single application PinmemberMember 364369413-May-09 17:41 
GeneralDoesn't work on Windows Vista 64 bits PinmemberRevengerPT20-Mar-09 10:54 
AnswerRe: Doesn't work on Windows Vista 64 bits Pinmembercodesnipper.net28-Mar-09 22:42 
GeneralRe: Doesn't work on Windows Vista 64 bits Pinmembercers12-May-09 7:34 
AnswerRe: Doesn't work on Windows Vista 64 bits PinmemberMember 309455523-May-09 3:48 
GeneralRe: Doesn't work on Windows Vista 64 bits Pinmemberdaubsi29-Oct-09 12:16 
GeneralRe: Doesn't work on Windows Vista 64 bits Pinmembercdebel8-Mar-11 5:09 
GeneralRe: Doesn't work on Windows Vista 64 bits PinmemberThe-Great-Kazoo15-Apr-11 9:01 
GeneralRe: Doesn't work on Windows Vista 64 bits [modified] Pinmemberjellev23-Aug-11 11:03 
GeneralRe: Doesn't work on Windows Vista 64 bits [modified] PinmemberMember 849417413-Feb-12 22:58 
GeneralRe: Doesn't work on Windows Vista 64 bits [modified] Pinmemberjeho4u11-Jun-12 15:37 
GeneralRe: Doesn't work on Windows Vista 64 bits [modified] Pinmemberjellev12-Aug-12 22:09 
Questionlicensing and window open and close Pinmemberstyloverte11619-Jan-09 17:08 
Generalwhy not use #pragma data_seg() to store hwnd Pinmemberbborn15-Dec-08 3:15 
QuestionHooking window messages? PinmvpGiorgi Dalakishvili9-Dec-08 8:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 3 May 2007
Article Copyright 2007 by ChrisP1118
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid