如何避免橡皮筋变成一条线以及如何设置橡皮筋只显示边框线?

How to aviod rubberband become a a line and How to set rubberband only show border line?

我写了一个简单的应用程序,拖动或缩放MainView时,PartView橡皮筋会在PartView中显示场景区域。但有时橡皮筋会变成一条线,有时橡皮筋disappear.So如何避免出现这种现象?有时我希望橡皮筋只显示它的边框线,而不包含它的浅蓝色矩形,那么我该如何编写代码?

我的代码

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import random
import math

r = lambda : random.randint(0, 255)
r255 = lambda : (r(), r(), r())

class Scene(QGraphicsScene):
    def __init__(self):
        super().__init__()
        for i in range(1000):
            item = QGraphicsEllipseItem()
            item.setRect(0, 0, r(), r())
            item.setBrush(QColor(*r255()))
            item.setPos(r()*100, r()*100)
            self.addItem(item)

class MainView(QGraphicsView):
    sigExposeRect = pyqtSignal(QRectF)
    def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
        super().drawBackground(painter, rect)
        self.sigExposeRect.emit(rect)

    def wheelEvent(self, event: QWheelEvent) -> None:
        factor = math.pow(2.7, event.angleDelta().y()/360)
        self.scale(factor, factor)

class PartView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.r = QRubberBand(QRubberBand.Rectangle, self)
        self.r.setWindowOpacity(1)
        self.r.show()

class View(QSplitter):
    def __init__(self):
        super().__init__()
        self.m = MainView()
        self.m.setMouseTracking(True)
        self.m.setDragMode(QGraphicsView.ScrollHandDrag)
        self.m.sigExposeRect.connect(self.onExposeRect)

        self.p = PartView()

        self.m.setScene(Scene())
        self.p.setScene(self.m.scene())
        self.p.fitInView(self.m.scene().itemsBoundingRect())

        self.addWidget(self.m)
        self.addWidget(self.p)

    def onExposeRect(self, rect: QRectF):
        prect = self.p.mapFromScene(rect).boundingRect()
        self.p.r.setGeometry(prect)

app = QApplication([])
v = View()
v.show()
app.exec()

我的结果

我认为问题在于传递给 drawBackground 方法的 qrect 仅包括之前不在视口中的背景部分。不过对此并不乐观。

无论哪种方式,通过将整个视口的区域发送到 onExposeRect 插槽,我都能够实现您避免只绘制一部分橡皮筋的目标。


class MainView(QGraphicsView):
    sigExposeRect = pyqtSignal(QRectF)
    def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
        # Adding this next line was the only change I made
        orect = self.mapToScene(self.viewport().geometry()).boundingRect() 
        super().drawBackground(painter, rect)
        self.sigExposeRect.emit(orect)  # and passing it to the slot.

    def wheelEvent(self, event: QWheelEvent) -> None:
        factor = math.pow(2.7, event.angleDelta().y()/360)
        self.scale(factor, factor)


Graphics View 的一个基本方面是它在绘制甚至数千个元素时的高性能。

为了实现这一点,最重要的优化之一是只更新场景中真正需要重绘的部分,类似于项目视图所做的,因为它们通常只重绘 实际 需要更新,而不是 总是 绘制整个可见区域,这可能是一个巨大的瓶颈。

这就是覆盖drawBackground无效的原因:有时,只有一小部分场景被更新(而且,在某些情况下,甚至没有更新完全完成),drawBackgroundrect 参数仅包括 that 部分,而不是整个可见区域。结果是在这些情况下,信号会发出一个与可见区域不一致的矩形。

由于可见区域是相对于滚动区域的 viewport 而言的,因此接收有关该区域更新的唯一安全方法是连接到水平和垂直滚动条 (即使它们被隐藏也总是有效)。

进一步的预防措施是通过连接到源的 sceneRectChanged signal and also overriding the setSceneRect() 来确保每当场景矩形发生更改时也更新可见矩形(因为滚动条可能不会反映该更改)看法。考虑到垂直条和滚动条的变化可能会重合,通常最好用0-delay QTimer延迟信号,这样它只在可见区域同时发生更多变化时才发送一次。

请注意,由于您实际上并没有使用 QRubberBand 的功能,因此它的用途不大,尤其是当您还需要自定义绘画时。此外,由于橡皮筋是视图的子视图,因此即使滚动预览视图,它也会始终保持其位置。

在下面的示例中,我将展示两种绘制“假”橡皮筋的方法(但只选择其中一种,评论一个或另一个来测试它们),这将始终与两个源一致和目标视图。

class MainView(QGraphicsView):
    sigExposeRect = pyqtSignal(QRectF)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.signalDelay = QTimer(self, singleShot=True, interval=0, 
            timeout=self.emitExposeRect)
        # signals might have arguments that collide with the start(interval)
        # override of QTimer, let's use a basic lambda that ignores them
        self.delayEmit = lambda *args: self.signalDelay.start()
        self.verticalScrollBar().valueChanged.connect(self.delayEmit)
        self.horizontalScrollBar().valueChanged.connect(self.delayEmit)

    def emitExposeRect(self):
        topLeft = self.mapToScene(self.viewport().geometry().topLeft())
        bottomRight = self.mapToScene(self.viewport().geometry().bottomRight())
        self.sigExposeRect.emit(QRectF(topLeft, bottomRight))

    def setScene(self, scene):
        if self.scene() == scene:
            return
        if self.scene():
            try:
                self.scene().sceneRectChanged.disconnect(self.delayEmit)
            except TypeError:
                pass
        super().setScene(scene)
        if scene:
            scene.sceneRectChanged.connect(self.delayEmit)

    def setSceneRect(self, rect):
        super().setSceneRect(rect)
        self.delayEmit()

    def wheelEvent(self, event: QWheelEvent) -> None:
        factor = math.pow(2.7, event.angleDelta().y()/360)
        self.scale(factor, factor)


class PartView(QGraphicsView):
    exposeRect = None
    def updateExposeRect(self, rect):
        if self.exposeRect != rect:
            self.exposeRect = rect
            self.viewport().update()

    def paintEvent(self, event):
        super().paintEvent(event)
        if not self.exposeRect:
            return
        rect = self.mapFromScene(self.exposeRect).boundingRect()

        # use either *one* of the following:

        # 1. QStyle implementation, imitates QRubberBand
        qp = QStylePainter(self.viewport())
        opt = QStyleOptionRubberBand()
        opt.initFrom(self)
        opt.rect = rect
        qp.drawControl(QStyle.CE_RubberBand, opt)

        # 2. basic QPainter
        qp = QPainter(self.viewport())
        color = self.palette().highlight().color()
        qp.setPen(self.palette().highlight().color())
        # for background
        bgd = QColor(color)
        bgd.setAlpha(40)
        qp.setBrush(bgd)
        qp.drawRect(rect)


class View(QSplitter):
    def __init__(self):
        super().__init__()
        self.m = MainView()
        self.m.setMouseTracking(True)
        self.m.setDragMode(QGraphicsView.ScrollHandDrag)

        self.p = PartView()

        self.m.setScene(Scene())
        self.p.setScene(self.m.scene())
        self.p.fitInView(self.m.scene().itemsBoundingRect())

        self.addWidget(self.m)
        self.addWidget(self.p)

        self.m.sigExposeRect.connect(self.p.updateExposeRect)

PS:请在真正有意义的时候使用单字母变量(常用变量、坐标、循环占位符等),不要用于复杂对象,尤其是属性:有使用 self.mself.p 没有任何好处,您获得的唯一结果是降低代码对您和其他人的可读性。