最近、egui
を使用している際に問題に直面しました。ウィンドウを非表示にし、トレイアイコンから呼び出したいと思ったのですが、こんなに簡単な要件が非常に簡単であるべきだと思っていましたが、egui
の公式フレームワークeframe
はこの機能のサポートがあまり良くないことがわかりました。GitHub では長期間にわたって関連する議論が行われています。例えば、この Issue。
この記事では、まずeframe
の問題を再現し、その後、egui_glow
とegui_winit
を使用して実現した解決策を紹介し、まだ使用可能なサンプルを提供します。
問題の再現#
前提依存関係#
以下は私のCargo.toml
ファイルです:
[package]
name = "test_egui"
version = "0.1.0"
edition = "2024"
[dependencies]
egui = "0.31.1"
eframe = "0.31.1"
tokio = { version = "1.44.1", features = ["full"] }
問題の再現#
問題を簡略化するために、上記の問題を次のようなモデルに抽象化しました:Rust プログラムを作成し、バックグラウンドスレッドが定期的に表示または非表示にする。
この機能を実現するために、次のようなコードを書くのは簡単です:
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("こんにちは、世界!");
});
});
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let _guard = rt.enter();
eframe::run_native(
"Egui テストアプリ",
Default::default(),
Box::new(|_cc| {
let ctx = _cc.egui_ctx.clone();
rt.spawn(async move {
loop {
println!("バックグラウンドタスクを実行中...");
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(false));
ctx.request_repaint();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
ctx.request_repaint();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
Ok(Box::new(MyApp {}))
}),
)?;
Ok(())
}
しかし残念ながら、このコードは正常に動作しません。バックグラウンドスレッドではウィンドウを非表示にすることができることがわかりますが、ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
を呼び出しても何の効果もありません。出力されたログから、ループは正常に実行されていることがわかりますが、ウィンドウは再表示されません。これは、eframe
がウィンドウが非表示のときにこれらのイベントを処理しないためです。GitHub の関連する議論によれば、これは現在解決されていない問題です。したがって、eframe
を一時的に使用せず、egui
の低レベルライブラリであるegui_glow
とegui_winit
を使用してこの機能を実現する必要があります。
解決策#
もし私たちが自分でwinit
のイベントループを処理できれば、ウィンドウの動作を簡単にカスタマイズできます。公式のサンプルを見つけました:Prue-glow。
依存関係の更新#
eframe
を使用しないことになったので、上記のリンクを参考にしてCargo.toml
の依存関係を次のように更新する必要があります:
[dependencies]
egui = "0.31.1"
egui-winit = "0.31.1"
winit = "0.30.9"
glow = "0.16.0"
egui_glow = {version = "0.31.1", features = ["winit"]}
glutin = "0.32.2"
glutin-winit = "0.5.0"
tokio = { version = "1.44.1", features = ["full"] }
log = "0.4.26"
改造後のmain
#
fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = winit::event_loop::EventLoop::<event::UserEvent>::with_user_event()
.build()
.unwrap();
let proxy = event_loop.create_proxy();
let rt = tokio::runtime::Runtime::new()?;
let _guard = rt.enter();
let proxy_clone = proxy.clone(); // !注意: バックグラウンドタスク用にプロキシをクローン
rt.spawn(async move {
loop {
println!("バックグラウンドタスクを実行中...");
proxy_clone
.send_event(event::UserEvent::HideWindow)
.unwrap();
proxy_clone
.send_event(event::UserEvent::Redraw(Duration::ZERO))
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
proxy_clone
.send_event(event::UserEvent::ShowWindow)
.unwrap();
proxy_clone
.send_event(event::UserEvent::Redraw(Duration::ZERO))
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
let mut app = app::GlowApp::new(
proxy,
Box::new(|egui_ctx| {
egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.heading("こんにちは世界!");
});
}),
);
event_loop.run_app(&mut app).expect("アプリの実行に失敗しました");
Ok(())
}
これが私が改造した後のmain
関数で、主な変更点はwinit
のイベントループを使用してウィンドウの動作を処理することです。バックグラウンドスレッドでEventLoopProxy<UserEvent>
を通じてUserEvent
を送信してウィンドウの表示と非表示を制御します。
イベント処理#
同様に、ApplicationHandler<UserEvent>
を実装する必要があります。ここでUserEvent
を受け取り処理します。アプリの状態を管理する必要がある場合は、GlowApp
構造体に関連フィールド(私がコメントアウトしたAppState
など)を追加し、user_event
で処理と更新を行うことができます。ここでは、単純なデータ表示などの操作を行うことをお勧めします。計算部分はバックグラウンドで行う方が良いでしょう。そうしないと UI の流暢さに影響を与えます。また、update_ui
を更新してAppState
を受け入れる必要があるかもしれません。
pub struct GlowApp {
proxy: winit::event_loop::EventLoopProxy<UserEvent>,
gl_window: Option<GlutinWindowContext>,
gl: Option<Arc<glow::Context>>,
egui_glow: Option<egui_glow::EguiGlow>,
repaint_delay: std::time::Duration,
clear_color: [f32; 3],
window_hidden: bool,
update_ui: Box<dyn Fn(&egui::Context) + Send + Sync + 'static>,
// state: AppState,
}
impl winit::application::ApplicationHandler<UserEvent> for GlowApp {
// ... 他のメソッドは省略
fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::Redraw(delay) => self.repaint_delay = delay,
UserEvent::ShowWindow => {
self.window_hidden = false;
if let Some(ref gl_window) = self.gl_window {
gl_window.window().set_visible(true);
gl_window.window().request_redraw();
}
}
UserEvent::HideWindow => {
self.window_hidden = true;
if let Some(ref gl_window) = self.gl_window {
gl_window.window().set_visible(false);
}
}
}
}
}
まとめ#
これでプログラムのウィンドウは定期的に非表示になり、再表示されるようになります。トレイアイコンのイベント処理機能を実現したい場合は、このサンプルを参考にしてトレイイベントを私たちのイベントに追加し、user_event
で処理します。
完全なコード#
なぜ上で直接すべてのコードを提示しなかったのか?それは長すぎるからです…… 私は、簡単な要件を実現するためには、上記の部分を理解するだけで迅速に修正できると思っています。私の(または公式の)例をコピーして使ってください。もちろん、私のコードには多くの小さな問題があり、直接生産環境で使用するには適していませんので、使用する前に注意深く確認してください。
また、私は egui デスクトップアプリのテンプレートを構築しようとしていますので、私の GitHub ページをフォローしてください:mcthesw。成功した場合は、ここで関連情報を更新します。