Kivy weird width property consequences - python

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.

Related

How do you generate 6 colored labels in kivy based off of a list?

everyone.
I'm trying to utilize a python function to generate labels that will be based off strings in a list. However, from what I understand, because I am writing the code that creates the labels in the python file, it doesn't exactly work the way that I want it to.
I have two options. I could rewrite the python code function that generates the list so that it properly initializes the labels, OR, I can rewrite the code in the kv file so that I have a gridlayout that auto-updates based off of the map_object array that holds the strings that will be converted into the map 'tiles'.
I would rather generate generate labels and configure the style of the UI with kivy and keep the logic inside python, but I will do whatever method is easier. However, I'm not sure how to to pull off either method, which is why I'm here asking this question.
Also, keep in mind, the color for each item will be based off of the string. The '^' will be green, the '_' will be brown, and the '*' will be white. I'll have hex values in a dictionary that will be linked to each individual string, so when it comes time to generate the label's color, I will use that dictionary to get it. However, in this case, I simply used an RGBA value to simplify the code.
One last strange issue is that the code will currently generate 6 colorless labels and also add a red square in the bottom-right corner. I'm assuming this is because I create the labels with the python function without initializing them properly, which is why I would rather outright have the labels be created in the kivy file and be based off of an object, 'map_object' that can change array size with the push off a button and have a gridlayout that will properly update with the size of that array as well.
What do you suggest?
The code is below:
kivy-tests.py
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.graphics import Color, Rectangle
class Entry(BoxLayout):
# '^' label background color will be green
# '*' label background color will be white
# '_' label background color will be brown
map_object = [['^', '*', '_'], ['*', '^', '*']]
map_layout = GridLayout(cols = 3)
def generate_tile_labels(self):
# This if-else statement clears the grid of tiles
# each time the function is called.
if self.map_layout in self.children:
self.map_layout.clear_widgets()
else:
self.add_widget(self.map_layout)
for row in self.map_object:
for tile in row:
self.new_label = Label(text = 'Test')
self.new_label.canvas.before.add(Color(1,0,0,1))
self.new_label.canvas.before.add(Rectangle(pos=self.pos))
self.map_layout.add_widget(self.new_label)
class kvfiletests(App):
def build(self):
return Entry()
if __name__ == '__main__':
kvfiletests().run()
kvfiletests.kv
<Entry>
orientation: 'vertical'
Button:
text: 'Generate the Map'
on_press: root.generate_tile_labels()
GridLayout:
You can use a rule in the kv for the Label, and a dictionary of colors for the Labels:
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
kv = '''
<Entry>
orientation: 'vertical'
Button:
text: 'Generate the Map'
on_press: root.generate_tile_labels()
GridLayout:
id: grid
cols: 3
padding: 4
spacing: 4
<MyLabel>:
color: 0,0,0,1
canvas.before:
Color:
rgba: self.bg_color
Rectangle:
pos: self.pos
size: self.size
'''
class MyLabel(Label):
bg_color = ListProperty([0,0,0,1])
class Entry(BoxLayout):
# '^' label background color will be green
# '*' label background color will be white
# '_' label background color will be brown
colors = {'^': [0,1,0,1], '*': [1,1,1,1], '_': [150.0/256.0, 75.0/256.0, 0, 1]}
map_object = [['^', '*', '_'], ['*', '^', '*']]
def generate_tile_labels(self):
self.map_layout = self.ids.grid
self.map_layout.clear_widgets()
for row in self.map_object:
for tile in row:
self.new_label = MyLabel(text=tile, bg_color=self.colors[tile])
self.map_layout.add_widget(self.new_label)
class kvfiletests(App):
def build(self):
Builder.load_string(kv)
return Entry()
if __name__ == '__main__':
kvfiletests().run()
I have used a kv string just for my own convenience.
The reason you were getting a red square at the lower left is because your code creates a red Rectangle at the default size and position (which is (100,100) and (0,0) respectively). The Rectangle size and position are not updated in python unless you write the code to do the updating. Using kv provides automatic updating.

Width of root - kivy

I've got a problem with my kivy program... Especially with the width of my root, it's more less than the width of the window...
Like this :
here
I don't understand...
Here my code:
First python file :
from kivy.app import App
from kivy.config import Config
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.uix.button import Button
Config.set('graphics','width','450')
Config.set('graphics','height','800')
class Saisi(Widget):
pass
class Jeu(Widget):
pass
class WorDown(App):
def build(self):
return Jeu()
if __name__ == '__main__':
WorDown().run()
And my kivy file:
<Saisi>:
canvas:
Rectangle:
pos: self.pos
size: root.width , 50 ← I think, this is it...
<Jeu>:
Saisi:
y: root.height / 2
Someone can help me ? I just want to "resize" the "root width", because all my elements have a max width like this...
Thanks for reading.
<Jeu>:
Saisi:
y: root.height / 2
Jeu is a widget and not a special layout type, so it doesn't impose any position or size on its children, therefore the Saisi instance has the default position of (0, 0) and size of (100, 100).
Make Jeu inherit from e.g. BoxLayout (recommended), or alternatively manually set the Saisi pos/size to match that of the Jeu in the above rule.

kivy NumericProperty to StringProperty

I tried to make my own Coockie Clicker in kivy, but with cristmas coockies.
I created an Image of an Coockie, you can click on and an Label that shows you how many times you have clicked.
The Label required an String, so I tried to convert the numeric property into an string, but it did not work, because I got the error message:
<kivy.properties.NumericProperty object at 0xa6e32cc>
This is the part of the code, where I suspect the error:
class Kecks(Widget):
count = NumericProperty(0)
amount = NumericProperty(1)
txt = StringProperty(str(count))
Here is the rest of the code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.animation import Animation
from kivy.core.text.markup import *
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import NumericProperty
from kivy.properties import StringProperty
Builder.load_string('''
<Root>:
Kecks:
pos: 300, 300
<Kecks>:
Image:
pos: root.pos
id: my_image
source: 'piernik.png'
Label:
id: my_Label
font_size: 50
text: root.txt
center_x: root.width / 4
''')
class Root(FloatLayout):
pass
class Kecks(Widget):
count = NumericProperty(0)
amount = NumericProperty(1)
txt = StringProperty(str(count))
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.count += self.amount
print self.txt
class app(App):
def build(self):
Window.clearcolor = (10, 0, 0, 1)
return Root()
if __name__ == "__main__":
app().run()
That's not an error message, that's a string identifying a property instance. Do you really mean you got an error, or is that what's printed by your program? I guess the latter, because...
count = NumericProperty(0)
amount = NumericProperty(1)
txt = StringProperty(str(count))
To understand this, you need to know a little about how properties work - you declare them at the class level, not within a method, so they are attributes of the class (which are inherited as attributes of individual instances). One effect of this is that all instances of the class share the same property objects.
When you access them within a method, e.g. with self.count, you get an instance-specific result that behaves like a normal non-property attribute, even though it's really a property object, because the property internally takes care of returning the right thing (I think it's right to say it's a descriptor).
What's happening here is that you're accessing the result at class level, when it doesn't have any special behaviour - you asked for str(count) and that's what you got, the string identifying the property object.
Probably the correct pythonic way to resolve this is
class Kecks(Widget):
count = NumericProperty(0)
amount = NumericProperty(1)
txt = StringProperty()
def __init__(self, *args, **kwargs):
super(Kecks, self).__init__(*args, **kwargs):
self.txt = str(self.count)
By setting the value in __init__, you get the correct instance-level behaviour. You can also do stuff like mess around with accessing count.defaultvalue to get its actual value, but this is probably a bad idea.
If you want txt to be bound to the value of count (automatically changing to track str(count)), you have to do more again, but I'm not clear if that's your intention so I won't go into it and you can check the doc anyway.

kivy: my function points to other/new widget when scheduled with clock

I have my code set up to initially add a few button widgets.
The first button will spawn a rectangular box. This is written as addfunction().
the second button is bound to perform hookupfull(), which does two things - gethooks() and hookfunc():
First it grabs the pos, width, and height of the children of my MainWindowWidget, and puts it in a list for each of those, then deletes extra entries from the buttons (I have my buttons as children of the MainWindowWidget currently, but I only want the properties of the boxes). This is written as gethooks()
Secondly it calculates fancy coordinates from the list and draws a line. This is written as hookfunc()
So, if I press the first button twice and the second button once, it will create two boxes and then draw a line connecting them together. This works fine. Next on my agenda is to schedule stuff like canvas.clear() and redraw the line, etc. every 1/60th of a second. I then create a third button widget to set a flag to start update loop run.
However if I try to schedule hookupfull() with Clock.schedule_interval() it doesn't work as I think it should - not sure how to explain it or what is going on, but the scheduled code doesn't seem to "go" to the MainWindowWidget I want. It seems to be spawning a whole bunch of other MainWindowWidgets.
I figured it's the way I'm referring to the widgets or something with the arguments (which is what I assume to be the (self, *args) portion of the code) or the way I'm declaring the method/function (I'm not sure of the difference between methods and functions still, sorry)
So, I tried to debug it by adding stuff like print self in several places to see what self was.
My code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.scatter import Scatter
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.graphics import Color, Line
from kivy.clock import Clock
boxscale = 1
global startenable
global debug_1
startenable = False
debug_1 = True
class Scatterer(Scatter):
pass
class Drawer(FloatLayout):
pass
class MainWindowWidget(FloatLayout):
def addfunction(self, *args):
s = Scatterer()
d = Drawer()
s.size = 60 * boxscale , 111 * boxscale
# default size * scale
d.size = s.size
self.add_widget(s)
s.add_widget(d)
print "button is pressed"
def startfunc(obj):
global startenable
if startenable == False:
startenable = True
else:
startenable = False
print'startenable set to',startenable
def gethooks(self, *args):
# get hook locations
self.p = []
for child in self.children:
self.p.append(child.pos)
del self.p[(len(self.p)-3):(len(self.p))]
self.w = []
for child in self.children:
self.w.append(child.width)
del self.w[(len(self.w)-3):(len(self.w))]
self.h = []
for child in self.children:
self.h.append(child.height)
del self.h[(len(self.h)-3):(len(self.h))]
if debug_1 == True:
print 'getting hook location........'
print 'self.p:',self.p # list of positions
print 'length of self.p:',len(self.p)
print 'widths:',self.w # list of widths
print 'heights:',self.h# list of heights
print self
def hookfunc(self, *args):
# draw line based on hooks' position
self.h_01_x = \
self.p[0][0]
self.h_01_y = \
self.p[0][1] + (self.h[0]/2)
self.h_02_x = \
self.p[1][0] + self.w[1]
self.h_02_y = \
self.p[1][1] + (self.h[1]/2)
with self.canvas:
Line(bezier=(
self.h_01_x, self.h_01_y,
self.h_01_x - 20, self.h_01_y,
self.h_02_x + 20, self.h_02_y,
self.h_02_x, self.h_02_y,
), width=2)
print self
def hookupfull(self, *args):
self.gethooks()
self.hookfunc()
def update(self, *args):
global debug_1
if startenable == True:
mww= MainWindowWidget()
print mww
mww.hookupfull()
else: # if startenable is false
pass
class Test2App(App):
def build(self):
Clock.schedule_interval(MainWindowWidget.update, \
5.0/60.0)
return MainWindowWidget()
if __name__ == '__main__':
Test2App().run()
kv file:
#:kivy 1.0.9
<MainWindowWidget>
NewButton:
text: 'add'
pos: 100, 0
on_release: root.addfunction()
NewButton:
text: 'start'
pos: 200, 0
on_release: root.startfunc()
NewButton:
text: 'hook up full'
pos: 400, 0
on_release: root.hookupfull()
<NewButton#Button>:
font_size: 15
size_hint: None, None
size: 100, 100
<Scatterer>:
do_rotation: False
size_hint: None, None
size: self.size
<Drawer>:
size: self.size
canvas:
Color:
rgba: 0, 1, 0, 0.3
Rectangle:
pos: self.pos
size: self.size
What I found out was, when I clicked the button that does hookupfull(), which print self, they always return the same thing such as <__main__.MainWindowWidget object at 0x7f110fcef530> no matter how many times hookupfull() is called.
However when the same function is scheduled, self always returns different widgets.
Some other questions I'm wondering about:
Is there a way to assign id's or references to dynamically created widgets? For example, in my code, pressing the first button will create a new Scatter object. Is there a way to point to that specific Scatter object?
How do I refer to widget grandchildren? For example, in my code, I have a FloatLayout object set up as a child of a Scatter object, which is a child of the MainWindowWidget. The FloatLayout object changes in size due to the Scatter, which acts like a controller (and to my knowledge, doesn't actually change in size). I would like to access the properties such as pos and width of the FloatLayout.
arguments for the method/function declarations such as (self, *args) - what do they do?
Each function you define in your class should have self as the first parameter (except for static methods) by convention. It's the instance object of the class when the function is called. You were also creating new instances of the main widget in some functions: refer to inclement's comments above. It doesn't make much sense to do this. Please read the manual: https://docs.python.org/2/tutorial/classes.html#class-objects
Regarding your other questions:
Is there a way to assign id's or references to dynamically created
widgets?
You can assign and id directly by using the named id parameter when instantiating the object e.g. Scatterer(id='myid'). You can refer to it by its id but my personal preference would be to track the objects with a list (see below)
How do I refer to widget grandchildren?
Simply put, don't! Track them directly in a list instead and use references to their properties instead of creating secondary lists of dimensions:
def __init__(self, *args, **kwargs):
self.box_list = [] # <---- set up list to track boxes
super(MainWindowWidget, self).__init__(*args, **kwargs)
....
....
if len(self.box_list) > 1:
box1 = self.box_list[-2] # grab the last two boxes added to the list
box2 = self.box_list[-1]
h_01_x = box1.x
h_01_y = box1.y + (box1.height/2)
h_02_x = box2.x + box2.width
h_02_y = box2.y + (box2.height/2)
with self.canvas:
Line(bezier=(
h_01_x, h_01_y,
h_01_x - 20, h_01_y,
h_02_x + 20, h_02_y,
h_02_x, h_02_y,
), width=2)
I would like to access the properties such as pos and width of the
FloatLayout.
The scatter does have both size and pos. Both are updated when you change the object. It would also be better if you declared your FloatLayout directly in kivy as a child of Scatterer.. There's no need to create them separately
arguments for the method/function declarations such as (self, *args) - what do they do?
self is a reference to the instance whose function has been called. *args is the expansion of the list of unnamed parameters. Take a look at this question: *args and **kwargs?
As a final comment, if you call canvas.clear as you mentioned in your description then you'll erase the buttons and boxes too because you're using the canvas of the main widget. Create another class to contain your line and link its position to the boxes. When you call canvas.clear on that object then it'll clear only the line. If you update the line object when the box position/size changes then you can also move the rendering of the line into the kivy file and get rid of the extraneous clock scheduling.

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