一、概念介紹
之前最項目,一直在用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時間線類,效果如下: