Archivo por años: 2007

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()

A vueltas con Mplayer

Parece que no ha bastando con usar la versión del SVN de Mplayer/Mencoder para solucionar los problemas: al generar DVDs en formato PAL, en algunos casos lo hace en blanco y negro, amén de otros pequeños problemas.

Ante ésto he preferido curarme en salud y he retrocedido a la versión 0.99pre8; para ser más exactos, la que venía en Ubuntu Edgy. Dicha versión funcionó siempre razonablemente bien.

Para evitar más problemas de dependencias, he preferido empaquetar sólo los ejecutables en un tar.bz2, e incluir un script que sustituya los que ya haya en el sistema por los «nuevos». Esto lo hace, además, compatible con cualquier distribución, y no sólo con Ubuntu.

Actualización: por petición popular he añadido otro paquete con Mplayer/Mencoder para 64 bits.

Paquetes

Parafraseando a Germán Poo, la llegada de la nueva Ubuntu Feisty no es para montar una fiesta, precisamente. La razón es que la versión que trae de Mencoder (la 1.0-rc1, actual versión estable) tiene un fallo al codificar el sonido, con lo que DeVeDe es completamente inútil.

El bug lleva tiempo coleando, y la única solución en estos momentos parece ser utilizar la versión en desarrollo, sacada del SVN (en mi web, por despiste y costumbre, hablo de CVS. ¡Perdón, perdón!). Como una cosa es hacer un sudo ./install, que cualquiera lo hace, y otra muy distinta bajarse fuentes de un SVN y descubrir qué bibliotecas son necesarias para compilar un programa tan complejo como MPlayer/Mencoder, he decidido hacer yo mismo un paquete .deb con la versión del SVN del 14 de abril de 2007, compilada sobre Feisty, para que los usuarios no queden atados.

Nueva versión de GAG

A menos de 15 horas de irme de viaje a Bilbao (¡merecidas vacaciones, proclamo!) acabo de subir la versión 4.8 de GAG.

La verdad es que ha sido una sesión maratoniana para terminar el trabajo de casi tres semanas, durante las cuales hice algunas cosas que tenía que haber hecho hace mucho, mucho tiempo.

Para empezar he escrito una excelente documentación de instalación y uso en HTML (y con multitud de capturas de pantalla), por lo que ahora es mucho más fácil entender como se instala y configura GAG.

También actualicé la FAQ, añadiendo entre otras cosas que también se puede usar GRUB, así como la manera de recuperar el arranque de Linux si se instala GAG antes de meter LILO o GRUB en el superblock de la partición raiz, y algunos detalles más.

Otra sección de la documentación es la de detalles técnicos. Ahí explico el formato de los datos de configuración, así como el formato de los iconos, fuentes de letra, etc. La idea es que otros puedan crear iconos y fuentes de letra para GAG (por ejemplo, para añadir soporte realmente nativo a lenguas como el japonés o el griego).

Respecto a la configuración, la he cambiado de sitio y ahora está siempre al principio del segundo sector del disco duro. Esto significa que ya es posible escribir programas de configuración externos (para, por ejemplo, cambiar la configuración directamente desde un instalador de Linux).

Otra gran novedad es el instalador nativo desde Linux: un simple gag-install /dev/sda instalará GAG en la primera unidad (haciendo las comprobaciones pertinentes para evitar pisar una partición que empiece en donde no deba, claro). También se puede especificar desde la línea de comandos la lengua y el tipo de teclado a instalar.

Y ya por último corregí un par de detalles en los ficheros de lenguas Bable y Turco.

Ahora sólo queda que se me pase el miedo que siempre acompaña a cualquier lanzamiento de GAG: miedo a haber metido la gamba con cualquier tontería y destrozar los datos de medio planeta… O:-)

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.

El silencio

Comentaba en mi anterior entrada «¿Termoqué?» la dificultad que estaba teniendo para encontrar un disipador adecuado para mi nuevo equipo. Y no es para menos: paso mucho tiempo delante de la pantalla y lo último que quiero es una turbina de avión destrozándome el oído.

Después de muchas pruebas y mediciones (de tamaño: la nueva caja tiene un travesaño que limita la altura del disipador) me decanté por un Zalman CNPS8000. Este disipador tiene un tamaño adecuado para entrar en la caja y es muy silencioso (a baja velocidad, claro). El único defecto es que la regulación de velocidad es manual. Afortunadamente mi nueva placa es una ASUS con tecnología Q-Fan. Este sistema, propio de ASUS, permite a la BIOS controlar la velocidad de giro de cualquier ventilador (no tiene por qué tener cable de control de velocidad) en base a la temperatura de la CPU, exactamente igual que hacía mi viejo Artic Cooling. El resultado es justo el que quiero: trabajar con mi ordenador disfrutando en todo momento del máximo silencio posible, y sin tener que preocuparme de si el procesador se puede estar sobrecalentando.

Nueva versión de DeVeDe

DeVeDe 2.9 está en la calle por fin. La gran novedad es que ahora se puede ejecutar también en Windows, aunque hacen falta unas cuantas dependencias: además de Mplayer, Mencoder, DVDAuthor, VCDImager y MKisoFS para windows es necesario Python, PyGTK, GTK, Pyglade y LibGlade para windows. Ahí es nada. Por supuesto tengo que agradecer a Peter Gill el trabajo realizado, pues ha sido todo obra suya.

Otro cambio ha sido el detectar cuando se produce un error en MKISOFS. En efecto, aunque el código estaba ahí, faltaba poner a TRUE una variable, por lo que si MKISOFS fallaba por cualquier razón, DeVeDe devolvía un mensaje de éxito en lugar de error.

También añadí un aviso sobre el uso de particiones FAT32/VFAT para el directorio de destino: los DVDs de vídeo tienen una estructura de directorios con sus nombres en mayúsculas. Desgraciadamente las particiones FAT32/VFAT no distinguen entre ambas y MKISOFS se hace un lío y falla miserablemente (cinco céntimos de euro a quien adivine qué relación tiene este bug con el anterior 🙂

Por último, y aparte de un fallito tonto en el Drag&Drop, también corregí un problema con ciertos vídeos provenientes de capturadoras de vídeo. Estos vídeos provocan una avalancha de líneas ID cuando se usa la opción -identify de MPlayer, necesaria para conocer la longitud, resolución, etc. del vídeo. En el código original esperaba a que terminase la ejecución de MPlayer y sólo entonces leía el buffer de STDOUT para ver los valores ID, con lo que si había demasiados de éstos el buffer se llenaba y no seguía la ejecución. Ahora voy leyendo las líneas a la vez que se ejecuta, por lo que ya no falla.