Luckily, we have platinum support for the vendor in question, so I contacted support and asked them what exactly is intercepting these shortcuts and how to circumvent it. It seems no one could give me a straight answer, but they did offer to sell me a "source code license" for the low low price of $xxxx. I spent some time going through the scores of obfuscated classes my decompiler spat out with no luck, but if I have to guess the shortcuts in question were probably intercepted by an IMessageFilter hooked up to our application.
Therefore, the .Net solution was out, WinAPI it is! I spent hours looking at half-working, half-assed solutions involving GetMessage / PeekMessage and a few involving GetAsyncKeyState. Now, when I say half-working I mean most of the time the solution didn't take modifier keys into account, was completely detached from .Net-land or was outright broken. When I say half-assed, I mean hard-coded case statements of 3 key shortcuts right there in the class, checking that the result value of GetAsyncKeyState is non-zero and calling it a day.
Needless to say, I ended up rolling my own solution. After some performance profiling, and after taking a quick gander at GetKeyState vs. GetAsyncKeyState I went with a solution that uses GetKeyState.
Usage
The class is a singleton, partly for ease of use, but mostly because I had to get it into some places where dependency injection just wasn't an option (don't ask). You register a shortcut to an object (usually a form), listen to an event that fires when a shortcut is triggered and unregister the shortcut when the subscribed object is disposed. Easy, right?Register shortcuts
'When (un)registering "a lot" of shortcuts or subscribers, it's faster to just pause capture while doing it GlobalShortcutObserver.GetInstance.PauseCapture() GlobalShortcutObserver.GetInstance.RegisterShortcut(Me, Shortcut.CtrlW) GlobalShortcutObserver.GetInstance.RegisterShortcut(Me, Shortcut.CtrlA) GlobalShortcutObserver.GetInstance.RegisterShortcut(Me, Shortcut.CtrlS) GlobalShortcutObserver.GetInstance.RegisterShortcut(Me, Shortcut.CtrlD) GlobalShortcutObserver.GetInstance.RegisterShortcut(Me, Shortcut.CtrlShiftS) GlobalShortcutObserver.GetInstance.ResumeCapture()
Unregister shortcuts
GlobalShortcutObserver.GetInstance.UnregisterShortcuts(Me)
Shortcut event handler
'Executes when any globally registered shortcut is triggered Private Sub Event_GlobalShortcutObserver_OnShortcutPressed(Subscriber As Object, Shortcut As Shortcut) 'This shortcut was not intended for us If Subscriber IsNot Me Then Return MsgBox("You pressed " & Shortcut.ToString()) End Sub
The goods
Imports System.Windows.Forms Public Class GlobalShortcutObserver Implements IDisposable Public Declare Function GetKeyState Lib "user32" (ByVal vKey As Integer) As Short 'Holds a list of all subscribers for a given shortcut Private mShortcutToSubscriber As Dictionary(Of Shortcut, List(Of Object)) 'Breaks down a shortcut to its modifier and key components for faster checks Private mModifiersToKey As Dictionary(Of Modifier, List(Of Keys)) 'A timer that polls the keystate of registered shortcuts Private mKeyboardPollHeartBeat As Timer Const KeyboardPollHeartBeatInterval As Integer = 25 'How often the heartbeat ticks Const KeyboardPollHeartBeatDelay As Integer = 250 'How much we delay the next tick after a valid shortcut is pressed Const VK_MENU As Integer = &H12 Const VK_SHIFT As Integer = &H10 Const VK_CONTROL As Integer = &H11 Const SIGNIFICANT_BIT As Integer = &H8000 <Flags()> Private Enum Modifier None = 0 Alt = 1 Ctrl = 1 << 1 Shift = 1 << 2 End Enum #Region "Public" Public Event OnShortcutPressed(Subscriber As Object, Shortcut As Shortcut) Public Shared Function GetInstance() As GlobalShortcutObserver Static Instance As New GlobalShortcutObserver Return Instance End Function Private Sub New() Me.mShortcutToSubscriber = New Dictionary(Of Shortcut, List(Of Object)) Me.mModifiersToKey = New Dictionary(Of Modifier, List(Of Keys)) 'Periodically poll the keyboard if a shotcut is pressed Me.mKeyboardPollHeartBeat = New Timer() With {.Interval = KeyboardPollHeartBeatInterval} Me.mKeyboardPollHeartBeat.Start() Me.AttachKeyboardPollHeartBeatEvents() End Sub '''''' Pausees shortcut capturing. ''' '''Used when registering/unregistering a large amount of shortcuts Public Sub PauseCapture() Me.mKeyboardPollHeartBeat.Stop() End Sub '''''' Resumes shortcut capturing. ''' '''Used when registering/unregistering a large amount of shortcuts Public Sub ResumeCapture() Me.mKeyboardPollHeartBeat.Start() End Sub '''''' Registers a single shortcut for a a given subscriber. ''' Public Sub RegisterShortcut(Subscriber As Object, Shortcut As Shortcut) Dim Composite = Me.GetShortcutComposite(Shortcut) 'Populate the ModifierToKey collection last, since this is used to probe for existing shortcuts by the polling timer. If Not Me.mShortcutToSubscriber.ContainsKey(Shortcut) Then Me.mShortcutToSubscriber.Add(Shortcut, New List(Of Object)) End If If Not Me.mShortcutToSubscriber(Shortcut).Contains(Subscriber) Then Me.mShortcutToSubscriber(Shortcut).Add(Subscriber) End If If Not Me.mModifiersToKey.ContainsKey(Composite.Modifiers) Then Me.mModifiersToKey.Add(Composite.Modifiers, New List(Of Keys)) End If If Not Me.mModifiersToKey(Composite.Modifiers).Contains(Composite.Key) Then Me.mModifiersToKey(Composite.Modifiers).Add(Composite.Key) End If End Sub '''''' Unregisters all shortcuts for a given subscriber. ''' Public Sub UnregisterShortcuts(Subscriber As Object) Dim Composite As ShortcutComposite 'Remove the subscriber For Each Subscribers In Me.mShortcutToSubscriber.Values If Subscribers.Contains(Subscriber) Then Subscribers.Remove(Subscriber) End If Next 'Remove any shortcut that has no more subscribers For Each Row In Me.mShortcutToSubscriber.Where(Function(x) x.Value.Count = 0).ToList Me.mShortcutToSubscriber.Remove(Row.Key) 'Remove the current Key for this Modifier Composite = Me.GetShortcutComposite(Row.Key) Me.mModifiersToKey(Composite.Modifiers).Remove(Composite.Key) 'If that Modifier has no more Keys associate, we should remove it as well If Me.mModifiersToKey(Composite.Modifiers).Count = 0 Then Me.mModifiersToKey.Remove(Composite.Modifiers) End If Next End Sub #End Region #Region "Private" Private Structure ShortcutComposite Public Modifiers As Modifier Public Key As Keys Public Sub New(Modifiers As Modifier, Key As Keys) Me.Modifiers = Modifiers Me.Key = Key End Sub End Structure 'Breaks down a Shortcut into a Keys enum and a Modifiers bitmask Private Function GetShortcutComposite(Shortcut As Shortcut) As ShortcutComposite Dim strShortcut = Shortcut.ToString Dim Key As Keys = Nothing Dim Modifiers = Modifier.None Dim CurrentModifier As Modifier 'Strip out the modifiers from the passed in shortcut and append them to our Modifiers bitmask 'Note: Since we do all our string checks as StartsWith, we have to get the modifiers list in the correct order For Each strModifier In New String() {"Alt", "Ctrl", "Shift"} If strShortcut.StartsWith(strModifier) Then 'Convert our string modifier into a valid KeyModifier If Not [Enum].TryParse(strModifier, CurrentModifier) Then Throw New ApplicationException(String.Format("Could not recognize modifier ""{0}"".", strModifier)) End If 'Add the current modifier to our modifiers bitmask Modifiers = Modifiers Or CurrentModifier 'And strip out the modifier from the shortcut string strShortcut = strShortcut.Replace(strModifier, "") End If Next 'At this point we should have a single key in the shortcut string a valid modifiers bitmask. 'Convert our string into a valid Keys enum. If Not [Enum].TryParse(strShortcut, Key) Then Throw New ApplicationException(String.Format("Could not recognize Key ""{0}"".", strShortcut)) End If Return New ShortcutComposite(Modifiers, Key) End Function 'Checks if a known shortcut is pressed and notifies all subscribers if it is Private Sub Event_KeyboardPollHeartBeat_Tick(sender As Object, e As EventArgs) Try If Me.mKeyboardPollHeartBeat.Interval <> KeyboardPollHeartBeatInterval Then Me.mKeyboardPollHeartBeat.Interval = KeyboardPollHeartBeatInterval End If Dim Modifiers = Modifier.None Dim strModifiers = "" Dim KeysPressed As List(Of Keys) Dim CurrentShortcut As Shortcut Dim strCurrentShortcut As String If (GetKeyState(VK_MENU) And SIGNIFICANT_BIT) <> 0 Then Modifiers = Modifiers Or Modifier.Alt strModifiers &= Modifier.Alt.ToString End If If (GetKeyState(VK_CONTROL) And SIGNIFICANT_BIT) <> 0 Then Modifiers = Modifiers Or Modifier.Ctrl strModifiers &= Modifier.Ctrl.ToString End If If (GetKeyState(VK_SHIFT) And SIGNIFICANT_BIT) <> 0 Then Modifiers = Modifiers Or Modifier.Shift strModifiers &= Modifier.Shift.ToString End If 'No modifiers have been pressed, this can't be a shortcut If Modifiers = Modifier.None Then Return 'No shortcut is associated with the current modifiers If Not Me.mModifiersToKey.ContainsKey(Modifiers) Then Return 'Determine if any of the keys associated with this modifier are pressed KeysPressed = New List(Of Keys) For Each Key In Me.mModifiersToKey(Modifiers).ToList 'Guard against mid-loop mutation If (GetKeyState(Key) And SIGNIFICANT_BIT) <> 0 Then KeysPressed.Add(Key) End If Next 'No keys that were expected with the current modifier are pressed If KeysPressed.Count = 0 Then Return 'Let's put a bit of delay before we capture the next shortcut Me.mKeyboardPollHeartBeat.Interval = KeyboardPollHeartBeatDelay 'Time to reconsitute the modifiers and keys into a shortcut and notify the appropriate subscribers For Each Key In KeysPressed strCurrentShortcut = strModifiers & Key.ToString 'Convert our string into a valid Shortcut enum. If Not [Enum].TryParse(strCurrentShortcut, CurrentShortcut) Then Throw New ApplicationException(String.Format("Could not recognize Shortcut ""{0}"".", strCurrentShortcut)) End If 'Notify all subscribers to this shortcut For Each Subscriber In Me.mShortcutToSubscriber(CurrentShortcut).ToList 'Guard against mid-loop mutation RaiseEvent OnShortcutPressed(Subscriber, CurrentShortcut) Next Next Catch ex As Exception Me.mKeyboardPollHeartBeat.Stop() ErrorHelper.ErrorHandler(ex) Me.mKeyboardPollHeartBeat.Start() End Try End Sub Private Sub AttachKeyboardPollHeartBeatEvents(Optional ByVal RemoveOnly As Boolean = False) RemoveHandler Me.mKeyboardPollHeartBeat.Tick, AddressOf Me.Event_KeyboardPollHeartBeat_Tick If Not RemoveOnly Then AddHandler Me.mKeyboardPollHeartBeat.Tick, AddressOf Me.Event_KeyboardPollHeartBeat_Tick End Sub #End Region #Region "IDisposable" Private mDisposedValue As Boolean = False Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub Protected Overridable Sub Dispose(ByVal Disposing As Boolean) If Not Me.mDisposedValue Then If Disposing Then 'Get rid of the heartbeat object Me.AttachKeyboardPollHeartBeatEvents(True) Me.mKeyboardPollHeartBeat.Dispose() End If End If Me.mDisposedValue = True End Sub #End Region End Class