5. スタイル調整と2Dカスタム描画
テーマの切り替え(ダークモードとライトモード)
egui は最初から「ダークモード(暗い背景)」と「ライトモード(明るい背景)」の両方の配色テーマを備えています。アプリ全体の配色を切り替えるには、ctx.set_visuals メソッドに egui::Visuals::dark() または egui::Visuals::light() を渡します。
#![allow(unused)]
fn main() {
ui.horizontal(|ui| {
ui.label("画面テーマ:");
if ui.button("🌙 ダーク").clicked() {
ctx.set_visuals(egui::Visuals::dark());
}
if ui.button("☀ ライト").clicked() {
ctx.set_visuals(egui::Visuals::light());
}
});
}
また、標準で用意されているテーマ切り替えボタンを置きたい場合は、egui::widgets::global_theme_preference_buttons(ui) を呼ぶだけで、ライト・ダーク・システム設定追従の 3 つの切り替えボタンを 1 行で配置できます。
StyleとVisualsによる見た目のカスタマイズ
ボタンの大きさや部品同士のすき間、角の丸み、背景色などを細かく調整したいときは、余白やサイズを管理する Spacing と、色や角丸を管理する Visuals をも含んだ egui::Style を書き換えます。
▸ アプリ全体のスタイルを変更する(ctx.style_mut)
起動時(MyApp::new)などに ctx.style_mut を呼ぶと、アプリ全体の標準デザインを一括でカスタマイズできます。
#![allow(unused)]
fn main() {
fn customize_style(ctx: &egui::Context) {
ctx.style_mut(|style| {
// 1. 部品と部品の間のすき間(横, 縦)を広げる
style.spacing.item_spacing = egui::vec2(10.0, 8.0);
// 2. ボタンの内側の余白(横, 縦)を大きくして押しやすくする
style.spacing.button_padding = egui::vec2(12.0, 6.0);
// 3. すべてのウィジェットの角を丸くする(半径 8px)
let rounding = egui::Rounding::same(8.0);
style.visuals.widgets.noninteractive.rounding = rounding;
style.visuals.widgets.inactive.rounding = rounding;
style.visuals.widgets.hovered.rounding = rounding;
style.visuals.widgets.active.rounding = rounding;
});
}
}
style.visuals.widgets の中には、部品の状態ごとに 4 つの設定(操作できない部品 noninteractive、通常時 inactive、マウスが乗ったとき hovered、押されているとき active)が分かれており、それぞれの背景色(bg_fill)や枠線(bg_stroke)、角丸(rounding)を自由に変更できます。
▸ 特定の領域だけスタイルを変える(ui.scope)
「この警告枠の中だけボタンを赤くしたい」「このツールバーの中だけ余白をゼロにしたい」という場合は、ui.scope(|ui| { ... }) で囲んで ui.style_mut() を書き換えます。scope を抜けると自動的に元のスタイルに戻るため、他の場所に影響を与えません。
#![allow(unused)]
fn main() {
ui.scope(|ui| {
// このスコープの中だけ、部品の間隔をゼロにする
ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
let _ = ui.button("左");
let _ = ui.button("中央");
let _ = ui.button("右");
});
}
画像ファイルの読み込みと表示
アプリの中にアイコンやイラスト、写真を表示するには、公式の補助クレートである egui_extras の画像ローダー機能を使います。
まず Cargo.toml に egui_extras(all_loaders フィーチャー有効)と、対応したい画像形式を指定した image クレートを追加します。
[dependencies]
eframe = "0.29"
egui_extras = { version = "0.29", features = ["all_loaders"] }
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
次に、アプリ起動時に 1 回だけ egui_extras::install_image_loaders(&cc.egui_ctx) を呼び出します。あとは egui::include_image! マクロやファイルパス・URL を使って ui.image で簡単に画像を表示できます。
#![allow(unused)]
fn main() {
// 起動時(MyApp::new の中)に1回だけ呼ぶ
egui_extras::install_image_loaders(&cc.egui_ctx);
// update メソッド内で画像を表示する(横幅を最大 200px に制限し、角を丸める例)
ui.add(
egui::Image::new(egui::include_image!("../assets/logo.png"))
.max_width(200.0)
.rounding(10.0),
);
}
Painterを使った2D図形の直接描画
ボタンやラベルといった既存のウィジェットだけでは表現できないグラフ、図解、ゲーム画面、カスタムメーターなどを作りたいときは、egui::Painter を使って画面に直線や円、四角形を直接描くことができます。
flowchart LR
Step1["1. ui.allocate_painter で<br/>描画サイズとマウス反応領域を確保"] --> Step2["2. 戻り値から<br/>Response(座標・入力)と Painter を取得"]
Step2 --> Step3["3. painter.line_segment や<br/>painter.circle_filled で図形を描画"]
カスタム描画を行うときは、まず ui.allocate_painter(サイズ, 検知したいマウス操作) を呼び出してキャンバス領域を確保します。
#![allow(unused)]
fn main() {
use eframe::egui::{self, Color32, Pos2, Rect, Rounding, Sense, Stroke, Vec2};
// 幅 300px、高さ 150px のキャンバス領域を確保する
let (response, painter) = ui.allocate_painter(Vec2::new(300.0, 150.0), Sense::hover());
// 確保された領域の画面上の矩形座標(左上 min 〜 右下 max)を取得する
let rect: Rect = response.rect;
// 1. キャンバスの背景を塗りつぶす(角丸 6px)
painter.rect_filled(rect, Rounding::same(6.0), Color32::from_rgb(30, 30, 40));
// 2. 左上から右下へ緑色の対角線を引く(太さ 2px)
painter.line_segment(
[rect.left_top(), rect.right_bottom()],
Stroke::new(2.0, Color32::GREEN),
);
// 3. キャンバスの中心に赤い円を描く(半径 30px)
let center: Pos2 = rect.center();
painter.circle_filled(center, 30.0, Color32::from_rgb(220, 60, 60));
}
注意すべきポイントは、Painter に渡す座標は「キャンバスの左上を (0, 0) とした相対座標」ではなく、「ウィンドウ全体の左上を (0, 0) とした絶対座標(スクリーン座標)」である点です。そのため、response.rect.min(キャンバスの左上座標)や response.rect.center()(キャンバスの中心座標)を基準にして足し算することで、正しい位置に図形を描けます。
マウス操作と連動するお絵かきキャンバスの実装
allocate_painter の第 2 引数に Sense::drag() を指定すると、その領域内でマウスがドラッグされたときの座標を response.interact_pointer_pos() でリアルタイムに取得できます。これを利用して、マウスで自由に線が描ける「お絵かきアプリ」を作ってみましょう。
use eframe::egui::{self, Color32, Pos2, Rounding, Sense, Stroke};
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([500.0, 400.0]),
..Default::default()
};
eframe::run_native(
"Mini Drawing Canvas",
options,
Box::new(|_cc| Ok(Box::<DrawingApp>::default())),
)
}
struct DrawingApp {
// 1本の線は「座標(Pos2)の並び(Vec<Pos2>)」として表現し、複数本の線を保持する
lines: Vec<Vec<Pos2>>,
stroke_color: Color32,
stroke_width: f32,
}
impl Default for DrawingApp {
fn default() -> Self {
Self {
lines: Vec::new(),
stroke_color: Color32::LIGHT_BLUE,
stroke_width: 3.0,
}
}
}
impl eframe::App for DrawingApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
// 上部のツールバー(色選択・太さスライダー・クリアボタン)
ui.horizontal(|ui| {
ui.label("Pen Color:");
ui.color_edit_button_srgba(&mut self.stroke_color);
ui.label("Width:");
ui.add(egui::Slider::new(&mut self.stroke_width, 1.0..=15.0));
if ui.button("Clear").clicked() {
self.lines.clear();
}
});
ui.separator();
// 残りの画面領域いっぱいにドラッグ検知可能なキャンバスを確保する
let available_size = ui.available_size();
let (response, painter) = ui.allocate_painter(available_size, Sense::drag());
// キャンバスの背景を塗る
painter.rect_filled(response.rect, Rounding::same(4.0), Color32::from_gray(25));
// ドラッグ開始時に新しい線(空の Vec)を追加する
if response.drag_started() {
self.lines.push(Vec::new());
}
// ドラッグ中であれば、現在のマウス座標を最後の線に追加する
if let Some(pointer_pos) = response.interact_pointer_pos() {
if response.dragged() {
if let Some(current_line) = self.lines.last_mut() {
// 前回の点と異なる場合のみ追加する
if current_line.last() != Some(&pointer_pos) {
current_line.push(pointer_pos);
}
}
}
}
// 記録されているすべての線を順番に描画する
let stroke = Stroke::new(self.stroke_width, self.stroke_color);
for line in &self.lines {
if line.len() >= 2 {
painter.add(egui::Shape::line(line.clone(), stroke));
}
}
});
}
}
このプログラムに登場する ui.color_edit_button_srgba(&mut self.stroke_color) は、egui に標準搭載されているカラーピッカー(色選択ボタン)です。クリックするだけで本格的な色選択パレットが開き、選んだ色が即座に self.stroke_color に反映されます。
次章では、アプリを閉じてもデータを消さないための自動保存(Persistence)と、時間のかかる処理を裏側で動かして画面のフリーズを防ぐマルチスレッド連携を解説します。