多读书多实践,勤思考善领悟

Qt项目升级到Qt6移植总结

本文于401天之前发表,文中内容可能已经过时。

一、前言

Qt 6.2 也是 Qt 6 的第一个版本,Qt 公司将为 Qt 商业客户提供长期支持。

在 Qt 6.2 中,包含了 Qt 5.15 中的所有常用功能以及为 Qt 6 添加的新功能。随着 Qt 6.2 的发布,几乎所有的用户都应该能够将他们的代码从 Qt 5 迁移到 Qt 6。

目前Qt的支持路线为:Qt5.15.2编译的程序支持WIN7及以上的系统,Qt5.6.3编译的程序支持XP系统。

二、Qt 6 的架构变化

我们在 Qt 6 中进行了一些更广泛的架构更改,我们现在正在构建 Qt 6.2 和任何未来版本。

  • 利用 C++17 在处理 Qt 6 时,我们希望建立在现代 C++ 标准之上。C++17 是最新发布的版本,而 Qt 6 现在依赖于 C++17 兼容编译器。这使得我们可以清理和改进我们的代码库,并为我们的用户提供更现代的 API。
  • 在处理大型数据集和性能方面改进了我们的低级容器类。
  • 下一代 QML 我们已经开始更加努力地更新 QML 语言,使其在未来更安全、更易于使用。Qt 6.0 – 6.2 在这里奠定了基础,但这种持续的努力将在整个 Qt 6 系列中继续进行。
  • 将属性绑定引入 C++ 属性绑定是使 QML 如此成功的概念之一。在 Qt 6 中,我们一直在 C++ 中提供该概念 。
  • 新的图形架构 Qt 6 在如何处理与底层操作系统 API 的集成方面采用了 全新的架构。得益于新的渲染硬件接口 (RHI),Qt 6 现在默认使用每个系统上可用的最佳图形 API,显着提高了兼容性——尤其是在桌面和移动操作系统(如 Windows 和 macOS 以及 iOS)上。
  • Qt Quick 的统一 2D 和 3D Qt Quick 一直是构建动画和流畅的 2D 用户界面的框架。使用 Qt 6,我们也简化了将 3D 内容集成到基于 QML 的应用程序的过程。深度集成使得在任何级别混合 2D 和 3D 内容变得微不足道,同时从系统中获得最大性能。
  • CMake 构建系统 在 Qt 6 中,我们将构建系统从 qmake切换到 cmake,这是当今基于 C++ 的应用程序的标准构建系统。虽然在 Qt 6 的整个生命周期内仍支持 qmake,但初步用户报告表明切换到 cmake 后有显着改进。

三、添加模块

开发 Qt 6.2 的主要工作之一是重新添加我们在 Qt 6.0 中遗漏的所有模块和功能。除了极少数例外,Qt 5.15 支持的所有模块现在也支持 Qt 6.2。

在 Qt 6.2 中,我们添加了对以下附加模块的支持(在 Qt 6.1 中已有的模块之上):

  • Qt Bluetooth
  • Qt Multimedia
  • Qt NFC
  • Qt Positioning
  • Qt Quick Dialogs
  • Qt RemoteObjects
  • Qt Sensors
  • Qt SerialBus
  • Qt SerialPort
  • Qt WebChannel
  • Qt WebEngine
  • Qt WebSockets
  • Qt WebView

这些模块的 API 主要向后兼容 Qt 5,并且在移植到 Qt 6 时只需要对用户代码进行少量调整。

Qt 6.2 支持的 完整模块列表 可以在我们的文档中找到 。

四、Qt 6.2 中的新功能

除了我们从 Qt 5 带来的许多模块之外,我们 在 6.2 中还有大量的 新特性和功能。我们来看一下。

1、Qt 快速 3D

Qt Quick 3D 获得了一些很酷的新功能,现在支持 实例化渲染,允许您使用不同的变换渲染大量相同的对象。我们还添加了一个新的 API,用于向 场景添加 3D 粒子效果。

输入处理已得到改进,我们现在可以为嵌入在 3D 场景中的 2D 项目正确创建 Qt Quick 输入事件。我们还添加了一个新的 API,用于从场景中的任意点进行基于光线的拾取。

2、QML工具

Qt 6.2 对 QML 工具进行了较大改进。我们现在有一个公共的 CMake API ,它极大地简化了创建您自己的 QML 模块的过程。

QML linter (qmlint) 是一种工具,用于检查 QML 源代码的最佳实践、潜在的编码和性能问题,并帮助编写更易于维护的 QML。该工具经历了很大的变化,现在可以完全配置,无论是在命令行级别,还是通过配置文件,甚至是 QML 文件本身中的各个块。此外,它现在可以生成 JSON 输出以简化与其他工具或自动化系统的集成。

QML 格式化程序 (qmlformat) 现在使用 QML dom 库,大大改进了生成的输出。

3、Qt多媒体

Qt 多媒体在 Qt 6 中发生了一些相当大的变化。它是我们在 Qt 5 的生命周期中不满意的 API 之一。因此,我们退后一步,对 Qt 6 进行了一些更广泛的 API 和架构更改。没有过多考虑向后兼容性的模块。

尽管如此,从 Qt 5 中的 Qt 多媒体移植到 Qt 6 应该相对简单。

Qt 6 中的 Qt 多媒体确实支持一些我们在 Qt 5 中从未设法正确支持的高度要求的功能。示例包括播放的字幕和语言选择支持以及媒体捕获的可配置设置。

内部架构已经过清理,不再像 Qt 5 那样通过公共 API 公开。这将使我们能够更快地修复错误,并使将来添加新功能变得更加容易。您可以在有关 Qt 6 中的 Qt 多媒体的单独博客文章中找到更多详细信息。

然而,由于这些巨大的变化,该模块仍然存在粗糙的边缘,并且可能在实现中存在相当多的错误。但是,我们相信多媒体是一项必不可少的功能,我们将在 Qt 6.2 中完全支持该模块。

因此,我们将在补丁级别版本的常规提交策略上有所偏离,如果需要修复较大的问题,可能会添加一些较小的 API。

此外,我们将努力在即将发布的补丁级别版本中尽快修复任何报告的错误。

4、整个过程中的小改进

几乎所有其他模块都看到了许多较小的 API 添加和改进。

我们已经移植了许多 API 以利用新的属性系统,以便您可以使用 C++ 中的属性绑定。这项工作尚未完成,我们将在未来的版本中继续。

我们还在各个地方修复了许多 API 缺点和缺失的功能。仅举几个例子:

  • Qt Charts 获得了一些新的 API,以提高便利性并使事情更加可定制。
  • 我们为 QImage 添加了浮点图像格式。
  • QByteArray::number() 现在可以正确处理 10 以外的基数的负值。
  • QLockFile 现在具有采用 std::chrono 的重载
  • Qt Network 支持多个可以在运行时共存的 SSL 后端。

5、Qt Creator 和 Qt Design Studio

Qt Creator 和 Qt Design Studio 也做了大量工作,以确保它们为 Qt 6.2 提供一流的支持。Qt Creator 5 包含您为 Qt 6.2 开发所需的一切。

我们今天还发布了全新版本的 Qt Design Studio。Qt Design Studio 2.2 基于 Qt 6.2,极大地支持在一个图形工具中创建基于 Qt Quick 和 Qt Quick 的 3D 用户界面。您可以轻松地在目标硬件上测试这些,无论是台式机、移动设备还是嵌入式设备。有关 更多详细信息,请查看有关Qt Design Studio 2.2的单独博客文章。

五、新平台

对于 Qt 6.2,我们做了很多工作来改进我们对当前支持平台的支持,包括桌面和移动端,例如,通过改进我们对 HighDPI 渲染的支持和在 iOS 上添加 NFC 后端。

最重要的是,Qt 6.2 大大扩展了支持平台的范围:

Qt 6.2 完全支持 Apple Silicon 上的 macOS。Qt 现在可以轻松创建通用二进制文件并在 Intel 和 Apple Silicon 上为 macOS 进行开发。当然,该版本也在我们的 CI 系统中进行了全面测试。一直可以通过 Rosetta 层在 Apple 芯片上运行 Qt 应用程序,但 Qt 6.2 现在提供了在 Apple 芯片上本地运行的完整支持。

Qt 6.2 还恢复了对 INTEGRITY 和 QNX 实时操作系统的支持。支持需要 C++17 工具链和最新版本的操作系统。QNX 的最低要求是 7.1 版,在 INTEGRITY 上,我们支持 19.0.13 版。

针对 Qt 6.2 的 webOS 验证也已完成,以进一步加强 Qt 对 webOS 的承诺。

有很多工作正在进行以支持 Windows 11,我们希望能够在 6.2 补丁级别版本中为其提供全面支持。Windows on ARM HW 也可作为 Qt 6.2 的技术预览版提供。

最后,我们做了进一步的工作来改进我们对 WebAssembly 的支持,它在 Qt 6.2 中作为技术预览提供支持。

Qt for Python 今天也发布了,大家可以试一试。未来几天将发布一篇单独的博客文章,重点介绍最新 Qt 6.2 更改中采用的所有功能。敬请关注!

六、Qt6 核心的变化

Qt 6 是有意识地努力使框架更加高效和易于使用的结果。

我们尝试维护每个版本中所有公共 API 的二进制和源代码兼容性。但是为了使 Qt 成为更好的框架,一些变化是不可避免的。

在本主题中,我们总结了 Qt Core 中的这些变化,并提供了处理它们的指导。

1、容器类

QHash、QMultiHash、QSet

qHash() 签名

对于自定义类型,QHashQMultiHash依赖于您在同一命名空间中提供自定义 qHash() 函数。在 Qt 4 和 Qt 5 中,函数的返回值和可选的第二个参数的qHash类型是uint. 在 Qt 6 中,它是size_t.

也就是说,你需要改变

1
uint qHash(MyType x, uint seed);

1
size_t qHash(MyType x, size_t seed);

这允许QHashQMultiHashQSet在 64 位平台上保存超过 2^32 个项目。

参考文献的稳定性

Qt 6 中QHashQMultiHashQSet的实现从基于节点的方法更改为两阶段查找表。这种设计允许保持哈希实例的内存开销非常小,同时提供良好的性能。

需要注意的一个行为变化是,当表需要增长或删除条目时,新实现将不会提供对散列中元素的稳定引用。依赖这种稳定性的应用程序现在可能会遇到未定义的行为。

移除 QHash::insertMulti

在 Qt 5 中,QHash可以通过使用 QHash::insertMulti 来创建多值散列,而QMultiHash是派生 vom QHash 的

在 Qt 6 中,类型和用例都是不同的,并且 QHash::insertMulti 被删除了。

QVector,QList

在 Qt 6 之前,QVectorQList是独立的类。在 Qt 6 中,它们是统一的:Qt 5 QList实现消失了,两个类都使用更新的 QVector实现。QList是具有实际实现的类,QVectorQList的别名(typedef) 。

QList的 fromVector() 和 toVector() 以及QVector的 fromList() 和 toList() 在 Qt 6 中不再涉及数据复制。它们现在返回调用它们的对象。

API 更改

QList的(以及QVector的)大小类型从 更改intqsizetype。连同大小类型,所有相关方法的签名都更新为使用qsizetype. 这允许QList在 64 位平台上保存超过 2^31 个项目。

在将代码库升级到 Qt 6 时,此 API 更改很可能会导致编译器警告关于缩小类型转换。具有以下示例代码:

1
2
3
4
5
6
7
void myFunction(QList<MyType> &data) {
int size = data.size();
// ...
const int pos = getInsertPosition(size);
data.insert(pos, MyType());
// ...
}

您需要更新它以使用其中一个qsizetype或一个 auto 关键字:

1
2
3
4
5
6
7
void myFunction(QList<MyType> &data) {
auto size = data.size();
// ...
const auto pos = getInsertPosition(size);
data.insert(pos, MyType());
// ...
}

或者,您可以使用类型转换并将所有内容转换为intqsizetype

注意:如果您想同时针对 Qt 5 和 Qt 6 进行构建,auto 关键字是一个很好的解决方案,可以覆盖版本之间的签名差异。

内存布局

QList收到了与 Qt 6 中的内存布局相关的多项更改。

在 Qt 5 中,sizeof(QList<T>)等于指针的大小。现在,额外的指针间接被删除,QList数据成员直接存储在对象中。默认情况下,期望sizeof(QList<T>)等于 3 个指针的大小。

同时,元素的内存布局也更新了。QList现在总是将其元素直接存储在分配的内存区域中,而不是 Qt 5,在 Qt 5 中,某些对象被单独分配在堆上,而指向对象的指针被放置到QList中。

请注意,后者尤其会影响大型对象。要具有 Qt 5 行为,您可以将对象包装成智能指针并将这些智能指针直接存储在QList中。在这种情况下,您的QList的类型将与 Qt 5 中的类型QList<MySmartPointer<MyLargeObject>>相反。QList<MyLargeObject>

参考文献的稳定性

对QVector / QList实现进行了一些更改。QVector相关的一个是:优化了开头的插入(类似于Qt 5中的QList)。QList相关的一个是:元素的内存布局被简化。

重要提示:这些更改会影响参考的稳定性。在 Qt 6 中,您应该考虑使用任何大小或容量修改方法来使所有引用无效,即使QList不是隐式共享的。此规则的例外情况已明确记录。

依赖某些参考稳定性的应用程序在升级到使用 Qt 6 时可能会遇到未定义的行为。您应该特别注意最初使用具有非 C 兼容数组布局的QVectorQList的情况。

2、在 Qt6 中查看类

总体概述

ViewQt6有几个新的类。已经存在QStringView,现在伴随着QByteArrayView,然后是专门的QUtf8StringView和更通用的QAnyStringView

QStringView示例上的视图类介绍

QStringView类通过QString API的只读子集提供统一的 UTF-16 字符串视图与保留自己的字符串副本(可能是引用计数)的QString不同, QStringView提供了存储在其他地方的字符串的视图。

1
2
3
4
5
6
7
8
char hello[]{ "Hello." };   // // 窄多字节字符串字面量
QString str{hello}; // 需要复制字符串文字
QString strToStr(str); // 原子增量涉及不再创建 hello 的副本

// 上面的代码可以重写以避免复制和原子增量.

QStringView view{ u"Hello." }; // 查看到 UTF-16 编码的字符串字面量
QStringView viewToView{ view }; // 查看相同的 UTF-16 编码字符串文字

字符串"Hello."存储在二进制文件中,在运行时不分配。view只是对 string 的一个视图"Hello.",因此不必创建副本。当我们复制QStringView时,viewToView观察到的字符串与复制源观察到的字符串相同view。这意味着viewToView不需要创建副本或原子增量。它们是现有字符串的视图"Hello."

作为函数参数的视图

视图应该按值传递,而不是通过对 const 的引用。

1
2
void myfun1(QStringView sv);        // 首选
void myfun2(const QStringView &sv); // 编译和工作,但速度较慢

视图操作函数

QStringView支持让我们操作字符串视图的函数。这允许我们在不创建已查看字符串的部分副本的情况下更改视图。

1
2
3
4
5
6
7
QString pineapple = "Pineapple";
QString pine = pineapple.left(4);

// 可以重写上面的代码以避免创建部分副本.

QStringView pineappleView{ pineapple };
QStringView pineView = pineappleView.left(4);

非空终止的字符串和包含的字符串 '\0'

QStringView支持空终止和非空终止字符串。不同之处在于您初始化QStringView的方式:

1
2
3
4
5
6
7
8
9
10
11
QChar aToE[]{ 'a', 'b', 'c', 'd', 'e' };

QStringView nonNull{ aToE, std::size(aToE) }; // 给定长度QStringView nonNull{aToE};
// 自动确定长度

QChar fToJ[]{ 'f', 'g', 'h', '\0', 'j' };

// 使用给定长度,不搜索 '\0',所以位置 3'\0'
// 被认为是字符串的一部分,类似于 'h''j'
QStringView nonNull{ fToJ, std::size(fToJ) };
QStringView part{ fToJ }; //在第一次遇到'\0'时停止

视图的所有权模型

由于views不拥有它们引用的内存,因此必须注意确保引用的数据(例如,由QString拥有)view在所有代码路径上的寿命都更长。

1
2
3
4
5
6
7
8
9
10
11
QStringView sayHello()
{
QString hello("Hello.");
return QStringView{ hello }; // hello 超出范围并被销毁
}

void main()
{
QStringView hello{ sayHello() };
qDebug() << hello; // 未定义的行为
}

将 QStringView 转换为 QString

QStringView不会隐式或显式转换为QString,但可以创建其数据的深层副本:

1
2
3
4
5
6
7
8
9
10
11
12
void print(const QString &s) { qDebug() << s; }

void main()
{
QStringView string{ u"string"};

// print(string); // 无效,没有隐式转换
// QString str{ string }; // 无效,没有显式转换

print(string.toString());
QString str = string.toString(); // 从view创建QString
}

重要笔记

通过利用新的视图类,可以在许多用例中实现大量性能提升。但是,重要的是要知道可能有一些警告。因此,重要的是要记住:

  • 视图应该按值传递,而不是通过对 const 的引用。
  • 构造具有负长度的视图是未定义的行为。
  • 必须注意确保引用的数据(例如,由QString拥有)比所有代码路径上的视图都长。

3、字符串相关类

QStringView 类

从 Qt6 开始,通常建议使用QStringView over QStringRefQStringView引用它不拥有的 UTF-16 字符串的连续部分。它充当各种 UTF-16 字符串的接口类型,无需先构造QString。QStringView类公开了几乎所有QString的只读方法以前存在的QStringRef类。

注意:必须注意确保引用的字符串数据(例如,由QString拥有)在所有代码路径上都超过QStringView 。

注意:如果QStringView包装了QString,则需要小心,因为与QStringRef QStringView不同,一旦QString数据重定位,将不会更新内部数据指针。

1
2
3
4
5
6
QString string = ...;
QStringView view{string};

// 附加很长的内容可能会导致重定位,并且
// 最终会导致 QStringView 出现乱码.
string += ...;

QStringRef 类

在 Qt6中, QStringRef已从 Qt Core 中删除。为了在不涉及整个代码库的情况下简化现有应用程序的移植,QStringRef该类并没有完全消失,而是被移到了 Qt5Compat 模块中。如果您想QStringRef进一步使用,请参阅使用 Qt5Compat 模块

不幸的是,QString公开的一些方法返回 a QStringRef,无法移动到 Qt5Compat。因此,可能需要一些手动移植。如果您的代码使用以下一个或多个函数,您需要将它们移植到使用QStringViewQStringTokenizer。对于性能关键代码,还建议使用QStringView::tokenize而不是QStringView::split 。

使用以下代码更改代码QStringRef

1
2
3
4
5
6
7
8
9
QString string = ...;
QStringRef left = string.leftRef(n);
QStringRef mid = string.midRef(n);
QStringRef right = string.rightRef(n);

QString value = ...;
const QVector<QStringRef> refs = string.splitRef(' ');
if (refs.contains(value))
return true;

到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
QString string = ...;
QStringView left = QStringView{string}.left(n);
QStringView mid = QStringView{string}.mid(n);
QStringView right = QStringView{string}.right(n);

QString value = ...;
const QList<QStringView> refs = QStringView{string}.split(u' ');
if (refs.contains(QStringView{value}))
return true;
// 或
const auto refs = QStringView{string}.tokenize(u' ');
for (auto ref : refs) {
if (ref == value)
return true;
}

4、QMutex 和相关类

在 Qt 6 中,QRecursiveMutex不再继承自QMutex。进行此更改是为了提高QMutexQRecursiveMutex的性能。

由于这些更改, QMutex::RecursionMode 枚举已被删除,并且QMutexLocker现在是一个模板类,可以在QMutexQRecursiveMutex上运行。

5、QFuture 及相关类

QFuture 类

为避免意外使用QFuture ,Qt 6 中对QFuture API进行了一些更改,这可能会导致源代码兼容性中断。

QFuture 和其他类型之间的隐式转换

QFuture<T>到的转换T已被禁用。转换操作符调用QFuture::result (),如果用户在尝试进行转换之前通过QFuture::takeResult () 从QFuture移动了结果,这可能会导致未定义的行为。在需要转换QFuture<T>T

QFuture<T>从to的隐式转换QFuture<void>也被禁用。如果您真的打算进行转换,请使用显式QFuture<void>(const QFuture<T> &)构造函数:

1
2
QFuture<int> future = ...
QFuture<void> voidFuture = QFuture<void>(future);

等式运算符

QFuture的相等运算符已被删除。他们正在比较底层的 d 指针,而不是比较结果,这可能不是用户所期望的。如果需要比较QFuture对象,使用QFuture::result()orQFuture::takeResult()方法。例如:

1
2
3
4
QFuture<int> future1 = ...;
QFuture<int> future2 = ...;
if (future1.result() == future2.result())
// ...

QFuture 和 QFutureWatcher 的行为变化

在 Qt 6 中,对QFutureQFutureWatcher进行了一些改进,导致以下行为变化:

  • 在暂停QFutureQFutureWatcher(通过调用pause()or setPaused(true))后,QFutureWatcher不会立即停止传递进度和结果就绪信号。在暂停的那一刻,可能仍有计算正在进行且无法停止。此类计算的信号可能在暂停后仍被传递,而不是被推迟并仅在下一次恢复后才报告。要在暂停实际生效时得到通知,可以使用QFutureWatcher::suspended () 信号。此外,还有新的isSuspending()isSuspended()方法,用于检查QFuture是处于挂起过程中还是已经处于挂起状态。请注意,出于一致性原因,对于两者QFutureQFutureWatcher与暂停相关的 API 已被弃用,取而代之的是名称中带有“suspend”的类似方法。
  • QFuture::waitForFinished () 现在会等到QFuture实际上处于完成状态,而不是在它不处于运行状态时立即退出。如果在调用它的那一刻未来还没有开始,这可以防止waitForFinished()立即退出。这同样适用于QFutureWatcher::waitForFinished ()。此更改不会影响将QFutureQtConcurrent一起使用的代码的行为。只有在未记录的情况下使用它的代码QFutureInterface可能会受到影响。
  • QFutureWatcher::isFinished () 现在反映 QFuture 的完成状态,而不是在QFutureWatcher::finished () 发出之前返回 false 。

QPromise 类

在 Qt 6 中,应该使用新的QPromise类而不是非官方的 QFutureInterface 作为QFuture的“setter”对应物。

6、IO 类

QProcess 类

在 Qt 6 中,通过将单个命令字符串拆分为程序名称和参数来解释单个命令字符串的QProcess::start () 重载重命名为QProcess::startCommand ()。但是,存在采用单个字符串的QProcess::start () 重载以及用于参数的QStringList 。由于QStringList参数默认为空列表,因此仅传递字符串的现有代码仍然可以编译,但如果它是包含参数的完整命令字符串,则将无法执行该过程。

Qt 5.15 为相应的重载引入了弃用警告,以便于发现和更新现有代码:

1
2
3
4
5
6
7
8
9
10
QProcess process;

// 在 5.15 中编译时出现警告,在 Qt 6
process.start("dir \"My Documents\"");

// 适用于 Qt 5 和 Qt 6; 另请参阅QProcess::splitCommand()
process.start("dir", QStringList({"My Documents"});

// 适用于 Qt 6
process.startCommand("dir \"My Documents\"");

QProcess::pid() 和 Q_PID 类型已被移除;使用QProcess::processId () 来获取本机进程标识符。PROCESS_INFORMATION不再支持使用本机 Win32 API 以 Win32 结构访问 Q_PID 中的数据的代码。

7、元类型系统

QVariant 类

QVariant已被重写以QMetaType用于其所有操作。这意味着一些方法的行为改变:

  • QVariant::isNull()现在仅true在 theQVariant为空或包含 a时返回nullptr。在 Qt 5 中,如果 qtbase 中的类isNull本身有一个方法返回 true,它也会返回 true。依赖于旧行为的代码需要检查包含的值是否返回 isNull ——然而这种代码在实践中不太可能出现,因为isNull()很少有人感兴趣的属性(比较QString::isEmpty()/isNull()QTime::isValid/ isNull)。
  • QVariant::operator==在 Qt 6 中使用QMetaType::equals。因此,某些图形类型,如QPixmap,QImage并且QIcon永远不会比较相等。此外,存储在中的浮点数QVariant不再与 进行比较qFuzzyCompare,而是使用精确比较。

此外,QVariant::operator<、QVariant::operator<=、QVariant::operator> 和 QVariant::operator>= 被删除,因为不同的变体并不总是可订购的。这也意味着QVariant不能再用作QMap中的键。

QMetaType 类

在 Qt 6 中,比较器以及QDebugQDataStream流操作符的注册是自动完成的。因此,、QMetaType::registerEqualsComparator()和不再存在。移植到 Qt 6 时,必须删除对这些方法的调用。QMetaType::registerComparators()`qRegisterMetaTypeStreamOperators()QMetaType::registerDebugStreamOperator()`

类型注册

中使用的类型Q_PROPERTY将其元类型存储在类’QMetaObject中。这要求当 moc 看到它们时类型是完整的,这可能导致在 Qt 5 中工作的代码中出现编译错误。有三种方法可以解决此问题:

  • 包括定义类型的标题。
  • 不要使用包含,而是使用Q_MOC_INCLUDE宏。如果包含标头会导致循环依赖,或者会减慢编译速度,这将很有帮助。
  • 如果头文件存在于实现类的 cpp 文件中,也可以在其中包含 moc 生成的文件。

8、正则表达式类

QRegularExpression 类

在 Qt6 中,所有采用 的方法都已QRegExp从我们的代码库中删除。因此,您很可能必须将您的应用程序或库移植到QRegularExpression

QRegularExpression实现了与 Perl 兼容的正则表达式。它完全支持Unicode。有关QRegularExpression支持的正则表达式语法的概述,请参阅前面提到的 pcrepattern(3) 手册页。一个正则表达式由两部分组成:一个模式字符串和一组改变模式字符串含义的模式选项。

QRegularExpression之间存在一些细微的差异QRegExp,本文档将对此进行解释以简化移植工作。

QRegularExpression在正则表达式的语法方面更加严格。因此检查表达式的有效性总是好的。

QRegularExpression几乎总是可以声明为 const (模式改变时除外),而QRegExp几乎不可能。

CaretMode枚举没有替代品。QRegularExpression ::AnchoredMatchOption匹配选项可用于模拟QRegExp::CaretAtOffset行为。其他QRegExp::CaretMode模式没有等价物。

QRegularExpression仅支持与 Perl 兼容的正则表达式。尽管如此,它仍然不支持 Perl 兼容的正则表达式中可用的所有功能。最值得注意的是不支持捕获组的重复名称,使用它们可能会导致未定义的行为。这可能会在未来的 Qt 版本中改变。

通配符匹配

QRegularExpression中没有直接的方法来进行通配符匹配。但是,提供了QRegularExpression::wildcardToRegularExpression方法来将 glob 模式转换为可用于该目的的 Perl 兼容的正则表达式。

例如,如果您有类似的代码

1
2
QRegExp wildcard("*.txt");
wildcard.setPatternSyntax(QRegExp::Wildcard);

您可以将其重写为

1
auto wildcard = QRegularExpression(QRegularExpression::wildcardToRegularExpression("*.txt"));

请注意,并非所有类似于通配符模式的 shell 都可能以您期望的方式进行翻译。如果简单地使用上述函数进行转换,以下示例代码将静默中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
const QString fp1("C:/Users/dummy/files/content.txt");
const QString fp2("/home/dummy/files/content.txt");

QRegExp re1("*/files/*");
re1.setPatternSyntax(QRegExp::Wildcard);
... = re1.exactMatch(fp1); // 返回true
... = re1.exactMatch(fp2); // 返回true

// 但是使用 QRegularExpression::wildcardToRegularExpression()

QRegularExpression re2(QRegularExpression::wildcardToRegularExpression("*/files/*"));
... = re2.match(fp1).hasMatch(); // returns false
... = re2.match(fp2).hasMatch(); // returns false

向前搜索

字符串内的前向搜索通常通过循环使用QRegExp::indexIn和不断增长的偏移量来实现,但现在可以使用QRegularExpressionMatchIteratorQString::indexOf轻松实现。

例如,如果您有类似的代码

1
2
3
4
5
6
7
8
QString subject("the quick fox");

int offset = 0;
QRegExp re("(\\w+)");
while ((offset = re.indexIn(subject, offset)) != -1) {
offset += re.matchedLength();
// ...
}

您可以将其重写为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QRegularExpression re("(\\w+)");
QString subject("the quick fox");

QRegularExpressionMatchIterator i = re.globalMatch(subject);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
// ...
}

// 或者使用 QString::indexOf

qsizetype from = 0;
QRegularExpressionMatch match;
while ((from = subject.indexOf(re, from, &match)) != -1) {
from += match.capturedLength();
// ...
}

向后搜索

在字符串中向后搜索通常是作为循环实现的QRegExp::lastIndexIn,但现在可以使用QString::lastIndexOfQRegularExpressionMatch轻松实现。

注意:QRegularExpressionMatchIterator不能执行向后搜索。

例如,如果您有类似的代码

1
2
3
4
5
6
7
8
int offset = -1;
QString subject("Lorem ipsum dolor sit amet, consetetur sadipscing.");

QRegExp re("\\s+([ids]\\w+)");
while ((offset = re.lastIndexIn(subject, offset)) != -1) {
--offset;
// ...
}

您可以将其重写为

1
2
3
4
5
6
7
8
9
qsizetype from = -1;
QString subject("Lorem ipsum dolor sit amet, consetetur sadipscing.");

QRegularExpressionMatch match;
QRegularExpression re("\\s+([ids]\\w+)");
while ((from = subject.lastIndexOf(re, from, &match)) != -1) {
--from;
// ...
}

exactMatch 与 match.hasMatch

QRegExp::exactMatch有两个目的:将正则表达式与主题字符串完全匹配,并实现部分匹配。精确匹配表示正则表达式是否匹配整个主题字符串。例如:

1
2
3
4
5
6
7
QString source("abc123");

QRegExp("\\d+").exactMatch(source); // 返回false
QRegExp("[a-z]+\\d+").exactMatch(source); // 返回true

QRegularExpression("\\d+").match(source).hasMatch(); // 返回true
QRegularExpression("[a-z]+\\d+").match(source).hasMatch(); // 返回true

精确匹配未反映在QRegularExpression中。如果要确保主题字符串与正则表达式完全匹配,可以使用QRegularExpression::anchoredPattern函数包装模式:

1
2
3
4
5
6
7
QString source("abc123");

QString pattern("\\d+");
QRegularExpression(pattern).match(source).hasMatch(); // 返回true

pattern = QRegularExpression::anchoredPattern(pattern);
QRegularExpression(pattern).match(source).hasMatch(); // 返回false

最小匹配

QRegExp::setMinimal()通过简单地反转量词的贪婪来实现最小匹配(QRegExp不支持惰性量词,如*?、+?等)。相反, QRegularExpression确实支持贪婪、懒惰和所有格量词。QRegularExpression ::InvertedGreedinessOption模式选项可用于模拟QRegExp::setMinimal(): 如果启用,它会反转量词的贪婪(贪婪的变得懒惰,反之亦然)。

不同的模式语法

将正则表达式从 QRegularExpression 移植QRegExp可能需要更改模式本身。因此建议检查与QRegularExpression::isValid方法一起使用的模式。这对于用户提供的模式或不受开发人员控制的模式尤其重要。

在其他情况下,从QRegularExpressionQRegExp移植的模式可能会默默地改变语义。因此,有必要审查所使用的模式。最值得注意的无声不兼容案例是:

  • \xHHHH为了使用超过 2 位的十六进制转义,需要大括号。\x2022需要将类似的模式移植到\x{2022},否则它将匹配(0x20)字符串后面的空格"22"\x通常,无论指定多少位数,强烈建议始终使用带有转义的花括号。
  • 需要移植类似的0-to-n量化以保留语义。否则,诸如这样的模式实际上会匹配一个数字后跟确切的字符串。{,n} {0,n} \d{,3} "{,3}"

部分匹配

使用 时QRegExp::exactMatch(),如果没有找到精确匹配,仍然可以通过调用QRegExp::matchedLength(). 如果返回的长度等于主题字符串的长度,则可以断定找到了部分匹配。QRegularExpression通过适当的QRegularExpression::MatchType显式支持部分匹配。

全局匹配

由于QRegExpAPI 的限制,不可能正确地实现全局匹配(也就是说,就像 Perl 一样)。特别是,可以匹配零个字符的模式(如“a*”)是有问题的。QRegularExpression::wildcardToRegularExpression正确实现 Perl 全局匹配,返回的迭代器可用于检查每个结果。

Unicode 属性支持

使用 时QRegExp,诸如 、 等字符类\w匹配\d具有相应 Unicode 属性的字符:例如,\d匹配具有 Unicode Nd(十进制数字)属性的任何字符。默认情况下,这些字符类仅匹配 ASCII 字符。使用QRegularExpression时:例如,\d精确匹配 0-9 ASCII 范围内的字符。可以通过使用QRegularExpression::UseUnicodePropertiesOption模式选项来更改此行为。

QRegExp 类

在 Qt6,QRegExp从 Qt Core 中移除。如果您的应用程序现在无法移植QRegExp,Qt5Compat 中仍然存在以保持这些代码库正常工作。如果您想QRegExp进一步使用,请参阅使用 Qt5Compat 模块

QEvent类定义了一个复制构造函数和一个赋值运算符,尽管它是一个多态类。在将不同类中的对象相互分配时,使用虚拟方法复制类可能会导致切片。由于复制和分配经常隐含地发生,这可能会导致难以调试的问题。

在 Qt 6 中,QEvent子类的复制构造函数和赋值运算符已被保护以防止隐式复制。如果您需要复制事件,请使用clone方法,该方法将返回QEvent对象的堆分配副本。确保删除克隆,可能使用 std::unique_ptr,除非您发布它(在这种情况下,Qt 将在交付后删除它)。

在您的QEvent子类中,覆盖 clone(),并声明受保护和默认实现的复制构造函数和赋值运算符,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyEvent : public QEvent
{
public:
// ...

MyEvent *clone() const override { return new MyEvent(*this); }

protected:
MyEvent(const MyEvent &other) = default;
MyEvent &operator=(const MyEvent &other) = default;
MyEvent(MyEvent &&) = delete;
MyEvent &operator=(MyEvent &&) = delete;
// 成员数据
};

请注意,如果您的 MyEvent 类分配内存(例如,通过指向实现模式的指针),那么您将必须实现自定义复制语义。

9、序列化类

在 Qt 6 中,用于将其转换为/从 Qt 的遗留 JSON 二进制格式转换的QJsonDocument方法被删除,以支持标准化的 CBOR 格式。Qt JSON 类型可以转换为 Qt CBOR 类型,后者又可以序列化为 CBOR 二进制格式,反之亦然。例如,参见QCborValue::fromJsonValue () 和QCborValue::toJsonValue ()。

如果仍然需要使用二进制 JSON 格式,可以使用 Qt5Compat 模块中提供的替换。它们可以在QBinaryJson命名空间中找到。请参阅使用 Qt5Compat 模块以了解如何在您的应用程序中使用该模块。

10、其他类

在 Qt 5 中,QCoreApplication::quit () 等价于调用QCoreApplication::exit ()。这刚刚退出了主事件循环。

在 Qt 6 中,该方法将尝试通过发布关闭事件来关闭所有顶级窗口。窗口可以通过忽略该事件来自由取消关闭过程。

调用QCoreApplication::exit () 来保持非条件行为。

由于命名不一致,不推荐使用 QLibraryInfo::location() 和 QLibraryInfo::Location。请改用新的 API QLibraryInfo::path () 和QLibraryInfo::LibraryPath

11、Qt 状态机框架

Qt 状态机被移入 Qt SCXML 模块(即将重命名为 Qt 状态机),因此它不再是 Qt 核心的一部分。Qt Core 内部很少有交叉依赖项,最终导致了这个决定。

12、使用 Qt5Compat 模块

要使用Qt5Compat模块,您需要在包含路径中使用它的头文件进行构建,并链接到它的库。如果您使用的是qmake,请将以下内容添加到您的.pro文件中:

1
QT += core5compat

如果您使用cmake构建应用程序或库,请将以下内容添加到您的CMakeList.txt:

1
2
PUBLIC_LIBRARIES
Qt::Core5Compat

七、从 Qt 5 移植

在开发 Qt 6 时,与 Qt 5 的源代码兼容性一直是我们工作的关键部分。有一些地方我们不得不在某种程度上打破这种兼容性,以进行一些必需的架构更改或为我们带来一些巨大的性能优势。

在大多数情况下,从 Qt 5 移植到 Qt 6 应该很简单。在Qt的6移植指南 列出了所需要的步骤,并具有更多的信息。您还可以从我们的合作伙伴之一或我们的顾问那里获得移植帮助。

移植到 Qt 6 的典型步骤是:

  • 检查您是否使用了受支持的编译器和平台版本
  • 首先在 Qt 6 模式下使用 Qt 5.15 编译(使用 QT_DISABLE_DEPRECATED_BEFORE 宏)
  • 然后用 Qt 6.x 编译 - 如果需要,在移植阶段利用兼容性模块

有了这些,您就可以在 Qt 6 上运行应用程序,并可以开始使用它提供的所有新特性和功能。例如,如果您的应用程序使用 QML,请运行 qmlint 工具并修复它给出的警告。

1、变化总括

1). 增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理。

2). 把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能。
3). 把一些Qt5中兼容Qt4的方法废弃了,必须用Qt5中对应的新的函数。
4). 跟随时代脚步,增加了不少新特性以满足日益增长的客户需求。
5). 对某些模块和类型及处理进行了革命性的重写,运行效率提高不少。
6). 有参数类型的变化,比如 long 到 qintptr 等,更加适应后续的拓展以及同时对32 64位不同系统的兼容。
7). 源码中的double数据类型全部换成了qreal,和Qt内部数据类型高度一致和统一。
8). 我测试的都是QWidget部分,quick部分没有测试,估计quick部分更新可能会更多。
9). 强烈建议暂时不要用Qt6.0到Qt6.2之间的版本,一些模块还缺失,相对来说BUG也比较多,推荐6.2版本开始正式迁移。

2、移植案例

新的 Qt 模块状态机

具有 QStateMachine、QState 和 QFinalState 等类的状态机框架已从模块 Core 移至新模块 StateMachine。我们StateMachineCMakeLists.txt文件中添加find_package调用和调用。Qt6::StateMachine`target_link_libraries`

Error: no match for ‘operator<’ (operand types are ‘const QVariant’ and ‘const QVariant’)

问题

1
2
QMap<std::pair<QString, QVariant>, QObject *> m_entities;
m_impl->m_entities.insert(it.key(), it.value());

该函数返回一个> 类型的对象,它与 . 的键类型相同。QMap 需要在键类型上定义才能知道在哪里插入新值。it.key()`std::pair<QString, QVariantQMapoperator<`

1
QMap`比较该对`p1`是否小于`p2`,两者都`p1`具有`p2`类型。如果等于,则比较必须计算。G++-9 正确地抱怨没有为s 定义。G++-7 忽略了 s的缺失。`std::pair<QString, QVariant>``p1.first``p2.first``p1.second < p2.second``operator<``QVariant``operator<``QVariant

修复

1
2
QMap<std::pair<QString, QString>, QObject *> m_entities;
m_impl->m_entities.insert(it.key(), it.value());

将 aQVariant转换为QStringwith适用于. 由于仅对其键的第二个元素使用支持的类型,我们可以安全地替换键类型中的by 。QVariant::toString()`QVariantm_entitiesQVariantQString`

Error: QTextCodec: No such file or directory

问题

1
2
3
4
#include <QTextCodec>
...
QTextStream is{&csvFile};
is.setCodec(QTextCodec::codecForName("ISO-8859-1"));

该类QTextCodec已从 Qt 6 中删除,并被新的类所取代QStringConverter。同样,类QTextEncoderandQTextDecoder被 and 取代QStringEncoderQStringDecoder参见这篇文章)。使用任何旧类的代码都无法编译。

修复

1
2
3
4
#include <QStringConverter>
...
QTextStream is{&csvFile};
is.setEncoding(QStringConverter::Latin1);

我们包括替换类的标题QStringConverter。搜索 的功能最QStringConvertersetEncoding可能替代setCodec. 了解 ISO-8859-1 是 Latin1 的正式名称有助于我们setEncoding使用正确的常量调用。QStringConverter::Latin1

Error: QRegExp: No such file or directory

问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <QRegExp>

// In anonymous namespace
QRegExp &rxHexFilename()
{
static QRegExp rx("_(A\\d\\d)_V_(\\d\\d)_(\\d\\d)\\.hex$");
return rx;
}

// In constructor
auto pos = rxHexFilename().indexIn(fileName);
auto ecuName = rxHexFilename().cap(1);
if (pos == -1 || ...) {
return;
}
m_version = QString("v%1.%2")
.arg(rxHexFilename().cap(2).toUInt())
.arg(rxHexFilename().cap(3).toUInt());
m_ecuType = fileName.left(fileName.size() -
rxHexFilename().matchedLength());

替换QRegExp——Qt 4 的遗物——QRegularExpression自 Qt 5.0 以来一直在酝酿之中。QRegularExpression是在 Qt 6 中使用正则表达式的唯一方法。界面发生了很大变化。

修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <QRegularExpression>

// In anonymous namespace
const QRegularExpression rxHexFilename("_(A\\d\\d)_V_(\\d\\d)_(\\d\\d)\\.hex$");

// In constructor
auto match = rxHexFilename.match(fileName);
auto ecuName = match.captured(1);
if (!match.hasMatch() || ...) {
return;
}
m_version = QString("v%1.%2")
.arg(match.captured(2).toUInt())
.arg(match.captured(3).toUInt());
m_ecuType = fileName.left(fileName.size() -
match.capturedLength(0));

将字符串与正则表达式匹配的函数有它的自然名称:(match而不是indexIn)。它返回一个QRegularExpressionMatch对象match而不是索引。我们通过调用来检索nth捕获的子字符串。match.hasMatch() 告诉我们字符串是否匹配正则表达式。match.captured(nth)

1
QRegExp::matchedLength`返回匹配完整正则表达式的子字符串的长度。此子字符串与第 0 个捕获的子字符串相同,其长度为。`match.captured(0)``match.capturedLength(0)

Error: ‘class QString’ has no member named ‘midRef

问题

1
subDev = m_deviceName.midRef(splitPos + 1).toLatin1();

该函数在 Qt 6 中已过时。该函数也是如此。QString::midRef`QString::leftRef`

修复

1
subDev = m_deviceName.mid(splitPos + 1).toLatin1();

我们将和分别替换为和。QString::midRef`QString::leftRefQString::midQString::left`

Error: cannot convert ‘QString’ to ‘const QFileInfo&’

问题

1
2
3
4
QString extractVersion(const QFileInfo &info) const;

QString fileName(...);
auto version = extractVersion(fileName);

在 Qt 5 中,该函数使用构造函数extractVersion将 隐式转换QString filenameQFileInfo对象。在 Qt 6 中,这个构造函数被标记为,它阻止了隐式转换。QFileInfo(const QString &)`explicit`

修复

1
2
3
4
QString extractVersion(const QFileInfo &info) const;

QString fileName(...);
auto version = extractVersion(QFileInfo(fileName));

我们通过显式调用构造函数来消除错误。QFileInfo(const QString &)

Error: invalid application of ‘sizeof’ to incomplete type ‘IntegerRangeModel’

问题

1
2
3
4
5
6
class IntegerRangeModel;

class DateTimeModel : public QObject
{
Q_OBJECT
Q_PROPERTY(IntegerRangeModel* days READ days CONSTANT)

的前向声明对于Qt 5IntegerRangeModel中的定义来说已经足够好了,因为它被声明为一个指针。Qt 6 添加了一个静态断言。另一条错误消息记录了对静态断言的违反:Q_PROPERTY`days`

1
static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");

修复

1
2
3
4
5
6
#include "IntegerRangeModel.h";

class DateTimeModel : public QObject
{
Q_OBJECT
Q_PROPERTY(IntegerRangeModel* days READ days CONSTANT)

我们通过包含头文件IntegerRangeModel而不是前向声明来消除错误消息IntegerRangeModel

Warning: ‘Type’ is deprecated: Use QMetaType::Type instead. [-Wdeprecated-declarations]

问题

1
EntityColumn(const QString &name, QVariant::Type type, ...)

枚举列出了可以存储在. 它大致是 enumeration 的一个子集,它列出了 Qt 元对象系统已知的类型。建议自己替换。不再包含在 Qt 6 中。QVariant::Type`QVariantQMetaType::TypeQVariant::TypeQMetaType::TypeQVariant::Type`

修复

1
EntityColumn(const QString &name, QMetaType::Type type, ...)

在 Qt 6 中,我们使用而不是. 我们还必须分别替换过时的枚举常量,例如,和,和。QMetaType::Type`QVariant::TypeQVariant::InvalidQVariant::UIntQVariant::BoolQMetaType::UnknownTypeQMetaType::UIntQMetaType::Bool`

Error: cannot convert ‘QVariant::Type’ to ‘QMetaType::Type’

问题

1
2
3
4
bool typesAreAffine(QMetaType::Type sqlType, ...) const;

QSqlField tableColumn = ...;
if (!typesAreAffine(tableColumn.type(), ...)) ...

最初, 的第一个参数typesAreAffine具有 type ,它被前面的迁移步骤替换。具有返回类型的函数在 Qt 6 中不存在。QVariant::Type`QMetaType::TypeQSqlField::typeQVariant::Type`

修复

1
2
3
4
bool typesAreAffine(QMetaType::Type sqlType, ...) const;

QSqlField tableColumn = ...;
if (!typesAreAffine(QMetaType::Type(tableColumn.metaType().id()), ...)) ...

我们用 Qt 6 函数替换现在已过时的 Qt 5 函数。返回一个对象。我们使用 检索对象的类型 ID ,它返回一个而不是 a来为自定义元类型打开。从整数创建一个枚举常量。QSqlField::metaType()`QSqlField::type()QSqlField::metaType()QMetaTypeQMetaTypeid()intQMetaType::TypeQMetaType::Type(typeID)typeID`

Error: warning: conversion from ‘qsizetype’ {aka ‘long long int’} to ‘int’ may change value [-Wconversion]

问题

1
2
3
4
5
6
7
QList<QFileInfo> m_dirEntries.

int DirectoryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_impl->m_dirEntries.size();
}

从 Qt 6.0 开始,具有返回类型,相当于. DirectoryModel 类间接派生自,它像这样声明纯虚函数:QList<T>::size()`qsizetypelong long intQAbstractItemModelrowCount`

1
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0

return语句可以将一个值缩小为一个值,这可能会改变该值。似乎 Qt 开发人员忘记了将from的返回类型更改为.long long int`introwCountintqsizetype`

类似beginInsertRows,beginRemoveRowsbeginMoveRows的函数beginInsertColumns也接受int-type 参数。我们可能还需要注意缩小转化率。

修复

1
2
3
4
5
6
7
QList<QFileInfo> m_dirEntries.

int DirectoryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return static_cast<int>(m_impl->m_dirEntries.size());
}

有时没有办法绕过静态演员表。这是其中之一。我们将值强制qsizetype转换为 intvalue并消除错误。

Warning: conversion from ‘size_t’ {aka ‘long unsigned int’} to ‘uint’ {aka ‘unsigned int’} may change value [-Wconversion]

问题:

1
2
3
4
5
6
7
enum class UnitId : quint16 { ... }

inline uint qHash(UnitId unit)
{
return qHash(static_cast<quint16>(unit));
}
qHash``uint`函数在 Qt 5 中返回值`uint`,其中. 在 Qt 6 中,它们返回值,其中是. 语句中的函数是一个内置的散列函数。它返回一个值,该值可以通过自定义散列函数缩小为一个值。由于这可能会更改值,因此编译器会发出警告。`unsigned int``size_t``size_t``long unsigned int``qHash``return``long unsigned int``unsigned int

修复:

1
2
3
4
5
6
enum class UnitId : quint16 { ... }

inline size_t qHash(UnitId unit)
{
return qHash(static_cast<quint16>(unit));
}

我们将自定义散列函数的返回类型从uint更改为size_t

Warning: ‘static QQmlFileSelector QQmlFileSelector::get(QQmlEngine)’ is deprecated [-Wdeprecated-declarations]

问题

1
2
auto fs = QQmlFileSelector::get(engine);
fs->setExtraSelectors({"left"});

静态函数返回 QML 上当前活动的文件选择器。它在 Qt 6 中已过时,因为它复制了构造函数提供的功能。QQmlFileSelector::get`engineQQmlFileSelector`

修复

1
2
auto fs = new QQmlFileSelector(engine);
fs->setExtraSelectors({"left"});

在堆上创建QQmlFileSelector对象与调用. 我们必须在堆上创建对象,因为构造函数参数拥有文件选择器的所有权。QQmlFileSelector::get`engine`

Error: using typedef-name ‘using QStringList = class QList’ after ‘class

问题

1
class QStringList;

包含的 Qt 头文件介绍了类型定义。有问题的行向前声明了编译器不喜欢的这种类型定义。using QStringList = class QList<QString>

修复

1
#include <QStringList>

我们通过包含头文件来替换前向声明。

Error: ‘QLatin1Literal’ was not declared in this scope; did you mean ‘QStringLiteral’?

问题

1
2
3
QStringList errors;
Q_ASSERT_X(false, __PRETTY_FUNCTION__,
errors.join(QLatin1Literal("; ")).toUtf8());

QLatin1Literal Qt 6 中不再存在。

修复

1
2
3
QStringList errors;
Q_ASSERT_X(false, __PRETTY_FUNCTION__,
errors.join(QLatin1String("; ")).toUtf8());

我们替换QLatin1LiteralQLatin1String

Error: No type name ‘State’ in QMediaPlayer

问题:

1
2
3
4
5
6
void onStateChanged(QMediaPlayer::State state);

connect(&m_impl->m_player, &QMediaPlayer::stateChanged,
m_impl, &Impl::onStateChanged);

if (m_player.state() != QMediaPlayer::PlayingState) { ... }

我们用信号替换并连接插槽。我们还需要替换对by 的调用。QMediaPlayer::State`QMediaPlayer::PlaybackStateonStateChangedQMediaPlayer::playbackStateChangedQMediaPlayer::state()QMediaPlayer::playbackState()`

修复:

1
2
3
4
5
6
void onStateChanged(QMediaPlayer::PlaybackState state);

connect(&m_impl->m_player, &QMediaPlayer::playbackStateChanged,
m_impl, &Impl::onStateChanged);

if (m_player.playbackState() != QMediaPlayer::PlayingState) { ... }

Error: ‘QMediaPlaylist’: No such file or directory

问题:

1
#include <QMediaPlaylist>

Changes to Qt Multimedia页面通知我们该类QMediaPlaylist已从 Qt 6 中删除。当显示信息、警告或错误对话框时,应用程序会播放不同的 MP3 文件。它将QMediaPlaylist带有正确 MP3 文件的 a 传递给QMediaPlayer. 用户还可以更改播放音量。

修复:

1
QMediaPlaylist`替换为。音量不能直接用 改变,但可以用 间接改变。以下有关 QMediaPlayer 和 QMediaPlaylist 的错误消息告诉我们要修复什么。`QMediaPlayer::setSource``QMediaPlayer::setVolume``QAudioOutput::setVolume

Error: No member named ‘setVolume’ in ‘QMediaPlayer’

问题:

1
m_player.setVolume(80);

Qt 6 将函数setVolumevolumeQMediaPlayer移至QAudioOutput.

修复:

使用的客户端类必须在其构造函数中向该对象QMediaPlayer注册一个QAudioOutput对象。我们必须告诉对象它应该在哪个扬声器上播放 MP3 文件。我们可以使用 访问默认扬声器。这对于只有一个扬声器的终端硬件来说已经足够了。QMediaPlayer`m_playerQAudioOutputQMediaDevices::defaultAudioOutput`

客户端类将音量设置为默认值(例如,0.8)。请注意,新的采用 0.0 到 1.0 之间的浮点值作为参数,而旧的采用 0 到 100 之间的整数。QAudioOutput::setVolume`QMediaPlayer::setVolume`

1
2
3
4
auto audioOut = new QAudioOutput{};
audioOut->setDevice(QMediaDevices::defaultAudioOutput());
audioOut->setVolume(0.8f);
m_player.setAudioOutput(audioOut);

客户端类的函数改变音量如下:setVolume(int vol)

1
m_player.audioOutput()->setVolume(0.8f);

Error: QMediaPlayer cannot play back a QMediaPlaylist any more

问题:

当然,标题错误信息并不是真正的编译器错误。由于 QMediaPlaylist 从 Qt 6 中删除,以下错误类别的播放功能不再编译。

1
2
3
4
5
6
void AudioService::Impl::playFeedback(int category)
{
m_playlist.addMedia(m_audioFiles[category]);
m_player.play();
}
m_audioFiles[category]`返回给定错误的 MP3 文件的 QRC URL `category`。例如,它返回类别 2。`QUrl("qrc:/Can/Audio/ding.mp3")

修复:

为给定播放 MP3 文件的修改函数category如下所示。

1
2
3
4
5
void AudioService::Impl::playFeedback(int category)
{
m_player.setSource(m_audioFiles[category]);
m_player.play();
}

QML 警告、错误和改进

Remove versions from import statements

问题:

1
2
3
import QtQuick 2.15
import QtQuick.Controls 2.15
import Ag.Models 1.0

Qt 5 需要导入模块的版本。Qt 6 不会抱怨这些版本,但它不再需要它们。

使固定:

1
2
3
import QtQuick
import QtQuick.Controls
import Ag.Models

在 Qt 6 中只导入一个没有版本的模块就足够了。我们从所有导入语句中删除了版本。

警告:未声明参数“page”。不推荐将参数注入信号处理程序。

问题:

1
2
3
4
5
6
7
8
9
// File: RDirectBar.qml
Pane
{
signal selectedPage(url page, bool fullscreen)

// File: main.qml
RDirectBar
{
onSelectedPage: { ... }

信号在 的实例化中selectedPage连接到信号处理程序。Qt 6 接受带有警告的旧式信号处理程序。onSelectedPage`RDirectBar`

使固定:

新方法是使用箭头函数或匿名函数(参见Qt 6.2 文档中的信号参数)。我们在 C++ 中称它们为 lambda 函数。

3、移植总结

1). 万能方法:安装5.15版本,定位到报错的函数,切换到源码头文件,可以看到对应提示字样 QT_DEPRECATED_X(“Use sizeInBytes”) 和新函数。按照这个提示类修改就没错,一些函数是从Qt5.7 5.9 5.10等版本新增加的,可能你的项目还用的Qt4的方法,但是Qt6以前都兼容这些旧方法,到了Qt6就彻底需要用新方法了。
2). Qt6对core这个核心类进行了拆分,多出来core5compat,因此你需要在pro增加对应的模块已经代码中引入对应的头文件。

1
2
3
4
5
6
7
8
9
10
11
//pro文件引入模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
greaterThan(QT_MAJOR_VERSION, 5): QT += core5compat

//代码中引入头文件
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
#include <QtWidgets>
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
#include <QtCore5Compat>
#endif

(1). 默认Qt6开启了高分屏支持,界面会变得很大,甚至字体发虚,很多人会不习惯,因为这种模式如果程序很多坐标计算没有采用devicePixelRatio进行运算的话,100%会出现奇奇怪怪的问题,因为坐标不准确了。要取消这种效果可以设置高分屏缩放因子。

1
2
3
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Floor);
#endif

(1). 原有的随机数函数提示用QRandomGenerator替代,为了兼容所有qt版本,改动最小的办法是直接用c++中的随机数,比如qsrand函数换成srand,qrand函数换成rand,查看过源代码,其实封装的就是c++中的随机数,很多类似的封装比如qSin封装的sin。
(2). QColor的 light 改成 lighter ,dark 改成 darker,其实 lighter、darker 这两个方法以前一直有。
(3). QFontMetricsF 中的 fm.width 换成 fm.horizontalAdvance ,从5.11开始用新函数。
(4). QPalette调色板枚举值,Foreground = WindowText, Background = Window,其中 Foreground 和 Background 没有了,要用 WindowText 和 Window 替代,以前就有。类似的还有 setTextColor 改成了 setForeground 。
(5). QWheelEvent的 delta() 改成 angleDelta().y(),pos() 改成 position() 。
(6). svg模块拆分出来了svgwidgets,如果用到了该模块则需要在pro增加 QT += svgwidgets 。
(7). qlayout中的 margin() 函数换成 contentsMargins().left(),查看源码得知以前的 margin() 返回的就是 contentsMargins().left(),在四个数值一样的时候,默认四个数值就是一样。类似的还有setMargin移除了,统统用setContentsMargins。
(8). 之前 QChar c = 0xf105 全部要改成强制转换 QChar c = (QChar)0xf105,不再有隐式转换,不然编译报错提示error: conversion from ‘int’ to ‘QChar’ is ambiguous 。
(9). qSort等一些函数用回c++的 std::sort 。

1
2
3
4
5
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
std::sort(ipv4s.begin(), ipv4s.end());
#else
qSort(ipv4s);
#endif

(1). Qt::WA_NoBackground 改成 Qt::WA_OpaquePaintEvent 。
(2). QMatrix 类废弃了没有了,换成 QTransform ,函数功能基本一致,QTransform 类在Qt4就一直有。
(3). QTime 计时去掉了,需要改成 QElapsedTimer ,QElapsedTimer 类在Qt4就一直有。
(4). QApplication::desktop()废弃了, 换成了 QApplication::primaryScreen()。

1
2
3
4
5
6
7
8
9
#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
#include "qscreen.h"
#define deskGeometry qApp->primaryScreen()->geometry()
#define deskGeometry2 qApp->primaryScreen()->availableGeometry()
#else
#include "qdesktopwidget.h"
#define deskGeometry qApp->desktop()->geometry()
#define deskGeometry2 qApp->desktop()->availableGeometry()
#endif

(1). 获取当前屏幕索引以及尺寸需要分别处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//获取当前屏幕索引
int QUIHelper::getScreenIndex()
{
//需要对多个屏幕进行处理
int screenIndex = 0;
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
int screenCount = qApp->screens().count();
#else
int screenCount = qApp->desktop()->screenCount();
#endif

if (screenCount > 1) {
//找到当前鼠标所在屏幕
QPoint pos = QCursor::pos();
for (int i = 0; i < screenCount; ++i) {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
if (qApp->screens().at(i)->geometry().contains(pos)) {
#else
if (qApp->desktop()->screenGeometry(i).contains(pos)) {
#endif
screenIndex = i;
break;
}
}
}
return screenIndex;
}

//获取当前屏幕尺寸区域
QRect QUIHelper::getScreenRect(bool available)
{
QRect rect;
int screenIndex = QUIHelper::getScreenIndex();
if (available) {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
rect = qApp->screens().at(screenIndex)->availableGeometry();
#else
rect = qApp->desktop()->availableGeometry(screenIndex);
#endif
} else {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
rect = qApp->screens().at(screenIndex)->geometry();
#else
rect = qApp->desktop()->screenGeometry(screenIndex);
#endif
}
return rect;
}

(1). QRegExp类移到了core5compat模块,需要主动引入头文件 #include 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    //设置限制只能输入数字+小数位
QString pattern = "^-?[0-9]+([.]{1}[0-9]+){0,1}$";
//设置IP地址校验过滤
QString pattern = "(2[0-5]{2}|2[0-4][0-9]|1?[0-9]{1,2})";

//确切的说 QRegularExpression QRegularExpressionValidator 从5.0 5.1开始就有
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QRegularExpression regExp(pattern);
QRegularExpressionValidator *validator = new QRegularExpressionValidator(regExp, this);
#else
QRegExp regExp(pattern);
QRegExpValidator *validator = new QRegExpValidator(regExp, this);
#endif
lineEdit->setValidator(validator);

(1). QWheelEvent构造参数和对应的计算方位函数变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//模拟鼠标滚轮
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
QWheelEvent wheelEvent(QPoint(0, 0), -scal, Qt::LeftButton, Qt::NoModifier);
#else
QWheelEvent wheelEvent(QPointF(0, 0), QPointF(0, 0), QPoint(0, 0), QPoint(0, -scal), Qt::LeftButton, Qt::NoModifier, Qt::ScrollBegin, false);
#endif
QApplication::sendEvent(widget, &wheelEvent);

//鼠标滚轮直接修改值
QWheelEvent *whellEvent = (QWheelEvent *)event;
//滚动的角度,*8就是鼠标滚动的距离
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
int degrees = whellEvent->delta() / 8;
#else
int degrees = whellEvent->angleDelta().x() / 8;
#endif
//滚动的步数,*15就是鼠标滚动的角度
int steps = degrees / 15;

(1). qVariantValue 改成 qvariant_cast ,qVariantSetValue(v, value) 改成了 v.setValue(val)。相当于退回到最原始的方法,查看qVariantValue源码封装的就是qvariant_cast。
(2). QStyleOption的init改成了initFrom。
(3). QVariant::Type 换成了 QMetaType::Type ,本身以前的 QVariant::Type 封装的就是 QMetaType::Type 。
(4). QStyleOptionViewItemV2 V3 V4 之类的全部没有了,暂时可以用 QStyleOptionViewItem 替代。
(5). QFont的 resolve 的一个重载函数换成了 resolveMask。
(6). QSettings的 setIniCodec 方法移除了,默认就是utf8,不需要设置。
(7). qcombobox 的 activated(QString) 和 currentIndexChanged(QString) 信号删除了,用int索引参数的那个,然后自己通过索引获取值。个人觉得这个没必要删除。
(8). qtscript模块彻底没有了,尽管从Qt5时代的后期版本就提示为废弃模块,一致坚持到Qt6才正式废弃,各种json数据解析全部换成qjson类解析。
(9). QByteArray 的 append indexOf lastIndexOf 等众多方法的QString参数重载函数废弃了,要直接传 QByteArray,就在原来参数基础上加上 .toUtf8() 。查看源码也看得到以前的QString参数也是转成.toUtf8()再去比较。
(10). QDateTime的时间转换函数 toTime_t + setTime_t 名字改了,对应改成了 toSecsSinceEpoch + setSecsSinceEpoch ,这两个方法在Qt5.8时候新增加的。
(11). QLabel的 pixmap 函数之前是指针 *pixmap() 现在换成了引用 pixmap()。
(12). QTableWidget的 sortByColumn 方法移除了默认升序的方法,必须要填入第二个参数表示升序还是降序。
(13). qtnetwork中的错误信号error换成了errorOccurred。

1
2
3
4
5
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
connect(tcpSocket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(error()));
#else
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error()));
#endif

(1). XmlPatterns模块木有了,全部用xml模块重新解析。
(2). nativeEvent的参数类型变了。

1
2
3
4
5
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result);
#else
bool nativeEvent(const QByteArray &eventType, void *message, long *result);
#endif

(1). QButtonGroup的buttonClicked信号中int参数的函数全部改名字叫idClicked。

1
2
3
4
5
6
    QButtonGroup *btnGroup = new QButtonGroup(this);
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
connect(btnGroup, SIGNAL(idClicked(int)), ui->xstackWidget, SLOT(setCurrentIndex(int)));
#else
connect(btnGroup, SIGNAL(buttonClicked(int)), ui->xstackWidget, SLOT(setCurrentIndex(int)));
#endif

(1). QWebEngineSettings之前是QWebEngineSettings::defaultSettings();现在改成了QWebEngineProfile::defaultProfile()->settings();通过查看之前的源码得知QWebEngineSettings::defaultSettings();封装的就是QWebEngineProfile::defaultProfile()->settings();因为Qt6去除了N多过度封装的函数。

1
2
3
4
5
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QWebEngineSettings *webSetting = QWebEngineProfile::defaultProfile()->settings();
#else
QWebEngineSettings *webSetting = QWebEngineSettings::defaultSettings();
#endif