Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / Win32
Article

Toggle Keys Controller

Rate me:
Please Sign up or sign in to vote.
4.89/5 (27 votes)
3 Sep 2009CPOL6 min read 49.7K   1.4K   50   17
A class that enables you to control and monitor the toggle keys such as CapsLock, NumLock, and ScrollLock.

screenshot.png

Table of Contents

Overview

A couple of days before I wrote this article, a member asked how to control the Caps, Num, and Scroll lock keys. I knew there were several implementations around, so I searched with a view to providing the poster with a link, but I couldn't find one that was complete. Many just read the state of these keys, some also allowed you to set them, but I couldn't find any that monitored them so your application was updated accordingly. So, this article is the result. The ToggleKeysController class in the code attached will monitor these keys and raise events when a change is detected, and allow you to get or set the state.

If you just want to see how to use the class in your application(s) and aren't interested in how it does its thing, click here.

Unanticipated Problems

This wasn't quite as simple as I first thought it would be! The first thing to do was to get the state of the keys. I knew the Console class had a CapsLock property so I figured it would probably be the answer. Unfortunately, it doesn't have a ScrollLock property.

The next step was to set the key states. The ones that are in the Console class are read only, so now that class was definitely out of the question. I had a look at SendKeys for this, but that doesn't allow you to permanently change the state as demonstrated here.

Finally, I wanted to be able to monitor the state of these keys system wide so the information I was providing was always up to date. I couldn't find a way to do this either using classes in the framework, so not a good start.

The answer to all these problems was a little bit of PInvoke into user32.dll (and kernel32.dll) which has all the native functions we need.

Getting the Key States

Getting the state of a key requires a call to the GetKeyState function. This function simply takes a value that represents a key, and returns a value indicating its state. This example shows just how easy it is to use to get the state of the CapsLock key.

C#
bool isCapsLockOn = (GetKeyState(VK_CAPITAL) & 1) == 1;

The function definition is:

C#
[DllImport("user32.dll")]
static extern short GetKeyState(byte nVirtKey);

Note: For simplicity elsewhere, I have defined each key as a byte. Strictly speaking, the function above should be declared with an int.

Setting the Key States

After investigating several different approaches to this, I decided the easiest way to do this was to simulate a key up/down cycle using the keybd_event function when the state needed changing. The following example shows how to call this function to simulate a keypress of the CapsLock key.

C#
keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);

... and the function definition:

C#
[DllImport("user32.dll")]
static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);

Monitoring the Keys

This was the most challenging part of all of this. The only way I could make this work successfully was to use a global hook. Global hooks literally hook into windows to intercept key / mouse events. Before we can hook into the system, we need the handle of our process' main module. To do this, we get the current process, get the main module of that process, then pass the name of that to the GetModuleHandle function. Once we have the handle, we can install the hook using the SetWindowsHookEx function specifying the type of hook we need, in our case, keyboard. This sounds more complicated than it actually is! Here is the code, followed by the function definitions:

C#
IntPtr hookHandle = IntPtr.Zero;
using (Process currentProcess = Process.GetCurrentProcess())
{
    using (ProcessModule currentModule = currentProcess.MainModule)
    {
        hookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, 
            GetModuleHandle(currentModule.ModuleName), 0);
    }
}

[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, 
    LowLevelKeyboardProc lpfn, IntPtr hMod, int dwThreadId);

The SetWindowsHookEx function takes a delegate of type LowLevelKeyboardProc as its second parameter. I have passed it hookProc, which is an instance of that delegate. This will call its associated method whenever a key event occurs. The documentation states that if the first parameter of the called method is less than zero, we should return with no further processing, so all we need to do is check that for zero and make sure it's a KeyDown and then check the key to make sure it's one we're interested in.

C#
LowLevelKeyboardProc hookProc = KeyHookCallback;
IntPtr KeyHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    int wParamValue = wParam.ToInt32();
    if (nCode >= 0 && (wParamValue == WM_KEYDOWN || wParamValue == WM_KEYUP))
    {
        ToggleKey key = (ToggleKey)Marshal.ReadByte(lParam);
        if (key == ToggleKey.CapsLock || key == ToggleKey.NumLock || 
            key == ToggleKey.ScrollLock)
        {
            // Do our stuff here
        }
    }
    return CallNextHookEx(hookHandle, nCode, wParam, lParam);
}

delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

So now we know that one of our keys has been pressed. Unfortunately, it's not quite as simple as toggling a boolean field/property. Firstly, holding down a toggle button will call this method repeatedly, but the toggle state only changes the first time. The obvious answer is to read the state of the key instead, but if you try that, you will find it hasn't yet been updated and won't be until our thread's inactive, which is the second problem. In the first version, I launched a worker thread, then posted back to the original thread and checked the state. This was good for accuracy, but was probably overkill. In this version, I toggle a flag on keydown and reset the flag on keyup. This way, only the first keydown is considered valid. This presents a potential problem if the key in question is being held down during the initial synchronization of the states, as the first keydown received may not be a valid one. To deal with this, the first keyup after synchronization tests the state and corrects the value if needed.

C#
// Do our stuff here
switch (key)
{
    case ToggleKey.CapsLock:
        if (wParamValue == WM_KEYUP)
        {
            keys[key].IsDown = false;
            if (!keys[key].Confirmed)
            {
                bool originalValue = keys[key].IsOn;
                keys[key].IsOn = (GetKeyState(VK_CAPITAL) & 1) == 1;
                if (keys[key].IsOn != originalValue)
                {
                    ToggleKeyChangedEventArgs e = 
                       new ToggleKeyChangedEventArgs(key, keys[key].IsOn);
                    OnCapsLockChanged(e);
                    OnToggleKeyChanged(e);
                }
                keys[key].Confirmed = true;
            }
        }
        else if (!keys[key].IsDown)
        {
            keys[key].IsDown = true;
            keys[key].IsOn = !keys[key].IsOn;
            ToggleKeyChangedEventArgs e = 
              new ToggleKeyChangedEventArgs(key, keys[key].IsOn);
            OnCapsLockChanged(e);
            OnToggleKeyChanged(e);
        }
        break;
// ...

Now, all that's left is to unhook when we're done. I've placed the hooking in the constructor, so to keep things clean, I've implemented the standard Dispose pattern and unhooked in Dispose(bool disposing). Unhooking is simply a case of calling UnhookWindowsHookEx passing the handle we got from the original SetWindowsHookEx call.

C#
UnhookWindowsHookEx(hookHandle);
hookHandle = IntPtr.Zero;

[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hhk);

The ToggleKeysController Class

The final class wraps all the above into a simple to use class with just three public properties, three additional public methods (plus Dispose), and four events. All of these should be very logical and self explanatory, but all the code is documented and commented just in case! Here is the (public) class diagram.

classdiagram.png

The properties get or set the state of the key. Start and Stop install/uninstall the hook. These probably won't be needed as Start is called by the constructor and Stop is called in Dispose. Toggle simply toggles the state of the key you pass to it. Finally, the events are raised as keystates change, with the ToggleKeyChanged event being raised for every toggle key state change.

The Demo

The demo app uses the ToggleKeysController class via a status strip. The state of the keys is indicated by the ForeColor of the labels. For example, this code is called by the CapsLockStateChanged event handler.

C#
if (toggleKeysController.CapsLockOn)
    toolStripStatusLabelCAPS.ForeColor = OnForeColor;
else
    toolStripStatusLabelCAPS.ForeColor = OffForeColor;

Clicking a label toggles the state of the respective button by calling the ToggleKey method.

C#
void toolStripStatusLabelCAPS_Click(object sender, EventArgs e)
{
    toggleKeysController.ToggleKey(ToggleKeys.CapsLock);
}

Possible Limitation?

I haven't been able to verify this, but there are a few reports that apparently some antivirus software doesn't like programs that use global hooks as it thinks they are key loggers. I have tested this with the latest version of Norton AV available at the time of writing (under Vista and XP) and AVG Network Edition (under XP) with no problems. If your AV takes exception to it, then please let me know.

Conclusion

There's not much else to say; as now all the more complex stuff is wrapped in one class, using it is trivial. I hope you find some use for it!

History

  • 30th August, 2009: Initial version.
  • 3rd September, 2009: Version 2.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO Dave Meadowcroft
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionStuck in VS2005 Pin
Jalapeno Bob2-Sep-09 9:14
professionalJalapeno Bob2-Sep-09 9:14 
AnswerRe: Stuck in VS2005 Pin
DaveyM692-Sep-09 11:36
professionalDaveyM692-Sep-09 11:36 
AnswerRe: Stuck in VS2005 Pin
DaveyM693-Sep-09 12:01
professionalDaveyM693-Sep-09 12:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.