1. 程式人生 > >qt之圖形檢視框架(上)

qt之圖形檢視框架(上)

轉載自:https://wizardforcel.gitbooks.io/qt-beginning/content/24.html

導語

在前面講的基本繪圖中,我們可以自己繪製各種圖形,並且控制它們。但是,如果需要同時繪製很多個相同或不同的圖形,並且要控制它們的移動,檢測它們的碰撞和疊加;或者我們想讓自己繪製的圖形可以拖動位置,進行縮放和旋轉等操作。實現這些功能,要是還使用以前的方法,那麼會十分困難。解決這些問題,可以使用Qt提供的圖形檢視框架。

圖形檢視可以對大量定製的2D圖形項進行管理和相互作用。檢視部件可以讓所有圖形項視覺化,它還提供了縮放和旋轉功能。我們在幫助中搜索Graphics View關鍵字,內容如下圖:

這裡一開始對這個框架進行了簡單介紹,整個圖形檢視結構主要包含三部分:場景(Scene)、檢視(View)和圖形項(Item),它們分別對應 QGraphicsSceneQGraphicsView 、QGraphicsItem三個類。其實圖形檢視框架是一組類的集合,在幫助中可以看到所有與它相關的類。下面我們就開始結合程式對整個框架進行介紹。

環境:Windows Xp + Qt 4.8.4+QtCreator 2.6.2

目錄

  • 一、基本應用
  • 二、圖形項(QGraphicsItem)
    • (一)自定義圖形項
    • (二)游標和提示
    • (三)拖放
    • (四)鍵盤與滑鼠事件
    • (五)碰撞檢測
    • (六)移動
    • (七)動畫
    • (八)右鍵選單

正文

一、基本應用

我們新建空的Qt專案(在其他專案中),專案名稱為graphicsView01。然後在這個專案中新增新的C++ 原始檔,命名為main.cpp

我們將main.cpp的內容更改如下。

#include <QtGui>

int main(int argc,char* argv[ ])
{
   QApplication app(argc,argv);

   QGraphicsScene *scene = new QGraphicsScene;  //場景
   QGraphicsRectItem *item = new QGraphicsRectItem(100,100,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進行分組
  • 碰撞檢測

(一)自定義圖形項

1.在前面的專案中新增新的C++類,類名設為 MyItem,基類設為QGraphicsItem

2.然後,我們在myitem.h檔案中新增標頭檔案#include <QtGui>。(說明:QtGui模組裡面包含了所有圖形介面類,所以為了簡便,這裡只包含了該標頭檔案,正式開發程式時不推薦這麼做!)

3.再新增兩個函式的宣告:

QRectFboundingRect() const;
voidpaint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget*widget);

4.下面到myitem.cpp中對兩個函式進行定義:

QRectFMyItem::boundingRect() const
{
    qreal penWidth = 1;
    return QRectF(0 - penWidth / 2, 0 -penWidth / 2,
               20 + penWidth, 20 + penWidth);
}


voidMyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget)
{
Q_UNUSED(option);  //標明該引數沒有使用
    Q_UNUSED(widget);
    painter->setBrush(Qt::red);
    painter->drawRect(0,0,20,20);
}

5.下面到main.cpp中新增#include "myitem.h"

然後將以前那個矩形項的定義語句改為:

MyItem *item =new MyItem;

執行程式,效果如下:

可以看到,我們要繼承QGraphicsItem類實現自定義的圖形項,必須先實現兩個純虛擬函式boundingRect()paint(),前者用於定義Item的繪製範圍,後者用於繪製圖形項。其實boundingRect()還有很多用途,後面會涉及到。

(二)游標和提示

1.在myitem.cpp 中的建構函式中新增兩行程式碼,如下:

MyItem::MyItem()
{
   setToolTip("Click and drag me!");  //提示
   setCursor(Qt::OpenHandCursor);   //改變游標形狀
}

然後執行程式,效果如下:

當游標放到小方塊上時,游標變為了手型,並且彈出了提示。更多的游標形狀可以檢視Qt::CursorShape,我們也可以使用圖片自定義游標形狀。

(三)拖放

下面寫這樣一個程式,有幾個不同顏色的圓形和一個大矩形,我們可以拖動圓形到矩形上,從而改變矩形的顏色為該圓形的顏色。

1.將上面的程式進行改進,用來實現圓形圖形項。

myitem.h中新增一個私有變數和幾個鍵盤事件處理函式的宣告:

protected:
   void mousePressEvent(QGraphicsSceneMouseEvent *event);
   void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
   void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
private:
   QColor color;

2.然後到myitem.cpp中,在建構函式中初始化顏色變數:

color = QColor(qrand() % 256, qrand() %256, qrand() % 256); //初始化隨機顏色

paint()函式中將繪製矩形的程式碼更改如下:

painter->setBrush(color);
painter->drawEllipse(0, 0, 20, 20);

3.下面我們定義幾個鍵盤事件處理函式:

voidMyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
   if(event->button() != Qt::LeftButton)
    {
       event->ignore();  //如果不是滑鼠左鍵按下,則忽略該事件
       return;
    }
   setCursor(Qt::ClosedHandCursor); //如果是滑鼠左鍵按下,改變游標形狀
}


voidMyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
   if(QLineF(event->screenPos(),event->buttonDownScreenPos(Qt::LeftButton))
       .length() < QApplication::startDragDistance())
{
//如果滑鼠按下的點到現在的點的距離小於程式預設的拖動距離,表明沒有拖動,則返回
       return;
    }
   QDrag *drag = new QDrag(event->widget()); //為event所在視窗部件新建拖動物件
   QMimeData *mime = new QMimeData; //新建QMimeData物件,它用來儲存拖動的資料
   drag->setMimeData(mime); //關聯
   mime->setColorData(color);  //放入顏色資料

   QPixmap pix(21,21);  //新建QPixmap物件,它用來重新繪製圓形,在拖動時顯示
   pix.fill(Qt::white);
   QPainter painter(&pix);
   paint(&painter,0,0);
   drag->setPixmap(pix);

   drag->setHotSpot(QPoint(10, 15)); //我們讓指標指向圓形的(10,15)點
   drag->exec();  //開始拖動
   setCursor(Qt::OpenHandCursor);   //改變游標形狀
}


voidMyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
   setCursor(Qt::OpenHandCursor);   //改變游標形狀
}

此時執行程式,效果如下:

4.下面我們新添一個類,它用來提供矩形圖形項,並且可以接收拖動的資料。在myitem.h中,我們加入該類的宣告:

class RectItem : public QGraphicsItem
{
public:
   RectItem();
   QRectF boundingRect() const;
   void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
protected:
    void dragEnterEvent(QGraphicsSceneDragDropEvent *event); //拖動進入事件
    void dragLeaveEvent(QGraphicsSceneDragDropEvent *event); //拖動離開事件
    void dropEvent(QGraphicsSceneDragDropEvent *event);  //放入事件
private:
    QColor color;
    bool dragOver;  //標誌是否有拖動進入
};

5.然後進入myitem.cpp進行相關函式的定義:

RectItem::RectItem()
{
   setAcceptDrops(true);  //設定接收拖放
   color = QColor(Qt::lightGray);
}

QRectF RectItem::boundingRect() const
{
    return QRectF(0, 0, 50, 50);
}

void RectItem::paint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setBrush(dragOver? color.light(130) : color);  //如果其上有拖動,顏色變亮
   painter->drawRect(0,0,50,50);
}


voidRectItem::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
   if(event->mimeData()->hasColor()) //如果拖動的資料中有顏色資料,便接收
    {
       event->setAccepted(true);
       dragOver = true;
       update();
    }
   else event->setAccepted(false);
}

voidRectItem::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
{
   Q_UNUSED(event);
   dragOver = false;
   update();
}

void RectItem::dropEvent(QGraphicsSceneDragDropEvent*event)
{
   dragOver = false;
if(event->mimeData()->hasColor())
//我們通過型別轉換來獲得顏色
       color =qVariantValue<QColor>(event->mimeData()->colorData());
   update();
}

6.下面進入main.cpp檔案,更改main()函式中的內容如下:

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();
}

這是執行程式,效果如下:

這時我們已經實現了想要的效果。可以看到,要想實現拖放,必須源圖形項和目標圖形項都進行相關設定。在源圖形項的滑鼠事件中新建並執行拖動,而在目標圖形項中必須指定setAcceptDrops(true); 這個函式,這樣才能接收拖放,然後需要實現拖放的幾個事件處理函式。

(四)鍵盤與滑鼠事件

1.新建專案graphicsView02,然後按照(一)中自定義圖形項進行操作(可以直接把那裡的程式碼拷貝過來)。下面我們先來看鍵盤事件。

2.在myitem.h檔案中宣告鍵盤按下事件處理函式:

protected:
voidkeyPressEvent(QKeyEvent *event);

然後在myitem.cpp中進行定義:

void MyItem::keyPressEvent(QKeyEvent*event)
{
   moveBy(0, 10);  //相對現在的位置移動
}

這時執行程式,發現無論怎樣方塊都不會移動。其實要想使圖形項接收鍵盤事件,就必須使其可獲得焦點。我們在建構函式裡新增一行程式碼:

setFlag(QGraphicsItem::ItemIsFocusable);  //圖形項可獲得焦點

(我們在新建圖形項時指定也是可以的,如item->setFlag(QGraphicsItem::ItemIsFocusable);

這時執行程式,然後用滑鼠點選一下方塊,再按下任意按鍵,方塊就會向下移動。效果如下圖所示。

3.再看滑鼠事件。我們先在myitem.h檔案中宣告滑鼠按下事件處理函式:

voidmousePressEvent(QGraphicsSceneMouseEvent *event);

然後再myitem.cpp檔案中對其進行定義:

voidMyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
   moveBy(10,0);
}

此時執行程式,點選小方塊,它便會向右移動。如果我們想讓滑鼠可以拖動小方塊,那麼我們可以重新實現mouseMoveEvent()函式,還有一種更簡單的方法是,我們在建構函式中指明該圖形項是可移動的:

setFlag(QGraphicsItem::ItemIsMovable);

(當然我們也可以在新建圖形項時指定它)

執行程式,效果如下:

(五)碰撞檢測

下面先看一個例子,再進行講解。

我們將上面程式中myitem.cpp檔案中的paint()函式中的設定畫刷的程式碼更改如下:

//如果與其他圖形項碰撞則顯示紅色,否則顯示綠色
painter->setBrush(!collidingItems().isEmpty()?Qt::red : Qt::green);

然後再main.cpp檔案中在場景中新增一個直線圖形項:

QGraphicsLineItem *line = newQGraphicsLineItem(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:圖形項的boundingrectangle被完全包含時,或者圖形項與其邊界相交。

如果我們不設定該引數,那麼他預設使用Qt::IntersectsItemShape。這裡所說的shape是指什麼呢?在QGraphicsItem類中我們可以找到shape()函式,它返回的是一個QPainterPath物件,也就是說它能確定我們圖形項的形狀。但是預設的,它只是返回boundingRect()函式返回的矩形的形狀。下面我們具體驗證一下。

main.cpp函式中新增兩行程式碼:

qDebug()<< item->shape();   //輸出item的shape資訊
qDebug()<< item->boundingRect();  //輸出item的boundingRect資訊

這時執行程式,在下面的程式輸出視窗會輸出如下資訊:

我們發現,現在shapeboundingRect的大小是一樣的。這時我們在到myitem.cpp中更改函式boundingRect()函式中的內容,將大小由20,改為50:

return QRectF(0 - penWidth / 2, 0 -penWidth / 2,
               50 + penWidth, 50 + penWidth);

這時再次執行程式,效果如下:

小方塊一出來便成為了紅色,下面的輸出資訊也顯示了,現在shape的大小也變成了50。怎樣才能使小方塊按照它本身的形狀,而不是其boundingRect的大小來進行碰撞檢測呢?我們需要重新實現shape()函式。

myitem.h中,我們在public裡進行函式宣告:QPainterPath shape() const;

然後到myitem.cpp中進行其定義:

QPainterPath MyItem::shape() const
{
   QPainterPath path;
   path.addRect(0,0,20,20);  //圖形項的真實大小
   return path;
}

這時我們再執行程式,效果如下:

可以看到,現在shapeboundingRect的大小已經不同了。所以對於不是矩形的形狀,我們都可以利用shape()函式來返回它的真實形狀。

(六)移動

對於圖形項的移動,我們有很多辦法實現,也可以在很多層面上對其進行控制,比如說在View上控制或者在Scene上控制。但是對於大量的不同型別的圖形項,怎樣能一起控制呢?在圖形檢視框架中提供了advance()槽函式,這個函式在QGraphicsSceneQGraphicsItem中都有,在圖形項類中它的原型是advance(int phase)。它的實現流程是,我們利用QGraphicsScene類的物件呼叫QGraphicsSceneadvance()函式,這時就會執行兩次該場景中所有圖形項的advance(int phase)函式,第一次phase為0,告訴所有圖形項即將要移動;第二次phase的值為1,這時執行移動。下面我們看一個例子。

我們在myitem.h中的protected中宣告函式:void advance(int phase);

然後在myitem.cpp中對其進行定義:

void MyItem::advance(int phase)
{
   if(!phase) return;  //如果phase為0,則返回
   moveBy(0,10);
}

在到main.cpp中新增以下程式碼:

QTimer timer;
QObject::connect(&timer, SIGNAL(timeout()),scene, SLOT(advance()));
timer.start(1000);

這時執行程式,小方塊就會每秒下移一下。

(七)動畫

其實實現圖形項的動畫效果,也可以在不同的層面進行。如果我們只想控制一兩個圖形項的動畫,一般在場景或檢視中實現。但是要是想讓一個圖形項類的多個物件都進行同樣的動畫,那麼我們就可以在圖形項類中進行實現。我們先看一個例子。

myitem.cpp檔案中的建構函式中新增程式碼:

MyItem::MyItem()
{
   setFlag(QGraphicsItem::ItemIsFocusable); //圖形項可獲得焦點
   setFlag(QGraphicsItem::ItemIsMovable); //圖形項可移動
   QGraphicsItemAnimation *anim = new QGraphicsItemAnimation; //新建動畫類物件
   anim->setItem(this);  //將該圖形項加入動畫類物件中
   QTimeLine *timeLine = new QTimeLine(1000);  //新建長為1秒的時間線
   timeLine->setLoopCount(0);  //動畫迴圈次數為0,表示無限迴圈
   anim->setTimeLine(timeLine); //將時間線加入動畫類物件中
   anim->setRotationAt(0.5,180); //在動畫時間的一半時圖形項旋轉180度
   anim->setRotationAt(1,360);  //在動畫執行完時圖形項旋轉360度
   timeLine->start();  //開始動畫
}

這時執行程式,效果如下:

小方塊會在一秒內旋轉一圈。我們這裡使用了QGraphicsItemAnimation動畫類和QTimeLine時間線類,關於這些內容我們會在後面的動畫框架中細講,所以在這裡就不再介紹。

(八)右鍵選單

圖形項支援右鍵選單,不過QGraphicsItem類並不是從QObject類繼承而來的,所以它預設不能使用訊號和槽機制,為了能使用訊號和槽,我們需要將我們的MyItem類進行多重繼承。

myitem.h中,將MyItem類改為

class MyItem : public QObject, publicQGraphicsItem
{
    Q_OBJECT    //進行巨集定義
    … …
}

這樣我們就可以使用訊號和槽機制了,這時我們在下面新增一個槽:

public slots:
   void moveTo(){setPos(0,0);}

因為其實現的功能很簡單,我們在宣告它的同時進行了定義,就是讓圖形項移動到(0,0)點。然後我們在protected中宣告右鍵選單事件處理函式:

voidcontextMenuEvent(QGraphicsSceneContextMenuEvent *event);

最後我們在myitem.cpp檔案中對該事件處理函式進行定義:

voidMyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
   QMenu menu;
   QAction *action = menu.addAction("moveTo(0,0)");
   connect(action,SIGNAL(triggered()),this,SLOT(moveTo()));
   menu.exec(event->screenPos()); //在按下滑鼠左鍵的地方彈出選單
}

這裡我們只是設定了一個選單,當按下該選單是,圖形項移動到(0,0)點。

我們執行程式,效果如下:

結語

這一節先介紹了圖形項的相關內容,而場景、檢視等內容放到下一節來講。

涉及到的原始碼: