Managing Keyboard State in DOS, VB6, and VB .NET: A Brief History

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Sandeep from Calcutta, India wrote me asking how he could programmatically manage keyboard state information in Visual Basic 6. Specifically, he asked about managing the caps lock state. As I usually do, if someone sends me some running code showing what they tried, I spend some time to help them work out their problem. I was surprised at both how much I had forgotten and how much I remembered.

They say that as people age they remember the ancient past vividly and the recent past not so well. I am not quite middle-aged, so still haven’t resolved why I could remember the BIOS memory address for keyboard state information, as well as the old BASIC peek and poke commands but not the newer Windows API methods for getting and setting keyboard states. This trip down memory lane, past the “The Peter Norton Programmer’s Guide to the IBM PC,” got me wondering. So, enroute to helping Sandeep I wrote some notes about the keyboard state as a refresher, including ways in which we can manage the keyboard state for VB6 and VB.NET. I have included those examples here for our mutual edification and amusement.

Looking at BIOS Memory

Did you know that underneath all of the layers in your PC still lurks the timid heart of a DOS machine? I don’t mean to imply that Windows runs on top of DOS, but I do mean that when you open a command prompt you can still poke around in memory just like in the (imagined) good old days of DOS when there where few restrictions on programming practices and your code could go anywhere your lions heart (or bugs) were willing to take you. This is immediately evident if you open a command prompt from Start|Run and type cmd, and once there, you run the debug.exe utility.

Debug.exe is still alive and well even on the Windows 2000 box I am writing this article on. For example, if I run debug and type eB800:0 41 1F 42 1F, followed by pressing enter, I can begin entering the ABC’s directly into the EGA video buffer in a white font with a blue background (as shown in black and white in figure 1). A similar technique very close to this one is how programmers used to create windowing applications for MS-DOS a decade or so ago.

Figure 1: Writing directly to the video buffer using the debug.exe utility.

Poke around in low memory and you can find where the keyboard state information is stored. This still appears to be stored at 0:0417-read as memory segment 0, offset 417 hexadecimal. You can view this information by entering-still in debug.exe-d0:417. When you hit enter you should get a screen dump similar to the one shown in figure 2.

Figure 2: Keyboard state information stored at absolute address 0x417 (or the 1,047 byte) from the beginning of memory.

Try an experiment. Enter the d0:417 command. View the value at that location then press the caps lock key. Issue the same debug.exe command a second time. The value should now include the value that represents the state of the caps lock key. This is more than sufficient background information and concludes the amusing portion of our program.

Keyboard State in VB6

There are several-perhaps too many-ways to obtain and modify keyboard state. What I would prefer is an object-oriented solution where the keyboard is accessible as a Singleton object, and I could write code like Keyboard.CapsLock.On. Unfortunately in VB6 we are still working with the Windows API, which is structured, and no such simplified solution exists.

Older versions of BASIC supported peeking (PEEK) from and poking (POKE) to memory. For example, we could poke &h417, 64 to toggle the Caps Lock key on. VB6 supports managing keyboard state through safer API calls. We can call SetKeyboardState, GetKeyboardState, and GetKeyState from user32.dll to set states for things like the Caps Lock and Num Lock. These API methods, however, do not change the keyboard state lights. If we want to synthesize a keystroke and change the keyboard state lights, we can call the user32.dll keybd_event method in conjunction with MapVirtualKey. This will have the effect of changing the keyboard state and lights. Finally, one version of MSDN suggests that these methods have been deprecated in favor of SendInput for Windows NT, 2000, and XP. Clearly there are a lot of ways to manage keyboard state and subjectively none of them are very satisfactory.

I am not going to attempt to demonstrate each of these methods for VB6, but I have included two of them here. Listing 1 demonstrates how to use SetKeyboardState and GetKeyState, which work reasonably well if you don’t mind a little thing like the keyboard light being out of synch. Listing 2 demonstrates how to use the keybd_event and MapVirtualKey methods, which will toggle the state and keyboard lights (at least on Windows 2000). I left the SendInput method for you to experiment with.

Listing 1: Managing keyboard state with SetKeyboardState and GetKeyState.

1:  Option Explicit
2:
3:  Private Declare Function SetKeyboardState Lib "user32" _
4:    (lppbKeyState As Byte) As Long
5:  Private Declare Function GetKeyState Lib "user32" _
6:    (ByVal nVirtKey As Long) As Integer
7:
8:  Private Sub SetKeyState(ByVal Key As Long, ByVal State As Boolean)
9:
10:   Dim Keys(0 To 255) As Byte
11:   Call GetKeyboardState(Keys(0))
12:   Keys(Key) = Abs(CInt(State))
13:   Call SetKeyboardState(Keys(0))
14:
15: End Sub
16:
17: Private Property Get CapsLock() As Boolean
18:
19:   CapsLock = GetKeyState(KeyCodeConstants.vbKeyCapital) = 1
20:
21: End Property
22:
23: Private Property Let CapsLock(ByVal Value As Boolean)
24:
25:   Call SetKeyState(KeyCodeConstants.vbKeyCapital, Value)
26:
27: End Property
28:
29: Private Property Get NumLock() As Boolean
30:
31:   NumLock = GetKeyState(KeyCodeConstants.vbKeyNumlock) = 1
32:
33: End Property
34:
35: Private Property Let NumLock(ByVal Value As Boolean)
36:
37:   Call SetKeyState(KeyCodeConstants.vbKeyNumlock, Value)
38:
39: End Property
40:
41: Private Property Get ScrollLock() As Boolean
42:
43:   ScrollLock = GetKeyState(KeyCodeConstants.vbKeyScrollLock) = 1
44:
45: End Property
46:
47: Private Property Let ScrollLock(ByVal Value As Boolean)
48:
49:   Call SetKeyState(KeyCodeConstants.vbKeyScrollLock, Value)
50:
51: End Property
52:
53: Private Sub CommandCapsLock_Click()
54:   CapsLock = Not CapsLock
55: End Sub
56:
57: Private Sub CommandNumLock_Click()
58:   NumLock = Not NumLock
59: End Sub
60:
61: Private Sub CommandScroll_Click()
62:   ScrollLock = Not ScrollLock
63:
64: End Sub
65:
66: Private Sub Timer1_Timer()
67:   CommandCapsLock.Font.Bold = CapsLock
68:   CommandNumLock.Font.Bold = NumLock
69:   CommandScrollLock.Font.Bold = ScrollLock
70: End Sub

(The line numbers were added for reference only.) Lines 2 through 6 demonstrate how to add references to methods in external libraries. Within a Form these have to be declared as Private declarations. I included SetKeyboardState and GetKeyState from the users32.dll. Each of Caps Lock, Num Lock, and Scroll Lock are represented by Property Get and Property Let methods. Each of the Get methods calls GetKeyState, passing the KeyCodeConstants, and comparing the result to 1 to convert the state to a Boolean value. For example, lines 17 through 21 implement the CapsLock Getter returning a True value if the state is 1 (or on) and otherwise False. The Let methods all call the SetKeyState wrapper subroutine. SetKeyState gets the existing keyboard state on line 11, modifies just the key state I am inertested in changing and sends the new array of key states back to the hardware through the API method SetKeyboardState.

As I mentioned this approach is deficient because I have to send all keys and the keyboard lights aren’t updated. I’d prefer that the keyboard status lights accurately reflect the state and that I only need modify the key state I am interested in changing. Listing 2 demonstrates a slightly better approach using the keybd_event API method.

Listing 2: Demonstrates how to use the keybd_event and MapVirtualKey API methods to manage keyboard state.

Option Explicit

Private Declare Sub keybd_event Lib "user32" _
  (ByVal bVk As Byte, ByVal bScan As Byte, _
  ByVal dwFlags As Long, ByVal dwExtraInfo As Long)

Private Declare Function MapVirtualKey Lib "user32" _
   Alias "MapVirtualKeyA" _
  (ByVal wCode As Long, ByVal wMapType As Long) As Long

Private Const KEYEVENTF_EXTENDEDKEY = &H1
Private Const KEYEVENTF_KEYUP = &H2

Private Sub SetKeyState(ByVal Key As Long, ByVal State As Boolean)

  Call keybd_event(Key, MapVirtualKey(Key, 0), _
     KEYEVENTF_EXTENDEDKEY Or 0, 0)

    Call keybd_event(Key, MapVirtualKey(Key, 0), _
     KEYEVENTF_EXTENDEDKEY Or KEYEVENTF_KEYUP, 0)

End Sub

Private Property Get CapsLock() As Boolean

  CapsLock = GetKeyState(KeyCodeConstants.vbKeyCapital) = 1

End Property

Private Property Let CapsLock(ByVal Value As Boolean)

  Call SetKeyState(KeyCodeConstants.vbKeyCapital, Value)

End Property

Private Property Get NumLock() As Boolean

  NumLock = GetKeyState(KeyCodeConstants.vbKeyNumlock) = 1

End Property

Private Property Let NumLock(ByVal Value As Boolean)

  Call SetKeyState(KeyCodeConstants.vbKeyNumlock, Value)

End Property

Private Property Get ScrollLock() As Boolean

  ScrollLock = GetKeyState(KeyCodeConstants.vbKeyScrollLock) = 1

End Property

Private Property Let ScrollLock(ByVal Value As Boolean)

  Call SetKeyState(KeyCodeConstants.vbKeyScrollLock, Value)

End Property

Private Sub CommandCapsLock_Click()
  CapsLock = Not CapsLock
End Sub

Private Sub CommandNumLock_Click()
  NumLock = Not NumLock
End Sub

Private Sub CommandScroll_Click()
  ScrollLock = Not ScrollLock

End Sub

Private Sub Timer1_Timer()
  CommandCapsLock.Font.Bold = CapsLock
  CommandNumLock.Font.Bold = NumLock
  CommandScrollLock.Font.Bold = ScrollLock
End Sub

Tip: As you may recall all of the API constant, method, and type declarations can be imported from the Visual Basic 6 API Viewer Add-In.

The revised code is just the code in bold font at the beginning of the listing. I included the two new API methods, keybd_event and MapVirtualKey and two constants for the dwFlags argument of keybd_event. The most significant change between listing 1 and listing 2 is in the wrapper subroutine SetKeyState. In this revised version I call keybd_event twice. The first time simulates the key being pressed and the second simulates the key being released. If you add three command buttons and a Timer to a form, you will see that the application and the keyboard both reflect the state of the keyboard. Finally, MapVirtualKey converts a virtual key constant to a physical key constant. You can ascertain from the code how and where this API method was used.

Managing Keyboard State in VB.NET

A huge part of the .NET framework simplifies routine tasks and adds object-oriented support for complex tasks. Surprisingly, this is true only to a limited degree for the keyboard in .NET. Visual Basic .NET allows you to send key codes and keys as a string to the System.Windows.Forms.SendKeys.Send shared method. Send accepts a string which can be comprised of literal and key codes. The key codes include codes for things like the caps lock key. For example, if I set focus to a TextBox and call SendKeys.Send(“{CAPSLOCK}the ballad of bilbo baggins”) then the text THE BALLAD OF BILBO BAGGINS will be sent to the TextBox. However, the caps lock is not left on (or toggled off, as the case may be.)

SendKeys is simple to use but does not permanently change the state of the keyboard; effects are only temporal for the duration of the SendKeys call. If you want to set the keyboard state in .NET it appears that you have to rely on COM Interop and the same Windows API methods we used in VB6. (The framework is huge, and if I find out differently, I will keep you updated.) The code in listing 3 demonstrates how we can wrap API methods in an internal class and import the library methods demonstrated in the examples in listings 1 and 2.

Listing 3: Using COM Interop capabilities to invoke Windows API methods in VB.NET.

1:  [ComVisibleAttribute(false),
2:  SuppressUnmanagedCodeSecurityAttribute()]
3:  internal class NativeMethods
4:  {
5:
6:    [DllImport("user32.dll", CharSet=CharSet.Auto,
7:    ExactSpelling=true, CallingConvention=CallingConvention.Winapi)]
8:    public static extern short GetKeyState(int keyCode);
9:
10:   [DllImport("user32.dll", CharSet=CharSet.Auto,
11:   ExactSpelling=true, CallingConvention=CallingConvention.Winapi)]
12:   public static extern bool SetKeyboardState( byte[] keys );
13:
14:   [DllImport("user32.dll", CharSet=CharSet.Auto,
15:   ExactSpelling=true, CallingConvention=CallingConvention.Winapi)]
16:   public static extern bool GetKeyboardState( byte[] keys );
17:
18:   public static int HIWORD(int n)
19:   {
20:    return ((n >> 16) & 0xffff/*=~0x0000*/);
21:   }
22:
23:   public static int LOWORD(int n)
24:   {
25:     return (n & 0xffff/*=~0x0000*/);
26:   }
27: }

Line 1 demonstrates a suitable use for the internal class modifier. Internal means that the NativeMethods class can only be used within the assembly that defines it. NativeMethods imports three functions from user32.dll: GetKeyState, SetKeyboardState, and GetKeyboardState, on lines 8, 12, and 16, respectively. The DLLImportAttribute preceding each declaration replaces the Declares syntax from VB6. The DLLImportAttribute is defining the library, character set, name-aliasing (ExactSpelling), and calling convention. The calling convention indicates the order and way arguments are moved through the computer’s CPU. You can look up more information on the DllImportAttribute in the System.Runtime.InteropServices namespace in the Visual Studio .NET help.

Because the NativeMethods declarations are declared as static all we need to do is precede the invocation with the NativeMethods class, connected by the member-of operator (.), the method name, and a suitable argument. Both SetKeyboardState and GetKeyboardState require an array of 256 bytes, and GetKeyState simply needs a key code. Here is an example demonstrating how to get the state of the caps lock key using NativeMethods.GetKeyboardState.

byte[] keys = new Byte[256];
NativeMethods.GetKeyboardState(keys);
Debug.WriteLine(keys[20].ToString());

Simply convert your VB6 syntax to Visual Basic .NET syntax and invoke the methods.

Summary

Programmers have wanted or needed to manage the keyboard state as long as there have been keyboards. The way we interact with device states has evolved but underneath the covers things are pretty much the same as they have been since the first PC rolled off the assembly line.

It would be nice to see a very simply object-oriented interface to physical devices, but my guess is this has been a pretty low priority so far. While we wait you can use the Windows API and COM Interop services to manage keyboard state.

About the Author

Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book “Advanced C# Programming” from McGraw-Hill/Osborne on Amazon.com. Paul will be a keynote speaker in “Great Debate: .NET or .NOT?” at the Fall 2002 Comdex in Las Vegas, Nevada. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at pkimmel@softconcepts.com.

# # #

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read