Introduction to PM Programming - Dec 1995

From EDM2
Jump to: navigation, search

Written by Larry Salomon Jr.

Introduction

The purpose of this column is to provide the readers out there who are not familiar with PM application development the information necessary to satisfy their curiosity, educate themselves, and give them an advantage over the documentation supplied by IBM. Of course, much of this stuff could probably be found in one of the many books out there, but the problem with books in general is that they don't answer the questions you have after you read the book the first time through.

I will gladly entertain feedback from the readers about what was "glossed over" or what was detailed well, what tangential topics need to be covered and what superfluous crap should have been removed. This feedback is essential in guaranteeing that you get what you pay for. [grin]

It should be said that you must not depend solely on this column to teach you how to develop PM applications; instead, this should be viewed as a supplement to your other information storehouses (books, the network conferences, etc.). Because this column must take a general approach, there will be some topics that you would like to see discussed that really do not belong here. Specific questions can be directed to me via email and I will do my best to answer them in a timely fashion.

This Month

This month, we will look at a sample application which allow you to use the scrollbars to "manage" an amount of data than the window will display if sized small enough.

The Application

The application, shown below, is a simple one. It is aware of 30 lines of text, where each line is 96 characters wide.

Intropm1dec1995.gif

Figure 1: This month's sample application.

If the window is not large enough to display all 30 lines at 96 characters per line, scrollbars are used to allow the user to change the viewport position within the virtual document.

We will take a look at two versions of this application: one will show us how the scrollbars are used within an application, the other will show us how the scrollbars are used effectively within an application. The main difference between the two versions is in how scrolling and painting are accomplished. But, I'm getting ahead of myself...

A Code Overview

As I looked over last month's column, I realized that it read like a rehashing of the PM Reference. After getting over my initial shock, it occured to me that we are getting to be "adults" as programmers and that the information would still be useful to you. However, more useful is a real application to dissect and scrutinize, so I wrote this.

As before, we will look at various aspects of the code based on landmarks. These are places marked by a special comment, which begins with a "@" character. Thus, to find landmark 11, you would search for "// @11". Since I have two versions, the first digit of the landmark specifies which version to look at. Thus, landmark 11 is really landmark 1 in version 1. We will, however, refer to this as landmark 11.

With that, let me now explain how version 1 works in a nutshell before we look at specific landmarks. Our application is created using the same main() as always, with one exception: while we could have created the scrollbars ourselves, it is much easier to let the system do it for us. Fortunately, there are two frame control flags which will do this: FCF_HORZSCROLL and FCF_VERTSCROLL. Specifying these flags creates two scrollbars which are children of the frame, with the identifiers FID_HORZSCROLL and FID_VERTSCROLL.

As we saw in the last issue, whenever the scrollbars are used, we receive a WM_HSCROLL or WM_VSCROLL message, depending on the style of the scrollbar. We process this in a fairly standard fashion, which we will see shortly.

Finally, the other portion of the code in which we are interested is the painting of the window. This can get complex in its explanation, so I will defer such until we look at the code itself.

The Code Itself, Take 1

The first thing we are going to look at is the instance data, which is the only thing without a landmark. It is at the top of the source file, INTRO1.C.

//-------------------------------------------------------------------------
// Instance data structure
//-------------------------------------------------------------------------
typedef struct {
   ULONG ulSzStruct;     // Size of the structure
   HAB habAnchor;        // Anchor block handle
   HWND hwndFrame;       // Frame window handle
   HWND hwndHorz;        // Horizontal scrollbar handle
   HWND hwndVert;        // Vertical scrollbar handle
   LONG lHorzPos;        // Position of the horizontal scrollbar
   LONG lHorzMin;        // Minimum value of the horizontal scrollbar
   LONG lHorzMax;        // Maximum value of the horizontal scrollbar
   LONG lVertPos;        // Position of the vertical scrollbar
   LONG lVertMin;        // Minimum value of the vertical scrollbar
   LONG lVertMax;        // Maximum value of the vertical scrollbar
   ULONG ulWidth;        // Width in characters
   ULONG ulHeight;       // Height in characters
   ULONG ulCxChar;       // Width of a character
   ULONG ulCyChar;       // Height of a character
} INSTDATA, *PINSTDATA;

Figure 2: Instance data definition.

I have nothing to say about this, but you need to know these fields, which we will use heavily elsewhere.

Moving along, we see the WM_CREATE and WM_DESTROY processing, which handles the allocation/deallocation and initialization/termination of the instance data. Find landmark 11.

//----------------------------------------------------------------
// @11
//
// Set the font to be a fixed pitch font before we determine the
// character width and height.
//----------------------------------------------------------------
strcpy(achFont,"10.System Monospaced");
WinSetPresParam(hwndWnd,PP_FONTNAMESIZE,strlen(achFont)+1,achFont);

hpsWnd=WinGetPS(hwndWnd);
GpiQueryFontMetrics(hpsWnd,sizeof(fmFont),&fmFont);
WinReleasePS(hpsWnd);

pidData->ulCxChar=fmFont.lAveCharWidth;
pidData->ulCyChar=fmFont.lMaxBaselineExt;

Figure 3: Initialization of the instance data.

This is specific to the application, but notice that we are specifying a fixed-pitch font. This is one, for those of you who do not know, where all charactes are the same width. We need to do this so that we may properly calculate the virtual width of the window. Immediately following this, we query the attributes of the font using the GpiQueryFontMetrics() API.

(BOOL)GpiQueryFontMetrics(HPS hpsWnd,
          ULONG ulSzdata,
          PFONTMETRICS pfmMetrics);

Figure 4: Determining the attributes of the current font.

The topic of fonts in general is beyond the scope of this discussion, but suffice it to say that this function returns a wealth of information about the font currently used in the HPS specified. The two fields that we are interested in are the lAveCharWidth and lMaxBaselineExt. The former returns the weighted average of the characters in the font. (The character weights are based on their frequency of occuring within text.) Since we are using a fixed-pitch font, this is also equal to the width of any character. The latter field returns the total height of the largest character. We will use these two values to define a "character box" - note that PM has this concept also but it is defined differently. We will always use our definition.

At landmark 12, we see the WM_SIZE message:

//-------------------------------------------------------------------
// @12
//
// Recalculate the size-dependent fields of the instance data.
//-------------------------------------------------------------------
pidData->ulWidth=SHORT1FROMMP(mpParm2)/pidData->ulCxChar;
pidData->ulHeight=SHORT2FROMMP(mpParm2)/pidData->ulCyChar;

//-------------------------------------------------------------------
// Remember, the scrollbars allow you to see what is _not_ visible,
// so subtract out the visible portion.
//-------------------------------------------------------------------
pidData->lHorzMax=MAX_WIDTH-pidData->ulWidth;
if (pidData->lHorzMax<0) {
   pidData->lHorzMax=0;
} /* endif */

pidData->lVertMax=MAX_HEIGHT-pidData->ulHeight;
if (pidData->lVertMax<0) {
   pidData->lVertMax=0;
} /* endif */

Figure 5: Handling the WM_SIZE message.

WM_SIZE sends four parameters along: the previous width and height of the window that has just been sized, and the current width and height. These can be extracted with SHORT1FROMMP() for the width and SHORT2FROMMP() for the height and using mpParm1 for the previous values and mpParm2 for the current values. We divide the current values by the character width and height to get the width and height of the window in characters. We then calculate the amount that is not visible by subtracting out the visible portion from the virtual document. This is used in updating the scrollbar ranges...

Which is seen at landmark 13. The code, not shown here, sends two messages to each scrollbar to update both the range and the size of the thumb (for user feedback).

Moving along to landmark 14, notice how the programmer defines what a line and a page is. We define a line to be 1 "unit" and a page to be 5 "units", but we could just as easily have defined them to be something else. Switch the two, recompile, and see what happens! It isn't as intuitive, eh?

//----------------------------------------------------------------
// @14
//
// We are the ones to define what a "line" and a "page" is.
//----------------------------------------------------------------
switch (SHORT2FROMMP(mpParm2)) {
case SB_LINELEFT:
   lDx=-1;
   break;
case SB_PAGELEFT:
   lDx=-5;
   break;
case SB_LINERIGHT:
   lDx=1;
   break;
case SB_PAGERIGHT:
   lDx=5;
   break;
case SB_SLIDERPOSITION:
   //-------------------------------------------------------------
   // Determine what the delta should be to get from where we
   // were to where the message says we are now.
   //-------------------------------------------------------------
   lDx=SHORT1FROMMP(mpParm2)-pidData->lHorzPos;
   break;
default:
   return MRFROMSHORT(FALSE);
} /* endswitch */

Figure 6: Defining a "line" and a "page".

Landmark 15 is significant because you will see that while you can imagine that easier to code means less efficient, here it means easier means much less efficient. It's ugly too. Try scrolling by a line. The entire window is repainted! This is not necessary and will be addressed in version 2 of the code.

//----------------------------------------------------------------
// @15
//
// Invalidate and repaint.  _Extremely_ inefficient!
//----------------------------------------------------------------
WinInvalidateRect(hwndWnd,NULL,FALSE);
WinUpdateWindow(hwndWnd);

Figure 7: Inefficient repainting after processing a scroll message.

In the painting section of the code we find landmark 16. Here, we calculate the string to be written out, based on the position of the horizontal scrollbar. Nothing too complicated, but it shows where we use the position of the horizontal scrollbar.

//----------------------------------------------------------------
// @16
//
// Determine the string that we're going to write out.  Don't
// forget to take the horizontal scrollbar position into account!
//----------------------------------------------------------------
lFirst=pidData->lHorzPos;
lLast=MAX_WIDTH;

if (lLast-lFirst+1>pidData->ulWidth) {
   lLast=lFirst+pidData->ulWidth;
} /* endif */

memset(achText,0,sizeof(achText));

for (lIndex=lFirst; lIndex<lLast; lIndex++) {
   achText[lIndex-lFirst]=32+lIndex;
} /* endfor */

Figure 8: Calculating the string to be drawn.

Finally, at landmark 17, we calculate the starting and ending lines to be drawn and then loop to actually perform the drawing. This, too, is inefficient by a long way.

//----------------------------------------------------------------
// @17
//
// Determine the start and end lines to draw, initialize the
// starting point, and loop-to-draw.
//----------------------------------------------------------------
lFirst=pidData->lVertPos;
lLast=MAX_HEIGHT;

if (lLast-lFirst+1>pidData->ulHeight) {
   lLast=lFirst+pidData->ulHeight;
} /* endif */

WinQueryWindowRect(hwndWnd,&rclWnd);

ptlPoint.x=0;
ptlPoint.y=rclWnd.yTop-1-pidData->ulCyChar;

for (lIndex=lFirst; lIndex<lLast; lIndex++) {
   //-------------------------------------------------------------
   // Overwrite the first four characters with the line number,
   // so that we know we're doing things properly.
   //-------------------------------------------------------------
   sprintf(achLine,"%2d ",lIndex+1);
   strncpy(achText,achLine,strlen(achLine));

   GpiCharStringAt(hpsPaint,&ptlPoint,strlen(achText),achText);
   ptlPoint.y-=pidData->ulCyChar;
} /* endfor */

Figure 9: The actual drawing of the window viewport.

The Code Itself, Take 2

When you consider that the display device is extremely slow compared to the CPU, especially with the indirection added to allow output device independence (i.e. the same commands may be used on both the display and a printer or plotter), it is very true that the painting code is going to be your bottleneck. Thus, it is an important responsibility for you as the programmer to determine where the line between efficient painting and maintainable code is. You want - need, even - your painting to be fast, but how much of your code's readability are you willing to sacrifice to achieve this?

If you haven't played with INTRO1, you should do so before continuing in order to fully appreciate what we're about to do. The changes we make are to achieve one goal: faster painting. How we do that will not be difficult, but we will encounter a few crossroads and a few difficulties along the way.

Since the instance data hasn't changed, we don't need to look at it. Instead, skip right to landmark 21, where we see a subtle change.

case SB_SLIDERTRACK:
   //-------------------------------------------------------------
   // @21
   //
   // Determine what the delta should be to get from where we
   // were to where the message says we are now.
   //-------------------------------------------------------------
   lDx=SHORT1FROMMP(mpParm2)-pidData->lHorzPos;
   break;

Figure 10: Dynamic updating of the display, using SB_SLIDERPOSITION.

If you look back at landmark 14, you will see that we process the SB_SLIDERPOSITION command, while here we are instead processing the SB_SLIDERTRACK command (in the same way, even!). To see the effects of this, start both INTRO1 and INTRO2 and scroll the window by dragging the thumb. Do you see the difference? My disclaimer here is that using SB_SLIDERTRACK might create difficulties in your painting code, so don't use the latter just because it is possible. If there are difficulties in your development, SB_SLIDERPOSITION may help.

At landmark 22, we see another change.

//----------------------------------------------------------------
// @22
//
// Calculate the new position, perform range checking, and -
// if the new position is different from the old - tell the
// scrollbar that it should use the new position.
//----------------------------------------------------------------
lNew=pidData->lHorzPos+lDx;

if (lNew<pidData->lHorzMin) {
   lNew=pidData->lHorzMin;
} /* endif */

if (lNew>pidData->lHorzMax) {
   lNew=pidData->lHorzMax;
} /* endif */

if (lNew==pidData->lHorzPos) {
   return MRFROMSHORT(FALSE);
} /* endif */

pidData->lHorzPos=lNew;

WinSendMsg(pidData->hwndHorz,
       SBM_SETPOS,
       MPFROMSHORT(pidData->lHorzPos),
       0);

Figure 11: Using local variables as a workaround to a PM bug.

Instead of updating the position field within our instance data directly, we update a local variable and only continue if it is different than the current position. Why do we need to do this? This is due to what I consider to be a bug in the scrollbar code. Look at the debug output below to understand:

Horz Min=0, Max=11, Pos=0, Delta=0, Adjusted Pos=0
Horz Min=0, Max=11, Pos=0, Delta=1, Adjusted Pos=1
Horz Min=0, Max=11, Pos=1, Delta=1, Adjusted Pos=2
Horz Min=0, Max=11, Pos=2, Delta=1, Adjusted Pos=3
Horz Min=0, Max=11, Pos=3, Delta=1, Adjusted Pos=4
Horz Min=0, Max=11, Pos=4, Delta=1, Adjusted Pos=5
Horz Min=0, Max=11, Pos=5, Delta=1, Adjusted Pos=6
Horz Min=0, Max=11, Pos=6, Delta=1, Adjusted Pos=7
Horz Min=0, Max=11, Pos=7, Delta=1, Adjusted Pos=8
Horz Min=0, Max=11, Pos=8, Delta=1, Adjusted Pos=9
Horz Min=0, Max=11, Pos=9, Delta=1, Adjusted Pos=10
Horz Min=0, Max=11, Pos=10, Delta=1, Adjusted Pos=11
Horz Min=0, Max=11, Pos=11, Delta=1, Adjusted Pos=11

Figure 12: Debug output illustrating the problem.

I was experiencing painting problems where an extra column of either end of the virtual window would get drawn if I held down the left or right arrow of the horizontal scrollbar. I was puzzled, so I put in some debug statements which generated the output above.

When the adjusted position is equal to the maximum value in the range, the scrollbar should stop scrolling, but it goes one more time. This causes painting problems because we now scroll the window based on the delta (lDx and lDy in WM_HSCROLL and WM_VSCROLL, respectively) and these need to be coded around. The workaround is to calculate the new position in a local variable and continue only if it is different than the current position.

Landmark 23 gives us the biggest performance boost.

//----------------------------------------------------------------
// @23
//
// Scroll the window.  Since the scroll will likely include
// "partial" characters, inflate the invalid rectangle by a
// character's width and height, and intersect the new rectangle
// with that of the window.
//----------------------------------------------------------------
WinScrollWindow(hwndWnd,
        -lDx*pidData->ulCxChar,
        0,
        NULL,
        NULL,
        NULLHANDLE,
        &rclUpdate,
        0);

WinInflateRect(pidData->habAnchor,
       &rclUpdate,
       pidData->ulCxChar,
       pidData->ulCyChar);

WinQueryWindowRect(hwndWnd,&rclWnd);

WinIntersectRect(pidData->habAnchor,
         &rclUpdate,
         &rclUpdate,
         &rclWnd);

//----------------------------------------------------------------
// Invalidate the intersection and repaint.
//----------------------------------------------------------------
WinInvalidateRect(hwndWnd,&rclUpdate,FALSE);
WinUpdateWindow(hwndWnd);

Figure 13: Calling WinScrollWindow().

This achieves the performance gains by calling our childhood sweetheart, the WinScrollWindow() function.

(BOOL)WinScrollWindow(HWND hwndWnd,
          LONG lPelsRight,
          LONG lPelsUp,
          PRECTL prclToScroll,
          PRECTL prclToClip,
          HRGN hrInvalid,
          PRECTL prclInvalid,
          ULONG ulFlags);

Figure 14: WinScrollWindow() declaration.

hwndWnd s the window handle to be scrolled. lPelsDown and lPelsUp specify the number of pels to scroll in the positive horizontal and vertical directions. prclToScroll points to the rectangle within the window to scroll. If this is NULL, the entire window is scrolled. prclToClip points to the rectangle to which the scrolling is clipped. If this is NULL, no clipping is performed. hrInvalid specifies the handle of the region which is updated to specify the invalid portion of the window. Regions are another "beyond the scope of this column" topic. prclUpdate points to the rectangle which, upon return, contains the bounding rectangle of the invalid area. Finally, ulFlags specifies one of two flags which can control the behavior of the scrolling: SW_SCROLLCHILDREN, which will not clip the scrolling so that child windows are not scrolled, and SW_INVALIDATERGN, which will automatically add the invalid area to the area to be updated (which is returned by WinBeginPaint() if you remember).

To better understand the effect of prclToScroll and prclToClip, take a look at the figure below:

Intropm2dec1995.gif

Figure 15: The effects of scrolling and clipping rectangles on WinScrollWindow().

In the upper half, we see what happens if we scroll the entire window with no clipping in effect. The first line is invalidated, which we will assume is then painted properly. In the lower half, a clipping rectangle is specified which is indicated by the dashed-line box. Since we are scrolling the entire window, the line above the clipping rectange is scrolled, but only the area within the clipping rectange is actually updated. To be honest, I'm dubious about the usefulness of this, and I'm not even sure what is considered to be the invalid portion in the second example, if there is one.

In our code, we need to perform some post-processing, so we don't want the invalid rectangle added to the area to be updated (yet). Instead, we expand the rectangle by one character's width and height to include "partial characters," clip the expanded rectangle to that of the window (by calculating the intersection of the expanded rectangle and the window rectangle), and then use the newly calculated rectangle as the invalid area. Finally, we tell ourselves to repaint the invalid area.

Wow. That's quite a mouthful.

Let's go to landmark 24.

//----------------------------------------------------------------
// @24
//
// Inflate the invalid rectangle by a character's width and
// height, and intersect the new rectangle with that of the
// window.  The eliminates "partial" characters.  Just because
// we do this in the WM_xSCROLL processing doesn't mean that
// we only get invalidated by that code.  What about window
// overlapping, etc.?
//----------------------------------------------------------------
WinInflateRect(pidData->habAnchor,
       &rclPaint,
       pidData->ulCxChar,
       pidData->ulCyChar);

WinQueryWindowRect(hwndWnd,&rclWnd);

WinIntersectRect(pidData->habAnchor,
         &rclPaint,
         &rclPaint,
         &rclWnd);

rclPaint.xLeft/=pidData->ulCxChar;
rclPaint.xRight/=pidData->ulCxChar;
rclPaint.yBottom/=pidData->ulCyChar;
rclPaint.yTop/=pidData->ulCyChar;

Figure 16: Eliminating partial character painting.

I only point this out to remind you that WM_PAINT messages will be generated by events other than scrolling, so we need to duplicate the expand and intersect processing here. Why didn't we just do it here and eliminate the code in the WM_HSCROLL / WM_VSCROLL messages? The answer is that, when you obtain an HPS via WinBeginPaint() call, many things are done for you. One of these things is that the HPS is clipped to allow painting only in the invalid area. So, in the scroll processing, we might have scrolled a partial character, but we should repaint the entire character. However, removing the code there will result in a clipping region that bisects a character instead of including the entire character.

Ah, the nuances of painting. Just like a fine wine - you never know what to expect.

Finally, we convert the units of the invalid rectangle from pels to characters to save us some calculations later.

At landmark 25, we again determine the string to be written, but we only calculate the characters which will actually be displayed, instead of the entire string.

//----------------------------------------------------------------
// @25
//
// Determine the string that we're going to write out.  Don't
// forget to take the horizontal scrollbar position into account!
//----------------------------------------------------------------
lXFirst=rclPaint.xLeft+pidData->lHorzPos;
lXLast=rclPaint.xRight+pidData->lHorzPos;

if (lXLast>MAX_WIDTH) {
   lXLast=MAX_WIDTH;
} /* endif */

memset(achText,0,sizeof(achText));

for (lIndex=lXFirst; lIndex<lXLast; lIndex++) {
   achText[lIndex-lXFirst]=32+lIndex;
} /* endfor */

Figure 17: Calculating the substring to be drawn.

Finally, at landmark 26, we calculate the starting and ending lines, based on the invalid rectangle.

//----------------------------------------------------------------
// @26
//
// Determine the start and end lines to draw, initialize the
// starting point, and loop-to-draw.
//----------------------------------------------------------------
lYFirst=pidData->lVertPos;
lYLast=MAX_HEIGHT;

if (lYLast-lYFirst+1>pidData->ulHeight) {
   lYLast=lYFirst+pidData->ulHeight;
} /* endif */

ptlPoint.x=rclPaint.xLeft*pidData->ulCxChar;
ptlPoint.y=rclWnd.yTop-1-pidData->ulCyChar;

for (lIndex=lYFirst; lIndex<lYLast; lIndex++) {
   //-------------------------------------------------------------
   // @27
   //
   // We can no longer simply overwrite the first four characters,
   // since WinScrollWindow only invalidates the minimum amount
   // of the window.  If the line numbers get scrolled off of
   // the window, that area isn't included in the invalide region,
   // so any drawing we do will get clipped.  Getting around this
   // in an efficient manner is beyond the scope of this example.
   //-------------------------------------------------------------
   //sprintf(achLine,"%2d ",lIndex+1);
   //strncpy(achText,achLine,strlen(achLine));

   GpiCharStringAt(hpsPaint,&ptlPoint,strlen(achText),achText);
   ptlPoint.y-=pidData->ulCyChar;
} /* endfor */

Figure 18: The new drawing loop.

Notice how we can no longer write the line number out as the first 4 characters of the line, regardless of the scrollbar position. See if you can figure out how to do this using one or both of the prclToScroll and prclToClip parameters of WinScrollWindow().

Errata

Michael Hohner (100425.1754@compuserve.com) pointed out to me, after last month's column, that the SBS_AUTOTRACK style is indeed documented in the latest PM Guide and Reference. So, to the list of styles that I presented, add the following:

SBS_AUTOTRACK - Specifying this indicates that the entire thumb should track the movement of the pointer when the user scrolls the window. If this style is not specified, only an outlined image of the slider tracks the movement of the mouse pointer, and the slider jumps to the new location when the user releases the mouse button.

I immediately ran INTRO2 to see if this were indeed the case, but (on my system, at least) the entire thumb "tracked the movement of the pointer", and I didn't specify this style.

Conclusion

We have seen the scrollbar in action, finally. More importantly, we have also seen how to use it effectively, using WinScrollWindow() and some calculations to implement a viewport into a virtual document. There are some problems that still exist, however; scroll INTRO2 up or down and notice the partial line written at the bottom or top of the window, respectively. How would you fix that? Hopefully, you now have the knowledge to answer this question.

Enjoy, and send me some feedback!