This is Version 0.2 of my Tkinter Pillow Application, Version 0.1 being here.
This is an ongoing project to develop an image editing application using Tkinter for the UI (with other GUI toolkits as a long-term objective) and the Pillow library for image editing functionality.
Version 0.1 established the overall architecture of the solution and enabled users to open and display an image. For Version 0.2 I will add a number of essential improvements to the user interface.
Version 0.2 Enhancements
The individual enhancements for this version are all quite minor but they add up to a significant improvement in the way the application looks and behaves.
- Info panel at the bottom of the window to show data about the current image
- An About box showing application information
- An application quit event handler, ultimately providing "do you wish to save changes" functionality
- Toolbar button graphics
- Scrollbars for when the image is too big for the window.
- An application icon.
- A function in the UI to handle changes to the image. This is passed to and called by the class performing changes. It calls:
- A function to display the image
- A function to size the canvas displaying the image
- A function to populate the info panel
- A function to set the window title
This is a screenshot of Version 0.2.
The Code
The project consists of two Python files:
- pillowappengine.py
- pillowapptkinter.py
as well as a few graphics files for the buttons and icon. The files can be downloaded as a zip, or you can clone/download the Github repository if you prefer.
Source Code Links
Much of the code is the same as Version 0.1 so I will not repeat it all here; I'll just show the snippets implementing the Version 0.2 enhancements.
Application Icon
pillowapptkinter.py
self.icon = PhotoImage(file='icon.gif') self.window.tk.call('wm', 'iconphoto', self.window._w, self.icon)
The icon attribute is set to an image loaded from icon.gif, and then set as the application icon.
Scrollbars
pillowapptkinter.py
self.hscroll = Scrollbar(self.image_frame, orient='horizontal') self.hscroll.grid(row=1, column=0, sticky=E+W) self.vscroll = Scrollbar(self.image_frame, orient='vertical') self.vscroll.grid(row=0, column=1, sticky=N+S)
This comes just after the code to create a frame for the image, and simply creates a pair of scrollbars and adds them to the frame.
Info Panel
pillowapptkinter.py
self.infobar = Frame(self.window, borderwidth=1, relief="raised") self.infobar.grid(row=2, column=0, padx=0, pady=0, sticky=W+E) self.filename_label = Label(self.infobar, text="Filename") self.filename_label.grid(row=0, column=0, padx=2, pady=2, sticky=W) self.filename_text = Label(self.infobar, bg="white", text="", borderwidth=1, relief="sunken", width=32, anchor="w") self.filename_text.grid(row=0, column=1, padx=2, pady=2, sticky=W) self.width_label = Label(self.infobar, text="Width") self.width_label.grid(row=0, column=2, padx=2, pady=2, sticky=W) self.width_text = Label(self.infobar, bg="white", text="", borderwidth=1, relief="sunken", width=5) self.width_text.grid(row=0, column=3, padx=2, pady=2, sticky=W) self.height_label = Label(self.infobar, text="Height") self.height_label.grid(row=0, column=4, padx=2, pady=2, sticky=W) self.height_text = Label(self.infobar, bg="white", text="", borderwidth=1, relief="sunken", width=5) self.height_text.grid(row=0, column=5, padx=2, pady=2, sticky=W) self.format_label = Label(self.infobar, text="Format") self.format_label.grid(row=0, column=6, padx=2, pady=2, sticky=W) self.format_text = Label(self.infobar, bg="white", text="", borderwidth=1, relief="sunken", width=5) self.format_text.grid(row=0, column=7, padx=2, pady=2, sticky=W) self.mode_label = Label(self.infobar, text="Mode") self.mode_label.grid(row=0, column=8, padx=2, pady=2, sticky=W) self.mode_text = Label(self.infobar, bg="white", text="", borderwidth=1, relief="sunken", width=5) self.mode_text.grid(row=0, column=9, padx=2, pady=2, sticky=W)
Firstly we create a separate frame for the buttons, and then add a few labels to it.
About
pillowapptkinter.py
def about(self): """ Shows a message box containing application information. """ about_text = "CodeDrome\ncodedrome.com\n\n{}\n\nUsing Pillow {}" about_text = about_text.format(self.application_name, pillowappengine.PillowAppEngine.PILLOW_VERSION) messagebox.showinfo('About', about_text)
This code generates a string in two stages, then shows it in a message box.
Toolbar Buttons
pillowapptkinter.py
self.toolbar = Frame(self.window, borderwidth=1, relief="raised") self.toolbar.grid(row=0, column=0, padx=0, pady=0, sticky=W+E) self.open_button = Button(self.toolbar, text="Open", command=self.open) self.open_button.grid(row=0, column=0, padx=2, pady=2, sticky=W) self.open_graphic = PhotoImage(file="open.png") self.open_button.config(image=self.open_graphic, width="26", height="26") self.save_button = Button(self.toolbar, text="Save", command=self.save) self.save_button.grid(row=0, column=1, padx=2, pady=2, sticky=W) self.save_graphic = PhotoImage(file="save.png") self.save_button.config(image=self.save_graphic, width="26", height="26") self.help_button = Button(self.toolbar, text="Help", command=self.about) self.help_button.grid(row=0, column=2, padx=2, pady=2, sticky=W) self.help_graphic = PhotoImage(file="help.png") self.help_button.config(image=self.help_graphic, width="26", height="26")
The buttons existed in Version 0.1 but only displayed text. Here we load graphics and add them to the buttons.
on_image_change
pillowapptkinter.py
on_image_change(self): """ Called by the PillowAppEngine when any changes are made to the image, or when an image is opened or closed. Calls the necessary functions to update the UI. """ self.show_image() self.set_image_canvas_size() self.show_info() self.set_window_title()
This function is passed to PillowAppEngine's __init__, and the engine calls it when an image is opened, closed or edited. As you can see it just calls other functions to update various parts of the UI.
show_image
pillowapptkinter.py
def show_image(self): """ Creates a PIL.ImageTk.PhotoImage and displays it in the UI. """ if self.pae.image is not None: self.tkinter_image = PIL.ImageTk.PhotoImage(self.pae.image) self.image_canvas.create_image(0, 0, image=self.tkinter_image, anchor="nw") self.image_canvas.grid(row=0, column=0, padx=2, pady=2)
Pillow includes a module called ImageTk which creates a Tkinter-compatible image, which you can see in action in the first line. Note that this image is saved as an attribute so it doesn't go out of scope when the function exits. The image is then added to the canvas which is then inserted into the grid.
set_image_canvas_size
pillowapptkinter.py
def set_image_canvas_size(self): """ Sets the size of the canvas to that of the image. If this is larger than the parent window scrollbars will be enabled, so scrollregion needs to be set. """ if self.pae.image is not None: self.window.update() w = self.tkinter_image.width() h = self.tkinter_image.height() self.image_canvas.config(width=w, height=h, scrollregion=(0, 0, w, h))
The canvas is contained in a frame which is sized to fit the window. The canvas itself is sized to the image as you can see here. Note that we also need to set the scrollregion to the coordinates of the top left and the width/height of the canvas; if we don't the scrollbars in the frame will not work. (Why can a frame or its scrollbars not figure out the size of their contents automatically? I don't know!)
show_info
pillowapptkinter.py
def show_info(self): """ Sets the image data in the panel if an image is open, or sets data to empty strings if there is no image. """ if self.pae.image != None: info = self.pae.get_properties() self.filename_text.config(text=info["filename"]) self.width_text.config(text=info["width"]) self.height_text.config(text=info["height"]) self.format_text.config(text=info["format"]) self.mode_text.config(text=info["mode"]) else: self.filename_text.config(text="") self.width_text.config(text="") self.height_text.config(text="") self.format_text.config(text="") self.mode_text.config(text="")
This function populates the labels in the info panel. If an image is open it calls the PillowAppEngine's get_properties method and sets the content from the resulting dictionary. If no image is open it just sets everything to empty strings.
set_window_title
pillowapptkinter.py
def set_window_title(self): """ Shows just the application name if no image is open, or the application name and filename if an image is open. """ if self.pae.image != None: self.window.title(self.application_name + ": " + self.pae.get_properties()["filename"]) else: self.window.title(self.application_name)
Lastly we need to set the window title. If an image is open the title is set to the string set as the application name plus the filename, otherwise just the application name is used.
Now we can run the program with this command, and check out the new functionality:
Running the program
python3.7 pillowapptkinter.py