【 Python + Kivy で Android 】仕事の合間企画! Python で Kivy を使って GUI ストップウォッチを作って、python-for-android と Buildozer を使って Android APK にしてスマホで動かしてみた、もちろん Windows 上でも動きます、Mac? 多分、でも試してないから分からない、の巻 投稿一覧へ戻る

Tags: python , python-for-android , android , buildozer , kivy , windows

Published 2020年10月8日12:38 by T.Tsuyoshi

1: ( Android ) スマホへのインストール後、アイコン登録スクショ






2: ( Android ) アプリが立ち上がりスクショ






3: ( Android ) ストップウォッチ動作中スクショ






4: ( Windows ) ストップウォッチ立ち上げスクショ






5: ( Windows ) ストップウォッチ稼動スクショ






6: ( Windows ) ストップウォッチ稼動中ウィンドウ縦長変形スクショ






7: ( Windows ) ストップウォッチ稼動中ウィンドウ横長変形スクショ






全てのファイルは 1 つのディレクトリに入っていて、構成は以下の通りです (興味がある方は GitHub からダウンロードできます)。


button_down.png # 青ボタン (押された状態)
button_normal.png # 青ボタン (通常の状態)
red_button_down.png # 赤ボタン (押された状態)
red_button_normal.png # 赤ボタン (通常の状態)
Roboto-Medium.ttf # フォントファイル (ノーマル)
Robot-Thin.ttf # フォントファイル (細字)
moo.ico # アプリアイコンファイル (ストップウォッチとは何の関係もない牛さん)
clock.kv # Kivy ファイル
main.py # おなじみ Python ファイル



注意:

私、kivy を利用するのは今回が初めてです。


python と kivy で、うまくロジック部分と UI 部分を分けられているか、といわれれば「グチャグチャです」と答えざるを得ません。


今回はあくまでも興味本位で、前から気になっていた kivy を使ってみた、というだけですので、「何だこのコーディング」とか「なんだこの実装」などという冷たいご批判は受け付けません。


力技で、とにかくまずは動かしてみよう、ということですので、あまり参考になさらず、「こんな感じなんだ」とご覧ください。




clock.kv


<BackgroundColor@Widget>: # なんと kivy には Label などに背景色設定のためのプロパティがない。自分で用意...
background_color: 1, 1, 1, 1
canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos

<BackgroundLayout@BoxLayout+BackgroundColor>: # サブクラス定義
font_name: 'Roboto'
background_color: 1, 0, 0, 1

<WatchLabel@Label>: # サブクラス定義。Python だと class WatchLabel(Label): に相当
markup: True
font_name: 'Roboto'
on_size: app.on_size()

<WatchButton@Button>: # サブクラス定義
font_name: 'Roboto'
font_size: 25
bold: True
border: [2] * 4 # (2, 2, 2, 2) という指定の代わりにこんな書き方も可能です

ClockLayout: # ここからが実際のレイアウト (これが root 要素。python ファイル内で BoxLayout のサブクラスとして定義してます)
orientation: 'vertical'

time_prop: time # この 5 行は widget の id と pythonコード内で使用するこの widget を参照するプロパティ名との関連付け
watch_prop: stopwatch # こんなことをせずに python コード内でただ単に ids.stopwatch としても参照可能
btn_box: button_layout
start_btn: start
reset_btn: reset

WatchLabel:
id: time
text: '[b]00[/b]:00:00' # kivy におけるタグは [...] と角括弧で囲みます
# また、このようにタグを使用するときは必ず markup: True も指定します (ここではクラス内でまとめて指定)

BackgroundLayout:
id: button_layout
height: 90
orientation: 'horizontal'
padding: 20
spacing: 20
size_hint: (1, None) # このレイアウトでは通常は親レイアウト ( ClockLayout ) が垂直方向に 3 つに等分されますが、
# この BoxLayout に関しては指定した高さ ( height ) を使うために必要な設定

WatchButton: # スタート/ストップ青ボタン
id: start
text: 'Start'
background_normal: 'button_normal.png'
background_down: 'button_down.png'
on_press: app.cb_start() # 押されたときのコールバック関数の登録

WatchButton: # リセット赤ボタン
id: reset
text: 'Reset'
background_normal: 'red_button_normal.png'
background_down: 'red_button_down.png'
on_press: app.cb_reset()

WatchLabel:
id: stopwatch
text: '00:00.00'



以上が Kivy のレイアウトファイルです。


次に、このレイアウトファイルを裏で操る Python ファイルがこちら ↓


main.py


from time import strftime
import re

from kivy.app import App
from kivy.core.text import LabelBase
from kivy.core.window import Window
from kivy.resources import resource_add_path
from kivy.utils import get_color_from_hex
from kivy.clock import Clock
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout


class ClockLayout(BoxLayout):
"""
root widget を象徴するクラス
"""

time_prop = ObjectProperty(None) # 時刻を表示する Label widget
watch_prop = ObjectProperty(None) # ストップウォッチを表示する Label widget
btn_box = ObjectProperty(None) # ボタンを配置する BoxLayout widget
start_btn = ObjectProperty(None) # スタート/ストップ Button widget
reset_btn = ObjectProperty(None) # リセット Button widget


class ClockApp(App):
"""
紐付ける kivy ファイルの名前 ( clock.kv ) に App を繋ぎ合わせたクラス名 ( ClockApp ) にすることで、
Python ファイルを実行する際に対応する kivy ファイルが自動的に読み込まれるようになります
"""

stop_watch = False
total_past = 0.0

on_size_call = False
font_ratio = 0 # 初期状態での texture_size.y に対する font_size の比率
texture_ratio = 0 # 初期状態での texture_size.y に対する texture_size.x の比率
stop_watch_pat = re.compile(r'(?:\[size=\d+\])?(?P<size>\d+)(?:\[/size\])?')

def on_start(self):
"""
アプリケーションの初期化が終了した時点で呼び出されます
"""

Clock.schedule_interval(self.update, 0) # update 関数を繰り返し呼び出すようにスケジュールします
# 第 2 引数には間隔を指定。0 だと毎フレーム毎に呼び出します

self.root.btn_box.background_color = get_color_from_hex('#e6e6e6')

def update(self, nap):
"""
:param nap: 前回の呼び出しからの経過時間 (秒)。もし 60fps 動作であれば、1 (秒) / 60 (fps) = 0.0167 秒位
"""

# self.root.ids.time.text = strftime('[b]%H[/b]:%M:%S') # id を利用した widget の参照
self.root.time_prop.text = strftime('[b]%H[/b]:%M:%S') # property を利用した widget の参照
if self.stop_watch:
self.total_past += nap
minutes, seconds = divmod(self.total_past, 60)
micro_size = int(self.root.watch_prop.font_size * 0.7)
self.root.watch_prop.text = f"{int(minutes):02}:{int(seconds):02}.[size={micro_size}]{int(seconds * 100 % 100):02}[/size]"

def cb_start(self):
"""
ストップウォッチの start / stop ボタンが押されたときに呼び出される
"""

self.root.start_btn.text = 'Start' if self.stop_watch else 'Stop'
self.stop_watch = not self.stop_watch

def cb_reset(self):
"""
ストップウォッチの reset ボタンが押されたときに呼び出される
"""

self.stop_watch = False
self.total_past = 0.0
self.root.start_btn.text = 'Start'
micro_size = int(self.root.watch_prop.font_size * 0.7)
self.root.watch_prop.text = f"00:00.[size={micro_size}]00[/size]"

def on_size(self):
"""
window の大きさが変化した場合に呼び出される (正確には時計 / ストップウォッチを表示する Label widget の大きさ)
"""

if self.on_size_call:
label_x, label_y = self.root.time_prop.size

# texture_size の 8 割に対してのサイズを基にする
if (label_x / label_y) < self.texture_ratio: # x 方向に対しての比率でフォントサイズを計算
new_ratio = (label_x * 0.8) / self.texture_ratio
else: # y 方向に対しての比率でフォントサイズを計算
new_ratio = label_y * 0.8
self.root.time_prop.font_size = self.font_ratio * new_ratio
self.root.watch_prop.font_size = self.font_ratio * new_ratio

stop_txt_lst = self.root.watch_prop.text.split('.')
micro_size = int(self.root.watch_prop.font_size * 0.6)
m = self.stop_watch_pat.search(stop_txt_lst[1])
self.root.watch_prop.text = f"{stop_txt_lst[0]}.[size={micro_size}]{m.group('size')}[/size]"
box_height = int(self.root.time_prop.size[1] * 0.4)
self.root.btn_box.height = box_height
self.root.btn_box.padding = int(box_height * 0.15)
self.root.btn_box.spacing = int(box_height * 0.15)

else:
self.font_ratio = self.root.time_prop.font_size / self.root.time_prop.texture_size[1]
self.texture_ratio = self.root.time_prop.texture_size[0] / self.root.time_prop.texture_size[1]

self.on_size_call = True


if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('#ff6666')
# resource_add_path(r"C:\Windows\Fonts")
LabelBase.register(
name='Roboto',
fn_regular='Roboto-Thin.ttf',
fn_bold='Roboto-Medium.ttf'
)
ClockApp().run()



今回かなり躓いたのが on_size() の実装です。


Kivy では Label widget のフォントサイズのデフォルト値が 15 に設定されています。


まず、これをどうやって Label widget の大きさに合わせてそれなりの大きさで表示するのか、という基本部分で悩みました (絶対値指定はしたくなかったので)。


また、クロスプラットフォーム ( cross platform ) 上で動作することが魅力的なんですが、


デスクトップ環境下における利用時に顕著な「window 領域の大きさの変化」にフォントサイズ等を如何に対応させるのか、が問題でした。


今回の結論が on_size() でございます。


見ていただければ一目瞭然、ほぼ力技実装でお恥ずかしい限りです。


Android 用 APK の作成は CentOS 上 (何を隠そうこのサイトを動かしているサーバー = conoha VPS です) で行いました。


Buildozer をインストールし、python-for-android が必要としている dependencis を満たし、出来上がった APK をダウンロードし、AndroidStudio のエミュレーターでは動かなかったので実機に USB 経由でインストールし、何とか動作を確認しました。


この APK を Google Play Store で公開できるのかできないのか、よく分かりませんがちょっとやって後日報告したいと思います。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)