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

Создание собственного QStyle с нуля вместо QSS

Введение

QSS отлично подходит для большинства задач кастомизации внешнего вида, но имеет границы — для полностью нестандартной отрисовки виджетов, недостижимой через комбинацию доступных QSS-свойств, или для случаев, когда стиль должен динамически адаптироваться к сложной логике (а не просто к статичным CSS-подобным правилам), нужен полноценный собственный класс QStyle. В статье разберём базовую структуру создания и подключения кастомного стиля.

Концепция

QStyle — абстрактный базовый класс, определяющий, как именно отрисовывается каждый визуальный элемент каждого виджета — вместо наследования напрямую от QStyle (что требует реализации огромного количества методов) на практике почти всегда наследуются от QProxyStyle, который делегирует отрисовку базовому стилю (обычно QStyleFactory::create("Fusion")) по умолчанию, и переопределяют только конкретные методы для тех элементов, которые нужно изменить — это позволяет точечно настроить только нужные аспекты отрисовки, не переписывая всю логику стиля с нуля.

Пример кода: Создание собственного QStyle

// customstyle.h — кастомный стиль на основе QProxyStyle
#pragma once
#include <QProxyStyle>
#include <QPainter>
#include <QStyleOption>

class CustomStyle : public QProxyStyle
{
public:
    explicit CustomStyle(QStyle *baseStyle = nullptr) : QProxyStyle(baseStyle) {}

    // Переопределение отрисовки конкретного элемента управления —
    // в данном случае кастомизируем индикатор кнопки (фокусную рамку)
    void drawPrimitive(PrimitiveElement element, const QStyleOption *option,
                       QPainter *painter, const QWidget *widget) const override
    {
        if (element == PE_FrameFocusRect) {
            // Полностью отключаем стандартную пунктирную рамку фокуса —
            // вместо неё фокус будет показан другим способом (например, через QSS :focus)
            return;
        }

        // Для всех остальных элементов — делегируем отрисовку базовому стилю без изменений
        QProxyStyle::drawPrimitive(element, option, painter, widget);
    }

    // Переопределение размеров элемента — увеличиваем стандартный отступ кнопок
    int pixelMetric(PixelMetric metric, const QStyleOption *option,
                    const QWidget *widget) const override
    {
        if (metric == PM_ButtonMargin) {
            return 12; // увеличенный отступ внутри кнопок по сравнению со стандартным стилем
        }
        return QProxyStyle::pixelMetric(metric, option, widget);
    }

    // Переопределение отрисовки сложного контрола целиком —
    // пример полностью кастомной отрисовки индикатора прогресс-бара
    void drawControl(ControlElement element, const QStyleOption *option,
                     QPainter *painter, const QWidget *widget) const override
    {
        if (element == CE_ProgressBarContents) {
            if (const auto *progressBarOption = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) {
                painter->save();
                painter->setRenderHint(QPainter::Antialiasing);

                QRectF rect = option->rect;
                const double progress = static_cast<double>(progressBarOption->progress - progressBarOption->minimum)
                                       / (progressBarOption->maximum - progressBarOption->minimum);

                // Кастомная отрисовка прогресс-бара с закруглёнными краями и градиентом
                QLinearGradient gradient(rect.topLeft(), rect.topRight());
                gradient.setColorAt(0, QColor("#3b82f6"));
                gradient.setColorAt(1, QColor("#2563eb"));

                QRectF filledRect = rect;
                filledRect.setWidth(rect.width() * progress);

                painter->setPen(Qt::NoPen);
                painter->setBrush(gradient);
                painter->drawRoundedRect(filledRect, 4, 4);

                painter->restore();
                return;
            }
        }

        QProxyStyle::drawControl(element, option, painter, widget);
    }
};
// main.cpp — подключение кастомного стиля ко всему приложению
#include <QApplication>
#include <QStyleFactory>
#include "customstyle.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // CustomStyle оборачивает базовый Fusion-стиль, делегируя ему всё,
    // что явно не переопределено в самом CustomStyle
    app.setStyle(new CustomStyle(QStyleFactory::create("Fusion")));

    // остальной код приложения не нуждается в изменениях —
    // все стандартные виджеты автоматически используют новый стиль
    return app.exec();
}

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

drawPrimitive() переопределяет отрисовку отдельных «примитивных» визуальных элементов — в примере полностью отключается стандартная рамка фокуса (PE_FrameFocusRect), оставляя её отрисовку пустой (просто return без вызова базовой реализации), что может использоваться, если фокус показывается каким-то другим, более кастомным способом. pixelMetric() позволяет переопределить числовые метрики (размеры отступов, толщину линий) — в примере увеличивается стандартный внутренний отступ кнопок без необходимости переопределять всю их отрисовку целиком.

drawControl() показывает более сложный случай — полную кастомную отрисовку прогресс-бара через прямую работу с QPainter, с градиентной заливкой и закруглёнными краями, что было бы невозможно достичь только через QSS-свойства без поддержки градиентов с произвольным контролем формы. Критичный паттерн во всех трёх методах — проверка конкретного элемента/метрики и условный return после кастомной обработки, либо безусловный вызов QProxyStyle::method(...) для делегирования остальной работы базовому стилю — без этой делегации придётся самостоятельно реализовывать отрисовку абсолютно всех элементов всех виджетов, что нереалистичный объём работы.

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

  • Переопределение слишком многих методов QStyle без необходимости значительно увеличивает объём кода и риск визуальных несоответствий между кастомизированными и не кастомизированными частями интерфейса — стоит переопределять только то, что действительно требует нестандартной отрисовки, оставляя всё остальное на делегирование базовому стилю, а не пытаться переписать весь визуальный язык приложения через QStyle целиком.
  • Создание QStyle через прямое наследование (не через QProxyStyle) требует реализации огромного количества чисто виртуальных методов, многие из которых для большинства практических задач кастомизации не нужны — начинающие разработчики иногда по ошибке наследуются прямо от QStyle, что создаёт неоправданно большой объём необходимого кода по сравнению с QProxyStyle, который покрывает эту потребность через делегирование.
  • QStyleOptionProgressBar и аналогичные классы опций стиля содержат много полей, специфичных для конкретного состояния и конфигурации виджета, и забытая обработка краевых случаев (например, minimum == maximum, что даёт деление на ноль при вычислении progress в примере) может привести к крашу или визуальным артефактам — код кастомной отрисовки должен явно защищаться от таких краевых случаев, в отличие от уже отработанной, проверенной годами использования логики базовых стилей Qt.
  • Производительность кастомной отрисовки через QPainter с антиалиасингом и градиентами ниже, чем у оптимизированных, давно существующих базовых стилей — для виджетов, перерисовываемых очень часто (например, прогресс-бар, обновляемый много раз в секунду во время длительной операции), стоит явно профилировать влияние кастомной отрисовки на общую производительность приложения, особенно на менее мощном оборудовании.