简介:《Qt5范例开发大全(完整目录版)》是一份面向具备C++基础、想系统掌握Qt5桌面开发的初中级读者的PDF文档,从环境搭建到应用打包部署形成完整学习链路。压缩包内仅含1个PDF文件,体积约2.84MB,轻量便携,便于随时翻阅查证。全书按十四章展开:开发环境与Hello World入门、窗体创建与大小位置背景控制、按钮文本下拉框时间日期及列表树表布局等常用控件、组件与文件操作,并延伸至图形图像、多媒体、系统操作、注册表、数据库、网络、进程线程与数据安全,末尾讲解可执行文件打包与跨平台部署策略。每个知识点配一段可参照的范例场景,配合完整目录结构便于按模块跳查,把零散API串成开发链路。目前已有2459人学习。
1. 从一台没装开发环境的机器说起:这套 Qt5 范例体系要解决什么
去年接手一个很普通的需求:给某实验室做一台设备参数配置工具,qt5 C++ 写桌面端,界面上二十来个输入框、几个下拉框、一张设备示意图,参数存本地配置文件,最后拷一个 exe 过去让操作员双击就能开。功能本身一周能写完,真正卡人的全是边角:MinGW 的 bin 目录没进 PATH,exe 双击一闪而过;参数里带中文,写进 INI 变乱码;开发机上跑得好好的程序换台机器就报缺 DLL。这些坑在框架手册里查不到成体系的答案,得有人把环境、窗体、控件、持久化、打包这几段串成一条线完整走一遍。
下面这套结构就是按那条线排的:先立工程骨架,再铺界面层,然后接数据与系统能力,最后把坑和验收单写清楚。它适合正在交一份桌面客户端课设或毕设的人,适合要把内部小工具交付出去的从业者;不适合只想复制一段控件代码就交差的人,那种做法在这个项目规模上一定会在打包那一步翻车。
2. 环境与工程骨架:MinGW 工具链、.pro 文件与第一个可运行工程
2.1 工具链锁定:版本、位数与安装路径的三个硬约束
Qt5 的版本和编译器是绑定关系,装错一个,后面所有范例都跑不起来,而且报错信息往往指向别处,属于典型的多绕三小时。范例里的工程都是 qmake 体系,也就是 .pro 文件驱动,不是 CMake,所以构建套件要选 Qt5 自带的 MinGW 版本,位数要和系统匹配。
32 位编译器编出来的 exe 能在 64 位 Windows 上跑,反过来不行。选 32 位是最稳的做法,代价是寻址空间受限,但这个体量的配置工具、管理工具完全够用。安装路径尽量避开中文和空格,C:\Qt这种短路径最省事,因为 qmake 生成的 Makefile 里会带绝对路径,路径里一有空格,构建脚本就容易在某一步断开。
环境变量是本项目最容易被跳过、又最先出问题的一环。把编译器的 bin 目录写进系统 PATH,例如:
# 把 Qt 的运行时和编译器目录加入 PATH,注意前面带分号与已有项分隔 D:\Qt\Qt5.1.1\5.1.1\mingw48_32\bin这个目录里同时放着qmake.exe、mingw32-make.exe和Qt5Core.dll、Qt5Gui.dll等运行时库。加进去的直接好处是:编译出的 exe 能双击直接运行,不用手动把一堆 DLL 拷到同级目录。很多人以为这是打包问题,其实是 PATH 问题,先分清这两件事,能省掉半天排查。
另外要注意 Qt5 各小版本之间的 API 有差异,尤其是多媒体和网络模块。范例给的是哪一套,就先用哪一套把功能跑通,再考虑升级。升级的收益通常不大,风险却是界面能编译但一运行就崩。
2.2 最小可运行骨架:main.cpp、窗体类与 .pro 各管一段
一个 Qt5 GUI 工程最少三件东西:入口文件、窗体类、工程描述文件。很多教程直接从拖控件开始,跳过了这三者的职责边界,导致后面加模块时不知道代码该往哪放。先把它拆开看。
工程文件决定编译什么、链接什么:
# QtOne.pro —— 工程描述文件,qmake 按它生成 Makefile QT += core gui # Qt5 之后控件从 gui 模块拆到 widgets,这行保证跨版本都能编 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = QtOne # 生成的可执行文件名 TEMPLATE = app # 表示这是一个应用程序,不是库 SOURCES += main.cpp \ mainwindow.cpp HEADERS += mainwindow.h FORMS += mainwindow.ui # 不用的 Designer 可以整行删掉入口文件只做四件事:创建应用对象、创建主窗体、显示、进入事件循环。
// main.cpp —— 程序入口,负责应用对象与事件循环的生死 #include <QApplication> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); // 必须在任何 QWidget 之前创建 MainWindow w; w.show(); // show 只是把窗体标记为可见 return a.exec(); // exec 之后程序才真正开始处理消息 }argc和argv是命令行参数,Qt 自己会消费掉-style、-platform这类内置参数,剩下的才留给业务代码。a.exec()是阻塞的,主线程在这里跑事件循环,所有界面重绘、鼠标键盘事件、信号槽投递都在这个循环里分发,所以任何耗时操作都不能直接写在这条线上,这是第 5 章要展开的内容。
窗体类的构造函数负责搭界面,下面的写法用布局接管坐标,避免控件随窗口缩放时错位:
// mainwindow.cpp —— 窗体构造,界面元素的组织都在这里 #include "mainwindow.h" #include <QLabel> #include <QVBoxLayout> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(QStringLiteral("Qt5 范例工程")); // 标题用 QStringLiteral 避免编码问题 resize(480, 320); QWidget *central = new QWidget(this); // 中央部件容器 QVBoxLayout *layout = new QVBoxLayout(central); // 垂直布局,自动管理子控件位置 QLabel *label = new QLabel(QStringLiteral("Hello World"), central); label->setAlignment(Qt::AlignCenter); layout->addWidget(label); // 交给布局后不用再算坐标 setCentralWidget(central); // QMainWindow 必须指定中央部件 }这段代码里有三个容易被忽略的点。第一,new出来的对象都挂了父对象,父对象析构时会统一回收,不需要手动delete,这是 Qt 对象树的核心约定。第二,setCentralWidget是QMainWindow专属,QWidget基类没有这个接口,用错基类编译不过。第三,控件位置交给布局后,move()和resize()对子控件就不该再用了,混着写会出现控件跑到窗口外面去的现象。
2.3 构建目录与 Debug/Release:源码到可分发 exe 之间的那段路
默认的构建方式会在工程目录里生成debug和release两个文件夹,里面是可执行文件和中间产物。这个默认行为有几个副作用:源码目录被污染,版本管理里混进一堆.o文件;切换构建模式时容易拿到旧的 exe;打包时不知道该去哪个目录取文件。
通常的做法是开启影子构建,让构建目录和源码目录彻底分开。在构建套件里把构建目录指到源码目录外面,比如D:\build\QtOne-Desktop_Qt_5_1_1_MinGW_32bit-Release。这样源码目录永远是干净的,只有.pro、.cpp、.h、.ui和资源文件;构建目录可以随时整体删掉重建。
Debug 和 Release 的差别不只是有没有调试信息。Debug 版链接的是带d后缀的 Qt 库,体积大、带断言、不做优化,适合断点调试;Release 版链接正式库,编译器会做内联和裁剪,是最终交付用的。这两套产物的 DLL 不能混用,把 Debug 版 exe 拷到只有 Release 库的机器上,表现就是双击没反应或者弹一个缺库提示。
判断当前编出来的是哪一版,最直接的办法是看构建目录名和 exe 体积,Release 版通常小一截。切换构建模式后强制走一次「清理 → qmake → 重新构建」,不要图省事直接点运行,qmake 生成的 Makefile 在切换模式时不一定完整更新,血泪经验是这类问题排查起来完全没线索,只能靠强制重建规避。
3. 窗体与控件层:三个基类的选型、无边框拖动与二十个控件的落位
3.1 三个窗体基类的选型:QMainWindow、QWidget、QDialog 各管什么
新建工程时那个三选一的对话框,是这个项目里第一个真正需要判断的地方。选错不会立刻报错,但会在加菜单、加工具栏、弹模态框的时候开始别扭。
| 基类 | 自带结构 | 典型用途 | 什么时候别用 |
|---|---|---|---|
| QMainWindow | 菜单栏、工具栏、状态栏、中央部件 | 主界面、带菜单的管理工具 | 小型弹窗,用它会多出一堆空区域 |
| QWidget | 纯空白矩形,什么都没有 | 自定义控件、嵌入式的子页面 | 需要菜单栏时得自己拼 |
| QDialog | 返回值、默认按钮、模态控制 | 登录框、参数设置、确认框 | 做主窗口会缺少中央部件概念 |
这套判断可以简化成一句话:一个程序只有一个主窗口,主窗口用QMainWindow;所有需要「弹出后等用户处理完再继续」的,用QDialog;其余用来组合界面的,全部用QWidget。参数配置工具这类项目,主界面是QMainWindow,里面的每一个参数页签内容都是QWidget,点「高级设置」弹出来的那一层是QDialog。
模态和非模态的区别也在这里定下来。exec()打开的是模态对话框,代码会停在那一行等用户关掉窗口,适合登录、确认这类必须先有结果的场景;show()是非模态,窗口开着也能操作主界面,适合工具箱、日志面板。把该用模态的地方写成show(),用户就能在你读取参数之前把配置改掉,这类 bug 排查起来非常隐蔽。
3.2 无边框窗体与自定义标题栏:鼠标三个事件把拖动做出来
去掉系统标题栏之后,窗口就不能拖动了,因为拖动本来就是标题栏的行为。要自己实现,就得接管鼠标按下、移动、释放三个事件,记录按下时的全局坐标,移动时算出偏移量,再把窗口挪过去。
头文件里先声明三个事件和位置变量:
// mainwindow.h —— 声明重写的事件与状态变量 #include <QPoint> protected: void mousePressEvent(QMouseEvent *e) override; // 按下:记起点 void mouseMoveEvent(QMouseEvent *e) override; // 移动:算偏移并挪窗 void mouseReleaseEvent(QMouseEvent *e) override; // 释放:收尾 private: QPoint m_lastPos; // 上一次鼠标的全局坐标,用于计算增量实现部分注意用全局坐标而不是窗口内坐标,否则窗口一移动,坐标系跟着变,会出现窗口抖动或者越拖越快:
// mainwindow.cpp —— 无边框窗体的拖动实现 this->setWindowFlags(Qt::FramelessWindowHint); // 去掉系统标题栏与边框 void MainWindow::mousePressEvent(QMouseEvent *e) { m_lastPos = e->globalPos(); // 记录按下时的屏幕坐标 } void MainWindow::mouseMoveEvent(QMouseEvent *e) { const QPoint delta = e->globalPos() - m_lastPos; // 本次移动的增量 move(pos() + delta); // 当前窗口左上角加上增量 m_lastPos = e->globalPos(); // 更新基准点,必须写 } void MainWindow::mouseReleaseEvent(QMouseEvent *e) { Q_UNUSED(e); // 释放时不要再 move 一次,增量已经在 mouseMoveEvent 里消费掉了 }最后那个空实现是很多人会踩的坑。如果在mouseReleaseEvent里再按同样的方式move一次,等于把最后一段位移重复应用,窗口会在松手瞬间跳一下,位移越大跳得越明显。释放事件里只该做清理和状态复位。
去掉标题栏的代价是关闭、最小化按钮也没了,得自己在窗口右上角放按钮,用connect接到close()和showMinimized()。如果还需要阴影或圆角,通常的做法是给主窗口套一个带边距的容器,容器设WA_TranslucentBackground,内部用样式表画圆角矩形。这套组合在 Windows 上表现稳定,不需要额外的系统接口。
3.3 控件与布局:从二十个控件到一套可维护的界面
范例里列了二十多个控件,一个个试过去效率很低。按「输入 — 展示 — 选择 — 容器」四类分组记,用的时候直接查表:
| 控件 | 归类 | 关键信号 / 属性 | 使用注意 |
|---|---|---|---|
| QPushButton | 输入 | clicked() | 长按操作别绑 clicked,用 pressed/released |
| QLineEdit | 输入 | textChanged()、editingFinished() | 密码框要设 EchoMode |
| QTextEdit / QPlainTextEdit | 输入 | toPlainText() | QPlainTextEdit 适合大段日志,性能更好 |
| QComboBox | 输入 | currentIndexChanged() | 存业务 ID 用 addItem(text, userData) |
| QSpinBox | 输入 | valueChanged() | 一定设 setRange,默认范围是 0 到 99 |
| QDateEdit / QTimeEdit | 输入 | dateChanged()、timeChanged() | 注意与 QDateTime 的时区换算 |
| QRadioButton / QCheckBox | 输入 | toggled() | 单选按钮要放在同一个 QButtonGroup 里才互斥 |
| QLabel | 展示 | setPixmap()、setText() | 显示长文本记得开 wordWrap |
| QListView / QTreeView / QTableView | 展示 | 配合 Model 使用 | 数据超过几百条必须上 Model,别逐条 addItem |
| QGroupBox | 容器 | setTitle() | 分组框本身不提供布局,要手动塞一个布局进去 |
| QTabWidget | 容器 | currentChanged() | 页签多时考虑延迟创建 |
| QScrollBar | 容器 | valueChanged() | 大多数场景由 QScrollArea 代劳 |
布局是这张表之外最值得花时间的地方。绝对坐标在固定尺寸窗口上没问题,一旦窗口可缩放就全乱。横向排列用QHBoxLayout,纵向用QVBoxLayout,需要行列对齐的表单用QGridLayout,再配合addStretch()把控件往一边顶。嵌套关系通常是:QGroupBox里放一个QGridLayout,几个QGroupBox再塞进主窗口的QVBoxLayout。这样改一处间距不会牵动全局。
还有一个细节:QSpinBox的默认范围是 0 到 99,用来输端口号会直接卡死。参数类控件全部显式设范围,是这类工具最基本的要求。
3.4 绘制与多媒体:QPainter、QPixmap 与播放器的落位
界面上那张设备示意图,静态图用QLabel加QPixmap就够,关键是保持比例缩放,否则窗口一拉伸图就变形:
// 加载图片并按标签大小等比缩放,SmoothTransformation 保证不出现锯齿 QPixmap pix(":/res/device.png"); QPixmap scaled = pix.scaled(label->size(), Qt::KeepAspectRatio, // 保持宽高比 Qt::SmoothTransformation); // 平滑缩放 label->setPixmap(scaled);:/res/device.png是资源文件路径,前面那段冒号表示从编译进 exe 的资源里读取,不依赖外部文件,这样打包时不用担心图片丢失。
需要自己画线、画圆、标文字的场景,重写paintEvent:
// 自定义绘制,所有画图操作都在 QPainter 上完成 void MainWindow::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 抗锯齿,线条才不毛糙 painter.setPen(QPen(Qt::darkGray, 2)); // 画笔颜色和线宽 painter.drawLine(20, 20, 260, 20); // 画一条横线 painter.drawEllipse(QPoint(120, 90), 60, 40); // 圆心 + 横向半轴 + 纵向半轴 painter.drawText(QRect(20, 140, 240, 30), Qt::AlignCenter, QStringLiteral("设备状态示意图")); // 文字居中在指定矩形内 }paintEvent会在窗口需要重绘时被反复调用,里面不能做耗时计算,也不能创建大量临时对象。需要动态数据就提前算好存成成员变量,绘制时只负责画。
动图用QMovie,视频用QMediaPlayer配QVideoWidget,这两者在工程文件里要显式加模块依赖:
# 多媒体模块,缺了这一行编译时报找不到头文件 QT += multimedia multimediawidgets播放器需要设置媒体源、音频输出和视频输出三样东西才能出声出画,缺一样就是静音或者黑屏。这类「编译通过但功能不生效」的问题,八成是输出端没接上。
4. 数据与系统集成:文件、SQLite、多线程与打包交付
4.1 文件与配置的三层选择:QFile、QTextStream 与 QSettings
参数存哪里,取决于它长什么样。纯文本日志用QFile加QTextStream;需要结构化又能被别的程序读,用 INI 或 XML;只是本程序自己用的键值对,QSettings最省事。
QSettings的坑集中在编码和路径两点。它默认把配置写到系统用户目录,换个账户登录就读不到了,所以通常显式指定到 exe 同级目录:
// 配置文件放在 exe 同级,随程序一起拷走 QString iniPath = QCoreApplication::applicationDirPath() + "/config.ini"; QSettings settings(iniPath, QSettings::IniFormat); settings.setIniCodec("UTF-8"); // 不设这行,中文写进去就是乱码 settings.beginGroup("Device"); // 分组,避免键名冲突 settings.setValue("port", 502); // 数值直接存,读出来还是数值 settings.setValue("name", QStringLiteral("一号机")); settings.endGroup(); settings.sync(); // 显式落盘,防止异常退出丢配置setIniCodec在 Qt6 里被移除,因为 Qt6 默认就用 UTF-8。如果工程以后要升版本,这行得删,这是版本迁移时最常见的编译错误之一。sync()也值得养成习惯,虽然析构时通常会写盘,但程序崩溃时不一定走到析构。
XML 用QDomDocument,适合需要层级结构且要被外部工具消费的场景,比如导出一份设备清单给别的系统导入。代价是解析代码比 INI 长不少,字段不多的时候没必要上。
4.2 SQLite 与 QSqlQuery:从建表到列表展示的完整链路
配置项一旦超过几十条,还带查询和排序需求,就该换数据库。SQLite 不需要装服务,整个库就是一个文件,跟着程序走,是这类桌面工具最合适的选择。
打开数据库之前要先确认驱动存在,驱动缺失的表现是open()返回 false 但看不出原因:
// 检查可用的数据库驱动,排查环境问题第一步 qDebug() << QSqlDatabase::drivers();建库、建表和插入走标准流程:
// 打开 SQLite 库,路径同样放在 exe 同级 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName(QCoreApplication::applicationDirPath() + "/data.db"); if (!db.open()) { qDebug() << "打开失败:" << db.lastError().text(); // 一定要打错误信息 return; } QSqlQuery query(db); query.exec("CREATE TABLE IF NOT EXISTS device (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "value TEXT)"); // 带参数的插入,绝对不要用字符串拼接 query.prepare("INSERT INTO device (name, value) VALUES (?, ?)"); query.addBindValue(QStringLiteral("温度")); query.addBindValue(QString::number(36.5)); if (!query.exec()) { qDebug() << "插入失败:" << query.lastError().text(); }用prepare加addBindValue而不是拼字符串,一是防注入,二是避免值里带单引号时 SQL 语法直接断掉,三是同一个语句多次执行时性能更好。
列表展示不要用QTableWidget一行行往里塞,数据上千条时界面会明显卡顿。用QSqlTableModel挂到QTableView上,查询和排序都交给数据库:
// 模型直接对接表,界面自动同步 QSqlTableModel *model = new QSqlTableModel(this, db); model->setTable("device"); model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交,便于撤销 model->select(); ui->tableView->setModel(model);OnManualSubmit意味着setData之后不会立刻写库,要调submitAll()。这个策略适合表单编辑,用户点「保存」才落盘;如果希望改一格存一格,用OnFieldChange。选错策略的后果是用户改完没保存就关了窗口,数据凭空消失。
4.3 多线程与加密:把耗时活儿挪出主线程
界面卡死的根因只有一个:主线程在跑耗时任务,事件循环停转。文件扫描、批量导入、网络请求都属于这一类。QThread配合moveToThread是 Qt5 推荐的做法,比继承QThread重写run更灵活:
// 工作对象,定义在子线程里执行的任务 class ScanWorker : public QObject { Q_OBJECT public slots: void run(const QString &dirPath) { // 在子线程中执行 QDir dir(dirPath); const int count = dir.entryList(QDir::Files).size(); emit finished(count); // 结果通过信号回主线程 } signals: void finished(int count); }; // 在主线程里组装:创建线程、迁移对象、连接信号 QThread *thread = new QThread(this); ScanWorker *worker = new ScanWorker; worker->moveToThread(thread); connect(thread, &QThread::started, worker, [=]{ worker->run(path); }); connect(worker, &ScanWorker::finished, this, &MainWindow::onScanFinished); connect(worker, &ScanWorker::finished, thread, &QThread::quit); thread->start();这里三个connect各有用处:线程启动时才触发任务、任务完成后回主线程更新界面、任务完成后让线程退出。少连第二个,界面就拿不到结果;少连第三个,线程一直挂着,程序退出时会报警告。
线程里绝对不能碰界面控件。QLabel::setText在子线程里调用,轻则界面不刷新,重则直接崩。所有界面更新都通过信号回到主线程做,这是硬规矩。
加密方面,口令不要用 MD5。MD5 已经能被快速碰撞,用来存口令等于没存。用 SHA-256 加一段随机盐,长度够、实现也简单:
// 加盐哈希,盐值随用户记录一起存 const QByteArray salt = "a5f9c2"; // 实际项目里每个用户单独生成 const QByteArray digest = QCryptographicHash::hash( pwd.toUtf8() + salt, QCryptographicHash::Sha256); const QString stored = QString(digest.toHex()); // 转十六进制字符串入库随机盐不能用rand(),种子固定的话每次启动生成的序列一样,等于没加盐,用QRandomGenerator。
4.4 打包部署:从构建目录到一台裸机能双击的 exe
程序编译出来能跑,不等于能交付。裸机上缺的就是那批 Qt 的 DLL 和平台插件,其中平台插件qwindows.dll最容易漏,缺了它的典型症状是弹一个「could not find or load the Qt platform plugin windows」然后退出。
Qt 自带一个部署工具,会自动把依赖抄到 exe 旁边:
# 在 Release 产物目录执行,工具会扫描依赖并复制 DLL 与插件 windeployqt.exe --release --no-translations D:\build\release\Tool.exe--release指定部署正式版依赖,--no-translations跳过翻译文件减小体积。执行完目录里会多出platforms、imageformats等子目录,这些目录必须和 exe 保持相对位置不变,整个文件夹一起打包,不要只拷 exe。
需要做成安装包的话,常见做法是用一个安装脚本工具把整个目录压成 setup.exe,配置里指定安装路径、快捷方式和卸载项。这一步做不做取决于交付对象,内部工具直接给文件夹就够了,对外交付才需要安装包。
5. 避坑与常见问题排查:五条最容易翻车的现场经验
5.1 双击 exe 没反应,或者提示缺 DLL
现象:在开发机双击能开,拷到别的机器双击一闪而过,或者弹「缺少 Qt5Core.dll」「无法定位程序输入点」。
原因:编译器 bin 目录在开发机的 PATH 里,所以能找到运行时库;裸机没有这个目录,也不认识 Debug 版的带 d 后缀库。
解决:先确认编的是 Release 版,再在产物目录跑一次部署工具,把整个文件夹拷走而不是只拷 exe。如果仍然报错,用依赖查看工具看具体缺哪个 DLL,多数情况是平台插件没跟着走。养成「交付前在一台没装过 Qt 的机器上试一次」的习惯,比任何检查清单都管用。
5.2 中文写进配置文件变乱码
现象:界面上输入「一号机」,写进 INI 之后变成问号,或者读回来是乱码。
原因:QSettings在没有指定编码时按本地编码读写,源码里的中文字面量又和运行环境的编码不一致,两边一错位就成了乱码。
解决:统一 UTF-8。源码文件本身存成 UTF-8,QSettings显式调setIniCodec("UTF-8"),界面上取文本一律用QString而不是std::string。字符串字面量用QStringLiteral包裹,避免编译期转换。已经乱码的旧配置文件没法自动修复,只能让用户重建。
5.3 点一下按钮界面就假死
现象:点了「导入」之后窗口变白,鼠标转圈,几秒到几十秒才恢复,数据量大时干脆无响应。
原因:导入逻辑直接写在槽函数里,槽函数在主线程执行,事件循环被占住,重绘和输入全部排队。
解决:把耗时逻辑挪进工作对象,用moveToThread放到子线程,结果通过信号回主线程更新界面。中间要刷进度的话,再发一个进度信号。判断标准很简单:单次操作超过 100 毫秒就该考虑挪线程。
5.4 信号槽连了但槽函数不执行
现象:connect写了,编译通过,运行时点了没反应,也不报错。
原因:常见三种。一是自定义信号忘了在类声明里加Q_OBJECT宏,moc 没生成元信息,连接静默失败;二是用字符串版SIGNAL/SLOT写错了参数类型,运行时匹配不上;三是同一个连接写了两次,槽函数执行两遍,看起来像「反应不对」。
解决:新代码一律用函数指针版connect,参数类型在编译期就能校验。类里只要声明了信号槽,Q_OBJECT必须写在类声明第一行。改动头文件之后强制重跑 qmake,否则 moc 不会重新生成。
5.5 打包后图标和图片全丢了
现象:开发时图片正常显示,打包到别的机器上一片空白,图标也没了。
原因:图片是按相对路径从磁盘读的,比如QPixmap("res/device.png"),打包时没把资源目录带上,或者工作目录变了。
解决:所有图片、图标、样式表统一走资源文件,路径写成:/res/device.png,编译进 exe 内部,天然跟着程序走。已经用磁盘路径的,改成资源路径后记得重新构建,资源文件的改动不重新编译是不生效的。
6. 验收与答辩:一份能当场跑通的检查单和一个演示顺序
这套东西做完,怎么证明它是合格的,比功能清单本身更重要。我一般按下面这张表走一遍,每项都要求当场可复现:
| 检查项 | 验收动作 | 通过标准 |
|---|---|---|
| 环境可重建 | 换一台没装过 Qt 的机器 | 双击 exe 直接出主界面,无缺库提示 |
| 界面自适应 | 拖动窗口边缘缩放 | 控件不错位、图片不变形 |
| 数据落地 | 改参数后关闭再打开 | 配置原样恢复,中文不乱码 |
| 异常处理 | 手动删掉配置文件 / 数据库文件 | 程序自动重建,不崩溃 |
| 响应性 | 触发批量导入或扫描 | 界面不假死,有进度反馈 |
| 卸载干净 | 删除整个程序目录 | 系统里不留残留项 |
答辩时的演示顺序建议反过来走:先打开做好的成品,展示完整业务流程,让人看到它是个能用的东西;再回到代码讲三个关键点——窗体和控件的组织方式、数据是怎么存的、耗时操作是怎么挪出主线程的;最后主动讲你踩过的坑,尤其是打包和编码这两类。主动交代问题比被问出来强,评委通常更在意你有没有把问题想清楚,而不是功能有多少个。
被问到「为什么用 Qt 不用别的方案」时,别背框架特性,讲你的场景:需要跨平台、需要一套 C++ 代码同时管界面和底层、需要离线单文件分发。反过来问「为什么不用 Web 技术做」,答案就是交付形态——目标机器是内网机器、没有浏览器环境、要求双击即用。
从那以后我每次交这类桌面工具,都强制在干净环境里走一遍「拷文件夹 → 双击 → 改参数 → 关掉 → 重开」这条链,哪怕代码一行没改。翻车最狠的一次就是改了个图片路径,开发机上好好的,交付当天投影演示时图片区一片白。希望帮到你。
本文还有配套的精品资源,点击获取