Freigeben über


Message Hooks in Add-ins

Just like my earlier post on message filters, this is an advanced scenario – so be warned: you almost certainly don’t want to do this. However, there are probably some extreme edge-case scenarios where this technique might be useful. For example, Office apps are notoriously parsimonious with their events. I’m sure you can all cite situations where it would be really nice if Office fired an event, but it just doesn’t. In this situation, it might be useful to intercept raw windows messages instead (and it might not – you’d have to think very carefully if this possibility is worth the potential pain of the technique I’m about to describe). Another scenario where this might be applicable is if you want to show modeless managed dialogs and not have the host app process all the keyboard messages.

You can use the SetWindowsHookEx native API to insert your custom method into the hook chain for a given category of windows messages. Note that inserting a hook obviously slows down the whole system slightly. Note also that if you don’t implement your hook methods in a careful and socially-responsible manner, you risk causing far-reaching damage across the whole system.

Windows maintains an independent hook chain for each type of hook. Each chain is a list of pointers to user-defined callback functions called hook procedures. When a message occurs that is associated with a particular type of hook, Windows passes the message to each hook procedure in the chain, one after the other. For some hooks, the hook procedure can only read messages; others can modify messages or prevent them proceeding on through the chain. Here’s a concrete example: suppose you want to prevent your users from invoking the VB Editor in a given Office app? One way you could do this is by inserting a keyboard hook that intercepts the Alt-F11 keystroke message and does not forward the message on down the chain.

Another scenario where a message hook might be useful is where you use modeless dialogs or custom propertysheets in your add-in. In this scenario, the main window (the Office window) still owns the message pump, so, all the keystroke messages are taken by the host application and not dispatched to the modeless dialog box or propertysheet window. (This doesn’t happen with modal dialogs, because these get their own message pump.)

The main MSDN documentation on hooks is here. The basic set of operations is as follows:

· Write a method with the prescribed signature that will handle the messages you’re interested in, including (most often) forwarding the message down the chain with CallNextHookEx.

· Insert your method into the hook chain with SetWindowsHookEx.

· When you’re done, remove your method from the chain, with UnhookWindowsHookEx.

Let’s look at this in more detail with a practical example. First, declare an int to hold the return from SetWindowsHookEx – this will be the global handle to your hook, and we’ll need this later when we want to call UnhookWindowsHookEx. Also declare a delegate type for your callback, and a variable to hold an instance of this type.

private int hookHandle = 0;

private delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

private HookProc messageHookProcedure;

Next, import all the Windows API methods you’ll need, including: SetWindowsHookEx to install the hook; UnhookWindowsHookEx to uninstall the hook; and CallNextHookEx to pass the hook information to the next hook procedure in the chain. Note that SetWindowsHookEx takes a pointer to a callback as one of its parameters – in managed code, of course, this translates to a delegate:

[DllImport("user32.dll", CharSet = CharSet.Auto)]

private static extern int SetWindowsHookEx(

    int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

[DllImport("user32.dll", CharSet = CharSet.Auto)]

private static extern bool UnhookWindowsHookEx(int idHook);

[DllImport("user32.dll", CharSet = CharSet.Auto,

 CallingConvention = CallingConvention.StdCall)]

private static extern int CallNextHookEx(

    int idHook, int nCode, IntPtr wParam, IntPtr lParam);

Also, define the managed equivalent of the MSG struct, and it’s nested POINT type:

[StructLayout(LayoutKind.Sequential)]

private struct POINT

{

    public int x;

    public int y;

}

[StructLayout(LayoutKind.Sequential)]

private struct MSG

{

    internal int hwnd;

    internal uint message;

    internal uint wParam;

    internal int lParam;

    internal uint time;

    internal POINT pt;

}

To set the hook, instantiate your delegate and call SetWindowsHookEx. You have to specify the type of hook you want to install. In this example, I’m setting a hook for generic windows messages, using the constant WH_GETMESSAGE, defined in winuser.h. When you set up this type of message hook, you have to pass the current thread ID. If this parameter is zero, the hook procedure is associated with all existing threads running in the same desktop as the calling thread, which is not usually what you want. Note that using System.AppDomain.GetCurrentThreadId procudes a compiler warning: “GetCurrentThreadId has been deprecated because it does not provide a stable Id when managed threads are running on fibers. To get a stable Id for a managed thread, use the Thread.ManagedThreadId property.” However, in the context of Office add-ins, we don’t care about fibers, and anyway, passing the Thread.ManagedThreadId to SetWindowsHookEx always fails. (I don’t know why this is.)

internal void SetMessageHook()

{

    const int WH_GETMESSAGE = 3;
    messageHookProcedure = new HookProc(MessageHookProc);
hookHandle =
SetWindowsHookEx(WH_GETMESSAGE, messageHookProcedure,
(IntPtr)0, AppDomain.GetCurrentThreadId());
}

When you instantiate your delegate, you initialize it to your custom callback method. Bear in mind that the types of operation you can legally perform in this callback depend on the type of the message hook. In this case, with a generic message hook, we have a fair amount of leeway, but there are still some restrictions. First, if the first parameter (nCode) < 0, the hook procedure must pass the message to CallNextHookEx without further processing, and should return the value returned by CallNextHookEx. Secondly, we only want to process the message if it has not already been removed from the queue, so we need to compare the wParam against PM_NOREMOVE. For this type of message hook callback, the lParam that Windows passes us is a pointer to a MSG struct, and we can retrieve this by using Marshal.PtrToStructure. In the following example, I’m simply extracting the message values and putting them into a ListBox in a custom taskpane. Finally, make sure we don’t break the hook chain, by calling CallNextHookEx:

private int MessageHookProc(int nCode, IntPtr wParam, IntPtr lParam)

{

    if (nCode < 0)

    {

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

    else

    {

        const int PM_NOREMOVE = 0;

        if (wParam.ToInt32() == PM_NOREMOVE)

        {

            MSG msg =

                (MSG)Marshal.PtrToStructure(lParam, typeof(MSG));

            listBox.Items.Add(String.Format("{0}: {1},{2}",

                (WM)msg.message, msg.pt.x, msg.pt.y));

            listBox.SelectedIndex = listBox.Items.Count - 1;

        }

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

}

Note that I’m using a custom enum (WM) based on a listing I found on pinvoke.net:

public enum WM : uint

{

    NULL = 0x0000,

    CREATE = 0x0001,

    DESTROY = 0x0002,

    MOVE = 0x0003,

    SIZE = 0x0005,

    //... lots more message IDs

    SYSTIMER = 0x118

}

Don’t forget to make sure you remove the hook whenever you don’t need it any more, and before you exit. You could implement a suitable method, and make sure to call it (for instance) in the ThisAddIn_Shutdown method:

internal void ClearHook()

{

    if (hookHandle != 0)

    {

        bool ret = UnhookWindowsHookEx(hookHandle);

        if (ret == false)

        {

            Debug.WriteLine("UnhookWindowsHookEx Failed");

            return;

        }

        hookHandle = 0;

    }

}

That’s pretty much it. The implementation would be very similar for all types of hook, with minor differences. For example, to hook only mouse messages, you’d declare a managed equivalent of the MOUSEHOOKSTRUCT defined in winuser.h – note that this also uses the POINT struct type. You can use the same HookProc delegate type, the same hook handle, and the same ClearHook method to call UnhookWindowsHookEx. To set up the hook, specify WH_MOUSE in the call to SetWindowsHookEx:

[StructLayout(LayoutKind.Sequential)]

private class MouseHookStruct

{

    internal POINT pt;

    internal int hwnd;

    internal int wHitTestCode;

    internal int dwExtraInfo;

}

internal void SetMouseHook()

{

    const int WH_MOUSE = 7;
mouseHookProcedure = new HookProc(MouseHookProc);
hookHandle =
SetWindowsHookEx(WH_MOUSE, mouseHookProcedure,
(IntPtr)0, AppDomain.GetCurrentThreadId());
}

The mouse hook callback is very similar to the generic message callback, except that the lParam that Windows passes us in this case is a pointer to a MOUSEHOOKSTRUCT:

private int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)

{

    if (nCode < 0)

    {

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

    else

    {

        MouseHookStruct mouseHookStruct =

            (MouseHookStruct)

            Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));

        // Compose a string that shows the current mouse message Id

        // and coordinates.

        string mouseMessage = string.Format("{0}: x={1},y={2}",

            ((WM)wParam.ToInt32()),

            mouseHookStruct.pt.x, mouseHookStruct.pt.y);

        // Put the mouse message values into the listbox in the

        // taskpane, and scroll to the bottom.

        listBox.Items.Add(mouseMessage);

        listBox.SelectedIndex = listBox.Items.Count - 1;

        // Ensure that we don't break the hook chain.

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

}

To hook only keyboard messages, you’d declare a managed equivalent of the KBDLLHOOKSTRUCT defined in winuser.h. Again, you can use the same HookProc delegate type, the same hook handle, and the same ClearHook method to call UnhookWindowsHookEx. To set up the hook, specify WH_KEYBOARD in the call to SetWindowsHookEx. In this case, you pass the module handle for the current process, which you can get with the GetModuleHandle API:

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]

private static extern IntPtr GetModuleHandle(string lpModuleName);

[StructLayout(LayoutKind.Sequential)]

private struct KbDllHookStruct

{

    internal int vkCode;

    internal int scanCode;

    internal int flags;

    internal int time;

    internal int dwExtraInfo;

}

internal void SetKeyboardHook()

{

   const int WH_KEYBOARD_LL = 13;
    keyboardHookProcedure = new HookProc(KeyboardHookProc);

    using (Process curProcess = Process.GetCurrentProcess())

    using (ProcessModule curModule = curProcess.MainModule)

    {

        hookHandle =

            SetWindowsHookEx(WH_KEYBOARD_LL, keyboardHookProcedure,

            GetModuleHandle(curModule.ModuleName), 0);

    }

}

The keyboard hook callback is very similar to the generic/mouse message callbacks, except that the lParam that Windows passes us in this case is a pointer to a KBDLLHOOKSTRUCT. In the following implementation, in addition to logging the message information in my ListBox, I’m also trapping specifically Alt-F11 keystrokes – when I get this message, I simply return 1, which prevents the message from being propagated further down the chain:

private int KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)

{

    if (nCode < 0)

    {

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

    else

    {

      if ((wParam == (IntPtr)WM.KEYDOWN)

            || (wParam == (IntPtr)WM.SYSKEYDOWN))

        {

            KbDllHookStruct kbDllHookStruct =

                (KbDllHookStruct)

                Marshal.PtrToStructure(lParam, typeof(KbDllHookStruct));

            // Put the message values into the listbox in the

            // taskpane, and scroll to the bottom.

            listBox.Items.Add(String.Format(

                "{0}: {1}", (WM)wParam, (Keys)kbDllHookStruct.vkCode));

            listBox.SelectedIndex = listBox.Items.Count - 1;

            // Supress Alt-F11.

            if (wParam == (IntPtr)WM.SYSKEYDOWN

                && kbDllHookStruct.vkCode == (int)Keys.F11)

            {

                return 1;

            }

        }

        return CallNextHookEx(hookHandle, nCode, wParam, lParam);

    }

}

That’s it. The sample solution attached to this post includes all 3 hook types described. Drive safe now.

WordAddInHook.zip

Comments

  • Anonymous
    February 24, 2009
    PingBack from http://www.clickandsolve.com/?p=14043
  • Anonymous
    February 26, 2009
    Note that this injects the CLR into the target process, which can be a serious problem if the target process is using (or plans to us) a version of the CLR different from yours.
  • Anonymous
    February 26, 2009
    oldnewthing - you're correct - and I should have made it clear that this posting is in the context of a managed add-in - in which case, the specific CLR used by the add-in is already running in the process. The attached sample solution is a VSTO Word add-in.