Qt 图形视图框架<一>——<QGraphicsItem>
一、概念介绍
之前最项目,一直在用2D绘图的QGraphics/view,由于今年肺炎疫情的影响无法出门,所以有时间把这块做一个总结。
在我们平时绘图中,如果我们在一块画布上绘制多个不规则图形并且还要监控每一个图形的行为(比如移动、叠加、碰撞、拖动、缩放、旋转等操作)时,我们就要用到Qt里的图形视图框架,QGraphicScene(场景)可以管理多个图形项QGraphicsItem(比如:QGraphicsRectItem(矩形的图形项,也就是图元)),QGraphicsView(视图)关联场景可以让场景中的所有图形项可视化,其次还提供了缩放和旋转,可以帮助文档中搜索Graphics View 关键字查阅。
二、简单应用示例
分别新建了一个场景,一个矩形图形项和一个视图,并将图形项添加到场景中,将视图与场景关联,最后显示视图就行了,场景是管理图形项的,所有的图形项必须添加到一个场景中,但是场景本身无法可视化,要想看到场景上的内容,必须使用视图,代码如下:
#include <QtWidgets> #include <QApplication> int main(int argc,char* argv[ ]) { QApplication app(argc,argv); // 场景 QGraphicsScene *scene = new QGraphicsScene; // 矩形项 QGraphicsRectItem *item = new QGraphicsRectItem(150,150,50,50); // 项添加到场景 scene->addItem(item); // 视图 QGraphicsView *view = new QGraphicsView; // 视图关联场景 view->setScene(scene); // 显示视图 view->show(); return app.exec(); }
运行如下:
三、图像项QGraphicsItem
QGraphicsItem类是所有图形项的基类。图形视图框架对一些典型的形状提供了一些标准的图形项。比如上面我们使用的矩形(QGraphicsRectItem)、椭圆(QGraphicsEllipseItem)、文本(QGraphicsTextItem)等多个图形项。但只有继承QGraphicsItem 类实现我们自定义的图形项时,才能显示出这个类的强大。QGraphicsItem支持以下功能:
- 鼠标的按下、移动、释放和双击事件,也支持鼠标悬停、滚轮和右键菜单事件。
- 键盘输入焦点和键盘事件
- 拖放
- 利用QGraphicsItemGroup进行分组
- 碰撞检测
3.1 自定义图形项
我们继承QGraphicsItem类实现自定义的图形项,必须先实现两个纯虚函数boundingRect()和paint(),前者用于定义Item的绘制范围,后者用于绘制图形项,首先我们新增MyItem类,代码如下:
#ifndef MYITEM_H #define MYITEM_H #include <QGraphicsItem> class MyItem : public QGraphicsItem { public: MyItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; }; #endif // MYITEM_H
#include "MyItem.h" #include <QPainter> MyItem::MyItem() { } QRectF MyItem::boundingRect() const { qreal penWidth = 1; return QRectF(0 - penWidth/2,0-penWidth/2,20+penWidth,20+penWidth); } void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { //标明该参数没有使用 Q_UNUSED(option) Q_UNUSED(widget) painter->setBrush(Qt::yellow); painter->drawEllipse(0,0,50,50); }
#include "MyItem.h" #include <QtWidgets> #include <QApplication> int main(int argc,char* argv[ ]) { QApplication app(argc,argv); // 场景 QGraphicsScene *scene = new QGraphicsScene; // 椭圆项 MyItem *item = new MyItem; // QGraphicsRectItem *item = // new QGraphicsRectItem(150,150,50,50); // 项添加到场景 scene->addItem(item); // 视图 QGraphicsView *view = new QGraphicsView; // 视图关联场景 view->setScene(scene); // 显示视图 view->show(); return app.exec(); }
运行结果如下:
3.2 添加光标提示
添加光标提示可以通过QCursor来实现,setCursor设置光标的形状,setToolTip设置提示文字,在构造函数中添加如下代码:
MyItem::MyItem() { setToolTip(QString("提示信息")); setCursor(Qt::OpenHandCursor); //改变光标形状,光标变为了手型 }
运行结果如下:
3.3 拖放操作
(a)修改myitem.h和myitem.cpp,通过鼠标事件来实现拖动操作,修改代码如下:
#ifndef MYITEM_H #define MYITEM_H #include <QGraphicsItem> class MyItem : public QGraphicsItem { public: MyItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; private: QColor color; }; #endif // MYITEM_H
#include "MyItem.h" #include <QPainter> #include <QCursor> #include <QGraphicsSceneMouseEvent> #include <QDrag> #include <QMimeData> #include <QApplication> #include <QWidget> MyItem::MyItem() { setToolTip(QString("提示信息")); setCursor(Qt::OpenHandCursor); //改变光标形状,光标变为了手型 //初始化随机颜色 color = QColor(qrand() % 256, qrand() % 256, qrand() % 256); } QRectF MyItem::boundingRect() const { qreal penWidth = 1; return QRectF(0 - penWidth/2,0-penWidth/2,20+penWidth,20+penWidth); } void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { //标明该参数没有使用 Q_UNUSED(option) Q_UNUSED(widget) painter->setBrush(Qt::yellow); painter->drawEllipse(0,0,20,20); } void MyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { setCursor(Qt::OpenHandCursor); //改变光标形状 } void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if(Qt::LeftButton != event->button()) { //如果不是鼠标左键按下,则忽略该事件 event->ignore(); return; } //如果是鼠标左键按下,改变光标形状 setCursor(Qt::ClosedHandCursor); } void MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { /*QLineF类使用浮点精度提供二维矢量。 QLineF在二维表面上描述了有限长度的线(或线段)。 QLineF使用浮点精度来定义坐标线的起点和终点。 使用toLine()函数可检索此行的基于整数的副本。*/ if(QLineF(event->screenPos(),event->buttonDownScenePos(Qt::LeftButton)).length() < QApplication::startDragDistance()) { //如果按下的点到现在的点的距离小于程序默认的拖动距离,表明没有拖动,则返回 //QApplication::startDragDistance如果您在应用程序中支持拖放操作,并且想在用户按住某个按钮将光标移动一定距离后开始拖放操作,则应使用此属性的值作为所需的最小距离。 return; } //QDrag类为基于MIME的拖放数据传输提供支持。 //QDrag拖放是用户在应用程序中复制或移动数据的一种直观方式,并且在许多桌面环境中用作在应用程序之间复制数据的机制。 Qt中的拖放支持以QDrag类为中心 //为event所在窗口部件新建拖动对象 QDrag *drag = new QDrag(event->widget()); //新建QMimeData对象,它用来存储拖动的数据 QMimeData *mime = new QMimeData; //关联 drag->setMimeData(mime); //放入颜色数据 mime->setColorData(color); //新建QPixmap对象,它用来重新绘制圆形,在拖动时显示 QPixmap pix(21,21); pix.fill(Qt::white); QPainter painter(&pix); paint(&painter, nullptr, nullptr); drag->setPixmap(pix); //我们让指针指向圆形的(10,15)点 drag->setHotSpot(QPoint(10, 15)); //开始拖动 drag->exec(); //改变光标形状 setCursor(Qt::OpenHandCursor); }
运行后我们就可以鼠标按住拖放了:
(b) 接收拖拽来的数据
新建一个RectItem 类,处理接收拖拽来的数据,要想实现拖放,必须源图形项和目标图形项都进行相关设置。在源图形项的鼠标事件中新建并执行拖动,而在目标图形项中必须指定setAcceptDrops(true),这样才能接收拖放,然后需要实现拖放的几个事件处理函数,修改了MyItem类,所有文件的代码如下:
#ifndef MYITEM_H #define MYITEM_H #include <QGraphicsItem> class MyItem : public QGraphicsItem { public: MyItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; private: QColor color; }; #endif // MYITEM_H
#include "MyItem.h" #include <QPainter> #include <QCursor> #include <QGraphicsSceneMouseEvent> #include <QDrag> #include <QMimeData> #include <QApplication> #include <QWidget> MyItem::MyItem() { setToolTip(QString("提示信息")); setCursor(Qt::OpenHandCursor); //改变光标形状,光标变为了手型 //初始化随机颜色 color = QColor(qrand() % 256, qrand() % 256, qrand() % 256); } QRectF MyItem::boundingRect() const { qreal penWidth = 1; return QRectF(0 - penWidth/2,0-penWidth/2,20+penWidth,20+penWidth); } void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { //标明该参数没有使用 Q_UNUSED(option) Q_UNUSED(widget) painter->setBrush(color); painter->drawEllipse(0, 0, 20, 20); } void MyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { setCursor(Qt::OpenHandCursor); //改变光标形状 } void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if(Qt::LeftButton != event->button()) { //如果不是鼠标左键按下,则忽略该事件 event->ignore(); return; } //如果是鼠标左键按下,改变光标形状 setCursor(Qt::ClosedHandCursor); } void MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { /*QLineF类使用浮点精度提供二维矢量。 QLineF在二维表面上描述了有限长度的线(或线段)。 QLineF使用浮点精度来定义坐标线的起点和终点。 使用toLine()函数可检索此行的基于整数的副本。*/ if(QLineF(event->screenPos(),event->buttonDownScenePos(Qt::LeftButton)).length() < QApplication::startDragDistance()) { //如果按下的点到现在的点的距离小于程序默认的拖动距离,表明没有拖动,则返回 //QApplication::startDragDistance如果您在应用程序中支持拖放操作,并且想在用户按住某个按钮将光标移动一定距离后开始拖放操作,则应使用此属性的值作为所需的最小距离。 return; } //QDrag类为基于MIME的拖放数据传输提供支持。 //QDrag拖放是用户在应用程序中复制或移动数据的一种直观方式,并且在许多桌面环境中用作在应用程序之间复制数据的机制。 Qt中的拖放支持以QDrag类为中心 //为event所在窗口部件新建拖动对象 QDrag *drag = new QDrag(event->widget()); //新建QMimeData对象,它用来存储拖动的数据 QMimeData *mime = new QMimeData; //关联 drag->setMimeData(mime); //放入颜色数据 mime->setColorData(color); //新建QPixmap对象,它用来重新绘制圆形,在拖动时显示 QPixmap pix(21,21); pix.fill(Qt::white); QPainter painter(&pix); paint(&painter, nullptr, nullptr); drag->setPixmap(pix); //我们让指针指向圆形的(10,15)点 drag->setHotSpot(QPoint(10, 15)); //开始拖动 drag->exec(); //改变光标形状 setCursor(Qt::OpenHandCursor); }
#ifndef RECTITEM_H #define RECTITEM_H #include <QGraphicsItem> class RectItem:public QGraphicsItem { public: RectItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: void dropEvent(QGraphicsSceneDragDropEvent *event) override; void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override; void dragLeaveEvent(QGraphicsSceneDragDropEvent *event) override; private: QColor color; bool dragOver; //标志是否有拖动进入 }; #endif // RECTITEM_H
#include "RectItem.h" #include <QPainter> #include <QGraphicsSceneDragDropEvent> #include <QDrag> #include <QMimeData> RectItem::RectItem() { setAcceptDrops(true); //设置接收拖放 color = QColor(Qt::lightGray); } QRectF RectItem::boundingRect() const { return QRectF(0, 0, 100, 100); } void RectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) //如果其上有拖动,颜色变亮 painter->setBrush(dragOver ? color.light(130) : color); painter->drawRect(0,0,100,100); } void RectItem::dropEvent(QGraphicsSceneDragDropEvent *event) { if(event->mimeData()->hasColor()) //如果拖动的数据中有颜色数据,便接收 { event->setAccepted(true); dragOver = true; update(); } else event->setAccepted(false); } void RectItem::dragEnterEvent(QGraphicsSceneDragDropEvent *event) { dragOver = false; if (event->mimeData()->hasColor()) //我们通过类型转换来获得颜色 color = qvariant_cast<QColor>(event->mimeData()->colorData()); update(); } void RectItem::dragLeaveEvent(QGraphicsSceneDragDropEvent *event) { Q_UNUSED(event) dragOver = false; update(); }
#include "MyItem.h" #include "RectItem.h" #include <QtWidgets> #include <QApplication> int main(int argc,char* argv[ ]) { QApplication app(argc,argv); //设置随机数初值 qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); QGraphicsScene *scene = new QGraphicsScene; for(int i=0; i<5; i++) //在不同位置新建5个圆形 { MyItem *item = new MyItem; item->setPos(i*50+20, 100); scene->addItem(item); } RectItem *rect = new RectItem; //新建矩形 rect->setPos(100,200); scene->addItem(rect); QGraphicsView *view = new QGraphicsView; view->setScene(scene); view->resize(400, 300); //设置视图大小 view->show(); return app.exec(); }
运行效果如下,可以通过拖动小圆到矩形中以修改矩形颜色:
3.4 键盘和鼠标事件
3.4.1 键盘事件
我们申明keyPressEvent函数,并在图元获得焦点后每一次的键盘操作都会移动moveBy(0, 10),也就是向下移动10个像素,代码如下:
#ifndef MYITEM_H #define MYITEM_H #include <QGraphicsItem> class MyItem : public QGraphicsItem { public: MyItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: void keyPressEvent(QKeyEvent *event); }; #endif // MYITEM_H
#include "MyItem.h" #include <QPainter> MyItem::MyItem() { //图形项可获得焦点,必须设置方块才能移动 setFlag(QGraphicsItem::ItemIsFocusable); } QRectF MyItem::boundingRect() const { qreal penWidth = 1; return QRectF(0 - penWidth/2,0-penWidth/2,20+penWidth,20+penWidth); } void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { //标明该参数没有使用 Q_UNUSED(option) Q_UNUSED(widget) painter->setBrush(Qt::red); painter->drawRect(0, 0, 20, 20); } void MyItem::keyPressEvent(QKeyEvent *event) { moveBy(0, 10); //相对现在的位置移动 }
#include "MyItem.h" #include <QApplication> #include <QtWidgets> int main(int argc,char* argv[ ]) { QApplication app(argc,argv); // 场景 QGraphicsScene *scene = new QGraphicsScene; // 椭圆项 MyItem *item = new MyItem; // QGraphicsRectItem *item = // new QGraphicsRectItem(150,150,50,50); // 项添加到场景 scene->addItem(item); // 视图 QGraphicsView *view = new QGraphicsView; // 视图关联场景 view->setScene(scene); // 显示视图 view->show(); return app.exec(); }
测试截图如下:
3.4.2 鼠标事件
在MyItem.h中添加如下方法
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
在MyItem.cpp中实现方法:
void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { Q_UNUSED(event) moveBy(10,0); }
此时每次点击小方块将会向右移动10像素,如果我们想让鼠标可以拖动小方块,那么我们可以重新实现mouseMoveEvent()函数,其次我们可以在构造函数中指明该图形项是可移动的:
//指明该图形项是可移动的 setFlag(QGraphicsItem::ItemIsMovable);
这样我们就可以鼠标拖动该图元方块:
3.5 碰撞检测
collidingItems函数返回与该项目冲突的所有项目的列表,下面我们用它做个例子:
将MyItem.cpp中paint函数设置画刷的代码改成如下:
painter->setBrush(!collidingItems().isEmpty()? Qt::red : Qt::green);
然后在main.cpp文件中在场景中添加一个直线图形项:
QGraphicsLineItem *line = new QGraphicsLineItem(0, 50, 300, 50); scene->addItem(line);
运行如下,起初方块时绿色的,当我们拖动它与直线接触时会变成红色:
在QGraphicsItem类中有三个碰撞检测函数,分别是collidesWithItem()、collidesWithPath()和collidingItems(),我们使用的是第三个。第一个是该图形项是否与指定的图形项碰撞,第二个是该图形项是否与指定的路径碰撞,第三个是返回所有与该图形项碰撞的图形项的列表。在帮助中我们可以查看它们的函数原型和介绍,这里要说明的是,这三个函数都有一个共同的参数Qt::ItemSelectionMode,它指明了怎样去检测碰撞。我们在帮助中进行查看,可以发现它是一个枚举类型,一共有四个值,分别是:
- Qt::ContainsItemShape :只有图形项的shape被完全包含时;
- Qt::IntersectsItemShape :当图形项的shape被完全包含时,或者图形项与其边界相交;
- Qt::ContainsItemBoundingRect : 只有图形项的bounding rectangle被完全包含时;
- Qt::IntersectsItemBoundingRect :图形项的bounding rectangle被完全包含时,或者图形项与其边界相交。
如果我们不设置该参数,那么他默认使用Qt::IntersectsItemShape 。这里所说的shape是指什么呢?在QGraphicsItem类中我们可以找到shape()函数,它返回的是一个QPainterPath对象,也就是说它能确定我们图形项的形状。但是默认的,它只是返回boundingRect()函数返回的矩形的形状。下面我们具体验证一下。在main.cpp函数中添加两行代码:
qDebug() << item->shape(); //输出item的shape信息 qDebug() << item->boundingRect(); //输出item的boundingRect信息
这时运行程序,在下面的程序输出窗口会输出如下信息:
我们发现,现在shape和boundingRect函数里设置的大小是一样的。这时我们在到myitem.cpp中更改函数boundingRect()函数中的内容,将大小由20,改为50:
return QRectF(0 - penWidth/2,0-penWidth/2,50+penWidth,50+penWidth);
运行结果如下:
小方块一出来便成为了红色(之前是绿色),下面的输出信息也显示了shape的大小变成了50(我们画小方块指定的大小为20,但是边界指定的50),它默认按照boundingRect来进行检测了,但是怎样才能使小方块按照它本身的形状,而不是其boundingRect的大小来进行碰撞检测呢?我们需要重新实现shape()函数。
QPainterPath shape() const override;
QPainterPath MyItem::shape() const { QPainterPath path; path.addRect(0,0,20,20); //图形项的真实大小 return path; }
运行结果如下:
现在shape和boundingRect的大小已经不同了,所以对于非矩形的形状,我们都可以利用shape()函数来返回它的真实形状,下面是shape函数的帮助文档介绍,意思就是说该函数以局部坐标形式将此项的形状作为QPainterPath返回。 该形状用于许多事物,包括碰撞检测,命中测试以及QGraphicsScene :: items()函数。
默认实现调用boundingRect()返回简单的矩形形状,但是子类可以重新实现此函数以为非矩形项目返回更准确的形状。 例如,圆形物品可以选择返回椭圆形以更好地进行碰撞检测。
3.6 移动
void advance(int phase) override;
然后在myitem.cpp中对其进行定义:
void MyItem::advance(int phase) { if(!phase) return; //如果phase为0,表示将开始移动则返回 moveBy(0,10); }
在到main.cpp中添加以下定时器代码:
QTimer timer; QObject::connect(&timer, &QTimer::timeout, scene, &QGraphicsScene::advance); timer.start(1000);
运行程序,小方块就会每秒下移一下。
3.7 动画
#include <QGraphicsItemAnimation>
#include <QTimeLine>
然后在构造函数中添加代码:
//新建动画类对象 QGraphicsItemAnimation *anim = new QGraphicsItemAnimation; //将该图形项加入动画类对象中 anim->setItem(this); //新建长为1秒的时间线 QTimeLine *timeLine = new QTimeLine(1000); //动画循环次数为0,表示无限循环 timeLine->setLoopCount(0); //将时间线加入动画类对象中 anim->setTimeLine(timeLine); //在动画时间的一半时图形项旋转180度 anim->setRotationAt(0.5,180); //在动画执行完时图形项旋转360度 anim->setRotationAt(1,360); //开始动画 timeLine->start();
运行程序,小方块会在一秒内旋转一圈并向下移动10个像素,我们这里使用了QGraphicsItemAnimation动画类和QTimeLine时间线类,效果如下: