在之前工作(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>

最低0.47元/天 解锁文章
4万+

被折叠的 条评论
为什么被折叠?



