Example (XForms (Forms Library) - Version 1.2 (Rev. 4) A Graphical User Interface Toolkit for X)

Previous: Free Object, Up: Part I Free Objects [Contents][Index]

### 5.2 An Example

We conclude our discussion of the free object by examining a simple drawing program capable of drawing simple geometric figures like squares, circles, and triangles of various colors and sizes, and of course it also utilizes a free object.

The basic UI consists of three logical parts. A drawing area onto which the squares etc. are to be drawn; a group of objects that control what figure to draw and with what size; and a group of objects that control the color with which the figure is to be drawn.

The entire UI is designed interactively using the GUI builder `fdesign` with most objects having their own callbacks. `fdesign` writes two files, one is a header file containing forward declarations of callback functions and other function prototypes:

    #ifndef FD_drawfree_h_
    #define FD_drawfree_h_

    extern void change_color(FL_OBJECT *, long);
    extern void switch_figure(FL_OBJECT *, long);

    /* more callback declarations omitted */

    typedef struct {
        FL_FORM   * drawfree;
        FL_OBJECT * freeobj;
        FL_OBJECT * figgrp;
        FL_OBJECT * colgrp;
        FL_OBJECT * colorobj;
        FL_OBJECT * miscgrp;
        FL_OBJECT * sizegrp;
        FL_OBJECT * wsli;
        FL_OBJECT * hsli;
        FL_OBJECT * drobj[3];
        void      * vdata;
        long        ldata;
    } FD_drawfree;

    extern FD_drawfree *create_form_drawfree(void);
    #endif /* FD_drawfree_h_ */

The other file contains the actual C-code that creates the form when compiled and executed. Since free objects are not directly supported by fdesign, a box was used as a stub for the location and size of the drawing area. After the C-code was generated, the box was changed manually to a free object by replacing `fl_add_box(FL_DOWN_BOX,...)` with `fl_add_free(FL_NORMAL_FREE,...)`. We list below the output generated by fdesign with some comments:

    FD_drawfree *create_form_drawfree(void) {
        FL_OBJECT *obj;
        FD_drawfree *fdui = fl_calloc(1, sizeof *fdui);

        fdui->drawfree = fl_bgn_form(FL_NO_BOX, 530, 490);
        obj = fl_add_box(FL_UP_BOX, 0, 0, 530, 490, "");

This is almost always the same for any form definition: we allocate a structure that will hold all objects on the form as well as the form itself. In this case, the first object on the form is a box of type `FL_UP_BOX`.

        fdui->figgrp = fl_bgn_group();

        obj = fl_add_button(FL_RADIO_BUTTON, 10, 60, 40, 40,
                            "@#circle");
        fl_set_object_lcolor(obj,FL_YELLOW);
        fl_set_object_callback(obj, switch_figure, 0);

        obj = fl_add_button(FL_RADIO_BUTTON, 50, 60, 40, 40,
                            "@#square");
        fl_set_object_lcolor(obj, FL_YELLOW);
        fl_set_object_callback(obj, switch_figure, 1);

        obj = fl_add_button(FL_RADIO_BUTTON, 90, 60, 40, 40,
                            "@#8*>");
        fl_set_object_lcolor(obj, FL_YELLOW);
        fl_set_object_callback(obj, switch_figure, 2);

        fl_end_group();

This creates three buttons that control what figures are to be drawn. Since figure selection is mutually exclusive, we use `RADIO_BUTTON` for this. Further, the three buttons are placed inside a group so that they won’t interfere with other radio buttons on the same form. Notice that the callback function `switch_figure()` is bound to all three buttons but with different arguments. Thus the callback function can resolve the associated object via the callback function argument. In this case, 0 is used for circle, 1 for square and 2 for triangle. This association of a callback function with a piece of user data can often reduce the amount of code substantially, especially if you have a large group of objects that control similar things. The advantage will become clear as we proceed.

Next we add three sliders to the form. By using appropriate colors for these sliding bars (red, green, blue), there is no need to label them. There’s also no need to store their addresses as their callback routine `change_color()` will receive them automatically.

       fdui->colgrp = fl_bgn_group();

       obj = fl_add_slider(FL_VERT_FILL_SLIDER, 25, 170, 30, 125, "");
       fl_set_object_color(obj, FL_COL1, FL_RED);
       fl_set_object_callback(obj, change_color, 0);

       obj = fl_add_slider(FL_VERT_FILL_SLIDER, 55, 170, 30, 125, "");
       fl_set_object_color(obj, FL_COL1, FL_GREEN);
       fl_set_object_callback(obj, change_color, 1);

        obj = fl_add_slider(FL_VERT_FILL_SLIDER, 85, 170, 30, 125, "");
        fl_set_object_color(obj, FL_COL1, FL_BLUE);
        fl_set_object_callback(obj, change_color, 2);

        fdui->colorobj = obj = fl_add_box(FL_BORDER_BOX,
                                          25, 140, 90, 25, "");
        fl_set_object_color(obj, FL_FREE_COL1, FL_FREE_COL1);

        fl_end_group();

Again, a single callback function, `change_color()`, is bound to all three sliders. In addition to the sliders, a box object is added to the form. This box is set to use the color indexed by `FL_FREE_COL1` and will be used to show visually what the current color setting looks like. This implies that in the `change_color()` callback function, the entry `FL_FREE_COL1` in the Forms Library’s internal colormap will be changed. We also place all the color related objects inside a group even though they are not of radio buttons. This is to facilitate gravity settings which otherwise require setting the gravities of each individual object.

Next we create our drawing area which is simply a free object of type `NORMAL_FREE` with a handler to be written

        obj = fl_add_frame(FL_DOWN_FRAME, 145, 30, 370, 405, "");
        fl_set_object_gravity(obj, FL_NorthWest, FL_SouthEast);

        fdui->freeobj = obj = fl_add_free(FL_NORMAL_FREE,
                                          145, 30, 370, 405, "",
                                          freeobject_handler);
        fl_set_object_boxtype(obj, FL_FLAT_BOX);
        fl_set_object_gravity(obj, FL_NorthWest, FL_SouthEast);

The frame is added for decoration purposes only. Although a free object with a down box would appear the same, the down box can be written over by the free object drawing while the free object can’t draw on top of the frame since the frame is outside of the free object. Notice the gravity settings. This kind of setting maximizes the real estate of the free object when the form is resized.

Next, we need to have control over the size of the object. For this, two sliders are added, using the same callback function but with different user data (0 and 1 in this case):

        fdui->sizegrp = fl_bgn_group();

        fdui->wsli = obj = fl_add_valslider(FL_HOR_SLIDER,
                                            15, 370, 120, 25, "Width");
        fl_set_object_lalign(obj, FL_ALIGN_TOP);
        fl_set_object_callback(obj, change_size, 0);

        fdui->hsli = obj = fl_add_valslider(FL_HOR_SLIDER,
                                            15, 55, 410,25, "Height");
        fl_set_object_lalign(obj, FL_ALIGN_TOP);
        fl_set_object_callback(obj, change_size, 1);

        fl_end_group();

The rest of the UI consists of some buttons the user can use to exit the program, elect to draw outlined instead of filled figures etc. The form definition ends with `fl_end_form()`. The structure that holds the form as well as all the objects within it is returned to the caller:

        fdui->miscgrp = fl_bgn_group();

        obj = fl_add_button(FL_NORMAL_BUTTON, 395, 445, 105, 30,
                            "Quit");
        fl_set_button_shortcut(obj, "Qq#q", 1);

        obj = fl_add_button(FL_NORMAL_BUTTON, 280, 445, 105, 30,
                            "Refresh");
        fl_set_object_callback(obj, refresh_cb, 0);

        obj = fl_add_button(FL_NORMAL_BUTTON, 165, 445, 105, 30,
                            "Clear");
        fl_set_object_callback(obj,clear_cb,0); fl_end_group();

        obj = fl_add_checkbutton(FL_PUSH_BUTTON, 15, 25, 100, 35,
                                 "Outline");
        fl_set_object_color(obj, FL_MCOL, FL_BLUE);
        fl_set_object_callback(obj, fill_cb, 0);
        fl_set_object_gravity(obj, FL_NorthWest, FL_NorthWest);

        fl_end_form();

        return fdui;
    }

After creating the UI we need to write the callback functions and the free object handler. The callback functions are relatively easy since each object is designed to perform a very specific task.

Before we proceed to code the callback functions we first need to define the overall data structure that will be used to glue together the UI and the routines that do real work.

The basic structure is the DrawFigure structure that holds the current drawing function as well as object attributes such as size and color:

    #define MAX_FIGURES 500

    typedef void (*DrawFunc)(int                  /* fill */,
                             int, int, int, int,  /* x,y,w,h */
                             FL_COLOR             /* color */ );

    typedef struct {
        DrawFunc drawit;      /* how to draw this figure */
        int      fill,        /* is it to be filled? */
                 x, y, w, h;  /* position and sizes */
        int      pc[3];       /* primary color R,G,B */
        int      newfig;      /* indicate a new figure */
        FL_COLOR col;         /* color index */
    } DrawFigure;

    static DrawFigure saved_figure[MAX_FIGURES],
                      *cur_fig;
    static FD_drawfree *drawui;
    int max_w = 30,               /* max size of figures */
        max_h = 30;

All changes to the figure attributes will be buffered in `cur_fig` and when the actual drawing command is issued (mouse click inside the free object), `cur_fig` is copied into `saved_figure` array buffer.

Forms Library contains some low-level drawing routines that can draw and optionally fill arbitrary polygonal regions, so in principle, there is no need to use Xlib calls directly. To show how Xlib drawing routines are combined with Forms Library, we use Xlib routines to draw a triangle:

    void draw_triangle(int fill, int x, int y,
                       int w, int h, FL_COLOR col) {
        XPoint xp[4];
        GC gc = fl_state[fl_get_vclass()].gc[0];
        Window win = fl_winget();
        Display *disp = fl_get_display();

        xp[0].x = x;
        xp[0].y = y + h - 1;
        xp[1].x = x + w / 2;
        xp[1].y = y;
        xp[2].x = x + w - 1;
        xp[2].y = y + h - 1;
        XSetForeground(disp, gc, fl_get_pixel(col));

        if (fill)
            XFillPolygon(disp, win, gc, xp, 3, Nonconvex, Unsorted);
        else {
            xp[3].x = xp[0].x;
            xp[3].y = xp[0].y;
            XDrawLines(disp, win, gc, xp, 4, CoordModeOrigin);
        }
    }

Although more or less standard stuff, some explanation is in order. As you have probably guessed, `fl_winget()` returns the current "active" window, defined to be the window the object receiving the dispatcher’s messages (`FL_DRAW` etc.) belongs to10. Similarly the routine `fl_get_display()` returns the current connection to the X server. Part IV has more details on the utility functions in the Forms Library.

The array of structures `fl_state[]` keeps much "inside" information on the state of the Forms Library. For simplicity, we choose to use the Forms Library’s default GC. There is no fundamental reason that this has be so. We certainly can copy the default GC and change the foreground color in the copy. Of course unlike using the default GC directly, we might have to set the clip mask in the copy whereas the default GC always have the proper clip mask (in this case, to the bounding box of the free object).

We use the Forms Library’s built-in drawing routines to draw circles and rectangles. Then our drawing functions can be defined as follows:

    static DrawFunc drawfunc[] = {
        fl_oval, fl_rectangle, draw_triangle };

Switching what figure to draw is just changing the member `drawit` in `cur_fig`. By using the proper object callback argument, figure switching is achieved by the following callback routine that is bound to all figure buttons

    void switch_object(FL_OBJECT *obj, long which) {
        cur_fig->drawit = drawfunc[which];
    }

So this takes care of the drawing functions. Similarly, the color callback function can be written as follows

    void change_color(FL_OBJECT *obj, long which) {
        cur_fig->c[which] = 255 * fl_get_slider_value(obj);
        fl_mapcolor(cur_fig->col,
                    cur_fig->c[0], cur_fig->c[1], cur_fig->c[2]);
        fl_mapcolor(FL_FREE_COL1,
                    cur_fig->c[0], cur_fig->c[1], cur_fig->c[2]);
        fl_redraw_object(drawui->colorobj);
    }

The first call of `fl_mapcolor()` defines the RGB components for index `cur_fig->col` and the second `fl_mapcolor()` call defines the RGB component for index `FL_FREE_COL1`, which is the color index used by `colorobj` that serves as current color visual feedback.

Object size is taken care of in a similar fashion by using a callback function bound to both size sliders:

    void change_size(FL_OBJECT * obj, long which) {
        if (which == 0)
            cur_fig->w = fl_get_slider_value(obj);
        else
            cur_fig->h = fl_get_slider_value(obj);
    }

Lastly, we toggle the fill/outline option by querying the state of the push button

    void outline_callback(FL_OBJECT *obj, long data) {
        cur_fig->fill = !fl_get_button(obj);
    }

To clear the drawing area and delete all saved figures, a Clear button is provided with the following callback:

    void clear_cb(FL_OBJECT *obj, long notused) {
        saved_figure[0] = *cur_fig;  /* copy attributes */
        cur_fig = saved_figure;
        fl_redraw_object(drawui->freeobj);
    }

To clear the drawing area and redraw all saved figures, a Refresh button is provided with the following callback:

    void refresh_cb(FL_OBJECT *obj, long notused) {
        fl_redraw_object(drawui->freeobj);
    }

With all attributes and other services taken care of, it is time to write the free object handler. The user can issue a drawing command inside the free object by clicking either the left or right mouse button.

    int freeobject_handler(FL_OBJECT *obj, int event,
                           FL_Coord mx, FL_Coord my,
                           int key, void *xev) {
        DrawFigure *dr;

        switch (event) {
            case FL_DRAW:
                if (cur_fig->newfig == 1)
                     cur_fig->drawit(cur_fig->fill,
                                     cur_fig->x + obj->x,
                                     cur_fig->y + obj->y,
                                     cur_fig->w, cur_fig->h,
                                     cur_fig->col);
                else {
                    fl_draw_box(obj->boxtype, obj->x, obj->y, obj->w,
                                obj->h, obj->col1, obj->bw);

                    for (dr = saved_figure; dr < cur_fig; dr++) {
                        fl_mapcolor(FL_FREE_COL1,
                                    dr->c[0], dr->c[1], dr->c[2]);
                        dr->drawit(dr->fill,dr->x + obj->x,
                                   dr->y + obj->y,
                                   dr->w, dr->h, dr->col);
                    }
                }
                cur_fig->newfig = 0;
                break;

            case FL_PUSH:
                if (key == FL_MIDDLE_MOUSE)
                    break;

                cur_fig->x = mx - cur_fig->w / 2;
                cur_fig->y = my - cur_fig->h / 2;

                /* convert figure center to relative to the object*/
                cur_fig->x -= obj->x;
                cur_fig->y -= obj->y;

                cur_fig->newfig = 1;
                fl_redraw_object(obj);
                *(cur_fig + 1) = *cur_fig;
                fl_mapcolor(cur_fig->col + 1, cur_fig->c[0],
                            cur_fig->c[1], cur_fig->c[2] );
                cur_fig++;
                cur_fig->col++;
                break;
        }

        return FL_RETURN_NONE;
    }

In this particular program, we are only interested in mouse clicks and redraw. The event dispatching routine cooks the X event and drives the handler via a set of events (messages). For a mouse click inside the free object, its handler is notified with an FL_PUSH together with the current mouse position mx, my. In addition, the driver also sets the clipping mask to the bounding box of the free object prior to sending `FL_DRAW`. Mouse position (always relative to the origin of the form) is directly usable in the drawing function. However, it is a good idea to convert the mouse position so it is relative to the origin of the free object if the position is to be used later. The reason for this is that the free object can be resized or moved in ways unknown to the handler and only the position relative to the free object is meaningful in these situations.

It is tempting to call the drawing function in response to `FL_PUSH` since it is `FL_PUSH` that triggers the drawing. However, it is a (common) mistake to do this. The reason is that much bookkeeping is performed prior to sending `FL_DRAW`, such as clipping, double buffer preparation and possibly active window setting etc. All of these is not done if the message is anything else than `FL_DRAW`. So always use `fl_redraw_object()` to draw unless it is a response to `FL_DRAW`. Internally `fl_redraw_object()` calls the handler with `FL_DRAW` (after some bookkeeping), so we only need to mark `FL_PUSH` with a flag `newfig` and let the drawing part of the handler draw the newly added figure.

`FL_DRAW` has two parts. One is simply to add a figure indicated by `newfig` being true and in this case, we only need to draw the figure that is being added. The other branch might be triggered as a response to damaged drawing area resulting from `Expose` event or as a response to `Refresh` command. We simply loop over all saved figures and (re)draw each of them.

The only thing left to do is to initialize the program, which includes initial color and size, and initial drawing function. Since we will allow interactive resizing and also some of the objects on the form are not resizeable, we need to take care of the gravities.

    void draw_initialize(FD_drawfree *ui) {
        fl_set_form_minsize(ui->drawfree, 530, 490);
        fl_set_object_gravity(ui->colgrp, FL_West, FL_West);
        fl_set_object_gravity(ui->sizegrp, FL_SouthWest, FL_SouthWest);
        fl_set_object_gravity(ui->figgrp, FL_NorthWest, FL_NorthWest);
        fl_set_object_gravity(ui->miscgrp, FL_South, FL_South);
        fl_set_object_resize(ui->miscgrp, FL_RESIZE_NONE);

        cur_fig = saved_figure;
        cur_fig->pc[0] = cur_fig->pc[1] = cur_fig->pc[2] = 127;
        cur_fig->w = cur->fig->h = 30;
        cur_fig->drawit = fl_oval;
        cur_fig->col = FL_FREE_COL1 + 1;
        cur_fig->fill = 1;
        fl_set_button(ui->drobj[0], 1);  /* show current selection */

        fl_mapcolor(cur_fig->col, cur_fig->pc[0],
                    cur->fig->pc[1], cur->fig->pc[2]);
        fl_mapcolor(FL_FREE_COL1, cur_fig->pc[0],
                    cur->fig->pc[1], cur->fig->pc[2]);

        fl_set_slider_bounds(ui->wsli, 1, max_w);
        fl_set_slider_bounds(ui->hsli, 1, max_h);
        fl_set_slider_precision(ui->wsli, 0);
        fl_set_slider_precision(ui->hsli, 0);
        fl_set_slider_value(ui->wsli, cur_fig->w);
        fl_set_slider_value(ui->hsli, cur_fig->h);
    }

With all the parts in place, the main program simply creates, initializes and shows the UI, then enters the main loop:

    int main(int argc, char *argv[]) {
        fl_initialize(&argc, argv, "FormDemo", 0, 0);
        drawui = create_form_drawfree();
        draw_initialize(drawui);
        fl_show_form(drawui->drawfree, FL_PLACE_CENTER|FL_FREE_SIZE,
                     FL_FULLBORDER, "Draw");
        fl_do_forms();
        return 0;
    }

Since the only object that does not have a callback is the Quit button, `fl_do_forms()` will return only if that button is pushed. Full source code to this simple drawing program can be found in `demos/freedraw.c`.

#### Footnotes

### (10)

If `fl_winget()` is called while not handling messages, the return value must be checked.

Previous: Free Object, Up: Part I Free Objects [Contents][Index]