Contents Previous Next

27 Advanced UIL Programming


This chapter describes advanced concepts and programming techniques in UIL. It builds on the UIL material explained in the previous chapters.

This chapter introduces and examines ways that you can make the most of UIL's more advanced features. In the following sections, we describe how to add non-Motif widgets to an interface description, discuss methods and ideas for organizing UIL files, and examine the considerations that you face when setting resources in UIL. Finally, we present material on advanced uses of UIL lists and user-interface prototyping.

27.1 Using Non-Motif Widgets

With UIL, it is easy to define instances of any of the Motif widgets, because their type names are built into the compiler. However, you may need to use your own widget or a third-party widget in an application to provide functionality that is not available in the Motif widget set. Fortunately, it is possible to include other widgets using the special user_defined widget class along with the argument and reason value types.

OSF/Motif also supports non-Motif widget descriptions using the widget meta-language (WML). These widgets are written into a separate WML description file which is run through the WML compiler. WML is typically used for describing alternative widget sets; many third party widget sets include compiled WML description files. The use of compiled WML description files is covered in #suilwmlopt, but a complete description of WML syntax and usage is beyond the scope of this book.

Getting back to UIL, here are the steps involved in defining and creating a user-defined widget:

To illustrate these steps, we are going to present an example that uses the Athena (Xaw) Tree and Panner widgets. The Tree widget is a constraint widget that arranges its children in a tree, while the Panner is a two-dimensional scroll bar.

27.1.1 The Widget Creation Procedure

In order to create a non-Motif widget, you must write a creation procedure and register it with Mrm. A user-defined widget creation procedure takes the same form as the Motif widget creation routines. The parent argument specifies the parent of the widget to create, and the name argument is the widget's name. The args and num_args parameters supply the initial resource settings for the widget. Most creation procedures create a widget simply by calling XtCreateWidget().

Mrm must know about a new creation function before you can create widgets with it. Widget creation functions are registered with MrmRegisterClass(), which must be called before any user-defined widgets are created. This function takes the following form:

   Cardinal
   MrmRegisterClass(class_code, class_name, proc_name, create_proc,
                    widget_class)
       MrmType     class_code;
       String      class_name;
       String      proc_name;
       Widget    (*create_proc)();
       WidgetClass widget_class;
The first two arguments, class_code and class_name, are obsolete but remain to preserve backwards compatibility. You should always pass 0 and NULL for these arguments, respectively. The proc_name parameter specifies the name of the creation procedure as it appears in a UIL module. To avoid confusion, it is a good idea to use the same name in both application code and UIL. The create_proc argument is the address of the creation procedure, and widget_class is a pointer to the class structure of the widget. The function indicates the result of the operation by returning either MrmSUCCESS or MrmFAILURE. A failure only occurs when the function cannot allocate memory.

MrmRegisterClass() does not take an MrmHierarchy argument like many of the Mrm routines, which means that any user-defined widgets that you register with this function are accessible from all open hierarchies. Mrm does not provide a way to register a widget class with an individual hierarchy. the source code demonstrates the use of MrmRegisterClass().

   /* tree.c --
    * Program to show the Tree and Panner widgets.
    */

   #include <stdio.h>
   #include <X11/Intrinsic.h>
   #include <X11/Xaw/Tree.h>
   #include <X11/Xaw/Panner.h>
   #include <X11/StringDefs.h>
   #include <Mrm/MrmAppl.h>

   void pan();

   static MrmRegisterArg callback_list[] = {
       { "pan",          (XtPointer) pan },
       /* Add additional callback procedures here... */
   };

   Widget
   XawCreateTreeWidget (parent, name, args, num_args)
   Widget parent;
   String name;
   ArgList args;
   Cardinal num_args;
   {
       return (XtCreateWidget (name, treeWidgetClass, parent, args, num_args));
   }

   Widget
   XawCreatePannerWidget (parent, name, args, num_args)
   Widget parent;
   String name;
   ArgList args;
   Cardinal num_args;
   {
       return (XtCreateWidget (name, pannerWidgetClass, parent, args, num_args));
   }

   void
   pan (panner, client_data, call_data)
   Widget panner;
   XtPointer client_data;
   XtPointer call_data;
   {
       Widget tree = (Widget) client_data;
       XawPannerReport *report = (XawPannerReport *) call_data;

       /* Should use XtSetValues, but DrawingArea bug prevents us */
       XtMoveWidget (tree, -report->slider_x, -report->slider_y);
   }

   int
   main (argc, argv)
   int   argc;
   char *argv[];
   {
       XtAppContext  app_context;
       Widget        toplevel, root_widget;
       Cardinal      status;
       static String uid_file_list[] = { "tree" };
       MrmType       class_code;
       MrmHierarchy  hierarchy;

       XtSetLanguageProc (NULL, NULL, NULL);

       MrmInitialize();

       toplevel = XtVaAppInitialize (&app_context, "Demos", NULL, 0,
           &argc, argv, NULL, NULL);

       status = MrmOpenHierarchyPerDisplay (XtDisplay (toplevel),
           XtNumber (uid_file_list), uid_file_list, NULL, &hierarchy);

       if (status != MrmSUCCESS) {
           XtAppError (app_context, "MrmOpenHierarchyPerDisplay failed");
           exit (1);
       }

       MrmRegisterNames (callback_list, XtNumber (callback_list));
       MrmRegisterClass (0, NULL, "XawCreateTreeWidget",
           XawCreateTreeWidget, treeWidgetClass);
       MrmRegisterClass (0, NULL, "XawCreatePannerWidget",
           XawCreatePannerWidget, pannerWidgetClass);

       status = MrmFetchWidget (hierarchy, "root", toplevel, &root_widget,
           &class_code);

       if (status != MrmSUCCESS) {
           XtAppError (app_context, "MrmFetchWidget failed");
           exit (1);
       }

       XtManageChild (root_widget);
       XtRealizeWidget (toplevel);

       XtAppMainLoop (app_context);
   }
This program defines widget creation functions for the Tree and Panner widgets and registers the routines with Mrm. The program also defines the pan() callback routine for the Panner widget. This routine is specified for the XtNreportCallback, as we'll show you shortly.

27.1.2 Widget Include Files

It usually makes sense to place the declarations for a user-defined widget in an include file so that they can be used in more than one module. The include file for the Tree widget is shown in the source code

   /* UIL declarations for the Xaw Tree widget. */

   ! Declare the creation procedure
   procedure
     XawCreateTreeWidget();

   ! Declare resources
   value
     XtNautoReconfigure : argument ('autoReconfigure', boolean);
     XtNhSpace          : argument ('hSpace',          integer);
     XtNlineWidth       : argument ('lineWidth',       integer);
     XtNvSpace          : argument ('vSpace',          integer);
     XtNgravity         : argument ('gravity',         integer);
       NorthGravity : 2;
       WestGravity  : 4;
       EastGravity  : 6;
       SouthGravity : 8;

     ! Constraint resources
     XtNtreeGC          : argument ('treeGC',          any);
     XtNtreeParent      : argument ('treeParent',      widget);

The Tree creation procedure is declared in a procedure section, even though the application registers a widget creation procedure differently from a callback procedure. Creation procedures should be declared as taking no arguments.

The resources for the Tree widget are defined using the UIL argument type. The syntax is the same as for any other value, although argument values cannot be imported or exported. The argument literal specifies the internal name of the widget resource and the type of the resource. If a widget follows the Xt coding conventions, the internal resource name is the name of the resource minus the XtN prefix. The UIL compiler uses the type argument for type checking the resource, just like the built-in resources. UIL does not support a type that corresponds to a GC (graphics context), so the type of the XtNtreeGC argument is specified as any. This resource can only be set correctly using a GC imported from the application as an identifier . The file also contains constraint resource definitions for the children of the Tree.

The include file also defines variables for the possible values of the XtNgravity resource. These values are merely a convenience, as there is no way to make the UIL compiler check the setting of the resource for these values. (This type of checking is possible for widget descriptions written with WML, however.) Once the resources are defined, the UIL compiler allows you to set user-defined resources, such as XtNtreeGC and XtNtreeParent, in the arguments subsection of any widget, including the built-in Motif widgets. You can also use the built-in Motif resources in a user-defined widget definition, which is why we did not define the Tree widget's XtNbackground or XtNforeground resources. These resources are the same as the Motif XmNbackground and XmNforeground resources, even though the prefix is different. The names can be used interchangeably.

We use a separate include file for the Panner widget. The UIL definitions for the Panner appear in the source code

   /* UIL declarations for thw Xaw Panner widget. */

   ! Declare the creation procedure
   procedure
     XawCreatePannerWidget();

   ! Declare resources
   value
     XtNallowOff          : argument ('allowOff',          boolean);
     XtNbackgroundStipple : argument ('backgroundStipple', string);
     XtNcanvasWidth       : argument ('canvasWidth',       integer);
     XtNcanvasHeight      : argument ('canvasHeight',      integer);
     XtNdefaultScale      : argument ('defaultScale',      integer);
     XtNinternalSpace     : argument ('internalSpace',     integer);
     XtNresize            : argument ('resize',            boolean);
     XtNrubberBand        : argument ('rubberBand',        boolean);
     XtNshadowThickness   : argument ('shadowThickness',   integer);
     XtNsliderX           : argument ('sliderX',           integer);
     XtNsliderY           : argument ('sliderY',           integer);
     XtNsliderWidth       : argument ('sliderWidth',       integer);
     XtNsliderHeight      : argument ('sliderHeight',      integer);

   ! Declare callbacks
   value
     XtNreportCallback    : reason   ('reportCallback');

This file declares the widget creation function and defines a number of resources. The Panner also has a callback, so the include file declares the callback using the UIL reason type. The reason literal simply specifies the string name of the callback resource. Unlike the argument literal, no type is necessary because the type is always a callback.

27.1.3 Creating User-defined Widgets

A module can create instances of the user-defined Tree and Panner widgets by including the two files shown above. the source code illustrates a typical application of these widgets.

   module tree

   include file 'XawTree.uih';
   include file 'XawPanner.uih';

   procedure
     pan (widget);

   object root : XmForm {
     controls {
        user_defined panner;
        XmDrawingArea viewport;
     };
     arguments {
       XmNdialogTitle = "Motif Widget Classes";
     };
   };

   object panner : user_defined procedure XawCreatePannerWidget {
     arguments {
       XtNdefaultScale   = 10;
       XtNcanvasWidth    = 325;
       XtNcanvasHeight   = 300;
       XtNsliderWidth    = 200;
       XtNsliderHeight   = 200;
       XmNleftAttachment = XmATTACH_FORM;
       XmNtopAttachment  = XmATTACH_FORM;
     };
     callbacks {
       XtNreportCallback = procedure pan (motif_widgets);
     };
   };

   object viewport : XmDrawingArea {
     arguments {
       XmNmarginWidth      = 0;
       XmNmarginHeight     = 0;
       XmNwidth            = 200;
       XmNheight           = 200;
       XmNtopAttachment    = XmATTACH_FORM;
       XmNbottomAttachment = XmATTACH_FORM;
       XmNleftAttachment   = XmATTACH_FORM;
       XmNrightAttachment  = XmATTACH_FORM;
     };
     controls {
       user_defined motif_widgets;
     };
   };

   object motif_widgets : user_defined procedure XawCreateTreeWidget {
     controls {
       Primitive : XmLabel { };
       ArrowButton : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       Label : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       CascadeButton : XmLabel {
         arguments {
           XtNtreeParent = Label;
          };
       };
       DrawnButton : XmLabel {
         arguments {
           XtNtreeParent = Label;
         };
       };
       PushButton : XmLabel {
         arguments {
           XtNtreeParent = Label;
         };
       };
       ToggleButton : XmLabel {
         arguments {
           XtNtreeParent = Label;
         };
       };
       List : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       Sash : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       ScrollBar : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       Separator : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       Text : XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
       TextField: XmLabel {
         arguments {
           XtNtreeParent = Primitive;
         };
       };
     };
     arguments {
       XtNlineWidth = 2;
       XmNborderWidth = 0;
       XtNhSpace = 22;
       XtNvSpace = 10;
     };
   };

   end module;
This module defines a Form that contains a DrawingArea and a Panner widget. The DrawingArea contains a Tree widget that depicts the class hierarchy of the Motif primitive widgets. The Panner scrolls the Tree, so that the user can view the entire hierarchy. The output of this example is shown in the figure.

figs.eps/V6a.26.01.eps.png
User interface of tree.uil

The syntax of the Panner and Tree widget definitions is almost the same as the many Motif widget definitions that you've already seen. The main difference is that you replace the Motif widget class name with user_defined procedure followed by the name of the widget creation procedure. To reference a user-defined widget in the controls subsection of a widget, you just use the keyword user_defined followed by the name of the widget.

You can mix user-defined resources and built-in resources as in the Panner definition, which contains some of its own resource settings as well as a couple of Form constraint resource settings. You can also specify user-defined resource names in the arguments subsection of a built-in widget definition. For example, the Label widget children of the Tree contain resource settings for the user-defined XtNtreeParent resource. The Panner widget contains a user-defined callback setting; the pan() callback routine is invoked when the user adjusts the Panner. The syntax of using user-defined resources and callbacks is the same as that for built-in resources and callbacks. The main drawback of user-defined widgets is that, unlike with the built-in Motif widgets, the compiler does not know which resources are valid for the widget, so it lets you set any resources. This generally is not a problem when you are only working with a few user-defined widgets, but as the number of user-defined widget instances increases, so does the probability of making an error that the UIL compiler cannot catch. You can avoid this problem by describing user-defined widgets with WML, at the expense of learning a more complicated widget description format.

27.2 Organizing UIL Modules

When developing an application with UIL, it is important to think about the organization of the modules that make up the interface. There are a number of benefits to be gained from careful organization, such as code that is easier to edit, test, and maintain, interface components that are reusable, and applications that are easier to internationalize. While these benefits are not all unique to UIL, there are specific organizational strategies that make it easier to realize the benefits. This section presents some organizational techniques for UIL modules. Since there are no rules when it comes to organization, you should consider these techniques as guidelines.

You should adhere to the general principal of grouping things by purpose or function. Some of the things you should consider grouping in UIL are interface components, procedures, lists and strings. Basically, it makes sense to group any collection of UIL declarations or definitions that you consider to be logically related. You can organize aspects of an application both in separate files and by sections within a file.

27.2.1 Using Separate Modules

When an application uses multiple top-level windows, such as a main window and various dialog boxes, it makes sense to describe each window in a separate UIL module. When a window is especially complex, you may want to describe the interface using several modules. For example, the menu system of an application could be described in a separate module, as illustrated by the example in Chapter 25, Building an Application With UIL.

When you divide an application into separate modules, it is easier to find specific declarations or definitions because the modules map directly to the appearance of the interface. Multiple files also help to reduce editing conflicts when you are working on a project with a team of programmers. Breaking components into separate files helps to eliminate dependencies between logically separate parts of the interface. For example, if you make a change to a dialog, it shouldn't affect any other parts of the interface. Another benefit is rapid prototyping and testing. With a few modifications, you can use the showuid program from Chapter 24, Creating a User Interface With UIL, to preview a component without needing to have a complete application program. We'll take a closer look at prototyping a user interface later in this chapter.

27.2.2 Organizing Within a Module

Within a UIL module, one organizational decision involves whether widgets are declared from the top-down or from the bottom-up. Top-down organization means that you define the parent widgets first, followed by their children. Bottom-up is the opposite, in that you define the child widgets and then define their parents. We recommend using the top-down approach, since it is an extension of the organization at the file level. In addition, it is more natural for developers who are accustomed to creating an interface in application code. Whichever approach you choose, you should be sure to use it consistently in all of your modules.

Using value and list sections to define settings for widget resources that you use or change often is another useful practice. This technique makes a module easier to read and maintain. As you know, UIL lets you define variables that can be fetched by an application. It makes sense to place values that you intend to fetch in the same module as the part of the interface to which they correspond. Although UIL allows forward references to most variables, you should try to define or declare variables before using them, as it is a more familiar style. Grouping variables together at the beginning of a module is another common practice that we recommend.

In some cases, you may need to share a variable or a list among several modules. For most types of variables, you can place the definition in one module and include declarations in any other modules that use the value. For lists and for values that cannot be exported, you must place the values directly in an include file. This kind of reuse frees you from trying to maintain the same information in more than one place.

27.2.3 Supporting Internationalization

Although it is a good idea to put variable definitions in the module where you use them, we need to make an exception to this guideline for internationalization purposes. To support internationalization, an application should not use literal values for strings, compound strings, character sets, fonts, and font sets in a UIL module that contains widget declarations. You should consolidate these values into one module per language and define a variable for each value. Then, you can use the variables in widget declarations instead of literal values. You can create a single include file that contains declarations of all of these values and include it anywhere you need to use one of the values. the source code illustrates this ­technique.

   ! i18n_dialog.uil - Dialog used to prompt the user for a filename

   module savebox

   include file 'strings.uih';

   object root : XmPromptDialog {
     arguments {
       XmNokLabelString = ok_text;
       XmNcancelLabelString = cancel_text;
       XmNhelpLabelString = help_text;
       XmNselectionLabelString = save_prompt_text;
       XmNdialogTitle = save_title_text;
     };
   };

   end module;

As you can see, a variable is used everywhere that we needed to set a language-dependent resource string. the source code shows the corresponding strings.uih declaration file.
   ! strings.uih - Interface string declarations

   value
     ok_text          : imported compound_string;
     cancel_text      : imported compound_string;
     help_text        : imported compound_string;
     save_title_text  : imported compound_string;
     save_prompt_text : imported compound_string;

The declaration file is the same for every module that uses the strings, so you don't have to declare them in each module. Each string variable is declared as an imported value, which we explained in Section #suilshare. All you need is a string definition file for each language that is supported by the application. the source code contains the English version of the strings for this dialog.
   ! strings.uil - English version of interface strings

   module strings

   value
     ok_text          : exported compound_string ("Ok");
     cancel_text      : exported compound_string ("Cancel");
     help_text        : exported compound_string ("Help");
     save_title_text  : exported compound_string ("Save Dialog");
     save_prompt_text : exported compound_string ("File Name:");

   end module;

To support another language, we only need to create a new version of strings.uil for that language. The English version of the dialog is shown in the figure.

figs.eps/V6a.26.02.eps.png
User interface of i18n_dialog.uil using an English strings.uil

A separate subdirectory in the development environment is typically created for each supported language. If the directory names correspond to possible values of the LANG environment variable, it is easy to test the interface for each language by setting the LANG and UIDPATH variables, which are described in Chapter 22, Introduction to UIL.

At run-time, language-dependent UID files are normally installed in /usr/lib/X11/%L/uid/%N, where %L stands for the LANG environment variable and %N is the application class name. For example, if the class name of the application that uses the save dialog is Demos, you install the English version of the strings in /usr/lib/X11/C/uid/Demos/strings.uid. Likewise, you install the French version in /usr/lib/X11/Fr/uid/Demos/strings.uid. The language-dependent files, such as strings.uid, are the only ones for which we need to have multiple versions; the rest of the UID files are installed in the /usr/lib/X11/uid/Demos directory.

27.2.4 Organizing With Include Files

UIL include files are quite useful for organizing a user interface. You can use include files that declare or define values and define lists, as shown in Section #suiladvlist. You can also use include files for information defined in the application program, namely callback procedures and identifiers. Include files provide a convenient place to organize these declarations by functioning like C header files. If you use an include file, there is no need to declare the same procedure or identifier in more than one module. Another advantage of keeping callback declarations and identifiers in an include file is that you can make any changes and additions in a single file.

27.2.5 Creating Reusable Components

Reusability is another benefit of proper organization. You can organize for reusability at a number of levels, from complete dialogs to individual widgets. You may not have recognized it at the time, but you've already seen an example of reusable components in the guise of user-defined widgets. Widgets are generally designed to be reusable items. You can extend this notion in UIL by creating a separate include file for any user-defined widgets that you use, as Section #suiluserdef illustrated. The other alternative is to place the necessary definitions and declarations directly in the module where the widget is used. Clearly, using an include file is the more flexible of the two techniques.

If you are developing several different applications, you may find that there are a number of common components such as help windows, print dialogs, and various menus. While the Motif widget set provides some reusable components, such as the FileSelectionBox, you can create your own reusable user-interface components using UIL. For example, if you create a reusable context-sensitive help facility using UIL, it is easy to add help to all of your applications. This use of UIL not only saves you time, but it also promotes a common look and feel across your set of applications.

You can also reuse callbacks to a certain degree. While most callbacks are specific to the task at hand, it is possible to write generic callbacks that you can use in multiple applications. Section #suilproto describes several such callbacks. You are not limited to a single callback procedure for a callback action, so it is possible to split the behavior of a certain action over more than one callback. For example, a typical Cancel button on a dialog might reset the dialog contents and then pop down the dialog. These two actions can be handled by two callbacks. Other dialogs can have their own reset callback, but can reuse the pop down callback. If you have a group of related callbacks, it may make sense to put the callbacks into a single source code file and write one UIL include file for all of the callbacks.

27.3 Specifying Resource Values

Even though all of the UIL examples in this book specify widget resource settings directly in UIL modules, it is important to realize that this approach is not always the best, especially for real-world applications. With any application, you have the choice of setting resources in a UIL module, in an X resource file, or directly in application code. This section looks at the advantages and disadvantages of setting resources in each of these places.

27.3.1 Resource Name Checking

When it comes to error checking of resource settings, UIL comes out on top. Anytime you attempt to set a built-in resource or callback, the UIL compiler makes sure that the resource exists and that it is supported by the widget in which you are setting it. If you make a typing error or mistakenly set an unsupported resource, the compiler issues an error message.

Neither one of these errors is caught in an X resource file because the X resource manager waits until run-time to perform a search of the resource database. Since resource files are not validated, it is up to you to notice mistakes at run-time when a widget doesn't look or act as you expect.

When you set a resource in application code, you only get partial error checking. The C preprocessor or the C compiler generates an undefined symbol error if you use a resource name that does not exist, but this mechanism does not prevent you from using a resource in the wrong context, as the compiler cannot detect this error.

27.3.2 Resource Type Checking

Type checking takes place for resources set in both UIL modules and X resource files. The UIL compiler knows the expected type for each built-in resource as well as the type of a value, so it is able to produce an error message if you try to set a resource to a value of an incompatible type. However, there are a few cases in which UIL doesn't perform complete type checking. The UIL compiler only checks resources that can be set to enumerated values, such as XmNalignment and XmNleftAttachment, for integer assignments. In addition, no type checking is performed on user-defined resources of the type any or on resources that are set to identifier variables.

The type checking in X resource files is similar, but has slightly different limitations. The string is the only recognized type in an X resource file. At run-time, Xt automatically calls the appropriate resource converter, which creates a value of the type the widget expects for a particular resource. If you specify a string that cannot be converted, the converter function generates a warning message. The Motif library includes a separate converter for each of the enumerated types, so you cannot accidentally use an incorrect value. There is no type checking for string-typed resources, however, as their values are taken literally. For example, if you set a string resource to False thinking that its type is Boolean , you won't get a warning. The biggest weakness of setting resources in an X resource file is that type checking does not occur until the widgets are created at run-time.

There is essentially no type checking when you set resources in application code because of the general nature of the Xt resource-setting API. The XtSetValues() macro takes an array of generic structures, and all of the values are represented by the XtArgVal type. Therefore, the compiler cannot perform its usual type checking. You can get partial type checking in C by using convenience functions, but only a few exist compared to the total number of resources. If you set a resource to the wrong type in application code, the results are unpredictable. Nothing may happen, but more likely the error will cause strange behavior and/or crash the application. Unfortunately, the source of this kind of error is usually difficult to track down.

27.3.3 Resource Type Support

You can set every resource of a Motif widget from either a UIL module or application source code, while X resource files allow you to set nearly every type. In source code, resource type support is almost inherent. As long as you include the right header file and know the format of a resource type, you can create a value. Creating values for types in non-Motif widget sets is no different, since you can use X, Xt, or Motif functions to create complex types.

Type support in UIL is built into the compiler; each type is specified with a unique literal syntax. Because UIL is designed to support Motif, the built-in types are limited to the types used by the Motif widget set, and there is no way to add a new type short of modifying the UIL compiler. You can work with new types imported from the application using identifiers, but that's not quite the same as being able to define the values directly in a module.

As we just mentioned, type support in resource files is limited to the available resource converters. Without adding any new converters, you can set most Motif resources. Two notable exceptions are resources that specify widget and color pixmap values. You can add new type converters, but in order to do so you must have a strong knowledge about the workings of Xt. Even then, perfecting a converter takes a good deal of work. See Volume Four, X Toolkit Intrinsics Programming Manual, for more information on resource converters.

27.3.4 Callback Specifications

You can set callbacks in both UIL and application source code, but not in X resource files. You might think of callbacks differently because they determine the behavior of an interface, while most other resources affect the appearance. As far as a widget is concerned, however, a callback is just another resource whose type is an array of function pointers. In UIL you use the callbacks subsection to set a callback resource. With Xt, you normally use XtAddCallback() or XtAddCallbacks(). The situation is different in a resource file, where all values are considered equal. In order to set a callback in a resource file, you need a callback resource converter, but such a converter is not available by default. Several third-party software packages provide this functionality. Most notable is the Widget Creation Language (Wcl), which also supports complete interface specifications with capabilities similar to UIL. For more information on Wcl, see Issue 2 of The X Resource.

27.3.5 Wildcard Specification

Only X resource files give you the ability to set resources by widget class and to represent one or more widgets in the hierarchy with wildcards. Neither of these powerful features are available from UIL or application source code. In both UIL and application code, you are limited to setting resources on a specific widget instance. In an X resource file, you can write a single resource setting that applies to a single widget, to widgets of a specific class, or to all of the widgets in a hierarchy. This feature is particularly useful for specifying a common appearance for a group of widgets. Doing the same thing in UIL or source code requires a lot of repetition. You can use a list in UIL or a convenience routine in source code to factor out common settings, but at some point you still need an explicit list reference or function call.

27.3.6 User Customization

Another difference between the three methods of specifying resources is the freedom that they give a user to customize or override a resource setting. Any settings that you place in an X resource file can be overridden by the user. Settings made in UIL or in source code are fixed and cannot be changed by the user. This fact is generally true about UIL. A user could conceivably replace a compiled UID module with a new version that contains different settings, but it is rather unlikely. Neither behavior is any better or worse than the other. Allowing or disallowing user customization is just something for you to consider when deciding where to set a resource, rather than a limitation of the method.

27.3.7 Dynamic Updating

Many resource settings are not just set at widget creation time and left alone, but are continually updated to indicate the state of the application in response to user actions. You can make dynamic resource changes in source code using XtSetValues() or XtVaSetValues() and application-created values. The MrmFetchSetValues() function is the counterpart for UIL. Instead of using application-defined values, the routine automatically loads the data from exported values in a UIL module. (For more information on MrmFetchSetValues() see Section #suilfetch.) With X resource files, the resource settings are loaded and applied only when a widget is created. Once again, it is possible to write code to dynamically fetch and convert a value from a resource file, but the Xt library does not provide this functionality.

27.3.8 Guidelines for Setting Resources

Now that we have explained the capabilities and limitations of each method of specifying resources, we can look at where to place resource settings in some common situations. The first thing to think about for a resource setting is whether you want to let the user change it. If so, using an X resource file is the only way to go. You should try to allow the user to override settings that affect the appearance of widgets, such as colors and fonts. On the other hand, you normally do not want to let the user change layout or behavior settings, such as form attachments and widget sensitivity. It is fairly common to allow changes to translation tables, however.

You should also consider which specification methods support the resource in question, and how much work is involved in creating the value. If you want to set a color pixmap resource, UIL is clearly the best choice, as the values are not supported in X resource files or application code. Compound strings are good candidates for resource files or UIL. It is easier to specify a compound string in one of these than it is in C code, where you must worry about creating and freeing the values. However, X resource files only handle 8-bit left-to-right strings, so if you need to do anything beyond that, look to UIL. Callbacks present yet another choice where you can go with source code or UIL.

When it is an option, UIL is generally the best method for setting resources, as it provides the most comprehensive error-checking capabilities. The most common reason for not using UIL is to allow user customization. Dynamically-computed or complex resource settings are best made in application source code. Sometimes a combined approach is possible, in which the values are defined in UIL, but manipulated and set within the application.

These are only a few guidelines. When you encounter a new situation and have a choice, try to determine the consequences of using a particular method. summarizes the features supported by each method. Finally, don't forget to think about ease of use and reliability for both you and the people using your application. tab(@), linesize(2); l | l | l | l l | l | l | l.
Feature@UIL@Resource File@Application
_
Existence Checking@Yes@No@Yes Validity Checking@Yes@No@No Type Checking@Yes@Yes@No Motif Types Supported@Yes@No@Yes New Types Supported@No@Yes@Yes Callbacks Supported@Yes@No@Yes Wildcard Specifications@No@Yes@No User Customization@No@Yes@No Dynamic Updating@Yes@No@Yes
_

27.4 Using Lists Effectively

Lists are a powerful feature of UIL because they give you an alternative to specifying widget children, callbacks, and arguments directly in a widget definition. Lists also let you specify multiple procedures for a specific callback resource. The ability to include lists in other lists makes them even more useful, as we'll show you in this section.

27.4.1 Specifying Common Resources

We talked about reusing interface components and callbacks earlier. By using lists, you can take this technique one step further to the level of widget children, resources, and callbacks. You can reduce the size of your modules by using lists to factor out common sets of resources. This technique is particularly useful for dealing with Form resource settings, as this fragment illustrates:

   list attachments : arguments {
     XmNleftAttachment = XmATTACH_FORM;
     XmNleftOffset = 3;
     XmNrightAttachment = XmATTACH_FORM;
     XmNrightOffset = 3;
   };
The attachments list defines some attachments that we can apply to a group of widgets in a Form, as shown in the following definitions:
   object name : XmTextField {
     arguments {
       arguments attach_args;
       XmNtopAttachment = XmATTACH_FORM;
     };
   };

   object phone : XmTextField {
     arguments {
       arguments attach_args;
        XmNtopAttachment = XmATTACH_WIDGET;
        XmNtopWidget = name;
        XmNbottomAttachment = XmATTACH_FORM;
     };
Each TextField definition includes its own attachments and the list, instead of restating the common settings. Although some of the resources are specified twice, only the last setting has any effect.

An extension of this concept gives us another interesting and useful way of working with lists. By including one or more lists in another list, you can create a flexible hierarchy of resource or callback settings. For example, you might use a list to specify a base style for all widgets, as in the following fragment:

   list base_style : arguments {
     XmNforeground = color ('black');
     XmNbackground = color ('magenta');
     XmNtraversalOn = true;
   };
When you add components to the interface, you can build on this base style. The following fragment shows how you might handle defining a ToggleButton-specific style:
   list toggle_btn_style : arguments {
     arguments base_style;
     XmNindicatorSize = 10;
     XmNindicatorType = XmN_OF_MANY;
     XmNselectColor = color ('yellow');
   };
Unfortunately, it turns out that most of the resource settings that work well in these situations are best left to resource files, as we discussed in the previous section. This use of lists can still be useful, however, when you are prototyping an application and don't feel like using an X resource file. When you are done prototyping, be sure to move the resource settings that affect appearance to a resource file, so that they can be modified by the user.

You can also use a hierarchy of lists to specify callback resources. If you are developing an application that supports context sensitive help, the root of your callback hierarchy might be specified as in the following list:

   list help_cbs : callbacks {
     XmNhelpCallback = procedure help();
   };
You can include this list in each widget that supports help. For a group of ToggleButtons, you can augment the list as follows:
   list toggle_cbs : callbacks {
     callbacks help_cbs;
     XmNvalueChangedCallback = procedure toggle_changed();
   };
The following widget definition illustrates how both the style and callback lists might be used:
   object hot_fudge : XmToggleButton {
     arguments {
       arguments attach_args;
       arguments toggle_args;
       XmNlabelString = "Hot Fudge";
     };
     callbacks toggle_cbs;
   };
You gain a couple of advantages by using lists this way. First, if you decide to change a color or a callback, you only need to make the change in one place, not in every widget definition. Second, each widget definition is considerably shorter than it would be without the list, which saves typing and generally makes a module shorter and easier to read.

27.4.2 Reusing Components

You can also use lists to help create reusable components. Earlier in this chapter, we talked about reusing components for top-level windows and dialogs. Although reusing parts of a dialog is not very common, reusing the panel of buttons that compose the action area of a dialog makes some sense. An application may use the definitions of OK, Cancel, and Help PushButtons repeatedly. Using lists, you can create a hierarchy of include files that allows you to reuse the same definitions for each dialog that needs them.

The first thing that needs to be defined is a container widget to hold the buttons. We keep this example simple by using a RowColumn manager widget. the source code shows the definition of the RowColumn.

   ! btn_panel.uih -- Button panel container

   object button_panel : XmRowColumn {
     controls buttons;
     arguments {
       XmNorientation = XmHORIZONTAL;
       XmNpacking = XmPACK_COLUMN;
       XmNentryAlignment = XmALIGNMENT_CENTER;
       !  Assume bottom of form placement
       XmNleftAttachment = XmATTACH_FORM;
       XmNrightAttachment = XmATTACH_FORM;
       XmNbottomAttachment = XmATTACH_FORM;
       ! Provide a hook for additional arguments
       arguments button_panel_args;
     };
   };
The button_panel RowColumn is an open-ended definition, as the children are specified as an undefined list. The definition also contains an undefined list in its arguments subsection, which allows the specification of additional resources. The next step is to add the OK, Cancel, and Help buttons to the panel using another include file, which is shown in the source code
   ! three_btn.uih -- OK, Cancel, Help button definitions

   list buttons : controls {
     OK : XmPushButton {
       arguments ok_args;
       callbacks ok_cbs;
     };
     Cancel : XmPushButton {
       arguments cancel_args;
       callbacks cancel_cbs;
     };
     Help : XmPushButton {
       arguments help_args;
       callbacks help_cbs;
     };
     ! Hook for additional buttons
     controls more_buttons;
   };

   include file 'btn_panel.uih';
So far, all of the widget definitions have been placed in include files. This technique is necessary because UIL does not support imported list values, so the only way you can reuse lists is by placing them in include files. We also define the PushButtons in an include file because they reference callback and argument lists. These lists must be defined by the module that includes the button definition. If the buttons were defined in a separate module, there would be no way to specify their behavior from within UIL.

The three_btn.uih file defines the three buttons for the panel and defines the controls list referenced in btn_panel.uih. An important feature of this example is that we have incorporated undefined lists in each of the widget's callbacks and arguments subsections. These lists work like macros in that they allow a module to use customized versions of the standard definitions. We also keep the list of buttons open-ended by including another undefined controls list named more_buttons. The file ends by including the button panel definition so that the main module does not need to include it. We normally recommend putting the include directives at the top of a file, but in early releases of Motif 1.2, a forward reference to a list causes the UIL compiler to crash. Therefore, it is necessary to include object definitions after the lists that they reference.

Now that the button panel and button definitions are complete, we can create a dialog that uses them. the source code demonstrates the creation of such a dialog.

   /* dialog.uil - a simple dialog that contains reusable PushButtons */

   module dialog

   include file 'procedures.uih';

   object root : XmForm {
     controls {
       XmRowColumn dialog_contents;
       XmSeparator separator;
       XmRowColumn button_panel;
     };
     arguments {
       XmNdialogTitle = "Login";
       XmNdefaultButton = OK;
     };
   };

   object dialog_contents : XmRowColumn {
     controls {
       User     : XmLabel { };
       Password : XmLabel { };
       XmTextField user_field;
       XmTextField pw_field; 
     };
     arguments {
       XmNnumColumns = 2;
       XmNpacking = XmPACK_COLUMN;
       XmNtopAttachment = XmATTACH_FORM;
       XmNleftAttachment = XmATTACH_FORM;
       XmNrightAttachment = XmATTACH_FORM;
       XmNbottomAttachment = XmATTACH_WIDGET;
       XmNbottomWidget = separator;
     };
   };

   object user_field : XmTextField {
     arguments {
       XmNcolumns = 2;
     };
   };

   object pw_field : XmTextField {
     arguments {
       XmNcolumns = 2;
     };
   };

   object separator : XmSeparator {
     arguments {
       XmNrightAttachment  = XmATTACH_FORM;
       XmNleftAttachment   = XmATTACH_FORM;
       XmNbottomAttachment = XmATTACH_WIDGET;
       XmNbottomWidget     = button_panel;
     };
   };

   ! Button specific settings.
   list ok_args : arguments { };
   list ok_cbs : callbacks {
     XmNactivateCallback = procedure do_it();
   };

   list cancel_args : arguments { };
   list cancel_cbs : callbacks {
     XmNactivateCallback = procedure forget_it();
   };

   list help_args : arguments { };
   list help_cbs : callbacks {
     XmNactivateCallback = procedure help_me();
   };

   ! No additional button panel arguments
   list button_panel_args : arguments { };

   ! No more buttons
   list more_buttons : controls { };

   ! Include the button panel definition
   include file 'three_btn.uih';

   end module;


The module starts by including the files that contain the callback procedure declarations. The top-level Form of the dialog contains the work area, a separator, and the button_panel. The work area consists of two labeled TextFields in a RowColumn. We complete the button_panel by defining the lists referenced in the include files. The arguments and callbacks lists for the OK, Cancel, and Help buttons are defined. There are no additional arguments for the buttons, the arguments lists are empty. Since there are no additional button_panel arguments and the dialog only has three buttons, the button_panel_args and more_buttons lists are also empty. With these list definitions in place, the module finally includes the file that defines the buttons and the button panel. The output of the dialog is shown in the figure.

You can reuse the button panel in as many dialogs as you want by following the model used in the source code Although we did not use many of the undefined lists, they make the button panel more flexible and configurable. These techniques can also be applied in a number of other situations throughout an application. This method of using lists and include files can help to reduce redundancy in an interface definition.

figs.eps/V6a.26.03.eps.png
User interface of dialog.uil


27.5 Prototyping an Interface With UIL

The showuid program in Chapter 24, Creating a User Interface With UIL, provides a useful foundation for prototyping a user interface. The program contains two simple callbacks: one for printing a message and one for exiting the program. By adding a few more callbacks, we can make the program even more useful as a building block for developing interfaces.

27.5.1 Managing Widgets

Most user interfaces consist of a main application window and several support windows or dialogs. Since dialogs are frequently posted and unposted, we can make prototyping easier by writing callbacks that manage and unmanage widgets. the source code shows these routines.

   void
   manage (w, client_data,call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       Widget target = (Widget) client_data;
       XtManageChild (target);
   }

   void
   unmanage (w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       Widget target = (Widget) client_data;
       XtUnmanageChild (target);
   }
When adding new callbacks, we also need to update the list of callbacks that the showuid application registers with MrmRegisterNames(). With the addition of these two functions, the list is declared as follows:
   static MrmRegisterArg callback_list[] = {
       { "quit",     (XtPointer) quit },
       { "print",    (XtPointer) print },
       { "manage",   (XtPointer) manage },
       { "unmanage", (XtPointer) unmanage },
       /* Add additional callback procedures here... */
   };
To demonstrate these new callbacks, we can create an interface with a Quit PushButton that displays a confirmation dialog. This example is realistic, as many applications bring up a confirmation dialog to prevent you from quitting accidentally or making irreversible changes. A module that uses the manage() callback is shown in the source code
   module quitbox

   procedure
     quit();
     manage (widget);
     unmanage (widget);

   object quit_dialog : XmQuestionDialog {
     controls {
       Xm_Help {
         ! Disable the help button for now.
         arguments {
           XmNsensitive = false;
         };
       };
     };
     callbacks {
       XmNokCallback = procedure quit();
     };
     arguments {
       XmNmessageString = "Really Quit?";
       XmNdialogTitle = "Confirm Quit";
       XmNdialogStyle = XmDIALOG_FULL_APPLICATION_MODAL;
     };
   };

   object quitb : XmPushButton {
     arguments {
       XmNlabelString = "Quit";
     };
     callbacks {
       XmNactivateCallback = procedure manage (quit_dialog);
     };
   };

   object root : XmMainWindow {
     controls {
       XmPushButton quitb;
       unmanaged XmQuestionDialog quit_dialog;
     };
   };

   end module;
The output of this example is shown in the figure.

figs.eps/V6a.26.04.eps.png
User interface of quitbox.uil

The Quit PushButton manages the dialog, which causes it to be displayed. Pressing the OK PushButton exits the application. There is no need to use the unmanage() callback in this example, as the Cancel PushButton unmanages the dialog by default. You can easily apply the manage() and unmanage() callbacks to other dialogs in an interface.

27.5.2 Creating Widgets

In the source code the entire user interface is defined in a single widget hierarchy. This technique is fine for a small application, but for performance reasons it is not practical in a larger application. Creating a separate hierarchy for each window allows you to divide an interface into separate modules, as discussed earlier in this chapter. The only drawback to distributing dialog creation is that it takes longer for a dialog to appear the first time it is displayed. Since this delay is typically not noticeable, we still recommend this approach.

You can support the as-needed dialog creation policy in UIL by adding another callback that creates a widget hierarchy. A new widget hierarchy is created by calling MrmFetchWidget(). As a reminder, this function takes the following form:

   Cardinal
   MrmFetchWidget(hierarchy, widget_name, parent, widget_return,
                  class_return)
       MrmHierarchy   hierarchy;
       String         widget_name;
       Widget         parent;
       Widget        *widget_return;
       MrmType       *class_return;
A creation callback needs three values to be able to create a new widget hierarchy. These values are the first three arguments to MrmFetchWidget(). The MrmHierarchy is already available in the application program, so we only need to make it a global variable instead of a local variable in main(). Since the callback accesses the hierarchy repeatedly, we also need to remove the call to MrmCloseHierarchy(). In order to get the other two arguments, we use an asciz_table that contains the names of the parent widget and the widget to create. We can convert the name of the parent to a widget ID using XtNameToWidget(). This technique only works if we specify a unique name for each widget. Since we need the top-level widget as an argument to this routine, it becomes a global variable as well. The complete callback appears in the source code
   void
   create (w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       String *args = (String *) client_data;
       String parent_name = args[0];
       Widget parent;

       /* Get a widget id for the parent widget. */
       if (strcmp (parent_name, "toplevel") != 0)
           parent = XtNameToWidget (toplevel, parent_name);
       else
           parent = toplevel;

       /* If the parent was found try to create the hierarchy. */
       if (parent == NULL)
           fprintf (stderr, "Create: No such widget '%s'0, args[0]);
       else {
           String child_name = args[1];
           Widget new_w;
           Cardinal status;
           MrmType class;

           status = MrmFetchWidget (hierarchy, child_name, parent,
               &new_w, &class);
           if (status != MrmSUCCESS)
               fprintf (stderr, "Failed to create hierarchy '%s'0, child_name);
         }

       /* After the widget is created, this callback can be removed. */
       XtRemoveCallback (w, XmNactivateCallback, create, client_data);
   }
The callback assigns the client_data argument to a String array, since that is appropriate for the UIL asciz_table type. The routine also converts the name of the parent widget to a widget ID using XtNameToWidget(). Since that routine does not consider the top-level widget in its search, the callback performs a special test for that widget. If the routine finds the ID for the parent widget, it attempts to create the new widget hierarchy. Once the hierarchy is created, the callback is removed so that the widget hierarchy is not created more than once. We make the assumption that if the creation fails once, subsequent attempts will also fail.

With the addition of the create() callback, we can split the source code into two modules. Unfortunately, breaking up the module into two introduces a new problem. Now that we have two separate widget hierarchies, the manage() callback can no longer reference the quit_dialog widget because it is defined in another hierarchy. One solution to this problem is to export the widget ID of the dialog as a UIL identifier, using the MrmNcreateCallback (illustrated in Chapter 25, Building an Application With UIL). The problem with this approach is that you must maintain a list of identifiers for all of the exported widgets. Another solution, which avoids this problem, uses XtNameToWidget() in the manage() and unmanage() callbacks to obtain a widget ID given a widget name. The revised callbacks are shown in the source code

   void
   manage (w, client_data,call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       String name = (String) client_data;
       Widget target = XtNameToWidget (toplevel, name);
       if (target != NULL)
           XtManageChild (target);
       else
           fprintf (stderr, "Cannot manage widget named %s0, name);
   }

   void
   unmanage (w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       String name = (String) client_data;
       Widget target = XtNameToWidget (toplevel, name);
       if (target != NULL)
           XtUnmanageChild (target);
       else
           fprintf (stderr, "Cannot unmanage widget named %s0, name);
   }
Now we can rewrite the quitbox example using two modules. The main window definition is listed in the source code
   module mainwindow

   procedure
     manage (string);
     create (asciz_table);

   list confirm_quit : procedures {
     create (asciz_table ("toplevel", "quit_dialog"));
     manage ("*quit_dialog");
   };

   object quitb : XmPushButton {
     arguments {
       XmNlabelString = "Quit";
     };
     callbacks {
       XmNactivateCallback = procedures confirm_quit;
     };
   };

   object root : XmMainWindow {
     controls {
       XmPushButton quitb;
     };
   };

   end module;

The XmNactivateCallback of the Quit PushButton now creates the confirmation dialog and manages it. The parent and widget to be created are passed to the creation callback in an asciz_table . Because the create() callback removes itself, subsequent invocations of the callback only manage the dialog. XtNameToWidget() expects a qualified widget name, much like resource specifications, so we must precede the name passed to the manage() callback with an asterisk. The quit_dialog is now defined in a separate module, shown in the source code
   module quitbox

   procedure
     quit();

   object quit_dialog : XmQuestionDialog {
     controls {
       Xm_Help {
         ! Disable the help button for now.
         arguments {
           XmNsensitive = false;
         };
       };
     };
     callbacks {
       XmNokCallback = procedure quit();
     };
     arguments {
       XmNmessageString = "Really Quit?";
       XmNdialogTitle = "Confirm Quit";
       XmNdialogStyle = XmDIALOG_FULL_APPLICATION_MODAL;
     };
   };

   end module;
The only potential disadvantage of this method of creating dialogs is that string-to-widget lookup is slightly slower than using a widget pointer directly. For most moderately-sized widget trees, the difference should not be noticeable. The creation callback is a useful tool that can be especially helpful when you are prototyping an interface.

27.6 Summary

Once you've learned the basics of UIL, you can begin to take full advantage of its features. Some advanced techniques include: defining non-Motif widgets; using lists to shorten modules and create reusable interface components, and using UIL to rapidly prototype the interface for an application. As UIL modules grow, it is important to pay attention to their organization. A well-organized set of modules can ease the task of editing and maintaining an interface. When you develop a real-world application with UIL, it is also important to consider the best location for resource settings. While most fixed resources can be set in UIL and possibly modified in application code, it is usually best to specify values that a user might want to change in an app-defaults file.


Contents Previous Next