Qt技术话题
一、基础(待续)
我想:“你一定会长成健康的胖子”。虽然是胖子但你很敏捷,让加载更快吧。让胖子飞起来吧~~。
不喜欢在vs里通过插件的方式使用qt,creator的调试功能足够了。
资源
项目文件
pro文件
- 想知道配置项的内容是什么?简单的message($$CONFIG)就可以搞定。输出的信息展示在"6.摘要信息"中。
- 平台依赖的代码怎么定义呢?看看下面的内容,显然不难。
unix{
DEFINES += PLATFORM_LINUX
}
win32{
LIBS += -lws2_32
DEFINES += PLATFORM_WIN32
}
- LIBS的用处,一是精确定义依赖库的位置和名称;二是通知调试器依赖库的查找位置。第二点可以有效解决creator调试时无法启动目标软件的问题。
LIBS += -Lyour_libs_path
pri文件
字符和数字
UTF-8,UTF-16,ANSI等字符编码,你就使劲的玩儿吧。
QString 是UTF-16编码,从16bit字符转到8bit字符转换,Qt提供三种方法:
QString str("hello qt world");
QByteArray byte1 = str.toUtf8(); //utf-8编码的8bit字符,utf-8是US-ASCII (ANSI X3.4-1986)的超集
QByteArray byte2 = str.toLatin1(); //拉丁文8bit字符
QByteArray byte3 = str.toLocal8Bit(); //本地系统默认8字符
序/反列化
文件读写是非常基础的技术,在数据序/反列化中,需要特别强调的是:自定义格式中的版本兼容问题。QDataStream支持许多QT原生类型的序列化与反序列化,不的不说这个类方便耐用。
以下是我对官方文档中关于使用QDataStream类进行数据序列化和反序列化的理解。
- 自定义文件格式的头部至少应设计为包括magic_number + version两部分信息。看代码。
- magic_number用于确定文件格式是否正确。
- version用于确定使用那部分代码读取数据。
- QDataStream要求待序列化数据为强类型&必须是它支持的类型。
//序列化内存数据到文件
QDataStream out(&file);
out<<(quint32)0x0A0B0C0D;
out<<(qint32)123;
out.setVersion(Qt_4_0);
out<<some_intresting_data;
//反序列化文件到内存数据
QDataStream in(&file);
quint32 magicNum;
in>>magicNum;
if (0x0A0B0C0D != magicNum) {
return BAD_FILE_FORMAT;
}
qint32 version;
in>>version;
if (version < 100) {
in.setVersion(Qt_3_2);
else
in.setVersion(Qt_4_0);
}
in>>some_instresting_data;
信号与槽
N种款式,你喜欢啥?
- 传统型内部的较量
connect(sender, SIGNAL(signal_fun(QObject*)), this, SLOT(slot_fun(QObject*)));
connect(sender, SIGNAL(signal_fun(QObject*)), this, SLOT(slot_fun()));
connect(sender, SIGNAL(signal_fun()), this, SLOT(slot_fun()));
- 信号无参数,槽有参数,传统找死型
connect(sender, SIGNAL(signal_fun()), this, SLOT(slot_fun(QObject*)));
- 函数指针,简约型
QObject::connect(&objectA, &QObject::signal_fun, &objectB, &QObject::slotFun);
connect(action, &QAction::triggered, engine,
[=]() { engine->processAction(action->text()); });
可能的错误1:QMetaObject::connectSlotsByName: No matching signal for
原因:槽函数使用了“on_控件名_信号名”的格式。
解决办法:修改槽函数为非“on_控件名_信号名”的格式的名称。
总结:首选高级和简约型,5.0之前的版本另讲。
界面也要礼尚往来
- 创建一个全局单例对象,用于管理不同层次界面间的必要信号&槽链接。
退出应用的函数
事件循环是Qt的重要概念,与事件循环有密切关系的退出函数值得一提。这些细节就是生产环节必不可少的内容。
QCoreApplication::quit();
QCoreApplication::exit(0);
上面的两个函数会导致应用退出,不过也有例外。比如说:没有进入事件循环前,对他们的调用将没有任何效果。一个好的编码实践是(看看有下面的代码和你的代码有什么不同,然后我认为Qt的文档是业界最牛逼的-简单、明了,结构清晰,节约生命。):
QPushButton *quitButton = new QPushButton("Quit");
connect(quitButton, SIGNAL(clicked()), &app, SLOT(quit()), Qt::QueuedConnection);
事件循环
看几个问题:
- 谁开启了事件循环?
- 谁又导致事件循环退出?
- 事件循环的空闲时间怎么利用?
- 怎么适时释放动态资源?
关于问题的答案:
- 是它开启了应用程序的事件循环,似乎已经熟悉到忽略他的存在了。
return QCoreApplication::exec();
- 主动调用退出应用的函数,或者关闭了窗口。退出应用的函数文中已有介绍,不再赘述。需要多说的是,exec()是主函数的最后一句代码,动态资源释放将变的非常尴尬++(释放代码也希望在主函数最后)。Qt考虑了很多。Qt推荐:把释放资源的槽函数连接到aboutToQuit()信号,是非常好的编码实践。这个信号在退出事件循环后触发,同时,作为私有信号它不能被人为emit,感觉吧啦吧啦说到现在,终于可以安静的休息了。
- 事件循环由exec()接管,看似无法插手,但Qt考虑了很多,此时使用QTimer或者QCoreApplication::processEvents()是不二选择。比较形象的词是:狼嘴里抢肉就靠它两。
- 下面的一句话足以解决心中烦闷。
QObject::conncet(this, &QCoreApplication::aboutToQuit, this, &cleanUp);
还有要说的
如果想使用第三方信号槽机制,这句话丢到pro的肚子里。两方机制都想使用,把源码中的信号和槽用QT特有宏包裹起来吧,Q_SIGNALS/Q_SIGNAL,Q_SLOTS/Q_SLOT。
CONFIG += no_keywords
日期和时间
直奔主题,这两天在搞基于Linux的嵌入式设备代码,遇到一个时间同步的(上位机的时间同步到设备)问题。借此机会,把过程梳理一遍。
背景:设备接收上位机派发的任务,并在规定的时间开始执行任务。这个简单的需求中,需要考虑设备与上位机的时间的一致性问题。
我的实现方式是:上位机下发以UTC为标准的时间戳,设备把时间戳还原为具体时间。按理说很简单,不是吗?但是出问题了,经过调试发现,设备侧无法还原出与上位机一致的时间。导致设备不能按照要求准时执行任务。问题在哪里?
经过一番折腾,发现“时区”差异导致了时间还原不正确的问题。既然是时区问题,那么答案就简单了。如果通信双方都是通用计算机,建议设置时区,这样再简单不过。我的环境刚好是嵌入式设备,那么我的需要其他有效的方式。下面是我的代码:
//上位机测试代码
QDateTime localTime = QDateTime::currentDateTime();
QDateTime utcTime = localTime.toUTC();
quint64 secSinceE = utcTime.addSecs(localTime.offsetFromUtc()).toTime_T();
//下位机
printf("%s", asctime(localtime(secSinceE)));
重点说明的内容,localTime.toUTC()是含有时区概念的时间,这个时间需要调整,调整的量由offsetFromUtc()的结果决定。因为我是东半球,北京时间,所以win10系统下通过Qt的QDateTime产生的UTC时间与绝对UTC相差28800秒,也就是8小时。把8小时补回来,设备能容易的还原时间。
方法总结:
方法1.让通信双方的时区保持一致;
方法2.向设备的默认时区妥协,由上位机产生可还原时间戳。我使用的方法。
以上代码的前提条件:我的设备侧默认是绝对UTC,也就是UTC+0000。你的情况或许是另外一番景象,但是坚持方法总结中的第一条,不会走偏。
单元测试
在编码实践中,无论你是否老辣,单元测试应成为咱的守护天使。节约时间就是节省生命资源。单元测试框架我只用过googletest。这次项目中使用Qt原生的单元测试框架:QTest。除自身的测试框架外,Qt也非常好的支持googletest。本节只说QTest。
要素
- 独立的测试项目(不侵入待测试项目的源代码):新建项目中选择其他项目后,可以顺利的找到测试项目;
- 添加待测项目源文件目录到测试项目中,体现在pro的SOURCES和INCLUDES;
- 测试自定义类,为了访问类中的私有成员,请添加==#define private public==到测试文件尽量靠前的位置(见下);
#include <QTest>
#define private public
#include <your_source_header1>
#include <your_source_header2>
class autotest_uint
{
...
private slots:
void autotest_case1_fun();
}
void autotest_unit::autotest_case1_fun()
{
CYourClass test;
test.load(your_data_file);
QVERIFY(test.isOk());
QCOMPARE(test.mSize, 5);
}
数据驱动的测试
外围代码见要素,只看重点。
test_case2_bydata_data()3
test_case2_bydata()4
autotest::test_case2_bydata_data()
{
QTest::addColumns<QString>(your_define_test_data);
QTest::addColumns<qint32>(you_define_expect_value);
QTest::newRow("test_data_1")<<"aaaa"<<4;
QTest::newRow("test_data_2")<<""<<0;
}
autotest::test_case2_bydata()
{
QFETCH(QString, data);
QFETCH(qint32, value);
QCOMPARE(data.size(), value);
}
//test_case2_bydata_data() //组织测试数据
//test_case2_bydata() //测试用例
日志系统
QT日志定义
- qInstallMessageHandler(fun_pointer),qt开放给用户的日志信息方法,很周到。qInstallMessageHandler(0)可以告诉我们fun_pointer。
- 致命信息(QtFatalMsg)会导致应用立即退出。
- 日志应有的小弟:包括时间,等级、所在文件、所在行号、描述信息等。
- debug和release的日志都要输出?pro文件中增加
DEFINES += QT_MESSAGELOGCONTEXT
关闭日志输出
关闭日志输出注意两点,不然你会郁闷。关闭日志在QTest中特别给力。
- 在profile文件中增加一下内容,自由搭配,适合项目就好。
- 一定要重新构建。
DEFINES += QT_NO_WARNING_OUTPUT \
QT_NO_DEBUG_OUTPUT \
QT_NO_INFO_OUTPUT
本地化
应用打包
打包相关的章节,《坑坑哇哇》->“打包程序”。
必要的库文件
发布软件包时,考虑以下库文件的复制。笨人笨办法:
- 查看pro文件中的QT定义,请复制对应库到可执行文件位置。例如:
QT += gui network sql
此时最少需要从编译工具链对应的bin目录(C:\Qt\Qt5.12.9\5.12.9\mingw73_64\bin这是我的文件位置)下获取Qt5Gui.dll,Qt5Network.dll, Qt5Sql.dll文件,然后复制到待发布软件包目录。
- 查看pro文件中的LIBS定义,第三方的库文件同样需要亲自处理,复制到待发布软件包目录。
- plugins下的platforms目录(C:\Qt\Qt5.12.9\5.12.9\mingw73_64\plugins\platforms==我的文件位置)复制到待发布软件包目录。平台依赖库。
除plugins中的库文件外,其他库文件应放置在可执行文件同级目录。
二、进阶(待续)
状态机
视图模型
listview
它的模型之一:QStringListModel,QStringList内容的改变自动反馈到View,这就是MVC中的MV。例子:滚动展示日志的精要。
QStringList msgList;
msgList<<"有新的TCP连接请求";
msgList<<"设备磁盘占用超过80%";
QStringListModel *msgListModel = new QStringListModel;
msgListModel->setStringList(msgList);
listView->setModel(msgListModel);
void onMsgReady(QString logMsg)
{
QStringList msgList = listView->model().stringList();
msgList<<logMsg;
listView->model().setStringList(msgList);
}
定时器
通信
内部进程通信
仍不住的想说:QT的文档建档,简单明了,爱了。剩下的只需要知道关键字,然后就能办了问题。比如说这个主题《内部进程通信》。在帮助文档中通过索引的方式,搜索“inter-process”,一切真相大白。
这个话题放在这里有一段时间了,今天在嵌入式Linux中调试Qt5.6编写的IPC代码。背景:原有框架中已指定IPC的方式:AF_UNIX的内部通信机制。先加入新功能,仍以进程方式协作。为了提高开发效率,新加入的功能使用Qt5.6开发。按理说QLocalSocket可以很好的解决通信问题,结果是乌龙百出。
问题出在:QLocalSocket的具名连接方法()上。看上去,它的使用很简单,其实这里有个“具名”路径问题,否则会出现无法连接服务端的问题。
原有代码的“具名”路径是可执行文件的同级目录,连接方法默认的“具名”路径是一个犄角旮旯的地方,只有同时使用Qt开发C/S两端时不会出问题,除此以外很难建立连接关系。
你知道吗?当涉及文件时,路径是必须考虑的要素。而linux下一切皆文件,那么今后谈文件,就应该问路径,否则弯路是要走一些的。最终的关键代码如下:
QString serverName = QCoreApplication::applicationDirPath();
serverName += "/your_ipc_name";
QLocalSocket ipcSocket;
ipcSocket.connectToServer(serverName);
if (ipcSocket.waitForConnection()) {
//hardwork.
}
网络通信
QTimer
毫秒级的高级别定时器,用于非精度定时场合。其实这个定时的精度是可配置的,定时器分为3个级别,毫秒,偏差5%毫秒和秒。
- 默认偏差5%毫秒示例
#include <QTimer>
QTimer *timer = new QTimer();
timer->setInterval(1000); //<<这里是毫秒
timer->connect(timer, SIGNAL(timeout()), this, SLOT(onTimer()));
timer->start();
- 毫秒精度示例
#include <QTimer>
QTimer *timer = new QTimter();
timer->setTimerType(Qt::PreciseTimer);
timer->start(50); //是的,50毫秒定时器为你工作了。
- 秒级精度的代码不想写了。我认为QT的开发文档绝对是最牛的,应该颁发最佳责任奖。不但表达清晰而且示例结合超级好。
多线程
多线程也是双刃剑,如果不能很好的控制和理解,我想还是老老实实的在单个线程中干活吧(简单,省心)。如果单线程真的不能满足你的任务需求,也还是要绞尽脑汁的想出:为什么必须用多线程。
如果必须使用多线程处理你的业务,了解QT下的使用多线程思路是必要:
考虑一下这个问题
- 业务线程中是否使用信号槽?
可能的情况:
1.我的业务就是一个while(true) {do something}搞定,纯粹的使用CPU算力。
2.需要借助Qt自身的事件机制,分状态的处理业务。
我的看法:
“可能的情况”中描述了问题的解答思路,情况1是说:业务中不使用Qt的信号与槽核心机制,所有的事件由开发人员自行处理,相当与写一个专用与自身业务的事件处理机制。优点是可控性强,缺点是代码量大,且不能使用信号槽任何便捷。
情况2是说:我要像GUI线程一样,充分使用Qt框架提供的便捷,少操心就好。这是需要真正理解线程中的事件循环,退出和阻塞等要点。exec(),exit(),wait()是需要关注的内容。优点是Qt为我们考虑的所有机制都可以使用,省心省力。缺点是需要深入了解Qt的机制。
我倾向于情况2,以上是近期做任务的领悟。
并发
- 转移线程,moveToThread();
连接的艺术
- 自动连接是什么,它负责解决什么问题。Qt::AutoConnection;
- 直连是什么,它又负责解决什么问题。QT::DirectConnection;
- 为什么提供查询类的连接,Qt::QueuedConnection;
- 阻塞连接复杂吗?解决什么问题?Qt::BlockingQueuedConnection;
- 唯一连接难懂吗?又要解决什么问题?Qt::UniqueConnection。
绘图
反走样(抗锯齿)
在graphicview及其子类里提供反走样函数,绘制的图形更平滑,这种魅力是牺牲cpu时间换来的。下面代码不要放错位置。
your_view_object.setRenderHint(QPainter::Antialiasing);
QElapsedTimer
计算已消耗时间的便捷类。它提供的start和elapsed是高频方法。要重新计时只需要再次调用start方法就好。
QDeadlineTimer
三、win下编译Qt6
重要的参考资料
先看生成命令
关键的几条命令可以解决编译和安装问题。当然,一定有后话。
启动x64 native tool command type for vs2019环境,使用一下命令。
cd qt_source_dir
configure -debug -prefix setup_path -opensource -- -Bbuild_path
cmake --build . -parallel
cd build_path
ninja install
我的编译命令,供参考
没有包括examples的编译命令如下:
configure -release -prefix d:/qt6 -opensource -- -B../build/release -DCMAKE_PREFIX_PATH=e:/llvm/ -DFEATURE_clangcpp=ON
cd ../build/release/
ninja
ninja install
包括examples的编译命令如下:
configure -release -prefix d:/qt6 -make examples -qt-zlib -qt-libjpeg -qt-libpng -qt-freetype -qt-pcre -qt-harfbuzz -opensource -- -Be:/qt/build/release -DQT_NO_MAKE_EXAMPLES=ON -DCMAKE_PREFIX_PATH=e:/llvm/ -DFEATURE_clangcpp=ON
cd ../build/release/
ninja
ninja install
其他配置见"重要的参考资料"一节。
llvm用于编译QDoc,生成Qt离线文档。
cd ./build/release
ninja docs
ninja install_docs
assistant -register qtdoc.qch
ninja docs执行之后,doc目下自动生成qch类型文件和qhp类型,其中qch可以通过qtcreator的帮助界面查看。
参考的资料,适用于qt5。
编译环境准备
- vs 2019 构建工具,不必安装IDE。类似的内容,看看ffmpeg的编译是有用的。
- CMake下载和安装。
- python 3.9以上的下载和安装。
- perl下载和安装。
- ninja下载和安装,类似于make的轻量级但高效的编译文件组件工具
- Qt6源码下载
- QtCreator5.0下载
Qt6编译成功后的第一个应用程序
经过前面的准备,编译和安装,也许晕了多圈儿了,但是问题依然存在,请再保持清晰,看完这一节再做其他打算。
- 使用creator5新建GUI工程,不必编写任何代码,默认的内容足矣。
- 在creator的工具->选项中配置kit,主要是配置Kits中的Qt version字段为已编译并安装好的Qt6。
- 更加重要的是系统环境变量中的TEMP中不能包括中文字符,否则编译错误将是:can open 巴拉巴拉.jom for write。
Qt6结束
其实,这是最基础组件的的编译。更进一步的编译,像额外的模块编译和configure的有大量选项,都需要时间实践和消化,慢慢来,持续补充。
四、坑坑洼洼(待续)
字符编码
乱码,QString默认编码,本地系统编码。三者搞清楚后,乱码将从源头干掉。
这篇文章值得一看
编译链接
突然接到一个任务,编译一个满是中文字符串和中文注释的基于QT creator开发的软件源代码。因为是在windows10 x64编译,我理所当然的选择了msvc 2017 x64作为编译工具链。问题因此出现了,文件编码问题引发了大量编译错误。解决办法如下:
- 使用其他文本编辑工具转换所有源文件为utf-8 + BOM,然后编译;我没有采用这个方法,引用自网络资料;
- 所有源文件增加编译选项:#pragma execution_character_set(“utf-8”),我尝试过,文件太多,一定还有更好的办法;
- 干脆视而不见,屏蔽告警信息。<==不能解决根本问题。
- 更换编译工具,我换成了MinGW x64。换了编译器躁动的世界终于安静了。多说一句:msvc存在文件编码引起的编译问题。
没错你看到我经历了。我尝试处理所以错误,但这本身就是个错误,果断更换编译工具。
文件路径
- 软件运行时依赖的外部资源怎么处理?我的作法是:统一放置,然后定义全局相对路径,切记满世界硬编码路径。
QString path = QCoreApplication::applicationDirPath(); //可执行文件所在的目录
- QFileInfo和QDir能我很好的完成文件创建,重命名,路径创建,文件和路径分离等等能想到的文件管理功能。
//分离路径
QFileInfo fileInfo("your/path/file");
QString path = fileInfo.path();
QString fileName = fileInfo.fileName();
//创建路劲
QDir target("");
target.mkpath(path); //创建父级所有路径,如果路径已存在则返回true.
//复制文件到新的位置
target.copy("new/path/file");
运行时异常
- 传入实参的合法性一定要检查,不然死的很惨。
- 关注占用80%运行时间的20%代码逻辑。想好了,盯紧了,测稳了。
异常捕获
调试手段
- 程序运行时附带控制台窗口,观察输出信息。CONFIG += console
收不到信号
在调试QLocalSocket的时候,无论如何都没有收到过connected信号(Qt5.12.9),这是怎么一回事?请同行点拨一下。
打包程序
自动化方法
在程序开发的生命周期里,把开发成果打包是无法跳过的重要步骤。今天遇到release版+基础qt库无法打包后无法运行在目标机的问题。提示:no qt platform plugins initialized。
解决方式比较简单:
- 确定编译工具,例如:msvc 2017 32bit这是我的编译工具。
- 在qt安装目录下,找到msvc 2017目录下的plugins/platforms。把整个platform文件夹复制到可执行程序相同位置。
- platform下的内容包括调试版和发行版动态连接库。自行处理