如何在使用gpui的 Rust GUI应用中添加系统托盘图标?

编程语言 2026-07-09

我正在尝试用gpui编写一个Rust GUI应用。

如何将托盘图标添加到代码?

Cargo.toml:

[dependencies]
gpui = "0.2.2"
gpui-component = "0.5.1"
tray-icon = "0.24.0"
image = "0.25.10"

main.rs:

重写 main.rs,在用户状态 HelloWorld 中添加tray: Option,并在创建 HelloWorld 时初始化托盘。

use gpui::{div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
           WindowBounds, WindowOptions, actions, KeyBinding,  WindowKind};
use gpui_component::{TitleBar, Root, };
use gpui_component::button::{Button, };

use tray_icon::{TrayIconBuilder, TrayIcon,
                TrayIconEvent,
                menu::{ Menu, MenuItem, MenuEvent, MenuEventReceiver }};


actions!(HelloWorld, [Quit]);


// 定义我们的 "Hello, World!" 视图结构体
// 这个结构体将持有视图的状态
struct HelloWorld {
    text: SharedString,
    // hold tray-icon
    tray: Option<TrayIcon>,
}

impl HelloWorld {

    fn load_icon(path: &std::path::Path) -> tray_icon::Icon {
        let (icon_rgba, icon_width, icon_height) = {
            let image = image::open(path)
                .expect("Failed to open icon path")
                .into_rgba8();
            let (width, height) = image.dimensions();
            let rgba = image.into_raw();
            (rgba, width, height)
        };
        tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
    }

    fn init_tray() -> TrayIcon {
        let icon = HelloWorld::load_icon(std::path::Path::new("/Users/...../images/icon.png"));

        let tray_menu = HelloWorld::new_tray_menu();

        TrayIconBuilder::new()
            .with_menu(Box::new(tray_menu))
            .with_tooltip("system-tray - tray icon library!")
            .with_icon(icon)
            .build()
            .unwrap()
    }

    fn new_tray_menu() -> Menu {
        let menu = Menu::new();
        let item1 = MenuItem::new("item1", true, None);

        if let Err(err) = menu.append(&item1) {
            println!("{err:?}");
        }
        menu
    }

    fn init_events() {
        if let Ok(event) = TrayIconEvent::receiver().try_recv() {
            println!("{:?}", event); // not fired
        }

        TrayIconEvent::set_event_handler(Some(move |event| {
            // here ,
        }));

        if let Ok(event) = TrayIconEvent::receiver().try_recv() {
            println!("tray event: {:?}", event);    // not fired
        }

        if let Ok(event) = MenuEvent::receiver().try_recv() {
            println!("menu event: {:?}", event);    // not fired
        }
    }

}

// 为 HelloWorld 实现 Render trait,这是 GPUI 渲染系统要求的
impl Render for HelloWorld {

    // 渲染方法:这是 GPUI 的核心
    // 它描述了视图在任何时候应该是什么样子
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        div()
            // .flex() 使其成为一个 Flexbox 容器
            .flex()
            // 改为垂直方向排列
            .flex_col()
            // 添加元素间距
            .gap_3()
            // .bg() 设置背景色,rgb() 是一个颜色辅助函数
            .bg(rgb(0x505050))
            .size(px(500.0))
            // .items_center() 和 .justify_center() 实现垂直和水平居中
            .justify_center()
            .items_center()
            .shadow_lg()
            .border_1()
            .border_color(rgb(0x0000ff))
            .text_xl()
            .child(format!("Hello, {}!\r\n按 cmd + q 退出程序。", &self.text))
            .child(
                div()
                    .flex()
                    .gap_2()
                    .child(div().size_8().bg(gpui::red()))
                    .child(div().size_8().bg(gpui::green()))
                    .child(div().size_8().bg(gpui::blue()))
                    .child(div().size_8().bg(gpui::yellow()))
                    .child(div().size_8().bg(gpui::black()))
                    .child(div().size_8().bg(gpui::white())),
            )
            .child(
                Button::new("ok").label("我是一个按钮")
                    .on_click(|_evt, _window, _app| {
                        println!("我被按下了")
                    })
            )
    }
}

fn gpui_build() {
    Application::new().run(|cx: &mut App| {

        HelloWorld::init_events();

        //若要使用 gpui_component 下的控件,需要下面这句话
        gpui_component::init(cx);

        //绑定快捷键 cmd + q, 行为 Quit
        cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);

        // 设置窗口初始位置和大小
        let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);

        let _ = cx.open_window(
            WindowOptions {
                kind: WindowKind::Floating,
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                titlebar: Some(TitleBar::title_bar_options()),
                window_min_size: Some(gpui::Size {
                    width: px(480.0),
                    height: px(480.0),
                }),
                // kind: WindowKind::PopUp,
                ..Default::default()
            },

            // mac 系统下, 在关闭了主窗口后,退出应用程序 的作法
            |window, cx| {

                // 绑定 Quit 行为
                cx.on_action(|_: &Quit, _cx| {
                    println!("Quit shortcut triggered");
                    std::process::exit(0);
                });

                //绑定窗口的关闭按钮的点击事件
                window.on_window_should_close(cx, |_window, _cx| {
                    println!("打印>> Main window is closing. Exiting app.");

                    std::process::exit(0);
                });

                let view = cx.new(|cx| {
                    HelloWorld {
                        text: "world".into(),
                        tray: Some(HelloWorld::init_tray())
                    }
                });

                cx.new(|cx| Root::new(view, window, cx))
            },
        );
        //将窗口提到前台
        cx.activate(true);
    });
}

fn main() {
    gpui_build();
}

构建并运行时,托盘会显示出来,单击托盘后,会弹出包含项“item1”的菜单;点击该菜单项时,却没有触发任何菜单点击事件。请帮忙,如何监听菜单点击事件?

解决方案

你需要扩展状态结构,用以存储在创建托盘图标时返回的对象,使用 tray-icon

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章