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 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() |
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).
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:
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
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.
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 me lo creo, pero puedes usar las mias.