qt之圖形檢視框架(上)
轉載自:https://wizardforcel.gitbooks.io/qt-beginning/content/24.html
導語
在前面講的基本繪圖中,我們可以自己繪製各種圖形,並且控制它們。但是,如果需要同時繪製很多個相同或不同的圖形,並且要控制它們的移動,檢測它們的碰撞和疊加;或者我們想讓自己繪製的圖形可以拖動位置,進行縮放和旋轉等操作。實現這些功能,要是還使用以前的方法,那麼會十分困難。解決這些問題,可以使用Qt提供的圖形檢視框架。
圖形檢視可以對大量定製的2D圖形項進行管理和相互作用。檢視部件可以讓所有圖形項視覺化,它還提供了縮放和旋轉功能。我們在幫助中搜索Graphics View
關鍵字,內容如下圖:
這裡一開始對這個框架進行了簡單介紹,整個圖形檢視結構主要包含三部分:場景(Scene
)、檢視(View
)和圖形項(Item
),它們分別對應
QGraphicsScene
、QGraphicsView
、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資訊
這時執行程式,在下面的程式輸出視窗會輸出如下資訊:
我們發現,現在shape
和boundingRect
的大小是一樣的。這時我們在到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;
}
這時我們再執行程式,效果如下:
可以看到,現在shape
和boundingRect
的大小已經不同了。所以對於不是矩形的形狀,我們都可以利用shape()
函式來返回它的真實形狀。
(六)移動
對於圖形項的移動,我們有很多辦法實現,也可以在很多層面上對其進行控制,比如說在View
上控制或者在Scene
上控制。但是對於大量的不同型別的圖形項,怎樣能一起控制呢?在圖形檢視框架中提供了advance()
槽函式,這個函式在QGraphicsScene
和QGraphicsItem
中都有,在圖形項類中它的原型是advance(int
phase)
。它的實現流程是,我們利用QGraphicsScene
類的物件呼叫QGraphicsScene
的advance()
函式,這時就會執行兩次該場景中所有圖形項的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)
點。
我們執行程式,效果如下:
結語
這一節先介紹了圖形項的相關內容,而場景、檢視等內容放到下一節來講。
涉及到的原始碼: