Автоматическая подстройка диапазона оси значений QValueAxis на гистограммах Qt Charts

Добавлено 16 января 2021 в 16:49

Часто в разработке реальных приложений точный диапазон значений, которые необходимо вывести на гистограмму, бывает заранее неизвестен. Максимальные ожидаемые значения могут различаться в десятки и сотни раз. Поэтому установка шкал значений на гистограмме в какое-то предельно максимальное значение будет не лучшим решением – пострадает наглядность представления данных при выводе малых значений, по крайней мере, до выполнения пользователем ручной настройки шкалы.

В данной статье рассматривается, как автоматически изменять диапазон оси значений QValueAxis на гистограмме Qt Charts, исходя из значений, принимаемых наборами полос (объектами QBarset) из модели данных.

Пример автоматической подстройки диапазона оси значений QValueAxis

В данном примере на гистограмме выводятся получаемые из модели BitrateTableModel данные о скоростях элементарных потоков (в кбит/с).

Элементарные потоки со значениями PID, заканчивающимися на единицу, – это потоки видео. В момент запуска приложения их скорости составляют 2650-2750 кбит/с, а через 5 секунд после запуска их скорости увеличиваются до 7600-7700 кбит/с. Подстройка шкалы оси значений происходит в первый раз в момент запуска приложения и второй раз в момент изменения скоростей видеопотоков.

Создание диаграммы

Для простоты всё создание диаграммы выполним в конструкторе объекта класса, который наследуется от QMainWindow.

Процесс создания гистограммы практически аналогичен примеру, приведенному в статье «Использование гистограмм Qt Charts совместно с моделями данных», за исключением того, что гистограмма данной статье будет использоваться горизонтальная, плюс добавилась обработка выводимых на ней значений. Поэтому некоторые подробности будут опущены.

Начнем с создания экземпляра класса BitrateTableModel. Класс BitrateTableModel является производным от QAbstractTableModel и был создан только для этого примера. Конструктор этого класса заполняет внутреннее хранилище данных модели данными, необходимыми для нашего примера работы с диаграммами. А после создания модель каждые 500 мс обновляет данные о скоростях элементарных потоков псевдослучайными значениями.

model = new BitrateTableModel;

Данные мы хотим отображать не только на гистограмме, но и в таблице (объекте QTableView).

ui->tableView->setModel(model);
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
model->setParent(ui->tableView);

Теперь нам нужен экземпляр QChart для отображения на гистограмме тех же данных.

chart = new QChart;

Создаем новый объект последовательности полос QHorizontalBarSeries. Следующие три строки создают преобразователь данных, экземпляр класса QVBarModelMapper, и указывают, что данные для наборов полос должны быть взяты из столбца модели с индексом 1 (значение BitrateTableModel::ColumnBitrate). Чтобы создать связь между последовательностью полос и моделью, передаем оба этих объекта преобразователю данных, т.е. объекту mapper класса QVBarModelMapper.

Наконец, добавляем к диаграмме последовательность полос series.

QHorizontalBarSeries *series = new QHorizontalBarSeries;

mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(BitrateTableModel::ColumnBitrate);
mapper->setLastBarSetColumn(BitrateTableModel::ColumnBitrate);

mapper->setSeries(series);
mapper->setModel(model);

chart->addSeries(series);

Также неплохо было бы, чтобы на оси диаграммы были подписаны категории, которые описывали бы суть данных. Категории в данном примере – это значения идентификаторов пакетов (PID) элементарных потоков, хранящиеся в модели BitrateTableModel в столбце 0 (значение BitrateTableModel::ColumnPid). Следующий цикл сохраняет значения категорий в объект QStringList categories, плюс определяет максимальное текущее значение скорости, которое необходимо отобразить на гистограмме.

int rowCount = model->rowCount();
QStringList categories;
qreal maxValue = 0;
for(int i = 0; i < rowCount; ++i)
{
    QModelIndex idx = model->index(i, BitrateTableModel::ColumnPid);
    categories << model->data(idx).toString();

    idx = model->index(i, BitrateTableModel::ColumnBitrate);
    qreal curValue = model->data(idx).toDouble();
    if (curValue > maxValue)
        maxValue = curValue;
}

Создаем ось категорий (объект QBarCategoryAxis). Делаем ее линии сетки невидимыми и передаем ей сформированный список категорий. После чего добавляем ее на диаграмму (в этом случае она будет вертикальной, слева) и прикрепляем к последовательности полос.

QBarCategoryAxis *axisY = new QBarCategoryAxis();
axisY->setGridLineVisible(false);
axisY->setLineVisible(false);
axisY->append(categories);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);

Создаем ось значений (объект QValueAxis). Добавляем ее на диаграмму (она будет горизонтальной, внизу) и тоже прикрепляем к последовательности полос. После чего по определенному ранее максимальному значению, которое будет выведено на гистограмме, устанавливаем максимальное значение шкалы оси. Функция setAxisXMaxFromVal(), выполняющая эту задачу, будет описана ниже.

QValueAxis *axisX = new QValueAxis();
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
setAxisXMaxFromVal(maxValue, axisX);

Но нам необходимо, чтобы шкала оси значений QValueAxis подстраивалась автоматически не только в момент запуска, но и во время работы приложения, поскольку скорости предполагаемых элементарных потоков могут внезапно достаточно сильно увеличиться. Для этого мы будем обрабатывать изменения значений каждой выводимой на гистограмме полосы, т.е. каждой полосы в каждом объекте QBarSet.

В следующем фрагменте кода мы получаем все объекты QBarSet в нашей последовательности (так как мы выводим на диаграмму только один столбец модели, то в нашем случае объект QBarSet будет всего один) и к сигналу valueChanged(int index) каждого из них привязываем слот processBarSetValueChange(int index), обрабатывающий изменения значений.

QList<QBarSet*> barsets = series->barSets();
for(int i = 0; i < barsets.size(); ++i)
{
    connect(barsets.at(i), &QBarSet::valueChanged,
            this, &MainWindow::processBarSetValueChange);
}

Чтобы избежать настройки QGraphicsScene, мы используем класс QChartView, который делает это за нас. Указатель на объект QChart используется как параметр конструктора QChartView. Чтобы визуализация выглядела лучше, включаем антиалиасинг и устанавливаем минимальный размер виджета chartView.

chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumSize(640, 480);

И, наконец, помещаем виджет chartView в специально подготовленный в дизайнере виджет QGroupBox.

ui->chartGroupBox->layout()->addWidget(chartView);

Подстройка верхнего предела шкалы оси значений

Проверка необходимости изменения размера шкалы оси значений во время работы приложения выполняется в слоте processBarSetValueChange(int index), который ранее был привязан к сигналам valueChanged(int index) объектов QBarSet.

Здесь мы находим нашу ось значений QValueAxis, сравниваем ее максимальное значение с полученным измененным значением объекта QBarSet, выдавшего сигнал, и при необходимости изменяем верхний предел шкалы оси с помощью функции setAxisXMaxFromVal().

void MainWindow::processBarSetValueChange(int index)
{
    QList<QAbstractAxis *> hAxises = chart->axes(Qt::Horizontal);
    if(hAxises.isEmpty())
        return;
    QValueAxis *axisX = qobject_cast<QValueAxis *>(hAxises.at(0));

    QBarSet *barset = qobject_cast<QBarSet*>(sender());
    qreal value = barset->at(index);

    if (value > axisX->max())
        setAxisXMaxFromVal(value, axisX);
}

Изменение верхнего предела шкалы оси значений выполняется в функции setAxisXMaxFromVal(). Для удобства пользователя в зависимости от максимального отображаемого значения выбираем верхний предел шкалы из ряда 5, 10, 50, 100, 500, 1000 и т.д. И устанавливаем определенное значение в качестве предельного значения шкалы оси значений QValueAxis.

void MainWindow::setAxisXMaxFromVal(double val, QValueAxis *axis)
{
    int degree = (int)floor(log10(abs(val)));
    int scaled = val * pow(10, -degree);
    int step2 = ((scaled +4)/5)*5;
    int maxValue = step2 * pow(10, degree);

    axis->setMax(maxValue);
}

В результате получаем приложение следующего вида. Первый скриншот – сразу после запуска, второй – через 5 секунд.

Рисунок 1 Пример приложения для демонстрации подстройки шкалы оси значений гистограммы Qt Charts
Рисунок 1 – Пример приложения для демонстрации подстройки шкалы оси значений гистограммы Qt Charts
Рисунок 2 Пример приложения для демонстрации подстройки шкалы оси значений гистограммы Qt Charts
Рисунок 2 – Пример приложения для демонстрации подстройки шкалы оси значений гистограммы Qt Charts

Исходный код

Полный исходный код проекта доступен на GitHub.

Также ниже приведен код исходного файлов mainwindow.cpp и mainwindow.h.

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include "bitratetablemodel.h"

#include <QtCharts>

#include <QSortFilterProxyModel>

QT_CHARTS_USE_NAMESPACE

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

public slots:
    void processBarSetValueChange(int index);
    void setAxisXMaxFromVal(double val, QValueAxis *axis);

private:
    Ui::MainWindow *ui;

    BitrateTableModel *model;

    QVBarModelMapper *mapper;
    QChart *chart;
    QChartView *chartView;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QDebug>

//---------------------------------------------------------------------------------------
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    model = new BitrateTableModel;

    ui->tableView->setModel(model);
    ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    model->setParent(ui->tableView);

    chart = new QChart;

    QHorizontalBarSeries *series = new QHorizontalBarSeries;

    mapper = new QVBarModelMapper(this);
    mapper->setFirstBarSetColumn(BitrateTableModel::ColumnBitrate);
    mapper->setLastBarSetColumn(BitrateTableModel::ColumnBitrate);

    mapper->setSeries(series);
    mapper->setModel(model);

    chart->addSeries(series);

    int rowCount = model->rowCount();
    QStringList categories;
    qreal maxValue = 0;
    for(int i = 0; i < rowCount; ++i)
    {
        QModelIndex idx = model->index(i, BitrateTableModel::ColumnPid);
        categories << model->data(idx).toString();

        idx = model->index(i, BitrateTableModel::ColumnBitrate);
        qreal curValue = model->data(idx).toDouble();
        if (curValue > maxValue)
            maxValue = curValue;
    }

    QBarCategoryAxis *axisY = new QBarCategoryAxis();
    axisY->setGridLineVisible(false);
    axisY->setLineVisible(false);
    axisY->append(categories);
    chart->addAxis(axisY, Qt::AlignLeft);
    series->attachAxis(axisY);

    QValueAxis *axisX = new QValueAxis();
    chart->addAxis(axisX, Qt::AlignBottom);
    series->attachAxis(axisX);
    setAxisXMaxFromVal(maxValue, axisX);

    QList<QBarSet*> barsets = series->barSets();
    for(int i = 0; i < barsets.size(); ++i)
    {
        connect(barsets.at(i), &QBarSet::valueChanged,
                this, &MainWindow::processBarSetValueChange);
    }

    chartView = new QChartView(chart);
    chartView->setRenderHint(QPainter::Antialiasing);
    chartView->setMinimumSize(640, 480);

    ui->chartGroupBox->layout()->addWidget(chartView);
}

//---------------------------------------------------------------------------------------
MainWindow::~MainWindow()
{
    delete ui;
}

//---------------------------------------------------------------------------------------
void MainWindow::processBarSetValueChange(int index)
{
    QList<QAbstractAxis *> hAxises = chart->axes(Qt::Horizontal);
    if(hAxises.isEmpty())
        return;
    QValueAxis *axisX = qobject_cast<QValueAxis *>(hAxises.at(0));

    QBarSet *barset = qobject_cast<QBarSet*>(sender());
    qreal value = barset->at(index);

    if (value > axisX->max())
        setAxisXMaxFromVal(value, axisX);
}

//---------------------------------------------------------------------------------------
void MainWindow::setAxisXMaxFromVal(double val, QValueAxis *axis)
{
    int degree = (int)floor(log10(abs(val)));
    int scaled = val * pow(10, -degree);
    int step2 = ((scaled +4)/5)*5;
    int maxValue = step2 * pow(10, degree);

    axis->setMax(maxValue);
}

//---------------------------------------------------------------------------------------

 

Теги

C++ / CppGUI / Графический интерфейс пользователяMVC / Model-View-Controller / Модель-Представление-КонтроллерQBarSetQChartQHorizontalBarSeriesQtQtChartsQValueAxisQVBarModelMapperГистограммаПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.