程序设计实践 计算器
import tkinter as tk
from tkinter import messagebox
from math import sqrt
class CalcApp:
def __init__(self, root):
self.root = root
self.root.title("综合计算器")
self.mode = tk.StringVar(value="普通计算器")
self.create_menu()
self.norm_frame = tk.Frame(self.root)
self.mort_frame = tk.Frame(self.root)
self.create_norm_calc(self.norm_frame)
self.create_mort_calc(self.mort_frame)
self.norm_frame.grid(row=1, column=0, sticky="nsew")
self.mort_frame.grid(row=1, column=0, sticky="nsew")
self.root.grid_rowconfigure(1, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.expr = ""
def create_menu(self):
modes = ["普通计算器", "贷款计算器"]
tk.OptionMenu(self.root, self.mode, *modes, command=self.switch_mode).grid(row=0, column=0, padx=10, pady=10)
def switch_mode(self, mode):
if mode == "普通计算器":
self.mort_frame.grid_forget()
self.norm_frame.grid(row=1, column=0, padx=10, pady=10)
else:
self.norm_frame.grid_forget()
self.mort_frame.grid(row=1, column=0, padx=10, pady=10)
def create_norm_calc(self, frame):
self.res = tk.StringVar()
self.display = tk.Text(frame, height=2, font=('Arial', 20), bd=10, wrap=tk.WORD)
self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10, sticky="nsew")
btns = [
('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
('0', 4, 0), ('.', 4, 1), ('+', 4, 2), ('=', 4, 3),
('C', 5, 0), ('←', 5, 1), ('sqrt', 5, 2), ('%', 5, 3),
('(', 6, 0), (')', 6, 1), ('^', 6, 2), ('1/x', 6, 3)
]
for (text, row, col) in btns:
btn = tk.Button(frame, text=text, padx=20, pady=20, font=('Arial', 18),
command=lambda t=text: self.on_btn_click(t))
btn.grid(row=row, column=col, sticky="nsew")
for i in range(7):
frame.grid_rowconfigure(i, weight=1)
for i in range(4):
frame.grid_columnconfigure(i, weight=1)
def tokenize(self, expr):
tokens = []
current_num = []
i = 0
while i < len(expr):
char = expr[i]
if char.isdigit() or char == '.':
current_num.append(char)
else:
if current_num:
tokens.append(''.join(current_num))
current_num = []
if char in '+-*/^()':
tokens.append(char)
elif char == 's' and expr[i:i + 4] == 'sqrt':
tokens.append('sqrt')
i += 3
elif char == '1' and i + 1 < len(expr) and expr[i + 1] == '/':
tokens.append('1/x')
i += 1
elif char == '%':
tokens.append('%')
i += 1
if current_num:
tokens.append(''.join(current_num))
return tokens
def precedence(self, op) :
if op == 'sqrt':
return 4
if op in ('+', '-'):
return 1
if op in ('*', '/', '%'):
return 2
if op == '^':
return 3
return 0
def apply_op(self, a, b, op):
if op == '+':
return a + b
if op == '-':
return a - b
if op == '*':
return a * b
if op == '/':
return a / b
if op == '^':
return a ** b
if op == '%':
return a % b
if op == '1/x':
return 1 / b
def perform_op(self):
operator = self.op_stack.pop()
if operator == 'sqrt':
val = self.num_stack.pop()
self.num_stack.append(sqrt(val))
elif operator == '1/x':
val = self.num_stack.pop()
self.num_stack.append(1 / val)
else:
right = self.num_stack.pop()
left = self.num_stack.pop()
self.num_stack.append(self.apply_op(left, right, operator))
def evaluate(self):
self.num_stack = []
self.op_stack = []
tokens = self.tokenize(self.expr)
for token in tokens:
if token.isdigit() or '.' in token:
self.num_stack.append(float(token))
elif token in '+-*/^%':
while (self.op_stack and self.precedence(self.op_stack[-1]) >= self.precedence(token)):
self.perform_op()
self.op_stack.append(token)
elif token == '(':
self.op_stack.append(token)
elif token == ')':
while self.op_stack and self.op_stack[-1] != '(':
self.perform_op()
self.op_stack.pop() # 弹出'('
elif token == 'sqrt':
self.op_stack.append('sqrt')
elif token == '1/x':
self.op_stack.append('1/x')
while self.op_stack:
self.perform_op()
self.expr = str(self.num_stack.pop())
def on_btn_click(self, char):
if char == '=':
try:
self.evaluate()
except Exception:
self.expr = "Error"
elif char == 'C':
self.expr = ""
elif char == '←':
self.expr = self.expr[:-1]
else:
self.expr += str(char)
self.display.delete('1.0', tk.END)
self.display.insert(tk.END, self.expr)
def create_mort_calc(self, frame):
self.pay_type = tk.StringVar(value="等额本息")
self.years = tk.StringVar()
self.amount = tk.StringVar()
self.rate = tk.StringVar()
self.monthly_pay = tk.StringVar()
self.total_int = tk.StringVar()
self.total_pay = tk.StringVar()
tk.Label(frame, text="还款方式:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
tk.Radiobutton(frame, text="等额本息", variable=self.pay_type, value="等额本息").grid(row=0, column=1)
tk.Radiobutton(frame, text="等额本金", variable=self.pay_type, value="等额本金").grid(row=0, column=2)
tk.Label(frame, text="贷款年限(年):").grid(row=1, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.years).grid(row=1, column=1, columnspan=2)
tk.Label(frame, text="贷款金额(万元):").grid(row=2, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.amount).grid(row=2, column=1, columnspan=2)
tk.Label(frame, text="贷款利率(%):").grid(row=3, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.rate).grid(row=3, column=1, columnspan=2)
tk.Button(frame, text="计算", command=self.calc_mort).grid(row=4, column=0, columnspan=2, pady=10)
tk.Button(frame, text="重置", command=self.reset_mort).grid(row=4, column=2, pady=10)
tk.Label(frame, text="月均还款(元):").grid(row=5, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.monthly_pay, state='readonly').grid(row=5, column=1, columnspan=2)
tk.Label(frame, text="利息总额(元):").grid(row=6, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.total_int, state='readonly').grid(row=6, column=1, columnspan=2)
tk.Label(frame, text="还款总额(元):").grid(row=7, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.total_pay, state='readonly').grid(row=7, column=1, columnspan=2)
def calc_mort(self):
"""贷款计算器计算逻辑"""
try:
yrs = int(self.years.get())
princ = float(self.amount.get()) * 10000 # 万元转换为元
rate_yr = float(self.rate.get()) / 100 # 转换为小数利率
months = yrs * 12 # 贷款总月数
rate_mth = rate_yr / 12 # 月利率
if self.pay_type.get() == "等额本息":
# 等额本息公式
m_pay = princ * rate_mth * ((1 + rate_mth) ** months) / (((1 + rate_mth) ** months) - 1)
total_pay = m_pay * months
total_int = total_pay - princ
else:
# 等额本金公式
m_pay = princ / months + (princ * rate_mth)
total_pay = (months + 1) * princ * rate_mth / 2 + princ
total_int = total_pay - princ
self.monthly_pay.set(f"{m_pay:.2f}")
self.total_int.set(f"{total_int:.2f}")
self.total_pay.set(f"{total_pay:.2f}")
except ValueError:
messagebox.showerror("输入错误", "请输入有效的数字")
def reset_mort(self):
"""重置贷款计算器输入和输出"""
self.years.set("")
self.amount.set("")
self.rate.set("")
self.monthly_pay.set("")
self.total_int.set("")
self.total_pay.set("")
if __name__ == "__main__":
root = tk.Tk()
app = CalcApp(root)
root.mainloop()
这段代码实现了一个综合计算器应用程序,它使用Python的Tkinter库创建了一个图形用户界面(GUI)。该计算器包含两个主要功能:普通计算器和贷款计算器。下面是对代码的详细解释:
1. 导入模块
import tkinter as tk
from tkinter import messagebox
from math import sqrt
tkinter
:用于创建GUI应用程序。messagebox
:用于显示消息框。sqrt
:用于计算平方根。
2. 定义计算器应用程序类
class CalcApp:
def __init__(self, root):
self.root = root
self.root.title("综合计算器")
self.mode = tk.StringVar(value="普通计算器")
self.create_menu()
self.norm_frame = tk.Frame(self.root)
self.mort_frame = tk.Frame(self.root)
self.create_norm_calc(self.norm_frame)
self.create_mort_calc(self.mort_frame)
self.norm_frame.grid(row=1, column=0, padx=10, pady=10)
__init__
方法初始化应用程序,设置窗口标题,创建菜单和两个计算器界面(普通计算器和贷款计算器)。
3. 创建菜单
def create_menu(self):
modes = ["普通计算器", "贷款计算器"]
tk.OptionMenu(self.root, self.mode, *modes, command=self.switch_mode).grid(row=0, column=0, padx=10, pady=10)
- 创建一个下拉菜单,用于选择计算器模式(普通计算器或贷款计算器)。
4. 切换计算器模式
def switch_mode(self, mode):
if mode == "普通计算器":
self.mort_frame.grid_forget()
self.norm_frame.grid(row=1, column=0, padx=10, pady=10)
else:
self.norm_frame.grid_forget()
self.mort_frame.grid(row=1, column=0, padx=10, pady=10)
- 根据选择的模式切换不同的界面。
5. 创建普通计算器界面
def create_norm_calc(self, frame):
self.expr = ""
self.res = tk.StringVar()
self.display = tk.Entry(frame, textvariable=self.res, font=('Arial', 20), bd=10, insertwidth=4, width=14, borderwidth=4)
self.display.grid(row=0, column=0, columnspan=4)
btns = [
('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
('0', 4, 0), ('.', 4, 1), ('+', 4, 2), ('=', 4, 3),
('C', 5, 0), ('←', 5, 1), ('sqrt(', 5, 2), ('%', 5, 3),
('(', 6, 0), (')', 6, 1), ('^', 6, 2), ('1/x(', 6, 3)
]
for (text, row, col) in btns:
btn = tk.Button(frame, text=text, padx=20, pady=20, font=('Arial', 18), command=lambda t=text: self.on_btn_click(t))
btn.grid(row=row, column=col, sticky="nsew")
for i in range(7):
frame.grid_rowconfigure(i, weight=1)
for i in range(4):
frame.grid_columnconfigure(i, weight=1)
- 创建普通计算器的按钮布局,包括数字按钮、运算符按钮和功能按钮。
6. 普通计算器按钮点击事件
def on_btn_click(self, char):
if char == '=':
try:
self.evaluate()
except Exception:
self.expr = "Error"
elif char == 'C':
self.expr = ""
elif char == '←':
self.expr = self.expr[:-1]
else:
self.expr += str(char)
self.res.set(self.expr)
- 处理按钮点击事件,更新表达式并显示结果。
7. 表达式计算
def tokenize(self, expr):
tokens = []
current_num = []
i = 0
while i < len(expr):
char = expr[i]
if char.isdigit() or char == '.':
current_num.append(char)
else:
if current_num:
tokens.append(''.join(current_num))
current_num = []
if char in '+-*/^()':
tokens.append(char)
elif char == 's' and expr[i:i+4] == 'sqrt':
tokens.append('sqrt')
i += 3
elif char == '1' and i + 1 < len(expr) and expr[i + 1] == '/':
tokens.append('1/x')
i += 1
elif char == '%':
tokens.append('%')
i += 1
if current_num:
tokens.append(''.join(current_num))
return tokens
def precedence(self, op):
if op in ('+', '-'):
return 1
if op in ('*', '/'):
return 2
if op == '^':
return 3
if op == '%':
return 2
return 0
def apply_op(self, a, b, op):
if op == '+':
return a + b
if op == '-':
return a - b
if op == '*':
return a * b
if op == '/':
return a / b
if op == '^':
return a ** b
if op == '%':
return a % b
if op == '1/x':
return 1 / b
def perform_op(self):
operator = self.op_stack.pop()
if operator == 'sqrt':
val = self.num_stack.pop()
self.num_stack.append(sqrt(val))
elif operator == '1/x':
val = self.num_stack.pop()
self.num_stack.append(1 / val)
else:
right = self.num_stack.pop()
left = self.num_stack.pop()
self.num_stack.append(self.apply_op(left, right, operator))
def evaluate(self):
self.num_stack = []
self.op_stack = []
tokens = self.tokenize(self.expr)
for token in tokens:
if token.isdigit() or '.' in token:
self.num_stack.append(float(token))
elif token in '+-*/^%':
while (self.op_stack and self.precedence(self.op_stack[-1]) >= self.precedence(token)):
self.perform_op()
self.op_stack.append(token)
elif token == '(':
self.op_stack.append(token)
elif token == ')':
while self.op_stack and self.op_stack[-1] != '(':
self.perform_op()
self.op_stack.pop()
elif token == 'sqrt':
self.op_stack.append('sqrt')
elif token == '1/x':
self.op_stack.append('1/x')
while self.op_stack:
self.perform_op()
self.expr = str(self.num_stack.pop())
- 将表达式转换为标记列表,计算表达式的值。
8. 创建贷款计算器界面
def create_mort_calc(self, frame):
self.pay_type = tk.StringVar(value="等额本息")
self.years = tk.StringVar()
self.amount = tk.StringVar()
self.rate = tk.StringVar()
self.monthly_pay = tk.StringVar()
self.total_int = tk.StringVar()
self.total_pay = tk.StringVar()
tk.Label(frame, text="还款方式:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
tk.Radiobutton(frame, text="等额本息", variable=self.pay_type, value="等额本息").grid(row=0, column=1)
tk.Radiobutton(frame, text="等额本金", variable=self.pay_type, value="等额本金").grid(row=0, column=2)
tk.Label(frame, text="贷款年限(年):").grid(row=1, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.years).grid(row=1, column=1, columnspan=2)
tk.Label(frame, text="贷款金额(万元):").grid(row=2, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.amount).grid(row=2, column=1, columnspan=2)
tk.Label(frame, text="贷款利率(%):").grid(row=3, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.rate).grid(row=3, column=1, columnspan=2)
tk.Button(frame, text="计算", command=self.calc_mort).grid(row=4, column=0, columnspan=2, pady=10)
tk.Button(frame, text="重置", command=self.reset_mort).grid(row=4, column=2, pady=10)
tk.Label(frame, text="月均还款(元):").grid(row=5, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.monthly_pay, state='readonly').grid(row=5, column=1, columnspan=2)
tk.Label(frame, text="利息总额(元):").grid(row=6, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.total_int, state='readonly').grid(row=6, column=1, columnspan=2)
tk.Label(frame, text="还款总额(元):").grid(row=7, column=0, padx=5, pady=5, sticky='w')
tk.Entry(frame, textvariable=self.total_pay, state='readonly').grid(row=7, column=1, columnspan=2)
- 创建贷款计算器的UI,包括输入框和按钮。
9. 贷款计算器计算逻辑
def calc_mort(self):
try:
yrs = int(self.years.get())
princ = float(self.amount.get()) * 10000
rate_yr = float(self.rate.get()) / 100
months = yrs * 12
rate_mth = rate_yr / 12
if self.pay_type.get() == "等额本息":
m_pay = princ * rate_mth * ((1 + rate_mth) ** months) / (((1 + rate_mth) ** months) - 1)
total_pay = m_pay * months
total_int = total_pay - princ
else:
m_pay = princ / months + (princ * rate_mth)
total_pay = (months + 1) * princ * rate_mth / 2 + princ
total_int = total_pay - princ
self.monthly_pay.set(f"{m_pay:.2f}")
self.total_int.set(f"{total_int:.2f}")
self.total_pay.set(f"{total_pay:.2f}")
except ValueError:
messagebox.showerror("输入错误", "请输入有效的数字")
def reset_mort(self):
self.years.set("")
self.amount.set("")
self.rate.set("")
self.monthly_pay.set("")
self.total_int.set("")
self.total_pay.set("")
- 计算贷款的月均还款、利息总额和还款总额,并处理输入错误。
10. 运行应用程序
if __name__ == "__main__":
root = tk.Tk()
app = CalcApp(root)
root.mainloop()
- 创建Tkinter窗口并运行应用程序。
注意事项
- 该代码实现了基本的计算器功能,但未进行全面的错误处理和输入验证。
- 可以根据需要添加更多的功能和改进用户体验。
这段代码是贷款计算器中的一个方法 calc_mort
,用于计算贷款的相关信息,包括每月还款额、利息总额和还款总额。下面是对每一部分的详细解析:
方法说明
def calc_mort(self):
"""贷款计算器计算逻辑"""
- 定义了一个名为
calc_mort
的方法,这个方法负责贷款计算器的核心计算逻辑。
输入数据处理
try:
yrs = int(self.years.get())
princ = float(self.amount.get()) * 10000 # 万元转换为元
rate_yr = float(self.rate.get()) / 100 # 转换为小数利率
months = yrs * 12 # 贷款总月数
rate_mth = rate_yr / 12 # 月利率
- 使用
try
块来捕获潜在的输入错误。 yrs
:获取用户输入的贷款年限,并转换为整数。princ
:获取用户输入的贷款金额(单位为万元),并转换为浮点数,随后乘以 10,000 转换为元。rate_yr
:获取用户输入的年利率(百分比),并将其转换为小数形式(例如,将 5% 转换为 0.05)。months
:计算贷款的总月份数(年数乘以 12)。rate_mth
:计算月利率(年利率除以 12)。
计算每月还款额及总费用
if self.pay_type.get() == "等额本息":
# 等额本息公式
m_pay = princ * rate_mth * ((1 + rate_mth) ** months) / (((1 + rate_mth) ** months) - 1)
total_pay = m_pay * months
total_int = total_pay - princ
else:
# 等额本金公式
m_pay = princ / months + (princ * rate_mth)
total_pay = (months + 1) * princ * rate_mth / 2 + princ
total_int = total_pay - princ
- 根据用户选择的还款方式(
等额本息
或等额本金
),进行不同的计算:-
等额本息:
m_pay
:使用等额本息公式计算每月还款额。公式计算基于本金、月利率和总月数。total_pay
:计算还款总额(每月还款额乘以总月数)。total_int
:计算总利息(总还款额减去本金)。
-
等额本金:
m_pay
:计算每月还款额(每月本金 + 利息)。total_pay
:计算还款总额(根据每月本金和利息的分配计算)。total_int
:计算总利息(总还款额减去本金)。
-
更新界面显示
self.monthly_pay.set(f"{m_pay:.2f}")
self.total_int.set(f"{total_int:.2f}")
self.total_pay.set(f"{total_pay:.2f}")
- 将计算结果更新到界面的相应文本框,格式化为小数点后两位的字符串。
错误处理
except ValueError:
messagebox.showerror("输入错误", "请输入有效的数字")
- 如果在输入数据转换过程中发生
ValueError
(例如用户输入非数字字符),则弹出错误提示框,提示用户输入有效的数字。
总结
该方法实现了贷款计算器的核心功能,能够根据用户输入的贷款年限、金额和利率,计算出每月还款额、利息总额和还款总额,并在界面上显示这些结果。同时,它也考虑了输入验证,以提高用户体验。
要使页面内部组件在调整窗口大小时跟随变化,可以使用 Tkinter 的 grid
或 pack
布局管理器,并设置适当的权重。以下是如何做到这一点的步骤:
修改布局以响应窗口大小
- 设置行和列的权重:使用
grid_rowconfigure
和grid_columnconfigure
方法为各行和列设置权重,使其在窗口调整时能动态改变大小。 - 使用
sticky
属性:设置sticky
属性以使组件填充整个单元格。
更新 create_norm_calc
方法
以下是更新后的 create_norm_calc
方法,包含上述更改:
def create_norm_calc(self, frame):
"""创建普通计算器界面"""
self.expr = ""
self.res = tk.StringVar()
# 显示框,使用 Text 组件以支持多行
self.display = tk.Text(frame, height=2, font=('Arial', 20), bd=10, wrap=tk.WORD)
self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10, sticky="nsew")
# 按钮布局
btns = [
('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
('0', 4, 0), ('.', 4, 1), ('+', 4, 2), ('=', 4, 3),
('C', 5, 0), ('←', 5, 1), ('sqrt', 5, 2), ('%', 5, 3),
('(', 6, 0), (')', 6, 1), ('^', 6, 2), ('1/x', 6, 3)
]
# 创建按钮
for (text, row, col) in btns:
btn = tk.Button(frame, text=text, padx=20, pady=20, font=('Arial', 18),
command=lambda t=text: self.on_btn_click(t))
btn.grid(row=row, column=col, sticky="nsew")
# 设置窗口的行列权重,使其根据窗口大小自动缩放
for i in range(7):
frame.grid_rowconfigure(i, weight=1)
for i in range(4):
frame.grid_columnconfigure(i, weight=1)
确保框架可扩展
确保在主 CalcApp
类的构造函数中为 norm_frame
和 mort_frame
设置权重,以便它们可以扩展:
def __init__(self, root):
self.root = root
self.root.title("综合计算器")
# 初始化变量
self.mode = tk.StringVar(value="普通计算器")
# 创建菜单选择模式
self.create_menu()
# 初始化两个页面,分别是普通计算器和贷款计算器
self.norm_frame = tk.Frame(self.root)
self.mort_frame = tk.Frame(self.root)
self.create_norm_calc(self.norm_frame)
self.create_mort_calc(self.mort_frame)
# 设置权重,使页面可以扩展
self.norm_frame.grid(row=1, column=0, sticky="nsew")
self.mort_frame.grid(row=1, column=0, sticky="nsew")
# 设置主窗口的行列权重
self.root.grid_rowconfigure(1, weight=1)
self.root.grid_columnconfigure(0, weight=1)
# 显示默认页面
self.norm_frame.grid(row=1, column=0, padx=10, pady=10)
总结
sticky="nsew"
: 这个属性使得组件在其单元格中扩展,确保它们在窗口缩放时可以跟随变化。- 行和列权重: 使用
grid_rowconfigure
和grid_columnconfigure
来允许组件根据窗口大小自动调整其大小。
这些更改将使你的计算器界面在调整窗口大小时更具响应性和灵活性。