Die
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.
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.
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); |
De igual forma que hicimos en Formato del Layout hemos establecido ciertos márgenes y un ancho fijo para la columna de los controles.
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).
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.
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.
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.
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.
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 |
window_create(ekWINDOW_STDRES, &panel); |
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.
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.
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.
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
|
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.
7. El programa Die completo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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) |
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 |
/* 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_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)); 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, resid_image(USA_PNG)); popup_add_elem(popup1, TEXT_SPANISH, resid_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; } } |
1 2 3 4 5 6 7 8 |
/* Die Gui */ #include "die.hxx" __EXTERN_C Window *dgui_window(App *app); __END_C |