#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import ast import re from PyQt5.QtWidgets import ( QApplication, QMainWindow, QTreeWidget, QTreeWidgetItem, QGroupBox, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton, QCheckBox, QSpinBox, QDoubleSpinBox, QMessageBox, QSplitter, QScrollArea, QFrame ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QColor class ConfigEditorGUI(QMainWindow): def __init__(self, config_path="config.py", config_class_name="Config"): super().__init__() self.config_path = config_path self.config_class_name = config_class_name self.config_data = {} self.current_key = None self.edit_widgets = {} self.init_ui() self.load_config() def init_ui(self): """初始化用户界面""" self.setWindowTitle("配置文件编辑器") self.setGeometry(100, 100, 1200, 800) # 创建主分割器 splitter = QSplitter(Qt.Horizontal) self.setCentralWidget(splitter) # 左侧配置项树 self.tree = QTreeWidget() self.tree.setHeaderLabels(["配置项", "值", "类型"]) self.tree.setMinimumWidth(400) self.tree.setAlternatingRowColors(True) self.tree.itemSelectionChanged.connect(self.on_item_select) splitter.addWidget(self.tree) # 右侧编辑区域 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) # 创建滚动区域 scroll = QScrollArea() scroll.setWidgetResizable(True) scroll_widget = QWidget() self.edit_content_layout = QVBoxLayout(scroll_widget) self.edit_content_layout.setAlignment(Qt.AlignTop) scroll.setWidget(scroll_widget) right_layout.addWidget(scroll) # 按钮区域 button_layout = QHBoxLayout() refresh_btn = QPushButton("刷新配置") refresh_btn.clicked.connect(self.refresh_config) button_layout.addWidget(refresh_btn) save_btn = QPushButton("保存配置") save_btn.clicked.connect(self.save_config) save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") button_layout.addWidget(save_btn) button_layout.addStretch() right_layout.addLayout(button_layout) splitter.addWidget(right_widget) splitter.setSizes([400, 800]) def load_config(self): """加载配置文件""" try: # 尝试多种编码方式读取文件 encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin-1'] content = None used_encoding = None for encoding in encodings: try: with open(self.config_path, "r", encoding=encoding) as f: content = f.read() used_encoding = encoding break except UnicodeDecodeError: continue if content is None: raise UnicodeDecodeError("无法使用任何支持的编码读取文件") print(f"使用编码 {used_encoding} 成功读取配置文件") # 查找Config类的定义 class_start = content.find(f"class {self.config_class_name}") if class_start == -1: raise ValueError(f"未找到{self.config_class_name}类的定义") # 查找类的结束位置 class_end = len(content) lines = content[class_start:].split("\n") class_line_count = 0 # 计算类的实际范围 for i, line in enumerate(lines): if i == 0: # 类定义行 class_line_count += 1 continue # 空行或注释行 if not line.strip() or line.strip().startswith("#"): class_line_count += 1 continue # 如果是非空行且不以空格开头,则是下一个顶级元素 if line and not line[0].isspace(): class_line_count -= 1 # 回退一行 break class_line_count += 1 # 计算类的结束位置 class_end_pos = class_start for _ in range(class_line_count): class_end_pos = content.find("\n", class_end_pos) + 1 if class_end_pos == 0: # 如果找不到换行符,说明已经到文件末尾 class_end_pos = len(content) break class_content = content[class_start:class_end_pos] # 尝试使用AST解析 try: self.parse_config_with_ast(class_content) except Exception as e: # 如果AST解析失败,回退到正则表达式方法 print(f"AST解析失败,回退到正则表达式方法: {str(e)}") self.parse_config_regex(class_content) # 更新UI self.update_tree() except Exception as e: QMessageBox.critical(self, "错误", f"加载配置文件失败: {str(e)}") def parse_config_with_ast(self, class_content): """使用AST解析配置""" # 移除类定义中的文档字符串 import ast import re try: # 解析整个类内容 tree = ast.parse(class_content) self.config_data = {} self.config_categories = {} # 存储分类信息 # 先收集所有变量的字符串表示 assignments = {} # 提取分类信息(从 # ===== 开头的注释行) lines = class_content.split("\n") current_category = "未分类" category_items = {} # {category: [key1, key2, ...]} for line in lines: # 检测分类标记 if "# ====" in line and "====" in line: # 提取分类名称,例如: # ========== 数据配置 ========== category_match = re.search(r'#\s*=+\s*(.+?)\s*=+', line) if category_match: current_category = category_match.group(1).strip() if current_category not in category_items: category_items[current_category] = [] # 检测变量定义 elif "=" in line and not line.strip().startswith("#"): var_match = re.match(r'\s*(\w+)\s*=', line) if var_match: var_name = var_match.group(1) if not var_name.startswith("_"): category_items[current_category].append(var_name) class_node = tree.body[0] if isinstance(class_node, ast.ClassDef): for node in class_node.body: if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): key = node.targets[0].id # 跳过私有属性和特殊变量 if key.startswith("_") or key in ["self", "cls"]: continue # 保存变量的字符串表示 assignments[key] = ast.unparse(node.value) # 重新遍历并解析值 if isinstance(class_node, ast.ClassDef): for node in class_node.body: if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): key = node.targets[0].id # 跳过私有属性和特殊变量 if key.startswith("_") or key in ["self", "cls"]: continue # 获取注释 comment = "" lines = class_content.split("\n") line_no = getattr(node, 'lineno', 0) if line_no > 0 and line_no <= len(lines): line_content = lines[line_no-1] # AST行号从1开始 comment_match = re.search(r'#\s*(.+)$', line_content) if comment_match: comment = comment_match.group(1).strip() # 初始化字典键注释 dict_key_comments = {} # 如果是字典类型,提取键的注释 value_str = assignments[key] if '{' in value_str: # 查找该字典在源代码中的定义 in_dict = False for line_idx, line in enumerate(lines): # 检查是否开始这个字典 if f"{key} =" in line and "{" in line: in_dict = True continue # 如果在字典内,提取键的注释 if in_dict: if "}" in line: in_dict = False break # 匹配字典中的键值对和注释,例如: 'key': value, # 注释 dict_item_match = re.match(r"\s*['\"]([^'\"]+)['\"]\s*:\s*[^#]+(#.*)?$", line) if dict_item_match: dict_key = dict_item_match.group(1) dict_comment = dict_item_match.group(2).strip("# ") if dict_item_match.group(2) else "" if dict_comment: # 提取注释的第一部分(去掉括号中的说明) dict_comment_clean = re.sub(r'\s*[((].*?[))]\s*$', '', dict_comment).strip() dict_key_comments[dict_key] = dict_comment_clean # 尝试解析值 value = None try: # 直接解析配置项的值 value = ast.literal_eval(value_str) except Exception as e: # 如果解析失败,尝试替换变量引用 modified_value_str = value_str for var_name, var_value_str in assignments.items(): if var_name in modified_value_str: try: var_value = ast.literal_eval(var_value_str) var_value_repr = repr(var_value) except: var_value_repr = var_value_str modified_value_str = re.sub( rf'\b{re.escape(var_name)}\b', var_value_repr, modified_value_str ) # 再次尝试解析 try: value = ast.literal_eval(modified_value_str) except Exception as e2: # 如果仍然失败,直接使用字符串 print(f"解析配置项 {key} 失败: {str(e2)}") value = modified_value_str # 保存配置数据 self.config_data[key] = { "value": value, "original_str": value_str, "comment": comment, "dict_key_comments": dict_key_comments } # 保存分类信息 self.config_categories = category_items except Exception as e: # 如果AST解析失败,回退到正则表达式方法 print(f"AST解析失败: {str(e)}") self.parse_config_regex(class_content) def parse_config_regex(self, class_content): """使用正则表达式解析配置(备用方法)""" pattern = r'^\s*(\w+)\s*=\s*([^#]+)(#.*)?$' self.config_data = {} self.config_categories = {} # 存储分类信息 lines = class_content.split("\n") # 提取分类信息 current_category = "未分类" category_items = {} # {category: [key1, key2, ...]} for line in lines: # 检测分类标记 if "# ====" in line and "====" in line: # 提取分类名称,例如: # ========== 数据配置 ========== category_match = re.search(r'#\s*=+\s*(.+?)\s*=+', line) if category_match: current_category = category_match.group(1).strip() if current_category not in category_items: category_items[current_category] = [] # 检测变量定义 elif "=" in line and not line.strip().startswith("#"): var_match = re.match(r'\s*(\w+)\s*=', line) if var_match: var_name = var_match.group(1) if not var_name.startswith("_"): category_items[current_category].append(var_name) i = 0 # 先收集所有变量的值 assignments = {} while i < len(lines): line = lines[i] line_stripped = line.strip() if not line_stripped or line_stripped.startswith("#") or "class " in line_stripped: i += 1 continue if "=" not in line_stripped: i += 1 continue match = re.match(pattern, line) if match: key = match.group(1) value_str = match.group(2).strip() comment = match.group(3).strip("# ") if match.group(3) else "" # 跳过特殊属性 if key.startswith("_") or key in ["self", "cls"]: i += 1 continue # 保存原始值字符串 assignments[key] = value_str i += 1 # 重新遍历并解析值 i = 0 while i < len(lines): line = lines[i] line_stripped = line.strip() if not line_stripped or line_stripped.startswith("#") or "class " in line_stripped: i += 1 continue if "=" not in line_stripped: i += 1 continue match = re.match(pattern, line) if match: key = match.group(1) value_str = match.group(2).strip() comment = match.group(3).strip("# ") if match.group(3) else "" # 跳过特殊属性 if key.startswith("_") or key in ["self", "cls"]: i += 1 continue # 初始化字典键注释 dict_key_comments = {} # 如果是字典类型,提取键的注释 if '{' in value_str: # 查找该字典在源代码中的定义 in_dict = False for line_idx in range(len(lines)): line = lines[line_idx] # 检查是否开始这个字典 if f"{key} =" in line and "{" in line: in_dict = True continue # 如果在字典内,提取键的注释 if in_dict: if "}" in line: in_dict = False break # 匹配字典中的键值对和注释,例如: 'key': value, # 注释 dict_item_match = re.match(r"\s*['\"]([^'\"]+)['\"]\s*:\s*[^#]+(#.*)?$", line) if dict_item_match: dict_key = dict_item_match.group(1) dict_comment = dict_item_match.group(2).strip("# ") if dict_item_match.group(2) else "" if dict_comment: # 提取注释的第一部分(去掉括号中的说明) dict_comment_clean = re.sub(r'\s*[((].*?[))]\s*$', '', dict_comment).strip() dict_key_comments[dict_key] = dict_comment_clean try: # 尝试解析值 value = ast.literal_eval(value_str) self.config_data[key] = { "value": value, "original_str": value_str, "comment": comment, "dict_key_comments": dict_key_comments } except (SyntaxError, ValueError): # 如果解析失败,尝试替换变量引用 modified_value_str = value_str # 替换已知的变量引用 for var_name, var_value_str in assignments.items(): if var_name in modified_value_str: # 将变量名替换为实际值 try: var_value = ast.literal_eval(var_value_str) var_value_repr = repr(var_value) except: # 如果不能解析,保持原样 var_value_repr = var_value_str modified_value_str = re.sub( rf'\b{re.escape(var_name)}\b', var_value_repr, modified_value_str ) # 再次尝试解析 try: value = ast.literal_eval(modified_value_str) self.config_data[key] = { "value": value, "original_str": value_str, "comment": comment, "dict_key_comments": dict_key_comments } except (SyntaxError, ValueError): # 如果仍然失败,检查是否单纯引用 if value_str in assignments: try: ref_value = ast.literal_eval(assignments[value_str]) self.config_data[key] = { "value": ref_value, "original_str": value_str, "comment": comment, "dict_key_comments": {} } except: self.config_data[key] = { "value": assignments[value_str], "original_str": value_str, "comment": comment, "dict_key_comments": {} } else: # 作为字符串处理 self.config_data[key] = { "value": value_str, "original_str": value_str, "comment": comment, "dict_key_comments": {} } i += 1 # 保存分类信息 self.config_categories = category_items def update_tree(self): """更新配置项树""" self.tree.clear() # 如果有分类信息,按分类显示 if hasattr(self, 'config_categories') and self.config_categories: for category, keys in self.config_categories.items(): # 创建分类节点 category_item = QTreeWidgetItem(self.tree) category_item.setText(0, f"📁 {category}") category_item.setText(1, "") category_item.setText(2, "") # 设置分类节点样式 font = QFont("微软雅黑", 11, QFont.Bold) category_item.setFont(0, font) category_item.setForeground(0, QColor("#2196F3")) category_item.setExpanded(True) # 默认展开 # 添加该分类下的配置项 for key in keys: if key in self.config_data: value_info = self.config_data[key] value = value_info["value"] comment = value_info.get("comment", "") # 使用注释作为显示名称,如果没有注释则使用原始键名 display_name = comment if comment else key display_value = str(value) if len(str(value)) <= 30 else str(value)[:30] + "..." item = QTreeWidgetItem(category_item) item.setText(0, display_name) item.setText(1, display_value) item.setText(2, type(value).__name__) # 在工具提示中显示原始键名 item.setToolTip(0, f"{key}\n{comment}" if comment else key) item.setToolTip(1, str(value)) # 保存原始键名以便后续使用 item.setData(0, Qt.UserRole, key) else: # 如果没有分类信息,按原样显示 for key, value_info in self.config_data.items(): value = value_info["value"] comment = value_info.get("comment", "") display_name = comment if comment else key display_value = str(value) if len(str(value)) <= 30 else str(value)[:30] + "..." item = QTreeWidgetItem(self.tree) item.setText(0, display_name) item.setText(1, display_value) item.setText(2, type(value).__name__) # 设置提示信息 item.setToolTip(0, f"{key}\n{comment}" if comment else key) item.setToolTip(1, str(value)) item.setData(0, Qt.UserRole, key) def on_item_select(self): """选择配置项时的事件处理""" selected_items = self.tree.selectedItems() if not selected_items: return item = selected_items[0] # 获取原始键名(从 UserRole 中获取) key = item.data(0, Qt.UserRole) # 如果是分类节点,不处理 if not key: return if key in self.config_data: self.current_key = key value_info = self.config_data[key] value = value_info["value"] comment = value_info["comment"] # 清空编辑区域 self.clear_edit_area() # 创建配置项名称显示(使用注释作为主要显示) display_title = comment if comment else key name_group = QGroupBox(f"配置项: {display_title}") name_group.setFont(QFont("微软雅黑", 11, QFont.Bold)) name_layout = QVBoxLayout(name_group) # 显示原始键名(小字体,灰色) key_label = QLabel(f"键名: {key}") key_label.setStyleSheet("color: #999; font-size: 10px; font-style: italic;") name_layout.addWidget(key_label) if comment: comment_label = QLabel(f"说明: {comment}") comment_label.setWordWrap(True) comment_label.setStyleSheet("color: #666; font-style: italic; font-size: 12px;") name_layout.addWidget(comment_label) self.edit_content_layout.addWidget(name_group) # 根据类型创建不同的编辑界面 value_type = type(value).__name__ if value_type == "bool": self.create_bool_editor(value) elif value_type in ["int", "float"]: self.create_number_editor(value, value_type) elif value_type == "str": self.create_string_editor(value) elif value_type == "list": self.create_list_editor(value) elif value_type == "dict": self.create_dict_editor(value) else: self.create_text_editor(value) def clear_edit_area(self): """清空编辑区域""" # 清空所有控件 while self.edit_content_layout.count(): item = self.edit_content_layout.takeAt(0) if item.widget(): item.widget().deleteLater() self.edit_widgets.clear() def create_bool_editor(self, value): """创建布尔类型编辑器""" group = QGroupBox("值") layout = QVBoxLayout(group) checkbox = QCheckBox("启用") checkbox.setChecked(value) layout.addWidget(checkbox) self.edit_widgets["value"] = checkbox self.edit_content_layout.addWidget(group) def create_number_editor(self, value, value_type): """创建数字类型编辑器""" group = QGroupBox("值") layout = QHBoxLayout(group) if value_type == "int": spinbox = QSpinBox() spinbox.setRange(-2147483648, 2147483647) spinbox.setValue(int(value)) else: spinbox = QDoubleSpinBox() spinbox.setRange(-sys.float_info.max, sys.float_info.max) spinbox.setValue(float(value)) spinbox.setDecimals(6) layout.addWidget(spinbox) layout.addStretch() self.edit_widgets["value"] = spinbox self.edit_content_layout.addWidget(group) def create_string_editor(self, value): """创建字符串类型编辑器""" group = QGroupBox("值") layout = QVBoxLayout(group) line_edit = QLineEdit(value) layout.addWidget(line_edit) self.edit_widgets["value"] = line_edit self.edit_content_layout.addWidget(group) def create_list_editor(self, value): """创建列表类型编辑器""" group = QGroupBox("值") layout = QVBoxLayout(group) # 显示列表项 for i, item in enumerate(value): item_layout = QHBoxLayout() item_line_edit = QLineEdit(str(item)) remove_btn = QPushButton("删除") # 保存索引以便删除时使用 remove_btn.clicked.connect(lambda checked, idx=i: self.remove_list_item(idx)) item_layout.addWidget(QLabel(f"项目 {i+1}:")) item_layout.addWidget(item_line_edit) item_layout.addWidget(remove_btn) layout.addLayout(item_layout) self.edit_widgets[f"item_{i}"] = item_line_edit # 添加新项按钮 add_layout = QHBoxLayout() add_line_edit = QLineEdit() add_btn = QPushButton("添加项目") add_btn.clicked.connect(lambda: self.add_list_item(add_line_edit.text())) add_layout.addWidget(add_line_edit) add_layout.addWidget(add_btn) layout.addLayout(add_layout) self.edit_widgets["new_item"] = add_line_edit self.edit_content_layout.addWidget(group) def add_list_item(self, text): """添加列表项""" if not text: return # 获取当前列表长度 item_count = sum(1 for key in self.edit_widgets.keys() if key.startswith("item_")) # 创建新的列表项 item_layout = QHBoxLayout() item_line_edit = QLineEdit(text) remove_btn = QPushButton("删除") # 保存索引以便删除时使用 remove_btn.clicked.connect(lambda checked, idx=item_count: self.remove_list_item(idx)) item_layout.addWidget(QLabel(f"项目 {item_count+1}:")) item_layout.addWidget(item_line_edit) item_layout.addWidget(remove_btn) # 插入到"添加新项"之前 self.edit_content_layout.insertLayout(self.edit_content_layout.count()-1, item_layout) self.edit_widgets[f"item_{item_count}"] = item_line_edit # 清空输入框 self.edit_widgets["new_item"].setText("") def remove_list_item(self, index): """删除列表项""" if f"item_{index}" in self.edit_widgets: # 移除控件 del self.edit_widgets[f"item_{index}"] # 重新编号后续项 keys = [key for key in self.edit_widgets.keys() if key.startswith("item_")] keys.sort(key=lambda x: int(x.split("_")[1])) for i, key in enumerate(keys): if int(key.split("_")[1]) > index: old_widget = self.edit_widgets.pop(key) self.edit_widgets[f"item_{i-1}"] = old_widget def create_dict_editor(self, value): """创建字典类型编辑器""" group = QGroupBox("值") layout = QVBoxLayout(group) # 获取当前配置项的字典键注释 dict_key_comments = {} if self.current_key and self.current_key in self.config_data: dict_key_comments = self.config_data[self.current_key].get("dict_key_comments", {}) # 显示字典项 for key, val in value.items(): # 使用注释作为显示名称,如果没有注释则使用原始键名 display_name = dict_key_comments.get(key, key) item_group = QGroupBox(display_name) item_layout = QVBoxLayout(item_group) # 根据值的类型创建相应的编辑器 val_type = type(val).__name__ if val_type == "bool": checkbox = QCheckBox("启用") checkbox.setChecked(val) item_layout.addWidget(checkbox) self.edit_widgets[f"dict_{key}"] = checkbox elif val_type in ["int", "float"]: if val_type == "int": spinbox = QSpinBox() spinbox.setRange(-2147483648, 2147483647) spinbox.setValue(int(val)) else: spinbox = QDoubleSpinBox() spinbox.setRange(-sys.float_info.max, sys.float_info.max) spinbox.setValue(float(val)) spinbox.setDecimals(6) item_layout.addWidget(spinbox) self.edit_widgets[f"dict_{key}"] = spinbox elif val_type == "str": line_edit = QLineEdit(str(val)) item_layout.addWidget(line_edit) self.edit_widgets[f"dict_{key}"] = line_edit elif val_type == "list": # 对于列表,使用文本编辑器显示 text_edit = QTextEdit() text_edit.setMaximumHeight(100) text_edit.setText(repr(val)) item_layout.addWidget(text_edit) self.edit_widgets[f"dict_{key}"] = text_edit else: # 其他类型也使用文本编辑器 text_edit = QTextEdit() text_edit.setMaximumHeight(100) text_edit.setText(str(val)) item_layout.addWidget(text_edit) self.edit_widgets[f"dict_{key}"] = text_edit layout.addWidget(item_group) self.edit_content_layout.addWidget(group) def create_text_editor(self, value): """创建文本编辑器(用于复杂类型)""" group = QGroupBox("值") layout = QVBoxLayout(group) text_edit = QTextEdit() text_edit.setText(str(value)) text_edit.setMaximumHeight(200) layout.addWidget(text_edit) self.edit_widgets["value"] = text_edit self.edit_content_layout.addWidget(group) def refresh_config(self): """刷新配置""" self.load_config() def save_config(self): """保存配置到文件""" if not self.current_key: QMessageBox.warning(self, "警告", "请先选择一个配置项") return try: # 更新当前选中项的值 self.update_config_item() # 读取整个配置文件 encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin-1'] content = None used_encoding = None for encoding in encodings: try: with open(self.config_path, "r", encoding=encoding) as f: content = f.read() used_encoding = encoding break except UnicodeDecodeError: continue if content is None: raise UnicodeDecodeError("无法使用任何支持的编码读取文件") # 查找Config类的位置 class_start = content.find(f"class {self.config_class_name}") if class_start == -1: raise ValueError(f"未找到{self.config_class_name}类") # 找到类的结束位置 class_end = len(content) brace_count = 0 in_class = False lines = content.split("\n") line_start = 0 for i, line in enumerate(lines): # 计算到当前行的字符位置 if i > 0: line_start += len(lines[i-1]) + 1 # +1 for newline if line_start >= class_start and not in_class: in_class = True if in_class: brace_count += line.count("{") - line.count("}") if brace_count < 0: class_end = line_start + len(line) break # 提取类的内容 class_content = content[class_start:class_end] # 更新配置项 updated_class_content = self.update_config_in_class(class_content) # 替换原内容 new_content = content[:class_start] + updated_class_content + content[class_end:] # 写入文件 with open(self.config_path, "w", encoding=used_encoding) as f: f.write(new_content) QMessageBox.information(self, "成功", "配置已保存") except Exception as e: QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") def update_config_item(self): """更新当前配置项的值""" if not self.current_key or self.current_key not in self.config_data: return # 根据控件类型获取值 if "value" in self.edit_widgets: widget = self.edit_widgets["value"] if isinstance(widget, QCheckBox): new_value = widget.isChecked() elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): new_value = widget.value() elif isinstance(widget, QLineEdit): new_value = widget.text() elif isinstance(widget, QTextEdit): new_value = widget.toPlainText() else: new_value = str(widget) self.config_data[self.current_key]["value"] = new_value elif self.config_data[self.current_key]["value"].__class__.__name__ == "dict": # 处理字典类型 dict_value = self.config_data[self.current_key]["value"] for key in dict_value.keys(): widget_key = f"dict_{key}" if widget_key in self.edit_widgets: widget = self.edit_widgets[widget_key] if isinstance(widget, QCheckBox): dict_value[key] = widget.isChecked() elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): dict_value[key] = widget.value() elif isinstance(widget, QLineEdit): dict_value[key] = widget.text() elif isinstance(widget, QTextEdit): try: # 尝试解析文本为Python对象 dict_value[key] = eval(widget.toPlainText()) except: dict_value[key] = widget.toPlainText() self.config_data[self.current_key]["value"] = dict_value elif self.config_data[self.current_key]["value"].__class__.__name__ == "list": # 处理列表类型 list_items = [] for key, widget in self.edit_widgets.items(): if key.startswith("item_"): try: # 尝试转换为适当的类型 text = widget.text() if text.isdigit(): list_items.append(int(text)) elif text.replace('.', '', 1).isdigit(): list_items.append(float(text)) else: list_items.append(text) except: list_items.append(widget.text()) self.config_data[self.current_key]["value"] = list_items def update_config_in_class(self, class_content): """在类内容中更新配置项【彻底修复:保留原始格式和注释】""" lines = class_content.split("\n") key = self.current_key value_info = self.config_data[key] value = value_info["value"] # 查找配置项所在的行 for i, line in enumerate(lines): if line.strip().startswith(f"{key} ="): # 【关键修复1】找到该配置项的完整范围(多行字典/列表) start_line = i end_line = i # 检查是否是多行配置 if '{' in line or '[' in line: # 使用括号计数查找结束行 brace_count = line.count('{') - line.count('}') bracket_count = line.count('[') - line.count(']') total_count = brace_count + bracket_count j = i + 1 while j < len(lines) and total_count > 0: brace_count = lines[j].count('{') - lines[j].count('}') bracket_count = lines[j].count('[') - lines[j].count(']') total_count += brace_count + bracket_count end_line = j j += 1 # 【关键修复2】提取原始的缩进和注释 original_indent = len(line) - len(line.lstrip()) indent_str = ' ' * original_indent # 提取行尾注释(只提取第一行的注释) comment_match = re.search(r'#.*$', lines[start_line]) comment_str = " " + comment_match.group() if comment_match else "" # 【关键修复3】根据类型生成新内容,保留字典键注释 if isinstance(value, dict): # 对于字典,需要保留键的注释 new_lines = self.format_dict_with_comments(key, value, indent_str, comment_str) elif isinstance(value, list): new_lines = self.format_list_with_indent(value, indent_str, comment_str, key) elif isinstance(value, str): new_lines = [f"{indent_str}{key} = '{value}'{comment_str}"] else: new_lines = [f"{indent_str}{key} = {repr(value)}{comment_str}"] # 【关键修复4】删除旧内容,插入新内容 del lines[start_line:end_line+1] for idx, new_line in enumerate(new_lines): lines.insert(start_line + idx, new_line) break return "\n".join(lines) def format_dict_with_comments(self, key, dct, indent, top_comment): """格式化字典,保留原始键注释【新增】""" if not dct: return [f"{indent}{key} = {{}}{top_comment}"] # 获取原始的字典键注释 dict_key_comments = {} if key in self.config_data: dict_key_comments = self.config_data[key].get("dict_key_comments", {}) lines = [] lines.append(f"{indent}{key} = {{{top_comment}") dict_items = list(dct.items()) for idx, (dict_key, dict_value) in enumerate(dict_items): # 获取该键的注释 key_comment = dict_key_comments.get(dict_key, "") comment_suffix = f" # {key_comment}" if key_comment else "" # 格式化值 if isinstance(dict_value, str): value_repr = f"'{dict_value}'" elif isinstance(dict_value, bool): value_repr = str(dict_value) elif isinstance(dict_value, (int, float)): value_repr = str(dict_value) elif isinstance(dict_value, list): value_repr = repr(dict_value) elif isinstance(dict_value, dict): value_repr = repr(dict_value) elif dict_value is None: value_repr = 'None' else: value_repr = repr(dict_value) # 最后一项不加逗号 if idx == len(dict_items) - 1: lines.append(f"{indent} '{dict_key}': {value_repr}{comment_suffix}") else: lines.append(f"{indent} '{dict_key}': {value_repr},{comment_suffix}") lines.append(f"{indent}}}") return lines def format_list_with_indent(self, lst, indent, top_comment, key): """格式化列表,保留缩进【新增】""" if not lst: return [f"{indent}{key} = []{top_comment}"] # 单行格式 if len(lst) <= 4: items = [f"'{item}'" if isinstance(item, str) else repr(item) for item in lst] return [f"{indent}{key} = [{', '.join(items)}]{top_comment}"] # 多行格式 lines = [f"{indent}{key} = [{top_comment}"] for idx, item in enumerate(lst): item_repr = f"'{item}'" if isinstance(item, str) else repr(item) if idx == len(lst) - 1: lines.append(f"{indent} {item_repr}") else: lines.append(f"{indent} {item_repr},") lines.append(f"{indent}]") return lines if __name__ == "__main__": app = QApplication(sys.argv) editor = ConfigEditorGUI() editor.show() sys.exit(app.exec_())