Введение
Поддержка тёмной и светлой темы стала ожидаемым стандартом для десктопных приложений — пользователи рассчитывают, что приложение либо следует системной теме ОС, либо позволяет переключаться вручную. В Qt Widgets это реализуется через систему QPalette, описывающую набор цветовых ролей (фон, текст, выделение и так далее) для разных состояний виджетов. В статье разберём, как реализовать переключение темы во время работы приложения без перезапуска.
Концепция
QPalette определяет цвета для именованных ролей (QPalette::Window, QPalette::WindowText, QPalette::Button, QPalette::Highlight и другие), а также для разных «групп» состояния (Active, Disabled, Inactive) — виджеты используют эти роли вместо хардкода конкретных цветов, что позволяет менять палитру для всего приложения через QApplication::setPalette(), и все виджеты, которые не переопределяют стиль явно через CSS-подобные stylesheet, автоматически перекрасятся согласно новой палитре.
С Qt 6.5+ появилась более простая встроенная поддержка системной тёмной темы через QStyleHints::colorScheme(), но для полного контроля (включая принудительное переключение независимо от настроек ОС) собственная реализация на основе QPalette остаётся гибким и универсальным решением.
Пример кода: Темная и светлая тема в Qt-приложении
// thememanager.h
#pragma once
#include <QObject>
#include <QApplication>
#include <QPalette>
class ThemeManager : public QObject
{
Q_OBJECT
public:
enum class Theme { Light, Dark };
static ThemeManager &instance()
{
static ThemeManager instance;
return instance;
}
void applyTheme(Theme theme)
{
if (m_currentTheme == theme) return;
m_currentTheme = theme;
QPalette palette = (theme == Theme::Dark) ? darkPalette() : lightPalette();
qApp->setPalette(palette);
emit themeChanged(theme);
}
Theme currentTheme() const { return m_currentTheme; }
signals:
void themeChanged(Theme theme);
private:
ThemeManager() = default;
QPalette lightPalette() const
{
return QApplication::style()->standardPalette(); // палитра по умолчанию
}
QPalette darkPalette() const
{
QPalette palette;
palette.setColor(QPalette::Window, QColor(53, 53, 53));
palette.setColor(QPalette::WindowText, Qt::white);
palette.setColor(QPalette::Base, QColor(35, 35, 35));
palette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
palette.setColor(QPalette::ToolTipBase, Qt::white);
palette.setColor(QPalette::ToolTipText, Qt::white);
palette.setColor(QPalette::Text, Qt::white);
palette.setColor(QPalette::Button, QColor(53, 53, 53));
palette.setColor(QPalette::ButtonText, Qt::white);
palette.setColor(QPalette::Highlight, QColor(42, 130, 218));
palette.setColor(QPalette::HighlightedText, Qt::black);
palette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127));
palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127));
return palette;
}
Theme m_currentTheme = Theme::Light;
};
// Использование в главном окне
#include <QMainWindow>
#include <QPushButton>
#include "thememanager.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow()
{
auto *toggleButton = new QPushButton("Переключить тему", this);
connect(toggleButton, &QPushButton::clicked, []() {
auto &manager = ThemeManager::instance();
auto newTheme = (manager.currentTheme() == ThemeManager::Theme::Light)
? ThemeManager::Theme::Dark
: ThemeManager::Theme::Light;
manager.applyTheme(newTheme);
});
setCentralWidget(toggleButton);
}
};
Пояснения к коду
darkPalette() явно задаёт цвета для основных ролей QPalette, подобранные для тёмной темы — это полностью самодостаточное решение, не зависящее от системных настроек, что важно, если приложение должно поддерживать переключение независимо от ОС. applyTheme() вызывает qApp->setPalette(palette), что мгновенно применяет новую палитру ко всем существующим и будущим виджетам приложения без необходимости их пересоздания.
Использование QApplication::style()->standardPalette() для светлой темы вместо ручного определения каждого цвета — практичный приём: он возвращает палитру, соответствующую текущему стилю виджетов платформы, что для «светлого» варианта обычно и есть нужный результат без лишнего кода.
Подводные камни
- Виджеты с явно заданными через stylesheet (CSS-подобный синтаксис Qt) цветами не реагируют на смену
QPalette. Если в приложении использовалсяsetStyleSheet()с хардкодом конкретных цветов (background-color: white;), переключение палитры не повлияет на такие виджеты — для полной поддержки темизации стоит либо не использовать stylesheet с абсолютными цветами, либо параллельно обновлять stylesheet при смене темы. - Иконки и изображения, не предназначенные для тёмного фона. Чёрные иконки на тёмном фоне становятся практически невидимыми — полноценная поддержка тёмной темы требует либо отдельного набора иконок для каждой темы, либо использования SVG-иконок с цветом, управляемым программно через
QIconс разными режимами (QIcon::Normal,QIcon::Disabled). - Несинхронизация с системной темой ОС, если пользователь ожидает автоматического переключения при смене темы в настройках Windows/macOS/Linux, а приложение использует только собственный ручной переключатель — для лучшего пользовательского опыта стоит как минимум по умолчанию подхватывать системную тему при старте через
QStyleHints::colorScheme()(Qt 6.5+) или платформенные API. - QML-приложения не используют
QPaletteQt Widgets напрямую — для тематизации Qt Quick интерфейсов нужен отдельный механизм (собственная система токенов цвета в QML,Material/Universalstyle с поддержкойMaterial.theme, или ручная привязка к общему объекту настроек), и простое применение паттерна из этой статьи не сработает для чисто QML-приложений без архитектурной адаптации.