用 Rust 写一个桌面悬浮图标:为什么它比 Python 更适合 AI 桌面工具?

34 阅读5分钟

写在前面:如果你也曾幻想过像搜狗语音那样,在桌面上放一个常驻的悬浮图标,或者想做一个轻量级的 AI 桌面宠物,那么这篇文章就是为你准备的。我们将探讨如何用 Rust 实现它,并聊聊为什么在“桌面常驻工具”这个领域,Rust 是比 Python 更优雅的终局方案。


一、桌面悬浮图标的本质是什么?

在动手之前,我们需要先打破一个误区。

当你看到搜狗输入法的语音浮标、QQ 宠物或者系统托盘里的各种小图标时,它们绝不是普通的桌面快捷方式,而是:

  1. 一个特殊的 Windows 窗口(HWND) :它无边框、置顶、且背景透明。
  2. 一个系统托盘图标(NotifyIcon) :负责在后台常驻和唤起菜单。
  3. 业务逻辑的宿主:点击图标时,调用语音识别或 AI 接口。

在 Windows 底层,这类窗口通常带有 WS_EX_LAYERED(分层窗口)和 WS_EX_TOPMOST(置顶)扩展样式。


二、为什么不用 Python 写?

很多人的第一反应是:“这用 Python 的 pystray 或 tkinter 不是分分钟的事吗?”

确实,Python 写起来很快,但在“桌面常驻工具”这个特定场景下,Python 有几个无法回避的硬伤:

维度PythonRust
启动速度​需启动解释器,几百毫秒原生二进制,毫秒级
内存占用​常驻 80MB+,打包后体积巨大常驻 5-15MB,单文件 10MB 左右
打包分发​需要打包解释器和依赖库编译为单个 .exe,无依赖
系统交互​依赖第三方绑定,有时略显笨重直接调用系统 API,原生体验

对于“桌面宠物”这种需要开机自启、常驻后台、极低资源占用的工具来说,Python 显得有些过于“重”了。而 Rust,在提供 C++ 级别性能的同时,还保证了内存安全,是这类工具的绝佳选择。


三、实战:用 Rust 实现一个桌面悬浮图标

下面我们来实现一个最基础的桌面悬浮图标。它会在系统托盘常驻,点击退出。

1. 技术选型

我们将使用两个非常成熟的 Rust 库:

  • winit:跨平台窗口管理(我们主要用它创建窗口和事件循环)。
  • tray-icon:专门用来处理系统托盘图标的库(比自己调 Win32 API 方便太多)。

2. 初始化项目

cargo new rust_desktop_pet
cd rust_desktop_pet

编辑 Cargo.toml:

[package]
name = "rust_desktop_pet"
version = "0.1.0"
edition = "2021"

[dependencies]
winit = "0.29"
tray-icon = "0.12"
image = "0.24" # 用于处理图片

3. 编写核心代码

创建 src/main.rs,我们将实现一个“点击托盘图标退出”的最小示例。

use std::sync::Arc;
use tray_icon::{
    menu::{Menu, MenuItem},
    TrayIconBuilder,
};
use winit::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. 创建事件循环(不需要实际显示窗口,但需要它来维持程序运行)
    let event_loop = EventLoop::new();

    // 2. 创建托盘菜单
    let tray_menu = Menu::new();
    let quit_item = MenuItem::new("退出", true, None);
    tray_menu.append(&quit_item)?;

    // 3. 构建托盘图标
    // 注意:你需要准备一个名为 "icon.png" 的图标文件放在项目根目录
    let icon = load_icon(std::path::Path::new("icon.png"));
    
    let _tray_icon = TrayIconBuilder::new()
        .with_menu(Arc::new(tray_menu))
        .with_tooltip("我的 Rust 桌面宠物")
        .with_icon(icon)
        .build()?;

    println!("托盘图标已启动,右键点击退出...");

    // 4. 进入事件循环,等待用户操作
    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        if let Event::WindowEvent { event, .. } = event {
            if let WindowEvent::CloseRequested = event {
                *control_flow = ControlFlow::Exit;
            }
        }
    });
}

// 辅助函数:加载图标
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).unwrap()
}

4. 运行效果

  1. 准备一张 icon.png 放在项目根目录。
  2. 执行 cargo run。
  3. 你会发现系统托盘(右下角)出现了一个图标。右键点击,选择“退出”即可关闭程序。

这就完成了吗? ​ 是的,一个最基础的、常驻后台的 Rust 托盘程序已经跑起来了。如果你想实现“搜狗那种悬浮窗”,下一步就是在 winit 中创建一个无边框、透明的置顶窗口,并绑定鼠标拖拽事件。


四、进阶:如何实现真正的“悬浮窗”?

如果你想让图标像搜狗语音那样“浮”在桌面上,而不是缩在托盘里,你需要对 winit 创建的窗口进行特殊设置(以 Windows 为例):

  1. 无边框:设置 WindowBuilder::new().with_decorations(false)。
  2. 置顶:设置 WindowBuilder::new().with_window_level(winit::WindowLevel::AlwaysOnTop)。
  3. 透明:设置 WindowBuilder::new().with_transparent(true),并确保你的图标 PNG 带有透明通道。

由于 winit 是跨平台的,这套代码在 macOS 上也能运行(但 macOS 对悬浮窗的权限控制更严格)。


五、架构思考:AI 时代的最佳实践

虽然我们用 Rust 写了 UI 和托盘,但这并不意味着你必须用 Rust 重写所有的 AI 逻辑。

在真实的工程实践中,最推荐的架构是 “混合架构” :

  • UI 与系统层(Rust) :负责绘制悬浮窗、托盘、捕捉鼠标事件、调用系统 API。它极其轻量,常驻内存毫无压力。
  • AI 逻辑层(Python) :负责跑 PyTorch、调用 Whisper 或 Transformers。Rust 通过 std::process 或 pyo3 库来调用 Python 脚本,或者通过 HTTP/gRPC 与 Python 服务通信。

这样既享受了 Rust 带来的极致性能和低资源占用,又保留了 Python 在 AI 领域的统治级生态。


六、总结

特性Python 方案Rust 方案
开发效率​⭐⭐⭐⭐⭐(极快)⭐⭐⭐(需熟悉所有权)
运行性能​⭐⭐⭐⭐⭐⭐⭐
分发体验​需打包环境,体积大单文件 exe,即拷即用
适合阶段​原型验证、Demo正式产品、长期维护

结论:如果你只是想快速验证一个想法,Python 依然是你的好朋友;但如果你打算做一个真正“专业”、轻量且常驻的桌面 AI 工具,Rust 绝对是值得投入的“终局方案”。

希望这篇文章能帮你迈出用 Rust 构建桌面工具的第一步。如果你在实现悬浮窗透明效果或鼠标穿透时遇到了问题,欢迎在评论区交流!