Untitled
Lattuse
plain_text
9 months ago
10 kB
24
Indexable
import tkinter as tk
from tkinter import messagebox
import math
class Calculator(tk.Tk):
def __init__(self):
super().__init__()
# Main Window
self.title("Simple Calculator")
self.geometry("300x400")
self.resizable(False, False)
self.configure(bg="#f0f0f0") # background color of choice (light)
# Expression state
self.expression = tk.StringVar(value="")
# Build UI
self._build_ui()
# Keyboard bindings (no duplicates; Enter/Escape/Backspace handled)
self._bind_keys()
def _build_ui(self):
# Input field
entry = tk.Entry(self, textvariable=self.expression, font=("Segoe UI", 16),
justify="right", bd=1, relief="solid")
entry.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=8, pady=8, ipady=10)
entry.focus_set()
# Configure grid weights for uniform look
for r in range(1, 7):
self.grid_rowconfigure(r, weight=1)
for c in range(4):
self.grid_columnconfigure(c, weight=1)
# Button factory with role tagging
def make_btn(text, row, col, cmd=None, colspan=1, role="digit"):
btn = tk.Button(self, text=text, command=cmd, borderwidth=0, highlightthickness=0)
btn.role = role # tag to preserve role styling across theme changes
self._apply_role_style(btn, dark=False) # initial light style
btn.configure(font=("Segoe UI", 12), padx=8, pady=8)
btn.grid(row=row, column=col, columnspan=colspan, sticky="nsew", padx=6, pady=6)
return btn
# Row 1: C, ⌫, %, ÷
make_btn("C", 1, 0, cmd=self.clear, role="danger")
make_btn("⌫", 1, 1, cmd=self.backspace, role="act")
make_btn("%", 1, 2, cmd=lambda: self.add_operator("%"), role="op")
make_btn("÷", 1, 3, cmd=lambda: self.add_operator("/"), role="op")
# Row 2: 7 8 9 ×
make_btn("7", 2, 0, cmd=lambda: self.add_char("7"))
make_btn("8", 2, 1, cmd=lambda: self.add_char("8"))
make_btn("9", 2, 2, cmd=lambda: self.add_char("9"))
make_btn("×", 2, 3, cmd=lambda: self.add_operator("*"), role="op")
# Row 3: 4 5 6 −
make_btn("4", 3, 0, cmd=lambda: self.add_char("4"))
make_btn("5", 3, 1, cmd=lambda: self.add_char("5"))
make_btn("6", 3, 2, cmd=lambda: self.add_char("6"))
make_btn("−", 3, 3, cmd=lambda: self.add_operator("-"), role="op")
# Row 4: 1 2 3 +
make_btn("1", 4, 0, cmd=lambda: self.add_char("1"))
make_btn("2", 4, 1, cmd=lambda: self.add_char("2"))
make_btn("3", 4, 2, cmd=lambda: self.add_char("3"))
make_btn("+", 4, 3, cmd=lambda: self.add_operator("+"), role="op")
# Row 5: 0 . √ =
make_btn("0", 5, 0, cmd=lambda: self.add_char("0"))
make_btn(".", 5, 1, cmd=lambda: self.add_dot())
make_btn("√", 5, 2, cmd=self.sqrt, role="act")
make_btn("=", 5, 3, cmd=self.evaluate, role="eval")
# Row 6: Theme toggle (optional enhancement)
make_btn("Toggle Theme", 6, 0, cmd=self.toggle_theme, colspan=4, role="act")
# Keep a reference to entry for keyboard insertions
self.entry_widget = entry
# Light and dark theme palettes
self._light = {
"bg": "#f0f0f0",
"entry_bg": "white",
"entry_fg": "#111",
}
self._dark = {
"bg": "#222831",
"entry_bg": "#393e46",
"entry_fg": "#eeeeee",
}
self._dark_mode = False
def _apply_role_style(self, btn, dark: bool):
# Base palette per theme
if dark:
base_bg, base_fg, base_active = "#444", "#eee", "#555"
op_bg, op_active = "#7a5e3a", "#8b6a42" # amber-like in dark
act_bg, act_active = "#2e7d76", "#2b6f6a" # teal-like in dark
danger_bg, danger_active = "#7a3a3a", "#8b4242"
eval_bg, eval_active = "#4a6b30", "#557a36"
else:
base_bg, base_fg, base_active = "#e0e0e0", "#111", "#d5d5d5"
op_bg, op_active = "#ffe0b2", "#ffcc80"
act_bg, act_active = "#b2dfdb", "#80cbc4"
danger_bg, danger_active = "#ffcccc", "#ff9999"
eval_bg, eval_active = "#c5e1a5", "#aed581"
# Set base
btn.configure(bg=base_bg, fg=base_fg, activebackground=base_active)
# Overlay role-specific colors
role = getattr(btn, "role", "digit")
if role == "op":
btn.configure(bg=op_bg, activebackground=op_active)
elif role == "act":
btn.configure(bg=act_bg, activebackground=act_active)
elif role == "danger":
btn.configure(bg=danger_bg, activebackground=danger_active)
elif role == "eval":
btn.configure(bg=eval_bg, activebackground=eval_active)
def _bind_keys(self):
# Window/app-level fallbacks so keys work even if a button holds focus
self.bind_all("<Return>", lambda e: (self.evaluate(), "break")[1])
self.bind_all("<KP_Enter>", lambda e: (self.evaluate(), "break")[1])
self.bind_all("<Escape>", lambda e: (self.clear(), "break")[1])
# Backspace: prefer Entry binding; keep a global guard to avoid duplicates
self.bind_all("<BackSpace>", lambda e: (self.backspace(), "break")[1])
# Entry-specific typing to avoid duplication, and explicit Enter/Escape
self.entry_widget.bind("<Key>", self._typed_on_entry)
self.entry_widget.bind("<Return>", lambda e: (self.evaluate(), "break")[1])
self.entry_widget.bind("<KP_Enter>", lambda e: (self.evaluate(), "break")[1])
self.entry_widget.bind("<Escape>", lambda e: (self.clear(), "break")[1])
def _typed_on_entry(self, event):
ch = event.char
if not ch:
return
# Normalize x/X to * for multiply
if ch in "xX":
ch = "*"
# Allow only supported characters
if ch in "0123456789+-*/().%":
self.add_char(ch)
# Stop default insertion to prevent double characters
return "break"
# Button/logic handlers
def add_char(self, ch: str):
self.expression.set(self.expression.get() + ch)
def add_operator(self, op: str):
expr = self.expression.get()
if not expr:
if op in "+-":
self.expression.set(op)
return
if expr[-1] in "+-*/%.":
self.expression.set(expr[:-1] + op)
else:
self.expression.set(expr + op)
def add_dot(self):
expr = self.expression.get()
last_op = -1
for i in range(len(expr) - 1, -1, -1):
if expr[i] in "+-*/%":
last_op = i
break
token = expr[last_op + 1 :]
if "." in token:
return
self.expression.set(expr + ".")
def clear(self):
self.expression.set("")
def backspace(self):
expr = self.expression.get()
if expr:
self.expression.set(expr[:-1])
def sqrt(self):
try:
value = self._safe_eval(self.expression.get())
if value < 0:
raise ValueError("Cannot sqrt negative")
result = math.sqrt(value)
self.expression.set(self._format_result(result))
except Exception as e:
messagebox.showerror("Error", f"Invalid input for √: {e}")
self.expression.set("")
def evaluate(self):
try:
result = self._safe_eval(self.expression.get())
self.expression.set(self._format_result(result))
except ZeroDivisionError:
messagebox.showerror("Error", "Division by zero")
self.expression.set("")
except Exception as e:
messagebox.showerror("Error", f"Invalid input: {e}")
self.expression.set("")
def toggle_theme(self):
self._dark_mode = not self._dark_mode
pal = self._dark if self._dark_mode else self._light
self.configure(bg=pal["bg"])
self.entry_widget.configure(bg=pal["entry_bg"], fg=pal["entry_fg"],
insertbackground=pal["entry_fg"])
# Reapply per-button role styling so colors persist after theme change
for child in self.winfo_children():
if isinstance(child, tk.Button):
self._apply_role_style(child, dark=self._dark_mode)
def _preprocess_percent(self, expr: str) -> str:
out = []
i = 0
while i < len(expr):
if expr[i].isdigit() or (expr[i] == "." and i + 1 < len(expr) and expr[i + 1].isdigit()):
j = i
while j < len(expr) and (expr[j].isdigit() or expr[j] == "."):
j += 1
if j < len(expr) and expr[j] == "%":
out.append(f"({expr[i:j]}/100)")
i = j + 1
continue
else:
out.append(expr[i:j])
i = j
continue
else:
out.append(expr[i])
i += 1
return "".join(out)
def _safe_eval(self, expr: str) -> float:
expr = expr.strip()
if not expr:
return 0.0
expr = expr.replace("×", "*").replace("÷", "/")
expr = self._preprocess_percent(expr)
allowed = set("0123456789+-*/(). ")
if not all(ch in allowed for ch in expr):
raise ValueError("Unsupported character")
return float(eval(expr, {"__builtins__": {}}, {}))
def _format_result(self, value: float) -> str:
if value.is_integer():
return str(int(value))
return f"{value:.10g}"
if __name__ == "__main__":
app = Calculator()
app.mainloop()
Editor is loading...
Leave a Comment