aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiguel Costa <miguel.costa@qt.io>2023-05-17 12:28:05 +0200
committerMiguel Costa <miguel.costa@qt.io>2023-06-12 11:03:55 +0000
commit19c593fcc505d0127b097e0da880e0d0a9274d5d (patch)
tree9b1885ced2e7d3b386c7b91bdee1661972159f85
parenta1c6a7c1d48120ee477817257fee38f8adbfadb8 (diff)
Add example projects
Chronometer: * QML front-end to a .NET module with a well-defined "business logic". EmbeddedWindow: * WPF window containing an embedded QML view, which runs in-process. QtAzureIoT: * Integrating Qt and .NET in a non-Windows setting. Change-Id: I4dff485b95835a3d880fe03f110c46c457a419b3 Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
-rw-r--r--examples/Chronometer/Chronometer/Chronometer.cs232
-rw-r--r--examples/Chronometer/Chronometer/ChronometerModel.csproj9
-rw-r--r--examples/Chronometer/QmlChronometer/OutDir.props11
-rw-r--r--examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml125
-rw-r--r--examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml65
-rw-r--r--examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml31
-rw-r--r--examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj158
-rw-r--r--examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters156
-rw-r--r--examples/Chronometer/QmlChronometer/content/center.pngbin0 -> 2773 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_1_center.pngbin0 -> 3165 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_1_hand.pngbin0 -> 4973 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_2_center.pngbin0 -> 3168 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_2_hand.pngbin0 -> 4972 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_3_center.pngbin0 -> 3164 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/chrono_3_needle.pngbin0 -> 7268 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/hour_hand.pngbin0 -> 3675 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/minute_hand.pngbin0 -> 4163 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/second_hand.pngbin0 -> 5947 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/content/watchface.pngbin0 -> 93379 bytes
-rw-r--r--examples/Chronometer/QmlChronometer/main.cpp29
-rw-r--r--examples/Chronometer/QmlChronometer/main.qml453
-rw-r--r--examples/Chronometer/QmlChronometer/qchronometer.cpp163
-rw-r--r--examples/Chronometer/QmlChronometer/qchronometer.h84
-rw-r--r--examples/Chronometer/QmlChronometer/qlaprecorder.cpp128
-rw-r--r--examples/Chronometer/QmlChronometer/qlaprecorder.h69
-rw-r--r--examples/Chronometer/QmlChronometer/qml.qrc19
-rw-r--r--examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj111
-rw-r--r--examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters57
-rw-r--r--examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp39
-rw-r--r--examples/EmbeddedWindow/QmlApp/embeddedwindow.h31
-rw-r--r--examples/EmbeddedWindow/QmlApp/main.cpp65
-rw-r--r--examples/EmbeddedWindow/QmlApp/main.qml135
-rw-r--r--examples/EmbeddedWindow/QmlApp/mainwindow.cpp170
-rw-r--r--examples/EmbeddedWindow/QmlApp/mainwindow.h61
-rw-r--r--examples/EmbeddedWindow/QmlApp/qml.qrc6
-rw-r--r--examples/EmbeddedWindow/QmlApp/qt_logo.pngbin0 -> 6208 bytes
-rw-r--r--examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs10
-rw-r--r--examples/EmbeddedWindow/WpfApp/MainWindow.xaml58
-rw-r--r--examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs109
-rw-r--r--examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json11
-rw-r--r--examples/EmbeddedWindow/WpfApp/WpfApp.cs23
-rw-r--r--examples/EmbeddedWindow/WpfApp/WpfApp.csproj33
-rw-r--r--examples/QtAzureIoT/QtAzureIoT.sln88
-rw-r--r--examples/QtAzureIoT/common/PropertySet.cs27
-rw-r--r--examples/QtAzureIoT/common/Utils.csproj9
-rw-r--r--examples/QtAzureIoT/device/CardReader/CardReader.cs98
-rw-r--r--examples/QtAzureIoT/device/CardReader/CardReader.csproj17
-rw-r--r--examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs70
-rw-r--r--examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj17
-rw-r--r--examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs423
-rw-r--r--examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs483
-rw-r--r--examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs57
-rw-r--r--examples/QtAzureIoT/device/SensorData/SensorData.cs107
-rw-r--r--examples/QtAzureIoT/device/SensorData/SensorData.csproj17
-rw-r--r--examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj126
-rw-r--r--examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters41
-rw-r--r--examples/QtAzureIoT/device/deviceapp/main.cpp183
-rw-r--r--examples/QtAzureIoT/device/deviceapp/main.qml56
-rw-r--r--examples/QtAzureIoT/device/deviceapp/qml.qrc5
59 files changed, 4475 insertions, 0 deletions
diff --git a/examples/Chronometer/Chronometer/Chronometer.cs b/examples/Chronometer/Chronometer/Chronometer.cs
new file mode 100644
index 0000000..51db4bb
--- /dev/null
+++ b/examples/Chronometer/Chronometer/Chronometer.cs
@@ -0,0 +1,232 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System.ComponentModel;
+using System.Diagnostics;
+
+namespace WatchModels
+{
+ public interface ILapRecorder
+ {
+ void Mark(int hours, int minutes, int seconds, int milliseconds);
+ }
+
+ public class Chronometer : INotifyPropertyChanged
+ {
+ public double Hours
+ {
+ get => hours;
+ private set => SetProperty(ref hours, value, nameof(Hours));
+ }
+ public double Minutes
+ {
+ get => minutes;
+ private set => SetProperty(ref minutes, value, nameof(Minutes));
+ }
+ public double Seconds
+ {
+ get => seconds;
+ private set => SetProperty(ref seconds, value, nameof(Seconds));
+ }
+
+ public int Day
+ {
+ get => day;
+ private set => SetProperty(ref day, value, nameof(Day));
+ }
+
+ public double ElapsedHours
+ {
+ get => elapsedHours;
+ private set => SetProperty(ref elapsedHours, value, nameof(ElapsedHours));
+ }
+
+ public double ElapsedMinutes
+ {
+ get => elapsedMinutes;
+ private set => SetProperty(ref elapsedMinutes, value, nameof(ElapsedMinutes));
+ }
+
+ public double ElapsedSeconds
+ {
+ get => elapsedSeconds;
+ private set => SetProperty(ref elapsedSeconds, value, nameof(ElapsedSeconds));
+ }
+
+ public double ElapsedMilliseconds
+ {
+ get => elapsedMilliseconds;
+ private set => SetProperty(ref elapsedMilliseconds, value, nameof(ElapsedMilliseconds));
+ }
+
+ public bool Started
+ {
+ get => Stopwatch.IsRunning;
+ private set
+ {
+ if (value == Stopwatch.IsRunning)
+ return;
+ if (value)
+ Stopwatch.Start();
+ else
+ Stopwatch.Stop();
+ NotifyPropertyChanged(nameof(Started));
+ }
+ }
+
+ public bool AdjustDayMode
+ {
+ get => adjustDayMode;
+ set
+ {
+ if (value == adjustDayMode)
+ return;
+ adjustDayMode = value;
+ if (adjustDayMode) {
+ if (adjustTimeMode) {
+ adjustTimeMode = false;
+ NotifyPropertyChanged(nameof(AdjustTimeMode));
+ }
+ Started = false;
+ Reset();
+ Time.Stop();
+ baseTime = baseTime.Add(Time.Elapsed);
+ Time.Reset();
+ } else if (!adjustTimeMode) {
+ Time.Start();
+ }
+ NotifyPropertyChanged(nameof(AdjustDayMode));
+ }
+ }
+
+ public bool AdjustTimeMode
+ {
+ get => adjustTimeMode;
+ set
+ {
+ if (value == adjustTimeMode)
+ return;
+ adjustTimeMode = value;
+ if (adjustTimeMode) {
+ if (adjustDayMode) {
+ adjustDayMode = false;
+ NotifyPropertyChanged(nameof(AdjustDayMode));
+ }
+ Started = false;
+ Reset();
+ Time.Stop();
+ baseTime = baseTime.Add(Time.Elapsed);
+ Time.Reset();
+ } else if (!adjustDayMode) {
+ Time.Start();
+ }
+ NotifyPropertyChanged(nameof(AdjustTimeMode));
+ }
+ }
+
+ public ILapRecorder LapRecorder { get; set; }
+
+ public Chronometer()
+ {
+ Mechanism = new Task(async () => await MechanismLoopAsync(), MechanismLoop.Token);
+ Mechanism.Start();
+ baseTime = DateTime.Now;
+ Time.Start();
+ }
+
+ public void StartStop()
+ {
+ if (AdjustTimeMode || AdjustDayMode)
+ return;
+ Started = !Started;
+ }
+
+ public void Reset()
+ {
+ ElapsedHours = ElapsedMinutes = ElapsedSeconds = ElapsedMilliseconds = 0;
+ if (!Stopwatch.IsRunning) {
+ Stopwatch.Reset();
+ } else {
+ LapRecorder?.Mark(
+ Stopwatch.Elapsed.Hours,
+ Stopwatch.Elapsed.Minutes,
+ Stopwatch.Elapsed.Seconds,
+ Stopwatch.Elapsed.Milliseconds);
+ Stopwatch.Restart();
+ }
+ }
+
+ public void Adjust(int delta)
+ {
+ if (AdjustDayMode)
+ baseTime = baseTime.AddDays(delta);
+ else if (AdjustTimeMode)
+ baseTime = baseTime.AddSeconds(delta * 60);
+ Refresh();
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ private void NotifyPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void Refresh()
+ {
+ DateTime now = baseTime.Add(Time.Elapsed);
+ Day = now.Day;
+
+ TimeSpan time = now.TimeOfDay;
+ Hours = time.TotalHours;
+ Minutes = time.TotalMinutes
+ - (time.Hours * 60);
+ Seconds = time.TotalSeconds
+ - (time.Hours * 3600) - (time.Minutes * 60);
+
+ TimeSpan elapsed = Stopwatch.Elapsed;
+ ElapsedHours = elapsed.TotalHours;
+ ElapsedMinutes = elapsed.TotalMinutes
+ - (elapsed.Hours * 60);
+ ElapsedSeconds = elapsed.TotalSeconds
+ - (elapsed.Hours * 3600) - (elapsed.Minutes * 60);
+ ElapsedMilliseconds = elapsed.TotalMilliseconds
+ - (elapsed.Hours * 3600000) - (elapsed.Minutes * 60000) - (elapsed.Seconds * 1000);
+ }
+
+ private async Task MechanismLoopAsync()
+ {
+ while (!MechanismLoop.IsCancellationRequested) {
+ await Task.Delay(5);
+ Refresh();
+ }
+ }
+
+ private void SetProperty<T>(ref T currentValue, T newValue, string name)
+ {
+ if (newValue.Equals(currentValue))
+ return;
+ currentValue = newValue;
+ NotifyPropertyChanged(name);
+ }
+
+ private double hours;
+ private double minutes;
+ private double seconds;
+ private int day;
+ private double elapsedHours;
+ private double elapsedMinutes;
+ private double elapsedSeconds;
+ private double elapsedMilliseconds;
+ private bool adjustDayMode;
+ private bool adjustTimeMode;
+
+ private DateTime baseTime;
+ private Stopwatch Time { get; } = new();
+ private Stopwatch Stopwatch { get; } = new();
+
+ private CancellationTokenSource MechanismLoop { get; } = new();
+ private Task Mechanism { get; }
+ }
+}
diff --git a/examples/Chronometer/Chronometer/ChronometerModel.csproj b/examples/Chronometer/Chronometer/ChronometerModel.csproj
new file mode 100644
index 0000000..141e38f
--- /dev/null
+++ b/examples/Chronometer/Chronometer/ChronometerModel.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+</Project>
diff --git a/examples/Chronometer/QmlChronometer/OutDir.props b/examples/Chronometer/QmlChronometer/OutDir.props
new file mode 100644
index 0000000..c7a9463
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/OutDir.props
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ImportGroup Label="PropertySheets" />
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup>
+ <OutDir>bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</OutDir>
+ <IntDir>obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</IntDir>
+ </PropertyGroup>
+ <ItemDefinitionGroup />
+ <ItemGroup />
+</Project> \ No newline at end of file
diff --git a/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml b/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml
new file mode 100644
index 0000000..0c5d904
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml
@@ -0,0 +1,125 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+
+//////////////////////////////////////////////////////////////////////
+// Adjustment wheel
+Tumbler {
+ id: adjustmentWheel
+ model: 100
+ property int startX
+ property int startY
+
+ x: startX + ((chrono.adjustDayMode || chrono.adjustTimeMode) ? 8 : 0)
+ Behavior on x {
+ SpringAnimation { spring: 3; damping: 0.5 }
+ }
+ y: startY; width: 25; height: 70
+
+ enabled: chrono.adjustDayMode || chrono.adjustTimeMode
+ onEnabledChanged: {
+ if (enabled) {
+ chrono.reset();
+ if (!chrono.started) {
+ laps.reset();
+ showLap = currentLap;
+ }
+ }
+ }
+
+ property int lastIndex: 0
+ property var lastTime: Date.UTC(0)
+ property double turnSpeed: 0.0
+ onCurrentIndexChanged: {
+ if (currentIndex != lastIndex) {
+ var i1 = currentIndex;
+ var i0 = lastIndex;
+ if (Math.abs(i1 - i0) > 50) {
+ if (i1 < i0)
+ i1 += 100;
+ else
+ i0 += 100;
+ }
+ var deltaX = i1 - i0;
+ chrono.adjust(deltaX);
+ lastIndex = currentIndex;
+
+ var deltaT = Date.now() - lastTime;
+ lastTime += deltaT;
+ turnSpeed = Math.abs((deltaX * 1000) / deltaT);
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onWheel: function(wheel) {
+ turn(wheel.angleDelta.y > 0 ? 1 : -1);
+ }
+ }
+
+ function turn(delta) {
+ if (enabled) {
+ adjustmentWheel.currentIndex = (100 + adjustmentWheel.currentIndex + (delta)) % 100;
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Wheel surface
+ background: Rectangle {
+ anchors.fill: adjustmentWheel
+ color: gray6
+ border.color: gray8
+ border.width: 2
+ radius: 2
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Notches
+ delegate: Component {
+ Item {
+ Rectangle {
+ x: 4; y: 0; width: Tumbler.tumbler.width - 8; height: 2
+ color: gray3
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Wheel shadow
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width; height: parent.height
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: gray3 }
+ GradientStop { position: 0.3; color: "transparent" }
+ GradientStop { position: 0.5; color: "transparent" }
+ GradientStop { position: 0.7; color: "transparent" }
+ GradientStop { position: 1.0; color: gray3 }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Wheel axis
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: parent.startX - parent.x + 2
+ width: 20; height: 20
+ z: -1
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: gray3 }
+ GradientStop { position: 0.3; color: gray6 }
+ GradientStop { position: 0.5; color: gray6 }
+ GradientStop { position: 0.7; color: gray6 }
+ GradientStop { position: 1.0; color: gray3 }
+ }
+ border.color: "transparent"
+ }
+}
diff --git a/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml b/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml
new file mode 100644
index 0000000..7c07106
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml
@@ -0,0 +1,65 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+
+//////////////////////////////////////////////////////////////////
+// Inset dial
+Item {
+ id: insetDial
+ property string handSource
+ property string pinSource
+ property int centerX
+ property int centerY
+ property double rotationAngle
+ //////////////////////////////////////////////////////////////////
+ // Hand
+ Image {
+ source: insetDial.handSource
+ transform: Rotation {
+ origin.x: insetDial.centerX; origin.y: insetDial.centerY
+ Behavior on angle {
+ SpringAnimation { spring: 3; damping: 0.5; modulus: 360 }
+ }
+ angle: insetDial.rotationAngle
+ }
+ }
+ //////////////////////////////////////////////////////////////////
+ // Highlight
+ Shape {
+ id: insetDialHighlight
+ anchors.fill: insetDial
+ opacity: 0.5
+ property var centerX: insetDial.centerX
+ property var centerY: insetDial.centerY
+ property var color:
+ showLap == lastLap ? blue86 : showLap == bestLap ? green86 : "transparent"
+ ShapePath {
+ startX: insetDialHighlight.centerX; startY: insetDialHighlight.centerY
+ strokeColor: "transparent"
+ PathAngleArc {
+ centerX: insetDialHighlight.centerX; centerY: insetDialHighlight.centerY
+ radiusX: 55; radiusY: 55; startAngle: 0; sweepAngle: 360
+ }
+ fillGradient: RadialGradient {
+ centerX: insetDialHighlight.centerX; centerY: insetDialHighlight.centerY
+ centerRadius: 55;
+ focalX: centerX; focalY: centerY
+ GradientStop { position: 0; color: "transparent" }
+ GradientStop { position: 0.6; color: "transparent" }
+ GradientStop { position: 1; color: insetDialHighlight.color }
+ }
+ }
+ }
+ //////////////////////////////////////////////////////////////////
+ // Center pin
+ Image {
+ source: insetDial.pinSource
+ }
+}
diff --git a/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml b/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml
new file mode 100644
index 0000000..6894c69
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml
@@ -0,0 +1,31 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+
+RoundButton {
+ id: watchButton
+ property string buttonText
+ property bool split
+ property string color
+ width: 70; height: 70; radius: 25
+ palette.button: color
+ Text {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ font.bold: true
+ text: watchButton.buttonText
+ }
+ Rectangle {
+ visible: watchButton.split
+ color: "black"
+ width: 55; height: 1
+ anchors.centerIn: parent
+ }
+}
diff --git a/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj
new file mode 100644
index 0000000..8fb3def
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>{5A1E5424-CDB1-4776-A9CC-01B2CD57F516}</ProjectGuid>
+ <Keyword>QtVS_v304</Keyword>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
+ <Import Project="$(QtMsBuild)\qt_defaults.props" />
+ </ImportGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
+ <QtInstall>$(DefaultQtVersion)</QtInstall>
+ <QtModules>quick;core</QtModules>
+ <QtBuildConfig>debug</QtBuildConfig>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
+ <QtInstall>$(DefaultQtVersion)</QtInstall>
+ <QtModules>quick;core</QtModules>
+ <QtBuildConfig>release</QtBuildConfig>
+ </PropertyGroup>
+ <Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
+ <Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
+ </Target>
+ <ImportGroup Label="ExtensionSettings" />
+ <ImportGroup Label="Shared" />
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="OutDir.props" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="OutDir.props" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <IncludePath>..\..\..\include;$(IncludePath)</IncludePath>
+ <OutDir>bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</OutDir>
+ <IntDir>obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</IntDir>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <IncludePath>..\..\..\include;$(IncludePath)</IncludePath>
+ <OutDir>bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</OutDir>
+ <IntDir>obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\</IntDir>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+ <Optimization>Disabled</Optimization>
+ <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>None</DebugInformationFormat>
+ <Optimization>MaxSpeed</Optimization>
+ <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>false</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <ClCompile Include="main.cpp" />
+ <ClCompile Include="qchronometer.cpp" />
+ <ClCompile Include="qlaprecorder.cpp" />
+ <None Include="QChronometer\AdjustmentWheel.qml" />
+ <None Include="QChronometer\InsetDial.qml" />
+ <None Include="QChronometer\WatchButton.qml" />
+ <QtRcc Include="qml.qrc" />
+ <CopyFileToFolders Include="..\..\..\bin\Qt.DotNet.Adapter.dll">
+ <DeploymentContent>true</DeploymentContent>
+ <FileType>Document</FileType>
+ <TreatOutputAsContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatOutputAsContent>
+ </CopyFileToFolders>
+ <None Include="main.qml" />
+ </ItemGroup>
+ <ItemGroup>
+ <QtMoc Include="qchronometer.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\Chronometer\ChronometerModel.csproj">
+ <Project>{d3d5ed0e-fd65-4f54-b2d8-d3a380f247a2}</Project>
+ </ProjectReference>
+ <ProjectReference
+ Condition="Exists('..\..\..\src\Qt.DotNet.Adapter\Qt.DotNet.Adapter.csproj')"
+ Include="..\..\..\src\Qt.DotNet.Adapter\Qt.DotNet.Adapter.csproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <ClInclude Include="..\..\..\include\qdotnetadapter.h" />
+ <ClInclude Include="..\..\..\include\qdotnetarray.h" />
+ <ClInclude Include="..\..\..\include\qdotnetcallback.h" />
+ <ClInclude Include="..\..\..\include\qdotnetevent.h" />
+ <ClInclude Include="..\..\..\include\qdotnetexception.h" />
+ <ClInclude Include="..\..\..\include\qdotnetfunction.h" />
+ <ClInclude Include="..\..\..\include\qdotnethost.h" />
+ <ClInclude Include="..\..\..\include\qdotnethostfxr.h" />
+ <ClInclude Include="..\..\..\include\qdotnetinterface.h" />
+ <ClInclude Include="..\..\..\include\qdotnetmarshal.h" />
+ <ClInclude Include="..\..\..\include\qdotnetobject.h" />
+ <ClInclude Include="..\..\..\include\qdotnetparameter.h" />
+ <ClInclude Include="..\..\..\include\qdotnetref.h" />
+ <ClInclude Include="..\..\..\include\qdotnetsafemethod.h" />
+ <ClInclude Include="..\..\..\include\qdotnettype.h" />
+ <QtMoc Include="qlaprecorder.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <Image Include="content\center.png" />
+ <Image Include="content\chrono_1_center.png" />
+ <Image Include="content\chrono_1_hand.png" />
+ <Image Include="content\chrono_2_center.png" />
+ <Image Include="content\chrono_2_hand.png" />
+ <Image Include="content\chrono_3_center.png" />
+ <Image Include="content\chrono_3_needle.png" />
+ <Image Include="content\hour_hand.png" />
+ <Image Include="content\minute_hand.png" />
+ <Image Include="content\second_hand.png" />
+ <Image Include="content\watchface.png" />
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
+ <Import Project="$(QtMsBuild)\qt.targets" />
+ </ImportGroup>
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters
new file mode 100644
index 0000000..30ca20d
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <Filter Include="Source Files">
+ <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+ <Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+ </Filter>
+ <Filter Include="Header Files">
+ <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+ <Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
+ </Filter>
+ <Filter Include="Resource Files">
+ <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+ <Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+ </Filter>
+ <Filter Include="Form Files">
+ <UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
+ <Extensions>ui</Extensions>
+ </Filter>
+ <Filter Include="Translation Files">
+ <UniqueIdentifier>{639EADAA-A684-42e4-A9AD-28FC9BCB8F7C}</UniqueIdentifier>
+ <Extensions>ts</Extensions>
+ </Filter>
+ <Filter Include="Header Files\qtdotnet">
+ <UniqueIdentifier>{a17873af-389e-48bb-9b3d-0d6fcd150a7f}</UniqueIdentifier>
+ </Filter>
+ <Filter Include="Resource Files\png">
+ <UniqueIdentifier>{1f892826-c7e1-49be-b4eb-0007d1308298}</UniqueIdentifier>
+ </Filter>
+ <Filter Include="Source Files\qml">
+ <UniqueIdentifier>{9f5c583a-a421-4bcf-9f22-59ef59da1b60}</UniqueIdentifier>
+ <Extensions>.qml</Extensions>
+ </Filter>
+ </ItemGroup>
+ <ItemGroup>
+ <ClCompile Include="main.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <QtRcc Include="qml.qrc">
+ <Filter>Resource Files</Filter>
+ </QtRcc>
+ <ClCompile Include="qchronometer.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="qlaprecorder.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ </ItemGroup>
+ <ItemGroup>
+ <QtMoc Include="qchronometer.h">
+ <Filter>Header Files</Filter>
+ </QtMoc>
+ <QtMoc Include="qlaprecorder.h">
+ <Filter>Header Files</Filter>
+ </QtMoc>
+ </ItemGroup>
+ <ItemGroup>
+ <CopyFileToFolders Include="..\..\..\bin\Qt.DotNet.Adapter.dll" />
+ </ItemGroup>
+ <ItemGroup>
+ <ClInclude Include="..\..\..\include\qdotnetadapter.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetcallback.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetevent.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetfunction.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnethost.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnethostfxr.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetinterface.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetmarshal.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetobject.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetparameter.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetref.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnettype.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetarray.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetexception.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\..\include\qdotnetsafemethod.h">
+ <Filter>Header Files\qtdotnet</Filter>
+ </ClInclude>
+ </ItemGroup>
+ <ItemGroup>
+ <Image Include="content\center.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_1_center.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_1_hand.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_2_center.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_2_hand.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_3_center.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\chrono_3_needle.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\hour_hand.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\minute_hand.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\second_hand.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ <Image Include="content\watchface.png">
+ <Filter>Resource Files\png</Filter>
+ </Image>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="QChronometer\AdjustmentWheel.qml">
+ <Filter>Source Files\qml</Filter>
+ </None>
+ <None Include="QChronometer\InsetDial.qml">
+ <Filter>Source Files\qml</Filter>
+ </None>
+ <None Include="QChronometer\WatchButton.qml">
+ <Filter>Source Files\qml</Filter>
+ </None>
+ <None Include="main.qml">
+ <Filter>Source Files</Filter>
+ </None>
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/examples/Chronometer/QmlChronometer/content/center.png b/examples/Chronometer/QmlChronometer/content/center.png
new file mode 100644
index 0000000..a6c610f
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/center.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_1_center.png b/examples/Chronometer/QmlChronometer/content/chrono_1_center.png
new file mode 100644
index 0000000..eb57973
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_1_center.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png b/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png
new file mode 100644
index 0000000..d507b5e
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_2_center.png b/examples/Chronometer/QmlChronometer/content/chrono_2_center.png
new file mode 100644
index 0000000..1552403
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_2_center.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png b/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png
new file mode 100644
index 0000000..390b0e4
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_3_center.png b/examples/Chronometer/QmlChronometer/content/chrono_3_center.png
new file mode 100644
index 0000000..6dbfb64
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_3_center.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png b/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png
new file mode 100644
index 0000000..6499d0a
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/hour_hand.png b/examples/Chronometer/QmlChronometer/content/hour_hand.png
new file mode 100644
index 0000000..2bd7ad7
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/hour_hand.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/minute_hand.png b/examples/Chronometer/QmlChronometer/content/minute_hand.png
new file mode 100644
index 0000000..ee352fe
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/minute_hand.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/second_hand.png b/examples/Chronometer/QmlChronometer/content/second_hand.png
new file mode 100644
index 0000000..33f4504
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/second_hand.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/content/watchface.png b/examples/Chronometer/QmlChronometer/content/watchface.png
new file mode 100644
index 0000000..e336d5e
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/content/watchface.png
Binary files differ
diff --git a/examples/Chronometer/QmlChronometer/main.cpp b/examples/Chronometer/QmlChronometer/main.cpp
new file mode 100644
index 0000000..94f8f45
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/main.cpp
@@ -0,0 +1,29 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include "qchronometer.h"
+#include "qlaprecorder.h"
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+ QQmlApplicationEngine engine;
+
+ QLapRecorder lapRecorder;
+ engine.rootContext()->setContextProperty("laps", &lapRecorder);
+
+ QChronometer chrono(lapRecorder);
+ engine.rootContext()->setContextProperty("chrono", &chrono);
+
+ engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+ if (engine.rootObjects().isEmpty())
+ return -1;
+
+ return app.exec();
+}
diff --git a/examples/Chronometer/QmlChronometer/main.qml b/examples/Chronometer/QmlChronometer/main.qml
new file mode 100644
index 0000000..059c8ae
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/main.qml
@@ -0,0 +1,453 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+
+import "QChronometer"
+
+Window {
+ visible: true
+ property int windowWidth: 544 + (showHelp.checked ? 265 : 0)
+ Behavior on windowWidth { SmoothedAnimation { duration: 750 } }
+ width: windowWidth; minimumWidth: windowWidth; maximumWidth: windowWidth
+ height: 500; minimumHeight: height; maximumHeight: height
+ title: "QML Chronometer"
+
+ //////////////////////////////////////////////////////////////////
+ // Colors
+ readonly property var gray1: "#111111"
+ readonly property var gray3: "#333333"
+ readonly property var gray4: "#444444"
+ readonly property var gray6: "#666666"
+ readonly property var redF6: "#FF6666"
+ readonly property var green86: "#668866"
+ readonly property var blue86: "#666688"
+ readonly property var gray8: "#888888"
+
+ //////////////////////////////////////////////////////////////////
+ // Window background color
+ color: gray4
+
+ //////////////////////////////////////////////////////////////////
+ // Stopwatch mode
+ property int showLap: currentLap
+ // 0: showing current lap, or stopwatch not running
+ readonly property int currentLap: 0
+ // 1: showing last recorded lap
+ readonly property int lastLap: 1
+ // 2: showing best recorded lap
+ readonly property int bestLap: 2
+
+ //////////////////////////////////////////////////////////////////
+ // Watch
+ Image {
+ id: watch
+ source: "watchface.png"
+ }
+
+ //////////////////////////////////////////////////////////////////
+ // Rim
+ Rectangle {
+ color: "transparent"
+ border { color: gray8; width: 3 }
+ anchors.centerIn: watch
+ width: watch.width; height: watch.height; radius: watch.width / 2
+ }
+
+ //////////////////////////////////////////////////////////////////
+ // Calendar
+ Text {
+ enabled: false
+ x: 345; y: 295; width: 32; height: 22
+ transform: Rotation { origin.x: 0; origin.y: 0; angle: 30 }
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font { bold: true; pointSize: 11 }
+ text: chrono.day
+ }
+
+ //////////////////////////////////////////////////////////////////
+ // Inset dial #1 (above-left of center; 30x minutes)
+ InsetDial {
+ id: insetDial1
+ handSource: "/chrono_1_hand.png"
+ pinSource: "/chrono_1_center.png"
+ centerX: 176; centerY: 208
+ rotationAngle:
+ (showLap == lastLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show minutes of previous lap
+ (laps.lastMinutes % 30) * 12
+ ) : (showLap == bestLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show minutes of best lap
+ (laps.bestMinutes % 30) * 12
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // Show minutes of current lap
+ (chrono.elapsedMinutes % 30) * 12
+ )
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // Inset chrono counter #2 (above-right of center; 10x 1/10 second or 10x hours)
+ InsetDial {
+ id: insetDial2
+ handSource: "/chrono_2_hand.png"
+ pinSource: "/chrono_2_center.png"
+ centerX: 325; centerY: 208
+ rotationAngle:
+ (showLap == lastLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show previous lap
+ (laps.lastHours == 0 && laps.lastMinutes < 30) ? (
+ /////////////////////////////////////////////////////////////
+ // 1/10 seconds
+ laps.lastMilliseconds * 360 / 1000
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // hours
+ ((laps.lastHours % 10) + (laps.lastMinutes / 60)) * 360 / 10
+ )
+ ) : (showLap == bestLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show best lap
+ (laps.bestHours == 0 && laps.bestMinutes < 30) ? (
+ /////////////////////////////////////////////////////////////
+ // 1/10 seconds
+ laps.bestMilliseconds * 360 / 1000
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // hours
+ ((laps.bestHours % 10) + (laps.bestMinutes / 60)) * 360 / 10
+ )
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // Show current lap
+ (chrono.elapsedHours < 1 && chrono.elapsedMinutes < 30) ? (
+ /////////////////////////////////////////////////////////////
+ // 1/10 seconds
+ chrono.elapsedMilliseconds * 360 / 1000
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // hours
+ (chrono.elapsedHours % 10) * 360 / 10
+ )
+ )
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Inset chrono counter #3 (below center; 60x seconds)
+ InsetDial {
+ id: insetDial3
+ handSource: "/chrono_3_needle.png"
+ pinSource: "/chrono_3_center.png"
+ centerX: 250; centerY: 336
+ rotationAngle: 150 + (
+ (showLap == lastLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show seconds of previous lap
+ laps.lastSeconds * 6
+ ) : (showLap == bestLap) ? (
+ /////////////////////////////////////////////////////////////
+ // Show seconds of best lap
+ laps.bestSeconds * 6
+ ) : (
+ /////////////////////////////////////////////////////////////
+ // Show seconds of current (wall-clock) time
+ chrono.seconds * 6
+ ))
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Hours hand for current (wall-clock) time
+ Image {
+ id: hoursHand;
+ source: "hour_hand.png"
+ transform: Rotation {
+ origin.x: 249; origin.y: 251
+ angle: 110 + (chrono.hours % 12) * 30
+ Behavior on angle {
+ enabled: adjustmentWheel.turnSpeed < 75
+ SpringAnimation { spring: 3; damping: 0.5; modulus: 360 }
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Minutes hand for current (wall-clock) time
+ Image {
+ id: minutesHand;
+ source: "minute_hand.png"
+ transform: Rotation {
+ origin.x: 249; origin.y: 251
+ angle: -108 + chrono.minutes * 6
+ Behavior on angle {
+ enabled: adjustmentWheel.turnSpeed < 75
+ SpringAnimation { spring: 3; damping: 0.5; modulus: 360 }
+ }
+ }
+ }
+ Image {
+ source: "center.png"
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Stopwatch seconds hand
+ 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 }
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Adjustment wheel
+ AdjustmentWheel {
+ id: adjustmentWheel
+ startX: 498; startY: 215
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Adjust date
+ Switch {
+ id: adjustDay
+ x: 500; y: 290; padding: 0; spacing: -35
+ palette.button: gray8; font { bold: true; pointSize: 9 }
+ text: "Date"
+ checked: chrono.adjustDayMode
+ onToggled: chrono.adjustDayMode = (position == 1)
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Adjust time
+ Switch {
+ id: adjustTime
+ x: 500; y: 310; padding: 0; spacing: -35
+ palette.button: gray8; font { bold: true; pointSize: 9 }
+ text: "Time"
+ checked: chrono.adjustTimeMode
+ onToggled: chrono.adjustTimeMode = (position == 1)
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Stopwatch start/stop button
+ WatchButton {
+ id: buttonStartStop
+ x: 425; y: 5
+ buttonText: "Start\n\nStop"; split: true
+ color: chrono.started ? redF6 : !enabled ? gray3 : gray6
+ enabled: !chrono.adjustDayMode && !chrono.adjustTimeMode
+ onClicked: chrono.startStop()
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Stopwatch lap/reset button
+ WatchButton {
+ id: buttonLapReset
+ x: 425; y: 425
+ buttonText: "Lap\n\nReset"; split: true
+ color: !enabled ? gray3 : gray6
+ enabled: chrono.started
+ || chrono.elapsedHours > 0
+ || chrono.elapsedMinutes > 0
+ || chrono.elapsedSeconds > 0
+ || chrono.elapsedMilliseconds > 0
+ || laps.lapCount > 0
+ onClicked: {
+ chrono.reset();
+ if (!chrono.started) {
+ laps.reset();
+ showLap = currentLap;
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Stopwatch last lap button
+ WatchButton {
+ id: buttonLastLap
+ x: 5; y: 425
+ buttonText: "Last\nLap"; split: false
+ color: (showLap == lastLap) ? blue86 : !enabled ? gray3 : gray6
+ enabled: laps.lapCount > 0
+ onClicked: {
+ showLapTimer.stop();
+ if (laps.lapCount > 0) {
+ showLap = (showLap != lastLap) ? lastLap : currentLap;
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Stopwatch best lap button
+ WatchButton {
+ id: buttonBestLap
+ x: 5; y: 5
+ buttonText: "Best\nLap"; split: false
+ color: (showLap == bestLap) ? green86 : !enabled ? gray3 : gray6
+ enabled: laps.lapCount > 1
+ onClicked: {
+ showLapTimer.stop();
+ if (laps.lapCount > 1) {
+ showLap = (showLap != bestLap) ? bestLap : currentLap;
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Timer to show last/best lap for 5 secs. after mark
+ Timer {
+ id: showLapTimer
+ interval: 5000
+ running: false
+ repeat: false
+ onTriggered: showLap = currentLap
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Lap events
+ Connections {
+ target: laps
+
+ //////////////////////////////////////////////////////////////////////
+ // Lap counter changed: new lap recorded, or lap counter reset
+ function onLapCountChanged() {
+ if (laps.lapCount > 0) {
+ showLap = lastLap;
+ showLapTimer.restart()
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // New best lap recorded
+ function onNewBestLap() {
+ if (laps.lapCount > 1) {
+ showLap = bestLap;
+ showLapTimer.restart()
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Keyboard events
+ Shortcut {
+ sequence: " "
+ onActivated: buttonStartStop.clicked()
+ }
+ Shortcut {
+ sequences: [ "Return", "Enter" ]
+ onActivated: {
+ if (chrono.started)
+ buttonLapReset.clicked();
+ }
+ }
+ Shortcut {
+ sequence: "Escape"
+ onActivated: {
+ if (chrono.adjustDayMode || chrono.adjustTimeMode)
+ chrono.adjustDayMode = chrono.adjustTimeMode = false;
+ else if (!chrono.started)
+ buttonLapReset.clicked()
+ else
+ showLap = currentLap;
+ }
+ }
+ Shortcut {
+ sequence: "Tab"
+ onActivated: buttonLastLap.clicked()
+ }
+ Shortcut {
+ sequence: "Shift+Tab"
+ onActivated: buttonBestLap.clicked()
+ }
+ Shortcut {
+ sequence: "Ctrl+D"
+ onActivated: {
+ adjustDay.toggle();
+ adjustDay.onToggled();
+ }
+ }
+ Shortcut {
+ sequence: "Ctrl+T"
+ onActivated: {
+ adjustTime.toggle();
+ adjustTime.onToggled();
+ }
+ }
+ Shortcut {
+ sequence: "Up"
+ onActivated: adjustmentWheel.turn(1)
+ }
+ Shortcut {
+ sequence: "Down"
+ onActivated: adjustmentWheel.turn(-1)
+ }
+ Shortcut {
+ sequence: "F1"
+ onActivated: showHelp.toggle()
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // Usage instructions
+ RoundButton {
+ id: showHelp
+ checkable: true
+ x: 524; y: 0; width: 20; height: 40
+ palette.button: gray6
+ radius: 0
+ contentItem: Text {
+ font.bold: true
+ font.pointSize: 11
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: parent.checked ? "<" : "?"
+ }
+ }
+ Rectangle {
+ x: 544; y:0
+ width: 265
+ height: 500
+ color: gray6
+ border.width: 0
+ Text {
+ anchors.fill: parent
+ anchors.topMargin: 10
+ anchors.leftMargin: 5
+ anchors.rightMargin: 5
+ textFormat: Text.MarkdownText
+ wrapMode: Text.WordWrap
+ color: gray1
+ text: "### Usage instructions
+
+The **hours and minutes hands** show the current (wall-clock) time. The **seconds hand** shows the
+stopwatch elapsed seconds. **Inset dial #1** (above-left of center) shows elapsed minutes of
+current/last/best lap. **Inset dial #2** (above-right of center) shows a 1/10th-second counter of
+current/last/best lap. **Inset dial #3** (below center) shows seconds of current time, or elapsed
+seconds of last/best lap.
+
+Press **Start|Stop** (shortcut: **[Space]**) to begin timing. While the stopwatch is running, press
+**Lap|Reset** (shortcut: **[Enter]**) to record a lap. The stopwatch can memorize the last and the
+best lap. Press **Last**&nbsp;**Lap** (shortcut: **[Tab]**) or **Best**&nbsp;**Lap** (shortcut:
+**[Shift+Tab]**) to view the recorded time of the last or best lap. Press **Start|Stop** (shortcut:
+**[Space]**) again to stop timing. Press **Lap|Reset** (shortcut: **[Esc]**) to reset stopwatch
+counters and clear lap memory.
+
+Press the **Date** switch (shortcut: **[Ctrl+D]**) or **Time** switch (shortcut: **[Ctrl+T]**) to
+enter adjustment mode. Turn the **adjustment wheel** (shortcut: **mouse wheel** or **[Up]** /
+**[Down]**) to set the desired date or time. Press the active adjustment switch (or **[Esc]**) to
+leave adjustment mode. Note: entering adjustment mode resets the stopwatch."
+ }
+ }
+}
diff --git a/examples/Chronometer/QmlChronometer/qchronometer.cpp b/examples/Chronometer/QmlChronometer/qchronometer.cpp
new file mode 100644
index 0000000..16951a0
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/qchronometer.cpp
@@ -0,0 +1,163 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include "qchronometer.h"
+#include "qlaprecorder.h"
+
+#include <qdotnetevent.h>
+
+struct QChronometerPrivate : QDotNetObject::IEventHandler
+{
+ QChronometerPrivate(QChronometer *q)
+ :q(q)
+ {}
+
+ void handleEvent(const QString &eventName, QDotNetObject &sender, QDotNetObject &args) override
+ {
+ if (eventName != "PropertyChanged")
+ return;
+
+ if (args.type().fullName() != QDotNetPropertyEvent::FullyQualifiedTypeName)
+ return;
+
+ const auto propertyChangedEvent = args.cast<QDotNetPropertyEvent>();
+ if (propertyChangedEvent.propertyName() == "Hours")
+ emit q->hoursChanged();
+ else if (propertyChangedEvent.propertyName() == "Minutes")
+ emit q->minutesChanged();
+ else if (propertyChangedEvent.propertyName() == "Seconds")
+ emit q->secondsChanged();
+ else if (propertyChangedEvent.propertyName() == "Day")
+ emit q->dayChanged();
+ else if (propertyChangedEvent.propertyName() == "Started")
+ emit q->startedChanged();
+ else if (propertyChangedEvent.propertyName() == "ElapsedHours")
+ emit q->elapsedHoursChanged();
+ else if (propertyChangedEvent.propertyName() == "ElapsedMinutes")
+ emit q->elapsedMinutesChanged();
+ else if (propertyChangedEvent.propertyName() == "ElapsedSeconds")
+ emit q->elapsedSecondsChanged();
+ else if (propertyChangedEvent.propertyName() == "ElapsedMilliseconds")
+ emit q->elapsedMillisecondsChanged();
+ else if (propertyChangedEvent.propertyName() == "AdjustDayMode")
+ emit q->adjustDayModeChanged();
+ else if (propertyChangedEvent.propertyName() == "AdjustTimeMode")
+ emit q->adjustTimeModeChanged();
+ }
+
+ QChronometer *q = nullptr;
+
+ QDotNetFunction<double> hours= nullptr;
+ QDotNetFunction<double> minutes = nullptr;
+ QDotNetFunction<double> seconds = nullptr;
+ QDotNetFunction<int> day = nullptr;
+ QDotNetFunction<double> elapsedHours = nullptr;
+ QDotNetFunction<double> elapsedMinutes = nullptr;
+ QDotNetFunction<double> elapsedSeconds = nullptr;
+ QDotNetFunction<double> elapsedMilliseconds = nullptr;
+ QDotNetFunction<bool> started, adjustDayMode, adjustTimeMode = nullptr;
+ QDotNetFunction<void> startStop, reset, breakWatch = nullptr;
+ QDotNetFunction<void, bool> setAdjustDayMode, setAdjustTimeMode = nullptr;
+ QDotNetFunction<void, int> adjust = nullptr;
+};
+
+
+Q_DOTNET_OBJECT_IMPL(QChronometer, Q_DOTNET_OBJECT_INIT(d(new QChronometerPrivate(this))));
+
+
+QChronometer::QChronometer(const ILapRecorder &lapRecorder)
+ : d(new QChronometerPrivate(this))
+{
+ *this = constructor<QChronometer>().invoke(nullptr);
+ method<void, ILapRecorder>("set_LapRecorder").invoke(*this, lapRecorder);
+ subscribeEvent("PropertyChanged", d);
+}
+
+QChronometer::~QChronometer()
+{
+ if (isValid())
+ unsubscribeEvent("PropertyChanged", d);
+ delete d;
+}
+
+double QChronometer::hours() const
+{
+ return method("get_Hours", d->hours).invoke(*this);
+}
+
+double QChronometer::minutes() const
+{
+ return method("get_Minutes", d->minutes).invoke(*this);
+}
+
+double QChronometer::seconds() const
+{
+ return method("get_Seconds", d->seconds).invoke(*this);
+}
+
+int QChronometer::day() const
+{
+ return method("get_Day", d->day).invoke(*this);
+}
+
+bool QChronometer::started() const
+{
+ return method("get_Started", d->started).invoke(*this);
+}
+
+double QChronometer::elapsedHours() const
+{
+ return method("get_ElapsedHours", d->elapsedHours).invoke(*this);
+}
+
+double QChronometer::elapsedMinutes() const
+{
+ return method("get_ElapsedMinutes", d->elapsedMinutes).invoke(*this);
+}
+
+double QChronometer::elapsedSeconds() const
+{
+ return method("get_ElapsedSeconds", d->elapsedSeconds).invoke(*this);
+}
+
+double QChronometer::elapsedMilliseconds() const
+{
+ return method("get_ElapsedMilliseconds", d->elapsedMilliseconds).invoke(*this);
+}
+
+bool QChronometer::adjustDayMode() const
+{
+ return method("get_AdjustDayMode", d->adjustDayMode).invoke(*this);
+}
+
+void QChronometer::setAdjustDayMode(bool value)
+{
+ method("set_AdjustDayMode", d->setAdjustDayMode).invoke(*this, value);
+}
+
+bool QChronometer::adjustTimeMode() const
+{
+ return method("get_AdjustTimeMode", d->adjustTimeMode).invoke(*this);
+}
+
+void QChronometer::setAdjustTimeMode(bool value)
+{
+ method("set_AdjustTimeMode", d->setAdjustTimeMode).invoke(*this, value);
+}
+
+void QChronometer::adjust(int delta)
+{
+ method("Adjust", d->adjust).invoke(*this, delta);
+}
+
+void QChronometer::startStop()
+{
+ method("StartStop", d->startStop).invoke(*this);
+}
+
+void QChronometer::reset()
+{
+ method("Reset", d->reset).invoke(*this);
+}
diff --git a/examples/Chronometer/QmlChronometer/qchronometer.h b/examples/Chronometer/QmlChronometer/qchronometer.h
new file mode 100644
index 0000000..b90c0e5
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/qchronometer.h
@@ -0,0 +1,84 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#pragma once
+
+#include <qdotnetinterface.h>
+#include <qdotnetobject.h>
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wconversion"
+#endif
+#include <QObject>
+#include <QString>
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
+struct ILapRecorder;
+struct QChronometerPrivate;
+
+class QChronometer : public QObject, public QDotNetObject
+{
+ Q_OBJECT
+ Q_PROPERTY(double hours READ hours NOTIFY hoursChanged)
+ Q_PROPERTY(double minutes READ minutes NOTIFY minutesChanged)
+ Q_PROPERTY(double seconds READ seconds NOTIFY secondsChanged)
+ Q_PROPERTY(int day READ day NOTIFY dayChanged)
+ Q_PROPERTY(bool started READ started NOTIFY startedChanged)
+ Q_PROPERTY(double elapsedHours READ elapsedHours NOTIFY elapsedHoursChanged)
+ Q_PROPERTY(double elapsedMinutes READ elapsedMinutes NOTIFY elapsedMinutesChanged)
+ Q_PROPERTY(double elapsedSeconds READ elapsedSeconds NOTIFY elapsedSecondsChanged)
+ Q_PROPERTY(double elapsedMilliseconds
+ READ elapsedMilliseconds NOTIFY elapsedMillisecondsChanged)
+ Q_PROPERTY(bool adjustDayMode
+ READ adjustDayMode WRITE setAdjustDayMode NOTIFY adjustDayModeChanged)
+ Q_PROPERTY(bool adjustTimeMode
+ READ adjustTimeMode WRITE setAdjustTimeMode NOTIFY adjustTimeModeChanged)
+
+public:
+ Q_DOTNET_OBJECT(QChronometer, "WatchModels.Chronometer, ChronometerModel");
+
+ QChronometer(const ILapRecorder &lapRecorder);
+ ~QChronometer() override;
+
+ double hours() const;
+ double minutes() const;
+ double seconds() const;
+ int day() const;
+ bool started() const;
+ double elapsedHours() const;
+ double elapsedMinutes() const;
+ double elapsedSeconds() const;
+ double elapsedMilliseconds() const;
+ bool adjustDayMode() const;
+ bool adjustTimeMode() const;
+
+public slots:
+ void startStop();
+ void reset();
+ void setAdjustDayMode(bool value);
+ void setAdjustTimeMode(bool value);
+ void adjust(int delta);
+
+
+signals:
+ void hoursChanged();
+ void minutesChanged();
+ void secondsChanged();
+ void dayChanged();
+ void startedChanged();
+ void elapsedHoursChanged();
+ void elapsedMinutesChanged();
+ void elapsedSecondsChanged();
+ void elapsedMillisecondsChanged();
+ void adjustDayModeChanged();
+ void adjustTimeModeChanged();
+ void lap(int hours, int minutes, int seconds, int milliseconds);
+
+private:
+ QChronometerPrivate *d = nullptr;
+};
diff --git a/examples/Chronometer/QmlChronometer/qlaprecorder.cpp b/examples/Chronometer/QmlChronometer/qlaprecorder.cpp
new file mode 100644
index 0000000..20956c0
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/qlaprecorder.cpp
@@ -0,0 +1,128 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include "qlaprecorder.h"
+
+struct QLapRecorderPrivate
+{
+ QLapRecorderPrivate() = default;
+
+ int lapCount = 0;
+ int lastHours = 0;
+ int lastMinutes = 0;
+ int lastSeconds = 0;
+ int lastMilliseconds = 0;
+ int bestHours = 0;
+ int bestMinutes = 0;
+ int bestSeconds = 0;
+ int bestMilliseconds = 0;
+};
+
+
+ILapRecorder::ILapRecorder() : QDotNetInterface(FullyQualifiedTypeName)
+{
+ setCallback<void, int, int, int, int>(
+ "Mark", [this](int hours, int minutes, int seconds, int milliseconds)
+ {
+ mark(hours, minutes, seconds, milliseconds);
+ });
+}
+
+
+QLapRecorder::QLapRecorder(QObject *parent)
+ : QObject(parent), d(new QLapRecorderPrivate())
+{}
+
+QLapRecorder::~QLapRecorder()
+{
+ delete d;
+}
+
+int QLapRecorder::lapCount() const { return d->lapCount; }
+int QLapRecorder::lastHours() const { return d->lastHours; }
+int QLapRecorder::lastMinutes() const { return d->lastMinutes; }
+int QLapRecorder::lastSeconds() const { return d->lastSeconds; }
+int QLapRecorder::lastMilliseconds() const { return d->lastMilliseconds; }
+int QLapRecorder::bestHours() const { return d->bestHours; }
+int QLapRecorder::bestMinutes() const { return d->bestMinutes; }
+int QLapRecorder::bestSeconds() const { return d->bestSeconds; }
+int QLapRecorder::bestMilliseconds() const { return d->bestMilliseconds; }
+
+void QLapRecorder::mark(int hours, int minutes, int seconds, int milliseconds)
+{
+ d->lastHours = hours;
+ emit lastHoursChanged();
+
+ d->lastMinutes = minutes;
+ emit lastMinutesChanged();
+
+ d->lastSeconds = seconds;
+ emit lastSecondsChanged();
+
+ d->lastMilliseconds = milliseconds;
+ emit lastMillisecondsChanged();
+
+ d->lapCount++;
+ emit lapCountChanged();
+
+ if (d->lapCount > 1
+ && (d->lastHours > d->bestHours
+ || (d->lastHours == d->bestHours
+ && d->lastMinutes > d->bestMinutes)
+ || (d->lastHours == d->bestHours
+ && d->lastMinutes == d->bestMinutes
+ && d->lastSeconds > d->bestSeconds)
+ || (d->lastHours == d->bestHours
+ && d->lastMinutes == d->bestMinutes
+ && d->lastSeconds == d->bestSeconds
+ && d->lastMilliseconds > d->bestMilliseconds))) {
+ return;
+ }
+
+ d->bestHours = hours;
+ emit bestHoursChanged();
+
+ d->bestMinutes = minutes;
+ emit bestMinutesChanged();
+
+ d->bestSeconds = seconds;
+ emit bestSecondsChanged();
+
+ d->bestMilliseconds = milliseconds;
+ emit bestMillisecondsChanged();
+
+ if (d->lapCount > 1)
+ emit newBestLap();
+}
+
+void QLapRecorder::reset()
+{
+ d->lastHours = 0;
+ emit lastHoursChanged();
+
+ d->lastMinutes = 0;
+ emit lastMinutesChanged();
+
+ d->lastSeconds = 0;
+ emit lastSecondsChanged();
+
+ d->lastMilliseconds = 0;
+ emit lastMillisecondsChanged();
+
+ d->lapCount = 0;
+ emit lapCountChanged();
+
+ d->bestHours = 0;
+ emit bestHoursChanged();
+
+ d->bestMinutes = 0;
+ emit bestMinutesChanged();
+
+ d->bestSeconds = 0;
+ emit bestSecondsChanged();
+
+ d->bestMilliseconds = 0;
+ emit bestMillisecondsChanged();
+}
diff --git a/examples/Chronometer/QmlChronometer/qlaprecorder.h b/examples/Chronometer/QmlChronometer/qlaprecorder.h
new file mode 100644
index 0000000..0456288
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/qlaprecorder.h
@@ -0,0 +1,69 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#pragma once
+
+#include "qchronometer.h"
+
+#include <QObject>
+
+struct QLapRecorderPrivate;
+
+struct ILapRecorder : QDotNetInterface
+{
+ static inline const QString& FullyQualifiedTypeName =
+ QStringLiteral("WatchModels.ILapRecorder, ChronometerModel");
+ ILapRecorder();
+
+ virtual void mark(int hours, int minutes, int seconds, int milliseconds) = 0;
+ virtual void reset() = 0;
+};
+
+class QLapRecorder : public QObject, public ILapRecorder
+{
+ Q_OBJECT
+ Q_PROPERTY(int lastHours READ lastHours NOTIFY lastHoursChanged)
+ Q_PROPERTY(int lastMinutes READ lastMinutes NOTIFY lastMinutesChanged)
+ Q_PROPERTY(int lastSeconds READ lastSeconds NOTIFY lastSecondsChanged)
+ Q_PROPERTY(int lastMilliseconds READ lastMilliseconds NOTIFY lastMillisecondsChanged)
+ Q_PROPERTY(int bestHours READ bestHours NOTIFY bestHoursChanged)
+ Q_PROPERTY(int bestMinutes READ bestMinutes NOTIFY bestMinutesChanged)
+ Q_PROPERTY(int bestSeconds READ bestSeconds NOTIFY bestSecondsChanged)
+ Q_PROPERTY(int bestMilliseconds READ bestMilliseconds NOTIFY bestMillisecondsChanged)
+ Q_PROPERTY(int lapCount READ lapCount NOTIFY lapCountChanged)
+
+public:
+ QLapRecorder(QObject *parent = nullptr);
+ ~QLapRecorder() override;
+
+ [[nodiscard]] int lapCount() const;
+ [[nodiscard]] int lastHours() const;
+ [[nodiscard]] int lastMinutes() const;
+ [[nodiscard]] int lastSeconds() const;
+ [[nodiscard]] int lastMilliseconds() const;
+ [[nodiscard]] int bestHours() const;
+ [[nodiscard]] int bestMinutes() const;
+ [[nodiscard]] int bestSeconds() const;
+ [[nodiscard]] int bestMilliseconds() const;
+
+public slots:
+ void mark(int hours, int minutes, int seconds, int milliseconds) override;
+ void reset() override;
+
+signals:
+ void lapCountChanged();
+ void lastHoursChanged();
+ void lastMinutesChanged();
+ void lastSecondsChanged();
+ void lastMillisecondsChanged();
+ void bestHoursChanged();
+ void bestMinutesChanged();
+ void bestSecondsChanged();
+ void bestMillisecondsChanged();
+ void newBestLap();
+
+private:
+ QLapRecorderPrivate *d = nullptr;
+};
diff --git a/examples/Chronometer/QmlChronometer/qml.qrc b/examples/Chronometer/QmlChronometer/qml.qrc
new file mode 100644
index 0000000..15d835d
--- /dev/null
+++ b/examples/Chronometer/QmlChronometer/qml.qrc
@@ -0,0 +1,19 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file alias="watchface.png">content/watchface.png</file>
+ <file alias="chrono_1_hand.png">content/chrono_1_hand.png</file>
+ <file alias="chrono_2_hand.png">content/chrono_2_hand.png</file>
+ <file alias="chrono_3_needle.png">content/chrono_3_needle.png</file>
+ <file alias="hour_hand.png">content/hour_hand.png</file>
+ <file alias="minute_hand.png">content/minute_hand.png</file>
+ <file alias="second_hand.png">content/second_hand.png</file>
+ <file alias="center.png">content/center.png</file>
+ <file alias="chrono_1_center.png">content/chrono_1_center.png</file>
+ <file alias="chrono_2_center.png">content/chrono_2_center.png</file>
+ <file alias="chrono_3_center.png">content/chrono_3_center.png</file>
+ <file>QChronometer/AdjustmentWheel.qml</file>
+ <file>QChronometer/InsetDial.qml</file>
+ <file>QChronometer/WatchButton.qml</file>
+ </qresource>
+</RCC>
diff --git a/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj
new file mode 100644
index 0000000..2544420
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>{8C7C4962-6AAA-4A90-927A-BC88C782CAAA}</ProjectGuid>
+ <Keyword>QtVS_v304</Keyword>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
+ <Import Project="$(QtMsBuild)\qt_defaults.props" />
+ </ImportGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
+ <QtInstall>$(DefaultQtVersion)</QtInstall>
+ <QtModules>quick;core</QtModules>
+ <QtBuildConfig>debug</QtBuildConfig>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
+ <QtInstall>$(DefaultQtVersion)</QtInstall>
+ <QtModules>quick;core</QtModules>
+ <QtBuildConfig>release</QtBuildConfig>
+ </PropertyGroup>
+ <Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
+ <Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
+ </Target>
+ <ImportGroup Label="ExtensionSettings" />
+ <ImportGroup Label="Shared" />
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <IncludePath>../../../include/;$(IncludePath)</IncludePath>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <IncludePath>../../../include/;$(IncludePath)</IncludePath>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+ <Optimization>Disabled</Optimization>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>None</DebugInformationFormat>
+ <Optimization>MaxSpeed</Optimization>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>false</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <QtMoc Include="mainwindow.h" />
+ <QtMoc Include="embeddedwindow.h" />
+ <ClCompile Include="main.cpp" />
+ <QtRcc Include="qml.qrc" />
+ <ClCompile Include="mainwindow.cpp" />
+ <ClCompile Include="embeddedwindow.cpp" />
+ <None Include="main.qml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="Qt.DotNet.Adapter">
+ <HintPath>..\..\..\bin\Qt.DotNet.Adapter.dll</HintPath>
+ </Reference>
+ <ProjectReference Include="..\WpfApp\WpfApp.csproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Image Include="qt_logo.png" />
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
+ <Import Project="$(QtMsBuild)\qt.targets" />
+ </ImportGroup>
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters
new file mode 100644
index 0000000..d958351
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <Filter Include="Source Files">
+ <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+ <Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+ </Filter>
+ <Filter Include="Header Files">
+ <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+ <Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
+ </Filter>
+ <Filter Include="Resource Files">
+ <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+ <Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+ </Filter>
+ <Filter Include="Form Files">
+ <UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
+ <Extensions>ui</Extensions>
+ </Filter>
+ <Filter Include="Translation Files">
+ <UniqueIdentifier>{639EADAA-A684-42e4-A9AD-28FC9BCB8F7C}</UniqueIdentifier>
+ <Extensions>ts</Extensions>
+ </Filter>
+ </ItemGroup>
+ <ItemGroup>
+ <QtRcc Include="qml.qrc">
+ <Filter>Resource Files</Filter>
+ </QtRcc>
+ <None Include="main.qml">
+ <Filter>Source Files</Filter>
+ </None>
+ <ClCompile Include="mainwindow.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ </ItemGroup>
+ <ItemGroup>
+ <QtMoc Include="mainwindow.h">
+ <Filter>Header Files</Filter>
+ </QtMoc>
+ <QtMoc Include="embeddedwindow.h">
+ <Filter>Header Files</Filter>
+ </QtMoc>
+ </ItemGroup>
+ <ItemGroup>
+ <ClCompile Include="embeddedwindow.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="main.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ </ItemGroup>
+ <ItemGroup>
+ <Image Include="qt_logo.png">
+ <Filter>Resource Files</Filter>
+ </Image>
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp b/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp
new file mode 100644
index 0000000..3b7b17d
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp
@@ -0,0 +1,39 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include "embeddedwindow.h"
+
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickView>
+#include <QWindow>
+
+#include "mainwindow.h"
+
+EmbeddedWindow::EmbeddedWindow(QQmlEngine *qmlEngine, MainWindow *mainWindow)
+ : qmlEngine(qmlEngine), mainWindow(mainWindow)
+{
+ connect(mainWindow, &MainWindow::contentRendered, this, &EmbeddedWindow::show);
+ connect(mainWindow, &MainWindow::closed, this, &EmbeddedWindow::close);
+}
+
+EmbeddedWindow::~EmbeddedWindow()
+{
+ delete quickView;
+}
+
+void EmbeddedWindow::show()
+{
+ embeddedWindow = QWindow::fromWinId((WId)mainWindow->hostHandle());
+ quickView = new QQuickView(qmlEngine, embeddedWindow);
+ qmlEngine->rootContext()->setContextProperty("window", quickView);
+ quickView->setSource(QUrl(QStringLiteral("qrc:/main.qml")));
+ quickView->show();
+}
+
+void EmbeddedWindow::close()
+{
+ embeddedWindow->close();
+}
diff --git a/examples/EmbeddedWindow/QmlApp/embeddedwindow.h b/examples/EmbeddedWindow/QmlApp/embeddedwindow.h
new file mode 100644
index 0000000..c84a370
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/embeddedwindow.h
@@ -0,0 +1,31 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#pragma once
+
+#include <QObject>
+
+class QQmlEngine;
+class QQuickView;
+class QWindow;
+class MainWindow;
+
+class EmbeddedWindow : public QObject
+{
+ Q_OBJECT
+public:
+ EmbeddedWindow(QQmlEngine *qmlEngine, MainWindow *mainWindow);
+ ~EmbeddedWindow();
+
+public slots:
+ void show();
+ void close();
+
+private:
+ QQmlEngine *qmlEngine = nullptr;
+ QQuickView *quickView = nullptr;
+ MainWindow *mainWindow = nullptr;
+ QWindow *embeddedWindow = nullptr;
+};
diff --git a/examples/EmbeddedWindow/QmlApp/main.cpp b/examples/EmbeddedWindow/QmlApp/main.cpp
new file mode 100644
index 0000000..957c749
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/main.cpp
@@ -0,0 +1,65 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QThread>
+#include <QFile>
+
+#include <objbase.h>
+
+#include "mainwindow.h"
+#include "embeddedwindow.h"
+
+int main(int argc, char *argv[])
+{
+#if defined(Q_OS_WIN)
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+ QGuiApplication app(argc, argv);
+
+ MainWindow mainWindow;
+ QObject::connect(&mainWindow, &MainWindow::closed, &app, &QCoreApplication::quit);
+
+ QQmlApplicationEngine engine;
+ engine.rootContext()->setContextProperty("mainWindow", &mainWindow);
+
+ EmbeddedWindow embeddedWindow(&engine, &mainWindow);
+
+ QThread *wpfThread = QThread::create([&app, &mainWindow] {
+
+ if (FAILED(CoInitialize(nullptr))) {
+ app.quit();
+ return;
+ }
+
+ QString runtimeConfig = R"[json](
+{
+ "runtimeOptions": {
+ "tfm": "net6.0-windows",
+ "rollForward": "LatestMinor",
+ "framework": {
+ "name": "Microsoft.WindowsDesktop.App",
+ "version": "6.0.0"
+ }
+ }
+}
+)[json]";
+ QFile wpfAppRuntimeConfig(QGuiApplication::applicationDirPath() + "/WpfApp.runtimeconfig.json");
+ if (wpfAppRuntimeConfig.open(QFile::ReadOnly | QFile::Text))
+ runtimeConfig = QString(wpfAppRuntimeConfig.readAll());
+
+ QDotNetHost host;
+ if (!host.load(runtimeConfig)) {
+ app.quit();
+ return;
+ }
+ QDotNetAdapter::init(&host);
+ mainWindow.init();
+ });
+ wpfThread->start();
+ return app.exec();
+}
diff --git a/examples/EmbeddedWindow/QmlApp/main.qml b/examples/EmbeddedWindow/QmlApp/main.qml
new file mode 100644
index 0000000..04b018d
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/main.qml
@@ -0,0 +1,135 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQuick
+import QtQuick3D
+
+Rectangle {
+ width: mainWindow.hostWidth
+ height: mainWindow.hostHeight
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: mainWindow.backgroundColor }
+ GradientStop { position: 0.40; color: "#E6ECED" }
+ GradientStop { position: 0.50; color: "#CCD9DB" }
+ GradientStop { position: 0.60; color: "#B3C6C9" }
+ GradientStop { position: 0.70; color: "#99B3B7" }
+ GradientStop { position: 0.75; color: "#80A0A5" }
+ GradientStop { position: 0.80; color: "#668D92" }
+ GradientStop { position: 0.85; color: "#4D7A80" }
+ GradientStop { position: 0.90; color: "#33676E" }
+ GradientStop { position: 0.95; color: "#19545C" }
+ GradientStop { position: 1.0; color: "#00414A" }
+ }
+ Text {
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.bottomMargin: 5
+ anchors.rightMargin: 10
+ font.pointSize: 20
+ font.weight: Font.Bold
+ color: "#001012"
+ text: "QML"
+ }
+
+ View3D {
+ id: view
+ anchors.fill: parent
+
+ PerspectiveCamera {
+ position: Qt.vector3d(
+ mainWindow.cameraPositionX,
+ mainWindow.cameraPositionY + 200,
+ mainWindow.cameraPositionZ + 300)
+ eulerRotation.x: (mainWindow.cameraRotationX - 30) % 360
+ eulerRotation.y: mainWindow.cameraRotationY
+ eulerRotation.z: mainWindow.cameraRotationZ
+ }
+
+ DirectionalLight {
+ eulerRotation.x: (mainWindow.cameraRotationX - 30) % 360
+ eulerRotation.y: mainWindow.cameraRotationY
+ eulerRotation.z: mainWindow.cameraRotationZ
+ }
+
+ Model {
+ id: cube
+ source: "#Cube"
+ materials: DefaultMaterial {
+ diffuseMap: Texture {
+ sourceItem: Item {
+ id: qt_logo
+ width: 230
+ height: 230
+ visible: false
+ layer.enabled: true
+ Rectangle {
+ anchors.fill: parent
+ color: "black"
+ Image {
+ anchors.fill: parent
+ source: "qt_logo.png"
+ }
+ Text {
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pixelSize: 17
+ text: "The Future is Written with Qt"
+ }
+ Text {
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pixelSize: 17
+ text: "The Future is Written with Qt"
+ }
+ }
+ }
+ }
+ }
+ property var rotation: Qt.vector3d(0, 90, 0)
+
+ eulerRotation.x: rotation.x % 360
+ eulerRotation.y: rotation.y % 360
+ eulerRotation.z: rotation.z % 360
+
+ Vector3dAnimation on rotation {
+ property var delta: Qt.vector3d(0, 0, 0)
+ id: cubeAnimation
+ loops: Animation.Infinite
+ duration: mainWindow.animationDuration
+ from: Qt.vector3d(0, 0, 0).plus(delta)
+ to: Qt.vector3d(360, 0, 360).plus(delta)
+ onDurationChanged: {
+ delta = cube.eulerRotation;
+ restart();
+ }
+ }
+ }
+ }
+
+ property var t0: 0
+ property var n: 0
+
+ Component.onCompleted: {
+ 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++;
+ }
+ }
+ });
+ }
+}
diff --git a/examples/EmbeddedWindow/QmlApp/mainwindow.cpp b/examples/EmbeddedWindow/QmlApp/mainwindow.cpp
new file mode 100644
index 0000000..2829f89
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/mainwindow.cpp
@@ -0,0 +1,170 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include "mainwindow.h"
+
+class HwndHost : public QDotNetObject
+{
+public:
+ Q_DOTNET_OBJECT_INLINE(HwndHost, "System.Windows.Interop.HwndHost, PresentationFramework");
+ void *handle() { return method("get_Handle", fnGetHandle).invoke(*this); }
+ double width() { return method("get_Width", fnGetWidth).invoke(*this); }
+ double height() { return method("get_Height", fnGetHeight).invoke(*this); }
+private:
+ QDotNetFunction<void *> fnGetHandle = nullptr;
+ QDotNetFunction<double> fnGetWidth = nullptr;
+ QDotNetFunction<double> fnGetHeight = nullptr;
+};
+
+class MouseEventArgs : public QDotNetObject
+{
+public:
+ Q_DOTNET_OBJECT_INLINE(MouseEventArgs, "System.Windows.Input.MouseEventArgs, PresentationCore");
+};
+
+class MainWindowPrivate : public QDotNetObject::IEventHandler
+{
+public:
+ MainWindowPrivate(MainWindow *q) : q(q)
+ {}
+ void handleEvent(const QString &evName, QDotNetObject &evSource, QDotNetObject &evArgs) override
+ {
+ if (evName == "ContentRendered") {
+ emit q->contentRendered();
+ } else if (evName == "SizeChanged") {
+ double width = evArgs.object("NewSize").call<double>("get_Width");
+ double height = evArgs.object("NewSize").call<double>("get_Height");
+ if (width != hostWidth) {
+ hostWidth = width;
+ emit q->hostWidthChanged();
+ }
+ if (height != hostHeight) {
+ hostHeight = height;
+ emit q->hostHeightChanged();
+ }
+ } else if (evName == "Closed") {
+ emit q->closed();
+ } else if (evName == "PropertyChanged") {
+ QString propertyName = evArgs.call<QString>("get_PropertyName");
+ if (propertyName == "CameraPositionX")
+ emit q->cameraPositionXChanged();
+ else if (propertyName == "CameraPositionY")
+ emit q->cameraPositionYChanged();
+ else if (propertyName == "CameraPositionZ")
+ emit q->cameraPositionZChanged();
+ else if (propertyName == "CameraRotationX")
+ emit q->cameraRotationXChanged();
+ else if (propertyName == "CameraRotationY")
+ emit q->cameraRotationYChanged();
+ else if (propertyName == "CameraRotationZ")
+ emit q->cameraRotationZChanged();
+ else if (propertyName == "AnimationDuration")
+ emit q->animationDurationChanged();
+ }
+ };
+
+ HwndHost hwndHost = nullptr;
+ double hostWidth = 0.0, hostHeight = 0.0;
+ QDotNetFunction<void, double> fnSetEmbeddedFps = nullptr;
+ QDotNetFunction<double> fnGetCameraPositionX = nullptr;
+ QDotNetFunction<double> fnGetCameraPositionY = nullptr;
+ QDotNetFunction<double> fnGetCameraPositionZ = nullptr;
+ QDotNetFunction<double> fnGetCameraRotationX = nullptr;
+ QDotNetFunction<double> fnGetCameraRotationY = nullptr;
+ QDotNetFunction<double> fnGetCameraRotationZ = nullptr;
+ QDotNetFunction<double> fnGetAnimationDuration = nullptr;
+ QDotNetFunction<double> fnGetFramesPerSecond = nullptr;
+ QDotNetFunction<QString> fnGetBackgroundColor = nullptr;
+
+private:
+ MainWindow *q;
+};
+
+Q_DOTNET_OBJECT_IMPL(MainWindow, Q_DOTNET_OBJECT_INIT(d(new MainWindowPrivate(this))));
+
+MainWindow::MainWindow() : QDotNetObject(nullptr), d(new MainWindowPrivate(this))
+{}
+
+MainWindow::~MainWindow()
+{}
+
+void MainWindow::init()
+{
+ *this = constructor<MainWindow>().invoke(nullptr);
+ d->hwndHost = method<HwndHost>("get_HwndHost").invoke(*this);
+ subscribeEvent("ContentRendered", d);
+ subscribeEvent("Closed", d);
+ subscribeEvent("PropertyChanged", d);
+ d->hwndHost.subscribeEvent("SizeChanged", d);
+
+ QtDotNet::call<void, MainWindow>("WpfApp.Program, WpfApp", "set_MainWindow", *this);
+ QtDotNet::call<int>("WpfApp.Program, WpfApp", "Main");
+}
+
+void *MainWindow::hostHandle()
+{
+ return d->hwndHost.handle();
+}
+
+int MainWindow::hostWidth()
+{
+ return d->hostWidth;
+}
+
+int MainWindow::hostHeight()
+{
+ return d->hostHeight;
+}
+
+
+double MainWindow::cameraPositionX()
+{
+ return method("get_CameraPositionX", d->fnGetCameraPositionX).invoke(*this);
+}
+
+double MainWindow::cameraPositionY()
+{
+ return method("get_CameraPositionY", d->fnGetCameraPositionY).invoke(*this);
+}
+
+double MainWindow::cameraPositionZ()
+{
+ return method("get_CameraPositionZ", d->fnGetCameraPositionZ).invoke(*this);
+}
+
+double MainWindow::cameraRotationX()
+{
+ return method("get_CameraRotationX", d->fnGetCameraRotationX).invoke(*this);
+}
+
+double MainWindow::cameraRotationY()
+{
+ return method("get_CameraRotationY", d->fnGetCameraRotationY).invoke(*this);
+}
+
+double MainWindow::cameraRotationZ()
+{
+ return method("get_CameraRotationZ", d->fnGetCameraRotationZ).invoke(*this);
+}
+
+double MainWindow::animationDuration()
+{
+ return method("get_AnimationDuration", d->fnGetAnimationDuration).invoke(*this);
+}
+
+double MainWindow::framesPerSecond()
+{
+ return method("get_FramesPerSecond", d->fnGetFramesPerSecond).invoke(*this);
+}
+
+QString MainWindow::backgroundColor()
+{
+ return method("get_BackgroundColor", d->fnGetBackgroundColor).invoke(*this);
+}
+
+void MainWindow::setFramesPerSecond(double fps)
+{
+ method("set_FramesPerSecond", d->fnSetEmbeddedFps).invoke(*this, fps);
+}
diff --git a/examples/EmbeddedWindow/QmlApp/mainwindow.h b/examples/EmbeddedWindow/QmlApp/mainwindow.h
new file mode 100644
index 0000000..4d81ed0
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/mainwindow.h
@@ -0,0 +1,61 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#pragma once
+
+#include <QObject>
+#include <QDotNetObject>
+
+class MainWindowPrivate;
+
+class MainWindow : public QObject, public QDotNetObject
+{
+ Q_OBJECT
+ Q_PROPERTY(double hostWidth READ hostWidth NOTIFY hostWidthChanged)
+ Q_PROPERTY(double hostHeight READ hostHeight NOTIFY hostHeightChanged)
+ Q_PROPERTY(double cameraPositionX READ cameraPositionX NOTIFY cameraPositionXChanged)
+ Q_PROPERTY(double cameraPositionY READ cameraPositionY NOTIFY cameraPositionYChanged)
+ Q_PROPERTY(double cameraPositionZ READ cameraPositionZ NOTIFY cameraPositionZChanged)
+ Q_PROPERTY(double cameraRotationX READ cameraRotationX NOTIFY cameraRotationXChanged)
+ Q_PROPERTY(double cameraRotationY READ cameraRotationY NOTIFY cameraRotationYChanged)
+ Q_PROPERTY(double cameraRotationZ READ cameraRotationZ NOTIFY cameraRotationZChanged)
+ Q_PROPERTY(double animationDuration READ animationDuration NOTIFY animationDurationChanged)
+ Q_PROPERTY(QString backgroundColor READ backgroundColor NOTIFY backgroundColorChanged)
+ Q_PROPERTY(double framesPerSecond READ framesPerSecond WRITE setFramesPerSecond)
+public:
+ Q_DOTNET_OBJECT(MainWindow, "WpfApp.MainWindow, WpfApp");
+ MainWindow();
+ ~MainWindow();
+ void init();
+ void *hostHandle();
+ int hostWidth();
+ int hostHeight();
+ double cameraPositionX();
+ double cameraPositionY();
+ double cameraPositionZ();
+ double cameraRotationX();
+ double cameraRotationY();
+ double cameraRotationZ();
+ double animationDuration();
+ double framesPerSecond();
+ QString backgroundColor();
+signals:
+ void contentRendered();
+ void hostWidthChanged();
+ void hostHeightChanged();
+ void cameraPositionXChanged();
+ void cameraPositionYChanged();
+ void cameraPositionZChanged();
+ void cameraRotationXChanged();
+ void cameraRotationYChanged();
+ void cameraRotationZChanged();
+ void animationDurationChanged();
+ void backgroundColorChanged();
+ void closed();
+public slots:
+ void setFramesPerSecond(double fps);
+private:
+ MainWindowPrivate *d;
+};
diff --git a/examples/EmbeddedWindow/QmlApp/qml.qrc b/examples/EmbeddedWindow/QmlApp/qml.qrc
new file mode 100644
index 0000000..040dc48
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/qml.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file>qt_logo.png</file>
+ </qresource>
+</RCC>
diff --git a/examples/EmbeddedWindow/QmlApp/qt_logo.png b/examples/EmbeddedWindow/QmlApp/qt_logo.png
new file mode 100644
index 0000000..30c621c
--- /dev/null
+++ b/examples/EmbeddedWindow/QmlApp/qt_logo.png
Binary files differ
diff --git a/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs b/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs
new file mode 100644
index 0000000..8b5504e
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/examples/EmbeddedWindow/WpfApp/MainWindow.xaml b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml
new file mode 100644
index 0000000..2695ba9
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml
@@ -0,0 +1,58 @@
+<Window x:Class="WpfApp.MainWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
+ xmlns:local="clr-namespace:WpfApp" mc:Ignorable="d" Name="this"
+ Title="WPF + QML Embedded Window" Width="640" Height="480" MinWidth="640" MinHeight="480">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50*" />
+ <ColumnDefinition Width="100*" />
+ </Grid.ColumnDefinitions>
+ <StackPanel Name="StackPanel" Grid.Column="0" Orientation="Vertical" Background="#FFF0F0F0">
+ <Label Content="WPF" FontSize="20" FontWeight="Bold" HorizontalAlignment="Left"
+ VerticalAlignment="Top" Height="37" Margin="10,0,10,0" />
+ <Label Content="Camera Position X" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraPositionX" Minimum="-500" Maximum="500" SmallChange="1"
+ ValueChanged="Slider_ValueChanged" MouseDoubleClick="Slider_MouseDoubleClick"
+ Margin="10,0,10,0" />
+ <Label Content="Camera Position Y" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraPositionY" Minimum="-500" Maximum="500" SmallChange="1"
+ ValueChanged="Slider_ValueChanged" MouseDoubleClick="Slider_MouseDoubleClick"
+ Margin="10,0,10,0" />
+ <Label Content="Camera Position Z" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraPositionZ" Minimum="-500" Maximum="500" SmallChange="1"
+ IsDirectionReversed="True" ValueChanged="Slider_ValueChanged"
+ MouseDoubleClick="Slider_MouseDoubleClick" Margin="10,0,10,0" />
+ <Label Content="Camera Rotation X" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraRotationX" Minimum="-180" Maximum="180" SmallChange="1"
+ ValueChanged="Slider_ValueChanged" MouseDoubleClick="Slider_MouseDoubleClick"
+ Margin="10,0,10,0" />
+ <Label Content="Camera Rotation Y" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraRotationY" Minimum="-180" Maximum="180" SmallChange="1"
+ ValueChanged="Slider_ValueChanged" MouseDoubleClick="Slider_MouseDoubleClick"
+ Margin="10,0,10,0" />
+ <Label Content="Camera Rotation Z" Margin="10,0,10,0" />
+ <Slider Name="SliderCameraRotationZ" Minimum="-180" Maximum="180" SmallChange="1"
+ ValueChanged="Slider_ValueChanged" MouseDoubleClick="Slider_MouseDoubleClick"
+ Margin="10,0,10,0" />
+ <Label Content="Animation Speed" Margin="10,0,10,0" />
+ <Slider Name="SliderAnimationDuration" Minimum="0" Value="0.1" Maximum="1" SmallChange="0.001"
+ TickPlacement="BottomRight" TickFrequency="0.05" ValueChanged="Slider_ValueChanged"
+ MouseDoubleClick="Slider_MouseDoubleClick" Margin="10,0,10,0" />
+ <Label Content="{Binding Rpm, ElementName=this}" ContentStringFormat="{}{0:F0} rpm"
+ HorizontalContentAlignment="Center" Margin="10,-5,10,0" />
+ <Label Content="Frame Rate" Margin="10,0,10,0" />
+ <Grid Margin="10,0,10,0">
+ <ProgressBar Name="FpsValue" Value="0" Maximum="60" Margin="10,0,10,0" />
+ <TextBlock Name="FpsLabel" HorizontalAlignment="Center" VerticalAlignment="Center"
+ Text="--.- fps" />
+ </Grid>
+ </StackPanel>
+ <WindowsFormsHost Name="EmbeddedAppHost" Grid.Column="1">
+ <wf:Panel Name="EmbeddedAppPanel" BackColor="#AAAAAA" />
+ </WindowsFormsHost>
+ </Grid>
+</Window>
diff --git a/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs
new file mode 100644
index 0000000..77f1bf8
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs
@@ -0,0 +1,109 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace WpfApp
+{
+ public partial class MainWindow : Window, INotifyPropertyChanged
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+
+ public HwndHost HwndHost => EmbeddedAppHost;
+
+ public double CameraPositionX => WpfThread(() => SliderCameraPositionX.Value);
+ public double CameraPositionY => WpfThread(() => SliderCameraPositionY.Value);
+ public double CameraPositionZ => WpfThread(() => SliderCameraPositionZ.Value);
+ public double CameraRotationX => WpfThread(() => SliderCameraRotationX.Value);
+ public double CameraRotationY => WpfThread(() => SliderCameraRotationY.Value);
+ public double CameraRotationZ => WpfThread(() => SliderCameraRotationZ.Value);
+
+ public static double MaxRpm => 100;
+ public double Rpm => MaxRpm * Math.Max(1 / MaxRpm, SliderAnimationDuration.Value);
+ public double AnimationDuration => WpfThread(() => 60000 / Rpm);
+
+ public double FramesPerSecond
+ {
+ get => WpfThread(() => FpsValue.Value);
+ set
+ {
+ WpfThread(() =>
+ {
+ if (value <= FpsValue.Maximum)
+ FpsValue.Value = value;
+ FpsLabel.Text = $"{value:0.0} fps";
+ });
+ }
+ }
+
+ public string BackgroundColor { get; set; } = "#FFFFFF";
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ private void NotifyPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
+ {
+ if (sender is not Slider slider)
+ return;
+ NotifyPropertyChanged(slider.Name switch
+ {
+ nameof(SliderCameraPositionX) => nameof(CameraPositionX),
+ nameof(SliderCameraPositionY) => nameof(CameraPositionY),
+ nameof(SliderCameraPositionZ) => nameof(CameraPositionZ),
+ nameof(SliderCameraRotationX) => nameof(CameraRotationX),
+ nameof(SliderCameraRotationY) => nameof(CameraRotationY),
+ nameof(SliderCameraRotationZ) => nameof(CameraRotationZ),
+ nameof(SliderAnimationDuration) => nameof(AnimationDuration),
+ _ => throw new NotSupportedException()
+ });
+ if (slider.Name is nameof(SliderAnimationDuration))
+ NotifyPropertyChanged(nameof(Rpm));
+ }
+
+ private void Slider_MouseDoubleClick(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is not Slider slider)
+ return;
+ if (slider.Name is nameof(SliderAnimationDuration))
+ slider.Value = (slider.Maximum + slider.Minimum) / 10;
+ else
+ slider.Value = (slider.Maximum + slider.Minimum) / 2;
+ }
+
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ base.OnRender(drawingContext);
+ if (StackPanel.Background is not SolidColorBrush panelBrush)
+ return;
+ if (BackgroundColor != (BackgroundColor = panelBrush.Color.ToString()))
+ NotifyPropertyChanged(nameof(BackgroundColor));
+ }
+
+ private static void WpfThread(Action action)
+ {
+ if (Application.Current?.Dispatcher is { } dispatcher)
+ dispatcher.Invoke(action);
+ }
+
+ private static T WpfThread<T>(Func<T> func)
+ {
+ if (Application.Current?.Dispatcher is not { } dispatcher)
+ return default;
+ return dispatcher.Invoke(func);
+ }
+ }
+}
diff --git a/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json b/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json
new file mode 100644
index 0000000..70d4330
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "WpfApp": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "PATH": "C:/lib/Qt/6.2.4/msvc2019_64/bin;%PATH%"
+ },
+ "nativeDebugging": false
+ }
+ }
+} \ No newline at end of file
diff --git a/examples/EmbeddedWindow/WpfApp/WpfApp.cs b/examples/EmbeddedWindow/WpfApp/WpfApp.cs
new file mode 100644
index 0000000..b80d9df
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/WpfApp.cs
@@ -0,0 +1,23 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System;
+using System.Windows;
+
+namespace WpfApp
+{
+ public static class Program
+ {
+ [STAThread]
+ public static int Main()
+ {
+ var application = new Application();
+ (MainWindow ??= new MainWindow()).InitializeComponent();
+ MainWindow.Show();
+ return application.Run();
+ }
+ public static MainWindow MainWindow { get; set; }
+ }
+}
diff --git a/examples/EmbeddedWindow/WpfApp/WpfApp.csproj b/examples/EmbeddedWindow/WpfApp/WpfApp.csproj
new file mode 100644
index 0000000..8046823
--- /dev/null
+++ b/examples/EmbeddedWindow/WpfApp/WpfApp.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-windows</TargetFramework>
+ <Nullable>disable</Nullable>
+ <UseWPF>true</UseWPF>
+ <StartupObject>WpfApp.Program</StartupObject>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\src\Qt.DotNet.Adapter\Qt.DotNet.Adapter.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="Qt.DotNet.Adapter" Condition="Exists('..\..\..\bin\Qt.DotNet.Adapter.dll')">
+ <HintPath>..\..\..\bin\Qt.DotNet.Adapter.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Drawing.Common">
+ <HintPath>$(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Drawing.Common.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Windows.Forms">
+ <HintPath>$(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Windows.Forms.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Windows.Forms.Primitives">
+ <HintPath>$(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Windows.Forms.Primitives.dll</HintPath>
+ </Reference>
+ <Reference Include="WindowsFormsIntegration">
+ <HintPath>$(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\WindowsFormsIntegration.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+</Project>
diff --git a/examples/QtAzureIoT/QtAzureIoT.sln b/examples/QtAzureIoT/QtAzureIoT.sln
new file mode 100644
index 0000000..4f5a653
--- /dev/null
+++ b/examples/QtAzureIoT/QtAzureIoT.sln
@@ -0,0 +1,88 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.33130.402
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "device", "device", "{BD2258EF-0E47-4DAA-94DF-D08CA1B09A93}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SensorData", "device\SensorData\SensorData.csproj", "{D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceToBackoffice", "device\DeviceToBackoffice\DeviceToBackoffice.csproj", "{4F2A52A9-9DAE-4250-A881-F0A013A589F4}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "deviceapp", "device\deviceapp\deviceapp.vcxproj", "{2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{AC26C5B9-CA58-434C-B607-DC94BFE2A665}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "app", "app", "{60F1722D-12B9-4671-B9E3-EDE5C41F1086}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CardReader", "device\CardReader\CardReader.csproj", "{66A69341-3B00-4812-AA77-EC5C2E9EA23A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{34513197-4269-4943-9F6D-CE4D89CB4DD2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utils", "common\Utils.csproj", "{DEF7470A-3D27-4D71-9E48-A96C9129FA42}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|x64.Build.0 = Debug|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|x64.ActiveCfg = Release|Any CPU
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|x64.Build.0 = Release|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|x64.Build.0 = Debug|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|x64.ActiveCfg = Release|Any CPU
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|x64.Build.0 = Release|Any CPU
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|Any CPU.Build.0 = Debug|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|x64.ActiveCfg = Debug|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|x64.Build.0 = Debug|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|Any CPU.ActiveCfg = Release|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|Any CPU.Build.0 = Release|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|x64.ActiveCfg = Release|x64
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|x64.Build.0 = Release|x64
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|x64.Build.0 = Debug|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|x64.ActiveCfg = Release|Any CPU
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|x64.Build.0 = Release|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|x64.Build.0 = Debug|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|x64.ActiveCfg = Release|Any CPU
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|x64.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665}
+ {4F2A52A9-9DAE-4250-A881-F0A013A589F4} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665}
+ {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF} = {60F1722D-12B9-4671-B9E3-EDE5C41F1086}
+ {AC26C5B9-CA58-434C-B607-DC94BFE2A665} = {BD2258EF-0E47-4DAA-94DF-D08CA1B09A93}
+ {60F1722D-12B9-4671-B9E3-EDE5C41F1086} = {BD2258EF-0E47-4DAA-94DF-D08CA1B09A93}
+ {66A69341-3B00-4812-AA77-EC5C2E9EA23A} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665}
+ {DEF7470A-3D27-4D71-9E48-A96C9129FA42} = {34513197-4269-4943-9F6D-CE4D89CB4DD2}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9EC3D98D-2164-4E07-A5D2-334E9C93C570}
+ EndGlobalSection
+EndGlobal
diff --git a/examples/QtAzureIoT/common/PropertySet.cs b/examples/QtAzureIoT/common/PropertySet.cs
new file mode 100644
index 0000000..2744a5c
--- /dev/null
+++ b/examples/QtAzureIoT/common/PropertySet.cs
@@ -0,0 +1,27 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System.ComponentModel;
+
+namespace QtAzureIoT.Utils
+{
+ public class PropertySet : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void NotifyPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ protected void SetProperty<T>(ref T currentValue, T newValue, string name)
+ {
+ if (newValue.Equals(currentValue))
+ return;
+ currentValue = newValue;
+ NotifyPropertyChanged(name);
+ }
+ }
+}
diff --git a/examples/QtAzureIoT/common/Utils.csproj b/examples/QtAzureIoT/common/Utils.csproj
new file mode 100644
index 0000000..141e38f
--- /dev/null
+++ b/examples/QtAzureIoT/common/Utils.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+</Project>
diff --git a/examples/QtAzureIoT/device/CardReader/CardReader.cs b/examples/QtAzureIoT/device/CardReader/CardReader.cs
new file mode 100644
index 0000000..e97de9e
--- /dev/null
+++ b/examples/QtAzureIoT/device/CardReader/CardReader.cs
@@ -0,0 +1,98 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System.Device.I2c;
+using System.Diagnostics;
+using Iot.Device.Pn532;
+using Iot.Device.Pn532.ListPassive;
+using Iot.Device.Pn532.RfConfiguration;
+using QtAzureIoT.Utils;
+
+namespace QtAzureIoT.Device
+{
+ public class CardReader : PropertySet, IDisposable
+ {
+ public CardReader()
+ { }
+
+ public bool CardInReader
+ {
+ get => propertyCardInReader;
+ private set => SetProperty(ref propertyCardInReader, value, nameof(CardInReader));
+ }
+
+ public void StartPolling()
+ {
+ if (Nfc != null)
+ return;
+ BusConnectionSettings = new I2cConnectionSettings(1, 0x24);
+ BusDevice = I2cDevice.Create(BusConnectionSettings);
+ Nfc = new Pn532(BusDevice);
+
+ PollingLoop = new CancellationTokenSource();
+ Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token);
+ Polling.Start();
+ }
+
+ public void StopPolling()
+ {
+ if (Nfc == null)
+ return;
+ PollingLoop.Cancel();
+ Polling.Wait();
+ Nfc.Dispose();
+ Nfc = null;
+ BusDevice.Dispose();
+ BusDevice = null;
+ BusConnectionSettings = null;
+ PollingLoop.Dispose();
+ PollingLoop = null;
+ Polling.Dispose();
+ Polling = null;
+ }
+
+ #region private
+ private I2cConnectionSettings BusConnectionSettings { get; set; }
+ private I2cDevice BusDevice { get; set; }
+ private Pn532 Nfc { get; set; }
+ private CancellationTokenSource PollingLoop { get; set; }
+ private Task Polling { get; set; }
+
+
+ private async Task PollingLoopAsync()
+ {
+ TargetBaudRate cardType = TargetBaudRate.B106kbpsTypeA;
+ while (!PollingLoop.IsCancellationRequested) {
+ try {
+ if (Nfc.ListPassiveTarget(MaxTarget.One, cardType) is object) {
+ CardInReader = true;
+ var timeSinceDetected = Stopwatch.StartNew();
+ while (timeSinceDetected.ElapsedMilliseconds < 3000) {
+ if (Nfc.ListPassiveTarget(MaxTarget.One, cardType) is object)
+ timeSinceDetected.Restart();
+ await Task.Delay(200);
+ }
+ CardInReader = false;
+ } else {
+ Nfc.SetRfField(RfFieldMode.None);
+ await Task.Delay(1000);
+ Nfc.SetRfField(RfFieldMode.RF);
+ }
+ } catch (Exception e) {
+ Debug.WriteLine($"Exception: {e.GetType().Name}: {e.Message}");
+ Nfc.SetRfField(RfFieldMode.None);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ StopPolling();
+ }
+
+ private bool propertyCardInReader = false;
+ #endregion
+ }
+}
diff --git a/examples/QtAzureIoT/device/CardReader/CardReader.csproj b/examples/QtAzureIoT/device/CardReader/CardReader.csproj
new file mode 100644
index 0000000..c694f5b
--- /dev/null
+++ b/examples/QtAzureIoT/device/CardReader/CardReader.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Iot.Device.Bindings" Version="2.2.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\common\Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs
new file mode 100644
index 0000000..5fe3cb9
--- /dev/null
+++ b/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs
@@ -0,0 +1,70 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Devices.Client;
+using Microsoft.Azure.Devices.Client.Samples;
+using Microsoft.Azure.Devices.Provisioning.Client;
+using Microsoft.Azure.Devices.Provisioning.Client.PlugAndPlay;
+using Microsoft.Azure.Devices.Provisioning.Client.Transport;
+using Microsoft.Azure.Devices.Shared;
+using Microsoft.Extensions.Logging;
+
+namespace QtAzureIoT.Device
+{
+ public class Backoffice
+ {
+ public Backoffice()
+ {
+ DeviceClient = DeviceClient.CreateFromConnectionString(
+ "HostName=QtDotNetDemo-Hub.azure-devices.net;DeviceId=QtDotNetDemoDevice;SharedAccessKey=YkZmsSOZf8lvQb5HDthosRHP4XV1hYSuDEoExe/2Fj8=",
+ TransportType.Mqtt,
+ new ClientOptions
+ {
+ ModelId = "dtmi:com:example:TemperatureController;2"
+ });
+ BackofficeInterface = new TemperatureControllerSample(DeviceClient);
+ }
+
+ public void SetTelemetry(string name, double value)
+ {
+ BackofficeInterface.SetTelemetry(name, value);
+ }
+
+ public void SetTelemetry(string name, bool value)
+ {
+ BackofficeInterface.SetTelemetry(name, value);
+ }
+
+ public void StartPolling()
+ {
+ PollingLoop = new CancellationTokenSource();
+ Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token);
+ Polling.Start();
+ }
+
+ public void StopPolling()
+ {
+ PollingLoop.Cancel();
+ }
+
+ #region private
+ private CancellationTokenSource PollingLoop { get; set; }
+ private Task Polling { get; set; }
+ private DeviceClient DeviceClient { get; }
+ private TemperatureControllerSample BackofficeInterface { get; }
+
+ private async Task PollingLoopAsync()
+ {
+ await BackofficeInterface.InitOperationsAsync(PollingLoop.Token);
+ while (!PollingLoop.IsCancellationRequested) {
+ await BackofficeInterface.PerformOperationsAsync(PollingLoop.Token);
+ }
+ }
+ #endregion
+ }
+}
diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj b/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj
new file mode 100644
index 0000000..a53efc9
--- /dev/null
+++ b/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Azure.Devices" Version="1.38.2" />
+ <PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.41.3" />
+ <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Client" Version="1.19.2" />
+ <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Mqtt" Version="1.17.2" />
+ <PackageReference Include="Microsoft.Azure.Devices.Shared" Version="1.30.2" />
+ </ItemGroup>
+
+</Project>
diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs
new file mode 100644
index 0000000..16bef2b
--- /dev/null
+++ b/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs
@@ -0,0 +1,423 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Azure.Devices.Client;
+using Microsoft.Azure.Devices.Shared;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace PnpHelpers
+{
+ public class PnpConvention
+ {
+ /// <summary>
+ /// The content type for a plug and play compatible telemetry message.
+ /// </summary>
+ public const string ContentApplicationJson = "application/json";
+
+ /// <summary>
+ /// The key for a component identifier within a property update patch. Corresponding value is <see cref="PropertyComponentIdentifierValue"/>.
+ /// </summary>
+ public const string PropertyComponentIdentifierKey = "__t";
+
+ /// <summary>
+ /// The value for a component identifier within a property update patch. Corresponding key is <see cref="PropertyComponentIdentifierKey"/>.
+ /// </summary>
+ public const string PropertyComponentIdentifierValue = "c";
+
+ /// <summary>
+ /// Create a plug and play compatible telemetry message.
+ /// </summary>
+ /// <param name="telemetryName">The name of the telemetry, as defined in the DTDL interface. Must be 64 characters or less. For more details see
+ /// <see href="https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v2/dtdlv2.md#telemetry"/>.</param>
+ /// <param name="telemetryValue">The unserialized telemetry payload, in the format defined in the DTDL interface.</param>
+ /// <param name="componentName">The name of the component in which the telemetry is defined. Can be null for telemetry defined under the root interface.</param>
+ /// <param name="encoding">The character encoding to be used when encoding the message body to bytes. This defaults to utf-8.</param>
+ /// <returns>A plug and play compatible telemetry message, which can be sent to IoT Hub. The caller must dispose this object when finished.</returns>
+ public static Message CreateMessage(string telemetryName, object telemetryValue, string componentName = default, Encoding encoding = default)
+ {
+ if (string.IsNullOrWhiteSpace(telemetryName))
+ {
+ throw new ArgumentNullException(nameof(telemetryName));
+ }
+ if (telemetryValue == null)
+ {
+ throw new ArgumentNullException(nameof(telemetryValue));
+ }
+
+ return CreateMessage(new Dictionary<string, object> { { telemetryName, telemetryValue } }, componentName, encoding);
+ }
+
+ /// <summary>
+ /// Create a plug and play compatible telemetry message.
+ /// </summary>
+ /// <param name="componentName">The name of the component in which the telemetry is defined. Can be null for telemetry defined under the root interface.</param>
+ /// <param name="telemetryPairs">The unserialized name and value telemetry pairs, as defined in the DTDL interface. Names must be 64 characters or less. For more details see
+ /// <see href="https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v2/dtdlv2.md#telemetry"/>.</param>
+ /// <param name="encoding">The character encoding to be used when encoding the message body to bytes. This defaults to utf-8.</param>
+ /// <returns>A plug and play compatible telemetry message, which can be sent to IoT Hub. The caller must dispose this object when finished.</returns>
+ public static Message CreateMessage(IDictionary<string, object> telemetryPairs, string componentName = default, Encoding encoding = default)
+ {
+ if (telemetryPairs == null)
+ {
+ throw new ArgumentNullException(nameof(telemetryPairs));
+ }
+
+ Encoding messageEncoding = encoding ?? Encoding.UTF8;
+ string payload = JsonConvert.SerializeObject(telemetryPairs);
+ var message = new Message(messageEncoding.GetBytes(payload))
+ {
+ ContentEncoding = messageEncoding.WebName,
+ ContentType = ContentApplicationJson,
+ };
+
+ if (!string.IsNullOrWhiteSpace(componentName))
+ {
+ message.ComponentName = componentName;
+ }
+
+ return message;
+ }
+
+ /// <summary>
+ /// Creates a batch property update payload for the specified property key/value pairs.
+ /// </summary>
+ /// <param name="propertyName">The name of the twin property.</param>
+ /// <param name="propertyValue">The unserialized value of the twin property.</param>
+ /// <returns>A compact payload of the properties to update.</returns>
+ /// <remarks>
+ /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective.
+ /// All properties are read-write from a device's perspective.
+ /// For a root-level property update, the patch is in the format: <c>{ "samplePropertyName": 20 }</c>
+ /// </remarks>
+ public static TwinCollection CreatePropertyPatch(string propertyName, object propertyValue)
+ {
+ return CreatePropertyPatch(new Dictionary<string, object> { { propertyName, propertyValue } });
+ }
+
+ /// <summary>
+ /// Creates a batch property update payload for the specified property key/value pairs
+ /// </summary>
+ /// <remarks>
+ /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective.
+ /// All properties are read-write from a device's perspective.
+ /// For a root-level property update, the patch is in the format: <c>{ "samplePropertyName": 20 }</c>
+ /// </remarks>
+ /// <param name="propertyPairs">The twin properties and values to update.</param>
+ /// <returns>A compact payload of the properties to update.</returns>
+ public static TwinCollection CreatePropertyPatch(IDictionary<string, object> propertyPairs)
+ {
+ return new TwinCollection(JsonConvert.SerializeObject(propertyPairs));
+ }
+
+ /// <summary>
+ /// Create a key/value property patch for updating digital twin properties.
+ /// </summary>
+ /// <remarks>
+ /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective.
+ /// All properties are read-write from a device's perspective.
+ /// For a component-level property update, the patch is in the format:
+ /// <code>
+ /// {
+ /// "sampleComponentName": {
+ /// "__t": "c",
+ /// "samplePropertyName"": 20
+ /// }
+ /// }
+ /// </code>
+ /// </remarks>
+ /// <param name="componentName">The name of the component in which the property is defined. Can be null for property defined under the root interface.</param>
+ /// <param name="propertyName">The name of the twin property.</param>
+ /// <param name="propertyValue">The unserialized value of the twin property.</param>
+ /// <returns>The property patch for read-only and read-write property updates.</returns>
+ public static TwinCollection CreateComponentPropertyPatch(string componentName, string propertyName, object propertyValue)
+ {
+ if (string.IsNullOrWhiteSpace(propertyName))
+ {
+ throw new ArgumentNullException(nameof(propertyName));
+ }
+ if (propertyValue == null)
+ {
+ throw new ArgumentNullException(nameof(propertyValue));
+ }
+
+ return CreateComponentPropertyPatch(componentName, new Dictionary<string, object> { { propertyName, propertyValue } });
+ }
+
+ /// <summary>
+ /// Create a key/value property patch for updating digital twin properties.
+ /// </summary>
+ /// <remarks>
+ /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective.
+ /// All properties are read-write from a device's perspective.
+ /// For a component-level property update, the patch is in the format:
+ /// <code>
+ /// {
+ /// "sampleComponentName": {
+ /// "__t": "c",
+ /// "samplePropertyName": 20
+ /// }
+ /// }
+ /// </code>
+ /// </remarks>
+ /// <param name="componentName">The name of the component in which the property is defined. Can be null for property defined under the root interface.</param>
+ /// <param name="propertyPairs">The property name and an unserialized value, as defined in the DTDL interface.</param>
+ /// <returns>The property patch for read-only and read-write property updates.</returns>
+ public static TwinCollection CreateComponentPropertyPatch(string componentName, IDictionary<string, object> propertyPairs)
+ {
+ if (string.IsNullOrWhiteSpace(componentName))
+ {
+ throw new ArgumentNullException(nameof(componentName));
+ }
+ if (propertyPairs == null)
+ {
+ throw new ArgumentNullException(nameof(propertyPairs));
+ }
+
+ var propertyPatch = new StringBuilder();
+ propertyPatch.Append('{');
+ propertyPatch.Append($"\"{componentName}\":");
+ propertyPatch.Append('{');
+ propertyPatch.Append($"\"{PropertyComponentIdentifierKey}\":\"{PropertyComponentIdentifierValue}\",");
+ foreach (var kvp in propertyPairs)
+ {
+ propertyPatch.Append($"\"{kvp.Key}\":{JsonConvert.SerializeObject(kvp.Value)},");
+ }
+
+ // remove the extra comma
+ propertyPatch.Remove(propertyPatch.Length - 1, 1);
+
+ propertyPatch.Append("}}");
+
+ return new TwinCollection(propertyPatch.ToString());
+ }
+
+ /// <summary>
+ /// Creates a response to a write request on a device property.
+ /// </summary>
+ /// <remarks>
+ /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective.
+ /// All properties are read-write from a device's perspective.
+ /// For a component-level property update, the patch is in the format:
+ /// <code>
+ /// {
+ /// "sampleComponentName": {
+ /// "__t": "c",
+ /// "samplePropertyName": 20
+ /// }
+ /// }
+ /// </code>
+ /// </remarks>
+ /// <param name="propertyName">The name of the property to report.</param>
+ /// <param name="propertyValue">The unserialized property value.</param>
+ /// <param name="ackCode">The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.</param>
+ /// <param name="ackVersion">The acknowledgment version, as supplied in the property update request.</param>
+ /// <param name="ackDescription">The acknowledgment description, an optional, human-readable message about the result of the property update.</param>
+ /// <returns>A serialized json string response.</returns>
+ public static TwinCollection CreateWritablePropertyResponse(
+ string propertyName,
+ object propertyValue,
+ int ackCode,
+ long ackVersion,
+ string ackDescription = null)
+ {
+ if (string.IsNullOrWhiteSpace(propertyName))
+ {
+ throw new ArgumentNullException(nameof(propertyName));
+ }
+
+ return CreateWritablePropertyResponse(
+ new Dictionary<string, object> { { propertyName, propertyValue } },
+ ackCode,
+ ackVersion,
+ ackDescription);
+ }
+
+ /// <summary>
+ /// Creates a response to a write request on a device property.
+ /// </summary>
+ /// <param name="propertyPairs">The name and unserialized value of the property to report.</param>
+ /// <param name="ackCode">The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.</param>
+ /// <param name="ackVersion">The acknowledgment version, as supplied in the property update request.</param>
+ /// <param name="ackDescription">The acknowledgment description, an optional, human-readable message about the result of the property update.</param>
+ /// <returns>A serialized json string response.</returns>
+ public static TwinCollection CreateWritablePropertyResponse(
+ IDictionary<string, object> propertyPairs,
+ int ackCode,
+ long ackVersion,
+ string ackDescription = null)
+ {
+ if (propertyPairs == null)
+ {
+ throw new ArgumentNullException(nameof(propertyPairs));
+ }
+
+ var response = new Dictionary<string, WritablePropertyResponse>(propertyPairs.Count);
+ foreach (var kvp in propertyPairs)
+ {
+ if (string.IsNullOrWhiteSpace(kvp.Key))
+ {
+ throw new ArgumentNullException(nameof(kvp.Key), $"One of the propertyPairs keys was null, empty, or white space.");
+ }
+ response.Add(kvp.Key, new WritablePropertyResponse(kvp.Value, ackCode, ackVersion, ackDescription));
+ }
+
+ return new TwinCollection(JsonConvert.SerializeObject(response));
+ }
+
+ /// <summary>
+ /// Creates a response to a write request on a device property.
+ /// </summary>
+ /// <remarks>
+ /// For a component-level property update, the patch is in the format:
+ /// <code>
+ /// "sampleComponentName": {
+ /// "__t": "c",
+ /// "samplePropertyName": {
+ /// "value": 20,
+ /// "ac": 200,
+ /// "av": 5,
+ /// "ad": "The update was successful."
+ /// }
+ /// }
+ /// }
+ /// </code>
+ /// </remarks>
+ /// <param name="componentName">The component to which the property belongs.</param>
+ /// <param name="propertyName">The name of the property to report.</param>
+ /// <param name="propertyValue">The unserialized property value.</param>
+ /// <param name="ackCode">The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.</param>
+ /// <param name="ackVersion">The acknowledgment version, as supplied in the property update request.</param>
+ /// <param name="ackDescription">The acknowledgment description, an optional, human-readable message about the result of the property update.</param>
+ /// <returns>A serialized json string response.</returns>
+ public static TwinCollection CreateComponentWritablePropertyResponse(
+ string componentName,
+ string propertyName,
+ object propertyValue,
+ int ackCode,
+ long ackVersion,
+ string ackDescription = null)
+ {
+ if (string.IsNullOrWhiteSpace(componentName))
+ {
+ throw new ArgumentNullException(nameof(componentName));
+ }
+ if (string.IsNullOrWhiteSpace(propertyName))
+ {
+ throw new ArgumentNullException(nameof(propertyName));
+ }
+
+ return CreateComponentWritablePropertyResponse(
+ componentName,
+ new Dictionary<string, object> { { propertyName, propertyValue } },
+ ackCode,
+ ackVersion,
+ ackDescription);
+ }
+
+ /// <summary>
+ /// Creates a response to a write request on a device property.
+ /// </summary>
+ /// <remarks>
+ /// For a component-level property update, the patch is in the format:
+ /// <code>
+ /// "sampleComponentName": {
+ /// "__t": "c",
+ /// "samplePropertyName": {
+ /// "value": 20,
+ /// "ac": 200,
+ /// "av": 5,
+ /// "ad": "The update was successful."
+ /// }
+ /// }
+ /// }
+ /// </code>
+ /// </remarks>
+ /// <param name="componentName">The component to which the property belongs.</param>
+ /// <param name="propertyPairs">The name and unserialized value of the property to report.</param>
+ /// <param name="ackCode">The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.</param>
+ /// <param name="ackVersion">The acknowledgment version, as supplied in the property update request.</param>
+ /// <param name="ackDescription">The acknowledgment description, an optional, human-readable message about the result of the property update.</param>
+ /// <returns>A serialized json string response.</returns>
+ public static TwinCollection CreateComponentWritablePropertyResponse(
+ string componentName,
+ IDictionary<string, object> propertyPairs,
+ int ackCode,
+ long ackVersion,
+ string ackDescription = null)
+ {
+ if (string.IsNullOrWhiteSpace(componentName))
+ {
+ throw new ArgumentNullException(nameof(componentName));
+ }
+ if (propertyPairs == null)
+ {
+ throw new ArgumentNullException(nameof(propertyPairs));
+ }
+
+ var propertyPatch = new Dictionary<string, object>
+ {
+ { PropertyComponentIdentifierKey, PropertyComponentIdentifierValue },
+ };
+ foreach (var kvp in propertyPairs)
+ {
+ if (string.IsNullOrWhiteSpace(kvp.Key))
+ {
+ throw new ArgumentNullException(nameof(kvp.Key), $"One of the propertyPairs keys was null, empty, or white space.");
+ }
+ propertyPatch.Add(kvp.Key, new WritablePropertyResponse(kvp.Value, ackCode, ackVersion, ackDescription));
+ }
+
+ var response = new Dictionary<string, object>
+ {
+ { componentName, propertyPatch },
+ };
+
+ return new TwinCollection(JsonConvert.SerializeObject(response));
+ }
+
+ /// <summary>
+ /// Helper to retrieve the property value from the <see cref="TwinCollection"/> property update patch which was received as a result of service-initiated update.
+ /// </summary>
+ /// <typeparam name="T">The data type of the property, as defined in the DTDL interface.</typeparam>
+ /// <param name="collection">The <see cref="TwinCollection"/> property update patch received as a result of service-initiated update.</param>
+ /// <param name="propertyName">The property name, as defined in the DTDL interface.</param>
+ /// <param name="propertyValue">The corresponding property value.</param>
+ /// <param name="componentName">The name of the component in which the property is defined. Can be null for property defined under the root interface.</param>
+ /// <returns>A boolean indicating if the <see cref="TwinCollection"/> property update patch received contains the property update.</returns>
+ public static bool TryGetPropertyFromTwin<T>(TwinCollection collection, string propertyName, out T propertyValue, string componentName = null)
+ {
+ if (collection == null)
+ {
+ throw new ArgumentNullException(nameof(collection));
+ }
+
+ // If the desired property update is for a root component or nested component, verify that property patch received contains the desired property update.
+ propertyValue = default;
+
+ if (string.IsNullOrWhiteSpace(componentName))
+ {
+ if (collection.Contains(propertyName))
+ {
+ propertyValue = (T)collection[propertyName];
+ return true;
+ }
+ }
+
+ if (collection.Contains(componentName))
+ {
+ JObject componentProperty = collection[componentName];
+ if (componentProperty.ContainsKey(propertyName))
+ {
+ propertyValue = componentProperty.Value<T>(propertyName);
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs
new file mode 100644
index 0000000..58e7885
--- /dev/null
+++ b/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs
@@ -0,0 +1,483 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Devices.Shared;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using PnpHelpers;
+
+namespace Microsoft.Azure.Devices.Client.Samples
+{
+ internal enum StatusCode
+ {
+ Completed = 200,
+ InProgress = 202,
+ ReportDeviceInitialProperty = 203,
+ BadRequest = 400,
+ NotFound = 404
+ }
+
+
+ public class TemperatureControllerSample
+ {
+ public ConcurrentDictionary<string, object> Telemetry = new();
+ public void SetTelemetry(string name, double value)
+ {
+ Telemetry.AddOrUpdate(name, value, (k, v) => value);
+ }
+ public void SetTelemetry(string name, bool value)
+ {
+ Telemetry.AddOrUpdate(name, value, (k, v) => value);
+ }
+
+ // The default reported "value" and "av" for each "Thermostat" component on the client initial startup.
+ // See https://docs.microsoft.com/azure/iot-develop/concepts-convention#writable-properties for more details in acknowledgment responses.
+ private const double DefaultPropertyValue = 0d;
+
+ private const long DefaultAckVersion = 0L;
+
+ private const string TargetTemperatureProperty = "targetTemperature";
+
+ private const string Thermostat1 = "thermostat1";
+ private const string Thermostat2 = "thermostat2";
+ private const string SerialNumber = "SR-123456";
+
+ private static readonly Random s_random = new Random();
+
+ private readonly DeviceClient _deviceClient;
+ //private readonly ILogger _logger;
+
+ // Dictionary to hold the temperature updates sent over each "Thermostat" component.
+ // NOTE: Memory constrained devices should leverage storage capabilities of an external service to store this
+ // information and perform computation.
+ // See https://docs.microsoft.com/en-us/azure/event-grid/compare-messaging-services for more details.
+ private readonly Dictionary<string, Dictionary<DateTimeOffset, double>> _temperatureReadingsDateTimeOffset =
+ new Dictionary<string, Dictionary<DateTimeOffset, double>>();
+
+ // A dictionary to hold all desired property change callbacks that this pnp device should be able to handle.
+ // The key for this dictionary is the componentName.
+ private readonly IDictionary<string, DesiredPropertyUpdateCallback> _desiredPropertyUpdateCallbacks =
+ new Dictionary<string, DesiredPropertyUpdateCallback>();
+
+ // Dictionary to hold the current temperature for each "Thermostat" component.
+ private readonly Dictionary<string, double> _temperature = new Dictionary<string, double>();
+
+ // Dictionary to hold the max temperature since last reboot, for each "Thermostat" component.
+ private readonly Dictionary<string, double> _maxTemp = new Dictionary<string, double>();
+
+ // A safe initial value for caching the writable properties version is 1, so the client
+ // will process all previous property change requests and initialize the device application
+ // after which this version will be updated to that, so we have a high water mark of which version number
+ // has been processed.
+ private static long s_localWritablePropertiesVersion = 1;
+
+ public TemperatureControllerSample(DeviceClient deviceClient)
+ {
+ _deviceClient = deviceClient ?? throw new ArgumentNullException(nameof(deviceClient));
+ //_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task InitOperationsAsync(CancellationToken cancellationToken)
+ {
+ _deviceClient.SetConnectionStatusChangesHandler(async (status, reason) =>
+ {
+ //_logger.LogDebug($"Connection status change registered - status={status}, reason={reason}.");
+
+ // Call GetWritablePropertiesAndHandleChangesAsync() to get writable properties from the server once the connection status changes into Connected.
+ // This can get back "lost" property updates in a device reconnection from status Disconnected_Retrying or Disconnected.
+ if (status == ConnectionStatus.Connected) {
+ await GetWritablePropertiesAndHandleChangesAsync();
+ }
+ });
+
+ //_logger.LogDebug("Set handler for 'reboot' command.");
+ await _deviceClient.SetMethodHandlerAsync("reboot", HandleRebootCommandAsync, _deviceClient, cancellationToken);
+
+ // For a component-level command, the command name is in the format "<component-name>*<command-name>".
+ //_logger.LogDebug($"Set handler for \"getMaxMinReport\" command.");
+ await _deviceClient.SetMethodHandlerAsync("thermostat1*getMaxMinReport", HandleMaxMinReportCommand, Thermostat1, cancellationToken);
+ await _deviceClient.SetMethodHandlerAsync("thermostat2*getMaxMinReport", HandleMaxMinReportCommand, Thermostat2, cancellationToken);
+
+ //_logger.LogDebug("Set handler to receive 'targetTemperature' updates.");
+ await _deviceClient.SetDesiredPropertyUpdateCallbackAsync(SetDesiredPropertyUpdateCallback, null, cancellationToken);
+ _desiredPropertyUpdateCallbacks.Add(Thermostat1, TargetTemperatureUpdateCallbackAsync);
+ _desiredPropertyUpdateCallbacks.Add(Thermostat2, TargetTemperatureUpdateCallbackAsync);
+
+ //_logger.LogDebug("For each component, check if the device properties are empty on the initial startup.");
+ await CheckEmptyPropertiesAsync(Thermostat1, cancellationToken);
+ await CheckEmptyPropertiesAsync(Thermostat2, cancellationToken);
+
+ await UpdateDeviceInformationAsync(cancellationToken);
+ await SendDeviceSerialNumberAsync(cancellationToken);
+
+ _maxTemp[Thermostat1] = 0d;
+ _maxTemp[Thermostat2] = 0d;
+ }
+
+ public async Task PerformOperationsAsync(CancellationToken cancellationToken)
+ {
+ // This sample follows the following workflow:
+ // -> Set handler to receive and respond to connection status changes.
+ // -> Set handler to receive "reboot" command - root interface.
+ // -> Set handler to receive "getMaxMinReport" command - on "Thermostat" components.
+ // -> Set handler to receive "targetTemperature" property updates from service - on "Thermostat" components.
+ // -> Check if the properties are empty on the initial startup - for each "Thermostat" component. If so, report the default values with ACK to the hub.
+ // -> Update device information on "deviceInformation" component.
+ // -> Send initial device info - "workingSet" over telemetry, "serialNumber" over reported property update - root interface.
+ // -> Periodically send "temperature" over telemetry - on "Thermostat" components.
+ // -> Send "maxTempSinceLastReboot" over property update, when a new max temperature is set - on "Thermostat" components.
+
+
+
+ //while (!cancellationToken.IsCancellationRequested)
+ {
+ //if (temperatureReset)
+ //{
+ // // Generate a random value between 5.0°C and 45.0°C for the current temperature reading for each "Thermostat" component.
+ // _temperature[Thermostat1] = Math.Round(s_random.NextDouble() * 40.0 + 5.0, 1);
+ // _temperature[Thermostat2] = Math.Round(s_random.NextDouble() * 40.0 + 5.0, 1);
+ //}
+
+ await SendTemperatureAsync(Thermostat1, cancellationToken);
+ //await SendTemperatureAsync(Thermostat2, cancellationToken);
+ //await SendDeviceMemoryAsync(cancellationToken);
+
+ //temperatureReset = _temperature[Thermostat1] == 0 && _temperature[Thermostat2] == 0;
+ await Task.Delay(5 * 1000, cancellationToken);
+ }
+ }
+
+ private async Task GetWritablePropertiesAndHandleChangesAsync()
+ {
+ Twin twin = await _deviceClient.GetTwinAsync();
+ //_logger.LogInformation($"Device retrieving twin values on CONNECT: {twin.ToJson()}");
+
+ TwinCollection twinCollection = twin.Properties.Desired;
+ long serverWritablePropertiesVersion = twinCollection.Version;
+
+ // Check if the writable property version is outdated on the local side.
+ // For the purpose of this sample, we'll only check the writable property versions between local and server
+ // side without comparing the property values.
+ if (serverWritablePropertiesVersion > s_localWritablePropertiesVersion)
+ {
+ //_logger.LogInformation($"The writable property version cached on local is changing " +
+ //$"from {s_localWritablePropertiesVersion} to {serverWritablePropertiesVersion}.");
+
+ foreach (KeyValuePair<string, object> propertyUpdate in twinCollection)
+ {
+ string componentName = propertyUpdate.Key;
+ switch (componentName)
+ {
+ case Thermostat1:
+ case Thermostat2:
+ // This will be called when a device client gets initialized and the _temperature dictionary is still empty.
+ if (!_temperature.TryGetValue(componentName, out double value))
+ {
+ _temperature[componentName] = 21d; // The default temperature value is 21°C.
+ }
+ await TargetTemperatureUpdateCallbackAsync(twinCollection, componentName);
+ break;
+
+ default:
+ //_logger.LogWarning($"Property: Received an unrecognized property update from service:" +
+ //$"\n[ {propertyUpdate.Key}: {propertyUpdate.Value} ].");
+ break;
+ }
+ }
+
+ //_logger.LogInformation($"The writable property version on local is currently {s_localWritablePropertiesVersion}.");
+ }
+ }
+
+ // The callback to handle "reboot" command. This method will send a temperature update (of 0°C) over telemetry for both associated components.
+ private async Task<MethodResponse> HandleRebootCommandAsync(MethodRequest request, object userContext)
+ {
+ try
+ {
+ int delay = JsonConvert.DeserializeObject<int>(request.DataAsJson);
+
+ //_logger.LogDebug($"Command: Received - Rebooting thermostat (resetting temperature reading to 0°C after {delay} seconds).");
+ await Task.Delay(delay * 1000);
+
+ //_logger.LogDebug("\tRebooting...");
+
+ _temperature[Thermostat1] = _maxTemp[Thermostat1] = 0;
+ _temperature[Thermostat2] = _maxTemp[Thermostat2] = 0;
+
+ _temperatureReadingsDateTimeOffset.Clear();
+
+ //_logger.LogDebug("\tRestored.");
+ } catch (JsonReaderException /*ex*/)
+ {
+ //_logger.LogDebug($"Command input is invalid: {ex.Message}.");
+ return new MethodResponse((int)StatusCode.BadRequest);
+ }
+
+ return new MethodResponse((int)StatusCode.Completed);
+ }
+
+ // The callback to handle "getMaxMinReport" command. This method will returns the max, min and average temperature from the
+ // specified time to the current time.
+ private Task<MethodResponse> HandleMaxMinReportCommand(MethodRequest request, object userContext)
+ {
+ try
+ {
+ string componentName = (string)userContext;
+ DateTime sinceInUtc = JsonConvert.DeserializeObject<DateTime>(request.DataAsJson);
+ var sinceInDateTimeOffset = new DateTimeOffset(sinceInUtc);
+
+ if (_temperatureReadingsDateTimeOffset.ContainsKey(componentName))
+ {
+ //_logger.LogDebug($"Command: Received - component=\"{componentName}\", generating max, min and avg temperature " +
+ //$"report since {sinceInDateTimeOffset.LocalDateTime}.");
+
+ Dictionary<DateTimeOffset, double> allReadings = _temperatureReadingsDateTimeOffset[componentName];
+ Dictionary<DateTimeOffset, double> filteredReadings = allReadings.Where(i => i.Key > sinceInDateTimeOffset)
+ .ToDictionary(i => i.Key, i => i.Value);
+
+ if (filteredReadings != null && filteredReadings.Any())
+ {
+ var report = new
+ {
+ maxTemp = filteredReadings.Values.Max<double>(),
+ minTemp = filteredReadings.Values.Min<double>(),
+ avgTemp = filteredReadings.Values.Average(),
+ startTime = filteredReadings.Keys.Min(),
+ endTime = filteredReadings.Keys.Max(),
+ };
+
+ //_logger.LogDebug($"Command: component=\"{componentName}\", MaxMinReport since {sinceInDateTimeOffset.LocalDateTime}:" +
+ // $" maxTemp={report.maxTemp}, minTemp={report.minTemp}, avgTemp={report.avgTemp}, startTime={report.startTime.LocalDateTime}, " +
+ // $"endTime={report.endTime.LocalDateTime}");
+
+ byte[] responsePayload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(report));
+ return Task.FromResult(new MethodResponse(responsePayload, (int)StatusCode.Completed));
+ }
+
+ //_logger.LogDebug($"Command: component=\"{componentName}\", no relevant readings found since {sinceInDateTimeOffset.LocalDateTime}, " +
+ // $"cannot generate any report.");
+ return Task.FromResult(new MethodResponse((int)StatusCode.NotFound));
+ }
+
+ //_logger.LogDebug($"Command: component=\"{componentName}\", no temperature readings sent yet, cannot generate any report.");
+ return Task.FromResult(new MethodResponse((int)StatusCode.NotFound));
+ }
+ catch (JsonReaderException /*ex*/)
+ {
+ //_logger.LogDebug($"Command input is invalid: {ex.Message}.");
+ return Task.FromResult(new MethodResponse((int)StatusCode.BadRequest));
+ }
+ }
+
+ private Task SetDesiredPropertyUpdateCallback(TwinCollection desiredProperties, object userContext)
+ {
+ bool callbackNotInvoked = true;
+
+ foreach (KeyValuePair<string, object> propertyUpdate in desiredProperties)
+ {
+ string componentName = propertyUpdate.Key;
+ if (_desiredPropertyUpdateCallbacks.ContainsKey(componentName))
+ {
+ _desiredPropertyUpdateCallbacks[componentName]?.Invoke(desiredProperties, componentName);
+ callbackNotInvoked = false;
+ }
+ }
+
+ if (callbackNotInvoked)
+ {
+ //_logger.LogDebug($"Property: Received a property update that is not implemented by any associated component.");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ // The desired property update callback, which receives the target temperature as a desired property update,
+ // and updates the current temperature value over telemetry and property update.
+ private async Task TargetTemperatureUpdateCallbackAsync(TwinCollection desiredProperties, object userContext)
+ {
+ string componentName = (string)userContext;
+
+ bool targetTempUpdateReceived = PnpConvention.TryGetPropertyFromTwin(
+ desiredProperties,
+ TargetTemperatureProperty,
+ out double targetTemperature,
+ componentName);
+ if (!targetTempUpdateReceived)
+ {
+ //_logger.LogDebug($"Property: Update - component=\"{componentName}\", received an update which is not associated with a valid property.\n{desiredProperties.ToJson()}");
+ return;
+ }
+
+ //_logger.LogDebug($"Property: Received - component=\"{componentName}\", {{ \"{TargetTemperatureProperty}\": {targetTemperature}°C }}.");
+
+ s_localWritablePropertiesVersion = desiredProperties.Version;
+
+ TwinCollection pendingReportedProperty = PnpConvention.CreateComponentWritablePropertyResponse(
+ componentName,
+ TargetTemperatureProperty,
+ targetTemperature,
+ (int)StatusCode.InProgress,
+ desiredProperties.Version,
+ "In progress - reporting current temperature");
+
+ await _deviceClient.UpdateReportedPropertiesAsync(pendingReportedProperty);
+ //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{\"{TargetTemperatureProperty}\": {targetTemperature} }} in °C is {StatusCode.InProgress}.");
+
+ // Update Temperature in 2 steps
+ double step = (targetTemperature - _temperature[componentName]) / 2d;
+ for (int i = 1; i <= 2; i++)
+ {
+ _temperature[componentName] = Math.Round(_temperature[componentName] + step, 1);
+ await Task.Delay(6 * 1000);
+ }
+
+ TwinCollection completedReportedProperty = PnpConvention.CreateComponentWritablePropertyResponse(
+ componentName,
+ TargetTemperatureProperty,
+ _temperature[componentName],
+ (int)StatusCode.Completed,
+ desiredProperties.Version,
+ "Successfully updated target temperature");
+
+ await _deviceClient.UpdateReportedPropertiesAsync(completedReportedProperty);
+ //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{\"{TargetTemperatureProperty}\": {_temperature[componentName]} }} in °C is {StatusCode.Completed}");
+ }
+
+ // Report the property updates on "deviceInformation" component.
+ private async Task UpdateDeviceInformationAsync(CancellationToken cancellationToken)
+ {
+ const string componentName = "deviceInformation";
+
+ TwinCollection deviceInfoTc = PnpConvention.CreateComponentPropertyPatch(
+ componentName,
+ new Dictionary<string, object>
+ {
+ { "manufacturer", "element15" },
+ { "model", "ModelIDxcdvmk" },
+ { "swVersion", "1.0.0" },
+ { "osName", "Windows 10" },
+ { "processorArchitecture", "64-bit" },
+ { "processorManufacturer", "Intel" },
+ { "totalStorage", 256 },
+ { "totalMemory", 1024 },
+ });
+
+ await _deviceClient.UpdateReportedPropertiesAsync(deviceInfoTc, cancellationToken);
+ //_logger.LogDebug($"Property: Update - component = '{componentName}', properties update is complete.");
+ }
+
+ // Send working set of device memory over telemetry.
+ private async Task SendDeviceMemoryAsync(CancellationToken cancellationToken)
+ {
+ const string workingSetName = "workingSet";
+
+ long workingSet = Process.GetCurrentProcess().PrivateMemorySize64 / 1024;
+
+ var telemetry = new Dictionary<string, object>
+ {
+ { workingSetName, workingSet },
+ };
+
+ using Message msg = PnpConvention.CreateMessage(telemetry);
+
+ await _deviceClient.SendEventAsync(msg, cancellationToken);
+ //_logger.LogDebug($"Telemetry: Sent - {JsonConvert.SerializeObject(telemetry)} in KB.");
+ }
+
+ // Send device serial number over property update.
+ private async Task SendDeviceSerialNumberAsync(CancellationToken cancellationToken)
+ {
+ const string propertyName = "serialNumber";
+ TwinCollection reportedProperties = PnpConvention.CreatePropertyPatch(propertyName, SerialNumber);
+
+ await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken);
+ //var oBrace = '{';
+ //var cBrace = '}';
+ //_logger.LogDebug($"Property: Update - {oBrace} \"{propertyName}\": \"{SerialNumber}\" {cBrace} is complete.");
+ }
+
+ private async Task SendTemperatureAsync(string componentName, CancellationToken cancellationToken)
+ {
+ await SendTemperatureTelemetryAsync(componentName, cancellationToken);
+
+ double maxTemp = _temperatureReadingsDateTimeOffset[componentName].Values.Max<double>();
+ if (maxTemp > _maxTemp[componentName])
+ {
+ _maxTemp[componentName] = maxTemp;
+ await UpdateMaxTemperatureSinceLastRebootAsync(componentName, cancellationToken);
+ }
+ }
+
+ private async Task SendTemperatureTelemetryAsync(string componentName, CancellationToken cancellationToken)
+ {
+ //const string telemetryName = "temperature";
+ double currentTemperature = _temperature[componentName];
+ using Message msg = PnpConvention.CreateMessage(Telemetry, /*telemetryName, currentTemperature, */componentName);
+ await _deviceClient.SendEventAsync(msg, cancellationToken);
+
+ //_logger.LogDebug($"Telemetry: Sent - component=\"{componentName}\", {{ \"{telemetryName}\": {currentTemperature} }} in °C.");
+
+ if (_temperatureReadingsDateTimeOffset.ContainsKey(componentName))
+ {
+ _temperatureReadingsDateTimeOffset[componentName].TryAdd(DateTimeOffset.UtcNow, currentTemperature);
+ }
+ else
+ {
+ _temperatureReadingsDateTimeOffset.TryAdd(
+ componentName,
+ new Dictionary<DateTimeOffset, double>
+ {
+ { DateTimeOffset.UtcNow, currentTemperature },
+ });
+ }
+ }
+
+ private async Task UpdateMaxTemperatureSinceLastRebootAsync(string componentName, CancellationToken cancellationToken)
+ {
+ const string propertyName = "maxTempSinceLastReboot";
+ double maxTemp = _maxTemp[componentName];
+ TwinCollection reportedProperties = PnpConvention.CreateComponentPropertyPatch(componentName, propertyName, maxTemp);
+
+ await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken);
+ //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{ \"{propertyName}\": {maxTemp} }} in °C is complete.");
+ }
+
+ private async Task CheckEmptyPropertiesAsync(string componentName, CancellationToken cancellationToken)
+ {
+ Twin twin = await _deviceClient.GetTwinAsync(cancellationToken);
+ TwinCollection writableProperty = twin.Properties.Desired;
+ TwinCollection reportedProperty = twin.Properties.Reported;
+
+ // Check if the device properties (both writable and reported) for the current component are empty.
+ if (!writableProperty.Contains(componentName) && !reportedProperty.Contains(componentName))
+ {
+ await ReportInitialPropertyAsync(componentName, TargetTemperatureProperty, cancellationToken);
+ }
+ }
+
+ private async Task ReportInitialPropertyAsync(string componentName, string propertyName, CancellationToken cancellationToken)
+ {
+ // If the device properties are empty, report the default value with ACK(ac=203, av=0) as part of the PnP convention.
+ // "DefaultPropertyValue" is set from the device when the desired property is not set via the hub.
+ TwinCollection reportedProperties = PnpConvention.CreateComponentWritablePropertyResponse(
+ componentName,
+ propertyName,
+ DefaultPropertyValue,
+ (int)StatusCode.ReportDeviceInitialProperty,
+ DefaultAckVersion,
+ "Initialized with default value");
+
+ await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken);
+
+ //_logger.LogDebug($"Report the default values for \"{componentName}\".\nProperty: Update - {reportedProperties.ToJson()} is complete.");
+ }
+ }
+}
diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs
new file mode 100644
index 0000000..3cbfb64
--- /dev/null
+++ b/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Newtonsoft.Json;
+
+namespace PnpHelpers
+{
+ /// <summary>
+ /// The payload for a property update response.
+ /// </summary>
+ public class WritablePropertyResponse
+ {
+ /// <summary>
+ /// Empty constructor.
+ /// </summary>
+ public WritablePropertyResponse() { }
+
+ /// <summary>
+ /// Convenience constructor for specifying the properties.
+ /// </summary>
+ /// <param name="propertyValue">The unserialized property value.</param>
+ /// <param name="ackCode">The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.</param>
+ /// <param name="ackVersion">The acknowledgment version, as supplied in the property update request.</param>
+ /// <param name="ackDescription">The acknowledgment description, an optional, human-readable message about the result of the property update.</param>
+ public WritablePropertyResponse(object propertyValue, int ackCode, long ackVersion, string ackDescription = null)
+ {
+ PropertyValue = propertyValue;
+ AckCode = ackCode;
+ AckVersion = ackVersion;
+ AckDescription = ackDescription;
+ }
+
+ /// <summary>
+ /// The unserialized property value.
+ /// </summary>
+ [JsonProperty("value")]
+ public object PropertyValue { get; set; }
+
+ /// <summary>
+ /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400.
+ /// </summary>
+ [JsonProperty("ac")]
+ public int AckCode { get; set; }
+
+ /// <summary>
+ /// The acknowledgment version, as supplied in the property update request.
+ /// </summary>
+ [JsonProperty("av")]
+ public long AckVersion { get; set; }
+
+ /// <summary>
+ /// The acknowledgment description, an optional, human-readable message about the result of the property update.
+ /// </summary>
+ [JsonProperty("ad", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public string AckDescription { get; set; }
+ }
+}
diff --git a/examples/QtAzureIoT/device/SensorData/SensorData.cs b/examples/QtAzureIoT/device/SensorData/SensorData.cs
new file mode 100644
index 0000000..c58c5c1
--- /dev/null
+++ b/examples/QtAzureIoT/device/SensorData/SensorData.cs
@@ -0,0 +1,107 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+using System.Diagnostics;
+using System.Device.I2c;
+using Iot.Device.Bmxx80;
+using Iot.Device.Bmxx80.PowerMode;
+using QtAzureIoT.Utils;
+
+namespace QtAzureIoT.Device
+{
+ public class SensorData : PropertySet, IDisposable
+ {
+ public SensorData()
+ { }
+
+ public double Temperature
+ {
+ get => propertyTemperature;
+ private set => SetProperty(ref propertyTemperature, value, nameof(Temperature));
+ }
+
+ public double Pressure
+ {
+ get => propertyPressure;
+ private set => SetProperty(ref propertyPressure, value, nameof(Pressure));
+ }
+
+ public double Humidity
+ {
+ get => propertyHumidity;
+ private set => SetProperty(ref propertyHumidity, value, nameof(Humidity));
+ }
+
+ public void StartPolling()
+ {
+ if (Sensor != null)
+ return;
+ BusConnectionSettings = new I2cConnectionSettings(1, Bme280.DefaultI2cAddress);
+ BusDevice = I2cDevice.Create(BusConnectionSettings);
+ Sensor = new Bme280(BusDevice);
+ MesasuramentDelay = Sensor.GetMeasurementDuration();
+
+ PollingLoop = new CancellationTokenSource();
+ Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token);
+ Polling.Start();
+ }
+
+ public void StopPolling()
+ {
+ if (Sensor == null)
+ return;
+ PollingLoop.Cancel();
+ Polling.Wait();
+ Sensor.Dispose();
+ Sensor = null;
+ BusDevice.Dispose();
+ BusDevice = null;
+ BusConnectionSettings = null;
+ PollingLoop.Dispose();
+ PollingLoop = null;
+ Polling.Dispose();
+ Polling = null;
+ }
+
+ #region private
+ private I2cConnectionSettings BusConnectionSettings { get; set; }
+ private I2cDevice BusDevice { get; set; }
+ private Bme280 Sensor { get; set; }
+ int MesasuramentDelay { get; set; }
+ private CancellationTokenSource PollingLoop { get; set; }
+ private Task Polling { get; set; }
+
+
+ private async Task PollingLoopAsync()
+ {
+ while (!PollingLoop.IsCancellationRequested) {
+ try {
+ Sensor.SetPowerMode(Bmx280PowerMode.Forced);
+ await Task.Delay(MesasuramentDelay);
+
+ if (Sensor.TryReadTemperature(out var tempValue))
+ Temperature = tempValue.DegreesCelsius;
+ if (Sensor.TryReadPressure(out var preValue))
+ Pressure = preValue.Hectopascals;
+ if (Sensor.TryReadHumidity(out var humValue))
+ Humidity = humValue.Percent;
+ } catch (Exception e) {
+ Debug.WriteLine($"Exception: {e.GetType().Name}: {e.Message}");
+ }
+ await Task.Delay(1000);
+ }
+ }
+
+ public void Dispose()
+ {
+ StopPolling();
+ }
+
+ double propertyTemperature;
+ double propertyPressure;
+ double propertyHumidity;
+ #endregion
+ }
+}
diff --git a/examples/QtAzureIoT/device/SensorData/SensorData.csproj b/examples/QtAzureIoT/device/SensorData/SensorData.csproj
new file mode 100644
index 0000000..c694f5b
--- /dev/null
+++ b/examples/QtAzureIoT/device/SensorData/SensorData.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Iot.Device.Bindings" Version="2.2.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\common\Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj
new file mode 100644
index 0000000..fb25e64
--- /dev/null
+++ b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>{2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}</ProjectGuid>
+ <Keyword>QtVS_v304</Keyword>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0.19041.0</WindowsTargetPlatformVersion>
+ <QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ConfigurationType>Application</ConfigurationType>
+ <PlatformToolset>v143</PlatformToolset>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
+ <Import Project="$(QtMsBuild)\qt_defaults.props" />
+ </ImportGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
+ <QtInstall>6.2.7_msvc2019_64</QtInstall>
+ <QtModules>core;quick</QtModules>
+ <QtBuildConfig>debug</QtBuildConfig>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
+ <QtInstall>6.2.7_msvc2019_64</QtInstall>
+ <QtModules>core;quick</QtModules>
+ <QtBuildConfig>release</QtBuildConfig>
+ </PropertyGroup>
+ <Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
+ <Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
+ </Target>
+ <ImportGroup Label="ExtensionSettings" />
+ <ImportGroup Label="Shared" />
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ <Import Project="$(QtMsBuild)\Qt.props" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ <IncludePath>C:\dev\source\qt-labs\qtdotnet\include;$(IncludePath)</IncludePath>
+ <OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
+ <IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
+ <CopyLocalProjectReference>true</CopyLocalProjectReference>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ <IncludePath>C:\dev\source\qt-labs\qtdotnet\include;$(IncludePath)</IncludePath>
+ <OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
+ <IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
+ <CopyLocalProjectReference>true</CopyLocalProjectReference>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+ <Optimization>Disabled</Optimization>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
+ <ClCompile>
+ <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+ <MultiProcessorCompilation>true</MultiProcessorCompilation>
+ <DebugInformationFormat>None</DebugInformationFormat>
+ <Optimization>MaxSpeed</Optimization>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>false</GenerateDebugInformation>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <QtMoc Include="main.cpp">
+ <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
+ <QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
+ <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
+ <QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
+ </QtMoc>
+ <QtRcc Include="qml.qrc" />
+ <CopyFileToFolders Include="..\..\..\qtdotnet\bin\QtVsTools.QtDotNet.Adapter.dll">
+ <FileType>Document</FileType>
+ <TreatOutputAsContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatOutputAsContent>
+ <TreatOutputAsContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</TreatOutputAsContent>
+ </CopyFileToFolders>
+ <None Include="main.qml" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\CardReader\CardReader.csproj">
+ <Project>{66a69341-3b00-4812-aa77-ec5c2e9ea23a}</Project>
+ <ReferenceOutputAssembly>true</ReferenceOutputAssembly>
+ <LinkLibraryDependencies>false</LinkLibraryDependencies>
+ </ProjectReference>
+ <ProjectReference Include="..\SensorData\SensorData.csproj">
+ <Project>{d8cd7e8d-7eca-46a7-aa39-e471f99c94f0}</Project>
+ <ReferenceOutputAssembly>true</ReferenceOutputAssembly>
+ <LinkLibraryDependencies>false</LinkLibraryDependencies>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
+ <Import Project="$(QtMsBuild)\qt.targets" />
+ </ImportGroup>
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters
new file mode 100644
index 0000000..4965748
--- /dev/null
+++ b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <Filter Include="Source Files">
+ <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+ <Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+ </Filter>
+ <Filter Include="Header Files">
+ <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+ <Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
+ </Filter>
+ <Filter Include="Resource Files">
+ <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+ <Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+ </Filter>
+ <Filter Include="Form Files">
+ <UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
+ <Extensions>ui</Extensions>
+ </Filter>
+ <Filter Include="Translation Files">
+ <UniqueIdentifier>{639EADAA-A684-42e4-A9AD-28FC9BCB8F7C}</UniqueIdentifier>
+ <Extensions>ts</Extensions>
+ </Filter>
+ </ItemGroup>
+ <ItemGroup>
+ <QtRcc Include="qml.qrc">
+ <Filter>Resource Files</Filter>
+ </QtRcc>
+ <None Include="main.qml">
+ <Filter>Source Files</Filter>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <CopyFileToFolders Include="..\..\..\qtdotnet\bin\QtVsTools.QtDotNet.Adapter.dll" />
+ </ItemGroup>
+ <ItemGroup>
+ <QtMoc Include="main.cpp">
+ <Filter>Source Files</Filter>
+ </QtMoc>
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/examples/QtAzureIoT/device/deviceapp/main.cpp b/examples/QtAzureIoT/device/deviceapp/main.cpp
new file mode 100644
index 0000000..ba049d1
--- /dev/null
+++ b/examples/QtAzureIoT/device/deviceapp/main.cpp
@@ -0,0 +1,183 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+#include <QtGui/QGuiApplication>
+#include <QtQml/QQmlApplicationEngine>
+#include <QtQml/QQmlContext>
+
+#include <qdotnetobject.h>
+#include <qdotnetevent.h>
+
+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);
+ }
+ void setTelemetry(QString name, bool value)
+ {
+ getMethod("SetTelemetry", fnSetTelemetryBool).invoke(*this, name, value);
+ }
+public:
+ void startPolling() {
+ getMethod("StartPolling", fnStartPolling).invoke(*this);
+ }
+ void stopPolling() {
+ getMethod("StopPolling", fnStopPolling).invoke(*this);
+ }
+private:
+ mutable QDotNetFunction<void, QString, double> fnSetTelemetryDouble;
+ mutable QDotNetFunction<void, QString, bool> fnSetTelemetryBool;
+ mutable QDotNetFunction<void> fnStartPolling;
+ mutable QDotNetFunction<void> fnStopPolling;
+};
+
+class SensorData : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler
+{
+ Q_OBJECT
+ Q_PROPERTY(double temperature READ temperature NOTIFY temperatureChanged)
+ Q_PROPERTY(double pressure READ pressure NOTIFY pressureChanged)
+ Q_PROPERTY(double humidity READ humidity NOTIFY humidityChanged)
+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);
+ }
+ double pressure() const
+ {
+ return getMethod("get_Pressure", fnGet_Pressure).invoke(*this);
+ }
+ double humidity() const
+ {
+ return getMethod("get_Humidity", fnGet_Humidity).invoke(*this);
+ }
+public slots:
+ void startPolling() {
+ getMethod("StartPolling", fnStartPolling).invoke(*this);
+ }
+ void stopPolling() {
+ getMethod("StopPolling", fnStopPolling).invoke(*this);
+ }
+signals:
+ void temperatureChanged();
+ void pressureChanged();
+ void humidityChanged();
+private:
+ void handleEvent(const QString& evName, QDotNetObject& evSrc, QDotNetObject& evArgs) override
+ {
+ if (evName == "PropertyChanged") {
+ if (evArgs.type().fullName() == QDotNetPropertyEvent::FullyQualifiedTypeName) {
+ auto propertyChangedEvent = evArgs.cast<QDotNetPropertyEvent>();
+ if (propertyChangedEvent.propertyName() == "Temperature")
+ emit temperatureChanged();
+ else if (propertyChangedEvent.propertyName() == "Pressure")
+ emit pressureChanged();
+ else if (propertyChangedEvent.propertyName() == "Humidity")
+ emit humidityChanged();
+ }
+ }
+ }
+ mutable QDotNetFunction<double> fnGet_Temperature;
+ mutable QDotNetFunction<double> fnGet_Pressure;
+ mutable QDotNetFunction<double> fnGet_Humidity;
+ mutable QDotNetFunction<void> fnStartPolling;
+ mutable QDotNetFunction<void> fnStopPolling;
+};
+
+class CardReader : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler
+{
+ Q_OBJECT
+ Q_PROPERTY(bool cardInReader READ cardInReader NOTIFY cardInReaderChanged)
+public:
+ Q_DOTNET_OBJECT_INLINE(CardReader, "QtAzureIoT.Device.CardReader, CardReader");
+ CardReader() : QDotNetObject(getConstructor<CardReader>().invoke(nullptr))
+ {
+ subscribeEvent("PropertyChanged", this);
+ }
+ bool cardInReader() const
+ {
+ return getMethod("get_CardInReader", fnGet_CardInReader).invoke(*this);
+ }
+public slots:
+ void startPolling() {
+ getMethod("StartPolling", fnStartPolling).invoke(*this);
+ }
+ void stopPolling() {
+ getMethod("StopPolling", fnStopPolling).invoke(*this);
+ }
+signals:
+ void cardInReaderChanged();
+private:
+ void handleEvent(const QString& evName, QDotNetObject& evSrc, QDotNetObject& evArgs) override
+ {
+ if (evName == "PropertyChanged") {
+ if (evArgs.type().fullName() == QDotNetPropertyEvent::FullyQualifiedTypeName) {
+ auto propertyChangedEvent = evArgs.cast<QDotNetPropertyEvent>();
+ if (propertyChangedEvent.propertyName() == "CardInReader")
+ emit cardInReaderChanged();
+ }
+ }
+ }
+ mutable QDotNetFunction<bool> fnGet_CardInReader;
+ mutable QDotNetFunction<void> fnStartPolling;
+ mutable QDotNetFunction<void> fnStopPolling;
+};
+
+int main(int argc, char* argv[])
+{
+ QGuiApplication app(argc, argv);
+ QQmlApplicationEngine engine;
+
+ CardReader card;
+ card.startPolling();
+ engine.rootContext()->setContextProperty("card", &card);
+
+ SensorData sensor;
+ sensor.startPolling();
+ engine.rootContext()->setContextProperty("sensor", &sensor);
+
+ Backoffice backoffice;
+ QObject::connect(&card, &CardReader::cardInReaderChanged,
+ [&backoffice, &card]()
+ {
+ backoffice.setTelemetry("card", card.cardInReader());
+ });
+
+ QObject::connect(&sensor, &SensorData::temperatureChanged,
+ [&backoffice, &sensor]()
+ {
+ backoffice.setTelemetry("temperature", sensor.temperature());
+ });
+
+ QObject::connect(&sensor, &SensorData::pressureChanged,
+ [&backoffice, &sensor]()
+ {
+ backoffice.setTelemetry("pressure", sensor.pressure());
+ });
+
+ QObject::connect(&sensor, &SensorData::humidityChanged,
+ [&backoffice, &sensor]()
+ {
+ backoffice.setTelemetry("humidity", sensor.humidity());
+ });
+ backoffice.startPolling();
+
+ engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+ if (engine.rootObjects().isEmpty())
+ return -1;
+
+ return app.exec();
+}
+
+#include "main.moc"
diff --git a/examples/QtAzureIoT/device/deviceapp/main.qml b/examples/QtAzureIoT/device/deviceapp/main.qml
new file mode 100644
index 0000000..368ce0c
--- /dev/null
+++ b/examples/QtAzureIoT/device/deviceapp/main.qml
@@ -0,0 +1,56 @@
+/***************************************************************************************************
+ Copyright (C) 2023 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
+***************************************************************************************************/
+
+import QtQml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+
+Window {
+ visible: true
+ width: 800
+ height: 480
+ flags: Qt.FramelessWindowHint
+ color: "black"
+ GridLayout {
+ anchors.fill: parent
+ columns: 2
+ Text {
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter
+ font { bold: true; pointSize: 32 }
+ color: "white"
+ text: sensor.temperature.toFixed(2) + " deg.C."
+ }
+ Text {
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter
+ font { bold: true; pointSize: 32 }
+ color: "white"
+ text: card.cardInReader ? "CARD DETECTED" : "No card";
+ }
+ Text {
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter
+ font { bold: true; pointSize: 32 }
+ color: "white"
+ text: sensor.pressure.toFixed(2) + " hPa."
+ }
+ Text {
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter
+ font { bold: true; pointSize: 32 }
+ color: "white"
+ text: sensor.humidity.toFixed(2) + " %"
+ }
+ }
+ Button {
+ text: "EXIT"
+ onClicked: Qt.exit(0)
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ }
+}
diff --git a/examples/QtAzureIoT/device/deviceapp/qml.qrc b/examples/QtAzureIoT/device/deviceapp/qml.qrc
new file mode 100644
index 0000000..5f6483a
--- /dev/null
+++ b/examples/QtAzureIoT/device/deviceapp/qml.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ </qresource>
+</RCC>