當前位置:
首頁 > 最新 > 第20篇 Qt5之2D繪圖圖形視圖框架(下)

第20篇 Qt5之2D繪圖圖形視圖框架(下)

導語

這兩天抽時間看了下最近大火的《摔跤吧!爸爸》,說實話,因為太忙,以前對這個電影沒有一點關注,也沒有看宣傳片,就是正好有時間了,而且看到這個片評分很高,所以沒有多想直接看了,整部影片新奇、好笑也很感動,雖然劇情可能有些套路,但是並不討人厭,話說一部勵志說教電影能拍成這樣,真是出人意料,如果後期知道了阿米爾·汗為了拍這個電影先增肥再減肥的故事,那麼對這部電影真是沒有什麼可吐槽的了。只想說最近兩年的國產電影都把觀眾看睡了,現在卻被一部印度電影感動到了,我們真該想想為什麼會這樣?不光是電影,其他方面也是這樣,比如說我們的教程,是該反思的時候了,什麼時候能把這樣枯燥的事情變成一個大家喜聞樂見的事情,這才是我們應該做的。等我們這50篇教程更新完之後,我們將探索全新的內容和講解方式,大家期待一下吧!

環境

Windows 7 + Qt 5.8.0(包含Qt Creator 4.2.1)

場景QGraphicsScene

QGraphicsScene提供了圖形視圖框架的場景,它有以下功能:

提供了一個管理大量圖形項的快速介面

向每個圖形項傳播事件

管理圖形項的狀態,比如選擇和焦點處理

提供無轉換的渲染功能,主要用於列印

我們新建空的Qt項目(Empty qmake Project),項目名稱為graphicsview03。然後在這個項目中添加新的C++ 源文件,命名為main.cpp,將main.cpp的內容更改如下。

#include

intmain(intargc,char*argv[])

{

QApplicationapp(argc,argv);

QGraphicsScenescene;

scene.addText("Hello,world!");

QGraphicsViewview(&scene);

view.show();

returnapp.exec();

}

因為使用了Qt Widgets模塊,所以需要在graphicsview03.pro文件中添加QT += widgets一行代碼。運行效果如下。

這裡使用addText()函數添加了一個文本圖形項。執行這條語句就相當於執行了下面兩條語句:

QGraphicsTextItem *item = new QGraphicsTextItem("Hello, world!");

scene.addItem(item);

如果要刪除一個圖形項我們可以調用removeItem()函數,如:

scene.removeItem(item);

(一)場景層

一個場景分為三個層:圖形項層(ItemLayer)、前景層(ForegroundLayer)和背景層(BackgroundLayer)。場景的繪製總是從背景層開始,然後是圖形項層,最後是前景層。看下面的例子:

我們在上面的程序中添加代碼:

scene.setForegroundBrush(QColor(255, 255, 255, 100));

scene.setBackgroundBrush(Qt::green);

運行程序,效果如下:

對於前景層,我們一般不進行設置,或者像上面這樣設置為半透明的白色。對於背景層,這裡設置為了綠色,當然,我們也可以將一張圖片設置為背景。

scene.setBackgroundBrush(QPixmap("../graphicsview03/back.jpg"));

運行程序,我們可以看到,圖片默認是平鋪的。

如果想進一步控制前景和背景層,我們可以重新實現drawForeground()函數和drawBackground()函數。

(二)索引演算法

索引演算法,是指在場景中進行圖形項查找的演算法。QGraphicsScene中提供了兩種選擇,它們在一個枚舉類型QGraphicsScene::ItemIndexMethod中,分別是:

QGraphicsSecne::BspTreeIndex:應用Binary Space Partition tree,適合於大量的靜態圖形項。這個是默認值。

QGraphicsScene::NoIndex:不用索引,搜索場景中所有的圖形項,適合於經常進行圖形項的添加、移動和刪除等操作的情況。

我們可以使用setItemIndexMethod()函數進行索引演算法的更改。

(三)邊界矩形

圖形項可以放到場景的任何位置,場景的大小默認是沒有限制的。而場景的邊界矩形僅用於場景內部進行索引的維護。因為如果沒有邊界矩形,場景就要搜索所有的圖形項,然後確定出其邊界,這是十分費時的。所以如果要操作一個較大的場景,我們應該給出它的邊界矩形。設置邊界矩形,可以使用setSceneRect()函數。

(四)圖形項查找

場景最大的優勢之一就是可以快速的鎖定圖形項的位置,即使有上百萬個圖形項,items()函數也能在數毫秒的時間內鎖定一個圖形項的位置。items()函數有幾個重載函數來方便的進行圖形項的查找。但是有時在場景的一個點可能重疊著幾個圖形項,這時我們可以使用itemAt()函數返回最上面的一個圖形項。對於這些函數的使用,我們到後面講視圖時再舉例講解。

(五)事件處理和傳播

場景可以傳播來自視圖的事件,將事件傳播給該點最頂層的圖形項。但是就像我們在講圖形項時所說的那樣,如果一個圖形項要接收鍵盤事件,那麼它必須獲得焦點。而且,如果我們在場景中重寫了事件處理函數,那麼在該函數的最後,必須調用場景默認的事件處理函數,只有這樣,圖形項才能接收到該事件。這一點我們也到後面講視圖時再細講。

(六)列印

該部分內容也放到後面和視圖一起講。

視圖QGraphicsView

QGraphicsView 提供了視圖窗口部件,它使場景的內容可視化。可以給一個場景關聯多個視圖,從而給一個數據集提供多個視口。視圖部件是一個滾動區域,也就是說,它可以提供一個滾動條來顯示大型的場景。如果要使用OpenGL,可以使用QGraphicsView::setViewport()函數來添加QGLWidget 。

(一)縮放與旋轉

在前面項目的基礎上進行修改,再新添一個C++類,類名為MyView,基類為QGraphicsView。 然後在myview.h中添加頭文件:

#include

然後將類聲明修改如下:

classMyView:publicQGraphicsView

{

Q_OBJECT

public:

explicitMyView(QWidget*parent=);

protected:

voidwheelEvent(QWheelEvent*event);

voidmousePressEvent(QMouseEvent*event);

};

我們到myview.cpp文件中進行函數的定義:

MyView::MyView(QWidget*parent):

QGraphicsView(parent)

{

resize(400,400);

setBackgroundBrush(QPixmap("../graphicsview03/back.jpg"));

QGraphicsScene*scene=newQGraphicsScene(this);

scene->setSceneRect(,,200,200);

QGraphicsRectItem*item=newQGraphicsRectItem(,,20,20);

item->setBrush(Qt::red);

scene->addItem(item);

setScene(scene);

}

//滾輪事件

voidMyView::wheelEvent(QWheelEvent*event)

{

if(event->delta()>)

scale(0.5,0.5);//視圖縮放

elsescale(2,2);

}

voidMyView::mousePressEvent(QMouseEvent*event)

{

rotate(90);//視圖旋轉順時針90度

}

這裡我們定義了滑鼠的滾輪事件和按下事件,在滾輪事件中,利用delta()函數返回值的正負來判斷滾輪的移動方向,然後我們讓視圖進行縮放。最後到main.cpp文件中,更改其內容如下:

#include"myview.h"

intmain(intargc,char*argv[])

{

QApplicationapp(argc,argv);

MyView*view=newMyView;

view->show();

returnapp.exec();

}

我們運行程序,效果如下:

上面四幅圖分別是:正常,旋轉90度後,縮小後,放大後的效果。可以看到實現視圖的變換是十分簡單的。

(二)場景邊框與場景對齊方式

我們在上面講場景時就提到了場景邊框(SceneRect),這裡再說說它在視圖中的作用。我們前面說過,視圖是可以提供滾動條的,但是,這只是在視圖窗口小於場景時才自動出現的。如果我們不定義場景邊框,那麼當場景中的圖形項移動到視圖可視窗口以外的地方時,視圖就會自動出現滾動條,但是即使是圖形項再次回到可視區域,滾動條也不會消失。為了解決這個問題,我們可以為場景設置邊框,這樣,當圖形項移動到場景邊框以外時,視圖是不會提供額外的滾動區域的。

而當整個場景都可視時,也就是說視圖沒有滾動條時,我們可以通過setAlignment()函數來設置場景在視圖中的對齊方式,如左對齊Qt::AlignLeft,向上對齊Qt::AlignTop,中心對齊Qt::AlignCenter。更多的對齊方式,可以查看幫助中Qt::Alignment關鍵字。默認的對齊方式是Qt::AlignCenter。而且幾種對齊方式可以通過「按位或」操作一起使用。我們在上面的程序中的myview.cpp文件中的構造函數最後添加一行代碼:setAlignment(Qt::AlignLeft Qt::AlignTop);

運行效果如下圖所示。

(三)拖動模式

在QGraphicView中提供了三種拖動模式,分別是:

QGraphicsView::NoDrag:忽略滑鼠事件,不可以拖動。

QGraphicsView::ScrollHandDrag:游標變為手型,可以拖動場景進行移動。

QGraphicsView::RubberBandDrag:使用橡皮筋效果,進行區域選擇,可以選中一個區域內的所有圖形項。

我們可以利用setDragMode()函數進行相應設置。下面更改前面的程序,在myview.cpp中的構造函數中的最後添加代碼:

setDragMode(QGraphicsView::ScrollHandDrag);//手型拖動

scene->setSceneRect(,,800,800);

這時運行程序,雖然出現了小手,但是並不能拖動場景。為什麼呢?我們在mousePressEvent()函數中添加一行代碼:

QGraphicsView::mousePressEvent(event);

這時再運行程序,發現已經成功了。效果如下:

這裡在事件函數的最後添加了一行:

QGraphicsView::mousePressEvent(event);

這樣程序才能執行默認的事件。這也是我們下面要說的事件傳播的內容的一部分。

(四)事件傳遞

在上面我們看到必須在事件函數的最後將event參數傳遞出去,才能執行默認的事件操作。其實不止上面那一種情況,在圖形視圖框架中,滑鼠鍵盤等事件是從視圖進入的,視圖將它們傳遞給場景,場景再將事件傳遞給該點的圖形項,如果該點有多個圖形項,那麼就傳給最上面的圖形項。所以要想使這個事件能一直傳播下去,我們就需要在重新實現事件處理函數時,在其最後將event參數傳給默認的事件處理函數。比如我們重寫了場景的鍵盤按下事件處理函數,那麼我們就在該函數的最後寫上:

QGraphicsScene::keyPressEvent(event);

一行代碼。

(五)背景緩存

如果場景的背景需要大量耗時的渲染,可以利用CacheBackground來緩存背景,當下次需要渲染背景時,可以快速進行渲染。它的原理就是,把整個視口先繪製到一個pixmap上。但是這個只適合較小的視口,也就是說,如果視圖窗口很大,而且有滾動條,那麼就不再適合緩存背景。我們可以使用setCacheMode(QGraphicsView::CacheBackground);來設置背景緩存。默認設置是沒有緩存QGraphicsView::CacheNone 。

(六)OpenGL渲染

QGraphicsView默認使用一個QWidget作為視口部件,如果我們要使用OpenGL進行渲染,可以使用setViewport()函數來添加一個QGLWidget對象。看下面的例子。我們先在項目文件graphicsview03.pro中加入:

QT += opengl

說明要使用OpenGL模塊,然後在myview.cpp文件中添加頭文件:

#include

最後在構造函數中加入代碼:

QGLWidget*widget=newQGLWidget(this);

setViewport(widget);

這樣便使用OpenGL進行渲染了。

(七)圖形項查找與圖形項組

在前面講場景時,我們就涉及了圖形項查找的內容,當時沒有細講,現在我們把它和圖形項組放到一起來講解。先看一個例子,然後再介紹。

在myview.cpp中的構造函數里將以前那個item改名為item1,然後再加入一個item2和一個圖形項組對象group。更改後構造函數的代碼如下:

MyView::MyView(QWidget*parent):

QGraphicsView(parent)

{

resize(400,400);

setBackgroundBrush(QPixmap("../graphicsview03/back.jpg"));

QGraphicsScene*scene=newQGraphicsScene(this);

scene->setSceneRect(,,200,200);

QGraphicsRectItem*item1=newQGraphicsRectItem(,,20,20);

item1->setBrush(Qt::red);

item1->setPos(10,);

scene->addItem(item1);

QGraphicsRectItem*item2=newQGraphicsRectItem(,,20,20);

item2->setBrush(Qt::green);

item2->setPos(40,);

scene->addItem(item2);

//新建圖形項組

QGraphicsItemGroup*group=newQGraphicsItemGroup;

group->addToGroup(item1);

group->addToGroup(item2);

scene->addItem(group);

setScene(scene);

setAlignment(Qt::AlignLeftQt::AlignTop);

//手型拖動

setDragMode(QGraphicsView::ScrollHandDrag);

scene->setSceneRect(,,800,800);

QGLWidget*widget=newQGLWidget(this);

setViewport(widget);

//輸出(10,0)點的圖形項

qDebug()

qDebug()

qDebug()

}

然後我們到myview.h文件中protected部分聲明鍵盤按下事件槽函數:

voidkeyPressEvent(QKeyEvent*event);

再到myview.cpp中定義它,如下:

voidMyView::keyPressEvent(QKeyEvent*event)

{

//輸出場景中所有的圖形項

qDebug()

items().at()->setPos(100,);

items().at(1)->setPos(,100);

//執行默認的事件處理

QGraphicsView::keyPressEvent(event);

}

這時運行程序,當按下鍵盤上任意鍵後,效果如下:

可以看到,itemAt()函數可以輸出場景上任意點的圖形項。而items()函數可以輸出場景上所有的圖形項。這裡應該說明,items()函數返回的圖形項列表是按棧的降序排序的,也就是說,items().at(0)返回的是最後加入場景的圖形項。從上面可以看出,最後加入的圖形項是item2,其實,因為我們使用了group,而item1和item2都在group里,所以我們只需將group加入場景中就可以了,前面把item1和item2也加入場景是多餘的。我們可以將scene->addItem(item1);和scene->addItem(item2);兩行代碼刪掉。那麼這時加入場景的順序就是,先加入group,因為item1先加入group,所以下面將item1加入場景,最後加入場景的是item2,這就是為什麼items.at(0)會是item2的原因。

下面再說圖形項組,其實圖形項組也是一個圖形項,它有圖形項所擁有的所有特性。其作用就是,將加入它的所有圖形項作為一個整體,對這個圖形項組進行操作,就相當於對齊中所有圖形項進行操作。圖形項組是加入它的所有圖形項的父圖形項,在上面的輸出的parent信息中我們可以看到這一點。下面我們將程序中的代碼更改如下:

voidMyView::keyPressEvent(QKeyEvent*event)

{

items().at(2)->setPos(100,100);

QGraphicsView::keyPressEvent(event);

}

運行程序,按下鍵盤上任意鍵,效果如下:

可以看到,兩個圖形項是同時移動的。我們要從圖形項組中移除一個圖形項,可以使用removeFromGroup()函數,它可以將給定的item從group中刪除,要注意這時item依然存在,它會回到group的父圖形項中,如果group沒有父圖形項,那麼item就會回到場景中。我們可以使用場景的removeItme()函數來刪除group,這樣也會將group中所有的圖形項從場景中刪除。還有一種辦法是利用場景的destroyItemGroup()函數,它會刪除group並銷毀它,但是group中的所有圖形項會回到group的父圖形項中,如果它沒有父圖形項,那麼所有圖形項就會回到場景中。

(八)列印

圖形視圖框架提供了兩個列印函數render(),一個是在QGraphicsScene中,一個是在QGraphicsView中,並且它們的函數原型是一模一樣的。不過它們實現的效果稍有不同。看一面的例子。我們更改滑鼠按下事件槽函數的內容如下:

voidMyView::mousePressEvent(QMouseEvent*event)

{

rotate(90);//視圖旋轉順時針90度

QPixmappixmap(400,400);//必須指定大小

QPainterpainter(&pixmap);

render(&painter,QRectF(,,400,400),

QRect(,,400,400));//列印視圖指定區域內容

pixmap.save("../graphicsview03/save.png");

QGraphicsView::mousePressEvent(event);

}

這裡我們使用了視圖的render()函數,其中的QRectF參數是指設備的區域,這裡是指pixmap。而QRect參數是指視圖上要列印的區域。我們利用QPixmap類的save()函數,將pixmap圖片保存到我們項目源碼目錄中,文件名為「save.png」。下面是運行程序後,點擊滑鼠,生成的圖片的效果:

我們每點擊一次滑鼠,就會旋轉視圖,那麼生成的圖片就是當前視口的截圖。下面我們使用場景的列印函數,將上面的列印一行的代碼改為:

scene()->render(&painter,QRectF(,,400,400),

QRect(,,400,400));//列印場景內容

運行程序,看圖片效果:

這時無論視圖怎樣變換,生成的圖片總是一樣的。而且它並沒有列印背景的圖片。就像我們看到的,視圖的列印函數是依據視圖的坐標系進行列印的,我們看到的就是列印出來後的效果,它可以看做是程序窗口的截屏。而場景的列印函數,是依據場景的坐標系的,無論視圖怎麼轉換,只要場景坐標系沒有變換,它列印出來的圖片都是一樣的。

結語

圖形視圖框架是一個非常強大而且龐雜的系統,我們教程中也只是很籠統的介紹了一些最基本最常用的內容。想深入學習,大家還是要進行大量的實戰練習的。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!

TAG: |

您可能感興趣

36張二戰時期戰鬥機和攻擊機內視圖!第14天:MC.200/202/205飛機
銷售易V1804新功能詳解:「角色化首頁」與「360°客戶視圖」
36張二戰時期戰鬥機和攻擊機內視圖!第16天:Bf-109G/K飛機
36張二戰時期戰鬥機和攻擊機內視圖!第15天:Bf-109E飛機
36張二戰時期戰鬥機和攻擊機內視圖!第17天:Bf-110飛機
36張二戰時期戰鬥機和攻擊機內視圖!第18天:Me-163「彗星」飛機
36張二戰時期戰鬥機和攻擊機內視圖!第9天:He-162「火怪」
36張二戰時期戰鬥機和攻擊機內視圖!第1天:P-39「空中眼鏡蛇」
EF Core 2.1路線圖:視圖、GROUP BY和惰性載入
36張二戰時期戰鬥機和攻擊機內視圖!第19天:A6M「齊克」
IJCAI 2019 | 整合多類信息,阿里文娛提出多視圖多標記演算法SIMM
36張二戰時期戰鬥機和攻擊機內視圖!第23天:F4U「海盜」式飛機
微軟照片Win10版即將加入「時間軸」視圖
36張二戰時期戰鬥機和攻擊機內視圖!第10天:「巴克」/「野獸」
全高清IPS硬屏 微鯨43D液晶電視圖賞
美國陸軍M1A1主戰坦克四視圖
002俯視圖公開 與遼寧艦對比一目了然:戰機出動能力超30%
002俯視圖公開!比遼寧艦強多少一目了然:戰機出動力超30%
DOTA梗百科, 四大皆空.A盾行者.萬磁王.少林寺等導視圖
《魔道書7使者》劇場版第2彈公開主視圖、特報影像