En uno de los capítulos anteriores, empezamos a crear la ventana de Login. Lo hicimos con Tkinter, pero me pedisteis que lo hiciéramos con CustomTkinter, pues vuestros deseos son órdenes. Vamos a modificar la ventana de Login hecha con Tkinter y la adaptaremos de forma sencilla a CustomTkinter.
Ventana de Login con Tkinter
Esta es la ventana hecha con Tkinter:
#Importaciones import tkinter as tk import os from PIL import ImageTk, Image # ---> Rutas # Carpeta principal del proyecto carpeta_principal = os.path.dirname(__file__) # Carpeta de imágenes carpeta_imagenes = os.path.join(carpeta_principal, "imagenes") # Ventana de Login class Login: def __init__(self): # Creación de la ventana principal self.root = tk.Tk() self.root.title("Programación Fácil - Proyecto de bases de datos") self.root.iconbitmap(os.path.join(carpeta_imagenes, "logo.ico")) self.root.geometry("450x450") # Contenido de la ventana principal # Logo logo = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_imagenes, "logo.png"))) tk.Label(self.root, image=logo).pack() # Campos de texto # Usuario tk.Label(self.root, text="Usuario").pack() self.usuario = tk.Entry(self.root) self.usuario.insert(0, "Ej:Laura") self.usuario.bind("<Button-1>", lambda e: self.usuario.delete(0, tk.END)) self.usuario.pack() # Contraseña tk.Label(self.root, text="Contraseña").pack() self.contrasena = tk.Entry(self.root) self.contrasena.insert(0, "*******") self.contrasena.bind("<Button-1>", lambda e: self.contrasena.delete(0, tk.END)) self.contrasena.pack() # Botón de envío tk.Button(self.root, text="Entrar").pack() # Bucle de ejecución self.root.mainloop()
El aspecto, aunque aceptable, es mejorable:

Migración de Tkinter a CustomTkinter
Dejamos esto así. La importación de Tkinter, de momento, la vamos a quitar. Si la necesitamos en algún momento, la añadiremos.
#Importaciones import customtkinter as ctk import os from PIL import ImageTk, Image # 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 de CustomTkinter
Debajo de estas líneas, vamos a colocar la configuración global de colores del sistema y tema de la aplicación (estas dos líneas, afectarán a todas las ventanas que creemos en el programa).
# Modo de color y tema ctk.set_appearance_mode("Sytem") ctk.set_default_color_theme("blue")
Declaración de la clase Login y personalización general de la ventana
Lo primero, es declarar la clase con el método __init__() que le dará unos valores iniciales a la ventana:
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
En la línea 4, se instancia la ventana. Esta vez, con CTk() en lugar de Tk().
A continuación se le añade el título, el icono, el tamaño inicial y con resizable() nos aseguramos de que el usuario no pueda destruir la estética de la ventana, evitando que pueda redimensionar esta ventana.
Cargar y mostrar imágenes con CustomTkinter
Lo siguiente, es cargar la imagen del logo con «CTkImage()», la cual, podemos especificar con dos rutas alternativas, una para el modo oscuro (atributo «dark_image») y otra para el modo claro (atributo «light_image»).
Finalmente, se muestra la imagen con la etiqueta de tipo «CTkLabel()» que es el equivalente al widget Label() de Tkinter.
# 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)
Le doy al pack() unos márgenes de 15 píxeles para que no se pegue mucho a los elementos de abajo ni a la barra de título de la ventana.
Te dejo los archivos de imagen que necesitas para realizar este capítulo (puedes usar las tuyas propias, claro está).
Campos de texto con CustomTkinter
Los campos de texto (Entry) en CustomTkinter son «CTkEntry()».
# 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()
Por último, nos queda el botón. Este es CTkButton (Button en Tkinter):
También le dejo un poco de «pady» para que no se pegue a los elementos de arriba.
# Botón de envío ctk.CTkButton(self.root, text="Entrar").pack(pady=10)
No te olvides del bucle de ejecución:
# Bucle de ejecución self.root.mainloop()
Pruebas de la ventana de Login
Ejecutamos desde «app.py»:
#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()
En modo oscuro se ve así:

Y en modo claro así:

Estos son unos de los excelentes resultados que se pueden crear fácilmente gracias a CustomTkinter.
Establecer una comprobación de usuario válido en el Login con CustomTkinter
Vamos a ver como establecer una comprobación en el login de nuestra ventana principal. Esta verificación se va a realizar mediante el nombre de usuario y la contraseña del servidor MySQL. Si es correcto, nos dejará usar el programa, en cambio, si no lo es, no.
En las importaciones de la interfaz, añade el diccionario de acceso a la base de datos.
from bd.base_datos import acceso_bd
Abajo del método __init__ (fíjate que quede dentro de la clase, pero fuera del __init__), voy a añadir un método para validar la contraseña:
# Función para validar el login def validar(self): obtener_usuario = self.usuario.get() obtener_contrasena = self.contrasena.get() if obtener_usuario != acceso_bd["user"] or obtener_contrasena != acceso_bd["password"]: ctk.CTkLabel(self.root, text="Usuario o contraseña incorrectos.").pack() else: ctk.CTkLabel(self.root, text=f"Hola, {obtener_usuario}. Espere unos instantes...").pack()
En el botón de envío, debes añadir el atributo «command» para llamar al método validar en el momento de pulsar el botón.
#Botón de envío ctk.CTKButton(text="Entrar", command=self.validar).pack()
Probemos si funciona.
Si me equivoco en la contraseña o en el usuario o en ambas, me genera debajo del login la etiqueta de error:

En cambio, si pongo la contraseña y usuario correctos (los que tengo en el servidor MySQL) me genera la etiqueta de acceso válido:

Perfecto. Sin embargo, esto es mejorable.
El método destroy de la clase Label de CustomTkinter
El problema de usar estas etiquetas así con Tkinter o con CustomTkinter, es que en la ventana no se llegan a mostrar las siguientes etiquetas, puesto que no caben. Esto, deja un aspecto poco serio y poco presentable. Vamos a utilizar un método llamado destroy() (destruir) de la clase CTkLabel(), el cual, hará desaparecer la etiqueta anterior, si se necesita una nueva.

La función predefinida hasattr() de Python
Para poder hacer este propósito, necesitas aprender una de las funciones predefinidas de Python.
Esta función se usa para verificar si un objeto tiene un atributo determinado. La sintaxis de hasattr() es la siguiente:
hasattr(objeto, atributo)
Primero, se verifica si el valor de nombre de usuario del CTkEntry() pasado por el usuario concuerda con el valor que lleva el diccionario con los datos de nuestra conexión con el servidor MySQL.
Si ambos o uno de ellos (or) son diferentes (!=) a los valores del diccionario, se toma por inválido el login. Siempre que hasattr encuentre un elemento con el nombre de variable «info_login», lo eliminará con el destroy() y luego creará la etiqueta con «Usuario o contraseña incorrectos.». De esta forma, nos aseguramos de que no haya nunca más de una etiqueta.
El else se ejecutará solo si el nombre de usuario es correcto y la contraseña también.
Aquí lo mismo, siempre se comprueba primero si hay ya un elemento «info_login» en la ventana. Si lo está, lo va a eliminar y mostrará el valor «Hola, nombre_usuario. Espere unos instantes…».
# 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()
Con esto, hemos avanzado bastante en el proyecto. En el siguiente capítulo podemos empezar ya con la creación de una nueva ventana principal que se va a generar en caso de login correcto. Esto hará que se elimine esta ventana de login para dar paso a la segunda ventana principal del programa.
Puesto que las ventanas principales funcionan con el bucle mainloop(), deberemos destruir una para crear la otra.
En esta ventana, crearemos todos los botones con las opciones del programa.
No te pierdas nada del curso de Máster en Python.
Hola a todos, les hago este aporte por si tuvieron problemas al ejecutar. Nuestro querido amigo Kike nos jugó una broma y no escribió de esta manera el Button…… ctk.CTkButton(self.root,text=»Entrar», command=self.validar).pack()
Ya lo he solucionado. De «broma» nada, es un fallo por trabajar con Tkinter y CustomTkinter a la vez.
Espero que no tardaras en darte cuenta. Siento si te causó un gran problema el botón.
Gracias por avisar.
Por cierto, no me llamo Kike.
¡Un saludo!
Hola, muy bueno el proyecto, espero seguir hasta el final.
Tengo un detalle al migrar a customtkinter, al terminar de migrar para mostrar el login en dark o light, me vota el siguiente mensaje:
Traceback (most recent call last):
File «d:\4\Curso HTML-PYTHON\app.py», line 28, in
ventana_login = gui.Login()
File «d:\4\Curso HTML-PYTHON\interfaz\interfaz_grafica.py», line 32, in __init__
etiqueta = ctk.CTkLabel(master = self.root,
File «C:\Users\jonat\AppData\Local\Programs\Python\Python310\lib\site-packages\customtkinter\windows\widgets\ctk_label.py», line 82, in __init__
self._canvas = CTkCanvas(master=self,
File «C:\Users\jonat\AppData\Local\Programs\Python\Python310\lib\site-packages\customtkinter\windows\widgets\core_rendering\ctk_canvas.py», line 31, in __init__
super().__init__(*args, **kwargs)
File «C:\Users\jonat\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py», line 2717, in __init__
Widget.__init__(self, master, ‘canvas’, cnf, kw)
File «C:\Users\jonat\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py», line 2601, in __init__
self.tk.call(
_tkinter.TclError: bad screen distance «0.0»
Ya he investigado y no logro dar con la solución, me puedes ayudar Quique, gracias…
Buenas! Gracias una vez más por todo. Me surge este problema:
AttributeError: module ‘customtkinter’ has no attribute ‘CTkImage’. Did you mean: ‘CTkFrame’?
Por si el error lo tenía yo, copie y pegué tu código y me sigue dando ese error.
ImageTk en la importación de PIL se queda en gris (no se si tiene relación, es por darte un dato más)
Y para terminar, la variable etiqueta, también se queda en gris.