kivyでドラッグで並べ替えできるリストを作った時のメモ
Pythonのマルチプラットフォーム向けGUIライブラリkivyで
ドラッグで並べ替えできるリスト(AndroidでよくあるUI、RecycleView
とItemTouchHelper
で作るんだったかな?)
を作ってみた時のメモ。
ちょっと違うけど、大体こんな感じ
ソースは↓の「開く」をクリックすると表示されます。
ダウンロードしたいときは
こちら
からどうぞ。
reorderablelist.py
ソースは、表示する項目を制御するクラスReorderableItem
と
リスト全体を制御するクラスReorderableList
で構成される。
(あと、単体実行で動作するテストプログラム)
動作としては、
ReorderableList
にReorderableItem
を追加していきリスト化ReorderableItem
をドラッグすることで並べ替えReorderableItem
を左右にスワイプすることでハンドラ実行(デフォルトでは右スワイプで項目削除)
といった感じ。テストプログラムではReorderableList
をScrollView
で囲んでスクロール可能にしています。
スクロール不要ならそのまま配置しても可(テストプログラムではwith_scroll
をFalse
に設定)。
ReorderableList
はリスト全体を制御するクラスで、
BoxLayout
を継承したクラス。
追加したプロパティは以下。
名称 | 内容 |
item_bg | 項目の背景色 |
item_fg | 項目の文字色 |
list_bg | リストの背景色 |
swipe_distance | Swipe検出距離 |
コンストラクタでは、基底クラスの初期化
(orientation
は"vertical"
固定(項目を縦方向に並べるため)、
size_hint_y
はNone
固定(ScrollViewに包むため))
のほか、
背景色の設定と、スクロール可能にするためminimum_height変更時の処理のbindを行っている。
add_item
が項目追加処理。
パラメータはcls
で項目表示に使用するクラス(デフォルトはReorderableItem
)と、項目表示クラスの初期化パラメータを受け取る。
これは項目に表示する内容を柔軟に切り替えられるよう、項目表示クラスをReorderableItem
を継承したクラスに切り替えられるようにするため。
kivyでは、レイアウトの変更命令と実際に変更が行われるまでにディレイがある。
具体的には、add_widget(wid)
して直後にwid.posを読み出すと実際に追加されたあとの位置が読み出せない。
これは、add_widget()
では処理の予約だけ行い、実際に描画が行われるのは別のところ(mainloop
?)で
他のウィジェットのレイアウト結果を五月雨式に反映していくため、
描画結果がposに反映されるまでタイムラグが発生するためと思われる。
そこで、ReorderableList
のレイアウトが変更され、処理が終了したタイミングで子ウィジェットに
レイアウト完了を通知できるようdo_layout()
をオーバライドし、基底クラスの処理が完了した後
各子ウィジェットのdone_parent_layout()
をコールしている。
ReorderableItem
はリストに表示する項目を制御するクラスで、
DragBehavior
クラスとBoxLayout
を継承している。
DragBehavior
クラスはドラッグ処理を実現するクラス。
もう一つの基底クラスをBoxLayout
とすることで、派生クラスのレイアウトを柔軟に設定できるようにしている。
追加したプロパティは以下。
名称 | 内容 |
text | 項目に表示する文字列 |
bg_color | 項目の背景色 |
fg_color | 項目の文字色 |
swipe_distance | Swipe検出距離 |
コンストラクタでは、基底クラスの初期化
のほか、
項目内部の構築(add_inner_widget()
)、
背景色の設定と、pos/size変更時にドラッグ範囲を変更する処理のbind、
インスタンス変数の設定を行っている
add_inner_widget()
)項目の内部の表示を構築する処理。
デフォルトではLabelを1個追加している。
項目の表示内容を変更したい場合は、派生クラスでこの処理をオーバライドし、
必要な内容を追加していく。
on_touch_down()
)まず、基底クラスのドラッグ開始処理を実行する。
DragBehavior
を継承したクラスの場合、自身がドラッグ対象であればここでTrue
が返ってくるので
ドラッグ対象としての処理(インスタンス変数の設定)を行い、
自身の描画Canvasを親ウィジェット(self.parent
)のcanvasからcanvas.afterに繋ぎかえる。
これはドラッグ中の表示が前面に表示されるようにするためである。
[!NOTE] 通常、add_widget()すると追加されたウィジェットの描画Canvasは親ウィジェットのCanvasに繋がれる。
この親ウィジェットのCanvasに繋がれた描画ウィジェットは後から繋がれたものが前面に表示される。
(縦方向のBoxLayoutの場合下に表示されているウィジェットが前に表示される)
重ならない表示であれば問題ないが、ドラッグを行うとドラッグ中のウィジェットが背面に表示されることがある。
そこで、ドラッグ中のウィジェット他のウィジェットより前面に表示するため、CanvasからCanvas.afterに繋ぎかえ、前面に表示されるようにする。
仕様を読む限り、parent.add_widget(child, index=index, canvas='after')
でadd_widget時にcanvas.afterに接続できるようなのだが、 実際にはバグ(仕様制限?)により、indexが0のときのみcanvasパラメータが有効になるらしい。
これを回避するため、parent.add_widget(child, index=index)
で通常通りウィジェットを追加した後、parent.canvas.remove(child.canvas) parent.canvas.after.add(child.canvas)
として描画Canvasを繋ぎかえている。 canvas.afterに繋いだ描画Canvasをcanvasに戻す場合は、
cur_index = parent.children.index(child) # canvas.after から canvasへ繋ぎかえるため parent.remove_widget(child) # 一旦削除して(canvasかcanvas.afterは自動的に判別してくれる) parent.add_widget(child, index=cur_index) # 再度同じ場所に挿入
と実行する。
remove_widget()
は描画Canvasがどこに繋がっていても探して削除してくれるので、 そのまま実行して問題ない。
on_touch_move()
)まず、基底クラスのドラッグ中処理を実行する。
DragBehavior
を継承したクラスの場合、自身がドラッグ対象であればここでTrue
が返ってくるので
ドラッグ中の処理を行う。
親ウィジェットに繋がっている子ウィジェットをサーチして、自分以外で自分の中心がウィジェットの表示範囲内に入っているウィジェットを探す
(child.collide_point(*self.center)
)
自分の上端が対象ウィジェットの上端を超えたか、自分の下端が対象ウィジェットの下端を超えた場合は自分と対象ウィジェットを入れ替える。
(child.collide_point(*self.center)
だけで判断すると、自分より高さが高いウィジェットと入れ替えるときに不都合が起こる)
位置を交換するには、自分を一旦削除(remove_widget()
)して、対象ウィジェットの位置(i
)に繋ぐ(add_widget()
)。
その後、ドラッグを継続するので、ドラッグ開始時と同様に描画Canvasをcanvas.afterに繋ぎかえる。
on_touch_up()
)まず、基底クラスのドラッグ終了処理を実行する。
DragBehavior
を継承したクラスの場合、自身がドラッグ対象であればここでTrue
が返ってくるので
ドラッグ終了の処理を行う。
ドラッグ開始/ドラッグ中処理でcanvas.afterに繋ぎかえた描画Canvasをcanvasに戻すために一旦remove_widget()
してから
add_widget()
する(これによりドラッグにより表示が移動していた対象ウィジェットが通常位置に戻る)。
その後、対象ウィジェットの表示位置がドラッグ開始時と変わっていなく、ドラッグした距離が設定値を超えている場合は
スワイプ処理(do_swipe_right()
またはdo_swipe_left()
)を実行する。
do_swipe_right()
)現状、対象ウィジェットを削除する。
処理を変更する場合は派生クラスでオーバライドする。
do_swipe_right()
)現状、デバッグ用にログ出力のみ。
処理を変更する場合は派生クラスでオーバライドする。
done_parent_layout()
)親ウィジェットのレイアウト完了時に子ウィジェットすべてのこのメソッドがコールされる。
ドラッグ中であれば必要な処理(現在位置の記憶とドラッグ中位置への移動)を行う。
ソースは↓の「開く」をクリックすると表示されます。
ダウンロードしたいときは
こちら
からどうぞ。
サンプルプルグラム