Пишем виджет рабочего стола под Maemo5 на Qt. Часть вторая и заключительная

Доброго времени суток, хабрапользователь. Продолжаю цикл статей о Nokia N900. И продолжаю начатую в прошлый раз статью о написании виджета для N900 на Qt. И так, в прошлый раз мы сделали простое приложение, которое получает список из файла специального формата (xml). На это раз мы это приложение доделаем и превратим его в такой вот виджет рабочего стола:







Так как я пишу больше о Maemo5 (MeeGo в перспективе), то попытаюсь поменьше уделять внимания на Qt и побольше на интеграцию с Maemo5/Hildon. И очень постараюсь показать, что разработка приложений на Qt не требует знаний специфики платформы и «родной среды» (в Нашем случае — это GTK).

В этой статье я напишу о том, как:


  • Написать делегат, для «красивого» отображения информации.
  • Реализовать собственную модель, для обеспечения функционала приложения.
  • Сделать составной виджет из нескольких других виджетов и набора функционала.
  • Оформить это как виджет рабочего стола и сделать .deb-пакет.
  • Добавить к этому виджету страницу с настройками.



Самое главное, что все вышеперечисленное не требует особых глубоких знаний и адаптации приложения под Maemo5, полученный виджет может отлично работать и на настольной системе (Mac/Win/Lin), но не как виджет рабочего стола, а как простое приложение.


Делегат для отображения списка



В первую очередь нужно определиться с тем, что и как отображать. Достаточный минимум информации:


  1. Время отправления.
  2. Сколько времени осталось до отправления.
  3. Заметки (по каким числам, в какие дни).



Немного прикинув решил сделать так: большими цифрами отображать время до отправления и через пробел время отправления. Цвет времени до отправления решил сделать динамически. Например, если до отправления осталось менее 5 минут — то рисоваться будет красным, если менее 15 минут — то темно желтым. Если же до отправления более 15 минут — то темно синим. Получается два порога срабатывания. В моем примере — это 5 и 15 минут. Было решено сделать эти пороги настраиваемыми и вынести в настройки(платформо-специфичное хранилище, QSettings). Хорошо бы было сделать еще и вот сами цвета настраиваемыми, но пусть это будет в «Roadmap'е». Сразу за предыдущими двумя полями на все оставшееся пространство нужно разместить заметку. Причем, по высоте должно влезть две строчки текста из заметки, так как заметка может содержать достаточно много информации.



Выглядеть в итоге это должно как-то так:







В связи с тем, что на мою просьбу о помощи в дизайне интерфейса приложения никто не откликнулся, я решил все сделать своими силами. Так как я в этом самом дизайне не особо силен, то решил возложить отрисовку элементов списка по максимуму на систему. Из-за этого код делегата, который отвечает за отрисовку этого элемента списка стал немного сложноват и нуждается в дополнительном описании (но зато удалось продемонстрировать стиле-независимую отрисовку виджета).



Для реализации делегата, необходимо перекрыть всего два метода (так как наш делегат не модифицирует данные):


void paint(QPainter * painter,const QStyleOptionViewItem & option, const QModelIndex & index) const;
QSize sizeHint (const QStyleOptionViewItem & option, const QModelIndex& index) const;





Первый метод рисует элемент, а второй возвращает необходимые размеры для отрисовки одного элемента. Ну и собственно сам делегат является наследником QStyledItemDelegate. Более подробней о дерегатах можно почитать в официальной документации. Благодаря тому, что мы отнаследовались от стилизованного делегата, в оба вышеупомянутых метода в виде параметров мы получаем QStyleOptionViewItem переменную. Благодаря этой переменной можно «достучаться» до параметров текущего стиля. Вот некоторые примеры:


  • option.rect — размеры прямоугольника для рисования
  • option.palette — QPallete для рисования
  • option.palette.text().color() — цвет текста текущей палитры для рисования
  • option.state — состояние текущего элемента (элемент может быть выделен, например)
  • option.font.pointSize() — размер шрифта
  • option.palette.base() — базовая палитра
  • option.palette.alternateBase() — альтернативная базовая палитра



В Qt доступно очень много разной информации и я бы рекомендовал ознакомиться с системой отрисовки виджетов с помощью стилей, чтоб понять как и что используется.



Как видно из моего кода, для определения размера шрифта для отрисовки времени и заметки я использовал статическую константу fontDelta. Она равна 3 на настольной системе и 6 на Маемо5 устройстве (N900). Это очень «грязный» обходной путь (workaround) или в простонародье — гвоздь. Связан он с тем, что на N900 при дельте 6 две строчки заметок помещаются в одну строку времени, а на настольной системе при дельте равной 3. Вот пример неправильной отрисовки:







Но если появится система, для которой дельта уже будет другой, то нужно будет добавлять еще одно значение с дельтой, что очень не хорошо. Поэтому эта дельта должна рассчитываться программно (при помощи QFonrMetrics например). Так что данная реализация является черновой и временной.Тем не менее, в результате мы получили делегата, который отрисовал следующее на экране:







Я считаю, что результат получился читабельным и красивым. Строки выделены между собой закраской (четные имеют один цвет, а нечетные другой). И цвета для подложки четных и нечетных строк соответствуют системным (системны цвета можно вдеть на заднем плане в окне отладчика).


Элементы управления



Так как расписаний у нас может быть несколько (как минимум нужно иметь два расписания: «туда» и «обратно»), то нужно уметь переключаться между ними. Для этого я подумал о двух стрелочках: «вперед» и «назад» по списку расписаний. Чтоб знать какое расписание сейчас отображается, необходимо поместить на виджет текст с названием направления. В процессе 4-х минутного дизайна у меня получился следующих виджет:







При нажатии на стрелочки «влево» и «вправо» происходит переключение на предыдущее и следующее расписание в списке расписаний. На нажатии на надписи с направлением — список позиционируется на ближайшей отравляющейся электричке.



Забегая вперед расскажу, что виджет на рабочем столе «не правильно» перехватывает событие пролистывания и перетягивания мышью. Поэтому список прокручивать не получилось ни с помощью полосы прокрутки ни с помощью обычного пролистывания пальцем (прикоснулся и протянул вверх или вниз). Проблема была решена таким образом: если нажать на элемент в списке, находящийся выше середины отображаемого списка, то список «провернется» таким образом, что этот элемент станет последним в видимом отрезке списка(Scroll Up):







А если нажать на элемент, находящийся ниже середины видимого списка, то он станет первым в видимом списке соответственно (Scroll Down):







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


Модель



Модель была изменена немного от предыдущего состояния. Был добавлен список расписаний, так как появилась возможность манипулировать несколькими расписаниями. Правильней было бы для каждого отдельного расписания инстантинировать свою модель, и в виджете переключаться между ними. Но так как я изначально механизм загрузки «спрятал в модель» и все что с ним связано туда-же, то логичнее и проще было и список расписаний переложить на модель. Поэтому прошу не критиковать сильно за такое отклонение от шаблона (тем более что в данном конкретном примере «потерь» от этого «отклонения» практически нет).


Запуск в виде обычного приложения







Если запустить получившийся код на данном этапе, то мы получим приложение. Не имеющего никакого упоминания о виджетах и Maemo5. Результат выполненния на N900 Вы видите выше.

Вот так приложение выглядит на Mac OS:







В симуляторе для кнопочных симбиан телефонов на примере N95 результат выглядит так:







ну и для тачевых симбианов на примере N97 вот так:







Результат получился хороший. Под каждую систему список отображатеся «родными» цветами, хотя полностью рисуется нашим кодом. Причем, если поменять стиль на устройстве, соответственно поменяется и список. Что очень здорово, так как сделает виджет «родным» для любого установленного стиля.


Начало



Именно отсюда можно начинать писать статью. Все, что было до этого, никакого отношения к мобильной разработке не имеет. Это введение. Но хотелось показать весь цикл написания приложения для N900, чтоб показать, что разработка для N900 (или другой Qt платформы, Symbian например) не отличается от разработки настольного приложения на Qt. Более того, разработка приложения на Qt для мобильных платформ по большей части является обычной разработкой на Qt.



То приложение, что есть сейчас у нас, не привязано ни к одной платформе. Его можно собрать и запустить на любой поддерживаемой платформе без изменения кода вообще.


Идентификация



В некоторых случаях необходимо «понимать» в коде под какую платформу и с какими параметрами собирается приложение. И в Qt коде можно применять такие определения препроцессора: Q_WS_XXXXXX. Расшифровывается это как «Qt Window System ». Для Maemo это будет звучать так: Q_WS_MAEMO_5. В .pro файле существуют команды ветвления для каждой платформы: win, mac, maemo5… Это тоже нужно использовать для идентификации платформы, если нужно использовать специфичный для этой платформы код. Для того, чтоб понять, что мы собираем код для Maemo5 и приложение собирается в виде виджета рабочего стола, в коде используется стандартная директива Q_WS_MAEMO5 и введенная директива RASP_WIDGET. Если последней директивы нет, то проект собирается как простое приложение для Maemo5 платформы.


Создание виджета







Существует специальный проект, который представляет из себя адаптер, реализованный по принципу шаблона «мост». С одной стороны — это интерфейс Hildon виджета рабочего стола, а со второй — это QObject для взаимодействия с Qt кодом. Называется QMaemo5HomescreenAdaptor и пакет с исходным кодом и примером можно скачать тут. Все что нужно сделать — это добавить секцию в .pro файл приблизительно такого содержания:


maemo5:contains( DEFINES, RASP_WIDGET ): include(./qmaemo5homescreenadaptor/qmaemo5homescreenadaptor.pri)





То есть, в случае, если проект собирается для maemo5 и указана директива препроцессора RASP_WIDGET, то добавляем в проект вложенный проект адаптера рабочего стола.



И в main.cpp файл добавляем такой код:


#elif defined(Q_WS_MAEMO_5) && defined(RASP_WIDGET)
  QMaemo5HomescreenAdaptor *adaptor = new QMaemo5HomescreenAdaptor(&w);
  adaptor->setSettingsAvailable(true);
  QObject::connect(adaptor, SIGNAL(settingsRequested()), &w, SLOT(showSettingsDialog()));
  w.show();
#elif defined(Q_WS_MAEMO_5)
  w.showMaximized();
#else
  w.show();
#endif





Тут все просто: если мы собираем виджет, то создаем адаптер и соединяем сигнал о вызове окна настроек с соответствующим слотом виджета. В случае сборки проекта как приложения для Maemo5 (не виджета), адаптер не создается, а виджет отображается на весь экран.



И еще нужно не забыть в заголовке main.cpp файла добавить заголовочный файл адаптера:


#if defined(Q_WS_MAEMO_5) && defined(RASP_WIDGET)
#include "qmaemo5homescreenadaptor.h"
#endif





Вот и всё. Весь код для реализации виджета написан. Но просто так запустить его как виджет нельзя, для этого нужно установить в систему, как виджет рабочего стола. А для этого нужно собрать .deb пакет.


Сборка виджета в .deb пакет



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



За основу взял вот эту очень полезную статью.



Теперь пошагово. Для выполнения всех команд нужно прописать путь к бинарникам от MADDE. Под Windows достаточно запустить специальный терминал, доступный в пункте Nokia Qt SDK после установки. Под *nix системами нужно просто добавить путь к переменной PATH:


export PATH=~/NokiaQtSDK/Maemo/4.6.2/bin/:$PATH





Окружение можно считать настроенным.



1. Создаем каталог



Далее создаем отдельный каталог с названием <имя приложения>-<номер версии>. Причем строго в таком виде. В моем случае получается: «rule-raspisanie-widget-0.1». Именно так и будет называться в менеджере пакетов получившиеся приложение. После этого перетаскиваем туда все что нужно для сборки приложения: .pro, .cpp, .h, .qrc,.



2. Создаем .desktop файл



В моем случае получилось вот такое содержание этого файла:


[Desktop Entry]
Name=Qt desktop timetable
Comment=Qt based widget example for habrahabr
Type=qt
X-Path=/opt/rule-raspisanie-widget/raspisanie
X-Multiple-Instances=false
X-home-applet-minwidth=200
X-home-applet-minheight=200





Тут особых комментариев не нужно. Главное тут указать путь к выполняемому файлу — «X-Path» и отключить возможность запуска нескольких экземпляров приложения — «X-Multiple-Instances». Подробней о формате файла можно почитать тут и тут.



3. Изменяем .pro файл



Этот .desktop файл необходимо скопировать в определенную папку в системе во время установки, поэтому нужно добавить это правило в .pro файл, чтоб сгенерировалось соответствующее правило в Makefile'е.



Добавляем в .pro файл в секцию с maemo5 такие вот строки:


PREFIX = debian/rule-raspisanie-widget
desktop.path = $$PREFIX/usr/share/applications/hildon-home
desktop.files = *.desktop

target.path = $$PREFIX/opt/rule-raspisanie-widget/
INSTALLS += target desktop





Тут префикс — это корень системы в .deb пакете. Вышеуказанный код делает два шага:


  • Создает новое правило «desktop» с путем установки "/usr/share/applications/hildon-home" в целевой системе. Объектами этой цели являются все .desktop файлы.
  • Назначает для цели сборки проекта (в нашем случае — это исполняемый файл) путь "/opt/rule-raspisanie-widget/" на этапе установки, благодаря этому цель сборки (получившееся приложение) будет скопировано в "/opt/rule-raspisanie-widget/" во время установки.



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



4. Генерируем скелет для .deb пакета



В этом шаге за нас все сделает MADDE, все что нужно сделать — это выполнить такую команду в созданном каталоге:


mad dh_make -createorig -single -e a@example.com -c gpl





Более подробно об упаковке с помощью MADDE можно почитать в официальной документации.

После этого появится каталог «debian» с кучей файлов внутри. Нам потребуется подкорректировать некоторые из них.



файл «control»



Этот файл содержит всю информацию, необходимую для сборки и установки приложения.

Вот так у меня выглядит этот файл:


Source: rule-raspisanie-widget
Section: user/desktop
Priority: extra
Maintainer: Ievgen Rudenko <erule.biz@gmail.com>
Build-Depends: debhelper (>= 5), libqt4-dev (>= 4.6.1), libhildon1-dev, libhildondesktop1-dev
Standards-Version: 3.7.3
Homepage: <erudenko.com, rule.habrahabr.ru>

Package: rule-raspisanie-widget
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, qt4-homescreen-loader
Description: The test widget for desktop, shoew timetable
It's Qt example of making Desktop Widget for Maemo.
It's Timetable that use simple XML format to retrieve via web.





Важными параметрами являются:


  • Build-Depends — тут нужно прописать от каких пакетов зависит сборка приложения, в нашем случае это Qt — libqt4-dev (>= 4.6.1) и еще два пакета для адаптера ( libhildon1-dev, libhildondesktop1-dev);
  • Depends — указать какие пакеты обязаны быть в системе для запуска приложения. Наше приложение требует наличие адаптера qt4-homescreen-loader;
  • Section — к какой категории относится приложения (информация для менеджера пакетов).



Ну и естественно не забудьте заполнить поля с названием, мейнтейнером и описанием.



файл «rules»



Этот файл описывает шаги для сборки проекта. Нужно прописать в нем, что сначала необходимо запустить qmake перед выполнением make (чтоб сгенерировать Makefile) на этапе сборки и то-же самое для этапа очистки.

Ищем правило build-stamp: configure-stamp и правим(жирным выделено то, что нужно вставить):


build-stamp: configure-stamp
dh_testdir

# Add here commands to compile the package.
<b>qmake «DEFINES+=RASP_WIDGET» &&</b> $(MAKE)





Не забываем про формат Makefile'a: перед каждым шагом для сборки цели необходимо ставить символ табуляции.



Теперь меняем этап очистки, ищем одноименную цель — clean в файле rules и меняем его соответственно. После изменения этот этап будет выглядеть таким образом:


clean:
dh_testdir
dh_testroot
rm -f build-stamp configure-stamp

# Add here commands to clean up after the build process.
<b>qmake «DEFINES+=RASP_WIDGET» &&</b> $(MAKE) clean

dh_clean





файл «postinst»



Этот файл выполняется после установки пакета в систему и тут можно писать дополнительные команды, если необходимо. В нашем случае нужно обойти один небольшой баг в MADDE, который заключается в том, что в приложении «сбрасывается» флаг «x» у приложение, информирующий о том, что файл является исполнительным. Всё что нужно сделать — это написать в файле команду для установки этого флага:


#!/bin/sh
chmod +x /opt/rule-raspisanie-widget/raspisanie





Таким образом, после установки запуститься этот скрипт, который установит флаг «x» нашему приложению.



файл «compat»



Согласно документации — это файл версии для проверки совместимости с Debhelper: «Debian helper compatibily version.» Просто убедитесь, что там стоит «5», а не «7».



5. Собираем .deb пакет



Все готово, все что нужно сделать — это выполнить команду:


mad dpkg-buildpackage -us -uc





И в директории уровнем выше "../" Вы найдете три файла с названием пакета и расширениями: dsc, tar.gz, changes, deb. Вот последний нам и нужен.

P.S.: Подробней о вышеописанном «шаманстве» можно почитать в документации по созданию .deb пакетов.


Установка получившегося пакета



Я делал это просто — копировал c помощью scp на устройство и запускал из стандартного файлового менеджера непосредственно на устройстве. После этого пакет устанавливался и нужно просто добавить виджет на рабочий стол обычным и путем и все будет работать. Еще можно отправить себе по почте во вложениях и если два раза клацнуть по вложенному .deb пакету на устройстве, автоматически откроется менеджер пакетов и установит пакет из вложения письма.

Можно также сделать это с помощью команд «dpkg -i » на устройстве через ssh или MADDE Device Runtime.


Пересборка .deb пакета



Тут тоже все просто: удаляем сгенерированные файлы dsc, tar.gz, changes, deb. Копируем новые исходники поверх старых в папке сборки пакета и выполняем снова команду:


mad dpkg-buildpackage -us -uc





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


Выводы



Получившейся результат Вы можете наблюдать в самом верху, это скриншот с моего N900. Приложение не идеально, и явно нуждается в дальнейшем развитии, но вполне работоспособно и выполняет основные функции. Главную задачу (написания приложения без знаний платформы) я считаю выполненным. Единственное, с чем пришлось повозиться — это упаковка в .deb файл. Но во-первых, этот формат пакетов не является спецификой Maemo. А во-вторых, в следующих релизах MADDE эти все шаги будут «спрятаны» в Qt-Creator (большая часть уже спрятана).



Итог затраченного времени на втором шаге:


  • доработка существующего кода и отладка: 3 часа
  • изучение документации и информации по упаковке в .deb: 30 минут
  • отладка приложения как «виджета рабочего стола»: 1 час



Итого времени 4 с половиной часа. Если учитывать «первый этап», то общее время по созданию приложения получается почти 7 часов. Один рабочий день.

Вот на написание этой статьи у меня ушло около 16 часов чистого времени (почти неделя «грязного»). Так что выгодней писать код, чем статьи :-)



Код можно скачать отсюда.

Директорию с заготовкой для создания .deb пакета можно скачать отсюда.



Перепечатано с разрешения автора с оригинальной статьи.