简介:《Qt5范例开发大全(完整目录版)》PDF 面向具备一定 C++ 基础、希望系统掌握 Qt5 桌面应用开发的读者,也可供有经验的开发者查阅控件用法与工程实践。内容以范例驱动,完整目录覆盖开发环境搭建、窗体与控件应用、组件使用、文件操作、图形图像与多媒体处理、系统操作、注册表、数据库基础、网络开发、进程与线程、数据安全及打包部署等十四章,便于按主题定位知识点并对照练习。资源包共 1 个 PDF 文件,大小约 2.84MB,单文件结构清晰,适合在电脑端检索目录、通读章节或随查随用。目前已有 2459 人学习,说明其在 Qt5 入门与实战查阅场景中具有一定参考价值。读者可从中获得从 Hello World 到界面布局、多窗体交互,再到网络、数据库和部署发布的完整学习线索,也能积累常见控件的使用范例与问题解决思路。
1. 从一个反复重写的无边框窗口说起
接手一个 C++ Qt5 的桌面小工具时,最容易翻车的往往不是算法,而是界面:加一个无边框窗口,鼠标拖动逻辑就得再抄一遍;想改一处背景色,翻遍三个 .cpp 才找到那句setStyleSheet。这种重复劳动攒到第四个窗口,项目基本就失控了。《Qt5范例开发大全》按 14 个模块组织目录——开发环境、窗体、控件、组件、文件、图形图像、多媒体、系统、注册表、数据库、网络、线程、数据安全、打包部署,恰好把这类“写完就忘、改一处崩一片”的问题拆成了可检索的索引。这份资源适合三类人:第一次做 C++ Qt5 课程设计的学生、要交毕设需要一份完整功能清单的人,以及从别的语言转过来想快速对齐 Qt API 习惯的从业者。它不负责讲深 C++ 语法,负责让你在对号入座之后知道下一行该写什么。
2. 开发环境与窗体骨架:从 Hello World 到可交付主界面的最小闭环
很多人跳过环境直接写界面,结果第一次打包就卡在“换台机器双击没反应”。骨架这一层要解决两件事:一是让工程从建立那一刻起就有稳定的目录约定,二是把窗体的生命周期、外观和拖动这类高频需求一次性写好,后面所有窗口都从这里复制。
2.1 Qt5 + MinGW 环境搭建与工程目录约定
安装环节本身不复杂,选 Windows 32 位、MinGW 编译器、带 OpenGL 的离线包,一路下一步即可。真正容易被忽略的是环境变量:把 Qt 的 bin 目录追加进系统 PATH,之后编译出来的 exe 才能直接双击运行,而不用每次手动把一堆 DLL 拖到 exe 旁边。这一步做完,调试期的验证成本会低很多。
# 把 Qt 运行时目录追加进 PATH,注意分号是 Windows 下的分隔符 # 路径按自己的实际安装位置改,不要照抄盘符 set PATH=%PATH%;D:\Qt\Qt5.1.1\5.1.1\mingw48_32\bin这条命令只在当前命令行会话生效,关掉窗口就失效。要长期有效,得进系统属性的环境变量面板里改,改完重开终端。参数上唯一需要注意的是末尾那个分号,漏掉会把原路径和新路径粘成一串,系统找不到任何命令。
工程目录我一般按固定结构铺开,不让编译产物污染源码目录:
| 目录 / 文件 | 作用 | 是否纳入版本管理 |
|---|---|---|
| QtOne.pro | 工程描述,qmake 据此生成 Makefile | 是 |
| main.cpp | 程序入口,创建 QApplication 与主窗体 | 是 |
| mainwindow.h / .cpp | 主窗体的声明与实现 | 是 |
| resource/image.qrc | 资源索引,图标、图片、样式表统一走这里 | 是 |
| debug / release | 编译产物,含 exe 与中间 obj | 否 |
工程描述文件是整个项目的黑匣子,出问题时第一个该看的就是它:
# core 与 gui 是基础模块,widgets 在 Qt5 里已经从 gui 中单独拆出来了 QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = QtOne # 生成的 exe 文件名 TEMPLATE = app # app 表示应用程序,lib 表示库 SOURCES += main.cpp \ mainwindow.cpp HEADERS += mainwindow.h RESOURCES += resource/image.qrc # 漏了这行,运行时所有 :/ 开头的路径都是空greaterThan(QT_MAJOR_VERSION, 4)这行的作用是兼容 Qt4 与 Qt5 双版本:Qt5 把 widgets 从 gui 里拆出来独立成模块,不写这句,在 Qt5 下编译会报一堆找不到 QApplication 的错。TEMPLATE = app决定 qmake 生成的是应用程序还是动态库,初学阶段改了它会出现“编译通过但没有 exe”的怪现象,通常的做法是新建工程而不是手改。RESOURCES那行最容易漏,因为它漏掉时编译器不报错,只有运行时图标是空白。
2.2 三种窗体基类的选型判断
新建工程时编辑器会给三个基类选项,选错后面要重构。判断标准其实很朴素:需要菜单栏、工具栏、状态栏和中央部件的,用 QMainWindow;需要弹一个短期任务窗口、并且要拿到用户“确定/取消”结果的,用 QDialog;其余自定义面板、异形窗口、嵌入用的容器,一律 QWidget。
QMainWindow 自带布局骨架,中央部件只能有一个,多出来的内容要么塞进停靠窗口,要么自己套一层 QWidget 再放布局。QDialog 支持模态与非模态,exec()会阻塞并返回一个整型结果,登录框、设置页用它最省事。QWidget 是所有界面对象的基类,最轻,也最自由,代价是什么都得自己搭。
入口文件的写法固定,但每一行的作用值得记住:
#include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); // argc/argv 交给 Qt 做命令行参数解析 MainWindow w; w.show(); // show 只是把窗口标记为可见 return a.exec(); // exec 进入事件循环,窗口才开始响应鼠标键盘 }a.exec()是整段代码的命门。把它删掉,窗口会一闪而过;把它放在show()之前,窗口根本不会出现。参数argc和argv不是摆设,Qt 内部会用它们处理-style、-platform这类启动参数,做跨平台调试时能救命。
2.3 窗体外观与交互的六个可复用片段
窗体层的需求高度重复,把这六段写好,后面每建一个窗口直接复制粘贴改参数就行。
固定尺寸靠最大值和最小值同时设置,只设其中一个,用户还是能单向拉伸:
this->setWindowTitle("固定尺寸窗体"); this->setMaximumSize(300, 300); // 上限 this->setMinimumSize(300, 300); // 下限,与上限相等即锁死初始位置用move(100, 100),它相对屏幕左上角;setGeometry则是“位置 + 尺寸”一次到位。默认情况下窗体是居中的,如果你在构造函数里调了 move 却看不出效果,检查一下是不是在 show 之后又被布局管理器拉回去了。背景色用setStyleSheet("background:red")最快,但要注意样式表会向下继承,父窗体染红之后,里面的 QLabel、QPushButton 也会跟着红。通常的做法是加对象名限定:setStyleSheet("QWidget#centralWidget{background:red}")。
标题栏图标需要走资源系统,不是把 ico 丢进目录就完事。先新建 .qrc 资源文件,添加前缀,再把图片加进去并起别名,最后:
// 别名起作 ico 时,路径就是 :/前缀/ico;不起别名则要写完整的文件名带后缀 this->setWindowIcon(QIcon(":/new/prefix1/ico"));无边框窗体的拖动是翻车率最高的一段,关键在于三个鼠标事件的配合。用全局坐标而不是相对坐标,窗体移动时才不会抖:
// 头文件里必须声明为 protected,否则不会覆盖基类实现,事件根本进不来 protected: void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; private: QPoint last; // 上一次的全局坐标,用来算位移// 构造函数:去掉标题栏之后,系统关闭按钮也没了,得自己补一个 setWindowFlags(Qt::FramelessWindowHint); QPushButton *btClose = new QPushButton(this); btClose->setText("关闭"); btClose->move(260, 260); connect(btClose, SIGNAL(clicked()), this, SLOT(close())); void MainWindow::mousePressEvent(QMouseEvent *e) { last = e->globalPos(); // 相对屏幕的坐标,不受窗体自身位置影响 } void MainWindow::mouseMoveEvent(QMouseEvent *e) { int dx = e->globalX() - last.x(); int dy = e->globalY() - last.y(); last = e->globalPos(); move(x() + dx, y() + dy); // 当前窗体位置 + 本次位移 } void MainWindow::mouseReleaseEvent(QMouseEvent *e) { Q_UNUSED(e); // 松手时不需要再移动,留空实现即可 }这段代码里最值得说的是mouseReleaseEvent。有些示例会在松手时再算一次位移并 move,看起来没问题,实际用起来会有“甩尾”的观感——鼠标已经停了,窗体还往前窜一小段。原因是从按下到松手的整个过程里,move 事件已经把位移消费掉了,松手那次是重复计算。我一般留空,只在按下和移动两处处理。
去掉最大化、最小化按钮同样靠窗口标志位,在setWindowFlags里用按位或组合Qt::WindowMinimizeButtonHint之类的枚举取反即可。注意窗口标志一旦设置,窗体可能被隐藏,需要再调一次show()。
3. 控件与组件层:把散装界面元素组织成可维护的结构
控件这一层最容易犯的错是“能显示就行”,结果界面在 1080p 上正常,换到 4K 屏或者系统字体调大之后全挤在一起。解决思路只有一条:用布局管理器,不用绝对坐标。
3.1 常用控件的能力边界与组合方式
书里覆盖的控件基本就是桌面程序的全部家当,关键在于知道每个控件的边界在哪:
| 控件 | 典型用途 | 容易踩的地方 |
|---|---|---|
| QPushButton | 触发动作 | 长按会重复触发 clicked,需判断是否要 autoRepeat |
| QLabel | 静态文本、图片 | 显示大图时不缩放会撑破布局 |
| QLineEdit | 单行输入 | 密码框忘记 setEchoMode |
| QTextEdit | 富文本、HTML | 数据量大时卡顿 |
| QPlainTextEdit | 纯文本、日志 | 不支持富文本,别指望它渲染表格 |
| QComboBox | 下拉选择 | 动态增删项后索引会错位 |
| QSpinBox | 数值范围输入 | 边界值要显式 setRange |
| QListView / QTreeView / QTableView | 结构化数据展示 | 当成 QListWidget 用,直接 addItem 会失败 |
QTextEdit 和 QPlainTextEdit 这组要特别区分。前者支持富文本、能塞 HTML,适合编辑说明文档;后者只处理纯文本,但大段落追加时的性能明显更好,日志窗口、串口接收区一律用它。选错了不会报错,只会在数据涨到几万行时突然卡成幻灯片。
三个 View 类走的是 model/view 架构,和 QListWidget、QTableWidget 那套“往里塞 item”的写法是两条路子。用 View 就得配 Model:
// 用 model/view 方式填表,适合数据量会动态变化的场景 QStandardItemModel *model = new QStandardItemModel(0, 3, this); model->setHorizontalHeaderLabels({"文件名", "大小", "修改时间"}); QList<QFileInfo> files = QDir(".").entryInfoList(QDir::Files); for (const QFileInfo &fi : files) { QList<QStandardItem *> row; row << new QStandardItem(fi.fileName()) << new QStandardItem(QString::number(fi.size())) << new QStandardItem(fi.lastModified().toString("yyyy-MM-dd hh:mm")); model->appendRow(row); // 每次追加一整行,单元格顺序与表头一一对应 } ui->tableView->setModel(model); ui->tableView->horizontalHeader()->setStretchLastSection(true);QStandardItemModel(0, 3, this)三个参数分别是初始行数、列数、父对象。父对象传 this 是为了让 Qt 的对象树接管内存,窗口销毁时模型跟着释放,不用手动 delete。appendRow接收的是一个行列表,元素个数少于列数时后面的格子留空。setModel之后视图只是“看”这个模型,数据改了刷新的是模型,视图自动跟着变——这正是它比 QTableWidget 更适合动态数据的原因。
3.2 布局管理与任务栏托盘
布局的核心价值不是“排得好看”,而是“随窗口缩放自适应”。写死坐标的界面,在系统缩放 125% 或者换了语言包之后,文字一长就互相压。QHBoxLayout 管横向、QVBoxLayout 管纵向、QGridLayout 管表单式的行列,实际项目里通常是三层嵌套:外层纵向分成标题区和内容区,内容区里再横向分成侧栏和主区,主区里用网格排表单。
setContentsMargins控制布局到边缘的留白,setSpacing控制控件之间的间隙,这两个参数调好了,界面立刻从“挤成一坨”变成能交付的样子。QGroupBox 用来做分组框,QTabWidget 用来做分页,两者都是把复杂界面切块降低认知负担的手段。
QMenu 和 QToolBar 通常挂在 QMainWindow 上,菜单项和工具栏按钮共享同一个 QAction,改一处两处同步。任务栏托盘是个例外,它不属于任何窗口。
// 托盘图标要活过主窗口的关闭动作,父对象给 this 表示跟随主窗口生命周期 QSystemTrayIcon *tray = new QSystemTrayIcon(QIcon(":/new/prefix1/ico"), this); QMenu *menu = new QMenu(this); QAction *actShow = menu->addAction("显示主界面"); QAction *actQuit = menu->addAction("退出"); connect(actShow, &QAction::triggered, this, &MainWindow::showNormal); connect(actQuit, &QAction::triggered, qApp, &QApplication::quit); tray->setContextMenu(menu); tray->show();qApp是 QApplication 的全局指针宏,指向唯一那个应用对象。这里用showNormal而不是show,是因为从最小化恢复时 show 不会把窗口还原到正常尺寸。漏掉tray->show()是最常见的失误——菜单都建好了,托盘区却什么都没有。
3.3 组件化实例:登录窗口、日历、进度条与定时器
登录窗口用 QDialog,因为它天生支持返回值:
// 模态执行,用户点确定返回 Accepted,点取消或关闭返回 Rejected LoginDialog dlg(this); if (dlg.exec() == QDialog::Accepted) { showMainPanel(); // 只有验通过才进主界面 }密码框记得setEchoMode(QLineEdit::Password),默认是明文,这个低级错误在答辩现场被抓到会很难看。日历用 QCalendarWidget,选中日期通过selectionChanged信号取selectedDate()。进度条配定时器是最经典的一组:
// 构造函数里挂一个 1 秒触发一次的定时器,同时驱动标题时间和进度 QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [=]() { ui->labTime->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")); int v = ui->progressBar->value() + 1; ui->progressBar->setValue(v > 100 ? 0 : v); // 到 100 回绕,模拟循环任务 }); timer->start(1000); // 单位是毫秒,写 1 会变成每秒触发一千次start(1000)的参数单位是毫秒,写成start(1)会让界面直接卡死,这是新手期的血泪经验。lambda 里捕获了this,只要定时器的父对象是 this,窗口销毁时定时器一并销毁,不会出现野指针;但如果把父对象写成 nullptr,就必须自己在析构里 stop。QDateTime 的格式化串里hh是 24 小时制,HH写反了在某些区域设置下会得到意料之外的结果。
4. 数据与系统能力:文件、图像、数据库、网络、线程五条主线
界面搭好之后,程序得干活。这一层的难点不在单个 API,而在几条链路之间的衔接:文件读写完了要落到数据库,数据库查询要放到子线程,子线程算完还得回主线程刷界面。任何一环断掉,表现出来都是“程序没反应”。
4.1 文件与配置:文本、INI、XML 的读写约定
纯文本用 QFile 配合 QTextStream,二进制或大文件直接用 QFile 的 read/write。INI 是桌面程序最常用的配置格式,Qt 提供了 QSettings 把它封装到几乎零成本:
; config.ini 的最终形态,QSettings 按“节 / 键”两级组织 [server] host=192.168.1.10 port=8080 [ui] theme=dark// 第二个参数必须显式给 IniFormat,否则 Windows 下默认写进注册表 QSettings cfg("config.ini", QSettings::IniFormat); cfg.setValue("server/host", "192.168.1.10"); cfg.setValue("server/port", 8080); cfg.sync(); // 不 sync 有可能还在缓冲区里没落盘 // 读取时第二个参数是键不存在时的兜底值,类型靠 toXxx 转 QString host = cfg.value("server/host", "127.0.0.1").toString(); int port = cfg.value("server/port", 80).toInt();QSettings::IniFormat这个参数是最容易漏的。Windows 平台下 QSettings 默认用 NativeFormat,也就是写注册表,代码跑起来不报错,但你在项目目录里怎么都找不到 config.ini。value()返回 QVariant,必须显式转成目标类型,直接赋给 QString 编译不过。XML 则用 QDomDocument,先setContent解析,再逐层firstChildElement往下找,写的时候用createElement加appendChild,最后save到 QFile——路径层级多的时候,建议先画一棵节点树再动笔。
4.2 图形图像与多媒体处理链
绘制统一在paintEvent里做,这是 Qt 的硬规矩,在别的函数里 new 一个 QPainter 画不出东西:
// 所有绘制都发生在 begin 与析构之间,出了这个函数画的内容就没了 void MainWindow::paintEvent(QPaintEvent *e) { Q_UNUSED(e); QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); // 抗锯齿,画圆和斜线必开 p.setPen(QPen(Qt::darkCyan, 2)); // 颜色 + 线宽 p.drawLine(20, 20, 200, 20); // (x1, y1, x2, y2) p.drawEllipse(QPoint(110, 90), 60, 30); // 圆心 + 横半径 + 纵半径 p.drawText(QRect(20, 130, 200, 30), Qt::AlignCenter, "Hello Qt5"); QPixmap pix(":/new/prefix1/logo"); p.drawPixmap(20, 180, pix.scaled(160, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation)); }setRenderHint不开启时,圆弧边缘会有明显锯齿,这个参数是画质的分水岭。scaled的两个枚举分别控制“保持宽高比”和“平滑缩放”,后者在缩小图片时效果差别肉眼可见。QPixmap 适合直接上屏显示,QImage 适合做像素级运算(比如逐点改颜色),两者之间可以互相转换。
静态图之外的动效,翻转、旋转、着色、模糊、阴影、透明这一串,本质是两类做法:几何变换用 QTransform,视觉特效挂 QGraphicsEffect。比如阴影就是给控件setGraphicsEffect(new QGraphicsDropShadowEffect)再调偏移和模糊半径,不需要自己算像素。动态图用 QMovie,加载 gif 之后start()即可,注意它的 frameChanged 信号可以拿到当前帧号。音视频播放走多媒体模块,播放器、Flash 动画、图片轮播在书里各有一个完整实例,核心都是“创建对象 → 设置源 → 连接状态信号 → 播放”。
4.3 数据库、网络与多线程的衔接
数据库第一步不是写 SQL,是确认驱动在不在:
// 先查驱动,再 addDatabase;顺序反了连接会静默失败,很难查 if (!QSqlDatabase::isDriverAvailable("QSQLITE")) { qDebug() << "SQLite 驱动缺失,检查 Qt 安装目录下的 plugins/sqldrivers"; } QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("data.db"); // SQLite 就是文件路径,不需要 host/user/password if (!db.open()) { qDebug() << db.lastError().text(); // 打不开时这句是唯一的线索 } QSqlQuery q(db); q.prepare("INSERT INTO t_user(name, age) VALUES(?, ?)"); // 占位符防注入 q.addBindValue("A同学"); q.addBindValue(20); q.exec(); q.exec("SELECT id, name FROM t_user ORDER BY id DESC"); while (q.next()) { qDebug() << q.value(0).toInt() << q.value(1).toString(); }addDatabase还有第二个参数是连接名,单线程可以省略,一旦涉及多线程就必须给每个线程独立的连接名,否则会出现“连接被占用”的报错。prepare加addBindValue不只是防注入,还能让同结构的语句复用执行计划,批量插入时快很多。Access 走 QODBC 驱动,需要先在系统里配好数据源;SQLite 不需要任何外部服务,一个文件就是全部,做课程设计演示时最省事。
网络部分的主线是 QTcpServer 与 QTcpSocket 做点对点,QUdpSocket 做局域网广播,SMTP 协议发邮件。TCP 有个绕不开的坑是粘包:readyRead触发一次不代表收到一条完整消息,通常的做法是在消息头里带长度,或者在末尾加分隔符,攒够再解析。多线程有两条路,继承 QThread 重写 run,或者用 QRunnable 配 QThreadPool 做短任务:
class Worker : public QThread { Q_OBJECT protected: void run() override { for (int i = 0; i < 100; ++i) { QThread::msleep(50); // 模拟耗时计算 emit progress(i); // 结果用信号甩回主线程,别在 run 里碰界面 } } signals: void progress(int value); };run()里绝对不能 new QWidget 或者调用 setText,界面对象只属于主线程,跨线程操作会随机崩溃——这种崩溃的调用栈往往还看不出问题在哪。用信号把结果发出去,Qt 默认的 Qt::QueuedConnection 会把调用排进主线程的事件循环,安全且不用自己加锁。
4.4 系统操作、注册表与数据安全
这一块是“让程序融入系统”的部分。屏幕分辨率从 QScreen 取,本机名用QHostInfo::localHostName(),本机 IP 和域名解析都走 QHostInfo 的异步接口。键盘按键值在keyPressEvent里通过e->key()拿,和 Qt::Key_A 这类枚举比对。环境变量用qgetenv读,执行外部命令用 QProcess,注意它默认是异步的,想同步等结果得调waitForFinished()。
注册表在 Windows 下不需要专门的 API,QSettings 用 NativeFormat 时路径就是注册表键:
// NativeFormat 在 Windows 下映射到注册表,在其他平台映射到对应配置文件 QSettings reg("HKEY_CURRENT_USER\\Software\\MyApp", QSettings::NativeFormat); reg.setValue("installPath", QCoreApplication::applicationDirPath()); QString path = reg.value("installPath").toString();写入的路径层级要写全,少一层就会跑到别的键下面。这类操作涉及系统配置,测试时建议只在自己的应用命名空间下读写,别去动别人的键值。
数据安全部分,QByteArray 可以做简单的编码与异或,AES 走对称加密,MD5 用来做完整性校验。这里有个必须说清的边界:MD5 是摘要不是加密,不能用来存密码,真要做口令存储得加盐并用更慢的哈希算法。随机数在需要质量的地方用 QRandomGenerator,qrand()那套老接口的随机性只适合做演示。
// 计算文件摘要,用于校验文件有没有被改动;不要拿它存密码 QFile f("data.db"); if (f.open(QIODevice::ReadOnly)) { QCryptographicHash hash(QCryptographicHash::Md5); hash.addData(&f); // 传设备指针,流式读取,不必整文件进内存 qDebug() << hash.result().toHex(); // 转十六进制字符串便于比对 }addData有多个重载,传 QIODevice* 时会一直读到文件末尾,适合大文件;传 QByteArray 则适合已经在内存里的短数据。result()返回的是原始字节,不复用 toHex 打印出来是一堆乱码。
5. 避坑与排查:Qt5 桌面项目最容易翻车的五处
下面这五条不分先后,但每一条我都在真实项目里见过不止一次。共同特征是:编译不报错,运行才出问题,而且现象和原因隔得比较远。
5.1 中文在界面或文件里变问号
现象:源码里的中文字符串显示成方块或问号;读取一个 GBK 编码的 txt,界面上全是乱码,但用记事本打开正常。
原因:Qt5 默认把const char*按 UTF-8 解释,而文件实际是 GBK 编码,两套编码对不上。另一种情况是源码文件本身存成了 GBK,编译器按本地编码读进去,运行时再按 UTF-8 解释,一个字都留不下。
解决:源文件统一存 UTF-8。读外部文件时显式指定编码,用QTextCodec::codecForName("GBK")配合QTextStream::setCodec;或者读成 QByteArray 后用QString::fromLocal8Bit()转换。写文件同理,别指望系统默认。改完记得清理构建目录重新编译,增量编译有时会保留旧的中间文件。
5.2 资源文件里的图标运行时是空白
现象:编译通过,界面能出来,但所有:/开头的路径都加载失败,图标位置一片空白,控制台也没有明显报错。
原因:三种可能——.qrc 没被写进 .pro 的 RESOURCES;改了 .qrc 之后没有重新执行 qmake;资源前缀或别名写错,路径和代码里的字符串对不上。
解决:先看 .pro 里有没有 RESOURCES 那行,再看 .qrc 文件是否真的被加进了工程树。改完 .qrc 必须执行一次 qmake 再构建,光点运行是不够的。路径核对的办法是在资源编辑器里右键复制资源路径,粘到代码里,别手敲。
5.3 子线程里刷界面,程序随机崩
现象:功能测试时一切正常,跑一段时间后偶发崩溃,调用栈指向某个 setText 或 update,重启后又不复现。
原因:QWidget 及其子类只能在主线程操作。在 QThread 的 run 里直接调界面方法,短时间可能侥幸不崩,但事件循环一乱就会踩内存。
解决:所有跨线程的界面更新都改成信号槽。子线程 emit 一个带数据的信号,主线程的槽函数接收后再刷界面。连接方式保持默认的 AutoConnection 即可,跨线程时 Qt 会自动排队。如果必须同步等待结果,用 QMetaObject::invokeMethod 配 Qt::BlockingQueuedConnection,但要确保不会互相等待造成死锁。
5.4 打包后换台机器双击没反应
现象:开发机上好好的 exe,拷到别人电脑上双击毫无反应,或者弹一个“缺少 xxx.dll”。
原因:Qt 程序依赖 Qt5Core、Qt5Gui、Qt5Widgets 这些动态库,以及 platforms 目录下的平台插件。开发机装了 Qt 且配了 PATH,所以能找到;目标机什么都没有。
解决:手动部署的话,把上述 DLL 和platforms/qwindows.dll按目录结构拷到 exe 同级,注意 platforms 是个子目录,不能把 dll 全堆在一起。新版 Qt 安装包里带了自动分析依赖的部署工具,能省掉逐个找库的功夫。用打包工具做成安装包时,要把整个运行目录打进安装脚本,而不是只打一个 exe。做完之后一定要在一台没装过 Qt 的机器上验证一次,这是唯一的验收标准。
5.5 信号槽连一次,触发却执行了多次
现象:点一次按钮,槽函数跑了两遍甚至更多;界面弹两个一样的对话框。
原因:connect 写在了会被反复调用的函数里,比如某个初始化方法或者刷新函数,每调一次就多一条连接。
解决:connect 尽量放在构造函数里,只连一次。确实需要在运行期动态连接的,加Qt::UniqueConnection参数,重复连接会被忽略。排查时可以在槽函数里打一行日志数调用次数,比盯着代码看快得多。
6. 验收与答辩:把十四个模块讲成一条完整主线
到这个阶段,功能大概率都能跑,问题是怎么证明它“做完了”。答辩现场最常见的失分不是功能少,而是讲成了一盘散沙:这里有个数据库,那里有个线程,听众记不住任何一个。我的做法是提前准备一张验收表,把每个模块对应到一个具体动作和一个可观察的结果,讲的时候按表走。
| 模块 | 验收动作 | 通过标准 |
|---|---|---|
| 开发环境 | 在干净机器上从源码构建一次 | 无报错,qmake 与编译全通过 |
| 窗体应用 | 拖动无边框窗体、切换多窗口 | 不抖动、不甩尾、关闭正常 |
| 控件应用 | 列表、树、表格各加载 100 条数据 | 滚动流畅,无错位 |
| 文件操作 | 读写 ini、xml 各一轮 | 重启后配置仍在 |
| 图形图像 | 加载图片并缩放、加文字 | 无锯齿,比例不变形 |
| 多媒体 | 播放音视频与 gif | 可暂停、可停止、状态正确 |
| 数据库 | 增删改查各一次 | 断电重启数据不丢 |
| 网络 | 两个实例互发消息 | 中文不乱码,长消息不粘包 |
| 线程 | 后台任务运行中拖动窗口 | 界面不卡死,进度实时更新 |
| 打包部署 | 在未装 Qt 的机器上安装运行 | 双击可启动,图标正常 |
这张表还有个隐藏用途:答辩前按它逐条走一遍,等于做了一次完整的回归测试,能提前发现大部分现场会翻车的地方。
讲的时候别按目录顺序念。我的建议是选一条贯穿线,比如“从登录到数据保存”:登录窗口用 QDialog 拿返回值,主界面用 QMainWindow 挂菜单和托盘,主区用布局摆控件,配置存 ini,业务数据进 SQLite,耗时操作用子线程跑并回传进度,最后打包成安装包。这条线一走完,十四个模块自然全都带到了,听众也能形成一个完整印象。
有个细节值得单独留时间:把“为什么这么选”讲清楚。为什么列表用 QTableView 而不是 QTableWidget,为什么配置用 QSettings 而不是自己写解析,为什么耗时任务用 QThread 而不是开个定时器硬扛。这些选择背后是对场景的判断,比“我实现了什么功能”更能体现你确实想过。
最后说个习惯。早些年我交付前只在自己机器上点一遍,觉得能跑就算完,结果第一次在别人电脑上演示,程序因为缺一个平台插件直接没起来,站在那儿干等了半分钟。从那以后我每次交付前都强制走一遍“干净机器验证”:找一台没装过开发环境的电脑,从安装包开始,走完整个主流程,再随手点几下边角功能。这一步花不了多少时间,但能把最丢人的那类问题挡在门外。希望帮到你。
本文还有配套的精品资源,点击获取