Tauri(2.5.1)+Leptos(0.7.8)开发桌面应用---后台调用Python Matplotlib绘制图形
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::Uint8Arraylet uint8_array = Uint8Array::from(&binary_data[..]);// 创建 Bloblet 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;}};// 生成图片 URLlet 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;"><imgsrc=""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结构体转换为HashMaplet 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_2global label_Name_1, label_Name_2, label_Name_3, label_Name_4, label_Name_5, label_Name_6, label_Name_7global Pdt_chem_list1, Pdt_chem_list2, Pdt_chem_list3, Pdt_chem_list4, Pdt_chem_list5, Pdt_chem_list6, Pdt_chem_list7B0_1 = B1_1 = T0_1 = 0.0B0_2 = B1_2 = T0_2 = 0.0label_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 = 999999y_max_1 = 0y_min_2 = 999999y_max_2 = 0y_min_3 = 999999y_max_3 = 0y_min_4 = 999999y_max_4 = 0y_min_5 = 999999y_max_5 = 0y_min_6 = 999999y_max_6 = 0y_min_7 = 999999y_max_7 = 0for 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显示。