Development of a New Window Class - Part 2

Written by Larry Salomon Jr.

0 to 60 MPH in 1 Paragraph
Last issue we discussed the functional and design considerations for the development of a loose-leaf paper control. Decided was the control that the application programmer should be able to exert on our window class through messages and the notifications that should be sent for different types of actions. Also, it was determined that window words would be needed to store instance data for each window of this class.

Is There Anybody Out There?
Aside from defining the interfaces to the users of this control, we can sit in our room with the door shut and - assuming someone slips a slice of pizza or two under the door every now and then - eventually we will have something useable. So where do we begin? Even though entering text is the primary function of this class, we have to know where to draw the text, so painting the control will be our first component to implement.

Painting
Painting is easier to implement if you can break it down into distinct sections; since we defined various components of the paper control last issue, we can use those as a starting point. -+-- Border +-- Top margin ++- Side margin ||--- Top, middle, and bottom holes +-- Paper body +--- One or more text lines ''Figure 1. Different parts of the paper control''

Above all it most be noted that we cannot make any assumptions about the size of the control, unless we force the control to follow sizing constraints (we will not). Our options, therefore, are to paint the control ignoring the size or to use an abstract coordinate system based on the size when painting begins. Obviously, the latter is the more desirable so this will be implemented.

The breakdown of the painting as I chose it is as follows:
 * Paint the border, if it exists
 * Paint the lines, both horizontal and vertical
 * Paint the holes
 * Paint the title text
 * Paint the body text

Paint the border
Since some developers might not want a border, a paper style was added - PPS_BORDER - to allow them to control this. PM defines its window styles (that encompass all windows) to be in the upper word of a ULONG, so PPS_BORDER is defined to be x'0001'. hpsPaint=WinBeginPaint(hwndWnd,NULLHANDLE,&rclPaint); WinFillRect(hpsPaint,&rclPaint,CLR_WHITE); WinQueryWindowRect(hwndWnd,&rclWnd); //--   // Paint the border first //   //               ++    //              +|---+|    //              ||           ||    //              ||           ||    //              ||           ||    //   White ---> ||           || <--- Dark gray //             ||           ||    //              |++    //              ++    //--    if ((ulStyle & PPS_BORDER)!=0) { GpiSetColor(hpsPaint,CLR_WHITE); ptlPoint.x=rclWnd.xLeft+1; ptlPoint.y=rclWnd.yBottom; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; ptlPoint.y=rclWnd.yTop-1; GpiBox(hpsPaint,DRO_OUTLINE,&ptlPoint,0,0); GpiSetColor(hpsPaint,CLR_DARKGRAY); ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclWnd.yBottom+1; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight-1; ptlPoint.y=rclWnd.yTop; GpiBox(hpsPaint,DRO_OUTLINE,&ptlPoint,0,0); WinInflateRect(ppidData->habAnchor,                     &rclWnd,-2,-2); } /* endif */

Paint the lines
I don't know why, but it just seemed better sense to paint the vertical line first, which was pink, if memory serves me correctly. This is followed by the horizontal lines, which were of a cyan tint. Here, we added three new styles - holes left (PPS_HOLESLEFT x'0000'), holes right (PPS_HOLESRIGHT x'0002'), and no holes (PPS_HOLESNONE x'0004') - which we check when drawing the vertical line. //--   // Paint the vertical line. Check the window style // to see what side it is on. //--   if ((ulStyle & PPS_HOLESNONE)!=0) { ptlPoint.x=rclWnd.xLeft+ppidData->fmFont.lAveCharWidth*5; } else if ((ulStyle & PPS_HOLESRIGHT)!=0) { ptlPoint.x=rclWnd.xRight-ppidData->fmFont.lAveCharWidth*5; } else { ptlPoint.x=rclWnd.xLeft+ppidData->fmFont.lAveCharWidth*5; } /* endif */ ptlPoint.y=rclWnd.yBottom; GpiMove(hpsPaint,&ptlPoint); ptlPoint.y=rclWnd.yTop; GpiSetColor(hpsPaint,CLR_PINK); GpiLine(hpsPaint,&ptlPoint); //--   // Paint the horizontal lines. Our strategy is to query each // line rectangle, and draw a line along the top edge of this // rectangle. This means the bottom edge of the bottom line // will not get painted, so explicitly handle this case. //--   GpiSetColor(hpsPaint,CLR_DARKCYAN); for (sIndex=0; sIndexsMaxLines; sIndex++) { WinSendMsg(hwndWnd,                 PPM_QUERYLINERECT,                  MPFROMP(&rclLine),                  MPFROMSHORT(sIndex)); ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclLine.yTop; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; GpiLine(hpsPaint,&ptlPoint); } /* endfor */ ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclLine.yBottom-1; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; GpiLine(hpsPaint,&ptlPoint);

Paint the holes
In the future, we might want to support four- and seven-hole paper, so we have to query the number of holes. We then loop, drawing each hole, until done. //--   // Note that if PPS_HOLESNONE was specified, // 0 is returned //--   usMaxHoles=SHORT1FROMMR(WinSendMsg(hwndWnd, PPM_QUERYNUMHOLES,0,0)); for (sIndex=0; sIndexfmFont.lAveCharWidth,0)); GpiSetColor(hpsPaint,CLR_DARKGRAY); GpiFullArc(hpsPaint,                 DRO_OUTLINE,                  MAKEFIXED(ppidData->fmFont.lAveCharWidth,0)); } /* endfor */

Paint the title text
If there is any title text (set on the WinCreateWindow call or via WinSetWindowText), we draw it here. if (ppidData->pchTitle!=NULL) { WinSendMsg(hwndWnd,                 PPM_QUERYLINERECT,                  MPFROMP(&rclLine),                  MPFROMSHORT(0)); rclLine.yBottom=rclLine.yTop+1; rclLine.yTop=rclLine.yBottom+ ppidData->fmFont.lMaxBaselineExt; WinDrawText(hpsPaint,                  -1,                   ppidData->pchTitle,                   &rclLine,                   ppidData->lForeClr,                   0,                   DT_CENTER); } /* endif */

Paint the body text
Finally, we paint the body text, using the PPM_CONVERTPOINTS message to convert the invalid rectangle from world to line coordinates. While this is the only optimization of the paint process, we could easily extend this to the other components of the control. WinSendMsg(hwndWnd,              PPM_CONVERTPOINTS,               MPFROMP(&rclPaint),               MPFROMSHORT(2)); if (rclPaint.yTop<0) { rclPaint.yTop=0; } /* endif */ if ((rclPaint.yBottom>ppidData->sMaxLines-1) ||       (rclPaint.yBottom<0)) { rclPaint.yBottom=ppidData->sMaxLines-1; } /* endif */ while (rclPaint.yTop<=rclPaint.yBottom) { WinSendMsg(hwndWnd,                 PPM_QUERYLINERECT,                  MPFROMP(&rclLine),                  MPFROMSHORT(rclPaint.yTop)); if (strlen(ppidData->aachLines[rclPaint.yTop])>0) { WinDrawText(hpsPaint,                     -1,                      ppidData->aachLines[rclPaint.yTop],                      &rclLine,                      ppidData->lBackClr,                      0,                      DT_LEFT|DT_BOTTOM); } /* endif */ rclPaint.yTop++; } /* endwhile */ WinEndPaint(hpsPaint);

User Input
By definition, a window can only receive input when it has the input focus. Well, this isn't entirely true, since a window receives mouse movement messages regardless of whom has the focus, with the exception of mouse capture, only if...All kidding aside, character keystrokes go to the window with the input focus and the system notifies a window when it receives and loses the focus, so we use this to implement keystroke processing. Our 1000-foot view shows an entryfield that belongs to us, created without ES_BORDER, which we position over the line in which keystrokes are entered. We let the entryfield handle the keyboard interface and we need only to initialize it with any text currently on the line, and query it when the line is to change.

First, when can the line number be changed? The answer is ours to define. I chose to allow first button clicks to set an absolute line and the up and down arrows to change the line by one in either direction.

Second, what happens when the line number changes? Derived from our 1000-foot view, we query the entryfield text and update our internal array of line text, determine the world coordinates of the new line, call WinSetWindowPos to change the position of the entryfield to reflect this new position, and finally initialize the entryfield via WinSetWindowText with the text of the new line as stored in our internal array.

These two questions translate to the messages WM_BUTTON1DOWN and WM_CHAR. case WM_BUTTON1DOWN: {         RECTL rclWnd; //         // Before we change the line, update the text // array from the current line //         if (ppidData->sLine>-1) { WinQueryWindowText(ppidData->hwndText,             sizeof(ppidData->aachLines[ppidData->sLine]),                    ppidData->aachLines[ppidData->sLine]); } /* endif */ //         // Query the line clicked //         rclWnd.xLeft=SHORT1FROMMP(mpParm1); rclWnd.yBottom=SHORT2FROMMP(mpParm1); WinSendMsg(hwndWnd,                    PPM_CONVERTPOINTS,                     MPFROMP(&rclWnd.xLeft),                     MPFROMSHORT(1)); //         // If the place clicked on is one of the lines, // set the new entryfield position to that line //         if (rclWnd.yBottom>-1) { WinSendMsg(hwndWnd,PPM_SETCURRENTLINE,                       MPFROMP(rclWnd.yBottom),0); WinShowWindow(ppidData->hwndText,TRUE); WinSetWindowText(ppidData->hwndText,                             ppidData->aachLines[ppidData->sLine]); } else { ppidData->sLine=-1; WinShowWindow(ppidData->hwndText,FALSE); } /* endif */ WinSetFocus(HWND_DESKTOP,hwndWnd); }      break; case WM_CHAR: if ((CHARMSG(&ulMsg)->fs & (KC_VIRTUALKEY | KC_KEYUP))==KC_VIRTUALKEY) { switch (CHARMSG(&ulMsg)->vkey) { case VK_UP: {               RECTL rclLine; //--               // Remember, we can only go up if there // is another line above us               //-- if (ppidData->sLine>0) { WinQueryWindowText(ppidData->hwndText,          sizeof(ppidData->aachLines[ppidData->sLine]),                         ppidData->aachLines[ppidData->sLine]); ppidData->sLine--; WinSendMsg(hwndWnd,                             PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(ppidData->sLine)); WinSetWindowPos(ppidData->hwndText,                                  NULLHANDLE,                                   rclLine.xLeft,                                   rclLine.yBottom,                                   0,                                   0,                                   SWP_MOVE); WinSetWindowText(ppidData->hwndText,                           ppidData->aachLines[ppidData->sLine]); //---                  // We only invalidate the line we left // because the entryfield paints itself //---                  WinSendMsg(hwndWnd,                              PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(ppidData->sLine+1)); WinInvalidateRect(hwndWnd,&rclLine,TRUE); WinUpdateWindow(hwndWnd); WinSendMsg(ppidData->hwndOwner,WM_CONTROL,          MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_UP),MPFROMSHORT(ppidData->sLine)); } else if (ppidData->sLine==0) { WinSendMsg(ppidData->hwndOwner,WM_CONTROL,                      MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_BEGINPAGE),MPFROMSHORT(ppidData->sLine)); } /* endif */ }            break; //         // We treat newline and enter the same as down arrow //         case VK_DOWN: case VK_NEWLINE: case VK_ENTER: {               RECTL rclLine; //--               // Remember, we can only go down if there // is another line below us               //-- if ((ppidData->sLine>-1) &&                   (ppidData->sLinesMaxLines-1)) { WinQueryWindowText(ppidData->hwndText,      sizeof(ppidData->aachLines[ppidData->sLine]),                         ppidData->aachLines[ppidData->sLine]); ppidData->sLine++; WinSendMsg(hwndWnd,                             PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(ppidData->sLine)); WinSetWindowPos(ppidData->hwndText,                                  NULLHANDLE,                                   rclLine.xLeft,                                   rclLine.yBottom,                                   0,                                   0,                                   SWP_MOVE); WinSetWindowText(ppidData->hwndText,                           ppidData->aachLines[ppidData->sLine]); //---                  // We only invalidate the line we left // because the entryfield paints itself //---                  WinSendMsg(hwndWnd,                              PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(ppidData->sLine-1)); WinInvalidateRect(hwndWnd,&rclLine,TRUE); WinUpdateWindow(hwndWnd); WinSendMsg(ppidData->hwndOwner,WM_CONTROL,         MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_DOWN),MPFROMSHORT(ppidData->sLine)); } else if (ppidData->sLine==ppidData->sMaxLines-1) { WinSendMsg(ppidData->hwndOwner,WM_CONTROL,         MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_ENDPAGE),MPFROMSHORT(ppidData->sLine)); } /* endif */ }            break; default: return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endswitch */ } else { return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endif */ break; Notice the notifications PPN_UP, PPN_DOWN, PPN_BEGINPAGE and PPN_ENDPAGE.

Time Out
You will not notice this now, but if you click on the paper control, the line number is supposed to change. Why do we not get a flashing cursor where the "invisible" entryfield exists? The answer is in the way the system handles focus changing.

When the input focus window changes, a number of messages are sent as a result to both the window losing the focus and to the window receiving the focus. While this messages are being processed, the system considers no one to have the focus, so any attempt to change the focus via WinSetFocus or WinFocusChange will have no effect because the system will "overwrite" the focus change as it completes its processing.

The result of this gibberish is that, if we are clicked on, we want the entryfield to receive the focus, but since we need to do some processing, we cannot just call WinSetFocus(HWND_DESKTOP,ppidData->hwndText) since we will never receive any notification.

The result of that gibberish is that we need to get the focus and then somehow pass the focus on to the entryfield. Since we cannot change the focus while the system changes the focus, we need a little hocus-pocus to achieve this. #define PRVM_SETFOCUS              (WM_USER) case WM_SETFOCUS: WinPostMsg(hwndWnd,PRVM_SETFOCUS,mpParm1,mpParm2); break; case PRVM_SETFOCUS: if (SHORT1FROMMP(mpParm2)) { if (ppidData->sLine>-1) { WinShowWindow(ppidData->hwndText,TRUE); WinFocusChange(HWND_DESKTOP,ppidData->hwndText,         FC_NOLOSEACTIVE|FC_NOLOSEFOCUS|FC_NOLOSESELECTION); } /* endif */ WinSendMsg(ppidData->hwndOwner,                WM_CONTROL,                 MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_SETFOCUS),                0); } else { //         // If we're losing the focus, update the text // array, but leave the entryfield text alone. //         if (ppidData->sLine>-1) { WinQueryWindowText(ppidData->hwndText,          sizeof(ppidData->aachLines[ppidData->sLine]),                 ppidData->aachLines[ppidData->sLine]); WinShowWindow(ppidData->hwndText,FALSE); } /* endif */ WinSendMsg(ppidData->hwndOwner,              WM_CONTROL,               MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_KILLFOCUS),              0); } /* endif */ break; In the code above, a couple of things need to be noted:


 * The hocus-pocus is in the call to WinPostMsg. While it is not guaranteed, it is highly likely that our message will get processed after the system finished the focus change. This will allow us to do any processing necessary and then change the focus again to the entryfield.
 * The focus change added two new notifications - PPN_SETFOCUS and PPN_KILLFOCUS. These have the same semantics as the corresponding entryfield notifications EN_SETFOCUS and EN_KILLFOCUS.
 * The call to WinFocusChange specifies through the use of the FC_* constants that the window with the focus (that's us) should not receive any notification that it is losing the focus. This is needed so that we don't send the PPN_KILLFOCUS notification.

Owner Notifications
Last issue, we defined a list of four events that should result in a notification to the owner window. The system allows us to define any notification code as a USHORT that we feel is necessary, since there is no "standard list" of notifications present for all window classes (versus window styles, where there is a common set of styles).

The cursor moves up or down
This is implemented in the processing for WM_CHAR. The code consists of the following call to WinSendMsg. WinSendMsg(ppidData->hwndOwner,      WM_CONTROL,       MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID),PPN_UP),       MPFROMSHORT(ppidData->sLine)); Substitute PPN_DOWN for PPN_UP as needed. Note that if the cursor is on line 1/ppidData->sMaxLines and the user presses the up/down arrow, we instead send a PPN_BEGINPAGE/PPN_ENDPAGE notification to let the application know that the page boundary was reached.

Any mouse button is clicked or double-clicked
This is implemented in the processing for the various WM_BUTTONxUP and WM_BUTTONxDBLCLK messages. The code is the same as for the PPN_UP/PPN_DOWN notifications with the notification code changed as appropriate. Note that we could use the second half of mpParm2 to include which button was pressed, as a convenience to the user.

The system-defined sequence for displaying the context menu is pressed

Again, we use the same call to WinSendMsg, this time from the WM_CONTEXTMENU message.

Help is requested

And finally one more WinSendMsg from the WM_HELP message.

That was easy, wasn't it?

Presentation Parameters
Processing these is probably the most interesting, because...well, because the ability for the user to change the color and font of a window was new with OS/2 2.0. [Get on soapbox] "I remember the days when you didn't have to deal with these dad-burned paramitization presentations or whatever the hell they're called."[Get off soapbox]. Because the user has the ability, via the color and font palettes, to change your presentation parameters, you can no longer avoid them when developing a new window class.

Changing a presentation parameter is done for you by the system, providing you use a micro-cache'd presentation space in all of your drawing operations (obtained through WinBeginPaint with NULLHANDLE specified as the second parameter, or through WinGetPS). If you have your own screen presentation space which you specify in the call to WinBeginPaint, you will need to intercept the WM_PRESPARAMCHANGED message. Another time this message is needed is if you need the size of the current font for other processing or will unconditionally set the color to some color but want to restore it for other operations.

Gee, aren't those familiar operations?

We intercept this message and if a color was changed, we query the new color value and store that in our instance data. If the font changed, we re-query the FONTMETRICS structure values so that we can base our line size on the height of the font. case WM_PRESPARAMCHANGED: switch (LONGFROMMP(mpParm1)) { case PP_FOREGROUNDCOLOR: case PP_FOREGROUNDCOLORINDEX: {            ULONG ulId; LONG lColor; HPS hpsWnd; SHORT sIndex; RECTL rclLine; WinQueryPresParam(hwndWnd,                              PP_FOREGROUNDCOLORINDEX,                               LONGFROMMP(mpParm1),                               &ulId,                               sizeof(lColor),                               &lColor,                               QPF_NOINHERIT); if (ulId==PP_FOREGROUNDCOLOR) { hpsWnd=WinGetPS(hwndWnd); lColor=GpiQueryColorIndex(hpsWnd,0,lColor); WinReleasePS(hpsWnd); } /* endif */ ppidData->lForeClr=lColor; for (sIndex=0; sIndex                                 sMaxLines; sIndex++) { if (ppidData->aachLines[sIndex][0]!=0) { WinSendMsg(hwndWnd,                             PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(sIndex)); WinInvalidateRect(hwndWnd,NULL,TRUE); } /* endif */ } /* endfor */ WinUpdateWindow(hwndWnd); }         break; case PP_BACKGROUNDCOLOR: case PP_BACKGROUNDCOLORINDEX: {            ULONG ulId; LONG lColor; HPS hpsWnd; SHORT sIndex; RECTL rclLine; WinQueryPresParam(hwndWnd,                              PP_BACKGROUNDCOLORINDEX,                               LONGFROMMP(mpParm1),                               &ulId,                               sizeof(lColor),                               &lColor,                               QPF_NOINHERIT); if (ulId==PP_BACKGROUNDCOLOR) { hpsWnd=WinGetPS(hwndWnd); lColor=GpiQueryColorIndex(hpsWnd,0,lColor); WinReleasePS(hpsWnd); } /* endif */ ppidData->lBackClr=lColor; for (sIndex=0; sIndex                                 sMaxLines; sIndex++) { if (ppidData->aachLines[sIndex][0]!=0) { WinSendMsg(hwndWnd,                             PPM_QUERYLINERECT,                              MPFROMP(&rclLine),                              MPFROMSHORT(sIndex)); WinInvalidateRect(hwndWnd,NULL,TRUE); } /* endif */ } /* endfor */ WinUpdateWindow(hwndWnd); }         break; case PP_FONTNAMESIZE: case PP_FONTHANDLE: {            HPS hpsWnd; RECTL rclWnd; hpsWnd=WinGetPS(hwndWnd); GpiQueryFontMetrics(hpsWnd,sizeof(FONTMETRICS),                                &ppidData->fmFont); WinReleasePS(hpsWnd); WinQueryWindowRect(hwndWnd,&rclWnd); WinSendMsg(hwndWnd,                       WM_SIZE,                        0,                        MPFROM2SHORT((SHORT)rclWnd.xRight, (SHORT)rclWnd.yTop)); WinInvalidateRect(hwndWnd,NULL,TRUE); WinUpdateWindow(hwndWnd); }         break; default: return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endswitch */ break; Again, there are other (better) ways of implementing things. Here, we could query the RGB value of the changed color and use that in our painting to get the most accuracy. This exercise is left up to the programmer.

While it doesn't really belong here, the window text processing has nowhere else to go so it is discussed here. The system's interface to a window class for items like this is through the WM_SETWINDOWPARAMS and WM_QUERYWINDOWPARAMS messages. The former is sent when a window parameter changes and the latter is sent to query the data from the window. A window parameter is either the window text or the control data (which is specific to a window class) and both are bundled in the WNDPARAMS structure. typedef struct _WNDPARAMS { ULONG fsStatus; ULONG cchText; PSZ pszText; ULONG cbPresParams; PVOID pPresParams; ULONG cbCtlData; PVOID pCtlData; } WNDPARAMS, *PWNDPARAMS;
 * fsStatus:For queries by the system, this specifies (as a combination of WPM_* constants) the parameters to query.
 * cchText:This specifies the size of the data pointed to by pszText
 * pszText:This points to the window text, terminated by a NULL character
 * cbPresParams:This specifies the size of the data pointed to by pPresParams
 * pPresParams:This points to the presentation parameters
 * cbCtlData:This specifies the size of the data pointed to by pCtlData
 * pCtlData:This points to the control data

Since we are interested in the text only, we check the fsStatus field for the WPM_TEXT constant explicitly and act accordingly. The resulting code is shown below: case WM_QUERYWINDOWPARAMS: {         PWNDPARAMS pwpParms; pwpParms=(PWNDPARAMS)PVOIDFROMMP(mpParm1); if ((pwpParms->fsStatus & WPM_TEXT)!=0) { pwpParms->pszText[0]=0; strncat(pwpParms->pszText,                    ppidData->pchTitle,pwpParms->cchText); WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); return MRFROMSHORT(TRUE); } /* endif */ return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); }   case WM_SETWINDOWPARAMS: {         BOOL bProcessed; PWNDPARAMS pwpParms; bProcessed=FALSE; pwpParms=(PWNDPARAMS)PVOIDFROMMP(mpParm1); if ((pwpParms->fsStatus & WPM_TEXT)!=0) { if (ppidData->pchTitle!=NULL) { free(ppidData->pchTitle); ppidData->pchTitle=NULL; } /* endif */ if ((pwpParms->pszText!=NULL) &&                (strlen(pwpParms->pszText)>0)) {               ppidData->pchTitle=malloc(strlen(pwpParms->pszText)+1); strcpy(ppidData->pchTitle,pwpParms->pszText); } /* endif */ bProcessed=TRUE; } /* endif */ if (bProcessed) { WinInvalidateRect(hwndWnd,NULL,TRUE); WinUpdateWindow(hwndWnd); WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); return MRFROMSHORT(TRUE); } else { return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endif */ }

Summary
Although a lot of code was presented here, all of it is really only the extension of our thoughts from last issue when this new class was being designed. It cannot be stressed enough the importance of a good design; no one expects you to get it right the first time, but if you think most of it through the rest becomes significantly easier.

As a final note, all coordinates that we discussed here were (if not already) mapped to line numbers via the message PPM_CONVERTPOINTS. This allows us to change the size of each line in one place only. This logic was applied in other places (PPM_QUERYNUMHOLES) to make our maintenance easier and goes way back to Computer Science 101 when code modularity was discussed.