Разработка приложения для настольных или встраиваемых платформ часто упирается в выбор между 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 ограничен границами окна 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 проще, чем кажется. Вам понадобятся два класса:
Класс, представляющий окно виджета.
Класс-прослойка для интерфейса с 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:

Теперь добавим сигнал 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. Код, приведённый в этом руководстве, взят из этого демо.
Движущийся текст должен работать на всех настольных системах, кроме сеансов Wayland в Linux. Причина в том, что я анимирую абсолютное положение окна (в Wayland это ограничено по соображениям безопасности), а не содержимое внутри него. Плюс такого подхода в том, что окно не «зависает» над одним местом и меньше мешает другим приложениям. Иначе при клике по окну оно перехватывало бы события мыши, из-за чего они не попадали бы в приложение за ним.
Пример применения в реальном проекте
Впервые я использовал этот приём в своём проекте FOSS, QPrompt. Там он нужен для пользовательского диалога выбора шрифта, который одновременно служит предпросмотром. Собственный диалог даёт полный контроль над доступными пользователю параметрами форматирования, а для этого приложения нам требовались только предпросмотр крупного текста и ComboBox для выбора системных шрифтов. QPrompt тоже открыт: исходники, относящиеся к этой технике, можно посмотреть на GitHub.

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