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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值