2006-09-23
| Table of Contents: |
| Rate This Article: | Add This Article To: |
( Page 4 of 5 )
-Level Input">
Using Low-level Input
Programs that use low-level console input typically perform some other processing as a primary function, and process keystrokes, mouse actions, and other events as a secondary function. For many — and perhaps most — scenarios, the KeyAvailable and GetKey methods will provide sufficient capability.
An application can call KeyAvailable periodically to see if a key is available at the console and, if so, call GetKey to obtain the key, and then process as necessary. A variation of that technique would be to spawn a separate thread to handle the input. That second thread blocks on a call to GetKey, and then performs the necessary processing when a key is pressed.
That works fine if all you want is normal keystrokes. But if you want your console program to respond to mouse events, or to respond to Shift key presses, for example, you need a little bit more: a .NET-like event interface around the input buffer events.
Creating such a wrapper is not especially difficult. Mostly it's a matter of figuring out what .NET events you want to create, and then a whole lot of typing to create code that converts the raw event data into events with corresponding EventArgs classes. Finally, I ended up with nine different console input events to which clients can subscribe:
- The
KeyDownandKeyUpevents work as you'd expect, although you can get multipleKeyDownmessages before you get aKeyUp. That is, if you press and hold the 'F' key, you get severalKeyDownmessages, but only oneKeyUpwhen you release the key. MouseMovereports the position of the mouse in the screen buffer, not in the screen buffer window. To determine the position of the mouse in the screen buffer window, subtract the X and Y coordinates from the screen buffer window position from the X and Y coordinates reported by theMouseMoveevent.- The
MouseScrollevent is raised when the user rolls the vertical or horizontal mouse wheel. MouseButtonis somewhat different that what you've become used to with Windows applications. There's noWM_LBUTTONDOWNequivalent. Rather,MouseButtonjust says, "a mouse button was pressed or released." TheButtonStateproperty of theConsoleMouseEventArgsclass tells which buttons are currently down. It's your job to keep track of the previous state to determine which buttons were pressed or released.- As you probably expected,
MouseDoubleClickreports when the user double-clicks a mouse button. TheButtonStateproperty of theConsoleMouseEventArgsparameter reports which of the buttons was double-clicked. Be aware, however, that the first click is also reported in twoMouseButtonevents: one for when the user presses the button, and one for when he lets go. A double click results in four events: - A
MouseButtonevent that describes the first time the mouse button is pressed. - A
MouseButtonevent that says, in effect, "the button was released." - A
MouseDoubleClickmessage for the clicked button. - A
MouseButtonevent that says, again, "the button was released." BufferSizeChangeis raised whenever the screen buffer size changes. Such a change can be caused by your program changing the buffer size, another program changing the buffer size, or the user changing the buffer size through the console menu. This event simply reports the new size of the screen buffer.- The
Focusevent, according to the documentation, is used internally and should be ignored by application programs. AllFocusreports is whether the console is gaining or losing keyboard focus. I find this an incredibly useful bit of information because I can use it to suspend animations or other processing that does not occur with the application does not have the keyboard focus. - Finally, the
Menuevent is raised whenever the user activates the console menu. I thought that this would allow me to see what the user is doing, but in this case the documentation is right. Menu events are used internally and should be ignored. I've included them here just for the sake of completeness, perhaps hoping that Microsoft might in the future allow me to see what menu items are selected.
By default, all of the event types except the window buffer size events are enabled when the input buffer is created. To process window buffer size events, you must set the WindowInput property to true, as shown in Listing 2. If you know that your program won't be handling mouse events, you can turn off mouse input by setting MouseInput to false. Leaving it turned on won't hurt anything, and probably won't seriously impact performance. Any mouse events in the input buffer are discarded as other events are processed.
The code in Listing 2 shows how to handle console input events in your program. The general idea is to set up the event handlers just as you would in any other .NET application, and then call the input buffer object's ProcessEvents method. That method reads the events and raises the proper events.
Listing 2: Using events to process input records
using System;
using System.Collections.Generic;
using System.Text;
using Mischel.ConsoleDotNet;
namespace eventTest
{
class eventTest
{
static private ConsoleScreenBuffer sb;
static void Main(string[] args)
{
Console.Title = "testo";
sb = JConsole.GetActiveScreenBuffer();
try
{
using (ConsoleInputBuffer ib = JConsole.GetInputBuffer())
{
// Enable screen buffer window size events
ib.WindowInput = true;
// show the current input mode
ConsoleInputModeFlags mf = ib.InputMode;
sb.WriteLine(string.Format("Input mode = {0}, hex: {1:X}", mf, (int)mf));
sb.WriteLine(string.Format("Window Input = {0}", ib.WindowInput));
// set up the event handlers
ib.KeyDown += new ConsoleKeyEventHandler(ib_KeyDown);
ib.KeyUp += new ConsoleKeyEventHandler(ib_KeyUp);
ib.MouseButton += new ConsoleMouseEventHandler(ib_MouseButton);
ib.MouseMove += new ConsoleMouseEventHandler(ib_MouseMove);
ib.MouseDoubleClick += new ConsoleMouseEventHandler(ib_MouseDoubleClick);
ib.MouseScroll += new ConsoleMouseEventHandler(ib_MouseScroll);
ib.BufferSizeChange += new ConsoleBufferSizeEventHandler(ib_BufferSizeChange);
ib.Focus += new ConsoleFocusEventHandler(ib_Focus);
ib.Menu += new ConsoleMenuEventHandler(ib_Menu);
// Change buffer size to test window sizing events.
sb.SetBufferSize(100, 300);
// process events. Control+C will exit the application.
while (true)
{
ib.ProcessEvents();
// Sleep at least 1 ms. If you don't do this, your program
// will consume 100% of the processor time.
System.Threading.Thread.Sleep(1);
}
}
}
finally
{
sb.Dispose();
}
}
static void ib_MouseScroll(object sender, ConsoleMouseEventArgs e)
{
bool bScrollDown = (e.ButtonState & ConsoleMouseButtonState.ScrollDown) != 0;
sb.WriteLine(string.Format("Mouse scroll: {0}, {1}",
e.EventType, bScrollDown ? "down" : "up"));
}
static void ib_MouseDoubleClick(object sender, ConsoleMouseEventArgs e)
{
sb.WriteLine(string.Format("Double click: {0}", e.ButtonState));
}
static void ib_MouseMove(object sender, ConsoleMouseEventArgs e)
{
sb.WriteLine(string.Format("Mouse move: ({0},{1})", e.X, e.Y));
}
static void ib_MouseButton(object sender, ConsoleMouseEventArgs e)
{
sb.WriteLine(string.Format("Mouse button: {0}", e.ButtonState));
}
static void ib_KeyUp(object sender, ConsoleKeyEventArgs e)
{
sb.WriteLine(string.Format("Key Up, {0}", e.Key));
}
static void ib_KeyDown(object sender, ConsoleKeyEventArgs e)
{
sb.WriteLine(string.Format("Key Down, {0}", e.Key));
}
static void ib_Menu(object sender, ConsoleMenuEventArgs e)
{
sb.WriteLine(string.Format("Menu event: {0}", e.CommandId));
}
static void ib_Focus(object sender, ConsoleFocusEventArgs e)
{
sb.WriteLine(string.Format("Focus: {0}", e.SetFocus));
}
static void ib_BufferSizeChange(object sender, ConsoleWindowBufferSizeEventArgs e)
{
sb.WriteLine(string.Format("Buffer size change: ({0},{1})", e.X, e.Y));
}
}
}
In my input loop, I make the thread sleep for one millisecond. This causes the operating system not to schedule the thread for execution during that period. If I had specified a sleep value of zero, then the thread would give up the remainder of its timeslice and allow other threads to execute. If no other threads were waiting to execute, then the input thread would be rescheduled immediately. The difference in the amount of processor time used, though, is tremendous. With a sleep value of zero, the program consumes 100% of the available processor time. With a sleep value of one millisecond, the program takes very little—less than one percent—of the available processor time. In most programs I write, a one-millisecond delay isn't even noticeable, so I tend to use a sleep value of one.
If your program spends most of its time waiting on input from the user and doesn't take more than a half second or so to perform any task that the user requests, a structure such as shown in Listing 2 is sufficient.
However, if you want the program to be processing but still be responsive to user input, you have to do something more. In simple cases, you could sprinkle calls to ProcessMessages throughout your program. This can get confusing in a hurry, though, and you end with some odd and difficult to debug reentrancy problems. The best solution in that case is to spawn a new thread so that one thread can handle input and the other can do processing.
When you create a new thread, you have a choice of whether to use the main thread for input, or use the main thread for the processing and push the input off to the background thread. My preference is to do the input on the main thread because it seems simpler, especially when a user input action causes me to spawn another thread. In addition, when the user exits the program, I want the primary thread to shut down all the other threads and clean up gracefully.
It just takes a small change to the program shown in Listing 2 to create a program that simulates using a background thread for processing while keeping the main thread responsive to user input.
First, add the line:
using System.Threading;
at the top of the program. Then, add this code just before the while loop in the Main method:
// Queue the thread System.Threading.ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));Finally, add the
ThreadProc shown here.
static void ThreadProc(Object stateInfo)
{
// Simulate a long process.
while (true)
{
System.Threading.Thread.Sleep(1000);
sb.WriteLine("tick");
}
}
When you run this program, the background thread displays "tick" on the console every second, but the program will still be responsive to user input.
Note that I didn't change any of the console input mode flags that affect how high-level input is processed. That is, processed input is still enabled, as are echo input and end-of-line input mode. Those modes only affect high-level input, so you don't have to disable them if you use low-level input. That also means that you can use Read or ReadLine easily without having to change any modes: simply call those functions rather than ProcessEvents. However, understand that when you're using high-level input, all events other than normal character key events are discarded. You will not be able to receive mouse events that occur when the program is waiting for processed input.
![]() |
|


