OS/2 v4.0 Notebooks

From EDM2
Jump to: navigation, search

Written by Stephane Bessette


I recently desired to create a notebook control. Since that seemed to be a frequently used control, I thought I'd find plenty of information. Sadly, that wasn't the case. And the sample code I did find was quite obtuse. But now that I've finally figured it out, I'll try to show a simple process of creating an OS/2 v4.0 style notebook.

[Note: notebook.zip here is a link to the source code for this article. Ed.]

The Controls

I've used the Universal Resource Editor v1.1b (URE) to design the notebook controls. The first step is to create a simple dialogue, with a title bar, system menu, a dialogue frame, and a hide button. I've sized it at 293cx and 203cy, so that its size will be like that of other notebooks.

Four controls are placed on that dialogue. On the bottom edge, around the horizontal centre, are placed three push buttons: Undo, Default, and Help. The fourth control is the notebook control. The dimension of this control determines the dimension of the notebook pages (in URE at least). I've sized this control at 289cx and 174cy, so that it fits snugly in the notebook dialogue and is just above the push buttons. And I've selected a spiral binding (there's a reason for this choice, as we'll see shortly).

The next step is to create the notebook pages. In URE, you simply select the notebook page control, set the ID of the parent notebook dialogue and the ID of the notebook control. And then place whatever other controls you require (entry field, combo box, checkbox, ...) on that notebook page. When you're done, save the work as a .rc source file, not as a .res (we'll edit the resource file shortly).

OS/2 v4.0 Style Notebook

Version 1.1b of URE only allows the creation of the old type of notebook. Fortunately it's not too difficult to patch up the resource definition file in order to obtain an OS/2 v4.0 style notebook. Manually, you'd simply locate the definition of the notebook dialogue in the .rc file and add 0x800 to the styles, as in "BKS_TABTEXTLEFT | BKS_SPIRALBIND | 0x800 |". Or, a small REXX script could be used to automate the task, by searching for a keyword that is unique to notebooks, such as BKS_SPIRALBIND. (I've included !Resource.cmd in the source code for just that purpose.)

Creating The Application

A notebook dialogue can be treated just like any other dialogue, and started with WinDlgBox(). The notebook pages have to be inserted during the initialization of the notebook dialogue, upon receipt of the WM_INITDLG message. I've written a small C++ class to help out by handling the menial tasks of this insertion. The first step is to create an instance of the class:

Class_Notebook *Notebook;
Notebook = new Class_Notebook( hwnd, NBK_NOTEBOOK );

The parameters are the handle of the parent window (i.e. the dialogue that contains the notebook control), and the notebook control resource ID. After this, major and minor tabs can be easily inserted through AddMajor() and AddMinor(). Both member functions have the same parameters. NOTE: When adding a major tab, the text on the tab and just below the tabs will be the same. However, when adding a minor tab, the second parameter to AddMinor() will specify the text that appears just below the tabs. It should be possible for major tabs to also have a text just below the tabs that is different than the text on the tab, as do the Icon/Tree/Details view tab, but I lack that knowledge. On the other hand, this may be a feature available in the OS/2 Toolkit v4.0, in which case it would make sense that I cannot access it from the OS/2 Toolkit v3.0. Anyhow, to add a major tab:

Notebook->AddMajor( NBKP_NOTEBOOK1,     // ID of the notebook page
                    "First Major Page", // Text to appear on the tab
                    Dlg_NbkpPage,       // Dialog procedure for this page
                    "Status Text 1" );  // Status text

And to add a minor tab:

Notebook->AddMinor( NBKP_NOTEBOOK2,    // ID of the notebook page
                    "Special Text For Minor Tab Of First Major Page",
                    Dlg_NbkpPage,      // Dialog procedure for this page
                    "Status Text 2" ); // Status text

It's that simple. In addition, the OS/2 Warp v4.0 notebook handles the sizing of the notebook tabs, so that text will always fit within them. So creating a notebook is pretty straightforward. The next topic to cover is the handling of the pushbuttons at the bottom of the notebook dialogue.

Undo, Default, Help

When the Undo push button is pressed, the expected behaviour is to undo the changes, but only on the current page. When the Default push button is pressed, the expected behaviour is to set all the controls of the current page to some default value. And when the Help push button is pressed, the expected behaviour is to provide help about the current page. So how do we know what the current page is? By monitoring the WM_CONTROL message in the notebook dialogue procedure.

When a new page is selected, the second short of mparam1 will contain the BKN_PAGESELECTED notification code and mparam2 will contain a pointer to a PAGESELECTNOTIFY structure. That structure contains the ID of the currently displayed notebook page (ulPageIdCur) and the ID of the newly selected page (ulPageIdNew).

   switch( SHORT1FROMMP( mp1 ) )
   case NBK_NOTEBOOK: // ID of the notebook control
        switch( SHORT2FROMMP( mp1 ) )
        case BKN_PAGESELECTED: // Notification code
             // Retrieve the ID of the new page
             ulPageID = ( (PPAGESELECTNOTIFY) mp2 )->ulPageIdNew;

Sending a BKM_QUERYPAGEWINDOWHWND message to the notebook control with this ID will return the handle of the notebook page dialogue. A message can then be sent to that page. The C++ class simplifies this by providing SendMessage() and PostMessage() member functions. The following code snippet would be used to notify, by posting a message, the currently displayed notebook page that the Undo push button has been pressed:

Notebook->PostMessage( ulPageID,          // ID of the page
                       WM_COMMAND,        // Message
                       MPARAM( PB_UNDO ), // mparam1
                       0 );               // mparam2

Closing The Notebook

The last item of business to cover is the termination of the notebook. You'd be wrong to assume that a WM_CLOSE is sent to every notebook page: it is only sent to the notebook control dialogue procedure. That's not a big deal, as we'll pass that message along to all notebook pages. So what are the IDs of all the notebook pages? We could keep track of those when we insert the notebook pages. Or we could enumerate. This is coming up next.

Enumerating The Notebook Pages

The BKM_QUERYPAGEID message returns the ID of the specified page: BKA_FIRST, BKA_LAST, BKA_NEXT, or BKA_PREV. Thus, to go through every notebook page, we'd start by obtaining the ID of the first notebook page and then successively obtain the ID of the subsequent notebook page:

ulPageID = (ULONG) WinSendMsg( hwndNotebook,
                               MPFROM2SHORT( BKA_FIRST, 0 ) );
while( ulPageID )
  ulPageID = (ULONG) WinPostMsg( hwndNotebook,
                                 MPFROMP( ulPageID ),
                                 MPFROM2SHORT( BKA_NEXT, 0 ) );

This dynamic has been included in the C++ class. To send a message to all notebook pages, simply pass zero as the ID of the notebook page to the SendMessage() member function, as in:

Notebook->SendMessage( 0,        // All notebook pages
                       WM_CLOSE, // Message
                       0,        // mparam1
                       0 );      // mparam2


The data entered in the notebook could be validated and retrieved when each page receives the WM_CLOSE message. But this would mean that users would be allowed to enter anything, and only be told they made a mistake when they close the notebook. There's a better way. We've already seen how to monitor a notebook page change in WM_CONTROL. We could respond to that event by sending the current notebook page a message instructing it to validate all its contents. For this purpose, I've created a new message called WM_VALIDATE:

#define WM_VALIDATE       WM_USER+1

And that message can then be sent to the notebook page:

if( Notebook->SendMessage( ulPageID, WM_VALIDATE, 0, 0 ) )
     // Notebook page successfully validated
     // Validation error on notebook page