Callback System¶
pgwidgets uses a callback model where the browser sends event messages to Python over WebSocket. You register handlers in Python; they fire when the user interacts with widgets in the browser.
Registering Callbacks¶
There are two ways to register a callback on any widget:
on() – handler receives only the callback arguments:
# Sync
btn.on("activated", lambda: print("clicked"))
entry.on("activated", lambda text: print(f"Entered: {text}"))
# Async
await btn.on("activated", on_click)
add_callback() – handler receives the widget as the first argument:
# Sync
btn.add_callback("activated", lambda widget: print(f"{widget} clicked"))
# Async
await btn.add_callback("activated", on_click)
Extra Arguments¶
Both on() and add_callback() accept extra positional and keyword
arguments that are appended to every invocation:
def on_button(label, tag):
label.set_text(f"Button {tag} clicked")
btn_a.on("activated", on_button, status_label, "A")
btn_b.on("activated", on_button, status_label, "B")
Callback Signatures¶
Different callbacks pass different arguments to the handler. Below are the common patterns:
Callback |
Widgets |
Handler receives |
|---|---|---|
|
Button |
(nothing) |
|
CheckBox |
|
|
TextEntry |
|
|
Slider, SpinBox, Dial |
|
|
ComboBox |
|
|
Dialog |
|
|
TabWidget, StackWidget, MDIWidget |
|
|
TabWidget, StackWidget, MDIWidget |
|
|
All containers |
|
|
All containers |
|
|
TreeView, TableView |
|
|
Image, Canvas |
|
|
Image, Canvas, Label, … |
|
|
Timer |
(nothing) |
Async Callbacks¶
In the async API, callback handlers can be sync or async functions. Async handlers are automatically awaited:
async def on_click():
await status.set_text("Clicked!")
await btn.on("activated", on_click)
File Transfers (Chunked Protocol)¶
When a user drags and drops files onto a widget (e.g., Image or Canvas with
drop-end), the file data is transferred in chunks to avoid blocking the
WebSocket with large payloads.
The protocol works as follows:
The browser sends a
callbackmessage withtransfer_idand file metadata (names, sizes, MIME types) but no file data.The framework fires a drop-start callback with the metadata so you can show progress UI.
The browser sends
binary-chunkJSON headers, each followed by a raw binary WebSocket frame carrying the chunk’s bytes (no base64 inflation).The framework fires drop-progress callbacks with transfer status.
When all chunks arrive, the framework reassembles the bytes and fires the drop-end callback with the complete payload.
drop-start¶
Fires once at the beginning of a file transfer. The handler receives a dict with file metadata:
def on_drop_start(payload):
files = payload["files"] # list of {name, size, type}
print(f"Receiving {len(files)} files...")
widget.on("drop-start", on_drop_start)
drop-progress¶
Fires after each chunk. The handler receives a dict:
def on_progress(info):
pct = info["transferred_bytes"] / info["total_bytes"] * 100
progress_bar.set_value(pct)
if info["complete"]:
print("Transfer complete!")
widget.on("drop-progress", on_progress)
The progress dict contains:
transfer_id– unique ID for this transferfile_index– which file (0-based)chunk_index– which chunk of the current filenum_chunks– total chunks for the current filetransferred_bytes– bytes received so far (all files)total_bytes– total bytes expected (all files)complete– True when all files are fully received
drop-end¶
Fires when all file data has been received. The handler receives the full payload; each file dict carries:
name(str) — original filenamesize(int) — byte sizetype(str) — MIME type from the browser (e.g."image/png")encoding(str) — wire format ofdata. Currently always"bytes"(datais raw bytes); reserved for"base64"if a future sender chooses to deliver the file body without binary reassembly.data(bytes or None) — file contents.Noneon read error; in that case an additionalerrorfield carries the message.
def on_drop(payload):
for f in payload["files"]:
if f["encoding"] != "bytes":
continue # currently only "bytes" is emitted
name = f["name"]
size = f["size"]
mime = f["type"] # e.g. "image/png"
content = f["data"] # bytes
print(f"Received {name}: {len(content)} bytes")
widget.on("drop-end", on_drop)
Example: File Drop Zone¶
drop_label = Widgets.Label("Drop files here")
drop_label.set_color("#e8f0fe", "#4a86c8")
textarea = Widgets.TextArea("")
def on_drop_start(payload):
n = len(payload["files"])
drop_label.set_text(f"Receiving {n} file(s)...")
def on_drop(payload):
f = payload["files"][0]
text = f["data"].decode("utf-8", errors="replace")
textarea.set_text(text)
drop_label.set_text(f"Loaded: {f['name']}")
drop_label.on("drop-start", on_drop_start)
drop_label.on("drop-end", on_drop)