Загрузка данных
import customtkinter as ctk
import math
class CalculatorApp(ctk.CTk):
def __init__(self):
super().__init__()
# Настройка окна
self.title("Калькулятор")
self.geometry("360x680")
self.resizable(False, False)
ctk.set_appearance_mode("dark") # Темная тема как в Apple
# Цвета (в стиле Apple Calculator)
self.COLOR_BG = "#000000"
self.COLOR_DISPLAY_BG = "#000000"
self.COLOR_BTN_NUM = "#333333"
self.COLOR_BTN_UTIL = "#A5A5A5"
self.COLOR_BTN_OP = "#FF9F0A"
self.COLOR_TEXT_NUM = "#FFFFFF"
self.COLOR_TEXT_UTIL = "#000000"
self.configure(fg_color=self.COLOR_BG)
# Переменные состояния
self.current = "0"
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = False
self.memory = 0.0
self.history = []
self.symbols = {"+": "+", "-": "−", "*": "×", "/": "÷"}
self.setup_ui()
# Привязка клавиатуры
self.bind("<Key>", self.handle_keypress)
def setup_ui(self):
# --- Дисплей ---
self.display_frame = ctk.CTkFrame(self, fg_color=self.COLOR_DISPLAY_BG)
self.display_frame.pack(fill="x", padx=20, pady=(30, 10))
self.mem_badge = ctk.CTkLabel(self.display_frame, text="M", font=("Arial", 14), text_color="#FF9F0A")
self.mem_badge.pack(anchor="ne", padx=10, pady=0)
self.mem_badge.pack_forget() # Скрываем по умолчанию
self.expr_label = ctk.CTkLabel(self.display_frame, text="", font=("Arial", 18), text_color="#A5A5A5", anchor="e")
self.expr_label.pack(fill="x", padx=10, pady=0)
self.result_label = ctk.CTkLabel(self.display_frame, text="0", font=("Arial", 56), text_color="#FFFFFF", anchor="e")
self.result_label.pack(fill="x", padx=10, pady=(0, 10))
# --- Память ---
self.mem_frame = ctk.CTkFrame(self, fg_color="transparent")
self.mem_frame.pack(fill="x", padx=15, pady=5)
mem_buttons = [("MC", "mc"), ("MR", "mr"), ("M+", "m+"), ("M-", "m-")]
for i, (text, cmd) in enumerate(mem_buttons):
btn = ctk.CTkButton(self.mem_frame, text=text, font=("Arial", 14), fg_color="transparent",
text_color="#A5A5A5", hover_color="#333333", corner_radius=20, width=60, height=30,
command=lambda c=cmd: self.memory_action(c))
btn.grid(row=0, column=i, padx=5, sticky="ew")
self.mem_frame.grid_columnconfigure(i, weight=1)
# --- Клавиатура ---
self.grid_frame = ctk.CTkFrame(self, fg_color="transparent")
self.grid_frame.pack(fill="both", expand=True, padx=15, pady=10)
buttons_layout = [
("AC", "util", "clear"), ("⌫", "util", "back"), ("%", "util", "percent"), ("÷", "op", "/"),
("7", "num", "7"), ("8", "num", "8"), ("9", "num", "9"), ("×", "op", "*"),
("4", "num", "4"), ("5", "num", "5"), ("6", "num", "6"), ("−", "op", "-"),
("1", "num", "1"), ("2", "num", "2"), ("3", "num", "3"), ("+", "op", "+"),
("±", "util", "sign"), ("0", "num", "0"), (".", "num", "dot"), ("=", "op", "equals")
]
for i, (text, btn_type, action) in enumerate(buttons_layout):
row = i // 4
col = i % 4
fg_col = self.COLOR_BTN_NUM
text_col = self.COLOR_TEXT_NUM
if btn_type == "util":
fg_col = self.COLOR_BTN_UTIL
text_col = self.COLOR_TEXT_UTIL
elif btn_type == "op":
fg_col = self.COLOR_BTN_OP
text_col = self.COLOR_TEXT_NUM
btn = ctk.CTkButton(self.grid_frame, text=text, font=("Arial", 28),
fg_color=fg_col, text_color=text_col,
hover_color=self.adjust_color(fg_col),
corner_radius=35, width=75, height=75,
command=lambda a=action, t=btn_type: self.button_click(a, t))
btn.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
for i in range(4):
self.grid_frame.grid_columnconfigure(i, weight=1)
for i in range(5):
self.grid_frame.grid_rowconfigure(i, weight=1)
# --- История ---
self.history_frame = ctk.CTkFrame(self, fg_color="transparent", height=80)
self.history_frame.pack(fill="x", padx=20, pady=(0, 20))
self.render_history()
def adjust_color(self, hex_color):
# Простое затемнение цвета для эффекта наведения
return hex_color + "CC" if hex_color != "transparent" else "#333333"
def update_display(self):
text = self.shown()
self.result_label.configure(text=text)
# Динамический размер шрифта
if len(text) > 18:
self.result_label.configure(font=("Arial", 32))
elif len(text) > 12:
self.result_label.configure(font=("Arial", 40))
else:
self.result_label.configure(font=("Arial", 56))
if self.operator is not None and self.previous is not None:
self.expr_label.configure(text=f"{self.fmt(self.previous)} {self.symbols[self.operator]}")
else:
self.expr_label.configure(text="")
if self.memory != 0:
self.mem_badge.pack(anchor="ne", padx=10, pady=0)
else:
self.mem_badge.pack_forget()
def render_history(self):
for widget in self.history_frame.winfo_children():
widget.destroy()
if not self.history:
empty_lbl = ctk.CTkLabel(self.history_frame, text="История пуста", text_color="#555555", font=("Arial", 12))
empty_lbl.pack(pady=10)
return
for item in self.history[:4]:
btn = ctk.CTkButton(self.history_frame, text=f"{item['expr']} = {item['res']}",
font=("Arial", 14), fg_color="transparent", text_color="#A5A5A5",
hover_color="#222222", anchor="w", height=25,
command=lambda raw=item['raw']: self.load_history(raw))
btn.pack(fill="x", pady=1)
def load_history(self, raw_val):
self.current = raw_val
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = True
self.update_display()
def fmt(self, n):
if not math.isfinite(n):
return "Ошибка"
a = abs(n)
if a != 0 and (a >= 1e12 or a < 1e-8):
return f"{n:.6e}".replace(".", ",").replace("e+", "e").replace("e-", "e−")
# Форматирование с пробелами для тысяч и запятой для дробей
formatted = f"{n:,.10f}".rstrip('0').rstrip('.')
if '.' in formatted:
formatted = formatted.replace('.', ',')
# Заменяем запятые (разделители тысяч) на пробелы
return formatted.replace(',', ' ')
def shown(self):
if self.current == "Ошибка":
return self.current
if self.just_evaluated:
return self.fmt(float(self.current))
return self.current.replace('.', ',')
def set_error(self):
self.current = "Ошибка"
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = True
self.update_display()
def input_digit(self, digit):
if self.current == "Ошибка":
self.current = "0"
if self.waiting or self.just_evaluated:
self.current = digit
self.waiting = False
self.just_evaluated = False
else:
if self.current == "0":
self.current = digit
elif len(self.current.replace("-", "").replace(".", "")) < 12:
self.current += digit
self.update_display()
def input_dot(self):
if self.current == "Ошибка":
self.current = "0"
if self.waiting or self.just_evaluated:
self.current = "0."
self.waiting = False
self.just_evaluated = False
elif "." not in self.current:
self.current += "."
self.update_display()
def toggle_sign(self):
if self.current in ["Ошибка", "0"]:
return
self.current = self.current[1:] if self.current.startswith("-") else "-" + self.current
if self.current == "-0":
self.current = "0"
self.update_display()
def percent(self):
try:
x = float(self.current)
except ValueError:
return
if self.previous is not None and self.operator in ["+", "-"]:
x = self.previous * x / 100
else:
x = x / 100
x = round(x, 10)
self.current = str(x)
self.waiting = False
self.just_evaluated = False
self.update_display()
def resolve(self, b):
if self.previous is None or self.operator is None:
return False
try:
if self.operator == "+":
r = self.previous + b
elif self.operator == "-":
r = self.previous - b
elif self.operator == "*":
r = self.previous * b
elif self.operator == "/":
if b == 0:
self.set_error()
return False
r = self.previous / b
else:
return False
r = round(r, 10)
if not math.isfinite(r) or abs(r) > 1e15:
self.set_error()
return False
# Добавление в историю
self.history.insert(0, {
"expr": f"{self.fmt(self.previous)} {self.symbols[self.operator]} {self.fmt(b)}",
"res": self.fmt(r),
"raw": str(r)
})
self.history = self.history[:4]
self.render_history()
self.current = str(r)
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = True
self.update_display()
return True
except Exception:
self.set_error()
return False
def set_operator(self, op):
if self.current == "Ошибка":
return
if self.operator is not None and not self.waiting:
if not self.resolve(float(self.current)):
return
else:
self.just_evaluated = False
self.previous = float(self.current)
self.operator = op
self.waiting = True
self.update_display()
def equals(self):
if self.current == "Ошибка" or self.operator is None or self.previous is None:
return
b = self.previous if self.waiting else float(self.current)
self.resolve(b)
def backspace(self):
if self.current == "Ошибка" or self.just_evaluated:
self.clear_all()
return
if self.waiting:
self.operator = None
self.previous = None
self.waiting = False
self.update_display()
return
self.current = self.current[:-1] if len(self.current) > 1 else "0"
if self.current in ["-", "-0"]:
self.current = "0"
self.update_display()
def clear_all(self):
self.current = "0"
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = False
self.update_display()
def memory_action(self, action):
try:
x = float(self.current)
except ValueError:
return
if action == "mc":
self.memory = 0.0
elif action == "mr":
if math.isfinite(self.memory):
self.current = str(self.memory)
self.previous = None
self.operator = None
self.waiting = False
self.just_evaluated = True
elif action == "m+":
self.memory = round(self.memory + x, 10)
elif action == "m-":
self.memory = round(self.memory - x, 10)
self.update_display()
def button_click(self, action, btn_type):
if btn_type == "num" or (btn_type == "util" and action in ["sign", "dot"]):
if action == "dot": self.input_dot()
elif action == "sign": self.toggle_sign()
else: self.input_digit(action)
elif btn_type == "op":
if action == "equals": self.equals()
else: self.set_operator(action)
elif btn_type == "util":
if action == "clear": self.clear_all()
elif action == "back": self.backspace()
elif action == "percent": self.percent()
def handle_keypress(self, event):
k = event.char
if k.isdigit():
self.input_digit(k)
elif k in [".", ","]:
self.input_dot()
elif k in ["+", "-", "*", "/"]:
self.set_operator(k)
elif k in ["\r", "="]:
self.equals()
elif event.keysym == "BackSpace":
self.backspace()
elif event.keysym in ["Escape", "Delete"]:
self.clear_all()
elif k == "%":
self.percent()
if __name__ == "__main__":
app = CalculatorApp()
app.mainloop()