Tauri(2.5.1)+Leptos(0.7.8)开发桌面应用---后台调用Python Matplotlib绘制图形

发布于:2025-05-01 ⋅ 阅读:(47) ⋅ 点赞:(0)

Rust语言最接近Python Matplotlib绘图库的应该是Plotters,但是试用下来还是没有Matplotlib效果好,所以尝试在Tauri + Leptos项目中,后台调用Python Matplotlib绘制图形,并返回给前端Leptos展示。

具体效果如下:

1. 前端Leptos

Leptos前端需要从数据库选取用于绘图的产品成分数据,使用信号selected_pdt_data(结构变量数列)实时更新,然后invoke调用Tauri后台命令,将包含产品成分数据的结构变量传递给后台命令。具体代码如下:

use serde::{Deserialize, Serialize};

#[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;
}

#[derive(Serialize, Deserialize, Clone, Debug)]
struct Pdt {
    pdt_id:i64,
    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,
}

#[component]
pub fn AcidInput() -> impl IntoView {
    let (selected_pdt_data, set_selected_pdt_data) = signal::<Vec<Pdt>>(vec![]);

    let python_acid_image = move|ev:SubmitEvent| {
        ev.prevent_default();
        //跳转到images绘图页面,主要由images.rs定义。
        //navigate("/images", Default::default());
        spawn_local(async move {
            // 调用 Tauri 的 invoke 方法获取 base64 图片数据
            let selected_count = selected_pdt_data.get_untracked().len();
            if selected_count == 0 {
                set_vic_plot_error.set(String::from("错误:未选中产品数据,请勾选数据前的复选框!!"));
                return;
            } else if selected_count > 7 {
                set_vic_plot_error.set(String::from("错误: 选中产品数据超过7个,请重新选择!!"));
                return;
            }
            set_vic_plot_error.set(String::new());
            let pdts_data = SelectedPdtData{
                productdata: selected_pdt_data.get_untracked(),
            };
            let args_js = serde_wasm_bindgen::to_value(&pdts_data).unwrap();   //直接序列化数组
            let pdts_curve_js = invoke("python_acid_plot", args_js).await;
            // 处理Tauri命令返回
            if let Some(err) = pdts_curve_js.dyn_ref::<js_sys::Error>() {
                set_vic_plot_error.set(format!("后端错误: {}", err.to_string()));
                return;
            }

            let result = match pdts_curve_js.as_string() {
                Some(s) => s,
                None => {
                    set_vic_plot_error.set(format!("无效的返回类型: {:?}", pdts_curve_js));
                    return;
                }
            };

            // 处理图片数据
            let image = result;
            //log!("图片数据: {:?}", image);
            // 检查 Base64 数据是否包含前缀
            let base64_data = if image.starts_with("data:image/png;base64,") {
                image.trim_start_matches("data:image/png;base64,").to_string()
            } else {
                image
            };
            // 将 Base64 字符串解码为二进制数据
            let binary_data = match STANDARD.decode(&base64_data) {
                Ok(data) => data,
                Err(_) => {
                    set_vic_plot_error.set("Base64解码失败".to_string());
                    return;
                }
            };
            
            // 将二进制数据转换为 js_sys::Uint8Array
            let uint8_array = Uint8Array::from(&binary_data[..]);
            
            // 创建 Blob
            let options = BlobPropertyBag::new();
            options.set_type("image/png");
            let blob = match Blob::new_with_u8_array_sequence_and_options(
                &Array::of1(&uint8_array),
                &options,
            ) {
                Ok(blob) => blob,
                Err(_) => {
                    set_vic_plot_error.set("创建图片Blob失败".to_string());
                    return;
                }
            };

            // 生成图片 URL
            let image_url = match Url::create_object_url_with_blob(&blob) {
                Ok(url) => url,
                Err(_) => {
                    set_vic_plot_error.set("创建图片URL失败".to_string());
                    return;
                }
            };

            // 打印生成的 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", "1000").expect("Failed to set width");

            // 将 <img> 插入到 DOM 中
            let img_div = document().get_element_by_id("img_div_python").expect("img_div not found");
            // 清空 div 内容(避免重复插入)
            img_div.set_inner_html("");
            img_div.append_child(&img).expect("Failed to append img");
        
        });
    };


    view! {                                              //view!宏作为App()函数的返回值返回IntoView类型
        <main class="container">
            <div>
                <form id="img_python" on:submit=python_acid_image>
                    <div class="error-message" style="color: red; font-weight: bold;">
                        {move || vic_plot_error.get() }
                    </div>
                    <button type="submit">"绘制温粘曲线Matplotlib"</button>
                    <p></p>                   
                    <div id="img_div_python" style="flex: 1;">
                    <img
                    src=""
                    width="1000"
                    />
                    </div>
                </form>
            </div>
        </main>
    }
}

2. 后台Tauri

前端调用了后台的python_acid_plot命令,将包含产品成分数据的结构变量序列化后传递给后台命令,后台将结构体转换成HashMap格式再传递给调用的python脚本。具体代码如下:

struct Pdt {
    pdt_id:i64,
    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]
fn python_acid_plot(app: tauri::AppHandle, productdata: Vec<Pdt>) -> Result<String, String> {
    use std::collections::HashMap;
    
    let resource_path = app.path().resolve("resources/views.py", BaseDirectory::Resource)
        .expect("Failed to resolve resource");

    // 将Pdt结构体转换为HashMap
    let data: Vec<HashMap<&str, serde_json::Value>> = productdata.iter().map(|pdt| {
        let mut map = HashMap::new();
        map.insert("pdt_id", serde_json::json!(pdt.pdt_id));
        map.insert("pdt_name", serde_json::json!(pdt.pdt_name));
        map.insert("pdt_si", serde_json::json!(pdt.pdt_si));
        map.insert("pdt_al", serde_json::json!(pdt.pdt_al));
        map.insert("pdt_ca", serde_json::json!(pdt.pdt_ca));
        map.insert("pdt_mg", serde_json::json!(pdt.pdt_mg));
        map.insert("pdt_fe", serde_json::json!(pdt.pdt_fe));
        map.insert("pdt_ti", serde_json::json!(pdt.pdt_ti));
        map.insert("pdt_ka", serde_json::json!(pdt.pdt_ka));
        map.insert("pdt_na", serde_json::json!(pdt.pdt_na));
        map.insert("pdt_mn", serde_json::json!(pdt.pdt_mn));
        map.insert("pdt_date", serde_json::json!(pdt.pdt_date));
        map
    }).collect();

    
    // 将HashMap序列化为JSON字符串
    // 添加调试日志
    //println!("Input data to Python script: {:?}", data);

    let input_data = serde_json::to_string(&data)
        .map_err(|e| e.to_string())?;

    // 添加调试日志
    //println!("JSON input data: {}", input_data);

    // 创建Python进程并将数据通过标准输入传递
    let mut command = Command::new("E:/python_envs/eric7/python.exe")
        .arg(resource_path)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| e.to_string())?;

    // 将JSON数据写入Python进程的标准输入
    if let Some(stdin) = command.stdin.as_mut() {
        Write::write_all(stdin, input_data.as_bytes())
            .map_err(|e| e.to_string())?;
    }

    // 等待命令完成并获取输出
    let output = command.wait_with_output()
        .map_err(|e| e.to_string())?;

    if output.status.success() {
        let image_data = String::from_utf8(output.stdout)
            .map_err(|e| e.to_string())?
            .trim()
            .to_string();
        Ok(image_data)
    } else {
        let error_message = String::from_utf8(output.stderr)
            .map_err(|e| e.to_string())?;
        Err(error_message)
    }
}

3. python脚本

Python脚本为调用Matplotlib绘图,需要将传递的JSON数据处理成字典数列,作为函数参数。具体代码如下:

# -*- coding: utf-8 -*-
import sys
import json
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib import font_manager
import matplotlib.pyplot as plt
import numpy as np
from io import BytesIO
import base64


# 尝试多种中文字体
try:
    plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Zen Hei', 'Arial Unicode MS']
    plt.rcParams['axes.unicode_minus'] = False
except:
    # 如果设置失败,尝试指定字体路径
    try:
        font_path = 'C:/Windows/Fonts/msyh.ttc'  # 微软雅黑字体路径
        font_prop = font_manager.FontProperties(fname=font_path)
        plt.rcParams['font.family'] = font_prop.get_name()
    except Exception as e:
        print(f"字体设置失败: {str(e)}", file=sys.stderr)


    
def draw_vis_temp_curve(productdata):
    # 初始化所有可能用到的变量
    global B0_1, B1_1, T0_1, B0_2, B1_2, T0_2
    global label_Name_1, label_Name_2, label_Name_3, label_Name_4, label_Name_5, label_Name_6, label_Name_7
    global Pdt_chem_list1, Pdt_chem_list2, Pdt_chem_list3, Pdt_chem_list4, Pdt_chem_list5, Pdt_chem_list6, Pdt_chem_list7
    B0_1 = B1_1 = T0_1 = 0.0
    B0_2 = B1_2 = T0_2 = 0.0
    label_Name_1 = label_Name_2 = label_Name_3 = label_Name_4 = label_Name_5 = label_Name_6 = label_Name_7 = ""
    Pdt_chem_list1 = Pdt_chem_list2 = Pdt_chem_list3 = Pdt_chem_list4 = Pdt_chem_list5 = Pdt_chem_list6 = Pdt_chem_list7 = []
    curve_num = len(productdata)

########################### 开始绘制温粘曲线#########################################

    para_list = [[1375.76, 122.29, 1.06247, 1.57233, 1.61648, 1.44738, 1.92899, 1.47337], [1272.64, 117.64, 1.05336, 1.42246, 1.48036, 1.51099, 1.86207, 1.36590],\
    [1192.44, 112.99, 1.03567, 1.27336, 1.43136, 1.41448, 1.65966, 1.20929]]
    Mol_list = [60.084, 101.96, 56.077, 40.3044, 159.6882, 79.9, 61.98, 94.2, 86.94]
    y_min_1 = 999999
    y_max_1 = 0
    y_min_2 = 999999
    y_max_2 = 0
    y_min_3 = 999999
    y_max_3 = 0
    y_min_4 = 999999
    y_max_4 = 0
    y_min_5 = 999999
    y_max_5 = 0
    y_min_6 = 999999
    y_max_6 = 0
    y_min_7 = 999999
    y_max_7 = 0



    for j,temp_obj in enumerate(productdata,1):
        if j == 1:
            label_Name_1 = temp_obj['pdt_name']
            Pdt1_Si_val = temp_obj['pdt_si']
            Pdt1_Al_val = temp_obj['pdt_al']
            Pdt1_Ca_val = temp_obj['pdt_ca']
            Pdt1_Mg_val = temp_obj['pdt_mg']
            Pdt1_Fe_val = temp_obj['pdt_fe']
            Pdt1_Ti_val = temp_obj['pdt_ti']
            Pdt1_Na_val = temp_obj['pdt_na']
            Pdt1_K_val = temp_obj['pdt_ka']
            Pdt1_Mn_val = temp_obj['pdt_mn']

        ......
        //具体绘图程序不便展示

    plt.tight_layout()
    buffer = BytesIO()
    plt.savefig(buffer, dpi=400, format='png', transparent=True, facecolor = 'none')
    buffer.seek(0)
    imageVis = base64.b64encode(buffer.read()).decode('utf-8')
    return f"data:image/png;base64,{imageVis}"

########################### 温粘曲线绘制完成 #########################################        

# 从标准输入读取JSON数据并处理编码
input_data = sys.stdin.buffer.read().decode('utf-8')
try:
    productdata = json.loads(input_data)

    # 调用绘图函数
    result = draw_vis_temp_curve(productdata)
    # 输出结果
    print(result, end="")
except Exception as e:
    print(f"Error processing input: {str(e)}", file=sys.stderr)
    sys.exit(1)

4. tauri.conf.json设置

为了让程序找到python程序文件,并在cargo tauri build编译时将python文件编译进程序中,需要将python文件放在src-tauri\resources目录下,并修改tauri.conf.json程序,具体内容如下:

{

    "beforeBuildCommand": "trunk build && xcopy /E /I src-tauri\\resources src-tauri\\target\\release\\resources",

  },

  "bundle": {
    "resources":["resources/plot.py", "resources/views.py"]
  }
}

至此就实现了前端Leptos调用后台Tauri命令,并传递结构变量作为参数,后台再调用Python Matplotlib程序绘图,并将图形返回给前端Leptos显示。


网站公告

今日签到

点亮在社区的每一天
去签到