SDK Multiplataforma en C logo

SDK Multiplataforma en C

Recursos

❮ Anterior

Si internacionalizamos todo, terminamos con reglas que sofocan la libertad y la innovación. Myron Scholes


Los recursos son datos necesarios para la aplicación pero que no residen en el área del ejecutable. En otras palabras, no son accesibles directamente mediante las variables del programa, sino que hay que realizar una carga previa para poderlos utilizar. Los más habituales son los textos e imágenes utilizadas en la interfaz de usuario, aunque cualquier tipo de archivo puede convertirse en un recurso (sonidos, tipografías, modelos 3d, páginas html, etc). Para ilustrar su uso con un ejemplo real, vamos a utilizar la aplicación Die (Figura 1), incluida en /src/demo/die.

Captura de la aplicación de ejemplo Die.
Figura 1: Aplicación Die.

1. Tipos de recursos

  • Textos: Si bien es muy sencillo incluir textos en el código como variables de C, en la práctica esto no es aconsejable por dos motivos: El primero es que, normalmente, no son los programadores los que redactan los mensajes que muestra el programa. Separándolos en un archivo a parte, otros miembros del equipo pueden revisarlos y editarlos sin tener que acceder directamente al código. La segunda razón es la internacionalización. Es requisito casi indispensable a día de hoy poder cambiar el idioma del programa y esto puede involucrar a varios miembros del equipo, así como el hecho de que varias cadenas de texto hagan referencia al mismo mensaje. Por tanto, extraerlos del código fuente será casi indispensable.
  • Imágenes: No es habitual que los iconos del programa cambien en función del idioma, aunque pueda darse el caso. Lo complicado aquí es transformar un archivo .jpg o .png en una variable de C (Listado 1). Hay que serializar el archivo y pegarlo en el código, algo muy tedioso y difícil de mantener para el programador. Es preferible tener las imágenes en una carpeta separada y acceder a ellas en tiempo de ejecución.
  • Listado 1: Imagen .png incrustada en el código fuente.
    1
    2
    3
    4
    5
    6
    
    const uint32_t IMG_SIZE = 1262;
    
    const byte_t IMG[] = {
                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
                0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
                ... };
    
  • Archivos: Al margen de texto e imágenes, cualquier archivo puede convertirse en un recurso. En este caso, la aplicación recibirá un bloque de bytes con el contenido del mismo, que deberá saber interpretar.

2. Crear recursos

Si vamos al directorio fuente de la aplicación (/src/demo/die), vemos que hay una carpeta llamada /res añadida por CMake al crear el proyecto. Dentro hay varios archivos logo.* con el Icono de aplicación.

También puedes ver una carpeta llamada /res/res_die que no fué creada por CMake, sino añadida posteriormente al escribir el programa. Esta subcarpeta se considera un paquete de recursos y contendrá un conjunto de textos, imágenes o archivos que serán cargados "en bloque" en algún momento de la ejecución. Podemos crear tantos paquetes como sean necesarios en función del tamaño y lógica de nuestro programa.

En aplicaciones grandes, organiza tus recursos de tal forma que no sea necesario cargarlos todos al arrancar la aplicación. Es posible que ciertos recursos solo sean necesarios cuando el usuario realice alguna acción.

Verás que dentro de /res/res_die existe un strings.msg cuyo contenido mostramos a continuación:

Listado 2: Fichero de mensajes de Die.
 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

También contiene la imagen cards.png y los iconos spain.png y usa.png (Figura 2).

Captura del explorador de Windows mostrando el directorio de recursos de la aplicación.
Figura 2: Paquete de recursos en src/die/res/res_die.

Cada línea dentro del archivo strings.msg define un nuevo mensaje que consta de un identificador (p.e. TEXT_FACE) seguido del texto que se mostrará en el programa (Face en este caso). Se considera texto desde el primer carácter no blanco después del identificador hasta el final de la línea. No es necesario ponerlo entre comillas ("Face") como ocurre en C:

 
BILLY   Billy "the Kid" was an American Old West outlaw.
OTHER   Other text.

Tampoco hay que utilizar secuencias de escape ('\\', '\'', ...), con la única excepción de '\n' para mensajes multilínea:

 
TWO_LINES   This is the first line\nAnd this is the second.

El identificador del mensaje sigue las reglas de los identificadores de C, exceptuando que las letras deben estar en mayúscula:

 
_ID1    Ok
0ID2    Wrong!!
id3     Wrong!!
ID3     Ok

Los mensajes aceptan cualquier carácter Unicode. Podemos dividir los textos en tantos archivos *.msg como sea necesario y deben ser almacenados en formato UTF8.

Visual Studio no guarda por defecto los archivos en UTF8. Asegurate de hacerlo en cada *.msg que contenga caracteres no US-ASCII. File->Save As->Save with encoding-> Unicode (UTF8 Without Signature) - Codepage 65001.

3. Internacionalización (i18n)

Hemos utilizado el Inglés como idioma principal en el programa, pero queremos que también esté traducido al Español. Para ello volvemos a la carpeta /res/res_die, donde vemos el subdirectorio /es_es que contiene otro archivo strings.msg. Los identificadores en dicho archivo son los mismos que en /res_die/strings.msg pero los textos están en otro idioma. Dependiendo del lenguaje seleccionado, el programa utilizará una versión u otra.

Listado 3: Fichero de mensajes de Die, traducido al Español.
 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

Debemos tener en cuenta unas sencillas reglas a la hora de localizar recursos:

  • Si no existe la versión local de un recurso, se utilizará la versión global del mismo. CMake avisará en el caso que existan textos sin traducir Avisos de nrc.
  • Se ignorarán aquellos recursos solo presentes en carpetas locales. Es imperativo que exista la versión global de cada uno.
  • No se permiten "subpaquetes" de recursos. Solo se procesarán dos niveles: src/res/packname para los globales y src/res/packname/local para los localizados.
  • Los paquetes de recursos deberán tener un nombre único dentro de la solución. Una estrategia puede ser anteponer el nombre del proyecto: /appname_pack1, libname_pack2, etc.
  • Se ignorarán los recursos existentes en la carpeta raíz (/res). Todos los recursos deben estar incluidos en un paquete /res/pack1/, /res/pack2/, etc.
  • Los textos localizados deben tener el mismo identificador que su correspondiente global. De lo contrario se consideran mensajes diferentes.
  • Para crear la versión localizada de una imagen u otro archivo, incluirlo en su correspondiente carpeta local (p.e. /res/res_die/es_es/cards.png) utilizando el mismo nombre de fichero que la versión global.
  • Para nombrar las carpetas localizadas, utilizar el código de idioma de dos letras ISO 639-1 (en, es, fr, de, zh, ...) y, opcionalmente, el código de país de dos letras ISO-3166 (en_us, en_gb, ...).

4. Traducción en ejecución

Para cada paquete de recursos, CMake crea un *.h con el mismo nombre que la carpeta: res_die.h en este caso (Listado 4). Este archivo contiene los identificadores de recursos, así como una función que nos permite acceder a los mismos res_die_respack(). En (Listado 5) vemos las acciones a realizar para utilizar dichos recursos en nuestro programa.

Listado 4: Fichero de cabecera res_die.h.
 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
/* Automatic generated by NAppGUI Resource Compiler (nrc-r1490) */

#include "core.hxx"

__EXTERN_C

/* Messages */
extern ResId TEXT_FACE;
extern ResId TEXT_PADDING;
extern ResId TEXT_CORNER;
extern ResId TEXT_RADIUS;
extern ResId TEXT_ONE;
extern ResId TEXT_TWO;
extern ResId TEXT_THREE;
extern ResId TEXT_FOUR;
extern ResId TEXT_FIVE;
extern ResId TEXT_SIX;
extern ResId TEXT_TITLE;
extern ResId TEXT_INFO;
extern ResId TEXT_LANG;
extern ResId TEXT_ENGLISH;
extern ResId TEXT_SPANISH;

/* Files */
extern ResId CARDS_PNG;
extern ResId SPAIN_PNG;
extern ResId USA_PNG;

ResPack *res_die_respack(const char_t *local);

__END_C
Listado 5: Carga y uso de recursos.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include "res_die.h"

gui_respack(res_die_respack);
gui_language("");
...
label_text(label1, TEXT_FACE);
imageview_image(vimg, CARDS_PNG);
...
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);
}
  • La línea 1 incluye la cabecera del paquete de recursos (Listado 4), que ha sido generada automáticamente por CMake.
  • La línea 3 registra el paquete en Gui, la librería encargada de la interfaz gráfica. Si la aplicación tuviese más paquetes de recursos los añadiríamos de la misma forma.
  • La línea 4 establece el lenguaje por defecto (Inglés).
  • Las líneas 6 y 7 asignan un texto y una imagen a dos controles respectivamente. Los identificadores están definidos en "res_die.h", como acabamos de ver.
  • La línea 13 traduce toda la interfaz como respuesta a un cambio en el control PopUp (Figura 3).
  • Animación de la aplicación Die, traduciendo la interfaz en tiempo de ejecución.
    Figura 3: Traducción de la aplicación Die, sin destruir la ventana ni reiniciar.

Básicamente, una llamada a gui_language, implica coordinar tres acciones:

  • Cargar los recursos localizados y sustituirlos por los actuales.
  • Asignar los nuevos textos e imágenes a todos los controles y menús del programa.
  • Volver a dimensionar las ventanas y los menús, ya que el cambio de textos e imágenes influirá en el tamaño de los controles.

5. Editar recursos

Para añadir nuevos archivos de recursos o eliminar alguno de los existentes, tan solo debemos ir a la carpeta res/res_die mediante el explorador de archivos y hacerlo allí directamente. Los archivos de mensajes *.msg se pueden editar desde Visual Studio, ya que CMake los incluye dentro del IDE (Figura 4). Tras realizar cualquier cambio en la carpeta de recursos o editar un archivo *.msg, debemos volver a lanzar CMake para que estas modificaciones se vuelvan a integrar en el proyecto. Tras cada actualización, se crearán los identificadores de los nuevos recursos y se eliminarán aquellos cuyo recurso asociado haya desaparecido, lo que producirá errores de compilación que facilitarán la corrección del código.

Captura de Visual Studio editando un archivo de mensajes.
Figura 4: Edición de recursos dentro de Visual Studio.

6. Gestión manual

Aunque lo habitual será delegar la gestión de recursos en la librería gui, es posible acceder al contenido de los paquetes directamente, como vemos en (Listado 6).

Listado 6: Acceso directo a los recursos.
1
2
3
4
5
6
7
8
#include "res_die.h"

ResPack *pack = res_die_respack("es_es");
...
label_text(label1, respack_text(pack, TEXT_FACE));
imageview_image(vimg, respack_image(pack, CARDS_PNG));
...
respack_destroy(&pack);
  • La línea 1 incluye la cabecera del paquete de recursos.
  • La línea 3 crea un objeto con el contenido del paquete en idioma Español. Cada paquete de recursos proporcionará su propio constructor, cuyo nombre comenzará por el de su carpeta xxxx_respack().
  • Las líneas 5 y 6 obtienen un texto y una imagen respectivamente para asignarlos a los controles de interfaz.
  • La línea 8 destruye el paquete de recursos, al finalizar su uso.

Hay una gran diferencia entre asignar recursos mediante ResId o mediante funciones respack_ (Listado 7). En el primer caso, el control label será "sensible" a los cambios de idioma realizados por gui_language. Sin embargo, en los casos 2 y 3 se ha asignado un texto constante al control, que no se verá afectado por esta función. Seremos los responsables de cambiar el texto, llegado el caso.

Listado 7: Diferentes modos de acceso a recursos.
1
2
3
label_text(label1, TEXT_FACE);
label_text(label1, respack_text(pack, TEXT_FACE));
label_text(label1, "Face");

La elección de uno u otro modo de acceso dependerá de los requisitos del programa. Recordamos que para poder llevar a cabo las traducciones automáticas, los recursos deben registrarse con gui_respack.


7. Procesamiento de recursos

Vamos a ver con un poco más de detalle como NAppGUI genera los módulos de recursos. Al establecer NRC_EMBEDDED en el comando desktopApp(), le indicamos a CMake que debe procesar los recursos del proyecto Die. También podemos elegir la opción NRC_PACKED de la que hablaremos a continuación. Cuando lanzamos CMake se recorren las subcarpetas dentro del directorio res de cada proyecto, llamando a la utilidad nrc (NAppGUI Resource Compiler) (Figura 5). Este programa se encuentra en la carpeta prj/scripts de la distribución del SDK. Para cada paquete de recursos, nrc crea dos archivos fuente (un .c y un .h) y los vincula con el proyecto. El .h contiene los identificadores y el constructor que hemos visto en (Listado 4). Por su parte, el .c realiza la implementación del paquete en función del contenido de cada carpeta y de la modalidad nrcMode.

Esquema que muestra como el compilador nrc procesa las carpetas de recursos.
Figura 5: Procesamiento de recursos mediante CMake y nrc.
Los archivos creados por nrc se consideran código generado y no se guardan en la carpeta src sino en la carpeta build. Se actualizarán cada vez que se ejecute CMake, independientemente de la plataforma en la que estemos trabajando. Por el contrario, los archivos originales de recursos (ubicados en la carpeta res) sí se consideran parte del código fuente.

8. Distribución de recursos

En el capítulo anterior, al crear la solución de Visual Studio, indicamos que había que utilizar la constante NRC_EMBEDDED en la sentencia desktopApp() dentro del archivo CMakeLists.txt. Existen otras dos modalidades más relacionadas con la gestión de recursos y que pueden ser configuradas por separado dentro de cada comando desktopApp():

  • NRC_NONE: CMake ignorará el contenido de la carpeta res, a excepción del icono de la aplicación. No se generarán paquetes de recursos aunque exista contenido dentro de esta carpeta.
  • NRC_EMBEDDED: Los recursos, con todas sus traducciones, se integran como parte del ejecutable (Figura 6). Es una opción muy interesante para aplicaciones de pequeño o medio tamaño, ya que en un único archivo *.exe suministraremos todo el programa. No hará falta un instalador y tendremos la certeza de que el software no fallará por la falta de algún fichero externo. El inconveniente es que, obviamente, el tamaño del ejecutable crecerá considerablemente por lo que no es aconsejable en programas con muchos recursos, muy pesados, o con multitud de traducciones.
  • NRC_PACKED: Para cada paquete de recursos, se creará un archivo *.res externo al ejecutable que será cargado y liberado en tiempo de ejecución a medida que sea necesario (Figura 7). Las ventajas de este método son las desventajas del anterior y viceversa: Ejecutables más pequeños, pero con dependencias externas (los propios .res) que deberán ser distribuidos conjuntamente. También se optimizará el uso de la memoria, al poder cargar los *.res bajo demanda.
  • Captura de la distribución macOS con recursos embebidos.
    Figura 6: Distribución de una aplicación macOS con recursos embebidos.
    Captura de la distribución macOS con recursos empaquetados.
    Figura 7: Distribución de la misma aplicación macOS con recursos empaquetados.

CMake gestiona por nosotros la ubicación de los paquetes de recursos. En aplicaciones Windows y Linux copiará todos los *.res en el directorio del ejecutable. En macOS los situará en la carpeta resources del bundle. Un hecho muy importante es que no tenemos que modificar el código fuente al pasar de una modalidad a otra. nrc ya se encarga de gestionar la carga según el tipo de paquete. Algo lógico puede ser comenzar por NRC_EMBEDDED, y si el proyecto crece, cambiar a NRC_PACKED. Tan solo deberemos volver a lanzar CMake y recompilar el proyecto para que el cambio se haga efectivo.

En Windows y Linux los archivos *.res deben instalarse siempre en el mismo directorio que el ejecutable. En el caso de macOS, CMake genera un bundle listo para distribución e instala los paquetes de recursos en el directorio /resources de dicho bundle.

9. Avisos de nrc

nrc es un script silencioso cuyo trabajo se integra en el build process de CMake, pasando desapercibido en la mayoría de ocasiones. Pero hay veces que detecta anomalías en los directorios de recursos y debe informarnos de alguna manera. En estos casos aparecerá una línea roja en la consola de CMake indicando el proyecto y paquete(s) afectado(s) (Figura 8). Los detalles los vuelca en el archivo NRCLog.txt ubicado en la carpeta de recursos generados (CMake muestra la ruta completa).

Captura de CMake mostrando anomalías detectadas en el procesamiento de recursos por parte de nrc.
Figura 8: nrc ha encontrado anomalías al procesar recursos.

Si los fallos son críticos, nrc no podrá generar los *.h y *.c asociados al paquete, impidiendo que la aplicación se pueda compilar (en esencia no deja de ser un error de compilación). Otras veces son meros warnings que convendría arreglar, pero permiten seguir compilando. Concretamente, los errores críticos que afectan a nrc son los siguientes: (los mostramos en Inglés tal y como son escritos en NRCLog.txt).

  • MsgError (%s:%d): Comment not closed (%s).
  • MsgError (%s:%d): Invalid TEXT_ID (%s).
  • MsgError (%s:%d): Unexpected end of file after string ID (%s).
  • Duplicate resource id in '%s' (%s).
  • Can't load resource file '%s'.
  • Error reading '%s' resource directory.
  • Error reading '%s' subdirectories.
  • Error creating '%s' header file.
  • Error creating '%s' source file.
  • Error creating '%s' packed file.

Por otro lado, los avisos no-críticos:

  • Empty message file '%s'.
  • Ignored localized text '%s' in '%s'. Global resource doesn't exists.
  • Ignored localized file '%s' in '%s'. Global resource doesn't exists.
  • There is no localized version of the text '%s' in '%s'.
  • Localized directory '%s' is empty or has invalid resources.

10. Icono de aplicación

Cuando creamos un nuevo proyecto, CMake establece un icono por defecto para la aplicación, que ubica en el directorio /res, con el nombre logo*. Esta imagen quedará "incrustada" en el ejecutable y el sistema operativo la utilizará para representar la aplicación en el escritorio (Figura 9). Windows y Linux también la utilizan en la barra de título de la ventana. Disponemos de tres versiones:

  • logo256.ico: Versión para Windows Vista y posteriores. Deben incluir las resoluciones: 256x256, 48x48, 32x32 y 16x16.
  • logo48.ico: Versión para Linux y VisualStudio 2008 y 2005, que no admiten resoluciones de 256x256. Esta versión solo incluye: 48x48, 32x32 y 16x16.
  • logo.icns: Versión para macOS. Resoluciones 512x512, 256x256, 128x128, 32x32 y 16x16 tanto en resolución normal (@1x) como Retina Display (@2x).
  • Captura de la barra de tareas de Windows con varios iconos de aplicación.
    Figura 9: Iconos de aplicación en la barra de tareas de Windows.

CMake ya se encarga de utilizar la versión apropiada del icono según la plataforma en la estemos compilando. Para cambiar el icono por defecto, abrir los archivos logo* con algún editor gráfico (Figura 10), realizar los cambios y volver a lanzar CMake. Muy importante: no cambiar los nombres de los archivo, siempre deben ser logo256.ico, logo48.ico y logo.icns.

Captura de un editor de imágenes, modificando el icono del programa.
Figura 10: Editando logo.ico.
❮ Anterior