| . | . |
| . | ![]() |
A Win32 console app is not a DOS box! | . |
| . | . |
| . |
Win32 Console Applications - Part 5.This section of the tutorial covers a few topics which, although not difficult to use, can be a little hard to conceptually grasp. What we're going to use now are console "events". A console "event" is something that happens to the console. There are several types of event, but in this tutorial, we'll concentrate on keyboard and mouse events. Console event handling is a very big area, I couldn't hope to cover everything you can do in a short tutorial like this - if this area interests you, read around MSDN or look up these and other console routines in your compilers help. The function we will use to get console events is ReadConsoleInput(). This function takes 4 parameters. The first is the input handle which we've already covered. The second parameter is a pointer to a INPUT_RECORD structure. This is a fairly simple structure which contains a value indicating the type of event that has happened, (keyboard, mouse etc.), and another structure which contains the details of the event. The third parameter is the number of events to read. Console events are buffered - that means the system keeps recording events as they happen, but only delivers them to your program when you ask for one. They are delivered in the order in which they happened. Often, you will ask for one event at a time, i.e. get an event - process it - get the next. You can, if you wish, use the third parameter to read several events into an array of INPUT_RECORD structures. If you ask for more events than exist in the buffer, the routine will return those available, and the fourth parameter will contain the actual number retrieved, i.e. it will not wait until there are the requested number. Something that is very important to grasp is that if there are no events in the buffer, the call to ReadConsoleInput() will not return! It is a blocking call, it waits until there is one. In a later program, I will show you how to avoid blocking until an event happens. Consider this program. It is well worth compiling this and running it for yourself.
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
HANDLE hIn;
HANDLE hOut;
COORD KeyWhere;
COORD MouseWhere;
COORD EndWhere;
bool Continue = TRUE;
int KeyEvents = 0;
int MouseEvents = 0;
INPUT_RECORD InRec;
DWORD NumRead;
hIn = GetStdHandle(STD_INPUT_HANDLE);
hOut = GetStdHandle(STD_OUTPUT_HANDLE);
cout << "Key Events : " << endl;
cout << "Mouse Events : " << flush;
KeyWhere.X = 15;
KeyWhere.Y = 0;
MouseWhere.X = 15;
MouseWhere.Y = 1;
EndWhere.X = 0;
EndWhere.Y = 3;
while (Continue)
{
ReadConsoleInput(hIn,
&InRec,
1,
&NumRead);
switch (InRec.EventType)
{
case KEY_EVENT:
++KeyEvents;
SetConsoleCursorPosition(hOut,
KeyWhere);
cout << KeyEvents << flush;
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
{
SetConsoleCursorPosition(hOut,
EndWhere);
cout << "Exiting..." << endl;
Continue = FALSE;
}
break;
case MOUSE_EVENT:
++MouseEvents;
SetConsoleCursorPosition(hOut,
MouseWhere);
cout << MouseEvents << flush;
break;
}
}
return 0;
}
The output of the program, after several key presses and mouse actions, and then pressing the 'x' key is hown here.
The first thing to notice is the loop...
while (Continue)
{
ReadConsoleInput(hIn,
&InRec,
1,
&NumRead);
...
}
... you'll see that although the loop is constructed to be continuously looping, the program is not in fact doing so. This is what I mean by ReadConsoleInput() being a blocking call. It only returns when there is something to return. Look at the task manager display for example, and you will see that the program is not using any CPU time, normally, a continuously executing loop would be. Next, look at the switch statement...
switch (InRec.EventType)
{
case KEY_EVENT:
...
break;
case MOUSE_EVENT:
...
break;
}
... the INPUT_RECORD structure has a member called EventType, which says what kind of event is being reported. Windows defines constants for the values returned, in this case, we check to see if the event reported is a KEY_EVENT - a keyboard event, or MOUSE_EVENT - a mouse event. In both cases, we bump a counter. In the case of a keyboard event, there is this additional code...
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
{
SetConsoleCursorPosition(hOut,
EndWhere);
cout << "Exiting..." << endl;
Continue = FALSE;
}
... this looks at the other item in an INPUT_RECORD structure, the actual event record. The content of this structure varies depending on the type of event. It would not, for example, be sensible to include mouse wheel events in a keyboard event structure! In the INPUT_RECORD structure it is declared in fact, as a union of different EVENT_RECORD structures.
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
InRec.Event now says we want to look at the actual details of what happened, rather than general information about an event.
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
We know already that this is a keyboard event, this code exists within the KEY_EVENT case. So we can select the KeyEvent member of the EVENT_RECORD union.
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
uChar is a member of the KEY_EVENT_RECORD structure, and is, again, a union. Why a union? Because some systems work with 8bit ANSI characters and others with 16bit UniCode characters, we need to specify which we want.
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
This says I want the ASCII code of the key that was pressed. The entire if() statement is saying has the lower case 'x' character been pressed? If so, we set the exit conditions for the loop and terminate the program. It is worth having a play with this program. Note, for example, that pressing a key generates 2 events, a key going down and it coming back up again are reported seperately. Leaving a key pressed down for a while can, on systems so configured, cause the auto repeat function to start, each "repeat is reported as a seperate "key down" event. Similar to key presses, a mouse button going down and coming up are seperate events. Sweeping the mouse back and forth across the screen generates a lot of events which stop if the mouse pointer moves out of the console area. In the last program, the loop only executed when an event happened. In the next program, the opposite is the case, it loops contiuously until an event occurs, when it does, it processes it. It addresses some of the other issues found in the first program as well. Here's the program.
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
HANDLE hIn;
HANDLE hOut;
COORD KeyWhere;
COORD LoopWhere;
COORD EndWhere;
bool Continue = TRUE;
DWORD EventCount;
int LoopCount = 0;
int KeyEvents = 0;
INPUT_RECORD InRec;
DWORD NumRead;
unsigned char HoldKey;
hIn = GetStdHandle(STD_INPUT_HANDLE);
hOut = GetStdHandle(STD_OUTPUT_HANDLE);
cout << "Key Events : " << flush;
KeyWhere.X = 15;
KeyWhere.Y = 0;
LoopWhere.X = 0;
LoopWhere.Y = 1;
EndWhere.X = 0;
EndWhere.Y = 2;
while (Continue)
{
SetConsoleCursorPosition(hOut,
LoopWhere);
cout << LoopCount++ << " " << flush;
Sleep(10); // To slow it down!!
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
while (EventCount > 0)
{
ReadConsoleInput(hIn,
&InRec,
1,
&NumRead);
if (InRec.EventType == KEY_EVENT)
{
if (InRec.Event.KeyEvent.bKeyDown)
{
HoldKey = InRec.Event.KeyEvent.uChar.AsciiChar;
}
else
{
++KeyEvents;
SetConsoleCursorPosition(hOut,
KeyWhere);
cout << KeyEvents << flush;
LoopCount = 0;
if (HoldKey == 'x')
{
if (InRec.Event.KeyEvent.dwControlKeyState & LEFT_ALT_PRESSED)
{
SetConsoleCursorPosition(hOut,
EndWhere);
cout << "Exiting..." << endl;
Continue = FALSE;
}
}
}
}
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
}
}
return 0;
}
The program output looks like this.
Once again, the program's loop runs until the boolean variable "Continue" becomes false. The program outputs the loop counter which is reset to zero each time a key is pressed.
Sleep(10); // To slow it down!!
This line I added because on the machine I am working, the loop was executing so fast, I couldn't see the results! Remove it or change the value of the Sleep(), (Sleep() causes the programs execution to suspend for the specified number of milliseconds).
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
This function "looks ahead" to see how many events are in the programs input queue, the number being returned in "EventCount".
while (EventCount > 0)
{
ReadConsoleInput(hIn,
&InRec,
1,
&NumRead);
...
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
}
The inner loop retrieves and processes outstanding events if any.
if (InRec.EventType == KEY_EVENT)
{
...
}
This if() block is saying "we are only interested in keyboard events", it is like the case: KEY_EVENT in the last program.
if (InRec.Event.KeyEvent.bKeyDown)
{
HoldKey = InRec.Event.KeyEvent.uChar.AsciiChar;
}
else
{
...
}
This if() requires a little explanation. The bKeyDown member of the KEY_EVENT_RECORD is a boolean field and is TRUE if the event is reporting a key going down, (remember, the auto repeat is reported as a series of key down events), and FALSE if it is a key up event, (reported once). By checking to see if this is FALSE we get a single event for a key press even if someone holds the key down for a while, this is often very useful. That is what I want to do in this program. So why am I keeping a copy of the key pressed in the key down event? I'll explain why in a short while.
++KeyEvents;
SetConsoleCursorPosition(hOut,
KeyWhere);
cout << KeyEvents << flush;
If the event is a key up event, we increment the event counter and zero the loop counter. This is old stuff.
if (HoldKey == 'x')
{
if (InRec.Event.KeyEvent.dwControlKeyState & LEFT_ALT_PRESSED)
{
SetConsoleCursorPosition(hOut,
EndWhere);
cout << "Exiting..." << endl;
Continue = FALSE;
}
}
The next test checks to see if the key we kept a copy of in the key down event is the 'x' character. Next, we do an additional test, we check the state of the dwControlKeyState member of the KEY_EVENT_RECORD structure. In this case, we check to see if the left 'Alt' key is also down, i.e. the user has actually pressed 'Alt - x', if so we set the exit condition as before. In the default console mode, many Alt and Ctrl keys are not passed to the program, rather, they are handled by Windows - the upshot of that is that when the user presses 'Alt - x', the key down event correctly reports the key pressed, (hence we can hold a copy of it). When the key up event occurs, because Windows deals with the 'control key - key' combination, the identity of the key going up is not reported! Thus if we want to react to 'Alt -x', we need to jump through this little hoop. There are several other 'control keys' you can test for, browse your compilers documentation for inspiration! This program thus loops continually, reacting to key presses in any way desired and exits when 'Alt - x' is pressed. A very useful skeleton program. The basis of many a console mode game. Why did I show this "fiddle? There are, in fact, several reasons. Firstly, from a program design point of view, by terminating on a complex "key" i.e. 'Alt-x', I reduce the chances of someone accidentally closing the program by striking the wrong key. I also retain the use of all of the normal keys for use in my application. Secondly, I wanted to demonstrate the technique, it is a bit of a fiddle, but it is there if you want/need it. Important it will not work with all control sequences, 'Ctrl - c' for example. Thirdly, I wanted to comment on the concept of console "mode" which frankly, is outside the scope of this tutorial. There is an API routine, SetConsoleMode(), which allows you to select various input/output options. In this tutorial I have assumed the default mode. The "trick" I have used here gets around the "ENABLE_PROCESSED_INPUT" mode, by which many "special" characters/sequences which are normally intercepted/handled and not reported/hidden by the OS can be "caught". If you intend to "get around" many of these features, it may be worth resetting the console mode to remove this flag, however, doing so will remove normal processing of things like tab and carriage return, you will have to process these yourself, (for this reason, I have elected not to use this technique in the tutorial). I have already shown that capturing mouse events is just as easy as keyboard events. This program demonstrates basic mouse input. It is based on the last program, in as much as the loop runs continually, and reports mouse events when they happen. All of the basic mouse events are demonstrated.
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
HANDLE hIn;
HANDLE hOut;
COORD MouseWhere = {19, 0};
COORD DClickWhere = {19, 1};
COORD LeftWhere = {19, 2};
COORD RightWhere = {19, 3};
COORD WheelWhere = {19, 4};
COORD LoopWhere = {0, 5};
COORD EndWhere = {0, 6};
bool Continue = TRUE;
DWORD EventCount;
int LoopCount = 0;
int KeyEvents = 0;
INPUT_RECORD InRec;
DWORD NumRead;
hIn = GetStdHandle(STD_INPUT_HANDLE);
hOut = GetStdHandle(STD_OUTPUT_HANDLE);
cout << "Mouse is at : " << endl;
cout << "Double Click at : " << endl;
cout << "Left button is : Up" << endl;
cout << "Right button is : Up" << endl;
cout << "Wheel scrolled : " << flush;
while (Continue)
{
SetConsoleCursorPosition(hOut,
LoopWhere);
cout << LoopCount++ << " " << flush;
Sleep(10); // To slow it down!!
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
while (EventCount > 0)
{
ReadConsoleInput(hIn,
&InRec,
1,
&NumRead);
if (InRec.EventType == KEY_EVENT)
{
if (InRec.Event.KeyEvent.uChar.AsciiChar == 'x')
{
SetConsoleCursorPosition(hOut,
EndWhere);
cout << "Exiting..." << endl;
Continue = FALSE;
}
}
else if (InRec.EventType == MOUSE_EVENT)
{
if (InRec.Event.MouseEvent.dwEventFlags == 0)
{
SetConsoleCursorPosition(hOut,
LeftWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0x01)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
SetConsoleCursorPosition(hOut,
RightWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0x02)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
}
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_MOVED)
{
SetConsoleCursorPosition(hOut,
MouseWhere);
cout << InRec.Event.MouseEvent.dwMousePosition.X << "," <<
InRec.Event.MouseEvent.dwMousePosition.Y << " " << flush;
}
else if (InRec.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
{
SetConsoleCursorPosition(hOut,
DClickWhere);
cout << InRec.Event.MouseEvent.dwMousePosition.X << "," <<
InRec.Event.MouseEvent.dwMousePosition.Y << " " << flush;
}
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_WHEELED)
{
SetConsoleCursorPosition(hOut,
WheelWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0xFF000000)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
}
}
GetNumberOfConsoleInputEvents(hIn,
&EventCount);
}
}
return 0;
}
A typical output looks like this.
Most of the program's logic is the same as the last, so I will not dwell on it. We loop checking to see if there are any console events, if there are we process them. A single keyboard event is trapped, that when an 'x' is pressed, when we set the exit condition for the loop, which basically runs the whole time interupted only when mouse events occur. All of the mouse events available are described below.
else if (InRec.EventType == MOUSE_EVENT)
{
if (InRec.Event.MouseEvent.dwEventFlags == 0)
{
...
}
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_MOVED)
{
...
}
else if (InRec.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
{
...
}
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_WHEELED)
{
...
}
}
This if() block identifies the available mouse events. Oddly, there are defined constants for movement, double clicking and wheel action, but not for mouse button events which are represented by having the dwEventFlags member of the MOUSE_EVENT_RECORD set to zero. The contents of each block are loosely described below, but most of it, by now, should be obvious!
if (InRec.Event.MouseEvent.dwEventFlags == 0)
{
SetConsoleCursorPosition(hOut,
LeftWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0x01)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
SetConsoleCursorPosition(hOut,
RightWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0x02)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
}
If the dwEventFlags is zero, a mouse button event has happened. Most mice have 2 buttons, and in the past all had only 2. Bear this in mind while I explain the apparently ridiculous way of checking which button has been pressed or released! Firstly, note that as with keys, a mouse button going down, and coming up are reported as seperate events. Traditionally, it is the button up event that is regarded as significant, and I would advocate you design your applications with this in mind. Simply, if a user places the mouse where they want to click, presses the button, then change their mind, (users huh?), they can move the pointer away from where it was and release the button and try again. Now, bit 0 in the dwButtonState member of the MOUSE_EVENT_RECORD represents the left button on the mouse, so we do a bitwise AND to see if this bit is set, if so, we output "down", otherwise, "up". Bit 1, represents the right button, and we do the same as before to flag it's state. I would always advocate using the declared constant rather than using a numerical value. I have broken my own rule here, because on a web site, horizontal scrolling is often a problem for users of certain browsers. There are symbolic constants for these values, namely, 0x01 is FROM_LEFT_1ST_BUTTON_PRESSED and 0x02 is RIGHTMOST_BUTTON_PRESSED. These I find simply too cumbersome to use. There are others, if you have a multibuttoned mouse, i.e. FROM_LEFT_2ND_BUTTON_PRESSED, (0x04), FROM_LEFT_3RD_BUTTON_PRESSED, (0x08), and FROM LEFT_4TH_BUTTON_PRESSED, (0x10), use them, do as I say, not as I do!
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_MOVED)
{
SetConsoleCursorPosition(hOut,
MouseWhere);
cout << InRec.Event.MouseEvent.dwMousePosition.X << "," <<
InRec.Event.MouseEvent.dwMousePosition.Y << " " << flush;
}
At each event, the position of the mouse pointer is reported in a COORD structure. Remember, consoles are character cell based screens. Although nudging the mouse only a very small amount will generate a MOUSE_MOVED event, the result is always reported in character cell coordinates, thus moving the mouse a pixel or two, will generate events, but not change the value reported for the mouse position.
else if (InRec.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
{
SetConsoleCursorPosition(hOut,
DClickWhere);
cout << InRec.Event.MouseEvent.dwMousePosition.X << "," <<
InRec.Event.MouseEvent.dwMousePosition.Y << " " << flush;
}
If I have to explain to you what a double click is, then you really need to stop reading this tutorial now! The DOUBLE_CLICK event is reported on the "button up" of the second click. 2 clicks close together are regarded as a double click depending upon the mouse settings in the computers "Control Panel" settings.
else if (InRec.Event.MouseEvent.dwEventFlags == MOUSE_WHEELED)
{
SetConsoleCursorPosition(hOut,
WheelWhere);
if (InRec.Event.MouseEvent.dwButtonState & 0xFF000000)
{
cout << "Down" << flush;
}
else
{
cout << "Up " << flush;
}
}
Mouse wheels are a relatively new inovation. Early version of Windows, (pre Windows 2000), do not support them at all. If you have a wheel mouse and want to use it, well, you can. Whether the wheel is rolled up or down, is not actually documented at MSDN. By experiment, I have discovered the code shown, and it seems to work, however, as with any undocumented code, the results are not guaranteed. I have not demonstrated this, but it is possible to use the same control key events with mouse events as we used with keyboard events. Thus it is quite possible to check for 'Alt-RightClick', 'Ctrl-LeftClick' etc. In the next part of this tutorial, I investigate the console's size and a few other generally useful functions. |
. |
| . | . |
| . | . |
| . | . |