如何在Kivy的ScrollView中正确创建长文本标签?

1 投票
3 回答
2213 浏览
提问于 2025-04-18 12:03

我试着创建一个最小可重现示例来展示我的问题,但结果让我更加困惑。

我想做一个应用程序,里面有几个小部件。其中一个小部件会显示很长的文本,我希望这个文本大约能占三行。我希望显示这个文本的标签能够根据文本的大小自动增加高度。

这是我目前的进展:

    layout = BoxLayout(orientation='horizontal')
    subLayout = TwoLabelCombo()
    subLayout.label1.text = ('foo:')
    subLayout.label2.text = 'bar'*50
    subLayout.label2.width = int(Window.width*0.8)
    subLayout.label2.text_size = [subLayout.label2.width,None]
    subLayout.label2.size_hint_y = None
    subLayout.label2.height = subLayout.label2.texture_size[1]
    subLayout.height = subLayout.label2.texture_size[1]
    layout.height = subLayout.height

TwoLabelCombo 是一个 BoxLayout,它的方向设置为水平,里面放了两个标签,size_hint_x 分别设置为 0.2 和 0.8。layout 在应用程序中占据一整行,周围有类似的布局。文本确实跨越了好几行,但布局没有随着文本的变化而更新,因此它和其他小部件重叠了。我打印了 texture_size,发现它的高度是 0。我该如何获取正确的文本大小呢?

3 个回答

0

如果有人在找解决方案,想让一个可以自我扩展的ScrollView里面包含一个BoxLayout或GridLayout,这里有一些经验分享。参考了Ryan P的回答,我差不多找到了方向,但后来我意识到他是在解决一个非常特定的情况,所以他使用了max函数。

我需要的是一个可以随着新组件的添加而扩展的框,所以我简单地修改了他的_update_size()代码,变成了:

def _update_size(self, *_):
    temp_height = 0
    if self.size_hint_y is None:
        for c in self.children:
            temp_height += c.height
        self.height = temp_height + 10  # Add a buffer to the bottom to prevent text clipping.
    if self.size_hint_x is None:
        self.width = max(c.width for c in self.children) if self.children else 0

有几点需要注意,我把宽度的部分保持不变,因为这样对我的情况来说比较合理,但你也可以用类似的方式进行修改。

另外,使用这种方法改变Label组件大小的最简单方式是先用remove_widget()删除这个组件,然后用kivy.lang.Builder.load_string()创建一个新的组件,再把这个新组件添加到你的自我扩展/收缩的BoxLayout或GridLayout中,这样它会重新计算大小,从而调整ScrollView的滚动距离。别忘了,size_hint_y要设置为None,否则就不会有更新。

希望这些细节能帮助到遇到类似问题的人。

0

问题可能出在 texture_size 这个值不会在纹理真正生成并显示之前更新,而这个更新可能在你的方法结束时还没有发生(但在下一帧之前可能会发生)。所以,一般来说,你应该绑定到属性上,而不是直接获取它们当时的值。

你整个规则可以更简单地表达,而且还可以免费获得额外的功能,使用 kv 语言(我对你没有在帖子中提到的小部件添加做了一些猜测,但你应该能理解大致的意思)。

: size_hint_y: None height: label2.texture_size[1] Label: # label1 text: 'foo:' Label: # label2 id: label2 text: 'bar' * 50 width: int(Window.width*0.8) # 这可能通过布局来做会更好 text_size: self.width, None

我也稍微改了一下,设置了根 BoxLayout 的高度,而不是标签的高度,我觉得这样会更好。

当然,你也可以在 Python 中做到这一点,通过使用小部件的 bind 方法重新创建相关的绑定(这就是 kv 在后台做的事情),但这样写会更繁琐。

我有一个视频涵盖了一些这些内容,还有其他的东西,在这里

1

这里有两个问题。首先是关于如何让 Label 的大小适应它的内容。

你已经在正确的方向上了,通过 texture_size 来设置 height。但问题是,当你第一次创建 Label 时,纹理还没有被渲染出来,所以你无法知道它的大小。关键是要使用 属性绑定,这样每当 texture_size 发生变化时,height 就会自动更新

所以,在 kv语言中(这是在Kivy中定义用户界面的推荐方式):

Label:
    text: 'some really really really long text here'
    size_hint_y: None
    height: self.texture_size[1]
    text_size: self.width, None

在kv中引用属性时,它们会自动绑定。但你也可以在Python中以类似的方式做到这一点:

lbl = Label(text='some really really really long text here',
            size_hint_y=None)
lbl.bind(texture_size=lambda instance, value: setattr(instance, 'height', value[1]))
lbl.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))

第二个问题是关于包含 LabelBoxLayout。即使你把这些 Label 设置成了合适的大小,它们所在的 BoxLayout 仍然会按照正常的方式布局。这里有一个可以替代 BoxLayout 的方案,它会根据子元素的大小自动调整自身的大小(当 size_hint 设置得当时):

class CollapsingBoxLayout(BoxLayout):
    def __init__(self, **kwargs):
        super(CollapsingBoxLayout, self).__init__(**kwargs)
        self._trigger_update_size = Clock.create_trigger(self._update_size)

    def on_children(self, *_):
        for c in self.children:
            c.bind(size=self._trigger_update_size)
        self._update_size()

    def _update_size(self, *_):
        if self.size_hint_y is None:
            self.height = max(c.height for c in self.children) if self.children else 0
        if self.size_hint_x is None:
            self.width = max(c.width for c in self.children) if self.children else 0

最后,这里有一个完整的工作示例:

import kivy
kivy.require('1.8.0')

from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label

class CollapsingBoxLayout(BoxLayout):
    def __init__(self, **kwargs):
        super(CollapsingBoxLayout, self).__init__(**kwargs)
        self._trigger_update_size = Clock.create_trigger(self._update_size)

    def on_children(self, *_):
        for c in self.children:
            c.bind(size=self._trigger_update_size)
        self._trigger_update_size()

    def _update_size(self, *_):
        if self.size_hint_y is None:
            self.height = max(c.height for c in self.children) if self.children else 0
        if self.size_hint_x is None:
            self.width = max(c.width for c in self.children) if self.children else 0

root = Builder.load_string('''
BoxLayout:
    orientation: 'vertical'
    CollapsingBoxLayout:
        size_hint_y: None

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

        Label:
            text: 'Paragraph 1'
            size_hint_x: 0.2
            size_hint_y: None
            height: self.texture_size[1]
            text_size: self.width, None
        Label:
            canvas.before:
                Color:
                    rgba: 0, 1, 0, 0.2
                Rectangle:
                    pos: self.pos
                    size: self.size
            size_hint_x: 0.8
            text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sollicitudin dignissim orci. Pellentesque laoreet magna quis augue dictum fringilla. Vivamus nec adipiscing nunc. Aliquam pharetra auctor justo vel rutrum. Sed sodales nulla sed odio fermentum, vel pulvinar nibh hendrerit. Phasellus a volutpat leo. Donec id hendrerit velit. Curabitur eget suscipit neque, nec tincidunt nulla. Donec feugiat, urna quis porttitor aliquet, nibh est laoreet ligula, vitae vestibulum leo purus quis ante. Maecenas magna nisi, molestie eu ipsum quis, tempor tempor turpis. Vivamus a fringilla enim. Quisque aliquam elit tortor, nec mollis tellus facilisis accumsan. Phasellus sagittis commodo mauris in vestibulum. Mauris sed ultrices enim.'
            size_hint_y: None
            height: self.texture_size[1]
            text_size: self.width, None

    CollapsingBoxLayout:
        size_hint_y: None

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

        Label:
            text: 'Paragraph 2'
            size_hint_x: 0.2
            size_hint_y: None
            height: self.texture_size[1]
            text_size: self.width, None
        Label:
            canvas.before:
                Color:
                    rgba: 0, 1, 0, 0.2
                Rectangle:
                    pos: self.pos
                    size: self.size
            size_hint_x: 0.8
            text: 'Duis egestas dui lobortis ante rutrum, nec consectetur arcu sollicitudin. Phasellus ut felis facilisis, eleifend odio malesuada, placerat odio. Etiam convallis non mi at tempor. Nunc gravida est magna, a hendrerit nulla condimentum a. Proin tristique velit quis dui convallis, vitae sodales nunc condimentum. In sollicitudin eros augue, sit amet blandit neque accumsan eu. Mauris non risus at nisl vestibulum dignissim quis non arcu. Integer ullamcorper felis eu neque viverra placerat. Vivamus magna quam, porta ac tincidunt a, imperdiet sed purus. Phasellus tempus ac neque vel accumsan. Pellentesque ligula justo, auctor eget aliquet ultricies, volutpat scelerisque ligula. Maecenas dictum velit id neque rhoncus fermentum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur dictum enim nisl, ut elementum lectus viverra sit amet. Praesent vel tempor risus, at congue turpis. Praesent in justo lobortis, gravida lacus id, facilisis orci.'
            size_hint_y: None
            height: self.texture_size[1]
            text_size: self.width, None
''')

class TestApp(App):
    def build(self):
        lbl = Label(text=('hello this is some long long text! ' * 10), size_hint_y=None)
        lbl.bind(texture_size=lambda instance, value: setattr(instance, 'height', value[1]))
        lbl.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
        root.add_widget(lbl)
        return root

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

撰写回答