继续改

This commit is contained in:
Robofish 2025-07-25 02:54:03 +08:00
parent 78661f450b
commit 9fc6b4577a
10 changed files with 2070 additions and 1770 deletions

BIN
1.xlsx Normal file

Binary file not shown.

1718
MRobot.py

File diff suppressed because it is too large Load Diff

1714
MRobot_old.py Normal file

File diff suppressed because it is too large Load Diff

22
app.py
View File

@ -1,22 +0,0 @@
import os
import sys
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from app.main_window import MainWindow
# 启用 DPI 缩放
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) # 使用高 DPI 图标
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings) # 避免创建原生窗口小部件的兄弟窗口
w = MainWindow()
sys.exit(app.exec_()) # 启动应用程序并进入主事件循环
# 注意:在 PyQt5 中exec_() 是一个阻塞调用,直到应用程序退出。

45
app/about_interface.py Normal file
View File

@ -0,0 +1,45 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox
from PyQt5.QtCore import Qt
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon
from qfluentwidgets import InfoBar, InfoBarPosition
from .function_fit_interface import FunctionFitInterface
from app.tools.check_update import check_update
__version__ = "1.0.0"
class AboutInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("aboutInterface")
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop)
card = PrimaryPushSettingCard(
text="检查更新",
icon=FluentIcon.DOWNLOAD,
title="关于",
content=f"MRobot_Toolbox 当前版本:{__version__}",
)
card.clicked.connect(self.on_check_update_clicked)
layout.addWidget(card)
def on_check_update_clicked(self):
latest = check_update(__version__)
if latest:
InfoBar.success(
title="发现新版本",
content=f"检测到新版本:{latest},请前往官网或仓库下载更新。",
parent=self,
position=InfoBarPosition.TOP,
duration=5000
)
else:
InfoBar.info(
title="已是最新版本",
content="当前已是最新版本,无需更新。",
parent=self,
position=InfoBarPosition.TOP,
duration=3000
)

View File

@ -65,19 +65,16 @@ class DataInterface(QWidget):
# 主标题 # 主标题
title = TitleLabel("MRobot 代码生成") title = TitleLabel("MRobot 代码生成")
title.setAlignment(Qt.AlignCenter) title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("font-size: 36px; font-weight: bold; color: #2d7d9a;")
content_layout.addWidget(title) content_layout.addWidget(title)
# 副标题 # 副标题
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径.ico所在的目录然后开启代码之旅") subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径.ico所在的目录然后开启代码之旅")
subtitle.setAlignment(Qt.AlignCenter) subtitle.setAlignment(Qt.AlignCenter)
subtitle.setStyleSheet("font-size: 16px; color: #4a6fa5;")
content_layout.addWidget(subtitle) content_layout.addWidget(subtitle)
# 简要说明 # 简要说明
desc = BodyLabel("支持自动配置和生成任务自主选择模块代码倒入自动识别cubemx配置") desc = BodyLabel("支持自动配置和生成任务自主选择模块代码倒入自动识别cubemx配置")
desc.setAlignment(Qt.AlignCenter) desc.setAlignment(Qt.AlignCenter)
desc.setStyleSheet("font-size: 14px; color: #6b7b8c;")
content_layout.addWidget(desc) content_layout.addWidget(desc)
content_layout.addSpacing(18) content_layout.addSpacing(18)
@ -85,14 +82,12 @@ class DataInterface(QWidget):
# 选择项目路径按钮 # 选择项目路径按钮
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径") self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
self.choose_btn.setFixedWidth(200) self.choose_btn.setFixedWidth(200)
self.choose_btn.setStyleSheet("font-size: 17px;")
self.choose_btn.clicked.connect(self.choose_project_folder) self.choose_btn.clicked.connect(self.choose_project_folder)
content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter) content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
# 更新代码库按钮 # 更新代码库按钮
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库") self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
self.update_template_btn.setFixedWidth(200) self.update_template_btn.setFixedWidth(200)
self.update_template_btn.setStyleSheet("font-size: 17px;")
self.update_template_btn.clicked.connect(self.update_user_template) self.update_template_btn.clicked.connect(self.update_user_template)
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter) content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)

View File

@ -1,8 +1,18 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QLabel from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from qfluentwidgets import TitleLabel, BodyLabel from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
import pandas as pd import pandas as pd
import io import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt5.QtWebEngineWidgets import QWebEngineView
import plotly.graph_objs as go
import plotly.io as pio
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Source Han Sans', 'STHeiti', 'Heiti TC']
matplotlib.rcParams['axes.unicode_minus'] = False
class FunctionFitInterface(QWidget): class FunctionFitInterface(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -10,56 +20,127 @@ class FunctionFitInterface(QWidget):
self.setObjectName("functionFitInterface") self.setObjectName("functionFitInterface")
main_layout = QHBoxLayout(self) main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(32, 32, 32, 32)
main_layout.setSpacing(24) main_layout.setSpacing(24)
# 左侧:数据输入区 # 左侧:数据输入区
left_layout = QVBoxLayout() left_layout = QVBoxLayout()
left_layout.setSpacing(16) left_layout.setSpacing(16)
left_layout.addWidget(TitleLabel("数据输入/导入")) self.dataTable = TableWidget(self)
self.dataEdit = QTextEdit() self.dataTable.setColumnCount(2)
self.dataEdit.setPlaceholderText("输入数据每行格式x,y") self.dataTable.setHorizontalHeaderLabels(["x", "y"])
left_layout.addWidget(self.dataEdit) self.dataTable.setColumnWidth(0, 125)
self.dataTable.setColumnWidth(1, 125)
left_layout.addWidget(self.dataTable)
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
import_btn = QPushButton("导入 Excel") 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) import_btn.clicked.connect(self.import_excel)
export_btn = QPushButton("导出 Excel") export_btn = PushButton("导出 Excel")
export_btn.clicked.connect(self.export_excel) export_btn.clicked.connect(self.export_excel)
btn_layout.addWidget(import_btn) btn_layout.addWidget(import_btn)
btn_layout.addWidget(export_btn) btn_layout.addWidget(export_btn)
left_layout.addLayout(btn_layout) left_layout.addLayout(btn_layout)
fit_btn = QPushButton("拟合并绘图") self.dataTable.setMinimumWidth(280)
fit_btn.clicked.connect(self.fit_and_plot) self.dataTable.setMaximumWidth(280)
left_layout.addWidget(fit_btn)
main_layout.addLayout(left_layout, 1) main_layout.addLayout(left_layout, 1)
self.add_row()
# 右侧:图像展示区 # 右侧:图像展示区
right_layout = QVBoxLayout() right_layout = QVBoxLayout()
right_layout.setSpacing(16) right_layout.setSpacing(12)
right_layout.addWidget(TitleLabel("函数拟合图像")) right_layout.addWidget(SubtitleLabel("函数图像预览"))
import matplotlib.pyplot as plt self.figure = Figure(figsize=(5, 4))
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
self.figure, self.ax = plt.subplots()
self.canvas = FigureCanvas(self.figure) self.canvas = FigureCanvas(self.figure)
right_layout.addWidget(self.canvas, stretch=1) right_layout.addWidget(self.canvas, stretch=1)
self.resultLabel = BodyLabel("") self.resultLabel = BodyLabel("")
self.resultLabel.setWordWrap(True) # 自动换行
right_layout.addWidget(self.resultLabel) 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) main_layout.addLayout(right_layout, 2)
# 默认显示空图像
self.figure.clear()
ax = self.figure.add_subplot(111)
ax.set_xlabel('x')
ax.set_ylabel('y')
self.canvas.draw()
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): def import_excel(self):
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx *.xls)") path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx *.xls)")
if path: if path:
df = pd.read_excel(path) df = pd.read_excel(path)
text = "\n".join(f"{row[0]},{row[1]}" for row in df.values) self.dataTable.setRowCount(0) # 清空原有数据
self.dataEdit.setText(text) for row_data in df.values.tolist():
row = self.dataTable.rowCount()
self.dataTable.insertRow(row)
for col, value in enumerate(row_data):
item = QTableWidgetItem(str(value))
self.dataTable.setItem(row, col, item)
def export_excel(self): def export_excel(self):
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)") path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
@ -70,11 +151,16 @@ class FunctionFitInterface(QWidget):
df.to_excel(path, index=False) df.to_excel(path, index=False)
def parse_data(self): def parse_data(self):
lines = self.dataEdit.toPlainText().strip().split('\n')
data = [] data = []
for line in lines: row_count = self.dataTable.rowCount()
for row in range(row_count):
try: try:
x, y = map(float, line.split(',')) 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]) data.append([x, y])
except Exception: except Exception:
continue continue
@ -84,18 +170,84 @@ class FunctionFitInterface(QWidget):
data = self.parse_data() data = self.parse_data()
if not data: if not data:
self.resultLabel.setText("数据格式错误或为空") self.resultLabel.setText("数据格式错误或为空")
self.codeLabel.setText("")
return return
import numpy as np
import matplotlib.pyplot as plt
x = np.array([d[0] for d in data]) x = np.array([d[0] for d in data])
y = np.array([d[1] for d in data]) y = np.array([d[1] for d in data])
# 简单线性拟合 degree = self.spinBox.value()
coeffs = np.polyfit(x, y, 1) coeffs = np.polyfit(x, y, degree)
y_fit = np.polyval(coeffs, x)
self.ax.clear() # 用更密集的横坐标画拟合曲线
self.ax.scatter(x, y, label="原始数据") x_fit = np.linspace(x.min(), x.max(), 100)
self.ax.plot(x, y_fit, color='r', label=f"拟合: y={coeffs[0]:.3f}x+{coeffs[1]:.3f}") y_fit = np.polyval(coeffs, x_fit)
self.ax.legend()
self.figure.clear()
ax = self.figure.add_subplot(111)
ax.scatter(x, y, color='blue', label='原始数据')
ax.plot(x_fit, y_fit, color='red', label=f'拟合: {degree}')
ax.set_title('函数图像')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
self.canvas.draw() self.canvas.draw()
self.resultLabel.setText(f"拟合公式: y = {coeffs[0]:.3f}x + {coeffs[1]:.3f}")
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
)

View File

@ -10,9 +10,10 @@ with redirect_stdout(None):
from .home_interface import HomeInterface from .home_interface import HomeInterface
from .serial_terminal_interface import SerialTerminalInterface from .serial_terminal_interface import SerialTerminalInterface
from .function_fit_interface import FunctionFitInterface
from .part_library_interface import PartLibraryInterface from .part_library_interface import PartLibraryInterface
from .data_interface import DataInterface from .data_interface import DataInterface
from .mini_tool_interface import MiniToolInterface
from .about_interface import AboutInterface
import base64 import base64
@ -47,17 +48,20 @@ class MainWindow(FluentWindow):
def initInterface(self): def initInterface(self):
self.homeInterface = HomeInterface(self) self.homeInterface = HomeInterface(self)
self.serialTerminalInterface = SerialTerminalInterface(self) self.serialTerminalInterface = SerialTerminalInterface(self)
self.functionFitInterface = FunctionFitInterface(self)
self.partLibraryInterface = PartLibraryInterface(self) self.partLibraryInterface = PartLibraryInterface(self)
self.dataInterface = DataInterface(self) self.dataInterface = DataInterface(self)
self.miniToolInterface = MiniToolInterface(self)
def initNavigation(self): def initNavigation(self):
self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页')) self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页'))
self.addSubInterface(self.dataInterface, FIF.CODE, self.tr('代码生成'))
self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手')) self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手'))
self.addSubInterface(self.functionFitInterface, FIF.ROBOT, self.tr('函数拟合'))
self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库')) self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库'))
self.addSubInterface(self.dataInterface, FIF.DOWNLOAD, self.tr('代码生成')) self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱'))
self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM)
# self.navigationInterface.addWidget( # self.navigationInterface.addWidget(
# 'startGameButton', # 'startGameButton',
# NavigationBarPushButton(FIF.PLAY, '启动游戏', isSelectable=False), # NavigationBarPushButton(FIF.PLAY, '启动游戏', isSelectable=False),

View File

@ -0,0 +1,82 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
from PyQt5.QtCore import Qt
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
from .function_fit_interface import FunctionFitInterface
class MiniToolInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("minitoolInterface")
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignTop)
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
# 顶部标签栏,横向拉伸
self.tabBar = TabBar(self)
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.vBoxLayout.addWidget(self.tabBar) # 移除 Qt.AlignLeft
self.stackedWidget = QStackedWidget(self)
self.vBoxLayout.addWidget(self.stackedWidget) # 加入布局
# 初始主页面
self.mainPage = QWidget(self)
mainLayout = QVBoxLayout(self.mainPage)
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
self.card = PushSettingCard(
text="▶启动",
icon=FluentIcon.UNIT,
title="曲线拟合工具",
content="简单的曲线拟合工具,支持多种函数类型",
)
mainLayout.addWidget(self.card)
self.mainPage.setLayout(mainLayout)
# 添加主页面到堆叠窗口
self.addSubInterface(self.mainPage, "mainPage", "工具箱主页")
self.setLayout(self.vBoxLayout)
# 信号连接
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
# self.tabBar.tabAddRequested.connect(self.onAddNewTab)
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
self.card.clicked.connect(self.open_fit_tab)
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
widget.setObjectName(objectName)
self.stackedWidget.addWidget(widget)
self.tabBar.addTab(
routeKey=objectName,
text=text,
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
)
def onCurrentIndexChanged(self, index):
widget = self.stackedWidget.widget(index)
self.tabBar.setCurrentTab(widget.objectName())
def onAddNewTab(self):
pass # 可自定义添加新标签页逻辑
def onCloseTab(self, index: int):
item = self.tabBar.tabItem(index)
widget = self.findChild(QWidget, item.routeKey())
self.stackedWidget.removeWidget(widget)
self.tabBar.removeTab(index)
widget.deleteLater()
def open_fit_tab(self):
# 检查是否已存在标签页,避免重复添加
for i in range(self.stackedWidget.count()):
widget = self.stackedWidget.widget(i)
if widget.objectName() == "fitPage":
self.stackedWidget.setCurrentWidget(widget)
self.tabBar.setCurrentTab("fitPage")
return
fit_page = FunctionFitInterface(self)
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
self.stackedWidget.setCurrentWidget(fit_page)
self.tabBar.setCurrentTab("fitPage")

20
app/tools/check_update.py Normal file
View File

@ -0,0 +1,20 @@
import requests
from packaging.version import parse as vparse
def check_update(local_version, repo="goldenfishs/MRobot"):
"""
检查 GitHub 上是否有新版本
:param local_version: 当前版本号字符串 "1.0.2"
:param repo: 仓库名格式 "用户名/仓库名"
:return: 最新版本号字符串如果有新版本否则 None
"""
url = f"https://api.github.com/repos/{repo}/releases/latest"
try:
resp = requests.get(url, timeout=5)
if resp.status_code == 200:
latest = resp.json()["tag_name"].lstrip("v")
if vparse(latest) > vparse(local_version):
return latest
except Exception as e:
print(f"检查更新失败: {e}")
return None