Add scientific calculator with modern Catppuccin Mocha UI
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>
This commit is contained in:
commit
05fdd13cd6
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Scientific Calculator
|
||||
|
||||
A desktop scientific calculator built with Python and Tkinter.
|
||||
|
||||
## Features
|
||||
|
||||
**Basic arithmetic**
|
||||
- Addition, subtraction, multiplication, division
|
||||
- Modulo (`%`), power (`xʸ`), parentheses
|
||||
- Negate (`±`), decimal point
|
||||
|
||||
**Scientific functions**
|
||||
- Trigonometry: `sin`, `cos`, `tan` (degrees)
|
||||
- `√x` — square root
|
||||
- `log` — base-10 logarithm
|
||||
- `ln` — natural logarithm
|
||||
- `x!` — factorial
|
||||
- Constants: `π` and `e`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+ (Tkinter is included in the standard library)
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
python calculator.py
|
||||
```
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `0-9`, `.` | Digits |
|
||||
| `+`, `-`, `*`, `/`, `%` | Operators |
|
||||
| `(`, `)` | Parentheses |
|
||||
| `Enter` | Evaluate |
|
||||
| `Backspace` | Delete last character |
|
||||
| `Esc` | Clear all |
|
||||
363
calculator.py
Normal file
363
calculator.py
Normal file
@ -0,0 +1,363 @@
|
||||
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()
|
||||
Loading…
Reference in New Issue
Block a user