An Introduction to GUI Programming with UIL and Motif.

Why Motif?

On the UNIX platform (including Linux), there are several wndowing systems available, unlike the PC and Macintosh platofrms where choices are much more limited. This presents a problem since the knowledge of learning one user interface may not transfer to others. However, the Motif windowing system has gained may followers because it tries to adhere to industry-wide standards for interaction with a computer using a graphical user interface (GUI). Motif is based on the X windowing system that is freely available on UNIX platforms, and adds high-level features and usability without losing any of the basic capabilities of X. Thus when you learn Motif, you are also learning X, and this knowledge will transfer to other systems since X is used the basis for other GUIs as well. Motif was designed by the Open Software Foundation for a consortium of dozens of companies, including HP, Digital and IBM. They intentionally incorporated many aspects of Microsoft Windowsand OS/2 (IBM's own OS), which in turn borrowed heavily from the Mac OS. In fact, we could imagine a hypothetical source for all these windowing systems that gives them all the same flavor ("look and feel"), even though some of the details are different:

Programming for a GUI

When writing programs for a particular GUI, the programmer needs access to the libraries that contain all of the pre-compiled code for the interface, and a window manager that can run the resulting programs. Again, on other platforms this is not a problem, since Windows programs run in the Windows environment, and Mac programs run on the Mac. However, not all Motif programs will run on any UNIX system, unless the window manager, which is that part of the operating system that creates and manages the display, and allows interaction with the user, is compatible. Having said that, Motif programs will run on all of our machines in the CS department, so this problem will not arise.

So, we will be using the basic tools available for general C++ programming, i.e. the compiler g++, and the text editor of your choice, but incorporating Motif library functions that will interact successfully with the X-based window manager on either Solaris or Linux machines. In order to do this, we have to understand the principles of GUI programming, and how to make these work with the Motif libraries, and C++.

Principles of Motif programs

All Motif programs, whether C or C++ have the same basic main function which carries out these steps:

  1. initialize the X system
  2. create a main window and possibly other interface items
  3. hook the interface items to the application program
  4. display the interface
  5. enter the event loop, waiting for user interaction

The essence of these steps is that a Motif program does nothing until the user requests some action. The actions are requested by selecting a menu-item, by clicking a button or other window, or by pressing a key. These, collectively are user gestures. Each gesture will trigger an application function to carry out some tasks. Thus the familiar linear sequence of tasks in a program without a user interface is replaced by a collection of tasks, each of which may be triggered by a user gesture. The diagram below shows these two models of programs and how they differ:

Terminology

This basic model is so different there are special words that go with it. The interface itself consists of widgets, which may be windows, menus or various controls, such as buttons, check-boxes, or scroll bars. A user gesture, whether from the keyboard or mouse, corresponds to an event. Events are generated by the widgets that receive the gesture and are either handled by the widget itself (such as turning on a check-box) or is sent on to an application function which is called a callback. So, for instance, the user may select a menu-item. This gesture causes the X system to highlight the item (it calls the window manager to do this) and then to call the callback which has been registered to handle the mouse click event. It is the callback that actually gets work done in the program.

The event loop

The reason the program's main functions call a function that contains a loop (essentially an infinite loop that runs until the user requests it to stop) is that user gestures are asynchronous, i.e. the user must be able to make a gesture independently of where the program's point of execution is. If the program is busy doing something when a gesture occurs, the loop will simply put the event on a queue of events until the program has chance to look at it. The trick is to balance the necessity of remembering all user gestures (not missing a single one) while still allowing normal program execution to proceed. Luckily all the hard work has been done and is encapsulated in the Motif library and the X system itself. The only thing a Motif program has to do to make the balancing act work is to register the callbacks, and call the event loop function. Everything else happens transparently.

Designing the interface

Designing a Motif interface is a pleasant task that can be done independently of writing the code for it. As long as the range of available widgets and their capabilities is know, it is essentially a question of assembling a set of off-the-shelf components into a pleasing design. Issues of usability, simplicity coherence and consistency come into play here. If there is a bottom line, it is keep it simple. Complex interfaces are a major reason for programs being left unused, that might otherwise be prefectly capable of carrying out a task. Menus that are too deeply nested, interfaces with too many windows and buttons that seem to have no effect are common mistakes.

Programming the interface

Widgets have to be created with program code at some point. It is actually possible to make direct calls to the basic X system libraries to do this, but there are so many details to remember it is very difficult. All these library functions start with a single upper-case 'X'. A set of consistent library functions that allow a higher level of programming exist in the X toolkit. These functions start with 'Xt'. However, Motif packages these up in an even higher level programming interface and these functions start with 'Xm'. However, even these functions can be hard to use, so there exists a separate language for designing Motif interfaces that makes the task of taking a design and realizing it in code as simple as possible. This is the language UIL (user interface language). Functions in the UIL library start with 'Mrm' (Motif Resource Manager). [Actually there is an even simpler way to build an interface--the drag and drop visual technique, but we do not have this available for Motif. ]

UIL code looks vaguely like C, and is quite easy to learn. Once the UIL code is written, a compiler (called uil) turns it into a binary form that can be read by a special Mrm library function to create the widgets for the application. The original Motif main function now looks like this:

  1. initialize the X system
  2. call the Mrm function to read in widget binaries
  3. hook the interface items to the application program
  4. display the interface
  5. enter the event loop, waiting for user interaction

C++ to the rescue

One of the strengths of C++ is the ability to hide implementation detail from the programmer. With this in mind, I have incorporated the above steps, since they are the same for any application, into a class called UILApp, and compiled its code into a library. Using this library is as easy as deriving a new class from UILApp, and putting in code to register the callbacks, and code specify the files containing the compiled UIL interface. In the code below, three macros (#define) are used to add a callback, declare a callback and define it externally to the class. A Motif program that uses UIL now looks like this:

// UILApp

#include <iostream.h>
#include <Mrm/MrmAppl.h>
#include "uilapp.h"

//
// derive an application class
//

class MyApp : public UILApp {
public:
  MyApp() {
    // register the callbacks with Motif -- this application has two: print and quit
    // are hooked to interface components
    ADD_CALLBACK(print);
    ADD_CALLBACK(quit);
  }
  // declare the callbacks
  DECLARE_CALLBACK(print);
  DECLARE_CALLBACK(quit);
};

// define each callback function
DEFINE_CALLBACK(MyApp, quit) {
  cout << "Exiting..." << endl;
  exit(0);
}

DEFINE_CALLBACK(MyApp, print) {
  char* message = (char*)client_data;
  cout << message << endl;
}

int main(int argc, char* argv[]) {
  // the main fucntion crates an application object, registers
  // any number of UIL module files, and calls run
  MyApp app;
  app.AddUILModule("cb");
  app.run(argc, argv);
}

 

It may be worth while looking at the code for the library class UILApp, its header file and, of course, the UIL interface sepcification.