ReportGeneratorLocal/info_core/MyQtClass.py

2553 lines
99 KiB
Python
Raw 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.

from PySide6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QCheckBox,
QPushButton, QDialog, QLineEdit, QFontComboBox,
QFileDialog, QMessageBox, QLabel, QWidget,
QTextBrowser, QApplication, QCompleter, QFrame,
QListWidgetItem, QListWidget, QAbstractItemView,
QStackedWidget, QStackedLayout, QStyledItemDelegate,
QTreeView, QSplitter, QFileSystemModel, QScrollArea,
QToolTip, QGridLayout, QSizePolicy, QProgressDialog,
QButtonGroup, QInputDialog, QMenu)
from PySide6.QtCore import (QDateTime,QTimer,QDateTime,Signal,QSettings, QPoint, QEvent,
QSortFilterProxyModel, QSize, QDir, QMimeData, QRunnable, QObject,
QThreadPool, QPoint, QRect, QUrl)
from PySide6.QtGui import (QPixmap, QDragEnterEvent, QDropEvent, Qt, QIcon,
QFontMetrics, QStandardItem, QStandardItemModel,
QAction, QColor, QImageReader, QDrag, QCursor,
QPainter)
import json, sys, os
from info_core.defines import *
class ConfigComboBoxGroup(QGroupBox):
"""可复用的配置下拉框组件(带完整样式)"""
def __init__(self, title, allow_add=True, parent=None, is_project = True):
super().__init__(title, parent)
self.allow_add = allow_add
self.is_project = is_project
self.config_dir = os.path.join(os.getcwd(), "config", title)
# 确保配置目录存在
os.makedirs(self.config_dir, exist_ok=True)
# 设置组框样式
self.setStyleSheet(GROUP_BOX_STYLE)
self.setMinimumSize(GROUP_BOX_MIN_WIDTH, GROUP_BOX_MIN_HEIGHT)
# 创建UI
self.init_ui()
self.load_config_files()
def init_ui(self):
"""初始化UI组件"""
layout = QVBoxLayout()
layout.setSpacing(GROUP_BOX_SPACING)
layout.setContentsMargins(*GROUP_BOX_MARGINS)
self.combo_box_init()
# 按钮布局
btn_layout = QHBoxLayout()
btn_layout.addStretch() # 将按钮推到右侧
self.modify_btn = self.create_button("修改")
self.modify_btn.clicked.connect(self.on_modify_clicked)
btn_layout.addWidget(self.modify_btn)
if self.allow_add:
self.add_btn = self.create_button("添加")
self.add_btn.clicked.connect(self.on_add_clicked)
btn_layout.addWidget(self.add_btn)
self.delete_btn = self.create_button("删除")
self.delete_btn.clicked.connect(self.on_delete_clicked)
btn_layout.addWidget(self.delete_btn)
layout.addWidget(self.combo_box)
layout.addLayout(btn_layout)
self.setLayout(layout)
def combo_box_init(self):
# 下拉框
self.combo_box = QFontComboBox()
self.combo_box.setEditable(True)
# 代理模型(支持中文模糊匹配)
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.combo_box.model())
self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
# 补全器
self.completer = QCompleter(self.proxy_model, self.combo_box)
self.completer.setFilterMode(Qt.MatchContains)
self.combo_box.setCompleter(self.completer)
def create_button(self, text):
"""创建统一风格的按钮"""
btn = QPushButton(text)
btn.setStyleSheet(BUTTON_STYLE)
btn.setFixedSize(BUTTON_WIDTH, BUTTON_HEIGHT)
return btn
def load_config_files(self):
"""加载config文件夹中的JSON文件"""
self.combo_box.clear()
# 获取所有.json文件
config_files = [f for f in os.listdir(self.config_dir)
if f.endswith('.json')]
# 添加到下拉框(不带.json后缀
for file in config_files:
self.combo_box.addItem(file[:-5])
def on_modify_clicked(self):
"""修改按钮点击事件"""
if self.is_project:
project_widget = AddProjectDialog(self.config_dir, self, os.path.join(self.config_dir, self.combo_box.currentText() + ".json"))
project_widget.exec()
else:
person_widget = AddPersonnelDialog(self.config_dir, self, os.path.join(self.config_dir, self.combo_box.currentText() + ".json"))
person_widget.exec()
self.load_config_files()
def on_add_clicked(self):
"""添加按钮点击,创建默认ConfigEditDialog"""
if self.is_project:
project_widget = AddProjectDialog(self.config_dir, self)
project_widget.exec()
else:
person_widget = AddPersonnelDialog(self.config_dir, self)
person_widget.exec()
self.load_config_files()
def on_delete_clicked(self):
"""删除选中的项目"""
# 获取当前选中的项目名称
selected_project_name = self.combo_box.currentText()
if selected_project_name:
# 确认删除操作
confirm_message = f"你确定要删除项目 '{selected_project_name}' 吗?此操作不可恢复!"
reply = QMessageBox.question(self, '警告', confirm_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
# 构建项目文件的路径
project_file_path = os.path.join(self.config_dir, selected_project_name + ".json")
# 检查文件是否存在并删除
if os.path.exists(project_file_path):
os.remove(project_file_path)
else:
QMessageBox.warning(self, '警告', f"项目文件 '{selected_project_name}.json' 不存在。")
# 从UI中移除该项目
self.combo_box.removeItem(self.combo_box.currentIndex())
# 重新加载配置文件列表
self.load_config_files()
else:
QMessageBox.information(self, '信息', '删除操作已取消。')
else:
QMessageBox.warning(self, '警告', '没有选中任何项目。')
def get_current_config_file(self):
"""获取当前选中的配置文件"""
if self.combo_box.currentText():
return os.path.join(self.config_dir, self.combo_box.currentText() + ".json")
class AddProjectDialog(QDialog):
def __init__(self, dir, parent=None, path=None, combobox=None):
super(AddProjectDialog, self).__init__(parent)
self.setWindowTitle("添加/修改项目信息")
self.resize(800, 400) # 增加宽度以适应多列布局
self.dir = dir
self.path = path
self.combobox = combobox
# 定义所有表单项的配置
self.form_fields = [
{"key": "项目名称", "label": "项目名称:", "default": ""},
{"key": "风场名", "label": "风场名:", "default": ""},
{"key": "风场地址", "label": "风场地址:", "default": ""},
{"key": "甲方公司", "label": "甲方公司:", "default": ""},
{"key": "甲方负责人", "label": "甲方负责人:", "default": ""},
{"key": "甲方负责人电话", "label": "甲方负责人电话:", "default": ""},
{"key": "乙方公司", "label": "乙方公司:", "default": "武汉迪特聚能科技有限公司"},
{"key": "乙方负责人", "label": "乙方负责人:", "default": ""},
{"key": "乙方负责人电话", "label": "乙方负责人电话:", "default": ""},
{"key": "项目规格", "label": "项目规格:", "default": ""},
{"key": "项目工期", "label": "项目工期:", "default": ""},
]
# 加载现有数据(如果有)
self.data = {}
if self.path:
self.load_existing_data()
self.init_ui()
def load_existing_data(self):
"""加载现有项目数据"""
try:
with open(self.path, 'r', encoding='utf-8') as f:
self.data = json.load(f)
except json.JSONDecodeError:
QMessageBox.critical(self, "错误", "JSON 文件格式错误!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载文件失败: {str(e)}")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout()
# 创建表单布局
form_widget = QWidget()
form_layout = QHBoxLayout(form_widget)
# 创建两列布局
column1 = QVBoxLayout()
column2 = QVBoxLayout()
# 将表单字段添加到列中
self.fields = {}
for i, field in enumerate(self.form_fields):
# 创建标签和输入框
label = QLabel(field["label"])
edit = QLineEdit()
# 设置默认值或从现有数据加载
if self.path and field["key"] in self.data:
edit.setText(self.data[field["key"]])
else:
edit.setText(field["default"])
# 存储输入框引用
self.fields[field["key"]] = edit
# 根据索引决定添加到哪一列
if i < len(self.form_fields) / 2:
column1.addWidget(label)
column1.addWidget(edit)
else:
column2.addWidget(label)
column2.addWidget(edit)
# 添加间距和弹性空间
column1.addStretch()
column2.addStretch()
# 将列添加到表单布局
form_layout.addLayout(column1)
form_layout.addSpacing(20) # 列间距
form_layout.addLayout(column2)
# 创建按钮
self.save_button = QPushButton("保存")
self.save_button.clicked.connect(self.on_save_button_clicked)
# 添加到主布局
main_layout.addWidget(form_widget)
main_layout.addWidget(self.save_button)
self.setLayout(main_layout)
def on_save_button_clicked(self):
"""保存按钮点击事件"""
# 收集所有字段数据
project_info = {
field["key"]: self.fields[field["key"]].text()
for field in self.form_fields
}
project_info["json路径"] = self.dir
# 验证保存目录
if not os.path.exists(self.dir) or not os.path.isdir(self.dir):
QMessageBox.warning(self, "警告", "请选择一个有效的保存项目信息的文件夹路径")
return
# 构建默认文件名
default_filename = f"{project_info['项目名称']}.json"
# 让用户选择文件名
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存项目信息",
os.path.join(self.dir, default_filename),
"JSON Files (*.json)"
)
if not file_path:
QMessageBox.warning(self, "警告", "未选择文件名")
return
# 保存项目信息到文件
try:
with open(file_path, 'w', encoding='utf-8') as file:
json.dump(project_info, file, ensure_ascii=False, indent=4)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
return
self.parent().load_config_files()
# 提示用户保存成功
QMessageBox.information(self, "信息", f"项目信息已成功保存到 {file_path}")
# 关闭对话框
self.close()
class AddPersonnelDialog(QDialog):
def __init__(self, dir, parent=None, path=None, combobox=None):
super(AddPersonnelDialog, self).__init__(parent)
self.setWindowTitle("添加/修改单次检查信息")
self.resize(800, 400) # 增加宽度以适应多列布局
self.dir = dir
self.path = path
self.combobox = combobox
# 定义所有表单项的配置
self.form_fields = [
{"key": "检查人员", "label": "检查人员:", "default": ""},
{"key": "厂家人员", "label": "厂家人员:", "default": ""},
{"key": "业主人员", "label": "业主人员:", "default": ""},
{"key": "数据处理人员", "label": "数据处理人员:", "default": ""},
{"key": "报告编制人员", "label": "报告编制人员:", "default": ""},
{"key": "机组型号", "label": "机组型号:", "default": ""},
{"key": "机组厂家", "label": "机组厂家:", "default": ""},
{"key": "叶片型号", "label": "叶片型号", "default": ""},
{"key": "叶片厂家", "label": "叶片厂家:", "default": ""},
{"key": "施工日期", "label": "施工日期", "default": "开始-结束"},
#{"key": "", "label": "", "default": ""},
# 可以轻松添加更多人员字段
]
# 添加复选框字段
self.checkbox_fields = [
{"key": "外部检查", "label": "外部检查", "default": False},
{"key": "内部检查", "label": "内部检查", "default": False},
{"key": "防雷检查", "label": "防雷检查", "default": False}
]
# 加载现有数据(如果有)
self.data = {}
if self.path:
self.load_existing_data()
self.init_ui()
def load_existing_data(self):
"""加载现有人员数据"""
try:
with open(self.path, 'r', encoding='utf-8') as f:
self.data = json.load(f)
except json.JSONDecodeError:
QMessageBox.critical(self, "错误", "JSON 文件格式错误!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载文件失败: {str(e)}")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout()
# 创建表单布局
form_widget = QWidget()
form_layout = QHBoxLayout(form_widget)
# 创建两列布局
column1 = QVBoxLayout()
column2 = QVBoxLayout()
# 将表单字段添加到列中
self.fields = {}
for i, field in enumerate(self.form_fields):
# 创建标签和输入框
label = QLabel(field["label"])
edit = QLineEdit()
# 设置默认值或从现有数据加载
if self.path and field["key"] in self.data:
edit.setText(self.data[field["key"]])
else:
edit.setText(field["default"])
# 存储输入框引用
self.fields[field["key"]] = edit
# 根据索引决定添加到哪一列
if i < len(self.form_fields) / 2:
column1.addWidget(label)
column1.addWidget(edit)
else:
column2.addWidget(label)
column2.addWidget(edit)
# 添加间距和弹性空间
column1.addStretch()
column2.addStretch()
# 将列添加到表单布局
form_layout.addLayout(column1)
form_layout.addSpacing(20) # 列间距
form_layout.addLayout(column2)
# 创建按钮
self.save_button = QPushButton("保存")
self.save_button.clicked.connect(self.on_save_button_clicked)
# 创建复选框布局
checkbox_group = QGroupBox("检查类型")
checkbox_layout = QHBoxLayout()
# 添加复选框
self.checkboxes = {}
for field in self.checkbox_fields:
checkbox = QCheckBox(field["label"])
if self.path and field["key"] in self.data:
checkbox.setChecked(bool(self.data[field["key"]]))
else:
checkbox.setChecked(field["default"])
self.checkboxes[field["key"]] = checkbox
checkbox_layout.addWidget(checkbox)
checkbox_group.setLayout(checkbox_layout)
# 添加到主布局
main_layout.addWidget(checkbox_group)
main_layout.addWidget(form_widget)
main_layout.addWidget(self.save_button)
self.setLayout(main_layout)
def on_save_button_clicked(self):
"""保存按钮点击事件"""
# 收集所有字段数据
personnel_info = {
field["key"]: self.fields[field["key"]].text()
for field in self.form_fields
}
# 添加复选框数据
for key, checkbox in self.checkboxes.items():
personnel_info[key] = checkbox.isChecked()
personnel_info["json路径"] = self.dir
# 验证保存目录
if not os.path.exists(self.dir) or not os.path.isdir(self.dir):
QMessageBox.warning(self, "警告", "请选择一个有效的保存人员信息的文件夹路径")
return
# 构建默认文件名
default_filename = "人员信息.json"
# 让用户选择文件名
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存人员信息",
os.path.join(self.dir, default_filename),
"JSON Files (*.json)"
)
if not file_path:
QMessageBox.warning(self, "警告", "未选择文件名")
return
# 保存人员信息到文件
try:
with open(file_path, 'w', encoding='utf-8') as file:
json.dump(personnel_info, file, ensure_ascii=False, indent=4)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
return
self.parent().load_config_files()
# 提示用户保存成功
QMessageBox.information(self, "信息", f"人员信息已成功保存到 {file_path}")
# 关闭对话框
self.close()
class SmartDropdown(QWidget):
"""
支持动态同步的智能下拉框
-全局可保存共享选择池
-可编辑共享池
存储形式:
"""
shared_pool = {} # 类变量存储共享选项
class MyFontComboBox(QFontComboBox):
def __init__(self, parent=None):
super().__init__(parent)
# 获取内部的QLineEdit部件
self.line_edit = self.lineEdit()
# 当获得焦点时全选文本
self.line_edit.selectionChanged.connect(self.select_all_text)
def select_all_text(self):
if self.line_edit.hasFocus():
self.line_edit.selectAll()
@classmethod
def update_shared_pool(cls, dropdown_type, options):
"""更新指定类别的共享选项"""
if not options: # 如果选项为空,添加默认选项
options = ["未选中"]
cls.shared_pool[dropdown_type] = list(options)
cls.save_shared_pool()
# 自动刷新所有该类型的下拉框
for widget in QApplication.allWidgets():
if isinstance(widget, SmartDropdown) and widget.dropdown_type == dropdown_type:
widget._load_options()
@classmethod
def save_shared_pool(cls):
"""保存共享池到QSettings"""
settings = QSettings()
settings.setValue("SmartDropdown/shared_pool", cls.shared_pool)
@classmethod
def load_shared_pool(cls):
"""从QSettings加载共享池"""
settings = QSettings()
pool = settings.value("SmartDropdown/shared_pool", {})
cls.shared_pool = {k: v for k, v in pool.items()} if pool else {}
# 确保每个类别至少有一个默认选项
for dropdown_type in cls.shared_pool:
if not cls.shared_pool[dropdown_type]:
cls.shared_pool[dropdown_type] = ["未选中"]
def __init__(self, dropdown_type, messagebrowser, label_text=None, parent=None):
super().__init__(parent)
self.dropdown_type = dropdown_type
self.label_text = label_text
self.messagebrowser = messagebrowser
# 初始化共享池
if not self.shared_pool:
self.load_shared_pool()
# 确保当前类型在共享池中存在
if self.dropdown_type not in self.shared_pool:
self.shared_pool[self.dropdown_type] = ["未选中"]
self.save_shared_pool()
self._setup_ui()
self._load_options()
self._initialize_defect_type()
self.messagebrowser.append(f"初始化下拉框: {dropdown_type}")
@property
def current_index(self):
"""当前选中索引0表示未选中"""
return self.combo_box.currentIndex()
@current_index.setter
def current_index(self, index):
if self.combo_box.count() > 0:
clamped_index = max(0, min(index, self.combo_box.count()-1))
self.combo_box.setCurrentIndex(clamped_index)
def _initialize_defect_type(self):
"""初始化缺陷类型选项,如果选项不足则自动重置"""
if self.dropdown_type == "缺陷类型":
# 检查当前选项数量是否不足
if len(self.shared_pool.get(self.dropdown_type, [])) <= 1:
default_options = [
"未选中...", "前缘涂层损伤(风损)", "表面裂纹", "腻子脱落", "漆面脱落",
"油污","布层破损(深层、不规则面状)", "裂纹(浅层线状)", "开裂(深层线带窄面状)",
"贯穿损伤", "鼓包", "凹陷", "雷击45d", "雷击zzw", "雷击ck",
"合模缝开裂", "透光", "雷击碳化", "褶皱", "脱胶", "缺胶",
"防雷线断裂", "开裂"
]
self.update_shared_pool(self.dropdown_type, default_options)
self.messagebrowser.append(
f"初始化提示: 检测到{self.dropdown_type}选项不足,已自动重置为默认选项集"
f"(原因: 第一次初始化或选项被清空)"
)
def _setup_ui(self):
"""初始化UI组件"""
self.combo_box = QFontComboBox()
self.combo_box.setEditable(True)
# 操作按钮
self.add_btn = QPushButton("+添加输入的类别")
self.del_btn = QPushButton("-删除选中类别")
self.refresh_btn = QPushButton()
self.refresh_btn.setIcon(QIcon.fromTheme("view-refresh")) # 使用系统刷新图标
self.reset_btn = QPushButton("重置")
# 设置按钮大小
for btn in [self.add_btn, self.del_btn, self.reset_btn]:
btn.setFixedWidth(150)
self.refresh_btn.setFixedWidth(30)
# 设置工具提示
self.add_btn.setToolTip("添加选项到共享池")
self.del_btn.setToolTip("从共享池删除当前选项")
self.refresh_btn.setToolTip("同步共享池选项")
self.reset_btn.setToolTip("重置为默认选项")
# 按钮布局
btn_layout = QHBoxLayout()
btn_layout.addWidget(self.add_btn)
btn_layout.addWidget(self.del_btn)
btn_layout.addWidget(self.reset_btn)
btn_layout.addWidget(self.refresh_btn)
btn_layout.setSpacing(2)
btn_layout.setContentsMargins(0, 0, 0, 0)
# 主布局
main_layout = QVBoxLayout()
main_layout.setSpacing(5)
if self.label_text:
main_layout.addWidget(QLabel(self.label_text))
main_layout.addWidget(self.combo_box)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
# 信号连接
self.add_btn.clicked.connect(self._add_option)
self.del_btn.clicked.connect(self._del_option)
self.refresh_btn.clicked.connect(self._refresh_options)
self.reset_btn.clicked.connect(self._reset_options)
self.combo_box.currentIndexChanged.connect(self._update_buttons)
def _reset_options(self):
"""重置选项为默认值"""
if self.dropdown_type == "缺陷类型":
default_options = [
"未选中...", "前缘涂层损伤(风损)", "表面裂纹", "腻子脱落", "漆面脱落",
"油污","布层破损(深层、不规则面状)", "裂纹(浅层线状)", "开裂(深层线带窄面状)",
"贯穿损伤", "鼓包", "凹陷", "雷击45d", "雷击zzw", "雷击ck",
"合模缝开裂", "透光", "雷击碳化", "褶皱", "脱胶", "缺胶",
"防雷线断裂", "开裂"
]
reply = QMessageBox.question(
self, '确认重置',
'确定要重置缺陷类型为默认选项吗?此操作不可撤销!',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.update_shared_pool(self.dropdown_type, default_options)
self.messagebrowser.append(f"已重置 {self.dropdown_type} 为默认选项")
else:
QMessageBox.information(
self, '提示',
f'当前类型 {self.dropdown_type} 没有预定义的重置选项')
def _load_options(self):
"""加载选项(确保只有一个未选中选项)"""
current_text = self.combo_box.currentText() if self.combo_box.count() > 0 else ""
self.combo_box.blockSignals(True) # 防止触发信号
self.combo_box.clear()
# 确保共享池中有当前类型的选项
if (self.dropdown_type not in self.shared_pool or
not self.shared_pool[self.dropdown_type]):
self.shared_pool[self.dropdown_type] = ["未选中"]
self.save_shared_pool()
# 直接从共享池加载选项
self.combo_box.addItems(self.shared_pool[self.dropdown_type])
# 恢复之前的选中项(如果有)
if current_text:
idx = self.combo_box.findText(current_text)
if idx >= 0:
self.combo_box.setCurrentIndex(idx)
self.combo_box.blockSignals(False)
self._update_buttons()
self.messagebrowser.append(f"加载选项: {self.dropdown_type} - {self.shared_pool[self.dropdown_type]}")
def _refresh_options(self):
"""手动刷新选项"""
self.messagebrowser.append(f"手动刷新选项: {self.dropdown_type}")
self._load_options()
def _update_buttons(self):
"""根据当前状态更新按钮可用性"""
has_options = len(self.shared_pool.get(self.dropdown_type, [])) > 0
has_selection = self.current_index >= 0
self.del_btn.setEnabled(has_options and has_selection)
self.add_btn.setEnabled(True)
self.refresh_btn.setEnabled(True)
self.reset_btn.setEnabled(True)
def _add_option(self):
"""添加新选项到共享池"""
new_option = self.combo_box.currentText().strip()
if not new_option:
return
if new_option not in self.shared_pool[self.dropdown_type]:
if QMessageBox.Yes == QMessageBox.question(
self, "确认添加",
f"添加 '{new_option}''{self.dropdown_type}' 选项池?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No):
if self.dropdown_type not in self.shared_pool:
self.shared_pool[self.dropdown_type] = ["未选中"]
self.shared_pool[self.dropdown_type].append(new_option)
self.messagebrowser.append(f"添加选项: {new_option}{self.dropdown_type}")
self.update_shared_pool(self.dropdown_type, self.shared_pool[self.dropdown_type])
self.combo_box.setCurrentIndex(self.combo_box.findText(new_option))
else:
self.messagebrowser.append(f"添加失败: 选项 {new_option} 已存在于 {self.dropdown_type} 选项池")
def _del_option(self):
"""从共享池删除当前选项"""
if self.current_index < 0: # 无效选择
return
current_text = self.combo_box.currentText()
if current_text == "未选中":
self.messagebrowser.append("不能删除默认的'未选中'选项")
return
if QMessageBox.Yes == QMessageBox.question(
self, "确认删除",
f"'{self.dropdown_type}' 删除 '{current_text}'",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No):
try:
self.shared_pool[self.dropdown_type].remove(current_text)
# 如果删除后选项为空,恢复默认选项
if not self.shared_pool[self.dropdown_type]:
self.shared_pool[self.dropdown_type] = ["未选中"]
self.messagebrowser.append(f"删除选项: {current_text}{self.dropdown_type}")
self.update_shared_pool(self.dropdown_type, self.shared_pool[self.dropdown_type])
except ValueError:
self.messagebrowser.append(f"删除失败: 选项 {current_text} 不存在")
class FolderDropWidget(QWidget):
selection_changed = Signal(list)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.current_dir = None
self.selected_folders = set()
self.init_ui()
self.apply_styles()
def init_ui(self):
main_layout = QVBoxLayout()
main_layout.setSpacing(5)
main_layout.setContentsMargins(0, 0, 0, 0)
# 顶部按钮区域
btn_layout = QHBoxLayout()
self.reset_btn = QPushButton("重新选择项目文件夹")
self.reset_btn.clicked.connect(self.reset_selection)
self.reset_btn.setFixedSize(120, 40)
self.reset_btn.setVisible(False)
self.clear_btn = QPushButton("清空")
self.clear_btn.clicked.connect(self.clear_selected)
self.clear_btn.setFixedSize(120, 40)
self.clear_btn.setVisible(False)
self.full_select_btn = QPushButton("全选")
self.full_select_btn.clicked.connect(self.select_all_folders)
self.full_select_btn.setFixedSize(120, 40)
self.full_select_btn.setVisible(False)
# 添加返回按钮
self.back_btn = QPushButton("返回项目选择")
self.back_btn.clicked.connect(lambda: (self.stacked_layout.setCurrentIndex(1) or self.clear_selected() or self.reset_btn.setVisible(True) or self.back_btn.setVisible(False)))
self.back_btn.setVisible(False)
btn_layout.addWidget(self.reset_btn)
btn_layout.addWidget(self.back_btn)
btn_layout.addWidget(self.full_select_btn)
btn_layout.addWidget(self.clear_btn)
btn_layout.addStretch()
main_layout.addLayout(btn_layout)
# 主内容区域 - 使用堆叠布局管理不同状态
self.stacked_layout = QStackedLayout()
# 状态1: 未选择文件夹时的提示
self.prompt_container = QWidget()
prompt_layout = QVBoxLayout()
self.prompt_label = QLabel("拖放文件夹到此处或点击选择文件夹")
self.prompt_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.prompt_label.mousePressEvent = self.select_folder
prompt_layout.addWidget(self.prompt_label)
self.prompt_container.setLayout(prompt_layout)
self.stacked_layout.addWidget(self.prompt_container)
# 状态2: 显示第一层文件夹 (项目选择)
self.level1_container = QWidget()
level1_main_layout = QVBoxLayout()
level1_main_layout.setSpacing(0)
level1_main_layout.setContentsMargins(0, 0, 0, 0)
# 添加状态标签
self.level1_title = QLabel("项目选择")
self.level1_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
level1_main_layout.addWidget(self.level1_title)
self.level1_list = QListWidget()
self.level1_list.setIconSize(QSize(48, 48))
self.level1_list.setViewMode(QListWidget.ViewMode.IconMode)
self.level1_list.setResizeMode(QListWidget.ResizeMode.Adjust)
self.level1_list.setMovement(QListWidget.Movement.Static)
self.level1_list.setSpacing(10)
self.level1_list.itemDoubleClicked.connect(self.show_level2_folders)
level1_main_layout.addWidget(self.level1_list)
self.level1_container.setLayout(level1_main_layout)
self.stacked_layout.addWidget(self.level1_container)
# 状态3: 显示第二层文件夹 (机组选择)
self.level2_container = QWidget()
level2_main_layout = QVBoxLayout()
level2_main_layout.setSpacing(0)
level2_main_layout.setContentsMargins(0, 0, 0, 0)
# 添加状态标签
self.level2_title = QLabel("机组选择")
self.level2_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
level2_main_layout.addWidget(self.level2_title)
self.level2_list = QListWidget()
self.level2_list.setIconSize(QSize(48, 48))
self.level2_list.setViewMode(QListWidget.ViewMode.IconMode)
self.level2_list.setResizeMode(QListWidget.ResizeMode.Adjust)
self.level2_list.setMovement(QListWidget.Movement.Static)
self.level2_list.setSpacing(10)
level2_main_layout.addWidget(self.level2_list)
self.level2_container.setLayout(level2_main_layout)
self.stacked_layout.addWidget(self.level2_container)
# 将堆叠布局添加到主布局
stack_container = QWidget()
stack_container.setLayout(self.stacked_layout)
main_layout.addWidget(stack_container)
# 已选目录列表 (始终显示在底部)
self.selected_group = QGroupBox("已选机组")
self.selected_group.setVisible(False)
selected_layout = QVBoxLayout()
self.selected_list = QListWidget()
self.selected_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
selected_layout.addWidget(self.selected_list)
self.selected_group.setLayout(selected_layout)
main_layout.addWidget(self.selected_group)
self.setLayout(main_layout)
def select_all_folders(self):
self.selected_folders.update(
set(item.data(Qt.ItemDataRole.UserRole) for item in self.level2_list.findItems("", Qt.MatchFlag.MatchContains))
)
self.update_checkbox_states()
self.selection_changed.emit(self.get_selected_folders())
def apply_styles(self):
self.prompt_label.setStyleSheet(f"""
{PATH_DISPLAY_STYLE}
border: 2px dashed {SECONDARY_COLOR};
min-height: 150px;
""")
self.level1_list.setStyleSheet(f"""
QListWidget {{
background-color: #f8f9fa;
min-height: 150px;
}}
""")
self.level2_list.setStyleSheet(f"""
QListWidget {{
background-color: #f8f9fa;
min-height: 150px;
}}
""")
self.level1_title.setStyleSheet(f"""
{LABEL_STYLE}
font-size: {TITLE_FONT_SIZE}pt;
font-weight: bold;
padding: 10px;
""")
self.level2_title.setStyleSheet(f"""
{LABEL_STYLE}
font-size: {TITLE_FONT_SIZE}pt;
font-weight: bold;
padding: 10px;
""")
self.selected_group.setStyleSheet(GROUP_BOX_STYLE)
self.full_select_btn.setStyleSheet(BUTTON_STYLE)
self.reset_btn.setStyleSheet(BUTTON_STYLE)
self.clear_btn.setStyleSheet(BUTTON_STYLE)
self.back_btn.setStyleSheet(BUTTON_STYLE)
def select_folder(self, event=None):
folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
if folder:
self.load_folder(folder)
def reset_selection(self):
self.current_dir = None
self.selected_folders.clear()
self.level1_list.clear()
self.level2_list.clear()
self.selected_list.clear()
self.stacked_layout.setCurrentIndex(0) # 显示提示
self.reset_btn.setVisible(False)
self.clear_btn.setVisible(False)
self.back_btn.setVisible(False)
self.selected_group.setVisible(False)
self.selection_changed.emit([])
def clear_selected(self):
self.selected_folders.clear()
self.selected_list.clear()
self.update_checkbox_states()
self.selection_changed.emit([])
def load_folder(self, path):
self.current_dir = path
self.load_level1_folders(path)
self.stacked_layout.setCurrentIndex(1) # 显示第一层
self.reset_btn.setVisible(True)
self.back_btn.setVisible(False)
self.clear_btn.setVisible(True)
self.full_select_btn.setVisible(True)
self.selected_group.setVisible(True)
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
for url in event.mimeData().urls():
path = url.toLocalFile()
if os.path.isdir(path):
self.load_folder(path)
break
def load_level1_folders(self, root_path):
self.level1_list.clear()
self.level2_list.clear()
for item in os.listdir(root_path):
item_path = os.path.join(root_path, item)
if os.path.isdir(item_path):
item_widget = QListWidgetItem(QIcon.fromTheme("folder"), item)
item_widget.setData(Qt.ItemDataRole.UserRole, item_path)
item_widget.setSizeHint(QSize(80, 80))
item_widget.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignBottom)
self.level1_list.addItem(item_widget)
def show_level2_folders(self, item):
self.level2_list.clear()
self.reset_btn.setVisible(False)
self.back_btn.setVisible(True)
folder_path = item.data(Qt.ItemDataRole.UserRole)
for sub_item in os.listdir(folder_path):
sub_item_path = os.path.join(folder_path, sub_item)
if os.path.isdir(sub_item_path):
list_item = QListWidgetItem()
list_item.setSizeHint(QSize(100, 80))
list_item.setData(Qt.ItemDataRole.UserRole, sub_item_path)
widget = QWidget()
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
icon_label = QLabel()
icon_label.setPixmap(QIcon.fromTheme("folder").pixmap(48, 48))
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label)
name_check_layout = QHBoxLayout()
checkbox = QCheckBox(sub_item)
checkbox.setStyleSheet(f"""
{PRIMARY_CHECKBOX_STYLE}
QCheckBox::indicator:checked {{
background-color: {PRIMARY_COLOR};
border: 1px solid {PRIMARY_COLOR};
}}
""")
checkbox.stateChanged.connect(lambda state, path=sub_item_path: self.toggle_folder_selection(path, state))
checkbox.setChecked(sub_item_path in self.selected_folders)
name_check_layout.addWidget(checkbox)
layout.addLayout(name_check_layout)
widget.setLayout(layout)
list_item.setSizeHint(widget.sizeHint())
self.level2_list.addItem(list_item)
self.level2_list.setItemWidget(list_item, widget)
self.stacked_layout.setCurrentIndex(2) # 显示第二层
def toggle_folder_selection(self, folder_path, state):
if state == Qt.CheckState.Checked.value:
self.selected_folders.add(folder_path)
item = QListWidgetItem(folder_path)
self.selected_list.addItem(item)
else:
self.selected_folders.discard(folder_path)
items = self.selected_list.findItems(folder_path, Qt.MatchFlag.MatchExactly)
for item in items:
self.selected_list.takeItem(self.selected_list.row(item))
self.selection_changed.emit(self.get_selected_folders())
def update_checkbox_states(self):
for i in range(self.level2_list.count()):
item = self.level2_list.item(i)
widget = self.level2_list.itemWidget(item)
if widget:
checkbox = widget.findChild(QCheckBox)
if checkbox:
path = item.data(Qt.ItemDataRole.UserRole)
checkbox.setChecked(path in self.selected_folders)
def get_selected_folders(self):
return list(self.selected_folders)
class DraggableLine(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setFrameShape(QFrame.HLine)
self.setFrameShadow(QFrame.Sunken)
self.setLineWidth(2)
self.setFixedHeight(10)
self.dragging = False
self.setCursor(Qt.SizeVerCursor)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging = True
self.start_pos = event.globalPos()
def mouseMoveEvent(self, event):
if self.dragging:
delta = event.globalPos() - self.start_pos
self.start_pos = event.globalPos()
# 通知父窗口调整布局
self.parent().adjust_row_height(delta.y())
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging = False
class OutputDirSelector(QWidget):
# 定义一个信号,当路径改变时发出
path_changed = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.output_dir = ""
self.init_ui()
self.set_style()
def init_ui(self):
# 主布局
layout = QVBoxLayout(self)
layout.setSpacing(10)
layout.setContentsMargins(0, 0, 0, 0)
# 组框
self.group_box = QFrame()
self.group_box.setFrameShape(QFrame.StyledPanel)
group_layout = QVBoxLayout(self.group_box)
group_layout.setSpacing(GROUP_BOX_SPACING)
group_layout.setContentsMargins(*GROUP_BOX_MARGINS)
# 标题标签
self.setWindowTitle("选择输出目录")
# 路径显示标签
self.path_label = QLabel("未选择文件夹")
self.path_label.setWordWrap(True)
self.path_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
# 选择按钮
self.select_button = QPushButton("选择输出文件夹")
self.select_button.clicked.connect(self.select_output_dir)
# 添加部件到布局
group_layout.addWidget(self.path_label)
group_layout.addWidget(self.select_button)
# 添加组框到主布局
layout.addWidget(self.group_box)
def set_style(self):
# 设置组框样式
self.group_box.setStyleSheet(GROUP_BOX_STYLE)
# 设置路径显示样式
self.path_label.setStyleSheet(PATH_DISPLAY_STYLE)
# 设置按钮样式
self.select_button.setStyleSheet(PRIMARY_BUTTON_STYLE)
# 设置最小尺寸
self.group_box.setMinimumSize(GROUP_BOX_MIN_WIDTH, GROUP_BOX_MIN_HEIGHT)
def select_output_dir(self):
"""打开文件夹选择对话框"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择输出文件夹",
"", # 默认路径为空,使用系统默认
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
if dir_path: # 如果用户选择了文件夹
self.output_dir = dir_path
self.path_label.setText(dir_path)
self.path_changed.emit(dir_path) # 发出路径改变信号
def get_output_dir(self):
"""获取输出路径"""
return self.output_dir
def set_output_dir(self, path):
"""设置输出路径"""
if path:
self.output_dir = path
self.path_label.setText(path)
self.path_changed.emit(path)
class JsonFileHandler:
"""处理JSON文件的读写操作"""
@staticmethod
def read_json(file_path):
"""读取JSON文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error reading JSON file {file_path}: {e}")
return None
@staticmethod
def write_json(file_path, data):
try:
# 检查目录是否可写
dir_path = os.path.dirname(file_path)
if not os.access(dir_path, os.W_OK):
print(f"错误:目录不可写 {dir_path}")
return False
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print(f"成功写入文件:{file_path}")
return True
except Exception as e:
print(f"写入文件错误:{str(e)}")
return False
class TreeModelManager:
"""管理树形模型的数据操作"""
# 中英文映射字典
TRANSLATION_MAP = {
# 左侧树
"Directory Structure": "目录结构",
# 右侧树
"Information": "信息",
"Value": "",
"turbinecode": "机组编号",
"part1": "叶片1",
"part2": "叶片2",
"part3": "叶片3",
"code": "部件代码",
"defect_picture_dict": "缺陷图字典",
"typical_picture_dict": "典型图字典"
}
@staticmethod
def translate_text(key):
"""根据映射表翻译文本"""
return TreeModelManager.TRANSLATION_MAP.get(key, key)
@staticmethod
def reverse_translate_text(chinese_text):
"""根据中文反向查找英文键"""
reverse_map = {v: k for k, v in TreeModelManager.TRANSLATION_MAP.items()}
return reverse_map.get(chinese_text, chinese_text)
@staticmethod
def set_item_color(item, color=QColor(255, 0, 0)):
"""设置树节点的文本颜色"""
if item is not None:
item.setForeground(color)
@staticmethod
def create_left_tree_model(folder_list, required_child_count=3):
"""创建左侧文件夹树模型
Args:
folder_list: 文件夹路径列表
required_child_count: 要求的子目录数量默认为3
"""
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["目录结构"])
folder_map = {}
for folder_path in folder_list:
folder_name = os.path.basename(folder_path)
folder_map[folder_name] = folder_path
parent_item = QStandardItem(folder_name)
model.appendRow(parent_item)
try:
child_dirs = []
for child in os.listdir(folder_path):
child_path = os.path.join(folder_path, child)
folder_map[child] = child_path
if os.path.isdir(child_path):
child_dirs.append(child)
child_item = QStandardItem(child)
parent_item.appendRow(child_item)
# 检查子目录数量是否满足要求
if len(child_dirs) < required_child_count:
TreeModelManager.set_item_color(parent_item) # 不满足则标红
except Exception as e:
print(f"Error reading folder {folder_path}: {e}")
TreeModelManager.set_item_color(parent_item) # 读取错误也标红
return model, folder_map
@staticmethod
def create_right_tree_model(folder_names):
"""创建右侧可编辑树模型"""
model = QStandardItemModel()
model.setHorizontalHeaderLabels([
TreeModelManager.translate_text("Information"),
TreeModelManager.translate_text("Value")
])
json_data = {}
for folder_name in folder_names:
parent_item = QStandardItem(folder_name)
parent_item.setEditable(False)
# 添加机组编号
turbine_item = QStandardItem(TreeModelManager.translate_text("turbinecode"))
turbine_value = QStandardItem(folder_name.replace("机组", "").strip())
turbine_value.setEditable(True)
parent_item.appendRow([turbine_item, turbine_value])
# 添加默认的三个子部件
for i in range(1, 4):
part_item = QStandardItem(TreeModelManager.translate_text(f"part{i}"))
part_item.setEditable(False)
# 部件代码
code_item = QStandardItem(TreeModelManager.translate_text("code"))
code_value = QStandardItem("001")
code_value.setEditable(True)
part_item.appendRow([code_item, code_value])
# 缺陷图字典
defect_pic_item = QStandardItem(TreeModelManager.translate_text("defect_picture_dict"))
defect_pic_item.setEditable(False)
part_item.appendRow([defect_pic_item, QStandardItem("{}")])
# 典型图字典
typical_pic_item = QStandardItem(TreeModelManager.translate_text("typical_picture_dict"))
typical_pic_item.setEditable(False)
part_item.appendRow([typical_pic_item, QStandardItem("{}")])
parent_item.appendRow(part_item)
model.appendRow(parent_item)
# 初始化JSON数据保持英文键
json_data[folder_name] = {
"turbinecode": folder_name.replace("机组", "").strip(),
"part1": {
"code": "001",
"defect_picture_dict": {},
"typical_picture_dict": {}
},
"part2": {
"code": "001",
"defect_picture_dict": {},
"typical_picture_dict": {}
},
"part3": {
"code": "001",
"defect_picture_dict": {},
"typical_picture_dict": {}
}
}
return model, json_data
@staticmethod
def update_model_from_json(model, folder_name, json_data):
"""根据JSON数据更新模型"""
parent_item = None
for i in range(model.rowCount()):
item = model.item(i)
if item and item.text() == folder_name:
parent_item = item
break
if parent_item and folder_name in json_data:
data = json_data[folder_name]
# 更新机组编号
turbine_item = parent_item.child(0)
if turbine_item and turbine_item.child(0, 1):
turbine_item.child(0, 1).setText(data.get("turbinecode", ""))
# 更新部件信息
for j in range(1, 4):
part_item = parent_item.child(j)
if part_item:
part_name = TreeModelManager.reverse_translate_text(part_item.text()) # 反向查找英文键
if part_name in data:
# 更新部件代码
code_item = part_item.child(0, 1)
if code_item:
code_item.setText(data[part_name].get("code", "001"))
# 更新缺陷图字典
defect_pic_item = part_item.child(1, 1)
if defect_pic_item:
defect_pic_dict = data[part_name].get("defect_picture_dict", {})
defect_pic_item.setText(str(defect_pic_dict))
# 更新典型图字典
typical_pic_item = part_item.child(2, 1)
if typical_pic_item:
typical_pic_dict = data[part_name].get("typical_picture_dict", {})
typical_pic_item.setText(str(typical_pic_dict))
@staticmethod
def update_json_from_model(model, json_data):
"""从模型更新JSON数据"""
for i in range(model.rowCount()):
folder_index = model.index(i, 0)
folder_name = model.data(folder_index)
if not folder_name:
continue
# 确保数据结构存在
if folder_name not in json_data:
json_data[folder_name] = {
"turbinecode": "",
"part1": {"code": "001", "defect_picture_dict": {}, "typical_picture_dict": {}},
"part2": {"code": "001", "defect_picture_dict": {}, "typical_picture_dict": {}},
"part3": {"code": "001", "defect_picture_dict": {}, "typical_picture_dict": {}}
}
# 获取 turbinecode 值
turbine_index = model.index(0, 1, folder_index)
if turbine_index.isValid():
new_value = model.data(turbine_index)
json_data[folder_name]["turbinecode"] = new_value
# 更新parts
for part_num in range(1, 4):
part_index = model.index(part_num, 0, folder_index)
if part_index.isValid():
part_name = TreeModelManager.reverse_translate_text(model.data(part_index)) # 反向查找英文键
# 更新部件代码
code_index = model.index(0, 1, part_index)
if code_index.isValid():
json_data[folder_name][part_name]["code"] = model.data(code_index, Qt.DisplayRole)
# 更新缺陷图字典
defect_pic_index = model.index(1, 1, part_index)
if defect_pic_index.isValid():
defect_pic_str = model.data(defect_pic_index, Qt.DisplayRole)
try:
json_data[folder_name][part_name]["defect_picture_dict"] = eval(defect_pic_str)
except:
json_data[folder_name][part_name]["defect_picture_dict"] = {}
# 更新典型图字典
typical_pic_index = model.index(2, 1, part_index)
if typical_pic_index.isValid():
typical_pic_str = model.data(typical_pic_index, Qt.DisplayRole)
try:
json_data[folder_name][part_name]["typical_picture_dict"] = eval(typical_pic_str)
except:
json_data[folder_name][part_name]["typical_picture_dict"] = {}
class DirectSaveDelegate(QStyledItemDelegate):
"""直接保存的委托实现"""
def __init__(self, save_callback, parent=None):
super().__init__(parent)
self.save_callback = save_callback
def setModelData(self, editor, model, index):
try:
# 提交数据前验证索引
if not index.isValid():
print("⚠️ 无效的模型索引")
return
old_value = index.data(Qt.DisplayRole)
super().setModelData(editor, model, index)
new_value = index.data(Qt.DisplayRole)
print(f"📝 数据变更: {old_value}{new_value}")
# 获取顶层文件夹索引
top_index = index
while top_index.parent().isValid():
top_index = top_index.parent()
if model.hasIndex(top_index.row(), top_index.column(), top_index.parent()):
folder_name = model.data(top_index, Qt.DisplayRole)
self.save_callback(folder_name)
except Exception as e:
print(f"❌ 委托错误: {str(e)}")
import traceback
traceback.print_exc()
class FolderBrowser(QWidget):
"""文件夹浏览编辑主窗口"""
data_changed = Signal(dict) # 数据变更信号
folder_selected = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.output_path = QDir.currentPath() # 默认输出路径
self.json_data = {} # 存储所有JSON数据
self.folder_map = {} # 文件夹路径映射
self.current_image_dir = "" # 当前显示的图片目录
self.init_ui()
self.setup_connections()
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(5, 5, 5, 5)
# 上下分割的主区域
main_splitter = QSplitter(Qt.Vertical)
# 上半部分 - 左右分割的树视图
tree_splitter = QSplitter(Qt.Horizontal)
# 左侧文件夹树视图
self.left_model = QStandardItemModel()
self.left_model.setHorizontalHeaderLabels(["目录结构"])
self.left_tree = QTreeView()
self.left_tree.setModel(self.left_model)
self.left_tree.setEditTriggers(QTreeView.NoEditTriggers)
# 右侧可编辑树视图
self.right_model = QStandardItemModel()
self.right_model.setHorizontalHeaderLabels(["信息", ""])
self.right_tree = QTreeView()
self.right_tree.setModel(self.right_model)
self.right_tree.setItemDelegate(
DirectSaveDelegate(self.handle_immediate_save, self.right_tree)
)
# 添加到水平分割器
tree_splitter.addWidget(self.left_tree)
tree_splitter.addWidget(self.right_tree)
tree_splitter.setSizes([300, 500])
# 下半部分 - 图片浏览器和字典浏览器
image_splitter = QSplitter(Qt.Horizontal)
# 左侧图片浏览器
self.image_browser = ImageBrowser()
# 右侧字典浏览器
self.dictionary_browser = DictionaryBrowser()
# 添加到水平分割器
image_splitter.addWidget(self.image_browser)
image_splitter.addWidget(self.dictionary_browser)
image_splitter.setSizes([400, 400])
# 添加到垂直分割器
main_splitter.addWidget(tree_splitter)
main_splitter.addWidget(image_splitter)
main_splitter.setSizes([400, 400])
main_layout.addWidget(main_splitter)
# 设置拖放接收
self.dictionary_browser.setAcceptDrops(True)
def setup_connections(self):
"""设置信号和槽的连接"""
self.right_model.dataChanged.connect(self.on_right_data_changed)
self.left_tree.expanded.connect(self.sync_right_tree_expand)
self.left_tree.collapsed.connect(self.sync_right_tree_collapse)
self.right_tree.expanded.connect(self.sync_left_tree_expand)
self.right_tree.collapsed.connect(self.sync_left_tree_collapse)
self.right_tree.clicked.connect(self.on_right_tree_clicked)
self.left_tree.clicked.connect(self.on_left_tree_clicked)
self.folder_selected.connect(self.image_browser.show_images)
self.folder_selected.connect(self.update_blade_name)
def on_right_tree_clicked(self, index):
"""处理右侧树视图点击事件"""
if not index.isValid():
return
item = self.right_model.itemFromIndex(index)
path = self.get_item_path(item)
print(f"点击了右侧树节点: {item.text()}")
# 检查是否点击了字典节点
if "dictionaries" in path:
parts = path.split("/dictionaries/")
if len(parts) == 2:
blade_name = parts[0]
dict_type = parts[1]
# 获取字典数据
dict_data = self.json_data.get(blade_name, {}).get("dictionaries", {}).get(dict_type, {})
# 更新字典浏览器
self.dictionary_browser.show_dictionary(blade_name, dict_type, dict_data)
def get_item_path(self, item):
"""获取树节点路径"""
path = []
while item is not None:
path.append(item.text())
item = item.parent()
return "/".join(reversed(path))
def update_blade_name(self, folder_path):
"""从文件夹路径中提取叶片名称并更新字典浏览器"""
# 假设叶片名称是文件夹路径的最后一部分
blade_name = os.path.basename(folder_path.rstrip('/\\'))
self.dictionary_browser.set_current_blade(blade_name)
def save_dictionary_data(self, dict_type, blade_name, data):
"""保存字典数据到JSON"""
# 找到对应的文件夹项
for folder_name in self.json_data:
if blade_name in folder_name: # 简单匹配逻辑,可根据实际情况调整
# 在JSON数据中创建或更新字典数据
if "dictionaries" not in self.json_data[folder_name]:
self.json_data[folder_name]["dictionaries"] = {}
if dict_type not in self.json_data[folder_name]["dictionaries"]:
self.json_data[folder_name]["dictionaries"][dict_type] = {}
self.json_data[folder_name]["dictionaries"][dict_type] = data
self.save_to_json(folder_name)
break
def on_left_tree_clicked(self, index):
"""处理左侧树视图选择变化事件"""
if not index.isValid():
return
item = self.left_model.itemFromIndex(index)
relative_path = self.folder_map.get(item.text(), "")
print(f"🔍 选择文件夹: {relative_path}")
self.folder_selected.emit(relative_path)
def handle_immediate_save(self, folder_name):
print(f"🔧 保存触发 [{folder_name}]")
try:
# 安全获取文件夹索引
matches = self.right_model.match(
self.right_model.index(0, 0),
Qt.DisplayRole,
folder_name,
hits=1,
flags=Qt.MatchExactly
)
if not matches:
print(f"❌ 找不到文件夹: {folder_name}")
return
folder_index = matches[0]
# 获取 turbinecode 值(使用标准模型访问方式)
turbine_index = self.right_model.index(0, 1, folder_index)
turbine_value = self.right_model.data(turbine_index, Qt.DisplayRole)
print(f"📊 当前值 - turbinecode: {turbine_value or '<空>'}")
# 更新数据
TreeModelManager.update_json_from_model(self.right_model, self.json_data)
self.save_to_json(folder_name)
except Exception as e:
print(f"❌ 保存错误: {str(e)}")
def refresh_views(self):
"""刷新左右视图显示"""
self.left_tree.setModel(None) # 先重置模型
self.left_tree.setModel(self.left_model)
self.right_tree.setModel(None)
self.right_tree.setModel(self.right_model)
# 展开第一层节点
for i in range(self.left_model.rowCount()):
self.left_tree.expand(self.left_model.index(i, 0))
self.right_tree.expand(self.right_model.index(i, 0))
def copy_left_tree_text(self):
"""复制左侧树选中的文本"""
index = self.left_tree.currentIndex()
if index.isValid():
text = self.left_model.data(index, Qt.DisplayRole)
QApplication.clipboard().setText(text)
def sync_right_tree_expand(self, index):
"""同步右侧树的展开状态(仅第一层)"""
if not index.parent().isValid(): # 只处理第一层
right_index = self.right_model.index(index.row(), 0)
self.right_tree.expand(right_index)
def sync_right_tree_collapse(self, index):
"""同步右侧树的折叠状态(仅第一层)"""
if not index.parent().isValid(): # 只处理第一层
right_index = self.right_model.index(index.row(), 0)
self.right_tree.collapse(right_index)
def sync_left_tree_expand(self, index):
"""同步左侧树的展开状态(仅第一层)"""
if not index.parent().isValid(): # 只处理第一层
left_index = self.left_model.index(index.row(), 0)
self.left_tree.expand(left_index)
def sync_left_tree_collapse(self, index):
"""同步左侧树的折叠状态(仅第一层)"""
if not index.parent().isValid(): # 只处理第一层
left_index = self.left_model.index(index.row(), 0)
self.left_tree.collapse(left_index)
def set_output_path(self, path):
"""设置JSON文件输出路径"""
self.output_path = path
if not os.path.exists(path):
os.makedirs(path)
def set_initial_folders(self, folder_list):
"""初始化文件夹结构"""
self.left_model.clear()
self.right_model.clear()
self.left_model.setHorizontalHeaderLabels(["目录结构"])
self.right_model.setHorizontalHeaderLabels(["信息", ""])
self.json_data = {}
self.folder_map = {}
if not folder_list: # 添加空列表检查
print("Warning: folder_list is empty")
return
# 创建左侧树模型
self.left_model, self.folder_map = TreeModelManager.create_left_tree_model(folder_list)
# 创建右侧树模型
folder_names = [os.path.basename(f) for f in folder_list]
self.right_model, self.json_data = TreeModelManager.create_right_tree_model(folder_names)
# 初始化JSON文件
for folder_name in folder_names:
self.initialize_json_file(folder_name)
self.refresh_views() # 添加视图刷新
def initialize_json_file(self, folder_name):
"""初始化JSON文件"""
if not hasattr(self, 'output_path') or not self.output_path:
QMessageBox.warning(self, "Warning", "Output path not set!")
return
json_filename = self.get_json_file_path(folder_name)
# 如果文件已存在,则读取现有数据
if os.path.exists(json_filename):
existing_data = JsonFileHandler.read_json(json_filename)
if existing_data:
self.json_data[folder_name].update(existing_data)
print(f"加载已有json文件: {json_filename},{existing_data}")
TreeModelManager.update_model_from_json(
self.right_model, folder_name, self.json_data
)
else:
# 创建新JSON文件
self.save_to_json(folder_name)
def get_json_file_path(self, folder_name):
"""
获取JSON文件路径
规则: 使用原始文件夹名 + .json后缀保存到输出目录
"""
# 确保文件夹名是有效的文件名
safe_name = folder_name.strip()
# 只添加.json后缀不做其他修改
if not safe_name.lower().endswith('.json'):
safe_name += '.json'
return os.path.join(self.output_path, safe_name)
def on_right_data_changed(self, top_left, bottom_right):
folder_item = self.right_model.itemFromIndex(top_left)
while folder_item and folder_item.parent() is not None:
folder_item = folder_item.parent()
print(f"开始更改")
if folder_item:
folder_name = folder_item.text()
print(f"变更检测到文件夹:{folder_name}")
# 调试:打印变更前的数据
print("变更前数据:", self.json_data.get(folder_name))
# 更新数据
TreeModelManager.update_json_from_model(self.right_model, self.json_data)
# 调试:打印变更后的数据
print("变更后数据:", self.json_data.get(folder_name))
self.save_to_json(folder_name)
self.data_changed.emit(self.json_data)
def save_to_json(self, folder_name):
json_path = self.get_json_file_path(folder_name)
print(f"🛠️ 准备保存到: {json_path}")
# 验证路径
if os.path.isdir(json_path):
print(f"❌ 错误:路径是目录,自动添加.json后缀")
json_path += '.json'
try:
# 确保目录存在
os.makedirs(os.path.dirname(json_path), exist_ok=True)
# 写入文件
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(self.json_data[folder_name], f, ensure_ascii=False, indent=4)
print(f"✅ 成功保存: {json_path}")
return True
except Exception as e:
print(f"❌ 保存失败: {str(e)}")
return False
def batch_update_folders(self, folder_mapping):
"""批量更新文件夹结构"""
for folder_name, new_data in folder_mapping.items():
if folder_name in self.json_data:
self.json_data[folder_name].update(new_data)
TreeModelManager.update_model_from_json(
self.right_model, folder_name, self.json_data
)
self.save_to_json(folder_name)
self.data_changed.emit(self.json_data)
def get_output_path(self):
"""获取当前输出路径"""
return self.output_path
def get_current_data(self):
"""获取当前所有数据"""
return self.json_data
class ImageLoaderSignals(QObject):
"""加载信号"""
image_loaded = Signal(str, QPixmap) # 文件路径, 加载好的图片
loading_finished = Signal() # 加载完成
progress_updated = Signal(int) # 进度更新信号
class ImageLoaderWorker(QRunnable):
"""单个图片加载任务"""
def __init__(self, file_path, signals, thumbnail_size = 300):
super().__init__()
self.file_path = file_path
self.signals = signals
self.thumbnail_size = thumbnail_size
def run(self):
"""加载图片"""
try:
# 方法1: 直接读取并缩放为缩略图 (最快)
reader = QImageReader(self.file_path)
# 设置缩放尺寸以提高读取速度
size = reader.size()
if size.width() > 1000 or size.height() > 1000:
reader.setScaledSize(QSize(1000, 1000))
# 读取图像 (不进行高质量转换)
image = reader.read()
if image.isNull():
return
# 快速生成缩略图 (低质量)
thumb = image.scaled(
self.thumbnail_size, self.thumbnail_size,
Qt.KeepAspectRatio,
Qt.FastTransformation # 使用快速转换模式
)
# 转换为QPixmap
pixmap = QPixmap.fromImage(thumb)
if not pixmap.isNull():
self.signals.image_loaded.emit(self.file_path, pixmap)
except Exception as e:
print(f"加载图片出错: {self.file_path}, 错误: {str(e)}")
class ImageLoader(QObject):
"""图片加载管理器"""
def __init__(self, image_files, parent=None):
super().__init__(parent)
self.image_files = image_files
self.signals = ImageLoaderSignals()
self.thread_pool = QThreadPool.globalInstance()
self.batch_size = 40 # 每批加载数量
self.current_index = 0
self.timer = QTimer()
self.timer.timeout.connect(self.load_next_batch)
self.cancel_requested = False
# 暴露信号
self.image_loaded = self.signals.image_loaded
self.loading_finished = self.signals.loading_finished
self.progress_updated = self.signals.progress_updated
def start_loading(self):
"""开始加载图片"""
self.current_index = 0
self.cancel_requested = False
self.load_next_batch()
def load_next_batch(self):
"""加载下一批图片"""
if self.cancel_requested:
self.signals.loading_finished.emit()
return
try:
end_index = min(self.current_index + self.batch_size, len(self.image_files))
# 批量提交任务
for i in range(self.current_index, end_index):
file_path = self.image_files[i]
worker = ImageLoaderWorker(file_path, self.signals)
self.thread_pool.start(worker)
self.current_index = end_index
# 计算并发射进度
progress = int((self.current_index / len(self.image_files)) * 100)
self.signals.progress_updated.emit(progress)
if self.current_index < len(self.image_files):
# 更短的间隔加载下一批
self.timer.start(50) # 50ms后加载下一批
else:
# 全部加载完成
self.signals.loading_finished.emit()
self.signals.progress_updated.emit(100)
self.timer.stop()
except Exception as e:
print(f"加载过程中出错: {str(e)}")
self.signals.loading_finished.emit()
self.timer.stop()
def cancel(self):
"""取消加载"""
self.cancel_requested = True
self.timer.stop()
class ImageBrowser(QWidget):
"""主显示窗口"""
def __init__(self, parent=None):
super().__init__(parent)
self.current_dir = ""
self.image_labels = {} # key: 文件路径, value: QLabel
self.image_loader = None # 图片加载器实例
self.cols_per_row = 4 # 默认每行列数
self.image_files = [] # 存储当前目录下的图片文件路径
self.progress_dialog = None # 进度条对话框
self.thumbnail_size = 100 # 缩略图大小
self.init_ui()
def init_ui(self):
"""初始化界面"""
self.setMinimumHeight(200)
# 主布局
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# 分割线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
layout.addWidget(line)
# 图片浏览区域
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
# 使用网格布局
self.image_container = QWidget()
self.image_layout = QGridLayout(self.image_container)
self.image_layout.setContentsMargins(5, 5, 5, 5)
self.image_layout.setSpacing(10)
self.image_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.image_container)
layout.addWidget(self.scroll_area)
# 初始状态提示
self.placeholder_label = QLabel("请点击左侧目录查看图片")
self.placeholder_label.setAlignment(Qt.AlignCenter)
self.placeholder_label.setStyleSheet("color: gray; font-size: 14px;")
self.scroll_area.setWidget(self.placeholder_label)
# 创建简单的加载中图标
self.loading_pixmap = self.create_loading_pixmap()
def create_loading_pixmap(self):
"""创建加载中图标"""
pixmap = QPixmap(50, 50)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.setPen(QColor(200, 200, 200))
painter.drawEllipse(5, 5, 40, 40)
painter.drawText(QRect(5, 5, 40, 40), Qt.AlignCenter, "...")
painter.end()
return pixmap
def show_images(self, dir_path):
"""显示指定目录下的图片"""
self.current_dir = dir_path
self.clear_images()
if not dir_path:
self.scroll_area.setWidget(self.placeholder_label)
return
# 读取目录中的图片文件
dir = QDir(dir_path)
self.image_files = [dir.filePath(f) for f in dir.entryList(
["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.JPG", "*.JPEG", "*.PNG", "*.BMP"],
QDir.Files, QDir.Name
)]
if not self.image_files:
no_image_label = QLabel(f"该目录没有图片文件: {dir.path()}")
no_image_label.setAlignment(Qt.AlignCenter)
no_image_label.setStyleSheet("color: gray; font-size: 14px;")
self.scroll_area.setWidget(no_image_label)
return
# 创建图片容器
self.image_container = QWidget()
self.image_layout = QGridLayout(self.image_container)
self.image_layout.setContentsMargins(5, 5, 5, 5)
self.image_layout.setSpacing(5)
self.image_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.image_container)
# 先创建所有占位符
for i, file_path in enumerate(self.image_files):
self.add_placeholder(file_path, i)
# 创建并启动图片加载器
if self.image_loader:
self.image_loader.deleteLater()
# 创建进度对话框
self.progress_dialog = QProgressDialog("正在加载图片...", "取消", 0, 100, self)
self.progress_dialog.setWindowTitle("图片加载进度")
self.progress_dialog.setWindowModality(Qt.WindowModal)
self.progress_dialog.setAutoClose(True)
self.progress_dialog.setAutoReset(True)
self.progress_dialog.canceled.connect(self.cancel_loading)
self.image_loader = ImageLoader(self.image_files)
self.image_loader.image_loaded.connect(self.update_image)
self.image_loader.loading_finished.connect(self.on_loading_finished)
self.image_loader.progress_updated.connect(self.update_progress)
self.image_loader.start_loading()
def cancel_loading(self):
"""取消加载"""
if self.image_loader:
self.image_loader.cancel()
self.image_loader.deleteLater()
self.image_loader = None
if self.progress_dialog:
self.progress_dialog.close()
self.progress_dialog = None
def update_progress(self, value):
"""更新进度条"""
if self.progress_dialog:
self.progress_dialog.setValue(value)
def on_loading_finished(self):
"""图片加载完成后的处理"""
if self.progress_dialog:
self.progress_dialog.close()
self.progress_dialog = None
if self.image_loader:
self.image_loader.deleteLater()
self.image_loader = None
def add_placeholder(self, file_path, index):
"""添加占位符到指定位置"""
self.update_columns_count()
row = index // self.cols_per_row
col = index % self.cols_per_row
placeholder = self.create_placeholder(file_path)
self.image_layout.addWidget(placeholder, row, col)
self.image_labels[file_path] = placeholder
def create_placeholder(self, file_path):
"""创建单个占位符 - 添加双击事件和拖动支持"""
file_name = QDir(file_path).dirName()
placeholder = QLabel()
placeholder.setFixedSize(self.thumbnail_size, self.thumbnail_size)
placeholder.setAlignment(Qt.AlignCenter)
placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
placeholder.setStyleSheet("""
QLabel {
border: 1px solid #eee;
background: white;
qproperty-alignment: 'AlignCenter';
}
QLabel:hover {
border: 1px solid #aaa;
background: #f5f5f5;
}
""")
placeholder.setPixmap(self.loading_pixmap)
placeholder.setProperty("file_path", file_path)
placeholder.setToolTip(f"加载中: {file_name}")
# 安装事件过滤器处理鼠标事件
placeholder.installEventFilter(self)
# 存储拖动相关状态
placeholder.setProperty("drag_start_pos", None)
placeholder.setProperty("drag_threshold", 5) # 拖动触发阈值(像素)
return placeholder
def update_image(self, file_path, pixmap):
"""更新图片显示"""
if file_path in self.image_labels:
label = self.image_labels[file_path]
label.setPixmap(pixmap.scaled(
self.thumbnail_size, self.thumbnail_size,
Qt.KeepAspectRatio, Qt.SmoothTransformation
))
file_name = QDir(file_path).dirName()
label.setToolTip(f"文件名: {file_name}")
label.setStyleSheet("""
QLabel { border: 1px solid #ddd; background: white; }
QLabel:hover { border: 1px solid #aaa; background: #f0f0f0; }
""")
def open_image(self, file_path):
"""用系统默认程序打开图片"""
try:
if os.name == 'nt': # Windows
os.startfile(file_path)
elif os.name == 'posix': # macOS/Linux
opener = "open" if sys.platform == "darwin" else "xdg-open"
import subprocess
subprocess.call([opener, file_path])
except Exception as e:
QMessageBox.warning(self, "打开失败", f"无法打开图片: {str(e)}")
def update_columns_count(self):
"""根据当前宽度更新每行列数"""
if not self.image_container or not self.isVisible():
return
try:
# 获取容器实际可用宽度(减去左右边距和间距)
margins = self.image_layout.contentsMargins()
spacing = self.image_layout.spacing()
# 可用宽度 = 容器宽度 - 左边距 - 右边距
available_width = self.image_container.width() - margins.left() - margins.right()
# 每个项目需要的宽度 = 缩略图宽度 + 间距
item_width = self.thumbnail_size + spacing
# 计算最大列数至少1列
self.cols_per_row = max(1, available_width // item_width)
except RuntimeError:
# 对象已被删除,忽略错误
pass
def resizeEvent(self, event):
"""窗口大小改变时重新计算列数"""
super().resizeEvent(event)
if not self.current_dir or not self.image_labels:
return
try:
self.update_columns_count()
QTimer.singleShot(100, self.recalculate_layout)
except RuntimeError:
# 对象已被删除,忽略错误
pass
def recalculate_layout(self):
"""重新计算布局"""
try:
if not hasattr(self, 'image_files') or not self.image_files or not self.image_container:
return
# 保存当前滚动位置
scroll_bar = self.scroll_area.verticalScrollBar()
old_scroll = scroll_bar.value()
# 清除现有布局
for i in reversed(range(self.image_layout.count())):
widget = self.image_layout.itemAt(i).widget()
if widget:
self.image_layout.removeWidget(widget)
# 更新列数
self.update_columns_count()
# 重新添加已加载的图片
for i, file_path in enumerate(self.image_files):
if file_path in self.image_labels:
row = i // self.cols_per_row
col = i % self.cols_per_row
self.image_layout.addWidget(self.image_labels[file_path], row, col)
# 设置列拉伸,使最后一列填充剩余空间
for c in range(self.cols_per_row):
self.image_layout.setColumnStretch(c, 1)
# 恢复滚动位置
scroll_bar.setValue(old_scroll)
except RuntimeError:
# 对象已被删除,忽略错误
pass
def clear_images(self):
"""清除所有图片"""
for label in self.image_labels.values():
try:
label.deleteLater()
except RuntimeError:
pass
self.image_labels.clear()
if self.image_loader:
try:
self.image_loader.deleteLater()
except RuntimeError:
pass
self.image_loader = None
def eventFilter(self, obj, event):
"""处理鼠标事件以实现双击和拖动"""
try:
if isinstance(obj, QLabel) and obj in self.image_labels.values():
file_path = obj.property("file_path")
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
# 记录鼠标按下位置和时间
obj.setProperty("drag_start_pos", event.pos())
obj.setProperty("press_time", QDateTime.currentMSecsSinceEpoch())
return True
elif event.type() == QEvent.MouseMove:
if obj.property("drag_start_pos") is not None:
# 检查是否达到拖动阈值
start_pos = obj.property("drag_start_pos")
if (event.pos() - start_pos).manhattanLength() > obj.property("drag_threshold"):
# 触发拖动
self.start_drag(obj, event)
obj.setProperty("drag_start_pos", None)
return True
elif event.type() == QEvent.MouseButtonRelease:
if event.button() == Qt.LeftButton and obj.property("drag_start_pos") is not None:
# 只清除状态,不触发任何动作
obj.setProperty("drag_start_pos", None)
return True
elif event.type() == QEvent.MouseButtonDblClick:
if event.button() == Qt.LeftButton:
self.open_image(file_path)
return True
except RuntimeError:
pass
return super().eventFilter(obj, event)
def open_image(self, file_path):
"""用默认方式打开图片"""
try:
import os
if os.name == 'nt': # Windows
os.startfile(file_path)
elif os.name == 'posix': # macOS/Linux
import subprocess
opener = "open" if sys.platform == "darwin" else "xdg-open"
subprocess.call([opener, file_path])
except Exception as e:
QMessageBox.warning(self, "打开失败", f"无法打开图片: {str(e)}")
def start_drag(self, label, event):
"""开始拖动图片"""
try:
if label.pixmap() and not label.pixmap().isNull():
file_path = label.property("file_path")
drag = QDrag(self)
mime_data = QMimeData()
# 设置多种MIME类型以支持不同场景
url = QUrl.fromLocalFile(file_path)
mime_data.setUrls([url])
mime_data.setText(file_path)
mime_data.setImageData(label.pixmap().toImage())
drag.setMimeData(mime_data)
# 设置拖动时的缩略图
thumb = label.pixmap().scaled(
200, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
drag.setPixmap(thumb)
# 设置热点位置
drag.setHotSpot(QPoint(
thumb.width() // 2,
thumb.height() // 2
))
# 执行拖动操作
drag.exec(Qt.CopyAction | Qt.MoveAction)
except Exception as e:
print(f"拖动失败: {str(e)}")
class DictionaryBrowser(QWidget):
"""右侧叶片典型/缺陷字典浏览窗口"""
dictionary_updated = Signal(str, str, dict) # 字典类型, 叶片名称, 字典数据
def __init__(self, parent=None):
super().__init__(parent)
self.current_blade = "" # 当前叶片名称
self.current_dict_type = "" # 当前字典类型(典型/缺陷)
self.dictionaries = {} # 存储所有字典数据
self.init_ui()
def init_ui(self):
"""初始化用户界面"""
self.setMinimumWidth(300)
layout = QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5)
# 标题标签
self.title_label = QLabel("请选择字典节点")
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(self.title_label)
# 图片浏览区域
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
# 使用网格布局
self.image_container = QWidget()
self.image_layout = QGridLayout(self.image_container)
self.image_layout.setContentsMargins(5, 5, 5, 5)
self.image_layout.setSpacing(10)
self.image_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.image_container)
layout.addWidget(self.scroll_area)
# 初始状态提示
self.placeholder_label = QLabel("请点击右侧JSON树中的字典节点")
self.placeholder_label.setAlignment(Qt.AlignCenter)
self.placeholder_label.setStyleSheet("color: gray; font-size: 14px;")
self.scroll_area.setWidget(self.placeholder_label)
# 设置拖放接收
self.setAcceptDrops(True)
def show_dictionary(self, blade_name, dict_type, dict_data):
"""显示指定字典内容"""
self.current_blade = blade_name
self.current_dict_type = dict_type
self.dictionaries = dict_data
self.update_title()
self.refresh_dictionary_view()
def setup_connections(self):
"""设置信号和槽的连接"""
self.dict_type_group.buttonClicked.connect(self.on_dict_type_changed)
def on_dict_type_changed(self, button):
"""处理字典类型选择变化"""
# 直接比较按钮对象来确定类型
if button == self.typical_btn:
self.current_dict_type = "典型图"
else:
self.current_dict_type = "缺陷图"
self.update_title()
self.refresh_dictionary_view()
def set_current_blade(self, blade_name):
"""设置当前叶片名称"""
self.current_blade = blade_name
self.update_title()
self.refresh_dictionary_view()
def update_title(self):
"""更新标题显示"""
if not self.current_blade or not self.current_dict_type:
self.title_label.setText("请选择字典节点")
else:
self.title_label.setText(f"{self.current_blade} - {self.current_dict_type}")
def refresh_dictionary_view(self):
"""刷新字典视图"""
if not self.current_dict_type or not self.current_blade:
self.scroll_area.setWidget(self.placeholder_label)
return
# 获取当前字典数据
dict_data = self.dictionaries.get(self.current_dict_type, {})
if not dict_data:
no_data_label = QLabel(f"该字典没有数据")
no_data_label.setAlignment(Qt.AlignCenter)
no_data_label.setStyleSheet("color: gray; font-size: 14px;")
self.scroll_area.setWidget(no_data_label)
return
# 创建图片容器
self.image_container = QWidget()
self.image_layout = QGridLayout(self.image_container)
self.image_layout.setContentsMargins(5, 5, 5, 5)
self.image_layout.setSpacing(5)
self.image_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.image_container)
# 添加图片项
for i, (desc, img_path) in enumerate(dict_data.items()):
self.add_image_item(img_path, desc, i)
def add_image_item(self, img_path, description, index):
"""添加单个图片项到布局"""
thumbnail_size = 150 # 缩略图大小
# 创建容器widget
item_widget = QWidget()
item_layout = QVBoxLayout(item_widget)
item_layout.setContentsMargins(5, 5, 5, 5)
item_layout.setSpacing(3)
# 图片标签
img_label = QLabel()
img_label.setFixedSize(thumbnail_size, thumbnail_size)
img_label.setAlignment(Qt.AlignCenter)
img_label.setStyleSheet("border: 1px solid #ddd; background: white;")
# 加载图片
if os.path.exists(img_path):
pixmap = QPixmap(img_path)
if not pixmap.isNull():
img_label.setPixmap(pixmap.scaled(
thumbnail_size, thumbnail_size,
Qt.KeepAspectRatio, Qt.SmoothTransformation
))
else:
img_label.setText("无效图片")
else:
img_label.setText("图片不存在")
# 描述标签
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignCenter)
desc_label.setStyleSheet("font-size: 11px;")
desc_label.setWordWrap(True)
# 添加到布局
item_layout.addWidget(img_label)
item_layout.addWidget(desc_label)
# 添加上下文菜单
item_widget.setContextMenuPolicy(Qt.CustomContextMenu)
item_widget.customContextMenuRequested.connect(
lambda pos, path=img_path, desc=description: self.show_context_menu(pos, path, desc)
)
# 计算位置
cols_per_row = max(1, self.width() // (thumbnail_size + 20))
row = index // cols_per_row
col = index % cols_per_row
self.image_layout.addWidget(item_widget, row, col)
def show_context_menu(self, pos, img_path, description):
"""显示上下文菜单"""
menu = QMenu(self)
# 编辑描述动作
edit_action = QAction("编辑描述", self)
edit_action.triggered.connect(lambda: self.edit_description(img_path, description))
# 删除图片动作
delete_action = QAction("删除图片", self)
delete_action.triggered.connect(lambda: self.delete_image(img_path))
menu.addAction(edit_action)
menu.addAction(delete_action)
menu.exec_(QCursor.pos())
def edit_description(self, img_path, old_desc):
"""编辑图片描述"""
new_desc, ok = QInputDialog.getText(
self, "编辑描述", "请输入新的描述:",
QLineEdit.Normal, old_desc
)
if ok and new_desc and new_desc != old_desc:
# 更新字典数据
dict_data = self.dictionaries[self.current_dict_type][self.current_blade]
if img_path in dict_data.values():
# 找到对应的条目并更新
for desc, path in dict_data.items():
if path == img_path:
dict_data.pop(desc)
dict_data[new_desc] = img_path
break
# 刷新视图并发射信号
self.refresh_dictionary_view()
self.dictionary_updated.emit(
self.current_dict_type,
self.current_blade,
dict_data
)
def delete_image(self, img_path):
"""删除图片"""
reply = QMessageBox.question(
self, "确认删除",
"确定要从字典中删除这张图片吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
# 从字典中删除
dict_data = self.dictionaries[self.current_dict_type][self.current_blade]
for desc, path in list(dict_data.items()):
if path == img_path:
dict_data.pop(desc)
break
# 刷新视图并发射信号
self.refresh_dictionary_view()
self.dictionary_updated.emit(
self.current_dict_type,
self.current_blade,
dict_data
)
def add_image_to_dictionary(self, img_path):
"""添加图片到当前字典"""
if not self.current_dict_type or not self.current_blade:
QMessageBox.warning(self, "警告", "请先选择叶片和字典类型")
return
# 获取或创建当前字典
if self.current_blade not in self.dictionaries[self.current_dict_type]:
self.dictionaries[self.current_dict_type][self.current_blade] = {}
dict_data = self.dictionaries[self.current_dict_type][self.current_blade]
# 生成默认描述(序号)
default_desc = f"图片_{len(dict_data) + 1}"
# 添加到字典
dict_data[default_desc] = img_path
# 刷新视图并发射信号
self.refresh_dictionary_view()
self.dictionary_updated.emit(
self.current_dict_type,
self.current_blade,
dict_data
)
def set_dictionary_data(self, dict_type, blade_name, data):
"""设置字典数据"""
if dict_type in self.dictionaries:
if data:
self.dictionaries[dict_type][blade_name] = data
elif blade_name in self.dictionaries[dict_type]:
del self.dictionaries[dict_type][blade_name]
# 如果当前显示的是这个字典,则刷新视图
if self.current_dict_type == dict_type and self.current_blade == blade_name:
self.refresh_dictionary_view()
def resizeEvent(self, event):
"""窗口大小改变时重新计算布局"""
super().resizeEvent(event)
if hasattr(self, 'current_dict_type') and hasattr(self, 'current_blade'):
QTimer.singleShot(100, self.refresh_dictionary_view)