В статье рассказывается о функционале, о котором забыли даже разработчики Qt. Дело в том, что этот функционал даёт уникальные возможности стилизации. В отличие от QML и CSS, использование QStyleFactory позволяет предоставить пользователю широкий выбор QStyle‑дизайнов виджетов без затрат времени на настройку QML и CSS. Я много раз видел, как программисты тратят много времени на настройку дизайна через QML и CSS, хотя в Qt 5 и Qt 6 есть база уже готовых дизайнов, которую никто не предоставляет пользователю в качестве выбора. Считаю необходимым рассказать об этом очень удобном и даже ленивом способе настройки дизайна.

Установка и запуск

Шаблон QWidget (QStyleSelect) для быстрого и простого изменения дизайна приложения с возможностью выбора между стилями. Скачать, собрать и запустить исходный код примера:

git clone https://github.com/PVE-Vladimir/QStyleSelect.git
mkdir -p QStyleSelect/build
cd QStyleSelect/build
cmake ..
make
./QStyleSelect

Установка стилей происходит через стандартный менеджер пакетов apt команды:

apt search styl | grep "qt5" | awk -F'/' '{print $1}' | sed 's/Development.*//g' | xargs sudo apt install -y  

apt search styl | grep "qt6" | awk -F'/' '{print $1}' | sed 's/Development.*//g' | xargs sudo apt install -y

Установка происходит в директорию /usr/lib/x86_64-linux-gnu/qt5/plugins/styles/ или /usr/lib/x86_64-linux-gnu/qt6/plugins/styles/ Сюда же можно установить стиль, собранный из исходного кода. К примеру, я собирал и использовал в локальном профиле Qt вот этот стиль: file:///usr/local/Qt-6.4.2/plugins/styles/k4oxygen.so

Рисунок 1 — Пример доступных стилей Qt

Как работает технология QStyle (QStyleFactory)

Установка стилей через QApplication не полностью поддерживается в Qt Creator. В руководстве Qt Creator сказано: «Внимание: в настоящее время таблицы стилей Qt не поддерживаются для пользовательских подклассов стилей QStyle. Мы планируем решить эту проблему в одном из будущих релизов». Однако это не значит, что этот функционал не работает. Как сказано в руководстве, вы можете вызвать QApplication::setStyle() в любое время, но, вызвав его до конструктора, вы гарантируете, что предпочтения пользователя, установленные с помощью ‑style, будут учтены. Пример использования:

QApplication::setStyle(QStyleFactory::create("Fusion"));

QStyleFactory создаёт объект QStyle с использованием функции create() и ключа, идентифицирующего стиль. Стили либо встроенные, либо динамически загружаются из плагина стилей.

В общем, как выяснилось далее, функционал CustomStyle действительно не доработан до конца. Однако мне удалось реализовать его для Qt5 полностью, а для Qt6 — частично. Всё равно считаю это хорошим результатом.

Qt5 как основной инструмент сборки располагает более развитым репозиторием стилей, чем Qt6.

Скомпилированные стили Qt 5 в репозитории Ubuntu, которые удалось найти: breeze5.so libkvantum.so libqgtk2style.so libqplastiquestyle.so libukui‑proxy‑style.so qtcurve.so breeze.so libqcleanlooksstyle.so libqmotifstyle.so libqt5-style‑ukui.so oxygen.so

Рисунок 2 - Выбор доступных стилей Qt5
Рисунок 2 — Выбор доступных стилей Qt5

Скомпилированные стили Qt6 в репозитории Ubuntu, которые удалось найти: adwaita.so libqt6ct‑style.so

Рисунок 3 — Выбор доступных стилей Qt6

Эти стили имеют в своём составе несколько вариаций цветовой палитры, чаще всего это вариации светлой и чёрной тем. В моём примере программы отсутствует удаление повторяющихся названий стилей. В случае необходимости вы можете сами изменить переменную списка стилей по своему усмотрению. Может быть, различия и есть, но я их просто не заметил. Список стилей в коде формируется через QStringList:

 QStringList styles = QStyleFactory::keys();

Далее для Qt5 я добавил в код логику обработки свойств имеющихся стилей для тёмных версий:

Добавление в список темных версий стилей:

QStringList styles = QStyleFactory::keys();
    //qDebug() << "Current Style:" << styles;

    int indexwindows = styles.indexOf("Windows");
    int indexmotif = styles.indexOf("motif");
    int indexcde = styles.indexOf("cde");
    int indexdark = styles.indexOf("kvantum-dark");
    // Insert a new style. Combination of properties of different topics Qt 5
    if (indexwindows != -1 && indexdark != -1) {
        styles.insert(indexwindows + 1, "Windows-dark");
    }
    if (indexmotif != -1 && indexdark != -1 ){
        styles.insert(indexmotif + 1, "motif-dark");
    }
    if (indexcde != -1 && indexdark != -1 ){
        styles.insert(indexcde + 1, "cde-dark");
    }
    qDebug() << "Current Style:" << styles;

Логика создания темных версий стилей:

  //Combination of properties of different topics Qt 5
  if (style == "Windows-dark") {
    QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
    QApplication::setStyle(QStyleFactory::create("Windows"));
  } else if (style == "ukui-dark") {
    QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
    QApplication::setStyle(QStyleFactory::create("ukui"));
  } else if (style == "motif-dark") {
    QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
    QApplication::setStyle(QStyleFactory::create("motif"));
  } else if (style == "cde-dark") {
    QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
    QApplication::setStyle(QStyleFactory::create("cde"));
  } else {
    QApplication::setStyle(QStyleFactory::create(style));
  }

Как видно из кода, возможно совмещать свойства нескольких тем в одну. Это противоречит тому, что указано в электронном руководстве Qt Creator, но тут жизнь расходится с теорией. Дело в том, что некоторые свойства пользовательских стилей несовместимы с обычными стилями Qt. Поэтому в коде примера QStyleSelect применяются такого рода уловки, чтобы всё работало.

Уловка QPalette

Некоторые пользовательские стили по какой‑то причине необратимо изменяют QPalette. Класс QPalette содержит цветовые группы для каждого состояния виджета. Поэтому в коде перед применением цвета сохраняются системные цветовые группы.

oldPalette = new QPalette(QApplication::palette()); 

После чего перед применением нового стиля происходит сброс цветовых групп и на всякий случай устанавливается стиль Fusion. Fusion — базовый стиль Qt.

QApplication::setPalette(*oldPalette);
QApplication::setStyle(QStyleFactory::create("Fusion"));
//Disables QPalette from the system in Qt 5
//white_QPalette();

Метод white_QPalette() работает только в Qt 5. По неизвестной мне причине в Qt 6, если цвет установить через setColor, этот параметр будет доминировать над setStyle, а в Qt 5 существует последовательность установки цвета. Метод white_QPalette() реализует другой подход к сбросу цветовой группы. Если взять системную палитру, есть вероятность, что в системе будет выбрана чёрная тема, и тогда некоторые стили применят чёрную цветовую группу. Чтобы сделать программу независимой от системы, создан метод white_QPalette(), который устанавливает светлую цветовую группу по умолчанию.

void MainWindow::white_QPalette()
{
    QPalette Theme;

    // QPalette palette = QApplication::palette();
    // for (int role = 0; role < QPalette::NColorRoles; ++role) {
    //     qDebug() << "Role" << role << ":" << palette.color((QPalette::ColorRole)role);
    // }

    Theme.setColor(QPalette::WindowText, QColor::fromRgbF(0.137255, 0.14902, 0.160784, 1));
    Theme.setColor(QPalette::Button, QColor::fromRgbF(0.988235, 0.988235, 0.988235, 1));
    Theme.setColor(QPalette::Light, QColor::fromRgbF(1, 1, 1, 1));
    Theme.setColor(QPalette::Midlight, QColor::fromRgbF(1, 1, 1, 1));
    Theme.setColor(QPalette::Dark, QColor::fromRgbF(0.494118, 0.494118, 0.494118, 1));
    Theme.setColor(QPalette::Mid, QColor::fromRgbF(0.658824, 0.658824, 0.658824, 1));
    Theme.setColor(QPalette::Text, QColor::fromRgbF(0.137255, 0.14902, 0.160784, 1));
    Theme.setColor(QPalette::BrightText, QColor::fromRgbF(1, 1, 1, 1));
    Theme.setColor(QPalette::ButtonText, QColor::fromRgbF(0.137255, 0.14902, 0.160784, 1));
    Theme.setColor(QPalette::Base, QColor::fromRgbF(1, 1, 1, 1));
    Theme.setColor(QPalette::Window, QColor::fromRgbF(0.937255, 0.941176, 0.945098, 1));
    Theme.setColor(QPalette::Shadow, QColor::fromRgbF(0.462821, 0.462821, 0.462821, 1));
    Theme.setColor(QPalette::Highlight, QColor::fromRgbF(0.239216, 0.682353, 0.913725, 1));
    Theme.setColor(QPalette::HighlightedText, QColor::fromRgbF(1, 1, 1, 1));
    Theme.setColor(QPalette::Link, QColor::fromRgbF(0.160784, 0.501961, 0.72549, 1));
    Theme.setColor(QPalette::LinkVisited, QColor::fromRgbF(0.607843, 0.34902, 0.713726, 1));
    Theme.setColor(QPalette::AlternateBase, QColor::fromRgbF(0.968627, 0.968627, 0.968627, 1));
    Theme.setColor(QPalette::NoRole, QColor::fromRgbF(0, 0, 0, 1));
    Theme.setColor(QPalette::ToolTipBase, QColor::fromRgbF(0.968627, 0.968627, 0.968627, 1));
    Theme.setColor(QPalette::ToolTipText, QColor::fromRgbF(0.137255, 0.14902, 0.160784, 1));
    Theme.setColor(QPalette::PlaceholderText, QColor::fromRgbF(0, 0, 0, 0.501961));

    QApplication::setPalette(Theme);
}

Этот цикл выводит последовательность всех цветовых групп:

  QPalette palette = QApplication::palette();
  for (int role = 0; role < QPalette::NColorRoles; ++role) {
    qDebug() << "Role" << role << ":" << palette.color((QPalette::ColorRole)role);
  }

Уловка QApplication::setStyle

Существует проблема с тем, что некоторые стили используют прозрачные окна. Поэтому необходимо инициализировать их первыми в конструкторе класса, как рекомендуют в руководстве Qt. После этого можно устанавливать другие стили в любой последовательности.

//Installing the first theme of a custom theme. Otherwise, transparency issues occur.
if ( styles.indexOf("kvantum-dark") != -1 ) {
    QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
} else {
    QApplication::setStyle(QStyleFactory::create("Adwaita-HighContrastInverse"));
}

Также в некоторых случаях возможно совмещать свойства двух стилей в один пример:

QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
QApplication::setStyle(QStyleFactory::create("Windows"));

Алгоритм работы QStyleSelect

В конструкторе класса основного окна при первом запуске применяется начальная тема «kvantum‑dark» или «Adwaita‑HighContrastInverse», а если их нет, то остаётся базовая. Затем, после создания объекта MainWindow, запускается метод history_QStyle(), который создаёт файл истории config_style.json в случае его отсутствия. Через файл config_style.json реализовано сохранение выбранной пользователем предыдущей темы по умолчанию после закрытия программы. Начальная тема при первом запуске — ukui. Если темы нет, метод setStyle установит тему по умолчанию «Fusion». Если файл существует, тема по умолчанию будет установлена из файла config_style.json. В методе history_QStyle также выполняется сброс цветовой группы.

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    w.history_QStyle();

    return a.exec();
}

Меню стилей формируется из списка QStringList styles и добавляется в стандартный menubar

ui->menubar->addMenu(menu);

Для каждого стиля в конструкторе класса MainWindow создаётся свой слот:

    // Add actions for each style
    for (const QString &style : styles) {
        QAction *action = menu->addAction(style);

        // Set up a slot to handle style selection
        connect(action, &QAction::triggered, [this, style, configFilePath]() {
            //Reset QPalette values ​​to default
            QApplication::setPalette(*oldPalette);
            QApplication::setStyle(QStyleFactory::create("Fusion"));
            //Disables QPalette from the system in Qt 5
            //white_QPalette();

            QFile file(configFilePath);
            if (file.open(QIODevice::ReadWrite | QIODevice::Text)) {
                QByteArray fileData = file.readAll();
                QJsonDocument jsonDoc = QJsonDocument::fromJson(fileData);

                QJsonObject jsonObj = jsonDoc.object();
                jsonObj["style"] = style;

                file.resize(0);
                QJsonDocument updatedDoc(jsonObj);
                file.write(updatedDoc.toJson());
                file.close();
            }else{
                QMessageBox::information(this, "Style .json", "Failed to create file: " + style);
            }
            //Combination of properties of different topics Qt 5
            if (style == "Windows-dark") {
                QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
                QApplication::setStyle(QStyleFactory::create("Windows"));
            } else if (style == "ukui-dark") {
                QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
                QApplication::setStyle(QStyleFactory::create("ukui"));
            } else if (style == "motif-dark") {
                QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
                QApplication::setStyle(QStyleFactory::create("motif"));
            } else if (style == "cde-dark") {
                QApplication::setStyle(QStyleFactory::create("kvantum-dark"));
                QApplication::setStyle(QStyleFactory::create("cde"));
            } else {
                QApplication::setStyle(QStyleFactory::create(style));
            }

            QMessageBox::information(this, "Style changed", "Style changed to " + style);
        });
    }
Рисунок 4 - Результат работы QStyleSelect
Рисунок 4 — Результат работы QStyleSelect

Заключение

Мне очень интересно узнать мнение общественности в комментариях о шаблоне QStyleSelect. В этой статье, как мне кажется, удалось представить интересный пример использования QStyle (QStyleFactory). Данный шаблон QStyleSelect благодаря своим вариантам стилей даёт возможность расширенной стилизации приложений. В качестве существенного плюса можно отметить то, что не нужно тратить время на QML и CSS, а можно использовать уже готовые стили.