import sys import numpy as np import pandas as pd from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QPushButton, QTextEdit, QVBoxLayout, QHBoxLayout, QStackedWidget, QSizePolicy, QFrame, QGraphicsDropShadowEffect, QSpinBox, QTableWidget, QTableWidgetItem, QFileDialog, QComboBox, QMessageBox, QHeaderView ) from PyQt5.QtGui import QPixmap, QFont, QIcon from PyQt5.QtCore import Qt import matplotlib from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import os import numpy as np import pandas as pd import requests import webbrowser import serial import serial.tools.list_ports from PyQt5.QtWidgets import QGroupBox, QGridLayout, QLineEdit, QTextBrowser from PyQt5.QtCore import QTimer, pyqtSlot from PyQt5.QtWidgets import QCheckBox from PyQt5.QtCore import QTimer def resource_path(relative_path): """兼容PyInstaller打包后资源路径""" if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) # --------- 主页 --------- class HomePage(QWidget): def __init__(self): super().__init__() layout = QVBoxLayout(self) layout.setContentsMargins(60, 60, 60, 60) layout.setSpacing(32) self.setStyleSheet(""" QWidget { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fbfd, stop:1 #eaf6fb); border-radius: 18px; } """) # 欢迎标题 title = QLabel("欢迎来到 MRobot 工具箱!") title.setFont(QFont("微软雅黑", 26, QFont.Bold)) title.setAlignment(Qt.AlignCenter) title.setStyleSheet("color: #2980b9; letter-spacing: 3px;") layout.addWidget(title) # 设置高度 title.setFixedHeight(120) # 分割线 line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) line.setStyleSheet("color: #d6eaf8; background: #d6eaf8; min-height: 2px;") layout.addWidget(line) # 介绍内容 desc = QLabel( "🤖 本工具箱由青岛理工大学(QUT)机器人战队开发,\n" "涵盖沧溟(Robocon)与MOVE(Robomaster)两支队伍。\n\n" "集成了常用小工具与助手功能,持续更新中ing!\n" "👉 可通过左侧选择不同模块,助力更高效的机器人开发,\n" "节约开发时间,减少繁琐操作。\n\n" "欢迎反馈建议,共同完善工具箱!" ) desc.setFont(QFont("微软雅黑", 16)) desc.setAlignment(Qt.AlignCenter) desc.setStyleSheet("color: #34495e;") desc.setWordWrap(True) layout.addWidget(desc) # 作者&版本信息 info = QLabel( "作者: QUT RMer & RCer   |   " "版本: 0.0.2   |   " "联系方式: QQ群 : 857466609" ) info.setFont(QFont("微软雅黑", 14)) info.setAlignment(Qt.AlignCenter) info.setStyleSheet("color: #7f8c8d; margin-top: 24px;") info.setFixedHeight(100) # 修改为固定高度 layout.addWidget(info) # 页脚 footer = QLabel("© 2025 MRobot. 保留所有权利。") footer.setFont(QFont("微软雅黑", 12)) footer.setAlignment(Qt.AlignCenter) footer.setStyleSheet("color: #b2bec3; margin-top: 18px;") footer.setFixedHeight(100) # 修改为固定高度 layout.addWidget(footer) # --------- 功能一:多项式拟合工具页面 --------- class PolyFitApp(QWidget): def __init__(self): super().__init__() self.setFont(QFont("微软雅黑", 15)) self.data_x = [] self.data_y = [] self.last_coeffs = None self.last_xmin = None self.last_xmax = None # 统一背景和边框 self.setStyleSheet(""" QWidget { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); border-radius: 16px; border: 1px solid #d6eaf8; } """) main_layout = QHBoxLayout(self) main_layout.setContentsMargins(24, 24, 24, 24) main_layout.setSpacing(24) left_layout = QVBoxLayout() left_layout.setSpacing(18) right_layout = QVBoxLayout() right_layout.setSpacing(18) main_layout.addLayout(left_layout, 0) main_layout.addLayout(right_layout, 1) # 标题 title = QLabel("曲线拟合工具") # title.setFont(QFont("微软雅黑", 2, QFont.Bold)) # 设置文字大小 title.setFont(QFont("微软雅黑", 14, QFont.Bold)) title.setAlignment(Qt.AlignCenter) title.setStyleSheet("color: #2980b9; letter-spacing: 2px;") left_layout.addWidget(title) # 数据表 self.table = QTableWidget(0, 2) self.table.setFont(QFont("Consolas", 16)) self.table.setHorizontalHeaderLabels(["x", "y"]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setStyleSheet(""" QTableWidget { background: #f8fbfd; border-radius: 10px; border: 1px solid #d6eaf8; font-size: 16px; } QHeaderView::section { background-color: #eaf6fb; color: #2980b9; font-size: 16px; font-weight: bold; border: 1px solid #d6eaf8; height: 36px; } """) self.table.setMinimumHeight(200) # 设置最小高度 left_layout.addWidget(self.table, stretch=1) # 让表格尽量撑大 # 添加/删除行 btn_row = QHBoxLayout() self.add_row_btn = QPushButton("添加数据") self.add_row_btn.setFont(QFont("微软雅黑", 20, QFont.Bold)) self.add_row_btn.setMinimumHeight(44) self.add_row_btn.clicked.connect(self.add_point_row) self.del_row_btn = QPushButton("删除选中行") self.del_row_btn.setFont(QFont("微软雅黑", 20, QFont.Bold)) self.del_row_btn.setMinimumHeight(44) self.del_row_btn.clicked.connect(self.delete_selected_rows) for btn in [self.add_row_btn, self.del_row_btn]: btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2980b9; border-radius: 20px; font-size: 20px; font-weight: 600; padding: 10px 0; border: 1px solid #d6eaf8; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 1.5px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 1.5px solid #a4cbe3; } """) btn_row.addWidget(self.add_row_btn) btn_row.addWidget(self.del_row_btn) left_layout.addLayout(btn_row) # 导入/导出 file_btn_row = QHBoxLayout() self.import_btn = QPushButton("导入Excel文件") self.import_btn.setFont(QFont("微软雅黑", 18, QFont.Bold)) self.import_btn.setMinimumHeight(44) self.import_btn.clicked.connect(self.load_excel) self.export_btn = QPushButton("导出Excel文件") self.export_btn.setFont(QFont("微软雅黑", 18, QFont.Bold)) self.export_btn.setMinimumHeight(44) self.export_btn.clicked.connect(self.export_excel_and_plot) for btn in [self.import_btn, self.export_btn]: btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2980b9; border-radius: 20px; font-size: 20px; font-weight: 600; padding: 10px 0; border: 1px solid #d6eaf8; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 1.5px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 1.5px solid #a4cbe3; } """) file_btn_row.addWidget(self.import_btn) file_btn_row.addWidget(self.export_btn) left_layout.addLayout(file_btn_row) # 阶数选择 param_layout = QHBoxLayout() label_order = QLabel("多项式阶数:") # 文字居中 label_order.setAlignment(Qt.AlignCenter) # 文字加粗 label_order.setStyleSheet("color: #2980b9;") param_layout.addWidget(label_order) self.order_spin = QSpinBox() self.order_spin.setFont(QFont("微软雅黑", 18)) self.order_spin.setRange(1, 10) self.order_spin.setValue(2) self.order_spin.setStyleSheet(""" QSpinBox { background: #f8fbfd; border-radius: 10px; border: 1px solid #d6eaf8; font-size: 18px; padding: 4px 12px; } """) param_layout.addWidget(self.order_spin) left_layout.addLayout(param_layout) # 拟合按钮 self.fit_btn = QPushButton("拟合并显示") self.fit_btn.setFont(QFont("微软雅黑", 20, QFont.Bold)) self.fit_btn.setMinimumHeight(48) self.fit_btn.clicked.connect(self.fit_and_plot) self.fit_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2980b9; border-radius: 20px; font-size: 22px; font-weight: 600; padding: 12px 0; border: 1px solid #d6eaf8; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 1.5px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 1.5px solid #a4cbe3; } """) left_layout.addWidget(self.fit_btn) # 输出区 self.output = QTextEdit() self.output.setReadOnly(False) self.output.setFont(QFont("Consolas", 16)) self.output.setMaximumHeight(160) self.output.setStyleSheet(""" QTextEdit { background: #f4f6f7; border-radius: 8px; border: 1px solid #d6eaf8; font-size: 16px; color: #2c3e50; padding: 10px; } """) # self.table.setFixedHeight(400) # 设置表格高度为260像素 left_layout.addWidget(self.output) # 代码生成 code_layout = QHBoxLayout() label_code = QLabel("输出代码格式:") # label_code.setFont(QFont("微软雅黑", 18, QFont.Bold)) label_code.setStyleSheet("color: #2980b9;") code_layout.addWidget(label_code) self.code_type = QComboBox() self.code_type.setFont(QFont("微软雅黑", 18)) self.code_type.addItems(["C", "C++", "Python"]) self.code_type.setStyleSheet(""" QComboBox { background: #f8fbfd; border-radius: 10px; border: 1px solid #d6eaf8; font-size: 18px; padding: 4px 12px; } """) code_layout.addWidget(self.code_type) self.gen_code_btn = QPushButton("生成代码") self.gen_code_btn.setFont(QFont("微软雅黑", 18, QFont.Bold)) self.gen_code_btn.setMinimumHeight(44) self.gen_code_btn.clicked.connect(self.generate_code) self.gen_code_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2980b9; border-radius: 20px; font-size: 20px; font-weight: 600; padding: 10px 0; border: 1px solid #d6eaf8; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 1.5px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 1.5px solid #a4cbe3; } """) code_layout.addWidget(self.gen_code_btn) left_layout.addLayout(code_layout) # 曲线区加圆角和阴影 curve_frame = QFrame() curve_frame.setStyleSheet(""" QFrame { background: #fff; border-radius: 16px; border: 1px solid #d6eaf8; } """) curve_shadow = QGraphicsDropShadowEffect(self) curve_shadow.setBlurRadius(24) curve_shadow.setOffset(0, 4) curve_shadow.setColor(Qt.gray) curve_frame.setGraphicsEffect(curve_shadow) curve_layout = QVBoxLayout(curve_frame) curve_layout.setContentsMargins(10, 10, 10, 10) self.figure = Figure(figsize=(6, 5)) self.canvas = FigureCanvas(self.figure) curve_layout.addWidget(self.canvas) right_layout.addWidget(curve_frame) # 默认显示空坐标系 self.figure.clear() ax = self.figure.add_subplot(111) # ax.set_title("拟合结果", fontsize=22, fontweight='bold') ax.set_xlabel("x", fontsize=18) ax.set_ylabel("y", fontsize=18) ax.tick_params(labelsize=15) # ax.set_title("拟合结果", fontsize=22, fontweight='bold') # 中文标题 self.canvas.draw() def add_point_row(self, x_val="", y_val=""): row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(str(x_val))) self.table.setItem(row, 1, QTableWidgetItem(str(y_val))) def delete_selected_rows(self): selected = self.table.selectionModel().selectedRows() for idx in sorted(selected, reverse=True): self.table.removeRow(idx.row()) def load_excel(self): file, _ = QFileDialog.getOpenFileName(self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)") if file: try: data = pd.read_excel(file, usecols=[0, 1]) new_x = data.iloc[:, 0].values.tolist() new_y = data.iloc[:, 1].values.tolist() for x, y in zip(new_x, new_y): self.add_point_row(x, y) QMessageBox.information(self, "成功", "数据导入成功!") except Exception as e: QMessageBox.critical(self, "错误", f"读取Excel失败: {e}") def export_excel_and_plot(self): file, _ = QFileDialog.getSaveFileName(self, "导出Excel文件", "", "Excel Files (*.xlsx *.xls)") if file: x_list, y_list = [], [] for row in range(self.table.rowCount()): try: x = float(self.table.item(row, 0).text()) y = float(self.table.item(row, 1).text()) x_list.append(x) y_list.append(y) except Exception: continue if not x_list or not y_list: QMessageBox.warning(self, "导出失败", "没有可导出的数据!") return df = pd.DataFrame({'x': x_list, 'y': y_list}) try: df.to_excel(file, index=False) png_file = file if png_file.lower().endswith('.xlsx') or png_file.lower().endswith('.xls'): png_file = png_file.rsplit('.', 1)[0] + '.png' else: png_file = png_file + '.png' self.figure.savefig(png_file, dpi=150, bbox_inches='tight') QMessageBox.information(self, "导出成功", f"数据已成功导出到Excel文件!\n图像已导出为:{png_file}") except Exception as e: QMessageBox.critical(self, "导出错误", f"导出Excel或图像失败: {e}") def get_manual_points(self): x_list, y_list = [], [] for row in range(self.table.rowCount()): try: x = float(self.table.item(row, 0).text()) y = float(self.table.item(row, 1).text()) x_list.append(x) y_list.append(y) except Exception: continue return x_list, y_list def fit_and_plot(self): self.data_x, self.data_y = self.get_manual_points() try: order = int(self.order_spin.value()) except ValueError: QMessageBox.warning(self, "输入错误", "阶数必须为整数!") return n_points = len(self.data_x) if n_points < order + 1: QMessageBox.warning(self, "数据不足", "数据点数量不足以拟合该阶多项式!") return x = np.array(self.data_x, dtype=np.float64) y = np.array(self.data_y, dtype=np.float64) x_min, x_max = x.min(), x.max() if x_max - x_min == 0: QMessageBox.warning(self, "数据错误", "所有x值都相同,无法拟合!") return try: coeffs = np.polyfit(x, y, order) except Exception as e: QMessageBox.critical(self, "拟合错误", f"多项式拟合失败:{e}") return poly = np.poly1d(coeffs) expr = "y = " + " + ".join([f"{c:.6g}*x^{order-i}" for i, c in enumerate(coeffs)]) self.output.setPlainText(f"{expr}\n") self.figure.clear() ax = self.figure.add_subplot(111) # ax.set_title("拟合结果", fontsize=22, fontweight='bold') ax.set_xlabel("x", fontsize=18) ax.set_ylabel("y", fontsize=18) ax.scatter(x, y, color='red', label='Data') x_fit = np.linspace(x_min, x_max, 200) y_fit = poly(x_fit) ax.plot(x_fit, y_fit, label='Fit Curve') ax.legend() self.canvas.draw() self.last_coeffs = coeffs self.last_xmin = x_min self.last_xmax = x_max def generate_code(self): if self.last_coeffs is None: QMessageBox.warning(self, "未拟合", "请先拟合数据!") return coeffs = self.last_coeffs code_type = self.code_type.currentText() if code_type == "C": code = self.create_c_function(coeffs) elif code_type == "C++": code = self.create_cpp_function(coeffs) else: code = self.create_py_function(coeffs) self.output.setPlainText(code) def create_c_function(self, coeffs): lines = ["#include ", "double polynomial(double x) {", " return "] n = len(coeffs) terms = [] for i, c in enumerate(coeffs): exp = n - i - 1 if exp == 0: terms.append(f"{c:.8g}") elif exp == 1: terms.append(f"{c:.8g}*x") else: terms.append(f"{c:.8g}*pow(x,{exp})") lines[-1] += " + ".join(terms) + ";" lines.append("}") return "\n".join(lines) def create_cpp_function(self, coeffs): lines = ["#include ", "double polynomial(double x) {", " return "] n = len(coeffs) terms = [] for i, c in enumerate(coeffs): exp = n - i - 1 if exp == 0: terms.append(f"{c:.8g}") elif exp == 1: terms.append(f"{c:.8g}*x") else: terms.append(f"{c:.8g}*pow(x,{exp})") lines[-1] += " + ".join(terms) + ";" lines.append("}") return "\n".join(lines) def create_py_function(self, coeffs): n = len(coeffs) lines = ["def polynomial(x):", " return "] terms = [] for i, c in enumerate(coeffs): exp = n - i - 1 if exp == 0: terms.append(f"{c:.8g}") elif exp == 1: terms.append(f"{c:.8g}*x") else: terms.append(f"{c:.8g}*x**{exp}") lines[-1] += " + ".join(terms) return "\n".join(lines) # --------- 功能二:下载 --------- class DownloadPage(QWidget): def __init__(self): super().__init__() self.setFont(QFont("微软雅黑", 15)) self.setStyleSheet(""" QWidget { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); border-radius: 18px; border: 1px solid #d6eaf8; } """) main_layout = QVBoxLayout(self) main_layout.setContentsMargins(60, 60, 60, 60) main_layout.setSpacing(32) # 标题 title = QLabel("常用工具下载") title.setFont(QFont("微软雅黑", 26, QFont.Bold)) title.setAlignment(Qt.AlignCenter) title.setStyleSheet("color: #2980b9; letter-spacing: 3px; margin-bottom: 10px;") main_layout.addWidget(title) # 分割线 line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) line.setStyleSheet("color: #d6eaf8; background: #d6eaf8; min-height: 2px;") main_layout.addWidget(line) # # 说明 # desc = QLabel("点击下方按钮可直接跳转到常用工具或开发软件的官方下载页面:") # desc.setFont(QFont("微软雅黑", 17)) # desc.setAlignment(Qt.AlignCenter) # desc.setStyleSheet("color: #34495e; margin-bottom: 18px;") # main_layout.addWidget(desc) # 两大类布局 from PyQt5.QtWidgets import QGridLayout, QGroupBox # 小工具类 tools_tools = [ ("Geek Uninstaller", "https://geekuninstaller.com/download", "🧹"), ("Neat Download Manager", "https://www.neatdownloadmanager.com/index.php/en/", "⬇️"), ("Everything", "https://www.voidtools.com/zh-cn/downloads/", "🔍"), ("Bandizip", "https://www.bandisoft.com/bandizip/", "🗜️"), ("PotPlayer", "https://potplayer.daum.net/", "🎬"), ("Typora", "https://typora.io/", "📝"), ("Git", "https://git-scm.com/download/win", "🟥"), ("Python", "https://www.python.org/downloads/", "🐍"), ] tools_group = QGroupBox("常用小工具") tools_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) tools_group.setStyleSheet(""" QGroupBox { border: 2px solid #b5d0ea; border-radius: 12px; margin-top: 16px; background: #f8fbfd; color: #2471a3; padding: 10px 0 0 0; } QGroupBox:title { subcontrol-origin: margin; left: 18px; top: -10px; background: transparent; padding: 0 8px; } """) tools_layout = QGridLayout() tools_layout.setSpacing(18) tools_layout.setContentsMargins(24, 24, 24, 24) for idx, (name, url, icon) in enumerate(tools_tools): btn = QPushButton(f"{icon} {name}") btn.setFont(QFont("微软雅黑", 16, QFont.Bold)) btn.setMinimumHeight(60) btn.setCursor(Qt.PointingHandCursor) btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2471a3; border-radius: 14px; font-size: 16px; font-weight: 600; padding: 8px 0; border: 1.5px solid #d6eaf8; letter-spacing: 1px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 2px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 2px solid #a4cbe3; } """) btn.clicked.connect(lambda checked, link=url: webbrowser.open(link)) row, col = divmod(idx, 4) tools_layout.addWidget(btn, row, col) tools_group.setLayout(tools_layout) main_layout.addWidget(tools_group) # 开发/设计软件类 dev_tools = [ ("STM32CubeMX", "https://www.st.com/zh/development-tools/stm32cubemx.html", "🟦"), ("Keil MDK", "https://www.keil.com/download/product/", "🟩"), ("Visual Studio Code", "https://code.visualstudio.com/", "🟦"), ("CLion", "https://www.jetbrains.com/clion/download/", "🟧"), ("MATLAB", "https://www.mathworks.com/downloads/", "🟨"), ("SolidWorks", "https://www.solidworks.com/sw/support/downloads.htm", "🟫"), ("Altium Designer", "https://www.altium.com/zh/altium-designer/downloads", "🟪"), ("原神", "https://download-porter.hoyoverse.com/download-porter/2025/03/27/GenshinImpact_install_202503072011.exe?trace_key=GenshinImpact_install_ua_679d0b4e9b10", "🟫"), ] dev_group = QGroupBox("开发/设计软件") dev_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) dev_group.setStyleSheet(""" QGroupBox { border: 2px solid #b5d0ea; border-radius: 12px; margin-top: 16px; background: #f8fbfd; color: #2471a3; padding: 10px 0 0 0; } QGroupBox:title { subcontrol-origin: margin; left: 18px; top: -10px; background: transparent; padding: 0 8px; } """) dev_layout = QGridLayout() dev_layout.setSpacing(18) dev_layout.setContentsMargins(24, 24, 24, 24) for idx, (name, url, icon) in enumerate(dev_tools): btn = QPushButton(f"{icon} {name}") btn.setFont(QFont("微软雅黑", 16, QFont.Bold)) btn.setMinimumHeight(60) btn.setCursor(Qt.PointingHandCursor) btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2471a3; border-radius: 14px; font-size: 16px; font-weight: 600; padding: 8px 0; border: 1.5px solid #d6eaf8; letter-spacing: 1px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 2px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 2px solid #a4cbe3; } """) btn.clicked.connect(lambda checked, link=url: webbrowser.open(link)) row, col = divmod(idx, 4) dev_layout.addWidget(btn, row, col) dev_group.setLayout(dev_layout) main_layout.addWidget(dev_group) main_layout.addStretch(1) # 页脚 footer = QLabel("如有问题或建议,欢迎反馈至QQ群:857466609") footer.setFont(QFont("微软雅黑", 13)) footer.setAlignment(Qt.AlignCenter) footer.setStyleSheet("color: #b2bec3; margin-top: 18px;") main_layout.addWidget(footer) # --------- 功能三:串口助手 --------- class SerialAssistant(QWidget): def __init__(self): super().__init__() self.setFont(QFont("微软雅黑", 15)) self.ser = None self.timer = None self.recv_buffer = b"" self.plot_data = {} self.curve_colors = ["#e74c3c", "#2980b9", "#27ae60", "#f1c40f", "#8e44ad", "#16a085"] self.data_types = ["float", "int16", "uint16", "int8", "uint8"] self.data_type = "float" self.data_count = 2 self.sample_idx = 0 # 新增:HEX模式复选框 self.hex_send_chk = QCheckBox("HEX发送") self.hex_recv_chk = QCheckBox("HEX接收") self.hex_send_chk.setFont(QFont("微软雅黑", 10)) self.hex_recv_chk.setFont(QFont("微软雅黑", 10)) self.hex_send_chk.setChecked(False) self.hex_recv_chk.setChecked(False) # 主体布局 main_layout = QHBoxLayout(self) main_layout.setContentsMargins(32, 32, 32, 32) main_layout.setSpacing(28) # 左侧面板 left_panel = QVBoxLayout() left_panel.setSpacing(20) left_panel.setContentsMargins(0, 0, 0, 0) # 串口配置区 config_group = QGroupBox("串口配置") config_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) config_group.setStyleSheet(""" QGroupBox { border: 2px solid #b5d0ea; border-radius: 12px; margin-top: 12px; background: #f8fbfd; color: #2471a3; padding: 8px 0 0 0; } QGroupBox:title { subcontrol-origin: margin; left: 16px; top: -8px; background: transparent; padding: 0 8px; } """) config_layout = QGridLayout() config_layout.setSpacing(12) config_layout.setContentsMargins(16, 16, 16, 16) config_layout.addWidget(QLabel("串口号:"), 0, 0) self.port_box = QComboBox() self.port_box.setMinimumWidth(120) self.refresh_ports() config_layout.addWidget(self.port_box, 0, 1) config_layout.addWidget(QLabel("波特率:"), 1, 0) self.baud_box = QComboBox() self.baud_box.addItems(["9600", "115200", "57600", "38400", "19200", "4800"]) self.baud_box.setCurrentText("115200") config_layout.addWidget(self.baud_box, 1, 1) self.refresh_btn = QPushButton("刷新串口") self.refresh_btn.clicked.connect(self.refresh_ports) self.refresh_btn.setStyleSheet(self._btn_style()) config_layout.addWidget(self.refresh_btn, 2, 0) self.open_btn = QPushButton("打开串口") self.open_btn.setCheckable(True) self.open_btn.clicked.connect(self.toggle_serial) self.open_btn.setStyleSheet(self._btn_style("#27ae60")) config_layout.addWidget(self.open_btn, 2, 1) config_group.setLayout(config_layout) left_panel.addWidget(config_group) # 数据协议配置区 proto_group = QGroupBox("数据协议配置") proto_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) proto_group.setStyleSheet(config_group.styleSheet()) proto_layout = QGridLayout() proto_layout.setSpacing(12) proto_layout.setContentsMargins(16, 16, 16, 16) proto_layout.addWidget(QLabel("数据数量:"), 0, 0) self.data_count_spin = QSpinBox() self.data_count_spin.setRange(1, 16) self.data_count_spin.setValue(self.data_count) self.data_count_spin.valueChanged.connect(self.apply_proto_config) proto_layout.addWidget(self.data_count_spin, 0, 1) proto_layout.addWidget(QLabel("数据类型:"), 1, 0) self.data_type_box = QComboBox() self.data_type_box.addItems(self.data_types) self.data_type_box.setCurrentText(self.data_type) self.data_type_box.currentTextChanged.connect(self.apply_proto_config) proto_layout.addWidget(self.data_type_box, 1, 1) proto_group.setLayout(proto_layout) left_panel.addWidget(proto_group) # 发送数据区 send_group = QGroupBox("发送数据") send_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) send_group.setStyleSheet(""" QGroupBox { border: 2px solid #b5d0ea; border-radius: 12px; margin-top: 12px; background: #f8fbfd; color: #2471a3; padding: 8px 0 0 0; } QGroupBox:title { subcontrol-origin: margin; left: 16px; top: -8px; background: transparent; padding: 0 8px; } """) send_layout = QVBoxLayout() send_layout.setSpacing(14) send_layout.setContentsMargins(12, 12, 12, 12) # 输入框(更大更高字体) self.send_edit = QLineEdit() self.send_edit.setFont(QFont("Consolas", 22)) self.send_edit.setPlaceholderText("输入要发送的数据...") self.send_edit.setMinimumHeight(54) self.send_edit.setMinimumWidth(360) self.send_edit.setStyleSheet(""" QLineEdit { background: #f8fbfd; border-radius: 10px; border: 1.5px solid #d6eaf8; font-size: 22px; padding: 12px 18px; } """) send_layout.addWidget(self.send_edit) # HEX复选框行(字体更小) hex_chk_row = QHBoxLayout() self.hex_send_chk.setFont(QFont("微软雅黑", 10)) self.hex_recv_chk.setFont(QFont("微软雅黑", 10)) for chk in [self.hex_send_chk, self.hex_recv_chk]: chk.setStyleSheet(""" QCheckBox { color: #2471a3; spacing: 12px; } QCheckBox::indicator { width: 18px; height: 18px; } QCheckBox::indicator:checked { background-color: #2980b9; border: 1.5px solid #2980b9; } QCheckBox::indicator:unchecked { background-color: #fff; border: 1.5px solid #b5d0ea; } """) self.hex_send_chk.setChecked(False) self.hex_recv_chk.setChecked(False) hex_chk_row.addWidget(self.hex_send_chk) hex_chk_row.addWidget(self.hex_recv_chk) hex_chk_row.addStretch(1) send_layout.addLayout(hex_chk_row) # 发送按钮 send_btn_row = QHBoxLayout() self.send_btn = QPushButton("发送") self.send_btn.clicked.connect(self.send_data) self.send_btn.setFont(QFont("微软雅黑", 16, QFont.Bold)) self.send_btn.setStyleSheet(self._btn_style("#2980b9")) send_btn_row.addWidget(self.send_btn) send_layout.addLayout(send_btn_row) send_group.setLayout(send_layout) left_panel.addWidget(send_group, stretch=1) # 让发送区弹性填充 # 清空按钮 self.clear_btn = QPushButton("清空接收和曲线") self.clear_btn.clicked.connect(self.clear_all) self.clear_btn.setStyleSheet(self._btn_style("#e74c3c")) left_panel.addWidget(self.clear_btn) # 弹性空隙(动态扩展填满) left_panel.addStretch(1) # 使用说明始终在最下方 usage_group = QGroupBox("使用说明") usage_group.setFont(QFont("微软雅黑", 13, QFont.Bold)) usage_group.setStyleSheet(""" QGroupBox { border: 2px solid #b5d0ea; border-radius: 10px; margin-top: 10px; background: #f8fbfd; color: #2471a3; padding: 6px 0 0 0; } QGroupBox:title { subcontrol-origin: margin; left: 10px; top: -8px; background: transparent; padding: 0 6px; } """) usage_layout = QVBoxLayout() usage_label = QLabel( "1. 选择串口号和波特率,点击“打开串口”。\n" "2. 在“数据协议配置”中选择数据数量和数据类型。\n" "3. 下位机发送格式:\n" " 0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n" " 校验和为包头到最后一个数据字节的累加和的低8位。\n" "4. 每包数据自动绘制曲线,X轴为采样点(或时间),Y轴为各通道数据。\n" "5. 支持float/int16/uint16/int8/uint8类型,最多16通道。\n" "6. 可点击“测试绘图”按钮模拟数据包接收效果。" ) usage_label.setWordWrap(True) usage_label.setFont(QFont("微软雅黑", 9)) usage_layout.addWidget(usage_label) usage_group.setLayout(usage_layout) left_panel.addWidget(usage_group) main_layout.addLayout(left_panel, 0) # 右侧面板 right_panel = QVBoxLayout() right_panel.setSpacing(20) # 接收区 recv_group = QGroupBox("串口接收区") recv_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) recv_group.setStyleSheet(config_group.styleSheet()) recv_layout = QVBoxLayout() self.recv_box = QTextEdit() self.recv_box.setFont(QFont("Consolas", 13)) self.recv_box.setReadOnly(True) self.recv_box.setMinimumHeight(120) self.recv_box.setStyleSheet(""" QTextEdit { background: #f8fbfd; border-radius: 10px; border: 1px solid #d6eaf8; font-size: 15px; color: #2c3e50; padding: 8px; } """) recv_layout.addWidget(self.recv_box) recv_group.setLayout(recv_layout) right_panel.addWidget(recv_group) # 曲线绘图区 plot_frame = QFrame() plot_frame.setStyleSheet(""" QFrame { background: #fff; border-radius: 16px; border: 1px solid #d6eaf8; } """) plot_shadow = QGraphicsDropShadowEffect(self) plot_shadow.setBlurRadius(18) plot_shadow.setOffset(0, 4) plot_shadow.setColor(Qt.gray) plot_frame.setGraphicsEffect(plot_shadow) plot_layout2 = QVBoxLayout(plot_frame) plot_layout2.setContentsMargins(10, 10, 10, 10) self.figure = Figure(figsize=(7, 4)) self.canvas = FigureCanvas(self.figure) plot_layout2.addWidget(self.canvas) right_panel.addWidget(plot_frame, 2) main_layout.addLayout(right_panel, 1) # 定时器接收 self.timer = QTimer(self) self.timer.timeout.connect(self.read_serial) # 新增:正弦波测试定时器 self.sine_timer = QTimer(self) self.sine_timer.timeout.connect(self.send_sine_data) self.sine_phase = 0 # 默认配置 self.apply_proto_config() def simulate_data(self): """模拟一包数据并自动解析绘图""" import struct import random # 构造协议包 head = 0x55 count = self.data_count dtype = self.data_type # 随机生成数据 if dtype == "float": vals = [random.uniform(-10, 10) for _ in range(count)] data_bytes = struct.pack(f"<{count}f", *vals) elif dtype == "int16": vals = [random.randint(-30000, 30000) for _ in range(count)] data_bytes = struct.pack(f"<{count}h", *vals) elif dtype == "uint16": vals = [random.randint(0, 65535) for _ in range(count)] data_bytes = struct.pack(f"<{count}H", *vals) elif dtype == "int8": vals = [random.randint(-128, 127) for _ in range(count)] data_bytes = struct.pack(f"<{count}b", *vals) elif dtype == "uint8": vals = [random.randint(0, 255) for _ in range(count)] data_bytes = struct.pack(f"<{count}B", *vals) else: vals = [0] * count data_bytes = b"\x00" * (count * self._type_size()) # 拼包 pkt = bytes([head, count]) + data_bytes checksum = sum(pkt) & 0xFF pkt += bytes([checksum]) # 加入接收缓冲区并解析 self.recv_buffer += pkt self.parse_and_plot_bin() def _btn_style(self, color="#2980b9"): return f""" QPushButton {{ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: {color}; border-radius: 14px; font-size: 16px; font-weight: 600; padding: 8px 0; border: 1.5px solid #d6eaf8; letter-spacing: 1px; }} QPushButton:hover {{ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 2px solid #b5d0ea; }} QPushButton:pressed {{ background: #e3f0fa; color: {color}; border: 2px solid #a4cbe3; }} """ def apply_proto_config(self): self.data_count = self.data_count_spin.value() self.data_type = self.data_type_box.currentText() self.plot_data = {i: [[], []] for i in range(self.data_count)} self.sample_idx = 0 self.update_plot() def refresh_ports(self): self.port_box.clear() ports = serial.tools.list_ports.comports() for port in ports: self.port_box.addItem(port.device) if self.port_box.count() == 0: self.port_box.addItem("无可用串口") def toggle_serial(self): if self.open_btn.isChecked(): port = self.port_box.currentText() baud = int(self.baud_box.currentText()) try: self.ser = serial.Serial(port, baud, timeout=0.1) self.open_btn.setText("关闭串口") self.recv_box.append(f"已打开串口 {port} @ {baud}bps") self.timer.start(50) except Exception as e: self.recv_box.append(f"打开串口失败: {e}") self.open_btn.setChecked(False) else: if self.ser and self.ser.is_open: self.ser.close() self.open_btn.setText("打开串口") self.recv_box.append("串口已关闭") self.timer.stop() def send_data(self): if self.ser and self.ser.is_open: data = self.send_edit.text() try: if self.hex_send_chk.isChecked(): # HEX模式发送 data_bytes = bytes.fromhex(data.replace(' ', '')) self.ser.write(data_bytes) self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}") else: self.ser.write(data.encode('utf-8')) self.recv_box.append(f"发送: {data}") except Exception as e: self.recv_box.append(f"发送失败: {e}") else: self.recv_box.append("串口未打开,无法发送。") def send_multi_data(self): if self.ser and self.ser.is_open: text = self.send_edit.text() lines = text.split(";") for line in lines: if line.strip(): try: if self.hex_send_chk.isChecked(): data_bytes = bytes.fromhex(line.strip().replace(' ', '')) self.ser.write(data_bytes) self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}") else: self.ser.write(line.strip().encode('utf-8')) self.recv_box.append(f"发送: {line.strip()}") except Exception as e: self.recv_box.append(f"发送失败: {e}") else: self.recv_box.append("串口未打开,无法发送。") def read_serial(self): if self.ser and self.ser.is_open: try: data = self.ser.read_all() if data: if self.hex_recv_chk.isChecked(): self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}") else: try: self.recv_box.append(f"接收: {data.decode('utf-8', errors='replace')}") except Exception: self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}") self.recv_buffer += data self.parse_and_plot_bin() except Exception as e: self.recv_box.append(f"接收失败: {e}") def toggle_sine_test(self): if self.test_btn.isChecked(): self.test_btn.setText("停止测试") self.sine_phase = 0 self.sine_timer.start(80) # 80ms周期 else: self.test_btn.setText("测试正弦波(持续)") self.sine_timer.stop() def send_sine_data(self): import struct, math head = 0x55 count = self.data_count dtype = self.data_type t = self.sine_phase vals = [] for i in range(count): # 多通道不同相位 val = math.sin(t / 10.0 + i * math.pi / 4) * 10 if dtype == "float": vals.append(float(val)) elif dtype == "int16": vals.append(int(val * 1000)) elif dtype == "uint16": vals.append(int(val * 1000 + 20000)) elif dtype == "int8": vals.append(int(val * 10)) elif dtype == "uint8": vals.append(int(val * 10 + 100)) # 打包 if dtype == "float": data_bytes = struct.pack(f"<{count}f", *vals) elif dtype == "int16": data_bytes = struct.pack(f"<{count}h", *vals) elif dtype == "uint16": data_bytes = struct.pack(f"<{count}H", *vals) elif dtype == "int8": data_bytes = struct.pack(f"<{count}b", *vals) elif dtype == "uint8": data_bytes = struct.pack(f"<{count}B", *vals) else: data_bytes = b"\x00" * (count * self._type_size()) pkt = bytes([head, count]) + data_bytes checksum = sum(pkt) & 0xFF pkt += bytes([checksum]) # 直接走接收流程模拟 self.recv_buffer += pkt self.parse_and_plot_bin() # 在接收区实时显示理论值 self.recv_box.append(f"理论: {['%.3f'%v for v in vals]}") self.sine_phase += 1 def parse_and_plot_bin(self): # 协议:0x55 + 数据数量(1B) + 数据 + 校验(1B) min_len = 1 + 1 + self.data_count * self._type_size() + 1 while len(self.recv_buffer) >= min_len: idx = self.recv_buffer.find(b'\x55') if idx == -1: self.recv_buffer = b"" break if idx > 0: self.recv_buffer = self.recv_buffer[idx:] if len(self.recv_buffer) < min_len: break # 检查数量 count = self.recv_buffer[1] if count != self.data_count: self.recv_buffer = self.recv_buffer[2:] continue data_bytes = self.recv_buffer[2:2+count*self._type_size()] checksum = self.recv_buffer[2+count*self._type_size()] calc_sum = (sum(self.recv_buffer[:2+count*self._type_size()])) & 0xFF if checksum != calc_sum: self.recv_box.append("校验和错误,丢弃包") self.recv_buffer = self.recv_buffer[1:] continue # 解析数据 values = self._unpack_data(data_bytes, count) self.recv_box.append(f"接收: {values}") for i, v in enumerate(values): self.plot_data[i][0].append(self.sample_idx) self.plot_data[i][1].append(v) if len(self.plot_data[i][0]) > 200: self.plot_data[i][0].pop(0) self.plot_data[i][1].pop(0) self.sample_idx += 1 self.recv_buffer = self.recv_buffer[min_len:] self.update_plot() def _type_size(self): if self.data_type == "float": return 4 elif self.data_type in ("int16", "uint16"): return 2 elif self.data_type in ("int8", "uint8"): return 1 return 4 def _unpack_data(self, data_bytes, count): import struct fmt = { "float": f"<{count}f", "int16": f"<{count}h", "uint16": f"<{count}H", "int8": f"<{count}b", "uint8": f"<{count}B" }[self.data_type] try: return struct.unpack(fmt, data_bytes) except Exception: return [0] * count def update_plot(self): self.figure.clear() ax = self.figure.add_subplot(111) ax.set_xlabel("采样点", fontsize=14) ax.set_ylabel("数据值", fontsize=14) has_curve = False for idx in range(self.data_count): color = self.curve_colors[idx % len(self.curve_colors)] x_list, y_list = self.plot_data.get(idx, ([], [])) if x_list and y_list: ax.plot(x_list, y_list, label=f"CH{idx+1}", color=color, linewidth=2) has_curve = True if has_curve: ax.legend() ax.grid(True, linestyle="--", alpha=0.5) self.canvas.draw() def clear_all(self): self.recv_box.clear() self.plot_data = {i: [[], []] for i in range(self.data_count)} self.sample_idx = 0 self.update_plot() # --------- 功能四:MRobot架构生成 --------- class GenerateMRobotCode(QWidget): def __init__(self): super().__init__() self.setFont(QFont("微软雅黑", 15)) self.setStyleSheet(""" QWidget { background: #f8fbfd; border-radius: 16px; padding: 20px; } """) self.init_ui() def init_ui(self): main_layout = QVBoxLayout(self) main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(20) # 功能说明 desc_label = QLabel("MRobot架构生成工具,帮助您快速生成MRobot项目代码。") desc_label.setFont(QFont("微软雅黑", 14)) desc_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(desc_label) # 其他UI元素... # --------- 主工具箱UI --------- class ToolboxUI(QWidget): def __init__(self): super().__init__() self.setWindowTitle("MRobot 工具箱") self.resize(1920, 1080) self.setMinimumSize(900, 600) self.setStyleSheet(""" QWidget { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); border-radius: 16px; } """) self.init_ui() def init_ui(self): main_layout = QHBoxLayout(self) main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(20) # 左侧导航 left_frame = QFrame() left_frame.setFrameShape(QFrame.StyledPanel) left_frame.setStyleSheet(""" QFrame { background: #f8fbfd; border-radius: 14px; border: 1px solid #d6eaf8; } """) left_shadow = QGraphicsDropShadowEffect(self) left_shadow.setBlurRadius(16) left_shadow.setOffset(0, 4) left_shadow.setColor(Qt.gray) left_frame.setGraphicsEffect(left_shadow) left_layout = QVBoxLayout(left_frame) left_layout.setSpacing(24) left_layout.setContentsMargins(18, 18, 18, 18) left_frame.setFixedWidth(260) main_layout.addWidget(left_frame) # Logo logo_label = QLabel() logo_pixmap = QPixmap(resource_path("mr_tool_img/MRobot.png")) if not logo_pixmap.isNull(): logo_label.setPixmap(logo_pixmap.scaled(180, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)) else: logo_label.setText("MRobot") logo_label.setAlignment(Qt.AlignCenter) logo_label.setFont(QFont("Arial", 36, QFont.Bold)) logo_label.setStyleSheet("color: #3498db;") logo_label.setFixedHeight(120) left_layout.addWidget(logo_label) # 按钮区 self.button_names = ["主页", "曲线拟合", "Mini串口助手", "MR架构配置","软件指南"] self.buttons = [] for idx, name in enumerate(self.button_names): btn = QPushButton(name) btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) btn.setMinimumHeight(48) btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eaf6fb, stop:1 #d6eaf8); color: #2980b9; border-radius: 20px; font-size: 22px; font-weight: 600; padding: 14px 0; border: 1px solid #d6eaf8; letter-spacing: 2px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #f8fffe, stop:1 #cfe7fa); color: #1a6fae; border: 1.5px solid #b5d0ea; } QPushButton:pressed { background: #e3f0fa; color: #2471a3; border: 1.5px solid #a4cbe3; } """) btn.clicked.connect(lambda checked, i=idx: self.switch_function(i)) self.buttons.append(btn) left_layout.addWidget(btn) left_layout.addStretch(1) # 文本输出框 self.output_box = QTextEdit() self.output_box.setReadOnly(True) self.output_box.setFixedHeight(180) self.output_box.setStyleSheet(""" QTextEdit { background: #f4f6f7; border-radius: 8px; border: 1px solid #d6eaf8; font-size: 16px; color: #2c3e50; padding: 10px; } """) left_layout.addWidget(self.output_box) # 右侧功能区 self.stack = QStackedWidget() self.stack.setStyleSheet(""" QStackedWidget { background: #fff; border-radius: 16px; border: 1px solid #d6eaf8; } """) right_shadow = QGraphicsDropShadowEffect(self) right_shadow.setBlurRadius(24) right_shadow.setOffset(0, 4) right_shadow.setColor(Qt.gray) self.stack.setGraphicsEffect(right_shadow) # 功能页面注册(后续扩展只需在这里添加页面类即可) self.page_widgets = { 0: HomePage(), # 主页 1: PolyFitApp(), # 多项式拟合 2: SerialAssistant(), # 串口助手 3: GenerateMRobotCode(), # MRobot架构生成 4: DownloadPage(), # 下载页面 } for i in range(len(self.button_names)): self.stack.addWidget(self.page_widgets[i]) main_layout.addWidget(self.stack) self.output_box.append("欢迎使用 MRobot 工具箱!请选择左侧功能。") def placeholder_page(self, text): page = QWidget() layout = QVBoxLayout(page) label = QLabel(text) label.setAlignment(Qt.AlignCenter) label.setFont(QFont("微软雅黑", 22, QFont.Bold)) label.setStyleSheet("color: #2980b9;") layout.addStretch(1) layout.addWidget(label) layout.addStretch(1) return page def switch_function(self, idx): self.stack.setCurrentIndex(idx) self.output_box.append(f"已切换到功能:{self.button_names[idx]}") if __name__ == "__main__": app = QApplication(sys.argv) win = ToolboxUI() win.show() sys.exit(app.exec_())