Más bocaditos de Qt

Sigo de lleno con Qt, eso es parte del motivo por el que lleve algo de tiempo sin escribir. Irónicamente, no tengo mucho para contar en esta ocasión, pero aprovecho para escribir algunos “bocaditos”.

Tipografías incrustadas

Dar un toque de distinción a nuestras aplicaciones puede incluir el utilizar tipografías que no son estándares, o que sólo vienen incluidas con instalaciones de determinados programas que no podemos asumir estén siempre presentes (ejemplo típico, MS Office).

La opción más conveniente es la de incluirlas con nuestra aplicación (no hablaré de licencias ni nada parecido, asumo que todo está en orden). Para esto se pueden bien copiar junto al ejecutable, o incluir en un fichero de recursos (QRC). En cualquier de ambos casos habrá que registrar las fuentes antes de poder utilizarlas, especialmente si deseamos usarlas desde hojas de estilos.

QFontDatabase::addApplicationFont(":/fonts/arialn.ttf");
QFontDatabase::addApplicationFont(":/fonts/arialnb.ttf");
QFontDatabase::addApplicationFont(":/fonts/arialnbi.ttf");
QFontDatabase::addApplicationFont(":/fonts/arialni.ttf");

Esto podemos hacerlo en nuestra función main(), justo después de instanciar la aplicación por ejemplo. A partir de ese momento podremos utilizar las fuentes por su nombre como si se tratase de otra fuente del sistema.

Qt5 y la “plaftorms/qwindows.dll”

Desde Qt5 existe una DLL nueva de la que no está muy bien documentada su importancia en el proceso de distribución de la aplicación. Se trata de la platforms/qwindows.dll. Junto a las DLL base, como Qt5Core.dll, Qt5Widgets.dll, etc. El caso es que si Qt no está instalado en el ordenador destino, debemos especificar la ruta hasta esta DLL. La forma más sencilla que he encontrado hasta ahora es especificar la ruta de la carpeta de plugins (donde también deberán estar las DLL para cargas imágenes, por cierto).

En Qt4 podíamos especificar la carpeta de plugins de la siguiente forma:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QApplication::addLibraryPath(qApp->applicationDirPath() + "/qtplugins/");

    // ...
}

De esta forma tendríamos una carpeta “qtplugins” junto a nuestro ejecutable, y siempre podríamos obtener la ruta hasta ella. La diferencia ahora está en que en Qt5 debe hacerse antes de instanciar la aplicación, ya que es ahí cuando se cargará dicha DLL. El problema es que “qApp” es una macro de conveniencia a “QCoreApplication::instance()” (patrón singleton), por lo que es necesario que hayamos instanciado la clase antes de utilizarla. Esto nos lleva a que tenemos que asumir que el directorio actual sigue siendo el del ejecutable:

int main(int argc, char *argv[])
{
    QApplication::addLibraryPath("./qtplugins/");

    QApplication a(argc, argv);

    // ...
}

Esto sirve siempre y cuando no lancemos la aplicación desde un acceso directo que tenga cambiado el directorio de trabajo.

Extendiendo clases

Esto no es exclusivo de Qt, pero el ejemplo lo tomo de ahí. En Qt4, el método “tabBar()” de la clase QTabWidget era protegido (a partir de Qt5 es público). Si queríamos acceder a él teníamos que recurrir a la herencia para crear un método público tal que

class MyTabWidget : public QTabWidget {
public:
    QTabBar *tabBar()
    {
        return QTabWidget::tabBar();
    }
};

Ahora bien, una cosa que no todos los programadores de C++ sabrán, pero que verán claro como el agua después de leerlo: no es necesario crear el nuestro TabWidget utilizando el objeto MyTabWidget, podemos hacerlo con QTabWidget (por ejemplo, desde Qt Designer), y luego hacer un simple cast para acceder al método público.

((MyTabWidget*)ui.tabWidget)->tabBar()->setTabText(0, tr("Customers"));

Este “truco” podemos hacerlo con cualquier clase y con cualquier número de métodos, accediendo a atributos y métodos protegidos (nunca privados), siempre y cuando no declaremos nuevos atributos. Esto se debe a que la definición de nuevos métodos no modifica el contenido del objeto, por lo que el casting sigue siendo válido (el objeto sigue teniendo la estructura del tipo base), pero con una tabla de métodos extendida.

Objective-C tiene un mecanismo similar, aunque algo más elegante (y con una implementación diferente debido a que usa paso de mensajes en lugar de llamadas a funciones, pero eso es harina de otro costal). Este mecanismo se llama “categorías“, y consiste en agregar nuevos métodos a una clase existente. Este método puede acceder a atributos privados (en Objective-C no existen atributos protegidos, y cualquier método puede ser invocado siempre que se conozca su cabecera). Lo más destacable de este mecanismo es que no hace falta realizar un casting al objeto, sino que automáticamente la clase adquiere los métodos de las categorías a las que tiene acceso (incluidas en un fichero de cabecera previo). Pueden imaginar la de plugins que pueden hacerse con esto, y lo limpio que queda el código. El único cuidado que hay que tener es el de documentar bien el código a fin de no confundir métodos estándar de categorías propias.

Traducción de recursos

La traducción de cadenas de textos en Qt está muy documentada, pero no me pareció lo mismo al tratar de incorporar soporte multi-idioma en los recursos de las aplicaciones. Así que acá va. Imaginemos que tenemos una imagen “:/hola.png” que queremos traducir. Nuestro QRC originalmente contendrá algo tal que:

<qresource>
  <file>hola.png</file>
</qresource>

Para agregar las traducciones, debemos crear alias a dicha imagen desde varios ficheros (en el ejemplo, el castellano es el idioma por defecto):

<qresource>
  <file alias="hola.png">hola_es.png</file>
</qresource>
<qresource lang="en">
  <file alias="hola.png">hola_en.png</file>
</qresource>

A modo resumen, para traducir un texto debemos borrar el traductor anterior de la aplicación (si lo hubiera), crear un nuevo traductor desde nuestro fichero QM (fichero compilado de traducciones), instalar el nuevo traductor, re-traducir todas nuestras interfaces abiertas mediante “::retranslateUi()” (las ventanas sin abrir usarán la nueva traducción automáticamente al crearlas), y volver a generar los textos que programáticamente hayamos generado.

Con los ficheros de recursos debemos cambiar el parámetro global de localización de la aplicación antes de re-cargar los recursos. Para ello:

QLocale::setDefault(QLocale("en"));

Donde “en” es el código de localización deseado (“es”, “en”, “de”, “it”, etc).

Una sugerencia personal si queremos que el idioma cambie dinámicamente (sin tener que cerrar y volver a abrir nuestra aplicación) es crear una señal que se emita desde la configuración del idioma después de haber configurado el nuevo traductor y la localización, y que reciban todos los objetos que deban traducir sus textos o recursos.

Hasta la próxima, ¡espero disfruten de los bocaditos!