from time import sleep from tkinter import * import tkinter.font as tkFont from tkinter import messagebox import numpy as np from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import sympy from numpy import arange from sympy import symbols, lambdify from functionUtil import FunctionUtil import math from PIL import ImageTk, Image import sys # 在画布中央绘制文本 def center_text(canvas, text, font): canvas_width = int(canvas.cget("width")) 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.config(bg="white") self.bili_x = 20 self.bili_y = 20 self.draw_axis() self.draw_scale() self.fig = FigureCanvasTkAgg() self.y_scale = 1.0 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 draw_graph_3d(self, func, draw_precision=0.1, count=1000, bili_x=20, bili_y=40, ): self.fig.get_tk_widget().destroy() self.delete("all") # 创建 sympy 符号 x, y = symbols('x y') self.bili_x, self.bili_y = int(bili_x), int(bili_y) # 将表达式转换为可计算的函数 expr = lambdify((x, y), func, modules=['numpy']) xmin, xmax = -math.ceil((self.origin[0] / self.bili_x)) + 1, math.ceil( (self.width - self.origin[0]) / self.bili_x) # 创建数据网格 X = np.linspace(xmin, xmax, int(draw_precision * 100)) Y = np.linspace(xmin, xmax, int(draw_precision * 100)) X, Y = np.meshgrid(X, Y) Z = expr(X, Y) # 创建一个 Figure 对象 fig = Figure(figsize=(7, 5.5), dpi=120) # 在 Figure 上创建一个 3D 子图 ax = fig.add_subplot(111, projection='3d') ax.auto_scale_xyz(X, Y, Z) # 绘制三维图形 ax.plot_surface(X, Y, Z, cmap='viridis') self.fig = FigureCanvasTkAgg(fig, master=self) self.fig.draw() self.fig.get_tk_widget().pack(fill=BOTH, ipadx=0, ipady=0, pady=0, padx=0, expand=True) def draw_graph_2d(self, func, draw_precision=0.1, count=1000, bili_x=1, bili_y=1, c='blue'): 'xmin,xmax 自变量的取值范围; c 图像颜色' 'x0,y0 原点坐标 w,h 横纵轴半长 draw_precision 步进' self.fig.get_tk_widget().destroy() 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 = -math.ceil((self.origin[0] / self.bili_x)) + 1, math.ceil( (self.width - self.origin[0]) / self.bili_x) 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 = 100 self.node_height = 40 self.x_spacing = 15 self.y_spacing = 70 self.level = {} def add_node(self, text, x, y, parent=None): node_width = len(text) * 15 + 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", font=("", 15)) sleep(0.2) self.update() # 绘制父节点和当前节点的连线 if parent: try: 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() except Exception as E: pass 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(Canvas): def __init__(self, master, *args, **kwargs): super().__init__(master, *args, **kwargs) self.font_style = tkFont.Font(family="Lucida Grande", size=30) self.functions = FunctionUtil() self.width = int(self.cget("width")) self.height = int(self.cget("height")) self.background_img = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/背景@3x.jpg").resize((self.width, self.height))) self.create_image(0, 0, image=self.background_img, anchor=NW) self.master = master self.init_window() self.load_user_function() 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) self.load_user_function() return self.functions.add_function(left_var, right_var) messagebox.showinfo(title="提示", message="添加成功!") self.func_input.delete(0, END) self.load_user_function() 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: input_func = input_func.split("=")[1] if self.functions.check_function_exit(input_func): func = self.functions.get_function_by_iter(input_func) self.axis_canvas.draw_graph_3d(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_3d(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 load_user_function(self): self.user_function_canvas.delete(ALL) self.user_function_canvas.create_text(20 + 90, 35, text='可识别基本函数', fill='red', font=("Purisa", 20, "bold")) num = 2 self.user_function_canvas.create_text(0, 70, anchor="nw", text="函数名", font=("", 15)) self.user_function_canvas.create_text(100, 70, anchor="nw", text="函数体", font=("", 15)) for function, function_body in self.functions.data.items(): self.user_function_canvas.create_text(0, 20 * num + 50, anchor="nw", text=function, font=("", 15)) self.user_function_canvas.create_text(100, 20 * num + 50, anchor="nw", text=function_body, font=("", 15)) num += 1 self.user_function_canvas.update() self.user_function_canvas.configure(scrollregion=self.user_function_canvas.bbox("all")) def create_form(self): bottom_frame = Canvas(self, width=self.width, height=self.height * 0.3, highlightthickness=0) self.back_image = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/下部背景@3x.png").resize((self.width, int(self.height * 0.3)))) bottom_frame.create_image(0, 0, image=self.back_image, anchor=NW) label_width, entry_width, label_margin_left, margin_left, height, margin_top = 100, 215, 120 + 50, 120, 42, 20 self.func_input = Entry(bottom_frame, font=self.font_style) self.func_input.place(x=margin_left, y=margin_top, anchor=NW, width=1000, height=height) self.add_func_image = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/用户命名并新增基本函数@3x.png").resize((150, 42))) self.add_func_button = Button(bottom_frame, image=self.add_func_image, bd=0, relief="solid", bg="#f7f7f7", highlightthickness=0, command=self.add_function) self.add_func_button.place(x=1140, y=20, anchor=NW) self.label_img = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/文字背景@3x.png").resize((100, 42))) bottom_frame.create_image(label_margin_left, 112, image=self.label_img) bottom_frame.create_text(label_margin_left, 112, text="x步长", fill="white", font=("", 20)) self.x_step = Entry(bottom_frame, font=self.font_style) self.x_step.place(x=margin_left + label_width, y=112 - 21, width=215, height=42) self.label1_img = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/文字背景@3x.png").resize((100, 42))) bottom_frame.create_image(label_margin_left, 171, image=self.label1_img) bottom_frame.create_text(label_margin_left, 170, text="x个数", fill="white", font=("", 20)) self.x_count = Entry(bottom_frame, font=self.font_style) self.x_count.place(x=margin_left + label_width, y=112 + 39, width=215, height=42) self.big_label_img = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/文字背景2@3x.png").resize((label_width+5, height * 2 + margin_top))) bottom_frame.create_image(label_margin_left + label_width + entry_width + 108, 112 + 31, image=self.big_label_img) bottom_frame.create_text(label_margin_left + label_width + entry_width + 108, 112 + 31, text="坐标轴", fill="white", font=("", 25)) self.quadrant = IntVar() self.quadrant.set(4) Radiobutton(bottom_frame, text="四象限", command=self.update_quadrant, font=("", 17), bg="#b9b9f7",highlightthickness=0, highlightcolor="#aaaaf2", value=4, variable=self.quadrant).place(x=label_margin_left + label_width * 2 + entry_width +58, y=90, height=(height * 2 + margin_top) / 2, width=entry_width, anchor=NW) Radiobutton(bottom_frame, text="一象限", command=self.update_quadrant, font=("", 17), bg="#b9b9f7", highlightthickness=0, highlightcolor="#aaaaf2", value=1, variable=self.quadrant).place(x=label_margin_left + label_width * 2 + entry_width +58, y=90 + (height * 2 + margin_top) / 2, height=(height * 2 + margin_top) / 2, width=entry_width, anchor=NW) self.x_scale = Entry(bottom_frame, font=self.font_style) self.x_scale.place(x=margin_left + label_width, y=112 - 21, width=215, height=42) label_x = label_margin_left + 840 label_y = 112 self.label_img1 = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/文字背景@3x.png").resize((130, 42))) bottom_frame.create_image(label_x, label_y, image=self.label_img1) bottom_frame.create_text(label_x, label_y, text="x放大倍数", fill="white", font=("", 20)) entry_x = margin_left + label_width + 855 entry_y = 112 - 21 self.x_scale.place(x=entry_x, y=entry_y, width=215, height=42) self.y_scale = Entry(bottom_frame, font=self.font_style) self.y_scale.place(x=margin_left + label_width, y=112 - 21, width=215, height=42) label_x = label_margin_left + 840 label_y = 170 bottom_frame.create_image(label_x, label_y, image=self.label_img1) bottom_frame.create_text(label_x, label_y, text="y放大倍数", fill="white", font=("", 20)) entry_x = margin_left + label_width + 855 entry_y = 170 - 21 self.y_scale.place(x=entry_x, y=entry_y, width=215, height=42) self.print_function_image = ImageTk.PhotoImage(Image.open(sys.path[0]+"/./image/输出-点击@3x.png").resize((100, 42))) self.print_function_button = Button(bottom_frame, image=self.print_function_image, bd=0, relief="solid", bg="#f7f7f7", highlightthickness=0, command=self.print_function) self.print_function_button.place(x=1350, y=95, anchor=NW) # Button(bottom_frame, text="输出", command=self.print_function, width=5) \ # .place(x=1350, y=95, anchor=NW) bottom_frame.place(x=0, y=self.height * 0.65 + 40, anchor=NW) def init_window(self): self.axis_canvas = Graph(self, width=self.width * 0.45, height=self.height * 0.65,highlightthickness=0) self.axis_canvas.place(x=0, y=0, anchor=NW) self.text_canvas = ExpressionCanvas(self, width=self.width * 0.35, height=self.height * 0.65, bg="white",highlightthickness=0) self.text_canvas.place(x=int(self.width * 0.45), y=0) user_function = Frame(self, width=self.width * 0.2, height=self.height * 0.65) user_function.place(x=int(self.width * 0.8), y=0) self.user_function_canvas = Canvas(user_function, width=self.width * 0.19, height=self.height * 0.65, bg="white",highlightthickness=0) y_scrollbar = Scrollbar(user_function, orient="vertical", command=self.user_function_canvas.yview) x_scrollbar = Scrollbar(user_function, orient="horizontal", command=self.user_function_canvas.xview) x_scrollbar.pack(side=BOTTOM, fill=BOTH) y_scrollbar.pack(side=RIGHT, fill=BOTH) self.user_function_canvas.pack(side=TOP, fill=BOTH) self.user_function_canvas.config(yscrollcommand=y_scrollbar.set, xscrollcommand=x_scrollbar.set) self.user_function_canvas.config(scrollregion=self.user_function_canvas.bbox("all")) self.create_form() if __name__ == '__main__': root = Tk() screenwidth = root.winfo_screenwidth() screenheight = root.winfo_screenheight() root_attr = { "width": int(screenwidth * 1), "height": int(screenheight * 1), } 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, width=root_attr["width"], height=root_attr["height"]) app.place(x=0, y=0, anchor=NW) root.mainloop()