技術文章Qt


 
2次元グラフィックスのお話
Nobuhide Tsuda

画面にグラフィックスオブジェクトを多量に配置する場合は、3次元が苦手の人にも安心の「Qt 2次元グラフィックス フレームワーク」を使うといいぞ。
素直な設計で使いやすく、十分な機能も揃っていて、カスタマイズ性・拡張性もあり、パフォーマンスも充分だ。

基本

グラフィックスシーンに矩形・楕円等の幾何図形を埋め込んでインタラクティブ操作が可能

モデル・ビュー アーキテクチャ (モデル:QGraphicsScene、ビュー:QGraphicsView)
モデルはアイテム(QGraphicsItem 派生クラス)オブジェクトを管理する
シーン・ビュー・アイテム それぞれの派生クラスを宣言・実装する

アイテム親子関係、拡大縮小・回転・透視変換などの座標変換、ドラッグ&ドロップ、印刷・印刷プレビュー、イフェクト、アニメーション機能などをサポート

おいらの感想

QGraphicsScene, QGraphicsView

QGraphicsScene 派生クラスオブジェクトを生成し、それを引数にして QGraphicsView 派生クラスオブジェクトを生成し、 それをセントラルウィジットに設定する。
※ メニュー等が必要なければ、ビューを直接表示してもよい。

class MainWindow : public QMainWindow
{
    .....
private:
    class Scene *m_scene;
    class View *m_view;
};
MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags)
    : QMainWindow(parent, flags)
{
    createActions();
    createMenus();
    m_scene = new Scene(QRect(0, 0, 640, 480));     //  シーン矩形部分
    m_view = new View(m_scene);
    m_view->setBackgroundBrush(QBrush(Qt::gray));
    setCentralWidget(m_view);
}

上記コードだけでは、シーン矩形部分がはっきりしないので、背景をグレイにし([1])、 シーン矩形部分に矩形を配置する([2])。

MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags)
    : QMainWindow(parent, flags)
{
    m_scene = new Scene(QRect(0, 0, 640, 480));     //  シーン矩形部分
    m_scene->addRect(0, 0, 640, 480, QPen(Qt::black), QBrush(Qt::white));   //  [2]
    m_view = new View(m_scene);
    m_view->setBackgroundBrush(QBrush(Qt::gray));   //  [1]
    setCentralWidget(m_view);
}

ひとつのシーンに対して複数のビューを作成することが出来る。

MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags)
    : QMainWindow(parent, flags)
{
    m_scene = new Scene(QRect(0, 0, 640, 480));
    m_view = new View(m_scene);
    .....
    View *view = new View(m_scene);
    view->show();
}

どちらのビューに対して操作を行っても、同じ内容が表示される。

ビューごとにスケールを変えたり、回転させることも可能

ビューは自分で新たに実装することも可能なので、魚眼レンズみたいなのも不可能ではない。

演習問題

  1. シーン、ビューの派生クラスを定義・実装し、ビューをトップレベルウィンドウとして表示しなさい。

グラフィックス アイテム

直線、矩形、テキストなどのグラフィックスアイテムは QGraphicsItem 派生クラスとして定義されている。

矩形 QGraphicsRectItem
楕円 QGraphicsEllipseItem
ポリゴン QGraphicsPolygonItem
直線 QGraphicsLineItem
テキスト QGraphicsTextItem
シンプルテキスト QGraphicsSimpleTextItem
ピックスマップ QGraphicsPixmapItem
パス(曲線・直線) QGraphicsPathItem

グラフィックスオブジェクトを生成し、QGraphicsScene 派生クラスオブジェクトに配置する。

    QGraphicsEllipseItem *item = new QGraphicsEllipseItem(100, 100, 200, 100);
    m_scene->addItem(item);

addEllipse() 等でオブジェクトを生成&シーンに追加することも可能

    QGraphicsEllipseItem *ellipse = m_scene->addEllipse(100, 300, 200, 100);

グラフィックス アイテム は以下のような属性を持つ。XXX() で参照、setXXX() で値を設定することが可能(XXX は属性名)。

zValue Z値。兄弟アイテムが重なった場合に、どちらを前面に描画するかを決める。Z値が大きいほど前面に描画される。
toolTip マウスをアイテム上に静止させた時に表示されるツールチップテキスト
pen 線を描画するペン(デフォルトは黒)
brush 内部を塗りつぶすブラシ(デフォルトは透明)
flags 移動可能・選択可能・フォーカス取得可能 など(参照:enum QGraphicsItem::GraphicsItemFlag)
    QGraphicsEllipseItem *ellipse = m_scene->addEllipse(150, 150, 200, 100);
    ellipse->setBrush(QBrush(QColor("pink")));
    ellipse->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);

塗りつぶし色をピンクにし、選択・移動可能にした場合

演習問題

  1. シーンに、サイズ:200x100、塗りつぶし色:緑の矩形を描きなさい
  2. 上記矩形をマウスで選択可能にしなさい
  3. 上記矩形をマウスで選択&移動可能にしなさい

マウスイベント処理

モデル・ビュー アーキテクチャではUIに関する処理はビューが行うのが一般的だと思うが、 Qt のグラフィックスフレームワークでは、アイテムまたはシーン クラスの方でUI処理を行うことができる。
(※ 各オブジェクトへのイベントは各オブジェクトが処理するので、こちらの方がよりOOP的?)

「void QGraphicsScene::mousePressEvent ( QGraphicsSceneMouseEvent * mouseEvent )」 はマウスが押下された時にコールされるイベントハンドラ。
「QGraphicsSceneMouseEvent」は「QMouseEvent」と同じようなマウスイベントクラス。 「scenePos()」でマウス座標を取得などのメソッドを持つ。

void Scene::mousePressEvent ( QGraphicsSceneMouseEvent * mouseEvent )
{
    QPointF p = mouseEvent->scenePos();
    QGraphicsRectItem *item = addRect(QRectF(p, p + QPointF(64, 32)));
}

例えば、上記コードはマウスクリック位置に 64x32 の矩形オブジェクトを追加する

QGraphicsScene のマウス関連のイベントには以下のものがある。 (※ Scene, Item で同じ。View にも同様のセットがあるが、引数型は QMouseEvent )

void Scene::mousePressEvent ( QGraphicsSceneMouseEvent * mouseEvent )
{
    if( mouseEvent->button() == Qt::LeftButton ) {
        m_mousePos = mouseEvent->scenePos();
        m_rectItem = addRect(QRectF(m_mousePos, m_mousePos));
    }
}
void Scene::mouseReleaseEvent ( QGraphicsSceneMouseEvent * mouseEvent )
{
    m_rectItem = 0;
}
void Scene::mouseMoveEvent ( QGraphicsSceneMouseEvent * mouseEvent )
{
    if( m_rectItem ) {
        m_rectItem->setRect(QRectF(m_mousePos, mouseEvent->scenePos()));
    }
}

上記コードは、マウス左ボタンを押下した位置と放した位置の矩形アイテムを生成するもの。
実行結果を下図に示す。

※ mouseMoveEvent() は常にコールされるわけではなく、マウスボタンが押されている場合のみコールされる

マウスクリック位置のグラフィックスアイテムを取得したい場合は、

QGraphicsItem * QGraphicsScene::itemAt ( const QPointF & position, const QTransform & deviceTransform ) const

を利用する。
第1引数には「mouseEvent->scenePos()」を、第2引数には「QTransform()」を指定する。
取得したアイテムのクラスを識別したい場合は「int QGraphicsItem::type()」を利用する。

マウスイベントはグラフィックスアイテムへも送られるので、シーンで処理するだけでなく、アイテムクラスの方で処理することもできる。
その具体例は、ドラッグ&ドロップの節で説明する。

キーイベント処理

仮想関数「void keyPressEvent ( QKeyEvent * event )」でシーンに対するキーイベントを処理することが出来る。

詳細

座標

グラフィックスアイテムの座標値の型は qreal である。通常は double に型定義されている。

シーン上の座標系は論理座標系と呼ばれる。x 軸は右方向、y 軸は下方向がプラス。

アイテムオブジェクトは、ローカル座標系によりオブジェクト構成要素の座標を持つ。
pos() のみが親オブジェクトからの相対座標となる。

例えば、pos() が (10, 20)、矩形が (100, 100, 150, 50) の場合、親オブジェクトからの論理座標値は (110, 120, 150, 50) となる。

従って、グラフィックアイテムを移動した場合、全体座標を変更するか、各位置座標を変更するかという選択肢がある。
デフォルトの実装では、アイテム移動時に各位置座標は変化せず、アイテム座標(pos())が変更される。

トップレベルアイテムの親はシーンであるが、深い親子関係がある場合は、シーン座標系での位置を参照したくなる場合がある。 その場合は scenePos() を使用する。

座標変換

移動、回転、拡大・縮小、平行四辺形変換が可能
透視変換も可能(X軸・Y軸回りの回転、QTransform 利用)

QGraphicsItem::setTransform ( const QTransform & matrix, bool combine = false ) でアイテム毎に座標変換を指定出来る

親にも座標変換が指定されている場合、子→親…と順次、座標変換が適用される
内部処理的には親から順に変換行列が適用されていると思う

QPointF QGraphicsItem::mapFromItem(QgrItem *, QPointF &) などでアイテム・シーン座標系間で座標変換可能

選択

各グラフィックスアイテムは選択状態・非選択状態をとることができる。
QGraphicsItem::ItemIsSelectable フラグが立っていれば、マウスで選択可能
下記のメソッドにより、選択状態を設定することもできる。

選択されているアイテムの一覧は、下記のメソッドにより取得できる。

選択を全解除する場合は、以下のメソッドをコールする。

マウスクリックなどにより選択状態が変化した場合は、「void selectionChanged()」シグナルが発行される。

ドラッグ&ドロップ

ドラッグ&ドロップは、クリップボード?を用いて、アプリケーション内外で可視的にデータを移動・コピーするための機構である。

ドラッグ&ドロップ処理は、以下の3つのステージに分かれる

・ ドラッグ開始処理:
mouseMoveEvent(QGraphicsSceneMouseEvent *event) で、ドラッグ処理を開始出来る
通常は、マウスクリック地点から一定距離移動した場合は、ドラッグモードに移行する (現ポインタ位置:event->scenePos()、マウスクリック位置:event->buttonDownScenePos(Qt::LeftButton))
QDrag オブジェクトを生成、適切なデータを設定し、QDrag::exec() を実行
QMimeData オブジェクトを生成し、QDrag::setMimeData() で QDrag オブジェクトにセット
QMimeData::setData(const QString & mimeType, const QByteArray & data ) でタイプを文字列で設定可能
HTML、画像、URL、テキストデータは予め定義されている
ドロップ処理が終ると、exec() から帰ってくる(exec() はブロッキング処理)。 (※ ここから先は、実はドロップ後の処理になる)
どのように処理されたかは exec() のリターン値で判別できる。 Qt::MoveAction の場合はドロップソースを削除

・ ドラッグ中処理:
ドロップ対象オブジェクトは、あらかじめ setAcceptDrops(true); しておく必要がある
dragEnterEvent ( QGraphicsSceneDragDropEvent * event ):
 event->mimeData() がドロップ可能かどうかを event->setAccepted(bool) で知らせる
 ドロップ先をハイライトするなどの処理も行う
dragMoveEvent ( QGraphicsSceneDragDropEvent * event ):
 vent->mimeData() がドロップ可能かどうかを event->setAccepted(bool) で知らせる
dragLeaveEvent ( QGraphicsSceneDragDropEvent * event ):
 ドロップ先のハイライト解除など

・ ドロップ処理:
dropEvent ( QGraphicsSceneDragDropEvent * event )
 アイテム上でドロップされた時の処理
 ドロップされたデータは event->mimeData() を参照
 QGraphicsSceneDragDropEvent::setDropAction ( Qt::DropAction action ) で、 どのようにドロップされたかを exex() で返すことができる

印刷・印刷プレビュー

印刷
 QPrintDialog(&printer).exec() == QDialog::Accepted ならば印刷を行う

印刷プレビュー
 QPrintPreviewDialog オブジェクトを作り
 paintRequested() シグナルを 印刷処理スロットにコネクト
 QPrintPreviewDialog::exec() を実行

印刷処理
 Scene::render(QPainter *) をコール
 必要があれば、ウィンドウ・ビューポートを設定:Scene::render(QPainter *, const QRectF &, const QRectF &)

演習問題

  1. 印刷・印刷プレビュー機能を実装し、動作確認しなさい

その他

ここで述べた以外にも「2D グラフィックス フレームワーク」には様々な機能がある。
詳細はヘルプをたんねんに読んでね。