1

I am creating a tkinter application using Python 3.4 that collects posts from an API, filter them and allow the user to review them and make a decision for each one (ignore, delete, share, etc.)

The user is expected to pick a date and some pages and then click on the 'Collect' button. The program then fetch the posts from the pages and stock them in 'wholeList'. When the user clicks on the second button 'Review', the posts must be filtered and passed to the Reviewer.

My problem is that the Reviewer receives no posts at all, and neither does the Filterer. I have added some debugging print() statements at some places, notably to handlerCollect(), and the result baffled me, hence this post.

Instead of finishing the handlerCollect() callback method when I click on 'Collect', the program puts it on hold somewhere between "DEBUG->1" and "DEBUG->2". The main window does not freezes or anything, for I can click on 'Review' and have it print "DEBUG->4" and open up the Reviewer. When I close the main window, "DEBUG->0" "DEBUG->2" and "DEBUG->3" finaly print, along with the rest of the handlerCollect() method executing.

The same behavior happens with handlerChoosePage(), with "DEBUG->0" being delayed until the tkinter root (TK()) is destroyed. My knowledge of structural programming tells me it should be the very first one printed. Instead, it is the very last. My best conclusion is that I must not be ending my Toplevel mainloop()s correctly. I have to admit I have never encountered something like this before. I thought the proper way of ending mainloop()s on Toplevels was with destroy() and I am very confused as to why methods calling mainloop()s get put on hold until the Tk root is destroyed; not really practical.

Relevant part of my interface

from GUICollector import GUICollector as Collector

class Launcher(tk.Frame):
    def __init__(self, *args, **kwargs):

        ...
        self.allPagesCB     = Checkbutton(self.dateFrame, text="Collect for all pages",
            variable = self.allPagesVar, command=self.handlerChoosePage)
        self.collectBtn = Button(self, text="Collect", command=self.handlerCollect)
        self.reviewBtn  = Button(self, text="Review", command=self.handlerReview)

    def handlerChoosePage(self):
        if self.allPagesVar.get() == 0:
            child = tk.Toplevel(self)
            selector = PageSelector(self.toCollect, child)
            selector.pack(side="top", fill="both", expand=True)
            selector.mainloop()
            print("DEBUG->0")

    def handlerCollect(self):
        print("DEBUG->1")
        self.collect()
        print("DEBUG->4")
        for post in self.collector.getPosts():
            if post not in self.wholeList:
                print("...")
                self.wholeList.append(post.copy())
        self.collector = None
        print(len(self.wholeList), "posts in wholeList")

    def collect(self):
        window = tk.Toplevel(self)
        self.collector = Collector(self.toCollect, self.sinceEpoch, window)
        self.collector.grid(row=0,column=0)
        self.collector.after(500, self.collector.start)
        print("DEBUG->2")
        self.collector.mainloop() # This is what seems to hang indefinetly
        print("DEBUG->3")

    def handlerReview(self):
        print("DEBUG->5")
        print(len(self.wholeList), "posts in wholeList")
        filterer = Filterer(self.wholeList)
        self.wholeList = filterer.done[:]
        window = tk.Toplevel()
        reviewer = Reviewer(self.wholeList[:], window)
        reviewer.grid(row=0,column=0)
        reviewer.mainloop()

The GUICollector module requires no interaction from the user at all. This module seems to work perfectly: doing its job, displaying it is done and then closing after the specified delay. Since the GuiCollector mainloop() seems to be the culprit of the hanging, here is how I end it:

class GUICollector(tk.Frame):

    def __init__(self, pagesList, since, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)

    def start(self, event=None):
        if some_logic:
            self.after(250,self.start)
        else:
            self.done() # Does get called.

    def done(self):
        # Some StringVar update to display we are done on screen
        self.after(1250, self.validate)

    def validate(self):
        self.master.destroy()

The PageSelector module is destroyed with the same call on the press of a button: self.master.destroy()

Here is the revelant output of the program:

DEBUG->1
DEBUG->2
=> collected data of page [PageName]
=> Found 3 posts in page 
DEBUG->5
0 posts in wholeList

[The main window (Launcher) is manually closed at this point]
DEBUG->3
DEBUG->4
...
...
...
3 posts in wholeList
DEBUG->0
  • 1
    You should just have ONE `mainloop`, not many. If you start more than one you might get nested event loops which is a sure way to see weird effects. – schlenk Mar 05 '16 at 22:19
  • @schlenk: not _might_: you _definitely_ will get nested event loops. – Bryan Oakley Mar 05 '16 at 23:07
  • I've seen many comments stating that there should be only one `mainloop` (because there is only one event loop by process if I recall correctly) and I'd love to follow the advice but I don't understand how I can make an application with many windows without calling `mainloop`s on each secondary windows... – Antoine Ender Ezechiel Mar 05 '16 at 23:19
  • Easy. Just don't do it. Remove your current mainloop() calls. Call `root=tk.Tk()` once and once only. All other windows should be a Toplevel, usually with `root` as the first parameter (but for a popup connected with another widget, use the widget). Call root.mainloop last thing, as in pmod's answer. – Terry Jan Reedy Mar 06 '16 at 02:51

2 Answers2

1

The concept of mainloop assumes that you first create and initialize objects (well, at least these that are required at application start, i.e. not used dynamically), set event handlers (implement interface logic) and then go into infinite event handling (what User Interface essentially is), i.e. main loop. So, that is why you see it as it "hangs". This is called event-driven programming

And the important thing is that this event handling is done in one single place, like that:

class GUIApp(tk.Tk):
   ...


app = GUIApp()
app.mainloop()

So, the mainloop returns when the window dies.

pmod
  • 10,450
  • 1
  • 37
  • 50
  • I'm sorry if I missed something, but isn't this how I structured my code? My problem is that, in fact, `mainloop` called on a `TopLevel` doesn't return when it's associated window dies, it only does when the `Tk` window dies. You are right, but I'm not going to call `Tk` for each window, am I? – Antoine Ender Ezechiel Mar 05 '16 at 23:21
  • @AntoineEnderEzechiel No, you didn't. mainloop cannot be called in event handler, this totally violates the concept. The mainloop _is_ the place from where event handlers gets called. Take a look at this http://stackoverflow.com/questions/16115378/tkinter-example-code-for-multiple-windows-why-wont-buttons-load-correctly – pmod Mar 05 '16 at 23:44
  • Note that the `Launcher`'s mainloop is called from the main program. Basically, we are coming back to the same question I asked @schlenk and @BryanOakley: How am I supposed to have multiple windows without having multiple `mainloop`s? – Antoine Ender Ezechiel Mar 05 '16 at 23:54
  • @AntoineEnderEzechiel Where is your particular problem? Try to come up with more specific question. – pmod Mar 06 '16 at 00:04
  • @AntoineEnderEzechiel In short, split the creating of layout and action handlers, then you will see – pmod Mar 06 '16 at 00:08
  • I reread your answer. You mention this _The concept of mainloop assumes that you first create and initialize objects[...]and then go into infinite event handling_. I first understood it in terms of widgets, but do you also mean secondary windows? If I initialize but hide all those windows before the` Launcher`'s `mainloop` is called, will they respond to my root (`Launcher`) `mainloop` once I display them to the user? – Antoine Ender Ezechiel Mar 06 '16 at 01:56
  • yes. The will all be handled by a single mainloop. In theory you could have more than one GUI mainloop (e.g. one per thread), but that is not how tkinter acts. – schlenk Mar 06 '16 at 10:36
0

Until I have some time to refactor my code, I solved the problem by adding the following line to my destroy() calls:

self.quit() # Ends mainloop
self.master.destroy() # Destroys master (window)

I understand this doesn't not solve the bad structure of my code, but it answers my specific question. destroy() doesn't end the mainloop of TopLevels, but quit() does. Adding this line makes my code execute in a predictable way.

I will be accepting @pmod 's answer as soon as I have refactored my code and verified his claim that the Tk() mainloop will cover all child TopLevels.