☰
Qt5 学习路线:从 79 章补全到 94 章实战避坑指南
2026/10/14 3:51:23 网站建设 项目流程

简介:这份《QT学习之路2-2020整理版》面向希望系统掌握Qt框架的开发者,从零基础入门到进阶实战均可参考。内容基于豆子博客整理,在原有79章基础上补充了2016年后更新的15章,共94章,涵盖Qt简介、信号槽机制、自定义信号槽、模块划分、MainWindow、资源文件、对象模型、布局管理器、菜单栏工具栏状态栏、对话框数据传递,以及多线程、国际化、数据库与网络编程等高级主题,并涉及Qt Creator使用与调试技巧。资源为1个PDF文件,压缩包约5.79MB,单文件结构便于通读与检索。目前已有869人学习,适合按章节顺序研读,也可作为日常开发中查阅信号槽、事件处理与界面布局等知识点的案头参考。

1. 从 79 章到 94 章:这份 445 页的 Qt 学习之路 2 整理版到底补了什么

网上搜 Qt 教程,十篇有八篇指向同一个源头,但版本参差不齐。最常见的情况是:你找到一份 PDF,翻到第 79 章 QML 组件就没了,后面全是空白或者重复的旧内容。这份 2020 年整理版把豆子 2016 年 3 月之后更新的 80 到 94 章补了进来,凑齐 94 章、445 页,从 Qt 简介一路讲到 QML 和 QtQuick 2。它适合两类人:一是刚接触 Qt5 和 QtCreator、需要一条完整学习路径的新手;二是早年看过前 79 章、想补齐后续内容的熟手。整份资料基于 Qt4 与 Qt5 并行介绍的思路,信号槽、事件系统、model/view、多线程、网络、XML/JSON 都有独立章节,不是那种只讲拖控件的速成手册。

2. 环境搭建与第一个 Qt5 工程:从 Qt SDK 选型到 Hello World 跑通

2.1 Qt SDK、Qt Library、Qt Creator 到底装哪个

资料第 3 章把安装选项拆得很清楚,但放到今天的环境里,有些细节需要按实际情况调整。原文列了三个下载项:Qt SDK 是完整开发环境,包含库、IDE 和工具链,Windows 平台大约 1.7G;Qt Library 只有库,适合已经装好环境、只想升级库版本的情况;Qt Creator 是那个轻量级 IDE,原文说最新版是 2.5.2,现在版本号早就翻了几番,但用法逻辑没变。

我一般建议直接装 Qt SDK 或者用官方在线安装器勾选 Qt Creator 加对应版本的 Qt 库。原文特别提醒了一点:Qt SDK 里自带的 mingw 编译器版本往往偏旧,比如当时 SDK 1.2.1 里带的是 mingw 4.4,而编译 Qt5 程序需要 gcc 4.5 以上。这个坑现在依然存在,只是表现形式变了——你装的 Qt 版本和编译器版本不匹配时,qmake 能跑,但 make 到一半报一堆链接错误。

提示:如果你在 Windows 上做 Qt5 开发,优先用官方安装器里配套的 mingw 或 MSVC 套件,不要自己混搭版本。原文提到在 Windows 上编译 Qt5 源码需要 perl、python、git 三件套并且要配好路径,这个操作在 Linux 上简单得多,原文作者用的是 openSUSE。

2.2 用 Qt Creator 建第一个工程并跑通

原文第 3 章 Hello World 部分给的是 Qt4 时代的步骤,但 Qt Creator 的操作逻辑基本没变。下面按 Qt5 工程的实际流程走一遍。

# 假设你已经装好 Qt Creator 和对应 Qt 版本 # 打开 Qt Creator -> File -> New File or Project # 选择 Application (Qt) -> Qt Widgets Application # 工程名填 HelloQt,路径不要有中文和空格 # Build system 选 qmake(原文时代只有 qmake,现在 CMake 也常用) # 基类选 QWidget,取消勾选 Generate form

工程建好后,Qt Creator 会自动生成main.cpp、hellowqt.cpp、hellowqt.h和.pro文件。打开hellowqt.cpp,把构造函数改成:

#include "hellowqt.h" #include <QLabel> HelloQt::HelloQt(QWidget *parent) : QWidget(parent) { // 创建一个 QLabel,父对象设为当前窗口 QLabel *label = new QLabel(tr("Hello, Qt5!"), this); // 设置标签位置和大小 label->setGeometry(50, 50, 200, 30); // 设置窗口标题和初始大小 setWindowTitle(tr("Hello Qt")); resize(300, 150); } HelloQt::~HelloQt() { }

这段代码的逻辑很直白:QLabel是 Qt 里最基础的显示控件,构造时传入this作为父对象,这样 label 的生命周期就挂到了窗口上,窗口销毁时 label 自动释放,不需要手动 delete。setGeometry设置的是相对父窗口的坐标和尺寸,前两个参数是 x、y,后两个是宽、高。tr()是 Qt 的国际化翻译函数,即使现在不做多语言,养成习惯后面省事。

.pro文件里确认有QT += widgets,Qt5 把 widgets 拆成了独立模块,Qt4 时代不需要这一行。点左下角绿色三角编译运行,如果弹出一个小窗口显示 “Hello, Qt5!”,环境就算通了。

2.3 编译报错时先查这三处

新手在这一步翻车的概率不低,常见现象和排查顺序如下。第一,报 “cannot find -lQt5Widgets”,说明.pro里没加QT += widgets,或者 Qt 版本选错了套件。第二,报 “undefined reference tovtable for HelloQt'”,通常是类声明里加了Q_OBJECT宏但没执行 qmake,Qt Creator 里右键工程选 Run qmake 再重新构建。第三,窗口一闪而过或者根本不弹,检查main.cpp里是不是写了return app.exec();`,没有这行事件循环起不来。

3. 信号槽与自定义信号:从 connect 写法到 Qt5 新语法

3.1 信号槽的基本连接方式和参数匹配规则

信号槽是 Qt 区别于其他 C++ 框架的核心机制,原文第 4 章和第 5 章用了大量篇幅讲这个。简单说,信号是对象状态变化时发出的通知,槽是接收通知后执行的函数。连接用QObject::connect,Qt4 和 Qt5 的写法有区别。

// Qt4 旧写法:SIGNAL/SLOT 宏,字符串匹配,编译期不检查 connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(updateValue(int))); // Qt5 新写法:函数指针,编译期检查参数类型 connect(sender, &QSlider::valueChanged, this, &MyWidget::updateValue); // 带参数的 lambda 写法,适合简单逻辑 connect(button, &QPushButton::clicked, this, [this]() { label->setText("按钮被点击了"); });

Qt5 新语法的优势在于编译期就能发现信号和槽参数不匹配的问题,而 Qt4 的宏写法要等到运行时才报 “No such signal” 或者 “No such slot”。原文第 16 章专门讲了 Qt5 信号槽新语法,核心变化就是去掉了宏,改用函数指针。参数匹配规则是:槽的参数个数可以少于信号,但类型必须能隐式转换,反过来不行。

3.2 自定义信号槽的完整流程

原文第 5 章给了一个自定义信号槽的例子,这里按 Qt5 的写法重新走一遍。假设有一个Counter类,内部计数到 10 时发出信号,外部接收后打印。

// counter.h #ifndef COUNTER_H #define COUNTER_H #include <QObject> class Counter : public QObject { Q_OBJECT public: explicit Counter(QObject *parent = nullptr); void startCounting(); signals: // 自定义信号:计数达到阈值时发出,携带当前值 void thresholdReached(int value); private: int m_count; }; #endif // COUNTER_H
// counter.cpp #include "counter.h" #include <QDebug> Counter::Counter(QObject *parent) : QObject(parent), m_count(0) { } void Counter::startCounting() { for (int i = 1; i <= 10; ++i) { m_count = i; if (m_count == 10) { // 发出信号,参数为当前计数值 emit thresholdReached(m_count); } } }
// main.cpp #include <QCoreApplication> #include "counter.h" #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Counter counter; // 连接自定义信号到 lambda 槽 QObject::connect(&counter, &Counter::thresholdReached, [](int value) { qDebug() << "阈值到达,当前值:" << value; }); counter.startCounting(); return app.exec(); }

几个关键点:signals:是 Qt 的宏,展开后是public,所以信号函数本质上是 public 的,但你不应该直接调用它,只能用emit触发。信号只需要声明,不需要实现,moc 会自动生成。emit也是个宏,展开后是空,纯粹为了代码可读性。自定义信号和槽的类必须在头文件里加Q_OBJECT宏,否则 moc 不会处理,连接时会报 “No such signal”。

3.3 连接失败时的排查清单

信号槽连接不上的现象通常是:编译通过,运行时输出 “QObject::connect: No such signal” 或者 “No such slot”,槽函数不执行。排查顺序:第一,检查类声明里有没有Q_OBJECT宏,加了之后有没有重新执行 qmake。第二,Qt4 宏写法下检查信号和槽的签名是否完全一致,包括参数类型和 const 修饰。第三,跨线程连接时检查连接类型,默认AutoConnection在跨线程时会变成队列连接,槽函数在接收者线程执行,如果接收者线程没有事件循环,槽永远不会被调用。第四,信号或槽被重载时,Qt5 函数指针写法需要显式转换,比如static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged)。

4. 事件系统与绘制:从 event() 到 Graphics View Framework

4.1 事件的接受与忽略机制

原文第 18 到 23 章集中讲事件系统,这是 Qt 里比信号槽更底层的一套机制。信号槽是对象间的通信,事件是窗口系统发给应用程序的通知。鼠标点击、键盘按下、窗口重绘,都是事件。Qt 里所有事件都继承自QEvent,处理入口是QObject::event()函数,具体类型的事件再分发给mousePressEvent()、keyPressEvent()这些虚函数。

事件的接受与忽略是原文第 19 章的重点。默认情况下,事件处理函数调用event->accept()表示事件已处理,不会再向上传播;调用event->ignore()表示忽略,事件会继续传给父控件。这个机制在自定义控件里很关键——如果你在子控件里忽略了一个鼠标事件,父控件才有机会处理它。

// 自定义按钮,只处理左键点击,右键点击忽略后传给父窗口 void MyButton::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { qDebug() << "左键点击,自己处理"; event->accept(); } else { // 右键点击不处理,交给父控件 event->ignore(); } }

4.2 事件过滤器与自定义事件

事件过滤器允许一个对象监视另一个对象的事件,不用继承就能拦截。用法是给目标对象安装过滤器,然后在过滤器的eventFilter()函数里处理。

// 在 Widget 构造函数里给 lineEdit 安装事件过滤器 lineEdit->installEventFilter(this); // 实现 eventFilter bool Widget::eventFilter(QObject *watched, QEvent *event) { if (watched == lineEdit && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { qDebug() << "回车被拦截,不传给 lineEdit"; return true; // 返回 true 表示事件已处理,不再分发 } } // 其他情况交给基类处理 return QWidget::eventFilter(watched, event); }

自定义事件的流程是:定义一个继承自QEvent的类,注册事件类型,然后用QCoreApplication::postEvent()或sendEvent()发送。原文第 23 章有完整示例,核心是QEvent::registerEventType()获取一个不冲突的类型值。

4.3 Graphics View Framework 的坐标与场景

原文第 30 章介绍了 Graphics View Framework,这是 Qt 里做复杂 2D 绘图的框架,贪吃蛇游戏那几章就是用它实现的。核心三件套是QGraphicsScene(场景)、QGraphicsView(视图)、QGraphicsItem(图元)。场景管理所有图元,视图负责显示,图元是场景里的基本元素。

坐标系统有三层:图元坐标、场景坐标、视图坐标。图元坐标是相对于父图元的,场景坐标是绝对的,视图坐标是窗口像素坐标。转换用mapToScene()、mapFromScene()、mapToParent()这些函数。原文第 28 章专门讲了坐标系统,这块不搞清楚,图元定位会各种偏移。

// 创建一个场景和视图 QGraphicsScene *scene = new QGraphicsScene(this); QGraphicsView *view = new QGraphicsView(scene, this); // 添加一个矩形图元,位置在场景坐标 (100, 100) QGraphicsRectItem *rect = scene->addRect(0, 0, 50, 50); rect->setPos(100, 100); // 视图设置抗锯齿和渲染提示 view->setRenderHint(QPainter::Antialiasing); view->setSceneRect(0, 0, 800, 600);

贪吃蛇游戏那四章(第 31 到 34 章)是 Graphics View 的实战,把蛇身每个节点做成图元,定时器驱动移动,碰撞检测判断是否吃到食物或撞墙。这个例子值得跟着敲一遍,比单纯看文档理解深得多。

5. 避坑与常见问题:线程、模型视图、文件读写的翻车记录

5.1 线程里直接操作 UI 导致程序崩溃

现象:在QThread::run()里调用ui->label->setText(),程序随机崩溃或者界面卡死。原因:Qt 的 UI 对象只能在主线程操作,子线程直接调用会触发未定义行为。解决:用信号槽跨线程通信,子线程发信号,主线程的槽函数更新 UI。连接类型用默认的AutoConnection即可,Qt 会自动排队到主线程事件循环。

// 子线程类 class Worker : public QObject { Q_OBJECT public slots: void doWork() { // 耗时操作 emit resultReady("完成"); } signals: void resultReady(const QString &result); }; // 主线程里 QThread *thread = new QThread; Worker *worker = new Worker; worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &MainWindow::handleResult); thread->start();

5.2 模型视图里数据改了但视图不刷新

现象:自定义模型里修改了底层数据,调用setData()返回 true,但视图显示没变。原因:setData()里没有发出dataChanged()信号,视图不知道数据变了。解决:在修改数据后发出emit dataChanged(index, index, {Qt::DisplayRole});,角色列表按实际修改的角色填。如果是插入或删除行,必须用beginInsertRows()/endInsertRows()包裹,否则视图和模型的行数会对不上,直接崩溃。

5.3 文本文件读写中文乱码

现象:用QFile加QTextStream读中文文本,读出来是乱码。原因:Qt5 默认用 UTF-8 编码,但 Windows 上很多文本文件是 GBK 编码。解决:显式设置编码,stream.setCodec("GBK");或者用QTextCodec::codecForName("GBK")。写文件时同理,不设置的话默认 UTF-8,用记事本打开可能乱码。原文第 37 章讲文本文件读写时提过编码问题,但当时 Qt4 的默认编码行为和 Qt5 不同,这个坑在 Qt5 里更常见。

5.4 信号槽连接了但槽函数不执行

现象:connect返回 true,编译运行都没报错,但槽函数就是不执行。原因有好几种:接收者对象在连接之后被销毁了,Qt 会自动断开连接;跨线程连接时接收者线程没有事件循环;信号被阻塞了,QObject::blockSignals(true)没恢复;或者槽函数被声明为 private 但用了 Qt4 宏写法,moc 找不到。解决:先确认接收者对象还活着,再检查线程事件循环,最后用QObject::receivers()查一下连接数。

5.5 QML 和 C++ 混合时上下文属性找不到

现象:QML 文件里用contextProperty注册的 C++ 对象,运行时报 “ReferenceError: xxx is not defined”。原因:注册时机不对,rootContext()->setContextProperty()必须在engine.load()之前调用。解决:把注册代码放到QQmlApplicationEngine加载 QML 之前。另外,如果用了qmlRegisterType注册类型,QML 里需要import对应的模块和版本号,版本号不匹配也会找不到。

6. 把 94 章拆成可执行的练习计划:我的三阶段推进法

这份资料 445 页、94 章,从头到尾读一遍很容易前面忘后面。我自己的做法是拆成三个阶段,每个阶段配一个能跑起来的小项目,不追求看完,追求跑通。

第一阶段是基础篇,覆盖第 1 到 30 章,重点是信号槽、事件、绘制系统。练习项目做一个带菜单栏、工具栏、状态栏的文本编辑器,能打开保存文件、能改字体颜色、能弹对话框。这个项目把 MainWindow、动作、资源文件、对话框、文件读写、事件处理全串起来了。做完这个,Qt 的 GUI 编程套路基本就熟了。

第二阶段是进阶篇,覆盖第 35 到 75 章,重点是文件、容器、model/view、数据库、XML/JSON、网络、多线程。练习项目做一个简易的本地数据管理器,用 SQLite 存数据,用QTableView加自定义模型显示,支持增删改查和排序过滤,数据能导出成 JSON。这个项目把 model/view 和数据库操作绑在一起练,比单独看章节效果好得多。

第三阶段是 QML 篇,覆盖第 76 到 94 章,重点是 QML 语法、基本元素、组件、和 C++ 的交互。练习项目做一个简单的待办事项应用,界面用 QML 写,数据逻辑用 C++ 写,通过信号槽和上下文属性通信。这个项目跑通,Qt Quick 的混合开发流程就清楚了。

每个阶段的时间分配大概是:基础篇两周,进阶篇三周,QML 篇两周。不用赶进度,每个练习项目跑通再往下走。原文第 1 章序言里豆子说这个系列定位是入门教程,穿插了一些实现层面的内容但不深入。我的体会是,入门阶段最怕的是只看不练,信号槽、事件、model/view 这三块,不亲手写一遍代码,光看文字很难真正理解。

从那以后我每次带新人,都强制要求先跑通一个最小可运行工程再去看理论章节,不然看多少章都是黑匣子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询