本文的第1部分中,我們展示了如何在Visual Studio中針對Windows和嵌入式Linux創建多平臺Qt Quick應用程序項目?,F在,我們將展示如何在嵌入式設備上運行該應用程序。然后,我們將繼續將該項目開發為我們打算創建的完整嵌入式應用程序。最后,我們將使用VS調試器對應用程序的C ++和QML代碼進行遠程調試。
在嵌入式設備上運行
我們已經展示了如何交叉編譯在Visual Studio中創建的“ hello world” Qt Quick應用程序。現在,我們將看到如何在Raspberry Pi上運行該應用程序。由于我們將以全屏模式運行,因此我們必須首先向應用程序窗口中添加一些內容。
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");
}
}
}
和以前一樣,選擇Linux項目配置,然后按F7鍵開始交叉編譯。
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==========
要在每次構建結束時自動復制應用程序文件,可以在“ WSL構建后事件”屬性頁中設置以下命令(ATTN: 這將以明文形式保存設備密碼)。
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>
在啟動Qt Quick應用程序之前,我們需要設置一些必需的環境變量:
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
樹莓派顯示器
在Raspberry Pi中運行“ Hello World”應用程序
我們的應用程序的要求包括顯示以下信息:
我們將把每個項目封裝為專用的QML類型。為此,我們必須首先將QML模塊定義(qmldir)文件添加到項目中:
向項目添加新的QML模塊定義
按下“添加”后,qmldir 文件將在項目樹中可用。我們將使用此文件來定義每種QML類型到其對應源文件的映射。
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
要將新的QML源文件添加到項目中:
我們將首先添加QML類型以顯示當前時間,當前日期和重要的周年紀念日。該Clock類型將顯示當前時間,每秒刷新一次。
QuickMirror.Clock.qml
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();
}
}
該Calendar類型將顯示當前日期,并在不同語言環境之間循環。
QuickMirror.Calendar.qml
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 } } } }
除了日期/時間,我們的應用程序還將依靠Web API來檢索信息。我們將curl在一個單獨的過程中運行以連接到Web API。流程創建將由名為的C ++類處理Process。然后,QML類型ApiCall將使用一個Process對象以curl必要的參數開始并收集其輸出。
QuickMirror.ApiCall.qml
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);
}
}
}
}
要創建ProcessC ++類:
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.cpp
Process(QObject* parent) : QProcess(parent)
{
connect(
this, QOverload::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; }
main.cpp
int main(int argc, char* argv[])
{
qmlRegisterType("Process", 1, 0, "Process");
...
該OnThisDay QML類型將使用的實例,通過它們來獲取顯著的紀念日列表和循環每隔幾秒鐘。 ApiCall
QuickMirror.OnThisDay.qml
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="?" + 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"
}
}
現在,我們已經定義了一些應用程序的QML類型,我們將它們排列在主QML文件上。
main.qml
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
}
}
}
}
最后,qmldir 必須將QML文件和該文件全部添加到應用程序的資源文件中:
構建和部署后,我們將能夠啟動應用程序并查看顯示的信息。
樹莓派顯示器
在Raspberry Pi上運行的應用程序
VS支持通過調試在WSL上運行的應用程序gdb。要在Raspberry Pi上運行時進行調試,我們將使用啟動應用程序gdbserver,然后配置gdb為連接到設備并啟動遠程調試會話。
為此,gdb WSL中安裝的組件必須支持目標設備體系結構。一種簡單的方法是安裝gdb-multiarch。為了確保VS使用正確的調試器,我們將創建從gdb到的符號鏈接gdb-multiarch。
WSL命令外殼
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$
要在Visual Studio中設置遠程調試會話,必須將兩個附加命令傳遞給gdb。這是在“ GDB調試器”屬性頁面中配置的。
Project Properties > Debugging > Additional Debugger Commands
target extended-remote 192.168.1.98:2345
set remote exec-file /home/pi/QuickMirror.out
在開始遠程調試會話之前,我們必須設置所需的環境變量并gdbserver在設備上啟動。
Raspberry Pi命令外殼
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
按F5將啟動遠程調試會話。
在遠程調試期間在C ++代碼中的斷點處停止
遠程QML調試
在嵌入式設備上運行應用程序時,也可以調試QML代碼。
項目屬性>調試>程序參數
-qmljsdebugger=port:8989,host:192.168.1.98,block
我們已經展示了如何使用Qt VS Tools擴展在帶有Qt Quick的Visual Studio中創建多平臺嵌入式應用程序。這包括:
該項目,包括所有源代碼,可從以下網址獲得:https : //github.com/micosta/quickmirror。
應用程序在嵌入式設備上運行
感謝您的閱讀和對Qt和VS Tools擴展的關注。如果您有任何疑問或建議,請在下面發表評論。
如果這篇文章沒能滿足你的需求、點擊“了解更多”!免費獲取更多Qt資源教程!
Qt(C++)是一個跨平臺的C++圖形用戶界面庫,Qt安裝程序分為商業版本和開源版本。個人和小型團隊選擇開源版本更實用。Qt的版本號一般由3個數值表示,如5.14.2是完整的Qt版本號,第一個數字5表示大版本號(major),第二個數字14表示小版本號(minor),第三個數字2表示補丁號(patch)。當兩個版本的大版本號和小版本號數字相同,那么這兩個Qt版本的功能就是一致的,比如5.14.*系列各版本功能都是一樣的。
但是大版本號更高并不如意味著功能越多,比如Qt 6系列更新到了Qt 6.2版本,才覆蓋Qt 5.15中大部分的功能。
目前Qt系列最新版本為Qt 6.3.1(2022年6月發布),但是國內選擇Qt 6進行開發的團隊不多,一方面是因為Qt 6系列功能不穩定,對win10以下系統支持不友好。另外一方面因為Qt 5.14.2(2020年4月發布)版本之后,開源版本只提供在線安裝程序,離線安裝程序必須購買商業版本。不過新的項目在win10下使用Qt 6.2 以上版本進行開發還是非常不錯的選擇,畢竟Qt 6解決了許多Qt 5中的bug。如果是追求穩定,那么Qt最后一個離線安裝版本Qt 5.14.2是比較好的選擇。因此這里介紹Qt 6.3.1和Qt 5.14.2的安裝。
Qt 6的安裝程序下載地址如下:
商用版本下載:
? https://www.qt.io/download??
開源版本下載(國內下載太慢):
?? https://download.qt.io/official_releases/online_installers/??
清華鏡像(國內使用):
?? https://mirrors.tuna.tsinghua.edu.cn/qt/development_releases/online_installers/??
所有Qt版本下載列表:
?? https://download.qt.io/archive/qt/??
Step 0
從下面開源版本列表中選擇window-online,下載最新的Qt安裝器。
如果是國內使用,推薦使用清華源鏡像:
Step 1
安裝程序的各個語言安裝版本內容是一樣的,打開安裝程序后,第一個界面是歡迎welcome。需要登錄Qt用戶,沒有點擊注冊即可?;蛘咴诰€注冊也可以,注冊網站:https://login.qt.io/register。
Step 2
登錄后進入開源義務界面open source obligations,開源版本條件勾選兩個,注意核對版權信息。
Step 3
安裝程序界面Setup,就是一句歡迎詞,直接點擊下一步。點擊后會遠程同步一些內容。
Step 4
詢問你是否運行Qt收集個人使用數據,一般不同意。
Step 5
安裝文件夾界面Installation Folder,主要設置安裝路徑,Qt 6支持安裝在任意路徑下,但是安裝目錄名以及路徑中所有目錄名都應為英文字符,且不能有空格。所以建議只更改安裝盤符,比如將C改為D。安裝方式選擇Custom installation,這樣可以自行勾選安裝組件,然后直接點擊下一步即可。
Step 6
對于組件的選擇看自己的安裝環境,要安裝的組件一般根據個人需求進行選擇,全部安裝非常耗時和占用硬盤空間。這里選擇的是Qt 6.3.1安裝,選擇的是MinGW編譯環境。MSVC指微軟的編譯器Microsoft Visual C++ Compiler。如果選擇MSVC2019作為編譯器,還需要安裝vs2019,并且安裝相應的build tool。MSVC2019版本相關庫比較齊全,但是僅僅針對pc平臺,而且安裝Qt過程比較麻煩,但對于一些第三方庫如OpenCV配置比較簡單。MinGW跨平臺,Qt安裝簡單,一些第三方庫如OpenCV則需要自行通過cmake源碼編譯。這里為了方便,用的是Qt自帶的開發環境Qt Creator,就選擇了MinGW。
此外,在組件選擇最下方 Developer and Designer Tools,如果選擇了MinGW作為編譯器,還需要選擇MinGW版本和調試工具。CMake編輯工具和Ninja系統構建工具建議也選擇。然后直接下一步。有些組件沒安裝不需要擔心,安裝完后,在線安裝允許修改刪除添加組件。
Step 7
許可協議License Agreement,直接選擇同意就好,下一步。
Step 8
開始菜單快捷方式,這個一般默認就行了,直接下一步。
Step 9
準備安裝,看看磁盤是否這么多空間,然后點擊安裝。
Step 10
正在安裝,Qt開始下載,視網速決定下載速度。
Step 11
打開Qt Creator集成開發環境,點擊文件-新建項目,然后測試安裝即可。
自Qt 6開始,已經默認禁用了Qt Quick Designer,如果要更改插件。點擊幫助-關于插件就可以重新選用。
Qt 5的安裝程序下載地址如下:
?? https://download.qt.io/archive/qt/5.14/5.14.2/??
Step 0
點擊下載qt-opensource-windows-x86-5.14.2.exe即可。這是離線安裝包,需要一定的下載時間。
Step 1
點擊下載好的Qt 5.14.2安裝包,進入歡迎頁面,直接下一步。
Step 2
Qt 賬戶登錄,如果斷開網絡會跳過這個界面。
點擊下一步。
Step 3
選擇安裝文件夾,安裝目錄名以及路徑中所有目錄名都應為英文字符,且不能有空格。如需更改路徑建議只更改安裝盤符,比如將C改為D。
Step 4
選擇要安裝的組件。對于Qt 5.14.2選項,如果選擇MSVC要安裝對應版本的visual studio。這里選擇安裝MinGW 64位版本。對于Developer and Designer Tools選項,選擇Qt creator調試工具,還有對應的MinGW 64位版本。
Step 5
許可協議,直接同意,進入下一步。
Step 6
安裝程序快捷方式,默認即可。
Step 7
直接安裝即可,注意磁盤空間是足夠的。
Step 8
等待安裝完成即可。
Step 9
打開Qt Creator集成開發環境,點擊文件-新建項目,然后測試安裝即可。
Qt 有一個官方資源下載網站:https://download.qt.io/。
該網站各個目錄如下所示。紅字表示能夠下載到各個發布版的安裝程序。推薦進入archive目錄進行下載,該目錄下安裝版本最全。
archive目錄下各文件夾介紹如下:Qt 5.15版本及以上進入online_installers目錄進行下載安裝。Qt 其他版本進入qt/目錄下載安裝包。
進入qt/目錄后,能夠看到各歷史Qt 版本源代碼目錄,進入對應的版本目錄即可下載安裝包,安裝步驟和Qt 5.14差不多,大概安裝步驟就這些。