【Tauri2.0教程(三)】自定义系统托盘

3,150 阅读4分钟

系统托盘,对Windows来说是右下角的应用程序小图标,对Mac来说是右上角的应用程序小图标。有系统托盘的好处是,在不打开应用程序主界面的情况下进行一些便捷处理,甚至对于一些功能简单的小程序来说甚至只保留系统托盘而不需要程序主界面,因此系统托盘对于一个应用程序来说是非常重要的。

如下图,是我电脑应用程序的一些系统托盘图标: image.png

本文同样参考了Tauri2.0官方教程,但是官方文档比较简略,因此在这里重新整理一下。

1.Cargo.toml文件中添加features

tauri = { version = "2.0.6", features = [ "tray-icon", "image-png" ] }

2.前端安装@tauri-apps/api依赖

npm install @tauri-apps/api

3.添加权限

\src-tauri\capabilities\default.json文件中添加如下权限

"permissions": [
  "core:window:default",
  "core:window:allow-start-dragging",
  "core:window:allow-close",
  "core:window:allow-minimize",
  "core:window:allow-toggle-maximize",
  "core:window:allow-destroy",
  "core:window:allow-hide",
  "core:window:allow-show",
  "core:window:allow-unminimize",
  "core:window:allow-set-focus"
]

4.实现托盘逻辑

实现托盘的逻辑可以通过前端js实现,也可以通过后端rust代码实现。在我看来,前端会更容易一些,但是使用前端有一个没有解决的问题,即托盘图标无法使用相对路径。

使用rust代码实现其实也没法使用相对路径。除非将存放图标的文件夹映射为资源目录,参考这篇文章,但是我并不希望使用这种方式。

rust还可以将icon图标加载为二进制数组的方式将图标引入。

注意下面两种实现方式任选其一即可!!!

4.1前端实现

tray.js里面创建托盘图标并添加菜单,然后再main.js引入

tray.js内容:

import {TrayIcon} from '@tauri-apps/api/tray';
import {Menu} from '@tauri-apps/api/menu';

export default async function tray_init() {
    const menu = await Menu.new({
        items: [
            {
                id: 'info',
                text: '关于',
                action: () => {
                    console.log("info press")
                }
            },
            {
                id: 'quit',
                text: '退出',
                action: () => {
                    // 退出逻辑
                    const appWindow = new Window('main');
                    appWindow.close()
                }
            },
        ],
    });

    const options = {
        // 这里使用了绝对路径,要求这个目录下面必须存在该文件,否则系统托盘图标为透明,显然系统打包以后分发是无法做到这个图标也一起走的
        icon: "C:\Users\xxx\RustProjects\star-app\src\assets\qq.png",
        menu,
        menuOnLeftClick: false,
        // 托盘行为
        action: (event) => {
            switch (event.type) {
                case 'Click':
                    console.log(
                        `mouse ${event.button} button pressed, state: ${event.buttonState}`
                    );
                    break;
                case 'DoubleClick':
                    // 在DoubleClick事件中添加了,双显示主窗口的逻辑,实现如下:
                    console.log(`mouse ${event.button} button pressed`);
                    // 双击图标显示主页面
                    const appWindow = new Window('main');
                    // 解除最小化
                    appWindow.unminimize();
                    // 显示主窗口
                    appWindow.show();
                    // 置于最顶层
                    appWindow.setFocus();
                    break;
                case 'Enter':
                    console.log(
                        `mouse hovered tray at ${event.rect.position.x}, ${event.rect.position.y}`
                    );
                    break;
                case 'Move':
                    console.log(
                        `mouse moved on tray at ${event.rect.position.x}, ${event.rect.position.y}`
                    );
                    break;
                case 'Leave':
                    console.log(
                        `mouse left tray at ${event.rect.position.x}, ${event.rect.position.y}`
                    );
                    break;
            }
        },
    };

    const tray = await TrayIcon.new(options);
}

main.js文件中添加第4-6行

import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import tray_init from "./tray.js";

// 初始化系统托盘
tray_init()
createApp(App).mount('#app')

上述内容配置完成以后再启动程序,在右下角会显示托盘图标,我程序的托盘图标就是上面图片中第一个白色背景的qq图标,如果右键点击会显示菜单,如图:

image.png

打包app,双击exe程序也会显示和上述效果一样的内容。 这里有个坑,如果tray.js文件中的内容放到main.js里面,本地运行可以成功,但是如果运行npx tauri build会报如下错误:

x Build failed in 1.74s
error during build:
[vite:esbuild-transpile] Transform failed with 1 error:
assets/index-!~{001}~.js:7484:13: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)

Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)
7482|  }
7483|
7484|  const menu = await Menu.new({
   |               ^
7485|      items: [
7486|          {

    at failureErrorWithLog (C:\Users\yulei\RustProjects\star-app\node_modules\esbuild\lib\main.js:1472:15)
    at C:\Users\yulei\RustProjects\star-app\node_modules\esbuild\lib\main.js:755:50
    at responseCallbacks.<computed> (C:\Users\yulei\RustProjects\star-app\node_modules\esbuild\lib\main.js:622:9)
    at handleIncomingPacket (C:\Users\yulei\RustProjects\star-app\node_modules\esbuild\lib\main.js:677:12)
    at Socket.readFromStdout (C:\Users\yulei\RustProjects\star-app\node_modules\esbuild\lib\main.js:600:7)
    at Socket.emit (node:events:394:28)
    at addChunk (node:internal/streams/readable:315:12)
    at readableAddChunk (node:internal/streams/readable:289:9)
    at Socket.Readable.push (node:internal/streams/readable:228:10)
    at Pipe.onStreamRead (node:internal/stream_base_commons:199:23)
beforeBuildCommand `npm run build` failed with exit code 1
    Error beforeBuildCommand `npm run build` failed with exit code 1

4.2后端实现

use serde::Deserialize;

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::{
    env, fs, io,
};
use tauri::{AppHandle, Manager, WindowEvent};
use tauri::image::Image;
use tauri_plugin_log::{Target, TargetKind};

use tauri::{
    menu::{Menu, MenuItem},
    tray::TrayIconBuilder,
};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::new().build())
        .setup(|app| {
            let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
            let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&quit_i, &show_i])?;

            let tray = TrayIconBuilder::new()
                // 关键代码是这一行,将磁盘上的文件加载到内存中,这样程序打包以后,不需要磁盘上的文件也能正常显示图标
                .icon(Image::from_bytes(include_bytes!("../icons/icon.png"))?)
                .menu(&menu)
                .show_menu_on_left_click(false)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "quit" => {
                        log::debug!("quit menu item was clicked");
                        app.exit(0);
                    }
                    _ => {
                        log::debug!("menu item {:?} not handled", event.id);
                    }
                })
                .on_tray_icon_event(|tray, event| match event {
                    TrayIconEvent::DoubleClick {
                        id: _,
                        position: _,
                        rect: _,
                        button: _
                    } => {
                        let app = tray.app_handle();
                        if let Some(window) = app.get_webview_window("main"){
                            let _ = window.unminimize();
                            let _ = window.show();
                            let _ = window.set_focus();
                        }
                    }
                    _ => {
                        // log::warn!("unhandled event {event:?}");
                    }
                })
                .build(app)?;

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

5.总结

本文实现了以下功能:

  1. 添加系统托盘并替换系统托盘图标
  2. 添加系统托盘菜单
  3. 监听系统托盘图标的各种行为