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

Темная и светлая тема в Qt-приложении: QPalette и динамическое переключение

Введение

Поддержка тёмной и светлой темы стала ожидаемым стандартом для десктопных приложений — пользователи рассчитывают, что приложение либо следует системной теме ОС, либо позволяет переключаться вручную. В 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-приложения не используют QPalette Qt Widgets напрямую — для тематизации Qt Quick интерфейсов нужен отдельный механизм (собственная система токенов цвета в QML, Material/Universal style с поддержкой Material.theme, или ручная привязка к общему объекту настроек), и простое применение паттерна из этой статьи не сработает для чисто QML-приложений без архитектурной адаптации.