Автоматическое изменение категорий на оси QBarCategoryAxis на гистограммах Qt Charts
Часто в разработке реальных приложений список категорий, которые необходимо вывести на гистограмму, бывает заранее неизвестен. Кроме того, список этих категорий может изменяться во время работы приложения, выводящего гистограмму (например, создание и завершение процессов в менеджере задач, появление новых элементарных потоков в транспортном потоке MPEG TS, и т.д.).
В данной статье рассматривается, как автоматически изменять выводимые категории на оси QBarCategoryAxis
на гистограмме Qt Charts, в зависимости от изменений, происходящих в модели данных.
Пример автоматической изменения категорий на оси QBarCategoryAxis
В данном примере на гистограмме выводятся получаемые из модели CustomTableModel
данные. Чтобы было проще понять, правильно ли всё отображается на гистограмме, в каждой строке модели значение в столбце, используемом для названий категорий гистограммы, совпадает со значением в столбце, используемом для значений гистограммы. Т.е. в первой строке значения в обоих столбцах равны 1, во второй – 2, в третьей – 3, и так далее. Это наглядно продемонстрировано на рисунке 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 секунд после запуска.
Исходный код
Полный исходный код проекта доступен на 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);