import sys import os import ast import json import re import traceback import datetime import shutil 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, QListWidgetItem, QComboBox, QInputDialog, QFileDialog, QMenuBar, QMenu) from PyQt6.QtCore import Qt, QSize, QTimer, QEvent, QMimeData from PyQt6.QtGui import QAction, QDrag, QColor, QBrush, QFont 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_type_combo = QComboBox() self.search_type_combo.addItems(["变量名", "显示名称"]) self.search_type_combo.setMaximumWidth(80) search_layout.addWidget(self.search_type_combo) 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) # 修改:添加分组显示说明 list_label = QLabel("配置项列表(上方为未隐藏项,下方为隐藏项):") list_label.setStyleSheet("color: #333333; font-weight: bold;") left_layout.addWidget(list_label) 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("隐藏此配置项(不在主界面显示)") self.hidden_checkbox.stateChanged.connect(self.on_hidden_changed) 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) # 批量操作 batch_group = QGroupBox("批量操作") batch_layout = QVBoxLayout(batch_group) batch_layout.setContentsMargins(8, 8, 8, 8) batch_btn_layout = QHBoxLayout() self.show_all_btn = QPushButton("全部显示") self.show_all_btn.clicked.connect(self.show_all_fields) self.hide_all_btn = QPushButton("全部隐藏") self.hide_all_btn.clicked.connect(self.hide_all_fields) batch_btn_layout.addWidget(self.show_all_btn) batch_btn_layout.addWidget(self.hide_all_btn) batch_btn_layout.addStretch() batch_layout.addLayout(batch_btn_layout) # === 新增:导入导出功能 === import_export_group = QGroupBox("导入导出规则文件") import_export_layout = QVBoxLayout(import_export_group) import_export_layout.setContentsMargins(8, 8, 8, 8) import_export_btn_layout = QHBoxLayout() self.import_rules_btn = QPushButton("导入规则文件") self.import_rules_btn.clicked.connect(self.import_rules_file) self.export_rules_btn = QPushButton("导出规则文件") self.export_rules_btn.clicked.connect(self.export_rules_file) import_export_btn_layout.addWidget(self.import_rules_btn) import_export_btn_layout.addWidget(self.export_rules_btn) import_export_btn_layout.addStretch() import_export_layout.addLayout(import_export_btn_layout) right_layout.addWidget(import_export_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() # 获取隐藏字段列表 hidden_fields = self.rules.get("hidden", []) # 分离未隐藏和已隐藏的字段 visible_fields = [] hidden_fields_list = [] for field_name in sorted(self.config_fields.keys()): if field_name in hidden_fields: hidden_fields_list.append(field_name) else: visible_fields.append(field_name) # 添加分隔项 if visible_fields and hidden_fields_list: # 添加分隔符(未隐藏部分) self.create_separator_item("未隐藏的配置项") # 添加未隐藏的字段(高亮显示) for field_name in visible_fields: self.create_field_item(field_name, is_hidden=False) # 添加分隔项 if visible_fields and hidden_fields_list: # 添加分隔符(隐藏部分) self.create_separator_item("已隐藏的配置项") # 添加已隐藏的字段(灰色显示) for field_name in hidden_fields_list: self.create_field_item(field_name, is_hidden=True) # 如果没有配置项,显示提示 if not visible_fields and not hidden_fields_list: item = QListWidgetItem("没有配置项") self.fields_list.addItem(item) item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 item.setForeground(QBrush(QColor("#999999"))) self.update_fields_count() # 加载分组 self.category_combo.clear() categories = list(self.rules.get("categories", {}).keys()) # 确保"未分类"分组始终存在 if "未分类" not in categories: categories.append("未分类") if categories: for category in sorted(categories): self.category_combo.addItem(category) def create_separator_item(self, text): """创建分隔项""" item = QListWidgetItem(f"--- {text} ---") self.fields_list.addItem(item) item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 item.setForeground(QBrush(QColor("#666666"))) font = item.font() font.setItalic(True) font.setBold(True) item.setFont(font) # 设置背景色 item.setBackground(QBrush(QColor("#f0f0f0"))) return item def create_field_item(self, field_name, is_hidden=False): """创建字段项""" item = QListWidgetItem(field_name) self.fields_list.addItem(item) # 获取显示名称用于搜索 display_name = self.rules.get("display_names", {}).get(field_name, field_name) # 设置自定义数据:变量名和显示名称 item.setData(Qt.ItemDataRole.UserRole, field_name) # 变量名 item.setData(Qt.ItemDataRole.UserRole + 1, display_name) # 显示名称 if is_hidden: # 隐藏字段:灰色,斜体 item.setForeground(QBrush(QColor("#999999"))) font = item.font() font.setItalic(True) item.setFont(font) item.setToolTip(f"已隐藏: {field_name} ({display_name})") else: # 未隐藏字段:高亮显示(浅绿色背景) item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 font = item.font() font.setBold(True) item.setFont(font) item.setToolTip(f"未隐藏: {field_name} ({display_name})") return item def filter_fields(self, search_text): """根据搜索文本过滤配置项列表(保持分组),支持变量名和显示名称搜索""" search_text = search_text.strip().lower() search_type = self.search_type_combo.currentText() # 获取搜索类型 if not search_text: # 如果搜索文本为空,显示所有项 for i in range(self.fields_list.count()): item = self.fields_list.item(i) # 跳过分隔符项 if not self.is_separator_item(item): item.setHidden(False) else: # 模糊匹配:根据搜索类型进行匹配 for i in range(self.fields_list.count()): item = self.fields_list.item(i) # 跳过分隔符项 if not self.is_separator_item(item): # 根据搜索类型获取要匹配的文本 if search_type == "变量名": # 搜索变量名 field_name = item.data(Qt.ItemDataRole.UserRole) match_text = field_name.lower() if field_name else "" else: # 显示名称 # 搜索显示名称 display_name = item.data(Qt.ItemDataRole.UserRole + 1) match_text = display_name.lower() if display_name else "" # 模糊匹配:检查搜索文本是否在匹配文本中 if search_text in match_text: item.setHidden(False) else: item.setHidden(True) self.update_fields_count() def is_separator_item(self, item): """检查是否是分隔符项""" text = item.text() return text.startswith("---") and text.endswith("---") def clear_search(self): """清除搜索框内容""" self.search_edit.clear() def update_fields_count(self): """更新配置项计数显示""" total_count = 0 visible_count = 0 hidden_count = 0 for i in range(self.fields_list.count()): item = self.fields_list.item(i) # 跳过分隔符项 if not self.is_separator_item(item): total_count += 1 if not item.isHidden(): visible_count += 1 # 统计隐藏状态 if "已隐藏" in item.toolTip(): hidden_count += 1 visible_unhidden = visible_count - hidden_count visible_hidden = hidden_count # 显示搜索类型信息 search_type = self.search_type_combo.currentText() self.fields_count_label.setText(f"总计: {total_count} 个配置项 (显示: {visible_unhidden} 未隐藏, {visible_hidden} 隐藏) - 搜索类型: {search_type}") 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_hidden_changed(self, state): """当隐藏状态改变时,更新左侧列表项的外观和位置""" current_item = self.fields_list.currentItem() if not current_item or self.is_separator_item(current_item): return field_name = current_item.data(Qt.ItemDataRole.UserRole) display_name = current_item.data(Qt.ItemDataRole.UserRole + 1) is_hidden = (state == Qt.CheckState.Checked.value) # 更新工具提示 if is_hidden: current_item.setToolTip(f"已隐藏: {field_name} ({display_name})") else: current_item.setToolTip(f"未隐藏: {field_name} ({display_name})") # 更新外观 if is_hidden: # 改为隐藏样式 current_item.setForeground(QBrush(QColor("#999999"))) font = current_item.font() font.setItalic(True) font.setBold(False) current_item.setFont(font) current_item.setBackground(QBrush()) # 清除背景色 else: # 改为未隐藏样式 current_item.setForeground(QBrush()) # 恢复默认前景色 font = current_item.font() font.setItalic(False) font.setBold(True) current_item.setFont(font) current_item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 def on_field_selected(self, current, previous): # 保存前一个字段的更改 if previous is not None and not self.is_separator_item(previous): previous_field_name = previous.data(Qt.ItemDataRole.UserRole) self.save_current_field_changes(previous_field_name) if not current or self.is_separator_item(current): # 清空右侧面板 self.name_label.setText("") self.display_name_edit.clear() self.category_combo.setCurrentText("") self.type_combo.setCurrentText("auto") self.decimal_spinbox.setValue(2) self.hidden_checkbox.setChecked(False) self.tooltip_edit.clear() self.min_edit.clear() self.max_edit.clear() self.regex_edit.clear() self.required_checkbox.setChecked(False) return field_name = current.data(Qt.ItemDataRole.UserRole) display_name = current.data(Qt.ItemDataRole.UserRole + 1) self.name_label.setText(field_name) # 从规则中获取显示名称,如果规则中没有则使用当前存储的显示名称 rule_display_name = self.rules.get("display_names", {}).get(field_name, display_name) self.display_name_edit.setText(rule_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 == "未分类": # 从所有分组中移除该字段 for cat, fields in self.rules.setdefault("categories", {}).items(): if cat != "未分类" and field_name in fields: fields.remove(field_name) # 确保在"未分类"分组中 if "未分类" not in self.rules["categories"]: self.rules["categories"]["未分类"] = [] if field_name not in self.rules["categories"]["未分类"]: self.rules["categories"]["未分类"].append(field_name) elif category: # 其他分组 # 从所有分组中移除该字段 for cat, fields in self.rules.setdefault("categories", {}).items(): if field_name in fields: fields.remove(field_name) # 确保"未分类"分组存在 if "未分类" not in self.rules["categories"]: self.rules["categories"]["未分类"] = [] # 添加到新分组 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 and not self.is_separator_item(current_item): field_name = current_item.data(Qt.ItemDataRole.UserRole) 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}' 吗?\n该分组中的所有配置项将被移到'未分类'分组。") if reply == QMessageBox.StandardButton.Yes: if current_group in self.rules.get("categories", {}): # 将该分组中的配置项移到"未分类"分组 fields_to_move = self.rules["categories"][current_group] if "未分类" not in self.rules["categories"]: self.rules["categories"]["未分类"] = [] for field_name in fields_to_move: if field_name not in self.rules["categories"]["未分类"]: self.rules["categories"]["未分类"].append(field_name) # 删除分组 del self.rules["categories"][current_group] # 从下拉框中移除 index = self.category_combo.findText(current_group) if index >= 0: self.category_combo.removeItem(index) # 设置当前选中的分组为"未分类" self.category_combo.setCurrentText("未分类") def show_all_fields(self): """批量显示所有配置项(取消所有隐藏)""" reply = QMessageBox.question(self, "确认", "确定要取消所有配置项的隐藏状态吗?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: # 清空隐藏列表 self.rules["hidden"] = [] # 重新加载数据以更新显示 self.load_data() QMessageBox.information(self, "完成", "已取消所有配置项的隐藏状态") def hide_all_fields(self): """批量隐藏所有配置项""" reply = QMessageBox.question(self, "确认", "确定要隐藏所有配置项吗?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: # 获取所有配置项的名称 all_fields = list(self.config_fields.keys()) # 将所有配置项添加到隐藏列表 self.rules["hidden"] = all_fields # 重新加载数据以更新显示 self.load_data() QMessageBox.information(self, "完成", "已隐藏所有配置项") # === 新增:导入规则文件功能 === def import_rules_file(self): """导入规则文件""" # 确认保存当前更改 current_item = self.fields_list.currentItem() if current_item and not self.is_separator_item(current_item): field_name = current_item.data(Qt.ItemDataRole.UserRole) self.save_current_field_changes(field_name) # 选择要导入的规则文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择规则文件", str(Path.home()), "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return try: # 读取规则文件 with open(file_path, 'r', encoding='utf-8') as f: imported_rules = json.load(f) # 验证规则文件格式 if not isinstance(imported_rules, dict): QMessageBox.warning(self, "导入失败", "规则文件格式不正确") return # 显示导入确认对话框 reply = QMessageBox.question( self, "确认导入", f"确定要导入规则文件吗?\n\n文件: {os.path.basename(file_path)}\n\n" f"这将覆盖当前的规则设置。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # 完全替换现有规则 self.rules.clear() self.rules.update(imported_rules) # 确保规则结构完整 self.ensure_rule_structure() # 重新加载数据 self.load_data() QMessageBox.information( self, "导入成功", f"规则文件导入成功!\n\n" f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" f"已导入 {len(self.rules.get('hidden', []))} 个隐藏设置" ) except json.JSONDecodeError: QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") except Exception as e: QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") # === 新增:导出规则文件功能 === def export_rules_file(self): """导出规则文件""" # 确认保存当前更改 current_item = self.fields_list.currentItem() if current_item and not self.is_separator_item(current_item): field_name = current_item.data(Qt.ItemDataRole.UserRole) self.save_current_field_changes(field_name) # 选择导出位置和文件名 default_name = f"config_editor_rules_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" file_path, _ = QFileDialog.getSaveFileName( self, "导出规则文件", default_name, "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return # 确保文件扩展名是.json if not file_path.lower().endswith('.json'): file_path += '.json' try: # 准备导出的规则数据 export_data = self.rules.copy() # 添加导出信息 export_data["_export_info"] = { "export_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "export_version": "1.0", "note": "配置文件编辑器规则文件" } # 添加统计信息 export_data["_statistics"] = { "total_fields": len(self.all_fields), "display_names_count": len(self.rules.get("display_names", {})), "categories_count": len(self.rules.get("categories", {})), "hidden_fields_count": len(self.rules.get("hidden", [])), "field_types_count": len(self.rules.get("field_types", {})), "validations_count": len(self.rules.get("validations", {})) } # 写入文件 with open(file_path, 'w', encoding='utf-8') as f: json.dump(export_data, f, indent=2, ensure_ascii=False) QMessageBox.information( self, "导出成功", f"规则文件已成功导出到:\n{file_path}\n\n" f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" f"规则数量:{len(export_data) - 2}" # 减去_export_info和_statistics ) except Exception as e: QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(e)}") def ensure_rule_structure(self): """确保规则结构完整""" # 确保规则中有必要的字段 if "categories" not in self.rules: self.rules["categories"] = {"未分类": []} if "未分类" not in self.rules["categories"]: self.rules["categories"]["未分类"] = [] if "display_names" not in self.rules: self.rules["display_names"] = {} if "field_types" not in self.rules: self.rules["field_types"] = {} if "field_decimals" not in self.rules: self.rules["field_decimals"] = {} if "tooltips" not in self.rules: self.rules["tooltips"] = {} if "hidden" not in self.rules: self.rules["hidden"] = [] if "validations" not in self.rules: self.rules["validations"] = {} if "field_order" not in self.rules: self.rules["field_order"] = {} if "last_saved_values" not in self.rules: self.rules["last_saved_values"] = {} 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, last_saved_value=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 {} # 校验规则 self.last_saved_value = last_saved_value # 新增:上次保存的值 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.dragging_field = None # 当前正在拖拽的字段名 self.drag_start_pos = None # 拖拽起始位置 self.drop_target_field = None # 拖拽目标字段名 self.field_order = {} # 存储每个分组中字段的顺序,格式:{category: [field1, field2, ...]} 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_icon(self): """加载程序图标 - 创建一个简单的CE色块图标""" from PyQt6.QtGui import QIcon, QPixmap, QPainter, QFont from PyQt6.QtCore import Qt import os # 方法1: 尝试加载本地图标文件 icon_paths = [ os.path.join(self.program_dir, "logo.ico"), os.path.join(self.program_dir, "logo.png"), os.path.join(self.program_dir, "resources", "logo.ico"), os.path.join(self.program_dir, "resources", "logo.png"), ] for path in icon_paths: if os.path.exists(path): try: icon = QIcon(path) if not icon.isNull(): print(f"加载图标: {path}") return icon except Exception as e: print(f"加载图标失败 {path}: {e}") # 方法2: 创建简单的CE色块图标(无需外部文件) try: # 创建不同尺寸的图标以确保在各种场景下都能清晰显示 sizes = [16, 32, 48, 64, 128] icon = QIcon() for size in sizes: pixmap = QPixmap(size, size) pixmap.fill(Qt.GlobalColor.transparent) # 在pixmap上绘制 from PyQt6.QtGui import QPen, QBrush, QColor painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # 绘制蓝色圆角矩形背景 bg_color = QColor(33, 150, 243) # 蓝色 painter.setBrush(QBrush(bg_color)) painter.setPen(Qt.PenStyle.NoPen) # 根据尺寸调整圆角 corner_radius = size // 8 painter.drawRoundedRect(0, 0, size, size, corner_radius, corner_radius) # 绘制白色CE文字 painter.setPen(QPen(Qt.GlobalColor.white, 1)) font_size = max(size // 3, 8) # 确保字体不会太小 painter.setFont(QFont("Arial", font_size, QFont.Weight.Bold)) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "CE") painter.end() # 将pixmap添加到图标 icon.addPixmap(pixmap) return icon except Exception as e: print(f"创建图标失败: {e}") return QIcon() # 返回空图标 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) # 设置窗口图标(显示在标题栏和任务栏) self.setWindowIcon(self.load_icon()) # 创建菜单栏 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) # === 新增:在工具菜单中添加导入导出规则文件 === tools_menu.addSeparator() self.import_rules_action = QAction("导入规则文件", self) self.import_rules_action.triggered.connect(self.import_rules_file) tools_menu.addAction(self.import_rules_action) self.export_rules_action = QAction("导出规则文件", self) self.export_rules_action.triggered.connect(self.export_rules_file) tools_menu.addAction(self.export_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: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; } """) info_layout = QHBoxLayout(info_frame) info_layout.setContentsMargins(12, 8, 12, 8) self.path_label = QLabel() self.path_label.setStyleSheet(""" QLabel { color: #495057; font-size: 11px; font-family: 'Consolas', 'Monaco', monospace; } """) 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) # 新增:一键隐藏所有配置项按钮 self.hide_all_btn = QPushButton("一键隐藏所有项") self.hide_all_btn.clicked.connect(self.hide_all_fields) self.hide_all_btn.setStyleSheet("QPushButton { background-color: #ff9800; color: white; }") button_layout.addWidget(self.hide_all_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.tab_widget.clear() self.category_widgets.clear() # 获取所有分组 categories = list(self.rules.get("categories", {}).keys()) if not categories: categories = ["未分类"] # 统计每个分组中未隐藏的配置项数量 category_counts = {} for category in categories: # 获取该分组中所有配置项 category_fields = self.rules.get("categories", {}).get(category, []) # 统计未隐藏的配置项数量 visible_count = 0 for field_name in category_fields: if field_name in self.config_fields: # config_fields只包含未隐藏的配置项 visible_count += 1 category_counts[category] = visible_count # 检查是否需要隐藏"未分类"分组 hide_uncategorized = False if "未分类" in categories: unclassified_count = category_counts.get("未分类", 0) # 如果有其他分组存在 other_categories = [c for c in categories if c != "未分类"] has_other_categories = len(other_categories) > 0 # 如果"未分类"分组没有未隐藏的配置项,且存在其他分组,则隐藏 if unclassified_count == 0 and has_other_categories: hide_uncategorized = True # 创建标签页 for category in categories: # 如果是"未分类"分组且需要隐藏,则跳过 if category == "未分类" and hide_uncategorized: continue 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: # 先备份现有规则文件 self.backup_existing_rules() # 检查是否已有规则文件 if os.path.exists(self.rules_file): with open(self.rules_file, 'r', encoding='utf-8') as f: existing_rules = json.load(f) # 检查规则文件是否为空或格式错误 if not existing_rules or not isinstance(existing_rules, dict): raise ValueError("规则文件为空或格式错误") # 从打包资源加载默认规则(用于合并新字段) default_rules = self.get_default_rules() # 智能合并:保留现有规则,只添加新字段 self.rules = self.merge_rules(existing_rules, default_rules) # 保存合并后的规则(包含新字段) self.save_rules() else: # 如果没有规则文件,使用默认规则 self.rules = self.get_default_rules() # 首次运行,创建规则文件 try: self.save_rules() except Exception as e: print(f"创建规则文件失败: {e}") except (json.JSONDecodeError, ValueError, KeyError) as e: # 如果规则文件损坏,备份后使用默认规则 print(f"加载规则文件失败: {e},将使用默认规则") self.backup_corrupted_rules() self.rules = self.get_default_rules() self.save_rules() except Exception as e: print(f"加载规则文件失败: {e}") self.rules = self.get_default_rules() # 确保规则中有必要的字段 self.ensure_rule_structure() def get_default_rules(self): """获取默认规则""" return { "categories": {"未分类": []}, "display_names": {}, "tooltips": {}, "field_types": {}, "field_decimals": {}, "hidden": [], "validations": {}, "field_order": {}, "last_saved_values": {} # 新增:上次保存的值 } def merge_rules(self, existing_rules, default_rules): """智能合并规则:保留现有规则,只添加默认规则中的新字段""" merged_rules = existing_rules.copy() # 确保所有必需的字段都存在 for key in default_rules: if key not in merged_rules: merged_rules[key] = default_rules[key] # 确保"未分类"分组始终存在 if "categories" not in merged_rules: merged_rules["categories"] = {} if "未分类" not in merged_rules["categories"]: merged_rules["categories"]["未分类"] = [] # 确保其他必要字段存在 for field in ["display_names", "tooltips", "field_types", "field_decimals", "hidden", "validations", "field_order", "last_saved_values"]: # 新增last_saved_values if field not in merged_rules: if field == "hidden": merged_rules[field] = [] else: merged_rules[field] = {} return merged_rules def ensure_rule_structure(self): """确保规则结构完整""" # 确保规则中有"未分类"分组 if "categories" not in self.rules: self.rules["categories"] = {} if "未分类" not in self.rules["categories"]: self.rules["categories"]["未分类"] = [] # 确保规则中有field_decimals字段 if "field_decimals" not in self.rules: self.rules["field_decimals"] = {} # 确保规则中有field_order字段 if "field_order" not in self.rules: self.rules["field_order"] = {} # 确保规则中有last_saved_values字段 if "last_saved_values" not in self.rules: self.rules["last_saved_values"] = {} def backup_existing_rules(self): """备份现有规则""" if os.path.exists(self.rules_file): try: # 创建备份目录 backup_dir = os.path.join(self.program_dir, "backups") os.makedirs(backup_dir, exist_ok=True) # 生成带时间戳的备份文件名 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_file = os.path.join(backup_dir, f"config_editor_rules_{timestamp}.json") # 备份文件 shutil.copy2(self.rules_file, backup_file) print(f"已备份规则文件到: {backup_file}") # 清理旧的备份文件(保留最近5个) self.cleanup_old_backups(backup_dir) except Exception as e: print(f"备份规则文件失败: {e}") def cleanup_old_backups(self, backup_dir): """清理旧的备份文件(保留最近5个)""" try: # 获取所有备份文件 backup_files = [] for filename in os.listdir(backup_dir): if filename.startswith("config_editor_rules_") and filename.endswith(".json"): filepath = os.path.join(backup_dir, filename) backup_files.append((filepath, os.path.getmtime(filepath))) # 按修改时间排序(从旧到新) backup_files.sort(key=lambda x: x[1]) # 删除旧的备份文件,保留最近5个 if len(backup_files) > 5: files_to_delete = backup_files[:-5] # 删除除最近5个外的所有文件 for filepath, _ in files_to_delete: os.remove(filepath) print(f"已删除旧备份文件: {filepath}") except Exception as e: print(f"清理旧备份文件失败: {e}") def backup_corrupted_rules(self): """备份损坏的规则文件""" if os.path.exists(self.rules_file): timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_file = f"{self.rules_file}.corrupted_{timestamp}.bak" try: shutil.copy2(self.rules_file, backup_file) print(f"已备份损坏的规则文件到: {backup_file}") except Exception as e: print(f"备份损坏规则文件失败: {e}") 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 import_rules_file(self): """在主界面直接导入规则文件""" # 检查当前是否有未保存的规则更改 if hasattr(self, 'rules_modified') and self.rules_modified: reply = QMessageBox.question( self, "确认导入", "当前有未保存的规则更改,导入将覆盖这些更改。\n是否继续?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # 选择要导入的规则文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择规则文件", str(Path.home()), "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return try: # 读取规则文件 with open(file_path, 'r', encoding='utf-8') as f: imported_rules = json.load(f) # 验证规则文件格式 if not isinstance(imported_rules, dict): QMessageBox.warning(self, "导入失败", "规则文件格式不正确") return # 显示导入确认对话框 reply = QMessageBox.question( self, "确认导入", f"确定要导入规则文件吗?\n\n文件: {os.path.basename(file_path)}\n\n" f"这将覆盖当前的规则设置,并重新加载配置文件。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # 完全替换现有规则 self.rules.clear() self.rules.update(imported_rules) # 确保规则结构完整 self.ensure_rule_structure() # 保存到规则文件 self.save_rules() # 重新加载配置(这会应用新规则) self.load_config() QMessageBox.information( self, "导入成功", f"规则文件导入成功!\n\n" f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" f"配置文件已重新加载以应用新规则。" ) except json.JSONDecodeError: QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") except Exception as e: QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") def export_rules_file(self): """在主界面直接导出规则文件""" # 首先保存当前规则到文件(确保最新) self.save_rules() # 选择导出位置和文件名 default_name = f"config_editor_rules_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" file_path, _ = QFileDialog.getSaveFileName( self, "导出规则文件", default_name, "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return # 确保文件扩展名是.json if not file_path.lower().endswith('.json'): file_path += '.json' try: # 复制规则文件到指定位置 shutil.copy2(self.rules_file, file_path) QMessageBox.information( self, "导出成功", f"规则文件已成功导出到:\n{file_path}\n\n" f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" f"规则文件:{os.path.basename(self.rules_file)}" ) except Exception as e: QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(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 get_last_saved_value(self, field_name, current_value): """获取配置项的上次保存值,如果没有则使用当前值""" last_saved_values = self.rules.get("last_saved_values", {}) if field_name in last_saved_values: return last_saved_values[field_name] return None # 第一次没有上次保存值,返回None 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 # 获取上次保存的值 last_saved_value = self.get_last_saved_value(var_name, var_value) 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), last_saved_value=last_saved_value # 新增:设置上次保存的值 ) except: # 如果无法解析值,使用字符串表示 try: value_str = ast.get_source_segment(content, node.value) config_data[var_name] = value_str # 获取上次保存的值 last_saved_value = self.get_last_saved_value(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), last_saved_value=last_saved_value # 新增:设置上次保存的值 ) except: config_data[var_name] = "无法解析的值" # 获取上次保存的值 last_saved_value = self.get_last_saved_value(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), last_saved_value=last_saved_value # 新增:设置上次保存的值 ) 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_%s { background-color: #fff9c4; } """ % field_name) 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_%s { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } QFrame#field_container_%s:hover { border: 1px solid #2196F3; background-color: #f8fdff; } """ % (field_name, field_name)) def update_status_bar(self): """更新状态栏显示修改数量""" if self.modified_fields: self.statusBar().showMessage(f"已修改 {len(self.modified_fields)} 个配置项") else: self.statusBar().showMessage("就绪") # === 新增:鼠标按下事件处理器 === def create_mouse_press_handler(self, field_name, label_widget): """创建鼠标按下事件处理器""" def mouse_press_handler(event): if event.button() == Qt.MouseButton.LeftButton: self.dragging_field = field_name self.drag_start_pos = event.pos() # 更改光标为抓取手型 label_widget.setCursor(Qt.CursorShape.ClosedHandCursor) # 高亮当前拖拽的字段 self.highlight_dragging_field(field_name, True) # 开始拖拽操作 drag = QDrag(label_widget) mime_data = QMimeData() mime_data.setText(field_name) drag.setMimeData(mime_data) # 设置拖拽时的缩略图 pixmap = label_widget.grab() drag.setPixmap(pixmap) drag.setHotSpot(event.pos()) drag.exec(Qt.DropAction.MoveAction) # 拖拽结束后恢复 label_widget.setCursor(Qt.CursorShape.OpenHandCursor) self.highlight_dragging_field(field_name, False) self.dragging_field = None return mouse_press_handler # === 新增:事件过滤器,用于处理拖拽进入、离开和放置 === def eventFilter(self, obj, event): """事件过滤器,用于处理拖拽相关事件""" # 检查是否是字段容器 if isinstance(obj, QFrame) and obj.objectName().startswith("field_container_"): if event.type() == QEvent.Type.DragEnter: # 拖拽进入容器 self.handle_drag_enter_event(obj, event) return True elif event.type() == QEvent.Type.DragLeave: # 拖拽离开容器 self.handle_drag_leave_event(obj) return True elif event.type() == QEvent.Type.Drop: # 放置到容器 self.handle_drop_event(obj, event) return True elif event.type() == QEvent.Type.DragMove: # 拖拽在容器上移动 self.handle_drag_move_event(obj, event) return True return super().eventFilter(obj, event) # === 新增:处理拖拽进入事件 === def handle_drag_enter_event(self, container, event): """处理拖拽进入事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 如果不是拖拽自身,则接受拖拽 if field_name != self.dragging_field: event.acceptProposedAction() self.drop_target_field = field_name self.highlight_drop_target(field_name, True) # === 新增:处理拖拽离开事件 === def handle_drag_leave_event(self, container): """处理拖拽离开事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 清除目标高亮 if field_name == self.drop_target_field: self.highlight_drop_target(field_name, False) self.drop_target_field = None # === 新增:处理放置事件 === def handle_drop_event(self, container, event): """处理放置事件""" # 获取源字段和目标字段 source_field = self.dragging_field target_field = container.objectName().replace("field_container_", "") if source_field and target_field and source_field != target_field: # 获取字段的分组 source_config = self.all_config_fields.get(source_field) target_config = self.all_config_fields.get(target_field) if source_config and target_config and source_config.category == target_config.category: # 在同一分组内移动字段 self.reorder_fields_in_category(source_field, target_field, source_config.category) # 重新生成UI show_hidden = getattr(self, 'show_hidden', False) self.generate_dynamic_ui(show_hidden) # 保存顺序到规则文件 self.save_field_order_to_rules() # === 修复:不显示弹窗,只更新状态栏 === self.statusBar().showMessage(f"已将 '{source_config.display_name}' 移动到 '{target_config.display_name}' 的位置", 3000) # 清除目标高亮 self.highlight_drop_target(target_field, False) self.drop_target_field = None event.acceptProposedAction() # === 新增:处理拖拽移动事件 === def handle_drag_move_event(self, container, event): """处理拖拽移动事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 如果不是拖拽自身,则接受拖拽移动 if field_name != self.dragging_field: event.acceptProposedAction() # === 新增:高亮显示拖拽中的字段 === def highlight_dragging_field(self, field_name, is_dragging): """高亮显示拖拽中的字段""" if field_name in self.field_containers: container = self.field_containers[field_name] if is_dragging: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 2px dashed #2196F3; background-color: #e3f2fd; opacity: 0.7; }} """) else: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; }} QFrame#field_container_{field_name}:hover {{ border: 1px solid #2196F3; background-color: #f8fdff; }} """) # === 新增:高亮显示放置目标 === def highlight_drop_target(self, field_name, is_target): """高亮显示放置目标""" if field_name in self.field_containers: container = self.field_containers[field_name] if is_target: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 2px solid #4CAF50; background-color: #e8f5e9; }} """) else: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; }} QFrame#field_container_{field_name}:hover {{ border: 1px solid #2196F3; background-color: #f8fdff; }} """) # === 新增:重新排序分组内的字段 === def reorder_fields_in_category(self, source_field, target_field, category): """重新排序分组内的字段""" # 获取当前分组内的字段顺序 if category not in self.field_order: # 如果还没有该分组的顺序记录,从配置字段中获取 fields_in_category = [] for field_name, config_field in self.all_config_fields.items(): if config_field.category == category: fields_in_category.append(field_name) self.field_order[category] = fields_in_category # 重新排序 order_list = self.field_order[category] # 移除源字段 if source_field in order_list: order_list.remove(source_field) # 找到目标字段的位置,在目标位置插入源字段 if target_field in order_list: target_index = order_list.index(target_field) order_list.insert(target_index, source_field) else: # 如果目标字段不在列表中(不应该发生),添加到末尾 order_list.append(source_field) # === 新增:保存字段顺序到规则文件 === def save_field_order_to_rules(self): """保存字段顺序到规则文件""" # 将字段顺序转换为规则格式 self.rules["field_order"] = self.field_order # 保存规则文件 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 initialize_field_order(self): """初始化字段顺序""" # 从规则中加载字段顺序 if "field_order" in self.rules: self.field_order = self.rules["field_order"] else: self.field_order = {} # 确保所有字段都在顺序列表中 for field_name, config_field in self.all_config_fields.items(): category = config_field.category if category not in self.field_order: self.field_order[category] = [] if field_name not in self.field_order[category]: self.field_order[category].append(field_name) def create_field_editor(self, config_field): """创建字段编辑器,返回容器和控件(新增拖拽支持)""" # 创建容器框架 container = QFrame() container.setObjectName(f"field_container_{config_field.name}") # 设置唯一ID container.setFrameShape(QFrame.Shape.StyledPanel) # === 新增:设置容器可接受拖拽事件 === container.setAcceptDrops(True) container.installEventFilter(self) # 设置基础样式 container.setStyleSheet(""" QFrame#field_container_%s { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } QFrame#field_container_%s:hover { border: 1px solid #2196F3; background-color: #f8fdff; } """ % (config_field.name, config_field.name)) layout = QHBoxLayout(container) layout.setContentsMargins(8, 4, 8, 4) layout.setSpacing(8) # 标签 - 设置为可拖拽的区域 label = QLabel(config_field.display_name) label.setMinimumWidth(180) # === 新增:为标签添加拖拽支持 === label.setProperty("field_name", config_field.name) # 存储字段名 label.setProperty("category", config_field.category) # 存储分组名 label.mousePressEvent = self.create_mouse_press_handler(config_field.name, label) label.setCursor(Qt.CursorShape.OpenHandCursor) # 设置手型光标 # 添加校验指示器(如果配置项有校验规则) 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) # === 新增:上次保存的值显示 === last_saved_label = QLabel() last_saved_label.setMinimumWidth(120) last_saved_label.setToolTip("上次保存的值") # 格式化显示上次保存的值 if config_field.last_saved_value is not None: # 检查是否是复杂类型 if isinstance(config_field.last_saved_value, (dict, list)): # 对于复杂类型,使用JSON格式化 try: formatted = json.dumps(config_field.last_saved_value, ensure_ascii=False) # 如果太长,截断显示 if len(formatted) > 20: formatted = formatted[:17] + "..." last_saved_label.setText(f"上次: {formatted}") except: value_type = type(config_field.last_saved_value).__name__ last_saved_label.setText(f"上次: [{value_type}]") else: # 对于简单类型,显示完整值 value_str = str(config_field.last_saved_value) # 如果太长,截断显示 if len(value_str) > 20: value_str = value_str[:17] + "..." last_saved_label.setText(f"上次: {value_str}") else: last_saved_label.setText("上次: 无") last_saved_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic; background-color: #f8f8f8; padding: 2px; border-radius: 2px;") layout.addWidget(last_saved_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() # 重新初始化标签页(考虑是否隐藏"未分类"分组) self.init_category_tabs() 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) # 初始化字段顺序 self.initialize_field_order() for category, fields in categorized_fields.items(): # 跳过不在当前标签页中的分组(如被隐藏的"未分类"分组) if category not in self.category_widgets: continue layout = self.category_widgets[category] # === 修改:按照保存的顺序排序 === if category in self.field_order and self.field_order[category]: # 创建从字段名到配置项的映射 field_dict = {f.name: f for f in fields} # 按照保存的顺序排序 ordered_fields = [] for field_name in self.field_order[category]: if field_name in field_dict: ordered_fields.append(field_dict[field_name]) del field_dict[field_name] # 添加不在顺序列表中的字段(新字段) remaining_fields = list(field_dict.values()) remaining_fields.sort(key=lambda x: x.name) # 按字母顺序排序剩余字段 ordered_fields.extend(remaining_fields) fields = ordered_fields else: # 如果没有保存的顺序,按字母顺序排序 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) # 统计显示的标签页数量 visible_tabs = self.tab_widget.count() hidden_count = len(self.all_config_fields) - len(self.config_fields) self.statusBar().showMessage(f"成功加载 {len(self.config_data)} 个配置项 ({len(self.config_fields)} 个显示, {hidden_count} 个隐藏, {visible_tabs} 个分组)") 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 hide_all_fields(self): """一键隐藏所有配置项""" reply = QMessageBox.question( self, "确认一键隐藏", "确定要隐藏所有配置项吗?\n隐藏后,页面上将不显示任何配置项。\n如需显示,需要在规则管理中取消隐藏。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: # 获取所有配置项的名称 all_field_names = list(self.all_config_fields.keys()) # 将所有配置项添加到隐藏列表 self.rules["hidden"] = all_field_names # 保存规则 self.save_rules() # 重新加载配置 self.load_config() # 更新状态栏 self.statusBar().showMessage("已隐藏所有配置项,页面已清空") QMessageBox.information(self, "完成", "已成功隐藏所有配置项。\n如需显示配置项,请在规则管理中取消隐藏。") 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 # === 新增:在保存前更新上次保存的值到规则文件 === for field_name, (old_value, new_value) in changes.items(): # 将修改前的值(旧值)保存为上次保存的值 self.rules.setdefault("last_saved_values", {})[field_name] = old_value # 立即保存规则文件,确保上次保存的值被持久化 self.save_rules() # 读取原始文件内容 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()