Pyside6 Graphview鼠标框选

上一篇博客给了一个QImageView类,其中实现了图片浏览器的基础鼠标操作,在此基础之上,进一步实现鼠标框选的操作。具体实现时,我们先构建一个场景类QImageScene,用于管理场景中的Item,场景中的Item主要有两类,一类是图像,另一类是选择框。对于选择框的操作,我们把鼠标事件定义在QImage Scene类中。

为了方便我们理清逻辑,这里强调下消息流的方向。视图框架事件的传递顺序是view->scene->item,如果需要将事件继续向后传递,使用event->ignore()是没用的,猜测因为view看做是一个控件,scene和item都是控件内的组件,ignore只能处理控件到控件的事件,但是控件内的事件无能为力。这里可以使用QGraphicsView::mousexxxEvent(event)这样的函数,将event事件再次传入视图。上面的例子也是用到了这样的方法。

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
                            QMetaObject, QObject, QPoint, QPointF, QRect, QRectF,
                            QSize, QSizeF, QTime, QUrl, Qt, Signal)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
                           QFont, QFontDatabase, QGradient, QIcon,
                           QImage, QKeySequence, QLinearGradient, QPainter,
                           QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractScrollArea, QApplication, QGraphicsView, QMainWindow,
                               QMenuBar, QSizePolicy, QStatusBar, QVBoxLayout,
                               QWidget)
from PySide6 import QtGui, QtWidgets


class QImageScene(QtWidgets.QGraphicsScene):
    def __init__(self, parent=None):
        super(QImageScene, self).__init__(QRectF(-500, -500, 1000, 1000), parent)
        self._start = QPointF()
        self.it_img = None
        self.it_rect = None

    def remove_img(self):
        if self.it_img is not None:
            self.removeItem(self.it_img)
            self.it_img = None

    def remove_rect(self):
        if self.it_rect is not None:
            self.removeItem(self.it_rect)
            self.it_rect = None

    def has_rect_roi(self):
        if self.it_rect is None or self.it_img is None:
            return False
        return True

    def get_rect_roi(self):
        if self.it_img is None:
            return None

        img_rect = self.it_img.boundingRect()
        if self.it_rect is None:
            return img_rect

        lab_rect = self.it_rect.rect()
        if not lab_rect.intersects(img_rect):
            return img_rect

        return lab_rect.intersected(img_rect)

    def set_image(self, img):
        if self.it_img is not None:
            self.removeItem(self.it_img)

        self.it_img = QtWidgets.QGraphicsPixmapItem(img)
        self.addItem(self.it_img)

    def mousePressEvent(self, event):
        #if self.itemAt(event.scenePos(), QtGui.QTransform()) is None:
        if event.button() == Qt.RightButton \
                and self.it_img is not None:
            if self.it_rect is not None:
                self.removeItem(self.it_rect)
            self.it_rect = QtWidgets.QGraphicsRectItem()
            self.it_rect.setPen(QtGui.QPen(Qt.red, 0, Qt.SolidLine))
            self.it_rect.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
            self.addItem(self.it_rect)
            self._start = event.scenePos()
            r = QRectF(self._start, self._start)
            self.it_rect.setRect(r)
        super(QImageScene, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.it_rect is not None and event.buttons() == Qt.RightButton:
            r = QRectF(self._start, event.scenePos()).normalized()
            self.it_rect.setRect(r)
        super(QImageScene, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.it_img is None:
            self.remove_img()

        if self.it_rect is not None:
            rect = self.it_rect.rect()
            rect_img = self.it_img.boundingRect()
            if rect.width() < 4 or rect.height() < 4 or not rect.intersects(rect_img):
                self.remove_rect()
            else:
                r = rect.intersected(rect_img).normalized()
                self.it_rect.setRect(r)
        super(QImageScene, self).mouseReleaseEvent(event)

下面是QImageView类,调用QImageScene实现完整功能(总体和前节博客给出的代码类似):

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
                            QMetaObject, QObject, QPoint, QPointF, QRect, QRectF,
                            QSize, QSizeF, QTime, QUrl, Qt, Signal)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
                           QFont, QFontDatabase, QGradient, QIcon,
                           QImage, QKeySequence, QLinearGradient, QPainter,
                           QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractScrollArea, QApplication, QGraphicsView, QMainWindow,
                               QMenuBar, QSizePolicy, QStatusBar, QVBoxLayout,
                               QWidget)
from PySide6 import QtGui, QtWidgets
from window.viewer.qimagescene import QImageScene


class QImageView(QGraphicsView):
    #signal_lclick = Signal(str)

    def __init__(self, parent):
        super(QImageView, self).__init__(parent)
        self.setMouseTracking(True)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setAcceptDrops(False)
        
        self.setStyleSheet(u"broder: none")
        #self.setCacheMode(QGraphicsView.CacheBackground)
        #self.setDragMode(QGraphicsView.ScrollHandDrag)
        #self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        #self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.it_img = QtWidgets.QGraphicsPixmapItem()

        self.scene = QImageScene()
        self.img = QtGui.QPixmap()
        self.img_sz = []

        self.scale_current = 1
        self.scale_fill = 1
        self.scale_index = 0
        self.scale_lut_base = [0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.25, 1.5, 1.75,
                               2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 50.0]
        self.scale_lut = self.scale_lut_base.copy()

        self.left_click = QPoint(0, 0)
        self.pointer_pos = QPoint(0, 0)

    def update_shown_image(self, img):
        self.img = img
        self.img_sz = self.img.size()
        self.scene.setSceneRect(0, 0, self.img_sz.width(), self.img_sz.height())
        self.scene.set_image(img)
        #self.scene.removeItem(self.it_img)
        #self.it_img = QtWidgets.QGraphicsPixmapItem(self.img)
        #self.scene.addItem(self.it_img)

    def set_shown_image(self, img):
        #self.img.load(filename)
        self.img = img
        self.img_sz = self.img.size()
        self.scene.setSceneRect(0, 0, self.img_sz.width(), self.img_sz.height())
        self.scene.set_image(img)
        self.setScene(self.scene)
        self.setTransform(QtGui.QTransform())

        # init scale lut
        size = self.size()
        scale_fill_h = size.height() / self.img_sz.height()
        scale_fill_w = size.width() / self.img_sz.width()
        self.scale_fill = min(scale_fill_w, scale_fill_h)
        self.scale_lut = self.scale_lut_base.copy()
        if scale_fill_h not in self.scale_lut:
            self.scale_lut.append(scale_fill_h)
        if scale_fill_w not in self.scale_lut:
            self.scale_lut.append(scale_fill_w)
        self.scale_lut.sort()
        self.scale_index = self.scale_lut.index(self.scale_fill)
        self.scale_current = self.scale_lut[self.scale_index]

        img_center = QPoint(self.img_sz.width() // 2, self.img_sz.height() // 2)
        self.setTransform(QtGui.QTransform(self.scale_fill, 0, 0, 0, self.scale_fill, 0, 0, 0, 1))
        self.centerOn(img_center)


    def wheelEvent(self, event):
        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        self.setResizeAnchor(QGraphicsView.NoAnchor)
        if event.angleDelta().y() > 0.5 and self.scale_index < len(self.scale_lut) - 1:  # zoom in
            self.scale_index = self.scale_index + 1
        elif event.angleDelta().y() < 0.5 and self.scale_index > 0:  # zoom out
            self.scale_index = self.scale_index - 1
        self.scale_current = self.scale_lut[self.scale_index]

        oldpos = self.mapToScene(QPoint(event.position().x(), event.position().y()))
        self.setTransform(QtGui.QTransform(self.scale_current, 0, 0, 0, self.scale_current, 0, 0, 0, 1))
        newpos = self.mapToScene(QPoint(event.position().x(), event.position().y()))
        delta = newpos - oldpos
        self.translate(delta.x(), delta.y())

        event.ignore()
        #super(QImageView, self).wheelEvent(event)

    def mousePressEvent(self, event):
        button = event.button()
        # move image
        if button == Qt.LeftButton:
            self.setCursor(Qt.OpenHandCursor)
            self.left_click = self.mapToScene(event.pos())
            event.accept()
        super(QImageView, self).mousePressEvent(event)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        button = event.buttons()
        self.pointer_pos = self.mapToScene(event.pos())

        # move image
        if button == Qt.LeftButton:
            mouse_pos = self.mapToScene(event.pos())
            delta = mouse_pos - self.left_click
            delta = delta * self.transform().m11()  # 乘以缩放比
            self.centerOn(self.mapToScene(QPoint(self.viewport().rect().width() / 2 - delta.x(),
                                                 self.viewport().rect().height() / 2 - delta.y())))
            event.accept()
        else:
            event.ignore()
        super(QImageView, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        button = event.button()
        # move image
        if button == Qt.LeftButton:
            self.setCursor(Qt.ArrowCursor)
            mouse_pos = self.mapToScene(event.pos())
            delta = mouse_pos - self.left_click
            delta = delta * self.transform().m11() # 乘以缩放比
            if delta.x() != 0 or delta.y() != 0:
                self.centerOn(self.mapToScene(QPoint(self.viewport().rect().width() / 2-delta.x(),
                                                     self.viewport().rect().height() / 2-delta.y())))
            event.accept()
        elif button == Qt.RightButton:
            pass
        super(QImageView, self).mouseReleaseEvent(event)

我们在窗体中加入提升过后的graphview就可以使用了。

发表评论