用 Qt 做一个简单的软件

早年用 Pyinstaller 做 Python 的软件,感觉软件体量太大了,不够轻量级,动不动就上百 M。后来研究了一下,发现问题根源在于软件嵌入了环境里的各种大的包和库,例如 numpy 或 matplotlib,再加上自身的 PyQt,有多大可想而知。

后来为了轻量化 exe,了解到还有

  1. cpp + Qt
  2. Rust + egui
  3. go + fyne

2 和 3 是新起之秀,好写,启动也快,界面还行,但数值/绘图库生态没 cpp 那么工业化,在学术科研上没啥优势,所以最后还是打算走传统主流的 cpp + Qt 算了。

此外 1 和 2/3 还有一个区别,Go/Fyne、Rust/egui 经常能一个 exe 拿走就跑,而 Qt 默认往往要 exe + 一堆 dll + plugins。这差异主要来自两点:静态链接的默认程度和插件/平台抽象的架构。

所谓的静态库(.lib/.a),就是在编译阶段,编译器会把库里所有用到的代码“复印”一份,直接塞进你的 .exe 文件里。缺点是体积大,更新麻烦。动态库(.dll/.so),编译时,.exe 里只记下了需要用到某个库的名字。程序运行的时候,它会去电脑文件夹里临时找这个 .dll 文件。缺点是依赖性强,只把 .exe 带走会报错。

Qt Creator 下载地址:https://www.qt.io/zh-cn/development/download

Qt 软件 GUI 的全局逻辑

必须先对整个软件制作的大局观有所了解,再细究每个环节的具体行为,我们才能对软件制作流程有更清晰的认识。

  1. 在 Qt Creator 里创建一个项目后,会得到一个文件目录

    1
    2
    3
    4
    .qtcreator/
    build/
    CMakeLists.txt
    main.cpp

    其中.qtcreator/是 Qt Creator IDE 的私有配置,是编辑器自己的配置目录,类似 VSCode 的.vscode/,不用管

    build/是编译输出的目录,是 CMake 构建的目录。里面会有编译产生的中间文件、自动生成的文件、最终exe、缓存等。该目录是生成物,可以随时删除重新生成。

    CMakeLists.txt是整个项目的构建规则,相当于是软件的构建说明书。它高速 CMake 这个项目叫什么,用什么 cpp 标准,依赖哪些 Qt 模块,哪些源文件要编译,要生成什么类型的 target(.exe)。因此,可以将 Qt Creator 理解为 CMake 的前端。

    main.cpp是 cpp 程序的入口,它做了三件事:

    • 创建 Qt 应用对象(QApplication)
    • 创建窗口(交互界面)
    • 启动事件循环
  2. 先了解 CMake 和 CMakeList 的关系和行为

    众所周知,光有程序代码,就算跑起来也不能算我们平时生活中认知的软件,顶多只能算脚本。普通人认为的软件,是一个可执行文件,点击即用,而跑程序代码相对于点击即用的软件,多了一道叫做编译的工序。所谓的编译就是将人类用高级语言例如 C,Python,Matlab 写的脚本翻译成计算机能读懂并执行的二进制可执行文件,CMake 就是干这件事的,但它不是编译器(MSVC / GCC / Clang)而是构建系统生成器(build system generator)。

    1
    2
    3
    4
    CMake → 生成 Makefile/Ninja
    Make/Ninja → 调用 编译器
    编译器 → 生成 .obj/.o
    链接器 → 生成 .exe

    就和人类翻译员一样,你需要根据不同场合不同对象来确定要翻译到什么程度,这样才能让计算机正确执行我们想要的功能。因此,Qt 就是通过 CMakeLists 去配置 CMake 的各种参数,包括:

    • 定义工程/全局规则:最低版本、项目名、Cpp 标准、编译选项等
    • 找到依赖:Qt6(以及 Widgets / Core / Gui …)
    • 生成目标(target)并把东西挂到 target 上
  3. 了解 main.cpp 干了什么

    main.cpp 作为 GUI 的核心,做了三件事

    • 创建了 QApplication(整个 GUI 系统的核心)
    • 创建主窗口对象
    • 启动事件循环a.exec()

    当你在软件界面上点击按钮时:

    1. 操作系统产生一个“鼠标点击事件”
    2. 这个事件被送进 Qt 的事件队列
    3. Qt 的事件循环(a.exec())取出事件
    4. Qt 找到对应按钮对象
    5. 触发按钮的 signal(信号)
    6. 调用连接的 slot(槽函数)

    由此实现了普通人认知上的软件功能

Qt 软件的调试与运行

这里先直接拿一个现成的案例跑,走走软件制作流程,这里就用示例里的计算器应用(Calqlatr)

计算器软件示例

点击左侧编辑栏,可以看到当前项目列表。calqlatr 是项目文件夹,包含了资源文件夹和源代码文件夹。GUI 设计代码就在源代码文件夹里改。

编辑栏

点击左侧调试栏,我们发现底侧输出栏和上方代码栏之间多了一个调试器,点击 Start Debugging… 就能调试并运行该计算器软件

调试运行软件

测试了没问题,打开左侧项目栏,可以看到构建设置。所谓的构建设置,就是前面提到的 CMake 如何构建不同场景下的软件包

构建配置

所谓的场景,指的是 CMake 的不同构建模式。它决定了:

  • 是否开启调试信息
  • 是否开启优化
  • 生成的程序体积
  • 是否适合调试
  • 运行速度如何

我们先不管每个模式具体有啥不同,直接添加 Release 发行版并在左下角启用

启用发行版模式

最后点击左下角的小锤子,构建软件包

构建软件包

点击左上方视图栏窗格,打开 build 的文件夹

找到项目在资源浏览器中的位置

我们可以看到,此时 build 文件夹内有俩构建好的软件包,分别对应 Debug 和 Release 模式

两种构建模式的软件包

Qt 软件的部署与打包

这里要明晰一个容易被混淆的概念,部署(Deploy)和分发(Distribute)

Deploy(部署)

把程序连同依赖环境一起“布置”到某个运行环境,使其可以正常运行。

关键词是:

  • 解决运行依赖
  • 放到目标机器
  • 让它能跑起来

Distribute(分发)

把已经可运行的软件交付给用户(发布给别人)。

关键词是:

  • 打包
  • 发布
  • 给用户下载/安装

我们现在想要干的事情同时包含部署和分发,因为你光在本地构建运行好了软件没用,你要让别人能够跑起来那才算成功!


Qt Creator 部署的流程如下:

1
2
3
4
Qt Creator 构建 Release
→ 生成 xxx.exe
→ 用 windeployqt 收集 Qt 依赖 DLL
→ 得到一个完整文件夹

这个步骤本质是:

在 Windows 上为你的程序做“部署准备”(deployment preparation)

因为 windeployqt 的作用是:

  • 扫描 exe
  • 找出 Qt 依赖
  • 拷贝 DLL
  • 拷贝 plugins
  • 拷贝 platforms/qwindows.dll
  • 拷贝 translations
  • 必要时拷贝 QML 模块

接下来详细介绍操作流程

  1. 首先在心仪的地方创建一个空文件夹。这里我直接在 build 目录下创建了一个 dist 文件夹,并在该位置打开 cmd

    创建空文件夹并在该处打开cmd
  2. 此时我们要使用 windeployqt 这个命令。但使用该命令前,我们需要将该命令的路径添加至环境变量,并将 Release 中的 exe 文件复制粘贴进 dist 文件夹

    添加环境变量
  3. 在 cmd 中输入 windeployqt --qmldir E:\Qt\Examples\Qt-6.10.2\demos\calqlatr\build\Desktop_Qt_6_10_2_MinGW_64_bit-Release\qml calqlatr.exe 并回车

    这里我贴出了 qml(Qt Meta-Object Language) 的完整路径,qml 是一种基于声明式语法的脚本语言,主要用于构建流畅、现代化、高动态的用户界面(UI),是 Qt Quick 技术的核心。如果没有 qml 就不写。

    运行完毕后我们发现 dist 文件夹中已经塞满了文件,双击 calqlatr.exe 后发现程序正常运行,部署成功。

    Windows 部署成功

    现在的 dist 文件夹已经是一个完整的软件包了,你可以将整个文件夹拷走。只要文件夹内的文件完整,操作系统匹配,软件在哪儿都能运行,至此我们完成了本地部署测试。

  4. 现在要准备分发工作了,所谓的分发,就是将整个软件包打包成一个单独的安装包,以供发布给用户下载安装。这里我们使用 Inno Setup Compiler 来演示软件的打包。

    首先新建一个脚本,跟着 Wizard 走

    Inno Setup Compiler

    找到软件执行 exe 文件,并将整个 dist 文件夹都选上

    找到软件执行文件

    后面直接一路 next(如果有想变更的设置可以自己改),最后 finish 了直接 compile
    编译安装包

    最后得到一个单独的安装包

    独立安装包

最后打包好的安装包,就是大家平时日常生活中使用的软件安装包了。大家只需要双击并按指引进行程序安装,就能在桌面上看到软件的快捷方式,双击就能使用该软件。

忘了说明,输出的安装包在 Build -> Open Output Folder

输出安装包的位置