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! .. figure:: concord.jpg :width: 80% 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 [#]_... ¿Qué hay que hacer ahora? .. [#] 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 [#]_. .. [#] 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! .. figure:: radio-14.print.jpg :width: 40% Funciona, pero tiene problemas. Esa ventana tiene *muchos* problemas. .. figure:: radio-15.print.jpg :width: 40% 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: .. figure:: radio-18.print.jpg :width: 40% 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: .. figure:: radio-17.print.jpg :width: 40% 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: .. figure:: radio-19.print.jpg :width: 40% 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: .. figure:: radio-16.print.jpg :width: 40% 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 [#]_ .. [#] 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) [#]_ .. [#] 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: .. figure:: radio-20.print.jpg :width: 54% 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: .. figure:: radio-21.print.jpg :width: 54% ¡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. .. raw:: pdf PageBreak La parte interesante (no mucho) del código es esta: .. class:: titulo-listado radio6.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/6/radio6.py :start-after: # XXX1 :end-before: # XXX2 ¿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"? .. figure:: filenew.pdf :width: 25% 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. .. figure:: radio-22.print.jpg :width: 54% ¡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: .. figure:: radio-23.print.jpg :width: 54% "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. .. figure:: radio-24.print.jpg :width: 53% 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. .. raw:: pdf PageBreak .. class:: titulo-listado radio7.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/6/radio7.py :start-after: #XXX11 :end-before: #XXX12 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 [#]_ .. [#] 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: .. class:: titulo-listado radio_es.ts .. class:: listado .. code-block:: xml :linenos: :linenos_offset: :include: codigo/6/radio_es.ts :end-at: 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: .. figure:: linguist-1.print.jpg :width: 43% 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. .. figure:: linguist-2.print.jpg :width: 43% 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: .. class:: titulo-listado radio7.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/6/radio7.py :start-at: # Cargamos las traducciones de la aplicación :end-at: # Fin de carga de traducciones Y nuestra aplicación está traducida: .. figure:: linguist-3.print.jpg :width: 53% ¡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í: .. code-block:: python item = QtGui.QTreeWidgetItem([nombre,"Edit","Remove"]) Hay que hacer así: .. code-block:: python 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: .. class:: titulo-listado Makefile .. class:: listado .. code-block:: makefile :linenos: :linenos_offset: :include: codigo/6/Makefile 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 [#]_. .. [#] 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: .. class:: titulo-listado radio9.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/6/radio9.py :start-after: # XXX9 :end-before: # XXX3 .. figure:: radio-25.print.jpg :width: 64% Musica tranqui.