I'm trying to build a log viewer on kivy using recycleview since logs can be pretty large. I'm assigning one label widget per line so I can have more control over the text in the future. Some lines will have more text than others so adapted the Label widget to resize according, but when putting that inside recycleview can't seem to be able to control the height of the widget per line anymore, it stays at the same size. What I expect is the label to wrap on the text and adjust height since don't need the extra space between lines. If there's to little text a lot of free space is shown, if I put to much text in the label it floods and label doesn't grow.
One workaround that I tried with different code was to assign at least a 200 lines per label, that seems to work, but I do need more control over each line of text.
This is the example code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
Builder.load_string('''
<Row#BoxLayout>:
canvas:
Color:
rgba: 1, 0.1, 0.1, 0.5 #Red Marker
Rectangle:
size: self.size
pos: self.pos
value: ''
orientation: 'vertical'
Label:
text: root.value
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: 20
<LogDisplayWidget>:
rv: rv
orientation: 'vertical'
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
''')
class LogDisplayWidget(BoxLayout):
rv = ObjectProperty()
def __init__(self):
super(LogDisplayWidget, self).__init__()
self.load_text()
def load_text(self):
for i in range(10):
line = str(i) + 'This is a test of a bunch of text'
self.rv.data.append({'value': line})
class TestApp(App):
def build(self):
return LogDisplayWidget()
if __name__ == "__main__":
TestApp().run()
enter image description here
enter image description here
Did a code rewrite, the labels appear resized correctly in first page, but getting jerky unexpected results after scrolling, it shows correct label size sometimes then some are to big, and the scroll skips like trying to adjust itself and it fixes size again. Does anyone have a better way to implement this or I'm missing something? I'm suspecting it has something to do with the way the view refreshes
This is the new code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
import random
Builder.load_string('''
<Row#Label>:
canvas.before:
Color:
rgba: 0.8, 0.1, 0.1, 0.5 #Red Marker
Rectangle:
size: self.size
pos: self.pos
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: dp(20)
<RV>:
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
line = ''
for i in range(50):
n = random.randint(0, 1)
if n:
j = random.randint(5, 30)
line = 'Line: ' + str(i+1) + ' This is a test of a bunch of text' * j
else:
line = 'Line: ' + str(i+1) + ' This is a test of a bunch of text'
self.data.append({'text': line})
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Related
How can I make a RecycleView in a Kivy python app display all its labels without truncating the text contents of the label nor adding huge spaces in-between the labels?
I'm trying to display a very large amount of text in a Kivy (5+ MB) without causing it to lock-up. I think objectively the best solution here is to use a RecycleView with each line of the text in its own Label.
Official Documentation Demo
The example given in the official Kivy documentation about RecycleView is fine because the amount of text in the label is extremely short.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'Label'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Demo with content
But if we update the example above so that the text in the label is actually substantial, mimicking real-world text, then the contents of the label's text gets truncated. And there's a huge space in-between each label.
import random
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'Label'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(self.get_random())} for x in range(100)]
def get_random(self):
# generate some random ASCII content
random_ascii = ''.join( [random.choice('0123456789abcdefghijklnmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(0,900)] )
random_ascii = 'START|' + random_ascii + '|END'
print( random_ascii)
return random_ascii
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Demo with content and 'text_size'
I've tried setting the text_size of the Label. That certainly displays much more of the text, but it's still not showing all of the text in each Label.
In this example, the gap between each label is now gone.
import random
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<MyLabel#Label>:
text_size: self.size
<RV>:
viewclass: 'MyLabel'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(self.get_random())} for x in range(100)]
def get_random(self):
# generate some random ASCII content
random_ascii = ''.join( [random.choice('0123456789abcdefghijklnmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(0,900)] )
random_ascii = 'START|' + random_ascii + '|END'
print( random_ascii)
return random_ascii
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
How can I display a vertical RecycleView of Labels such that the text contents of the Labels is not truncated, and there is no extra padding/margin between each row of Labels?
If you want the Label to stretch up as its text content, you can bind its width to its texture width. This will enable you to scroll horizontally within RecycleView. Again if you want to scroll vertically, you need to explicitly specify the height of each content (here Label).
Here's a modified version (of the last one) of your kvlang,
<MyLabel#Label>:
size_hint_x: None
width: self.texture_size[0]
# Canvas added for visual purpose.
canvas.before:
Color:
rgb: 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
<RV>:
viewclass: 'MyLabel'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
spacing: dp(1) # Adjust to your need (atyn).
padding: dp(2) # atyn.
default_size: None, dp(20) # atyn.
default_size_hint: None, None
size_hint: None, None
size: self.minimum_size
orientation: 'vertical'
Depending on the sample size (due to hardware) it may or may not be able to render the text. If so, try with smaller sample size (like in your examples 500/600 instead of 900).
I am trying to get a background around a label that fits the number of lines in the texted in it. For example, a label with text 'Line1\nLine1\nLine3' would have a larger Y dimension that just 'line1'
What currently happens is all of the labels are the same size and clip-off text that doesn't fit within them, the labels are also inside a recycleview layout because I would like to be able to scroll and update large amount of the labels often.
I have tried a few things but have had no luck, and am struggling to get a variable to be understood where I have added # HERE in the .kv file
from kivy.app import App
from kivy.properties import NumericProperty, Clock, ObjectProperty, StringProperty
from kivy.uix.widget import Widget
from kivy.uix.recycleview import RecycleView
from kivy.uix.button import Button
from kivy.uix.label import Label
class TopPostsTest(RecycleView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
message_height = NumericProperty(20)
items = ["hello\ntest\ntest", "test, gghgjhgjhgjhgjhghjghjgjhgjhgjhgjhgjhgjhgjhgjhg", "cheese"]
self.data = [{'text':str(p)} for p in items]
class LabelColor(Label):
pass
class TruthApp(App):
# def build(self):
# return super().build()
pass
if __name__ == "__main__":
TruthApp().run()
<MainControl#PageLayout>:
border: "25dp"
swipe_threshold: 0.4
TopPostsTest:
Settings:
<LabelColor>:
color: 0,0,0,1
text_size: self.size
halign: 'left'
valign: 'top'
font_name: "Assets/Fonts/Nunito-Bold.ttf"
font_size: "12dp"
multiline: True
canvas.before:
Color:
rgba: (0.8, 0.8, 0.8, 1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [5, 5, 5, 5]
canvas:
Color:
rgba:0,0.9,0.9,1
Line:
width:0.8
rounded_rectangle:(self.x,self.y,self.width,root.height, 5) # HERE
<TopPostsTest>:
viewclass: 'LabelColor'
scroll_y: 1
RecycleBoxLayout:
id: message_view
default_size: None, dp(40) # NewHERE
default_size_hint: 1, None
size_hint_y: None
padding: ["10dp", "16dp"]
spacing: "8dp"
height: self.minimum_height
orientation: 'vertical'
Thank you for any help :)
Edit:
I have found that I have been changing the wrong value and that the variable that needs changing has been marker with # NewHERE, however I am still unable to get it to work or get a variable from the py file into the kv
In order to get a Label that expands vertically as it's text-content grows you can set its height to its texture height.
Also, to fit the text within available space (width) you can change text_size.
Thus you can modify the kvlang for LabelColor as,
<LabelColor>:
color: 0,0,0,1
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
How to make press method on this code working? When I press a button the list populates, but when I call it from Clock then not. I can see populate print on the console but the list does not appear in the view. I mean simply: how to stimulate pressing the button in the code?
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
from random import sample
from string import ascii_lowercase
import pyrebase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
kv = """
<Row#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
id: populate_btn
text: 'Populate list'
on_press: root.populate()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def populate(self):
print("populate")
self.rv.data = [{'value': ''.join(sample(ascii_lowercase, 6))}
for x in range(50)]
def press(self):
self.ids.populate_btn.dispatch('on_press')
def interval(dt):
x = Test()
x.press()
Clock.schedule_interval(interval, 3)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
The error is caused because the Test object created in interval function is different from the Test object that returns build method, besides that the Test object created in interval is eliminated since it is a local variable. So the solution is to use the same reference by passing it to the interval function for it I will use functools.partial() function.
# ...
from functools import partial
# ...
def interval(x, dt):
x.press()
class TestApp(App):
def build(self):
t = Test()
Clock.schedule_interval(partial(interval, t), 3)
return t
# ...
How can I make my boxlayout expand according to dynamically added content?
Here is my code to illustrate what I am trying to do:
main.py
from kivy.app import App
from kivy.uix.label import Label
class TestApp(App):
def add_label(self):
label = Label(text='StackOverflow', color=(0,0,0,1))
self.root.ids.myBox.add_widget(label)
if __name__ == '__main__':
TestApp().run()
test.kv
BoxLayout:
orientation: 'vertical'
spacing: 400
BoxLayout:
orientation: 'vertical'
size_hint: None, None
size: dp(280), dp(100)
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
#width: self.minimum_width
#height: self.minimum_height
id: myBox
background_color: (1,1,1,1)
canvas.before:
Color:
rgba: self.background_color
Rectangle:
pos: self.pos
size: self.size
Button:
text: 'BUTTON'
size_hint: None, None
size: dp(100), dp(50)
pos_hint: {'center_x': 0.5, 'center_y': 0.1}
on_release: app.add_label()
The button has the effect of dynamically adding a label to the boxlayout. But the more the labels are added, the size of the boxlayout does not change and the texts are superimposed.
Thanks in advance for your help.
One way to do this is do recalculate the size of the BoxLayout yourself. By changing your add_label() method to:
def add_label(self):
label = Label(text='StackOverflow', color=(0,0,0,1))
self.root.ids.myBox.add_widget(label)
label.texture_update()
boxlayout = self.root.ids.myBox
height = 0
for child in boxlayout.children:
height += child.texture_size[1]
height += 2 * child.padding_y
boxlayout.height = height
The BoxLayout changes height on each Label addition. You would think that you could use the child.height in the calculation, but the default value for height is always 100 until the layout is actually calculated. But, as you have seen, when the layout is calculated, the height of the label is reduced to fit in the BoxLayout. The texture of the Label can be updated before layout, and therefore, I have used its height in the calculation. This will decrease the size of the BoxLayout with the first Label added, and will increase it on every following addition. Note that this will only work for widgets that have a texture property like the Label or Button.
I want to make a simple game where you tap and you earn money. I made some code which does that however I don't know how to remove the label. Right now all it does is add 1 to the money variable and make new label.
.py
money = 0
class GameScreen(Screen):
def money(self):
global money
money += 1
self.add_widget(Label(text=str(money), color=(1,0,0,1), font_size=(45),size_hint=(0.2,0.1), pos_hint={"center_x":0.5, "center_y":0.9}))
print(money)
.kv
<GameScreen>:
name: "GameScreen"
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
size: self.texture_size
on_release: root.money()
text: "Press"
font_size: 50
color: 1,1,1,1
background_color: (0,0,0,1)
background_normal: ""
background_down: ""
size_hint: None, None
pos_hint: {"center_x":0.5, "center_y":0.6}
width: self.texture_size[0] + dp(10)
height: self.texture_size[1] + dp(10)
Removing a Label to place a widget with another text consumes recourses in an unavoidable way, you only have to update the text. So you must add the label the first time and then update the text. On the other hand it is recommended that the name of variables, classes and functions are not the same. And try to avoid using global variables because they are difficult to debug.
Making those changes we obtain the following code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.screenmanager import Screen
class GameScreen(Screen):
def __init__(self, **args):
Screen.__init__(self, **args)
self.money = 0
self.label = Label(text=str(self.money), color=(1,0,0,1), font_size=(45),size_hint=(0.2,0.1), pos_hint={"center_x":0.5, "center_y":0.9})
self.add_widget(self.label)
def add_money(self):
self.money += 1
self.label.text = str(self.money)
Builder.load_string('''
<GameScreen>:
name: "GameScreen"
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
size: self.texture_size
on_release: root.add_money()
text: "Press"
font_size: 50
color: 1,1,1,1
background_color: (0,0,0,1)
background_normal: ""
background_down: ""
size_hint: None, None
pos_hint: {"center_x":0.5, "center_y":0.6}
width: self.texture_size[0] + dp(10)
height: self.texture_size[1] + dp(10)
''')
class TestApp(App):
def build(self):
return GameScreen()
if __name__ == '__main__':
TestApp().run()