Tauri2+Leptos开发桌面应用--绘制图形、制作GIF动画和mp4视频
在之前工作(Tauri2+Leptos开发桌面应用--新建窗口、自定义菜单和多页面切换_tauri实现多窗口-CSDN博客)的基础上继续尝试绘制图形、制作GIF动画和mp4视频
绘制图形主要尝试了两种方式,一种是调用python使用matplotlib模块绘制图形,一种是使用纯Rust开发的图形库Plotters来绘制图形,包括png、svg格式的图片,gif格式的动画。制作mp4视频主要是使用Plotters结合video_rs来完成的。
上述功能都是写成Tauri后台命令,通过前端Leptos调用显示实现的。
一、Leptos前端
为了方便,在主窗口单独新建了一个页面用于绘图练习。
1. main.rs文件
mod app;
use app::*;
use leptos::prelude::*;
//打开trunk serve --open 以开始开发您的应用程序。 Trunk 服务器将在文件更改时重新加载您的应用程序,从而使开发相对无缝。
fn main() {
console_error_panic_hook::set_once(); //浏览器中运行 WASM 代码发生 panic 时可以获得一个实际的 Rust 堆栈跟踪,其中包括 Rust 源代码中的一行。
mount_to_body(|| {
view! {
<App />
}
})
}
main.rs文件中使用了模块app,app.rs文件主要为导航页面。
2. app.rs文件
#[warn(unused_imports)]
use leptos::prelude::*;
use leptos_router::components::{ParentRoute, Route, Router, Routes, Outlet, A};
use leptos_router::path;
use leptos_router::hooks::use_params_map;
mod about;
mod practice;
mod acidinput;
mod images;
use about::*;
use practice::*;
use acidinput::*;
use images::*;
#[component]
pub fn App() -> impl IntoView {
view! {
<Router>
<nav>
<a class="nav" href="/">"产品录入"</a>
<a class="nav" href="/practice">"练习1"</a>
<a class="nav" href="/about">"练习2"</a>
<a class="nav" href="/images">"图形练习"</a>
<a class="nav" href="/embeddedpage">"嵌入HTML"</a>
<a class="nav" href="/contacts">"联系我们"</a>
<a class="nav" href="/embeddedweb">"嵌入网页"</a>
</nav>
<main>
<Routes fallback=|| "Not found.">
// / just has an un-nested "Home"
<Route path=path!("/") view=|| view! {<AcidInput />} />
<Route path=path!("/practice") view=|| view! {<Practice initial_value = 8 />}/>
<Route path=path!("/about") view=|| view! {<About />}/>
<Route path=path!("/images") view=|| view! {<ImagesPage />}/>
<Route path=path!("/embeddedpage") view=EmbeddedPage/>
//<Route path=path!("/embeddedweb") view=EmbeddedWeb/>
<ParentRoute path=path!("/contacts") view=ContactList>
// if no id specified, fall back
<ParentRoute path=path!(":id") view=ContactInfo>
<Route path=path!("") view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path=path!("conversations") view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</ParentRoute>
// if no id specified, fall back
<Route path=path!("") view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</ParentRoute>
</Routes>
</main>
</Router>
}
}
#[component]
fn EmbeddedPage() -> impl IntoView {
view! {
<h1>"嵌入HTML文件"</h1>
<iframe
src="public/about/insert.html"
width="100%"
height="500px"
style="border:none;"
></iframe>
}
}
#[component]
fn EmbeddedWeb() -> impl IntoView {
view! {
<h1>"嵌入网站"</h1>
<iframe
src="https://sina.com.cn"
width="100%"
height="600px"
style="border:none;"
></iframe>
}
}
#[component]
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<h3>"Contacts"</h3>
<div class="contact-list-contacts">
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map();
let id = move || params.read().get("id").unwrap_or_default();
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! {
<h4>{name}</h4>
<div class="contact-info">
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
页面效果如下:
其中/images路径的内容主要通过app/images.rs来实现。
3. app/images.rs文件
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos::ev::SubmitEvent;
//use serde::{Deserialize, Serialize};
//use leptos::ev::Event;
use wasm_bindgen::prelude::*;
//use chrono::{Local, NaiveDateTime};
use leptos::web_sys::{Blob, Url};
use web_sys::BlobPropertyBag;
use js_sys::{Array, Uint8Array};
use base64::engine::general_purpose::STANDARD; // 引入 STANDARD Engine
use base64::Engine; // 引入 Engine trait
use leptos::logging::log;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
async fn invoke_without_args(cmd: &str) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
#[component]
pub fn ImagesPage() -> impl IntoView { //函数返回IntoView类型,即返回view!宏,函数名App()也是主程序view!宏中的组件名(component name)。
let (img_error, set_img_error) = signal(String::new());
let (video_src, set_video_src) = signal(String::new());
let plot_svg_image = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_svg_curve").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀
let base64_data = if image.starts_with("data:image/svg+xml;base64,") {
image.trim_start_matches("data:image/svg+xml;base64,").to_string()
} else {
image
};
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("image/svg+xml");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成图片 URL
let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
//log!("Generated Blob URL: {}", image_url);
// 动态创建 <img> 元素
let img = document().create_element("img").expect("Failed to create img element");
img.set_attribute("src", &image_url).expect("Failed to set src");
img.set_attribute("alt", "Plot").expect("Failed to set alt");
// 设置宽度(例如 300px),高度会自动缩放
img.set_attribute("width", "600").expect("Failed to set width");
// 将 <img> 插入到 DOM 中
let img_div = document().get_element_by_id("img_svg").expect("img_div not found");
// 清空 div 内容(避免重复插入)
img_div.set_inner_html("");
img_div.append_child(&img).expect("Failed to append img");
});
};
let python_plot = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("python_plot").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀data:image/png;base64
let base64_data = if image.starts_with("data:image/png;base64,") {
image.trim_start_matches("data:image/png;base64,").to_string()
} else {
image
};
// 去除多余的换行符和空格
let base64_data = base64_data.trim().to_string();
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("image/png");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成图片 URL
let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
log!("Generated Blob URL: {}", image_url);
// 动态创建 <img> 元素
let img = document().create_element("img").expect("Failed to create img element");
img.set_attribute("src", &image_url).expect("Failed to set src");
img.set_attribute("alt", "Plot").expect("Failed to set alt");
// 设置宽度(例如 300px),高度会自动缩放
img.set_attribute("width", "600").expect("Failed to set width");
// 将 <img> 插入到 DOM 中
let img_div = document().get_element_by_id("img_python").expect("img_div not found");
// 清空 div 内容(避免重复插入)
img_div.set_inner_html("");
img_div.append_child(&img).expect("Failed to append img");
});
};
let plot_gif_image = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_gif_curve").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀
let base64_data = if image.starts_with("data:image/gif;base64,") {
image.trim_start_matches("data:image/gif;base64,").to_string()
} else {
image
};
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("image/gif");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成图片 URL
let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
//log!("Generated Blob URL: {}", image_url);
// 动态创建 <img> 元素
let img = document().create_element("img").expect("Failed to create img element");
img.set_attribute("src", &image_url).expect("Failed to set src");
img.set_attribute("alt", "Plot").expect("Failed to set alt");
// 设置宽度(例如 300px),高度会自动缩放
img.set_attribute("width", "600").expect("Failed to set width");
// 将 <img> 插入到 DOM 中
let img_div = document().get_element_by_id("gif_div").expect("gif_div not found");
// 清空 div 内容(避免重复插入)
img_div.set_inner_html("");
img_div.append_child(&img).expect("Failed to append img");
});
};
let plot_3d_image = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_3d_surface").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀
let base64_data = if image.starts_with("data:image/svg+xml;base64,") {
image.trim_start_matches("data:image/svg+xml;base64,").to_string()
} else {
image
};
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("image/svg+xml");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成图片 URL
let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
//log!("Generated Blob URL: {}", image_url);
// 动态创建 <img> 元素
let img = document().create_element("img").expect("Failed to create img element");
img.set_attribute("src", &image_url).expect("Failed to set src");
img.set_attribute("alt", "Plot").expect("Failed to set alt");
// 设置宽度(例如 300px),高度会自动缩放
img.set_attribute("width", "600").expect("Failed to set width");
// 将 <img> 插入到 DOM 中
let img_div = document().get_element_by_id("3d_div").expect("img_3d not found");
// 清空 div 内容(避免重复插入)
img_div.set_inner_html("");
img_div.append_child(&img).expect("Failed to append img");
});
};
let plot_gif_3d = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_gif_3d").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀
let base64_data = if image.starts_with("data:image/gif;base64,") {
image.trim_start_matches("data:image/gif;base64,").to_string()
} else {
image
};
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("image/gif");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成图片 URL
let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
//log!("Generated Blob URL: {}", image_url);
// 动态创建 <img> 元素
let img = document().create_element("img").expect("Failed to create img element");
img.set_attribute("src", &image_url).expect("Failed to set src");
img.set_attribute("alt", "Plot").expect("Failed to set alt");
// 设置宽度(例如 300px),高度会自动缩放
img.set_attribute("width", "600").expect("Failed to set width");
// 将 <img> 插入到 DOM 中
let img_div = document().get_element_by_id("gif_3d").expect("gif_3d not found");
// 清空 div 内容(避免重复插入)
img_div.set_inner_html("");
img_div.append_child(&img).expect("Failed to append img");
});
};
let plot_mp4_3d = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
// 调用 Tauri 的 invoke 方法获取 base64 图片数据
let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_mp4_3d").await).unwrap();
//log!("Received Base64 data: {}", result);
let mut image = String::new();
if result.len() != 0 {
// 将 base64 数据存储到信号中
image = result;
} else {
set_img_error.set("Failed to generate plot".to_string());
}
// 检查 Base64 数据是否包含前缀
let base64_data = if image.starts_with("data:video/mp4;base64,") {
image.trim_start_matches("data:video/mp4;base64,").to_string()
} else {
image
};
// 将 Base64 字符串解码为二进制数据
let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");
// 将二进制数据转换为 js_sys::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let options = BlobPropertyBag::new();
options.set_type("video/mp4");
let blob = Blob::new_with_u8_array_sequence_and_options(
&Array::of1(&uint8_array),
&options,
)
.expect("Failed to create Blob");
// 生成视频 URL
let video_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");
// 打印生成的 URL,用于调试
//log!("Generated Blob URL: {}", video_url);
set_video_src.set(video_url);
});
};
view! {
<main class="container">
<h1>"Images Page"</h1>
<div>
<p class="red">{move || img_error.get() }</p>
<p>"Generate PNG Image"</p>
<div id="img_div">
<img
src=""
width="600"
/>
</div>
<form id="form_svg" on:submit=plot_svg_image>
<button type="submit">"Generate SVG Image"</button>
<p></p>
<div id="img_svg">
<img
src=""
width="600"
/>
</div>
</form>
<form id="form_python" on:submit=python_plot>
<button type="submit">"Generate By Python"</button>
<p></p>
<div id="img_python">
<img
src=""
width="600"
/>
</div>
</form>
<form id="form_gif" on:submit=plot_gif_image>
<button type="submit">"Generate GIF"</button>
<p></p>
<div id="gif_div">
<img
src=""
width="600"
/>
</div>
</form>
<form id="form_3d" on:submit=plot_3d_image>
<button type="submit">"Generate 3D Surface"</button>
<p></p>
<div id="3d_div">
<img
src=""
width="600"
/>
</div>
</form>
<form id="form_gif_3d" on:submit=plot_gif_3d>
<button type="submit">"Generate 3D GIF"</button>
<p></p>
<div id="gif_3d">
<img
src=""
width="600"
/>
</div>
</form>
<form on:submit=plot_mp4_3d>
<button type="submit">"Generate Video"</button>
<div id="mp4_3d">
<video
src=video_src
controls=true
width="600"
style="margin-top: 20px;"
>
Your browser does not support the video tag.
</video>
</div>
</form>
</div>
</main>
}
}
该文件中定义了所有绘图功能的显示,其中生成png图片是由另外页面按钮调用切换到该页面后显示的,页面效果如下:
4. 绘图效果
(1)绘制SVG格式图片
(2)调用python的Matplotlib绘制图形
python代码被放在src-tauri\resources目录下,文件名plot.py,内容如下:
# -*_ coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib as mpl
import base64
from io import BytesIO
mpl.rcParams.update({
"font.family": "sans-serif",
"font.sans-serif": "SimHei",
"axes.unicode_minus":False
})
n = 101
R = 12
r = 5
u, v=np.meshgrid(np.linspace(0, 2*np.pi, n), np.linspace(0, 2*np.pi, n))
x = (R - r*np.sin(v))*np.sin(u)
y = (R - r*np.sin(v))*np.cos(u)
z = r*np.cos(v)
fig = plt.figure(figsize=(13, 10))
ax3d = plt.axes(projection='3d')
#设置画布透明
fig.patch.set_alpha(0)
#设置绘图区域透明
ax3d.patch.set_alpha(0.0)
#设置边框透明
for spine in ax3d.spines.values():
spine.set_alpha(0)
ax3d.set_xlabel("X")
ax3d.set_ylabel("Y")
ax3d.set_zlabel("Z")
ax3d.set_zlim(-15, 15)
ax3d.set_xlim(-15, 15)
ax3d.set_ylim(-15, 15)
ax3d.zaxis.set_major_locator(LinearLocator(10))
ax3d.zaxis.set_major_formatter(FormatStrFormatter('%.01f'))
plt.tick_params(labelsize=10)
surf = ax3d.plot_surface(x, y, z, cmap=plt.cm.Spectral)
ax3d.set_title("四次元百宝袋", fontsize=96, bbox={'facecolor': 'gold', 'alpha':0.8, 'pad': 10})
ax3d.view_init(30, 45)
plt.tight_layout()
# 将图像保存为 Base64
buf = BytesIO()
plt.savefig(buf, dpi=400, format='png', transparent=True, facecolor = 'none')
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode('utf-8')
# 打印 Base64 字符串(确保没有换行符)
print(f"data:image/png;base64,{image_base64}", end="")
为了便于程序编译定位到plot.py文件,需要修改tauri.conf.json文件的"beforeBuildCommand",添加"resources":。
"build": {
"beforeDevCommand": "trunk serve",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "trunk build && xcopy /E /I src-tauri\\resources src-tauri\\target\\release\\resources",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources":["resources/plot.py"]
}
(3)绘制GIF动画
(4)绘制SVG格式的三维立体图形
然后制作了响应的GIF动画(文件太大,不能上传)
(5)制作mp4视频
mp4视频是通过Plotters结合video_rs来完成的,Windows下需要预装LLVM和FFMpeg。视频效果如下:
Plotters结合video_rs生成mp4视频
二、Tauri后台绘图命令
lib.rs文件内容如下,里面包括一些前期工作写的命令,供前端Leptos调用。
use full_palette::PURPLE;
use futures::TryStreamExt;
use plotters::prelude::*;
use std::path::Path;
use sqlx::{migrate::MigrateDatabase, prelude::FromRow, sqlite::SqlitePoolOptions, Pool, Sqlite};
//use tauri::{App, Manager, WebviewWindowBuilder, Emitter};
use tauri::{menu::{CheckMenuItem, Menu, MenuItem, Submenu}, App, Emitter, Listener, Manager, WebviewWindowBuilder};
use serde::{Deserialize, Serialize};
type Db = Pool<Sqlite>;
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgba, DynamicImage, RgbImage};
use image::codecs::png::PngEncoder; // 引入 PngEncoder
use std::process::Command;
use std::env;
struct DbState {
db: Db,
}
async fn setup_db(app: &App) -> Db {
let mut path = app.path().app_data_dir().expect("获取程序数据文件夹路径失败!");
match std::fs::create_dir_all(path.clone()) {
Ok(_) => {}
Err(err) => {
panic!("创建文件夹错误:{}", err);
}
};
//C:\Users\<user_name>\AppData\Roaming\com.mynewapp.app\db.sqlite
path.push("db.sqlite");
Sqlite::create_database(
format!("sqlite:{}", path.to_str().expect("文件夹路径不能为空!")).as_str(),
)
.await
.expect("创建数据库失败!");
let db = SqlitePoolOptions::new()
.connect(path.to_str().unwrap())
.await
.unwrap();
//创建迁移文件位于./migrations/文件夹下
//cd src-tauri
//sqlx migrate add create_users_table
sqlx::migrate!("./migrations/").run(&db).await.unwrap();
db
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好, {}!Rust向你问候了!", name)
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct User {
id: u16,
username: String,
email: String,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct UserId {
id: u16,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct ProductId {
pdt_id: i64,
}
#[derive(Serialize, Deserialize)]
struct Product {
pdt_name:String,
pdt_si:f64,
pdt_al:f64,
pdt_ca:f64,
pdt_mg:f64,
pdt_fe:f64,
pdt_ti:f64,
pdt_ka:f64,
pdt_na:f64,
pdt_mn:f64,
pdt_date:String,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Pdt {
pdt_id:i64, //sqlx 会将 SQLite 的 INTEGER 类型映射为 i64(64 位有符号整数)
pdt_name:String,
pdt_si:f64,
pdt_al:f64,
pdt_ca:f64,
pdt_mg:f64,
pdt_fe:f64,
pdt_ti:f64,
pdt_ka:f64,
pdt_na:f64,
pdt_mn:f64,
pdt_date:String,
}
#[tauri::command]
async fn get_db_value(state: tauri::State<'_, DbState>, window: tauri::Window) -> Result<String, String> {
let db = &state.db;
let query_result:Vec<User> = sqlx::query_as::<_, User>( //查询数据以特定的格式输出
"SELECT * FROM users"
)
.fetch(db)
.try_collect()
.await.unwrap();
let mut div_content = String::new();
for user in query_result.iter(){
div_content += &format!(r###"<p><input type="checkbox" name="items" value="{}"> ID:{},姓名:{},邮箱:{}</p>"###, user.id, user.id, user.username, user.email);
}
// 获取当前窗口
let current_window = window.get_webview_window("main").unwrap();
let script = &format!("document.getElementById('db-item').innerHTML = '{}';",div_content);
current_window.eval(script).unwrap();
Ok(String::from("数据库读取成功!"))
}
#[tauri::command]
async fn send_db_item(state: tauri::State<'_, DbState>) -> Result<Vec<User>, String> {
let db = &state.db;
let query_result:Vec<User> = sqlx::query_as::<_, User>( //查询数据以特定的格式输出
"SELECT * FROM users"
)
.fetch(db)
.try_collect()
.await.unwrap();
Ok(query_result)
}
#[tauri::command]
async fn send_pdt_db(state: tauri::State<'_, DbState>) -> Result<Vec<Pdt>, String> {
let db = &state.db;
let query_result:Vec<Pdt> = sqlx::query_as::<_, Pdt>( //查询数据以特定的格式输出
"SELECT * FROM products"
)
.fetch(db)
.try_collect()
.await.unwrap();
Ok(query_result)
}
#[tauri::command]
async fn write_pdt_db(state: tauri::State<'_, DbState>, product:Product) -> Result<String, String> {
let db = &state.db;
sqlx::query("INSERT INTO products (pdt_name, pdt_si, pdt_al, pdt_ca, pdt_mg, pdt_fe, pdt_ti, pdt_ka, pdt_na, pdt_mn, pdt_date) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)")
.bind(product.pdt_name)
.bind(product.pdt_si)
.bind(product.pdt_al)
.bind(product.pdt_ca)
.bind(product.pdt_mg)
.bind(product.pdt_fe)
.bind(product.pdt_ti)
.bind(product.pdt_ka)
.bind(product.pdt_na)
.bind(product.pdt_mn)
.bind(product.pdt_date)
.execute(db)
.await
.map_err(|e| format!("数据库插入项目错误: {}", e))?;
Ok(String::from("插入数据成功!"))
}
//use tauri::Error;
#[tauri::command]
async fn insert_db_item(state: tauri::State<'_, DbState>, username: &str, email: Option<&str>) -> Result<String, String> {
let db = &state.db;
email.unwrap_or("not set yet"); //email类型为Option<&str>,其结果为None或者Some(&str)
sqlx::query("INSERT INTO users (username, email) VALUES (?1, ?2)")
.bind(username)
.bind(email)
.execute(db)
.await
.map_err(|e| format!("数据库插入项目错误: {}", e))?;
Ok(String::from("插入数据成功!"))
}
#[tauri::command]
async fn update_user(state: tauri::State<'_, DbState>, user: User) -> Result<(), String> {
let db = &state.db;
sqlx::query("UPDATE users SET username = ?1, email = ?2 WHERE id = ?3")
.bind(user.username)
.bind(user.email)
.bind(user.id)
.execute(db)
.await
.map_err(|e| format!("不能更新user:{}", e))?;
Ok(())
}
#[tauri::command]
async fn del_last_user(state: tauri::State<'_, DbState>) -> Result<String, String> {
let db = &state.db;
let last_id:UserId = sqlx::query_as::<_,UserId>("SELECT id FROM users ORDER BY id DESC LIMIT 1")
.fetch_one(db)
.await
.unwrap();
sqlx::query("DELETE FROM users WHERE id = ?1")
.bind(last_id.id)
.execute(db)
.await
.map_err(|e| format!("could not delete last user: {}", e))?;
Ok(String::from("最后一条数据删除成功!"))
}
#[tauri::command]
async fn del_last_pdt(state: tauri::State<'_, DbState>) -> Result<String, String> {
let db = &state.db;
let last_id:ProductId = sqlx::query_as::<_,ProductId>("SELECT pdt_id FROM products ORDER BY pdt_id DESC LIMIT 1")
.fetch_one(db)
.await
.unwrap();
sqlx::query("DELETE FROM products WHERE pdt_id = ?1")
.bind(last_id.pdt_id)
.execute(db)
.await
.map_err(|e| format!("could not delete last product: {}", e))?;
Ok(String::from("最后一条数据删除成功!"))
}
#[tauri::command]
async fn open_new_window(app: tauri::AppHandle, title:String, url:String) -> Result<(), String>{
let main_window = app.get_webview_window("main").unwrap();
let toggle = MenuItem::with_id(&app, "quit", "退出", true, None::<&str>).unwrap();
let submenu_1 = CheckMenuItem::with_id(&app, "check_me", "显示窗口",true, true, None::<&str>).unwrap();
let submenu_2 =CheckMenuItem::with_id(&app, "check_you", "隐藏窗口",true, false, None::<&str>).unwrap();
let check_menu = Submenu::with_id_and_items(&app, "check_one", "选择", true, &[&submenu_1, &submenu_2]).unwrap();
let menu = Menu::with_items(&app, &[&toggle, &check_menu]).unwrap();
let window = WebviewWindowBuilder::new(
&app,
"about",
tauri::WebviewUrl::App(url.into()))
.parent(&main_window).unwrap()
.always_on_top(false)
.title(&title)
.inner_size(800.0, 600.0)
.center()
.menu(menu)
.build()
.unwrap();
window.on_menu_event(move|app, event| {
match event.id.as_ref() {
"quit" => {
let _ = app.close();
},
"check_you" => {
let _ = app.hide();
},
"check_me" => {
let _ = app.show();
},
_ => {}
}
});
// 动态注入内容
//window
// .eval("document.getElementById('insert_div').innerHTML = '</br><h1>Hello from dynamically injected content!</h1>';")
// .expect("Failed to inject content");
window.show().unwrap();
// 监听主窗口发送的事件
window.clone().listen("update-content", move |event| {
let script = &format!("document.getElementById('insert_div').innerHTML = '{}';",event.payload());
window.eval(script).unwrap();
});
Ok(())
}
#[tauri::command]
async fn close_window(app: tauri::AppHandle) -> Result<(), String>{
if let Some(window) = app.get_webview_window("about"){
window.close().unwrap();
}
Ok(())
}
#[tauri::command]
async fn show_window(app: tauri::AppHandle) -> Result<(), String>{
if let Some(window) = app.get_webview_window("about"){
window.show().unwrap();
}
Ok(())
}
#[tauri::command]
async fn close_main_window(app: tauri::AppHandle) -> Result<(), String>{
if let Some(window) = app.get_webview_window("main"){
window.close().unwrap();
}
Ok(())
}
#[tauri::command]
async fn insert_div(app: tauri::AppHandle, label:String, content:String) -> Result<(), String>{
if let Some(window) = app.get_webview_window(&label){
window
.eval(&format!("document.getElementById('insert_div').innerHTML = '{}';",content))
.expect("Failed to inject content");
}
Ok(())
}
//从主窗口向其它窗口发送event
#[tauri::command]
async fn emit_event(app: tauri::AppHandle, label:String, content:String) -> Result<(), String>{
if let Some(window) = app.get_webview_window(&label){
window
.emit_to(&label, "update-content", content) //.emit_to(target, event, content)
.unwrap();
}
Ok(())
}
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
// 生成图表并返回 Base64 编码的 PNG 图片
#[tauri::command]
async fn generate_plot() -> Result<String, String> {
// 创建一个缓冲区,大小为 800x600 的 RGBA 图像
let mut buffer = vec![0; 800 * 600 * 3]; // 800x600 图像,每个像素 3 字节(RGB)
{
// 使用缓冲区创建 BitMapBackend
let root = BitMapBackend::with_buffer(&mut buffer, (800, 600)).into_drawing_area();
root.fill(&WHITE).map_err(|e| e.to_string())?;
// 定义绘图区域
let mut chart = ChartBuilder::on(&root)
.caption("Sine Curve", ("sans-serif", 50).into_font())
.build_cartesian_2d(-10.0..10.0, -1.5..1.5) // X 轴范围:-10 到 10,Y 轴范围:-1.5 到 1.5
.map_err(|e| e.to_string())?;
// 绘制正弦曲线
chart
.draw_series(LineSeries::new(
(-100..=100).map(|x| {
let x_val = x as f64 * 0.1; // 将 x 转换为浮点数
(x_val, x_val.sin()) // 计算正弦值
}),
&RED, // 使用红色绘制曲线
))
.map_err(|e| e.to_string())?;
// 将图表写入缓冲区
root.present().map_err(|e| e.to_string())?;
} // 这里 `root` 离开作用域,释放对 `buffer` 的可变借用
// 将 RGB 数据转换为 RGBA 数据(添加 Alpha 通道)
let mut rgba_buffer = Vec::with_capacity(800 * 600 * 4);
for pixel in buffer.chunks(3) {
// 判断是否为背景色(RGB 值为 (255, 255, 255))
let is_background = pixel[0] == 255 && pixel[1] == 255 && pixel[2] == 255;
// 设置 Alpha 通道的值
let alpha = if is_background {
0 // 背景部分完全透明
} else {
255 // 其他部分完全不透明
};
rgba_buffer.extend_from_slice(&[pixel[0], pixel[1], pixel[2], alpha]); // 添加 Alpha 通道
}
// 将缓冲区的 RGBA 数据转换为 PNG 格式
let image_buffer: ImageBuffer<Rgba<u8>, _> =
ImageBuffer::from_raw(800, 600, rgba_buffer).ok_or("Failed to create image buffer")?;
// 直接保存图片,检查是否乱码
//image_buffer.save("output.png").map_err(|e| e.to_string())?;
// 将 PNG 数据编码为 Base64
let mut png_data = Vec::new();
let encoder = PngEncoder::new(&mut png_data);
encoder
.write_image(
&image_buffer.to_vec(),
800,
600,
ExtendedColorType::Rgba8,
)
.map_err(|e| e.to_string())?;
// 将图片数据转换为 Base64 编码的字符串
let base64_data = STANDARD.encode(&png_data);
//use std::fs::File;
//use std::io::Write;
// 创建或打开文件
//let file_path = "output.txt"; // 输出文件路径
//let mut file = File::create(file_path).unwrap();
// 将 base64_data 写入文件
//file.write_all(base64_data.as_bytes()).unwrap();
// 返回 Base64 编码的图片数据
Ok(format!("data:image/png;base64,{}", base64_data))
}
// 绘制正弦曲线并返回 Base64 编码的 SVG 数据
#[tauri::command]
fn generate_svg_curve() -> Result<String, String> {
// 创建一个字符串缓冲区,用于存储 SVG 数据
let mut buffer = String::new();
{
// 使用缓冲区创建 SVGBackend
let root = SVGBackend::with_string(&mut buffer, (800, 800)).into_drawing_area();
// 设置背景为透明
root.fill(&TRANSPARENT).map_err(|e| e.to_string()).unwrap();
// 创建字体描述,设置粗体和斜体
//let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体
// 将 FontDesc 转换为 TextStyle
let yhfont = font
.into_text_style(&root) //.transform(FontTransform::Rotate180)
.color(&RED);
// 定义绘图区域
let mut chart = ChartBuilder::on(&root)
.caption("一元二次方程", yhfont) //.caption("正弦曲线", ("微软雅黑", 50).into_font())
.margin(10)
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(-4f32..4f32, 0f32..20f32) // X 轴范围:-10 到 10,Y 轴范围:-1到 10
.map_err(|e| e.to_string())?;
//chart.configure_mesh().draw().map_err(|e| e.to_string())?;
// 设置标签的字体大小
chart
.configure_mesh()
.x_labels(5) //X 轴主要标签数量
.y_labels(5)
.x_label_style(("sans-serif", 20).into_font())
.y_label_style(("sans-serif", 20).into_font())
.x_label_formatter(&|x| format!("{:.1}", x)) // 格式化 X 轴标签
.y_label_formatter(&|y| format!("{:.1}", y)) // 格式化 Y 轴标签
//.disable_y_mesh() // 隐藏 Y 轴网格线,.disable_x_mesh() // 隐藏 X 轴网格线
.light_line_style(&BLACK.mix(0.1)) // 设置次要网格线样式
.bold_line_style(&BLACK.mix(0.3)) // 设置主要网格线样式
.axis_style(&BLACK) // 设置坐标轴本身的样式
.draw()
.map_err(|e| e.to_string())?;
// 绘制二次方程
chart
.draw_series(DashedLineSeries::new(
(-200..=200).map(|x| x as f32/50.0).map(|x|(x,x*x)),
10, /* size = length of dash */
5, /* size = spacing of dash */
ShapeStyle {
color: RED.to_rgba(),
filled: false,
stroke_width: 3,
},
))
.map_err(|e| e.to_string())?
.label("一元二次方程")
.legend(|(x, y)| PathElement::new(vec![(x-20, y), (x-5, y)], &RED));
//.legend(|(x,y)| Rectangle::new([(x - 20, y + 1), (x, y)], BLACK));
chart
.configure_series_labels()
.position(SeriesLabelPosition::UpperRight)
.margin(25)
.legend_area_size(10)
.border_style(&BLUE)
.background_style(&PURPLE.mix(0.8))
.label_font(("微软雅黑", 20))
.draw()
.map_err(|e| e.to_string())?;
// 将图表写入缓冲区
root.present().map_err(|e| e.to_string())?;
}
// 将 SVG 数据转换为 Base64 编码的字符串
let base64_data = STANDARD.encode(&buffer);
// 返回 Base64 编码的 SVG 数据
Ok(format!("data:image/svg+xml;base64,{}", base64_data))
}
fn pdf(x: f64, y: f64) -> f64 {
const SDX: f64 = 0.1;
const SDY: f64 = 0.1;
const A: f64 = 5.0;
let x = x / 10.0;
let y = y / 10.0;
A * (-x * x / 2.0 / SDX / SDX - y * y / 2.0 / SDY / SDY).exp()
}
// 绘制正弦曲线并返回 Base64 编码的 SVG 数据
use uuid::Uuid;
use tauri::path::BaseDirectory;
use std::fs::read;
#[tauri::command]
fn generate_gif_curve(app: tauri::AppHandle) -> Result<String, String> {
// Generate a unique file name using UUID
let file_name = format!("{}.gif", Uuid::new_v4());
// Get the app data directory
let app_data_dir = app
.path()
.resolve(&file_name, BaseDirectory::AppData)
.expect("Failed to resolve app data directory");
// Create the directory if it doesn't exist
if let Some(parent) = app_data_dir.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| e.to_string())?;
}
{
// 使用缓冲区创建 gifBackend
let root = BitMapBackend::gif(&app_data_dir, (600, 400), 100).unwrap().into_drawing_area();
// for pitch in 0..157 {
for pitch in 0..50 {
root.fill(&WHITE)
.map_err(|e| e.to_string())?;
let mut chart = ChartBuilder::on(&root)
.caption("2D Gaussian PDF", ("sans-serif", 20))
.build_cartesian_3d(-3.0..3.0, 0.0..6.0, -3.0..3.0)
.map_err(|e| e.to_string())?;
chart.with_projection(|mut p| {
p.pitch = 1.57 - (1.57 - pitch as f64 / 50.0).abs();
p.scale = 0.7;
p.into_matrix() // build the projection matrix
});
chart
.configure_axes()
.light_grid_style(BLACK.mix(0.15))
.max_light_lines(3)
.draw()
.map_err(|e| e.to_string())?;
chart.draw_series(
SurfaceSeries::xoz(
(-15..=15).map(|x| x as f64 / 5.0),
(-15..=15).map(|x| x as f64 / 5.0),
pdf,
)
.style_func(&|&v| (VulcanoHSL::get_color(v / 5.0)).into()),
)
.map_err(|e| e.to_string())?;
root.present()
.map_err(|e| e.to_string())?;
}
// 将图表写入缓冲区
root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
}
//println!("Gif file has been saved to {:?}", app_data_dir);
// 读取文件内容
let file_data = read(app_data_dir).map_err(|e| e.to_string())?;
// 将文件内容转换为 Base64
let base64_data = STANDARD.encode(&file_data);
// 返回 Base64 编码的图片数据
Ok(format!("data:image/gif;base64,{}", base64_data))
}
#[tauri::command]
fn python_plot(app: tauri::AppHandle) -> Result<String, String> {
let resource_path = app.path().resolve("resources/plot.py", BaseDirectory::Resource) // 解析资源文件路径
.expect("Failed to resolve resource");
// 调用 Python 脚本
let output = Command::new("E:/python_envs/eric7/python.exe")
.arg(resource_path) // Python 脚本路径
.output()
.map_err(|e| e.to_string())?;
// 调用打包后的 Python 可执行文件
/*
let output = Command::new("E:/Rust_Program/tauri-app/acid-index/src-tauri/dist/plot.exe")
.output()
.map_err(|e| e.to_string())?;
*/
// 检查 Python 脚本是否成功运行
if output.status.success() {
// 获取 Python 脚本的输出(Base64 图像数据)
let image_data = String::from_utf8(output.stdout).map_err(|e| e.to_string())?;
// 去除多余的换行符
let image_data = image_data.trim().to_string();
Ok(image_data)
} else {
// 获取 Python 脚本的错误输出
let error_message = String::from_utf8(output.stderr).map_err(|e| e.to_string())?;
Err(error_message)
}
}
fn sin_sqrt(x: f64, y: f64) -> f64 {
const SQT: f64 = 3.0;
//let x = x / 10.0;
//let y = y / 10.0;
SQT * ((x * x + y * y ).sqrt()).sin().exp()
}
#[tauri::command]
fn generate_3d_surface() -> Result<String, String> {
// 创建一个字符串缓冲区,用于存储 SVG 数据
let mut buffer = String::new();
{
// 使用缓冲区创建 SVGBackend
let root = SVGBackend::with_string(&mut buffer, (800, 800)).into_drawing_area();
// 设置背景为透明
root.fill(&TRANSPARENT).map_err(|e| e.to_string()).unwrap();
// 创建字体描述,设置粗体和斜体
//let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体
// 将 FontDesc 转换为 TextStyle
let yhfont = font
.into_text_style(&root) //.transform(FontTransform::Rotate180)
.color(&RED);
// 定义绘图区域
let mut chart = ChartBuilder::on(&root)
.caption("三维曲面", yhfont) //.caption("正弦曲线", ("微软雅黑", 50).into_font())
.margin(10)
.build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
.map_err(|e| e.to_string())?;
// 通过闭包配置投影矩阵
chart.with_projection(|mut p| {
p.yaw = 0.5; // 设置偏航角(绕 Y 轴旋转)
p.pitch = 0.5; // 设置俯仰角
p.scale = 0.75; // 设置缩放比例
p.into_matrix() // 返回 ProjectionMatrix
});
chart
.configure_axes()
.light_grid_style(BLACK.mix(0.15))
.max_light_lines(3)
.draw()
.map_err(|e| e.to_string())?;
chart.draw_series(
SurfaceSeries::xoz(
(-30..=30).map(|x| x as f64 / 5.0),
(-30..=30).map(|x| x as f64 / 5.0),
sin_sqrt,
)
.style_func(&|&v| (VulcanoHSL::get_color(v / 8.0)).into()),
)
.map_err(|e| e.to_string())?;
root.present()
.map_err(|e| e.to_string())?;
}
// 将 SVG 数据转换为 Base64 编码的字符串
let base64_data = STANDARD.encode(&buffer);
// 返回 Base64 编码的 SVG 数据
Ok(format!("data:image/svg+xml;base64,{}", base64_data))
}
#[tauri::command]
fn generate_gif_3d(app: tauri::AppHandle) -> Result<String, String> {
// Generate a unique file name using UUID
let file_name = format!("{}.gif", Uuid::new_v4());
// Get the app data directory
let app_data_dir = app
.path()
.resolve(&file_name, BaseDirectory::AppData)
.expect("Failed to resolve app data directory");
// Create the directory if it doesn't exist
if let Some(parent) = app_data_dir.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| e.to_string())?;
}
{
// 使用缓冲区创建 gifBackend
let root = BitMapBackend::gif(&app_data_dir, (600, 620), 25).unwrap().into_drawing_area();
// 创建字体描述,设置粗体和斜体
//let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体
// 将 FontDesc 转换为 TextStyle
let yhfont = font
.into_text_style(&root) //.transform(FontTransform::Rotate180)
.color(&RED);
for yaw in 0..180 {
root.fill(&WHITE.mix(0.3))
.map_err(|e| e.to_string())?;
// 定义绘图区域
let mut chart = ChartBuilder::on(&root)
.caption("三维曲面", yhfont.clone()) //.caption("正弦曲线", ("微软雅黑", 50).into_font())
.margin(0)
.build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
.map_err(|e| e.to_string())?;
// 通过闭包配置投影矩阵
chart.with_projection(|mut p| {
p.yaw = yaw as f64/100.00; // 设置偏航角(绕 Y 轴旋转)
p.pitch = 0.5; // 设置俯仰角
p.scale = 0.75; // 设置缩放比例
p.into_matrix() // 返回 ProjectionMatrix
});
chart
.configure_axes()
.light_grid_style(BLACK.mix(0.15))
.max_light_lines(3)
.draw()
.map_err(|e| e.to_string())?;
chart.draw_series(
SurfaceSeries::xoz(
(-30..=30).map(|x| x as f64 / 5.0),
(-30..=30).map(|x| x as f64 / 5.0),
sin_sqrt,
)
.style_func(&|&v| (VulcanoHSL::get_color(v / (8.0 - (yaw as f64/4.0)%6.0).abs())).into()),
)
.map_err(|e| e.to_string())?;
root.present()
.map_err(|e| e.to_string())?;
}
// 将图表写入缓冲区
root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
}
//println!("Gif file has been saved to {:?}", app_data_dir);
// 读取文件内容
let file_data = read(app_data_dir).map_err(|e| e.to_string())?;
// 将文件内容转换为 Base64
let base64_data = STANDARD.encode(&file_data);
// 返回 Base64 编码的图片数据
Ok(format!("data:image/gif;base64,{}", base64_data))
}
use video_rs::encode::{Encoder, Settings};
use video_rs::time::Time;
use ndarray::Array3;
#[tauri::command]
fn generate_mp4_3d(app: tauri::AppHandle) -> Result<String, String> {
// Generate a unique file name using UUID
let file_name = format!("{}.mp4", Uuid::new_v4());
// Get the app data directory
let app_data_dir = app
.path()
.resolve(&file_name, BaseDirectory::AppData)
.expect("Failed to resolve app data directory");
// Create the directory if it doesn't exist
if let Some(parent) = app_data_dir.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| e.to_string())?;
}
// Initialize video encoder
let settings = Settings::preset_h264_yuv420p(600, 620, false);
let mut encoder = Encoder::new(app_data_dir.as_ref() as &Path, settings).map_err(|e| e.to_string())?;
let frame_rate = 25; // 帧率
let frame_duration_sec = 1.0 / frame_rate as f64; // 每帧的持续时间(秒)
for yaw in 0..180 {
// Create a buffer to hold the frame image
let mut buffer = vec![0; 600 * 620 * 3];
{
let root = BitMapBackend::with_buffer(&mut buffer, (600, 620)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| e.to_string())?;
let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体
// 将 FontDesc 转换为 TextStyle
let yhfont = font
.into_text_style(&root) //.transform(FontTransform::Rotate180)
.color(&RED);
// 定义绘图区域
let mut chart = ChartBuilder::on(&root)
.caption("三维曲面", yhfont.clone()) //.caption("正弦曲线", ("微软雅黑", 50).into_font())
.margin(0)
.build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
.map_err(|e| e.to_string())?;
// 通过闭包配置投影矩阵
chart.with_projection(|mut p| {
p.yaw = yaw as f64/100.00; // 设置偏航角(绕 Y 轴旋转)
p.pitch = 0.5; // 设置俯仰角
p.scale = 0.75; // 设置缩放比例
p.into_matrix() // 返回 ProjectionMatrix
});
chart
.configure_axes()
.light_grid_style(BLACK.mix(0.15))
.max_light_lines(3)
.draw()
.map_err(|e| e.to_string())?;
chart.draw_series(
SurfaceSeries::xoz(
(-30..=30).map(|x| x as f64 / 5.0),
(-30..=30).map(|x| x as f64 / 5.0),
sin_sqrt,
)
.style_func(&|&v| (VulcanoHSL::get_color(v / (8.0 - (yaw as f64/4.0)%6.0).abs())).into()),
)
.map_err(|e| e.to_string())?;
}
// Convert the buffer to an image
let image = RgbImage::from_raw(600, 620, buffer).ok_or("Failed to create image from buffer")?;
let dynamic_image = DynamicImage::ImageRgb8(image);
// Convert DynamicImage to ndarray
let rgb_image = dynamic_image.to_rgb8(); // 转换为 RGB 图像
let array = Array3::from_shape_fn((rgb_image.height() as usize, rgb_image.width() as usize, 3), |(y, x, c)| {
rgb_image.get_pixel(x as u32, y as u32)[c]
});
// Calculate PTS for the current frame
let pts = Time::from_secs_f64(frame_duration_sec * yaw as f64);
// Encode the frame into the video
encoder.encode(&array, pts).map_err(|e| e.to_string())?;
}
// Finish encoding
encoder.finish().map_err(|e| e.to_string())?;
// Read the video file and encode it as Base64
let file_data = std::fs::read(&app_data_dir).map_err(|e| e.to_string())?;
let base64_data = STANDARD.encode(&file_data);
// Return the Base64-encoded video data
Ok(format!("data:video/mp4;base64,{}", base64_data))
}
mod tray; //导入tray.rs模块
mod mymenu; //导入mynemu.rs模块
use mymenu::{create_menu, handle_menu_event};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet,
get_db_value,
insert_db_item,
update_user,
del_last_user,
send_db_item,
open_new_window,
close_window,
close_main_window,
show_window,
insert_div,
emit_event,
write_pdt_db,
send_pdt_db,
del_last_pdt,
generate_plot,
generate_svg_curve,
python_plot,
generate_gif_curve,
generate_3d_surface,
generate_gif_3d,
generate_mp4_3d
])
.menu(|app|{create_menu(app)})
.setup(|app| {
let main_window = app.get_webview_window("main").unwrap();
main_window.on_menu_event(move |window, event| handle_menu_event(window, event));
#[cfg(all(desktop))]
{
let handle = app.handle();
tray::create_tray(handle)?; //设置app系统托盘
}
tauri::async_runtime::block_on(async move {
let db = setup_db(&app).await; //setup_db(&app:&mut App)返回读写的数据库对象
app.manage(DbState { db }); //通过app.manage(DbState{db})把数据库对象传递给state:tauri::State<'_, DbState>
});
Ok(())
})
.run(tauri::generate_context!())
.expect("运行Tauri程序的时候出错!");
}
所有的图片和视频都是加密成base64字符串传递给前端的。
三、其它配置
主要是涉及一些依赖关系,src-tauri/Cargo.toml文件内容如下:
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
tokio = { version ="1", features = ["full"] }
futures = "0.3.31"
log = "0.4.22"
chrono = "0.4.39"
plotters = { version = "0.3.7", features = [] }
base64 = "0.22.1"
image = "0.25.5"
uuid = {version = "1.12.0", features = ["v4"] }
video-rs = { version = "0.10", features = ["ndarray"] }
ndarray = "0.16"
至此,基本上实现了Tauri+Leptos后台图形的绘制及其动画视频的制作,并供前端调用展示。