Анатомия Qt5 для Android

Вольный перевод Anatomy of a Qt 5 for Android application.

Предпосылки

Это выходит за рамки блога, чтобы объяснить, как работает слой абстракции Qt(Qt Platform Abstraction (QPA) ) . Кроме того, за рамками остается  подробное введение в разработку и сопровождение приложений в Android.  Она должна быть понятной и полезной без глубоких знаний технологий, если хотите знать больше, есть документация и блоги в интернете.

Достаточно сказать: Qt абстрагирует от оконной системы(windowing system) в API под названием «QPA«, так что зависимый от платформы код может быть выделен в плагин. Этот плагин будет управлять всеми обработчиками, от отображения, до передачи сообщений от оконной системы в цикл обработки событий Qt.  Android является одной из таких платформ, но во многом отличается от других платформ, поддерживаемых Qt, так как он по своей сути является Java-платформой. Android приложения это Java приложений, работающие под виртуальной машине «Dalvik». Это создает дополнительные проблемы при интеграции с C++  фреймворком  Qt.

Что приводит нас к «Java Native Interface» (или JNI для краткости). Это слой коммуникации между Java и C, и используется для передачи данных туда и обратно между операционной системой и платформой плагинов Qt. В Qt, мы работаем над некоторыми удобным API, вокруг JNI API, чтобы помочь вам объединить Qt код с кодом JNI, если вы хотите, чтобы ваш код взаимодействовал с Java.

В разрезе

На самом верхнем уровне, Qt для Android состоит из двух частей:

  • Qt приложение: Это кросс-платформенный код и ресурсы, которые вы, как разработчик приложения, управляете сами, и которые приведены в вашем qmake.pro файле
  • Пусковое приложение под Android, которое генерирует Qt Creator для вас, в при первом подключению к проекту.

Последнее состоит из:

  • Подкласса android.app.Application, что поддерживает привязку к Qt средствами Java’s Reflection API;
  • Подкласс android.app.Activity : Это точка входа в приложение.  Android, приложение может состоять из нескольких activitys, отвечая на различные intents. Однако по умолчанию приложений Qt будет состоять только из одного activity, которое может быть запущено из сетки приложений Android.  Несколько событий системы распространяется на activity вашего приложения, и распространяются по подклассам Qt, QtActivity класс также проводит загрузку родные файлы на основе выбранного метода развертывания и запуска приложения вызывая функцию main ();
  • Интерфейс, для подключения к службе Ministro: Ministro является механизмом развертывания, где Qt библиотеки загружаются и поддерживается внешней службой на целевом устройстве, и служит для уменьшения объема используемого пространства на каждое приложение Qt. Интерфейс используются для связи со службой в случае, если был выбран этот механизм развертывания
  • AndroidManifest.xml: Это jоснова мета-данных в Android приложениях. В какой-то момент вам придется изменить его, чтобы установить такие вещи, как имя вашего приложения, имя пакета, версию кода, значок, разрешений и т.д.
  • Другие мета-данные: Есть набор дополнительных файлов, используемых для хранения дополнительной информации о приложении. Это, например, информация о выбранном механизме развертывания в Qt Creator, макет для отображения заставки, перевод текста интерфейса Ministro и т.д.

Когда Qt Creator настраивает проект на работу с Qt под Android, он будет копировать эти файлы из каталога $QT/src/Android/Java . Он будет вносить изменения в файлы на основе параметров настройки развертывания вашей,  версии Android и т.д.  При разработке обычного приложения Qt, вам не придется изменять все это самостоятельно, за исключением AndroidManifest.xml и даже то, что может подождать, пока вы на самом деле не захотите развернуть приложения пользователям или в маркете. В этот момент вы, вероятно, захотите установить некоторые специализированные данные, такие как имя и значок.

Последняя часть пазла состоящая из:

  • QtActivityDelegate.java и других java файлов. Это позволит создать пользовательский интерфейс для приложений (только один SurfaceView для отрисовки  Qt), и заботиться о взаимодействия Android и QPA. Когда приложения получает activity события от операционной системы, они будут вызывать функции QtActivityDelegate и они будут передаваться  в Qt.
  • Платформа плагинов. Есть две платформы плагинов в Qt для Android, которые рассчитаны на двух различных вариантах использования. Первым из них является растровой плагин, который используется для QtWidget -приложений, которые не зависят от OpenGL.  Это имитирует некоторые поведения в традиционной оконной системы рабочего стола.  Другой на GL основе, и использоваться, например, для Qt Quick 2 приложения, которые зависят от OpenGL ES 2.

Запуск

Когда запускается Qt приложение, приложение будет просто обычным Java приложение. Точка входа будет в QtActivity.java которую можно найти в  android/src/ … в каталоге проекта. Этот код сначала проверит ваш проект мета-данных, который содержится в android/AndroidManifest.xml  и android/res/values/libs.xm , чтобы увидеть, какой механизм развертывания был выбран. Qt Creator будет обновлять мета-данных в соответствии с настройками проекта.  Для более глубокое представление о различных значениях, вы можете попробовать выбрать различные механизмы развертывания в Qt Creator и запустить приложение, затем просмотреть мета-данные, чтобы увидеть, что изменилось.

Существуют три различных механизмов развертывания, каждый из которых имеет несколько иной код запуска

  • Связка Qt библиотек в APK. При запуске, приложение скопирует некоторые из включенных в набор файлов в кэш;
  • Использовать сервис Ministro;
  • Развертывание  локальных Qt библиотек  во временный каталог.

Как только подготовка закончена, приложение будет первыми явно загружать Qt (и друге) библиотек, перечисленных в android/RES/values/libs.xml в указанном порядке. Когда это будет сделано, он загрузит платформу плагинов, который служит как плагин QPA и прослойку между Qt и Java. Этот плагин зарегистрирует набор собственных обратных вызовов, которые вызываются из Java в качестве реакции на Android событий, и это будет регистрацией интерфейсов QPA. Как только это будет сделано, то приложение будет загружать последнюю библиотеку: это ваше приложение.

UI в Destkop & Tablet pc. Адаптируемся к размеру и разрешению экрана.

В одном из проектов среди требований по UI было:

  • полноэкранный режим;
  • работа на обычных компьютерах;
  • работа на Tablet PC.

Используемые Tablet PC:

Сперва Samsung не тянул по производительности, но после существенной оптимизации удалось добиться необходимой производительности, и Samsung в одно части стал приоритетным.

Все ничего только приведу характеристики(диагональ, разрешение):

  • x230t 12,5 » 1366×768;
  • ATIV 11,6″ 1920×1080.

Если размер и разрешения x230t  де факто стандарт для ноутбуков, и требовалось лишь учесть HD мониторы с соотношением сторон 4:3, то с ATIV пришлось выкручиваться.

Первое от чего следует предостеречь, так это от фиксированных размеров элементов, т.к. при смене DPI все надписи вылезут из заданных границ.

Выходом стало получение размера монитора и его разрешения, и в зависимости от их значений выбираем нужный QSS если проект на Widgets, а в QtQuick я использовал State Machine что бы не создавать нескольких QML версток.

В коде выразилось наличием синглетона DisplayInfoSingleton со всей необходимой информацией.

#ifndef DISPLAYINFO_H_
#define DISPLAYINFO_H_

#include

#include <QtCore/QObject>

namespace GUI
{
	/*!
	 * @brief интерфейс информации о мониторе
	 */
	class IDisplayInfo
	{
	public:

		virtual ~IDisplayInfo ( )
		{
		}

		/*!
		 * @brief получить размер диагонали
		 * @return размер диагонали в дюймах
		 */
		virtual float diagonal ( ) const = 0;

		/*!
		 * @brief разрешение по горизонтали
		 */
		virtual int horizontalResolution ( ) const = 0;

		/*!
		 * @brief разрешение по вертикали
		 */
		virtual int verticalResolution ( ) const = 0;

	};

	/*!
	 * @brief реализация платформо независимых методов IDisplayInfo
	 */
	class AbstractDisplayInfo : public IDisplayInfo
	{
	public:
		virtual int horizontalResolution ( ) const;
		virtual int verticalResolution ( ) const;
	};

	/*!
	 * @brief синглетон содержащий конкретный объект IDisplayInfo
	 */
	class DisplayInfoSingleton
	{
	public:

		/*! миллиметров в дюйме */
		static const float MILLIMETR_2_INCH;

		/*! ширина формата Full HD */
		static const int FULL_HD_WIDTH = 1920;

		/*! высота формата Full HD */
		static const int FULL_HD_HEIGHT = 1080;

		/*!
		 * @brief инстанцирование синглетона
		 * @return ссылка на конкретного объекта DisplaInfo
		 */
		static IDisplayInfo& instance ( );

	protected:

		DisplayInfoSingleton ( );

	private:

		Q_DISABLE_COPY(DisplayInfoSingleton)

		/*!
		 * @brief создание конкретного объекта DisplayInfo
		 * @return платформозависимый объект класса DisplayInfo
		 */
		static IDisplayInfo* createObject ( );

		static std::unique_ptr m_info;  //!< конкретный DisplayInfo
	};

} /* namespace GUI */
#endif /* DISPLAYINFO_H_ */

 

#include

#include <QtGui/QApplication>
#include <QtGui/QDesktopWidget>

#include "DisplayInfo.h"

namespace GUI
{
	std::unique_ptr DisplayInfoSingleton::m_info = std::unique_ptr(DisplayInfoSingleton::createObject ());
	const float DisplayInfoSingleton::MILLIMETR_2_INCH = 0.0393700787;

	int AbstractDisplayInfo::horizontalResolution ()  const
	{
		return QApplication::desktop()->screenGeometry().width();
	}
	int AbstractDisplayInfo::verticalResolution ()  const
	{
		return QApplication::desktop()->screenGeometry().height();
	}

	IDisplayInfo& DisplayInfoSingleton::instance ( )
	{
		return *m_info;
	}
} /* namespace GUI */

 

#include

#include <QtCore/QDebug>

#include "DisplayInfo.h"

#include <X11/Xlib.h>

namespace
{
	using namespace GUI;

	/*!
	 * @brief Unix реализация информации дисплея
	 */
	class DisplayInfoUnix : public AbstractDisplayInfo
	{
	public:

		DisplayInfoUnix ( ) :
						m_display (0)
		{
			m_display = XOpenDisplay (NULL);
			if (m_display == NULL) {
				qDebug () << "Error XOpenDisplay";
			}
		}

		~DisplayInfoUnix ( )
		{
			if (m_display) {
				XCloseDisplay (m_display);
			}
		}

		float diagonal ( ) const
		{
			if (m_display) {
				const int horzSizeMM = DisplayWidthMM (m_display, 0);
				const int vertSizeMM = DisplayHeightMM (m_display, 0);

				return sqrt (pow (horzSizeMM, 2) + pow (vertSizeMM, 2)) * DisplayInfoSingleton::MILLIMETR_2_INCH;
			}

			static const int diagonalSize = 19;
			return diagonalSize;
		}

	private:

		Q_DISABLE_COPY(DisplayInfoUnix)

		Display* m_display;  //!< дисплей
	};
}

namespace GUI
{
	IDisplayInfo* DisplayInfoSingleton::createObject ( )
	{
		return new DisplayInfoUnix ();
	}
}

 

#include

#include <QtCore/qt_windows.h>

#include "DisplayInfo.h"

namespace
{
	using namespace GUI;

	/*!
	 * @brief Win реализация информации дисплея
	 */
	class DisplayInfoWin : public AbstractDisplayInfo
	{
	public:

		DisplayInfoWin ( )
		{
			m_screen = GetDC (NULL);
		}

		~DisplayInfoWin ( )
		{
			ReleaseDC (NULL, m_screen);
		}

		float diagonal ( ) const
		{
			const int horzSizeMM = GetDeviceCaps (m_screen, HORZSIZE);
			const int vertSizeMM = GetDeviceCaps (m_screen, VERTSIZE);

			return sqrt (pow (horzSizeMM, 2) + pow (vertSizeMM, 2)) * DisplayInfoSingleton::MILLIMETR_2_INCH;
		}

	private:

		Q_DISABLE_COPY(DisplayInfoWin)

		HDC m_screen;  //!< дескриптор экрана
	};

}

namespace GUI
{
	IDisplayInfo* DisplayInfoSingleton::createObject ( )
	{
		return new DisplayInfoWin ();
	}
}

 

SET(SOURCES DisplayInfo.cpp
#...
)
set(DEPEND_LIB
#...
)

IF(WIN32)
    SET(SOURCES
        ${SOURCES}
        DisplayInfoWin.cpp
     )
    SET (DEPEND_LIB
         ${DEPEND_LIB}
         Gdi32
     )
ELSE(WIN32)
    SET(SOURCES
        ${SOURCES}
        DisplayInfoUnix.cpp
    )
    SET (DEPEND_LIB
         ${DEPEND_LIB}
         XLib
     )
ENDIF(WIN32)

Мой подход к организации проекта QtQuick & C++ в Qt 5.1 for Android.

Продолжаю рассматривать программирование на Qt 5.1 под Android. Сейчас опишу свой доход к составлению и введению проекта.

Обычно для Desktop приложений использую CMake в качестве системы сборки своих приложений. Вместе с Qt 5 поставляется новый набор CMake скриптов, которые довольно сильно отличаются от  CMake модуля FindQt4 поставляемым вместе с CMake. Для этого потребуется настроить cmake на работу с toolchain что идет с Andoid NDK, благо есть проект Android-cmake облегчающий этот процесс.

Все хорошо, только в Andoid нельзя запускать бинарник. Там необходима Java  прослойка, так же нужна прослойка, для организации цикла обмена событий от системы в наше Qt приложение. Кому интересно может посмотреть видео Targeting Android with Qt

В общем простого способа использовать CMake, для разработки не нашел. Пришлось использовать скрипты qmake.

Поигравшись с различными шаблонами привожу проект на базе шаблона QtQuick 2. Окно выбора типового шаблона, обратите внимание на выделенные части


selectTemplate

Дальше идет выбор комплекта. Здесь я включил Destkop т.к. большую часть могу тестировать на дескотопе, а потом уже запускать на эмуляторе или девайсе, сделано это для экономия време


selectToolchain
ни, т.к. загрузка пакета требует времени.

Что мы имеем?

  1. код viewer’а  QML с предварительными настройками;
  2. привычный main.cpp создающий viewer и передающий путь к qml;
  3. main.qml код на qml.

template

При смене комплекта для Android появляется катало android со следующим содержимым:

  1. res каталог описания ресурсов, с описание Menistro и включаемых либ;
  2. src исходники Java прослойки. Jar файлы можно увидеть к примеру в Qt5.1.0\5.1.0\android_armv7\jar;
  3. манифест и версия, подробнее смотрите материалы по разработке приложений по андройд.

androidFolder

Но это еще не все! Каталог android используется при создание apk пакета. Создание пакета происходит во время запуска(Ctr+R), после чего пакет устанавливается на устройство или эмулятор и выполняется запуск удаленного процесса.

Во время запуска в каталоге android появятся подкаталоги: assest, bin, gen, libs.

На заметку!!!  в assest положиться весь наш каталог qml.

Моя организация проекта:

  1. components каталог сходных кодов кастомных компонентов на C++;
  2. registryTypes.cpp вынесена вся регистрация кастомных компонентов;
  3. Components кастомные компоненты на QML, для повторного использования;
  4. Интерфейс представляю в виде набора страниц, которая в свою очередь может состоять из множества специфических для данной страницы qml компонентов, все это храниться в каталоге с названием xxxPage
  5. mai.qml компоновка страниц и налаживание возможных связей между ними;
  6. constats.js перечисление констант таких как: общий цвет фона, размеры главного окна и т.д.

myProject

Первое приложение Qt 5.1 под Android. Секундомер.

3 июля увидел свет Qt 5.1, в котором появилась поддержка Android в качестве preview. Полноценную поддержку обещают в 5.2, но ничто не мешает пощупать и начать что-то делать уже сейчас.

У меня давно была идея, создать своё приложение, для записи, хранения, и отображения результатов своих тренировок в спортзале.

Мои требования к секундомеру:

  • задавать время отдыха между подходами
  • отображать прогресс
  • отображение времени

Вот что в итоге получилось:
Секундомер: итог

Рассмотрим по подробней назначение элементов:

  1. контрол задающий время отдыха(вращается по часовой стрелке);
  2. прогресс пройденного времени;
  3. отображение времени;
  4. общее отображение времени

trainingBookTimerItems

За не знанием канваса(canvas) прогресс представляет из себя кастомный объект на C++.

Все исходные коды доступны на Google Code