diff options
| author | Miguel Costa <miguel.costa@qt.io> | 2025-11-24 22:17:15 +0100 |
|---|---|---|
| committer | Miguel Costa <miguel.costa@qt.io> | 2025-11-28 15:17:47 +0000 |
| commit | d7f0145bf00b4c3c911decd152e279c14359627b (patch) | |
| tree | 52f13f9818fea747a250e395a4975523171ad3a6 | |
| parent | 0890c47cf9ed34dba56b86ad05b27d66321593ec (diff) | |
Update README.md
Change-Id: I3a7cb848a1b5ab0436f4ca50b097ac5cdeca36b6
Reviewed-by: Karsten Heimrich <karsten.heimrich@qt.io>
| -rw-r--r-- | README.md | 650 | ||||
| -rw-r--r-- | res/chronometer.png | bin | 175701 -> 0 bytes | |||
| -rw-r--r-- | res/hackathon23.png | bin | 502641 -> 0 bytes | |||
| -rw-r--r-- | res/qt_dotnet.png | bin | 313884 -> 0 bytes | |||
| -rw-r--r-- | res/wpf_qml_window.png | bin | 145468 -> 0 bytes |
5 files changed, 52 insertions, 598 deletions
@@ -1,622 +1,76 @@ <!--************************************************************************************************ - Copyright (C) 2023 The Qt Company Ltd. + Copyright (C) 2025 The Qt Company Ltd. SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only *************************************************************************************************--> -<img src="res/qt_dotnet.png" width="128" height="128"> +# Qt Bridge — C# -# Qt / .NET +## Pre-requisites -The Qt/.NET library allows Qt applications to use .NET code assets, by providing a -[custom runtime host](https://learn.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting) -for managed assemblies, through which managed code can be invoked from C++. +* [**Microsoft Visual Studio 2022**](https://visualstudio.microsoft.com/vs/older-downloads/#visual-studio-2022-and-other-products) + * [Workloads](https://learn.microsoft.com/en-us/visualstudio/install/modify-visual-studio?view=visualstudio) + * [.NET desktop development](https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-professional?view=visualstudio#net-desktop-development) + * [Desktop development with C++](https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-professional?view=visualstudio#desktop-development-with-c) +* [**Qt 6.10**](https://code.qt.io/cgit/qt/qt5.git/log/?h=6.10) built from source + * See instructions in: _[Build Qt 6 (subset) from source](HOW-TO%20build%20nuget%20and%20testapp.md#1-build-qt-6-subset-from-source)_ -* [Quick Start](#quick-start) -* [Requirements](#requirements) -* [Build](#build) -* [Deploy](#deploy) -* [Examples](#examples) - * [QML UI for a .NET module](#qml-ui-for-a-net-module) - * [QML window embedded in WPF application](#qml-window-embedded-in-wpf-application) - * [IoT sample application with Qt and Azure](#iot-sample-application-with-qt-and-azure) -* [Getting started](#getting-started) - * [Calling .NET static methods](#calling-net-static-methods) - * [Handling .NET exceptions](#handling-net-exceptions) - * [Creating .NET object](#creating-net-objects) - * [Calling instance methods](#calling-instance-methods) - * [Writing C++ wrapper classes for .NET types](#writing-c-wrapper-classes-for-net-types) - * [Emitting Qt signals from .NET events](#emitting-qt-signals-from-net-events) - * [Using .NET objects in QML](#using-net-objects-in-qml) - * [Using QML in a WPF application](#using-qml-in-a-wpf-application) +## Visual Studio IDE -## Quick Start +### Tools and installation package -1. Install [requirements](#requirements). -2. Clone repository -3. Open solution file `qtdotnet.sln`. -4. Press `F5`. +1. Open [`qtdotnet.sln`](qtdotnet.sln) solution in Visual Studio +2. Build tools and installation package + * Select menu option: ___Build___ > ___Build Solution___ + * Or press `Ctrl`+`Shift`+`B` -The ["Embedded Window"](#qml-window-embedded-in-wpf-application) example is then built and started. +#### Auto-tests -## Requirements +* View available tests + * Select menu option: ___View___ > ___Test Explorer___ + * Or press: `Ctrl`+`E`, `T` +* Run tests + * Select menu option: ___Test___ > ___Run All Tests___ + * Or press: _`Ctrl`+`R`, `A`_ -Qt/.NET requires Qt6, as well as .NET version 6 or greater. +### Examples -The minimum required version of Visual Studio is VS 2019 (recommended: VS 2022). +1. Open [`examples.sln`](examples/examples.sln) solution in Visual Studio +2. Build example projects + * Select menu option: ___Build___ > ___Build Solution___ + * Or press _`Ctrl`+`Shift`+`B`_ -Using the [Qt Visual Studio Tools](https://doc.qt.io/qtvstools/index.html) 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. +#### Run example app -## Build +1. In the Solution Explorer, right-click an example project +2. Select context menu option: ___Debug___ > ___Start New Instance___ -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. +## Command Prompt -## Deploy +### Tools and installation package -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. +1. Open a Command Prompt + * Windows ___Start___ menu > ___Command Prompt___ + * Or press _<code>⊞ Win</code>+`R`_, then type **`cmd`** and press _`Enter`_ +2. `cd` to [solution dir](./) +3. Build tools and installation package + * **`> dotnet build`** -## Examples +#### Auto-tests -### QML UI for a .NET module +1. Stay in the [solution dir](./) +2. Run tests + * **`> dotnet test`** -An example of a Qt application with a QML front-end for a .NET "business logic" module can be found -in the [`Chronometer`](examples/Chronometer) sample project. +### Examples -<sup><img src="res/chronometer.png" width="310"><br> -<i><code>Chronometer</code> example application.</i></sup> +1. `cd` to the [examples sub-dir](./examples/) + * **`> cd examples`** +2. Build example projects + * **`> dotnet build`** -```c# -public double ElapsedSeconds -{ - get => elapsedSeconds; - private set => SetProperty(ref elapsedSeconds, value, nameof(ElapsedSeconds)); -} -``` +#### Run example app -```cpp -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(); - ... - } -} -``` - -```qml -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`](examples/Chronometer). - -### QML window embedded in WPF application - -The [`EmbeddedWindow`](examples/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. - -<sup><img src="res/wpf_qml_window.png" width="480"><br><i><code>EmbeddedWindow</code> -example application.</i></sup> - -```qml -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++; - } - } - }); -``` - -```cpp -void MainWindow::setFramesPerSecond(double fps) -{ - method("set_FramesPerSecond", d->fnSetEmbeddedFps).invoke(*this, fps); -} -``` - -```c# -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`](examples/EmbeddedWindow). - -### IoT sample application with Qt and Azure - -This example project illustrates how to integrate Qt applications with .NET in a non-Windows setting -like the [Raspberry Pi OS](https://www.raspberrypi.com/software/). - -<sup><img src="res/hackathon23.png" width="320"><br><i><code>QtAzureIoT</code> -example application running on a Raspberry Pi device.</i></sup> - -```cpp -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 QDotNetEventHandler -{ - 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)) - { - subscribe("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`](examples/QtAzureIoT). - - -## Getting started - -### Calling .NET static methods - -The following code uses Qt/.NET to call the static method `Environment.GetEnvironmentVariable()`. - -```cpp -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: - -```cpp -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: - -```cpp -QtDotNet::call<QString, QString>("System.Environment", "GetEnvironmentVariable", "PATH"); -``` - -### Handling .NET exceptions - -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. - -```cpp -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. - -### Creating .NET objects - -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. - -```cpp -auto newStringBuilder = QDotNetType::constructor("System.Text.StringBuilder"); -QDotNetObject stringBuilder = newStringBuilder(); -``` - -### Calling instance methods - -With a `QDotNetObject`, it's possible to obtain a reference to, and then call an instance method of -the referenced object. - -```cpp -auto append = stringBuilder.method<QDotNetObject, QString>("Append"); -append("Hello"); -append(" World!"); -QString helloWorld = stringBuilder.toString(); //"Hello World!" -``` - -### Writing C++ wrapper classes for .NET types - -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. - -```cpp -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>"; -} -``` - -### Emitting Qt signals from .NET events - -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. - -```cpp -class Ping : public QObject, public QDotNetObject, public QDotNetEventHandler -{ - Q_OBJECT -public: - Q_DOTNET_OBJECT_INLINE(Ping, "System.Net.NetworkInformation.Ping, System"); - Ping() : QDotNetObject(constructor<Ping>().invoke(nullptr)) - { - subscribe("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 -``` - -### Using .NET objects in QML - -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. - -<blockquote> - -<sub>Managed class implementing property change notifications:</sub> -```c# -public class Chronometer : INotifyPropertyChanged -{ - public double Hours - { - get { ... } - private set - { - ... - NotifyPropertyChanged("Hours"); - } - } - ... -} -``` -</blockquote> - -<blockquote> - -<sub>Native wrapper with Qt properties mirroring those in the managed class:</sub> - -```cpp -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 QDotNetEventHandler -{ - ... -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); -``` -</blockquote> - -<blockquote> - -<sub>QML UI specification using properties of the .NET type:</sub> -```qml -... -Image { - id: hoursHand; - source: "hour_hand.png" - transform: Rotation { - origin.x: 249; origin.y: 251 - angle: 110 + (chrono.hours % 12) * 30 - } -} -... -``` -</blockquote> - -### Using QML in a WPF application - -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. - -<blockquote> - -<sub>WPF main window, including a host panel where the QML view will be embedded:</sub> -```xml -<Window> - <Grid> - <Label x:Name="labelCoords" Content="WPF Window" /> - <WindowsFormsHost Name="EmbeddedAppHost"> - <wf:Panel Name="EmbeddedAppPanel" BackColor="#AAAAAA"/> - </WindowsFormsHost> - </Grid> -</Window> -``` - -</blockquote> - -<blockquote> - -<sub>QML view to embed in the WPF application:</sub> -```qml -Item { - width: mainWindow.hostWidth - height: mainWindow.hostHeight - Rectangle { - anchors.fill: parent - color: "#AAAAFF" - Text { - id: coordinates - text: "QML Window" - ... - } - } - ... -} -``` -</blockquote> - -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. - -<blockquote> - -<sub>Initialization of the WPF thread:</sub> -```c++ -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"); -``` -</blockquote> - -<blockquote> - -<sub>Initialization of the QML thread:</sub> -```c++ -embeddedWindow = QWindow::fromWinId((WId)mainWindow->hostHandle()); -quickView = new QQuickView(qmlEngine, embeddedWindow); -quickView->setSource(QUrl(QStringLiteral("qrc:/main.qml"))); -quickView->show(); -``` -</blockquote> - -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. - -<blockquote> - -<sub>Native wrapper, including conversion of WPF events into signals:</sub> - -```c++ -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); -} -``` -</blockquote> - -<blockquote> - -<sub>Signals converted from WPF events are connected to slots in the QML UI:</sub> -```qml -... -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(); } -} -... -``` -</blockquote> - -<blockquote> - -<sub>WPF application method called from slot connected to QML signal:</sub> -```c# -public class MainWindow : Window -{ - ... - public void EmbeddedMousePosition(string source, double x, double y) - { - Application.Current.Dispatcher.Invoke(() => PrintMousePosition(source, x, y)); - } - ... -} -``` -</blockquote> +1. `cd` to [example project sub-dir](./examples/Primes/) + * E.g.: **`> cd Primes`** +2. Run example app + * **`> dotnet run`** diff --git a/res/chronometer.png b/res/chronometer.png Binary files differdeleted file mode 100644 index 35036ea..0000000 --- a/res/chronometer.png +++ /dev/null diff --git a/res/hackathon23.png b/res/hackathon23.png Binary files differdeleted file mode 100644 index e23a856..0000000 --- a/res/hackathon23.png +++ /dev/null diff --git a/res/qt_dotnet.png b/res/qt_dotnet.png Binary files differdeleted file mode 100644 index 3d4e748..0000000 --- a/res/qt_dotnet.png +++ /dev/null diff --git a/res/wpf_qml_window.png b/res/wpf_qml_window.png Binary files differdeleted file mode 100644 index e2dc08c..0000000 --- a/res/wpf_qml_window.png +++ /dev/null |
