Add multiple GridLayouts inside of a GridLayout kivy - python

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.

Related

Why Kivy ScrollView children of child are not scrollable?

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.

Kivy : How can i reset checkbox active value

When I switch between screens i want to clear the check boxes marked.Marked boxes remain checked when i change screens.
I think my problem will be solved if I find a way to change the activation of checkboxes when I switch to another screen.
But I don't know how to do that.
My Code also has a certain number of checkboxes I choose only six of them. The functions in my main file are to calculate them.
My main.py
class SkillChose(Screen):
checkboxvalues = {}
for i in range(1, 21):
checkboxvalues["s{}".format(i)] = -2
def __init__(self,**kwargs):
super(SkillChose,self).__init__(**kwargs)
self.click_count = 0
self.skills=[]
def click_plus(self,check,id):
if check is True:
self.click_count+=1
self.checkboxvalues[id]=1
return True
def click_extraction(self,id):
if self.checkboxvalues[id]==1:self.click_count-=1
self.checkboxvalues[id]=0
return False
def control(self,id):
if id==0:return False
count=0
for open in self.checkboxvalues.values():
if open==1:
count+=1
for i,j in self.checkboxvalues.items():
print(i,j)
if count<6:
return True
else:
return False
my.kv file
<SkillChose>:
name:"skill"
BoxLayout
ScrollView:
size: self.size
GridLayout:
id: grid
size_hint_y: None
row_default_height: '50sp'
height: self.minimum_height
cols:2
Label:
Label:
Label:
text:"skill1"
CheckBox:
value:"s1"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
Label:
text:"skill2"
CheckBox:
value:"s2"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
Label:
text:"skill3"
CheckBox:
value:"s3"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
The following enhancements (kv file & Python script) are required to clear the CheckBox's attribute active when leaving a screen.
kv file
Use ScreenManager on_leave event to invoke a callback e.g. reset_checkbox()
Snippets - kv file
<SkillChose>:
name:"skill"
on_leave: root.reset_checkbox()
BoxLayout:
...
Py file
Add import statement, from kivy.uix.checkbox import CheckBox
Use for loop to traverse the children of GridLayout: via ids.grid
Use isinstance() function to check for CheckBox widget
Snippets - Py file
class SkillChose(Screen):
...
def reset_checkbox(self):
for child in reversed(self.ids.grid.children):
if isinstance(child, CheckBox):
child.active = False
...

Python-kivy: Why does the for loop act differently from the while loop when removing items in kivy?

I noticed a difference between the for loop and the while loop when trying to remove items from a layout. Here is a little code to illustrate:
test.kv
BoxLayout:
orientation: 'vertical'
spacing: 20
BoxLayout:
orientation: 'vertical'
spacing: 20
id : box
Button:
text: 'BUTTON1'
Button:
text: 'BUTTON2'
Button:
text: 'BUTTON3'
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.del_button()
With a for loop:
main.py
from kivy.app import App
class TestApp(App):
def del_button(self):
children = self.root.ids.box.children
for i in children:
self.root.ids.box.remove_widget(i)
if __name__ == '__main__':
TestApp().run()
In this first case, when I press the 'BUTTON', the first and third buttons are removed, but the 'BUTTON2' remains visible.
With a while loop:
main.py
from kivy.app import App
class TestApp(App):
def del_button(self):
children = self.root.ids.box.children
i = 0
while i < len(children):
self.root.ids.box.remove_widget(children[i])
if __name__ == '__main__':
TestApp().run()
In this second case, all the buttons are removed directly.
With the for loop, not all elements are removed the first time, while the while loop does the job well. Why this strange behavior?
Thank you in advance for your answers.
The for statement creates an iterator to use in its looping. The iterator is created at the start of the loop (See for loop documentation), so it may not be correct if you have changed the list after the start of the loop.
The while loop, however, does not use an iterator and will not be confused by the changing list structure. In fact, a simpler implementation of your del_button() method could be:
def del_button(self):
children = self.root.ids.box.children
while len(children) > 0:
self.root.ids.box.remove_widget(children[0])
for loop
1st Iteration
Before the for loop, children contains a list of three memory location of button widgets, [Button3, Button2, Button1].
In the first iteration, i is referencing the first item in children and it removed widget, Button3.
2nd Iteration
Before the second iteration, children contains a list of two memory location of button widgets, [Button2, Button1].
In the second iteration, i is referencing the second item in children and it removed widget, Button1.
3rd Iteration
Before the third iteration, children contains a list of two memory location of button widgets, [Button2].
Since there is no third item in children, the for loop exit.
While loop
Since i (index) always remains at 0, therefore, it was successful in removing all the three button widgets because it is always referencing the first item in the list, children. self.root.ids.box.remove_widget(children[i]) is equivalent to self.root.ids.box.remove_widget(children[0])
Solution
The solution for the for loop is use reversed(self.root.ids.box.children).
Snippet
def del_button(self):
for child in reversed(self.root.ids.box.children):
self.root.ids.box.remove_widget(child)

getting an error AttributeError: 'FlashDisplayPage' object has no attribute 'label_name'

I am using carousel in FlashDisplayPage of my app. and I want to use label inside the carousel. I searched relevant sites and made the below code. I am new to kivy so I don't know if this is the right way. If its not how can I make it right?
And an error is also occurring here, even though I have defined label_name in FlashDisplayPage I am getting this error-AttributeError: 'FlashDisplayPage' object has no attribute 'label_name'. Why this error is showing up? how can I remove it?
here the part of the code where the error is occurring-
class FlashDisplayPage(Screen):
def on_enter(self):
Num=self.index
card=['flash_card1.json','flash_card2.json','flash_card3.json','flash_card4.json','flash_card5.json','flash_card6.json','flash_card7.json','flash_card8.json','flash_card9.json','flash_card10.json']
label_name='Flash Card '+ Num #here I have defined label_name
with open(card[Num+1]) as frfile:
flash_data=json.load(frfile)
for i in flash_data:
self.ids.CarDisplay.add_widget(Label(text=i['word']+' : '+i['meaning']))
# here I am adding label to carousel
def next_one(self):
self.ids.CarDisplay.direction='right' # next label in carousel
def previous_one(self):
self.ids.CarDisplay.direction='left' # previous label in carousel
here the part of kv code related to this-
<FlashDisplayPage>:
BoxLayout:
orientation: 'horizontal'
spacing:15
padding: 20
Label:
id: l
text: root.label_name
sixe_hint_y: None
height: 100
Carousel:
id: CarDisplay
loop: True
Button:
text:'next'
size_hint: None,.20
width: 30
on_press:root.next_one()
Button:
text:'next'
size_hint: None,.20
width: 130
on_press:root.previous_one()
label_name is a local variable within the on_enter function, and local variables can only be accessed within the function or scope where they were created. If you want to be accessible a possible solution is to make it a property of the widget:
from kivy.properties import StringProperty
class FlashDisplayPage(Screen):
label_name = StringProperty("")
def on_enter(self):
[...]
self.label_name='Flash Card {}'.format(Num)
[...]
Try using self.label_name to create an instance variable instead of a local variable in on_enter() which has local scope only.

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