Programming Mouse and Keyboard with DirectInput 3.0Peter Donnelly October 25, 1996 Peter Donnelly has been a game designer since 1972 and a programmer since 1984. He has authored four "paper" games with historical and fantasy themes, and his computer design credits include three interactive comic-book adventures and one full-scale adventure game. He has also developed shareware games and game editors for MS-DOS and Windows. AbstractIn September 1996 Microsoft released Version 3.0 of the DirectX™ SDK (software development kit), a set of tools for games and multimedia applications. This version of the SDK is the first to provide support for mouse and keyboard input. In previous versions, DirectInput™ was actually just another name for the extended joystick services in the standard Windows SDK. In this article I will guide you through the process of setting up and using DirectInput objects for mouse and keyboard. I will only touch on the existing joystick services, which are well documented elsewhere. The information here is intended to supplement the documentation found in DINPUT3E.DOC, part of the DirectX SDK. You should have that document at hand as a reference to the data structures and functions I will introduce as I step through the implementation of DirectInput. The Sample ProgramDINPUT.EXE is a simple program that illustrates most of the techniques described in this article. It relies entirely on DirectInput routines for mouse and keyboard input, except for input processed by Windows itself, such as mouse clicks on the menu. Two keystrokes are recognized: Esc to quit the program, and F2 to display absolute mouse coordinates. Clicking the left mouse button displays a message, and the right button flushes the mouse input buffer. Other options are available on the View menu. The program is in the form of a project for Microsoft® Visual C++® version 4.0. It uses C++ syntax but no classes other than the COM objects. It requires Windows® 95 and will not run under Windows NT®. From time to time I'll also be referring to Scrawl, the sample DirectInput application included with the DirectX SDK. You'll probably want to refer to the source code for that application as I go along. The Component Object ModelDirectX is based on the component object model (COM), defined by Williams and Kindel (see Further Reading at the end of this article) as "a component software architecture that allows applications and systems to be built from components supplied by different software vendors." Put another way, it is a protocol that allows software modules to work with one another even when they are written in different languages and running on different platforms. You may find it helpful to have some understanding of COM before delving into the DirectX SDK, but it is possible to implement DirectInput for a single-user Win32 application without getting into the nitty-gritty of COM interfaces. (In COM, an object's interface is a menu of its available methods.) Consequently I won't go into the theory, but at the end of the article I will point you to several good overviews. It is important to note here that although COM is object-oriented, you don't need an object-oriented programming language to use DirectInput or, indeed, any of the DirectX APIs (application programming interfaces). Although the examples I'll present here are written for C++, the samples in the DirectX package all use plain C. The only significant difference is the way of calling member functions of the DirectInput objects. For instance, in C you would use the following syntax to call the CreateDevice function for the DirectInput object (treated as a structure) that is pointed to by lpdi: lpdi->lpVtbl->CreateDevice(lpdi, &guid, &lpdiKeyboard, NULL); Here lpVtbl is a pointer to a table of pointers to functions. In C++, since we can treat lpdi as a pointer to an object, we access the member functions more directly: lpdi->CreateDevice(guid, &lpdiKeyboard, NULL); Setting up--An OverviewBefore an application can start reading the mouse or keyboard with DirectInput, it has to go through a few preliminary steps, first to set up the master DirectInput object and identify any available devices, then to set up each device to be used. Setting up DirectInputThe following two procedures have to be performed once only. The DirectInput object is then ready to manage all mouse and keyboard devices attached to the system. 1. Create the DirectInput object. This sets up the basic framework for handling all input. 2. Enumerate the devices. We obtain the GUID (globally unique identifier) for any input devices attached to the system. At the same time we can do any other processing we like, such as checking for a particular mouse model.This step can be skipped if the application is using only the standard system mouse and keyboard; in this case the predefined global variables GUID_SysMouse and GUID_SysKeyboard can be used to identify the devices. Setting Up a DeviceAfter the DirectInput object is created and the devices have been enumerated, the following steps are needed to set up each input device. Normally this would mean one mouse and one keyboard, but potentially an application could support multiple pointing devices. 1. Create the device. This step creates a code object for the mouse or keyboard and attaches it to the DirectInput object. 2. Set the cooperative level. You let the system know the desired level of access to the device. This step is optional if the default setting (nonexclusive background) is acceptable. 3. Set the data format. Tell DirectInput how to return the information you want about the device when you poll it. 4. Set properties. An optional step taken only if you want to change any of the default properties of the device, e.g. to tell the mouse to return absolute rather than relative coordinates. 5. Acquire the device. Finally, you tell the system that you wish to begin receiving data from the device. This step gives DirectInput a chance to set up the input buffer and otherwise optimize the device for your application. The step has to be repeated whenever your window has "lost" the device because it has been yielded to another application or to the system. Setting Up--The DetailsCreate DirectInput ObjectThe first step in setting up DirectInput in your application is to create the master object that provides access to the COM interface. It's done like this: LPDIRECTINPUT lpdi;DirectInputCreate(hTheInstance, DIRECTINPUT_VERSION, &lpdi, NULL); The first argument is the handle of the application or dynamic-link library (DLL) that is creating the object. The second is always DIRECTINPUT_VERSION, defined in DINPUT.H; this is simply a safety check to make sure the application is running with the correct version of the run-time DLLs. The third argument is the location where we want the pointer to the IDirectInput interface to be stored. Finally, we pass NULL to the function if we want the object to be created normally and initialized automatically. (To put it another way, if you don't know what the documentation for this argument means by "the controlling unknown for OLE aggregation," just pass NULL and don't worry.) If the call succeeds, lpdi points to the IDirectInput interface and, as we've seen, can be used just like a pointer to an object; for example, in C++, lpdi->EnumDevices() calls the EnumDevices method implemented for the IDirectInput interface. Enumerate DevicesThe EnumDevices function finds all mice (including similar devices such as trackballs) and keyboards on the system and calls an application-defined callback function for each one. If you want to restrict the enumeration to devices of one type or the other, pass DIDEVTYPE_MOUSE or DIDVETYPE_KEYBOARD as the dwDevType argument; to enumerate all devices, make this argument zero. You can't use subtypes in the argument to narrow down the search further, but you can check the subtype in the callback function. Here's an example that checks whether the machine has an enhanced keyboard attached. First the callback function: LPDIRECTINPUT lpdi; BOOL hasEnhanced; GUID KeyboardGUID = GUIDSysKeyboard; BOOL CALLBACK DIEnumKbdProc(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef) { *(GUID*) pvRef = lpddi->guidProduct; if (GET_DIDEVICE_SUBTYPE(lpddi->dwDevType) == DIDEVTYPEKEYBOARD_PCENH) { hasEnhanced = TRUE; return DIENUM_STOP; } return DIENUM_CONTINUE; } Then, in the setup section, we do the enumeration: lpdi->EnumDevices(DIDEVTYPE_KEYBOARD, DIEnumKbdProc, &KeyboardGUID, DIEDFL_ATTACHEDONLY); The third argument to EnumDevices can be a pointer to any data you want to process in the callback function. In this case we use it to return the GUID for the enhanced keyboard. We use DIENUM_CONTINUE as the default return value for the callback because we want to be sure to look at all keyboards attached to the system, in the unlikely event that there are more than one. As soon as an enhanced keyboard is found, DIENUM_STOP is returned so that EnumDevices stops enumerating--which doesn't accomplish much in the example except possibly shave a few milliseconds off the application's startup time. Create DeviceNow that you've obtained the GUID for a device, either through EnumDevices or by using the predefined GUID_SysMouse and GUID_SysKeyboard variables, you have to create a code object for communicating with the IDirectInputDevice interface. This is like what we did to create the master DirectInput object pointed to by lpdi. Here we go: LPDIRECTINPUTDEVICE lpdiKeyboard;lpdi->CreateDevice(GUID_SysKeyboard, &lpdiKeyboard, NULL); Our DirectInput object has now created a DirectInputDevice object, pointed to by lpdiKeyboard. This pointer gives us access to all the methods we need. As with CreateDirectInput, the NULL argument ensures that the device is created normally, in which case initialization is automatic.
Set Cooperative LevelThe cooperative level of a device determines the circumstances in which an application has access to it, and whether the Windows system has access. It's set by passing a combination of flags, plus the window handle, to SetCooperativeLevel: lpdiDevice->SetCooperativeLevel(hwnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND) For Windows 95, the valid flag combinations are:
DISCL_BACKGROUND really means "foreground and background." A device with a DISC_BACKGROUND cooperative level can be acquired (i.e. used) by an application at any time, even when the application does not have the focus. In the sample application, if you set the Access option button in the Mouse Properties dialog to Background, and turn on the display of mouse coordinates, you will see the coordinates changing even when another application is in the foreground. With the Foreground setting, the mouse is "unacquired" whenever another application is activated, so the coordinates don't change. The DISC_EXCLUSIVE argument gives the application exclusive access to the mouse while the application is in the foreground. This means that Windows stops monitoring the mouse, and no messages are generated for it. A side effect is that the cursor disappears, since Windows is no longer tracking the axes. (I'll get back to the topic of cursors later in this article.)
Set Data FormatBefore your application can acquire a device, you have to tell DirectInput the format of the data required from the device. This mechanism is doubtless intended to allow future compatibility with joysticks, force-feedback gloves, and other devices. At present, because only the mouse and keyboard are supported, you don't have to worry about the intricacies of the DIDATAFORMAT structure used in the argument toSetDataFormat. DirectInput provides a couple of global variables, c_dfDIKeyboard and c_dfDIMouse, that are declared in DINPUT.H and defined in DINPUT.LIB. This is all you need to do: lpdiKeyboard->SetDataFormat(&c_dfDIKeyboard);lpdiMouse->SetDataFormat(&c_dfDIMouse); Looking at the reference for the DIDATAFORMAT structure, you might be tempted to think that you can tinker with the device properties here by changing the contents of dwFlags. But you can't do this with c_dfDIMouse, because it is a const. If you want to change the properties, do so in the next step. Set PropertiesA device (e.g. mouse) or device object (e.g. mouse axis) has up to four properties:
Only one of these, the buffer size, applies to the keyboard. Two properties, the axis mode and the buffer size, can be altered at runtime. You can't set properties for an acquired device. If it's necessary to change a property after the initial setup, use Unacquire first. Before calling SetProperty (or GetProperty, for that matter), you need to set up a DIPROPDWORD structure, which consists of a header and a data word. Initialize the header with (1) its own size, (2) the size of the DIPROPDWORD structure, (3) an object identifier, and (4) a "how" code indicating the way the object ID should be interpreted. When setting properties, dwObj is always zero and dwHow is always DIPH_DEVICE. (Future versions of DirectInput may permit properties to be changed for individual objects, but for now they can only be changed for devices.) After these rather complicated preliminaries, you pass the address of the header into SetProperty, along with an identifier for the property you want to change. Here's an example that sets the buffer size for a device so that it will hold 10 data items: #define BufferSize 10 DIPROPDWORD dipdw; HRESULT hres; dipdw.diph.dwSize = sizeof(DIPROPDWORD); dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); dipdw.diph.dwObj = 0; dipdw.diph.dwHow = DIPH_DEVICE; dipdw.dwData = BufferSize; hres = lpdiDevice->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph); if (hres != DI_OK) OutputDebugString("Failed to set buffer size.\n"); As I've already stated, DirectInput 3.0 doesn't let you set object properties. So you can't set the axis mode for an individual axis; it must be set for the entire device, using zero in the dwObj field. Acquire DeviceWe've finally reached the last step in making the device ready for use. In DirectInput, "acquiring" a device essentially means getting permission to use it. Acquisition is not permanent; your application may acquire and unacquire the mouse or keyboard many times. It's a bit like an old-fashioned hotel where you're expected to leave your key at the front desk. You've already booked the room and checked in, but you still need to go to the desk every time you enter or leave the building. There might be a single key (exclusive access) or more than one (nonexclusive), but no one gets in without a key. Simple-minded analogies not enough? Okay, here's a more technical explanation of just why an application needs to acquire and unacquire devices. There are two main points. First, DirectInput has to be able to tell the application when the flow of data from the device has been interrupted by the system. For instance, if the user has switched to another application with ALT+TAB, and used the mouse or keyboard in that application, your application needs to know that the input no longer belongs to it and that the state of the buffers may have changed. DirectInput automatically unacquires the device for you in such circumstances. Second, because your application can alter the properties of the device, without safeguards DirectInput would have to check the properties each time you wanted to get data with GetDeviceState or GetDeviceData. Obviously this would be very inefficient. Even worse, messy things could happen like a hardware interrupt accessing a data buffer just as you were changing the buffer size. So DirectInput requires your application to unacquire the device before changing properties. When you reacquire it, DirectInput looks at the properties and decides on the optimal way of transferring data from the device to the application. This is done only once, thereby making GetDeviceState and GetDeviceData lightning-fast. If the device's cooperative level is set to foreground only, it is automatically unacquired whenever the window loses the focus, even to the application's own menu. The DINPUT sample copes by reacquiring the mouse and keyboard whenever the window is reactivated. This may not be the best way of doing things, in light of the warning in the DirectInput documentation that I quoted above--with some mouse drivers, DIERR_INPUTLOST may be reported "more frequently". See SCRAWL.CPP for another way of ensuring that your app is not trying to get data from an unacquired device. There's no harm in attempting to reacquire a device that is already acquired. Repeated calls to Acquire have no effect, and the device can still be unacquired with a single call to Unacquire. Remember, Windows doesn't have access to the mouse when your application is using it in exclusive mode. If you want to let Windows have the mouse, you must let it go. There's an example in the Scrawl application, which responds to a click of the right button by unacquiring the mouse, putting the Windows cursor in the same spot as its own, popping up a context menu, and letting Windows handle the input until a menu choice is made. Using the KeyboardYou can use DirectInput either to take a snapshot of the keyboard state or to maintain a buffer of keyboard incidents, an "incident" being the press or release of a key. (I'll use this word rather than "event" in order to avoid confusion with Windows messages or event objects.) We'll look at buffered input later; for now, here's a bit of code that takes a snapshot and checks for a depressed Esc key: BYTE diKeys[256]; if(lpdiKeyboard->GetDeviceState(256, &diKeys) == DI_OK) { if(diKeys[DIK_ESCAPE] & 0x80) DoSomething(); } The call to GetDeviceState simply returns an array of 256 bytes, each standing for a key. If the key is depressed, the high bit is set. The array can be indexed most conveniently with the DIK_* defines from DINPUT.H. Remember, GetDeviceState returns the present state of the keyboard, not a record of incidents. If your application does something in response to a keystroke, it's your responsibility to make sure the action isn't repeated inappropriately if the key is still down on the next pass through the polling loop. Using the MouseDirectInput Doesn't Do CursorsWhen programming the mouse with the standard Windows API, it's easy to think of the mouse and the cursor as being one and the same. With DirectInput you can't think this way, because DirectInput does not concern itself about the cursor at all. Its mouse services are first and last an interface to the device rolling around on the tabletop, and the screen might as well not exist at all. The Scrawl sample program shows how you can synthesize a mouse-controlled cursor with DirectInput. This technique is necessary if you need to have both a cursor and exclusive access to the mouse in a full-screen application. (Remember, exclusive mode kills the default cursor and all Windows mouse messages.) In other cases, if you need a cursor but not exclusive access--and if performance is not a big issue--it's way easier to let Windows handle cursor movement while you track it with GetCursorPos or WM_MOUSEMOVE messages. You can still use DirectInput to monitor mouse clicks. Bear in mind that if your application does synthesize a cursor, none of the user settings in the Control Panel mouse applet, such as speed and acceleration, are relevant. Also, there is no way for DirectInput to deduce how far the mouse has actually rolled in order to produce a given change of axis. That's why Scrawl allows the user to set mouse sensitivity within the program--at the default setting, the cursor may move too slowly or too quickly in response to mouse movements. Polling the MouseDirectInput bypasses the Windows message system, so the only way to find out what the mouse is up to is to ask it. To poll the mouse in real time (we'll get to buffered input later), use GetDeviceState, which returns the current state of the mouse in a DIMOUSESTATE structure. Here's an example that retrieves the axis values, and produces a beep if the left button is clicked: DIMOUSESTATE diMouseState; BOOL ClickHandled; LONG MouseX, MouseY; if(lpdiMouse->GetDeviceState(sizeof(diMouseState), &diMouseState) == DI_OK) { if(diMouseState.rgbButtons[0] & 0x80) { if(!ClickHandled) { MessageBeep(0); ClickHandled = TRUE; } } else ClickHandled = FALSE; MouseX = diMouseState.lX; MouseY = diMouseState.lY; } We use the ClickHandled variable to keep track of whether the button press has been dealt with. Remember, GetDeviceState returns the current state of the mouse buttons, not presses or releases, so we have to ensure that the program doesn't keep responding to the "mouse down" state. In DINPUT.EXE you can see the results of real-time mouse axis polling after setting Axis Display to Relative or Absolute in the Mouse Properties dialog. Relative and Absolute AxesBy default, the mouse axis coordinates are returned as relative values, i.e. the amount by which they have changed since the last call to GetDeviceState or, in the case of buffered input, since the last item was put in the buffer. You can use SetProperty to have the axes returned as absolute values, which show the position of the mouse relative to an arbitrary point (somewhere in the next county, judging from the size of the default offset in my tests). In both cases the units are "mickeys", a mickey being the smallest measurable movement of a given device. Using Buffered DataWhen real-time input is not what's needed, your application can read events from a buffer instead. It's your responsibility to set the size of the buffer; see the example under Set Properties, above. By default the buffer size is zero, so this is a crucial step. The value in the dwData field of the DIPROPHEADER structure passed into SetProperty is the number of data items you want buffered, not the memory size in bytes or words. For the keyboard, each press and each release of a key is an item of data. For the mouse, an item is a button press or release or any movement that generates an interrupt. The sample program, DINPUT.EXE, buffers both mouse and keyboard input. Press a few keys and then examine the contents of the keyboard buffer by choosing Keyboard buffer on the View menu. Move the mouse slightly, click the left button once or twice, then press the right button to display the contents of the mouse buffer. Displaying the buffer also has the effect of flushing it, though this behavior can be altered. The buffer display shows the sequence number and age of each incident. For the keyboard, the key scan code and the type of action is also shown. For the mouse, you see which button or axis generated the incident, the type of action, and--for axes--the relative movement, unless the axes have been changed to absolute in the Mouse Properties dialog. If more than 10 incidents have occurred since the buffer was last flushed, a buffer overflow warning is displayed as well. When the buffer overflows, it is the most recent events, not the oldest, that are lost. Items are in chronological order within the buffer, so you don't need the sequence numbers for determining the order in which they were generated. But there are a couple of tasks where sequence numbers do come in handy: 1. Recognizing simultaneous events. You'll observe in our sample program that a diagonal movement of the mouse produces the same sequence number for the changes in the X and Y axes. In Scrawl, a line is not drawn till simultaneous changes in the X and Y axes have both been processed--otherwise a right angle would be drawn instead of a slanting line. 2. Finding out which event came first when processing input from more than one device. Take, for example, a quiz game where two players vie to be the first to sound a buzzer, one player using the keyboard while the other uses the mouse. Sequence numbers would allow the application to determine whether the keystroke or the mouse click came first, regardless of how close together they were. (Note that DirectInput maintains a single sequence of numbers, not a separate sequence for each device.) Here's the code our sample uses to retrieve and display the contents of the keyboard buffer: HRESULT FlushKbdBuffer(void) { HRESULT hres; DWORD dwItems; char szScan[7]; char szAge[10]; char szOutput[99]; DWORD k; DIDEVICEOBJECTDATA *lpdidod; dwItems = BUFFERCOUNT; // defined as 10 hres = lpdiKeyboard->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), KbdBuffer, &dwItems, 0); if(hres == DI_BUFFEROVERFLOW) TextToScreen(hMainWindow, "Buffer overflow!"); for (k = 0; k < dwItems; k++) { lpdidod = &KbdBuffer[k]; sprintf(szOutput, "%d", lpdidod->dwSequence); strcat(szOutput, ". Scan code "); sprintf(szScan, "%x", lpdidod->dwOfs); strcat(szOutput, szScan); if (lpdidod->dwData & 0x80) strcat(szOutput, " pressed "); else strcat(szOutput, " released "); sprintf(szAge, "%d ms ago", GetTickCount() - lpdidod->dwTimeStamp); strcat(szOutput, szAge); TextToScreen(hMainWindow, szOutput); } return hres; } Points to note:
Event NotificationDirectInput provides support for thread synchronization in the form of the SetEventNotification function. You can see a simple implementation in Scrawl, which sets up an event object for the mouse with CreateEvent and then orders DirectInput's mouse object to report to the event object with SetEventNotification. The WinMain loop then calls MsgWaitForMultipleObjects to check whether the mouse event is signaled, i.e. whether there is input that needs to be processed. Running Control Panel AppletsI can't imagine the circumstances under which you would want to run Control Panel from your application, since most if not all of the Control Panel settings are ignored by DirectInput. However, if you want to run the applet for a device, it's a simple call: lpdiKeyboard->RunControlPanel(NULL, 0); The first argument is the parent window, and the second argument is for flags, none of which is so far defined. Other Input Devices under DirectInput 3.0Future releases of DirectX will include COM-based services for other input devices including the joystick, game pads, and virtual-reality gloves. At present, however, DirectInput's support for devices other than the mouse and keyboard is nominal; in fact, it is available in the standard Windows SDK in the form of the extended joystick services centered on joyGetPosEx. The joyGetPosEx function is actually quite powerful, supporting up to 32 buttons, six axes, and a point-of-view hat on digital and analog devices. It is well documented elsewhere, and I won't discuss it here further than to point you to the useful articles listed below. Incidentally, the 32-bit joystick driver VJOYD.VXD is improved in DirectX 3. Older versions were flawed, and several companies produced fixes, causing confusion. One problem that has been cleared up is the tendency for the joystick readings to "jitter" when they happened to coincide with DMA (direct memory access) transfers. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|