Notebook Key Processing

From EDM2
Jump to: navigation, search

Written by Roman Stangl

The problem

The OS/2 PM notebook control has a limitation that causes inconsistent usability compared to other PM controls, namely it does not pass on all unprocessed messages to its owner.

As a result the following consequences, which we want to avoid, exist:

  • Breaking this focus chain means, for example, that if the input focus is on a control of a notebook page, and one presses e.g. Alt+F and there exists a notebook tab having the letter F as the shortcut key, the key doesn't get passed on to the notebook, so that it can adjust itself to display the corresponding notebook page as the top page.
  • Pressing the ESC key to cancel would dismiss the dialog that implements the notebook page, but not the window containing the notebook control.
  • Pressing the Enter key does not dismiss the dialog window by selecting the default pushbutton (e.g. Save, Ok,...).
  • Pressing accelerator keys does not trigger pushbuttons on your dialog's window.

The window layout we assume

A typical window containing a notebook is a dialog window (most likely created with the dialog editor) that contains a Notebook Control and Pushbuttons in its client area. The notebook below should demonstrate this.

By the way, the screen capture was taken from my Program Commander/2 (PC/2) program, which is available from [my homepage], and it's Freeware!

A typical dialog containing a notebook  control

This will probably not be new to you, however I would like to summarize this from the point of the Z-Order and owner chain, as this is the basis to understand how to enhance the processing. The controls in the above notebook are created in the following order:

  1. The Dialog Window is the PM window that implements the whole dialog, which is created by the Dialog editor and processed by the OS/2 PM APIs WinDlgBox() or WinLoadDlg(), WinProcessDlg(). If you for example press ESC, you want this Dialog Window to be dismissed.
  2. The client area of the Dialog Window consists of the Notebook control and the Pushbuttons like Save, Cancel and Help. The important thing to mention here is, that the Notebook Control and the Pushbuttons are child windows of the Dialog Window.
  3. The Notebook Control is a complex OS/2 PM control that consists of the Notebook Tabs, the Notebook Page(s) (and the Status line and Page buttons not shown here).
  4. The Notebook Tabs are drawn by the Notebook Control. However you determine the text drawn onto the individual tabs and there is nothing that prevents you from including shortcut keys, for example to specify Options for your options notebook page.
  5. Finally, the Notebook Control contains an area where the Notebook page(s) are displayed at. Notebook Pages are also dialog windows usually created by the dialog editor. When adding to a notebook, the notebook implicitely performs the WinLoadDlg() to load the dialog from a resource.

The Notebook Control keeps track of all the pages you have inserted and displays the Notebook Page that corresponds to the currently selected Notebook Tab and hides the other pages.

The important thing to mention here is, that the Notebook Pages are child windows of the Notebook Control.

Possible solution

Instance data structure

In order to forward key events from the notebook control to its owner, we have to modify the way the Notebook Control processes such events, in case the notebook is not interested in that event.

When working with data that is window dependent (instance data), the proper way is to use the window words to save a pointer to your data. The QWL_USER window word is reserved for the user and would be a good starting point.

However, while implementing the advanced processing, it can be useful to store the data in the heap, as it can be accessed by the debugger even when not stepping through a window procedure. In the following code excerpt, the processing supports 3 different dialogs.

 typedef struct _NOTEBOOKSUBCLASS    NOTEBOOKSUBCLASS;

 /* Structure to save which notebook was subclassed
    from which previous window procedure */
 struct  _NOTEBOOKSUBCLASS
 {
 HWND    hwndNotebook;    /* Notebook window handle */
 PFNWP   pfnwpNotebook;   /* Notebook control's window procedure
                             before subclassing */
 };

 /* Desktop dialog notebook subclassed */
 #define DD_SUBCLASSEDNOTEBOOK       0

 /* Program Installation dialog notebook subclassed */
 #define PI_SUBCLASSEDNOTEBOOK       1

 /* Control Configuration dialog notebook subclassed */
 #define CC_SUBCLASSEDNOTEBOOK       2

 /* As only one instance of the Desktop and Program Installation
    dialogs is allowed, its save to avoid more complicated
    per dialog instantiation, but use module class storage */
 #define NOTEBOOKSUBCLASSMAX         3

 NOTEBOOKSUBCLASS    DialogNotebookSubclass[NOTEBOOKSUBCLASSMAX];

In this example our application consists of 3 different Dialog Windows for which we want to enhance the message processing.

Modifying the notebook processing

In order to forward key events from the notebook control to its owner, we have to modify the way the Notebook Control processes such events, in case the notebook is not interested in that event.

In order to change the Notebook Control's default message processing, we have to subclass the control by using the PM API WinSubclassWindow():

   /* Subclass notebook's window procedure to add handling of
      notebook pages, notebook and outer dialog accelerator keys */
   DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].hwndNotebook=hwndNB;
   DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].pfnwpNotebook=
      WinSubclassWindow(hwndNB, SubclassedNotebookProcedure);

Subclassing means, that we add a new window procedure to the existing window procedure of the Notebook Control to be able to add processing logic before the notebook's window procedure. In your own controls, you would add the processing somewhere in the big switch-Statement of your window procedure, however as the notebook (and all other controls shipped with OS/2) is implemented inside PM, you can only get access to the control's window procedure by using WinSubclassWindow().

Another thing to mention here is, that in our example all procedures are coded only once, that is they run in the context of the window that is currently processing the user input. As a consequence special caution needs to be taken not to share variables between different window contexts.

The subclassed window procedure of the Notebook Control is especially interested in WM_CHAR messages, as the WM_CHAR message reflects the keyboard input we want to change:

Subclassed window procedure. NBKEY1.C

The first thing we do in the subclassed window procedure is look for the control structure element that corresponds to the notebook control in whose context we are currently running in. As I said, the most elegant way would be to use the notebook's window words, but the easier to debug list approach is taken here.

In detail, the following happens for a WM_CHAR message is routed to the notebook:

  1. As we have subclassed the notebook, SubClassedNotebookProcedure() will be called when the Notebook control has the input focus (e.g. when you click on one of the tabs), because it is now part of the notebook control's window procedure.
  2. A lookup is done to find out the instance data.
  3. The original notebook window procedure is called to see if the notebook does accept that key, which we know by the result returned. We are interested in the result, as we have to forward keys not accepted by the Notebook Control to the owning Dialog Window.
  4. If the key is the shortcut of a Notebook Tab, as for example the O for a Options tab, then the notebook will accept that key and switch to the Notebook Page corresponding to that tab. If the key is not a shortcut accepted by the notebook, we will forward it to the Dialog Window (which is the owner of the Notebook Control), as for example the C key may be the accelerator key of a Cancel Pushbutton.

Modifying the notebook pages' processing

In the window procedure of the Notebook Pages we also have to catch the key events, by looking for WM_CHAR messages. As the dialog that implements the notebook page is created by us, we also supply the window procedure (or the dialog procedure to be exact), so we just can easily add the processing as shown here:

 case WM_CHAR:
 /*
  * Process the navigation keys on this page and forward all
  * unprocessed keys to the dialog where the notebook is part of.
  *
 \*
     ProcessPageKey(hwndDlg, WinQueryWindow(hwndNotebook,
                                            QW_OWNER), mp1, mp2);
     break;

In order to allow window procedures of different notebook pages to share the same implementation, all WM_CHAR messages are processed by ProcessPageKey().

Note: If you have multiple notebook pages you can write window procedure and share it for all notebook pages, just ensure that the controls of the individual notebook pages contain different identifiers.

ProcessPageKey (NBKEY2.C

This function is somewhat more complex. In order to make debugging easier, printf() calls are added to generate debug information. You can either redirect stdout to a file (e.g. by invoking myapp.exe with "myapp > debuginfo"), or you get the very useful IBM EWS-written PMPRINTF package, which replaces the C-library printf() function with one that writes into an OS/2 queue, and using the included PM viewer you can display that queue in real-time.

In detail, the following happens for a WM_CHAR message routed to a notebook page's dialog procedure:

  1. If the key event is a navigation key, that is the tab and cursor keys, we accept the processing of the Notebook Page's window procedure.
  2. If the key event is not a navigation key, we ask the Dialog Window if the key makes sense there, for example the ALT+C key for a Cancel Pushbutton).

Modifying the dialog window's processing

The Dialog Window may process messages either generated while the dialog had the input focus, or messages forwarded up from the owner chain, that is message forwarded by the Notebook Control and by the individual Notebook Pages. In any case, we are interested in the processing of WM_CHAR messages as shown below.

again, as the dialog that implements the dialog window is created by us, we also supply the window procedure (or the dialog procedure to be exact), so we just can easily add the WM_CHAR processing.

 case WM_CHAR:

 /*
  * All window procedure implementing a noteboook page and the notebook
  * control itself, which is part of this window's client, forward
  * keystroke messages they don't have handled themselves here.
 \*
 {
     static ULONG    ulRecursion=FALSE;

     return(DispatchKeyToNotebook(&ulRecursion, hwndDlg, hwndNB,
         DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].pfnwpNotebook, mp1, mp2));
 }

To share code, again we call a function named DispatchKeyToNotebook (NBKEY3.C). As a notebook page's window procedure calls the dialog's window procedure by using WinSendMsg() (as shown in ProcessPageKey() - NBKEY2.C) and DispatchKeyToNotebook() again calls the notebook page's window procedure, we have to check for recursion.

The reason why we may get into recursion is, that while processing a key in the Dialog Window's dialog procedure, we no longer know where it was generated, that is in the dialog window itself, or sent from one of the owned controls. However, we want our logic to enhance the key processing to work in all cases, so we have to take the recursion into account.

DispatchKeyToNotebook (NBKEY3.C)

In detail, the following happens for a WM_CHAR message routed to the dialog procedure:

  1. We check if this key is the ESC or Enter key, as they are used to dismiss the dialog, causing the Pushbuttons DID_CANCEL for ESC and DID_OK for Enter to be selected.

One thing to note is that Enter (VK_ENTER) is not the same as Newline (VK_NEWLINE). If Enter (that is the "Enter" key on the numeric keybad) is pressed, we want the dialog to be dismissed, but not in the case of Newline, because notebook pages may contain controls that allow you to input the "Enter" key (e.g. multiline entryfields), and your user for sure want to be able to input that key too. On a Laptop, some key combinations may be required to select the Enter key instead of the Newline key (as Laptops usually have no numeric keybad). You may try SHIFT+Enter, which works on IBM ThinkPads, or take a look into your User's Guide.

  1. To prevent recursion, one static variable per Dialog Window is used to track the recursion depth.
  2. After having protected us against recursion (and still running in the Dialog window's dialog procedure), we check if the key event is a key press while the ALT key is still pressed (as accelerator keys usually are used in conjunction with the ALT key).
  3. If the key is pressed while the ALT key is still pressed, we check if the dialog procedure of the Dialog Window can accept that key, by calling the dialog's window procedure and checking the result returned. This would allow for example the ALT+C key for Cancel to be accepted.
  4. If the dialog window procedure does not accept that key, we query the window handle of the top Notebook Page, that is the one displayed in the Notebook Control. If the notebook page could handle it we're done.
  5. If that Notebook Page can't accept that key, see if the Notebook page can accept that key as if no ALT key has been pressed. We remove the ALT modifier, as if a notebook control has the input focus one can navigate between the notebook pages by pressing the accelerator key, for example O for Options without being required to hold the ALT key meanwhile.
  6. If the Notebook Control can accept that key, find out the Notebook page that has been put onto top and activate the control that had the input focus immediately before that notebook page was paged away.

Credits

The code shown here was greatly influenced by some discussions in the OS/2 development fora on the IBMPC conference disk.