Tkinter Text Window, update on open and close - python

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

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.

Instance in another document doesn't recognize variables

class window:
def __init__(self):
addMovie = Tk()
addMovie.minsize(300, 150)
addMovie.maxsize(300, 150)
self.premier = IntVar()
isPremier = Checkbutton(self.addMovie, text="Premier", variable=self.premier)
isPremier.place(relx=0, x=5, y=40)
def detect(self):
if self.premier.get() == 1:
print("Premier")
else:
print("Not premier")
So this code works perfectly if I create the instance in the same file, but when I try to make the instance from another place, for example newWindow = GUI.newMovie.window() only the checkbuttons are not working properly, other elements as Entry and ttk.ComboBox does work as intend
Im calling the detect method from a button, if that info is needed

GUI programming using Tkinter 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()

Button Command with class in Python

I'm trying to get this right but so far no luck. Would appreciate it if someone can help me with it.
import tkinter
class MyGUI:
def __init__(self):
self.main_window = tkinter.Tk()
self.button1 = tkinter.Button(self.main_window,text='Average',command=self.average)
self.button1.pack()
tkinter.mainloop()
def average(self):
self.mini_window = tkinter.Tk()
self.avg_mess = tkinter.Label(self.mini_window,text='Results:')
self.avg_result_var = tkinter.StringVar()
self.avg_result_display = tkinter.Label(self.mini_window,textvariable=self.avg_result_var)
self.avg_mess.pack()
self.avg_result_display.pack()
self.button2 = tkinter.Button(self.mini_window,text='Calculate',command=self.avg_calc)
self.button2.pack()
def avg_calc(self):
self.avg_result = (100+300+80)/3
self.avg_result_var.set(self.avg_result)
gui = MyGUI()
The problem occurs when the Calculate button is clicked but the avg_result_var does not change its value. And hence the avg.result_display remains blank. I suspect there is something wrong with the function call when the button is pressed. I'm using Python 3.x. Thanks.
You're almost doing it correctly, but there are a couple of problems
First, the result never changes because you use the same numbers each time you do a calculation. The result is always the same so it appears that it is not changing.
The second problem is that you're creating two instances of Tk. Tkinter isn't designed to work like that, and it causes problems such as the one you are observing. If you need additional pop-up windows, use Toplevel rather than Tk.
Here's a modified version of your program, though I've added a random number in the computation so you can see it change each time.
import Tkinter as tkinter
import random
class MyGUI:
def __init__(self):
self.main_window = tkinter.Tk()
self.button1 = tkinter.Button(self.main_window,text='Average',command=self.average)
self.button1.pack()
tkinter.mainloop()
def average(self):
self.mini_window = tkinter.Toplevel()
self.avg_mess = tkinter.Label(self.mini_window,text='Results:')
self.avg_result_var = tkinter.StringVar()
self.avg_result_display = tkinter.Label(self.mini_window,textvariable=self.avg_result_var)
self.avg_mess.pack(fill="both")
self.avg_result_display.pack()
self.button2 = tkinter.Button(self.mini_window,text='Calculate',command=self.avg_calc)
self.button2.pack()
def avg_calc(self):
x = random.randint(100,200)
self.avg_result = (100+300+x)/3
print "result:", self.avg_result
self.avg_result_var.set(self.avg_result)
gui = MyGUI()

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