FAQ de la segunda práctica

Puedes contribuir a la expansión de este FAQ enviando por correo tus preguntas, respuestas o detección de errores a la dirección cvaca [at] infor.uva.es

Los sitios principales donde se puede conseguir información adicional son:

 

Estructura de una aplicación orientada a entorno gráfico

Las aplicaciones orientadas a entorno gráfico siguen el paradigma de Orientación a Eventos: Las aplicaciones no son monolíticas sino que consisten en un conjunto de funciones asociadas a determinados eventos (típicamente que el usuario actue sobre algún control), las cuales serán ejecutadas cuando suceda ese evento.

Como lo normal es que se necesite almacenar información sobre el estado de la aplicación que pueda ser accesible por todas esas funciones, lo lógico es usar Orientación a Objetos de forma que la aplicación se represente por una clase donde los atributos almacenen el estado y las funciones asociadas a eventos sean métodos de esa clase.

La interfaz de usuario tiene una organización jerárquica: Consta de una o más ventanas (gtk.Window) cada una de las cuales tiene un único contenedor que puede tener contener a su vez otros contenedores o controles.

Los contenedores son controles cuyo objetivo es contener a otros elementos y organizar su disposición visual. Ejemplos de contenedores: Caja horizontal (gtk.HBox), caja vertical (gtk.VBox), tabla (gtk.Table), centrador (gtk.Alignment), panel de posiciones prefijadas (gtk.Fixed), panel con borde y título (gtk.Frame), caja de eventos (gtk.EventBox),... Este último sirve para poder recibir eventos de pulsación de ratón en controles normalmente insensibles a ellos, como los controles de tipo imagen.

Los controles sirven para mostrar y recibir información al/del usuario. Ejemplo de controles: Etiquetas (gtk.Label), botones (gtk.Button, gtk.CheckButton, gtk.RadioButton), imágenes (gtk.Image), cajas de texto simples (gtk.Entry), cajas de texto multilínea (gtk.TextView), listas desplegables (gtk.ComboBox)... Hay que tener cuidado porque algunos controles (como los dos últimos mencionados) requieren crear objetos especiales y asociarlos a ellos (gtk.TextBuffer y gtk.ListStore, por ejemplo).

Algunos controles pueden actuar como contenedores y contener a otros controles en su interior (por ejemplo un botón puede contener una imagen).

Para crear una interfaz de usuario hay que realizar las siguientes etapas (en cualquier orden o mezclándolas):

  1. Crear la ventana y sus controles y contenedores.
  2. Asignarles las propiedades adecuadas (visibilidad, forma en que se ajustan dentro de su contenedor, texto o imágenes que muestran, etc).
  3. Añadir en el orden correcto los controles/contenedores a su contenedor.
  4. Almacenar las referencias a los controles/contenedores que vayamos a necesitar cambiar sus propiedades en el futuro.
  5. Establecer las asociaciones necesarias entre control-evento-función, para que cuando suceda un determinado evento sobre un determinado control se ejecute una determinada función.

Ejemplo: Queremos crear una aplicación que muestre el texto que introduce el usuario pero invirtiendo las palabras. Diseñamos la aplicación en Glade incluyendo 2 etiquetas y una caja de texto en un contenedor de tipo tabla 2x2 (la etiqueta inferior ocupa las 2 columnas). Controlamos 2 eventos, el de cambio del contenido de la caja de texto y el evento de pulsación del botón de cierre de la ventana (no mostrado, es el evento delete-event de la ventana):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pygtk
import gtk

class Aplicacion:
    def __init__(self):
        self.crea_ventana()

    def crea_ventana(self):
        builder = gtk.Builder()
        builder.add_from_file("ejemplo.glade")
        builder.connect_signals({
            "salir": self.salir,
            "cambio_texto": self.cambio_texto
        })
        self.edt_entrada = builder.get_object("entry1")
        self.etq_result = builder.get_object("label2")
        self.etq_result.set_text("")
       
    def salir(self, widget, data = None):
        gtk.main_quit()

    def cambio_texto(self, widget, data = None):
        txt = self.edt_entrada.get_text()
        res = ' '.join(txt.split(' ')[::-1])
        self.etq_result.set_text(res)
        
if __name__ == "__main__":
    Aplicacion()
    gtk.main()
  

Las etapas 1, 2 y 3 se realizan automáticamente por el objeto builder en la línea 10 usando la información del fichero generado por Glade. La etapa 4 se realiza en las líneas 15 y 16 usando el método get_object del objeto builder (atención, es necesario recordar los nombres que tienen los controles en el diseño de Glade). La etapa 5 se lleva a cabo en las líneas 11, 12, 13 y 14 usando el método connect_signals del objeto builder, cuyo parámetro es un diccionario donde las claves son los nombres de eventos introducidos en Glade y los valores los nombres de las funciones que se van a ejecutar cuando suceda el evento.

¿Se puede crear la aplicación sin Glade?

Por supuesto. Existen situaciones (ver siguiente entrada) donde puede ser conveniente el diseñar parte de la aplicación en Glade y otra parte crearla directamente en código (por ejemplo si el número de controles es variable).

La aplicación de ejemplo de la entrada anterior se podría escribir sin Glade de la siguiente forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import pygtk
import gtk
import pango

class Aplicacion:
    def __init__(self):
        self.crea_ventana()

    def crea_ventana(self):
        self.edt_entrada = gtk.Entry()
        self.edt_entrada.show()
        self.edt_entrada.connect("changed", self.cambio_texto)
        self.etq_result = gtk.Label("")
        self.etq_result.show()
        self.etq_result.set_alignment(0.0,0.5)
        self.etq_result.modify_font(pango.FontDescription("Sans italic 12"))
        etq = gtk.Label("Texto: ")
        etq.show()
        tabla = gtk.Table(2,2)
        tabla.show()
        tabla.set_row_spacings(5)
        tabla.set_col_spacings(5)
        tabla.attach(etq,0,1,0,1)
        tabla.attach(self.edt_entrada,1,2,0,1)
        tabla.attach(self.etq_result,0,2,1,2)
        ventana = gtk.Window()        
        ventana.show()
        ventana.set_title("Ejemplo")
        ventana.set_border_width(10)
        ventana.add(tabla)
        ventana.connect("delete-event", self.salir)
        
    def salir(self, widget, data = None):
        gtk.main_quit()

    def cambio_texto(self, widget, data = None):
        txt = self.edt_entrada.get_text()
        res = ' '.join(txt.split(' ')[::-1])
        self.etq_result.set_text(res)
        
if __name__ == "__main__":
    Aplicacion()
    gtk.main()

La etapa 1 (creación de controles) se lleva a cabo en las líneas 10, 13, 17, 19 y 26. La etapa 2 (asignación de propiedades) en las líneas 11, 14, 15, 16, 18, 20, 21, 22, 27, 28 y 29. La etapa 3 (introducción de controles en contenedores) en las líneas 23, 24, 25 y 30. La etapa 5 (conexión de evento-control-función) en las líneas 12 y 31. Respecto a la etapa 4 (almacenar referencias) notad que definimos atributos para las referencias a controles que vamos a usar en las funciones asociadas a eventos, y variables locales en el resto de casos.

El tablero puede llegar a ser de 30x30.. ¿Tengo que crear 900 botones en Glade?

Nein. Crea una tabla vacía en Glade y la rellenas con botones mediante código (Atención, ver pregunta sobre cómo redimensionar correctamente una tabla). El siguiente código rellena una tabla con 600 botones (20 filas x 30 columnas):

import pygtk
import gtk

class Aplicacion:
    def __init__(self):
        self.crea_ventana()

    def crea_ventana(self):
        builder = gtk.Builder()
        builder.add_from_file("ejemplo2.glade")
        builder.connect_signals({
            "salir": self.salir,
        })
        tabla = builder.get_object("table1")
        tabla.resize(20, 30)
        for fil in range(20):
            for col in range(30):
                btn = gtk.Button(str(fil)+","+str(col))
                btn.show()
                tabla.attach(btn, col, col+1, fil, fil+1)
       
    def salir(self, widget, data = None):
        gtk.main_quit()

if __name__ == "__main__":
    Aplicacion()
    gtk.main()
  
¿Y como detecto que botón concreto ha sido pulsado? ¿O tengo que definir 900 funciones distintas?

Una misma función puede asociarse a cualquier número de eventos distintos (o el mismo tipo de evento de muchos controles distintos). Para poder detectar que evento concreto se ha producido, en el proceso de asociación (llamada al método connect) se puede indicar un parámetro extra que representa a un dato cualquiera que puede usarse para identificar al evento, porque se pasa como parámetro en la llamada a la función asociada (el parámetro que se suele definir como data = None).

Podemos adaptar el código de la pregunta anterior para asociar los eventos de pulsación de cada uno de los 600 botones con una única función, añadiendo como dato extra una tupla que indique la fila y columna de cada botón:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import pygtk
import gtk

class Aplicacion:
    def __init__(self):
        self.crea_ventana()

    def crea_ventana(self):
        builder = gtk.Builder()
        builder.add_from_file("ejemplo2.glade")
        builder.connect_signals({
            "salir": self.salir,
        })
        tabla = builder.get_object("table1")
        self.etiq = builder.get_object("label1")
        tabla.resize(20,30)
        for fil in range(20):
            for col in range(30):
                btn = gtk.Button(str(fil)+","+str(col))
                btn.show()
                btn.connect("clicked", self.click, (fil,col))
                tabla.attach(btn, col, col+1, fil, fil+1)

    def click(self, widget, data = None):
        self.etiq.set_text("Pulsado boton posicion = "+str(data))
       
    def salir(self, widget, data = None):
        gtk.main_quit()

if __name__ == "__main__":
    Aplicacion()
    gtk.main()

La tupla (fil,col) que se pasa como tercer parámetro de connect en la línea 21 se envía en el parámetro data cuando se llama a click (línea 24).

¿Y como consigo colocar en disposición hexagonal (zig-zag) a los botones?

Al añadir elementos a una tabla (método attach) puedes hacer que ocupen varias filas o columnas consecutivas. Crea una tabla con el doble de columnas que las necesarias y haz que cada botón ocupe 2 columnas. Otra posibilidad es usar un contenedor de tipo gtk.Fixed e indicar directamente la posición en pixels de cada botón. También puedes no usar tablas en absoluto y crearte un único control de tipo gtk.DrawingArea y dibujar las imágenes de cada celda en la posición adecuada.

Cómo redimensionar correctamente una tabla

Para cambiar el número de filas y columnas de una tabla (gtk.Table), primero se deben eliminar los controles que contiene. Llamando al método destroy (gtk.Widget) de cada control provocamos que se elimine del contenedor a que pertenece (y destruimos el control):

# Quitar (y destruir) controles de la tabla
for ctrl in self.tabla:
   ctrl.destroy()
# Redimensionar tabla
self.tabla.resize(numFil, numCol)
Cómo se crean y gestionan varias ventanas o diálogos

Aunque existen otras clases más especializadas, se recomienda que todas las ventanas y diálogos (salvo el de selección de ficheros, ver pregunta posterior) se representen por la misma clase, gtk.Window. Reglas a seguir:

Cosas que es necesario conocer sobre algunos controles
Cómo gestionar la visualización del tiempo de juego transcurrido

En la ventana principal de juego debe existir un control (típicamente un gtk.Label) que muestre el tiempo transcurrido desde la primera jugada del usuario. La forma más sencilla de actualizar ese control es usando la función timeout_add(tiempo, callback) de la librería gobject, que hace que el sistema ejecute repetidamente la función callback con el periodo de tiempo especificado (en milisegundos). El control del tiempo transcurrido se puede calcular usando la función time() de la librería time, que devuelve el tiempo actual (en segundos).

Librerías necesarias:

import time
import gobject

Creación del temporizador (cuando se realice la primera jugada):

self.tpo0 = time.time()
self.timer = gobject.timeout_add(1000, self.click)

Destrucción del temporizador (al finalizar la partida):

if self.timer != None:
   gobject.source_remove(self.timer)
   self.timer = None

Actualización de la etiqueta (se supone que la referencia a la etiqueta se almacena en el atributo etq_tpo:

def click(self):
   dt = int(time.time() - self.tpo0)
   self.etq_tpo.set_label("{0:02}:{1:02}".format(dt/60,dt%60))
Cómo obtener el nombre de un fichero usando el diálogo estándar del sistema

La siguiente función muestra el diálogo estándar de selección de ficheros y devuelve el nombre del fichero escogido por el usuario o el valor None si ha pulsado Cancelar:

def elegirFichero(self):
    dlg = gtk.FileChooserDialog(
            "Abrir fichero", None,
            gtk.FILE_CHOOSER_ACTION_OPEN,
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
             gtk.STOCK_OPEN, gtk.RESPONSE_OK))

    if dlg.run() == gtk.RESPONSE_OK:
        res = dlg.get_filename()
    else:
        res = None
    dlg.destroy()
    return res
Cómo saber si se ha pulsado el botón izquierdo o derecho del ratón

El evento clicked no proporciona información sobre que botón del ratón ha sido pulsado. Es necesario utilizar el evento button-release-event, la función asociada a éste evento tiene un parámetro extra, event, que es un objeto con información adicional, en concreto el atributo event.button es un entero que indica el botón concreto utilizado (vale 1 si es el izquierdo, 3 si es el derecho).

El código para asociar el evento cada vez que se crea un botón (típicamente dentro de un bucle doble donde creas todos los botones que representan las celdas y los insertas en la tabla) sería parecido a éste:

btn = gtk.Button()
...
btn.set_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
btn.connect("button-release-event", self.click, ..)
...

En la función asociada al evento ya puedes detectar que botón del ratón se ha pulsado:

def click(self, widget, event, data = None):
    ...
    if event.button == 1:
        # Boton izquierdo pulsado
        ...
    elif event.button == 3:
        # Boton derecho pulsado
        ...
    ...
Cómo cambio la imagen de un botón (que representa a una celda)

A primera vista podría parecer que lo adecuado es usar controles de tipo gtk.Image ya que la clase gtk.Button dispone de un método set_image(..) cuyo parámetro es de tipo gtk.Image, pero existe un problema porque gtk.Image representa un control y por lo tanto no puede ser compartido por varios botones.

En nuestro caso tenemos un juego de imágenes que representan estados de la celda, y pueden existir varias celdas en el mismo estado y que por tanto deban mostrar la misma imagen.

La solución consiste en almacenar las imágenes en objetos de tipo gtk.gdk.Pixbuf, que no son controles. Por ejemplo, suponiendo que sólo existieran dos posibles estados en cada celda, leeríamos y almacenariamos las imágenes al principio de la aplicación usando un código parecido a éste:

def __init__(self):
    ...
    self.imgs = []
    self.imgs.append(gtk.gdk.pixbuf_new_from_file("estado0.png"))
    self.imgs.append(gtk.gdk.pixbuf_new_from_file("estado1.png"))
    ...

Al crear cada botón (típicamente dentro de un bucle doble donde creas todos los botones que representan las celdas y los insertas en la tabla) le añades un control imagen (vacía). También puede ser conveniente indicar que no queremos que se dibuje el borde:

...
btn = gtk.Button(None)
btn.set_image(gtk.Image())
btn.set_relief(gtk.RELIEF_NONE)
... 

En el sitio del código al que llamas cuando haya cambiado el estado de alguna celda del tablero, recorres todos los botones que representan celdas (que supongo que habras almacenado de forma adecuada) y para cada uno actualizas la imagen segun su estado de la siguiente forma:

...
btn.get_image().set_from_pixbuf(self.imgs[estado])
... 

Nota: En el código anterior supongo que la variable btn tiene la referencia del botón que representa una celda concreta y que la variable estado contiene un entero (0 ó 1 en éste ejemplo) que representa el estado de esa celda concreta.

No sé como crear imágenes que representen el estado de las celdas

No me lo creo, pero puedes usar este juego (cuadradas) o este otro (hexagonales).