1- using System ;
1+ using System ;
22using System . Collections . Generic ;
3+ using System . ComponentModel ;
4+ using System . Runtime . InteropServices ;
35using System . Windows . Forms ;
46
57namespace RT . Util
@@ -30,13 +32,22 @@ public sealed class GlobalKeyboardListener : IDisposable
3032 /// <summary>Keeps the managed delegate referenced so that the garbage collector doesn’t collect it.</summary>
3133 private WinAPI . KeyboardHookProc _hook ;
3234
35+ private DesktopLockNotifierForm _lockNotifier ;
36+ private Timer _afterLockTimer ;
37+
3338 /// <summary>
3439 /// Initializes a new instance of the <see cref="GlobalKeyboardListener" /> class and installs the keyboard hook.</summary>
3540 public GlobalKeyboardListener ( )
3641 {
3742 IntPtr hInstance = WinAPI . LoadLibrary ( "User32" ) ;
3843 _hook = hookProc ; // don’t remove this or the garbage collector will collect it while the global hook still tries to access it
3944 _hHook = WinAPI . SetWindowsHookEx ( WinAPI . WH_KEYBOARD_LL , _hook , IntPtr . Zero , 0 ) ;
45+ _lockNotifier = new DesktopLockNotifierForm ( ) ;
46+ _lockNotifier . SessionLocked += recheckPossibleSwallowedKeys ;
47+ _lockNotifier . SessionUnlocked += recheckPossibleSwallowedKeys ;
48+ _afterLockTimer = new Timer ( ) ;
49+ _afterLockTimer . Interval = 1000 ;
50+ _afterLockTimer . Tick += recheckPossibleSwallowedKeysTimer ;
4051 }
4152
4253 private bool _disposed = false ;
@@ -55,6 +66,9 @@ public void Dispose()
5566 if ( ! _disposed )
5667 {
5768 _disposed = true ;
69+ _lockNotifier . Close ( ) ;
70+ _lockNotifier . Dispose ( ) ;
71+ _afterLockTimer . Dispose ( ) ;
5872 WinAPI . UnhookWindowsHookEx ( _hHook ) ;
5973 }
6074 }
@@ -133,6 +147,36 @@ private int hookProc(int code, int wParam, ref WinAPI.KeyboardHookStruct lParam)
133147 }
134148 return WinAPI . CallNextHookEx ( _hHook , code , wParam , ref lParam ) ;
135149 }
150+
151+ private void recheckPossibleSwallowedKeys ( object sender , EventArgs e )
152+ {
153+ // When the desktop is locked with a quick press of Win+L, the up events for these keypresses are swallowed, even though all subsequent keypresses
154+ // get through to the handler even though we're on the lock screen already. In this scenario the "Session Locked" notification can arrive while the keys are
155+ // actually still down, even though the up events will still be swallowed shortly after, breaking this check. Hence the repeat on timeout.
156+ // A similar issue occurs with Ctrl/Alt when unlocking with Ctrl+Alt+Del, and it similarly requires a timer to re-check the keys.
157+ recheckPossibleSwallowedKeys ( ) ;
158+ _afterLockTimer . Start ( ) ;
159+ // All of this key swallowing on lock/unlock means that Up/Down events from this Global Keyboard Listener are not always paired correctly.
160+ // We do not attempt to emulate the swallowed events even for the modifier keys (which are easier because we track their state already).
161+ }
162+
163+ private void recheckPossibleSwallowedKeysTimer ( object sender , EventArgs e )
164+ {
165+ _afterLockTimer . Stop ( ) ;
166+ recheckPossibleSwallowedKeys ( ) ;
167+ }
168+
169+ private void recheckPossibleSwallowedKeys ( )
170+ {
171+ if ( _ctrl && WinAPI . GetKeyState ( ( int ) Keys . LControlKey ) >= 0 && WinAPI . GetKeyState ( ( int ) Keys . RControlKey ) >= 0 )
172+ _ctrl = false ;
173+ if ( _alt && WinAPI . GetKeyState ( ( int ) Keys . LMenu ) >= 0 && WinAPI . GetKeyState ( ( int ) Keys . RMenu ) >= 0 )
174+ _alt = false ;
175+ if ( _shift && WinAPI . GetKeyState ( ( int ) Keys . LShiftKey ) >= 0 && WinAPI . GetKeyState ( ( int ) Keys . RShiftKey ) >= 0 )
176+ _shift = false ;
177+ if ( _win && WinAPI . GetKeyState ( ( int ) Keys . LWin ) >= 0 && WinAPI . GetKeyState ( ( int ) Keys . RWin ) >= 0 )
178+ _win = false ;
179+ }
136180 }
137181
138182 /// <summary>Encapsulates the current state of modifier keys.</summary>
@@ -189,4 +233,46 @@ public GlobalKeyEventArgs(Keys virtualKeyCode, int scanCode, ModifierKeysState m
189233
190234 /// <summary>Used to trigger the KeyUp/KeyDown events in <see cref="GlobalKeyboardListener" />.</summary>
191235 public delegate void GlobalKeyEventHandler ( object sender , GlobalKeyEventArgs e ) ;
236+
237+ /// <summary>
238+ /// Subscribes to desktop (session) lock/unlock notifications and exposes events for these. It's untested whether
239+ /// disposing correctly unsubscribes from the notifications, so you should call Close and then Dispose to shut down
240+ /// the notifier.</summary>
241+ public class DesktopLockNotifierForm : Form
242+ {
243+ /// <summary>Constructor.</summary>
244+ public DesktopLockNotifierForm ( )
245+ {
246+ WTSRegisterSessionNotification ( Handle , 0 /*NOTIFY_FOR_THIS_SESSION*/ ) ;
247+ }
248+ /// <summary>Close handler.</summary>
249+ protected override void OnClosing ( CancelEventArgs e )
250+ {
251+ WTSUnRegisterSessionNotification ( Handle ) ;
252+ base . OnClosing ( e ) ;
253+ }
254+ /// <summary>Triggers every time the desktop (session) is locked.</summary>
255+ public event EventHandler SessionLocked ;
256+ /// <summary>Triggers every time the desktop (session) is unlocked.</summary>
257+ public event EventHandler SessionUnlocked ;
258+
259+ /// <summary>Message handler.</summary>
260+ protected override void WndProc ( ref Message m )
261+ {
262+ if ( m . Msg == 0x2b1 /*WM_WTSSESSION_CHANGE*/ )
263+ {
264+ int value = m . WParam . ToInt32 ( ) ;
265+ if ( value == 7 /*WTS_SESSION_LOCK*/ )
266+ SessionLocked ? . Invoke ( this , EventArgs . Empty ) ;
267+ if ( value == 8 /*WTS_SESSION_UNLOCK*/ )
268+ SessionUnlocked ? . Invoke ( this , EventArgs . Empty ) ;
269+ }
270+ base . WndProc ( ref m ) ;
271+ }
272+
273+ [ DllImport ( "wtsapi32.dll" , SetLastError = true ) ]
274+ static extern bool WTSRegisterSessionNotification ( IntPtr hWnd , [ MarshalAs ( UnmanagedType . U4 ) ] int dwFlags ) ;
275+ [ DllImport ( "WtsApi32.dll" ) ]
276+ static extern bool WTSUnRegisterSessionNotification ( IntPtr hWnd ) ;
277+ }
192278}
0 commit comments