.Net Keyboard Hook Extra KeyUp Event

974 Views Asked by At

I have a business requirement that for message boxes, the user cannot press the enter key to accept the default option, but has to press the key of the option. eg. Given a MessageBox with the options Yes/No, the user must press the Y or N keys. Now I've implemented this below using keyboard hooks, but when the code returns, the KeyUp event also gets returned to the calling code as well.

So the question is: How do I flush all the keyboard events before returning to the calling code?

I've removed boiler plate code, but if you need it, please advise.

The calling code:

    private static ResultMsgBox MsgResultBaseNoEnter(string msg, string caption, uint options)
    {
        ResultMsgBox res;
        _hookID = SetHook(_proc);
        try
        {
            res = MessageBox(GetForegroundWindow(), msg, caption, options);
        }
        finally
        {
            UnhookWindowsHookEx(_hookID);
        }
        return res;
    }

And the Hook Code:

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            if (vkCode == VK_RETURN)
                return (IntPtr)(-1);
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
3

There are 3 best solutions below

1
On BEST ANSWER

Add these lines of code somewhere in your class (or in some static class that can be used by other classes):

[StructLayout(LayoutKind.Sequential)]
public class MSG
{
    public IntPtr hwnd;
    public uint message;
    public IntPtr wParam;
    public IntPtr lParam;
    public uint time;
    int x;
    int y;
}

[DllImport("user32")]
public static extern bool PeekMessage([Out]MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, int wRemoveMsg);

/// <summary>
/// Examines the message queue for key messages.
/// </summary>
/// <param name="remove">If this parameter is true, the returned message is also removed from the queue.</param>
/// <returns>Returns the next available key message, or null if there is no key message available.</returns>
public static MSG PeekKeyMessage(bool remove)
{
    MSG msg = new MSG();
    if (PeekMessage(msg, IntPtr.Zero, 0x0100 /*WM_KEYFIRST*/, 0x0109 /*WM_KEYLAST*/, remove ? 1 : 0))
        return msg;
    return null;
}

public static void RemoveAllKeyMessages()
{
    while (PeekKeyMessage(true) != null) ; // Empty body. Every call to the method removes one key message.
}

Calling RemoveAllKeyMessages() does exactly what you want.

5
On

Actually you can't flush the keyboard events, but you can prevent the event to be received by the thread's message loop.
You should install a handler for WH_GETMESSAGE hook. The lParam of your hook procedure is a pointer to an MSG structure. After examining the structure, you can change it to avoid the message to be passed to the calling message processor. You should change the message to WM_NULL.
The actual procedure in .NET is a little long an requires a separate article. But briefly, here is how:

Copy this class exactly as is, in a new C# file in your project:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Unicorn
{
    public static class HookManager
    {
        #region Fields

        private delegate int HookDelegate(int ncode, IntPtr wParam, IntPtr lParam);
        private static HookDelegate getMessageHookProc;
        private static IntPtr getMessageHookHandle;
        private static List<EventHandler<GetMessageHookEventArgs>> getMessageHandlers =
            new List<EventHandler<GetMessageHookEventArgs>>();

        #endregion
        #region Private Methods - Installation and Uninstallation

        private static void InstallGetMessageHook()
        {
            if (getMessageHookProc != null)
                return;
            getMessageHookProc = new HookDelegate(GetMessageHookProc);
            getMessageHookHandle = SetWindowsHookEx(WH_GETMESSAGE, getMessageHookProc, 0, GetCurrentThreadId());
        }

        private static void UninstallGetMessageHook()
        {
            if (getMessageHookProc == null)
                return;
            UnhookWindowsHookEx(getMessageHookHandle);
            getMessageHookHandle = IntPtr.Zero;
            getMessageHookProc = null;
        }

        #endregion
        #region Public Methods - Add and Remove Handlers

        public static void AddGetMessageHookHandler(EventHandler<GetMessageHookEventArgs> handler)
        {
            if (getMessageHandlers.Contains(handler))
                return;
            getMessageHandlers.Add(handler);
            if (getMessageHandlers.Count == 1)
                InstallGetMessageHook();
        }

        public static void RemoveGetMessageHookHandler(EventHandler<GetMessageHookEventArgs> handler)
        {
            getMessageHandlers.Remove(handler);
            if (getMessageHandlers.Count == 0)
                UninstallGetMessageHook();
        }

        #endregion
        #region Private Methods - Hook Procedures

        [DebuggerStepThrough]
        private static int GetMessageHookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            if (code == 0) // HC_ACTION
            {
                MSG msg = new MSG();
                Marshal.PtrToStructure(lParam, msg);
                GetMessageHookEventArgs e = new GetMessageHookEventArgs()
                {
                    HWnd = msg.hwnd,
                    Msg = msg.message,
                    WParam = msg.wParam,
                    LParam = msg.lParam,
                    MessageRemoved = (int)wParam == 1,
                    ShouldApplyChanges = false
                };

                foreach (var handler in getMessageHandlers.ToArray())
                {
                    handler(null, e);
                    if (e.ShouldApplyChanges)
                    {
                        msg.hwnd = e.HWnd;
                        msg.message = e.Msg;
                        msg.wParam = e.WParam;
                        msg.lParam = e.LParam;
                        Marshal.StructureToPtr(msg, (IntPtr)lParam, false);
                        e.ShouldApplyChanges = false;
                    }
                }
            }

            return CallNextHookEx(getMessageHookHandle, code, wParam, lParam);
        }

        #endregion
        #region Win32 stuff

        private const int WH_KEYBOARD = 2;
        private const int WH_GETMESSAGE = 3;
        private const int WH_CALLWNDPROC = 4;
        private const int WH_MOUSE = 7;
        private const int WH_CALLWNDPROCRET = 12;

        [StructLayout(LayoutKind.Sequential)]
        public class MSG
        {
            public IntPtr hwnd;
            public uint message;
            public IntPtr wParam;
            public IntPtr lParam;
            public uint time;
            int x;
            int y;
        }


        [DllImport("USER32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookDelegate lpfn, int hMod, int dwThreadId);

        [DllImport("USER32.dll", CharSet = CharSet.Auto)]
        private static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("USER32.dll", CharSet = CharSet.Auto)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern int GetCurrentThreadId();
        #endregion
    }

    #region EventArgs

    public class GetMessageHookEventArgs : EventArgs
    {
        public uint Msg { get; set; }
        public IntPtr HWnd { get; set; }
        public IntPtr WParam { get; set; }
        public IntPtr LParam { get; set; }

        public bool MessageRemoved { get; set; }
        public bool ShouldApplyChanges { get; set; }
    }

    #endregion
}

This is a helper class that does all you need. My actual class was a little longer and could handle a few more hook types, but I cleared out the code to make it smaller.

After this, your code should look like this:

private static void GetMessageProcHook(object sender, Unicorn.GetMessageHookEventArgs e)
{
    if (e.Msg == 0x100 && (Keys)e.WParam == Keys.Return) // WM_KEYDOWN
    {
        // swallow the message
        e.Msg = 0; // WM_NULL
        e.WParam = IntPtr.Zero;
        e.LParam = IntPtr.Zero;
        e.ShouldApplyChanges = true; // This will tell the HookManager to copy the changes back.
    }
}

private static ResultMsgBox MsgResultBaseNoEnter(string msg, string caption, uint options)
{
    ResultMsgBox res;
    Unicorn.HookManager.AddGetMessageHookHandler(GetMessageProcHook);
    try
    {
        res = MessageBox(GetForegroundWindow(), msg, caption, options);
    }
    finally
    {
        Unicorn.HookManager.RemoveGetMessageHookHandler(GetMessageProcHook);
    }
    return res;
}

If you encountered any other problem, let me know.

2
On

Thanks MD.Unicorn. The PeekMessage and RemoveAllKeyMessages method is working out well except for a minor change.

I've been doing more research on this issue and apparently it is a known problem (even listed as a Won't Fix issue in Microsoft connect) that the MessageBox accepts the input option on the KeyDown event and then closes the window, then the returned window will receive the KeyUp event at a later time.

As I know this KeyUp event will occur as some point in the future but not immediately. (The RemoveAllKeyMessages by itself didn't fix the problem.) I simply adjusted the method to poll for it as follows. I've renamed the method to indicate it's custom use for the MessageBox problem.

    public static void RemoveMessageBoxKeyMessages()
    {
        //Loop until the MessageBox KeyUp event fires
        var timeOut = DateTime.Now;
        while (PeekKeyMessage(false) == null && DateTime.Now.Subtract(timeOut).TotalSeconds < 1)
           System.Threading.Thread.Sleep(100);

        while (PeekKeyMessage(true) != null) ; // Empty body. Every call to the method removes one key message.
    }

Unless there is an obvious flaw (other than if the messagebox doesn't send the KeyUp event), this should be the solution for others having a similar problems.