Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Разработка кроссплатформенных приложений на фреймворке Kivy

Николай Коваленко
Web-разработчик

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

Итак, Kivy — это кросс-платформенный фреймворк для создания приложений. На Kivy можно создавать приложения под ОС Linux, Windows, OS X, Android и IOS. Фреймворк свободно распространяем и не имеет ограничения на использование (лицензия MIT). Инструментарий постоянно разрабатывается и улучшается, есть подробное API и руководство к программированию.

Давайте изучим этот фреймворк на практике, собирая простое приложение-переводчик. Для перевода приложение будет использовать API Яндекс-переводчика.

В первой части статьи будет описан основной функционал приложения: создание и работа с основными экранами приложения, стилизация виджетов, и работа с API Яндекс-переводчика. Запускаться программа будет при помощи интерпретатора Python. Во второй части я рассмотрю функции, делающие приложение более удобным и законченным — анимирование элементов интерфейса и озвучивание переведенного текста, а также расскажу, как сделать сборки приложения для запуска на различных ОС.

Создание базового экрана

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

  • translator
    • app.py
    • widgets
      • kv
    • screens

Здесь translator — папка проекта, app.py — центральный скрипт, запускающий экземпляр приложения, widgets — папка для стилизованных виджетов, screens — папка для файлов экранов. Для удобства файлы графической разметки Kivy (*.kv) и файлы с программным кодом (*.py) находятся в разных директориях, kv и widgets, соответственно.

Сделаем наш первый экран. Для этого создадим соответствующие файлы в папке screens:

translator/screens/base.kv

<BaseScreen>:
    
  FloatLayout
        
    canvas.before:
            
      Color:
                
        rgba: 1, 1, 1, 1
            
      Rectangle:
                
        pos: self.pos
                
        size: self.size

translator/screens/screen.py

from kivy.lang import Builder

from kivy.uix.screenmanager import Screen




Builder.load_file('screens/base/base.kv')




class BaseScreen(Screen):
    
  pass

Для верстки элементов на экране мы будем использовать инструмент FloatLayout. С его помощью можно создать интерфейс, и при этом точно разместить элементы относительно друг друга (при верстке с макета). Код в блоке canvas.before описывает, что всю область экрана нужно закрасить в белый цвет.

Примечание

Kivy использует десятичную нотацию RGB и RGBA. Формула перевода из обычного представления (0-255) в данный вид: float_color = color / 255.0. При этом значение alpha-канала для RGBA остается таким же. Для удобства можно воспользоваться онлайн-конвертером.

Чтобы подсказать какой именно kv-файл необходимо использовать для данного модуля .py, мы используем kivy.lang.Builder. И наконец, наследуем экземпляр нашего первого экрана от базового класса kivy.uix.screenmanager.Screen.

Все, что мы увидим на экране при запуске — белый фон. Добавим к экрану также надпись «Hello S Media Link!», используя виджет Label и изменив цвет его текста на черный:

translator/screens/base.kv

<BaseScreen>:
    
  FloatLayout
        
    canvas.before:
            
      Color:
                
        rgba: 1, 1, 1, 1
            
      Rectangle:
                
        pos: self.pos
                
        size: self.size
    Label:
            
      text: 'Hello S Media Link!'
            
      color: 0,0,0,1  

Центральный скрипт и контроллер экранов

Запишем в app.py следующее содержимое:

translator/app.py

from kivy.app import App

from kivy.uix.button import Button



from kivy.config import Config

Config.set('graphics', 'width', '288')

Config.set('graphics', 'height', '512')



class TranslatorApp(App):

    title = 'Translator'

    root_widget = None



    def initialize_app(self):

        self.root_widget = Button(text='Test')



    def build(self):

        self.initialize_app()

        return self.root_widget

TranslatorApp().run()

Этот код представляет собой основу приложения. При помощи kivy.config.Config изначально объявлено базовое разрешение окна приложения. Попробуем запустить его, что все работает, и Kivy установлен правильно — и тогда мы увидим серую кнопку с надписью «Test».

Контроллер экранов позволит нам иметь в приложении любое количество различных экранов и осуществлять переходы между ними. Создадим файл screenmanager.py:

translator/screens/screenmanager.py

from kivy.uix.screenmanager import ScreenManager,SlideTransition



from screens.screen import BaseScreen




sm = ScreenManager(transition=SlideTransition())

screens = {
    
  'main': BaseScreen,

}

В этом файле мы создаем экземпляр класса kivy.uix.ScreenManager и храним словарь с ссылками на объекты созданных экранов.

Далее продолжим работу над контроллером экранов в центральном скрипте. Поменяем код так, чтобы вместо виджета кнопки экземпляр приложение возвращал созданный объект контроллера:

translator/screens/screenmanager.py

...



from screens.screenmanager import sm, screens

from kivy.uix.screenmanager import ScreenManagerException




class TranslatorApp(App):
    
    title = 'Translator'
   
    screen_manager = None

    

    def initialize_app(self):
        
        self.screen_manager = sm
        
        self.switch_screen('main')

    

    def switch_screen(self, screen_name):
        
        if screen_name in screens.keys():
            
            screen = screens[screen_name](name=screen_name)
            
            self.screen_manager.switch_to(screen)
            
            return
       
        else:
            
            raise ScreenManagerException('Screen {} not found'.format(screen_name))

    

    def build(self):
        
        self.initialize_app()
        
        return self.screen_manager



...

Функция switch_screen извлекает объект экрана по названию из созданного нами словаря и говорит контроллеру переключиться на него. При запуске приложения по умолчанию включится экран main.

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

Виджеты базового экрана

Сделаем созданный нами экран базовым, то есть будем наследовать от него другие экраны приложения. Переместим файлы экрана в новую папку screens/base.

translator/screens/base/base.kv

translator/screens/base/screen.py

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

Код для заголовка:

translator/widgets/kv/header.kv

<Header>

    text: ''

    canvas.before:

        Color:

            rgba: 1, 0.863, 0.38, 1

        Rectangle:

            pos: self.pos

            size: self.size

    size_hint: 1, 0.1

    pos_hint: {'center_x': 0.5, 'center_y': 0.95}



    Label:

        text: root.text

        color: 0, 0, 0, 1

        size_hint: 1, 1

        pos_hint: {'center_x': 0.5, 'center_y': 0.5}

Инструкция root.text означает, что мы берем значение свойства text виджета Label из свойства text родительского виджета Header. Далее я объясню, зачем нам это нужно.

translator/widgets/header.py

from kivy.lang import Builder

from kivy.uix.floatlayout import FloatLayout




Builder.load_file('widgets/kv/header.kv')




class Header(FloatLayout):
    
  pass

Код для меню:

translator/widgets/kv/footer_menu.kv

<FooterMenuButton@Button>:
    
    background_normal: ''
    
    background_color: 1, 1, 1, 1
    
    background_down: ''
    
    color: 0, 0, 0, 1



<FooterMenu>
    
    size_hint: 1, 0.1
    
    pos_hint: {'center_x': 0.5, 'center_y': 0.05}

    

    # a separator
    
    Widget:
        
        size_hint: 1, 0.2
        
        pos_hint: {'center_x': 0.5, 'center_y': 0.9}
        
        canvas:
            
            Color:
                
                rgb: 0.8, 0.8, 0.8
            
            Rectangle:
                
                pos: 0, self.center_y
                
                size: self.width, 2

    
    
    BoxLayout:
        
        size_hint: 1, 0.8
        
        pos_hint: {'center_x': 0.5, 'center_y': 0.47}
        
        FooterMenuButton:
            
            text: "Screen 1"

   

        FooterMenuButton:
            
            text: "Screen 2"

Код FooterMenuButton@Button означает, что мы создаем свой виджет FooterMenuButton, наследованного от виджета Button, и определив заранее значения некоторых свойств родительского виджета. Это позволит избежать повторения кода для кнопок меню. Иструкциями size_hint и pos_hint мы говорим FloatLayout, как именно разместить виджет на экране и сколько места он должен занимать. При верстке двух равнозначных по величине элементов применение BoxLayout немного уменьшает количество написанного кода, по сравнению с FloatLayout.

translator/widgets/footer_menu.py

from kivy.lang import Builder

from kivy.uix.floatlayout import FloatLayout




Builder.load_file('widgets/kv/footer_menu.kv')




class FooterMenu(FloatLayout):
    
  pass

Виджеты созданы. Теперь добавим их на экран.

translator/screens/base/base.kv

#:import Header widgets.header

#:import FooterMenu widgets.footer_menu



<BaseScreen>:

    title: ''



    FloatLayout

        canvas.before:

        Color:

            rgba: 1, 1, 1, 1

        Rectangle:

            pos: self.pos

            size: self.size



        Header:

            text: root.title



        FooterMenu:

Вверху (#:import) вы видите инструкции импорта kv-языка, позволяющие использовать пользовательские виджеты, объявленные в разных файлах. Также мы используем обращение к свойству родительского элемента, как в Header, и в этом случае. Это нужно для того, чтобы мы могли изменять надпись в заголовке экрана, при переключении между ними, просто изменяя свойство title родительского класса BaseScreen.

Запустим приложение:

Создание экранов-наследников и переключение между ними

В приложении будет два экрана — «Главный» для функционала переводчика и «О Приложении» с информацией об авторе. Давайте сделаем их.

Добавим новые папки в screensmain и about. В папках разместим следующие файлы.

Экран «О Приложении»:

translator/screens/about/about.kv

<AboutScreen>:
    
  title: 'О приложении'

translator/screens/about/screen.py

from kivy.lang import Builder

from kivy.uix.floatlayout import FloatLayout




Builder.load_file('widgets/kv/about.kv')




class AboutScreen(FloatLayout):
    
  pass
Для главного экрана:

translator/screens/main/main.kv

<MainScreen>:
    
    title: 'Главный'

translator/screens/main/screen.py

from kivy.lang import Builder

from kivy.uix.floatlayout import FloatLayout




Builder.load_file('widgets/kv/main.kv')




class MainScreen(FloatLayout):
    
  pass

Теперь сделаем так, чтобы по нажатию кнопок меню экраны переключались. Отредактируем код в следующих файлах:

translator/screens/screenmanager.py

...

from screens.main.screen import MainScreen

from screens.about.screen import AboutScreen




sm = ScreenManager(transition=SlideTransition())

screens = {
    
    'main': MainScreen,
    
    'about': AboutScreen,

}

Тут мы добавили созданные экраны в словарь контроллера экранов.

translator/widgets/footer_menu.py

...

from kivy.app import App



Builder.load_file('widgets/kv/footer_menu.kv')




class FooterMenu(FloatLayout):

    def switch_screen(self, screen_name):

        App.get_running_app().switch_screen(screen_name)

translator/widgets/kv/footer_menu.kv

...

FooterMenuButton:

    text: "Главная"

    on_press: root.switch_screen('main')



FooterMenuButton:

    text: "О приложении"

    on_press: root.switch_screen('about')

На событие on_press обоих кнопок происходит вызов функции switch_screen виджета FooterMenu.

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

Главный экран

Добавим на главный экран необходимые для работы переводчика виджеты.

translator/screens/main/main.kv

...

BoxLayout:

    size_hint: 1, 0.8

    pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    orientation: 'vertical'

    padding: 7

    spacing: 7



    TextInput:

        hint_text: 'Введите текст'

        font_size: 15

        padding: 10

        multiline: True

        size_hint: 1, .3



    ScrollView

        Label:

            size_hint_y: None

            text: ''

            font_size: 15

            padding: 5, 5

            halign: 'justify'

            color: 0, 0, 0, 1

            text_size: self.width, None

            height: self.texture_size[1]

...

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

translator/screens/main/main.py

...

class MainScreen(BaseScreen):

    original_lang = ['eng', 'Английский']

    expected_transl = ['rus', 'Русский']



    def __init__(self, *args, **kwargs):

        super(MainScreen, self).__init__()

        self.update_translation_direction()



    def update_translation_direction(self):

        self.original_lang, self.expected_transl = self.expected_transl, self.original_lang

        self.title = '{} -> {}'.format(self.original_lang[1],   self.expected_transl[1])

        self.ids.input.text = ''

        self.ids.output_view.text = ''

...

Далее сделаем заголовок главного экрана интерактивным.

translator/widgets/kv/header.kv

...

Label:

    id: label

    text: '[ref=*]' + root.text + '[/ref]'

    color: 0, 0, 0, 1

    size_hint: 1, 1

    pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    markup: True

translator/screens/base/base.py

...

class BaseScreen(Screen):

    def __init__(self, *args, **kwargs):

        super(BaseScreen, self).__init__()

        header = self.ids.header

        header_label = header.ids.label

        header_label.on_ref_press = self.on_title_press



    def on_title_press(self, *args):

        pass

Инструкция разметки ref говорит Kivy о том, что нужно совершить вызов функции on_title_press, если произойдёт нажатие на текст заголовка.

translator/screens/base/base.kv

...

Header:

    id: header

    text: root.title

...

translator/screens/main/main.py

...

def on_title_press(self, *args):

    self.update_translation_direction()

...

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

Экран «О Приложении»

Нет связанной функционирующей логики с этим экраном. Отображаться будет только информация об авторе приложения.

Добавим к файлу разметки, к примеру, следующие строки:

translator/screens/about/about.kv

Label:

    text: 'Иван Иванов\niivanow@smedialink.com\n2017'

    halign: "center"

    color: 0, 0, 0, 1

Функционал переводчика

Функциональную часть приложения расположим в папке translator/core.

Класс TranslatorEngine в файле translator/core/translator_engine.py будет представлять сервис для работы с API переводчиков. К примеру у Яндекса их два — Словарь и Переводчик. Словарь используется для перевода отдельных слов, а Переводчик как для перевода слов, так и фраз.

translator/core/translator_engine.py

from requests import Session


from core.translators.yandex_translator import YandexTranslator




class TranslatorEngine:

    _ya_translator = None


    def __init__(self):

        self._ya_translator = YandexTranslator()


    def translate_text(self, text, original_lang, expected_lang):

        res = self._ya_translator.translate_text(text, original_lang, expected_lang)

        return res

В данном приложении мы будем использовать функционал Яндекс.Переводчика. Нам понадобится файл настроек для хранения API ключа этого сервиса. Заодно запишем в файл настроек и параметры разрешения экрана.

translator/settings.py

WINDOW_WIDTH = 512

WINDOW_HEIGHT = 288



YANDEX_TRANSLATOR_API_KEY = ''



try:

    from local_settings import *

except ImportError:

    pass

translator/app.py

...

from settings import WINDOW_HEIGHT, WINDOW_WIDTH

...

Config.set('graphics', 'width', WINDOW_HEIGHT)

Config.set('graphics', 'height', WINDOW_WIDTH)

...

Для локальных настроек, которые нет необходимости отправлять в удаленный репозиторий (ключи), будем использовать local_settings.py.

translator/local_settings.py

YANDEX_TRANSLATOR_API_KEY = 'trnsl.1.1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx’

Теперь обьявим класс YandexTranslator для работы с API переводчика.

translator/core/translators/yandex_translator.py

import json

from requests import Session



from kivy.logger import Logger



from settings import YANDEX_TRANSLATOR_API_KEY




YANDEX_TRANSLATOR_URL = 'https://translate.yandex.net/api/v1.5/tr.json/translate'




class YandexTranslator:

    _languages_codes = {

        'eng': 'en',

        'rus': 'ru'

    }



    def translate_text(self, text, original_lang, expected_lang):

        data = {

            'key': YANDEX_TRANSLATOR_API_KEY,

            'text': text,

            'lang': '{}-{}'.format(self._languages_codes[original_lang], self._languages_codes[expected_lang])

        }


        return self._send_data(data)



    def _send_data(self, data):

        session = Session()

        res = session.post(YANDEX_TRANSLATOR_URL, data=data)

        res_json_text = json.loads(res.text)

        res_text = ''

        if res.status_code is not 200:

            Logger.error('{} - Bad request'.format(res.status_code))

            return res_text

        if 'text' in res_json_text:

            res_text = res_json_text['text'][0]

            return res_text

И наконец, сделаем запуск перевода по двойному клику (событие on_double_tap виджета TextInput) в область ввода текста:

translator/screens/main/main.kv

...

TextInput:

    id: input

    hint_text: 'Введите текст и щелкните два раза по полю ввода'

    font_size: 15

    padding: 10

    multiline: True

    size_hint: 1, .3

    on_double_tap: root.translate_text()

...

translator/screens/main/screen.py

...

def translate_text(self):

    text = self.ids.input.text

    translation = self.tr_engine.translate_text(text, self.original_lang[0], self.expected_transl[0])

    output_view = self.ids.output_view

    output_view.text = translation

...

Итак, приложение работает!

Использование иконок

Последним штрихом в создании переводчика будет замена надписей в меню и знака направления перевода на иконки с соответствующими значками. Самый простой способ добавить иконку в Kivy — воспользоваться шрифтом Font Awesome.

Файл шрифта будет расположен в translator/assets/fonts/fontawesome.ttf. Импортируем шрифт в приложение следующим образом:

translator/settings.py

...

KIVY_FONTS = [

    {

        "name": "FontAwesome",

        "fn_regular": "assets/fonts/fontawesome.ttf"

    },

]

...

translator/app.py

...

from settings import WINDOW_HEIGHT, WINDOW_WIDTH, KIVY_FONTS

...

def initialize_app(self):

    self.screen_manager = sm

    self.switch_screen('main')

    for font in KIVY_FONTS:

        LabelBase.register(**font)

...

Далее отредактируем элементы меню:

translator/widgets/kv/footer_menu.kv

<FooterMenuButton@Button>:

    font_name: 'FontAwesome'

    font_size: 22

    background_normal: ''

    background_color: 1, 1, 1, 1

    background_down: ''

    color: 0, 0, 0, 1



...



FooterMenuButton:

    text: "\uf1ab"

    on_press: root.switch_screen('main')



FooterMenuButton:

    text: "\uf05a"

    on_press: root.switch_screen('about')

Затем значок направления перевода:

...

def update_translation_direction(self):

    self.original_lang, self.expected_transl = self.expected_transl, self.original_lang

    self.title = '{} [font=FontAwesome]\uf07e[/font] {}'.format(self.original_lang[1], self.expected_transl[1])

...

Заключение

Приложение готово. Вот, что должно получиться после проделанной работы:

Надеюсь, вы видите то же самое! Итак, первая часть статьи и реализация минимального функционала переводчика завершена.

Ссылка на репозиторий проекта: https://github.com/nikolay-kovalenko91/translator_app_with_kivy.

Читать и комментировать
Xposed magic will be here

Сергей Опивалов

28 ноября 2016

Xposed magic will be here

Android Wear: основы разработки

Владислав Герасименко

15 ноября 2016

Android Wear: основы разработки

Skobbler maps

Марк Левковский

4 августа 2016

Skobbler maps

Работа с веб-сокетами в django за счет использования Django Channels