1

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);
    }
Cheval
  • 403
  • 4
  • 14
  • 1
    I don't care if you've pushed back on this 30 times or more - push back against it again. Overriding default behaviours like this is *bad* - do they not realise that, for every move like this they pull, they're actually increasing the training costs for every user who'll use this system? – Damien_The_Unbeliever Feb 01 '12 at 08:03
  • It is a custom build that I've inheritied. – Cheval Feb 03 '12 at 06:55
  • 1
    If you want custom message boxes, implement them as custom message boxes instead of throwing a keyboard hook at it. – CodesInChaos Feb 03 '12 at 13:21
  • @Damien_The_Unbeliever: Most of the time, you're statement is right. In this case, though, I disagree. This is a problem for most applications. Users blindly press the enter button to get rid of the box without reading it. Forcing the user to press a button might cause them to think before doing so. This kind of training cost is a good one. You're training the user to do what is right, instead of `what is the fastest way to get the box out of the way so I can get my job done` – Gabriel McAdams Aug 14 '12 at 23:51

3 Answers3

1

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.

Mohammad Dehghan
  • 17,853
  • 3
  • 55
  • 72
0

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.

Mohammad Dehghan
  • 17,853
  • 3
  • 55
  • 72
  • Thanks for that, but unfortunately it didn't fix the problem. After pressing the Y key, it's still returning from the message box and the KeyUp event is firing in the calling window. In the KeyUp event handler of the window, the event list the Y character in the data. – Cheval Feb 03 '12 at 06:36
  • I also tried an "else if (e.Msg == WM_KEYUP) //swallow the message code as above", but that didn't prevent the event either. – Cheval Feb 03 '12 at 07:12
  • So the return key worked fine, right? And you have another requirement: You want to after pressing the 'Y' button, keyup event won't get to the calling window. That can't be done like that, because when the `y` key is received by the message box, the message box is closed and after returning to the previous window, you uninstall the hook. So what you mentioned in your second comment won't work. I'll work on in and will attach it to my answer, but it is better that you mention your new requirement in your question. – Mohammad Dehghan Feb 03 '12 at 08:17
  • @Cheval I think I misunderstood your question in the first place! Sorry for that. I'll post another answer. – Mohammad Dehghan Feb 03 '12 at 10:05
  • Thanks for your help MD.Unicorn. Yes, I was unsure of the best way to explain the problem, so I thought BG first and bolding the question might be easiest to understand. I'll reverse the two parts next time. – Cheval Feb 06 '12 at 03:17
0

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.

Cheval
  • 403
  • 4
  • 14
  • @Chevel: When the user presses a key and then releases the key, there is a little time that the key is held down. So you are right. The key up event is not sent exactly after the key down event and closing the message box. But your solution may not work in situations where the user holds the key more that 100 milliseconds. In this situation, you may have to do something else. For example, wait for the keyup event. But it may freeze the application. – Mohammad Dehghan Feb 06 '12 at 05:42
  • I tested the case where the user holds down the key for longer than 100 milliseconds and yes the KeyUp event also got sent to the calling code. This is possibly the reason for allowing the event to pass through in the first place. Also good point about potential freeze. I've updated the code to put in a time out, just in case. – Cheval Feb 07 '12 at 00:22