Designing a Cross-Platform GUI



April 01, 1995
URL:http://www.drdobbs.com/designing-a-cross-platform-gui/184403013

April 1995/Designing a Cross-Platform GUI/Figure 1

Figure 1 The Library Class Hierarchy

April 1995/Designing a Cross-Platform GUI/Figure 2

Figure 2 Demo on AIX/X-Window

April 1995/Designing a Cross-Platform GUI/Listing 1

Listing 1 gui.h — Platform-independent header file for class library

gui.h

#ifndef _GUI_
#define _GUI_

#include <stdarg.h>

// platform specific includes and defines

#ifdef WIN_NT
   #include <windows.h>

   typedef HWND NativeWindow;
   typedef HDC GraphicsHandle;
   #define EXP1
   #define EXP2 _declspec(dllexport)
   #define NATIVE_PART_OF_GUI_APPLICATION HINSTANCE hInstance
   #define NATIVE_PART_OF_GUI_WINDOW
   #define NATIVE_FRIEND_OF_GUI_WINDOW   friend LRESULT CALLBACK \
          GuiWindowProc(HWND hWnd, UINT message, \
                      WPARAM uParam, LPARAM lParam)
   #define main  ApplicationMain
#endif

#ifdef OS2
   #define INCL_WIN
   #define INCL_GPI
   #include <os2.h>

   typedef HWND  NativeWindow;
   typedef HPS   GraphicsHandle;
   #define EXP1  _export
   #define EXP2
   #define NATIVE_PART_OF_GUI_APPLICATION HAB Hab; \
                                   HMQ Hmq
   #define NATIVE_PART_OF_GUI_WINDOW      NativeWindow frame
   #define NATIVE_FRIEND_OF_GUI_WINDOW    friend MRESULT \
          EXPENTRY GuiWindowProc(HWND hwnd, USHORT msg, \
                             MPARAM mp1, MPARAM mp2)
#endif

#ifdef X_WINDOWS
   #include <X11/Intrinsic.h>

   typedef Widget NativeWindow;
   typedef GC     GraphicsHandle;
   #define EXP1
   #define EXP2
   #define NATIVE_PART_OF_GUI_APPLICATION int dontQuit
   #define NATIVE_PART_OF_GUI_WINDOW      NativeWindow frame; \
                                   char          szGeometry[100]
   #define NATIVE_FRIEND_OF_GUI_WINDOW    \
       friend void ExposeCallback(Widget w, XtPointer client_data, \
                                  XtPointer call_data); \
       friend void ResizeCallback(Widget w, XtPointer client_data, \
                                  XtPointer call_data)
#endif

// coordinate system definition

#define MAX_X              10000
#define MAX_Y              10000

#define MAX_WINDOW         100 // max. windows in an application
#define MAX_CHILDREN       100 // max. children of a window

// VGA colors

#define COLOR_WHITE        0
#define COLOR_BLACK        1
#define COLOR_BLUE         2
#define COLOR_RED          3
#define COLOR_PINK         4
#define COLOR_GREEN        5
#define COLOR_CYAN         6
#define COLOR_YELLOW       7
#define COLOR_NEUTRAL      8
#define COLOR_DARKGRAY     9
#define COLOR_DARKBLUE     10
#define COLOR_DARKRED      11
#define COLOR_DARKPINK     12
#define COL0R_DARKGREEN    13
#define COLOR_DARKCYAN     14
#define COLOR_BROWN        15

// object types returned by GetType()

#define TYPE_LINE          1
#define TYPE_ELLIPSE       2
#define TYPE_TEXT          3
#define TYPE_BUTTON        4
#define TYPE_ENTRY         5
#define TYPE_WINDOW        6

// the only global function: logs into a file
EXP2 void EXP1 Log(char* fmt, ...);

// forward declaration of classes GUI_WINDOW and GUI_OBJECT
class GUI_WINDOW;
class GUI_OBJECT;

typedef struct _GUI_WINDOW_ELEM { // typedef for linked list of windows
   GUI_WINDOW*              pWindow;
   struct _GUI_WINDOW_ELEM* pNext;
} GUI_WINDOW_ELEM;

class EXP1 GUI_APPLICATION {
   private:
      NATIVE_PART_OF_GUI_APPLICATION;

      GUI_WINDOW_ELEM WindowElemList[MAX_WINDOW], *pUsed, *pFree;
      int             AddWindow(GUI_WINDOW *pWindow);
      int             DelWindow(GUI_WINDOW *pWindow);
   public:
      EXP2 GUI_APPLICATION(int* pArgc, char** argv, char* logFile);
      EXP2 virtual     ~GUI_APPLICATION(void);
      EXP2 void        MainLoop(void);
      EXP2 void        Terminate(void);
      EXP2 GUI_WINDOW* FindWindow(NativeWindow window);
      EXP2 GUI_OBJECT* FindChild(NativeWindow window);

   friend class GUI_WINDOW;
};

class EXP1 CALLABLE_OBJECT {
   public:
      EXP2 virtual int Activate(void) { return(1); }
      EXP2 virtual int LeftClick(void) { return(1); }
      EXP2 virtual int RightClick(void) { return(1); }
      EXP2 virtual int LeftDoubleClick(void) { return(1); }
      EXP2 virtual int RightDoubleClick(void) { return(1); }
};

class EXP1 GUI_OBJECT: public CALLABLE_OBJECT { // abstract class
   protected:
      GUI_WINDOW* pParent;
      int         id, x, y, width, height;
   public:
      EXP2 GUI_OBJECT(GUI_WINDOW* pParent, int id, int x=0, int y=0,
                      int width=MAX_X, int height=MAX_Y);
      EXP2 virtual              ~GUI_OBJECT(void);
      EXP2 virtual int          GetType(void)=0;
      EXP2 virtual void         Paint(GraphicsHandle gh) { }
      EXP2 virtual NativeWindow GetNativeWindow(void) { return(0); }
      EXP2 GUI_WINDOW*          GetParent(void) { return(pParent); }
      EXP2 int                  GetId(void) { return(id); }
      EXP2 int                  GetX(void) { return(x); }
      EXP2 int                  GetY(void) { return(y); }
      EXP2 int                  GetWidth(void) { return(width); }
      EXP2 int                  GetHeight(void) { return(height); }
};

class EXP1 GUI_GRAPHICS_OBJECT: public GUI_OBJECT { // abstract class
   protected:
      int color, lineWidth;
   public:
      EXP2 GUI_GRAPHICS_OBJECT(GUI_WINDOW* pParent, int id, int x=0,
                       int y=0,int width=MAX_X, int height=MAX_Y,
                       int color=COLOR_BLACK, int lineWidth=1);
      EXP2 virtual ~GUI_GRAPHICS_OBJECT(void);
};

class EXP1 GUI_LINE: public GUI_GRAPHICS_OBJECT {
   public:
      EXP2 GUI_LINE(GUI_WINDOW* pParent, int id, int x1=0, int y1=0,
             int x2=MAX_X, int y2=MAX_Y,
             int color=COLOR_BLACK, int lineWidth=1);
      EXP2 virtual     ~GUI_LINE(void);
      EXP2 virtual int GetType(void) { return(TYPE_LINE); }
      EXP2 virtual void Paint(GraphicsHandle gh);
};

// fill types

#define FILL_OUTER       0 // draw just the otline
#define FILL_SOLID       1 // draw and fill the shape

class EXP1 GUI_ELLIPSE: public GUI_GRAPHICS_OBJECT { // abstract class
   protected:
      int fillType;
   public:
      EXP2 GUI_ELLIPSE(GUI_WINDOW* pParent, int id, int x=0, int y=0,
             int width=MAX_X/2,  int height=MAX_Y/2,
             int color=COLOR_BLACK,  int fillType=FILL_OUTER,
             int lineWidth=1);
      EXP2 virtual     ~GUI_ELLIPSE(void);
      EXP2 virtual int GetType(void) { return(TYPE_ELLIPSE); }
      EXP2 virtual void Paint(GraphicsHandle gh);
};

class EXP1 GUI_WINDOW_OBJECT: public GUI_OBJECT { // abstract class
   protected:
      NativeWindow window;
   public:
      EXP2 GUI_WINDOW_OBJECT(GUI_WINDOW* pParent, int id, int x=0,
                     int y=0, int width=MAX_X, int height=MAX_Y);
      EXP2 virtual      ~GUI_WINDOW_OBJECT(void);
      EXP2 NativeWindow GetNativeWindow(void) { return(window); }
      EXP2 virtual void GetRealCoordinates(int *pRx, int *pRy,
                                 int *pRw, int *pRh);
      EXP2 virtual int  GetText(char *buffer, int bufferLength);
      EXP2 virtual int  SetText(char *text="");
};

class EXP1 GUI_TEXT: public GUI_WINDOW_OBJECT {
   public:
      EXP2 GUI_TEXT(GUI_WINDOW* pParent, int id, int x=0, int y=0,
             int width=MAX_X, int height=0,
             char *text="");
      EXP2 virtual int GetType(void) { return(TYPE_TEXT); }
};

class EXP1 GUI_BUTTON: public GUI_WINDOW_OBJECT {
   public:
      EXP2 GUI_BUTTON(GUI_WINDOW* pParent, int id, int x=0, int y=0,
                      int width=MAX_X, int height=0, char* text="");
      EXP2 virtual int  GetType(void) { return(TYPE_BUTTON; }
};

class EXP1 GUI_ENTRY: public GUI_WINDOW_OBJECT {
   protected:
      int length;
   public:
      EXP2 GUI_ENTRY(GUI_WINDOW* pParent, int id, int x=0, int y=0,
                     int width=MAX_X, int height=0,
                     char* text="", int maxTextLength=10);
      EXP2 virtual int GetType(void) { return(TYPE_ENTRY); }
#ifdef X_WINDOWS // if not under X_WINDOWS these are inherited from
                            // GUI_WINDOW_OBJECT
      EXP2 virtual int  GetText(char *buffer, int bufferLength);
      EXP2 virtual int  SetText(char *text="");
#endif
};

// frame styles

#define FRAME_THIN        1
#define FRAME_SIZEABLE    2
#define FRAME_TITLE       4
#define FRAME_MIN_BUTTON  8
#define FRAME_MAX_BUTTON  16
#define FRAME_DEFAULT     (FRAME_THIN | FRAME_TITLE)

typedef struct _GUI_OBJECT_ELEM { // typedef for linked list of children
   GUI_OBJECT*              pObject;
   struct _GUI_OBJECT_ELEM* pNext;
} GUI_OBJECT_ELEM;

class EXP1 GUI_WINDOW: public GUI_WINDOW_OBJECT {
   private:
      NATIVE_PART_OF_GUI_WINDOW;

      GUI_APPLICATION* pApplication;
      GraphicsHandle   gh;
      int              frameStyle, backgroundColor;
      GUI_OBJECT_ELEM  Children[MAX_CHILDREN], *pUsed, *pFree;
   protected:
      EXP2 void             SizeChildren(void);
      EXP2 int              AddChild(GUI_OBJECT* pChild);
      EXP2 int              DeleteChild(GUI_OBJECT* pChild);
   public:
      EXP2 GUI_WINDOW(GUI_APPLICATION* pApplication, int id, int x=0,
                      int y=0, int width=MAX_X, int height=MAX_Y,
                      int backgroundColor=COLOR_WHITE, char* title= "",
                      int frameStyle=FRAME_DEFAULT);
      EXP2 virtual        ~GUI_WINDOW(void);
      EXP2 virtual int    GetType(void) { return(TYPE_WINDOW); }
      EXP2 virtual void   Paint(GraphicsHandle gh);
      EXP2 virtual void   Show(void);
      EXP2 int            GetBackground(void) {return(backgroundColor); }
      EXP2 GraphicsHandle GetGraphicsHandle(void) { return(gh); }
      EXP2 GUI_OBJECT*    GetChildFromId(int id);
      EXP2 GUI_OBJECT*    GetChildFromWindow(NativeWindow window);
#ifndef WIN_NT // if under Windows these are inherited from GUI_WINDOW_OBJECT
      EXP2 virtual int    GetText(char *buffer, int bufferLength);
      EXP2 virtual int    SetText(char *text="");
#endif
      EXP2 virtual void   Message(char *title, char *text);
      // return value 0=OK, 1=Cancel
      EXP2 virtual int    Question(char *title, char *text);

   friend class GUI_APPLICATION;
   friend class GUI_OBJECT;
   NATIVE_FRIEND_OF_GUI_WINDOW;
};

#endif // #ifndef _GUI_

/* End of File */
April 1995/Designing a Cross-Platform GUI/Listing 2

Listing 2 gui.c — Implementation of platform-independent functions

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "gui.h"

extern int              charHeight;
extern FILE*            fErrLog;
extern GUI_APPLICATION* pGuiApplication;

//*** logging function into a file opened by GUI_APPLICATION

void Log(char* fmt, ...)
{
   va_list ap;

   if(fErrLog) {
     va_start(ap, fmt);
     vfprintf(fErrLog, fmt, ap);
     fflush(fErrLog);  // to protect against losing log if crashing
     va_end(ap);
   }
}

//*** Functions for GUI_APPLICATION

int GUI_APPLICATION::AddWindow(GUI_WINDOW *pWindow)
{
   int              ret = 1;
   GUI_WINDOW_ELEM* pTmp;

   // add a GUI_WINDOW to the linked list of windows
   if(pFree) {
     pTmp = pFree;
     pFree = pFree->pNext;
     pTmp->pWindow = pWindow;
     pTmp->pNext = pUsed;
     pUsed = pTmp;
     ret = 0;
   }

   if(ret) Log("Error: too many windows.\n");

   return(ret);
}

int GUI_APPLICATION::DelWindow(GUI_WINDOW* pWindow)
{
   int             ret = 1;
   GUI_WINDOW_ELEM *pTmp, *pPrev;

   // delete a window from the linked lists of windows
   for(pTmp = pUsed, pPrev = NULL; pTmp; pPrev = pTmp,
      pTmp = pTmp->pNext) {
     if(pTmp->pWindow == pWindow) {
       // take out of used list
       if(pPrev) pPrev->pNext = pTmp->pNext;
       else      pUsed        = pTmp->pNext;
       // put into free list
       pTmp->pNext = pFree;
       pFree = pTmp;
       ret = 0;
       break;
     }
   }

   if(ret) Log("Error: can't delete window %p\n", pWindow);

   return(ret);
}

GUI_WINDOW* GUI_APPLICATION::FindWindow(NativeWindow window)
{
   GUI_WINDOW_ELEM *pTmp;

   // find a window in its list of windows
   for(pTmp = pUsed; pTmp; pTmp = pTmp->pNext) {
      if(pTmp->pWindow->GetNativeWindow() == window) break;
   }

   return(pTmp ? pTmp->pWindow : NULL);
}

//*** Functions for GUI_OBJECT

GUI_OBJECT::GUI_OBJECT(GUI_WINDOW* pParent, int id, int x, int y,
                   int width, int height)
          : id(id), pParent(pParent), x(x), y(y),
            width(width), height(height)
{
   // add this object to its parent children list
   if(pParent) pParent->AddChild(this);
}

GUI_OBJECT::~GUI_OBJECT(void)
{
   // delete this object from its parent children list
   if(pParent) pParent->DeleteChild(this);
}

//*** Functions for GUI_GRAPHICS_OBJECT

GUI_GRAPHICS_OBJECT::GUI_GRAPHICS_OBJECT(GUI_WINDOW* pParent, int id,
                                  int x, int y,
                                  int width, int height,
                                  int color, int lineWidth)
                 :GUI_OBJECT(pParent, id, x, y, width, height),
                  color(color), lineWidth(lineWidth)
{
   // nothing to do
}

GUI_GRAPHICS_OBJECT::~GUI_GRAPHICS_OBJECT(void)
{
   // nothing to do
}

//*** Functions for GUI_LINE

GUI_LINE::GUI_LINE(GUI_WINDOW* pParent, int id, int x1, int y1, int x2,
                int y2, int color, int lineWidth)
        :GUI_GRAPHICS_OBJECT(pParent, id, x1, y1, x2, y2, color, lineWidth)
{
   // paint the first time around
   Paint(pParent->GetGraphicsHandle());
}

//*** Functions for GUI_ELLIPSE

GUI_ELLIPSE::GUI_ELLIPSE(GUI_WINDOW* pParent, int id, int x, int y,
                int width, int height,
                int color, int fillType, int lineWidth)
          :GUI_GRAPHICS_OBJECT(pParent, id, x, y, width, height,
                           color, lineWidth),
           fillType(fillType)
{
   // paint the first time around
   Paint(pParent->GetGraphicsHandle());
}

//*** Functions for GUI_WINDOW_OBJECT

GUI_WINDOW_OBJECT::GUI_WINDOW_OBJECT(GUI_WINDOW* pParent, int id,
                                     int x, int y, int width, int height)
                                     :GUI_OBJECT(pParent, id, x, y, width, height)
{
   window = 0;
}

//*** Functions for GUI_WINDOW

int GUI_WINDOW::AddChild(GUI_OBJECT* pChild)
{
   int              ret = 1;
   GUI_OBJECT_ELEM* pTmp;

   // add pChild to the linked list of children
   if(pFree) {
     pTmp = pFree;
     pFree = pFree->pNext;
     pTmp->pObject = pChild;
     pTmp->pNext = pUsed;
     pUsed = pTmp;
     ret = 0;
}

   if(ret) Log("Error: too many children.\n");

   return(ret);
}

int GUI_WINDOW::DeleteChild(GUI_OBJECT* pChild)
{
   int             ret = 1;
   GUI_OBJECT_ELEM *pTmp, *pPrev;

   // delete pChild from the linked list of children
   for(pTmp = pUsed, pPrev = NULL; pTmp; pPrev = pTmp,
      pTmp = pTmp->pNext) {
     if(pTmp->pObject == pChild) {
        // take out of used list
        if(pPrev) pPrev->pNext = pTmp->pNext;
        else      pUsed        = pTmp->pNext;
        // put into free list
        pTmp->pNext = pFree;
        pFree = pTmp;
        ret = 0;
        break;
     }
   }

   if(ret) Log("Error: can't delete child %p\n", pChild);

   return(ret);
// get child pointer from its id; it returns NULL if fails

GUI_OBJECT* GUI_WINDOW::GetChildFromId(int id)
{
   GUI_OBJECT_ELEM *pTmp;

   for(pTmp = pUsed; pTmp; pTmp = pTmp->pNext) {
     if(pTmp->pObject->GetId() == id) break;
   }

   return(pTmp ? pTmp->pObject : NULL);
}

// get child pointer from its native window; it returns NULL if fails

GUI_OBJECT* GUI_WINDOW::GetChildFromWindow(NativeWindow hwnd)
{
   GUI_OBJECT_ELEM *pTmp;

   for(pTmp = pUsed; pTmp; pTmp = pTmp->pNext) {
      if(pTmp->pObject->GetNativeWindow() == hwnd) break;
   }

   return(pTmp ? pTmp->pObject : NULL);
}

/* End of File */
April 1995/Designing a Cross-Platform GUI/Listing 3

Listing 3 demo.c — Short demonstration program

#include <stdio.h>
#include "gui.h"

#define MY_EXIT   1
#define MY_NEW    2
#define MY_CLOSE  3
#define MY_TEXT   4
#define MY_NAME   5

class MY_BUTTON: public GUI_BUTTON {
   public:
      MY_BUTTON(GUI_WINDOW *pParent, int id, int x=0, int y=0,
                int width=MAX_X, int height=MAX_Y, char *text="")
          :GUI_BUTTON(pParent, id, x, y, width, height, text) {}
      int Activate(void);
      int RightClick(void);
};

class MY_ENTRY: public GUI_ENTRY {
   public:
      MY_ENTRY(GUI_WINDOW* pParent, int id, int x=0, int y=0,
               int width=MAX_X, int height=0,
               char* text="", int length=10)
          :GUI_ENTRY(pParent, id, x, y, width, height, text,
                      length) {}
      int Activate(void);
};

GUI_APPLICATION *pApplication;

void CreateMyWindow(int id);
void DeleteMyWindow(GUI_WINDOW *pWindow);

void main(int argc, char **argv)
{
   pApplication = new GUI_APPLICATION(&argc, argv, "demo.log");
   CreateMyWindow(1);
   pApplication->MainLoop();
   delete pApplication;
}

int MY_BUTTON::Activate(void)
{
   static int windowCount = 1;

   switch(GetId() - pParent->GetId()) {
   case MY_EXIT:
      pApplication->Terminate();
      break;
   case MY_NEW:
      if(windowCount < 10) {
         CreateMyWindow(pParent->GetId() + 100);
         windowCount++;
      }
      else {
         GetParent()->Message("Application message",
                          "Too many windows");
      }
      break;
   case MY_CLOSE:
      if(--windowCount) DeleteMyWindow(GetParent());
      else              pApplication->Terminate();
      break;
   }
   return(0);
}

int MY_BUTTON::RightClick(void)
{
   GetParent()->Message("Applicaton Message", "RightClick");
   return(0);
}

int MY_ENTRY::Activate(void)
{
   char text[20+1];

   GetText(text, 20);
   GetParent()->SetText(text);
   return(0);
}

void CreateMyWindow(int id)
{
   int        color, childId;
   char       title[100];
   GUI_WINDOW *pWindow;

   color = id / 100;
   sprintf(title, "Window %d", id);
   pWindow = new GUI_WINDOW(pApplication, id, 15*id, 4900 - 10*id,
                        5000, 5000, color, title,
                        FRAME_TITLE | FRAME_SIZEABLE | FRAME_MAX_BUTTON);

   // create buttons, text and entry field
   new MY_BUTTON(pWindow, id+MY_EXIT,  1000, 9000, 2000, 0, "Exit");
   new MY_BUTTON(pWindow, id+MY_CLOSE, 4000, 9000, 2000, 0, "Close");
   new MY_BUTTON(pWindow, id+MY_NEW,   7000, 9000, 2000, 0, "New");
   new GUI_TEXT(pWindow,  id+MY_TEXT,  1000, 7500, 2500, 0, "New title:");
   new MY_ENTRY(pWindow,  id+MY_MAME,  4000, 7500, 2000, 0, "", 20);

   // create little figure
   childId = id + MY_NAME;
   //body
   new GUI_LINE(pWindow, ++childId, 7500, 7000, 8000, 6500, color+1, 2);
   new GUI_LINE(pWindow, ++childId, 8500, 7000, 8000, 6500, color+1, 2);
   new GUI_LINE(pWindow, ++childId, 8000, 6500, 8000, 4000, color+1, 2);
   new GUI_LINE(pWindow, ++childId, 7500, 5000, 8000, 4500, color+1, 2);
   new GUI_LINE(pWindow, ++childId, 8500, 5000, 8000, 4500, color+1, 2);
   //left eye
   new GUI_ELLIPSE(pWindow, ++childId, 7700, 2900, 300, 300, color+2,
                FILL_SOLID);
   //right eye
   new GUI_ELLIPSE(pWindow, ++childId, 8300, 2900, 300, 300, color+2,
                FILL_SOLID);
   //nose
   new GUI_LINE(pWindow, ++childId, 8000, 3200, 8000, 3600, color+2, 1);
   //mouth
   new GUI_LINE(pWindow, ++childId, 7900, 3800, 8100, 3800, color+2, 1);
   new GUI_LINE(pWindow, ++childId, 7800, 3700, 7900, 3800, color+2, 1);
   new GUI_LINE(pWindow, ++childId, 8100, 3800, 8200, 3700, color+2, 1);
   //head
   new GUI_ELLIPSE(pWindow, ++childId, 8000, 3200, 1400, 1600, color+1,
                FILL_SOLID);

   pWindow->Show();
}

void DeleteMyWindow(GUI_WINDOW *pWindow)
{
   int        id;
   GUI_OBJECT *pObj;

   for(id = MY_EXIT + pWindow->GetId();
      pObj = pWindow->GetChildFromId(id) ;
      id++) delete pObj;
   delete pWindow;
}
/* End of File */

April 1995/Designing a Cross-Platform GUI

Designing a Cross-Platform GUI

Laszlo Zeke


Laszlo Zeke has been supervising, designing, and developing GUI-based applications over 12 years. He has an M.A. in mathematics. He resides in Herndon, Virginia, and is the Director of Kernel Product Engineering for INCODE Corporation. Laszlo can be reached via e-mail at [email protected] or via CompuServe at 76667,1776.

Introduction

Sooner or later you're bound to meet a customer who likes your GUI-based application but wants it to run in his own GUI environment, different from yours. You may wish to accomodate customers such as this, but find it too expensive to maintain different code sets on different platforms.

A common solution is to package the GUI-dependent pieces into an object library; the application will then use the objects implemented in this GUI library only, instead of the native GUI interfaces.

You have two basic choices in implementing this type of object library: either purchase a third-party multi-platform toolkit (there are plenty), or develop your own, using the native APIs. Both approaches have their merits, as well as proponents.

In this article I demonstrate the latter approach by implementing a GUI library using the native APIs of Windows/NT, OS/2, and X-Windows. This is just a sample library since the source code of a full-blown, commercial-strength library would run well over 10,000 lines — obviously beyond the space available here.

This article presents the class design for the platform-independent portion of the GUI, plus a sample application. I also briefly describe the platform-specific parts of the code. The platform-dependent code is included on this month's code disk, and is available from the on-line sources listed on page 3.

Overall Library Features

An application that uses this library will have the following capabilities:

Library Design

Ideally, I might have based my design solely on its ability to meet a given set of criteria, such as the features presented above. In reality, any design will be influenced by the tools at hand, in this case, the compilers available for the various platforms.

Tools

I selected the following tools for these three target environments:

(Note: I am not suggesting that these tools are better than others; they are simply the tools I'm familiar with.)

The code presented in this article will compile and run on each of these platforms. Since error checking would double the size of the code, I have left it out entirely.

The General Application Framework

The library tries to make use of the common elements in the native toolkits. When there is nothing in common, the library tries to bridge the differences with the least possible effort.

Although each toolkit is different, any application that uses such a toolkit will follow the same general pattern, or framework:

1. some initialization of the toolkit

2. creation of one or more windows along with their children

3. processing messages in a loop that retrieves and dispatches different messages to the different windows and to their children based upon the user's actions

4. termination with some possible cleanup

To encapsulate this initialize-message loop-cleanup model, I introduce a class, GUI_APPLICATION (See Listing 1, a GUI-independent header file for the library). The application will create an object of this class; this object should be created first, and destroyed last in any application.

The main entities an application creates are windows. These are implemented in the class GUI_WINDOW. A window in this context has a frame, with some possible decorations (title, maximize button, sizable-border, etc.), and usually has one or more children. This library implements the child types static text, button, entry field, line, and ellipse. The corresponding classes are GUI_TEXT, GUI_BUTTON, GUI_ENTRY, GUI_LINE, and GUI_ELLIPSE.

These classes naturally fall into two groups: those implemented in the native toolkit as special native windows, and those that must be created by some drawing APIs. The two groups descend from two different base classes, since they behave differently in many respects. (For instance, most of the native windows are repainted automatically as needed, the other type are not.)

The two base classes are GUI_WINDOW_OBJECT and GUI_GRAPHICS_OBJECT. GUI_WINDOW_OBJECT is the common base class for GUI_TEXT, GUI_BUTTON, GUI_ENTRY, and GUI_WINDOW, whose objects are native windows. GUI_GRAPHICS_OBJECT is the common base class for GUI_LINE and GUI_ELLIPSE, whose objects must be drawn explicitly by the application.

Though they behave differently, these two base classes share some common elements. For instance, all of them have x and y coordinates, they have width and height, etc. To extract these and some other common characteristics, I introduce a common base class, GUI_OBJECT. Figure 1 shows the class heirarchy just described.

Processing User Input

Most of the objects mentioned above must be able to accept user input and execute some functions based upon it. For instance, the button objects must accept a LEFT MOUSE BUTTON SINGLE CLICK event and execute an application-defined function. There are two ways to accomplish this.

The first way is to store event-handler function pointers in the above-mentioned objects. The application will call the desired function through one of these pointers when a given event takes place. The other way to provide event responses is to create some virtual functions in the base class, to be overridden by an application-derived class. I chose the second approach, since it seems more modular and cleaner to me.

I provide a default LeftButtonSingleClick function for every class, which function does nothing. Thus, a mouse click on a button of type GUI_BUTTON causes no harm, but not much excitement either. If you derive a class MY_BUTTON from GUI_BUTTON, and override LeftButtonSingleClick, then this new overridden function will be called whenever a MY_BUTTON is clicked. I do not use C++'s virtual function call mechanism to determine which button was pushed. To do so would require a new derived button class for practically every button in the program. Rather, I find out which button was activated by introducing a numeric ID field for every GUI_OBJECT, enabling a switch statement to be based upon this ID.

These user input functions could be implemented in the GUI_OBJECT class, but I decided to implement them in a base class for GUI_OBJECT, CALLABLE_OBJECT. (If my class hierarchy were to grow, a new object class may not necessarily be derived from GUI_OBJECT, but I still might want to be able to process user input for that class.)

Display Features

My library assumes a window-relative MAX_X by MAX_Y coordinate system, with the upper left corner as (0,0). In such a coordinate system, a window with a width of MAX_X spans the whole width of the screen, while a button with a width of MAX_X spans the whole width of its parent window. The particular values for MAX_X and for MAX_Y are not really important but they should be finer than the actual display resolution. This library uses 10,000 for both MAX_X and MAX_Y, which is a safe choice for most contemporary displays.

Matching appropriate colors among the different GUI environment is a subject worth a book in itself. Therefore, I chose a very simple approach and implemented the 16 VGA colors. Font handling is another issue that naturally arises, but its complexity is beyond the scope of this article. (However, a subset of available fonts could be easily managed in all of the mentioned environments.)

Description of Classes

In this section I describe the platform-independent portion of the class library. Figure 1 shows the class hierarchy. The top three layers of the hierarchy consist of abstract classes. The CALLABLE_OBJECT class declares several functions for responding to user input, but these are all virtual functions, hence they must be overridden and "filled in" in descendant classes. The next lower class, GUI_OBJECT, introduces a data member id, which stores an ID number for each object. Every GUI_OBJECT must have a unique ID. I also briefly describe class GUI_APPLICATION here, though it is not a member of the hierarchy.

GUI_APPLICATION

The GUI_APPLICATION class has no descendants. It serves as a wrapper around the application and maintains the main message loop. A GUI_APPLICATION object has four basic functions: a constructor to initialize the GUI toolkit, a destructor to clean up, a MainLoop function to handle the main message loop, and a Terminate function which breaks the message loop. This class maintains a single-linked list of GUI_WINDOW objects, the windows of the application.

GUI_WINDOW_OBJECT

GUI_WINDOW_OBJECT serves as an abstract base class for GUI_WINDOW. A GUI_WINDOW object maintains a handle to its own native window, in protected member window of GUI_WINDOW_OBJECT. Each GUI_WINDOW object also maintains a single-linked list of GUI_OBJECT objects comprising its children, such as buttons, text elements, lines, etc. (Note that these "children" are not descendants of GUI_WINDOW; rather, they are children in the sense that they are owned by a GUI_WINDOW object.) When a GUI_WINDOW object is created it does not appear immediately, therefore the application has a chance to create its children first. Calling GUI_WINDOW's Show function finally makes it appear. GUI_WINDOW also has a function Message to put up a message box, and a function Question to ask the user a yes/no question. GUI_WINDOW's GetText/SetText function pair enables an application to dynamically get/set the title of its window.

GUI_TEXT, GUI_BUTTON, and GUI_ENTRY are descendants of GUI_WINDOW_OBJECT whose objects function as children of GUI_WINDOW objects. A GUI_TEXT object contains a non-modifiable piece of text to be displayed in the window. The user cannot change its contents. A GUI_BUTTON object represents a pushbutton. A GUI_ENTRY object represents a single-line, specified-text-length, autoscrolling entry field. These are the only two classes in this library that override the user-input functions declared in CALLABLE_OBJECT.

Specifically, a GUI_BUTTON object calls the following functions:

A GUI_ENTRY object calls the Activate function if the entry field is losing its focus.

Supplying a height parameter of zero to a GUI_TEXT, GUI_BUTTON, or GUI_ENTRY object will cause it to be created with the default font height. It is also possible to dynamically get/set these objects' text via the GetText/SetText function pair.

GUI_GRAPHICS_OBJECT

A GUI_GRAPHICS_OBJECT is an abstract base class for objects that can't be represented as native windows. It has two descendants, GUI_ELLIPSE and GUI_LINE. A GUI_ELLIPSE object represents a filled/outlined color-specified ellipse. A GUI_LINE object represents a color-specified straight line.

Class Implementations

I've divided the source for this class library (about 750 lines on every platform) into two parts: gui.c (Listing 2) contains functions that don't use native APIs; native.c, provided separately for each platform, contains functions that do use native APIs. Due to space limitations, the three native.c files are not shown here, but are available on this month's code disk and CUJ online sources (see p. 3 for more information on online sources).

At a structural level, each native implementation shares some common features. For example, each implementation must perform some sort of registration of a window or application with the operating system. Each implementation must interact with a main message loop and respond to messages from the system. Finally, each implementation must explicitly draw graphic objects of type GUI_LINE and GUI_ELLIPSE, rather than rely upon the native system to do the work.

The greatest difference in implementations occurs between the Windows look-alikes (Windows 3.1, Windows NT, and OS/2) and X-Window. For example, X-Window requires an application to register callback functions for painting and resizing windows, while the look-alikes do this in response to system messages (e.g. WM_PAINT). Another significant difference appears in the way the two implementations manage to catch all mouse events for nongraphical windows. The Windows look-alikes do so by "subclassing the window," overriding the built-in window procedure for buttons. The X implementation catches such events by registering an event handler with the system.

More details, as well as documentation, are provided on this month's code disk.

A Short Demo

The application presented here (Listing 3) , while not extremely "useful," demonstrates how easy it is to build an application using the library. Figure 2 is a screen capture of the demo running on AIX/X-Windows. The application window's Exit button terminates the application, while its Next button creates an identical window, shifted a little to the upper left and displayed in a different color. The application window's entry field determines the title of the window. Both of the window's buttons react to right-mouse-button clicks with popup messages.

Conclusion

Constructing a multi-platform GUI is not a simple task. Many problems will naturally arise that this article does not address: drawing optimization; creation of menu bars, accelerator keys, and popup menus; implementation of drag and drop operations, and z-order for children of a window; font management and national language support, just to mention a few. Some of these features can be added with relative ease (menus, optimized drawing, z-order), while others may require considerable effort (font management, drag and drop). However, if you're the kind of programmer who likes to experiment, or "roll your own," this class library should give you a good place to start.

Information Sources

Borland C++ for 0S/2 Ver 1.5, on CD-ROM. Produced by Borland International, 1994.

Microsoft Development Platform, on CD-ROM. Produced by Microsoft Corp., 1995.

O'Reilly, Tim, ed. X series, Release 4 and Release 5, vols. 0 - 8. O'Really & Associates, 1990-1994.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.