如何正确缩放 QGraphicsScene
How to properly scale GraphicsScene
我已经创建了一个小应用程序,我正在努力做到这一点,以便在调整主要 window 大小时(GraphicsView 和场景也是如此)整个场景(像素图和矩形)垂直缩放以完全适合 GraphicsView。我不想要垂直滚动条,也不希望它水平缩放。
我不知道如何正确缩放场景。我使用 GraphicsScene 来包含一个图形和一对垂直矩形 "markers"。当我可以通过重新绘制像素图然后重新附加它来缩放图形以适合时,z 顺序是错误的并且矩形小部件没有随之缩放。
我需要跟踪矩形小部件,所以我不能一直删除并重新添加它们,因为每个小部件都有元数据。
我知道适用于包含 GraphicsView 的 fitInView(来自此处:Issue with fitInView of QGraphicsView when ItemIgnoresTransformations is on),但我不明白为什么它需要一个参数。我只希望场景适合 GraphicsView(垂直但不是水平)那么为什么 GraphicsView 不缩放场景中的所有内容以适合其当前大小?使场景垂直适合的参数应该是什么样的?
在 resizeEvent 中,我可以重新绘制像素图并重新添加,但随后它会覆盖矩形,因为 z 顺序被打乱了。此外,它不会在场景中保持垂直居中,我需要复制元数据。
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
import PyQt5 as qt
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QGroupBox, QDialog, QVBoxLayout
from PyQt5.QtWidgets import QVBoxLayout, QGridLayout, QStackedWidget, QTabWidget
import numpy as np
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
def minimumSizeHint(self):
return QtCore.QSize(300, 200)
def dragMoveEvent(self, event):
print("dragMoveEvent", event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
#super(MainWindow).__init__()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
max_x, max_y = 2400, 700
max_x_view = 1200
self.max_x = max_x
self.max_y = max_y
self.first = True
self.setGeometry(200, 200, max_x_view, self.max_y)
self.gv = QtWidgets.QGraphicsView(self)
self.gv.setGeometry(0, 0, max_x_view, self.max_y)
self.gv2 = QtWidgets.QGraphicsView(self)
layout.addWidget(self.gv)
layout.addWidget(self.gv2)
scene = GraphicsScene()
self.scene = scene
self.gv.setScene(scene)
tab_widget = QTabWidget()
tab_widget.setTabPosition(QTabWidget.West)
widget = QWidget()
widget.setLayout(layout)
tab_widget.addTab(widget, "main")
self.setCentralWidget(tab_widget)
np.random.seed(777)
self.x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
self.data = .45*(np.sin(2*self.x_time) + rand_data) - .25*(np.sin(3*self.x_time))
self.first = True
pixmap_height = max_y//2 - 2*22 # 22 to take care of scrollbar height
pixmap = self.draw_graph()
pen = QtGui.QPen()
pen.setWidth(2)
pen.setColor(QtGui.QColor("red"))
self.gv1_pixmap = scene.addPixmap(pixmap)
rect = scene.sceneRect()
print("scene rect = {}".format(rect))
scene.setSceneRect(rect)
side, offset = 50, 200
for i in range(2):
r = QtCore.QRectF(QtCore.QPointF((i + 1)*offset + i * 2 * side, 2), QtCore.QSizeF(side, pixmap_height - 4))
rect_ref = scene.addRect(r, pen, QColor(255, 0, 0, 127))
rect_ref.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
all_items = scene.items()
print(all_items)
def draw_graph(self):
print("draw_graph: main Window size {}:".format(self.size()))
pixmap_height = self.height()//2 - 2*22 # 22 to take care of scrollbar height
x_final = self.x_time[-1]
data = self.data / np.max(np.abs(self.data))
data = [abs(int(k * pixmap_height)) for k in self.data]
x_pos = [int(self.x_time[i] * self.max_x / x_final) for i in range(len(data))]
pixmap = QtGui.QPixmap(self.max_x, pixmap_height)
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen()
pen.setWidth(2)
rect = pixmap.rect()
pen.setColor(QtGui.QColor("red"))
painter.drawRect(rect)
print("pixmap rect = {}".format(rect))
painter.fillRect(rect, QtGui.QColor('lightblue'))
pen.setWidth(2)
pen.setColor(QtGui.QColor("green"))
painter.setPen(pen)
for x, y in zip(x_pos, data):
painter.drawLine(x, pixmap_height, x, pixmap_height - y)
painter.end()
return pixmap
def resizeEvent(self, a0: QtGui.QResizeEvent):
#print("main Window resizeEvent")
print("main Window size {}:".format(a0.size()))
redraw = False
if redraw:
pixmap = self.draw_graph()
self.scene.removeItem(self.gv1_pixmap)
self.gv1_pixmap = self.scene.addPixmap(pixmap)
self.gv1_pixmap.moveBy(0, 30)
else:
#rect = QtCore.QRect(self.gv.startPos, self.gv.endPos)
#sceneRect = self.gv.mapToScene(rect).boundingRect()
#print 'Selected area: viewport coordinate:', rect,', scene coordinate:', sceneRect
#self.gv.fitInView(sceneRect)
pass
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
我的解决方案将使封装所有项目 (sceneRect) 的最小矩形的高度适合 QGraphicsView 的视口。因此,将项目的高度设置为一个不太小的值,这样图像质量就不会丢失。我还使用 QTransforms 缩放了项目。另外,QGraphicsView的坐标系统是倒置的,因为默认情况下垂直轴是上下的,我把它倒置了,这样绘画与数据更一致。
我重构了 OP 代码以使其更具可扩展性,有一个 GraphItem 获取数据 (x, y) 和图像尺寸。
综合以上,解决方案是:
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphItem(QtWidgets.QGraphicsPixmapItem):
def __init__(self, xdata, ydata, width, height, parent=None):
super(GraphItem, self).__init__(parent)
self._xdata = xdata
self._ydata = ydata
self._size = QtCore.QSize(width, height)
self.redraw()
def redraw(self):
x_final = self._xdata[-1]
pixmap = QtGui.QPixmap(self._size)
pixmap_height = pixmap.height()
pixmap.fill(QtGui.QColor("lightblue"))
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen(QtGui.QColor("green"))
pen.setWidth(2)
painter.setPen(pen)
for i, (x, y) in enumerate(
zip(self._xdata, self._ydata / np.max(np.abs(self._ydata)))
):
x_pos = int(x * self._size.width() / x_final)
y_pos = abs(int(y * pixmap_height))
painter.drawLine(x_pos, 0, x_pos, y_pos)
painter.end()
self.setPixmap(pixmap)
class HorizontalRectItem(QtWidgets.QGraphicsRectItem):
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.scene():
newPos = self.pos()
newPos.setX(value.x())
return newPos
return super(HorizontalRectItem, self).itemChange(change, value)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self.scale(1, -1)
def resizeEvent(self, event):
h = self.mapToScene(self.viewport().rect()).boundingRect().height()
r = self.sceneRect()
r.setHeight(h)
self.setSceneRect(r)
height = self.viewport().height()
for item in self.items():
item_height = item.boundingRect().height()
tr = QtGui.QTransform()
tr.scale(1, height / item_height)
item.setTransform(tr)
super(GraphicsView, self).resizeEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
tab_widget = QtWidgets.QTabWidget(tabPosition=QtWidgets.QTabWidget.West)
self.setCentralWidget(tab_widget)
self.graphics_view_top = GraphicsView()
self.graphics_view_bottom = QtWidgets.QGraphicsView()
container = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(container)
lay.addWidget(self.graphics_view_top)
lay.addWidget(self.graphics_view_bottom)
tab_widget.addTab(container, "main")
self.resize(640, 480)
side, offset, height = 50, 200, 400
np.random.seed(777)
x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
data = 0.45 * (np.sin(2 * x_time) + rand_data) - 0.25 * (np.sin(3 * x_time))
graph_item = GraphItem(x_time, data, 3000, height)
self.graphics_view_top.scene().addItem(graph_item)
for i in range(2):
r = QtCore.QRectF(
QtCore.QPointF((i + 1) * offset + i * 2 * side, 2),
QtCore.QSizeF(side, height),
)
it = HorizontalRectItem(r)
it.setPen(QtGui.QPen(QtGui.QColor("red"), 2))
it.setBrush(QtGui.QColor(255, 0, 0, 127))
self.graphics_view_top.scene().addItem(it)
it.setFlags(
it.flags()
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
我已经创建了一个小应用程序,我正在努力做到这一点,以便在调整主要 window 大小时(GraphicsView 和场景也是如此)整个场景(像素图和矩形)垂直缩放以完全适合 GraphicsView。我不想要垂直滚动条,也不希望它水平缩放。
我不知道如何正确缩放场景。我使用 GraphicsScene 来包含一个图形和一对垂直矩形 "markers"。当我可以通过重新绘制像素图然后重新附加它来缩放图形以适合时,z 顺序是错误的并且矩形小部件没有随之缩放。
我需要跟踪矩形小部件,所以我不能一直删除并重新添加它们,因为每个小部件都有元数据。
我知道适用于包含 GraphicsView 的 fitInView(来自此处:Issue with fitInView of QGraphicsView when ItemIgnoresTransformations is on),但我不明白为什么它需要一个参数。我只希望场景适合 GraphicsView(垂直但不是水平)那么为什么 GraphicsView 不缩放场景中的所有内容以适合其当前大小?使场景垂直适合的参数应该是什么样的?
在 resizeEvent 中,我可以重新绘制像素图并重新添加,但随后它会覆盖矩形,因为 z 顺序被打乱了。此外,它不会在场景中保持垂直居中,我需要复制元数据。
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
import PyQt5 as qt
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QGroupBox, QDialog, QVBoxLayout
from PyQt5.QtWidgets import QVBoxLayout, QGridLayout, QStackedWidget, QTabWidget
import numpy as np
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
def minimumSizeHint(self):
return QtCore.QSize(300, 200)
def dragMoveEvent(self, event):
print("dragMoveEvent", event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
#super(MainWindow).__init__()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
max_x, max_y = 2400, 700
max_x_view = 1200
self.max_x = max_x
self.max_y = max_y
self.first = True
self.setGeometry(200, 200, max_x_view, self.max_y)
self.gv = QtWidgets.QGraphicsView(self)
self.gv.setGeometry(0, 0, max_x_view, self.max_y)
self.gv2 = QtWidgets.QGraphicsView(self)
layout.addWidget(self.gv)
layout.addWidget(self.gv2)
scene = GraphicsScene()
self.scene = scene
self.gv.setScene(scene)
tab_widget = QTabWidget()
tab_widget.setTabPosition(QTabWidget.West)
widget = QWidget()
widget.setLayout(layout)
tab_widget.addTab(widget, "main")
self.setCentralWidget(tab_widget)
np.random.seed(777)
self.x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
self.data = .45*(np.sin(2*self.x_time) + rand_data) - .25*(np.sin(3*self.x_time))
self.first = True
pixmap_height = max_y//2 - 2*22 # 22 to take care of scrollbar height
pixmap = self.draw_graph()
pen = QtGui.QPen()
pen.setWidth(2)
pen.setColor(QtGui.QColor("red"))
self.gv1_pixmap = scene.addPixmap(pixmap)
rect = scene.sceneRect()
print("scene rect = {}".format(rect))
scene.setSceneRect(rect)
side, offset = 50, 200
for i in range(2):
r = QtCore.QRectF(QtCore.QPointF((i + 1)*offset + i * 2 * side, 2), QtCore.QSizeF(side, pixmap_height - 4))
rect_ref = scene.addRect(r, pen, QColor(255, 0, 0, 127))
rect_ref.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
all_items = scene.items()
print(all_items)
def draw_graph(self):
print("draw_graph: main Window size {}:".format(self.size()))
pixmap_height = self.height()//2 - 2*22 # 22 to take care of scrollbar height
x_final = self.x_time[-1]
data = self.data / np.max(np.abs(self.data))
data = [abs(int(k * pixmap_height)) for k in self.data]
x_pos = [int(self.x_time[i] * self.max_x / x_final) for i in range(len(data))]
pixmap = QtGui.QPixmap(self.max_x, pixmap_height)
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen()
pen.setWidth(2)
rect = pixmap.rect()
pen.setColor(QtGui.QColor("red"))
painter.drawRect(rect)
print("pixmap rect = {}".format(rect))
painter.fillRect(rect, QtGui.QColor('lightblue'))
pen.setWidth(2)
pen.setColor(QtGui.QColor("green"))
painter.setPen(pen)
for x, y in zip(x_pos, data):
painter.drawLine(x, pixmap_height, x, pixmap_height - y)
painter.end()
return pixmap
def resizeEvent(self, a0: QtGui.QResizeEvent):
#print("main Window resizeEvent")
print("main Window size {}:".format(a0.size()))
redraw = False
if redraw:
pixmap = self.draw_graph()
self.scene.removeItem(self.gv1_pixmap)
self.gv1_pixmap = self.scene.addPixmap(pixmap)
self.gv1_pixmap.moveBy(0, 30)
else:
#rect = QtCore.QRect(self.gv.startPos, self.gv.endPos)
#sceneRect = self.gv.mapToScene(rect).boundingRect()
#print 'Selected area: viewport coordinate:', rect,', scene coordinate:', sceneRect
#self.gv.fitInView(sceneRect)
pass
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
我的解决方案将使封装所有项目 (sceneRect) 的最小矩形的高度适合 QGraphicsView 的视口。因此,将项目的高度设置为一个不太小的值,这样图像质量就不会丢失。我还使用 QTransforms 缩放了项目。另外,QGraphicsView的坐标系统是倒置的,因为默认情况下垂直轴是上下的,我把它倒置了,这样绘画与数据更一致。
我重构了 OP 代码以使其更具可扩展性,有一个 GraphItem 获取数据 (x, y) 和图像尺寸。
综合以上,解决方案是:
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphItem(QtWidgets.QGraphicsPixmapItem):
def __init__(self, xdata, ydata, width, height, parent=None):
super(GraphItem, self).__init__(parent)
self._xdata = xdata
self._ydata = ydata
self._size = QtCore.QSize(width, height)
self.redraw()
def redraw(self):
x_final = self._xdata[-1]
pixmap = QtGui.QPixmap(self._size)
pixmap_height = pixmap.height()
pixmap.fill(QtGui.QColor("lightblue"))
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen(QtGui.QColor("green"))
pen.setWidth(2)
painter.setPen(pen)
for i, (x, y) in enumerate(
zip(self._xdata, self._ydata / np.max(np.abs(self._ydata)))
):
x_pos = int(x * self._size.width() / x_final)
y_pos = abs(int(y * pixmap_height))
painter.drawLine(x_pos, 0, x_pos, y_pos)
painter.end()
self.setPixmap(pixmap)
class HorizontalRectItem(QtWidgets.QGraphicsRectItem):
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.scene():
newPos = self.pos()
newPos.setX(value.x())
return newPos
return super(HorizontalRectItem, self).itemChange(change, value)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self.scale(1, -1)
def resizeEvent(self, event):
h = self.mapToScene(self.viewport().rect()).boundingRect().height()
r = self.sceneRect()
r.setHeight(h)
self.setSceneRect(r)
height = self.viewport().height()
for item in self.items():
item_height = item.boundingRect().height()
tr = QtGui.QTransform()
tr.scale(1, height / item_height)
item.setTransform(tr)
super(GraphicsView, self).resizeEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
tab_widget = QtWidgets.QTabWidget(tabPosition=QtWidgets.QTabWidget.West)
self.setCentralWidget(tab_widget)
self.graphics_view_top = GraphicsView()
self.graphics_view_bottom = QtWidgets.QGraphicsView()
container = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(container)
lay.addWidget(self.graphics_view_top)
lay.addWidget(self.graphics_view_bottom)
tab_widget.addTab(container, "main")
self.resize(640, 480)
side, offset, height = 50, 200, 400
np.random.seed(777)
x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
data = 0.45 * (np.sin(2 * x_time) + rand_data) - 0.25 * (np.sin(3 * x_time))
graph_item = GraphItem(x_time, data, 3000, height)
self.graphics_view_top.scene().addItem(graph_item)
for i in range(2):
r = QtCore.QRectF(
QtCore.QPointF((i + 1) * offset + i * 2 * side, 2),
QtCore.QSizeF(side, height),
)
it = HorizontalRectItem(r)
it.setPen(QtGui.QPen(QtGui.QColor("red"), 2))
it.setBrush(QtGui.QColor(255, 0, 0, 127))
self.graphics_view_top.scene().addItem(it)
it.setFlags(
it.flags()
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()