Recursos
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, volvemos con la aplicación Die
(Figura 1), ya tratada en capítulos anteriores.
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.
- 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.
1 2 3 4 5 6 |
2. Crear recursos
Si vamos al directorio fuente de la aplicación (/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:
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).
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.
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 ysrc/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.
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 |
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).
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.
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).
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, image_from_resource(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.
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 nap_desktop_app()
, 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 /bin
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
.
Los archivos creados por nrc se consideran código generado y no se guardan en la carpetasrc
sino en la carpetabuild
. 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 carpetares
) 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 nap_desktop_app()
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 nap_desktop_app()
, nap_command_app()
o nap_library()
:
NRC_NONE
: CMake ignorará el contenido de la carpetares
, 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.
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).
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).
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
.