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