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.pngy elmain.pycon 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
- Antes de empezar: la técnica Pomodoro
- El widget
Canvase imágenes en Tkinter - Completar la interfaz de usuario
- Programas dirigidos por eventos y el método
after - Tipado dinámico
- 🚀 Proyecto del día: el temporizador Pomodoro
- 🌟 Proyecto extra: el cronómetro del debate escolar
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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:
- Elegir una tarea exigente (escribir un ensayo, programar un proyecto…).
- Trabajar en ella 25 minutos sin interrupciones.
- Tomar un descanso corto de 5 minutos.
- 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,*argsy**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
CanvasyPhotoImage. - 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:
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.
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_imagenecesita 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 argumentoimageespera un objetoPhotoImage, 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.
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*argsson las coordenadas x e y; los**kwson las opciones comotext,fillofont. ¡Lo que aprendiste ayer! filles 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.
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). YELLOWva 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=0quita 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
PhotoImageen 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_rectangleen las proporciones correctas (investígalas). - 🟠 Reto 3: dibuja un semáforo con tres
create_ovaly 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).bges 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 porgrid(): 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_buttonen (0, 2) yreset_buttonen (2, 2), conhighlightthickness=0para quitar el borde blanco.check_marks: una etiqueta confg=GREENybg=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
whilecontime.sleepbloquea 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):
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
*argsque siguen se entregan a la función. ¡Otra vez*args!
Repetir con after: una función que se programa a sí misma.
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:
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_downantes de crear el lienzo, obtienesNameError: 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 usamath.floor(), que "devuelve el mayor entero menor o igual a x". - Segundos: el residuo con
%.245 % 60= 5.
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 enteracount // 60para números positivos, ydivmod(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:
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:
a = 3 # type(a) → int
a = "Hello" # type(a) → strPero Python también es fuertemente tipado. Siempre sabe de qué tipo es cada dato, y no mezcla tipos incompatibles:
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 luegoif count_sec < 10:, el programa falla al llegar a 0 segundos conTypeError: '<' 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:02dsignifica "entero de al menos 2 dígitos, rellenando con ceros". Con 245 segundos da4: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
- Ciclo: trabajo (25) → descanso corto (5) → trabajo → descanso corto → trabajo → descanso corto → trabajo → descanso largo (20).
- El título cambia: "Work" en verde, "Break" en rosado (descanso corto) y "Break" en rojo (descanso largo).
- Al terminar una cuenta, empieza automáticamente la siguiente.
- Una ✔ por cada sesión de trabajo completada.
- 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) ytimer. - Operador
%para decidir el tipo de sesión. window.afterywindow.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.
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 (
reps8) también es par. ¿Qué pasa si revisas primero "es par" y después "es múltiplo de 8"? Pruébalo mentalmente conreps = 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.
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? Porquereps / 2siempre da un float, yrange()necesita un entero. - Traza: al terminar el primer trabajo,
start_timer()suberepsa 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_markporcheck_marksen 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:
timer = None
def count_down(count):
...
if count > 0:
global timer
timer = window.after(1000, count_down, count - 1)Luego reset_timer() debe:
- Cancelar el temporizador:
window.after_cancel(timer). - Poner el texto del lienzo en
"00:00"(¡conitemconfig!). - Poner el título en
"Timer". - Borrar las marcas.
- Reiniciar
repsa 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!
repsseguía contando. Solución:global repsyreps = 0dentro dereset_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 * 60da6.0, un float (verificado), y el reloj mostraría algo como0: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:
- Reset antes de Start.
timervaleNoneywindow.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 dereset_timerno se ejecuta. (Pista: revisa sitimertiene un valor antes de cancelar.) - 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 yrepsavanza de dos en dos. (Pista: desactiva el botón constart_button.config(state="disabled")mientras corre, o no hagas nada si ya hay un temporizador activo. Recuerda reactivarlo en Reset.) - 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. Investigawindow.lift(),window.attributes("-topmost", True)ywindow.deiconify(), y prueba cuál funciona en tu sistema operativo. - Un sonido al terminar cada sesión (investiga
window.bell()). - Botones o una
Spinboxpara 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
- Los turnos se guardan en una lista de diccionarios (
[{"equipo": ..., "fase": ..., "minutos": ...}, ...]), no en una cadena deif. - Un lienzo o una etiqueta grande con el tiempo en formato
MM:SS. - El color de la ventana o del título cambia según el equipo (por ejemplo, azul "A favor" y naranja "En contra").
- Últimos 30 segundos: el reloj se pone en rojo.
- Botones Iniciar, Pausar/Continuar, Siguiente turno (saltar) y Reiniciar.
- Al terminar el último turno, mostrar "Debate finalizado".
- Los tres problemas de la sección 🧪 del Pomodoro deben estar resueltos desde el principio.
Restricciones técnicas
afteryafter_cancel(nada detime.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
- ¿Qué es un programa dirigido por eventos? ¿Por qué
time.sleepen un bucle lo arruina? - ¿Qué diferencia hay entre
configeitemconfig? - Explica con un ejemplo propio la diferencia entre "fuertemente tipado" y "tipado dinámico".
Análisis de código
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
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?
- Pídele a una IA: "En Tkinter, haz una cuenta regresiva de 10 segundos en una etiqueta". Revisa si usa
afterotime.sleep. - 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)? - 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.) - Pídele que resuelva el problema de "Start dos veces" y verifica su solución con la prueba de la tabla.
- 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 rangeencreate_image,TclErrorpor la imagen que no se encuentra,NameErrorpor usar el lienzo antes de crearlo,aftercon paréntesis,TypeErroral comparar"00" < 10,ValueErrordeafter_cancel(None)y las cuentas regresivas duplicadas.