Введение
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с антиалиасингом и градиентами ниже, чем у оптимизированных, давно существующих базовых стилей — для виджетов, перерисовываемых очень часто (например, прогресс-бар, обновляемый много раз в секунду во время длительной операции), стоит явно профилировать влияние кастомной отрисовки на общую производительность приложения, особенно на менее мощном оборудовании.