Declarative Widgets adding Qt Widgets support to QML

Declarative Widgets is a QML plugin that adds Qt Widgets support to QML. This means we can now easily mix the power of QML with the comfort of a Widgets UI on desktop.

import QtWidgets 1.0 MainWindow { width: 640 height: 400 Label { text: "Hello Declarative Widgets!" alignment: Qt.AlignHCenter | Qt.AlignVCenter } }

Background

Declarative Widgets was born out of a breakfast discussion about how awesome it would be to use QML to describe QWidget-based scenes. If you have ever worked on a Qt Quick project and then switched back to creating a Widgets UI you will understand how frustrating it can be to write and maintain a complex UI in plain C++, or even create and modify UI files in Qt Designer.

The real power of QML, however, is in property bindings. Property bindings allow us to set the value of a property as an expression that is evaluated when ever a property involved in that expression changes. Take the following example:

import QtWidgets 1.0 GroupBox { title: qsTr("New Contact: %1 %2").arg(firstName.text).arg(lastName.text) FormLayout { LineEdit { id: firstName FormLayout.label: qsTr("First Name") } LineEdit { id: lastName FormLayout.label: qsTr("Last Name") } } }

The title property of the GroupBox is updated when the text property of either LineEdit changes. We could build this example in C++, but in QML we don’t need to write any boilerplate code to connect to signals or define slots. By using Declarative Widgets we don’t need to worry about writing our own UI components either; we can make use of all the existing widgets we developed warm, fuzzy feelings for over the years.

Implementation

To get an idea of how the Declarative Widgets plugin works, lets take a look at how QWidget is integrated into QML.

qmlRegisterExtendedType<QWidget, DeclarativeWidgetExtension>(uri, 1, 0, "Widget");

QWidget needs a few tweaks in order to integrate it into QML: there is no default property, the x , y , width and height properties are read-only, and the geometry and visible properties do not have notify signals. Rather than modifying QWidget directly we can use qmlRegisterExtendedType to register an extension object which adds or overrides the properties we need.

class DeclarativeWidgetExtension : public DeclarativeObjectExtension { Q_OBJECT // repeat property declarations, qmlRegisterExtendedType doesn't see the ones from base class Q_PROPERTY(QQmlListProperty<QObject> data READ data DESIGNABLE false CONSTANT) Q_PROPERTY(int x READ x WRITE setX NOTIFY posChanged) Q_PROPERTY(int y READ y WRITE setY NOTIFY posChanged) Q_PROPERTY(int width READ width WRITE setWidth NOTIFY sizeChanged) Q_PROPERTY(int height READ height WRITE setHeight NOTIFY sizeChanged) Q_PROPERTY(QRect geometry READ geometry WRITE setGeometry NOTIFY geometryChanged) Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) Q_CLASSINFO("DefaultProperty", "data") }

Our extension object, DeclarativeWidgetExtension , derives from DeclarativeObjectExtension which provides us with a default property. A default property is the property to which a value is assigned if an object is declared within another object’s definition without declaring it as a value for a particular property. In Qt Quick, the default property is used to construct the visual scene hierarchy, and we do the same with Declarative Widgets to create the QWidget hierarchy, calling QWidget::setParent , QWidget::setLayout , or QWidget::addAction depending on the type of the declared object. Note that we have to redeclare the data property because qmlRegisterExtendedType doesn’t see the one from the base class.

To make the read-only properties writable, we override the existing property and provide a WRITE accessor function to make the appropriate change. Let’s take a look at the new x property:

QWidget *DeclarativeWidgetExtension::extendedWidget() const { QWidget *parentWidget = qobject_cast<QWidget*>(parent()); Q_ASSERT(parentWidget); Q_UNUSED(parentWidget); return parentWidget; } int DeclarativeWidgetExtension::x() const { return extendedWidget()->x(); } void DeclarativeWidgetExtension::setX(int value) { QWidget *widget = extendedWidget(); if (value == widget->x()) return; QRect geometry = widget->geometry(); geometry.moveLeft(value); widget->setGeometry(geometry); }

The READ accessor function simply calls the original READ accessor function on the extended type. However, QWidget does not have an existing setX function so we have to update the x property using QWidget::setGeometry .

Keen observers will notice that we haven’t emitted any of the NOTIFY signals that we declared. This is because widgets respond to events delivered to them by Qt as a result of things that have happened either within the application or as a result of outside activity that the application needs to know about. In order to hook into this system, our extension object installs itself as an event filter on the object we are extending. An event filter receives all the events for the target object before the target does, allowing us to observe and react to the events as required.

bool DeclarativeWidgetExtension::eventFilter(QObject *watched, QEvent *event) { Q_ASSERT(watched == parent()); Q_UNUSED(watched); switch (event->type()) { case QEvent::Move: emit posChanged(); emit geometryChanged(); break; case QEvent::Resize: emit sizeChanged(); emit geometryChanged(); break; case QEvent::Show: case QEvent::Hide: emit visibleChanged(isVisible()); break; default: break; } return false; }

In our event filter we simply emit the NOTIFY signals when we receive the appropriate event. In our x property example we receive a QEvent::Move event as a result of our call to QWidget::setGeometry . This is where we emit posChanged .

The geometry and visible properties that we overrode to add a NOTIFY signal to simply call the original QWidget READ and WRITE accessor functions. Then, in the event filter we emit the new signals when we receive the appropriate event.

What about QQuickWidget or QWebEngineView?

There are no additional limitations to using QQuickWidget with Declarative Widgets. One of the use cases we came up with for using Declarative Widgets is as a stepping stone to porting existing Qt Widgets applications to Qt Quick. The first step of the port would be to isolate the business logic and replicate the existing UI using Declarative Widgets (we even wrote a tool to generate QML files from .ui files). You could then replace chunks of the UI with QtQuick components displayed in QQuickWidgets .

To see QQuickWidget or QWebEngineView in action take a look through our examples on GitHub.

How do I get it?

The Declarative Widgets source code is available on GitHub: https://github.com/KDAB/DeclarativeWidgets

If you like Declarative Widgets please consider contributing to the project. Adding Qt Widgets support to QML is a large task and whilst we have done most of the ground work there are surely features we have missed. If there are features you need and you are unable to contribute, please get in touch and we will see what we can do about implementing them for you.