Kivy ScrollView和 ScreenManager:如何处理滚动超出部分的数值并在主屏幕触发事件

前端开发 2026-07-10

首先,编程对我来说是爱好,而不是职业。下面的示例代码在一个字典中创建了一些人工数据,我希望在ScrollView容器被过度滚动时逐键访问这些数据。我已经连续好几天在尝试寻找解决方案…… 一直在研究装饰器,并在网上查找类似的问题但没有结果。我在Linux上运行Python 3.14.1,配套Kivy 2.3.1。

示例main.kv

#: import Effect main.Effect

ManagerWidget:
    StartScreen:
    Page2Screen:

<StartScreen>:
    name: 'startscreen'

    BoxLayout:
        orientation: 'vertical'

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

        BoxLayout:
            size_hint_y: .1

            Button:
                id: btn1
                text: 'Simulation of desired "prev_key" function via overscroll event'
                on_release: root.prev_key ( )

            Button:
                id: btn2
                text: 'Simulation of desired "next_key" function via overscroll event'
                on_release: root.next_key ( )

        ScrollView:
            id: scroller
            effect_cls: Effect
            size_hint: 1, 1
            do_scroll_x: False
            GridLayout:
                id: container
                cols: 5
                spacing: 2
                size_hint_y: None
                height: self.minimum_height

        BoxLayout:
            size_hint_y: .1

            Button:
                text: 'Go to Page 2'
                on_release: root.manager.current = 'page2'

<Page2Screen>:
    name: 'page2'
    BoxLayout:
        padding: 5
        spacing: 5
        orientation: 'vertical'

        Label:
            text: 'Placeholder 1'
        Label:
            text: 'Placeholder 2'
        Label:
            text: 'Placeholder 3'

        BoxLayout:
            size_hint_y: .3
            Button:
                text: 'Back to main screen'
                on_release:
                    root.manager.current = 'startscreen'

以及main.py

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput

from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.metrics import dp
from kivy.clock import Clock

from time import time

class CallControl:

    def __init__ ( self, max_call_interval ):
        self._max_call_interval = max_call_interval
        self._last_call = time ( )

    def __call__ ( self, function ):

        def wrapped ( *args, **kwargs ):
            now = time ( )

            if now - self._last_call > self._max_call_interval:
                self._last_call = now

                function ( *args, **kwargs )

        return wrapped


class Effect ( DampedScrollEffect ):

    def __init__ ( self, *args, **kwargs ):
        super ( Effect, self ).__init__ ( **kwargs )

    def on_overscroll ( self, *args ):
        self.args = args

        if self.overscroll < -100:
            self.scrollup ( )

        elif self.overscroll > 100:
            self.scrolldown ( )


    @CallControl ( max_call_interval = 1 )
    def scrollup ( self ):
        print ( 'How to update the Scrollview widgets? (i.e. the prev_key method in StartScreen )' )
        # StartScreen.prev_key ( self.overscroll ) ???

    @CallControl ( max_call_interval = 1 )
    def scrolldown ( self ):
        print ( 'How to update the Scrollview widgets? (i.e. the next_key method in StartScreen )' )
        # StartScreen.next_key ( self.overscroll ) ???



class StartScreen ( Screen ):

    def __init__ ( self, **kwargs ):
        super( StartScreen, self).__init__( **kwargs )

        # Create some artificial data for this sample code to fill the GridLayout container
        self.num    = '11'  
        self.data   = { }
        self.keys   = [ ]
        lines       = [ ]
        for k in range ( 10, 15 ):

            for v in range ( 25 ):
                lines.append ( 'ONE,Key %d,%d,yxz,,white,gray,yellow,red,green,\n' % ( k, k+v ) )

            self.data[ str ( k ) ] = lines
            self.keys.append ( str ( k ) )
            lines = [ ]


        self.colour_mapping = { 'white'     : ( 1,1,1,1, ),
                                'gray'      : ( 0,0,0,.2 ),
                                'green'     : ( 0,1,0,.3 ),
                                'red'       : ( 1,0,0,.3 ),
                                'yellow'    : ( 1,1,0,.3 ) }

        Clock.schedule_once ( self.finish_init, 0 )

    def finish_init ( self, dt ):
        self.update_container ( )


    # @classmethod ? But how to call self.update_container then? And the keys list is empty anyway... hmpf...
    def prev_key ( self, *args ):
        idx = self.keys.index ( self.num )

        if idx in ( 0, len ( self.keys ) ):
            print ( 'no more keys in the dict...' )
            self.num = self.keys[ idx ]

        else:
            self.num = self.keys[ idx - 1 ]

        self.update_container ( )


    def next_key ( self, *args ):
        idx = self.keys.index ( self.num )

        try:
            self.num = self.keys[ idx + 1 ]

        except IndexError:
            print ( 'no more keys in the dict...' )
            self.num = self.keys[ idx ]

        self.update_container ( )



    def update_container ( self ):

        hgt_row     = 35
        fnt_size    = 15

        self.ids.container.clear_widgets ( )

        for line in self.data.get ( self.num ):

            d1, d2, d3, d4, empty, bg1,bg2,bg3,bg4,bg5, nl = line.split( ',' )[ : ]

            colour1 = self.colour_mapping.get ( bg1 )
            colour2 = self.colour_mapping.get ( bg2 )
            colour3 = self.colour_mapping.get ( bg3 )
            colour4 = self.colour_mapping.get ( bg4 )
            colour5 = self.colour_mapping.get ( bg5 )

            in_c1 = TextInput ( height              = hgt_row,\
                                text                = d1,\
                                font_size           = dp ( fnt_size ),\
                                size_hint           = ( .1, None ),\
                                background_color    = colour1 )

            in_c2 = TextInput ( height              = hgt_row,\
                                text                = d2,\
                                font_size           = dp ( fnt_size ),\
                                size_hint           = ( .12, None ),\
                                background_color    = colour2 )

            in_c3 = TextInput ( height              = hgt_row,\
                                text                = d3,\
                                size_hint           = ( .07, None ),\
                                background_color    = colour3 )

            in_c4 = TextInput ( height              = hgt_row,\
                                size_hint           = ( .09, None ),\
                                text                = d4,\
                                background_color    = colour4 )

            in_c5 = TextInput ( height              = hgt_row,\
                                size_hint           = ( .7, None ),\
                                hint_text           = empty,\
                                background_color    = colour5 )

            self.ids.container.add_widget ( in_c1 )
            self.ids.container.add_widget ( in_c2 )
            self.ids.container.add_widget ( in_c3 )
            self.ids.container.add_widget ( in_c4 )
            self.ids.container.add_widget ( in_c5 )


class Page2Screen ( Screen ):
    pass

class ManagerWidget ( ScreenManager ):
    pass

class MainApp ( App ):
    pass

if __name__ == '__main__':
    MainApp( ).run( )

我到底缺少什么?有什么建议吗?

解决方案

Effect 直接调用 StartScreen.prev_key() 的位置是错误的。在你的示例中,Effect 是由 ScrollView.effect_cls 生成的,因此它不知道自己属于哪个 StartScreen 实例,而 prev_key / next_key 无论如何都是实例方法。

最简单的修复办法是:不要在效果类内部调用屏幕。把屏幕绑定到 ScrollVieweffect_y 实例上,并在那里做出响应。

from time import time
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput
from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.metrics import dp
from kivy.clock import Clock


class Effect(DampedScrollEffect):
    pass


class StartScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._last_overscroll_call = 0

        self.num = '11'
        self.data = {}
        self.keys = []
        lines = []
        for k in range(10, 15):
            for v in range(25):
                lines.append(f'ONE,Key {k},{k+v},yxz,,white,gray,yellow,red,green,\n')
            self.data[str(k)] = lines
            self.keys.append(str(k))
            lines = []

        self.colour_mapping = {
            'white': (1, 1, 1, 1),
            'gray': (0, 0, 0, .2),
            'green': (0, 1, 0, .3),
            'red': (1, 0, 0, .3),
            'yellow': (1, 1, 0, .3),
        }

        Clock.schedule_once(self.finish_init, 0)

    def finish_init(self, dt):
        self.update_container()
        self.ids.scroller.effect_y.bind(overscroll=self.on_overscroll_y)

    def on_overscroll_y(self, effect, value):
        now = time()
        if now - self._last_overscroll_call < 1:
            return

        if value < -100:
            self._last_overscroll_call = now
            self.prev_key()
        elif value > 100:
            self._last_overscroll_call = now
            self.next_key()

    def prev_key(self, *args):
        idx = self.keys.index(self.num)
        if idx == 0:
            print('no more keys in the dict...')
        else:
            self.num = self.keys[idx - 1]
        self.update_container()

    def next_key(self, *args):
        idx = self.keys.index(self.num)
        if idx >= len(self.keys) - 1:
            print('no more keys in the dict...')
        else:
            self.num = self.keys[idx + 1]
        self.update_container()

    def update_container(self):
        hgt_row = 35
        fnt_size = 15
        self.ids.container.clear_widgets()

        for line in self.data[self.num]:
            d1, d2, d3, d4, empty, bg1, bg2, bg3, bg4, bg5, nl = line.split(',')[:]
            colours = [self.colour_mapping[c] for c in (bg1, bg2, bg3, bg4, bg5)]
            vals = [d1, d2, d3, d4, empty]
            hints = [(.1, None), (.12, None), (.07, None), (.09, None), (.7, None)]

            for i, (txt, colour, hint) in enumerate(zip(vals, colours, hints)):
                w = TextInput(
                    height=hgt_row,
                    text='' if i == 4 else txt,
                    hint_text=txt if i == 4 else '',
                    font_size=dp(fnt_size),
                    size_hint=hint,
                    background_color=colour
                )
                self.ids.container.add_widget(w)
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章