Kivy-文字换行错误 [英] Kivy - text wrapping works wrong
问题描述
我正在尝试在Kivy(1.8.0)应用程序中包装文本.
当文本不多时,一切正常.
但是,如果文本较长且窗口不是很大,则会剪切文本.
I'm trying to wrap text in Kivy (1.8.0) application.
When there's not so much text, everything works fine.
But if there's long text and window is not very big, it just cuts the text.
这是示例代码:
Here's the example code:
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)
在移动设备上,这简直无法忍受.
On mobile devices it's just unbearable.
屏幕截图:
有没有办法解决这个问题?
Is there a way to fix this?
推荐答案
我自己遇到了这个问题,并阅读了文档遇到了提示发生了什么事...
I ran into this issue myself and reading over the source regarding text inputs there's a few functions that handle word wrapping, namely _split_smart
and _refresh_hint_text
, and then reading over the documentation ran into a hint as to what is happening...
在更改需要重新绘制的TextInput属性时,例如修改文本后,更新将在下一个时钟周期发生,而不是立即...
When changing a TextInput property that requires re-drawing, e.g. modifying the text, the updates occur on the next clock cycle and not instantly...
...现在他们似乎并没有说出来,但是有效地将这些小部件的高度和宽度约束分配给某些默认值(在我输入文本的情况下,是width: 50
经过测试),然后将文本(如果有的话)通过 _smart_split
推到换行的位置,并且再也不会更新...
... now they don't seem to say it out right but effectively these widgets get assigned their height and width constraints to some default (width: 50
in the case of text inputs that I tested), then the text if any is shoved through _smart_split
at some point to wrap lines, and never updated again...
一种简单的解决方案,如果您也不担心父小部件的高度也被更新.
The simple solution if you're not concerned about the parent widget's heights being updated too.
class cTextInput(TextInput):
def on_width(self, instance, value):
self.text = self.text
更复杂,更完整的解决方案,还可以更新高度和其他魔术.
The more complex and complete solution that also updates heights and other magics.
#!/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()
这篇关于Kivy-文字换行错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!