docs/advanced/multi-window
Fähigkeiten · Native Fenster

Apps mit mehreren Fenstern

Erstellen Sie isolierte native Fenster für Editoren, Vorschauen oder Werkzeugpanels, alle gesteuert von einer Event-Loop, die nach window_id verteilt.

6 Min. Lesezeit

Wann einsetzen

Apps mit einem Fenster verwenden weiterhin App; dessen API bleibt unverändert. Wechseln Sie nur dann zu MultiWindowApp (auch exportiert als zenit_app.Application), wenn Fenster einen eigenen Lebenszyklus, eigenes Input-Routing, eine eigene GPU-Surface oder einen eigenen Accessibility-Baum brauchen.

Fenster erstellen

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()
}

Jeder Aufruf von createWindow* liefert ein *App und nimmt dieselbe Konfiguration wie ein App mit einem Fenster (window, console, frame_pacing, …). createWindow erstellt, ohne zu mounten – Sie rufen app.mount(mountFn) selbst auf; createWindowWith erledigt beides in einer Transaktion. Das Beispiel zig build multi-window im Repository zeigt beide Varianten.

Isolationsgarantien

PER-WINDOW STACK
Ein Event gelangt in genau ein Fenster, bestimmt durch window_id; das gemeinsame Modell liegt außerhalb, und die App benachrichtigt jedes Fenster explizit.
01
Pro Fenster getrennt

Natives Fenster, Cx (mit eigenem reaktivem Graphen), Font-Kontext, Metal-Surface / Renderer / Device-Queue, IME und Accessibility-Route.

02
Isolierte Events

Zeiger-, Tastatur-, IME- und Drag-Events werden nach nativer window_id geroutet; Menübefehle gehen an das beim Auslösen erfasste Key-Window, nicht an das Fenster, das sie zufällig aus der Queue holt.

03
Unabhängiger Abbau

Das Schließen eines Fensters baut nur dessen eigene Ressourcen ab, die anderen rendern weiter; run() kehrt zurück, wenn das letzte schließt.

Eine echte App mit zwei Fenstern: Das beobachtete Target und DevTools haben jeweils eigenen Cx und eigenes Fenster. Der Harness schaltet das Werkzeugfenster auf Console und filtert, während das Target weiterläuft.

Lebenszyklus

APIFunktion
run()Treibt die Schleife, bis das letzte Fenster schließt oder quit() aufgerufen wird
tick()Eine Runde Pump + Frame, um die Schleife selbst zu steuern
closeWindow(id)Schließt ein Fenster; aus einem Callback heraus wird es bis zu einer sicheren Grenze eingereiht
quit()Beendet die ganze App; verbleibende Fenster baut deinit() geordnet ab
window(id)Findet ein lebendes Fenster per id; null, sobald es geschlossen ist
activateWindow(id)Macht ein Fenster zum aktiven
setMenuModel / bindMenuCommandDas Menümodell ist prozessweit; Befehls-Callbacks werden pro Fenster gebunden
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
}

Zustand teilen

Das fensterübergreifende Modell gehört der App; jedes Fenster hält nur seinen eigenen View-Zustand. Jeder Cx hat seinen eigenen reaktiven Graphen – lassen Sie also kein Effect eines Fensters ein Signal aus dem Scope eines anderen lesen, und teilen Sie niemals Node-Zeiger oder Scope-Ressourcen über Cx hinweg.

  • ✓

    Das Modell lebt außerhalb der Fenster. Halten Sie Fakten in einem einfachen Struct oder einem App-weiten Store, der jedes einzelne Fenster überlebt.

  • ✓

    Jedes Fenster spiegelt, was es braucht. Ein Fenster erzeugt Signals in seinem eigenen Scope; ändert sich das Modell, schreibt die App explizit in den Spiegel jedes Fensters.

  • ✓

    Beim Schließen abmelden. Nachdem ein Fenster geschlossen ist, darf das Modell keine Zeiger auf dessen Signals mehr halten.

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30