【问题标题】:.Net Keyboard Hook Extra KeyUp Event.Net 键盘挂钩额外 KeyUp 事件
【发布时间】:2012-02-01 05:57:56
【问题描述】:

我有一个业务需求,对于消息框,用户不能按回车键接受默认选项,而必须按选项的键。例如。给定一个带有 Yes/No 选项的 MessageBox,用户必须按 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;
    }

还有钩子代码:

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

【问题讨论】:

  • 我不在乎你是否已经反击了 30 次或更多次 - 再反击一次。像这样覆盖默认行为是糟糕 - 他们是否没有意识到,对于他们采取的每一个这样的举动,他们实际上都在增加每个将使用该系统的用户的培训成本?
  • 这是我继承的自定义构建。
  • 如果您想要自定义消息框,请将它们实现为自定义消息框,而不是向其抛出键盘钩子。
  • @Damien_The_Unbeliever:大多数时候,你的说法是正确的。不过,在这种情况下,我不同意。这是大多数应用程序的问题。用户盲目地按回车键摆脱盒子而不阅读它。强迫用户按下按钮可能会导致他们在这样做之前进行思考。这种培训成本是不错的。您正在训练用户做正确的事,而不是 what is the fastest way to get the box out of the way so I can get my job done

标签: .net marshalling messagebox keyboard-hook


【解决方案1】:

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

[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() 完全符合您的要求。

【讨论】:

  • 我将在回复中显示一个小改动,这似乎就是答案。谢谢。
【解决方案2】:

实际上你不能刷新键盘事件,但是你可以阻止事件被线程的消息循环接收。
您应该为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;
}

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

【讨论】:

  • 谢谢你,但不幸的是它没有解决问题。按下 Y 键后,它仍然从消息框返回,并且 KeyUp 事件在调用窗口中触发。在窗口的 KeyUp 事件处理程序中,事件列出数据中的 Y 字符。
  • 我还尝试了“else if (e.Msg == WM_KEYUP) //如上吞下消息代码”,但这也没有阻止该事件。
  • 所以返回键工作正常,对吧?而且您还有另一个要求:您希望在按下“Y”按钮后,keyup 事件不会到达调用窗口。不能那样做,因为当消息框收到y键时,消息框关闭,返回上一个窗口后,你卸载了钩子。因此,您在第二条评论中提到的内容将不起作用。我会继续工作并将其附加到我的答案中,但最好在您的问题中提及您的新要求。
  • @Cheval 我想我首先误解了你的问题!对此感到抱歉。我会发布另一个答案。
  • 感谢您的帮助 MD.Unicorn。是的,我不确定解释问题的最佳方式,所以我认为首先 BG 并将问题加粗可能最容易理解。下次我会把这两个部分反过来。
【解决方案3】:

感谢 MD.Unicorn。 PeekMessage 和 RemoveAllKeyMessages 方法运行良好,只是做了一个小改动。

我一直在对此问题进行更多研究,显然这是一个已知问题(甚至在 Microsoft connect 中列为 Won't Fix 问题)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 事件),这应该是其他有类似问题的解决方案。

【讨论】:

  • @Chevel:当用户按下一个键然后释放该键时,有一点时间该键被按住。所以你是对的。按键按下事件并没有在按键按下事件和关闭消息框之后完全发送。但是您的解决方案可能不适用于用户持有密钥超过 100 毫秒的情况。在这种情况下,您可能需要做其他事情。例如,等待 keyup 事件。但它可能会冻结应用程序。
  • 我测试了用户按住键超过 100 毫秒的情况,是的,KeyUp 事件也被发送到调用代码。这可能是首先允许事件通过的原因。关于潜在冻结也很好。我已经更新了代码以设置超时,以防万一。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-18
相关资源
最近更新 更多