Why Kivy ScrollView children of child are not scrollable? - python

This is a part of a python-script running kivy:
class someclass(Widget):
# code
# code
Clock.schedule_interval(self.timeandlog, 0.1)
self.x = 20
def timeandlog(self,dt):
if condition == True: #
self.ids.pofchild.add_widget(Label(text=logmsg, pos = (10, self.x)))
self.x = self.x + 10 ### just playing with position
condition = False
kv file:
<someclass>
#somelabels and buttons:
ScrollView:
do_scroll_x: False
do_scroll_y: True
pos: root.width*0.3, root.height*0.7
size: root.width*0.8, root.height*0.7
Widget:
cols: 1
spacing: 10
id: pofchild
Now I know the ScrollView accepts one Widget so I added just one with an id: pofchild then I added labels inside it with self.ids.pofchild.add_widget(Label() and changing every new label's pos with pos=(20, self.x) but the labels are not scrollable and only fill the widget height then stop appearing. What are right attributions so they will be scrollable?

In general, when you want a Widget to contain other Widgets, you should use a Layout Widget. A simple Widget does not honor size_hint or pos_hint, so the children of a simple Widget often end up with the default size of (100,100) and the default position of (0,0).
So, a good start is to change:
class someclass(Widget):
to something like:
class Someclass(FloatLayout):
Note that the class name starts with a capital letter. Although it does not cause any difficulties in your example, it can produce errors when you use kv and your classname starts with lower case.
Similarly, the child of the ScrollView is also normally a Layout. A possibility is GridLayout, like this:
GridLayout:
size_hint_y: None
height: self.minimum_height
cols: 1
spacing: 10
id: pofchild
Keys attributes here are the size_hint_y: None and height: self.minimum_height. They allow the GridLayout to grow as more children are added, and its height will be calculated as the minimum height needed to contain the children.
Then, you can add children like this:
self.ids.pofchild.add_widget(Label(text=logmsg, pos=(10, self.x), size_hint_y=None, height=50))
Since we are expecting the GridLayout to calculate its minimum height, we must provide an explicit height for its children, thus the size_hint_y=None, height=50.

Related

How to correctly bind callbacks to each element in a loop in Python Kivy library

I have the following code:
from functools import partial
class DishesScreen(Screen):
def on_pre_enter(self):
Window.size = (800, 800)
for i in range(10):
self.layout.add_widget(AsyncImage(source='<url>', allow_stretch=True,
size_hint_y=None, size_hint_x=None))
dish_widget = self.get_params_layout()
dish_widget.bind(on_touch_down=partial(self.move, data[i]))
self.layout.add_widget(dish_widget)
def get_params_layout(self):
params_layout = BoxLayout(orientation='vertical', size_hint_y=None, spacing=10, padding=10)
return params_layout
def move(self, *args):
# some actions here
pass
What I want to do is to call move() function with parameters specific for each element in the list of BoxLayout's.
If I click on the second element - I expect move() function execution with the parameter specific for the second element, if I click on the 7th element - I expect move() function execution with the parameter specific for the 7th element, etc.
Instead, no matter on which element I click, the move() function executes 10 times, each time it uses the parameter for the element starting from the last and ending by the first element.
I think it is because I add dish_widget to the layout: self.layout.add_widget(dish_widget). And this means that when I click somewhere, actually I click on the layout, and the program executes all 10 move() functions attached to the layout. But cannot figure out how to change this behaviour. I need only one call of the move() function - the only that is attached to the element (BoxLayout) on which I clicked in the list. Can anybody help, please?
UPDATE:
Here is the markup:
<DishesScreen>:
layout: layout
ScrollView:
do_scroll_x: False
do_scroll_y: True
GridLayout:
id: layout
cols:2
size_hint: 1, None
height: self.minimum_height
spacing: 5, 5
All of your Widgets will receive the touch event. From the Widget Documentation:
on_touch_down(), on_touch_move(), on_touch_up() don’t do any sort of
collisions. If you want to know if the touch is inside your widget,
use collide_point().
So your move() method must do a collision test to determine if the touch was within the bounds of that Widget. Something like this:
def move(self, data, source, touch):
if source.collide_point(*touch.pos):
print('move', data)
return True # stop the bubbling of this touch to other widgets

Add multiple GridLayouts inside of a GridLayout kivy

I want to be able to dynamically add GridLayouts to a single GridLayout container.
my code:
layout = GridLayout(cols=1, size_hint= (None,None))
...
get_score = data.get(key, {}).get('score')
can = self.root.get_screen("three")
for x in range(-1, get_score): # had to use -1 to get correct amount of shapes
can.ids.my_box.add_widget(layout)
kv:
GridLayout:
cols: 1
id: my_box
size_hint_y: None
height: self.minimum_height
row_force_default: True
row_default_height: 50
When I try doing this I get error
kivy.uix.widget.WidgetException: Cannot add <kivy.uix.gridlayout.GridLayout object at 0x00000274FEFD9B40>, it already has a parent <kivy.uix.gridlayout.GridLayout object at 0x00000274FEFD99A0>
You're trying to add the same widget (a GridLayout named layout) multiple times in your loop. Try putting the layout = GridLayout(cols=1, size_hint= (None,None)) line inside the for loop, then it'll be a different widget each time and your code should run.

Dealing with ScrollViews in Kivy

I want to draw a border around a ScrollView in my Kivy app. The problem is that the content of the ScrollView overlap that border since I'm drawing it inside the widget.
So I'm wondering if one of those is a possible solution:
How can I draw outside the widget's boundaries?
When I tried to move a part of a canvas element outside the widget, it simply cut off that part, which is no surprise. Perhaps I could make another widget outside this one and draw on it?
How can I limit the content of the ScrollView? So, can I change the scroll boundaries? What I mean is that I don't want the children to go beyond a certain point in the widget to make them not touch the border
Here is some test code to demonstrate the issue. It's a slightly modified official example. The buttons overlap the green border when scrolling, which is what I don't want:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
Builder.load_string('''
<ScrollView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Color:
rgba: 0, 1, 0, 1
Line:
points: self.x, self.y + self.height,\
self.x + self.width, self.y + self.height,\
self.x + self.width, self.y, self.x, self.y,\
self.x, self.y + self.height
width: 1.2
''')
class TestApp(App):
def build(self):
layout = GridLayout(cols=1, padding=10, spacing=10,
size_hint=(None, None), width=500)
layout.bind(minimum_height=layout.setter('height'))
for i in range(30):
btn = Button(text=str(i), size=(480, 40),
size_hint=(None, None))
layout.add_widget(btn)
root = ScrollView(size_hint=(None, None), size=(500, 320),
pos_hint={'center_x': .5, 'center_y': .5}, do_scroll_x=False)
root.add_widget(layout)
return root
TestApp().run()
How can I draw outside the widget's boundaries?
When I tried to move a part of a canvas element outside the widget, it simply cut off that part, which is no surprise. Perhaps I could make another widget outside this one and draw on it?
Don't really have a clue what you mean by it. canvas has no boundaries, except if you use Stencil. When you try to draw outside of ScrollView, content out of the widget's space will be hidden, because originally it uses Stencil so that the unnecessary content is hidden and you could scroll through it as you can see here.
Now about that overlaping. canvas.after before the second Color makes it draw after the widgets are drawn. Similar to canvas.before, which is used for backgrounds.
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
canvas.after:
...
Basic canvas is drawn in the "same" time as the widgets are, therefore the item that is drawn later will be on the top no matter what you do. after will draw after all is drawn again with that order and before does the same thing before the widgets are even drawn at all.
How can I limit the content of the ScrollView? So, can I change the scroll boundaries? What I mean is that I don't want the children to go beyond a certain point in the widget to make them not touch the border
Use layout with fixed size that handles it's children's pos e.g. FloatLayout with pos_hint or BoxLayout depends on your need, therefore it will push the widgets inside to imaginary rectangle, which is basically [0, 0] to [width, height] i.e. size of your layout(if you use it right).

Kivy weird width property consequences

As I've coded a Kivy app, I've put a little width property into my custom widget definition for testing: my function should have created a new width and this one should not have been used anywhere. So I forgot to remove it over time. But now, when I got to cleaning up the code and tried to remove it, the height of the widget broke. The height is also dynamic, but as far as I can see, it has nothing to do with initial width of the widget, as the creation of the height occurs after the new width has been assigned. So I'm sort of confused as to what is causing this. Note: I do use a protected property to calculate the height, perhaps it is responsible? I've thrown together a quick dirty app, excuse the ugliness but I tried to shorten the code as much I could.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
global msg_stack
msg_stack = []
Builder.load_string('''
<Custom>:
x: 5
width: 500
BoxLayout:
pos: root.pos
height: self.height
TextInput:
pos: root.pos
size: root.size
id: msg
readonly: True
text: str(msg)
''')
class Custom(Widget):
pass
class TestApp(App):
def build(self):
msg = "A bunch of random words: words, words, words, words, words"
inst = Custom()
inst.ids['msg'].text = msg
inst.width = 500
inst.height = (len(inst.ids['msg']._lines_labels) + 1) * (inst.ids['msg'].line_height + 2)
for i in inst.walk():
i.height = inst.height
i.width = inst.width
bl = BoxLayout(orientation="vertical")
bl.add_widget(inst)
return bl
TestApp().run()
So if this property is set to 500, the height is just fitting for the amount of lines (you can see that if you add more text to the msg variable). But if I change it to something different like 50, suddenly the height increases.
Here is an attempt at dissection:
You originally set the width of Custom to 500. In build, text gets assigned to the TextInput, which computes the lines it needs for display from this original size, hence 1 line. If you set the width to 50 in the kv part, before instantiation, then the TextInput will need more lines to display the text, and since no constraint was put on height, it will be the default 100 (you can see this by commenting out inst.width = ... and inst.height = ... in build).
Now, if we comment out inst.width = ... but leave inst.height = ..., the height will be changed, because indeed we need some 16 lines to display all the text. And setting inst.width = 500 just before that line will make the container wide, then the new height will be calculated (which is still based on the TextInput's size, because the BoxLayout does not change its size), then you walk through the children and set their sizes explicitly to the Custom widget's size, which has the tall height.
I'm not sure what you want to achieve in the end, but this should explain the results you get.

Dynamically resizing a kivy label (and button) on the python side

How do I dynamically resize the a label or button, in particular, the text_size and height, depending on the amount of text, at run-time?
I am aware that this question has already been answered in one way with this question:
Dynamically resizing a Label within a Scrollview?
And I reflect that example in part of my code.
The problem is dynamically resizing the labels and buttons at run-time. Using, for example:
btn = Button(text_size=(self.width, self.height), text='blah blah')
...and so on, only makes the program think (and logically so) that the "self" is referring to the class which is containing the button.
So, how do I dynamically resize these attributes in the python language, not kivy?
My example code:
import kivy
kivy.require('1.7.2') # replace with your current kivy version !
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
i = range(20)
long_text = 'sometimes the search result could be rather long \
sometimes the search result could be rather long \
sometimes the search result could be rather long '
class ButtonILike(Button):
def get_text(self):
return long_text
class HomeScreen(Screen):
scroll_view = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super(HomeScreen, self).__init__(*args, **kwargs)
layout1 = GridLayout(cols=1, spacing=0, size_hint=(1, None), \
row_force_default=False, row_default_height=40)
layout1.bind(minimum_height=layout1.setter('height'),
minimum_width=layout1.setter('width'))
layout1.add_widget(ButtonILike())
for result in i:
btn1 = Button(font_name="data/fonts/DejaVuSans.ttf", \
size_hint=(1, None), valign='middle',)#, \
#height=self.texture_size[1], text_size=(self.width-10, None))
btn1.height = btn1.texture_size[1]
btn1.text_size = (btn1.width-20, layout1.row_default_height)
btn1.text = long_text
btn2 = Button(font_name="data/fonts/DejaVuSans.ttf", \
size_hint=(1, None), valign='middle')
btn2.bind(text_size=(btn2.width-20, None))
btn2.text = 'or short'
layout1.add_widget(btn1)
layout1.add_widget(btn2)
scrollview1 = self.scroll_view
scrollview1.clear_widgets()
scrollview1.add_widget(layout1)
class mybuttonsApp(App):
def build(self):
return HomeScreen()
if __name__ == '__main__':
mybuttonsApp().run()
And the kv file:
#:kivy 1.7.2
<ButtonILike>:
text_size: self.width-10, None
size_hint: (1, None)
height: self.texture_size[1]
text: root.get_text()
#on_release: root.RunSearchButton_pressed()
<HomeScreen>:
scroll_view: scrollviewID
AnchorLayout:
size_hint: 1, .1
pos_hint: {'x': 0, 'y': .9}
anchor_x: 'center'
anchor_y: 'center'
Label:
text: 'Button Tester'
ScrollView:
id: scrollviewID
orientation: 'vertical'
pos_hint: {'x': 0, 'y': 0}
size_hint: 1, .9
bar_width: '8dp'
You can see that I added the button from the kv file which displays all the behavior that I want at the top of the list. Resize your window while running it, and you can see the magic. And, of course, changing the text_size also makes it possible for me to align text.
I simply have not been able to achieve the same behavior on the python side. My app requires that the buttons be created at run-time. I think the answer might lie with "bind()", though admittedly, I'm not sure I used it correctly in my attempts or that I understand it fully. You can see that I tried with "btn2", which I thought would've thrown the text to the left (since halign defaults to left), but didn't seem to do anything.
I appreciate the help.
I think the best way is to set Label's/Button's size to texture_size:
Label:
text: "test"
size_hint: None, None
size: self.texture_size
canvas.before: # for testing purposes
Color:
rgb: 0, 1, 0
Rectangle:
pos: self.pos
size: self.size
My answer is slightly different from #martin's - I only want to modify the height.
def my_height_callback(obj, texture: Texture):
if texture:
obj.height = max(texture.size[1], 100)
class MyButton(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint = (1, None)
self.bind(texture=my_height_callback)
When the text is rendered the texture property of the button gets set. That texture's height is then pushed to the button's height via the callback. Calling max() allows for a minimum height to be set. This works fine with labels as well.
btn2.bind(text_size=(btn2.width-20, None))
As with your other question, the problem is that you have the syntax of bind wrong. You must pass a function, but you just wrote a tuple, and bind can't do anything useful with that - it certainly doesn't know you happened to write btn2.width there.
Also, the syntax is that bind calls the function when the given property changes. That's the opposite of what you want - you need to change the text_size when btn2.width changes, not call a function when text_size changes
I think something like the following would work. instance and value are the default arguments we ignored in the other question.
def setting_function(instance, value):
btn2.text_size = (value-20, None)
btn1.bind(width=setting_function)
I was looking to resize both the text_size width and height, the latter specifically with regard to the documented behaviour of kivy.Label that vertical alignment of text in a label cannot be achieved without doing this first. Further, I needed to do it in python, not .kv.
class WrappedVAlignedLabel(Label):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.bind(height=lambda *x:self.setter('text_size')(self, (self.width, self.height)))
strangely, binding on width instead of height would only set text_size[0], I guess due to some order of rendering self.height wasn't yet computed, so the setting of text_size[1] wasn't happening. Whereas binding on height gets them both.

Categories