Rendering UI
FrameLift's UI is Qt Quick (QML) over a retained scene graph. A plugin that
draws UI ships its own QML, embedded in the plugin, and a QObject view-model
that exposes state to it. The host's QmlCompositor instantiates the QML root and
layers it over the video by renderOrder. Qt repaints only what actually changed —
there is no per-frame Render() call.
The split is strict and worth internalizing:
- C++ (the view-model) exposes state with
Q_PROPERTY+NOTIFYsignals, actions withQ_INVOKABLE, and lists via models orQVariantdata. - QML owns all presentation, layout, and finite animation.
Making a plugin render
Three pieces wire a plugin's UI together.
1. A QObject view-model
The plugin's module is a QObject that also derives ModuleBase. Expose what QML
needs to read as Q_PROPERTY with a NOTIFY signal, and what QML can trigger as
Q_INVOKABLE:
#include <framelift/core.h>
#include <framelift/platform.h>
#include <QtCore/QObject>
class MyPanel final : public QObject, public ModuleBase
{
Q_OBJECT
Q_PROPERTY(bool open READ IsOpen NOTIFY openChanged)
Q_PROPERTY(QString title READ Title NOTIFY titleChanged)
public:
MyPanel() = default;
[[nodiscard]] bool IsOpen() const noexcept { return open_; }
[[nodiscard]] QString Title() const { return QString::fromStdString(title_); }
Q_INVOKABLE void Close()
{
open_ = false;
Q_EMIT openChanged();
}
Q_SIGNALS:
void openChanged();
void titleChanged();
protected:
const char* ModuleName() const override { return "MyPanel"; }
private:
bool open_ = true;
std::string title_;
};
FRAMELIFT_MODULE_ENTRY(MyPanel, {
.renderOrder = 50,
})
FRAMELIFT_MODULE_ENTRY defaults .qml = true, so the host treats the module
itself as the view-model handed to its QML root. A module with no UI opts out
explicitly with .qml = false (and then need not be a QObject).
2. The QML root
Author a QML file whose root receives the view-model as a required property:
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import FrameLift.Controls
Item {
id: root
required property var viewModel
Rectangle {
anchors.fill: parent
visible: root.viewModel !== null && root.viewModel.open
color: Theme.canvas
Text {
anchors.centerIn: parent
text: root.viewModel.title
color: Theme.text
}
}
}
The host injects the C++ view-model as viewModel; bind your properties off it and
call its Q_INVOKABLE methods directly (root.viewModel.Close()). The
FrameLift.Controls module exposes the shared Theme and common controls.
3. Register both with the build
Declare the QML entry and its URI in add_framelift_plugin:
add_framelift_plugin(MyPanel
PLUGIN_JSON "${CMAKE_CURRENT_SOURCE_DIR}/MyPanel.Plugin.json"
QML_URI FrameLift.Plugins.MyPanel
QML_ENTRY "${CMAKE_CURRENT_SOURCE_DIR}/MyPanel.qml"
"${CMAKE_CURRENT_SOURCE_DIR}/MyPanel.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MyPanel.h"
${FRAMELIFT_SDK_SOURCES})
To ship images or other assets the QML references, add them as Qt resources under
the plugin's QML prefix with qt_add_resources, then reference them by name from
QML (source: "icon.svg").
Render order
The .renderOrder field in FRAMELIFT_MODULE_ENTRY controls stacking relative
to other plugins — it maps to Qt's z. Lower numbers render first (further back);
higher numbers sit on top. The overlay, playlist, and history panels each pick an
order; choose yours to sit where you want in the stack.
The redraw contract
Because Qt repaints on demand, every plugin follows one rule: timers and background work must stop or slow down while the plugin's surface is hidden. Bind animations and polling to visibility so a hidden panel costs nothing. (Benchmark is the deliberate exception while open, because it measures live frame timing.)
A complete example
The bundled Overlay plugin is the canonical reference for this pattern —
Overlay.h (the Q_PROPERTY view-model), Overlay.qml (the QML root with
required property var viewModel), and its CMakeLists.txt (QML_URI /
QML_ENTRY plus a qt_add_resources icon). Read it alongside this guide.