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