Designing for Mouse and Keyboard
Copyright © 1999-2006 Clayton Jones
by Clayton Jones

One of the challenges we face in creating interfaces for database applications is providing for efficient operation using the keyboard as well as for the mouse.  The many types of events, handled differently by various components and often passed to event handlers through the parent/child chain, can be challenging to master.  I would like to present here a method which has proven to be very effective for data entry forms.  It is based upon three design principles which can make this task faster, easier and free from conflicts.

The First Principle

     The first principle: All action code is located in a single event handler routine.  This is similar to the concept we are familiar with in Clipper of having only one exit point in a function.   The centrally located code is easier to maintain and less prone to conflicts.

     In a graphical event-driven environment, the user may click the mouse or press a key while the application focus is on any one of the visual components on the window.  Users can and will do strange things, especially with a mouse, and we are faced with the challenge of trapping for both mouse and keyboard events which could come from many different sources on the form.

    A further complication arises when a component traps and handles a mouse or keyboard event internally and then generates a different type of external event.   A example of this is the xbpBrowse object which traps a Double-Click or Enter key event and generates an ItemSelected event.

The Second Principle

     To help make order out of chaos, we have the second principle: Translate all events into a single type before sending them to the central event handler.   With the PostAppEvent() function we have the ability to send any type of event we wish to any object.  Because of the large number of possible events, I have found the simplest approach is to have the event handler trap for keyboard events only.  The most efficient way to do this is to have a keyhandler function for the dialog window and redirect all mouse and keyboard events from components into it, with all non-keyboard events being "translated" into keyboard events.  Our task in writing action codeblocks on the component level becomes one of simply sending the appropriate keyboard event into the handler where the action code is executed.

     This process can be made more difficult, however, by the fact that some components pass certain keystrokes up the parent/child chain while retaining others.  For example, when an xbpPushbutton has focus, it handles keystrokes in three different ways.  Table 1 describes these three behaviors and the related keystrokes:

xbpPushbutton Behavior Keys
1) Triggers an evaluation of the button's action codeblock.  Does not pass the keyboard event up the parent/child chain (see Tip below). Space Bar
2) Passes the keyboard event up the parent/child chain.  If we have a keyhandler function for the parent dialog window, the event will find its way there and can be trapped. Esc, Enter, Tab, Shift-Tab, Backspace, Insert, Delete, Home, End, PgUp, PgDn, Function keys, Arrow keys.
3) Does not pass a keyboard event up the parent/child chain, and therefore does not get picked up by the window's keyhandler.   Letter and Number keys, Shift, Alt, Ctrl, Caps Lock, Num Lock, Scroll Lock, Pause, Print Screen.

Table 1

The Third Principle

          Knowledge of these behaviors leads us to the third principle: Always have a single entry point into the keyhandler.  It is tempting to ask, "Why not call the keyhandler directly from a component codeblock?  Why go the long way around by sending it to the window first?" 

     I did this very thing for awhile, thinking I was being more efficient, and it can work well much of the time.  I found, however, that under certain circumstances (especially in complex forms with many different types of components), certain keystrokes can get sent into the keyhandler twice - once by the component and once by the window (because some components will pass handled keystrokes up the parent/child chain) - with unpredictable results.  Following this third principle will generally keep the routine free from conflicts.  If there ever is a problem, you don't spend a lot of time searching down various obscure pathways looking for it.   It also adds a degree of consistency to the code to do it the same way in all routines.

Tip: When trapping for the Enter key with a button :keyboard codeblock, if a pushbutton has focus and the Enter key is pressed, if the button is not within a logical group of Xbase Parts, it will pass the keyboard event up the parent chain before evaluating its action codeblock.   If there is a keyhandler for the window, it will receive the Enter key code before the button codeblock action is performed.  However, if the button is part of a logical group, it will not pass the event up the chain (please refer to the Xbase++ documentation of the :group attribute for the xbpWindow).

     Therefore, as a general rule of thumb,  it is best not to call the event handler function directly from a component codeblock.  All component originated events should be posted to the dialog window, where they will be trapped and sent to the keyhandler.

     A good example of the three principles at work is found in the Top-Down Library function tdTabView().  This is a popup modal window with a browse object and two pushbuttons labeled "Select" and "Cancel".  It has a companion keyhandler function called TVkeys().

     This simple routine, usually called from a form view of a DBF record, allows the user to navigate a table view of the DBF and select a different record.   Clicking "Select" closes the window and the record pointer stays on the selected record.  Clicking "Cancel" moves the pointer back to the original record.  The Enter and Esc keys perform the same functions, so the routine may be used with or without the mouse.

The Code

     Looking first at the action code in the keyhandler function, TVkeys(), we see that only two keystrokes are trapped, Enter and Esc:

   IF nKey == xbeK_ESC
      PostAppEvent(xbeP_Close,,,oModal)
   ELSEIF nKey == xbeK_ENTER
      lSelect := .T.
      PostAppEvent(xbeP_Close,,,oModal)
   ENDIF

Both send Close events to the window (oModal), but Enter also sets the lSelect variable to True.  This flag signals the window's closeout routine to leave the record pointer where it is.  Otherwise, it is moved back to the original record

     At the top of tdTabView where the window is created, we find that the codeblock for the window's :keyboard slot has a direct call to the keyhandler.  Any keyboard event posted to the window will end up in this codeblock and be sent to TVkeys for handling:

   oModal:keyBoard := ;
     {|nKey,u1,self| TvKeys(nKey,oModal,@lSelect)}

     Further down, the browse object is created.  When the xbpBrowse has focus and internally traps a mouse Double-Click or an Enter keypress, it generates an ItemSelected event.  So we create a codeblock for the :itemSelected callback slot, and, using the PostAppEvent() function, post an Enter keyboard event to the window, where it is trapped and sent to TVkeys.  This is an example of how a mouse event can be translated into a keyboard event:

   oBrow:itemSelected := {|u1,u2,self| ;
      PostAppEvent(xbeP_Keyboard,xbeK_ENTER,,oModal)}

     Just below the browse code is the pushbutton code where we again use PostAppEvent, this time inserted into the buttons' action codeblocks, to post the appropriate keyboard events to the window:

   ******* Select
   oSelect := tdPshBtn(nRow,nCol,8,oDa,'Select', ;
         {||PostAppEvent(xbeP_Keyboard,xbeK_ENTER,,oModal)})

   ******* Cancel
   oCancel := tdPshBtn(nRow,nCol,8,oDa,'Cancel', ;
         {||PostAppEvent(xbeP_Keyboard,xbeK_ESC,,oModal)})

The Results

     At this point we have assigned code blocks to all of the objects: the window, the browse, and the two buttons.  Table 2 lists nine possible user actions for making a decision, and how they are handled:

User Action

What Happens

Browse has focus; Double-Click on record Browse > ItemSelected > Post Enter to Dlg > to TVkeys
Browse has focus; press Enter key Browse > ItemSelected > Post Enter to Dlg > to TVkeys
Browse has focus; press Esc key Browse passes Esc up chain; eventually trapped by Dlg, > to TVkeys
Click Select button Action block posts Enter to Dlg, > to TVkeys
Click Cancel button Action block posts Esc to Dlg, > to TVkeys
TAB to Select button; press Enter key Action block posts Enter to Dlg, > to TVkeys
TAB to Select button; press Esc key Button passes Esc up chain; eventually trapped by Dlg, > to TVkeys
TAB to Cancel button; press Enter key Action block posts Esc to Dlg, > to TVkeys
TAB to Cancel button; press Esc key Button passes Esc up chain; eventually trapped by Dlg, > to TVkeys

Table 2

   From these nine actions only two different keystrokes are sent to TVkeys: Enter and Esc.  This saves us from having to write complex logic to trap for many different possibilities.  Any other keystrokes which might be passed up the chain are also sent to TVkeys, but are ignored. 

Conclusion

     This example is an extremely simple one and serves well to illustrate the principles.  It may even seem like overkill to apply such logic to something so simple.  However, with a busy form containing group boxes, tab pages, and numerous buttons and data-aware objects, the complexity can easily get out of control.  Then the value of this method really proves itself.

     I find it helpful when designing a form to make a list of the components and what actions they will perform, along with all possible user actions.  This helps to get a clear picture of what keystrokes need to be trapped, which components need codeblocks, and what should be in them.  Doing this in advance more than makes up for the time it takes.  It virtually eliminates the possibility of conflicts that can keep a developer busy long after the form is written. 

Copyright © 1999-2006 Clayton Jones
All rights reserved.


Return to: Articles Page |Top-Down Page | Software And Services | Home