mcpField
E
electrobun-window-management

electrobun-window-management

github.com/rajavijayach開発1194 回インストール36 週間前に更新
リポジトリ

electrobun-window-management について

Electrobun アプリを開発する人向けのウィンドウ管理スキルです。複数ウィンドウや BrowserView の構成に加え、ライフサイクル、状態保存、ウィンドウ間通信のパターンを紹介します。

主な機能

  1. 01複数ウィンドウの管理
  2. 02BrowserView の埋め込みとタブ
  3. 03ウィンドウ状態の永続化
  4. 04親子ウィンドウのグループ化
  5. 05ウィンドウ間のメッセージとブロードキャスト
  6. 06フローティング、モーダル、位置指定ウィンドウ

活用シーン

  1. 01メイン、設定、エディターの各ウィンドウの構成
  2. 02BrowserView を使ったタブ画面の作成
  3. 03保存済みのウィンドウサイズと位置の復元
  4. 04複数ウィンドウ間のメッセージ調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rajavijayach/electrobun-skills --skill electrobun-window-management

インストール後、Claude Code で/electrobun-window-managementを入力して読み込まれたか確認します。

詳細

Electrobun のウィンドウ管理

Electrobun アプリケーションでウィンドウとビューを管理するための高度なパターンです。

マルチウィンドウアプリケーション

基本的なウィンドウマネージャー
import { BrowserWindow } from "electrobun/bun";

class WindowManager {
  private windows = new Map<string, BrowserWindow>();
  
  createWindow(id: string, options: any) {
    const win = new BrowserWindow(options);
    this.windows.set(id, win);
    
    win.on("close", () => {
      this.windows.delete(id);
    });
    
    return win;
  }
  
  getWindow(id: string) {
    return this.windows.get(id);
  }
  
  getAllWindows() {
    return Array.from(this.windows.values());
  }
  
  closeWindow(id: string) {
    const win = this.windows.get(id);
    if (win) {
      win.close();
    }
  }
  
  closeAllWindows() {
    this.windows.forEach(win => win.close());
    this.windows.clear();
  }
}

const windowManager = new WindowManager();
ウィンドウファクトリーパターン
type WindowType = "main" | "settings" | "editor" | "preview";

interface WindowConfig {
  type: WindowType;
  url: string;
  width: number;
  height: number;
  frame?: boolean;
  parent?: BrowserWindow;
}

class WindowFactory {
  private configs: Record<WindowType, Partial<WindowConfig>> = {
    main: {
      url: "views://main/index.html",
      width: 1200,
      height: 800,
      frame: true,
    },
    settings: {
      url: "views://settings/index.html",
      width: 600,
      height: 500,
      frame: true,
    },
    editor: {
      url: "views://editor/index.html",
      width: 1000,
      height: 700,
    },
    preview: {
      url: "views://preview/index.html",
      width: 800,
      height: 600,
      frame: false,
    }
  };
  
  create(type: WindowType, overrides?: Partial<WindowConfig>) {
    const config = { ...this.configs[type], ...overrides };
    return new BrowserWindow(config);
  }
}

const factory = new WindowFactory();
const mainWindow = factory.create("main");
const settingsWindow = factory.create("settings", { width: 700 });

BrowserView

BrowserView を使用すると、別のウィンドウを作成せずに、ウィンドウ内に WebView を埋め込めます。

基本的な BrowserView
import { BrowserWindow, BrowserView } from "electrobun/bun";

const win = new BrowserWindow({
  title: "Browser App",
  width: 1200,
  height: 800,
});

const view = new BrowserView({
  bounds: { x: 0, y: 60, width: 1200, height: 740 }
});

win.addBrowserView(view);
view.loadURL("https://example.com");

// Update bounds on window resize
win.on("resize", ({ width, height }) => {
  view.setBounds({ x: 0, y: 60, width, height: height - 60 });
});
BrowserView を使ったタブシステム
class TabManager {
  private tabs: Map<string, BrowserView> = new Map();
  private activeTab: string | null = null;
  private window: BrowserWindow;
  
  constructor(window: BrowserWindow) {
    this.window = window;
  }
  
  createTab(id: string, url: string) {
    const view = new BrowserView({
      bounds: this.getViewBounds()
    });
    
    view.loadURL(url);
    this.tabs.set(id, view);
    
    if (!this.activeTab) {
      this.activateTab(id);
    }
    
    return view;
  }
  
  activateTab(id: string) {
    const view = this.tabs.get(id);
    if (!view) return;
    
    // Hide current tab
    if (this.activeTab) {
      const currentView = this.tabs.get(this.activeTab);
      if (currentView) {
        this.window.removeBrowserView(currentView);
      }
    }
    
    // Show new tab
    this.window.addBrowserView(view);
    this.activeTab = id;
  }
  
  closeTab(id: string) {
    const view = this.tabs.get(id);
    if (!view) return;
    
    if (this.activeTab === id) {
      this.window.removeBrowserView(view);
      this.activeTab = null;
      
      // Activate another tab
      const otherTab = Array.from(this.tabs.keys()).find(k => k !== id);
      if (otherTab) {
        this.activateTab(otherTab);
      }
    }
    
    this.tabs.delete(id);
  }
  
  private getViewBounds() {
    const { width, height } = this.window.getBounds();
    return { x: 0, y: 60, width, height: height - 60 };
  }
  
  updateBounds() {
    const bounds = this.getViewBounds();
    this.tabs.forEach(view => {
      view.setBounds(bounds);
    });
  }
}

// Usage
const tabManager = new TabManager(mainWindow);

tabManager.createTab("tab1", "https://example.com");
tabManager.createTab("tab2", "https://github.com");
tabManager.activateTab("tab2");

mainWindow.on("resize", () => {
  tabManager.updateBounds();
});

ウィンドウのライフサイクル管理

ウィンドウ状態の永続化
import { paths } from "electrobun/bun";
import { join } from "path";

interface WindowState {
  x: number;
  y: number;
  width: number;
  height: number;
  maximized: boolean;
}

class WindowStateManager {
  private stateFile: string;
  
  constructor(windowId: string) {
    this.stateFile = join(paths.userData, `window-state-${windowId}.json`);
  }
  
  async save(state: WindowState) {
    await Bun.write(this.stateFile, JSON.stringify(state, null, 2));
  }
  
  async load(): Promise<WindowState | null> {
    try {
      const content = await Bun.file(this.stateFile).text();
      return JSON.parse(content);
    } catch {
      return null;
    }
  }
  
  async restore(window: BrowserWindow) {
    const state = await this.load();
    if (!state) return false;
    
    window.move({ x: state.x, y: state.y });
    window.resize({ width: state.width, height: state.height });
    
    if (state.maximized) {
      window.maximize();
    }
    
    return true;
  }
  
  startTracking(window: BrowserWindow) {
    let saveTimeout: Timer;
    
    const saveState = () => {
      clearTimeout(saveTimeout);
      saveTimeout = setTimeout(async () => {
        const bounds = window.getBounds();
        await this.save({
          x: bounds.x,
          y: bounds.y,
          width: bounds.width,
          height: bounds.height,
          maximized: window.isMaximized(),
        });
      }, 500);
    };
    
    window.on("move", saveState);
    window.on("resize", saveState);
  }
}

// Usage
const stateManager = new WindowStateManager("main");
const win = new BrowserWindow({ width: 1200, height: 800 });

await stateManager.restore(win);
stateManager.startTracking(win);
ウィンドウグループと親子関係
class WindowGroup {
  private parent: BrowserWindow;
  private children: BrowserWindow[] = [];
  
  constructor(parent: BrowserWindow) {
    this.parent = parent;
    
    // Close children when parent closes
    parent.on("close", () => {
      this.closeAllChildren();
    });
  }
  
  addChild(options: any) {
    const child = new BrowserWindow({
      ...options,
      parent: this.parent,
    });
    
    this.children.push(child);
    
    child.on("close", () => {
      const index = this.children.indexOf(child);
      if (index > -1) {
        this.children.splice(index, 1);
      }
    });
    
    return child;
  }
  
  closeAllChildren() {
    this.children.forEach(child => child.close());
    this.children = [];
  }
  
  showAll() {
    this.parent.show();
    this.children.forEach(child => child.show());
  }
  
  hideAll() {
    this.parent.hide();
    this.children.forEach(child => child.hide());
  }
}

// Usage
const mainWindow = new BrowserWindow({ width: 1200, height: 800 });
const group = new WindowGroup(mainWindow);

const palette = group.addChild({
  url: "views://palette/index.html",
  width: 250,
  height: 600,
  frame: false,
});

const inspector = group.addChild({
  url: "views://inspector/index.html",
  width: 300,
  height: 400,
});

ウィンドウのオーケストレーション

すべてのウィンドウへのブロードキャスト
class WindowBroadcaster {
  private windows: Set<BrowserWindow> = new Set();
  
  register(window: BrowserWindow) {
    this.windows.add(window);
    
    window.on("close", () => {
      this.windows.delete(window);
    });
  }
  
  async broadcast(method: string, ...args: any[]) {
    const promises = Array.from(this.windows).map(win =>
      win.rpc[method](...args).catch(err => {
        console.error(`Broadcast to window failed:`, err);
      })
    );
    
    await Promise.allSettled(promises);
  }
}

const broadcaster = new WindowBroadcaster();

// Register windows
broadcaster.register(mainWindow);
broadcaster.register(settingsWindow);

// Broadcast to all
await broadcaster.broadcast("updateTheme", { theme: "dark" });
ウィンドウ間通信ハブ
class WindowHub {
  private windows = new Map<string, BrowserWindow>();
  
  register(id: string, window: BrowserWindow) {
    this.windows.set(id, window);
    
    // Setup RPC handler for messaging
    window.defineRpc({
      handlers: {
        sendToWindow: async (targetId: string, method: string, ...args: any[]) => {
          return this.send(targetId, method, ...args);
        }
      }
    });
  }
  
  async send(targetId: string, method: string, ...args: any[]) {
    const target = this.windows.get(targetId);
    if (!target) {
      throw new Error(`Window ${targetId} not found`);
    }
    
    return await target.rpc[method](...args);
  }
  
  async broadcast(method: string, ...args: any[]) {
    const results = new Map();
    
    for (const [id, window] of this.windows) {
      try {
        const result = await window.rpc[method](...args);
        results.set(id, result);
      } catch (err) {
        results.set(id, { error: err.message });
      }
    }
    
    return results;
  }
}

const hub = new WindowHub();
hub.register("main", mainWindow);
hub.register("editor", editorWindow);

// Send from main to editor
await hub.send("editor", "updateContent", { text: "Hello" });

// Broadcast to all
await hub.broadcast("refreshData");

フローティングウィンドウとオーバーレイ

常に最前面に表示するウィンドウ
const floatingWindow = new BrowserWindow({
  url: "views://floating/index.html",
  width: 300,
  height: 200,
  frame: false,
  alwaysOnTop: true,
  skipTaskbar: true,
});

// Toggle always-on-top
floatingWindow.setAlwaysOnTop(!floatingWindow.isAlwaysOnTop());
ピクチャーインピクチャーモード
class PictureInPicture {
  private pipWindow: BrowserWindow | null = null;
  private sourceWindow: BrowserWindow;
  
  constructor(sourceWindow: BrowserWindow) {
    this.sourceWindow = sourceWindow;
  }
  
  enter(url: string) {
    if (this.pipWindow) return;
    
    this.pipWindow = new BrowserWindow({
      url,
      width: 400,
      height: 300,
      frame: false,
      alwaysOnTop: true,
      resizable: true,
    });
    
    this.pipWindow.on("close", () => {
      this.pipWindow = null;
    });
  }
  
  exit() {
    if (this.pipWindow) {
      this.pipWindow.close();
      this.pipWindow = null;
    }
  }
  
  toggle(url: string) {
    if (this.pipWindow) {
      this.exit();
    } else {
      this.enter(url);
    }
  }
}

const pip = new PictureInPicture(mainWindow);
pip.enter("views://video-player/index.html");

高度なパターン

モーダルダイアログ
function showModal(parent: BrowserWindow, options: any) {
  const modal = new BrowserWindow({
    ...options,
    parent,
    modal: true,
    frame: false,
  });
  
  // Disable parent interaction
  parent.setEnabled(false);
  
  modal.on("close", () => {
    parent.setEnabled(true);
    parent.focus();
  });
  
  return modal;
}

// Usage
const confirmDialog = showModal(mainWindow, {
  url: "views://confirm/index.html",
  width: 400,
  height: 200,
});
ウィンドウの配置
function centerWindow(window: BrowserWindow) {
  const { screen } = require("electrobun/bun");
  const display = screen.getPrimaryDisplay();
  const { width: screenWidth, height: screenHeight } = display.workArea;
  const { width, height } = window.getBounds();
  
  const x = Math.floor((screenWidth - width) / 2);
  const y = Math.floor((screenHeight - height) / 2);
  
  window.move({ x, y });
}

function cascadeWindows(windows: BrowserWindow[]) {
  const offset = 30;
  windows.forEach((win, index) => {
    win.move({ 
      x: 100 + (index * offset), 
      y: 100 + (index * offset) 
    });
  });
}

リソース

Electrobun の詳細については、以下をご覧ください。

  • コアスキル:electrobun - 基本的なウィンドウ作成と API
  • RPC パターン:electrobun-rpc-patterns - ウィンドウ間の RPC
  • ネイティブ UI:electrobun-native-ui - メニューとトレイの連携

コメント