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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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()