---
title: "多窗口 — zenit Zig UI 文档"
description: "为编辑器、预览器或工具面板创建彼此隔离的原生窗口，由一个事件循环按 window_id 分发。"
url: https://zenit.z.express/zh/docs/advanced/multi-window
language: zh-CN
alternate_en: https://zenit.z.express/docs/advanced/multi-window.md
alternate_es: https://zenit.z.express/es/docs/advanced/multi-window.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/multi-window.md
alternate_ko: https://zenit.z.express/ko/docs/advanced/multi-window.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/multi-window.md
alternate_de: https://zenit.z.express/de/docs/advanced/multi-window.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 多窗口应用

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

## 何时使用

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

## 创建窗口

`main.zig`

```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` 示例同时演示两种写法。

> NOTE
> 
> **最多 16 个窗口。** `MultiWindowApp.max_windows` 为 16，这是无分配生命周期账本的显式上限；超出时 `createWindow*` 返回 `error.TooManyWindows`。

## 隔离保证

PER-WINDOW STACK

事件按 window\_id 只进入一个窗口；共享的业务模型在窗口之外，由应用显式通知各窗口。

**每窗独立**

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

**事件隔离**

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

**独立销毁**

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

[Video](https://zenit.z.express/media/devtools-console.mp4?v=05e8c163e3)

真实的双窗口应用：被观察的 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`

```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
}
```

> WARNING
> 
> **关闭后 \*App 失效。** 窗口被销毁的那一刻，之前拿到的 `*App` 指针就悬空了。长期保存 window id，需要时用 `application.window(id)` 重新查找。

## 共享状态策略

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

-   **模型在窗口之外。**用普通结构体或应用级 Store 保存事实，生命周期长于任何一扇窗口。
    
-   **每个窗口镜像自己需要的部分。**窗口在自己的 Scope 里创建 Signal，模型变化时由应用显式写入各窗口的镜像。
    
-   **关闭时解除登记。**窗口关闭后，模型不能再持有指向该窗口 Signal 的指针。
    

> TIP
> 
> **DevTools 也是窗口。** 完整的 Elements / Components / Console / Performance 面板可以挂到第二扇窗口，只观察另一个窗口的 Cx；只看布局边界时，一行 overlay 更轻量。见 [调试与检查](https://zenit.z.express/zh/docs/advanced/devtools)。
