Ir al contenido

Apps, ventanas, formularios y componentes

Todo lo que escribes en Pyzed es uno de cuatro tipos de clase (o archivo). Cada uno tiene unos pocos métodos que Pyzed llama por ti en el momento justo: esta página los lista, dice cuándo se ejecutan y muestra qué poner en ellos.

Escribes Es Pyzed llama a
class MiApp(pz.App) La app y su ventana principal. view(), update(), on_start(), on_key(), subscriptions()…
class Ajustes(pz.Window) Otra ventana de la misma app. view(), on_open(), on_close(), on_resize()…
login.pzform + login.py Un formulario dibujado en Pyzed Design, con sus handlers. form_load(form, event), <control>_<evento>(form, event)…
class Rating(pz.Component) Una pieza de interfaz reutilizable con props, estado y eventos. setup(), render(), on_mount(), on_unmount()
flowchart LR
    Start(["pz.run(MiApp)"]) --> Init["__init__()<br/>estado inicial"]
    Init --> OnStart["on_start()"]
    OnStart --> View["view()"]
    View -->|árbol de widgets| Draw["Iced lo dibuja"]
    Draw -->|clic, tecla, temporizador…| Event{"evento"}
    Event -->|función| Call["tu método"]
    Event -->|mensaje| Update["update(mensaje)"]
    Call --> View
    Update --> View
    Draw -.->|se cierra la ventana| Close["on_close()"]
import pyzed as pz
from pyzed import ui
class Notes(pz.App):
title = "Notas" # las opciones de la ventana son atributos de clase
size = (720, 480)
theme = "One Dark"
def __init__(self): # 1. el estado inicial: atributos normales
self.notes = []
self.draft = ""
self.saved_at = None
def on_start(self): # 2. una vez, cuando la ventana está abierta
return pz.Command.focus("draft")
def view(self): # 3. después de cada cambio: describe la ventana
return pz.column([
ui.input(self.draft, self.set_draft, id="draft", placeholder="Nota nueva",
on_submit=self.add),
pz.column([ui.list_item(n, icon="sticky-note") for n in self.notes]),
ui.muted(f"Guardado a las {self.saved_at}" if self.saved_at else "Sin guardar"),
], spacing=8, padding=16)
def set_draft(self, text): # 4. los callbacks cambian el estado
self.draft = text
def add(self):
if self.draft.strip():
self.notes.append(self.draft.strip())
self.draft = ""
def update(self, message): # 5. aquí llegan los mensajes que no son funciones
if message == "save":
self.saved_at = "ahora"
def on_key(self, combo): # 6. cada tecla pulsada en la ventana
if combo == "ctrl-s":
return self.update("save")
def subscriptions(self): # 7. temporizadores y flujos activos ahora mismo
return [pz.every(60, "save")] if self.notes else []
pz.run(Notes)
Método Cuándo lo llama Pyzed Qué poner Puede devolver
__init__() Una vez, al crear la app (pz.run(MiApp) la crea). El estado inicial como atributos. Ni widgets ni E/S. —
view() Al empezar y después de cada evento, mensaje o comando. Construir y devolver el árbol de widgets a partir del estado. Nunca cambies el estado aquí. Un Widget (obligatorio).
update(message) Cuando el handler de un widget es un valor (texto, tupla, dataclass…) o una función devuelve uno. Cambiar el estado según el mensaje. None, un Command, una lista, una corrutina u otro mensaje.
on_start() Una vez, después de abrir la ventana. Cargar datos, enfocar un campo, empezar una tarea en segundo plano. Un Command, una corrutina o un mensaje.
on_key(combo) En cada tecla de la ventana, antes que el widget enfocado. combo es como "ctrl-shift-p". Atajos globales. Devolver algo (aunque sea []) evita que la tecla mueva el foco. Como update.
on_key_up(key) Cuando se suelta una tecla ("left", "space"). Juegos y controles de mantener pulsado. Como update.
subscriptions() Después de cada actualización. Devolver los temporizadores, flujos y callbacks por fotograma que deben correr ahora: pz.every, pz.listen, pz.frames. La que dejas de devolver se detiene. Una lista.
on_close() Cuando el usuario cierra la ventana. Preguntar si guardar. Devuelve False para dejarla abierta. None o False.
on_resize(width, height) Cuando la ventana cambia de tamaño. Cambiar de diseño (mira también self.window_size). Como update.
frame_menus() Cuando frame = "custom" dibuja la barra de título. Los menús de su izquierda: [ui.Menu("Archivo", [...])]. Una lista o None.

Y los que llamas tú:

Método Qué hace
run(reload=None) Abre la ventana y bloquea hasta que la app se cierra (igual que pz.run(MiApp)). reload=True activa la recarga en caliente.
screenshot(path, size=None) Dibuja el estado actual en un PNG sin pantalla (tests, documentación).
resolved_theme() El tema en uso ahora mismo, con pz.set_style aplicado: self.resolved_theme().accent.

Atributos útiles que el runtime mantiene al día: self.window_size, self.window_focused, self.window_maximized, self.window_position y self.system_dark. Las opciones de la ventana (title, size, min_size, resizable, frame, transparent, icon, splash, tab_focus…) están en la referencia de App.

Una Window es una App sin las partes de toda la aplicación: tiene su propio view() y sus opciones de ventana, y manda sus mensajes al update() de la app. Se abre devolviendo un comando:

class Settings(pz.Window):
title = "Ajustes"
size = (420, 300)
def on_open(self): # la ventana ya está en pantalla
self.draft = self.app.prefs.copy() # self.app es la App que la abrió
def view(self):
return pz.column([
ui.switch(self.draft["dark"], self.set_dark, label="Modo oscuro"),
ui.button("Guardar", self.save, variant="solid"),
], spacing=12, padding=16)
def set_dark(self, value):
self.draft["dark"] = value
def save(self):
self.app.prefs = self.draft
return self.close() # un comando
def on_close(self): # el usuario pulsó la X de la ventana
return None if self.draft == self.app.prefs else False
class Main(pz.App):
prefs = {"dark": True}
def view(self):
return ui.button("Ajustes…", lambda: Settings().open())
Método Cuándo Notas
view(), on_key(), on_key_up(), on_resize(), subscriptions() Como en App. —
on_open() La ventana se abrió (self.app ya está puesto). Puede devolver un comando.
on_close() El usuario pidió cerrarla. False la deja abierta.
update(message) Un mensaje producido dentro de la ventana. Por defecto lo reenvía a self.app.update.
open(), close(), drag(), minimize(), toggle_maximize() Los llamas tú. Cada uno devuelve un Command: devuélvelo desde un handler.

Mira Varias ventanas para posiciones, ventanas modales y marcos propios.

Un formulario que dibujas en Pyzed Design se guarda como login.pzform (los controles y sus propiedades) y login.py (tu código). El archivo de código no tiene clase: cada evento es una función llamada <control>_<evento>, y los eventos del propio formulario son form_<evento>. Cada handler recibe el form en marcha y el event:

# login.py: los handlers de login.pzform
def form_load(form, event): # se abrió la ventana
form.status.text = ""
return form.email.focus() # los handlers pueden devolver comandos
def email_change(form, event): # cada tecla: event.value es el texto nuevo
form.sign_in.enabled = "@" in event.value
def password_submit(form, event): # Enter en la contraseña
return sign_in_click(form, event)
def sign_in_click(form, event):
if not form.validate(): # aplica las reglas de validación del diseñador
return
form.progress.show()
return form.open("welcome", data=form.email.text)
def form_key(form, event): # cada tecla: event.value es la combinación
if event.value == "escape":
return form.close()
def form_close(form, event): # devuelve False para dejar la ventana abierta
if form.password.text:
return False
Handler Cuándo event.value
form_load Una vez, al abrirse la ventana. None
form_close La ventana se está cerrando; False la deja abierta. None
form_key Cada tecla pulsada. La combinación, "ctrl-s"
form_resize La ventana cambió de tamaño. (ancho, alto)
<control>_<evento> El control lanzó el evento (click, change, submit, tick…). Depende del evento: el texto nuevo, True/False, el elemento elegido…

El objeto form es tu acceso a la ventana: form.<control>.<propiedad> lee y escribe propiedades, form.<control>.show() / .hide() / .focus(), form.title, form.data (lo que pasó form.open(..., data=)), form.validate(), form.close() y form.open("otro_formulario"). Los eventos de cada control están en Eventos y handlers y en cada página de la biblioteca.

Ejecuta un formulario desde Python, o ábrelo desde una app escrita en código:

from pyzed import designer
designer.run("ui/login.pzform") # el formulario es la app (igual que `pyzed run`)
class Main(pz.App): # o un formulario como una ventana más
def view(self):
return ui.button("Entrar…", lambda: designer.open_window("login", data="ada@example.com"))

Un componente es una clase con props (atributos de clase con anotación, que pasa el padre), estado (atributos que conserva entre dibujados) y eventos (que lanza con self.emit). Llamarlo devuelve un widget.

class Stepper(pz.Component):
"""Un número con botones − y +."""
value: int = 0 # props
step: int = 1
min: int = 0
max: int = 10
events = {"change": "Cambió el valor (event.value es el número nuevo)."}
pressed: int = pz.state(0) # estado, privado de cada instancia
def setup(self): # una vez, antes del primer render
self.pressed = 0
def on_mount(self): # después del primer render
return None
def change(self, delta):
self.pressed += 1
new = max(self.min, min(self.max, self.value + delta))
return self.emit("change", new) # llama al on_change del padre
def render(self): # en cada render del padre
return pz.row([
ui.icon_button("minus", lambda: self.change(-self.step)),
pz.text(str(self.value), size=18),
ui.icon_button("plus", lambda: self.change(self.step)),
], spacing=8, align="center")
def on_unmount(self): # ya no se dibuja
print(f"pulsado {self.pressed} veces")
# En cualquier view:
Stepper(value=self.copies, max=99, on_change=lambda e: setattr(self, "copies", e.value))
Método Cuándo Notas
setup() Una vez, antes del primer render. Las props ya están puestas. Inicializa el estado.
render() Cada vez que el view() del padre llama al componente. Devuelve widgets; lee self.<prop> y el estado.
on_mount() Después del primer render. Puede devolver un Command o un mensaje. El handler on_mount= del padre también se ejecuta.
on_unmount() Cuando el padre deja de dibujarlo. Su estado se descarta. Para temporizadores, cierra archivos.
emit(name, value) Lo llamas tú. Llama al on_<nombre> del padre con un pz.Event(value=..., source=self). Devuelve su resultado.
handles(name) Lo llamas tú. Si el padre pasó on_<nombre>: oculta un botón que nadie escucha.
provide() Antes de cada render. Un dict compartido con todo lo que se dibuja dentro (pz.use_context(nombre)).
context(name) Lo llamas tú. El valor más cercano compartido con pz.provide o provide().
Rating.create(...) En tests. Una instancia sin dibujarla: Rating.create(value=3).render().

Todo componente acepta además los eventos comunes (on_click, on_drag, on_key_down…), style=, class_=, id=, animate= y key=. Mira Componentes propios para hijos, contexto, hojas de estilo y cómo aparecen los componentes en la caja de herramientas del diseñador.

Un evento es un nombre más un valor que una pieza de la interfaz le da a quien la usa. Hay tres formas de crear uno, de la más estructurada a la más simple.

Declara los eventos y lánzalos con self.emit(nombre, valor). El padre los escucha con on_<nombre>=:

class ColorRow(pz.Component):
colors: list = ()
selected: str = ""
events = {
"pick": "Se hizo clic en un color (event.value es el color).",
"remove": "Se pulsó la × de un color (event.value es el color).",
}
def render(self):
return pz.row([
pz.row([
ui.color_swatch(c, size=28 if c == self.selected else 20,
on_press=lambda c=c: self.emit("pick", c)),
self.handles("remove") and ui.icon_button("x", lambda c=c: self.emit("remove", c), size="xs"),
], spacing=2)
for c in self.colors
], spacing=8)
ColorRow(colors=self.palette, selected=self.color,
on_pick=lambda e: setattr(self, "color", e.value),
on_remove=lambda e: self.palette.remove(e.value))
  • events es un dict {nombre: descripción} (la descripción se ve en el diseñador y en la referencia) o una tupla de nombres. Las subclases suman eventos a los que heredan.
  • El handler recibe un pz.Event con type (el nombre), value y source (el componente). Un handler sin argumentos (on_pick=self.refresh) se llama sin nada.
  • emit de un nombre no declarado lanza un error, así que las erratas se ven enseguida.
  • Todo componente emite además mount y unmount.

Los componentes de pyzed.ui son funciones normales con parámetros on_*. Las tuyas también pueden serlo: recibe el callback como parámetro y dáselo a un widget, o envuélvelo para pasarle un valor.

def tag_list(tags, on_remove=None, on_add=None):
"""Etiquetas como chips. on_remove(tag) al pulsar la × de un chip, on_add() para el botón +."""
return pz.row([
*[ui.chip(t, on_remove=on_remove and (lambda t=t: on_remove(t))) for t in tags],
on_add and ui.icon_button("plus", on_add, tooltip="Añadir etiqueta"),
], spacing=6)
tag_list(self.tags, on_remove=self.tags.remove, on_add=self.new_tag)

Un mensaje es un evento para toda la app: cualquier valor que llega a update. Úsalo cuando lo mismo puede venir de varios sitios (un botón, un atajo, un menú, un temporizador, un hilo):

from dataclasses import dataclass
@dataclass
class Downloaded: # un mensaje con datos
name: str
size: int
def worker(url): # se ejecuta en un hilo
data = fetch(url)
pz.post(Downloaded(url, len(data))) # cualquier hilo puede mandar un mensaje a update()
class App(pz.App):
def update(self, message):
match message:
case Downloaded(name, size):
self.files.append((name, size))

En Pyzed Design, los eventos propios de un componente diseñado se añaden en su tarjeta Eventos y se lanzan desde su código con self.emit("change", valor). Mira Diseñador de componentes.