Pull to refresh

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

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

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

Сразу оговорюсь, что ниже предоставленный код демонстрирует только основные используемые фишки. Полную версию, если кому любопытно может взять здесь.
Первое удобство фреймворка открывается уже на этапе проектирования. Итак, план работ, который над подсказывает архитектура нашего инструмента:
  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-ов с данными.
  • Наша иерархия классов на порядок проще. И при дальнейшем расширении не видно причин, почему она должна стать сложнее.


Ссылки


Документация, развернутая с примерами.
Видео, если нельзя скачать с офф сайта, то спокойно находятся на youtube
Проект.

P.S. Проект демонстрационный, но если будут найдены баги, или кто поможет с улучшением — буду только рад.
P.P.S На всякий случай: текст опубликован под лицензией CC-BY 3.0

UPD итог полустараний:
Tags:
Hubs:
+27
Comments 18
Comments Comments 18

Articles