すぐ忘れる筆者用メモです。
適当に行き会ったものを挙げています。随時追加のつもり。
プロット画面のコントロールを設定
ズームやパンニングを行うプロット画面コントロールはそれぞれQwtPlotZoomer
、QwtPlotPanner
クラスが受け持ちます。
QwtPlotZoomer
の設定は必須のようで、設定しないと何も表示されません。
QwtPlotZoomer
はズーム枠を描画するためにQwtPlotPicker
というラバーバンドを取り扱うクラスの派生クラスになっていますが、
QwtPlotPanner
クラスはQwtPanner
クラスの派生クラスです。
そのため、設定方法は両者でまったく異なります。
ここではQwtPlot
クラスの派生クラスを作成している例をしめします。
MyPlot.h(QwtPlot派生クラス)
1: #pragma once 2: #include "qwt_plot.h" 3: 4: class MyPlot : 5: public QwtPlot 6: { 7: Q_OBJECT 8: 9: public: 10: MyPlot( QWidget *parent = NULL ); 11: ~MyPlot(void); 12: 13: private: 14: 15: // メンバ変数宣言 16: QwtPlotZoomer *m_pZoomer; 17: QwtPlotPanner *m_pPanner; 18: }; 19:
コンストラクタでそれぞれのオブジェクトを作成します。
MyPlot.cppのコンストラクタ部分
1: MyPlot::MyPlot( QWidget *parent ) 2: : QwtPlot( parent ) 3: { 4: // Zoomerの設定 5: m_pZoomer = new QwtPlotZoomer( canvas() ); 6: 7: // ズーム枠の色を設定 8: m_pZoomer->setRubberBandPen( QColor( Qt::darkBlue ) ); 9: 10: // マウスの座標値の色を設定 11: m_pZoomer->setTrackerPen( QColor( Qt::darkBlue ) ); 12: 13: // マウスボタンの割り当て 14: m_pZoomer->setMousePattern( QwtEventPattern::MouseSelect1, Qt::LeftButton ); 15: m_pZoomer->setMousePattern( QwtEventPattern::MouseSelect2, Qt::RightButton, Qt::ControlModifier ); 16: m_pZoomer->setMousePattern( QwtEventPattern::MouseSelect3, Qt::RightButton ); 17: 18: // 座標値の表示はズーム枠がアクティブなときのみ 19: m_pZoomer->setTrackerMode( QwtPicker::ActiveOnly ); 20: 21: 22: // Pannerの設定 23: m_pPanner = new QwtPlotPanner( canvas() ); 24: 25: // パンニングのボタンを中ボタンに設定 26: m_pPanner->setMouseButton( Qt::MiddleButton ); 27: 28: setAutoReplot( true ); 29: }
ZoomerのMouseSelect1
はズーム開始、MouseSelect2
はリセット、
MouseSelect3
は一つ前のズームに戻る、にそれぞれ対応せいています。
設定しなくても動作するはずなのですが、パンニングとボタンが重複したりするとおかしな動作をすることがあるので、
念のため設定しておいたほうが無難かと思われます。
ズームレベルはズームスタックに積まれており、通常はズームスタックをひとつ戻ることでズームアウトすることになりますが、
これが意外に使いにくいです。普通のズームアウトを実装する場合は一工夫必要です。
プロットの縦横比を固定
プロットウィジェットの目盛りは、何も設定していない状態ではプロットウィンドウの縦横比に応じて変化するようになっています。
縦軸と横軸の目盛り幅をウィンドウの縦横比によらず一定にしたい場合はQwtPlotRescaler
を利用します。
QwtPlotRescaler
を利用する場合はQwtPlot
クラスの派生クラスを作成したほうが楽です。
MyPlot.h(QwtPlot派生クラス)
1: #pragma once 2: #include "qwt_plot.h" 3: #include <qwt_plot_rescaler.h> 4: 5: class MyPlot : 6: public QwtPlot 7: { 8: Q_OBJECT 9: 10: public: 11: MyPlot( QWidget *parent = NULL ); 12: ~MyPlot(void); 13: 14: private: 15: QwtPlotRescaler *m_pRescaler; 16: }; 17:
このクラスでは15行目のようにQwtPlotRescaler *
のメンバを一つ持っています。
コンストラクタで以下のようにQwtPlotRescaler
オブジェクトを作成します。
MyPlot.cppのコンストラクタ部分
1: #include "MyPlot.h" 2: 3: MyPlot::MyPlot( QWidget *parent ) 4: : QwtPlot( parent ) 5: { 6: // QwtPlotRescalerのコンストラクタ、第1引数はQwtPlotCanvasオブジェクトへのポインタ、 7: // 第2引数は目盛り幅の基準軸、第3引数のように指定することで縦横比を固定 8: m_pRescaler = new QwtPlotRescaler( canvas(), QwtPlot::xBottom, QwtPlotRescaler::Fixed ); 9: m_pRescaler->setEnabled( true ); 10: 11: // X軸は目盛りが両側に引き伸ばされるように指定 12: m_pRescaler->setExpandingDirection( QwtPlot::xBottom, QwtPlotRescaler::ExpandBoth ); 13: 14: // Y軸は下方向に引き伸ばされるように指定 15: m_pRescaler->setExpandingDirection( QwtPlot::yLeft, QwtPlotRescaler::ExpandDown ); 16: 17: // 目盛りの縦横比を指定 18: m_pRescaler->setAspectRatio( 1.0 ); 19: 20: // 目盛り幅設定を実行 21: m_pRescaler->rescale(); 22: 23: setAutoReplot( true ); 24: }
特に、後述の画像をプロットする場合は、これをしておかないと表示される画像の縦横比がウィンドウによって変化してしまいます。
画像をプロット
QwtPlot
はグラフなどのデータをプロットするのが主目的だと思われますが、画像をプロットすることもできます。
画像をプロットするには、まず画像用のQwtPlotItem
派生クラスを作成します。
ImageItem.h
1: #pragma once 2: #include "qwt_plot_item.h" 3: 4: class ImageItem : 5: public QwtPlotItem 6: { 7: public: 8: ImageItem( QImage *pImg ); 9: ~ImageItem(void); 10: 11: virtual void draw(QPainter *painter, 12: const QwtScaleMap &xMap, const QwtScaleMap &yMap, 13: const QRectF &rc) const; 14: 15: private: 16: QImage *m_pimg; 17: QRectF m_rc; 18: }; 19:
この例ではプロット範囲の矩形と画像データをメンバ変数として持たせています。
また、draw()
関数をオーバーライドしています。
実装部は以下のような感じです。
ImageItem.cpp
1: #include "ImageItem.h" 2: #include <qwt_painter.h> 3: #include <qwt_scale_map.h> 4: 5: ImageItem::ImageItem( QImage *pImg ) 6: { 7: m_pimg = pImg; 8: m_rc = QRectF( QPoint( 256, 512 ), pImg->size() ); 9: } 10: 11: 12: ImageItem::~ImageItem(void) 13: { 14: } 15: 16: void ImageItem::draw(QPainter *painter, 17: const QwtScaleMap &xMap, const QwtScaleMap &yMap, 18: const QRectF &rc) const 19: { 20: QRectF rcImage = QwtScaleMap::transform( xMap, yMap, m_rc ); 21: QwtPainter::drawImage( painter, rcImage, *m_pimg ); 22: } 23:
8行目で画像をプロットする座標を決定しています。プロットの基準位置は画像の左下なので注意!
実用上はコンストラクタの引数で座標を指定できるようにすると良いでしょう。
draw()
関数内で実際に描画を行います。
20行目で実際の描画位置を決定しています。プロット位置とは違い、こちらはウィンドウ座標です。
この例のようにすることで、ビューのパンニングを行うと画像も動きます。
画像用プロットアイテムクラスを作成したら、実際にプロットにアタッチします。
こちらは普通のプロットアイテムと同じようにアタッチすればいいです。
ImageItem *item = new ImageItem( &m_qimg );
item->attach( m_pPlot );
実行例
ズームアウトの実装
上記のように普通のズームアウトを実装する場合はいろいろと面倒な実装をする必要があります。
以下の例ではズームアウトボタンが押されたときにプロット中心を基準にズームアウトする例です。
まずはズームアウトボタンが押されたときの関数の例です。
1: void MyPlot::ZoomOut() 2: { 3: QRectF rcNew; 4: 5: // 現在のズーム枠を取得 6: QRectF rc = m_pZoomer->zoomRect(); 7: rc.normalized(); 8: 9: // ズームアウト枠を設定 10: rcNew.setLeft( rc.left() - dRange*0.1 ); 11: rcNew.setRight( rc.right() + dRange*0.1 ); 12: rcNew.setTop( rc.top() - dRange*0.1 ); 13: rcNew.setBottom( rc.bottom() + dRange*0.1 ); 14: 15: // ズームアウト枠を適用 16: m_pZoomer->zoom( rcNew ); 17: 18: // ズームスタックをリセット 19: m_pZoomer->setZoomBase(); 20: 21: canvas()->update(); 22: }
自前のズームアウトを実装する場合はズームスタックはそのつどリセットしておくほうが無難です。
そこで、パンニングのときにもズームスタックをリセットしてズームベースを更新しておくようにします。
ズームベースとはズームの初期化時のズーム範囲です。
1: 2: // コンストラクタでパンニング完了時のシグナル・スロットを設定 3: QObject::connect( m_pPanner, SIGNAL( panned( int, int ) ), this, SLOT( updateZoom( int, int ) ) ); 4: 5: // パンニング完了時のスロット 6: void MyPlot::updateZoom( int nX, int nY ) 7: { 8: QRectF rc = m_pZoomer->zoomRect(); 9: QPointF pnt = rc.topLeft(); 10: 11: // 現在のズーム枠を移動してズームベースを更新 12: m_pZoomer->setZoomBase( QRectF( QPointF( pnt.x()+nX, pnt.y()+nY ), rc.size() ) ); 13: 14: canvas()->replot(); 15: } 16:
自前の目盛りを設定する
基本的に目盛りは実数値が表示されますが、この値を変えることができます。
以下の例は度単位の数値を度分秒表記にするものです。
まず、QwtScaleDraw
クラスの派生クラスを作成します。
1: 2: // 縦軸クラス 3: class DMSScaleDrawLat : public QwtScaleDraw 4: { 5: public: 6: DMSScaleDrawLat() 7: { 8: setLabelAlignment( Qt::AlignLeft | Qt::AlignVCenter ); 9: } 10: 11: // 与えられた数値を度分秒に変換 12: virtual QwtText label( double dVal ) const 13: { 14: int nD, nM; 15: double dS; 16: nD = (int)floor( dVal ); 17: dS = (dVal - nD)*60; 18: nM = (int)floor( dS ); 19: dS = (dS - nM)*60; 20: return QObject::tr( "%1dn %2' %3"" ).arg(nD).arg(nM).arg(dS); 21: } 22: }; 23: 24: // 横軸クラス 25: // 縦軸クラスの派生クラスとして、ラベルの向きだけを変更 26: class DMSScaleDrawLon : public DMSScaleDrawLat 27: { 28: public: 29: DMSScaleDrawLon() 30: { 31: setLabelRotation( -90.0 ); 32: } 33: };
あとはこれを各軸に割り当てます。
ここではQwtPlot
派生クラスを作成してそのコンストラクタ内で割り当てを行っています。
1: MyPlot::MyPlot( QWidget *parent ) 2: : QwtPlot( parent ) 3: { 4: // 中略 5: 6: // 作成した軸クラスを割り当てる 7: setAxisScaleDraw( QwtPlot::xBottom, new DMSScaleDrawLon() ); 8: setAxisScaleDraw( QwtPlot::yLeft, new DMSScaleDrawLat() ); 9: }
実行例
デフォルトの軸ラベルは、与えられた数値に対してQLocale().toString( value );
で返される文字列を表示します(qwt_abstract_scale_draw.cppのlabel()関数内
)。
このとき、大きい数値は指数型の文字列が返されます。これが嫌な場合も上記のようにサブクラスを作成して対処する必要があります。
マーカーを入れる
マーカーは縦横の線またはシンボルを描画することができます。また、マーカーにテキストのラベルを付加することができます。
Qwt付属のサンプルではピーク値の表示に使ったりしています。
以下の例は3種類のマーカーを作成しています。
1: 2: // QwtPlotをメンバに持つウィンドウクラスのコンストラクタ 3: // m_pPlotがQwtPlotクラスのポインタ 4: MainWindowGui::MainWindowGui() 5: { 6: // ポイントマーカー 7: m_pPMarker = new QwtPlotMarker(); 8: m_pPMarker->setXValue( 600.0 ); 9: m_pPMarker->setYValue( 600.0 ); 10: m_pPMarker->setLabelAlignment( Qt::AlignRight | Qt::AlignTop ); 11: QwtText textPMarker( "Point marker" ); 12: m_pPMarker->setLabel( textPMarker ); 13: // ポイントシンボル 14: QwtSymbol *pSymbol = new QwtSymbol( QwtSymbol::Ellipse, QBrush( QColor::fromRgb( 0, 0, 0 ) ), 15: QPen( QColor::fromRgb( 0, 0, 0 ) ), QSize( 20, 10 ) ); 16: m_pPMarker->setSymbol( pSymbol ); 17: m_pPMarker->attach( m_pPlot ); 18: 19: // 横ラインマーカー 20: m_pHMarker = new QwtPlotMarker(); 21: m_pHMarker->setXValue( 200.0 ); 22: m_pHMarker->setLabelAlignment( Qt::AlignRight | Qt::AlignTop ); 23: m_pHMarker->setLineStyle( QwtPlotMarker::VLine ); 24: m_pHMarker->setLinePen( QPen( Qt::magenta, 0, Qt::DashDotDotLine ) ); 25: QwtText textHMarker( "Virtical line marker" ); 26: m_pHMarker->setLabel( textHMarker ); 27: m_pHMarker->attach( m_pPlot ); 28: 29: // 縦ラインマーカー 30: m_pVMarker = new QwtPlotMarker(); 31: m_pVMarker->setYValue( 100.0 ); 32: m_pVMarker->setLabelAlignment( Qt::AlignRight | Qt::AlignTop ); 33: m_pVMarker->setLineStyle( QwtPlotMarker::HLine ); 34: m_pVMarker->setLinePen( QPen( Qt::cyan, 0, Qt::DashDotDotLine ) ); 35: QwtText textVMarker( "Horizontal line marker" ); 36: m_pVMarker->setLabel( textVMarker ); 37: m_pVMarker->attach( m_pPlot ); 38: 39: }
実行例
上記の例は全てのマーカーでテキストを右寄せ上寄せに設定してあります。
マーカーの見た目はいろいろと細かい設定が可能です。
ラバーバンドを描画する
プロット上に選択枠のような一時図形を描画する場合はQwtPlotPicker
クラスを利用します。
このクラスは上記のズーム枠の親クラスです。
使い方はそれほど難しくありませんが、いくつかトラップがあります。
以下の例では、QwtPlot
派生クラスのコンストラクタでQwtPlotPicker
オブジェクトを作成して各種設定を行っています。
1: 2: MyPlot::MyPlot( QWidget *parent ) 3: : QwtPlot( parent ) 4: { 5: // Picker 6: m_pPicker = new QwtPlotPicker( canvas() ); 7: m_pPicker->setRubberBandPen( QColor( Qt::darkRed ) ); 8: m_pPicker->setTrackerMode( QwtPicker::ActiveOnly ); 9: } 10:
マウスボタンの設定がありません。マウスボタンの設定はQwtPickerMachine
の派生クラスで管理しているようです。
例として、ズーム枠のように矩形を描画する場合は以下のようにします。
m_pPicker->setRubberBand( QwtPicker::RectRubberBand );
m_pPicker->setStateMachine( new QwtPickerDragRectMachine );
QwtPickerDragRectMachine
は左ボタンを押しながらドラッグして矩形を描画し、離すと終了という設定になります。
同様に楕円を描画する場合は以下のようにします。
m_pPicker->setRubberBand( QwtPicker::EllipseRubberBand );
m_pPicker->setStateMachine( new QwtPickerDragRectMachine );
問題はポリゴンを描画する場合です。
基本的には上記と同じような設定でいいはずです。
m_pPicker->setRubberBand( QwtPicker::PolygonRubberBand );
m_pPicker->setStateMachine( new QwtPickerPolygonMachine );
StateMachineがQwtPickerPolygonMachine
に変わっています。
このStateMachineは、ドキュメントでは左ボタンで点追加、右ボタンで終了となっていますが、
2012年4月時点では、実際動かしてみると左ボタンで最初の1点、右ボタンで2点目以降を追加、左ボタンで終了になっています。
これをドキュメントと同じ様な動作にするためにはQwtのコードに手を入れる必要があります。
以下のように何行かをコメントアウトすればいいようです。
行番号はぜんぜん違いますのでご了承ください。
qwt_picker_machin.cpp(379行目~、2012年4月時点)
1: //! Transition 2: QList<QwtPickerMachine::Command> QwtPickerPolygonMachine::transition( 3: const QwtEventPattern &eventPattern, const QEvent *event ) 4: { 5: QList<QwtPickerMachine::Command> cmdList; 6: 7: switch ( event->type() ) 8: { 9: case QEvent::MouseButtonPress: 10: { 11: if ( eventPattern.mouseMatch( 12: QwtEventPattern::MouseSelect1, ( const QMouseEvent * )event ) ) 13: { 14: // if ( state() == 0 ) 15: // { 16: cmdList += Begin; 17: cmdList += Append; 18: cmdList += Append; 19: setState( 1 ); 20: /* } 21: else 22: { 23: cmdList += End; 24: setState( 0 ); 25: }*/ 26: } 27: if ( eventPattern.mouseMatch( 28: QwtEventPattern::MouseSelect2, ( const QMouseEvent * )event ) ) 29: { 30: if ( state() == 1 ) 31: // cmdList += Append; 32: cmdList += End; 33: } 34: break; 35: }
さて、ラバーバンドで描画した図形を確定図形としてプロット上に描画したいとします。
その場合、ラバーバンド描画終了時の図形データをラバーバンドクラスから取り出す必要があるかと思います。
ここでは、ラバーバンド描画終了時のシグナルを捕まえて図形を取り出す作戦を採用します。
上の例ではQwtPlot
派生クラスにラバーバンドクラスがあるので、
プロットアイテムをプロットの親ウィンドウ側で管理する場合はシグナルをさらに親ウィンドウへ転送する必要があります。
コンストラクタで以下のように設定します。
// 矩形、楕円選択の場合 QObject::connect( m_pPicker, SIGNAL( selected( const QRectF& ) ), this, SIGNAL( selected( const QRectF& ) ) ); // ポリゴン選択の場合 QObject::connect( m_pPicker, SIGNAL( selected( const QVector< QPointF >& ) ), this, SIGNAL( selected( const QVector< QPointF >& ) ) );
QwtPlot
派生クラスの親ウィンドウで転送されてきたシグナルを捕まえます。
// 矩形、楕円選択の場合 QObject::connect( m_pPlot, SIGNAL( selected( const QRectF& ) ), this, SLOT( appendPoly( const QRectF& ) ) ); // ポリゴン選択の場合 QObject::connect( m_pPlot, SIGNAL( selected( const QVector< QPointF >& ) ), this, SLOT( appendPoly( const QVector< QPointF >& ) ) );
シグナルの引数がラバーバンドの矩形または頂点になっているので、スロットで図形を追加すればいいでしょう。
問題は楕円の場合で、楕円選択の場合でも矩形選択のスロットが帰ってくるので、
選択図形の楕円をスロット側で再作成する必要があるようです。
1: // 矩形の場合のスロット 2: void ParentWnd::appendPoly( const QRectF& rc ) 3: { 4: QVector<QPointF> tempPoly; 5: QPointF pnt; 6: 7: if ( /* 楕円選択の場合 */ ) 8: { 9: double dAlpha = 0.0; 10: 11: while ( dAlpha < 2*M_PI ) 12: { 13: pnt.setX( cos( dAlpha ) * rc.width() * 0.5 + rc.center().x() ); 14: pnt.setY( sin( dAlpha ) * rc.height() * 0.5 + rc.center().y() ); 15: tempPoly.push_back( pnt ); 16: dAlpha += M_PI / 128; 17: } 18: } 19: else /* 矩形選択の場合 */ 20: { 21: tempPoly.push_back( rc.topLeft() ); 22: tempPoly.push_back( rc.bottomLeft() ); 23: tempPoly.push_back( rc.bottomRight() ); 24: tempPoly.push_back( rc.topRight() ); 25: tempPoly.push_back( rc.topLeft() ); 26: } 27: 28: // QwtPlotCurveとして図形を追加 29: QwtPlotCurve *pCurve = new QwtPlotCurve(); 30: pCurve->setSamples( tempPoly ); 31: pCurve->setPen( QPen( QBrush( QColor::fromRgb( 255, 0, 0 ) ), 2.0 ) ); 32: pCurve->attach( m_pPlot ); 33: } 34: 35: 36: // ポリゴンの場合のスロット 37: void ParentWnd::appendPoly( const QVector< QPointF >& poly ) 38: { 39: // 点列の末尾に始点を追加してポリゴンを閉じる 40: QVector<QPointF> tempPoly( poly ); 41: tempPoly.push_back( tempPoly.front() ); 42: 43: // QwtPlotCurveとして図形を追加 44: QwtPlotCurve *pCurve = new QwtPlotCurve(); 45: pCurve->setSamples( tempPoly ); 46: pCurve->setPen( QPen( QBrush( QColor::fromRgb( 255, 0, 0 ) ), 2.0 ) ); 47: pCurve->attach( m_pPlot ); 48: }
プロット画面をエクスポート/印刷する
プロット画面をマーカーや軸とともにエクスポートまたは印刷するのは比較的簡単にできます。
どちらもQwtPlotRenderer
を使用します。
下のコードは、SVGにエクスポートおよび印刷の例です。
1: 2: // 印刷 3: void ParentWnd::onPrint() 4: { 5: // m_printerはQPrinterクラスのオブジェクト 6: QPrintDialog dlgPrint( &m_printer, this ); 7: if ( dlgPrint.exec() ) 8: { 9: QwtPlotRenderer renderer; 10: // m_pPlotはQwtPlotオブジェクトのポインタ 11: renderer.renderTo( m_pPlot, m_printer ); 12: } 13: } 14: 15: 16: // SVGへエクスポート 17: void ParentWnd::exportSVG( QString strFName ) 18: { 19: QwtPlotRenderer renderer; 20: 21: // 第3引数でフォーマットを指定 22: // 第4引数はドキュメントサイズをミリ単位で指定 23: // 第5引数は解像度(dpi) 24: renderer.renderDocument( m_pPlot, strFName, tr("svg"), QSizeF( 210, 210 ), 300 ); 25: }
ただし、プロットの縦横比を固定した場合でも印刷時の縦横比は紙サイズに合わせて変化してしまうようです。
印刷時でも縦横比を固定したい場合はQwtPlotRenderer::renderTo()
関数を使用するのではなく、QwtPlotRenderer::render()
関数を使用して印刷時の範囲を指定する必要があります。
QRectF rcPrint; QPainter printPainter( &m_printer ); rcPrint.setWidth( 100 ); rcPrint.setHeight( 100 ); renderer.render( m_pPlot, &printPainter, rcPrint );