0

I'm making an image manipulation class to be used in an intro CS course, using PIL for image manipulation and Tkinter to show the picture. In order for the users to be able to see the picture while manipulating it, I have the graphics operations running on a separate thread, using code similar to this question. This seems to be working (i.e., nothing crashes), but I can't get the image to display -- Tk is starting up, but no window comes up. The code looks like this:

self.root = Tk.Toplevel()
self.frame = tk.Frame(self.root, width=self.image.size[0], height=self.image.size[1])
img = ImageTk.PhotoImage(self.image)
self.label = tk.Label(self.frame, image=img)
self.label.image = img
self.label.pack()
self.frame.pack()
tick()
self.root.mainloop()

The tick function is similar to the one in the linked question. I suspect that my problem is due a a misunderstanding of Tkinter, but I really have no idea.

Also, I can't seem to get the program to exit nicely -- even if I set daemon=True when I construct the Thread which this is running on, I still have to hit C-c when I'm finished. That looks kinda ugly, and I'd rather not bother the students with spurious error messages.

EDIT: Here's some more code.

class Picture():

    ##
    # Constructor. Creates Picture either by passing in the path of an image file as param
    # or by passing in a tuple in the format of (x, y) to indicate the size of a blank image. 
    # Example 1: Picture("image.jpg") constructs Picture with image.jpg 
    # Example 2: Picture((500, 500)) constructs Picture with a blank image of size 500*500
    def __init__(self, param):
        # Check if parameter is the right type, because we can't
        # overload functions
        if isinstance(param, tuple) and len(param) == 2:
            self.image = Image.new('RGB', (param))
        elif isinstance(param, str):
            self.image = Image.open(param)
        else:
            raise TypeError('Parameter to Picture() should be a string or 2-tuple!')
        # Default values for pen
        self.pen_color = (0, 0, 0)
        self.pen_position = (0, 0)
        self.pen_width = 1.0
        self.pen_rotation = 0
        # Pixel data of the image
        self.pixel = self.image.load()
        # Draw object of the image
        self.draw = ImageDraw.Draw(self.image)
        # The main window, and associated widgets.
        self.root = None
        self.label = None
        self.frame = None
        # Threading support, so that we can show the image while
        # continuing to draw on it.
        self.request_queue = queue.Queue()
        self.result_queue = queue.Queue()
        self.thread = threading.Thread(target=self._thread_main)
        self.thread.start()

    def _thread_main(self):
        """
        Runs the main Tkinter loop, as well as setting up all the
        necessary GUI widgets and whatnot. By running Tkinter on
        a separate thread, we can keep the picture displaying
        even after the user's program is finished drawing on it.
        """
        def tick():
            """
            Called whenever Tk's main loop is idle. This lets us perform
            drawing operations on the right thread.
            """
            try:
                f, args, kwargs = self.request_queue.get_nowait()
            except queue.Empty:
                pass
            else:
                value = f(*args, **kwargs)
                self.result_queue.put(value)
            self.root.after_idle(tick)
        self.root = tk.Toplevel()
        self.frame = tk.Frame(self.root, width=self.image.size[0], height=self.image.size[1])
        img = ImageTk.PhotoImage(self.image)
        self.label = tk.Label(self.frame, image=img)
        # This line ensures that Python doesn't try to garbage collect
        # our photo, due to a bug in Tk.
        self.label.image = img
        self.label.pack()
        self.frame.pack()
        tick()
        self.root.mainloop()

    def _submit_operation(self, f, *args, **kwargs):
        """
        Submits an operation to the request queue. The arguments
        should consist of a function, any positional arguments
        to said function, and any keyword arguments to the function.
        If f returns a value, that value will be returned.

        Any function that does something with the picture (i.e.,
        saving it, drawing to it, reading from it, etc.) should
        be called only by submitting it to the queue.
        """
        self.request_queue.put((f, args, kwargs))
        return self.result_queue.get()

    ##
    # Display the picture.
    def display(self):
        def display_func():
            img = ImageTk.PhotoImage(self.image)
            self.label.configure(image=img)
            self.label.image = img
            self.label.pack()
            self.frame.pack()
        self._submit_operation(display_func)
Community
  • 1
  • 1
Peter
  • 1,349
  • 11
  • 21
  • Is the code that you're showing running in the main thread, or in a child thread? And how do you know that Tk is starting up if you don't see a window? – Bryan Oakley Jan 14 '13 at 22:32
  • @BryanOakley -- This runs in the child thread, as the `target` argument to `Thread()`. I know Tk is starting up because Python shows up in the command-tab list of running applications. – Peter Jan 15 '13 at 01:03
  • Tkinter only runs correctly from the main thread. – Bryan Oakley Jan 15 '13 at 02:08
  • see these: http://stackoverflow.com/questions/14073463/mttkinter-doesnt-terminate-threads http://stackoverflow.com/questions/3567238/i-need-a-little-help-with-python-tkinter-and-threading http://stackoverflow.com/questions/7141509/tkinter-wait-for-item-in-queue – Gonzo Jan 15 '13 at 16:17
  • You are all assuming that using threads is a good option here, what if it is not ? @Peter: Do you want to use threads simply because you like them or because you have an actual reason to ? Manipulating, as in drawing over a picture, doesn't require a separate thread. Manipulating, as in running two expensive operations "at once", would be better served as two separated processes as there is no sharing during the operations. I don't believe you have thought this well, and maybe this is just reusing some other code found around where someone didn't think well enough too. Step back, rethink. – mmgp Jan 15 '13 at 16:25
  • @mmgp -- I want to run `tkinter`'s main loop on a separate thread, because that way the image will stay open after the student's code has ended. I realize that they can draw on the image without running the main loop, but if it isn't running Python will exit as soon as their last line of code has executed. As of now, they have to resort to something like using `input('')` as the last line of their code. – Peter Jan 15 '13 at 16:37
  • @Peter: Now I think I understand a different goal, that you never mentioned before. You want, for example, to send commands to a window such that it displays an image or modify a present image. In that case, I would suggest using two separate processes. In one of them you run Tk, and the other one manipulates it. In the first of them you will need a interpreter for your commands, and then you arrange to communicate with the second process. Is that what you are after ? – mmgp Jan 15 '13 at 16:44
  • @Peter: there was I similar question of yours that I've answered at http://stackoverflow.com/questions/14379106/mutli-threading-python-with-tkinter/14381671#14381671. Does it do what you want ? – mmgp Jan 17 '13 at 19:24

0 Answers0