我怎样才能限制 GraphicsScene 以便只能在加载的图像上绘制?

How can I limit the GraphicsScene in order to only be able to draw on the image loaded?

我有一个脚本正在加载图像并将其显示在 QGraphicsView 中,它还将 GraphicsView 的大小设置为图像的尺寸。

我需要能够放大/缩小,同时只能在图像上绘图。目前我可以放大/缩小但可以在 GraphicsView 上的任何地方绘制,这意味着当图像缩小时我可以在它的外部绘制,如下所示。

如何才能使 RubberBand 只能在图像上绘制?

这是我的 GraphicsView:

from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QPen
from SelectionBand import *
from time import sleep
class CustomGraphicsView(QGraphicsView):
    
    def __init__(self, parent=None):
        super(CustomGraphicsView, self).__init__()
        self.setScene(QGraphicsScene(self))         
        self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.scene().setBackgroundBrush(Qt.gray)
        self.brush = QBrush(Qt.green)
        self.pen = QPen(Qt.blue)

    def setImage(self, image):
        self.img = self.scene().addPixmap(image) 
        self.resize(self.img.boundingRect().width(),self.img.boundingRect().height())
        self.scene().setSceneRect(self.img.boundingRect())

    def wheelEvent(self, event):
        if event.modifiers() & Qt.ControlModifier:
            x = event.angleDelta().y() / 120
            if x > 0:
                self.scale(1.05, 1.05)
            elif x < 0:
                self.scale(.95, .95)
        else:
            super().wheelEvent(event)

    def mousePressEvent(self, event):
        pos = self.mapToScene(event.pos())
        self.xPos = pos.x()
        self.yPos = pos.y()

        self.band = SelectionBand(self)
        self.band.setGeometry(pos.x(), pos.y(), 0, 0)
        item = self.scene().addWidget(self.band)
        item.setFlag(QGraphicsItem.ItemIsMovable)
        item.setZValue(1)
        
    def mouseMoveEvent(self, event):
        pos = self.mapToScene(event.pos())
        self.width = pos.x() - self.xPos
        self.height = pos.y() - self.yPos
        if self.width < 0 and self.height < 0:
            self.band.setGeometry(pos.x(), pos.y(), abs(self.width), abs(self.height))
        elif self.width < 0:
            self.band.setGeometry(pos.x(), self.yPos, abs(self.width), abs(self.height))
        elif self.height < 0:
            self.band.setGeometry(self.xPos, pos.y(), abs(self.width), abs(self.height))
        else: self.band.setGeometry(self.xPos, self.yPos, abs(self.width), abs(self.height))

这是 RubberBand 代码:

from typing import ItemsView
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QRubberBand, QWidget,QSizeGrip
from PyQt5.QtCore import QEasingCurve, Qt, QEvent
from PyQt5.QtGui import QCursor, QHoverEvent, QPixmap
import sys
# Functionality: 
#     - On initial mouse click 
#         Create new RubberBand, select area until mouse is released 
#     - Allow for resizing when selected


class SelectionBand(QWidget):
    def __init__(self, parent=None):
        super(SelectionBand, self).__init__()
        # self.setMouseTracking=True
        self.draggable = True
        self.dragging_threshold = 5
        self.borderRadius = 5
        self.setWindowFlags(Qt.SubWindow)
        layout = QHBoxLayout(self) 
        layout.setContentsMargins(0,0,0,0)
        layout.addWidget(
            QSizeGrip(self), 0,
            Qt.AlignRight | Qt.AlignBottom)
        self._band = QtWidgets.QRubberBand(
            QtWidgets.QRubberBand.Rectangle, self)

        self._band.show()
        self.show()

    def resizeEvent(self, event):
        self._band.resize(self.size())

由于您使用场景矩形作为参考,因此您必须检查选区的几何形状是否在场景边缘内,并且您还应该考虑橡皮筋小部件的最小尺寸。

此外,与其进行复杂的计算来检查宽度或高度是否为“负数”,您可以使用两个点(单击位置和从鼠标移动接收到的位置)创建 QRectF,然后使用 normalized() 函数得到相同的矩形,但具有正的宽度和高度。

class CustomGraphicsView(QGraphicsView):
    def mousePressEvent(self, event):
        self.startPos = self.mapToScene(event.pos())

        if not self.band:
            self.band = SelectionBand(self)
            self.bandItem = self.scene().addWidget(self.band)
            self.bandItem.setFlag(QGraphicsItem.ItemIsMovable)
            self.bandItem.setZValue(1)
            sceneRect = self.sceneRect()
            if self.startPos.x() < sceneRect.x():
                self.startPos.setX(sceneRect.x())
            elif self.startPos.x() > sceneRect.right() - self.band.minimumWidth():
                self.startPos.setX(sceneRect.right() - self.band.minimumWidth())
            if self.startPos.y() < sceneRect.y():
                self.startPos.setY(sceneRect.y())
            elif self.startPos.y() > sceneRect.bottom() - self.band.minimumHeight():
                self.startPos.setY(sceneRect.bottom() - self.band.minimumHeight())
            self.bandItem.setPos(self.startPos)
        
    def mouseMoveEvent(self, event):
        mousePos = self.mapToScene(event.pos())
        # create a normalized rectangle based on the two points
        rect = QRectF(self.startPos, mousePos).normalized()
        if rect.width() < self.band.minimumWidth():
            rect.setWidth(self.band.minimumWidth())
            if rect.x() < self.startPos.x():
                rect.moveLeft(self.startPos.x())
        if rect.height() < self.band.minimumHeight():
            rect.setHeight(self.band.minimumHeight())
            if rect.y() < self.startPos.y():
                rect.moveBottom(self.startPos.y())

        sceneRect = self.sceneRect()
        if rect.x() < sceneRect.x():
            rect.setX(sceneRect.x())
        elif rect.x() > sceneRect.right() - self.band.minimumWidth():
            rect.setX(sceneRect.right() - self.band.minimumWidth())
        if rect.right() > sceneRect.right():
            rect.setRight(sceneRect.right())

        if rect.y() < sceneRect.y():
            rect.setY(sceneRect.y())
        elif rect.y() > sceneRect.bottom() - self.band.minimumHeight():
            rect.setY(sceneRect.bottom() - self.band.minimumHeight())
        if rect.bottom() > sceneRect.bottom():
            rect.setBottom(sceneRect.bottom())

        self.bandItem.setGeometry(rect)

请考虑使用 QWidget 进行选择并不是一个好主意(并且您的实现无论如何都有点缺陷):您没有使用 QWidget 的大部分功能(除了大小控制,这是反正没用过),所以用起来有点无意义

您可以改为使用 QGraphicsRectItem 的子class。这有很多优点:不仅它的用法和图形视图框架更加连贯,而且,例如,您可以使用 cosmetic 笔作为边框,它始终是即使缩小也能清晰显示。

class SelectionBandItem(QGraphicsRectItem):
    _minimumWidth = _minimumHeight = 1
    def __init__(self):
        super().__init__()
        color = QApplication.palette().highlight().color()
        pen = QPen(color, 1)
        pen.setCosmetic(True)
        self.setPen(pen)
        color.setAlpha(128)
        self.setBrush(color)
        self.setFlag(QGraphicsItem.ItemIsMovable)

    def minimumWidth(self):
        return self._minimumWidth

    def minimumHeight(self):
        return self._minimumHeight

    def setGeometry(self, rect):
        # we cannot use setRect, as it will only set the drawn rectangle but
        # not the position of the item, which makes it less intuitive; it's
        # better to use the given rectangle to set the position and then
        # resize the item's rectangle
        self.setPos(rect.topLeft())
        current = self.rect()
        current.setSize(rect.size())
        self.setRect(current)

    def geometry(self):
        return self.rect().translated(self.pos())


class CustomGraphicsView(QGraphicsView):
    # ...
    def mousePressEvent(self, event):
        self.startPos = self.mapToScene(event.pos())

        if not self.band:
            self.band = SelectionBandItem()
            self.scene().addItem(self.band)
            sceneRect = self.sceneRect()
            if self.startPos.x() < sceneRect.x():
                self.startPos.setX(sceneRect.x())
            elif self.startPos.x() > sceneRect.right():
                self.startPos.setX(sceneRect.right())
            if self.startPos.y() < sceneRect.y():
                self.startPos.setY(sceneRect.y())
            elif self.startPos.y() > sceneRect.bottom():
                self.startPos.setY(sceneRect.bottom())
            self.band.setPos(self.startPos)

    def mouseMoveEvent(self, event):
        # all of the above, except for this:
        self.band.setGeometry(rect)

然后,如果你想添加对大小手柄的支持,请在 SelectionBandItem class 的 mouseMoveEvent 中实现它。

不相关的注释:

  • self.widthself.height 是所有 Qt 小部件的属性,您不应该用其他值覆盖它们,特别是如果这些值与 self 无关;此外,由于您不断更改 mouseMoveEvent 中的这些值,因此将它们设为实例属性真的没有意义:如果您需要从其他函数访问 band 的大小,只需使用 self.band.width()self.band.height();
  • 如果您只是检查角度增量是否大于或小于 0,则将角度增量除以 120 毫无意义;
  • 新项目总是添加到 0 z 级别的项目堆栈顶部,因此将其设置为 1 没什么用处;
  • setMouseTracking是函数不是变量;
  • 向场景添加 new 小部件时,您不应在其 __init__ 中调用 show,因为这会使它们显示为正常 window瞬间;
  • QBoxLayout.addWidgetstretch 参数已经默认为零,如果要指定对齐方式,最好显式使用关键字;另外,如果您将小部件添加到布局,则不需要父参数:layout.addWidget(QSizeGrip(), alignment=Qt.AlignRight|Qt.AlignBottom)
  • 如果您要将图形视图添加到布局中,调用它的 resize() 是完全没用的;布局管理器的范围是通过设置其所有项目的位置和大小来[=44​​=]管理布局,因此手动请求任何几何更改在概念上是错误的;虽然它 可以 调整小部件的大小,但只要调整了任何父级或兄弟级的大小,布局就会覆盖该请求;