Sworld

Sworld

The only way to do great work is to love what you do.

eguiウィンドウの非表示と再表示を実現する

最近、eguiを使用している際に問題に直面しました。ウィンドウを非表示にし、トレイアイコンから呼び出したいと思ったのですが、こんなに簡単な要件が非常に簡単であるべきだと思っていましたが、eguiの公式フレームワークeframeはこの機能のサポートがあまり良くないことがわかりました。GitHub では長期間にわたって関連する議論が行われています。例えば、この Issue

この記事では、まずeframeの問題を再現し、その後、egui_glowegui_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_glowegui_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。成功した場合は、ここで関連情報を更新します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。