Rust GUI egui 入門: 簡単デスクトップアプリ開発

はい、承知いたしました。RustのGUIライブラリ「egui」を使った簡単なデスクトップアプリケーション開発に関する詳細な入門記事を作成します。約5000語を目指して記述します。


Rust GUI egui 入門: 安全、高速、そして驚くほど簡単にデスクトップアプリを開発しよう

はじめに

Rustは、システムプログラミング、WebAssembly、コマンドラインツールなど、さまざまな分野で急速に普及しているプログラミング言語です。その最大の魅力は、コンパイル時の厳密なチェックによるメモリ安全性と、CやC++に匹敵する実行速度です。しかし、Rustでグラフィカルユーザーインターフェース(GUI)を持つデスクトップアプリケーションを開発するとなると、「難しそう」「情報が少ない」と感じる方もいるかもしれません。

確かに、Java、C#、Pythonといった言語に比べると、RustのGUIライブラリのエコシステムはまだ発展途上かもしれません。ですが、近年注目を集めているライブラリの一つに egui (easy GUI) があります。eguiは、その名前の通り「簡単に使えるGUI」を目指しており、特にツール開発やゲーム内のデバッグUIなどで人気がありますが、汎用的なデスクトップアプリケーション開発にも十分に活用できます。

本記事では、Rustの優れた特性を活かしつつ、eguiのシンプルで直感的なAPIを使って、簡単なデスクトップアプリケーションを開発する手順を詳細に解説します。

なぜRustでGUI開発をするのか?

RustでGUIアプリケーションを開発することには、いくつかの強力なメリットがあります。

  1. 安全性: Rustの最も特徴的な機能である所有権システムは、実行時エラーの一般的な原因であるデータ競合やヌルポインタ参照といった問題をコンパイル時に防ぎます。複雑なGUIアプリケーションでは、さまざまなデータが共有され、状態が頻繁に変化するため、安全性の保証は非常に重要です。
  2. パフォーマンス: Rustはゼロコスト抽象化を追求しており、ガベージコレクションなしでメモリを管理します。これにより、GUIの描画やデータ処理といったパフォーマンスが要求されるタスクにおいて、高速かつ効率的なアプリケーションを作成できます。
  3. 信頼性: 強力な型システムと明示的なエラーハンドリングにより、実行時パニックや未定義動作を最小限に抑えることができます。これは、ユーザーが安心して使える堅牢なアプリケーションを開発する上で不可欠です。
  4. クロスプラットフォーム: egui自体は特定のプラットフォームのGUIバックエンドに依存しません。eframeのようなラッパーライブラリを使うことで、Windows、macOS、Linuxといった主要なデスクトップ環境だけでなく、Web (WebAssembly) でも同じコードベースからアプリケーションをビルドできます。

これらのRustの強みと、eguiのシンプルさ、そして即時モードGUIというユニークなアプローチが組み合わさることで、RustでのGUI開発は非常に魅力的な選択肢となっています。

eguiとは何か?即時モードGUI (Immediate Mode GUI)

eguiは、スウェーデンのプログラマーであるEmil Ernerfeldt氏を中心に開発されている、Rust製の即時モードGUIライブラリです。

即時モードGUI (IMGUI) は、従来のGUIツールキット(Retained Mode GUI)とは異なるパラダイムで動作します。

  • Retained Mode GUI (例: Qt, WPF, JavaFX): UI要素(ボタン、テキストボックスなど)は一度オブジェクトとして作成され、内部の状態(テキスト内容、押されているかなど)や階層構造を持ち、イベントループの中でシステムによって管理されます。開発者はこれらのオブジェクトの状態を変更したり、イベントハンドラを登録したりすることでUIを操作します。システムは自身の状態に基づいて画面を「再描画」します。
  • Immediate Mode GUI (例: egui, Dear ImGui): UIの状態はアプリケーション側のデータ構造(例えばRustのstructのフィールド)として保持されます。画面の描画サイクルごとに、開発者はアプリケーションの現在の状態に基づいて、そのフレームで表示したいUI要素をコードで「再構築」します。ウィジェットを描画する関数を呼び出すと、そのウィジェットはただちに描画され、同時にユーザー入力(クリック、キー入力など)がチェックされ、その結果が関数の戻り値として返されます。開発者はこの戻り値を見て、アプリケーションの状態を更新します。

IMGUIの主な利点は以下の通りです。

  • コードのシンプルさ: UIの構造がアプリケーションの状態と密接に結びついており、毎フレーム状態に基づいてUIを描画するコードを書くだけなので、コードが直感的になりやすいです。UIの階層構造を管理するオブジェクトツリーなどを意識する必要がありません。
  • 状態管理の容易さ: アプリケーションの状態が中心となり、UIはそれを反映するビューとして機能します。状態変更に対するUIの更新は、単に次のフレームで新しい状態に基づいてUIを再描画するだけで済む場合が多いです。
  • デバッグのしやすさ: アプリケーションの状態を見れば、UIがどのように表示されるべきかが明確です。状態と表示が一致しない場合は、描画コードに問題があることがすぐにわかります。
  • 動的なUI: アプリケーションの状態が大きく変化するような、非常に動的なUIを構築するのに適しています。

反面、IMGUIは毎フレームUI全体(またはその大部分)を再構築・再描画するため、非常に複雑で巨大なUIや、高度なカスタム描画が必要な場合には、Retained Mode GUIの方がパフォーマンスや開発効率の面で有利になることもあります。しかし、多くの一般的なデスクトップアプリケーション、特にツール系のアプリケーションにとっては、eguiのIMGUIアプローチは十分実用的であり、開発の迅速さという大きなメリットを提供します。

この記事で学ぶこと

本記事を通して、あなたは以下のことを習得できます。

  • Rustプロジェクトにeguiとeframeを追加し、開発環境を整える方法。
  • eguiの基本的な概念である「即時モードGUI」「Context」「Ui」「描画ループ」を理解し、それらがどのように連携してGUIを構築するか。
  • eguiアプリケーションをデスクトップウィンドウとして実行するための eframe::App トレイトの実装方法。
  • egui::Label, egui::Button, egui::TextEdit, egui::Slider, egui::Checkbox など、基本的なウィジェットの基本的な使い方と、それらを組み合わせてUIを作成する方法。
  • ui.vertical, ui.horizontal といったレイアウトコンテナを使って、ウィジェットを配置し、UIを整える方法。
  • 即時モードGUIにおけるアプリケーションの状態管理と、ウィジェットの戻り値 (egui::Response) を使ってユーザーインタラクション(クリック、入力など)を処理する方法。
  • 学んだ知識を統合し、具体的な「シンプルなTODOリスト」アプリケーションを作成する実践的な例。

開発環境の準備

eguiアプリケーションを開発するには、まずRustの開発環境が必要です。まだインストールしていない場合は、Rustの公式インストールツールである rustup を使用するのが最も簡単です。

ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行します。

Linux/macOS:
bash
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Windows:
Rust公式ウェブサイト (https://www.rust-lang.org/tools/install) から rustup-init.exe をダウンロードして実行します。

インストール中に、デフォルトの設定(Stable版、一般的なコンポーネント)で進めるか問われますので、特にこだわりがなければデフォルトで問題ありません。インストールが完了したら、画面の指示に従って環境変数を反映させます(多くの場合、新しいターミナルを開けば自動的に反映されます)。

インストールが成功したか確認するために、以下のコマンドを実行します。

bash
rustc --version
cargo --version

Rustコンパイラ(rustc)とビルドツール/パッケージマネージャー(cargo)のバージョン情報が表示されれば成功です。

次に、新しいRustプロジェクトを作成します。今回はデスクトップアプリケーションなので、バイナリプロジェクトとして作成します。

bash
cargo new my_first_egui_app
cd my_first_egui_app

これにより、my_first_egui_appという名前の新しいディレクトリが作成され、中にCargo.toml(プロジェクト設定ファイル)とsrc/main.rs(メインソースファイル)が生成されます。

Cargo.toml の設定

eguiと、それをデスクトップアプリケーションとして実行するためのeframeクレートを依存関係に追加します。プロジェクトディレクトリにあるCargo.tomlファイルを開き、[dependencies]セクションに以下の行を追加してください。

“`toml
[dependencies]

egui: 即時モードGUIライブラリ本体

egui = “0.27.0”

eframe: eguiを様々なバックエンド(ネイティブウィンドウ、WebAssemblyなど)で実行するためのラッパー

eframe = “0.27.0”
“`

注意: 上記のバージョン番号 (0.27.0) は執筆時点での最新版です。eguiは活発に開発が進められているため、将来的には新しいバージョンがリリースされる可能性があります。crates.io (https://crates.io/crates/egui, https://crates.io/crates/eframe) で最新バージョンを確認し、必要に応じてバージョン番号を更新してください。基本的には最新バージョンを使うのがおすすめです。

eframeは内部で描画にwgpu(GPU API)やglow(OpenGLラッパー)といったレンダラーを使用します。また、デスクトップネイティブアプリケーション以外にWebAssemblyビルドなどもサポートしています。Cargo.tomlでこれらの機能フラグを明示的に指定することも可能ですが、上記のシンプルな記述でもデフォルトで主要なネイティブバックエンドが有効になるため、まずはこの設定で問題ありません。特定のバックエンドを強制したい場合や、WebAssembly向けにビルドしたい場合は、eframeのドキュメントを参照して適切なfeatureフラグ(例: features = ["wgpu"], features = ["glow"], features = ["web"])を指定してください。

Cargo.tomlを保存したら、これでeguiとeframeを使う準備ができました。

eguiの基本概念を掘り下げる

コードを書き始める前に、eguiアプリケーションの内部での動きをもう少し詳しく見てみましょう。即時モードGUIの仕組み、ContextUi、そして描画ループの役割を理解することは、効果的なeguiアプリケーションを開発する上で非常に重要です。

描画ループの仕組み

eframeのようなeguiのバックエンドは、内部でアプリケーションの「描画ループ」を実行します。このループは、例えば毎秒60回といった決まった頻度で繰り返されます。ループの各ステップで行われることの概略は以下の通りです。

  1. 入力イベントの収集: ウィンドウシステムから、マウスの移動、クリック、キー入力、ウィンドウのリサイズなどのイベントを収集します。
  2. eguiコンテキストの開始: 新しいフレームの描画を開始するために、eguiの Context オブジェクトを更新します。このコンテキストには、前のフレームで収集された入力情報や、前フレームの状態などが引き継がれます。
  3. アプリケーションの update メソッド呼び出し: eframeは、あなたが実装した eframe::App トレイトの update メソッドを呼び出します。このメソッドの中で、あなたはアプリケーションの現在の状態に基づいてUIを構築するeguiのウィジェット関数を呼び出します。update メソッドには &mut egui::Context&mut eframe::Frame が渡されます。
  4. UIの構築とインタラクション処理: update メソッド内で、Context を使ってウィンドウやパネルを作成し、それらの領域を表す Ui オブジェクトを取得します。そして、Ui オブジェクトのメソッド(例: ui.button(...), ui.text_edit(...))を呼び出してウィジェットを配置します。eguiはこれらのウィジェットを描画リストに追加すると同時に、収集した入力イベントを処理し、ウィジェットとのインタラクション(クリック、入力など)を検出します。ウィジェット関数は、その結果を egui::Response として返します。あなたは Response を見て、アプリケーションの状態を更新します。
  5. eguiコンテキストの終了と描画データの取得: update メソッドの実行後、eguiは現在のフレームで構築されたUIの描画に必要な情報(形状、テキスト、テクスチャなど)と、ウィンドウシステムへの指示(カーソル形状の変更、ツールチップの表示、ウィンドウのリサイズ要求など)をまとめます。
  6. 画面へのレンダリング: eframeのバックエンドは、eguiから取得した描画データ(通常はベクタ形状のリスト)を、GPUなどを利用して実際の画面にレンダリングします。
  7. ウィンドウシステムへの指示の実行: eframeはeguiからの指示に従って、ウィンドウシステムの操作を行います。

このサイクルが高速に繰り返されることで、ユーザーは滑らかに動作するGUIを体験します。ポイントは、あなたのコード(update メソッド)が毎フレーム実行され、その時点のアプリケーションの状態に基づいてUIをゼロから「定義」し直すということです。

egui::Contextegui::Ui の役割

  • egui::Context: eguiシステム全体のグローバルな状態を管理します。入力イベント、時間、フォント、スタイル設定、ウィジェット間の状態(フォーカス、IDなど)といった、フレーム間で引き継がれる情報やシステムレベルの設定を保持します。update メソッドは常に &mut egui::Context を受け取り、これを介してUIの構築を開始します(例: egui::CentralPanel::default().show(ctx, ...))。
  • egui::Ui: GUI上の特定の領域(例えば、ウィンドウの中央パネル、水平レイアウトの中、ボタンの上など)を表します。ウィジェットは Context に直接描画されるのではなく、常に Ui オブジェクトに対して配置されます(例: ui.label(...), ui.button(...))。Ui オブジェクトは、その領域内での要素の配置(垂直方向か水平方向か)、パディング、利用可能なスペースといったレイアウト情報を提供し、管理します。パネルやレイアウトコンテナ(ui.vertical, ui.horizontal)を作成する際に渡されるクロージャの引数として Ui オブジェクトが提供されます。

Context はシステム全体、Ui は特定のUI領域という役割分担を理解しておくと、eguiのコード構造が分かりやすくなります。

最初のeguiアプリケーションを作成する

それでは、実際にコードを書いて、最初のeguiデスクトップアプリケーションを作成してみましょう。プロジェクトのsrc/main.rsファイルを開き、既存の内容をすべて削除して以下のコードを記述します。

“`rust
// src/main.rs

// eframeクレートと、そこから再エクスポートされているeguiクレートをインポート
use eframe::{egui, NativeOptions};

// アプリケーションの状態を保持する構造体を定義
// 即時モードGUIでは、UIの状態はアプリケーション側のデータとして保持する
struct MyApp {
// テキスト入力フィールドの状態を保持するString
name: String,
// スライダーの状態を保持するu32
age: u32,
}

// アプリケーションの初期状態を提供するためにDefaultトレイトを実装
// eframe::run_nativeの第3引数でデフォルトインスタンスを生成するのに使われる
impl Default for MyApp {
fn default() -> Self {
Self {
// 初期値を設定
name: “Arthur”.to_owned(), // .to_owned() で String に変換
age: 42,
}
}
}

// eframe::App トレイトを実装
// このトレイトは、eguiアプリケーションとして実行するために必要なメソッドを定義
impl eframe::App for MyApp {
// アプリケーションのUIを更新・描画するメソッド
// 毎フレーム、eframeによって呼び出される
fn update(&mut self, ctx: &mut egui::Context, _frame: &mut eframe::Frame) {
// egui::CentralPanel: ウィンドウの中央領域全体を占めるパネル
// .default(): デフォルト設定のCentralPanelを作成
// .show(ctx, |ui| { … }): 指定したContextでパネルを表示し、
// パネル内のUI構築を行うクロージャを実行
egui::CentralPanel::default().show(ctx, |ui| {
// ui: このパネル領域内でUI要素を配置するためのUiオブジェクト

        // ui.heading: ヘディングスタイルでテキストラベルを表示
        ui.heading("My egui Application");

        // ui.add_space: 垂直方向にスペースを追加
        ui.add_space(10.0);

        // ui.horizontal: このクロージャ内のUI要素を水平方向に並べる
        ui.horizontal(|ui| {
            // ui.label: シンプルなテキストラベルを表示
            ui.label("Your name: ");
            // ui.text_edit_singleline: シングルラインのテキスト入力フィールドを配置
            // &mut self.name を渡すことで、ユーザーの入力が直接 self.name に反映される
            ui.text_edit_singleline(&mut self.name);
        });

        // ui.add_space: 垂直方向にスペースを追加
        ui.add_space(5.0);

        // egui::Slider::new: スライダーウィジェットを作成
        // &mut self.age: スライダーの値の変更を反映させる変数 (u32型)
        // 0..=120: スライダーの範囲 (0から120まで)
        // .text("age"): スライダーの横に表示するラベルテキストを設定
        // ui.add: egui::Widget トレイトを実装したカスタムウィジェットを追加
        ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));

        // ui.add_space: 垂直方向にスペースを追加
        ui.add_space(10.0);

        // ui.button: ボタンを配置
        // .clicked(): ボタンがこのフレームでクリックされたか判定
        if ui.button("Click Me").clicked() {
            // ボタンがクリックされたときの処理
            println!("Button clicked! Current name: {}, age: {}", self.name, self.age);
        }

        // ui.add_space: 垂直方向にスペースを追加
        ui.add_space(10.0);

        // ui.label: 入力された名前と年齢を反映したテキストラベルを表示
        // 即時モードなので、self.name や self.age が変更されると、次のフレームでここも自動的に更新される
        ui.label(format!("Hello '{}', age {}", self.name, self.age));

        // --- 開発中に便利なデバッグ機能 ---
        // ui.ctx().debug_on_hover(true); // マウスカーソルを乗せたウィジェットのデバッグ情報を表示
        //
        // egui::TopBottomPanel::bottom("debug_panel").show(ctx, |ui| {
        //     ui.ctx().debug_ui(ui); // eguiのデバッグUIを表示
        // });
    });
}

// 以下はeframe::Appのオプションメソッド。今回はデフォルト実装で十分なのでコメントアウトまたは定義しない

// アプリケーションを終了するかどうかを決定 (trueを返すと終了)
// fn on_close_event(&mut self) -> bool { true }

// アプリケーション終了前に呼ばれるクリーンアップ処理
// fn on_exit(&mut self, _gl: Option<&mut glow::Context>) {}

// アプリケーションの状態を保存 (eframe::NativeOptionsでpersist_native=trueが必要)
// fn save(&mut self, storage: &mut dyn eframe::Storage) {}

// 保存された状態をロード
// fn load(&mut self, storage: &dyn eframe::Storage) {}

}

// アプリケーションのエントリーポイント
fn main() -> Result<(), eframe::Error> {
// ウィンドウの初期設定を定義
let options = NativeOptions {
// ウィンドウの初期サイズを設定 (幅320ピクセル, 高さ240ピクセル)
initial_window_size: Some(egui::vec2(320.0, 240.0)),
// その他のオプションはデフォルト
..Default::default()
};

// eframe::run_native: デスクトップ環境でeguiアプリケーションを実行
eframe::run_native(
    "My First egui App", // ウィンドウタイトル
    options, // ウィンドウオプション
    // アプリケーションインスタンスを生成するファクトリ関数 (クロージャ)
    // _cc: CreationContext (今回は使わないので無視)
    // Box::new(MyApp::default()): Default実装を使ってMyAppのインスタンスを生成し、Boxでラップして返す
    Box::new(|_cc| Box::new(MyApp::default())),
)

}
“`

コードの解説:

  1. use eframe::{egui, NativeOptions};: eframeクレートと、そこから再エクスポートされているeguiクレートを使用することを宣言しています。NativeOptionsはウィンドウ設定に使います。
  2. struct MyApp { name: String, age: u32, }: アプリケーションの「状態」を保持するための構造体を定義しています。ここでは、UI上のテキスト入力フィールドとスライダーに対応するデータを保持します。
  3. impl Default for MyApp { ... }: MyApp構造体にDefaultトレイトを実装し、アプリケーションの初期状態を作成できるようにしています。eframe::run_native関数がアプリケーションの最初のインスタンスを作成する際にこれを使用します。
  4. impl eframe::App for MyApp { ... }: この構造体がeframe::Appトレイトを実装しています。これにより、MyAppがeguiアプリケーションとして実行可能になります。
  5. fn update(&mut self, ctx: &mut egui::Context, _frame: &mut eframe::Frame): このメソッドがGUIアプリケーションの描画ループの毎フレームごとにeframeによって呼び出されます。
    • &mut self: アプリケーションの状態(MyApp構造体のフィールド)を更新できるように、可変参照で渡されます。
    • ctx: &mut egui::Context: eguiのコンテキスト。これを通じてUIの構築を開始します。
    • _frame: &mut eframe::Frame: ウィンドウに関する操作(サイズ変更、終了要求など)を行うためのオブジェクト。今回は使用しないためアンダースコアで始まります。
  6. egui::CentralPanel::default().show(ctx, |ui| { ... });: Context (ctx) を使って、ウィンドウの中央領域全体を占めるパネルを作成し、そのパネルの中にUI要素を配置するためのクロージャを実行します。クロージャの引数 ui は、このパネル領域に対応する egui::Ui オブジェクトです。以降のウィジェット配置はこの ui オブジェクトに対して行います。
  7. ui.heading(...), ui.label(...): シンプルなテキストラベルをUIに配置します。headingは大きめのフォントで表示されます。
  8. ui.horizontal(|ui| { ... });: このクロージャ内のウィジェットは水平方向に並べられます。内部の ui は、この水平レイアウトコンテナ内の領域を表します。
  9. ui.text_edit_singleline(&mut self.name);: シングルラインのテキスト入力フィールドを作成し、UIに配置します。引数に &mut self.name を渡すことで、ユーザーがフィールドに入力したテキストが直接 MyApp 構造体の name フィールドに自動的に反映されます。即時モードGUIの典型的なパターンです。
  10. ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));: スライダーウィジェットを作成し、UIに配置します。egui::Slider::new(&mut self.age, 0..=120)self.age という u32 型の変数を0から120の範囲で操作するスライダーを作成します。.text("age") でスライダーの横に「age」というラベルを追加しています。ui.add() メソッドは、egui::Widgetトレイトを実装したオブジェクト(ここでは設定を追加したスライダー)をUIに追加するために使用します。
  11. if ui.button("Click Me").clicked() { ... }: ボタンをUIに配置し、そのユーザーインタラクションを処理します。ui.button("Click Me") はボタンを描画し、egui::Response オブジェクトを返します。response.clicked() は、そのフレームでこのボタンがユーザーによってクリックされた場合に true を返します。true の場合にブロック内のコード(標準出力へのメッセージ表示)が実行されます。
  12. ui.label(format!("Hello '{}', age {}", self.name, self.age));: format! マクロを使って、self.nameself.age の現在の値を埋め込んだ文字列を作成し、ラベルとして表示します。即時モードGUIなので、nameage の値がテキスト入力やスライダーによって変更されると、次のフレームでこのラベルの表示は自動的に更新されます。
  13. fn main() -> Result<(), eframe::Error>: アプリケーションのエントリーポイントです。eframe::Error はeframe実行時のエラーを表す型です。
  14. let options = NativeOptions { ... };: ウィンドウの初期サイズなどの設定を保持する NativeOptions 構造体を作成します。
  15. eframe::run_native(...): デスクトップ環境でeguiアプリケーションを実行するためのeframeの関数です。
    • 第1引数: ウィンドウのタイトルバーに表示される文字列。
    • 第2引数: 前述のウィンドウオプション。
    • 第3引数: アプリケーションの初期インスタンスを生成するためのクロージャ。eframeはこのクロージャを呼び出し、戻り値として Box<dyn eframe::App>eframe::AppトレイトオブジェクトをBoxに入れたもの)を期待します。|_cc| Box::new(MyApp::default()) は、引数を無視して MyApp::default() で作成したデフォルトインスタンスをBoxに入れて返しています。

src/main.rsを保存したら、ターミナルでプロジェクトディレクトリのルート (my_first_egui_app) にいることを確認し、以下のコマンドを実行します。

bash
cargo run

初めて実行する際は、依存クレートのダウンロードとコンパイルに時間がかかります。ビルドが成功すると、指定したタイトルとサイズのウィンドウが表示され、作成したUI(ラベル、テキスト入力、スライダー、ボタン)が表示されるはずです。テキストフィールドに名前を入力したり、スライダーを動かしたり、ボタンをクリックしたりして、UIがインタラクティブに動作することを確認してください。ボタンをクリックすると、ターミナルにメッセージが表示されるはずです。

基本的なウィジェットの使い方をさらに詳しく

最初のアプリケーションでいくつかのウィジェットを使用しましたが、eguiが提供する基本的なウィジェットはこれ以外にもあり、それぞれに便利なオプションがあります。

ui.label(text)ui.add(egui::Label::new(text))

  • ui.label("Hello");: 最もシンプルな使い方。単にテキストを表示します。戻り値は Response ですが、通常は使いません。
  • ui.add(egui::Label::new("Hello").strong());: ui.add() を使うことで、よりカスタマイズされたラベルを配置できます。egui::Label::new() でラベルウィジェットを作成し、メソッドチェーンで設定を追加します。.strong() は太字、.italics() は斜体、.code() は等幅フォントになります。.wrap(true) で自動折り返しを有効にしたり、.text_color() で色を変えたりもできます。ui.add() はそのウィジェットの Response を返します。

ui.button(text)ui.add(egui::Button::new(text))

  • let response = ui.button("Click");: シンプルなボタン。Response を返し、.clicked() などでイベントを処理します。
  • let response = ui.add(egui::Button::new("Click").min_size(ui.available_size()));: ui.add() 形式では、ボタンのサイズを.min_size(), .max_size(), .exact_size() などで制御できます。ui.available_size() は、現在のUI領域で利用可能な最大サイズを返します。他にも、ボタンの色(.fill(), .stroke())、枠線、有効/無効状態(後述のui.add_enabledを使う方が一般的ですが)などを設定できます。

ui.text_edit_singleline(&mut text)ui.add(egui::TextEdit::singleline(&mut text))

  • ui.text_edit_singleline(&mut my_string);: シンプルなシングルライン入力。
  • ui.add(egui::TextEdit::multiline(&mut my_string).desired_rows(5).hint_text("複数行入力"));: ui.add() 形式でより多くのオプションが利用できます。TextEdit::multiline() で複数行入力フィールドを作成できます。.desired_rows(n) で表示行数を指定したり、.hint_text() で何も入力されていないときに薄く表示されるテキストを設定したり、.password(true) でパスワード入力フィールドにしたり、.lock_focus() で常にフォーカスを保持させたりできます。

ui.add(egui::Slider::new(&mut value, range))

  • ui.add(egui::Slider::new(&mut self.age, 0..=120).text("Age"));: 最初の例で見た通り、スライダーはui.add() 形式で作成するのが一般的です。.logarithmic(true) で対数スケールにしたり、.max_decimals(n) で表示する小数点以下の最大桁数を指定したりできます。

ui.checkbox(&mut checked, text)

  • ui.checkbox(&mut self.is_checked, "Enable Option");: チェックボックスは、可変参照&mut boolとラベルを指定するシンプルなメソッドが用意されています。

これらのウィジェットは最も基本的なものですが、eguiは他にもラジオボタン (ui.radio_value), コンボボックス/ドロップダウン (egui::ComboBox), プログレスバー (egui::ProgressBar), イメージ (ui.image) など、様々なウィジェットを提供しています。これらも基本的な使い方や概念は共通しており、アプリケーションの状態を保持する変数への参照を渡すことでUIとデータを同期させ、ウィジェットが返す Response を見てユーザーインタラクションを処理します。

レイアウトの活用

見栄えの良いUIを作成するには、ウィジェットを適切に配置することが重要です。eguiは簡単なレイアウト機能を提供します。

  • 垂直レイアウト (Vertical Layout): これがデフォルトのレイアウト方向です。ui.vertical(|ui| { ... }); ブロック内でウィジェットを配置すると、上から下に順に積み重ねられます。CentralPanelSidePanelのようなパネルは、デフォルトで垂直レイアウトになります。
  • 水平レイアウト (Horizontal Layout): ui.horizontal(|ui| { ... }); ブロック内でウィジェットを配置すると、左から右に順に並べられます。

“`rust
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading(“レイアウトのデモ”);

ui.label("これは垂直レイアウトの例です。");
ui.label("ラベル1");
ui.label("ラベル2");

ui.separator(); // 水平線 (垂直レイアウト内なので)

ui.horizontal(|ui| {
    ui.label("これは水平レイアウトの例です:");
    ui.button("ボタンA");
    ui.button("ボタンB");
});

ui.separator(); // 水平線

// 異なるレイアウトを組み合わせる
ui.vertical(|ui| { // 垂直ブロック
    ui.label("垂直ブロック内の水平ブロック:");
    ui.horizontal(|ui| { // 水平ブロック
        ui.checkbox(&mut self.opt1, "オプション1");
        ui.checkbox(&mut self.opt2, "オプション2");
    });
});

ui.separator();

// add_space を使って明示的にスペースを空ける
ui.horizontal(|ui| {
    ui.label("左");
    ui.add_space(50.0); // 水平スペース
    ui.label("中");
    ui.add_space(50.0);
    ui.label("右");
});

ui.add_space(20.0); // 垂直スペース

ui.vertical(|ui| {
    ui.label("上");
    ui.add_space(20.0); // 垂直スペース
    ui.label("下");
});

});
“`

レイアウトブロック(verticalhorizontalのクロージャ)はネストできます。これにより、複雑なUI構造を構築できます。ui.separator() は現在のレイアウト方向に対して垂直な線(垂直レイアウト内なら水平線、水平レイアウト内なら垂直線)を引きます。ui.add_space(amount) は、現在のレイアウト方向に従って指定したピクセル数のスペースを追加します。

カラムレイアウト (ui.columns)

ui.columns(count, |columns| { ... }); を使うと、指定した数の列にUI要素を配置できます。クロージャには、各列に対応する Ui オブジェクトのベクタが渡されます。

“`rust
ui.columns(3, |columns| {
// 1列目
columns[0].heading(“列1”);
columns[0].label(“アイテム A”);
columns[0].label(“アイテム B”);

// 2列目
columns[1].heading("列2");
columns[1].checkbox(&mut self.col2_checked, "チェック");

// 3列目
columns[2].heading("列3");
if columns[2].button("アクション").clicked() {
    // ...
}

});
“`

カラムレイアウトを使うと、フォームのような整列されたUIを作成しやすくなります。

状態管理とインタラクションの詳細

即時モードGUIにおける状態管理は、アプリケーション開発において最も重要な側面の1つです。eguiでは、UIウィジェットは軽量であり、永続的な状態を持ちません。すべてのアプリケーション固有の状態は、あなたのRustコード内のデータ構造(通常はeframe::Appを実装する構造体)に保持されます。

アプリケーション状態の保持:

“`rust
// アプリケーションの状態を保持する構造体
struct MyAppState {
text_input_value: String,
slider_value: f32,
checkbox_state: bool,
}

impl Default for MyAppState {
fn default() -> Self {
Self {
text_input_value: “初期テキスト”.to_owned(),
slider_value: 50.0,
checkbox_state: false,
}
}
}

impl eframe::App for MyAppState {
fn update(&mut self, ctx: &mut egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading(“状態管理デモ”);

        // 状態 (self.text_input_value) を参照して TextEdit を描画
        // TextEdit への入力は直接 self.text_input_value を更新
        ui.text_edit_singleline(&mut self.text_input_value);
        ui.label(format!("入力値: {}", self.text_input_value));

        ui.add_space(10.0);

        // 状態 (self.slider_value) を参照して Slider を描画
        // Slider 操作は直接 self.slider_value を更新
        ui.add(egui::Slider::new(&mut self.slider_value, 0.0..=100.0).text("値"));
        ui.label(format!("スライダー値: {:.1}", self.slider_value));

        ui.add_space(10.0);

        // 状態 (self.checkbox_state) を参照して Checkbox を描画
        // Checkbox 操作は直接 self.checkbox_state を更新
        ui.checkbox(&mut self.checkbox_state, "有効");
        ui.label(format!("チェック状態: {}", self.checkbox_state));

        ui.add_space(10.0);

        // ボタンは状態を直接更新しないが、クリックイベントを処理して状態を変更
        if ui.button("値をリセット").clicked() {
            // ボタンがクリックされたら、アプリケーションの状態を初期値に戻す
            self.text_input_value = "リセットされました".to_owned();
            self.slider_value = 0.0;
            self.checkbox_state = false;
        }
    });
}
// ...

}
“`

この例では、MyAppState構造体がUIの状態(テキスト、スライダー値、チェックボックスの状態)を保持しています。updateメソッドの毎フレームで、これらの状態フィールドを参照してウィジェットが配置されます。テキスト入力、スライダー、チェックボックスといったウィジェットは、渡された &mut 参照を通じて直接アプリケーションの状態を更新します。ボタンのようにクリックイベントをトリガーとするウィジェットは、response.clicked() の結果を見て、アプリケーションの状態をコードで明示的に変更します。

egui::Response オブジェクトの活用:

ウィジェット関数は、そのウィジェットが描画された領域や、そのフレームでのユーザーインタラクションに関する情報を含む egui::Response オブジェクトを返します。

“`rust
let button_response = ui.button(“このボタンについて知りたい”);

if button_response.clicked() {
println!(“ボタンがクリックされました!”);
}
if button_response.hovered() {
// マウスカーソルがボタンの上にあります
button_response.on_hover_text(“ツールチップのテキスト”); // ツールチップを表示
}
if button_response.dragged() {
println!(“ボタンがドラッグされています。”);
}
if button_response.gained_focus() {
println!(“ボタンがフォーカスを得ました。”);
}

// ボタンの描画領域を取得
let button_rect = button_response.rect;
// この領域に何かカスタム描画をしたい場合など
// ui.painter().rect_stroke(button_rect.expand(1.0), 0.0, (1.0, egui::Color32::RED));
“`

Response は、特定のウィジェットがインタラクションされたことを検知したり、ウィジェットが画面上のどこに配置されたかといった情報を得るために使用されます。on_hover_text() メソッドは、ウィジェットの上にマウスカーソルが乗ったときに小さなツールチップを表示する便利な機能です。

このように、即時モードGUIでは「状態を参照して描画」し、「インタラクションを検知して状態を更新」するというサイクルを明示的にコードで記述します。これにより、UIの表示が常にアプリケーションの現在の状態を正確に反映するようになります。

簡単なデスクトップアプリの作成例: シンプルなTODOリスト (再掲と詳細化)

これまでに学んだ基本的な概念とウィジェット、レイアウト、状態管理の知識を使って、もう少し実用的なアプリケーションを作成してみましょう。シンプルなTODOリストアプリケーションは、これらの概念を組み合わせるのに適しています。

アプリケーションの要件:

  • 新しいTODO項目を入力できるテキストフィールドと追加ボタン。
  • TODO項目の一覧を表示。
  • 各TODO項目に完了を示すチェックボックス。
  • 各TODO項目を削除するボタン。
  • 完了した項目をまとめて削除するボタン。

コード全体 (src/main.rs)

“`rust
// src/main.rs

use eframe::{egui, NativeOptions};
use egui::RichText; // テキストスタイルの変更にRichTextを使用

// TODOアイテムの状態を保持する構造体

[derive(Default, serde::{Deserialize, Serialize})] // 後述のsave/loadのためにSerialize/Deserializeをderive

struct TodoItem {
text: String,
completed: bool,
}

impl TodoItem {
// 新しいTODOアイテムを作成する関連関数
fn new(text: String) -> Self {
Self { text, completed: false }
}
}

// アプリケーション全体の状態を保持する構造体
// #[derive(Default, serde::{Deserialize, Serialize})] // save/loadのためにderive (後述)

[derive(Default)] // シンプル化のため、まずはDefaultのみ

struct TodoApp {
// 新しいTODOを入力するためのテキストフィールドの状態
new_todo_text: String,
// TODOアイテムのリスト
todos: Vec,
}

// eframe::App トレイトを実装
impl eframe::App for TodoApp {
// アプリケーションのUIを更新・描画するメソッド
fn update(&mut self, ctx: &mut egui::Context, _frame: &mut eframe::Frame) {
// CentralPanelを使い、ウィンドウ中央に描画領域を確保
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading(“シンプルなTODOリスト”); // アプリケーションタイトル

        ui.add_space(10.0); // スペース

        // 新しいTODOを入力するエリア
        ui.horizontal(|ui| { // 水平レイアウト
            ui.label("新しいTODO:");
            // テキスト入力フィールド。入力値は self.new_todo_text にバインドされる
            let response = ui.add(egui::TextEdit::singleline(&mut self.new_todo_text).desired_width(200.0).hint_text("ここにTODOを入力"));

            // "追加" ボタン
            // ボタンがクリックされ、かつ入力フィールドが空でない場合、新しいTODOを追加
            if ui.button("追加").clicked() && !self.new_todo_text.trim().is_empty() {
                // 新しいTodoItemを作成し、リストの先頭に追加 (新しい項目が上に表示されるように)
                self.todos.insert(0, TodoItem::new(self.new_todo_text.trim().to_owned()));
                // 入力フィールドをクリア
                self.new_todo_text.clear();
                // 入力フィールドにフォーカスを戻す (連続入力しやすくする)
                response.request_focus();
            }
        });

        ui.separator(); // 区切り線

        // TODOアイテムのリスト表示エリア
        // ScrolledAreaを使えば項目が多くてもスクロール可能になる(任意)
        // egui::ScrollArea::vertical().show(ui, |ui| {

            let mut indices_to_remove = Vec::new(); // 削除対象のインデックスを保持するベクタ

            // TODOリストをイテレートして表示
            // リストを逆順に表示したい場合は .iter_mut().rev().enumerate() を使う
            // 今回は追加時に先頭に入れているので、そのままイテレート
            for (i, todo) in self.todos.iter_mut().enumerate() {
                ui.horizontal(|ui| { // 各TODOアイテムを水平レイアウトで表示
                    // チェックボックス (完了状態)
                    // &mut todo.completed を渡すことで、チェックボックスの操作が直接 TodoItem の状態に反映される
                    ui.checkbox(&mut todo.completed, ""); // ラベルなしのチェックボックス

                    // TODOテキスト
                    // 完了済みの場合は RichText で取り消し線を表示
                    let text = if todo.completed {
                        RichText::new(&todo.text).strikethrough()
                    } else {
                        RichText::new(&todo.text)
                    };
                    // RichText オブジェクトを Label ウィジェットとして追加
                    ui.add(egui::Label::new(text).wrap(true)); // 長いテキストを折り返す

                    // スペースを確保して削除ボタンを右端に寄せる
                    ui.add_space(ui.available_width() - ui.spacing().item_spacing.x);

                    // 削除ボタン ("×" マークのボタン)
                    // ボタンがクリックされたら、削除対象のインデックスを記録
                    if ui.button("×").clicked() {
                        indices_to_remove.push(i);
                    }
                });
            }

            // イテレーション後にまとめて削除処理を実行
            // インデックスが大きい方から削除しないと、remove() でインデックスがずれてしまう
            for i in indices_to_remove.iter().rev() {
                self.todos.remove(*i);
            }

        // }); // ScrollArea の終了

        ui.separator(); // 区切り線

        // 完了した項目を一括削除するボタン
        if ui.button("完了した項目をすべて削除").clicked() {
            // retain メソッドを使って、完了していない項目だけをリストに残す
            self.todos.retain(|todo| !todo.completed);
        }

        // デバッグ用のウィジェット。開発中はコメントアウトを外すと便利
        // egui::Window::new("Debug").show(ctx, |ui| {
        //     ui.ctx().debug_ui(ui);
        // });
    });
}

// eframe::App の save/load メソッド
// アプリケーションの状態を保存・ロードする
// NativeOptions で persist_native = true が設定されている場合に呼び出される
// この機能を使うには Cargo.toml の eframe 依存関係に `features = ["persistence"]` を追加する必要がある
// また、保存/ロードしたい構造体には serde::Serialize と serde::Deserialize を derive する必要がある
/*
fn save(&mut self, storage: &mut dyn eframe::Storage) {
    // serde_json::to_string(&self.todos) などで状態をJSON/バイナリにシリアライズし、
    // storage.set_string("todos", &json_string); などで保存
    // 今回のTodoApp構造体全体を保存する場合は
    // if let Ok(json) = serde_json::to_string(&self) {
    //    storage.set_string(eframe::APP_KEY, &json); // eframe::APP_KEY は推奨されるキー
    // }
}

fn load(storage: &dyn eframe::Storage) -> Option<Self> {
    // storage.get_string("todos")などで保存されたデータを取得し、
    // serde_json::from_str::<Vec<TodoItem>>(&json_string)などでデシリアライズ
    // 今回のTodoApp構造体全体をロードする場合は
    // storage.get_string(eframe::APP_KEY).and_then(|json| {
    //    serde_json::from_str(&json).ok()
    // })
    None // ロードしない場合はNoneを返す
}
*/

// ウィンドウを閉じようとしたときのイベント
// false を返すとウィンドウを閉じさせない
// fn on_close_event(&mut self) -> bool { true }

// アプリケーション終了時のクリーンアップ処理
// fn on_exit(&mut self, _gl: Option<&mut glow::Context>) {}

}

// アプリケーションのエントリーポイント
fn main() -> Result<(), eframe::Error> {
// ウィンドウオプションを設定
let options = NativeOptions {
// ウィンドウの初期サイズ
initial_window_size: Some(egui::vec2(400.0, 600.0)),
// ウィンドウのリサイズを許可
resizable: true,
// アプリケーションの状態を自動的に保存/ロードする場合にtrueにする
// true にする場合は Cargo.toml に serde 関連の依存関係を追加し、
// TodoApp 構造体と TodoItem 構造体に #[derive(serde::Serialize, serde::Deserialize)] を、
// Cargo.toml の eframe 依存関係に features = [“persistence”, “serde”] を追加する必要がある
// persist_native: false, // 今回は手動保存/ロードを実装しないため false のまま

    ..Default::default()
};

// eframe::run_native でアプリケーションを実行
eframe::run_native(
    "シンプルなTODOリスト", // ウィンドウタイトル
    options, // ウィンドウオプション
    // アプリケーションインスタンス生成ファクトリ
    // Default 実装を使って TodoApp の初期インスタンスを作成
    Box::new(|_cc| Box::new(TodoApp::default())),
)

}
“`

TODOリスト例の追加解説:

  1. TodoItem および TodoApp 構造体: アプリケーションの状態を保持します。TodoItemは個々のTODOのテキストと完了状態、TodoAppは全体のTODOリストと新しいTODO用の入力テキストを保持します。
  2. update メソッド:
    • 新しいTODOの入力と追加は、水平レイアウト内に配置された TextEditButton で行います。
    • response.request_focus(); は、新しいTODOを追加した後にテキストフィールドにカーソルを戻すことで、ユーザーが続けて入力しやすくするためのUX上の工夫です。
    • TODOリストの表示には、self.todosベクタをイテレートし、各TodoItemに対して水平レイアウトでチェックボックス、テキスト、削除ボタンを配置しています。
    • ui.add(egui::Label::new(text).wrap(true)); のように wrap(true) を追加することで、TODOのテキストが長すぎる場合に自動的に折り返されるようになります。
    • ui.add_space(ui.available_width() - ui.spacing().item_spacing.x); の行は、チェックボックスとテキストの後に可能な限りの水平スペースを追加することで、削除ボタンを右端に寄せるためのテクニックです。ui.available_width() は現在の水平レイアウト内で利用可能な残りの幅を返します。ui.spacing().item_spacing.x はウィジェット間の標準的な水平スペースです。
    • 削除ボタンがクリックされた項目のインデックスを indices_to_remove ベクタに収集し、ループ終了後にまとめて削除しています。これは、ベクタをイテレートしている最中に要素を削除するとイテレーターが無効になったりインデックスがずれたりするのを避けるためのRustにおける一般的なパターンです。削除はインデックスが大きい方から行う必要があります (.iter().rev())。
    • 「完了した項目をすべて削除」ボタンは、Vec::retain() メソッドを使用して、完了していない項目だけを効率的に残すように実装しています。
  3. 状態の永続化 (コメントアウト部分): コメントアウトされているsaveloadメソッドは、eframeが提供する状態永続化機能を利用するためのものです。これを使うには、Cargo.tomlserdeクレートを依存関係に追加し、保存/ロードしたい構造体 (TodoAppTodoItem) に #[derive(serde::Serialize, serde::Deserialize)] を付け、eframeのfeaturesに "persistence""serde"を追加し、NativeOptionspersist_native: trueを設定する必要があります。実装はコメントアウトとして残してありますが、本記事の範囲を超えるため詳細な解説は省略します。必要に応じてeframeのドキュメントを参照してください。

このコードをsrc/main.rsに保存し、cargo runで実行すると、シンプルなTODOリストアプリケーションが起動します。新しいTODOを追加したり、完了にチェックを入れたり、項目を削除したり、完了項目を一括削除したりできることを確認してください。

デバッグとトラブルシューティング

egui開発中に問題が発生した場合、以下の点がデバッグの助けになります。

  • コンパイルエラー: Rustコンパイラは非常に親切です。エラーメッセージをよく読み、指示に従ってコードを修正しましょう。型エラー、借用エラー、依存関係のエラーなどがコンパイル時に検出されます。
  • ランタイムパニック: panic!が発生した場合、デフォルトでは簡単なメッセージしか表示されません。詳細なトレースバックを得るには、アプリケーション実行前に環境変数 RUST_BACKTRACE=1 を設定します(例: RUST_BACKTRACE=1 cargo run)。トレースバックはパニックが発生したコード上の位置を特定するのに役立ちます。
  • UIが表示されない/おかしい: update メソッド内のロジックを確認してください。特に、アプリケーションの状態(selfのフィールド)がUIの表示(ウィジェットの有効/無効、テキスト内容など)や挙動(ボタンが有効になる条件など)と正しく連動しているかを確認します。即時モードGUIでは、毎フレームのupdateの実行結果がそのフレームのUIを決定します。
  • ウィジェットのインタラクションがない: ボタンがクリックされても反応しない、テキスト入力が反映されないなどの場合、ウィジェット関数が返したResponseを正しく処理できていないか、ウィジェットに渡している可変参照(&mut)が期待通りに動作していない可能性があります。
  • eguiのデバッグUI: eguiには組み込みのデバッグツールがあります。ui.ctx().debug_on_hover(true);updateメソッド内のどこかで有効にすると、UI要素にマウスカーソルを乗せたときにその要素のID、領域、インタラクション状態などの情報がポップアップ表示されます。また、egui::Window::new("egui Debug").show(ctx, |ui| { ui.ctx().debug_ui(ui); }); のようにすると、eguiの内部状態、フォント、スタイル、パフォーマンス情報などを表示するデバッグウィンドウを開けます。これらはUIの問題調査に非常に役立ちます。
  • eframe/バックエンド関連: 描画が全く行われない、特定の環境でのみ表示がおかしい、といった場合は、eframeが利用しているレンダリングバックエンド(wgpu, glow)に問題がある可能性があります。eframeや関連クレートのIssueトラッカーやドキュメントを確認したり、Cargo.tomlで特定のバックエンドfeatureを有効/無効にしてみたりすることを検討します。
  • リリースビルドとデバッグビルド: cargo runはデフォルトでデバッグビルドを行います。デバッグビルドは実行速度は遅いですが、デバッグ情報が豊富に含まれます。cargo run --release でリリースビルドを行うと速度は向上しますが、デバッグは難しくなります。開発中はデバッグビルド、配布時にはリリースビルドを使い分けるのが一般的です。

まとめ

本記事では、Rustとegui、eframeを組み合わせて簡単なデスクトップアプリケーションを開発するための基本的なステップと概念を詳細に解説しました。

  • Rustの安全性、パフォーマンス、信頼性がGUI開発にもたらす利点。
  • eguiの即時モードGUIというアプローチの特長と、ContextUi、描画ループの仕組み。
  • eframe::Appトレイトを実装してアプリケーション構造を定義する方法。
  • LabelButtonTextEditSliderCheckboxといった基本的なウィジェットの使い方。
  • verticalhorizontalcolumnsといったレイアウト機能によるUI配置。
  • 即時モードGUIにおけるアプリケーション状態の管理と、ウィジェットからのResponseを使ったインタラクション処理。
  • これらを組み合わせたシンプルなTODOリストアプリケーションの実装例。

eguiは、「手軽にGUI機能を組み込みたい」「ゲームやツールのデバッグUIを素早く作りたい」といった用途で特に輝きを放ちますが、今回作成したTODOリストのように、一般的なデスクトップアプリケーションの構築にも十分対応できます。Rustの強力な型システムとeguiのシンプルなAPIの組み合わせは、GUI開発を安全かつ楽しくしてくれます。

この記事で解説した内容はeguiの基本的な部分に焦点を当てたものですが、eguiには他にもカスタムウィジェット、ペインターによるカスタム描画、独立したウィンドウや様々なパネル、スタイルカスタマイズ、ファイルダイアログ連携など、より高度な機能が多数あります。興味を持たれた方は、ぜひ公式ドキュメントやGitHubリポジトリの豊富なサンプルコードを参照して、さらにeguiの世界を探求してみてください。

RustでのGUI開発はまだ新しい分野ですが、eguiのような使いやすいライブラリの登場により、その可能性は広がっています。安全で高速なデスクトップアプリケーションをRustで開発する道が、今、開かれています。

ぜひ、この記事を開発の第一歩として、あなた自身のアイデアを形にしてみてください。

さらに学ぶためのリソース

これらのリソースを活用し、Rust + eguiでのGUI開発をさらに深めていってください。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール