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,它包括 MouseView
和 ImageScene
,它们会自动构建垂直翻转和背景图像。我在这里使用的两个实用程序是 BoxSelector
和 PointLayout
.
# 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.
我有一些用 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,它包括 MouseView
和 ImageScene
,它们会自动构建垂直翻转和背景图像。我在这里使用的两个实用程序是 BoxSelector
和 PointLayout
.
# 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.