ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Qt5范例开发大全:C++上位机界面与工程实践

Qt5范例开发大全:C++上位机界面与工程实践

简介:《C++ Qt5 范例开发大全》面向Qt5初学者与需要快速查阅控件用法的C++开发者,以实践范例串联开发环境、窗体应用、控件应用、图形图像与加解密等主题,帮助读者从搭建Qt Creator环境起步,逐步完成可运行的桌面程序。包内为1个PDF文件,约2.83MB,按章节组织内容,依次讲解Hello World入门、窗体大小与背景色控制、无边框窗体移动、多窗体调用,以及QPushButton、QLabel、QLineEdit、QComboBox、QTableView、QTreeView等常用控件的用法,并补充横向布局、网格布局和文件操作等实例。目录结构清晰,示例驱动,适合按知识点检索并对照动手练习,也便于有经验的开发者查漏补缺。现有256人学习,可作为Qt5入门与常用组件实践的参考手册。

1. 接手老上位机时,我先翻的是这本 Qt5 范例集

接手一个老上位机维护项目时,界面是十年前的 MFC,客户只提了一个要求:加托盘图标,配置项抽出来做成文件。翻遍手头资料,最后救场的是这本《C++ Qt5 范例开发大全》——它按“一个小节一个可编译工程”来组织,窗体大小、标题栏图标、INI 读写、SQLite 建表、TCP 点对点聊天、线程池、打包部署,每一章都能直接对应一个具体活。这是 14 章、上百个独立 Qt5 示例堆起来的工程素材库,不讲抽象原理,讲“这段代码放进去能不能跑”。适合两类人:刚学完 C++ 语法、想尽快做出能点能看的界面的新手;以及像我这样在老项目里补功能、不想从零搭脚手架的老手。前提是机器上装了 Qt5 和 Qt Creator,书里示例环境基于 Qt5.1.1 + MinGW,新版本同样能用,个别接口有替换。

2. 工程骨架与目录映射:14 章范例是怎么组织起来的

这套资源的组织方式很朴素,一个目录一个示例工程,没有公共库、没有统一框架,好处是任何一节都能单独抽出来编译,坏处是你得自己先看懂它的工程约定,不然打开一片.pro文件不知道从哪个开始。

2.1 三层基类和 .pro:先搞清一个 Qt 工程的五脏

书里第二章开头讲清了 Qt Creator 新建工程时让你选的那三个基类,这段内容看起来像补课,实际决定了后面几十个示例的写法。QMainWindow 自带菜单栏、工具栏、状态栏和中央部件区,适合主窗口型应用;QWidget 是所有界面对象的基类,最小、最干净,做无边框窗体、异形窗体、浮层基本都用它;QDialog 是对话框基类,支持模态执行和返回值,登录窗口、文件选择、确认提示这类短期交互都用它。

选错基类最典型的翻车是:想做无边框拖拽窗体,选了 QMainWindow,结果setWindowFlags(Qt::FramelessWindowHint)之后菜单栏和工具栏还在原位顶着,拖起来一片空白,因为主窗口的布局逻辑和普通 Widget 不一样。我一般做这类练习工程统一选 QWidget,等结构定型了再换 QMainWindow 套菜单栏。

工程文件是理解整份资源的钥匙,.pro里那几行决定了这个示例能不能编过:

QT += core gui sql network multimedia # 该示例用到哪些 Qt 模块,缺一个就链接报错 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets # Qt5 起 QWidget 从 gui 拆到 widgets 模块 TARGET = QtDemo # 生成的 exe 名字 TEMPLATE = app # app=可执行程序,lib=库 SOURCES += main.cpp \ mainwindow.cpp HEADERS += mainwindow.h FORMS += mainwindow.ui RESOURCES += image.qrc # 图标、图片等资源打包进 exe,路径用 :/ 前缀访问

参数含义逐个说清楚:QT +=是模块开关,只用界面就core gui widgets,碰数据库加sql,做点对点聊天加network,播音频视频加multimedia;少写一个模块,编译期可能只是头文件找不到,链接期才会报一串undefined reference,新手很容易在这里卡半天。RESOURCES那行对应的是资源文件,书里改标题栏图标那一节专门用了一整段讲怎么新建.qrc、加前缀、加文件、起别名,因为这步在 Designer 里没法可视化完成,必须手点。

主程序的骨架每本书都一样,但这几行值得单独看一眼:

#include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 每个 Qt GUI 程序有且只有一个,管理事件循环和全局资源 MainWindow w; // 栈上创建,析构由作用域保证 w.show(); // 显示窗口,此时才真正建原生窗口句柄 return a.exec(); // 进入事件循环,返回值就是程序退出码 }

a.exec()是黑匣子式的存在:它把控制权交给 Qt 的事件分发器,鼠标、键盘、定时器、网络回调全在里面排队派发,你写在槽函数里的代码都是被它调起来的。很多人第一次写 Qt 会在w.show()后面写业务代码,然后发现界面卡住不刷新,原因就是没进exec(),或者在里面写了死循环把事件循环堵死了。

2.2 目录层级与章节对应关系

书里的示例按章归档,路径形如QtCode/QtTwo/Qt01/Qt01.pro,第一章只有一个 Hello World 工程,第二章开始按小节拆目录,越往后目录越碎。大致映射关系如下:

目录层级对应章节主要内容需要的 Qt 模块
QtOne第 1 章Hello World、工程创建、目录说明core gui widgets
QtTwo/Qt01~Qt04第 2 章窗体尺寸、位置、图标、无边框拖拽core gui widgets
QtThree第 3 章22 个常用控件与布局core gui widgets
QtFour第 4 章日历、登录框、文件/颜色对话框、进度条、Timercore gui widgets
QtFive第 5 章文件、INI、XML 读写core
QtSix第 6 章QPainter 绘图、图像变换、图形特效core gui widgets
QtSeven第 7 章音视频播放、动画core gui widgets multimedia
QtEight第 8 章分辨率、IP、环境变量、系统命令core gui network
QtNine第 9 章注册表读写(Windows 平台)core
QtTen第 10 章数据库驱动、Access/SQLite 操作core sql
QtEleven第 11 章TCP 聊天、UDP 广播、邮件发送core network
QtTwelve第 12 章进程管理、QThread、QRunnablecore
QtThirteen第 13 章Base64、AES、MD5、随机数core
QtFourteen第 14 章打包工具与安装包制作—

这张表的用法是反查:手上要做一个“扫码后写数据库并弹提示”的功能,就去 QtTen 找数据库连接那节,去 QtFour 找对话框那节,两个示例拼一拼就是初版。我一般把整份示例先全量编译一遍,编译不过的工程直接标记出来,后面要用时心里有数。

每个工程目录里还会出现debug/和release/两个文件夹,那是 Qt Creator 按构建配置自动生成的产物目录,里面是可执行文件和中间.o。新手常见困惑是“我改了代码怎么运行结果没变”,十有八九是构建配置切到了 release,而你在跑 debug 目录里的旧 exe,或者反过来。这两个目录可以随时删,删完重新构建会自动生成。

2.3 从零跑通第一个示例的完整动作

先把 Qt Creator 装好,安装时留意套件(Kits)选择,MinGW 和 MSVC 两套编译器不能混用,书里示例是 MinGW 版本,跟着选 MinGW 最省事,混选会在链接阶段报一堆奇怪的符号错误。装完把 Qt 的bin目录加进系统PATH,这一步书里专门写了一节,因为不加的话后面命令行调windeployqt会找不到命令。

打开示例的顺序建议是:先打开第一章那个 Hello World 的.pro,按 Ctrl+R 跑一次,确认弹窗出来、文字正常显示;再随便挑 QtTwo 里一个窗体示例跑一次。两次都过,说明工具链没问题,后面看代码才有意义。跑不通先看底部“编译输出”里的第一行错误,而不是最后一行——最后一行往往只是“构建失败”的汇总提示。

Qt Creator 的构建目录默认建在源码同级,书里示例很多是相对路径引用资源,如果你把构建目录改到别处,:/开头的资源路径不受影响,但用相对路径读文件的示例就会找不到文件。稳妥做法是保持默认,“影子构建”这个选项在示例工程里没必要开。

3. 窗体与控件主线:一个能交付的界面是怎么拼出来的

第 2、3、4 章加起来占了全书三分之一篇幅,逻辑是一条线:先让窗口长得对,再往窗口里塞控件,最后让控件之间产生联动。这条线走通,日常七成的界面活都能干。

3.1 窗体属性的最小可用集合

控制窗体大小、位置、背景,书里给了四个 API,每个都对应一个真实需求:

// 构造函数里即可设置 this->setWindowTitle("Qt5.1 窗体应用"); // 标题栏文字 this->setMaximumSize(300, 300); // 拖拽能放到的最大尺寸 this->setMinimumSize(300, 300); // 最小尺寸,和上面相同就锁死了尺寸 this->move(100, 100); // 相对屏幕左上角定位,含标题栏 this->setStyleSheet("background:red"); // 用 QSS 改背景,比重写 paintEvent 省事

把setMaximumSize和setMinimumSize设成一样,等价于setFixedSize(300, 300),后者更直白但少了单独调上下限的灵活性。move和setGeometry的区别要记牢:move(x, y)只改位置,setGeometry(x, y, w, h)位置和尺寸一起改,做自适应布局时后者更常用。顺带一句血泪经验,setStyleSheet("background:red")作用在 QWidget 上时,子控件也会被染色,如果只想染主窗体,得写成QWidget#对象名 { background: red; }这种带选择器的形式。

标题栏图标那节的重点不在代码,在资源系统。代码只有一行:

this->setWindowIcon(QIcon(":/new/prefix1/ico")); // 路径 = 冒号 + 前缀 + 别名

:/后面那一串来自.qrc文件里配置的前缀和别名,别名可以不写扩展名。书里强调用 16×16 的 ico,因为 Windows 标题栏和任务栏都按这个小尺寸取图,塞一张 256×256 的 PNG 进去,图标会糊成一块。资源文件加完之后一定要重新执行一次 qmake,否则编译进去的还是旧的资源表,改了图标没变化就是这么来的。

无边框拖拽是第 2 章唯一需要重写事件的例子,也是很多人第一次接触 Qt 事件机制的地方:

// mainwindow.h protected: void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; private: QPoint m_offset; // 按下瞬间,鼠标相对窗口左上角的偏移量 bool m_dragging; // 是否正在拖拽,避免鼠标移出窗口后误跟
// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QWidget(parent), m_dragging(false) { setWindowFlags(Qt::FramelessWindowHint); // 去掉标题栏和边框 resize(400, 300); } void MainWindow::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_dragging = true; m_offset = e->globalPos() - frameGeometry().topLeft(); // 屏幕坐标减去窗口左上角 e->accept(); // 标记事件已处理,不再向上传 } } void MainWindow::mouseMoveEvent(QMouseEvent *e) { if (m_dragging && (e->buttons() & Qt::LeftButton)) { move(e->globalPos() - m_offset); // 保持偏移量不变,窗口跟着鼠标走 e->accept(); } } void MainWindow::mouseReleaseEvent(QMouseEvent *e) { m_dragging = false; e->accept(); }

参数说明:e->globalPos()是鼠标在屏幕上的绝对坐标,frameGeometry().topLeft()是窗口含边框时的左上角坐标,两者相减得到的偏移在移动过程中保持不变,所以窗口跟手不会跳。如果这里用pos()代替frameGeometry().topLeft(),去掉边框时两者一致,一旦保留边框就会差出标题栏高度,拖起来窗口会突然往下蹦一下。Qt5.15 之后globalPos()被标记弃用,新代码用e->globalPosition().toPoint(),功能等价。

3.2 控件清单与布局管理器的实际搭配

第 3 章把 22 个控件挨个过了一遍,从 QPushButton、QLabel、QLineEdit、QTextEdit、QPlainTextEdit,到下拉框 QComboBox、QFontComboBox,数值型的 QSpinBox、QTimeEdit、QDateEdit、QScrollBar,再到 QRadioButton、QCheckBox、QListView、QTreeView、QTableView,最后到布局和容器 QHBoxLayout、QGridLayout、QGroupBox、QTabWidget、QMenu、QToolBar、任务栏托盘。数量多但套路一致:构造、设属性、连信号、加进布局。

几个容易用错的点值得单独拎出来。QTextEdit 和 QPlainTextEdit 的差别不只在多行:前者是富文本编辑器,支持 HTML 和格式化,适合写说明文档;后者是纯文本,针对大段日志做过优化,几万行往里怼也不卡,做日志窗口必须选它。QLineEdit 做输入框时记得设setValidator或setInputMask,否则用户能往端口输入框里敲字母,最后在toInt()那里静默得到 0。

布局这块,书里讲了横向布局和网格布局,实际项目里最常用的是网格加嵌套:

QGridLayout *g = new QGridLayout; QLabel *lbUser = new QLabel("账号"); QLineEdit *leUser = new QLineEdit; QLabel *lbPwd = new QLabel("密码"); QLineEdit *lePwd = new QLineEdit; lePwd->setEchoMode(QLineEdit::Password); // 密码框掩码 QPushButton *btnOk = new QPushButton("登录"); g->addWidget(lbUser, 0, 0); // 第 0 行第 0 列 g->addWidget(leUser, 0, 1); g->addWidget(lbPwd, 1, 0); g->addWidget(lePwd, 1, 1); g->addWidget(btnOk, 2, 1, Qt::AlignRight); // 第三行第二列,右对齐 QWidget *central = new QWidget(this); central->setLayout(g); // 布局必须挂到一个容器上才生效 setCentralWidget(central); // QMainWindow 专用;QWidget 直接用 setLayout

最后两行是新手最常见的漏项:布局对象 new 出来了,控件也 add 进去了,但窗口上什么都不显示,因为布局没挂到任何容器。另外布局里的控件不要再手动move(),一 move 就和布局打架,界面会闪。

QTabWidget 和 QGroupBox 属于“容器型控件”,作用是分组和切换,做设置页基本绕不开;QMenu + QToolBar 做菜单栏,QSystemTrayIcon 做托盘,后者的关键点是托盘对象的生命周期要跟程序一致,别在函数里 new 完就没人管了,否则图标刚出来就消失。

3.3 信号与槽:把散装控件连成一条逻辑

书里示例大量使用connect,早期写法是SIGNAL/SLOT宏,新写法是函数指针。两种写法并存,看清楚用的是哪套很重要:

// 新语法,编译期就能检查签名是否匹配 connect(btnOk, &QPushButton::clicked, this, &MainWindow::onLogin); // 旧语法,运行时字符串匹配,拼错只会在控制台打一行警告,程序照跑但不响应 connect(btnOk, SIGNAL(clicked()), this, SLOT(onLogin()));

我一般统一用新语法,理由很实在:参数类型不匹配直接编译报错,不用等运行时看那行黄色警告。旧语法唯一的优势是能连到重载槽,现在用QOverload<int>::of(&QSpinBox::valueChanged)也能解决。

自定义信号是这一章的进阶内容,写法固定:

// mainwindow.h signals: void loginSucceeded(const QString &user); // 只声明不实现,moc 自动生成 // mainwindow.cpp void MainWindow::onLogin() { // ...校验逻辑... emit loginSucceeded(leUser->text()); // emit 只是个空宏,标记语义 }

有个前提:类里必须写Q_OBJECT宏,而且这一行要放在class开头附近,写完保存,让 qmake 重新跑一次 moc。缺这一步的报错非常不直观,undefined reference to vtable for XXX基本就是它。

信号槽的连接还可能被对象生命周期坑到。connect的第三个参数是接收者,接收者销毁时连接会自动断开,这是 Qt 的兜底机制,但发送者如果先销毁,接收者还活着,回调里再访问发送者的成员就是野指针。书里的示例大多是短生命周期的小窗口,不太会碰到;搬到自己项目里,尤其是把窗口 new 出来又提前delete的时候,务必确认连接的两端谁活得更久。

4. 数据与 IO 主线:文件、配置、数据库三段打通

界面搭好之后,数据从哪来、存到哪去,是第二个必答题。第 5 章和第 10 章的内容串起来正好是一条完整的落地链路:读配置、写日志、存业务数据。

4.1 QFile、QDir、QTextStream 三件套

文件读写是基础中的基础,书里从创建文件夹、写入文件、修改内容、删除文件、改文件名一路讲下来,用的都是 QFile 加 QTextStream 的组合:

#include <QFile> #include <QDir> #include <QTextStream> #include <QCoreApplication> QString dirPath = QCoreApplication::applicationDirPath() + "/data"; QDir dir; if (!dir.exists(dirPath)) { dir.mkpath(dirPath); // 递归创建,父目录不存在也能建 } QFile f(dirPath + "/log.txt"); if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { qWarning() << "打开失败:" << f.errorString(); return; } QTextStream out(&f); out.setCodec("UTF-8"); // Qt5 必须显式设,否则中文按本地编码写出去 out << "启动时间: " << QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss") << endl; f.close(); // 不写也会在析构时关,但显式关能及时拿到写入错误

参数说明:QIODevice::WriteOnly只写,Text表示按文本模式处理换行(Windows 下会转成\r\n),Append追加不覆盖,做日志三个要一起用;applicationDirPath()返回 exe 所在目录,比currentPath()稳,因为后者取决于你是双击启动还是从别的程序拉起来的。setCodec("UTF-8")这行是中文乱码的高发点,Qt5 的 QTextStream 默认用系统本地编码,不设就是 GBK 写、UTF-8 读,一读一个乱码。

读文件时readAll()适合小文件,几百 KB 以上建议按行readLine()循环处理,避免一次性把内存顶上去。QFileDialog 那节配的是文件浏览对话框,getOpenFileName和getExistingDirectory两个静态函数最常用,返回空字符串就是用户点了取消,这个返回值必须判。

4.2 INI 与 XML:两种配置格式的取舍

书里 INI 那节用的其实是 QSettings,只是把格式固定成 IniFormat:

#include <QSettings> QString iniPath = QCoreApplication::applicationDirPath() + "/config.ini"; QSettings ini(iniPath, QSettings::IniFormat); ini.setIniCodec("UTF-8"); // INI 里的中文也要设编码 ini.setValue("db/host", "127.0.0.1"); // 斜杠自动分组,写成 [db] host= ini.setValue("db/port", 3306); ini.sync(); // 立即刷盘,别等析构 QString host = ini.value("db/host", "127.0.0.1").toString(); // 第二个参数是默认值 int port = ini.value("db/port", 3306).toInt();

QSettings 的好处是跨平台,Windows 上写注册表、Linux 上写配置文件都由它接管,指定 IniFormat 之后行为统一。默认值那一手要给,用户第一次运行没有配置文件,不给默认值拿到的是空字符串,toInt()之后是 0,端口变成 0 连不上还不好查。sync()也别省,程序如果被强杀,没刷盘的值就丢了。

XML 那节用 QDomDocument,写入和读取是两套流程。写入是建节点、挂子节点、设属性、拼文本,最后save()到 QFile;读取是setContent()解析、documentElement()取根、firstChildElement()逐层往下、attribute()取属性。用 XML 的场景通常是配置结构比较深、需要嵌套,比如一个采集任务里有多个通道、每个通道有多个参数,INI 的扁平结构表达起来就难看了。代价是 DOM 解析会把整棵树读进内存,几十 MB 的 XML 不建议这么干,书里示例的规模无所谓。

4.3 QSqlDatabase 操作 SQLite:从建表到视图

数据库这章先讲怎么查驱动,再讲 QODBC 连 Access,最后落到 SQLite。实际项目里 SQLite 用得最多,因为它免安装、单文件、跟着 exe 走:

#include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QSqlTableModel> QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // 连接名默认 qt_sql_default_connection db.setDatabaseName(QCoreApplication::applicationDirPath() + "/demo.db"); if (!db.open()) { qCritical() << "打开数据库失败:" << db.lastError().text(); return; } QSqlQuery q(db); if (!q.exec("CREATE TABLE IF NOT EXISTS user(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "created TEXT)")) { qCritical() << q.lastError().text(); } q.prepare("INSERT INTO user(name, created) VALUES(?, ?)"); // 占位符防注入 q.addBindValue("A同学"); q.addBindValue(QDateTime::currentDateTime().toString(Qt::ISODate)); if (!q.exec()) { qCritical() << q.lastError().text(); }

参数说明:addDatabase("QSQLITE")里的字符串是驱动名,写错会返回一个无效连接,open()直接 false;prepare+addBindValue是标准写法,别用字符串拼接 SQL,名字里带个单引号就把语句拼断了。lastError().text()是排查数据库问题的唯一可靠信息源,每步都打一遍不亏。

要把表显示到界面上,书里用的是 QSqlTableModel 配 QTableView:setTable("user")、setEditStrategy(QSqlTableModel::OnManualSubmit)、select(),再ui->tableView->setModel(model)。手动提交策略下,界面上改完要显式调submitAll(),这个不调会以为数据没保存,实际是攒在模型里没发出去。

一个容易忽略的细节:QSqlDatabase的连接是按线程隔离的,在线程里用连接必须各自addDatabase起一个新连接名,跨线程共用一个连接在 SQLite 上会直接崩。第 12 章讲线程,第 10 章讲数据库,两章内容撞在一起的时候这个坑特别隐蔽。

5. 常见问题排查:编译、运行、部署三段式踩坑记录

示例代码抄下来跑不通,九成问题集中在三个阶段:编译链接、运行期表现异常、拷到别的机器打不开。下面五条是这类范例工程最容易撞上的。

5.1 编译报 vtable 未定义,加完 Q_OBJECT 也没用

现象:类里明明写了Q_OBJECT,编译到链接阶段报undefined reference to 'vtable for MainWindow',或者槽函数找不到实现。

原因:Q_OBJECT需要 moc(元对象编译器)处理生成moc_xxx.cpp,而 moc 的触发依赖 qmake 扫描.pro。改完头文件只点“构建”,qmake 没重跑,moc 文件还是旧的,或者干脆没生成。另一种情况是.pro的HEADERS里漏了这个头文件,qmake 根本没看到它。

解决:先执行“构建 → 执行 qmake”,再重新构建;确认HEADERS +=里列全了所有带Q_OBJECT的头文件;还不行就把构建目录整个删掉重建,中间产物残留是玄学问题的主要来源。这条经验值最高,我在三个不同项目上碰到过同一个报错。

5.2 中文全是乱码或者方框

现象:界面上标签显示成????,或者日志文件里中文读出来是乱码,编译期还可能报一堆关于字符集的警告。

原因:三层编码没对齐——源文件本身存的是什么编码、编译器按什么编码读、QTextStream 按什么编码写出。Qt5 时代源文件如果存成不带 BOM 的 UTF-8,MSVC 编译器默认按本地编码解析,中文字符串常量就已经错了。

解决:源文件统一存 UTF-8 带 BOM,或者在 MSVC 的编译参数里加/utf-8;QTextStream 显式setCodec("UTF-8");QSettings 同样要setIniCodec("UTF-8")。Qt5.1 那个年代的示例有的用 GBK 保存,直接拷到自己工程里就会乱,所以抄代码时要连编码方式一起确认。

5.3 双击 exe 没反应,或提示缺少平台插件

现象:在 Qt Creator 里跑得好好的,把 exe 拷到另一台机器双击,要么毫无反应,要么弹窗报This application failed to start because no Qt platform plugin could be initialized。

原因:Qt 程序运行时依赖一堆动态库和插件目录,开发机上因为 PATH 里有 Qt 的 bin 目录所以能找到,换台机器就断了。缺的不只是 Qt5Core.dll,还有platforms/qwindows.dll这个平台插件。

解决:用 Qt 自带的部署工具,在 exe 所在目录执行一次:

# 在 exe 所在目录打开命令行,把依赖库和插件拷齐 windeployqt QtDemo.exe --release --sql --network # 部署完的目录大致长这样 # QtDemo.exe # Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll ... # platforms/qwindows.dll <- 少了这个就报平台插件错误 # sqldrivers/qsqlite.dll <- 用了数据库才会有,--sql 负责带出来

参数说明:--release对应 release 构建,debug 构建要换成--debug,两者混用会导致库版本不匹配;--sql、--network、--multimedia是按需带插件,不带的话运行到相应功能才崩,比启动失败更难查。部署完务必在一台没装过 Qt 的机器上验一遍,这是唯一可信的验证方式。

5.4 数据库连接报驱动未加载

现象:开发机上 SQLite 读写正常,打包出去运行,QSqlDatabase::open()返回 false,lastError().text()显示QSQLITE driver not loaded。

原因:qsqlite.dll在 Qt 安装目录的plugins/sqldrivers/下,没被一起拷过去。windeployqt 默认不一定带数据库插件。

解决:命令行加--sql,或者手动把sqldrivers整个目录拷到 exe 旁边。查驱动列表用QSqlDatabase::drivers(),打印出来看有没有QSQLITE,一眼就知道是插件没到还是驱动名写错。

5.5 界面卡死或直接崩溃,日志指向线程里的控件操作

现象:把耗时任务丢进 QThread 之后,界面偶尔卡顿,偶尔直接崩溃,崩溃位置不固定,堆栈里出现 QWidget 相关函数。

原因:Qt 的界面对象只能在主线程操作,子线程里调setText、append这类方法属于未定义行为,跑一百次可能九十九次正常,第一百次崩给你看。

解决:子线程算完通过信号把结果发回主线程,槽函数里再更新界面,跨线程的connect默认走队列连接,是安全的;或者用QMetaObject::invokeMethod(obj, "slotName", Qt::QueuedConnection, Q_ARG(QString, text))强制排队。第 12 章 QThread 那节示例本身是干净的,问题是抄的人把耗时逻辑和界面更新揉在一起了。

6. 二次开发:把散装范例拧成自己的工程骨架

这套范例最实际的用法不是从头到尾读一遍,而是把它当成零件仓库。我的做法是:先搭一个空壳工程,QMainWindow 做容器,左侧 QListWidget 做导航、右侧 QStackedWidget 做页面切换,把第 2 章到第 4 章的示例按“一个功能一个页面类”的方式往里搬。搬的时候只留业务代码,界面全部用代码手写而不是拖.ui,因为示例里的.ui文件版本差异大,混着用容易在 Designer 里打不开。

第二步是收敛配置和日志。把第 5 章的 INI 读写封成一个 AppConfig 单例,对外只暴露getString/getInt/setValue三个方法,内部统一走QSettings加setIniCodec("UTF-8");日志用 QPlainTextEdit 做一个只读窗口,配一个qInstallMessageHandler把 qDebug 的输出转进去,比满屏 qDebug 好查得多。这两块搭好之后,后面无论加什么模块都不用重复造轮子。

第三步是线程模型定调。第 12 章给了 QThread 和 QRunnable 两种写法,我的选择是:常驻的、需要持续收发的用 QThread 加moveToThread,一次性的批处理任务用 QRunnable 丢进 QThreadPool。所有跨线程的界面更新一律走信号槽,不写任何“应该没事”的直连。

验证方法很简单,每搬完一个模块就单独跑一次,跑不通就退回上一个能跑通的版本,不要一次搬五个模块再编译。我吃过这个亏:五个模块一起编,报出四十多个错,最后发现是.pro里少写了一行QT += sql。

改造目标直接可参考的章节需要动的地方
主窗口框架第 2 章窗体基类、第 3 章 QTabWidget换 QMainWindow + QStackedWidget
配置持久化第 5 章 INI 读写封装成单例,加默认值
业务数据存储第 10 章 SQLite连接按线程拆分,建表语句抽成独立初始化函数
局域网通信第 11 章 TCP/UDP拆包协议要自己补,示例只做了收发
打包发布第 14 章换成 windeployqt + 安装包工具两步走

从那以后我每次接这类老项目,都强制先跑通一个最小壳子再往里填功能,宁可多花半天搭骨架,也不在别人的散装代码上直接改。这份范例的价值就在这里:它不教你怎么设计架构,但它把每个零件都做成了能独立运转的样子,你需要的只是把它们拧到一起。希望帮到你。

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

返回列表