DEV Community

Cover image for 🐍Python GUI Project: A step-by-step guide to make Drawing Canvas app
RF Fahad Islam
RF Fahad Islam

Posted on • Edited on

🐍Python GUI Project: A step-by-step guide to make Drawing Canvas app

✨Introduction

This tutorial is not focused on advanced topics. This will teach you how to make your own simple Drawing GUI using basic Tkinter and Python concepts. Also, feel free to suggest your changes and new features for improving the program. Enhance it with your creativity by making pull requests. Let's get started!
Note: This is a simple project for beginners. So I will not go to advanced stuff like "Save as PNG, JPEG files" or "work with images".

GitHub Repo : - Drawing Pad

👀 Overview

What can it do :

  • Create a rectangle, circle, line with custom colors
  • Save the drawing in a pickle file.
  • Retrieve the drawing by opening the pickle file.

Take a look at the final project :

gui drawing pad demo.gif

🖥 Step by step: Drawing Pad Project

📌Imports and Modules

In this project, we will use only Python built-in Tkinter library for GUI building and pickle for saving drawing in a .pkl file. No external module is used in this project.



from tkinter import *
import tkinter.messagebox as tmsg 
from tkinter.filedialog import askopenfilename, asksaveasfilename #for saving files in a directory
import pickle #save draw object in pickle file
from tkinter.colorchooser import askcolor #custom color palates


Enter fullscreen mode Exit fullscreen mode

📌Defining Variables

First, we need some global variables to use within functions. So, here are all variables need for this project. Look at the variables and why its created.



# Starting point of mouse dragging or shapes
prev_x = 0 
prev_y = 0 
# Current x,y position of mouse cursor or end position of dragging
x = 0 
y = 0
created_element_info = [] #list of all shapes objects for saving drawing
new = [] # Each shapes of canvas
created = [] # Temporary list to hold info on every drag
shape = "Line" # Shape to draw
color = "blue" # Color of the shape
line_width = 3 # Width of the line shape


Enter fullscreen mode Exit fullscreen mode

🎨 Create Window and Canvas

So before logic building for our project, let's build the GUI window with Tkinter. if you don't know how to use Tkinter then read Tkinter guide for beginners.

Main Window:



# All the functions and logics go here
root = Tk()
root.title("Drawing Pad")
root.minsize(600,300) #Minimum Size of the window
# All Widgets here such as canvas, buttons etc
root.mainloop()


Enter fullscreen mode Exit fullscreen mode

Canvas Widget:

Under mainloop create the canvas

  • Specify Canvas width and height
  • Create Canvas widget and pack with root


CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400
canvas = Canvas(root, width=CANVAS_WIDTH, height=CANVAS_HEIGHT, bg="white")
canvas.pack()


Enter fullscreen mode Exit fullscreen mode
  • Bind Some Function with canvas. Note: These Functions are described in the logic section


# Binding Events to canvas
# Structure: canvas.bind("<eventcodename>", function-name)
canvas.bind("<1>", recordPosition) #On Mouse left click
canvas.bind("<B1-Motion>", drawShapesOnDragging) #Capture Mouse left click + move (dragging)
canvas.bind("<ButtonRelease-1>", generateShapesObj) #When Mouse left click release
canvas.bind("<Motion>", captureMotion) #Mouse Motion


Enter fullscreen mode Exit fullscreen mode

📌Building Design and Widgets

We will create Buttons, Frames, and Status Bar for our Drawing GUI

  • Frame: First Create a Bottom frame for the toolbar


frame = Frame(root)
frame.pack(side=BOTTOM)


Enter fullscreen mode Exit fullscreen mode
  • Radio Button: Now we will create radio buttons for selecting shapes and bind it with frame. We will create radios from the shapes list then manipulate them. Here the command is for running the function by its name. Don't worry we will create the function soon.


radiovalue = StringVar()
geometry_shapes = ["Line", "Rectangle", "Arc", "Oval"]
radiovalue.set("Line") #Default Select

# Manupulates Radios from the list
for shape in geometry_shapes:
    radio = Radiobutton(frame, text=shape, variable=radiovalue, font="comicsans     12 bold", value=shape, command=shapechanger).pack(side=LEFT, padx=6,pady=3)


Enter fullscreen mode Exit fullscreen mode
  • Button: Then create buttons for basic actions. These buttons command contains functions name that should be executed when the buttons click.


Button(frame, text="Save", font="comicsans 12 bold",
       command=saveDrawingFile).pack(side=BOTTOM, padx=6, pady=6)
Button(frame, text="Clear", font="comicsans 12 bold",
       command=clearCanvas).pack(side=BOTTOM, padx=6)
Button(frame, text="Color", font="comicsans 12 bold",
       command=colorPicker).pack(side=BOTTOM, padx=6)
Button(frame, text="Get", font="comicsans 12 bold",
       command=getsavedrawing).pack(side=BOTTOM, padx=6)


Enter fullscreen mode Exit fullscreen mode
  • Scale: Create a horizontal scale for controlling line thickness.


scale = Scale(root, from_=1, to=20, orient=HORIZONTAL, command=setlinewidth)
scale.pack(side=BOTTOM)


Enter fullscreen mode Exit fullscreen mode
  • Status Bar: Create status bar to show x,y position of the mouse cursor


status = StringVar()
status.set("Position : x - 0 , y - 0")
statusbar = Label(root, textvariable=status, anchor="w", relief=SUNKEN)
statusbar.pack(side=BOTTOM, fill=X)


Enter fullscreen mode Exit fullscreen mode

That's enough for our GUI. Now it looks like this

image.png

It is not working now. So lets created our app logic.


🧠App Logic

Our design is completed. Now we will create some functions to make it work! These function should be placed outside the mainloop.

Let's divide the app logic and problems to solve them easily

Divide Problems:

  • Create different shapes according to user radio selection
  • Draw the shapes and update the shape size dynamically on dragging
  • Delete all the shapes that will be created while dragging except the final shape
  • Create a list of dictonaries to store data of every shapes containning the detail of shape position, color etc.
  • Then save the list to pickle file and finally retrieve the data from the file and draw them on the canvas.

📌Function to Update Values :



#Capture Motions on every mouse position change
def captureMotion(e=""):
    #Update Status Bar
    status.set(f"Position : x - {e.x} , y - {e.y}")
    statusbar.update()

# Update the previous position on mouse left click
def recordPosition(e=""):
    global prev_x
    global prev_y
    prev_x = e.x
    prev_y = e.y

# Color Picker for color button
def colorPicker(e=""):
    global color
    color = askcolor(color=color)[1]
    #Set the color of shapes
    root.config(cursor=f'cursor {color} {color}', insertbackground=f"{color}")

# Update the current shape
def shapechanger(e=""):
    global shape
    shape = radiovalue.get() #selected radio value

# Runs On scale value change and update line width
def setlinewidth(e=""):
    global line_width
    line_width = scale.get()
    # Save the drawing on a file

# After Every drawing create info of drawing and add the element to new list and assign empty list to created
def generateShapesObj(e=""):
    global created,created_element_info
    new.append(created[-1])
    created = []
    created_element_info_obj = {
        "type": shape,
        "color": color,
        "prev_x": prev_x,
        "prev_y": prev_y,
        "x": x,
        "y": y
    }
    created_element_info.append(created_element_info_obj)


Enter fullscreen mode Exit fullscreen mode

Above functions are important for our generating shape logic

📌Draw Shapes on Canvas

This is the most important part of this project. First we will create some functions to update global variables that is defined before.

1. Create shape functions

Note: This function uses the global variables to know what shape, color, and where to draw.

This createElms() function will run by the drawShapesOnDragging() function on every mouse click with dragging.



# Create Elements on canvas based on shape variable
def createElms():
    if shape == "Rectangle":
        a = canvas.create_rectangle(prev_x, prev_y, x, y, fill=color)
    elif shape == "Oval":
        a = canvas.create_oval(prev_x, prev_y, x, y, fill=color)
    elif shape == "Arc":
        a = canvas.create_arc(prev_x, prev_y, x, y, fill=color)
    elif shape == "Line":
        a = canvas.create_line(prev_x, prev_y, x, y,
                               width=line_width, fill=color,
                               capstyle=ROUND, smooth=TRUE, splinesteps=3)
    return a


Enter fullscreen mode Exit fullscreen mode

Explain:
We want three shapes such as Rectangle, Oval, Arc, and Line for this project.

  • First, we use the "shape" and "color" variables to create responding shapes to the canvas. I am not going to explain the canvas shape creation rather than I want to tell the logic behind it.

2. Generate shapes on mouse dragging

For generating shapes on mouse dragging, grab x,y positions on every mouse cursor position change. Then create shapes on every change, keep the new shape and delete other shapes.

Note: Here we want to keep starting point of shape still on drag start event and the end point means x,y position will change dynamically on dragging.



# Create shapes on mouse dragging and resize and show the shapes on the canvas
def drawShapesOnDragging(e=""):
    global x,y
    try:
        # Update current Position
        x = e.x
        y = e.y

        #Generate Element
        element = createElms()
        deleteUnwanted(element) # Delete unwanted shapes
    except Exception as e:
        tmsg.showerror("Some Error Occurred!", e)


Enter fullscreen mode Exit fullscreen mode

Problem: Without deleteUnwanted() it will work like this. But We don't want these unwanted shapes on x,y position change of mouse cursor. This will create a lot of shapes on every position change of the mouse cursor. So we need to keep one element at the end. So we will delete other shapes from the canvas and keep the new one

gui firt stage.gif

*Solution: * So add the function to solve this problem.



def deleteUnwanted(element):
    global created
    created.append(element) #Elements that created
    for item in created[:-1]: 
        canvas.delete(item)


Enter fullscreen mode Exit fullscreen mode

gui final.gif

Now it is dynamic and change its endpoint on dragging but not the starting point.

📌Clear Canvas

We make a button before for clearing th canvas but we don't write the function for the button clearCanvas().



# Clear the canvas
def clearCanvas(e=""):
    global created_element_info, canvas, created, new
    canvas.delete("all")
    created_element_info = []
    created = []
    new = []


Enter fullscreen mode Exit fullscreen mode

📌Saving Drawing in Pickle file

To save the drawing, we need to save the generated object list of shapes info and write it on the pickle file.
It creates a list of drawing information dictionary such as below (given the structure of the data):



# This is not for the GUI. It's just a structure of the generated data.
created_element_info = [
    {
        "type": shape, #Shape of drawing like line, circle, rectangle
        "color": color, #Color of the shape
        "prev_x": prev_x, #Starting point from the x axis
        "prev_y": prev_y, #Starting point from the y axis
        "x": x, #End point of the x axis
        "y": y #End point of the y axis
    },
    {
        ......
        ......
    },
    # .....................
]


Enter fullscreen mode Exit fullscreen mode

Save the list to pickle file:



# Save the list of shapes objects on a pickle file
def saveDrawingFile(e=""):
    global created_element_info
    filename = asksaveasfilename(initialfile="drawing",defaultextension=".pkl",filetypes=[("Pickle Files", "*.pkl")]) #Save as
    if filename != None: 
        with open(filename, "wb") as f:
            pickle.dump(created_element_info, f)


Enter fullscreen mode Exit fullscreen mode

📌Retrieve Drawing from File

Retrieve the data from the pickle file and then draw the shapes from the info



def getsavedrawing():
    global x, y, prev_x, prev_y, shape, color
    filename = askopenfilename(defaultextension=".pkl", filetypes = [("Pickle Files", "*.pkl")])
    if filename != None:
        with open(filename, "rb") as f:
            data = pickle.load(f)
            for draw_info in data:
                x = draw_info["x"]
                y = draw_info["y"]
                prev_x = draw_info["prev_x"]
                prev_y = draw_info["prev_y"]
                shape = draw_info["type"]
                color = draw_info["color"]
                createElms() #Draw each shapes


Enter fullscreen mode Exit fullscreen mode

get.gif

That's the end. Now our app is complete!

Now Here is the final code .

👩‍💻 Final Code

Github Repo : - https://github.com/RF-Fahad-Islam/Drawing-Pad



from tkinter import *
import tkinter.messagebox as tmsg
from tkinter.filedialog import askopenfilename, asksaveasfilename #for saving files in a directory
import pickle #save draw object in pickle file
from tkinter.colorchooser import askcolor #custom color palates

# Starting point of mouse dragging or shapes
prev_x = 0
prev_y = 0
# Current x,y position of mouse cursor or end position of dragging
x = 0
y = 0
created_element_info = [] #list of all shapes objects for saving drawing
new = [] # Each shapes of canvas
created = [] # Temporary list to hold info on every drag
shape = "Line" # Shape to draw
color = "blue" # Color of the shape
line_width = 3 # Width of the line shape

# All the functions and logics go here

Capture Motions on every mouse position change

def captureMotion(e=""):
#Update Status Bar
status.set(f"Position : x - {e.x} , y - {e.y}")
statusbar.update()

# Update the previous position on mouse left click
def recordPosition(e=""):
global prev_x
global prev_y
prev_x = e.x
prev_y = e.y

# Color Picker
def colorPicker(e=""):
global color
color = askcolor(color=color)[1]
#Set the color of shapes
root.config(cursor=f'cursor {color} {color}', insertbackground=f"{color}")

# Update the current shape
def shapechanger(e=""):
global shape
shape = radiovalue.get() #selected radio value

# Runs On scale value change and update line width
def setlinewidth(e=""):
global line_width
line_width = scale.get()
# Save the drawing on a file

# After Every drawing create info of drawing and add the element to new list and assign empty list to created
def generateShapesObj(e=""):
global created,created_element_info
new.append(created[-1])
created = []
created_element_info_obj = {
"type": shape,
"color": color,
"prev_x": prev_x,
"prev_y": prev_y,
"x": x,
"y": y
}
created_element_info.append(created_element_info_obj)

# Create Elements on canvas based on shape variable
def createElms():
if shape == "Rectangle":
a = canvas.create_rectangle(prev_x, prev_y, x, y, fill=color)
elif shape == "Oval":
a = canvas.create_oval(prev_x, prev_y, x, y, fill=color)
elif shape == "Arc":
a = canvas.create_arc(prev_x, prev_y, x, y, fill=color)
elif shape == "Line":
a = canvas.create_line(prev_x, prev_y, x, y,
width=line_width, fill=color,
capstyle=ROUND, smooth=TRUE, splinesteps=3)
return a

# Create shapes on mouse dragging and resize and show the shapes on the canvas
def drawShapesOnDragging(e=""):
global x,y
try:
# Update current Position
x = e.x
y = e.y

    <span class="c1">#Generate Element
Enter fullscreen mode Exit fullscreen mode

element = createElms()
deleteUnwanted(element) # Delete unwanted shapes
except Exception as e:
tmsg.showerror("Some Error Occurred!", e)

def deleteUnwanted(element):
global created
created.append(element) #Elements that created
for item in created[:-1]:
canvas.delete(item)

# Save the list of shapes objects on a pickle file
def saveDrawingFile(e=""):
global created_element_info
filename = asksaveasfilename(initialfile="drawing",defaultextension=".pkl",filetypes=[("Pickle Files", "*.pkl")]) #Save as
if filename != None:
with open(filename, "wb") as f:
pickle.dump(created_element_info, f)

def getsavedrawing():
global x, y, prev_x, prev_y, shape, color
filename = askopenfilename(defaultextension=".pkl", filetypes = [("Pickle Files", "*.pkl")])
if filename != None:
with open(filename, "rb") as f:
data = pickle.load(f)
for draw_info in data:
x = draw_info["x"]
y = draw_info["y"]
prev_x = draw_info["prev_x"]
prev_y = draw_info["prev_y"]
shape = draw_info["type"]
color = draw_info["color"]
createElms() #Draw each shapes

# Clear the Canvas
def clearCanvas(e=""):
global created_element_info, canvas, created, new
canvas.delete("all")
created_element_info = []
created = []
new = []

root = Tk()
root.title("Drawing Pad")
root.minsize(600,300) #Minimum Size of the window

All Widgets here such as canvas, buttons etc


# Canvas
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400
canvas = Canvas(root, width=CANVAS_WIDTH, height=CANVAS_HEIGHT, bg="white")
canvas.pack()

# Binding Events to canvas

Structure: canvas.bind("<eventcodename>", function-name)

canvas.bind("<1>", recordPosition) #On Mouse left click
canvas.bind("<B1-Motion>", drawShapesOnDragging) #Capture Mouse left click + move (dragging)
canvas.bind("<ButtonRelease-1>", generateShapesObj) #When Mouse left click release
canvas.bind("<Motion>", captureMotion) #Mouse Motion
frame = Frame(root)
frame.pack(side=BOTTOM)
radiovalue = StringVar()
geometry_shapes = ["Line", "Rectangle", "Arc", "Oval"]
radiovalue.set("Line") #Default Select

# Manupulates Radios from the list
for shape in geometry_shapes:
radio = Radiobutton(frame, text=shape, variable=radiovalue, font="comicsans 12 bold", value=shape, command=shapechanger).pack(side=LEFT, padx=6,pady=3)

#Buttons
Button(frame, text="Save", font="comicsans 12 bold",
command=saveDrawingFile).pack(side=BOTTOM, padx=6, pady=6)
Button(frame, text="Clear", font="comicsans 12 bold",
command=clearCanvas).pack(side=BOTTOM, padx=6)
Button(frame, text="Color", font="comicsans 12 bold",
command=colorPicker).pack(side=BOTTOM, padx=6)
Button(frame, text="Get", font="comicsans 12 bold",
command=getsavedrawing).pack(side=BOTTOM, padx=6)

# Scale
scale = Scale(root, from_=1, to=20, orient=HORIZONTAL, command=setlinewidth)
scale.pack(side=BOTTOM)

# Status Bar
status = StringVar()
status.set("Position : x - 0 , y - 0")
statusbar = Label(root, textvariable=status, anchor="w", relief=SUNKEN)
statusbar.pack(side=BOTTOM, fill=X)
root.mainloop()

Enter fullscreen mode Exit fullscreen mode




📃Conclusion

Finally, Our project is finished. If you find some bugs, you can solve the bug and pull it to the GitHub. If you find this blog helpful then comment down and show your improvements to my GitHub make pull requests. You are welcome to make changes to this project. See you in the future. Happy Coding!

Github Profile : RF Fahad Islam
Instagram Profile: RF Fahad Islam
Hashnode Blog: My Hashnode Blog

Top comments (0)