Untitled

 avatar
Lattuse
plain_text
9 months ago
10 kB
22
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