WhatsApp

  

20 Construyendo la aplicación final en Tkinter – Parte 1

Guía paso a paso para crear la ventana principal, el menú y una pantalla funcional (listado de notas) usando Tkinter, con arquitectura basada en clases y módulos.

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

AspectoTkinterPyQt5
LicenciaBSD (incluido en Python)GPL / Comercial
Curva de aprendizajeBajaMedia-Alta
Soporte de temas modernosLimitado (ttk)Amplio (Qt Styles)
Tamaño del ejecutablePequeñoMayor (dependencias Qt)
ComunidadMuy ampliaFuerte en aplicaciones empresariales

Tkinter vs. Electron (JS)

AspectoTkinterElectron
Consumo de RAMMuy bajoAlto (Chromium)
PortabilidadPython + Tk (casi universal)Node + Chromium (requiere bundling)
Tiempo de desarrolloRápido para prototiposMás lento por arquitectura web
Actualizaciones UILimitadas a widgets nativosIlimitadas (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/except y muestra mensajes claros con messagebox.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 sqlite3 con PRAGMA foreign_keys = ON; para evitar datos huérfanos.
  • Seguridad: Si la aplicación se distribuye, empaqueta con PyInstaller y 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 grid con weight para que los widgets se redimensionen fluidamente en pantallas de alta resolución.
  • Depuración: Ejecuta la app con python -m trace --trace main.py o usa pdb.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.Style para tematizar la aplicación (modo oscuro).
  • Tests unitarios con pytest y unittest.mock.

¡Mantente atento y sigue practicando!

© 2025 TuNombre • Todos los derechos reservados.

 

20 Construyendo la aplicación final en Tkinter – Parte 1
ASIMOV Ingeniería S. de R.L. de C.V., Emiliano Nava 10 diciembre, 2025
Compartir
Iniciar sesión dejar un comentario

  
19 Proyecto final con Tkinter: diseño de una aplicación completa
Guía paso a paso para diseñar y estructurar un proyecto final con Tkinter, proponiendo una agenda de tareas con requisitos funcionales, pantallas, arquitectura de clases y fragmentos clave de código.