The Qt/.NET library allows Qt applications to use .NET code assets, by providing a custom runtime host for managed assemblies, through which managed code can be invoked from C++.
- Install requirements.
- Clone repository
- Open solution file
qtdotnet.sln
. - Press
F5
.
The "Embedded Window" example is then built and started.
Qt/.NET requires Qt6, as well as .NET version 6 or greater.
The minimum required version of Visual Studio is VS 2019 (recommended: VS 2022).
Using the Qt Visual Studio Tools extension is recommended,
but not required. However, the sample code provided in the examples
directory does require the Qt
VS Tools extension, as well as a Qt6 installation set as default Qt version.
To use the Qt/.NET library, the include
directory must be in the include path. Qt/.NET is a
header-only library, therefore no link time requirements exist.
At run time, the adapter assembly (Qt.DotNet.Adapter.dll
) must be present at a location accessible
by the .NET assembly locating services. This can be the directory of the application binary.
An example of a Qt application with a QML front-end for a .NET "business logic" module can be found
in the Chronometer
sample project.
Chronometer
example application.
public double ElapsedSeconds
{
get => elapsedSeconds;
private set => SetProperty(ref elapsedSeconds, value, nameof(ElapsedSeconds));
}
class QChronometer : public QObject, public QDotNetObject
{
Q_OBJECT
Q_PROPERTY(double elapsedSeconds READ elapsedSeconds NOTIFY elapsedSecondsChanged)
...
Q_DOTNET_OBJECT(QChronometer, "WatchModels.Chronometer, ChronometerModel");
...
void handleEvent(const QString &eventName, QDotNetObject &sender, QDotNetObject &args) override
{
...
const auto propertyChangedEvent = args.cast<QDotNetPropertyEvent>();
...
if (propertyChangedEvent.propertyName() == "ElapsedSeconds")
emit elapsedSecondsChanged();
...
}
}
Image {
id: secondsHand;
source: "second_hand.png"
transform: Rotation {
origin.x: 250; origin.y: 250
angle: chrono.elapsedSeconds * 6
Behavior on angle {
SpringAnimation { spring: 3; damping: 0.5; modulus: 360 }
}
}
}
Full source code in examples/Chronometer
.
The EmbeddedWindow
example illustrates how to embed a QML window
within a WPF application. The WPF and QML UI stacks run on separate threads of the same process.
EmbeddedWindow
example application.
window.afterFrameEnd.connect(
function() {
var t = Date.now();
if (t0 == 0) {
t0 = t;
n = 1;
} else {
var dt = t - t0;
if (dt >= 1000) {
mainWindow.framesPerSecond = (1000 * n) / dt;
n = 0;
t0 = t;
} else {
n++;
}
}
});
void MainWindow::setFramesPerSecond(double fps)
{
method("set_FramesPerSecond", d->fnSetEmbeddedFps).invoke(*this, fps);
}
public double FramesPerSecond
{
get => WpfThread(() => FpsValue.Value);
set
{
WpfThread(() =>
{
if (value <= FpsValue.Maximum)
FpsValue.Value = value;
FpsLabel.Text = $"{value:0.0} fps";
});
}
}
Full source code in examples/EmbeddedWindow
.
This example project illustrates how to integrate Qt applications with .NET in a non-Windows setting like the Raspberry Pi OS.
QtAzureIoT
example application running on a Raspberry Pi device.
class Backoffice : public QDotNetObject
{
public:
Q_DOTNET_OBJECT_INLINE(Backoffice, "QtAzureIoT.Device.Backoffice, DeviceToBackoffice");
Backoffice() : QDotNetObject(getConstructor<Backoffice>().invoke(nullptr))
{}
void setTelemetry(QString name, double value)
{
getMethod("SetTelemetry", fnSetTelemetryDouble).invoke(*this, name, value);
}
...
};
class SensorData : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler
{
Q_OBJECT
Q_PROPERTY(double temperature READ temperature NOTIFY temperatureChanged)
...
public:
Q_DOTNET_OBJECT_INLINE(SensorData, "QtAzureIoT.Device.SensorData, SensorData");
SensorData() : QDotNetObject(getConstructor<SensorData>().invoke(nullptr))
{
subscribeEvent("PropertyChanged", this);
}
double temperature() const
{
return getMethod("get_Temperature", fnGet_Temperature).invoke(*this);
}
...
signals:
void temperatureChanged();
...
};
...
QObject::connect(&sensor, &SensorData::temperatureChanged,
[&backoffice, &sensor]()
{
backoffice.setTelemetry("temperature", sensor.temperature());
});
...
Full source code in examples/QtAzureIoT
.
The following code uses Qt/.NET to call the static method Environment.GetEnvironmentVariable()
.
QDotNetType environmentType = QDotNetType::find("System.Environment");
auto getEnvironmentVariable = environmentType.method<QString, QString>("GetEnvironmentVariable");
QString path = getEnvironmentVariable("PATH");
The method()
function returns an instance of QDotNetFunction<QString, QString>
. This type is a
specialization of the following template type:
template<typename TResult, typename... TArg>
class QDotNetFunction {...};
This is a functor that encapsulates a call to a .NET function, where TResult
is the return type,
and TArg...
are the argument types.
The following shorthand form can also be used to call a .NET static method:
QtDotNet::call<QString, QString>("System.Environment", "GetEnvironmentVariable", "PATH");
The QDotNetFunction
type does not provide exception handling. If an exception is thrown during the
managed function call, the application will crash. To avoid this, use QDotNetSafeMethod
instead.
QDotNetSafeMethod<QString, QString> safeGetEnvironmentVariable
= environment.method<QString, QString>("GetEnvironmentVariable");
try {
path = safeGetEnvironmentVariable.invoke(nullptr, "PATH");
} catch (QDotNetException &e) {
path = "<ERROR>";
}
There is a performance cost to using QDotNetSafeMethod
. If a .NET call is guaranteed not to throw
an exception, using QDotNetFunction
will provide better performance.
To create a .NET object, a reference to a constructor method must first be obtained. The constructor
method will return a QDotNetObject
referencing the newly created managed object.
auto newStringBuilder = QDotNetType::constructor("System.Text.StringBuilder");
QDotNetObject stringBuilder = newStringBuilder();
With a QDotNetObject
, it's possible to obtain a reference to, and then call an instance method of
the referenced object.
auto append = stringBuilder.method<QDotNetObject, QString>("Append");
append("Hello");
append(" World!");
QString helloWorld = stringBuilder.toString(); //"Hello World!"
To make it easier to create managed objects and calling their methods, it's possible to extend the
QDotNetObject
class to write wrapper classes for .NET types used in Qt applications.
class StringBuilder : public QDotNetObject
{
public:
Q_DOTNET_OBJECT_INLINE(StringBuilder, "System.Text.StringBuilder");
StringBuilder() : QDotNetObject(constructor<StringBuilder>().invoke(nullptr))
{ }
StringBuilder append(const QString &str)
{
return method("Append", safeAppend).invoke(*this, str).cast<StringBuilder>();
}
private:
QDotNetSafeMethod<QDotNetObject, QString> safeAppend;
};
...
StringBuilder stringBuilder;
QString helloWorld;
try {
stringBuilder.append("Hello").append(" World!");
helloWorld = stringBuilder.toString(); //"Hello World!"
} catch (QDotNetException &e) {
helloWorld = "<ERROR>";
}
A wrapper for a .NET type can also extend QObject
, which will allow integration of managed
objects in a Qt application. This includes receiving notifications of .NET events and emitting
corresponding Qt signals.
class Ping : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler
{
Q_OBJECT
public:
Q_DOTNET_OBJECT_INLINE(Ping, "System.Net.NetworkInformation.Ping, System");
Ping() : QDotNetObject(constructor<Ping>().invoke(nullptr))
{
subscribeEvent("PingCompleted", this);
}
void sendAsync(const QString &hostNameOrAddress)
{
method("SendAsync", safeSendAsync).invoke(*this, hostNameOrAddress, nullptr);
}
signals:
void pingCompleted(QString address, qint64 roundtripTime);
private:
void handleEvent(const QString &evName, QDotNetObject &evSrc, QDotNetObject &evArgs) override
{
auto reply = evArgs.method<QDotNetObject>("get_Reply");
auto replyAddress = reply().method<QDotNetObject>("get_Address");
auto replyRoundtrip = reply().method<qint64>("get_RoundtripTime");
emit pingCompleted(replyAddress().toString(), replyRoundtrip());
}
QDotNetSafeMethod<void, QString, QDotNetNull> safeSendAsync;
};
...
Ping ping;
bool waiting = true;
QObject::connect(&ping, &Ping::pingCompleted,
[&waiting](QString address, qint64 roundtripMsecs)
{
qInfo() << "Reply from" << address << "in" << roundtripMsecs << "msecs";
waiting = false;
});
for (int i = 0; i < 4; ++i) {
waiting = true;
ping.sendAsync("www.qt.io");
while (waiting)
QCoreApplication::processEvents();
}
//// Console output:
// Reply from "199.60.103.31" in 18 msecs
// Reply from "199.60.103.31" in 14 msecs
// Reply from "199.60.103.31" in 13 msecs
// Reply from "199.60.103.31" in 12 msecs
The standard mechanism for property binding in .NET applications (for example, to bind data objects
with WPF UI specifications) requires that types implement the INotifyPropertyChanged
interface,
through which they will be able to synchronize bound properties.
This mechanism can also be used to bind properties of .NET objects in QML, by handling property change events and emitting corresponding property notification signals.
Managed class implementing property change notifications:
public class Chronometer : INotifyPropertyChanged { public double Hours { get { ... } private set { ... NotifyPropertyChanged("Hours"); } } ... }
Native wrapper with Qt properties mirroring those in the managed class:
class QChronometer : public QObject, public QDotNetObject { Q_OBJECT Q_PROPERTY(double hours READ hours NOTIFY hoursChanged) ... public: Q_DOTNET_OBJECT(QChronometer, "WatchModels.Chronometer, ChronometerModel"); ... double hours(); ... signals: void hoursChanged(); ... }; ... struct QChronometerPrivate : public QDotNetObject::IEventHandler { ... void handleEvent(const QString &eventName, QDotNetObject &sender, QDotNetObject &args) override { if (eventName != "PropertyChanged") return; if (args.type().fullName() != QDotNetPropertyEvent::FullyQualifiedTypeName) return; auto propertyChangedEvent = args.cast<QDotNetPropertyEvent>(); if (propertyChangedEvent.propertyName() == "Hours") emit q->hoursChanged(); ... } } ... QQmlApplicationEngine engine; QChronometer chrono(); engine.rootContext()->setContextProperty("chrono", &chrono);
QML UI specification using properties of the .NET type:
... Image { id: hoursHand; source: "hour_hand.png" transform: Rotation { origin.x: 249; origin.y: 251 angle: 110 + (chrono.hours % 12) * 30 } } ...
Qt/.NET can be used to integrate Qt in WPF applications. For example, it is possible to embed a QML view inside a WPF window, as illustrated below.
WPF main window, including a host panel where the QML view will be embedded:
<Window> <Grid> <Label x:Name="labelCoords" Content="WPF Window" /> <WindowsFormsHost Name="EmbeddedAppHost"> <wf:Panel Name="EmbeddedAppPanel" BackColor="#AAAAAA"/> </WindowsFormsHost> </Grid> </Window>
QML view to embed in the WPF application:
Item { width: mainWindow.hostWidth height: mainWindow.hostHeight Rectangle { anchors.fill: parent color: "#AAAAFF" Text { id: coordinates text: "QML Window" ... } } ... }
Since both frameworks require ownership of an UI thread, they must run in separate threads. This should not become an issue, as Qt is, by design, well suited for multi-threaded environments. In particular, the signal-slot mechanism offers a robust means to integrate Qt and WPF wrappers across multiple threads.
Initialization of the WPF thread:
mainWindow = constructor<MainWindow>().invoke(nullptr); mainWindow->setHostHandle(method<HwndHost>("get_HwndHost").invoke(mainWindow)); QtDotNet::call<void, MainWindow>("WpfApp.Program, WpfApp", "set_MainWindow", mainWindow); QtDotNet::call<int>("WpfApp.Program, WpfApp", "Main");
Initialization of the QML thread:
embeddedWindow = QWindow::fromWinId((WId)mainWindow->hostHandle()); quickView = new QQuickView(qmlEngine, embeddedWindow); quickView->setSource(QUrl(QStringLiteral("qrc:/main.qml"))); quickView->show();
WPF events are handled by the MainWindow wrapper and converted to signals connected to the QML UI. Simultaneously, signals from the QML UI connected to slots in the MainWindow wrapper will trigger corresponding changes in the WPF UI.
Native wrapper, including conversion of WPF events into signals:
class MainWindow : public QObject, public QDotNetObject { Q_OBJECT public: Q_DOTNET_OBJECT(MainWindow, "WpfApp.MainWindow, WpfApp"); ... signals: void mouseMove(double x, double y); void mouseLeave(); void closed(); public slots: void embeddedMousePosition(const QString &source, double x, double y); ... }; void handleEvent(const QString &evName, QDotNetObject &evSource, QDotNetObject &evArgs) override { if (evName == "MouseMove") { const auto &mouseEvent = evArgs.cast<MouseEventArgs>(); emit q->mouseMove(mouseEventX(mouseEvent), mouseEventY(mouseEvent)); } else if (evName == "MouseLeave") { emit q->mouseLeave(); } else if (evName == "Closed") { emit q->closed(); } }; ... void embeddedMousePosition(const QString &source, double x, double y) { method("EmbeddedMousePosition", d->fnEmbeddedMousePosition).invoke(*this, source, x, y); }
Signals converted from WPF events are connected to slots in the QML UI:
... MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true onPositionChanged: function(mouse) { updateCoordinates("QML", mouse.x, mouse.y); mainWindow.embeddedMousePosition("QML", mouse.x, mouse.y); } } Connections { target: mainWindow function onMouseMove(x, y) { updateCoordinates("WPF", x, y); } function onMouseLeave() { clearCoordinates(); } } ...
WPF application method called from slot connected to QML signal:
public class MainWindow : Window { ... public void EmbeddedMousePosition(string source, double x, double y) { Application.Current.Dispatcher.Invoke(() => PrintMousePosition(source, x, y)); } ... }