Table Of Contents
Quick Fix: Basic Custom Widget
import tkinter as tk
from tkinter import ttk
class CustomButton(tk.Frame):
def __init__(self, parent, text="Click Me", command=None, **kwargs):
super().__init__(parent, **kwargs)
self.command = command
self.is_pressed = False
self.create_widget(text)
self.bind_events()
def create_widget(self, text):
# Custom button with gradient-like effect
self.button_frame = tk.Frame(self, relief="raised", bd=2,
bg="#3498db", cursor="hand2")
self.button_frame.pack(fill="both", expand=True, padx=2, pady=2)
self.label = tk.Label(self.button_frame, text=text,
bg="#3498db", fg="white",
font=("Arial", 12, "bold"))
self.label.pack(expand=True)
def bind_events(self):
# Bind events to both frame and label
for widget in [self.button_frame, self.label]:
widget.bind("<Button-1>", self.on_press)
widget.bind("<ButtonRelease-1>", self.on_release)
widget.bind("<Enter>", self.on_enter)
widget.bind("<Leave>", self.on_leave)
def on_press(self, event):
self.is_pressed = True
self.button_frame.config(relief="sunken", bg="#2980b9")
self.label.config(bg="#2980b9")
def on_release(self, event):
if self.is_pressed:
self.is_pressed = False
self.button_frame.config(relief="raised", bg="#3498db")
self.label.config(bg="#3498db")
if self.command:
self.command()
def on_enter(self, event):
if not self.is_pressed:
self.button_frame.config(bg="#5dade2")
self.label.config(bg="#5dade2")
def on_leave(self, event):
if not self.is_pressed:
self.button_frame.config(bg="#3498db")
self.label.config(bg="#3498db")
def configure_text(self, text):
self.label.config(text=text)
class CustomEntry(tk.Frame):
def __init__(self, parent, placeholder="Enter text...", **kwargs):
super().__init__(parent, **kwargs)
self.placeholder = placeholder
self.placeholder_active = True
self.create_widget()
self.setup_placeholder()
def create_widget(self):
self.entry = tk.Entry(self, font=("Arial", 12), relief="flat", bd=5)
self.entry.pack(fill="both", expand=True, padx=5, pady=5)
# Custom border frame
self.config(relief="solid", bd=1, bg="#bdc3c7")
self.entry.bind("<FocusIn>", self.on_focus_in)
self.entry.bind("<FocusOut>", self.on_focus_out)
def setup_placeholder(self):
self.entry.insert(0, self.placeholder)
self.entry.config(fg="#7f8c8d")
self.placeholder_active = True
def on_focus_in(self, event):
if self.placeholder_active:
self.entry.delete(0, tk.END)
self.entry.config(fg="black")
self.placeholder_active = False
self.config(bg="#3498db")
def on_focus_out(self, event):
if not self.entry.get():
self.setup_placeholder()
self.config(bg="#bdc3c7")
def get(self):
if self.placeholder_active:
return ""
return self.entry.get()
def set(self, value):
if self.placeholder_active:
self.entry.delete(0, tk.END)
self.placeholder_active = False
self.entry.config(fg="black")
else:
self.entry.delete(0, tk.END)
self.entry.insert(0, value)
class CustomWidgetDemo:
def __init__(self):
self.root = tk.Tk()
self.root.title("Custom Widget Demo")
self.root.geometry("500x400")
self.root.config(bg="#ecf0f1")
self.create_interface()
def create_interface(self):
# Title
title_label = tk.Label(self.root, text="Custom Widget Demo",
font=("Arial", 18, "bold"),
bg="#ecf0f1", fg="#2c3e50")
title_label.pack(pady=20)
# Custom buttons
button_frame = tk.Frame(self.root, bg="#ecf0f1")
button_frame.pack(pady=20)
custom_btn1 = CustomButton(button_frame, text="Custom Button 1",
command=lambda: self.show_message("Button 1 clicked!"))
custom_btn1.pack(side="left", padx=10, ipadx=20, ipady=10)
custom_btn2 = CustomButton(button_frame, text="Custom Button 2",
command=lambda: self.show_message("Button 2 clicked!"))
custom_btn2.pack(side="left", padx=10, ipadx=20, ipady=10)
# Custom entry
entry_frame = tk.Frame(self.root, bg="#ecf0f1")
entry_frame.pack(pady=20, padx=50, fill="x")
tk.Label(entry_frame, text="Custom Entry Field:",
bg="#ecf0f1", font=("Arial", 12)).pack(anchor="w")
self.custom_entry = CustomEntry(entry_frame, placeholder="Type something here...")
self.custom_entry.pack(fill="x", pady=5)
# Get value button
get_btn = CustomButton(entry_frame, text="Get Value",
command=self.get_entry_value)
get_btn.pack(pady=10, ipadx=10, ipady=5)
# Result display
self.result_label = tk.Label(self.root, text="",
bg="#ecf0f1", fg="#e74c3c",
font=("Arial", 12))
self.result_label.pack(pady=10)
def show_message(self, message):
self.result_label.config(text=message)
def get_entry_value(self):
value = self.custom_entry.get()
if value:
self.result_label.config(text=f"Entry value: '{value}'")
else:
self.result_label.config(text="Entry is empty")
def run(self):
self.root.mainloop()
# Usage
demo = CustomWidgetDemo()
demo.run()
Advanced Custom Widget Components
import tkinter as tk
from tkinter import ttk
import math
from typing import Callable
class CircularProgressBar(tk.Canvas):
def __init__(self, parent, size=100, thickness=10, progress=0,
bg_color="#ecf0f1", fg_color="#3498db", text_color="#2c3e50", **kwargs):
super().__init__(parent, width=size, height=size, **kwargs)
self.size = size
self.thickness = thickness
self.progress = progress
self.bg_color = bg_color
self.fg_color = fg_color
self.text_color = text_color
self.center = size // 2
self.radius = (size - thickness) // 2
self.draw()
def draw(self):
self.delete("all")
# Background circle
self.create_oval(
self.center - self.radius, self.center - self.radius,
self.center + self.radius, self.center + self.radius,
outline=self.bg_color, width=self.thickness
)
# Progress arc
if self.progress > 0:
extent = (self.progress / 100) * 360
self.create_arc(
self.center - self.radius, self.center - self.radius,
self.center + self.radius, self.center + self.radius,
start=90, extent=-extent, outline=self.fg_color,
width=self.thickness, style="arc"
)
# Center text
self.create_text(
self.center, self.center,
text=f"{self.progress:.1f}%",
fill=self.text_color, font=("Arial", 12, "bold")
)
def set_progress(self, value):
self.progress = max(0, min(100, value))
self.draw()
def animate_to(self, target_value, duration=1000, callback=None):
target_value = max(0, min(100, target_value))
start_value = self.progress
steps = 50
step_value = (target_value - start_value) / steps
step_delay = duration // steps
def animate_step(step):
if step <= steps:
new_value = start_value + (step_value * step)
self.set_progress(new_value)
self.after(step_delay, lambda: animate_step(step + 1))
elif callback:
callback()
animate_step(0)
class ToggleSwitch(tk.Frame):
def __init__(self, parent, callback=None, initial_state=False, **kwargs):
super().__init__(parent, **kwargs)
self.callback = callback
self.state = initial_state
self.width = 60
self.height = 30
self.create_widget()
self.update_display()
def create_widget(self):
self.canvas = tk.Canvas(self, width=self.width, height=self.height,
highlightthickness=0, cursor="hand2")
self.canvas.pack()
self.canvas.bind("<Button-1>", self.toggle)
def toggle(self, event=None):
self.state = not self.state
self.update_display()
if self.callback:
self.callback(self.state)
def update_display(self):
self.canvas.delete("all")
# Background
bg_color = "#2ecc71" if self.state else "#bdc3c7"
self.canvas.create_rounded_rectangle(
2, 2, self.width-2, self.height-2,
radius=self.height//2, fill=bg_color, outline=""
)
# Knob
knob_x = self.width - 18 if self.state else 12
self.canvas.create_oval(
knob_x-8, 6, knob_x+8, self.height-6,
fill="white", outline="#bdc3c7", width=1
)
def set_state(self, state):
self.state = state
self.update_display()
# Add rounded rectangle method to Canvas
def create_rounded_rectangle(self, x1, y1, x2, y2, radius=25, **kwargs):
points = []
for x, y in [(x1, y1 + radius), (x1, y1), (x1 + radius, y1),
(x2 - radius, y1), (x2, y1), (x2, y1 + radius),
(x2, y2 - radius), (x2, y2), (x2 - radius, y2),
(x1 + radius, y2), (x1, y2), (x1, y2 - radius)]:
points.extend([x, y])
return self.create_polygon(points, smooth=True, **kwargs)
tk.Canvas.create_rounded_rectangle = create_rounded_rectangle
class NumericStepper(tk.Frame):
def __init__(self, parent, min_value=0, max_value=100, step=1,
initial_value=0, callback=None, **kwargs):
super().__init__(parent, **kwargs)
self.min_value = min_value
self.max_value = max_value
self.step = step
self.value = initial_value
self.callback = callback
self.create_widget()
self.update_display()
def create_widget(self):
# Decrease button
self.decrease_btn = tk.Button(self, text="−", font=("Arial", 14, "bold"),
command=self.decrease, width=3)
self.decrease_btn.pack(side="left")
# Value display
self.value_var = tk.StringVar()
self.value_entry = tk.Entry(self, textvariable=self.value_var,
width=10, justify="center", font=("Arial", 12))
self.value_entry.pack(side="left", padx=5)
self.value_entry.bind("<Return>", self.on_entry_change)
self.value_entry.bind("<FocusOut>", self.on_entry_change)
# Increase button
self.increase_btn = tk.Button(self, text="+", font=("Arial", 14, "bold"),
command=self.increase, width=3)
self.increase_btn.pack(side="left")
def decrease(self):
new_value = max(self.min_value, self.value - self.step)
self.set_value(new_value)
def increase(self):
new_value = min(self.max_value, self.value + self.step)
self.set_value(new_value)
def set_value(self, value):
old_value = self.value
self.value = max(self.min_value, min(self.max_value, value))
self.update_display()
if self.callback and old_value != self.value:
self.callback(self.value)
def get_value(self):
return self.value
def update_display(self):
self.value_var.set(str(self.value))
# Update button states
self.decrease_btn.config(state="normal" if self.value > self.min_value else "disabled")
self.increase_btn.config(state="normal" if self.value < self.max_value else "disabled")
def on_entry_change(self, event=None):
try:
new_value = float(self.value_var.get())
self.set_value(new_value)
except ValueError:
self.update_display() # Reset to current value
class ColorPicker(tk.Frame):
def __init__(self, parent, callback=None, initial_color="#3498db", **kwargs):
super().__init__(parent, **kwargs)
self.callback = callback
self.current_color = initial_color
self.create_widget()
def create_widget(self):
# Color display
self.color_frame = tk.Frame(self, width=50, height=30,
bg=self.current_color, relief="raised", bd=2)
self.color_frame.pack(side="left", padx=5)
self.color_frame.bind("<Button-1>", self.open_color_dialog)
# Color value
self.color_label = tk.Label(self, text=self.current_color,
font=("Arial", 10))
self.color_label.pack(side="left", padx=5)
# Quick color buttons
quick_colors = ["#e74c3c", "#f39c12", "#f1c40f", "#2ecc71",
"#3498db", "#9b59b6", "#34495e", "#95a5a6"]
for color in quick_colors:
btn = tk.Button(self, bg=color, width=2, relief="flat",
command=lambda c=color: self.set_color(c))
btn.pack(side="left", padx=1)
def open_color_dialog(self, event=None):
from tkinter import colorchooser
color = colorchooser.askcolor(initialcolor=self.current_color)[1]
if color:
self.set_color(color)
def set_color(self, color):
self.current_color = color
self.color_frame.config(bg=color)
self.color_label.config(text=color)
if self.callback:
self.callback(color)
def get_color(self):
return self.current_color
class RangeSlider(tk.Frame):
def __init__(self, parent, min_value=0, max_value=100,
initial_range=(20, 80), callback=None, **kwargs):
super().__init__(parent, **kwargs)
self.min_value = min_value
self.max_value = max_value
self.low_value, self.high_value = initial_range
self.callback = callback
self.width = 300
self.height = 40
self.create_widget()
def create_widget(self):
self.canvas = tk.Canvas(self, width=self.width, height=self.height,
highlightthickness=0)
self.canvas.pack()
self.canvas.bind("<Button-1>", self.on_click)
self.canvas.bind("<B1-Motion>", self.on_drag)
self.dragging = None
self.draw()
def draw(self):
self.canvas.delete("all")
# Track
track_y = self.height // 2
track_margin = 20
# Background track
self.canvas.create_line(
track_margin, track_y, self.width - track_margin, track_y,
width=4, fill="#bdc3c7"
)
# Active range
low_x = self.value_to_x(self.low_value)
high_x = self.value_to_x(self.high_value)
self.canvas.create_line(
low_x, track_y, high_x, track_y,
width=4, fill="#3498db"
)
# Handles
handle_radius = 8
# Low handle
self.low_handle = self.canvas.create_oval(
low_x - handle_radius, track_y - handle_radius,
low_x + handle_radius, track_y + handle_radius,
fill="#2980b9", outline="white", width=2
)
# High handle
self.high_handle = self.canvas.create_oval(
high_x - handle_radius, track_y - handle_radius,
high_x + handle_radius, track_y + handle_radius,
fill="#2980b9", outline="white", width=2
)
# Value labels
self.canvas.create_text(
low_x, track_y - 20, text=str(int(self.low_value)),
font=("Arial", 10)
)
self.canvas.create_text(
high_x, track_y - 20, text=str(int(self.high_value)),
font=("Arial", 10)
)
def value_to_x(self, value):
track_margin = 20
track_width = self.width - 2 * track_margin
ratio = (value - self.min_value) / (self.max_value - self.min_value)
return track_margin + ratio * track_width
def x_to_value(self, x):
track_margin = 20
track_width = self.width - 2 * track_margin
ratio = (x - track_margin) / track_width
return self.min_value + ratio * (self.max_value - self.min_value)
def on_click(self, event):
x, y = event.x, event.y
# Check which handle is closest
low_x = self.value_to_x(self.low_value)
high_x = self.value_to_x(self.high_value)
low_distance = abs(x - low_x)
high_distance = abs(x - high_x)
if low_distance < high_distance:
self.dragging = "low"
else:
self.dragging = "high"
self.on_drag(event)
def on_drag(self, event):
if self.dragging:
new_value = self.x_to_value(event.x)
new_value = max(self.min_value, min(self.max_value, new_value))
if self.dragging == "low":
self.low_value = min(new_value, self.high_value)
else:
self.high_value = max(new_value, self.low_value)
self.draw()
if self.callback:
self.callback((self.low_value, self.high_value))
def get_range(self):
return (self.low_value, self.high_value)
class AdvancedCustomWidgetDemo:
def __init__(self):
self.root = tk.Tk()
self.root.title("Advanced Custom Widgets")
self.root.geometry("800x600")
self.root.config(bg="#ecf0f1")
self.create_interface()
def create_interface(self):
# Title
title_label = tk.Label(self.root, text="Advanced Custom Widgets",
font=("Arial", 20, "bold"),
bg="#ecf0f1", fg="#2c3e50")
title_label.pack(pady=20)
# Main container
main_frame = tk.Frame(self.root, bg="#ecf0f1")
main_frame.pack(fill="both", expand=True, padx=20)
# Left column
left_frame = tk.Frame(main_frame, bg="#ecf0f1")
left_frame.pack(side="left", fill="both", expand=True, padx=10)
# Circular Progress Bar
progress_frame = tk.LabelFrame(left_frame, text="Circular Progress Bar",
bg="#ecf0f1", font=("Arial", 12, "bold"))
progress_frame.pack(fill="x", pady=10)
self.progress_bar = CircularProgressBar(progress_frame, size=120)
self.progress_bar.pack(pady=10)
progress_controls = tk.Frame(progress_frame, bg="#ecf0f1")
progress_controls.pack(pady=5)
tk.Button(progress_controls, text="Animate to 75%",
command=lambda: self.progress_bar.animate_to(75)).pack(side="left", padx=5)
tk.Button(progress_controls, text="Reset",
command=lambda: self.progress_bar.set_progress(0)).pack(side="left", padx=5)
# Toggle Switch
toggle_frame = tk.LabelFrame(left_frame, text="Toggle Switch",
bg="#ecf0f1", font=("Arial", 12, "bold"))
toggle_frame.pack(fill="x", pady=10)
self.toggle = ToggleSwitch(toggle_frame, callback=self.on_toggle)
self.toggle.pack(pady=10)
self.toggle_status = tk.Label(toggle_frame, text="OFF",
bg="#ecf0f1", font=("Arial", 12))
self.toggle_status.pack()
# Numeric Stepper
stepper_frame = tk.LabelFrame(left_frame, text="Numeric Stepper",
bg="#ecf0f1", font=("Arial", 12, "bold"))
stepper_frame.pack(fill="x", pady=10)
self.stepper = NumericStepper(stepper_frame, min_value=0, max_value=50,
step=5, callback=self.on_stepper_change)
self.stepper.pack(pady=10)
# Right column
right_frame = tk.Frame(main_frame, bg="#ecf0f1")
right_frame.pack(side="right", fill="both", expand=True, padx=10)
# Color Picker
color_frame = tk.LabelFrame(right_frame, text="Color Picker",
bg="#ecf0f1", font=("Arial", 12, "bold"))
color_frame.pack(fill="x", pady=10)
self.color_picker = ColorPicker(color_frame, callback=self.on_color_change)
self.color_picker.pack(pady=10)
# Range Slider
range_frame = tk.LabelFrame(right_frame, text="Range Slider",
bg="#ecf0f1", font=("Arial", 12, "bold"))
range_frame.pack(fill="x", pady=10)
self.range_slider = RangeSlider(range_frame, callback=self.on_range_change)
self.range_slider.pack(pady=10)
self.range_label = tk.Label(range_frame, text="Range: 20 - 80",
bg="#ecf0f1", font=("Arial", 10))
self.range_label.pack()
# Status display
status_frame = tk.LabelFrame(right_frame, text="Widget Status",
bg="#ecf0f1", font=("Arial", 12, "bold"))
status_frame.pack(fill="both", expand=True, pady=10)
self.status_text = tk.Text(status_frame, height=10, wrap="word")
scrollbar = tk.Scrollbar(status_frame, orient="vertical",
command=self.status_text.yview)
self.status_text.configure(yscrollcommand=scrollbar.set)
self.status_text.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def on_toggle(self, state):
self.toggle_status.config(text="ON" if state else "OFF")
self.log_status(f"Toggle switched: {'ON' if state else 'OFF'}")
def on_stepper_change(self, value):
self.log_status(f"Stepper value: {value}")
def on_color_change(self, color):
self.log_status(f"Color selected: {color}")
def on_range_change(self, range_values):
low, high = range_values
self.range_label.config(text=f"Range: {int(low)} - {int(high)}")
self.log_status(f"Range changed: {int(low)} - {int(high)}")
def log_status(self, message):
self.status_text.insert(tk.END, f"{message}\n")
self.status_text.see(tk.END)
def run(self):
self.root.mainloop()
# Usage
demo = AdvancedCustomWidgetDemo()
demo.run()
Works with tkinter 8.6+ and creates sophisticated custom widgets with advanced functionality.
Share this article
Add Comment
No comments yet. Be the first to comment!