Skip to main content

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 + NOTIFY signals, actions with Q_INVOKABLE, and lists via models or QVariant data.
  • 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.