Desarrollo de GUI de Python con Tkinter: Parte 2

Esta es la segunda entrega de nuestra serie de varias partes sobre el desarrollo de GUI en Python usando Tkinter. Echa un vistazo a los enlaces a continuación para ver las otras partes de esta serie...

Esta es la segunda entrega de nuestra serie de varias partes sobre el desarrollo de GUI en Python usando Tkinter. Echa un vistazo a los enlaces a continuación para ver las otras partes de esta serie:

Introducción

En la primera parte de la serie de tutoriales wikihtp Tkinter, aprendimos cómo construir rápidamente interfaces gráficas simples usando Python. El artículo explicaba cómo crear varios widgets diferentes y colocarlos en la pantalla usando dos métodos diferentes ofrecidos por Tkinter, pero aún así, apenas arañamos la superficie de las capacidades del módulo.

Prepárese para la segunda parte de nuestro tutorial, donde descubriremos cómo modificar la apariencia de nuestra interfaz gráfica durante el tiempo de ejecución de nuestro programa, cómo conectar inteligentemente la interfaz con el resto de nuestro código y cómo obtener entrada de texto de nuestros usuarios.

Opciones avanzadas de cuadrícula

En el último artículo, conocimos el método grid() que nos permite orientar los widgets en filas y columnas, lo que permite resultados mucho más ordenados que usando el método pack(). Sin embargo, las cuadrículas tradicionales tienen sus desventajas, que se pueden ilustrar con el siguiente ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew")

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Producción:

{.img-responsivo}

El código anterior debería ser fácilmente comprensible para ti si repasaste la primera parte de nuestro tutorial de Tkinter, pero hagamos un resumen rápido de todos modos. En la línea 3, creamos nuestra ventana raíz principal. En las líneas 5-7 creamos tres marcos: definimos que la raíz es su widget principal y que a sus bordes se les dará un sutil efecto 3D. En las líneas 9-11, los marcos se distribuyen dentro de la ventana usando el método grid(). Indicamos las celdas de la grilla que van a ser ocupadas por cada widget y usamos la opción sticky para estirarlas horizontal y verticalmente.

En las líneas 13-15 creamos tres widgets simples: una etiqueta, un botón que no hace nada y otro botón que cierra (destruye) la ventana principal: un widget por marco. Luego, en las líneas 17-19 usamos el método pack() para colocar los widgets dentro de sus respectivos marcos principales.

Como puede ver, tres widgets distribuidos en dos filas y dos columnas no generan un resultado estéticamente agradable. Aunque frame3 tiene toda su fila para sí mismo, y la opción sticky hace que se estire horizontalmente, solo puede estirarse dentro de los límites de su celda de cuadrícula individual. En el momento en que miramos la ventana, instintivamente sabemos que el marco que contiene button2 debe abarcar dos columnas, especialmente considerando la importante función que ejecuta el botón.

Bueno, afortunadamente, los creadores del método grid() predijeron este tipo de escenario y ofrecen una opción de intervalo de columnas. Después de aplicar una pequeña modificación a la línea 11:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Podemos hacer que nuestro frame3 se extienda por todo el ancho de nuestra ventana.

Producción:

{.img-responsive}

El método place()

Por lo general, cuando se construyen interfaces agradables y ordenadas basadas en Tkinter, los métodos place() y grid() deberían satisfacer todas sus necesidades. Aún así, el paquete ofrece un administrador de geometría más: el método place().

El método place() se basa en los principios más simples de los tres administradores de geometría de Tkinter. Usando place() puedes especificar explícitamente la posición de tu widget dentro de la ventana, ya sea proporcionando directamente sus coordenadas exactas o haciendo que su posición sea relativa al tamaño de la ventana. Echa un vistazo al siguiente ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import tkinter

root = tkinter.Tk()

root.minsize(width=300, height=300)
root.maxsize(width=300, height=300)

button1 = tkinter.Button(root, text="B")
button1.place(x=30, y=30, anchor="center")

root.mainloop()

Producción:

{.img-responsive}

En las líneas 5 y 6 especificamos que queremos que las dimensiones de nuestra ventana sean exactamente 300 por 300 píxeles. En la línea 8 creamos un botón. Finalmente, en la línea 9, usamos el método place() para colocar el botón dentro de nuestra ventana raíz.

Proporcionamos tres valores. Usando los parámetros x e y, definimos las coordenadas exactas del botón dentro de la ventana. La tercera opción, ancla, nos permite definir qué parte del widget terminará en el punto (x,y). En este caso, queremos que sea el píxel central de nuestro widget. De manera similar a la opción sticky de grid(), podemos usar diferentes combinaciones de n, s, e y w para anclar el widget por sus bordes o esquinas.

Al método place() no le importa si cometemos un error aquí. Si las coordenadas apuntan a un lugar fuera de los límites de nuestra ventana, el botón no se mostrará. Una forma más segura de usar este administrador de geometría es usar coordenadas relativas al tamaño de la ventana.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import tkinter

root = tkinter.Tk()

root.minsize(width=300, height=300)
root.maxsize(width=300, height=300)

button1 = tkinter.Button(root, text="B")
button1.place(relx=0.5, rely=0.5, anchor="center")

root.mainloop()

Producción

{.img-responsive}

En el ejemplo anterior, modificamos la línea 9. En lugar de las coordenadas x e y absolutas, ahora usamos coordenadas relativas. Al establecer relx y rely en 0.5, nos aseguramos de que, independientemente del tamaño de la ventana, nuestro botón se colocará en su centro.

Bien, hay una cosa más sobre el método place() que probablemente encontrarás interesante. Ahora combinemos los ejemplos 2 y 4 de este tutorial:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

button1 = tkinter.Button(root, text="B")
button1.place(relx=0.5, rely=0.5, anchor="center")

root.mainloop()

Producción:

{.img-responsive}

En el ejemplo anterior, simplemente tomamos el código del ejemplo 2 y luego, en las líneas 21 y 22, creamos y colocamos nuestro pequeño botón del ejemplo 4 dentro de la misma ventana. Puede que se sorprenda de que este código no provoque una excepción, aunque mezclamos claramente los métodos grid() y place() en la ventana raíz. Bueno, debido a la naturaleza simple y absoluta de place(), puedes mezclarlo con pack() y grid(). Pero solo si realmente tienes que hacerlo.

El resultado, en este caso, es obviamente bastante feo. Si el botón central fuera más grande, afectará la usabilidad de la interfaz. Ah, y como ejercicio, puede intentar mover las líneas 21 y 22 por encima de las definiciones de los marcos y ver qué sucede.

Por lo general, no es una buena idea usar place () en sus interfaces. Especialmente en GUI más grandes, establecer coordenadas (incluso relativas) para cada widget individual es mucho trabajo y su ventana puede volverse desordenada muy rápidamente, ya sea si su usuario decide cambiar el tamaño de la ventana o especialmente si decide agregar más contenido. lo.

Configuración de los widgets {#configuración de los widgets}

La apariencia de nuestros widgets se puede cambiar mientras se ejecuta el programa. La mayoría de los aspectos estéticos de los elementos de nuestras ventanas se pueden modificar en nuestro código con la ayuda de la opción configurar. Echemos un vistazo al siguiente ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import tkinter

root = tkinter.Tk()

def color_label():
    label1.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Configure button", command=color_label)
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Producción:

{.img-responsive}

En las líneas 5 y 6 agregamos una definición simple de una nueva función. Nuestra nueva función color_label() configura el estado de label1. Las opciones que toma el método configure() son las mismas opciones que usamos cuando creamos nuevos objetos widget y definimos los aspectos visuales iniciales de su apariencia.

En este caso, al presionar el botón "Configurar" recién renombrado, se cambia el texto, el color de fondo (bg) y el color de primer plano (fg, en este caso es el color del texto) de nuestra etiqueta1 ya existente. .

Ahora, digamos que agregamos otro botón a nuestra interfaz que queremos usar para colorear otros widgets de manera similar. En este punto, la función color_label() puede modificar solo un widget específico que se muestra en nuestra interfaz. Para modificar varios widgets, esta solución nos obligaría a definir tantas funciones idénticas como el número total de widgets que nos gustaría modificar. Esto sería posible, pero obviamente una solución muy pobre. Hay, por supuesto, formas de alcanzar ese objetivo de una manera más elegante. Ampliemos un poco nuestro ejemplo.

 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
import tkinter

root = tkinter.Tk()

def color_label():
    label1.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=color_label)
button2 = tkinter.Button(frame4, text="Configure button 2", command=color_label)

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')

root.mainloop()

Producción:

{.img-responsive}

Bien, ahora tenemos dos etiquetas y tres botones. Digamos que queremos "Configurar botón 1" para configurar "Etiqueta simple 1" y "Configurar botón 2" para configurar "Etiqueta simple 2" exactamente de la misma manera. Por supuesto, el código anterior no funciona de esta manera: ambos botones ejecutan la función color_label(), que solo modifica una de las etiquetas.

Probablemente la primera solución que se te ocurra es modificar la función color_label() para que tome un objeto widget como un argumento y lo configure. Entonces podríamos modificar la definición del botón para que cada uno de ellos pase su etiqueta individual en la opción de comando:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ...

def color_label(any_label):
    any_label.configure(text="Changed label", bg="green", fg="white")

# ...

button1 = tkinter.Button(frame3, text="Configure button 1", command=color_label(label1))
button2 = tkinter.Button(frame4, text="Configure button 2", command=color_label(label2))

# ...

Desafortunadamente, cuando ejecutamos este código, la función color_label() se ejecuta en el momento en que se crean los botones, lo cual no es un resultado deseable.

Entonces, ¿cómo hacemos que funcione correctamente?

Paso de argumentos a través de expresiones lambda {#paso de argumentos a través de expresiones lambda}

Las expresiones lambda ofrecen una sintaxis especial para crear las denominadas funciones anónimas, definidas en una sola línea. Entrar en detalles sobre cómo funcionan las expresiones lambda y cuándo se utilizan generalmente no es el objetivo de este tutorial, así que concentrémonos en nuestro caso, en el que las expresiones lambda definitivamente son útiles.

 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
import tkinter

root = tkinter.Tk()

def color_label(any_label):
    any_label.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=lambda: color_label(label1))
button2 = tkinter.Button(frame4, text="Configure button 2", command=lambda: color_label(label2))

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')

root.mainloop()

Producción:

{.img-responsivo}

Modificamos la función color_label() de la misma manera que lo hicimos en el ejemplo abreviado anterior. Hicimos que aceptara un argumento, que en este caso puede ser cualquier etiqueta (otros widgets con texto también funcionarían) y lo configuramos cambiando su texto, color de texto y color de fondo.

La parte interesante son las líneas 22 y 23. Aquí, en realidad definimos dos nuevas funciones lambda, que pasan diferentes argumentos a la función color_label() y la ejecutan. De esta manera, podemos evitar invocar la función color_label() en el momento en que se inicializan los botones.

Obtención de la entrada del usuario

Nos estamos acercando al final del segundo artículo de nuestra serie de tutoriales de Tkinter, por lo que en este punto, sería bueno mostrarle una forma de obtener información del usuario de su programa. Para hacerlo, el widget Entrada puede ser útil. Mira el siguiente guió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
33
34
35
36
37
38
import tkinter

root = tkinter.Tk()

def color_label(any_label, user_input):
    any_label.configure(text=user_input, bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame6 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)
frame6.grid(column=0, row=3, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=lambda: color_label(label1, entry.get()))
button2 = tkinter.Button(frame4, text="Configure button 2", command=lambda: color_label(label2, entry.get()))

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

entry = tkinter.Entry(frame6)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')
entry.pack(fill='x')

root.mainloop()

Producción:

{.img-responsive}

Eche un vistazo a las líneas 5 y 6. Como puede ver, el método color_label() ahora acepta un nuevo argumento. Este argumento, una cadena, se usa para modificar el parámetro texto configurado de la etiqueta. Además, en la línea 29 creamos un nuevo widget Entrada (y en la línea 36 lo empaquetamos dentro de un nuevo marco creado en la línea 13).

En las líneas 24 y 25, podemos ver que cada una de nuestras funciones lambda también pasa un argumento adicional. El método get() de la clase Entry devuelve una cadena que es lo que el usuario escribió en el campo de entrada. Entonces, como probablemente ya sospeche, después de hacer clic en los botones "configurar", el texto de las etiquetas asignadas a ellos se cambia por el texto que el usuario escribió en nuestro nuevo campo de entrada.

Conclusión

Espero que esta parte del tutorial llene algunos vacíos en su comprensión del módulo Tkinter. Aunque algunas características avanzadas de Tkinter pueden parecer un poco complicadas al principio, la filosofía general de construir interfaces usando el paquete GUI más popular para Python es muy simple e intuitiva.

Estén atentos a la última parte de nuestro tutorial básico de Tkinter, donde descubriremos algunos atajos muy inteligentes que nos permiten crear interfaces de usuario complejas con un código muy limitado.