Keyboard Class: From Windows API Chaos to VBA Class Module Calm

By Ken Getz

Working with VBA, you grow accustomed to working with objects, and their inherent properties and methods. In fact, it becomes difficult — even painful — to program any other way. Unfortunately, there’s still plenty of functionality that’s available only the old-fashioned, non-object-oriented way. An outstanding example is that great sea of function calls, the Windows API. One of the appealing features of working with class modules is that you can wrap up such bodies of functionality into manageable objects with a set of public interfaces. That is, you can hide the details of an interrelated set of information, and present it as an object.

For example, imagine you find the need, in some of your VBA projects, to interact with the user’s keyboard. You might want to speed up or slow down the cursor blink rate; find out how many function keys the user has; modify the keyboard repeat speed and the delay time before repeating; or perhaps retrieve and set the state of the toggle keys, i.e. n, c, and o. To do this requires working with the Windows API, and that requires declarations, external function calls, working with user-defined types and API-defined constants — all sorts of unpleasantness.

Wouldn’t it be nice if you had a simple Keyboard object that supplied properties to perform all the work for you? Of course it would! And, as you’ve probably figured out, that’s where this article is headed. The Windows API provides a lot of information about the keyboard, but retrieving it requires calling a number of functions, so you have to know which functions to call, how to call them, what information they return, and more. This article presents a VBA Keyboard class, with the properties described in FIGURE 1. (One of the more inviting reasons to use class modules and the objects they create is that you can instantiate multiple objects from the same class. Although you’ll never create multiple instances of the Keyboard class in a single application, that’s no reason not to use a class module.)

Property Description Allowable Values Read/Write
KeyboardType Determines the type (number of keys) of the keyboard. N/A R
FunctionKeys Determines the number of function keys. N/A R
CapsLock Retrieves or sets the state of the c toggle. True/False R/W
NumLock Retrieves or sets the state of the n toggle. True/False R/W
ScrollLock Retrieves or sets the state of the o toggle. True/False R/W
Delay Retrieves or sets the keyboard repeat-delay setting. 0-3 R/W
Speed Retrieves or sets the keyboard repeat speed. 0-31 R/W
CaretBlinkTime Retrieves or sets the number of milliseconds between blinks of the insertion caret. 200-1200 (generally in increments of 100) R/W

FIGURE 1: Properties provided by the Keyboard class.

To try out Keyboard.cls (you’ll find the code later in this article, and it’s available for download — see the end of the article for details), you’ll need to first create a class module named Keyboard, then get the code into your project. Once you’ve got the Keyboard class module in your project, you can use it like any other class. For example, to use the Keyboard class, create a new instance of the class, then set or retrieve its available properties. To retrieve and set the current keyboard delay setting, for example, you could use code like this:

Dim okb As Keyboard

Set okb = New Keyboard
If okb.Delay < 3 Then
  okb.Delay = okb.Delay + 1
End If

Certainly a lot easier than calling a bunch of Windows API functions directly!

Doing It the Hard Way

To do its work, the Keyboard class makes calls to the Windows API. This section of the article delves into the specific API calls. If you’re not interested, just skip to the next section, “Don’t Sweat the Details,” which describes how to use the class.

Keyboard type. Retrieving the type of keyboard and the number of function keys is simple. Call the GetKeyboardType API function, passing it a value of 0 to obtain the keyboard type, or 2 to get the number of function keys. The function returns keyboard types as shown in FIGURE 2, and the number of function keys as shown in FIGURE 3.

Value Keyboard Type
1 IBM PC/XT or compatible (83-key)
2 Olivetti “ICO” (102-key)
3 IBM PC/AT (84-key) or similar
4 IBM enhanced (101- or 102-key)
5 Nokia 1050 and similar
6 Nokia 9140 and similar
7 Japanese

FIGURE 2: Keyboard type return values for GetKeyboardType.

Value Number of Function Keys
1 10
2 12 (sometimes 18)
3 10
4 12
5 10
6 24
7 Hardware dependent; specified by the OEM

FIGURE 3. Function key return values for GetKeyboardType.

Of course, to use these functions, the class module requires the API function’s declaration, so it must include the following line of code in its Declarations section:

Private Declare Function GetKeyboardType Lib "User32" _
  (ByVal lngTypeFlag As Long) As Long

To retrieve the keyboard type, or number of function keys (as just described), the Keyboard class module contains the following procedures:

Property Get KeyboardType() As Long
  KeyboardType = GetKeyboardType(0)
End Property

Property Get FunctionKeys() As Long
  FunctionKeys = GetKeyboardType(2)
End Property

Keyboard toggles. To set a keyboard toggle, you must use the GetKeyboardState and SetKeyboardState API functions. To retrieve the current state of a toggle key, use the GetKeyState API function. These require the following declarations:

Private Declare Function GetKeyState Lib "User32" _
  (ByVal lngVirtKey As Long) As Integer
Private Declare Function GetKeyboardState Lib "User32" _
  (bytKeyState As Byte) As Long
Private Declare Function SetKeyboardState Lib "User32" _

  (bytKeyState As Byte) As Long

To retrieve the state of a key, call GetKeyState, passing in the Windows virtual key code for the keyboard key. Luckily, VBA provides constants for n and c (vbKeyNumlock and vbKeyCapital). However, not all implementations of VBA provide a similar key constant for o, so you may need to define the constant yourself:

Private Const vbKeyScrollLock = 145

The GetKeyState function returns an integer containing information about the key you’ve sent it, but the only piece of that information you need is the lowest bit. To retrieve only the lowest bit, use the And operator and the value 1 to strip off all the information except the lowest bit.

(For the bit-confused: In binary, the value 1 consists of a bunch of 0’s followed by a lonely 1. Each of these digits is called a “bit.” The And operator takes each bit of its two operands, and unless the bits in the same position from both operands are 1, the result is a 0 bit. If they were both 1, the result is a 1 bit. That way, by using And 1 with the return value, all the bits except the least significant bit are guaranteed to be 0 in the output. The final bit will either be 1 or 0 in the output, depending on its value in the return value from GetKeyState. That didn’t help? It’s OK — you can get by for a long time in VBA not digging too deep into bitwise arithmetic. You can also find plenty of help by searching on “And operator” in the online help in the VBA IDE. Or, search using “Logical Operators” to learn about the entire family of logical operators.)

To find out whether the o toggle is set, the Keyboard class module uses the following property procedure:

Property Get ScrollLock() As Boolean
  ' Return the ScrollLock toggle.
  ScrollLock = CBool(GetKeyState(vbKeyScrollLock) And 1)
End Property

You’ll find similar procedures to retrieve the state of the n and c toggles.

Setting the state of the toggle requires a bit more effort; you must follow these steps:

Call an API function (GetKeyboardState) to retrieve the current state of the entire keyboard as a series of bytes (one byte per key).

Change the state of the key (or keys) that interests you within the array of bytes.

Call an API function (SetKeyboardState), sending the modified array of bytes. This sets the state of the entire keyboard.

For each of the GetKeyboardState or SetKeyboardState functions, pass the first element of an array of 256 bytes to the API function. The function either fills the 256 elements of the array with information about the keyboard, or uses the bytes to set the state of the keyboard. The Keyboard class includes the following private procedure, which does all the work:

Private Sub SetKeyState(intKey As Integer, _
                        fTurnOn As Boolean)
  ' Retrieve the keyboard state, set the particular key in
  ' which you're interested; then set the entire keyboard
  ' state back the way it was, with the one key altered.
  Dim abytBuffer(0 To 255) As Byte

  Call GetKeyboardState(abytBuffer(0))
  abytBuffer(intKey) = CByte(Abs(fTurnOn))
  Call SetKeyboardState(abytBuffer(0))
End Sub

Code within the class module calls this procedure (passing key constants for n, c, and o). The SetKeyState procedure follows the steps listed previously to do its work. For example, the Property Let procedure, ScrollLock, looks like this:

Property Let ScrollLock(Value As Boolean)
  ' Set the ScrollLock toggle.
  Call SetKeyState(vbKeyScrollLock, Value)
End Property

Repeat delay and speed. Setting the keyboard repeat speed, and the number of milliseconds the keyboard waits before starting to repeat keys, is not terribly difficult. These tasks require calling the SystemParametersInfo function, requesting the necessary information, or setting the values. SystemParametersInfo is a “grab bag” function; it can do many things, and you indicate what you want it to do by passing it a constant. For example, to set the keyboard delay to 3, you could make the following call to SystemParametersInfo:

Call SystemParametersInfo(SPI_SETKEYBOARDDELAY, _
                          3, 0, SPIF_TELLALL)

In this case, the third parameter isn’t used, so leave it as 0. The fourth parameter is a constant defined in the class module, telling SystemParametersInfo to update both the appropriate .ini file and the Windows registry. The constants also instruct SystemParametersInfo to send a message to all the running Windows applications, telling them you’ve changed a setting. Setting the repeat speed is the same, except a different constant is used.

To use SystemParametersInfo, you must declare it, as well as any constants you’ll be using. The Keyboard class module includes these declarations:

Private Const SPI_GETKEYBOARDDELAY = 22
Private Const SPI_SETKEYBOARDDELAY = 23
Private Const SPI_GETKEYBOARDSPEED = 10
Private Const SPI_SETKEYBOARDSPEED = 11

' SystemParametersInfo flags
Private Const SPIF_UPDATEINIFILE = &H1
Private Const SPIF_SENDWININICHANGE = &H2

Private Declare Function SystemParametersInfo _
  Lib "User32" Alias "SystemParametersInfoA" ( _
  ByVal uAction As Long, ByVal uParam As Long, _
  lpvParam As Any, ByVal fuWinIni As Long) As Long

' This is a made-up constant.
Private Const SPIF_TELLALL = SPIF_UPDATEINIFILE Or _
                             SPIF_SENDWININICHANGE

Retrieving values using SystemParametersInfo requires a bit more effort. To retrieve values, you must pass a variable as the third parameter, and (normally) 0 as the fourth parameter. SystemParametersInfo fills the variable with the requested value, like this fragment from the Keyboard class module:

Property Get Speed() As Long
  ' Get the keyboard repeat-speed setting.
  Dim lngValue As Long

  Call SystemParametersInfo(SPI_GETKEYBOARDSPEED, 0, _
                            lngValue, 0)
  Speed = lngValue
End Property

Working with the caret. You can also set and retrieve the number of milliseconds between blinks of the text insertion caret using Windows API calls. The GetCaretBlinkTime function retrieves information about the caret, and the SetCaretBlinkTime sets the blink time. To use these functions, you must first declare them:

Private Declare Function GetCaretBlinkTime _
  Lib "User32" () As Long
Private Declare Function SetCaretBlinkTime _
  Lib "User32" (ByVal wMSeconds As Long) As Long

Once you’ve declared these functions, they’re simple to use. For example, to set the caret so that it blinks every 50 milliseconds (not a very good idea, by the way), you’d write code like this:

Call SetCaretBlinkTime(50)

Be careful! The caret is a global resource, so if you change the blink time for your application, it changes in all applications.

Don’t Sweat the Details

Of course, once you’ve got all the API calls under your belt, you’ve done most of the work required to create the class module. Given that you want to expose the properties listed in FIGURE 1, it’s only a matter of writing the appropriate Property Let and Property Get procedures. Listing One on page XX includes all the property procedures, neatly wrapping up the mess of Windows API calls.

To use the Keyboard class module, treat it like any other class (except, as noted earlier, that you’ll never create more than one instance of the class). For example, to set the caret blink time to 100 milliseconds and to turn on the c and n toggles (preserving the original states of all the settings), you could write code as shown in FIGURE 4.

Dim fCapsLock As Boolean
Dim fNumLock As Boolean
Dim intBlinkTime As Integer

Dim okb As Keyboard

Set okb = New Keyboard
intBlinkTime = okb.CaretBlinkTime
fCapsLock = okb.Capslock
fNumLock = okb.Numlock
okb.CaretBlinkTime = 100
okb.Capslock = True
okb.Numlock = True

' Later, to reset the states:
okb.CaretBlinkTime = intBlinkTime
okb.Capslock = fCapsLock
okb.Numlock = fNumLock

FIGURE 4: Using the Keyboard class to set the caret blink rate and c and n toggles.

Certainly, no one could disagree that using properties of a simple object, such as this one, is far easier than calling the Windows API directly. Once you’ve got this class module completed (and that should be easy: Simply copy and paste the code from the sample file — see the end of the article for download details), you can use it in any VBA application. What’s more, because of IntelliSense, you don’t even need to remember the names of the properties. (I admit, when I find myself required to revert to previous versions of Office or VB, I usually write the code in the current version, taking advantage of IntelliSense, then copy and paste the code into the older version when I’m done. Talk about lazy!)

Give It a Try

Take the Keyboard class, add it to a project, and use it. You’ll see that it couldn’t be easier. And you don’t need to know or remember how those pesky API calls work. As this column progresses, I’m sure we’ll present several more API wrapper classes. Mike Gilbert and I have written a number of these for various projects, and they’re incredibly rewarding to use in production code. Imagine being able to pull out a class that wraps up some complex API work, and use it without digging into obscure function calls and tons of constants. You shouldn’t need to sweat the details more than once, and that’s where class modules come in handy.

Begin Listing One

Property Get KeyboardType() As Long
  ' Determine the type of keyboard on the system.
  ' 1  IBM PC/XT or compatible (83-key) keyboard
  ' 2  Olivetti "ICO" (102-key) keyboard
  ' 3  IBM PC/AT (84-key) or similar keyboard
  ' 4  IBM enhanced (101- or 102-key) keyboard
  ' 5  Nokia 1050 and similar keyboards
  ' 6  Nokia 9140 and similar keyboards
  ' 7  Japanese keyboard
  KeyboardType = GetKeyboardType(0)
End Property

Property Get FunctionKeys() As Long
  ' Determine the number of function keys on the keyboard.
  ' 1  10
  ' 2  12 (sometimes 18)
  ' 3  10
  ' 4  12
  ' 5  10
  ' 6  24
  ' 7  Hardware dependent and specified by the OEM
  FunctionKeys = GetKeyboardType(2)
End Property

Property Get Capslock() As Boolean
  ' Return the Capslock toggle.
  Capslock = CBool(GetKeyState(vbKeyCapital) And 1)
End Property

Property Get Numlock() As Boolean
  ' Return the Numlock toggle.
  Numlock = CBool(GetKeyState(vbKeyNumlock) And 1)
End Property

Property Get ScrollLock() As Boolean
  ' Return the ScrollLock toggle.
  ' ScrollLock = CBool(GetKeyState(vbKeyScrollLock) And 1)
End Property

Property Let Capslock(Value As Boolean)
  ' Set the Capslock toggle.
  Call SetKeyState(vbKeyCapital, Value)
End Property

Property Let Numlock(Value As Boolean)
  ' Set the Numlock toggle.
  Call SetKeyState(vbKeyNumlock, Value)
End Property

Property Let ScrollLock(Value As Boolean)
  ' Set the ScrollLock toggle.
  Call SetKeyState(vbKeyScrollLock, Value)
End Property

Private Sub SetKeyState(intKey As Integer, _
                        fTurnOn As Boolean)
  ' Retrieve the keyboard state, set the particular
  ' key in which you're interested, and then set
  ' the entire keyboard state back the way it
  ' was, with the one key altered.
  Dim abytBuffer(0 To 255) As Byte

  Call GetKeyboardState(abytBuffer(0))
  abytBuffer(intKey) = CByte(Abs(fTurnOn))
  Call SetKeyboardState(abytBuffer(0))
End Sub

Property Let Delay(Value As Long)
  ' Sets the keyboard repeat-delay setting.
  ' Only values 0 through 3 are acceptable. Others will be
  ' set back to 0.
  Call SystemParametersInfo(SPI_SETKEYBOARDDELAY, Value, _
                            0, SPIF_TELLALL)
End Property

Property Get Delay() As Long
  Dim lngValue As Long

  Call SystemParametersInfo(SPI_GETKEYBOARDDELAY, 0, _
                            lngValue, 0)
  Delay = lngValue
End Property

Property Let Speed(Value As Long)
  ' Sets the keyboard repeat-speed setting.
  ' Only values 0 through 31 are acceptable. Others will
  ' be set back to 0.
  Call SystemParametersInfo(SPI_SETKEYBOARDSPEED, Value, _
                            0, SPIF_TELLALL)
End Property

Property Get Speed() As Long
  ' Get the keyboard repeat-speed setting.
  Dim lngValue As Long

  Call SystemParametersInfo(SPI_GETKEYBOARDSPEED, 0, _
                            lngValue, 0)
  Speed = lngValue
End Property

Property Get CaretBlinkTime() As Long
  ' Retrieve the number of milliseconds
  ' between blinks of the caret.
  ' SYSTEM RESOURCE. Change this with care.
  CaretBlinkTime = GetCaretBlinkTime()
End Property

Property Let CaretBlinkTime(Value As Long)
  ' Set the number of milliseconds
  ' between blinks of the caret.
  ' SYSTEM RESOURCE. Change this with care.
  ' Allowable values: 200 to 1200 (multiples of 100)
  Call SetCaretBlinkTime(Value)
End Property

End Listing One

The files referenced in this article are available for download from the Informant Web site at http://www.informant.com/mod/modnewupl.htm. File name: MOD9712KG.ZIP.

Ken Getz and Mike Gilbert are Senior Consultants with MCW Technologies, a Microsoft Solution Provider focusing on Visual Basic and the Office and BackOffice suites. They’ve recently completed VBA Developer’s Handbook and Access 97 Developer’s Handbook (co-authored with Paul Litwin), both for SYBEX.