docs/advanced/multi-window
应用能力 · Native windows

多窗口应用

为编辑器、预览器或工具面板创建彼此隔离的原生窗口,由一个事件循环按 window_id 分发。

预计阅读 6 分钟

何时使用

单窗口应用继续使用 App,它的 API 不受影响。只有当窗口必须拥有独立的生命周期、输入路由、GPU surface 或辅助功能树时,才升级为 MultiWindowApp(也导出为 zenit_app.Application)。

创建窗口

main.zig
const std = @import("std");
const ui = @import("ui");
const zenit_app = @import("zenit_app");

fn mountEditor(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

fn mountPreview(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var application = zenit_app.MultiWindowApp.init(gpa.allocator(), .{});
    defer application.deinit();

    // createWindowWith = create + mount in one transaction: if mount fails,
    // every native / GPU resource of that window is torn down again.
    const editor = try application.createWindowWith(.{
        .window = .{ .width = 900, .height = 700, .title = "Editor" },
    }, mountEditor);

    _ = try application.createWindowWith(.{
        .window = .{ .width = 480, .height = 700, .title = "Preview" },
    }, mountPreview);

    _ = application.activateWindow(editor.windowId());
    try application.run(); // returns after the last window closes or quit()
}

每次 createWindow* 返回一个 *App,配置项与单窗口 App 相同(window、console、frame_pacing 等)。createWindow 只创建不挂载,需要自己调用 app.mount(mountFn);createWindowWith 把创建与挂载合成一个事务。仓库的 zig build multi-window 示例同时演示两种写法。

隔离保证

PER-WINDOW STACK
事件按 window_id 只进入一个窗口;共享的业务模型在窗口之外,由应用显式通知各窗口。
01
每窗独立

原生 window、Cx(含自己的响应式图)、字体上下文、Metal surface / renderer / device queue、IME 与辅助功能路由。

02
事件隔离

指针、键盘、IME 与拖拽按原生 window_id 路由;菜单命令按触发时捕获的 key window 路由,而不是按哪个窗口恰好取到了事件。

03
独立销毁

关闭一扇窗口只销毁它自己的资源,其他窗口继续渲染;最后一扇关闭后 run() 返回。

真实的双窗口应用:被观察的 target 与 DevTools 各有独立的 Cx 和窗口。Harness 在工具窗里切到 Console 并过滤,target 窗口持续存活。

生命周期

API作用
run()驱动循环,直到最后一扇窗口关闭或调用 quit()
tick()执行一轮 pump + 按需渲染,适合自己控制循环
closeWindow(id)关闭一扇窗口;在回调里调用时排队到安全边界再提交
quit()整个应用退出;剩余窗口由 deinit() 统一有序销毁
window(id)按 id 查找存活窗口,已关闭时返回 null
activateWindow(id)把窗口设为活动窗口
setMenuModel / bindMenuCommand菜单模型是进程全局的;命令回调按窗口绑定
close.zig
const preview_id = preview.windowId(); // preview: *App from createWindowWith

// Safe from inside an input / menu / render callback: the close is queued
// and committed at the loop's iteration boundary.
_ = application.closeWindow(preview_id);

// Look windows up by id instead of holding *App across a close.
if (application.window(preview_id)) |app| {
    _ = app; // still alive
}

共享状态策略

跨窗口的业务模型由应用持有,每个窗口只保存自己的视图状态。每个 Cx 有独立的响应式图,因此不要让一个窗口的 Effect 读取另一个窗口 Scope 里的 Signal,也不要跨 Cx 共享 Node 指针或 Scope 资源。

  • ✓

    模型在窗口之外。用普通结构体或应用级 Store 保存事实,生命周期长于任何一扇窗口。

  • ✓

    每个窗口镜像自己需要的部分。窗口在自己的 Scope 里创建 Signal,模型变化时由应用显式写入各窗口的镜像。

  • ✓

    关闭时解除登记。窗口关闭后,模型不能再持有指向该窗口 Signal 的指针。

zenit · 双授权开源项目可按 GPL-3.0-only 免费使用;闭源或商业产品需要商业授权。可联系作者:zongyi.xzy#gmail.com(# 换成 @)zenit 5f9add5+wip 2026-09-30