Files
backtrader/config_editor_gui.py
2026-01-17 21:21:30 +08:00

1066 lines
45 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_())