Python Tkinter Circular Menu Source Code

How to Create an Animated Circular Menu in Python Tkinter

How to Create an Animated Circular Menu in Python Tkinter




In this Python Tutorial we will see How to Create an animated circular menu Using Python Tkinter.

What We Are Gonna Use In This Project:

- Python Programming Language.
- Tkinter (GUI).
- VS Editor.






Project Source Code:




import tkinter as tk
import math
import time


class CircularMenu(tk.Frame):
"""
A circular menu widget that displays menu items in a circle around a central button.
When clicked, the menu expands/collapses with smooth animation.
"""

def __init__(self, parent):
"""
Initialize the circular menu.
Args - parent: The parent widget
"""
super().__init__(parent) # Initialize the parent Frame class
# === CONFIGURATION CONSTANTS ===
# These control the size and appearance of the menu
self.MENU_RADIUS = 150 # How far menu items spread from center
self.CENTER_BUTTON_SIZE = 90 # Size of the main center button
self.ITEM_BUTTON_SIZE = 70 # Size of each menu item button
self.ANIMATION_DURATION = 400 # Animation time in milliseconds
# === COLOR SCHEME ===
# Define all colors used in the interface
self.PRIMARY_COLOR = "#2ecc71" # Green for menu items
self.ACCENT_COLOR = "#f39c12" # Orange for center button
self.HOVER_COLOR = "#3498db" # Blue when hovering over items
self.TEXT_COLOR = "#ecf0f1" # Light gray for text
self.CONTENT_BG = "#34495e" # Dark gray background
self.SHADOW_COLOR = "#2c3e50" # Darker gray for shadows
# === STATE VARIABLES ===
# These track the current state of the menu
self.is_expanded = False # Is the menu currently open?
self.animating = False # Is an animation currently running?
self.menu_buttons = [] # List to store all menu button objects
self.active_item = None # Which menu item is currently selected?
# Start building the user interface
self.setup_ui()
def setup_ui(self):
"""
Create all the visual elements of the menu.
This is called once when the menu is first created.
"""
# Set the background color of this widget
self.configure(bg=self.CONTENT_BG)
# Create a canvas - this is like a drawing surface where we can place shapes
self.canvas = tk.Canvas(
self, # Put the canvas inside this widget
bg=self.CONTENT_BG, # Same background color
highlightthickness=0, # No border highlight
bd=0 # No border
)
# Make the canvas fill the entire widget
self.canvas.pack(fill="both", expand=True)
# === DEFINE MENU ITEMS ===
# Each item has a name and a function that draws its icon
self.menu_items = [
("Dashboard", self.create_dashboard_icon), # Home/dashboard page
("Menu", self.create_menu_icon), # Menu management
("Orders", self.create_orders_icon), # Order tracking
("Customers", self.create_customers_icon), # Customer management
("Analytics", self.create_analytics_icon), # Data and reports
("Settings", self.create_settings_icon) # App settings
]
# === EVENT BINDING ===
# When the canvas size changes, update the positions of all elements
self.canvas.bind("<Configure>", self.on_canvas_configure)
# Create all the visual elements (buttons, icons, etc.)
self.create_menu_elements()
def on_canvas_configure(self, event):
"""
Called whenever the canvas is resized.
This ensures the menu stays centered when the window is resized.
Args:
event: Contains information about the resize event
"""
# Calculate the center point of the canvas
self.canvas_center_x = event.width // 2 # // means integer division
self.canvas_center_y = event.height // 2
# Move all elements to their correct positions
self.update_positions()
def create_menu_elements(self):
"""
Create all the visual elements that make up the menu.
This includes the center button, menu items, shadows, and labels.
"""
# === CREATE CENTER BUTTON ===
# First create a shadow behind the center button for depth
self.center_shadow = self.canvas.create_oval(
0, 0, self.CENTER_BUTTON_SIZE + 4, self.CENTER_BUTTON_SIZE + 4,
fill=self.SHADOW_COLOR, # Dark color for shadow
outline="", # No border
stipple="gray50" # Make it slightly transparent
)
# Create the main center button (a circle)
self.center_button = self.canvas.create_oval(
0, 0, self.CENTER_BUTTON_SIZE, self.CENTER_BUTTON_SIZE,
fill=self.ACCENT_COLOR, # Orange color
outline="", # No border
width=0 # No border width
)
# Add the hamburger menu icon (≡) to the center button
self.center_icon = self.canvas.create_text(
self.CENTER_BUTTON_SIZE // 2, self.CENTER_BUTTON_SIZE // 2,
text="☰", # Hamburger menu symbol
font=("Arial", 32, "bold"), # Large, bold font
fill=self.TEXT_COLOR # Light color for contrast
)
# === MAKE CENTER BUTTON INTERACTIVE ===
# Bind mouse events to both the button circle and the icon
# Click events (when user clicks the button)
self.canvas.tag_bind(self.center_button, "<Button-1>", self.toggle_menu)
self.canvas.tag_bind(self.center_icon, "<Button-1>", self.toggle_menu)
# Hover events (when mouse enters/leaves the button)
self.canvas.tag_bind(self.center_button, "<Enter>", self.on_center_hover)
self.canvas.tag_bind(self.center_button, "<Leave>", self.on_center_leave)
self.canvas.tag_bind(self.center_icon, "<Enter>", self.on_center_hover)
self.canvas.tag_bind(self.center_icon, "<Leave>", self.on_center_leave)
# === CREATE MENU ITEMS ===
# Loop through each menu item and create its visual elements
for i, (text, icon_func) in enumerate(self.menu_items):
# Create shadow for this menu item
shadow = self.canvas.create_oval(
0, 0, self.ITEM_BUTTON_SIZE + 3, self.ITEM_BUTTON_SIZE + 3,
fill=self.SHADOW_COLOR,
outline="",
stipple="gray50",
state='hidden' # Hidden initially (menu starts closed)
)
# Create the button circle for this menu item
button = self.canvas.create_oval(
0, 0, self.ITEM_BUTTON_SIZE, self.ITEM_BUTTON_SIZE,
fill=self.PRIMARY_COLOR, # Green color
outline="",
width=0,
state='hidden' # Hidden initially
)
# Create a small canvas to draw the icon on
icon_canvas = tk.Canvas(
self.canvas,
width=32, height=32, # Small square canvas
bg=self.PRIMARY_COLOR, # Same color as button
highlightthickness=0,
bd=0
)
# Draw the icon using the provided function
icon_func(icon_canvas, self.TEXT_COLOR)
# Place the icon canvas on the main canvas
icon_window = self.canvas.create_window(
0, 0, window=icon_canvas, state='hidden')
# Create text label for this menu item
label = self.canvas.create_text(
0, 0,
text=text, # The menu item name
font=("Arial", 12, "bold"),
fill=self.TEXT_COLOR,
state='hidden' # Hidden initially
)
# Store all parts of this menu item in a dictionary
# This makes it easy to manage all the pieces together
menu_item_data = {
'shadow': shadow, # The shadow behind the button
'button': button, # The main button circle
'icon': icon_window, # The icon canvas
'icon_canvas': icon_canvas, # Direct reference to canvas
'icon_func': icon_func, # Function to redraw icon
'label': label, # The text label
'text': text, # The menu item name
'index': i # Position in the list
}
self.menu_buttons.append(menu_item_data)
# === MAKE MENU ITEM INTERACTIVE ===
# Bind mouse events to make the button clickable and hoverable
# Click events
self.canvas.tag_bind(button, "<Button-1>",
lambda e, idx=i: self.on_item_click(idx))
# Hover events for the button
self.canvas.tag_bind(button, "<Enter>",
lambda e, idx=i: self.on_item_hover(idx))
self.canvas.tag_bind(button, "<Leave>",
lambda e, idx=i: self.on_item_leave(idx))
# Also bind events to the icon canvas
icon_canvas.bind("<Button-1>", lambda e, idx=i: self.on_item_click(idx))
icon_canvas.bind("<Enter>", lambda e, idx=i: self.on_item_hover(idx))
icon_canvas.bind("<Leave>", lambda e, idx=i: self.on_item_leave(idx))
# === CREATE CONTENT DISPLAY ===
# This text shows instructions and selected item information
self.content_label = self.canvas.create_text(
0, 0,
text="Click the center button to open menu",
font=("Arial", 18, "bold"),
fill=self.TEXT_COLOR
)
def update_positions(self):
"""
Calculate and update the positions of all menu elements.
This is called when the window is resized or during animations.
"""
# Make sure we have center coordinates calculated
if not hasattr(self, 'canvas_center_x'):
return
center_x = self.canvas_center_x
center_y = self.canvas_center_y
# === POSITION CENTER BUTTON AND SHADOW ===
shadow_offset = 2 # How far to offset the shadow
# Position the shadow slightly down and to the right
self.canvas.coords(
self.center_shadow,
center_x - self.CENTER_BUTTON_SIZE // 2 + shadow_offset,
center_y - self.CENTER_BUTTON_SIZE // 2 + shadow_offset * 2,
center_x + self.CENTER_BUTTON_SIZE // 2 + shadow_offset,
center_y + self.CENTER_BUTTON_SIZE // 2 + shadow_offset * 2
)
# Position the center button exactly in the center
self.canvas.coords(
self.center_button,
center_x - self.CENTER_BUTTON_SIZE // 2,
center_y - self.CENTER_BUTTON_SIZE // 2,
center_x + self.CENTER_BUTTON_SIZE // 2,
center_y + self.CENTER_BUTTON_SIZE // 2
)
# Position the icon in the center of the button
self.canvas.coords(self.center_icon, center_x, center_y)
# === POSITION MENU ITEMS (if menu is expanded) ===
if self.is_expanded:
for i, item in enumerate(self.menu_buttons):
# Calculate angle for this menu item
# We divide 360 degrees by the number of items to space them evenly
angle_degrees = i * 360 / len(self.menu_buttons)
angle_radians = angle_degrees * math.pi / 180 # Convert to radians
# Calculate x,y position using trigonometry
# cos gives us the x-component, sin gives us the y-component
item_x = center_x + self.MENU_RADIUS * math.cos(angle_radians)
item_y = center_y + self.MENU_RADIUS * math.sin(angle_radians)
# Position the shadow
shadow_offset = 2
self.canvas.coords(
item['shadow'],
item_x - self.ITEM_BUTTON_SIZE // 2 + shadow_offset,
item_y - self.ITEM_BUTTON_SIZE // 2 + shadow_offset * 2,
item_x + self.ITEM_BUTTON_SIZE // 2 + shadow_offset,
item_y + self.ITEM_BUTTON_SIZE // 2 + shadow_offset * 2
)
# Position the button
self.canvas.coords(
item['button'],
item_x - self.ITEM_BUTTON_SIZE // 2,
item_y - self.ITEM_BUTTON_SIZE // 2,
item_x + self.ITEM_BUTTON_SIZE // 2,
item_y + self.ITEM_BUTTON_SIZE // 2
)
# Position the icon in the center of the button
self.canvas.coords(item['icon'], item_x, item_y)
# Position the label below the button
label_y = item_y + self.ITEM_BUTTON_SIZE // 2 + 22
self.canvas.coords(item['label'], item_x, label_y)
# === POSITION CONTENT LABEL ===
# Place it below the menu area
content_y = center_y + 250
self.canvas.coords(self.content_label, center_x, content_y)


def toggle_menu(self, event=None):
"""
Open the menu if it's closed, or close it if it's open.
This is called when the center button is clicked.
Args:
event: Mouse click event (can be None)
"""
# Don't do anything if an animation is already running
if self.animating:
return
# Start the animation
self.animating = True
self.animate_menu()


def animate_menu(self):
"""
Smoothly animate the menu opening or closing.
This creates a smooth expanding/contracting effect.
"""
start_time = time.time() # Record when animation started
# Determine start and end positions for the animation
start_radius = self.MENU_RADIUS if self.is_expanded else 0
target_radius = 0 if self.is_expanded else self.MENU_RADIUS
# If we're expanding (opening) the menu, show all items
if not self.is_expanded:
for item in self.menu_buttons:
self.canvas.itemconfig(item['shadow'], state='normal')
self.canvas.itemconfig(item['button'], state='normal')
self.canvas.itemconfig(item['icon'], state='normal')
self.canvas.itemconfig(item['label'], state='normal')
def animate_step():
"""
This function is called repeatedly to create smooth animation.
It calculates the current frame of the animation.
"""
# Calculate how much time has passed
elapsed = time.time() - start_time
# Calculate progress as a value between 0 and 1
progress = min(elapsed / (self.ANIMATION_DURATION / 1000), 1.0)
# Apply easing function to make animation feel more natural
eased_progress = self.ease_out_cubic(progress)
# Calculate current radius based on progress
current_radius = start_radius + (
target_radius - start_radius) * eased_progress
center_x = self.canvas_center_x
center_y = self.canvas_center_y
# Update position of each menu item
for i, item in enumerate(self.menu_buttons):
# Calculate angle and position for this item
angle = (i * 360 / len(self.menu_buttons)) * math.pi / 180
item_x = center_x + current_radius * math.cos(angle)
item_y = center_y + current_radius * math.sin(angle)
# Update shadow position
shadow_offset = 2
self.canvas.coords(
item['shadow'],
item_x - self.ITEM_BUTTON_SIZE // 2 + shadow_offset,
item_y - self.ITEM_BUTTON_SIZE // 2 + shadow_offset * 2,
item_x + self.ITEM_BUTTON_SIZE // 2 + shadow_offset,
item_y + self.ITEM_BUTTON_SIZE // 2 + shadow_offset * 2
)
# Update button position
self.canvas.coords(
item['button'],
item_x - self.ITEM_BUTTON_SIZE // 2,
item_y - self.ITEM_BUTTON_SIZE // 2,
item_x + self.ITEM_BUTTON_SIZE // 2,
item_y + self.ITEM_BUTTON_SIZE // 2
)
# Update icon position
self.canvas.coords(item['icon'], item_x, item_y)
# Update label position (below the button)
label_y = item_y + self.ITEM_BUTTON_SIZE // 2 + 22
self.canvas.coords(item['label'], item_x, label_y)
# Check if animation is complete
if progress >= 1.0:
# Animation finished - update state
self.is_expanded = not self.is_expanded
self.animating = False
# If we just closed the menu, hide all items
if not self.is_expanded:
for item in self.menu_buttons:
self.canvas.itemconfig(item['shadow'], state='hidden')
self.canvas.itemconfig(item['button'], state='hidden')
self.canvas.itemconfig(item['icon'], state='hidden')
self.canvas.itemconfig(item['label'], state='hidden')
# Update the center button icon
# Show X when expanded, hamburger when collapsed
self.canvas.itemconfig(
self.center_icon,
text="✕" if self.is_expanded else "☰"
)
else:
# Animation not finished - schedule next frame
# 16ms = roughly 60 FPS (1000ms / 60 frames = 16.67ms)
self.after(16, animate_step)
# Start the animation
animate_step()


def ease_out_cubic(self, x):
"""
Apply cubic easing to make animations feel more natural.
Args:
x: Progress value between 0 and 1
Returns:
Eased progress value between 0 and 1
"""
return 1 - math.pow(1 - x, 3)
def on_center_hover(self, event):
"""
Called when mouse hovers over the center button.
Changes the button color to indicate it's interactive.
"""
self.canvas.itemconfig(self.center_button, fill=self.HOVER_COLOR)
def on_center_leave(self, event):
"""
Called when mouse leaves the center button.
Restores the original button color.
"""
self.canvas.itemconfig(self.center_button, fill=self.ACCENT_COLOR)

def on_item_click(self, index):
"""
Called when a menu item is clicked.
Sets this item as active and updates the display.
Args:
index: Which menu item was clicked (0, 1, 2, etc.)
"""
item = self.menu_buttons[index]
self.set_active_item(index)
# Update the content display to show which item was selected
content_text = f"Selected: {item['text']}"
self.canvas.itemconfig(self.content_label, text=content_text)

def on_item_hover(self, index):
"""
Called when mouse hovers over a menu item.
Changes the item's appearance to indicate it's interactive.
Args:
index: Which menu item is being hovered over
"""
# Don't change appearance if this item is already active
if self.active_item == index:
return
item = self.menu_buttons[index]
hover_color = self.HOVER_COLOR
# Change button color
self.canvas.itemconfig(item['button'], fill=hover_color)
# Change icon background color
item['icon_canvas'].configure(bg=hover_color)
# Redraw the icon with white color for better contrast
item['icon_canvas'].delete("all")
item['icon_func'](item['icon_canvas'], "#ffffff")




def on_item_leave(self, index):
"""
Called when mouse leaves a menu item.
Restores the item's original appearance.
Args:
index: Which menu item the mouse left
"""
# Don't change appearance if this item is active
if self.active_item == index:
return
item = self.menu_buttons[index]
# Restore original colors
self.canvas.itemconfig(item['button'], fill=self.PRIMARY_COLOR)
item['icon_canvas'].configure(bg=self.PRIMARY_COLOR)
# Redraw icon with original color
item['icon_canvas'].delete("all")
item['icon_func'](item['icon_canvas'], self.TEXT_COLOR)

def set_active_item(self, index):
"""
Mark a specific menu item as active (selected).
This changes its appearance to show it's the current selection.
Args:
index: Which menu item to make active
"""
# Reset the previously active item to normal appearance
if self.active_item is not None:
old_item = self.menu_buttons[self.active_item]
self.canvas.itemconfig(old_item['button'], fill=self.PRIMARY_COLOR)
old_item['icon_canvas'].configure(bg=self.PRIMARY_COLOR)
old_item['icon_canvas'].delete("all")
old_item['icon_func'](old_item['icon_canvas'], self.TEXT_COLOR)
# Set the new active item with special appearance
item = self.menu_buttons[index]
self.canvas.itemconfig(item['button'], fill=self.ACCENT_COLOR) # Orange color
item['icon_canvas'].configure(bg=self.ACCENT_COLOR)
item['icon_canvas'].delete("all")
item['icon_func'](item['icon_canvas'], "#ffffff") # White icon
# Remember which item is active
self.active_item = index


# === ICON DRAWING FUNCTIONS ===
# Each function draws a different icon on a small canvas
def create_dashboard_icon(self, canvas, color):
"""
Draw a house icon representing the dashboard/home page.
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
# Draw the main body of the house
canvas.create_rectangle(8, 18, 24, 28, fill=color, outline="")
# Draw the triangular roof
canvas.create_polygon(6, 18, 16, 8, 26, 18, fill=color, outline="")
# Draw the door (using background color to "cut out" the shape)
canvas.create_rectangle(12, 22, 16, 28, fill=canvas['bg'], outline="")
# Draw windows (also using background color)
canvas.create_rectangle(9, 20, 11, 22, fill=canvas['bg'], outline="")
canvas.create_rectangle(21, 20, 23, 22, fill=canvas['bg'], outline="")
# Draw chimney
canvas.create_rectangle(20, 10, 22, 16, fill=color, outline="")

def create_menu_icon(self, canvas, color):
"""
Draw a hamburger menu icon (three horizontal lines).
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
line_width = 3
# Draw three horizontal lines with rounded ends
canvas.create_line(6, 10, 26, 10, fill=color,
width=line_width, capstyle=tk.ROUND)
canvas.create_line(6, 16, 26, 16, fill=color,
width=line_width, capstyle=tk.ROUND)
canvas.create_line(6, 22, 26, 22, fill=color,
width=line_width, capstyle=tk.ROUND)

def create_orders_icon(self, canvas, color):
"""
Draw a clipboard icon with checkmarks representing orders.
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
# Draw clipboard base
canvas.create_rectangle(7, 6, 25, 28, fill=color, outline="")
canvas.create_rectangle(9, 8, 23, 26, fill=canvas['bg'], outline="")
# Draw the clip at the top
canvas.create_rectangle(13, 4, 19, 9, fill=color, outline="")
canvas.create_rectangle(14, 5, 18, 8, fill=canvas['bg'], outline="")
# Draw checklist items (circles and lines)
# Item 1
canvas.create_oval(11, 12, 13, 14, fill=color, outline="")
canvas.create_line(15, 13, 21, 13, fill=color, width=2, capstyle="round")
# Item 2
canvas.create_oval(11, 16, 13, 18, fill=color, outline="")
canvas.create_line(15, 17, 21, 17, fill=color, width=2, capstyle="round")
# Item 3
canvas.create_oval(11, 20, 13, 22, fill=color, outline="")
canvas.create_line(15, 21, 21, 21, fill=color, width=2, capstyle="round")

def create_customers_icon(self, canvas, color):
"""
Draw a person icon representing customers.
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
# Draw the head (circle)
canvas.create_oval(12, 6, 20, 14, fill=color, outline="")
# Draw the body/shoulders (semi-circle)
canvas.create_arc(8, 16, 24, 30, start=0, extent=180,
fill=color, outline="", style="pieslice")
# Draw neck connector
canvas.create_rectangle(15, 14, 17, 18, fill=color, outline="")

def create_analytics_icon(self, canvas, color):
"""
Draw a bar chart icon representing analytics/data.
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
# Draw bars of different heights to look like a chart
canvas.create_rectangle(6, 20, 9, 26, fill=color, outline="") # Short bar
canvas.create_rectangle(10, 16, 13, 26, fill=color, outline="") # Medium bar
canvas.create_rectangle(14, 12, 17, 26, fill=color, outline="") # Tall bar
canvas.create_rectangle(18, 18, 21, 26, fill=color, outline="") # Medium bar
canvas.create_rectangle(22, 8, 25, 26, fill=color, outline="") # Tallest bar
# Draw base line for the chart
canvas.create_line(5, 27, 27, 27, fill=color, width=2, capstyle=tk.ROUND)

def create_settings_icon(self, canvas, color):
"""
Draw a gear/cog icon representing settings.
Args:
canvas: Small canvas to draw on
color: Color to use for drawing
"""
center_x, center_y = 16, 16
# Outer gear teeth
for i in range(8):
angle = i * 45
rad = math.radians(angle)
# Outer tooth
inner_x = center_x + 7 * math.cos(rad)
inner_y = center_y + 7 * math.sin(rad)
outer_x = center_x + 11 * math.cos(rad)
outer_y = center_y + 11 * math.sin(rad)
# Create tooth as small rectangle
tooth_size = 1.5
canvas.create_rectangle(
outer_x - tooth_size, outer_y - tooth_size,
outer_x + tooth_size, outer_y + tooth_size,
fill=color, outline=""
)
# Main gear body
canvas.create_oval(9, 9, 23, 23, fill=color, outline="")
# Inner hole
canvas.create_oval(13, 13, 19, 19, fill=canvas['bg'], outline="")
# Center dot
canvas.create_oval(15, 15, 17, 17, fill=color, outline="")


class ModernApp(tk.Tk):
"""
The main application window that contains our circular menu.
This class creates the window and sets up the overall application.
"""
def __init__(self):
"""
Initialize the main application window.
"""
super().__init__() # Initialize the parent Tk class
# === WINDOW SETUP ===
self.title("Circular Menu") # Set window title
self.geometry("1000x800") # Set window size
self.configure(bg="#34495e") # Set window background color
# === CREATE AND DISPLAY THE MENU ===
# Create our circular menu widget
menu = CircularMenu(self)
# Pack it to fill the entire window with some padding
menu.pack(fill="both", expand=True, padx=20, pady=20)
# Center the window on the screen
self.center_window()
def center_window(self):
"""
Position the application window in the center of the screen.
"""
# Update the window to get accurate size measurements
self.update_idletasks()
# Get the window's current size
width = self.winfo_width()
height = self.winfo_height()
# Calculate center position based on screen size
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
# Move the window to the calculated position
self.geometry(f'+{x}+{y}')




# === RUN THE APPLICATION ===
if __name__ == "__main__":
app = ModernApp()
app.mainloop()



The Final Result:

Python Tkinter Circular Menu Source Code 1

Python Tkinter Circular Menu Source Code 2

Python Tkinter Circular Menu Source Code 3








Share this

Related Posts

Latest
Previous
Next Post »