Qt Quickでマウスによる範囲選択をC++で実装する
概要
Qt勉強会#38@Tokyoで、Qt Quickのマウスによる矩形範囲選択がうまくいかない、というお話を聞いてやってみた内容です。
Qt 勉強会 #38 @ Tokyo - connpass
現象
図のように、矩形の軌跡が残ってしまいます。
よく見ると、ペン幅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) }
うまく動くようになりました。
その他
アブノーマルな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さんが直してくれました。ありがとうございます。
. @Atsushi4 が一昨日 Qt 勉強会で言ってた Qt Creator のバグを直した。https://t.co/AALW8Swr16 #qtjp
— Tasuku Suzuki (@task_jp) 2016年8月22日