In this tutorial, we’ll build a modern, animated tip calculator desktop app using Python + Tkinter.
By the end, you’ll have:
Real-time tip calculations
Animated number updates
Dark mode toggle
Preset tip buttons
Calculation history with export
A polished UI using ttk and sv_ttk
No advanced Tkinter knowledge required—we’ll build it piece by piece.
🧰 Step 1: Import Required Modules
Let’s start by importing everything we need.
import sys
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sv_ttk
Why these imports?
tkinter → GUI framework
ttk → modern themed widgets
messagebox / filedialog → dialogs & file saving
sv_ttk → clean light/dark themes
sys / os → handle packaged app paths (PyInstaller-friendly)
📁 Step 2: Helper Functions
2.1 Loading files safely (for packaging)
def resource_path(file_name):
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, file_name)
This ensures your app works both normally and when bundled into an .exe.
2.2 Status bar helper
def set_status(msg):
status_var.set(msg)
root.update_idletasks()
We’ll use this to show messages like:
“Calculation completed!”
“History exported successfully”
🪟 Step 3: Create the Main App Window
root = tk.Tk()
root.title("Interactive Tip Calculator")
root.geometry("600x700")
root.minsize(600, 700)
sv_ttk.set_theme("light")
What’s happening?
We create the main window
Set a minimum size (prevents layout breakage)
Apply a modern light theme
📦 Step 4: Define App State (Global Variables)
dark_mode_var = tk.BooleanVar(value=False)
bill_var = tk.StringVar()
tip_var = tk.StringVar(value="15")
people_var = tk.StringVar(value="1")
tip_per_person_var = tk.StringVar(value="$0.00")
total_per_person_var = tk.StringVar(value="$0.00")
calculation_history = []
Why StringVar?
They automatically update the UI when their value changes—perfect for real-time apps.
🌗 Step 5: Dark Mode Toggle
def toggle_theme():
bg = "#2E2E2E" if dark_mode_var.get() else "#FFFFFF"
fg = "white" if dark_mode_var.get() else "black"
root.configure(bg=bg)
for w in ["TFrame", "TLabel", "TLabelframe", "TLabelframe.Label", "TCheckbutton"]:
style.configure(w, background=bg, foreground=fg)
for entry in [bill_entry, tip_entry, people_entry]:
entry.configure(background=bg, foreground=fg)
This:
Switches colors dynamically
Updates both ttk widgets and normal Entry fields
💰 Step 6: Tip Calculation Logic
def calculate_tip(real_time=False):
try:
bill = float(bill_var.get())
tip_percent = float(tip_var.get())
people = int(people_var.get())
if bill < 0 or tip_percent < 0 or people < 1:
raise ValueError
except ValueError:
if not real_time:
messagebox.showerror("Invalid Input", "Please enter valid numeric values.")
return
Why validation matters
We prevent:
Negative numbers
Division by zero
Non-numeric input
6.1 Perform the calculation
tip_total = bill * (tip_percent / 100)
total_bill = bill + tip_total
tip_per_person = tip_total / people
total_per_person = total_bill / people
6.2 Animate the result
animate_split(tip_per_person, total_per_person)
Instead of instantly updating numbers, we animate them for a smoother UX.
🎞 Step 7: Animated Number Updates
def animate_split(tip_final, total_final, steps=30, interval=15):
tip_current = 0.0
total_current = 0.0
tip_step = tip_final / steps
total_step = total_final / steps
We gradually increase values in small steps.
7.1 Animation loop
def step_animation(count=0):
nonlocal tip_current, total_current
if count >= steps:
tip_per_person_var.set(f"${tip_final:.2f}")
total_per_person_var.set(f"${total_final:.2f}")
return
tip_current += tip_step
total_current += total_step
tip_per_person_var.set(f"${tip_current:.2f}")
total_per_person_var.set(f"${total_current:.2f}")
root.after(interval, lambda: step_animation(count + 1))
root.after() lets Tkinter animate without freezing the UI.
⚡ Step 8: Preset Tip Buttons
def set_tip(value):
tip_var.set(str(value))
calculate_tip(real_time=True)
This allows buttons like 10% / 15% / 20% to instantly update results.
🗂 Step 9: Calculation History
9.1 Save results
def add_to_history(bill, tip_percent, people, tip_pp, total_pp):
calculation_history.append((bill, tip_percent, people, tip_pp, total_pp))
preview = (
f"${bill:.2f} | {tip_percent}% | {people} person(s) → "
f"Tip: ${tip_pp:.2f}, Total: ${total_pp:.2f}"
)
history_list.insert(tk.END, preview)
Each calculation is stored and shown in a list.
📤 Step 10: Export History to a Text File
def export_history_txt():
if not calculation_history:
messagebox.showinfo("Empty History", "No calculations to export.")
return
We prevent exporting empty data.
10.1 Save the file
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text Files", "*.txt")],
title="Export Tip History"
)
10.2 Write the data
with open(file_path, "w", encoding="utf-8") as f:
f.write("Tip Calculator History\n")
f.write("=" * 40 + "\n\n")
for i, (bill, tip, people, tip_pp, total_pp) in enumerate(calculation_history, 1):
f.write(f"{i}. Bill: ${bill:.2f}, Tip: {tip}%, People: {people}\n")
f.write(f" Tip per person: ${tip_pp:.2f}, Total per person: ${total_pp:.2f}\n\n")
🖱 Step 11: Double-Click History Viewer (Animated)
When a history item is double-clicked, we open a detail window and type the content line-by-line.
This section introduces:
Toplevel windows
Text widgets
Typing animations
(You already have excellent code here—great example of UI polish.)
🎨 Step 12: Styling
style = ttk.Style()
style.theme_use("clam")
style.configure(
"Action.TButton",
font=("Segoe UI", 11, "bold"),
padding=8
)
Custom styles make your app feel intentional and professional.
🧱 Step 13: Layout the Interface
We use:
Frame containers
grid() for structure
pack() only when appropriate
Each section (inputs, outputs, buttons, history) is clearly separated—perfect for maintainability.
🔄 Step 14: Real-Time Updates
bill_var.trace_add("write", lambda *args: calculate_tip(real_time=True))
tip_var.trace_add("write", lambda *args: calculate_tip(real_time=True))
people_var.trace_add("write", lambda *args: calculate_tip(real_time=True))
This makes the calculator react instantly as the user types.
▶️ Step 15: Run the App
root.mainloop()
This starts Tkinter’s event loop—and your app comes to life 🎉
🏁 Final Thoughts
You now have:
A polished Tkinter desktop app
Animations without threading headaches
A real-time, user-friendly experience
A great foundation for packaging with PyInstaller

Top comments (0)