Formularios
Los formularios son el punto de partida de la mayoría de las apps de escritorio: campos, botones, reglas y mensajes. Pyzed te da dos formas de hacerlos, y puedes mezclar las dos en una misma app:
- Dibujarlos en Pyzed Design, el diseñador visual: arrastras controles a una ventana,
configuras sus propiedades y escribes solo el Python que corre cuando pasa algo (un archivo
.pzformmás un.pycon los manejadores). Si usaste Visual Basic, Qt Designer o los editores de Godot y Unreal Engine, te resultará familiar. - Escribirlos en Python con la biblioteca de componentes:
ui.field,ui.input,ui.select,ui.switch… en un métodoview().
Formulario de acceso
Sección titulada «Formulario de acceso»Dibujado en el diseñador: un correo y una contraseña, una casilla “recordarme” y un botón que muestra una barra de progreso mientras “inicia sesión”. El archivo de Python son solo cuatro funciones pequeñas, una por evento.

- Los controles son atributos de
form:form.email.text,form.status.color = "error",form.progress.show(). - Un control Temporizador (
ticker) llama aticker_tickcada pocos milisegundos mientras corre; así se llena la barra de progreso sin bloquear la ventana.
uv run --with pyzed-ui pyzed run examples/designer/login.pzformpython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipyzed run examples/designer/login.pzformpy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipyzed run examples\designer\login.pzformpython3 -m pyzed run examples/designer/login.pzformpy -m pyzed run examples\designer\login.pzformManejadores de eventos: examples/designer/login.py
"""Event handlers for the login form.
Each function receives the running ``form`` (controls are attributes: ``form.button1.text``)and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``."""
def sign_in_click(form, event): """Runs when sign_in is clicked.""" if "@" not in form.email.text: form.status.text = "Enter a valid email address." form.status.color = "error" return form.email.focus() form.status.text = "" form.sign_in.enabled = False form.progress.value = 0 form.progress.show() form.ticker.running = True
def ticker_tick(form, event): """Runs every ``interval`` milliseconds while ticker is enabled.""" form.progress.value += 4 if form.progress.value >= 100: form.ticker.running = False form.progress.hide() form.status.color = "success" form.status.text = f"Signed in as {form.email.text}" form.sign_in.enabled = True
def password_submit(form, event): """Runs when Enter is pressed in password.""" return sign_in_click(form, event)
def form_load(form, event): """Runs once when login opens.""" form.status.text = "Tip: any email works in this demo." return form.email.focus()Registro con reglas
Sección titulada «Registro con reglas»Cada campo está enlazado a form.data y tiene reglas definidas en el panel Propiedades (obligatorio, un
correo, al menos 8 caracteres, igual que otro campo). Una línea, form.validate(), las revisa todas y muestra los
mensajes junto a los campos; el saludo y el resumen se actualizan solos mientras escribes.

uv run --with pyzed-ui pyzed run examples/tutorial/designer/signup.pzformpython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipyzed run examples/tutorial/designer/signup.pzformpy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipyzed run examples\tutorial\designer\signup.pzformpython3 -m pyzed run examples/tutorial/designer/signup.pzformpy -m pyzed run examples\tutorial\designer\signup.pzformManejadores de eventos: examples/tutorial/designer/signup.py
"""Event handlers for the sign-up form (tutorial: Data binding and validation).
pyzed run examples/tutorial/designer/signup.pzform
The controls are bound to fields of ``form.data`` (Properties → Data → Bind to) and carry theirvalidation rules, so the handler only has to call ``form.validate()`` and read ``form.data``."""
def create_click(form, event): """Runs when create is clicked.""" if not form.validate(): # checks every rule and shows the messages under the controls form.status.color = "error" form.status.text = f"{len(form.errors)} field(s) need your attention." return data = form.data # {"name": ..., "email": ..., "password": ..., "age": ..., "terms": True} print("New account:", {k: v for k, v in data.items() if k != "password"}) form.status.color = "success" form.status.text = f"Welcome aboard, {data['name']}!"Se construye paso a paso en Enlace de datos y validación.
Formulario de contacto
Sección titulada «Formulario de contacto»Nombre, correo, un desplegable de tema y un mensaje con un contador de caracteres en vivo. El botón Send revisa los campos y escribe el mensaje en la consola.

uv run --with pyzed-ui pyzed run examples/tutorial/designer/contact.pzformpython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipyzed run examples/tutorial/designer/contact.pzformpy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipyzed run examples\tutorial\designer\contact.pzformpython3 -m pyzed run examples/tutorial/designer/contact.pzformpy -m pyzed run examples\tutorial\designer\contact.pzformManejadores de eventos: examples/tutorial/designer/contact.py
"""Event handlers for the contact form (tutorial: A form with inputs and a button).
pyzed run examples/tutorial/designer/contact.pzform
Each function receives the running ``form`` and an ``event`` (``event.value`` holds the newvalue of a ``change`` event)."""
LIMIT = 500
def form_load(form, event): """Runs once when contact opens: put the cursor in the first box.""" return form.your_name.focus()
def message_change(form, event): """Runs on every keystroke in message: update the character counter.""" count = len(event.value) form.counter.text = f"{count} / {LIMIT}" form.counter.color = "error" if count > LIMIT else "text_muted"
def send_click(form, event): """Runs when send is clicked: check the fields, then 'send' the message.""" if not form.your_name.text.strip(): form.status.color = "error" form.status.text = "Please tell us your name." return form.your_name.focus() if "@" not in form.email.text: form.status.color = "error" form.status.text = "That email does not look right." return form.email.focus() if len(form.message.text) > LIMIT: form.status.color = "error" form.status.text = f"Keep the message under {LIMIT} characters." return None # print() shows up in the designer's console (and in the terminal) print(f"New {form.topic.selected.lower()} from {form.email.text}: {form.message.text!r}") form.status.color = "success" form.status.text = f"Thanks, {form.your_name.text.split()[0]}! We got your message." form.message.text = "" form.counter.text = f"0 / {LIMIT}" return NoneSe construye paso a paso en Un formulario que hace algo.
Valoraciones con componentes
Sección titulada «Valoraciones con componentes»Dos tarjetas de perfil, cada una con un componente Rating de cinco estrellas, una al lado de la otra en una
fila flexible que crece y se encoge con la ventana. Rating, ProfileCard y TeamCard son componentes dibujados
en el modo componente del diseñador (archivos .pzcomp): tienen sus propias propiedades, eventos y código, y
TeamCard hereda todo de ProfileCard.

uv run --with pyzed-ui pyzed run examples/designer/components/reviews.pzformpython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipyzed run examples/designer/components/reviews.pzformpy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipyzed run examples\designer\components\reviews.pzformpython3 -m pyzed run examples/designer/components/reviews.pzformpy -m pyzed run examples\designer\components\reviews.pzformManejadores de eventos: examples/designer/components/reviews.py
"""Event handlers for the Reviews form.
Each function receives the running ``form`` (controls are attributes: ``form.button1.text``)and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``."""
def ada_rated(form, event): """Runs when the card of ada is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars"
def grace_rated(form, event): """Runs when the card of grace is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars"Se construye paso a paso en Un componente propio.
Pantalla de login
Sección titulada «Pantalla de login»Una pantalla de inicio de sesión completa escrita en código: un panel de marca, botones de inicio de sesión único, una opción de passkey y un formulario de correo. Es uno de los Bloques, pantallas que puedes copiar en tu app y adaptar.


uv run --with pyzed-ui python examples/blocks/login.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/blocks/login.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\blocks\login.pypython3 examples/blocks/login.pypy examples\blocks\login.pyEl código y qué cambiar en él: bloque Login.
Asistente de bienvenida
Sección titulada «Asistente de bienvenida»Una configuración en varios pasos: una barra de pasos arriba, tarjetas para elegir, validación en cada paso y un resumen antes de terminar.


uv run --with pyzed-ui python examples/blocks/onboarding.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/blocks/onboarding.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\blocks\onboarding.pypython3 examples/blocks/onboarding.pypy examples\blocks\onboarding.pyEl código: bloque Onboarding.
Ajustes
Sección titulada «Ajustes»Secciones a la izquierda y filas agrupadas a la derecha: campos de perfil, un selector de idioma, tema claro, oscuro o del sistema, muestras de color de acento, un deslizador de tamaño de letra, interruptores de notificaciones y una zona de peligro que pregunta antes de borrar.


uv run --with pyzed-ui python examples/blocks/settings.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/blocks/settings.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\blocks\settings.pypython3 examples/blocks/settings.pypy examples\blocks\settings.pyEl código: bloque Settings.
Diálogos e idiomas
Sección titulada «Diálogos e idiomas»Cada botón abre uno de los diálogos listos de pyzed.ui (un aviso, una confirmación, una pregunta, una elección y
“cambios sin guardar”). Elige un idioma en la barra de herramientas y cada texto, incluidos los botones de los
diálogos, cambia mientras la app corre.

uv run --with pyzed-ui python examples/dialogs.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/dialogs.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\dialogs.pypython3 examples/dialogs.pypy examples\dialogs.pyCódigo completo: examples/dialogs.py
"""Dialogs and languages: ready-made dialogs and switching the app's language while it runs.
python examples/dialogs.py
- Pick a language in the toolbar; every text (and the dialogs' buttons) follows it.- Each button opens one of the dialogs in ``pyzed.ui``."""
import pyzed as pzfrom pyzed import i18n, ui
i18n.add("en", { "title": "Dialogs", "language": "Language", "files": {"zero": "No files", "one": "{count} file", "other": "{count} files"}, "actions": {"alert": "Show an alert", "confirm": "Delete files", "prompt": "Rename", "choice": "Export", "unsaved": "Close document"}, "delete_question": "Delete {files}? This cannot be undone.", "saved": "Your changes were saved.", "rename": "Rename file", "new_name": "New name", "export": "Export the report as…", "last": "Last action: {action}",})i18n.add("es", { "title": "Diálogos", "language": "Idioma", "files": {"zero": "Ningún archivo", "one": "{count} archivo", "other": "{count} archivos"}, "actions": {"alert": "Mostrar un aviso", "confirm": "Eliminar archivos", "prompt": "Renombrar", "choice": "Exportar", "unsaved": "Cerrar documento"}, "delete_question": "¿Eliminar {files}? No se puede deshacer.", "saved": "Tus cambios se guardaron.", "rename": "Renombrar archivo", "new_name": "Nombre nuevo", "export": "Exportar el informe como…", "last": "Última acción: {action}",})
LANGUAGES = {"en": "English", "es": "Español"}
class DialogsDemo(pz.App): size = (720, 460)
def __init__(self): self.dialog = None self.files = 3 self.name = "report.md" self.last = "-"
def title(self): return pz.t("title")
def open(self, kind): self.dialog = kind
def done(self, action): self.last = action self.dialog = None
def delete(self): self.files = 0 self.done("delete")
def dialog_view(self): close = lambda: self.done("cancel") # noqa: E731 if self.dialog == "alert": return ui.alert(pz.t("saved"), close, kind="success") if self.dialog == "confirm": return ui.confirm(pz.t("delete_question", files=pz.t("files", count=self.files)), self.delete, close, danger=True) if self.dialog == "prompt": return ui.prompt(pz.t("rename"), self.name, lambda v: setattr(self, "name", v), lambda: self.done(f"rename → {self.name}"), close, message=pz.t("new_name")) if self.dialog == "choice": return ui.choice(pz.t("export"), [(f, lambda f=f: self.done(f"export {f}")) for f in ("CSV", "PDF")], close) if self.dialog == "unsaved": return ui.unsaved_changes(self.name, lambda: self.done("save"), lambda: self.done("discard"), close) return None
def view(self): toolbar = pz.row([ ui.heading(pz.t("title")), pz.space(width="fill"), ui.label(pz.t("language"), color="text_muted"), ui.segmented([LANGUAGES[k] for k in LANGUAGES], LANGUAGES[i18n.language()[:2]], lambda name: i18n.set_language(next(k for k, v in LANGUAGES.items() if v == name))), ], align="center", spacing=10) buttons = pz.grid( [ui.button(pz.t(f"actions.{kind}"), lambda kind=kind: self.open(kind), full_width=True) for kind in ("alert", "confirm", "prompt", "choice", "unsaved")], columns=["1fr", "1fr"], spacing=8, ) body = pz.column([ toolbar, ui.label(f"{self.name} · {pz.t('files', count=self.files)}", color="text_muted"), buttons, ui.label(pz.t("last", action=self.last), mono=True, size="sm"), ], spacing=16, padding=24) return ui.dialog_layer(body, self.dialog_view(), on_dismiss=lambda: self.done("cancel"))
if __name__ == "__main__": pz.run(DialogsDemo)Para errores de campo, confirmaciones y toasts en tus propios formularios, mira Diálogos, notificaciones y validación.
Registro validado con Pydantic
Sección titulada «Registro validado con Pydantic»Las reglas de este formulario viven en un modelo de Pydantic, el mismo tipo de modelo
que usaría una API web. La ventana guarda lo que escribe el usuario como texto y se lo pasa a
SignUp.model_validate; Pydantic convierte los valores (una fecha, un correo, un sí/no), revisa cada regla, y
pz.validation_errors convierte sus errores en un mensaje por campo. Las cuentas se guardan en accounts.json.

Es un pequeño proyecto con sus propias dependencias, así que ejecútalo desde su carpeta:
cd examples/integrations/pydantic_signupuv run main.pycd examples/integrations/pydantic_signuppython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-ui "pydantic[email]>=2.6"python main.pycd examples\integrations\pydantic_signuppy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-ui "pydantic[email]>=2.6"python main.pycd examples/integrations/pydantic_signuppython main.pyCódigo completo: main.py y models.py
"""The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/)."""
from __future__ import annotations
from datetime import datefrom typing import Literal
from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator
class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False
@field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower()
@field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value
@field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value
@model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self
class Account(BaseModel): """What gets saved: the sign-up without the passwords."""
username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool
Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once"""A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json.
uv run main.py
The window keeps what the user types as plain strings and hands them to``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every ruleand reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``.Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/"""
from __future__ import annotations
from pathlib import Path
from pydantic import ValidationError
import pyzed as pzfrom pyzed import ui
from models import Account, Accounts, SignUp
FILE = Path(__file__).with_name("accounts.json")EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False}
def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else []
class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600)
def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account
def edit(self, key): return lambda value: self.draft.__setitem__(key, value)
def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {}
def form(self): e, d = self.errors.get, self.draft
def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key))
return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420)
def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel)
def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)])
if __name__ == "__main__": pz.run(SignUpForm)La explicación paso a paso está en Pydantic.