TypePHP\Qt
指南
控件
深入
参考
FAQ
  • English
  • 简体中文
GitHub
指南
控件
深入
参考
FAQ
  • English
  • 简体中文
GitHub
  • 指南

    • 指南
    • 安装
    • Qt 的安装与编译
    • 快速上手
    • 项目结构
    • 架构原理
    • 状态与视图
  • 界面

    • 事件与处理器
    • 属性
    • 布局
    • 对话框与系统集成
    • 多窗口、托盘与定时器
    • 增量补丁

多窗口、托盘与定时器

一个 QtApp = 一个窗口

副窗口就是再 new QtApp() 并 createWindow():

$log = new QtApp();
$log->createWindow('运行日志', ['width' => 560, 'height' => 300]);
$log->render(WidgetTree::vbox([
    WidgetTree::table(['时间', '事件'], [], ['id' => 'log_tbl', 'stretch_last' => true]),
]));

qt_app_create 是幂等的 —— QApplication 全程只有一个,多次调用不会出问题。

但 run() 只泵自己那个窗口

这是最容易踩的地方:$app->run() 只处理 $app 自己的窗口事件。多窗口要自己按帧轮流泵:

while ($app->isOpen()) {
    $app->runFrames(1);                       // 主窗口:泵事件 + 分派 + 重渲染

    $log = $state['log'];
    if ($log instanceof QtApp) {
        if ($log->isOpen()) {
            $log->runFrames(1);               // 副窗口
        } else {
            $log->destroy();                  // 被关掉就摘掉,别攒僵尸窗口
            $state['log'] = null;
        }
    }
}

要点:

  • 用 runFrames(1) 而不是 run() —— 后者会阻塞到窗口关闭。
  • 窗口关闭后要 destroy() 并从状态里摘掉,否则每帧都在泵一个死窗口。
  • 主窗口关闭(isOpen() 为假)时循环结束。

完整例子:主窗口 + 日志窗口

$state = ['log' => null, 'logLines' => []];

// 主窗口里开日志窗口的按钮
$app->on('open_log_btn', 'click', function () use (&$state) {
    if ($state['log'] instanceof QtApp) {
        return;                                   // 已经开着
    }

    $log = new QtApp();
    $log->createWindow('运行日志', ['width' => 560, 'height' => 300]);
    $log->render(WidgetTree::vbox([
        WidgetTree::table(['时间', '事件'], [], ['id' => 'log_tbl', 'stretch_last' => true]),
        WidgetTree::hbox([
            WidgetTree::button('关闭日志', ['id' => 'close_log_btn']),
            WidgetTree::spacer(1),
        ]),
    ]));
    $log->on('close_log_btn', 'click', function () use (&$state) {
        if ($state['log'] instanceof QtApp) {
            $state['log']->close();               // 请求关闭,下一帧 isOpen() 变假
        }
    });

    $state['log'] = $log;
    $log->runFrames(1);                           // 先泵一帧,让控件建出来
});

先泵一帧

patch() 找的是已存在的控件。新窗口如果不先泵一帧,第一条 patch 会因为控件还没建而丢掉。

往日志窗口追加行

用 patch() 走命令式旁路(日志流是热路径,整树重建太浪费):

function log_append(QtApp $log, string $line): void
{
    $log->patch([
        ['op' => 'call', 'id' => 'log_tbl', 'method' => 'appendRows',
         'args' => [[[date('H:i:s'), $line]]]],
    ]);
}

注意:patch 追加的行只在下次 render() 之前有效 —— 一旦重渲染,表格会以控件树为准被重建。所以日志这类"只增不减"的数据,要么写回状态(在 view() 里生成完整行列表),要么就用 patch 且接受它在重渲染时被清掉。

示例应用的做法是把日志行存进状态,view() 里生成表格 —— 这样重渲染不丢:

$app->view(function () use (&$state): array {
    return WidgetTree::table(
        ['时间', '事件'],
        $state['logLines'],                       // 数据来自状态
        ['id' => 'log_tbl', 'stretch_last' => true]
    );
});

定时器

$app->setTimer('clock', 1000);      // 每 1000ms 一次
$app->setTimer('clock', 0);         // 停

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

定时器在所有 handler 注册之后注册(run() 之前),到 run() 里才开始跳。

托盘应用:两个假设要反过来

托盘应用和普通窗口应用有两个结构性差异:

① 关掉窗口不该退出进程。

// 需要在桥接里设:QApplication::setQuitOnLastWindowClosed(false)

否则窗口一隐藏,进程就死了,托盘图标也跟着消失。

② 循环条件是"还活着"而不是"窗口开着"。

托盘应用大部分时间没有可见窗口,所以 while ($app->isOpen()) 会在第一次隐藏后就退出。要用存活标志。

包的现状

包当前把托盘定位为窗口应用的附加能力(setTray + onAny('tray', …)),主循环仍是 while (isOpen())。

要写纯托盘应用(无主窗口、常驻后台),需要手写桥接并改这两处 —— 见 手写桥接 与技能里的 references/system-tray.md。

无头验收多窗口

多窗口应用同样能跑 --selftest:

// 自检末尾记得把副窗口也销毁
if ($state['log'] instanceof QtApp) {
    $state['log']->destroy();
}
$app->destroy();
在 GitHub 上编辑此页
最后更新: 2026/10/6 03:13
Prev
对话框与系统集成
Next
增量补丁