Developing a Portable C++ GUI Class Library

Where would you start if you had to write a portable application for Windows, Presentation Manager, Motif, Open Look, and the Macintosh?


November 01, 1992
URL:http://www.drdobbs.com/cpp/developing-a-portable-c-gui-class-librar/184408881

NOV92: DEVELOPING A PORTABLE C++ GUI CLASS LIBRARY

Andreas is director of software development at Star Division GmbH. He can be reached at Sachsenfeld 4, 2000 Hamburg 1, Germany, or via the Internet at [email protected].


Over two years ago, we began developing a desktop publishing/word processing application that was to run on Microsoft Windows, IBM OS/2 Presentation Manager, Apple Macintosh, Open Look, and OSF/Motif.

We recognized there were two approaches to this project. The first was to write the application by directly accessing platform-specific API functions. The advantage of this approach is guaranteed fast execution without overhead. However, you have to code different implementations of the same application for each GUI.

The second approach was to use a single portable library between the application and the GUI. This was the approach we took, although writing the class library ended up taking a full year of development time.

The essential requirements of a library like this are that it provide GUI functionality and multiple-document interface (MDI) support, a help system, printing, combo controls, and a portable resource system. At the same time, we wanted to be able to take advantage of system-dependent capabilities (like Windows OLE or DDE) and not lose the basic look-and-feel of an individual GUI.

We chose to program in C++ because of its standard object-oriented features--encapsulation, inheritance, polymorphism, extendibility, and class reusability. No portable C++ toolkit was available at the time (at least none that supported the abstraction level of the GUI functionality and came with source code), so we built our own, naming it StarView.

An Overview of GUI Systems

Windows 3.x/NT, Presentation Manager (PM), Macintosh, Open Look, and OSF/ Motif have many similar features. For instance, they're all event driven, work with resources, and support screen coordinates with variable metrics. Since Windows and PM have roughly 100 different messages, Motif and Open Look about 40 Xlib events, and the Macintosh about ten standard events, we created common events for mouse and keyboard input, windows, controls, menus, and timers. We then mapped these events to about 40 virtual functions in our library's base classes. We defined symbolic key codes (KEY_A) and mouse modifiers for all platforms. We were then able to derive our own classes and overload the virtual functions to access the events. In Windows and PM, most messages are handled inside the library automatically; for the Mac, most had to be emulated.

When you consider the resource systems on the different platforms, you notice that each system supports different levels of functionality. Consequently, we created our own resource syntax and wrote compilers for each system to translate the system-independent resources into the target system's resources, developing a portable resource system with the same level of functionality on each platform. One disadvantage of this is that we couldn't use the resource tools of the different platforms. We had to write resource files with a text editor, compile them, test them, and rewrite them. Because it took a long time to create the first resource files, we constructed our own interactive resource editor called Design Editor which reads and writes our resource files and creates corresponding class definitions and constructors. From that point on, it was easy to develop portable resources and create corresponding classes for them.

Another problem involved the different coordinate systems of each platform. On PM, for example, the coordinate-system origin is the lower-left corner of the screen; on the other systems, it's the upper-left corner. We decided to use the upper left as the origin and developed a map mode for specifying the metric, a scale, and an offset in the coordinate system. We realized that it would be very difficult to use resource files with the same metrics on each platform. If you specify a dialog in pixels, it will be very small on a high-resolution screen. If you specify a dialog in inches, it will have the same size on all platforms and resolutions, yet look ugly on smaller displays. Normally, the size of the system font is a good measure by which to specify the metrics of dialogs, so we created the map mode SystemFont to specify a dialog in system- or application-font units. We specified all our dialogs in the map-mode application font.

Memory allocation was yet another problem. In Windows and PM, early PC C++ compilers used alloc() and free() in new and delete so that each object needed an entire segment. With only 8192 segments available, the application often ran out of segments. To avoid this, we wrote our own memory manager which allocated 4K segments from the system's heap and suballocated them according to the object's size. This let us overload new and delete and call our own memory manager to allocate the memory. We continued to use the standard C++ new and delete with the Mac, Open Look, and Motif.

As Table 1 shows, the platforms did support important common features: MDI, context-sensitive help, printers, and controls like Windows combo boxes. MDI support was very important for our word processor, but it was available only on Windows. Consequently, our MDI for OS/2, Open Look, and Motif looks very similar to the Windows version. You can arrange the document windows in an application window with an MDI menu in which the document windows are listed automatically. On the Macintosh, however, there are no application windows. Instead, the document windows are arranged on the desktop.

Table 1: Features supported by the various platforms.

                  Windows  OS/2  Mac  Motif  Open Look
  ----------------------------------------------------

  MDI               Yes     No   No    No       No
  Help System       Yes     Yes  No    No       Yes
  Printing          Yes     Yes  Yes   No       No
  Combo controls    Yes     Yes  No    No       Yes

Another important common feature was context-sensitive help like that found in Windows and OS/2. Because we didn't want to maintain different help files, we settled on the Windows help system with its RTF-formatted help files. To use this format on OS/2, we wrote a RTF-to-OS/2 help-system format converter. Unfortunately, it wasn't that easy for the Macintosh, Open Look, and Motif. The Macintosh balloon help, for instance, is useful only for short messages and doesn't support references to other topics. Therefore, we had to write our own Windows-like help system which is used on Macintosh, Open Look, and Motif. Fortunately, this is a StarView application, so we had to code it only once. This system has an integrated help compiler that converts RTF help files into our own help format.

For output, we designed the class OutputDevice. We then derived the classes Window, Printer, and VirtualDevice, which are used by applications to create output to a window, bitmap, or printer. This was straightforward on Windows, OS/2, and Macintosh, but difficult on OpenLook and Motif, where we used a printer library that supported all Xlib output functions.

To emulate the Windows-OS/2 controls ComboBox, DropDownComboBox, and DropDownListBox on the Macintosh, we combined edit menus, list boxes, and pop-up menus. For Open Look and Motif, we combined text fields, arrow buttons, and list boxes.

To manipulate the internals of an object, we created the class sysdepen, which lets you, for example, access the Macintosh graphport directly or send a Windows message to an object. However, this generates nonportable, system-dependent code and should always be encapsulated in system-dependent classes. Likewise, in the spirit of portability, we decided not to support platform-specific features like Windows DDE or Macintosh QuickTime on all platforms. We did, however, implement OLE and DDE support as nonportable features especially for the Windows environment.

Developing the Class Library

During the year-long class-library development phase, we made three major design and interface changes: We included an additional mechanism for event handling, provided a mechanism for multiple referenced objects, and added const.

Initially, we provided a virtual method for each event. To get an event, we had to derive a class and overload the virtual function. This isn't a problem with a window class, where you derive a class and add the functionality, but it is problematic with controls, menus, and accelerators.

For example, the OK button in a dialog overloaded the virtual method Click() in the button class, checked the status of the dialog, and terminated it. The problem with this was that we had to derive a new button class for each OK button in different dialogs, because they all called slightly different functions. Although we could use multiple instances of the same button class with the identical OK buttons, some individual buttons performed special functions. To eliminate the large number of classes, we added a callback mechanism to the controls, menus, and accelerators. This enabled us to use an instance of the library's button class directly. We set a callback into a button instance; when activated, the button executed the callback. Executing the callback is the default implementation of the virtual method Click() in the button class. If we don't need the callback, we derive a new button class and overload the virtual method Click(). Because the code is written in C++ (not in C), there's no way to use C function pointers for callbacks, so the callback is always a pair of two pointers. The first references the object itself, and the second references the appropriate method of that instance. For this pair of pointers, we created the class Link, which stores the pointers and executes the callback. In Example 1, the OK button terminates a dialog. GetParent() returns a pointer to the dialog and the method MyDialog::QuitDialog() terminates and dialog.

Example 1: A derived ModalDialog with a PushButton. In the constructor of MyDialog, the PushButton is initialized to call MyDialog:: QuitDialog when clicked. There, the state of the dialog is checked and the dialog is terminated. You don't need to derive your own Button class for this example.

  class MyDialog : public ModalDialog
  {
  protected:
      DefPushButton   aOkButton;
  public:
              MyDialog( Window* pParent );
      void    QuitDialog( DefPushButton* pButton );
  }

  MyDialog::MyDialog( Window* pParent ):
            ModalDialog( pParent ),
            aOkButton( this )

  {
      aOkButton.ChangeClickHdl( LINK (this, Mydialog::QuitDialog ) );
  }

  void MyDialog::QuitDialog( DefPushButton* pButton )
  {
      if( ... )    // check dialog state
           ModalDialog::EndDialog();

Our second redesign involved a mechanism for multiple referenced objects -- bitmaps, pens, fonts, and brushes -- used in different windows. A brush, for example, consists of a f reground color, a background color, and a style -- components that can be queried and changed.

In our first design, these elements were selected with a pointer into multiple dialogs. All dialogs used the same instance of the background brush, but it was difficult to decide when this brush object had to be deleted. ("Is anyone still using the brush?" "Can I delete it now?")

It became apparent that this approach would lead to programming errors. For instance, all dialogs in an application should have the same background color. However, we didn't want to give each dialog its own instance of a background brush because of memory and speed considerations. The solution was to use a dummy brush class that contained only a pointer to the brush data. Multiple instances of brushes can then share the same data. The data has an instance counter that is incremented in the copy constructor and decremented in the destructor of the brush class. Thus, instances of brushes can be passed to different dialogs because the instance has only the size of the pointer, and the copy constructor works very fast. The brush data is deleted when the instance count is decremented to 0.

We also split the methods of the brush class in two groups. The first group of methods returns only values, while the second modifies the brush instance. When we modify the brush, the instance data is copied, and only the copy is altered.

We use this mechanism not only in the classes described above, but also with bitmaps and in our String class. At first we worked with char*, allocated memory, copied strings, and different buffer sizes. Now we use String class, which is faster, requires less memory, and is easy to manipulate.

The last major change in the interface was the introduction of const. As described earlier, we separated the methods of the brush class into modifying and nonmodifying groups, allowing us to declare the nonmodifying methods const.

However, this approach introduced some unexpected problems. For one thing, the compiler returned an error when we modified an object in a const-declared method. Secondly, if there was a const* or a const& to an instance of a class, you could only use const methods of that class. Because of this, we had to separate the entire library into const and non-const methods. From this we learned that you should either use const everywhere or avoid its use altogether.

Developing the Application

After a year's worth of work, we'd developed the class library and were ready to finally begin work on our application -- the portable word processor.

Our development environment was a mirror of our platform requirements: PCs running Windows, Windows NT, or OS/2 2.0; Sun SPARCstations with Open Look or Motif; and Apple Macintoshes.

All computers were connected to an Ethernet-based network equipped with Novell Netware 3.11, with NFS and Appleshare support available. We put the entire source code for all platforms on a commonly accessible file server.

Our main development platform is Windows 3.1, so all modules are written for the Windows environment first. Only tested and validated Windows versions are then ported. Porting means not only copying the source code from the file server to the workstations and compiling it there, but also handling the ASCII-file format on the various systems, different makefiles, and special, compiler-dependent language problems.

Although the source code was on a common file server, all system-specific modifications were made on the target systems. Every modification was registered using a source-code control system that modifies a single logfile on the server; each module can be modified by only one programmer at a time. Finally, a GUI specialist fine-tuned the resource files to ensure adherence to system style guides for look-and-feel.

Compiler Experiences

On the PC, we initially used the Glockenspiel C++ compiler, but compilation time was too slow, so we switched to Zortech for both Windows and OS/2 (although in retrospect we wish it had supplied more warnings). We used MPW C++ on the Macintosh and Sun's C++ for Open Look and Motif development. (The library itself supports Zortech C++ 3.0, Borland C++ 3.1, and Microsoft C/C++ 7.0 for Windows developers; Zortech C++ and Borland C++ for PM; Sun C++ 2.1 for Motif; Sun C++ 3.0 for Open Look; and MPW C++ 3.1 for the Macintosh.)

When the Macintosh library was ready, we compiled the application sources using the MPW C++ compiler, a cfront with powerful language checking. However, expressions like those in Example 2(a) resulted in messages like "Sorry, not implemented," because of return objects created on the stack. To avoid this, use code like that in Example 2(b).

Example 2: (a) Code like this caused cfront-generated error messages; (b) code like this helps you avoid errors.

  (a)
  class object
  {
      Object GetNext();
  }
  if( bTest && ( aObj.GetNext() == aObj ) ) ...

  (b)
  Obj aNext = aObj.GetNext();

  if( bTest && ( aNext == aObj ) )...

The Sun C++ 2.1 compiler had difficulties with the precompiler. For example, we defined the macro in Example 3(a) to concatenate two strings. The result was a precompiler error, so we resorted to the code in Example 3(b).

Example 3: (a) This macro, which concatenates two strings, generated precompiler errors; (b) code that avoids precompiler errors.

  (a)
  #define CONCAT( a, b )  a##b
  CONCAT( A, B )          // this will throw an error

  (b)
  #define CONCAT( a, b )  a/**/b

A bug in all cfronts is that they don't accept a default object in the constructor Class:: Class( String aString = String ("Default") );. So, in spite of having a list of the C++ expressions that cause problems when porting source code, errors occurred again and again, causing us to repeatedly build new versions of our project on the Macintosh and Sun.

After compiling and linking the word processor on the Macintosh, we tried to run the application. It took us about a week to fix the thorniest bugs in the library and start up the application. In the process, we also discovered differences in the code generated by the various compilers. The most serious problems were bugs in the Zortech and Borland C++ compilers.

For instance, the Zortech 3.0 compiler does not adjust the virtual-function table in destructors of derived classes. If you destroy an instance of a derived class and you use a virtual function in the destructor of the base class, the compiler should call the implementation of the base class. Instead, the Zortech compiler calls the implementation of the derived class, as shown in Example 4. By destroying a derived instance, the destructor of Base calls the virtual method foo(), and while it should call Base:: foo(), it calls Derived::foo() instead.

Example 4: The Zortech compiler calls the implementation of the derived class this way.

  class Base
  {
      ~Base() { foo(); }
      virtual foo();
  }
  class Derived : public Base
  {
      virtual foo();
  }

The second problem with the Zortech compiler occurs if you have a NUL pointer to an object with a virtual destructor, and you delete this pointer. In such a case, that class's destructor should not be called, but the Zortech compiler does make the call because the compare with NUL is done inside the destructor, not outside.

We ran across a problem in the Borland 3.0 compiler when we passed nameless temporary objects in the constructor's initializer list. In Derived::Derived() : Base( String( "abc" ) );, for instance, the constructor of the string is called, but not the destructor. The same problem occurs in the new C/C++ 7.0 compiler from Microsoft. (In Borland C++ 3.1, however, this problem is corrected.)

We collected all our compiler experiences in a test suite. Whenever we received a new version of a compiler or we went to a new platform, we tested the compiler against the suite. Although we'd written many test applications for the class library, we found many more bugs in it. We learned that large applications are much better test suites for a library than a set of small tests.




_DEVELOPING A PORTABLE C++ GUI CLASS LIBRARY_
by Andreas Meyer

Example 1:

class MyDialog : public ModalDialog
{
protected:
    DefPushButton   aOkButton;
public:
            MyDialog( Window* pParent );
    void    QuitDialog( DefPushButton* pButton );
}

MyDialog::MyDialog( Window* pParent ) :
          ModalDialog( pParent ),
          aOkButton( this )
{
    aOkButton.ChangeClickHdl( LINK( this, MyDialog::QuitDialog ) );
}

void MyDialog::QuitDialog( DefPushButton* pButton )
{
    if( ... )   // check dialog state
        ModalDialog::EndDialog();
}



Example 2:


(a)

class Object
{
    Object GetNext();
}
if( bTest && ( aObj.GetNext() == aObj ) ) ...

(b)

Obj aNext = aObj.GetNext();
if( bTest && ( aNext == aObj ) ) ...


Example 3:

(a)

#define CONCAT( a, b )  a##b
CONCAT( A, B )          // this will throw an error

(b)

#define CONCAT( a, b ) a/**/b



Example 4:

class Base
{
    ~Base() { foo(); }
    virtual foo();
}
class Derived : public Base
{
    virtual foo();
}


Copyright © 1992, Dr. Dobb's Journal

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