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显示。