Использование полосы прокрутки QScrollBar для оси QBarCategoryAxis на гистограммах Qt Charts
При стандартном использовании гистограмм в QtCharts
при большом количестве категорий на оси QBarCategoryAxis
и недостаточном размере виджета полосы сужаются, а названия категорий просто «сворачиваются» до трех точек. При этом увеличение масштаба и прокрутка по оси категорий нативно не поддерживаются. Рассмотрим, как же всё-таки на гистограммах QChart
можно реализовать прокрутку по оси категорий QBarCategoryAxis
с помощью полосы прокрутки QScrollBar
.
Итак, при большом количестве категорий на оси QBarCategoryAxis
и недостаточном размере виджета мы имеем следующий вид гистограммы.
А вот, что мы в итоге хотим получить.
В данном примере на гистограмме выводятся получаемые из модели CustomTableModel
данные. Чтобы было проще понять, правильно ли всё отображается на гистограмме, в каждой строке модели значение в столбце, используемом для названий категорий гистограммы, совпадает со значением в столбце, используемом для значений гистограммы. Т.е. в первой строке значения в обоих столбцах равны 1, во второй – 2, в третьей – 3, и так далее. Это наглядно продемонстрировано на рисунке 3.
Создание диаграммы
Для простоты всё создание диаграммы выполним в конструкторе объекта класса, который наследуется от 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, 20);
axisX->setTickCount(11);
Для экономии места удаляем отступы и скрываем легенду гистограммы.
chart->setContentsMargins(-10, -10, -10, -10);
chart->layout()->setContentsMargins(0, 0, 0, 0);
chart->legend()->hide();
Чтобы избежать настройки QGraphicsScene
, мы используем класс QChartView
, который делает это за нас. Указатель на объект QChart
используется как параметр конструктора QChartView
. Чтобы визуализация выглядела лучше, включаем сглаживание. Устанавливаем минимальную высоту, чтобы на гистограмме помещалось минимум 5 категорий.
chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumHeight(5*BarSeriesHeight);
Далее создаем полосу прокрутки, объект QScrollBar
, с помощью которой будем управлять гистограммой. Устанавливаем ее начальное положение в значение 0. Поскольку по умолчанию у полосы прокрутки (объекта QScrollBar
) при вертикальном расположении минимальное значение находится вверху, а на нашей гистограмме минимальное (первое) значение категории находится внизу, то для удобства использования включаем у полосы прокрутки инвертированное (перевернутое) представление. А затем связываем сигнал полосы прокрутки об изменении положения со слотом, который будет обрабатывать эти манипуляции.
categoriesScrollBar = new QScrollBar(Qt::Vertical);
categoriesScrollBar->setValue(0);
categoriesScrollBar->setInvertedAppearance(true);
connect(categoriesScrollBar, &QScrollBar::valueChanged,
this, &MainWindow::scrollESBitratesChart);
И, наконец, помещаем виджеты chartView
и categoriesScrollBar
в специально подготовленный в дизайнере виджет QGroupBox
. В этом виджете находится менеджер компоновки QHBoxLayout
, и поэтому полоса прокрутки будет находиться справа от гистограммы.
ui->chartGroupBox->layout()->addWidget(chartView);
ui->chartGroupBox->layout()->addWidget(categoriesScrollBar);
Когда нужно обновлять представление гистограммы?
Во-первых, как ни странно, гистограмму необходимо обновлять при изменении состояния полосы прокрутки (объекта QScrollBar
) для обновления выводимых на ней полос/столбцов.
Но есть и второй случай: изменение размеров виджета гистограммы и, как следствие, изменение количества категорий, которое можно вывести на гистограмму без «сворачивания» их названий до трех точек. В этом случае изменится не только представление гистограммы (количество отображаемых на ней категорий), но и возможный диапазон значений полосы прокрутки (при увеличении размеров виджета увеличивается и количество отображаемых категорий – уменьшается количество категорий, не уместившихся на гистограмму, и соответственно уменьшается диапазон положений объекта QScrollBar
).
Под первый случай подпадает обработка сигнала об изменении значения объекта QScrollBar
. А под второй случай в нашем примере подпадают сигналы об изменениях размеров окна с гистограммой (поскольку ее размеры тоже меняются), то есть это обработка событий showEvent()
и resizeEvent()
.
В первом случае мы получаем положение QScrollBar
, это положение – первая категория (ее положение), которую необходимо отобразить на гистограмме. Сохраняем это положение и вызываем метод обновления гистограммы updateChartCategories()
.
void MainWindow::scrollCategoriesChart(int val)
{
minCategoryNumber = val;
updateChartCategories();
}
Во втором случае вызываются соответствующие обработчики событий класса MainWindow
. В них мы сначала вызываем обработчики событий родительского класса, а затем метод обновления гистограммы updateChartCategories()
.
void MainWindow::showEvent(QShowEvent *ev)
{
QMainWindow::showEvent(ev);
updateChartCategories();
}
void MainWindow::resizeEvent(QResizeEvent *ev)
{
QMainWindow::resizeEvent(ev);
updateChartCategories();
}
И, наконец, сам метод обновления гистограммы
Итак, обновление представления гистограммы относительно отображаемых на ней категорий выполняется в методе void MainWindow::updateChartCategories()
.
Сначала ищем ось категорий, для чего получаем вертикальных осей на диаграмме. А так как у нас всего одна вертикальная ось, то берем указатель на нее и приводим его к типу QBarCategoryAxis
. После чего у полученного объекта получаем список категорий.
QList<QAbstractAxis *> axises = chart->axes(Qt::Vertical);
if (axises.isEmpty())
return;
QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(axises.at(0));
auto categories = axisY->categories();
if (categories.isEmpty())
return;
Далее определяем количество категорий (значение catRange
), которое мы можем одновременно отобразить на гистограмме. Это значение определяется соотношением высоты гистограммы (объекта chartView
) и минимальной высоты отображаемой категории (значением константы BarSeriesHeight
). Если это вычисленное значение больше всего имеющегося количества категорий, то будем выводить все категории.
int catCount = categories.size();
int catRange = chartView->height() / BarSeriesHeight;
if (catCount < catRange)
catRange = catCount;
Определяем номер последней (максимальной) отображаемой категории.
int maxCategoryNumber = minCategoryNumber + catRange - 1;
if(maxCategoryNumber >= catCount)
maxCategoryNumber = catCount - 1;
Минимальная и максимальная категории в отображаемом диапазоне задаются с помощью строк (названий категорий), поэтому определяем их и устанавливаем диапазон оси категорий.
QString minCategoryName = categories.at(minCategoryNumber);
QString maxCategoryName = categories.at(maxCategoryNumber);
axisY->setMin(minCategoryName);
axisY->setMax(maxCategoryName);
И в конце обновляем диапазон для полосы прокрутки (объекта класса QScrollBar
). Это необходимо выполнять, если изменились размеры гистограммы.
categoriesScrollBar->setRange(0, catCount - catRange);
В результате получаем следующее поведение гистограммы (показано на видео).
Исходный код
Полный исходный код проекта доступен на 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:
const qreal BarSeriesHeight = 36;
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
void scrollCategoriesChart(int val);
protected:
void showEvent(QShowEvent *ev);
void resizeEvent(QResizeEvent *ev);
private:
void updateChartCategories();
Ui::MainWindow *ui;
CustomTableModel *model;
QVBarModelMapper *mapper;
QChart *chart;
QChartView *chartView;
QScrollBar *categoriesScrollBar;
int minCategoryNumber;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
//---------------------------------------------------------------------------------------
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, minCategoryNumber(0)
{
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, 20);
axisX->setTickCount(11);
chart->setContentsMargins(-10, -10, -10, -10);
chart->layout()->setContentsMargins(0, 0, 0, 0);
chart->legend()->hide();
chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumHeight(5*BarSeriesHeight);
categoriesScrollBar = new QScrollBar(Qt::Vertical);
categoriesScrollBar->setValue(0);
categoriesScrollBar->setInvertedAppearance(true);
connect(categoriesScrollBar, &QScrollBar::valueChanged,
this, &MainWindow::scrollCategoriesChart);
ui->chartGroupBox->layout()->addWidget(chartView);
ui->chartGroupBox->layout()->addWidget(categoriesScrollBar);
}
//---------------------------------------------------------------------------------------
MainWindow::~MainWindow()
{
delete ui;
}
//---------------------------------------------------------------------------------------
void MainWindow::scrollCategoriesChart(int val)
{
minCategoryNumber = val;
updateChartCategories();
}
//---------------------------------------------------------------------------------------
void MainWindow::showEvent(QShowEvent *ev)
{
QMainWindow::showEvent(ev);
updateChartCategories();
}
//---------------------------------------------------------------------------------------
void MainWindow::resizeEvent(QResizeEvent *ev)
{
QMainWindow::resizeEvent(ev);
updateChartCategories();
}
//---------------------------------------------------------------------------------------
void MainWindow::updateChartCategories()
{
QList<QAbstractAxis *> axises = chart->axes(Qt::Vertical);
if (axises.isEmpty())
return;
QBarCategoryAxis *axisY = qobject_cast<QBarCategoryAxis *>(axises.at(0));
auto categories = axisY->categories();
if (categories.isEmpty())
return;
int catCount = categories.size();
int catRange = chartView->height() / BarSeriesHeight;
if (catCount < catRange)
catRange = catCount;
int maxCategoryNumber = minCategoryNumber + catRange - 1;
if(maxCategoryNumber >= catCount)
maxCategoryNumber = catCount - 1;
QString minCategoryName = categories.at(minCategoryNumber);
QString maxCategoryName = categories.at(maxCategoryNumber);
axisY->setMin(minCategoryName);
axisY->setMax(maxCategoryName);
categoriesScrollBar->setRange(0, catCount - catRange);
}
//---------------------------------------------------------------------------------------