Welcome to CSharp Labs

Creating System Wide Hot Keys and Consuming Messages with a Native Sink

Thursday, June 20, 2013

Supporting system-wide hot keys can be extremely beneficial for applications by allowing user interaction while a form is deactivated. The RegisterHotKey function is responsible for registering new hot keys with an associated window while the UnregisterHotKey function frees a previously registered hot key. When the hot key is activated, a message is posted to the target window's message loop. I have created the HotKeyComponent class to add or remove hot keys and NativeHotKeySink to consume hot key messages.

How it Works

The HotKeyComponent can be added to a form and supports creating system-wide hot key through the HotKeyComponent.TryAdd methods. These methods are defined with parameters which indicate if subsequent key events are dropped, a user state object and call back function:

        /// <summary>
        /// Attempts to create a new system-wide hot key with repeat options, user state and a callback.
        /// </summary>
        /// <param name="hotkey">The hot key to register.</param>
        /// <param name="allow_repeat">true to allow repeating key events; otherwise, false.</param>
        /// <param name="state">An optional user state.</param>
        /// <param name="callback">A callback to invoke when the hot key is pressed.</param>
        /// <param name="result">When this method returns, contains an instance of a new state representing the hot key registration or null if the registration failed.</param>
        /// <returns>true if a new system-wide hot key was registered; otherwise, false.</returns>
        public bool TryAdd(HotKey hotkey, bool allow_repeat, object state, EventHandler<HotKeyActivatedEventArgs> callback, out HotKeyState result)
        {
            int id = ++_HotKeyID; //get next hotkey id

            if (_HotKeyID > 0xBFFF) //max value id can be
                _HotKeyID = 0;

            //create internal state for timestamps, state and callback
            InternalHotKeyState key_state = new InternalHotKeyState(this, id, hotkey, allow_repeat, state, callback);

            if (RegisterHotKey(key_state)) //register hot key with system
            {
                _LocalHotKeys.Add(id, key_state); //add to collection
                result = key_state; //set state
                return true;
            }

            result = null;
            return false;
        }

The InternalHotKeyState derives from HotKeyState to provide internal id, timestamp and callback properties boxed to the result parameter. The HotKeyState result value is intended to be used to remove the hot key through the HotKeyComponent.Remove method:

        /// <summary>
        /// Removes the specified hot key.
        /// </summary>
        /// <param name="state">The hot key to remove.</param>
        /// <returns>true if the hot key was removed successfully; otherwise false.</returns>
        /// <exception cref="ArgumentNullException">state is null</exception>
        /// <exception cref="ArgumentException">state was created by a different HotKeyComponent instance</exception>
        /// <exception cref="ArgumentException">state is invalid type</exception>
        public bool Remove(HotKeyState state)
        {
            if (state == null)
                throw new ArgumentNullException("state");

            if (state.Owner != this)
                throw new ArgumentException("The specified state was created by a different HotKeyComponent instance.", "state");

            InternalHotKeyState hotkey = state as InternalHotKeyState; //cast state

            if (hotkey == null) //validate state
                throw new ArgumentException("The specified state is an invalid type.", "state");

            if (_LocalHotKeys.Remove(hotkey.ID)) //attempt to remove the hotkey
            {
                UnregisterHotKey(hotkey); //unregister the hotkey
                return true;
            }
            else
                return false;
        }

The NativeHotKeySink class is derived from the NativeWindow class to process the WM_HOTKEY message and raise the NativeHotKeySink.HotKeyPressed event with the hot key id:

        /// <summary>
        /// This method is called when a window message is sent to the handle of the window.
        /// </summary>
        /// <param name="m">The Windows <see cref="System.Windows.Forms.Message"/> to process.</param>
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == (int)WindowsMessages.WM_HOTKEY)
            {
                if (HotKeyPressed != null)
                    HotKeyPressed(this, new HotKeyPressedEventArgs(m.WParam.ToInt32()));
            }

            base.WndProc(ref m);
        }

The HotKeyComponent subscribes to the NativeHotKeySink.HotKeyPressed event to validate the id, timestamp and invoke callback methods:

            //handles hot key notifications
            _Window.HotKeyPressed += (sender, e) =>
            {
                InternalHotKeyState hotkey = null;

                if (_LocalHotKeys.TryGetValue(e.HotKeyID, out hotkey))
                {
                    //utc timestamps for better performance
                    bool invoke = hotkey.AllowRepeat || DateTime.UtcNow.Subtract(hotkey.TimeStamp).TotalMilliseconds > minimumInterval;
                    hotkey.TimeStamp = DateTime.UtcNow; //update timestamp

                    if (invoke)
                    {
                        HotKeyActivatedEventArgs args = new HotKeyActivatedEventArgs(hotkey);

                        if (HotKeyActivated != null)
                            HotKeyActivated(this, args);

                        if (hotkey.Callback != null)
                            hotkey.Callback(this, args);
                    }
                }
            };

The minimumInterval value above is either the keyboard repeat delay or keyboard repeat, which ever is larger to optionally prevent repeated invokes while a hot key is held down. This is due to the KeyModifiers.MOD_NOREPEAT flag not being supported on Windows Vista and Windows XP/2000.

Hot keys are defined using the HotKey structure to represent a key and modifier. The HotKey.NativeModifier property translates the key modifier to a native modifier for the RegisterHotKey function.

Using

Drag the HotKeyComponent onto a form and use one of the HotKeyComponent.TryAdd methods to create a new system-wide hot key:

            HotKeyState state;
            hotkeyComponent1.TryAdd(key: Keys.L, modifier: Keys.Control | Keys.Shift, allow_repeat: false, callback: (obj, e) =>
            {
                Activate();
                MessageBox.Show("Hot key activated!");
            }, result: out state);

The resulting HotKeyState can be used to remove the hot key through the HotKeyComponent.Remove method. When HotKeyComponent is disposed, created system-wide hot keys are unsubscribed from the system.

Source Code

Download HotKeyComponent, NativeHotKeySink and Supporting Classes

Comments