LoginSignup
22
21

Kivy小ネタ集(随時更新)

Last updated at Posted at 2018-02-10

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))

11.png
windowの幅を縮めると
22.png

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)

Youtube

on_touch_down時の正確な位置が知りたければposではなくopos

ScrollViewDragBehaviorのような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に表れる文字列を全て大文字にしようとして以下のように書き換えると

import文は省略
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)

無事bindingが行われます。
Screenshot at 2019-03-27 21-23-59.png

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が行われます。
Screenshot at 2019-03-27 21-23-59.png
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を使う事を薦めます。

install方法
$ pip install kivymd

Animationは複数の物を同時にアニメーションできる

anim = Animation(...)
anim.start(widget1)
anim.start(widget2)

のような事をしても大丈夫です。

SequenceとParallelはBugの宝庫

Animation同士を足した時に作られるSequenceとAnimation同士を&演算した時にできるParallelはかなりBugが多いです。それらを直すPullRequestを出してはいますが、まだ採択されていません。 最新版では多くのbugが修正済みです。

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_distancesp 以上動いた時のみ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_MAXFPS0に設定することでも可能で、自動テストの際に役に立つ。

よく見かける間違い1 Builder.load_string()及びBuilder.load_file()の戻り値を受け取っていない

Kv言語には2種類のwidgetの定義方法があり、それはroot ruleclass 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

上のような書き方をしてしまうと、何も怒られないままtitlepriceが本当に只の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)
error
 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で見てみると

Screenshot at 2018-02-11 00-18-36.png

「textt」というpropertyが出来ているのが確認できます。[既存のpropertyへの値の設定]と[新しいpropertyの作成]は異なる文法の方が良かったと思います。

Appクラスの名前から自動でKvファイルを読みに行く仕組みを使うべきか

Kivyのdocumentationを読んでいると最初の方でこの仕組みを教えられるのですが、個人的にはこれは使わなくてもいいかなという感じです。以下のようにこの仕組みを使った場合と

main.py
from kivy.app import App

class TestApp(App):
    pass

TestApp().run()
test.kv
FloatLayout:
    Label:
        text: 'Hello World'

以下のように使わなかった場合とだと

main.py
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()
main.kv
# test.kvと同じ

どっちが良いかですが、私は後者の方が何をやっているのか分かり易いので好きです。また以下のように

main.py
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図

Untitled Diagram.png

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配列、arraybytesbytearray等があるが、どうやら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配列やarraybytearrayを渡してやれば無駄な一時copyを作らずに済むよということらしい。

まとめ

cythonを扱えない人でもnumpyを使って効率的な描画計算ができそうだと分かった。

22
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
21