slideshow using kivy

Kivyを用いて画像のスライドショーを実装したので備忘録として。

今回の仕様は

- 指定したディレクトリ内の画像をスライドショーとして表示
- ディレクトリをネットワーク上にマウントして、ディレクトリ内の画像ファイルが更新されたら動的にスライドショーに追加

執筆時点では1つ目の仕様を満たせている。
~~2つ目が完了した際はこの記事に追記する。~~
全仕様を満たしたのでプログラム上に追記した。

##ディレクトリを指定し、画像をスライドショーとして表示する。画像ディレクトリが更新されたらダイナミックにスライドに追加する。

プログラムは以下の様になった。

```python:追記前
import os
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen, ScreenManager

class MyImage(Image):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

class MyScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

class MyScreenManager(ScreenManager):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def slide(self, dt):
        #self.next()で次のScreenの名前を取得し、currentプロパティに代入。この処理を実行することでウィンドウに表示されている画像が変更される。
        #変更される際のアニメーションは、ScreenManagerのデフォルト設定のSlideTransitionになる。
        self.current = self.next()

class MyBoxLayout(BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        #指定したディレクトリ(imagesディレクトリ)内のファイルを取得
        images = [f.name for f in os.scandir('./images') if f.is_file()]
        sm = MyScreenManager()
        #取得したファイルリストから1ファイルずつ、絶対パスを取得しImageWidgetを作成。ScreenWidgetに追加し、ScreenWidgetをScreenMangerWidgetに追加。
        for image in images:
            img = MyImage(source=os.path.abspath('./images/'+image))
            sc = MyScreen(name=image)
            sc.add_widget(img)
            sm.add_widget(sc)
        #childrenを使うより楽なので、sm(screen manager)プロパティに、作成したScreenManagerオブジェクトを代入。
        self.sm = sm
        self.add_widget(sm)

class MyApp(App):
    def on_start(self):
        #5秒おきに、表示されている画像を次の画像に切替える処理を実行する。
        slide_proc = Clock.schedule_interval(self.bl.sm.slide, 5)
        slide_proc()

    def build(self):
        bl = MyBoxLayout()
        #self.sm = sm と同じ理由でblプロパティにオブジェクトを代入。
        self.bl = bl
        return bl

if __name__ == '__main__':
    app = MyApp()
    app.run()
```

全仕様を満たしたプログラムは以下の様になった。

```python:完成ver
import os
import glob
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen, ScreenManager

class MyImage(Image):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

class MyScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

class MyScreenManager(ScreenManager):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def slide(self, dt):
        #現在の要素番号を取得し、残りの要素数を計算する。
        current_index = self.screen_names.index(self.current)
        remaining_index = len(self.screen_names) - current_index
        #ここでhashの計算をし、比較をするのもアリ(今回はスライドショーのループを終える度に毎回最新の画像を取得している)
        #残りの要素数が1(現在の要素が一番最後の要素だった場合)
        if remaining_index == 1:
            #BoxLayoutのinitと同じ処理
            images = glob.glob('./images/*.jpg') + glob.glob('./images/*.jpeg') + glob.glob('./images/*.png')
            for image in images:
                img = MyImage(source=image)
                sc = MyScreen(name=image)
                sc.add_widget(img)
                self.add_widget(sc)
            #前のループのScreenを消す前に、次のループの最初の画像を表示
            self.current = self.next()
            #前のループに使ったScreenを全削除
            self.clear_widgets(self.screens)
        #残りの要素がまだある時
        else :
            self.current = self.next()

class MyBoxLayout(BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        #globを使う事で引数に渡したパスにマッチしたファイルのみ取得する。元のコードだと、画像ファイル以外も取得したり、macなら.DS_Storeが入り込む事もあるため。
        images = glob.glob('./images/*.jpg') + glob.glob('./images/*.jpeg') + glob.glob('./images/*.png')
        sm = MyScreenManager()
        for image in images:
            img = MyImage(source=image)
            sc = MyScreen(name=image)
            sc.add_widget(img)
            sm.add_widget(sc)
        self.sm = sm
        self.add_widget(sm)

class MyApp(App):
    def on_start(self):
        slide_proc = Clock.schedule_interval(self.bl.sm.slide, 5)
        slide_proc()

    def build(self):
        bl = MyBoxLayout()
        self.bl = bl
        return bl

if __name__ == '__main__':
    app = MyApp()
    app.run()
```
ScreenManagerのslide関数と、BoxLayoutのinit関数内でやってる画像追加処理は重複しているので関数化することが推奨されています。
[重複の排除](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)

##課題(解決済み)
1. ~~imagesディレクトリに画像以外のファイルが追加された時の例外処理を組み込めていない。~~
2. ~~仕様の2つ目を満たせていない。~~

~~1つ目の課題については、globやtry-catch等を用いて`.png`や`.jpg`といった画像ファイルとして扱える拡張子のモノのみ取得するようにしたり、Pillowを使うと上手いこと処理してくれるかを検証。
2つ目の課題に至ってはコードは汚いが動く実装、すら出来ていない状態。
考えられる手順としては、
1. `MyBoxLayout`内の`__init__`関数で行っているツリーの構成処理と同じ様に、取得した画像リストを1巡するたびにディレクトリを見て、更新されていればもう一度widgetの追加や削除を行う。(更新の有無はハッシュを計算し比較を行う等する)
がある。~~

##参考文献
[Kivyプログラミング ―Pythonで作るマルチタッチアプリ― (実践Pythonライブラリー)](https://www.amazon.co.jp/Kivy%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E2%80%95Python%E3%81%A7%E4%BD%9C%E3%82%8B%E3%83%9E%E3%83%AB%E3%83%81%E3%82%BF%E3%83%83%E3%83%81%E3%82%A2%E3%83%97%E3%83%AA%E2%80%95-%E5%AE%9F%E8%B7%B5Python%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%83%BC-%E5%8E%9F%E5%8F%A3-%E5%92%8C%E4%B9%9F/dp/4254128967)