Jump to content

Introduction and a bare-bones PM application: Difference between revisions

From EDM2
Created page with "Written by Gavin Baker ==Introduction== Welcome to the second installment of our exciting series on PM programming! (Well, I thought I'd better start on a high note ...) I..."
 
No edit summary
Line 1: Line 1:
Written by [[Gavin Baker]]
Written by [[Gavin Baker]]


==Introduction==
==Overview==


Welcome to the second installment of our exciting series on PM programming! (Well, I thought I'd better start on a high note ...)
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.


In this article, I present a simple program which takes advantage of OS/2 threads. It uses one thread to handle interacting with the user, and another thread to do all the work. It simply displays the current time in the middle of the window. It is obviously a trivial prorgam, but nonetheless serves to illustrate one possible use of threads.
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.


Basically you can have one program (process) doing more than one thing (threads) at once. A little like multi-multitasking... Anyway, in terms of the source code, a thread is just a function in your program which gets called in a special way which allows it to go off and run at the same time as the rest of the program. You can imagine how complicated things could get having to co-ordinate things, so OS/2 also provides IPC (Inter-Process Communication) functions to help you.
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.


The program uses other facets of OS/2 not discussed yet in this series (resources, and the Graphics Programming Interface [GPI]) so we will not spend too much time on them since the main focus is on threads. I have included the RC file which defines the dialog box and the menu, but I will leave off explaining resources to the next article.
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==
==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 Borland C++ for OS/2, but should work with most other compilers.
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.==
==Trademarks etc.==
Line 19: Line 19:
Please note that any trademarks referred to in this article remain the property of their respective companies.
Please note that any trademarks referred to in this article remain the property of their respective companies.


<nowiki>#include <std_disclaimer.h> </nowiki>
<nowiki>#include <std_disclaimer.h> </nowiki>


==Processes & Threads==
==Presentation Manager==


A process in OS/2 terms is just an application or program. Because OS/2 is multi-tasking, you can run multiple processes (in their own address spaces) at the same time. A thread is an execution unit within a process. It becomes clearer in this diagram:
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!


Editor's note: Due to time constraints, this diagram was not available in time for publication. It will be made available at a later date.
==The Event Driven Model==


The red shows OS/2 itself. The green represents our program, and the yellow boxes are the threads within it. You would typically have one thread to handle the user interface (the main window procedure), one to do all the work, and perhaps one for printing. Threads can have different priorities, and there are some very powerful functions to enable threads to communicate with each other, to synchronise and co-ordinate events.
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:
 
Threads within a process share the address space, which means the variables and functions within the program have the same scope between two threads. For example, consider these two threads:


  <small>
  <small>
  void Thread1()               void Thread2()
  DisplayMenu();
  {                            {
   
  while (1)                     while (1)
  while (!kbhit())
    i++;                         i--;
      UpdateTime();
}                            }
   
  c=getch();
  if (c==0x27)
      Quit();
  </small>
  </small>


Get the feeling we may not be getting anywhere...? These two threads are playing a "tug-of-war" with the poor old (overused) variable i. The first thread will be forever incrementing i, and the second thread will forever be undoing all that hard work. The variable i will stay around the same value, but won't be exactly zero due to the dynamic prioritising of threads that OS/2 performs - if a Thread1 gets a tiny bit more CPU time, i will go up a little.
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.
 
Threads allow your programs to be much more efficient, and responsive to the user. Here I will show you how.


==The Code==
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.


Right - now we'll go straight to the code. The general idea is to have the main program handle the user interactions, and during initialization we start the worker thread which carries on its merry way despite what the main thread may be doing.
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.


The way we acheive this is by creating the main window itself, and then have the second Worker thread create a dummy window (called an OBJECT WINDOW) which doesn't actually appear on screen, but gives us a window handle and a message queue to work with. This is not the only way to do this, but it is probably the simplest. The main window sends custom messages to the second dummy window on thread 2 to do all the work.
==Windows Everywhere==


Now here we specify which portions of the include files we need, and also include a few others.
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.


<small><nowiki>
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.
#define INCL_WIN
#define INCL_GPI
#define INCL_WINDIALOGS
#include <os2.h>
#include <process.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <dos.h>
#include "step02.h"
</nowiki></small>


Now we need to pass messages between the two threads, so we define our own by starting at WM_USER and going up. This ensures we don't accidentally use a system-defined message.
Don't worry too much if this sounds a bit vague - it will become clear once you start making your own windows.


<small><nowiki>
==Getting the Message==
#define WM_BEGIN_PAINT  WM_USER+1
#define WM_END_PAINT    WM_USER+2
#define WM_ACK          WM_USER+3
</nowiki></small>


Using global variables is generally considered a no-no, unless it can't be avoided. It makes things easier to implement and more self-contained if you keep globals to a bare minimum. We then supply prototypes for our functions.
The messages discussed in our talk of Events is simply a C struct, which looks like this:


  <small>
  <small>
HWND    hwndMain,
  /* Queue Message        */
        hwndWorker;
  typedef struct _QMSG
   
  {
  MRESULT EXPENTRY MainWndProc (HWND, ULONG, MPARAM, MPARAM);
    HWND    hwnd;
VOID            WorkerThread ();
    ULONG  msg;
  MRESULT EXPENTRY WorkWndProc (HWND, ULONG, MPARAM, MPARAM);
    MPARAM mp1;
MRESULT EXPENTRY DlgProc(HWND, ULONG, MPARAM, MPARAM);
    MPARAM mp2;
VOID            WorkPaint (HWND, HPS);
    ULONG  time;
    POINTL ptl;
    ULONG   reserved;
  } QMSG;
  </small>
  </small>


The main function is not very big - all it does is set up some variables, the flags for the window, initialize things for PM, then register and create our window. After we exit our main message loop (by getting a WM_QUIT) we clean up and exit.
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:


  <small>
  <small>
int main (void)
  Message  Description
{
  WM_CHAR  When the user presses a key
    HAB    hab;
   WM_CLOSE If the user chooses to Exit the program
    HMQ    hmq;
  WM_PAINT When an area of the screen needs to be drawn
    HWND    hwndFrame;
    QMSG    qmsg;
    ULONG   flFrameFlags = FCF_TITLEBAR      |
                            FCF_SYSMENU      |
                            FCF_SIZEBORDER    |
                            FCF_MINMAX        |
                            FCF_SHELLPOSITION |
                            FCF_TASKLIST      |
                            FCF_MENU;
    randomize();
    hab = WinInitialize (0);
    hmq = WinCreateMsgQueue (hab, 0);
    WinRegisterClass (hab, "STEP2",
                      MainWndProc, CS_SIZEREDRAW, 0);
    hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                                    WS_VISIBLE,
                                    &flFrameFlags,
                                    "STEP2",
                                    NULL,
                                    0,
                                    NULLHANDLE,
                                    ID_MAIN,
                                    &hwndMain);
    while (WinGetMsg (hab, &qmsg, 0, 0, 0))
            WinDispatchMsg (hab, &qmsg);
    WinDestroyWindow (hwndFrame);
    WinDestroyMsgQueue (hmq);
    WinTerminate (hab);
    return 0;
  }
  </small>
  </small>


Now here is the Window Procedure for the main window itself. The special bit to notice here is the call to _beginthread when we get created (WM_CREATE). This is where the second thread gets started. Note we just pass it the name of the function, and that function will start executing from there all by itself. It can operate more or less like any other function, with a few considerations.
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.


<small>
==Window Procedures==
MRESULT EXPENTRY MainWndProc (HWND hwnd,
 
                              ULONG msg,
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.
                              MPARAM mp1,
                              MPARAM mp2) {
    FILEDLG    fild;
    switch (msg)
    {
        case WM_CREATE:
            if (_beginthread (WorkerThread,
                              8192,
                              NULL) == -1)
            {
                WinMessageBox (HWND_DESKTOP, hwnd,
                    "Creation of second thread failed!",
                    "Step 2",
                    0, MB_OK | MB_CUACRITICAL);
                return 0;
            }
            return 0;
</small>


Here is the first user message we get. This message will be sent by the second thread's window procedure to the main one to say it is all setup and ready to go. We respond by telling it to go ahead and start painting the window.
Well, enough of this banter - let's do something useful and make a PM program.


<small>
==Your first PM Program==
        case WM_ACK:
            WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
            return 0;
</small>


When the main window gets resized, we stop the second thread from painting, and then restart it, causing it to update itself with the new size of the window. Note that we use WinSendMsg first, then WinPostMsg. We send the message first, which calls the target window procedure directly and will not continue until it returns (thus ensuring the message gets processed) and then we Post the message to restart in the second thread's message queue so it can keep going.
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.


  <small>
  <small>
        case WM_SIZE:
  /* Definitions */
            WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0);
   
   
            WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
  #define INCL_WINFRAMEMGR
            return 0;
  #define MYAPP          "MyApp"
  #define ID_MYWINDOW    42
  </small>
  </small>


We get this message when the user drops a font or colour onto the window. We Invalidate the entire window and force a repaint so we can display with the new font/colours.
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.


  <small>
  <small>
        case WM_PRESPARAMCHANGED:
  /* Includes */
            WinInvalidateRect(hwndMain,NULL,TRUE);
  #include
            return 0;
  </small>
  </small>


If the main window needs to be painted, we simply make sure that the second thread gets on with it. Note again the different use of sending and posting the message.
Having defined which sections we need from the headers, all we need at the moment is to include OS2.H.


  <small>
  <small>
        case WM_PAINT:
  /* Prototypes */
            WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0);
   
   
            WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
  MRESULT EXPENTRY MyWinProc (HWND, ULONG, MPARAM, MPARAM);
            return 0;
  </small>
  </small>


In order to simplify things, PM does some of the painting for us if we like - returning true from this message makes PM erase the entire window with the default colour.
We prototype our functions, like all good ANSI programmers!


  <small>
  <small>
        case WM_ERASEBACKGROUND:
  /* Global Variables */
            return (MRESULT) TRUE;
  HAB    hab;
  HWND    hwndClient,
          hwndFrame;
  </small>
  </small>


Now, whenever a user selects something from the menu, we get a WM_COMMAND message. We can then use the SHORT1FROMMP macro (which means extract a short value from the high word of a message parameter) to get the ID of the menu item selected. The first menu selection we process is the most important one - the About box. We call WinDlgBox which does a lot of work for us. It loads the dialog from the resource (check out STEP02.RC), runs the dialog procedure we specify (which works just like a window procedure) and will not return until the dialog is closed. This is called a modal dialog - it will not allow the user to select anything else until they finish with the dialog. Contrast this with a modeless dialog, which can be used at the same time as any other windows. These are often used for floating toolboxes, etc. We pass WinDlgBox the parent window (which will be the desktop), the owner window (our main window), a pointer to the dialog procedure which handles the messages, a handle to the module where the resource is located (by specifying NULLHANDLE OS/2 looks in the EXE file), and any extra info we want to pass the dialog procedure.
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.


<small>
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.
        case WM_COMMAND:
 
            switch (SHORT1FROMMP(mp1))
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.
            {
                case ID_ABOUT:
                    WinDlgBox(HWND_DESKTOP,
                              hwnd,
                              (PFNWP)DlgProc,
                              NULLHANDLE,
                              DLG_ABOUT,
                              NULL);
                    return 0;
</small>


Now we will use one of OS/2's standard dialogs - the File Open dialog. All we do is set up a structure with the appropriate options, and call WinFileDlg. Once it returns we can examine the FILEDLG struct for the file the user selected. This example only displays the dialog - it does nothing with what the user selected.
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.


  <small>
  <small>
                case ID_FILEOPEN:
  /* main function */
                    memset(&fild, 0, sizeof(FILEDLG));
                    fild.cbSize=sizeof(FILEDLG);
  void main (void)
                    fild.fl=FDS_OPEN_DIALOG | FDS_CENTER ;
  {
                    fild.pszIDrive="C:";
      HMQ    hmq;
                    WinFileDlg(HWND_DESKTOP,hwnd,&fild);
      QMSG    qmsg;
                    return 0;
      ULONG  flCreate;
      hab = WinInitialize (0);
      if (!hab)
          return;
  </small>
  </small>


If the user selects Exit from the File menu, we just send ourselves a WM_CLOSE message, which by default will shut down our application.
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.


<small>
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.
                case ID_FILEEXIT:
                    WinPostMsg(hwnd, WM_CLOSE, 0, 0);
                    return 0;
</small>


Any other messages we don't need to worry about, so let PM handle them by passing them on to the default PM window procedure.
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.


  <small>
  <small>
                default:
      hmq = WinCreateMsgQueue (hab, 0);
                    return WinDefWindowProc(hwnd,
                                            msg,
                                            mp1,
                                            mp2);
            }
  </small>
  </small>


Notice how we first destroy the second thread so it can clean up, before we close ourselves.
Here we create our message queue with our HAB, and get a handle to it so we can refer to it later.


  <small>
  <small>
        case WM_DESTROY:
      WinRegisterClass(
            WinSendMsg(hwndWorker, WM_DESTROY,
          hab,
                        mp1, mp2);
          MYAPP,
            return 0;
          (PFNWP) MyWinProc,
        }
          CS_SIZEREDRAW,
    return WinDefWindowProc (hwnd, msg, mp1, mp2);
          0);
}
  </small>
  </small>


Now we get to the interesting bit - our Worker thread. It looks like a normal function, it just gets called differently. Although this example does not cover it, you must keep in mind that having multiple threads in the one program requires some forethought. You can't have two threads trying to write to the one data file, for example. We will explore this problem and how to solve it (using Semaphores) in a later article.
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==


  <small>
  <small>
  VOID WorkerThread ()
      flCreate = FCF_TITLEBAR    | FCF_SYSMENU      |
  {
                  FCF_SIZEBORDER | FCF_MINMAX        |
    HAB  hab;
                  FCF_TASKLIST    | FCF_SHELLPOSITION ;
    HMQ hmq;
   
    HWND hwndObject,
      hwndFrame = WinCreateStdWindow(
           hwndParent;
          HWND_DESKTOP,
    QMSG qmsg;
          WS_VISIBLE,
          &flCreate,
          MYAPP,
          "Sample PM App",
          0L,
          NULLHANDLE,
           ID_MYWINDOW,
          &hwndClient);
  </small>
  </small>


This should look familiar - it looks very much like the main procedure. The only difference is that we specify HWND_OBJECT when we create the window. We need our own message queue so we can talk to the main window. Notice how we ACKnowledge the main window once we get created, and go into our message loop. All the work is actually done in the second thread's window procedure. One special thing to note is the call to _endthread at the end. This lets the C runtime library clean up after us, and shuts down the thread properly.
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!


  <small>
  <small>
    hab = WinInitialize (0);
      while (WinGetMsg (hab,&qmsg,(HWND)NULL,0,0))
          WinDispatchMsg (hab,&qmsg);
    hmq = WinCreateMsgQueue(hab, 0);
    WinRegisterClass(hab, "STEP2_B", WorkWndProc, 0, 0);
    hwndWorker = WinCreateWindow (HWND_OBJECT,
                                  "STEP2_B", "",
                                  0, 0, 0, 0, 0,
                                  HWND_OBJECT,
                                  HWND_BOTTOM,
                                  0, NULL, NULL );
    WinSendMsg( hwndMain, WM_ACK, 0, 0 );
    while( WinGetMsg ( hab, &qmsg, 0, 0, 0 ))
        WinDispatchMsg ( hab, &qmsg );
    WinPostMsg( hwndMain, WM_QUIT, 0, 0 );
    WinDestroyWindow( hwndWorker );
    WinDestroyMsgQueue( hmq );
    WinTerminate (hab);
    _endthread ();
}
  </small>
  </small>


This is the dialog procedure for the About box. It is just the same as a window procedure except that for the messages we don't process, we call WinDefDlgProc instead of WinDefWindowProc because certain things have to be handled differently. If the user presses a button in the dialog, we get a WM_COMMAND much the same as if they selected a menu item. We know that the OK button is the only button that will do anything so we don't bother checking and just close the dialog by calling WinDismissDlg. We pass it the handle of the dialog, and a BOOLean value which will be returned to the calling function (back at WinDlgBox) so we can tell if the user pressed OK or Cancel.
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.


  <small>
  <small>
MRESULT EXPENTRY DlgProc(HWND hwnd, ULONG msg,
      WinDestroyWindow(hwndFrame);
                          MPARAM mp1, MPARAM mp2)
      WinDestroyMsgQueue(hmq);
{
      WinTerminate(hab);
    switch (msg)
  }
    {
        case WM_COMMAND:
            WinDismissDlg(hwnd,TRUE);
            return 0;
        default:
            return WinDefDlgProc(hwnd,msg,mp1,mp2);
    }
}
  </small>
  </small>


This is the window procedure for the "dummy" OBJECT window which the second thread creates. We have to set up a few things when we get CREATEd. Firstly, we ask PM for a timer. We give it a handle to the anchor block (which we get from our handle), the handle itself, an ID for the timer (you can have more than one), and the delay in milliseconds between timer "ticks". We ask it to send us a WM_TIMER once a second, so we can update our clock. The next thing we do is get ourselves a Presentation Space (PS), which is like a handle which is used when we want to draw or paint in the window. We then say that we want the background to be cleared when we draw things. Otherwise the time we display would overwrite itself and soon become garbled. I will not go into to much detail on the drawing side of things, as the GPI (Graphics Programming Interface) itself could fill a book (and has).
This is just for cleanup - once the user exits the application, we clean up and release the resources we used.


  <small>
  <small>
  MRESULT EXPENTRY WorkWndProc(HWND hwnd,
  /* our window procedure */
   
  MRESULT EXPENTRY MyWinProc (HWND hwnd,
                               ULONG msg,
                               ULONG msg,
                               MPARAM mp1,
                               MPARAM mp1,
                               MPARAM mp2) {
                               MPARAM mp2)
    static BOOL Paint=FALSE;
  {
    static HPS  hps;
    SIZEL      sizel;
    switch (msg)
    {
        case WM_CREATE:
            if (!WinStartTimer(WinQueryAnchorBlock(hwnd),
                                hwnd,
                                1,
                                1000))
                WinMessageBox(HWND_DESKTOP,
                              hwnd,
                              "Could not start timer!",
                              "Error",
                              0,
                              MB_CUACRITICAL | MB_OK);
            hps = WinGetPS(hwndMain);
            GpiSetBackMix(hps, BM_OVERPAINT);
            return 0;
  </small>
  </small>


WM_BEGIN_PAINT is our first user message sent to us from the main window. All we do is set a flag saying it is OK to keep painting. If we get WM_END_PAINT then we stop painting for the moment (even while we are still getting WM_TIMER messages).
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).


  <small>
  <small>
        case WM_BEGIN_PAINT:
      switch (msg)
            Paint = TRUE;
      {
            return 0;
          case WM_ERASEBACKGROUND:
              return (MRESULT) TRUE;
        case WM_END_PAINT:
              break;
            Paint = FALSE;
            return 0;
  </small>
  </small>


Every second, we will get a WM_TIMER message, and all we do is check if it is Ok to paint, and then do so.
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.


  <small>
  <small>
        case WM_TIMER:
case WM_CLOSE:
            if (Paint)
    if (WinMessageBox(HWND_DESKTOP,
                WorkPaint(hwndMain, hps);
      HWND_DESKTOP,
            return 0;
      "Are you sure you want to quit?",
      "Sample",
      0,MB_YESNO | MB_QUERY) == MBID_YES)
          WinPostMsg(hwnd,WM_QUIT,0L,0L);
    break;
  </small>
  </small>


If we get closed, make sure we clean up by stopping our timer and releasing the PS we got to draw with. Cleanup is always very important, and could potentially cause some nasty bugs which may not be obvious. Always consult the PM Reference manual for functions you may use which require resources to be released or destroyed.
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.


  <small>
  <small>
        case WM_DESTROY:
          default:
            WinStopTimer(WinQueryAnchorBlock(hwnd),
              return WinDefWindowProc (hwnd, msg, mp1, mp2);
                          hwnd, 0);
      }
            WinReleasePS(hps);
      return FALSE;
            return 0;
  }
    }
    return WinDefWindowProc (hwnd, msg, mp1, mp2);
}
  </small>
  </small>


This function does the painting for us. We get the size of the main window (a RECTangLe), then calculate its width and height.
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.


<small>
Now we have written a bare-bones PM application, follow the instructions in the next section to run it.
VOID WorkPaint (HWND hwnd, HPS hps)
 
{
==Instructions==
    ULONG  x, y, cx, cy;
    RECTL  rect;
    POINTL  ptl;
    char    s[42];
    struct time  t;
    WinQueryWindowRect(hwnd, &rect);
    cx = rect.xRight - rect.xLeft;
    cy = rect.yTop - rect.yBottom;
</small>


We check what the current time is, and compose our string.
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:


  <small>
  <small>
    gettime(&t);
  [C:\WORK\PM]nmake /f step01.mak
    sprintf(s,"Current Time: %2.2d:%2.2d%colon.%2.2d",
            t.ti_hour,t.ti_min,t.ti_sec);
  </small>
  </small>


We want the time to be shown roughly in the middle of the screen, so we figure out where that is. We then randomly pick a colour and display the string at the specified point.
And then run:


  <small>
  <small>
    ptl.x=(cx/3); ptl.y=(cy/2);
  [C:\WORK\PM]step01
    GpiSetColor(hps, rand() % 16);
    GpiCharStringAt(hps, &ptl, strlen(s), s);
}
  </small>
  </small>


==Prologue==
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.
 
Well, that's it. It's not a particularly exciting program, and it could probably be more efficient, but it does provide a general skeleton for a multi-threaded PM application. Study the structure of the program, in blocks. The main function which sets up and creates the window, the main window procedure which handles all the messages and delegates work through user messages to the worker thread. Then the worker thread which sets up the object window, and its corresponding window procedure which does all the work.
 
Try a few things. First, drop a different font on the time. Then try a colour (this will only last until it updates itself, since it changes colour itself). Then bring up the About box and move it so you can see the time still updating itself.
 
Something we have not discussed is thread states. Basically, a thread can either be running (the currently executing thread of which there can only ever be one), ready to run, or blocked (waiting for something to happen). This makes for very efficient programming. Imagine a terminal program. One thread can be handling the user interface, one doing general work, and another monitoring the serial port for input. It would call DosRead from COM1 for example. If there is nothing to read, it doesn't have to sit in a loop and poll the port. OS/2 will block the thread and drop its priority until DosRead returns with something, so it won't get any CPU time until it needs it.
 
The possibilities for threads are endless - background recalculating, background printing, etc. are just some. See how threads could improve your programs.


==What Next?==
==What Next?==


There is a lot of ground to cover in this area, and we have only scratched the surface. But fear not - help is at hand! (You'll just have to wait until next month...)
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!
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!
Line 437: Line 298:
The following references were used in the preparation of this article:
The following references were used in the preparation of this article:


* Borland C++ for OS/2 Documentation
* OS/2 Version 2.0 - The Redbooks
* OS/2 Version 2.0 - The Redbooks
* OS/2 Version 2.0 Technical Library
* OS/2 Version 2.0 Technical Library


[[Category:PM Articles]]
[[Category:PM Articles]]

Revision as of 04:07, 7 March 2012

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:

  • OS/2 Version 2.0 - The Redbooks
  • OS/2 Version 2.0 Technical Library