[Qt教程] 第19篇 2D繪圖(九)圖形視圖框架(上)
樓主
發表於 2013-5-4 15:26:20 | 查看:
1798| 回複: 26

圖形視圖框架(上)
版權聲明
導語
在前麵講的基本繪圖中,我們可以自己繪製各種圖形,並且控製它們。但是,如果需要同時繪製很多個相同或不同的圖形,並且要控製它們的移動,檢測它們的碰撞和疊加;或者我們想讓自己繪製的圖形可以拖動位置,進行縮放和旋轉等操作。實現這些功能,要是還使用以前的方法,那麼會十分困難。解決這些問題,可以使用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支持以下功能:
(一)自定義圖形項
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::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)點。
我們運行程序,效果如下:
![]() 結語
這一節先介紹了圖形項的相關內容,而場景、視圖等內容放到下一節來講。
涉及到的源碼: ![]() ![]() |
最後更新:2017-04-03 14:54:15