Использование полосы прокрутки QScrollBar для оси QBarCategoryAxis на гистограммах Qt Charts

Добавлено 30 января 2021 в 11:41

При стандартном использовании гистограмм в QtCharts при большом количестве категорий на оси QBarCategoryAxis и недостаточном размере виджета полосы сужаются, а названия категорий просто «сворачиваются» до трех точек. При этом увеличение масштаба и прокрутка по оси категорий нативно не поддерживаются. Рассмотрим, как же всё-таки на гистограммах QChart можно реализовать прокрутку по оси категорий QBarCategoryAxis с помощью полосы прокрутки QScrollBar.

Итак, при большом количестве категорий на оси QBarCategoryAxis и недостаточном размере виджета мы имеем следующий вид гистограммы.

Рисунок 1 Сворачивание названий категорий на оси QBarCategoryAxis до трех точек при недостаточном размере виджета
Рисунок 1 – «Сворачивание» названий категорий на оси QBarCategoryAxis до трех точек при недостаточном размере виджета

А вот, что мы в итоге хотим получить.

Рисунок 2 Использование полосы QScrollBar для прокрутки по оси категорий QBarCategoryAxis
Рисунок 2 – Использование полосы QScrollBar для прокрутки по оси категорий QBarCategoryAxis

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

Рисунок 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);
}

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

 

Теги

C++ / CppGUI / Графический интерфейс пользователяQBarCategoryAxisQChartQChartViewQScrollBarQtQtChartsГистограммаПрограммирование

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

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