Ir al contenido

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 .pzform más un .py con 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étodo view().

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.

Un formulario de acceso en ejecución: correo, contraseña, recordarme y un botón Sign in
  • Los controles son atributos de form: form.email.text, form.status.color = "error", form.progress.show().
  • Un control Temporizador (ticker) llama a ticker_tick cada pocos milisegundos mientras corre; así se llena la barra de progreso sin bloquear la ventana.
Windows, macOS y Linux
uv run --with pyzed-ui pyzed run examples/designer/login.pzform
Manejadores de eventos: examples/designer/login.py
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()

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.

Un formulario de registro con mensajes de error junto a dos campos
Windows, macOS y Linux
uv run --with pyzed-ui pyzed run examples/tutorial/designer/signup.pzform
Manejadores de eventos: examples/tutorial/designer/signup.py
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 their
validation 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.

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.

Un formulario de contacto con un desplegable de tema y un contador de caracteres
Windows, macOS y Linux
uv run --with pyzed-ui pyzed run examples/tutorial/designer/contact.pzform
Manejadores de eventos: examples/tutorial/designer/contact.py
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 new
value 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 None

Se construye paso a paso en Un formulario que hace algo.

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.

Dos tarjetas de perfil con valoraciones de estrellas
Windows, macOS y Linux
uv run --with pyzed-ui pyzed run examples/designer/components/reviews.pzform
Manejadores de eventos: examples/designer/components/reviews.py
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.

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.

Una pantalla de login dividida con un panel de marca y un formularioUna pantalla de login dividida con un panel de marca y un formulario
Windows, macOS y Linux
uv run --with pyzed-ui python examples/blocks/login.py

El código y qué cambiar en él: bloque Login.

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.

Un asistente con barra de pasos y tarjetas para elegirUn asistente con barra de pasos y tarjetas para elegir
Windows, macOS y Linux
uv run --with pyzed-ui python examples/blocks/onboarding.py

El código: bloque Onboarding.

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.

Una página de ajustes con secciones, interruptores y opciones de temaUna página de ajustes con secciones, interruptores y opciones de tema
Windows, macOS y Linux
uv run --with pyzed-ui python examples/blocks/settings.py

El código: bloque Settings.

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.

El diálogo de confirmación: Are you sure? Delete 3 files? con Cancel y Delete
Windows, macOS y Linux
uv run --with pyzed-ui python examples/dialogs.py
Código completo: examples/dialogs.py
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 pz
from 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.

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.

El formulario de registro con un mensaje bajo cada campo no válido, y la cuenta guardada como JSON

Es un pequeño proyecto con sus propias dependencias, así que ejecútalo desde su carpeta:

Windows, macOS y Linux
cd examples/integrations/pydantic_signup
uv run main.py
Código completo: main.py y models.py
examples/integrations/pydantic_signup/models.py
"""The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/)."""
from __future__ import annotations
from datetime import date
from 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
examples/integrations/pydantic_signup/main.py
"""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 rule
and 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 pz
from 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.