MRobot/app/function_fit_interface.py
2026-01-01 21:23:47 +08:00

292 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
from PyQt5.QtCore import Qt
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
import pyqtgraph as pg
# 延迟导入:这些库只在需要时才导入,加快应用启动速度
# import numpy as np
# from openpyxl import load_workbook, Workbook
class FunctionFitInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 延迟导入标志
self._libs_loaded = False
self.setObjectName("functionFitInterface")
main_layout = QHBoxLayout(self)
main_layout.setSpacing(24)
# 左侧:数据输入区
left_layout = QVBoxLayout()
left_layout.setSpacing(16)
self.dataTable = TableWidget(self)
self.dataTable.setColumnCount(2)
self.dataTable.setHorizontalHeaderLabels(["x", "y"])
self.dataTable.setColumnWidth(0, 125)
self.dataTable.setColumnWidth(1, 125)
left_layout.addWidget(self.dataTable)
btn_layout = QHBoxLayout()
add_row_btn = PushButton("添加一行")
add_row_btn.clicked.connect(self.add_row)
del_row_btn = PushButton("删除选中行") # 新增按钮
del_row_btn.clicked.connect(self.delete_selected_row) # 绑定槽函数
btn_layout.addWidget(add_row_btn)
btn_layout.addWidget(del_row_btn) # 添加到布局
left_layout.addLayout(btn_layout)
btn_layout = QHBoxLayout()
import_btn = PushButton("导入 Excel")
import_btn.clicked.connect(self.import_excel)
export_btn = PushButton("导出 Excel")
export_btn.clicked.connect(self.export_excel)
btn_layout.addWidget(import_btn)
btn_layout.addWidget(export_btn)
left_layout.addLayout(btn_layout)
self.dataTable.setMinimumWidth(280)
self.dataTable.setMaximumWidth(280)
main_layout.addLayout(left_layout, 1)
self.add_row()
# 右侧:图像展示区
right_layout = QVBoxLayout()
right_layout.setSpacing(12)
right_layout.addWidget(SubtitleLabel("函数图像预览"))
# 占位符实际的canvas会在_load_heavy_libraries中创建
self.canvas_placeholder = QWidget()
self.canvas_layout = QVBoxLayout(self.canvas_placeholder)
self.canvas_layout.setContentsMargins(0, 0, 0, 0)
right_layout.addWidget(self.canvas_placeholder, stretch=1)
self.resultLabel = BodyLabel("")
self.resultLabel.setWordWrap(True) # 自动换行
right_layout.addWidget(self.resultLabel)
# 拟合阶数和输出语言选择(合并到同一行)
options_layout = QHBoxLayout()
self.spinBox = SpinBox()
self.spinBox.setRange(1, 10)
self.spinBox.setValue(2)
options_layout.addWidget(SubtitleLabel("拟合阶数"))
options_layout.addWidget(self.spinBox)
self.langBox = ComboBox()
self.langBox.addItems(["C/C++", "Python"])
options_layout.addWidget(SubtitleLabel("输出语言"))
options_layout.addWidget(self.langBox)
right_layout.addLayout(options_layout)
# 代码显示和复制按钮
self.codeLabel = BodyLabel("")
self.codeLabel.setWordWrap(True) # 自动换行
right_layout.addWidget(self.codeLabel)
btn_layout = QHBoxLayout() # 新增一行布局
fit_btn = PushButton(FluentIcon.UNIT,"拟合并绘图")
fit_btn.clicked.connect(self.fit_and_plot)
btn_layout.addWidget(fit_btn)
copy_btn = PushButton(FluentIcon.COPY, "复制代码")
copy_btn.clicked.connect(self.copy_code)
btn_layout.addWidget(copy_btn)
right_layout.addLayout(btn_layout)
main_layout.addLayout(right_layout, 2)
def _load_heavy_libraries(self):
"""延迟加载大型库,提高应用启动速度"""
if self._libs_loaded:
return
global np, load_workbook, Workbook
import numpy as np
from openpyxl import load_workbook, Workbook
# 创建 PyQtGraph 画布
self.plot_widget = pg.PlotWidget()
self.plot_widget.setBackground('w') # 白色背景
self.plot_widget.showGrid(x=True, y=True, alpha=0.3)
self.plot_widget.setLabel('left', 'y')
self.plot_widget.setLabel('bottom', 'x')
self.plot_widget.setTitle('graph of a function')
# 将 plot_widget 添加到占位符布局中
if hasattr(self, 'canvas_layout'):
self.canvas_layout.addWidget(self.plot_widget)
self._libs_loaded = True
def add_row(self):
row = self.dataTable.rowCount()
self.dataTable.insertRow(row)
# 可选:初始化为空字符串
self.dataTable.setItem(row, 0, QTableWidgetItem(""))
self.dataTable.setItem(row, 1, QTableWidgetItem(""))
def delete_selected_row(self):
selected = self.dataTable.selectedItems()
if selected:
rows = set(item.row() for item in selected)
for row in sorted(rows, reverse=True):
self.dataTable.removeRow(row)
def import_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
if path:
wb = load_workbook(path)
ws = wb.active
self.dataTable.setRowCount(0)
for row_data in ws.iter_rows(min_row=2, values_only=True): # 跳过表头
row = self.dataTable.rowCount()
self.dataTable.insertRow(row)
for col, value in enumerate(row_data[:2]):
item = QTableWidgetItem(str(value) if value is not None else "")
self.dataTable.setItem(row, col, item)
def export_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
if path:
data = self.parse_data()
if data is not None:
wb = Workbook()
ws = wb.active
ws.append(["x", "y"])
for row in data:
ws.append(row)
wb.save(path)
def parse_data(self):
data = []
row_count = self.dataTable.rowCount()
for row in range(row_count):
try:
x_item = self.dataTable.item(row, 0)
y_item = self.dataTable.item(row, 1)
if x_item is None or y_item is None:
continue
x = float(x_item.text())
y = float(y_item.text())
data.append([x, y])
except Exception:
continue
return data if data else None
def fit_and_plot(self):
self._load_heavy_libraries() # 延迟加载库
data = self.parse_data()
if not data:
self.resultLabel.setText("数据格式错误或为空")
self.codeLabel.setText("")
return
x = np.array([d[0] for d in data])
y = np.array([d[1] for d in data])
degree = self.spinBox.value()
coeffs = np.polyfit(x, y, degree)
# 用更密集的横坐标画拟合曲线
x_fit = np.linspace(x.min(), x.max(), 100)
y_fit = np.polyval(coeffs, x_fit)
# 清空并重新绘图
self.plot_widget.clear()
# 绘制原始数据点(蓝色散点)
scatter = pg.ScatterPlotItem(
x=x, y=y,
pen=None,
brush=pg.mkBrush(0, 0, 255, 120), # 蓝色半透明
size=10,
name='raw data'
)
self.plot_widget.addItem(scatter)
# 绘制拟合曲线(红色线条)
pen = pg.mkPen(color=(255, 0, 0), width=2) # 红色线条
curve = self.plot_widget.plot(
x_fit, y_fit,
pen=pen,
name='Fitted curve'
)
# 添加图例
self.plot_widget.addLegend()
formula = self.poly_formula(coeffs)
self.resultLabel.setText(f"拟合公式: {formula}")
lang = self.langBox.currentText()
code = self.generate_code(coeffs, lang)
self.codeLabel.setText(code)
def poly_formula(self, coeffs):
terms = []
degree = len(coeffs) - 1
for i, c in enumerate(coeffs):
power = degree - i
if abs(c) < 1e-8:
continue
if power == 0:
terms.append(f"{c:.6g}")
elif power == 1:
terms.append(f"{c:.6g}*x")
else:
terms.append(f"{c:.6g}*x^{power}")
return " + ".join(terms)
def generate_code(self, coeffs, lang):
degree = len(coeffs) - 1
if lang == "C/C++":
code = "double poly(double x) {\n return "
elif lang == "Python":
code = "def poly(x):\n return "
else:
code = ""
terms = []
for i, c in enumerate(coeffs):
power = degree - i
if abs(c) < 1e-8:
continue
if power == 0:
terms.append(f"{c:.6g}")
elif power == 1:
terms.append(f"{c:.6g}*x")
else:
terms.append(f"{c:.6g}*pow(x,{power})" if lang == "C/C++" else f"{c:.6g}*x**{power}")
code += " + ".join(terms)
code += ";\n}" if lang == "C/C++" else ""
return code
def copy_code(self):
clipboard = QApplication.clipboard()
clipboard.setText(self.codeLabel.text())
# 弹出提示
InfoBar.success(
title='复制成功',
content="代码已复制到剪贴板!",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)