SDK Multiplataforma en C logo

SDK Multiplataforma en C

Die

Siguiente ❯

El buen código debería ser simple, claro y fácil de entender. El buen código debería ser compacto: solo lo necesario para hacer el trabajo y nada más, pero no críptico, hasta el punto de que no se pueda entender. El buen código debería ser genérico, para que pueda resolver una amplia clase de problemas de manera uniforme. Podría describirse como elegante, muestra de buen gusto y refinamiento. Brian Kernighan


Como el camino se demuestra andando, vamos a dedicar unos capítulos a profundizar en el uso de NAppGUI de la mano de aplicaciones reales. Nuestro objetivo es presentar programas de cierto nivel, a medio camino entre los sencillos "ejemplos de libro" y las aplicaciones comerciales. En esta primera demo tenemos un programa que nos permite dibujar la silueta de un dado (Figura 1) y que nos servirá como excusa para introducir conceptos de dibujo paramétrico, composición de layouts y uso de recursos. El código fuente está en la caperta /src/demo/die de la distribución del SDK. En Crear nueva aplicación y Recursos vimos como crear el proyecto desde cero.

Captura de la aplicación Die en Windows.
Figura 1: Aplicación Die Simulator, versión Windows. Inspirado en DieView (Cocoa Programming for OSX, Hillegass et al.)
Captura de la aplicación Die en macOS.
Figura 2: Versión macOS.
Captura de la aplicación Die en Linux.
Figura 3: Versión Linux/GTK+.

1. Uso de sublayouts

Comenzamos trabajando en la interfaz de usuario, que hemos dividido en dos áreas: una vista personalizada (View) donde dibujaremos la representación del dado en 2D, y una zona de controles donde podremos interactuar con dicho dibujo. Como ya vimos en ¡Hola Mundo! utilizaremos objetos Layout para ubicar los controles dentro de la ventana principal. No obstante observamos que esta disposición de elementos no encaja bien en una única tabla, por lo tanto, usaremos dos celdas en horizontal como contenedor principal y un grid de dos columnas y seis filas para los controles (Listado 1) (Listado 1). Este segundo layout se ubicará en la celda derecha del primer contenedor y diremos que es un sublayout del layout principal.

Listado 1: Composición mediante sublayouts.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Layout *layout = layout_create(2, 1);
Layout *layout1 = layout_create(2, 6);
layout_view(layout, view, 0, 0);
layout_label(layout1, label1, 0, 0);
layout_label(layout1, label2, 0, 1);
layout_label(layout1, label3, 0, 2);
layout_label(layout1, label4, 0, 3);
layout_label(layout1, label5, 0, 4);
layout_view(layout1, vimg, 0, 5);
layout_popup(layout1, popup1, 1, 0);
layout_popup(layout1, popup2, 1, 1);
layout_slider(layout1, slider1, 1, 2);
layout_slider(layout1, slider2, 1, 3);
layout_slider(layout1, slider3, 1, 4);
layout_label(layout1, label6, 1, 5);
layout_layout(layout, layout1, 1, 0);
Captura que muestra la organización de los controles utilizando Layouts.
Figura 4: El uso de sublayouts añade flexibilidad a la hora de diseñar el gui.

De igual forma que hicimos en Formato del Layout hemos establecido ciertos márgenes y un ancho fijo para la columna de los controles.

Listado 2: Formato del layout
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
view_size(view, s2df(200.f, 200.f));
layout_margin(layout, 10.f);
layout_hsize(layout1, 1, 150.f);
layout_hmargin(layout, 0, 10.f);
layout_hmargin(layout1, 0, 5.f);
layout_vmargin(layout1, 0, 5.f);
layout_vmargin(layout1, 1, 5.f);
layout_vmargin(layout1, 2, 5.f);
layout_vmargin(layout1, 3, 5.f);
layout_vmargin(layout1, 4, 5.f);

2. Uso de vistas personalizadas

View son controles que nos permitirán diseñar nuestros propios widgets. Al contrario que ocurre con otro tipo de componentes, como Slider o Button, aquí tendremos total libertad para dibujar cualquier cosa. Podremos interactuar con el control capturando sus eventos (ratón, teclado, etc) e implementando los manejadores adecuados. Estas vistas se integran en el layout como cualquier otro componente (Listado 3).

Listado 3: Creación de una vista personalizada.
1
2
3
View *view = view_create();
view_size(view, s2df(200.f, 200.f));
layout_view(layout, view, 0, 0);

No podemos dibujar dentro de un View cuando queramos. Tendremos que realizar una solicitud al sistema operativo mediante el método view_update (Listado 4), ya que el área de dibujo puede afectar a ventanas superpuestas y esto debe gestionarse de forma centralizada. Cuando el control esté listo para refrescarse, el sistema enviará un evento EvDraw que debemos capturar a través de view_OnDraw.

Listado 4: Código básico de refresco del View.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static void i_OnPadding(App *app, Event *e)
{
    const EvSlider *params = event_params(e, EvSlider);
    app->padding = params->pos;
    view_update(app->view);
}

static void i_OnDraw(App *app, Event *e)
{
    const EvDraw *params = event_params(e, EvDraw);
    die_draw(params->context, params->width, params->height, app);
}

slider_OnMoved(slider1, listener(app, i_OnPadding, App));
view_OnDraw(view, listener(app, i_OnDraw, App));

Cada vez que el usuario mueve un slider (parámetro padding, por ejemplo) el sistema operativo captura la acción e informa a la aplicación a través del método i_OnPadding (Figura 5). Como la acción implica un cambio en el dibujo, este método llama a view_update para informar de nuevo al sistema que la vista debe actualizarse. Cuando este lo considera apropiado, manda el evento EvDraw, que es capturado por i_OnDraw donde se regenera el dibujo con los nuevos parámetros.

Esquema que muestra como funcionan los eventos en sistemas de escritorio.
Figura 5: Comprendiendo el flujo de eventos en dibujos interactivos.

3. Dibujo paramétrico

Bajo este concepto se describe la capacidad para generar imágenes vectoriales a partir de unos pocos valores numéricos conocidos como parámetros (Figura 6). Se usa bastante en el diseño asistido por computadora (CAD), ya permite realizar ajustes de forma sencilla en planos o modelos sin tener que editar, una por una, un sinfín de primitivas.

Ejemplo de dibujo paramétrico.
Figura 6: Principios del dibujo paramétrico, aplicados en Die.

En nuestra aplicación, la representación del dado puede cambiar en tiempo de ejecución a medida que el usuario manipula los sliders o dimensiona la ventana, por lo que calculamos la posición y el tamaño de sus primitivas utilizando fórmulas paramétricas. Una vez resueltas, creamos el dibujo con tres simples comandos del API Primitivas de dibujo.

  • draw_clear. Borra toda el área de dibujo utilizando un color sólido.
  • draw_rndrect. Dibuja un rectángulo con bordes redondeados.
  • draw_circle. Dibuja un círculo.
Listado 5: demo/casino/ddraw.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/* Die drawing */

#include "ddraw.h"
#include <draw2d/draw2dall.h>

/*---------------------------------------------------------------------------*/

static const real32_t i_MAX_PADDING = 0.2f;
const real32_t kDEF_PADDING = .15f;
const real32_t kDEF_CORNER = .15f;
const real32_t kDEF_RADIUS = .35f;

/*---------------------------------------------------------------------------*/

void die_draw(DCtx *ctx, const real32_t x, const real32_t y, const real32_t width, const real32_t height, const real32_t padding, const real32_t corner, const real32_t radius, const uint32_t face)
{
    color_t white = color_rgb(255, 255, 255);
    color_t black = color_rgb(0, 0, 0);
    real32_t dsize, dx, dy;
    real32_t rc, rr;
    real32_t p1, p2, p3;

    dsize = width < height ? width : height;
    dsize -= bmath_floorf(2.f * dsize * padding * i_MAX_PADDING);
    dx = x + .5f * (width - dsize);
    dy = y + .5f * (height - dsize);
    rc = dsize * (.1f + .3f * corner);
    rr = dsize * (.05f + .1f * radius);
    p1 = 0.5f * dsize;
    p2 = 0.2f * dsize;
    p3 = 0.8f * dsize;

    draw_fill_color(ctx, white);
    draw_rndrect(ctx, ekFILL, dx, dy, dsize, dsize, rc);
    draw_fill_color(ctx, black);

    if (face == 1 || face == 3 || face == 5)
        draw_circle(ctx, ekFILL, dx + p1, dy + p1, rr);

    if (face != 1)
    {
        draw_circle(ctx, ekFILL, dx + p3, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p2, dy + p3, rr);
    }

    if (face == 4 || face == 5 || face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p3, rr);
    }

    if (face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p1, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p1, rr);
    }
}

Los comandos de dibujo se plasman sobre un lienzo (canvas), también conocido como contexto DCtx. Este objeto llega a i_OnDraw como parámetro del evento EvDraw. En este caso, el lienzo lo proporciona el propio control View, pero también es posible crear contextos para dibujar directamente en memoria.


4. Redimensionado

En esta aplicación la ventana puede cambiar de tamaño estirando el cursor sobre sus bordes, algo habitual en programas de escritorio. Vamos a ver algunos aspectos básicos sobre esta característica no presente en ¡Hola Mundo!, que presentaba una ventana estática. Lo primero es habilitar la opción dentro del constructor de la ventana.

1

Cuando una ventana cambia de tamaño, los controles interiores deben hacerlo proporcionalmente así como cambiar su ubicación dentro del panel. Esta gestión se lleva a cabo dentro de cada objeto Layout. Cuando se inicia la ventana, el tamaño por defecto de cada layout se calcula aplicando el dimensionamiento natural, que es el resultante del tamaño inicial de los controles más los márgenes, como ya vimos en Formato del Layout. Cuando estiramos o contraemos la ventana, la diferencia de píxeles entre el dimensionamiento natural y el real se distribuye entre las columnas del layout (Figura 7). Lo mismo ocurre con la diferencia vertical, que se distribuye entre sus filas. Si una celda contiene un sublayout, este incremento se distribuirá recursivamente por sus propias columnas y filas.

Muestra como se reparte entre los controles el exceso al tamaño al redimensionar la ventana.
Figura 7: Al redimensionar, el exceso de píxeles se distribuye proporcionalmente por las filas y columnas del Layout.

Pero en este caso particular, queremos que todo el incremento vaya al área de dibujo (columna 0). Dicho de otro modo, buscamos que la columna de los controles permanezca con ancho fijo y no crezca (Figura 8). Para ello debemos cambiar la proporción del redimensionado:

1
layout_hexpand(layout, 0);

Con esta función el 100% del excedente horizontal irá a la columna 0. Por defecto, teníamos una proporción del (50%, 50%) ya que son dos columnas (33% para tres, 25% para cuatro, etc). Con esto ya tendríamos resuelto el redimensionado para la dimensión X de la ventana, pero ¿qué ocurre con la vertical?. En el layout principal, solo tenemos una fila que, al expandirse, cambiará la altura de la vista personalizada. Pero esta expansión también afectará a la celda de la derecha, donde los controles también crecerán verticalmente debido al aumento recursivo de píxeles en las filas del sublayout. Para resolverlo, forzamos la alineación vertical ekTOP en la celda derecha del layout.

1
layout_valign(layout, 1, 0, ekTOP);

en lugar del ekJUSTIFY, que es la alineación predeterminada para sublayouts. De esta manera, el contenido de la celda (el sublayout completo) no se expandirá verticalmente, si no que se ajustará al borde superior dejando todo el espacio libre en la parte inferior de la celda. Obviamente, si usamos ekCENTER o ekBOTTOM, el sublayout se centrará o ajustará al borde inferior.

Muestra como solo se redimensiona la vista de dibujo, el resto no.
Figura 8: Jugando con la proporción horizontal y la alineación vertical, solo el área de dibujo se verá afectada por los cambios de tamaño.

5. Uso de recursos

Tanto los texto como los iconos que hemos utilizado en Die se han externalizado en el paquete de recursos all. Gracias a ello, podemos realizar una traducción automática de la interfaz entre los idiomas Inglés y Español. Puedes consultar Recursos para obtener información detallada de como se han asignado los textos e imágenes en la interfaz del programa.

Listado 6: demo/die/res/res_die/strings.msg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Die strings */
TEXT_FACE       Face
TEXT_PADDING    Padding
TEXT_CORNER     Corner
TEXT_RADIUS     Radius
TEXT_ONE        One
TEXT_TWO        Two
TEXT_THREE      Three
TEXT_FOUR       Four
TEXT_FIVE       Five
TEXT_SIX        Six
TEXT_TITLE      Die Simulator
TEXT_INFO       Move the sliders to change the parametric representation of the die face.
TEXT_LANG       Language
TEXT_ENGLISH    English
TEXT_SPANISH    Spanish
Listado 7: demo/die/res/res_die/es_es/strings.msg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Die strings */
TEXT_FACE       Cara
TEXT_PADDING    Margen
TEXT_CORNER     Borde
TEXT_RADIUS     Radio
TEXT_ONE        Uno
TEXT_TWO        Dos
TEXT_THREE      Tres
TEXT_FOUR       Cuatro
TEXT_FIVE       Cinco
TEXT_SIX        Seis
TEXT_TITLE      Simulador de dado
TEXT_INFO       Mueve los sliders para cambiar la representación paramétrica de la cara del dado.
TEXT_LANG       Idioma
TEXT_ENGLISH    Inglés
TEXT_SPANISH    Español

6. Die y Dice

Esta aplicación se ha utilizado como hilo conductor del capítulo Crear nueva aplicación y siguientes del tutorial de NAppGUI. El ejemplo completo consta de dos aplicaciones (Die y Dice), así como de la librería casino que agrupa las rutinas comunes para ambos programas (Figura 9). Los tres proyectos completos los tienes listos para compilar y probar en la carpeta src/demo de la distribución del SDK.

Relación entre dos aplicaciones y sus dependencias comunes.
Figura 9: Las rutinas comunes para ambas aplicaciones se comparten mediante la librería casino.

7. El programa Die completo

Listado 8: demo/die/die.hxx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* Die Types */

#ifndef __DIE_HXX__
#define __DIE_HXX__

#include <gui/gui.hxx>

typedef struct _app_t App;

struct _app_t
{
    real32_t padding;
    real32_t corner;
    real32_t radius;
    uint32_t face;
    View *view;
    Window *window;
};

#endif
Listado 9: demo/die/main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* Die application */

#include "dgui.h"
#include <nappgui.h>

/*---------------------------------------------------------------------------*/

static void i_OnClose(App *app, Event *e)
{
    osapp_finish();
    unref(app);
    unref(e);
}

/*---------------------------------------------------------------------------*/

static App *i_create(void)
{
    App *app = heap_new0(App);
    app->padding = 0.2f;
    app->corner = 0.1f;
    app->radius = 0.5f;
    app->face = 5;
    app->window = dgui_window(app);
    window_origin(app->window, v2df(200.f, 200.f));
    window_OnClose(app->window, listener(app, i_OnClose, App));
    window_show(app->window);
    return app;
}

/*---------------------------------------------------------------------------*/

static void i_destroy(App **app)
{
    window_destroy(&(*app)->window);
    heap_delete(app, App);
}

/*---------------------------------------------------------------------------*/

#include "osmain.h"
osmain(i_create, i_destroy, "", App)
Listado 10: demo/die/dgui.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* Die Gui */

#include "dgui.h"
#include "ddraw.h"
#include "res_die.h"
#include <gui/guiall.h>

/*---------------------------------------------------------------------------*/

static void i_OnDraw(App *app, Event *e)
{
    color_t green = color_rgb(102, 153, 26);
    const EvDraw *params = event_params(e, EvDraw);
    draw_clear(params->ctx, green);
    die_draw(params->ctx, 0, 0, params->width, params->height, app->padding, app->corner, app->radius, app->face);
}

/*---------------------------------------------------------------------------*/

static void i_OnAcceptFocus(App *app, Event *e)
{
    bool_t *r = event_result(e, bool_t);
    unref(app);
    *r = FALSE;
}

/*---------------------------------------------------------------------------*/

static void i_OnFace(App *app, Event *e)
{
    const EvButton *params = event_params(e, EvButton);
    app->face = params->index + 1;
    view_update(app->view);
}

/*---------------------------------------------------------------------------*/

static void i_OnPadding(App *app, Event *e)
{
    const EvSlider *params = event_params(e, EvSlider);
    app->padding = params->pos;
    view_update(app->view);
}

/*---------------------------------------------------------------------------*/

static void i_OnCorner(App *app, Event *e)
{
    const EvSlider *params = event_params(e, EvSlider);
    app->corner = params->pos;
    view_update(app->view);
}

/*---------------------------------------------------------------------------*/

static void i_OnRadius(App *app, Event *e)
{
    const EvSlider *params = event_params(e, EvSlider);
    app->radius = params->pos;
    view_update(app->view);
}

/*---------------------------------------------------------------------------*/

static void i_OnLang(App *app, Event *e)
{
    const EvButton *params = event_params(e, EvButton);
    const char_t *lang = params->index == 0 ? "en_us" : "es_es";
    gui_language(lang);
    unref(app);
}

/*---------------------------------------------------------------------------*/

 static Panel *i_panel(App *app)
 {
    Panel *panel = panel_create();
    Layout *layout = layout_create(2, 1);
    Layout *layout1 = layout_create(2, 6);
    View *view = view_create();
    Label *label1 = label_create();
    Label *label2 = label_create();
    Label *label3 = label_create();
    Label *label4 = label_create();
    Label *label5 = label_create();
    Label *label6 = label_multiline();
    PopUp *popup1 = popup_create();
    PopUp *popup2 = popup_create();
    Slider *slider1 = slider_create();
    Slider *slider2 = slider_create();
    Slider *slider3 = slider_create();
    ImageView *img = imageview_create();
    app->view = view;
    view_size(view, s2df(200, 200));
    view_OnDraw(view, listener(app, i_OnDraw, App));
    view_OnAcceptFocus(view, listener(app, i_OnAcceptFocus, App));
    label_text(label1, TEXT_LANG);
    label_text(label2, TEXT_FACE);
    label_text(label3, TEXT_PADDING);
    label_text(label4, TEXT_CORNER);
    label_text(label5, TEXT_RADIUS);
    label_text(label6, TEXT_INFO);
    popup_add_elem(popup1, TEXT_ENGLISH, gui_image(USA_PNG));
    popup_add_elem(popup1, TEXT_SPANISH, gui_image(SPAIN_PNG));
    popup_OnSelect(popup1, listener(app, i_OnLang, App));
    popup_add_elem(popup2, TEXT_ONE, NULL);
    popup_add_elem(popup2, TEXT_TWO, NULL);
    popup_add_elem(popup2, TEXT_THREE, NULL);
    popup_add_elem(popup2, TEXT_FOUR, NULL);
    popup_add_elem(popup2, TEXT_FIVE, NULL);
    popup_add_elem(popup2, TEXT_SIX, NULL);
    popup_OnSelect(popup2, listener(app, i_OnFace, App));
    popup_selected(popup2, app->face - 1);
    slider_value(slider1, app->padding);
    slider_value(slider2, app->corner);
    slider_value(slider3, app->radius);
    slider_OnMoved(slider1, listener(app, i_OnPadding, App));
    slider_OnMoved(slider2, listener(app, i_OnCorner, App));
    slider_OnMoved(slider3, listener(app, i_OnRadius, App));
    imageview_image(img, (const Image*)CARDS_PNG);
    layout_view(layout, view, 0, 0);
    layout_label(layout1, label1, 0, 0);
    layout_label(layout1, label2, 0, 1);
    layout_label(layout1, label3, 0, 2);
    layout_label(layout1, label4, 0, 3);
    layout_label(layout1, label5, 0, 4);
    layout_imageview(layout1, img, 0, 5);
    layout_popup(layout1, popup1, 1, 0);
    layout_popup(layout1, popup2, 1, 1);
    layout_slider(layout1, slider1, 1, 2);
    layout_slider(layout1, slider2, 1, 3);
    layout_slider(layout1, slider3, 1, 4);
    layout_label(layout1, label6, 1, 5);
    layout_layout(layout, layout1, 1, 0);
    layout_margin(layout, 10);
    layout_hsize(layout1, 1, 150);
    layout_hmargin(layout, 0, 10);
    layout_hmargin(layout1, 0, 5);
    layout_vmargin(layout1, 0, 5);
    layout_vmargin(layout1, 1, 5);
    layout_vmargin(layout1, 2, 5);
    layout_vmargin(layout1, 3, 5);
    layout_vmargin(layout1, 4, 5);
    layout_hexpand(layout, 0);
    layout_valign(layout, 1, 0, ekTOP);
    panel_layout(panel, layout);
    return panel;
}

/*---------------------------------------------------------------------------*/

Window *dgui_window(App *app)
{
    gui_respack(res_die_respack);
    gui_language("");

    {
        Panel *panel = i_panel(app);
        Window *window = window_create(ekWINDOW_STDRES);
        window_panel(window, panel);
        window_title(window, TEXT_TITLE);
        return window;
    }
}
Listado 11: demo/die/dgui.h
1
2
3
4
5
6
7
8
/* Die Gui */

#include "die.hxx"

__EXTERN_C

Window *dgui_window(App *app);

__END_C
Siguiente ❯