例としてのQt Graphics View Frameworkの美しさとパワー

私の意見では、Qt Graphics Scene FrameWorkは強力なツールであり、Habréの注目を集めるに値しません。 私は彼に一連の記事を捧げることで状況を修正しようとします。 このパイロット記事では、この素晴らしいフレームワークを使用して、実際のタスクを例にプログラムを作成する方法を示します。



そして、そのようなタスクとして、グラフの構築を選択しました。 それにはすべてがあります:



すぐに、以下のコードが使用されるメインチップのみを示すことを予約してください。 フルバージョンは、誰かが興味を持っている場合は、 ここで入手できます

フレームワークの最初の利便性は、設計段階で開きます。 そのため、ツールのアーキテクチャを提案する作業計画:

  1. グラフを描画するシーンを作成しましょう。軸と座標領域のラベルを作成します。
  2. 座標グリッドを作成します(そして、ここでグラフをどうするかを決定します)。
  3. スケジュールのアイテムを作成します。
  4. 凡例を作成します。


最初の段階。 コンポジションを作成します。


非表示のテキスト
class GraphicsPlotNocksTube : public QGraphicsItem { public: GraphicsPlotNocksTube(QGraphicsItem *parent): QGraphicsItem(parent){} void updateNocks(const QList<QGraphicsSimpleTextItem*>& nocks); QRectF boundingRect()const {return m_boundRect;} void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *){} inline const QFont &font(){return m_NocksFont;} private: QList<QGraphicsSimpleTextItem*> m_nocks; QFont m_NocksFont; QPen m_nockPen; QRectF m_boundRect; }; class Graphics2DPlotGrid: public QGraphicsItem { public: Graphics2DPlotGrid(QGraphicsItem * parent); QRectF boundingRect() const; const QRectF & rect() const; void setRange(int axisNumber, double min, double max); void setMainGrid(int axisNumber, double zero, double step); void setSecondaryGrid(int axisNumber, double zero, double step); void setMainGridPen(const QPen & pen); void setSecondaryGridPen(const QPen &pen); inline QPen mainGridPen(){return m_mainPen;} inline QPen secondaryGridPen(){return m_secondaryPen;} void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); public: struct AxisGuideLines { AxisGuideLines(): showLines(true){} QVector<QLineF> lines; bool showLines; }; AxisGuideLines abscissMainLines; AxisGuideLines abscissSecondaryLines; AxisGuideLines ordinateMainLines; AxisGuideLines ordinateSecondaryLines; private: void paintAxeGuidLines(const AxisGuideLines& axe, QPainter *painter, const QPen &linePen); QPen m_mainPen; QPen m_secondaryPen; QRectF m_rect; }; void Graphics2DPlotGrid::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) paintAxeGuidLines(abscissSecondaryLines, painter, m_secondaryPen); paintAxeGuidLines(abscissMainLines, painter, m_mainPen); paintAxeGuidLines(ordinateSecondaryLines, painter, m_secondaryPen); paintAxeGuidLines(ordinateMainLines, painter, m_mainPen); painter->setPen(m_mainPen); painter->drawRect(m_rect); } class GraphicsPlotItemPrivate { Q_DECLARE_PUBLIC(GraphicsPlotItem) GraphicsPlotItem* q_ptr; GraphicsPlotItemPrivate(GraphicsPlotItem* parent); void compose(); void calculateAndSetTransForm(); void autoSetRange(); void autoSetGrid(); void calculateOrdinateGrid(); void calculateAbscissGrid(); void setAxisRange(int axisNumber, double min, double max); Graphics2DPlotGrid * gridItem; QGraphicsSimpleTextItem * abscissText; QGraphicsSimpleTextItem * ordinateText; QGraphicsSimpleTextItem *titleText; QFont titleFont; QFont ordinaateFont; QFont abscissFont; QRectF rect; QRectF m_sceneDataRect; GraphicsPlotLegend *m_legend; GraphicsPlotNocksTube* ordinateMainNocks; GraphicsPlotNocksTube* ordinateSecondaryNocks; GraphicsPlotNocksTube* abscissSecondaryNocks; GraphicsPlotNocksTube* abscissMainNocks; struct Range{ double min; double max; }; struct AxisGuideLines { AxisGuideLines():baseValue(0.0), step(0.0){} double baseValue; double step; }; AxisGuideLines abscissMainLines; AxisGuideLines abscissSecondaryLines; AxisGuideLines ordinateMainLines; AxisGuideLines ordinateSecondaryLines; Range abscissRange; Range ordinateRange; bool isAutoGrid; bool isAutoSecondaryGrid; public: void range(int axisNumber, double *min, double *max); };
      
      









私たちは構成します:



 void GraphicsPlotItemPrivate::compose() { titleText->setFont(titleFont); abscissText->setFont(abscissFont); if(titleText->boundingRect().width() > rect.width()){ //TODO case when titleText too long } //Composite by height qreal dataHeight = rect.height() - 2*titleText->boundingRect().height() - 2*(abscissText->boundingRect().height()); if(dataHeight < 0.5*rect.height()){ //TODO decrease font size } titleText->setPos((rect.width()-titleText->boundingRect().width())/2.0, rect.y()); //Compose by width qreal dataWidth = rect.width()-2*ordinateText->boundingRect().height(); if(dataWidth< 0.5*rect.width()){ //TODO decrease font size } ordinateMainNocks->setPos(-ordinateMainNocks->boundingRect().width(), -5*ordinateMainNocks->font().pointSizeF()/4.0); m_sceneDataRect.setRect(rect.width()-dataWidth, 2*titleText->boundingRect().height() , dataWidth, dataHeight); abscissText->setPos( (dataWidth - abscissText->boundingRect().width())/2.0 + m_sceneDataRect.y(), rect.bottom() - abscissText->boundingRect().height()); ordinateText->setPos(0, (dataHeight - ordinateText->boundingRect().width())/2.0 + m_sceneDataRect.y()); calculateAndSetTransForm(); q_ptr->update() }
      
      







グリッドを作成




それでは、グリッドの描画を始めましょう。 最初は、ラベルを座標線と一緒に描画する必要があるように思われたことに注意してください。 ただし、このアプローチは、フレームワークの宣言的イデオロギーに反するものです。最も単純なケースでアイテムがどのように見えるべきかを説明し、それをどのように扱うべきかをシーンに伝え、あらゆる条件で完全な画像を取得します。 そして最終的に、セリフのレイアウトは作曲に移されました。



それでは、それらを使用せずに、グリッドを描画してみましょう。 主なアイデアは、gridItemをグラフデータと同じスケールで描画し、Qtに表示された座標への変換を行わせることです。 グラフをgridItemの子孫にすると、既製のソリューションが得られます。





実装:

 void Graphics2DPlotGrid::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) paintAxeGuidLines(abscissSecondaryLines, painter, m_secondaryPen); paintAxeGuidLines(abscissMainLines, painter, m_mainPen); paintAxeGuidLines(ordinateSecondaryLines, painter, m_secondaryPen); paintAxeGuidLines(ordinateMainLines, painter, m_mainPen); painter->setPen(m_mainPen); painter->drawRect(m_rect); } void Graphics2DPlotGrid::paintAxeGuidLines(const AxisGuideLines& axe, QPainter *painter, const QPen &linePen) { if(axe.showLines){ painter->setPen(linePen); painter->drawLines(axe.lines); } } void GraphicsPlotItemPrivate::calculateAndSetTransForm() { double scaleX = m_sceneDataRect.width()/gridItem->rect().width(); double scaleY = m_sceneDataRect.height()/gridItem->rect().height(); QTransform transform = QTransform::fromTranslate( - gridItem->rect().x()*scaleX + m_sceneDataRect.x(), - gridItem->rect().y()*scaleY +m_sceneDataRect.y()); transform.scale(scaleX, -scaleY); gridItem->setTransform(transform); ordinateMainNocks->setTransform(transform); // ordinateSecondaryNocks->setTransform(transform); abscissMainNocks->setTransform(transform); // abscissSecondaryNocks->setTransform(transform); }
      
      







グリッドを計算する
 void GraphicsPlotItemPrivate::calculateOrdinateGrid() { const QRectF r = gridItem->boundingRect(); if(fabs(r.width()) < std::numeric_limits<float>::min()*5.0 || fabs(r.height()) < std::numeric_limits<float>::min()*5.0) return; QList<QGraphicsSimpleTextItem*> nocksList; auto calculteLine = [&] (AxisGuideLines* guides, QVector<QLineF> *lines) { int k; double minValue; int count; nocksList.clear(); if(fabs(guides->step) > std::numeric_limits<double>::min()*5.0 ) { k = (ordinateRange.min - guides->baseValue)/guides->step; minValue = k*guides->step+guides->baseValue; count = (ordinateRange.max - minValue)/guides->step; //TODO   ,     if( count >0){ lines->resize(count); nocksList.reserve(count); double guidCoordinate; for(int i = 0; i< count; i++){ guidCoordinate = minValue+i*guides->step; lines->operator[](i) = QLineF(abscissRange.max, guidCoordinate, abscissRange.min, guidCoordinate); nocksList.append(new QGraphicsSimpleTextItem(QString::number(guidCoordinate))); nocksList.last()->setPos(abscissRange.min, guidCoordinate); } } else lines->clear(); } else lines->clear(); }; calculteLine(&ordinateMainLines, &(gridItem->ordinateMainLines.lines)); ordinateMainNocks->updateNocks(nocksList); calculteLine(&ordinateSecondaryLines, &(gridItem->ordinateSecondaryLines.lines)); ordinateSecondaryNocks->updateNocks(nocksList); }
      
      









微妙な点が1つあります。gridItemのQTransformを使用して増加すると、ブラシサイズも大きくなるため、これが発生しないようにするには、QPenを化粧品として設定する必要があります。

  m_secondaryPen.setCosmetic(true); m_mainPen.setCosmetic(true);
      
      







アイテムのグラフィック


クラス宣言
 class GraphicsDataItem: public QGraphicsObject { Q_OBJECT public: GraphicsDataItem(QGraphicsItem *parent =0); ~GraphicsDataItem(); void setPen(const QPen& pen); QPen pen(); void setBrush(const QBrush & brush); QBrush brush(); void ordinateRange(double *min, double *max); void abscissRange(double *min, double *max); void setTitle(const QString & title); QString title(); inline int type() const {return GraphicsPlot::DataType;} Q_SIGNALS: void dataItemChange(); void penItemChange(); void titleChange(); protected: void setOrdinateRange(double min, double max); void setAbscissRange(double min, double max); private: Q_DECLARE_PRIVATE(GraphicsDataItem) GraphicsDataItemPrivate *d_ptr; }; class Graphics2DGraphItem: public GraphicsDataItem { Q_OBJECT public: Graphics2DGraphItem(QGraphicsItem *parent =0); Graphics2DGraphItem(double *absciss, double *ordinate, int length, QGraphicsItem *parent =0); ~Graphics2DGraphItem(); void setData(double *absciss, double *ordinate, int length); void setData(QList<double> absciss, QList<double> ordinate); void setData(QVector<double> absciss, QVector<double> ordinate); QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); private: Q_DECLARE_PRIVATE(Graphics2DGraphItem) Graphics2DGraphItemPrivate *d_ptr; };
      
      







グラフの実装は非常に単純であり、ほとんどのコードはboundRect境界を明確にすることで占められています。

 class Graphics2DGraphItemPrivate { Q_DECLARE_PUBLIC(Graphics2DGraphItem) Graphics2DGraphItem *q_ptr; Graphics2DGraphItemPrivate(Graphics2DGraphItem *parent):q_ptr(parent){} QVector<QLineF> m_lines; template<typename T> void setData(T absciss, T ordinate, qint32 length) { q_ptr->prepareGeometryChange(); --length; m_lines.resize(length); Range ordinateRange; ordinateRange.min = ordinate[0]; ordinateRange.max = ordinate[0]; Range abscissRange; abscissRange.min = absciss[0]; abscissRange.max = absciss[0]; for(int i =0; i < length; ++i) { if(ordinate[i+1] > ordinateRange.max) ordinateRange.max = ordinate[i+1]; else if(ordinate[i+1] < ordinateRange.min ) ordinateRange.min = ordinate[i+1]; if(absciss[i+1] > abscissRange.max) abscissRange.max = absciss[i+1]; else if(absciss[i+1] < abscissRange.min ) abscissRange.min = absciss[i+1]; m_lines[i].setLine(absciss[i], ordinate[i], absciss[i+1], ordinate[i+1]); } m_boundRect.setRect(abscissRange.min, ordinateRange.min, abscissRange.max - abscissRange.min, ordinateRange.max - abscissRange.min); q_ptr->setOrdinateRange(ordinateRange.min, ordinateRange.max); q_ptr->setAbscissRange(abscissRange.min, abscissRange.max); q_ptr->update(); QMetaObject::invokeMethod(q_ptr, "dataItemChange"); } QRect m_boundRect; }; QRectF Graphics2DGraphItem::boundingRect() const { return d_ptr->m_boundRect; } void Graphics2DGraphItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) painter->setBrush(brush()); painter->setPen(pen()); painter->drawLines(d_ptr->m_lines); }
      
      







凡例とクラスの相互作用




実際、すでに解決策があります。GraphicsDataItemがQGraphicsObjectから継承されており、信号がクラス本体で既に宣言されていることに注意してください。 つまり シーンオブジェクト間の相互作用は、通常の方法で発生します-信号とスロットを介して。



主観的結果


そして、主観的な結果はどうですか?

  1. フレームワークのロジックに従えば、手間のかからないアーキテクチャ開発。
  2. すべての要素は宣言的なスタイルで記述されており、メインコードはエッセンスを参照し、ダンスの程度はそれほどではありません。
  3. データを表示するアイテムを作成することの前立腺




Qwtの比較とキャリブレーション。





実際の利便性を評価し、主観的な感覚を調整するために、私たちが行った作業量と尊敬されているqwt開発者がしたことを比較しましょう。





参照資料



サンプルとともに展開されたドキュメント

ビデオ 、オフサイトからダウンロードできない場合は、YouTubeで簡単に見つけることができます

プロジェクト



PSこのプロジェクトはデモですが、バグが見つかった場合、または改善に協力してくれる方がいれば嬉しいです。

PPS念のため:CC-BY 3.0ライセンスで発行されたテキスト



UPDハーフエフォートの結果:








All Articles