tkinter button in class can't call function - python

Total noob, seriously and angrily struggling with Python...
What I'm trying to do SHOULD be simple:
Make a button.
Connect that button go a function.
Click button --> run function.
The problem comes when we have to use CLASS (which, no matter how much I read, study - or even pay to take classes continues to make zero sense to me)...
I've tried every concieveable combination of putting this little convert() function IN the class, of adding self.convert or root.convert - and NONE of it works. And, I am clueless why - or what to try next.
Here's the code:
from tkinter import *
from tkinter.ttk import Frame, Button, Style
def convert():
print("clicked")
kg = entry_kg.get()
print(kg)
class Example(Frame):
def __init__(self):
super().__init__()
self.initUI() # initiate the GUI
# -------------------------------
def initUI(self):
self.master.title("Weight Converter")
self.pack(fill=BOTH, expand=True)
# -------------------------------------
frame_kg = Frame(self) # frame for Kilograms
frame_kg.pack(fill=X)
lbl_kg = Label(frame_kg, text="Kilograms", width=16)
lbl_kg.pack(side=LEFT, padx=5, pady=5)
entry_kg = Entry(frame_kg)
entry_kg.pack(fill=X, padx=(5, 30), expand=True)
# ------------------------------------------------
frame_btn = Frame(self) # frame for buttons
frame_btn.pack(fill=BOTH, expand=True, padx=20, pady=5)
btn_convert=Button(frame_btn, text="Convert", command=convert)
btn_convert.pack(side=LEFT, padx=5, pady=5)
# -------------------------------------------
def main():
root = Tk()
root.geometry("300x200+300+200")
app = Example()
root.mainloop()
if __name__ == '__main__':
main()
What am I doing wrong?
How to do it right?
The seemingly arbitrary and useless over-complication of a simple task is seriously maddening...

If you want your functions to be designed outside of the class, but want to call it from a button defined in the class, the solution is to create a method in your class which takes the input values and then passes them to the external function.
This is called decoupling. Your function is decoupled from the implementation of the UI, and it means that you are free to completely change the implementation of the UI without changing the function, and vice versa. It also means that you can reuse the same function in many different programs without modification.
The overall structure of your code should look something like this:
# this is the external function, which could be in the same
# file or imported from some other module
def convert(kg):
pounds = kg * 2.2046
return pounds
class Example(...):
def initUI(self):
...
self.entry_kg = Entry(...)
btn_convert=Button(..., command=self.do_convert)
...
def do_convert(self):
kg = float(self.entry_kg.get())
result = convert(kg)
print("%d kg = %d lb" % (kg, result))

Here's a modified version of your code that works. The changes have been indicated with ALL CAPS line comments. I obviously misunderstood your question (which does say you could figure out how to make the convert() function part of the class. However, you mentioned you wanted the opposite of that, so I'm modified the code here accordingly.
Essentially the problem boils down to the convert() function needing to access a tkinter.Entry widget that's created somewhere else—inside your Example class in this case.
One way of doing that would be to save the widget in a global variable and access it through the variable name assigned to it. Many folks do that because it's easiest to thing to do, but as you should know, global variables are considered a bad practice and are generally something to be avoided.
There's a common way to avoid needing one with tkinter, which is sometimes called "The extra arguments trick". To use it all you need to do is create a short anonymous function with an argument that has a default value defined—which in this case will be the Entry widget you want passed to the now "wrapped" convert() function. This can be done using what's called a lambda expression. The revised code below and the comments in it show and describe how to do this:
from tkinter import *
from tkinter.ttk import Frame, Button, Style
def convert(entry_widget): # ADDED WIDGET ARGUMENT
""" Some function outside class. """
print("clicked")
kg = entry_widget.get() # REFERENCE ENTRY WIDGET PASSED AS ARGUMENT
print(kg)
class Example(Frame):
def __init__(self):
super().__init__()
self.initUI() # initiate the GUI
def initUI(self):
self.master.title("Weight Converter")
self.pack(fill=BOTH, expand=True)
frame_kg = Frame(self) # frame for Kilograms
frame_kg.pack(fill=X)
lbl_kg = Label(frame_kg, text="Kilograms", width=16)
lbl_kg.pack(side=LEFT, padx=5, pady=5)
entry_kg = Entry(frame_kg)
entry_kg.pack(fill=X, padx=(5, 30), expand=True)
frame_btn = Frame(self) # frame for buttons
frame_btn.pack(fill=BOTH, expand=True, padx=20, pady=5)
btn_convert=Button(frame_btn, text="Convert",
# DEFINE ANONYMOUS FUNCTION WITH DEFAULT ARGUMENT SO IT'S
# AUTOMATICALLY PASSED TO THE TARGET FUNCTION.
command=lambda entry_obj=entry_kg: convert(entry_obj))
btn_convert.pack(side=LEFT, padx=5, pady=5)
def main():
root = Tk()
root.geometry("300x200+300+200")
app = Example()
root.mainloop()
if __name__ == '__main__':
main()

Your entry_kg is not known anywhere outside the scope of initUI method. That is why. You could convert it from a method variable to instance attribute to be within reach for class methods by replacing:
entry_kg = Entry(frame_kg)
entry_kg.pack(fill=X, padx=(5, 30), expand=True)
with:
self.entry_kg = Entry(frame_kg)
self.entry_kg.pack(fill=X, padx=(5, 30), expand=True)
Only then:
You can mention it in a class method like:
...
kg = self.entry_kg.get()
That way you if you make your convert a method under Example again:
def initUI(self):
...
def convert(self): # is defined under the same scope as initUI
print("clicked")
kg = self.entry_kg.get() # notice that it now refers to self.e...
print(kg)
also don't forget to replace command option as well:
btn_convert=Button(..., command=self.convert)
Or only then:
When outside the class scope, by using dot notation on the object that the class creates:
def main():
root = Tk()
root.geometry("300x200+300+200")
app = Example()
kg = app.entry_kg.get() # This would return an empty string, but it would still be valid
root.mainloop()
to be used with global methods, such as the current state of convert you need to make app (the object it is an attribute of) global, or pass it explicitly to the method.

Related

Tkinter changing entry state based on radiobutton

I have four radio buttons. Underneath these four button is an Entry widget. I am trying to make this Entry widget only become available to type into when the last radio button is selected. The gui is in a class, as you can see in the code below:
class Gui:
def __init__(self):
pass
def draw(self):
global root
if not root:
root = tk.Tk()
root.geometry('280x350')
self.type = tk.StringVar()
self.type_label = tk.Label(text="Game Mode")
self.name_entry = tk.Entry()
self.name_entry.configure(state="disabled")
self.name_entry.update()
self.type_entry_one = tk.Radiobutton(text="Garage", value="garage", variable=self.type, command=self.disable_entry(self.name_entry))
self.type_entry_two = tk.Radiobutton(text="Festival", value="festival", variable=self.type, command=self.disable_entry(self.name_entry))
self.type_entry_three = tk.Radiobutton(text="Studio", value="studio", variable=self.type, command=self.disable_entry(self.name_entry))
self.type_entry_four = tk.Radiobutton(text="Rockslam", value="rockslam", variable=self.type, command=self.enable_entry(self.name_entry))
self.type_label.pack()
self.type_entry_one.pack()
self.type_entry_two.pack()
self.type_entry_three.pack()
self.type_entry_four.pack()
self.name_entry.pack()
root.mainloop()
def enable_entry(self, entry):
entry.configure(state="normal")
entry.update()
def disable_entry(self, entry):
entry.configure(state="disabled")
entry.update()
if __name__ == '__main__':
root = None
gui = Gui()
gui.draw()
However, the the self.name_entry is always available to type into. What am I doing wrong. If you still don't understand what is happening then please run this code yourself and you will see.
Thank you very much for your time and I look forward to responses.
You have the right idea about using the RadioButton to enable/disable the entry widget. Mostly it is your class design that is flawed - it is halfway between OO code, and procedural code...
I fixed the class structure and made it a subclass of tk.Tk so it completely encapsulate your GUI. The name_entry is now enabled only when type_entry_four radio button is selected, and disabled otherwise. I've set that last button to be selected at launch, but you can easily change that; it results in the entry being enabled at launch.
Superfluous variable passing through methods was removed, as was the draw method and the calls to it; all widget creation is now conveniently found in GUI.__init__
import tkinter as tk
class Gui(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('280x350')
self.select_type = tk.StringVar()
self.type_label = tk.Label(self, text='Game Mode')
self.name_entry = tk.Entry(self)
self.type_entry_one = tk.Radiobutton(self, text='Garage', value='garage', variable=self.select_type, command=self.disable_entry)
self.type_entry_two = tk.Radiobutton(self, text='Festival', value='festival', variable=self.select_type, command=self.disable_entry)
self.type_entry_three = tk.Radiobutton(self, text='Studio', value='studio', variable=self.select_type, command=self.disable_entry)
self.type_entry_four = tk.Radiobutton(self, text='Rockslam', value='rockslam', variable=self.select_type, command=self.enable_entry)
self.select_type.set('rockslam') # select the last radiobutton; also enables name_entry
self.type_label.pack()
self.type_entry_one.pack()
self.type_entry_two.pack()
self.type_entry_three.pack()
self.type_entry_four.pack()
self.name_entry.pack()
def enable_entry(self):
self.name_entry.configure(state='normal')
def disable_entry(self):
self.name_entry.configure(state='disabled')
if __name__ == '__main__':
Gui().mainloop()
The only problemS, I see, your facing here is because your not passing in the value "properly" into the function, when you use (..), your calling the function, so to get rid of that use lambda, like:
self.type_entry_one = tk.Radiobutton(text="Garage", value="garage", variable=self.type, command=lambda: self.disable_entry(self.name_entry))
self.type_entry_two = tk.Radiobutton(text="Festival", value="festival", variable=self.type, command=lambda:self.disable_entry(self.name_entry))
self.type_entry_three = tk.Radiobutton(text="Studio", value="studio", variable=self.type, command=lambda:self.disable_entry(self.name_entry))
self.type_entry_four = tk.Radiobutton(text="Rockslam", value="rockslam", variable=self.type, command=lambda:self.enable_entry(self.name_entry))
When using command=lambda:func(arg), this will get executed only when selecting a radiobutton. That is the point of using a radiobutton, right?
Also notice that when the initial code is run, the entire radiobuttons are selected, I think its probably because of tristate values, to get rid of that there are 2 ways I'm aware of:
Changing the declaration of self.type to:
self.type = tk.StringVar(value=' ')
Or, you could also go on adding an extra option to each radiobutton, tristatevalue=' ', like:
self.type_entry_one = tk.Radiobutton(text="Garage",..,tristatevalue=' ')
But make sure to do just one of the above solution. Take a read here about more on tristate values.
Also keep a note that your not passing in any master window to the widgets, its fine as long as your having just one window, when working with multiple windows, it may get confusing for where the widgets should appear.
Also side-note, if this is the complete code, then if nothing is being done on __init__(), its definition can be removed.

manipulate dynamically generated tk-inter widgets without global variables

This code of a minimal example is functional:
from tkinter import *
textbox =str()
def openpopup():
popupwindow = Toplevel(root)
global textbox
textbox = Text(popupwindow, height=20, width=40,font="Courier")
textbox.pack()
textbox.delete(1.0, END)
textbox.insert(1.0,"start")
Button(popupwindow, text="do it", command=changepopup).pack()
def changepopup():
global textbox
textbox.delete(1.0, END)
textbox.insert(1.0,"changed text")
root = Tk()
Button(root, text="open", command=openpopup).pack()
mainloop()
my goal is to open a popup dynamically on userinput and then have various gui elements interact.
I managed to do this using global. I've read using global variables should be avoided.
What is the recommended way of going about this? Can I avoid using globals? I am aware that this is an issue of scoping, this is how I came up with this "solution". I am not so familiar with OOP but I have a hunch this might be a solution here.
Question: Can I avoid using globals?
Yes, consider this OOP solution without any global.
Reference:
- 9.5. Inheritance
- class-and-instance-variables
- Dialog Windows
import tkinter as tk
from tkinter import tkSimpleDialog
class Popup(tkSimpleDialog.Dialog):
# def buttonbox(self):
# override if you don't want the standard buttons
def body(self, master):
self.text_content = ''
self.text = tk.Text(self)
self.text.pack()
return self.text # initial focus
def apply(self):
self.text_content = self.text.get(1.0, tk.END)
class App(tk.Tk):
def __init__(self):
super().__init__()
btn = tk.Button(self, text='Popup', command=self.on_popup)
btn.pack()
def on_popup(self):
# The widget `Popup(Dialog)`, waits to be destroyed.
popup = Popup(self, title='MyPopup')
print(popup.text_content)
if __name__ == '__main__':
App().mainloop()
The object-oriented way would be to create a class representing "popup" objects. The class' initializer method, __init__(), can create the popup's widgets as well as act as a storage area for the contents of the Text widget. This avoids needing a global variable because methods of class all has an first argument usually call self the is instance of the class.
Any data needed can be stored as attributes of self and can easily be "shared" all the methods of the class.
The other primary way to avoid global variables is by explicitly passing them as arguments to other callables — like main() does in the sample code below.
Here's an example based on the code in your question:
from tkinter import *
class Popup:
def __init__(self, parent):
popup_window = Toplevel(parent)
self.textbox = Text(popup_window, height=20, width=40, font="Courier")
self.textbox.pack()
self.textbox.insert(1.0, "start")
btn_frame = Frame(popup_window)
Button(btn_frame, text="Do it", command=self.do_it).pack(side=LEFT)
Button(btn_frame, text="Close", command=popup_window.destroy).pack(side=LEFT)
btn_frame.pack()
def do_it(self):
self.clear()
self.textbox.insert(1.0, "changed text")
def clear(self):
self.textbox.delete(1.0, END)
def main():
root = Tk()
Button(root, text="Open", command=lambda: Popup(root)).pack()
root.mainloop()
if __name__ == '__main__':
main()
The global you created, textbox is unnecessary. You can simply remove it from your program. and still get the same behavior
# textbox = str()
I hope my answer was helpful.

How can I assign an individual value to each button in the loop? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 3 years ago.
I am using a for loop and I would like to pass an individual argument to each separate button (in tkinter), this is for a calculator. However, I am unable to find a solution, to make my problem clear I will show the relevant code:
import tkinter as tk
entry_box = tk.Entry(master,width=35)
def insert_value(value):
entry_box.insert(0,value)
buttons = []
for i in range(0,10):
a = tk.Button(master,text=str(i),command=lambda:insert_value(str(i)))
buttons.append(a)
Note: I have removed the .grid() statements in the loop as it removes quite a large and unnecessary portion of code. And also I want each button to insert its displayed value so button 1 would insert 1, 2: 2 etc.
The problem occurs when passing "i" to insert value, as the value inserted into the entry box is always 9. I think this is because "i" will always end on 9 in the loop.
However, I can not find a solution to this. I have tried getting values from lists. I can't think of any other way to do this( in a loop that is).
I know I could just write the statements out 10 times but I am interested to know whether their is a solution.
TL;DR
The value passed to insert value is always 9.
Is there a way to insert the value that each button displays (1 to 9) into the entry box using a for loop?
Regardless of whether it's right all suggestions would be interesting and nice to see. Thankyou.
Since i is a free variable, it isn't evaluated until the callback is actually called, not when the function is first defined.
One hack is to pass the loop index as the default value of an otherwise unused function parameter. Note that you never need the integer i, only the string str(i), so we make the replacement before we call tk.Button.
buttons = []
for i in range(0,10):
i = str(i)
a = tk.Button(master,text=i, command=lambda i=i:insert_value(i))
buttons.append(a)
Another solution is to explicitly define a factory function to create the callback. make_callback is almost identical to the function created by your lambda expression, except it returns a closure around its argument rather than using default parameter values.
def make_callback(i):
return lambda: insert_value(i)
buttons = []
for i in range(0,10):
i = str(i)
a = tk.Button(master,text=i, command=make_callback(i))
buttons.append(a)
In fact, if you wanted, you could "inline" make_callback using nested lambda expressions, though I wouldn't recommend it for readability.
buttons = []
for i in range(0,10):
i = str(i)
a = tk.Button(master,text=i, command=(lambda x: lambda: insert_value(x))(i))
buttons.append(a)
below an oo solution.
#!/usr/bin/python3
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
class App(tk.Tk):
def __init__(self):
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.on_exit)
self.count = tk.IntVar()
self.set_title()
self.set_style()
self.init_ui()
def init_ui(self):
self.f = ttk.Frame()
self.label = ttk.Label(self.f, text="Hello, world!")
self.entry_box = ttk.Entry(self.f,textvariable=self.count)
self.entry_box.bind('<Return>', self.on_load_buttons)
self.label.grid(row=0,column=1, sticky=tk.W)
self.entry_box.grid(row=1, column=1, sticky=tk.W)
self.f.grid(row=0, column=0, sticky=tk.N+tk.W+tk.S+tk.E)
def on_load_buttons(self, evt=None):
if self.count.get():
for i in range(0,self.count.get()):
ttk.Button(self.f, text=str(i),
command=lambda:insert_value(str(i))).grid(row=i,
column=3,
sticky=tk.W+tk.E,
padx=5, pady=5)
def set_style(self):
self.style = ttk.Style()
self.style.theme_use("clam")
def set_title(self):
s = "{0}".format('Hello World')
self.title(s)
def on_exit(self):
"""Close all"""
if messagebox.askokcancel(self.title(), "Do you want to quit?", parent=self):
self.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()

tkinter variable in another class

Python 3.1, tkinter/ttk
I wrote something very simple to try and understand how tkinter's variables linked to widgets can be stored in a different class to the widget. Code below.
Questions:
1) why doesn't pressing the button change the label?
2) do I need quite so many selfs? Can the variables within each method manage without self. on the start?
Hopefully the answer will be a useful learning exercise for other tkinter newbies...
from tkinter import *
from tkinter.ttk import *
root = Tk()
class Store:
def __init__(self):
self.v = IntVar()
self.v.set(0)
def set(self, v):
self.v.set(v)
class Main:
def __init__(self):
self.counter = 0
self.label = Label(root, textvariable = a.v)
self.label.pack()
self.button = Button(root, command = self.counter, text = '+1')
self.button.pack()
def counter(self):
self.counter = self.counter + 1
a.set(self.counter)
a = Store()
b = Main()
root.mainloop()
Your problem is that you have both a method and a variable named counter. When you click the button, your function isn't being called so the variable isn't being set. At the time you create the button, tkinter thinks that self.counter is a variable rather than a command.
The solution to this particular problem is to rename either the function or the variable. It then should work as you expect.
To answer the question about "too many selfs": you need to use self so that python knows that the object you are referring to should be available everywhere within a specific instance of the object. So, yes, you need all those self's, if you want to refer to variables outside of the function they are defined in.
As for self in the definition of a method, those too are necessary. When you do object.method(), python will automatically send a reference to the object as the first argument to a method, so that the method knows specifically which method is being acted upon.
If you want to know more about the use of "self", there's a specific question related to self here: What is the purpose of self?
If I were you, I will do this:
import tkinter
root = tkinter.Tk()
var = tkinter.IntVar()
label = tkinter.Label(root, textvariable=var)
button = tkinter.Button(root, command=lambda: var.set(var.get() + 1), text='+1')
label.pack()
button.pack()
root.mainloop()
UPDATE:
But, if you insist doing this with two classes (where the first class is only a somewhat pointless interface), then I would do this:
import tkinter
class Store:
def __init__(self):
self.variable = tkinter.IntVar()
def add(self, value):
var = self.variable
var.set(var.get() + value)
return var.get()
class Main(tkinter.Tk):
def __init__(self, *args, **kwargs):
tkinter.Tk.__init__(self, *args, **kwargs)
var = Store()
self.label = tkinter.Label(self, textvariable=var.variable)
self.button = tkinter.Button(self, command=lambda: var.add(1), text='+1')
self.label.pack()
self.button.pack()
root = Main()
root.mainloop()
As you may notice, I used in both times the get() and set() methods of the IntVar (in your version I do this thru the Store interface we made), therefore you don't need to use a new variable (like self.counter) because the variable we instantiated is storing the data.
The other thing I used is a lambda expression instead of a full-blown function definition, since we only want to get the current value, add 1 to it, and store it as the new value.
And in my version, the Main class is a subclass of the original tkinter.Tk class -- that's why I call it's __init__ method inside the Main's __init__ method.

Python Calculator in Tkinter - Functions auto-running

I am making a graphical calculator in Python 2.7.3 with Tkinter. I have set it up so when the user pushes the 'b' button it will print 'b' to the console. I do this by making a function that is passed a variable called 'key' which it then adds to the label. However, when I first start the program, it automatically calls the function and prints 'b' to the console. Whenever I click the button, it does nothing. Here is my code:
from Tkinter import *
class Application(Frame):
def addkey(self,key):
print str(key)
def removekey(self):
if len(self.displaytext) > 0:
self.displaytext = self.displaytext[0:-1]
def createWidgets(self):
self.maxlength = 20
self.displaytext = ""
self.frame1 = Frame(self)
self.display = Label(self.frame1,textvariable=self.displaytext,width=self.maxlength+3,bg="black",fg="white",height=2)
self.frame1.pack()
self.display.pack()
self.frame2 = Frame(self)
self.bksp = Button(self.frame2,text="b",width=4,height=2,command=self.addkey("b"))
self.frame2.pack()
self.bksp.pack()
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()
app = Application()
app.mainloop()
try:
root.destroy()
except:
pass
command = ... expects to be passed a function (which accepts 0 arguments). As it is, you are passing the result of a function (which is None in this case).
One easy way to do this is to use an anonymous function to wrap around your function and call it appropriately:
command=lambda: self.addkey("b")
Or you could do it more verbosely:
def button_func():
return self.addkey("b")
self.bksp = Button(self.frame2,text="b",width=4,height=2,command=button_func)
But this starts to get pretty verbose if you have 15 buttons each calling the same underlying function with slightly different arguments.
You are calling self.addkey and assigning its result to the command argument. Instead, you need to pass a function that can be called.
In other words, change
command=self.addkey("b")
to
command=lambda: self.addkey("b")
If self.addkey didn't need any additional arguments, you could just do command=self.addkey, but since that is not the case you need the lambda.

Categories