当前位置: 首页 > article >正文

使用Tauri + Leptos开发带系统托盘桌面应用

初学Rust编程,尝试使用Tauri + Leptos开发带系统托盘的桌面小程序。Win10操作系统,使用VS Code及rust analyzer插件搭建的开发环境。

1. 安装Tauri

cargo install create-tauri-app
cargo create-tauri-app

在创建项目过程中,选择Leptos,程序名自己设定,此处为mynewapp。Cargo会自动下载相关依赖包,过程提示还需要如下操作。

cargo install tauri-cli
cargo install trunk
rustup target add wasm32-unknown-unknown

项目创建好后,进入项目目录,运行如下命令之一,均可编译程序。

cargo tauri dev
cargo tauri build

cargo tauri dev会直接启动编译好的程序,cargo tauri build编译后需手动运行或安装msi和exe文件。

2. 在程序界面上添加一个加减按钮并显示计算结果

程序界面如下 ,当结果小于0时字体变成红色:

mynewapp/Cargo.toml文件内容如下:

[package]
name = "mynewapp-ui"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { version = "0.6", features = ["csr"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1.7"
console_log = "1.0.0"
log = "0.4.22"

[workspace]
members = ["src-tauri"]

 mynewapp\src-tauri\tauri.conf.json文件内容如下:

{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "mynewapp",
  "version": "0.1.0",
  "identifier": "com.mynewapp.app",
  "build": {
    "beforeDevCommand": "trunk serve",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "trunk build",
    "frontendDist": "../dist"
  },
  "app": {
    "withGlobalTauri": true,
    "windows": [
      {
        "title": "Tauri+Leptos+Scybd",
        "width": 800,
        "height": 600
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

在main.rs文件中,增加了加减的初始值为8:

mod app;    //声明模块app,首先查找app.rs,然后查找app/mod.rs,mod.rs中是需要引入的模块代码,通过pub mod暴露目录下的其它mod文件

use app::*;     //引入模块中的函数
use leptos::*;

fn main() {
    console_error_panic_hook::set_once();   //初始化函数
    _ = console_log::init_with_level(log::Level::Debug);    //在 Web 浏览器控制台上正确初始化任何错误的日志记录
    mount_to_body(|| {                      //mount_to_body是一个leptos函数,将类型挂载到页面的body正文中
        view! {                             //view!宏接受类似 HTML 的语法
            <App initial_value=8 />         //app.rs模块中,App()函数通过view!宏返回一个IntoView类型。
        }
    })
}

main.rs主要是调用了app.rs模块,内容如下(注解是网络搜索结果和个人理解,不一定正确):

use leptos::leptos_dom::ev::SubmitEvent;
use leptos::*;
use serde::{Deserialize, Serialize};    //#derive(serialize, Deserialize)用于请求响应传递参数的序列化
use wasm_bindgen::prelude::*;

//‌WASM(WebAssembly)是一种低级字节码格式,可以从C、C++、Rust等高级语言编译而来,旨在在Web端实现接近原生的执行效率‌‌
//wasm 并不是传统意义上汇编语言(Assembly),而是一种中间编译的字节码,可以在浏览器上运行非 JavaScript 编写的代码
//wasm-bindgen主要目的是实现Rust与现有JavaScript环境的无缝集成‌,自动生成必要的绑定和胶水代码,确保Rust函数和JavaScript之间平滑通信
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

//derive是一个编译器指令,在类型定义(如结构体或枚举)上添加#[derive(...)]让编译器为一些特性提供基本的实现,
//...表示要为其提供基本实现的特性列表。#[derive(Serialize, Deserialize)],可以轻松地为结构体实现序列化和反序列化功能。
//要在Rust中使用序列化和反序列化,首先需要在Cargo.toml文件中引入serde库
//serde = { version = "1.0", features = ["derive"] }
//serde_json = "1.0"
//序列化后的变量作为函数invoke(cmd, args: JsValue)的参数,JsValue为序列化格式
#[derive(Serialize, Deserialize)]
struct GreetArgs<'a> {      //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。
    name: &'a str,          //name序列化,tauri后台使用类似 JSON-RPC 的协议来序列化请求和响应(Invoke command <-> serialize return),因此所有参数和返回数据都必须可序列化为 JSON。
}

//Component是一个重要的概念,它通常指的是项目中的一部分代码或功能模块。
#[component]
pub fn App(initial_value:i32) -> impl IntoView {    //函数返回IntoView类型,函数名App()也是view!宏中的组件名(component name)。
    let (name, set_name) = create_signal(String::new());    //signal是反应式类型,可在WASM应用程序声明周期内发生变化,初始化类型作为参数,返回getter(name)和setter(set_name)
    let (greet_msg, set_greet_msg) = create_signal(String::new());
    let (value, set_value) = create_signal(initial_value);

    let update_name = move |ev| {       //将闭包传递给按钮组件的onclick参数,更新之前创建signal中的值。
        let v = event_target_value(&ev);
        set_name.set(v);
    };

    let greet = move |ev: SubmitEvent| {
        ev.prevent_default();           //类似javascript中的Event.preventDefault(),处理<input>字段非常有用
        spawn_local(async move {                //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。
            let name = name.get_untracked();    //防止通常由Leptos signal产生的反应式绑定,因此即使值发生变化,Leptos也不会尝试更新闭包。
            if name.is_empty() {
                return;
            }

            let args = serde_wasm_bindgen::to_value(&GreetArgs { name: &name }).unwrap();   //参数序列化
            // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
            let new_msg = invoke("greet", args).await.as_string().unwrap();     //使用invoke调用greet命令,greet类似于API
            set_greet_msg.set(new_msg);
        });
    };

    let clear = move |_| {set_value.set(0);};
    let decrement = move |_| {set_value.update(|v| *v -=1);};       //update()函数接受闭包,参数是signal的setter值set_value
    let increment = move |_| {set_value.update(|v| *v +=1);};

    view! {                                     //view!宏作为App()函数的返回值返回IntoView类型
        <main class="container">
            <h1>"欢迎来到 Tauri + Leptos"</h1>

            <div class="row">
                <a href="https://tauri.app" target="_blank">
                    <img src="public/tauri.svg" class="logo tauri" alt="Tauri logo"/>
                </a>
                <a href="https://docs.rs/leptos/" target="_blank">
                    <img src="public/leptos.svg" class="logo leptos" alt="Leptos logo"/>
                </a>
                <a href="https://scybbd.com" target="_blank">
                    <img src="public/doughnutS.svg" class="logo scybbd" alt="YislWll's website"/>
                </a>
            </div>
            <p>"点击Tauri、Leptos和Scybbd的logo了解更多..."</p>

            <form class="row" id="greet-form" on:submit=greet>
                <input
                    id="greet-input"
                    placeholder="请输入一个名字..."
                    on:input=update_name
                />
                <button type="submit" id="greet-button">"打招呼"</button>
            </form>
            <p>{ move || greet_msg.get() }</p>

            <div>
                <button style="margin:0 10px 0 10px;" on:click = clear>"清零"</button>
                <button style="margin:0 10px 0 10px;" on:click = decrement>"-1"</button>
                <span style="margin:0 10px 0 10px;" class:red=move||{value.get()<0}>"数值:"{value}</span>     //当值小于0时,字体变成红色
                <button style="margin:0 10px 0 10px;" on:click = increment>"+1"</button>
            </div>
        </main>
    }
}

 最后还需在style.css中增加一个.red类属性

3. 添加系统托盘

主要参考了tauri版本2的系统托盘_tauri 系统托盘-CSDN博客,最终效果如下图所示,在托盘菜单中,通过在webview中执行JavaScript代码实现了对界面中的元素进行操作。

src-tauri/Cargo.toml文件内容如下:

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

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "mynewapp_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

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

[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
#for win7
#tauri-plugin-notification = { version = "2", features = [ "windows7-compat" ] }

系统托盘的主要内容及功能是在src-tauri/src/tray.rs实现,并作为模块被src-tauri/src/lib.rs调用。

tray.rs的文件内容如下:

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

pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
    let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
    let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
    let hide_i = MenuItem::with_id(app, "hide", "隐藏", true, None::<&str>)?;
    let china_i = MenuItem::with_id(app, "china", "中华人民共和国", true, None::<&str>)?;
    let people_i = MenuItem::with_id(app, "people", "伟大的中国人民", true, None::<&str>)?;
    let greet_i=Submenu::with_id_and_items(app, "greetone", "问候", true, &[&china_i, &people_i])?;
    // 分割线
    let menu = Menu::with_items(app, &[&quit_i, &show_i, &hide_i, &greet_i])?;

    let _ = TrayIconBuilder::with_id("tray")
        .icon(app.default_window_icon().unwrap().clone())
        .menu(&menu)
        .menu_on_left_click(false)
        .on_menu_event(move |app, event| match event.id.as_ref() {
            "quit" => {
                app.exit(0);
            },
            "show" => {
                let window = app.get_webview_window("main").unwrap();
                let _ = window.show();
            },
            "hide" => {
                let window = app.get_webview_window("main").unwrap();
                let _ = window.hide();
            },
            "china" => {
                let window = app.get_webview_window("main").unwrap();
                // 在webview中执行JavaScript代码来设置input元素的值
                let _ = window.show();
                let script = format!(
                    "
                    var input = document.getElementById('greet-input');\
                    input.value = '{}';\
                    var event = new Event('input', {{bubbles:true}});\
                    input.dispatchEvent(event);\
                    document.getElementById('greet-button').click();\
                    ",
                    String::from("中国人民共和国")
                );
                window.eval(&script).unwrap();
            },
            "people" => {
                let window = app.get_webview_window("main").unwrap();                             
                // 在webview中执行JavaScript代码来设置input元素的值
                let _ = window.show();
                let script = format!(
                    "
                    var input = document.getElementById('greet-input');\
                    input.value = '{}';\
                    var event = new Event('input', {{bubbles:true}});\
                    input.dispatchEvent(event);\
                    document.getElementById('greet-button').click();\
                    ",
                    String::from("伟大的中国人民")
                );
                window.eval(&script).unwrap();
            },
            // Add more events here
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                let app = tray.app_handle();
                if let Some(window) = app.get_webview_window("main") {
                    let _ = window.show();
                    let _ = window.set_focus();
                }
            }
        })
        .build(app);

    Ok(())
}

lib.rs文件内容如下:

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]               //创建tauri::command命令
fn greet(name: &str) -> String {
    format!("你好, {}! Rust向你打招呼了!", name)
}

mod tray;       //导入tray.rs模块

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet])    //定义invoke handler,在app.rs中通过invoke("greet", args)调用
        .setup(|app| {
            #[cfg(all(desktop))]
            {
            let handle = app.handle();
            tray::create_tray(handle)?;         //设置app系统托盘
            }
            Ok(())
            })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

最后编译发布程序:

\mynewapp> cargo tauri build
    Running beforeBuildCommand `trunk build`
2024-12-24T17:19:46.140004Z  INFO Starting trunk 0.21.5
2024-12-24T17:19:46.140667Z  INFO starting build
   Compiling mynewapp-ui v0.1.0 (E:\Rust_Program\tauri-app\mynewapp)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.61s
2024-12-24T17:19:49.347966Z  INFO applying new distribution
2024-12-24T17:19:49.389738Z  INFO success
   Compiling mynewapp v0.1.0 (E:\Rust_Program\tauri-app\mynewapp\src-tauri)
    Finished `release` profile [optimized] target(s) in 27.91s
    Built application at: E:\Rust_Program\tauri-app\mynewapp\target\release\mynewapp.exe
    Info Target: x64
    Running candle for "main.wxs"
    Running light to produce E:\Rust_Program\tauri-app\mynewapp\target\release\bundle\msi\mynewapp_0.1.0_x64_en-US.msi
    Info Target: x64
    Running makensis.exe to produce E:\Rust_Program\tauri-app\mynewapp\target\release\bundle\nsis\mynewapp_0.1.0_x64-setup.exe
    Finished 2 bundles at:
        E:\Rust_Program\tauri-app\mynewapp\target\release\bundle\msi\mynewapp_0.1.0_x64_en-US.msi
        E:\Rust_Program\tauri-app\mynewapp\target\release\bundle\nsis\mynewapp_0.1.0_x64-setup.exe

 至此,一个由Tauri + Leptos创建的带系统托盘的桌面小程序完成。


http://www.kler.cn/a/452319.html

相关文章:

  • Docker 镜像加速访问方案
  • python中使用selenium执行组合快捷键ctrl+v不生效问题
  • YOLOv10目标检测-训练自己的数据
  • Shion(时间追踪工具) v0.13.2
  • AttributeError: module ‘numpy‘ has no attribute ‘bool‘.
  • 《三角洲行动》游戏运行时提示“缺失kernel32.dll”:问题解析与解决方案
  • Spring Boot 整合 RabbitMQ:从入门到实践
  • Pytorch | 利用AI-FGTM针对CIFAR10上的ResNet分类器进行对抗攻击
  • 准备考试:解决大学入学考试问题
  • springMVC-请求响应
  • 【数学建模】利用Matlab绘图(2)
  • linux 常用 Linux 命令指南
  • Linux大数据方向shell
  • 借助Aspose.html控件, 使用 Java 编程将 HTML 转换为 BMP
  • 基于java出租车计价器设计与实现【源码+文档+部署讲解】
  • ffmpeg之播放一个yuv视频
  • 常见问题解决方案:Keen CommonWeb 开源项目
  • CVPR-2024 | 具身导航模型大一统!NaviLLM:学习迈向具身导航的通用模型
  • Unity中如何修改Sprite的渲染网格
  • NFC 碰一碰发视频源码搭建技术详解,支持OEM
  • 从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
  • 【Trick】解决服务器cuda报错——RuntimeError: cuDNN error: CUDNN_STATUS_NOT_INITIALIZED
  • 前端三大主流框架:React、Vue、Angular
  • 网络管理-期末项目(附源码)
  • PySide6如何实现点击TableWidget列表头在该列右侧显示列表选择框筛选列数据
  • 数据仓库是什么?数据仓库简介