You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
19 KiB

import math
from collections import OrderedDict
from time import sleep
from ttkbootstrap import *
import tkinter.font as tkFont
from tkinter import messagebox
from tkinter.ttk import Treeview
import sympy
from numpy import arange
from functionUtil import FunctionUtil
# 获取文本的宽度和高度
def get_text_dimensions(canvas, text, font):
text_id = canvas.create_text(0, 0, text=text, font=font, anchor="center")
bbox = canvas.bbox(text_id)
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
canvas.delete(text_id)
return width, height
# 在画布中央绘制文本
def center_text(canvas, text, font):
canvas_width = int(canvas.cget("width"))
text_width, text_height = get_text_dimensions(canvas, text, font)
x = canvas_width // 2
y = 200
canvas.create_text(x, y, text=text, font=font, anchor="center")
class Graph(Canvas):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.width = int(self.cget('width'))
self.height = int(self.cget('height'))
self.origin = (self.width / 2, self.height / 2)
self.bili_x = 20
self.bili_y = 20
self.draw_axis()
self.draw_scale()
def draw_axis(self):
"""
绘制坐标轴
"""
self.delete("all")
self.create_line(0, self.origin[1], self.width, self.origin[1], fill='black', arrow=LAST) # 一象限xy轴
self.create_line(self.origin[0], 0, self.origin[0], self.height, fill='black', arrow=FIRST)
def draw_scale(self):
"""
绘制刻度值
"""
for i in range(-math.ceil((self.origin[0] / self.bili_x)) + 1, math.ceil((self.width - self.origin[0]) / self.bili_x)):
j = i * self.bili_x
if (i % 10 == 0):
self.create_line(j + self.origin[0], self.origin[1], j + self.origin[0], self.origin[1] - 5, fill='black')
self.create_text(j + self.origin[0], self.origin[1] + 10, text=i)
for i in range(-math.ceil((self.height - self.origin[1]) / self.bili_y) + 1, math.ceil((self.origin[1] / self.bili_y))):
j = -(i * self.bili_y)
if (i == 0):
continue
elif (i % 2 == 0):
self.create_line(self.origin[0], j + self.origin[1], self.origin[0] + 5, j + self.origin[1], fill='black')
self.create_text(self.origin[0] - 10, j + self.origin[1], text=i)
def switch_quadrant(self, quadrant):
"""
切换象限
"""
if quadrant == 1:
self.origin = (50, self.height - 50)
else:
self.origin = (self.width / 2, self.height / 2)
self.draw_axis()
self.draw_scale()
def get_xy(self):
return -math.ceil((self.origin[0] / self.bili_x)) + 1, math.ceil((self.width - self.origin[0]) / self.bili_x)
def draw_graph(self, func, draw_precision=0.1, count=1000, bili_x=20, bili_y=40, c='blue'):
'xmin,xmax 自变量的取值范围; c 图像颜色'
'x0,y0 原点坐标 w,h 横纵轴半长 draw_precision 步进'
self.bili_x, self.bili_y = int(bili_x), int(bili_y)
self.draw_axis()
self.draw_scale()
w1, w2 = self.bili_x, self.bili_y # w1,w2为自变量和函数值在横纵轴上的放大倍数
xmin, xmax = self.get_xy()
co2 = []
try:
for x in arange(xmin, xmax, draw_precision): # draw_precision----画图精度
y = sympy.sympify(func, convert_xor=True).subs("x", x).evalf()
coord = self.origin[0] + w1 * x, self.origin[1] - w2 * y, self.origin[0] + w1 * x + 1, self.origin[1] - w2 * y + 1
if abs(coord[1]) < self.height: # 超过w,h就截断
co2.append((self.origin[0] + w1 * x, self.origin[1] - w2 * y))
if count is None:
length = len(co2)
else:
length = len(co2[:int(count)])
for i in range(length):
if (draw_precision >= 1):
self.create_line(co2, fill=c, width=1)
if (i + 1 == len(co2)):
break
if (abs(co2[i][1] - co2[i + 1][1]) > 100):
continue
else:
self.create_line(co2[i], co2[i + 1], fill=c, width=1)
sleep(0.01)
self.update()
except Exception as E:
messagebox.showerror("错误", message=f"函数有误!\n{E}")
class ExpressionCanvas(Canvas):
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.node_width = 60
self.node_height = 30
self.x_spacing = 10
self.y_spacing = 70
self.level = {}
def add_node(self, text, x, y, parent=None):
node_width = len(text) * 5 + 20
if node_width < 40:
node_width = 50
# 创建节点
coords = [x - node_width / 2, y, x + node_width / 2, y + self.node_height]
if coords[1] in self.level:
max_x = max(self.level[coords[1]])
if coords[0] <= max_x:
x = max_x + node_width / 2 + self.x_spacing
node_id = self.create_rectangle(x - node_width / 2, y, x + node_width / 2, y + self.node_height,
fill="white")
sleep(0.2)
self.update()
elif text == "x":
x = self.coords(parent)[0] + node_width / 2
node_id = self.create_rectangle(x - node_width / 2, y, x + node_width / 2, y + self.node_height,
fill="white")
sleep(0.2)
self.update()
else:
node_id = self.create_rectangle(x - node_width / 2, y, x + node_width / 2, y + self.node_height, fill="white")
sleep(0.2)
self.update()
coords = [x - node_width / 2, y, x + node_width / 2, y + self.node_height]
if coords[1] in self.level:
self.level[coords[1]].append(coords[2])
else:
self.level[coords[1]] = [coords[2]]
text_id = self.create_text(x + 5, y + self.node_height // 2, text=text, anchor="center")
sleep(0.2)
self.update()
# 绘制父节点和当前节点的连线
if parent:
parent_coords = self.coords(parent)
parent_x = parent_coords[0] + (parent_coords[2] - parent_coords[0]) // 2
parent_y = parent_coords[1] + (parent_coords[3] - parent_coords[1]) // 2
self.create_line(parent_x, parent_y + self.node_height // 2, x, y, fill="black")
self.update()
return node_id
def draw_expression(self, expression, x, y, parent=None, sibling_width=0):
# 创建节点并绘制表达式文本
node = self.add_node(str(expression), x, y, parent=parent)
if expression.is_Atom:
return node
# 处理子表达式
num_children = len(expression.args)
total_width = num_children * self.node_width + (num_children - 1) * self.x_spacing
start_x = x - total_width // 2
for subexpr in expression.args:
subnode = self.draw_expression(subexpr, start_x, y + self.y_spacing, parent=node, sibling_width=total_width)
start_x += self.node_width + self.x_spacing
return node
class FunctionDisplay(Frame):
def __init__(self, master, attr):
super().__init__(master)
self.attr = attr
self.font_style = tkFont.Font(family="Lucida Grande", size=30)
self.functions = FunctionUtil()
self.master = master
self.create_window()
def clear_text_canvas(self, event):
def clear_text():
self.text_canvas.delete("all")
self.text_canvas.create_text(20 + 180, 20, text='可识别基本函数', fill='red', font=("Purisa", 25, "bold"))
center_text(self.text_canvas, self.text, ("Lucida Grande", 15))
self.menu = Menu(self, tearoff=False)
self.menu.add_command(label="重置", command=clear_text)
self.menu.post(event.x_root, event.y_root)
def add_function(self):
"""
用户添加函数
"""
input_func = self.func_input.get()
if input_func == "":
messagebox.showwarning("注意", message="请输入函数!")
return
elif input_func.find("=") < 0:
messagebox.showerror("注意", message="添加函数的格式为:\nSinPlusCos(x)=sin(x)+cos(x)")
return
left_var = input_func.split("=")[0]
right_var = input_func.split("=")[1]
try:
function = self.functions.get_function_by_iter(right_var)
sympy.sympify(function, evaluate=False)
except Exception as E:
messagebox.showerror("注意", message="函数解析错误,请仔细检查函数是否正确!")
self.func_input.delete(0, END)
return
if self.functions.check_function_exit(left_var):
result = messagebox.askokcancel(title='标题~', message=f'函数{left_var}已经存在,是否需要覆盖!')
if result:
self.functions.add_function(left_var, right_var)
messagebox.showinfo(title="提示", message="覆盖成功!")
self.func_input.delete(0, END)
return
self.functions.add_function(left_var, right_var)
messagebox.showinfo(title="提示", message="添加成功!")
self.func_input.delete(0, END)
def update_quadrant(self):
"""
更换象限
"""
self.axis_canvas.switch_quadrant(self.quadrant.get())
self.print_function()
def print_function(self):
"""
输出
"""
self.text_canvas.delete("all")
self.text_canvas.level.clear()
input_func = self.func_input.get()
if input_func == "":
messagebox.showwarning("注意", message="请输入函数!")
return
step = eval(self.x_step.get()) if not self.x_step.get() == "" else 0.1
count = self.x_count.get() if not self.x_count.get() == "" else None
x_scale = self.x_scale.get() if not self.x_scale.get() == "" else 20
y_scale = self.y_scale.get() if not self.y_scale.get() == "" else 40
if input_func.find("=") >= 0:
left_var = input_func.split("=")[0]
right_var = input_func.split("=")[1]
if self.functions.check_function_exit(right_var):
func = self.functions.get_function_by_iter(right_var)
self.axis_canvas.draw_graph(func, step, count,
x_scale, y_scale)
self.text_canvas.draw_expression(sympy.sympify(func, evaluate=False, convert_xor=True), int(self.text_canvas.cget("width")) // 2, self.text_canvas.y_spacing)
else:
self.axis_canvas.draw_graph(right_var, step, count,
x_scale, y_scale)
self.text_canvas.draw_expression(sympy.sympify(right_var, evaluate=False, convert_xor=True), int(self.text_canvas.cget("width")) // 2, self.text_canvas.y_spacing)
else:
if self.functions.check_function_exit(input_func):
func = self.functions.get_function_by_iter(input_func)
self.axis_canvas.draw_graph(func, step, count,
x_scale, y_scale)
self.text_canvas.draw_expression(sympy.sympify(func, evaluate=False, convert_xor=True), int(self.text_canvas.cget("width")) // 2, self.text_canvas.y_spacing)
else:
self.axis_canvas.draw_graph(input_func, step, count,
x_scale, y_scale)
self.text_canvas.draw_expression(sympy.sympify(input_func, evaluate=False, convert_xor=True), int(self.text_canvas.cget("width")) // 2, self.text_canvas.y_spacing)
def create_form(self):
bottom_frame = Frame(self.master, width=self.attr["width"], height=self.attr["height"] * 0.3)
self.func_input = Entry(bottom_frame, font=self.font_style, width=40)
self.func_input.grid(row=0, column=0, columnspan=4, padx=30)
self.add_func_button = Button(bottom_frame, text="用户命名并新增基本函数", command=self.add_function)
self.add_func_button.grid(row=0, column=4, padx=10)
Label(bottom_frame, text="x步长", font=("Lucida Grande", 20)).grid(row=1, column=0, padx=30)
self.x_step = Entry(bottom_frame, font=self.font_style, width=10)
self.x_step.grid(row=1, column=1, padx=10, pady=10)
Label(bottom_frame, text="x个数", font=("Lucida Grande", 20)).grid(row=1, column=2, padx=30)
self.x_count = Entry(bottom_frame, font=self.font_style, width=10)
self.x_count.grid(row=1, column=3, padx=10, pady=10)
Label(bottom_frame, text="坐标轴", font=("Lucida Grande", 20)).grid(row=2, column=0, padx=30)
self.quadrant = IntVar()
self.quadrant.set(4)
Radiobutton(bottom_frame, text="四象限", command=self.update_quadrant, value=4, variable=self.quadrant).grid(row=2, column=1, padx=10, pady=10)
Radiobutton(bottom_frame, text="一象限", command=self.update_quadrant, value=1, variable=self.quadrant).grid(row=2, column=2, padx=10, pady=10)
Label(bottom_frame, text="x放大倍数", font=("Lucida Grande", 20)).grid(row=3, column=0, padx=30)
self.x_scale = Entry(bottom_frame, font=self.font_style, width=10)
self.x_scale.grid(row=3, column=1, padx=10, pady=10)
Label(bottom_frame, text="y放大倍数", font=("Lucida Grande", 20)).grid(row=3, column=2, padx=30)
self.y_scale = Entry(bottom_frame, font=self.font_style, width=10)
self.y_scale.grid(row=3, column=3, padx=10, pady=10)
Button(bottom_frame, text="输出",
command=self.print_function, width=5).grid(row=1, rowspan=3, column=4, sticky="w")
bottom_frame.place(x=0, y=self.attr["height"] * 0.65 + 20)
def show_user_function(self):
children_window = Toplevel(root)
children_window.title("用户自定义函数")
children_window.geometry('350x260+450+200')
packet_frame = Frame(children_window)
packet_frame.grid(row=0, column=0, columnspan=3, padx=10, pady=5)
function_treeview = Treeview(packet_frame, columns=("function_name", "function"), show="headings")
function_treeview.heading("function_name", text="函数名称")
function_treeview.column("function_name", width=100, anchor=CENTER)
function_treeview.heading("function", text="函数体")
function_treeview.column("function", width=200, anchor=CENTER)
function_treeview.pack(side="left", fill="both")
scrollbar = Scrollbar(packet_frame, orient="vertical", command=function_treeview.yview)
scrollbar.pack(side="right", fill="y")
function_treeview.configure(yscrollcommand=scrollbar.set)
def delete_item():
selected_item = function_treeview.selection()
for item in selected_item:
item_data = function_treeview.item(item)
function_treeview.delete(item)
self.functions.data.pop(item_data["values"][0])
self.functions.save()
context_menu = Menu(root, tearoff=False)
context_menu.add_command(label="删除", command=delete_item)
def popup_menu(event):
if function_treeview.identify_region(event.x, event.y) == "cell":
function_treeview.selection_set(function_treeview.identify_row(event.y))
context_menu.post(event.x_root, event.y_root)
function_treeview.bind("<Button-3>", popup_menu)
for function_name, function in self.functions.data.items():
function_treeview.insert("", "end", values=(function_name, function))
def create_window(self):
self.axis_canvas = Graph(self.master, width=self.attr["width"] * 0.65, height=self.attr["height"] * 0.65, bg="#cdcdcd")
self.axis_canvas.place(x=0, y=0)
self.text_canvas = ExpressionCanvas(self.master, width=self.attr["width"] * 0.35, height=self.attr["height"] * 0.65, bg="#cdcdcd")
menubar = Menu(root)
menubar.add_command(label='查看自定义函数', command=self.show_user_function)
root.config(menu=menubar)
# self.text = "可支持下列函数的加减乘除组合运算\n" \
# "常用运算符:+,-,*,/,**,//,%\n" \
# "常用函数:\n" \
# " sqrt(x):求平方根\n" \
# "数学常数:\n" \
# " 虚数单位I\n" \
# " 自然对数的底E\n" \
# " 无穷大oo\n" \
# " 圆周率pi\n" \
# "三角函数sin(x),cos(x),tan(x)\n" \
# " sec(x),csc(x),cot(x),sinc(x)\n" \
# " 及其反函数sinh(),cosh()等\n" \
# "复杂函数:\n" \
# " 伽马函数gamma(x)\n" \
# " 贝塔函数beta()\n" \
# " 误差函数erf(x)\n" \
# "指数运算:\n" \
# " 指数运算exp()\n" \
# " 自然对数log()\n" \
# " 以十为底的对数log(var, 10)\n" \
# " 自然对数ln()或log()\n"
self.text = "可支持下列函数的加减乘除组合运算\n" \
"\ty=sin(x)\n" \
"\tcos(x)\n" \
"\ttan(x)\n" \
"\tcot(x)\n" \
"\tx^n\n" \
"\tP1(x)=x^3+x^2+x+5\n" \
"---将括号内的表达式命名为P1,P1可\n" \
"出现于用户构造中"
self.text_canvas.create_text(20 + 180, 20, text='可识别基本函数', fill='red', font=("Purisa", 25, "bold"))
center_text(self.text_canvas, self.text, ("Lucida Grande", 15))
self.text_canvas.bind("<ButtonPress-3>", func=self.clear_text_canvas)
self.text_canvas.place(x=int(self.attr["width"] * 0.65), y=0)
self.create_form()
if __name__ == '__main__':
root = Window()
screenwidth = root.winfo_screenwidth()
screenheight = root.winfo_screenheight()
root_attr = {
"width": 1200,
"height": 800,
}
alignstr = '%dx%d+%d+%d' % (root_attr['width'], root_attr['height'], (screenwidth - root_attr['width']) / 2,
(screenheight - root_attr['height']) / 2)
root.geometry(alignstr)
root.resizable(width=False, height=False)
app = FunctionDisplay(root, root_attr)
ttk.Style().configure("TButton", font="-size 18")
ttk.Style().configure("TRadiobutton", font="-size 18")
root.mainloop()