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.
Related
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.
This question already has an answer here:
How to save askdirectory result in a variable I can use using tkinter with OOP?
(1 answer)
Closed 4 years ago.
first of all im pretty new to OOP coding, so sorry for asking stupid questions.
I have a problem returning a value from Class AskDir that gets its value from Class SelectDir to Class MainWindow if that makes sense
Currently the code i have works, in every other way, except I cannot save the actual "self" (which is the path i.e. "/home/etc/") return value from Class AskDir() to another variable no matter what i do.
From what i have read i should be able to get the return value with a = AskDir(self)
print(a), but what I get instead is ".!frame.!functionchangename.!askdir"
So in short, how to save actual return path from a function inside Class AskDir, to be saved to variable a in Class MainWindow()?
To clarify, what i want is for Class MainWindow() to have a variable (a) that gets the return value from a subfunction get() inside Class SelectDir(), and that value should be the path that get() function returns
simplified code:
class MainWindow:
self.controller = controller
# prints the button to this frame!
getdir = AskDir(self)
print(getdir) # this should return actual path (/home/etc/), but it doesnt
class AskDir(tk.Frame):
def __init__(self, container):
tk.Frame.__init__(self, container)
self.selectdir = SelectDir(self, "Select directory",
"/home/user/folder/")
button = tk.Button(frame, text="Select directory",
command=self.select_dir)
self.act_dir = tk.StringVar()
self.act_dir.set("/home/")
def select_dir(self):
self.selectdir.show()
self.act_dir.set(self.selectdir.get())
class SelectDir:
def __init__(self, container, title, initial):
self.master = container
def show(self):
result = filedialog.askdirectory()
if result:
self.selected = result
# THIS RETURNS THE ACTUAL PATH!
def get(self):
return self.selected
Does this work for you?
I can see no need to have SelectDir as a class given that its purpose is just to call the askdirectory method and return the selected folder path so I've change it to a function.
With this example, pressing the "Select directory" button will open the directory dialog and then place the selected directory text in to an Entry widget.
If the MainWindow class wishes to do anything with the path once it has been set then you just need to call the get_dir method of AskDir e.g. print(askDir.get_dir()
import tkinter as tk
from tkinter import filedialog
def doStuffFunction(value):
print(value)
class MainWindow(tk.Frame):
def __init__(self,master=None,**kw):
tk.Frame.__init__(self,master=master,**kw)
self.askDir = AskDir(self)
self.askDir.grid()
doStuffButton = tk.Button(self, text="Do Stuff", command=self.doStuff)
doStuffButton.grid()
def doStuff(self):
doStuffFunction(self.askDir.get_dir())
class AskDir(tk.Frame):
def __init__(self, master, **kw):
tk.Frame.__init__(self, master=master,**kw)
button = tk.Button(self, text="Select directory",
command=self.select_dir)
button.grid(row=0,column=1)
self.act_dir = tk.StringVar()
self.act_dir.set("/home/")
self.pathBox = tk.Entry(self, textvariable=self.act_dir,width=50)
self.pathBox.grid(row=0,column=0)
def select_dir(self):
selectdir = SelectDir("Select directory", "/home/user/folder/")
self.act_dir.set(selectdir)
def get_dir(self):
return self.act_dir.get()
def SelectDir(title,initial):
result = filedialog.askdirectory(initialdir=initial,title=title)
return result
if __name__ == '__main__':
root = tk.Tk()
MainWindow(root).grid()
root.mainloop()
You can now use the AskDir class as many times as you want in the MainWindow (perhaps to select Source and Destination paths). This has been added to my example with the doStuff method inside MainWindow.
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.
How can I call a function change_label whenever global variable a changes its value? With change_variable I am trying to simulate actual changing of the variable (the variable changes on button click).
from tkinter import *
a = 3
class Application(Frame):
def __init__(self, master):
Frame.__init__(self, master)
self.master = master
self.button = Button(self.master, text='Change Variable', command=self.change_variable)
self.button.grid(row=0)
self.label = Label(self.master, text='Test')
self.label.grid(row=1)
def change_label(self):
self.label.config(bg='Red', fg='Yellow')
def change_variable(self):
global a
a = 1
def main():
root = Tk()
Application(root)
root.mainloop()
if __name__ == '__main__':
main()
If you use one of tkinters special variables (StringVar, etc) you can add a "trace" that will trigger a callback whenever the variable is set or unset.
For example:
class Application(Frame):
def __init__(self, master):
Frame.__init__(self, master)
...
self.a = tk.IntVar(value=3)
self.a.trace("w", self.change_label)
...
def change_label(self, *args):
self.label.config(bg='Red', fg='Yellow')
def change_variable(self):
self.a.set(1)
With that, whenever you set the value of self.a via the set method, the function bound with the trace will be called.
Any widget that uses that variable also will be updated. For example, change your label to this:
self.label = tk.Label(self.master, textvariable=self.a)
When you click the button, notice that the label changes to reflect the change.
For a description of what arguments are passed to the trace function, see What are the arguments to Tkinter variable trace method callbacks?
These variables have a good description here: The Variable Classes (BooleanVar, DoubleVar, IntVar, StringVar)
If you are using Tk, this might be worth looking into: Tk's "Variable" classes
If that doesn't work for you (because you want to store your own type, or something like that), Shiva's comment is the way to go, and if you want to store multiple variables, this might be a good idea:
class Storage(dict):
def __getattribute__(self, name):
return self[name]
def __setattr__(self, name, value):
print(name, value) # react to the change of name
self[name] = value
storage = Storage({a: 3, b: 2})
storage.a = 4
print(storage.a)
If you don't want to be able to set the variable without triggering some code you put there, good luck. You can override __setitem__ too, but you can always call dict.__setitem__ with the Storage variable as the first argument.
Try running this code it will give you the idea of what you want.
import tkinter
count = 5
def change_text():
global count
if count != 2:
button.config(text='edit')
frame = tkinter.Frame(height=500,width=500)
button = tkinter.Button(frame,text='save',command=change_text)
button.place(x=0,y=0)
frame.pack()
frame.mainloop()
The code was supposed be like this.
from tkinter import *
a = 3
class Application(Frame):
def __init__(self, master):
Frame.__init__(self, master)
self.master = master
self.button = Button(self.master, text='Change Variable', command=self.change_label)
self.button.grid(row=0)
self.label = Label(self.master, text='Test')
self.label.grid(row=1)
def change_label(self):
global a
a = 1
if a != 3:
self.label.config(bg='Red', fg='Yellow')
def main():
root = Tk()
Application(root)
root.mainloop()
hope that is what you wanted.
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.