SDK Multiplataforma en C logo

SDK Multiplataforma en C

GUI Data binding

❮ Anterior
Siguiente ❯

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.

Interfaz de usuario en Windows con datos sincronizados con los controles.
Figura 1: Sincronización automática de datos con la interfaz de usuario.

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.

Listado 1: Sencillo modelo de datos.
 
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):

Listado 2: Registro en 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):

Listado 3: Controles de interfaz organizados en un layout (Figura 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;
}

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.

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.
  • Listado 5: Vinculación de un objeto con la interfaz.
     
    
    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);
    
  • 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.

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.

Listado 6: Valor no representable en el RadioGroup de (Figura 1).
 
data->uint16_val = 1678;
cell_dbind(layout_cell(layout, 1, 4), BasicTypes, uint16_t, uint16_val);
Tabla 1: Tipos de datos y rangos de los controles GUI.
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. 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!.

Interfaz de usuario en Windows con datos sincronizados con los controles.
Figura 2: Vinculación de datos con sub-estructuras.
Listado 7: Modelo de datos con estructuras anidadas y registro en 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);

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.

Listado 8: Layout para editar objetos de tipo 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.

Listado 9: Layout que soporta objetos de tipo 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).

Listado 10: Asociar objeto y sub-objetos a un 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);

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.

Interfaz de Windows con datos sincronizados en los controles.
Figura 3: Notificación de cambio de valor al programa principal.
  • 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.

Listado 11: Notificación de modificación de valores del objeto.
 
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).

Listado 12: Anulando los cambios realizados por el usuario.
 
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;
        }
    }
}
❮ Anterior
Siguiente ❯