Уважаемое читатели, приветствую!
В этой статье я хотел бы немного поделиться своим скромным опытом на пути познания Тай’Дзен (или Tizen). Как приобщиться к Истине, я, в меру разумения своего, постарался описать в предыдущей публикации. Будучи верным своим обещаниям, продолжаю цикл статей.
Статья разбита на три части и построена как поэтапное руководство по разработке простого нативного приложения для ведения списка покупок. В первой части подробно исследованы структура типового Tizen-проекта, некоторые подходы к проектированию архитектуры приложения, а также особенности работы с WYSIWYG-редактором GUI. Во второй части рассмотрены работа со сценами и редактором сцен, со списками графических элементов управления (контролов), их кастомизация и обработка событий. В третьей части показаны организация поиска и использование базы данных. В статье представлена ссылка на git-репозиторий, в тексте указаны метки на соответствующие коммиты. Материал рассчитан на читателей, знакомых с C++ и не имеющих опыта работы с Tizen SDK. Для тех, кто о Tizen слышит впервые, рекомендую предварительно ознакомиться со статьей, упомянутой выше (в ней подробно описан процесс установки IDE и запуск «Hello, world!» на целевом устройстве/эмуляторе). Опыт работы с мобильными платформами не помешает, но и не обязателен, поэтому добро пожаловать под кат.
ЧАСТЬ ПЕРВАЯ
Прежде чем приступать к разработке приложения для новой для себя платформы, необходимо ознакомиться с основными гайдлайнами от ее создателей. В случае с Tizen подавляющее большинство таковых находится в документации, поставляемой в составе SDK. Таким образом, есть шанс достичь просветления в Тай’Дзен даже без помощи гугла. Однако на практике неофит сталкивается с рядом особенностей, которые являются серьезным испытанием его силы воли и решимости. Эти особенности не то чтобы сложны, просто они становятся очевидны уже после того, как все доступные грабли будут приведены в действие. Собственно, как только адепт разбирается с очередной неожиданностью, он тут же находит ее описание в документации. Или не находит. Постичь Тай’Дзен можно, только перестав стремиться к его постижению. Поэтому перейдем непосредственно к практике.
Рис. by Leka
Запускаем Tizen IDE, создаем новый Tizen Native Project: на вкладке Template выбираем Tab-based Application, подтип With SceneManager.
Для текущей задачи также подходит и Form-based Application, его применяем, если требуется более сложная кастомизация GUI (т.е. в большинстве коммерческих проектов, наш же является учебным). SceneManager позволяет удобно управлять формами (экранами): активация/удаление форм, история переходов между ними, определение текущей формы и т.д. SceneManager естественным образом интегрируется в шаблон проектирования MVC, где форма – это просто пульт управления с обратной связью, т.е. Вид (View).
С помощью SceneManager Контроллер (Controller) активирует «пульт», затем принимает и обрабатывает его сигналы, а результаты расчетов сохраняет в Модели (Model). После осуществления расчетов Контроллер посылает Виду сообщение «обнови то-то», в ответ на которое Вид запрашивает свое состояние у Модели и выводит его на экран. Модель в данной схеме – самый «бесправный» элемент: ее задача заключается лишь в том, чтобы в удобной форме предоставлять данные контроллеру и View (контроллеру для модификации, View – только для чтения). Вполне возможно, что столь вольная трактовка паттерна MVC не найдет поддержки в академических кругах, но я успешно использую ее на практике.
Собираем проект и запускаем на целевом устройстве/эмуляторе (процесс описан в моей предыдущей статье). Не забываем про настройку сертификатов безопасности, иначе запустить приложение не получится.
Рассмотрим структуру проекта, который IDE сгенерировала по шаблону Tab-based Application. Для навигации по файлам проекта используем окно Project Explorer (Window -> Show View -> Project Explorer).
Я задал проекту имя ShoppingList.
В каталоге shared/res хранится иконка приложения. Иконка для HD-экранов содержится в подкаталоге screen-density-xhigh. Ее размер должен быть 117x117 пикселов, освещение обязательно сверху, только круглая (PNG 32 бит с альфа-каналом). Если эти требования нарушены, приложение не пройдет сертификацию в Tizen Store. Также в документации сказано, что для WVGA-экранов размер иконки должен быть 78x78 пикселов, но устройств с такими экранами еще нет. Я некогда попытался сделать отдельную иконку для WVGA-устройств, положил ее в отдельный подкаталог screen-density-wvga, проверил на эмуляторе, и… приложение не прошло сертификацию по причине «некорректного размера иконки». Поэтому в ближайшем будущем можно ограничиться одной иконкой для HD-экранов.
Пробелы в названиях проектов не допускаются, однако в отображаемое название приложения пробел вполне можно вставить в настройках проекта. Настройки хранятся в специальном файле проекта manifest.xml, который по умолчанию автоматически открывается с помощью утилиты Tizen Manifest Editor.
Для каждой локали можно указать отдельное название на соответствующем языке. Также, помимо всего прочего, в манифесте указываются разрешения (привилегии), которые приложение запрашивает у ОС. Попытки вызова приложением «незадекларированных» функций ОС жестко пресекаются на всех уровнях, начиная от локального запуска на целевом устройстве и заканчивая процессом модерации в Tizen Store.
В каталоге res хранятся ресурсы приложения: изображения, аудиозаписи, а также xml-файлы описания GUI. При работе приложения эти xml-файлы используются GUI-фрейворком для построения форм, панелей, кнопок и прочих GUI-компонентов (контролов). Формируются с помощью WYSIWYG-редактора Tizen UI Builder, она же связывает процесс визуального построения интерфейсов с системой автоматической генерации кода (подробнее далее в статье).
В документации приведено много текста на предмет того, в каких подкаталогах размещать ресурсы. Текст производит гнетущее впечатление: на выбор подкаталога влияют плотность пикселов, размер экрана, тип координатной системы (логическая или физическая, указывается в манифесте) и т.д. Однако в настоящий момент в Tizen Store предлагается указать в списке поддерживаемых одно-единственное разрешение 720x1280 (т.е. HD), что на практике означает использование подкаталога screen-size-normal. Тип координатной системы при этом значения не имеет.
В каталоге data будем хранить локальные данные, поскольку чтение-запись приложением без каких-либо ограничений со стороны ОС разрешены только в нем. Каталог lib, как и следует из названия, предназначен для хранения библиотек, каталог Debug содержит объектные файлы, появляется после компиляции (если собирать в режиме Release, добавится каталог с названием Release).
Каталоги inc и src содержат заголовочные файлы и исходный код соответственно. При этом в них могут храниться как собственные файлы, так и ссылки на файлы из других мест файловой системы рабочей станции. Ссылка удобна, если несколько проектов пользуются одним и тем же исходным кодом, который часто меняется (т.е. нет смысла собирать его в библиотеку). Добавить ссылку удобно путем перетаскивания файлов из Проводника/Finder/etc. в Project Explorer.
Рассмотрим процесс развертывания приложения в оперативной памяти.
Точка входа приложения содержится в файле ShoppingListEntry.cpp, в функции OspMain.
_EXPORT_ int
OspMain(int argc, char* pArgv[])
{
AppLog("Application started.");
ArrayList args(SingleObjectDeleter);
args.Construct();
for (int i = 0; i < argc; i++)
{
args.Add(new (std::nothrow) String(pArgv[i]));
}
result r = Tizen::App::UiApp::Execute(ShoppingListApp::CreateInstance, &args);
TryLog(r == E_SUCCESS, "[%s] Application execution failed.", GetErrorMessage(r));
AppLog("Application finished.");
return static_cast< int >(r);
}
В этой функции формируется основной объект приложения, который содержит цикл обработки событий системы и событий пользовательского интерфейса. Класс этого объекта в нашем случае называется ShoppingListApp и наследуется от системного класса Tizen::App::UiApp, инкапсулирующего упомянутый event-loop. Одним из событий, обработчики которых переопределены в классе ShoppingListApp, является событие OnAppInitialized().
bool
ShoppingListApp::OnAppInitialized(void)
{
ShoppingListFrame* pShoppingListFrame = new (std::nothrow) ShoppingListFrame;
TryReturn(pShoppingListFrame != null, false, "The memory is insufficient.");
pShoppingListFrame->Construct();
pShoppingListFrame->SetName(L"ShoppingList");
AddFrame(*pShoppingListFrame);
return true;
}
Оно происходит сразу после запуска приложения и означает, что система готова начать выполнение пользовательского кода. В обработчике этого события формируется корневая единица основного GUI – фрейм. Основного – поскольку всплывающие панели и уведомления находятся на том же уровне иерархии. Фрейм кастомизируется по той же схеме, что и наследник класса App, называется, в свою очередь, ShoppingListFrame. В обработчике события инициализации фрейма OnInitializing() формируется SceneManager.
result
ShoppingListFrame::OnInitializing(void)
{
SceneManager* pSceneManager = SceneManager::GetInstance();
static ShoppingListFormFactory formFactory;
static ShoppingListPanelFactory panelFactory;
pSceneManager->RegisterFormFactory(formFactory);
pSceneManager->RegisterPanelFactory(panelFactory);
pSceneManager->RegisterScene(L"workflow");
result r = pSceneManager->GoForward(SceneTransitionId(IDSCNT_START));
return r;
}
Для работы SceneManager-у необходим доступ к фабрикам GUI-компонентов (форм и панелей), а также список «сцен». Сцена – это сочетание формы и панели, отображаемых на экране в отдельно взятый момент. Каждая сцена имеет свой строковый идентификатор, наличие формы для сцены обязательно, а вот панель опциональна.
Регистрация сцены может происходить напрямую через вызов метода SceneManager::RegisterScene(const SceneId &sceneId, const Tizen::Base::String &formId, Tizen::Base::String &panelId)
и опосредованно, через загрузку в SceneManager xml-файла с описанием сцен SceneManager::RegisterScene(const Tizen::Base::String &resourceId)
.
В данном случае это файл workflow.xml, содержится в каталоге res/screen-size-normal. При использовании этого способа происходит регистрация не только сцен, но и переходов между сценами, что несколько неочевидно. Метод SceneManager::GoForward
как раз и активирует такой переход (transaction) через его строковый идентификатор IDSCNT_START.
Идентификаторы команд перехода, сцены, форм, панелей и т.д. формируются с помощью UI Builder и помещаются системой кодогенерации в файл AppResourceId. Логично было бы ожидать, что имя ресурсного файла описания сцен workflow.xml также автоматически будет помещено в AppResourceId, но этого не происходит.
SceneManager получает из пользовательского кода идентификаторы перехода и на их основе определяет идентификаторы форм и панелей. Фабрики форм и панелей выдают SceneManager-у «продукт» по этим идентификаторам. Затем SceneManager монтирует форму на фрейм, панель на форму и запускает рендеринг.
Таким образом, в результате перехода IDSCNT_START на экране отображается главная форма ShoppingListMainForm, с открытой вкладкой под номером 1 (Tab1).
Рассмотрим, что происходит в ShoppingListMainForm.
Первым после завершения работы конструктора будет вызван метод Initialize:
bool
ShoppingListMainForm::Initialize(void)
{
result r = Construct(IDL_FORM);
TryReturn(r == E_SUCCESS, false, "Failed to construct form");
return true;
}
Как следует из названия, он инициализирует форму, вызывая унаследованный метод Construct. Конструирование формы происходит на основе данных, содержащихся в файле описания GUI. Строка IDL_FORM является идентификатором этого файла.
Остальные методы – это обработчики событий, в частности, событие нажатия сенсорной кнопки «назад/отмена».
В типичном Tizen-устройстве на лицевой стороне под экраном имеются три кнопки: две сенсорных и одна механическая. Механическая кнопка называется «Home» и переводит пользователя на HomeScreen, при длительном нажатии запускает диспетчер задач. На ее поведение мы никак повлиять не можем. А вот нажатия на сенсорные кнопки порождают события, которые можно обработать. Как правило, левая кнопка (Menu Key) вызывает контекстное меню, а правая (Back Key) означает отмену, возврат, выход из приложения и т.д. (в зависимости от контекста).
void
ShoppingListMainForm::OnFormBackRequested(Tizen::Ui::Controls::Form& source)
{
UiApp* pApp = UiApp::GetInstance();
AppAssert(pApp);
pApp->Terminate();
}
Статический метод GetInstance возвращает указатель на объект ShoppingListApp. Вызов метода Terminate означает, что наследнику Tizen::App::UiApp пора сматывать удочки, т.е. завершать event-loop и заодно вызвать у себя метод ShoppingListApp::OnAppTerminating. В этом методе нужно закрыть все ресурсы, но сильно долго это делать не рекомендуется: система принудительно завершит работу приложения примерно через 2 секунды (1950 мс, если верить профайлеру).
Разумеется, чтобы обработчик OnFormBackRequested вообще был вызван, необходимо подписаться на это событие, зарегистрировав указатель на объект, который эти события будет слушать. В данном случае этим объектом является сама форма — она реализует интерфейс IFormBackEventListener и подписывает сама себя в обработчике OnInitializing:
result
ShoppingListMainForm::OnInitializing(void)
{
result r = E_SUCCESS;
Header* pHeader = GetHeader();
if (pHeader)
{
pHeader->AddActionEventListener(*this);
}
SetFormBackEventListener(this);
return r;
}
Это событие возникает в начале активации формы, вызывается SceneManager-ом. В данном случае в этом же обработчике форма подписывается на события Header-а – панели, которая переключает вкладки. Для этого форме нужно реализовать интерфейс IActionEventListener, т.е. описать реакцию на событие OnActionPerformed:
void
ShoppingListMainForm::OnActionPerformed(const Tizen::Ui::Control& source, int actionId)
{
SceneManager* pSceneManager = SceneManager::GetInstance();
AppAssert(pSceneManager);
switch(actionId)
{
caseID_HEADER_ITEM1:
pSceneManager->GoForward(SceneTransitionId(IDSCNT_1));
break;
caseID_HEADER_ITEM2:
pSceneManager->GoForward(SceneTransitionId(IDSCNT_2));
break;
caseID_HEADER_ITEM3:
pSceneManager->GoForward(SceneTransitionId(IDSCNT_3));
break;
default:
break;
}
}
Здесь все довольно очевидно: в зависимости от того, какая кнопка нажата на Header-е, просим SceneManager-а показать ту или иную панель.
Теперь кратко рассмотрим процесс формирования ресурсных xml-файлов описания GUI с помощью утилиты UI Builder. Кратко – поскольку инструмент весьма сырой, и обзор всех его «особенностей» может послужить материалом отдельной статьи. Пока же могу сказать, что при работе с UI Builder нужно использовать принцип «все или ничего». Это означает, что либо, используя его, нужно смириться со всеми странностями и ничего не менять (касается структуры и имен каталогов, файлов, исходников), либо не использовать вовсе. Сбережете нервы и время.
Для начала откроем IDL_FORM.xml — файл описания GUI. По умолчанию должен открываться через UI Builder: если этого не произошло, то через контекстное меню в Project Explorer указываем Open with -> Native UI Builder (пример).
Структура и принципы работы редактора сходны с общепринятыми: по центру расположена рабочая область, по краям – окна навигации и свойств, сверху – главное меню. Непривычно расположена только панель с графическими элементами; более того, ее можно случайно свернуть и потом долго искать:
Рабочая область может содержать несколько вкладок, каждая из которых отображает редактируемый GUI-элемент. Работа с элементами подробно описана в документации, там же приводятся примеры работы механизма кодогенерации. Открытие вкладки и переход на нее осуществляется с помощью окна Resources:
Можно редактировать 5 типов GUI-контейнеров: Forms, Panels, Popups, QuickPanelFrames и ScrollPanels. При этом, как уже упоминалось выше, SceneManager работает только с формами и панелями (и их фабриками). К ним же можно отнести и ScrollPanel, поскольку она наследуется от Panel. Назначение QuickPanelFrames не совсем понятно – с таким же успехом можно использовать обычную панель, с той лишь разницей, что QuickPanelFrames формируется в отдельном окне, наравне с фреймом (но монтируется все равно на форму). С Popup все понятно – это всплывающая панель, гарантированно отображаемая поверх формы и ее контролов.
При открытии файла IDL_FORM.xml мы наблюдаем только форму и ее Header с кнопками – панели Tab1, Tab 2 и Tab3 являются отдельно редактируемыми элементами. Они монтируются на форму не в процессе ее создания, а при участии SceneManager. Это сделано для того, чтобы можно было извне управлять временем жизни панелей (в обычном режиме временем жизни GUI-компонента управляет родительский контейнер). Например, если требуется, чтобы в процессе работы приложения в каждый отдельно взятый момент в памяти находилась только одна панель. Следует помнить, что приложение будет работать на мобильной платформе, в то время как каждая загруженная панель расходует оперативную память и, что важнее, видеопамять (куда загружаются текстуры).
Следующим типом ресурсов являются строки – Strings. Предназначены для локализации приложения. Они от разрешения экрана не зависят, поэтому располагаются сразу в каталоге res. Механизм работы не многим отличается от аналогов, например в iOS, если бы не одно НО: в качестве идентификатора строки приходится задавать не английский аналог фразы, а маловразумительное IDS_STRING + индекс. Их можно поменять, но при этом и редактор, и парсер полученного ресурса начинают глючить. В результате имеем две проблемы:
1) Легко ошибиться в коде, поскольку перепутать индексы сильно проще, чем читабельные фразы;
2) Если для какого-либо языка фраза не будет задана, то вместо английского текста в приложении получим этот самый IDS_STRING.
Разумеется, адепты, достигшие определенного уровня просветления, индексы не путают и легко заполняют таблицы локализаций на несколько тысяч значений, но этот путь еще надо пройти…
Последним в списке расположен ресурс Workflow: отвечает за формирование сцен и транзакций (переходов между сценами). При работе с ним редактор выдает рекордное количество глюков, поэтому в рабочих проектах я предпочитаю сцены и переходы (особенно переходы!) формировать напрямую в коде. Из чувства самосохранения, нервы дороже.
Как бы там ни было, с каждой новой версией SDK предлагаемые инструменты улучшаются, меньше тормозят, а в последней редакции с ними даже можно понемногу работать (если не воротить сложные кастомные структуры и отказаться от кодогенерации совсем).
Продолжение следует 24 февраля. Благодарю за внимание.
Автор: IFITOWS