Skip to content

📙 Clase 18 — El gran refactor: tu app como clase

Fase 3 · Programación Orientada a Objetos · Clase integradora ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Juntar TODA la Fase 3 en el patrón profesional de una app CTk.
  • La anatomía estándar: __init__ → estado → crear_widgets → callbacks-método.
  • El plan paso a paso para refactorizar tus apps (el código lo escribes tú 💪).

📖 PARTE TEÓRICA — la anatomía estándar

Toda app CTk profesional sigue esta estructura. Memorízala como plantilla mental:

python
import customtkinter as ctk

class App(ctk.CTk):                                  # ① herencia (Clase 15)
    def __init__(self):
        super().__init__()                           # ② construir la ventana
        self.title("Mi app")
        self.geometry("420x520")

        self._tareas = []                            # ③ ESTADO en self (Clases 14/16)

        self._crear_widgets()                        # ④ UI separada en un método

    # ---------- UI ----------
    def _crear_widgets(self):                        # _ = interno (Clase 16)
        self.entry = ctk.CTkEntry(self, placeholder_text="Nueva tarea…")
        self.entry.pack(padx=12, pady=12, fill="x")

        ctk.CTkButton(self, text="Agregar",
                      command=self._agregar).pack()  # callback = MÉTODO

        self.lista = ctk.CTkScrollableFrame(self)
        self.lista.pack(fill="both", expand=True, padx=12, pady=12)

        self.status = ctk.CTkLabel(self, text="")
        self.status.pack()

    # ---------- callbacks ----------
    def _agregar(self):
        ok, msg = self._validar(self.entry.get())    # lógica con return (Clase 08)
        if not ok:
            self.status.configure(text=msg)
            return
        self._tareas.append(Tarea(self.entry.get().strip()))
        self.entry.delete(0, "end")
        self._pintar()

    # ---------- lógica (sin tocar widgets) ----------
    @staticmethod
    def _validar(texto):
        if not texto.strip():
            return False, "⚠️ Escribe algo primero"
        return True, ""

    # ---------- render ----------
    def _pintar(self):
        for w in self.lista.winfo_children():        # limpiar el frame
            w.destroy()
        for tarea in self._tareas:                   # y redibujar desde el estado
            ctk.CTkCheckBox(self.lista, text=f"{tarea}",        # __str__ (Clase 17)
                            command=lambda t=tarea: self._alternar(t)  # (Clase 10)
                            ).pack(anchor="w", pady=2)
        self.status.configure(text=f"Pendientes: {self.pendientes}")

    def _alternar(self, tarea):
        tarea.alternar()
        self._pintar()

    @property
    def pendientes(self):                            # property (Clase 16)
        return sum(1 for t in self._tareas if not t.hecha)


class Tarea:                                         # el MODELO (Clases 13/17)
    def __init__(self, texto):
        self.texto = texto
        self.hecha = False

    def alternar(self):
        self.hecha = not self.hecha

    def __str__(self):
        return f"{'✅' if self.hecha else '⬜'} {self.texto}"


if __name__ == "__main__":                           # guardián (Clase 11)
    App().mainloop()

📌 Esta plantilla compila con CustomTkinter 6.0.0 (verificada por partes en el .venv; la ventana la abres tú con python app.py). Fíjate cuántas clases del curso se juntan aquí — cada línea la aprendiste en una clase concreta.

🗺️ El mapa de responsabilidades

class App(ctk.CTk)
├── __init__            configura ventana + estado + llama a _crear_widgets
├── _crear_widgets      SOLO construye la UI (no decide reglas)
├── callbacks (_agregar…)  orquestan: leen entrada → lógica → repintan
├── lógica (_validar…)     reglas puras: reciben datos, DEVUELVEN resultado
├── _pintar             borra y redibuja desde el estado (patrón de tu To-Do)
└── properties          datos calculados para mostrar (pendientes)

class Tarea             el MODELO: datos + comportamiento del registro

💡 Cuando en la Fase 6 agreguemos SQLite, solo cambia una cosa: el estado self._tareas se carga/guarda desde un módulo datos.py. La estructura ya no se mueve. Por eso este refactor es la inversión más rentable del curso.


💻 PARTE PRÁCTICA — tu misión (tú escribes el código)

Refactoriza tus dos apps reales a este patrón. Guía en modo guiado, sin solución — como tus ejercicios:

Refactor 1 — Formulario de registro (Ejercicio 1)

  1. Crea class App(ctk.CTk) con super().__init__(), título y tamaño.
  2. Mueve la creación de widgets a _crear_widgets(); guarda los entries como self.entry_....
  3. Convierte cada función-callback en un método (def _registrar(self):).
  4. Extrae la validación a un método puro que devuelva (ok, mensaje).
  5. Arranca con if __name__ == "__main__": App().mainloop().

Refactor 2 — Lista de tareas (Ejercicio 2)

  1. Crea la clase modelo Tarea (texto, hecha, alternar(), __str__).
  2. class App(ctk.CTk) con self._tareas = [].
  3. Tu función "redibujar" pasa a ser el método _pintar().
  4. Reto: agrega la property pendientes y muéstrala en un label de estado.

Checklist de éxito (márcalo al terminar):

  • [ ] Cero variables globales (todo en self)
  • [ ] Cero global en el archivo
  • [ ] La validación no toca widgets (recibe texto, devuelve (ok, msg))
  • [ ] python app.py abre y funciona igual (o mejor) que antes

🔐 Solución de referencia (ábrela solo DESPUÉS de intentarlo)

Ver solución · Refactor 1 — Formulario como clase
python
import customtkinter as ctk

class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Registro")
        self.geometry("380x300")
        self._crear_widgets()

    def _crear_widgets(self):
        self.entry_nombre = ctk.CTkEntry(self, placeholder_text="Nombre")
        self.entry_nombre.pack(padx=12, pady=6, fill="x")
        self.entry_email = ctk.CTkEntry(self, placeholder_text="Email")
        self.entry_email.pack(padx=12, pady=6, fill="x")
        ctk.CTkButton(self, text="Registrar", command=self._registrar).pack(pady=10)
        self.mensaje = ctk.CTkLabel(self, text="")
        self.mensaje.pack()

    def _registrar(self):
        ok, msg = self._validar(self.entry_nombre.get(), self.entry_email.get())
        if not ok:
            self.mensaje.configure(text=f"⚠️ {msg}")
            return
        self.mensaje.configure(text="✅ Registrado")
        self.entry_nombre.delete(0, "end")
        self.entry_email.delete(0, "end")

    @staticmethod
    def _validar(nombre, email):          # lógica pura: testeable sin ventana
        if not nombre.strip():
            return False, "El nombre está vacío"
        if "@" not in email:
            return False, "El email no es válido"
        return True, ""

if __name__ == "__main__":
    App().mainloop()
Ver solución · Refactor 2 — To-Do como clase

La plantilla completa de la parte teórica de esta misma clase ES la solución: class App(ctk.CTk) con self._tareas, _crear_widgets, _agregar, _pintar, _alternar, la property pendientes y la clase modelo Tarea. Compárala con la tuya sección por sección usando el mapa de responsabilidades. Si tu versión hace lo mismo con otros nombres, está bien — lo que importa es el checklist: cero globals, validación pura, callbacks-método.

🧪 Tip de entrevista: "¿Por qué organizar una GUI como clase?" → El estado vive encapsulado en la instancia (no en globals), los callbacks comparten ese estado vía self, y la app se vuelve testeable y componible (frames-clase reutilizables).


❓ Preguntas y respuestas (autoevaluación)

1. ¿Dónde vive el estado de la app en este patrón y qué reemplaza?

En atributos de la instancia (self._tareas); reemplaza las variables globales + global.

2. ¿Qué hace _crear_widgets y qué NO debe hacer?

Construye la interfaz. No debe contener reglas de negocio ni validaciones.

3. ¿Por qué _validar es @staticmethod?

No usa self: es lógica pura (recibe texto, devuelve resultado). Marcarla static lo documenta y permite probarla sin instanciar la app.

4. ¿Qué única pieza cambiará cuando llegue la base de datos en la Fase 6?

La carga/guardado del estado (self._tareasdatos.py/SQLite). La estructura queda igual.


📎 Apuntes relacionados

➡️ Siguiente

¡Fase 3 completa! 🎉 Fase 4: Clase 19 · Archivos — que tu app recuerde datos al cerrar.