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

activated

Button

(nothing)

activated

CheckBox

(state: bool)

activated

TextEntry

(text: str)

activated

Slider, SpinBox, Dial

(value: number)

activated

ComboBox

(index: int)

activated

Dialog

(button_text: str)

page-switch

TabWidget, StackWidget

(index: int)

page-close

TabWidget, MDIWidget

(index: int)

selected

TreeView, TableView

(selected_items)

pointer-down

Image, Canvas

(event: dict)

drop-end

Image, Canvas, Label, …

(payload: dict)

expired

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:

  1. The browser sends a callback message with transfer_id and file metadata (names, sizes, MIME types) but no file data.

  2. The framework fires a drop-start callback with the metadata so you can show progress UI.

  3. The browser sends file-chunk messages with base64-encoded data.

  4. The framework fires drop-progress callbacks with transfer status.

  5. When all chunks arrive, the framework reassembles the data 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 transfer

  • file_index – which file (0-based)

  • chunk_index – which chunk of the current file

  • num_chunks – total chunks for the current file

  • transferred_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 with base64 data URIs:

import base64

def on_drop(payload):
    for f in payload["files"]:
        name = f["name"]
        size = f["size"]
        mime = f["type"]
        data_uri = f["data"]  # "data:<mime>;base64,<data>"

        # Decode the file content
        b64 = data_uri.split(",", 1)[1]
        content = base64.b64decode(b64)
        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]
    b64 = f["data"].split(",", 1)[1]
    text = base64.b64decode(b64).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)