Features: basic arithmetic, trig/inverse-trig, log/ln, factorial, memory (MC/MR/M+/M−), DEG/RAD toggle, auto-resizing display, hover effects, keyboard bindings, and clipboard copy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
364 lines
15 KiB
Python
364 lines
15 KiB
Python
import tkinter as tk
|
||
import math
|
||
import re
|
||
|
||
# ── Catppuccin Mocha palette ──────────────────────────────────────────────────
|
||
BG = "#1e1e2e"
|
||
BASE = "#313244"
|
||
OVERLAY = "#45475a"
|
||
TEXT = "#cdd6f4"
|
||
SUBTEXT = "#6c7086"
|
||
BLUE = "#89b4fa"
|
||
PURPLE = "#cba6f7"
|
||
GREEN = "#a6e3a1"
|
||
RED = "#f38ba8"
|
||
YELLOW = "#f9e2af"
|
||
TEAL = "#94e2d5"
|
||
|
||
|
||
class Calculator(tk.Tk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.title("Scientific Calculator")
|
||
self.resizable(False, False)
|
||
self.configure(bg=BG)
|
||
|
||
self.expression = ""
|
||
self.result_shown = False
|
||
self.memory = 0.0
|
||
self.angle_mode = "DEG" # or "RAD"
|
||
|
||
self._build_display()
|
||
self._build_buttons()
|
||
self._center_window()
|
||
|
||
# ── window centering ──────────────────────────────────────────────────────
|
||
|
||
def _center_window(self):
|
||
self.update_idletasks()
|
||
w, h = self.winfo_width(), self.winfo_height()
|
||
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
|
||
self.geometry(f"+{(sw - w) // 2}+{(sh - h) // 2}")
|
||
|
||
# ── display ───────────────────────────────────────────────────────────────
|
||
|
||
def _build_display(self):
|
||
display_frame = tk.Frame(self, bg=BG, pady=12, padx=14)
|
||
display_frame.pack(fill="x")
|
||
|
||
# top bar: memory indicator (left) + angle-mode toggle (right)
|
||
top_bar = tk.Frame(display_frame, bg=BG)
|
||
top_bar.pack(fill="x")
|
||
|
||
self.mem_indicator = tk.Label(
|
||
top_bar, text="", bg=BG, fg=TEAL,
|
||
font=("Consolas", 10, "bold"), anchor="w",
|
||
)
|
||
self.mem_indicator.pack(side="left")
|
||
|
||
self.angle_lbl = tk.Label(
|
||
top_bar, text="DEG", bg=OVERLAY, fg=BLUE,
|
||
font=("Consolas", 10, "bold"),
|
||
padx=10, pady=3, cursor="hand2",
|
||
)
|
||
self.angle_lbl.pack(side="right")
|
||
self.angle_lbl.bind("<Button-1>", lambda _: self._toggle_angle())
|
||
|
||
# history line
|
||
self.history_var = tk.StringVar(value="")
|
||
tk.Label(
|
||
display_frame,
|
||
textvariable=self.history_var,
|
||
bg=BG, fg=SUBTEXT,
|
||
font=("Consolas", 12),
|
||
anchor="e", height=1,
|
||
).pack(fill="x")
|
||
|
||
# main display — font shrinks automatically for long text
|
||
self.display_var = tk.StringVar(value="0")
|
||
self.display_lbl = tk.Label(
|
||
display_frame,
|
||
textvariable=self.display_var,
|
||
bg=BG, fg=TEXT,
|
||
font=("Consolas", 32, "bold"),
|
||
anchor="e", height=2,
|
||
)
|
||
self.display_lbl.pack(fill="x")
|
||
|
||
# click display to copy result
|
||
self.display_lbl.bind("<Button-1>", lambda _: self._copy_result())
|
||
self.display_lbl.config(cursor="hand2")
|
||
|
||
# divider
|
||
tk.Frame(self, bg=OVERLAY, height=1).pack(fill="x")
|
||
|
||
# ── buttons ───────────────────────────────────────────────────────────────
|
||
|
||
def _build_buttons(self):
|
||
btn_frame = tk.Frame(self, bg=BG, padx=8, pady=8)
|
||
btn_frame.pack()
|
||
|
||
C = {
|
||
"digit": (BASE, TEXT),
|
||
"op": (OVERLAY, PURPLE),
|
||
"fn": (BASE, BLUE),
|
||
"equal": (PURPLE, BG),
|
||
"clear": (RED, BG),
|
||
"back": (OVERLAY, RED),
|
||
"const": (BASE, GREEN),
|
||
"mem": (BASE, TEAL),
|
||
}
|
||
|
||
layout = [
|
||
# ── Row 0 · memory ──────────────────────────────────────────────
|
||
("MC", 0, 0, 1, "mem", self._mem_clear),
|
||
("MR", 0, 1, 1, "mem", self._mem_recall),
|
||
("M+", 0, 2, 1, "mem", self._mem_add),
|
||
("M−", 0, 3, 1, "mem", self._mem_sub),
|
||
# ── Row 1 · basic trig ──────────────────────────────────────────
|
||
("sin", 1, 0, 1, "fn", lambda: self._insert_fn("sin")),
|
||
("cos", 1, 1, 1, "fn", lambda: self._insert_fn("cos")),
|
||
("tan", 1, 2, 1, "fn", lambda: self._insert_fn("tan")),
|
||
("√x", 1, 3, 1, "fn", lambda: self._insert_fn("sqrt")),
|
||
# ── Row 2 · inverse trig + power ────────────────────────────────
|
||
("asin", 2, 0, 1, "fn", lambda: self._insert_fn("asin")),
|
||
("acos", 2, 1, 1, "fn", lambda: self._insert_fn("acos")),
|
||
("atan", 2, 2, 1, "fn", lambda: self._insert_fn("atan")),
|
||
("xʸ", 2, 3, 1, "fn", lambda: self._append("**")),
|
||
# ── Row 3 · logarithms + factorial + square ──────────────────────
|
||
("log", 3, 0, 1, "fn", lambda: self._insert_fn("log")),
|
||
("ln", 3, 1, 1, "fn", lambda: self._insert_fn("ln")),
|
||
("x!", 3, 2, 1, "fn", lambda: self._insert_fn("fact")),
|
||
("x²", 3, 3, 1, "fn", lambda: self._append("**2")),
|
||
# ── Row 4 · parens + constants ───────────────────────────────────
|
||
("(", 4, 0, 1, "op", lambda: self._append("(")),
|
||
(")", 4, 1, 1, "op", lambda: self._append(")")),
|
||
("π", 4, 2, 1, "const", lambda: self._append("π")),
|
||
("e", 4, 3, 1, "const", lambda: self._append("e")),
|
||
# ── Row 5 · clear / back ─────────────────────────────────────────
|
||
("AC", 5, 0, 1, "clear", self._clear_all),
|
||
("CE", 5, 1, 1, "back", self._clear_entry),
|
||
("⌫", 5, 2, 1, "back", self._backspace),
|
||
("%", 5, 3, 1, "op", lambda: self._append("%")),
|
||
# ── Row 6 ────────────────────────────────────────────────────────
|
||
("7", 6, 0, 1, "digit", lambda: self._append("7")),
|
||
("8", 6, 1, 1, "digit", lambda: self._append("8")),
|
||
("9", 6, 2, 1, "digit", lambda: self._append("9")),
|
||
("÷", 6, 3, 1, "op", lambda: self._append("/")),
|
||
# ── Row 7 ────────────────────────────────────────────────────────
|
||
("4", 7, 0, 1, "digit", lambda: self._append("4")),
|
||
("5", 7, 1, 1, "digit", lambda: self._append("5")),
|
||
("6", 7, 2, 1, "digit", lambda: self._append("6")),
|
||
("×", 7, 3, 1, "op", lambda: self._append("*")),
|
||
# ── Row 8 ────────────────────────────────────────────────────────
|
||
("1", 8, 0, 1, "digit", lambda: self._append("1")),
|
||
("2", 8, 1, 1, "digit", lambda: self._append("2")),
|
||
("3", 8, 2, 1, "digit", lambda: self._append("3")),
|
||
("−", 8, 3, 1, "op", lambda: self._append("-")),
|
||
# ── Row 9 ────────────────────────────────────────────────────────
|
||
("±", 9, 0, 1, "op", self._negate),
|
||
("0", 9, 1, 1, "digit", lambda: self._append("0")),
|
||
(".", 9, 2, 1, "digit", lambda: self._append(".")),
|
||
("+", 9, 3, 1, "op", lambda: self._append("+")),
|
||
# ── Row 10 · equals ──────────────────────────────────────────────
|
||
("=", 10, 0, 4, "equal", self._evaluate),
|
||
]
|
||
|
||
for (label, row, col, span, ckey, cmd) in layout:
|
||
bg, fg = C[ckey]
|
||
btn = tk.Button(
|
||
btn_frame,
|
||
text=label,
|
||
bg=bg, fg=fg,
|
||
activebackground=self._lighten(bg),
|
||
activeforeground=fg,
|
||
font=("Consolas", 14, "bold"),
|
||
relief="flat", bd=0,
|
||
width=5 if span == 1 else 23,
|
||
height=2,
|
||
cursor="hand2",
|
||
command=cmd,
|
||
)
|
||
btn.grid(row=row, column=col, columnspan=span,
|
||
padx=4, pady=3, sticky="ew")
|
||
# hover highlight
|
||
btn.bind("<Enter>", lambda _e, b=btn, c=bg: b.config(bg=self._lighten(c)))
|
||
btn.bind("<Leave>", lambda _e, b=btn, c=bg: b.config(bg=c))
|
||
|
||
# keyboard bindings
|
||
self.bind("<Key>", self._on_key)
|
||
self.bind("<Return>", lambda _: self._evaluate())
|
||
self.bind("<KP_Enter>", lambda _: self._evaluate())
|
||
self.bind("<BackSpace>", lambda _: self._backspace())
|
||
self.bind("<Escape>", lambda _: self._clear_all())
|
||
self.bind("<Delete>", lambda _: self._clear_all())
|
||
self.bind("<Control-c>", lambda _: self._copy_result())
|
||
|
||
# ── helpers ───────────────────────────────────────────────────────────────
|
||
|
||
@staticmethod
|
||
def _lighten(hex_color: str) -> str:
|
||
h = hex_color.lstrip("#")
|
||
r, g, b = (int(h[i:i+2], 16) for i in (0, 2, 4))
|
||
return f"#{min(255, r+35):02x}{min(255, g+35):02x}{min(255, b+35):02x}"
|
||
|
||
def _set_display(self, text: str):
|
||
text = text or "0"
|
||
n = len(text)
|
||
size = 18 if n > 20 else 22 if n > 14 else 26 if n > 9 else 32
|
||
self.display_lbl.config(font=("Consolas", size, "bold"))
|
||
self.display_var.set(text)
|
||
|
||
def _append(self, char: str):
|
||
if self.result_shown:
|
||
if char in "0123456789.":
|
||
self.expression = ""
|
||
self.result_shown = False
|
||
self.expression += char
|
||
self._set_display(self.expression)
|
||
|
||
def _insert_fn(self, fn: str):
|
||
self.result_shown = False
|
||
self.expression += fn + "("
|
||
self._set_display(self.expression)
|
||
|
||
def _clear_all(self):
|
||
self.expression = ""
|
||
self.history_var.set("")
|
||
self.result_shown = False
|
||
self._set_display("")
|
||
|
||
def _clear_entry(self):
|
||
self.expression = ""
|
||
self.result_shown = False
|
||
self._set_display("")
|
||
|
||
def _backspace(self):
|
||
if self.result_shown:
|
||
self._clear_all()
|
||
return
|
||
self.expression = self.expression[:-1]
|
||
self._set_display(self.expression)
|
||
|
||
def _negate(self):
|
||
if not self.expression or self.expression == "0":
|
||
return
|
||
if self.expression.startswith("-"):
|
||
self.expression = self.expression[1:]
|
||
else:
|
||
self.expression = "-" + self.expression
|
||
self._set_display(self.expression)
|
||
|
||
def _toggle_angle(self):
|
||
if self.angle_mode == "DEG":
|
||
self.angle_mode = "RAD"
|
||
self.angle_lbl.config(text="RAD", fg=YELLOW)
|
||
else:
|
||
self.angle_mode = "DEG"
|
||
self.angle_lbl.config(text="DEG", fg=BLUE)
|
||
|
||
def _copy_result(self):
|
||
self.clipboard_clear()
|
||
self.clipboard_append(self.display_var.get())
|
||
|
||
# ── memory ────────────────────────────────────────────────────────────────
|
||
|
||
def _update_mem_indicator(self):
|
||
self.mem_indicator.config(
|
||
text=f"M = {self.memory:.6g}" if self.memory != 0 else ""
|
||
)
|
||
|
||
def _mem_clear(self):
|
||
self.memory = 0.0
|
||
self._update_mem_indicator()
|
||
|
||
def _mem_recall(self):
|
||
val = f"{self.memory:.10g}"
|
||
if self.result_shown or not self.expression:
|
||
self.expression = val
|
||
else:
|
||
self.expression += val
|
||
self.result_shown = False
|
||
self._set_display(self.expression)
|
||
|
||
def _mem_add(self):
|
||
try:
|
||
self.memory += float(self._safe_eval(self.expression))
|
||
self._update_mem_indicator()
|
||
except Exception:
|
||
pass
|
||
|
||
def _mem_sub(self):
|
||
try:
|
||
self.memory -= float(self._safe_eval(self.expression))
|
||
self._update_mem_indicator()
|
||
except Exception:
|
||
pass
|
||
|
||
# ── keyboard ──────────────────────────────────────────────────────────────
|
||
|
||
def _on_key(self, event: tk.Event):
|
||
char = event.char
|
||
if char in "0123456789.+-*/()%":
|
||
self._append(char)
|
||
elif char == "^":
|
||
self._append("**")
|
||
|
||
# ── evaluate ──────────────────────────────────────────────────────────────
|
||
|
||
def _evaluate(self):
|
||
if not self.expression:
|
||
return
|
||
try:
|
||
result = self._safe_eval(self.expression)
|
||
self.history_var.set(self.expression + " =")
|
||
if isinstance(result, float) and result.is_integer():
|
||
display = str(int(result))
|
||
else:
|
||
display = f"{result:.10g}"
|
||
self.expression = display
|
||
self._set_display(display)
|
||
self.result_shown = True
|
||
except ZeroDivisionError:
|
||
self._show_error("Error: ÷ 0")
|
||
except ValueError as exc:
|
||
self._show_error(f"Error: {exc}")
|
||
except Exception:
|
||
self._show_error("Error")
|
||
|
||
def _show_error(self, msg: str):
|
||
self._set_display(msg)
|
||
self.expression = ""
|
||
self.result_shown = False
|
||
|
||
def _safe_eval(self, expr: str):
|
||
expr = expr.replace("π", str(math.pi))
|
||
# Replace standalone 'e' constant but not 'e' in scientific notation (e.g. 1e5)
|
||
expr = re.sub(r"(?<![0-9Ee])e(?![0-9+\-])", str(math.e), expr)
|
||
|
||
rad = self.angle_mode == "RAD"
|
||
|
||
def _to_rad(x): return x if rad else math.radians(x)
|
||
def _from_rad(x): return x if rad else math.degrees(x)
|
||
|
||
safe_ns = {
|
||
"__builtins__": {},
|
||
"sin": lambda x: math.sin(_to_rad(x)),
|
||
"cos": lambda x: math.cos(_to_rad(x)),
|
||
"tan": lambda x: math.tan(_to_rad(x)),
|
||
"asin": lambda x: _from_rad(math.asin(x)),
|
||
"acos": lambda x: _from_rad(math.acos(x)),
|
||
"atan": lambda x: _from_rad(math.atan(x)),
|
||
"sqrt": math.sqrt,
|
||
"log": math.log10,
|
||
"ln": math.log,
|
||
"fact": math.factorial,
|
||
"abs": abs,
|
||
}
|
||
return eval(expr, safe_ns) # noqa: S307
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = Calculator()
|
||
app.mainloop()
|