
QT001是我给自己的第一个真正意义上的Qt程序起的名字从编号就能看出来这系列日记写到第24篇终于鼓足勇气开始碰代码了。之前大部分时间都花在看文档、配环境、照着别人的截图点点点这次不一样我要自己把整个工程从创建到运行走一遍亲眼看着一个窗口从代码里长出来。这一篇日记主要记录三件事QT001这个程序到底是干什么的、它的代码长什么样、以及我在编译运行过程中踩到的那些坑。文章里所有的代码、操作、报错都是我在自己电脑上跑过的没有美化也没有跳过细节。如果你也正卡在工具装好了但不知道怎么写第一个程序这个阶段这篇应该能帮上忙。1. 今天要解决的事情让QT001从代码变成窗口1.1 为什么程序名叫QT001很多人学框架的第一件事就是照着教程敲一个Hello World我也这么干过但Qt和命令行程序不一样它折腾了半天最后屏幕上只是弹出一个窗口里面什么都没有。这个落差很容易让人觉得那我到底学会了什么QT001就是为了回答这个问题而存在的。我不追求花哨的功能目标只有三个创建一个窗口、让窗口能正常显示、让窗口能正常关闭。三个目标看起来很简单但它们背后牵扯到Qt最核心的一套运行机制把这套机制跑通了后面学按钮、输入框、列表控件都是在这个基础上做加法。我给程序起名字的时候刻意不用HelloWorld这种烂大街的命名因为后续还会有QT002、QT003我希望每一篇日记对应一个独立的小程序每个程序都解决一个具体的小问题这样积累到QT010的时候我手里就有十个能跑的Demo而不是十个互相抄袭的HelloWorld。1.2 QT001的验收标准代码这个东西写之前一定要先定验收标准不然写完了都不知道自己算不算做完了。我给QT001定了四个硬性指标程序启动后出现一个800x600的空白窗口标题显示QT001 - 我的第一个窗口窗口能在屏幕上正常拖动不会卡顿不会白屏点窗口右上角的关闭按钮程序能完整退出进程不残留从双击运行到窗口出现整个过程不能有任何报错输出这四个标准看起来随意但其实每一项都在考验Qt的不同环节。第一条考验的是窗口系统和尺寸设置第二条考验的是事件循环是否正常运转第三条考验的是程序退出时资源有没有被正确释放第四条考验的是整个开发环境是不是干净的。写程序最怕的就是能跑就行的心态问题是能跑和跑得对之间差着十万八千里。QT001的目标不是能跑而是跑得明白。2. 环境准备动手写代码之前的最后一道坎2.1 我用的开发环境组合在记代码之前先把环境交代清楚。我的这套组合是两个星期前踩了无数坑之后定下来的直接照着配就行不要在这个阶段自作聪明操作系统Windows 10 64位Qt版本5.15.2开源版编译器MinGW 8.1.0 64-bitIDEQt Creator 4.14.2构建方式qmake这套组合最大的优点是省心。Qt 5.15.2是Qt 5系列的最后一个LTS版本资料多、坑少、网上随便搜一个报错都能找到解决方案不像最新的Qt 6那样代码写法变了但网上大部分教程还停留在Qt 5。MinGW是Qt自带安装包里的编译器不用额外装Visual Studio也不用配复杂的路径。对我这种新手来说减少一个变量就少踩一个坑。如果你用的是Qt 6以上版本这篇日记里的代码大概率也能跑但有些头文件的路径和写法会有出入建议先发现问题再翻文档不要上来就照抄。2.2 创建工程时最容易踩的坑打开Qt Creator点击新建项目选Application下的Qt Widgets Application然后一路下一步。看起来没什么难度但这里藏着三个坑我挨个说第一个坑是项目名称。项目名称不能带中文不能有空格最好是纯英文加数字。我一开始给项目起名叫第一个程序Qt Creator倒是没拦我但编译的时候直接报了一堆看不懂的错误后来查了半天才知道是路径里的中文搞的鬼。所以项目名就叫QT001干净利落。第二个坑是构建套件的选择。你电脑上可能同时装了MSVC、MinGW、甚至还有别的编译器选哪个很讲究。MSVC编译出来的程序依赖Visual C运行库换一台电脑跑可能就报错MinGW编译出来的程序相对独立。而且MSVC和MinGW的调试器不通用如果你选错了套件运行程序之后可能需要单独去配置调试器。新建向导里Kit的选择页面看到MinGW 64-bit就选它不要犹豫。第三个坑是默认生成代码。向导会自动帮你生成main.cpp、mainwindow.cpp、mainwindow.h和工程文件很多人打开之后看代码一片茫然然后就开始乱改改坏了又不知道怎么恢复。正确的姿势是什么都不动先按运行按钮等确认默认的空白窗口能跑起来再去改代码。这句话值得记下来先让模板跑通再谈改模板。3. 逐行拆解QT001的完整代码3.1 工程文件.pro里到底写了什么工程文件是整个Qt项目的地基它的作用是告诉构建系统这个项目里有哪些文件、需要哪些模块、最终生成的程序叫什么名字。在新手阶段你可能不需要手写这个文件但必须能看懂它。我把QT001自动生成的.pro文件贴出来一行一行解释QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET QT001 TEMPLATE app DEFINES QT_DEPRECATED_WARNINGS SOURCES \ main.cpp \ mainwindow.cpp HEADERS \ mainwindow.h第一行QT core gui的意思是这个项目需要core模块和gui模块。core模块是Qt的基础设施任何Qt程序都离不开gui模块负责窗口和图形界面的底层支持。到了Qt 5以上版本窗口控件的代码独立成了widgets模块所以第二行用greaterThan做了个版本判断如果Qt主版本号大于4就额外加上widgets。target是最终生成的exe文件名template声明这个项目是一个应用程序而不是库。sources和headers分别是源文件和头文件列表。有一件事新手容易忽略你每新建一个.cpp或.h文件都要在.pro里添加一行。Qt Creator一般会自动加但如果你用其他编辑器写代码或者手动改了文件名.pro没更新一编译就会报找不到文件或者更诡异的下划线错误。3.2 main.cpp三行代码背后的运行机制main.cpp是程序的入口也是我第一次觉得Qt神奇的地方。完整代码如下#include mainwindow.h #include QApplication int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }去掉空行核心逻辑只有四步创建QApplication对象、创建主窗口对象、让窗口显示出来、进入事件循环。很多教程会用这段代码创建了一个窗口一笔带过但我觉得这里值得深挖一层因为理解这四行代码的顺序几乎等于理解了Qt整个运行模型。第一步QApplication a(argc, argv)。这个对象是整个Qt程序的命脉它负责初始化Qt运行环境、管理全局资源、处理系统级事件。没有它后面创建窗口的代码会导致程序直接崩溃。一个Qt程序有且只能有一个QApplication对象。这句话等号右边是规则左边是崩溃现场你可以在一个程序里写两个QApplication试试第二个构造时就会收到明确的错误信息。第二步MainWindow w。这一行创建了主窗口对象。注意这个对象是一个普通的C栈对象不是指针也不需要delete。它定义在main函数里生命周期一直持续到main函数结束。Qt官方在Widget窗口类上专门设计了一个属性叫如果父对象是nullptr就是顶级窗口这里w没有父对象所以它就会作为独立的顶级窗口出现在桌面上。第三步w.show()。这一步特别容易和创建窗口搞混。w被构造出来的时候窗口对象是存在的但它是隐藏的。不调用show程序能正常运行屏幕上什么都看不到。show只是把窗口设置为可见状态这一步不做你的程序就是正在运行但无处可寻。第四步return a.exec()。这是最难理解的一步。exec()会进入一个死循环Qt管它叫事件循环。在这个循环里程序持续等待并分发事件比如鼠标点击、按键、窗口重绘每一个动作都被封装成事件塞进事件队列然后由exec()把它们分发到对应的控件上。打个比方前面的代码是把舞台搭好、演员请上台exec()是大幕拉开演出开始。这层大幕一旦拉上只有所有窗口被关闭、程序收到退出的信号exec()才会返回程序才会结束。所以你看到main函数最后一行写return a.exec()就是等演出结束把结果带回去。3.3 窗口类MainWindow的头文件和实现Qt的Widget程序会自动生成一个MainWindow类它继承自QMainWindow。QT001还没用到菜单栏、工具栏、状态栏所以这个类目前是个空壳但它存在的意义特别大因为后续所有功能代码都会往这个类里面塞。先看头文件#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); }; #endif // MAINWINDOW_H这里最显眼的Q_OBJECT是一个宏且只能出现在类定义的私有区它展开后会生成一堆成员函数声明让这个类获得信号槽机制的支持。新手阶段不需要手动写信号槽但要让这个宏留在那里。你要是觉得没用把它删了Qt会在编译的时候报出一串跟元对象相关的错误。还有#ifndef MAINWINDOW_H这种写法叫头文件保护宏作用是一份头文件在同一个编译单元只加载一次。这是C的惯例写法不用自作聪明改名字每个头文件都有自己独特的宏名就行。实现文件长这样#include mainwindow.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { resize(800, 600); setWindowTitle(QT001 - 我的第一个窗口); } MainWindow::~MainWindow() { }构造函数里只有两个操作resize设置窗口初始大小为800x600像素setWindowTitle设置标题栏文字。这两行就是我在验收标准里说过的窗口尺寸和标题的实现。可能有细心的人会问为什么resize后面不写setFixedSize因为setFixedSize会把窗口锁定成固定大小你不能拖拽改变窗口尺寸后续接手的人可能还想在这个窗口里加东西到时候他拖不动窗口排查半天最后发现是当时手贱写了setFixedSize。用resize是给窗口建议一个尺寸而不是命令它用户体验上更灵活。parent nullptr这个默认参数也别忽略。它的意思是这个窗口默认没有父窗口是顶级窗口。如果传一个父窗口进去这个窗口就会自动出现在父窗口内部随父窗口一起关闭一起销毁。这个机制叫父子关系是Qt内存管理的核心之一。我再补充一个QT001阶段就能用到的记忆技巧**父窗口负责销毁子窗口所以子窗口用new创建也不用delete父窗口销毁时Qt会自动清理子对象。**很多内存泄漏的代码就是绕过了这个规则才出事的。4. 编译运行的完整流程从代码到窗口4.1 三种构建方式我推荐先会手动Qt Creator里点那个绿色三角就能直接跑程序但在QT001阶段我强烈建议你至少手动编译一次。这不是自找麻烦而是因为在点三角就能跑的背后Qt默默帮你做了很多事而这些事恰恰是理解Qt的关键。一个Qt程序从源码到可执行文件至少要经过两个阶段qmake阶段和make阶段。qmake的作用是读.pro文件生成一个Makefilemake的作用是根据Makefile里的规则去编译每一个.cpp文件、链接最终的程序。Qt Creator的构建按钮只是把这两个步骤打包了跟你手动执行没有本质区别。在终端里在一个干净的构建目录下运行这两条命令qmake ../QT001/QT001.pro mingw32-makeqmake会生成Makefilemingw32-make会按规则编译。第一遍编译QT001这么小的工程可能只需要几秒但屏幕上会刷出一堆类似mainwindow.o、moc_mainwindow.cpp这样的中间产物。见到moc开头的文件不要慌它是Qt的元对象编译器自动生成的代码Q_OBJECT宏能正常工作就靠它。整个过程能看到就说明环境配置没问题。如果你在终端里运行命令提示找不到qmake大概率是环境变量没配好。先检查Qt安装目录的bin文件夹下有没有qmake.exe再把路径加到系统环境变量里。这一步在Qt Creator里被隐藏了因为IDE会自己找qmake但到了命令行环境Windows的PATH不认识qmake就是一串不是内部或外部命令的报错。4.2 第一次运行的顿悟时刻构建成功之后可以去构建目录里找到QT001.exe双击运行。那一刻屏幕上弹出的窗口就是QT001从我原来只是存在于编辑器中的代码变成了可以被别人双击打开的程序的转折点。但这种事情只靠眼睛看一遍是不够的。我建议你顺手做两个实验能帮你把前面讲的事件循环理解得更深第一个实验在main.cpp的w.show()后面加一行qDebug() show called;在return a.exec();后面加一行qDebug() app exited;。运行程序你会发现show called在窗口弹出来之前就打印出来了而app exited要等你关闭窗口之后才会打印。这说明exec()确实阻塞在那边它把程序挂住处理事件直到事件循环结束才返回。第二个实验把w.show()注释掉重新编译运行。程序一样能启动、进程一样在运行但屏幕上什么都不显示只能去任务管理器看到QT001.exe在后台待着。这个实验最大的价值是让你分清创建对象和显示对象是两步完全不同的操作。很多新手的程序看起来没反应不是因为窗口没创建而是因为忘了show。5. 新手最容易遇到的报错与解决全程实录5.1 编译报错速查表我在写QT001的过程中前前后后大概遇到了七八种报错这里整理成一张表全是实战记录报错信息典型原因解决方案无法打开文件mainwindow.h: No such file文件路径不对或.pro里没写HEADERS检查源码文件是否在工程目录下检查.pro的HEADERSerror: unknown type name QMainWindow头文件没include或模块没加检查是否#include QMainWindow注释掉Q_OBJECT可能导致伴随报错error: cannot find -lQt5Widgetsd编译器与Qt库位数不一致检查Kit里选的编译器是32位还是64位和安装的Qt是否匹配undefined reference to vtable for MainWindow漏掉了moc文件或Q_OBJECT被删除别手动删Q_OBJECT删除build目录重新构建中文显示成乱码源文件编码不对Qt 5下默认utf-8用Notepad或QCreator转成UTF-8编码双击exe提示缺少Qt5Core.dll、Qt5Widgets.dll动态链接库路径没配用windeployqt工具自动拷贝依赖库后面细说这张表里我想特别展开一下undefined reference to vtable这个问题。Qt的程序编译过程跟普通C不一样它有一个元对象编译器moc的步骤。每当头文件里出现Q_OBJECT这个宏moc就会扫描这个头文件单独生成一个moc_mainwindow.cpp源文件然后在链接阶段把它一起编进去。如果你手动删除了Q_OBJECT或者构建系统没有自动把moc文件加进来就会出现上面那个未定义引用的报错。新手看到这种报错很懵因为报错信息里根本不会出现moc这个词但排查方向基本就是Q_OBJECT对不对、moc有没有生效。5.2 双击运行闪退的排查思路编译通过、在Qt Creator里能跑、但双击exe却打不开窗口这是QT001阶段最容易出现的环境依赖问题。我没有用调试器用的是物理排查法步骤如下第一步看应用启动后有没有进程。打开任务管理器如果QT001.exe出现了又消失说明程序本身能启动但可能崩溃在某个初始化阶段。如果根本没出现说明缺DLL系统连启动条件都不满足。第二步缺DLL的话Qt安装目录自带一个工具叫windeployqt它能把程序需要的所有Qt动态库拷贝到exe同一目录。在命令行里进入构建目录执行windeployqt QT001.exe再双击运行基本能解决。第三步如果进程出现但窗口没弹出来就在main.cpp里加两行日志看程序卡在哪。我遇到过一次是QApplication构造时直接abort最后发现是我同时装了Qt 5和Qt 6环境变量里PAT的qmake指向混乱exe运行时加载了不同版本的Qt库两个版本相互打架。这个问题的根治方法是把不需要的Qt版本从PATH中清理掉只留当前使用的版本。5.3 隐藏的坑Debug和Release版本别混用Qt Creator下方有个小图标可以切换Debug和Release两种构建模式。新手可以理解为Debug版包含调试信息方便单步跟踪但运行速度慢、体积大Release版做了优化体积小是最终要发给别人的版本。很多人没注意这两套配置的区别运行时不小心把Debug生成的exe和Release构建目录混在一起就会导致加载的库不对应出现奇怪的崩溃。我的建议是学习阶段一直用Debug就行所有排查都基于Debug版等到QT001版本稳定了再去试一下Release构建。两种配置的构建目录本身是分开的你会在工程目录下看到build-QT001-Desktop_Qt_5_15_2_MinGW_64_bit-Debug和...-Release两个文件夹这是Qt Creator刻意分开的不要手动去乱复制。6. 到这一步我觉得学Qt的基本节奏是这样的QT001虽然只是一个空白窗口但我整个过程走下来收获最大的不是我会用Qt了而是我终于知道Qt程序是怎么跑起来的了。从.pro到qmake从QApplication到事件循环从moc到动态库依赖这些东西在任何一个图形界面框架里都是绕不开的。学Qt的路线图我个人觉得分成三阶段第一阶段就是QT001这种跑通一个窗口理解整个构建链条和事件循环模型建立程序不是从第二行代码开始跑的全局观。第二阶段是把常用控件摸一遍按钮、标签、输入框、列表、滑动条每个控件拿一个小Demo去点一点感受信号和槽是怎么连起来的。这个阶段不要图快每学一个控件就想想底层是什么机制让它在工作。第三阶段才轮到布局管理器、自定义控件、绘图这些更深的东西。很多人一上来就啃布局管理器啃到一半就放弃我觉得很可惜因为布局真的是后面才需要的东西。QT001我是建议不要急着往下走先把那个空窗口反复玩明白。试着给它改颜色、加一个固定的标题、把尺寸调大调小每次改完看变化其实就是在积累手感。代码这个东西看100遍不如自己改10遍改10遍不如自己从头写1遍。如果你已经能独立完成QT001的创建、编译、运行那恭喜你已经跨过Qt最陡的那道坡了。下一篇日记我打算在QT001的基础上加一个按钮试试信号槽到底是个什么脾气到时候再把这中间的新坑记下来留给跟我一样从零开始折腾的人看。