Bucle autogenerador de BOTONES y DESTRUIR ventanas con Tkinter y CustomTkinter

Bucle autogenerador de BOTONES y DESTRUIR ventanas con Tkinter y CustomTkinter

Esta vez, vamos a crear la ventana principal que se abre después de la de login. Para ello, crearemos otra clase. En ella, pondremos una serie de botones de Tkinter y CustomTkinter con un bucle autogenerador.


Como expliqué en el capítulo anterior, si cerramos la ventana principal, se cierra todo el programa, pero lo que quiero hacer yo, es crear una nueva ventana, que también sea principal, ya que quiero que la del login se cierre al acceder al programa.

¿Es posible tener más de una ventana principal en Tkinter o en CustomTkinter?

El problema es que Tkinter, solo quiere una ventana principal a la vez (Tk()) lo mismo con CustomTkinter, solo que con CTk(). Luego, podemos tener las Toplevel() que queramos, pero yo quiero que se destruya la ventana principal (login) y que se cree otra ventana principal más con todas las opciones del programa, pero sin finalizar la ejecución total del programa.

Puede parecer supercomplicado, pero este reto se soluciona fácilmente con clases. Vamos a crear una nueva clase, que contendrá el código de la ventana de opciones del programa.

Clase VentanaOpciones

Empecemos por la declaración de la clase:

class VentanaOpciones:

Esta ventana tendrá en concreto 13 botones, todos iguales, solo cambiarán dos cosas, el texto que se mostrará encima de ellos y el command (llamada a función) que harán. Por este motivo, podemos crear los botones a partir de un bucle que los autogenere, con el fin de no tener 13 parrafadas casi idénticas.

Para este propósito, necesitaremos un diccionario de Python. Las claves podrán ser el valor "text" del botón y los valores, las diferentes llamadas a funciones (command).

Sin embargo, para este capítulo, utilizaremos solo una lista con los valores de texto de los botones. El objetivo de hoy, es, al menos, generar los botones, colocarlos en grid() automáticamente y mostrarlos con su correspondiente valor de texto. Los "command" para las llamadas a funciones, los haremos en otro capítulo (todavía ni tenemos las ventanas necesarias para ejecutar con los botones).

La lista puede quedar así. En total, son 13 botones (uno para cada método del archivo con la lógica):

# Lista de textos de los botones
botones = ['Consulta SQL', 'Mostrar Bases de Datos','Eliminar Bases de Datos', 'Crear Bases de Datos', 'Crear Respaldos', 'Crear Tablas', 'Eliminar Tablas', 'Mostrar Tablas', 'Mostrar Columnas', 'Insertar Registros', 'Eliminar Registros', 'Vaciar Tablas', 'Actualizar Registros']

Método __init__()

Ahora, vamos a añadir el método __init__() que cree la nueva ventana principal en la clase "VentanaOpciones".

	def __init__(self):
        self.root = ctk.CTk()
        self.root.title("Opciones para trabajar con bases de datos.")

        #Contador para la posición de los botones
        contador = 0

        # Crea los botones y establece su texto
        for texto_boton in self.botones:
            button = ctk.CTkButton(
                master=self.root,
                text=texto_boton,
                height=25,
                width=200
            )
            button.grid(row=contador//3, column=contador%3, padx=5, pady=5)
        
            # Incrementa el contador
            contador += 1
	self.root.mainloop()

Se declara e inicializa en cero un contador en la línea 6. Se trata de una variable que se utiliza para ir contabilizando la cantidad de iteraciones que se hacen en el bucle for. En este caso, el contador se usa para determinar la posición de los botones que se generan en el bucle. Al iniciar en cero y aumentar su valor en uno en cada iteración, se usa para calcular la fila y la columna en la que se deben colocar los botones en el "grid" que se está construyendo en la ventana.

Por ejemplo, al iniciar el bucle for, el contador es 0 y se coloca el primer botón en la fila 0 y columna 0. En la segunda iteración, el contador es 1 y se coloca el botón en la fila 0 y columna 1. En la tercera iteración, el contador es 2 y se coloca el botón en la fila 0 y columna 2. En la cuarta iteración, el contador es 3 y se coloca el botón en la fila 1 y columna 0, y así sucesivamente hasta completar todas las iteraciones del bucle.

El texto se carga cada iteración con el valor del iterador "texto_boton" (una posición de la lista cada vez).

Lista de iteraciones

Para que entiendas bien este cálculo, te dejo un listado con todas las operaciones que realiza esta línea:

button.grid(row=contador//3, column=contador%3, padx=5, pady=5)
  • Iteración 1: row=0, column=0. El botón se coloca en la fila 0, columna 0. contador (0) // 3 es 0, contador % 3 es 0.
  • Iteración 2: row=0, column=1. El botón se coloca en la fila 0, columna 1. contador (1) // 3 es 0, contador % 3 es 1.
  • Iteración 3: row=0, column=2. El botón se coloca en la fila 0, columna 2. contador (2) // 3 es 0, contador % 3 es 2.
  • Iteración 4: row=1, column=0. El botón se coloca en la fila 1, columna 0. contador (3) // 3 es 1, contador % 3 es 0.
  • Iteración 5: row=1, column=1. El botón se coloca en la fila 1, columna 1. contador (4) // 3 es 1, contador % 3 es 1.
  • Iteración 6: row=1, column=2. El botón se coloca en la fila 1, columna 2. contador (5) // 3 es 1, contador % 3 es 2.
  • Iteración 7: row=2, column=0. El botón se coloca en la fila 2, columna 0. contador (6) // 3 es 2, contador % 3 es 0.
  • Iteración 8: row=2, column=1. El botón se coloca en la fila 2, columna 1. contador (7) // 3 es 2, contador % 3 es 1.
  • Iteración 9: row=2, column=2. El botón se coloca en la fila 2, columna 2. contador (8) // 3 es 2, contador % 3 es 2.
  • Iteración 10: row=3, column=0. El botón se coloca en la fila 3, columna 0. contador (9) // 3 es 3, contador % 3 es 0.
  • Iteración 11: row=3, column=1. El botón se coloca en la fila 3, columna 1. contador(10) // 3 es 3, contador % 3 es 1.
  • Iteración 12: row=3, column=2. El botón se coloca en la fila 3, columna 2. contador (11) // 3 es 3, contador % 3 es 2.
  • Iteración 13: row=4, column=0. El botón se coloca en la fila 4, columna 0. contador (12) // 3 es 4, contador % 3 es 0.

¿Como se llega a la conclusión de parar el bucle for sin un condicional o un len()?

Por si te preguntas como sabe el bucle for que se debe detener en la iteración 12, es por lo que expliqué del "for in" de Python, este itera la lista hasta su último elemento sin necesidad de añadir un "len()" para obtener la longitud del índice.

Entonces, quiere decir, que si más adelante le vas añadiendo botones a la lista (imágenes en este caso), el bucle seguirá generando los botones necesarios.

Destruir ventanas en Tkinter y CustomTkinter

Antes de terminar y probar la nueva ventana, debes tener en cuenta lo que te he comentado que no puedes tener dos ventanas (Tk() o CTk() (principales)). Entonces, antes de instanciar esta nueva ventana, hay que destruir la ventana de login.


Para ello, Vamos al método validar de la clase Login. En las dos últimas líneas, he añadido el "destroy()" y acto seguido, se instancia la nueva ventana, la cual, ya lleva su iniciación de ventana y su propio "mainloop()". Fíjate en las líneas de al 21 a la 24.

    # Función para validar el login
    def validar(self):
        obtener_usuario = self.usuario.get() # Obtenemos el nombre de usuario
        obtener_contrasena = self.contrasena.get() # Obtenemos la contraseña
        
        # Verifica si el valor que tiene el usuario o la contraseña o ambos no coinciden
        if obtener_usuario != acceso_bd["user"] or obtener_contrasena != acceso_bd["password"]:
            # En caso de tener ya un elemento "info_login" (etiqueta) creado, lo borra
            if hasattr(self, "info_login"):
                self.info_login.destroy()
            # Crea esta etiqueta siempre que el login sea incorrecto
            self.info_login = ctk.CTkLabel(self.root, text="Usuario o contraseña incorrectos.")
            self.info_login.pack()
        else:
            # En caso de tener ya un elemento "info_login" (etiqueta) creado, lo borra
            if hasattr(self, "info_login"):
                self.info_login.destroy()
            # Crea esta etiqueta siempre que el login sea correcto
            self. info_login = ctk.CTkLabel(self.root, text=f"Hola, {obtener_usuario}. Espere unos instantes...")
            self.info_login.pack()
            # Se destruye la ventana de login
            self.root.destroy()
            # Se instancia la ventana de opciones
            ventana_opciones = VentanaOpciones()

Probemos desde app.py.

El programa empieza por el login, así que dejamos esto como estaba anteriormente en capítulos anteriores:

#Importaciones
import bd.base_datos as sqlbd
import bd.tablas as tbl
import interfaz.interfaz_grafica as gui

base_datos = sqlbd.BaseDatos(**sqlbd.acceso_bd)

ventana_login = gui.Login()

Pues ahí tenemos con poco código un montón de botones para todas las opciones de nuestro programa.

Misión cumplida. En el siguiente capítulo, empezaremos ya a crear las 13 ventanas que utilizarán todas estas funcionalidades del programa. Te dejo el código completo de la interfaz por si te has liado con algo:

#Importaciones
import customtkinter as ctk
import os
from PIL import ImageTk, Image
from bd.base_datos import acceso_bd

# Configuraciones globales para la aplicación

# ---> Rutas
# Carpeta principal del proyecto
carpeta_principal = os.path.dirname(__file__)
# Carpeta de imágenes
carpeta_imagenes = os.path.join(carpeta_principal, "imagenes")

# Modo de color y tema
ctk.set_appearance_mode("system")
ctk.set_default_color_theme("blue")

class Login:
    def __init__(self):
        # Creación de la ventana principal
        self.root = ctk.CTk() # Instancia
        self.root.title("Programación Fácil - Proyecto de bases de datos") # Título
        self.root.iconbitmap(os.path.join(carpeta_imagenes, "logo.ico")) # Icono
        self.root.geometry("400x500") # Tamaño de la ventana
        self.root.resizable(False,False) # Bloqueo de redimensión de ventana en alto y ancho
        
        # Contenido de la ventana principal
        # Carga de la imagen
        logo = ctk.CTkImage(
            light_image=Image.open((os.path.join(carpeta_imagenes, "logo_claro.png"))), # Imagen modo claro
            dark_image=Image.open((os.path.join(carpeta_imagenes, "logo_oscuro.png"))), # Imagen modo oscuro
            size=(250, 250)) # Tamaño de las imágenes
        
        # Etiqueta para mostrar la imagen
        etiqueta = ctk.CTkLabel(master=self.root,
                               image=logo,
                               text="")
        etiqueta.pack(pady=15)

        # Campos de texto
        # Usuario
        ctk.CTkLabel(self.root, text="Usuario").pack()
        self.usuario = ctk.CTkEntry(self.root)
        self.usuario.insert(0, "Nombre de usuario")
        self.usuario.bind("<Button-1>", lambda e: self.usuario.delete(0, 'end'))
        self.usuario.pack()

        # Contraseña
        ctk.CTkLabel(self.root, text="Contraseña").pack()
        self.contrasena = ctk.CTkEntry(self.root)
        self.contrasena.insert(0, "*******")
        self.contrasena.bind("<Button-1>", lambda e: self.contrasena.delete(0, 'end'))
        self.contrasena.pack()
        # Botón de envío
        ctk.CTkButton(self.root, text="Entrar", command=self.validar).pack(pady=10)

        # Bucle de ejecución
        self.root.mainloop()
        
    # Función para validar el login
    def validar(self):
        obtener_usuario = self.usuario.get() # Obtenemos el nombre de usuario
        obtener_contrasena = self.contrasena.get() # Obtenemos la contraseña
        
        # Verifica si el valor que tiene el usuario o la contraseña o ambos no coinciden
        if obtener_usuario != acceso_bd["user"] or obtener_contrasena != acceso_bd["password"]:
            # En caso de tener ya un elemento "info_login" (etiqueta) creado, lo borra
            if hasattr(self, "info_login"):
                self.info_login.destroy()
            # Crea esta etiqueta siempre que el login sea incorrecto
            self.info_login = ctk.CTkLabel(self.root, text="Usuario o contraseña incorrectos.")
            self.info_login.pack()
        else:
            # En caso de tener ya un elemento "info_login" (etiqueta) creado, lo borra
            if hasattr(self, "info_login"):
                self.info_login.destroy()
            # Crea esta etiqueta siempre que el login sea correcto
            self. info_login = ctk.CTkLabel(self.root, text=f"Hola, {obtener_usuario}. Espere unos instantes...")
            self.info_login.pack()
            # Se destruye la ventana de login
            self.root.destroy()
            # Se instancia la ventana de opciones
            ventana_opciones = VentanaOpciones()

class VentanaOpciones:
    # Lista de textos de los botones
    botones = ['Consulta SQL', 'Mostrar Bases de Datos','Eliminar Bases de Datos', 'Crear Bases de Datos', 'Crear Respaldos', 'Crear Tablas', 'Eliminar Tablas', 'Mostrar Tablas', 'Mostrar Columnas', 'Insertar Registros', 'Eliminar Registros', 'Vaciar Tablas', 'Actualizar Registros']
    
    def __init__(self):
        self.root = ctk.CTk()
        self.root.title("Opciones para trabajar con bases de datos.")

        #Contador para la posición de los botones
        contador = 0

        # Crea los botones y establece su texto
        for texto_boton in self.botones:
            button = ctk.CTkButton(
                master=self.root,
                text=texto_boton,
                height=25,
                width=200
            )
            button.grid(row=contador//3, column=contador%3, padx=5, pady=5)
        
            # Incrementa el contador
            contador += 1
            

        self.root.mainloop()

Una última cosa. Mientras estemos haciendo pruebas, puedes dejar el login deshabilitado con un pequeño truco, para no tener que iniciar sesión cada vez que pruebes el programa. En el if para validar, invierte su expresión de diferente que por igual que. Así, cualquier contraseña y usuario que no sean los correctos, te dejarán pasar.

if obtener_usuario == acceso_bd["user"] or obtener_contrasena == acceso_bd["password"]:

No te pierdas nada del curso Máster en Python.


3 comentarios en «0»

  1. Hola! Estoy haciendo tus videos y cursos hace un par de meses y queria darte las gracias. Asi que… Gracias!
    Tambien queria preguntarte: Al cerrar la ventana de Login y abrirse la de Opciones salta el error:
    invalid command name «2647471714432update»
    while executing
    «2647471714432update»
    Estuve buscando y parece que es un problema de mainloop(). El error salta al ejecutarse la variable ventana_opciones al final del metodo Validar. Se abre un mainloop antes de cerrar el anterior.

  2. Quique, el direccionamiento desde el menú del curso hasta está correcto, manda a la pagina del tema anterior, como ya te había comentado te pasa en varios temas.
    Yo he tenido que escribir manualmente la dirección que he averiguado por lógica, pero entiendo que no es tu idea original jaja, así que te vuelvo a dejar aquí el aviso.

    1. Hola, nuevamente, gracias por avisar Javi. Qué desastre de menú, jajaja. He reparado el enlace que me has indicado, el de este capítulo y he revisado los que le siguen. Tendré que revisar bien capítulo por capítulo para ver si hay más fallos. De todas formas, creo que voy a pasar este menú a una página de mi nueva web y lo quitaré de WordPress para que sea todo más limpio y claro.
      Nuevamente, muchas gracias por la info y disculpa las molestias.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

programación Entrada anterior Las variables en programación con ejemplos
curso sql Entrada siguiente Introducción a las consultas SELECT de SQL