.Net 键盘挂钩额外 KeyUp 事件

发布于 2025-01-01 10:18:10 字数 1038 浏览 1 评论 0原文

我有一个业务需求,对于消息框,用户不能按回车键接受默认选项,而必须按选项键。例如。给定一个带有“是/否”选项的消息框,用户必须按 Y 或 N 键。现在我已经使用键盘挂钩在下面实现了这一点,但是当代码返回时,KeyUp 事件也会返回到调用代码。

所以问题是:如何在返回调用代码之前刷新所有键盘事件?

我已经删除了样板代码,但如果您需要它,请告知。

调用代码:

    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;
    }

和 Hook 代码:

    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);
    }

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);
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

梦幻的心爱 2025-01-08 10:18:10

将这些代码行添加到您的类中(或其他类可以使用的某个静态类中):

[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.
}

调用 RemoveAllKeyMessages() 正是您想要的。

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.

带刺的爱情 2025-01-08 10:18:10

实际上你不能刷新键盘事件,但你可以阻止线程的消息循环接收该事件。
您应该为 WH_GETMESSAGE 挂钩安装一个处理程序。钩子过程的 lParam 是一个指向 MSG 结构的指针。检查该结构后,您可以更改它以避免消息传递到调用消息处理器。您应该将消息更改为 WM_NULL。
.NET 中的实际过程有点长,需要单独的文章。但简单地说,方法如下:

将此类完全按原样复制到项目中的新 C# 文件中:

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
}

这是一个帮助器类,可以完成您所需的一切。我的实际类有点长,可以处理更多的钩子类型,但我清除了代码以使其更小。

之后,您的代码应如下所示:

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;
}

如果您遇到任何其他问题,请告诉我。

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.

小瓶盖 2025-01-08 10:18:10

谢谢独角兽博士。除了细微的变化外,PeekMessage 和RemoveAllKeyMessages 方法运行良好。

我一直在对这个问题进行更多研究,显然这是一个已知问题(甚至在 Microsoft connect 中被列为“无法修复”问题):MessageBox 接受 KeyDown 事件上的输入选项,然后关闭窗口,然后返回的窗口稍后将收到 KeyUp 事件。

据我所知,此 KeyUp 事件将在将来的某个时刻发生,但不会立即发生。 (RemoveAllKeyMessages 本身并没有解决问题。)我只是调整了轮询它的方法,如下所示。我已重命名该方法以指示它是针对 MessageBox 问题的自定义用途。

    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.
    }

除非存在明显的缺陷(除非消息框不发送 KeyUp 事件),否则这应该是其他有类似问题的解决方案。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文