XForms: Review and Tutorial
In this article I would like to introduce you to XForms, a graphical user interface (GUI) toolkit for C and C++ I discovered a few months ago. I've been writing programs for several platforms (including, of course, Linux) for some time now and lately have been snooping around for a good GUI toolkit for X: one that works, can be obtained free of charge, and (most importantly) one that you can use right away, without any knowledge of X intrinsics. Then I heard about XForms, downloaded it, and tried it out. I wrote my first X program in less than a day. Impressive!
If you have ever looked into one of the programmer's books about the bare X library, you understand the difficulties I'm talking about. Writing a “hello world” program using only X11 itself is no small task. Even the smallest program will require several hundreds of lines of C code.
This is where toolkits come in. A GUI toolkit is a library of functions that make the creation and maintenance of a user interface more programmer-friendly. One of the better-known packages is, of course, Motif. If you're lucky, the Motif package you buy comes with a code generator which enables you to define a simplified GUI, which is then converted into C code. If you're very lucky, your package has an interactive GUI builder, where you can see how your interface will look once coded. Many GUI toolkits are, however, expensive; and many GUI toolkits aren't at all friendly to a novice X-programmer.
The XForms GUI package is not only free (for non-commercial applications), but it also makes good-looking programs, has an excellent interactive design builder, comes with good documentation, and is very easy for novices to use.
XForms is written by T.C. Zhao and Mark Overmars.
You can obtain XForms via anonymous ftp at:
ftp://bloch.phys.uwm.edu/pub/xforms andftp://ftp.cs.ruu.nl/pub/XFORMS
You should download the documentation and the XForms toolkit archive itself. The documentation is contained in a subdirectory DOC and is available in single or double sided postscript and dvi format. Since July 1995, the Linux ELF executable format is supported, as well as the a.out format. To download the toolkit in a.out format, get the file bxform-075.tgz from the directory linux. The ELF-based library is in linux/elf. The package is also available for other platforms; see the documentation.
Related WWW pages are at http://bragg.phys.uwm.edu/xforms/ and www.uwm.edu/~zhao
Finally, you can join a mailing list. Send a message to listserv@imageek.york.cuny.edu and put in the body of the mail message:
Every program that uses XForms for its user interface manages one or more “forms”. A form is simply a window under X: with or without a border, with or without resizing capabilities, with some window title, etc. The form is a box, into which you can put “objects”: buttons, dials, input fields and lots more. Objects are what many other toolboxes call “widgets”. Each object is one of a given “class”. For instance, XForms supports the object class “button”, of which many flavors exist (a simple button, with a light-bulb on it, round ones, check buttons, radio buttons etc.). Most programs can be written using XForms' built-in classes, though XForms supports a free object class to handle special situations.
The concepts of XForms, such as classes and objects, resemble object-oriented programming (and C++ in particular). However, the XForms code is strictly C, which means that you can use it for both C and C++. What's more, XForms is built on top of the bare X11 library. Linking an XForms program requires only the presence of the XForms library, the X11 library and the math library; no other libraries are necessary.
The programming paradigm you have to use with XForms (and in fact, with any X application) is somewhat different from what you might be used to. When writing an interactive program that reads its input from only one source (say, the keyboard), you are likely to use the following scheme:
/* forever do: */ while (1) { /* fetch user's input */ input = get_input (); /* determine what it is and * take appropriate action */ if (input == one_thing) do_one_thing (); else if (input == other_thing) do_other_thing (); . . . /* if `quit` signaled: we're * all done here */ else if (input == all_done) break; }
Conversely, when you build a user interface for X, you are likely to create several input sources. For example, you can have several buttons which, when pressed, activate some part of your program's code. Input sources can even be objects that are not selectable by a user, such as a timer that runs out. The programming paradigm that represents this is implemented in X with callbacks. You create several buttons, each with its own specific callback function. The main program loop then consists of polling the events at the various input sources and activating the appropriate callback when something happens. This is the approach taken by XForms, and we will see a typical program with callbacks later on.
In general, an XForms program has two types of code: the user interface that uses the XForms toolkit and the code that performs some useful actions, i.e., what you want the program to do when, for instance, a button is pressed. The user interface part of a the program consists of the initializing code, the definition of the forms and commands to put the forms on-screen. Then, the program activates XForms' main loop. This loop does the event polling described above.
For example, a tiny program that only shows a “quit” button would look as follows:
#include <forms.h> /* Call back function, called when `quit` * button is pressed */ void quit_button_cb (FL_OBJECT *obj, long data) { exit (0); } void main (int argc, char **argv) { FL_FORM *myform; /* form to display */ FL_OBJECT *quitbutton; /* temp variable */ /* Initializer code: */ fl_initialize (argv [0], "XMyprog", 0, 0, &argc, argv); /* Form definition: */ myform = fl_bgn_form (FL_FLAT_BOX, 200, 150); quitbutton = fl_add_button (FL_NORMAL_BUTTON, 50, 50, 100, 50, "Quit"); fl_set_object_callback (quitbutton, quit_button_cb, 0); fl_end_form (); /* Putting the form on-screen: */ fl_show_form (myform, FL_PLACE_FREE, FL_FULLBORDER, "Button program"); /* Main XForms loop: */ fl_do_forms (); /* Never reached.. */ }
The definition of a function quit_button_cb() can be seen in this listing. This function is the callback, which is activated when the user presses the quit button. All following code is in the main() function: the initialization, the form definition, the placing of the form on-screen, and the activation of the main XForms loop. The initialization part is done by the function fl_initialize(), which connects to the X server, parses the command line, etc. In this example, fl_initialize() doesn't need to parse any application-specific flags; hence the two zero arguments.
The form definition is enclosed by fl_bgn_form() and fl_end_form(). Normally you do not write this code by hand, but leave it to the designer (discussed below). The form is started by stating the box type and the size of the form. Between the fl_bgn_form() and fl_end_form() statements, objects can be added to the form. An object is added using fl_add_...(). In this example, only one button is put in the form at the x and y coordinates 50,50 (relative to the form's lower left corner) and with a size of 100 by 50 pixels. The button text is Quit.
Following the button definition, the callback of the button is assigned. Normally you leave even this definition to the designer. In our case, activation of the button leads to a call to quit_button_cb().
Note that to add the button, a local variable quitbutton is used. The same variable is used in the next statement to assign the callback. These are the only statements that require the variable; in other words, an object can be created (typically in a dedicated function generated by fdesign) without having to use a global variable. Readability and maintenance are usually improved by minimal use of global variables. Later on, you can always find the object when its callback function is invoked—the first argument to the callback is always a pointer to the object in question.
After the form definition (terminated by fl_end_form()), the form is placed on the screen using fl_show_form(). The placement type (resizing capabilities, etc.), border type and window title are stated here. After this, XForms' main loop fl_do_forms() is called. This function never returns; the program terminates via the callback.
The XForms package contains an excellent designer called fdesign to create and maintain a graphical user interface. The designer lets you create forms, add objects to the forms, set the objects' properties, such as colors, etc., and define the callbacks of the objects.
The designer can be finely tuned. I strongly suggest that you read the chapter in the documentation about the subject. When tuning the designer, you specify the type of the generated code, whether the designer should emit a main() function and/or templates for your callbacks, the stepsize in pixels of the size and placement of objects when designing a form, and lots more.
In fact, you should never modify the code the designer generates; if you want to add special fl_...() calls to modify the objects' appearance, do so outside of the generated code. The reason for this is that later on you may want to change the look of your program, e.g., by changing some colors. All you have to do then is to start up the designer, perform your changes, save the files and re-make. If you modify the generated code, the changes are lost once you re-save the designer file. (Of course, I came to this conclusion the hard way—I made the mistake of modifying fdesign's output.)
The archive containing the XForms toolkit is currently named bxform-075.tgz, 075 being the version number. After you obtain the archive, change-dir to a sources directory (such as /usr/src) and extract the archive using tar xvzf bxform-075.tgz
The archive spills into a subdirectory xforms where you will find the Makefile. By default, a make install copies the library libforms.a and the header file forms.h to respectively /usr/lib and /usr/include. The designer, fdesign, goes in /usr/local/bin. If you want to use other paths, edit the file mkconfig.h before you make install.
If you want to see the demos, type make/and wait (this takes some time to complete). Then cd DEMOS and type ./demo.
Having introduced XForms, I want to show you the steps to create a real XForms program. For the sake of simplicity, I present a program that manages only two forms. One of the forms has an input object, in which a user can enter a filename. This form also has a quit button to terminate the program. The other form has a browser object, in which the contents of the file are displayed. Using the scroll bar in the browser, the user can view different parts of the file.
As for the behavior of the program, we'll decide that only the form with the input object and the quit button is initially on-screen. When the user enters a filename, the form with the browser containing the file's contents appears. When an empty filename is entered, the form with the browser disappears again.
First of all, we should decide on some names. The forms will be called respectively name_form and browser_form. Inside the name_form there will be two objects: one input object to allow the user to type a filename and one button to quit. Both objects will need callbacks to take the appropriate actions: the functions will be respectively input_cb() and exit_cb(). These objects can remain nameless, which means that the designer will not generate global variables to address them.
The browser_form needs only one object in it: a browser object. This object does not need a callback since the browser will not accept user interaction (except the movement of the scroll-bar, which is handled internally by the browser). However, we will need a global variable for this browser since the callback of the input object of the first form must be able to fill the browser with the contents of a given file. We will call the browser file_browser.
Now that we have decided on the names of forms and objects, we can design the forms. The XForms designer can be started with fdesign design &, telling it to save its output in the files design.fd (the design itself), design.c (generated C code) and design.h (generated header file). The ampersand starts fdesign in the background.
Once the designer comes up, click on the “New Form” button to start a new form and enter the form name name_form. You can rescale the window that appears to a suitable size. After this, put the required objects in the form: an input object and a button. These objects can be selected in the “Objects” menu of the designer. Once an object is placed in the form, you can resize or move it using the mouse. The “Align” button lets you define the stepsize in pixels by which the objects are scaled or moved. Pressing F1 activates an “Attributes” window, in which the object attributes (such as font, color, callback) can be defined.
As far as the name_form is concerned, you should create something that looks like the screen dumps in figures 1 and Figure 2. Figure 1 shows the designer window itself, the name form, and the attribute window of the input object. (Yes, I wrote the program around noon.) In this figure the input object is “active” as can be seen by the red box around it.
Figure 2 shows the quit button and its properties.
To define the browser form, click again on “New Form” to start a second form. Enter browser_form as the form name, and resize the form window to your liking. Then select a “browser” object from the Objects menu and add it to the form. The form, its browser object and the properties of the browser are shown in Figure 3.
Having defined the form, we can let fdesign generate its code. Click on the “Options” button and toggle the “Alt format” entry to light up the button in front of this menu entry. This instructs fdesign to generate simple code, instead of placing form and object variables into structs. For this application, simple code suffices. Then click on the “File” button, save the file, and exit fdesign.
We can rely on fdesign to generate the code for the definition of all forms and objects. That leaves us with two tasks: the initialization code and the callback code.
The initialization code is shown in listing 1. This code defines the main() function in which XForms is initialized, the forms are created, the first form is put on-screen, and the main XForms loop is entered. You can see the call to create_the_forms(); the code of this function is generated by fdesign in the file design.c.
/* xviewfile.c -- main function of xviewfile */ #include \<>forms.h #include "design.h" void main (int argc, char **argv) { /* initialize XForms, parse arguments */ fl_initialize (argv [0], "XViewfile", 0, 0, &argc, argv); /* create the forms (fdesign-generated function) */ create_the_forms (); /* show the name input form */ fl_show_form (name_form, FL_PLACE_MOUSE, FL_FULLBORDER, "Enter a name"); /* enter XForms loop */ fl_do_forms (); /* not reached... */ }
Now for the second task, the callback code. The callback functions are shown in listing 2. The names of the callback functions were already mentioned in the design specifications; it is therefore important that the exact names are used when writing the functions.
Each callback activated by an object is passed two arguments. The first argument is a pointer to a FL_OBJECT. This argument points by definition to the object which invoked the callback. We will see how this argument is used in the input_cb() function. The second argument is a long int—a value which can be set to any number when defining the callback in the designer. For example, you could have two different objects invoking the same callback function but providing different numeric arguments; inside the function you could distinguish by inspecting the second argument. We won't use this approach in this application.
The callback activated by the quit button is called exit_cb(). This is the easy one—all it does is exit(0). The callback for the input object, input_cb(), needs to perform more tasks. First, the name which was typed by the user must be retrieved. This is done by XForms' function fl_get_input(). Then, we must decide what to do with the input. As specified by the program description above, an empty name should lead to the removal of the browser window from the screen. A non-empty name should be interpreted as a request to view a file.
To accomplish this, input_cb() uses a static int variable to flag whether the browser form is yet on-screen. When a non-empty name is entered and when the browser form is not yet on the screen, fl_show_form() is called to show the browser. Similarly, when an empty name is entered and when the browser form is on-screen, fl_hide_form() is called to remove the browser form. (Instead of using an extra static int, you could also inspect the field int visible, which is part of the FL_FORM struct. I leave such optimizations to the reader.)
The browser itself is manipulated with browser-specific functions fl_clear_browser() and fl_load_browser().
/* callbacks.c --contains the callback routines */ #include <\<>forms.h> #include "design.h" void exit_cb (FL_OBJECT *obj, long data) { exit (0); } void input_cb (FL_OBJECT *obj, long data) { char const *name; /* entered filename */ static int browser_on_screen = 0; /* is browser on-screen yet ? */ /* determine the entered name */ name = fl_get_input (obj); if (name && *name) /* a name was entered */ { if (! browser_on_screen) /* make sure browser is there */ { fl_show_form (browser_form, FL_PLACE_CENTER, FL_FULLBORDER, name); browser_on_screen = 1; } fl_clear_browser (file_browser); /* clear previous contents */ fl_load_browser (file_browser, /* load in file */ name); } else /* empty input was given */ { if (browser_on_screen) /* remove browser from screen */ { fl_hide_form (browser_form); browser_on_screen = 0; } } }
Finally, no program is complete without an automatic maintenance description in a Makefile. Here is an example:
# Makefile -- makefile for xviewfile. # Used objects: OBJ = xviewfile.o callbacks.o design.o # Compilation flags: CFLAGS = -c -O2 -Wall # How to make the program: xviewfile: $(OBJ) $(CC) -o xviewfile $(OBJ) -lforms -lX11 -lm -s # How to clean up the mess. clean: rm -f $(OBJ)
The listings show only the tip of the iceberg of as far as XForms' possibilities are concerned. For the sake of brevity, I did not address subjects such as color definition or font selection.
One thing you will want to try is making resizable forms. Whether a form can be resized is defined in the call to fl_show_form(), which displays the form. How the form is resized is defined in the form definition itself. Again, you should use fdesign to set these options. For example, you might have a form with a button and an input object at the top, and with a browser below thrm. When such a form would be resized, you'd probably want only the browser to expand or shrink, not the button and input object. Such capabilities are set with objects' resizing options and gravities. XForms, and hence the designer, supports “object groups” for this purpose: you can group objects and define the resizing options and gravities to apply to a whole group.
Another useful property of any X program is the ability to let the user overrule settings of the program via command line flags, resources set via xrdb, or resources set via an application defaults file. XForms supports all of these; fl_initialize() can be used in combination with fl_get_resources() to scan for meaningful flags or resource settings. Implementing the resource recognition is not entirely trivial but goes beyond the scope of this article. If you're interested, look at some of the programs built using XForms. The XForms distribution as well as the WWW resources contain pointers to useful and illustrative programs.
Naturally, there are lots of other useful XForms possibilities. I leave it to you to read through the excellent documentation on a quiet evening with a glass of (virtual) beer.
Needless to say, I am very enthusiastic about XForms. The setup of the XForms toolkit (using forms, objects, classes) is very intuitive even when you have no previous experience with X programming at all. You do need good knowledge of C, though.
The range of object classes in XForms is so wide that I haven't (yet) needed to define a free object class. If you plan to write everyday programs, XForms probably has the classes to suit your needs, and the classes have plenty of dedicated functions to make the objects appear the way you like.
As far as the designer is concerned, I have come to cherish this tool, even though I first regarded it as just a flashy extra. The ease with which you can adapt a program to perform an extra task, by expanding an existing form to hold, say, an extra button, is incredible. No more code editing to adjust the coordinates of all widgets!
There are, of course, drawbacks. I think it's a shame that XForms is not distributed with the full sources. The fact that XForms can be used free of charge for non-commercial applications is of course a huge advantage, but still, I'd rather have the sources at hand. This irritation can be overcome if you're willing to pay the price: the authors do sell license agreements that include the source files.
Another drawback is that if you do not use the ELF-based version of XForms, you have to link your programs against a static libforms.a. There is no shared a.out-based library for XForms. Therefore, unless you migrate to the ELF executable format, even the smallest program turns out to be about 130KB. (On the other hand, I also use statically linked Motif programs. The 130KB is, by comparison, trivial.)
The XForms library is “nice” to programmers, which is an advantage when you are a new user or when you're developing a program. For example, when a program tries to remove a form from the screen which is not on-screen in the first place, XForms will pop up a warning window. You are then presented with three choices: to ignore the action, to abort the program, or to suppress such warnings. This feature is very handy when you are testing a program. However, such safety nets add “superfluous” code to a program. Once a program has been tested, such precautions are, in my opinion, no longer necessary and therefore only a burden. Ideally, I'd prefer two flavors of the XForms library: a developer's version with the full safety features and a production version without them. On the authors' part this would require only some #ifdef/#endif compilation directives in the code.
On the whole, I find that there are many more advantages to XForms than there are disadvantages. I use XForms now and I'm planning to continue to do so. I still haven't hit a wall in my X programming efforts that could not be overcome using XForms' own features. And I haven't even explored all possibilities of XForms yet.
Karel Kubat lives in the northern part of the Netherlands and is currently working on his PhD thesis. He is a Linux fanatic and therefore pretends to know everything about the subject, since that's what being a fanatic is all about. He can be reached via e-mail at the University of Groningen at karel@icce.rug.nl.