Использование гистограмм Qt Charts совместно с моделями данных
В данной статье представлен простой пример использования диаграмм Qt Charts, в частности гистограмм, для отображения данных из модели данных, производной от класса QAbstractTableModel
.
MVC (модель-представление-контроллер)
MVC (модель-представление-контроллер) - это паттерн проектирования, созданный на основе Smalltalk, который часто используется при создании пользовательских интерфейсов. В «Паттернах проектирования» Э. Гамма и др. написано:
MVC состоит из трех типов объектов. Модель – это объект приложения, Представление – это его экранное представление, а Контроллер определяет способ реакции пользовательского интерфейса на ввод пользователя. До MVC проекты пользовательского интерфейса имели тенденцию объединять эти объекты вместе. MVC разделяет их для повышения гибкости и повторного использования.
Qt так же содержит набор классов, которые для управления связями между данными и способами их представления пользователю используют архитектуру модель/представление. Разделение функций, введенное этой архитектурой, дает разработчикам большую гибкость для настройки представления элементов и предоставляет стандартный интерфейс модели, позволяющий использовать широкий диапазон источников данных с существующими представлениями элементов.
В данном примере мы будем использовать одну модель (источник данных) для одновременного преставления данных из нее двумя способами: в таблице и на диаграмме.
Использование моделей данных с гистограммами
Для простоты всё создание диаграммы выполним в конструкторе объекта класса, который наследуется от QMainWindow
.
Начнем с создания экземпляра класса CustomTableModel
. Класс CustomTableModel
является производным от QAbstractTableModel
и был создан только для этого примера. Конструктор этого класса заполняет внутреннее хранилище данных модели данными, необходимыми для нашего примера работы с диаграммами.
m_model = new CustomTableModel;
Теперь у нас есть модель с данными, которые мы хотели бы отображать как на гистограмме, так и в QTableView
. Объект QTableView
уже создан в дизайнере, и теперь говорим ему использовать только что созданную модель в качестве источника данных. Для хорошего представления данных режим изменения размера заголовков tableView
изменен на растягивающийся. В дизайнере также задана минимальная ширина виджета.
ui->tableView->setModel(m_model);
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_model->setParent(ui->tableView);
Теперь нам нужен экземпляр QChart
для отображения на гистограмме тех же данных. Также включаем у него анимацию. Так легче увидеть, как изменение данных модели влияет на диаграмму.
QChart *chart = new QChart;
chart->setAnimationOptions(QChart::AllAnimations);
Первая строка приведенного ниже кода создает новый объект последовательности полос. Переменные firstRow
и rowCount
используются для определения настраиваемого сопоставления модели. Настраиваемое сопоставление позволяет взять из модели только часть данных. В этом случае данные из пяти строк, начиная со строки с индексом 3. Следующие три строки создают преобразователь данных, экземпляр класса QVBarModelMapper
, и указывают, что данные для наборов полос должны быть взяты из столбцов модели с индексами от 1 до 4 (включительно). Чтобы создать связь между последовательностью полос и моделью, мы передаем оба этих объекта преобразователю данных, т.е. объекту mapper
класса QVBarModelMapper
.
Наконец, добавляем к диаграмме последовательность полос series
.
QBarSeries *series = new QBarSeries;
int firstRow = 3;
int rowCount = 5;
QVBarModelMapper *mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(1);
mapper->setLastBarSetColumn(4);
mapper->setFirstRow(firstRow);
mapper->setRowCount(rowCount);
mapper->setSeries(series);
mapper->setModel(m_model);
chart->addSeries(series);
Чтобы показать в QTableView
, какие данные соответствуют какому набору полос, в этом примере используется раскраска таблицы. Когда последовательность полос добавляется к диаграмме, ей назначается цвет в зависимости от выбранной в данный момент темы. Код ниже извлекает этот цвет из последовательности полос и использует его для создания фона ячеек QTableView
. Раскрашивание другого представления не является частью функционала QChart
.
// для хранения hex-кода цвета из последовательности полос
QString seriesColorHex = "#000000";
// получаем цвет последовательности и используем его индикации области таблицы, отображаемой на диаграмме
QList<QBarSet *> barsets = series->barSets();
for (int i = 0; i < barsets.count(); i++)
{
seriesColorHex = "#" + QString::number(barsets.at(i)->brush().color().rgb(), 16).right(6).toUpper();
m_model->addMapping(seriesColorHex, QRect(1 + i, firstRow, 1, barsets.at(i)->count()));
}
Также неплохо было бы, чтобы на оси диаграммы были подписаны категории, которые описывали бы суть данных. В следующем фрагменте показано, как это сделать. Здесь для примера всё сильно упрощено, в реальном проекте эти категории так же брались бы из модели.
QStringList categories;
categories << "April" << "May" << "June" << "July" << "August";
QBarCategoryAxis *axisX = new QBarCategoryAxis();
axisX->append(categories);
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
QValueAxis *axisY = new QValueAxis();
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
Чтобы избежать настройки QGraphicsScene
, мы используем класс QChartView
, который делает это за нас. Указатель на объект QChart
используется как параметр конструктора QChartView
. Чтобы визуализация выглядела лучше, включаем антиалиасинг и устанавливаем минимальный размер виджета chartView
.
QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumSize(640, 480);
И, наконец, помещаем виджет chartView
в специально подготовленный в дизайнере виджет QGroupBox
.
ui->chartGroupBox->layout()->addWidget(chartView);
В результате получаем приложение следующего вида. Изменение данных в таблице влияет на их представление на диаграмме.
Исходный код
Полный исходный код проекта доступен на GitHub.
Также ниже приведен код исходного файла mainwindow.cpp и, в частности, конструктора, котором создается гистограмма.
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QtCharts>
using namespace QtCharts;
//---------------------------------------------------------------------------------------
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_model = new CustomTableModel;
ui->tableView->setModel(m_model);
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_model->setParent(ui->tableView);
QChart *chart = new QChart;
chart->setAnimationOptions(QChart::AllAnimations);
QBarSeries *series = new QBarSeries;
int firstRow = 3;
int rowCount = 5;
QVBarModelMapper *mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(1);
mapper->setLastBarSetColumn(4);
mapper->setFirstRow(firstRow);
mapper->setRowCount(rowCount);
mapper->setSeries(series);
mapper->setModel(m_model);
chart->addSeries(series);
// for storing color hex from the series
QString seriesColorHex = "#000000";
// get the color of the series and use it for showing the mapped area
QList<QBarSet *> barsets = series->barSets();
for (int i = 0; i < barsets.count(); i++)
{
seriesColorHex = "#" + QString::number(barsets.at(i)->brush().color().rgb(), 16).right(6).toUpper();
m_model->addMapping(seriesColorHex, QRect(1 + i, firstRow, 1, barsets.at(i)->count()));
}
QStringList categories;
categories << "April" << "May" << "June" << "July" << "August";
QBarCategoryAxis *axisX = new QBarCategoryAxis();
axisX->append(categories);
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
QValueAxis *axisY = new QValueAxis();
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumSize(640, 480);
ui->chartGroupBox->layout()->addWidget(chartView);
}
//---------------------------------------------------------------------------------------
MainWindow::~MainWindow()
{
delete ui;
}
//---------------------------------------------------------------------------------------