Anatomy of a PM Program
By Les Bell
The OS/2 base operating system is fairly traditional in design. Although it offers many new features to the programmer who hitherto has been limited to DOS, these are mostly unremarkable. Apart from functions which have always been in DOS, though extended in some cases, the new API comprises functions for setting process priorities, memory management, I/O and the like, and are very similar to those found in systems like UNIX and VMS. The most interesting additions concern multi-threading and dynamic link libraries.
But there is one area where OS/2 is quite different from DOS, and this is the Presentation Manager graphical user interface. Corporate developers who have previously put together applications for DOS are in for a shock when they first encounter PM, as its complexity, structure and design are quite different from almost anything they will have seen before.
There is one exception: Presentation Manager is similar - at least in its window management and program structure - to Microsoft Windows. There are two differences: Windows is non-preemptive and so the programmer has to be careful to keep yielding the CPU so that other programs get a chance to run (OS/2 does this automatically) and OS/2 has many high-level graphics functions in a layer called the GPI, which Windows does not have.
Presentation Manager is a message-driven operating system within an operating system. The OS/2 base operating system, which provides the underlying services, consists of a library of functions which are called directly from application programs. PM, by contrast, is an object-oriented system in which different objects - termed windows - send each other messages, although there are still lots of functions. The first obstacle the beginning PM programmer will encounter is learning the various functions and messages, so as to select the right ones in different situations. Unfortunately there is no easy way to do this; it comes down to experience.
Why Message Passing?
Like other graphical environments, PM supports multiple forms of input to an application program. At least keyboard and mouse are usually present, but pen input is also supported with extensions. Now, consider what might happen if a program - operating in a preemptive multitasking environment like OS/2 - performed its own input processing with two sets of logic for keyboard and mouse. Keyboard processing is relatively simple: just do a table lookup on the input keystroke and branch to the appropriate subroutine. But mouse input is much more complex. We have to deal with selection of objects like blocks of text or even possibly graphics, as well as pull-down menu selections.
It's quite possible that a user might use the mouse to pull down a menu option to save a file, and then immediately press Alt-F4 to quit the application. If the keystroke processing is much faster, then the application might quit before saving the file. Result? One irate user. Of course, most apps would be smart enough to ask the user whether the file should be saved before quitting, but the result would still be annoying and more dangerous problems would likely result.
As a consequence, PM applications have to deal with external events in the order in which they are generated, and to do this PM uses a message-passing architecture. Events, such as mouse movement and button actions, keystrokes and the like, generate messages, which are dispatched to Presentation Manager.
What's a message? Here's how it's defined in the PMWIN.H file:
/* QMSG structure */ typedef struct _QMSG { /* qmsg */ HWND hwnd; ULONG msg; MPARAM mp1; MPARAM mp2; ULONG time; POINTL ptl; ULONG reserved; } QMSG;
The various members of the structure have the following meanings: hwnd is the handle of the target window. Now, in DOS the only objects which have handles are files, but in OS/2 most things have handles: pipes, queues, semaphores and of course windows. PM will automatically send the message to the appropriate window procedure based upon this value. Of course, that window may in turn hand off the message to another window.
The msg field specifies just what the message is. The OS/2 Presentation Manager Programmer's Toolkit header files define several hundred messages. Typical examples would include:
- WM_PAINT
- Tells the target window that it should repaint its contents on the screen as a result of being opened, brought to the foreground or some other circumstance.
- WM_COMMAND
- The user has chosen a menu option on the application's menu bar or pressed a button.
- WM_CONTROL
- Something happened to a control window, such as a text-entry field. Just what can be found from the other parts of the message and can range from a user selecting the field with the mouse to typing something into the control.
- WM_CHAR
- A keystroke.
- WM_CLOSE
- User is closing the window.
The next two components of the message are MPARAMs. An MPARAM is a 32-bit value which is essentially typeless: it could contain two sixteen-bit values or four eight-bit values or a 32-bit far pointer or almost anything. Generally, programmers break apart an MPARAM, using special macros such as SHORT1FROMMP(), to get at the components. For example, the WM_COMMAND message simply means that the user did something with a menu or some other window component; exactly what is found out by looking at the low word of mp1.
Following the MPARAMS is an unsigned long timestamp which plays a key role in resolving the problem described above. It is the timestamp which keeps messages from being processed in the wrong order. Most applications don't care about the timestamp on a message, and in fact, as you'll see, don't receive the timestamp; as long as the messages are processed in the correct sequence, that's the most important thing.
Lastly, there's a POINTL structure, which contains the x- and y-coordinates of the mouse at the time the message was generated. Again, most programs don't use this use and don't automatically receive it; but it (like the timestamp) can easily be retrieved.
Notice that keystrokes appear to the program as WM_CHAR messages. In fact, PM programs do not read input via C library functions such as gets() and scanf(). Nor do they produce output with puts() or printf(). Instead, all input comes through a message queue and all output is produced by calling PM functions, while the stdin and stdout streams are redirected to the NUL device.
In actual fact, there are two different types of messages: queue messages, which are posted by the system to the application's message queue, and window messages, which are passed as parameters when the system invokes a window procedure. Window messages do not contain the timestamp or mouse position fields.
The main procedure of a PM program will generally receive messages through an input queue, so one of the first things it must do is to create that queue. It will then probably create a window on the screen (not all PM programs do this).
Windows
The application will produce output and receive input through a window. However, not all windows are visible - it is possible to create an 'object window' which will respond to messages and update related data structures without ever appearing on the screen. This corresponds with the idea of an object class in languages like Smalltalk.
In fact, what most users would consider the screen is actually a window - the desktop window (there's also a desktop object window). Windows have relationships in two senses: relative positioning (one window within/on top of another, or behind another) and ownership (one window coordinates the behaviour of another by sending it messages and receiving messages back about the owned window's state). The desktop window is generally the parent for application windows.
The application window the user sees is actually composed of several other windows. In the background is the frame window, which provides a 'base' for several frame controls and the client window area. The frame controls are things like the border, system menu, title bar, minimize/maximize buttons, application menu, icon and scroll bars (if specified) and these are all windows in their own right. These windows are predefined - the code for them is in the operating system itself. The client window is the (usually white) central area where the application displays its output, and the code for this must be written by the programmer.
The frame window coordinates the actions of all the other windows so that they behave according to the OS/2 user-interface guidelines. Move the frame window by dragging the title bar, and all the other components redraw in the correct locations, for example.
There are other kinds of windows, too. For example, dialog windows are usually used to interact with the user - say, to fill out a form or create a new file - and these windows in turn will contain other control windows such as entry fields, list boxes or push buttons. In many cases, the buttons are predefined public classes, although the dialog windows, like client windows, have to be coded by the programmer.
Let's look at a simple OS/2 Presentation Manager application. You'll all be familiar with every C programmer's first C program:
#include void main() { printf("Hello World\n"); }
Now, the same code can be compiled and run under OS/2, but it would run as a full-screen or windowed character-kernel application. We want the graphical version.
The first job is to include all the definitions of constants and prototypes for the window-manager functions from the Presentation Manager Programmer's Toolkit. Generally, this is done with the preprocessor directive #include but before this, we specify which subsections of the header files we want by defining some constants such as INCL_BASE, INCL_GPIERRORS and so on. This is much faster than including all the definitions, which are quite massive.
Next, we need some handles to refer to the various windows. DOS uses handles to identify open files; in OS/2 there are handles for all kinds of objects - files, pipes, queues, and of course, windows. A handle is probably a pointer to some kind of data structure, but you don't need to know - and don't want to know, in case you start to rely on knowledge of the structure and thereby write non-portable code.
Most special types used by PM are typedef'ed in the header files, and PM programmers use these types, even in place of standard C types. The reason is this: in OS/2 1.x, running on a 16-bit processor, an integer is 16 bits in size; but on OS/2 2.x, which runs on 32-bit processors, an integer is 32 bits in size. This is why a message is declared as ULONG above; if we declared it as int then it would be different sizes to different compilers, but a ULONG is always 32-bit.
You'll also notice that OS/2 programmers use Hungarian notation for variable naming. This prefixes the variable name with its type, so that hwndClient is pretty obviously a handle to a window, while szClassName is a string terminated with a zero byte.
HWND hwndFrame; HWND hwndClient; static char szClassName[] = "Hello";
Likewise, hab is a handle to an anchor block, while hmq is a handle to a message queue, and ulFrameFlags is an unsigned long.
The first thing the program must do is register with Presentation Manager. This will cause PM to allocate memory to store graphics images and workspace on behalf of the application. This is done by calling the WinInitialize() function, which returns a handle to an anchor block. Nowhere does the PM documentation define an anchor block, but my guess is that it is an instance data segment created by the PM dynamic link library functions. In any case, we don't want to know. From now on, the hab is used as the first parameter for many PM function calls.
Next, the app must create a message queue so that it can read messages. Again, this is a single function call to WinCreateMsgQueue(), which returns a handle to the queue. Incidentally, it is this function call which defines a Presentation Manager application. If the program makes this call, it is a PM program; if it does not, it is not, and cannot call most PM functions.
Once this has been done, we can now register a private window class. It's called a class because this one window definition can handle multiple windows, so really a window is an object while the code is a class. Basically, what we are doing is associating a window class name (an English word) with a procedure which will perform input and output processing for all windows of that class. So the parameters to the WinRegisterClass() function call are basically the handle to the anchor block, the classname string and a pointer to the window procedure (which is lower in the program code). We also pass some flags which specify class styles, such as redrawing the entire window whenever it is resized. The last parameter is the size of the window data. This is not used in simple applications, but becomes important when an app has multiple windows of the same class. Because a single window procedure must perform processing for multiple windows, it must be reentrant. This means no static variables, and therefore we get the system to reserve a few bytes of memory for each window, and either store each window's private variables along with the window, or store a pointer to a data structure in the window.
Having set all of this up, we can now go ahead and create a window on the screen. Applications can create any type of window with the WinCreateWindow() function call, but this requires many parameters and a lot of setting up. In this case, we simply want to create a standard application window, and let the system worry about its size, where on the screen it is placed, and so on.
The WinCreateStdWindow() function actually creates (at least) two windows: a frame window and a client window. The client window is the (usually white) area in the centre of the application window where work actually gets done. The frame window - only the border of which is visible - is the parent of the client window and other related windows, such as the title bar, system menu, application menu, any scroll bars and so on. The frame window serves to coordinate their behaviour and paint them in the correct relative positions.
The first thing to do is to specify which component parts we want: border, title bar, menu, and other on-screen real estate. This is done by setting the component bits of a 32-bit unsigned long. In this case, we want a standard window, except no menu, no accelerator table and no icon:
ulFrameFlags = FCF_STANDARD & ~FCF_MENU & ~FCF_ACCELTABLE & ~FCF_ICON;
We could also have OR'ed in bits for FCF_VERTSCROLL and FCF_HORZSCROLL, if we wanted them. Now comes the actual function call which creates the window. WinCreateStdWindow() takes lots of parameters, but fewer than WinCreateWindow()! These include the handle of the parent window (in this case, the desktop), the window style, the frame creation flags, the classname, title bar text and the address of the variable which will receive the client window handle.
The window styles can include:
- WS_CLIPCHILDREN
- Prevents a window from over-painting its children
- WS_CLIPSIBLINGS
- Prevents a window from over-painting its siblings
- WS_DISABLED
- Disables mouse and keyboard input to the window.
- WS_MAXIMIZED
- Enlarges the window to maximum size
- WS_MINIMIZED
- Reduces the window to smallest (iconized) size
- WS_PARENTCLIP
- Prevents a window from over-painting its parent
- WS_SAVEBITS
- Saves the image in the window as a bitmap, so that if the window is moved or un-hidden, the system can restore the window contents by copying the bitmap without requiring the application to repaint it.
- WS_SYNCPAINT
- Causes the system to send a WM_PAINT message to the window whenever any part of it becomes invalid. Without this style, the system accumulates invalid regions and send one WM_PAINT message when no other messages are pending
- WS_VISIBLE
- Makes the window visible (unless it is totally obscured by other windows). Windows without this style are hidden.
- WS_ANIMATE
- Enables a zooming effect on this window (disabled if turned off in the System object in System Setup).
WinCreateStdWindow() actually has two parameters of this type: the first specifies the style of the frame window, while the second specifies the style of the client. A frame window can have some additional style flags:
- FS_ACCELTABLE
- Creates an accelerator table
- FS_BORDER
- Creates a window that has an inner border the same colour as the title bar. This style is used by dialog windows ;
- FS_DLGBORDER
- Creates a window with a single line border (again, a dialog window)
- FS_ICON
- Creates a window with an icon
- FS_MOUSEALIGN
- Creates a window which is positioned relative to the current mouse coordinates. Sometimes used by dialog windows
- FS_NOBYTEALIGN
- Creates a window which is not aligned on a byte boundary in video memory. This will degrade drawing performance.
- FS_SCREENALIGN
- Creates a window which is aligned to the screen (dialog windows)
- FS_SHELLPOSITION
- Allows PM to size the window and place it in a position cascaded from the previously started application window
- FS_SIZEBORDER
- Creates a size border
- FS_SYSMODAL
- Creates a system modal window
- FS_TASKLIST
- Adds the window title to the Window List
- FS_STANDARD
- Combination of FS_ICON, FS_ACCELTABLE, FS_SHELLPOSITION and FS_TASKLIST
At this point in the program, the window gets created and should pop onto the screen. As the window is created, PM sends it various messages, such as WM_CREATE, WM_SIZE and WM_PAINT, to allow the window to initialize, resize itself to the correct size and paint its contents. Of course, the user can also interact with the window, with either the mouse or keyboard (this simple app has almost no keyboard support), and this will cause input to be sent to the app. As messages arrive on the input queue, we must read these messages and then direct them to the correct window. This is done by the event loop:
while(WinGetMsg(hab, &qmsg, 0, 0, 0))
WinDispatchMsg(hab, &qmsg); This reads each message in turn, and dispatches it to the correct window. The process continues until WinGetMsg() gets a WM_QUIT message, which causes it to return FALSE and exit the while loop.
After this, it is all over bar the clean-up, which destroys the window and message queue and releases the anchor block. But where does the actual work get done? We haven't seen any code which prints the message "Hello World".
Window Procedures
Most of the work of PM applications gets done in window procedures, or winprocs, which contain the code which responds to the various messages. A winproc is declared to be of type MRESULT EXPENTRY, which means that it returns a 32-bit result code and is called using the _system calling convention. This is because it will be called directly by the operating system, not by the application itself (which may use C or OptLink calling conventions). The parameters to the winproc are, of course, the target window handle, the message and its parameters.
The code of most winprocs reduces to a case statement, to handle the different messages, and this one is no exception. Apart from a couple of trivial stubs to handle specific messages, we are really only concerned with one message, WM_PAINT. This message is sent whenever the window is created, when it is resized, when it is maximized or brought to the foreground from a partially obscured background position. A really smart application will only redraw that part of its window which was previously invalidated, and it finds that out from the parameters to the WM_ERASEBACKGROUND message. This is faster for some applications. However, for simplicity our example ignores WM_ERASEBACKGROUND messages and simply redraws the entire window whenever it receives the WM_PAINT message.
Here's how it does it:
hps = WinBeginPaint(hwnd, 0, &rc);
The WinBeginPaint() function call obtains a Presentation Space (PS) and returns a handle to it. Calls to the Presentation Manager GPI (Graphics Programming Interface) functions - and there are hundreds of these - will cause a graphics image to be painted on the screen. One of the nice things about a Presentation Space is that the graphics image can be retained within it and can be output on multiple Device Contexts - that is, a screen window, a printer, a plotter and so on. In this case, the function call obtains a cached micro-PS which is smaller and faster, but can only be used with one Device Context, in this case the screen window. One of the parameters passed to the function is the address of a RECTL (rectangle) structure, rc, which it obligingly fills in with the window coordinates.
Next, we put our message in the window:
WinDrawText(hps, -1, szMessage, &rc, CLR_NEUTRAL, CLR_BACKGROUND, DT_CENTER | DT_VCENTER | DT_ERASERECT);
WinDrawText() will draw a single line of text in a rectangle in a PS. One of the parameters passed is the address of the RECTL structure containing the rectangle coordinates which we obtained from the WinBeginPaint() call. Others include a pointer to the message string, foreground and background colours and flags to control centring and background erase. Finally
WinEndPaint(hps);
ends the painting and releases the PS. That's it.
There are lots of other ways of doing the same thing. For example, I could have used WinFillRect() to paint the window white, and then GpiCharStringAt() to output the message to the window. And for good measure, I could have added some lines to query the fonts on the system and changed to Times Rmn New, for example.
Of course, other messages also find their way to the winproc, but we don't care about them, so we let the WinDefWindowProc() function call PM to perform default message processing.
The complete application is listed below, and includes a few other little twists for error handling. Real-world apps are of course more complex, but they follow the same basic structure. Bear in mind that because of the need to process messages in strict sequence as described above, the winproc must complete processing one message before it can start another. For this reason, time-consuming message-processing code in one winproc would freeze the system until it was completed. To get around this problem, PM applications will typically start a thread to perform time-consuming tasks such as loading files, while the main thread resumes message processing. This option is not available in Windows, and is the reason why the hourglass icon is so often displayed.
Having seen the amount of code required for this trivial application, you might be asking yourself how much more is involved in a real application. The answer is, not as much as you might think. And if you play with this program, you'll discover it does quite a lot for so few lines of code: it can be moved around the screen, resized, maximized, minimized, sent to the background, brought to the foreground and it has a system menu that allows you to do the same things from the keyboard. It wouldn't be too difficult to turn it into a program that displays a text file, for example. And from there, adding font support and other fancy features is simply a matter of calling the appropriate functions.
And bear in mind that these functions and messages can do a lot. For example, call WinCreateWindow() a few times to create some entry-field controls and you have an on-screen form with text editing. Or call WinCreateWindow() to create an MLE window: this is a Multiple Line Entry field which provides all the capabilities of a simple text editor, including cut, copy, paste, word wrap, tab stops and undo.
One intriguing capability of PM is subclassing windows. This means redefining some of the message-processing characteristics of a window or adding new capabilities while leaving others unchanged. In essence, it means creating a new class of window which inherits some of the properties of its 'parent', in line with the object-oriented programming concept of inheritance.
For a good example of this, take a look at the standard OS/2 system editor, E.EXE. This is simply an MLE field which has been subclassed to add features such as font and colour selection, as well as file open and save. It contains embarrassingly few lines of code, although I take my hat off to the programmer for being smart enough to do it this way!
Programming PM tends to be frustrating initially, but becomes more and more rewarding as you master the various function calls and build yourself a library of reusable routines. Program design is quite different from conventional procedural code in languages such as C, Pascal or dBASE, as there is no neat hierarchy of functions being called from menus and submenus, nor is there a conventional hierarchical input-process-output organisation. A programmer needs to be disciplined to employ good modular coding techniques, hide data behind windows and avoid globals and statics like the plague.
But once the basic concepts are mastered, the result can be some very slick applications indeed.
Listing 1.
#define INCL_WIN /* This includes the window manager function prototypes, */
/* constants, etc. */
#include <os2.h> /* This is the main OS/2 definitions include file */
/* Prototype for the applications window procedure */
MRESULT EXPENTRY HelloWndProc(HWND hwnd, ULONG ulMessage, MPARAM mp1, MPARAM mp2);
/* We need handles for both the frame window and the client window */
HWND hwndFrame;
HWND hwndClient;
/* Multiple windows can be maintained by one wndproc, so it's a class, rather
than an object. The class is registered by name with the system */
static char szClassName[] = "Hello";
void main()
{
HAB hab; /* Handle to an 'anchor block' */
HMQ hmq; /* Handle to the input message queue */
QMSG qmsg; /* Queue message structure to hold the incoming message */
ULONG ulFrameFlags; /* Frame creation flags */
/* First, register with PM to get services. This returns a handle to an anchor block */
hab = WinInitialize(0);
/* Next, create a message queue */
hmq = WinCreateMsgQueue(hab, 0);
/* Now register a window class for the application */
if(!WinRegisterClass(hab,
(PCH)szClassName,
(PFNWP)HelloWndProc, /* Pointer to winproc function */
CS_SYNCPAINT | CS_SIZEREDRAW, /* Flags to force redraw when resized, etc. */
0)) /* Number of bytes in 'window words' */
DosExit(EXIT_PROCESS, 1);
/* We want a regular window, but without a menu, accelerator table and icon */
ulFrameFlags = FCF_STANDARD & ~FCF_MENU & ~FCF_ACCELTABLE & ~FCF_ICON;
/* Now go ahead and create the window */
/* This function call returns the frame window handle and also sets the client window handle */
hwndFrame = WinCreateStdWindow(HWND_DESKTOP, /* Parent window */
WS_VISIBLE, /* Window style visible */
&ulFrameFlags, /* pointer to frame flags */
(PCH)szClassName, /* Registered class name */
"Hello", /* Text for title bar */
WS_VISIBLE, /* Client window style */
(HMODULE)NULL, /* Pointer to resource module */
0, /* Resource ID within module */
(HWND *)&hwndClient); /* Pointer to client window handle */
if(hwndFrame == 0) {
WinAlarm(HWND_DESKTOP, WA_ERROR);
DosExit(EXIT_PROCESS, 1);
}
/* Now loop around processing events. This is called the 'event loop' */
/* WinGetMsg returns FALSE when it gets the WM_QUIT message */
while(WinGetMsg(hab, &qmsg, 0, 0, 0)) /* Get a message */
WinDispatchMsg(hab, &qmsg); /* Dispatch it to the window */
/* Lastly, clean up */
WinDestroyWindow(hwndFrame);
WinDestroyMsgQueue(hmq);
WinTerminate(hab);
}
/* The client window procedure. Gets called with message components as parameters */
MRESULT EXPENTRY HelloWndProc(HWND hwnd, ULONG ulMessage, MPARAM mp1, MPARAM mp2)
{
HPS hps; /* Handle to a presentation space. This is where a PM program 'draws' */
RECTL rc; /* A rectangle structure, used to store the window coordinates */
CHAR szMessage[] = "Hello World!"; /* The message we'll display */
switch(ulMessage) {
case WM_CREATE: /* process this message by returning FALSE. This lets the */
return (MRESULT)FALSE; /* system continue creating the window */
break;
case WM_ERASEBACKGROUND: /* Let the frame window procedure redraw the background */
return (MRESULT)TRUE; /* in SYSCLR_WINDOW (usually white) */
break;
case WM_PAINT: /* The 'guts' of the application */
hps = WinBeginPaint(hwnd, 0, &rc); /* Get a presentation space */
WinQueryWindowRect(hwnd, &rc);
/* Draw the message, in rectangle rc, coloured CLR_NEUTRAL (black) on
CLR_BACKGROUND (white), centered, over-writing the entire rectangle */
WinDrawText(hps, -1, szMessage, &rc, CLR_NEUTRAL, CLR_BACKGROUND,
DT_CENTER | DT_VCENTER | DT_ERASERECT);
WinEndPaint(hps); /* Release the presentation space */
break;
case WM_CLOSE: /* User chose CLOSE on system menu or double-clicked */
WinPostMsg(hwnd, WM_QUIT, 0, 0); /* So send back WM_QUIT, causing */
break; /* WinGetMsg to return FALSE and exit the event loop */
default: /* Let the system handle messages we don't */
return WinDefWindowProc(hwnd, ulMessage, mp1, mp2);
break;
}
return 0L;
}
Copyright © 1992 Les Bell and Associates Pty Ltd. First published in OS/2 Monthly Vol. 1 No. 1, February 1992.