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)