diff --git a/info_core/MyQtClass.py b/info_core/MyQtClass.py index e1027f1..bb27d1b 100644 --- a/info_core/MyQtClass.py +++ b/info_core/MyQtClass.py @@ -4,12 +4,16 @@ from PySide6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QCheckBox, QTextBrowser, QApplication, QCompleter, QFrame, QListWidgetItem, QListWidget, QAbstractItemView, QStackedWidget, QStackedLayout, QStyledItemDelegate, - QTreeView, QSplitter, QFileSystemModel) -from PySide6.QtCore import (QDateTime,QTimer,QDateTime,Signal,QSettings, - QSortFilterProxyModel, QSize, QDir) + 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) + QAction, QColor, QImageReader, QDrag, QCursor, + QPainter) import json, sys, os from info_core.defines import * @@ -1167,11 +1171,49 @@ class JsonFileHandler: class TreeModelManager: """管理树形模型的数据操作""" + # 中英文映射字典 + TRANSLATION_MAP = { + # 左侧树 + "Directory Structure": "目录结构", + + # 右侧树 + "Information": "信息", + "Value": "值", + "turbinecode": "机组编号", + "part1": "叶片1", + "part2": "叶片2", + "part3": "叶片3", + "code": "部件代码", + "defect_picture_dict": "缺陷图字典", + "typical_picture_dict": "典型图字典" + } + @staticmethod - def create_left_tree_model(folder_list): - """创建左侧文件夹树模型""" + 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 Structure"]) + model.setHorizontalHeaderLabels(["目录结构"]) folder_map = {} for folder_path in folder_list: @@ -1182,13 +1224,23 @@ class TreeModelManager: 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 @@ -1196,7 +1248,10 @@ class TreeModelManager: def create_right_tree_model(folder_names): """创建右侧可编辑树模型""" model = QStandardItemModel() - model.setHorizontalHeaderLabels(["Name", "Value"]) + model.setHorizontalHeaderLabels([ + TreeModelManager.translate_text("Information"), + TreeModelManager.translate_text("Value") + ]) json_data = {} for folder_name in folder_names: @@ -1204,30 +1259,54 @@ class TreeModelManager: parent_item.setEditable(False) # 添加机组编号 - turbine_item = QStandardItem("turbinecode") + 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(f"part{i}") + part_item = QStandardItem(TreeModelManager.translate_text(f"part{i}")) part_item.setEditable(False) - code_item = QStandardItem("001") - code_item.setEditable(True) + # 部件代码 + 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("{}")]) - part_item.appendRow([QStandardItem("code"), code_item]) parent_item.appendRow(part_item) model.appendRow(parent_item) - # 初始化JSON数据 + # 初始化JSON数据(保持英文键) json_data[folder_name] = { "turbinecode": folder_name.replace("机组", "").strip(), - "part1": {"code": "001"}, - "part2": {"code": "001"}, - "part3": {"code": "001"} + "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 @@ -1254,14 +1333,28 @@ class TreeModelManager: for j in range(1, 4): part_item = parent_item.child(j) if part_item: - part_name = part_item.text() + 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) @@ -1271,22 +1364,47 @@ class TreeModelManager: # 确保数据结构存在 if folder_name not in json_data: - json_data[folder_name] = {"turbinecode": "", "part1": {}, "part2": {}, "part3": {}} + 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) # 第一行第二列 + # 获取 turbinecode 值 + turbine_index = model.index(0, 1, folder_index) if turbine_index.isValid(): new_value = model.data(turbine_index) - print(f"更新 turbinecode: {json_data[folder_name]['turbinecode']} -> {new_value}") 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][f"part{part_num}"]["code"] = model.data(code_index, Qt.DisplayRole) + 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): """直接保存的委托实现""" @@ -1324,48 +1442,145 @@ class DirectSaveDelegate(QStyledItemDelegate): 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 = QHBoxLayout(self) - splitter = QSplitter(Qt.Horizontal) + 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(["Folder Structure"]) + self.left_model.setHorizontalHeaderLabels(["目录结构"]) self.left_tree = QTreeView() self.left_tree.setModel(self.left_model) self.left_tree.setEditTriggers(QTreeView.NoEditTriggers) - # 添加上下文菜单(复制功能) - self.left_tree.setContextMenuPolicy(Qt.ActionsContextMenu) - copy_action = QAction("Copy", self.left_tree) - copy_action.triggered.connect(self.copy_left_tree_text) - self.left_tree.addAction(copy_action) - # 右侧可编辑树视图 self.right_model = QStandardItemModel() - self.right_model.setHorizontalHeaderLabels(["Name", "Value"]) + 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) ) - # 添加到分割器 - splitter.addWidget(self.left_tree) - splitter.addWidget(self.right_tree) - splitter.setSizes([300, 500]) - main_layout.addWidget(splitter) + # 添加到水平分割器 + 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}]") @@ -1398,14 +1613,6 @@ class FolderBrowser(QWidget): except Exception as e: print(f"❌ 保存错误: {str(e)}") - 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) - def refresh_views(self): """刷新左右视图显示""" self.left_tree.setModel(None) # 先重置模型 @@ -1460,21 +1667,14 @@ class FolderBrowser(QWidget): """初始化文件夹结构""" self.left_model.clear() self.right_model.clear() - self.left_model.setHorizontalHeaderLabels(["Folder Structure"]) - self.right_model.setHorizontalHeaderLabels(["Name", "Value"]) + 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.folder_map = {} # 新增: {文件夹名: 初始文件名} - - for folder_path in folder_list: - folder_name = os.path.basename(folder_path) - # 使用初始 turbinecode 作为固定文件名 - initial_code = folder_name.replace("机组", "").strip() - self.folder_map[folder_name] = f"{initial_code}.json" # 存储固定文件名 # 创建左侧树模型 self.left_model, self.folder_map = TreeModelManager.create_left_tree_model(folder_list) @@ -1583,4 +1783,771 @@ class FolderBrowser(QWidget): def get_current_data(self): """获取当前所有数据""" - return self.json_data \ No newline at end of file + 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) \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..05ab597 --- /dev/null +++ b/test.py @@ -0,0 +1,64 @@ +import sys +import time +from PySide6.QtWidgets import QApplication, QMainWindow, QProgressBar, QVBoxLayout, QWidget, QPushButton +from PySide6.QtCore import QThread, Signal + + +class WorkerThread(QThread): + progress_updated = Signal(int) # 进度更新信号 + finished = Signal() # 完成信号 + + def run(self): + # 模拟耗时操作 + for i in range(1, 101): + time.sleep(0.05) # 模拟耗时操作 + self.progress_updated.emit(i) # 发射进度信号 + self.finished.emit() + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("独立进度条示例") + self.setGeometry(100, 100, 400, 200) + + # 创建进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + + # 创建开始按钮 + self.start_button = QPushButton("开始加载") + self.start_button.clicked.connect(self.start_loading) + + # 设置布局 + layout = QVBoxLayout() + layout.addWidget(self.progress_bar) + layout.addWidget(self.start_button) + + container = QWidget() + container.setLayout(layout) + self.setCentralWidget(container) + + # 初始化工作线程 + self.worker_thread = WorkerThread() + self.worker_thread.progress_updated.connect(self.update_progress) + self.worker_thread.finished.connect(self.loading_finished) + + def start_loading(self): + self.start_button.setEnabled(False) + self.worker_thread.start() + + def update_progress(self, value): + self.progress_bar.setValue(value) + + def loading_finished(self): + self.start_button.setEnabled(True) + # 这里可以添加加载完成后的操作 + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) \ No newline at end of file