接着上一章的部分,本章介紹圖形視圖框架中的視圖和場景部分,三者關系本章就不再講了,請參考上一章內容
一 場景(QGaphicsScene)
- 提供了一個管理大量圖形項的快速接口
- 向每個圖形項傳播事件
- 管理圖形項的狀態,比如選擇和焦點處理
- 提供無轉換的渲染功能,主要用於打印
#include <QApplication>
#include <QtWidgets> int main(int argc,char**argv) { QApplication app(argc,argv); QGraphicsScene scene; scene.addText("Hello, world!"); QGraphicsView view(&scene); view.show(); return app.exec(); }
運行效果如下:
代碼使用addText()函數添加了一個文本圖形項。執行這條語句就相當於執行了下面兩條語句:
QGraphicsTextItem *item = new QGraphicsTextItem("Hello,world!"); scene.addItem(item);
1.1 場景層
一個場景分為三個層:圖形項層(ItemLayer)、前景層(ForegroundLayer)和背景層(BackgroundLayer)。場景的繪制總是從背景層開始,然后是圖形項層,最后是前景層。我們修改代碼如下:
#include <QApplication> #include <QtWidgets> int main(int argc,char**argv) { QApplication app(argc,argv); QGraphicsScene scene; scene.addText("Hello, world!"); scene.setForegroundBrush(QColor(255, 255, 255, 100)); scene.setBackgroundBrush(Qt::green); QGraphicsView view(&scene); view.show(); return app.exec(); }
運行結果如下:
對於前景層,我們一般不進行設置,或者像上面這樣設置為半透明的白色。對於背景層,這里設置為了綠色,當然,我們也可以將一張圖片設置為背景,修改代碼如下:
scene.setBackgroundBrush(QPixmap(":/20200212141848.png"));
運行結果如下:
圖片默認是平鋪的,如果想進一步控制前景和背景層,我們可以重新實現drawForeground()函數和drawBackground()函數。
1.2 索引算法
索引算法,是指在場景中進行圖形項查找的算法。QGraphicsScene中提供了兩種選擇,它們在一個枚舉類型QGraphicsScene::ItemIndexMethod中,分別是:
- QGraphicsSecne::BspTreeIndex :應用Binary Space Partition tree,適合於大量的靜態圖形項。這個是默認值。
- QGraphicsScene::NoIndex :不用索引,搜索場景中所有的圖形項,適合於經常進行圖形項的添加、移動和刪除等操作的情況。
我們可以使用setItemIndexMethod()函數進行索引算法的更改。
1.3 邊界矩形
圖形項可以放到場景的任何位置,場景的大小默認是沒有限制的。而場景的邊界矩形僅用於場景內部進行索引的維護(能很快進行索引查找圖元,不設置會通過itemsBoundingRect默認生成,但是不方便我們后續操作,一般我們設置了邊界矩形后續索引可以只關注這個矩形區域);因為如果沒有邊界矩形,場景就要搜索所有的圖形項,然后確定出其邊界,這是十分費時的。所以如果要操作一個較大的場景,我們應該給出它的邊界矩形。設置邊界矩形,可以使用setSceneRect()函數。
注意:可以通過itemsBoundingRect()來得知現在場景的大小,itemsBoundingRect()是一個比較耗時的功能,它的運作通過收集場景中所有元素的位置信息找出其中最遠坐標的位置,所以最好先設置好自己的場景大小不要使用默認大小。
1.4 圖形項查找
場景最大的優勢之一就是可以快速的鎖定圖形項的位置,即使有上百萬個圖形項,items()函數也能在數毫秒的時間內鎖定一個圖形項的位置。items()函數有幾個重載函數來方便的進行圖形項的查找。但是有時在場景的一個點可能重疊着幾個圖形項,這時我們可以使用itemAt()函數返回最上面的一個圖形項。對於這些函數的使用,我們到后面講視圖時再舉例講解。
1.5 事件處理和傳播
場景可以傳播來自視圖的事件,將事件傳播給該點最頂層的圖形項。但是就像我們在講圖形項時所說的那樣,如果一個圖形項要接收鍵盤事件,那么它必須獲得焦點。而且,如果我們在場景中重寫了事件處理函數,那么在該函數的最后,必須調用場景默認的事件處理函數,只有這樣,圖形項才能接收到該事件。這一點我們也到后面講視圖時再細講。
二、視圖(QGraphicsView)
QGraphicsView 提供了視圖窗口部件,它使場景的內容可視化。可以給一個場景關聯多個視圖,從而給一個數據集提供多個視口。視圖部件是一個滾動區域,也就是說,它可以提供一個滾動條來顯示大型的場景。如果要使用OpenGL,可以使用QGraphicsView::setViewport()函數來添加QGLWidget 。
2.1 縮放與旋轉
在前面項目基礎上,我們添加處理類,代碼如下:
#ifndef MYVIEW_H #define MYVIEW_H #include <QGraphicsView> #include <QWheelEvent > class MyView : public QGraphicsView { public: MyView(); protected: void wheelEvent(QWheelEvent *event)override;//鼠標滾輪事件 void mousePressEvent(QMouseEvent *event) override; }; #endif // MYVIEW_H
#include "MyView.h" #include <QGraphicsRectItem> MyView::MyView() { resize(400, 400); setBackgroundBrush(QPixmap(":/20200212141848.png")); QGraphicsScene *scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, 100, 100); QGraphicsRectItem *item = new QGraphicsRectItem(0, 0, 20, 20); item->setBrush(Qt::red); scene->addItem(item); setScene(scene); } void MyView::wheelEvent(QWheelEvent *event) { //利用delta()函數返回值的正負來判斷滾輪的移動方向,然后我們讓視圖進行縮放 if(event->delta() > 0) scale(0.5,0.5); //視圖縮放 else scale(2,2); } void MyView::mousePressEvent(QMouseEvent *event) { rotate(90); //視圖旋轉順時針90度 }
#include "MyView.h" #include <QApplication> #include <QtWidgets> int main(int argc,char**argv) { QApplication app(argc,argv); MyView *view = new MyView; view->show(); return app.exec(); }
運行效果如下:
2.2 場景邊框與場景對齊方式
上面的窗口大小看的一臉懵逼,別急,在文章最后我們會講解坐標系統,我們在上面講場景時就提到了場景邊框(SceneRect),這里再說說它在視圖中的作用。我們前面說過,視圖是可以提供滾動條的,但是,這只是在視圖窗口小於場景時才自動出現的。如果我們不定義場景邊框,那么當場景中的圖形項移動到視圖可視窗口以外的地方時,視圖就會自動出現滾動條,但是即使是圖形項再次回到可視區域,滾動條也不會消失。為了解決這個問題,我們可以為場景設置邊框,這樣,當圖形項移動到場景邊框以外時,視圖是不會提供額外的滾動區域的。
而當整個場景都可視時,也就是說視圖沒有滾動條時,我們可以通過setAlignment()函數來設置場景在視圖中的對齊方式,如左對齊Qt::AlignLeft ,向上對齊Qt::AlignTop ,中心對齊Qt::AlignCenter。更多的對齊方式,可以查看幫助中Qt::Alignment 關鍵字。默認的對齊方式是Qt::AlignCenter 。而且幾種對齊方式可以通過“按位或”操作一起使用。我們在上面的程序中的myview.cpp文件中的構造函數最后添加一行代碼:
setAlignment(Qt::AlignLeft | Qt::AlignTop);
運行效果如下:
2.3 拖動模式
QGraphicView中提供了三種拖動模式,分別是:
- QGraphicsView::NoDrag :忽略鼠標事件,不可以拖動。
- QGraphicsView::ScrollHandDrag :光標變為手型,可以拖動場景進行移動。
- QGraphicsView::RubberBandDrag :使用橡皮筋效果,進行區域選擇,可以選中一個區域內的所有圖形項。
我們可以利用setDragMode()函數進行相應設置。下面更改前面的程序,在myview.cpp中的構造函數中的最后添加代碼:
構造函數修改添加代碼:
setDragMode(QGraphicsView::ScrollHandDrag ); //手型拖動 scene->setSceneRect(0, 0, 800, 800);
mousePressEvent函數修改如下:
void MyView::mousePressEvent(QMouseEvent *event) { //rotate(90); //視圖旋轉順時針90度 QGraphicsView::mousePressEvent(event); }
此時,由於我們設置的場景大於視圖窗口(resize(400, 400);),所以我們可以點擊鼠標進行拖動:
2.4 事件傳遞
event參數傳遞出去,才能執行默認的事件操作。其實不止上面那一種情況,在圖形視圖框架中,鼠標鍵盤等事件是從視圖進入的,視圖將它們傳遞給場景,場景再將事件傳遞給該點的圖形項,如果該點有多個圖形項,那么就傳給最上面的圖形項。所以要想使這個事件能一直傳播下去,我們就需要在重新實現事件處理函數時,在其最后將event參數傳給默認的事件處理函數。比如我們重寫了場景的鍵盤按下事件處理函數,那么我們就在該函數的最后寫上QGraphicsScene::keyPressEvent(event);一行代碼。
QGraphicsView收到event,就會轉換成QGraphicsScene的事件,QGraphicsScene對象再傳遞給對應的QGraphicsItem:
2.5 背景緩存
2.6 OpenGL渲染
QT += opengl
構造函數添加:
QGLWidget *widget =new QGLWidget(this); setViewport(widget);
這樣便使用OpenGL進行渲染了。
2.7 圖形項查找與圖形項組
resize(400, 400); setBackgroundBrush(QPixmap("../graphicsview03/back.jpg")); QGraphicsScene *scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, 200, 200); QGraphicsRectItem *item1 = new QGraphicsRectItem(0,0,20,20); item1->setBrush(Qt::red); item1->setPos(10,0); //scene->addItem(item1); QGraphicsRectItem *item2 = new QGraphicsRectItem(0,0,20,20); item2->setBrush(Qt::green); item2->setPos(40,0); //scene->addItem(item2); //新建圖形項組 QGraphicsItemGroup *group = new QGraphicsItemGroup; group->addToGroup(item1); group->addToGroup(item2); scene->addItem(group); setScene(scene); setAlignment(Qt::AlignLeft | Qt::AlignTop); //手型拖動 setDragMode(QGraphicsView::ScrollHandDrag); scene->setSceneRect(0, 0, 800, 800); QGLWidget *widget =new QGLWidget(this); setViewport(widget); //輸出(10, 0)點的圖形項 qDebug() << "itemAt(10,0) : " <<itemAt(10, 0); qDebug() << "itemAt(40,0) : " <<itemAt(40, 0); qDebug() << "#################################";
然后我們到myview.h文件中protected部分聲明鍵盤按下事件槽函數:
void keyPressEvent(QKeyEvent *event);
再到myview.cpp中定義它,如下:
void MyView::keyPressEvent(QKeyEvent *event) { //輸出場景中所有的圖形項 qDebug() << items(); items().at(0)->setPos(100,0); items().at(1)->setPos(0,100); //執行默認的事件處理 QGraphicsView::keyPressEvent(event); }
這時運行程序,當按下鍵盤上任意鍵后,效果如下:
可以看到,itemAt()函數可以輸出場景上任意點的圖形項。而items()函數可以輸出場景上所有的圖形項。這里應該說明,items()函數返回的圖形項列表是按棧的降序排序的,也就是說,items().at(0)返回的是最后加入場景的圖形項。從上面可以看出,最后加入的圖形項是item2,其實,因為我們使用了group,而item1和item2都在group里,所以我們只需將group加入場景中就可以了,那么這時加入場景的順序就是,先加入group,因為item1先加入group,所以下面將item1加入場景,最后加入場景的是item2,這就是為什么items.at(0)會是item2的原因,此時里面有3個圖形項:
再說圖形項組,其實圖形項組也是一個圖形項,它有圖形項所擁有的所有特性。其作用就是,將加入它的所有圖形項作為一個整體,對這個圖形項組進行操作,就相當於對其中所有圖形項進行操作。圖形項組是加入它的所有圖形項的父圖形項,在上面的輸出的parent信息中我們可以看到這一點。下面我們將程序中的代碼更改如下:
void MyView::keyPressEvent(QKeyEvent *event) { items().at(2)->setPos(100,100); QGraphicsView::keyPressEvent(event);
items().at(2)操作的是group
可以看到,兩個圖形項是同時移動的。我們要從圖形項組中移除一個圖形項,可以使用removeFromGroup()函數,它可以將給定的item從group中刪除,要注意這時item依然存在,它會回到group的父圖形項中,如果group沒有父圖形項,那么item就會回到場景中。我們可以使用場景的removeItme()函數來刪除group,這樣也會將group中所有的圖形項從場景中刪除。還有一種辦法是利用場景的destroyItemGroup()函數,它會刪除group並銷毀它,但是group中的所有圖形項會回到group的父圖形項中,如果它沒有父圖形項,那么所有圖形項就會回到場景中。
2.8 打印
void MyView::mousePressEvent(QMouseEvent *event) { rotate(90); //視圖旋轉順時針90度 QPixmap pixmap(400,400); //必須指定大小 QPainter painter(&pixmap); render(&painter,QRectF(0,0,400,400), QRect(0,0,400,400)); //打印視圖指定區域內容 pixmap.save("../graphicsview03/save.png"); QGraphicsView::mousePressEvent(event); }
這里我們使用了視圖的render()函數,其中的QRectF參數是指設備的區域,這里是指pixmap。而QRect參數是指視圖上要打印的區域。我們利用QPixmap類的save()函數,將pixmap圖片保存到我們項目源碼目錄中,文件名為“save.png”。下面是運行程序后,點擊鼠標,生成的圖片的效果:
我們每點擊一次鼠標,就會旋轉視圖,那么生成的圖片就是當前視口的截圖。下面我們使用場景的打印函數,將上面的打印一行的代碼改為:
scene()->render(&painter,QRectF(0,0,400,400),QRect(0,0,400,400)); //打印場景內容
運行程序,看圖片效果:
這時無論視圖怎樣變換,生成的圖片總是一樣的。而且它並沒有打印背景的圖片。就像我們看到的,視圖的打印函數是依據視圖的坐標系進行打印的,我們看到的就是打印出來后的效果,它可以看做是程序窗口的截屏。而場景的打印函數,是依據場景的坐標系的,無論視圖怎么轉換,只要場景坐標系沒有變換,它打印出來的圖片都是一樣的,在下一章,我們講解視圖中的坐標系。