Qt 圖形檢視框架 (一)

    如果要繪製成千上萬的圖形,並且對它們進行控制,比如拖動這些圖形、檢測它們的位置以及判斷它們是否碰撞等,可以使用Qt提供的圖形檢視框架來進行設計。

    圖形檢視框架提供了一個基於圖形項的模型檢視程式設計方法,主要由場景、檢視和圖形項三部分組成,這三部分分別由QGraphicsScene、QGraphicsView和QGraphicsItem這三個類來表示。多個檢視可以檢視一個場景,場景中包含各種各樣幾何形狀的圖形項。

    圖形檢視框架可以管理數量龐大的自定義2D圖形項,並且可以與它們進行互動。使用檢視部件可以使這些圖形項視覺化,檢視還支援縮放與旋轉。框架中包含了一個事件傳播架構,提供了和場景中的圖形項進行精確的雙精度互動的能力。圖形檢視框架使用一個BSP(Binary Space Partitioning)樹來快速發現圖形項。可通過Graphics View Framework關鍵字檢視相關幫助。

 場景

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

1) 提供用於管理大量圖形項的高速介面

2) 傳播事件到每一個圖形項

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

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

    QGraphicsScene的事件傳播構架可以將場景事件傳遞給圖形項,也可以管理圖形項之間事件的傳遞。例如,如果場景在一個特定的點接收到了一個滑鼠按下事件,那麼場景就會把這個事件傳遞給該點的圖形項。

    一個場景分為3層:圖形項層(ItemLayer)、前景層(ForegroundLayer)、背景層(BackgroundLayer)。場景的繪製總是從背景層開始,然後是圖形項層,最後是前景層。前景層和背景層都可以使用QBrush進行填充,比如使用漸變和貼圖等。

檢視

    QGraphicsView提供了檢視部件,它用來使場景中的內容視覺化。可以連線多個檢視到同一個場景來為相同的資料集提供多個視口。檢視部件是一個可滾動的區域,它提供了一個滾動條來瀏覽大的場景。預設的QGraphicsView提供了一個QWidget作為視口部件,如果要使用OpenGl進行渲染,則可呼叫QGraphicsView::setViewport()設定QOpenGlWidget作為視口。QGraphicsView會獲取視口部件的所有權。

    檢視從鍵盤或者滑鼠接收輸入事件,然後會在傳送這些事件到視覺化場景前將他們轉換為場景事件(將座標轉換為合適的場景座標)。另外,使用檢視的變換矩陣函式QGraphicsView::transform()時,可以通過檢視來變換場景的座標系統,這樣便可以實現縮放和旋轉等高階的導航功能。

圖形項

    QGraphicsItem是場景中圖形項的基類。典型的形狀的標準圖形項有矩形(QGraphicsRectItem)、橢圓(QGraphicsEllipseItem)和文字項(QGraphicsTextItem)等。但只有編寫自定義的圖形項才能發揮QGraphicsItem的強大功能。

    QGraphicsItem主要支援如下功能:

1) 滑鼠按下、移動、釋放、雙擊、懸停、滾輪和右鍵選單事件

2) 鍵盤輸入焦點和鍵盤事件

3) 拖放事件

4) 碰撞檢測

    除此之外,圖形項還可以儲存自定義的資料,可以使用setData()進行資料儲存,然後使用data()獲取其中的資料。

    要實現自定義的圖形項,那麼首先要建立一個QGraphicsItem的子類,然後重新實現它的兩個純虛公共函式:boundingRect()和paint(),前者用來返回要繪製圖形項的矩形區域,後者用來執行實際的繪圖操作。其中boundingRect()函式將圖形項的外部邊界定義為一個矩形,所有的繪圖操作都必須限制在圖形項的邊界矩形中。這個矩形對於剔除不可見圖形項、確定繪製交叉專案時哪些區域需要重新構建、碰撞檢測機制都很重要。一定要保證所有繪圖都在boundingRect()的邊界之中,特別是當QPainter使用了指定的QPen來渲染圖形的邊界輪廓時,繪製的圖形的邊界線的一般會在外面,一半會在裡面(例如使用了寬度為兩個單位的畫筆,就必須在boundingRect()裡繪製一個單位的邊界線),這也是在boundingRect()中要包含半個畫筆寬度的原因。

    下面是純虛擬函式的實現示例:

QRectF MyItem::boundingRect() const
{
qreal penWidth = 1; //畫筆寬度
return QRectF(0 - penWidth / 2, 0 - penWidth / 2,
20   penWidth, 20   penWidth);
}
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->setBrush(Qt::red);
painter->drawRect(0, 0, 20, 20);
}

圖形檢視框架的座標系統

    圖形檢視框架基於笛卡爾座標系統。圖形檢視框架中有3個有效的座標系統:圖形項座標、場景座標和檢視座標。進行繪圖時,場景座標對應QPainter的邏輯座標,檢視座標對應裝置座標。

圖形項座標

    圖形項使用自己的本地座標系統,座標通常是以(0,0)為原點,而這也是所有變換的中心。當要建立一個自定義圖形項時,只需要考慮圖形項的座標系統,而且一個圖形項的邊界矩形和圖形形狀都是在圖形項座標系統中的。

    圖形項的位置是指圖形項的原點在其父圖形項或者場景中的位置。可以使用setPos()函式來指定圖形項的位置,如果沒有指定,預設出現在父圖形項或者場景的原點處。

    子圖形項的位置和座標是相對於父圖形項的,雖然父圖形項的座標變換會隱含地變換子圖形項,但子圖形項的座標不會受到父圖形項的變換的影響。例如,在沒有座標變換時,子圖形項就在父圖形項的(10,0)點,那麼子圖形項中的(0,10)點就對應了父圖形項的(10,10)點。現在即使父圖形項進行了旋轉或者縮放,子圖形項的(0,10)點仍對應著父圖形項的(10,10)點。但是相對於場景,子圖形項就會跟隨父圖形項的變換,例如,父圖形項放大為(2x,2x),那麼子圖形項在場景中的位置就會變成(20,0),它的(10,0)點就會對應著場景中的(40,0)點。

    所有的圖形項都會使用確定的順序來進行繪製,這個順序也決定了單擊場景時哪個圖形項會先獲得滑鼠輸入。一個子圖形項會堆疊在父圖形項的上面,而兄弟圖形項會以插入順序進行堆疊。所有圖形項都包含一個Z值來設定它們的層疊順序,一個圖形項的Z值預設為0,可以使用QGraphicsItem::setZValue()來改變一個圖形項的Z值,從而使它堆疊到其兄弟圖形項的上面(使用較大的Z值)或者下面(使用較小的Z值)。

場景座標

    場景座標是所有圖形項的基礎座標系統。場景座標的原點在場景的中心,x和y座標分別向右和向下增大。

檢視座標

    檢視座標的每一個單位對應一個畫素,原點(0,0)總在QGraphicsView視口的左上角,而右下角是(寬,高)。所有的滑鼠事件和拖放事件最初都是使用檢視座標接收的。

座標對映

    不僅可以在檢視、場景和圖形項之間使用座標對映,還可以在子圖形項、父圖形項或者圖形項、圖形項之間進行座標對映。所有的對映函式都可以對映點、矩形、多邊形和路徑。例如要獲取在檢視中的一個橢圓形中包含的圖形項,則可以先傳遞一個QPainterPath物件作為引數給mapToScene()函式,然後傳遞對映後的路徑給QGraphicsScene::items()函式。

圖形檢視框架的對映函式
對映函式描述
QGraphicsView::mapToScene( )從檢視座標系統對映到場景座標系統
QGraphicsView::mapFromScene( )從場景座標系統對映到檢視座標系統
QGraphicsItem;:mapToScene( )從圖形項的座標系統對映到場景的座標系統
QGraphicsItem;:mapFromScene( )從場景的座標系統對映到圖形項的座標系統
QGraphicsItem;:mapToParent( )從本圖形項的座標系統對映到其父圖形項的座標系統
QGraphicsItem;:mapFromParent( )從父圖形項的座標系統對映到本圖形項的座標系統
QGraphicsItem;:mapToItem( )從本圖形項的座標系統對映到另一個圖形項的座標系統
QGraphicsItem;:mapFromItem( )從另一個圖形項的座標系統對映到本圖形項的座標系統

事件處理與傳播

    圖形檢視框架中的事件都是先由檢視進行接收,然後傳遞給場景,再由場景傳遞給相應的圖形項。而對於鍵盤事件,它會傳遞給獲得焦點的圖形項,可以使用QGraphicsScene類的setFocusItem()函式或者圖形項自身呼叫setFocus()函式來設定焦點圖形項。預設的,如果場景沒有獲得焦點,那麼所有的鍵盤事件都會被丟棄。場景中的圖形項獲得了焦點,場景也會自動獲得焦點。

下面是一個應用例項:

main.cpp
#include <QApplication>
#include "myitem.h"
#include "myview.h"
#include <QTime>
int main(int argc, char* argv[ ])
{
QApplication app(argc, argv);
qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime()));
QGraphicsScene scene;
scene.setSceneRect(-200, -150, 400, 300);
for (int i = 0; i < 5;   i) {
MyItem *item = new MyItem;
item->setColor(QColor(qrand() % 256, qrand() % 256, qrand() % 256));
item->setPos(i * 50 - 90, -50);
scene.addItem(item);
}
MyView view;
view.setScene(&scene);
view.setBackgroundBrush(QPixmap("../myView/background.png"));
view.show();
return app.exec();
}
myitem.h
#include <QGraphicsItem>
class MyItem : public QGraphicsItem
{
public:
MyItem();
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget);
void setColor(const QColor &color) { brushColor = color; }
private:
QColor brushColor;
protected:
void keyPressEvent(QKeyEvent *event);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);
};
myitem.cpp
#include "myitem.h"
#include <QPainter>
#include <QCursor>
#include <QKeyEvent>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>
MyItem::MyItem()
{
brushColor = Qt::red;
setFlag(QGraphicsItem::ItemIsFocusable);
setFlag(QGraphicsItem::ItemIsMovable);
setAcceptHoverEvents(true);
}
QRectF MyItem::boundingRect() const
{
qreal adjust = 0.5;
return QRectF(-10 - adjust, -10 - adjust,
20   adjust, 20   adjust);
}
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *,
QWidget *)
{
if (hasFocus()) {
painter->setPen(QPen(QColor(255, 255, 255, 200)));
} else {
painter->setPen(QPen(QColor(100, 100, 100, 100)));
}
painter->setBrush(brushColor);
painter->drawRect(-10, -10, 20, 20);
}
// 滑鼠按下事件處理函式,設定被點選的圖形項獲得焦點,並改變游標外觀
void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *)
{
setFocus();
setCursor(Qt::ClosedHandCursor); //設定游標為手握下的形狀
}
// 鍵盤按下事件處理函式,判斷是否是向下方向鍵,如果是,則向下移動圖形項
void MyItem::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Down)
moveBy(0, 10);
}
// 懸停事件處理函式,設定游標外觀和提示
void MyItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
setCursor(Qt::OpenHandCursor); //設定游標為手張開的形狀
setToolTip("I am item");
}
// 右鍵選單事件處理函式,為圖形項新增一個右鍵選單
void MyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
QMenu menu;
QAction *moveAction = menu.addAction("move back");
QAction *selectedAction = menu.exec(event->screenPos());
if (selectedAction == moveAction) {
setPos(0, 0);
}
}

myview.h
#include <QGraphicsView>
class MyView : public QGraphicsView
{
Q_OBJECT
public:
explicit MyView(QWidget *parent = 0);
protected:
void keyPressEvent(QKeyEvent *event);
};

myview.cpp
#include "myview.h"
#include <QKeyEvent>
MyView::MyView(QWidget *parent) :
QGraphicsView(parent)
{
}
void MyView::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_Plus :
scale(1.2, 1.2);
break;
case Qt::Key_Minus :
scale(1 / 1.2, 1 / 1.2);
break;
case Qt::Key_Right :
rotate(30);
break;
}
QGraphicsView::keyPressEvent(event);//一定要加上這個,否則在場景和圖形項中就無法再接收到該事件了
}

滑鼠拖動item後:

鍵盤按下 – 進行縮放後: