Cómo crear una interfaz gráfica para consultas SQL en Python: Tutorial paso a paso

Cómo crear una interfaz gráfica para consultas SQL en Python: Tutorial paso a paso

Empezamos ya a escribir el código correspondiente a cada ventana del programa. La finalidad es que aprendas a utilizar la clase con la lógica de bases de datos en la interfaz gráfica de Python.


Comencemos por la primera ventana, que es la que verás en este capítulo. La ventana para realizar consultas en SQL.

Esta ventana necesitará contar con las siguientes cosas:

  • Un Entry() para introducir consultas SQL.
  • Un Button() para enviar la consulta SQL.
  • Otro Button() para eliminar los resultados.
  • Un apartado donde mostrar la salida de la consulta SQL.
  • Una Label() para mostrar la cantidad de registros devueltos en las consultas.
  • Un mensaje Messagebox() para avisar al usuario de consultas erróneas.

Código inicial de la ventana de consultas

Inicialmente, el método está así:

    def ventana_consultas(self):
        # Crea la ventana
        ventana = ctk.CTkToplevel()
        # Le da un título
        ventana.title("Ventana de consultas SQL")

Código de app.py

En el archivo app.py (el principal de la aplicación), ya podemos prescindir de casi todo. Quedará con solo dos líneas de código que harán toda la magia.

#Importaciones
import interfaz.interfaz_grafica as gui

#Instancia la parte gráfica del programa
ventana_login = gui.Login()

Importaciones en el archivo de interfaz y adición de lógica

El objeto, de bases de datos, para poder utilizar sus métodos, quedará instanciado en el archivo de la interfaz gráfica. Por eso, tienes que dejar ahí la importación (línea 5) y la instanciación:

#Importaciones
import customtkinter as ctk
import os
from PIL import Image
import bd.base_datos as sqlbd

...

# Objeto para manejar bases de datos MySQL
base_datos = sqlbd.BaseDatos(**sqlbd.acceso_bd)

Añadiendo una fuente personalizada para widgets

Lo siguiente que voy a hacer, es añadir una tupla con un valor de fuente para poder aplicarlo a cada widget que desee.

Te recomiendo colocar esta variable arriba, junto a las configuraciones globales de la interfaz gráfica.

Esto es una tupla con un valor string de nombre de fuente, un int con el tamaño de fuente y una constante para ponerla en negrita.

Ciertos widgets cuentan con el atributo "font", al cual, le tenemos que pasar una tupla como "fuente_widgets".

# Fuente para algunos widgets
fuente_widgets = ('Raleway', 16, BOLD)

Esta constante es de Tkinter, así que al menos, hay que importarla:

from tkinter.font import BOLD

Creando la ventana de consultas SQL

Ahora si, ya lo tenemos todo listo para empezar a crear la nueva ventana de consultas SQL.

Añadiendo el marco principal de la ventana

Añadimos el marco con unos márgenes frente a la ventana de 10 píxeles:

def ventana_consultas(self):
        ventana = ctk.CTkToplevel()
        ventana.title("Ventana de consultas SQL")
        
        # Crea el frame y añádelo a la ventana
        marco = ctk.CTkFrame(ventana)
        marco.pack(padx=10, pady=10)

Añadiendo la entrada de texto de la ventana

Vamos a crear un Entry() con el que escribir las consultas y poder enviarlas.

Este tiene las siguientes características:

  • Un Entry() posicionado en el marco principal con un ancho de 300 píxeles.
  • Una fuente modificada.
  • Posicionamiento en grid.
        # Crea el entry y establece su tamaño a 300px de ancho
        self.entrada = ctk.CTkEntry(marco, width=300)
        # Establece un valor personalizado de fuente
        self.entrada.configure(font=fuente_widgets)
        # Posiciona el elemento en grid
        self.entrada.grid(row=0,column=0)

Método para utilizar la lógica del método consulta de base_datos.py

El siguiente paso es crear un método que utilice la lógica del método consulta() de base_datos.py.

Este método tiene las siguientes características:

  • Obtiene el valor de la entrada de texto (línea 4).
  • Almacena el resultado (línea 6).
  • Lo itera y presenta. Un registro por línea (línea 7 a 9).
        # Método para utilizar la lógica del método consulta de base_datos.py
        def procesar_datos():
            # obtiene el contenido del entry
            datos = self.entrada.get()
            # llama al método base_datos.consulta() con los datos como argumento
            resultado = base_datos.consulta(datos)
            for registro in resultado:
                self.texto.insert('end', registro)
                self.texto.insert('end', '\n')

El insert(), aún no tiene sentido, ya que lo vamos a utilizar sobre un widget llamado "texto" que voy a crear más abajo, que será en el que se mostrarán los resultados.

Crear el botón de envío de consulta

A continuación, vamos a crear el primer botón, el de envío de consultas. Este va a enviar la consulta, llamando al método que acabamos de crear, que este, a su vez, llama al método consulta() de base_datos.py. Esto se traduce, en que le pasará el valor del Entry al servidor MySQL.

        # Crea el botón de envío
        boton_envio = ctk.CTkButton(marco, 
                                text="Enviar",
                                command=lambda : procesar_datos())
        # Posiciona el botón a la derecha del Entry()
        boton_envio.grid(row=0, column=1)

Crear el botón para borrar la consulta anterior

Hacer la consulta, está muy bien, pero luego, ¿quién lo borra todo de la caja de texto donde la mostremos?

Para esto, vamos a crear un botón que vacíe el resultado de cualquier consulta que tengamos en la ventana.


Este tiene una llamada a un método que todavía no hemos escrito.

        # Crea el botón de borrado
        boton_borrar = ctk.CTkButton(marco, 
                                 text="Borrar",
                                 command=self.limpiar_texto)
        # Posiciona el botón a la derecha del botón de envío
        boton_borrar.grid(row=0, column=2)

Crear el widget de texto Textbox

El widget de texto, es una caja de texto parecida al Entry(), pero pensada para salida de datos (mostrar datos) en lugar de introducirlos como el Entry(). Además, normalmente, se utiliza para poner más de una línea de texto.

Este widget se llama Textbox en CustomTkinter y Text en Tkinter.

        # Crea el widget de texto
        self.texto = ctk.CTkTextbox(marco, 
                                    width=600, 
                                    height=300)

        # Coloca el widget texto debajo del entry y el botón usando grid
        self.texto.grid(row=1, column=0, padx=10, pady=10)

Crear el método para vaciar el contenido del widget Textbox

No nos queda hacer mucho más, añade el método para vaciar el contenido del widget Textbox e intenta ejecutar el programa.

Sencillamente, cuando pulsemos el botón para borrar que lleva el "command" con la llamada a este método, se va a borrar todo.

    def limpiar_texto(self):
        # borra todo el contenido del widget Text
        self.texto.delete('1.0', 'end')

El método delete del widget Text o TextBox, se utiliza para eliminar texto que contiene. Los argumentos '1.0' y 'end' especifican el rango de texto que se va a eliminar. '1.0' significa que se debe comenzar a eliminar desde la primera línea y la primera columna (es decir, desde el principio del texto), mientras que 'end' significa que se debe eliminar hasta el final del texto. En resumen, este método borra todo el contenido del widget de texto.

El atributo columnspan de Tkinter y CustomTkinter

Si cargamos el programa, nos aparecen todos los elementos, pero no están como seguramente queremos.

Resulta, que el widget de texto ocupa demasiado y hace que su columna en el grid sea muy grande, desplazando los botones de las otras columnas muy a la derecha.

Para las veces en las que quieras que un widget ocupe varias columnas, puedes utilizar el atributo columspan.

problemas columnas tkinter

Se lo vamos a añadir al widget de texto:

Si le pongo un valor de 2, ocupa dos columnas en el grid:

self.texto.grid(row=1, column=0, columnspan=2, padx=10, pady=10)
customtkinter problemas con grid

Entonces, para que ocupe las columnas de la 0 a la 2 (3 en total), hay que poner un 3:


self.texto.grid(row=1, column=0, columnspan=3, padx=10, pady=10)

El resultado está bien, pero queda el widget de texto un poco más pequeño que el conjunto de elementos de arriba. Solo hay que cambiarle el valor "width" (ancho) de 600 a 610. No es algo que se note mucho, pero me molesta.

self.texto = ctk.CTkTextbox(marco, width=610, height=300)
customtkinter ventana consultas sql

Probando el botón de borrado

El siguiente paso va a ser probar si funciona el botón de borrado. El de enviar todavía no, ya que hay que hacer unos ajustes.

widget text de tkinter

Al pulsarlo, desaparece todo el contenido:

ventana de consultas sql

Adaptación del método de consultas de consola a GUI

Ha llegado la hora de la verdad, probemos el botón de envío con una consulta.

Pongas lo que pongas, te saldrá este error en la consola:

Error en la consola

in procesar_datos
for registro in resultado:
TypeError: 'NoneType' object is not iterable

Este error ocurre, porque la variable que guarda el resultado en el método procesar_datos() del método ventana_consultas(), nos guarda un tipo NoneType. Esto no se puede iterar con el bucle for que quiere mostrar los resultados en la ventana.

Vayamos a modificar el método de consultas del archivo base_datos.py, para adaptarlo a la interfaz gráfica, ya que está diseñado para funcionar solo en consola.

En el método de consulta, cambia este print():

print(self.cursor.fetchall())

Por una variable:


self.resultado = self.cursor.fetchall()

Ahora, haremos que el propio método de consultas devuelva el resultado, así lo utilizaremos con métodos de la interfaz gráfica.

Fíjate solo en la línea 28:

    # Decorador para el cierre del cursor y la base de datos
    def conexion(funcion_parametro):
        def interno(self, *args, **kwargs):
            try:
                if self.conexion_cerrada:
                    self.conector = mysql.connector.connect(
                        host = self.host,
                        user = self.usuario,
                        password = self.contrasena
                    )
                    self.cursor = self.conector.cursor()
                    self.conexion_cerrada = False
                    print("Se abrió la conexión con el servidor.")
                # Se llama a la función externa
                funcion_parametro(self, *args, **kwargs)
            except:
              	# Se informa de un error en la llamada
                print("Ocurrió un error con la llamada.")
            finally:
                if self.conexion_cerrada:
                    pass
                else:
                    # Cerramos el cursor y la conexión
                    self.cursor.close()
                    self.conector.close()
                    print("Se cerró la conexión con el servidor.")
                    self.conexion_cerrada = True
            return self.resultado
        return interno

Quédate con que este return nos devuelve el valor de la variable resultado que tenga cualquier método decorado con el decorador "conexion".

Probemos un par de consultas:

consultas sql desde python

En esta, puedes hacer scroll con la rueda del mouse o utilizar el botón de scroll generado a la derecha:

consultas interfaz usuario mysql python

Este botón de scroll no se genera de forma automática en Tkinter. Es una ventaja propia de CustomTkinter.

Si no quieres que haya scroll, hay formas de desactivar la opción por defecto.

Contador de registros devueltos

Esta ventana se puede mejorar, vamos a añadir una etiqueta abajo del todo que nos diga cuantos registros ha devuelto la consulta.

Esto lo puedes colocar, debajo del código del Textbox():

        # Agrega un nuevo widget Label para mostrar el número de registros devueltos
        self.contador_registros = ctk.CTkLabel(marco, text="Registros devueltos: 0")
        self.contador_registros.grid(row=2, column=0, columnspan=3, padx=10, pady=10)

Le he dado un columnspan de 3 para que quede centrado frente al widget de texto.

Lo siguiente, es modificar el método que procesa los datos en esta ventana.

        # Método para utilizar la lógica del método consulta de base_datos.py
        def procesar_datos():
            # obtiene el contenido del entry
            datos = self.entrada.get()
            # llama al método base_datos.consulta() con los datos como argumento
            resultado = base_datos.consulta(datos)
            for registro in resultado:
                self.texto.insert('end', registro)
                self.texto.insert('end', '\n')
            # Actualiza el contador de registros devueltos
            numero_registros = len(resultado)
            self.contador.configure(text=f"Registros devueltos: {numero_registros}")

En la variable "numero_registros", se almacena con la función predefinida len(), el número de tuplas que devuelve la lista del método "fetchall()". Así, ya tenemos el resultado.

Luego, solo queda utilizar el método configure() que también está disponible en los widget Label(). En este caso, le añade un valor con "Registros devueltos: número de registros".

Esto le da un toque más interesante a la ventana.

mostrar consultas sql con custom tkinter

Mejorando las etiquetas del login

Si te has fijado, la etiqueta, por defecto, lleva el valor "Registros devueltos: 0", el cual, se reemplaza por el valor del configure(). Esto quiere decir, que no es necesario destruir una etiqueta para poner otra en su lugar, si no, que podemos modificar su valor en cualquier momento sin tener que eliminar recursos y crearlos de nuevo.

Esto, seguramente, te recuerde a la acción que hacemos en la ventana de login del programa, en la cual, estamos eliminando una etiqueta para poner otra.

    # 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 == sqlbd.acceso_bd["user"] or obtener_contrasena == sqlbd.acceso_bd["password"]:
         # En caso de tener ya un elemento "info_login" (etiqueta) creado, lo borra
            if hasattr(self, "info_login"):
                self.info_login.configure(text="Usuario o contraseña incorrectos.")
            else:
                # 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.configure(text=f"Hola, {obtener_usuario}. Espere unos instantes...")
            else:
                # 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 del programa
            ventana_opciones = VentanaOpciones()

En este caso, el código es el mismo, solo que en lugar de destroy(), utilizamos configure().

¿Qué es mejor, destroy() o configure()?

El método configure() para cambiar las opciones de un widget en lugar de destruirlo y crear uno nuevo puede ser más eficiente en términos de uso de recursos. Cuando llamas al método destroy() en un widget, este se elimina por completo y todos los recursos asociados con él se liberan. Si luego creas un nuevo widget para reemplazarlo, se deben asignar nuevos recursos para ese widget.

Por otro lado, si usas el método configure() para cambiar las opciones de un widget existente en lugar de destruirlo y crear uno nuevo, no es necesario liberar y volver a asignar recursos. En su lugar, simplemente estás cambiando las opciones del widget existente.

En general, usar el método configure() para cambiar las opciones de un widget puede ser una mejor opción si solo necesitas realizar cambios menores en el widget y no necesitas eliminarlo por completo.

Mensaje inicial para la etiqueta

Estaría bien, que antes de enviar cualquier consulta, el mensaje inicial de la etiqueta, fuera algo como "Esperando una instrucción…" o algo por el estilo.

Después de pulsar por primera vez el botón de envío, que se cambie por el valor esperado.


interfaz grafica customtkinter python y mysql

Bien, en cuanto pulses el botón, realizando una consulta, este valor inicial, se cambiará con el número de registros.

Manejar consultas erróneas

Una de las cosas que es más probable que ocurran, es que el usuario pulse el botón "Enviar", sin nada en el Entry() o con una consulta errónea. Es ese caso, pasa esto:

Error en la consola

AttributeError: 'BaseDatos' object has no attribute 'resultado'

No pasa nada, lo solucionamos rápido con un try-except en el método procesar_datos(), que es en el que se produce el error final.

        def procesar_datos():
            try:
                # obtiene el contenido del entry
                datos = self.entrada.get()
                # llama al método base_datos.consulta() con los datos como argumento
                resultado = base_datos.consulta(datos)
                for registro in resultado:
                    self.texto.insert('end', registro)
                    self.texto.insert('end', '\n')
                # Actualiza el contador de registros devueltos
                numero_registros = len(resultado)
                self.contador_registros.configure(text=f"Registros devueltos: {numero_registros}")
            except AttributeError:
                self.contador_registros.configure(text=f"Hay un error en tu consulta SQL. Por favor, revísela.")

En el bloque try ponemos todo el código de antes. El except, lo enfocamos al error AtributteError para ser más específicos. Le decimos que hay un error con un mensaje en la etiqueta inferior.

¿Hay Messagebox en CustomTkinter?

Si lo prefieres, también puedes sacar un showerror() de Messagebox().

En principio, CustomTkinter no tiene Messagebox(), así que bien podemos utilizar los de Tkinter o utilizar unos geniales que han creado en una biblioteca llamada CTkMessagebox.

Esta biblioteca, la puedes encontrar aquí: biblioteca CTkMessagebox.

Se trata de una pequeña biblioteca que añade cinco ventanitas de estilo Messagebox con los diseños de CustomTkinter.

Estas son las que vamos a usar.

Instalación de CTkMessagebox

Para instalar CTkMessagebox, ves a la consola y escribe el siguiente comando:

pip install CTkMessagebox

Importación de CTkMessagebox

Para importar CTkMessagebox, solo hay que poner lo siguiente en la zona donde tenemos las importaciones:

from CTkMessagebox import CTkMessagebox

Lo dejaré sin alias para que se vea muy claro de donde sale lo que utilizamos con esta biblioteca.

Creando una ventana de error con CTkMessagebox

Ahora, solo tienes que añadir el código para sacar la ventana de error.

En el caso de esta biblioteca, es algo diferente a Messagebox de Tkinter. Si lo recuerdas de capítulos anteriores, cada alerta, tenía su propio nombre. showinfo, showwarning, etc.

Con CTkMessagebox, solo tienes que poner "CTkMessagebox()" y pasarle una serie de argumentos para configurarla.

 CTkMessagebox(title="Error", message="¡Hay un error en tu consulta SQL! Por favor, revísela.", icon="cancel")

El except, quedará así:

except AttributeError:
	self.contador_registros.configure(text=f"Hay un error en tu consulta SQL. Por favor, revísela.")
	CTkMessagebox(title="Error", message="¡Hay un error en tu consulta SQL! Por favor, revísela.", icon="cancel")

El problema, ahora, está en un funcionamiento inesperado de los errores del programa. Sí, nos equivocamos al abrir la ventana, en la primera consulta, no pasa nada, nos suelta el error con el except de la propia ventana de consultas. Pero si ponemos lo primero una consulta válida o la ponemos después de producir un error, saldrá el error en el propio método de consultas de base_datos.py.

Así pues, quitemos todo lo que ya no necesita este método.

    #Consultas SQL 
    @conexion   
    def consulta(self, sql):
        self.cursor.execute(sql)
        self.resultado = self.cursor.fetchall()

Ahora, el error se nos va al except del decorador "conexion". Si quitamos except, veremos que es un error del servidor al pasarle una instrucción errónea. El problema es que desde Python, es un poco complicado manejar este error, ya que no se produce en el propio programa de Python, si no, en el servidor.

La solución es aprender algo nuevo sobre el manejo de excepciones. La palabra "raise".

La propagación de excepciones en Python con raise

raise es una palabra clave en Python que se utiliza para generar (lanzar) una excepción. Cuando se ejecuta la instrucción raise, el programa detiene su ejecución y busca un bloque except que pueda manejar la excepción generada. Si no encuentra un bloque except adecuado, el programa termina con un mensaje de error.

Entonces se puede utilizar raise en el decorador para propagar la excepción y permitir que sea manejada por el bloque except en el método ventana, que es el último lugar donde podemos manejarla.

El decorador "conexion" queda así. Fíjate en las líneas 16 a la 18.

# Decorador para el cierre del cursor y la base de datos
    def conexion(funcion_parametro):
        def interno(self, *args, **kwargs):
            try:
                if self.conexion_cerrada:
                    self.conector = mysql.connector.connect(
                        host = self.host,
                        user = self.usuario,
                        password = self.contrasena
                    )
                    self.cursor = self.conector.cursor()
                    self.conexion_cerrada = False
                    print("Se abrió la conexión con el servidor.")
                # Se llama a la función externa
                funcion_parametro(self, *args, **kwargs)
            except Exception as e:
                print(f"Ocurrió un error: {e}")
                # Propaga la excepción
                raise e
            finally:
                if self.conexion_cerrada:
                    pass
                else:
                    # Cerramos el cursor y la conexión
                    self.cursor.close()
                    self.conector.close()
                    print("Se cerró la conexión con el servidor.")
                    self.conexion_cerrada = True
            return self.resultado
        return interno

Solo nos queda, quitar el "except AttributeError:" por un "except Exception:", el cual, es más recomendable que except a secas. Ya hablaré más en detalle en otros capítulos sobre estos tipos de manejadores.

            except Exception:
                self.contador_registros.configure(text=f"Hay un error en tu consulta SQL. Por favor, revísela.")
                CTkMessagebox(title="Error", message="¡Hay un error en tu consulta SQL! Por favor, revísela.", icon="cancel")
ctkmessagebox error

Borrar el contenido de la caja de texto al hacer nuevas consultas

Otro comportamiento que no me gusta, es el de apilamiento de consultas en la caja de resultados. Por ejemplo:

consultas sql  desde python con tkinter

El contador de registros, siempre se actualiza bien, pero no se borra el contenido de las consultas solo al hacer una nueva, si no, que hay que obligar al usuario a que limpie la caja con el botón "Borrar".

Entonces, en este caso, haremos que al pulsar el botón "Enviar", limpie la caja automáticamente.

No te preocupes, ya que es muy fácil. Con una línea lo tenemos. Hacemos que en el bloque try se actualice el valor de la caja siempre que entre en él. Será lo primero que haga.

Solo tienes que añadirle la línea 4:

       def procesar_datos():
            try:
                # Borra el contenido de "texto"
                self.texto.delete('1.0', 'end')
                # obtiene el contenido del entry
                datos = self.entrada.get()
                # llama al método base_datos.consulta() con los datos como argumento
                resultado = base_datos.consulta(datos)
                for registro in resultado:
                    self.texto.insert('end', registro)
                    self.texto.insert('end', '\n')
                # Actualiza el contador de registros devueltos
                numero_registros = len(resultado)
                self.contador_registros.configure(text=f"Registros devueltos: {numero_registros}")
            except Exception:
                self.contador_registros.configure(text=f"Hay un error en tu consulta SQL. Por favor, revísela.")
                CTkMessagebox(title="Error", message="¡Hay un error en tu consulta SQL! Por favor, revísela.", icon="cancel")

El primer argumento de self.texto.delete('1.0', 'end') indica la posición inicial desde donde se quiere borrar el contenido del widget de texto. En este caso, '1.0' indica la primera línea y la primera columna (es decir, el principio del texto). El segundo argumento 'end' indica la posición final hasta donde se quiere borrar el contenido del widget de texto. 'end' indica el final del texto.

Poner el foco en las ventanas TopLevel() y CTkTopLevel()

Otro comportamiento que me molesta también, es el de la ventana principal, la cual, al crear ventanas secundarias TopLevel o CTkTopLevel, no les pone el foco, lo que hace que se queden detrás de la principal. Este comportamiento es horrible para el usuario, y si no me crees, haz pruebas un rato y te acabarás hartando de tener que ir a hacer click cada vez en estas ventanas para poder utilizarlas.

Es algo muy sencillo, tanto en Tkinter como en CustomTkinter, vamos a utilizar el método "grab_set()" con la ventana secundaria.

class FuncionesPrograma:
    def ventana_consultas(self):
        # Creación de la ventana secundaria
        ventana = ctk.CTkToplevel()
        # Título de la ventana
        ventana.title("Ventana de consultas SQL")
        # Pone el foco en la ventana
        ventana.grab_set()
        
...
ventana consultas SQL

Pruebas de funcionamiento en el modo claro de CustomTkinter

Algo que deberías hacer siempre, es probar todo lo posible. Es por este motivo, que vamos a comprobar la interfaz de esta nueva ventana en el modo claro, a ver si todo encaja tan bien como en el modo oscuro.

modo claro customtkinter

No está nada mal, personalmente, me gusta más el modo oscuro, pero está bien que tengamos un 2x1. Haces la interfaz en un color, y la tienes en otro gratis.

Sin embargo, no me gusta que queden los elementos superiores tan pegados al borde del marco. En el modo oscuro, esto no era muy evidente, pero aquí, se ha quedado muy al descubierto.

Solo hay que añadir un pequeño espacio de margen en "y", por ejemplo, al Entry(). Esto forzará a toda su fila en el grid() a mantener ese margen.

self.entrada.grid(row=0,column=0, pady=10)

¡Perfecto! Ya tenemos un buen resultado final. Échale un vistazo tanto en el modo claro como al modo oscuro.

mysql connector python tkinter

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


3 comentarios en «0»

  1. Buenas Programación Fácil, me gustaría obtener un ejemplo de como implementarlo con una clase separada, es decir que se cree cada Ventana nueva en una clase separada e importarla con todos sus métodos.

    Que es lo esencial que tendría que hacerse?

    Gracias.

  2. Que tal he seguido a este punto los tutoriales quería saber si me podías ayudar ya le di vueltas al código en la funciones procesar datos, conexión, mostrar_bd, consulta etc para solucionar el error «Ocurrio un error : ‘BaseDatos’ object has no attribute ‘conexion_cerrada’ «, sin tener resultados positivos mas menos me pudieras apoyar con ello.

    También algo curioso en el archivo app.py, en las importaciones en mi código tengo que escribir «import bd.interfaz.interfaz_grafica as gui» por que si lo dejo como tu lo tienes en tu código no me funciona será por la versión de python.

    De antemano te agradezco el apoyo prestado.

Deja una respuesta

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

curso sql Entrada anterior SQL: Selección de columnas específicas con SELECT
curso sql Entrada siguiente Ejercicios resueltos para practicar SQL