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:
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.
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.
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.
Our first task will consist in creating a simple bar control with a 3D look.
The bar should be used, like the group box control, to separate groups of related controls and should support the following styles for:
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.
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:
Just to get the control text we should:
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:
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.
You can get more details from the source file which is richly commented.
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().
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.
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:
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:
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.
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:
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.
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.
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 (xpmsrc01.zip) 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.
    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;
    if (NULL != (pbar = BarData(hwnd))) {
            if (pbar->pct) memfree(pbar->pct);
            memfree(pbar);
            memheapmin();
    } /* endif */
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:
The CtrlTextGet() function takes care of all the job, optionally returning or stripping the mnemonic tag character (~).
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:
    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 */                                               
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 */
    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);
    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 */
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.
This article is courtesy of www.os2ezine.com. You can view it online at http://www.os2ezine.com/20020216/page_10.html.