Note

本篇带你从零建起第一个 Qt 窗口程序:先看懂 qmake(.pro)与 CMake(CMakeLists.txt)两种工程文件的最小样例,再逐行解读 main() 函数里 QApplication、QWidget、show()、exec() 各自扮演的角色,最后梳理 Qt 程序从启动到退出的完整运行流程。适合刚装好环境、迫不及待想跑通第一个程序的读者。

工程文件:两种建法

Qt 程序不是单个源文件,它需要告诉构建系统”用了哪些 Qt 模块、源文件有哪些”。现代 Qt6 项目推荐 CMake,老项目与部分教程则使用 qmake,两种都要看得懂。

qmake:.pro 文件

# helloworld.pro —— qmake 的工程描述文件
QT += core gui          # 依赖的 Qt 模块:Core 和 Gui
 
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
                        # Qt5/6 起单独的 widgets 模块,按版本号条件加入
 
TARGET = helloworld     # 生成的可执行文件名
TEMPLATE = app          # 构建类型:应用程序(其他常见值:lib、subdirs)
 
SOURCES += main.cpp     # 源文件列表

qmake 会解析 .pro 生成 Makefile,再由编译器构建。日常写代码时 Qt Creator 代劳了这一步,但模块依赖这行 QT += 出问题时必须自己改。

CMake:CMakeLists.txt

# CMakeLists.txt —— Qt 官方推荐的现代构建方案
cmake_minimum_required(VERSION 3.16)
 
project(helloworld LANGUAGES CXX)
 
set(CMAKE_CXX_STANDARD 17)          # Qt6 要求 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
find_package(Qt6 REQUIRED COMPONENTS Widgets)
                                    # 找到 Qt6 的 Widgets 模块
 
add_executable(helloworld main.cpp) # 可执行目标及其源文件
 
target_link_libraries(helloworld PRIVATE Qt6::Widgets)
                                    # 把 Widgets 模块链接进来

与 qmake 对应:find_package + target_link_libraries 扮演了 QT += widgets 的角色。Qt6 的新特性(如 CMake API 友好的模块划分)只在 CMake 中可用,所以新项目优先选它。

Warning

用了 QWidgetQPushButton 等界面类却没在构建文件里声明 widgets 模块(.pro 里漏 QT += widgets,CMake 里漏 Qt6::Widgets),会得到一大串 undefined reference to vtable... 链接错误。看到 vtable 字样先查模块声明,再看有没有漏写 Q_OBJECT

解读第一个窗口程序

下面是 Qt5/Qt6 通用、完整可编译的最小窗口程序,每行都值得读懂:

#include <QApplication>   // 应用程序管理类
#include <QWidget>        // 空白窗口(控件基类)
#include <QPushButton>    // 按钮
 
int main(int argc, char *argv[])
{
    // 1. 创建 QApplication 对象:管理程序级资源
    //    它处理命令行参数,并掌管整个程序的事件循环
    QApplication app(argc, argv);
 
    // 2. 创建一个按钮控件,构造参数是按钮上显示的文字
    //    顶层控件(没有 parent 的 widget)会独立成窗口
    QPushButton button("Hello Qt!点我退出");
    button.resize(220, 60);   // 设置按钮(窗口)大小
 
    // 3. 把控件显示到屏幕上
    //    没有 show(),控件一直处于隐藏状态
    button.show();
 
    // 4. 进入事件循环:程序在这里"停住"等待事件
    //    鼠标点击、键盘输入、窗口重绘都会被送到这里分发
    //    直到所有顶层窗口关闭,exec() 才返回,main 结束
    return app.exec();
}

QPushButton 换成 QWidget window;window.show(),得到的就是一个空白窗口;控件都继承自 QWidget,所以行为一致。试着在 exec() 之前再创建几个控件、改改标题文字,运行体会一下。

每个角色都在干什么

  • QApplication:整个程序的”总管家”。它负责初始化图形环境、接收操作系统投递的事件、维护事件队列。每个 GUI 程序有且仅有一个实例,且必须先于任何控件创建。
  • QWidget:所有可见控件的基类,按钮、输入框、窗口都继承了它。一个没有 parent 的 QWidget(或其子类对象)就是独立窗口;有 parent 的则成为父窗口里的子控件——这也是下一篇对象树机制的入口。
  • resize / showresize() 只是设定尺寸,show() 才真正把控件画到屏幕上。漏写 show() 的程序会”启动了但什么都看不见”。
  • exec():启动事件循环。GUI 程序与命令行程序最大的区别就在这里——main() 并不会一路执行到底,而是停在事件循环里响应外界,这是初学者需要转变的第一个思维。

Warning

最常见的崩溃:把 QApplication 的创建放在了控件创建之后,或者用 if/else 写出”某些分支没有创建 QApplication 就创建控件”的代码。QApplication 必须是第一个创建的 Qt 对象,否则直接异常。

程序运行流程

把上面四步串起来,就是一张 Qt 程序的完整生命周期图:

Qt 程序运行流程:main 创建 QApplication 与控件,show 后进入 exec 事件循环

流程里有两个值得咀嚼的点:其一,exec() 是个阻塞调用,main 函数的大半生命都耗在这里,但它阻塞的是 main 这条执行流,Qt 内部还在源源不断处理事件;其二,exec() 的返回值成为 main 的返回值,它返回意味着事件循环结束、程序即将退出。

Success

调试 GUI 程序时,善用 qDebug() << "标记" 在关键路径上打点。比如怀疑某个槽函数没被调用,就在槽函数第一行加 qDebug 输出——它写入 Qt Creator 底部的”应用程序输出”面板,是 GUI 开发里的 printf 调试法。

本篇小结

工程文件声明模块与源文件,qmake 和 CMake 只是两种表达方式;QApplication 管全局,QWidget 管显示,exec() 启动事件循环让程序”活”起来。下一篇我们让窗口真正”动”起来——用信号与槽响应按钮点击。