You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用RelativeLayout时ScrollView无法滚动,求Midi谱实现方案

问题描述

我想要实现如图所示的Midi谱:
Midi谱示例

我用RelativeLayout定位按钮(还在测试阶段,未定稿),但ScrollView无法横向或纵向滚动。希望有人帮忙解决,或者提供更优的实现方案。

以下是创建该RelativeLayout的MidiSheet类代码:

class MidiSheet(Screen):

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

        screen = ScrollView(size=(Window.width, Window.height), do_scroll_x=True, do_scroll_y=False)

        notes = get_notes_in_song("../data/" + self.name)

        layout = RelativeLayout(size=(Window.width, Window.height))
        layout.add_widget(BackButton(self))

        initial_y_pos = Window.height/2
        initial_x_pos = Window.width*0.1
        for note in notes:
            note_name = note.split(":")[1]

            layout.add_widget(MidiButton(note_name, initial_x_pos, initial_y_pos))

            initial_x_pos += 100

        screen.add_widget(layout)

        self.add_widget(screen)

完整Python代码:

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.core.window import Window
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition

import os


def get_notes_in_song(song):

    with open(song) as f:
        lines = f.readlines()

    return lines


class SongButton(Button):

    def __init__(self, file_name, **kwargs):
        super(SongButton, self).__init__(**kwargs)

        self.background_color = [0, 0, 0, 0]
        self.size_hint = (0.5, None)
        self.height = 40

        self.file_name = file_name

    def on_press(self):
        App.get_running_app().root.add_widget(MidiSheet(name=self.file_name))
        App.get_running_app().root.transition.direction = "left"
        App.get_running_app().root.current = self.file_name


class BackButton(Button):
    def __init__(self, screen_instance, **kwargs):
        super(BackButton, self).__init__(**kwargs)

        self.background_color = [1, 1, 1, 1]
        self.size_hint = (0.1, 0.1)
        self.text = "Back"

        self.screen_instance = screen_instance

    def on_press(self):
        App.get_running_app().root.remove_widget(self.screen_instance)
        App.get_running_app().root.transition.direction = "right"
        App.get_running_app().root.current = "home"


class MidiButton(Button):
    def __init__(self, name, pos_x, pos_y, **kwargs):
        super(MidiButton, self).__init__(**kwargs)

        self.background_color = [1, 1, 1, 1]
        self.size_hint = (0.1, 0.1)
        self.pos = (pos_x, pos_y)
        self.text = name
        self.padding = (10, 10)

    def on_press(self):
        self.background_color = [0, 1, 0, 1] if self.background_color == [1, 1, 1, 1] else [1, 1, 1, 1]


class Home(Screen):
    def __init__(self, **kwargs):
        super(Home, self).__init__(**kwargs)

        screen = ScrollView(size_hint=(1, None), size=(Window.width, Window.height))

        layout = GridLayout(cols=1, spacing=10, size_hint_y=None)

        songs = [x for x in os.listdir("../data/") if ".txt" in x]

        for song in songs:
            name = song.replace("_", " ").replace(".txt", "")

            layout.add_widget(SongButton(song, text=name))

        screen.add_widget(layout)

        self.add_widget(screen)


class MidiSheet(Screen):

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

        screen = ScrollView(size=(Window.width, Window.height), do_scroll_x=True, do_scroll_y=False)

        notes = get_notes_in_song("../data/" + self.name)

        layout = RelativeLayout(size=(Window.width, Window.height))
        layout.add_widget(BackButton(self))

        initial_y_pos = Window.height/2
        initial_x_pos = Window.width*0.1
        for note in notes:
            note_name = note.split(":")[1]

            layout.add_widget(MidiButton(note_name, initial_x_pos, initial_y_pos))

            initial_x_pos += 100

        screen.add_widget(layout)

        self.add_widget(screen)


class Localiser(App):

    def build(self):
        sm = ScreenManager(transition=SlideTransition())
        sm.add_widget(Home(name="home"))

        return sm
解决方案

问题根源

ScrollView无法滚动的核心原因是:给RelativeLayout设置了固定的size=(Window.width, Window.height),即使按钮超出窗口范围,ScrollView也会认为内部布局未超出自身尺寸,因此不会触发滚动逻辑。

另外,RelativeLayout适合精准定位场景,但对于横向排列的按钮列表,使用BoxLayout更简洁,无需手动计算位置,还能自动适配布局宽度。

修改方案

方案1:修复RelativeLayout的滚动问题

修改MidiSheet类,根据按钮总数量计算RelativeLayout的实际宽度,让ScrollView识别到内容超出:

class MidiSheet(Screen):
    def __init__(self, **kwargs):
        super(MidiSheet, self).__init__(**kwargs)

        screen = ScrollView(size=(Window.width, Window.height), do_scroll_x=True, do_scroll_y=False)

        notes = get_notes_in_song("../data/" + self.name)
        # 计算布局总宽度:初始偏移 + 按钮数量*100间距
        total_width = Window.width*0.1 + len(notes)*100
        # 设置RelativeLayout的宽度为内容总宽度,高度保持窗口高度
        layout = RelativeLayout(size=(total_width, Window.height))
        layout.add_widget(BackButton(self))

        initial_y_pos = Window.height/2
        initial_x_pos = Window.width*0.1
        for note in notes:
            note_name = note.split(":")[1]
            layout.add_widget(MidiButton(note_name, initial_x_pos, initial_y_pos))
            initial_x_pos += 100

        screen.add_widget(layout)
        self.add_widget(screen)

方案2:改用BoxLayout实现更简洁的横向滚动布局

BoxLayout会自动排列子控件,无需手动计算位置,推荐使用这种方式:

# 先导入BoxLayout
from kivy.uix.boxlayout import BoxLayout

class MidiSheet(Screen):
    def __init__(self, **kwargs):
        super(MidiSheet, self).__init__(**kwargs)

        screen = ScrollView(size=(Window.width, Window.height), do_scroll_x=True, do_scroll_y=False)
        # 使用横向BoxLayout,size_hint_x设为None,宽度由子控件自动计算
        layout = BoxLayout(orientation='horizontal', size_hint_x=None, spacing=20, padding=(Window.width*0.1, 0))
        # 让BoxLayout的宽度自适应内容
        layout.bind(minimum_width=layout.setter('width'))
        
        # 添加返回按钮(放在布局最左边)
        back_btn = BackButton(self)
        back_btn.size_hint = (0.1, 0.1)
        layout.add_widget(back_btn)

        notes = get_notes_in_song("../data/" + self.name)
        for note in notes:
            note_name = note.split(":")[1].strip()  # 去除换行符
            # MidiButton去掉固定pos,用固定宽度控制大小
            btn = MidiButton(note_name)
            btn.size_hint = (None, 0.1)
            btn.width = 100  # 固定按钮宽度,保证一致
            layout.add_widget(btn)

        screen.add_widget(layout)
        self.add_widget(screen)

同时修改MidiButton的初始化方法,去掉冗余的pos参数:

class MidiButton(Button):
    def __init__(self, name, **kwargs):
        super(MidiButton, self).__init__(**kwargs)
        self.background_color = [1, 1, 1, 1]
        self.size_hint = (None, 0.1)
        self.width = 100
        self.text = name
        self.padding = (10, 10)

    def on_press(self):
        self.background_color = [0, 1, 0, 1] if self.background_color == [1, 1, 1, 1] else [1, 1, 1, 1]

额外优化点

  • 返回按钮固定化:可以把BackButton直接添加到Screen中,而非滚动布局内,这样用户无需滚动就能点击返回。
  • 清理文本格式:get_notes_in_song返回的列表包含换行符,分割后用.strip()去除空白,避免按钮文本显示异常。

内容的提问来源于stack exchange,提问作者Diyon335

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 12:20:27