User Interface Class Library (UICL for short)

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

Welcome to the latest EDM/2 feature, OOPS Corner. As the title suggests, this column is dedicated to exploring various aspects of using C++ to make OS/2 programming easier. I have taken the liberty of assuming that the reader has a good working knowledge of C++ concepts and has C++ coding experience.

So What's On The Menu For This Issue?

We'll start the column off by looking at some of the basics of the User Interface Class Library (UICL for short) provided by IBM and included with the C-Set++ compiler product. Although, this library is C-Set++ specific, I will try to make this interesting for everyone by touching on some of the design aspects behind the library and discussing generic C++ considerations. Additionally, since the best way of learning is by example, we will look at extending the library by adding a generic dialog class.

The Basics of Event Handling

What Are Events?

The UICL uses the concept of events to encapsulate messages sent by the various PM controls. The base object in the event hierarchy is called the IEvent class. When looking at the UICL, one should always keep in mind the basics of the underlying PM API; even though the encapsulation of the PM message structure is done to a level which could allow the UICL to be ported to different platforms, it is still firmly rooted in PM.

PM messages are composed of three 32-bit numbers: the first is the message identifier, while the remaining two are used as the message data. Depending upon the message identifier, the message data can have many different interpretations. Part of the event encapsulation relates the message identifier to the specific meaning of the other two numbers.

Events are grouped into classes based on semantic relationships (i.e. menu-related, notification, etc.) or originator (i.e. container) and each event class is processed by one or more member functions denoted event handlers. The base handler class is called IHandler and it contains the virtual member function dispatchHandlerEvent(). When a window has one or more handlers associated with it, the primary dispatch function calls dispatchHandlerEvent() for each registered handler. (See my article [../0104/cppencap.html C++ Encapsulation of PM in volume 1 issue 4], for the basic design goals, and a definition of the primary dispatch function). If true is returned, the event is considered processed; otherwise, the next handler is invoked, and so on until true is returned from dispatchHandlerEvent().

Planes, Trains, Events and Handlers

Well planes and trains don't really fit with the UICL, but the title sounded good :). To understand handlers, let's use the ICommandHandler class as an example. Following is a stripped down version of the class definition given in icmdhdr.hpp.

 class ICommandHandler : public IHandler {
 typedef IHandler
    Inherited;
 
 public:
    ICommandHandler();
    virtual ~ICommandHandler();
    Boolean dispatchHandlerEvent( IEvent& event );
 
 protected:
    virtual Boolean command( ICommandEvent& event );
    virtual Boolean systemCommand( ICommandEvent& event );
 };

When called, the function dispatchHandlerEvent() acts like a filter. It checks the message ID of the event and calls the appropriate virtual function. If the message is WM_COMMAND, it returns the value returned by the virtual function command(); if the message is WM_SYSCOMMAND, it returns the value returned by the virtual function systemCommand(). Otherwise, it returns the value false indicating that the next registered handler should be invoked.

Figure 1) Event Dispatch Function Chain

What's This ICommandEvent Object?

A quick peek at the PM reference, yields the following information about the WM_COMMAND and WM_SYSCOMMAND messages:

 param1
    USHORT usCmd         Command value.
 
 param2
    USHORT usSource      Source type.
    USHORT usPointer     Pointing-device indicator.
 
 returns
    ULONG  flReply       Reserved.

The ICommandEvent allows access to the value in param1 through the member function commandId(). Similarly, the member function source() allows access to usSource although the value returned is also abstracted in order to be independent of the PM definitions for the CMDSRC_* constants. So not only does the dispatchHandlerEvent() function act as a filter, it also transforms generic IEvent's into specific event objects, as in the ICommandEvent example.

Notice the flReply variable above. It is set using the IEvent member function setResult(). This is different than the way a window procedure in C returns flReply to PM.

Moving On Up

One thing "missing" from the UICL is a dialog window object. It isn't really missing, it's just not packaged as a dialog class. Dialog windows pose interesting encapsulation problems for the class designers - do they allow the class users to use a PM-like method of communicating to the controls? Or do they force you to create instances of C++ objects that mirror the controls? The UICL uses the second approach. Being somewhat of a rebel, I decided that I didn't like that approach I created the IDialogWindow class. The IDialogWindow class is specifically designed to break the predefined rules of encapsulation. The price you pay is that the application becomes tightly bound to the PM messaging structure. Following is the class definition for IDialogWindow.

Note: _exp_ is used to toggle the header file between "DLL" mode and "EXE" mode.

 class IDialogWindow:  public IFrameWindow, public IFrameHandler {
 
 public:
    _exp_ IDialogWindow(unsigned long ID);
    _exp_ IDialogWindow(unsigned long ID, IWindow *Parent, IWindow *Owner);
    _exp_ ~IDialogWindow();
 
    IDialogWindow& _exp_ setItemText(unsigned long ID, char *Text);
    IDialogWindow& _exp_ sendItemMessage(unsigned long ID,
                                         unsigned long iMess,
                                         unsigned long mp1=0,
                                         unsigned long mp2=0);
    IDialogWindow& _exp_ sendItemMessage(unsigned long ID,
                                         unsigned long iMess,
                                         unsigned short int ch1,
                                         unsigned short int ch2,
                                         unsigned long mp2=0);
    IDialogWindow& _exp_ sendItemMessage(unsigned long ID,
                                         unsigned long iMess,
                                         void* mp1,
                                         void* mp2=0);
 
    IDialogWindow& _exp_ queryItemText(unsigned long ID,
                                       unsigned long maxLen,
                                       char *retBuf);
    unsigned long _exp_ queryItemTextLength(unsigned long ID);
 
 protected:
    Boolean _exp_ dispatchHandlerEvent(IEvent& evt);
 };

The member functions mimic the WinSetDlgItemText(), WinQueryDlgItemText(), WinSendDlgItemMsg(), etc. functions. The IDialogWindow class is derived from both the IFrameWindow and the IFrameHandler object. If you are already familiar with the UICL, you know that the IFrameWindow class can create dialog windows if the appropriate constructor is called. Thus, it makes sense that we would subclass it. But why is the IFrameHandler class used as a parent? The answer is easy - the IDialogWindow has the added feature of hiding controls that may obscure the icon when the window is minimized. In order to be notified whenever we are minimized, we need to override the IFrameHandler::dispatchHandlerEvent() member function. An alternative method, would have been to create another handler class that would take care of this case.

Peeking at the Constructors

The constructors for IDialogWindow merely call the correct IFrameWindow constructor, and register the IDialogWindow instance as a handler.

 IDialogWindow::IDialogWindow(unsigned long ID) :
    IFrameWindow(IResourceId(ID))
 {
    handleEventsFor(this);
 }
 
 IDialogWindow::IDialogWindow(unsigned long ID,
                              IWindow *Parent,
                              IWindow *Owner) :
    IFrameWindow(IResourceId(ID),Parent,Owner)
 {
    handleEventsFor(this);
 }

Breaking the Rules

As I said in the beginning of this section, the IDialogWindow is specifically designed to break some of the original design choices. In keeping with this rebellious spirit, we break yet another design decision. Way back, when we looked at handlers and events, we seen that one of the tasks a handler performed was to repackage the event and then pass it on to a specific virtual function that would do the actual processing of the event. The PM message we are after here is WM_MINMAXFRAME; this message is sent every time the window is minimized, restored, or maximized. If we were to follow with the original design, we should have created a new event object to encapsulate this PM message. But because we only want to process this message in the IDialogWindow object, we take a short cut. Following is the modified dispatchHandlerEvent() function:

 Boolean IDialogWindow::dispatchHandlerEvent(IEvent& evt)
 {
    HWND hwnd;
    HENUM henum;
    PSWP pswp;
    Boolean ShowState;
    SWP winPos,Pos;
    int cx,cy;
 
    // check the message
    if (evt.eventId()==WM_MINMAXFRAME) {
       // start the child window enumeration
       henum=WinBeginEnumWindows(handle());
 
       //extract the PSWP paramter from the event
       pswp=(PSWP)evt.parameter1().asUnsignedLong();
 
       //check if the window is being minimized or not
       if ((pswp->fl) & SWP_MINIMIZE)
          ShowState=false;
       else
          ShowState=true;
 
       // Just for fun, query the dialog window's position
       WinQueryWindowPos(handle(),&Pos);
 
       // loop through all child windows
       while (hwnd=WinGetNextWindow(henum)) {
          // query the child windows position
          WinQueryWindowPos(hwnd,&winPos);
 
          // check if the child window overlaps the icon and hide/restore
          // if necessary
          if (((winPos.x)<=pswp->cx) &&
              ((winPos.y)<=pswp->cy))
             WinShowWindow(hwnd,ShowState);
       }
 
       // all children are done, end the enumeration
       WinEndEnumWindows(henum);
    }
 
    // be sure to pass the event to the proper handler!
    return IFrameHandler::dispatchHandlerEvent(evt);
 }

OK. OK. We didn't break the rules, we just bent them a bit. The call to IFrameHandler::dispatchHandlerEvent() insures that the events get to the other IFrameHandler instances that the user may have registered.

Getting WM_CONTROL Messages

In some cases, we may want to get the WM_CONTROL messages in their PM form. To do this, we need to create a new handler class that will pass unmodified WM_CONTROL messages to our handlers. The following handler class performs this task, and is modelled after the ICommandHandler class.

 class ControlHandler: public IHandler {
 
 public:
    Boolean _exp_ dispatchHandlerEvent(IEvent& evt);
 
 protected:
    virtual Boolean _exp_ control(const IControlEvent &control);
 };
 
    :
    :
 
 Boolean ControlHandler::dispatchHandlerEvent(IEvent& evt) {
    // verify message ID
    if (evt.eventId()==WM_CONTROL) {
       IControlEvent controlEvnt(evt);
 
       // call control member function
       Boolean rc=control(controlEvnt);
 
       evt.setResult(controlEvnt.result());
       return rc;
    }
    return false;
 }
 
 // Default handler returns false to indicate the message (event) has not
 // been processed.
 Boolean ControlHandler::control(const IControlEvent &control) {
    return false;
 }

Keeping with the original design of the UICL, to create an instance of the handler, one does not override the dispatchHandlerEvent() function, but rather some other go-between function. In this case, the member function control() is overridden.

Putting It All Together

So far we've examined some of the underlying design decisions in the UICL, and found ways to work around them. A new dialog window class has been developed to allow one work around, and to enhance the basic dialog window support already present in the UICL. The file [dlgdem.zip dlgdem.zip] contains a sample application that uses this class.

Fun For The Reader

For those interested in testing their understanding, modify the sample application to process WM_CONTROL messages. In particular, watch for the BN_DBLCLICKED notification message coming from the exit button. When this message is received (ie. a ControlEvent has occurred), change the text in the button.

Hint
You will need to disable the processing of the exit button in the command() member function, and subclass the ControlHandler object.

Source Files

The file idialog.zip contains the files:

  • idialog.cpp
  • idialog.def
  • IDIALOG.DEP
  • idialog.hpp
  • IDIALOG.MAK

The idialog.mak file should be passed through NMAKE before making the demo program.

The file dlgdem.zip contains the files:

  • DlgDem.def
  • DlgDem.dep
  • dlgdem.ICO
  • DlgDem.mak
  • DlgDem.RC
  • DlgDemMain.cpp
  • DlgDemMain.h
  • DlgDemRC.H
  • main.cpp

No executables are included, because you must have the C-Set++ compiler to have the UICL run time DLL files.

Wrapping Things Up

Here we are again, at the end of another C++ adventure. You should now understand how event and handler objects encapsulate the PM message structure, how to create new window classes, and how to pull a few tricks to make the UICL fit your style.

What's up for the Next Issue?

I'm not too sure what we'll look at next time. If my copy of C-Set++ 2.1 finally arrives, an in-depth review of it will be presented. If it doesn't, we'll look at either more UICL topics, or encapsulating extended attributes.