New Layout Initiative

by Alexey Parshin


The Introduction

I'm developing FLTK applications for about a year for now, and have ported some of my Windows projects from Borland C++ Builder to FLTK. The process went more or less painful, and now I have several projects working. During the porting I've noticed that some part of coding took most of my time. I also noticed that I didn't like this exact part of coding :) I'm talking about placing the widgets on the form or window. FLTK allows you to place the widgets just nicely, it also allows to define how widgets should be resized if the form or window is resized. Unfortunately, the traditional FLTK way resizes widgets in both directions – height and width. It's not exactly what user of the application expects from it, especially when the application has widgets like combo box or numeric input. These widgets should be resized the very certain way, keeping the vertical size in certain limits specific to widget. Trying to meet such size requirements, I had to implement a virtual resize() method in every window or form of my programs. I didn't like it. It takes too much work and too much time. Also, the constructor of every window contained widgets with sizes defined. Unfortunately, resize function doesn't really need these sizes, or even ignores them completely. I didn't like it ether.

The Idea

After some experiments, I've decided to implement the automatic layout manager. The main goal was to provide minimum work required from the programmer to use it, and to make program shorter. It should also better use the constructor information, and shouldn't require to define virtual resize() method.

First, the idea was to define a widget alignment as one of: right, left, top, bottom and client. The widget alignment supposed to tell the application how the widget should be located.

Second, every widget has different size limitations or preferences. To solve this problem, someone in FLTK Usenet group proposed to define a preferred size method. That method would tell the system how the widget wants to be resized. I'd be glad to mention the author of this nice idea as soon as I have his name.

Third, to handle the extra widget information it takes to use a special group. That group should be smart enough to ask the widgets about their preferred sizes, and locate them accordingly to the available space. Also, the group often enough may need the extra space and should be able to grow, so all the widgets inside would fit.

The implementation

From the idea' description I've pictured two basic classes: CLayoutClient and CLayoutManager. The CLayoutClient is used as one of the basic classes for the widget in order to let the layout manager know – it's our guy and we have to locate him properly. CLayoutManager class is managing the client widgets that is derived from CLayoutClient. It doesn't mean the widgets should be derived from only CLayoutClient. The widgets should be derived from the original widget, say, Fl_Input, and CLayoutClient. Here is the example (not a real class):

class CMyInput : public Fl_Input, public CLayoutClient {

public:

CMyInput(int x, int y, int w, int h, const char *label=0L) : Fl_Input(x,y,w,h,label), CLayoutClient(SP_ALIGN_TOP) {}

virtual void preferredSize(int& width, int& height) const;

};

As soon as we have such class definition, we can use that class with CLayoutManager. The only problem is – the layout manager doesn't need the location and size of the widget defined in constructor. What it needs, however, is the optional size of the widget (depending on the alignment, it's height or width) and the widget alignment in layout. So, the optimal constructor should look like this:

CMyInput(const char *label=0L, int layoutSize=10, CLayoutAlign alignment=SP_ALIGN_TOP);

The default values are chosen based on the most popular values: A lot of the forms are placing the widgets from top to bottom, that gives us the alignment=SP_ALIGN_TOP. And, in many cases, the widgets have the preferredSize() method defining the exact size of the widget, or at least – the exact height of it. Want to see the example? The Fl_Input widget, for instance, if it's not in multi-line mode, should always have the certain height, based on the widget font. The CDateTimeInput widget is even more strict, requiring both height and width defined based on the widget font. But, the widgets like Fl_Browser may have the height and width only limited by the minimal value (something like double font size), and we have to provide some information about the desirable widget size. What should be inside the preferredSize() method? It is very simple. If the widget wants to take any size it's asked – the body of that method should be empty. Otherwise, the widget simply checks the values of arguments width and height and changes them if necessary. For instance, if the widget may have any width bigger than 20, and the height always 20, the method should look like:

void CMyInput::preferredSize(int& width, int& height) const {

if (width < 20) width = 20;

height = 20;

}

In SPTK, the kind of the constructor I've just described, is widely used. In fact, to use the regular FLTK constructors for SPTK widgets, you have to define __COMPATIBILITY_MODE__ constant during the SPTK compilation. Otherwise, the old style constructors are simply not included. So the code creating several of widgets looks like:

new CInput(“first name”);

new CInput(“last name”);

new CDateInput(“birth date”);

Looks pretty strange for FLTK, isn't it? Yet, it works. Of course, it doesn't work as it should if used with the regular Fl_Group. In order for layout to work properly, the widgets with layout support should be placed in ClayoutManager-based class. SPTK provides two of such classes: CGroup and CWindow. So, the complete window code looks like:

CWindow *w = new CWindow(300,300,”My window”);

new CInput(“first name”);

new CInput(“last name”);

new CDateInput(“birth date”);

w->end();

w->resizable(w);

That's all! CWindow will place the widgets one by one from the top to the bottom, resize them correctly even if the window size is changed.

If you need to use the widget with less strictly defined preferredSize(), the code should include an extra size value, like:

CWindow *w = new CWindow(300,300,”My window”);

new CInput(“first name”);

new CInput(“last name”);

new CDateInput(“birth date”);

new CListView(“Work history”,150);

w->end();

w->resizable(w);

In that example, ListView will have the height of 150 pixels, and will keep this height even if the window is resized. If you want the widget to occupy all the space not taken by other widgets, the widget is created with the client-alignment. The size value is simply ignored.

new CListView(“Work history”, 10, SP_ALIGN_CLIENT);



How Does it work?

Let's say we start from the empty CWindow object, and add several widgets. The following code is a simplified version from the examples/cgroup_test.cpp (see SPTK distribution):

#include <sptk3/cgui>

using namespace sptk;

int main() {
    CWindow w(500,400,"CGroup test");
    w.resizable(w);
    w.color(0xC0C8FF00);

    // CCheckButtons figures out the size from the list
    // of choices. The default alignment is SP_ALIGN_TOP,
    // and you can change it in ctor
    CCheckButtons cbl("Check Buttons: ");
    cbl.buttons(CStringList("first,second,third,*",","));

    CRadioButtons rbl("Radio Buttons: ");
    rbl.buttons(CStringList("first,second,third,*",","));

    // CListView is more flexible, than CCheckButtons or
    // CRadioButtons, it's vertical size is defined with
    // the layoutSize parameter in ctor as 150.
    // SP_ALIGN_CLIENT allows that widget to occupy all
    // the space left after all the other widgets are 
    // put in place
    CListView listView("List View:",10,SP_ALIGN_CLIENT);
    listView.columns().add(CColumn("column 1",VAR_INT,70));
    listView.columns().add(CColumn("column 2",VAR_INT,70));
    listView.columns().add(CColumn("column 3",VAR_STRING,200));
    listView.columns().add(CColumn("column 4",VAR_STRING));

    // That group keeps togeteher the buttons. These
    // buttons use the default alignment for buttons - 
    // SP_ALIGN_RIGHT, and the text/icon defined by the 
    // button kind.
    CGroup buttonGroup("",10,SP_ALIGN_BOTTOM);
    buttonGroup.color(0xA0A0E000);
    CButton okButton(SP_OK_BUTTON);
    okButton.defaultButton(true);
    CButton addButton(SP_ADD_BUTTON);
    CButton nextButton(SP_NEXT_BUTTON);
    buttonGroup.end();

    w.end();
    w.show();

    Fl::run();

    return 0;
}



At the moment we create the window and fill it with the widgets – none of the widgets has the correct size. When the window is shown, at some moment FLTK calls that window resize() method, and layout manager comes forward. The layout manager processes the widgets in the order they were created. So, let's try to watch how it's done.



The very beginning. When the layout manager starts to work, none of the components is located and sized correctly. The is considered as an empty area, with the size (ww, hh) pixels. This area is slightly smaller than the available window area if the layoutSpacing() is not 0. layoutSpacing() defines the extra space between the widgets



The first widget to place is CCheckButtons cbl. Layout manager calls the preferredSize() method for that widget and finds out what is the optimal widht and height of the widget. For the check buttons the height is determined based on the actual number of check buttons inside. The width of the widget isn't limited, so it takes all the available width of the area.

The default layout alignment is SP_ALIGN_TOP, so the widget is aligned to the top of available area.

After placing the widget, layout manager decreases the size of the available by the size taken by the widget.



After placing the first widget, the layout manager switches to the second one (in the order the widgets were created). Once again, the widget is asked through the preferredSize() method how it wants to be resized. The layout manager resizes the widget with the default layout align to the top of the available area and once again decreases the area size.

Next widget to process is CListView with the layout alignment as SP_ALIGN_CLIENT. This is a special alignment – only one widget inside the group may have it. Layout manager takes a note about this widget but doesn't resize it before all the other widgets in the group are resized.



Next, the CGroup buttonGroup is asked for the preferredSize(). This is little bit more complicated than before, but not for you – for layout manager. Layout manager first computes the preferredSize() for every widget inside the group, and then computes the preferredSize() for itself. After it's done, the parent group (the window) resizes the buttonGroup and puts it to the buttom of the available area as defined by the constructor:

CGroup buttonGroup("",10,SP_ALIGN_BOTTOM);

The group that was originally created with the height 10 grows to fit all the buttons inside.



And, finaly, the moment of truth - the very last step. After all the other widgets are placed, layout manager places the widget with the layout align SP_ALIGN_CLIENT (if you had one, of course). It tries to take all the remaining space, if the preferredSize() allows to do so.

The whole set of operations is repeated when you resize the window. Of course, nothing is perfect. The layout manager ignores the regular widgets, not based on CLayoutClient, completely. But, if you want to use some favorite widget of yours with the layout manager, simply subclass it from your original widget and CLayoutClient.



How Do You Compile It?

If you save the example above into file main.cpp, then you just have to compile it and link SPTK libraries to it. For any GUI program written with SPTK, at least the following SPTK libraries are required: sputil3, spdb3, sptk3. The GCC command line would look like this:

g++ main.cpp -lsputil3 -lspdb3 -lsptk3