原文地址:http://www.kdab.com/porting-from-qt-4-to-qt-5/
将 Qt 4 代码迁移到 Qt 5 还是比较简单的。实际上,在 Qt 5 开发过程中就已经注意了与 Qt 4 代码保持兼容性。
与 Qt 3 到 Qt 4 的迁移不同,Qt 5 的核心类库并没有作大的 API 的修改,几乎也没有新的类取代了旧的(例如,像 Qt 4 的QList
取代了QPtrList
和 QValueList
;itemview 取代了 Q3ListView;graphicsview 取代了 Canvas API);同时也没有那些编译通过了,但运行时的行为却与之前的不一致的(例如,QWidget::show()
现在是非虚函数,绘制应该在 paintEvent 中进行等等)。
但是,迁移的代价也不会是零。本文总结了 KDE 部分代码从 Qt 4 迁移到 Qt 5 所需要注意的问题。
迁移之前
迁移策略中应该有这么一条:能够同时使用新版本和旧版本的 Qt 编译代码,也就是保持 Qt 4 和 Qt 5 的兼容性。这么做的好处是,能够保证你的代码在最小化的库上可以通过编译,让你的代码在 Qt 4 依然可用;也能够保证在迁移过程中,单元测试代码能够顺利运行;最后,还能够很快地区别出,哪些是本来就有的 bug,哪些是由于迁移到 Qt 5 新引入的 bug。
迁移 Qt3Support
迁移代码,可以从让当前 Qt 4 代码“现代化”开始。
从代码迁移角度来看,Qt 5 的有意义的改变是,移除了 Qt3Support 模块,移除了所有标记为 Qt3Support 的 API。在大多数情况下,Qt3Support 的代码在 Qt 4 中有一个更适合的名字。有的函数直接改名,例如QWidget::setShown
改为QWidget::setVisible
。部分 KDE 代码仍然使用了旧的函数,这种情况也发生在其他古老的第三方代码库中。
从 Qt 4 迁移到 Qt 5,移除代码中的 Qt3Support API 是必要的、不可避免的。虽然理论上,我们也可以为 Qt 5 单独编译 Qt3Support 模块。
修正 include
相对于 Qt 4,Qt 5 的一个主要的基础架构修改是,将 widget 从 QtGui 模块剥离开来,放到了全新的 QtWidgets 模块。这显然需要改变构建系统,同时也要求新引入一些原本不需要单独引入的头文件,因为这些头文件可能从现有 QtGui 模块中删除了。举个例子,Qt 5 中我们需要添加#include <QDrag>
,这在之前的 Qt 4 的代码是不需要的。这是因为,在 Qt 4 中,它已经被引入到 gui/kernel/qevent.h 头文件,但是 Qt 5 则没有。
另外一个有关 include 的修正是,你必须将之前的 QtGui 模块的头文件改成 QtWidgets,例如,
#include <QtGui/QWidget>
在 Qt 5 中应该写成
#include <QtWidgets/QWidget>
为了避免更多的修改,我的建议是,使用下面这种更具可移植性的写法(这种写法在 Qt 4 和 Qt 5 中同样适用):
#include <QWidget>
我们可以编写一个简单的脚本来执行这个枯燥的操作。当然,你也可以利用 IDE 提供的批量替换功能。就像清理 Qt3Support 一样,修正 include 的工作也应该在真正的迁移之前完成。
修正平台相关的定义
许多 Qt 和 KDE 程序都会有特定平台的代码。预处理器需要使用特定的宏,而在 Qt 5 中,所有的Q_WS_*
都变成了Q_OS_*
。例如,在 Qt 4 中的代码
#ifdef Q_WS_WIN // call windows API #endif
在 Qt 5 中应该写成
#ifdef Q_OS_WIN // call windows API #endif
Qt 5 移除了Q_WS_*
宏,所以所有包含了这些宏的代码都不会通过编译。这些代码(例如,特定操作系统,而不是特定窗口系统的代码)所包围的宏都应该改成Q_OS_*
。
丢失Q_OBJECT
宏以及清理 metatype
Qt 4 中很容易忘记在需要的地方添加Q_OBJECT
宏,这会导致某些不可预见的运行时 bug。这些在 Qt 5 中也是类似的,但是如果你通过使用Q_DECLARE_METATYPE
宏,将一个 QObject 子类的指针保存到 QVariant
时,你会得到一个编译错误(Qt 4 中也会有编译错误,但 Qt 4 的错误与 Qt 5 不同)。这是因为,QVariant
现在需要保存QObject
子类的指针(确切的子类,强类型指针),这是 QtDeclarative、语言绑定以及严重依赖 QMetaObject 内省 API 的程序所需要的新的特性。
另外一个影响是,Q_DECLARE_METATYPE
宏的参数必须是完整定义,而不能是前向声明。因此,下面的代码是不能通过编译的:
class MyType; Q_DECLARE_METATYPE(MyType);
这个宏现在必须放到MyType
完整定义的地方去(例如定义它的头文件)。另外,如果MyType
继承自QObject
,这个宏就可以完全删除。
重构
Qt 5 最大的变化之一是更加注重 QML(一个运行时解释语言,用于创建用户界面)和 QtQuick(语言相关的 API)。尽管 QtWidgets 依然可用,迁移到 QML 可能获得更好的性能和用户交互特性。
QML 是运行时解释型的,不像 C++ 那样具有类型安全的限制,它适合于结合使用QObject
子类表达的数据模型,这种数据的属性以及其他类型信息都可以由QVariant
包含。
如果迁移到 Qt 5 的目的之一是增加 QML 的使用量,那么,你就应该注意重构已有代码,让业务逻辑和数据模型(也就是应用程序的状态表示以及数据内容)分离。这种重构可以基于 Qt 4 的代码。我们甚至可以基于 Qt 4 提供一个正常工作的或者是试验用的 QML 移植,来验证我们的概念。这是 Qt 4 就提供了 QML 的原因之一,我们可以把它看成是 Qt5Support。
移除 QWS
QWS 系统不再是 Qt 5 的一部分,它的 API 也已经被移除。使用了这些 API 的代码应该移植到新的 QPA 系统,这是 Qt 5 的核心部分之一。QPA 实际上在 Qt 4.8 就已经引入(Qt 5 的 API 可能有些许不同)。
所以说,现在也可以将代码直接迁移到 Qt 4 的 QPA,当然,以后我们还得再迁移到 Qt 5,但是整体思路并不会发生重大改变。关键在于,现在没有什么有关 Qt 4.8 的 QPA 文档,只能比较 Qt 5 的文档做相应的处理。
迁移
如果你已经按照前面的步骤来到了这一步,那么就意味着你的代码可以完全兼容 Qt 4,也可以把眼光放到 Qt 5 上。一些 API 在 Qt 5 中不是源代码兼容的,这些大部分在修改文件中可以找到。大多数情况下,对于“通常的”代码,这些都不是问题,因为这些修改的部分很少用到,或者是仅在边缘条件下有所改变。
不管如何,这些改变都需要在迁移中进行处理。它们构成了 Qt 4 和 Qt 5 实际的区别,它们会强迫你放弃对 Qt 4 的支持,或者是使用 #ifdef 预处理宏来兼容其它版本。
插件加载
另外一个迁移问题是,在大量使用插件的系统中,插件部分的用户代码需要改变。Qt 5 中,moc 用于负责生产插件元数据,所以,不同于 Qt 4 中仅仅需要在 C++ 文件中添加一个预处理宏Q_EXPORT_PLUGIN2
,Qt 5 需要在头文件中添加一个新的宏,以便 moc 能够处理。
这个过程还是比较直观的。但是,问题在于,如果 Q_EXPORT_PLUGIN2
宏被包裹在另外一个宏中,类似 KDE 的K_EXPORT_PLUGIN
,那么这种处理就必须修改,因为 Qt 5 的 moc 不能处理这种宏的嵌套(moc 不做完整的预处理)。
迁移单元测试
前面我们提到,有些代码不是源代码兼容的,这其中就包括 QTestLib 模块——对于 QSKIP 宏的改变。Qt 4 中,这个宏有两个参数,但是在 Qt 5 则只有一个。
这造成了一个明显的迁移问题。KDE 对此的解决方案是,创建一个包装宏,接受两个参数,而对于 Qt 5 则舍弃一个。这应该是在未来需要放弃的,因为我们的程序不应该在将来还对 Qt 4 作一定的支持。也许这个“未来”会相当的遥远。
另外一个解决方案是,使用 C99 和 C++ 11。如果你使用了-std=c++11
, 进行编译,那么就不会有这个问题。
QMetaMethod::signature
在某些严重依赖于QMetaObject
内省系统的程序中,经常会使用QMetaMethod::signature
API。在 Qt 4,这个函数返回const char *
。而在 Qt 5,其返回值是动态构建的,所以需要进行一定的修改(因为不能返回局部变量的指针)。现在,等价的函数有一个不同的返回值(QByteArray
),以及不同的名字(QMetaMethod::methodSignature
)。仅仅改变函数名,会有运行时错误:
// 旧的 Qt 4 代码 // 返回的 const char * 赋值给本地变量,然后直接使用 const char *name = mm.signature(); otherApi(name); // 新的 Qt 5 代码 const char *name = mm.methodSignature(); // name 会成为野指针! // methodSignature() 返回的是 QByteArray,在函数返回时已经析构 otherApi(name); // 等着程序崩溃吧!
这个运行时 bug 可以通过在构建时添加QT_NO_CAST_FROM_BYTEARRAY
来避免。这应该是在 Qt 4 就应该启用的。这个函数名的改变以及返回值的变化应该在迁移工作的一个独立步骤中修正。
改变的 virtual 函数
Qt 5 中,少量 virtual 函数有了变化。这不会在迁移时构成编译错误(除非纯虚函数的改变),但是会在运行时引发异常(因为重写的函数不能被执行)。其中一个是,QAbstractItemView::dataChanged
信号有了一个参数。
我们有许多方法来解决这个问题。新的 C++ 11 标准有一个语法,能够指示一个类的特定函数是不是对父类同名函数的一个覆盖。使用这个语法,我们可以在迁移时使其成为编译错误。编译错误总是要比运行时错误好,因为它们更容易找到(事实上,它们是不能回避的)。
另外一种方法是,开启编译器警告。GCC 可以对子类的函数隐藏了父类的虚函数提出警告(-Woverloaded-virtual)。我们可以在构建之前开启这个特性,这样就可以更加明显地找到问题。另外,始终采用最严格的编译器警告级别当然是值得推荐的做法,因此你应该将这个警告添加到你的构建系统的参数中。
迁移之后
所有的程序都是要维护的,而不仅仅是编写或者迁移完这么简单。当你把你的程序迁移到 Qt 5 之后,还有更多的步骤值得你去做。
我们不应该在同一个分支同时提供对 Qt 4 和 Qt 5 的支持。在结束了整个迁移之后,单元测试也能够全部通过,这就意味着,我们可以开始两个分支:基于 Qt 4 的和基于 Qt 5 的。原因之一就是能够使用那些在 Qt 5 废止了的 Qt 4 的 API。
移除标记为废弃的函数
标记为 deprecated 的函数在 Qt 5 中还是存在的,但是可能会在未来消失。这种情况是 Qt 5 为了与 Qt 4 抱持兼容而保留的,就像 Qt 4 保留了 Qt 3 的支持一样。
我们应该尽快移除 deprecated 函数,因为它们会在编译时发出警告,这会与其它有用的警告混杂在一起,而新的 API 可能更快,可能用起来更方便。
迁移到 QML
将已有的 UI 迁移到 QML 是可选的步骤,可以在迁移之前完成,也可以在迁移之后进行。
Qt 4 提供的 QML 版本,在 Qt 5 中依然适用,名字为 QtQuick1。不过,这仅供迁移用,以后也不会进行性能提升等。使用了这些的代码应该再迁移到 QML2(以及 Qt 5 中的 QtQuick API),以获得更好的性能。迁移到 QML2 更像是使用另外名字的 C++ API,同时要修改绘制自定义组件的方法。因为 QML2 使用的是 scene-graph 机制,而不是 QPainter API(正是由于这个原因,才会导致性能的提升),自定义组件需要使用不同的 更新 API 进行绘制。
结论
前面所说的从 Qt 4 迁移到 Qt 5 的步骤不是要求严格遵守的。在 Qt 5 正式发布之后,应该会有一个更加完整的列表。
注意,这篇文章关注的是迁移到 Qt 5 时会遇到的构建问题,没有关于运行于 bug 的解决。后者才是真正会花费时间的。
1 个评论
无意中逛到这里来,觉得楼主的文章写的不错。正在学习Qt,多交流。