You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 13:57:17