1. Immediate Mode GUIの仕組みとeguiの特徴
GUIにおける2つの設計方式
パソコンやスマートフォンの画面にボタンや入力欄を表示するプログラムを「GUI(グラフィカル・ユーザー・インターフェース)」と呼びます。GUI ライブラリには、画面の部品をどのように管理して描画するかによって「Retained Mode(保持モード)」と「Immediate Mode(即時モード)」という 2 つの設計方式があります。
▸ Retained Mode(保持モード)の仕組み
HTML/DOM(Web ページ)、Windows Forms、Qt、GTK など、従来の多くの GUI ツールキットは Retained Mode を採用しています。この方式では、プログラムの起動時に「ボタンオブジェクト」や「ラベルオブジェクト」を作成し、GUI ライブラリ側に預けて保持(Retain)してもらいます。
画面の数字を 1 つ増やしたいときは、プログラム内の変数を増やすだけでなく、あらかじめ作っておいたラベルオブジェクトを探し出して label.set_text("1") のように書き換える命令を送る必要があります。また、ボタンが押されたときの処理は「コールバック関数(イベントハンドラ)」としてボタンに登録しておきます。
▸ Immediate Mode(即時モード)の仕組み
egui が採用している Immediate Mode GUI(IMGUI)では、GUI ライブラリ側にボタンやラベルのオブジェクトを保持させません。その代わり、1 秒間に 60 回(または画面の更新が必要なタイミングごと)実行される描画ループの中で、毎回ゼロから「いま画面にあるべき部品」をコードで記述します。
flowchart TD
subgraph Retained["Retained Mode(保持モード)"]
R1["1. 初期化時にボタンやラベルのオブジェクトを生成"] --> R2["2. コールバック関数を部品に登録"]
R2 --> R3["3. イベント発生時にコールバックが呼ばれる"]
R3 --> R4["4. データの更新と画面部品の書き換えを手動で同期"]
end
subgraph Immediate["Immediate Mode(即時モード:egui)"]
I1["1. アプリ自身のデータ(構造体)だけを持つ"] --> I2["2. 毎フレームの描画関数(update)が呼ばれる"]
I2 --> I3["3. 現在のデータを見ながらUI部品を描画し、その場で入力判定"]
I3 --> I1
end
たとえば「ボタンを描画し、もし今のフレームで押されていたらカウンターを 1 増やす」という処理を、1 つの if 文としてその場に直接書きます。次のフレームでは増えた後のカウンターの値がそのまま画面に描かれるため、データと画面表示のズレが原理的に発生しません。
なぜRustではImmediate Modeが選ばれるのか
Rust でプログラムを書くとき、最も特徴的なルールが「所有権(Ownership)」と「借用(Borrowing)」です。Rust のコンパイラは、同じデータに対して複数の場所から同時に書き換えが行われないように厳しくチェックします。
Retained Mode の GUI を Rust で作ろうとすると、この所有権のルールが大きな壁になります。画面のツリー構造を管理する GUI ライブラリ側と、クリックされたときにデータを書き換えるコールバック関数側の両方が、同じデータの参照を長期間持ち続けようとするためです。結果として、Rc<RefCell<T>> や Arc<Mutex<T>> といった複雑な型を大量に組み合わせなければならず、コードが読みにくくなります。
一方、Immediate Mode である egui では、コールバック関数を登録する必要がありません。毎フレーム呼び出される update メソッドの中で、アプリ自身の構造体(&mut self)を上から下へ順番に読み書きするだけです。借用はその場限りで即座に終わるため、Rust の所有権ルールとぶつかることなく、普通の Rust コードと同じ感覚ですっきりと画面を作ることができます。
#![allow(unused)]
fn main() {
// Immediate Mode (egui) での直感的な記述イメージ
if ui.button("カウントアップ").clicked() {
self.counter += 1;
}
ui.label(format!("現在の値: {}", self.counter));
}
上のわずか 4 行のコードだけで、ボタンの配置、クリックの判定、データの更新、そして最新データの表示までが完結します。
eguiとeframeの役割分担
Rust で egui を使ったアプリを作るときは、egui と eframe という 2 つのクレート(ライブラリ)が連携して動きます。それぞれの役割を知っておくと、公式ドキュメントを読むときに迷いません。
| クレート名 | 役割と担当範囲 |
|---|---|
egui | GUI のコアライブラリ。ボタンや文字の配置計算、マウス位置との当たり判定、2D 図形の描画データ(三角形の頂点リスト)の生成だけを担当します。OS のウィンドウ管理や GPU の直接操作は行いません。 |
eframe | egui を動かすための公式アプリケーションフレームワーク。OS のウィンドウ作成(winit)や Web ブラウザの Canvas 取得、そして GPU を使った画面への描画(glow や wgpu)をまとめて引き受けます。 |
flowchart LR
App["あなたのRustコード<br/>(eframe::App を実装)"] --> Eframe["eframe<br/>(ウィンドウ管理・描画統合)"]
Eframe --> Egui["egui<br/>(レイアウト計算・UI部品・入力判定)"]
Eframe --> Platform["OSウィンドウ(Windows / macOS / Linux)<br/>または Webブラウザ(WebAssembly)"]
自分で一からゲームエンジン(Bevy など)に組み込むような特殊なケースを除き、一般的なデスクトップアプリや Web アプリを作る場合は、Cargo.toml に eframe を追加するだけで egui の機能もすべて利用できます。eframe の中で egui が再エクスポート(eframe::egui)されているためです。
Immediate Modeのメリットと注意点
egui を採用することで得られるメリットと、開発時に知っておくべき注意点を整理します。
▸ メリット1:状態同期のバグが起きない
画面の表示は常に「アプリ構造体の現在のフィールド値」から直接作られます。「データは変わったのに画面の表示が古いまま」「画面のチェックを外したのに内部フラグが残っている」といった、GUI 開発で最も多い同期バグが起きません。
▸ メリット2:UIの動的な出し分けが簡単
「チェックボックスが ON のときだけ詳細設定欄を表示する」といった処理は、特別な表示・非表示メソッドを呼ぶ必要がありません。通常の if 文で囲むだけで、条件を満たすフレームだけその部品が描画されます。リストの表示も for ループを回すだけです。
▸ メリット3:ネイティブとWebの両方で動く
egui は OS 固有の部品(Windows の標準ボタンなど)を使わず、すべての部品を自前の 2D グラフィックスとして描画します。そのため、Windows、macOS、Linux、そして Web ブラウザ(WebAssembly)のどの環境で動かしても、1 ピクセルの狂いもなく同じ見た目と動作を再現できます。
▸ 注意点1:updateメソッド内で重い処理をしない
update メソッドは画面を描き直すたびに呼ばれます。この中で数秒かかるファイル読み込みやネットワーク通信を直接行うと、その間は次のフレームが描かれず、ウィンドウが固まってしまいます。重い処理は第 6 章で学ぶように別スレッドへ逃がす必要があります。
▸ 注意点2:前フレームのレイアウト結果を使う処理
Immediate Mode は上から順番に部品を置いていくため、「まだ描画していない下側の部品のサイズに合わせて、上側の部品を中央揃えにする」といった処理が苦手です。ただし、egui にはパネル分割やグリッド、2 パスレイアウト(前のフレームのサイズを記憶して調整する仕組み)が用意されており、実用上のレイアウトで困ることはほとんどありません。
次章では、実際に Cargo プロジェクトを作成し、最初の eframe アプリケーションを動かしながら日本語表示の設定までを行います。