Пользовательский интерфейс в Qt

Кастомная отрисовка таблиц: QStyledItemDelegate на практике

Введение

Стандартное отображение ячеек в QTableView/QListView (через модель и стандартный делегат) подходит для простого текста, но когда нужна нестандартная визуализация — цветные индикаторы статуса, рейтинг звёздами, прогресс-бары прямо в ячейке таблицы — требуется собственный делегат. QStyledItemDelegate — базовый класс для этого: переопределяя paint() и опционально sizeHint(), можно полностью контролировать визуальное представление ячейки, не трогая саму модель данных. В статье построим делегат, отображающий рейтинг в виде звёзд.

Концепция

Делегат получает данные ячейки через QStyleOptionViewItem (содержит, в частности, область отрисовки rect и состояние — выделена ли ячейка, под курсором ли) и QModelIndex (для прямого обращения к данным модели через index.data(role)). Переопределение paint() рисует содержимое ячейки вручную через QPainter, полностью заменяя стандартную отрисовку текста. Для интерактивного редактирования (не только отображения) дополнительно переопределяются createEditor(), setEditorData(), setModelData().

Пример кода: Кастомная отрисовка таблиц

// starratingdelegate.h
#pragma once
#include <QStyledItemDelegate>
#include <QPainter>
#include <QApplication>

class StarRatingDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    explicit StarRatingDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}

    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override
    {
        const int rating = index.data(Qt::UserRole).toInt(); // от 0 до 5

        painter->save();

        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        }

        const int starSize = 16;
        const int spacing = 2;
        int x = option.rect.left() + 4;
        const int y = option.rect.top() + (option.rect.height() - starSize) / 2;

        painter->setRenderHint(QPainter::Antialiasing);

        for (int i = 0; i < 5; ++i) {
            const QColor color = (i < rating) ? QColor(255, 193, 7) : QColor(220, 220, 220);
            drawStar(painter, QRect(x, y, starSize, starSize), color);
            x += starSize + spacing;
        }

        painter->restore();
    }

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        Q_UNUSED(option);
        Q_UNUSED(index);
        return QSize(5 * 18 + 8, 24); // 5 звёзд + отступы
    }

private:
    void drawStar(QPainter *painter, const QRect &rect, const QColor &color) const
    {
        QPolygonF star;
        const QPointF center = rect.center();
        const double outerRadius = rect.width() / 2.0;
        const double innerRadius = outerRadius * 0.4;

        for (int i = 0; i < 10; ++i) {
            const double angle = -M_PI / 2 + i * M_PI / 5;
            const double radius = (i % 2 == 0) ? outerRadius : innerRadius;
            star << QPointF(center.x() + radius * cos(angle), center.y() + radius * sin(angle));
        }

        painter->setBrush(color);
        painter->setPen(Qt::NoPen);
        painter->drawPolygon(star);
    }
};
// Использование с QTableView
#include <QTableView>
#include <QStandardItemModel>
#include "starratingdelegate.h"

void setupRatingTable(QTableView *tableView)
{
    auto *model = new QStandardItemModel(3, 1, tableView);

    auto setRating = [model](int row, int rating) {
        model->setData(model->index(row, 0), rating, Qt::UserRole);
    };
    setRating(0, 5);
    setRating(1, 3);
    setRating(2, 1);

    tableView->setModel(model);
    tableView->setItemDelegateForColumn(0, new StarRatingDelegate(tableView));
}

Пояснения к коду

paint() полностью игнорирует стандартный механизм отрисовки текста и вместо этого читает рейтинг из роли Qt::UserRole через index.data(), после чего рисует пять звёзд, закрашенных или серых в зависимости от того, входит ли индекс звезды в значение рейтинга. Метод drawStar() строит геометрию пятиконечной звезды через тригонометрические вычисления координат вершин и закрашивает получившийся QPolygonF.

Обработка состояния выделения (option.state & QStyle::State_Selected) — важная деталь: без неё выделенная ячейка таблицы выглядела бы так же, как невыделенная, что нарушает стандартные пользовательские ожидания от поведения таблицы. setItemDelegateForColumn() устанавливает кастомный делегат только для конкретного столбца, оставляя остальные столбцы с поведением по умолчанию.

Подводные камни

  • Отсутствие painter->save()/painter->restore() вокруг кастомной отрисовки может «протечь» изменённые настройки QPainter (кисть, перо, шрифт) в отрисовку следующих ячеек или элементов — особенно важно при наличии множества разных делегатов или смешанного использования стандартной и кастомной отрисовки в одной таблице.
  • Неверный sizeHint() или его полное отсутствие приводит к тому, что таблица выделяет под ячейку размер, рассчитанный по стандартной логике (исходя из текста), который может не соответствовать реальному визуальному содержимому делегата — звёзды могут обрезаться или, наоборот, оставлять много пустого пространства.
  • Тяжёлые вычисления (как генерация полигона звезды) внутри paint() при каждой перерисовке, без кэширования результата — для таблиц с большим числом строк и частыми перерисовками (например, при скролле) стоит кэшировать заранее вычисленную геометрию звезды как статическую константу, а не пересчитывать её координаты на каждый вызов paint().
  • Игнорирование доступности (accessibility) при полностью кастомной визуализации. Стандартный текстовый делегат автоматически интегрируется с screen reader’ами через текстовое представление данных — полностью кастомная графическая отрисовка (звёзды без текстовой альтернативы) может сделать таблицу непригодной для пользователей с нарушениями зрения без дополнительной работы по обеспечению доступности.