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
用了
QWidget、QPushButton等界面类却没在构建文件里声明 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 / show:
resize()只是设定尺寸,show()才真正把控件画到屏幕上。漏写show()的程序会”启动了但什么都看不见”。 - exec():启动事件循环。GUI 程序与命令行程序最大的区别就在这里——main() 并不会一路执行到底,而是停在事件循环里响应外界,这是初学者需要转变的第一个思维。
Warning
最常见的崩溃:把 QApplication 的创建放在了控件创建之后,或者用 if/else 写出”某些分支没有创建 QApplication 就创建控件”的代码。QApplication 必须是第一个创建的 Qt 对象,否则直接异常。
程序运行流程
把上面四步串起来,就是一张 Qt 程序的完整生命周期图:
流程里有两个值得咀嚼的点:其一,exec() 是个阻塞调用,main 函数的大半生命都耗在这里,但它阻塞的是 main 这条执行流,Qt 内部还在源源不断处理事件;其二,exec() 的返回值成为 main 的返回值,它返回意味着事件循环结束、程序即将退出。
Success
调试 GUI 程序时,善用
qDebug() << "标记"在关键路径上打点。比如怀疑某个槽函数没被调用,就在槽函数第一行加 qDebug 输出——它写入 Qt Creator 底部的”应用程序输出”面板,是 GUI 开发里的 printf 调试法。
本篇小结
工程文件声明模块与源文件,qmake 和 CMake 只是两种表达方式;QApplication 管全局,QWidget 管显示,exec() 启动事件循环让程序”活”起来。下一篇我们让窗口真正”动”起来——用信号与槽响应按钮点击。
自测:三个问题检验掌握程度
.pro里QT += widgets和 CMake 里target_link_libraries(... Qt6::Widgets)分别解决什么问题?- QApplication 必须在什么时机创建?exec() 的返回值代表什么?
- 程序能编译链接通过,但运行后一闪而过或什么都看不见,最可能漏写了哪一句?
(答案都在上文)