简述
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 的联系与区别:
-
Bounding Rect
将 item 的外边界定义为矩形,所有绘制必须限制在此区域内,QGraphicsView 使用它来确定 item 是否需要重绘。
虽然 item 的形状可以是任意的(例如:直线、椭圆、矩形 ),但是 bounding rect 总是矩形,并且不受 item 变换的影响。
-
Shape
以本地坐标中的 QPainterPath 形式返回 item 的形状。形状可用于许多事情,包括:碰撞检测,命中测试以及 QGraphicsScene::items() 函数。
shape() 默认实现调用 boundingRect() 返回一个简单的矩形形状,但子类可以重新实现该函数,以返回非矩形 item 更准确的形状。例如,一个圆形 item 可以选择返回椭圆形状,以便更好地进行碰撞检测。
shape() 由 contains() 和 collidesWithPath() 的默认实现调用。
参照模型
来一个笑脸,瞬间萌萌哒……笑一笑,十年少!
要实现这个效果很简单,可以逐步分解:
- 整体(最外侧的圆)
- 眼睛(左眼/右眼/眼球)
- 嘴(笑容)
分别计算出各部分的区域坐标、大小,然后根据形状进行绘制。
上述图案标识的是绝对位置,为了适应各种大小, 可以进行比例及相对位置换算,将各部分进行逐一转换。
使用示例
效果
下图显示了 3 个不同大小的笑脸:
源码
根据以上思路,我们可以很快的实现一个自定义的笑脸 - 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() 为视图设置场景,并显示视图。