乘风破浪,遇见最美Windows 11之现代Windows桌面应用开发 - 用于Visual Studio 2022/2019的QT VS工具

2018年10月08日,Visual Studio中的QML调试

image

https://www.qt.io/blog/2018/10/08/qml-debugging-visual-studio

Qt Visual Studio工具的下一个版本,即v2.3.0,将允许在Visual Studio中调试QML应用程序。它将有可能在QML文件中设置断点并逐步执行QML代码。在断点模式下,它可以观察变量并改变它们的值,以及评估任意表达式。QML调试会话将与C++调试会话同时运行,因此,在应用程序的同一调试运行期间,可以在C++和QML中设置断点和观察变量。

Qt VS工具的这个新的调试功能集成了QML调试基础设施(QML debugging infrastructure),它是Qt QML模块的一部分,通过一个TCP端口为调试、检查和剖析应用程序提供服务。为了用QML调试基础设施的功能来扩展Visual Studio调试器,我们提供了一个Visual Studio QML调试引擎。这个调试引擎大部分是由Visual Studio调试器(Visual Studio debugger)的Active Debugging 7 (AD7)可扩展框架的接口实现组成的。

如果一个Qt项目包含任何QML资源文件,启动一个调试会话(例如按F5键),除了启动本地应用程序,现在也连接到该应用程序的QML调试基础设施。这可以在Visual Studio调试器的进程窗口中看到:列出了两个进程,一个是对应于为C++调试会话创建的实际物理进程的本地进程,另一个是QML进程,它不对应于机器上运行的任何物理进程,而是代表与本地进程中QML调试运行时的连接。

image

由于本地进程和QML进程都存在,所以有可能在C++或QML代码中都请求断点。Visual Studio调试器将把请求转发给适当的调试引擎。像往常一样,在QML代码中,一个填满的圆形断点标记表示一个有效的断点;这意味着该文件位置的断点请求已经被发送到QML运行时(QML runtime),并得到确认。

image

当一个断点被击中时,Visual Studio将显示调用栈的当前状态。与其他调试使用不同语言的应用程序的情况不同(如.NET + Native调试),QML调试引擎不提供真正的混合模式调试。它与本地调试引擎同时运行,从Visual Studio调试器的角度来看,它与本地进程没有关系。这意味着,即使在同一个调试会话中可以同时调试C++和QML,当QML断点被击中时,显示的堆栈将只包括QML函数调用 -- 这些调用的C++上下文将不可用。

image

与本地调试的情况一样,在断点模式下,可以在当前活动的调用堆栈帧的上下文中查看和修改局部变量的值,以及为任何变量或表达式创建监视。立即窗口也可用于在当前堆栈帧的上下文中评估任何表达式。

image

把鼠标移到一个QML表达式上,会弹出一个即时观察窗口(或 "DataTip")。该表达式在当前上下文中的值被显示出来,也可以被修改。

image

对于任何Qt QML应用程序,QML调试是默认启用的。通过打开Qt项目设置对话框,将 "QML调试(QML Debug)"选项设置为"禁用(Disable)",可以禁用QML调试,并恢复到仅有本地调试。在这个对话框中,它也可以改变QML调试运行时使用的端口。

image

如前所述,Qt VS工具的QML调试功能将在下一个版本中提供,计划于今年晚些时候在Visual Studio Marketplace发布。预览版将很快可以在Qt网站上下载;当它可用时,我们会在这里发布一个快速更新。

2018年11月28日,Qt Visual Studio工具2.3.0版发布

image

https://www.qt.io/blog/2018/11/28/qt-visual-studio-tools-2-3-0-released

Qt VS工具的新开发版本,2.3.0版,现在可以在Qt下载页面下载。

重要的变化包括:

关于这个版本所包含的细节,请参考变化日志

这个开发版本不会被发布到Visual Studio Marketplace。我们将继续开发即将发布到VS Marketplace的正式版本,它将包括一些额外的功能和错误修复。

2019年1月21日,Qt Visual Studio工具2.3.1版发布

image

https://www.qt.io/blog/2019/01/21/qt-visual-studio-tools-2-3-1-released

Qt VS Tools 2.3.1版现在已经发布到Visual Studio Marketplace

重要的变化包括:

有关该版本所包含的细节,请参考变更日志

安装包可以直接从Visual Studio内部安装(通过 "工具>扩展和更新... "菜单)。另外,它也可以从这个页面下载。

QML调试(QML Debugging)

与之前的帖子中提到的相反,新的QML调试功能不会在默认情况下被启用。它必须通过打开Qt项目设置并将 "QML调试 "选项设置为 "启用 "来明确启用。

2019年8月19日,Qt Visual Studio工具2.4 RC版发布

image

https://www.qt.io/blog/2019/08/19/qt-visual-studio-tools-2-4-rc-released

我们已经发布了Qt Visual Studio Tools 2.4 RC(2.4.0版);安装包可以在Qt下载页面找到。这个版本的特点是改进了Qt工具与Visual Studio项目系统的集成,解决了当前集成方法的一些局限性,最明显的是不能为每个项目配置不同的Qt设置,以及不支持从共享属性表导入这些设置。

在Visual Studio项目系统中使用Qt

Visual Studio项目系统被广泛用作VS中C++项目的构建系统。 在这个系统中,MSBuild提供了项目文件格式和构建框架。Qt VS工具利用MSBuild的可扩展性,在VS项目中提供Qt的设计和构建集成--在文章的最后,我们将仔细看看这种集成是如何工作的,以及新版本中的变化。

到目前为止,Qt VS Tools扩展以一种孤立的方式管理自己的项目设置。这种方式阻碍了Qt在Visual Studio中的整合,使其不能完全受益于VS项目和MSBuild的功能。重要的是,Qt的设置不可能根据构建配置的不同而变化(例如,不同的配置有不同的Qt模块列表),包括Qt本身:只能选择一个Qt的版本/构建,并适用于所有的配置,在多平台项目的情况下是一个重大的缺陷。

Qt VS工具的用户报告的另一个重要限制是不支持从共享属性表文件中导入Qt相关设置。这个功能允许VS项目中的设置在一个团队或组织内共享,从而为这些信息提供一个单一的来源。到目前为止,由Qt VS工具管理的设置还不可能做到这一点

为了克服这些和其他相关的限制,所有的Qt设置--例如Qt的版本、要使用的模块或生成的源的路径--现在将被存储为完全成熟的项目属性。目前的Qt设置对话框将被删除,取而代之的是Qt设置属性页。因此,它将有可能根据配置来设置所有的Qt设置值,以及从属性表文件中导入这些值。

image

仔细观察(A closer look)

一个过于简化的入门指南可以这样描述MSBuild:

  • 一个MSBuild项目由对源文件的引用和为处理这些源文件而采取的行动的描述组成,这些描述被称为目标
  • 构建过程在项目配置的上下文中运行(例如,调试(Debug)、发布(Release)等),一个项目可以包含任何数量的配置。
  • 与源文件和项目本身相关的数据可以通过属性访问。MSBuild属性是名-值定义,按配置指定(即每个配置有自己的属性定义集)。

image

属性可以适用于项目本身或项目中的特定文件,并且可以全局或局部定义:

  • 项目范围的属性总是全局的(如项目的输出目录或目标文件名)。
  • 适用于源文件的属性可以全局定义,在这种情况下,相同的值将适用于所有文件(例如,默认编译器警告级别被全局定义为3级)。
  • 这种全局的、文件范围的定义可以被本地定义的同名属性覆盖(例如,其中一个源文件需要以警告级别4编译)。
  • 全局定义存储在项目文件中或从属性表文件中导入。
  • 本地属性的定义存储在项目文件中,在相关的源文件引用中。

Qt Visual Studio工具扩展与MSBuild项目系统集成,提供了一组Qt特定的目标,描述了如何用适当的Qt工具处理文件(如moc头文件)。

image

就MSBuild项目系统的功能而言,目前的集成有一些限制:

  • 用户管理的Qt构建设置在改变时被复制到项目属性。鉴于这种单向的同步,项目属性可能会与相应的Qt设置不同步。
  • Qt构建设置的值对所有配置都是一样的,例如,无论选择何种配置,都会使用相同的Qt构建和模块。
  • 不可能覆盖生成文件中的属性,如moc的元对象源代码输出。
  • Qt设置只能存储在项目文件中。因此,不可能从共享的属性表中导入Qt定义,例如在几个项目中共享的共同Qt构建。

如上所述,解决这些限制的方法是使Qt设置成为完全成熟的项目属性。通过这种方式,Qt设置将被保证与项目中的所有其他属性同步,并有可能为每个构建配置定义不同的内容。它将有可能从属性表中导入Qt设置,而生成C++代码的Qt工具的属性页,如moc,现在将允许在生成的文件中覆盖编译器属性。

image

2019年11月06日,Qt Visual Studio工具2.4.2版发布

image

https://www.qt.io/blog/qt-visual-studio-tools-2.4.2-released

我们很高兴地宣布Qt Visual Studio Tools 2.4.2版本的发布

获取Qt VS工具

安装包可以在Visual Studio Marketplace获得。请注意,如果你安装了以前版本的Qt VS Tools,并且VS扩展设置为自动更新--这是默认设置--那么你的安装可能已经是最新的了。你可以通过打开Qt VS Tools菜单来检查当前安装的是哪个版本。

image

变化

这个版本修复了在v2.4版本中引入新功能后发现的几个问题。请参考变更日志中的修复列表。一些已知的问题仍未解决,将在以后的版本中修复。在此期间,请随时在Qt Visual Studio Tools的bug追踪器中报告任何问题,或提出任何建议或意见。

2020年2月10日,用Qt和Visual Studio进行跨平台开发

image

https://www.qt.io/blog/cross-platform-development-with-qt-and-visual-studio

在即将发布的Qt Visual Studio工具(计划在今年夏天发布)中,我们计划增加对Visual Studio Linux项目的支持。自从引入C++ Linux工作负载后,用户就有可能在Visual Studio中进行Linux开发。考虑到Qt本身的跨平台特性,这个功能对Qt开发者来说有潜在的兴趣,这就是为什么我们现在计划在Qt VS Tools扩展中加入对它的支持。

至于交叉编译的实际工作方式,当在VS中构建Linux项目时,构建过程和整体协调将依赖于MSBuild或CMake,与传统的Win32项目相同。构建工具本身将在一个编译服务器中运行,可以通过SSH访问。对于MSBuild项目,Visual Studio允许选择gcc或clang作为C++编译器

image

我们增加对Linux项目的支持的计划是使用VS Linux开发工作负载中的功能来扩展现有的Qt/MSBuild目标。我们的目标是能够构建和运行支持Qt的MSBuild项目,既可以是Windows的原生项目,也可以是交叉构建的Linux项目。

作为实现这一目标的第一步,并作为一个概念验证,我们手动将一个简单的Qt例子(wiggly)转换为一个MSBuild项目,同时针对Windows和Linux。按下F5将为所选目标构建项目,然后启动调试会话,如果是Windows构建,可以在本地启动,如果是Linux,则通过gdbserver启动。

image

这个练习表明,使用Visual Studio IDE来开发、构建甚至调试多平台的Qt应用程序是可能的。因此,无论目标平台如何,Qt VS Tools扩展应该能够无缝地协助MSBuild项目中Qt工具的设置和使用。我们还了解到,对Qt/MSBuild目标的修改,是支持本地和跨平台构建的需要,主要集中在Qt工具的调用方式上。大部分(如果不是全部)支持Qt与MSBuild集成的逻辑将在Linux上保持有效。

在这篇文章中,我们已经涵盖了VS被用来针对Linux主机的情况。然而,考虑到VS的Linux工作负载只需要一个SSH可访问的GNU工具链,因此可以使用一个跨编译器的工具链来代替主机的。可以想象,使用VS作为替代IDE来开发Qt的MCU应该是可能的。我们将在另一篇博文中探讨这种情况。

感谢你的阅读,感谢你对Qt和VS工具扩展的兴趣。如果你认为(或不认为)这个话题是相关的,或者你有任何问题或建议,请在下面留言。

2020年6月29日,Qt Visual Studio工具2.5.2版发布

image

https://www.qt.io/blog/qt-visual-studio-tools-2.5.2-released

我们很高兴地宣布Qt Visual Studio工具2.5.2版的发布。安装包现在可以在Visual Studio Marketplace上找到。

除了几个重要的错误修复,这个新版本的Qt VS工具允许创建参考多个Qt版本的项目。重新设计的项目创建向导现在可以根据需要定义多个项目配置,并在每个配置中指定使用哪个版本的Qt和Qt模块集。例如,现在可以创建一个同时针对32位和64位Windows的Qt项目。

image

这一变化代表了我们为支持Visual Studio / MSBuild C++项目的多平台Qt开发而持续努力的第一步。在下一个版本(v2.6)中,我们计划通过与Visual Studio C++ Linux Workload的集成,引入Qt项目的交叉编译。

你可以在bugreports.qt.io中跟踪我们的工作进展。请随时报告任何问题,或提出任何建议/意见,帮助我们进一步改进Qt Visual Studio工具。

2020年9月24日,Qt Visual Studio工具2.6版发布

image

https://www.qt.io/blog/qt-visual-studio-tools-2.6.0-released

我们很高兴地宣布Qt Visual Studio工具2.6.0版的发布。安装包现在可以在Visual Studio Marketplace获得。

v2.6版通过Linux开发VS工作负载,增加了对Visual Studio/MSBuild C++项目多平台Qt开发的支持。这使得在Visual Studio中使用远程托管的交叉编译器(或在本地WSL实例上)构建Qt项目成为可能。

image

你可以在bugreports.qt.io跟踪工作进展并提供反馈。请随时报告任何问题,或提出任何建议/意见,帮助我们进一步改进Qt Visual Studio工具。

我们将在不久之后发表一篇内容广泛的文章,详细介绍如何在Visual Studio中开发针对嵌入式Linux设备的多平台Qt Quick应用程序。我们将描述开发过程的所有步骤,从Visual Studio中创建一个新的多平台Qt Quick项目开始,到交叉编译、部署和远程调试。

敬请关注!

2020年9月24日,在Visual Studio中创建一个嵌入式Qt Quick应用程序(1/2)

https://www.qt.io/blog/creating-an-embedded-qt-quick-application-in-visual-studio-1/2

image

Qt Visual Studio工具现在支持Qt项目的交叉编译。这是由于Qt/MSBuild和Linux开发VS工作负载的整合而实现的。在这篇文章中,我们将展示如何使用这一功能在Visual Studio中用Qt Quick创建一个嵌入式应用程序。

我们将开发一个用于在镜像屏幕上显示公共信息的应用程序。这些是在休息区、洗手间等地方安装的一部分。该应用程序将针对运行嵌入式Linux的设备。在这篇文章中,我们将使用一个运行Raspbian(Buster)的Raspberry Pi 3 Model B

image

我们的Qt Quick嵌入式应用程序在目标设备上运行。

准备开发环境

开发主机将是一台Windows 10机器。必须安装Visual Studio 2019,包括桌面开发(Desktop development)Linux开发(Linux development) 工作负载。

image

所需的Visual Studio工作负载

我们将在Windows Subsystem for Linux(WSL)的一个实例中运行交叉编译器。设置开发环境的下一步是启用WSL并安装一个Linux发行版。然后应该可以打开一个终端窗口并运行Linux命令。

WSL Command Shell

user@buildhost:~$ uname -srvo
Linux 4.4.0-18362-Microsoft #836-Microsoft Mon May 05 16:04:00 PST 2020 GNU/Linux
user@buildhost:~$

要使用Qt Visual Studio工具,至少要有一个Qt for Windows的安装。这也将允许在开发机器上运行应用程序,以达到测试目的。

image

开发环境概述

交叉编译(Cross-compiling)Qt

我们现在可以着手设置交叉编译器。我们可以安装一个预建的工具链,或者编译我们自己的工具链。如果一切顺利的话,我们应该能够在设备上运行一个简单的程序。

WSL Command Shell

user@buildhost:~$ echo "#include <iostream>" > main.cpp
user@buildhost:~$ echo "int main() {" >> main.cpp
user@buildhost:~$ echo "std::cout << \"Hello World from Raspberry Pi\\n\";" >> main.cpp
user@buildhost:~$ echo "return 0;" >> main.cpp
user@buildhost:~$ echo "}" >> main.cpp
user@buildhost:~$ /opt/cross-pi-gcc-8.3.0/bin/arm-linux-gnueabihf-g++ main.cpp
user@buildhost:~$ scp a.out pi@192.168.1.99:/home/pi
pi@192.168.1.99's password:
a.out    100% 12KB 218.1KB/s 00:00
user@buildhost:~$ ssh pi@192.168.1.99 ./a.out
pi@192.168.1.99's password:
Hello World from Raspberry Pi
user@buildhost:~$

在Raspberry Pi上运行 "Hello World"。

我们已经准备好为Raspberry Pi构建Qt了。为此,我们将按照教程"RaspberryPi2EGLFS--在Raspbian上用eglfs交叉编译Qt用于HW加速OpenGL并设置Qt Creator的现代指南"。一旦构建完成,我们应该能够在Raspberry Pi上运行一个Qt应用程序样本。

WSL Command Shell

user@buildhost:~$ echo "#include <QDebug>" > main.cpp
user@buildhost:~$ echo "int main() {" >> main.cpp
user@buildhost:~$ echo "qInfo() << \"Hello World with Qt version\" << qVersion();" >> main.cpp
user@buildhost:~$ echo "return 0;" >> main.cpp
user@buildhost:~$ echo "}" >> main.cpp
user@buildhost:~$ echo "SOURCES += main.cpp" > hello.pro
user@buildhost:~$ raspi/qt5/bin/qmake
user@buildhost:~$ make -s
user@buildhost:~$ scp hello pi@192.168.1.99:/home/pi
pi@192.168.1.99's password:
hello    100%  12KB 359.6KB/s  00:00
user@buildhost:~$ ssh pi@192.168.1.99 ./hello
pi@192.168.1.99's password:
Hello World with Qt version 5.12.9
user@buildhost:~$

在Raspberry Pi上运行一个Qt示例应用程序

设置Qt Visual Studio工具

要从IDE中安装Qt Visual Studio工具(Qt Visual Studio Tools),请打开扩展(Extensions) > 管理扩展(Manage Extensions)。搜索"qt":搜索结果应显示"Qt Visual Studio Tools"扩展;选择它并按"下载"。关闭VS,按照安装说明操作。重新打开VS后,Qt Visual Studio工具应该可用。

image

Qt Visual Studio Tools菜单

为了使用Qt Visual Studio工具,我们必须首先注册我们将要使用的Qt版本。我们将从注册Windows版的Qt开始。

  • 打开 "Qt VS Tools > Qt Options",在 "Qt Versions"标签上按"Add"。
  • 提供一个名称和Qt安装的路径。

image

在Qt VS工具中注册Qt for Windows

我们现在要注册驻扎在WSL实例中的交叉编译的Qt。

  • 在 "Qt版本 "标签上再次按 "添加",打开 "添加新的Qt版本 "对话框。
  • 将 "构建主机 "设置为 "Linux WSL"(如果我们使用的是物理构建主机而不是通过WSL的虚拟主机,那么我们可以选择 "Linux SSH")。
  • 提供Qt安装的名称和路径,以及要使用的编译器(默认为g++)。这里我们将提供交叉编译器的路径。

image

为Raspberry Pi注册Qt build

创建一个多平台的Qt Quick项目

在注册了我们将要使用的Qt版本后,我们现在可以创建一个Qt项目。

  • 选择 "文件">"新建">"项目",打开Visual Studio创建新项目的对话框。
  • 在项目模板搜索中输入 "qt "并选择 "Qt Quick Application"。
  • 按 "下一步";为项目提供一个名称和位置,然后按 "创建"。
  • Qt Quick Application Wizard对话框将打开;按 "Next >"进行配置设置。
  • 为Windows和Raspberry Pi版本的Qt添加项目配置。

image

新项目配置

按 "Finish "来完成项目创建向导。Visual Studio现在应该显示新的多平台Qt Quick项目。选择一个Windows配置,按F5键来构建和运行该应用程序。一个带有 "Hello World "标题的空窗口应该随之打开。

在Visual Studio中用WSL进行交叉编译

使用WSL的一个实例来运行交叉编译器比使用SSH连接到构建主机要快。当使用SSH时,MSBuild必须首先将源文件上传到远程主机。另一方面,WSL会在/mnt中挂载本地驱动器,使文件的访问变得瞬息万变。在这种情况下,MSBuild会将Windows的路径映射成对应的Linux路径,并将其扎根在/mnt上,例如。

C:\include\foolib maps to /mnt/c/include/foolib

MSBuild会假设属性页中提供的路径是本地路径,并将它们映射到/mnt。当在属性页中使用绝对的Linux路径时,这可能导致不正确的映射,例如。

/home/user/raspi maps to /mnt/home/user/raspi

为了避免这个问题,我们将在/mnt创建符号链接到WSL实例文件系统的其他顶级目录。

WSL Command Shell

user@buildhost:/mnt$ sudo ln -s /home home
user@buildhost:/mnt$ ls -go
total 0
drwxrwxrwx 1 4096 Sep 1 15:29 c
drwxrwxrwx 1 4096 Sep 1 15:29 d
drwxrwxrwx 1 4096 Sep 1 15:29 e
lrwxrwxrwx 1 5 Sep 1 17:10 home -> /home
user@buildhost:/mnt$

/mnt创建一个符号链接

我们现在准备对项目进行交叉编译。选择一个Linux配置,按F7开始构建。

Visual Studio Build Output

1>------ Rebuild All started: Project: QuickMirror, Configuration: Release_RPi x64 ------
1>Invoking 'mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp/props.txt); mkdir -p /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp; cd /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp; ("/home/user/raspi/qt5/bin/qmake" -query) 1> props.txt', working directory: '/mnt/c/Users/user/Source/Repos/QuickMirror'
1>Reading Qt configuration (/home/user/raspi/qt5/bin/qmake)
1>Invoking 'mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp/qtvars.pro); mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp/Makefile); mkdir -p $(dirname /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp/qtvars.log); mkdir -p /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp; cd /mnt/c/Users/user/Source/Repos/QuickMirror/obj/x64/Release_RPi/qmake/temp; ("/home/user/raspi/qt5/bin/qmake" "CONFIG -= debug release debug_and_release" "CONFIG += release" qtvars.pro) 1> qtvars.log 2>&1', working directory: '/mnt/c/Users/user/Source/Repos/QuickMirror'
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/Release_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/Release_RPi/rcc/qrc_qml.cpp)', working directory: '/mnt/c/Users/user/Source/Repos/QuickMirror'
1>Starting remote build
1>Compiling sources:
1>main.cpp
1>qrc_qml.cpp
1>Linking objects
1>QuickMirror.vcxproj -> C:\Users\user\Source\Repos\QuickMirror\bin\x64\Release_RPi\QuickMirror.out
1>Done building project "QuickMirror.vcxproj".
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

在VS中交叉编译Qt项目

即将上线

综上所述,我们在Visual Studio中创建了一个多平台的Qt Quick项目,并使用交叉编译器为Raspberry Pi构建了它。在本篇文章的第二部分,我们将看一下。

  • 部署和运行该应用程序
  • QML中的应用代码
  • 在目标设备中进行调试

2020年9月30日,在Visual Studio中创建一个嵌入式Qt Quick应用程序(2/2)

https://www.qt.io/blog/creating-an-embedded-qt-quick-application-in-visual-studio-2/2

image

在这篇文章的第一部分,我们展示了如何在Visual Studio中创建一个针对Windows和嵌入式Linux的多平台Qt Quick应用程序项目。现在我们将展示如何在嵌入式设备上运行该应用程序。然后,我们将继续将该项目开发成我们所要创建的完整的嵌入式应用程序。最后,我们将使用VS调试器对应用程序的C++和QML代码进行远程调试

在嵌入式设备上运行

我们已经展示了如何交叉编译一个在Visual Studio中创建的"hello world" Qt Quick应用程序。现在我们将看看如何在Raspberry Pi上运行该应用程序。由于我们将在全屏模式下运行,我们必须首先向应用程序窗口添加一些内容。

main.qml

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"

像以前一样,选择一个Linux项目配置,然后按F7开始交叉编译。

Visual Studio Build Output

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 ==========

在VS中交叉编译Qt项目

现在我们将把应用程序的二进制文件上传到Raspberry Pi。构建输出窗口显示了生成的二进制文件的位置(上面突出显示)。

Windows Command Prompt

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>

将应用程序二进制文件上传到目标设备

为了在每次构建结束时自动复制应用程序文件,可以在"WSL构建后事件"属性页中设置以下命令(ATTN:这将以明文形式保存设备密码)。

Project Properties > WSL Post-Build Event > Command Line

curl --insecure --user pi:<password> -T /mnt/$(TargetPath.Replace('\','/').Replace(':','').ToLower()) scp://<device-addr>/home/pi/$(TargetFileName)

在每次构建结束时将二进制文件复制到设备上

在启动Qt Quick应用程序之前,我们需要设置一些必要的环境变量。

  • LD_LIBRARY_PATH

Qt 二进制文件安装目录的路径。

  • QT_QPA_PLATFORM

平台插件。

  • QT_QPA_PLATFORM_PLUGIN_PATH

平台插件的安装路径。

  • QT_QPA_EGLFS_PHYSICAL_WIDTH
  • QT_QPA_EGLFS_PHYSICAL_HEIGHT

物理屏幕的宽度和高度,单位是毫米。

  • QML2_IMPORT_PATH

安装QML模块的路径

Raspberry Pi Command Shell

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 Display

image

在Raspberry Pi中运行 "Hello World"应用程序

开发应用程序

我们的应用程序的要求包括显示以下信息。

  • 当前时间
  • 当前日期
  • 值得注意的周年纪念日
  • 天气预报
  • 公共交通的下一个出发点
  • 新闻

我们将把这些项目中的每一项都封装成一个专门的QML类型。为此,我们必须首先在项目中添加一个QML模块定义(qmldir)文件。

  • 选择 "项目 > 添加新项目... > Qt > QML Module Definition"。
  • 在位置栏中,指出将包含QML文件的文件夹的路径。

image

在项目中添加一个新的QML模块定义

在按下 "添加 "后,一个qmldir文件将在项目树中出现。我们将使用这个文件来定义每个QML类型到其相应的源文件的映射。

qmldir

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源文件到项目中。

  • 选择 "Project > Add New Item... > Qt > QML文件"。
  • 将位置设置为创建qmldir文件的同一目录。
  • 设置QML文件的名称。
  • 按 "Add"。

image

在项目中添加一个新的QML文件

我们将首先添加QML类型来显示当前时间、当前日期和值得注意的纪念日。Clock类型将显示当前时间,每秒钟刷新一次。

QuickMirror.Clock.qml

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();
    }
}

时钟QML类型的定义

日历类型将显示当前的日期,在不同的地域之间循环。

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 }
        }
    }
}

Calendar QML类型的定义

除了日期/时间,我们的应用程序将依靠Web API来检索信息。我们将在一个单独的进程中运行curl来连接到Web API's。进程的创建将由一个名为Process的C++类来处理。然后,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);
            }
        }
    }
}

ApiCall QML类型的定义

要创建Process C++类。

  • 选择 "项目>添加Qt类>Qt类"
  • 将类的名称设为Process
  • 按 "添加"。

image

在项目中添加一个新的Qt C++类

Process.h

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<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;
}

main.cpp

int main(int argc, char* argv[])
{
    qmlRegisterType<Process>("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 = "<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"
    }
}

OnThisDay QML类型的定义

现在我们已经定义了一些应用程序的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
            }
        }
    }
}

带有时钟、日历和OnThisDay的主QML

最后,QML文件和qmldir文件都必须被添加到应用程序的资源文件中。

  • 双击项目树中的QRC文件
  • 在Qt资源编辑器窗口中,按 "添加>添加文件"
  • 选择所有的QML文件和qmldir文件
  • 在Qt资源编辑器中按 "保存 "键

image

QML文件和qmldir添加到资源文件中

在构建和部署之后,我们就可以启动应用程序并看到显示的信息。

Raspberry Pi Display

image

在Raspberry Pi上运行的应用程序

在Visual Studio中调试

VS支持通过gdb调试运行在WSL上的应用程序。为了在Raspberry Pi上运行时进行调试,我们将使用gdbserver启动应用程序,然后配置gdb连接到设备并启动远程调试会话。

image

使用gdb和gdbserver从Visual Studio进行远程调试

要做到这一点,安装在WSL中的gdb必须支持目标设备的架构。实现这一点的一个简单方法是安装gdb-multiarch。为了确保VS使用正确的调试器,我们将创建一个从gdbgdb-multiarch的符号链接。

WSL Command Shell

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$

用gdb-multiarch替换gdb

为了在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

image

用于远程调试会话的附加gdb命令

在开始远程调试会话之前,我们必须设置必要的环境变量并在设备上启动gdbserver。

Raspberry Pi Command Shell

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

在Raspberry Pi上启动gdbserver

按F5将启动远程调试会话。

image

在远程调试过程中停在C++代码的断点上

远程QML调试

在嵌入式设备上运行应用程序时,也可以对QML代码进行调试。

  • 在Qt设置(Qt settings)中启用QML调试(QML debugging)。项目属性(Project Properties) > Qt项目设置(Qt Project Settings)
  • 设置启动QML调试会话(QML debug session)的程序参数

Project Properties > Debugging > Program Arguments

-qmljsdebugger=port:8989,host:192.168.1.98,block

启动QML调试会话的程序参数

image

在远程调试过程中停在QML代码的断点上

总结

我们已经展示了如何使用Qt VS Tools扩展,在Visual Studio中用Qt Quick创建一个多平台的嵌入式应用程序。这包括

  • 从头开始创建一个Qt Quick项目
  • 用QML编写应用程序的代码
  • 交叉编译该应用程序
  • 在嵌入式设备上部署和运行
  • 在Visual Studio中对C++和QML代码进行远程调试

该项目,包括所有的源代码,可在以下网站获得:https://github.com/micosta/quickmirror

image

我们的应用程序在嵌入式设备上运行

感谢你的阅读,感谢你对Qt和VS Tools扩展的兴趣。如果你有任何问题或建议,请在下面留言。

2021年11月15日,用于Visual Studio 2022的QT VS工具

image

https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022

我们很高兴地宣布,Visual Studio 2022的Qt Visual Studio工具(v2.8.0)扩展已经发布。安装包可在VS Marketplace下载,或直接在VS 2022 IDE中下载:在IDE菜单中选择扩展 > 管理扩展,然后搜索"qt"。

image

Qt VS Tools扩展的2.8.0版本也可作为Visual Studio 2019和2017的开发版本。安装包可以从download.qt.io下载。

你可以在bugreports.qt.io跟踪我们的工作进展并提供反馈。请随时报告任何问题,或提出任何建议/意见,帮助我们进一步改进Qt Visual Studio工具。

参考

posted @ 2022-02-24 18:25  TaylorShi  阅读(492)  评论(0编辑  收藏  举报