Jump to content

Notebook Key Processing: Difference between revisions

From EDM2
mNo edit summary
mNo edit summary
Line 2: Line 2:


==The problem==
==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
<p>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.
unprocessed messages to its owner.


</p><p>As a result the following consequences, which we want to avoid, exist:
As a result the following consequences, which we want to avoid, exist:
 
</p><ul>
 
<li>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.


</li><li>Pressing the ESC key to cancel would dismiss the dialog that
* 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.
    implements the notebook page, but not the window containing the notebook
    control.


</li><li>Pressing the Enter key does not dismiss the dialog window by selecting
* Pressing the ESC key to cancel would dismiss the dialog that implements the notebook page, but not the window containing the notebook control.
    the default pushbutton (e.g. Save, Ok,...).


</li><li>Pressing accelerator keys does not trigger pushbuttons on your
* Pressing the Enter key does not dismiss the dialog window by selecting the default pushbutton (e.g. Save, Ok,...).
    dialog's window.


</li></ul>
* Pressing accelerator keys does not trigger pushbuttons on your dialog's window.


==The window layout we assume==
==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 <b>Notebook Control</b>
and '''Pushbuttons''' in its client area.  The notebook below should demonstrate this.


<p>A typical window containing a notebook is a dialog window (most likely
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!
created with the dialog editor) that contains a <b>Notebook Control</b>
and <b>Pushbuttons</b> in its client area.  The notebook below should
demontrate this.
 
</p><p>By the way, the screen capture was taken from my <b>Program
Commander/2</b> (PC/2) program, which is available from <a href="http://www.geocities.com/SiliconValley/Pines/7885/">my homepage</a>,
and it's Freeware!


[[Image:Nbkey.gif|A typical dialog containing a notebook  control]]  
[[Image:Nbkey.gif|A typical dialog containing a notebook  control]]  


</p><p>This will probably not be new to you, however I would like to summarize
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:
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:


</p><ol>
<ol>


<p></p><li>The <b>Dialog Window</b> is the PM window that implements the whole
<li>The <b>Dialog Window</b> is the PM window that implements the whole dialog, which is created by the Dialog editor and processed by the OS/2 PM APIs <i>WinDlgBox()</i> or <i>WinLoadDlg(), WinProcessDlg()</i>.  If you for example press ESC, you want this <b>Dialog Window</b> to be dismissed.
    dialog, which is created by the Dialog editor and processed by the OS/2 PM
    APIs <i>WinDlgBox()</i> or <i>WinLoadDlg(), WinProcessDlg()</i>.  If you
    for example press ESC, you want this <b>Dialog Window</b> to be dismissed.


<p></p></li><li>The client area of the <b>Dialog Window</b> consists of the
</li><li>The client area of the <b>Dialog Window</b> consists of the <b>Notebook control</b> and the <b>Pushbuttons</b> like <u>S</u>ave, <u>C</u>ancel and <u>H</u>elp. The important thing to mention here is, that the <b>Notebook Control</b> and the <b>Pushbuttons</b> are child windows of the <b>Dialog Window</b>.
    <b>Notebook control</b> and the <b>Pushbuttons</b> like <u>S</u>ave,
    <u>C</u>ancel and <u>H</u>elp. The important thing to mention here is,
    that the <b>Notebook Control</b> and the <b>Pushbuttons</b> are child
    windows of the <b>Dialog Window</b>.


<p></p></li><li>The <b>Notebook Control</b> is a complex OS/2 PM control that
</li><li>The <b>Notebook Control</b> is a complex OS/2 PM control that consists of the <b>Notebook Tabs</b>, the <b>Notebook Page(s)</b> (and the Status line and Page buttons not shown here).
    consists of the <b>Notebook Tabs</b>, the <b>Notebook Page(s)</b> (and the
    Status line and Page buttons not shown here).


<p></p></li><li>The <b>Notebook Tabs</b> are drawn by the <b>Notebook Control</b>.
</li><li>The <b>Notebook Tabs</b> are drawn by the <b>Notebook Control</b>. 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 <u>O</u>ptions for your options notebook page.
    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 <u>O</u>ptions for your options notebook page.


<p></p></li><li>Finally, the <b>Notebook Control</b> contains an area where the
</li><li>Finally, the <b>Notebook Control</b> contains an area where the <b>Notebook page(s)</b> are displayed at.  <b>Notebook Pages</b> are also dialog windows usually created by the dialog editor. When adding to a notebook, the notebook implicitely performs the <i>WinLoadDlg()</i> to load the dialog from a resource.
    <b>Notebook page(s)</b> are displayed at.  <b>Notebook Pages</b> are also
    dialog windows usually created by the dialog editor. When adding to a
    notebook, the notebook implicitely performs the <i>WinLoadDlg()</i> to
    load the dialog from a resource.


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


    </p><p>The important thing to mention here is, that the <b>Notebook Pages</b>
The important thing to mention here is, that the <b>Notebook Pages</b> are child windows of the <b>Notebook Control</b>.
    are child windows of the <b>Notebook Control</b>.


</p></li></ol>
</li></ol>


==Possible solution==
==Possible solution==


====Instance data structure====
====Instance data structure====
In order to forward key events from the notebook control to its owner, we have to modify the way the <b>Notebook Control</b> processes such events, in case the notebook is not interested in that event.


<p>In order to forward key events from the notebook control to its owner, we
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.
have to modify the way the <b>Notebook Control</b> processes such events,
in case the notebook is not interested in that event.


</p><p>When working with data that is window dependent (instance data), the
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 procedureIn the following code excerpt, the processing supports 3 different dialogs.
proper way is to use the window words to save a pointer to your dataThe
QWL_USER window word is reserved for the user and would be a good starting
point.


</p><p>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.
</p><pre><small>
   typedef struct _NOTEBOOKSUBCLASS    NOTEBOOKSUBCLASS;
   typedef struct _NOTEBOOKSUBCLASS    NOTEBOOKSUBCLASS;
 
   /* Structure to save which notebook was subclassed
   /* Structure to save which notebook was subclassed
     from which previous window procedure */
     from which previous window procedure */
Line 113: Line 62:
                               before subclassing */
                               before subclassing */
   };
   };
 
   /* Desktop dialog notebook subclassed */
   /* Desktop dialog notebook subclassed */
   #define DD_SUBCLASSEDNOTEBOOK      0
   #define DD_SUBCLASSEDNOTEBOOK      0
 
   /* Program Installation dialog notebook subclassed */
   /* Program Installation dialog notebook subclassed */
   #define PI_SUBCLASSEDNOTEBOOK      1
   #define PI_SUBCLASSEDNOTEBOOK      1
 
   /* Control Configuration dialog notebook subclassed */
   /* Control Configuration dialog notebook subclassed */
   #define CC_SUBCLASSEDNOTEBOOK      2
   #define CC_SUBCLASSEDNOTEBOOK      2
 
   /* As only one instance of the Desktop and Program Installation
   /* As only one instance of the Desktop and Program Installation
     dialogs is allowed, its save to avoid more complicated
     dialogs is allowed, its save to avoid more complicated
     per dialog instantiation, but use module class storage */
     per dialog instantiation, but use module class storage */
   #define NOTEBOOKSUBCLASSMAX        3
   #define NOTEBOOKSUBCLASSMAX        3
 
   NOTEBOOKSUBCLASS    DialogNotebookSubclass[NOTEBOOKSUBCLASSMAX];
   NOTEBOOKSUBCLASS    DialogNotebookSubclass[NOTEBOOKSUBCLASSMAX];
</small></pre>


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


====Modifying the notebook processing====
====Modifying the notebook processing====


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


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


</p><pre><small>
     /* Subclass notebook's window procedure to add handling of
     /* Subclass notebook's window procedure to add handling of
       notebook pages, notebook and outer dialog accelerator keys */
       notebook pages, notebook and outer dialog accelerator keys */
Line 150: Line 93:
     DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].pfnwpNotebook=
     DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].pfnwpNotebook=
       WinSubclassWindow(hwndNB, SubclassedNotebookProcedure);
       WinSubclassWindow(hwndNB, SubclassedNotebookProcedure);
</small></pre>


Subclassing means, that we add a new window procedure to the existing
Subclassing means, that we add a new window procedure to the existing window procedure of the <b>Notebook Control</b> to be able to add
window procedure of the <b>Notebook Control</b> 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
processing logic before the notebook's window procedure.  In your own
<i>switch-Statement</i> 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 <i>WinSubclassWindow()</i>.
controls, you would add the processing somewhere in the big
<i>switch-Statement</i> 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
<i>WinSubclassWindow()</i>.


<p>Another thing to mention here is, that in our example all procedures
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.
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.


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


</p><p><a href="http://www.edm2.com/0508/nbkey1.c">Subclassed window procedure.</a>
<a href="http://www.edm2.com/0508/nbkey1.c">Subclassed window procedure.</a>


</p><p>The first thing we do in the subclassed window procedure is look for
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.
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.


</p><p>In detail, the following happens for a <i>WM_CHAR</i> message is routed
In detail, the following happens for a <i>WM_CHAR</i> message is routed to the notebook:
to the notebook:


</p><ol>
</p><ol>
Line 208: Line 134:


====Modifying the notebook pages' processing====
====Modifying the notebook pages' processing====
 
In the window procedure of the <b>Notebook Pages</b> we also have to catch the key events, by looking for <i>WM_CHAR</i> 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:
<p>In the window procedure of the <b>Notebook Pages</b> we also have to catch
the key events, by looking for <i>WM_CHAR</i> 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:
   case WM_CHAR:
Line 225: Line 145:
                                             QW_OWNER), mp1, mp2);
                                             QW_OWNER), mp1, mp2);
       break;
       break;
In order to allow window procedures of different notebook pages to share the same implementation, all <i>WM_CHAR</i> messages are processed by
In order to allow window procedures of different notebook pages to share the same implementation, all <i>WM_CHAR</i> messages are processed by
ProcessPageKey().
ProcessPageKey().
Line 230: Line 151:
'''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.
'''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.


</p><p><a href="http://www.edm2.com/0508/nbkey2.c">ProcessPageKey</a>
<a href="http://www.edm2.com/0508/nbkey2.c">ProcessPageKey</a>
 
</p><p>This function is somewhat more complex.  In order to make debugging easier, <i>printf()</i> calls are added to generate debug information. You can either redirect stdout to a file (e.g. by invoking myapp.exe with "myapp &gt; debuginfo"), or you get the very useful IBM EWS-written <b>PMPRINTF</b> package, which replaces the C-library <i>printf()</i> function with one that writes into an OS/2 queue, and using the included PM viewer you can display that queue in real-time.
 
</p><p>In detail, the following happens for a <i>WM_CHAR</i> message routed to a notebook page's dialog procedure:


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


<p></p><li>If the key event is a navigation key, that is the tab and cursor keys, we accept the processing of the <b>Notebook Page</b>'s window procedure.
In detail, the following happens for a <i>WM_CHAR</i> message routed to a notebook page's dialog procedure:
 
<p></p></li><li>If the key event is not a navigation key, we ask the <b>Dialog Window</b> if the key makes sense there, for example the ALT+C key for a <u>C</u>ancel <b>Pushbutton</b>).
 
</li></ol>


<h3>Modifying the dialog window's processing</h3>
<ol>
<li>If the key event is a navigation key, that is the tab and cursor keys, we accept the processing of the <b>Notebook Page</b>'s window procedure.</li>
<li>If the key event is not a navigation key, we ask the <b>Dialog Window</b> if the key makes sense there, for example the ALT+C key for a <u>C</u>ancel <b>Pushbutton</b>).</li>
</ol>


<p>The <b>Dialog Window</b> 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 <b>Notebook Control</b> and by the individual <b>Notebook Pages</b>.  In any case, we are interested in the processing of WM_CHAR messages as shown below.
====Modifying the dialog window's processing====
The <b>Dialog Window</b> 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 <b>Notebook Control</b> and by the individual <b>Notebook Pages</b>.  In any case, we are interested in the processing of WM_CHAR messages as shown below.


</p><p>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 <i>WM_CHAR</i> processing.
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 <i>WM_CHAR</i> processing.


   case WM_CHAR:
   case WM_CHAR:
Line 269: Line 186:
The reason why we may get into recursion is, that while processing a key in the <b>Dialog Window</b>'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.
The reason why we may get into recursion is, that while processing a key in the <b>Dialog Window</b>'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.


<p><a href="http://www.edm2.com/0508/nbkey3.c">DispatchKeyToNotebook</a>
<a href="http://www.edm2.com/0508/nbkey3.c">DispatchKeyToNotebook</a>


In detail, the following happens for a <i>WM_CHAR</i> message routed to the dialog procedure:
In detail, the following happens for a <i>WM_CHAR</i> message routed to the dialog procedure:

Revision as of 19:29, 3 February 2012

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:

<a href="http://www.edm2.com/0508/nbkey1.c">Subclassed window procedure.</a>

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.

<a href="http://www.edm2.com/0508/nbkey2.c">ProcessPageKey</a>

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 <a href="http://www.edm2.com/0508/nbkey3.c">DispatchKeyToNotebook()</a>. As a notebook page's window procedure calls the dialog's window procedure by using WinSendMsg() (as shown in <a href="http://www.edm2.com/0508/nbkey2.c">ProcessPageKey()</a>) 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.

<a href="http://www.edm2.com/0508/nbkey3.c">DispatchKeyToNotebook</a>

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 Windowis 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.