我有一个构成网络的数据集(m_ramales
泡菜文件),这是在一个具有交互属性(例如可移动节点和选择选项)的QGraphicsView
中绘制的。该GUI需要处理约3000个节点,每个边缘都需要显示一个描述数据文本,如示例所示。
有两个问题:
QGraphicsView
场景中显示旋转的文本时,平移和缩放变得非常慢。QGraphicsView
类中将setViewport
设置为QtOpenGL.QGLWidget()
,则问题编号1是固定的,但是绘制的几十年的质量很大,并且在从原始位置移动节点时出现惯性错误。我需要获得具有可接受的绘图质量并且在用户平移或缩放时具有可接受的性能的QGraphicsView
。这可能吗?
示例代码如下,请在与示例脚本相同的文件夹中复制pickle文件。
数据例如:https://drive.google.com/file/d/1k0wZ5Logfu7pfyDJmJyF0VZvOne94H7Y/view?usp=sharing
import sys
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets, QtOpenGL
import pickle
def rotate_vector(origin, point, angle):
"""
Rotate a point counterclockwise by a given angle around a given origin.
The angle should be given in radians.
"""
ox, oy = origin
px, py = point
qx = ox + np.cos(angle) * (px - ox) - np.sin(angle) * (py - oy)
qy = oy + np.sin(angle) * (px - ox) + np.cos(angle) * (py - oy)
return qx, qy
def ang_vector(x_v, y_v):
y_v, x_v = np.asarray(y_v), np.asarray(x_v)
return (np.degrees(np.arctan(np.true_divide(y_v[1:] - y_v[:-1], x_v[1:] - x_v[:-1]))), list(zip(np.sign(x_v[1:] - x_v[:-1]), np.sign(y_v[1:] - y_v[:-1]))))
dict_pos = {(1, 1): [-1, 1], (-1, 1): [-1, 1], (-1, -1): [-1, -1], (1, -1): [-1, -1]}
font_family = 'Arial Unicode MS'
dat_Font = QtGui.QFont("Arial", 1.9, QtGui.QFont.Normal)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, scene, parent):
super(GraphicsView, self).__init__(scene, parent)
"VARIABLES INICIALES"
self.pos_init_class = None
"ASIGNAR LINEAS DE MARCO"
self.setFrameShape(QtWidgets.QFrame.VLine)
"ACTIVAR TRACKING DE POSICION DE MOUSE"
self.setMouseTracking(True)
"REMOVER BARRAS DE SCROLL"
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
"ASIGNAR ANCLA PARA HACER ZOOM SOBRE EL MISMO PUNTO"
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
"MEJORAR EL RENDER DE VECTORES"
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setOptimizationFlags(QtWidgets.QGraphicsView.DontAdjustForAntialiasing or QtWidgets.QGraphicsView.DontClipPainteror or QtWidgets.QGraphicsView.DontSavePainterState or QtWidgets.QGraphicsView.DontAdjustForAntialiasing)
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
# self.setViewport(QtOpenGL.QGLWidget())
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
"Pan mouse cursor"
if event.button() == QtCore.Qt.MiddleButton:
self.pos_init_class = pos
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor)
super(GraphicsView, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
"Pan"
if self.pos_init_class:
delta = self.pos_init_class - self.mapToScene(event.pos())
r = self.mapToScene(self.viewport().rect()).boundingRect()
self.setSceneRect(r.translated(delta))
else:
super(GraphicsView, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"Pan"
self.pos_init_class = None
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
super(GraphicsView, self).mouseReleaseEvent(event)
def wheelEvent(self, event):
"ZOOM"
scale_factor = 1.25
if event.angleDelta().y() > 0:
self.scale(scale_factor, scale_factor)
else:
self.scale(1 / scale_factor, 1 / scale_factor)
class Node(QtWidgets.QGraphicsEllipseItem):
r_pozo = 0.45
def __init__(self, arrow, hydraulic_text, ang, point,rect=QtCore.QRectF(-r_pozo, -r_pozo, 2 * r_pozo, 2 * r_pozo), parent=None):
super(Node, self).__init__(rect, parent)
"arrow"
self.arrow = arrow
"hydraulic text"
self.hydraulic_text = hydraulic_text
"angle"
self._angle = ang
"point"
self.origin = (np.array([0, 0, 0]), np.array([0, 0, 0]))
self.point = point
"edges"
self.edges = []
self.setZValue(1)
self.setBrush(QtCore.Qt.darkGray)
self.setPen(QtGui.QPen(QtGui.QColor(51, 153, 255), 0.1))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
def addEdge(self, edge):
self.edges.append(edge)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
self.setBrush(QtCore.Qt.green if value else QtCore.Qt.darkGray)
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
for edge in self.edges:
"angulo de linea antes de ajustar"
x1, y1 = edge.dest.x(), -edge.dest.y()
x2, y2 = edge.source.x(), -edge.source.y()
ang1 = np.rad2deg(np.arctan2(y2 - y1, x2 - x1))
"diferencia de angulo actual con anterior"
ang = ang1 - self._angle[edge.toolTip()]
"actualizar angulo anterior"
self._angle[edge.toolTip()] = ang1
"Ajustar linea"
edge.adjust()
"Arrow position"
x_arrow = (edge.source.x() + edge.dest.x()) / 2.0
y_arrow = (edge.source.y() + edge.dest.y()) / 2.0
self.arrow[edge.toolTip()].setPos(x_arrow, y_arrow)
"Arrow rotation"
x_rot, y_rot = rotate_vector(self.origin, self.point[edge.toolTip()], np.deg2rad(-ang))
self.arrow[edge.toolTip()].elementPath.setElementPositionAt(0, -x_rot[0], -y_rot[0])
self.arrow[edge.toolTip()].elementPath.setElementPositionAt(1, -x_rot[1], -y_rot[1])
self.arrow[edge.toolTip()].elementPath.setElementPositionAt(2, -x_rot[2], -y_rot[2])
"actualizar nueva posicion de PathItem"
self.point[edge.toolTip()] = (x_rot, y_rot)
"Hydraulic_text position"
angle_array, cuad_array = ang_vector(x_v=[edge.source.x(), edge.dest.x()],
y_v=[edge.source.y(), edge.dest.y()])
text_width = self.hydraulic_text[edge.toolTip()].boundingRect().width() * 0.5
dx, dy = text_width * np.cos(np.radians(abs(ang1))), text_width * np.sin(np.radians(abs(ang1)))
pos0, pos1 = dict_pos[cuad_array[0]]
dx, dy = dx * pos0, dy * pos1
"Hydraulic_text rotation"
if 90 < -ang1 < 180 or -90 > -ang1 > -180:
self.hydraulic_text[edge.toolTip()].setPos(x_arrow - dx, y_arrow - dy)
self.hydraulic_text[edge.toolTip()].setRotation(180 - ang1)
else:
self.hydraulic_text[edge.toolTip()].setPos(x_arrow + dx, y_arrow + dy)
self.hydraulic_text[edge.toolTip()].setRotation(-ang1)
return super(Node, self).itemChange(change, value)
class Edge(QtWidgets.QGraphicsLineItem):
def __init__(self, source, dest, parent=None):
QtWidgets.QGraphicsLineItem.__init__(self, parent)
self.source = source
self.dest = dest
self.source.addEdge(self)
self.dest.addEdge(self)
self.setPen(QtGui.QPen(QtCore.Qt.red, 0.2))
self.adjust()
def adjust(self):
self.prepareGeometryChange()
self.setLine(QtCore.QLineF(self.dest.pos(), self.source.pos()))
class GripItem_Arrow(QtWidgets.QGraphicsPathItem):
def __init__(self, angle):
super(GripItem_Arrow, self).__init__()
origin = (np.array([0, 0, 0]), np.array([0, 0, 0]))
point = (np.array([0.6, -0.6, -0.60]), np.array([0, 0.4, -0.4]))
self.x_init, self.y_init = rotate_vector(origin, point, np.deg2rad(-angle))
arrow = QtGui.QPainterPath()
arrow.moveTo(self.x_init[0], self.y_init[0])
arrow.lineTo(self.x_init[1], self.y_init[1])
arrow.lineTo(self.x_init[2], self.y_init[2])
self._arrow = arrow
self.setPath(self._arrow)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 0.))
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(2)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
@property
def elementPath(self):
return self._arrow
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
self.setPath(self._arrow)
return QtWidgets.QGraphicsPathItem.itemChange(self, change, value)
class Ui_MainWindow(object):
"TAMAÑOS ELEMENTOS"
def __init__(self):
"TAMAÑO MAINWINDOW"
self.main_width, self.main_height = QtWidgets.QDesktopWidget().screenGeometry(0).width(), QtWidgets.QDesktopWidget().availableGeometry().height()
"PLOT FUNCTIONS"
def plt_plot(self):
"LOAD DATA"
pkl_file = open('m_ramales.pkl', 'rb')
m_ramales = pickle.load(pkl_file)
pkl_file.close()
ramal = m_ramales.keys()
"CREAR DICCIONARIOS"
dict_node = {}
dict_arrow = {}
dict_edge = {}
dict_angle = {}
dict_point = {}
dict_hydraulic_text = {}
origin = (np.array([0, 0, 0]), np.array([0, 0, 0]))
point = (np.array([0.6, -0.6, -0.60]), np.array([0, 0.4, -0.4]))
for j in ramal:
x = m_ramales[j]['X']
y = m_ramales[j]['Y']
angle_array, cuad_array = ang_vector(x_v=x, y_v=y)
for i in range(0, len(x) - 1, 1):
if i == 0:
"coords"
x1, y1 = x[i], -y[i]
x2, y2 = x[i + 1], -y[i + 1]
"nombre Tramo"
str1 = m_ramales[j]['Tramo'][i+1]
"angulo"
dict_angle[str1] = np.rad2deg(np.arctan2(y2 - y1, x2 - x1))
"puntos iniciales"
dict_point[str1] = rotate_vector(origin, point, np.deg2rad(-dict_angle[str1]))
"flecha"
dict_arrow[str1] = GripItem_Arrow(angle=-dict_angle[str1])
"asignar posicion de flecha"
dict_arrow[str1].setPos((x1 + x2) / 2.0, (y1 + y2) / 2.0)
"asignar identificador de flecha"
dict_arrow[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_arrow[str1])
"agregar texto"
dict_hydraulic_text[str1] = QtWidgets.QGraphicsSimpleTextItem()
"definir tipo de texto, fuente y tamaño"
dict_hydraulic_text[str1].setFont(dat_Font)
"agregar contenido del texto"
text_dat = str1 + '\n' + 'D = 300 mm PVC'
dict_hydraulic_text[str1].setText(text_dat)
"ajustar posicion del texto"
text_width = dict_hydraulic_text[str1].boundingRect().width() * 0.5
dx, dy = text_width * np.cos(np.radians(abs(dict_angle[str1]))), text_width * np.sin(
np.radians(abs(dict_angle[str1])))
pos0, pos1 = dict_pos[cuad_array[i]][0], dict_pos[cuad_array[i]][1]
dx, dy = dx * pos0, dy * pos1
x_m, y_m = (x1 + x2) / 2.0, (y1 + y2) / 2.0
"asignar posicion y rotacion del texto"
dict_hydraulic_text[str1].setPos(x_m + dx, y_m + dy)
dict_hydraulic_text[str1].setRotation(dict_angle[str1])
"asignar nombre de tramo"
dict_hydraulic_text[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_hydraulic_text[str1])
"nombres de nodos"
node_str1 = 'Node.' + m_ramales[j]['Ramal'][0] + '.' + m_ramales[j]['Pozo'][i]
node_str2 = 'Node.' + m_ramales[j]['Ramal'][0] + '.' + m_ramales[j]['Pozo'][i + 1]
"crear nodos"
dict_node[node_str1] = Node(arrow=dict_arrow, hydraulic_text=dict_hydraulic_text, ang=dict_angle, point=dict_point)
dict_node[node_str2] = Node(arrow=dict_arrow, hydraulic_text=dict_hydraulic_text, ang=dict_angle, point=dict_point)
"asignar posicion de nodo"
dict_node[node_str1].setPos(x1, y1)
dict_node[node_str2].setPos(x2, y2)
"agregar nodo a escena"
self.graphicsView.scene().addItem(dict_node[node_str1])
self.graphicsView.scene().addItem(dict_node[node_str2])
"crear linea"
dict_edge[str1] = Edge(dict_node[node_str1], dict_node[node_str2])
"asignar nombre de tramo"
dict_edge[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_edge[str1])
else:
"coords"
x1, y1 = x[i], -y[i]
x2, y2 = x[i + 1], -y[i + 1]
"nombre Tramo"
str1 = m_ramales[j]['Tramo'][i+1]
"angulo"
dict_angle[str1] = np.rad2deg(np.arctan2(y2 - y1, x2 - x1))
"puntos iniciales"
dict_point[str1] = rotate_vector(origin, point, np.deg2rad(-dict_angle[str1]))
"flecha"
dict_arrow[str1] = GripItem_Arrow(angle=-dict_angle[str1])
"asignar posicion de flecha"
dict_arrow[str1].setPos((x1 + x2) / 2.0, (y1 + y2) / 2.0)
"asignar identificador de flecha"
dict_arrow[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_arrow[str1])
"agregar texto"
dict_hydraulic_text[str1] = QtWidgets.QGraphicsSimpleTextItem()
"definir tipo de texto, fuente y tamaño"
dict_hydraulic_text[str1].setFont(dat_Font)
"agregar contenido del texto"
text_dat = str1 + '\n' + 'D = 300 mm PVC'
dict_hydraulic_text[str1].setText(text_dat)
"ajustar posicion del texto"
text_width = dict_hydraulic_text[str1].boundingRect().width() * 0.5
dx, dy = text_width * np.cos(np.radians(abs(dict_angle[str1]))), text_width * np.sin(np.radians(abs(dict_angle[str1])))
pos0, pos1 = dict_pos[cuad_array[i]][0], dict_pos[cuad_array[i]][1]
dx, dy = dx * pos0, dy * pos1
x_m, y_m = (x1 + x2) / 2.0, (y1 + y2) / 2.0
"asignar posicion y rotacion del texto"
dict_hydraulic_text[str1].setPos(x_m + dx, y_m + dy)
dict_hydraulic_text[str1].setRotation(dict_angle[str1])
"asignar nombre de tramo"
dict_hydraulic_text[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_hydraulic_text[str1])
dict_hydraulic_text[str1].setCacheMode(QtWidgets.QGraphicsItem.NoCache)
"nombres de nodos"
node_str1 = 'Node.' + m_ramales[j]['Ramal'][0] + '.' + m_ramales[j]['Pozo'][i]
node_str2 = 'Node.' + m_ramales[j]['Ramal'][0] + '.' + m_ramales[j]['Pozo'][i + 1]
"crear nodos"
dict_node[node_str2] = Node(arrow=dict_arrow, hydraulic_text=dict_hydraulic_text, ang=dict_angle, point=dict_point)
"asignar posicion de nodo"
dict_node[node_str2].setPos(x2, y2)
"agregar nodo a escena"
self.graphicsView.scene().addItem(dict_node[node_str2])
"crear linea"
dict_edge[str1] = Edge(dict_node[node_str1], dict_node[node_str2])
"asignar nombre de tramo"
dict_edge[str1].setToolTip(str1)
"agregar a escena"
self.graphicsView.scene().addItem(dict_edge[str1])
def setupUi(self, MainWindow):
"DEFINIRI WIDGET DE MAIN WINDOW"
self.centralwidget = QtWidgets.QWidget(MainWindow)
MainWindow.setCentralWidget(self.centralwidget)
MainWindow.setWindowState(QtCore.Qt.WindowMaximized)
"SIZE MAIN WINDOW"
MainWindow.resize(self.main_width, self.main_height)
"DEFINIR GRAPHIC WIDGET"
self.scene = QtWidgets.QGraphicsScene()
self.graphicsView = GraphicsView(scene=self.scene, parent=self.centralwidget)
"POSICION Y TAMAÑO DE GRAPHIC WIDGET"
self.graphicsView.setGeometry(QtCore.QRect(0, 0, self.main_width, self.main_height))
"DIBUJAR PLOT"
self.plt_plot()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
QtWidgets.QApplication.setFont(QtGui.QFont(font_family, 10, QtGui.QFont.Normal))
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
此问题尚未解决,可以通过更改显示区域的文本项上的缓存模式来解决。
通过在场景的可观察区域中选择文本项并将其缓存模式从以下更改,可以在场景中具有数千个文本项来获得可接受的摇摄和缩放性能事件:
setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
至:
setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
。
这是在wheel事件结束并由QtCore.QTimer()触发后完成的 信号设置在150毫秒左右。
如果有帮助,这是所使用的代码片段。但是,如果我对PyQt5相当陌生,那么对改善此问题的任何帮助将不胜感激。
def wheelEvent(self, event):
"Change Cache Mode"
self.update_text = self.items(QtCore.QRect(0, 0, self.viewport().width(), self.viewport().height()))
scene_extent = self.mapToScene(QtCore.QRect(0, 0, self.viewport().width(), self.viewport().height())).boundingRect()
self.x_orig, self.y_orig, self.scene_width, self.scene_heigth = scene_extent.x(), scene_extent.y(), scene_extent.width(), scene_extent.height()
if self.scene_width > self.len_scene:
[i.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache) for i in self.update_text if i.type() == 9]
"ZOOM"
scale_factor = 1.25
if event.angleDelta().y() > 0:
self.scale(scale_factor, scale_factor)
else:
self.scale(1 / scale_factor, 1 / scale_factor)
"set emit signal QTimer"
self.t.timeout.connect(self.suspend_text)
self.t.start(150)
def suspend_text(self):
"Select text on the current scene area"
self.update_text = self.items(QtCore.QRect(0, 0, self.viewport().width(), self.viewport().height()))
"self.len_scene is the width of the scene neccesary to begin to see text and needs to be clear(No Cache)"
if self.scene_width < self.len_scene:
"type 9 is the type of the Text item"
[i.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) for i in self.update_text if i.type() == 9]
"Stop Timer signal"
self.t.stop()