Construyendo la aplicación final en Tkinter – Parte 1
En el post anterior definimos la arquitectura de nuestra aplicación de gestión académica (alumnos, notas, profesores) y diseñamos la base de datos SQLite. En esta entrega, pasaremos a la capa de presentación con Tkinter, creando la ventana principal, un menú completo y la primera pantalla funcional: el listado de notas.
El objetivo es que cualquier estudiante de programación pueda seguir el proceso, entender la organización en paquetes y aplicar buenas prácticas de POO y modularidad.
Requisitos previos
- Python 3.9+ instalado.
- Conocimientos básicos de
Tkinter(ventanas, widgets). - SQLite3 (viene con Python).
- Editor de código (VS Code, PyCharm, etc.).
Instala la librería ttkbootstrap para mejorar la estética con Bootstrap‑like themes:
pip install ttkbootstrap
Estructura del proyecto
my_school_app/
├─ app.py # Punto de entrada
├─ config.py # Configuración global
├─ db/
│ └─ database.py # Acceso a SQLite
├─ ui/
│ ├─ __init__.py
│ ├─ main_window.py # Clase MainWindow
│ ├─ menu_bar.py # Clase MenuBar
│ └─ notes_view.py # Vista de listado de notas
└─ models/
├─ __init__.py
├─ student.py # Entidad Student
└─ note.py # Entidad Note
Esta organización permite:
- Separación de responsabilidades: UI, lógica de negocio y acceso a datos están aislados.
- Escalabilidad: Añadir nuevas vistas (alumnos, profesores) sin tocar el núcleo.
- Testabilidad: Cada módulo puede ser testeado de forma independiente.
config.py
"""Configuración central de la aplicación"""
DB_PATH = "db/school.db"
APP_TITLE = "Gestor Académico"
WINDOW_SIZE = "900x600"
THEME = "flatly" # Tema ttkbootstrap (Bootstrap 4 like)
models/note.py
from dataclasses import dataclass
@dataclass
class Note:
id: int
student_id: int
subject: str
grade: float
date: str # ISO format YYYY-MM-DD
Utilizamos dataclass para simplificar la definición de la entidad.
db/database.py
import sqlite3
from pathlib import Path
from ..config import DB_PATH
class Database:
def __init__(self):
self.conn = sqlite3.connect(DB_PATH)
self.conn.row_factory = sqlite3.Row
self._ensure_schema()
def _ensure_schema(self):
"""Crea tablas si no existen – idempotente"""
cur = self.conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER NOT NULL,
subject TEXT NOT NULL,
grade REAL NOT NULL,
date TEXT NOT NULL
)
''')
self.conn.commit()
def fetch_notes(self):
cur = self.conn.cursor()
cur.execute('SELECT * FROM notes ORDER BY date DESC')
rows = cur.fetchall()
return [dict(row) for row in rows]
def insert_note(self, student_id, subject, grade, date):
cur = self.conn.cursor()
cur.execute(
'INSERT INTO notes (student_id, subject, grade, date) VALUES (?,?,?,?)',
(student_id, subject, grade, date)
)
self.conn.commit()
return cur.lastrowid
def close(self):
self.conn.close()
El método _ensure_schema garantiza que la tabla exista siempre que la app se inicie, facilitando despliegues en máquinas nuevas.
ui/main_window.py
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from .menu_bar import MenuBar
from .notes_view import NotesView
class MainWindow(ttk.Window):
def __init__(self, db, *args, **kwargs):
super().__init__(themename=kwargs.pop('theme', 'flatly'), *args, **kwargs)
self.title(kwargs.get('title', 'Aplicación'))
self.geometry(kwargs.get('size', '800x600'))
self.db = db
# --- Menú superior ---
self.menu = MenuBar(self)
self.config(menu=self.menu)
# --- Contenedor principal (frame) ---
self.container = ttk.Frame(self)
self.container.pack(fill=BOTH, expand=YES, padx=10, pady=10)
# Vista por defecto: listado de notas
self.show_notes_view()
def clear_container(self):
for widget in self.container.winfo_children():
widget.destroy()
def show_notes_view(self):
self.clear_container()
NotesView(self.container, self.db).pack(fill=BOTH, expand=YES)
Observa cómo MainWindow delega la creación del menú a MenuBar y la lógica de la pantalla a NotesView. La función clear_container permite cambiar de vista sin crear una nueva ventana.
ui/menu_bar.py
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
class MenuBar(ttk.Menu):
def __init__(self, master):
super().__init__(master)
self.master = master
self._build_menus()
def _build_menus(self):
# Archivo
file_menu = ttk.Menu(self, tearoff=0)
file_menu.add_command(label='Salir', command=self.master.quit, accelerator='Ctrl+Q')
self.add_cascade(label='Archivo', menu=file_menu)
# Ver
view_menu = ttk.Menu(self, tearoff=0)
view_menu.add_command(label='Notas', command=self.master.show_notes_view)
# Futuro: view_menu.add_command(label='Alumnos', command=self.master.show_students_view)
self.add_cascade(label='Ver', menu=view_menu)
# Ayuda
help_menu = ttk.Menu(self, tearoff=0)
help_menu.add_command(label='Acerca de', command=self._show_about)
self.add_cascade(label='Ayuda', menu=help_menu)
# Atajos de teclado
self.master.bind_all('', lambda e: self.master.quit())
def _show_about(self):
ttk.Messagebox.showinfo('Acerca de', 'Gestor Académico – Versión 1.0')
ui/notes_view.py
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.tableview import Tableview
class NotesView(ttk.Frame):
def __init__(self, master, db, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.db = db
self._build_ui()
self._load_data()
def _build_ui(self):
# Título
ttk.Label(self, text='Listado de Notas', font=('Helvetica', 16, 'bold')).pack(pady=10)
# Tabla (Tableview de ttkbootstrap)
columns = ('id', 'student_id', 'subject', 'grade', 'date')
self.table = Tableview(self,
paginated=False,
searchable=False,
bootstyle=PRIMARY,
columns=columns,
show='headings')
for col in columns:
self.table.heading(col, text=col.title().replace('_', ' '))
self.table.column(col, width=100, anchor=CENTER)
self.table.pack(fill=BOTH, expand=YES, padx=5, pady=5)
# Botón de refresco
ttk.Button(self, text='Refrescar', command=self._load_data, bootstyle=SUCCESS).pack(pady=5)
def _load_data(self):
self.table.delete(*self.table.get_children())
notes = self.db.fetch_notes()
for note in notes:
self.table.insert('', END, values=(note['id'], note['student_id'], note['subject'], note['grade'], note['date']))
La tabla utiliza Tableview de ttkbootstrap, que ofrece paginación y búsqueda integradas (desactivadas aquí para simplificar). Cada fila muestra los campos de la tabla notes.
app.py (punto de entrada)
#!/usr/bin/env python3
"""Gestor Académico – Entrypoint"""
import sys
from pathlib import Path
# Añadir el directorio raíz al PYTHONPATH (para imports relativos)
BASE_DIR = Path(__file__).resolve().parent
sys.path.append(str(BASE_DIR))
from config import APP_TITLE, WINDOW_SIZE, THEME
from db.database import Database
from ui.main_window import MainWindow
def main():
db = Database()
try:
root = MainWindow(db, title=APP_TITLE, size=WINDOW_SIZE, theme=THEME)
root.mainloop()
finally:
db.close()
if __name__ == '__main__':
main()
El bloque try/finally garantiza que la conexión SQLite se cierre adecuadamente, evitando bloqueos de archivo.
Ejecutar la aplicación
- Clona o descarga el repositorio.
- Desde la raíz del proyecto, instala dependencias:
pip install -r requirements.txt # (contendrá ttkbootstrap) - Ejecuta:
python app.py - La ventana mostrará el listado de notas (vacío la primera vez).
Solución de problemas (troubleshooting)
- ImportError: No module named 'ttkbootstrap' – Asegúrate de haber ejecutado
pip install ttkbootstrapdentro del mismo entorno virtual. - Database locked – Cierra correctamente la aplicación (Ctrl+Q) o verifica que no haya otro proceso manteniendo la conexión.
- La tabla no se muestra – Revisa que la versión de
ttkbootstrapsea >=1.10; versiones antiguas no incluyenTableview.
Mejores prácticas y seguridad
- Validación de datos de entrada: antes de llamar a
insert_notevalida quegradeesté entre 0 y 10 y quedatesea una fecha ISO válida. - Principio de menor privilegio: el archivo
school.dbdebe tener permisos600(solo el propietario). - Separación de capas: si la aplicación crece, considera mover la lógica de negocio a un paquete
services/y exponer una API REST conFastAPIpara futuras versiones. - Internacionalización (i18n): utiliza
gettextpara traducir los textos del menú y mensajes.
Tkinter vs. alternativas modernas (PyQt5 / Kivy)
Tkinter + ttkbootstrap
- Incluido en la instalación estándar de Python (no dependencias externas).
- Ligero, bajo consumo de memoria.
- Facilidad de despliegue en entornos corporativos con restricciones.
- Estética mejorada mediante
ttkbootstrap(temas Bootstrap).
PyQt5 / PySide2
- Widgets más ricos y soporte nativo para estilos avanzados.
- Mayor curva de aprendizaje y licencia GPL/comercial.
- Mayor peso del ejecutable, pero mejor para aplicaciones con gráficos complejos.
- Soporte integrado para internacionalización y accesibilidad.
Para proyectos educativos y prototipos rápidos, Tkinter + ttkbootstrap ofrece la mejor relación simplicidad‑funcionalidad.
¿Qué sigue?
En la Parte 2 continuaremos:
- Implementando la vista de Listado de Alumnos con edición en línea.
- Añadiendo validaciones de formulario y manejo de errores.
- Integrando un módulo de services para separar lógica de negocio.
- Empaquetando la aplicación con
PyInstallerpara distribución.