C++ Encapsulation of PM

From EDM2
Revision as of 23:55, 19 March 2018 by Ak120 (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

The basic goal of encapsulating the PM interface is relating the non-C++ based instance information into a C++ based form. Specifically, in PM, each window has its own unique handle. In C++, each instance of a window would have its own unique this pointer. Thus, the goal in encapsulating PM, at the simplest level, is to relate the handle passed to the window procedure to the C++ instance.

There are two methods by which the C++ "version" of the window can be associated with the PM "version" which are discussed below, followed by a detailed look at the implementation for one of these two methods. This purpose of this article is to promote the usage of the objects described here; it is merely to illustrate some of the concepts of PM encapsulation and some of the methods that can be used to implement this encapsulation.

Having said this, the objects illustrated here are only part of my collection of OS/2 objects, which are used in other programming projects. The primary design consideration behind this object hierarchy is to eliminate as much tedium as possible with a minimization in complexity and performance degradation. Also, the development of this library is not my primary goal; the library grows as I need more power or features in my other programs.

Given sufficient interest, future articles could be written detailing the encapsulation of other controls, thread manipulation in C++, and extending the object hierarchy.

Encapsulation Strategies

Method 1

PM allows the application to allocate additional storage space that the application can use to store window instance related data in (called window words). The first method of window encapsulation would use this additional storage space to hold this information.

The problem with this approach is that in order to access this extra storage space, the application must know the window's handle. Generally, this is not a problem but the C++ dispatch function has to rely on having a valid pointer. The first opportunity the application has to validate the pointer is not until the WM_CREATE message is received. More importantly, while creating new windows, the application cannot assume that any message received has a valid pointer until the window being created has received the WM_CREATE message. During this time, the application must rely on method 2 to dispatch the messages to the appropriate window instance.

Method 2

This method does not use any additional storage space allocated by PM. Instead, the application maintains a list of handles and their corresponding instances and window procedures. This is the approach taken here.

Support Objects

The class hierarchy can be divided into two principle sections: 1) the objects that are used to support the windowing objects and 2) the window objects themselves. The support objects will be described first so that their usage can be seen when the window objects are detailed later. Some of these objects merely encapsulate individual data items used by PM into single objects while others implement the primary message dispatching functions.

PMainThread

This class is used to register the thread with PM and to create its message queue.

class PMmainThread {
   protected:
      HAB hab;
      HMQ hmq;

   public:
      PMmainThread()
      {
         //----------------------------------------------------------------
         // Either of the two indicate an error has occurred.
         //----------------------------------------------------------------
         if ((hab = WinInitialize(0)) == NULLHANDLE)
            exit(1);
         if ((hmq = WinCreateMsgQueue(hab,0)) == NULLHANDLE)
            exit(1);
      }

      virtual ~PMmainThread()
      {
         WinDestroyMsgQueue(hmq);
         WinTerminate(hab);
      }

      BOOL MessageLoop(void);
      HAB GetHAB() { return hab; }
      HMQ GetHMQ() { return hmq; }
};

Also, this class provides a generic message loop that can be used by the application.

MessageStruct

This class simply encapsulates the message parameters sent by PM into a single structure.

struct MessageStruct {
   BaseWindow *TheWin;
   ULONG iMessage;
   MPARAM mParam1;
   MPARAM mParam2;

   MessageStruct(BaseWindow *TW,ULONG iMess, MPARAM mP1, MPARAM mP2) {
      TheWin=TW;
      iMessage=iMess;
      mParam1=mP1;
      mParam2=mP2;
   }
};

Message Dispatch Objects

WinEventHandler

It is desirable to have a single generalized dispatch object that can be referred to by a pointer which would then hide the multi-threading and message dispatching from the rest of the code. While it would be extremely difficult for a single object to do all this, a hierarchy of objects can be used instead. The parent of this hierarchy follows:

class WinEventHandler {
   public:
      static const char Action_Chain;
      static const char Action_CallDef;

      WinEventHandler() {};

      HAB GetAppHAB() { return (BaseWindow::App)->GetHAB(); }
      virtual HAB GetHAB()=0;
      virtual void SetHAB(HAB hb) {}

      virtual int MultiThread()=0;
      virtual MRESULT Run(MessageStruct &Msg,char &Action)=0;
};

The above class provides a generic interface to objects which actually implement the code necessary to dispatch a message in a transparent manner. It should be noted that it is an abstract class.

WinNewEventHandler (template)

Generally, since all derived classes need to implement the pure virtual functions of their parent abstract class, object class templates are used to allow the message handling function to be a non-virtual member function. The template below assumes that the message dispatch function belongs to the class of the window object and it additionally specifies that a new thread should be created to process the messages.

template<class C1> class WinNewThreadHandler: public WinEventHandler {
      HAB hab;
      MRESULT (C1:: WinProc)(WinEventHandler &, MessageStruct &, char &);

public:
      WinNewThreadHandler(MRESULT (C1::* Proc)(WinEventHandler &,
                                               MessageStruct &,
                                               char &)): WinEventHandler()
      { WinProc=Proc; }

      virtual int MultiThread() { return 1; }

      virtual MRESULT Run(MessageStruct &Msg,char &Action) {
         return (((C1 *)(Msg.TheWin))->*WinProc)(*this,Msg,Action);
      }

      virtual HAB GetHAB() { return hab; }

      virtual void SetHAB(HAB hb) { hab=hb; }
};

WinSameEventHandler (template)

This object is similar to the object WinNewThreadHandler in every aspect except that the message processing function is dispatched in the same thread as the primary message dispatch function.

template<class C1> class WinSameThreadHandler: public WinEventHandler {
   MRESULT (C1::* WinProc)(WinEventHandler &,
                           MessageStruct &,
                           char &);
   public:
      WinSameThreadHandler(MRESULT (C1::* Proc)(WinEventHandler &,
                                                MessageStruct &,
                                                char &)): WinEventHandler()
  { WinProc=Proc; }


      virtual int MultiThread() { return 0; }

      virtual MRESULT Run(MessageStruct &Msg,char &Action) {
         return (((C1*)(Msg.TheWin))->*WinProc)(*this,Msg,Action);
      }

      virtual HAB GetHAB() { return GetAppHAB(); }
};

WinNewEventHandlerDifInst (template)

In some cases, it is desirable to have the function which processes the message belonging to an object type different than that of the window. The following object allows this type of scenario to occur in a multi-threaded nature.

template<class C1> class WinNewThreadHandlerDifInst: public WinEventHandler
{

   HAB hab;
   C1 *TheClass;

   MRESULT (C1::* WinProc)(WinEventHandler &,
                           MessageStruct &,
                           char &);
   public:
      WinNewThreadHandlerDifInst(C1* Inst,
                                 MRESULT (C1::* Proc)(WinEventHandler &,
                                                      MessageStruct&,
                                                      char &)):
  WinEventHandler() {
          TheClass=Inst;
          WinProc=Proc;
        }

        virtual int MultiThread() { return 1; }

  virtual MRESULT Run(MessageStruct &Msg,char &Action) {
          return (TheClass->*WinProc)(*this,Msg,Action);
  }

  virtual HAB GetHAB() { return hab; }

  virtual void SetHAB(HAB hb) { hab=hb; }
};

WinSameEventHandlerDifInst (template)

Like the above, this object allows mixing of object types. The difference here is that the message is processed in the same thread that it is dispatched.

template<class C1> class WinSameThreadHandlerDifInst: public
WinEventHandler {

   C1 *TheClass;
   MRESULT (C1::* WinProc)(WinEventHandler &,
                           MessageStruct &,
                           char &);
   public:
      WinSameThreadHandlerDifInst(C1* Inst,
                                  MRESULT (C1::* Proc)(WinEventHandler &,
                                                       MessageStruct &,
                                                       char &)):
  WinEventHandler() {
          TheClass=Inst;
          WinProc=Proc;
        }

      virtual int MultiThread() { return 0; }

      virtual MRESULT Run(MessageStruct &Msg,char &Action) {
         return (TheClass->*WinProc)(*this,Msg,Action);
      }

      virtual HAB GetHAB() { return GetAppHAB(); }
};

Handler Thunks

To create a thread under Borland C++ for OS/2 or IBM C Set++, one has to use the function _beginthread(). Note that the function has different parameters in the two compilers. This function does not make any provision for starting threads on C++ member functions, so a helper function is needed. The helper function takes an object of the following class as an argument and dispatches the multi-threaded message handler by issuing a call to (TheHandler->run)(*TheMessage,temp).

struct HandlerThunkData {
   WinEventHandler *TheHandler;
   MessageStruct *TheMessage;

   HandlerThunkData(WinEventHandler * TH,MessageStruct * TM) {
      TheHandler=TH;
      TheMessage=TM;
   }
};

WindowInfo

This class is used to relate the window handle to the corresponding C++ window instance.

struct WindowInfo {
   HWND hwnd;
   BaseWindow *THIS;

   WindowInfo(HWND hw, BaseWindow * BW) {
      hwnd=hw;
      THIS=BW;
   }
};

InitializationData

Objects of this class are passed to the window procedures so that when the WM_INITDLG or WM_CREATE message is received, the window event handler can be registered enabling it to process these messages.

struct InitializationData {
   BaseWindow *BW;
   WinEventHandler *Event;

   InitializationData(BaseWindow *B, WinEventHandler *E) {
      BW=B;
      Event=E;
   }
};

Window Objects

Because the common denominator between all type of windows in PM is the window handle, we will begin by creating a generic window object which encapsulates the translation of the window handle to the instance pointer of the C++ object. This generalized class also stores the window's handle.

In PM, nearly every window consists of a frame window and the client window. This is so common, in fact, that a special function - WinCreateStdWindow - exists in PM to create the frame and client windows simultaneously. However, a frame window is merely an instance of the window class WC_FRAME allowing us to create it separately using the WinCreateWindow function. Because of this, our C++ objects can be separated into frame windows and client windows.

The window hierarchy is based upon the object BaseWindow. Since this seems to be the logical place to start, we will first describe this class.

BaseWindow

class BaseWindow {
   protected:

      static PApplication App;        // Pointer to Primary window thread
      object

      static ULONG NumWindows;        // Each instance has a handler
      static ULONG MaxNumWindows;     // list
      static WindowInfo **Instances;

      USHORT NumHandlers;
      USHORT MaxNumHandlers;
      WinEventHandler **Handlers;

      HWND hWndFrame;                 // Window handle
      HWND hWndParent;

      PSZ pszClassName;               // Window-class name
      ULONG flClassStyle;             // Default-window style
      USHORT usExtra;                 // Reserved storage

      static MRESULT EXPENTRY _export PrimWndProc(HWND hWnd,
                                                  ULONG iMessage,
                                                  MPARAM mParam1,
                                                  MPARAM mParam2);

      static void AddWindow(BaseWindow *win, HWND hand);
      static void RemoveWindow(BaseWindow *win, HWND hand);

   public:
      BaseWindow();

      BaseWindow(PSZ ClassName, ULONG ClassStyle, USHORT Extra, HWND Parent);

      BaseWindow(PSZ ClassName, HWND Parent=HWND_DESKTOP);

      virtual ~BaseWindow();

      void Show();
      void Hide();
      void Close();

      static void SetApplication(PApplication applic) {
         App=applic;
      }

      static PApplication GetApplication() { return App; }
      void AddStyle(ULONG style) { flClassStyle|=style; }

      BOOL Register() {
         return WinRegisterClass(App->GetHAB(),
                                 pszClassName,
                                 &PrimWndProc,
                                 flClassStyle,
                                 usExtra);
      }

      HWND GetHandle() { return hWndFrame; }
      HWND GetParentHandle() { return hWndParent; }

      void ChangeIcon(ULONG Icon);

      void AddHandler(WinEventHandler *Event);

      BOOL PostMessage(ULONG iMess, MPARAM mP1=0, MPARAM mP2=0) {
         return WinPostMsg(hWndFrame,iMess,mP1,mP2);
      }

      MRESULT SendMessage(ULONG iMess, MPARAM mP1=0, MPARAM mP2=0) {
         return WinSendMsg(hWndFrame,iMess,mP1,mP2);
      }

      //-------------------------------------------------------------------
      // In CSET, a static member function cannot be used as a window
      // procedure thus they are friends here.  But above, the static
      // functions are used in BC4OS2
      //-------------------------------------------------------------------
      friend MRESULT EXPENTRY PrimWndProc(HWND hWnd,
                                          ULONG iMessage,
                                          MPARAM mParam1,
                                          MPARAM mParam2);

      friend MRESULT EXPENTRY PrimDlgProc(HWND hWnd,
                                          ULONG iMessage,
                                          MPARAM mParam1,
                                          MPARAM mParam2);

   friend WinEventHandler;
   friend class ModalDlgBox;
};

Now let's look at exact role this class fills. The data objects and member functions

  • NumWindows
  • MaxNumWindows
  • Instances
  • PrimWndProc
  • AddWindow
  • RemoveWindow

store and manipulate the list of created windows. The window information is stored in an array which is unsorted and traversed by the function PrimWndProc each time a message is received. When the handle is matched, the instance of the window is known. At this time, the list of registered window handlers is traversed.

The following data objects and functions are used to manipulate the list of registered handlers:

  • NumHandlers
  • MaxNumHandlers
  • Handlers
  • AddHandler

In summary, the window instance is registered using the function AddWindow. Message handlers are registered using the function AddHandler. Each time a message is received, the list of registered windows is traversed. If the window handle is found, the list of handlers corresponding to the instance found is traversed and each message handler is allowed to process the message until one of them block further processing.

ModalDlgBox

The dialog window creation process can also be encapsulated. The following class encapsulates the process of creating a modal dialog window contained in the application's resources. A similar class exists for modeless dialog windows. Also, classes exist for both type of dialog windows with menus.

The function PrimDlgProc is identical to the function PrimWndProc except that this function calls the default dialog procedure for unprocessed messages and looks for the WM_INITDLG message whereas the latter calls the default window procedure and looks for the WM_CREATE message.

class ModalDlgBox: public BaseWindow {
   protected:
      ULONG RetVal;

      static MRESULT EXPENTRY _export PrimDlgProc(HWND hWnd,
                                                  ULONG iMessage,
                                                  MPARAM mParam1,
                                                  MPARAM mParam2);

   public:
       ModalDlgBox(BaseWindow *Owner,
                   ULONG ResID,
                   HMODULE ResMod=NULLHANDLE,
                   HWND Parent=HWND_DESKTOP);

       ModalDlgBox(BaseWindow * Owner,
                   ULONG ResID,
                   WinEventHandler *Event,
                   HMODULE ResMod=NULLHANDLE,
                   HWND Parent=HWND_DESKTOP);

       ModalDlgBox(HWND Owner,
                   ULONG ResID,
                   HMODULE ResMod=NULLHANDLE,
                   HWND Parent=HWND_DESKTOP);

       ModalDlgBox(HWND Owner,
                   ULONG ResID,
                   WinEventHandler *Event,
                   HMODULE ResMod=NULLHANDLE,
                   HWND Parent=HWND_DESKTOP);

       ULONG GetReturn() { return RetVal; }
       BOOL Dismiss(ULONG x){ return WinDismissDlg(hWndFrame,x); }

       HWND GetControlID(ULONG ID) {
           return WinWindowFromID(hWndFrame,ID);
       }

   friend class ModlessDlgBox;
};

FrameWindow

Following is a generic frame window class. This class is usable in this form but one still has to provide the various control flags and such as parameters. It would be a trivial task to derive subclasses that specify the various frame creation options to this object without requiring them to be passed to their constructors as parameters.

The frame window (PM class WC_FRAME) is created by this object using the WinCreateWindow function.

class FrameWindow: public BaseWindow {
   FRAMECDATA fcdata;
   ULONG flFrameControlFlags;
   PSZ Title;

   public:
      FrameWindow();
      FrameWindow(PSZ pTitle,
                  ULONG ControlFlags,
                  HMODULE ResMod,
                  ULONG ResID,
                  HWND ZOrder=HWND_TOP,
                  ULONG IDNum=1,
                  HWND Parent=HWND_DESKTOP);
      FrameWindow(PSZ pTitle,
                  ULONG ControlFlags,
                  HMODULE ResMod,
                  ULONG ResID,
                  LONG XPos,
                  LONG YPos,
                  LONG Height,
                  ULONG Width,
                  HWND ZOrder=HWND_TOP,
                  ULONG IDNum=1,
                  HWND Parent=HWND_DESKTOP);

      ULONG GetFlags() { return flFrameControlFlags; }
};

ClientWindow

This object serves as the base from which client windows are derived. This object simply encapsulates the data and method of interacting with the object FrameWindow. Note a bug in BC4OS2's virtual function tables prevents this object to work properly. A work around is done in this code but it will be changed once I get the product-level C/Set++.

class ClientWindow :public BaseWindow {
   FrameWindow *ParentWin;

   public:
      static const char NoCreateWin;
      static const char CreateWin;

      ClientWindow(FrameWindow* Parent,
                   PSZ ClassName,
                   ULONG ClassStyle,
                   USHORT Extra,
                   char Create=CreateWin);

      /* BUG in BC virtual table (corrupt)...trying the following...
      virtual BOOL IsRegistered()=0;
      virtual void SetRegistered()=0;
      */

      virtual BOOL IsRegistered() { return 0; }
      virtual void SetRegistered() {};
};

Example Client Window Subclass

Following is a simple client window that illustrates the procedure of creating new client window types. The client object must keep track of whether it has been previously registered to PM via WinRegisterClass so that it is not re-registered.

class SimpClient: public ClientWindow {
   static BOOL Registered;
   WinEventHandler *Event;

   public:
      SimpClient(FrameWindow *Frm);
      ~SimpClient();

      virtual BOOL IsRegistered() { return Registered; }
      virtual void SetRegistered() { Registered=1; }

      MRESULT MessageHandler(WinEventHandler &hand,
                             MessageStruct &Msg,
                             char &Action);
};

The static data member Registered is used to keep track of whether it has been previously registered or not. The data member Event is initialised in the constructor with:

Event=new WinSameThreadHandler<SimpClient>(&SimpClient::MessageHandler);

The function MessageHandler is the message handler for this object.