Портируем Qt4 приложение на Qt5

в 14:00, , рубрики: c++, porting, programming, qt, Qt Software, qt5, Программирование, метки: , , ,

Где-то здесь не так давно был пост о нововведених в Qt5. Всё вроде выглядит замечательно, но как же обстоят дела на самом деле в отношении уже имеющихся приложений? В этой статье я рассмотрю пример портирования одного из своих проектов на Qt5 с сохранением совместимости исходников с Qt4.

Итак, как и следовало ожидать мое GUI приложение не собирается. И не собирается оно потому, что я использую стандартные Qt виджеты. После недолгих разбирательств выясняется, что виджеты сейчас в отдельном модуле и должны быть явно включены. Сделаем это совместимым с Qt4 способом — добавим следующий код в наш файл qmake проекта (*.pro|*.pri):

greaterThan(QT_MAJOR_VERSION, 4) {
  QT += widgets
  DEFINES += HAVE_QT5
}

Также с этого момента благодаря добавленноу дефайну можно в любом месте своего кода вставить #ifdef HAVE_QT5 и далее написать код специфичный для Qt5. Того же эффекта с ifdef можно было добиться создав precompiled header с содержимым:

#define HAVE_QT5 (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))

И опять мой проект не собирается. На этот раз причина в отсутствующих дефайнах Q_WS_*. С Q_WS_WIN и Q_WS_MAC вроде бы всё предельно ясно, просто меняем их на Q_OS_WIN и Q_OS_MAC соответственно. Но что же делать с Q_WS_X11? Довольно часто Q_WS_X11 используется как просто признак юникс-подобных систем за исключением Mac OS (X), но и бывает так, что под этим дефайном скрывается код специфичный для X11. Но в моем случае это в общем-то одно и тоже, так что можно просто заменить на какой-либо другой дефайн (просто переопределить Q_WS_X11 мы не можем, так как некоторые части Qt всё ещё используют это определение) и объявить его в положенном месте, в файле проекта например:

greaterThan(QT_MAJOR_VERSION, 4):unix:!mac:DEFINES += HAVE_X11

Идем дальше. Проект у меня довольно старый и в нем используется множество устаревших классов или методов классов или чего угодно, что уже выброшено или запрещено в Qt5. Сборка опять не идет. Часть из того, что устарело, можно найти в документации по Qt5. Как правило в той же документации сказано, что надо использовать взамен, хотя есть и исключения, которые я рассмотрю ниже. Если же в документации по Qt5 не найдено устарвших классов и их методов, то вероятно это остатки Qt3 и их можно найти в документации на Qt4. Из того на что я наткнулся стоит отметить: QIconSet, QMenuItem (qt3. меняем на QIcon и QAction соответственно), QAbstractItemModel::reset (используем beginResetModel/endResetModel), QKeySequence больше не кастуется в int (делаем цикл по элементам последовательности), часть QUrl перекочевала в новый класс QUrlQuery, Qt::escape удален (свой inline враппер над Qt::escape и QString::toHtmlEscaped), qInstallMsgHandler(qInstallMessageHandler)…

Далее QPA. Всё бы ничего, но с появлением этой штуки из Qt исчез весьма полезный класс QX11Info, и в документации по этому поводу не сказано ничего вменяемого. Исчез он потому, что был жестко завязан на Xlib. Вместо него в Qt5 появился QPlatformNativeInterface, который, впрочем, вскоре был запрещен и сейчас иммеется только в виде приватного класса/заголовка. В своем проекте, как быстрое решение всё ещё завязанное на Xlib, я просто написал враппер над QX11Info, который в случае Qt5 имеет свою собственную реализаию:
x11info.h

#ifndef X11INFO_H
#define X11INFO_H

typedef struct _XDisplay Display;

class X11Info
{
	static Display *_display;
public:
	static Display* display();
	static unsigned long appRootWindow(int screen = -1);
};

#endif // X11INFO_H

x11info.cpp

#include "x11info.h"

#ifdef HAVE_QT5
# include <X11/Xlib.h>
# include <QtGlobal>
#else
# include <QX11Info>
#endif


Display* X11Info::display()
{
#ifdef HAVE_QT5
	if (!_display) {
		_display = XOpenDisplay(NULL);
	}
	return _display;
#else
	return QX11Info::display();
#endif
}

unsigned long X11Info::appRootWindow(int screen)
{
#ifdef HAVE_QT5
	return screen == -1?
				XDefaultRootWindow(display()) :
				XRootWindowOfScreen(XScreenOfDisplay(display(), screen));
#else
	return QX11Info::appRootWindow(screen);
#endif
}

Display* X11Info::_display = 0;

Не ручаюсь за логическую верность реализации, так как до конца написания статьи код ещё не был протестирован.
Также у меня имеются нативные обработчики событий, такие как QApplication::x11EventFilter например. В Qt5 конечно же их пришлось переписать. Для этого нам понадобится QAbstractNativeEventFilter и немного знаний программирования xcb (Xlib здесь не сработает так как QPA о нем ничего не знает). В Принципе, переход на xcb не слишком сложен ввиду схожести API этих двух библиотек, но мануалами запастись не помешает. В моем случае реализация была довольно тривиальна: в своем классе приложения рядом с x11EventFilter добавил ещё один метод под названием xcbEventFilter и несколько ifdefов для компиляции только нужного метода. Далее создал класс унаследованный от QAbstractNativeEventFilter и из него просто обработку всех xcb событий перенаправил на наш метод:

#ifdef HAVE_X11
# ifdef HAVE_QT5
class XcbEventFiter : public QAbstractNativeEventFilter
{
	MyApplication *app;

public:
	XcbEventFiter(MyApplication *app) : app(app) {}

	virtual bool nativeEventFilter(const QByteArray &eventType, void *message, long *) Q_DECL_OVERRIDE
	{
		if (eventType == "xcb_generic_event_t") {
			return app->xcbEventFilter(message);
		}
		return false;
	}
};
# endif
// ....
#endif

Если душе угодно инициализацию приложением можно заменить кастом сингелтона приложения в nativeEventFilter.

Далее плагины. В Qt5 они загружаются другим, несовместимым способом и объявлены должны быть тоже по-другому. Сделана такая неприятная поломка в целях оптимизации (сейчас необязательно грузить весь плагин как полноценную разделяемую библиотеку, что бы убедиться что это вообще Qt плагин и что бы получить из него какую-либо мета информацию, такую как, например, версия программы, с которой данный плагин совместим. подробнее здесь), но чинить тем не менее её всё равно надо.
Для начала, все макросы экспорта наших плагинов делаем условными примерно так:

#ifndef HAVE_QT5
Q_EXPORT_PLUGIN2(myplugin, MyPlugin);
#endif

Также в классе плагина условно добавляем новый специфичный для Qt5 макрос Q_PLUGIN_METADATA где-нибудь рядом с Q_INTERFACES но после Q_OBJECT:

#ifdef HAVE_QT5
	Q_PLUGIN_METADATA(IID "tld.domain.Project.MyPluginInterface" FILE "myplugin.json")
#endif

Часть FILE «myplugin.json» необходима только если нам на самом деле нужны метаданные в плагине, а что касается интерфейса «tld.domain.Project.MyPluginInterface», то это тот же интерфейс, что и в Q_DECLARE_INTERFACE. В моем случае в метаданных будут храниться: версия плагина, минимальная версия самой программы и приоритет загрузки. Также надо не забыть добавить магию с объявлением HAVE_QT5 в файлы проектов плагинов, ну или, как быстрый вариант без магии, использовать #if QT_VERSION >= 0x050000.
В случае же статических плагинов, придется поменять вызов Q_IMPORT_PLUGIN макросов. В качестве параметра они сейчас принимают имя класса плагина, а не то, что было первым параметром в Q_EXPORT_PLUGIN2.

Итак, запуск! И, как следовало ождать, segfault. Мой код ожидает, что QMetaType::Void == 0, но в Qt5 это не так. Отлично, исправляем, запускаем и снова segfault. На этот раз проблема в том, что некоторые типы объявлены в одном месте, а Q_DECLARE_METATYPE для них — в другом. Из-за чего последний, даже при явно включенном хидере с типом, работает неправильно. Я не стал разбираться в чем именно загвоздка, просто перенес Q_DECLARE_METATYPE для типов в их заголовочные файлы. И снова запуск — работает!

Программа запустилась, но работы ещё много. Переход на xcb таки должен быть полноценным, т.е. мой класс X11Info должен быть переписать с использованием xcb. Также надо проверить на работоспособность всё, что было пропатчено, впрочем, как и не пропатченное. Но, я надеюсь, самое сложное уже позади!

Надеюсь мой опыт будет вам полезен. Ниже приведу несколько ссылок, которые мне помогли в решение Qt5 головоломок:

www.kdab.com/porting-from-qt-4-to-qt-5/
xcb.freedesktop.org/tutorial/
qt-project.org/doc/qt-5.0/qtwidgets/tools-plugandpaint.html
google.com

Автор: rionekb

Источник

Поделиться

* - обязательные к заполнению поля