Construyendo la aplicación final en Tkinter – Parte 1
Continuamos el proyecto iniciado en el post anterior y ahora veremos cómo montar la ventana principal, el menú de la aplicación y una pantalla funcional (listado de notas). Todo el código está organizado en clases y módulos para que sea fácil de mantener y escalar.
1. ¿Por qué estructurar con clases y módulos?
Separar la lógica de la UI en módulos y usar clases tiene varias ventajas:
- Reusabilidad: Puedes reutilizar componentes (por ejemplo, el menú) en otras aplicaciones.
- Testabilidad: Cada clase puede ser testeada de forma aislada.
- Escalabilidad: Añadir nuevas pantallas (alumnos, materias, etc.) no requiere reescribir la ventana principal.
- Mantenibilidad: El código es más legible y sigue los principios SOLID.
2. Estructura de directorios recomendada
my_tkinter_app/
│
├── main.py # punto de entrada de la aplicación
├── gui/
│ ├── __init__.py
│ ├── app_window.py # clase que representa la ventana principal
│ ├── menu_bar.py # definición del menú
│ └── notes_view.py # pantalla de listado de notas
│
├── models/
│ ├── __init__.py
│ └── note.py # modelo de datos (Nota)
│
├── services/
│ ├── __init__.py
│ └── note_service.py # lógica de negocio (CRUD de notas)
│
└── resources/
└── icons/ # iconos .png o .ico para el menú
Esta estructura separa claramente UI, modelo de datos y servicios. En proyectos más grandes, podrías añadir capas como repositories o controllers.
3. Creando la ventana principal (app_window.py)
La clase AppWindow hereda de tk.Tk y encapsula la configuración básica: título, tamaño, icono y la carga del menú.
import tkinter as tk
from .menu_bar import MenuBar
class AppWindow(tk.Tk):
"""Ventana raíz de la aplicación.
- Configura la estética global.
- Instancia y coloca el menú.
- Provee un contenedor (self.content_frame) donde se cargarán las diferentes vistas.
"""
def __init__(self):
super().__init__()
self.title("Gestor de Notas – Tkinter")
self.geometry("900x600")
self.minsize(800, 500)
# Icono opcional (compatibilidad Windows/macOS/Linux)
try:
self.iconbitmap("resources/icons/app.ico")
except Exception:
pass # Si el icono no existe, la app sigue funcionando.
# Contenedor central donde se insertarán las vistas.
self.content_frame = tk.Frame(self, bg="#f8f9fa")
self.content_frame.pack(fill=tk.BOTH, expand=True)
# Menú de la aplicación.
self.menu_bar = MenuBar(self)
self.config(menu=self.menu_bar)
def show_view(self, view_class, *args, **kwargs):
"""Destruye la vista actual y muestra una nueva.
Parámetros:
- view_class: clase que hereda de tk.Frame.
- *args, **kwargs: argumentos que la vista necesita.
"""
# Limpiar vista previa
for widget in self.content_frame.winfo_children():
widget.destroy()
# Instanciar la nueva vista dentro del contenedor.
view = view_class(self.content_frame, *args, **kwargs)
view.pack(fill=tk.BOTH, expand=True)
self.current_view = view
La función show_view es esencial para la navegación entre pantallas sin crear múltiples ventanas.
5. Modelo de datos (models/note.py)
Para mantener la lógica de negocio separada de la UI, definimos un data class que representa una nota.
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Note:
id: int
title: str
content: str
created_at: datetime = datetime.now()
En proyectos reales podrías usar SQLAlchemy o pydantic para validar campos.
6. Servicio de notas (services/note_service.py)
Este servicio simula un CRUD en memoria, pero está preparado para ser sustituido por una base de datos SQLite o PostgreSQL sin cambiar la UI.
from typing import List
from ..models.note import Note
class NoteService:
"""Manejo simple en memoria de notas.
En producción, reemplaza la lista por una capa ORM.
"""
def __init__(self):
self._notes: List[Note] = []
self._next_id = 1
def list_notes(self) -> List[Note]:
return list(self._notes) # copia para evitar mutaciones externas
def add_note(self, title: str, content: str) -> Note:
note = Note(id=self._next_id, title=title, content=content)
self._notes.append(note)
self._next_id += 1
return note
def delete_note(self, note_id: int) -> bool:
for note in self._notes:
if note.id == note_id:
self._notes.remove(note)
return True
return False
def get_note(self, note_id: int) -> Note | None:
for note in self._notes:
if note.id == note_id:
return note
return None
Los métodos devuelven valores claros y manejan los casos de error (por ejemplo, intentar borrar una nota inexistente).
7. Pantalla de listado de notas (gui/notes_view.py)
Esta vista muestra una tabla con ttk.Treeview, permite crear una nueva nota mediante un dialog y borrar notas seleccionadas.
import tkinter as tk
from tkinter import ttk, simpledialog, messagebox
from ..services.note_service import NoteService
class NotesView(tk.Frame):
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.service = NoteService()
self._build_ui()
self._load_data()
def _build_ui(self):
# Toolbar
toolbar = tk.Frame(self, bg="#e9ecef")
toolbar.pack(fill=tk.X, pady=5)
btn_new = ttk.Button(toolbar, text="Nueva Nota", command=self._new_note)
btn_new.pack(side=tk.LEFT, padx=5)
btn_del = ttk.Button(toolbar, text="Eliminar", command=self._delete_selected)
btn_del.pack(side=tk.LEFT, padx=5)
# Tabla
columns = ("id", "title", "created")
self.tree = ttk.Treeview(self, columns=columns, show="headings")
self.tree.heading("id", text="ID")
self.tree.heading("title", text="Título")
self.tree.heading("created", text="Creado")
self.tree.column("id", width=50, anchor=tk.CENTER)
self.tree.column("title", width=300)
self.tree.column("created", width=150)
self.tree.pack(fill=tk.BOTH, expand=True, pady=10, padx=10)
# Scrollbars
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
self.tree.configure(yscroll=vsb.set)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
def _load_data(self):
# Vaciar tabla
for i in self.tree.get_children():
self.tree.delete(i)
# Insertar notas
for note in self.service.list_notes():
self.tree.insert("", tk.END, values=(note.id, note.title, note.created_at.strftime("%Y-%m-%d %H:%M")))
def _new_note(self):
title = simpledialog.askstring("Nueva Nota", "Título:")
if not title:
return
content = simpledialog.askstring("Nueva Nota", "Contenido:")
if content is None:
content = ""
self.service.add_note(title, content)
self._load_data()
messagebox.showinfo("Éxito", "Nota creada correctamente.")
def _delete_selected(self):
selected = self.tree.selection()
if not selected:
messagebox.showwarning("Eliminar", "Seleccione una nota para eliminar.")
return
confirm = messagebox.askyesno("Confirmar", "¿Seguro que desea eliminar la nota seleccionada?")
if not confirm:
return
for item in selected:
note_id = int(self.tree.item(item, "values")[0])
self.service.delete_note(note_id)
self._load_data()
messagebox.showinfo("Éxito", "Nota(s) eliminada(s).")
Esta vista es totalmente autocontenida; si en el futuro deseas cambiar la fuente de datos (por ejemplo, usar SQLite), solo tendrás que modificar NoteService.
8. Archivo de arranque (main.py)
import tkinter as tk
from gui.app_window import AppWindow
if __name__ == "__main__":
# Mejora de rendimiento: activar el modo "high DPI" en Windows
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
app = AppWindow()
# Mostrar la vista inicial (listado de notas)
app.show_view(lambda master: __import__('gui.notes_view', fromlist=['NotesView']).NotesView(master))
app.mainloop()
El pequeño bloque try/except asegura compatibilidad con Windows 10/11 y evita errores en Linux/macOS.
Tkinter vs. PyQt5
| Aspecto | Tkinter | PyQt5 |
|---|---|---|
| Licencia | BSD (incluido en Python) | GPL / Comercial |
| Curva de aprendizaje | Baja | Media-Alta |
| Soporte de temas modernos | Limitado (ttk) | Amplio (Qt Styles) |
| Tamaño del ejecutable | Pequeño | Mayor (dependencias Qt) |
| Comunidad | Muy amplia | Fuerte en aplicaciones empresariales |
Tkinter vs. Electron (JS)
| Aspecto | Tkinter | Electron |
|---|---|---|
| Consumo de RAM | Muy bajo | Alto (Chromium) |
| Portabilidad | Python + Tk (casi universal) | Node + Chromium (requiere bundling) |
| Tiempo de desarrollo | Rápido para prototipos | Más lento por arquitectura web |
| Actualizaciones UI | Limitadas a widgets nativos | Ilimitadas (HTML/CSS/JS) |
9. Buenas prácticas, seguridad y troubleshooting
- Separación de capas: Mantén siempre UI ⇢ Servicios ⇢ Modelo. Evita mezclar lógica de negocio dentro de callbacks de UI.
- Manejo de excepciones: Envuelve todo el código de interacción con el usuario en
try/excepty muestra mensajes claros conmessagebox.showerror. - Validación de datos: Antes de crear una nota, verifica que el título no esté vacío y que no supere una longitud razonable (p.ej., 150 caracteres).
- Persistencia: Cuando migres a SQLite, usa
sqlite3conPRAGMA foreign_keys = ON;para evitar datos huérfanos. - Seguridad: Si la aplicación se distribuye, empaqueta con
PyInstallery firma el ejecutable para evitar alertas de Windows Defender. - Rendimiento: Para tablas con >10 000 filas, habilita
self.tree.configure(displaycolumns=...)y carga datos por lotes (paginación). - Escalabilidad UI: Usa
gridconweightpara que los widgets se redimensionen fluidamente en pantallas de alta resolución. - Depuración: Ejecuta la app con
python -m trace --trace main.pyo usapdb.set_trace()dentro de callbacks problemáticos.
10. ¿Qué sigue?
En la Parte 2 implementaremos:
- Persistencia con SQLite +
SQLAlchemy. - Una segunda vista: Listado de alumnos con relaciones many‑to‑many (alumnos‑notas).
- Uso de
ttk.Stylepara tematizar la aplicación (modo oscuro). - Tests unitarios con
pytestyunittest.mock.
¡Mantente atento y sigue practicando!
20 Construyendo la aplicación final en Tkinter – Parte 1