GUI Data binding
Por GUI Data Binding entendemos el mapeo automático entre las variables del programa y los controles de la interfaz de usuario (Figura 1). De esta forma ambos estarán sincronizados sin que el programador tenga que realizar ningún trabajo extra como la captura de eventos, asignación de valores, comprobación de rangos, etc. En ¡Hola Gui Binding! tienes el código fuente completo del ejemplo que mostraremos a continuación.
1. Vinculación de tipos básicos
Partimos de una estructura de datos compuesta de varios campos de tipos básicos (Listado 1), donde no hay anidadas otras estructuras u objetos.
|
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; }; |
Lo primero que debemos hacer es registrar los campos de la estructura con dbind
(Listado 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
es un registro, dentro de la aplicación, que permite automatizar ciertas operaciones sobre los datos, así como establecer rangos, precisiones o alias. Su uso va más allá de las interfaces gráficas de usuario. Más información en Data binding.
Por otro lado, construimos un Layout que alberga los diferentes controles de la interfaz de usuario (Listado 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; } |
Ahora vincularemos las celdas de nuestro layout con los campos de la estructura (Listado 4). Presta atención a que aún no hemos creado ningún objeto del tipo BasicTypes
. Por lo tanto, se trata de un enlace semántico donde no intervienen posiciones de memoria, sino los desplazamientos (offset) de los campos dentro de la estructura de datos.
- Utiliza cell_dbind para vincular un campo con una celda individual.
- Utiliza layout_dbind para vincular una estructura con un layout.
- Utiliza layout_cell para obtener una celda de un 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); |
Al vincular una estructura de datos con layout_dbind
debemos tener presente que las celdas de dicho layout solo pueden asociarse con campos de la misma estructura. De lo contrario, obtendremos un error en tiempo de ejecución, debido a la incoherencia de datos que se produciría. Dicho de otro modo, no podemos mezclar estructuras dentro de un mismo layout.
No se pueden utilizar variables aisladas en el Data Binding. Todas deben pertenecer a un struct
ya que, internamente, se establecen las relaciones (Layout --> Estructura) y (Cell --> Campo o Variable).
Ya, por último, asociaremos un objeto de tipo BasicTypes
con el layout creado anteriormente (Listado 5).
- Utiliza layout_dbind_obj para vincular un objeto con la interfaz de usuario.
- Puedes cambiar el objeto que se está "editando" en cualquier momento, con una nueva llamada a layout_dbind_obj.
- Si pasamos
NULL
a layout_dbind_obj se deshabilitarán las celdas vinculadas con campos de la estructura.
|
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. Límites y rangos
Ten presente que la capacidad expresiva de los controles estará, en general, muy por debajo del rango de valores soportados por los tipos de datos (Listado 6). Por ejemplo, si vinculamos un uint16_t
con un RadioGroup
este último solo soportará valores entre 0
y n-1
, donde n
es el número total de radios. Los controles están preparados para manejar los valores fuera de rango de la forma más coherente posible, pero esto no exime al programador de hacer las cosas bien. En (Tabla 1) tienes un resumen de los tipos de datos y rangos soportados por los controles estándar.
|
data->uint16_val = 1678; cell_dbind(layout_cell(layout, 1, 4), BasicTypes, uint16_t, uint16_val); |
3. Estructuras anidadas
Veamos ahora un modelo de datos algo más complicado, que incluye estructuras anidadas además de los tipos básicos (Figura 2). En este caso contamos con una estructura denominada StructTypes
que contiene instancias de otra estructura denominada Vector
(Listado 7). El código fuente completo de este segundo ejemplo lo tienes en ¡Hola 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); |
Empezamos con la misma metodología que empleamos con el primer ejemplo. Creamos un layout y lo vinculamos con la estructura Vector
(Listado 8). Esto no presenta problemas, al estar compuesta exclusivamente por tipos básicos 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; } |
La idea ahora es utilizar esta función para crear Sub-layouts y asociarlos a celdas de un layout de más alto nivel, que pueda soportar objetos de tipo StructTypes
(Listado 9). Los sub-layout de tipo Vector
se vinculan con los campos { Vector vec1, Vector *pvec1, ... }
utilizando cell_dbind, de forma similar a como lo hicimos con los tipos básicos.
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; } |
Y ya, por último, solo nos queda vincular objetos de tipo StructTypes
con el layout principal (Listado 10). DBind detectará los sub-layouts de tipo Vector
y asociará de forma automática los sub-objetos (por valor o por puntero) correspondientes. Por tanto, solo será necesaria una llamada a layout_dbind_obj (la del objeto principal).
|
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); |
En resumen:
- Para cada sub-estructura creamos un sub-layout, vinculando los campos a nivel local.
- Las celdas que contienen estos sub-layouts se vincularán con la estructura principal.
- Asignamos al layout principal el objeto a editar.
4. Notificaciones y campos calculados
Si aplicamos lo visto en las secciones anteriores, la sincronización entre datos e interfaz se realiza en estas dos situaciones:
- Cuando el programa llama a layout_dbind_obj. En ese momento la interfaz reflejará el estado del objeto.
- Cuando el usuario manipula cualquier control, momento en que se actualizará valor del objeto.
No obstante, es posible que el programa deba ser notificado cuando el usuario modifique el objeto, con el fin de realizar ciertas acciones (actualizar dibujos, guardar datos en ficheros, lanzar algoritmos de cálculo, etc). Esto se resolverá por medio de eventos, como se refleja en (Figura 3). Por otro lado, el programa puede alterar los valores de ciertos campos del objeto y deberá notificar los cambios a la interfaz (layout) para que se mantenga actualizada.
- Utiliza layout_dbind para incluir un
listener
que notifique los cambios a la aplicación. - Utiliza evbind_object para obtener, dentro del
callback
, el objeto que se está editando. - Utiliza event_sender para obtener, dentro del
callback
, el layout que ha mandado la notificación. - Utiliza evbind_modify para saber, dentro del
callback
, si un campo del objeto ha cambiado o no. - Utiliza layout_dbind_update para notificar al layout que un campo del objeto ha sido modificado por la aplicación.
Todo esto puede verse reflejado en (Listado 11). Cada vez que el usuario cambie cualquier valor de StructTypes
se lanzará una notificación de tipo ekGUI_EVENT_OBJCHANGE que comprobará si el campo vec1
ha cambiado. En caso afirmativo, se recalculará su longitud y se actualizarán los controles GUI asociados con dicha variable.
|
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); |
Si, por algún motivo, el valor modificado no es admisible por la aplicación, se puede revertir devolviendo FALSE
como resultado del evento (Listado 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; } } } |