加载中...

tauri 2.0 创建系统托盘

tauri 2.0 创建系统托盘

tauri 提供了两种系统托盘的创建方式,托盘 API 在 JavaScript 和 Rust 中均可用。

通用基本配置(不管是 JavaScript 还是 Rust):

我们需要在 src-tauri/Cargo.toml 中写入包含系统托盘的必要功能 tray-iconimage-png

[package]
name = "tauri-app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

[lib]
name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
# 为 tauri 的 features 数组里加入 "tray-icon",保存时自动更新依赖。
# "image-png" 是为了支持 Image api 读取文件存在的,详细请参考源码:
# import { Image } from ''@tauri-apps/api/image''; static fromPath 函数的注释
# 如果没有添加 "image-png",则在读取 png 时将会出现该错误:"Uncaught (in promise) expected RGBA image data, found a file path"
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

一、使用 JavaScript 创建系统托盘

一. 使用 JavaScript 创建系统托盘:

// 导入系统托盘, 
import { TrayIcon } from ''@tauri-apps/api/tray'';

/**
 * 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
 */
const options = {
	// icon 的相对路径基于:项目根目录/src-tauri/
    icon : "icons/32x32.png",
    // 托盘提示,悬浮在托盘图标上可以显示 tauri-app
    tooltip: ''tauri-app'',
    // 是否在左键点击时显示托盘菜单,默认为 true。当然不能为 true 啦,程序进入后台不得左键点击图标显示窗口啊。
    menuOnLeftClick: false,
    // 托盘菜单,后续创建.....
    menu: undefined,
    // 托盘图标上事件的处理程序。这个下面需要详细说一下
    action: (event) => {}
}
/**
 * 创建系统托盘
 */
export async function createTray() {
    const tray = await TrayIcon.new(options);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

二. 设置 JavaScript 系统托盘事件

在上面的代码中,我们通过运行 createTray 函数就可以得到一个系统托盘图标。但是光有一个图标还是不够的,所以我们还得有相对应的事件,通过 Ctrl+左键 进入源码部分,我们可以看到 options.action 函数传入了一个 TrayIconEvent 联合类型事件:

export type TrayIconEvent = (TrayIconEventBase<''Click''> & TrayIconClickEvent) | (TrayIconEventBase<''DoubleClick''> & Omit<TrayIconClickEvent, ''buttonState''>) | TrayIconEventBase<''Enter''> | TrayIconEventBase<''Move''> | TrayIconEventBase<''Leave''>;
  • 1

在这里,它提供了 ClickDoubleClickEnterMoveLeave(单击,双击,鼠标移入,鼠标拖动,鼠标移出)这五个托盘事件。
action 属性函数的 event 参数我们需要使用 switch 或者是 if 把它的 type 筛选出来,在 case 选项中执行我们的事件:

action: (event) => {
        switch(event.type) {
            case ''Click'': 
                console.log(''单击事件'');
                console.log(''Click event:'', event);
                console.log(''Mouse button:'', event.button);
                console.log(''Button state:'', event.buttonState);
                break;
            case ''DoubleClick'':
                console.log(''双击事件'');
                console.log(''DoubleClick event:'', event);
                break;
            case ''Enter'':
                console.log(''鼠标进入托盘图标'');
                console.log(''Mouse entered tray icon area:'', event);
                break;
            case ''Move'':
                console.log(''鼠标移动托盘图标'');
                console.log(''Mouse moved over tray icon:'', event);
                break;
            case ''Leave'':
                console.log(''鼠标离开托盘图标'');
                console.log(''Mouse left tray icon area:'', event);
                break;    
            default: 
                console.log(`未定义事件类型: ${event.type}`);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

对于我们来说,常用的也就只有第一个 Click 事件,其他的用的不多,所以别的我也就不说了,大家自行选择就好。

那么接下来,我们来试一试单击托盘图标会怎么样:
New Image

看,它单击事件触发了两次,一次是鼠标按下(Button state: Down),一次是鼠标松开(Button state: Up)。 对于我们来说,我们只需要一次就可以,通过简单的 if 判断可以过滤掉一次事件:

if(event.buttonState === ''Down'')
或者
if(event.buttonState === ''Up'')

New Image

当然还有一件事,我们多次修改了文件,vite 在多次的重载中难免会造成多个系统托盘图标,但是没关系,重启应用后,我还是一个 o( ̄▽ ̄)o
New Image

三. JavaScript 设置窗口关闭后,应用进入后台运行:

其实吧,我们只需要隐藏掉窗口就好,至于怎么隐藏呢,我在这里说一下步骤:
tauri 提供了 import { getCurrentWindow } from ''@tauri-apps/api/window''; api,执行 getCurrentWindow 函数可以获取到一个 Window 当前窗体实例,我们可以通过这个实例的 hide() 函数隐藏窗口。

  1. 关闭系统自带的拖拽栏:设置 tauri.conf.json 属性:app.windows.decorations = false
    New Image

  2. 使用 HTML+CSS+JS 替代系统拖拽栏,重点是这个关闭按钮得换成 hide()

  3. 下一步就是参考我的上一篇文档,看看这两步是怎么做的:tauri 2.0创建项目 https://blog.csdn.net/xiaoyuanbaobao/article/details/143751093

在干完上面的操作后,我们就可以关闭窗口,在创建托盘的页面引入 getCurrentWindow 函数在单击事件中调用它的 show() 方法把进入后台运行的程序窗口打开。

只是把窗口显示出来就够了?当然不是,我们还得在它窗口没隐藏的情况下把窗口置顶聚焦,这才符合我们的程序使用习惯。

  • 通过 isVisible 方法可以检查窗口是否是可视状态

  • 通过 show 方法可以把窗口显示出来

  • 通过 setFocus 方法可以聚焦窗口,让窗口置顶显示

  • 通过 unminimize 方法可以解除最小化,怎么好像混进去什么奇怪的东西?
    这 TM 是个大坑!!!!!!

    在进入最小化的情况下,聚焦无法将页面置顶只有解除最小化之后聚焦才是有效的
    这个在 rustapi 中并没有这个问题,但是在 Javascriptapi 是这样的。!!!需要特别注意!!!

tauri 遵循最小权限原则,在运行中可能会碰到如下权限不足的问题:
New Image
src-tauri/capabilities/default.json 文件的 permissions 属性数组中加入这几个权限:
"core:window:allow-set-focus""core:window:allow-close
"core:window:allow-show""core:window:allow-is-visible"
"core:window:allow-unminimize""core:window:allow-is-minimized"
New Image

写到现在给大家再看看现在的 Tray 代码吧,目前它任然是不完整的,我还没挂托盘菜单 Menu 呢:

// 导入系统托盘
import { TrayIcon } from ''@tauri-apps/api/tray'';
import { getCurrentWindow } from ''@tauri-apps/api/window'';

/**
 * 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
 */
const options = {
    // icon 的相对路径基于:项目根目录/src-tauri/
    icon : "icons/32x32.png",
    // 托盘提示,悬浮在托盘图标上可以显示 tauri-app
    tooltip: ''tauri-app'',
    // 是否在左键点击时显示托盘菜单,默认为 true。当然不能为 true 啦,程序进入后台不得左键点击图标显示窗口啊。
    menuOnLeftClick: false,
    // 托盘图标上事件的处理程序。
    action: (event) => {
        // 左键点击事件
        if(event.type === ''Click'' && event.button === "Left" && event.buttonState === ''Down'') {
            console.log(''单击事件'');
            // 显示窗口
            winShowFocus();
        }
    }
}
/**
 * 窗口置顶显示
 */
async function winShowFocus() {
    // 获取窗体实例
    const win = getCurrentWindow();
    // 检查窗口是否见,如果不可见则显示出来
    if(!(await win.isVisible())) {
        win.show();
    }else {
        // 检查是否处于最小化状态,如果处于最小化状态则解除最小化
        if(await win.isMinimized()) {
            await win.unminimize();
        }
        // 窗口置顶
        await win.setFocus();
    }
}

/**
 * 创建系统托盘
 */
export async function createTray() {
    const tray = await TrayIcon.new(options);
    console.log(tray)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

四. Javascript 前端添加托盘菜单 Menu

创建系统托盘菜单:

import { Menu } from ''@tauri-apps/api/menu'';
/**
 * 创建托盘菜单
 */
async function createMenu() {
    return await Menu.new({
        // items 的显示顺序是倒过来的
        items: [
            {
                id: ''show'',
                text: ''显示窗口'',
                action: () => {
                    winShowFocus();
                }
            },
            {
                // 菜单 id
                id: ''quit'',
                // 菜单文本
                text: ''退出'',
                // 菜单事件处理程序
                action: () => {
                	 // 退出应用还需要引入别的依赖
                    console.log(''退出应用'');
                }
            }
        ]
    })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

将托盘菜单添加进系统托盘,修改创建系统托盘的 createTray 函数:

/**
 * 创建系统托盘
 */
export async function createTray() {
    // 获取 menu
    options.menu = await createMenu();
    const tray = await TrayIcon.new(options);
    console.log(tray)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

New Image

五. JavaScript 退出应用

在窗体化应用中,通常关闭最后一个窗口,就可以退出整个应用:

getCurrentWindow().close();

但是这样可能并不一定保险,我在 getCurrentWindow 并没有发现类似于 app.exit 或者 app.quit 的函数。所以我们又得引入新的东西了:进程 process https://tauri.app/zh-cn/plugin/process/

  1. 首先使用项目的包管理器来添加依赖:

    npm run tauri add process

  2. 引入 api

    import { exit, relaunch } from ‘@tauri-apps/plugin-process’;

在进程管理中,只有两个东西,一个是 exit 退出应用函数,一个是 relaunch 重启应用函数,没了 ಠ_ಠ

用法也没啥好说的,直接调用就好:

// 退出应用
await exit(0);
// 重启应用
await relaunch();

六. JavaScript 创建系统托盘最终代码:

createTray 函数是对外导出的,外部调用跑就行,不会真的有人没调这个方法吧?(⓿_⓿)

// 导入系统托盘
import { TrayIcon } from ''@tauri-apps/api/tray'';
// 获取当前窗口
import { getCurrentWindow } from ''@tauri-apps/api/window'';
// 托盘菜单
import { Menu } from ''@tauri-apps/api/menu'';
// 进程管理
import { exit } from ''@tauri-apps/plugin-process'';

/**
 * 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
 */
const options = {
    // icon 的相对路径基于:项目根目录/src-tauri/,其他 tauri api 相对路径大抵都是这个套路
    icon : "icons/32x32.png",
    // 托盘提示,悬浮在托盘图标上可以显示 tauri-app
    tooltip: ''tauri-app'',
    // 是否在左键点击时显示托盘菜单,默认为 true。当然不能为 true 啦,程序进入后台不得左键点击图标显示窗口啊。
    menuOnLeftClick: false,
    // 托盘图标上事件的处理程序。
    action: (event) => {
        // 左键点击事件
        if(event.type === ''Click'' && event.button === "Left" && event.buttonState === ''Down'') {
            console.log(''单击事件'');
            // 显示窗口
            winShowFocus();
        }
    }
}

/**
 * 窗口置顶显示
 */
async function winShowFocus() {
    // 获取窗体实例
    const win = getCurrentWindow();
    // 检查窗口是否见,如果不可见则显示出来
    if(!(await win.isVisible())) {
        win.show();
    }else {
        // 检查是否处于最小化状态,如果处于最小化状态则解除最小化
        if(await win.isMinimized()) {
            await win.unminimize();
        }
        // 窗口置顶
        await win.setFocus();
    }
}

/**
 * 创建托盘菜单
 */
async function createMenu() {
    return await Menu.new({
        // items 的显示顺序是倒过来的
        items: [
            {
                id: ''show'',
                text: ''显示窗口'',
                action: () => {
                    winShowFocus();
                }
            },
            {
                // 菜单 id
                id: ''quit'',
                // 菜单文本
                text: ''退出'',
                //  菜单项点击事件
                action: () => {
                    // 退出应用
                    exit(0);
                }
            }
        ]
    })
}

/**
 * 创建系统托盘
 */
export async function createTray() {
    // 获取 menu
    options.menu = await createMenu();
    await TrayIcon.new(options);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

二、在 Rust 创建系统托盘菜单:

其实吧,我觉得 JavaScript api 创建托盘菜单已经足够了,但是由于我 rust 学的一坨屎 (啥也不会!),所以在这里我不会说太多,直接将操作甩这就完了。

1. 在 src-tauri/Cargo.toml 中写入包含系统托盘的必要功能 tray-icon

2. 使用 html+css+Javascript 替换掉系统默认的拖拽栏,详情请参考:tauri 2.0创建项目 https://blog.csdn.net/xiaoyuanbaobao/article/details/143751093

3.Rust 创建系统托盘:

修改 src-tauri/lib.rs 文件,在文件中添加如下内容:

// 导入系统托盘所需的依赖, 导入的全都是
use tauri::{
    tray::{
        TrayIconBuilder,
        MouseButtonState,
        MouseButton,
        TrayIconEvent
    },
    menu::{
        Menu,
        MenuItem
    },
    Manager
};

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You''ve been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_process::init())
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        // setup 部分是系统托盘
        .setup(|app| {
            let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
            let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&show_i, &quit_i])?;
            // 创建系统托盘
            let _tray = TrayIconBuilder::new()
                // 添加托盘图标
                .icon(app.default_window_icon().unwrap().clone())
                // 添加菜单
                .menu(&menu)
                // 禁用鼠标左键点击图标显示托盘菜单
                .menu_on_left_click(false)
                // 监听托盘图标发出的鼠标事件
                .on_tray_icon_event(|tray, event| match event {
                    // 左键点击托盘图标显示窗口
                    TrayIconEvent::Click {
                        id: _,
                        position: _,
                        rect: _,
                        button: MouseButton::Left,
                        button_state: MouseButtonState::Up,
                    } => {
                        let win = tray
                            .app_handle()
                            .get_webview_window("main")
                            .expect("REASON");
                        match win.is_visible() {
                            Ok(visible) if !visible => {
                                win.show().unwrap();
                            }
                            Err(e) => eprintln!("{}", e),
                            _ => (),
                        };
                        // 获取窗口焦点
                        win.set_focus().unwrap();
                    }
                    _ => {}
                })
                // 监听菜单事件
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "show" => {
                        let win = app.get_webview_window("main").unwrap();
                        match win.is_visible() {
                            Ok(visible) if !visible => {
                                win.show().unwrap();
                            }
                            Err(e) => eprintln!("{}", e),
                            _ => (),
                        };
                        // 获取窗口焦点
                        win.set_focus().unwrap();
                    }
                    "quit" => {
                        app.exit(0);
                    }
                    _ => {}
                })
                .build(app)?;
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

在这一大串代码中,.setup 部分是系统托盘部分的代码,注意别忘了粘贴的位置,以及导入依赖。

三、结束

tauri 2.0 创建系统托盘的教程到这里就结束了,让我想想接下来要在官方文档里薅哪一步分下来写 ヽ( ̄ω ̄( ̄ω ̄〃)ゝ