diff --git a/info_core/Img_edit.py b/info_core/Img_edit.py new file mode 100644 index 0000000..1bc91a9 --- /dev/null +++ b/info_core/Img_edit.py @@ -0,0 +1,374 @@ +from PySide6.QtCore import Qt, QPointF, QRectF, Signal +from PySide6.QtGui import QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QPainterPath +from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, + QGraphicsView, QGraphicsScene, QGraphicsItem, + QGraphicsRectItem, QGraphicsEllipseItem, + QGraphicsPixmapItem, QSizePolicy) + +class ResizableGraphicsItem(QGraphicsRectItem): + def __init__(self, x, y, width, height, parent=None): + super().__init__(x, y, width, height, parent) + self.setFlag(QGraphicsItem.ItemIsSelectable, True) + self.setFlag(QGraphicsItem.ItemIsMovable, True) + self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) + self.setAcceptHoverEvents(True) + + self.handle_size = 8 + self.handles = {} + self.selected_handle = None + self.mouse_press_pos = None + self.mouse_press_rect = None + + # 创建调整大小的手柄 + self.create_handles() + + def create_handles(self): + """创建调整大小的手柄""" + rect = self.rect() + self.handles = { + "top_left": QRectF(rect.left(), rect.top(), self.handle_size, self.handle_size), + "top_right": QRectF(rect.right() - self.handle_size, rect.top(), self.handle_size, self.handle_size), + "bottom_left": QRectF(rect.left(), rect.bottom() - self.handle_size, self.handle_size, self.handle_size), + "bottom_right": QRectF(rect.right() - self.handle_size, rect.bottom() - self.handle_size, self.handle_size, self.handle_size), + "top": QRectF(rect.center().x() - self.handle_size/2, rect.top(), self.handle_size, self.handle_size), + "bottom": QRectF(rect.center().x() - self.handle_size/2, rect.bottom() - self.handle_size, self.handle_size, self.handle_size), + "left": QRectF(rect.left(), rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size), + "right": QRectF(rect.right() - self.handle_size, rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size), + } + + def update_handles(self): + """更新手柄位置""" + rect = self.rect() + self.handles["top_left"].moveTopLeft(rect.topLeft()) + self.handles["top_right"].moveTopRight(rect.topRight()) + self.handles["bottom_left"].moveBottomLeft(rect.bottomLeft()) + self.handles["bottom_right"].moveBottomRight(rect.bottomRight()) + self.handles["top"].moveCenter(QPointF(rect.center().x(), rect.top() + self.handle_size/2)) + self.handles["bottom"].moveCenter(QPointF(rect.center().x(), rect.bottom() - self.handle_size/2)) + self.handles["left"].moveCenter(QPointF(rect.left() + self.handle_size/2, rect.center().y())) + self.handles["right"].moveCenter(QPointF(rect.right() - self.handle_size/2, rect.center().y())) + + def paint(self, painter, option, widget=None): + """绘制项和手柄""" + # 绘制主矩形 + pen = QPen(Qt.blue, 2, Qt.SolidLine) + if self.isSelected(): + pen.setStyle(Qt.DashLine) + painter.setPen(pen) + painter.setBrush(QBrush(Qt.transparent)) + painter.drawRect(self.rect()) + + # 如果选中,绘制手柄 + if self.isSelected(): + painter.setBrush(QBrush(Qt.white)) + painter.setPen(QPen(Qt.black, 1)) + for handle in self.handles.values(): + painter.drawRect(handle) + + def hoverMoveEvent(self, event): + """鼠标悬停时检查是否在手柄上""" + for handle_name, handle_rect in self.handles.items(): + if handle_rect.contains(event.pos()): + # 根据手柄位置设置不同的光标 + if handle_name in ["top_left", "bottom_right"]: + self.setCursor(Qt.SizeFDiagCursor) + elif handle_name in ["top_right", "bottom_left"]: + self.setCursor(Qt.SizeBDiagCursor) + elif handle_name in ["top", "bottom"]: + self.setCursor(Qt.SizeVerCursor) + elif handle_name in ["left", "right"]: + self.setCursor(Qt.SizeHorCursor) + self.selected_handle = handle_name + return + + self.setCursor(Qt.SizeAllCursor) + self.selected_handle = None + super().hoverMoveEvent(event) + + def hoverLeaveEvent(self, event): + """鼠标离开时恢复默认光标""" + self.setCursor(Qt.ArrowCursor) + self.selected_handle = None + super().hoverLeaveEvent(event) + + def mousePressEvent(self, event): + """鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.mouse_press_pos = event.pos() + self.mouse_press_rect = self.rect() + + # 如果点击的是手柄,则开始调整大小 + if self.selected_handle: + event.accept() + return + + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """鼠标移动事件""" + if event.buttons() & Qt.LeftButton and self.mouse_press_pos: + if self.selected_handle: + # 调整大小 + self.resize_item(event.pos()) + event.accept() + return + else: + # 移动项 + super().mouseMoveEvent(event) + else: + super().mouseMoveEvent(event) + + def resize_item(self, mouse_pos): + """根据鼠标位置调整项大小""" + rect = self.mouse_press_rect + pos = mouse_pos + new_rect = QRectF(rect) + + # 根据选中的手柄调整矩形 + if self.selected_handle == "top_left": + new_rect.setTopLeft(pos) + elif self.selected_handle == "top_right": + new_rect.setTopRight(pos) + elif self.selected_handle == "bottom_left": + new_rect.setBottomLeft(pos) + elif self.selected_handle == "bottom_right": + new_rect.setBottomRight(pos) + elif self.selected_handle == "top": + new_rect.setTop(pos.y()) + elif self.selected_handle == "bottom": + new_rect.setBottom(pos.y()) + elif self.selected_handle == "left": + new_rect.setLeft(pos.x()) + elif self.selected_handle == "right": + new_rect.setRight(pos.x()) + + # 确保矩形不会太小 + if new_rect.width() < 10: + if self.selected_handle in ["left", "top_left", "bottom_left"]: + new_rect.setLeft(new_rect.right() - 10) + else: + new_rect.setRight(new_rect.left() + 10) + + if new_rect.height() < 10: + if self.selected_handle in ["top", "top_left", "top_right"]: + new_rect.setTop(new_rect.bottom() - 10) + else: + new_rect.setBottom(new_rect.top() + 10) + + self.setRect(new_rect) + self.update_handles() + + def mouseReleaseEvent(self, event): + """鼠标释放事件""" + self.mouse_press_pos = None + self.mouse_press_rect = None + self.selected_handle = None + super().mouseReleaseEvent(event) + + def itemChange(self, change, value): + """项变化时更新手柄位置""" + if change == QGraphicsItem.ItemSelectedChange: + self.update_handles() + elif change == QGraphicsItem.ItemPositionHasChanged or change == QGraphicsItem.ItemTransformHasChanged: + self.update_handles() + + return super().itemChange(change, value) + +class ResizableEllipseItem(ResizableGraphicsItem): + def paint(self, painter, option, widget=None): + """绘制椭圆和手柄""" + # 绘制主椭圆 + pen = QPen(Qt.red, 2, Qt.SolidLine) + if self.isSelected(): + pen.setStyle(Qt.DashLine) + painter.setPen(pen) + painter.setBrush(QBrush(Qt.transparent)) + painter.drawEllipse(self.rect()) + + # 如果选中,绘制手柄 + if self.isSelected(): + painter.setBrush(QBrush(Qt.white)) + painter.setPen(QPen(Qt.black, 1)) + for handle in self.handles.values(): + painter.drawRect(handle) + +class DefectMarkEditor(QDialog): + def __init__(self, parent=None, image_path="", current_description=""): + super().__init__(parent) + self.setWindowTitle("缺陷标记编辑器") + self.setMinimumSize(800, 600) + + self.image_path = image_path + self.current_description = current_description + self.mouse_state = "select" # select | create_rect | create_ellipse + self.start_pos = None + self.current_item = None + self.scale_factor = 1.0 + + self.init_ui() + self.load_image() + + def init_ui(self): + main_layout = QVBoxLayout(self) + + # 工具栏 + toolbar = QHBoxLayout() + + self.select_btn = QPushButton("选择") + self.select_btn.setCheckable(True) + self.select_btn.setChecked(True) + self.select_btn.clicked.connect(self.set_select_mode) + + self.rect_btn = QPushButton("矩形框") + self.rect_btn.setCheckable(True) + self.rect_btn.clicked.connect(self.set_create_rect_mode) + + self.ellipse_btn = QPushButton("圆形框") + self.ellipse_btn.setCheckable(True) + self.ellipse_btn.clicked.connect(self.set_create_ellipse_mode) + + self.clear_btn = QPushButton("清除所有标记") + self.clear_btn.clicked.connect(self.clear_all_items) + + toolbar.addWidget(self.select_btn) + toolbar.addWidget(self.rect_btn) + toolbar.addWidget(self.ellipse_btn) + toolbar.addWidget(self.clear_btn) + + # 场景和视图 + self.scene = QGraphicsScene(self) + self.view = CustomGraphicsView(self.scene, self) + self.view.setRenderHint(QPainter.Antialiasing) + self.view.setDragMode(QGraphicsView.RubberBandDrag) + self.view.setMouseTracking(True) + + main_layout.addLayout(toolbar) + main_layout.addWidget(self.view) + + def load_image(self): + """加载图片到场景""" + if not self.image_path: + return + + self.scene.clear() + self.pixmap_item = QGraphicsPixmapItem(QPixmap(self.image_path)) + self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable, False) + self.pixmap_item.setFlag(QGraphicsItem.ItemIsSelectable, False) + self.scene.addItem(self.pixmap_item) + + # 设置场景大小为图片大小 + self.scene.setSceneRect(self.pixmap_item.boundingRect()) + + def set_select_mode(self): + """设置为选择模式""" + self.mouse_state = "select" + self.select_btn.setChecked(True) + self.rect_btn.setChecked(False) + self.ellipse_btn.setChecked(False) + self.view.setDragMode(QGraphicsView.RubberBandDrag) + + def set_create_rect_mode(self): + """设置为创建矩形模式""" + self.mouse_state = "create_rect" + self.select_btn.setChecked(False) + self.rect_btn.setChecked(True) + self.ellipse_btn.setChecked(False) + self.view.setDragMode(QGraphicsView.NoDrag) + + def set_create_ellipse_mode(self): + """设置为创建圆形模式""" + self.mouse_state = "create_ellipse" + self.select_btn.setChecked(False) + self.rect_btn.setChecked(False) + self.ellipse_btn.setChecked(True) + self.view.setDragMode(QGraphicsView.NoDrag) + + def clear_all_items(self): + """清除所有标记项""" + for item in self.scene.items(): + if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)): + self.scene.removeItem(item) + + def mouse_press_on_scene(self, pos): + """场景鼠标按下事件""" + if self.mouse_state == "select": + return + + self.start_pos = pos + if self.mouse_state == "create_rect": + self.current_item = ResizableGraphicsItem(pos.x(), pos.y(), 1, 1) + elif self.mouse_state == "create_ellipse": + self.current_item = ResizableEllipseItem(pos.x(), pos.y(), 1, 1) + + if self.current_item: + self.scene.addItem(self.current_item) + + def mouse_move_on_scene(self, pos): + """场景鼠标移动事件""" + if not self.start_pos or not self.current_item: + return + + rect = QRectF(self.start_pos, pos).normalized() + self.current_item.setRect(rect) + + def mouse_release_on_scene(self, pos): + """场景鼠标释放事件""" + if self.current_item and self.mouse_state != "select": + # 如果创建的项太小,则删除 + if self.current_item.rect().width() < 10 or self.current_item.rect().height() < 10: + self.scene.removeItem(self.current_item) + + self.start_pos = None + self.current_item = None + + # 创建完成后自动回到选择模式 + self.set_select_mode() + +class CustomGraphicsView(QGraphicsView): + def __init__(self, scene, editor, parent=None): + super().__init__(scene, parent) + self.editor = editor + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + def wheelEvent(self, event): + """鼠标滚轮缩放""" + factor = 1.2 + if event.angleDelta().y() < 0: + factor = 1.0 / factor + + self.scale(factor, factor) + self.editor.scale_factor *= factor + + def mousePressEvent(self, event): + """鼠标按下事件""" + if event.button() == Qt.LeftButton: + scene_pos = self.mapToScene(event.pos()) + items = self.scene().items(scene_pos) + + # 如果点击的是背景或图片,则开始创建 + if not items or items[-1] == self.editor.pixmap_item: + self.editor.mouse_press_on_scene(scene_pos) + else: + super().mousePressEvent(event) + else: + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """鼠标移动事件""" + if self.editor.start_pos and self.editor.current_item: + scene_pos = self.mapToScene(event.pos()) + self.editor.mouse_move_on_scene(scene_pos) + else: + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件""" + if event.button() == Qt.LeftButton and self.editor.start_pos: + scene_pos = self.mapToScene(event.pos()) + self.editor.mouse_release_on_scene(scene_pos) + else: + super().mouseReleaseEvent(event) diff --git a/info_core/MyQtClass.py b/info_core/MyQtClass.py index bb27d1b..960deaf 100644 --- a/info_core/MyQtClass.py +++ b/info_core/MyQtClass.py @@ -6,7 +6,8 @@ from PySide6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QCheckBox, QStackedWidget, QStackedLayout, QStyledItemDelegate, QTreeView, QSplitter, QFileSystemModel, QScrollArea, QToolTip, QGridLayout, QSizePolicy, QProgressDialog, - QButtonGroup, QInputDialog, QMenu) + QButtonGroup, QInputDialog, QMenu, QComboBox, QDialogButtonBox, + QTextEdit, QStyle) from PySide6.QtCore import (QDateTime,QTimer,QDateTime,Signal,QSettings, QPoint, QEvent, QSortFilterProxyModel, QSize, QDir, QMimeData, QRunnable, QObject, QThreadPool, QPoint, QRect, QUrl) @@ -14,8 +15,9 @@ from PySide6.QtGui import (QPixmap, QDragEnterEvent, QDropEvent, Qt, QIcon, QFontMetrics, QStandardItem, QStandardItemModel, QAction, QColor, QImageReader, QDrag, QCursor, QPainter) -import json, sys, os +import json, sys, os, re, time +from info_core.Img_edit import DefectMarkEditor from info_core.defines import * class ConfigComboBoxGroup(QGroupBox): @@ -1514,34 +1516,69 @@ class FolderBrowser(QWidget): 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.left_tree.clicked.connect(self.on_left_tree_clicked) + self.folder_selected.connect(self.image_browser.show_images) + 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) + + self.dictionary_browser.dictionary_updated.connect(self.handle_dictionary_update) + + def handle_dictionary_update(self, folder_name, part_name, dict_type_en, dict_data): + """处理字典更新""" + print(f"🔧 字典更新 {self.json_data}:\n[{folder_name}][{part_name}][{dict_type_en}] -> \n{dict_data}") + # 1. 更新JSON数据 + if folder_name in self.json_data and part_name in self.json_data[folder_name]: + self.json_data[folder_name][part_name][dict_type_en] = dict_data + + # 2. 更新树模型 + self.update_right_tree(folder_name) + + # 3. 保存到文件 + self.save_to_json(folder_name) + + def update_right_tree(self, folder_name): + """更新右侧树视图""" + # 找到对应的文件夹项 + for row in range(self.right_model.rowCount()): + folder_item = self.right_model.item(row, 0) + if folder_item.text() == folder_name: + # 更新整个子树 + TreeModelManager.update_model_from_json( + self.right_model, + folder_name, + self.json_data + ) + # 展开该节点 + self.right_tree.expand(folder_item.index()) + break 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) - + text = item.text() # 获取显示文本(中文) + print(f"点击了右侧树节点: {text}") + # 检查是否点击了字典节点(中文) + if text in ["缺陷图字典", "典型图字典"]: + # 获取part名称和原文件夹名 + part_item = item.parent() + folder_item = part_item.parent() if part_item else None + + if part_item and folder_item: + folder_name = folder_item.text() + part_name = part_item.text() + # 转换为英文键名获取数据 + dict_type = DictionaryBrowser.DICT_TYPE_MAPPING.get(text, "") + if dict_type: + dict_data = self.json_data.get(folder_name, {}).get(DictionaryBrowser.DICT_TYPE_MAPPING.get(part_name, "")).get(dict_type, {}) + + # 传递中文显示名称 + self.dictionary_browser.show_dictionary(folder_name, part_name, text, dict_data) + def get_item_path(self, item): """获取树节点路径""" path = [] @@ -1555,22 +1592,6 @@ class FolderBrowser(QWidget): # 假设叶片名称是文件夹路径的最后一部分 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): """处理左侧树视图选择变化事件""" @@ -1744,6 +1765,11 @@ class FolderBrowser(QWidget): self.data_changed.emit(self.json_data) def save_to_json(self, folder_name): + """将类中现在的self.json_data保存到JSON文件 + + Args: + folder_name (str): 文件夹名称(要保存的json文件名,文件名为机组号) + """ json_path = self.get_json_file_path(folder_name) print(f"🛠️ 准备保存到: {json_path}") @@ -2278,20 +2304,59 @@ class ImageBrowser(QWidget): except Exception as e: print(f"拖动失败: {str(e)}") - -class DictionaryBrowser(QWidget): - """右侧叶片典型/缺陷字典浏览窗口""" - dictionary_updated = Signal(str, str, dict) # 字典类型, 叶片名称, 字典数据 - +class OverlayLabel(QLabel): + """支持叠加图标的标签控件""" def __init__(self, parent=None): super().__init__(parent) - self.current_blade = "" # 当前叶片名称 - self.current_dict_type = "" # 当前字典类型(典型/缺陷) - self.dictionaries = {} # 存储所有字典数据 - self.init_ui() + self._overlay_icon = None + + def set_overlay_icon(self, standard_pixmap): + """设置叠加图标""" + self._overlay_icon = standard_pixmap + self.update() + + def paintEvent(self, event): + """重绘事件,绘制图标叠加""" + super().paintEvent(event) + if self._overlay_icon and self.pixmap(): + # 获取系统标准图标 + icon = self.style().standardIcon(self._overlay_icon) + pixmap = icon.pixmap(24, 24) # 图标大小 + + # 在右下角绘制图标 + painter = QPainter(self) + margin = 5 + x = self.width() - pixmap.width() - margin + y = self.height() - pixmap.height() - margin + painter.drawPixmap(x, y, pixmap) + painter.end() + +class DictionaryBrowser(QWidget): + """字典浏览编辑窗口""" + dictionary_updated = Signal(str, str, str, dict) # 原文件夹名, part名称, 字典类型(英文), 字典数据 + + # 中英文映射字典 + DICT_TYPE_MAPPING = { + "缺陷图字典": "defect_picture_dict", + "典型图字典": "typical_picture_dict", + "叶片1" : "part1", + "叶片2" : "part2", + "叶片3" : "part3", + } + + REVERSE_DICT_MAPPING = {v: k for k, v in DICT_TYPE_MAPPING.items()} + + def __init__(self, parent=None): + super().__init__(parent) + self.original_folder_name = "" # 原文件夹名 + self.current_part = "" # 当前part名称(part1/part2/part3) + self.current_dict_type = "" # 当前字典类型(英文) + self.current_dict_data = {} # 当前字典数据 + self.init_ui() + def init_ui(self): - """初始化用户界面""" + """初始化界面""" self.setMinimumWidth(300) layout = QVBoxLayout(self) layout.setContentsMargins(5, 5, 5, 5) @@ -2305,106 +2370,112 @@ class DictionaryBrowser(QWidget): # 图片浏览区域 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.set_placeholder("请点击右侧JSON树中的字典节点") # 设置拖放接收 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 + def set_placeholder(self, text): + """设置占位文本""" + label = QLabel(text) + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("color: gray; font-size: 14px;") + self.scroll_area.setWidget(label) + + def show_dictionary(self, folder_name, part_name, dict_type_display, dict_data): + """显示指定字典内容 + :param dict_type_display: 显示用的字典类型名称(中文) + """ + self.original_folder_name = folder_name + self.current_part = self.DICT_TYPE_MAPPING.get(part_name, "") + # 转换为英文键名存储 + self.current_dict_type = self.DICT_TYPE_MAPPING.get(dict_type_display, "") + self.current_dict_data = dict_data or {} 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() + self.refresh_view() def update_title(self): - """更新标题显示""" - if not self.current_blade or not self.current_dict_type: + """更新标题显示(使用中文)""" + if not all([self.original_folder_name, self.current_part, self.current_dict_type]): self.title_label.setText("请选择字典节点") else: - self.title_label.setText(f"{self.current_blade} - {self.current_dict_type}") + # 转换为中文显示 + dict_name = self.REVERSE_DICT_MAPPING.get(self.current_dict_type, self.current_dict_type) + self.title_label.setText(f"{self.original_folder_name} - {self.current_part} - {dict_name}") - def refresh_dictionary_view(self): - """刷新字典视图""" - if not self.current_dict_type or not self.current_blade: - self.scroll_area.setWidget(self.placeholder_label) + def refresh_view(self): + """刷新视图""" + # 安全清除旧内容 + old_widget = self.scroll_area.takeWidget() + if old_widget: + old_widget.deleteLater() + + if not all([self.original_folder_name, self.current_part, self.current_dict_type]): + self.set_placeholder("请选择有效的字典节点") 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) + if not self.current_dict_data: + self.set_placeholder("该字典没有数据") 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) + # 创建新的容器 + container = QWidget() + layout = QVBoxLayout(container) # 改为垂直布局 + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) - self.scroll_area.setWidget(self.image_container) + # 创建水平流式布局的容器 + flow_widget = QWidget() + flow_layout = QHBoxLayout(flow_widget) + flow_layout.setContentsMargins(0, 0, 0, 0) + flow_layout.setSpacing(5) + flow_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) # 左对齐且顶部对齐 # 添加图片项 - 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 # 缩略图大小 + for key, img_path in self.current_dict_data.items(): + self._add_image_item(flow_layout, img_path, key) + + # 添加流式布局到主布局 + layout.addWidget(flow_widget) + layout.addStretch(1) # 添加拉伸因子使内容保持在顶部 + + self.scroll_area.setWidget(container) + + def _check_description_format(self, key): + """检查描述是否符合标准格式""" + if not key or "_" not in key: + return False + + parts = key.split("_") + # 检查是否有足够的字段(序号+6个标准字段+建议) + return len(parts) >= 8 + + def _add_image_item(self, parent_layout, img_path, key): + """添加单个图片项 - 只显示图片,符合格式的显示对勾""" + thumbnail_size = 150 # 创建容器widget item_widget = QWidget() + + # 设置悬停提示 + hover_text = f"图片路径: {img_path}\n\n描述详情:\n{self.get_description_display(key)}" + item_widget.setToolTip(hover_text) + item_layout = QVBoxLayout(item_widget) item_layout.setContentsMargins(5, 5, 5, 5) - item_layout.setSpacing(3) + item_layout.setSpacing(0) - # 图片标签 - img_label = QLabel() + # 图片标签 - 使用自定义的OverlayLabel类 + img_label = OverlayLabel() img_label.setFixedSize(thumbnail_size, thumbnail_size) img_label.setAlignment(Qt.AlignCenter) - img_label.setStyleSheet("border: 1px solid #ddd; background: white;") + + # 检查描述是否符合格式 + is_valid_format = self._check_description_format(key) # 加载图片 if os.path.exists(img_path): @@ -2414,140 +2485,398 @@ class DictionaryBrowser(QWidget): thumbnail_size, thumbnail_size, Qt.KeepAspectRatio, Qt.SmoothTransformation )) + # 如果格式正确,显示对勾 + if is_valid_format: + img_label.set_overlay_icon(QStyle.SP_DialogApplyButton) else: img_label.setText("无效图片") + img_label.setStyleSheet("color: red;") else: img_label.setText("图片不存在") + img_label.setStyleSheet("color: red;") - # 描述标签 - 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) + lambda pos, path=img_path, k=key: self.show_context_menu(pos, path, k)) + + parent_layout.addWidget(item_widget) + + def get_description_display(self, key): + """获取描述信息的显示文本 - 用于悬停提示""" + if not key or "_" not in key: + return "⚠ 无描述信息" + + parts = key.split("_") + if len(parts) < 8: + return f"⚠ 描述格式不完整\n当前描述: {key}" + + # 格式化显示 + description = ( + f"✔ 已编辑描述\n" + f"缺陷类型: {parts[1]}\n" + f"位置: {parts[2]}\n" + f"尺寸: {parts[3]}\n" + f"可见程度: {parts[4]}\n" + f"紧急程度: {parts[5]}\n" + f"危重等级: {parts[6]}\n" + f"维修建议: {'_'.join(parts[7:])}" ) - # 计算位置 - 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) + return description + + + def dragEnterEvent(self, event): + """拖拽进入事件""" + if event.mimeData().hasUrls(): + event.acceptProposedAction() - def show_context_menu(self, pos, img_path, description): + def dropEvent(self, event): + """处理拖放事件""" + if not all([self.original_folder_name, self.current_part, self.current_dict_type]): + QMessageBox.warning(self, "警告", "请先选择有效的字典节点") + return + + urls = event.mimeData().urls() + if urls: + img_path = urls[0].toLocalFile() + if os.path.isfile(img_path): + # 生成新键(使用当前最大数字+1) + keys = [int(k) for k in self.current_dict_data.keys() if k.isdigit()] + max_key = max(keys) if keys else -1 + new_key = str(max_key + 1) + + # 更新字典数据 + self.current_dict_data[new_key] = img_path + + # 刷新显示 + self.refresh_view() + + # 发射信号通知更新 (包含英文字典类型) + self.dictionary_updated.emit( + self.original_folder_name, + self.current_part, + self.current_dict_type, # 英文键名 + self.current_dict_data + ) + + def show_context_menu(self, pos, img_path, key): """显示上下文菜单""" 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)) + delete_action.triggered.connect(lambda: self.delete_image(key)) + + # 添加编辑描述动作 (仅当是缺陷图字典时显示) + edit_desc_action = None + if self.current_dict_type == "defect_picture_dict": + edit_desc_action = QAction("编辑缺陷描述", self) + edit_desc_action.triggered.connect(lambda: self.edit_defect_description(key, img_path)) + menu.addAction(edit_desc_action) - 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 - ) + + def edit_defect_description(self, key, img_path): + """编辑缺陷图描述""" + # 获取当前描述 (如果没有则使用默认格式) + current_desc = self.current_dict_data.get(key, "") if key in self.current_dict_data else "" - 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): - """删除图片""" + # 创建并显示编辑对话框 + editor = DefectDescriptionEditor(self, current_description=current_desc) + if editor.exec_() == QDialog.Accepted: + # 获取新描述 + new_desc = editor.get_description() + + # 更新字典数据 - 删除旧键并添加新键 + if key in self.current_dict_data: + # 保留图片路径,只更新键名 + img_path = self.current_dict_data.pop(key) + self.current_dict_data[new_desc] = img_path + + # 刷新显示 + self.refresh_view() + + # 发射信号通知更新 + self.dictionary_updated.emit( + self.original_folder_name, + self.current_part, + self.current_dict_type, + self.current_dict_data + ) + + + def delete_image(self, key): + """删除指定图片""" reply = QMessageBox.question( self, "确认删除", - "确定要从字典中删除这张图片吗?", + f"确定要从字典中删除Key为 {key} 的图片吗?", 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.current_dict_data.pop(key, None) - # 刷新视图并发射信号 - self.refresh_dictionary_view() + # 刷新显示 + self.refresh_view() + + # 发射信号通知更新 self.dictionary_updated.emit( - self.current_dict_type, - self.current_blade, - dict_data + self.original_folder_name, + self.current_part, + self.current_dict_type, + self.current_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 + +class DefectDescriptionEditor(QDialog): + def __init__(self, parent=None, current_description="", img_path=""): + super().__init__(parent) + self.setWindowTitle("编辑缺陷图描述") + self.setMinimumSize(1000, 700) # 增大窗口尺寸以适应两个编辑器 + self.setStyleSheet(DEFECT_EDIT_WINDOW_STYLE) - # 获取或创建当前字典 - if self.current_blade not in self.dictionaries[self.current_dict_type]: - self.dictionaries[self.current_dict_type][self.current_blade] = {} + self.current_description = current_description + self.img_path = img_path + self.init_ui() - dict_data = self.dictionaries[self.current_dict_type][self.current_blade] + def init_ui(self): + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(5, 5, 5, 5) - # 生成默认描述(序号) - default_desc = f"图片_{len(dict_data) + 1}" + # 左侧 - 图片编辑器 (使用DefectMarkEditor) + self.image_editor = DefectMarkEditor(self, self.img_path) + self.image_editor.setMinimumSize(600, 600) + main_layout.addWidget(self.image_editor, stretch=2) - # 添加到字典 - dict_data[default_desc] = img_path + # 右侧 - 描述编辑器 + self.init_description_editor() + main_layout.addWidget(self.desc_editor_widget, stretch=1) - # 刷新视图并发射信号 - self.refresh_dictionary_view() - self.dictionary_updated.emit( - self.current_dict_type, - self.current_blade, - dict_data + def init_description_editor(self): + self.desc_editor_widget = QWidget() + layout = QVBoxLayout(self.desc_editor_widget) + layout.setContentsMargins(5, 0, 0, 0) + + # 创建一个简单的 QTextEdit 作为消息浏览器 + self.message_browser = QTextEdit() + self.message_browser.setReadOnly(True) + layout.addWidget(self.message_browser) + + # 解析当前描述 + parts = self.parse_current_description() + + # 1. 缺陷类型 + type_group = QGroupBox("缺陷类型") + type_group.setStyleSheet(GROUP_BOX_STYLE) + type_layout = QHBoxLayout(type_group) + + # 使用 SmartDropdown 作为缺陷类型选择器 + self.defect_type_dropdown = SmartDropdown( + dropdown_type="缺陷类型", + messagebrowser=self.message_browser, + parent=self ) + if "type" in parts: + self.defect_type_dropdown.combo_box.setCurrentText(parts["type"]) + type_layout.addWidget(self.defect_type_dropdown) + + layout.addWidget(type_group) + + # 2. 缺陷位置 + location_group = QGroupBox("缺陷位置") + location_group.setStyleSheet(GROUP_BOX_STYLE) + location_layout = QVBoxLayout(location_group) + + self.location_edit = QLineEdit() + self.location_edit.setStyleSheet(LINE_EDIT_STYLE) + self.location_edit.setText(parts.get("location", "")) + location_layout.addWidget(self.location_edit) + + layout.addWidget(location_group) + + # 3. 缺陷尺寸 + size_group = QGroupBox("缺陷尺寸") + size_group.setStyleSheet(GROUP_BOX_STYLE) + size_layout = QVBoxLayout(size_group) + + # 轴向尺寸 + axial_container = QWidget() + axial_container.setStyleSheet(DIMENSION_CONTAINER_STYLE) + axial_layout = QHBoxLayout(axial_container) + axial_layout.setContentsMargins(0, 0, 0, 0) + + axial_label = QLabel("轴向尺寸:") + axial_label.setStyleSheet(DIMENSION_LABEL_STYLE) + axial_layout.addWidget(axial_label) + + self.axial_edit = QLineEdit() + self.axial_edit.setStyleSheet(DIMENSION_EDIT_STYLE) + self.axial_edit.setText(parts.get("axial_size", "")) + axial_layout.addWidget(self.axial_edit) + + # 添加单位标签 + axial_unit = QLabel("mm") + axial_unit.setStyleSheet(LABEL_STYLE) + axial_layout.addWidget(axial_unit) + + axial_layout.addStretch() + size_layout.addWidget(axial_container) + + # 弦向尺寸 + chord_container = QWidget() + chord_container.setStyleSheet(DIMENSION_CONTAINER_STYLE) + chord_layout = QHBoxLayout(chord_container) + chord_layout.setContentsMargins(0, 0, 0, 0) + + chord_label = QLabel("弦向尺寸:") + chord_label.setStyleSheet(DIMENSION_LABEL_STYLE) + chord_layout.addWidget(chord_label) + + self.chord_edit = QLineEdit() + self.chord_edit.setStyleSheet(DIMENSION_EDIT_STYLE) + self.chord_edit.setText(parts.get("chord_size", "")) + chord_layout.addWidget(self.chord_edit) + + # 添加单位标签 + chord_unit = QLabel("mm") + chord_unit.setStyleSheet(LABEL_STYLE) + chord_layout.addWidget(chord_unit) + + chord_layout.addStretch() + size_layout.addWidget(chord_container) + + layout.addWidget(size_group) + + # 4. 其他属性 + attr_group = QGroupBox("其他属性") + attr_group.setStyleSheet(GROUP_BOX_STYLE) + attr_layout = QGridLayout(attr_group) + + # 可见程度 + visibility_label = QLabel("可见程度:") + visibility_label.setStyleSheet(LABEL_STYLE) + self.visibility_combo = QComboBox() + self.visibility_combo.setStyleSheet(COMBO_BOX_STYLE) + self.visibility_combo.addItems(["轻微", "一般", "重要", "严重"]) + if "visibility" in parts: + self.visibility_combo.setCurrentText(parts["visibility"]) + attr_layout.addWidget(visibility_label, 0, 0) + attr_layout.addWidget(self.visibility_combo, 0, 1) + + # 紧急程度 + urgency_label = QLabel("紧急程度:") + urgency_label.setStyleSheet(LABEL_STYLE) + self.urgency_combo = QComboBox() + self.urgency_combo.setStyleSheet(COMBO_BOX_STYLE) + self.urgency_combo.addItems(["不紧急", "一般", "紧急", "非常紧急"]) + if "urgency" in parts: + self.urgency_combo.setCurrentText(parts["urgency"]) + attr_layout.addWidget(urgency_label, 1, 0) + attr_layout.addWidget(self.urgency_combo, 1, 1) + + # 危重等级 + severity_label = QLabel("危重等级:") + severity_label.setStyleSheet(LABEL_STYLE) + self.severity_combo = QComboBox() + self.severity_combo.setStyleSheet(COMBO_BOX_STYLE) + self.severity_combo.addItems(["轻微", "一般", "重要", "严重"]) + if "severity" in parts: + self.severity_combo.setCurrentText(parts["severity"]) + attr_layout.addWidget(severity_label, 2, 0) + attr_layout.addWidget(self.severity_combo, 2, 1) + + layout.addWidget(attr_group) + + # 5. 维修建议 + suggestion_group = QGroupBox("维修建议") + suggestion_group.setStyleSheet(GROUP_BOX_STYLE) + suggestion_layout = QVBoxLayout(suggestion_group) + + self.suggestion_edit = QLineEdit() + self.suggestion_edit.setStyleSheet(LINE_EDIT_STYLE) + self.suggestion_edit.setText(parts.get("suggestion", "")) + suggestion_layout.addWidget(self.suggestion_edit) + + layout.addWidget(suggestion_group) + + # 按钮 + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.setStyleSheet(PRIMARY_BUTTON_STYLE) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) - 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] + def parse_current_description(self): + """解析当前描述字符串""" + parts = { + "type": "", + "location": "", + "axial_size": "", + "chord_size": "", + "visibility": "一般", + "urgency": "一般", + "severity": "一般", + "suggestion": "" + } + + if not self.current_description: + return parts - # 如果当前显示的是这个字典,则刷新视图 - if self.current_dict_type == dict_type and self.current_blade == blade_name: - self.refresh_dictionary_view() + # 尝试解析格式: 1_缺陷类型_缺陷位置_缺陷尺寸_可见程度_紧急程度_危重等级_维修建议 + desc_parts = self.current_description.split("_") + if len(desc_parts) >= 8: + parts["type"] = desc_parts[1] + parts["location"] = desc_parts[2] + + # 解析尺寸 (格式: 轴向XXmm弦向XXmm) + size_str = desc_parts[3] + axial_match = re.search(r"轴向(\d+)mm", size_str) + chord_match = re.search(r"弦向(\d+)mm", size_str) + if axial_match: + parts["axial_size"] = axial_match.group(1) + if chord_match: + parts["chord_size"] = chord_match.group(1) + + if len(desc_parts) > 4: + parts["visibility"] = desc_parts[4] + if len(desc_parts) > 5: + parts["urgency"] = desc_parts[5] + if len(desc_parts) > 6: + parts["severity"] = desc_parts[6] + if len(desc_parts) > 7: + parts["suggestion"] = "_".join(desc_parts[7:]) + + return parts - 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 + def get_description(self): + """生成描述字符串""" + # 生成序号 (使用当前时间戳的最后4位确保唯一性) + seq = str(int(time.time()) % 10000) + + # 获取各字段值 + defect_type = self.defect_type_dropdown.combo_box.currentText() + location = self.location_edit.text().strip() + axial_size = self.axial_edit.text().strip() + chord_size = self.chord_edit.text().strip() + visibility = self.visibility_combo.currentText() + urgency = self.urgency_combo.currentText() + severity = self.severity_combo.currentText() + suggestion = self.suggestion_edit.text().strip() + + # 构建尺寸字符串 + size_str = "" + if axial_size or chord_size: + size_parts = [] + if axial_size: + size_parts.append(f"轴向{axial_size}mm") + if chord_size: + size_parts.append(f"弦向{chord_size}mm") + size_str = "".join(size_parts) + + # 构建完整描述 + description = f"{seq}_{defect_type}_{location}_{size_str}_{visibility}_{urgency}_{severity}_{suggestion}" + return description \ No newline at end of file diff --git a/info_core/defines.py b/info_core/defines.py index ce52253..a624ffc 100644 --- a/info_core/defines.py +++ b/info_core/defines.py @@ -287,4 +287,47 @@ MESSAGE_BOX_BUTTON_STYLE = f""" background-color: #e74c3c; color: {LIGHT_TEXT_COLOR}; }} -""" \ No newline at end of file +""" + +# ====================== 缺陷描述编辑相关样式 ====================== +# 缺陷描述编辑窗口样式 +DEFECT_EDIT_WINDOW_STYLE = f""" + QWidget {{ + background-color: {LIGHT_COLOR}; + }} +""" + +# 输入框样式 +LINE_EDIT_STYLE = f""" + QLineEdit {{ + font-family: "{FONT_FAMILY}"; + font-size: {CONTENT_FONT_SIZE}pt; + min-height: {COMBO_BOX_HEIGHT + 5}px; + padding: 5px 10px; + border: 1px solid #bdc3c7; + border-radius: 4px; + }} +""" + +# 尺寸输入框样式 +DIMENSION_EDIT_STYLE = f""" + QLineEdit {{ + {LINE_EDIT_STYLE} + min-width: 60px; + }} +""" + +# 尺寸标签样式 +DIMENSION_LABEL_STYLE = f""" + QLabel {{ + {LABEL_STYLE} + min-width: 60px; + }} +""" + +# 尺寸控件容器样式 +DIMENSION_CONTAINER_STYLE = f""" + QWidget {{ + background-color: transparent; + }} +""" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3b9b0d2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,36 @@ +filelock==3.18.0 +fsspec==2025.7.0 +Jinja2==3.1.6 +lxml==6.0.0 +MarkupSafe==3.0.2 +mpmath==1.3.0 +networkx==3.4.2 +numpy==2.2.6 +nvidia-cublas-cu12==12.6.4.1 +nvidia-cuda-cupti-cu12==12.6.80 +nvidia-cuda-nvrtc-cu12==12.6.77 +nvidia-cuda-runtime-cu12==12.6.77 +nvidia-cudnn-cu12==9.5.1.17 +nvidia-cufft-cu12==11.3.0.4 +nvidia-cufile-cu12==1.11.1.6 +nvidia-curand-cu12==10.3.7.77 +nvidia-cusolver-cu12==11.7.1.2 +nvidia-cusparse-cu12==12.5.4.2 +nvidia-cusparselt-cu12==0.6.3 +nvidia-nccl-cu12==2.26.2 +nvidia-nvjitlink-cu12==12.6.85 +nvidia-nvtx-cu12==12.6.77 +opencv-python==4.12.0.88 +piexif==1.1.3 +pillow==11.3.0 +PySide6==6.9.1 +PySide6_Addons==6.9.1 +PySide6_Essentials==6.9.1 +python-docx==1.2.0 +shiboken6==6.9.1 +sympy==1.14.0 +torch==2.7.1 +torchaudio==2.7.1 +torchvision==0.22.1 +triton==3.3.1 +typing_extensions==4.14.1