1

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?

Skitzafreak
  • 1,797
  • 7
  • 32
  • 51

2 Answers2

1

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()
Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • 1
    First off, holy cow thank you for the wonderful answer. Secondly, is there a way for me to make it so that if I close the `Toplevel` window, if I open it again it will still have the text that was previously written to it? – Skitzafreak Jul 10 '17 at 19:56
  • 1
    You can. If you store the text in a file or in a variable/attribute of the main class then it will still be there. If you use the file method the text will always be there even if you close and reopen the entire program. The variable.attribute method will keep the text as long as your main class is still alive. You could also keep the top level alive and have it be hidden instead, this will allow the program to keep using top level until you want to close the app or make the top level visible again. – Mike - SMT Jul 10 '17 at 19:58
  • 1
    @Skitzafreak: I changed the answer to give an example of how to retain the values for the text box even after its closed using the list method I mentioned. Let me know if that helps. :) – Mike - SMT Jul 10 '17 at 20:09
  • 1
    I wish I had more votes to give you. Thank you very much :) – Skitzafreak Jul 10 '17 at 20:13
  • 1
    @Skitzafreak: It was bugging me that the list was being called ever loop so I changed the code to call the list only once and then use the timed_loop to append the text box. This should prevent any issues of lag caused by a massive list being called ever loop. – Mike - SMT Jul 10 '17 at 20:18
0

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

Jason S
  • 129
  • 3
  • Hmmm...I see. Yeah that definitely makes sense now that I think about it. What would be another way to go about updating it then? So I have a method that's caleld with `root.after()` that checks if the window is alive, and if it is update it? – Skitzafreak Jul 10 '17 at 18:41
  • This answer is not a good answer. Tkinter and `sleep()` don't work well together. Its better to move the entire program into one class and call top level from a method within that class – Mike - SMT Jul 10 '17 at 18:53