読者です 読者をやめる 読者になる 読者になる

Atsushi4のブログ

へたれQtプログラマの備忘録的ななにか

Qt Quickでマウスによる範囲選択をC++で実装する

概要

Qt勉強会#38@Tokyoで、Qt Quickのマウスによる矩形範囲選択がうまくいかない、というお話を聞いてやってみた内容です。
Qt 勉強会 #38 @ Tokyo - connpass

現象

図のように、矩形の軌跡が残ってしまいます。
f:id:Atsushi4:20160910132433g:plain
よく見ると、ペン幅5pixelを指定しているのに左と上は3pixel、右と下は2pixelになっている、という問題もあります。

再現コード

QQuickPaintedItemを継承、QPainterで描きます。

myitem.h

class MyItem : public QQuickPaintedItem
{
...
private:
    QPoint begin;
    QRect band;
    int penWidth = 5;
};

mousePressで始点を保存、mouseMoveで矩形更新してpaintで描画します。

myitem.cpp

void MyItem::mousePressEvent(QMouseEvent *event)
{
    begin = event->pos();
    band = QRect();
    update();
}
void MyItem::mouseMoveEvent(QMouseEvent *event)
{
    if (begin.isNull()) return;

    band = QRect(begin, event->pos()); // --- (1)
    update(band); // --- (2)
}
void MyItem::paint(QPainter *painter)
{
    painter->fillRect(this->boundingRect(), Qt::green);
    if (band.isNull()) return;

    painter->save();
    QPen pen(QBrush(Qt::red), penWidth);
    painter->setPen(pen);
    painter->drawRect(band);
    painter->restore();
}
void MyItem::mouseReleaseEvent(QMouseEvent *event)
{
    Q_UNUSED(event)
    begin = QPoint();
}

原因と修正

原因

(1) 更新領域の指定に使用している終点はマウスが動いた後のposなので、矩形が小さくなる時に前回の領域が残ってしまう
(2) penWidthを考慮してマージンを入れる必要がある(参考:QPainter::drawRect
http://doc.qt.io/qt-5/qpainter.html#drawRect

修正

というわけでmouseMoveを修正します。

myitem.cpp (diff)

void MyItem::mouseMoveEvent(QMouseEvent *event)
{
    if (begin.isNull()) return;

-   band = QRect(begin, event->pos());
-   update(band);
+   const auto pre = band; // 前回の矩形
+   band = QRect(begin, event->pos()); // 今回の矩形

+   const int topLeftMargin = penWidth / 2;
+   const int bottomRightMargin = (penWidth + 1) / 2; // penWidthが奇数のときは右下にはみ出る
+   const QMargins margins(topLeftMargin, topLeftMargin, bottomRightMargin, bottomRightMargin);

+   update((band | pre) + margins); // 前回+今回+margin --- (3)
}

うまく動くようになりました。
f:id:Atsushi4:20160910145110g:plain

その他

アブノーマルなQRectの算術演算子

(3)で

update((band + margins) | (pre + margins)); // --- (4)

とやると、等価な式に見えるのですがうまくいきません。
QRectのwidthやheightがマイナスの時にマージンを足すと、想定した挙動にならないからです。

QRect r1(30, 40, -10, -30); // not equal (20, 10, 10, 30)
QRect r2 = r1 + QMargins(10, 10, 10, 10); // r2 == QRect(40, 50, 10, -10)

(3)と等価にするには

update((band.normalized() + margins) | (pre.normalized() + margins));

と書く必要があります面倒です意味わかりません何とかしt

ええと、幅-10にマージン20乗せたら幅10だよね、とか、今の仕様も分かるのですが、やっぱり(3)と(4)の結果が等しくないというのは違和感があります。
修正したら誰か困るんかナー。

今回書いた以外の実装

QWidgetアプリケーションでQGraphicsViewを使う場合は

view->setDragMode(QGraphicsView::RubberBandDrag);

でいけると思います。
他にQRubberBandというクラスもあるようですが、使ったことがないのでよくわかりません。

パフォーマンスがあまり気にならないのであれば、
QWidgetではQGraphicsRectItemを、Qt QuickではRectangleを置いてしまう、という手もあると思います。

ついでに

そんなことをしていたら、Qt Creatorでシンボルの名前変更中に「コピー」しようとすると「ペースト」されるという変なバグを踏みまして、勉強会の最後にお話ししたところ早速task_jpさんが直してくれました。ありがとうございます。