import sys
import xml.etree.ElementTree as ET
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QTableWidgetItem,
QPushButton, QFileDialog, QLineEdit, QLabel, QMessageBox,
QHBoxLayout, QVBoxLayout, QWidget, QHeaderView, QAbstractItemView,
QComboBox, QSizePolicy, QStyledItemDelegate)
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QBrush, QColor, QFont
import os
class ComboBoxDelegate(QStyledItemDelegate):
def __init__(self, parent=None, options=None):
super().__init__(parent)
self.options = options or []
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.setEditable(True)
editor.addItems(self.options)
return editor
def setEditorData(self, editor, index):
value = index.data(Qt.DisplayRole)
if value:
editor.setCurrentText(value)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText(), Qt.EditRole)
class XMLDataManager(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("RawDataConfig编辑器")
self.setGeometry(100, 100, 1400, 800)
self.current_file = None
self.original_data = [] # 原始数据,保持不变
self.display_data = [] # 当前显示的数据
self.temp_data = [] # 临时数据(用于保存修改)
self.row_counter = 1
self.type_options = set()
self.data_type_id_options = set()
self.is_searching = False
self.search_results_indices = [] # 保存搜索结果的原始索引
self.search_text = "" # 保存当前搜索文本
# 定义字体
self.normal_font = QFont()
self.bold_font = QFont()
self.bold_font.setBold(True)
self.column_names = [
"path", "type", "variable", "id", "data_type_id",
"subsystem_id", "parts", "parts_id", "attribute",
"attribute_id", "mapping"
]
self.required_columns = [col for col in self.column_names if col != "mapping"]
self.init_ui()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.load_btn = QPushButton("加载文件")
self.add_btn = QPushButton("添加记录")
self.delete_btn = QPushButton("删除记录")
self.save_btn = QPushButton("保存修改")
self.export_btn = QPushButton("导出文件")
self.search_btn = QPushButton("搜索")
self.clear_btn = QPushButton("清空搜索")
button_style = """
QPushButton {
background-color: rgb(65, 105, 225);
color: white;
border: none;
padding: 8px 16px;
text-align: center;
font-size: 14px;
margin: 4px 2px;
border-radius: 4px;
}
QPushButton:hover {
background-color: rgb(55, 95, 215);
}
QPushButton:pressed {
background-color: rgb(45, 85, 205);
}
"""
for btn in [self.load_btn, self.export_btn, self.add_btn,
self.delete_btn, self.save_btn, self.search_btn,
self.clear_btn]:
btn.setStyleSheet(button_style)
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("输入搜索内容...")
self.search_box.setMinimumWidth(300)
self.search_box.setStyleSheet("""
QLineEdit {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
""")
self.column_combo = QComboBox()
self.column_combo.setMinimumWidth(120)
self.column_combo.addItem("所有列")
self.column_combo.addItems(self.column_names)
self.column_combo.setCurrentIndex(0)
self.column_combo.setStyleSheet("""
QComboBox {
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
""")
self.table = QTableWidget()
self.table.setColumnCount(len(self.column_names) + 1)
headers = ["序号"] + self.column_names
self.table.setHorizontalHeaderLabels(headers)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
self.table.horizontalHeader().setStretchLastSection(True)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.table.itemChanged.connect(self.update_cell_style)
self.table.setStyleSheet("""
QTableWidget {
gridline-color: #ddd;
font-size: 12px;
}
QHeaderView::section {
background-color: #f2f2f2;
padding: 4px;
border: 1px solid #ddd;
font-weight: bold;
}
QTableCornerButton::section {
background-color: #f2f2f2;
border: 1px solid #ddd;
}
""")
self.table.setColumnWidth(0, 60)
self.table.verticalHeader().setVisible(False)
self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
top_layout = QHBoxLayout()
top_layout.addWidget(self.load_btn)
top_layout.addWidget(self.add_btn)
top_layout.addWidget(self.delete_btn)
top_layout.addWidget(self.save_btn)
top_layout.addWidget(self.export_btn)
top_layout.addStretch()
search_col_label = QLabel("搜索列:")
search_col_label.setStyleSheet("font-size: 14px;")
top_layout.addWidget(search_col_label)
top_layout.addWidget(self.column_combo)
search_col_label1 = QLabel("搜索内容:")
search_col_label1.setStyleSheet("font-size: 14px;")
top_layout.addWidget(search_col_label1)
top_layout.addWidget(self.search_box)
top_layout.addWidget(self.search_btn)
top_layout.addWidget(self.clear_btn)
main_layout = QVBoxLayout()
main_layout.addLayout(top_layout)
main_layout.addWidget(self.table)
central_widget.setLayout(main_layout)
self.load_btn.clicked.connect(self.load_xml)
self.export_btn.clicked.connect(self.export_xml)
self.add_btn.clicked.connect(self.add_record)
self.delete_btn.clicked.connect(self.delete_record)
self.save_btn.clicked.connect(self.save_changes)
self.search_btn.clicked.connect(self.search_records)
self.clear_btn.clicked.connect(self.clear_search)
self.statusBar = self.statusBar()
self.statusBar.setStyleSheet("background-color: #f0f0f0; padding: 4px;")
def update_cell_style(self, item):
col_idx = item.column() - 1
if col_idx < 0 or col_idx >= len(self.column_names):
return
col_name = self.column_names[col_idx]
cell_text = item.text().lower()
# 重置字体
item.setFont(self.normal_font)
# 检查必填项
if col_name in self.required_columns:
if not item.text().strip():
item.setBackground(QColor(255, 200, 200))
else:
item.setBackground(QColor(255, 255, 255))
# 高亮显示搜索匹配的内容
if self.is_searching and self.search_text:
if self.search_text in cell_text:
item.setFont(self.bold_font) # 匹配项加粗
if col_name in self.required_columns and not item.text().strip():
item.setBackground(QColor(255, 200, 200))
else:
item.setBackground(QColor(255, 255, 0)) # 黄色高亮
elif col_name in self.required_columns and not item.text().strip():
item.setBackground(QColor(255, 200, 200))
else:
item.setBackground(QColor(255, 255, 255))
def load_xml(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择XML文件", "", "XML文件 (*.xml)"
)
if not file_path:
return
try:
tree = ET.parse(file_path)
root = tree.getroot()
self.current_file = file_path
file_name = os.path.basename(file_path)
self.setWindowTitle(f"RawDataConfig编辑器 - {file_name}")
self.table.setRowCount(0)
self.original_data = []
self.display_data = []
self.temp_data = []
self.row_counter = 1
self.type_options = set()
self.data_type_id_options = set()
self.is_searching = False
self.search_results_indices = []
self.search_text = ""
for record in root.findall('Record'):
row_data = {}
for col_name in self.column_names:
value = record.get(col_name, '')
row_data[col_name] = value
if col_name == 'type' and value:
self.type_options.add(value)
elif col_name == 'data_type_id' and value:
self.data_type_id_options.add(value)
self.original_data.append(row_data)
self.temp_data = [row.copy() for row in self.original_data]
self.display_data = self.temp_data.copy()
self.populate_table()
self.adjust_column_widths()
self.statusBar.showMessage(f"成功加载文件: {file_path}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"加载XML文件失败:\n{str(e)}")
def populate_table(self, data=None, indices=None):
if data is None:
data = self.display_data
try:
self.table.itemChanged.disconnect(self.update_cell_style)
except:
pass
self.table.setRowCount(len(data))
type_col_index = self.column_names.index('type') + 1
data_type_id_col_index = self.column_names.index('data_type_id') + 1
type_delegate = ComboBoxDelegate(self.table, sorted(self.type_options))
data_type_id_delegate = ComboBoxDelegate(self.table, sorted(self.data_type_id_options))
self.table.setItemDelegateForColumn(type_col_index, type_delegate)
self.table.setItemDelegateForColumn(data_type_id_col_index, data_type_id_delegate)
for row_idx, record in enumerate(data):
# 保留原始序号
if indices and row_idx < len(indices):
original_idx = indices[row_idx]
seq_item = QTableWidgetItem(str(original_idx + 1))
else:
seq_item = QTableWidgetItem(str(self.row_counter + row_idx))
seq_item.setTextAlignment(Qt.AlignCenter)
seq_item.setFlags(seq_item.flags() & ~Qt.ItemIsEditable)
seq_item.setFont(self.normal_font)
self.table.setItem(row_idx, 0, seq_item)
for col_idx, col_name in enumerate(self.column_names):
value = record.get(col_name, '')
item = QTableWidgetItem(value)
if self.is_searching and self.search_text and self.search_text in value.lower():
item.setFont(self.bold_font)
else:
item.setFont(self.normal_font)
# item.setFont(self.normal_font)
if col_name in ['type', 'data_type_id']:
item.setFlags(item.flags() | Qt.ItemIsEditable)
if col_name in self.required_columns and not value.strip():
item.setBackground(QColor(255, 200, 200))
self.table.setItem(row_idx, col_idx + 1, item)
self.table.itemChanged.connect(self.update_cell_style)
def adjust_column_widths(self):
self.table.setColumnWidth(0, 60)
for col in range(1, self.table.columnCount()):
self.table.resizeColumnToContents(col)
if self.table.columnWidth(col) < 100:
self.table.setColumnWidth(col, 100)
def export_xml(self):
if not self.temp_data:
QMessageBox.warning(self, "警告", "没有数据可导出")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "导出XML文件", "", "XML文件 (*.xml)"
)
if not file_path:
return
try:
with open(file_path, 'wb') as f:
f.write(b'<?xml version="1.0" ?>\n')
f.write(b'<DataRecords>\n')
for record in self.temp_data:
attrs = ' '.join([f'{k}="{v}"' for k, v in record.items()])
f.write(f' <Record {attrs}/>\n'.encode('utf-8'))
f.write(b'</DataRecords>')
self.statusBar.showMessage(f"成功导出到: {file_path}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"导出XML文件失败:\n{str(e)}")
def add_record(self):
selected_rows = set(index.row() for index in self.table.selectedIndexes())
insert_row = self.table.rowCount()
if selected_rows:
insert_row = min(selected_rows) + 1
self.table.insertRow(insert_row)
new_record = {col_name: "" for col_name in self.column_names}
# 根据当前状态添加到不同的数据列表
if self.is_searching:
# 在搜索状态下添加记录到临时数据和显示数据
self.display_data.insert(insert_row, new_record)
# 找到合适的原始索引位置插入
insert_original_pos = self.search_results_indices[insert_row] if insert_row < len(
self.search_results_indices) else len(self.temp_data)
self.temp_data.insert(insert_original_pos, new_record)
# 更新后续的搜索结果索引
self.search_results_indices = [x if x < insert_original_pos else x + 1 for x in self.search_results_indices]
else:
self.display_data.insert(insert_row, new_record)
self.temp_data.insert(insert_row, new_record)
seq_item = QTableWidgetItem(str(self.row_counter + insert_row))
seq_item.setTextAlignment(Qt.AlignCenter)
seq_item.setFlags(seq_item.flags() & ~Qt.ItemIsEditable)
seq_item.setFont(self.normal_font)
self.table.setItem(insert_row, 0, seq_item)
for col_idx, col_name in enumerate(self.column_names):
item = QTableWidgetItem("")
item.setFont(self.normal_font)
if col_name in ['type', 'data_type_id']:
item.setFlags(item.flags() | Qt.ItemIsEditable)
if col_name in self.required_columns:
item.setBackground(QColor(255, 200, 200))
self.table.setItem(insert_row, col_idx + 1, item)
self.update_sequence_numbers()
self.table.scrollToItem(self.table.item(insert_row, 0))
self.table.selectRow(insert_row)
self.statusBar.showMessage("已添加新记录", 2000)
def delete_record(self):
selected_rows = set(index.row() for index in self.table.selectedIndexes())
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要删除的记录")
return
reply = QMessageBox.question(self, "确认删除",
f"确定要删除选中的 {len(selected_rows)} 条记录吗?",
QMessageBox.Yes | QMessageBox.No)
if reply != QMessageBox.Yes:
return
# 按降序删除以避免索引问题
for row in sorted(selected_rows, reverse=True):
self.table.removeRow(row)
if row < len(self.display_data):
del self.display_data[row]
if self.is_searching and row < len(self.search_results_indices):
# 如果是搜索状态,需要从原始数据中删除
original_idx = self.search_results_indices[row]
if original_idx < len(self.temp_data):
del self.temp_data[original_idx]
# 更新搜索结果索引
self.search_results_indices = [x if x < original_idx else x - 1 for x in self.search_results_indices]
self.search_results_indices.pop(row)
elif not self.is_searching and row < len(self.temp_data):
del self.temp_data[row]
self.update_sequence_numbers()
self.statusBar.showMessage(f"已删除 {len(selected_rows)} 条记录", 3000)
def update_sequence_numbers(self):
for row_idx in range(self.table.rowCount()):
if self.is_searching and row_idx < len(self.search_results_indices):
original_idx = self.search_results_indices[row_idx]
seq_item = QTableWidgetItem(str(original_idx + 1))
else:
seq_item = QTableWidgetItem(str(self.row_counter + row_idx))
seq_item.setTextAlignment(Qt.AlignCenter)
seq_item.setFlags(seq_item.flags() & ~Qt.ItemIsEditable)
seq_item.setFont(self.normal_font)
self.table.setItem(row_idx, 0, seq_item)
def save_changes(self):
if not self.display_data:
QMessageBox.warning(self, "警告", "没有数据可保存")
return
empty_fields = []
for row_idx in range(self.table.rowCount()):
for col_idx, col_name in enumerate(self.column_names):
if col_name in self.required_columns:
item = self.table.item(row_idx, col_idx + 1)
if item is None or not item.text().strip():
seq_item = self.table.item(row_idx, 0)
row_num = seq_item.text() if seq_item else str(row_idx + 1)
empty_fields.append(f"行 {row_num} 的 '{col_name}' 列")
if empty_fields:
error_msg = "以下必填项为空,请填写完整后保存:\n"
error_msg += "\n".join(empty_fields[:10])
if len(empty_fields) > 10:
error_msg += f"\n...(共{len(empty_fields)}个错误)"
QMessageBox.warning(self, "保存失败", error_msg)
return
try:
# 更新显示数据
for row_idx in range(self.table.rowCount()):
while row_idx >= len(self.display_data):
self.display_data.append({col_name: "" for col_name in self.column_names})
for col_idx, col_name in enumerate(self.column_names):
item = self.table.item(row_idx, col_idx + 1)
if item:
self.display_data[row_idx][col_name] = item.text()
if col_name == 'type' and item.text():
self.type_options.add(item.text())
elif col_name == 'data_type_id' and item.text():
self.data_type_id_options.add(item.text())
else:
self.display_data[row_idx][col_name] = ""
# 更新临时数据
if self.is_searching:
# 在搜索状态下,需要特殊处理以保持数据一致性
for display_idx, original_idx in enumerate(self.search_results_indices):
if display_idx < len(self.display_data) and original_idx < len(self.temp_data):
for col_name in self.column_names:
self.temp_data[original_idx][col_name] = self.display_data[display_idx][col_name]
else:
# 非搜索状态下,直接更新临时数据
self.temp_data = [row.copy() for row in self.display_data]
self.statusBar.showMessage("修改已保存到内存", 2000)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
def search_records(self):
search_text = self.search_box.text().strip()
selected_column = self.column_combo.currentText()
if not search_text:
QMessageBox.warning(self, "警告", "请输入搜索内容")
return
self.is_searching = True
self.search_text = search_text.lower() # 保存搜索文本用于高亮
self.search_results_indices = []
matched_rows = []
# 搜索基于临时数据(包含所有修改)
for original_idx, record in enumerate(self.temp_data):
match_found = False
if selected_column == "所有列":
for col_name in self.column_names:
value = record.get(col_name, '').lower()
if self.search_text in value:
match_found = True
break
else:
value = record.get(selected_column, '').lower()
if self.search_text in value:
match_found = True
if match_found:
self.search_results_indices.append(original_idx)
matched_rows.append(record)
if matched_rows:
self.display_data = matched_rows
# 传递原始索引用于显示正确的序号
self.populate_table(self.display_data, self.search_results_indices)
self.adjust_column_widths()
self.statusBar.showMessage(f"找到 {len(matched_rows)} 条匹配记录", 3000)
else:
QMessageBox.information(self, "搜索结果", "未找到匹配记录")
self.statusBar.showMessage("未找到匹配记录", 2000)
def clear_search(self):
# 恢复显示所有数据,保留所有修改
self.display_data = [row.copy() for row in self.temp_data]
self.is_searching = False
self.search_text = "" # 清空搜索文本
self.search_box.clear() # 清空搜索框内容
self.search_results_indices = []
self.populate_table()
self.adjust_column_widths()
self.statusBar.showMessage("已显示所有记录", 2000)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = XMLDataManager()
window.show()
sys.exit(app.exec_())
去除搜索结果黄色高亮显示,其余部分程序不变