amaurea
amaurea

Reputation: 5067

Displaying multiple independent windows with images in tkinter, and having the main loop exit when they have all been closed

My plotting library needs to be able to show multiple plots at the same time, each of which is represented as a PIL image, and each of which should show up as its own window. The windows should be independent, so closing any one of them should not affect the others, but when all of them have been closed the main loop should exit. This behavior was easy to achieve in qt and wx, but in qt it's proving difficult so far.

Here's the closest I've come so far:

from six.moves import tkinter
from PIL import ImageTk

class Window:
  def __init__(self, img):
    self.window = tkinter.Toplevel()
    self.window.minsize(img.width, img.height)
    self.canvas = tkinter.Canvas(self.window, width=img.width, height=img.height)
    self.canvas.pack()
    self.canvas.configure(background="white")
    self.photo  = ImageTk.PhotoImage(img)
    self.sprite = self.canvas.create_image(0, 0, image=self.photo, anchor=tkinter.NW)
windows = []
for img in imgs:
  windows.append(Window(img))
if len(windows) > 0: windows[0].window.mainloop()

This displays an image in each window, and each of those windows can be closed independently. But it also displays an empty root window which needs to be closed for the main loop to exit, and which will cause all windows to close when closed, which is not the behavior I want.

If I replace tkinter.Toplevel() with tkinter.Tk(), then create_image fails for the second window with an obscure "pyimageX does not exist" error message, where X is an incrementing integer.

Will I have to make an invisible root window, and then manually count how many child windows have closed and trigger destruction of the invisible root window when all of them have closed in order to get the behavior I'm looking for? Or is there a simple way to achieve this?

Edit: Just to clarify: My program is not mainly a Tk app. It spends almost all its time doing other stuff, and only temporarily uses Tk in a single function to display some plots. That's why it's important that the main loop exits after the plots have been closed, to the program can resume its normal operation. Think about how show() in matplotlib works for an example of this scenario.

Upvotes: 0

Views: 560

Answers (2)

Miriam
Miriam

Reputation: 2711

Ok so here's a couple of classes I came up with to solve this problem:

class ImgRoot(tkinter.Tk):
    def __init__(self, imgs):
        super(ImgRoot, self).__init__()
        for i in imgs:
            Window(self, i)
        self.withdraw()
        self.open=True
        self.tick()
    def tick(self):
        if not self.open:
            self.destroy()
        self.open=False
        self.after(100, self.tick)
    def checkin(self):
        self.open=True
class Window(tkinter.Toplevel):
    def __init__(self, root, img):
        super(Window, self).__init__()
        self.root=root
        self.tick()
        self.minsize(img.width, img.height)
        self.canvas = tkinter.Canvas(self, width=img.width, height=img.height)
        self.canvas.pack()
        self.canvas.configure(background="white")
        self.photo  = ImageTk.PhotoImage(img)
        self.sprite = self.canvas.create_image(0, 0, image=self.photo, anchor=tkinter.NW)
    def tick(self):
        self.root.checkin()
        self.after(100, self.tick)

The idea here is to create a main class (ImgRoot) which handles the whole thing. Then, every 0.1 seconds (100 miliseconds), it will check if any of the image windows have told it that they are still alive, and, if not, close. The image windows (Windows) do this by setting the ImgRoot's open attribute to True every 0.1 seconds that they are alive. Here is an example usage:

import tkinter
#above classes go here
ImgRoot(imgs) #imgs is a list as defined in your question
tkinter.mainloop()
print('done') #or whatever you want to do next

Upvotes: 0

Mike - SMT
Mike - SMT

Reputation: 15226

Here is an example of how you might want to do this. This example uses the root window to house a button that will open up all images at the top level.

Make sure you change self.path to your image folder.

import tkinter as tk
import os

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        tk.Button(self, text="Open Images", command=self.open_images).pack()
        self.path = ".\RGB"
    def open_images(self):
        directory = os.fsencode(self.path)

        for file in os.listdir(directory):
            filename = os.fsdecode(file)
            if filename.endswith(".gif"): 
                print(filename)
                top = tk.Toplevel(self)
                img = tk.PhotoImage(file="{}\{}".format(self.path, filename))
                lbl = tk.Label(top, image=img)
                lbl.image = img
                lbl.pack()


if __name__ == '__main__':   
    app = App()
    app.mainloop()

Here is my 2nd example where you can hide the root window and when the last top level window is closed the tkinter instance is also destroyed. This is maned with a simple tracking variable.

import tkinter as tk
import os


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.top_level_count = 0
        self.path = ".\RGB"
        self.open_images()
        self.withdraw()

    def open_images(self):
        directory = os.fsencode(self.path)
        for file in os.listdir(directory):
            filename = os.fsdecode(file)
            if filename.endswith(".gif"): 
                self.top_level_count += 1
                image_top(self, self.path, filename)


    def check_top_count(self):
        print(self.top_level_count)
        if self.top_level_count <= 0:
            self.destroy()


class image_top(tk.Toplevel):
    def __init__(self, controller, path, filename):
        tk.Toplevel.__init__(self, controller)  
        self.controller = controller 
        self.protocol("WM_DELETE_WINDOW", self.handle_close)             
        img = tk.PhotoImage(file="{}\{}".format(path, filename))
        lbl = tk.Label(self, image=img)
        lbl.image = img
        lbl.pack()

    def handle_close(self):
        self.controller.top_level_count -= 1
        self.destroy()
        self.controller.check_top_count()


if __name__ == '__main__':   
    app = App()
    app.mainloop()

Upvotes: 1

Related Questions