PyQt5中自定义QGraphicsItem形状以优化斜线选中区域
问题:QGraphicsLineItem斜线点击误选问题及解决方法
环境信息
- PyQt5 5.15.7
- Python 3.10
- PyCharm 2022.1.3社区版(Windows 11)
问题场景
开发简易Logo编辑器,使用QGraphicsScene绘制线条,支持自定义长度、颜色、zValue等属性,通过鼠标点击QGraphicsItem修改属性。但绘制斜线时,QGraphicsItem默认的boundingRect范围过大,导致多条线条并存时,点击某条线会误选附近的斜线Item。
演示代码
from PyQt5.QtWidgets import ( QApplication, QWidget, QGraphicsView, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsItem, QTextEdit, ) from PyQt5.QtGui import QPolygonF, QPen, QTransform from PyQt5.QtCore import Qt, QPointF, QLineF import sys # Own QGraphicsScene Subclass with some drawings and points as example class MyScene( QGraphicsScene ): def __init__( self ): super().__init__( -300, -300, 600, 600 ) # Set QPen for drawings self.my_pen = QPen( Qt.darkBlue ) self.my_pen.setWidthF( 15 ) self.my_pen.setCapStyle( Qt.RoundCap ) # Set Start- & End-Points for my line self.start = QPointF( 0, 0 ) self.end = QPointF( 200, 200 ) # Draw a line (boundingRect is created automatically) self.lin = self.addLine( QLineF( self.start, self.end ), self.my_pen ) self.lin.setFlags( QGraphicsItem.ItemIsSelectable ) # Change Pen-Setttings for new Item self.my_pen.setWidthF( 2 ) self.my_pen.setColor( Qt.darkRed ) self.my_pen.setStyle( Qt.DotLine ) # Draw polygon, which I would like to apply on my line as a bounding rect self.poly = self.addPolygon( QPolygonF( [ QPointF( 20, -30 ), QPointF( -30, 20 ), QPointF( 180, 230 ), QPointF( 230, 180 ), ] ), self.my_pen ) # Reimplementing the mousePressEvent for catching some information def mousePressEvent( self, sceneEvent: QGraphicsSceneMouseEvent ): # Get position and item at the position of the event #### EDIT after Comments from musicamente #### FIRST pass the event to the original implementation of the mousePressEvent super().mousePressEvent( sceneEvent ) #### and THEN get the position of the item at the event-scenePosition pp = sceneEvent.scenePos() current_item = self.itemAt( pp, QTransform() ) # So if there is an item at the clicked position, then write some information if current_item is not None: text = f"scenePos() = {pp} \n"\ f"screenPos() = {sceneEvent.screenPos()}\n"\ f"current_item.boundingRect() = "\ f"{current_item.boundingRect()}\n"\ f"current_item.shape() = {current_item.shape()}\n"\ f"current_item.shape().boundingRect() = "\ f"{current_item.shape().boundingRect()}\n"\ f"current_item.shape().controlPointRect() = "\ f"{current_item.shape().controlPointRect()}\n"\ f"" my_gui.my_textedit.setText( text ) current_item.mousePressEvent( sceneEvent ) # The Class/Widget for placing the view and a QTextEdit class MyGui( QWidget ): def __init__( self ): super().__init__() self.setGeometry( 50, 50, 800, 800 ) self.my_scene = MyScene() self.my_view = QGraphicsView( self ) self.my_view.setScene( self.my_scene ) # QTextEdit for displaying some stuff for debuggin self.my_textedit = QTextEdit( self ) self.my_textedit.setGeometry( 0, 610, 600, 150 ) # Starting the App my_app = QApplication( sys.argv ) my_gui = MyGui() my_gui.show() sys.exit( my_app.exec() )
调试信息
current_item.shape() = <PyQt5.QtGui.QPainterPath object at 0x00000213C8DD9A10>
current_item.shape().boundingRect() = PyQt5.QtCore.QRectF(-30.707106781186553, -30.999999999983665, 261.70710678117024, 261.70710678117024)
current_item.shape().controlPointRect() = PyQt5.QtCore.QRectF(-30.707106781186553, -31.09763107291604, 261.8047378541026, 261.8047378541026)
解决方案:重写QGraphicsLineItem的shape()方法
默认QGraphicsLineItem的shape是线条的外接矩形,导致斜线可点击范围过大。自定义LineItem类,重写shape()方法生成贴合线条实际区域的路径:
from PyQt5.QtGui import QPainterPath, QRectF class CustomLineItem(QGraphicsLineItem): def __init__(self, line: QLineF, pen: QPen, parent=None): super().__init__(line, parent) self.setPen(pen) self.setFlags(QGraphicsItem.ItemIsSelectable) def shape(self): path = QPainterPath() start = self.line().p1() end = self.line().p2() pen_width = self.pen().widthF() # 计算线条垂直方向的单位向量 line_vec = end - start if line_vec.isNull(): return path perp_vec = QPointF(-line_vec.y(), line_vec.x()) perp_vec /= line_vec.manhattanLength() perp_vec *= pen_width / 2 # 生成带圆角的线条路径(匹配RoundCap) path.moveTo(start + perp_vec) path.lineTo(end + perp_vec) path.arcTo(QRectF(end - perp_vec, end + perp_vec), 0, 180) path.lineTo(start - perp_vec) path.arcTo(QRectF(start - perp_vec, start + perp_vec), 180, 180) path.closeSubpath() return path
替换MyScene中创建线条的代码:
# 替换原self.lin = ... 为: self.lin = CustomLineItem(QLineF(self.start, self.end), self.my_pen) self.addItem(self.lin)
此方法生成的shape精准贴合斜线的实际绘制区域,彻底解决误选问题。
PyQt5图形编程资源推荐
书籍
- 《PyQt5快速开发与实战》:覆盖PyQt5核心组件,包含图形视图框架的基础案例,适合入门。
- 《Qt5编程入门》:虽为C++版本,但Qt图形视图框架逻辑通用,可帮助理解底层原理,适配PyQt5使用。
官方文档
- PyQt5官方文档:详细讲解QGraphicsScene、QGraphicsItem等类的API,是权威参考资料。
- Qt官方文档:原生文档可辅助理解图形视图框架的设计思想与最佳实践。
内容的提问来源于stack exchange,提问作者Bartek Kahl
相关产品推荐
相关产品推荐

