Build a GUI from Scratch: A Step-by-Step Python Guide

Does GUI programming sound interesting to you? If yes, let’s build one from scratch!

This guide will help you get started with building GUI applications in Python with the help of Tkinter. There are a lot of GUI frameworks available but we’ll be using Tkinter simply for the fact that it’s easy to use, and the syntax remains the same no matter whether you are using Windows, Mac, or Ubuntu.

Today, we will be building a Photo Editor GUI with some basic functionalities. We start by importing Tkinter and other required libraries.

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog, colorchooser
from PIL import Image, ImageTk, ImageFilter, ImageEnhance
After importing the very important step is to create a window wherein all that we want to create will be displayed, we do this with the following code:

root = tk.Tk() # To create a window
root.geometry("1100x600")
root.title("Photo Editor")
root.mainloop() # To hold the window

One can create multiple windows and may desire different widgets for each window. To distinguish which window should have which widgets, we write our code between these two lines — window declaration and .mainloop().

Here I have defined the size of the window using root.geometry() and added a title using root.title(), running the above code should display a blank window like this:

Notice the top left side displays the title of our window Now that we have our window screen ready, we can start adding widgets to it. In Tkinter, we can use .pack(), .place(), or .grid() to place any widget.

.pack() — places the widgets in one of the 4 available positions i.e. top (default), bottom, left, or right

.place() — places the widgets using the x, y coordinates

.grid() — places the widget using row and column coordinates

Let’s understand what Buttons, Labels, Entry, and Combobox are.

ttk.Button() — creates a clickable button which can perform certain action

ttk.Label() — creates a label which can display text or an image

ttk.Entry() — lets user enter information, can be text or numbers

ttk.Combobox() — used to display a list of options to choose from (basically a dropdown list)

I’m planning to add a couple of buttons for performing some actions like opening an image, applying filter, picking a border color, applying border, clearing all the applied filters and finally saving the image.

root = tk.Tk()
root.geometry("1150x600")
root.title("Photo Editor")

ttk.Button(root, text="Open Image").grid(row=0, column=0, padx=10, pady=10)

ttk.Label(root, text="Filter:").grid(row=0, column=1, padx=10, pady=10)
filter_options = ["Black and White", "Sepia"]
ttk.Combobox(root, values=filter_options).grid(row=0, column=2, padx=10, pady=10)

ttk.Button(root, text="Apply Filter").grid(row=0, column=3, padx=10, pady=10)

ttk.Label(root, text="Border Size:").grid(row=0, column=4, padx=10, pady=10)
ttk.Entry(root).grid(row=0, column=5, padx=10, pady=10)

ttk.Label(root, text="Border Color:").grid(row=0, column=6, padx=10, pady=10)
ttk.Button(root, text="Pick Color").grid(row=0, column=7, padx=10, pady=10)

ttk.Button(root, text="Add Border").grid(row=0, column=8, padx=10, pady=10)

ttk.Button(root, text="Clear All Filters").grid(row=0, column=9, padx=10, pady=10)

ttk.Button(root, text="Save Image").grid(row=0, column=10, padx=10, pady=10)

root.mainloop()

I’ve used ttk.Combobox() which lets the user select filter to be applied and ttk.Entry() to lets the user decide what border size he/she wants to be applied.

Further you may notice how .grid() lets you easily place your widgets and padding allows you to add space between the widgets, have a look at the following images!

Left Image (With Padding) Vs Right Image (Without Padding) So far this is how your window should look like. But wait none of the buttons are working because we haven’t defined any action to these buttons, let’s add them!

filter_var = tk.StringVar()
border_size = tk.IntVar()
border_color = tk.StringVar()

original_image = None
edited_image = None

def process_image(root, filter_var, border_size, border_color):
global original_image, edited_image
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.gif")])

    if file_path:
        image_path = file_path
        original_image = Image.open(image_path)
        edited_image = original_image.copy()
        display_image(edited_image)

We first start by opening the image, we do this with the help of filedialog.askopenfilename() and then make a copy of the original image and save it as edited image (Don’t worry we’ll keep updating this image!)

def apply_filter():
global edited_image
selected_filter = filter_var.get()

    if edited_image and selected_filter == "Black and White":
        edited_image = ImageEnhance.Color(edited_image).enhance(0.0)
    elif edited_image and selected_filter == "Sepia":
        edited_image = apply_sepia_filter(edited_image)

    display_image(edited_image)

def apply_sepia_filter(image):
sepia = Image.new('RGB', image.size, (112, 66, 20))
sepia = Image.blend(image, sepia, 0.5)
return sepia

Now we start by applying filters, for now we will only have 2 filters — Black & White and Sepia.

The .enhance() in the apply_filter() is used to enhance the image, here we set its value to 0.0 which means there should be no color enhancement in the image eventually making it a black and white image and saving it in the edited_image For applying the sepia filter, I have defined another function and assigned its RGB triplet i.e. (112, 66, 20).

border_size.set(20) # Default border size: 20
border_color.set("#000000") # Default border color: Black

def add_border():
global edited_image
if edited_image:
size = border_size.get()
color = border_color.get()

        # Adding a border
        new_width = edited_image.width + 2 * size
        new_height = edited_image.height + 2 * size

        bordered_image = Image.new("RGB", (new_width, new_height), color)
        bordered_image.paste(edited_image, (size, size))

        edited_image = bordered_image
        display_image(edited_image)

def pick_border_color():
color = colorchooser.askcolor(title="Pick a Border Color", initialcolor=border_color.get())[1]
if color:
border_color.set(color)
Next up, I have added border to the image. The default border size is 20, and the color is Black. But what if you don’t want a black color? Yesss, you can change it. I have used colorchooser.askcolor() so when you click on the ‘Pick Color’ button it will pop up a color selection window and you can select a color of your choice!

def clear_filters():
global original_image, edited_image
edited_image = original_image.copy()
display_image(edited_image)

def save_image():
global edited_image
save_path = filedialog.asksaveasfilename(defaultextension=".png",
filetypes=[("PNG files", "*.png"),
("JPEG files", "*.jpg;*.jpeg")])
if save_path:
edited_image.save(save_path)
Not happy with the edits? I’ve added a ‘Clear Filter’ button which will erase all the edits you have made so far and restores your original image. Lastly added a ‘Save Image’ button which will save your edited image in your system!

def display_image(edited_image):
global canvas
if edited_image: # Resizing image to fit the canvas
resized_image = edited_image.resize((500, 500), Image.ANTIALIAS)
photo = ImageTk.PhotoImage(resized_image)

        # Updating canvas with the new image
        canvas.config(width=photo.width(), height=photo.height())
        canvas.create_image(0, 0, anchor=tk.NW, image=photo)
        canvas.image = photo

Wait, where are we going to see all this? The answer is canvas, the display_image() helps in displaying the image on the canvas.

Now to link these functions to their respective buttons I’ve made used of the command argument. Here’s the fully functional code:

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog, colorchooser
from PIL import Image, ImageTk, ImageFilter, ImageEnhance

def create_widgets(root, filter_var, border_size, border_color):
ttk.Button(root, text="Open Image", command=lambda: process_image(root, filter_var, border_size, border_color)).grid(row=0, column=0, padx=10, pady=10)
ttk.Label(root, text="Filter:").grid(row=0, column=1, padx=10, pady=10)
filter_options = ["Black and White", "Sepia"]
ttk.Combobox(root, textvariable=filter_var, values=filter_options).grid(row=0, column=2, padx=10, pady=10)
ttk.Button(root, text="Apply Filter", command=apply_filter).grid(row=0, column=3, padx=10, pady=10)
ttk.Label(root, text="Border Size:").grid(row=0, column=4, padx=10, pady=10)
ttk.Entry(root, textvariable=border_size).grid(row=0, column=5, padx=10, pady=10)
ttk.Label(root, text="Border Color:").grid(row=0, column=6, padx=10, pady=10)
ttk.Button(root, text="Pick Color", command=pick_border_color).grid(row=0, column=7, padx=10, pady=10)
ttk.Button(root, text="Add Border", command=add_border).grid(row=0, column=8, padx=10, pady=10)
ttk.Button(root, text="Clear All Filters", command=clear_filters).grid(row=0, column=9, padx=10, pady=10)
ttk.Button(root, text="Save Image", command=save_image).grid(row=0, column=10, padx=10, pady=10)

    canvas = tk.Canvas(root, width=500, height=500)
    canvas.grid(row=1, column=0, columnspan=11, padx=10, pady=10)

    for i in range(11):
        root.columnconfigure(i, weight=1)
    root.rowconfigure(1, weight=1)

    return canvas

def process_image(root, filter_var, border_size, border_color):
global original_image, edited_image
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.gif")])

    if file_path:
        image_path = file_path
        original_image = Image.open(image_path)
        edited_image = original_image.copy()
        display_image(edited_image)

def apply_filter():
global edited_image
selected_filter = filter_var.get()

    if edited_image and selected_filter == "Black and White":
        edited_image = ImageEnhance.Color(edited_image).enhance(0.0)
    elif edited_image and selected_filter == "Sepia":
        edited_image = apply_sepia_filter(edited_image)

    display_image(edited_image)

def apply_sepia_filter(image):
sepia = Image.new('RGB', image.size, (112, 66, 20))
sepia = Image.blend(image, sepia, 0.5)
return sepia

def add_border():
global edited_image
if edited_image:
size = border_size.get()
color = border_color.get()

        # Adding a border
        new_width = edited_image.width + 2 * size
        new_height = edited_image.height + 2 * size

        bordered_image = Image.new("RGB", (new_width, new_height), color)
        bordered_image.paste(edited_image, (size, size))

        edited_image = bordered_image
        display_image(edited_image)

def pick_border_color():
color = colorchooser.askcolor(title="Pick a Border Color", initialcolor=border_color.get())[1]
if color:
border_color.set(color)

def clear_filters():
global original_image, edited_image
edited_image = original_image.copy()
display_image(edited_image)

def save_image():
global edited_image
save_path = filedialog.asksaveasfilename(defaultextension=".png",
filetypes=[("PNG files", "*.png"),
("JPEG files", "*.jpg;*.jpeg")])
if save_path:
edited_image.save(save_path)

def display_image(edited_image):
global canvas
if edited_image: # Resizing image to fit the canvas
resized_image = edited_image.resize((500, 500), Image.ANTIALIAS)
photo = ImageTk.PhotoImage(resized_image)

        # Updating canvas with the new image
        canvas.config(width=photo.width(), height=photo.height())
        canvas.create_image(0, 0, anchor=tk.NW, image=photo)
        canvas.image = photo

root = tk.Tk()
root.geometry("1100x600")
root.title("Photo Editor")

filter_var = tk.StringVar()
border_size = tk.IntVar()
border_color = tk.StringVar()
border_size.set(20) # Default border size: 20
border_color.set("#000000") # Default border color: Black

canvas = create_widgets(root, filter_var, border_size, border_color)

original_image = None
edited_image = None

root.mainloop()

That’s how I built this very basic Photo Editor in Python.

I hope this helped you get familiar with GUI!

Thanks for reading!

Continue Learning

Discover more articles on similar topics