Введение
Поддержка светлой и тёмной темы — ожидаемая пользователями функциональность современных приложений. Основной цикл статей уже затрагивал переключение тем (статья 83) на уровне палитры — в этой статье разберём более глубокий подход через структурированную организацию QSS-файлов для нескольких тем и динамическое переключение между ними во время работы приложения.
Концепция
Практичный подход к организации многотемного QSS — выделение цветовых значений в отдельные переменные на уровне кода (а не дублирование всего QSS-текста для каждой темы), и генерация итоговой строки стилей через подстановку конкретных цветов темы в общий шаблон QSS. Это позволяет поддерживать одну структуру стилей и просто переключать набор используемых в подстановке цветов, вместо поддержания нескольких полностью независимых, потенциально расходящихся друг от друга наборов правил QSS для каждой темы отдельно.
Пример кода: Поддержка тем (light/dark)
// thememanager.h — генерация QSS из шаблона с подстановкой цветов конкретной темы
#pragma once
#include <QApplication>
#include <QMap>
#include <QString>
struct ThemeColors
{
QString background;
QString surface;
QString primaryText;
QString secondaryText;
QString accentColor;
QString borderColor;
};
class ThemeManager : public QObject
{
Q_OBJECT
public:
enum class Theme { Light, Dark };
static ThemeManager &instance()
{
static ThemeManager manager;
return manager;
}
void applyTheme(Theme theme)
{
m_currentTheme = theme;
const ThemeColors &colors = (theme == Theme::Dark) ? darkColors() : lightColors();
QString styleSheet = styleTemplate();
styleSheet.replace("@background", colors.background);
styleSheet.replace("@surface", colors.surface);
styleSheet.replace("@primaryText", colors.primaryText);
styleSheet.replace("@secondaryText", colors.secondaryText);
styleSheet.replace("@accentColor", colors.accentColor);
styleSheet.replace("@borderColor", colors.borderColor);
qApp->setStyleSheet(styleSheet);
emit themeChanged(theme);
}
Theme currentTheme() const { return m_currentTheme; }
signals:
void themeChanged(Theme theme);
private:
static ThemeColors lightColors()
{
return {"#ffffff", "#f3f4f6", "#111827", "#6b7280", "#2563eb", "#d1d5db"};
}
static ThemeColors darkColors()
{
return {"#111827", "#1f2937", "#f9fafb", "#9ca3af", "#3b82f6", "#374151"};
}
static QString styleTemplate()
{
return R"(
QWidget {
background-color: @background;
color: @primaryText;
}
QPushButton {
background-color: @surface;
border: 1px solid @borderColor;
color: @primaryText;
padding: 8px 16px;
border-radius: 4px;
}
QPushButton:hover {
border-color: @accentColor;
}
QLabel[secondary="true"] {
color: @secondaryText;
}
)";
}
Theme m_currentTheme = Theme::Light;
};
// Переключение темы из интерфейса приложения
connect(themeToggleButton, &QPushButton::clicked, []() {
const auto newTheme = ThemeManager::instance().currentTheme() == ThemeManager::Theme::Light
? ThemeManager::Theme::Dark : ThemeManager::Theme::Light;
ThemeManager::instance().applyTheme(newTheme);
});
// Сохранение выбранной темы между запусками приложения
connect(&ThemeManager::instance(), &ThemeManager::themeChanged, [](ThemeManager::Theme theme) {
QSettings().setValue("ui/theme", theme == ThemeManager::Theme::Dark ? "dark" : "light");
});
Пояснения к коду
styleTemplate() содержит единый QSS-текст с символическими плейсхолдерами (@background, @accentColor и так далее) вместо конкретных цветовых значений — этот шаблон пишется и поддерживается ровно один раз, независимо от количества доступных тем. applyTheme() берёт конкретный набор цветов (lightColors()/darkColors()) и последовательно заменяет каждый плейсхолдер на реальное значение через QString::replace(), формируя итоговую, готовую к применению строку QSS.
Такой подход гарантирует структурное единообразие между темами — поскольку обе темы генерируются из одного и того же шаблона, невозможна ситуация, когда светлая тема стилизует какой-то виджет, а тёмная версия того же правила была забыта или написана с опечаткой в отдельном, независимом QSS-файле для тёмной темы — структура правил гарантированно идентична, отличаются только подставляемые цвета.
Подводные камни
- Использование простого
QString::replace()для подстановки плейсхолдеров рискует случайным совпадением подстроки — если, например, значение цвета случайно содержит текст, совпадающий с другим плейсхолдером, или порядок замены плейсхолдеров выбран неудачно (один плейсхолдер является подстрокой другого), результат подстановки может быть некорректным; для более сложных шаблонов стоит использовать чуть более строгий формат плейсхолдеров (например, с явными разделителями) или специализированный движок шаблонов. - Полная регенерация и повторное применение всего QSS приложения при каждом переключении темы (через
qApp->setStyleSheet()) пересчитывает стили для каждого виджета приложения — для очень большого, сложного интерфейса с множеством виджетов это может вызвать заметную, хотя обычно кратковременную задержку в момент переключения темы, что стоит иметь в виду при тестировании на представительном по сложности экране приложения. - Изображения и иконки, специфичные для конкретной темы (например, чёрная иконка для светлой темы, белая — для тёмной), не меняются автоматически вместе с QSS-цветами, если ссылки на эти иконки заданы статично в коде интерфейса, а не вычисляются динамически в зависимости от текущей темы — нужна отдельная логика, реагирующая на сигнал
themeChangedи подставляющая нужный вариант иконки, аналогичная обработке смены палитры в основном цикле статей (статья 83). - Не все аспекты внешнего вида виджетов покрываются QSS-цветами — некоторые системные элементы (например, нативные системные диалоги выбора файла/цвета на некоторых платформах) могут не подчиняться кастомной теме приложения вовсе, оставаясь в системном виде, и пользователь может заметить визуальную рассогласованность между основным интерфейсом приложения (полностью тематизированным) и такими системными элементами (которые продолжают использовать собственную, системную тему ОС).