Разработка приложения для настольных или встраиваемых платформ часто упирается в выбор между Qt Widgets и Qt Quick для создания интерфейса. У каждого подхода свои плюсы и минусы. Qt как гибкий фреймворк позволяет комбинировать их разными способами, а выбор способа интеграции этих API зависит от того, чего вы хотите добиться. В этой статье я покажу, как отображать окна Qt Widgets в приложении, написанном преимущественно на Qt Quick.

Зачем показывать окно Qt Widgets в приложении Qt Quick

Qt Quick отлично подходит для ПО, где акцент сделан на визуальный язык. Графический конвейер, построенный вокруг Qt Quick Scene Graph, эффективно рендерит интерфейс с использованием GPU. Это означает, что элементы UI можно быстро рисовать, оформлять и анимировать, если использовать подходящие инструменты (например, шейдеры, аниматоры и Qt Quick Shapes вместо Canvas API (HTML5 Canvas)).

Однако из особенностей Scene Graph вытекают и некоторые слабые стороны Quick. Элементы интерфейса, которые в других приложениях могут выходить за пределы окна (например, всплывающие подсказки и элемент управления ComboBox), в Qt Quick можно отрисовать только внутри окна Qt Quick. Когда вы видите, как в других приложениях подсказки и выпадающие списки выходят за границы окна, на самом деле они отрисовываются в отдельном окне без рамки и заголовка (то есть в безрамочном окне). Рендеринг всего в одном окне помогает обеспечить совместимость с системами, где одновременно может отображаться только одно окно, например Android и iOS, но для настольных сред это может приводить к неэффективному использованию пространства.

Анимация показывает небольшое окно, где для сравнения открываются ComboBox из QML и из Widgets.
Анимация показывает небольшое окно, где для сравнения открываются ComboBox из QML и из Widgets.

ComboBox в QML ограничен границами окна Qt Quick, тогда как ComboBox из Widgets может выходить за пределы окна.

Qt позволяет комбинировать Qt Widgets и Qt Quick несколькими способами. Один из распространенных подходов — встроить QML в приложение на Widgets через QQuickWidget. Этот подход уместен для приложений, которые в основном используют Qt Widgets. Другой вариант — отрисовать элементы Qt Widgets внутри компонента Qt Quick через QQuickPaintedItem. Однако такой компонент будет ограничен теми же границами окна, что и остальные элементы в вашем окне Qt Quick, и не получит оптимизаций рендеринга Scene Graph, то есть вы получите худшее из обоих миров.

Третье решение — открывать окна виджетов из приложений на Qt Quick. У этого варианта нет перечисленных выше недостатков, однако есть и свои минусы. Во-первых, приложение должно выполняться в среде, поддерживающей несколько окон. Во-вторых, окна виджетов нельзя сделать дочерними по отношению к окнам Qt Quick; из-за этого некоторые функции, зависящие от z-порядка окон, например установка модальности окна в Qt::WindowModal, не будут действовать на исходное окно, когда из Qt Quick открывается окно виджетов. Обойти это можно, если установить модальность Qt::ApplicationModal — при условии, что вас устраивает блокировка всех остальных окон.

Отображение окон Qt Widgets в приложениях Qt Quick не раз выручало меня на практике, и я не встречал документации по этой теме — поэтому и написал это руководство.

Как отобразить окно виджета Qt в приложении Qt Quick

Архитектура в целом

Отображать окно Qt Widgets из Qt Quick проще, чем кажется. Вам понадобятся два класса:

  1. Класс, представляющий окно виджета.

  2. Класс-прослойка для интерфейса с QML, который будет владеть этим окном и создавать его экземпляр.

Не регистрируйте QWidget как QML-тип. Создавайте и управляйте им из C++-прослойки (QObject) в GUI-потоке, а в QML экспортируйте только свойства/сигналы этого прокси.

Обновляем CMakeLists.txt

Помимо этих классов, нужно убедиться, что ваше приложение линкуется и с Qt::Quick, и с Qt::Widgets. Пример для CMake-проекта:

// Найти пакеты
find_package(Qt6 6.5 REQUIRED COMPONENTS
    Quick
    Widgets)

// Связать цель сборки с библиотеками
target_link_libraries(${TARGET_NAME} PRIVATE
    Qt6::Quick
    Qt6::Widgets)

// Замените ${TARGET_NAME} на имя вашего целевого исполняемого файла

Обновляем main.cpp

Кроме того, в main.cpp нужно использовать QApplication вместо QGuiApplication.

QApplication app(argc, argv);

Интерфейсный слой

Подготовьте интерфейсный слой так же, как любой компонент Qt Quick на C++. То есть унаследуйтесь от QObject и используйте макросы Q_OBJECT и QML_ELEMENT, чтобы сделать ваш класс доступным из QML.

// widgetFormHandler.h
#pragma once
class WidgetFormHandler : public QObject
{
    Q_OBJECT
    QML_ELEMENT

public:
    explicit WidgetFormHandler(QObject *parent = nullptr);
};
// widgetFormHandler.cpp

WidgetFormHandler::WidgetFormHandler(QObject *parent)
    : QObject(parent)
{
}

Создание окна Widgets

// widgetFormHandler.h
#pragma once
class WidgetsForm;
class WidgetFormHandler : public QObject
{
    Q_OBJECT
    QML_ELEMENT

public:
    explicit WidgetFormHandler(QObject *parent = nullptr);
~WidgetFormHandler();

private:
    std::unique_ptr<WidgetsForm> m_window;
}

Используйте std::make_unique в конструкторе, чтобы инициализировать unique_ptr m_window.

Определите деструктор класса, создающего экземпляр, чтобы гарантировать освобождение указателей и избежать утечек памяти. Если вы повсюду используете умные указатели, за вас всё сделает C++: достаточно дефолтного деструктора, как в примере ниже. 

// widgetFormHandler.cpp
#include "widgetFormHandler.h"

WidgetFormHandler::WidgetFormHandler(QObject *parent)
    : QObject(parent)
    , m_window(std::make_unique<WidgetsForm>())
{
    // ...
}

WidgetFormHandler::~WidgetFormHandler() = default;

Делаем свойства доступными в QML

Теперь нам нужно сделать свойства виджета доступными из QML. Конкретный способ зависит от самого свойства и от того, будет ли его значение изменяться в обе стороны, или только с одной стороны с обновлением на другой.

Рассмотрим двусторонний пример: добавим возможность управлять видимостью окна виджета из QML. В C++-прослойку добавим свойство «visible», чтобы оно соответствовало свойству visible у окон Qt Quick в QML. Объявляем свойство через Q_PROPERTY. Для управления состоянием окна используем методы READ и WRITE.

Вот как это будет выглядеть:

// widgetFormHandler.h
#pragma once
class WidgetsForm;

class WidgetFormHandler : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged)

public:
    explicit WidgetFormHandler(QObject *parent = nullptr);    
~WidgetFormHandler();
    const bool isVisible();    
    void setVisible(bool);

signals:
    void visibleChanged();

private:
    std::unique_ptr<WidgetsForm> m_window;
};
// widgetFormHandler.cpp
#include "widgetFormHandler.h"

#include "widgetForm.h"

WidgetFormHandler::WidgetFormHandler(QObject *parent)
    : QObject(parent)
    , m_window(std::make_unique<WidgetsForm>())
{
    // По умолчанию скрываем окно
    m_window->setVisible(false);
}
WidgetFormHandler::~WidgetFormHandler() = default;
const bool WidgetFormHandler::isVisible()
{
    return m_window->isVisible();
}
void WidgetFormHandler::setVisible(bool visible)
{
    m_window->setVisible(visible);
    emit visibleChanged();
}

Чтобы сделать поведение двусторонним, укажите в Q_PROPERTY сигнал в параметре NOTIFY, который позволит обновлять значение свойства в QML после испускания сигнала, и испускайте этот сигнал там, где это уместно. Мы испускаем его в setVisible() этого класса; если бы у QWidget был сигнал об изменении видимости, стоило бы связать его с visibleChanged нашего обработчика. Поскольку такого сигнала нет, нам необходимо испускать его самостоятельно.

Сделать сигналы доступными в QML

Разрабатывайте окно виджета так же, как любой другой виджет. Если вы используете UI-формы, добавьте в заголовочный файл сигнал для каждого действия, которое хотите пробрасывать в QML.

В этом примере мы пробросим нажатие кнопки из UI-файла, поэтому создадим в файле .ui кнопку с именем pushButton:

Qt Designer показывает UI-файл с кнопкой с именем pushButton в стиле camelCase.
Qt Designer показывает UI-файл с кнопкой с именем pushButton в стиле camelCase.

Теперь добавим сигнал buttonClicked в заголовочный файл:

// widgetsForm.h
#pragma once
#include <QWidget>

namespace Ui
{
    class WidgetsForm;
}

class WidgetsForm : public QWidget
{
    Q_OBJECT
public:
    explicit WidgetsForm(QWidget *parent = nullptr);
    ~WidgetsForm();
signals:
    void buttonClicked();
    // Сигнал для проброса клика кнопки из окна Widgets в QML
private:
    std::unique_ptr<Ui::WidgetsForm> ui;
};

И снова используем unique_ptr, на этот раз для хранения объекта ui. Это лучше, чем то, что генерируют шаблоны Qt Creator, потому что управление памятью возьмёт на себя C++, и нам не понадобится delete в деструкторе.

В конструкторе окна свяжем сигнал кнопки из UI с нашим сигналом, который мы добавили для дальнейшего проброса:

// widgetsForm.cpp
#include "widgetsform.h"
#include "ui_widgetsform.h"

WidgetsForm::WidgetsForm(QWidget *parent)
    : QWidget(parent)
    , ui(std::make_unique<Ui::WidgetsForm>())
{
    ui->setupUi(this);
    // Expose click
    connect(ui->pushButton, &QPushButton::clicked, this, &WidgetsForm::buttonClicked);
}

WidgetsForm::~WidgetsForm() = default;

Прежде чем подключить проброшенный сигнал к QML-интерфейсу, нам понадобится ещё один сигнал на стороне интерфейсного слоя, чтобы передать событие в QML. Для этого добавим сигнал qmlSignalEmitter:

// widgetFormHandler.h
[..]
signals:
    void visibleChanged();
    void qmlSignalEmitter();  // Сигнал для проброса клика кнопки в QML
[..]

Чтобы завершить все соединения, перейдите в конструктор интерфейсного слоя и свяжите сигнал вашего класса окна с сигналом интерфейсного слоя. Это будет выглядеть так:

// widgetFormHandler.cpp
[..]
WidgetFormHandler::WidgetFormHandler(QObject *parent)
    : QObject(parent)
    , m_window(std::make_unique<WidgetsForm>())
{
    QObject::connect(m_window, &WidgetsForm::buttonClicked, this,
    &WidgetFormHandler::qmlSignalEmitter);
}
[..]

Связывая один сигнал с другим, мы разделяем области ответственности классов и сокращаем шаблонный код, что упрощает поддержку.

На стороне QML подключаемся к qmlSignalEmitter с помощью префикса on. Это выглядит так:

import NameOfAppQmlModule  // Должен совпадать с URI из qt_add_qml_module в CMake

WidgetFormHandler {
    id: fontWidgetsForm

    visible: true  // Сделать окно Widgets видимым из QML

    onQmlSignalEmitter: () => {
        console.log("Button pressed in widgets")  // Логируем событие клика QPushButton из QML 
    }
}

Финальный продукт

Я подготовил демо-приложение, где можно увидеть эту технику в действии. В демо отображается текст, который «прыгает» по экрану, как логотип старого DVD-плеера. Текст и шрифт можно менять через две одинаковые формы: одна реализована на QML, другая — на Widgets. Код, приведённый в этом руководстве, взят из этого демо.

Пример кода на GitHub.  

Движущийся текст должен работать на всех настольных системах, кроме сеансов Wayland в Linux. Причина в том, что я анимирую абсолютное положение окна (в Wayland это ограничено по соображениям безопасности), а не содержимое внутри него. Плюс такого подхода в том, что окно не «зависает» над одним местом и меньше мешает другим приложениям. Иначе при клике по окну оно перехватывало бы события мыши, из-за чего они не попадали бы в приложение за ним.

Пример применения в реальном проекте

Впервые я использовал этот приём в своём проекте FOSS, QPrompt. Там он нужен для пользовательского диалога выбора шрифта, который одновременно служит предпросмотром. Собственный диалог даёт полный контроль над доступными пользователю параметрами форматирования, а для этого приложения нам требовались только предпросмотр крупного текста и ComboBox для выбора системных шрифтов. QPrompt тоже открыт: исходники, относящиеся к этой технике, можно посмотреть на GitHub.


Если вам интересна работа на стыке QML и Widgets, стоит глубже погрузиться в сам фреймворк. В OTUS скоро стартует курс «Разработка прикладного ПО на Qt и ОС Аврора», где за четыре месяца можно системно освоить Qt, QML и нативную разработку под Аврору — от архитектуры до готовых приложений для бизнеса и госкомпаний. Подойдет ли вам программа курса? Пройдите вступительный тест.

А тем, кто настроен на серьезное системное обучение, крайне рекомедуем рассмотреть Подписку — выбираете курсы под свои задачи, экономите на обучении, получаете профессиональный рост. Узнать подробне

Комментарии (0)