GUI programming using Tkinter Python - python

I have a Tkinter GUI having 2 entry fields, 2 buttons ( initialization of these not shown in code). There is one more button (initialized in code) which performs the main task of performing change detection on two images. Also there is a progress bar.
Now, when the task of change detection has been completed, I want to display the 4 images(pre, post, aligned, chng) returned by wave.changedetection() in a separate Tkinter window. I want the new window to come only after changedetection() has completed.(wave.py is my own file, not some module)
Unfortunately, if I try to add code to make new window, Tk.Toplevel() ,after the wave.changedetection() call, nothing happens and the main GUI window becomes unresponsive and has to be killed.
There is no way to know when the new created thread (start_thread)completes it's work, so that I can do Tk.Toplevel() there.
How can I do what I require?
class GUI(Tkinter.Tk):
def __init__(self, parent)
Tkinter.Tk.__init__(self, parent)
self.parent = parent
self.initialize()
def initialize(self):
self.button = Tkinter.Button(text = "Start")
self.button.bind('<Button-1>', self.OnButtonClick)
self.button.pack()
self.int = Tkinter.IntVar()
self.pgbar = Tkinter.ProgressBar(variable = self.int, mode = determinate)
def OnButtonClick(self,event):
#this func has been made since I have more buttons.It may seem redundant here
self.button['command'] = self.start_thread()
self.update_idletasks()
def start_thread(self):
self.int_var.set(1)
q = queue.Queue()
self.secondary_thread = threading.Thread(target = self.change)
self.secondary_thread.start()
self.after(50, self.check_queue, q)
def check_queue(self, q):
while True:
try:
x = wave.q.get_nowait()
except queue.Empty :
self.after(50,self.check_queue,q)
break
else:
self.int_var.set(x)
if x == 6:
self.button3['state'] = 'normal'
break
def change(self):
'''The 6 functions of wave.changedetection() change the value of self.int
due to which progress bar progresses.'''
pre, post, aligned, chng = wave.changedetection(self.entry_1.get(),
self.entry_2.get())
if __name__ == '__main__':
gui = GUI(None)
gui.mainloop()
code to update progress bar taken from here (2nd answer,Honest Abe's answer)

You have to be able to differentiate name spaces, i.e. this is in the main window and this is in the Toplevel. I would suggest that you get the Toplevels working first and then decide if you want to add threading or not. The code below is a simple example of creating Toplevels and shows how to place widgets in a specific name space (window in this case). You may or may not want a separate "create a Toplevel" class if there are functions you want to associate with each Toplevel's namespace. Also there are examples on the web on using Tkinter's "after" to update a progressbar. That is a different question so start another thread if you have questions about the progressbar.
try:
import Tkinter as tk ## Python 2.x
except ImportError:
import tkinter as tk ## Python 3.x
from functools import partial
class OpenToplevels():
""" open and close additional Toplevels with a button
"""
def __init__(self):
self.root = tk.Tk()
self.button_ctr=0
## in the "root" namespace *********************
but=tk.Button(self.root, text="Open a Toplevel",
command=self.open_another)
but.grid(row=0, column=0)
tk.Button(self.root, text="Exit Tkinter", bg="red",
command=self.root.quit).grid(row=1, column=0, sticky="we")
self.root.mainloop()
def close_it(self, id):
## destroy the window in this id's namespace ***********
id.destroy()
## id.withdraw()
## id.iconify()
def open_another(self):
self.button_ctr += 1
id = tk.Toplevel(self.root)
id.title("Toplevel #%d" % (self.button_ctr))
## in the "id for this Toplevel" namespace ***********
tk.Button(id, text="Close Toplevel #%d" % (self.button_ctr),
command=partial(self.close_it, id),
bg="orange", width=20).grid(row=1, column=0)
Ot=OpenToplevels()

Related

tkinter - insert text in ScrolledText from external function

I am new to tkinter GUI programming. I tried to search my problem, but I was unable to formulate the correct question without a description of my problem.
I designed a small GUI - for the example here - with a button and a ScrolledText item.
import tkinter as tk
from tkinter import messagebox as msg
from tkinter.ttk import Notebook
from tkinter import filedialog
import tkinter.scrolledtext as tkscrolled
import do_something as ds
import os
import time
class Fatt(tk.Tk):
def __init__(self):
super().__init__()
# window setup
self.title("Test Gui")
self.geometry("1024x768")
self.resizable(0, 0)
# tab
self.notebook = Notebook(self)
# define tabs
res_avg_tab = tk.Frame(self.notebook)
# group nodal averaging
group_avg = tk.LabelFrame(res_avg_tab, text="Perform nodal averaging of all selected DB files")
group_avg.pack(padx=10, pady=10)
# nodal averaging button
self.avg_button = tk.Button(group_avg, text="Perform Nodal Averaging",
command=self.nodal_avg, bg="lightgrey", fg="black", width=50)
self.avg_button.pack(side=tk.TOP, pady=10, padx=10)
# scrolled log-text window
# group LOG
group_log = tk.LabelFrame(res_avg_tab, text="Result Averaging Output (LOG)")
group_log.pack(padx=10, pady=10)
self.avg_log = tkscrolled.ScrolledText(group_log, bg="white", fg="black", height=13, width=110)
self.avg_log.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)
# status-bar
self.status_text = tk.StringVar(res_avg_tab)
self.status_text.set("---")
self.status = tk.Label(res_avg_tab, textvar=self.status_text,
bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status.pack(side=tk.BOTTOM, fill=tk.BOTH)
# add everything to tabs
self.notebook.add(res_avg_tab, text="Average Results")
self.notebook.pack(fill=tk.BOTH, expand=True)
def show_cmb_file_creator(self):
pass
def nodal_avg(self):
sel_dbs = ["file1", "file2", "file3"]
# write file-list to log-window
self.avg_log.insert(tk.INSERT, "\nSelected Files for Nodal Averaging:\n")
for i in sel_dbs:
self.avg_log.insert(tk.INSERT, i+'\n')
self.avg_log.see(tk.END)
# if yes --> average results
if msg.askyesno("Nodal Averaging", "Perform nodal averaging with selected db-results?"):
start = time.time()
self.status_text.set("processing...")
self.config(cursor="wait")
self.avg_log.insert(tk.INSERT, "Start nodal averaging - this may take some time...\n")
class_obj = ds.DoSomething(i, self.avg_log)
for i in sel_dbs:
class_obj.do_something()
end = time.time()
overall_time_str = " Overall Averaging RUNTIME: {0:.2f} sec ({1:.1f} min) ".format(end-start, (end-start)/60.0)
self.avg_log.insert(tk.INSERT, "\n{0:*^80}".format(overall_time_str))
self.avg_log.see(tk.END)
self.status_text.set("---")
self.config(cursor="")
def browse_dir(self):
pass
def copy_to_clipboard(self, text=None):
pass
if __name__=="__main__":
fatt = Fatt()
fatt.mainloop()
The button "avg_button" executes the function "nodal_avg" and the main purpose of this function is to instantiate an external class and run a method.
class_obj = ds.DoSomething(i, self.avg_log)
for i in sel_dbs:
class_obj.do_something()
This class contains the main logic of my software and it contains a lot of print outputs.
import tkinter.scrolledtext as tkscrolled
class DoSomething:
def __init__(self, my_file, outp_print=print):
self.my_file = my_file
self.outp_print = outp_print
# my-print function
# for tkinter-log output
def myprint(self, text):
if self.outp_print==print:
print(text)
elif isinstance(self.outp_print, tkscrolled.ScrolledText):
self.outp_print.insert("end", text+'\n')
else:
print("myprint - ERROR: {0}".format(str(self.outp_print)))
def do_something(self):
for i in range(0,100000):
self.myprint("{0:d} - printed output".format(i))
I would like to print the output of the class/method to the ScrolledText window, but I also like to maintain the classic print functionality. Therefore I use the "myprint" method - which is able to use print or ScrolledText.insert for printing (I do not know if this is a smart approach?!).
If I run the code it basically works - but the ScrolledText window does not update on every print, only when the method in the external class is finished - then the output appears.
So my question is - how can I continuously update my ScrolledText window with my output string?
Thank you very much.
Best Regards
Michael

Tkinter Text Window, update on open and close

I am trying to create a Tkinter app, where when you press a button, a new Window opens which has continually updating text about certain parts of the program. My problem is the section of code where I am trying to add the text to the screen. This is what I have written:
import tkinter as tk
import time
class TextWindow(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.textArea = tk.Text(self, height = 10, width = 30)
self.textArea.pack(side = "left", fill = "y")
bar = tk.Scrollbar(self)
bar.pack(side = "right", fill = "y")
bar.config(command = self.textArea.yview)
def output(self, value):
outputVal = str(value)
self.textArea.inser('end', "{0}\n".format(outputVal))
self.textArea.see('end')
def openWindow():
textWindow = tk.Toplevel(root)
textFrame = TextWindow(textWindow)
textFrame.pack()
value = 0.0
alive = True
while alive:
if textWindow.winfo_exists:
value = value + 0.1
textFrame.output(value)
time.sleep(0.1)
else:
alive = False
root = tk.Tk
btn = tk.Button(root, text = "Click", command = openWindow)
btn.pack()
root.mainloop()
When I comment out the while loop in the openWindow method, the window opens and closes, and reopens, no problem. However when the code is there, I never see the window when I press the button.
I tried running it through the IDLE debugger, and I am not getting any errors, and everything runs through the loop fine, however the Window still never appears. What is my problem?
The answer that Jason S gave is not a good example. You can avoid any issues with sleep by just using after() instead. Don't settle for "Kinda works".
Here is a break down of how you could accomplish what you need without having the problems associated with sleep() and tkinter.
First you are importing Tk() wrong. Don't do tk.Tk do tk.Tk()
Now lets move the entire program into a single class. This will provide us with the ability to use class attributes and make things a bit easier to work with.
Here we create a class called guiapp(tk.Frame): you can name it what you want but this is just my example. Then make sure you are passing root using guiapp(root) so we can work in this class on the tk.Tk() instance. This will be shown at the bottom of the program where the class is instantiated.
Because we have passed root to the class we can place the button that opens the Toplevel window on our self.master attribute.
UPDATE: Changed how data is sent to the Textbox in Toplevel so we can retain the information in case you want to reopen top level. per your comment.
import tkinter as tk
class guiapp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.value = 0.0
self.alive = True
self.list_for_toplevel = [] # added list to retain values for Toplevel
btn = tk.Button(self.master, text = "Click", command = self.TextWindow)
btn.pack()
Here we add the method to define the Topelevel we are going to create.
Because everything is inside this one class we can create this Topelevel as a Toplevel of self.master. At the end of this method we call the self.timed_loop() method I added that manages the timed portion of your program. UPDATE: added a call to a new function.
def TextWindow(self):
self.textWindow = tk.Toplevel(self.master)
self.textFrame = tk.Frame(self.textWindow)
self.textFrame.pack()
self.textArea = tk.Text(self.textWindow, height = 10, width = 30)
self.textArea.pack(side = "left", fill = "y")
bar = tk.Scrollbar(self.textWindow)
bar.pack(side = "right", fill = "y")
bar.config(command = self.textArea.yview)
self.alive = True
self.add_list_first()
UPDATE: Added a new function called add_list_first(self):. This will allow us to first add any values that are stored in the list then we can call timed_loop() to continue appending the list and counting.
def add_list_first(self):
for item in self.list_for_toplevel:
self.textArea.insert('end', "{}\n".format(item))
self.textArea.see('end')
self.timed_loop()
Here we have created a method to perform the task you have in you code for the Toplevel that uses the after() function from tkinter. ever 1000 is equal to 1 second, so play with that timer if you want. The first part of after() is for the time in milliseconds and the 2nd part is the function being called. In this case it calls itself to continue the loop until either the Toplevel window self.textWindow is closed or the self.alive variable is no longer True.
UPDATE: I have added a for loop to insert the list instead of directly imputing each value. This way we can retain the data if we want to reopen the Toplevel.
def timed_loop(self):
if self.alive == True and tk.Toplevel.winfo_exists(self.textWindow):
self.master.after(1000, self.timed_loop)
self.value += 1
self.list_for_toplevel.append(self.value)
self.textArea.delete(1.0, "end-1c")
for item in self.list_for_toplevel:
self.textArea.insert('end', "{}\n".format(item))
self.textArea.see('end')
else:
self.alive = False
This is the preferred way to start your class going in tkinter. As you can see we have created root as tk.Tk() and passed root into the the class guiapp(). Also note that I assigned this instance of the class to the variable name myapp. This will allow us to interact with the class from outside of the class if you ever need to. It does not make a difference in this case but I thought I would add it just the same.
if __name__ == "__main__":
root = tk.Tk()
myapp = guiapp(root)
root.mainloop()
Here is the copy paste version for you to use.
import tkinter as tk
class guiapp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.value = 0.0
self.alive = True
self.list_for_toplevel = []
btn = tk.Button(self.master, text = "Click", command = self.TextWindow)
btn.pack()
def TextWindow(self):
self.textWindow = tk.Toplevel(self.master)
self.textFrame = tk.Frame(self.textWindow)
self.textFrame.pack()
self.textArea = tk.Text(self.textWindow, height = 10, width = 30)
self.textArea.pack(side = "left", fill = "y")
bar = tk.Scrollbar(self.textWindow)
bar.pack(side = "right", fill = "y")
bar.config(command = self.textArea.yview)
self.alive = True
self.add_list_first()
def add_list_first(self):
for item in self.list_for_toplevel:
self.textArea.insert('end', "{}\n".format(item))
self.textArea.see('end')
self.timed_loop()
def timed_loop(self):
if self.alive == True and tk.Toplevel.winfo_exists(self.textWindow):
self.master.after(1000, self.timed_loop)
self.value += 1
self.list_for_toplevel.append(self.value)
outputVal = str(self.value)
self.textArea.insert('end', "{0}\n".format(outputVal))
self.textArea.see('end')
else:
self.alive = False
if __name__ == "__main__":
root = tk.Tk()
myapp = guiapp(root)
root.mainloop()
The problem is that you are never giving control back to the Tkinter main loop. The code gets stuck executing in the while loop, meaning Tkinter never gets to refresh the display or process any other events. You could force update by calling root.update() right before time.sleep(0.1), but this is not really optimal and the display will be unresponsive while sleeping. Depending on what you are doing, it may be good enough.
See here for additional explanation

Python Tkinter custom msgbox fails during import

I am trying to create a custom text entry box that has a text entry field in the center and can return the user's input. I'm specifically trying to make something that can be imported and re-used in other programs. My current iteration uses an "inputdialog" class that works fine for getting the input when it's in it's own .py file, but if I try to import the module into another script, the "ok" button seemingly does nothing, and the "x" button throws an "ImportError: cannot import name 'inputdialog'" error after closing the pop-up.
The following is my code which currently works if run as a standalone script:
from tkinter import *
class inputdialog:
def __init__(self):
self.value = None
self.root = Tk()
self.root.withdraw()
self.top = Toplevel(self.root)
Label(self.top, text="Value").pack()
self.e = Entry(self.top)
self.e.pack(padx=5)
b = Button(self.top, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
self.root.mainloop()
def ok(self):
self.value = self.e.get()
self.root.destroy()
if __name__ == "__main__":
test = inputdialog()
print(test.value)
The following is how I've imported this module into another program, which does not currently work:
if __name__ == "__main__":
# These two lines below are needed to make sure that "askopenfilename"
# doesn't show it's top level tkinter window
root = Tk()
root.withdraw()
entdbemp = askopenfilename(title="Please select a file: ")
# Here is my non-working module call
master = inputdialog()
print(master.value)
Why would my "OK" button cease to function when imported? Is there any way I can fix my code to allow it to be imported? Is there a better way of trying to make an importable text entry module using tkinter?
The problem has nothing to do with importing. The problem is that in your second example you call Tk() twice: once in each file. Also, when you call 'destroy' you are only removing the GUI from the screen, you are not exiting the mainloop.
To make this work, you need to remove the lines that create a root in the inputdialog class, move the mainloop() call to a point after starting the class, and then call the quit method for the toplevel window (not the root window):
from tkinter import *
class inputdialog:
def __init__(self, master=None):
self.value = None
self.top = Toplevel(master)
Label(self.top, text="Value").pack()
self.e = Entry(self.top)
self.e.pack(padx=5)
b = Button(self.top, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
def ok(self):
self.value = self.e.get()
self.top.quit()
if __name__ == "__main__":
root = Tk()
root.withdraw()
master = inputdialog(root)
root.mainloop()
print(master.value)
If you want to be neat and proper: rather than making a class that wraps around a different class, in OOP and GUIs we like to make a subclass:
import tkinter as tk
class inputdialog(tk.Toplevel):
def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.value = None
tk.Label(self, text="Value").pack()
self.e = tk.Entry(self)
self.e.pack(padx=5)
b = tk.Button(self, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
def ok(self):
self.value = self.e.get()
self.quit()
if __name__ == "__main__":
root = tk.Tk()
root.withdraw()
master = inputdialog(root)
root.mainloop()
print(master.value)
Also, wildcard imports (from module import *) are ugly and against PEP8; don't use them.
BTW, the easygui package has done all this already; you may just want to install and use that.

Tkinter python multithread (avoid "Not Responding")

I'm trying to write a Python GUI program with tkinter.
I want to make two thread. One runing with the main_form function to keep tkinter from keep update and loop (avoid "Not Responding").
The other, when the button1 (btn1) is clicked make function sci_thread() start running and start thread2 that execute the main_scikit with long time code.
But tkinter keep Not Responding.
Below is my code:
import threading
class class_one:
def main_scikit(seft):
######
code_take_loooong_time
######
def save(seft):
pass
def main_form(seft):
root = Tk( )
root.minsize(width=300, height=500)
ent1 = Entry(width=30)
ent1.grid(row=0,column=1,padx = 10,pady=5)
bnt1 = Button(root,text = "Start",command=lambda : seft.sci_thread())
bnt1.grid(row=5,column=0,padx = 10)
root.update()
root.mainloop()
def sci_thread(seft):
maincal = threading.Thread(2,seft.main_scikit())
maincal.start()
co = class_one()
mainfz = threading.Thread(1,co.main_form());
mainfz.start()
Your app is unresponsive because your target parameter executed when declared and result of that passed as target. And, obviously, because of that you GUI is unresponsive while code_take_loooong_time being executed in GUI's thread. To deal with it - get rid of redundant parentheses.
Try this snippet:
import threading
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class class_one:
def main_scikit(self):
######
# code_take_loooong_time
# same as sleep
threading.Event().wait(5)
# some print
self.print_active_threads_count()
######
def save(self):
pass
def main_form(self):
self.root = tk.Tk()
self.root.minsize(width=300, height=500)
self.ent1 = tk.Entry(self.root, width=30)
self.ent1.grid(row=0, column=1, padx=10, pady=5)
self.bnt1 = tk.Button(self.root, text="Start", command=self.sci_thread)
self.bnt1.grid(row=5, column=0, padx=10)
self.root.update()
self.root.mainloop()
def sci_thread(self):
maincal = threading.Thread(target=self.main_scikit)
maincal.start()
def print_active_threads_count(self):
msg = 'Active threads: %d ' % threading.active_count()
self.ent1.delete(0, 'end')
self.ent1.insert(0, msg)
print(msg)
co = class_one()
mainfz = threading.Thread(target=co.main_form)
mainfz.start()
Links:
Similar problem with a tkinter button's parameter
More general: How to pass a method as an parameter
Threading docs
P.S.:
Also, be careful when you start a tkinter application not in the main thread because tkinter expects (in general) that mainloop is outer-most loop possible and that all Tcl commands invoked from the same thread. So there can be many and more synchronisation problem with all that, even if you just trying to quit GUI!
In conclusion, maybe this and that would give you some new ideas.

How to run a code whenever a Tkinter widget value changes?

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

Categories