Posicionar elementos en Tcl/Tk (tkinter)



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.

Un botón ubicado en forma absoluta en Tcl/Tk (tkinter)

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.

Posición absoluta y tamaño de un elemento en Tcl/Tk (tkinter)

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.

Elementos posicionados vía pack() en Tcl/Tk (tkinter)

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()

Elementos posicionados verticalmente via pack() en Tcl/Tk (tkinter)

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)

Elementos posicionados vía pack() en Tcl/Tk (tkinter)

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)

Posición de los elementos utilizando pack() en Tcl/Tk (tkinter)

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()

Margen externo e interno en Tcl/Tk (tkinter)

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)

Elemento con las propiedades expand y fill en Tcl/Tk (tkinter)

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.

Grilla en Tcl/Tk (tkinter)

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)

Grilla en Tcl/Tk (tkinter)

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)

Grilla en Tcl/Tk (tkinter)

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")

Grilla con el parámetro sticky en Tcl/Tk (tkinter)

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")

Grilla expansible en Tcl/Tk (tkinter)

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.

Dos elementos expansibles en Tcl/Tk (tkinter)

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 vice-versa. 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.

Dos elementos expansibles en Tcl/Tk (tkinter)

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.



Deja un comentario