そして、そのようなタスクとして、グラフの構築を選択しました。 それにはすべてがあります:
- さまざまなタイプのオブジェクト:テキストとプリミティブのさまざまな組み合わせの両方
- 構成タスク:見出し、ラベルを軸に配置する必要があります、まあ、グラフとの座標領域自体
- ある座標から別の座標に転送するタスク:データに関連する参照フレームから表示されたフレームへ
- 要素間の相互作用:少なくともスケジュールの追加、削除、および変更時には、凡例を更新します。
すぐに、以下のコードが使用されるメインチップのみを示すことを予約してください。 フルバージョンは、誰かが興味を持っている場合は、 ここで入手できます 。
フレームワークの最初の利便性は、設計段階で開きます。 そのため、ツールのアーキテクチャを提案する作業計画:
- グラフを描画するシーンを作成しましょう。軸と座標領域のラベルを作成します。
- 座標グリッドを作成します(そして、ここでグラフをどうするかを決定します)。
- スケジュールのアイテムを作成します。
- 凡例を作成します。
最初の段階。 コンポジションを作成します。
非表示のテキスト
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の子孫にすると、既製のソリューションが得られます。
- データスケールでグラフ線を描画するだけで十分です。 それら自体が目的の領域に表示され、追加した場合
gridItem->setFlag(QGraphicsItem::ItemClipsChildrenToShape)
- すべてのシーンイベント(キーボードイベントやマウスイベントなど)は自動的にデータスケールに変換されるため、処理が簡素化されます。
実装:
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から継承されており、信号がクラス本体で既に宣言されていることに注意してください。 つまり シーンオブジェクト間の相互作用は、通常の方法で発生します-信号とスロットを介して。
主観的結果
そして、主観的な結果はどうですか?
- フレームワークのロジックに従えば、手間のかからないアーキテクチャ開発。
- すべての要素は宣言的なスタイルで記述されており、メインコードはエッセンスを参照し、ダンスの程度はそれほどではありません。
- データを表示するアイテムを作成することの前立腺
Qwtの比較とキャリブレーション。
実際の利便性を評価し、主観的な感覚を調整するために、私たちが行った作業量と尊敬されているqwt開発者がしたことを比較しましょう。
- 最初に目を引くのは、drawItemの巨大なリストです。 私たちのものはずっと短いです。 各メンバーのレンダリングの最適化について心配する必要はありません。 ビューポートを設定するときに、一度と一か所でそれを行います..
- QwtLegendData、QwtLegendLabel、QwtPainter、QwtPainterCommand、QwtPlotDirectPainterなどに多くの作業が投資されています。 私たちはこれをすべてやったわけではなく、私たちの状況でこれをすべて実装する理由は明らかではありません。
- あるシステムから別のシステムへの座標の変換および変換の独自のクラスを記述する必要はなく、座標を手動で変換する必要もありません。
- データを使用したiemの抽象性が大幅に向上しました。
- クラス階層は一桁単純です。 そして、さらに拡張しても、それがより複雑になる理由はわかりません。
参照資料
サンプルとともに展開されたドキュメント 。
ビデオ 、オフサイトからダウンロードできない場合は、YouTubeで簡単に見つけることができます
プロジェクト
PSこのプロジェクトはデモですが、バグが見つかった場合、または改善に協力してくれる方がいれば嬉しいです。
PPS念のため:CC-BY 3.0ライセンスで発行されたテキスト
UPDハーフエフォートの結果: