I am trying to create a light weight cross platform Message Box that contains a list of items. Ideally it has an API that allows you to pass in a message to to display, a title, and tuple of choices. When pressing OK it would return the currently selected choice. It would also be preferred that the required modules be part of the standard python distributions.
Easygui has what I am looking for called a choicebox found at http://easygui.sourceforge.net/download/version0.95/tutorial/index.html#contents_item_10.1. However the window it pops up is monstrous and it always sorts your list of choices alphabetically. Because of these 'features', easygui is not ideal.
I have also looked into bwidgets, pmw, and Tix. While trying these I have come across a few issues including: difficultly finding working examples and failures across different platforms.
My working model is using Tkinter's OptionMenu and pickle to return the data (see code samples below). While this works, it is rather annoying having to save the choice to the file system to avoid using global variables. Is there a way to return the selection upon destruction of the gui?
Any help / advice would be greatly appreciated. Note that these examples are only for reference, they may or may not run properly on your system.
State Management Module
import pickle
def store(pkl_path, data_to_store):
try:
fid = open(pkl_path, 'w')
pickle.dump(data_to_store, fid)
except:
print 'Unable to store data in ' + pkl_path
else:
fid.close()
def load(pkl_path):
try:
fid = open(pkl_path, 'r')
loaded_state = pickle.load(fid)
fid.close()
except:
loaded_state = None
else:
fid.close()
return loaded_state
Menu Module
from Tkinter import *
def Prompt_Dropdown_Ok_Cancel(title, options, pickle_file, default_selection=0):
master = Tk()
master.title(title)
var = StringVar(master)
var.set(options[default_selection]) # default value
w = OptionMenu(master, var, *options)
w.pack()
def ok():
state.store(pickle_file, var.get())
master.quit()
def cancel():
state.store(pickle_file, None)
master.quit()
button = Button(master, text="OK", command=ok)
button.pack()
b2 = Button(master, text="Cancel", command=cancel)
b2.pack()
mainloop()
Example Usage
from menu_module import *
def display_com_selection():
pkl_path = '.tmp/comm_selection'
title = 'COM Port Selection'
Prompt_Dropdown_Ok_Cancel(title,get_available_com(),pkl_path)
selection = state.load(pkl_path)
return selection
EDIT
Disregarding my concern about global variables, I tried an implementation using them to see if it was any easier. It makes things substantially easier, however my question still stands for a better way to do this.
Below is the reworked Menu Module
from Tkinter import *
Prompt_Dropdown_Ok_Cancel_Selection = None
def Prompt_Dropdown_Ok_Cancel(title, message, options, default_selection=0):
master = Tk()
master.title(title)
var = StringVar(master)
var.set(options[default_selection]) # default value
l = Label(master, text=message)
l.pack()
w = OptionMenu(master, var, *options)
w.pack(fill=BOTH, expand=1)
def ok():
global Prompt_Dropdown_Ok_Cancel_Selection
Prompt_Dropdown_Ok_Cancel_Selection = str(var.get())
master.destroy()
def cancel():
global Prompt_Dropdown_Ok_Cancel_Selection
Prompt_Dropdown_Ok_Cancel_Selection = str(var.get())
master.destroy()
button = Button(master, text="OK", command=ok)
button.pack(side=LEFT)
b2 = Button(master, text="Cancel", command=cancel)
b2.pack(side=LEFT)
mainloop()
return Prompt_Dropdown_Ok_Cancel_Selection
The normal way dialogs work is something like this:
mydialog = SomeDialogClass(...)
result = mydialog.Show()
if result == "OK":
print "you clicked OK; dialog value is", mydialog.GetValue()
else:
print "you clicked cancel"
mydialog.Destroy()
This is pseudocode, intended to be GUI toolkit agnostic (though admittedly it looks a lot like wxPython). The main idea is, you create the dialog as an object, ask the object to show itself, wait until the user is done (by virtue of clicking "OK" or "Cancel"), then asking the object for its data and then finally destroying the object (or, keep it around for re-use).
A second way to do this is to write your code such that you give the dialog a function to call in order to set the value. Something like this:
mydialog = SomeDialogClass(..., callback=self.foo)
....
def foo(self, button, result):
if button == "OK":
print "you clicked OK; result is", result
elif button == "Cancel":
print "you clicked Cancel"
This second method works well if your dialog is not modal (ie: your program continues to run while the dialog is present).
Related
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.
I want to have a login window pop up at first, then once the conditions are satisfied, it closes the login window and opens a new window.
from Tkinter import *
import tkMessageBox
#Not really sure what i'm doing here anymore
while True:
Login = Tk()
Login.title('Login')
Login.geometry('400x130')
NameLabel = Label(Login, text='Username')
NameLabel.place(bordermode=OUTSIDE, height=25, width=100, x=100)
NameEntryRaw = Entry(Login)
NameEntryRaw.place(bordermode=OUTSIDE, height=25, width=100, x=200)
CodeLabel = Label(Login, text='Code')
CodeLabel.place(bordermode=OUTSIDE, height=25, width=100, x=100, y=30)
CodeEntryRaw = Entry(Login)
CodeEntryRaw.place(bordermode=OUTSIDE, height=25, width=100, x=200, y=30)
def tbd():
tkMessageBox.showinfo('Congrats!', 'This program is not done yet')
def login():
Usernames=list()
Usernames.append('Mordecai')
Usernames.append('Ezekiel')
Usernames.append('Goliath')
Usernames.append('Abraham')
Usernames.append('Bartholomew')
Usernames.append('Jedediah')
Usernames.append('Solomon')
Usernames.append('Tobias')
Usernames.append('Yohanan')
Usernames.append('Lucifer')
NameEntry=NameEntryRaw.get()
CodeEntry=CodeEntryRaw.get()
CodeReal='116987'
if Usernames.count(NameEntry) == 0:
tkMessageBox.showinfo('Error!', 'Your Username is invalid! Try Again.')
else:
()
if CodeEntry != CodeReal:
tkMessageBox.showinfo('Error!', 'The Code entered is invalid! Try Again.')
else:
()
LoginButton = Button(Login, text="Login", command=login)
LoginButton.place(bordermode=OUTSIDE, height=50, width=200, x=100, y=60)
Login.mainloop()
else:
DataBase = Tk()
#this will be the data base
DataBase.mainloop()
You don't want to use two mainloop's. Generally speaking, your tkinter app should have a single .mainloop called.
As for how to get the login popup and then switch to the window... You can create your root window with the app and when you start your program have a Toplevel window be shown with the login stuff, maybe hide / withdraw the root additionally?, have the toplevel have a submit button or something that would validate the credentials. If the credentials are valid, then you can use the destroy() method and remove the toplevel widget / go to the root window for your main app.
Having a while True repeat the process of creating a GUI + mainloop is bad for obvious reasons.
Edit: static is probably a bad term for a mutable object, since it's mutable... Added tuple vs list for global.
You don't need to create a blank list and then use append to add your usernames to the usernames list. You can make this global. Since, username is a mutable object (it's a list) you could still perform operations elsewhere in your code on this global, say .append etc. Or, if you never have these change i'd make it a tuple, since they're immutable and this "fits" better with the intent.
Instead of using .count(*) to get the occurrences of an element in a list you can just use:
if (object) in (some list) #Object is an abstraction, not the type
If CodeReal is static, which it looks to be, you can also make this global.
Here's a quick edit to your code, you can do this without classes, but I used classes here to try to distinctly show the logical separation in the program.
I changed a few variable names as well, so that it was easier to read / understand. I also used .pack() and .grid() as this was quicker to code than having to use .place() everywhere this is an arbitrary choice.
import Tkinter as tk
import tkMessageBox as messagebox
import sys
#No need to do usernames = list() and then .append for each one.
#Just make a global list holding them all...
USERNAMES = ('Mordecai', 'Ezekiel', 'Goliath', 'Abraham', 'Bartholomew',
'Jedediah', 'Solomon', 'Tobias', 'Yohanan', 'Lucifer')
PASSWORD = '116987' #Was CodeReal, this can be global
#We overrode the closing protocol here.
def closing_protocol():
if messagebox.askokcancel("Quit", "Do you want to quit?"):
sys.exit()
#A container for our Login "app".
class Login(tk.Toplevel):
def __init__(self, *args, **kwargs):
#We init the toplevel widget.
tk.Toplevel.__init__(self, *args, **kwargs)
#We set the closing protocol to be the overridden version / func.
self.wm_protocol("WM_DELETE_WINDOW", closing_protocol)
tk.Label(self, text='Username').grid(row=0, column=0)
self.username = tk.Entry(self)
self.username.grid(row=0, column=1)
tk.Label(self, text='Password').grid(row=1, column=0)
#Show = '*' just hides the password instead of plain-text like
#you typically see
self.password = tk.Entry(self, text='Password', show='*')
self.password.grid(row=1, column=1)
#When the button is clicked the _callback function will be called
tk.Button(self, text='Login', command=self._callback).\
grid(row=2, column=0, columnspan=2, sticky="nsew")
def _callback(self):
#If the username or password is bad, raise an error message.
if (self.username.get() not in USERNAMES or
self.password.get() != PASSWORD):
messagebox.showerror("Validation Error!",
"You have entered invalid credentials.\n" +
"Please try again.")
#otherwise, we're good to go. Destroy login / show main app.
else:
root.deiconify()
app.pack()
self.destroy()
class Main(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
tk.Label(self, text="MAIN APP STUFF").pack()
if __name__ == '__main__':
root = tk.Tk()
root.withdraw() #Hides the root window initially.
app = Main(root)
Login()
root.mainloop()
I want to thank you for clarifying what you did for me. It proved to be very helpful! Here is what I've settled with. I put the names back and made it to work without classes, as I found it a little too difficult to work with.
from Tkinter import *
import tkMessageBox
import sys
import binascii #Not for this part
Codenames = ['Mordecai', 'Ezekiel', 'Goliath', 'Abraham', 'Bartholomew',
'Jedediah', 'Solomon', 'Tobias', 'Yohanan', 'Lucifer']
Password = '116987'
def close_protocol():
if tkMessageBox.askokcancel("Quit", "Do you want to quit?"):
sys.exit()
#Below for a later part...
def text_to_bits(text, encoding='utf-8', errors='surrogatepass'):
bits = bin(int(binascii.hexlify(text.encode(encoding, errors)), 16))[2:]
return bits.zfill(8 * ((len(bits) + 7) // 8))
def text_from_bits(bits, encoding='utf-8', errors='surrogatepass'):
n = int(bits, 2)
return int2bytes(n).decode(encoding, errors)
def int2bytes(i):
hex_string = '%x' % i
n = len(hex_string)
return binascii.unhexlify(hex_string.zfill(n + (n & 1)))
#Above for a later part...
SDB = Tk()
SDB.title("SMEGJALBYT DATA BASE")
SDB.withdraw()
SDBLogin = Toplevel()
SDBLogin.title("Login")
SDBLogin.wm_protocol("WM_DELETE_WINDOW", close_protocol)
CodenameLabel = Label(SDBLogin, text="Codename")
CodenameLabel.grid(row=0, column=0)
CodenameEntry = Entry(SDBLogin)
CodenameEntry.grid(row=0, column=2)
PasswordLabel = Label(SDBLogin, text="Password")
PasswordLabel.grid(row=1, column=0)
PasswordEntry = Entry(SDBLogin, show='*')
PasswordEntry.grid(row=1, column=2)
def login_operation():
if CodenameEntry.get() not in Codenames:
tkMessageBox.showinfo("INVALID CODENAME!", "Please verify input in the 'Codename' field")
elif PasswordEntry.get() != Password:
tkMessageBox.showinfo("INVALID PASSWORD!", "Please verify input in the 'Password' field")
else:
SDB.deiconify()
SDBLogin.destroy()
LoginButton = Button(SDBLogin, text="Login", command=login_operation)
LoginButton.grid(row=2, column=1)
#Code continues...
#Code finisles....
SDB.mainloop()
This has the same basic functionality, just organised the way I wanted it.
So I'm using .place to set the location of my widgets at the moment.
def printresults():
SClabelspare=Label(cwindow, text ="Please enter the Customers ID Number:" ).place(x=10,y=560)
I'm looking to call another subroutine that will destroy these widgets. I believe there is something called .destroy() or .place_destroy? I'm not quite sure how these would work though and I have tried to create one that looked like this:
def destroy_widgets():
SClabelspare.destroy()
but it just produces an error code that says NameError: global name 'SClabelspare' is not defined
Any help will be appreciated!
First, place() returns None so SClabelspare==None not a Tkinter ID. Second it is local, so is garbage collected when the function exits. You have to keep a reference to the object which can be done in many ways. A Python tutorial would be a good idea to get the basics before you go further https://wiki.python.org/moin/BeginnersGuide/Programmers Also, programming a Tkinter app without using class structures is a frustrating experience, unless it is something very simple. Otherwise you get errors like yours and have to spend much time and effort trying to overcome them. This is an example that I already have and is meant to to give a general idea of the process.
from Tkinter import *
from functools import partial
class ButtonsTest:
def __init__(self):
self.top = Tk()
self.top.title("Click a button to remove")
Label(self.top, text="Click a button to remove it",
bg="lightyellow").grid(row=0)
self.top_frame = Frame(self.top, width =400, height=400)
self.button_dic = {}
self.buttons()
self.top_frame.grid(row=1, column=0)
Button(self.top_frame, text='Exit', bg="orange",
command=self.top.quit).grid(row=10,column=0, columnspan=5)
self.top.mainloop()
##-------------------------------------------------------------------
def buttons(self):
b_row=1
b_col=0
for but_num in range(1, 11):
## create a button and send the button's number to
## self.cb_handler when the button is pressed
b = Button(self.top_frame, text = str(but_num),
command=partial(self.cb_handler, but_num))
b.grid(row=b_row, column=b_col)
## dictionary key=button number --> button instance
self.button_dic[but_num] = b
b_col += 1
if b_col > 4:
b_col = 0
b_row += 1
##----------------------------------------------------------------
def cb_handler( self, cb_number ):
print "\ncb_handler", cb_number
self.button_dic[cb_number].grid_forget()
##===================================================================
BT=ButtonsTest()
Or, if this is supposed to be very simple, without a lot of hard-to-manage global variables, and if class structures would only introduce needless complexity, you might try something like this (it worked for me in the python3 interpreter from the command line):
from tkinter import *
root = Tk()
def victim():
global vic
vic = Toplevel(root)
vicblab = Label(vic, text='Please bump me off')
vicblab.grid()
def bumper():
global vic
bump = Toplevel(root)
bumpbutt = Button(bump, text='Bump off', command=vic.destroy)
bumpbutt.grid()
victim()
bumper()
My problem is that I'm trying to make a math quiz in tkinter which asks for a name, and then picks a question using the randint function. The person inputs an answer, clicks submit and the app responds to whether it was right or wrong.
The problem arises in that I then want the program to after it has been submitted clear the question and answer and come up with new ones 3 times over (each time adding to a score which is then shown at the end), however I can't seem to find a way to easily do this; I've currently been trying a while loop but it isn't working.
So my question is how would I make that part of the code loop 3 times over asking a different question each time?
My code thus far:
from tkinter import*;from random import randint
class Tk_app(Frame):
def __init__(self, root):
super(Tk_app, self).__init__(root);self.grid();self.createElements()
def nameElements(self):
self.NmLbl = Label(self, text="Name:").grid(row=0)
self.Name = Entry(self);self.Name.grid(row=0, column=1);self.score = int(0)
def createElements(self):
Frame.grid_forget(self)
self.QNum = randint(1, 2)
self.QEnt = Entry(self);self.QEnt.grid(row=1, column=1)
if(self.QNum == 1):
self.QLbl = Label(self, text="What is the air speed velocity of a flying swallow?").grid(row=1)
self.a = "African or European?"
elif(self.QNum == 2):
self.QLbl = Label(self, text="What is your quest?").grid(row=1)
self.a = "To find the holy grail."
else:
self.QLbl = Label(self, text="What is your favourite colour?").grid(row=1)
self.a = "Green"
def submit(self):
FinNam = self.Name.get()
Ans = self.QEnt.get()
if(Ans == self.a):
AnsLbl = Label(self, text = "Well done you got it right, "+FinNam).grid(row=2, column=1)
self.score+=1
else:
AnsLbl = Label(self, text = "Sorry not this time, "+FinNam+" The answer was " + self.a).grid(row=2, column=1)
self.SBut = Button(self, text="submit", command=lambda:submit(self)).grid(row=2)
root = Tk();root.title("Monty Questions")
app = Tk_app.nameElements(root)
fin = int(0)
while(fin<3):
fin+=1
app2 = Tk_app.createElements(root)
root.mainloop()
You don't want to have a while loop outside of your app class. When the program is running, it should have called root.mainloop() before the user interacts with it at all, and stay that way until it is finished. The general structure of this code is not correct.
In Tkinter I would only have this outside of the class definition:
root = Tk()
root.title("Monty Questions")
app = Tk_app()
root.mainloop()
And then you set up all of your tk widgets and whatnot in init:
class Tk_app(Frame):
def __init__(self, root):
Frame.__init__(root);
self.grid();
self.createElements()
self.nameElements()
etc.
Finally, if you just define submit() as a member function of Tk_app instead of as a nested function definition like you have it, you don't need to use a lambda function to pass self. Just do:
class Tk_app():
... __init__ and other things...
def createElements(self):
... some code ...
self.SBut = Button(self, text="submit", command=self.submit ).grid(row=2)
def submit(self, Event):
... submit code ...
The Event is necessary because not only will submit be passed self, as all member functions are, it also gets passed the event that triggered its call.
This might not get you all the way but will hopefully help structure your code in a way that will allow Tkinter to work properly. Check out examples, especially this one, to see how to structure your code. Explore that site and you should get an idea of the vibe of Tkinter.
I'm using Python and Tkinter, and I want the equivalent of onchange event from other toolkits/languages. I want to run code whenever the user updates the state of some widgets.
In my case, I have many Entry, Checkbutton, Spinbox and Radiobutton widgets. Whenever any one of these changes, I want to run my code (in this case, update a text box on the other panel).
(just remember that user may interact with those widgets using either mouse or keyboard, and even using Ctrl+V to paste text)
I think the correct method is to use trace on a tkinter variable that has been assigned to a widget.
For example...
import tkinter
root = tkinter.Tk()
myvar = tkinter.StringVar()
myvar.set('')
mywidget = tkinter.Entry(root,textvariable=myvar,width=10)
mywidget.pack()
def oddblue(a,b,c):
if len(myvar.get())%2 == 0:
mywidget.config(bg='red')
else:
mywidget.config(bg='blue')
mywidget.update_idletasks()
myvar.trace('w',oddblue)
root.mainloop()
The w in trace tells tkinter whenever somebody writes (updates) the variable, which would happen every time someone wrote something in the Entry widget, do oddblue. The trace always passes three values to whatever function you've listed, so you'll need to expect them in your function, hence a,b,c. I usually do nothing with them as everything I need is defined locally anyway. From what I can tell a is the variable object, b is blank (not sure why), and c is the trace mode (i.e.w).
For more info on tkinter variables check this out.
How I would solve this in Tcl would be to make sure that the checkbutton, spinbox and radiobutton widgets are all associated with an array variable. I would then put a trace on the array which would cause a function to be called each time that variable is written. Tcl makes this trivial.
Unfortunately Tkinter doesn't support working with Tcl arrays. Fortunately, it's fairly easy to hack in. If you're adventurous, try the following code.
From the full disclosure department: I threw this together this morning in about half an hour. I haven't actually used this technique in any real code. I couldn't resist the challenge, though, to figure out how to use arrays with Tkinter.
import Tkinter as tk
class MyApp(tk.Tk):
'''Example app that uses Tcl arrays'''
def __init__(self):
tk.Tk.__init__(self)
self.arrayvar = ArrayVar()
self.labelvar = tk.StringVar()
rb1 = tk.Radiobutton(text="one", variable=self.arrayvar("radiobutton"), value=1)
rb2 = tk.Radiobutton(text="two", variable=self.arrayvar("radiobutton"), value=2)
cb = tk.Checkbutton(text="checked?", variable=self.arrayvar("checkbutton"),
onvalue="on", offvalue="off")
entry = tk.Entry(textvariable=self.arrayvar("entry"))
label = tk.Label(textvariable=self.labelvar)
spinbox = tk.Spinbox(from_=1, to=11, textvariable=self.arrayvar("spinbox"))
button = tk.Button(text="click to print contents of array", command=self.OnDump)
for widget in (cb, rb1, rb2, spinbox, entry, button, label):
widget.pack(anchor="w", padx=10)
self.labelvar.set("Click on a widget to see this message change")
self.arrayvar["entry"] = "something witty"
self.arrayvar["radiobutton"] = 2
self.arrayvar["checkbutton"] = "on"
self.arrayvar["spinbox"] = 11
self.arrayvar.trace(mode="w", callback=self.OnTrace)
def OnDump(self):
'''Print the contents of the array'''
print self.arrayvar.get()
def OnTrace(self, varname, elementname, mode):
'''Show the new value in a label'''
self.labelvar.set("%s changed; new value='%s'" % (elementname, self.arrayvar[elementname]))
class ArrayVar(tk.Variable):
'''A variable that works as a Tcl array variable'''
_default = {}
_elementvars = {}
def __del__(self):
self._tk.globalunsetvar(self._name)
for elementvar in self._elementvars:
del elementvar
def __setitem__(self, elementname, value):
if elementname not in self._elementvars:
v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
self._elementvars[elementname] = v
self._elementvars[elementname].set(value)
def __getitem__(self, name):
if name in self._elementvars:
return self._elementvars[name].get()
return None
def __call__(self, elementname):
'''Create a new StringVar as an element in the array'''
if elementname not in self._elementvars:
v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
self._elementvars[elementname] = v
return self._elementvars[elementname]
def set(self, dictvalue):
# this establishes the variable as an array
# as far as the Tcl interpreter is concerned
self._master.eval("array set {%s} {}" % self._name)
for (k, v) in dictvalue.iteritems():
self._tk.call("array","set",self._name, k, v)
def get(self):
'''Return a dictionary that represents the Tcl array'''
value = {}
for (elementname, elementvar) in self._elementvars.iteritems():
value[elementname] = elementvar.get()
return value
class ArrayElementVar(tk.StringVar):
'''A StringVar that represents an element of an array'''
_default = ""
def __init__(self, varname, elementname, master):
self._master = master
self._tk = master.tk
self._name = "%s(%s)" % (varname, elementname)
self.set(self._default)
def __del__(self):
"""Unset the variable in Tcl."""
self._tk.globalunsetvar(self._name)
if __name__ == "__main__":
app=MyApp()
app.wm_geometry("400x200")
app.mainloop()
You have three different ways of doing the same:
1) Use the built-in "command" configuration, like the one you use on buttons
import tkinter as tk
from tkinter import messagebox as tk_messagebox
def spinbox1_callback():
tk_messagebox.showinfo("Spinbox callback", "You changed the spinbox.")
if __name__ == "__main__":
master = tk.Tk()
spinbox1 = tk.Spinbox(master, from_=0, to=10, command=spinbox1_callback)
spinbox1.pack()
tk.mainloop()
2) Use the event bindings to capture specific events:
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
import tkinter as tk
from tkinter import messagebox as tk_messagebox
root = tk.Tk()
def callback(event):
tk_messagebox.showinfo("clicked at", event.x, event.y)
frame = tk.Frame(root, width=100, height=100)
frame.bind("<Button-1>", callback)
frame.pack()
root.mainloop()
3) "trace" changes on a tkinter variable classes, so if your widget uses a StringVar, BooleanVar, IntVar, or DoubleVar in the textvariable parameter, you will get a callback once it gets updated. https://effbot.org/tkinterbook/variable.htm
import tkinter as tk
from tkinter import messagebox as tk_messagebox
if __name__ == "__main__":
master = tk.Tk()
widget_contents = tk.StringVar()
widget_contents.set('')
some_entry = tk.Entry(master,textvariable=widget_contents,width=10)
some_entry.pack()
def entry1_callback(*args):
tk_messagebox.showinfo("entry callback", "You changed the entry %s" % str(args))
some_entry.update_idletasks()
widget_contents.trace('w',entry1_callback)
tk.mainloop()
It's quite late, but yet, somebody found something that might be useful.
The whole idea comes from #bryan Oakley's post
If I understand well, the main problem is to detech Entry widget's . To detect it in spinbox, Checkbutton and Radiobutton you can use command options when creating widget.
To catch the <onChange> in Entry widget you can use Bryan`s approach using Tcl, which generates this event. As I said, this is not my solution, I've only changed it slightly for this case.
For example:
import tkinter as tk
from tkinter import ttk
def generateOnChange(obj):
obj.tk.eval('''
proc widget_proxy {widget widget_command args} {
# call the real tk widget command with the real args
set result [uplevel [linsert $args 0 $widget_command]]
# generate the event for certain types of commands
if {([lindex $args 0] in {insert replace delete}) ||
([lrange $args 0 2] == {mark set insert}) ||
([lrange $args 0 1] == {xview moveto}) ||
([lrange $args 0 1] == {xview scroll}) ||
([lrange $args 0 1] == {yview moveto}) ||
([lrange $args 0 1] == {yview scroll})} {
event generate $widget <<Change>> -when tail
}
# return the result from the real widget command
return $result
}
''')
obj.tk.eval('''
rename {widget} _{widget}
interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
'''.format(widget=str(obj)))
def onEntryChanged(event = None):
print("Entry changed")
def onCheckChanged(event = None):
print("Check button changed")
def onSpinboxChanged(event = None):
print("Spinbox changed")
def onRadioChanged(event = None):
print("Radio changed")
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root, width=400, height=400)
entry = tk.Entry(frame, width=30)
entry.grid(row=0, column=0)
generateOnChange(entry)
entry.bind('<<Change>>', onEntryChanged)
checkbutton = tk.Checkbutton(frame, command=onCheckChanged)
checkbutton.grid(row=1, column=0)
spinbox = tk.Spinbox(frame, width=100, from_=1.0, to=100.0, command=onSpinboxChanged)
spinbox.grid(row=2, column=0)
phone = tk.StringVar()
home = ttk.Radiobutton(frame, text='Home', variable=phone, value='home', command=onRadioChanged)
home.grid(row=3, column=0, sticky=tk.W)
office = ttk.Radiobutton(frame, text='Office', variable=phone, value='office', command=onRadioChanged)
office.grid(row=3, column=0, sticky=tk.E)
frame.pack()
root.mainloop()
Of course modify it to create different callback for plenty of instances (as you mentioned in the question) is easy now.
I hope somebody will find it useful.
So far, I have not encountered any thing equivalent of onChange in Tkinter.
Widgets can be bound to the various events and I have done that explicitly.
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm