Автоматическое изменение категорий на оси QBarCategoryAxis на гистограммах Qt Charts

Добавлено 23 января 2021 в 15:36

Часто в разработке реальных приложений список категорий, которые необходимо вывести на гистограмму, бывает заранее неизвестен. Кроме того, список этих категорий может изменяться во время работы приложения, выводящего гистограмму (например, создание и завершение процессов в менеджере задач, появление новых элементарных потоков в транспортном потоке MPEG TS, и т.д.).

В данной статье рассматривается, как автоматически изменять выводимые категории на оси QBarCategoryAxis на гистограмме Qt Charts, в зависимости от изменений, происходящих в модели данных.

Пример автоматической изменения категорий на оси QBarCategoryAxis

В данном примере на гистограмме выводятся получаемые из модели CustomTableModel данные. Чтобы было проще понять, правильно ли всё отображается на гистограмме, в каждой строке модели значение в столбце, используемом для названий категорий гистограммы, совпадает со значением в столбце, используемом для значений гистограммы. Т.е. в первой строке значения в обоих столбцах равны 1, во второй – 2, в третьей – 3, и так далее. Это наглядно продемонстрировано на рисунке 1.

Рисунок 1 Соответствие между названиями категорий и значениями полос гистограммы
Рисунок 1 – Соответствие между названиями категорий и значениями полос гистограммы

При запуске приложения в модели создается четыре строки: со значениями ID 1, 2, 4 и 5. Через 3 секунды после запуска добавляется строка с ID = 3, а еще через 2 секунды – строка с ID = 5. Затем начинается удаление строк: через 7 секунд после запуска удаляется строка с ID = 2, а еще через 2 секунды – строка с ID = 5. Этот процесс показан на видео ниже.

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

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

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

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

model = new CustomTableModel(this);

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

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

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

chart = new QChart;

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

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

QHorizontalBarSeries *series = new QHorizontalBarSeries;

mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(CustomTableModel::ColumnSomeData);
mapper->setLastBarSetColumn(CustomTableModel::ColumnSomeData);

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

chart->addSeries(series);

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

int rowCount = model->rowCount();
QStringList categories;
for(int i = 0; i < rowCount; ++i)
{
    QModelIndex idx = model->index(i, CustomTableModel::ColumnId);
    categories << model->data(idx).toString();
}

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

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

Создаем ось значений (объект QValueAxis). Добавляем ее на диаграмму (она будет горизонтальной, внизу) и тоже прикрепляем к последовательности полос. После чего устанавливаем фиксированный диапазон ее значений. О том, как автоматически устанавливать и подстраивать диапазон оси значений, можно прочитать в статье «Автоматическая подстройка диапазона оси значений QValueAxis на гистограммах Qt Charts».

QValueAxis *axisX = new QValueAxis();
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
axisX->setRange(0, 10);

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

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

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

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

Автоматическое изменение категорий на оси QBarCategoryAxis

В нашем случае добавление/удаление категорий на гистограмме означает добавление/удаление строк в нашем источнике данных, т.е. модели CustomTableModel. Поэтому нам необходимо обрабатывать соответствующие сигналы, выдаваемые моделью данных:

  • rowsInserted(const QModelIndex &parent, int first, int last) – сигнал, выдаваемый после вставки строк в модель. Новые строки находятся между значениями first и last включительно;
  • rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) – сигнал, выдаваемый перед удалением строк из модели. Строки, которые будут удалены, находятся между значениями first и last включительно.

В следующем фрагменте кода мы связываем эти сигналы модели CustomTableModel со слотами объекта MainWindow, созданными специально для их обработки.

connect(model, &CustomTableModel::rowsInserted,
        this, &MainWindow::processCategoryAdding);
connect(model, &CustomTableModel::rowsAboutToBeRemoved,
        this, &MainWindow::processCategoryRemoving);

Добавление категорий на гистограмму

Добавление категорий на гистограмму выполняется в слоте processCategoryAdding.

void MainWindow::processCategoryAdding(const QModelIndex &parentIndex, int first, int last)

Здесь мы сначала ищем ось категорий, на которой необходимо выполнить изменения. То есть сначала ищем все вертикальные оси на диаграмме, а затем берем первую из них, поскольку знаем, что вертикальная ось у нас всего одна, и это ось категорий.

QList<QAbstractAxis *> vAxises = chart->axes(Qt::Vertical);
if(vAxises.isEmpty())
    return;
QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(vAxises.at(0));

Далее проходимся по всем новым строкам, извлекаем название каждой новой категории и добавляем его на ось QBarCategoryAxis в то же место, где строка этой категории находится в модели данных.

QModelIndex idx;
for (int i = first; i <= last; ++i)
{
    idx = model->index(i, CustomTableModel::ColumnId);
    QString category = model->data(idx).toString();
    axisY->insert(i, category);
}

Удаление категорий с гистограммы

Удаление категорий с гистограммы выполняется в слоте processCategoryRemoving.

void MainWindow::processCategoryRemoving(const QModelIndex &parentIndex, int first, int last)

Здесь мы сначала так же ищем ось категорий, на которой необходимо выполнить изменения.

QList<QAbstractAxis *> axises = chart->axes(Qt::Vertical);
if(axises.isEmpty())
    return;
QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(axises.at(0));

А затем проходимся по всем строкам, которые будут удалены, извлекаем название каждой новой категории и удаляем его с оси QBarCategoryAxis.

for (int i = first; i <= last; ++i)
{
    QModelIndex idx = model->index(i, CustomTableModel::ColumnId);
    QString category = model->data(idx).toString();
    axisY->remove(category);
}

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

Рисунок 2 Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (сразу после запуска)
Рисунок 2 – Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (сразу после запуска)
Рисунок 3 Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (через 5 секунд после запуска)
Рисунок 3 – Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (через 5 секунд после запуска)
Рисунок 4 Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (через 10 секунд после запуска)
Рисунок 4 – Пример приложения для демонстрации автоматического изменения категорий на гистограмме Qt Charts (через 10 секунд после запуска)

Исходный код

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

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

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include "customtablemodel.h"

#include <QtCharts>

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 processCategoryAdding(const QModelIndex &parentIndex, int first, int last);
    void processCategoryRemoving(const QModelIndex &parentIndex, int first, int last);

private:
    Ui::MainWindow *ui;

    CustomTableModel *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 CustomTableModel(this);

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

    chart = new QChart;

    QHorizontalBarSeries *series = new QHorizontalBarSeries;

    mapper = new QVBarModelMapper(this);
    mapper->setFirstBarSetColumn(CustomTableModel::ColumnSomeData);
    mapper->setLastBarSetColumn(CustomTableModel::ColumnSomeData);

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

    chart->addSeries(series);

    int rowCount = model->rowCount();
    QStringList categories;
    for(int i = 0; i < rowCount; ++i)
    {
        QModelIndex idx = model->index(i, CustomTableModel::ColumnId);
        categories << model->data(idx).toString();
    }

    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);
    axisX->setRange(0, 10);

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

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

    connect(model, &CustomTableModel::rowsInserted,
            this, &MainWindow::processCategoryAdding);
    connect(model, &CustomTableModel::rowsAboutToBeRemoved,
            this, &MainWindow::processCategoryRemoving);

}

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

//---------------------------------------------------------------------------------------
void MainWindow::processCategoryAdding(const QModelIndex &parentIndex, int first, int last)
{
    Q_UNUSED(parentIndex)

    QList<QAbstractAxis *> vAxises = chart->axes(Qt::Vertical);
    if(vAxises.isEmpty())
        return;
    QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(vAxises.at(0));

    QModelIndex idx;
    for (int i = first; i <= last; ++i)
    {
        idx = model->index(i, CustomTableModel::ColumnId);
        QString category = model->data(idx).toString();
        axisY->insert(i, category);
    }
}

//---------------------------------------------------------------------------------------
void MainWindow::processCategoryRemoving(const QModelIndex &parentIndex, int first, int last)
{
    Q_UNUSED(parentIndex)

    QList<QAbstractAxis *> axises = chart->axes(Qt::Vertical);
    if(axises.isEmpty())
        return;
    QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(axises.at(0));

    for (int i = first; i <= last; ++i)
    {
        QModelIndex idx = model->index(i, CustomTableModel::ColumnId);
        QString category = model->data(idx).toString();
        axisY->remove(category);
    }
}

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

Сортированный вывод категорий на ось гистограммы

Если же вы хотите выводить категории на ось QBarCategoryAxis в сортированном виде по возрастанию или по убыванию, но в вашей модели сортировка не реализована, то можно воспользоваться промежуточной моделью класса QSortFilterProxyModel. То есть связать промежуточную модель с исходной моделью данных и при последующей работе с гистограммой использовать уже эту прокси-модель так, как использовали бы исходную модель.

chartProxyModel = new QSortFilterProxyModel(this);
chartProxyModel->setSourceModel(model);
chartProxyModel->sort(CustomTableModel::ColumnId);

Теги

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

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

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