QGraphicsItem 线宽/转换从 PyQt4 到 PyQt5

QGraphicsItem line width / transformation changes from PyQt4 to PyQt5

我有一些用 PyQt 制作的小型实用程序 GUI,它们使用 QGraphicsScene 和其中的一些项目,以及一个响应用户点击的视图(用于制作框、选择点等)。

在我使用的(离线)机器上,软件刚刚从Anaconda 2.5升级到Anaconda 4.3,包括从PyQt4到PyQt5的切换。一切仍然有效,除了如果场景矩形是在除像素坐标以外的任何地方定义的,我的各种 QGraphicsItem 对象的转换不知何故会搞砸。

前期问题:从 PyQt4 到 PyQt5 的项转换发生了什么变化?

这是我正在谈论的示例:顶行是一个框选择器,在场景中包含虚拟灰度,边界矩形为 (0, 0, 2pi, 4pi)。绿色框是用户绘制的 QGraphicsRectItem,在单击 "Done" 后,我从中获取 LL 和 UR 点(在场景坐标中)。底行是带有用户单击椭圆的点布局,位于放大 20 倍的小虚拟图像上。

左边和右边是用相同的代码制作的。左边的版本是在Anaconda 2.5 Python 3.5下使用PyQt4的结果,而右边的是在Anaconda 4.3 Python 3.6.Python 3.6.

下使用PyQt5的结果

显然有某种项目转换的处理方式不同,但我无法在任何 PyQt4->PyQt5 文档中找到它(都是关于 API 更改)。

如何使 QGraphicsItem 的线宽在设备坐标中保持一致,同时仍保持场景坐标中的正确位置?更一般地说,我如何缩放一般 QGraphicsItem,使其不会根据场景大小膨胀或变胖?

代码如下。 SimpleDialog 是我用于各种选择器实用程序的主要基础 class,它包括 MouseViewImageScene,它们会自动构建垂直翻转和背景图像。我在这里使用的两个实用程序是 BoxSelectorPointLayout.

# Imports
import numpy as np
try:
    from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
    from PyQt5.QtGui import QImage, QPixmap, QFont, QBrush, QPen, QTransform
    from PyQt5.QtWidgets import (QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
                                 QVBoxLayout, QPushButton, QMainWindow, QApplication)
except ImportError:
    from PyQt4.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
    from PyQt4.QtGui import (QImage, QPixmap, QFont, QBrush, QPen, QTransform,
                             QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
                             QVBoxLayout, QPushButton, QMainWindow, QApplication)

class MouseView(QGraphicsView):
    """A subclass of QGraphicsView that returns mouse click events."""

    mousedown = pyqtSignal(QPointF)
    mouseup = pyqtSignal(QPointF)
    mousemove = pyqtSignal(QPointF)

    def __init__(self, scene, parent=None):
        super(MouseView, self).__init__(scene, parent=parent)

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.Fixed)
        self.scale(1, -1)

        self.moving = False

    def mousePressEvent(self, event):
        """Emit a mouse click signal."""
        self.mousedown.emit(self.mapToScene(event.pos()))

    def mouseReleaseEvent(self, event):
        """Emit a mouse release signal."""
        self.mouseup.emit(self.mapToScene(event.pos()))

    def mouseMoveEvent(self, event):
        """Emit a mouse move signal."""
        if self.moving:
            self.mousemove.emit(self.mapToScene(event.pos()))


class ImageScene(QGraphicsScene):
    """A subclass of QGraphicsScene that includes a background pixmap."""

    def __init__(self, data, scene_rect, parent=None):
        super(ImageScene, self).__init__(parent=parent)

        bdata = ((data - np.min(data)) / (np.max(data) - np.min(data)) * 255).astype(np.uint8)
        wid, hgt = data.shape

        img = QImage(bdata.T.copy(), wid, hgt, wid, QImage.Format_Indexed8)

        self.setSceneRect(*scene_rect)

        px = QPixmap.fromImage(img)

        self.px = self.addPixmap(px)

        px_trans = QTransform.fromTranslate(scene_rect[0], scene_rect[1])
        px_trans = px_trans.scale(scene_rect[2]/wid, scene_rect[3]/hgt)
        self.px.setTransform(px_trans)

class SimpleDialog(QDialog):
    """A base class for utility dialogs using a background image in scene."""

    def __init__(self, data, bounds=None, grow=[1.0, 1.0], wsize=None, parent=None):
        super(SimpleDialog, self).__init__(parent=parent)

        self.grow = grow

        wid, hgt = data.shape
        if bounds is None:
            bounds = [0, 0, wid, hgt]

        if wsize is None:
            wsize = [wid, hgt]

        vscale = [grow[0]*wsize[0]/bounds[2], grow[1]*wsize[1]/bounds[3]]

        self.scene = ImageScene(data, bounds, parent=self)

        self.view = MouseView(self.scene, parent=self)
        self.view.scale(vscale[0], vscale[1])

        quitb = QPushButton("Done")
        quitb.clicked.connect(self.close)

        lay = QVBoxLayout()
        lay.addWidget(self.view)
        lay.addWidget(quitb)

        self.setLayout(lay)

    def close(self):
        self.accept()

class BoxSelector(SimpleDialog):
    """Simple box selector."""

    def __init__(self, *args, **kwargs):
        super(BoxSelector, self).__init__(*args, **kwargs)

        self.rpen = QPen(Qt.green)
        self.rect = self.scene.addRect(0, 0, 0, 0, pen=self.rpen)

        self.view.mousedown.connect(self.start_box)
        self.view.mouseup.connect(self.end_box)
        self.view.mousemove.connect(self.draw_box)

        self.start_point = []
        self.points = []

        self.setWindowTitle('Box Selector')

    @pyqtSlot(QPointF)
    def start_box(self, xy):
        self.start_point = [xy.x(), xy.y()]
        self.view.moving = True

    @pyqtSlot(QPointF)
    def end_box(self, xy):
        lx = np.minimum(xy.x(), self.start_point[0])
        ly = np.minimum(xy.y(), self.start_point[1])
        rx = np.maximum(xy.x(), self.start_point[0])
        ry = np.maximum(xy.y(), self.start_point[1])
        self.points = [[lx, ly], [rx, ry]]
        self.view.moving = False

    @pyqtSlot(QPointF)
    def draw_box(self, xy):
        newpoint = [xy.x(), xy.y()]
        minx = np.minimum(self.start_point[0], newpoint[0])
        miny = np.minimum(self.start_point[1], newpoint[1])
        size = [np.abs(i - j) for i, j in zip(self.start_point, newpoint)]
        self.rect.setRect(minx, miny, size[0], size[1])

class PointLayout(SimpleDialog):
    """Simple point display."""

    def __init__(self, *args, **kwargs):
        super(PointLayout, self).__init__(*args, **kwargs)

        self.pen = QPen(Qt.green)

        self.view.mousedown.connect(self.mouse_click)

        self.circles = []
        self.points = []

        self.setWindowTitle('Point Layout')

    @pyqtSlot(QPointF)
    def mouse_click(self, xy):
        self.points.append((xy.x(), xy.y()))
        pt = self.scene.addEllipse(xy.x()-0.5, xy.y()-0.5, 1, 1, pen=self.pen)
        self.circles.append(pt)

这是我用来做测试的代码:

def test_box():

    x, y = np.mgrid[0:175, 0:100]
    img = x * y

    app = QApplication.instance()
    if app is None:
        app = QApplication(['python'])

    picker = BoxSelector(img, bounds=[0, 0, 2*np.pi, 4*np.pi])
    picker.show()
    app.exec_()

    return picker

def test_point():

    np.random.seed(159753)
    img = np.random.randn(10, 5)

    app = QApplication.instance()
    if app is None:
        app = QApplication(['python'])

    pointer = PointLayout(img, bounds=[0, 0, 10, 5], grow=[20, 20])
    pointer.show()

    app.exec_()

    return pointer

if __name__ == "__main__":
    pick = test_box()
    point = test_point()

我发现将笔宽明确设置为零可以恢复以前的行为:

class BoxSelector(SimpleDialog):
    def __init__(self, *args, **kwargs):
        ...    
        self.rpen = QPen(Qt.green)
        self.rpen.setWidth(0)
        ...

class PointLayout(SimpleDialog):    
    def __init__(self, *args, **kwargs):
        ...
        self.pen = QPen(Qt.green)
        self.pen.setWidth(0)
        ...

Qt4里好像默认是0,Qt5里是1

来自 QPen.setWidth 的 Qt 文档:

A line width of zero indicates a cosmetic pen. This means that the pen width is always drawn one pixel wide, independent of the transformation set on the painter.