Files
ArcGis_Py/tools/ui/tabs/config_editor_dialog.py
2026-04-22 12:27:49 +08:00

597 lines
25 KiB
Python

# --- START OF FILE config_editor_dialog_visual.py ---
import json
import os
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QPushButton, QFileDialog, QMessageBox, QLabel,
QListWidget, QListWidgetItem, QStackedWidget,
QWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QSizePolicy, QSplitter, QGroupBox, QGridLayout,
QLineEdit, QInputDialog) # Added QInputDialog
from PyQt6.QtCore import Qt
class ConfigEditorDialogVisual(QDialog):
"""
用于可视化编辑JSON配置文件的模态对话框
"""
def __init__(self, config_file_path, parent=None):
super().__init__(parent)
self.setWindowTitle("编辑配置文件 (可视化)")
self.resize(800, 600)
self.config_file_path = config_file_path
self._config_data = {} # Internal dictionary to hold the parsed JSON
self.original_content_hash = None # To track changes
self.init_ui()
self.load_config_data()
# Connect signals after UI is initialized and data is loaded
self.parameter_list_widget.currentItemChanged.connect(self._load_parameter_details)
self._load_parameter_details(self.parameter_list_widget.currentItem(), None) # Load initial item details if any
def init_ui(self):
"""初始化界面"""
main_layout = QVBoxLayout(self)
# Splitter for parameter list on the left and details on the right
splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(splitter)
# Left Panel: Parameter List
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.addWidget(QLabel("参数列表:"))
self.parameter_list_widget = QListWidget()
left_layout.addWidget(self.parameter_list_widget)
# Buttons for adding/removing parameters
param_btn_layout = QHBoxLayout()
self.add_param_btn = QPushButton("添加参数")
self.remove_param_btn = QPushButton("移除参数")
param_btn_layout.addWidget(self.add_param_btn)
param_btn_layout.addWidget(self.remove_param_btn)
left_layout.addLayout(param_btn_layout)
self.add_param_btn.clicked.connect(self._add_parameter)
self.remove_param_btn.clicked.connect(self._remove_parameter)
splitter.addWidget(left_widget)
# Right Panel: Parameter Details (StackedWidget to potentially show empty state)
self.details_stack = QStackedWidget()
self.empty_details_widget = QLabel("请从左侧选择一个参数或添加新参数")
self.empty_details_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_stack.addWidget(self.empty_details_widget) # Index 0
self.parameter_details_widget = QWidget()
self._setup_parameter_details_ui(self.parameter_details_widget)
self.details_stack.addWidget(self.parameter_details_widget)
splitter.addWidget(self.details_stack)
# Set initial sizes for splitter panes
splitter.setSizes([200, 600])
# Bottom Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("保存")
self.save_as_btn = QPushButton("另存为...")
self.cancel_btn = QPushButton("取消")
self.save_btn.clicked.connect(self.save_config)
self.save_as_btn.clicked.connect(self.save_config_as)
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
button_layout.addWidget(self.save_as_btn)
button_layout.addStretch()
button_layout.addWidget(self.cancel_btn)
main_layout.addLayout(button_layout)
# Initial state: Disable save buttons until something is loaded/changed
self.save_btn.setEnabled(False)
self.save_as_btn.setEnabled(False)
self.remove_param_btn.setEnabled(False)
def _setup_parameter_details_ui(self, parent_widget):
"""Setup the UI for the parameter details panel"""
layout = QVBoxLayout(parent_widget)
# Fixed fields
fixed_fields_group = QGroupBox("基本信息")
fixed_fields_layout = QGridLayout(fixed_fields_group)
self.name_edit = QTextEdit()
self.name_edit.setFixedHeight(40) # Allow multi-line but not too tall
self.method_edit = QTextEdit()
self.method_edit.setFixedHeight(40)
self.param_level_edit = QTextEdit()
self.param_level_edit.setFixedHeight(40)
self.standard_edit = QTextEdit()
self.standard_edit.setFixedHeight(40)
fixed_fields_layout.addWidget(QLabel("项目名称:"), 0, 0)
fixed_fields_layout.addWidget(self.name_edit, 0, 1)
fixed_fields_layout.addWidget(QLabel("分析方法:"), 1, 0)
fixed_fields_layout.addWidget(self.method_edit, 1, 1)
fixed_fields_layout.addWidget(QLabel("项目分级:"), 2, 0)
fixed_fields_layout.addWidget(self.param_level_edit, 2, 1)
fixed_fields_layout.addWidget(QLabel("分级标准名称:"), 3, 0) # Renamed for clarity
fixed_fields_layout.addWidget(self.standard_edit, 3, 1)
layout.addWidget(fixed_fields_group)
# Standard Levels Table
levels_group = QGroupBox("标准等级")
levels_layout = QVBoxLayout(levels_group)
self.levels_table = QTableWidget(0, 2)
self.levels_table.setHorizontalHeaderLabels(["等级名称 (Key)", "标准值 (Value)"])
self.levels_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.levels_table.verticalHeader().setVisible(False)
levels_layout.addWidget(self.levels_table)
# Buttons for adding/removing levels
level_btn_layout = QHBoxLayout()
self.add_level_btn = QPushButton("添加等级")
self.remove_level_btn = QPushButton("移除等级")
level_btn_layout.addWidget(self.add_level_btn)
level_btn_layout.addWidget(self.remove_level_btn)
levels_layout.addLayout(level_btn_layout)
self.add_level_btn.clicked.connect(self._add_standard_level)
self.remove_level_btn.clicked.connect(self._remove_standard_level)
layout.addWidget(levels_group)
layout.addStretch() # Push everything to the top
# Connect signals for tracking changes (set dirty flag)
self.name_edit.textChanged.connect(self._set_dirty)
self.method_edit.textChanged.connect(self._set_dirty)
self.param_level_edit.textChanged.connect(self._set_dirty)
self.standard_edit.textChanged.connect(self._set_dirty)
self.levels_table.cellChanged.connect(self._set_dirty)
# Adding/removing items/levels also makes it dirty
self.add_param_btn.clicked.connect(self._set_dirty)
self.remove_param_btn.clicked.connect(self._set_dirty)
self.add_level_btn.clicked.connect(self._set_dirty)
self.remove_level_btn.clicked.connect(self._set_dirty)
def load_config_data(self):
"""加载文件内容到内部字典并填充UI"""
self._config_data = {} # Clear previous data
self.parameter_list_widget.clear()
if not self.config_file_path or not os.path.exists(self.config_file_path):
QMessageBox.warning(self, "加载错误", f"配置文件不存在或路径无效:\n{self.config_file_path}")
self.details_stack.setCurrentIndex(0) # Show empty message
return
try:
with open(self.config_file_path, 'r', encoding='utf-8') as f:
self._config_data = json.load(f)
# Store a hash or string representation for change detection
# Use json.dumps to handle ordering consistency
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
# Populate the list widget from the loaded data
if "export_config" in self._config_data and isinstance(self._config_data["export_config"], dict):
for key in sorted(self._config_data["export_config"].keys()):
self.parameter_list_widget.addItem(key)
if self.parameter_list_widget.count() > 0:
self.parameter_list_widget.setCurrentRow(0)
self.details_stack.setCurrentIndex(1)
else:
self.details_stack.setCurrentIndex(0)
else:
QMessageBox.warning(self, "加载错误", "配置文件缺少 'export_config' 键或格式不正确。")
self.details_stack.setCurrentIndex(0)
except json.JSONDecodeError as e:
QMessageBox.critical(self, "加载错误", f"配置文件JSON格式无效:\n{e}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
except Exception as e:
QMessageBox.critical(self, "加载错误", f"无法读取或解析配置文件:\n{str(e)}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
def _load_parameter_details(self, current_item, previous_item):
"""Load details of the newly selected parameter"""
# Before loading new details, save changes from the previous item
if previous_item:
self._save_parameter_details(previous_item.text())
# Clear the details UI
self.name_edit.clear()
self.method_edit.clear()
self.param_level_edit.clear()
self.standard_edit.clear()
self.levels_table.setRowCount(0) # Clear table
if current_item:
key = current_item.text()
param_data = self._config_data.get("export_config", {}).get(key, {})
self.name_edit.setPlainText(param_data.get("项目名称", ""))
self.method_edit.setPlainText(param_data.get("分析方法", ""))
self.param_level_edit.setPlainText(param_data.get("项目分级", ""))
self.standard_edit.setPlainText(param_data.get("分级标准", ""))
# Load standard levels into the table
levels = param_data.get("标准等级", {})
if isinstance(levels, dict):
self.levels_table.setRowCount(len(levels))
# Sort levels by key (等级一, 等级二, etc.) for consistency
# for i, (level_key, level_value) in enumerate(sort(levels.items())):
for i, (level_key, level_value) in enumerate(levels.items()):
self.levels_table.setItem(i, 0, QTableWidgetItem(str(level_key)))
self.levels_table.setItem(i, 1, QTableWidgetItem(str(level_value).replace("\n", "\\n")))
else:
print(f"警告: 参数 '{key}''标准等级' 不是一个有效的字典。")
self.details_stack.setCurrentIndex(1)
self.remove_param_btn.setEnabled(True)
self._set_dirty(False)
else:
self.details_stack.setCurrentIndex(0)
self.remove_param_btn.setEnabled(False)
self._set_dirty(False)
def _save_parameter_details(self, key):
"""Save details from the UI widgets back to the internal dictionary for the given key"""
if not key or "export_config" not in self._config_data or key not in self._config_data["export_config"]:
return # Nothing to save if key is invalid or not in data
param_data = self._config_data["export_config"][key]
param_data["项目名称"] = self.name_edit.toPlainText()
param_data["分析方法"] = self.method_edit.toPlainText()
param_data["项目分级"] = self.param_level_edit.toPlainText()
param_data["分级标准"] = self.standard_edit.toPlainText()
# Save standard levels from the table
levels = {}
for row in range(self.levels_table.rowCount()):
key_item = self.levels_table.item(row, 0)
value_item = self.levels_table.item(row, 1)
if key_item and value_item:
level_key = key_item.text().strip()
level_value = value_item.text().strip().replace("\\n", "\n")
if level_key: # Only save if level key is not empty
levels[level_key] = level_value
else:
print(f"警告: 参数 '{key}' 中存在空的等级名称,该行将被忽略。")
param_data["标准等级"] = levels
# print(f"Saved details for parameter: {key}") # Debugging
def _add_parameter(self):
"""Add a new parameter item"""
key, ok = QInputDialog.getText(self, "添加新参数", "输入新的参数键 (例如: NEW):")
if ok and key:
key = key.strip()
if not key:
QMessageBox.warning(self, "输入错误", "参数键不能为空。")
return
if "export_config" not in self._config_data:
self._config_data["export_config"] = {}
if key in self._config_data["export_config"]:
QMessageBox.warning(self, "输入错误", f"参数键 '{key}' 已存在。")
return
# Add a default structure for the new parameter
self._config_data["export_config"][key] = {
"项目名称": "新项目名称",
"分析方法": "新分析方法",
"项目分级": "新项目分级",
"分级标准": "新分级标准",
"标准等级": {}
}
# Add to list widget and select it
self.parameter_list_widget.addItem(key)
# Re-sort the list
self.parameter_list_widget.sortItems()
# Find and select the new item
items = self.parameter_list_widget.findItems(key, Qt.MatchFlag.MatchExactly)
if items:
self.parameter_list_widget.setCurrentItem(items[0])
self._set_dirty()
def _remove_parameter(self):
"""Remove the currently selected parameter item"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
return # Nothing selected
key_to_remove = current_item.text()
reply = QMessageBox.question(
self, "移除参数", f"确定要移除参数 '{key_to_remove}' 吗?\n此操作不可撤销。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# Save details of the item being removed *before* removing it from data
# (This handles case where user edits an item then immediately removes it)
self._save_parameter_details(key_to_remove)
# Remove from internal data
if "export_config" in self._config_data and key_to_remove in self._config_data["export_config"]:
del self._config_data["export_config"][key_to_remove]
# Remove from list widget
row = self.parameter_list_widget.row(current_item)
self.parameter_list_widget.takeItem(row)
# Clear details panel and maybe select next item or show empty
self._load_parameter_details(self.parameter_list_widget.currentItem(), current_item)
self._set_dirty()
def _add_standard_level(self):
"""Add a new row to the standard levels table"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
QMessageBox.warning(self, "添加等级", "请先选择一个参数。")
return
row_count = self.levels_table.rowCount()
self.levels_table.insertRow(row_count)
# Add default items (editable)
self.levels_table.setItem(row_count, 0, QTableWidgetItem("新等级"))
self.levels_table.setItem(row_count, 1, QTableWidgetItem(""))
self._set_dirty()
def _remove_standard_level(self):
"""Remove selected rows from the standard levels table"""
# selected_rows = sorted(list(set(index.row() for index in self.levels_table.selectedIndexes())), reverse=True)
selected_rows = list(set(index.row() for index in self.levels_table.selectedIndexes()))
if not selected_rows:
QMessageBox.warning(self, "移除等级", "请选择要移除的等级行。")
return
for row in selected_rows:
self.levels_table.removeRow(row)
self._set_dirty()
def _set_dirty(self, dirty=True):
"""Set or clear the dirty flag and update button states"""
# This slot might receive a bool from cellChanged, or be called with True/False
# Ensure it's treated as a boolean
self._is_dirty = bool(dirty)
# Enable save buttons if dirty
self.save_btn.setEnabled(self._is_dirty)
self.save_as_btn.setEnabled(True)
# print(f"Dirty state: {self._is_dirty}") # Debugging
def is_dirty(self):
"""Check if the configuration has been modified"""
if not self._is_dirty:
return False
# Perform a more thorough check by saving current state to a temp dict
# and comparing its hash with the original hash
try:
# Save details of the currently selected item first
current_key = self.parameter_list_widget.currentItem()
if current_key:
self._save_parameter_details(current_key.text())
current_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
return current_hash != self.original_content_hash
except Exception as e:
print(f"Error checking dirty state: {e}")
# If hashing fails, assume it's dirty to be safe
return True
def validate_config(self):
"""Validate the structure of the internal config data before saving"""
if "export_config" not in self._config_data or not isinstance(self._config_data["export_config"], dict):
QMessageBox.warning(self, "验证失败", "配置缺少主键 'export_config' 或其格式不正确。")
return False
standards_dict = self._config_data["export_config"]
error_keys = []
for key, value in standards_dict.items():
# Check for required fields and types
if not isinstance(value, dict):
error_keys.append(f"{key}: 结构不是字典")
continue
required_keys = ["项目名称", "分析方法", "项目分级", "分级标准", "标准等级"]
if not all(k in value for k in required_keys):
error_keys.append(f"{key}: 缺少必要字段")
continue
if not isinstance(value.get("标准等级"), dict):
error_keys.append(f"{key}: '标准等级' 不是字典")
continue
if error_keys:
QMessageBox.warning(self, "验证失败", "配置文件结构或内容存在问题:\n" + "\n".join(error_keys))
return False
return True
def save_config(self):
"""保存配置到当前文件路径"""
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if not self.config_file_path:
# If no path is set (e.g., started with no file and added items), force Save As
return self.save_config_as()
return self._perform_save(self.config_file_path)
def save_config_as(self):
"""另存配置"""
file_path, _ = QFileDialog.getSaveFileName(
self, "另存配置文件", self.config_file_path if self.config_file_path else "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if self._perform_save(file_path):
# Update the current file path if Save As was successful
self.config_file_path = file_path
# Reload from the new path to reset original_content_hash and dirty state
self.load_config_data()
QMessageBox.information(self, "另存成功", f"配置文件已另存为:\n{self.config_file_path}")
self.accept()
return True
return False
return False
def _perform_save(self, path):
"""Execute the saving of the config data to a file"""
try:
# Ensure directory exists
dir_name = os.path.dirname(path)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
# Use indent=4 for readability and ensure_ascii=False for Chinese chars
json.dump(self._config_data, f, indent=4, ensure_ascii=False)
# Update the original content hash after saving
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
self._set_dirty(False) # Clear dirty flag
# For standard save, don't close the dialog, just inform
# QMessageBox.information(self, "保存成功", f"配置文件已保存:\n{path}") # Removed msgbox for standard save
return True
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存文件时出错:\n{str(e)}")
return False
def closeEvent(self, event):
"""Handle close event, ask to save if dirty"""
if self.is_dirty():
reply = QMessageBox.question(
self, "保存更改",
"配置文件已修改,是否保存?",
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Save
)
if reply == QMessageBox.StandardButton.Save:
if self.save_config():
event.accept()
else:
event.ignore()
elif reply == QMessageBox.StandardButton.Discard:
event.accept()
else:
event.ignore()
else:
event.accept()
# Method to retrieve the potentially new file path after saving (e.g., Save As)
def get_new_config_path(self):
return self.config_file_path
# Example running (for testing this dialog itself)
if __name__ == '__main__':
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
# Create a dummy config file for testing if it doesn't exist
test_dir = "temp_config_editor_test"
os.makedirs(test_dir, exist_ok=True)
test_config_path = os.path.join(test_dir, "test_config.json")
dummy_data = {
"export_config": {
"AB": {
"项目名称": "测试硼",
"分析方法": "测试方法",
"项目分级": "硼级别",
"分级标准": "硼标准",
"标准等级": {
"等级1": ">2.00",
"等级2": "1.00~2.00"
}
},
"ZN": {
"项目名称": "测试锌",
"分析方法": "锌方法",
"项目分级": "锌级别",
"分级标准": "锌标准",
"标准等级": {
"A级": ">3.00",
"B级": "2.00~3.00",
"C级": "<2.00"
}
}
}
}
if not os.path.exists(test_config_path):
try:
with open(test_config_path, 'w', encoding='utf-8') as f:
json.dump(dummy_data, f, indent=4, ensure_ascii=False)
print(f"Created dummy config file: {test_config_path}")
except Exception as e:
print(f"Error creating dummy file: {e}")
test_config_path = "" # Clear path if creation fails
dialog = ConfigEditorDialogVisual(test_config_path)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
final_path = dialog.get_new_config_path()
print(f"Dialog accepted. Final config path: {final_path}")
else:
print("Dialog rejected or cancelled.")
# Optional: Clean up test files/directory
# import shutil
# if os.path.exists(test_dir):
# shutil.rmtree(test_dir)
sys.exit(app.exec())