---
title: "マルチウィンドウ — zenit Zig UI ドキュメント"
description: "エディタ、プレビュー、ツールパネル向けに互いに分離されたネイティブウィンドウを作成し、window_id で振り分ける 1 つのイベントループで駆動します。"
url: https://zenit.z.express/ja/docs/advanced/multi-window
language: ja
alternate_en: https://zenit.z.express/docs/advanced/multi-window.md
alternate_zh: https://zenit.z.express/zh/docs/advanced/multi-window.md
alternate_es: https://zenit.z.express/es/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 で振り分ける 1 つのイベントループで駆動します。

## 使うべき場面

単一ウィンドウのアプリは引き続き `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` は作成とマウントを 1 つのトランザクションで行います。リポジトリの `zig build multi-window` サンプルで両方を確認できます。

> NOTE
> 
> **ウィンドウは最大 16 個。** `MultiWindowApp.max_windows` は 16 で、アロケーションなしのライフサイクル台帳のための明示的な上限です。これを超えると `createWindow*` は `error.TooManyWindows` を返します。

## 分離の保証

PER-WINDOW STACK

イベントは window\_id で選ばれたちょうど 1 つのウィンドウにだけ入ります。共有モデルはウィンドウの外にあり、アプリが各ウィンドウに明示的に通知します。

**ウィンドウごとに独立**

ネイティブ window、Cx（独自のリアクティブグラフを含む）、フォントコンテキスト、Metal surface / renderer / device queue、IME とアクセシビリティのルート。

**イベントの分離**

ポインタ、キーボード、IME、ドラッグのイベントはネイティブの window\_id でルーティングされます。メニューコマンドは、たまたまイベントを取り出したウィンドウではなく、発火時に捕捉した key window に送られます。

**独立した破棄**

ウィンドウを閉じると自身のリソースだけが破棄され、他のウィンドウは描画を続けます。最後の 1 つが閉じると run() が戻ります。

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

実際の 2 ウィンドウアプリ：観察対象の target と DevTools はそれぞれ独自の Cx とウィンドウを持ちます。harness がツールウィンドウを Console に切り替えてフィルタする間も、target ウィンドウは動き続けます。

## ライフサイクル

| API | 役割 |
| --- | --- |
| `run()` | 最後のウィンドウが閉じるか quit() が呼ばれるまでループを回す |
| `tick()` | pump + フレームを 1 回実行。ループを自分で回す場合に |
| `closeWindow(id)` | ウィンドウを 1 つ閉じる。コールバック内では安全な境界までキューに積まれる |
| `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 を読ませてはいけません。Node ポインタや Scope のリソースを Cx をまたいで共有することも避けてください。

-   **モデルはウィンドウの外に置く。**事実は素の struct かアプリレベルの store に保持し、どのウィンドウよりも長く生存させます。
    
-   **各ウィンドウは必要な部分だけをミラーする。**ウィンドウは自分の Scope に Signal を作り、モデルが変わったらアプリが各ウィンドウのミラーへ明示的に書き込みます。
    
-   **閉じるときに登録を解除する。**ウィンドウが閉じた後、モデルはそのウィンドウの Signal へのポインタを保持してはいけません。
    

> TIP
> 
> **DevTools もウィンドウです。** Elements / Components / Console / Performance の完全なパネルを 2 つ目のウィンドウに置き、別のウィンドウの Cx だけを観察できます。レイアウト境界をさっと見るだけなら、1 行の overlay のほうが軽量です。[DevTools](https://zenit.z.express/ja/docs/advanced/devtools) を参照してください。
