PyQt4 - 拖动后 QGraphicsItem 位置未正确映射到场景中

Posted

技术标签:

【中文标题】PyQt4 - 拖动后 QGraphicsItem 位置未正确映射到场景中【英文标题】:PyQt4 - QGraphicsItem position doesn't map into scene properly after drag 【发布时间】:2015-08-14 20:32:52 【问题描述】:

我创建了一个基于QWidgetImageView 小部件,其中包含一个QGraphicsView。此小部件显示图像,并允许您使用鼠标通过QGraphicsRectItem 绘制 ROI(感兴趣区域)。 ImageView 小部件运行良好,但如果拖动 ROI 矩形并且您想在另一个地方重绘它,则鼠标事件捕获的位置无法正确映射到场景。

这里有几张图片来解释我的意思。

包含 ImageView 小部件的对话框,其操作控制小部件本身:

如果启用选择,您可以绘制一个矩形:

注意矩形右下角的指针位置。 ROI 可以在图像内部绘制。

如果禁用选择,您可以拖动之前绘制的矩形:

在此之后,如果启用了选择并且您想重绘矩形:

指针位置被很好地捕获(这是事实!),正如您在对话框状态栏中看到的那样,但是这个位置(用于设置矩形几何形状)不再对应于矩形位置。

我对 Qt 很陌生,这里是代码:

代码

class ImageView(QtGui.QWidget):
    scaleChanged = QtCore.pyqtSignal()
    statusChanged = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(ImageView, self).__init__(parent)
        self.scale_factor = 0.0

        # Imagen
        self.image_item = QtGui.QGraphicsPixmapItem()

        # ROI
        self.ROI_item = FancyQGraphicsRectItem(self.image_item)
        self.ROI_item.setFlag(self.ROI_item.ItemIsMovable)
        self.ROI_item.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
        self.ROI_item.setPen(QtGui.QPen(QtCore.Qt.white, 0, QtCore.Qt.DashDotLine))
        self.ROI_item.setCursor(QtCore.Qt.OpenHandCursor)
        self.ROI_added = False

        # Escena
        self.scene = QtGui.QGraphicsScene()

        # Vista
        self.view = FancyQGraphicsView()
        self.view.ROI_item = self.ROI_item
        self.view.statusChanged.connect(self.change_status)
        self.view.setScene(self.scene)
        self.view.setBackgroundRole(QtGui.QPalette.Dark)
        self.view.setAlignment(QtCore.Qt.AlignCenter)
        self.view.setFrameShape(QtGui.QFrame.NoFrame)
        self.view.setRenderHint(QtGui.QPainter.Antialiasing, False)
        self.view.setMouseTracking(True)

        # Disposición
        layout = QtGui.QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.view)

        self.setLayout(layout)

    def setImage(self, pixmap):
        self.image_item.setPixmap(pixmap)
        self.scene.addItem(self.image_item)
        self.scene.setSceneRect(0, 0, self.image_item.boundingRect().right(), self.image_item.boundingRect().bottom())
        self.view.setSceneSize()

    def selectionEnable(self, value):
        self.view.selectionEnable(value)
        if value:
            self.ROI_item.setCursor(QtCore.Qt.CrossCursor)
            self.view.setInteractive(False)
            self.view.viewport().setCursor(QtCore.Qt.CrossCursor)
            if not self.ROI_added:
                self.ROI_added = True
        else:
            self.view.viewport().setCursor(QtCore.Qt.ArrowCursor)
            self.ROI_item.setCursor(QtCore.Qt.OpenHandCursor)
            self.view.setInteractive(True)

    def setupDrag(self, value):
        if value:
            self.view.setInteractive(False)
            self.view.setDragMode(self.view.ScrollHandDrag)
        else:
            self.view.setDragMode(self.view.NoDrag)
            self.view.setInteractive(True)

    def normal_size(self):
        if self.scale_factor != 1.0:
            self.view.resetMatrix()
            self.scale_factor = 1.0
            self.scaleChanged.emit()

    def scale_image(self, factor):
        self.scale_factor *= factor
        self.view.scale(factor, factor)
        self.scaleChanged.emit()

    def delete_roi(self):
        self.ROI_item.setRect(0, 0, 0, 0)

    @QtCore.pyqtSlot(str)
    def change_status(self, message):
        self.statusChanged.emit(message)


class FancyQGraphicsView(QtGui.QGraphicsView):
    statusChanged = QtCore.pyqtSignal(str)
    scene_size = (0, 0)
    ROI_item = None
    event_origin = None
    selection = False
    click = False

    def mousePressEvent(self, event):
        if self.selection:
            event_pos = self.mapToScene(event.pos())
            pos = (int(event_pos.x()), int(event_pos.y()))
            if 0 <= pos[0] < self.scene_size[0] and 0 <= pos[1] < self.scene_size[1]:
                self.event_origin = event_pos
            else:
                self.event_origin = None
            self.click = True
        else:
            QtGui.QGraphicsView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        event_pos = self.mapToScene(event.pos())
        if self.selection and self.click:
            if self.event_origin:
                self.statusChanged.emit("x1: 0:>5d    y1: 1:>5d    "
                                        "x2: 2:>5d    y2: 3:>5d".format(int(self.event_origin.x()),
                                                                            int(self.event_origin.y()),
                                                                            int(event_pos.x()),
                                                                            int(event_pos.y())))
                if event_pos.x() < 0:
                    event_pos.setX(0)
                elif event_pos.x() > self.scene_size[0] - 1:
                    event_pos.setX(self.scene_size[0] - 1)
                if event_pos.y() < 0:
                    event_pos.setY(0)
                elif event_pos.y() > self.scene_size[1] - 1:
                    event_pos.setY(self.scene_size[1] - 1)
                self.ROI_item.setRect(QtCore.QRectF(self.event_origin, event_pos).normalized())
                print self.ROI_item.rect(), self.event_origin, event_pos
            else:
                self.statusChanged.emit("x: 0:>5d    y: 1:>5d".format(int(event_pos.x()), int(event_pos.y())))
        else:
            self.statusChanged.emit("x: 0:>5d    y: 1:>5d".format(int(event_pos.x()), int(event_pos.y())))
            QtGui.QGraphicsView.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.selection:
            self.click = False
            if self.event_origin:
                self.event_origin = None
        else:
            QtGui.QGraphicsView.mouseReleaseEvent(self, event)

    def selectionEnable(self, value):
        self.selection = value

    def setSceneSize(self):
        rect = self.scene().sceneRect()
        self.scene_size = (rect.width(), rect.height())


class FancyQGraphicsRectItem(QtGui.QGraphicsRectItem):

    def mousePressEvent(self, event):
        self.setCursor(QtCore.Qt.ClosedHandCursor)
        QtGui.QGraphicsRectItem.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        QtGui.QGraphicsRectItem.mouseMoveEvent(self, event)
        # Maybe this could be modified

    def mouseReleaseEvent(self, event):
        self.setCursor(QtCore.Qt.OpenHandCursor)
        QtGui.QGraphicsRectItem.mouseReleaseEvent(self, event)

为什么会这样?以前我尝试在矩形的图像中实现一个受限的可移动区域,但是当被拖动时,该项目无法正确识别其在场景中的位置。

【问题讨论】:

我知道这并不能解决您的问题,但是您是否有理由从头开始构建 ROI 功能?如果我没记错的话,Pyqtgraph 有可移动的投资回报率。 @three_pineapples 因为我想学习。 ;) 我知道 PyQtGraph,但因为我只需要这个基本功能,所以我想自己构建它。 酷!我只是想检查一下你没有浪费时间:) 【参考方案1】:

您的问题是由于您使用QGraphicsRectItem 的方式造成的。当您最初正确设置矩形时,该项目最初放置在场景的坐标(0,0) 处。因此,QGraphicsRectItem(0,0) 延伸到矩形的右下角坐标(在场景坐标中)。

当您移动 ROI 时,您正在平移整个项目,而不仅仅是项目内的矩形。这意味着该项目不再位于(0,0),因此您提供给它的坐标是偏移的,因为您使用的是场景坐标而不是项目坐标。

有多种方法(例如QGraphicsItem.mapFromScene())可以将坐标转换为正确的参考点(请注意,这应该考虑到您的 ROI 也是self.image_item 的子级这一事实,如果有的话离开(0,0))。

另一种选择是您可以将 ROI 重新定位到初始点击坐标,然后根据初始点击坐标和当前点击坐标之间的差异调整大小。所以在mouseMoveEvent 你可以这样做:

self.ROI_item.setPos(self.event_origin)
self.ROI_item.setRect(QtCore.QRectF(QtCore.QPointF(0,0), event_pos-self.event_origin).normalized())

但是,我怀疑如果父项被移动,或者如果对QGraphicsView 应用了缩放,这可能会中断。在这种情况下,您可能需要使用QGraphicsItem.mapFromScene() 方法进行调查(尽管始终将项目重新定位到初始点击位置可能很有用,如果只是为了减少项目的边界框)

【讨论】:

以上是关于PyQt4 - 拖动后 QGraphicsItem 位置未正确映射到场景中的主要内容,如果未能解决你的问题,请参考以下文章

用鼠标调整 QGraphicsItem 的大小

拖动视图时QGraphicsItem消失

pyqt4的可拖动窗口

Python / PyQt4:如何使 QComboBox 项可拖动

重新实现 mousePressEvent 后无法在 QGraphicsView 中拖动项目

为啥 itemAt() 并不总能找到 QGraphicsItem