使用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创建的带系统托盘的桌面小程序完成。