Using buttons with sender
-
Hi, I’m very new to Pythonista, new-ish to Python in general. I’m having trouble with getting my buttons working. I’m trying to have a button that prints a statement from the body of code. I keep getting a sender error that says I need to include sender, but when I add sender into the code for the button action it says ‘sender’ is not defined. I dont even know if I have it in the right place! So my code goes something like this (I took some parts from the game menu example included):
(Dice is a class I defined to give me a random number like rolling a die)
def touch_began(self, touch): touch_loc = self.menu_bg.point_from_scene(touch.location) for btn in self.buttons: if touch_loc in btn.frame: sound.play_effect(etc...) btn.texture = Texture(etc...) if btn.title == ‘Roll the Dice’: print(Dice.roll_the_dice(6, 1, sender)
I’m sure there’s multiple things I’m missing here but help would be appreciated!
Does anyone have any suggestions?
-
@timjhinton please post your code here, using </> button for right formatting it
-
@timjhinton example of usage
import ui v = ui.View() v.background_color = 'white' def b_action(sender): print(sender.title) b = ui.Button() b.frame = (10,10,100,20) b.title = 'tap me' b.action = b_action v.add_subview(b) v.present('fullscreen')
-
Just make sure your button action function accept an argument
sender
-- you are not required to use it, but the function will be called with a sender argument so must be declared with itdef action(sender): # your code here
If your button actions are methods in a class, you need to use self and sender:
class myclass(object): def action(self, sender): # your code here
-
@JonB said:
class myclass(object): def action(self, sender): # your code here
So it should look like this?
Class Dice(object): def roll_the_dice(self, sender): Roll_total = 0 Roll_list = [] For i in range(self.number): Num = randint(1, self.size) Roll.list.append(num) For i in roll_list: Roll_total += i Return roll_total
And what if the function takes other parameters? Like...
Class Dice(object): def roll_the_dice(self, size, number, sender): Roll_total = 0 Roll_list = [] For i in range(self.number): Num = randint(1, self.size) Roll.list.append(num) For i in roll_list: Roll_total += i Return roll_total
But I thought the documentation says a function like that can only have 1 parameter, 2 if it is a method in a class?
Thanks for the response, I appreciate the help.
-
No, in a method, the second argument (first non-self argument) must be sender.
Now, you could do
def roll_the_dice(self, sender, size=6, number=1): #sender is first non self param, and other params have default kwargs listed
-- basically provide default arguments so that when the ui system calls fcn(sender), it will still work
Or, you can set your action using a lambda or partial. For instance, in your example, you could set your button action as
button.action=lambda sender:dice.roll_the_dice(5, 10, sender)
Basically, what the ui system will do is try to execute:
button.action(button)
As long as you can call the action this way, you are all set.
As an aside, since you can set your own attribs on ui.Button, a common thing to do is set all the other parameters as attribs in your button:
button.dice_number=3 button.dice_size=6 # then your action would look like.. def roll_the_dice(self, sender): number=sender.dice_number size=sender.dice_size # rest of your original code
You could also have simple wrappers that call existing classes based on attribs in sender, and strip out the sender argument if the existing method doesn't need it.
def roll_action(sender): dice.roll_the_dice(sender.dice_number, sender.dice_size)
-
@JonB said:
No, in a method, the second argument (first non-self argument) must be sender.
Now, you could do
def roll_the_dice(self, sender, size=6, number=1): #sender is first non self param, and other params have default kwargs listed
-- basically provide default arguments so that when the ui system calls fcn(sender), it will still work
Or, you can set your action using a lambda or partial. For instance, in your example, you could set your button action as
button.action=lambda sender:dice.roll_the_dice(5, 10, sender)
Basically, what the ui system will do is try to execute:
button.action(button)
As long as you can call the action this way, you are all set.
As an aside, since you can set your own attribs on ui.Button, a common thing to do is set all the other parameters as attribs in your button:
button.dice_number=3 button.dice_size=6 # then your action would look like.. def roll_the_dice(self, sender): number=sender.dice_number size=sender.dice_size # rest of your original code
You could also have simple wrappers that call existing classes based on attribs in sender, and strip out the sender argument if the existing method doesn't need it.
def roll_action(sender): dice.roll_the_dice(sender.dice_number, sender.dice_size)
Okay maybe I am missing something. I went back to TKinter in Python on my laptop and was able to get the buttons working within minutes with this very code. For instance:
normal_roll = Button(root, padx=20, pady=20, command=lambda: roll_the_dice)
Then place the button in root with grid or pack, of course. But, this worked just fine. It prints the info to the console that it is supposed to and it is easy.
Hopefully there is an easy correlation from TKinter to the ui module. I’m thinking its just me not understanding the reasoning and placement of sender, but I could be wrong about that too. Any thoughts or advice you have would be appreciated, @JonB, or anyone else. Thanks again.
-
Tkinter is different than pythonista ui.
In pythonista ui, the ui system calls your action with the object that triggered the action. The reason for that is so you can have generic actions that are shared by different buttons.
You can use a lambda as an action. So, if you aren't using sender, just create an argument using a lambda, that you don't pass in.
Let's try it a slightly different way. Button.action must be a
callable
that accepts a single argument,sender
, which will be the button itself.That's it.
mybutton.action(mybutton)
Is how your action will be called. If you cannot type the above at the console without an error, the system won't be able to call it either.
So, you can use a lambda, partial, an class method, instance method. You could use a custom class that implements a
__call__
method. Whatever you want -- it just has to accept one single argument when called in the above manner. That argument will be the object that triggered the action.
-
Btw, in your example:
B=ui.Button(title='roll') B.action= lambda sender:roll_the_dice()
Is probably what you want.
-
@JonB said:
Btw, in your example:
B=ui.Button(title='roll') B.action= lambda sender:roll_the_dice()
Is probably what you want.
Okay, I have taken what i think you were saying and applied it to my code:
def norm_attack(self, sender): self.Timboslice.normal_attack() self.normal = ButtonNode('Normal >attack') self.normal.action = lambda >sender:norm_attack() self.buttons.append(self.normal)
Now I can run my program without any errors, the buttons click and have the right titles, but nothing happens when i click them. The functions that I am calling with these buttons (self.Timboslice.normal_attack()) have a prints statement at the end of them and they return some information, though I haven’t used that returned info yet. But I was expecting it to print to the console. Currently I have the buttons defined, as above, in setup. I’m wondering if I need to move them to update or touch_began sections?
Again, I really appreciate your help, I’m really trying to get a better understanding and since I dont have a ton of experience sometimes what is clear to you doesn’t have the same context to me.
-
In your case, since you are presumably defining the button inside of init? It should be
def norm_attack(self, sender): self.Timboslice.normal_attack() # below is, I assume, inside of __init__??? self.normal = ButtonNode('Normal attack') self.normal.action = self.norm_attack self.buttons.append(self.normal)
In that case, since your method accepted sender, action can just point to the method. But of course it must be self.norm_attack, since norm_attack is an instance method.
-
@timjhinton said:
The functions that I am calling with these buttons (self.Timboslice.normal_attack()) have a prints statement at the end of them and they return some information, though I haven’t used that returned info yet.
Not to sound condescending, but are your print statements before your returns, or accessible before returns?
-
@mcriley821 said:
@timjhinton said:
The functions that I am calling with these buttons (self.Timboslice.normal_attack()) have a prints statement at the end of them and they return some information, though I haven’t used that returned info yet.
Not to sound condescending, but are your print statements before your returns, or accessible before returns?
Yes, if the function prints a statement then the print statement is before the return. The line above the return, actually.
-
@JonB said:
In your case, since you are presumably defining the button inside of init? It should be
def norm_attack(self, sender): self.Timboslice.normal_attack() # below is, I assume, inside of __init__??? self.normal = ButtonNode('Normal attack') self.normal.action = self.norm_attack self.buttons.append(self.normal)
In that case, since your method accepted sender, action can just point to the method. But of course it must be self.norm_attack, since norm_attack is an instance method.
This, I think, is one of my main problems. I had the above code under def setup in the Menuscene class. I moved the 3 lines after your comment into the Menuscene class init and now it is giving me an error that ‘Menuscene’ object has no attribute ‘norm_attack’. I am probably still not putting it in the correct spot. I have a Player class that has the methods such as self.norm_attack, I have the ButtonNode class and I have the Menuscene class. In the Menuscene class i have the methods setup (which the def norm_attack() is), did_change_size, update, touch_began and touch_ended.I have tried several different locations for these buttons with no success. I am sorry, but I must have really missed where it specifies this in the documentation. Thanks for your help so far.
-
@timjhinton
Okay great! Some people come from different languages, and beginners sometimes confuse return with print.If you’re still having issues with it not working the way it should (not printing like you think it should), it would be better to post all the code and/or the smallest example code that reproduces the problem you’re having.
I usually find the real issue when I’m writing a smaller example!
-
@mcriley821 if you look above, i have a function there that has the code. What I’m really struggling with is getting a button to run it. I’m not sure if I am just putting it in the wrong place or what. I took the code that was in the game_menu.py example included with the app and used it as the skeleton to build my code. It works great and pulls up a very good looking menu, I was able to rename the buttons to what I wanted, they click and change to yellow when they are clicked but I haven’t yet been able to attach a function call to it yet. Here is a good portion of it:
#class from game menu example class ButtonNode (SpriteNode): def __init__(self, title, *args, **kwargs): SpriteNode.__init__(self, 'pzl:Button1', *args, **kwargs) button_font = ('Avenir Next', 20) self.title_label = LabelNode(title, font=button_font, color='black', position=(0, 1), parent=self) self.title = title #class from game menu example class MenuScene (Scene): def __init__(self, title, subtitle, button_titles): Scene.__init__(self) self.title = title self.subtitle = subtitle self.button_titles = button_titles def setup(self): button_font = ('Avenir Next', 20) title_font = ('Avenir Next', 36) num_buttons = len(self.button_titles) self.bg = SpriteNode(color='black', parent=self) bg_shape = ui.Path.rounded_rect(0, 0, 340, num_buttons * 64 + 140, 8) bg_shape.line_width = 4 shadow = ((0, 0, 0, 0.35), 0, 0, 24) self.menu_bg = ShapeNode(bg_shape, (1,1,1,0.9), '#15a4ff', shadow=shadow, parent=self) self.title_label = LabelNode(self.title, font=title_font, color='black', position=(0, self.menu_bg.size.h/2 - 40), parent=self.menu_bg) self.title_label.anchor_point = (0.5, 1) self.subtitle_label = LabelNode(self.subtitle, font=button_font, position=(0, self.menu_bg.size.h/2 - 100), color='black', parent=self.menu_bg) self.subtitle_label.anchor_point = (0.5, 1) self.buttons = [] for i, title in enumerate(reversed(self.button_titles)): btn = ButtonNode(title, parent=self.menu_bg) btn.position = 0, i * 64 - (num_buttons-1) * 32 - 50 self.buttons.append(btn) self.did_change_size() self.menu_bg.scale = 0 self.bg.alpha = 0 self.bg.run_action(A.fade_to(0.4)) self.menu_bg.run_action(A.scale_to(1, 0.3, TIMING_EASE_OUT_2)) self.background_color = 'white' def norm_attack(self, sender): self.Timboslice.normal_attack()
This is the main portion of the ui portion that I am trying to learn. I got the rest of my code, that includes the classes and functions in the last 2 lines, to work in a normal python environment so I know that those should not be the issue. It’s learning how to incorporate the ui module to get the buttons to run the methods.
-
@timjhinton
Ahhh. So I believe the issue you’re having is that a SpriteNode is not a button. In the example, the class ButtonNode is created just to make the “menu button” (actually just a sprite) change appearance when you tap it.Everything in the scene module can only respond to taps, and you have to implement the touch_began method to do hit-testing on your sprites. After determining which sprite you’re touching, you could do something like:
btn_node.run_action(Action.call(lambda: print('hello'))) # or btn_node.run_action(Action.call(some_func))
If you want to actually combine the UI module with the scene module, you would need to make a .pyui file and set it’s custom view attribute to your main scene class. Like here: Combine Scene and UI
Also, if you haven’t already, check out the docs. They’re super informative, and most things have examples. There’s also a snippet in the scene docs that explains how to integrate scene and UI if the above link didn’t help.
UI
SceneApart from all this, if you’re making a game, I would recommend not trying to integrate the UI module, and do hit-testing with the sprites in the scene module. If you’re not doing a game, just go full UI. Integrating will complicate things while you’re still learning IMO.
-
@mcriley821 said:
@timjhinton
Ahhh. So I believe the issue you’re having is that a SpriteNode is not a button. In the example, the class ButtonNode is created just to make the “menu button” (actually just a sprite) change appearance when you tap it.Everything in the scene module can only respond to taps, and you have to implement the touch_began method to do hit-testing on your sprites. After determining which sprite you’re touching, you could do something like:
btn_node.run_action(Action.call(lambda: print('hello'))) # or btn_node.run_action(Action.call(some_func))
If you want to actually combine the UI module with the scene module, you would need to make a .pyui file and set it’s custom view attribute to your main scene class. Like here: Combine Scene and UI
Also, if you haven’t already, check out the docs. They’re super informative, and most things have examples. There’s also a snippet in the scene docs that explains how to integrate scene and UI if the above link didn’t help.
UI
SceneApart from all this, if you’re making a game, I would recommend not trying to integrate the UI module, and do hit-testing with the sprites in the scene module. If you’re not doing a game, just go full UI. Integrating will complicate things while you’re still learning IMO.
I think you were exactly right. I was trying to use ui with scene and it wasn’t working how I wanted. So I switched to using ui with the visual editor, using .pyui. I believe I have everything working! Thank you for your help and also @JonB for helping me sort this out.
Unfortunately, I ve been looking through the documentation throughout the day and I have not yet been able to find how to have a button change the text for a label/text field/text view. I tried all of them to see if any would do what I wanted, but no luck. Do you have a suggestion for how to get this to work? Say I have a label/text field that is displaying the background text or maybe just shows a 0 to start. I’d like to be able to have my buttons change what that that label displays. For example, so far I’ve tried things like:label1 = ui.Label(title=‘0’)
Def change_label(sender):
Label1.text=‘New value’
Change = ui.Button(title=‘Change label’)
Change.action = lambda sender: change_label()Or, with a text view:
Original_text = ui.TextView(Title=‘0’)
Def change_text(sender):
Original_text.replace_range(0, ‘1’)If there is an easy way to do it with the .pyui, that would be useful as well!
I hope that’s clear. I’m just hoping to have a text that will update with a new value/text when I push the button (that now works)
Thanks!
-
I'm wondering if you have seen the tutorials at
https://github.com/humberry/ui-tutorialYou might be interested in UsingSubviews for example, which uses both a custom class and pyui and instance method actions.
There were some other tutorials years ago with various aspect, but I'm not finding them on the pythonista-tools GitHub, or forum search, but maybe I'm mixing up something else.
-
v=ui.View(frame=[0,0,400,400]) label1 = ui.Label(title=‘0’, name='label1') v.add_subview(label1) def change_label(sender): #increment label. Note this needs the global label1 to be available label1.text=str(int(label1.text)+1) #alternatively, this will be more robust, for instance if you run in a panel, and later your globals gets cleared label1=sender.parent['label1'] label1.text=str(int(label1.text)+1) change = ui.Button(title=‘Change label’, frame=[0,50,100,100]) change.action = change_label # not necessary here, but the following would also work. Note lambda must CALL the function! change.action = lambda sender: change_label(sender) v.add_subview(change) v.present()
-
Sorry, rereading your last message, are you saying you have working buttons and just want to them to work with pyui?
To get things to work with pyui, you have to predefined the actions before you use load_ui. The action names you use must already be defined -- no lambdas here. There are some techniques for working with custom classes, but an easy way is to implement a on_load method -- see some of humberry's examples. This also let's you create "convienence" attributes so you can refer to ui objects by variable/attribute name, instead of finding them via sender.parent[Component name].