February 16, 2002 Alessandro Cantatore is the developer behind Styler/2 and ecStyler. If you have a comment about the content of this article, please feel free to vent in the OS/2 eZine discussion forums. There is also a Printer Friendly version of this page. |
|
Controlling PM Controls
(Some ideas about improving the OS/2 Presentation Manager)
| |||
ForewordMore 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:
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 conventionsIn 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 controlsStatic 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.
The bar should be used, like the group box control, to separate groups of related controls and should support the following styles for:
Subclassing and superclassingSince 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.
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):
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 dataTo 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 textWe 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:
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. The colorsOS/2 provides various way to control the colors used to paint the various PM windows:
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:
The sizeThe 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:
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. The styleRegarding 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:
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 structureIn order to manage the data we have discussed so far I used the following structure:
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 procedureNow 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 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.
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.
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.
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.
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.
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:
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:
The valid flags for WM_SETWINDOWPARAMS are just:
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.
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.
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.
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.
The paint procedureThere is nothing special about it:
ConclusionI 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.
|
|||||
|