Back to Blog home

Qt 4迁移至Qt 5

Published on 星期一 七月 09, 2012 by Cheng Liang in Qt Qt 5 qtearth-blogs-chinese | Comments

原文地址:KDAB: Stephen Kelly - 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所需要注意的问题。

KAddressbook迁移到Qt 5和KDE Frameworks 5

KDE PIM是最后一个完整迁移到Qt 4和kdelibs 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进行绘制。

使用了QML1的KDE PIM的Contacts Touch

结论

前面所说的从Qt 4迁移到Qt 5的步骤不是要求严格遵守的。在Qt 5正式发布之后,应该会有一个更加完整的列表。

另外需要注意的是,这篇文章关注的是(另人厌烦的)那些使用Qt 5构建应用程序所需要的步骤。然而关于升级时出现的运行时bug只字未提,而这才是迁移时真正花费时间的。目前一些KDE应用程序出现了一些微妙的bug急需处理:

Dolphin About页面出现编码问题

Konqueror菜单有小bug

变更日志中列出的边界条件的更改暗示了那些在Qt 5中有所不同的部分可能会在迁移代码中引发bug。

Subscribe to Our Blog

Stay up to date with the latest marketing, sales and service tips and news.