import sys import os import ast import json import re import traceback from pathlib import Path from PyQt6.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, QTextEdit, QScrollArea, QMessageBox, QFrame, QSpinBox, QDoubleSpinBox, QTableWidget, QTableWidgetItem, QHeaderView, QToolBar, QStatusBar, QDialog, QDialogButtonBox, QGroupBox, QSplitter, QListWidget, QComboBox, QInputDialog, QFileDialog, QMenuBar, QMenu) from PyQt6.QtCore import Qt, QSize, QTimer from PyQt6.QtGui import QAction class ConfigSettingsDialog(QDialog): """配置文件设置对话框""" def __init__(self, current_path="", parent=None): super().__init__(parent) self.setWindowTitle("配置文件设置") self.setModal(True) self.setMinimumSize(500, 200) self.current_path = current_path self.init_ui() def init_ui(self): layout = QVBoxLayout(self) layout.setSpacing(12) # 文件路径设置 file_group = QGroupBox("配置文件路径") file_layout = QVBoxLayout(file_group) path_layout = QHBoxLayout() path_layout.addWidget(QLabel("配置文件:")) self.path_edit = QLineEdit() self.path_edit.setText(self.current_path) path_layout.addWidget(self.path_edit) self.browse_btn = QPushButton("浏览...") self.browse_btn.clicked.connect(self.browse_file) path_layout.addWidget(self.browse_btn) file_layout.addLayout(path_layout) # 提示信息 tip_label = QLabel("提示:请选择或输入要编辑的配置文件路径") tip_label.setStyleSheet("color: #666666; font-size: 11px;") file_layout.addWidget(tip_label) layout.addWidget(file_group) # 使用相对路径选项 self.use_relative_checkbox = QCheckBox("使用相对路径(相对于程序所在目录)") self.use_relative_checkbox.setChecked(True) layout.addWidget(self.use_relative_checkbox) layout.addStretch() # 按钮 button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def browse_file(self): """浏览文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择配置文件", self.path_edit.text() or str(Path.home()), "Python Files (*.py);;All Files (*)" ) if file_path: self.path_edit.setText(file_path) def get_settings(self): """获取设置""" config_path = self.path_edit.text().strip() # 如果使用相对路径,则转换为相对于程序目录的路径 if self.use_relative_checkbox.isChecked() and config_path: if not os.path.isabs(config_path): # 如果已经是相对路径,相对于程序所在目录 program_dir = Path(__file__).parent.absolute() config_path = str(program_dir / config_path) return { "config_file_path": config_path, "use_relative_path": self.use_relative_checkbox.isChecked() } class ConfigManagementWindow(QDialog): def __init__(self, rules, config_fields, parent=None): super().__init__(parent) self.rules = rules self.config_fields = config_fields self.all_fields = list(config_fields.keys()) # 保存所有字段名用于搜索 self.setWindowTitle("规则管理") self.setModal(True) self.setMinimumSize(700, 600) self.resize(700, 600) self.init_ui() self.load_data() def init_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(8) # 分割窗口 splitter = QSplitter(Qt.Orientation.Horizontal) layout.addWidget(splitter) # 左侧:配置项列表 left_widget = QWidget() left_layout = QVBoxLayout(left_widget) left_layout.setContentsMargins(4, 4, 4, 4) # 搜索框 search_layout = QHBoxLayout() search_layout.addWidget(QLabel("搜索:")) self.search_edit = QLineEdit() self.search_edit.setPlaceholderText("输入配置项名称进行搜索...") self.search_edit.textChanged.connect(self.filter_fields) search_layout.addWidget(self.search_edit) # 清除搜索按钮 self.clear_search_btn = QPushButton("清除") self.clear_search_btn.clicked.connect(self.clear_search) self.clear_search_btn.setMaximumWidth(60) search_layout.addWidget(self.clear_search_btn) left_layout.addLayout(search_layout) left_layout.addWidget(QLabel("配置项列表:")) self.fields_list = QListWidget() self.fields_list.currentItemChanged.connect(self.on_field_selected) left_layout.addWidget(self.fields_list) # 添加统计信息 self.fields_count_label = QLabel("") left_layout.addWidget(self.fields_count_label) splitter.addWidget(left_widget) # 右侧:编辑区域 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setContentsMargins(4, 4, 4, 4) # 基本信息 info_group = QGroupBox("字段属性") form_layout = QVBoxLayout(info_group) form_layout.setContentsMargins(8, 8, 8, 8) # 变量名 self.name_label = QLabel() self.create_form_row(form_layout, "变量名:", self.name_label) # 显示名称 self.display_name_edit = QLineEdit() self.create_form_row(form_layout, "显示名称:", self.display_name_edit) # 分组和类型 category_layout = QHBoxLayout() category_layout.addWidget(QLabel("分组:")) self.category_combo = QComboBox() self.category_combo.setEditable(True) category_layout.addWidget(self.category_combo) category_layout.addStretch() category_layout.addWidget(QLabel("类型:")) self.type_combo = QComboBox() self.type_combo.addItems(["auto", "str", "int", "float", "bool", "json"]) self.type_combo.currentTextChanged.connect(self.on_type_changed) category_layout.addWidget(self.type_combo) form_layout.addLayout(category_layout) # 小数位数设置(仅当类型为float或int时显示) self.decimal_row = QHBoxLayout() self.decimal_label = QLabel("小数位数:") self.decimal_spinbox = QSpinBox() self.decimal_spinbox.setRange(0, 10) self.decimal_spinbox.setValue(2) # 默认值 self.decimal_spinbox.setEnabled(False) # 默认禁用,只有float类型才启用 self.decimal_row.addWidget(self.decimal_label) self.decimal_row.addWidget(self.decimal_spinbox) self.decimal_row.addStretch() form_layout.addLayout(self.decimal_row) # 隐藏状态 self.hidden_checkbox = QCheckBox("隐藏此配置项(不在主界面显示)") form_layout.addWidget(self.hidden_checkbox) # 提示信息 form_layout.addWidget(QLabel("提示信息:")) self.tooltip_edit = QTextEdit() self.tooltip_edit.setMaximumHeight(80) form_layout.addWidget(self.tooltip_edit) right_layout.addWidget(info_group) # 校验规则 validation_group = QGroupBox("校验规则") validation_layout = QVBoxLayout(validation_group) validation_layout.setContentsMargins(8, 8, 8, 8) # 最小值 min_layout = QHBoxLayout() min_layout.addWidget(QLabel("最小值:")) self.min_edit = QLineEdit() self.min_edit.setPlaceholderText("对于数字类型有效") min_layout.addWidget(self.min_edit) min_layout.addStretch() validation_layout.addLayout(min_layout) # 最大值 max_layout = QHBoxLayout() max_layout.addWidget(QLabel("最大值:")) self.max_edit = QLineEdit() self.max_edit.setPlaceholderText("对于数字类型有效") max_layout.addWidget(self.max_edit) max_layout.addStretch() validation_layout.addLayout(max_layout) # 正则表达式 validation_layout.addWidget(QLabel("正则表达式:")) self.regex_edit = QLineEdit() self.regex_edit.setPlaceholderText("对于字符串类型有效,如: ^[A-Za-z0-9_]+$") validation_layout.addWidget(self.regex_edit) # 必填项 self.required_checkbox = QCheckBox("必填项") validation_layout.addWidget(self.required_checkbox) right_layout.addWidget(validation_group) # 分组管理 group_group = QGroupBox("分组管理") group_layout = QVBoxLayout(group_group) group_btn_layout = QHBoxLayout() self.add_group_btn = QPushButton("添加分组") self.remove_group_btn = QPushButton("删除分组") self.add_group_btn.clicked.connect(self.add_group) self.remove_group_btn.clicked.connect(self.remove_group) group_btn_layout.addWidget(self.add_group_btn) group_btn_layout.addWidget(self.remove_group_btn) group_btn_layout.addStretch() group_layout.addLayout(group_btn_layout) right_layout.addWidget(group_group) right_layout.addStretch() splitter.addWidget(right_widget) # 设置分割比例 splitter.setSizes([250, 420]) # 按钮 button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def create_form_row(self, layout, label_text, widget): """创建舒适的表单行""" row_layout = QHBoxLayout() label = QLabel(label_text) label.setMinimumWidth(80) row_layout.addWidget(label) row_layout.addWidget(widget) layout.addLayout(row_layout) def load_data(self): """加载数据到界面""" self.fields_list.clear() for field_name in sorted(self.config_fields.keys()): self.fields_list.addItem(field_name) self.update_fields_count() self.category_combo.clear() categories = list(self.rules.get("categories", {}).keys()) for category in sorted(categories): self.category_combo.addItem(category) self.category_combo.addItem("未分类") def filter_fields(self, search_text): """根据搜索文本过滤配置项列表""" search_text = search_text.strip().lower() if not search_text: # 如果搜索文本为空,显示所有项 for i in range(self.fields_list.count()): item = self.fields_list.item(i) item.setHidden(False) else: # 模糊匹配:显示包含搜索文本的项 for i in range(self.fields_list.count()): item = self.fields_list.item(i) field_name = item.text().lower() if search_text in field_name: item.setHidden(False) else: item.setHidden(True) self.update_fields_count() def clear_search(self): """清除搜索框内容""" self.search_edit.clear() def update_fields_count(self): """更新配置项计数显示""" total_count = self.fields_list.count() visible_count = 0 for i in range(total_count): if not self.fields_list.item(i).isHidden(): visible_count += 1 if visible_count == total_count: self.fields_count_label.setText(f"总计: {total_count} 个配置项") else: self.fields_count_label.setText(f"显示: {visible_count}/{total_count} 个配置项") def on_type_changed(self, field_type): """当类型改变时,控制小数位数设置的显示""" # 只有float类型才显示小数位数设置 if field_type == "float": self.decimal_label.setVisible(True) self.decimal_spinbox.setVisible(True) self.decimal_spinbox.setEnabled(True) else: self.decimal_label.setVisible(False) self.decimal_spinbox.setVisible(False) self.decimal_spinbox.setEnabled(False) def on_field_selected(self, current, previous): # 保存前一个字段的更改 if previous is not None: previous_field_name = previous.text() self.save_current_field_changes(previous_field_name) if not current: return field_name = current.text() self.name_label.setText(field_name) display_name = self.rules.get("display_names", {}).get(field_name, field_name) self.display_name_edit.setText(display_name) category = "未分类" for cat, fields in self.rules.get("categories", {}).items(): if field_name in fields: category = cat break index = self.category_combo.findText(category) if index >= 0: self.category_combo.setCurrentIndex(index) else: self.category_combo.setCurrentText(category) field_type = self.rules.get("field_types", {}).get(field_name, "auto") index = self.type_combo.findText(field_type) if index >= 0: self.type_combo.setCurrentIndex(index) # 设置小数位数 decimals = self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位小数 self.decimal_spinbox.setValue(decimals) # 根据类型显示/隐藏小数位数设置 self.on_type_changed(field_type) # 隐藏状态 hidden = field_name in self.rules.get("hidden", []) self.hidden_checkbox.setChecked(hidden) tooltip = self.rules.get("tooltips", {}).get(field_name, "") self.tooltip_edit.setPlainText(tooltip) # 校验规则 validation = self.rules.get("validations", {}).get(field_name, {}) self.min_edit.setText(validation.get("min", "")) self.max_edit.setText(validation.get("max", "")) self.regex_edit.setText(validation.get("regex", "")) self.required_checkbox.setChecked(validation.get("required", False)) def save_current_field_changes(self, field_name): """保存当前字段的更改到规则字典""" if not field_name: return # 更新显示名称 display_name = self.display_name_edit.text().strip() if display_name: self.rules.setdefault("display_names", {})[field_name] = display_name elif field_name in self.rules.get("display_names", {}): del self.rules["display_names"][field_name] # 更新分组 category = self.category_combo.currentText().strip() if category and category != "未分类": # 从旧分组移除 for cat, fields in self.rules.get("categories", {}).items(): if field_name in fields: fields.remove(field_name) break # 添加到新分组 if category not in self.rules["categories"]: self.rules["categories"][category] = [] if field_name not in self.rules["categories"][category]: self.rules["categories"][category].append(field_name) # 更新字段类型 field_type = self.type_combo.currentText() if field_type != "auto": self.rules.setdefault("field_types", {})[field_name] = field_type # 更新小数位数(仅当类型为float时保存) if field_type == "float": decimals = self.decimal_spinbox.value() self.rules.setdefault("field_decimals", {})[field_name] = decimals elif field_name in self.rules.get("field_decimals", {}): # 如果类型不是float但存在小数位数设置,删除它 del self.rules["field_decimals"][field_name] elif field_name in self.rules.get("field_types", {}): del self.rules["field_types"][field_name] # 如果字段类型被删除,也删除小数位数设置 if field_name in self.rules.get("field_decimals", {}): del self.rules["field_decimals"][field_name] # 更新提示信息 tooltip = self.tooltip_edit.toPlainText().strip() if tooltip: self.rules.setdefault("tooltips", {})[field_name] = tooltip elif field_name in self.rules.get("tooltips", {}): del self.rules["tooltips"][field_name] # 更新隐藏状态 hidden = self.hidden_checkbox.isChecked() hidden_list = self.rules.setdefault("hidden", []) if hidden and field_name not in hidden_list: hidden_list.append(field_name) elif not hidden and field_name in hidden_list: hidden_list.remove(field_name) # 更新校验规则 validation = {} min_val = self.min_edit.text().strip() if min_val: validation["min"] = min_val max_val = self.max_edit.text().strip() if max_val: validation["max"] = max_val regex_val = self.regex_edit.text().strip() if regex_val: validation["regex"] = regex_val validation["required"] = self.required_checkbox.isChecked() if validation: self.rules.setdefault("validations", {})[field_name] = validation elif field_name in self.rules.get("validations", {}): del self.rules["validations"][field_name] def accept(self): """重写accept方法,确保保存当前字段的更改""" current_item = self.fields_list.currentItem() if current_item: field_name = current_item.text() self.save_current_field_changes(field_name) super().accept() def add_group(self): new_group, ok = QInputDialog.getText(self, "添加分组", "请输入新分组名称:") if ok and new_group.strip(): if new_group not in self.rules.setdefault("categories", {}): self.rules["categories"][new_group] = [] self.category_combo.addItem(new_group) def remove_group(self): current_group = self.category_combo.currentText() if current_group and current_group != "未分类": reply = QMessageBox.question(self, "确认删除", f"确定要删除分组 '{current_group}' 吗?") if reply == QMessageBox.StandardButton.Yes: if current_group in self.rules.get("categories", {}): del self.rules["categories"][current_group] self.category_combo.removeItem(self.category_combo.findText(current_group)) def get_updated_rules(self): """获取更新后的规则""" return self.rules class NoWheelSpinBox(QSpinBox): """禁用鼠标滚轮的SpinBox""" def wheelEvent(self, event): event.ignore() class NoWheelDoubleSpinBox(QDoubleSpinBox): """禁用鼠标滚轮的DoubleSpinBox""" def wheelEvent(self, event): event.ignore() def setDecimalsFromRules(self, decimals): """根据规则设置小数位数""" self.setDecimals(decimals) class ConfigField: """配置项元数据类""" def __init__(self, name, value, category="未分类", display_name=None, field_type="auto", decimals=None, tooltip="", hidden=False, line_number=None, original_lines=None, validation=None): self.name = name self.value = value self.category = category self.display_name = display_name or name self.field_type = field_type self.decimals = decimals # 小数位数(仅对float类型有效) self.tooltip = tooltip self.hidden = hidden self.line_number = line_number # 记录配置项在文件中的行号 self.original_lines = original_lines or [] # 保存原始行内容(包括注释) self.validation = validation or {} # 校验规则 def get_actual_field_type(self): """获取实际的字段类型""" if self.field_type != "auto": return self.field_type if isinstance(self.value, bool): return "bool" elif isinstance(self.value, int): return "int" elif isinstance(self.value, float): return "float" elif isinstance(self.value, (list, dict)): return "json" else: return "str" class ChangeConfirmDialog(QDialog): """变更确认对话框""" def __init__(self, changes, parent=None): super().__init__(parent) self.setWindowTitle("确认配置变更") self.setModal(True) self.setMinimumSize(600, 400) self.resize(600, 400) # 设置窗口标志,避免重影问题 self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowStaysOnTopHint) self.init_ui(changes) def init_ui(self, changes): layout = QVBoxLayout(self) # 标题 title_label = QLabel(f"以下 {len(changes)} 个配置项将被修改:") title_label.setStyleSheet("font-weight: bold; font-size: 14px;") layout.addWidget(title_label) # 变更列表 table = QTableWidget() table.setColumnCount(3) table.setHorizontalHeaderLabels(["配置项", "原值", "新值"]) table.setRowCount(len(changes)) for row, (field_name, (old_value, new_value)) in enumerate(changes.items()): table.setItem(row, 0, QTableWidgetItem(field_name)) table.setItem(row, 1, QTableWidgetItem(str(old_value))) table.setItem(row, 2, QTableWidgetItem(str(new_value))) table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) layout.addWidget(table) # 按钮 button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) class ConfigEditor(QMainWindow): def __init__(self): super().__init__() # 获取程序所在目录 if getattr(sys, 'frozen', False): # 打包后的可执行文件 self.program_dir = Path(sys.executable).parent else: # 直接运行脚本 self.program_dir = Path(__file__).parent # 规则文件固定位置:与主程序在同一目录 self.rules_file = str(self.program_dir / "config_editor_rules.json") # 用户设置文件 self.settings_file = str(self.program_dir / "config_editor_settings.json") # 加载用户设置 self.user_settings = self.load_user_settings() # 获取配置文件路径(如果有) self.config_file_path = self.user_settings.get("config_file_path", "") self.config_data = {} self.original_config_data = {} # 保存原始配置数据用于比较 self.config_fields = {} # 过滤后的配置项(不包含隐藏的) self.all_config_fields = {} # 所有配置项(包含隐藏的) self.dynamic_widgets = {} self.category_widgets = {} self.original_content = "" self.rules = {} self.config_lines = [] # 存储每行内容和对应的配置项 self.modified_fields = set() # 记录被修改的字段 self.field_containers = {} # 存储字段的容器 self.init_ui() self.load_rules() # 检查是否第一次启动(没有配置文件地址) if not self.config_file_path or not os.path.exists(self.config_file_path): # 首次启动,显示设置对话框 self.show_settings_dialog(initial_setup=True) else: # 非首次启动,加载配置文件 self.load_config() def load_user_settings(self): """加载用户设置""" try: if os.path.exists(self.settings_file): with open(self.settings_file, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: print(f"加载用户设置失败: {e}") return {} def save_user_settings(self): """保存用户设置""" try: self.user_settings.update({ "config_file_path": self.config_file_path, "last_used": str(Path(self.config_file_path).absolute()) if self.config_file_path and os.path.exists(self.config_file_path) else "" }) with open(self.settings_file, 'w', encoding='utf-8') as f: json.dump(self.user_settings, f, indent=2, ensure_ascii=False) except Exception as e: print(f"保存用户设置失败: {e}") def init_ui(self): self.setWindowTitle("配置管理页") self.setGeometry(100, 100, 1000, 700) self.resize(1000, 700) # 创建菜单栏 menubar = self.menuBar() # 文件菜单 file_menu = menubar.addMenu("文件") self.open_action = QAction("打开配置文件", self) self.open_action.triggered.connect(self.open_config_file) file_menu.addAction(self.open_action) self.settings_action = QAction("配置文件设置", self) self.settings_action.triggered.connect(lambda: self.show_settings_dialog(initial_setup=False)) file_menu.addAction(self.settings_action) file_menu.addSeparator() self.exit_action = QAction("退出", self) self.exit_action.triggered.connect(self.close) file_menu.addAction(self.exit_action) # 编辑菜单 edit_menu = menubar.addMenu("编辑") self.reload_action = QAction("重新加载", self) self.reload_action.triggered.connect(self.load_config) edit_menu.addAction(self.reload_action) self.save_action = QAction("保存配置", self) self.save_action.triggered.connect(self.save_config) edit_menu.addAction(self.save_action) # 工具菜单 tools_menu = menubar.addMenu("工具") self.rules_action = QAction("管理规则", self) self.rules_action.triggered.connect(self.manage_rules) tools_menu.addAction(self.rules_action) # 状态栏 self.statusBar().showMessage("就绪") central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(8) # 顶部信息栏 info_frame = QFrame() info_frame.setFrameShape(QFrame.Shape.StyledPanel) info_frame.setStyleSheet(""" QFrame { background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; } """) info_layout = QHBoxLayout(info_frame) info_layout.setContentsMargins(8, 4, 8, 4) self.path_label = QLabel() self.path_label.setStyleSheet("color: #666666; font-size: 11px;") info_layout.addWidget(self.path_label) info_layout.addStretch() main_layout.addWidget(info_frame) self.tab_widget = QTabWidget() main_layout.addWidget(self.tab_widget) self.init_category_tabs() # 按钮布局 button_layout = QHBoxLayout() button_layout.setSpacing(8) self.open_btn = QPushButton("打开文件") self.open_btn.clicked.connect(self.open_config_file) button_layout.addWidget(self.open_btn) self.settings_btn = QPushButton("配置文件设置") self.settings_btn.clicked.connect(lambda: self.show_settings_dialog(initial_setup=False)) button_layout.addWidget(self.settings_btn) button_layout.addStretch() self.save_btn = QPushButton("保存配置") self.save_btn.clicked.connect(self.save_config) self.save_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }") button_layout.addWidget(self.save_btn) self.reload_btn = QPushButton("重新加载") self.reload_btn.clicked.connect(self.load_config) button_layout.addWidget(self.reload_btn) self.rules_btn = QPushButton("管理规则") self.rules_btn.clicked.connect(self.manage_rules) button_layout.addWidget(self.rules_btn) # 显示/隐藏配置项按钮 self.toggle_hidden_btn = QPushButton("显示隐藏项") self.toggle_hidden_btn.setCheckable(True) self.toggle_hidden_btn.toggled.connect(self.toggle_hidden_fields) button_layout.addWidget(self.toggle_hidden_btn) main_layout.addLayout(button_layout) # 更新路径显示 self.update_path_display() def update_path_display(self): """更新路径显示""" if self.config_file_path and os.path.exists(self.config_file_path): abs_path = os.path.abspath(self.config_file_path) self.path_label.setText(f"配置文件: {abs_path}") self.path_label.setStyleSheet("color: #666666; font-size: 11px;") elif self.config_file_path: self.path_label.setText(f"配置文件不存在: {self.config_file_path} (点击'配置文件设置'调整)") self.path_label.setStyleSheet("color: #ff0000; font-size: 11px;") else: self.path_label.setText("未设置配置文件路径 (点击'配置文件设置'配置)") self.path_label.setStyleSheet("color: #ff9900; font-size: 11px;") def check_config_file(self): """检查配置文件是否存在""" if not self.config_file_path or not os.path.exists(self.config_file_path): return False return True def show_settings_dialog(self, initial_setup=False): """显示设置对话框 initial_setup: 是否是首次启动的设置 """ dialog = ConfigSettingsDialog(self.config_file_path, self) if initial_setup: # 首次启动,设置对话框模态且必须设置 dialog.setWindowTitle("首次启动 - 配置文件设置") dialog.setWindowModality(Qt.WindowModality.ApplicationModal) if dialog.exec() == QDialog.DialogCode.Accepted: settings = dialog.get_settings() # 更新设置 self.config_file_path = settings["config_file_path"] # 保存用户设置 self.save_user_settings() # 更新路径显示 self.update_path_display() # 重新加载配置 if self.check_config_file(): self.load_config() if not initial_setup: QMessageBox.information(self, "成功", "配置文件路径已更新并重新加载配置") else: QMessageBox.warning(self, "警告", "配置文件不存在,请检查路径") elif initial_setup: # 首次启动用户取消了设置,退出程序 QMessageBox.warning(self, "提示", "首次启动需要设置配置文件路径,程序将退出。") sys.exit(0) def open_config_file(self): """打开配置文件""" if self.modified_fields: reply = QMessageBox.question( self, "确认", "当前有未保存的修改,是否保存?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel ) if reply == QMessageBox.StandardButton.Cancel: return elif reply == QMessageBox.StandardButton.Yes: self.save_config() # 使用文件对话框选择文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择配置文件", self.config_file_path or str(Path.home()), "Python Files (*.py);;All Files (*)" ) if file_path: self.config_file_path = file_path self.save_user_settings() self.update_path_display() self.load_config() def init_category_tabs(self): self.category_widgets.clear() categories = list(self.rules.get("categories", {}).keys()) if not categories: categories = ["未分类"] for category in categories: self.create_category_tab(category) def create_category_tab(self, category): tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(4, 4, 4, 4) scroll = QScrollArea() scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) scroll_layout.setContentsMargins(8, 8, 8, 8) scroll_layout.setSpacing(6) self.category_widgets[category] = scroll_layout scroll.setWidget(scroll_widget) scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) layout.addWidget(scroll) self.tab_widget.addTab(tab, category) def load_rules(self): try: if os.path.exists(self.rules_file): with open(self.rules_file, 'r', encoding='utf-8') as f: self.rules = json.load(f) # 确保规则中有field_decimals字段 if "field_decimals" not in self.rules: self.rules["field_decimals"] = {} else: self.rules = { "categories": {}, "display_names": {}, "tooltips": {}, "field_types": {}, "field_decimals": {}, # 新增:小数位数设置 "hidden": [], # 新增隐藏列表 "validations": {} # 新增校验规则 } # 首次运行,尝试创建规则文件 try: self.save_rules() except Exception as e: print(f"创建规则文件失败: {e}") except Exception as e: print(f"加载规则文件失败: {e}") self.rules = { "categories": {}, "display_names": {}, "tooltips": {}, "field_types": {}, "field_decimals": {}, "hidden": [], "validations": {} } def save_rules(self): try: with open(self.rules_file, 'w', encoding='utf-8') as f: json.dump(self.rules, f, indent=2, ensure_ascii=False) except Exception as e: print(f"保存规则文件失败: {e}") def categorize_field(self, field_name): for category, fields in self.rules.get("categories", {}).items(): if field_name in fields: return category return "未分类" def get_display_name(self, field_name): return self.rules.get("display_names", {}).get(field_name, field_name) def get_tooltip(self, field_name): return self.rules.get("tooltips", {}).get(field_name, f"配置项: {field_name}") def get_field_type(self, field_name, value): if field_name in self.rules.get("field_types", {}): return self.rules["field_types"][field_name] if isinstance(value, bool): return "bool" elif isinstance(value, int): return "int" elif isinstance(value, float): return "float" elif isinstance(value, (list, dict)): return "json" else: return "str" def get_field_decimals(self, field_name): """获取字段的小数位数""" return self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位 def get_validation(self, field_name): """获取配置项的校验规则""" return self.rules.get("validations", {}).get(field_name, {}) def is_hidden(self, field_name): """检查配置项是否被标记为隐藏""" return field_name in self.rules.get("hidden", []) def parse_config_file(self): """解析配置文件,精确匹配每个配置项的位置和注释""" try: with open(self.config_file_path, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') tree = ast.parse(content) config_data = {} all_config_fields = {} # 第一遍:识别所有配置项及其位置 for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and target.id.isupper(): var_name = target.id # 获取配置项在文件中的位置 assignment_line = node.lineno - 1 # ast行号从1开始,我们使用0基索引 # 向上查找配置项上面的注释行 comment_lines = [] current_line = assignment_line - 1 while current_line >= 0: line = lines[current_line].strip() # 如果是注释行或空行,则包含在注释中 if line.startswith('#') or line == '': comment_lines.insert(0, lines[current_line]) current_line -= 1 # 如果是多行注释的开始 elif line.startswith("'''") or line.startswith('"""'): # 找到多行注释的起始位置 comment_start = current_line while comment_start >= 0 and not (lines[comment_start].strip().endswith("'''") or lines[comment_start].strip().endswith('"""')): comment_start -= 1 if comment_start >= 0: for i in range(comment_start, current_line + 1): comment_lines.insert(0, lines[i]) current_line = comment_start - 1 else: break else: break # 完整的配置项块(包括注释和赋值行) full_block = comment_lines + [lines[assignment_line]] # 如果赋值行有后续行(如多行字符串),添加它们 end_line = getattr(node, 'end_lineno', node.lineno) - 1 if end_line > assignment_line: for i in range(assignment_line + 1, end_line + 1): full_block.append(lines[i]) try: # 获取配置项的值 var_value = eval(compile(ast.Expression(node.value), '', 'eval')) config_data[var_name] = var_value # 获取字段类型和小数位数 field_type = self.get_field_type(var_name, var_value) decimals = self.get_field_decimals(var_name) if field_type == "float" else None all_config_fields[var_name] = ConfigField( name=var_name, value=var_value, category=self.categorize_field(var_name), display_name=self.get_display_name(var_name), field_type=field_type, decimals=decimals, tooltip=self.get_tooltip(var_name), hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, validation=self.get_validation(var_name) ) except: # 如果无法解析值,使用字符串表示 try: value_str = ast.get_source_segment(content, node.value) config_data[var_name] = value_str all_config_fields[var_name] = ConfigField( name=var_name, value=value_str, category=self.categorize_field(var_name), display_name=self.get_display_name(var_name), field_type="str", decimals=None, tooltip=self.get_tooltip(var_name), hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, validation=self.get_validation(var_name) ) except: config_data[var_name] = "无法解析的值" all_config_fields[var_name] = ConfigField( name=var_name, value="无法解析的值", category=self.categorize_field(var_name), display_name=self.get_display_name(var_name), field_type="str", decimals=None, tooltip=self.get_tooltip(var_name), hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, validation=self.get_validation(var_name) ) return config_data, all_config_fields, content except Exception as e: QMessageBox.critical(self, "错误", f"解析配置文件失败:{str(e)}") return {}, {}, "" def create_field_widget(self, config_field): field_type = config_field.get_actual_field_type() if field_type == "bool": widget = QCheckBox() widget.setChecked(bool(config_field.value)) # 连接状态改变信号 widget.stateChanged.connect(lambda state, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "int": widget = NoWheelSpinBox() # 使用禁用了滚轮的SpinBox widget.setRange(-1000000, 1000000) if config_field.value is not None: widget.setValue(int(config_field.value)) # 连接值改变信号 widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "float": widget = NoWheelDoubleSpinBox() # 使用禁用了滚轮的DoubleSpinBox widget.setRange(-1000000.0, 1000000.0) # 根据规则设置小数位数 decimals = config_field.decimals or 6 # 默认6位 widget.setDecimals(decimals) if config_field.value is not None: widget.setValue(float(config_field.value)) # 连接值改变信号 widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "json": widget = QTextEdit() widget.setMaximumHeight(120) if config_field.value is not None: # 对于字典,使用漂亮的格式化显示 formatted_json = json.dumps(config_field.value, indent=4, ensure_ascii=False) widget.setPlainText(formatted_json) # 连接文本改变信号 widget.textChanged.connect(lambda fn=config_field.name: self.on_field_modified(fn)) else: widget = QLineEdit() if config_field.value is not None: widget.setText(str(config_field.value)) # 连接文本改变信号 widget.textChanged.connect(lambda text, fn=config_field.name: self.on_field_modified(fn)) # 添加校验提示到tooltip validation_tooltip = self.get_validation_tooltip(config_field) full_tooltip = config_field.tooltip if validation_tooltip: full_tooltip += f"\n\n校验规则:\n{validation_tooltip}" widget.setToolTip(full_tooltip) widget.setMinimumWidth(300) return widget def get_validation_tooltip(self, config_field): """生成校验规则的提示信息""" validation = config_field.validation if not validation: return "" rules = [] if validation.get("required"): rules.append("• 必填项") if "min" in validation: rules.append(f"• 最小值: {validation['min']}") if "max" in validation: rules.append(f"• 最大值: {validation['max']}") if "regex" in validation: rules.append(f"• 正则表达式: {validation['regex']}") return "\n".join(rules) def on_field_modified(self, field_name): """当字段值被修改时的处理""" if field_name not in self.modified_fields: self.modified_fields.add(field_name) # 高亮显示修改的字段 self.highlight_modified_field(field_name) # 更新状态栏显示修改数量 self.update_status_bar() def highlight_modified_field(self, field_name): """高亮显示被修改的字段,但不覆盖子控件样式""" if field_name in self.field_containers: container = self.field_containers[field_name] # 只修改背景色,使用更精确的选择器确保不影响子控件 container.setStyleSheet(""" QFrame#field_container { background-color: #fff9c4; } """) def clear_field_highlight(self, field_name): """清除字段的高亮显示,恢复原有样式""" if field_name in self.field_containers: container = self.field_containers[field_name] # 恢复原始样式,不影响子控件 container.setStyleSheet(""" QFrame#field_container { background-color: transparent; } """) def update_status_bar(self): """更新状态栏显示修改数量""" if self.modified_fields: self.statusBar().showMessage(f"已修改 {len(self.modified_fields)} 个配置项") else: self.statusBar().showMessage("就绪") def create_field_editor(self, config_field): """创建字段编辑器,返回容器和控件""" # 创建容器框架 container = QFrame() container.setObjectName("field_container") # 设置对象名用于样式选择 container.setFrameShape(QFrame.Shape.StyledPanel) # 设置基础样式,使用对象名选择器确保只影响容器本身 container.setStyleSheet(""" QFrame#field_container { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } """) layout = QHBoxLayout(container) layout.setContentsMargins(8, 4, 8, 4) layout.setSpacing(8) # 标签 label = QLabel(config_field.display_name) label.setMinimumWidth(180) # 添加校验指示器(如果配置项有校验规则) validation_indicator = "" if config_field.validation: if config_field.validation.get("required"): validation_indicator = " *" label.setStyleSheet("color: #d32f2f; font-weight: bold;") else: label.setStyleSheet("color: #1976d2;") label.setText(f"{config_field.display_name}{validation_indicator}") label.setToolTip(self.get_validation_tooltip(config_field)) # 如果配置项是隐藏的,添加视觉提示 if config_field.hidden: label.setStyleSheet("color: #999999; font-style: italic;") label.setText(f"{config_field.display_name} [隐藏]") layout.addWidget(label) # 控件 widget = self.create_field_widget(config_field) # 如果配置项是隐藏的,禁用控件 if config_field.hidden: widget.setEnabled(False) widget.setStyleSheet("background-color: #f0f0f0; color: #999999;") layout.addWidget(widget) # 变量名显示 name_label = QLabel(f"({config_field.name})") name_label.setStyleSheet("color: #666666; font-size: 10px; font-style: italic;") name_label.setMinimumWidth(120) name_label.setToolTip(f"配置变量名: {config_field.name}") layout.addWidget(name_label) layout.addStretch() # 存储容器引用 self.field_containers[config_field.name] = container return container, widget def generate_dynamic_ui(self, show_hidden=False): """生成动态UI,可选择是否显示隐藏的配置项""" self.dynamic_widgets.clear() self.modified_fields.clear() # 重置修改记录 self.field_containers.clear() # 清空容器引用 for category, layout in self.category_widgets.items(): while layout.count(): child = layout.takeAt(0) if child.widget(): child.widget().deleteLater() # 根据是否显示隐藏项,选择使用哪个配置项集合 config_fields_to_use = self.all_config_fields if show_hidden else self.config_fields categorized_fields = {} for field_name, config_field in config_fields_to_use.items(): category = config_field.category if category not in categorized_fields: categorized_fields[category] = [] categorized_fields[category].append(config_field) for category, fields in categorized_fields.items(): if category not in self.category_widgets: self.create_category_tab(category) layout = self.category_widgets[category] fields.sort(key=lambda x: x.name) # 使用GroupBox,更清晰的分组 group_box = QGroupBox(f"{category}配置项") group_layout = QVBoxLayout(group_box) group_layout.setSpacing(6) for config_field in fields: container, widget = self.create_field_editor(config_field) group_layout.addWidget(container) self.dynamic_widgets[config_field.name] = (widget, config_field.get_actual_field_type()) group_layout.addStretch() layout.addWidget(group_box) def load_config(self): try: # 检查配置文件是否存在 if not self.check_config_file(): QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}") return self.config_data, self.all_config_fields, self.original_content = self.parse_config_file() if not self.config_data: QMessageBox.warning(self, "警告", "未找到任何配置项") return # 保存原始配置数据用于比较 self.original_config_data = self.config_data.copy() # 过滤掉隐藏的配置项 self.config_fields = {k: v for k, v in self.all_config_fields.items() if not v.hidden} # 根据当前显示状态生成UI show_hidden = getattr(self, 'show_hidden', False) self.generate_dynamic_ui(show_hidden) hidden_count = len(self.all_config_fields) - len(self.config_fields) self.statusBar().showMessage(f"成功加载 {len(self.config_fields)} 个配置项 ({hidden_count} 个已隐藏)") except Exception as e: QMessageBox.critical(self, "错误", f"加载配置文件失败:{str(e)}") self.statusBar().showMessage("加载配置文件失败") def toggle_hidden_fields(self, checked): """切换显示/隐藏配置项""" self.show_hidden = checked self.generate_dynamic_ui(checked) # 更新按钮文本 if checked: self.toggle_hidden_btn.setText("隐藏隐藏项") self.statusBar().showMessage("已显示所有配置项(包括隐藏的)") else: self.toggle_hidden_btn.setText("显示隐藏项") self.statusBar().showMessage("已隐藏标记为隐藏的配置项") def manage_rules(self): dialog = ConfigManagementWindow(self.rules, self.all_config_fields, self) if dialog.exec() == QDialog.DialogCode.Accepted: updated_rules = dialog.get_updated_rules() self.rules.update(updated_rules) self.save_rules() self.load_config() QMessageBox.information(self, "成功", "规则已更新并应用!") def collect_ui_data(self): """从UI收集数据""" ui_data = {} for field_name, (widget, field_type) in self.dynamic_widgets.items(): # 跳过隐藏且禁用的配置项 if not widget.isEnabled(): continue if field_type == "bool": ui_data[field_name] = widget.isChecked() elif field_type == "int": ui_data[field_name] = widget.value() elif field_type == "float": ui_data[field_name] = widget.value() elif field_type == "json": try: text = widget.toPlainText().strip() if text: ui_data[field_name] = json.loads(text) else: ui_data[field_name] = {} except json.JSONDecodeError: QMessageBox.warning(self, "JSON格式错误", f"配置项 {field_name} 的JSON格式不正确") return None else: text = widget.text().strip() ui_data[field_name] = text return ui_data def validate_config_data(self, config_data): """校验配置数据""" errors = [] for field_name, value in config_data.items(): config_field = self.all_config_fields.get(field_name) if not config_field: continue validation = config_field.validation if not validation: continue # 必填校验 if validation.get("required") and (value is None or value == ""): errors.append(f"配置项 '{config_field.display_name}' 是必填项") continue # 数字范围校验 if isinstance(value, (int, float)): if "min" in validation: try: min_val = float(validation["min"]) if value < min_val: errors.append(f"配置项 '{config_field.display_name}' 的值不能小于 {min_val}") except ValueError: pass if "max" in validation: try: max_val = float(validation["max"]) if value > max_val: errors.append(f"配置项 '{config_field.display_name}' 的值不能大于 {max_val}") except ValueError: pass # 字符串正则校验 elif isinstance(value, str) and "regex" in validation: try: if not re.match(validation["regex"], value): errors.append(f"配置项 '{config_field.display_name}' 的值不符合格式要求") except re.error: errors.append(f"配置项 '{config_field.display_name}' 的正则表达式格式错误") return errors def get_changes(self, new_data): """获取配置变更列表""" changes = {} for field_name, new_value in new_data.items(): old_value = self.original_config_data.get(field_name) if old_value != new_value: changes[field_name] = (old_value, new_value) return changes def format_dict_value(self, value, original_lines=None): """专门格式化字典值,保持多行格式""" if not isinstance(value, dict): return json.dumps(value, ensure_ascii=False) # 如果有原始行信息,尝试保持原始格式 if original_lines and len(original_lines) > 1: # 检查原始格式是否是漂亮的多行格式 first_line = original_lines[0].strip() if first_line.endswith('{') and len(original_lines) > 2: # 原始是多行格式,我们也使用多行格式 result = "{\n" for i, (key, val) in enumerate(value.items()): # 使用4个空格缩进 result += f' "{key}": {json.dumps(val, ensure_ascii=False)}' if i < len(value) - 1: result += ",\n" else: result += "\n" result += "}" return result # 如果没有原始格式信息或不是多行格式,使用漂亮的JSON格式 return json.dumps(value, indent=4, ensure_ascii=False) def format_value(self, value, config_field=None): """格式化值,保持与Python兼容的格式""" if value is None: return "None" elif isinstance(value, bool): return "True" if value else "False" elif isinstance(value, (int, float)): # 根据小数位数格式化浮点数 if isinstance(value, float) and config_field and config_field.decimals is not None: # 格式化浮点数,保留指定小数位数 format_str = f"{{:.{config_field.decimals}f}}" return format_str.format(value) else: return str(value) elif isinstance(value, str): # 检查字符串是否已经用引号包围 if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): return value # 检查字符串是否包含特殊字符 elif any(char in value for char in [' ', '\t', '\n', '#', '=']): return f'"{value}"' else: return f'"{value}"' elif isinstance(value, dict): # 对于字典,使用专门的格式化函数 return self.format_dict_value(value, config_field.original_lines if config_field else None) elif isinstance(value, list): return json.dumps(value, ensure_ascii=False, indent=4) else: return repr(value) def save_config(self): """保存配置,保留所有注释、空格和格式""" try: # 检查配置文件是否存在 if not self.check_config_file(): QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}") return # 收集UI数据 new_data = self.collect_ui_data() if new_data is None: # JSON解析错误 return # 校验配置数据 validation_errors = self.validate_config_data(new_data) if validation_errors: error_msg = "配置校验失败:\n\n" + "\n".join(validation_errors) QMessageBox.warning(self, "配置校验失败", error_msg) return # 获取变更列表 changes = self.get_changes(new_data) if not changes: QMessageBox.information(self, "提示", "没有检测到任何配置变更") return # 显示变更确认对话框 dialog = ChangeConfirmDialog(changes, self) result = dialog.exec() if result != QDialog.DialogCode.Accepted: self.statusBar().showMessage("用户取消了保存操作") return # 读取原始文件内容 with open(self.config_file_path, 'r', encoding='utf-8') as f: original_content = f.read() original_lines = original_content.split('\n') # 构建新文件内容 - 只修改值部分,保留所有其他内容 new_lines = original_lines.copy() # 对每个变更的配置项,找到其位置并修改值,保留空格和格式 for field_name, (old_value, new_value) in changes.items(): config_field = self.all_config_fields.get(field_name) if not config_field or config_field.line_number is None: continue # 获取配置项的原始块 original_block = config_field.original_lines # 查找赋值行在原始块中的位置 assignment_line_index = -1 for i, line in enumerate(original_block): stripped = line.strip() if stripped.startswith(field_name) and '=' in stripped: assignment_line_index = i break if assignment_line_index >= 0: # 获取原始赋值行的完整内容(包括注释等) original_assignment_line = original_block[assignment_line_index] # 使用改进的正则表达式匹配赋值语句 # 这个正则表达式会捕获: # 1. 变量名前的所有空格 # 2. 变量名 # 3. 等号前的空格 # 4. 等号 # 5. 等号后的空格 # 6. 原始值(直到注释或行尾) # 7. 行尾注释(如果有) pattern = rf'^(\s*)({re.escape(field_name)})(\s*)(=)(\s*)(.*?)(\s*(#.*)?)$' match = re.match(pattern, original_assignment_line) if match: # 获取匹配的各个部分 leading_spaces = match.group(1) # 变量名前的空格 var_name_part = match.group(2) # 变量名 spaces_before_eq = match.group(3) # 等号前的空格 eq_sign = match.group(4) # 等号 spaces_after_eq = match.group(5) # 等号后的空格 old_value_part = match.group(6) # 原始值 trailing_comment = match.group(7) if match.group(7) else '' # 行尾注释 # 格式化新值 formatted_value = self.format_value(new_value, config_field) # 构建新行,保持原始的空格格式 new_line = (f"{leading_spaces}{var_name_part}{spaces_before_eq}" f"{eq_sign}{spaces_after_eq}{formatted_value}" f"{trailing_comment}") # 找到赋值行在原始文件中的实际位置 # assignment_line_in_file 是赋值行在原始文件中的行号(从0开始) assignment_line_in_file = config_field.line_number + assignment_line_index if assignment_line_in_file < len(new_lines): new_lines[assignment_line_in_file] = new_line else: # 如果正则匹配失败,使用简单的替换方法 # 查找赋值行在原始文件中的位置 for i in range(len(new_lines)): line = new_lines[i] stripped = line.strip() if stripped.startswith(field_name) and '=' in stripped: # 使用更简单的正则表达式 pattern = rf'^(\s*{re.escape(field_name)}\s*=\s*).*$' match = re.match(pattern, line.rstrip()) if match: # 格式化新值 formatted_value = self.format_value(new_value, config_field) # 检查是否有行尾注释 comment_pos = line.find('#') if comment_pos > 0: # 有行尾注释,保留注释 line_comment = line[comment_pos:] prefix = match.group(1) new_line = prefix + formatted_value + line_comment else: # 没有行尾注释 prefix = match.group(1) new_line = prefix + formatted_value new_lines[i] = new_line break # 写入文件,保持原有的换行符格式 with open(self.config_file_path, 'w', encoding='utf-8', newline='') as f: f.write('\n'.join(new_lines)) # 清除所有高亮显示 for field_name in self.modified_fields.copy(): self.clear_field_highlight(field_name) self.modified_fields.remove(field_name) # 重新加载配置 self.load_config() self.statusBar().showMessage(f"配置文件保存成功,修改了 {len(changes)} 个配置项") # 使用定时器延迟显示成功消息 QTimer.singleShot(100, lambda: QMessageBox.information( self, "成功", f"配置文件保存成功!\n修改了 {len(changes)} 个配置项" )) except Exception as e: self.statusBar().showMessage("保存配置文件失败") QMessageBox.critical(self, "错误", f"保存配置文件失败:{str(e)}\n\n错误详情:{traceback.format_exc()}") def main(): app = QApplication(sys.argv) app.setStyle('Fusion') # 创建编辑器 editor = ConfigEditor() editor.show() sys.exit(app.exec()) if __name__ == '__main__': main()