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

Брендирование приложения: единая дизайн-система через QSS

Введение

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

Концепция

Дизайн-система на уровне QSS обычно строится вокруг небольшого набора базовых «токенов» — переменных, определяющих фирменные цвета, типографику, отступы и радиусы скругления — и набора переиспользуемых стилевых «компонентов» (стиль первичной кнопки, стиль вторичной кнопки, стиль карточки), определённых через комбинацию селекторов по динамическим свойствам, аналогично подходу из статьи 260. Централизация этих значений в одном месте (как в ThemeManager из статьи 265, но расширенная сверх просто цветов тёмной/светлой темы до полноценной системы токенов) гарантирует визуальную согласованность и облегчает последующие изменения дизайна.

Пример кода: Брендирование приложения

// designtokens.h — централизованные "токены" дизайн-системы бренда
#pragma once
#include <QString>

namespace DesignTokens
{
    // Фирменные цвета — единственное место в коде, где они определены
    inline const QString BrandPrimary = "#7c3aed";
    inline const QString BrandPrimaryHover = "#6d28d9";
    inline const QString BrandSecondary = "#f3f4f6";
    inline const QString TextPrimary = "#111827";
    inline const QString TextSecondary = "#6b7280";
    inline const QString BorderColor = "#e5e7eb";
    inline const QString DangerColor = "#dc2626";

    // Типографика
    inline const QString FontFamily = "\"Inter\", \"Segoe UI\", sans-serif";
    inline const int FontSizeBody = 14;
    inline const int FontSizeHeading = 20;

    // Отступы и формы — последовательная шкала вместо произвольных значений
    inline const int SpacingSmall = 8;
    inline const int SpacingMedium = 16;
    inline const int SpacingLarge = 24;
    inline const int RadiusDefault = 6;
}
// designsystem.h — генерация единого QSS на основе токенов, с компонентами через свойства
#pragma once
#include "designtokens.h"
#include <QString>

inline QString buildApplicationStyleSheet()
{
    using namespace DesignTokens;

    return QString(R"(
        QWidget {
            font-family: %1;
            font-size: %2px;
            color: %3;
        }

        /* Компонент "первичная кнопка" — главное действие на экране, используется через свойство */
        QPushButton[variant="primary"] {
            background-color: %4;
            color: white;
            border: none;
            border-radius: %5px;
            padding: %6px %7px;
            font-weight: 600;
        }

        QPushButton[variant="primary"]:hover {
            background-color: %8;
        }

        /* Компонент "вторичная кнопка" — менее значимое действие */
        QPushButton[variant="secondary"] {
            background-color: %9;
            color: %3;
            border: 1px solid %10;
            border-radius: %5px;
            padding: %6px %7px;
        }

        /* Компонент "опасное действие" — для удаления и аналогичных деструктивных операций */
        QPushButton[variant="danger"] {
            background-color: transparent;
            color: %11;
            border: 1px solid %11;
            border-radius: %5px;
            padding: %6px %7px;
        }

        QLabel[variant="heading"] {
            font-size: %12px;
            font-weight: 700;
            color: %3;
        }

        QLabel[variant="caption"] {
            color: %13;
            font-size: 12px;
        }
    )").arg(FontFamily).arg(FontSizeBody).arg(TextPrimary)
       .arg(BrandPrimary).arg(RadiusDefault).arg(SpacingSmall).arg(SpacingMedium * 1.25)
       .arg(BrandPrimaryHover).arg(BrandSecondary).arg(BorderColor)
       .arg(DangerColor).arg(FontSizeHeading).arg(TextSecondary);
}
// Использование компонентов дизайн-системы через свойства — единообразно по всему приложению
auto *saveButton = new QPushButton("Сохранить");
saveButton->setProperty("variant", "primary");

auto *cancelButton = new QPushButton("Отмена");
cancelButton->setProperty("variant", "secondary");

auto *deleteButton = new QPushButton("Удалить аккаунт");
deleteButton->setProperty("variant", "danger");

auto *titleLabel = new QLabel("Настройки профиля");
titleLabel->setProperty("variant", "heading");

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

DesignTokens собирает все фирменные значения (цвета, шрифты, отступы) в одном файле — любое изменение бренда (например, смена основного фирменного цвета) требует правки ровно в одном месте, а не поиска и замены повторяющихся значений по всему кодовой базе разрозненных QSS-строк. buildApplicationStyleSheet() генерирует итоговый QSS, подставляя токены через QString::arg(), и определяет набор переиспользуемых «компонентов» дизайн-системы (variant="primary", variant="secondary", variant="danger") как стандартизированные визуальные роли, используемые единообразно на всех экранах приложения.

Использование setProperty("variant", "primary") в коде, создающем конкретные кнопки, показывает декларативный, основанный на роли подход — разработчик, создающий новый экран, просто выбирает подходящую семантическую роль («это главное действие», «это опасное действие») для каждого элемента, не задумываясь каждый раз заново о конкретных пиксельных значениях отступов или точном оттенке цвета — вся эта детализация уже зафиксирована один раз в дизайн-системе.

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

  • QString::arg() с большим количеством позиционных аргументов (%1, %2, … %13 в примере) подвержен ошибкам при добавлении или удалении токенов — несовпадение порядка вызовов .arg() с порядком плейсхолдеров в строке шаблона приводит к тихой, не вызывающей ошибку компиляции подстановке неправильных значений на неправильные места; для дизайн-систем с большим количеством токенов стоит рассмотреть более явный, защищённый от такой путаницы способ подстановки (например, через QString::replace() с именованными плейсхолдерами, как в статье 265).
  • Использование variant-свойств без единого, документированного списка допустимых значений может привести к постепенному, неконтролируемому разрастанию числа вариантов, когда разные разработчики команды независимо придумывают новые, слегка отличающиеся друг от друга роли вместо переиспользования уже существующих — для эффективной дизайн-системы важна не только техническая реализация через QSS, но и согласованная договорённость команды об ограниченном, осознанно поддерживаемом наборе компонентов.
  • Отсутствие единого визуального источника истины (например, актуального демонстрационного экрана со всеми вариантами компонентов дизайн-системы) для проверки, что изменение токенов не сломало визуальный вид где-то неожиданно — для дизайн-систем, используемых в большом приложении, полезно держать отдельный, тестовый экран, демонстрирующий все варианты всех компонентов одновременно, что облегчает визуальную проверку после любых изменений токенов или шаблона стилей.
  • Дизайн-система, спроектированная и захардкоженная только для одной конкретной темы (светлой), без учёта последующей необходимости поддержки тёмной темы — токены и генерация стилей из этой статьи и подход к темам из статьи 265 логически тесно связаны, и при проектировании дизайн-системы с нуля стоит сразу продумывать структуру, совместимую с несколькими наборами цветовых значений, а не привязывать токены жёстко к одной единственной цветовой палитре, которую сложно будет параметризовать впоследствии.