【问题标题】:Kivy - text wrapping works wrongKivy - 文本换行错误
【发布时间】:2014-07-11 14:21:53
【问题描述】:

我正在尝试在 Kivy (1.8.0) 应用程序中换行。
当文本不多时,一切正常。
但是如果文本很长,而且窗口不是很大,它只是剪切文本。

示例代码如下:

vbox = BoxLayout(orientation="vertical", size_hint_y=None)
text = Label(text="Some very long text")
text.bind(size=text.setter("text_size"))
title = Label(text="Some very long title")
title.bind(size=title.setter("text_size"))
vbox.add_widget(title)
vbox.add_widget(text)

在移动设备上简直无法忍受。

截图:

全屏

小窗口

有没有办法解决这个问题?

【问题讨论】:

  • 你能澄清一下,文本根本没有换行吗?当 text_size 水平太长时,绑定到 text_size 应该给你换行。

标签: python user-interface layout kivy word-wrap


【解决方案1】:

text_size 必须与窗口大小相同,因此请尝试text.bind(text_size=text.setter("size"))

https://kivy.org/planet/2014/07/wrapping-text-in-kivy 的标签/ 这是一个示例,但在 kv 中而不是在 python 中

【讨论】:

    【解决方案2】:

    我自己遇到了这个问题并阅读了source关于文本输入的一些函数,即_split_smart_refresh_hint_text,然后阅读documentation遇到了一个提示正在发生的事情......

    当更改需要重新绘制的 TextInput 属性时,例如修改文本,更新发生在下一个时钟周期,而不是立即...

    ...现在 他们 似乎没有说清楚,但实际上这些小部件被分配了它们的高度和宽度约束到一些默认值(width: 50 在我输入的文本的情况下测试),然后文本(如果有的话)会在某个时候通过 _smart_split 来换行,并且永远不会再次更新......

    如果您不关心父窗口小部件的高度也会被更新,这是一个简单的解决方案。

    class cTextInput(TextInput):
        def on_width(self, instance, value):
            self.text = self.text
    

    更复杂、更完整的解决方案,还能更新高度和其他魔法。

    #!/usr/bin/env python
    
    from kivy.app import App
    from kivy.lang import Builder
    from kivy.uix.label import Label
    from kivy.uix.textinput import TextInput
    from kivy.uix.gridlayout import GridLayout
    from kivy.clock import Clock
    
    from collections import OrderedDict
    
    kv_string = """
    #:set rgb_red [1, 0, 0, 0.25]
    #:set rgb_purple [0.98, 0.06, 1.0, 0.5]
    #:set rgb_green [0.05, 0.47, 0.35, 0.5]
    #:set rgb_cyan [0.43, 0.87, 0.81, 0.5]
    #:set rgb_blue [0.14, 0.09, 0.76, 0.5]
    #:set rgb_salmon [0.98, 0.47, 0.35, 0.5]
    #:set rgb_clear [0, 0, 0, 0]
    
    Main_GridLayout: ## Defined root so Builder.load_string returns something useful
    
    <Main_GridLayout>:
        cols: 1
        rows: 2
        spacing: 0
        row_force_default: True
        rows_minimum: {0: action_bar.height, 1: self.height - action_bar.height}
        Custom_ActionBar: ## IDs for scope of this widget seem to allow for auto sizing to available space
            id: action_bar
        ScrollView:
            id: scroller
    
    
    <Custom_ActionBar@ActionBar>: ## Place holder to show that scrolling is within a target widget.
        ActionView:
            ActionPrevious: ## Hidden without side effects
                with_previous: False
                app_icon: ''
                app_icon_width: 0
                app_icon_height: 0
                title: ''
            ActionGroup: ## Text changes with user selection
                id: foobar_action_group
                mode: 'spinner'
                text: 'Foo'
                ActionButton:
                    text: "Foo"
                    on_press: foobar_action_group.text = self.text
                ActionButton:
                    text: "Bar"
                    on_press: foobar_action_group.text = self.text
    
    
    <Adaptive_GridLayout>: ## Going to have to read the Python methods for all of the fanciness this widget has.
        spacing: 10
        row_force_default: True
        size_hint_y: None
        rows_minimum: self.calc_rows_minimum()
        height: self.calc_min_height()
    
        default_background_color: rgb_green
        selected_background_color: rgb_cyan
        background_color: 0.05, 0.47, 0.35, 0.5
        # background_color: rgb_green ## TypeError: 'NoneType' object is not iterable -- Line 262 of context_instructions.py
        canvas.before:
            Color:
                rgba: self.background_color
            Rectangle:
                pos: self.pos
                size: self.size
    
    
    <Row_GridLayout>: ## Inherits from Adaptive_GridLayout
        rows: 1
        spacing: 0
        default_background_color: rgb_blue
        selected_background_color: rgb_salmon
        background_color: rgb_blue ## But this is okay?
    
        Row_Label:
            id: label
    
    
    <Row_Label@Label>: ## Example of kv only widget that does stuff
        size_hint_y: None
        height: self.texture_size[1]
    
        selected: False
    
        default_background_color: rgb_clear
        selected_background_color: rgb_red
        background_color: 0, 0, 0, 0
        canvas.before:
            Color:
                rgba: self.background_color
            Rectangle:
                pos: self.pos
                size: self.size
    
        on_touch_down:
            caller = args[0]
            touch = args[1]
    
            touched = caller.collide_point(*touch.pos)
    
            if touched:\
            caller.selected = caller.background_color == caller.default_background_color;\
            print('{0}.selected -> {1}'.format(caller, caller.selected))
    
            if touched and caller.selected: caller.background_color = self.selected_background_color
            elif touched and not caller.selected: caller.background_color = caller.default_background_color
    
    
    <Adaptive_TextInput>:
        synopsis_line_limit: 2
        use_bubble: True
        multiline: True
        # readonly: True
        allow_copy: True
        # text: ('Foobarred' * 10) * 40
        text: ''
        size_hint_y: None
        height: self.minimum_height
    
    """
    
    
    class Adaptive_TextInput(TextInput):
        def __init__(self, synopsis_line_limit = None, **kwargs):
            self.old_width = self.width
            self.old_height = self.height
            self.synopsis_line_limit = synopsis_line_limit
            self.synopsis_text = ''
            self.full_text = ''
    
            self.my_hero = super(Adaptive_TextInput, self)
            self.my_hero.__init__(**kwargs)
    
        def refresh_overflow_values(self, text):
            """ Uses '_split_smart' and '_get_text_width' methods from TextInput to generate synopsis text. """
            self.full_text = text
    
            lines, lines_flags = self._split_smart(text)
            if self.synopsis_line_limit is None:
                synopsis_line_limit = len(lines)
            else:
                synopsis_line_limit = self.synopsis_line_limit
    
            if len(lines) > synopsis_line_limit:
                synopsis_lines = lines[:synopsis_line_limit]
                synopsis_line = ''.join(synopsis_lines)
                available_width = self.width - self.padding[0] - self.padding[2]
                text_width = self._get_text_width(synopsis_line, self.tab_width, self._label_cached)
                if (text_width + 3) > available_width:
                    self.synopsis_text = '{0}...'.format(synopsis_line[:-3])
                else:
                    self.synopsis_text = synopsis_line
            else:
                self.synopsis_text = text
    
        def refresh_text_value(self):
            """ Sets 'self.text' to either 'self.full_text' or 'self.synopsis_text' based off 'self.focused' value. """
            if self.focused is True:
                self.text = self.full_text
            else:
                self.text = self.synopsis_text
            self._trigger_update_graphics() ## Does not seem to be needed but tis what refreshing of 'hint_text' method does.
    
        def propagate_height_updates(self):
            """ Update grid layouts to height changes. """
            containing_grids = [g for g in self.walk_reverse() if hasattr(g, 'refresh_grids_y_dimension')]
            for grid in containing_grids:
                grid.refresh_grids_y_dimension()
    
        def on_focused(self, instance, value):
            """ Sets 'self.focused' value and triggers updates to methods that are interested in such values. """
            self.focused = value
            self.refresh_text_value()
            self.propagate_height_updates()
    
        def on_size(self, instance, value):
            """ This is the magic that fires updates for line wrapping when widget obtains a new width size as well as
                updating grid layouts and their progenitors on new heights.
            """
            self.my_hero.on_size(instance, value)
            if self.old_width is not self.width: ## Only bother if width has changed
                self.old_width = self.width
                self.refresh_overflow_values(text = self.full_text)
                self.refresh_text_value()
            if self.old_height is not self.height:
                self.old_height = self.height
                self.propagate_height_updates()
    
        def on_text(self, instance, text):
            """ Updates text values via 'self.refresh_overflow_values(text = value)' only if focused. """
            if self.focused is True:
                self.refresh_overflow_values(text = text)
    
        def on_parent(self, instance, value):
            """ Wait for parenting to set customized text values because 'self.text' maybe set after initialization. """
            self.refresh_overflow_values(text = self.text)
            self.refresh_text_value()
    
    
    class Adaptive_GridLayout(GridLayout):
        """ Adaptive height and row heights for grid layouts. """
        def __init__(self, **kwargs):
            self.my_hero = super(Adaptive_GridLayout, self)
            self.my_hero.__init__(**kwargs)
    
        def yield_tallest_of_each_row(self):
            """ Yields tallest child of each row within gridlayout. """
            current_tallest = None
            for i, c in enumerate(list(reversed(self.children))):
                if current_tallest is None:
                    current_tallest = c
    
                if c.height > current_tallest.height:
                    current_tallest = c
    
                if self.cols is None or self.cols is 0: ## Should work around grids without value for 'cols'
                    yield current_tallest
                    current_tallest = None
                elif ((i + 1) % self.cols == 0) is True: ## Reached last item of current row.
                    yield current_tallest
                    current_tallest = None
    
        def calc_child_padding_y(self, child):
            """ Returns total padding for a given child. """
            try: ## Likely faster than asking permission with an if statement as most widgets seem to have padding
                child_padding = child.padding
            except AttributeError as e:
                child_padding = [0]
    
            len_child_padding = len(child_padding)
            if len_child_padding is 1:
                padding = child_padding[0] * 2
            elif len_child_padding is 2:
                padding = child_padding[1] * 2
            elif len_child_padding > 2:
                padding = child_padding[1] + child_padding[3]
            else:
                padding = 0
    
            return padding
    
        def calc_min_height(self):
            """ Returns total height required to display tallest children of each row plus spacing between widgets. """
            min_height = 0
            for c in self.yield_tallest_of_each_row():
                c_height = c.height + self.calc_child_padding_y(child = c)
                min_height += c_height + self.spacing[1]
            return min_height
    
        def calc_rows_minimum(self):
            """ Returns ordered dictionary of how high each row should be to accommodate tallest children of each row. """
            rows_minimum = OrderedDict()
            for i, c in enumerate(self.yield_tallest_of_each_row()):
                c_height = c.height + self.calc_child_padding_y(child = c)
                rows_minimum.update({i: c_height})
            return rows_minimum
    
        def refresh_height(self):
            """ Resets 'self.height' using value returned by 'calc_min_height' method. """
            self.height = self.calc_min_height()
    
        def refresh_rows_minimum(self):
            """ Resets 'self.rows_minimum' using value returned by 'calc_rows_minimum' method. """
            self.rows_minimum = self.calc_rows_minimum()
    
        def refresh_grids_y_dimension(self):
            """ Updates 'height' and 'rows_minimum' first for spawn, then for self, and finally for any progenitors. """
            grid_spawn = [x for x in self.walk(restrict = True) if hasattr(x, 'refresh_grids_y_dimension') and x is not self]
            for spawn in grid_spawn:
                spawn.refresh_rows_minimum()
                spawn.refresh_height()
    
            self.refresh_rows_minimum()
            self.refresh_height()
    
            grid_progenitors = [x for x in self.walk_reverse() if hasattr(x, 'refresh_grids_y_dimension') and x is not self]
            for progenitor in grid_progenitors:
                progenitor.refresh_rows_minimum()
                progenitor.refresh_height()
    
        def on_parent(self, instance, value):
            """ Some adjustments maybe needed to get top row behaving on all platforms. """
            Clock.schedule_once(lambda _ : self.refresh_grids_y_dimension(), 0.461251)
    
        def on_touch_down(self, touch):
            """ Place to throw debugging lines for test interactions as this should be removed before release. """
            touched = self.collide_point(*touch.pos)
            spawn_touched = [x.collide_point(*touch.pos) for x in self.walk(restrict = True) if x is not self]
            if touched is True and True not in spawn_touched: ## Example of how to fire on lonely touches...
                if self.background_color == self.default_background_color:
                    self.background_color = self.selected_background_color
                else:
                    self.background_color = self.default_background_color
    
                print('{0}.height -> {1}'.format(self, self.height))
                for c in self.children:
                    print('\t{0}.height -> {1}'.format(c, c.height))
    
            else: ## ... and return to defaults if others where touched.
                self.background_color = self.default_background_color
    
            ## Supering allows spawn to also register touch events
            self.my_hero.on_touch_down(touch)
    
    
    class Row_GridLayout(Adaptive_GridLayout):
        """ Magic is inherited from Adaptive_GridLayout, mostly... """
        def on_parent(self, instance, value):
            """ Overwriting inherited on_parent method to avoid over calling Clock. """
            pass
    
    
    class Main_GridLayout(GridLayout):
        """ Check 'kv_string' for layout widgets. """
        pass
    
    
    class SomeApp(App):
        """ SomeApp flaunts it because it has gots it. """
        def build(self):
            root_layout = Builder.load_string(kv_string)
    
            container = Adaptive_GridLayout(cols = 1)
            for x in range(0, 5):
                # row = Row_GridLayout(rows = 1) ## IndexError: list index out of range -- Line 324 of gridlayout.py
                row = Row_GridLayout(cols = 2)
                row.ids.label.text = 'Row #{0}'.format(x)
    
                ## Growing amount of junk text quickly on each iteration for swifter demonstration
                text = 'Foobarred' * ((x + 1) * (x + 2))
                textinput = Adaptive_TextInput()
                textinput.text = text
    
                row.add_widget(textinput)
                container.add_widget(row)
    
            root_layout.ids.scroller.add_widget(container)
    
            return root_layout
    
    
    if __name__ == '__main__':
        SomeApp().run()
    

    【讨论】:

      猜你喜欢
      • 2016-02-05
      • 1970-01-01
      • 2013-04-15
      • 1970-01-01
      • 2020-01-27
      • 2021-04-29
      • 2020-10-04
      • 2016-01-04
      • 1970-01-01
      相关资源
      最近更新 更多