Diseño de Interfaz Gráfica

“¿Cómo se hace una estatua de un elefante? Empezás con un bloque de mármol y sacás todo lo que no parece un elefante.”

—Anónimo.

“Abandonen la esperanza del valor añadido a través de la rareza. Es mejor usar técnicas de interacción consistentes que le den a los usuarios el poder de enfocarse en tu contenido, en vez de preguntarse como se llega a él.”

—Jakob Nielsen

¿Siendo un programador, qué sabe uno de diseños de interfaces? La respuesta, al menos en mi caso es poco y nada. Sin embargo, hay unos cuantos principios que ayudan a que uno no cree interfaces demasiado horribles, o a veces hasta agradables.

  • Aprender de otros.

    Estamos rodeados de ejemplos de buenas y malas interfaces. Copiar es bueno.

  • Contenerse.

    Tenemos una tendencia natural a crear cabinas de Concord. No te digo que no está buena la cabina de un Concord, lo que te digo es que para hacer tostadas es demasiado.

    En general, dado que uno no tiene la habilidad (en principio) de crear asombrosas interfaces, lo mejor es crear lo menos posible. ¡Lo que no está ahí no puede estar tan mal!

_images/concord.jpg

Concord cockpit by wynner3, licencia CC-BY-NC (http://www.flickr.com/photos/wynner3/3805698150/)

  • Pensar mucho antes.

    Siempre es más fácil agregar y mantener un feature bien pensado, con una interfaz limitada, que tratar de hacer que funcione una pila de cosas a medio definir.

    Si no sabés exactamente cómo funciona tu aplicación, no estás listo para hacer una interfaz usable para ella. Sí podés hacer una de prueba.

  • Tirá una.

    Hacé una interfaz mientras estás empezando. Después tirála. Si hiciste una clara separación de capas eso debería ser posible.

  • Pedí ayuda.

    Si tenés la posibilidad de que te de una mano un experto en usabilidad, usála. Sí, ya sé que vos podés crear una interfaz que funcione, eso es lo fácil, lo difícil es crear una interfaz que alguien quiera usar.

Más allá de esos criterios, en este capítulo vamos a tomar la interfaz creada en el capítulo anterior y la vamos a rehacer, pero bien. Porque esa era la de desarrollo, y la vamos a tirar.

Proyecto

Asumamos que la aplicación de streaming de radio que desarrollamos en el capítulo anterior funciona correctamente y carece de bugs [1]... ¿Qué hay que hacer ahora?

[1]No es así, pero estoy escuchando música con ella ¡En este mismo momento!

Bueno, falta resolver todas las cosas que no son bugs desde el punto de vista de funcionamiento pero que están mal.

Corrigiendo la Interfaz Gráfica

Empecemos con la ventana de configuración, viendo algunos problemas de base en el diseño. Desde ya que el 90% de lo que veamos ahora es discutible. Es más, como no soy un experto en el tema, es probable que el 90% esté equivocado. Sin embargo, hasta que consiga un experto en UI que le pegue una revisada... es lo que hay [2].

[2]De hecho, pedí ayuda en twitter/identi.ca y mi blog y salieron unas cuantas respuestas, incluyendo un post en otro blog. ¡Con mockups y todo!
radio-14.print.jpg

Funciona, pero tiene problemas.

Esa ventana tiene muchos problemas.

radio-15.print.jpg

Botón “Close” no alineado.

Normalmente no vas a ver este caso cubierto en las guías de diseño de interfaz porque estamos usando un layout “columna de botones” que no es de lo más standard.

Si hubiera más de un botón abajo, entonces tal vez “Close” se vería como perteneciente a ese elemento visual, sin embargo, al estar solo, se lo ve como un elemento de la columna, aunque “destacado” por la separación vertical.

Al ser “absorbido” visualmente por esa columna, queda muy raro que no tenga el mismo ancho que los otros botones.

Como no debemos asignar anchos fijos a los botones (por motivos que vamos a ver más adelante) debemos solucionarlo usando layout managers.

Una manera de resolverlo es una matriz 2x2 con un grid layout:

radio-18.print.jpg

Botón “Close” alineado.

El resultado final es bastante más armónico, y divide visualmente el diálogo en dos componentes claros, la lista a la izquierda, los controles a la derecha.

Lo que nos lleva al segundo problema:

radio-17.print.jpg

Espacio muerto.

Si el layout es “dos columnas” entonces no tiene sentido que la lista termine antes del fondo del diálogo. Nuevamente, si hubiera dos botones abajo (por ejemplo, “Accept” y “Reject”), entonces sí tendría sentido extender ese componente visual hacia la izquierda.

Al tener sólo uno, ese espacio vacío es innecesario y antifuncional.

Entonces cambiamos el esquema de layouts, y terminamos con un layout horizontal de dos elementos, el derecho un layout vertical conteniendo todos los botones:

radio-19.print.jpg

Resultado con layout horizontal.

El siguiente problema es que al tener iconos y texto, y al estar centrado el contenido de los botones, se ve horrible:

radio-16.print.jpg

Etiquetas centradas con iconos a la izquierda.

Hay varias soluciones para esto:

  • Podemos no poner iconos: El texto centrado no molesta tanto visualmente.
  • Podemos no centrar el contenido de los botones: Se ve mejor, pero es muy poco standard [3]
[3]Ver la cita de Nielsen al principio del capítulo.
  • Podemos no poner texto en el botón sino en un tooltip: Funciona, es standard, resuelve el alineamiento, hace la interfaz levemente menos obvia.
  • Mover algunos elementos inline en cada item (los que afectan a un único item) y mover los demás a una línea horizontal por debajo de la lista.

O ... podemos dejar de ponerle lapiz de labios al chancho y admitir que es un chancho.

El problema de este diálogo no es que los botones estén desalineados, es que no sabemos siquiera porqué los botones están.

Así que, teniendo una interfaz que funciona, hagamos un desarrollo racional de la versión nueva, y olvidemos la vieja.

¿Qué estamos haciendo?

Pensemos el objetivo, la tarea a realizar. Es controlar una lista de radios. Lo mínimo sería esto:

  • Agregar radios nuevas (Add).
  • Cambiar algo en una radio ya existente (Edit).
  • Sacar radios que no nos gustan más (Delete).
  • Cerrar el diálogo (Close) [4]
[4]Podríamos tener “Apply”, “Cancel”, etc, pero me gusta más la idea de este diálogo como de aplicación instantánea, “aplicar cambios” es un concepto nerd. La manipulación directa es la metáfora moderna. Bah, es una opinión.

Adicionalmente teníamos esto:

  • Cambiar el orden de las radios en la lista

¿Pero... porqué estaba? En nuestro caso es porque nos robamos la interfaz de RadioTray, pero... ¿alguien necesita hacerlo? ¿Porqué?

Veamos las justificaciones que se me ocurren:

  1. Poner las radios más usadas al principio.

    Pero... ¿No sería mejor si el programa mostrara las últimas radios usadas al principio en forma automática?

  2. Organizarlas por tipo de radio (ejemplo: tener todas las de música country juntas)

    Para hacer esto correctamente, creo que sería mejor tener múltiples niveles de menúes. También podríamos agregarle a cada radio un campo “género” o tags, y usar eso para clasificarlas.

En ambos casos, me parece que el ordenamiento manual no es la manera correcta de resolver el problema. Es casi lo contrario de un feature. Es un anti-feature que sólo sirve para que a los que realmente querrían un feature determinado se les pueda decir “usá los botones de ordenar”.

Si existe algún modelo de uso para el que mover las radios usando flechitas es el modo de interacción correcta... no se me ocurre y perdón desde ya.

Por lo tanto, este “feature” va a desaparecer por ahora.

Si no tenemos los botones de subir y bajar, no tiene tanto sentido la idea de una columna de botones a la derecha, y podemos pasar a un layout con botones horizontales:

radio-20.print.jpg

Repensando el diálogo. Ya que estamos “Done” es más adecuado para el botón que “Close”.

¿En qué se parecen y en qué se diferencian esos cuatro botones que tenemos ahí abajo?

  • Edit y Remove afectan a una radio que esté seleccionada.
  • Add y Done no dependen de la selección en la lista.

¿Que pasaría si pusiéramos Edit y Remove en los items mismos? Bueno, lo primero que pasaría es que tendríamos que cambiar código porque el QListWidget soporta una sola columna y tenemos que pasar a un QTreeWidget. Veamos como funciona en la GUI:

radio-21.print.jpg

¡Less is more!

También al no tener más botones de Edit y Remove, hay que mover un poco el código porque ahora responde a otras señales.

La parte interesante (no mucho) del código es esta:

titulo-listado

radio6.py

class listado

¿Es esto todo lo que está mal? Vaya que no.

Pulido

Los iconos que venimos usando son del set “Reinhardt” que a mí personalmente me gusta mucho, pero algunos de sus iconos no son exactamente obvios. ¿Por ejemplo, esto te dice “Agregar”?

filenew.pdf

Bueno, en cierta forma sí, pero está pensado para documentos. Sería mejor por ejemplo un signo +. De la misma forma, si bien la X funciona como “remove”, si usamos un + para “Add”, es mejor un - para “Remove”.

Y para “Edit” es mejor usar un lápiz y no un destornillador. El problema ahí es usar el mismo icono que para “Configure”. Si bien ambos casos son acciones relacionadas, son lo suficientemente distintas para merecer su propio icono.

radio-22.print.jpg

¡Shiny!

¿Quiere decir que este diálogo ya está terminado? No, en absoluto.

Nombres y Descripciones

En algunos sistemas operativos tu ventana va a tener un botón extra, generalmente un signo de pregunta. Eso activa el “What’s This?” o “¿Qué es esto?” y tambien se lo accede con un atajo de teclado (muchas veces Shift+F1).

Luego, al hacer click en un elemento de la interfaz, se ve un tooltip extendido con información detallada acerca del mismo. Esta información es útil como ayuda online.

Es sencillo agregarlo usando designer, y si lo hacemos se ve de esta forma:

radio-23.print.jpg

“What’s This?” de la lista de radios.

Los programas deberían ser accesibles para personas con problemas de visión, por lo cual es importante ocuparse de todo lo que sea teconologías asistivas. En Qt, eso quiere decir por lo menos completar los campos accessibleName y accessibleDescription de todos los widgets con los que el usuario pueda interactuar.

radio-24.print.jpg

Datos de accesibilidad.

Uso Desde el Teclado

Es importante que una aplicación no obligue al uso del mouse a menos que sea absolutamente indispensable. La única manera de hacer eso que conozco es... usándola completa sin tocar el mouse.

Probar esta aplicación en su estado actual muestra varias partes que fallan esa prueba.

  • En el diálogo de agregar radios no es obvio como usar los botones “Add” y “Cancel” porque no tienen atajo de teclado asignado.

    Eso es fácil de arreglar con Designer, y se hizo en addradio2.ui. De ahora en más utilizaremos la aplicación radio7.py que usa ese archivo.

  • En el diálogo de configuración no hay manera de editar o eliminar radios sin usar el mouse.

    Esto es bastante más complicado, porque involucra varias partes del diseño, y podría hasta ser suficiente para hacernos repensar la idea del “Edit/Remove” dentro de la lista. Veamos qué podemos hacer al respecto.

El primer problema es que la lista de radios está configurada para no aceptar selección, con lo que no hay manera de elegir un item. Eso lo cambiamos en designer, poniendo la propiedad selectionMode en SingleSelection.

Con eso, será posible seleccionar una radio. Luego, debemos permitir que se apliquen acciones a la misma. Una manera es habilitar atajos de teclado para Edit y Remove, por ejemplo “Ctrl+E” y “Delete”.

La forma más sencilla es crear dos acciones (clase QAction) con esos atajos y hacer que hagan lo correcto.

titulo-listado

radio7.py

class listado

Traducciones

Uno no hace aplicaciones para uno mismo, o aún si las hace, está bueno si las pueden usar otros. Y está muy bueno si la puede usar gente de otros países. Y para eso es fundamental que puedan tenerla en su propio idioma [5]

[5]Yo personalmente es rarísimo que use las aplicaciones traducidas, pero para otros es necesario.

Esta parte es una de esas que dependen mucho de como sea lo que se está programando. Vamos a hacer un ejemplo con las herramientas de Qt, para otros desarrolos hay cosas parecidas.

Hay varios pasos, extracción de strings, traducción, y compilación de los strings generados a un formato usable.

A fin de poder traducir lo que un programa dice, necesitamos saber exactamente qué dice. Las herramientas de extracción de strings se encargan de buscar todas esas cosas en nuestro código y ponerlas en un archivo para que podamos trabajar con ellas.

En la versión actual de nuestro programa, tenemos los siguientes archivos:

  • radio7.py (nuestro programa principal)
  • plsparser.py (parser de archivos .pls, no tiene interfaz)
  • addradio2.ui (diálogo de agregar una radio)
  • radio3.ui (diálogo de configuración)

¡Extraigamos esos strings! Este comando crea un archivo radio.ts con todo lo traducible de esos archivos, para crear una traducción al castellano:

[codigo/6]$ pylupdate4 radio7.py plsparser.py addradio2.ui \
    radio3.ui -ts radio_es.ts

Los archivos .ts son un XML bastante obvio. Este es un ejemplo de una traducción al castellano:

titulo-listado

radio_es.ts

class listado

Otras herramientas crean archivos en otros formatos, más o menos fáciles de editar a mano, y/o proveen herramientas para editarlos.

¿Ahora, como editamos la traducción? Usando Linguist, que viene con Qt. Lo primero que hará es preguntarnos a qué idioma queremos traducir:

linguist-1.print.jpg

Diálogo inicial de Linguist

Linguist es muy interesante porque te muestra cómo queda la interfaz con la traducción mientras lo estás traduciendo (por lo menos para los archivos .ui), lo que permite apreciar si estamos haciendo macanas.

linguist-2.print.jpg

Linguist en acción

Entonces uno tradujo todo lo mejor que pudo, ¿cómo hacemos que la aplicación use nuestra traducción? Por suerte es muy standard. Primero, creamos un archivo “release” de la traducción, con extensión .qm, donde compilamos a un formato más eficiente:

[codigo/6]$ lrelease radio_es.ts -compress -qm radio_es.qm
Updating 'radio_es.qm'...
Generated 15 translation(s) (15 finished and 0 unfinished)

Del lado del código, debemos decirle a nuestra aplicación donde está el archivo .qm. Asumiendo que está junto con el script principal:

titulo-listado

radio7.py

class listado

Y nuestra aplicación está traducida:

linguist-3.print.jpg

¡Traducida! ... ¿Traducida?

Nos olvidamos que no todo nuestro texto visible (y traducible) viene de designer. Hay partes que están escritas en el código python, y hay que marcarlas como traducibles, para que pylupdate4 las agregue al archivo .ts.

Eso se hace pasando los strings a traducir por el método tr de la aplicación o del widget del que forman parte. Por ejemplo, en vez de hacer así:

item = QtGui.QTreeWidgetItem([nombre,"Edit","Remove"])

Hay que hacer así:

item = QtGui.QTreeWidgetItem([nombre,self.tr("Edit"),
    self.tr("Remove")])

Esta operación hay que repetirla en cada lugar donde queden strings sin traducir. Por ese motivo... ¡hay que marcar para traducción desde el principio!

Como esto modifica fragmentos de código por todas partes, vamos a crear una nueva versión del programa, radio8.py.

Al agregar nuevos strings que necesitan traducción, es necesario actualizar el archivo .ts:

[codigo/6]$ pylupdate4 -verbose radio8.py plsparser.py addradio2.ui\
    radio3.ui -ts radio_es.ts
Updating 'radio_es.ts'...
Found 24 source texts (9 new and 15 already existing)

Y, luego de traducir con linguist, recompilar el .qm:

[codigo/6]$ lrelease radio_es.ts -compress -qm radio_es.qm
Updating 'radio_es.qm'...
Generated 24 translation(s) (24 finished and 0 unfinished)

Como todo este proceso es muy engorroso, puede ser práctico crear un Makefile o algún otro mecanismo de automatización de la actualización y compilación de traducciones. Por ejemplo, con este Makefile un make traducciones se encarga de todo:

titulo-listado

Makefile

class listado

Feedback

En este momento, cuando el usuario elige una radio que desea escuchar, suena. ¿Pero qué está sonando? ¿Cuál radio está escuchando? ¿Que tema están pasando en este momento? Deberíamos brindar esa información, si el usuario la desea, de manera lo menos molesta posible.

En este caso puntual, lo que queremos es el “metadata” del objeto reproductor, y un mecanismo posible para mostrar esa información es un OSD (On Screen Display) o usar una de las APIs de notificación del sistema [6].

[6]Hay pros y contras para cada una de las formas de mostrar notificaciones. Voy a hacer una que tal vez no es óptima, pero que funciona en todas las plataformas.

En cuanto a qué notificar, es sencillo, cada vez que nuestro reproductor de audio emita la señal metaDataChanged tenemos que ver el resultado de metaData() y ahí está todo.

También es importante que se pueda ver qué radio se está escuchando en este momento. Eso lo vamos a hacer mediante una marca junto al nombre de la radio actual.

Ya que estamos, tiene más sentido que “Quit” esté en el menú principal (el del botón izquierdo) que en el secundario, así que lo movemos.

Ah, y implementamos que “Turn Off Radio” solo aparezca si hay una radio en uso (y hacemos que funcione).

Para que quede claro qué modificamos, creamos una nueva versión de nuestro programa, radio9.py, y esta es la parte interesante:

titulo-listado

radio9.py

class listado
radio-25.print.jpg

Musica tranqui.