Python数学可视化——显函数、隐函数及复杂曲线的交互式绘图技术_python 画方程
Python数学可视化——显函数、隐函数及复杂曲线的交互式绘图技术
一、引言
在科学计算和数据分析中,函数与方程的可视化是理解数学关系和物理现象的重要工具。本文基于Python的Tkinter和Matplotlib库,实现一个功能完善的函数与方程可视化工具,支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图,并提供安全的表达式解析、图像保存等功能。
二、核心技术架构
2.1 系统架构与技术选型
- 界面层:使用Tkinter构建GUI,包含类型选择、表达式输入、预设函数下拉菜单等控件
- 计算层:
- 显函数:通过
np.linspace
生成采样点,安全计算函数值 - 隐函数:基于等高线算法
contour
绘制等值线 - 安全机制:通过正则表达式过滤非法字符,限制白名单函数防止代码注入
- 显函数:通过
- 可视化层:Matplotlib实现图表渲染,支持动态更新和交互式工具条
2.2 安全表达式解析
def is_valid_expression(expr): \"\"\"验证表达式安全性\"\"\" allowed_chars = set(\"0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ \") invalid_chars = set(expr.replace(\'.\', \'\').replace(\'_\', \'\')) - allowed_chars if invalid_chars: raise ValueError(f\"非法字符: {\'\'.join(invalid_chars)}\") # 括号匹配检查 stack = [] for char in expr: if char == \'(\': stack.append(char) elif char == \')\': if not stack: raise ValueError(\"括号不匹配\") stack.pop() if stack: raise ValueError(\"括号不匹配\") return Truedef safe_eval(expr, namespace): \"\"\"安全执行表达式\"\"\" expr = expr.replace(\'^\', \'**\') # 替换幂运算符 allowed_funcs = { \'np\': np, \'sin\': np.sin, \'cos\': np.cos, \'tan\': np.tan, \'exp\': np.exp, \'sqrt\': np.sqrt, \'log\': np.log, \'pi\': np.pi } safe_globals = {\"__builtins__\": None} safe_locals = {**allowed_funcs, **namespace} compiled_code = compile(expr, \'\', \'eval\') return eval(compiled_code, safe_globals, safe_locals)
三、显函数可视化
3.1 核心实现
def plot_explicit_function(self, f, x_range, title): \"\"\"绘制显函数\"\"\" self.fig.clear() ax = self.fig.add_subplot(111) ax.set_facecolor(\'white\') x = np.linspace(x_range[0], x_range[1], 1000) y = np.array([f(xi) for xi in x]) # 逐点计算防止数组错误 ax.plot(x, y, \'b-\', linewidth=2.5) ax.set_title(title) ax.grid(True, linestyle=\'--\', alpha=0.6) self.optimize_ticks(ax, x_range, (y.min(), y.max()))
3.2 案例演示
案例1:三次函数
# 预设函数定义self.explicit_presets = { \"三次函数\": { \"func\": lambda x: x**3 - 3*x, \"expr\": \"x**3 - 3*x\", \"x_range\": (-2.5, 2.5), \"title\": \"三次函数: $y = x^3 - 3x$\", }}
案例2:双曲线
plot_explicit(\"1/x\", x_range=(-5,5)) # 输入表达式直接绘制
四、隐函数可视化
4.1 核心实现
def plot_implicit_equation(self, eq, x_range, y_range): \"\"\"绘制隐函数F(x,y)=0\"\"\" x = np.linspace(x_range[0], x_range[1], 500) y = np.linspace(y_range[0], y_range[1], 500) X, Y = np.meshgrid(x, y) Z = eq(X, Y) self.fig.contour(X, Y, Z, levels=[0], colors=\'red\', linewidths=2.5) self.fig.contourf(X, Y, Z, alpha=0.6) # 填充色显示数值分布 self.fig.colorbar(label=\'F(x,y)\')
4.2 案例演示
案例1:圆方程
# 预设隐函数self.implicit_presets[\"圆\"] = { \"eq\": lambda x, y: x**2 + y**2 - 4, \"title\": \"圆: $x^2 + y^2 = 4$\",}
案例2:笛卡尔叶形线
plot_implicit(\"x**3 + y**3 - 3*x*y\", x_range=(-3,3), y_range=(-3,3))
五、特色曲线与物理应用
5.1 心形线(数学艺术)
def plot_heart_curve(self): \"\"\"笛卡尔心形线\"\"\" eq = lambda x,y: (x**2 + y**2 -1)**3 - x**2*y**3 self.plot_implicit_equation(eq, x_range=(-1.5,1.5), y_range=(-1.5,1.5)) self.fig.contourf(..., colors=\'pink\', alpha=0.4) # 填充爱心区域
5.2 电势分布(物理应用)
def plot_electric_potential(self): \"\"\"点电荷电势分布\"\"\" charges = [{\"x\":-1,\"y\":0,\"q\":1}, {\"x\":1,\"y\":0,\"q\":-1}] x = np.linspace(-2.5,2.5,500) y = np.linspace(-2,2,500) X,Y = np.meshgrid(x,y) V = sum(charge[\'q\']/np.sqrt((X-c[\'x\'])**2 + (Y-c[\'y\'])**2) for c in charges) self.fig.contourf(X,Y,V, cmap=\'coolwarm\') # 温度映射显示电势 self.fig.scatter([c[\'x\']], [c[\'y\']], s=300, c=[\'red\',\'blue\'], marker=\'+-\')
六、交互式GUI设计
6.1 界面布局
def __init__(self, root): self.root = root self.root.geometry(\"1200x800\") # 左侧控制面板 left_frame = ttk.LabelFrame(root, text=\"可视化选项\") ttk.Radiobutton(left_frame, text=\"显函数\", variable=self.viz_type, value=\"explicit\") ttk.Radiobutton(left_frame, text=\"隐函数\", variable=self.viz_type, value=\"implicit\") ttk.Radiobutton(left_frame, text=\"心形线\", variable=self.viz_type, value=\"heart\") # 右侧绘图区域 self.canvas = FigureCanvasTkAgg(self.fig, master=right_frame) self.toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame) # 集成缩放工具
6.2 动态控件更新
def update_controls(self): \"\"\"根据选择类型显示对应控件\"\"\" if self.viz_type.get() == \"explicit\": self.explicit_frame.pack() self.update_preset_options(self.explicit_presets.keys()) elif self.viz_type.get() == \"implicit\": self.implicit_frame.pack() self.update_preset_options(self.implicit_presets.keys()) # 隐藏其他面板
七、高级功能
7.1 图像保存
def save_image(self): filename = simpledialog.askstring(\"保存\", \"文件名\") if filename: self.fig.savefig(f\"{filename}.png\", dpi=150, bbox_inches=\"tight\") messagebox.showinfo(\"成功\", f\"保存至: {os.path.abspath(filename)}\")
7.2 公式渲染
def get_function_label(self, expr): \"\"\"生成LaTeX公式\"\"\" expr = expr.replace(\'np.sin\', \'\\\\sin\').replace(\'**\', \'^\') expr = re.sub(r\'(\\d)/(\\d)\', r\'\\\\frac{\\1}{\\2}\', expr) # 自动转换分数 return f\"${expr}$\"
八、教学与应用场景
8.1 教学场景
- 基础数学:演示函数图像变换(平移、缩放、翻转)
- 解析几何:对比显式方程与隐式方程的几何意义
- 高等数学:展示参数方程(如心形线)与极坐标方程
- 大学物理:可视化电场、磁场等物理场分布
8.2 扩展方向
- 支持极坐标绘图
- 添加导数/积分可视化
- 集成3D绘图功能(使用mpl_toolkits.mplot3d)
- 开发数据导入功能(CSV/Excel)
九、总结
本文实现的函数可视化工具具备以下特点:
- 安全性:通过表达式过滤和白名单机制防止代码注入
- 交互性:支持实时切换函数类型、调整参数、缩放图表
- 扩展性:预设函数与自定义输入结合,方便扩展新类型
- 专业性:支持LaTeX公式渲染、物理场可视化等专业需求
该工具可广泛应用于数学教学、工程仿真、科学研究等地方,帮助用户快速建立数学表达式与图形之间的直观联系。
十、完整代码
import tkinter as tkfrom tkinter import ttk, messagebox, simpledialogimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tkfrom matplotlib.figure import Figureimport matplotlib.patches as patchesfrom matplotlib import tickerfrom matplotlib.colors import ListedColormapimport reimport os# 设置matplotlib支持中文显示plt.rcParams[\"font.family\"] = [ \"SimHei\", \"WenQuanYi Micro Hei\", \"Heiti TC\", \"Arial Unicode MS\",]plt.rcParams[\"axes.unicode_minus\"] = False # 解决负号显示问题class FunctionVisualizer: def __init__(self, root): self.root = root self.root.title(\"函数与方程可视化工具\") self.root.geometry(\"1200x800\") self.root.minsize(1000, 700) # 设置主题颜色 self.bg_color = \"#f5f5f5\" self.frame_color = \"#ffffff\" self.button_color = \"#3b82f6\" self.button_text_color = \"#ffffff\" # 预设函数分组(显函数/隐函数) self.explicit_presets = { \"三次函数\": { \"func\": lambda x: x**3 - 3 * x, \"expr\": \"x**3 - 3*x\", \"x_range\": (-2.5, 2.5), \"title\": \"三次函数: $y = x^3 - 3x$\", }, \"双曲线\": { \"func\": lambda x: 1 / x, \"expr\": \"1/x\", \"x_range\": (-5, 5), \"title\": \"双曲线: $y = \\\\frac{1}{x}$\", }, \"指数函数\": { \"func\": lambda x: np.exp(x), \"expr\": \"np.exp(x)\", \"x_range\": (-3, 3), \"title\": \"指数函数: $y = e^x$\", }, } self.implicit_presets = { \"圆\": { \"eq\": lambda x, y: x**2 + y**2 - 4, \"expr\": \"x**2 + y**2 - 4\", \"x_range\": (-3, 3), \"y_range\": (-3, 3), \"title\": \"圆: $x^2 + y^2 = 4$\", }, \"椭圆\": { \"eq\": lambda x, y: x**2 / 4 + y**2 / 9 - 1, \"expr\": \"x**2/4 + y**2/9 - 1\", \"x_range\": (-3, 3), \"y_range\": (-4, 4), \"title\": \"椭圆: $\\\\frac{x^2}{4} + \\\\frac{y^2}{9} = 1$\", }, \"双曲线(隐式)\": { \"eq\": lambda x, y: x**2 - y**2 - 1, \"expr\": \"x**2 - y**2 - 1\", \"x_range\": (-3, 3), \"y_range\": (-3, 3), \"title\": \"双曲线: $x^2 - y^2 = 1$\", }, \"笛卡尔叶形线\": { \"eq\": lambda x, y: x**3 + y**3 - 3 * x * y, \"expr\": \"x**3 + y**3 - 3*x*y\", \"x_range\": (-3, 3), \"y_range\": (-3, 3), \"title\": \"笛卡尔叶形线: $x^3 + y^3 = 3xy$\", }, } # 创建主框架 main_frame = ttk.Frame(self.root, padding=10) main_frame.pack(fill=tk.BOTH, expand=True) # 创建左侧控制面板 left_frame = ttk.LabelFrame( main_frame, text=\"函数与方程可视化选项\", padding=10, width=375 ) left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) left_frame.pack_propagate(False) # 固定宽度 # 创建右侧绘图区域 right_frame = ttk.Frame(main_frame) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) # 创建绘图区域和工具栏容器 self.plot_frame = ttk.Frame(right_frame) self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 初始化绘图区域 self.fig = Figure(figsize=(8, 6), dpi=100) self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # 添加工具栏 self.toolbar_frame = ttk.Frame(right_frame, height=40) self.toolbar_frame.pack(fill=tk.X, padx=5, pady=(0, 5)) self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame) self.toolbar.update() # 添加控制选项 self.create_controls(left_frame) # 初始显示 self.plot_predefined_function() def create_controls(self, parent): \"\"\"创建控制选项\"\"\" ttk.Label(parent, text=\"选择可视化类型:\", font=(\"SimHei\", 10, \"bold\")).pack( anchor=tk.W, pady=(0, 10) ) # 可视化类型选择 self.viz_type = tk.StringVar(value=\"explicit\") types = [ (\"显函数\", \"explicit\"), (\"隐函数\", \"implicit\"), (\"心形线\", \"heart\"), (\"电势分布\", \"potential\"), ] for text, value in types: ttk.Radiobutton( parent, text=text, variable=self.viz_type, value=value, command=self.update_controls, ).pack(anchor=tk.W, padx=5, pady=2) # 预设函数下拉菜单(动态更新选项) self.preset_frame = ttk.LabelFrame(parent, text=\"预设函数\", padding=10) self.preset_frame.pack(fill=tk.X, pady=10) # 动态选项变量 self.preset_functions = tk.StringVar() self.preset_combobox = ttk.Combobox( self.preset_frame, textvariable=self.preset_functions, width=30 ) self.preset_combobox.pack(fill=tk.X, pady=5) ttk.Button( self.preset_frame, text=\"绘制预设函数\", command=self.plot_predefined_function, ).pack(fill=tk.X, pady=5) # 显函数输入 self.explicit_frame = ttk.LabelFrame(parent, text=\"显函数输入\", padding=10) self.explicit_frame.pack(fill=tk.X, pady=10) ttk.Label(self.explicit_frame, text=\"函数表达式 (例如 x**2):\").pack(anchor=tk.W) self.explicit_entry = ttk.Entry(self.explicit_frame, width=30) self.explicit_entry.insert(0, \"x**3 - 3*x\") self.explicit_entry.pack(fill=tk.X, pady=5) ttk.Label(self.explicit_frame, text=\"X范围 (min,max):\").pack(anchor=tk.W) self.x_range_entry = ttk.Entry(self.explicit_frame, width=30) self.x_range_entry.insert(0, \"-2.5,2.5\") self.x_range_entry.pack(fill=tk.X, pady=5) ttk.Button( self.explicit_frame, text=\"绘制显函数\", command=self.plot_explicit ).pack(fill=tk.X, pady=5) # 隐函数输入 self.implicit_frame = ttk.LabelFrame(parent, text=\"隐函数输入\", padding=10) self.implicit_frame.pack(fill=tk.X, pady=10) ttk.Label(self.implicit_frame, text=\"方程表达式 (例如 x**2 + y**2 - 4):\").pack( anchor=tk.W ) self.implicit_entry = ttk.Entry(self.implicit_frame, width=30) self.implicit_entry.insert(0, \"x**3 + y**3 - 3*x*y\") self.implicit_entry.pack(fill=tk.X, pady=5) ttk.Label(self.implicit_frame, text=\"X范围 (min,max):\").pack(anchor=tk.W) self.implicit_x_range_entry = ttk.Entry(self.implicit_frame, width=30) self.implicit_x_range_entry.insert(0, \"-3,3\") self.implicit_x_range_entry.pack(fill=tk.X, pady=5) ttk.Label(self.implicit_frame, text=\"Y范围 (min,max):\").pack(anchor=tk.W) self.implicit_y_range_entry = ttk.Entry(self.implicit_frame, width=30) self.implicit_y_range_entry.insert(0, \"-3,3\") self.implicit_y_range_entry.pack(fill=tk.X, pady=5) ttk.Button( self.implicit_frame, text=\"绘制隐函数\", command=self.plot_implicit ).pack(fill=tk.X, pady=5) # 保存图像按钮 ttk.Button(parent, text=\"保存图像\", command=self.save_image).pack( side=tk.BOTTOM, pady=10 ) # 初始更新控件状态 self.update_controls() def update_controls(self): \"\"\"更新控件状态\"\"\" viz_type = self.viz_type.get() # 隐藏所有输入面板 self.preset_frame.pack_forget() self.explicit_frame.pack_forget() self.implicit_frame.pack_forget() # 显示对应面板 if viz_type == \"explicit\": self.explicit_frame.pack(fill=tk.X, pady=10) self.update_preset_options(self.explicit_presets.keys()) # 显函数预设 elif viz_type == \"implicit\": self.implicit_frame.pack(fill=tk.X, pady=10) self.update_preset_options(self.implicit_presets.keys()) # 隐函数预设 elif viz_type == \"heart\": self.plot_heart_curve() elif viz_type == \"potential\": self.plot_electric_potential() # 显示预设框架 self.preset_frame.pack(fill=tk.X, pady=10) def update_preset_options(self, options=None): \"\"\"动态更新预设函数选项\"\"\" if options is None: options = [] self.preset_combobox[\"values\"] = list(options) if options: self.preset_functions.set(list(options)[0]) # 默认选择第一个 def plot_predefined_function(self): \"\"\"绘制预设函数\"\"\" viz_type = self.viz_type.get() selected = self.preset_functions.get() self.fig.clear() ax = self.fig.add_subplot(111) ax.set_facecolor(\"white\") self.fig.set_facecolor(\"white\") if viz_type == \"explicit\" and selected in self.explicit_presets: data = self.explicit_presets[selected] self.plot_explicit_function( f=data[\"func\"], x_range=data[\"x_range\"], title=data[\"title\"] ) # 更新显函数输入框 self.explicit_entry.delete(0, tk.END) self.explicit_entry.insert(0, data[\"expr\"]) self.x_range_entry.delete(0, tk.END) self.x_range_entry.insert(0, f\"{data[\'x_range\'][0]},{data[\'x_range\'][1]}\") elif viz_type == \"implicit\" and selected in self.implicit_presets: data = self.implicit_presets[selected] self.plot_implicit_equation( eq=data[\"eq\"], x_range=data[\"x_range\"], y_range=data[\"y_range\"], title=data[\"title\"], ) # 更新隐函数输入框 self.implicit_entry.delete(0, tk.END) self.implicit_entry.insert(0, data[\"expr\"]) self.implicit_x_range_entry.delete(0, tk.END) self.implicit_x_range_entry.insert( 0, f\"{data[\'x_range\'][0]},{data[\'x_range\'][1]}\" ) self.implicit_y_range_entry.delete(0, tk.END) self.implicit_y_range_entry.insert( 0, f\"{data[\'y_range\'][0]},{data[\'y_range\'][1]}\" ) self.canvas.draw() def is_valid_expression(self, expr): \"\"\"验证表达式是否为有效的数学表达式\"\"\" # 允许的字符:数字、运算符、函数名、xy变量、小数点、括号、空格 allowed_chars = set(\"0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ \") # 移除所有允许的字符,检查是否还有剩余 cleaned = expr.replace(\'.\', \'\').replace(\'_\', \'\') invalid_chars = set(cleaned) - allowed_chars if invalid_chars: raise ValueError(f\"非法字符: {\'\'.join(invalid_chars)}\") # 检查括号匹配 stack = [] for char in expr: if char == \'(\': stack.append(char) elif char == \')\': if not stack: raise ValueError(\"括号不匹配:缺少左括号\") stack.pop() if stack: raise ValueError(\"括号不匹配:缺少右括号\") return True def safe_eval(self, expr, namespace): \"\"\"安全地执行表达式计算\"\"\" try: self.is_valid_expression(expr) # 替换常见函数别名 expr = expr.replace(\'^\', \'**\') # 替换^为** # 白名单函数和变量 allowed_funcs = { \'np\': np, \'sin\': np.sin, \'cos\': np.cos, \'tan\': np.tan, \'exp\': np.exp, \'sqrt\': np.sqrt, \'log\': np.log, \'pi\': np.pi, \'arctan2\': np.arctan2, } # 创建安全命名空间 safe_globals = {\"__builtins__\": None} safe_locals = {**allowed_funcs, **namespace} # 使用编译后的代码提高安全性 compiled_code = compile(expr, \'\', \'eval\') return eval(compiled_code, safe_globals, safe_locals) except Exception as e: raise ValueError(f\"表达式错误: {str(e)}\") def plot_explicit(self): \"\"\"绘制用户输入的显函数\"\"\" try: func_str = self.explicit_entry.get().strip() x_range_str = self.x_range_entry.get().strip() if not func_str or not x_range_str: raise ValueError(\"请输入函数表达式和X范围\") # 解析x范围 x_min, x_max = map(float, x_range_str.split(\",\")) if x_min >= x_max: raise ValueError(\"X范围的最小值必须小于最大值\") # 生成x值 x_vals = np.linspace(x_min, x_max, 1000) # 安全计算y值(逐个点计算,避免数组错误) y_vals = np.zeros_like(x_vals) for i, x in enumerate(x_vals): y_vals[i] = self.safe_eval(func_str, {\'x\': x}) # 绘制函数 self.plot_explicit_function( f=lambda x: y_vals, x_range=(x_min, x_max), title=f\"显函数: $y = {self.get_function_label(func_str)}$\", ) self.canvas.draw() except Exception as e: messagebox.showerror(\"错误\", f\"绘制显函数时出错: {str(e)}\") def plot_implicit(self): \"\"\"绘制用户输入的隐函数(修复网格点数不匹配问题)\"\"\" try: eq_str = self.implicit_entry.get().strip() x_range_str = self.implicit_x_range_entry.get().strip() y_range_str = self.implicit_y_range_entry.get().strip() if not eq_str or not x_range_str or not y_range_str: raise ValueError(\"请输入完整的方程表达式和范围\") # 解析范围 x_min, x_max = map(float, x_range_str.split(\",\")) y_min, y_max = map(float, y_range_str.split(\",\")) if x_min >= x_max or y_min >= y_max: raise ValueError(\"范围的最小值必须小于最大值\") # 创建向量化的方程函数(直接处理数组输入) eq = lambda X, Y: self.safe_eval(eq_str, {\'x\': X, \'y\': Y}) # 调用隐函数绘图函数,使用默认分辨率500(与函数内部一致) self.plot_implicit_equation( eq=eq, x_range=(x_min, x_max), y_range=(y_min, y_max), title=f\"隐函数: ${self.get_function_label(eq_str)} = 0$\", ) self.canvas.draw() except Exception as e: messagebox.showerror(\"错误\", f\"绘制隐函数时出错: {str(e)}\") except Exception as e: messagebox.showerror(\"错误\", f\"绘制隐函数时出错: {str(e)}\") def plot_explicit_function(self, f, x_range=(-5, 5), title=\"显函数图像\"): \"\"\" 绘制显函数 y = f(x) 的图像 参数: f: 函数对象 x_range: x轴范围 title: 图像标题 \"\"\" self.fig.clear() ax = self.fig.add_subplot(111) # 设置背景为白色 ax.set_facecolor(\"white\") self.fig.set_facecolor(\"white\") # 创建网格和样式 ax.grid(True, linestyle=\"--\", alpha=0.6) ax.spines[\"left\"].set_position(\"zero\") ax.spines[\"bottom\"].set_position(\"zero\") ax.spines[\"right\"].set_visible(False) ax.spines[\"top\"].set_visible(False) # 生成数据 x = np.linspace(x_range[0], x_range[1], 1000) try: y = f(x) except Exception as e: messagebox.showerror(\"函数错误\", f\"计算函数值时出错: {str(e)}\") return # 绘制函数曲线 ax.plot(x, y, \"b-\", linewidth=2.5) # 设置标题和标签 ax.set_title(title, fontsize=16, pad=20) ax.set_xlabel(\"x\", fontsize=12, labelpad=-10, x=1.02) ax.set_ylabel(\"y\", fontsize=12, labelpad=-20, y=1.02, rotation=0) # 优化坐标轴刻度 self.optimize_ticks(ax, x_range, (np.min(y), np.max(y))) self.fig.tight_layout() def plot_implicit_equation( self, eq, x_range=(-3, 3), y_range=(-3, 3), resolution=500, levels=[0], cmap=\"viridis\", title=\"隐函数图像\", ): \"\"\" 绘制隐函数 F(x, y) = 0 的图像 参数: eq: 函数对象 x_range, y_range: 绘图范围 resolution: 网格分辨率 levels: 绘制等高线的值 cmap: 颜色映射 title: 图像标题 \"\"\" self.fig.clear() ax = self.fig.add_subplot(111) # 设置背景为白色 ax.set_facecolor(\"white\") self.fig.set_facecolor(\"white\") # 创建网格 x = np.linspace(x_range[0], x_range[1], resolution) y = np.linspace(y_range[0], y_range[1], resolution) X, Y = np.meshgrid(x, y) # 计算方程值 try: Z = eq(X, Y) except Exception as e: messagebox.showerror(\"方程错误\", f\"计算方程值时出错: {str(e)}\") return # 绘制等高线 (隐函数曲线) contour = ax.contour(X, Y, Z, levels=levels, colors=\"red\", linewidths=2.5) # 添加填充色显示方程值的变化 (只在需要时) if len(levels) > 1: ax.contourf( X, Y, Z, levels=np.linspace(Z.min(), Z.max(), 100), cmap=cmap, alpha=0.6 ) # 添加颜色条 cbar = self.fig.colorbar(contour) cbar.set_label(\"F(x, y)\", rotation=270, labelpad=20) # 设置网格和样式 ax.grid(True, linestyle=\"--\", alpha=0.4) ax.set_aspect(\"equal\") # 设置标题和标签 ax.set_title(title, fontsize=16, pad=20) ax.set_xlabel(\"x\", fontsize=12) ax.set_ylabel(\"y\", fontsize=12) # 添加零线 ax.axhline(0, color=\"black\", linewidth=0.8, alpha=0.7) ax.axvline(0, color=\"black\", linewidth=0.8, alpha=0.7) # 优化坐标轴刻度 self.optimize_ticks(ax, x_range, y_range) self.fig.tight_layout() def optimize_ticks(self, ax, x_range, y_range): \"\"\"优化坐标轴刻度,避免刻度过于密集\"\"\" x_min, x_max = x_range y_min, y_max = y_range # 根据数据范围自动设置刻度 x_span = x_max - x_min y_span = y_max - y_min # 设置合理的刻度间隔 x_major_locator = ticker.MaxNLocator(nbins=7) y_major_locator = ticker.MaxNLocator(nbins=7) ax.xaxis.set_major_locator(x_major_locator) ax.yaxis.set_major_locator(y_major_locator) def plot_heart_curve(self): \"\"\"绘制心形线\"\"\" self.fig.clear() # 创建图像和子图 ax1 = self.fig.add_subplot(111) ax1.set_aspect(\"equal\") ax1.set_title(\"心形线: $(x^2+y^2-1)^3 - x^2y^3 = 0$\", fontsize=14) # 设置背景为白色 ax1.set_facecolor(\"white\") self.fig.set_facecolor(\"white\") # 定义心形线方程 def heart_eq(x, y): return (x**2 + y**2 - 1) ** 3 - x**2 * y**3 # 生成网格 x = np.linspace(-1.5, 1.5, 500) y = np.linspace(-1.5, 1.5, 500) X, Y = np.meshgrid(x, y) Z = heart_eq(X, Y) # 绘制心形线 contour = ax1.contour(X, Y, Z, levels=[0], colors=\"red\", linewidths=3) # 填充颜色 ax1.contourf(X, Y, Z, levels=[-1000, 0], colors=[\"pink\"], alpha=0.4) # 添加网格和样式 ax1.grid(True, linestyle=\"--\", alpha=0.3) ax1.set_xlim(-1.5, 1.5) ax1.set_ylim(-1.5, 1.5) # 优化坐标轴刻度 self.optimize_ticks(ax1, (-1.5, 1.5), (-1.5, 1.5)) self.fig.tight_layout() self.canvas.draw() def plot_electric_potential(self): \"\"\"可视化点电荷系统的电势分布\"\"\" self.fig.clear() ax = self.fig.add_subplot(111) # 设置背景为白色 ax.set_facecolor(\"white\") self.fig.set_facecolor(\"white\") # 定义两个点电荷的位置和电荷量 charges = [ {\"x\": -1, \"y\": 0, \"q\": 1}, # 正电荷 {\"x\": 1, \"y\": 0, \"q\": -1}, # 负电荷 ] # 创建网格 x = np.linspace(-2.5, 2.5, 500) y = np.linspace(-2, 2, 500) X, Y = np.meshgrid(x, y) # 计算电势 (k=1) V = np.zeros_like(X) for charge in charges: r = np.sqrt((X - charge[\"x\"]) ** 2 + (Y - charge[\"y\"]) ** 2) V += charge[\"q\"] / r # 避免除以零 V = np.nan_to_num(V, posinf=10, neginf=-10) # 绘制电势等高线 (使用contourf创建填充等高线) levels = np.linspace(-10, 10, 21) contourf = ax.contourf(X, Y, V, levels=levels, cmap=\"coolwarm\", alpha=0.8) contour = ax.contour(X, Y, V, levels=levels, colors=\"k\", linewidths=0.5) ax.clabel(contour, inline=True, fontsize=8) # 绘制电荷位置 for charge in charges: color = \"red\" if charge[\"q\"] > 0 else \"blue\" marker = \"+\" if charge[\"q\"] > 0 else \"_\" ax.scatter( charge[\"x\"], charge[\"y\"], s=300, c=color, marker=marker, linewidths=2 ) ax.text( charge[\"x\"], charge[\"y\"] + 0.2, f\"{charge[\'q\']}q\", ha=\"center\", fontsize=12, weight=\"bold\", ) # 设置标题和标签 ax.set_title(\"两个点电荷的电势分布\", fontsize=16, pad=20) ax.set_xlabel(\"x (m)\", fontsize=12) ax.set_ylabel(\"y (m)\", fontsize=12) # 添加网格和样式 ax.set_aspect(\"equal\") ax.grid(True, linestyle=\"--\", alpha=0.4) # 添加坐标轴 ax.axhline(0, color=\"k\", linewidth=0.8, alpha=0.7) ax.axvline(0, color=\"k\", linewidth=0.8, alpha=0.7) # 添加物理公式 ax.text( 1.5, 1.8, r\"$V = \\sum \\frac{kq_i}{r_i}$\", fontsize=14, bbox=dict(facecolor=\"white\", alpha=0.8), ) # 添加颜色条 cbar = self.fig.colorbar(contourf, label=\"电势 (V)\") # 优化坐标轴刻度 self.optimize_ticks(ax, (-2.5, 2.5), (-2, 2)) self.fig.tight_layout() self.canvas.draw() def get_function_label(self, func_str): \"\"\"生成函数的LaTeX标签\"\"\" # 安全检查,防止恶意代码 if any( word in func_str.lower() for word in [\"import\", \"os\", \"sys\", \"subprocess\"] ): raise ValueError(\"检测到不安全的代码\") # 直接使用原始字符串,不再进行转义 safe_str = func_str # 替换常见的数学函数 replacements = { r\'np\\.sin\\(([^)]+)\\)\': r\'\\sin(\\1)\', r\'np\\.cos\\(([^)]+)\\)\': r\'\\cos(\\1)\', r\'np\\.tan\\(([^)]+)\\)\': r\'\\tan(\\1)\', r\'np\\.exp\\(([^)]+)\\)\': r\'\\exp(\\1)\', r\'np\\.sqrt\\(([^)]+)\\)\': r\'\\sqrt{\\1}\', r\'np\\.log\\(([^)]+)\\)\': r\'\\ln(\\1)\', r\'np\\.pi\': r\'\\pi\', r\'\\*\\*\': r\'^\', r\'\\*\': r\'\\cdot\', } # 应用所有替换,捕获可能的正则表达式错误 for pattern, replacement in replacements.items(): try: safe_str = re.sub(pattern, replacement, safe_str) except re.error as e: continue # 跳过有问题的替换 # 处理分数 - 更稳健的方法 if \'/\' in safe_str: # 只替换不包含字母的分数表达式 if re.search(r\'\\d+\\.?\\d*/\\d+\\.?\\d*\', safe_str): parts = safe_str.split(\'/\') if len(parts) == 2: numerator = parts[0].strip() denominator = parts[1].strip() safe_str = r\'\\frac{\' + numerator + \'}{\' + denominator + \'}\' return safe_str def save_image(self): \"\"\"保存当前图像\"\"\" try: filename = simpledialog.askstring( \"保存图像\", \"请输入文件名:\", initialvalue=\"function_plot.png\" ) if filename: if not filename.endswith(\".png\"): filename += \".png\" self.fig.savefig(filename, dpi=150, bbox_inches=\"tight\") messagebox.showinfo( \"成功\", f\"图像已保存至: {os.path.abspath(filename)}\" ) except Exception as e: messagebox.showerror(\"保存错误\", f\"保存图像时出错: {e}\")def main(): root = tk.Tk() # 设置样式 style = ttk.Style() style.configure(\"TFrame\", background=\"#f5f5f5\") style.configure(\"TLabelframe\", background=\"#ffffff\", relief=\"sunken\") style.configure( \"TLabelframe.Label\", background=\"#ffffff\", font=(\"SimHei\", 10, \"bold\") ) style.configure(\"TButton\", padding=5) # 尝试设置中文字体 try: plt.rcParams[\"font.family\"] = [\"SimHei\"] except: try: plt.rcParams[\"font.family\"] = [\"WenQuanYi Micro Hei\"] except: try: plt.rcParams[\"font.family\"] = [\"Heiti TC\"] except: try: plt.rcParams[\"font.family\"] = [\"Arial Unicode MS\"] except: plt.rcParams[\"font.family\"] = [\"DejaVu Sans\", \"sans-serif\"] print(\"警告: 未找到中文字体,图表文字可能无法正确显示\") app = FunctionVisualizer(root) root.mainloop()if __name__ == \"__main__\": main()