WPP
Kivy を Pyinstaller で Windows Exe 化するときにした諸々

久々に Kivy でプログラムをつくったのですが.Exe するときにハマったのでメモ。自分の状況にピッタリマッチする記事がなかなかなかったので誰かの役にたつかも。

Kivy で UI をつくり .mp4 ファイルの生成するアプリを作成しました。 .py から import 部分を抜き出すとこんな感じになっています。

ソースは app.py という名前です。

ざっくりとしたモジュール構成は、
Kivy のいくつかの UI 部品 (ビデオ pane を含む) を配置し、 plyer でポップアップ通知がでるというものです。

pyinstaller のインストール方法や作りたい実行ファイルと同じ OS でやる必要があるとかそういったことは特に解説しません。

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.video import Video
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.popup import Popup
from kivy.uix.filechooser import FileChooserListView

from plyer import notification

# このへんは、 pyinstaller が自動的に "よしな" にししてくれます。
import os
import platform
from typing import Callable, Optional
from dataclasses import dataclass
import json
from pathlib import Path
from typing import Optional
from enum import Enum

pyinstaller 自体はとても簡単に使えるのでおむろに pyinstall --onefile app.py してみます。app.exe と app.spec ファイルが生成されます。

が、 app.exe を実行してみるといろいろファイルが足りないようで追加していききます。

sdl2 と angle を取り込む

詳細な仕組みはよくわかっていませんが、Kivy で GUI を表示するとsdl2 か glew を使うものらしいです。先程つくった .exe は sdl を使っているようなのでそれを明示してやります。

import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'

from kivy_deps import sdl2, angle

deps = []
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]

exe = EXE(
  ...
  # [] のあたりに入れる
  *deps,
  ...
)

app.py にも KIVY_GL_BACKEND = ‘angle_sdl2’ を設定してやります。

今度は、編集した app.spec を使って pyinstaller app.spec を実行しします。 誤ってさっきと同じ app.py を指定するとせっかく変更した app.spec が上書きされるので注意してください。

gstreamer の取込み

app.exe を実行してみると、どうやら gstreamer 関係の初期化に失敗するようです。

なので app.spec に追記していきます。

ビデオ再生には、ffpyplayer が使われるという記述もみうけられるのですが、試した環境では gstreamer が使われていたので素直にそれに従います。
ffpyplayer の場合にも同じ様に import と deps に追加してやれば多分うまくいきます。

import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'

from kivy_deps import sdl2, angle

deps = []
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]
deps += [Tree(p) for p in gstreamer.dep_bins]

exe = EXE(
  ...
  # [] のあたりに入れる
  *deps,
  ...
)

再度 pyinstaller app.spec を実行する。

今度は、pywin32 のエラーが出た。pywin32 は おそらく app.py で明示的に Kivy のタイマー処理を使っている箇所がありそこで使われているっぽい。

pywin32 は今までと異なり pyinstaller から collect_all というモジュールをインポートしてこないといけないらしい。

from PyInstaller.utils.hooks import collect_all

pywin32_datas, pywin32_binaries, pywin32_hiddenimports = collect_all('pywin32')

a = Analysis(
  ...
  hidenimports = [
    'win32timezone',
    'win32api',
    'win32con'
  ] + pywin32_hiddenimports
  ...
)

でここまでの内容をまとめると app.spec はこうなった。

# -*- mode: python ; coding: utf-8 -*-

import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'

from kivy_deps import sdl2, angle, gstreamer
from PyInstaller.utils.hooks import collect_all

deps =[]
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]
deps += [Tree(p) for p in gstreamer.dep_bins]

pywin32_datas, pywin32_binaries, pywin32_hiddenimports = collect_all('pywin32')

a = Analysis(
    ['app-v18.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[
        'win32timezone',
        'pywintypes',
        'win32api',
        'win32con',
        'plyer',
        'plyer.platforms',
        'plyer.platforms.win',
        'plyer.platforms.win.notification'
    ] + pywin32_hiddenimports,
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    # set deps
    *deps,
    exclude_binaries=False,
    name='app', # exe の名前なので好きに変えていい
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

Kivy をパッキングするときは割とハマるので、次回はここをベースにしてなんとかしたい。

にしてもインストーラ系のビルドは、動作確認が面倒で、時間かかるのであまり好きにはなれない。

参考

Build with PyInstaller, cannot find proper OpenGL after lock windows with “Win+L” · Issue #7177 · kivy/kivy

KivyMD の NavigationDrawer に RecycleView を追加するサンプル

引き続き、Kivy です。

今回は、KivyMD の NavigationDrawer の中に RecycleView を突っ込んでみます。Kivy / KivyMD はここのコンポーネントを動作確認する分には特別難しいことはありませんが、複数組み合わせるとトタンになかなかいい感じに配置することができないのが難点だと思う。(自分の理解の浅さのせいかもしれないが、HTML/CSS の方がまだましに感じます。)

提示するサンプルを実行するとこんな感じになります。前回と同じように Appbar が画面上部にあり、 Appbar のアイコンを押すと、NavigationDrawer が表示されます。その NavigationDrawer の中に 果物の名前のリストを表示しています。

Appbar のアイコンが左で、ドロワーが右から出てくるのは違和感があるかもしれませんが anchor の確認のためにあえて行っています。

環境

  • Kivy 2.3.0
  • KivyMD 2.0.1.dev

コード

from kivy.lang import Builder

from kivy.properties import StringProperty

from kivymd.app import MDApp
from kivymd.uix.label import MDLabel
from kivymd.uix.recycleview import MDRecycleView


KV = '''
MDScreen:

    MDNavigationLayout:
        # size_hint_x: 1

        ScreenManager:
            MDScreen:

                MDBoxLayout:
                    md_bg_color: self.theme_cls.secondaryContainerColor
                    orientation:'vertical'
                    
                    MDTopAppBar:                        
                        MDTopAppBarLeadingButtonContainer:
                            MDActionTopAppBarButton:
                                icon: 'menu'
                                on_release: nav_drawer.set_state('toggle')
                            
                        MDTopAppBarTitle:
                            text: 'this is AppBar'
                            pos_hint: {'center_x': .5}
                            
                    MDLabel:
                        text: "I'm MDLabel"

        MDNavigationDrawer:
            anchor: 'right'

            MDBoxLayout:
                orientation: 'vertical'

                MDNavigationDrawerLabel:
                    text: 'drawer items!!'
                    icon: 'format-list-bulleted'

                MDNavigationDrawerDivider:

                NvDrawerRecycleItem:                    
                    id: rv
                    padding: 0, nav_drawer.height, 0, 0
                    viewclass: 'Food'
                    RecycleBoxLayout:
                        size_hint: (1, None)
                        height: self.minimum_height
                        orientation:'vertical'

<NvDrawerRecycleItem>:

<Food>:
    text: 'name: {}'.format(self.item)
    adaptive_size: True
    padding: 0, dp(16), 0, dp(16)

'''

class Food(MDLabel):
    item = StringProperty('')

class NvDrawerRecycleItem(MDRecycleView):
    pass


class TestNavigationDrawer(MDApp):
    def build(self):
        self.theme_cls.theme_style = 'Dark'  # or 'Light'
        return Builder.load_string(KV)
    
    def on_start(self):
        rv = self.root.ids.rv
        rv.data = (
            { 'item': it }
            for it in [
                'Apple', 'Banana', 'Cherry', 'Grape', 'Lemon',
                'Mikan', 'Orange', 'Pear', 'Lychee', 'Strawberry',
                'Peach', 'Fig'
            ]
        )


TestNavigationDrawer().run()

公式のサンプルでは MDNavigationDrawerMenu の中に Drawer の中身を入れてるのですが、そうすると RecycleView と MDNavigationDrawerMenu の間に大きな隙間ができてしまい。どうしても制御することができませんでした。

また、MDBoxLayout: orientation: ‘vertical’ で囲ってあげないと ドロワー系のアイテムと RecycleView が横並びになってしまいました。

そのため、上のようなサンプルコードになりました。


        MDNavigationDrawer:
            id: nav_drawer
            anchor: 'right'

            MDBoxLayout:
                orientation: 'vertical'

                MDNavigationDrawerMenul:
                    MDNavigationDrawerLabel:
                       text: 'drawer items!!'
                       icon: 'format-list-bulleted'

                      MDNavigationDrawerDivider:

                NvDrawerRecycleItem:                    
                    id: rv
                    padding: 0, nav_drawer.height, 0, 0
                    viewclass: 'Food'
                    RecycleBoxLayout:
                        size_hint: (1, None)
                        height: self.minimum_height
                        orientation:'vertical'

今作りたいものをやるのにドロワーの中にリスト入れればいけるんじゃね?と安直に考えてやってみたがなかなかしんどかった。Kivy の道はきびしい。

KivyMD の Appbar 画面上部にピッタリに配置する方法

さあ、今日も Kivy の話です。

KivyMD にはあたかも Android アプリのような Appbar というコンポーネントがあります。これを画面のトップに配置して下部分に大きさピッタリのコンテンツを配置する方法のメモです。

やりたいことは、2つあります。

  1. Appbar を画面トップに配置する。
  2. 空いた下の領域いっぱいにコンテンツを表示する。

結果はこんな感じになります。

画像を入れる

環境

  • Kivy 2.3.0
  • KivyMD 2.0.1.dev

最終的なサンプル

from kivy.lang import Builder

from kivymd.app import MDApp

KV = '''
MDScreen:
    MDBoxLayout:
        orientation: 'vertical'
        padding: 0,app_bar.height, 0,0      # L T R B

        MDBoxLayout:
            md_bg_color: 0.2,0.2,0.4, 1
            MDLabel:
                text: 'text label'
                valign: 'bottom'

    MDTopAppBar:
        id: app_bar
        pos_hint: {'top': 1}
        
        MDTopAppBarLeadingButtonContainer:
            MDActionTopAppBarButton:
                icon: 'menu'
                
        MDTopAppBarTitle:
            text: 'top appbar'
        
        MDTopAppBarTrailingButtonContainer:
            MDActionTopAppBarButton:
                icon: "github"
'''

class MainApp(MDApp):
    def build(self):
        self.theme_cls.theme_style = 'Dark'  # or 'Light'
        return Builder.load_string(KV)
    
if __name__ == '__main__':
    MainApp().run()

画面トップに配置

自分が試した限りでは、 TopAppBar を公式サンプルのまま配置しただけでは画面の上部に表示することはできませんでした。(これバグ何じゃないだろうか ?)

なので pos_hint に ‘top’: 1 を設定してやります。kivy では画面左下が原点になるので ‘top’: 1 は画面の最上部を意味します。(0 – 1.0 が入り割合を示すらしい。)

    MDTopAppBar:
        id: app_bar
        pos_hint: {'top': 1}
        

コンテンツを Appbar の下ギリギリまで配置する

次に、コンテンツ領域の制御です。ここはなにも制限しないと親の大きさいっぱいまで使ってしまいます。つまりコンテンツ領域が Appbar で隠れるか、 Appbar を隠すかしてしまいます。(これは .KV 内の定義の順番によるっぽいです。)

Stack overflow で見つけた解決策は padding を設定して下の領域の大きさを明示的に制限するというものでした。こんな感じです。

padding に設定してやるのは Appbar の高さなので id: 属性を追加します。

追加した id (Appbar) の高さ分 padding でコンテンツ領域を下げてやります。

    MDBoxLayout:
        orientation: 'vertical'
        padding: 0,app_bar.height, 0,0      # Left Top Right Bottom

   # いろいろ省略

    MDTopAppBar:
        id: app_bar
        pos_hint: {'top': 1}
        # size_hint_x: 1

Kivy / KivyMD は配置周りの挙動がよくわからないのが辛い。

参考

python – How do i make an MDBottomAppBar appear on top of a list view in KivyMD – Stack Overflow

Appbar — KivyMD 2.0.1.dev0 documentation

Kivy AnchorLayout が効かない時はサイズを指定すると良い

今回も Kivy ネタです。

下記のようなコードで(コメントアウトは除き) で右寄せが効かずちょいハマりました。

Stack Overflow にスレがあり、CSS のブロックエレメント/インラインエレメント のような感じでMDLabel の幅が max に設定されてしまうようです。

コメントアウトを解除すると無事に右寄せが機能しました。

Label を MDLabel にすると別のスタイルが有効になるようで文字が見切れてしまいます。その場合は size を texture_size から変更する必要があるようです。ここは追求していません。

from kivy.lang import Builder
from kivymd.app import MDApp

KV = '''
MDScreen:
    MDBoxLayout:
        orientation: 'vertical'
        
        AnchorLayout:
            # size_hint_x: 1
            anchor_x: 'right'
            anchor_y: 'center'
    
            Label:
                text: 'menu-'

                # この2行を追加すれば OK
                # size_hint: (None, None)
                # size: self.texture_size
                max_lines: 1                    
                padding: (dp(16), dp(0))
'''

class MainApp(MDApp):
    def build(self):
        self.theme_cls.theme_style = 'Dark'  # or 'Light'
        return Builder.load_string(KV)
    

if __name__ == '__main__':
    MainApp().run()

参考

python – Kivy AnchorLayout .Kv file – Stack Overflow

Kivy のログレベル変更

さて、今日も Kivy です。 Kivy 戦記ってブログがあるんですが、Kivy をやってる時はまさに「戦い」って感じでいちいち面倒くさいです。

Kivy をコンソールから実行するとログが流れてきます。そのログレベルを変更しようと思います。
というのも、.kv ファイルの記述どおりにどうも動かない現象にぶち当たりましたがデフォルトのログレベルではなにも異常はなさそうだったのでもう少し詳しく見る必要がありました。

どうやら Kivy.config にコンフィグ設定値があるようです。
ログレベルに限らず、Config は環境変数で渡せるようですが、設定ファイルを使ってみます。

自分の環境ではそもそも設定ファイルは、存在しませんでした。おそらくそれが普通のようです。Config のドキュメントから設定値読み取っていちいち手入力するのはだるいなと思っていたら、ファイルに書き出す機能がありました。

$ python -m kivy
>>> from kivy.config import Config as cfg
>>> cfg.write()

こうすると ~/.kivy/config.ini が生成されているはずです。Win/Mac もそれっぽいところにあります。

それはそうと config.ini を書き換えていきます。log 周りの設定だけでなくデフォルトフォントの設定など色々ありそうですが、今回はつかわないので log_level を debug に変更します。

info や debug 以外にもドキュメントにある通り warn, error, trace なんかも設定できるようです。

[kivy]
..... いろいと並んでいる
log_dir =
log_enable =1
log_level = debug
....

にしても Kivy の API (クラス) リファレンスは、キーワードと説明の区別がつけにくくて読み取りにくいのがどうも好きになれない。

参考

Configure Kivy — Kivy 2.3.0 documentation

Configuration object — Kivy 2.3.0 documentation

pipenv で setup.py のパッケージをインストールする方法

pipenv の仮想環境で setup.py をインストールする方法です。

KivyMD の最新を git から持ってくると setup.py でのインストールを求められますが、やり方を知らなかったのでメモします。

pipenv の公式に載っているのですが run pip install すればいいらしい。

$ pipenv run pip install -e .

ちなみに、git で取ってきた KivyMD の example を動作させるには、プロジェクトのルートで python example/xxx.py のように実行するか、PYTHONPATH に追加する必要がある。インストールしても仮想環境にコピーされないので注意。

ついでに requirements.txt でインストールする場合は以下のようになる

$ pipenv install -r ./requirements.txt

参考

pipenv · PyPI

kivymd/KivyMD: KivyMD is a collection of Material Design compliant widgets for use with Kivy