GUI Data binding
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.
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.
|
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):
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):
|
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.
- Use cell_dbind to bind a field to an individual cell.
- Use layout_dbind to link a structure with a layout.
- Use layout_cell to get a cell from a Layout.
|
cell_dbind(layout_cell(layout, 1, 0), BasicTypes, String*, str_val); cell_dbind(layout_cell(layout, 1, 1), BasicTypes, String*, str_val); cell_dbind(layout_cell(layout, 1, 2), BasicTypes, bool_t, bool_val); cell_dbind(layout_cell(layout, 1, 3), BasicTypes, gui_state_t, enum3_val); cell_dbind(layout_cell(layout, 1, 4), BasicTypes, uint16_t, uint16_val); cell_dbind(layout_cell(layout, 1, 5), BasicTypes, myenum_t, enum_val); cell_dbind(layout_cell(layout, 1, 6), BasicTypes, myenum_t, enum_val); cell_dbind(layout_cell(layout, 1, 7), BasicTypes, real32_t, real32_val); cell_dbind(layout_cell(layout, 1, 8), BasicTypes, real32_t, real32_val); layout_dbind(layout, NULL, BasicTypes); |
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.
- 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.
|
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); |
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.
|
data->uint16_val = 1678; cell_dbind(layout_cell(layout, 1, 4), BasicTypes, uint16_t, uint16_val); |
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!.
|
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
.
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.
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).
|
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.
- 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.
|
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).
|
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; } } } |