Archivo de la categoría: tutoriales

Trabajando con PyCairo y GTK

En la nueva versión de DeVeDe (que espero sacar en un par de días) he tenido que trabajar con Cairo para poder generar menús para el disco. La información la saqué de este tutorial sobre PyCairo (en inglés), en donde viene mucha información interesante. Sin embargo, faltaba un detalle importante: como mostrar el dibujo hecho en cairo en una ventana GTK.

En efecto, esto era fundamental porque el usuario tiene que poder ver el resultado final del menú antes de generar el disco (para, por ejemplo, asegurarse de que los títulos no son demasiado largos), y para eso necesito pintarlo en una ventana y no sólo volcarlo a un PNG en disco. Después de mucho buscar y de hacer varias pruebas encontré por fin como hacerlo.

Lo primero es crear un widget GtkDrawingArea, en el que mostraremos el dibujo generado con Cairo, y crear un callback para el evento expose-event. Este evento se produce cada vez que hay que redibujar la ventana (bien porque estaba tapada por otra y la hemos destapado, bien porque estaba minimizada y la hemos maximizado…). En dicho callback será donde hagamos el pintado. Para ello, lo primero que tenemos que hacer es conseguir un contexto de Cairo para dicho widget. Así, si el widget se llama MiDibujo, y arbol es el objeto Glade con nuestras ventanas, haríamos:

# asignamos el callback a nuestra funcion de repintado
w=arbol.get_widget("MiDibujo")
w.connect("expose-event",repintado)

# y definimos el callback. Tiene dos argumentos
def repintado(dwidget,evento):
    cr=dwidget.window.cairo_create()
    [funciones y métodos de Cairo para pintar]

Y ya podremos usar ese contexto para pintar y trazar todo lo que queramos.

Sin embargo, si el dibujo es estático no tiene sentido que lo tracemos una y otra vez cada vez que se produzca un evento, porque desperdiciamos tiempo de proceso. Lo mejor es tenerlo pintado desde el principio y limitarnos a copiar el bitmapa final en el callback de repintado. Para ello sólo tenemos que crear primero una superficie de Cairo y obtener un contexto para ella:

sf=cairo.ImageSurface(cairo.FORMAT_ARGB32,ResX,Resy)
cr1=cairo.Context(sf)

Ahora pintaremos nuestro dibujo en dicha superficie con ese contexto, y una vez que hayamos terminado, almacenaremos el objeto superficie de alguna manera que nos permita acceder a él desde nuestro callback (una variable global es la solución más rápida y menos elegante). Allí sólo tenemos que usar los métodos set_source_surface y paint para transferir la superficie a nuestro widget GtkDrawingArea, ahorrando mucho tiempo de proceso.

def repintado(dwidget,evento):
    cr=dwidget.window.cairo_create()
    # sf es la que pintamos en el paso anterior
    cr.set_source_surface(sf)

    # transferimos la superficie con el dibujo al Widget
    cr.paint()

Este truco nos permite también pegar cualquier imagen en la superficie en la que estamos trabajando, e incluso redimensionarla en el proceso. Un ejemplo: queremos crear una imagen de tamaño X,Y, usando como fondo un PNG de tamaño cualquiera, pero de manera que éste se expanda o encoja para ocupar exactamente la superficie de nuestra imagen final. Sólo tendríamos que hacer:

sf_base= cairo.ImageSurface.create_from_png("imagen.png")
sf_final=cairo.ImageSurface(cairo.FORMAT_ARGB32,X,Y)

# creamos un contexto para pintar
cr_final=cairo.Context(sf_final)

# cogemos el ancho y alto de la imagen.
# Importante: en coma flotante
xbase=float(sf_base.get_width())
ybase=float(sf_base.get_height())

# aplicamos el escalado para que la imagen de fondo
# pase a tener el mismo tamaño que la imagen final
cr_final.scale(float(X)/xbase,float(Y)/ybase)

# y estampamos la imagen de origen en la superficie final
cr_final.set_source_surface(sf_base)
cr_final.paint()

# podemos usar el método identity para
#restaurar la escala a 1:1
cr.identity_matrix()

Trabajando con GtkTreeView en Python

Esta semana tuve que trabajar en serio con el widget GtkTreeView en Python y se me hizo completamente cuesta arriba hasta que, después de buscar en múltiples tutoriales y pelearme con el código, me vino una epifanía y entendí su complejo pero potente esquema de funcionamiento; así que, en base a la experiencia adquirida, voy a escribir un pequeño tutorial, acompañado de algo de código para ejemplificar.

En GtkTreeView tenemos una clara división entre los datos, la presentación y el motor. Esto es lo que hace que sea un poco lioso al principio, pero también le da una potencia y versatilidad sorprendente.

Lo primero que necesitamos es añadir los módulos gtk, pygtk, gtk.glade y gobject. El último es necesario para los tipos de datos que vamos a manejar.

A continuación tenemos el widget GtkTreeView. Este elemento es el que mostrará nuestra lista de datos. Obtenemos un puntero a dicho objeto mediante GLADE:

treeview = arbol_xml.get_widget("nuestro_gtktreeview")

Ahora vamos a definir los tipos de datos que contendrá cada fila. A ésto se le denomina modelo. Es importante entender que no todos los campos del modelo tienen por qué ser visibles. En este ejemplo vamos a reservar un entero para saber, cuando el usuario marque una opción, cual es la que está escogiendo. También habrá tres cadenas, que contendrán el texto que queremos mostrar en el TreeView y el color de una de ellas.

modelo = gtk.ListStore (gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)

Existen dos tipos de modelos: ListStore y TreeStore. Ambos se manejan prácticamente igual, salvo que el segundo permite tener elementos anidados en forma de arbol. La diferencia principal se da a la hora de añadir los datos en sí, como veremos luego.

El siguiente paso es asociar el modelo al TreeView, de manera que éste sepa qué datos almacenar en cada fila:

treeview.set_model(modelo)

Hasta aquí hemos definido los tipos de datos (mediante el modelo), y tenemos el motor (el TreeView), por lo que sólo nos falta la presentación. Para ello tenemos que usar dos clases más para definir cada una de las columnas que mostraremos:

render=gtk.CellRendererText() # renderer para la primera columna
columna = gtk.TreeViewColumn ("titulo", render, text=1, background=2) # primera columna de datos
treeview.append_column (columna)

render2=gtk.CellRendererText()
columna2 = gtk.TreeViewColumn ("otro titulo", render2, text=3) # segunda columna de datos
treeview.append_column(columna2)

CellRendererText es una clase encargada de mostrar un texto en una columna de un TreeView. En el ejemplo vemos que creamos dos de ellos, uno para cada columna que vamos a mostrar. Luego, creamos una TreeViewColumn que tendrá varios parámetros: el primero es el título que se mostrará en la parte superior del TreeView para dicha columna (si estamos mostrando los nombres de personas, el título podría ser nombre, por ejemplo), y el segundo es la referencia al tipo de CellRenderer que vamos a utilizar para esa columna (en este caso, como es texto, un CellRendererText, pero hay más tipos). El resto de parámetros depende del CellRenderer que estemos utilizando. En el caso del CellRendererText tenemos, entre otros, el parámetro text, que contendrá el texto que queremos mostrar, y el parámetro background, que contiene el color del texto en formato de cadena de texto.

Vemos que en la primera columna le asociamos el valor 1 para el texto y el 2 para el color de fondo, y para la segunda columna usamos el valor 3 para el texto. Estos valores se refieren a la variable correspondiente del modelo TreeStore que generamos unos pasos antes: la variable 0 es el entero, la 1 y la 2 son los dos strings que contendrán, respectivamente, el texto y el color de la primera columna, y la 3 el texto de la segunda columna.

Tras crear cada una de las columnas la añadimos a nuestro TreeView mediante el método append_column, con lo que nuestro TreeView estará listo para ser rellenado con los datos que deseemos. Esto lo hacemos de la siguiente manera:

iterador = modelo.insert(posicion) # el iterador es el objeto que contiene todos los valores de una fila concreta
modelo.set_value(iterador,0,0) # el segundo cero es el dato que metemos (un entero)
modelo.set_value(iterador,1,"Un texto")
modelo.set_value(iterador,2,"#00FFE0")
modelo.set_value(iterador,3,"Otro texto")

Estas llamadas se repetirán tantas veces como filas queramos insertar en el TreeView. La variable posicion contendrá el número de fila en la que se insertarán estos datos.

En el caso de utilizar un TreeStore en lugar de un ListStore, el método insert precisaría de un parámetro adicional, antes de la posición, para indicar qué elemento (referenciado mediante un TreeIter) es el padre. Si se pone None, se supondrá que cuelga del padre.

Con ésto ya tenemos nuestro TreeView listo, pero aún nos faltan dos cosas: borrarlo para meter nuevas filas, y saber qué fila es la que un usuario ha escogido. Lo primero es tan simple como llamar al método clear del modelo, con lo que podemos volver al último paso (con los set_values) para introducir las nuevas columnas. Para lo segundo sólo tenemos que hacer:

seleccion,iterador = treeview.get_selection().get_selected()

Si iterador es None, significa que no hay ninguna fila seleccionada. En caso contrario podemos utilizar

seleccion.get_value(iterador,VARIABLE)

para obtener el valor de la columna indicada por VARIABLE. Si usamos el valor 1 nos devolvería el texto de la primera columna; si usamos el valor 2, el color de fondo de la primera columna, y con el valor 3, el texto de la segunda columna. Con el valor 0 nos devolvería un entero, que hasta ahora no hemos utilizado para nada; pero si a dicho entero le asignamos valores consecutivos al crear las filas, nos dirá cual es el número de fila que escogió el usuario.

Python, MySQL, UTF-8 y la madre que los parió

Errar es humano, pero para liar las cosas de verdad se necesita un ordenador… que trabaje con UTF-8.

Para los que no lo conozcan, UTF-8 es una codificación multibyte para la tabla de caracteres UNICODE. Un caracter UNICODE está representado por un número entre 0 y 1,114,111, y UTF-8 es un sistema para representar dicho número mediante una secuencia de bytes. Su característica más atractiva es que los primeros 128 caracteres se corresponden con los de la tabla ASCII y, además, ocupan un solo byte, lo que significa que un texto en ASCII estándar (de 7 bits) es, a la vez, un texto UTF-8; sin embargo, tan pronto comenzamos a usar otros caracteres más raros (como nuestra querida letra Ñ, o nuestras vocales acentuadas), ocuparemos dos, tres o hasta cuatro bytes por caracter.

En efecto, hagamos una pequeña prueba: abramos nuestro interprete de Python y escribamos:

Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02)
[GCC 4.1.2 (Ubuntu 4.1.1-13ubuntu5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> cadena = u"Papá"
>>> print len(cadena)
4

Hasta aquí nada raro: hemos definido una cadena de tipo UTF-8 (la letra u antes de las comillas sirve para eso) y le hemos pedido al interprete que nos diga su longitud, que, efectivamente, es cuatro. Pero si cambiamos un poco el experimento llegan las cosas «raras»:

>>> cadena2 = "Papá"
>>> print len(cadena2)
5

Ahora la longitud es cinco, lo que no supone ningún misterio si nos damos cuenta de que la cadena la estamos definiendo como cadena normal, no cadena UTF-8, y por tanto nos devuelve el número de bytes que ocupa; y como la letra á (con tilde) necesita dos bytes (pues mi sistema utiliza UTF-8), el misterio queda resuelto.

Por desgracia las cosas se complican cuando uno tiene que usar Python para transmitir y recibir cadenas en UTF-8, byte a byte, por la red; cadenas de las que tomará trozos y compondrá con ellos peticiones para MySQL. Entonces es cuando llega la pesadilla… ¿Y por qué? Pues porque por defecto MySQL espera y entrega sus datos en Latin-1, y también los almacena en dicho formato; pero si yo meto una cadena UTF-8 sin decirle que es UTF-8, el la considera una secuencia el Latin-1, con lo que los datos que me entrega llegan bien, y los que yo le paso también… si no fuese porque el módulo de Python para trabajar con MySQL sabe que éste espera los datos en Latin-1 en intenta convertirlos, soltando a veces una excepción, o metiendo basura en la base de datos el resto.

Por todo ello debemos ser muy cuidadosos a la hora de trabajar con MySQL y Python en UTF-8, y aquí van las pistas que he ido recolectando después de varios días de sufrimientos varios:

Convertir las tablas a UTF-8 Es el primer paso: si creaste tus tablas en formato Latin-1, lo mejor es pasarlas a UTF-8 para evitarse problemas. Para ello usaremos

mysqldump --opt --password=miclave --user=miuser mibasededatos > archivo.sql

A continuación editaremos el código y cambiaremos la palabra latin1 por utf8, con lo que al recuperar los datos, las tablas se crearán en el nuevo formato. Ahora sólo queda volver a grabarlas con

mysql -u miuser -p miclave mibasededatos < archivo.sql

y listo, ya tenemos nuestras tablas en UTF-8.

Acceder a MySQL mediante UTF-8 Esta parte es sencilla también, pero la documentación es escasa y sólo la encontré después de bucear por muchas páginas de Internet y juntar resultados. Tenemos que usar la línea en python:

mibase=MySQLdb.connect(host="mihost", user="usuario", passwd="clave", db="basedatos", charset="utf8", init_command="set names utf8")

(atención a la negrita). Justo a continuación tenemos que añadir:

mibase.names="utf8"

y con ésto ya tendremos garantizado el acceso a la base de datos mediante UTF-8.

En principio con esto ya tendríamos todo listo para poder trabajar cómodamente con MySQL, Python y UTF-8, pero aún falta algo, y es que la cadena que contenga las peticiones no basta con que contenga secuencias UTF-8, sino que tiene que ser un string de tipo UTF-8. Sin embargo, puede ocurrir que tengamos las secuencias UTF-8 almacenadas en cadenas normales. En estos casos, el método encode(«utf-8») nos sacará del apuro. También nos será util si necesitamos mezclar cadenas normales con los resultados que nos devuelva, en UTF-8, la base de datos.

Programando para Windows Mobile 5 con Mono (parte 2)

Ahora que ya establecimos contacto entre la PDA y el PC podemos empezar la parte de programación.

Windows Mobile 5 incorpora una máquina virtual de .NET, por lo que, en principio, parece que podemos utilizar Mono directamente para escribir un programa, transferir el .exe directamente a la PDA y ejecutarlo. Por desgracia la cosa no es tan sencilla, pues las PDAs disponen de una versión reducida de .NET denominada .NET Compact Framework. Se trata, básicamente, de un subconjunto de la máquina virtual clásica, por lo que hay una serie de clases y métodos que no estarán disponibles. Es por esta razón que los ejecutables .NET para PDAs usan una firma digital diferente que los que son para equipos clásicos, y por eso si intentamos ejecutar en una PDA un programa hecho con Mono recibiremos un mensaje de error, indicándonos que ese fichero no es un programa válido.

Afortunadamente, Jean-Baptiste Evain publicó en su blog un programa que cambia la firma de un ejecutable .NET, de manera que ya puede correr en una máquina con Compact Framework (por supuesto, deja de ser ejecutable en un equipo clásico). Con ésto ya tenemos salvado el principal escollo, aunque hay que tener en cuenta que, para usarlo, es necesaria la versión 0.4 o posterior de CECIL, un módulo escrito y mantenido por el propio Jean-Baptiste Evain que da acceso a bajo nivel a los ficheros .exe de .NET. En Debian testing y en Ubuntu Edgy está dicha versión, pero en Ubuntu Dapper, que todavía es la versión estable de Ubuntu, está la versión 0.3. De todas maneras, si tenemos en cuenta que Edgy saldrá el próximo día 10, no es una espera demasiado larga.

Para usar este programa tan sólo tenemos que compilarlo con Mono y luego llamarlo desde la línea de comandos, poniendo como primer parámetro el fichero .exe que queremos convertir.

Ahora que ya podemos meter código ejecutable en nuestra PDA debemos saber que hay algunas diferencias a la hora de programar para ésta, las cuales vienen dadas por la diferente naturaleza del dispositivo (sin teclado, poca memoria, sin ventanas que se solapan…). Veamos algunas de ellas.

Para empezar está el tema de ventanas y widgets. Para trabajar con ellas usaremos las WinForms, pero teniendo en cuenta que algunos widgets (como, por ejemplo, RichTextForm) no estarán disponibles. En esta página de microsoft (en castellano, además) hay una excelente relación de las diferencias entre .NET Framework y .NET Compact Framework.

Existen, sin embargo, un par de detalles extra que me gustaría comentar. Para empezar, por defecto no aparece el botón para desplegar el teclado virtual. Para disponer de él tenemos que añadir a nuestro Form un menú, de la siguiente manera:

MiForm.Menu = new MainMenu();

(siendo MiForm el objeto formulario al que le queremos añadir el menú y el teclado). En esta página podemos encontrar más detalles sobre el teclado virtual. OJO: aunque en ella dice que dicho menú se añade automáticamente, ésto sólo es cierto cuando trabajamos con Visual Studio, no con Mono.

La segunda cuestión se refiere al botón de la ventana con forma de X. La primera impresión que da es que dicho botón destruye el Form; sin embargo, lo que hace realmente es minimizarlo. Tal y como se explica en esta página de Microsoft sobre el botón de los formularios, para que el botón destruya el Form es necesario hacer lo siguiente:

MiForm.ControlBox = True
MiForm.MinimizeBox = False

aunque, en las pruebas que hice, parece que basta con poner a False la propiedad MinimizeBox únicamente, sin tocar ControlBox. Por último, si ponemos ambas a False el Form no tendrá ningún botón.

Un detalle importante es que cuando cerramos el último formulario la aplicación se cierra. En la plataforma clásica, sin embargo, al cerrar un formulario simplemente se retorna de Application.Run(formulario). Esto lo descubrí de casualidad, porque, en la aplicación que estoy haciendo, necesito cambiar por completo los widgets del formulario en función de las acciones del usuario. Al principio destruía el formulario actual (llamando a su método close()) y creaba uno nuevo, pero si bien funcionaba en la plataforma clásica, no lo hacía en la PDA. La solución en este caso fue llamar al método Clear() del formulario, que elimina todos los widgets de éste y lo deja listo para ser repoblado con nuevos elementos.

Comentar también que si la PDA nos devuelve un error indicando que necesitamos una versión posterior de .NET para ejecutar dicho programa, significa que hemos usado alguna clase o método no soportado en el Compact Framework. Al pulsar en OK podremos ver el error completo, y en concreto qué método o clase es la que provocó el error. Si nos ocurre ésto tendremos que buscar la manera de sustituir dicha clase o método por otro que sí esté soportado.

Esto es todo lo que llevo descubierto hasta ahora. A medida que encuentre nuevas cosas interesantes las iré poniendo por aquí.

Por último, añadir dos URLs con información extra (en inglés) sobre el .NET Compact Framework.

Building applications for platforms and components

Product information for .NET Compact Framework

Programando para Windows Mobile 5 con Mono

La política no es la única que hace extraños compañeros de cama: en estos momentos estoy compartiendo mi vida con una PDA con Windows Mobile 5.

La razón es que, por motivos laborales, tengo que escribir un programa para dicho sistema operativo, y aunque los entornos de desarrollo de Micro$oft son conocidos por su gran calidad, la idea de cambiar mi escritorio del trabajo a Windows (con todo lo que ello conlleva: configurar de nuevo el programa de correo, enlaces del navegador, etc) me tiraba mucho para atrás. Por eso decidí realizar el programa con .NET, para lo cual podría usar Mono y MonoDevelop desde GNU/Linux. Además podría vender la moto de que el mismo programa también les funcionará en GNU/Linux…

Por supuesto, las cosas no fueron sencillas. El primer problema era conseguir comunicar la PDA con Linux, problema que parecía resuelto con SynCe (y su Wiki de documentación). Tras un par de pruebas confirmé que el driver ipaq no reconocía a la PDA, así que me puse a bucear en la página de documentación para Windows Mobile 5 y descubrí que los procesadores Intel PXA27x todavía no están soportados por un fallo en su controlador USB.

Como no podía ser de otra manera, mi PDA tenía un procesador de esa familia, lo que reducía mis opciones a una: usar BlueTooth. Afortunadamente, en la documentación anterior se explica muy bien como configurarlo, aunque hay un par de detalles que no comentan bien y que hay que tener en cuenta:

  • Para establecer la conexión no sirve el comando triggerconnection, sino que hay que lanzar ActiveSync y escoger Conectar por BlueTooth en el menú.
  • Una vez conectado toda la comunicación se realiza a través del puerto 990 de TCP/IP, por lo que es necesario abrir en el firewall el acceso a nuestro equipo a través de la interfaz ppp0.
  • De las dos direcciones IP especificadas en el fichero /etc/ppp/peers/dun, la primera es la que se le asigna a nuestro ordenador y la segunda la de la PDA.
  • La misma conexión TCP/IP está disponible para otros programas, aunque en mi caso no puedo usar Internet Explorer para navegar porque se empeña en conectar de nuevo mediante alguno de los otros perfiles disponibles.
  • Parece que no basta con añadir el servicio SP (serial port) para que aparezca el ActiveSync en la PDA, sino que también tiene que estar el servicio LAN y el DUN.
  • Al contrario de lo que dice en la documentación para BlueTooth, hay que lanzar vdccm y no dccm. Además no hay que olvidarse de los parámetros que aparecen en Iniciando la conexión.

Añadir que hay un bug en la pila BlueTooth actual (la que traen Ubuntu Edgy y Debian testing/inestable) que desactiva el ISCAN nada más lanzar el servicio, por lo que el ordenador no aparecerá en la lista de dispositivos BlueTooth de la PDA. Para solucionarlo hay que añadir el comando discovto 0; en la sección device del fichero hcid.conf, y cada vez que se inicie la pila BlueTooth (al arrancar el ordenador o hacer un /etc/init.d/bluetooth restart) ejecutar el comando:

dbus-send --system --dest=org.bluez /org/bluez/hci0
org.bluez.Adapter.SetMode string:discoverable

para activar el ISCAN de nuevo. Podemos comprobar el estado con

hciconfig -a

Por último, para los usuarios de Ubuntu Dapper, añadir que la versión de SynCe que trae es demasiado antigua y no soporta Windows Mobile 5, así que es necesario bajarse los fuentes desde la página y recompilar.

La parte de como escribir programas para la PDA la dejo para una segunda entrada, que esta ya es demasiado larga.