diff --git a/config_editor.py b/config_editor.py deleted file mode 100644 index 2576834..0000000 --- a/config_editor.py +++ /dev/null @@ -1,2854 +0,0 @@ -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() \ No newline at end of file