PyQt5设置QGraphicsView视口为QOpenGLWidget时的线条渲染异常问题
PyQt5 QGraphicsPathItem 搭配OpenGL视口渲染异常问题
场景
我开发了一个PyQt5应用,用于展示线条、文本和圆形,原本显示正常,但文本渲染速度较慢,所有功能基于自定义QGraphicsView类实现。
问题
为QGraphicsView设置OpenGL视口后,文本和圆形渲染速度大幅提升且显示正常,但QGraphicsPathItem绘制的线条出现异常,配置代码如下:
self.gl_widget = QOpenGLWidget() format = QSurfaceFormat() format.setProfile(QSurfaceFormat.CoreProfile) self.gl_widget.setFormat(format) self.setViewport(self.gl_widget)
具体异常表现:
- 线条宽度随机变化,并非设置的固定值
- 缩放操作时,部分线条随机消失或出现
- 仅
QGraphicsPathItem受影响,QGraphicsLineItem无异常 - 增大线条宽度可缓解但无法彻底解决问题
最小复现代码
import sys from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsTextItem from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtWidgets import QOpenGLWidget import numpy as np from PyQt5.QtGui import QPainterPath, QPen from PyQt5.QtWidgets import QGraphicsPathItem, QGraphicsLineItem, QGraphicsPolygonItem from PyQt5.QtGui import QPolygonF from PyQt5.QtCore import QLineF, QPointF from PyQt5.QtGui import QSurfaceFormat class GraphicsView(QtWidgets.QGraphicsView): def __init__(self): super(GraphicsView, self).__init__() self.pos_init_class = None self.scale_factor = 1.5 self.setFrameShape(QtWidgets.QFrame.VLine) self.setMouseTracking(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) # 渲染优化设置 self.setRenderHint(QtGui.QPainter.Antialiasing, False) self.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, False) self.setRenderHint(QtGui.QPainter.TextAntialiasing, False) self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, False) self.setRenderHint(QtGui.QPainter.NonCosmeticDefaultPen, True) self.setOptimizationFlag(QtWidgets.QGraphicsView.DontAdjustForAntialiasing, True) self.setOptimizationFlag(QtWidgets.QGraphicsView.DontSavePainterState, True) self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) # OpenGL视口配置 self.gl_widget = QOpenGLWidget() format = QSurfaceFormat() format.setVersion(2, 8) format.setProfile(QSurfaceFormat.CoreProfile) self.gl_widget.setFormat(format) self.setViewport(self.gl_widget) def mousePressEvent(self, event): pos = self.mapToScene(event.pos()) if event.button() == QtCore.Qt.MiddleButton: self.pos_init_class = pos QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor) super(GraphicsView, self).mousePressEvent(event) def mouseReleaseEvent(self, event): if self.pos_init_class and event.button() == QtCore.Qt.MiddleButton: self.pos_init_class = None QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor) super(GraphicsView, self).mouseReleaseEvent(event) def mouseMoveEvent(self, event): 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)) super(GraphicsView, self).mouseMoveEvent(event) def wheelEvent(self, event): old_pos = self.mapToScene(event.pos()) zoom_factor = self.scale_factor if event.angleDelta().y() > 0 else 1 / self.scale_factor transform = QtGui.QTransform() transform.translate(old_pos.x(), old_pos.y()) transform.scale(zoom_factor, zoom_factor) transform.translate(-old_pos.x(), -old_pos.y()) current_transform = self.transform() self.setTransform(transform * current_transform) def zoom_extent(self): x_range, y_range, h_range, w_range = self.scene().itemsBoundingRect().getRect() rect = QtCore.QRectF(x_range, y_range, h_range, w_range) self.setSceneRect(rect) unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height()) self.scale(factor, factor) class MainWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.view = GraphicsView() self.scene = QGraphicsScene() self.view.setScene(self.scene) self.generate_random_lines() self.setCentralWidget(self.view) self.showMaximized() self.view.zoom_extent() def rotate_vector(self, origin, point, angle): 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 create_line_with_arrow_path(self, x1, y1, x2, y2, arr_width, arr_len): path = QPainterPath() path.moveTo(x1, y1) path.lineTo(x2, y2) mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 arrow_x = np.array([arr_width, -arr_len, -arr_width, -arr_len, arr_width]) * 5 arrow_y = np.array([0, arr_width, 0, -arr_width, 0]) * 5 arrow_x += mid_x arrow_y += mid_y angle = np.rad2deg(np.arctan2(y2 - y1, x2 - x1)) origin = (np.array([mid_x]*5), np.array([mid_y]*5)) point = (arrow_x, arrow_y) self.x_init, self.y_init = self.rotate_vector(origin, point, np.deg2rad(angle)) arrow_path = QtGui.QPainterPath() arrow_path.moveTo(self.x_init[0], self.y_init[0]) for i in range(1, len(arrow_x)): arrow_path.lineTo(self.x_init[i], self.y_init[i]) path.addPath(arrow_path) item = QGraphicsPathItem(path) pen = QPen() pen.setWidthF(0.1) item.setPen(pen) return item, angle def create_line_with_arrow_item(self, x1, y1, x2, y2, arr_width, arr_len): mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 arrow_x = np.array([arr_width, -arr_len, -arr_width, -arr_len, arr_width]) * 10 arrow_y = np.array([0, arr_width, 0, -arr_width, 0]) * 10 arrow_x += mid_x arrow_y += mid_y angle = np.rad2deg(np.arctan2(y2 - y1, x2 - x1)) origin = (np.array([mid_x]*5), np.array([mid_y]*5)) point = (arrow_x, arrow_y) x_init, y_init = self.rotate_vector(origin, point, np.deg2rad(angle)) line = QLineF(x1, y1, x2, y2) arrow = QPolygonF([QPointF(x_init[0], y_init[0]), QPointF(x_init[1], y_init[1]), QPointF(x_init[2], y_init[2]), QPointF(x_init[3], y_init[3]), QPointF(x_init[4], y_init[4])]) item = QGraphicsLineItem(line) item_arrow = QGraphicsPolygonItem(arrow) pen = QPen() pen.setWidthF(1) item.setPen(pen) item_arrow.setPen(pen) return item, item_arrow, angle def generate_random_lines(self): case = 'issue' x = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) * 10 y = np.array([0, 20, 10, 0, 35, 90, 10, 60, 60, 90, 100]) * 10 for pos, i in enumerate(range(len(x) - 1)): x1 = x[i] y1 = y[i] x2 = x[i + 1] y2 = y[i + 1] if case in ['issue']: path, angle = self.create_line_with_arrow_path(x1, y1, x2, y2, 0.5, 1.5) self.scene.addItem(path) text1 = QGraphicsTextItem() text1.setPlainText(str(pos)) text1.setPos(x1, y1) text1.setRotation(angle) self.scene.addItem(text1) else: line, arrow, angle = self.create_line_with_arrow_item(x1, y1, x2, y2, 0.5, 1.5) self.scene.addItem(line) self.scene.addItem(arrow) text1 = QGraphicsTextItem() text1.setPlainText(str(pos)) text1.setPos(x1, y1) text1.setRotation(angle) self.scene.addItem(text1) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())
效果对比
- 异常效果:

- 正常效果:

问题原因
- Core Profile兼容性局限:OpenGL Core Profile对旧渲染特性支持有限,Qt的QPainter在该模式下处理非cosmetic细线条时,容易因精度丢失出现渲染异常。
- 路径渲染精度问题:
QGraphicsPathItem的路径渲染逻辑在OpenGL视口下,对极小宽度线条(如代码中0.1)的变换容错性不如QGraphicsLineItem,缩放时浮点计算误差会放大问题。
解决方案
方案1:切换到OpenGL兼容性配置
将Core Profile改为Compatibility Profile,兼容旧OpenGL特性,直接解决细线条渲染问题:
self.gl_widget = QOpenGLWidget() format = QSurfaceFormat() # 替换为兼容性配置 format.setProfile(QSurfaceFormat.CompatibilityProfile) self.gl_widget.setFormat(format) self.setViewport(self.gl_widget)
方案2:使用Cosmetic Pen
将笔设置为cosmetic类型,确保线条宽度不受视图变换影响,保持固定像素宽度:
pen = QPen() pen.setWidthF(0.1) # 开启cosmetic模式 pen.setCosmetic(True) item.setPen(pen)
注意:cosmetic pen的宽度是像素单位,不会随视图缩放变化。若需线条宽度随场景缩放,可结合当前视图缩放比例动态调整宽度值。
方案3:优化路径与渲染设置
- 简化
QPainterPath,减少冗余点降低渲染误差:
path.addPath(arrow_path) # 简化路径 path = path.simplified() item = QGraphicsPathItem(path)
- 关闭
NonCosmeticDefaultPen渲染提示,避免强制非cosmetic笔的渲染逻辑:
self.setRenderHint(QtGui.QPainter.NonCosmeticDefaultPen, False)
验证建议
优先尝试方案1+方案2的组合,既能保留OpenGL的性能提升,又能彻底解决线条异常问题。若需线条随场景缩放,可使用方案1+动态计算笔宽度的方式实现。
内容的提问来源于stack exchange,提问作者MBV
相关产品推荐
相关产品推荐

