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:
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):
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):
|
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.
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.
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() |
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).
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.
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)
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:
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))
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
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 ... ...
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 me lo creo, pero puedes usar este juego (cuadradas) o este otro (hexagonales).