Rust系统托盘图标库ksni的使用,ksni提供跨平台系统托盘图标和菜单功能集成

Rust系统托盘图标库ksni的使用

ksni是一个实现了KDE/freedesktop StatusNotifierItem规范的Rust库,提供了跨平台的系统托盘图标和菜单功能集成。

基本示例

以下是ksni的基本使用示例:

use ksni::TrayMethods; // 提供spawn方法

#[derive(Debug)]
struct MyTray {
    selected_option: usize,
    checked: bool,
}

impl ksni::Tray for MyTray {
    fn id(&self) -> String {
        env!("CARGO_PKG_NAME").into()
    }
    fn icon_name(&self) -> String {
        "help-about".into()
    }
    fn title(&self) -> String {
        if self.checked { "CHECKED!" } else { "MyTray" }.into()
    }
    fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
        use ksni::menu::*;
        vec![
            SubMenu {
                label: "a".into(),
                submenu: vec![
                    SubMenu {
                        label: "a1".into(),
                        submenu: vec![
                            StandardItem {
                                label: "a1.1".into(),
                                ..Default::default()
                            }
                            .into(),
                            StandardItem {
                                label: "a1.2".into(),
                                ..Default::default()
                            }
                            .into(),
                        ],
                        ..Default::default()
                    }
                    .into(),
                    StandardItem {
                        label: "a2".into(),
                        ..Default::default()
                    }
                    .into(),
                ],
                ..Default::default()
            }
            .into(),
            MenuItem::Separator,
            RadioGroup {
                selected: self.selected_option,
                select: Box::new(|this: &mut Self, current| {
                    this.selected_option = current;
                }),
                options: vec![
                    RadioItem {
                        label: "Option 0".into(),
                        ..Default::default()
                    },
                    RadioItem {
                        label: "Option 1".into(),
                        ..Default::default()
                    },
                    RadioItem {
                        label: "Option 2".into(),
                        ..Default::default()
                    },
                ],
                ..Default::default()
            }
            .into(),
            CheckmarkItem {
                label: "Checkable".into(),
                checked: self.checked,
                activate: Box::new(|this: &mut Self| this.checked = !this.checked),
                ..Default::default()
            }
            .into(),
            StandardItem {
                label: "Exit".into(),
                icon_name: "application-exit".into(),
                activate: Box::new(|_| std::process::exit(0)),
                ..Default::default()
            }
            .into(),
        ]
    }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let tray = MyTray {
        selected_option: 0,
        checked: false,
    };
    let handle = tray.spawn().await.unwrap();

    tokio::time::sleep(std::time::Duration::from_secs(5)).await;
    // 我们可以修改托盘
    handle.update(|tray: &mut MyTray| tray.checked = true).await;
    // 永久运行
    std::future::pending().await
}

完整示例代码

use ksni::TrayMethods;

#[derive(Debug)]
struct AppTray {
    counter: u32,
    is_active: bool,
}

impl ksni::Tray for AppTray {
    fn id(&self) -> String {
        "my-app-tray".into()
    }
    
    fn icon_name(&self) -> String {
        if self.is_active {
            "network-wired-activated"
        } else {
            "network-wired"
        }.into()
    }
    
    fn title(&self) -> String {
        format!("Counter: {}", self.counter)
    }
    
    fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
        use ksni::menu::*;
        
        vec![
            StandardItem {
                label: format!("Click count: {}", self.counter),
                enabled: false,
                ..Default::default()
            }.into(),
            
            MenuItem::Separator,
            
            CheckmarkItem {
                label: "Active".into(),
                checked: self.is_active,
                activate: Box::new(|this: &mut Self| {
                    this.is_active = !this.is_active;
                }),
                ..Default::default()
            }.into(),
            
            StandardItem {
                label: "Increment".into(),
                icon_name: "list-add".into(),
                activate: Box::new(|this: &mut Self| {
                    this.counter += 1;
                }),
                ..Default::default()
            }.into(),
            
            StandardItem {
                label: "Reset".into(),
                icon_name: "edit-clear".into(),
                activate: Box::new(|this: &mut Self| {
                    this.counter = 0;
                }),
                ..Default::default()
            }.into(),
            
            MenuItem::Separator,
            
            StandardItem {
                label: "Quit".into(),
                icon_name: "application-exit".into(),
                activate: Box::new(|_| std::process::exit(0)),
                ..Default::default()
            }.into(),
        ]
    }
}

#[tokio::main]
async fn main() {
    let tray = AppTray {
        counter: 0,
        is_active: true,
    };
    
    let handle = tray.spawn().await.unwrap();
    
    // 每5秒更新一次计数器
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
    loop {
        interval.tick().await;
        handle.update(|tray| {
            tray.counter += 1;
        }).await;
    }
}

功能支持

  • [x] org.kde.StatusNotifierItem
  • [x] com.canonical.dbusmenu
  • [x] org.freedesktop.DBus.Introspectable
  • [x] org.freedesktop.DBus.Properties
  • [x] 单选项目
  • [ ] 文档
  • [x] 异步支持
  • [x] 可变菜单项

许可证

这是免费且不受限制的软件,发布到公共领域。任何人都可以自由复制、修改、发布、使用、编译、出售或分发此软件,无论是源代码形式还是编译后的二进制形式,用于任何目的,商业或非商业,以及通过任何方式。


1 回复

Rust系统托盘图标库ksni的使用指南

ksni是一个跨平台的Rust库,用于在系统托盘中创建和管理图标及菜单。它支持Linux、Windows和macOS等主流操作系统,提供了一个统一的API来创建系统托盘应用。

基本特性

  • 跨平台支持(Linux/Windows/macOS)
  • 支持自定义图标
  • 可添加右键菜单
  • 支持图标状态变化
  • 轻量级实现

使用方法

添加依赖

首先在Cargo.toml中添加ksni依赖:

[dependencies]
ksni = "0.1"

基本示例

下面是一个创建简单系统托盘图标的基本示例:

use ksni::{Tray, TrayService, MenuItem};
use std::sync::Arc;

struct MyTray {
    // 可以在这里添加你的应用状态
}

impl Tray for MyTray {
    fn icon_name(&self) -> String {
        // 返回图标名称或路径
        "input-mouse".to_string() // 这是一个示例,使用系统图标
    }

    fn title(&self) -> String {
        "My App".to_string()
    }

    fn menu(&self) -> Vec<MenuItem<Self>> {
        vec![
            MenuItem {
                label: "退出".to_string(),
                activate: Arc::new(|_| std::process::exit(0)),
                ..Default::default()
            }
        ]
    }
}

fn main() {
    let tray = MyTray {};
    let service = TrayService::new(tray);
    service.spawn();
    
    // 保持程序运行
    loop {
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

完整示例代码

下面是一个完整的ksni系统托盘应用示例,包含自定义图标、动态更新和复杂菜单功能:

use ksni::{Tray, TrayService, MenuItem, TrayUpdate};
use std::sync::Arc;
use std::time::Duration;

// 自定义托盘结构体
struct AppTray {
    counter: u32,  // 示例状态
    icon_index: u8,  // 当前图标索引
}

impl Tray for AppTray {
    fn icon_name(&self) -> String {
        // 根据状态返回不同图标
        match self.icon_index {
            0 => "face-smile".to_string(),
            1 => "face-sad".to_string(),
            _ => "face-laugh".to_string(),
        }
    }

    fn title(&self) -> String {
        format!("计数器: {}", self.counter)
    }

    fn menu(&self) -> Vec<MenuItem<Self>> {
        vec![
            MenuItem {
                label: "增加计数".to_string(),
                activate: Arc::new(|tray| {
                    println!("增加计数");
                    tray.counter += 1;
                }),
                ..Default::default()
            },
            MenuItem {
                label: "减少计数".to_string(),
                activate: Arc::new(|tray| {
                    println!("减少计数");
                    tray.counter -= 1;
                }),
                ..Default::default()
            },
            MenuItem::separator(),
            MenuItem {
                label: "切换图标".to_string(),
                submenu: vec![
                    MenuItem {
                        label: "笑脸".to_string(),
                        activate: Arc::new(|tray| {
                            tray.icon_index = 0;
                        }),
                        ..Default::default()
                    },
                    MenuItem {
                        label: "哭脸".to_string(),
                        activate: Arc::new(|tray| {
                            tray.icon_index = 1;
                        }),
                        ..Default::default()
                    },
                    MenuItem {
                        label: "大笑".to_string(),
                        activate: Arc::new(|tray| {
                            tray.icon_index = 2;
                        }),
                        ..Default::default()
                    },
                ],
                ..Default::default()
            },
            MenuItem::separator(),
            MenuItem {
                label: "退出".to_string(),
                activate: Arc::new(|_| std::process::exit(0)),
                ..Default::default()
            }
        ]
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建托盘实例
    let tray = AppTray {
        counter: 0,
        icon_index: 0,
    };
    
    // 创建托盘服务
    let service = TrayService::new(tray);
    let handle = service.handle();
    
    // 启动托盘服务
    service.spawn()?;
    
    // 在后台线程中动态更新托盘
    std::thread::spawn(move || {
        let mut count = 0;
        loop {
            std::thread::sleep(Duration::from_secs(5));
            handle.update(|tray: &mut AppTray| {
                count += 1;
                tray.counter = count;
                TrayUpdate::Title(format!("已运行: {}秒", count * 5))
            });
        }
    });
    
    // 保持主线程运行
    loop {
        std::thread::sleep(Duration::from_secs(1));
    }
}

平台注意事项

  1. Linux: 需要DBus服务,大多数桌面环境都支持
  2. Windows: 原生支持
  3. macOS: 使用NSStatusItem实现

错误处理

ksni可能会在初始化时失败,特别是在缺少必要依赖的环境中。建议添加适当的错误处理:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tray = MyTray {};
    let service = TrayService::new(tray);
    service.spawn()?;
    
    // 保持程序运行
    loop {
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

ksni提供了一种简单而强大的方式来为Rust应用程序添加系统托盘功能,使得创建后台服务或实用程序变得更加容易。

回到顶部