Cross-platform C SDK logo

Cross-platform C SDK

GUI Data binding

❮ Back
Next ❯

By GUI Data Binding we mean automatic mapping between program variables and user interface controls (Figure 1). In this way both will be synchronized without the programmer having to do any extra work such as capturing events, assigning values, checking ranges, etc. In Hello Gui Binding! you have the complete source code of the example that we will show below.

User interface in Windows with data synchronized with controls.
Figure 1: Automatic data synchronization with the user interface.

1. Basic type binding

We start from a data structure composed of several basic types fields (Listing 1), where no other structures or objects are nested.

Listing 1: Simple data model.
 
typedef struct _basictypes_t BasicTypes;

typedef enum _myenum_t
{
    ekRED,
    ekBLUE,
    ekGREEN,
    ekBLACK,
    ekMAGENTA,
    ekCYAN,
    ekYELLOW,
    ekWHITE
} myenum_t;

struct _basictypes_t
{
    bool_t bool_val;
    uint16_t uint16_val;
    real32_t real32_val;
    myenum_t enum_val;
    gui_state_t enum3_val;
    String *str_val;
};

The first thing we must do is register the fields of the structure with dbind (Listing 2):

Listing 2: Register in dbind de los campos de la estructura.
 
dbind_enum(gui_state_t, ekGUI_OFF, "");
dbind_enum(gui_state_t, ekGUI_ON, "");
dbind_enum(gui_state_t, ekGUI_MIXED, "");
dbind_enum(myenum_t, ekRED, "Red");
dbind_enum(myenum_t, ekBLUE, "Blue");
dbind_enum(myenum_t, ekGREEN, "Green");
dbind_enum(myenum_t, ekBLACK, "Black");
dbind_enum(myenum_t, ekMAGENTA, "Magenta");
dbind_enum(myenum_t, ekCYAN, "Cyan");
dbind_enum(myenum_t, ekYELLOW, "Yellow");
dbind_enum(myenum_t, ekWHITE, "While");
dbind(BasicTypes, bool_t, bool_val);
dbind(BasicTypes, uint16_t, uint16_val);
dbind(BasicTypes, real32_t, real32_val);
dbind(BasicTypes, gui_state_t, enum3_val);
dbind(BasicTypes, myenum_t, enum_val);
dbind(BasicTypes, String*, str_val);
dbind_range(BasicTypes, real32_t, real32_val, -50, 50);
dbind_increment(BasicTypes, real32_t, real32_val, 5);
DBind is a registry, within the application, that allows automating certain operations on the data, as well as establishing ranges, precisions or aliases. Its use goes beyond graphical user interfaces. More information in Data binding.

On the other hand, we build a Layout that hosts the different controls of the user interface (Listing 3):

Listing 3: Interface controls organized in a layout (Figure 1).
 
static Layout *i_layout(void)
{
    Layout *layout = layout_create(3, 9);
    Label *label = label_create();
    Edit *edit = edit_create();
    Button *check = button_check();
    Button *check3 = button_check3();
    Layout *radios = i_radio_layout();
    PopUp *popup = popup_create();
    ListBox *listbox = listbox_create();
    Slider *slider = slider_create();
    UpDown *updown = updown_create();
    layout_label(layout, label, 1, 0);
    layout_edit(layout, edit, 1, 1);
    layout_button(layout, check, 1, 2);
    layout_button(layout, check3, 1, 3);
    layout_layout(layout, radios, 1, 4);
    layout_popup(layout, popup, 1, 5);
    layout_listbox(layout, listbox, 1, 6);
    layout_slider(layout, slider, 1, 7);
    layout_updown(layout, updown, 1, 8);
    layout_halign(layout, 1, 0, ekJUSTIFY);
    layout_halign(layout, 1, 8, ekLEFT);
    return layout;
}

Now we will link the cells of our layout with the fields of the structure (Listing 4). Pay attention that we have not yet created any object of type BasicTypes. Therefore, it is a semantic link where memory positions do not intervene, but the displacements (offset) of the fields within the data structure.

When linking a data structure with layout_dbind we must bear in mind that the cells of said layout can only be associated with fields of the same structure. Otherwise, we will get a run-time error, due to the data inconsistency that would occur. In other words, we cannot mix structures within the same layout.

Isolated variables cannot be used in Data Binding. They must all belong to a struct since, internally, the relations (Layout --> Struct) and (Cell --> Field or Variable) are established.

Finally, we will associate an object of type BasicTypes with the layout created previously (Listing 5).

  • Use layout_dbind_obj to bind an object to the user interface.
  • Listing 5: Binding an object to the interface.
     
    
    BasicTypes *data = heap_new(BasicTypes);
    data->bool_val = TRUE;
    data->uint16_val = 4;
    data->real32_val = 15.5f;
    data->enum3_val = ekGUI_MIXED;
    data->enum_val = ekCYAN;
    data->str_val = str_c("Text String");
    layout_dbind_obj(layout, data, BasicTypes);
    
  • You can change the object being "edited" at any time, with a new call to layout_dbind_obj.
  • If we pass NULL to layout_dbind_obj the cells linked to fields of the structure will be disabled.

2. Limits and ranges

Keep in mind that the expressiveness of controls will, generally, be well below the range of values ​​supported by data types (Listing 6). For example, if we link a uint16_t with a RadioGroup the latter will only support values ​​between 0 and n-1, where n is the total number of radios. The controls are set up to handle out-of-range values ​​as consistently as possible, but this does not exempt the programmer from getting it right. In (Table 1) you have a summary of the data types and ranges supported by the standard controls.

Listing 6: Value not representable in the RadioGroup of (Figure 1).
 
data->uint16_val = 1678;
cell_dbind(layout_cell(layout, 1, 4), BasicTypes, uint16_t, uint16_val);
Table 1: Data types and ranges of GUI controls.
Control Data Type
Label String, Number, Enum
Edit String, Number
Button (CheckBox) Boolean
Button (CheckBox3) Enum (3 values), Integer (0,1,2)
RadioGroup Enum, Integer (0,1,2...n-1)
PopUp Enum, Integer (0,1,2...n-1)
ListBox Enum, Integer (0,1,2...n-1)
Slider Number (min..max)
UpDown Enum, Number

3. Nested structures

Let's now look at a somewhat more complicated data model, which includes nested structures in addition to the basic types (Figure 2). In this case we have a structure called StructTypes that contains instances of another structure called Vector (Listing 7). You can find the complete source code for this second example at Hello Struct Binding!.

User interface in Windows with data synchronized with controls.
Figure 2: Data binding with substructures.
Listing 7: Data model with nested structures and registry in dbind.
 
typedef struct _vector_t Vector;
typedef struct _structtypes_t StructTypes;

struct _vector_t
{
    real32_t x;
    real32_t y;
    real32_t z;
};

struct _structtypes_t
{
    String *name;
    Vector vec1;
    Vector vec2;
    Vector vec3;
    Vector *pvec1;
    Vector *pvec2;
    Vector *pvec3;
    real32_t length1;
    real32_t length2;
    real32_t length3;
    real32_t length4;
    real32_t length5;
    real32_t length6;
};

dbind(Vector, real32_t, x);
dbind(Vector, real32_t, y);
dbind(Vector, real32_t, z);
dbind(StructTypes, String*, name);
dbind(StructTypes, Vector, vec1);
dbind(StructTypes, Vector, vec2);
dbind(StructTypes, Vector, vec3);
dbind(StructTypes, Vector*, pvec1);
dbind(StructTypes, Vector*, pvec2);
dbind(StructTypes, Vector*, pvec3);
dbind(StructTypes, real32_t, length1);
dbind(StructTypes, real32_t, length2);
dbind(StructTypes, real32_t, length3);
dbind(StructTypes, real32_t, length4);
dbind(StructTypes, real32_t, length5);
dbind(StructTypes, real32_t, length6);
dbind_range(Vector, real32_t, x, -5, 5);
dbind_range(Vector, real32_t, y, -5, 5);
dbind_range(Vector, real32_t, z, -5, 5);
dbind_increment(Vector, real32_t, x, .1f);
dbind_increment(Vector, real32_t, y, .1f);
dbind_increment(Vector, real32_t, z, .1f);

We started with the same methodology that we used with the first example. We create a layout and link it with the Vector structure (Listing 8). This does not present problems, as it is composed exclusively of basic types real32_t.

Listing 8: Layout for editing objects of type Vector.
 
static Layout *i_vector_layout(void)
{
    Layout *layout = layout_create(3, 3);
    Label *label1 = label_create();
    Label *label2 = label_create();
    Label *label3 = label_create();
    Edit *edit1 = edit_create();
    Edit *edit2 = edit_create();
    Edit *edit3 = edit_create();
    UpDown *updown1 = updown_create();
    UpDown *updown2 = updown_create();
    UpDown *updown3 = updown_create();
    label_text(label1, "X:");
    label_text(label2, "Y:");
    label_text(label3, "Z:");
    edit_align(edit1, ekRIGHT);
    edit_align(edit2, ekRIGHT);
    edit_align(edit3, ekRIGHT);
    layout_label(layout, label1, 0, 0);
    layout_label(layout, label2, 0, 1);
    layout_label(layout, label3, 0, 2);
    layout_edit(layout, edit1, 1, 0);
    layout_edit(layout, edit2, 1, 1);
    layout_edit(layout, edit3, 1, 2);
    layout_updown(layout, updown1, 2, 0);
    layout_updown(layout, updown2, 2, 1);
    layout_updown(layout, updown3, 2, 2);
    cell_dbind(layout_cell(layout, 1, 0), Vector, real32_t, x);
    cell_dbind(layout_cell(layout, 1, 1), Vector, real32_t, y);
    cell_dbind(layout_cell(layout, 1, 2), Vector, real32_t, z);
    cell_dbind(layout_cell(layout, 2, 0), Vector, real32_t, x);
    cell_dbind(layout_cell(layout, 2, 1), Vector, real32_t, y);
    cell_dbind(layout_cell(layout, 2, 2), Vector, real32_t, z);
    layout_dbind(layout, NULL, Vector);
    return layout;
}

The idea now is to use this function to create Sub-layouts and associate them to cells of a higher level layout, which can support objects of type StructTypes (Listing 9). Sub-layouts of type Vector are linked to the fields {Vector vec1, Vector * pvec1, ...} using cell_dbind, so similar to how we did it with the basic types.

Listing 9: Layout that supports objects of type StructTypes.
 
static Layout *i_struct_types_layout(void)
{
    Layout *layout1 = i_create_layout();
    Layout *layout2 = i_vector_layout();
    Layout *layout3 = i_vector_layout();
    Layout *layout4 = i_vector_layout();
    Layout *layout5 = i_vector_layout();
    Layout *layout6 = i_vector_layout();
    Layout *layout7 = i_vector_layout();
    Label *label1 = label_create();
    Label *label2 = label_create();
    Label *label3 = label_create();
    layout_layout(layout1, layout2, 0, 0);
    layout_layout(layout1, layout3, 1, 0);
    layout_layout(layout1, layout4, 2, 0);
    layout_layout(layout1, layout5, 0, 1);
    layout_layout(layout1, layout6, 1, 1);
    layout_layout(layout1, layout7, 2, 1);
    layout_label(layout1, label1, 0, 2);
    layout_label(layout1, label2, 1, 2);
    layout_label(layout1, label3, 2, 2);
    cell_dbind(layout_cell(layout1, 0, 0), StructTypes, Vector, vec1);
    cell_dbind(layout_cell(layout1, 1, 0), StructTypes, Vector, vec2);
    cell_dbind(layout_cell(layout1, 2, 0), StructTypes, Vector, vec3);
    cell_dbind(layout_cell(layout1, 0, 1), StructTypes, Vector*, pvec1);
    cell_dbind(layout_cell(layout1, 1, 1), StructTypes, Vector*, pvec2);
    cell_dbind(layout_cell(layout1, 2, 1), StructTypes, Vector*, pvec3);
    cell_dbind(layout_cell(layout1, 0, 2), StructTypes, real32_t, length1);
    cell_dbind(layout_cell(layout1, 1, 2), StructTypes, real32_t, length2);
    cell_dbind(layout_cell(layout1, 2, 2), StructTypes, real32_t, length3);
    layout_dbind(layout1, NULL, StructTypes);
    return layout1;
}

And finally, we only have to link objects of type StructTypes with the main layout (Listing 10). DBind will detect sub-layouts of type Vector and will automatically associate the corresponding sub-objects (by value or by pointer). Therefore, only one call to layout_dbind_obj will be necessary (the one of the main object).

Listing 10: Associate object and sub-objects to a layout.
 
StructTypes *data = heap_new(StructTypes);
Layout *layout = i_struct_types_layout();
data->name = str_c("Generic Object");
data->pvec1 = heap_new(Vector);
data->pvec2 = heap_new(Vector);
data->pvec3 = heap_new(Vector);
data->vec1 = i_vec_init(1.2f, 2.1f, -3.4f);
data->vec2 = i_vec_init(-0.2f, 1.8f, 2.3f);
data->vec3 = i_vec_init(-3.2f, 4.9f, -4.7f);
*data->pvec1 = i_vec_init(0.9f, 7.9f, -2.0f);
*data->pvec2 = i_vec_init(-6.9f, 2.2f, 8.6f);
*data->pvec3 = i_vec_init(3.9f, -5.5f, 0.3f);
data->length1 = i_vec_length(&data->vec1);
data->length2 = i_vec_length(&data->vec2);
data->length3 = i_vec_length(&data->vec3);
data->length4 = i_vec_length(data->pvec1);
data->length5 = i_vec_length(data->pvec2);
data->length6 = i_vec_length(data->pvec3);

layout_dbind_obj(layout, data, StructTypes);

In summary:

  • For each sub-structure we create a sub-layout, linking the fields locally.
  • The cells that contain these sub-layouts will be linked to the main structure.
  • We assign the object to edit to the main layout.

4. Notifications and calculated fields

If we apply what was seen in the previous sections, the synchronization between data and interface is carried out in these two situations:

  • When the program calls layout_dbind_obj. At that time the interface will reflect the state of the object.
  • When the user manipulates any control, then the object's value will be updated.

However, it is possible that the program must be notified when the user modifies the object, in order to carry out certain actions (update drawings, save data in files, launch calculus algorithms, etc.). This will be resolved by events, as reflected in (Figure 3). On the other hand, the program can alter the values ​​of certain fields of the object and must notify the changes to the interface (layout) so that it remains updated.

Windows interface with synchronized data on controls.
Figure 3: Notification of value change to main program.
  • Use layout_dbind to include a listener that notifies changes to the application.
  • Use evbind_object to obtain, within the callback, the object that is being edited.
  • Use event_sender to obtain, within the callback, the layout that sent the notification.
  • Use evbind_modify to know, inside the callback, if a field of the object has changed or not.
  • Use layout_dbind_update to notify the layout that a field of the object has been modified by the application.

All of this can be seen in (Listing 11). Every time the user changes any StructTypes value, a notification of type ekGUI_EVENT_OBJCHANGE will be launched that will check if the vec1 field has changed. If so, its length will be recalculated and the GUI controls associated with that variable will be updated.

Listing 11: Notification of object values ​​modification.
 
static void i_OnDataChange(App *app, Event *e)
{
    StructTypes *data = evbind_object(e, StructTypes);
    Layout *layout = event_sender(e, Layout);
    cassert(event_type(e) == ekGUI_EVENT_OBJCHANGE);

    if (evbind_modify(e, StructTypes, Vector, vec1) == TRUE)
    {
        app_update_drawing(app);
        data->length1 = i_vec_length(&data->vec1);
        layout_dbind_update(layout, StructTypes, real32_t, length1);
    }
}

layout_dbind(layout, listener(app, i_OnDataChange, App), StructTypes);

If, for some reason, the modified value is not allowed by the application, it can be reverted by returning FALSE as a result of the event (Listing 12).

Listing 12: Canceling changes made by the user.
 
static void i_OnDataChange(App *app, Event *e)
{
    StructTypes *data = evbind_object(e, StructTypes);
    Layout *layout = event_sender(e, Layout);

    if (evbind_modify(e, StructTypes, Vector, vec1) == TRUE)
    {
        real32_t length = i_vec_length(&data->vec1);
        if (length < 5.f)
        {
            app_update_drawing(app);
            data->length1 = length;
            layout_dbind_update(layout, StructTypes, real32_t, length1);
        }
        else
        {
            // This will REVERT the changes in 'vec1' variable
            bool_t *res = event_result(e, bool_t);
            *res = FALSE;
        }
    }
}
❮ Back
Next ❯