Introduction and a bare-bones PM application

From EDM2
Jump to: navigation, search

Written by Gavin Baker

Overview

It can be quite a daunting task to sit down and learn to write Presentation Manager programs. There is so much ground to cover, but fear not - getting over the initial learning curve is the hardest part. Once you have a grasp of the basics, it is fairly easy to pick up on more advanced techniques.

Here I intend to provide a starting point for C programmers to get into PM, so that you do have a solid grounding. Now unfortunately there are not many books on PM which explain not just the what, but the why. That is, not just presenting a syntax diagram and saying that (for example) WinBeginPaint is used when you want to paint a window, but why you need it, when to use it (and not WinGetPS), and how to use it.

I will endeavour to explain the why, so you understand exactly what you are doing, instead of blindly copying code from examples. You will see the code, and at each step we will discuss the important sections, so you can see just what each API does. You certainly don't have to remember every single API though (there are hundreds). As you have a need to do something, you will learn the new APIs required to achieve that.

Please remember that, as this is an introductory article, it is not presented as being 100% technically correct and accurate. It is more to give the reader a practical feel for what is going on. Technical references for technical questions appear in the Bibliography. And remember, you can always read the headers!

Scope

I am assuming that you are a competent C programmer, and have a working knowledge of OS/2 from a user's perspective. The sample code here was produced with IBM's C-Set/2, and is ANSI C. I do not assume anything but a familiarity with GUIs in general.

Trademarks etc.

Please note that any trademarks referred to in this article remain the property of their respective companies.

#include <std_disclaimer.h>

Presentation Manager

Presentation Manager is the GUI which sits atop OS/2. It consists mainly of a series of DLLs which contain all the controls, APIs and other stuff which makes PM tick. The WPS is in effect a PM application itself, albeit with certain other special properties. One good thing about PM is that because the bulk of the code to handle the user interface is in DLLs, applications tend to be smaller, as they contain largely just the code to handle the processing of the information. This lets the programmer focus on the "guts" of the program, rather than being concerned with the user interface. Also, PM provides a consistent and intuitive interface for the user along standard design guidelines. Of course, as you will no doubt see, there is still a great deal to learn about PM!

The Event Driven Model

GUIs are modelled on an Event Driven paradigm. This is better understood when contrasted with the sequential nature of conventional programs. Imagine the following fictitious code which might appear in the menu of a program:

DisplayMenu();

while (!kbhit())
    UpdateTime();

c=getch();

if (c==0x27)
    Quit();

You can see that in the while loop, the program is constantly polling the keyboard to check if a key has been pressed. This is a very inefficient practice, as the machine spends most of its time in that loop, doing nothing. In a multitasking environment, this waste of CPU time reduces system throughput, as the CPU could be doing something more useful.

In OS/2 however, programs do not poll for input (whether from the mouse or keyboard). All input is managed by PM itself. This means that when the user clicks the mouse on a button for example, PM handles this event. It puts the event into a queue for that application, and the function which handles this event (the window procedure) only gets called when it has a message waiting for it. As a result, the program only acts when it needs to, rather than constantly checking if it actually has something to do.

So rather than following sequentially through the program, PM programs send, receive and respond to events. This technique requires a shift in thinking, but with practice it will become clear. It is arguably a more intuitive model for programming, and it also promotes good technique.

Windows Everywhere

A Window is simply a rectangular area on the screen. A window can have certain properties or flags, and PM is full of them. They may or may not have a frame or border, a titlebar, or a menu.

You may only think of a window as being the Frame window of an application. However buttons are windows too. So are dialog boxes, list boxes, scrollbars - in fact each control in PM is a window. We will explore this further in a future article, but for now just take it that a window is rectangle.

Don't worry too much if this sounds a bit vague - it will become clear once you start making your own windows.

Getting the Message

The messages discussed in our talk of Events is simply a C struct, which looks like this:

/* Queue Message        */
typedef struct _QMSG
{
   HWND    hwnd;
   ULONG   msg;
   MPARAM  mp1;
   MPARAM  mp2;
   ULONG   time;
   POINTL  ptl;
   ULONG   reserved;
} QMSG;

This struct is a key element in PM. The fields are explained thus:

  • hwnd - Window Handle - unique reference for a given window.
  • msg - A system- or user-defined message
  • mp1 and mp2 - Message parameters (meaning dependent upon the message). Can be a number, a pointer, a handle, etc.
  • time - The time stamp when the event occurred.
  • ptl - The (x,y) coordinates of the mouse.
  • reserved - Just what it says!

All messages have a unique number, and the system messages are all #defined in the OS/2 headers. Some typical messages include:

Message   Description
WM_CHAR   When the user presses a key
WM_CLOSE  If the user chooses to Exit the program
WM_PAINT  When an area of the screen needs to be drawn

You will not normally deal with the QMSG structure directly - your window procedure will handle this, and deals only with the handle, the message and the two parameters (mp1 and mp2).

Having a Handle on Things

A handle is a unique number or reference for any given system object. The most common handles refer to windows. An application can have any number of windows, and handles are a convenient way to keep track of them. Handles are also used internally by PM to manage its own information on the system.

Window Procedures

A Window Procedure is one of the most important parts of any PM application. Its job is to handle all of the messages that get sent to its window. It usually consists of a big switch statement, with a case for each message. You can handle as many or as few messages as you wish, because (fortunately!) one of the APIs in PM is a default window procedure. So any message you don't need or want to handle, you pass on to WinDefWindowProc, which handles it in the default manner.

Well, enough of this banter - let's do something useful and make a PM program.

Your first PM Program

Let's start simply by getting a plain window on screen. I will show you the code, followed by explanations, with the full source at the end of this section.

/* Definitions */

#define INCL_WINFRAMEMGR

#define MYAPP           "MyApp"
#define ID_MYWINDOW     42

If you look at the file OS2.H, you will see a whole bunch of #ifdefs, and #includes. Because the header files are so large and numerous, and you don't always want to include everything since you may not use all of it, all you need do is #define which parts you need to include. There are certain things which are included by default, so here we only need to define INCL_WINFRAMEMGR, for the definition of WM_ERASEBACKGROUND, as all the rest is already there. The most important header is PMWIN.H, so take some time to browse through it as it has some very important things in it.

/* Includes */
#include

Having defined which sections we need from the headers, all we need at the moment is to include OS2.H.

/* Prototypes */

MRESULT EXPENTRY MyWinProc (HWND, ULONG, MPARAM, MPARAM);

We prototype our functions, like all good ANSI programmers!

/* Global Variables */

HAB     hab;
HWND    hwndClient,
        hwndFrame;

The HAB stands for a Handle to the Anchor Block. As you know, a handle is just a reference, but the anchor block is an internal data structure which PM manages. The origin of the name is a little obscure. Anyway, each instance of an application has an HAB. There are few times when you actually use it, usually only during the initialization of your program. For now, let's say it is like a handle to your application.

Now we have two other handles, this time handles to windows. Notice that we have two handles. One for the client, one for the frame. This requires a little more detail.

The Frame Window is the window from the edges of the resizeable frame border. It contains the border, titlebar, minimize and maximize buttons, system menu, and optionally a menu.

The Client Window is the area of whitespace bordered by the frame and the menu. This is the space where information is displayed, and the user interacts with. Take a look at the System Editor for example. The frame window encompasses the entire application window. The client area is the area of white where the text appears (the edit window). Having a separate client window makes it easier to paint, scroll, and other such operations.

/* main function */

void main (void)
{
    HMQ     hmq;
    QMSG    qmsg;
    ULONG   flCreate;

    hab = WinInitialize (0);
    if (!hab)
        return;

Here we launch straight into the main function. First we declare some local variables. The HMQ is a handle to a message queue. When an application is created, it needs a queue to keep the pending messages in, as they await processing by the window procedure.

The QMSG is the data structure we looked at earlier (see Messages). This is used in the main message loop below. The flCreate is used to keep the flags we need to create our window.

The very first thing we do is call WinInitialize. This initializes certain things in PM and gives us an HAB. If we could not get a HAB, something really bad has gone wrong, so we exit immediately.

     hmq = WinCreateMsgQueue (hab, 0);

Here we create our message queue with our HAB, and get a handle to it so we can refer to it later.

     WinRegisterClass(
         hab,
         MYAPP,
         (PFNWP) MyWinProc,
         CS_SIZEREDRAW,
         0);

Because each application behaves differently and has their own window procedure, PM needs to know certain things about your application. This is called registering. You give it your HAB, a unique string (called the class name) which identifies your program, a pointer to your window procedure (which is why you cast it) and any particular flags you may require. These flags are called Class Styles, and CS_SIZEREDRAW means we want our application to be redrawn if it is resized. The last parameter allows us to reserve some space within each window for instance-specific data, but we don't need this yet.

Creating the Window

     flCreate =  FCF_TITLEBAR    | FCF_SYSMENU       |
                 FCF_SIZEBORDER  | FCF_MINMAX        |
                 FCF_TASKLIST    | FCF_SHELLPOSITION ;

     hwndFrame = WinCreateStdWindow(
         HWND_DESKTOP,
         WS_VISIBLE,
         &flCreate,
         MYAPP,
         "Sample PM App",
         0L,
         NULLHANDLE,
         ID_MYWINDOW,
         &hwndClient);

First we set up a variable which has ORed together a bunch of flags (called Frame Control Flags) to tell PM what sort of window we want. They mean (in order) we want a titlebar, a system menu, a resizeable border, both minimize and maximize buttons, we want it to appear in the task list, and we don't care what it's initial size or position is (let the shell decide).

Now comes the actual window creation. We specify the parent window (usually the desktop, as is here). WS_VISIBLE means we want it to be visible, pass the frame control flags, our class name (so it knows which window procedure to use), the title of the window, then any other special flags. NULLHANDLE tells it to look for Resources (such as icons, menus, dialogs, etc.) in the executable itself. We pass the ID of the window (to distinguish it from other windows, and to associate resources with), and finally get a handle to our client window. I hope you're keeping up so far, because we're almost there!

     while (WinGetMsg (hab,&qmsg,(HWND)NULL,0,0))
         WinDispatchMsg (hab,&qmsg);

This is the most crucial part of the program, and is really what gives it the capability to respond to events. It is a constant loop which gets a message from the queue and dispatches it to the window procedure. WinGetMsg will keep going until it receives the WM_QUIT message. WinDispatchMsg actually gets PM to call your window procedure, which is why all window procedures have the same parameters (hwnd, msg, mp1, mp2). The other parameters in WinGetMsg allow you to do fancy things which we won't get into now.

     WinDestroyWindow(hwndFrame);
     WinDestroyMsgQueue(hmq);
     WinTerminate(hab);
 }

This is just for cleanup - once the user exits the application, we clean up and release the resources we used.

/* our window procedure */

MRESULT EXPENTRY MyWinProc (HWND hwnd,
                            ULONG msg,
                            MPARAM mp1,
                            MPARAM mp2)
{

As I mentioned above, because PM calls your window procedure, you must have it declared exactly like this. EXPENTRY tells the compiler that this function will be in the list of exports (a list of "public" functions).

     switch (msg)
     {
         case WM_ERASEBACKGROUND:
             return (MRESULT) TRUE;
             break;

Here we begin our amazing switch statement, which branches on the message. For each case, we have a Window Message. This first one is PM asking if we want it to erase the background for us. If we return true, it will do so. If we want to paint the background (or client area) ourselves, we would return False.

case WM_CLOSE:
   if (WinMessageBox(HWND_DESKTOP,
      HWND_DESKTOP,
      "Are you sure you want to quit?",
      "Sample",
      0,MB_YESNO | MB_QUERY) == MBID_YES)
         WinPostMsg(hwnd,WM_QUIT,0L,0L);
   break;

This case is somewhat special. WM_CLOSE is sent when the user presses Alt-F4, selects Close from the System menu, double clicks on the System menu, selects Close from the Window List, or selects File|Exit from the menu (if there is one). This gives us a chance to ask the user if they are sure they want to quit, and make sure there are no changes to any files they might wish to save. Here we use the WinMessageBox API, passing it the parent and owner, the message we wish to display, the title of the message box, an ID (used for providing Help), and some flags. The flags specify that we want Yes and No buttons, and a Query icon. The call will return MBID_YES if they pressed the Yes button, in which case we send our application the WM_QUIT message. This causes WinGetMsg (in main) to return FALSE, the while loop to terminate, and finally cleanup before exiting.

         default:
             return WinDefWindowProc (hwnd, msg, mp1, mp2);
     }
     return FALSE;
 }

This part makes life easy for us. There are a myriad of other messages that our application receives, and WinDefWindowProc will handle them all for us. This is part of the beauty of the event-driven model - we grab only the messages which interest us, and let PM do the rest. This gives us a standard look and feel, with no extra work.

Now we have written a bare-bones PM application, follow the instructions in the next section to run it.

Instructions

You can extract the source code to a file. To do this, for each file, go to the code, press Ctrl-F. This will create a file called TEXT.TMP in the root directory of the current drive. Go to an OS/2 Window and copy that file to a working directory, renaming it to the respective name (which appears in the title). You should be able to compile and run the program straight away. Just issue the command:

[C:\WORK\PM]nmake /f step01.mak

And then run:

[C:\WORK\PM]step01

You should see a plain window with a titlebar. Closing the window will confirm with a dialog. Once you have seen the program running, go back and reread the section explaining the code, so you can see just what is happening.

What Next?

Now that you have a basic PM application running, in the next issue we will add a menu, some dialogs, and respond to some messages that do something useful. It may seem like a lot of code to just get a plain window to appear, but we will see how easy it is to add features next month.

I welcome any feedback on this article (netmail preferred) - any comments or suggestions you may have, questions on this article, or things you would like to see in a future article. I hope you have learned something!

Bibliography

The following references were used in the preparation of this article: