Fitting a Notebook into a Dialog

Written by Roman Stangl

The problem
In my experience, is seems to be difficult to create dialog windows containing a notebook in a way that:
 * the dialog has no sizing border, that is the user can't increase the dialog's size to improve readability.
 * the notebook fits into the dialog under both the old OS/2 2.x and WARP 3 notebook style on one hand, and under the new WARP 4 notebook style on the other hand.
 * the notebook does not overlay or is too far away from the push buttons usually found below a notebook (e.g. the Save, Ok, Cancel, and Help push buttons, and so on).
 * the notebook pages are displayed correctly, that is, neither parts of the notebook pages are missing nor is there excessive space left over.Notebook1.gif

Figure 1.a is an example of what should be avoided. The screen capture was taken under WARP 3, as the new notebook style of WARP 4 has improved the situation a lot. I've captured one of Netscape for OS/2's dialogs, but it also applies to many of the dialogs shipped with OS/2 itself. This is no criticism of Netscape, in fact I (almost) could not live without Netscape Navigator/2, but just an example what I'm talking about.

Figure 1.a shows 2 pitfalls, first, the notebook page is partly clipped away, and second, there is some excessive space above and right of the notebook.

I admit that I'm not using a default OS/2 installation. Immediately after having installed OS/2, I use the REXX batch file shown in Figure 1.b to set the default OS/2 font to 8.Helv. Additionally, I patch the display drivers to use the VGA font sizes under the screen resolution that normally use the XGA font sizes (1024*768 and 1280*1024). I really need the additional space created by these patches (e.g. to have space for all the debugger windows in addition to the debuggee), and this is much cheaper than buying a 21-inch display ;-).

Figure 1.b

/* Set default font to Helv size 8 or restore to default */ parse upper arg option. call RxFuncAdd "SysIni", "RexxUtil", "SysIni" AppName = "PM_SystemFonts" KeyName = "DefaultFont" if option = "RESTORE" then do FontName = "10.System Proportional" say "Restoring default font 10.System Proportional" end else do FontName = "8.Helv" say "Setting default font 8.Helv" end call SysIni "USER", AppName, KeyName, FontName||"0"x say "Font will be active after next reboot" exit For WARP 4 you likely want to replace 10.System Proportional by 9.WarpSans.

With the solution outlined in this document, you will be able to avoid the pitfalls outlined above. The following two screen captures were taken from my Program Commander/2 (PC/2) program, which is available from my homepage, and it's Freeware !



Figure 1.c

Figures #Figure_1.c and #Figure_1.d show the same notebook, the only difference is the old and new notebook style.



Figure 1.d

Now let's take a look how this can be implemented.

Definitions
Figure 2.1.a shows the definition of the notebook. It make things easier when all notebook pages are of the same size. In this example all notebook pages measure NOTEBOOKPAGE_CX by NOTEBOOKPAGE_CY dialog units.

A few comments about dialog units. When you draw a dialog with the dialog editor, all coordinates are in dialog units and not pixels. The concept of dialog units was introduced to allow dialogs to adapt to different resolutions for a given screen diagonals. For example, the goal was that a dialog that takes half of a display under VGA resolution (that is having a size of 320*240 pixels), does also take half of that display under XGA resolution (that is the dialog would now have a size of 512*384 pixels). If such an adaption would not have been made, the same dialog that takes half of a VGA display would just take 320*240 pixels on a XGA display, which is less than a third compared to VGA. Using higher resolutions would then display unreadable small dialogs.

When you use the dialog editor to design your dialog measures in dialog units, 1 dialog unit for example may translate to 1.5 pixels on a VGA and to 2 pixels on a XGA resolution. You can of course translate between dialog units and pixels by using the WinMapDlgPoints API.

Figure 2.1.a /* All notebook dialog pages that are loaded into any of       PC/2's notebooks are 215 * 165 dialog units in size. These dialog units are used to size the notebook so that a notebook dialog page fits into the notebook area where dialogs are loaded into. The dialog window is somewhat larger as required on 8514/A and VGA displays to have some spare place to size the notebook within */
 * 1) define NOTEBOOKPAGE_CX      215
 * 2) define NOTEBOOKPAGE_CY      162


 * 1) define DID_HELP               3       /* DID_CANCEL+1 */

struct _NBPAGE                         /* Notebook pages setup data */ { PFNWP          pDialogWndProc;         /* Window procedure of a notebook page */ HWND           hwndNBPage;             /* Window handle of a notebook page */ ULONG          ulIDPage;               /* ID of notebook page */ PSZ            pszStatusLine;          /* Status line text */ PSZ            pszTab;                 /* Tab text */ ULONG          ulIDDialogPage;         /* ID of a dialog resource */ ULONG          ulIDFocus;              /* ID of control to receive focus */ };

Additionally, when all your notebook pages (which are dialogs you design with the dialog editor) are all of the same size:
 * you can adjust your notebook to a known page size instead of having to query all notebook pages to find out the largest one,
 * you get best usability, as there are no notebook pages that have excessive space inside the notebook (assuming you adjust the notebook's page size being able to contain the largest notebook page, to avoid it being clipped away partly).

Most likely, you will also use dialog editor to design the dialog that contains the notebook and adapts to it accordingly. Figure 2.1.b shows the resource used as the basis to create the dialog shown in Figures 1.c and 1.d

Figure 2.1.b DLGTEMPLATE DDID_DESKTOPDIALOG LOADONCALL MOVEABLE DISCARDABLE BEGIN DIALOG "PC/2 - Program Commander/2 (Desktop Configuration)", DDID_DESKTOPDIALOG, 50, 4, 316, 215, WS_VISIBLE, FCF_TITLEBAR | FCF_NOBYTEALIGN BEGIN DEFPUSHBUTTON  "~Save", DID_OK, 4, 3, 43, 14, WS_GROUP PUSHBUTTON     "~Cancel", DID_CANCEL, 136, 3, 43, 14 PUSHBUTTON     "~Help", DID_HELP, 269, 3, 43, 14, BS_HELP END END It does not make much difference how large your dialog will be, as it will be resized so that the notebook perfectly fits into it. You can also position the push buttons horizontally as you like (not vertically though), and they will also be align to fit perfectly. Just ensure that they are of the same size (otherwise the calculations become more difficult).

The dialog's creation
Once you requested the creation of the dialog, for example by calling WinDlgBox or WinLoadDlg, the dialog's window procedure will be called with a message of WM_CREATE being sent, as shown in Figure 2.2.a

The first thing during WM_CREATE processing is to add the notebook control to the dialog. Depending on the version of OS/2, the notebook gets created with either the old WARP 3 or the new WARP 4 notebook style.

The notebook must be created dynamically, as the dialog editor currently doesn't support the new notebook style, even after adding this style to the *.DLG file and resource compiling it, WinDlgBox will not be able to correctly load it from the resource. Additionally, you can't add the notebook to the resource having the old style and then changing the style at run-time in the QWL_STYLE window word during WM_CREATE processing, as the resulting notebook will not be drawn correctly (which I believe to be a PM limitation).

You can get the version of OS/2 you are running under by calling the DosQuerySysInfo(QSV_VERSION_MAJOR, QSV_VERSION_MINOR,...) API, whose output has already been cached in this example.

Figure 2.2.a /*---*\ * This dialog procedure handles the PC/2 - Desktop configuration dialog.* * Req: none                                                            * \*---*/ MRESULT EXPENTRY DD_DialogProcedure(HWND hwndDlg, ULONG msg,                                     MPARAM mp1, MPARAM mp2) { /* Program Installation dialog notebook window handle */ static HWND        hwndNB;

switch(msg) { case WM_INITDLG: {   /* Notebook style (to take the new Merlin notebook style into       account, just try out SET NEWNOTEBOOKS=YES in your Merlin       beta CONFIG.SYS with your "old"-style notebooks). Under OS/2 WARP 4 (aka "Merlin"), there seems to be a bug, as a notebook created from a dialog resouce must already have the BKS_TABBEDNOTEBOOK style set when being created, as setting the window style dynamically does corrupt the notebook drawing */ ULONG      ulNBStyle; USHORT     usTabTextLength=50; USHORT     usTabTextHeight=5;

WinDefDlgProc(hwndDlg, msg, mp1, mp2); ulNBStyle=WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE; if(((pHP->ulVersionMajor==OS2_MAJOR) && (pHP->ulVersionMinor>=OS2_MINOR_400)) ||       (pHP->ulVersionMajor>OS2_MAJOR)) ulNBStyle|=(BKS_TABBEDDIALOG|BKS_MAJORTABTOP|BKS_BACKPAGESTR|           BKS_STATUSTEXTRIGHT|BKS_TABTEXTCENTER|BKS_POLYGONTABS); else ulNBStyle|=(BKS_SPIRALBIND|BKS_MAJORTABRIGHT|BKS_BACKPAGESBR|           BKS_STATUSTEXTCENTER|BKS_TABTEXTRIGHT); hwndNB=WinCreateWindow(hwndDlg, WC_NOTEBOOK, "", ulNBStyle, 0, 0, 0, 0, hwndDlg,       HWND_BOTTOM, DDNB_NOTEBOOK, NULL, NULL); /* As the Lockup Settings notebook page may display a message box being this dialog the owner, associate the main helptable with this dialog */ WinAssociateHelpInstance(pHP->hwndHelp, hwndDlg); ...   /* Fill the notebook and return the tab text requirements */ AddPagesToNotebook(hwndDlg, hwndNB, nbDDNotebookPage,                      &usTabTextLength, &usTabTextHeight); /* Adjust dialog to optimally cover the notebook */ AdjustNotebook(hwndDlg, hwndNB, ulNBStyle, usTabTextLength, usTabTextHeight); ...   WinSetFocus(HWND_DESKTOP, nbDDNotebookPage[DD_PAGE_1A].ulIDFocus); return((MRESULT)TRUE); } It is important to note the style WS_CLIPSIBLINGS|WS_CLIPCHILDREN that the notebook is created with. The reason for this is that as the notebook reserves space below itself in case minor tabs will be added, it would overdraw (at least partly) the push buttons of the dialog. The special style tells the notebook not to draw over siblings and children windows, and creating the notebook at HWND_BOTTOM Z-order relative to the owning dialog ensures that the push buttons are siblings.

After you have created the notebook control into your dialog (note that it has been created zero-sized!), pages have to be added into the notebook and finally the notebook and the dialog have to be adjusted accordingly.

Adding pages to the notebook control
The notebook has been created (still zero-sized), now it's time to add some notebook pages, as shown in Figure 2.3.a. As said above, notebook pages are (preferably same sized) dialogs created with the dialog editor. The description of the individual pages is passed in an array of NBPAGE (Figure_2.1.a) structures, terminated by an array element initialized to NULL. The dialog implementing the notebook pages are loaded with the WinLoadDlg API, which you have surely expected.

In order to perfectly fit the notebook into the dialog, the size of the notebook tabs has also to be taken into account, therefore while adding a notebook page, the textbox of the tab text is queried, and saved if it is the largest one up to now.

Figure 2.3.a /*---*\ * This procedure adds notebook pages from a NBPAGE template into a notebook, an * * calculates the size required to hold the largest notebook tab text. * * Req:                                                                         * *     hwndDialog .... Dialog where the notebook control is part of the client * *     hwndNotebook .. Notebook control that is part of the client             * *     nbpageNotebook  The NBPAGE structure the notebook pages are filled from  * *     pusTabTextLength                                                         * *                     Maximum length of the notebook's tab text                * *     pusTabTextHeight                                                         * *                     Maximum height of the notebook's tab text (from          * *                      FONTMETRICS.lMaxBaselineExt)                             * * Ret:                                                                         * *     mpRc .......... Return code: FALSE no error, errorcode otherwise        * * Ref:                                                                         * \*---*/ MPARAM AddPagesToNotebook(HWND hwndDialog, HWND hwndNotebook,                           NBPAGE nbpageNotebook[], USHORT *pusTabTextLength,                           USHORT *pusTabTextHeight) { HPS        hps;           /* Used to query font metrics for tab size */ FONTMETRICS fmFontMetrics; POINTL     aptlTabText[TXTBOX_COUNT]; ULONG      ulPage, ulPageID;

/* Get presentation space to query fontmetrics */ hps=WinGetPS(hwndNotebook); memset(&fmFontMetrics, 0, sizeof(FONTMETRICS)); if(GpiQueryFontMetrics(hps, sizeof(FONTMETRICS), &fmFontMetrics)) fmFontMetrics.lMaxBaselineExt<<=1; else fmFontMetrics.lMaxBaselineExt=30; /* Load and associate all dialogs of the Program Installation dialog to notebook pages */ for(ulPage=0; nbpageNotebook[ulPage].pDialogWndProc!=NULL; ulPage++) {   USHORT  usPageStyle=(BKA_STATUSTEXTON | BKA_AUTOPAGESIZE); int    iTabLength; /* Load a dialog into a page */ if(!(nbpageNotebook[ulPage].hwndNBPage=WinLoadDlg(       hwndDialog,                     /* Parent window */        hwndDialog,                     /* Owner window */                                        /* Window procedure */        nbpageNotebook[ulPage].pDialogWndProc,        pHP->hDLLPc2Resource,           /* Resource idendity */                                        /* Dialog idendity */        nbpageNotebook[ulPage].ulIDDialogPage,        NULL)))                         /* Application defined data area */ /* On error suggest exiting */ PM_ERR(pHP->habPc2, pHP->hwndFrame, HELP_CREATEDIALOG,              MB_ERROR|MB_OK|MB_HELP|MB_MOVEABLE|MB_DEFBUTTON1,            "Creation of a dialog box failed - continuing..."); /* Query length of largest tab text */ iTabLength=strlen(nbpageNotebook[ulPage].pszTab); if(iTabLength) {       if(GpiQueryTextBox(hps, iTabLength, nbpageNotebook[ulPage].pszTab, TXTBOX_COUNT, aptlTabText)) if(*pusTabTextLength<aptlTabText[TXTBOX_CONCAT].x)               *pusTabTextLength=aptlTabText[TXTBOX_CONCAT].x;        usPageStyle|=BKA_MAJOR; }   ulPageID=(ULONG)WinSendMsg(         /* Insert a page into the notebook */        hwndNotebook,        BKM_INSERTPAGE,        (MPARAM)NULL, /* Page ID, ignored if BKA_FIRST or BKA_LAST specified */                                        /* Style and order attribute */        MPFROM2SHORT(usPageStyle, BKA_LAST)); nbpageNotebook[ulPage].ulIDPage=ulPageID; /* Set focus */ WinSetFocus(HWND_DESKTOP, nbpageNotebook[ulPage].ulIDFocus); /* Set text into the status line */ WinSendMsg(hwndNotebook,       BKM_SETSTATUSLINETEXT,        MPFROMP(ulPageID),        MPFROMP(nbpageNotebook[ulPage].pszStatusLine)); /* Set text into tab */ WinSendMsg(hwndNotebook,       BKM_SETTABTEXT,        MPFROMP(ulPageID),        MPFROMP(nbpageNotebook[ulPage].pszTab)); /* Associate page with dialog */ WinSendMsg(hwndNotebook,       BKM_SETPAGEWINDOWHWND,        MPFROMP(ulPageID),        MPFROMLONG(nbpageNotebook[ulPage].hwndNBPage)); } WinReleasePS(hps); return(0); }
 * pusTabTextHeight=fmFontMetrics.lMaxBaselineExt;

Adjusting the notebook and dialog
Having created the notebook control (still zero-sized), added all notebook pages and knowing the tab size required to fit the longest tab text, it's now time to size the notebook control and dialog window and to adjust the pushbuttons as shown in Figure 2.4.a.

Not surprisingly, the size of the notebook tabs is configured. You may notice that the size of the textbox is modified slightly, in my opinion, the notebook tabs look better then, but that's up to you.

Much more important to note is, that the size of the minor tabs is explicitely set to 0, even if we haven't loaded any minor tab.

As all dialogs implementing the notebook pages are of the same known size, we now translate the dialog units into pixels. Then we ask the notebook control to reserve the resulting space, measured in pixels, for the space the notebook pages will require to draw completely.

The notebook is still zero-sized, so we subtract the number of pixels by which it is too large to fit the notebook pages (as it is still too small, we subtract a negative number, thus adding something), while taking the space required for the notebook tabs into account. Now we know the required size of the notebook control and we resize it accordingly, that's all what has to be done to size a notebook control to perfectly fit the notebook pages and tabs!

The next step is to move the pushbuttons so that they are equidistant and the leftmost one's left edge aligns to the left border of the notebook and the rightmost one's right edge aligns to the right border of the notebook. You may notice, that for WARP 4, a adjustment is made to the y (vertical) position of the notebook control. The simple answer is, that for unknown reasons, even after having set the minor tab size to 0 and aligning the notebook at 0 vertically, the notebook's bottom corner starts drawing some pixels higher.

Once we know the size of the notebook control, it is easy to calculate the size of a frame window (and a dialog is still a kind of frame window) that the notebook control perfectly fits into. Alternatively, it might be worth trying the WinCalcFrameRect API as it promises to do the same.

Figure 2.4.a /*---*\ * This procedure ensures that the dialog is optimally sized regarding to its   * * client controls, which are the notebook and the DID_OK, DID_CANCEL and       * * DID_HELP pushbuttons. The notebook pages are assumed to be NOTEBOOKPAGE_CX by * *               NOTEBOOKPAGE_CY                                                * * in size, the DID_HELP value is assumed to be DID_CANCEL+1. * * Req:                                                                         * *     hwndDialog .... Dialog where the notebook control is part of the client * *     hwndNotebook .. Notebook control that is part of the client             * *     ulNotebookStyle QWL_STYLE of the notebook control (required to take the  * *                      new notebook style into account that was introduced by   * *                      Merlin beta)                                             * *     usTabTextLength Maximum length of the notebook's tab text                * *     usTabTextLength Maximum height of the notebook's tab text (from          * *                      FONTMETRICS.lMaxBaselineExt, where we can apply a factor * *                        0.8)                                                   * * Ret:                                                                         * *     mpRc .......... Return code: FALSE no error, errorcode otherwise        * * Ref:                                                                         * \*---*/ MPARAM AdjustNotebook(HWND hwndDialog, HWND hwndNotebook, ULONG ulNotebookStyle,                       USHORT usTabTextLength, USHORT usTabTextHeight) { RECTL      rectlNB;    /* Notebook rectangle to load notebook dialog page into */ POINTL     ptlNB;      /* Notebook page conversion from dialog units to pixels */ SWP        swpNB; SWP        swpDialog; ULONG      ulButton; ULONG      ulEdge; ULONG      ulSpace; SWP        swpButton;

WinSendMsg(hwndNotebook, /* Set background to dialog box background */    BKM_SETNOTEBOOKCOLORS,    MPFROMLONG(SYSCLR_FIELDBACKGROUND),    MPFROMSHORT(BKA_BACKGROUNDPAGECOLORINDEX)); WinSendMsg(hwndNotebook, /* Set tab dimension */    BKM_SETDIMENSIONS,    MPFROM2SHORT(usTabTextLength+5, (SHORT)((float)usTabTextHeight*0.8)),    MPFROMSHORT(BKA_MAJORTAB)); WinSendMsg(hwndNotebook, /* Set tab dimension */    BKM_SETDIMENSIONS,    MPFROM2SHORT(0 ,0),    MPFROMSHORT(BKA_MINORTAB)); /* Notebook page dialog panel size in dialog units */ ptlNB.x=NOTEBOOKPAGE_CX; ptlNB.y=NOTEBOOKPAGE_CY; /* Map dialog units to screen pixels */ WinMapDlgPoints(hwndDialog, &ptlNB, 1, TRUE); /* Query frame and notebook size and position in pixels */ WinQueryWindowPos(hwndDialog, &swpDialog); WinQueryWindowPos(hwndNotebook, &swpNB); rectlNB.yBottom=rectlNB.xLeft=0; rectlNB.xRight=swpNB.cx; rectlNB.yTop=swpNB.cy; /* Return size of notebook page in pixel for given notebook window size (with tab text size set) */ WinSendMsg(hwndNotebook, BKM_CALCPAGERECT, MPFROMP(&rectlNB), MPFROMSHORT(TRUE)); /* Calculate notebook size so that notebook dialog page fits into the notebook page */ swpNB.cx-=(rectlNB.xRight-rectlNB.xLeft)-ptlNB.x; swpNB.cy-=(rectlNB.yTop-rectlNB.yBottom)-ptlNB.y;                                       /* Size notebook accordingly */ swpNB.fl=SWP_SIZE|SWP_MOVE; swpNB.hwndInsertBehind=NULLHANDLE; /* Use the DID_OK pushbutton as a reference */ WinQueryWindowPos(WinWindowFromID(hwndDialog, DID_OK), &swpButton); /* As we have created the notebook dynamically (not from            dialog resource) because Merlin does not support to set the BKS_TABBEDDIALOG window style to be set by QWL_STYLE, we have to adjust from 0 position and size */ ulEdge=swpNB.x=swpButton.x;         /* Take pushbutton selection border into account */ swpButton.fl=SWP_MOVE; swpButton.x++; WinSetMultWindowPos(pHP->habPc2, &swpButton, 1); /* Fit the notebook above the pushbuttons (equidistant            to the pushbutton's position to the frame border). This requires the CS_CLIPCHILDREN style not to            overpaint the pushbuttons */ swpNB.y=swpButton.cy+(swpButton.y<<1)-1; if(((pHP-=OS2_MINOR_400)) ||   (pHP-OS2_MAJOR)) swpNB.y=(swpButton.y<<1)-2; /* Size the dialog to the notebook's size (including the            vertical space requirements of the pushbuttons) */ swpDialog.fl=SWP_SIZE; swpDialog.cx=swpNB.cx+(ulEdge<<1); swpDialog.cy=swpNB.y+swpNB.cy+WinQuerySysValue(HWND_DESKTOP, SV_CYTITLEBAR)+ (WinQuerySysValue(HWND_DESKTOP,SV_CYDLGFRAME)<<1); WinSetMultWindowPos(pHP->habPc2, &swpDialog, 1); /* Move the DID_CANCEL and DID_HELP pushbuttons to be            inside the new dialog's size and being equidistant. Note: DID_HELP pushbutton must have ID 3 */ ulSpace=(swpDialog.cx-(ulEdge<<1)-(swpButton.cx*3))>>1; for(ulButton=DID_HELP; ulButton>=DID_CANCEL; ulButton--) {   WinQueryWindowPos(WinWindowFromID(hwndDialog, ulButton), &swpButton); swpButton.fl=SWP_MOVE; swpButton.x=swpDialog.cx-ulEdge- (swpButton.cx*(DID_HELP-ulButton+1))- (ulSpace*(DID_HELP-ulButton)); WinSetMultWindowPos(pHP->habPc2, &swpButton, 1); } WinSetMultWindowPos(pHP->habPc2, &swpNB, 1); /* Center frame window */ CenterWindow(hwndDialog); return(0); } The function CenterWindow just centers the dialog at the center of the screen. It's nothing more than comparing the dialog's size with the screen's size and calculate the position the dialog must be moved to to be centered.

Summary
Hopefully above example has shown that it is not too difficult to create dialogs containing a notebook control and pushbuttons that adapt to differenct resolutions, fonts and notebook styles.

Credits
The code shown here is a solution developed by myself. Some ideas how this can be done I have taken from methods of frame windows available in IBM's OCL (Open Class Library) of IBM's VisualAge C++ compiler. By the way, the OCL allows you to implement above functionality much more effiently with just a few lines of code!

Roman Stangl (rstangl@vnet.ibm.com)