1 (12.10.2012 21:25:08 отредактировано divan)

Тема: Портирование Python-приложений на Maemo/Harmattan

Аннотация

В этой статье читатель откроет для себя, с какой легкостью популярные python-программы могут быть портированы на платформу Maemo/Harmattan, при этом соответствуя правилам создания UI и не теряя в функционале.

Хотя в статье подразумевается, что читатель имеет базовое понятие о языках Python и QML, она также подойдет тем, кто хочет начать их изучение, а простота приведенных примеров должна послужить этой цели. Код примеров, использованных в статье работает на мобильных устройствах Nokia N900, N9 и N950.

Одним из самых больших плюсов открытых программных платформ является свобода в выборе инструментария - вас никто не обязывает писать только на специально созданном под отдельную платформу языке. Harmattan (как и Maemo/Meego) в этом плане показывает себя с самой лучшей стороны - как и для любой уважающей себя POSIX-совместимой платформы, вы можете писать свой код и на C/C++, на QML/JS, на Python, на Bash - да хоть на brainfuck или .NET через mono! Помимо свободы выбора эта возможность открывает широчайший потенциал для портирования уже существующих приложений, о чем мы и поговорим в статье.

Почему Python?

На языке Python написано огромнейшее количество приложений - от чат-мессенджеров до видео-редакторов. Для Python написано нереальное количество модулей и расширений, и практически каждая востребованная опен-соурс библиотека на C/C++ имеет биндинги для Python. Интуитивно понятный и простой синтаксис с динамической типизацией, модульность, поддержка как процедурного и так и объектно-ориентированного программирования, открытый код, кроссплатформенность, колоссальная поддержка сообщества и многие другие достоинства сделали Python практически передовым средством для разработки в наши дни.


Нюансы портирования

Но, перейдем к делу. Python поддерживается в телефонах Nokia N900/N9/N950 из коробки. И есть немало качественного софта, который было бы полезно портировать на телефон. Казалось бы, все просто - даже не нужено пересобирать код, ведь Python - интерпетируемый язык. Но тут нас поджидает интересный момент в виде рекомендаций по созданию графического интерфейса(далее - UI) для Harmattan. Если кратко, то все приложения должны соответствовать определенным правилам и рекомендациям - формат навигации, цвета, формы виджетов и многие другие вещи должны быть узнаваемыми и привычными пользователю. Свои велосипеды в UI изобретать можно, но это считается дурным тоном и, как правило, ведет к невозможности нормально пользоваться программой.

Впрочем, инженеры Nokia проделали колоссальную работу и облегчили программистам и/или дизайнерам софта задачу - для соответствия рекомендациям UI достаточно использовать QML для описания интерфейсной части программы и стандартные виджеты. Остальное дело техники. Этим-то мы и воспользуемся, чтобы взять полезную python-программу для десктопа и портировать ее так, чтобы она выглядела нативно на мобильном телефоне.
* Примечание: Фреймворк для реализации UI называется QtQuick - QML это часть QtQuick, но далее в статье будет использоваться термин QML для большей ясности.

Вначале мы поэкспериментируем с простыми примерами, а потом перейдем к имплементации серьезного проекта, полностью созданного на связке Python+QML.


Что нам понадобится?

Как уже было сказано, для описания интерфейсов Harmattan использует язык QML, который официально является частью Qt начиная с версии 4.7. Поэтому для связки Python c QML нам понадобятся питоновские биндинги к Qt - называются они PySide, разрабатывались также под крылом Nokia и великолепно поддерживают практически все классы Qt. Простым языком, PySide позволяет использовать всю мощь Qt без использования C++ и надобности компилировать код.

Устанавливаем PySide
* Примечание: пользователю, который будет инсталлировать программу написанную с использованием PySide не нужно ничего инсталлировать или активировать - библиотеки подтянутся при необходимости автоматически, но для наших целей, нужно позаботиться заранее

Для телефонов N9/N950 вначале нужно активировать режим разработчика - это делается элементарно в настройках телефона. Заходим Параметры->Безопасность->Режим Разработчика и включаем единственный чекбокс. Впрочем, полагаю, у большинства читающих эту статью, данный режим и так включен.

http://t.imgbox.com/abf775kw.jpg

Далее, запускаем терминал и,
если у вас Nokia N950:

# ничего не нужно делать, PySide установлен из коробки.

если у вас Nokia N9:

devel-su
pass: # вводим пароль - rootme
apt-get update
apt-get install python python-pyside python-qtmobility

если у вас Nokia N900:

# проверяем вначале, что у нас активирован репозиторий extras-devel
root
apt-get update
apt-get install python python-pyside pyside-mobility pyside-qt4-core pyside-qt4-gui python-pyside.qtdeclarative

Всё, переходим к самому интересному.


Пишем код

Прежде чем приступить к портированию, мы должны освоить базовые способы взаимодействия python-кода и QML. И начнем  мы, конечно же, с реализации Hello, World!

Итак, выбираете самый удобный для себя способ работы с кодом и файлами на устройстве - можно писать код прямо на телефоне (в случае с N900/N950), можно (рекомендуемый вариант) подключиться к телефону по SSH и работать с компьютера, а можно создавать код на рабочем компьютере и копировать на телефон. В любой папке (скажем, /home/user/MyDocs/devel - она же просто devel, если телефон подключен как флешка) создаем два файла - hello.py и hello.qml.

hello.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import QDeclarativeView

app = QApplication(sys.argv)
view = QDeclarativeView()

url = QUrl('hello.qml')

view.setSource(url)
view.show()

sys.exit(app.exec_())

hello.qml

import Qt 4.7

Rectangle {
    width: 400
    height: 400
    color: "white"

    Text {
        text: "Hello World"
        font.pointSize: 28
        anchors.centerIn: parent
    }
}

Этот код достаточно прост и красноречив, поэтому останавливаться подробно на каждой строчке мы не будем.
Сохранив файлы, запустите из командной строки наш первый пример:

python ./hello.py

Результатом будет запустившееся приложение с квадратом в центре и текстом "Hello, world!", со статус-баром - первый первый шаг, который нам показывает, как легко связываются две такие, казалось бы, разные технологии, как QML и Python.

http://t.imgbox.com/acvSVmoc.jpg http://t.imgbox.com/adqjkJx2.jpg

Отлично, идем дальше.


Взимодействие Python и QML

Рассмотрим, какие у нас есть инструменты для взаимодействия кода на Python и QML.

Python-объекты в QML
Поскольку QML оперирует объектами, а Python великолепно поддерживает парадигму объектно-ориентированности - попробуем их связать именно таким способом. Создадим класс в Python и попробуем использовать объект класса в QML. Берем наш код для "Hello, World" и добавляем следующее:

hello2.py

...
class Console(QObject):
    @Slot(str)
    def printStdout(self, s):
        print s
...
con = Console()
context = view.rootContext()
context.setContextProperty("con", con)
...

+ Показать весь код

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import QDeclarativeView

class Console(QObject):
    @Slot(str)
    def printStdout(self, s):
        print s

app = QApplication(sys.argv)
view = QDeclarativeView()

con = Console()
context = view.rootContext()
context.setContextProperty("con", con)

url = QUrl('hello2.qml')

view.setSource(url)
view.show()

sys.exit(app.exec_())

Мы добавляем класс Console, который содержит единственную функцию - printStdout(), выводящую текст в консоль, создаем объект этого класса и с помощью функции setContextProperty() делаем этот объект видимым в QML.
Все, вот так все просто!
Смотрите сами, добавляем следующие строки в наш QML файл:
hello2.qml

...
    MouseArea {
        anchors.fill: parent
        onClicked: {
            con.printStdout("Hello, world!")
        }
    }

+ Показать весь код

import Qt 4.7

Rectangle {
    width: 400
    height: 400
    color: "white"

    Text {
        text: "Hello World"
        font.pointSize: 28
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            con.printStdout("Hello, world!")
        }
    }

}

Запускаем код, нажимаем на квадрат и видим результат функции printStdout в виде вывода в консоли.

http://t.imgbox.com/abfQr22p.jpg http://t.imgbox.com/abyctsO2.jpg


Теперь попробуем не только вызывать функцию, но и получать от нее какое-то значение - пусть, к примеру, наш код в Python занимается подсчетом количества нажатий, а QML - отображением этого.
hello3.py

...
class Test(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.counter = 0

    @Slot(result=int)
    def count(self):
        self.counter += 1
        return self.counter
...
test = Test()
context = view.rootContext()
context.setContextProperty("test", test)
...

+ Показать весь код

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import QDeclarativeView

class Test(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.counter = 0

    @Slot(result=int)
    def count(self):
        self.counter += 1
        return self.counter

app = QApplication(sys.argv)
view = QDeclarativeView()

test = Test()
context = view.rootContext()
context.setContextProperty("test", test)

url = QUrl('hello3.qml')

view.setSource(url)
view.show()

sys.exit(app.exec_())

hello3.qml

...
    Text {
        id: text
        property int count: 0
        text: "Нажато " + count + " раз"
    ...
        onClicked: {
           text.count = test.count()
        }

+ Показать весь код

import Qt 4.7

Rectangle {
    width: 400
    height: 400
    color: "white"

    Text {
        id: text
        property int count: 0
        text:"Нажато " + count + " раз"

        font.pointSize: 28
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
           text.count = test.count()
        }
    }
}


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

http://t.imgbox.com/accNwrlF.jpg http://t.imgbox.com/acuUU4fz.jpg

Не сильно полезная программка, с точки зрения пользователя, но для нас она бесценна, поскольку открывает для нас колоссальные возможности для следующих экспериментов!

Но, идем дальше, небольшими шагами. Следующий наш шаг - это научиться, наоборот, из Python получать доступ к объектам в QML.

QML-объекты в Python

Здесь нам понадобится такой полезный метод класса QDeclarativeView как rootObject(), который возвращает объект, соответствующий корневному объекту в подключенном нами QML файле.

root = view.rootObject()

Теперь, мы можем обращаться к rootObject, и через него получать доступ ко всем компонентам и/или javascript-функциям, определенным в QML.

Для примера, используем наш "Hello, World", и сделаем так, чтобы цвет квадратика изменялся из Python по таймеру, скажем, каждую секунду.

hello4.py

...
    @Slot()
    def press(self):
        root.changeColor()
...
test = Test()
context = view.rootContext()
context.setContextProperty("test", test)
...
root = view.rootObject()

+ Показать весь код

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import QDeclarativeView
class Test(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.counter = 0

    @Slot()
    def press(self):
        root.changeColor()

app = QApplication(sys.argv)
view = QDeclarativeView()

test = Test()
context = view.rootContext()
context.setContextProperty("test", test)

url = QUrl('hello4.qml')

view.setSource(url)

root = view.rootObject()

view.show()
sys.exit(app.exec_())

hello4.qml

text: "Hello World"
    ...
        onClicked: {
             test.press()
        }
    ...

    function changeColor() {
        if (rect.color == "#ffffff")
            rect.color = "red";
        else
            rect.color = "white";
    }

+ Показать весь код

import Qt 4.7

Rectangle {
    id: rect
    width: 400
    height: 400
    color: "white"

    Text {
        id: text
        text: "Hello World"
        font.pointSize: 28
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
             test.press()
        }
    }

    function changeColor() {
        if (rect.color == "#ffffff")
            rect.color = "red";
        else
            rect.color = "white";
    }

}

Результат:
http://t.imgbox.com/acvSVmoc.jpg  http://t.imgbox.com/adgIen9E.jpg

Внимательный читатель увидит, что гораздо эффективнее было бы вызывать changeColor() напрямую из onClicked(), но ведь наша задача сейчас показать механизм взимодействия QML и Python. Более того, это не единственный способ добиться желаемого - это можно сделать красивее при помощи еще одного очень важного в Qt механизма - сигналов, которые мы сейчас и рассмотрим.

Сигналы

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

hello5.py

...
area = root.findChild(QObject, "mouseArea")
area.clicked.connect(lambda: root.changeColor())
...

+ Показать весь код

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import QDeclarativeView

app = QApplication(sys.argv)
view = QDeclarativeView()

url = QUrl('hello5.qml')
view.setSource(url)

root = view.rootObject()

area = root.findChild(QObject, "mouseArea")
area.clicked.connect(lambda: root.changeColor())

view.show()
sys.exit(app.exec_())

Здесь мы соединяем сигнал clicked с функцией из QML (экспортированной в Python). Мы использовали лямбда-функцию для вызова root, но не знакомым с концепцией лямбда-функций можно пока не заморачиваться с этим - это лишь удобный способ определять однострочные безымянные функции.

hello5.qml

MouseArea {
        anchors.fill: parent
        objectName: "mouseArea"
    }

+ Показать весь код

import Qt 4.7

Rectangle {
    id: rect
    width: 400
    height: 400
    color: "white"

    Text {
        id: text
        text: "Hello World"
        font.pointSize: 28
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        objectName: "mouseArea"
    }

    function changeColor() {
        if (rect.color == "#ffffff")
            rect.color = "red";
        else
            rect.color = "white";
    }
}

Здесь мы всего лишь добавляем имя объекта mouseArea, чтобы позволить функции findChild правильно находить нужный нам объект.

Запускаем, пробуем.

Как все просто, не правда ли?

Конечно, за этой внешней простотой скрывается гораздо больше интересного, и есть масса других вариантов использования вышеописанных связок, но, по крайней мере, мы познакомились с основными способами подружить код QML и код Python - этого должно быть достаточно, чтобы начать делать что-то серьезное и портировать желаемую программу на мобильную платформу.

Теперь, приступим к реальному проекту.


Портируем серьезную программу

Мой выбор пал на очень популярную программу для изучения слов по методу словарных карточек - Anki. Это великолепнейшая программа, использующая алгоритм, основанный на эмпирически выведенном годами алгоритме SuperMemo2, для помощи в изучении информации - слов иностранного языка, математических формул, иероглифов или чего-угодно еще. Алгоритм с помощью оценок предыдущих ответов сам выбирает когда и через какое время вам показывать ту или иную карточку, делая это так, чтобы информация усваивалась наиболее эффективным для человеческого мозга образом. Программа позволяет создавать и наполнять колоды карт, изучать материал по карточкам и синхронизировать свои колоды через интернет между различными устройствами - очень удобная вещь, и, конечно же, хочется иметь эту полезную программу на телефоне.

http://t.imgbox.com/abrUNSmk.jpg http://t.imgbox.com/abzP6px8.jpg
http://t.imgbox.com/acqNRYuR.jpg http://t.imgbox.com/adw0KZam.jpg

Основная ее сила именно в алгоритме - изначально я решил его реализовать на C++ и QML, но увидев объем кода, количество недокументированных нюансов использования алгоритма и жесткую привязку к формату колод (используются базы sqlite3) стало очевидно, что нужно использовать уже существующий код, который, к счастью, написан на языке Python.

Проблема заключалась в том, что графический интерфейс, хоть и написан на Qt, но совершенно не адаптирован для мобильной платформы и использовать его на телефоне нереально. И самым оптимальным решением было создание своего интерфейса на QML, используя код алгоритма и функционал работы с карточками от Anki.

Сказано - сделано. Итак, что же нам нужно для портирования такого немаленького проекта.

План разработки
  1. Изучение кода проекта, понимание структуры программы.

  2. Создание враппера-прокси (класс-обертка, другими словами) для основного класса(или классов) программы.

  3. Создание красивого UI и привязка его к нашему классу-обертке.


1. Изучение кода программы

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

С кодом Anki получилась двоякая ситуация - с одной стороны, у него неплохо разнесен код алгоритмической части и его родного UI, что нам очень на руку, но с другой, алгоритмика оказалась очень сильно завязанной на формате карточек, без какой либо абстракции - все работает напрямую с SQLite базой данных.

После анализа был выделен основной класс Deck ("Колода" по-русски), который являлся центральным и использовал уже практически все остальные классы в программе.

ШАГ 1. Создание дерева исходников для нашей портированной версии.

Первым делом создадим новый каталог, скажем, /home/user/MyDocs/devel/ankiqml и скопируем необходимые для нас файлы из исходников Anki. Это будет весь каталог anki/libanki/ - скопируем его и переименуем просто в anki.
Далее, создадим наш главный исполняемый файл и простой qml для главной страницы - за основу можно взять наш пример с "Hello, World". Попробуем подключить уже упомянутый класс Deck:

from anki import Deck

и запустить:

~/MyDocs/devel/ankiqml $ python ./ankiqml.py
Traceback (most recent call last):
  File "./ankiqml", line 12, in <module>
    from anki import Deck
  File "/home/user/MyDocs/devel/ankiqml/anki/__init__.py", line 58, in <module>
    from anki.deck import DeckStorage
  File "/home/user/MyDocs/devel/ankiqml/anki/deck.py", line 12, in <module>
    import types, traceback, simplejson, datetime

ImportError: No module named simplejson
~/MyDocs/devel/ankiqml $

Мы имеем то, о чем упоминалось чуть выше - зависимости программы, которые так или иначе необходимо разрешить. Для python существует масса модулей, и многие из них собраны для Maemo/Harmattan, но есть и такие, которые отсутствуют в стандартных репозиториях.
Варианта решения этой ситуации три:

  1. Доустановить необходимые модули (и не забыть прописать их в зависимости)

  2. Собрать модуль самому под Harmattan и выложить в репозиторий (для продвинутых пользователей)

  3. Просто скопировать код модуля в дерево исходников нашего проекта (самый быстрый способ)

Остановимся на третьем решении, чтобы не отвлекаться от главной задачи и скачав исходный код simplejson скопируем его в папку anki/simplejson, а в коде, который пытается импортировать этот модуль(anki/deck.py, в нашем случае), добавим путь для поиска модулей:

import sys
sys.path.append("anki/simplejson")

* Примечание: тут можно указать и абсолютный путь - для финальной версии это будет даже лучше.

Попытаемся запустить нашу программу снова, и встретим подобную же ошибку с другим модулем - sqlalchemy. Поступим точно так же, как и в случае с simplejson. И так, пока программа не будет запускаться без ошибок об отсутствующих модулях. Если мы этого достигли, то получили запускающийся код, который, впрочем еще бесполезен и ничего не делает. Но, идем далее.


2. Создание враппера-прокси

Вторым шагом у нас будет задача "подружить" код программы с нашим QML-интерфейсом. Дать возможность оперировать объектами anki из нашего интерфейса. Для этого создадим враппер для, как уже упоминалось, главного класса Deck - при этом важно, чтобы наш класс наследовал класс QObject, иначе мы не сможем его сделать видимым в QML:

class DeckWrapper(QtCore.QObject):

    def __init__(self, parent = None):
        QtCore.QObject.__init__(self, parent)

    @QtCore.Slot(str)
    def open(self, name):
        self.name = name
        self.deck = anki.DeckStorage.Deck(self.name, backup=False)
        self.opened = True

    @QtCore.Slot()
    def close(self):
        if not self.opened:
            return
        self.deck.save()
        self.deck.s.flush()
        self.deck.close()
        self.opened = False

    @QtCore.Slot(result=bool)
    def is_opened(self):
        return self.opened

и уже известным нам методом "экспортируем" объект этого класса для использования в QML:

deck = DeckWrapper()
ctx = view.rootContext()
ctx.setContextProperty('Deck', deck)

и изменим наш QML из примера - будем вместо изменения цвета квадратика загружать и выгружать колоду (методы open() и close()):

function toggleDeck() {
    if (Deck.is_opened)
        Deck.open('./CoolDeck.anki');
    else
        Deck.close();
}

Пока что этот код тоже не особо функционален, но фактически мы уже сделали самую важную часть - связали наш QML-проект и python-код проекта Anki и имеем все возможности для использования абсолютно всего функционала, который нам дает модуль libanki. Пока что мы только загружаем и выгружаем колоду, но абсолютно таким же способом - через методы класса-враппера - мы можем делать все остальное - получать список карт в колоде, добавлять/редактировать/удалять карты, инициировать процесс повторения слов, получая следующую карточку, выставлять свои оценки, откладывать карточки и все-все-все, что умеет официальный UI Anki.

Но, конечно, придется потрудиться и одной из главных задач будет создание красивого мобильного UI - что, впрочем, является довольно приятной частью любой разработки. Кроме того, придется решать различные дополнительные задачи и реализовывать их в контексте нашей python-обертки - такие как использование NetworkManager'а, чтобы вызывались стандартные диалоги выбора соединения, или использование многопоточности средствами PySide - если нужно будет запускать некоторые процессы в фоне. Все это тоже является своеобразными врапперами для функционала программы, которую мы портируем.

Перейдем к UI - здесь нам понадобятся все наши познания (или, что важнее - желание познать) QML. Благо, документация и поддержка сообщества у QML великолепная, поэтому почти всегда ответ на свой вопрос можно найти очень быстро.


3. Создание красивого UI

Стандартный интерфейс Anki очень прост (хоть и несколько загрязнен, с точки зрения автора статьи) - окно со списком колод, кнопки для основных операций над колодой, дефолтной из которых является "Изучение карточек" в колоде. На этом этапе очень важно понимать, как пользователь будет использовать программу, ее стандартный способ использования. Это важно, как минимум, потому, что нам пока не нужно реализовать весь возможный функционал, который зачастую может быть избытычным. Например, нам вряд ли понадобятся статистические графики, отображающие прогресс в изучении слов - если кому-то эта информация и нужна - ее всегда можно посмотреть на компьютере. Но такие функции, как добавление новых слов или редактирование - очень важны, без них нормально пользоваться программой будет накладно.

Итак, после тщательного обдумывания, мы приходим к выводу, что у нас несколько основных экранов:

  • Экран со списком колод

  • Экран с информацией об отдельной колоде

  • Экран изучения слов в колоде

  • Экран настроек

  • Диалоги создания/редактирования колод и слов (которые тоже для удобства использования на телефоне будут отдельными экранами)

От этого и будем отталкиваться. В уже упомянутых в начале статьи рекомендациях к интерфейсам рекомендуется использовать модель PageStack для реализации экранов. Компоненты Page и PageStack берут на себя всю заботу по переключению экранов - нам нужно лишь в нужные моменты открывать и закрывать новые экраны с помощью функций PageStack.push() и PageStack.pop(). Остальное - дело техники, QML позаботится о плавных переходах, о порядке отображения и всех остальных деталях.

Далее, одним из важных моментов в интерфейсе для Harmattan является функциональная панель - toolbar, которая, помимо кнопок навигации между экранами, может отображать разную полезную информацию (например, сколько карточек осталось или лимит времени), а также обеспечивать доступ к меню с настройками. Toolbar тоже обеспечивается стандартным компонентом toolBarLayout и компонентами вроде toolIcon и toolButton. Фактически, удобный и нативный интерфейс для Harmattan делается невероятно просто, с помощью, буквально, нескольких строк кода:

ToolBarLayout {
        id: defaultTools
        ToolIcon {
            iconId: "icon-m-toolbar-back"
            onClicked: pageStack.pop()
        }
        ToolIcon {
            iconId: "icon-m-toolbar-refresh"
            onClicked: syncDeck()
        }
        ToolIcon {
            iconId: "icon-m-toolbar-view-menu"
            onClicked: launchMenu()
        }
    }

Далее, если приложение обладает каким-то особым подходом к интерфейсу, есть смысл его постараться максимально повторить, но все же, при портировании, стоит относиться к данному вопросу с точки зрения здравого смысла. Самым правильным считается отнестись к программе как к новой и представить, как пользователю было бы удобнее взаимодействовать с ней на телефоне. То что удобно делать мышкой на большом мониторе - неудобно делать пальцем на маленьком экране. Плюс стоит помнить про такие вкусности, как мультитач- и swipe-жесты,  которые делают работу с программой более интуитивной и приятной на телефоне.

Я выбрал вариант создания полностью нового UI, отличного от Anki, более интуитивного и приятного глазу. Отталкивался я от простых суждений - если это колода карточек, то она и должна выглядеть в телефоне, как колода карт. Если я "переворачиваю" карточку, чтобы посмотреть ответ - то нужно показать, что карточка переворачивается. Если я перехожу к следующей карточке, то есть смысл показать смену карт - вобщем максимально обеспечить скрестить привычный опыт взаимодействия с миром и пользовательский интерфейс.

И вот для этой цели QML подходит как нельзя более удачно. Великолепные возможности описания интерфейса, легкость манипулирования состояниями компонента, простота анимирования чего-угодно и как-угодно, позволяют воплотить все задуманное на QML достаточно просто.
К примеру, переворот карточки при ответе делается с помощью функционала Transforms:

transform: [
        Rotation {
            id: rotation
            origin.x: card.width/2
            origin.y: card.height/2
            axis.x: 0;  axis.y: 1; axis.z: 0
            angle: 0
        },
]

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

PinchArea {
        id: pinchArea
        anchors.fill: parent
        enabled: true
        pinch.minimumScale: 0.5
        pinch.maximumScale: 6
        onPinchFinished: {
            ankiCard.adjustFonts(pinch.scale);
        }
    }

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

Немного скриншотов:
http://t.imgbox.com/adxRtmVS.jpg http://t.imgbox.com/aby2Nd6E.jpg http://t.imgbox.com/adjRG2HG.jpg
http://t.imgbox.com/acrXf1dT.jpg http://t.imgbox.com/aduSaY8L.jpg http://t.imgbox.com/acv4TKBr.jpg http://t.imgbox.com/aalS5145.jpg

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


Создание deb-пакета

Отдельной темой стоит создание deb-пакета, который позволит опубликовать программу в Ovi Store или инсталлировать программу на Harmattan максимально просто.

С одной стороны, для упаковки в deb-формат python-программ есть очень удобный инструмент PySide Assistant. Он умеет и сам создавать дерево исходников, и "знает" структуру директорий Harmattan - у него есть даже специальный шаблон для проектов под эту платформу. С другой стороны, магазин Ovi Store налагает некоторые требования к расположению файлов, которые несколько расходятся с PySide Assistant и приходится прибегать к другим методам.
Одним из таких методов является создание собственного скрипта, который создает временную папку, копирует нужные файлы в нужные каталоги и создает deb-пакет, попутно создавая необходимые файлы для системы безопасности Aegis.

Пример скрипта, используемого для упаковки ankqml:

+ Показать код скрипта

#!/bin/sh

VERSION=0.1.0
DEB_NAME=ankiqml-$VERSION-1_i386.deb
BUILD_DIR=./build
APP_DIR=$BUILD_DIR/opt/ankiqml
USR_DIR=$BUILD_DIR/usr/share

echo "Cleaning up..."
rm -rf $APP_DIR

echo "Copying files to ./build..."
mkdir -p $APP_DIR
mkdir -p $APP_DIR/bin
mkdir -p $APP_DIR/qml
mkdir -p $USR_DIR
mkdir -p $USR_DIR/applications
mkdir -p $USR_DIR/icons/hicolor/64x64/apps
cp -fv ./ankiqml $APP_DIR/bin/
cp -fa ./images $APP_DIR
cp -fa ./anki $APP_DIR
cp -fa ./qml/* $APP_DIR/qml/
cp -f ./ankiqml.png $USR_DIR/icons/hicolor/64x64/apps/
cp -f ./ankiqml.desktop $USR_DIR/applications/

echo "Calculating digsigsums..."
python $BUILD_DIR/digsigsums.py ./build/

echo "Building deb..."
fakeroot tar -C $BUILD_DIR -czf $BUILD_DIR/data.tar.gz opt/ usr/
tar -C $BUILD_DIR/DEBIAN -czf $BUILD_DIR/control.tar.gz .
ar -r $DEB_NAME $BUILD_DIR/debian-binary \
    $BUILD_DIR/control.tar.gz \
    $BUILD_DIR/data.tar.gz
rm -rf $BUILD_DIR/control.tar.gz $BUILD_DIR/data.tar.gz

echo "Cleaning up..."
rm -f $BUILD_DIR/DEBIAN/digsigsums
rm -rf $APP_DIR
rm -rf $USR_DIR

echo "Done."
echo "deb-file stored at: ./$DEB_NAME"

exit 0

Или по ссылке - https://github.com/divan/ankiqml/blob/m … ake_deb.sh

Особое внимание тут стоит обратить на скрипт digsigsums.py, который нужно будет добавить к проекту - он генерирует подписи файлов, которые необходимы для корректной инсталляции на Harmattan. Подробнее про этот механизм можно прочесть по ссылке: http://forum.meego.com/showthread.php?t=5880


Результат

Финальный результат портирования Anki на Harmattan находится на Github'е - https://github.com/divan/ankiqml.
В Ovi Store программа еще не опубликована, поскольку еще не до конца отлажена синхронизация колод (персональных и публичных) с сервером Anki - а без отточенного идеально работающего фукнционала программу лучше не выкладывать вообще. Но автор этой статьи уже сам давно и ежедневно пользуется этой программой для изучения слов иностранного языка и надеется, что совсем скоро программа увидит свет в магазине Ovi Store.


Заключение

Описанная в статье методика позволяет переносить на мобильную платформу уникальные и сложные программы, разработка с нуля которых заняла бы огромное количество времени. Автор этих строк надеется, что статья у кого-то вызовет желание поближе познакомиться с описанными в ней технологиями, и сподвигнет на портирование замечательных программ.

---
Быть мрачным и непонятным очень просто. Охрененно трудно быть добрым и ясным. (с) Стивен Содеберг.

2

Re: Портирование Python-приложений на Maemo/Harmattan

Спасибо, очень полезный материал. Окакзывается связка питона с qml довольно проста. надо освоить.

Panasonic GD67=>Siemens C(SL)65=>Siemens CX75=>Siemens S65=> Siemens S75(208 Mhz)=>Nokia 6260(купил сломаный, сам поменял шлейф)=>Nokia 5530=>Nokia N9 [s]16[/s] 32 Gb Black

Поделиться

3

Re: Портирование Python-приложений на Maemo/Harmattan

Не ставятся python-pyside и python-qtmobility пишет невозможно загрузить некоторые архивы.  apt-get update не помогает

Поделиться