KivyのScrollViewの中にTabbedPanelがあるとTabbedPanelからのスワイプでスクロールが効かない、に対処したの巻

KivyのScrollViewの中にTabbedPanelがあるとTabbedPanelからのスワイプでスクロールが効かない、に対処したの巻

はじめに

ええと、タイトルそのママです。。。

KivyでScrollViewの内側にTabbedPanelを使っていると、Tabの部分を起点にしてスワイプしてもスクロールさせてくれません。

なんかスクロールしてる時に引っかかりがあるなーと思ったらそういうことでした。

下の動画がその例で、

ScrollViewの中にLabel(青色)を縦に並べて、Label3とLabel4の間にTabbedPanelを挟んであるのですが、tab(Tab1およびTab2と表示している部分)を起点にスワイプでスクロールしようとしてもうんともすんとも言いません。

と、いうわけで、ScrollViewの内側にあるTabbedPanelのtabを起点にスワイプしたときもスクロールできるようにしますよ、という話の備忘録です。

実施環境

Python 3.9.12

Kivy 2.1.0

macOS Monterey 12.6.1

やり方

元のコードに以下の項目を追加したり置換したりします。最終的な全体のコードはコードの項にて。

・追加で必要なモジュール群をimportする

from kivy.clock import Clock
from functools import partial
from kivy.metrics import dp
from kivy.uix.widget import Widget

・ScrollViewクラスを継承したScrollViewParentを作成し、on_scroll_stop関数をオーバーライドする。
このときのon_scroll_stop関数を

def on_scroll_stop(self, touch, check_children=True):
    super().on_scroll_stop(touch, check_children=False)

とする。
 => check_children=Falseとして子ScrollView内起点でのスクロールを効かせる

・ScrollViewクラスを継承したScrollViewChildrenを作成し、on_scroll_move関数をオーバーライドする。このときのon_scroll_move関数を

def on_scroll_move(self, touch):
    super().on_scroll_move(touch)
    touch.ud['sv.handled']['y'] = False

とする。
 => 親widgetにもy軸方向のイベントが伝わるようにする

・TabbedPanelクラスを継承したTabbedPanelRを作成する。_update_tabs関数の中にScrollViewがあるので、この部分をScrollViewChildrenに置換して関数をまるっとオーバーライドする。

 少し長いのでコードはコードの項をご参照ください。

・kvファイル中の親側のScrollViewクラスをScrollViewParentクラスに置換する

・kvファイル中のTabbedPanelクラスをTabbedPanelRクラスに置換する

コード

main.pyとmain.kvを以下に示します。

追記・置換が必要な部分は

・import additional moduls 部分

・additional part from here からadditional part endまでの部分

・modified code lineのコメントの部分

追加しています。

main.py

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem

#import additional modules
from kivy.clock import Clock
from functools import partial
from kivy.metrics import dp
from kivy.uix.widget import Widget

class Otameshi(BoxLayout):
    pass

#####################################
##### additional part from here #####
#####################################

#該当のwidget内の操作でスクロールするようにScrollViewクラスをオーバーライド
#適用したい親子関係のScrollViewクラスにこれらを使用する

class ScrollViewParent(ScrollView):
    def on_scroll_stop(self, touch, check_children=True):
        #check_children=Falseとして子ScrollView内起点でのスクロールを効かせる
        super().on_scroll_stop(touch, check_children=False)

class ScrollViewChild(ScrollView):
    # 親widgetにもy軸方向のイベントが伝わるようにする
    def on_scroll_move(self, touch):
        super().on_scroll_move(touch)
        touch.ud['sv.handled']['y'] = False

#TabbedPanel内のScrollViewクラスをScrollViewChildに置換(1か所;modified code lineとコメントしている行)
class TabbedPanelR(TabbedPanel):
    def _update_tabs(self, *l):
        self_content = self.content
        if not self_content:
            return
        # cache variables for faster access
        tab_pos = self.tab_pos
        tab_layout = self._tab_layout
        tab_layout.clear_widgets()
        scrl_v = ScrollViewChild(size_hint=(None, 1), always_overscroll=False) # modified code line #
        tabs = self._tab_strip
        parent = tabs.parent
        if parent:
            parent.remove_widget(tabs)
        scrl_v.add_widget(tabs)
        scrl_v.pos = (0, 0)
        self_update_scrollview = self._update_scrollview

        # update scrlv width when tab width changes depends on tab_pos
        if self._partial_update_scrollview is not None:
            tabs.unbind(width=self._partial_update_scrollview)
        self._partial_update_scrollview = partial(
            self_update_scrollview, scrl_v)
        tabs.bind(width=self._partial_update_scrollview)

        # remove all widgets from the tab_strip
        super(TabbedPanel, self).clear_widgets()
        tab_height = self.tab_height

        widget_list = []
        tab_list = []
        pos_letter = tab_pos[0]
        if pos_letter == 'b' or pos_letter == 't':
            # bottom or top positions
            # one col containing the tab_strip and the content
            self.cols = 1
            self.rows = 2
            # tab_layout contains the scrollview containing tabs and two blank
            # dummy widgets for spacing
            tab_layout.rows = 1
            tab_layout.cols = 3
            tab_layout.size_hint = (1, None)
            tab_layout.height = (tab_height + tab_layout.padding[1] +
                                 tab_layout.padding[3] + dp(2))
            self_update_scrollview(scrl_v)

            if pos_letter == 'b':
                # bottom
                if tab_pos == 'bottom_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                    widget_list = (self_content, tab_layout)
                else:
                    if tab_pos == 'bottom_left':
                        tab_list = (scrl_v, Widget(), Widget())
                    elif tab_pos == 'bottom_right':
                        # add two dummy widgets
                        tab_list = (Widget(), Widget(), scrl_v)
                    widget_list = (self_content, tab_layout)
            else:
                # top
                if tab_pos == 'top_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                elif tab_pos == 'top_left':
                    tab_list = (scrl_v, Widget(), Widget())
                elif tab_pos == 'top_right':
                    tab_list = (Widget(), Widget(), scrl_v)
                widget_list = (tab_layout, self_content)
        elif pos_letter == 'l' or pos_letter == 'r':
            # left or right positions
            # one row containing the tab_strip and the content
            self.cols = 2
            self.rows = 1
            # tab_layout contains two blank dummy widgets for spacing
            # "vertically" and the scatter containing scrollview
            # containing tabs
            tab_layout.rows = 3
            tab_layout.cols = 1
            tab_layout.size_hint = (None, 1)
            tab_layout.width = tab_height
            scrl_v.height = tab_height
            self_update_scrollview(scrl_v)

            # rotate the scatter for vertical positions
            rotation = 90 if tab_pos[0] == 'l' else -90
            sctr = Scatter(do_translation=False,
                           rotation=rotation,
                           do_rotation=False,
                           do_scale=False,
                           size_hint=(None, None),
                           auto_bring_to_front=False,
                           size=scrl_v.size)
            sctr.add_widget(scrl_v)

            lentab_pos = len(tab_pos)
            # Update scatter's top when its pos changes.
            # Needed for repositioning scatter to the correct place after its
            # added to the parent. Use clock_schedule_once to ensure top is
            # calculated after the parent's pos on canvas has been calculated.
            # This is needed for when tab_pos changes to correctly position
            # scatter. Without clock.schedule_once the positions would look
            # fine but touch won't translate to the correct position


            if tab_pos[lentab_pos - 4:] == '_top':
                # on positions 'left_top' and 'right_top'
                sctr.bind(pos=partial(self._update_top, sctr, 'top', None))
                tab_list = (sctr, )
            elif tab_pos[lentab_pos - 4:] == '_mid':
                # calculate top of scatter
                sctr.bind(pos=partial(self._update_top, sctr, 'mid',
                                      scrl_v.width))
                tab_list = (Widget(), sctr, Widget())
            elif tab_pos[lentab_pos - 7:] == '_bottom':
                tab_list = (Widget(), Widget(), sctr)

            if pos_letter == 'l':
                widget_list = (tab_layout, self_content)
            else:
                widget_list = (self_content, tab_layout)

        # add widgets to tab_layout
        add = tab_layout.add_widget
        for widg in tab_list:
            add(widg)

        # add widgets to self
        add = self.add_widget
        for widg in widget_list:
            add(widg)

#####################################
#####    additional part end    #####
#####################################

class MainApp(App):
    def __init__(self, **kwargs):
        super(MainApp, self).__init__(**kwargs)

main.kv

<Otameshi>
    ScrollViewParent: #modified code line
        do_scroll_x: False
        do_scroll_y: True
        size:self.size
        BoxLayout:
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            spacing: dp(5)
            Label:
                text: 'Label1'
            Label:
                text: 'Label2'
            Label:
                text: 'Label3'
            TabbedPanelR: #modified code line
                padding: dp(2),dp(2)
                spacing: dp(5)
                do_default_tab:False
                size_hint_y: None
                height:dp(215)
                tab_height: dp(100) 
                tab_width: self.width/3 
                TabbedPanelItem:
                    text: 'Tab1'
                    Button:
                        text:'Tab1 Item'
                TabbedPanelItem:
                    text: 'Tab2'
                    Button:
                        text:'Tab2 Item'
            Label:
                text: 'Label4'
            Label:
                text: 'Label5'
            Label:
                text: 'Label6'
            Label:
                text: 'Label7'
            Label:
                text: 'Label8'
<Label>
    size_hint_y: None
    font_size: sp(40)
    text_size: None, None
    size: 100, 200
    padding: 10, 10
    canvas.before:
        Color:
            rgba:(48/255,84/255,150/255,1)
        Rectangle:
            size: self.size
            pos: self.pos

結果

で、実行してみると、、、

これで、ScrollViewの内側にあるTabbedPanelのtabを起点にスワイプしたときもスクロールできるようになりました。

めでたしめでたし。

おしまい。

参考にしたサイトなど

https://stackoverflow.com/questions/64470608/python-kivy-nested-scrollviews

https://kivy.org/doc/stable/api-kivy.uix.scrollview.html

https://github.com/kivy/kivy/blob/master/kivy/uix/scrollview.py

https://github.com/kivy/kivy/blob/master/kivy/uix/tabbedpanel.py

・Kivy 2.1.0のライセンス

MIT License

Copyright (c) 2010-2022 Kivy Team and other contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

本記事上のサンプルコードについては(新しいことほぼないけど)、

上記のMITライセンス条文にCopyright ©︎ 2022 buを付加したものとさせてください。

ライセンス遵守下でご自由にお使いください。


さてと、こんなのいかがでしょう?


GUI Programming with Python and Kivy



Pythonコードレシピ集



ちょっと広告です
https://business.xserver.ne.jp/

https://www.xdomain.ne.jp/

wpX Speed / wpX

★LOLIPOP★

.tokyo

MuuMuu Domain!