🐍Python Project: 🎨Drawing Pad GUI

✨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 :
    🖥 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
    📌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
    🎨 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()
    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()
  • 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
    📌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)
  • 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)
  • 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)
  • Scale: Create a horizontal scale for controlling line thickness.
  • scale = Scale(root, from_=1, to=20, orient=HORIZONTAL, command=setlinewidth)
    scale.pack(side=BOTTOM)
  • 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)
    That's enough for our GUI. Now it looks like this
    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)
    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
    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)
    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
    *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)
    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 = []
    📌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
        },
        {
            ......
            ......
        },
        # .....................
    ]
    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)
    📌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
    That's the end. Now our app is complete!
    Now Here is the final code .
    👩‍💻 Final Code
    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
    
            #Generate Element
            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()
    📃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

    20

    This website collects cookies to deliver better user experience

    🐍Python Project: 🎨Drawing Pad GUI