Tk provee tres métodos para establecer la posición de los controles o widgets dentro de una ventana, que se corresponden con las funciones pack()
, place()
y grid()
. Algunos son más versátiles, otros más restrictivos. ¿Cuál debes usar? Dependerá del resultado que quieras conseguir. Hagamos un repaso por cada uno de ellos y pon a prueba tu criterio. (Aunque claro, te daremos algunos consejos).
Cabe aclarar que no deben mezclarse distintos métodos dentro de una misma aplicación.
Posición absoluta (place
)
La función place()
permite ubicar elementos indicando su posición (X e Y) respecto de un elemento padre. En general casi todas las librerías gráficas proveen una opción de este tipo, ya que es la más intuitiva. Para ver un ejemplo, consideremos el siguiente código.
#!/usr/bin/env python # -*- coding: utf-8 -*- import tkinter as tk from tkinter import ttk class Application(ttk.Frame): def __init__(self, main_window): super().__init__(main_window) main_window.title("Posicionar elementos en Tcl/Tk") main_window.configure(width=300, height=200) # Ignorar esto por el momento. self.place(relwidth=1, relheight=1) main_window = tk.Tk() app = Application(main_window) app.mainloop()
Este pequeño programa simplemente crea una ventana (main_window
) con un widget padre (Application
que hereda de ttk.Frame
) que contentrá al resto de los elementos. Tanto la ventana principal como el elemento padre tienen un tamaño de 300×200 píxeles.
Ahora bien, vamos a crear un botón y lo vamos a ubicar en la posición (60, 40)
.
self.button = ttk.Button(self, text="Hola, mundo!") self.button.place(x=60, y=40)
Ya que el origen de coordenadas (es decir, la posición (0, 0)
) es la esquina superior izquierda, esto quiere decir que entre el borde izquierdo de la ventana y nuestro botón habrá una distancia de 60 píxeles y entre el borde superior de la ventana y el botón habrá 40 píxeles.
Es posible indicar el tamaño de cualquier otro elemento de Tk utilizando los parámetros width
y height
, que indican el ancho y el alto en píxeles.
self.button.place(x=60, y=40, width=100, height=30)
La siguiente imagen ilusta cómo influyen los cuatro argumentos (x
, y
, width
, height
) en la posición y el tamaño del widget.
Estas cuatro propiedades también pueden formularse en términos de proporción respecto del elemento padre. Por ejemplo, podemos decirle a Tk que el tamaño del botón debe ser la mitad del tamaño de la ventana.
self.button.place(relwidth=0.5, relheight=0.5)
De este modo, cuando la ventana se expanda o se contraiga, Tk automáticamente ajustará su tamaño para que cumpla con la proporción indicada.
Esto explica por qué utilizamos la siguiente línea para que el marco de la aplicación (que es una instancia de ttk.Frame
y que nos sirve como elemento padre) tenga siempre el mismo tamaño de la ventana.
self.place(relwidth=1, relheight=1)
Del mismo modo operan relx
y rely
, que expresan la posición de un elemento en términos proporcionales.
self.button.place(relx=0.1, rely=0.1, relwidth=0.5, relheight=0.5)
Así, al abrir el programa, cuando el tamaño de la ventana es de 300×200, el botón se encontrará en la posición (30, 20)
, ya que 300x0.1 = 30
y 200x0.1 = 20
. A medida que el tamaño de la ventana cambie, Tk actualizará la posición del botón para que siempre cumpla con el 10% de la medida.
relwidth
, relheight
, relx
y rely
aceptan valores entre 0
y 1
.
El método place()
para posicionar elementos es bastante sencillo de comprender, sobre todo para usuarios de otras librerías y otros lenguajes con los que hayan desarrollado aplicaciones de escritorio. Brinda exactitud en cada uno de los objetos de nuestra interfaz y puede resultar útil en muchos casos.
El principal problema radica al momento de expandir o contraer la ventana. Si bien los argumentos proporcionales descriptos anteriormente pueden ser útiles, generalmente resultan no ser suficientes. Ubicar elementos de forma absoluta (indicando su posición como coordenadas X e Y) implica una ventana estática, que generará espacios vacíos cuando el usuario la agrande o bien se perderán algunos elementos de la vista cuando ésta se contraiga.
Posicionamiento relativo (pack
)
Este método es el más sencillo de los tres. En lugar de especificar las coordenadas de un elemento, simplemente le decimos que debe ir arriba, abajo, a la izquierda o a la derecha respecto de algún otro control o bien la ventana principal.
A pesar de su sencillez es muy potente y, dentro de sus limitaciones, puede resolver interfaces de usuario complejas sin perder versatilidad.
class Application(ttk.Frame): def __init__(self, main_window): super().__init__(main_window) main_window.title("Posicionar elementos en Tcl/Tk") self.entry = ttk.Entry(self) self.entry.pack() self.button = ttk.Button(self, text="Hola, mundo!") self.button.pack() self.pack()
En el ejemplo, creamos una caja de texto y un botón y los ubicamos en la ventana vía la función pack
. Como no indicamos ningún argumento, por defecto Tk posicionará los elementos uno arriba del otro, como se observa en la imagen.
De modo que si añadimos otro elemento, por ejemplo, una etiqueta (ttk.Label
), será ubicado debajo del botón.
self.entry = ttk.Entry(self) self.entry.pack() self.button = ttk.Button(self, text="Hola, mundo!") self.button.pack() self.label = ttk.Label(self, text="...desde Tkinter!") self.label.pack()
La propiedad que controla la posición relativa de los elementos es side
, que puede equivaler a tk.TOP
(por defecto), tk.BOTTOM
, tk.LEFT
o tk.RIGHT
. De este modo, si indicamos que la caja de texto debe ir ubicada a la izquierda, los otros dos controles se seguirán manteniendo uno arriba del otro.
self.entry = ttk.Entry(self) self.entry.pack(side=tk.LEFT)
Del mismo modo, usando side=tk.RIGHT
produce el efecto contrario, posicionando la caja de texto a la derecha del botón y de la etiqueta.
Te propongo que intentes por tu cuenta distintos valores para el parámetro side
para comprender mejor cómo se comporta Tk.
La función pack
también admite los parámetros after
y before
, que nos permiten controlar el orden en el que se ubican los elementos en la ventana. El siguiente ejemplo obliga a Tk a colocar la etiqueta self.label
antes (before) que la caja de texto.
self.entry = ttk.Entry(self) self.entry.pack() self.button = ttk.Button(self, text="Hola, mundo!") self.button.pack() self.label = ttk.Label(self, text="...desde Tkinter!") self.label.pack(before=self.entry)
Tanto before
como after
aceptan como valor cualquier widget para tomar como referencia.
Otras propiedades incluyen padx
, ipadx
, pady
y ipady
que especifican (en píxeles) los margenes externos e internos de un elemento. Por ejemplo, en el siguiente código habrá un espacio de 30 píxeles entre el botón y la ventana (margen externo), pero un espacio de 50 píxeles entre el borde del botón y el texto del mismo (margen interno).
def __init__(self, main_window): super().__init__(main_window) main_window.title("Posicionar elementos en Tcl/Tk") self.button = ttk.Button(self, text="Hola, mundo!") self.button.pack(padx=30, pady=30, ipadx=50, ipady=50) self.pack()
Por último, es posible especificar qué elementos deben expandirse o contraerse a medida que el tamaño de la ventana cambia, y en qué sentido deben hacerlo (vertical u horizontal), vía las propiedades expand
y fill
.
self.button = ttk.Button(self, text="Hola, mundo!") self.button.pack(expand=True, fill=tk.X) self.pack(expand=True, fill=tk.BOTH)
En el ejemplo, le indicamos al elemento padre (self
) que ocupe todo el tamaño posible (expand=True
) y que lo haga en ambas direcciones (fill=tk.BOTH
). El botón, por otra parte, únicamente ajustará su tamaño horizontal (fill=tk.X
). Si quisiéramos que se expanda solo de forma vertical, la propiedad sería fill=tk.Y
o fill=tk.BOTH
para expandirse en ambos sentidos.
self.button.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
Manejo en forma de grilla (grid
)
El método de grilla siempre es una buena elección, desde pequeñas hasta grandes y complejas interfaces. Consiste en dividir conceptualmente la ventana principal en filas (rows) y columnas (columns), formando celdas en donde se ubican los elementos. Veamos un ejemplo.
class Application(ttk.Frame): def __init__(self, main_window): super().__init__(main_window) main_window.title("Posicionar elementos en Tcl/Tk") main_window.columnconfigure(0, weight=1) main_window.rowconfigure(0, weight=1) self.entry = ttk.Entry(self) self.entry.grid(row=0, column=0) self.button = ttk.Button(self, text="Presione aquí") self.button.grid(row=0, column=1) self.label = ttk.Label(self, text="¡Hola, mundo!") self.label.grid(row=1, column=0) self.grid(sticky="nsew")
Por el momento vamos a concentrarnos únicamente en el código entre las líneas 9 y 16. Allí se crean tres controles (una caja de texto, un botón y una etiqueta) y vía la función grid
se especifica su posición en la grilla.
Como se observa en la imagen, nuestra grilla consta hasta el momento de dos filas y dos columnas, dando un total de (dos por dos es cuatro, ¿no?) cuatro celdas.
La caja de texto está en la columna 0 y la fila 0. Siguiendo esta convención, el botón está en la celda (1, 0)
y la etiqueta en la posición (0, 1)
. La celda (1, 1)
no contiene ningún elemento. Una grilla puede tener tantas columnas y filas como queramos.
Podemos indicarle a un elemento que debe ocupar más de una fila o columna. Por ejemplo, ya que la celda (1, 1)
está vacía, nuestra etiqueta podría ocuparla para que el diseño sea más agradable.
self.label.grid(row=1, column=0, columnspan=2)
columnspan
indica cuántas columnas debe ocupar el control (por defecto 1). El parámetro rowspan
opera de forma similar para las filas.
Por defecto las columnas y las filas no se expanden o contraen si la ventana cambia su tamaño. Para esto, usamos las funciones rowconfigure
y columnconfigure
con el parámetro weight
. Por ejemplo, el siguiente código indica que la columna 0 y la fila 0 deben expandirse.
# Expandir horizontalmente a columna 0. self.columnconfigure(0, weight=1) # Expandir verticalmente la fila 0. self.rowconfigure(0, weight=1)
La imagen muestra cómo se ha expandido la celda una vez agrandada la ventana y cómo nuestra caja de texto se mantuvo en el centro. Para que el elemento se posicione arriba, abajo, a la derecha o izquierda de la celda que lo contiene, podemos usar el parámetro sticky
con las opciones "n"
(norte), "s"
(sur), "e"
(este) o "w"
(oeste), respectivamente.
# Anclar en la parte superior (n) de la celda. self.entry.grid(row=0, column=0, sticky="n")
Combinando estas propiedades, podemos lograr que el widget se expanda de forma horizontal ("ew"
), vertical ("ns"
) o en ambas direcciones ("nsew"
).
# Expandir en todas las direcciones. self.entry.grid(row=0, column=0, sticky="nsew")
La función grid
acepta, al igual que pack()
, los argumentos padx
, pady
, ipadx
, ipady
para establecer márgenes.
self.entry.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
Por último, el método de grillas en Tk permite configurar en qué medida se expanden las columnas y filas. Por ejemplo, consideremos el siguiente código.
def __init__(self, main_window): super().__init__(main_window) main_window.title("Posicionar elementos en Tcl/Tk") main_window.columnconfigure(0, weight=1) main_window.rowconfigure(0, weight=1) self.label1 = tk.Label( self, text="¡Hola, mundo!", bg="#FFA500") self.label1.grid(row=0, column=0, sticky="nsew") self.label2 = tk.Label( self, text="¡Hola, mundo!", bg="#1E90FF") self.label2.grid(row=1, column=0, sticky="nsew") self.grid(sticky="nsew") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.rowconfigure(1, weight=1)
En esta ventana creamos dos etiquetas y las ubicamos en la misma columna (0) pero en diferentes filas (0 y 1). Luego, vía rowconfigure
y columnconfigure
indicamos que deben expandirse y contraerse junto con la ventana.
La imagen nos muestra cómo los dos elementos comparten el espacio disponible, de modo que uno siempre tiene el mismo tamaño que el otro, independientemente de cuán chica o grande sea la ventana. Pero en ocasiones es deseable que un control se expanda más que otro y viceversa. Para esto, podemos aumentar el «peso» (weight
) que ejerce una fila o columna a Tk al momento de expandirse.
self.rowconfigure(0, weight=5) self.rowconfigure(1, weight=1)
Con esta configuración, la fila 0 (correspondiente a la etiqueta naranja) siempre tendrá un tamaño cinco veces mayor a la fila 1, como observamos a continuación.
Conclusión
El método place()
brinda total control sobre la ubicación de cada uno de los elementos, pues su posición es absoluta. Esto es generalmente conveniente para pequeñas y medianas interfaces que se mantegan estáticas y no tengan aspiración de ser expandidas.
pack()
, por su parte, es bastante sencillo de utilizar y con él pueden obtenerse interfaces ricas y complejas. Sin embargo, el hecho de que la posición de cada widget dependa de otro puede generar complicaciones al momento de realizar cambios, sobre todo modificaciones a aplicaciones ya existentes.
Por último, el manejo a través de una grilla, como comentaba anteriormente, es siempre una buena elección. Conociendo todas sus propiedades y aplicándolas cuidadosamente podremos obtener desde pequeñas hasta grandes interfaces de usuario completamente adaptables y fáciles de utilizar.
Curso online 👨💻
¡Ya lanzamos el curso oficial de Recursos Python en Udemy!
Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter
en 2024.
Consultoría 💡
Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.
JC says:
Intenté utilizar el after y before en la función pack() y no me funcionó, estoy utilizando python 3, busqué en la documentación y no aparece, ¿Aplica para todas las versiones de python?
JC says:
En tu código si funciona, pero en el mio no, es bastante sencillo, ¿Sabes si lo estoy aplicando mal? :
from tkinter import *
root = Tk()
root.title(«Función Pack»)
entry = Entry(root).pack()
button = Button(root,text=»Hola Mundo»).pack()
label = Label(root,text=»…desde Tkinter!»).pack(before=entrada)
root.mainloop()
JC says:
* Corrigiendo:
label = Label(root,text=»…desde Tkinter!»).pack(before=entry)
y no funciona
Recursos Python says:
Hola. Tu error está en utilizar
pack()
en la misma línea en que creás el control. Debería ser:from tkinter import *
root = Tk()
root.title("Función pack")
entry = Entry(root)
entry.pack()
button = Button(root,text="Hola Mundo")
button.pack()
label = Label(root,text="…desde Tkinter!")
label.pack(before=entry)
root.mainloop()
Saludos
JC says:
Oh ya veo, ¡Muchas gracias por tu respuesta!
Saludos
Jhovany says:
gracias por la informacion, si es posible una pregunta por favor, cuando tengo un widgets y en él quiero introducir una lista, por ejmplo (1, 2, 3, 4, 5, 6, 7), para poder hacer operaciones en funciones con otras listas o elementos, al ejecutar no reconoce la lits, sale diferentes errores dependiendo del valos del widgets si lo cambio a IntVar o DoubleVar o StringVAr, a que se debe esto? gracias
[------] says:
Gracias por la informacion que publicas me ayudo mucho 🙂
Javier says:
Realmente bueno.
Muchas gracias por tu aporte.
Bien explicado con gráficos y ejemplos.
Soy ya un seguidor tuyo.
Una buena idea es incluir un tutorial de tkinter Menu con submenus y elementos desplegables.
Recursos Python says:
Gracias Javier. Saludos