QGraphicsItem鼠标精准拾取(pick/select)研究

在QT场景视图中,一个2D图形项是一个QGraphicsItem,我们可以通过继承来定义我们自己的图形项。

主要有以下三个虚函数需要重点关注:

1)   边界矩形(必须实现)

virtual QRectF boundingRect() const = 0;

2)   图形形状(可选实现),该函数返回图形项的实际形状路径,常用于碰撞检测、命中测试等等,默认实现返回boundingRect的矩形形状(具体的图形项的形状是任意变化的,默认的矩形形状显然不能正确表示图形的实际形状,所以建议重写该函数)。需要注意的是,形状的轮廓线可能会根据画笔大小以及线型而有所不同,所以实际的形状也应该包括轮廓线的区域。

virtual QPainterPath shape() const;

3)   图形内容(必须实现)

virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) = 0;

 

图形一般的表现形式有两种:封闭和非封闭,如直线、曲线等都是非封闭图形,而矩形、椭圆等为封闭图形,非封闭图形无法使用填充,实际的形状为线条所指定的路径区域,封闭图形可以使用填充,实际的形状包括线条以及封闭填充区域。

QT的QPainter类提供了绘制最常见图形(如矩形、椭圆、多边形、文本等)API,对于一些不规则形状的复杂图形,则提供了drawPath方法通过绘制路径来达到。

QPainterPath

QPainterPath 类(绘图路径)提供了一个容器,用于绘图操作,可以创建和重用图形形状。

绘图路径是由许多图形化的构建块组成的对象,例如:矩形、椭圆、直线和曲线。构建块可以加入在封闭的子路径中,例如:矩形或椭圆。封闭的路径的起点和终点是一致的,或者他们可以作为未封闭的子路径独立存在,如:直线和曲线。

与正常绘图相比,QPainterPath 的主要优点在于:复杂的图形只需创建一次,然后只需调用 QPainter::drawPath() 函数即可绘制多次。QPainterPath 提供了一组函数,可用于获取绘图路径及其元素的信息。除了可以使用 toReversed() 函数来改变元素的顺序外,还有几个函数将 QPainterPath 对象转换成一个多边形表示。

QPainterPathStroker

QPainterPath 可以被填充(fill)、描绘轮廓(outline)、裁剪(clip)。要为一个指定的绘图路径生成可填充的轮廓,可以使用 QPainterPathStroker 类。。

通过调用createStroke()函数,将给定的QPainterPath作为参数传递,将创建一个表示给定路径轮廓的新画家路径(outlinepath)。 然后可以填充新创建的画家路径用于绘制原始画家路径(path)的轮廓。

您可以使用以下函数控制轮廓的各种设计方面(画笔宽度,帽子样式,连接样式和点画线模式):

  • setWidth()
  • setCapStyle()
  • setJoinStyle()
  • setDashPattern()

setDashPattern()函数既可以接受Qt::PenStyle对象,也可以接受模式的vector表示作为参数。

此外,您可以使用setCurveThreshold()函数指定曲线的阈值,控制绘制曲线的粒度。默认阈值是经过良好调整的值(0.25),通常您不需要修改它。但是,您可以通过降低其值来使曲线的外观更平滑。

您还可以使用setMiterLimit()函数控制生成的轮廓的斜接限制。斜接限制描述了斜接连接可以延伸到每个连接的距离。限制以宽度为单位指定,因此像素化斜接限制将为miterlimit * width。仅当连接样式为Qt :: MiterJoin时才使用此值。

注意,createStroke()函数生成的painter路径只能用于概述给定的painter路径,否则可能会导致意外行为。生成的轮廓也需要默认设置的Qt :: WindingFill规则。

 

QT场景视图中要实现2D图形的精准拾取,就需要关注图形的shape而不是boundingRect,下面是一个测试例子,仅供参考:

新建ItemBase类,继承自QGraphicsItem,用于规定子Item的一些共同行为:

ItemBase.h 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 
#ifndef ITEMBASE_H
#define ITEMBASE_H

#include <QGraphicsItem>

class QGraphicsSceneMouseEvent;
class ItemBase : public QGraphicsItem
{
public:
    ItemBase(QSize size, QGraphicsItem *parent = nullptr);

    
virtual ~ItemBase() override;

    QRectF boundingRect() 
const override;

    
void paint(QPainter *painter,
               
const QStyleOptionGraphicsItem *option,
               QWidget *widget) override;

protected:
    
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
    
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
    
void wheelEvent(QGraphicsSceneWheelEvent *event) override;
    
bool isInResizeArea(const QPointF &pos);

protected:
    QSize   m_size;

private:
    
bool    m_isResizing;
    
bool    m_isRotating;

};

#endif // ITEMBASE_H

在ItemBase类中,我们重写了boundingRect以及paint函数,图形项的具体形状在子类中重写shape()来实现:

ItemBase.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
QRectF ItemBase::boundingRect() const
{
    
// 实际图形形状的边界矩形
    return shape().boundingRect();
}
 
void ItemBase::paint(QPainter *painter, 
                     
const QStyleOptionGraphicsItem *option, 
                     QWidget *widget)
{
    Q_UNUSED(widget);
    
if (option->state & QStyle::State_Selected) {
        painter->setRenderHint(QPainter::Antialiasing, 
true);
        
if (option->state & QStyle::State_HasFocus) {
            painter->setPen(QPen(Qt::yellow, 
3));
        }
        
else {
            painter->setPen(Qt::white);
        }
        painter->drawRect(boundingRect());
           }
    painter->setRenderHint(QPainter::Antialiasing, 
false);
}

以ItemPolyline为例进行说明:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
 
#ifndef ITEMPOLYLINE_H
#define ITEMPOLYLINE_H

#include "ItemBase.h"

class ItemPolyline : public ItemBase
{
public:
    ItemPolyline(QSize size, QGraphicsItem *parent = nullptr);


    
virtual void paint(QPainter *painter,
                       
const QStyleOptionGraphicsItem *option,
                       QWidget *widget = nullptr);
    
// overwrite shape()
    QPainterPath shape() const;
};

#endif // ITEMPOLYLINE_H



#include "ItemPolyline.h"
#include <QPainter>
#include <QPainterPath>

ItemPolyline::ItemPolyline(QSize size, QGraphicsItem *parent)
    : ItemBase (size, parent)
{

}

void ItemPolyline::paint(QPainter *painter,
                         
const QStyleOptionGraphicsItem *option,
                         QWidget *widget)
{
    
static const QPointF points[3] =
    {
        QPointF(
10.0100.0),
        QPointF(
20.010.0),
        QPointF(
100.030.0),
    };

    painter->save();
    QPen pen(Qt::blue);
    pen.setWidth(
10);
    pen.setJoinStyle(Qt::MiterJoin);    
// MiterJoin, BevelJoin, RoundJoin
    pen.setCapStyle(Qt::RoundCap);      // FlatCap, SquareCap, RoundCap
    pen.setStyle(Qt::DashLine);
    painter->setPen(pen);
    painter->drawPolyline(points, 
3);
    painter->restore();

    ItemBase::paint(painter, option, widget);
}

QPainterPath ItemPolyline::shape() 
const
{
    
static const QPointF points[3] =
    {
        QPointF(
10.0100.0),
        QPointF(
20.010.0),
        QPointF(
100.030.0),
    };
    QPainterPath path;
    path.moveTo(points[
0]);
    path.lineTo(points[
1]);
    path.lineTo(points[
2]);
    QPainterPathStroker stroker;
    stroker.setWidth(
10);
    stroker.setJoinStyle(Qt::MiterJoin);
    stroker.setCapStyle(Qt::RoundCap);
    stroker.setDashPattern(Qt::DashLine);
    
return stroker.createStroke(path);
}

ItemPolyline类中重写shape()函数,使用QPainterPath和QPainterPathStroker比较精准地获取了图形的轮廓形状,有利于鼠标对图形的精准拾取。注意:对于封闭形状,既要考虑其形状所围填充区域,又要考虑其边界轮廓的宽度区域。

除了Polyline外,我还做了Rectangle、Ellipse、Bezier、ClosedBezier以及line和lines等2D图形,以下是运行截图:

 

鼠标点击2D图形的有效区域(即Shape所规定的路径区域)会比较精准地选中图形,而其它空白区域则无法选中,仅供参考,欢迎交流!

posted on 2019-07-22 13:52  我来乔23  阅读(3459)  评论(0编辑  收藏  举报

导航