- PVSM.RU - https://www.pvsm.ru -
На мой взгляд Qt Graphics Scene FrameWork — мощный инструмент, незаслуженно обделенный вниманием на Хабре. Я попытаюсь исправить ситуацию, посвятив ему цикл статей. И в этой, пилотной, статье покажу как можно программировать с помощью этого замечательного фреймворка на примере более-менее реальной задачи.
И в качестве такой задачи я выбрал построение графиков. В ней есть все:
Сразу оговорюсь, что ниже предоставленный код демонстрирует только основные используемые фишки. Полную версию, если кому любопытно может взять здесь [1].
Первое удобство фреймворка открывается уже на этапе проектирования. Итак, план работ, который над подсказывает архитектура нашего инструмента:
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()
}
Теперь приступим к рисовании сетки. Надо заметить, что первоначально представлялось, что метки надо рисовать вместе с координатными линиями. Однако такой подход идет вразрез с декларативной идеологией фреймворка: описать как в простейшем случае должен выглядеть item, а затем рассказать сцене как она должна с ним поступать, и на выходе получить идеальную картинку в любых условиях. И в итоге верстка засечек была перенесена в compose.
А сейчас пока обойдемся без них и нарисуем просто координатную сетку. Наша основная идея: 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);
}
Тут есть один тонкий момент: при увеличении с помощью QTransform нашего gridItem размер кисти тоже растет, чтоб этого не происходило необходимо задать QPen как cosmetic:
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:
Документация [2], развернутая с примерами.
Видео [3], если нельзя скачать с офф сайта, то спокойно находятся на youtube
Проект [1].
P.S. Проект демонстрационный, но если будут найдены баги, или кто поможет с улучшением — буду только рад.
P.P.S На всякий случай: текст опубликован под лицензией CC-BY 3.0
Автор: DancingOnWater
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/35978
Ссылки в тексте:
[1] здесь: http://github.com/DancingOnWater/GraphicsScenePlot
[2] Документация: http://qt-project.org/doc/qt-5.0/qtwidgets/graphicsview.html
[3] Видео: http://qt-project.org/videos#c-85
[4] Источник: http://habrahabr.ru/post/182142/
Нажмите здесь для печати.