Python Kivy でポップアップ内プログレスバー

# はじめに
Kivyのポップアップウィジェット内に設置したプログレスバーウィジェットで関数の進捗を表示することを考えてみました．
進捗表示を行いたい関数をジェネレータとすることで，関数内からプログレスバーを更新しているように見せています．

# 環境
- Python 3.5 
- Kivy 1.9.1 
- Windows 10 64bit 

# ポップアップウィジェットのカスタマイズ
プログレスバー，ラベル，ボタンの3部品からなる以下のようなイメージのポップアップを使用します．
![2017-04-23_14h29_40.png](https://qiita-image-store.s3.amazonaws.com/0/43136/a6e4781e-e23e-fe7f-dec7-45d8460de12b.png)



また，ポップアップウィジェットには以下の2つのメソッドを追加しています．
1. `set_value()`: プログレスバーの値とラベルを更新するメソッド
2. `enable_close_button()`: ポップアップを閉じるボタンを有効にするメソッド


コードは以下の通りです．

```python:Class-PopupProgress
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label

class PopupProgress():
    def __init__(self, value_init=0, value_max=1, title='', size=None, 
                 auto_close=False):
        self._auto_close = auto_close
        self._box = BoxLayout(orientation='vertical')
        
        if size is None:
            size_hint = (1,1)
        else:
            size_hint = (None, None)
        
        self.popup = Popup(title=title, 
                           content=self._box, 
                           auto_dismiss=False, 
                           size_hint=size_hint, 
                           size=tuple(size)
                           )
        
        # progress bar
        self._pb = ProgressBar(max=value_max, value=value_init)
        self._box.add_widget(self._pb)
        
        # label
        self._label = Label(text='')
        self._box.add_widget(self._label)
        
        # close button
        self._button = Button(text='Close', height=40, size_hint_y=None, 
                        disabled=True)
        self._button.bind(on_press=self.popup.dismiss)
        self._box.add_widget(self._button)
        
        self.popup.open()    # ポップアップを開く
        
        
    def set_value(self, value=None, message=''):
        if not value is None:
            self._pb.value = value
        self._label.text = message
        if self._pb.value_normalized>=1:
            if self._auto_close:
                self.popup.dismiss()
            else:
                self.enable_close_button()
    
    
    def enable_close_button(self):
        self._button.disabled = False
        
```


# Root Widget 
Root Widget にはボタンを作成するためのコンストラクタ（\_\_init\_\_）以外に以下の3つのメソッドを設定しています． 
 
1. `func_test()`: 時間のかかる処理の進捗を確認したいメソッド（ジェネレータ） 
> - 時間のかかる処理の進捗を確認したいメソッドはジェネレータとします ．
> - 処理の進捗を確認したい場所に `yield value, message` を挿入し，進捗とメッセージを返すようにしています． 
> - 処理の最後では `value=1` を返し，閉じるボタンを有効にしています．
> - ここではプログレスバーの Max値=1 としていますが，`func_test_pp()`で任意の値を設定可能です． 
> - ここでは時間のかかる処理を模して `time.sleep(1)` を設定しています． 
 
2. `func_test_pp()`: プログレスバーの値を更新するループを開始させるためのメソッド 
> - ポップアップウィジェットクラスのインスタンス `self.pp` を生成します． 
> - 上記の`func_test()`ジェネレータからイテレータオブジェクト`self.gen` を作成します． 
> - `Clock.schedule_once()` を使用して 再帰メソッド`pp_update()` を実行し，ループを開始します． 

3. `pp_update()`: プログレスバーの値を更新する再帰メソッド 
> - イテレータオブジェクト`self.gen` を一度参照した後，プログレスバーの値を更新します． 
> - `StopIteration` またはその他のエラーが発生するまで，`Clock.schedule_once()` を使用して再度自身を呼び出します．
> - エラーが発生した場合，ポップアップ内のボタンを有効にしてユーザーがポップアップを閉じることが出来るようにしています．

※`Clock.schedule_once()` は0秒だと表示が不安定であったため，差し当たり0.1秒後に実行するように設定しています．
 
```python:Class-RootWidget他
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
import time

class RootWidget(BoxLayout):
    
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        button = Button(text='popup!')
        button.bind(on_press=self.func_test_pp)
        self.add_widget(button)
        
        
    def pp_update(self, *args):
        try:
            value, message = next(self.gen)
            self.pp.set_value(value, message)
            Clock.schedule_once(self.pp_update, 0.1)
        except StopIteration:
            self.pp.enable_close_button()
        except Exception as e:
            self.pp.set_value(message=str(e))
            self.pp.enable_close_button()
    
    
    def func_test_pp(self, *args):
        self.pp = PopupProgress(
                title='TestPopupProgress', 
                size=(self.width/2, self.height/3)
                )
        self.gen = self.func_test()
        Clock.schedule_once(self.pp_update, 0.1)
    
    
    def func_test(self):
        yield 0, 'Preparing...'    # yield を使用して現在の進捗とコメントを返す（はじまり）
        
        # 進捗確認を行いたい時間のかかる処理
        allitems = 5
        for i in range(allitems):
            time.sleep(1) # 何か時間のかかる処理
            yield (i+1)/allitems, '{:d}/{:d}'.format(i+1, allitems)    # yield を使用して現在の進捗とコメントを返す
        
        yield 1, 'Complete.'    # yield を使用して現在の進捗とコメントを返す（おわり）
        
        
class TestPopupProgressApp(App):
    def build(self):
        return RootWidget()
    
    
if __name__ == '__main__':
    TestPopupProgressApp().run()
```

# まとめ
上記をまとめると以下の通りとなります． 
![2017-04-23_14h33_04_.gif](https://qiita-image-store.s3.amazonaws.com/0/43136/7a160cd9-f61f-7682-71e3-02d1d0d1b7c4.gif)


 
```python:test_popup_progress.py
from kivy.app import App

from kivy.clock import Clock

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label

import time

class PopupProgress():
    def __init__(self, value_init=0, value_max=1, title='', size=None, 
                 auto_close=False):
        self._auto_close = auto_close
        self._box = BoxLayout(orientation='vertical')
        
        if size is None:
            size_hint = (1,1)
        else:
            size_hint = (None, None)
        
        self.popup = Popup(title=title, 
                           content=self._box, 
                           auto_dismiss=False, 
                           size_hint=size_hint, 
                           size=tuple(size)
                           )
        
        # progress bar
        self._pb = ProgressBar(max=value_max, value = value_init)
        self._box.add_widget(self._pb)
        
        # label
        self._label = Label(text='')
        self._box.add_widget(self._label)
        
        # close button
        self._button = Button(text='Close', height=40, size_hint_y=None, 
                        disabled=True)
        self._button.bind(on_press=self.popup.dismiss)
        self._box.add_widget(self._button)
        
        self.popup.open()
        
        
    def set_value(self, value=None, message=''):
        if not value is None:
            self._pb.value = value
        self._label.text = message
        if self._pb.value_normalized>=1:
            if self._auto_close:
                self.popup.dismiss()
            else:
                self.enable_close_button()
    
                    
    def enable_close_button(self):
        self._button.disabled = False
        
class RootWidget(BoxLayout):
    
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        button = Button(text='popup!')
        button.bind(on_press=self.func_test_pp)
        self.add_widget(button)
        
        
    def pp_update(self, *args):
        try:
            value, message = next(self.gen)
            self.pp.set_value(value, message)
            Clock.schedule_once(self.pp_update, 0.1)
        except StopIteration:
            self.pp.enable_close_button()
        except Exception as e:
            self.pp.set_value(message=str(e))
            self.pp.enable_close_button()
    
    
    def func_test_pp(self, *args):
        self.pp = PopupProgress(
                title='TestPopupProgress', 
                size=(self.width/2, self.height/3)
                )
        self.gen = self.func_test()
        Clock.schedule_once(self.pp_update, 0.1)
    
    
    def func_test(self):
        yield 0, 'Preparing...'
        
        allitems = 5
        for i in range(allitems):
            time.sleep(1)
            yield (i+1)/allitems, '{:d}/{:d}'.format(i+1, allitems)
        
        yield 1, 'Complete.'
        
        
class TestPopupProgressApp(App):
    def build(self):
        return RootWidget()
    
    
if __name__ == '__main__':
    TestPopupProgressApp().run()
```