Ir al contenido

Lista de tareas

En este tutorial construimos una lista de tareas: la primera app clásica, y una buena, porque tiene todo lo que tiene una app real: datos, un formulario para añadirlos, una lista para mostrarlos y formas de cambiarlos.

Objetivo Tiempo Nivel Necesitas
Una lista de tareas con filtros y atajos 30 minutos Básico · Python Tutorial en 5 pasos (pasos 1 y 2)
  • Guardar los datos de la app en objetos de Python normales (un dataclass).
  • Leer un campo de texto mientras el usuario escribe y enviarlo con Enter.
  • Dibujar una lista de filas, cada una con una casilla y un botón de borrar.
  • Filtrar lo que se muestra y añadir atajos de teclado.

La app terminada

  1. El modelo. Una tarea es un dataclass. El estado de la app es la lista de tareas, el texto que se está escribiendo y el filtro activo:

    @dataclass
    class Task:
    title: str
    done: bool = False
    class Todo(pz.App):
    title = "Tareas"
    size = (520, 560)
    def __init__(self):
    self.tasks = [Task("Crear mi primera app")]
    self.draft = ""
    self.filter = "Todas"
  2. Añadir tareas. El campo es controlado: muestra self.draft y cada pulsación lo actualiza. Enter (on_submit) y el botón llaman al mismo método:

    def add(self):
    if self.draft.strip():
    self.tasks.append(Task(self.draft.strip()))
    self.draft = ""
    return Command.focus("new-task") # devuelve el foco al campo
    # en view():
    pz.row([
    ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
    placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"),
    ui.button("Añadir", self.add, variant="solid"),
    ], spacing=8)
  3. Dibujar la lista. Cada tarea es un ui.list_item con una casilla al inicio y un botón de borrar al final. Las lambdas capturan task porque task_row recibe la tarea como argumento:

    def task_row(self, task):
    return ui.list_item(
    ui.label(task.title, color="text_muted" if task.done else "text"),
    lambda: self.toggle(task),
    start=ui.checkbox(task.done, lambda _: self.toggle(task)),
    end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"),
    height=34,
    inset=True,
    )
  4. Filtrar. ui.segmented cambia self.filter, y view() solo muestra las tareas visibles. Como view() se vuelve a llamar después de cada cambio, no hay que “refrescar” nada:

    ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter, lambda f: setattr(self, "filter", f))
  5. Atajos. on_key recibe cada tecla como texto:

    def on_key(self, combo):
    if combo == "ctrl-l":
    return self.clear_done()
    if combo == "ctrl-t":
    self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
  6. Foco inicial. on_start se ejecuta una vez al abrir la ventana:

    def on_start(self):
    return Command.focus("new-task")
examples/todo.py
"""A to-do list: state, text input, list items, filters and keyboard shortcuts.
python examples/todo.py
Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/todo-app/
"""
from dataclasses import dataclass
from itertools import count
import pyzed as pz
from pyzed import Command, ui
_ids = count(1)
@dataclass
class Task:
title: str
done: bool = False
id: int = 0
def __post_init__(self):
self.id = self.id or next(_ids)
class Todo(pz.App):
title = "Tareas"
size = (520, 560)
min_size = (400, 360)
def __init__(self):
self.tasks = [Task("Leer la guía de Pyzed", done=True), Task("Crear mi primera app"), Task("Probar los temas")]
self.draft = ""
self.filter = "Todas"
# -- actions -----------------------------------------------------------
def add(self):
if self.draft.strip():
self.tasks.append(Task(self.draft.strip()))
self.draft = ""
return Command.focus("new-task")
def toggle(self, task):
task.done = not task.done
def remove(self, task):
self.tasks.remove(task)
def clear_done(self):
self.tasks = [t for t in self.tasks if not t.done]
def on_start(self):
return Command.focus("new-task")
def on_key(self, combo):
if combo == "ctrl-l":
return self.clear_done()
if combo == "ctrl-t":
self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
# -- view ----------------------------------------------------------------
def visible(self):
if self.filter == "Pendientes":
return [t for t in self.tasks if not t.done]
if self.filter == "Hechas":
return [t for t in self.tasks if t.done]
return self.tasks
def task_row(self, task):
return ui.list_item(
ui.label(task.title, color="text_muted" if task.done else "text"),
lambda: self.toggle(task),
start=ui.checkbox(task.done, lambda _: self.toggle(task)),
end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"),
height=34,
inset=True,
)
def view(self):
pending = sum(not t.done for t in self.tasks)
tasks = self.visible()
return pz.container(
pz.column(
[
pz.row([ui.heading("Tareas", level=2), pz.hspace(), ui.badge(f"{pending} pendientes")],
align="center"),
pz.row(
[
ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"),
ui.button("Añadir", self.add, variant="solid"),
],
spacing=8,
),
pz.row(
[
ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter,
lambda f: setattr(self, "filter", f)),
pz.hspace(),
ui.button("Limpiar hechas", self.clear_done, variant="ghost", size="sm", keys="ctrl-l"),
],
align="center",
),
ui.card(
pz.scrollable(pz.column([self.task_row(t) for t in tasks], spacing=2), height="fill")
if tasks else pz.center(ui.muted("No hay tareas aquí."), height=120),
padding=6,
),
],
spacing=14,
max_width=640,
),
padding=24,
width="fill",
height="fill",
align_x="center",
bg=self.resolved_theme().editor,
)
if __name__ == "__main__":
pz.run(Todo)
Windows, macOS y Linux
uv run --with pyzed-ui python examples/todo.py
Widget Evento Handler Recibe
ui.input on_input lambda text: setattr(self, "draft", text) El texto.
ui.input, Añadir on_submit, on_press self.add Nada.
ui.segmented on_select lambda f: setattr(self, "filter", f) El filtro.
ui.list_item on_press lambda: self.toggle(task) Nada.
ui.checkbox on_toggle lambda _: self.toggle(task) True/False (se ignora: _).
ui.icon_button on_press lambda: self.remove(task) Nada.
La app on_start, on_key enfoca el campo; ctrl-l, ctrl-t —, la combinación.

Renombra una tarea con doble clic y abre un menú con clic derecho:

def __init__(self):
...
self.renaming = None # la tarea que se renombra
self.menu_for = None # la tarea con el menú abierto
def task_row(self, task):
return ui.list_item(
ui.label(task.title), lambda: self.toggle(task),
start=ui.checkbox(task.done, lambda _: self.toggle(task)),
on_double_press=lambda: setattr(self, "renaming", task), # doble clic
on_right_press=lambda: setattr(self, "menu_for", task), # clic derecho
height=34, inset=True,
)
def view(self):
page = ... # la página del tutorial
dialog = None
if self.renaming:
task = self.renaming
dialog = ui.prompt("Renombrar", task.title, lambda t: setattr(task, "title", t),
lambda: setattr(self, "renaming", None), lambda: setattr(self, "renaming", None))
return ui.dialog_layer(page, dialog)