PyInstallerを使う際に必要そうな事
私はPyInstallerを使ったことがないので動作未確認ですがよく見かけるので貼っておきます。
if not sys.stderr:
os.environ['KIVY_NO_CONSOLELOG'] = '1'
from kivy.resources import resource_add_path
if getattr(sys, 'frozen', False):
resource_add_path(sys._MEIPASS)
markupのtagがあるとそこが優先的に折り返されてしまう
from kivy.app import runTouchApp
from kivy.lang import Builder
KV_CODE = '''
Label:
markup: True
text: '[color=#00FF00]Kivy[/color]は楽しい'
text_size: self.width, None
font_size: 50
font_name: 'yomogifont.otf'
'''
runTouchApp(Builder.load_string(KV_CODE))
software keyboardがTextInputを覆ってしまって入力中の文字が見えなくなる問題への対処
from kivy.core.window import Window
Window.softinput_mode = 'below_target'
mouseのbuttonを押し下げていない時にもcursorの座標を得たい時はWindow.mouse_pos
(2023/10/03 追記) 現在はEvent Managerという物を用いるのが正攻法のようです。
多分desktop環境限定ですがWindow.mouse_pos
にはbuttonを押し下げているかどうかに関わらず常にmouse cursorの座標が入っているようです。注意点として座標はWindow座標系で表されているので、widgetと衝突判定したいなら例えば以下のように座標変換が必要です。
from kivy.core.window import Window
def on_mouse_pos(window, mouse_pos):
if some_widget.collide_point(*some_widget.parent.to_widget(*mouse_pos)):
print('衝突しました')
Window.bind(mouse_pos=on_mouse_pos)
on_touch_down
時の正確な位置が知りたければpos
ではなくopos
を
ScrollView
やDragBehavior
のようなdrag操作を受け付けるwidget/behaviorがwidget階層の中にある時、on_touch_down
時のtouch.pos
が正しい値ではなくなる場合があります。なのでon_touch_down
時は常にopos
を利用するようにしておくと安全です。
__init__()
内でKv言語で書いた物が使える保証はない
以下のように定義されたwidgetがある時、
class MyWidget(Widget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.ids.label.text = 'Hello' # A
Builder.load_string('''
<MyWidget>: # B
Label: # B
id: label # B
''')
このwidgetを以下のように作ることはできますが
widget = MyWidget()
以下のようには作れません。
widget = Builder.load_string('''
Widget:
MyWidget:
'''
後者ではA行が実行された時にはBがまだ適用されていない為です。そしてこの問題に対応する為には以下のように書き換える必要があります。
class MyWidget(Widget):
def on_kv_post(self, *args, **kwargs):
super().on_kv_post(*args, **kwargs)
self.ids.label.text = 'Hello'
on_kv_post
はeventとして実装されているのでKv言語内で以下のように使う事もできて便利です
何かのWidget:
何かのWidget:
id: hoge
何かのWidget:
何かのWidget:
on_kv_post: print(hoge)
propertyのbindingが行われる条件
bindingがうまくいかない例
以下のコードはTextInput
に書いた文字列がそのままLabel
に表れるだけの単純な物ですが
from kivy.lang import Builder
from kivy.app import runTouchApp
layout = Builder.load_string('''
BoxLayout:
TextInput:
id: ti
Label:
text: ti.text
''')
runTouchApp(layout)
例えばLabel
に表れる文字列を全て大文字にしようとして以下のように書き換えると
layout = Builder.load_string('''
BoxLayout:
TextInput:
id: ti
Label:
text: ti.text.upper() # 変更点
''')
runTouchApp(layout)
bindingは行われずTextInput
に何を書こうともLabel
には何も表れなくなります。
これはKv言語の解析器が、ドットで連なった識別子の末端がKivyのpropertyである場合でのみbindingを行うからと思われます。(ti.text
はKivyのpropertyですがti.text.upper
は関数なのでbindingされなかった)。
対策
なので以下のように末端をtext
にしてしまえば
layout = Builder.load_string('''
BoxLayout:
TextInput:
id: ti
Label:
text: str.upper(ti.text)
''')
runTouchApp(layout)
spaceを使った技
面白いのが、Pythonでは a = 'aaa'.upper()
を a = 'aaa' .upper()
という風にspaceを置く書き方も許されているわけですが、これを利用した場合でも
layout = Builder.load_string('''
BoxLayout:
TextInput:
id: ti
Label:
text: ti.text .upper()
''')
runTouchApp(layout)
bindingが行われます。
spaceを置いた為に、解析器はtext
が末端であると思い込みbindingしてしまったのでしょう。
文字列リテラル内はbindingの対象外
Python3.6で導入されたf-strings
を使うと
layout = Builder.load_string('''
BoxLayout:
TextInput:
id: ti
Label:
text: f'{ti.text}'
''')
runTouchApp(layout)
bindingされません。
ただ代わりにstr.format()
を使えば解決できます。
(このPRによって開発版ではf-string内でもbindingが行われます。)
KivyMDの公式repositoryを使ってはいけない
KivyMDの公式repositoryはGitlabにあるこれで、もう更新はされてなくていくつか不具合が残ったままになっています。なので代わりに開発が盛んなこのrepositoryを使う事を薦めます。
$ pip install kivymd
Animationは複数の物を同時にアニメーションできる
anim = Animation(...)
anim.start(widget1)
anim.start(widget2)
のような事をしても大丈夫です。
SequenceとParallelはBugの宝庫
Animation同士を足した時に作られる 最新版では多くのbugが修正済みです。Sequence
とAnimation同士を&演算した時にできるParallel
はかなりBugが多いです。それらを直すPullRequestを出してはいますが、まだ採択されていません。
gardenのmoduleをprojectのdirectory内に含めてしまう方法
普通にgarden install xxxx
とするとinstall先は<ユーザーのホームディレクトリ>/.kivy/garden
になり仮想環境による分離がなされませんが、garden install --app xxxx
とするとinstall先は<カレントディレクトリ>/libs/garden
になり<カレントディレクトリ>
内の.py
を立ち上げた時にのみ利用可能な状態になります。なのでproject毎に分離された環境へgardenをinstallできるわけです。
アプリのデータの保存先
はApp.user_data_dirを使うのが便利です。
Android向けへのPackagingに関して
Kivyはマルチプラットホームを謳っていますが、いくつか注意点があり
- AndroidのAPKを作るにはLinuxが要り(追記: MacOSXでも可能なようです)
- iOS向けへのPackagingにはMacOSXが要ります
私はMaxOSXは持っていないのでiOS向けへのPackagingに関しては分かりませんが、少なくともAndroid向けへのPackagingは地獄のように難しいです。又、Androidに持って行く場合は使えるPythonのVersionの選択肢も狭いです。一応私が成功しているのは今の所 2.7.2 と 3.5.0 だけで、3.6系ではまだ一度もうまくいってません。なのでもしAndroidへ持っていく予定があるなら、簡単なHelloWorldでいいので自分がどのversionのPythonでのPackagingに成功するか確かめた上で、Projectで採用するPythonのversionを決めるといいと思います。
また使えるPythonのversionの選択肢が狭いです。現在(2019/03/22)もしPython3系を使いたいならPython3.7.1の一択です。なので開発PC側でも3.7.1を使うべきだと思います。
kivy.config
kivy.config.Config
を使うことで、Kivyの様々な設定をいじる事ができます。例えば描画の最大フレームレートを20にしたいなら
from kivy.config import Config
Config.set('graphics', 'maxfps', 20)
とします。設定できる値の一覧は<ユーザーのホームディレクトリ>/.kivy/config.ini
を見るとわかります。以下が私のconfig.ini
の一部で
[graphics]
display = -1
fullscreen = 0
height = 600
left = 0
maxfps = 40
multisamples = 2
position = auto
rotation = 0
show_cursor = 1
top = 0
width = 800
resizable = 1
(略)
[modules]
これを見て自分が作るアプリに適した設定が行えます。
from kivy.config import Config
Config.set('modules', 'inspector', '') # Inspectorを有効にする
Config.set('graphics', 'width', 1280) # Windowの幅を1280にする
Config.set('graphics', 'height', 720) # Windowの高さを720にする
Config.set('graphics', 'maxfps', 20) # フレームレートを最大で20にする
Config.set('graphics', 'resizable', 0) # Windowの大きさを変えられなくする
もちろんconfig.ini
自体を書き換える事もできますが、その場合はそのユーザーが実行するすべてのKivyプログラムに影響を及ぼすので注意してくだい。また幾つかのmoduleは初期化の際にこの設定値を参照しているので、ちゃんと設定値を反映させたいなら必ず 他のKivy関係のmoduleのimportに先立って書き換えてください 。
from kivy.config import Config
Config.set(...)
Config.set(...)
# ここで他のKivy関係のmoduleをimport
私が書き換えた事のある設定値
log関係
Android端末で動くアプリケーションのlogはlogcat
を使って見るのが普通だと思いますが、訳あってそれが使えない時は以下のようにして実機上にlogを書き出させています。
Config.set('kivy', 'log_dir', '/mnt/sdcard/kivy_logs')
Config.set('kivy', 'log_enable', 1)
また原因不明のBugに悩まされた時は一時的にlogのレベルをあげています。
Config.set('kivy', 'log_level', 'debug') # または'trace'
graphics関係
Config.set('graphics', 'fullscreen', 1) # Fullscreen
Config.set('graphics', 'width', 1280) # Windowの幅
Config.set('graphics', 'height', 720) # Windowの高さ
Config.set('graphics', 'maxfps', 20) # 最大フレームレート
Config.set('graphics', 'resizable', 0) # Windowの大きさを変えられなくする
上の5つの設定値の内、maxfps
以外はAndroidでは意味をなさないことに注意してください。Android上では(おそらくiOSもですが)常にモニター全体を使ったfullscreenで実行されるからです。(この理解はもしかすると間違ってるかもしれないので、いつか調査するかもしれません。)
Scroll関係
import kivy.utils
if kivy.utils.platform in ('linux', 'win', 'macosx', ):
Config.set('widgets', 'scroll_timeout', 400) # 既定値は250
# Config.set('widgets', 'scroll_distance', 20) # 既定値は20
ScrollView
はこの値を使って通常のtouchとscrollを区別しています。具体的には、画面に指を触れてから(mouseのボタンを押し下げてから)scroll_timeout
ミリ秒以内にscroll_distance
sp 以上動いた時のみscrollとして認識されます。またDragBehavior
もこの値を通常のtouchとdragの区別に用いています。
上記のコードのように実行環境がLinux,Windows,MacOSXである時のみ(つまりはdesktop環境である時のみ)scroll_timeout
を増やしている理由ですが、私が利き手じゃない方の手でmouseを操作しているせいかよくscrollに失敗して通常のtouchになってしまうからです。
modules
Config.set('modules', 'inspector', '')
Config.set('modules', 'touchring', 'image=好きな画像.png')
この値についてはこの記事を参照してください。
KCFG_ 環境変数
上で述べたconfigの値は環境変数からもいじれる。例えばConfig.set('graphics', 'maxfps', 0)
は環境変数KCFG_GRAPHICS_MAXFPS
を0
に設定することでも可能で、自動テストの際に役に立つ。
よく見かける間違い1 Builder.load_string()及びBuilder.load_file()の戻り値を受け取っていない
Kv言語には2種類のwidgetの定義方法があり、それはroot rule
とclass rule
です。書いたKv言語のコードにroot rule
が含まれている場合、それを読み取ったBuilder.load_string()
及びBuilder.load_file()
はroot rule
で定義されたwidgetのinstanceを返すのですが、それを受け取らずに捨てている人をよく見かけます。結果root rule
に書いたものが全て無駄になります。
よく見かける間違い2 Kivyのpropertyをclass属性として扱っている
Kivyのpropertyは以下のように
from kivy.event import EventDispatcher
from kivy.properties import StringProperty, NumericProperty
class Book(EventDispatcher):
title = StringProperty()
price = NumericProperty()
class属性の位置で作るせいなのか、そのままclass属性として使っている人をときどき見ます。
Book.title = 'Fluent Python'
Book.price = 5000
print(Book.title) # => Fluent Python
print(Book.price) # => 5000
上のような書き方をしてしまうと、何も怒られないままtitle
とprice
が本当に只のclass属性に置き換わってしまうので危険です。必ずinstance属性として扱うべきです。
book1 = Book(title='Fluent Python', price=5000)
book2 = Book(title='Dive Into Python 3', price=100)
print(book1.title) # => Fluent Python
print(book1.price) # => 5000
print(book2.title) # => Dive Into Python 3
print(book2.price) # => 100
詳しくはこの記事に書きました。
間違ったkeyword引数を渡した時のerrorが不親切
Python3を使っている場合、上で定義したBookに渡すkeyword引数を以下のように打ち間違えると
book = Book(title='Hoge', plice=5000)
Traceback (most recent call last):
File "/somewhere/main.py", line 21, in <module>
book = Book(title='Hoge', plice=5000)
File "kivy/_event.pyx", line 254, in kivy._event.EventDispatcher.__init__ (/tmp/pip-build-1ulcgpqm/kivy/kivy/_event.c:4922)
TypeError: object.__init__() takes no parameters
と怒られます。この object.__init__() takes no parameters
が出たら十中八九渡すkeyword引数のkeyが間違っています。またPython2だとこれに対して何も怒らないため、さらにたちが悪いです。
widgetのクラス名は大文字から始め、Kivyのproperty名は小文字かアンダーバーで始める
この命名規則はKv言語を用いない場合は守らなくても動くようですが、用いる場合は強制です。理由はKv言語の解析器がそれでpropertyとwidgetを区別しているからです。
Kv言語内でのproperty名の打ち間違いには何もerrorを出してくれない
以下のように「text」を「textt」と打ち間違えても
from kivy.app import runTouchApp
from kivy.factory import Factory
from kivy.lang import Builder
root = Builder.load_string(r'''
Button:
textt: 'Hoge'
''')
runTouchApp(root)
何も怒ってくれません。これは__存在しないproperty名に出会ったら新たなpropertyの作成とみなすというKv言語の(個人的にあまり良くないと思う)仕様__によるものです。実際inspectorで見てみると
「textt」というpropertyが出来ているのが確認できます。[既存のpropertyへの値の設定]と[新しいpropertyの作成]は異なる文法の方が良かったと思います。
Appクラスの名前から自動でKvファイルを読みに行く仕組みを使うべきか
Kivyのdocumentationを読んでいると最初の方でこの仕組みを教えられるのですが、個人的にはこれは使わなくてもいいかなという感じです。以下のようにこの仕組みを使った場合と
from kivy.app import App
class TestApp(App):
pass
TestApp().run()
FloatLayout:
Label:
text: 'Hello World'
以下のように使わなかった場合とだと
from kivy.app import App
from kivy.lang import Builder
class TestApp(App):
def build(self):
# 注意: kvファイルの名前を"test.kv"以外にしないといけない。
# もし"test.kv"のままだと此処で手動で読み込むのに加えてAppクラスが自身の名前から自動で読み込むため
# 二重に読み込まれてしまう。
return Builder.load_file('./main.kv')
TestApp().run()
# test.kvと同じ
どっちが良いかですが、私は後者の方が何をやっているのか分かり易いので好きです。また以下のように
from kivy.app import App
from kivy.lang import Builder
KV_CODE = r'''
FloatLayout:
Label:
text: 'Hello World'
'''
class TestApp(App):
def build(self):
return Builder.load_string(KV_CODE)
TestApp().run()
.pyファイルの中に.kvの中身を含めてしまう手もあり、これもパッと見て何をしているのか分かり易いので好きです。ただこの方法だと使っているEditorがKv言語の色付けに対応していても使えないという短所があります。
animationできる物の最低条件
(ここでいう「animationできる物」とは「kivy.animation.Animation
でanimationできる物」という意味です)
公式の説明を読むとwidgetをanimationする例しか載ってないのでwidget専用の物と思いがちですが、実はその適用範囲は広いです。その例の一つがこれで、描画命令classのinstanceをanimationの対象にしているのが分かります。この「animationできる物」は突き詰めていくと以下のようになります。
以下のようなkivy.animation.Animation
のinstanceがある時、
from kivy.animation import Animation
anim = Animation(value=1)
これのanimation対象としての最低条件を満たす物は
from kivy.event import ObjectWithUid
class Minimum(ObjectWithUid): # A
pass
anim_target = Minimum()
anim_target.value = 0 # B
です。**ObjectWithUid
のsubclassで(A行)かつanimation対象の属性(又は属性のように読み書きできる物)であるvalueを持っていれば(B行)**何でもいけます。実際に以下のコードで確かめてみると
from kivy.animation import Animation
from kivy.app import App
from kivy.factory import Factory
from kivy.event import ObjectWithUid
class Minimum(ObjectWithUid):
def __init__(self):
self._value = 0
def _get_value(self):
print('_get_value', self._value)
return self._value
def _set_value(self, value):
print('_set_value', value)
self._value = value
value = property(_get_value, _set_value)
class TestApp(App):
def build(self):
return Factory.Widget()
def on_start(self):
self._anim_target = anim_target = Minimum()
Animation(value=1, step=0.5, duration=3).start(anim_target)
TestApp().run()
ちゃんとanimationしている事が確認できます。
_get_value 0
_set_value 0.0
_set_value 0.1662893669999903
_set_value 0.3324920220002241
_set_value 0.49867277366683993
_set_value 0.6648306916667934
_set_value 0.8310590653333444
_set_value 0.9975033390001045
_set_value 1.0
Class図
GridLayoutの列数を算出
GridLayoutの子の幅が揃っている場合は以下のやり方で子が収まるだけの列数を計算できる。
ScrollView:
GridLayout:
size_hint: None, None
size: self.minimum_size
cols:
max(self.parent.width - self.padding[0] - self.padding[2] + self.spacing[0]) // (self.spacing[0] + 子の幅)), 1)
長くて読みづらいがPython3.8を使っていれば代入式を使って以下のように読みやすくできる。
ScrollView:
GridLayout:
size_hint: None, None
size: self.minimum_size
cols:
(p := self.padding, s := self.spacing, ) and \\
max(((self.parent.width - p[0] - p[2] + s[0]) // (s[0] + 子の幅)), 1)
またKivy2.0.0からはGridLayoutは縦方向を先に埋めることもできるので、その場合は以下のように行数を求められる。
ScrollView:
GridLayout:
size_hint: None, None
size: self.minimum_size
orientation: 'tb-lr' # 縦から埋める。tbはtop-to-bottom、lrはleft-to-rightの意。
cols:
(p := self.padding, s := self.spacing, ) and \\
max(((self.parent.height - p[1] - p[3] + s[1]) // (s[1] + 子の高さ)), 1)
効率的な描画計算
pythonにはbuffer protocolという物がある。これはデータがメモリ上で連続して並んでいるような物、つまりC-levelで以下のような単純なindexingでデータにアクセスできる物が従っているprotocolだったはずだ(私の記憶が正しければ)。
float data[N];
for (i=0;i<N;i++){
data[i] = ...
}
これに従っている物としてnumpy配列、array、bytes、bytearray等があるが、どうやらKivyにはこのような物を用いる事で描画過程に於ける無駄な一時データの生成を抑える仕組みがあるようなのである。それが以下の二つでだ。
Mesh
Changed in version 1.8.1: Before, vertices and indices would always be converted to a list, now, they are only converted to a list if they do not implement the buffer interface. So e.g. numpy arrays, python arrays etc. are used in place, without creating any additional copies. However, the buffers cannot be readonly (even though they are not changed, due to a cython limitation) and must be contiguous in memory.
訳すと
Kivy1.8.1より前は頂点データとindexデータは常にlistに変換されていたが、以降はそれらがbuffer interfaceを備えていない時のみ変換される。なのでnumpy配列やarrayは無駄な変換を生じさせずに使える。この時Kivy自体が中身を書き換えないにも関わらずそれらは読み取り専用であってはならないし、メモリ上で連続して並んでいなければならない。
のような感じになると思いますが、最後の「メモリ上で連続して並んでいなければならない」の意味は良く分からない。buffer protocolに従っている時点で連続して並んでいる事は保証されているのでは?もしかするとnumpyでいう
numpy配列[真偽値の入ったnumpy配列]
のような要素の取捨選択をすると"必要な"要素は連続して並んでいないから駄目って言うことなのだろうか?この辺は実際に試してみる必要がありそうだ。
Texture
Since 1.9.0, you can blit data stored in a instance that implements the python buffer interface, or a memoryview thereof, such as numpy arrays, python array.array, a bytearray, or a cython array. This is beneficial if you expect to blit similar data, with perhaps a few changes in the data.
When using a bytes representation of the data, for every change you have to regenerate the bytes instance, from perhaps a list, which is very inefficient. When using a buffer object, you can simply edit parts of the original data. Similarly, unless starting with a bytes object, converting to bytes requires a full copy, however, when using a buffer instance, no memory is copied, except to upload it to the GPU.
これも大体似たようなことを言っていて、要するにTexture_blit_buffer()
にnumpy配列やarrayやbytearrayを渡してやれば無駄な一時copyを作らずに済むよということらしい。
まとめ
cythonを扱えない人でもnumpyを使って効率的な描画計算ができそうだと分かった。