Controlling PM Controls

From EDM2
Jump to: navigation, search

by Alessandro Cantatore

Foreword

More than five years have passed since IBM released Warp 4. Since those days, we haven't seen any improvements in the OS user interface. Besides that, if we consider that Warp 4, apart from some minor cosmetic features, is not so much different from Warp 3 or OS/2 2.x, we can realize that we are still using a pretty old interface.

Although this shows how good the original Presentation Manager design was, I think that, as OS/2 developers, many of us have often felt the lack of some more advanced PM controls, while users have often found that the solutions implemented in various applications lacked any consistency in behaviour and look, when they were not just too buggy.

I'm not just meaning fancy titlebars, transparent windows or shaded push buttons, but the fact that all those modern controls like toolbars, status bars, etc., which are nowadays common on most other operating systems, are on OS/2 still left to the good will and often limited resources of the individual developers, with the above mentioned problems.

On the other side, richer sets of well designed controls, some enhancements of the already existing controls, some new APIs and standard dialogs, seamlessly integrated with the Presentation Manager layer, can bring a lot of advantages:

  • reduced application development times;
  • less chance of producing buggy code (since less code has to be written);
  • different programs can share the controls code from the same DLL so the system can use much less resources;
  • the users faces a set of controls with a consistent look and feel, rather than having to guess each time the logic of a new control.

Here I'll try to describe, in a series of articles, various methods of enhancing the operating system interface. While some of these methods may only benefit the application developer, others can be applied directly to existing PM controls, improving both the appearance and the functionality of already existing OS/2 applications and directly benefitting the end user.

I'll try to guide you, step by step, from the simplest controls, running inside an executable, discussing what are, in my opinion, the most common implementation flaws, showing you how we can use the controls in a resource script, how we can put them into a DLL and how we can make them work just like any other OS/2 standard controls.

Some conventions

In the examples I'll show you, I often make use of some preprocessor macros, mostly because I'm lazy and like to type as little as possible, but also because, sometimes, this will make complex pieces of code shorter and easier to understand.

For instance, since many PM messages have to return either FALSE or TRUE, I've defined MRFALSE and MRTRUE as ((MRESULT)FALSE) and ((MRESULT)TRUE) and have defined other macros like WinParent(hwnd), WinOwner(hwnd), etc...

You are warned, if you find something odd, just check the headers :-) ...

Besides that, since memory allocation will be handled in different ways as we proceed in our PM controls analysis, I have defined memalloc(), memfree() and memheapmin() to allocate and free the memory and to minimize the heap usage.

Finally, since the term control data may refer both to the data used internally by the control window procedure to store its text, size, etc. and to the data used to change some control styles via the pCtlData parameter of WinCreateWindow() or the CTLDATA statement in resource scripts, I will later use inner control data when referring to the first case and public control data for the other one.

Just in case you don't know yet what a window control is...

I assume you already have some knowledge of PM programming and know that a control is just an ordinary window, with its own window procedure, exactly like the client window we create when we use WinCreateStdWindow().

We just have to take into account the processing of many more messages because our window must behave exactly like the other standard controls: it must respond as usual, for instance, when a color or a font is dropped from the palette, the tab key or the mnemonic character is pressed in dialog windows, etc...

Besides that, unlike ordinary client windows, since our window control must be usable by other applications, we must compile it into a DLL.

Anyway, I will leave the discussion about DLL's for later, since that involves other kinds of problems (and common design flaws) such as, heap management.

Static controls

Static controls, like group boxes, static text, bitmap and icon controls, etc..., are the simplest kind of controls so they are the ideal subject to start from.

Our first task will consist in creating a simple bar control with a 3D look.

Xpmapi001.png

The bar should be used, like the group box control, to separate groups of related controls and should support the following styles for:

  • direction (horizontal or vertical)
  • appearance (depressed or raised)
  • text alignment (left, centered, right)
  • size (autosize)
  • other (mnemonic style)

For consistency with the standard PM controls, we define:

  1. the bar control class name as WC_BAR
  2. the bar control styles by the BARS_* prefix
  3. the bar messages by the BARM_* prefix
  4. the bar notification codes (passed to the owner window as the second USHORT of the first message parameter) by the BARN_* prefix (as a static control, anyway, the bar doesn't need to support any notification message)

and will apply this scheme to all the other controls we will deal with: WC_{ControlName}, {acronym}S_*, {acronym}M_* and {acronym}N_*...

Subclassing and superclassing

Since the bar control shares many features with some other static controls, like, for instance, the group box, we might just think to create a WC_STATIC window with the SS_GROUPBOX style, then subclass it and override just its standard paint procedure.

Unfortunately, while this may work if we just want to use the control in our application, we will not be able to make it easily available to other developers or to use it by adding a proper statement in a resource script.

A quite easy, yet not so well known, solution to this problem is superclassing.

When we subclass a control, we just substitute its window procedure, with superclassing, we register a new window class, derived from an already existing class (WC_STATIC in our example), overriding, when needed, its class styles, its class procedure, and the reserved storage size (often referred to as window words).

First of all we need to define some global variables where we will store the procedure address and the size of the window words of the WC_STATIC class.

PFNWP pfnWcStatic;
ULONG cbWcStatic;

Then, since we need to register our class before being able to use any window belonging to it, we need a registration procedure (that has to be exported if we build a DLL version):

BOOL APIENTRY BarRegister(HAB hab) {
    CLASSINFO ci;
    if (WinQueryClassInfo(hab, WC_STATIC, &ci)) {
           // store the default class procedure in the global variables
           pfnWcStatic = ci.pfnWindowProc;
           cbWcStatic = ci.cbWindowData;
           return WinRegisterClass(hab, WC_BAR, BarProc,
                     ci.flClassStyle & ~CS_PUBLIC,
                     ci.cbWindowData + sizeof(PVOID));
    }
    return FALSE;
}

As the code example shows, we first get from the system the data of the WC_STATIC class, store its original procedure and the size of its reserved storage, and then register a new class: the WC_BAR class. This new class uses a new window procedure, the WC_STATIC class style flags, (apart from CS_PUBLIC) and a larger size for the window words.

We have to remove the CS_PUBLIC class style flag since it is necessary to follow a special procedure (whose details will be dealt with in a future article) to make a window class public.

The reason for increasing the window words size is that in most cases we need to store some data (the control text, size, etc.) to manage our control. We cannot use QWL_USER for that purpose, since that offset must remain available for the user's (i.e. the developer) needs. We rather add enough space to store a pointer address and use the WC_STATIC reserved storage size (cbWcStatic) as an offset when calling WinSetWindowPtr() to store our inner control data and WinQueryWindowPtr() to access them.

Control data

To design the superclassed window procedure, we must take into account which behaviour of the parent class we want to override. By the way, in order to override the behaviour of the parent class, we have just to filter some messages, adding our code before or after or in complete substitution of the WC_STATIC window procedure (pfnWcStatic).

As we previously wrote, the bar control should behave like a groupbox, apart from its appearance, so our work should be focused mainly on redesigning the control paint procedure. To able to paint the control, though, we also need to know some relevant data like:

  • the control text,
  • the colors used to paint the various parts of the control
  • the control size,
  • the control style.

The text

We might just leave all the work to the WC_STATIC window procedure, getting all the data each time we need to paint the control. This might be anyway a bit expensive in terms of processing time.

Just to get the control text we should:

  1. call WinQueryWindowTextLength() to get the amount of memory we have to allocate to retrieve the control text,
  2. allocate memory
  3. call WinQueryWindowText() to get the text,
  4. free the allocated memory after having displayed the text.

Anyway, since the control text must be stored somewhere, we might just store it ourselves, rather than leaving that task to the WC_STATIC window procedure. But we cannot just store it during the window creation, we must also make our control able to respond to API calls like WinQueryWindowTextLength(), WinQueryWindowText() and WinSetWindowText().

To achieve all this we must process WM_CREATE, WM_QUERYWINDOWPARAMS and WM_SETWINDOWPARAMS:

  • On WM_CREATE we store the window text, the offset of the mnemonic character (if the control has the BARS_MNEMONIC style), calculate the size of the text box, and set the pszText member of the window CREATESTRUCT (that is the second message parameter of WM_CREATE) to NULL so the WC_STATIC procedure doesn't store any text.
  • On WM_QUERYWINDOWPARAMS, which is sent by the system when processing WinQueryWindowText() and WinQueryWindowTextLength(), according to the values of the first message parameter, we return the control text or the text length.
  • On WM_SETWINDOWPARAMS, sent by the system as a consequence of a WinSetWindowText() call, we update the text and the size of the text box.
  • Finally, if our control has the BARS_MNEMONIC style, we must also process WM_MATCHMNEMONIC, returning TRUE when the keyboard event corresponds to the control mnemonic character.

You might wonder why I cared to measure (and store) the size of the text box rather that just leaving that job to WinDrawText().

I made a small test program and found that this approach, while it doesn't use much more memory (12 bytes per window) it reduces the time needed to paint the control, since the text box size doesn't have to be recalculated each time the control is painted.

Please notice that in order to make this solution work effectively, the text box size must be calculated each time the window text is modified or when a different font is set for the window (WM_PRESPARAMCHANGED - PP_FONTNAMESIZE).

The file ctrlutil.c contains some utility functions to deal with the text of controls when it is single line, with or without the mnemonic character.

CtrlTextSize()
calculates all the data needed to draw the text and the line under the mnemonic character (if present);
CtrlTextSet()
(re-)allocates memory to store the text and optionally finds the offset of the mnemonic character removing the mnemonic tag (~);
CtrlTextGet()
copies the text to a buffer in response of WinQueryWindowText(), optionally including the mnemonic tag;
CtrlTextDraw()
paints the text faster than WinDrawText() and with some more options (3D text).

You can get more details from the source file which is richly commented.

The colors

OS/2 provides various way to control the colors used to paint the various PM windows:

  • The presentation parameter colors have the highest priority. When a control has to be painted, if a proper presentation parameter is found, the control must use that color. In some cases, like for the background and foreground colors of static controls, the presentation parameters can be inherited from the control owner window.
  • The control colors are something new, but probably never used by any application.
    • This new API, introduced since Warp 4, allows you to finely tune the colors at a global level and at a single application level.
    • Each color can be set in an independent way. For instance, the system colors whose values are often shared among various kind of controls, you can set completely different colors for the background, foreground and borders of menu, dialogs, scrollbars and buttons.
    • The WinQueryControlColors() API provides an easy way to get, with one call, all the colors used to paint a control, checking the presentation parameters first, then the control colors at application level, the control colors at system level and at the end, if no other color has been found, the system colors or the predefined hard coded RGB color values.
    • Unfortunately there are various disadvantages with this method.
    • While we can make a new control behave like an already existing control, returning a proper value to the WM_QUERYCTLCOLOR message, we cannot define a new set of colors. For instance, if we make a new button class which uses four different colors to paint its border (e.g. eStylerLite enhanced push buttons) we cannot use the push button control colors, since not all the needed color indexes are defined for the CCT_PUSHBUTTON control type.
    • Besides that, WinQueryControlColors() requires more stack space (since it needs an array of CTLCOLOR structures as a parameter) and is much slower than WinQueryPresParam().
  • The system colors are used when no other color has been defined. They can be set by drag and drop from the color palette (keeping the Alt key pressed), by drag and drop from the scheme palette, by calls to WinSetSysColors() or by directly modifying the values in the USER profile (OS2.INI).
    • Unfortunately not all controls use the system color values as default colors. So, quite often, it is not possible to control all the control colors via the system colors.

After having tested different solutions I came to the conclusion that it is better to completely ignore the control colors and I decided to design a new API to get the current colors:

LONG CtrlClrGet(HWND hwnd, ULONG ulid1, ULONG ulid2, LONG ldef, BOOL bi) {
    LONG lclr = 0;
    bi = bi? 0 : QPF_NOINHERIT;
    // first checks for presentation parameters
    if (WinQueryPresParam(hwnd, ulid1, ulid2, NULL, 4,
        (PVOID)&lclr, bi | QPF_PURERGBCOLOR | QPF_ID2COLORINDEX))
           return lclr;
    // if ldef refers to a valid SYSCLR_* index gets the RGB value
    if ((ldef >= SYSCLR_SHADOWHILITEBGND) && (ldef <= SYSCLR_HELPHILITE))
           return WinQuerySysColor(HWND_DESKTOP, ldef, 0L);
    return ldef;
}

where:

HWND hwnd
is the control window handle
ULONG ulid1
is a presentation parameter id (e.g. PP_BACKGROUNDCOLOR)
ULONG ulid2
is a presentation parameter id expressed as color index (e.g. PP_BACKGROUNDCOLORINDEX)
LONG ldef
is the default color expressed either as a SYSCLR_* value or as an RGB value (e.g. SYSCLR_DIALOGBACKGROUND or 0xcccccc)
BOOL bi
is the inheritance flag, specifying that the current presentation parameter can be inherited from the control owner

Since this call is usually processed quite quickly by the system, rather than caching the control colors, retrieving them only during window creation and when they are changed, (by filtering WM_SYSCOLORCHANGE or WM_PRESPARAMCHANGED) I decided to get them each time the control has to be painted.

The size

The easiest way to get the current size of our control is via the WinQueryWindowRect() API. This always gives a rectangle whose lower left-corner is at 0,0 coordinate and whose upper-right corner is set to the control size.

In some cases, especially if the control supports an AUTOSIZE style, which might be affected by a font change or modifications of other styles, it might be more convenient, even if the WinQueryWindowRect() calls are usually processed in a matter of nanoseconds, to cache our control size in the inner control data.

In our bar control sample I used this approach, defining the SIZES structure to store such data:

typedef struct {
     SHORT cx, cy;
} SIZES, * PSIZES;

Then, in the control window procedure the size data are processed as follows:

  • During window creation (WM_CREATE) we check if the BARS_AUTOSIZE style flag is set: if the bar is vertical we set its width to its thickness, otherwise we set its height to the maximum value between its thickness and the text box height. Finally we store the size in the inner control data.
  • If the control size is changed (WM_ADJUSTWINDOWPOS) and the BARS_AUTOSIZE is set, we adjust the new size to the calculated size. The new size is stored in the inner control data.
  • If the bar thickness changes the control style is automatically set to BARS_AUTOSIZE, the size is recalculated and the window is resized accordingly.
  • If the font changes and the BARS_AUTOSIZE style flag is set, the size is recalculated and the control is resized.
  • In the paint procedure we use the size previously stored in the inner control data to paint the various control parts.

Of course, if a control just needs to check its size during its paint procedure, then there is no need to cache the data and to process all these messages.

The style

Regarding the control style, we have to make a distinction between static styles and dynamic styles.

Static styles are those styles which cannot be changed except by destroying and recreating the window. For instance if we create a WC_BUTTON class window with BS_AUTOCHECKBOX style, we cannot pretend to just change its style to BS_PUSHBUTTON or BS_RADIOBUTTON.

In a similar way, we cannot pretend to change a vertical bar to a horizontal bar by just calling WinSetWindowULong() or WinSetWindowBits(). Of course, other style modifications, like text alignment, 3D appearance, enabled/disabled state, etc. should be allowed without any restriction.

It may be useful to cache the static styles in the control data during window creation since in various messages we just need to know that, and with a call to WinQueryWindowPtr() we get all the data needed to manage the control.

In our bar window example a USHORT has been reserved to cache the direction style (BARS_VERTICAL or BARS_HORIZONTAL) and the autosize style (BARS_AUTOSIZE).

Another distinction regards styles which can be set just by changing the QWL_STYLE window words and more complex styles which cannot fit in a bit flag.

Such styles can only be managed by defining a new presentation parameter, of value PP_USER or above, in the case of simple values, or by using a proper data structure.

User defined presentation parameters may be set or queried via the usual APIs while data structures, generally referred to as control data, are set via:

  • the pCtlData parameter of the WinCreateWindow() API, or
  • by filling the CTLDATA statement in a resource script, or
  • by sending the WM_SETWINDOWPARAMS message.

Some standard PM controls use the control data to set some additional styles: WC_BUTTON windows use the BTNCTLDATA structure, entry fields use ENTRYFDATA, frame windows use FRAMECDATA, etc...

In our static bar example, even if a presentation parameter would have been more than enough, I used the control data to change the bar thickness, with the purpose of providing a more significant example.

Since all that we need is to store the control thickness, I just used a USHORT (you can check SUPERCLASS.RC to see how to do that in a resource script).

Anyway, in most cases, it is better to define a structure whose first member should be the structure size in bytes and/or a version id. So, in future, if we need to make a new control version, with some more features, when processing the control data we just have to check the version id to be able to process them accordingly, avoiding any problem of backward compatibility.

The inner data structure

In order to manage the data we have discussed so far I used the following structure:

typedef struct {
    USHORT style;     // just for caching the static styles
    USHORT thkns;     // bar control thickness
    SIZES sz;             // control size
    PCTRLTXT pct;     // control text data structure
} BAR, * PBAR;

The needed memory is allocated during window creation and its address is stored in the window words of the control at the cbWcStatic offset.

This structure is only intended for internal usage, and should not be confounded with the control data structures we just mentioned above.

The superclassed window procedure

Now that we have seen which data is needed to properly implement our new bar control, let's check which messages we have to process in our control window procedure.

I will not report the whole source code here, so in order to better understand the following notes, you should check the included source files.

All the files are in the xpmsrc01.zip [1] archive. SUPERCLASS.C, SUPERCLASS.H and SUPERCLASS.RC contain both the bar window procedure and the sample executable code, while CTRLUTIL.C and the other header files contain some utility functions and general purpose macros.

The control creation (WM_CREATE)

The first thing to do, on window creation, is to allocate the memory to store the inner control data. If this operation fails we just return TRUE to dismiss the window creation, otherwise we store the memory address in the window words at the cbWcStatic offset.

 pbar = (PBAR)memalloc(sizeof(BAR));
 if (!pbar) return MRTRUE;
 WinSetWindowPtr(hwnd, cbWcStatic, (PVOID)pbar);

Once we have allocated the needed storage, we need to initialize it. We store the static styles and check if we are dealing with a vertical or horizontal bar, in which case we store the control text and calculate the size of the text box.

Then we set the pszText member of the CREATESTRUCT to NULL, because, at the bottom of our code, we have to pass the WM_CREATE message back to the WC_STATIC window procedure and we want to avoid to store the control text twice: once in the bar inner control data and then in the WC_STATIC inner data.

    pbar->style = ((PCREATESTRUCT)mp2)->flStyle;
    if (pbar->style & BARS_VERTICAL) {
           pbar->pct = NULL;
    } else {
           pbar->pct = CtrlTextSet(NULL, ((PCREATESTRUCT)mp2)->pszText,
                                                           -1, pbar->style & BARS_MNEMONIC);
           // if the control has any text, measures the text box size
           if (!pbar->pct ||
                   (pbar->pct->len && !CtrlTextSize(hwnd, pbar->pct))) {
                memfree(pbar);
                return MRTRUE;
           } /* endif */
    } /* endif */
    ((PCREATESTRUCT)mp2)->pszText = NULL;

The next step consists in checking the public control data and setting the bar thickness. If a non-default bar thickness is set via the control data, the bar style is automatically set to BARS_AUTOSIZE.

if (((PCREATESTRUCT)mp2)->pCtlData) {
       pbar->thkns = *((PUSHORT)((PCREATESTRUCT)mp2)->pCtlData) & 0x7e;
       pbar->style |= BARS_AUTOSIZE;
} else {
       pbar->thkns = (pbar->style & BARS_THICK) ? 4 : 2;
} /* endif */

Now that we have all the needed data, we can check if the BARS_AUTOSIZE style flag is set. If it is, we calculate the control size and resize the bar accordingly.

Finally, we store the control size and call pfnWcStatic to let the WC_STATIC window procedure initialize the other data for its inner usage.

    if (pbar->style & BARS_AUTOSIZE) {
           if (pbar->style & BARS_VERTICAL) {
                ((PCREATESTRUCT)mp2)->cx = pbar->thkns;
           } else {
                ((PCREATESTRUCT)mp2)->cy = max(pbar->thkns, pbar->pct->cy + 2);
           } /* endif */
           WinSetWindowPos(hwnd, 0, 0, 0,
                                       ((PCREATESTRUCT)mp2)->cx, ((PCREATESTRUCT)mp2)->cy,
                                       SWP_SIZE | SWP_NOADJUST);
    } /* endif */
    pbar->sz.cx = ((PCREATESTRUCT)mp2)->cx;
    pbar->sz.cy = ((PCREATESTRUCT)mp2)->cy;

Freeing the resources (WM_DESTROY)

Nothing special here. We get the inner data address, if any text data had been allocated, we free it, free the inner data memory and fall back to pfnWcStatic to let it free its own resources.

if (NULL != (pbar = BarData(hwnd))) {
       if (pbar->pct) memfree(pbar->pct);
       memfree(pbar);
       memheapmin();
} /* endif */

The window parameters (WM_QUERYWINDOWPARAMS and WM_SETWINDOWPARAMS) These messages are sent by the user indirectly via API calls or directly to change some control styles or attributes (the bar thickness in our example.)

The first message parameter is, in both cases, the address of a WNDPARAMS structure. The fsStatus member identifies which parameters are to be set or queried.

Although defined in PMWIN.H, the WPM_PRESPARAMS and WPM_CBPRESPARAMS flags don't produce any effect in Warp 4 (and probably even in the older versions of OS/2) when passed to the default window procedure.

Besides that, no message is sent as a consequence of WinQueryPresParam() or WinSetPresParam(). These flags might be used by developers to set some further control data or attribute. In most cases, anyway they will be of no use and we can safely ignore them, especially when designing new controls (i.e. not superclassed controls).

The valid flags for WM_QUERYWINDOWPARAMS are:

WPM_TEXT: which occurs when the WinQueryWindowText() API is called. In this case the cchText member reports the size of the buffer, provided as pszText , where the control text should be copied.

The CtrlTextGet() function takes care of all the job, optionally returning or stripping the mnemonic tag character (~).

WPM_CCHTEXT: occurs when the WinQueryTextLength() API is called. If the control has any text we just return its length.

WPM_CTLDATA: usually is sent by the developer to retrieve the public control data, for instance to get the handle of the image displayed on a push button. We just return the bar thickness.

WPM_CBCTLDATA: is used to get the size of the public control data associated with the control. Some control may use a variable size control data structure. In that case we have to use this flag to know how much memory is needed to retrieve the current control data.

According to the documentation, as each requested item is successfully processed, the corresponding flag of the fsStatus member must be unset. When all items have been processed if fsStatus is 0 the procedure should return TRUE, otherwise it should return FALSE to indicate that it failed to process some data.

Please notice that in most custom control code examples I have had a chance to read, the developers didn't process this message properly. Besides not taking care of unsetting the fsStatus flag, they used to call WinDefWindowProc() before or after processing the message. This is completely nonsensical since the default window procedure just returns FALSE as stated by the documentation. The correct way of handling this message is by putting at the bottom just:

return (MRESULT)!((PWNDPARAMS)mp1)->fsStatus;

The valid flags for WM_SETWINDOWPARAMS are just:

WPM_TEXT: which is sent by the system when the WinSetWindowText() API is called. In our example, store the new text, update the text box size and update the control.

WPM_CTLDATA: which is sent by the developer to change the public control data. On processing this we just resend BARM_SETTHICKNESS with the proper parameters to the bar procedure.

Also with this message there is absolutely no need to call WinDefWindowProc() neither before nor after having processed the message. We just return TRUE if the processing was successful.

The mnemonic character (WM_MATCHMNEMONIC)

This message is sent by the dialog procedure to its children as a consequence of a keyboard event. The first message parameter is the ascii value corresponding to the pressed key. If the control has the BARS_MNEMONIC style set and the character following the mnemonic tag (~) matches the one in mp1, we return TRUE.

    if (!(WinStyle(hwnd) & WS_DISABLED) &&
            (NULL != (pbar = BarData(hwnd))) && pbar->pct &&
            (pbar->pct->mnemo >= 0)) {
           HAB hab = WinHAB(hwnd);
           return (MRESULT)
                        (WinUpperChar(hab, 0, 0, (ULONG)mp1) ==
                           WinUpperChar(hab, 0, 0,
                                           (ULONG)pbar->pct->ach[pbar->pct->mnemo]));
    } /* endif */                                               

Controlling the window size (WM_ADJUSTWINDOWPOS)

This message is sent to the control before it is moved or sized. By modifying the new control coordinates it is possible to change the new position or size of the control.

We are just interested in the control size. So, if the control is being resized and it has the BARS_AUTOSIZE flag set, the new size is modified according to the calculated size, otherwise the new size is just cached to be used later to repaint the control.

if ((((PSWP)mp1)->fl & SWP_SIZE) &&
       (NULL != (pbar = BarData(hwnd)))) {
       if (pbar->style & BARS_AUTOSIZE) {
            if (pbar->style & BARS_VERTICAL) {
                   ((PSWP)mp1)->cx = pbar->thkns;
            } else {
// horizontal bar
                   ((PSWP)mp1)->cy = max(pbar->thkns, pbar->pct->cy);
            } /* endif */
       } else {
            pbar->sz.cx = ((PSWP)mp1)->cx;
            pbar->sz.cy = ((PSWP)mp1)->cy;
       } /* endif */
} /* end if */

The presentation parameters (WM_PRESPARAMCHANGED)

Since, as we previously saw, we are not cacheing the control colors, this message is processed only when the control font changes. In that case the size of the text box is calculated and, if the BARS_AUTOSIZE style flags is set the control is resized accordingly. Finally the control is updated.

    if (((LONG)mp1 == PP_FONTNAMESIZE) &&
            (NULL != (pbar = BarData(hwnd))) &&
            !(pbar->style & BARS_VERTICAL) && pbar->pct) {
            CtrlTextSize(hwnd, pbar->pct);
            if (pbar->style & BARS_AUTOSIZE) {
                   pbar->sz.cy = pbar->pct->cy + 2;
                   WinSetWindowPos(hwnd, 0, 0, 0, pbar->sz.cx, pbar->sz.cy,
                                                   SWP_SIZE | SWP_NOADJUST);
            } /* endif */
    } /* endif */
    CtrlUpdate(hwnd, FALSE);

The bar control specific messages (BARM_SETTHICKNESS and BARM_QUERYTHICKNESS)

Although these two messages are not strictly necessary, since it is possible to query and set the control thickness via WM_QUERYWINDOWPARAMS and WM_SETWINDOWPARAMS, I redefined a couple of the WC_STATIC SM_* messages (SM_SETHANDLE and SM_QUERYHANDLE) just to show you an example of control specific messages.

    case BARM_SETTHICKNESS:
           if (!mp1 || (NULL == (pbar = BarData(hwnd)))) return MRFALSE;
           pbar->style |= BARS_AUTOSIZE;
           pbar->thkns = ((ULONG)mp1) & 0x7e;
           if (pbar->style & BARS_VERTICAL) {
                pbar->sz.cx = pbar->thkns;
           } else {
                pbar->sz.cy = max(pbar->thkns, (pbar->pct? pbar->pct->cy + 2: 0));
           } /* endif */
           WinSetWindowPos(hwnd, 0, 0, 0, pbar->sz.cx, pbar->sz.cy,
                                           SWP_SIZE | SWP_NOADJUST);
           return MRTRUE;
    /* end case BARM_SETTHICKNESS */
  
    // Query the bar thickness -------------------------------------------
    case BARM_QUERYTHICKNESS:
           if (NULL == (pbar = BarData(hwnd))) return (MRESULT)0xffff;
           return (MRESULT)pbar->thkns;
    /* end case BARM_QUERYTHICKNESS */

The paint procedure

There is nothing special about it:

  1. We first get the value of the colors we have to use to paint the control,
  2. set the palette to RGB mode,
  3. erase the control background
  4. draw the bar,
  5. if it is an horizontal bar with text, calculate the text coordinates according to the alignment style,
  6. finally draw the text

Conclusion

I tried to give you most if not all the details needed to design a basic window control by superclassing an existing one.

In the next article I will show you how to design a completely new control (i.e. not relying on an already existing one), how to make it available to any application and how to make it a public control.

If you find this subject interesting, I'll go on with more advanced topics.

Please feel free to email me if you find any bugs, want to suggest any improvement, want more details about this subject or would like me to deal with some particular control or other PM programming subject.