大致如下:
让客户端代码知道何时在代码窗格中捕获按键
启用注册和处理热键
因此界面看起来像这样:
public interface IKeyHook
{
/// <summary>
/// Raised when system keyhook captures a keypress in the VBE.
/// </summary>
event EventHandler<KeyHookEventArgs> KeyPressed;
/// <summary>
/// Registers specified delegate for specified key combination.
/// </summary>
/// <param name="key">The key combination string, including modifiers ('+': Shift, '%': Alt, '^': Control).</param>
/// <param name="action">Any <c>void</c>, parameterless method that handles the hotkey.</param>
void OnHotKey(string key, Action action = null);
/// <summary>
/// Removes all hotkey hooks and detaches low-level keyboard hook.
/// </summary>
void UnHookAll();
}
我已经有了一个
KeyHook
类,该类处理解析器的低级密钥钩子,所以我决定将所有private static extern
声明都移到专用的User32
和Kernel32
静态类,然后在名为KeyHook
的类中实现热键钩子是自然的(对吗?),所以这里是:残酷,我知道。 HookCallback
是低级挂钩处理程序,只要代码模块被修改(通过键入),最终它就会启动解析器任务。 WindowProc
是接收所有发送到VBE主窗口的Windows消息的处理程序;每当VBE失去焦点时,钩子就会分离-当VBE重新激活时,钩子会重新连接。优雅地提供给Rubberduck项目的Smart Indenter VB6源代码,但也可能因我对p / invoke的理解而受苦。我很高兴终于有了一些行之有效的方法。我认为需要重构它;我不喜欢正在进行的所有铸造,而且我发现这门课相当庞大。我正在考虑从那里拿走热键内容,但是光是热键内容就很庞大-我将如何最好地重新排列代码?
(在GitHub上查看)
#1 楼
/// <summary>
/// Registers specified delegate for specified key combination.
/// </summary>
/// <param name="key">The key combination string, including modifiers ('+': Shift, '%': Alt, '^': Control).</param>
/// <param name="action">Any <c>void</c>, parameterless method that handles the hotkey.</param>
void OnHotKey(string key, Action action = null);
有一些
OnXXX
方法总是使我想起OnEvent
,因此它仅应用于事件。该方法的摘要指出Registers specified delegate for specified key combination.
,这不是该接口方法的实现所执行的。该实现做得太多,因为它基于action
的值注册或取消注册密钥的挂钩。 我在这里要做的是使用两种方法
Register(string key, Action action)
和UnRegister(string key)
来清楚地区分这两个动作。 在
GetModifierValue()
方法内部,您使用了太多的字符串。您可以考虑改用char
,就像这样private static uint GetModifierValue(ref string key)
{
int i;
uint lShift = 0;
for (i = 0; i < 3; i++)
{
var firstChar = key[i];
if (firstChar == '+')
{
lShift |= (uint)KeyModifier.SHIFT;
}
else if (firstChar == '%')
{
lShift |= (uint)KeyModifier.ALT;
}
else if (firstChar == '^')
{
lShift |= (uint)KeyModifier.CONTROL;
}
else
{
break;
}
}
key = key.Substring(i + 1);
return lShift;
}
这样,变量名称
firstChar
不再会涉及其类型。 但是,更危险的是,您永远不要评估传递的
key
至少包含4
个字符。您应该使用(希望是以前的)OnHotKey()
方法执行此操作。 我对com和interop的使用并不多,但是您使用了很多
IntPtr
,应该正确释放IMO。另外,您应该使用适当的IDisposable
模式来执行此操作,而不会出现异常。我不知道在Detach()
方法内调用Dispose()
方法之前是否需要“解钩”键挂钩。因为
_hookedKeys
是IDictionary<TKey, TValue>
,您应该使用属性Count
而不是扩展方法Any()
检查是否包含类似的内容private void HookKey(uint keyCode, uint shift, Action action)
{
UnHookKey(keyCode, shift);
if (_hookedKeys.Count == 0)
{
HookWindow();
}
最好首先检查
if
条件下的bool
值。所以这个private void TimerCallback(IntPtr hWnd, WindowLongFlags msg, IntPtr timerId, uint time)
{
// check if the VBE is still in the foreground
if (User32.GetForegroundWindow() == _hWndVbe && !_isRegistered)
{
应该变成这个
private void TimerCallback(IntPtr hWnd, WindowLongFlags msg, IntPtr timerId, uint time)
{
// check if the VBE is still in the foreground
if (!_isRegistered && User32.GetForegroundWindow() == _hWndVbe)
{
评论
\ $ \ begingroup \ $
Detach()取消激活活动代码窗格中用于通知按键的低级键盘挂钩;该钩子与热键无关,请参见链接后异步解析:-)
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2015年12月8日14:15在
\ $ \ begingroup \ $
另外IntPtr是一种值类型,请说明“应正确释放”的意思是什么?
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2015年12月8日14:46
\ $ \ begingroup \ $
IntPtr可能是值类型,但它引用的非托管资源不是。如果创建资源,则需要释放它;具体操作取决于您使用的特定API(例如,User32.SetWindowsHookEx分配,而User32.UnhookWindowsHookEx释放)
\ $ \ endgroup \ $
–安德鲁·维尔米(Andrew Vermie)
2015年12月8日在17:43
#2 楼
我认为您应该将公共成员排在班级的顶部(构造函数为第一),将私人成员排在底部。在将公共方法和私有方法混合在一起的类中导航是非常困难的。
您的类显然承担着一项以上的责任。您至少应提取解析逻辑(
GetKey
,GetModifierKey
等)以分离实体。您可能还希望提取钩子逻辑://this interface should encapsualte your hooking logic
//and fire an event whenever key is pressed
interface IKeySource
{
event EventHandler<KeyPressedArgs> KeyPressed;
}
//this interface should manage defined hotkeys
//and subscribe to IKeySource.KeyPressed event
interface IHotkeyManager
{
void Register(...);
void Unregister(...);
}
为什么要将钩子注册为全局系统热键,然后编写大量代码来检查是否您的应用程序处于活动状态?那么拥有全局热键有什么意义呢?它没有任何意义。您不应该使用本地应用程序挂钩吗?这是我前一阵子写的一个钩子,当我需要一堆棘手的热键时:
花几天时间浏览Windows api文档和互操作(ofc)。您的代码虽然超级复杂!在某种程度上,令我惊讶的是它完全起作用。也许我缺少一些关键的上下文,但是我不明白为什么解决方案需要如此复杂。
评论
\ $ \ begingroup \ $
TBH我将处理Smart Indenter热键的有效VB6代码转换为C#,我什至不知道我什至可以使用本地热键钩子。。。
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2015年12月8日14:21
\ $ \ begingroup \ $
@ Mat'sMug,哦,明白了。当您需要全局键盘挂钩时,通常使用WH_KEYBOARD_LL挂钩。例如,如果要实现按键记录器,则应使用它。 RegisterHotKey方法也是如此。它用于注册全局热键,因此在浏览Firefox时可以按CTRL-SHIFT-R,并且重构窗口仍将在另一个屏幕上打开。在您的情况下,您需要的是本地WH_KEYBOARD挂钩。当您的应用程序处于活动状态时,它将仅拦截键盘消息。然后,您可以在处理程序中执行映射的操作,或者如果无法识别热键,则忽略该消息。
\ $ \ endgroup \ $
– Nikita B
15年12月8日在15:14
\ $ \ begingroup \ $
“忽略”是指您必须通过CallNextHookEx调用将其向下传递。调用_oldWndProc可能也可以,但是看起来很混乱。
\ $ \ endgroup \ $
– Nikita B
15年12月8日在15:17
\ $ \ begingroup \ $
我正在使用WH_KEYBOARD_LL在活动代码窗格中拾取按键,并通知加载项需要重新解析活动模块(通过引发KeyPressed事件);禁用VBE时,该低级挂钩和热键都不会被钩住,因此,除非VBE是活动窗口,否则Ctrl + Shift + R不会执行任何操作。有趣的是,我在多个地方读到,在C#中起作用的唯一钩子是WH_KEYBOARD_LL和WH_MOUSE_LL ...
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
15年12月8日在15:20
\ $ \ begingroup \ $
@ Mat'sMug本地钩子(WH_KEYBOARD和WH_MOUSE)应适用于具有消息循环的任何应用程序。只要该语言允许您对WinAPI进行本机调用,并且只要循环本身存在,就可以使用任何编程语言来加入该循环。例如,它不适用于控制台应用程序,因为没有循环可钩。它几乎适用于任何带有GUI的应用程序,无论是WPF,WinForms还是Qt。
\ $ \ endgroup \ $
– Nikita B
2015年12月9日10:33
评论
为什么要使用键盘挂钩代替全局快捷键而不使用RegisterHotKey?@CodesInChaos热键已通过RegisterHotKey注册;键盘挂钩用于通知外接程序活动模块已被修改,从而触发解析器任务。