Saltar al contenido
Inicio 100 Días de Código en Python — Día 28: Tkinter, tipado dinámico y la aplicación Pomodoro

100 Días de Código en Python — Día 28: Tkinter, tipado dinámico y la aplicación Pomodoro

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 28 (nivel intermedio).

Este tutorial adapta pedagógicamente el Día 28. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso. Los retos 🟢🟡🟠🔴 y el proyecto extra son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: los ejemplos se ejecutaron con Python 3.14 y Tk 8.6 en Windows. Los archivos iniciales del proyecto (tomato.png y el main.py con las constantes) no están en la carpeta. La lección dice que el archivo trae constantes de colores (rosado, rojo, verde y amarillo), el nombre de la fuente (Courier) y los minutos de trabajo y de descanso, pero no muestra los códigos hexadecimales. Los códigos que aparecen en este tutorial son de ejemplo; puedes elegir los tuyos en colorhunt.co (se abre en otra pestaña), el sitio que recomienda el curso.


Tabla de contenidos

  1. Antes de empezar: la técnica Pomodoro
  2. El widget Canvas e imágenes en Tkinter
  3. Completar la interfaz de usuario
  4. Programas dirigidos por eventos y el método after
  5. Tipado dinámico
  6. 🚀 Proyecto del día: el temporizador Pomodoro
  7. 🌟 Proyecto extra: el cronómetro del debate escolar
  8. 🧩 Comprueba lo aprendido
  9. 🤖 Programar con apoyo de IA
  10. 🛠️ Correcciones técnicas al material original
  11. 📋 Matriz de trazabilidad
  12. 📦 Evidencias de aprendizaje del Día 28

0. Antes de empezar: la técnica Pomodoro

(Fuente: 213 - Day 28 Goals- what we will make by the end of the day.md)

Hoy construiremos, desde cero, una aplicación completa con Tkinter: un temporizador Pomodoro.

La técnica, creada por Francesco Cirillo y descrita en su libro, consiste en:

  1. Elegir una tarea exigente (escribir un ensayo, programar un proyecto…).
  2. Trabajar en ella 25 minutos sin interrupciones.
  3. Tomar un descanso corto de 5 minutos.
  4. Después de 4 ciclos de trabajo, tomar un descanso largo (en la aplicación del curso, de 20 minutos).

"Pomodoro" significa tomate en italiano; por eso la aplicación tiene un tomate con el reloj encima. Cada sesión de trabajo completada suma una marca de verificación ✔.

💡 Para pensar: ¿cuánto tiempo puedes concentrarte antes de revisar el celular? Prueba la técnica durante una semana de estudio y registra cómo te fue. Es un buen dato para la reflexión final del día.

Prerequisito general: todo el Día 27 (ventanas, etiquetas, botones, grid, config, command, *args y **kwargs), el operador módulo % (Día 3), variables globales (Día 12), funciones que se llaman a sí mismas y f-strings.


1. El widget Canvas e imágenes en Tkinter

(Fuente: 214 - How to work with the Canvas Widget and Add Images to Tkinter.md)

🎯 Objetivo de aprendizaje

  • Poner una imagen en una ventana usando Canvas y PhotoImage.
  • Escribir texto encima de la imagen.
  • Usar colores hexadecimales y quitar el borde del lienzo.

📖 Explicación conceptual

Punto de partida. El proyecto inicial tiene tomato.png y un main.py dividido en secciones (constantes, reinicio del temporizador, mecanismo del temporizador, cuenta regresiva y configuración de la interfaz). Primero la ventana:

Python
from tkinter import *

window = Tk()
window.title("Pomodoro")

window.mainloop()

El lienzo (Canvas). Funciona como un lienzo real: permite dibujar cosas unas encima de otras. Lo necesitamos para poner el tomate y, sobre él, el texto del reloj.

Python
canvas = Canvas(width=200, height=224)
tomato_img = PhotoImage(file="tomato.png")
canvas.create_image(100, 112, image=tomato_img)
canvas.pack()
  • La imagen mide aproximadamente 200 × 223 píxeles; el curso usa 224 para trabajar con números pares.
  • create_image necesita la posición (x, y) del centro de la imagen. Para centrarla: la mitad del ancho y la mitad del alto.
  • No basta con escribir "tomato.png". El argumento image espera un objeto PhotoImage, que lee el archivo. Si la imagen está en otra carpeta, usa su ruta relativa o absoluta (Día 24).
  • El lienzo también tiene create_text, create_rectangle, create_oval…

Texto sobre la imagen.

Python
canvas.create_text(100, 130, text="00:00", fill="white", font=(FONT_NAME, 35, "bold"))
  • Observa la ayuda de PyCharm: create_text(*args, **kw). Los *args son las coordenadas x e y; los **kw son las opciones como text, fill o font. ¡Lo que aprendiste ayer!
  • fill es el color del texto.
  • La posición y = 130, un poco más abajo del centro, se encontró probando: así el texto queda sobre la parte roja y redonda del tomate.

Relleno, colores y el borde del lienzo.

Python
YELLOW = "#f7f5dd"   # código de EJEMPLO (ver la nota inicial)

window.config(padx=100, pady=50, bg=YELLOW)
canvas = Canvas(width=200, height=224, bg=YELLOW, highlightthickness=0)
  • Un código hexadecimal empieza con # seguido de 6 dígitos o letras (de la A a la F).
  • YELLOW va sin comillas porque es el nombre de una constante, no el texto "YELLOW".
  • bg (background) es una opción estándar, común a casi todos los widgets.
  • highlightthickness=0 quita el borde blanco del lienzo. El curso cuenta que encontrarla le costó bastante búsqueda en Google y Stack Overflow, "porque Tkinter no está bien documentado". Ojo: se escribe todo junto, sin guion bajo.

Consejo del curso: con Tkinter hay que ajustar a ojo las posiciones. Después de quitar el borde, el tomate se desplazó y hubo que volver a centrar la imagen y el texto en x = 100.

⚠️ Errores frecuentes

Error Qué ocurre Solución
create_image(image=img) sin coordenadas IndexError: tuple index out of range (verificado) Pasa primero x e y
image="tomato.png" TclError: image "tomato.png" doesn't exist (verificado): se espera un PhotoImage, no un string PhotoImage(file="tomato.png")
Nombre de archivo mal escrito o carpeta equivocada TclError: couldn't open "tomato.png": no such file or directory (verificado con otro nombre) Revisa la ortografía y la carpeta desde donde se ejecuta el programa (Día 24)
bg="YELLOW" con comillas Tkinter busca un color llamado así; puede funcionar con otro tono o fallar Usa la constante sin comillas
highlight_thickness=0 TclError: unknown option highlightthickness, todo junto

🛠️ Nota técnica: el curso crea el PhotoImage en el programa principal. Si algún día lo creas dentro de una función, guárdalo en una variable que siga existiendo (por ejemplo, un atributo o una variable global). Si Python lo elimina de la memoria al terminar la función, la imagen desaparece del lienzo sin mostrar ningún error. Es un problema clásico de Tkinter.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: crea un lienzo con el escudo de tu colegio y el nombre del colegio escrito encima.
  • 🟡 Reto 2: sin imágenes, dibuja en un lienzo una bandera de Colombia con tres create_rectangle en las proporciones correctas (investígalas).
  • 🟠 Reto 3: dibuja un semáforo con tres create_oval y un botón que "encienda" cada luz por turnos. (Pista: canvas.itemconfig, que veremos en la sección 3.)
  • 🔴 Reto 4: busca en colorhunt.co una paleta que represente tu región y explica por qué la elegiste. Úsala en el Reto 1.

2. Completar la interfaz de usuario

(Fuente: 215 - Challenge - Complete the Application's User Interface (UI).md)

📝 Ejercicio original del curso

Enunciado: completa la interfaz para que se vea como la demostración: un título "Timer" arriba, el tomate en el centro, los botones Start y Reset abajo a cada lado, y una marca ✔ en la última fila.

Indicaciones del curso:

  • Para el color del texto de una etiqueta se usa fg (foreground, primer plano). bg es el fondo.
  • Para conseguir el símbolo ✔ en Windows, cópialo de una página (por ejemplo, Wikipedia) y pégalo como texto. En Mac: Edición → Emoji y símbolos.
  • Cambia el pack() del lienzo por grid(): hay muchos widgets y no se pueden mezclar gestores.
  • Si no sabes los tamaños o las fuentes, adivina. Lo importante es la distribución y los widgets correctos.

🔍 Antes de programar

Dibuja la cuadrícula. El curso propone 4 filas × 3 columnas.

Columna 0 Columna 1 Columna 2
Fila 0 Título "Timer"
Fila 1 Lienzo (tomate)
Fila 2 Start Reset
Fila 3 Marcas ✔

Solución del curso (resumen):

  • title_label: text="Timer", fg=GREEN, bg=YELLOW, font=(FONT_NAME, 50), en (1, 0).
  • canvas.grid(column=1, row=1).
  • start_button en (0, 2) y reset_button en (2, 2), con highlightthickness=0 para quitar el borde blanco.
  • check_marks: una etiqueta con fg=GREEN y bg=YELLOW, en (1, 3).

Reto adicional (creado para esta adaptación)

  • 🟠 Reto: agrega una etiqueta en la fila 4 que diga en qué sesión vas ("Sesión 1 de 4"). Aún no funcionará; solo prepárala en la interfaz.

3. Programas dirigidos por eventos y el método after

(Fuente: 216 - Add a Count Down Mechanism.md)

🎯 Objetivo de aprendizaje

  • Explicar por qué un while con time.sleep bloquea una interfaz gráfica.
  • Crear una cuenta regresiva con window.after.
  • Convertir segundos al formato minutos:segundos.

📖 Explicación conceptual

La primera idea (que no funciona). Un while que haga time.sleep(1) y reste 1 cada vez. En la consola funcionaría, pero en una GUI no.

¿Por qué? Un programa de consola solo actúa cuando le das una instrucción y pulsas Enter. Una GUI, en cambio, debe estar vigilando todo el tiempo: ¿hicieron clic?, ¿escribieron algo?, ¿movieron la ventana? Ese vigilante es mainloop(), que revisa cada milisegundo si pasó algo. Por eso decimos que las GUI son dirigidas por eventos (event-driven).

Si pones otro bucle infinito antes de mainloop(), el programa nunca llega a él y la ventana ni siquiera aparece.

La solución: after. Todo widget tiene el método after(ms, función, *args):

Python
def say_something(a, b, c):
    print(a)
    print(b)
    print(c)

window.after(1000, say_something, 3, 5, 8)   # después de 1 s imprime 3, 5 y 8
  • El tiempo se da en milisegundos: 1000 ms = 1 s.
  • La función va sin paréntesis (igual que en command).
  • Los *args que siguen se entregan a la función. ¡Otra vez *args!

Repetir con after: una función que se programa a sí misma.

Python
def count_down(count):
    print(count)
    if count > 0:
        window.after(1000, count_down, count - 1)

count_down(5)   # 5, 4, 3, 2, 1, 0 (uno por segundo)

Sin el if, la cuenta sigue con números negativos.

Mostrar la cuenta en el lienzo. Para cambiar un elemento de un lienzo no se usa config, sino itemconfig:

Python
timer_text = canvas.create_text(100, 130, text="00:00", ...)   # guardamos una referencia

def count_down(count):
    canvas.itemconfig(timer_text, text=count)
    ...
Qué quieres cambiar Método
Un widget (Label, Button…) widget.config(opción=valor)
Un elemento dibujado en un lienzo canvas.itemconfig(elemento, opción=valor)

Error que muestra el curso: si llamas a count_down antes de crear el lienzo, obtienes NameError: name 'canvas' is not defined. El orden importa.

Conectarlo al botón Start. Se crea start_timer(), que llama a count_down(...), y se conecta con command=start_timer.

De segundos a minutos:segundos. Para contar 5 minutos: count_down(5 * 60). Pero ver "288" en el reloj no es útil. Hay que convertir:

  • Minutos: dividir entre 60 y redondear hacia abajo. 245 / 60 = 4.083… → 4. El curso usa math.floor(), que "devuelve el mayor entero menor o igual a x".
  • Segundos: el residuo con %. 245 % 60 = 5.
Python
import math

count_min = math.floor(count / 60)
count_sec = count % 60
canvas.itemconfig(timer_text, text=f"{count_min}:{count_sec}")

El problema pendiente: 300 segundos se muestran como 5:0, no como 5:00. Para arreglarlo necesitamos entender el tipado dinámico.

🛠️ Nota técnica: math.floor(count / 60) es equivalente a la división entera count // 60 para números positivos, y divmod(count, 60) devuelve ambos valores a la vez: divmod(245, 60) → (4, 5) (verificado). Las tres formas son válidas.

⚠️ Errores frecuentes

Error Qué ocurre Solución
while + time.sleep en una GUI La ventana se congela o no aparece Usa after
window.after(1000, count_down(count - 1)) Se ejecuta de inmediato, sin esperar, y todo el conteo ocurre de golpe La función va sin paréntesis y el argumento aparte: after(1000, count_down, count - 1)
canvas.config(timer_text, ...) AttributeError: 'int' object has no attribute 'items' (verificado): config no sirve para elementos del lienzo canvas.itemconfig(timer_text, ...)
Olvidar el if count > 0 La cuenta sigue con negativos Agrega la condición de parada

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: una ventana con una etiqueta que cuente de 10 a 0 y luego muestre "¡Despegue! 🚀".
  • 🟡 Reto 2: un reloj que muestre la hora actual y se actualice cada segundo. (Investiga time.strftime("%H:%M:%S").)
  • 🟠 Reto 3: un cronómetro que cuente hacia arriba desde 00:00 con botones Iniciar y Pausar.
  • 🔴 Reto 4: un contador de "días para vacaciones" que muestre días, horas, minutos y segundos hasta una fecha. (Investiga el módulo datetime; lo trabajaremos en el Día 32.)

4. Tipado dinámico

(Fuente: 217 - Dynamic Typing Explained.md)

🎯 Objetivo de aprendizaje

  • Explicar qué significa que Python sea fuertemente tipado y de tipado dinámico.
  • Formatear los segundos con dos dígitos.

📖 Explicación conceptual

La primera solución del curso para 5:0:

Python
if count_sec == 0:
    count_sec = "00"

¡Funciona! Pero observa algo extraño: la variable tenía un entero (0) y ahora tiene un string ("00"). ¿Cómo es posible?

Tipado dinámico. En Python, el tipo de una variable puede cambiar si le asignas un valor de otro tipo:

Python
a = 3        # type(a) → int
a = "Hello"  # type(a) → str

Pero Python también es fuertemente tipado. Siempre sabe de qué tipo es cada dato, y no mezcla tipos incompatibles:

Python
3 + "4"      # TypeError
"Hello" ** 2 # TypeError
Característica Significado Ejemplo
Fuertemente tipado Los tipos importan; no se mezclan sin conversión 3 + "4" → TypeError
Tipado dinámico Una variable puede cambiar de tipo al reasignarla a = 3 y luego a = "Hola"

El curso compara con lenguajes como C, Java o Swift: allí, una vez declaras una variable con un tipo, siempre debe guardar ese tipo. (Enlaza una respuesta de Stack Overflow que explica esta diferencia.)

📝 Ejercicio original del curso: los segundos de un solo dígito

El problema: con la solución anterior, al llegar a 9 segundos el reloj muestra 4:9, 4:8… en lugar de 4:09, 4:08.

Enunciado: modifica esas dos líneas para que cualquier número de segundos menor que 10 se muestre con un cero adelante (incluido el 00).

🔍 Antes de programar

  • ¿Qué condición cubre a la vez el 0 y los números del 1 al 9?
  • ¿Cómo pegas un "0" delante de un número? (Pista: f-strings.)
  • ¿Sigue haciendo falta el if count_sec == 0?

Solución del curso (en palabras): verificar si count_sec < 10 y, en ese caso, reemplazarlo por un f-string con un cero delante del número.

🛠️ Nota técnica (verificada): si dejas las dos condiciones en este orden, primero if count_sec == 0: count_sec = "00" y luego if count_sec < 10:, el programa falla al llegar a 0 segundos con TypeError: '<' not supported between instances of 'str' and 'int': la variable ya es un string y no se puede comparar con un número. La nueva condición reemplaza a la anterior. Es un buen ejemplo de los riesgos del tipado dinámico.

Alternativa (verificada): el formato de los f-strings resuelve todo sin cambiar el tipo: f"{count_min}:{count_sec:02d}". El :02d significa "entero de al menos 2 dígitos, rellenando con ceros". Con 245 segundos da 4:05; con 300, 5:00.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: en la consola de Python, crea una variable y cámbiale el tipo cuatro veces (int, float, str, list). Imprime type() cada vez.
  • 🟡 Reto 2: escribe una función formatear(segundos) que devuelva "MM:SS" con ambos valores de dos dígitos ("04:05"). Pruébala con 0, 9, 59, 60 y 3599.
  • 🟠 Reto 3: amplíala a "HH:MM:SS". ¿Qué pasa con 86 400 segundos (un día)?
  • 🔴 Reto 4: investiga las anotaciones de tipo de Python (def formatear(segundos: int) -> str:). ¿Obligan a Python a revisar los tipos? ¿Para qué sirven entonces? Compara con lo que dice el curso sobre Java y C.

5. 🚀 Proyecto del día: el temporizador Pomodoro

(Fuentes: 214 a 219)

Problema

Terminar la aplicación: que alterne automáticamente trabajo y descansos, que indique en qué fase está el usuario, que sume marcas ✔ y que el botón Reset funcione.

Contexto

Es una herramienta real y útil para estudiar. El curso invita a personalizarla y a compartirla con amigos y familia.

Requisitos funcionales

  1. Ciclo: trabajo (25) → descanso corto (5) → trabajo → descanso corto → trabajo → descanso corto → trabajo → descanso largo (20).
  2. El título cambia: "Work" en verde, "Break" en rosado (descanso corto) y "Break" en rojo (descanso largo).
  3. Al terminar una cuenta, empieza automáticamente la siguiente.
  4. Una ✔ por cada sesión de trabajo completada.
  5. Reset: detiene el temporizador, pone 00:00, restaura el título "Timer", borra las marcas y reinicia el conteo.

Restricciones técnicas

  • Variables globales reps (repeticiones) y timer.
  • Operador % para decidir el tipo de sesión.
  • window.after y window.after_cancel.

Planificación

Pregunta clave: si numeramos las sesiones (reps) 1, 2, 3…, ¿qué tienen en común las de trabajo? ¿Y los descansos cortos? ¿Y el largo?

reps 1 2 3 4 5 6 7 8
Sesión Trabajo Corto Trabajo Corto Trabajo Corto Trabajo Largo

Construcción paso a paso

Versión 1 — Decidir la sesión (lección 218). Desafío del curso: con una variable global reps y el operador %, haz que start_timer elija cuántos segundos contar.

Python
reps = 0

def start_timer():
    global reps
    reps += 1

    work_sec = WORK_MIN * 60
    short_break_sec = SHORT_BREAK_MIN * 60
    long_break_sec = LONG_BREAK_MIN * 60

    # 🧩 Completa: ¿qué condición identifica el descanso largo?
    # ¿Y el corto? ¿Y el trabajo?

Orden de las condiciones: el descanso largo (reps 8) también es par. ¿Qué pasa si revisas primero "es par" y después "es múltiplo de 8"? Pruébalo mentalmente con reps = 8.

Solución del curso (en palabras): primero if reps % 8 == 0 (largo), luego elif reps % 2 == 0 (corto) y por último else (trabajo).

Versión 2 — ¡Se detiene! Probar para encontrar el error. Consejo del curso: cambia WORK_MIN a 1 minuto para probar rápido. Al llegar a 00:00… no pasa nada. La cuenta termina y nada vuelve a llamar a start_timer.

Lección del curso: hay que probar el programa tan seguido como se escribe código, para encontrar los errores de lógica (los que no muestran ningún mensaje).

Solución: en count_down, agregar un else que llame a start_timer() cuando la cuenta llegue a 0.

Versión 3 — El título indica la fase (desafío del curso). En cada rama del if, cambia title_label con config(text=..., fg=...): "Work" con GREEN, "Break" con PINK y "Break" con RED.

Versión 4 — Las marcas ✔ (lección 219). La etiqueta empieza vacía. Desafío del curso: cada dos repeticiones se completa una sesión de trabajo.

Python
    else:
        start_timer()
        marks = ""
        work_sessions = math.floor(reps / 2)
        # 🧩 Completa: un bucle que agregue "✔" a marks tantas veces como work_sessions
        check_marks.config(text=marks)
  • ¿Por qué math.floor? Porque reps / 2 siempre da un float, y range() necesita un entero.
  • Traza: al terminar el primer trabajo, start_timer() sube reps a 2 → work_sessions = 1 → una ✔. Al terminar el descanso, reps = 3 → floor(1.5) = 1 → sigue una ✔. ✅
  • El curso usa Refactor → Rename de PyCharm para cambiar check_mark por check_marks en todo el archivo de una vez.

Versión final — El botón Reset. Para detener un after hay que usar window.after_cancel(id), y para eso necesitamos guardar lo que devuelve after:

Python
timer = None

def count_down(count):
    ...
    if count > 0:
        global timer
        timer = window.after(1000, count_down, count - 1)

Luego reset_timer() debe:

  1. Cancelar el temporizador: window.after_cancel(timer).
  2. Poner el texto del lienzo en "00:00" (¡con itemconfig!).
  3. Poner el título en "Timer".
  4. Borrar las marcas.
  5. Reiniciar reps a 0.

Conéctala con command=reset_timer.

El error que encuentra el curso: sin el paso 5, si pulsas Reset en medio de un trabajo y luego Start, ¡empieza un descanso! reps seguía contando. Solución: global reps y reps = 0 dentro de reset_timer.

Por último, vuelve a poner los minutos reales: 25, 5 y 20.

Pruebas

Usa tiempos de prueba muy cortos para recorrer el ciclo completo. El curso pone WORK_MIN = 1. Para ir aún más rápido, cambia temporalmente los segundos en start_timer (por ejemplo, work_sec = 6).

⚠️ No uses minutos decimales como WORK_MIN = 0.1: 0.1 * 60 da 6.0, un float (verificado), y el reloj mostraría algo como 0:06.0. Otra consecuencia del tipado dinámico.

Caso Esperado
Start y esperar Work (verde) → Break (rosado) → Work…
Completar 4 trabajos El 8.º período es Break en rojo
Terminar un trabajo Aparece una ✔ nueva
Reset en medio de un trabajo 00:00, "Timer", sin marcas; Start empieza de nuevo con Work
Reset sin haber pulsado Start ⚠️ (ver 🧪)
Start dos veces seguidas ⚠️ (ver 🧪)

🧪 Prueba y depuración

El código del curso tiene tres problemas que no se mencionan en las lecciones:

  1. Reset antes de Start. timer vale None y window.after_cancel(None) produce, en la versión verificada (Python 3.14), ValueError: id must be a valid identifier returned from after or after_idle. El error aparece en la consola y el resto de reset_timer no se ejecuta. (Pista: revisa si timer tiene un valor antes de cancelar.)
  2. Start dos veces. Cada clic inicia otra cadena de after. Quedan dos cuentas regresivas escribiendo sobre el mismo texto: el reloj parece ir el doble de rápido y reps avanza de dos en dos. (Pista: desactiva el botón con start_button.config(state="disabled") mientras corre, o no hagas nada si ya hay un temporizador activo. Recuerda reactivarlo en Reset.)
  3. Después del descanso largo. El ciclo continúa con reps = 9, 10… y las marcas siguen creciendo sin límite. ¿Qué debería pasar después de 4 Pomodoros? Decide tú: ¿se reinician las marcas?, ¿se detiene la aplicación?, ¿se pregunta al usuario?

⭐ Extensión del proyecto

  • Traer la ventana al frente. La demostración del curso (lección 213) muestra que al terminar una sesión la ventana aparece encima de las demás, pero las lecciones de la carpeta no muestran cómo hacerlo. Investiga window.lift(), window.attributes("-topmost", True) y window.deiconify(), y prueba cuál funciona en tu sistema operativo.
  • Un sonido al terminar cada sesión (investiga window.bell()).
  • Botones o una Spinbox para elegir la duración del trabajo.
  • Una etiqueta con la tarea en la que estás trabajando.

6. 🌟 Proyecto extra: el cronómetro del debate escolar

Situación

El área de Ciencias Sociales organiza debates con turnos cronometrados. Hoy los moderadores usan el celular y siempre se confunden de turno. Necesitan una aplicación que muestre de quién es el turno, cuánto tiempo le queda y que avance sola.

Formato del debate (puedes ajustarlo con tu profesor)

Turno Equipo Duración
1 A favor — apertura 3 min
2 En contra — apertura 3 min
3 A favor — réplica 2 min
4 En contra — réplica 2 min
5 A favor — cierre 1 min
6 En contra — cierre 1 min

Requisitos

  1. Los turnos se guardan en una lista de diccionarios ([{"equipo": ..., "fase": ..., "minutos": ...}, ...]), no en una cadena de if.
  2. Un lienzo o una etiqueta grande con el tiempo en formato MM:SS.
  3. El color de la ventana o del título cambia según el equipo (por ejemplo, azul "A favor" y naranja "En contra").
  4. Últimos 30 segundos: el reloj se pone en rojo.
  5. Botones Iniciar, Pausar/Continuar, Siguiente turno (saltar) y Reiniciar.
  6. Al terminar el último turno, mostrar "Debate finalizado".
  7. Los tres problemas de la sección 🧪 del Pomodoro deben estar resueltos desde el principio.

Restricciones técnicas

  • after y after_cancel (nada de time.sleep).
  • Una sola función que formatee el tiempo, usada en todas partes.
  • Nombres descriptivos y comentarios que expliquen las variables globales.

Diferencia clave con el Pomodoro

En el Pomodoro, la secuencia se calcula con %. Aquí la secuencia está escrita en datos. ¿Qué ventaja tiene? Pista: ¿qué tendrías que cambiar si el colegio decide agregar un turno de preguntas del público?

Casos de prueba

Caso Esperado
Iniciar y esperar al final del turno 1 Pasa solo al turno 2 y cambia el color
Pausar y continuar El tiempo sigue donde quedó
"Siguiente turno" en medio de un turno Pasa al siguiente sin que queden dos relojes activos
Terminar el turno 6 "Debate finalizado" y los botones quedan en un estado coherente

7. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué es un programa dirigido por eventos? ¿Por qué time.sleep en un bucle lo arruina?
  2. ¿Qué diferencia hay entre config e itemconfig?
  3. Explica con un ejemplo propio la diferencia entre "fuertemente tipado" y "tipado dinámico".

Análisis de código

Python
def count_down(count):
    canvas.itemconfig(timer_text, text=count)
    if count > 0:
        window.after(1000, count_down, count - 1)

¿Cuántas veces se ejecuta count_down si se llama con count_down(3)? ¿Qué valores muestra?

Predicción

Para cada valor de reps, ¿qué sesión elige el código del curso?

reps = 1, 2, 7, 8, 9, 16

Depuración

Python
reps = 0

def start_timer():
    reps += 1
    if reps % 2 == 0:
        count_down(SHORT_BREAK_MIN * 60)
    elif reps % 8 == 0:
        count_down(LONG_BREAK_MIN * 60)
    else:
        count_down(WORK_MIN * 60)

Hay dos errores. Uno detiene el programa con un mensaje; el otro no muestra ningún mensaje, pero hace que nunca haya descanso largo. Encuéntralos.

Modificación

Cambia el Pomodoro para que el descanso largo ocurra después de 3 sesiones de trabajo en lugar de 4. ¿Qué números tienes que cambiar? ¿Y en la fórmula de las marcas?

Creación

Crea una aplicación "Respira": un círculo en un lienzo que crece durante 4 segundos (inhala), se mantiene 4 segundos y se encoge durante 4 segundos (exhala), con el texto de cada fase. (Pista: canvas.coords o volver a dibujar el óvalo en cada after.)


8. 🤖 Programar con apoyo de IA

Actividad: ¿la IA entiende los programas dirigidos por eventos?

  1. Pídele a una IA: "En Tkinter, haz una cuenta regresiva de 10 segundos en una etiqueta". Revisa si usa after o time.sleep.
  2. Si usó time.sleep, ejecútalo y describe qué pasa con la ventana. Luego pídele que explique por qué falla. ¿Su explicación coincide con la del curso (sección 3)?
  3. Pregúntale: "¿Qué pasa si llamo a after_cancel(None) en Tkinter?". Compara su respuesta con lo que obtuviste tú al probarlo (sección 🧪). Si no coinciden, ¿a quién le crees y por qué? (Pista: el comportamiento puede cambiar según la versión de Python.)
  4. Pídele que resuelva el problema de "Start dos veces" y verifica su solución con la prueba de la tabla.
  5. Anota: ¿qué afirmaciones de la IA comprobaste, cómo y con qué resultado?

9. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
214 Archivos iniciales tomato.png y main.py con constantes No están en la carpeta; los colores de este tutorial son de ejemplo Nota inicial
214 PhotoImage en el programa principal Correcto; si se crea dentro de una función sin guardar la referencia, la imagen desaparece Nota técnica del Tema 1
216 "245/60 es 4.08, 3 recurrentes" 245/60 = 4.0833… (verificado) Tema 3
216 "…después de que lo hayamos dividido limpiamente por 600" Es entre 60; probablemente un error de transcripción Tema 3
216 math.floor(count / 60) Equivale a count // 60; divmod da minutos y segundos a la vez (verificado) Nota técnica del Tema 3
217 Primera solución if count_sec == 0: count_sec = "00" y luego if count_sec < 10 Si se dejan ambas en ese orden: TypeError al llegar a 0 segundos (verificado). La segunda reemplaza a la primera. Alternativa: :02d (verificado) Nota técnica del Tema 4
219 reset_timer llama a window.after_cancel(timer) con timer = None al inicio Si se pulsa Reset antes de Start: ValueError en Python 3.14 (verificado) 🧪 del proyecto
219 El botón Start siempre inicia un temporizador Pulsarlo dos veces crea dos cuentas simultáneas 🧪 del proyecto
213 La ventana aparece al frente al terminar la sesión Las lecciones de la carpeta no muestran cómo se logra Extensión del proyecto

10. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Técnica Pomodoro y objetivos 213 Sección 0 — Prueba personal de la técnica Sección 5
Canvas, PhotoImage, create_image, create_text, bg, highlightthickness, colores hexadecimales 214 Tema 1 Tomate con el texto 00:00 Retos 1–4 del Tema 1 Secciones 5 y 6
fg, ✔ como texto, cambiar pack por grid 215 Tema 2 Completar la interfaz Reto del Tema 2 Sección 5
Programas dirigidos por eventos, after, itemconfig, math.floor, % 216 Tema 3 Cuenta regresiva y botón Start Retos 1–4 del Tema 3 Secciones 5 y 6
Tipado dinámico y fuerte 217 Tema 4 Segundos con dos dígitos Retos 1–4 del Tema 4 Sección 6
reps, sesiones con %, título y colores por fase 218 Sección 5, versiones 1–3 Sesiones distintas; título según la fase Depuración (orden de las condiciones) Sección 6
Marcas ✔, after_cancel, reset_timer, reiniciar reps 219 Sección 5, versiones 4 y final Marcas y Reset Depuración (Reset antes de Start, Start doble) Sección 6
(No existe en el curso original) — — — Cronómetro del debate escolar Sección 6

11. 📦 Evidencias de aprendizaje del Día 28

  • Captura de la interfaz del Pomodoro con tu paleta de colores y la tabla de la cuadrícula.
  • El Pomodoro funcionando, con la tabla de pruebas completa y los tres problemas de 🧪 corregidos.
  • Tu explicación escrita (con un ejemplo) de tipado dinámico frente a tipado fuerte.
  • El cronómetro del debate (proyecto extra).
  • Reflexión: usa tu Pomodoro para estudiar durante una semana. ¿Te ayudó a concentrarte? ¿Qué cambiarías de la aplicación después de usarla de verdad?
  • Cuaderno de errores: IndexError: tuple index out of range en create_image, TclError por la imagen que no se encuentra, NameError por usar el lienzo antes de crearlo, after con paréntesis, TypeError al comparar "00" < 10, ValueError de after_cancel(None) y las cuentas regresivas duplicadas.