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

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.

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

How to position kivy widgets in floatlayout using pos_hint?

I'm stuck trying to position widgets in kivy in a FloatLayout using pos_hint.
If the label exists from the beginning, e.g. if I can define the pos_hint in the .kv file, everything works as expected. However, I'm trying to create buttons later on. Using this .kv file (named layouttest.kv):
<NewButton#Button>:
size_hint: (0.1, 0.1)
<BasicFloatLayout#FloatLayout>:
Button:
size_hint: (0.4, 0.2)
pos_hint: {'x': 0.0, 'top': 1.0}
text: 'create Button'
on_release: self.parent.create_Button()
and this python code, I am trying to position newly created blank buttons at a random y-position ranging from 0%-100% of the size of my BasicFloatLayout, and at a random x-position ranging from 0-200px.
If I press the button once, everything behaves as expected. On a second press, the first created button will change its y-position such that it is identical with the newly created button. On a third press, both old buttons will align with the newly created button and so on. The x-positioning will however remain as expected. Can someone please explain what I'm doing wrong here?
(Bonus points if you can help me moving the buttons using the update function and pos_hint)
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.properties import NumericProperty
from kivy.clock import Clock
import random
class BasicFloatLayout(FloatLayout):
timer = NumericProperty(0)
def update(self, *args):
self.timer += 1
for child in self.children:
try: child.update()
except AttributeError:
pass
def create_Button(self):
button = NewButton( (random.random(), random.random()) )
self.add_widget(button)
class NewButton(Button):
def __init__(self, pos, **kwargs):
super(NewButton, self).__init__(**kwargs)
self.pos[0] = 200*pos[0]
self.pos_hint['y'] = pos[1]
class layouttestApp(App):
def build(self):
GUI = BasicFloatLayout()
Clock.schedule_interval(GUI.update, 1/30.)
return GUI
if __name__ == "__main__":
layouttestApp().run()
First, make pos_hint: {'x': 0.0, 'y': 0.5}(because it's hard to get 2 different things work, if you are using x, use y instead of Top, if you are using Top, then use Bottom insead of x)
Second, instead of giving on_release: self.parent.create_Button() in the kv file, do this: on_release: root.create_Button()
Third, you have only assigned for the y value , you should also assign for the x value, the line is self.pos_hint['y'] = pos[1] inside the NewButton class.
But you can make it more simple by doing this:
#from your first class......
def create_Button(self):
button = NewButton(self.pos_hint{'x' : random.random(), 'y' : random.random()}
self.add_widget(button)
class NewButton(Button):
def __init__(self, *kwargs):
pass
Hope this makes some kind of sense, and you can modify it more.
(Note: I haven't wrote the begining part of your main class, I am lazy ;-p)

Kivy: Updating a Label-Text letter by letter via interval doesn't work

I am deperately trying to create a "typewriter" effect in a Kivy Label. The Text 'That is my sample text' below should fill my label letter by letter with a 0.5 interval between each letter. Just like someone typing it with a type writer.
However the result i get is weird: Instead of getting the desired effect, the whole string is added after 0.5 and thats it. It seems like my for-loop is completely ignored.
Any idea what I can do?
That's my class TestScreen(Screen):
def __init__ (self,**kwargs):
super(TestScreen, self).__init__(**kwargs)
my_box = FloatLayout()
self.mylabel = Label(
text='',
font_size=26,
pos_hint={'center_x': 0.5, 'center_y': 0.05})
my_box.add_widget(self.mylabel)
self.add_widget(my_box)
for letter in 'That is my sample text':
Clock.schedule_once(partial(self.setLetterByLetter, letter=letter), 1)
def setLetterByLetter(self, dt, letter):
self.mylabel.text += letter
return True
Question
get the text starting in the upper left corner of the label
Solution - text in top-left corner
Add the following in the constructor, __init__() method.
def __init__(self, string, **kwargs):
super(TestScreen, self).__init__(**kwargs)
self.bind(size=self.setter('text_size'))
self.halign = 'left'
self.valign = 'top'
Text alignment and wrapping
The Label has halign and valign properties to control the alignment of
its text. However, by default the text image (texture) is only just
large enough to contain the characters and is positioned in the center
of the Label. The valign property will have no effect and halign will
only have an effect if your text has newlines; a single line of text
will appear to be centered even though halign is set to left (by
default).
In order for the alignment properties to take effect, set the
text_size, which specifies the size of the bounding box within which
text is aligned. For instance, the following code binds this size to
the size of the Label, so text will be aligned within the widget
bounds. This will also automatically wrap the text of the Label to
remain within this area.
Label:
text_size: self.size
halign: 'right'
valign: 'middle'
Output - text in top-left corner
Solution - text in center
Using Clock.create_trigger() to simulate a typewriter.
Triggered Events
A triggered event is a way to defer a callback. It functions exactly
like schedule_once() and schedule_interval() except that it doesn’t
immediately schedule the callback. Instead, one schedules the callback
using the ClockEvent returned by it. This ensures that you can
call the event multiple times but it won’t be scheduled more than
once. This is not the case with Clock.schedule_once()
main.py
from kivy.app import App
from kivy.uix.label import Label
from kivy.clock import Clock
class TestScreen(Label):
def __init__(self, string, **kwargs):
super(TestScreen, self).__init__(**kwargs)
self.font_size = 26
self.string = string
self.typewriter = Clock.create_trigger(self.typeit, 1)
self.typewriter()
def typeit(self, dt):
self.text += self.string[0]
self.string = self.string[1:]
if len(self.string) > 0:
self.typewriter()
class TestApp(App):
title = "Kivy Typewriter"
def build(self):
return TestScreen("That is my Kivy Typewriter demo")
if __name__ == "__main__":
TestApp().run()
Output - text in center
Don't need to use kwargs
To create a typewriter effect, just follow the code below.
def anything(str):
for letter in str:
sys.stdout.write(letter)
sys.stdout.flush()
time.sleep(0.5)
anything("That is my sample text.")
The code I've written gives a 0.5 second waiting time between every letter in the string, and only takes six lines of code.

Drop down menu in Kivy Python

I had found a similar topic at stackoverflow but unluckily it didn't help me.
It's the first time I try to seriously program a GUI and I'm really getting mad.
I'm doing one step at a time, towards what I will finally need.
Now I'm trying to add a simple drop down menu in the top left corner of my widget, whose element should call a function whenever they are selected. I really looked for this in kivy documentation, and in this forum but I can't really solve this.
import multiprocessing
from mesh import MeshApp
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
import os
MAINDIR = os.path.dirname(os.path.realpath(__file__))
categories = {}
def getCategories():
for dir in os.walk(MAINDIR):
if len(dir[1]) == 0:
filelist = set()
for mesh in dir[2]:
filelist.add(mesh.replace('_FRONT.png','').replace('_SIDE.png','').replace('_TOP.png',''))
categories.update({dir[0]: filelist})
#class CategoriesList(DropDown):
# pass
class MainWindow(Widget):
def __init__(self):
#self.categorieslist = CategoriesList()
categories_list = DropDown()
for i in categories.keys():
btn = Button(text=i.replace(MAINDIR, ''), size_hint_y=None, height=30)
btn.bind(on_release=lambda btn: categories_list.select(btn.text))
categories_list.add_widget(btn)
mainbutton = Button(text='Choose directory', size_hint=(1, 1))
mainbutton.bind(on_release=categories_list.open)
categories_list.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
#and now???
class RenderApp(App):
def build(self):
self.launchMeshApp()
return MainWindow()
def launchMeshApp(self):
app = MeshApp()
p = multiprocessing.Process(target=app.run)
p.start()
if __name__ == '__main__':
getCategories()
RenderApp().run()
And:
#:kivy 1.9.1
<MainWindow>:
canvas.before:
Color:
rgba: 0.6, 0.6, 1, 1
Rectangle:
pos: self.pos
size: self.size
canvas:
Color:
rgba: 0, 0, 0, 0.5
Rectangle:
pos: 0, self.height * 5 / 6 - 1
size: self.width, 2
I've created the dropdown as seen in the docs and in several other forum. But I need to place it in the top left corner, and I never found, or understood, the way to do this. Moreover I didn't get how to make them call a function with a parameter whenever they are clicked.
Thank you very much
EDIT: I don't know why but the first line with "Hi all" is automatically deleted
I don't know about the dropdown menu, but I can answer the data one.
When you bind a callback, the first argument you receive will be which widget that is bound to the callback. So the idea is to create a class that uses Button as its base class, then you can define whatever extra information you need.
Here is a rough, non tested example based on the button API example:
class MyAwesomeButton(Button):
def __init__(self, **kwargs):
super(MyAwesomeButton, self).__init__(**kwargs)
self.my_data = {} # enter your data here
def callback(instance):
print('The button <%s> is being pressed' % instance.text)
print instance.my_data
btn1 = MyAwesomeButton(text='Hello world 1')
btn1.bind(on_press=callback)
btn2 = MyAwesomeButton(text='Hello world 2')
btn2.bind(on_press=callback)

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.

Categories