Apariencia
📙 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) yfiledialog. - 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étodo | Qué 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.
pythondef _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ápido — CTkInputDialog:
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 avisar — tkinter.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ón | Botones | Devuelve |
|---|---|---|
showinfo / showwarning / showerror | OK | — |
askyesno | Sí / No | True/False |
askokcancel | OK / Cancelar | True/False |
Elegir archivos — tkinter.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,messageboxyfiledialogdisponibles. 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 sonCTkToplevel.
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?
askyesno→True/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
- Ventana-clase (herencia) → Clase 15
- El CRUD que estos diálogos disparan → Clase 31
- filedialog + CSV → Clase 21
➡️ Siguiente
Clase 33 · Threading — operaciones largas sin congelar la ventana.