Qt 初体验:第一个窗口程序踩的那些坑
大三了,想着暑期得找份实习,翻 BOSS 直聘看了一圈 C++ 岗,发现"熟悉 Qt"几乎是个标配技能。学校虽然开过 Qt 课,但当时就跟着老师拖了几个控件做了一个小作业,原理基本一窍不通,纯属应付完事。
正好这阵子课不算忙,决定自己重新学一遍 Qt,目标是能独立写点小工具放到简历里凑项目经历。这篇就记一下学 Qt 头一天踩的几个坑,主要是搞清楚一个最简单的窗口程序到底是怎么跑起来的,给自己留个备忘,也希望同样在自学 Qt 的同学能少走点弯路。
Hello World:用代码版还是 Designer 版
打开 Qt Creator,新建项目走 Qt Widgets Application,一路下一步选 MinGW Kit。生成出来五个文件:
helloworld/
├── helloworld.pro # 工程描述
├── main.cpp # 入口
├── widget.h
├── widget.cpp
└── widget.ui # 这玩意是 Designer 用的
我第一反应是双击 widget.ui 进 Designer,拖一个 QLabel,改文字"Hello World",按 Ctrl+R 跑------成了,但我心里其实有点虚。因为整个过程里我啥也没写,IDE 帮我藏了太多东西。万一 Designer 出 bug 或者我想动态改界面,根本不知道从哪儿下手。
所以我又重新搞了一遍纯代码版的。把 .ui 那行从 .pro 里删掉,然后在 widget.cpp 的构造函数里手动 new 控件:
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setWindowTitle("Hello Qt");
resize(480, 320);
// 注意这个 this,后面专门吐槽
QLabel *label = new QLabel("Hello, Qt!", this);
label->setAlignment(Qt::AlignCenter);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(label);
}
跑起来效果是这样:

第一个真正让我懵的概念:对象树
上面那段代码里 new QLabel("Hello, Qt!", this) 那个 this 是干啥的?我一开始以为是"指定 label 显示在哪个窗口里"。
直到我手贱试了一下不传 this:
cpp
QLabel *label = new QLabel("Hello, Qt!"); // 没传 parent
label->setAlignment(Qt::AlignCenter);
// 跑起来发现 label 根本不在窗口里!
// 它变成一个独立的小窗口飘在桌面上某个角落,而且我没 show() 它,所以你压根看不见
查了 Qt Assistant 才搞明白:new 一个 QWidget 时传的那个指针不是"放到哪里",是"谁是它爹" 。Qt 有个东西叫对象树------你给一个控件指定 parent 之后:
- 它会自动作为 parent 的子控件渲染
- 当 parent 析构时,所有 child 自动 delete,不用你管
这个机制让 C++ 写 GUI 不用动不动手动 delete,挺香的。但坑也来了:
cpp
// 我一开始这么写的,觉得既然 new 了就该自己 delete
QLabel *label = new QLabel(this);
// ... 用完了想清理
delete label;
// 然后程序退出时 this 析构,Qt 又 delete 了一次这个 label
// 程序崩溃,直接 abort
正确做法是要么交给 Qt 管,要么自己 setParent(nullptr) 把它从对象树脱离再 delete。这事儿文档其实写了,但我没仔细看,被坑了大概 20 分钟。
为了把这个事儿讲清楚,我在窗口里加了俩按钮做实验:一个"加一个子控件",一个"看看我有几个孩子"。点几下看效果:


注意看:我点了两次"加一个子控件",眼睛看到的可见控件是2 个标题 label + 2 个按钮 + 2 个新加的 label = 6 个 ,但 children() 数出来是 7。
多出来那个是 QVBoxLayout ------ 布局管理器也是 QObject,也算 child 。这个细节文档上有写,但藏得挺深,我是 debug print 了一遍 children() 列表里每个对象的 className 才反应过来。
.pro 文件其实就是一行行的赋值
之前看 .pro 觉得是某种神秘配置,后来发现就是 qmake 自己定义的 DSL,本质就是变量赋值:
pro
QT += core gui widgets # 用哪些 Qt 模块,widgets 必须有
CONFIG += c++17 # 编译选项
TARGET = helloworld # 输出 exe 名字
TEMPLATE = app # app=可执行,lib=动态库
SOURCES += main.cpp widget.cpp
HEADERS += widget.h
+= 是追加,= 是覆盖。语法有点像 Makefile 又不太像,反正记住几个关键变量就够用。
一个新手必踩的坑:改了 .pro 之后必须 "Run qmake" 一次 ,不能直接 Ctrl+R。否则 qmake 不会重新生成 Makefile,你新加的源文件根本不会被编译。Qt Creator 里右键项目能看到这个菜单项。
必须有的 Q_OBJECT 宏
widget.h 里有这么一行:
cpp
class Widget : public QWidget
{
Q_OBJECT // ← 这个
public:
// ...
};
我第一次自己写 Qt 类的时候没加这个,自定义信号槽,编译器给我报:
undefined reference to `vtable for MyClass'
链接错误,看上去跟 Q_OBJECT 八竿子打不着。查了半天才发现是这个宏漏了。
原理:Qt 的信号槽、属性、动态调用这一套不是纯 C++ 能实现的,需要一个叫 moc (meta object compiler)的预处理器,扫描带 Q_OBJECT 的类,生成额外的 moc_xxx.cpp 文件来支撑这些功能。没这个宏 moc 就跳过你这个类,链接的时候找不到那些"虚的"函数。
记住一条规则:只要类里用了信号槽,就必须加 Q_OBJECT。
一些零碎踩坑
- 中文显示成
???:源文件保存编码改成UTF-8 (with BOM),或者用QStringLiteral包字符串。我后来全用 UTF-8 BOM 省事。 - 改了头文件后编译异常 :删掉
build-xxx/目录重新构建。Qt Creator 的 shadow build 有时候缓存抽风。 Ctrl+R提示找不到 Kit :去Edit → Preferences → Kits看一下,红色感叹号的话点进去看缺啥(一般是 Qt Versions 或 Compilers 没指定)。- 运行起来报缺
Qt6Core.dll:这是你想把 exe 拷到别的地方跑出来的报错。要把 Qt 的bin/目录加到 PATH,或者用windeployqt.exe your.exe一键复制所有依赖。
一点感想
Qt 这套机制有点反 C++ 直觉------对象树自动管内存、moc 生成元信息、信号槽这种"魔法"。一开始挺别扭,会觉得"这不像我熟悉的 C++"。
但跑通几个 demo 之后慢慢发现,Qt 是在用一套自己的运行时把 C++ 改造成"更适合写 GUI 的 C++"。你不需要每个控件手撸智能指针,不需要自己注册回调,不需要纠结跨线程通信。它把这些复杂度都吞下去了。
代价就是有自己的一套思维方式要学,而不是一上来就能写。我大概折腾了一晚上才有点感觉,先记到这。下次想搞搞信号槽,把按钮点击之后干点别的事儿这套流程跑顺。
代码我放在自己机器的 D:/QT_Learn/projects/ch01_helloworld/ 下面,编译命令是:
bash
qmake.exe ch01_helloworld.pro
mingw32-make.exe release
./release/ch01_helloworld.exe
如果有人也在折腾 Qt 入门,欢迎评论区交流踩过的坑。