自定义QGraphicsItem

简述

QGraphicsItem 是场景中 item 的基类。图形视图提供了一些典型形状的标准 item,例如:矩形 ( QGraphicsRectItem )、椭圆 ( QGraphicsEllipseItem ) 、文本项 ( QGraphicsTextItem )。当这些不满足需求时(例如:在一些复杂的工作流场景中),往往需要自定义,通常的做法就是继承 QGraphicsItem。

自定义 QGraphicsItem

要实现自定义 item,需要覆盖 QGraphicsItem 的两个纯虚函数:

  • void paint()
    • 以本地坐标绘制 item 的内容
  • QRectF boundingRect()
    • 将 item 的外边界作为矩形返回
    • 由 QGraphicsView 调用以确定什么区域需要重绘

除此之外,可能还需要附加其他需求,例如:

  • QPainterPath shape() - item 的形状
    • 由 contains() 和 collidesWithPath() 用于碰撞检测
    • 如果未实现,则默认为 boundingRect()
  • 使用信号/槽、属性机制:继承 QObject 和 QGraphicsItem(或直接继承 QGraphicsObject)
  • 处理鼠标事件:重新实现 mouse***Event()
  • 处理键盘事件:重新实现 key***Event()
  • 处理拖放事件:重新实现 drag***Event()、dropEvent()
    ……

关于信号/槽、事件、算法相关的内容,本节暂时不做讲解,放到后面章节。

Bounding Rect 和 Shape

先来一张效果图,解释 Bounding Rect 和 Shape 的联系与区别:

自定义QGraphicsItem

  • Bounding Rect

    将 item 的外边界定义为矩形,所有绘制必须限制在此区域内,QGraphicsView 使用它来确定 item 是否需要重绘。

    虽然 item 的形状可以是任意的(例如:直线、椭圆、矩形 ),但是 bounding rect 总是矩形,并且不受 item 变换的影响。

  • Shape

    以本地坐标中的 QPainterPath 形式返回 item 的形状。形状可用于许多事情,包括:碰撞检测,命中测试以及 QGraphicsScene::items() 函数。

    shape() 默认实现调用 boundingRect() 返回一个简单的矩形形状,但子类可以重新实现该函数,以返回非矩形 item 更准确的形状。例如,一个圆形 item 可以选择返回椭圆形状,以便更好地进行碰撞检测。

    shape() 由 contains() 和 collidesWithPath() 的默认实现调用。

参照模型

来一个笑脸,瞬间萌萌哒……笑一笑,十年少!

自定义QGraphicsItem

要实现这个效果很简单,可以逐步分解:

  • 整体(最外侧的圆)
  • 眼睛(左眼/右眼/眼球)
  • 嘴(笑容)

分别计算出各部分的区域坐标、大小,然后根据形状进行绘制。

上述图案标识的是绝对位置,为了适应各种大小, 可以进行比例及相对位置换算,将各部分进行逐一转换。

使用示例

效果

下图显示了 3 个不同大小的笑脸:

自定义QGraphicsItem

源码

根据以上思路,我们可以很快的实现一个自定义的笑脸 - SmileItem。

SmileItem.h:

#ifndef SMILE_ITEM_H
#define SMILE_ITEM_H

#include <QGraphicsItem>

class SmileItem : public QGraphicsItem
{
public:
    explicit SmileItem(QGraphicsItem *parent = Q_NULLPTR);
    explicit SmileItem(const QRectF &rect, QGraphicsItem *parent = Q_NULLPTR);
    explicit SmileItem(qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent = Q_NULLPTR);
    ~SmileItem();
    QRectF rect() const;
    void setRect(const QRectF &rect);
    inline void setRect(qreal x, qreal y, qreal w, qreal h);

    QRectF boundingRect() const Q_DECL_OVERRIDE;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) Q_DECL_OVERRIDE;

private:
    void updateRect();

private:
    QRectF m_rect;
    mutable QRectF m_boundingRect;

    // 缩放比例
    double m_dScale;

    // 左眼、右眼、嘴的中点
    QPointF m_leftEyeCenter;
    QPointF m_rightEyecenter;
    QPointF m_smileCenter;

    // 眼睛的宽度、高度
    double m_dEyeWidth;
    double m_dEyeHeight;

    // 眼球宽度(高和宽相同)
    double m_dEyeBallWidth;

    // 嘴的高度、宽度
    double m_dSmileWidth;
    double m_dSmileHeight;
};

inline void SmileItem::setRect(qreal ax, qreal ay, qreal w, qreal h)
{
    setRect(QRectF(ax, ay, w, h));
}

#endif  //SMILE_ITEM_H

除了上述必须实现的两个函数之外,我们还提供一些额外的接口,例如:setRect() 来更改 item 的大小,在更新大小之后,则会调用 updateRect() 来重新计算笑脸中各个部位的坐标、大小。

SmileItem.cpp:

#include <QPainter>
#include "SmileItem.h"

SmileItem::SmileItem(QGraphicsItem *parent)
    : QGraphicsItem(parent)
{
    setRect(QRect(-50, -50, 100, 100));
}

SmileItem::SmileItem(const QRectF &rect, QGraphicsItem *parent)
    : QGraphicsItem(parent)
{
    setRect(rect);
}

SmileItem::SmileItem(qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent)
    : QGraphicsItem(parent)
{
    setRect(x, y, w, h);
}

SmileItem::~SmileItem()
{
}

QRectF SmileItem::rect() const
{
    return m_rect;
}

void SmileItem::setRect(const QRectF &rect)
{
    if (m_rect == rect)
        return;

    prepareGeometryChange();
    m_rect = rect;
    m_boundingRect = QRectF();
    updateRect();
    update();
}

QRectF SmileItem::boundingRect() const
{
    if (m_boundingRect.isNull())
        m_boundingRect = m_rect;

    return m_boundingRect;
}

void SmileItem::updateRect()
{
    // 缩放比例
    m_dScale = m_rect.width() / 100.0;

    // 左眼的中点
    m_leftEyeCenter.setX(-15 * m_dScale);
    m_leftEyeCenter.setY(- 25 * m_dScale);

    // 右眼的中点
    m_rightEyecenter.setX(15 * m_dScale);
    m_rightEyecenter.setY(- 25 * m_dScale);

    // 嘴的中点
    m_smileCenter.setX(0);
    m_smileCenter.setY(10 * m_dScale);

    // 眼睛的宽度、高度(宽度的 2 倍)
    m_dEyeWidth = m_rect.width() / (100.0 / 12);
    m_dEyeHeight = m_dEyeWidth * 2;

    // 眼球为眼睛大小的 1/4
    m_dEyeBallWidth = m_dEyeWidth / 4;

    // 嘴的高度、宽度
    m_dSmileWidth = m_rect.width() / (100.0 / 66);
    m_dSmileHeight = m_rect.height() / (100.0 / 50);
}

void SmileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);

    // 反走样
    painter->setRenderHint(QPainter::Antialiasing, true);

    // 脸
    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::yellow);
    painter->drawEllipse(m_rect);

    // 左眼
    painter->setPen(QPen(Qt::black));
    painter->setBrush(Qt::white);
    painter->drawEllipse(QRectF(m_leftEyeCenter.x() - m_dEyeWidth / 2, m_leftEyeCenter.y() - m_dEyeHeight / 2, m_dEyeWidth, m_dEyeHeight));

    // 左眼球
    painter->setPen(QPen(Qt::black));
    painter->setBrush(Qt::black);
    painter->drawEllipse(QRectF(m_leftEyeCenter.x() - m_dEyeBallWidth / 2, m_leftEyeCenter.y() - m_dEyeBallWidth / 2, m_dEyeBallWidth / 2, m_dEyeBallWidth / 2));

    // 右眼
    painter->setPen(QPen(Qt::black));
    painter->setBrush(Qt::white);
    painter->drawEllipse(QRectF(m_rightEyecenter.x() - m_dEyeWidth / 2, m_rightEyecenter.y() - m_dEyeHeight / 2, m_dEyeWidth, m_dEyeHeight));

    // 右眼球
    painter->setPen(QPen(Qt::black));
    painter->setBrush(Qt::black);
    painter->drawEllipse(QRectF(m_rightEyecenter.x() - m_dEyeBallWidth / 2, m_rightEyecenter.y() - m_dEyeBallWidth / 2, m_dEyeBallWidth / 2, m_dEyeBallWidth / 2));

    // 嘴 - 笑容
    painter->setPen(QPen(Qt::red));
    painter->setBrush(Qt::NoBrush);

    QPainterPath path;
    path.arcMoveTo(QRectF(- m_dSmileWidth / 2, - (m_dSmileHeight / 2 - m_smileCenter.y()), m_dSmileWidth, m_dSmileHeight), 0);
    path.arcTo(QRectF(- m_dSmileWidth / 2, - (m_dSmileHeight / 2 - m_smileCenter.y()), m_dSmileWidth, m_dSmileHeight), 0, -180);
    painter->drawPath(path);
}

正如 setRect(),无论以任何方式更改 item 的几何形状,必须首先调用prepareGeometryChange(),以保证 QGraphicsScene 中的索引是最新的。

为了实现大小的自适应,在 updateRect() 中实现了坐标、大小的换算。并通过调用 update() 重新对 item 进行绘制。

然后就可以使用了:

// 定义笑脸
SmileItem *pItem = new SmileItem();
pItem->setRect(QRect(-25, -25, 50, 50));
pItem->setPos(10, 50);

SmileItem *pItem2 = new SmileItem();
pItem2->setRect(QRect(-50, -50, 100, 100));
pItem2->setPos(100, 50);

SmileItem *pItem3 = new SmileItem();
pItem3->setRect(QRect(-75, -75, 150, 150));
pItem3->setPos(250, 50);

// 将笑脸添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);
pScene->addItem(pItem2);
pScene->addItem(pItem3);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

首先,构造三个不同大小的笑脸,并调用 setPos() 设置它们的位置。然后通过 QGraphicsScene::addItem() 将笑脸添加至场景中。最后,调用 QGraphicsView::setScene() 为视图设置场景,并显示视图。

上一篇:java创建线程的三种方式及其对照


下一篇:-bash:syntax error near unexpected token '('