A tutorial designed to help you build your own GUI.
This article is directed toward those who have spent their time in the pleasant arena of development and are finding it harder and harder to ignore the growth of graphical front-ends. My perspective is that of a developer of commercial UNIX applications distributed largely on commercial versions of UNIX. I have an interest in making these applications easily portable to Linux and developing skills that are useful across OS boundaries. Given a choice, I prefer to use technologies which can be readily applied in both the commercial and open-source worlds.
My goal is to give you a basic understanding of the essential components of an X/Motif application—enough to begin developing your own applications. Development of server-side programs or command-line interfaces (CLIs) tends to be more cut-and-dried than that of graphical user interfaces (GUIs). Human beings are notoriously unpredictable, especially when one is used to developing according to strict protocols and well-defined boundaries. The complicated interaction with a GUI is usually best handled by a toolkit or library—someone else can worry about the details of where the mouse pointer happened to be when the left button was pressed, and how to render those beautiful 3-D effects on the components on the screen. Although every UNIX ships with libraries to take care of these details (X, Xt), they are still quite tedious and time-consuming. Motif is a library that provides enough insulation from the underlying primitives to make development of GUIs realistic, while still allowing easy access to those primitives should you need them.
Strictly speaking, Motif is a standard put forward by the Open Software Foundation (OSF) that describes expected behavior and the look and feel of an application. An application can be described as Motif-compliant; OSF even provides for branding of applications that adhere to the Motif standard. The practical utility of the standard is arguable; however, the benefits of the framework provided by the Motif libraries is unquestionable. You can build a Motif application that uses the libraries but is not compliant with the standard; in fact, most programmers simply use the library and ignore the more Byzantine dictums of the standards.
Henceforth, when I say Motif, I mean the Motif library or any one of the various API-compatible libraries (Lesstif, Mootif, Moteeth, etc.). Differences between Motif (v. 1.2) and Lesstif will be pointed out. Although Motif 2.1 has been released, I will focus on version 1.2 since this is the revision level of most installed clients and the most common open-source alternative, Lesstif. The newer releases of Motif are generally backward compatible, although some techniques and elements of the API may be deprecated. The biggest differences between versions 1.2 and 2.1 include thread-safe libraries and some additional widgets, such as the Notebook.
Why Motif?
My decision to use Motif is motivated by several very specific concerns, some of which will strike a chord in those of you involved with commercial software development. The tools I use in commercial efforts must:
- be tried and proven—the less risk the better
- be readily available to the target end users
- not place undue burdens on development time
- have a reasonable learning curve (for my sake)
- have a reasonable licensing policy, preferably no additional run-time fee for the end users (for marketing’s sake)
- utilize a development skill set that is relatively easy to find (have mercy on your manager)
The Motif library is a toolkit that meets my requirements. Virtually every commercial UNIX ships with a run-time license for Motif. Free UNIX variants (including Linux and BSD) also have access to Motif libraries (although at a fee); however, a Motif “clone” called Lesstif is now stable and mature enough to use in place of a “real” Motif library. Alternative GUI toolkits exist (GTK, Qt, etc.) and may be a good choice for projects which dodge the commercial world—for those of us who make a living writing software for commercial distributions, I think Motif is still a good choice.
The Common Desktop Environment (CDE) that ships with most commercial UNIX operating systems is built on Motif 2.1, which was intended to provide a backward-compatible API for applications built on version 1.2. I have found I can work on the same project on Solaris (using Motif) and Linux (using Lesstif), moving the source back and forth regularly with surprisingly few hiccups.
Finally, the end product looks sharp. Motif makes it easy to get a professional-looking GUI without any extra effort. 3-D effects, text clipboard operations (cut, copy and paste) and other trappings users have come to expect from even small applications are taken care of automatically, so you can focus on your application’s problem domain.
Getting and Installing Motif/Lesstif
A Motif development license can be had from Red Hat software (http://www.redhat.com/) or the Open Group (http://www.opengroup.org/desktop/motif/). If you have to pay for the development license, I strongly recommend you use Lesstif and find a commercial workstation with a Motif run-time license already installed. Odds are, you can tweak any errant code without needing to purchase Motif. If you have a spare machine on which to install Solaris for the Intel platform, Sun now makes Solaris available for non-commercial use for the cost of the media (about $10 US); this includes a run-time license for Motif and the headers needed for development. It is fairly easy to do your development on Linux/Lesstif, then make a quick build under Solaris/Motif to check compatibility.
Remember, though, Lesstif is an implementation of Motif version 1.2 with limited support for Motif 2.0, while the commercial implementation of Motif 2.1 has already been released. The good news is the vast majority of systems are still using Motif 1.2.
Lesstif can be downloaded from a number of places, beginning with http://www.hungryprogrammers.org/. Binaries are available for Linux (and some other UNIX flavors) as RPMs or gzipped tar files from the site and its mirrors. Installing Lesstif on my Red Hat-based workstation was as simple as typing:
rpm -i lesstif.rpm
You don’t need the development RPM unless you plan on contributing to the Lesstif project.
For most commercial UNIX systems, the developer will not need to do anything extra to start developing using Motif; for example, Sun ships the Motif headers and libraries with Solaris. If you choose a commercial Motif license for Linux, you will need to follow the steps outlined in the package. Getting things rolling on Slackware or some other non-RPM-based system is rather trivial. The libXm files should end up in the /usr/X11R6/lib directory (or /usr/dt/lib on Solaris), and the headers should be placed in their own directory (named /Xm) off the /X11 include directory, e.g., /usr/X11R6/include/Xm or /usr/dt/include/Xm.
X Components
Access to the X Window System in a Motif application is accomplished primarily through three libraries: the X library (commonly referred to as Xlib) which provides the most primitive interface, the X Toolkit (Xt) upon which Motif is built and Motif (see Figure 1). A book covering Xt, or at least the Motif Programming Manual, would be a good investment if you become serious about GUI development. Many libraries choose to build on Xt rather than X, so it is reasonable to expect X and Xt to be consistent across UNIX flavors and available on any end-user configuration.
The X library functions are typically prefixed with “X”, and the X toolkit widgets and functions are prefixed with “Xt”. Although the toolkit is used fairly heavily within a Motif application, the need to access X library routines is rare. Symbols provided by Motif (functions, constants and variables) are prefixed with “Xm”.
The X server via the window manager is responsible for interacting with the video hardware and dispatching messages and events to the applications. It is important to note applications running under X are implemented using an event-driven paradigm. The programs generally spend their time waiting to receive messages that indicate such events as mouse movement, key presses, or exposed windows—more on this later.
Graphic Primitives
The graphic components used to build an X/Motif application are called widgets. Some simple widgets are a push button, a radio button and a text field; more-involved widgets might be a file selection dialog or an HTML widget. Many graphic components are composites of widgets coupled within a routine or C++ class. You could spend your career developing GUIs without ever learning how to create a custom widget. It is best to avoid writing custom widgets until you are very comfortable with the X/Motif environment; even then, you should have good reasons for not using more conventional techniques. The Motif widget libraries have been implemented in a very object-oriented fashion (especially for something written entirely in C). If this sort of thing interests you, I strongly encourage a study of the widget libraries (refer to the Lesstif source). Many widgets are available that implement functionality not addressed by the Motif library.
A number of generic routines can be used to create and alter instances of widgets (e.g., XtCreateManagedWidget); in addition, widgets typically make some specific routines available (e.g., XmCreateScrolledText). The run-time behavior of a widget is controlled through resources; that is, variables specific to a single instance of the widget which determine aspects of its behavior such as position, color, button labels, etc.
Getting Started
A good way to get a grip on a new technology is by looking at something built with it. Motif provides a rich and extensible environment, and it would take far too long to explain all the details behind the steps I will take; in fact, I will just scratch the surface of what can be done using Motif. The application I will use to illustrate Motif is a simple text-file viewer (see Figure 2). You should download the source (it’s long) from ftp.linuxjournal.com/pub/lj/listings/issue64/3392.tgz. I will refer to specific line numbers as I explain different aspects of the Motif API.
Motif (and X) development takes advantage of “Event-Driven Programming” using an event-dispatch loop. An application must set up the interface (or at least some minimal components) before turning control over to the loop that handles events (via XtAppMainLoop). Once the event loop is invoked, your code will be called only in response to an event. The important thing to remember is that as long as your code is running, your application is not communicating with the X server. You must take care to return control to the event loop regularly, especially during time-intensive operations. Callbacks, which are functions intended to be invoked by the event loop in response to a specific message, are used heavily. Motif and the Xt library (upon which Motif is built) take care of the vast majority of the events an application might receive—the only ones you must handle are those for which the default behavior is inadequate.
Compiling a Motif application is not too involved—you must ensure the Motif header files are available and the libraries are listed in the correct order on the link command. The following command will build the sample application on Linux using Lesstif or Motif:
gcc -o xtxtvw xtxtvw.c -I/usr/X11R6/include\ -L/usr/X11R6/lib -lXm -lXmu -lXt -lX11
To build the application on Solaris, issue the following command:
gcc -o xtxtvw xtxtvw.c -I/usr/dt/include\ -L/usr/openwin/lib -lXm -lXmu -lXt -lX11
If these don’t work, look for a directory called /include/Xm in the directory tree containing your X Window System and specify that directory. The libraries are typically in the X library directory—search for a file named libXm*. Motif is most often made available as a shared library; the version will vary depending on your system. If you cannot find one or the other (especially the headers), you might not have loaded the X or Motif development package.
Setting Up the Application
Let’s start by examining the code used to initialize the application, in order to cover some of the essentials. A fair amount of work has been done to make life easier on developers of international software; localization and internationalization are addressed by a number of features available in the X and Motif libraries. I will avoid treating this issue at all, except to say that you should make a habit of including a call to XtSetLanguageProc in all of your X applications.
The second toolkit function I call is XtAppInitialize (line 85), which has a few interesting calling arguments. This function is responsible for initializing the Xt library, evaluating any command-line arguments meant for the toolkit (such as geometry) and initializing X resources used by your application. context is a return parameter used by the X toolkit; you will not typically do much with this argument, apart from passing it to the main event loop. It is a work area for the libraries—a place for them to store state information about instances of widgets in your application. The second parameter is the class of your application. It is used to refer to resources that belong to the application by both internal and external clients, such as editres.
Listing 1
/* xtxtvw.c X11/Motif based text file viewer - example program meant to accompany Linux Journal article on Motif development the following command should build the application on most linux systems gcc -o xtxtvw xtxtvw.c -I/usr/X11R6/include -L/usr/X11R6/lib -lXm -lXt -lX11 the following command should build the application on many Solaris systems gcc -o xtxtvw xtxtvw.c -I/usr/dt/include -L/usr/openwin/lib -lXm -lXt -lX11 */ #include <stdlib.h> #include <unistd.h> #include <X11/Xmu/Editres.h> #include <Xm/FileSB.h> #include <Xm/Form.h> #include <Xm/LabelG.h> #include <Xm/PushB.h> #include <Xm/RowColumn.h> /* for the menu widget */ #include <Xm/SelectioB.h> #include <Xm/Text.h> #include <Xm/TextF.h> /* the class is used to identify application resources */ const char *X_APP_CLASS = "XTxtVw"; /* reasonable default values for some essential resources */ const String FALLBACKRES[] = { "*background: grey" , "*forground: black" /* don't use * here otherwise all widgets will have these dimensions */ , "XTxtVw.width: 400" , "XTxtVw.height: 400" , NULL }; /* most applications have only 1 top level shell */ Widget g_topshell; /* the file selection dialog is invoked from multiple routines so it is easiest to make the widget global */ Widget g_filedlg; /* we use the widget to keep the file name */ Widget g_filenamew; /* we would like to alter the text display from many routines so... */ Widget g_filetextw; /* callback routine names are suffixed with CB by convention */ void fileCB(Widget widget, XtPointer client_data, XtPointer call_data); void readfileCB(Widget widget, XtPointer client_data, XtPointer call_data); void fileselCB(Widget widget, XtPointer client_data, XtPointer call_data); void file_okCB(Widget widget, XtPointer client_data, XtPointer call_data); void setMainTitle(char *fn); /*---------------------------------------- main */ int main(int argc, char *argv[]) { XtAppContext app; Widget menubar; Widget form; Widget label; Widget browseb; Widget w; XmString xms_file; XmString xms_open; XmString xms_exit; XmString xms_openacc; XmString xms_exitacc; char *fn; Arg args[10]; int nargs; /* initialize toolkit and create top level shell */ XtSetLanguageProc(NULL, NULL, NULL); g_topshell = XtAppInitialize(&app, X_APP_CLASS, NULL, 0, &argc, argv , (String *) FALLBACKRES, NULL, 0); /* enable editres support */ XtAddEventHandler(g_topshell, (EventMask) 0, True , (XtEventHandler) _XEditResCheckMessages, 0); setMainTitle(NULL); /* handle command line arguments that remain - Xt has already taken care of any arguments intended for it, what remains is yours */ if(argc > 1) fn = argv[1]; /* build GUI components */ form = XmCreateForm(g_topshell, "form", NULL, 0); /* build the menu bar */ /* most of the strings that get displayed by Motif are handled as the XmString type in order to support localization */ xms_file = XmStringCreateLocalized("File"); /* XmVa* functions require a trailing 'NULL' argument */ menubar = XmVaCreateSimpleMenuBar(form, "menubar" , XmVaCASCADEBUTTON, xms_file, 'F' , XmNtopAttachment, XmATTACH_FORM , XmNleftAttachment, XmATTACH_FORM , XmNrightAttachment, XmATTACH_FORM , NULL); /* if Motif Creates or Gets an XmString then you must normally explicitly release the associated storage */ XmStringFree(xms_file); /* build the File pulldown menu */ xms_open = XmStringCreateLocalized("Open"); xms_openacc = XmStringCreateLocalized("Ctrl+O"); xms_exit = XmStringCreateLocalized("Exit"); xms_exitacc = XmStringCreateLocalized("Ctrl+X"); XmVaCreateSimplePulldownMenu(menubar, "filemenu", 0, fileCB , XmVaPUSHBUTTON, xms_open, 'O', "Ctrl<Key>O", xms_openacc , XmVaSEPARATOR , XmVaPUSHBUTTON, xms_exit, 'x', "Ctrl<Key>X", xms_exitacc , NULL); XmStringFree(xms_open); XmStringFree(xms_exit); XtManageChild(menubar); /* build file selection dialog that will be used by the Browse button */ g_filedlg = XmCreateFileSelectionDialog(g_topshell, "filesb", NULL, 0); XtAddCallback(g_filedlg, XmNokCallback, fileselCB, NULL); XtAddCallback(g_filedlg, XmNcancelCallback, fileselCB, NULL); w = XmSelectionBoxGetChild(g_filedlg, XmDIALOG_HELP_BUTTON); XtUnmanageChild(w); w = XmSelectionBoxGetChild(g_filedlg, XmDIALOG_APPLY_BUTTON); XtUnmanageChild(w); /* build the filename entry/display area */ label = XtVaCreateManagedWidget("Filename: ", xmLabelGadgetClass, form , XmNtopAttachment, XmATTACH_WIDGET , XmNtopWidget, menubar , XmNtopOffset, 10 , XmNleftAttachment, XmATTACH_FORM , NULL); g_filenamew = XtVaCreateManagedWidget("filename", xmTextFieldWidgetClass, form , XmNtopAttachment, XmATTACH_WIDGET , XmNtopWidget, menubar , XmNtopOffset, 5 , XmNleftAttachment, XmATTACH_WIDGET , XmNleftWidget, label , XmNleftOffset, 2 , NULL); browseb = XtVaCreateManagedWidget("Browse", xmPushButtonWidgetClass, form , XmNtopAttachment, XmATTACH_WIDGET , XmNtopWidget, menubar , XmNtopOffset, 6 , XmNrightAttachment, XmATTACH_FORM , XmNrightOffset, 10 , NULL); XtVaSetValues(g_filenamew , XmNrightAttachment, XmATTACH_WIDGET , XmNrightWidget, browseb , XmNrightOffset, 10 , NULL); /* build the file contents area */ /* an alternative to using the XmVa*() is to build an argument list and passing it to the function. I have noticed that sometimes Lesstif/gcc and the XmVa*() functions can choke the compiler */ nargs = 0; XtSetArg(args[nargs], XmNtopAttachment, XmATTACH_WIDGET); nargs++; XtSetArg(args[nargs], XmNtopWidget, g_filenamew); nargs++; XtSetArg(args[nargs], XmNleftAttachment, XmATTACH_FORM); nargs++; XtSetArg(args[nargs], XmNrightAttachment, XmATTACH_FORM); nargs++; XtSetArg(args[nargs], XmNbottomAttachment, XmATTACH_FORM); nargs++; XtSetArg(args[nargs], XmNeditable, False); nargs++; XtSetArg(args[nargs], XmNeditMode, XmMULTI_LINE_EDIT); nargs++; g_filetextw = XmCreateScrolledText(form, "filetext", args, nargs); XtManageChild(g_filetextw); /* tie in some callbacks to make this thing live */ XtAddCallback(g_filenamew, XmNactivateCallback, readfileCB, NULL); XtAddCallback(browseb, XmNactivateCallback, fileCB, NULL); /* start up the X event loop and expose the GUI */ XtManageChild(form); XtRealizeWidget(g_topshell); XtAppMainLoop(app); return 0; } /* main */ /*---------------------------------------- fileCB file menu callback client_data is a reserved for your use normally, call_data is typically valued by Motif - see fileselCB */ void fileCB(Widget widget, XtPointer client_data, XtPointer call_data) { static Widget filename; int menuitem; if(call_data == NULL) { filename = (Widget) client_data; return; } menuitem = (int) client_data; switch(menuitem) { case 0: /* open */ XtManageChild(g_filedlg); break; case 1: /* exit */ exit(0); break; default: break; } /* switch */ return; } /* fileCB */ /*---------------------------------------- fileselCB callback used by the action buttons in the file selection dialog Motif callbacks usually populate the call_data parameter with a widget specific Callback structure, in this case it is a superset of XmSelectionBoxCallbackStruct (the first few members are identical) */ void fileselCB(Widget widget, XtPointer client_data, XtPointer call_data) { char *fn; int allswell = 0; XmFileSelectionBoxCallbackStruct *cbs; cbs = (XmFileSelectionBoxCallbackStruct *) call_data; if(cbs->reason == XmCR_OK) { if(!XmStringGetLtoR(cbs->value, XmFONTLIST_DEFAULT_TAG, &fn)) return; if(strlen(fn) > 0 && access(fn, R_OK) == 0) { XmTextFieldSetString(g_filenamew, fn); allswell = 1; } XtFree(fn); } /* dialog will disappear but it is not deallocated - may need it later */ XtUnmanageChild(g_filedlg); if(allswell) { XmUpdateDisplay(g_topshell); readfileCB(NULL, NULL, NULL); } return; } /* fileselCB */ /*---------------------------------------- readfileCB callback used to trigger file-read invoked by user pressing Enter on the filename text field or by selecting OK from the file selection box */ void readfileCB(Widget widget, XtPointer client_data, XtPointer call_data) { const int bufsz = 1024; FILE *fh; char *fn; char buf[bufsz]; char *txt; char *msg; int pos = 0; /* clear the text display */ XmTextSetString(g_filetextw, ""); fn = XmTextFieldGetString(g_filenamew); fh = fopen(fn, "r"); if(fh) { setMainTitle(fn); while(fgets(buf, bufsz, fh)) { XmTextInsert(g_filetextw, pos, buf); pos += strlen(buf); } fclose(fh); } else { msg = (char *) malloc(strlen(fn) + 9); sprintf(msg, "fopen (%s)", fn); perror(msg); free(msg); } XtFree(fn); return; } /* readfileCB */ /*---------------------------------------- setMainTitle set the application title - icon titles should also be assigned here */ void setMainTitle(char *fn) { Arg args[3]; int nargs = 0; char title[256]; char *p = NULL; /* strip any path info from the filename */ if(fn) { p = strrchr(fn, '/'); if(p) p++; else p = fn; } sprintf(title, "xTxtVw%s%s", (p ? ": " : ""), (p ? p : "")); XtSetArg(args[nargs], XmNtitle, title); nargs++; XtSetArg(args[nargs], XmNiconName, title); nargs++; XtSetValues(g_topshell, args, nargs); return; } /* setMainTitle */ /* xtxtvw.c */
The fifth and sixth arguments to XtAppInitialize are the argument count and argument list passed on the command line. The X toolkit will consume any command-line options intended for it, for example the location and initial size of the application’s main window and the default background color. See the X11 man page to get a better idea of exactly which options are accepted. After this call, the remaining options can be processed by your application.
The other parameter of interest is the fallback resource string array. This is optional, but a good idea, as it specifies default values for resources used by the widgets in the application. Command-line options and any hard-coded values will override the settings listed here, but you should make a habit of specifying at least the background and foreground colors (and arguably the fonts) to help keep the interface appearance consistent.
The widget that gets created by XtAppInitialize is the application shell, containing the window that appears on the desktop as the application. This is a special widget which provides interaction with the window manager. Applications can have multiple shells if more than one primary window is required. This sample application has a single shell that ultimately parents all other widgets.
Application and Widget Resources
The application and the widgets used within that application are presented on the display, based on their resource values. Even a simple text label has a set of resources used by X to determine its appearance. It is probably best to think of resources as data members for a specific instance of a widget. Some examples of resources are colors, the text of a label or text-entry widget, size, location, font and callback routines for events. Widgets are constructed in a hierarchy: each widget has a parent widget, and each widget may have zero or more children. Each resource is a name,value pair. The name is comprised of the application class, the name of each widget in the hierarchy and the actual resource name, in that order.
X and Motif define a number of primitive types which can be used as values for resources such as XmString (the Motif display string), Widget (any widget), Position (location) and XtPointer (used for passing data of any type to callback functions). Resource values can be retrieved and set using XtSetValue and XtGetValue. (There are also the XtVa* versions of these functions.) If you specify the wrong type for a value, you won’t find out until runtime, so it is a good idea to verify the types using a man page or the reference manual. It is also important to keep an eye on your application’s STDERR output for messages from the X libraries.
An example of a name for a Widget resource would be “.xtxtvw.title” (the text in the application window title bar). It could be specified in the ~/.Xdefaults file as the application resource “XTxtVw.xtxtvw.title”. I encourage you to explore resource names using editres (see the section on debugging). The X server maintains a database of resource names for all applications. These specifications come from a number of sources, including the initialization file for your window manager (such as ~/.fvwmrc), the X defaults (~/.Xdefaults), application-specific defaults (on Linux /usr/X11R6/lib/X11/app-defaults, on Solaris /usr/openwin/lib/app-defaults) and runtime specification by the application.
The xrdb command provides a means for viewing and changing the resource settings in the database at runtime (changes affect only widgets created afterward), although editres is far easier to use for applications which support it. Take a look at the manual page for xrdb, but note it typically replaces the database settings. If you want to merge additional settings, you must specify this using the -merge option.
I specify some application resources in the fallback resources passed to the initialize function. A simple example of setting some widget resources can be seen in the setMainTitle routine.
Widgets
Some widgets are designed to handle layout and event management for other widgets. These are called manager widgets and include XmForm, XmBulletinBoard and XmRowColumn. A number of widgets derived from the manager widgets provide enhanced behavior, such as the XmSimpleMenubar.
XmBulletinBoard provides the means for placement of widgets at specific locations—you must specify the X and Y offsets within the bulletin board. This widget is generally not used alone, but instead to derive other more useful widgets such as XmForm. XmForm allows you to specify the placement of widgets relative to others within a form. This is probably the most useful and commonly used manager, since it automatically handles resizing and dynamic changes in its children widgets at run-time.
When a widget is created, it is not actually displayed until it and every widget in the hierarchy above it is managed. Widgets created as children of an XmForm widget will not appear on the screen until the form widget is managed via a call to XtManageChild. This will cause every managed widget in the form to suddenly appear. Typically, you will want to create a manager widget as the single child of the top-level shell, then use that widget as a parent for the remaining GUI components (line 103).
Another widget used in the application is XmLabel (line 157). This is used for simple output of text strings and images that typically do not interact with the user. Thus, labels are often created as Gadgets, a special sort of widget that defers much of its event handling to its parent. This means fewer callbacks are registered, fewer events get dispatched and generally helps improve performance. I try to use gadgets when I do not expect a widget to interact with the user or when there are many instances of a particular widget (such as rows in a table). Push buttons (line 173) are derived from XmLabel.
The XmTextField widget (line 164) is used to provide a single-line data entry field or a more flexible output mechanism. The text field is a specialized sort of text widget intended for situations where the more fully featured XmText widget would be overkill. The XmText widget can do everything the XmTextField can, and more. The XmScrolledText widget (line 202) is used to display large amounts of text and is actually packed with enough features to be used as a nice text editor. In fact, you could implement a text editor using the example application by adding fewer than 20 lines of code.
Strings
Motif uses a special representation for strings that are intended to be used in the display—usually of type XmString. Many widget resources require values of type XmString, and the most common method for creating them is via a call to XmStringCreateLocalized (line 110). Note that storage is allocated for created strings, and it must be returned via a call to XmStringFree when no longer needed (line 124). If the application must be internationalized at a later time, strings constructed this way will be presented in the local language and typeface.
Strings can also be retrieved from widget resources using XmStringGetLtoR (line 277); this call allocates storage for a character array using memory management routines available in the Xt library (XtMalloc). Storage for strings created using this technique must be freed using XtFree (line 284). Some convenience functions provided by Motif, such as XmTextGetString (line 323), also use Xt memory management, and their results must be released using XtFree.
“Normal” character arrays are still used in a number of places in Motif, but it is important to recognize that XmString will pop up all over the place. This is another area where the loosely typed nature of C is going to rear its ugly head. Be careful about releasing storage associated with these functions—memory leaks in applications using these functions are quite common.
Creating Widgets
The menu bar created in line 114 illustrates some important issues; the most obvious is the variable argument list accepted by the function. The Xt and Motif functions rely heavily on variable argument lists; the advantage of this is simplified semantics, the disadvantage is no compile-time-type checking. A good defense against errors is to carefully columnize the argument lists in your source—it helps provide a visual check that the call is properly constructed. Some errors in the argument lists will show up on STDERR at runtime, and some will just pass quietly.
The first argument to the function is the widget that will parent the one being created; the second is the name of the widget as it will appear in the resource database (try to find this using editres). The remaining arguments are resource specifications as pairs of resources and their associated values. The entire list is always terminated by NULL.
An alternative method of creating widgets, used beginning with line 194, requires you to generate an argument list. The create call is similar to the variable-argument list in that the parent and widget name are the first two arguments, followed by the number of arguments in the list and the list itself. It is common practice to define an array of type Arg (this is from Xt) and reuse it in each call. Although the fixed-length argument list is a bit more cumbersome, some widgets do not provide a variable-argument create call. In addition, I have had trouble with gcc 2.7.2 and some of the variable-argument functions—it has something to do with macros used by Lesstif. If you receive odd complaints from the compiler on one of the variable-argument list functions, you may want to try converting it to the fixed-length argument list function. I have had none of these sorts of problems with the newer releases of gcc and Lesstif.
The resources not specified in the call to the create function will be valued from the resource database maintained by the X server and from defaults specific to the widget. You can also change them later by calling XtSetValues (line 376) or XtVaSetValues (line 181). In fact, this is the most common method employed to change aspects of the interface at runtime. Some Motif widgets also provide convenience functions to accomplish the same thing (such as XmTextSetString). They have the distinct advantage of providing compile-time-type checking and usually present simpler semantics.
Widgets are often created as unmanaged—this defers their display until they are managed. In fact, widgets are not managed by default and require a call to XtManageChild to get them to actually display. There is a way of building widgets which are managed from the time they are created, that is, by using the XtVaCreateManagedWidget and XtCreateManagedWidget functions (line 157). Since this is a call to the Xt library, the order of the initial arguments differs slightly from the Motif syntax. The first is the name of the widget (normally the second argument in the Motif create functions); the second argument is the widget class, a constant that matches the name in the Motif create function with the first character in lower case. The third argument is the parent widget, which is followed by the resource names and values.
Menu Details
Motif provides a simple menu bar that is easy to build but makes some assumptions about menu behavior. For a more flexible widget with more involved menus, you will want to explore XmMenuBar. Note that the menu bar is created as a child of the form. The menu bar built on line 114 specifies a single menu as a cascade button (line 115). If you want multiple pull-down menus, you simply add more lines. The string is the name of the menu as it will appear in the menu bar, and the single letter is the meta keystroke that will cause the menu to pull down (in this case, alt-F or meta-F).
Once the menu bar is constructed, the actual pull-down menus can be created as children of the menu-bar widget. Line 133 specifies the parent widget and name for the “File” menu; the arguments indicate the initial item and the callback function invoked if the user selects a menu item. Each of the subsequent lines describes a single menu item and each item begins with its type. The Menus can contain more than just the traditional push buttons; for more information, refer to the Motif Programming Manual. The next argument is the string displayed for the menu item, followed by a hot key the user can press when the menu is displayed, then an accelerator key sequence the user can press to invoke the menu item without actually pulling down the menu. The last argument for the item is the display string for the accelerator keystroke—this will be displayed right-justified in the menu.
Horizontal lines (separators) can be placed in the menu by specifying XmSEPARATOR for the type (with no associated values) as in line 135. This is another case where columnizing your source is a very good idea. It is easy to leave out one of the items, resulting in catastrophic (or at least bizarre) behavior at runtime. If you don’t want to specify one of the options, a place holder such as an empty string must be used.
Once all the pull-down menus are created, the menu bar must be managed (line 142), so that when the form gets managed, it will be free to display the menu bar. In this case (as with some of the specialty manager widgets), it is not necessary to manage the pull-down menus. Remember, they aren’t actually displayed until the user does something to make them appear.
File Selection Dialog
The next widget we instantiate is the file selection dialog. It will not actually appear until invoked by the user (by a menu selection or button press), but I chose to build it here since it is logically related to the file menu. Motif provides a number of very nice specialty dialog boxes; one of the most interesting is the File Selection Dialog, XmFileSelectionDialog. This widget is derived from XmSelectionBox, so when you are looking for some of the resources in the Motif Reference Manual, you may need to refer to both widgets.
At times, it may be more convenient to assign callbacks after creating a widget, either because you don’t have all the information you need at the time, or the callbacks cannot be easily supplied to the create function as in our example. Another reason is the explicit call to XtAddCallback makes it obvious the assignment took place; in addition, you get the benefit of compile-time-type checking.
The stock file-selection dialog handles all heavy lifting for you, file name globing, directory navigation and even file name filtering. The default dialog also provides a few extra buttons I will not use. To avoid confusing the user, set their XmNSensitive resource to false (causing them to be grayed out) or remove them altogether. Motif provides a convenience function for all widgets derived from the Selection Box to get the widget ID of specific children widgets in the dialog (line 150), XmSelectionBoxGetChild. Once I have the widget ID, a call to XtUnmanageChild will prevent that particular widget from appearing when the rest of the dialog is displayed.
Form Attachments
The code on lines 157 to 204 is concerned with adding the remaining components to the display. I will use these widgets to explain the way XmForm handles the layout of its children. Attachments describe a widget’s relationship with other widgets within the form; it may take a while to get used to the way attachments work. Widgets appear in the form in the order they are created—important to keep in mind as you code. I will describe the most basic aspects of form attachments.
Each widget parented by a form has attachment resources for the top, bottom, left and right sides. I will cover how to use XmATTACH_FORM and XmATTACH_WIDGET and attachment offsets. Other attachment types include XmATTACH_NONE, XmATTACH_SELF, XmATTACH_POSITION and some based on form and widget opposing sides. On line 116, you see the top, left and right sides of the menu bar are attached to the form. This means each of the sides will “stick” to the respective sides of the form, stretching the widget as needed. The bottom attachment is left as XmATTACH_NONE, since I need to use the remaining portion of the form for other display elements. If the attachments were not specified, the size and position of the menu bar would default to a fairly unattractive box in the upper left corner of the form.
On line 158, I specify that the top of the label is attached to the menu bar widget and line 159 specifies this attachment will stay 10 pixels off the menu. The left side of the label is attached to the form and the right and bottom sides are left unattached. If attachments were specified and a border was visible for the label, you would see it stretch as the position of the other widgets changed during a resize operation.
Mistakes in attachments can hide a widget altogether or generate ugly screens; in some cases, the errors are bad enough to be printed on STDERR (e.g., circular dependencies). If you specify a widget attachment, you must also specify the widget to which the side should be attached. Since no compile-time checking is done on these things, you won’t discover your omission until runtime.
I want the text field used to enter a file name to stretch horizontally; so once the Browse button is created, I add a final attachment to the text field (line 181). This is an excellent example of the strength of using XmForm for your layouts.
Exposing the Interface
Once the interface has been constructed, a call to XtRealizeWidget (line 215) for the application’s top-level shell will cause all of the managed widgets in the hierarchy parented by that shell to display. Widgets can be constructed and destroyed at runtime. You are not required to create them all before exposing them; in fact, in some cases you will not have enough information to do so, but as much as possible of the main screen of the application should appear on startup.
The call to XtAppMainLoop (line 217) relinquishes control to the event dispatch loop. The only way for code in your application to execute from this point is in response to an event. Many events (such as expose and hide) are handled automatically by the Xt library. It is generally a good idea to assume XtAppMainLoop will never return. Applications commonly provide a routine to handle shutdown tasks such as closing files and cleaning up temporary workspaces. This routine would be called from the “Exit” menu item, for example.
The callback functions installed for the various widgets will actually give life to your application. Mundane tasks such as reacting to mouse button presses and keyboard input are all handled by the libraries supporting the instantiated widgets. This can make the application challenging to debug, since the flow of control is rather flexible, and it underscores the importance of careful design. Callbacks that call other functions invoking actions of other widgets—you get the idea. There is no easy way to automatically generate a call graph with this sort of implementation.
If your application needs to do something time consuming, it may be best to use a co-process, or at the very least, issue regular calls to XmUpdateDisplay (as on line 293). This will prevent the GUI from appearing locked. A number of different approaches for implementing progress bars and other user feedback are also available. You must remember that your application can react to the user’s input only when it is not executing your code.
Callback Functions
Callbacks can be intimidating to deal with initially, but they are nothing more than pointers to functions placed in an event table. When the event for which they are registered occurs, the event dispatch loop invokes them with a few parameters. Since callbacks are passed limited state information, much of that information must be retrieved from the environment.
Callbacks are associated with events explicitly or implicitly. The XmVaCreateSimplePulldownMenu call on line 133 implicitly registers fileCB to be called whenever the user selects a menu item. The call to XtAddCallback on line 147 registers fileselCB to be called when the OK button in the file selection dialog box is pressed.
When Xt or Motif invokes a callback, it passes the widget for which the callback was registered, a single piece of client-specified data and a callback data structure specific to the widget and event for which the callback was invoked. When a callback is added to a widget via XtAddCallback, the final argument to the function is user-specified data of type XtPointer. This gives you a hook to pass information via pointers to structures. The user data element would then be cast back to the original type within the callback—be very careful here, unless you enjoy generating core files.
The call_data parameter is usually an instance of a callback structure derived from a basic callback structure—the first few elements in all of the Xm*CallbackStruct types are the same. For XmFileSelectionBox, the structure is defined as:
typedef struct { int reason; XEvent * event; XmString value; int length; XmString mask; int mask_length; XmString dir; int dir_length; XmString pattern; int pattern_length; } XmFileSelectionBoxCallbackStruct;
The meaning of each element is explained in detail in the Motif Reference Manual, and I make use of the reason and value elements on lines 275 and 277. The reason tells you why the callback was invoked; this is especially important with something like the file selection box, since it is common to use the same callback for multiple events. The value member is the actual file that was selected (its complete path). A quick look at an instance of a file selection box on the screen will give you a good idea of how the other elements are used. If you are not sure about the type of callback structure at runtime, you should cast it to type XmAnyCallbackStruct. This at least has the reason and event members which can be used to determine the nature of the call.
Debugging and Experimenting
Debugging Motif applications can be challenging, due to the heavy use of callback functions. I find the trusty printf is often the quickest way to ferret out anomalies. The situation is not improved by the way the library relies on type casting to implement the object-oriented design of the widget libraries. Refer to documentation on function signatures unless you are absolutely certain you have it right.
A program called editres provides a mechanism to quickly tweak resource settings at runtime. By pointing the program to your running application, you can retrieve the widget tree and examine or change resources for nearly every widget instance. This is a great way to experiment with alternative fonts, colors, attachments and labels. Applications that are properly linked can speak the editres protocol, hosting a two-way communications link to the outside world without changing a single line of code.
In order to add support for editres to your application, add the following two lines:
#include <X11/Xmu/Editres.h> XtAddEventHandler(g_topshell, (EventMask) 0,\ True,\ (XtEventHandler) _XEditResCheckMessages, 0);
and link against the Xmu library—place it after the Motif library on the link command. Kenton Lee has a good article on using editres at www.rahul.net/kenton/editres.html. The source for editres is included with X distributions, so any machine running X should have the utility.
Alternative Approaches
Coding an application using the technique described here can be a bit tedious at times, although it yields the most flexibility and control over the interface. There are many different approaches to developing Motif applications, including wrapper libraries and GUI builders. Two of the most common are the User Interface Language (UIL) and the Motif Tools library. Both reduce the amount of energy that goes into coding, and each has different advantages.
UIL is a C-like description language used to create an interface implemented via Motif and other Xt widgets. The UIL files are compiled and fed to the Motif resource manager (MRM) at runtime; the MRM then renders the interface, installs callbacks, etc. The chief advantages of UIL are rapid layout of screens, extensive error checking, easier internationalization and less complex code. The main disadvantages of UIL are reduced flexibility (due to the language’s simplicity), which could require additional coding in Motif, yet another language to learn, and instability problems. Another issue for the aspiring Lesstif developer is that UIL/MRM is not yet fully supported.
Motif Tools (Xmt) is a library that came with the O’Reilly book, Motif Tools by David Flanagan. This library provides a few additional widgets and a significant number of support functions, letting you program using resource files. This approach has the advantage of providing more runtime flexibility, since the resource files are read at runtime. Minor changes to the GUI may not require you to recompile, and end users can edit the resource files to alter the interface to their tastes. In addition, application code can be significantly simpler, since you rely on Xmt to do the detailed implementation work. Xmt does have a fairly steep learning curve and may still lack some of the flexibility provided by coding directly with the Motif API.
I stuck to C in this article, since there was so much I wanted to cover; however, C++ is also a good fit for Motif development. The biggest issue is that callback functions must be static member functions or global functions. In fact, user interfaces are particularly well-suited for GUI development, since you have visual objects that can be represented by the C++ objects. Also, the encapsulation C++ brings to the table can make the programs more modular and easier to maintain.