aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiguel Costa <miguel.costa@qt.io>2025-11-24 22:17:15 +0100
committerMiguel Costa <miguel.costa@qt.io>2025-11-28 15:17:47 +0000
commitd7f0145bf00b4c3c911decd152e279c14359627b (patch)
tree52f13f9818fea747a250e395a4975523171ad3a6
parent0890c47cf9ed34dba56b86ad05b27d66321593ec (diff)
Update README.md
Change-Id: I3a7cb848a1b5ab0436f4ca50b097ac5cdeca36b6 Reviewed-by: Karsten Heimrich <karsten.heimrich@qt.io>
-rw-r--r--README.md650
-rw-r--r--res/chronometer.pngbin175701 -> 0 bytes
-rw-r--r--res/hackathon23.pngbin502641 -> 0 bytes
-rw-r--r--res/qt_dotnet.pngbin313884 -> 0 bytes
-rw-r--r--res/wpf_qml_window.pngbin145468 -> 0 bytes
5 files changed, 52 insertions, 598 deletions
diff --git a/README.md b/README.md
index 60cad33..392f0b1 100644
--- a/README.md
+++ b/README.md
@@ -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 &mdash; 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>&#x229E; 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
deleted file mode 100644
index 35036ea..0000000
--- a/res/chronometer.png
+++ /dev/null
Binary files differ
diff --git a/res/hackathon23.png b/res/hackathon23.png
deleted file mode 100644
index e23a856..0000000
--- a/res/hackathon23.png
+++ /dev/null
Binary files differ
diff --git a/res/qt_dotnet.png b/res/qt_dotnet.png
deleted file mode 100644
index 3d4e748..0000000
--- a/res/qt_dotnet.png
+++ /dev/null
Binary files differ
diff --git a/res/wpf_qml_window.png b/res/wpf_qml_window.png
deleted file mode 100644
index e2dc08c..0000000
--- a/res/wpf_qml_window.png
+++ /dev/null
Binary files differ