KivyでのImageの更新

先日(昨日)、PythonでGUIを作らなきゃいけなくなって調べてみたら、
Kivyってのがいいよ
的な記事を見つけたので、Kivyを使ってみることに。

[ここ](https://qiita.com/dario_okazaki/items/7892b24fcfa787faface)とかを参考[^1]にさせていただいて色々やってみたのですが、
[^1]: すごくわかりやすかったです。ありがとうございます。@dario_okazakiさん

画像の更新に苦しんだので、そのメモ。

固定画像を表示させたり、ボタンが押されたら画像を差し替えるのは見つかったが、
プログラム上で、あるタイミングで切り替えるってのがなかなかできなかった&見つからなかった。

# 画像の表示 
これは、簡単。こんな感じ。

```ImageApp.py
from kivy.app import App
from kivy.properties import StringProperty
from kivy.uix.widget import Widget


class ImageWidget(Widget):
    image_src = StringProperty('')

    def __init__(self, **kwargs):
        super(ImageWidget, self).__init__(**kwargs)
        self.image_src = './image.png'
        pass


class ImageApp(App):
    def __init__(self, **kwargs):
        super(ImageApp, self).__init__(**kwargs)
        self.title = 'Image Slide'

    def build(self):
        return ImageWidget()


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

```image.kv
#:kivy 1.10

<ImageWidget>:
    canvas.before:
        Color:
            rgb: 1,1,1
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        size: root.size
        orientation: 'vertical'

        Label:
            text: root.image_src
            color: 0, 0, 0, 1
            font_size: 20
            size_hint_y: 0.1

        Image:
            size_hint_y: 0.9
            allow_stretch: True
            source: root.image_src
```
これを実行すると、画像とその上に画像ファイル名が表示されるGUIが出る。

# 画像の更新
ここで、プログラム上で表示する画像を変更するようなアプリケーションにしたいとする。
たとえば、
あるディレクトリ内にある画像を順番にスライドショーするようなアプリケーション

## 最初のソース (問題点あり)
上記のアプリケーションを実現しようとしてこんな感じのソースを書いた。

```ImageSlideApp_1.py
from kivy.app import App
from kivy.properties import StringProperty
from kivy.uix.widget import Widget

import os
import glob
import random
import threading
from time import sleep


class ImageSlideWidget(Widget):
    image_src = StringProperty('')

    def __init__(self, **kwargs):
        super(ImageSlideWidget, self).__init__(**kwargs)
        self.image_src = './image.png'
        pass

    def set_image_src(self, src):
        self.image_src = src


class ImageSlideApp(App):
    def __init__(self, **kwargs):
        super(ImageSlideApp, self).__init__(**kwargs)
        self.title = 'Image Slide'

    def build(self):
        self.widget = ImageSlideWidget()
        return self.widget

    def set_image_src(self, src):
        self.widget.set_image_src(src)


def slides(app, a):
    dir_name = "./images"
    files = glob.glob(os.path.join(dir_name, "*.png"))

    while True:
        random.shuffle(files)
        for file_name in files:
            sleep(1)
            app.set_image_src(file_name)


def app_run(app, a):
    app.run()


if __name__ == '__main__':
    app = ImageSlideApp()

    th_app = threading.Thread(target=app_run, args=(app, 0), daemon=True)
    th_app.start()

    sleep(1)

    th_slide = threading.Thread(target=slides, args=(app, 0), daemon=True)
    th_slide.start()

    input('Pless any key.\n')
```

```imageSlide.kv
  ~ image.kvとクラス名以外同じなので省略 ~
```

とりあえず、Threadの使い方や、構造がいいかどうかは置いておいて、これで動きそうかなという感じで書いてみたが、
最初に、`./image.png`が表示され、1秒ごとに、画像ファイル名は、更新されるようになった。
しかし、画像はちゃんと更新されず、真っ黒になった。
(おそらく、ブランク画像。)

## 次のソース (問題点あり)
Imageのsourceを入れ替えただけでは、画像のロードが行われないのだろう
と思い、調べたところ、
Imageが、reload()を持っていることがわかったので、下のように修正してみた。

```ImageSlideApp_2.py
from kivy.app import App
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.widget import Widget

import os
import glob
import random
import threading
from time import sleep


class ImageSlideWidget(Widget):
    image = ObjectProperty(None)
    image_src = StringProperty('')

    def __init__(self, **kwargs):
        super(ImageSlideWidget, self).__init__(**kwargs)
        self.image_src = './image.png'
        pass

    def set_image_src(self, src):
        self.image_src = src
        self.image.reload()


  ~ 以下、上記ImageSlideApp_1.pyと同じなので省略 ~
```

```imageSlide.kv
#:kivy 1.10

<ImageSlideWidget>:
    image: image

    canvas.before:
        Color:
            rgb: 1,1,1
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        size: root.size
        orientation: 'vertical'

        Label:
            text: root.image_src
            color: 0, 0, 0, 1
            font_size: 20
            size_hint_y: 0.1

        Image:
            id: image
            size_hint_y: 0.9
            allow_stretch: True
            source: root.image_src
```
が、上記と変わらず、画像ファイル名は更新されるが、画像箇所は更新されず、真っ黒になった。

## 最終的なソース
色々さがしていたら、
`Clock.schedule_interval()`というスケジューラ(?)があって、ここで、更新処理とかする
というのを見つけたので、
以下のように修正。

```ImageSlideApp_3.py
from kivy.app import App
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.widget import Widget
from kivy.clock import Clock

import os
import glob
import random
import threading
from time import sleep


class ImageSlideWidget(Widget):
    image = ObjectProperty(None)
    image_src = StringProperty('')

    def __init__(self, **kwargs):
        super(ImageSlideWidget, self).__init__(**kwargs)
        self.image_src = './image.png'
        Clock.schedule_interval(self.update, 0.01)
        pass

    def update(self, dt):
        self.image.reload()

    def set_image_src(self, src):
        self.image_src = src

  ~ 以下、上記ImageSlideApp_1.py, ImageSlideApp_2.pyと同じなので省略 ~
```

```imageSlide.kv
  ~ 同じなので省略 ~
```

これで、1秒毎に、画像ファイル名と画像が更新されるようになりました。


## まとめ
- 画像のソースを変えたら`Image.reload()`する。
- `Image.reload()`は、`Clock.schedule_interval()`とかで、実行する。

[05/24 追記]
ここで紹介してるのは、sourceを使用した場合のImageの更新方法。
texureを使用した場合、この方法ではダメな様子。

# 備考
- 全体的に構造が良くないのは、すみません。
- もっといい方法知ってる方いらっしゃいましたら、ご享受ください。