from time import sleep from ttkbootstrap 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 # 在画布中央绘制文本 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.bili_x = 20 self.bili_y = 20 self.draw_axis() self.draw_scale() self.fig = FigureCanvasTkAgg() 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=(5.5, 6), dpi=100) # 在 Figure 上创建一个 3D 子图 ax = fig.add_subplot(111, projection='3d') # 绘制三维图形 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) 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.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 = 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: 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(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() 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 = 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 create_window(self): self.axis_canvas = Graph(self.master, width=self.attr["width"] * 0.45, 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") self.text_canvas.place(x=int(self.attr["width"] * 0.45), y=0) user_function = Frame(self.master, width=self.attr["width"] * 0.2, height=self.attr["height"] * 0.65) user_function.place(x=int(self.attr["width"] * 0.8), y=0) self.user_function_canvas = Canvas(user_function, width=self.attr["width"] * 0.18, height=self.attr["height"] * 0.63, background="#cdcdcd") 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) for i in range(1, 200): self.user_function_canvas.create_text(80, i * 20 + 35, text="fdsaf{}".format(i)) self.user_function_canvas.config(scrollregion=self.user_function_canvas.bbox("all")) self.create_form() if __name__ == '__main__': from pycallgraph2 import PyCallGraph from pycallgraph2.output import GraphvizOutput from pycallgraph2 import Config from pycallgraph2 import GlobbingFilter 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) output = GraphvizOutput(font_size=30) output.output_file = "basic.png" output.group_font_size = 40 config = Config() config.trace_filter = GlobbingFilter(include=[ 'FunctionDisplay.*', 'Graph.*', 'ExpressionCanvas.*', 'FunctionUtil.*' ]) with PyCallGraph(output=output, config=config): app = FunctionDisplay(root, root_attr) ttk.Style().configure("TButton", font="-size 18") ttk.Style().configure("TRadiobutton", font="-size 18") root.mainloop()