- PVSM.RU - https://www.pvsm.ru -

Красота и мощь Qt Graphics View Framework на примере

На мой взгляд Qt Graphics Scene FrameWork — мощный инструмент, незаслуженно обделенный вниманием на Хабре. Я попытаюсь исправить ситуацию, посвятив ему цикл статей. И в этой, пилотной, статье покажу как можно программировать с помощью этого замечательного фреймворка на примере более-менее реальной задачи.

И в качестве такой задачи я выбрал построение графиков. В ней есть все:

  • Различные виды объектов: как текст, так и различные комбинации примитивов
  • Задача композиции: необходимо расположить заголовок, подписи к осям, ну и саму координатную область с графиками
  • Задачи перевода из одних координат в другую: из системы отсчета, связанных с данными, в отображаемую
  • Взаимодействие между элементами: как минимум при добавлении, удалении и изменении графика обновлять легенду.

Сразу оговорюсь, что ниже предоставленный код демонстрирует только основные используемые фишки. Полную версию, если кому любопытно может взять здесь [1].
Первое удобство фреймворка открывается уже на этапе проектирования. Итак, план работ, который над подсказывает архитектура нашего инструмента:

  1. Создадим сцену, на которой будем рисовать графики: скомпонуем подписи к осям и координатную область.
  2. Создадим координатную сетку (И здесь решим, как будем поступать с графиками).
  3. Создадим Item для графика.
  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()
}
Создание координатной сетки

Теперь приступим к рисовании сетки. Надо заметить, что первоначально представлялось, что метки надо рисовать вместе с координатными линиями. Однако такой подход идет вразрез с декларативной идеологией фреймворка: описать как в простейшем случае должен выглядеть 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);
Item графика

Объявление класса

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. Простата создания item-ов для отображения данных

Сравнение и калибровка по qwt.

Чтоб оценить удобство давайте проведем сравнение объема работ, проделанного нами и тем, что сделали уважаемые разработчики qwt:

  • Первое, что бросается в глаза — огромные листинги в drawItem. Наши на порядок короче. Нам не надо заботиться об оптимизации отрисовки каждого члена. Мы делаем один раз и в одном месте — когда задаем viewport..
  • Куча трудов вложено в QwtLegendData, QwtLegendLabel, QwtPainter, QwtPainterCommand, QwtPlotDirectPainter и т.д. Мы всего это не делали и не ясны причины зачем в нашей ситуации все это реализовывать.
  • Нам не надо писать свои классы трансформации и пересчета координат из одной системы в другую, и нам не надо вручную производить трансформацию координат.
  • Мы добились гораздо большей абстрагированности iem-ов с данными.
  • Наша иерархия классов на порядок проще. И при дальнейшем расширении не видно причин, почему она должна стать сложнее.

Ссылки

Документация [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/