Skip to content

📙 Clase 32 — Ventanas múltiples y diálogos

Fase 6 · Especialización: Apps de escritorio 🎯 ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Segundas ventanas con CTkToplevel (editar, preferencias, "acerca de").
  • Diálogos listos: CTkInputDialog, messagebox (confirmar/avisar) y filedialog.
  • Los gotchas de ventanas: el foco (grab_set) y el Toplevel que se abre duplicado.

📖 PARTE TEÓRICA

🪟 1. CTkToplevel: la segunda ventana

Una app tiene una ventana raíz (ctk.CTk) y las demás son CTkToplevel — ventanas hijas que viven mientras viva la principal.

python
class VentanaEditar(ctk.CTkToplevel):        # ventana-clase, como tu App (Clase 15)
    def __init__(self, padre, contacto):
        super().__init__(padre)
        self.title(f"Editar: {contacto['nombre']}")
        self.geometry("300x200")

        self.entry = ctk.CTkEntry(self)
        self.entry.insert(0, contacto["tel"])     # precargar el valor actual
        self.entry.pack(padx=10, pady=10)
        ctk.CTkButton(self, text="Guardar", command=self._guardar).pack()

        self.grab_set()                       # 🔑 MODAL: bloquea la principal
MétodoQué hace
grab_set()modal: el usuario debe cerrar esta ventana antes de tocar la principal
transient(padre)la mantiene visualmente "encima" de la principal
destroy()cerrarla por código (tras guardar)

⚠️ Gotcha #1 — la ventana duplicada: cada clic en "Editar" crea OTRA ventana. El patrón estándar: guardar la referencia y reutilizar.

python
def _abrir_editor(self):
    if self.editor is not None and self.editor.winfo_exists():
        self.editor.focus()              # ya está abierta: tráela al frente
        return
    self.editor = VentanaEditar(self, contacto)

💬 2. Diálogos que ya vienen hechos

Pedir un texto rápidoCTkInputDialog:

python
dialogo = ctk.CTkInputDialog(text="Nombre de la categoría:", title="Nueva categoría")
valor = dialogo.get_input()        # bloquea hasta que responda; None si cancela
if valor:
    crear_categoria(valor)

Confirmar y avisartkinter.messagebox (nativos del sistema; combinan bien con CTk):

python
from tkinter import messagebox

if messagebox.askyesno("Confirmar", "¿Eliminar este contacto?"):   # True/False
    datos.eliminar_contacto(self.con, id)

messagebox.showinfo("Listo", "Exportación completada")
messagebox.showerror("Error", "No se pudo conectar a la base de datos")
FunciónBotonesDevuelve
showinfo / showwarning / showerrorOK
askyesnoSí / NoTrue/False
askokcancelOK / CancelarTrue/False

Elegir archivostkinter.filedialog (lo usaste al exportar CSV):

python
from tkinter import filedialog

ruta = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])       # abrir
ruta = filedialog.asksaveasfilename(defaultextension=".csv")          # guardar como
# ambos devuelven "" si el usuario cancela → compruébalo SIEMPRE

📌 APIs verificadas en tu venv (CustomTkinter 6.0.0): CTkToplevel, CTkInputDialog, messagebox y filedialog disponibles. Pruébalos con ventana real desde tu terminal.

🧪 Tip de entrevista (UX): toda acción destructiva (borrar, sobrescribir) pide confirmación (askyesno); toda acción larga muestra progreso (Clase 33); todo error se explica en idioma humano (showerror), nunca un traceback al usuario.


🖥️ EN TU APP — el flujo editar con ventana modal

El patrón completo, de clic a BD, juntando SQLite II y esta clase:

python
class App(ctk.CTk):
    def _editar(self, id):
        fila = datos.buscar_por_id(self.con, id)
        if fila is None:
            messagebox.showerror("Error", "El contacto ya no existe")
            return
        editor = VentanaEditar(self, dict(fila))     # Toplevel modal
        self.wait_window(editor)                     # espera a que se cierre…
        self._pintar()                               # …y repinta desde la BD

    def _eliminar(self, id):
        if not messagebox.askyesno("Confirmar", "¿Eliminar este contacto?"):
            return                                   # el usuario se arrepintió
        datos.eliminar_contacto(self.con, id)
        self._pintar()
clic ✏️ ─▶ SELECT id ─▶ Toplevel modal (grab_set) ─▶ usuario edita ─▶ UPDATE

   lista repintada  ◀── wait_window despierta  ◀── destroy()┘

🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Confirmación honesta

Escribe el callback _vaciar_lista que pida confirmación y, solo si aceptan, borre todas las tareas hechas y muestre cuántas borró.

Ver solución
python
def _vaciar_lista(self):
    if not messagebox.askyesno("Confirmar", "¿Borrar todas las tareas completadas?"):
        return
    n = datos.borrar_completadas(self.con)       # el rowcount de la Clase 31
    messagebox.showinfo("Listo", f"Se borraron {n} tareas")
    self._pintar()

Ejercicio 2 — Renombrar con InputDialog

Escribe _renombrar(id) que pida el nuevo nombre con CTkInputDialog y actualice (ojo con la cancelación).

Ver solución
python
def _renombrar(self, id):
    dialogo = ctk.CTkInputDialog(text="Nuevo nombre:", title="Renombrar")
    nuevo = dialogo.get_input()
    if not nuevo:                     # None (canceló) o "" (vacío): no tocar nada
        return
    datos.actualizar_contacto(self.con, id, {"nombre": nuevo.strip()})
    self._pintar()

Ejercicio 3 — La ventana única

Escribe _abrir_preferencias que nunca abra dos ventanas de preferencias a la vez.

Ver solución
python
def _abrir_preferencias(self):
    if getattr(self, "prefs", None) is not None and self.prefs.winfo_exists():
        self.prefs.focus()            # ya existe: al frente
        return
    self.prefs = VentanaPreferencias(self)

❓ Preguntas y respuestas (autoevaluación)

1. ¿Cuántas ventanas ctk.CTk puede tener una app y qué usa para las demás?

Una sola raíz CTk; las demás son CTkToplevel.

2. ¿Qué hace grab_set() en un Toplevel?

Lo vuelve modal: bloquea la interacción con la ventana principal hasta cerrarlo.

3. ¿Qué devuelven askyesno y un filedialog cancelado?

askyesnoTrue/False; el filedialog cancelado devuelve "" (¡compruébalo antes de abrir!).

4. ¿Cómo evitas que un botón abra la misma ventana 5 veces?

Guardando la referencia y comprobando winfo_exists() antes de crear otra (si existe, focus()).


📎 Apuntes relacionados

➡️ Siguiente

Clase 33 · Threading — operaciones largas sin congelar la ventana.