TypePHP\Qt
Guide
Widgets
Advanced
Reference
FAQ
  • English
  • 简体中文
GitHub
Guide
Widgets
Advanced
Reference
FAQ
  • English
  • 简体中文
GitHub
  • Guide

    • Guide
    • Installation
    • Installing and Building Qt
    • Quick Start
    • Project Structure
    • Architecture
    • State and View
  • The UI

    • Events and Handlers
    • Properties
    • Layout
    • Dialogs and System Integration
    • Multiple Windows, Tray and Timers
    • Patching

Events and Handlers

Registering

// by id + type
$app->on('greet_btn', 'click', function () use (&$state) { ... });

// for events with no id (a tray click, for example) — type only
$app->onAny('tray', function () use (&$state) { ... });

Handler signatures

A handler may declare only the parameters it needs:

$app->on('btn', 'click', function () { ... });                 // no event
$app->on('input', 'change', function (array $event) { ... });  // wants the event

The framework probes the required parameter count at registration time and calls the handler with exactly that many arguments.

This is not an optional nicety

AOT-compiled closures validate argument count exactly and throw ArgumentCountError on an extra argument; the plain PHP interpreter silently ignores extras. So a function () {} handler is perfectly fine under the interpreter and crashes the moment you click that button in the compiled binary.

The framework already handles this for you (it probes arity at registration), but keep it in mind for your own callbacks. Details in AOT Notes.

Event types

EventEmitted byvaluepayload
clickbutton, linkthe link's href—
changelineedit, textedit, spin, doublespin, slider, combothe new valueindex for combo
submitlineeditthe text—
togglecheckbox, radio, checkable button, checkable group'0' / '1'—
selectlist, table, treerow / item idindex
activatelist, table, treerow / item id—
tabtabs, stackthe indexindex
menumenu item—checked
timertimer——
traysystem traythe gesture—
press / releasebutton——
commitlineedit (focus lost or Enter)the text—
itemClicklist (every click, even on the already-selected row)item id—
celltable (cell edited, needs editable)new textrow, col
expand / collapsetreenode idexpanded
closetabs (close button, needs closable)the indexindex

An event is an associative array:

[
    'type'    => 'click',        // event type
    'id'      => 'greet_btn',    // widget id
    'value'   => '...',          // new value / row id (depends on the type)
    'payload' => ['index' => 2], // extra structure (optional)
]

Worked examples

Buttons

$app->on('save_btn', 'click', function () use ($app, $state) {
    $state->save();
});

Text input — live vs. Enter

// fires on every keystroke
$app->on('search_input', 'change', function (array $event) use ($state) {
    $state->query = (string) $event['value'];
});

// fires only on Enter
$app->on('search_input', 'submit', function (array $event) use ($state) {
    $state->query = (string) $event['value'];
    $state->search();
});

Checkboxes / radios

$app->on('dark_toggle', 'toggle', function (array $event) use ($state) {
    $state->dark = $event['value'] === '1';   // note: a string
});

Table / list selection

$app->on('task_table', 'select', function (array $event) use ($state) {
    $state->selectedId = (string) $event['value'];   // the row id
});

$app->on('task_table', 'activate', function (array $event) use ($state) {
    $state->open((string) $event['value']);          // double click
});

Combo boxes

$app->on('theme_combo', 'change', function (array $event) use ($state) {
    $state->theme = (string) $event['value'];               // the text
    $state->themeIndex = (int) $event['payload']['index'];  // the index
});

Tabs

$app->on('main_tabs', 'tab', function (array $event) use ($state) {
    $state->activeTab = (int) $event['payload']['index'];
});

Menus

$app->on('menu.about', 'menu', function () use ($app) {
    $app->alert('TypePHP\Qt example');
});

$app->on('menu.wrap', 'menu', function (array $event) use ($state) {
    $state->wrap = (bool) ($event['payload']['checked'] ?? false);
});

Timers

$app->setTimer('clock', 1000);        // every 1000ms; interval <= 0 stops it

$app->on('clock', 'timer', function () use ($state) {
    $state->ticks++;
});

Tray

A tray activation emits an event with no id, so it can only be caught with onAny. The activation kind arrives in value:

$app->onAny('tray', function (array $event) use ($state) {
    $state->lastTrayKind = (string) $event['value'];   // 'left' | 'right' | 'double' | 'middle'
});
valueGesture
leftleft click (Trigger)
rightright click (Context) — only when no tray menu is set
doubledouble click
middlemiddle click (needs a mouse that has one)

Right click with a menu attached

If setTray([... 'menu' => [...]]) is set, Qt owns the right click and pops the menu — no right event is emitted (that is Qt's own behaviour). Menu items then fire ordinary menu events:

$app->on('tray.quit', 'menu', function () use ($app) { $app->close(); });

Wildcards: onAny

$app->onAny('click', function (array $event) use ($state) {
    $state->lastClicked = $event['id'];   // every button's click lands here
});

on($id, $type, …) and onAny($type, …) may both be registered, and both will fire — the id-specific handler first, then the wildcard one. That makes onAny a reliable place for cross-cutting concerns (analytics, logging, a global shortcut) regardless of what else is registered.

What a handler may do

Only change state. Do not manipulate widgets directly — that is the declarative view's job.

A few imperative APIs are the exception (they are not in the widget tree, so they skip the diff):

$app->setTitle('New title');
$app->setStatus(['Ready', '3 items']);
$app->setMenu([...]);
$app->setTray([...]);
$app->setTimer('id', 500);
$app->resize(800, 600);

Error handling

A handler that throws does not kill the process. The framework catches it, shows an error box, records it in lastError(), and the loop continues:

$app->on('risky', 'click', function () {
    throw new RuntimeException('file not found');
});

// afterwards
$app->lastError();   // 'file not found @ /path/main.php:42'

In headless mode (headless(true)) no box is shown — it is only recorded, so --selftest can run to completion and report which case failed.

Edit this page on GitHub
Last updated: 10/6/26, 3:13 AM
Next
Properties