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.

Tengo un tablero de 13x13 botones y etiquetas.. ¿Tengo que crear los 169 controles en Glade?

Nein. Crea una tabla vacía en Glade y la rellenas con controles mediante código. 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 600 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).

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 y contenedores
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 cambio la imagen de un botón (que representa a una ficha)

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 el contenido de un punto del tablero (rebelde, oficial, celda vacía y sus versiones alternativas para mostrar que están seleccionados), y pueden existir varios controles que 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 punto del tablero, 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 puntos del tablero 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 algun punto del tablero, recorres todos los botones que representan puntos (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 situado en un punto concreto y que la variable estado contiene un entero (0 ó 1 en éste ejemplo) que representa el estado de ese punto.

Práctica avanzada: Imágenes y Animación

Los controles de tipo gtk.Image tienen la ventaja, respecto a los botones, de que pueden mostrar imágenes con zonas transparentes, y la desventaja de que no responden a eventos del ratón. El método habitual para sortear esta limitación es incluirlas en un contenedor de tipo gtk.EventBox, en el que se oculta el fondo y borde llamando a set_visible_window(False).

Para colocar controles en cualquier posición se debe usar un contenedor de tipo gtk.Fixed. Los controles se añaden al contenedor usando el método put(control, x, y) y posteriormente se pueden desplazar usando el método move(control, x, y). En ambos casos las coordenadas son pixels con origen la esquina superior izquierda del contenedor.

Por último, la manera más sencilla de conseguir animar elementos es usando la función timeout_add(tpo, callback, datos = None) de la librería gobject. Una llamada a ésta función hace que el sistema ejecute la función callback con el parámetro datos (si lo hemos incluido) cada tpo milisegundos. El ciclo de llamadas se detiene cuando callback devuelve el valor False.

El código siguiente usa las técnicas anteriores para ejecutar una animación sencilla de varios elementos que se pueden hacer desaparecer pulsando sobre ellos. El fichero EjemploAnim.glade se puede generar facilmente en Glade creando una ventana y añadiendola un contenedor Fixed. A la ventana se le marca la propiedad Visible y se le añade el nombre "Salir" en el evento GtkWidget.delete-event

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# coding=utf-8
import gtk
import random
import gobject

TAMCON = 500 # Tamaño en pixels del contenedor
TAMIMG = 80  # Tamaño en pixels de las imágenes

class CosaMovible:
    def __init__(self, x, y, imgs, callback):
        self.x, self.y = x, y               # Coordenadas
        self.imgs = imgs                    # Juego de imagenes
        self.ind = 0                        # Imagen mostrada
        # Se crea el control: Una caja de eventos que contiene una imagen
        self.box = gtk.EventBox()
        self.box.show()                    # Visible
        self.box.set_visible_window(False) # Sin fondo
        # Añadimos una imagen dentro de la caja de eventos
        self.img = gtk.Image()
        self.img.show()
        self.img.set_from_pixbuf(imgs[0])
        self.box.add(self.img)
        # Queremos que se llame a callback cuando se pulse en la caja
        self.box.connect('button-press-event', callback, self)
        self.inicializar()

    def seleccionar(self):
        self.ind = 1 - self.ind
        # Se cambia la imagen
        self.img.set_from_pixbuf(self.imgs[self.ind])

    # Métodos "abstractos", definidos en descendientes
    def inicializar(self):
        pass

    def mover(self):
        pass

class Oficial(CosaMovible):
    # Los oficiales se mueven en linea recta
    def inicializar(self):
        self.vx = random.randint(-5,5)
        self.vy = random.randint(-5,5)

    def mover(self):
        self.x += self.vx
        self.y += self.vy
        if not (0 <= self.x <= TAMCON-TAMIMG):
            self.vx = -self.vx
            self.seleccionar()
        if not (0 <= self.y <= TAMCON-TAMIMG):
            self.vy = -self.vy
            self.seleccionar()
        # El contenedor del control debe ser un gtk.Fixed
        fixed = self.box.get_parent()
        fixed.move(self.box, self.x, self.y)

class Rebelde(CosaMovible):
    # Los rebeldes son más caóticos
    def mover(self):
        self.x = abs(self.x + random.randint(-5,5))
        self.y = abs(self.y + random.randint(-5,5))
        if random.randint(0,50) == 42:
            self.seleccionar()
        # El contenedor del control debe ser un gtk.Fixed
        fixed = self.box.get_parent()
        fixed.move(self.box, self.x, self.y)

class Aplicacion:
    def __init__(self):
        # Juegos de imágenes, disponibles en el FAQ de la práctica
        imgs_ofi = [gtk.gdk.pixbuf_new_from_file("oficial.png"),
                    gtk.gdk.pixbuf_new_from_file("oficial_sel.png")]
        imgs_reb = [gtk.gdk.pixbuf_new_from_file("rebelde.png"),
                    gtk.gdk.pixbuf_new_from_file("rebelde_sel.png")]
        builder = gtk.Builder()
        builder.add_from_file("EjemploAnim.glade")
        builder.connect_signals({"salir": self.evt_salir})
        # Contenedor de tipo Fixed
        fixed = builder.get_object("fixed1")
        # Creamos los oficiales y rebeldes en posiciones al azar
        # y los añadimos al contenedor
        self.lis_cosas = []
        for i in range(10):
            x = random.randint(0, TAMCON - TAMIMG)
            y = random.randint(0, TAMCON - TAMIMG)
            rebelde = Rebelde(x, y, imgs_reb, self.evt_pulsacion)
            fixed.put(rebelde.box, x, y)
            self.lis_cosas.append(rebelde)

        for i in range(4):
            x = random.randint(0, TAMCON - TAMIMG)
            y = random.randint(0, TAMCON - TAMIMG)
            oficial = Oficial(x, y, imgs_ofi, self.evt_pulsacion)
            fixed.put(oficial.box, x, y)
            self.lis_cosas.append(oficial)

        # Proceso de animación: Pedimos que se llame a la función
        # evt_animar cada 10 milisegunos
        gobject.timeout_add(10, self.evt_animar)

    def evt_animar(self):
        # Movemos todas las cosas que queden vivas
        for cosa in self.lis_cosas:
            cosa.mover()
        # Si se devuelve False se termina la animación
        return len(self.lis_cosas) > 1

    def evt_pulsacion(self, widget, event, cosa):
        # Solo se pueden "matar" las cosas seleccionadas
        if cosa.ind > 0:
            # Eliminar el control
            cosa.box.destroy()
            # Quitar el objeto de la lista
            i = self.lis_cosas.index(cosa)
            del self.lis_cosas[i]

    def evt_salir(self, widget, data=None):
        gtk.main_quit()


if __name__ == "__main__":
    Aplicacion()
    gtk.main()
No sé como crear imágenes para este juego

No me lo creo, pero puedes usar las mias.