Qt 5.3: низкий старт в мобильной кроссплатформе

В этой главе мы разберёмся, как связать элементы из QML-файла с C++ бэкендом, как обращаться к функциям и объектам в QML из C++ и наоборот, как делать GET/POST-запросы к серверу и парсить JSON-ответ.Все действия будем выполнять с локальным сервером-заглушкой, описывать установку/конфигурацию/запуск оного здесь не буду, по теме очень много материалов в Сети.

В папке с сайтами локального сервера создадим новую папку «qt_api», внутри неё создаём файл «test.php». Содержимое файла:

Простенький JSON, будем делать запрос к «test.php» (http://localhost/qt_api/test.php) с отправкой полей и парсить выдаваемый ответ.

Теперь создаём новый C++ класс в Qt Creator: правой кнопкой по «Sample» в дереве → «Добавить новый…» → «C++» / «Класс C++». Прописываем имя класса «Backend», как базовый класс выбираем «QQuickItem», остальное не трогаем: image

Жмём «Продолжить», а в следующем окне «Готово». В проекте открываем заголовочный файл «backend.h». Изменяем следующим образом:

#ifndef BACKEND_H #define BACKEND_H

#include #include #include #include

class Backend: public QQuickItem {

Q_OBJECT

public: explicit Backend (QQuickItem *parent = 0);

Q_INVOKABLE void makeRequest (int id);

private: QQmlApplicationEngine engine; QObject * mainWindow; QObject * lvList; QObject * btnRequest; QNetworkAccessManager * namRequest;

static const QString color_example;

signals:

private slots: void slotRequestFinished (QNetworkReply*);

};

#endif // BACKEND_H Сначала подключаем все необходимые для работы библиотеки.

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

Теперь объявляем функцию makeRequest. Чтобы иметь возможность вызывать её внутри QML-файла, добавляем макрос Q_INVOKABLE. На вход функция будет принимать некоторый id (просто заглушка).

Объявляем закрытыми (private) необходимые объекты. QQmlApplicationEngine загружает основной QML-файл. Три QObject будут нужны для доступа к соответствующим элементам в QML. QNetworkAccessManager используется для выполнения запросов к серверу (что-то вроде DefaultHttpClient в Android). Также создаём константу цвета, которую потом будем использовать в QML.

Теперь о сигналах и слотах. Есть много объяснений сущности в сети, постараюсь объяснить по-своему. Сигналы/слоты — интересная фича в Qt, отчасти напоминающая, например, колбэки. В заголовочном файле мы можем описать сигнал следующим образом:

void mySignal (); Описанный сигнал можно вызывать из кода:

emit mySignal (); Главная суть в том, что сигналы и слоты объектов можно связывать (функция connect ()) между собой. Так, чтобы, например, при вызове сигнала одного объекта у другого выполнялась какая-нибудь слот-функция. В нашем случае сигнал создавать не будем, потому что он уже есть у объекта namRequest. А вот слот нам будет нужен (slotRequestFinished (QNetworkReply*)) — это что-то вроде колбэка для QNetworkAccessManager: как только придёт ответ от сервера, у namRequest вызовется сигнал finished (QNetworkReply*), его мы свяжем с нашим слотом, чтобы обработать полученный ответ (QNetworkReply).

Открываем теперь «backend.cpp», изменяем:

#include «backend.h»

const QString Backend: color_example = »#000000»;

Backend: Backend (QQuickItem *parent) : QQuickItem (parent) { engine.rootContext ()→setContextProperty («color_example», color_example);

engine.load (QUrl (QStringLiteral («qrc:///main.qml»)));

mainWindow = engine.rootObjects ().value (0); lvList = mainWindow→findChild(«lvList»); btnRequest = mainWindow→findChild(«btnRequest»);

engine.rootContext ()→setContextProperty («backend», this);

namRequest = new QNetworkAccessManager (this); connect (namRequest, SIGNAL (finished (QNetworkReply*)), this, SLOT (slotRequestFinished (QNetworkReply*))); }

void Backend: makeRequest (int id) { btnRequest→setProperty («enabled», «false»); // btnRequest→property («enabled»);

QString prepareRequest («http://localhost/qt_api/test»);

// HttpGet prepareRequest.append (»? id=»); prepareRequest.append (QString: number (id)); qDebug (prepareRequest.toUtf8()); QNetworkRequest request (QUrl (prepareRequest.toUtf8())); namRequest→get (request);

// HttpPost /*QNetworkRequest request (QUrl (prepareRequest.toUtf8())); request.setHeader (QNetworkRequest: ContentTypeHeader, «application/x-www-form-urlencoded»); QString params («id=»); params.append (QString: number (id)); qDebug (params.toUtf8()); namRequest→post (request, QByteArray (params.toUtf8()));*/ }

void Backend: slotRequestFinished (QNetworkReply * reply) { if (reply→error () != QNetworkReply: NoError) { qDebug (reply→errorString ().toUtf8()); } else { QJsonDocument jsonDoc = QJsonDocument: fromJson (reply→readAll ()); QJsonObject jsonObj; QJsonValue jsonVal; QJsonArray jsonArr;

jsonObj = jsonDoc.object (); jsonVal = jsonObj.value («done»); if (! jsonVal.isNull () && jsonVal.isObject ()) { jsonObj = jsonVal.toObject (); jsonVal = jsonObj.value («number»); if (! jsonVal.isNull () && jsonVal.isDouble ()) { qDebug (QString: number (jsonVal.toDouble (), 'f', 3).toUtf8()); } }

if (jsonDoc.object ().value («done»).toObject ().value («boolean»).toBool ()) { qDebug («json true»); } else { qDebug («json false»); }

jsonArr = jsonDoc.object ().value («done»).toObject ().value («list»).toArray (); QMetaObject: invokeMethod (lvList, «clear»); for (int i=0; i

btnRequest→setProperty («enabled», «true»);

reply→deleteLater (); } Очень много всего, понимаю. По порядку: присваиваем занчение константе цвета.

В конструкторе класса прописываем путь к корневому QML-файлу для QQmlApplicationEngine.

Находим корневой экран QML:

mainWindow = engine.rootObjects ().value (0); Все дочерние объекты корневого экрана находятся по их objectName, прописанных в QML. Пример для списка lvList:

lvList = mainWindow→findChild(«lvList»); Теперь мы связываем слово «backend» с самим классом Backend. Таким образом мы сможем обращаться к нашему классу (ко всем его публичным Q_INVOKABLE-функциям) из QML:

engine.rootContext ()→setContextProperty («backend», this); В самом начале конструктора мы связывали константу цвета со словом «color_example» в QML:

engine.rootContext ()→setContextProperty («color_example», color_example); Осталось создать объект namRequest:

namRequest = new QNetworkAccessManager (this); И связать его сигнал finished (QNetworkReply*) с нашим слотом slotRequestFinished (QNetworkReply*):

connect (namRequest, SIGNAL (finished (QNetworkReply*)), this, SLOT (slotRequestFinished (QNetworkReply*))); Функция connect в данном случае имеет 4 параметра: — объект, сигнал которого слушаем; — непосредственно сигнал этого объекта; — объект, который будет реагировать на услышанный сигнал (наш класс); — слот объекта, который сработает при вызове сигнала.

Переходим к функции makeRequest. Первым делом отключаем кнопку, чтобы во время запроса на неё нельзя было нажать снова:

btnRequest→setProperty («enabled», «false»); Похожим образом можно также считывать любые поля элемента из QML (следующая закоментированная строчка). Пишем адрес запроса в QString:

QString prepareRequest («http://localhost/qt_api/test»); Чтобы сделать GET-запрос с параметрами, просто добавляем их в строку запроса:

prepareRequest.append (»? id=»); prepareRequest.append (QString: number (id)); Обратите внимание на преобразование int-переменной (которая идёт на вход в функции makeRequest) к QString. Для этого используется функция QString: number ().

На всякий случай выводим в лог весь запрос (QString преобразуем к QByteArray):

qDebug (prepareRequest.toUtf8()); Далее формируем QNetworkRequest из строки запроса и просим QNetworkAccessManager выполнить GET-запрос:

QNetworkRequest request (QUrl (prepareRequest.toUtf8())); namRequest→get (request); POST-запрос выполняется похожим образом, оставил его закомментированным. Похоже, это единственный способ сделать POST-запрос, чтобы сайт, написанный на CodeIgniter«е, не выдавал «Disallowed Characters Error», с этим было много возни.

Переходим к нашему слоту, который запустится, как только придёт ответ с сервера.

Сначала проверяем, успешно ли прошёл запрос, выводим в лог сообщение об ошибке, если таковая возникнет:

if (reply→error () != QNetworkReply: NoError) { qDebug (reply→errorString ().toUtf8()); } Если ошибок нет, то будем заполнять список. Создаём QJsonDocument и основные объекты для работы с JSON-ом. Чтобы получить QJsonDocument из ответа используем функцию QJsonDocument: fromJson (reply→readAll ()), которой на вход подаём полученный ответ сервера: reply→readAll (). Отмечу, что если мы один раз считаем ответ из QNetworkReply, второй раз его считать будет уже нельзя, учтите это (поэтому ответ желательно сохранять в какой-нибудь переменной).

Теперь сначала получаем JSON-объект из QJsonDocument:

jsonObj = jsonDoc.object (); Затем получаем QJsonValue из поля «done» JSON-объекта:

jsonVal = jsonObj.value («done»); QJsonValue — это значение поля JSON-объекта, точная сущность которого нам неизвестна (может быть объектом, массивом, строкой и т.д.). Затем мы явно проверяем на пустоту наш QJsonValue и является ли он объектом. Если условие истинно, то мы преобразовываем QJsonValue в JSON-объект и получаем новое QJsonValue (поле «number») из этого объекта. Делаем аналогичную проверку, но только теперь нам нужно узнать, является ли QJsonValue значением типа double. В случае истинности выводим в лог поле «number» с форматированием (3 знака после точки).Следующий блок кода проверяет значение поля «boolean» внутри объекта «done» и в зависимости от его значения выводит в лог сообщение.

Небольшой нюанс работы с JSON-ами в Qt: если мы будем пытаться получить несуществующий JSON-объект или его поле, то никакой ошибки не произойдёт (в случае нативной разработки на Android, например, выскочит JSONException). Вместо этого несуществующее значение заполнится значением по умолчанию (например, нулём), учтите это.

Ещё отмечу, что Qt очень «строг» к соответствию типов в JSON-объекте, преобразовать самостоятельно, предположим, число из JSON«а к строке не сможет.

Теперь немного детальней рассмотрим заполнение списка в QML JSON-массивом, полученным в ответе от сервера.

Сначала мы создаём из JSON-массива «list» объект класса QJsonArray:

jsonArr = jsonDoc.object ().value («done»).toObject ().value («list»).toArray (); Затем вызываем функцию «clear» у объекта «lvList» (функцию «clear» внутри ListView в QML-файле создадим позже):

QMetaObject: invokeMethod (lvList, «clear»); Затем в цикле заполняем список «lvList» элементами JSON-массива — сначала создаём карту ключ-значение, затем вставляем в неё значение текущего элемента массива, используя ключ «name» (помните, мы указывали этот ключ в делегате ListView?):

QVariantMap map; map.insert («name», jsonArr.at (i).toString ()); И в конце вызываем функцию «append» у «lvList» (её мы тоже создадим совсем скоро):

QMetaObject: invokeMethod (lvList, «append», Q_ARG (QVariant, QVariant: fromValue (map))); Синтаксис функции QMetaObject: invokeMethod () следующий: — первый параметр — объект, у которого будем вызывать функцию; — второй параметр — функция, которую нужно вызвать у объекта «lvList»; — третий — параметры, которые будут идти на вход функции «append», в нашем случае это карта «map».

После обработки запроса вновь делаем кнопку активной:

btnRequest→setProperty («enabled», «true»); И удаляем обработанный ответ:

reply→deleteLater (); Почти всё. Осталось чуточку поменять файл «main.cpp»:

#include #include «backend.h»

int main (int argc, char *argv[]) { QApplication app (argc, argv);

new Backend ();

return app.exec (); } При старте приложения будет создаваться экземпляр класса «Backend», внутри которого вся прописанная нами логика.

Теперь добавим функции «clear», «append» в первый ListView («lvList») в файле «main.qml»:

function clear () { lvList.model.clear () } function append (newElement) { lvList.model.append (newElement) } В C++ бэкенде на вход функции «append» мы подавали заполненную карту (этот объект мы видим в аргументе функции), карта присоединиться к модели списка при вызове этой функции.

Также закомментируем элементы модели в первом списке:

model: ListModel { //ListElement { name: «Элемент 0» } //ListElement { name: «Элемент 1» } //ListElement { name: «Элемент 2» } } Осталось добавить кнопку, которая бы вызывала функцию makeRequest () класса Backend:

Button { objectName: «btnRequest» property int _id: 3 width: mainWindow.width / 4.5 height: mainWindow.height / 10 x: mainWindow.width — width y: mainWindow.height — height style: ButtonStyle { background: ButtonBackground { border.color: color_example } label: ButtonLabel { text: «Request» } } onClicked: backend.makeRequest (_id) } У кнопки есть имя «btnRequest», которое нужно, чтобы мы нашли её в C++ коде, поле

property int _id: 3  — это что-то вроде инициализации переменной в самом QML-файле: ключевое слово «property», тип, имя переменной и значение. Обращаться к переменной можно так: id_элемента.имя_переменной.

Остановимся здесь ещё на двух моментах. Во-первых, мы переопределяем цвет рамки у кнопки:

background: ButtonBackground { border.color: color_example } Помните, как мы связывали значение цвета с именем «color_example» в классе «Backend»? Во-вторых, обратите внимание на вызов функции по нажатию кнопки:

onClicked: backend.makeRequest (_id) Сначала пишем ключевое слово, которое определили в бэкенде для доступа к его функциям, затем пишем непосредственно имя функции и указываем параметр, идущий на вход (в данном случае property »_id», которое объявляли чуть выше).

Полный листинг «main.qml»:

import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Controls.Styles 1.2 import «QMLs»

ApplicationWindow { id: mainWindow objectName: «mainWindow» visible: true width: 640 height: 480 color:»#F0F0FF»

Button { function hello () { if (textField.text!= ») { text.text = «Привет, » + textField.text.toUpperCase () + »!» } } width: mainWindow.width / 2.5 height: mainWindow.height / 10 x: 0 y: mainWindow.height — height style: ButtonStyle { background: ButtonBackground {} label: ButtonLabel { text: «Тест» } } onClicked: hello () }

TextField { id: textField width: parent.width height: parent.height / 10 horizontalAlignment: Text.AlignHCenter placeholderText: «ваше имя» validator: RegExpValidator { regExp: /[а-яА-Яa-zA-Z]{16}/ } style: TextFieldStyle { background: Rectangle {color: «white»} textColor:»#00AAAA» placeholderTextColor:»#00EEEE» font: font.capitalization = Font.Capitalize, font.bold = true, font.pixelSize = mainWindow.height / 25 } Keys.onPressed: { if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return || event.key == Qt.Key_Back) { Qt.inputMethod.hide () loader.forceActiveFocus () event.accepted = true } } }

Text { id: text y: textField.height width: parent.width height: parent.height / 10 font.pixelSize: height / 2 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap }

ListView { id: lvList objectName: «lvList» y: text.y + text.height width: parent.width height: parent.height * 0.3 clip: true spacing: 8 model: ListModel { //ListElement { name: «Элемент 0» } //ListElement { name: «Элемент 1» } //ListElement { name: «Элемент 2» } } delegate: Rectangle { width: lvList.width height: lvList.height / 3 color:»#00AAAA» Text { text: name } MouseArea { anchors.fill: parent onClicked: console.log («ListView el#» + index + » clicked!») } } function clear () { lvList.model.clear () } function append (newElement) { lvList.model.append (newElement) } }

ListView { id: lvPager y: lvList.y + lvList.height width: parent.width height: parent.height * 0.2 clip: true model: ListModel { ListElement { map_url: «http://maps.googleapis.com/maps/api/staticmap? size=640×320&scalse=2&sensor=false¢er=Moscow» } ListElement { map_url: «http://maps.googleapis.com/maps/api/staticmap? size=640×320&scalse=2&sensor=false¢er=London» } ListElement { map_url: «http://maps.googleapis.com/maps/api/staticmap? size=640×320&scalse=2&sensor=false¢er=Rio» } } delegate: Image { width: lvPager.width height: lvPager.height source: map_url fillMode: Image.PreserveAspectFit } orientation: ListView.Horizontal snapMode: ListView.SnapOneItem }

Loader { id: loader y: lvPager.y + lvPager.height width: parent.width height: parent.height * 0.2 focus: true source: «qrc:/QMLs/Loader1.qml» }

Button { width: mainWindow.width / 4.5 height: mainWindow.height / 10 x: mainWindow.width / 2 y: mainWindow.height — height style: ButtonStyle { background: ButtonBackground {} label: ButtonLabel { text: «Loader» } } onClicked: loader.setSource («qrc:/QMLs/Loader2.qml») }

Button { objectName: «btnRequest» property int _id: 3 width: mainWindow.width / 4.5 height: mainWindow.height / 10 x: mainWindow.width — width y: mainWindow.height — height style: ButtonStyle { background: ButtonBackground { border.color: color_example } label: ButtonLabel { text: «Request» } } onClicked: backend.makeRequest (_id) }

} Всё сохраняем, запускаем. При нажатии на «Request» (на iPhone размеры текста не рассчитал немного) заполняется список: image

В логах видим:

localhost/qt_api/test? id=3123.000json true

Всё верно. Вот такая сборная солянка получилось для демонстрации :). Прикрепляю также сам проект на всякий случай (https://www.dropbox.com/s/to9kk0l71d6ma4h/Sample.zip).

И в качестве небольшого приложения пример кода сохранения параметров, использования даты и создания уникального идентификатора посредством Qt:

// Создаём объект QSettings, указываем имя файла, в котором будут содержаться данные QSettings settings («settings.ini», QSettings: IniFormat); // Создаём строку, которую запишем в файл настроек QString stringToSave; // Строка будет содержать текущую дату, поэтому создаём объект QDate QDate date = QDate: currentDate (); // Присваиваем значение текущей даты строке для записи stringToSave = date.toString («ddd-dd-MM-yyyy»); // Создаём уникальный id посредством класса QUuid QUuid uniq_id = QUuid: createUuid (); // Добавляем полученный id к строке stringToSave.append (» id=»); stringToSave.append (uniq_id.toByteArray ()); // Записываем значение в файл настроек settings.setValue («value1», stringToSave); settings.sync (); // Считываем записнное значение и выводим в лог qDebug (settings.value («value1»).toString ().toUtf8()); Это в принципе всё, что я хотел рассказать в этой публикации. Остался только один момент, о котором говорил в самом начале — как избежать скачков изображения на Android при запуске приложения. Самое простое решение — запускать приложение в полноэкранном режиме, а в ApplicationWindow сразу прописывать размеры экрана устройства. Делается в два шага — в самом начале конструктора класса «Backend» создаём поля для QML со значениями ширины и высоты экрана:

engine.rootContext ()→setContextProperty («screen_width», this→width ()); engine.rootContext ()→setContextProperty («screen_height», this→height ()); А непосредственно в QML-файле указываем как ширину и высоту у «ApplicationWindow» только что созданные поля:

ApplicationWindow { . . . width: screen_width height: screen_height color:»#F0F0FF» . . . Вот теперь точно всё :). Надеюсь, получилось не слишком сумбурно. В случае Android осталось только подписать приложение (эта функция, кстати, уже встроена в Qt, так что не нужно возиться с командной строкой, как обычно), создать иконки и можно публиковать. Все эти настройки для деплоя находятся на вкладке «Проекты» → «Android» → «Запуск».

© Habrahabr.ru