Sep 30, 2020
Comments
In part 1 of this post we showed how to create a multi-platform Qt Quick application project in Visual Studio targeting Windows and Embedded Linux. We will now show how to run the application on an embedded device. We will then continue developing the project into the full embedded application that we set out to create. Finally, we will use the VS debugger to remotely debug the application's C++ and QML code.
We've already shown how to cross-compile a "hello world" Qt Quick application created in Visual Studio. We will now see how to run that application on the Raspberry Pi. As we will be running in full-screen mode, we must first add some content to the application window.
Window {
visible: true
title: qsTr("Hello World")
Text {
id: clock
font.pointSize: 72
Timer {
interval: 1000; running: true; repeat: true
onTriggered: clock.text = (new Date).toLocaleTimeString(Qt.locale("de_DE"), "hh:mm:ss");
}
}
}
Qt Quick "Hello World"
As before, select a Linux project configuration and press F7 to start the cross-compilation.
1>------ Build started: Project: QuickMirror, Configuration: Debug_RPi x64 ------
1>rcc qml.qrc
1>Invoking 'mkdir -p $(dirname qml.qrc); mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/main.qml); mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Debug_RPi/rcc/qrc_qml.cpp); (/home/user/raspi/qt5/bin/rcc /mnt/c/Users/user/Source/Repos/QuickMirror/qml.qrc --name qml -o /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Debug_RPi/rcc/qrc_qml.cpp)', working directory: '/mnt/c/Users/user/Source/Repos/QuickMirror'
1>Starting remote build
1>Compiling sources:
1>qrc_qml.cpp
1>Linking objects
1>QuickMirror.vcxproj -> C:\Users\user\Source\Repos\QuickMirror\bin\x64\Debug_RPi\QuickMirror.out
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
Cross-compiling Qt project in VS
We will now upload the application binary to the Raspberry Pi. The build output window shows the location of the generated binary (highlighted above).
C:\Users\user> scp C:\Users\user\Source\Repos\QuickMirror\bin\x64\Debug_RPi\QuickMirror.out pi@192.168.1.98:/home/pi/
pi@192.168.1.98's password:
QuickMirror.out 100% 465KB 1.6MB/s 00:00
C:\Users\user>
Uploading application binary to target device
To automatically copy the application file at the end of each build, the following command can be set in the "WSL Post-Build Event" property page (ATTN: this will save the device password in clear-text).
curl --insecure --user pi:<password> -T /mnt/$(TargetPath.Replace('\','/').Replace(':','').ToLower()) scp://<device-addr>/home/pi/$(TargetFileName)
Copy binary to device at the end of every build
Before starting the Qt Quick application we need to setup some required environment variables:
LD_LIBRARY_PATHQT_QPA_PLATFORMQT_QPA_PLATFORM_PLUGIN_PATHQT_QPA_EGLFS_PHYSICAL_WIDTH
QT_QPA_EGLFS_PHYSICAL_HEIGHTQML2_IMPORT_PATH
pi@raspberry-pi:~$ export LD_LIBRARY_PATH="/usr/local/qt5pi/lib"
pi@raspberry-pi:~$ export QT_QPA_PLATFORM="eglfs"
pi@raspberry-pi:~$ export QT_QPA_PLATFORM_PLUGIN_PATH="/usr/local/qt5pi/plugins/platforms"
pi@raspberry-pi:~$ export QT_QPA_EGLFS_PHYSICAL_WIDTH="326"
pi@raspberry-pi:~$ export QT_QPA_EGLFS_PHYSICAL_HEIGHT="520"
pi@raspberry-pi:~$ export QML2_IMPORT_PATH="/usr/local/qt5pi/qml"
pi@raspberry-pi:~$ ./QuickMirror.out

Running "Hello World" application in the Raspberry Pi
The requirements of our application include displaying the following information:
We will encapsulate each of these items into a dedicated QML type. For that we must first add a QML Module Definition (qmldir) file to the project:

Adding a new QML module definition to the project
After pressing "Add", a qmldir file will become available in the project tree. We will use this file to define the mapping of each QML type to its corresponding source file.
ApiCall 1.0 QuickMirror.ApiCall.qml
Calendar 1.0 QuickMirror.Calendar.qml
Clock 1.0 QuickMirror.Clock.qml
NewsTicker 1.0 QuickMirror.NewsTicker.qml
OnThisDay 1.0 QuickMirror.OnThisDay.qml
PublicTransport 1.0 QuickMirror.PublicTransport.qml
Weather 1.0 QuickMirror.Weather.qml
Mapping QML types to source files
To add a new QML source file to the project:
qmldir file was created.
Adding a new QML file to the project
We will start by adding QML types for displaying the current time, current date and notable anniversaries. The Clock type will display the current time, refreshing every second.
Text {
font.family: FontFamily_Clock
font.styleName: FontStyle_Clock
font.pointSize: 144
color: "white"
renderType: Text.NativeRendering
antialiasing: false
function refresh() {
text = (new Date).toLocaleTimeString(Qt.locale("de_DE"), "hh:mm");
}
Component.onCompleted : refresh();
Timer {
interval: 1000; running: true; repeat: true onTriggered: parent.refresh();
}
}
Definition of the Clock QML type
The Calendar type will display the current date, cycling between various locales.
Text {
renderType: Text.NativeRendering
id: calendar
color: "white"
font.family: FontFamily_Bold
font.styleName: FontStyle_Bold
font.pointSize: 72
property var locales: ["en_US", "de_DE", "pt_PT"]
property var localeIdx: 0
function capitalize(s) {
return s.replace(/(^|-)./g, function(c) { return c.toUpperCase(); });
}
function setNextLocale() {
localeIdx = (localeIdx + 1) % locales.length;
}
function getCurrentText() {
var date = new Date;
var locale = Qt.locale(locales[localeIdx]);
var calendarText = capitalize(date.toLocaleDateString(locale, "dddd, dd"));
var monthShort = date.toLocaleDateString(locale, "MMM");
var monthLong = date.toLocaleDateString(locale, "MMMM");
if (monthLong.length <= 5) {
calendarText += capitalize(monthLong);
} else {
calendarText += capitalize(monthShort);
if (!monthShort.endsWith("."))
calendarText += ".";
}
calendarText += date.toLocaleDateString(locale, " yyyy");
return calendarText;
}
Component.onCompleted: {
text = getCurrentText();
}
Timer {
interval: 15000; running: true; repeat: true
onTriggered: {
setNextLocale();
text = getCurrentText();
}
}
Behavior on text {
SequentialAnimation {
NumberAnimation { target: calendar; property: "opacity"; to: 0.0; duration: 1000 }
PropertyAction { target: calendar; property: "text" }
NumberAnimation { target: calendar; property: "opacity"; to: 1.0; duration: 500 }
}
}
}
Definition of the Calendar QML type
Besides the date/time, our application will rely on Web API's for retrieving information. We will run curl in a separate process to connect to Web API's. The process creation will be handled by a C++ class named Process. The QML type ApiCall will then use a Process object to start curl with the necessary parameters and gather its output.
Item {
property var url: ""
property var path: []
property var query: []
signal response(var response)
signal error(var error)
Process {
id: curl
property var path: Q_OS_WIN ? "C:\\Windows\\System32\\curl.exe" : "/usr/bin/curl"
property var request: ""
command: path + " -s \"" + request + "\""
}
function sendRequest() {
curl.request = url;
if (path.length > 0)
curl.request += "/" + path.join("/");
if (query.length > 0)
curl.request += "?" + query.join("&");
curl.start();
}
Connections {
target: curl
onExit /*(int exitCode, QByteArray processOutput)*/ : {
if (exitCode != 0) {
console.log("ApiCall: exit " + exitCode);
console.log("==== ApiCall: request: " + curl.request);
return error("exit " + exitCode);
}
try {
return response(JSON.parse(processOutput));
} catch (err) {
console.log("ApiCall: error: " + err.toString());
console.log("==== ApiCall: request: " + curl.request);
console.log("==== ApiCall: response: " + processOutput);
return error(err);
}
}
}
}
Definition of the ApiCall QML type
To create the Process C++ class:

Adding a new Qt C++ class to the project
class Process : public QProcess
{
Q_OBJECT
Q_PROPERTY(QString command READ command WRITE setCommand NOTIFY commandChanged)
public:
Process(QObject* parent = 0);
~Process();
public:
Q_INVOKABLE void start();
void setCommand(const QString& cmd);
QString command() const;
signals:
void commandChanged();
void exit(int exitCode, QByteArray processOutput);
protected:
void onFinished(int exitCode, QProcess::ExitStatus status);
void onErrorOccurred(QProcess::ProcessError error);
private:
QString m_command;
};
Process(QObject* parent) : QProcess(parent)
{
connect(
this, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &Process::onFinished);
connect(
this, &QProcess::errorOccurred,
this, &Process::onErrorOccurred);
}
Process::~Process()
{
}
void Process::setCommand(const QString& cmd)
{
if (cmd != m_command) {
m_command = cmd;
emit commandChanged();
}
}
QString Process::command() const
{
return m_command;
}
void Process::start()
{
if (state() == ProcessState::NotRunning)
QProcess::start(m_command);
else
qInfo() << "==== QProcess: ERROR already running:" << m_command;
}
void Process::onFinished(int exitCode, QProcess::ExitStatus status)
{
emit exit((status == ExitStatus::NormalExit) ? exitCode : -1, readAll());
}
void Process::onErrorOccurred(QProcess::ProcessError error)
{
qInfo() << "==== QProcess: ERROR " << error;
}
int main(int argc, char* argv[])
{
qmlRegisterType<Process>("Process", 1, 0, "Process");
...
Definition of the Process class
The OnThisDay QML type will use an instance of ApiCall to retrieve a list of notable anniversaries and cycle through them every few seconds.
Item {
id: onThisDay
clip: true
property int viewportHeight
property var events: []
property var births: []
property var deaths: []
property int idxEventType: -1
ApiCall {
id: onThisDayApi
property int month: 0
property int day: 0
property string eventType: ""
url: "https://byabbe.se"; path: ["on-this-day", month, day, eventType + ".json" ]
onResponse: {
if ("events" in response) {
events = shuffle(response.events);
eventType = "births";
sendRequest();
} else if ("births" in response) {
births = shuffle(response.births);
for (var i in births)
births[i].year = "*" + births[i].year;
eventType = "deaths";
sendRequest();
} else if ("deaths" in response) {
deaths = shuffle(response.deaths);
for (var i in deaths)
deaths[i].year = "<sup>†</sup>" + deaths[i].year;
next();
}
}
}
function init() {
events = [];
births = [];
deaths = [];
idxEventType = -1;
var today = new Date;
onThisDayApi.month = today.getMonth() + 1;
onThisDayApi.day = today.getDate();
onThisDayApi.eventType = "events";
onThisDayApi.sendRequest();
}
function next() {
if (events.length + births.length + deaths.length == 0)
return;
var today = new Date;
if (onThisDayApi.month != today.getMonth() + 1 || onThisDayApi.day != today.getDate())
return init();
onThisDayText.color = "white";
idxEventType = (idxEventType + 1) % 3;
var event;
switch (idxEventType) {
case 0:
if (events.length == 0)
return next();
event = events.shift();
events = shuffle(events);
events.push(event);
break;
case 1:
if (births.length == 0)
return next();
event = births.shift();
births = shuffle(births);
births.push(event);
break;
case 2:
if (deaths.length == 0)
return next();
event = deaths.shift();
deaths = shuffle(deaths);
deaths.push(event);
break;
}
onThisDayText.text = event.year + " – " + event.description;
showText.start();
}
Component.onCompleted: {
init();
}
Timer {
id: timerRetry
interval: 10000; running: true; repeat: true
onTriggered: {
if (events.length + births.length + deaths.length == 0)
init();
}
}
SequentialAnimation {
id: showText
PropertyAction { target: onThisDayText; property: "y"; value: 25 }
NumberAnimation { target: onThisDayText; property: "opacity"; to: 1.0; duration: 500 }
PauseAnimation { duration: 3000 }
NumberAnimation {
target: onThisDayText
property: "y"
to: Math.min(-(25 + onThisDayText.contentHeight) + viewportHeight, 25)
duration: Math.max(0, (Math.abs(to - from) * 1000) / 25)
}
PauseAnimation { duration: 3000 }
NumberAnimation { target: onThisDayText; property: "opacity"; to: 0.0; duration: 1000 }
onFinished: {
onThisDay.next();
}
}
Text {
renderType: Text.NativeRendering
id: onThisDayText
wrapMode: Text.WordWrap
font.family: FontFamily_Normal
font.styleName: FontStyle_Normal
font.pointSize: 40
textFormat: Text.RichText
color: "white"
y: 25
anchors.left: parent.left
width: parent.width
height: contentHeight
opacity: 0
}
Rectangle {
id: top
anchors.top: parent.top
anchors.left: parent.left
width: parent.width
height: 10
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "black" }
GradientStop { position: 0.5; color: "transparent" }
}
}
Rectangle {
id: bottomFade
anchors.top: parent.top
anchors.topMargin: viewportHeight
anchors.left: parent.left
width: parent.width
height: 0.1 * viewportHeight
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.5; color: "black" }
}
}
Rectangle {
anchors.top: bottomFade.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width
color: "black"
}
}
Definition of the OnThisDay QML type
Now that we've defined some of the application's QML types, we will arrange them on the main QML file.
import "QuickMirrorTypes"
Window {
visible: true
title: qsTr("Quick Mirror")
Flickable {
anchors.fill: parent
contentWidth: mirror.width
contentHeight: mirror.height
Rectangle {
id: mirror
width: 1080
height: 1920
color: "black"
Clock {
id: clock
anchors.top: mirror.top
anchors.left: mirror.left
}
Calendar {
id: calendar
anchors.top: clock.bottom
anchors.topMargin: -20
anchors.left: mirror.left
}
Rectangle {
anchors.top: calendar.bottom
anchors.topMargin: -5
anchors.left: mirror.left
width: 800
height: 2
color: "white"
}
OnThisDay {
id: onThisDay
anchors.top: calendar.bottom
anchors.left: mirror.left
anchors.leftMargin: 10
anchors.bottom: mirror.bottom
width: 780
viewportHeight: 260
}
}
}
}
Main QML with Clock, Calendar and OnThisDay
Finally, the QML files and the qmldir file must all be added to the application's resource file:

QML files and qmldir added to the resource file
After building and deploying we'll be able to start the application and see the information displayed.

Application running on the Raspberry Pi
VS supports debugging applications running on WSL through gdb. To debug while running on the Raspberry Pi we will launch the application using gdbserver and then configure gdb to connect to the device and start a remote debug session.

Remote debugging from Visual Studio using gdb and gdbserver
For this to work, the gdb installed in the WSL must support the target device architecture. A simple way to achieve this is to install gdb-multiarch. To ensure VS uses the correct debugger, we will create a symbolic link from gdb to gdb-multiarch.
user@buildhost:~$ sudo apt-get install gdb-multiarch
...
user@buildhost:~$ cd /usr/bin
user@buildhost:/usr/bin$ sudo mv gdb gdb-bkup
user@buildhost:/usr/bin$ sudo ln -s gdb-multiarch gdb
user@buildhost:/usr/bin$ ls -go gdb*
lrwxrwxrwx 1 13 Sep 2 11:31 gdb -> gdb-multiarch
-rwxr-xr-x 1 8440200 Feb 11 2020 gdb-bkup
-rwxr-xr-x 1 15192808 Feb 11 2020 gdb-multiarch
user@buildhost:/usr/bin$
Replacing gdb with gdb-multiarch
To setup the remote debugging session in Visual Studio, two additional commands must be passed to gdb. This is configured in the "GDB Debugger" property page.
target extended-remote 192.168.1.98:2345
set remote exec-file /home/pi/QuickMirror.out

Additional gdb commands for remote debugging session
Before starting the remote debugging session, we must set the required environment variables and launch gdbserver on the device.
pi@raspberry-pi:~$ export LD_LIBRARY_PATH="/usr/local/qt5pi/lib"
pi@raspberry-pi:~$ export QT_QPA_PLATFORM="eglfs"
pi@raspberry-pi:~$ export QT_QPA_PLATFORM_PLUGIN_PATH="/usr/local/qt5pi/plugins/platforms"
pi@raspberry-pi:~$ export QT_QPA_EGLFS_PHYSICAL_WIDTH="326"
pi@raspberry-pi:~$ export QT_QPA_EGLFS_PHYSICAL_HEIGHT="520"
pi@raspberry-pi:~$ export QML2_IMPORT_PATH="/usr/local/qt5pi/qml"
pi@raspberry-pi:~$ gdbserver --once --multi :2345
Listening on port 2345
Starting gdbserver on the Raspberry Pi
Pressing F5 will start the remote debugging session.

Stopped at breakpoint in C++ code during remote debugging
It is also possible to debug QML code while running the application on the embedded device.

-qmljsdebugger=port:8989,host:192.168.1.98,block
Program arguments for starting a QML debug session

Stopped at breakpoint in QML code during remote debugging
We've shown how to use the Qt VS Tools extension to create a multi-platform embedded application in Visual Studio with Qt Quick. This included:
The project, including all source code, is available at: https://github.com/micosta/quickmirror.

Our application running on the embedded device
Thank you for reading and for your interest in Qt and the VS Tools extension. If you have any questions or suggestions, please leave a comment below.
Download the latest release here: www.qt.io/download
Qt 6.10 is now available, with new features and improvements for application developers and device creators.
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.
The latest Qt release, Qt 6.11, is just around the corner. This short blog..
Read Article
We're happy to announce the release of version 1.12.0 of the Qt Extension..
Read Article
We are happy to announce the release of Qt Creator 19 RC. Please head over..
Read Article