はじめに
Kivyのポップアップウィジェット内に設置したプログレスバーウィジェットで関数の進捗を表示することを考えてみました.
進捗表示を行いたい関数をジェネレータとすることで,関数内からプログレスバーを更新しているように見せています.
環境
- Python 3.5
- Kivy 1.9.1
- Windows 10 64bit
ポップアップウィジェットのカスタマイズ
プログレスバー,ラベル,ボタンの3部品からなる以下のようなイメージのポップアップを使用します.
また,ポップアップウィジェットには以下の2つのメソッドを追加しています.
-
set_value()
: プログレスバーの値とラベルを更新するメソッド -
enable_close_button()
: ポップアップを閉じるボタンを有効にするメソッド
コードは以下の通りです.
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つのメソッドを設定しています.
-
func_test()
: 時間のかかる処理の進捗を確認したいメソッド(ジェネレータ)
- 時間のかかる処理の進捗を確認したいメソッドはジェネレータとします .
- 処理の進捗を確認したい場所に
yield value, message
を挿入し,進捗とメッセージを返すようにしています.- 処理の最後では
value=1
を返し,閉じるボタンを有効にしています.- ここではプログレスバーの Max値=1 としていますが,
func_test_pp()
で任意の値を設定可能です.- ここでは時間のかかる処理を模して
time.sleep(1)
を設定しています.
-
func_test_pp()
: プログレスバーの値を更新するループを開始させるためのメソッド
- ポップアップウィジェットクラスのインスタンス
self.pp
を生成します.- 上記の
func_test()
ジェネレータからイテレータオブジェクトself.gen
を作成します.Clock.schedule_once()
を使用して 再帰メソッドpp_update()
を実行し,ループを開始します.
-
pp_update()
: プログレスバーの値を更新する再帰メソッド
- イテレータオブジェクト
self.gen
を一度参照した後,プログレスバーの値を更新します.StopIteration
またはその他のエラーが発生するまで,Clock.schedule_once()
を使用して再度自身を呼び出します.- エラーが発生した場合,ポップアップ内のボタンを有効にしてユーザーがポップアップを閉じることが出来るようにしています.
※Clock.schedule_once()
は0秒だと表示が不安定であったため,差し当たり0.1秒後に実行するように設定しています.
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()
まとめ
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()