Dynamically created kivy buttons run on_press and on_release immediately

bosky picture bosky · Jan 25, 2014 · Viewed 10.5k times · Source

Why do my kivy buttons act pressed the moment they are created in python?

So, let me start by saying I know that there seems to be a answer to this question here:

on_press in Kivy keeps running at start up instead

...however, there is no working example. I have tried to copy the example there to understand the answer, but lack the experience to do fill in what's missing from the example.

So, this will probably be an easy answer for someone who looks at the other answer, can apply it here, and explain in plainer english for the noob.

Here is a small working example of the problem:

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 = ['some', 'words']

class HomeScreen(Screen):
    grid_l = ObjectProperty(None)
    top_lbl = ObjectProperty(None)

    def search_btn_pressed(self):
        grid = self.grid_l
        grid.bind(minimum_height=grid.setter('height'),
                     minimum_width=grid.setter('width'))

        for result in i:

                btn1 = Button(size_hint=(1, None))
                btn1.text = '%r' % result
                btn1.bind(on_release=self.btn1_pressed(result))

                btn2 = Button(size_hint=(1, None))
                btn2.text = 'Remove result buttons'
                btn2.bind(on_release=self.btn2_pressed)

                grid.add_widget(btn1)
                grid.add_widget(btn2)

    def btn1_pressed(self, result, *args):
        new_text = result
        self.top_lbl.text = new_text

    def btn2_pressed(self, *args):
        self.grid_l.clear_widgets()
        #pass

class buttons_pressedApp(App):

    def build(self):

        return HomeScreen()

if __name__ == '__main__':
    buttons_pressedApp().run()

And the kv file:

#:kivy 1.7.2

<HomeScreen>:
    scroll_view: scrollviewID
    top_lbl: lblID
    grid_l: gridlayoutID
    AnchorLayout:
        size_hint: 1, .1   
        pos_hint: {'x': 0, 'y': .9}
        anchor_x: 'center'
        anchor_y: 'center'
        Label:
            id: lblID
            text: 'Button Tester'
    Button:
        size_hint: 1, .1   
        pos_hint: {'x': 0, 'y': .8}
        text: 'Add theoretical search results'
        on_release: root.search_btn_pressed()
    ScrollView:
        id: scrollviewID
        orientation: 'vertical'
        pos_hint: {'x': 0, 'y': 0}
        size_hint: 1, .8
        bar_width: '8dp'
        GridLayout:
            id: gridlayoutID
            cols: 1
            size_hint: 1, None
            row_default_height: 40
            row_force_default: False

As you can see when you run it, the first button created in kivy works super. However, you will also notice that the top label changed immediately, indicating that the first dynamically created button already executed the on_release function.

You can't tell that the second dynamically created button, "Remove result buttons" was already executed because it removed all buttons when there were still none. However, it is evident the "Remove result buttons" button is being immediately executed when you press the "Add theoretical search results" button a second time. It should add two more buttons, but seems like nothing happens. This is because the "Remove result buttons" button is removing the previous two buttons, and then they are immediately being replaced.

Then, of course, neither of the buttons seem to do anything.

Should be easy for someone to fix the example given the similar question!

Thanks in advance.

EDIT:

I've altered btn2 to reflect inclement's answer. Works perfectly. However, as he pointed out, when I do the same to btn1, some default arguments are being passed into the method and it gives an error. So, I left btn1 as before with the parenthesis and 'result' inside them as the argument. Of course, this is run immediately and return nothing to be bound (as inclement explained). I want to be able to pass in 'result' as already defined in the example, but naturally not run it immediately. My apologies.. I should have written it this way the first time.

EDIT 2, the answer:

To reflect inclements last comment, I just wanted to posted the whole working example again with the answer included.

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
from functools import partial

i = ['some', 'words']

class HomeScreen(Screen):
    grid_l = ObjectProperty(None)
    top_lbl = ObjectProperty(None)

    def search_btn_pressed(self):
        grid = self.grid_l
        grid.bind(minimum_height=grid.setter('height'),
                     minimum_width=grid.setter('width'))

        for result in i:

                btn1 = Button(size_hint=(1, None))
                btn1.text = '%r' % result
                btn1.bind(on_release=partial(self.btn1_pressed, result))

                btn2 = Button(size_hint=(1, None))
                btn2.text = 'Remove result buttons'
                btn2.bind(on_release=self.btn2_pressed)

                grid.add_widget(btn1)
                grid.add_widget(btn2)

    def btn1_pressed(self, result, *args):
        new_text = result
        self.top_lbl.text = new_text

    def btn2_pressed(self, *args):
        self.grid_l.clear_widgets()
        #pass

class buttons_pressedApp(App):

    def build(self):

        return HomeScreen()

if __name__ == '__main__':
    buttons_pressedApp().run()

It works!

Answer

inclement picture inclement · Jan 25, 2014

btn2.bind(on_release=self.btn2_pressed())

You have the syntax to bind wrong (and this is also the problem in your other question).

Bind takes a function, but you aren't passing a function, you're calling the function. bind never sees that you happened to write btn2_pressed in its argument box, because python calls the function and passes only the result into bind.

So the solution is, you really want to write something like

btn2.bind(on_release=self.btn2_pressed)

Note the removed brackets - these are the syntax to call the function, but we specifically don't want to do that. Instead we pass the function itself.

bind also passes in some default arguments to the function, whereas your function is defined to only accept one. Since you don't care about the extra arguments here, you can just define your function with

def btn2_pressed(self, *args):

The *args catches the spare arguments. You can look up this syntax if you are not familiar with it.