Tkinter Pillow Application 0.2

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

ZIP File
GitHub

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