miércoles, 23 de marzo de 2022

El gestor de geometría Grid.


resultado del diseño


El gestor de geometría Grid con Tkinter y Guizero.

Este gestor de ventanas trata las mismas como si fueran una cuadrícula, formada por filas y columnas. El símil podría ser un tablero de ajedrez, donde se pueden situar las piezas, widgets en nuestro caso, dando la referencia de la columna y fila donde queremos colocarlo. Incluso, podemos indicarle a un elemento que ocupe más de una fila o columna. 

Con este gestor se pueden construir aplicaciones muy complejas, y hacer que los elementos se adapten a la misma, si las ventanas varían su tamaño.


Tkinter.


Grid con ventana no dimensionable. Ejemplo.


resultado con gestor grid


Empezamos con el mismo código que en el capitulo anterior.


from tkinter import *
from tkinter import ttk, font
import getpass

# Gestor de geometría (grid). Ventana no dimensionable

class Aplicacion():
    def __init__(self):
        self.raiz = Tk()
        self.raiz.title("Acceso")

Para hacer que una ventana no sea redimensionable se utiliza el método resizable(0,0) que es la forma abreviada de resizable(width = False, height = False)

                self.raiz.resizable(0,0)
        fuente = font.Font(weight='bold')


Crearemos un marco que contenga al resto de elementos. Este marco tiene un borde de 2 y la opción "relief". Vamos a utilizar esta opción para crear un efecto en 3d del marco.

 La opción relief puede tener los siguientes valores:

- FLAT (llano)

- RAISED (elevado)

- SUNKEN (hundido)

- GROOVE (hendidura)

- RIDGE (borde elevado) 

Luego utilizamos la opción "padding" para conseguir un espacio extra interior y que los widgets no queden pegados al marco. Otra forma sería usar padx y pady con cada uno de los widgets que usamos dentro del marco.

                 self.marco = ttk.Frame(self.raiz, borderwidth=2,
                               relief="raised", padding=(10,10))

Diseñaremos todos los elementos de la ventana con la diferencia de que los vamos a situar dentro del Frame (marco) que hemos creado.

                  self.etiq1 = ttk.Label(self.marco, text="Usuario:", 
                               font=fuente, padding=(5,5))
        self.etiq2 = ttk.Label(self.marco, text="Contraseña:",
                               font=fuente, padding=(5,5))
                               
        # Define variables para las opciones 'textvariable' de
        # cada caja de entrada 'ttk.Entry()'.
        
        self.usuario = StringVar()
        self.clave = StringVar()
        self.usuario.set(getpass.getuser())        
        self.ctext1 = ttk.Entry(self.marco, textvariable=self.usuario, 
                                width=30)
        self.ctext2 = ttk.Entry(self.marco, textvariable=self.clave, 
                                show="*", 
                                width=30)
        self.separ1 = ttk.Separator(self.marco, orient=HORIZONTAL)
        self.boton1 = ttk.Button(self.marco, text="Aceptar", 
                                 padding=(5,5), command=self.aceptar)
        self.boton2 = ttk.Button(self.marco, text="Cancelar", 
                                 padding=(5,5), command=quit)

Ahora viene la parte interesante, que es el colocar los elementos en las casillas o cuadrículas dentro del marco. En realidad habrá dos cuadrículas. Una de una fila por una columna que está en la ventana que ocupará el Frame (marco) y otra de cinco filas por tres columnas que ocuparán el resto de los widgets. Ten en cuenta que las filas y columnas se empiezan a contar en el cero. 

cuadricula de 5 x 3

Para seleccionar la casilla donde irá el widget, usaremos la opción "column" para indicar el número de la columna y la opción "row" para indicar la fila.
La opción "columnspan" indica al gestor cuantas columnas ocupará el widget en cuestión. 
Las cajas para las entradas self.ctext1 y self.ctext2 ocuparán dos columnas y la barra de separación self.separ1 ocupara tres columnas.

                 self.marco.grid(column=0, row=0)
        self.etiq1.grid(column=0, row=0)
        self.ctext1.grid(column=1, row=0, columnspan=2)
        self.etiq2.grid(column=0, row=1)
        self.ctext2.grid(column=1, row=1, columnspan=2)
        self.separ1.grid(column=0, row=3, columnspan=3, sticky="ew")
        self.boton1.grid(column=1, row=4)
        self.boton2.grid(column=2, row=4)

El diseño es algo como esto:

resultado del diseño


El resto del código es igual al del otro capítulo.


# Establece el foco en la caja de entrada de la
        # contraseña.

        self.ctext2.focus_set()
        self.raiz.mainloop()
    
    def aceptar(self):
        if self.clave.get() == 'herodoto':
            print("Acceso permitido")
            print("Usuario:   ", self.ctext1.get())
            print("Contraseña:", self.ctext2.get())
        else:
            print("Acceso denegado")
            self.clave.set("")
            self.ctext2.focus_set()

def main():
    mi_app = Aplicacion()
    return 0

if __name__ == '__main__':
    main()

En el siguiente enlace puedes encontrar el código del programa.


Grid con ventana dimensionable.

Tamaño standar.


Redimensionada.



Lo que vamos a ver ahora es como hacer lo mismo, pero cuando la ventana es dimensionable y tenemos que adaptar los widgets a ese aumento o disminución del tamaño de la ventana que los contiene.

El comienzo es el mismo que en el caso anterior. La parte que variará será a partir de que definamos la posición de los widgets en el marco.

from tkinter import *
from tkinter import ttk, font
import getpass

'''Gestor de geometría (grid). Ventana dimensionable'''


class Aplicacion():

    def __init__(self):
        self.raiz = Tk()
        self.raiz.title("Acceso")
         
        fuente = font.Font(weight='bold')

        self.marco = ttk.Frame(self.raiz, borderwidth=2,
                               relief='raised', padding=(10,10))

#         Define el resto de widgets pero en este caos el primer
#         parámetro indica que se situarán en el widget del marco
#         anterior 'self.marco'.

        self.etiq1 = ttk.Label(self.marco, text="Usuario:",
                               font=fuente, padding=(5,5))
        self.etiq2 = ttk.Label(
            self.marco, text="Contraseña:", font=fuente, padding=(5,5))

#         Define variables para las opciones 'textvariable' de
#         cada caja de entrada 'ttk.Entry()'.

        self.usuario = StringVar()
        self.clave = StringVar()
        self.usuario.set(getpass.getuser())
        self.ctext1 = ttk.Entry(self.marco, textvariable=self.usuario, width=30)
        self.ctext2 = ttk.Entry(self.marco, textvariable=self.clave, show="*", width=30)
        self.separ1 = ttk.Separator(self.marco, orient=HORIZONTAL)
        self.boton1 = ttk.Button(self.marco, text="Aceptar", padding=(5,5), command=self.aceptar)
        self.boton2 = ttk.Button(self.marco, text="Cancelar", padding=(5, 5), command=quit)

Para conseguir que cuando se amplíe o reduzca el tamaño de la ventana los widgets se adapten al mismo hay que introducir el parámetro "sticky", que viene del termino "pegajoso" en inglés. 

Cuando un widget se coloca en su cuadrícula lo hace en la parte central de la misma, la cual se adapta a su tamaño. Al usar este parámetro le estamos diciendo como tiene que comportarse cuando se modifiquen las dimensiones de la ventana. Se usan como valores los puntos cardinales en ingles: N (norte), S (sur), E (este) y W (oeste) solos o de forma combinada. El widget se quedará pegado a los lados de su celda en las direcciones que se indiquen. Aunque esto solo no basta, además de definir los stickies hay que activarlos, lo cual veremos en los siguientes pasos. 

Comencemos definiendo el sticky para cada uno de los elementos

                  self.marco.grid(column=0, row=0, sticky=(N,S,E,W))
        self.etiq1.grid(column=0, row=0, sticky=(N,S,E,W))
        self.ctext1.grid(column=1, row=0, columnspan=2, sticky=(E,W))
        self.etiq2.grid(column=0, row=1, sticky=(N,S,E,W))
        self.ctext2.grid(column=1, row=1, columnspan=2, sticky=(E,W))
        self.separ1.grid(column=1, row=3, columnspan=3, sticky="nsew")
        self.boton1.grid(column=1, row=4,sticky=(E))
        self.boton2.grid(column=2, row=4,sticky=(W))

Para a continuación pasar a activar la opción sticky, cuando la ventana aumente o disminuya de tamaño. La activación se hace por cada contenedor (Frame), y por cada fila y/o columna. Esto se lleva a cabo con el parámetro weight. Lo que vamos a hacer es asignar un peso (relativo) a cada fila o columna que va a servir para redistribuir ese nuevo espacio entre ambas.

Por ejemplo. Cuando se expanda la ventana una columna o fila que tenga un peso 2, crecerá dos veces más rápido que aquella que tenga un peso de 1. El valor predeterminado es el 0, lo que significa que la columna o fila que lo contenga no variará su tamaño. 

Lo habitual es asignar los pesos en las filas o columnas en los que haya widgets. En nuestro ejemplo el código podría quedar de la siguiente forma:

           # Repartimos el peso en cada contenedor.
        self.raiz.columnconfigure(0, weight=1)
        self.raiz.rowconfigure(0, weight=1)

        self.marco.columnconfigure(0, weight=1)
        self.marco.columnconfigure(1, weight=1)
        self.marco.columnconfigure(2, weight=1)
        self.marco.rowconfigure(0, weight=1)
        self.marco.rowconfigure(1, weight=1)
        self.marco.rowconfigure(4, weight=1)

El resto de la aplicación es igual que en el ejemplo anterior.

  # Establece el foco en la caja de entrada de la contraseña

        self.ctext2.focus_set()
        self.raiz.mainloop()

    def aceptar(self):
        if self.clave.get()=='herodoto':
            print('Acceso Permitido')
            print('Usuario:    ', self.ctext1.get())
            print('Contraseña: ', self.ctext2.get())
        else:
            print('Acceso denegado')
            self.clave.set("")
            self.ctext2.focus_set()

def main():
    mi_app = Aplicacion()
    return 0

if __name__=='__main__':
    main()

Puedes encontrar el código de este ejemplo en el siguiente enlace.


El gestor de geometría Grid con Guizero.


gestor de geometría grid con guizero



En este ejemplo vamos a explicar el método Grid en Guizero y utilizaremos un elemento de Tkinter como es el separador. Así veremos como usar elementos de tkinter en guizero y también como funciona el gestor Grid.

Empezamos importando todos los elementos de Guizero y también el elemento "Separator", solo que esta vez de la líbrería tkinter.tkk así como "HORIZONTAL" de Tkinter. Es widget no existe como tal en la librería de Guizero así que vamos a introducirlo de esta forma en el programa.


from guizero import *

#HORIZONTAL is Tkinter's variable. If you want to use it you have to import it or have to use like Tkinter.HORIZONTAL
#If you dont want to add Tkinter then you can do from Tkinter import HORIZONTAL
from tkinter import HORIZONTAL
# CUALQUIER VARIABLE DE TK QUE NO APAREZCA HAY QUE IMPORTARLA. 
from tkinter.ttk import Separator #Importamos el widget que necesitemos de ttk 
from getpass import getuser

def aceptar():
        
        if ctext2.value == 'herodoto':
            print('Acceso Permitido')
            print('Usuario:   ', ctext1.value)
            print('Contraseña: ', ctext2.value)
            ctext2.value="" #Borra el contenido del 2º cuadro de texto
            quit() #Sale del programa
        else:
            print('Acceso denegado')
            ctext2.value="" # Borra el contenido del 2º cuadro de texto si se falla.

usuario = getuser() # Captura el nombre del usuario que usa el sistema.

En Guizero tenemos también que especificar al contenedor cual es el gestor de ventanas que se va a utilizar. Si no ponemos nada por defecto es Pack.

Al igual que en los ejemplos anteriores vamos a usar una cuadrícula de tres columnas por cinco filas. El orden de colocación en el Grid también es (nº columna, nº fila). Ahora bien a diferencia de tkinter aquí definimos y colocamos el widget de una sola vez, en una instrucción. 

Para decir que un contenedor usa un gestor de geometría especifico se utiliza el parámetro "layout". En este caso como usamos el metodo Grid, el contenedor será Box(raiz, layout="grid").

A partir de ahí, al igual que en Tkinter, colocamos todos los widgets usando coordenadas. Aquí se usa el parámetro grid(nº columna, nº fila) empezando a contar desde el cero, al igual que anteriormente.

La forma de expandir los widgets para que ocupen varias casillas es un poco distinta a Tkinter. En vez de utilizar columspan o rowspan para ampliar las columnas que ocupa el widget, aquí se funciona de la siguiente forma.

Imagina que colocamos cualquier elemento en la columna 1 y en la fila 1. Se utilizaría para posicionarlo grid = [1,1]. Ahora bien si ese elemento tiene que expandirse a otras filas o columnas se utiliza la siguiente expresión:

grid = [1, 1, 1+[columnas a expandir], 1+(filas a expandir)]

Para que se entienda mejor. Si ese elemento que tenemos en la casilla [1, 1] queremos que ocupe dos columnas (la suya por colocación y una más hacia la derecha) usaremos grid = [1,1, 2, 1]. Si lo que queremos es que ocupe dos filas (la suya por colación y una fila más abajo) entonces usaremos grid = [1,1, 1, 2 ].

Dicho lo cual el resto del código es el siguiente.

raiz = App(width=390, height=120, title="Acceso")
raiz.tk.resizable(0, 0) # Es otra forma de decirle que la ventana es fija y no se puede cambiar el tamaño.

marco_sup = Box(raiz) # Creamos un marco normal, que se coloca arriba
Text(marco_sup, text="", size=5) # Pongo esto para separar lo siguiente del borde superior.

marco = Box(raiz, layout="grid") # el marco usará (3 columnas x 5 filas) grid.
# el tamaño de cada celda viene determinado por lo que contiene dentro. 

etiq1 = Text(marco, text="Usuario", grid=[0, 0], align='left', font='padmaa-Bold.1.1', size=14)
ctext1 = TextBox(marco, grid=[1, 0, 2, 1], width=30, align='left') # lo alineamos a la izquierda dentro del grid.
etiq1 = Text(marco, text="Contraseña", grid=[0, 1], align='left', font='padmaa-Bold.1.1', size=14) 
# size es el tamaño de la letra
ctext2 = TextBox(marco, grid=[1, 1, 2, 1], width=30, hide_text=True) 
# hide_text=True cambia las letras por *
# grid=[1,1,2,1] situa el grid en la columna 1 y fila 1 y luego lo expande 1 columnas y 0 fila (sumamos 1 a lo q queremos expandir)
# El grid no admite [1,1,0,0], xq por defecto  el grid [1,1] es como si fuera [1,1,1,1]
#x lo que expandir una columna seria [1,1,2,1] o expandir una fila [1,1,1,2]
#------------------------------------------------------------------------------------------

separ1 = Separator(marco.tk, orient=HORIZONTAL)
separ1.grid(column=1, row=3, columnspan=3, sticky="ew")
'''Para que un widget de tk o ttk funcione en guizero hay que diseñarlo y luego pack o grid o el gestor de
geometria que se use'''
#--------------------------------------------------------------------------------------------
boton1 = PushButton(marco, grid=[1, 4], text='Aceptar', padx=16, pady=6, command=aceptar)
boton2 = PushButton(marco, grid=[2, 4], text='Cancelar', padx=16, pady=6, command=quit)

ctext1.value=usuario
ctext2.focus() # Pone el foco en el cuadro 2 donde pondremos la contraseña.


raiz.display()

Nota: cualquier elemento de Guizero se basa en un widget de tkinter. Por eso al usar el separador que es un elemento de tkinker.tkk lo colocamos en el contenedor usando marco.tk y no solo el nombre marco.
Esto también ocurre si queremos utilizar propiedades de Tkinter en un elemento de Guizero, tendremos que usar la variable al que hayamos asignado el widget + sufijo.tk y el método de TKinter que queramos utilizar.


Puedes encontrar el código de este ejercicio en el siguiente enlace.

No hay comentarios:

Publicar un comentario