
Chapter 8: Event-Driven Input
8.1: Event-Driven Input Versus "Hurry Up and Wait"
Immediately after a new Win32 programmer gets over the shock of discovering that event-driven output is nothing like the type of programming they're used to, they take a look at event-driven input and go into shock again. Most programmers are comfortable using statement like "readln" and "gets" that they can call to read whole lines of text, lists of values, etc. This has become known as the "hurry up and wait" model because the programmer writes code that runs as quickly as possible between two input operations (so the user doesn't notice a delay between inputs) and then the computer sits idle for a very long time waiting for theuser to enter data from the keyboard.
The event-driven input model is far more efficient from the CPU's point of view. The operating system sends your application messages whenever there is some input to process. Therefore, the system isn't just sitting around waiting for the input to arrive - it can be doing other things (including passing other messages on to your application for processing).
Unfortunately, from the application's point of view, event-driven input is far more complex. With a statement like "readln( i32);" in Pascal or "cin >> i32;" in C++, you're telling the operating system to take charge until the user enters an integer value (assuming i32 is an integer variable). The application doesn't have to deal with collecting the individual characters together that make up the string that represents this integer. Nor does the application have to deal with the conversion of that string to an integer value. More importantly, the application doesn't have to worry about dealing with other possible inputs while waiting for the user to input this integer value. All of these simplifications go away when working with input in an event-driven system like Windows.
The input model in Windows is actually quite simple. If an input event occurs (that is associated with your application), then Windows sends your application a message specifying the type of input event. The bad news is that these are very simple events, like "someone just pressed a key on the keyboard" or "someone just pressed a button on the mouse." It is up to your application to interpret these events in an abstract manner (e.g., the user has pressed a sequence of keystrokes whose ASCII codes form a numeric string that the application can convert to an integer value).
While the concept is simple, there is a major problem with the Windows' model: the user of your application can decide to pause the input of some integer value (for example) half-way through the input operation, switch to some other input box and enter data there, then return and finish the original input string. This fact adds conceptual complexity on top of programming complexity (that is, it's hard to visualize how the end-user might behave when designing the application, thus increasing the likelihood that your program may not consider some possible input sequence by the user). In this chapter, we're going to consider how to deal with these complexities.
Although the primary focus of this chapter is going to be on the keyboard, we're also going to look at mouse input and timer events in this chapter. Mouse (or pointer device) input is probably the second-most common form of input in use under Windows. Timer events are less common, but very important. A timer event lets you wake up an application by sending it a periodic (or timed) message even in the absence of any other events or messages directed at the application. We'll consider these three forms of input in this chapter.
8.2: Focus on Focus
Because a typical PC has only a single set of input devices, applications must share the use of these input devices. In particular, the operating system will typically send all input events some device generates to a single application. For example, the operating system only sends keystrokes to a single application's window (imagine the confusion that would ensue if the OS sent the same keystroke messages to a word processor, a spreadsheet, and a database application simultaneously). To control which application (or, more specifically, which window in an application) receives input messages, Windows uses the concept of input focus.
Focus is associated with the currently active Window. This is the window to which Windows will send all keyboard (and possibly other) input events. If the user switches between active windows on their desktop, then the input focus switches to the new active window and all keyboard messages are sent to the new active window rather than the original window.
When the Windows makes some window the active window, it sends that application's window procedure a w.WM_SETFOCUS message so the application is made aware that it might be receiving keyboard messages before too much longer. Conversely, when the user switches from one window to another, Windows first sends the window that originally had the keyboard focus a w.WM_KILLFOCUS message to let it know that a focus change is about to occur and should take appropriate action.
8.3: Stick and Caret
Whenever a window is expecting keyboard input, it usually displays a flashing block, underline, vertical bar, or some other symbol to indicate the position on the window where the application will place any characters input via the keyboard. Although the common term for this symbol is cursor, Windows actually uses the term caret to describe the keyboard input position1. In this section we'll take a look at the functions that control the display of the caret within your application's window.
Because only one window can have the input focus, that is, only one application window can receive keyboard input, there is only one system-wide caret. It wouldn't do for two open application windows to be displaying that blinking caret - the poor user wouldn't be able to tell which window would receive the next input character that they type. Therefore, your applications can display the caret when they are given the input focus and they must relinquish the input caret when they give up the input focus. As noted above, Windows sends your application's window procedure the w.WM_SETFOCUS and w.WM_KILLFOCUS messages when it gives or retracts the input focus (respectively). By handling these messages, your applications can properly obtain and release the caret as needed.
When an application is given the focus (i.e., it receives a w.WM_SETFOCUS message) it should call the w.CreateCaret function to create a caret specifically for the application. The prototype for this function is the following:
static CreateCaret: procedure ( hWnd :dword; hBitmap :dword; nWidth :dword; nHeight :dword ); @stdcall; @returns( "eax" ); @external( "__imp__CreateCaret@16" );The hWnd parameter is the handle of the window that will own the caret (e.g., your application's main window). The hBitmap parameter is the handle of a bitmap object that Windows will use for the caret. If this parameter is NULL, Windows will not use a bitmap for the caret and will, instead, use the nHeight and nWidth parameters to define the caret. The nHeight and nWidth parameters specify a block-style cursor that is nWidth pixels wide and nHeight pixels high.
When your program gives up the focus (i.e., when you receive a w.WM_KILLFOCUS message), you must destroy the caret you've created with a w.DestroyCaret API call. Here's the prototype for that function:
static DestroyCaret: procedure; @stdcall; @returns( "eax" ); @external( "__imp__DestroyCaret@0" );Note that this function doesn't require any parameters. Because there is only one system caret, Windows will destroy the only one in existence (the one associated with the window handle you originally passed to w.CreateCaret).
Whenever you create a caret via the w.CreateCaret API call, Windows creates an invisible caret. In order to actually display the caret you need to call the w.ShowCaret API function. While your application is holding the focus (and the caret), you can make the caret invisible again by calling the w.HideCaret API function. Here are their prototypes:
static ShowCaret: procedure ( hWnd :dword ); @stdcall; @returns( "eax" ); @external( "__imp__ShowCaret@4" ); HideCaret: procedure ( hwnd :dword ); @stdcall; @returns( "eax" ); @external( "__imp__HideCaret@4" );For both procedures, the hwnd parameter is the handle of the window that currently has the caret attached to it. This must be the same handle you originally passed to the w.CreateCaret function.
Windows maintains an internal "caret visible" counter that w.ShowCaret increments and w.HideCaret decrements. While this counter is positive, Windows shows the caret. When this value is zero, Windows hides the cursor. Therefore, if you call w.ShowCaret several times without a comparable number of calls to w.HideCaret, Windows will keep the caret visible until you've made the corresponding number of calls to w.HideCaret.
Once you enable the display of the caret in your application's window, you control the position of the caret via the w.SetCaretPos function call. This function has the following prototype:
static SetCaretPos: procedure ( X :dword; Y :dword ); @stdcall; @returns( "eax" ); @external( "__imp__SetCaretPos@8" );The (X,Y) parameters specify the x-coordinate and y-coordinate of the caret in the client area of your window. In particular, these coordinates specify the upper-left hand corner of the caret bitmap or block in your window.
Armed with this information about the caret and the w.WM_SETFOCUS and w.WM_KILLFOCUS messages, it's now possible to write some code that will automatically show the caret whenever your application gains the focus and hides the caret whenever it loses the focus. Here are a couple of routines, SetFocus and KillFocus, that handle these messages and take the appropriate actions:
// SetFocus- // // This procedure gets called whenever this application gains the // input focus. procedure SetFocus( hwnd: dword; wParam:dword; lParam:dword ); begin SetFocus; w.CreateCaret( hwnd, NULL, AverageCharWidth, AverageCharHeight ); w.SetCaretPos( 0, 0 ); // "Home" the cursor w.ShowCaret( hwnd ); xor( eax, eax ); // Return success end SetFocus; // KillFocus- // // Processes the WM_KILLFOCUS message that gets sent whenever this // application is losing the input focus. procedure KillFocus( hwnd: dword; wParam:dword; lParam:dword ); begin KillFocus; w.HideCaret( hwnd ); w.DestroyCaret(); xor( eax, eax ); // Return success end KillFocus;These routines are not completely general. First of all, the SetFocus function always homes the cursor to position (0, 0). Second, you might not always want to show the caret whenever you get the focus (e.g., the application may not be prepared to accept keyboard input just because it has received the focus). Nonetheless, these two message handling procedures demonstrate how to call these five different API functions.
There are a couple of additional API functions related to the caret that you might find useful. The first of these is w.GetCaretPos which returns the current caret position in the window. The prototype for this function is the following:
static GetCaretPos: procedure ( var lpPoint :POINT ); @stdcall; @returns( "eax" ); @external( "__imp__GetCaretPos@4" );This function returns the current caret position in the lpPoint parameter you pass by reference (w.POINT objects have an x and a y field that receive the caret coordinates).
The w.GetCaretBlinkTime and the w.SetCaretBlinkTime functions have the following prototypes:
static GetCaretBlinkTime: procedure; @stdcall; @returns( "eax" ); @external( "__imp__GetCaretBlinkTime@0" ); SetCaretBlinkTime: procedure ( uMSeconds :dword ); @stdcall; @returns( "eax" ); @external( "__imp__SetCaretBlinkTime@4" );The w.GetCaretBlinkTime function returns the number of milliseconds that pass between inversions of the caret on the display (the alternating inversions of the bitmap are what causes the caret to "blink"). The uMSeconds parameter you pass to w.SetCaretBlinkTime specifies the number of milliseconds between inversions of the caret bitmap.
8.4: Keyboard Messages
Windows sends several different keyboard related messages to your applications. In fact, a single keystroke typically winds up sending three different messages to your application's window procedure. Fortunately, you can ignore many of the messages Windows sends to your applications. Most of the time, there are only two types of messages to which you will normally respond. Nevertheless, it's important to understand the purpose of each of these messages in order to properly process the keyboard messages that are important to you.
Windows will send the following keyboard messages to your application:
- w.WM_KEYDOWN
- w.WM_KEYUP
- w.WM_SYSKEYDOWN
- w.WM_SYSKEYUP
- w.WM_CHAR
- w.WM_SYSCHAR
- w.WM_DEADCHAR
- w.WM_SYSDEADCHAR
Most of these messages you can ignore. In fact, the vast majority of the time you can get by processing only w.WM_KEYDOWN and w.WM_CHAR messages.
To understand the purpose of each of these messages, a brief discussion of the keyboard's operation is necessary. The standard PC keyboard does not produce ASCII character code whenever you process a key. Instead, the keyboard sends out one of two numeric codes (known as "scan codes"). One scan code indicates that the user has just pressed the key (a down code), a second scan code indicates that the user has released the key (an up code). Usually, you will get one up code for each down code (i.e., the user presses and releases a key, generating the two key codes). The exception is when the user holds down a key long enough for it to being autorepeating. In this case you will get a sequence of down codes without a corresponding up code. When the user finally releases the key, you will get a single up code for that key.
Note that a user can hold a key down while pressing other keys. The system can use the down and up codes to determine if you are holding down one key (e.g., a shift or control key) while pressing and releasing other keys. This allows the system to translate a scan code for some key like "A" into different ASCII codes, based on whether you're holding down other keys while pressing the "A". For example, "A" without a modifier key like shift, control, or alt, produces the `a' character; holding down shift while pressing "A" produces the "A" character; holding down the control or alt key produces CTRL-A or ALT-A, respectively.
An important thing to realize is that keyboard scan codes are not the same thing as ASCII codes. These are simply some numeric values that the hardware manufacturer chose when desiging the keyboard. Indeed, different manufacturers have been known to use different scan code sets for their computer keyboards. To eliminate problems with different scan code sets, Windows defines a "virtual keycode" set. People who write keyboard drivers translate their "OEM scan codes" into virtual keycodes and pass those keycodes on to applications. This allows applications to deal with a single set of codes rather than having to worry about differences in the underlying hardware. Table 8-1 lists the standard Windows virtual key codes.
Table 8-1: Windows Virtual Keycodes
The first pair of messages to look at are the w.WM_KEYDOWN and w.WM_KEYUP messages. Windows sends these two messages whenever the user presses or releases an application key (versus a system key), respectively. The wParam field of the message payload (i.e., the wParam parameter in the window procedure call) specifies the Windows' virtual keycode for the message (see Table 8-1). The lParam parameter contains the information found in Table 8-2.
Table 8-2: lParam Data in a w.WM_KEYDOWN or w.WM_KEYUP Message
The w.WM_KEYDOWN and w.WM_KEYUP messages are useful for determining when the user presses a function key, cursor control key, or other special (non-ASCII) key on the keyboard. In fact, Windows will send these messages for every key you press on the keyboard if you are not holding the ALT key down simultaneously. If you are holding down the ALT key when you press a key on the keyboard, Windows will actually send w.WM_SYSKEYDOWN and w.WM_SYSKEYUP messages to your application. In general, however, your application should simply ignore these messages and let the default message handler process them. Windows will convert system keyboard messages into other message types and may pass those new messages on to your application for further processing.
To demonstrate how you would use the w.WM_KEYDOWN and w.WM_KEYUP messages in your applications to process virtual scan codes, we'll modify the System Metrics application that originally appeared in the chapter on Windows' text processing. This kbSysmet program extends the sysmet program by handling w.WM_KEYDOWN messages and translating certain cursor control keys (the arrow keys, page up, page down, home, and end) into messages that will cause the application to scroll the window in appropriate directions. The following is a typical case in a switch statement:
// If they press the "HOME" key, scroll to the top of the window. // Do this by sending a w.WM_VSCROLL message to do the scrolling // routines to reposition the window to the beginning. case( w.VK_HOME ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_TOP, 0 );Other cases handle the other cursor control operations by translating the key press message into a corresponding mouse event on the scroll bar.
// kbSysmet.hla- // // System metrics display program that supports keyboard messages. unit kbSysmets; // Set the following to true to display interesting information // during program operation. You must be running // the "DebugWindow" application for this output to appear. ?debug := false; #includeonce( "excepts.hhf" ) #includeonce( "conv.hhf" ) #includeonce( "hll.hhf" ) #includeonce( "memory.hhf" ) #includeonce( "w.hhf" ) #includeonce( "wpa.hhf" ) #includeonce( "winmain.hhf" ) ?@NoDisplay := true; ?@NoStackAlign := true; type // Data type for the system metrics data array: MetricRec_t: record MetConst :uns32; MetStr :string; MetDesc :string; endrecord; static AverageCapsWidth :dword; // Font metric values. AverageCharWidth :dword; AverageCharHeight :dword; ClientSizeX :int32 := 0; // Size of the client area ClientSizeY :int32 := 0; // where we can paint. MaxWidth :int32 := 0; // Maximum output width VscrollPos :int32 := 0; // Tracks where we are in the document VscrollMax :int32 := 0; // Max display position (vertical). HscrollPos :int32 := 0; // Current Horz position. HscrollMax :int32 := 0; // Max Horz position. readonly ClassName :string := "kbSysmetsWinClass"; // Window Class Name AppCaption :string := "kbSysmets Program"; // Caption for Window // The dispatch table: // // This table is where you add new messages and message handlers // to the program. Each entry in the table must be a MsgProcPtr_t // record containing two entries: the message value (a constant, // typically one of the w.WM_***** constants found in windows.hhf) // and a pointer to a "MsgProcPtr_t" procedure that will handle the // message. Dispatch :MsgProcPtr_t; @nostorage; MsgProcPtr_t MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication ], MsgProcPtr_t:[ w.WM_PAINT, &Paint ], MsgProcPtr_t:[ w.WM_CREATE, &Create ], MsgProcPtr_t:[ w.WM_HSCROLL, &HScroll ], MsgProcPtr_t:[ w.WM_VSCROLL, &VScroll ], MsgProcPtr_t:[ w.WM_SIZE, &Size ], MsgProcPtr_t:[ w.WM_KEYDOWN, &KeyDown ], // Insert new message handler records here. MsgProcPtr_t:[ 0, NULL ]; // This marks the end of the list. readonly MetricData: MetricRec_t[] := [ MetricRec_t:[ w.SM_CXSCREEN, "w.SM_CXSCREEN", "Screen width" ], MetricRec_t:[ w.SM_CYSCREEN, "w.SM_CYSCREEN", "Screen height" ], MetricRec_t:[ w.SM_CXVSCROLL, "w.SM_CXVSCROLL", "Vert scroll arrow width" ], MetricRec_t:[ w.SM_CYVSCROLL, "w.SM_CYVSCROLL", "Vert scroll arrow ht" ], MetricRec_t:[ w.SM_CXHSCROLL, "w.SM_CXHSCROLL", "Horz scroll arrow width" ], MetricRec_t:[ w.SM_CYHSCROLL, "w.SM_CYHSCROLL", "Horz scroll arrow ht" ], MetricRec_t:[ w.SM_CYCAPTION, "w.SM_CYCAPTION", "Caption bar ht" ], MetricRec_t:[ w.SM_CXBORDER, "w.SM_CXBORDER", "Window border width" ], MetricRec_t:[ w.SM_CYBORDER, "w.SM_CYBORDER", "Window border height" ], MetricRec_t:[ w.SM_CXDLGFRAME, "w.SM_CXDLGFRAME", "Dialog frame width" ], MetricRec_t:[ w.SM_CYDLGFRAME, "w.SM_CYDLGFRAME", "Dialog frame height" ], MetricRec_t:[ w.SM_CXHTHUMB, "w.SM_CXHTHUMB", "Horz scroll thumb width" ], MetricRec_t:[ w.SM_CYVTHUMB, "w.SM_CYVTHUMB", "Vert scroll thumb width" ], MetricRec_t:[ w.SM_CXICON, "w.SM_CXICON", "Icon width" ], MetricRec_t:[ w.SM_CYICON, "w.SM_CYICON", "Icon height" ], MetricRec_t:[ w.SM_CXCURSOR, "w.SM_CXCURSOR", "Cursor width" ], MetricRec_t:[ w.SM_CYCURSOR, "w.SM_CYCURSOR", "Cursor height" ], MetricRec_t:[ w.SM_CYMENU, "w.SM_CYMENU", "Menu bar height" ], MetricRec_t:[ w.SM_CXFULLSCREEN, "w.SM_CXFULLSCREEN", "Largest client width" ], MetricRec_t:[ w.SM_CYFULLSCREEN, "w.SM_CYFULLSCREEN", "Largets client ht" ], MetricRec_t:[ w.SM_DEBUG, "w.SM_CDEBUG", "Debug version flag" ], MetricRec_t:[ w.SM_SWAPBUTTON, "w.SM_CSWAPBUTTON", "Mouse buttons swapped" ], MetricRec_t:[ w.SM_CXMIN, "w.SM_CXMIN", "Minimum window width" ], MetricRec_t:[ w.SM_CYMIN, "w.SM_CYMIN", "Minimum window height" ], MetricRec_t:[ w.SM_CXSIZE, "w.SM_CXSIZE", "Minimize/maximize icon width" ], MetricRec_t:[ w.SM_CYSIZE, "w.SM_CYSIZE", "Minimize/maximize icon height" ], MetricRec_t:[ w.SM_CXFRAME, "w.SM_CXFRAME", "Window frame width" ], MetricRec_t:[ w.SM_CYFRAME, "w.SM_CYFRAME", "Window frame height" ], MetricRec_t:[ w.SM_CXMINTRACK, "w.SM_CXMINTRACK", "Minimum tracking width" ], MetricRec_t:[ w.SM_CXMAXTRACK, "w.SM_CXMAXTRACK", "Maximum tracking width" ], MetricRec_t:[ w.SM_CYMINTRACK, "w.SM_CYMINTRACK", "Minimum tracking ht" ], MetricRec_t:[ w.SM_CYMAXTRACK, "w.SM_CYMAXTRACK", "Maximum tracking ht" ], MetricRec_t:[ w.SM_CXDOUBLECLK, "w.SM_CXDOUBLECLK", "Dbl-click X tolerance" ], MetricRec_t:[ w.SM_CYDOUBLECLK, "w.SM_CYDOUBLECLK", "Dbl-click Y tolerance" ], MetricRec_t:[ w.SM_CXICONSPACING, "w.SM_CXICONSPACING", "Horz icon spacing" ], MetricRec_t:[ w.SM_CYICONSPACING, "w.SM_CYICONSPACING", "Vert icon spacing" ], MetricRec_t:[ w.SM_CMOUSEBUTTONS, "w.SM_CMOUSEBUTTONS", " # of mouse btns" ] ]; const NumMetrics := @elements( MetricData ); /**************************************************************************/ /* W I N M A I N S U P P O R T C O D E */ /**************************************************************************/ // initWC - We don't have any initialization to do, so just return: procedure initWC; @noframe; begin initWC; dbg.put( hwnd, nl "Bitmaps3----------------", nl ); ret(); end initWC; // appCreateWindow- the default window creation code is fine, so just // call defaultCreateWindow. procedure appCreateWindow; @noframe; begin appCreateWindow; jmp defaultCreateWindow; end appCreateWindow; // appException- // // Gives the application the opportunity to clean up before // aborting when an unhandled exception comes along: procedure appException( theException:dword in eax ); begin appException; raise( eax ); end appException; // This is the custom message translation procedure. // We're not doing any custom translation, so just return EAX=0 // to tell the caller to go ahead and call the default translation // code. procedure LocalProcessMsg( var lpmsg:w.MSG ); begin LocalProcessMsg; xor( eax, eax ); end LocalProcessMsg; /**************************************************************************/ /* A P P L I C A T I O N S P E C I F I C C O D E */ /**************************************************************************/ // QuitApplication: // // This procedure handles the w.WM_DESTROY message. // It tells the application to terminate. This code sends // the appropriate message to the main program's message loop // that will cause the application to terminate. procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword ); begin QuitApplication; w.PostQuitMessage( 0 ); end QuitApplication; // Create- // // This procedure responds to the w.WM_CREATE message. // Windows sends this message once when it creates the // main window for the application. We will use this // procedure to do any one-time initialization that // must take place in a message handler. procedure Create( hwnd: dword; wParam:dword; lParam:dword ); var hdc: dword; // Handle to video display device context tm: w.TEXTMETRIC; begin Create; GetDC( hwnd, hdc ); // Initialization: // // Get the text metric information so we can compute // the average character heights and widths. GetTextMetrics( tm ); mov( tm.tmHeight, eax ); add( tm.tmExternalLeading, eax ); mov( eax, AverageCharHeight ); mov( tm.tmAveCharWidth, eax ); mov( eax, AverageCharWidth ); // If bit #0 of tm.tmPitchAndFamily is set, then // we've got a proportional font. In that case // set the average capital width value to 1.5 times // the average character width. If bit #0 is clear, // then we've got a fixed-pitch font and the average // capital letter width is equal to the average // character width. mov( eax, ebx ); shl( 1, tm.tmPitchAndFamily ); if( @c ) then shl( 1, ebx ); // 2*AverageCharWidth endif; add( ebx, eax ); // Computes 2 or 3 times eax. shr( 1, eax ); // Computes 1 or 1.5 times eax. mov( eax, AverageCapsWidth ); ReleaseDC; intmul( 40, AverageCharWidth, eax ); intmul( 25, AverageCapsWidth, ecx ); add( ecx, eax ); mov( eax, MaxWidth ); end Create; // Paint: // // This procedure handles the w.WM_PAINT message. // For this System Metrics program, the Paint procedure // displays three columns of text in the main window. // This procedure computes and displays the appropriate text. procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); var x :int32; // x-coordinate of start of output str. y :int32; // y-coordinate of start of output str. CurVar :string; // Current system metrics variable name. CVlen :uns32; // Length of CurVar string. CurDesc :string; // Current system metrics description. CDlen :string; // Length of the above. CDx :int32; // X position for CurDesc string. value :string; valData :char[32]; CVx :int32; // X position for value string. vallen :uns32; // Length of value string. firstMet :int32; // Starting metric to begin drawing lastMet :int32; // Ending metric index to draw. hdc :dword; // Handle to video display device context ps :w.PAINTSTRUCT; // Used while painting text. begin Paint; // Message handlers must preserve EBX, ESI, and EDI. // (They've also got to preserve EBP, but HLA's procedure // entry code already does that.) push( ebx ); push( esi ); push( edi ); // Initialize the value->valData string object: mov( str.init( (type char valData), 32 ), value ); // When Windows requests that we draw the window, // fill in the string in the center of the screen. // Note that all GDI calls (e.g., w.DrawText) must // appear within a BeginPaint..EndPaint pair. BeginPaint( hwnd, ps, hdc ); // Figure out which metric we should start drawing // (firstMet = // max( 0, VscrollPos + ps.rcPaint.top/AverageCharHeight - 1)): mov( ps.rcPaint.top, eax ); cdq(); idiv( AverageCharHeight ); add( VscrollPos, eax ); dec( eax ); if( (type int32 eax) < 0 ) then xor( eax, eax ); endif; mov( eax, firstMet ); // Figure out the last metric we should be drawing // ( lastMet = // min( NumMetrics, // VscrollPos + ps.rcPaint.bottom/AverageCharHeight )): mov( ps.rcPaint.bottom, eax ); cdq(); idiv( AverageCharHeight ); add( VscrollPos, eax ); if( (type int32 eax) > NumMetrics ) then mov( NumMetrics, eax ); endif; mov( eax, lastMet ); // The following loop processes each entry in the // MetricData array. The loop control variable (EDI) // also determines the Y-coordinate where this code // will display each line of text in the window. // Note that this loop counts on the fact that Windows // API calls preserve the EDI register. for( mov( firstMet, edi ); edi < lastMet; inc( edi )) do // Before making any Windows API calls (which have // a nasty habit of wiping out registers), compute // all the values we will need for these calls // and save those values in local variables. // // A typical "high level language solution" would // be to compute these values as needed, immediately // before each Windows API calls. By moving this // code here, we can take advantage of values previously // computed in registers without having to worry about // Windows wiping out the values in those registers. // Compute index into MetricData: intmul( @size( MetricRec_t ), edi, esi ); // Grab the string from the current MetricData element: mov( MetricData.MetStr[ esi ], eax ); mov( eax, CurVar ); mov( (type str.strRec [eax]).length, eax ); mov( eax, CVlen ); mov( MetricData.MetDesc[ esi ], eax ); mov( eax, CurDesc ); mov( (type str.strRec [eax]).length, eax ); mov( eax, CDlen ); // Column one begins at X-position AverageCharWidth (ACW). // Col 2 begins at ACW + 25*AverageCapsWidth. // Col 3 begins at ACW + 25*AverageCapsWidth + 40*ACW. // Compute the Col 2 and Col 3 values here. mov( 1, eax ); sub( HscrollPos, eax ); intmul( AverageCharWidth, eax ); mov( eax, x ); intmul( 25, AverageCapsWidth, eax ); add( x, eax ); mov( eax, CDx ); intmul( 40, AverageCharWidth, ecx ); add( ecx, eax ); mov( eax, CVx ); // The Y-coordinate for the line of text we're writing // is computed as AverageCharHeight * (1-VscrollPos+edi). // Compute that value here: mov( 1, eax ); sub( VscrollPos, eax ); add( edi, eax ); intmul( AverageCharHeight, eax ); mov( eax, y ); // Now generate the string we're going to print // as the value for the current metric variable: w.GetSystemMetrics( MetricData.MetConst[ esi ] ); conv.i32ToStr( eax, 0, ' ', value ); mov( str.length( value ), vallen ); // First two columns have left-aligned text: SetTextAlign( w.TA_LEFT | w.TA_TOP ); // Output the name of the metric variable: TextOut( x, y, CurVar, CVlen ); // Output the description of the metric variable: TextOut( CDx, y, CurDesc, CDlen ); // Output the metric's value in the third column. This is // a numeric value, so we'll right align this data. SetTextAlign( w.TA_RIGHT | w.TA_TOP ); TextOut( CVx, y, value, vallen ); // Although not strictly necessary for this program, // it's a good idea to always restore the alignment // back to the default (top/left) after you done using // some other alignment. SetTextAlign( w.TA_LEFT | w.TA_TOP ); endfor; EndPaint; pop( edi ); pop( esi ); pop( ebx ); end Paint; // Size- // // This procedure handles the w.WM_SIZE message // // L.O. word of lParam contains the new X Size // H.O. word of lParam contains the new Y Size procedure Size( hwnd: dword; wParam:dword; lParam:dword ); begin Size; // Convert new X size to 32 bits and save: movzx( (type word lParam), eax ); mov( eax, ClientSizeX ); // Convert new Y size to 32 bits and save: movzx( (type word lParam[2]), eax ); mov( eax, ClientSizeY ); // VscrollMax = max( 0, NumMetrics+2 - ClientSizeY/AverageCharHeight ) cdq(); idiv( AverageCharHeight ); mov( NumMetrics+2, ecx ); sub( eax, ecx ); if( @s ) then xor( ecx, ecx ); endif; mov( ecx, VscrollMax ); // VscrollPos = min( VscrollPos, VscrollMax ) if( ecx > VscrollPos ) then mov( VscrollPos, ecx ); endif; mov( ecx, VscrollPos ); w.SetScrollRange( hwnd, w.SB_VERT, 0, VscrollMax, false ); w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true ); // HscrollMax = // max( 0, 2 + (MaxWidth - ClientSizeX) / AverageCharWidth); mov( MaxWidth, eax ); sub( ClientSizeX, eax ); cdq(); idiv( AverageCharWidth ); add( 2, eax ); if( @s ) then xor( eax, eax ); endif; mov( eax, HscrollMax ); // HscrollPos = min( HscrollMax, HscrollPos ) if( eax > HscrollPos ) then mov( HscrollPos, eax ); endif; mov( eax, HscrollPos ); w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false ); w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true ); xor( eax, eax ); // return success. end Size; // HScroll- // // Handles w.WM_HSCROLL messages. // On entry, L.O. word of wParam contains the scroll bar activity. procedure HScroll( hwnd: dword; wParam:dword; lParam:dword ); begin HScroll; // Convert 16-bit command to 32 bits so we can use switch macro: movzx( (type word wParam), eax ); switch( eax ) case( w.SB_LINELEFT ) mov( -1, eax ); case( w.SB_LINERIGHT ) mov( 1, eax ); case( w.SB_PAGELEFT ) mov( -8, eax ); case( w.SB_PAGERIGHT ) mov( 8, eax ); case( w.SB_THUMBPOSITION ) movzx( (type word wParam[2]), eax ); sub( HscrollPos, eax ); default xor( eax, eax ); endswitch; // eax = // max( -HscrollPos, min( eax, HscrollMax - HscrollPos )) mov( HscrollPos, edx ); neg( edx ); mov( HscrollMax, ecx ); add( edx, ecx ); if( eax > (type int32 ecx) ) then mov( ecx, eax ); endif; if( eax < (type int32 edx )) then mov( edx, eax ); endif; if( eax <> 0 ) then add( eax, HscrollPos ); imul( AverageCharWidth, eax ); neg( eax ); w.ScrollWindow( hwnd, eax, 0, NULL, NULL ); w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true ); endif; xor( eax, eax ); // return success end HScroll; // VScroll- // // Handles the w.WM_VSCROLL messages from Windows. // The L.O. word of wParam contains the action/command to be taken. // The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK // message. procedure VScroll( hwnd: dword; wParam:dword; lParam:dword ); begin VScroll; movzx( (type word wParam), eax ); switch( eax ) case( w.SB_TOP ) mov( VscrollPos, eax ); neg( eax ); case( w.SB_BOTTOM ) mov( VscrollMax, eax ); sub( VscrollPos, eax ); case( w.SB_LINEUP ) mov( -1, eax ); case( w.SB_LINEDOWN ) mov( 1, eax ); case( w.SB_PAGEUP ) mov( ClientSizeY, eax ); cdq(); idiv( AverageCharHeight ); neg( eax ); if( (type int32 eax) > -1 ) then mov( -1, eax ); endif; case( w.SB_PAGEDOWN ) mov( ClientSizeY, eax ); cdq(); idiv( AverageCharHeight ); if( (type int32 eax) < 1 ) then mov( 1, eax ); endif; case( w.SB_THUMBTRACK ) movzx( (type word wParam[2]), eax ); sub( VscrollPos, eax ); default xor( eax, eax ); endswitch; // eax = max( -VscrollPos, min( eax, VscrollMax - VscrollPos )) mov( VscrollPos, edx ); neg( edx ); mov( VscrollMax, ecx ); add( edx, ecx ); if( eax > (type int32 ecx) ) then mov( ecx, eax ); endif; if( eax < (type int32 edx)) then mov( edx, eax ); endif; if( eax <> 0 ) then add( eax, VscrollPos ); intmul( AverageCharHeight, eax ); neg( eax ); w.ScrollWindow( hwnd, 0, eax, NULL, NULL ); w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true ); w.UpdateWindow( hwnd ); endif; xor( eax, eax ); // return success. end VScroll; // KeyDown- // // Handles the w.WM_KEYDOWN messages from Windows. // The L.O. word of wParam contains the action/command to be taken. // The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK // message. procedure KeyDown( hwnd: dword; wParam:dword; lParam:dword ); begin KeyDown; mov( wParam, eax ); switch( eax ) // If they press the "HOME" key, scroll to the top of the window: case( w.VK_HOME ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_TOP, 0 ); // If they press the "END" key, scroll to the bottom of the window: case( w.VK_END ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_BOTTOM, 0 ); // If they press the "PgUp" key, scroll up one page: case( w.VK_PRIOR ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_PAGEUP, 0 ); // If they press the "PgDn" key, scroll down one page: case( w.VK_NEXT ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_PAGEDOWN, 0 ); // If they press the "Up" key, scroll up one line: case( w.VK_UP ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_LINEUP, 0 ); // If they press the "Down" key, scroll down one line: case( w.VK_DOWN ) w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_LINEDOWN, 0 ); // If they press the "Left" key, scroll text to the right: case( w.VK_LEFT ) w.SendMessage( hwnd, w.WM_HSCROLL, w.SB_PAGEUP, 0 ); // If they press the "Right" key, scroll text to the left: case( w.VK_RIGHT ) w.SendMessage( hwnd, w.WM_HSCROLL, w.SB_PAGEDOWN, 0 ); endswitch; end KeyDown; end kbSysmets;The kbSysmets program demonstrates how to process raw virtual keycodes in your program. Most of the time, however, you're not interested in the virtual keycodes; what you really want are ASCII codes for whatever keys the user presses. The only time you'll really want to deal with virtual key codes is when reading keystrokes from the user that have no corresponding ASCII codes.
In theory, you could convert virtual keycodes into ASCII codes yourself. The catch is that you must maintain certain state information, such as which modifier keys (shift, control, alt, capslock, numlock, etc.) are currently active and use this information to translate a virtual key code into the corresponding ASCII code. For example, if you get the virtual keycode $41 (`A'), you would have to translate this code to $61 (`a'), $41 (`A'), or $01 (ctrl-A), depending on the state of the shift and control keys (note that Windows treats the alt modifier key specially, you wouldn't normally have to deal with this). There are a couple of problems with this translation - first of all it's a lot of work. Second, and more important, there is no single translation you can do. Different Windows systems in different countries do the translation differently. Trying to handle the translation of virtual key codes for the dozens of different keyboards that exist today would be overwhelming. Fortunately, you don't have to do this translation yourself (nor should you attempt it): Windows will automatically do the translation for you. Consider the main message processing loop in the Winmain.hla module:
forever w.GetMessage( msg, NULL, 0, 0 ); breakif( eax == 0 ); if( LocalProcessMsg( msg ) == 0) then w.TranslateMessage( msg ); endif; w.DispatchMessage( msg ); endfor;The w.TranslateMessage API call intercepts w.WM_KEYDOWN and w.WM_KEYUP messages and generates a new message to send to your application if the key message corresponds to some ASCII character. Note that upon return, the message processing loop still sends the w.WM_KEYDOWN or w.WM_KEYUP message to your application; the new message that w.TranslateMessage creates will follow shortly. Therefore, when the w.TranslateMessage function does a translation, your application will actually receive two messages.
Before looking at these new message types, let's first discuss the LocalProcessMsg function call that appears in the loop above. This is not a Windows' API function call (note the lack of a "w." prefix). Instead, this is a call to an application-supplied function that determines whether any local message processing should take place. LocalProcessMsg returns zero/not zero in EAX to determine whether the main message processing loop should call the w.TranslateMessage API function (EAX is a "skip translation" flag, zero indicates that translation should take place and non-zero means to skip the translation operation). The vast majority of the time, you'll want to call the Windows' w.TranslateMessage API function and not mess around with the message. Therefore, the typical default LocalProcessMessage function looks like this:
procedure LocalProcessMsg( var lpmsg:w.MSG ); begin LocalProcessMsg; xor( eax, eax ); end LocalProcessMsg;That is, the function simply returns with EAX zero so that the main message processing loop will call the w.TranslateMessage API function. The single parameter is a pointer to a message structure, this object takes the following form:
type MSG: record hwnd: dword; message: dword; wParam: dword; lParam: dword; time: dword; pt: POINT; endrecord;The three fields of particular interest are the message, wParam, and lParam fields. These fields contain the values that Windows will pass as parameters to your Window procedure (i.e., the parameters that wind up being passed to your message handling procedures in your application). You can peek at the message field's valueto determine if you want to do any special processing on the message. If so, you can fetch other values from the wParam and lParam fields. Any changes you make to the fields of this structure will be passed along to w.TranslateMessage (if you set EAX to zero before returning) and to the w.DispatchMessage API function (that winds up calling your window procedure). Therefore, you can do some sophisticated translation of your own, should you choose to do so, within the LocalProcessMsg function. The vast majority of the time, however, you'll not bother with doing any translations inside this code.
If the message processing loop sends w.TranslateMessage a w.WM_KEYDOWN or w.WM_KEYUP message, then the w.TranslateMessage function may inject a new message into the message queue containing an ASCII translation of the keypress. This new message is a w.WM_CHAR message. The w.WM_CHAR message contains the same information in the lParam field as the w.WM_KEYDOWN and w.WM_KEYUP messages, the wParam field contains an ASCII key code rather than a Windows virtual key code.
Whenever you press (and release) a key that has a corresponding ASCII key code, Windows will actually send three messages to your application: a w.WM_KEYDOWN message, a w.WM_CHAR message, and then a w.WM_KEYUP message, in that order. Generally, you will ignore the keydown and key up messages and process only the w.WM_CHAR messages within your application (in fact, most applications also ignore all w.WM_KEYUP messages, as well).
Windows also sends a couple of other keyboard-related messages to your applications. These messages are w.WM_DEADCHAR, w.WM_SYSCHAR, and w.WM_SYSDEADCHAR. The "...SYS..." messages correspond to system keystrokes. You application can safely ignore these messages. Windows sends a w.WM_DEADCHAR message whenever you press an "accent key prefix" key on the keyboard that will produce an accented character (e.g., on non-U.S. keyboards). You can usually ignore all dead character messages as the following w.WM_CHAR message will incorporate the dead key information. The only reason for looking at these messages is to create your own, special, accented characters that Windows doesn't normally support. As such a need is rare, we won't consider the dead codes here any farther.
The following application, keytest.hla, is another application inspired by a program in Petzold's book. This program processes the keyboard messages and writes their data payloads to the application's window. This short application lets you view all the keyboard messages that come along whenever you press a key on the PC's keyboard.
// keytest.hla- // // This program reads keystroke messages from the system and displays them. unit keytest; // Set the following to true to display interesting information // during program operation. You must be running // the "DebugWindow" application for this output to appear. ?debug := false; #includeonce( "excepts.hhf" ) #includeonce( "conv.hhf" ) #includeonce( "hll.hhf" ) #includeonce( "memory.hhf" ) #includeonce( "w.hhf" ) #includeonce( "wpa.hhf" ) #includeonce( "winmain.hhf" ) ?@NoDisplay := true; ?@NoStackAlign := true; type // Data type for the keyboard message: keymsgPtr_t :pointer to keymsg_t; keymsg_t: record // Maintains list of records: Next :keymsgPtr_t; // MsgStr points at a string specifying the // message type: "WM_CHAR", "WM_KEYDOWN", etc: MsgStr :string; // Virtual key code in WM_KEYDOWN/WM_KEYUP // messages: VirtKey :dword; // Repeat count in message (# of autorepeated // keys passed on this message): RepeatCnt :uns16;