Python实现数学函数画图器
函数图像是数学的望远镜与显微镜。中学生学习数学函数,使用数学函数图像工具是很有意义的。
打包成品下载地址见附录。
现在介绍使用python设计一个GUI界面的函数画图器。
输入函数,如
y=2x^2+1.3x+1
(x/3)^2+(y/2)^2=1
1/4x^2+1/4xy+1/3y^2-3/8x+1/9y-2=0
y-2x^(1/3)=0
解决了一般分数幂x^(a/b),如y= x^(1/3)图像不全问题,根据我原先另一个版本设计情况这是一个难点,解决下了很大功夫,成功解决。
①对于一般函数(方程),自变量和因变量分别用 x 和 y 表示。
②对于参数方程,自变量用 t 表示,因变量用 x 和 y 表示,两个参数方程用英文分号分隔。如 x=3cos(t) ; y=4sin(t)
③对于极坐标方程,自变量用 t 表示,因变量用 r 表示。如 r=4/(1+0.5cos(t)) ,注:为了输入方便考虑,不使用θ和ρ。
运行效果
这个程序涉及到多个Python模块/包/库。下面简要说明功能及哪些需要安装:
numpy:这是一个用于科学计算的基础库,提供了多维数组对象以及对数组进行操作的大量函数。通常需要通过pip安装(除非你的环境中已经预装了它)。
matplotlib及其组件(matplotlib.pyplot, matplotlib.backends.backend_tkagg, NavigationToolbar2Tk):matplotlib是一个绘图库,用于生成出版质量的图表。pyplot是其提供的一种类似MATLAB的绘图接口,而后两个模块是与Tkinter结合使用时需要用到的部分。需要安装(除非你的环境中已经预装了它)。
tkinter:这是Python的标准GUI库,通常随Python解释器一起分发,因此不需要单独安装。但在某些Linux发行版上可能需要单独安装Python-tk包。
sympy:这是一个用于符号数学计算的Python库。如果你需要进行代数、微积分等符号运算,就需要安装(除非你的环境中已经预装了它)。
re:这是Python的内置模块,用于正则表达式操作,因此不需要安装。
math:这是Python的内置模块,提供了数学函数定义(如三角函数、对数等),也不需要安装。
fractions:这也是一个Python标准库,用于分数计算,不需要安装。
总之,你需要通过pip或其他方式安装的是numpy, matplotlib, 和 sympy。其他提到的模块要么是Python的标准库(如re, math, fractions),要么通常是随Python一起提供的。
打包好的exe文件,地址:https://download.csdn.net/download/cnds123/90796911
源码如下:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sympy as sp
import re
import math
import fractions
class HelpWindow:
def __init__(self, parent):
self.window = tk.Toplevel(parent)
self.window.title("使用帮助")
self.window.geometry("620x590")
self.window.resizable(True, True)
# self.window.transient(parent) # 设置它将限制了窗口的某些功能,如没有最小化等
# self.window.grab_set() # 模态窗口
# 保持窗口始终在最前面,但不阻止主窗口操作
self.window.attributes('-topmost', True)
# 创建帮助内容框架
main_frame = ttk.Frame(self.window, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text="函数画图器使用说明", font=("Arial", 14, "bold"))
title_label.pack(pady=(0, 20))
# 创建文本区域
help_text = """
函数画图器支持三种类型的函数绘制:
1. 普通函数(方程)
• 自变量和因变量分别用 x 和 y 表示
• 显式函数格式:y = f(x)
例如:y = 2x^2 + 1.3x + 1
y=abs(x^2+x-2)+x
• 隐函数格式:F(x,y) = G(x,y)
例如:(x/3)^2 + (y/2)^2 = 1
y^2-(x^3+1)=0 或 y^2=(x^3+1)
2. 参数方程
• 自变量用 t 表示,因变量用 x 和 y 表示
• 两个参数方程用英文分号分隔
• 格式:x = f(t); y = g(t)
例如:x = 3cos(t); y = 4sin(t)
x=4Cos(2t)Sin(t) ; y=4Sin(2t)Sin(t)
3. 极坐标方程
• 自变量用 t 表示,因变量用 r 表示
• 格式:r = f(t)
例如:r = 4/(1+0.5cos(t))
r=3(Sin(2t)+2Sin(3t))
这个程序支持的数学函数和数学常量是通过NumPy库提供的。有所调整,解决了一般分数幂x^(a/b),如y=x^(1/3)图像不全问题(这是一个难点)。
支持的数学函数:
sin(x), cos(x), tan(x), exp(x), log(x), sqrt(x), abs(x)
支持的常量:
pi, e
例如:y=e^x+pi/2
使用步骤:
1. 选择函数类型
2. 输入函数表达式
3. 设置坐标范围
4. 点击"绘制图像"按钮
“显示模式”按钮是一个切换式按钮:“显示模式:等比例”绘图区显示为正方形,“显示模式:自适应”绘图区显示处于自适应状态。你可以画一个圆x^2 + y^2 = 9,在两种模式看看差别。
"""
# 使用文本框显示帮助内容,允许滚动
text_frame = ttk.Frame(main_frame)
text_frame.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
help_text_widget = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set)
help_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
help_text_widget.insert(tk.END, help_text)
help_text_widget.config(state=tk.DISABLED) # 设置为只读
scrollbar.config(command=help_text_widget.yview)
# 关闭按钮
close_button = ttk.Button(main_frame, text="关闭", command=self.window.destroy)
close_button.pack(pady=10)
# 居中窗口
self.center_window()
def center_window(self):
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
class FunctionPlotter:
def __init__(self, root):
self.root = root
self.root.title("函数画图器 - V1.02 设计:WKJ")
self.root.geometry("1000x800")
# 设置matplotlib中文支持
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei','SimHei','DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 创建主框架
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 创建一个水平框架来包含参数区和函数列表
top_frame = ttk.Frame(self.main_frame)
top_frame.pack(fill=tk.X, pady=5)
# 创建参数区
self.param_frame = ttk.LabelFrame(top_frame, text="参数设置", padding="10")
self.param_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 函数输入框
ttk.Label(self.param_frame, text="函数表达式:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.func_entry = ttk.Entry(self.param_frame, width=40)
self.func_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
self.func_entry.insert(0, "y=2x^2+1.3x+1")
# 函数类型选择
ttk.Label(self.param_frame, text="函数类型:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.func_type = tk.StringVar(value="普通函数")
self.type_combo = ttk.Combobox(self.param_frame, textvariable=self.func_type,
values=["普通函数", "参数方程", "极坐标方程"])
self.type_combo.grid(row=1, column=1, sticky=tk.W, pady=5)
# 在这里添加画笔线条宽度选择
ttk.Label(self.param_frame, text="画笔线条宽度:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.linewidth_var = tk.DoubleVar(value=1.5)
linewidth_spin = ttk.Spinbox(self.param_frame, from_=0.5, to=4.0, increment=0.5,
textvariable=self.linewidth_var, width=5)
linewidth_spin.grid(row=2, column=1, sticky=tk.W, pady=5)
# 坐标范围设置
range_frame = ttk.Frame(self.param_frame)
range_frame.grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=5) # 行号改为3
ttk.Label(range_frame, text="x范围:").grid(row=0, column=0, sticky=tk.W)
self.x_min = ttk.Entry(range_frame, width=8)
self.x_min.insert(0, "-10")
self.x_min.grid(row=0, column=1, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=2, sticky=tk.W)
self.x_max = ttk.Entry(range_frame, width=8)
self.x_max.insert(0, "10")
self.x_max.grid(row=0, column=3, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="y范围:").grid(row=0, column=4, sticky=tk.W, padx=(20, 0))
self.y_min = ttk.Entry(range_frame, width=8)
self.y_min.insert(0, "-10")
self.y_min.grid(row=0, column=5, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=6, sticky=tk.W)
self.y_max = ttk.Entry(range_frame, width=8)
self.y_max.insert(0, "10")
self.y_max.grid(row=0, column=7, sticky=tk.W, padx=2)
# 参数方程和极坐标方程的t范围
ttk.Label(range_frame, text="t范围:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0))
self.t_min = ttk.Entry(range_frame, width=8)
self.t_min.insert(0, "0")
self.t_min.grid(row=1, column=1, sticky=tk.W, padx=2, pady=(5, 0))
ttk.Label(range_frame, text="到").grid(row=1, column=2, sticky=tk.W, pady=(5, 0))
self.t_max = ttk.Entry(range_frame, width=8)
self.t_max.insert(0, "2*pi")
self.t_max.grid(row=1, column=3, sticky=tk.W, padx=2, pady=(5, 0))
# 按钮区域
button_frame = ttk.Frame(self.param_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=10)
# 绘图按钮
self.plot_button = ttk.Button(button_frame, text="绘制图像", command=self.plot_function)
self.plot_button.pack(side=tk.LEFT, padx=5)
# 帮助按钮
self.help_button = ttk.Button(button_frame, text="使用帮助", command=self.show_help)
self.help_button.pack(side=tk.LEFT, padx=5)
# 清除按钮
self.clear_button = ttk.Button(button_frame, text="清除图像", command=self.clear_plot)
self.clear_button.pack(side=tk.LEFT, padx=5)
# 保存图像按钮
self.save_button = ttk.Button(button_frame, text="保存图像", command=self.save_figure)
self.save_button.pack(side=tk.LEFT, padx=5)
# 显示模式切换按钮(默认为等比例显示)
self.is_square_aspect = True
self.aspect_button = ttk.Button(button_frame, text="显示模式:等比例", command=self.toggle_aspect)
self.aspect_button.pack(side=tk.LEFT, padx=5)
# 添加函数列表显示区域 - 放在参数区域的右侧
func_list_frame = ttk.LabelFrame(top_frame, text="已绘制函数列表", padding="10")
func_list_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# 创建文本框和滚动条的容器框架
list_container = ttk.Frame(func_list_frame)
list_container.pack(fill=tk.BOTH, expand=True)
# 使用文本框显示函数列表
self.func_list_text = tk.Text(list_container, height=10, width=40, wrap=tk.WORD)
# 创建滚动条并设置它始终可见
scrollbar = ttk.Scrollbar(list_container, command=self.func_list_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 先包装滚动条
self.func_list_text.configure(yscrollcommand=scrollbar.set)
self.func_list_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 后包装文本框
# 设置文本框为只读
self.func_list_text.config(state=tk.DISABLED)
# 创建绘图区
self.plot_frame = ttk.LabelFrame(self.main_frame, text="绘图区", padding="10")
self.plot_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 创建matplotlib图形
self.fig, self.ax = plt.subplots(figsize=(8, 6))
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 添加matplotlib工具栏
toolbar_frame = ttk.Frame(self.plot_frame)
toolbar_frame.pack(fill=tk.X)
toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)
toolbar.update()
# 简短提示
tip_text = "提示: 点击“绘制图像”按钮添加新函数,函数会显示在右侧列表中"
self.tip_label = ttk.Label(self.main_frame, text=tip_text, foreground="gray")
self.tip_label.pack(fill=tk.X, pady=2)
# 初始化绘图区,但不立即绘图
self.init_plot()
# 函数计数器
self.function_counter = 0
# 颜色循环列表
self.colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
# 强制布局更新
self.root.update_idletasks()
# 设置初始状态
if self.is_square_aspect:
self.aspect_button.config(text="显示模式:等比例")
self.ax.set_aspect('equal')
else:
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
self.fig.tight_layout()
self.canvas.draw()
def update_grid(self):
"""更新网格线,确保水平和垂直方向的网格线数量相同"""
# 获取当前坐标范围
x_min, x_max = self.ax.get_xlim()
y_min, y_max = self.ax.get_ylim()
# 固定的网格线数量(可以根据需要调整)
grid_count = 10
# 设置主刻度定位器
self.ax.xaxis.set_major_locator(plt.MaxNLocator(grid_count))
self.ax.yaxis.set_major_locator(plt.MaxNLocator(grid_count))
# 绘制网格
self.ax.grid(True)
def toggle_aspect(self):
"""在等比例和自适应显示模式之间切换"""
self.is_square_aspect = not self.is_square_aspect
if self.is_square_aspect:
# 切换到等比例模式
self.aspect_button.config(text="显示模式:等比例")
self.set_square_aspect()
else:
# 切换到自适应模式
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
# 添加以下行以确保布局正确
self.fig.tight_layout()
self.canvas.draw()
def set_square_aspect(self):
"""设置绘图区域为正方形显示(等比例坐标轴)"""
try:
# 获取当前坐标范围
x_min, x_max = self.ax.get_xlim()
y_min, y_max = self.ax.get_ylim()
# 设置等比例
self.ax.set_aspect('equal')
# 确保原始范围不变
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
# 调整图形布局以适应正方形显示
self.fig.tight_layout()
# 重新绘制
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"设置正方形显示时出错: {str(e)}")
def show_help(self):
"""显示帮助窗口"""
HelpWindow(self.root)
def clear_plot(self):
"""清除当前图像和函数列表"""
self.init_plot()
# 清除函数列表
self.func_list_text.config(state=tk.NORMAL)
self.func_list_text.delete(1.0, tk.END)
self.func_list_text.config(state=tk.DISABLED)
# 重置函数计数器
self.function_counter = 0
def init_plot(self):
"""初始化绘图区但不绘制实际图形"""
self.ax.clear()
self.ax.grid(True)
self.ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
self.ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
self.ax.set_xlim(-10, 10)
self.ax.set_ylim(-10, 10)
self.ax.set_title("点击“绘制图像”按钮以绘制函数图像")
# 应用当前的显示模式
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
# 更新网格线
self.update_grid()
# 确保布局正确
self.fig.tight_layout()
self.canvas.draw()
def add_function_to_list(self, func_str, func_type, color):
"""添加函数到列表显示区域"""
self.function_counter += 1
# 根据函数类型生成描述
if func_type == "普通函数":
description = f"函数 {self.function_counter}: {func_str}"
elif func_type == "参数方程":
description = f"参数方程 {self.function_counter}: {func_str}"
else: # 极坐标方程
description = f"极坐标方程 {self.function_counter}: {func_str}"
# 添加到文本框
self.func_list_text.config(state=tk.NORMAL)
# 使用颜色标记
color_hex = color if isinstance(color, str) else '#%02x%02x%02x' % (
int(color[0]*255), int(color[1]*255), int(color[2]*255))
# 创建颜色标签
tag_name = f"color_{self.function_counter}"
self.func_list_text.tag_configure(tag_name, foreground=color_hex)
# 插入文本
self.func_list_text.insert(tk.END, description + "\n", tag_name)
self.func_list_text.config(state=tk.DISABLED)
return self.function_counter
def save_figure(self):
"""保存当前图像到文件"""
try:
# 获取当前图形是否有内容
if len(self.ax.get_lines()) == 0 and len(self.ax.collections) == 0:
messagebox.showinfo("提示", "当前没有图像可保存,请先绘制图像。")
return
# 打开文件保存对话框
file_types = [
('PNG 图像', '*.png'),
('JPEG 图像', '*.jpg;*.jpeg'),
('SVG 矢量图', '*.svg'),
('PDF 文档', '*.pdf'),
('所有文件', '*.*')
]
file_path = filedialog.asksaveasfilename(
title="保存图像",
filetypes=file_types,
defaultextension=".png"
)
if not file_path: # 用户取消了对话框
return
# 保存图像前应用当前显示设置
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
# 保存图像
self.fig.savefig(file_path, dpi=300, bbox_inches='tight')
messagebox.showinfo("成功", f"图像已保存至:\n{file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存图像时出错: {str(e)}")
def parse_expr(self, expr_str):
"""预处理表达式,替换常见的数学函数和常量"""
# 处理大写的三角函数名称
expr_str = re.sub(r'(?<![a-zA-Z])Sin\(', 'sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Cos\(', 'cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Tan\(', 'tan(', expr_str, flags=re.IGNORECASE)
# 插入乘号:将 3(表达式) 类型的表达式转换为 3*(表达式)
expr_str = re.sub(r'(\d)(\()', r'\1*\2', expr_str)
# 插入乘号:将 2x 类型的表达式转换为 2*x
expr_str = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 cos(t)sin(t) 类型的表达式转换为 cos(t)*sin(t)
expr_str = re.sub(r'(\))([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 2sin(x) 类型的表达式转换为 2*sin(x)
expr_str = re.sub(r'(\d)(?=sin|cos|tan|exp|log|sqrt|abs)', r'\1*', expr_str, flags=re.IGNORECASE)
# 替换^为**
expr_str = expr_str.replace("^", "**")
# 替换常见的三角函数和其他函数
expr_str = re.sub(r'(?<![a-zA-Z])sin\(', 'np.sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])cos\(', 'np.cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])tan\(', 'np.tan(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])exp\(', 'np.exp(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])log\(', 'np.log(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])sqrt\(', 'np.sqrt(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])abs\(', 'np.abs(', expr_str, flags=re.IGNORECASE)
# 替换常量
expr_str = expr_str.replace('pi', 'np.pi')
expr_str = expr_str.replace('e', 'np.e')
# 处理立方根:将 x**(1/3) 替换为 np.cbrt(x)
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*1\s*/\s*3\s*\)', r'np.cbrt(\1)', expr_str, flags=re.IGNORECASE)
# 处理一般分数幂 - 最后处理,避免被其他替换影响
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*(\d+)\s*/\s*(\d+)\s*\)',
r'self.fractional_power(\1, \2, \3)',
expr_str, flags=re.IGNORECASE)
return expr_str
def fractional_power(self, x, numerator, denominator):
"""
安全计算分数幂,正确处理负数
"""
# 将参数转换为整数
numerator = int(numerator)
denominator = int(denominator)
# 分子为奇数且分母为奇数时,保留负号
if numerator % 2 == 1 and denominator % 2 == 1:
return np.sign(x) * np.abs(x)**(numerator/denominator)
# 其他情况,如分母为偶数或x为负数时,使用常规计算
else:
# 创建结果数组
if hasattr(x, 'shape'):
result = np.zeros_like(x, dtype=float)
# 处理正数部分
mask_positive = x >= 0
result[mask_positive] = x[mask_positive]**(numerator/denominator)
# 负数的偶分母幂为复数,在实数域中为NaN
result[~mask_positive] = np.nan
else:
if x >= 0:
result = x**(numerator/denominator)
else:
result = np.nan
return result
def evaluate_range(self, range_str):
"""安全地评估范围表达式"""
# 处理常见的数学常量
if 'pi' in range_str.lower():
range_str = range_str.lower().replace('pi', str(np.pi))
try:
# 直接使用eval,但提供安全的命名空间
safe_dict = {'np': np, 'pi': np.pi, 'e': np.e, 'sin': np.sin, 'cos': np.cos,
'tan': np.tan, 'sqrt': np.sqrt, 'log': np.log, 'exp': np.exp}
return float(eval(range_str, {"__builtins__": {}}, safe_dict))
except Exception as e:
try:
# 如果上面失败,尝试使用sympy评估
return float(sp.sympify(range_str).evalf())
except Exception as e2:
raise ValueError(f"无法评估范围表达式: {range_str}, 错误: {str(e2)}")
def plot_function(self):
try:
# 获取当前输入的函数信息
func_str = self.func_entry.get().strip()
func_type = self.func_type.get()
if not func_str:
messagebox.showwarning("警告", "请输入函数表达式")
return
# 获取坐标范围
x_min = self.evaluate_range(self.x_min.get())
x_max = self.evaluate_range(self.x_max.get())
y_min = self.evaluate_range(self.y_min.get())
y_max = self.evaluate_range(self.y_max.get())
# 设置坐标轴(不清除之前的图像)
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
self.ax.grid(True)
# 选择颜色(循环使用颜色列表)
color = self.colors[self.function_counter % len(self.colors)]
# 根据函数类型绘制函数
if func_type == "普通函数":
func_num = self.plot_regular_function(func_str, x_min, x_max, y_min, y_max, color)
elif func_type == "参数方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
func_num = self.plot_parametric_function(func_str, t_min, t_max, color)
elif func_type == "极坐标方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
func_num = self.plot_polar_function(func_str, t_min, t_max, color)
# 添加函数到列表
self.add_function_to_list(func_str, func_type, color)
# 更新图表标题
if self.function_counter == 1:
self.ax.set_title(f"函数图像")
else:
self.ax.set_title(f"多函数比较图像")
# 更新网格线
self.update_grid()
# 根据当前模式应用相应的显示方式
if self.is_square_aspect:
self.set_square_aspect()
else:
self.ax.set_aspect('auto')
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"绘图错误: {str(e)}")
import traceback
traceback.print_exc() # 打印详细错误信息到控制台
def plot_regular_function(self, func_str, x_min, x_max, y_min, y_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 检查是否是显式函数 (y = f(x))
if "=" in func_str:
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) == 2 and parts[0].lower() == "y":
# 显式函数 y = f(x)
expr = parts[1]
expr = self.parse_expr(expr)
x = np.linspace(x_min, x_max, 1000)
# 使用numpy直接计算
try:
def f(x):
# 创建安全的局部命名空间,包含self以访问fractional_power
local_dict = {'x': x, 'np': np, 'self': self}
return eval(expr, {"__builtins__": {}}, local_dict)
y = f(x)
# 处理标量结果 - 处理常数函数情况
if np.isscalar(y):
y = np.full_like(x, y) # 创建与x相同大小的数组,所有元素都是y
# 过滤掉无穷大和NaN值
mask = np.isfinite(y)
# 绘制函数
func_num = self.function_counter + 1
self.ax.plot(x[mask], y[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x[mask], y[mask], color=color, label=f"{func_num}: {func_str}") # 绘制函数并添加序号标签
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x[mask]) // 2 # 取中间点
## if idx < len(x[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x[mask][idx], y[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算函数: {func_str}, 错误: {str(e)}")
# 检查是否是隐函数 (F(x,y) = 0 或 F(x,y) = G(x,y))
else:
left_side = parts[0]
right_side = parts[1]
# 预处理两边的表达式
left_side = self.parse_expr(left_side)
right_side = self.parse_expr(right_side)
# 将方程转换为 F(x,y) = 0 的形式
equation = f"({left_side}) - ({right_side})"
# 创建网格点
x = np.linspace(x_min, x_max, 300)
y = np.linspace(y_min, y_max, 300)
X, Y = np.meshgrid(x, y)
try:
def f(x, y):
local_dict = {'x': x, 'y': y, 'np': np, 'self': self}
return eval(equation, {"__builtins__": {}}, local_dict)
Z = f(X, Y)
# 绘制等高线 (F(x,y) = 0)
func_num = self.function_counter + 1
contour = self.ax.contour(X, Y, Z, levels=[0], colors=[color], linewidths=linewidth)
## # 添加标签
## fmt = {0: f'{func_num}'}
## self.ax.clabel(contour, inline=True, fontsize=10, fmt=fmt)
##
## # 添加到图例
## proxy = plt.Line2D([0], [0], color=color, linestyle='-')
## self.ax.legend([proxy], [f"{func_num}: {func_str}"], loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算隐函数: {func_str}, 错误: {str(e)}")
else:
raise ValueError("无法识别的函数格式。请使用 y=f(x) 或 F(x,y)=G(x,y) 格式")
def plot_parametric_function(self, func_str, t_min, t_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 分割参数方程
if ";" not in func_str:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
equations = [eq.strip() for eq in func_str.split(";")]
if len(equations) != 2:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
x_eq = equations[0]
y_eq = equations[1]
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
x_parts = [p.strip() for p in x_eq.split("=", 1)]
y_parts = [p.strip() for p in y_eq.split("=", 1)]
if len(x_parts) != 2 or len(y_parts) != 2:
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
if x_parts[0].lower() != "x" or y_parts[0].lower() != "y":
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
# 提取表达式
x_expr = self.parse_expr(x_parts[1])
y_expr = self.parse_expr(y_parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def x_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(x_expr, {"__builtins__": {}}, local_dict)
def y_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(y_expr, {"__builtins__": {}}, local_dict)
x_vals = x_func(t)
y_vals = y_func(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x_vals) & np.isfinite(y_vals)
# 绘制参数曲线
func_num = self.function_counter + 1
line, = self.ax.plot(x_vals[mask], y_vals[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x_vals[mask], y_vals[mask], color=color, label=f"{func_num}: {func_str}")
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x_vals[mask]) // 2 # 取中间点
## if idx < len(x_vals[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x_vals[mask][idx], y_vals[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算参数方程: {func_str}, 错误: {str(e)}")
def plot_polar_function(self, func_str, t_min, t_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) != 2 or parts[0].lower() != "r":
raise ValueError("极坐标方程应使用格式: r=f(t)")
# 提取表达式
r_expr = self.parse_expr(parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def r_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(r_expr, {"__builtins__": {}}, local_dict)
r = r_func(t)
# 转换为笛卡尔坐标
x = r * np.cos(t)
y = r * np.sin(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x) & np.isfinite(y)
# 绘制极坐标线
func_num = self.function_counter + 1
line, = self.ax.plot(x[mask], y[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x[mask], y[mask], color=color, label=f"{func_num}: {func_str}")
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x[mask]) // 2 # 取中间点
## if idx < len(x[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x[mask][idx], y[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算极坐标方程: {func_str}, 错误: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
# 应对Matplotlib 的图形区域没有正确显示(显示不完整)
root.state('zoomed') # 设置窗口初始状态为最大化
root.after(100, lambda: root.state('normal')) # 可选 延迟 还原
root.mainloop()
--早期备份-
运行效果
源码如下:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sympy as sp
import re
import math
import fractions # 确保已导入fractions
class HelpWindow:
def __init__(self, parent):
self.window = tk.Toplevel(parent)
self.window.title("使用帮助")
self.window.geometry("620x590")
self.window.resizable(True, True)
# self.window.transient(parent) # 设置它将限制了窗口的某些功能,如没有最小化等
# self.window.grab_set() # 模态窗口
# 保持窗口始终在最前面,但不阻止主窗口操作
self.window.attributes('-topmost', True)
# 创建帮助内容框架
main_frame = ttk.Frame(self.window, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text="函数画图器使用说明", font=("Arial", 14, "bold"))
title_label.pack(pady=(0, 20))
# 创建文本区域
help_text = """
函数画图器支持三种类型的函数绘制:
1. 普通函数(方程)
• 自变量和因变量分别用 x 和 y 表示
• 显式函数格式:y = f(x)
例如:y = 2x^2 + 1.3x + 1
y=abs(x^2+x-2)+x
• 隐函数格式:F(x,y) = G(x,y)
例如:(x/3)^2 + (y/2)^2 = 1
y^2-(x^3+1)=0 或 y^2=(x^3+1)
2. 参数方程
• 自变量用 t 表示,因变量用 x 和 y 表示
• 两个参数方程用英文分号分隔
• 格式:x = f(t); y = g(t)
例如:x = 3cos(t); y = 4sin(t)
x=4Cos(2t)Sin(t) ; y=4Sin(2t)Sin(t)
3. 极坐标方程
• 自变量用 t 表示,因变量用 r 表示
• 格式:r = f(t)
例如:r = 4/(1+0.5cos(t))
r=3(Sin(2t)+2Sin(3t))
这个程序支持的数学函数和数学常量是通过NumPy库提供的。有所调整,解决了一般分数幂x^(a/b),如y=x^(1/3)图像不全问题(这是一个难点)。
支持的数学函数:
sin(x), cos(x), tan(x), exp(x), log(x), sqrt(x), abs(x)
支持的常量:
pi, e
例如:y=e^x+pi/2
使用步骤:
1. 选择函数类型
2. 输入函数表达式
3. 设置坐标范围
4. 点击"绘制图像"按钮
“显示模式”按钮是一个切换式按钮:“显示模式:等比例”绘图区显示为正方形,“显示模式:自适应”绘图区显示处于自适应状态。你可以画一个圆x^2 + y^2 = 9,在两种模式看看差别。
"""
# 使用文本框显示帮助内容,允许滚动
text_frame = ttk.Frame(main_frame)
text_frame.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
help_text_widget = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set)
help_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
help_text_widget.insert(tk.END, help_text)
help_text_widget.config(state=tk.DISABLED) # 设置为只读
scrollbar.config(command=help_text_widget.yview)
# 关闭按钮
close_button = ttk.Button(main_frame, text="关闭", command=self.window.destroy)
close_button.pack(pady=10)
# 居中窗口
self.center_window()
def center_window(self):
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
class FunctionPlotter:
def __init__(self, root):
self.root = root
self.root.title("函数画图器 - V1.02 设计:WKJ")
self.root.geometry("1000x800")
# 设置matplotlib中文支持
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei','SimHei','DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 创建主框架
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 创建一个水平框架来包含参数区和函数列表
top_frame = ttk.Frame(self.main_frame)
top_frame.pack(fill=tk.X, pady=5)
# 创建参数区
self.param_frame = ttk.LabelFrame(top_frame, text="参数设置", padding="10")
self.param_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 函数输入框
ttk.Label(self.param_frame, text="函数表达式:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.func_entry = ttk.Entry(self.param_frame, width=40)
self.func_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
self.func_entry.insert(0, "y=2x^2+1.3x+1")
# 函数类型选择
ttk.Label(self.param_frame, text="函数类型:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.func_type = tk.StringVar(value="普通函数")
self.type_combo = ttk.Combobox(self.param_frame, textvariable=self.func_type,
values=["普通函数", "参数方程", "极坐标方程"])
self.type_combo.grid(row=1, column=1, sticky=tk.W, pady=5)
# 在这里添加画笔线条宽度选择
ttk.Label(self.param_frame, text="画笔线条宽度:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.linewidth_var = tk.DoubleVar(value=1.5)
linewidth_spin = ttk.Spinbox(self.param_frame, from_=0.5, to=4.0, increment=0.5,
textvariable=self.linewidth_var, width=5)
linewidth_spin.grid(row=2, column=1, sticky=tk.W, pady=5)
# 坐标范围设置
range_frame = ttk.Frame(self.param_frame)
range_frame.grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=5) # 行号改为3
ttk.Label(range_frame, text="x范围:").grid(row=0, column=0, sticky=tk.W)
self.x_min = ttk.Entry(range_frame, width=8)
self.x_min.insert(0, "-10")
self.x_min.grid(row=0, column=1, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=2, sticky=tk.W)
self.x_max = ttk.Entry(range_frame, width=8)
self.x_max.insert(0, "10")
self.x_max.grid(row=0, column=3, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="y范围:").grid(row=0, column=4, sticky=tk.W, padx=(20, 0))
self.y_min = ttk.Entry(range_frame, width=8)
self.y_min.insert(0, "-10")
self.y_min.grid(row=0, column=5, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=6, sticky=tk.W)
self.y_max = ttk.Entry(range_frame, width=8)
self.y_max.insert(0, "10")
self.y_max.grid(row=0, column=7, sticky=tk.W, padx=2)
# 参数方程和极坐标方程的t范围
ttk.Label(range_frame, text="t范围:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0))
self.t_min = ttk.Entry(range_frame, width=8)
self.t_min.insert(0, "0")
self.t_min.grid(row=1, column=1, sticky=tk.W, padx=2, pady=(5, 0))
ttk.Label(range_frame, text="到").grid(row=1, column=2, sticky=tk.W, pady=(5, 0))
self.t_max = ttk.Entry(range_frame, width=8)
self.t_max.insert(0, "2*pi")
self.t_max.grid(row=1, column=3, sticky=tk.W, padx=2, pady=(5, 0))
# 按钮区域
button_frame = ttk.Frame(self.param_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=10)
# 绘图按钮
self.plot_button = ttk.Button(button_frame, text="绘制图像", command=self.plot_function)
self.plot_button.pack(side=tk.LEFT, padx=5)
# 帮助按钮
self.help_button = ttk.Button(button_frame, text="使用帮助", command=self.show_help)
self.help_button.pack(side=tk.LEFT, padx=5)
# 清除按钮
self.clear_button = ttk.Button(button_frame, text="清除图像", command=self.clear_plot)
self.clear_button.pack(side=tk.LEFT, padx=5)
# 保存图像按钮
self.save_button = ttk.Button(button_frame, text="保存图像", command=self.save_figure)
self.save_button.pack(side=tk.LEFT, padx=5)
# 显示模式切换按钮(默认为等比例显示)
self.is_square_aspect = True
self.aspect_button = ttk.Button(button_frame, text="显示模式:等比例", command=self.toggle_aspect)
self.aspect_button.pack(side=tk.LEFT, padx=5)
# 添加函数列表显示区域 - 放在参数区域的右侧
func_list_frame = ttk.LabelFrame(top_frame, text="已绘制函数列表", padding="10")
func_list_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# 创建文本框和滚动条的容器框架
list_container = ttk.Frame(func_list_frame)
list_container.pack(fill=tk.BOTH, expand=True)
# 使用文本框显示函数列表
self.func_list_text = tk.Text(list_container, height=10, width=40, wrap=tk.WORD)
# 创建滚动条并设置它始终可见
scrollbar = ttk.Scrollbar(list_container, command=self.func_list_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 先包装滚动条
self.func_list_text.configure(yscrollcommand=scrollbar.set)
self.func_list_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 后包装文本框
# 设置文本框为只读
self.func_list_text.config(state=tk.DISABLED)
# 创建绘图区
self.plot_frame = ttk.LabelFrame(self.main_frame, text="绘图区", padding="10")
self.plot_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 创建matplotlib图形
self.fig, self.ax = plt.subplots(figsize=(8, 6))
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 添加matplotlib工具栏
toolbar_frame = ttk.Frame(self.plot_frame)
toolbar_frame.pack(fill=tk.X)
toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)
toolbar.update()
# 简短提示
tip_text = "提示: 点击“绘制图像”按钮添加新函数,函数会显示在右侧列表中"
self.tip_label = ttk.Label(self.main_frame, text=tip_text, foreground="gray")
self.tip_label.pack(fill=tk.X, pady=2)
# 初始化绘图区,但不立即绘图
self.init_plot()
# 函数计数器
self.function_counter = 0
# 颜色循环列表
self.colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
# 强制布局更新
self.root.update_idletasks()
# 设置初始状态
if self.is_square_aspect:
self.aspect_button.config(text="显示模式:等比例")
self.ax.set_aspect('equal')
else:
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
self.fig.tight_layout()
self.canvas.draw()
def toggle_aspect(self):
"""在等比例和自适应显示模式之间切换"""
self.is_square_aspect = not self.is_square_aspect
if self.is_square_aspect:
# 切换到等比例模式
self.aspect_button.config(text="显示模式:等比例")
self.set_square_aspect()
else:
# 切换到自适应模式
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
# 添加以下行以确保布局正确
self.fig.tight_layout()
self.canvas.draw()
def set_square_aspect(self):
"""设置绘图区域为正方形显示(等比例坐标轴)"""
try:
# 获取当前坐标范围
x_min, x_max = self.ax.get_xlim()
y_min, y_max = self.ax.get_ylim()
# 设置等比例
self.ax.set_aspect('equal')
# 确保原始范围不变
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
# 调整图形布局以适应正方形显示
self.fig.tight_layout()
# 重新绘制
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"设置正方形显示时出错: {str(e)}")
def show_help(self):
"""显示帮助窗口"""
HelpWindow(self.root)
def clear_plot(self):
"""清除当前图像和函数列表"""
self.init_plot()
# 清除函数列表
self.func_list_text.config(state=tk.NORMAL)
self.func_list_text.delete(1.0, tk.END)
self.func_list_text.config(state=tk.DISABLED)
# 重置函数计数器
self.function_counter = 0
def init_plot(self):
"""初始化绘图区但不绘制实际图形"""
self.ax.clear()
self.ax.grid(True)
self.ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
self.ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
self.ax.set_xlim(-10, 10)
self.ax.set_ylim(-10, 10)
self.ax.set_title("点击“绘制图像”按钮以绘制函数图像")
# 应用当前的显示模式
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
# 确保布局正确
self.fig.tight_layout()
self.canvas.draw()
def add_function_to_list(self, func_str, func_type, color):
"""添加函数到列表显示区域"""
self.function_counter += 1
# 根据函数类型生成描述
if func_type == "普通函数":
description = f"函数 {self.function_counter}: {func_str}"
elif func_type == "参数方程":
description = f"参数方程 {self.function_counter}: {func_str}"
else: # 极坐标方程
description = f"极坐标方程 {self.function_counter}: {func_str}"
# 添加到文本框
self.func_list_text.config(state=tk.NORMAL)
# 使用颜色标记
color_hex = color if isinstance(color, str) else '#%02x%02x%02x' % (
int(color[0]*255), int(color[1]*255), int(color[2]*255))
# 创建颜色标签
tag_name = f"color_{self.function_counter}"
self.func_list_text.tag_configure(tag_name, foreground=color_hex)
# 插入文本
self.func_list_text.insert(tk.END, description + "\n", tag_name)
self.func_list_text.config(state=tk.DISABLED)
return self.function_counter
def save_figure(self):
"""保存当前图像到文件"""
try:
# 获取当前图形是否有内容
if len(self.ax.get_lines()) == 0 and len(self.ax.collections) == 0:
messagebox.showinfo("提示", "当前没有图像可保存,请先绘制图像。")
return
# 打开文件保存对话框
file_types = [
('PNG 图像', '*.png'),
('JPEG 图像', '*.jpg;*.jpeg'),
('SVG 矢量图', '*.svg'),
('PDF 文档', '*.pdf'),
('所有文件', '*.*')
]
file_path = filedialog.asksaveasfilename(
title="保存图像",
filetypes=file_types,
defaultextension=".png"
)
if not file_path: # 用户取消了对话框
return
# 保存图像前应用当前显示设置
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
# 保存图像
self.fig.savefig(file_path, dpi=300, bbox_inches='tight')
messagebox.showinfo("成功", f"图像已保存至:\n{file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存图像时出错: {str(e)}")
def parse_expr(self, expr_str):
"""预处理表达式,替换常见的数学函数和常量"""
# 处理大写的三角函数名称
expr_str = re.sub(r'(?<![a-zA-Z])Sin\(', 'sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Cos\(', 'cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Tan\(', 'tan(', expr_str, flags=re.IGNORECASE)
# 插入乘号:将 3(表达式) 类型的表达式转换为 3*(表达式)
expr_str = re.sub(r'(\d)(\()', r'\1*\2', expr_str)
# 插入乘号:将 2x 类型的表达式转换为 2*x
expr_str = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 cos(t)sin(t) 类型的表达式转换为 cos(t)*sin(t)
expr_str = re.sub(r'(\))([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 2sin(x) 类型的表达式转换为 2*sin(x)
expr_str = re.sub(r'(\d)(?=sin|cos|tan|exp|log|sqrt|abs)', r'\1*', expr_str, flags=re.IGNORECASE)
# 替换^为**
expr_str = expr_str.replace("^", "**")
# 替换常见的三角函数和其他函数
expr_str = re.sub(r'(?<![a-zA-Z])sin\(', 'np.sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])cos\(', 'np.cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])tan\(', 'np.tan(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])exp\(', 'np.exp(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])log\(', 'np.log(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])sqrt\(', 'np.sqrt(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])abs\(', 'np.abs(', expr_str, flags=re.IGNORECASE)
# 替换常量
expr_str = expr_str.replace('pi', 'np.pi')
expr_str = expr_str.replace('e', 'np.e')
# 处理立方根:将 x**(1/3) 替换为 np.cbrt(x)
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*1\s*/\s*3\s*\)', r'np.cbrt(\1)', expr_str, flags=re.IGNORECASE)
# 处理一般分数幂 - 最后处理,避免被其他替换影响
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*(\d+)\s*/\s*(\d+)\s*\)',
r'self.fractional_power(\1, \2, \3)',
expr_str, flags=re.IGNORECASE)
return expr_str
def fractional_power(self, x, numerator, denominator):
"""
安全计算分数幂,正确处理负数
"""
# 将参数转换为整数
numerator = int(numerator)
denominator = int(denominator)
# 分子为奇数且分母为奇数时,保留负号
if numerator % 2 == 1 and denominator % 2 == 1:
return np.sign(x) * np.abs(x)**(numerator/denominator)
# 其他情况,如分母为偶数或x为负数时,使用常规计算
else:
# 创建结果数组
if hasattr(x, 'shape'):
result = np.zeros_like(x, dtype=float)
# 处理正数部分
mask_positive = x >= 0
result[mask_positive] = x[mask_positive]**(numerator/denominator)
# 负数的偶分母幂为复数,在实数域中为NaN
result[~mask_positive] = np.nan
else:
if x >= 0:
result = x**(numerator/denominator)
else:
result = np.nan
return result
def evaluate_range(self, range_str):
"""安全地评估范围表达式"""
# 处理常见的数学常量
if 'pi' in range_str.lower():
range_str = range_str.lower().replace('pi', str(np.pi))
try:
# 直接使用eval,但提供安全的命名空间
safe_dict = {'np': np, 'pi': np.pi, 'e': np.e, 'sin': np.sin, 'cos': np.cos,
'tan': np.tan, 'sqrt': np.sqrt, 'log': np.log, 'exp': np.exp}
return float(eval(range_str, {"__builtins__": {}}, safe_dict))
except Exception as e:
try:
# 如果上面失败,尝试使用sympy评估
return float(sp.sympify(range_str).evalf())
except Exception as e2:
raise ValueError(f"无法评估范围表达式: {range_str}, 错误: {str(e2)}")
def plot_function(self):
try:
# 获取当前输入的函数信息
func_str = self.func_entry.get().strip()
func_type = self.func_type.get()
if not func_str:
messagebox.showwarning("警告", "请输入函数表达式")
return
# 获取坐标范围
x_min = self.evaluate_range(self.x_min.get())
x_max = self.evaluate_range(self.x_max.get())
y_min = self.evaluate_range(self.y_min.get())
y_max = self.evaluate_range(self.y_max.get())
# 设置坐标轴(不清除之前的图像)
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
self.ax.grid(True)
# 选择颜色(循环使用颜色列表)
color = self.colors[self.function_counter % len(self.colors)]
# 根据函数类型绘制函数
if func_type == "普通函数":
func_num = self.plot_regular_function(func_str, x_min, x_max, y_min, y_max, color)
elif func_type == "参数方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
func_num = self.plot_parametric_function(func_str, t_min, t_max, color)
elif func_type == "极坐标方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
func_num = self.plot_polar_function(func_str, t_min, t_max, color)
# 添加函数到列表
self.add_function_to_list(func_str, func_type, color)
# 更新图表标题
if self.function_counter == 1:
self.ax.set_title(f"函数图像")
else:
self.ax.set_title(f"多函数比较图像")
# 根据当前模式应用相应的显示方式
if self.is_square_aspect:
self.set_square_aspect()
else:
self.ax.set_aspect('auto')
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"绘图错误: {str(e)}")
import traceback
traceback.print_exc() # 打印详细错误信息到控制台
def plot_regular_function(self, func_str, x_min, x_max, y_min, y_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 检查是否是显式函数 (y = f(x))
if "=" in func_str:
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) == 2 and parts[0].lower() == "y":
# 显式函数 y = f(x)
expr = parts[1]
expr = self.parse_expr(expr)
x = np.linspace(x_min, x_max, 1000)
# 使用numpy直接计算
try:
def f(x):
# 创建安全的局部命名空间,包含self以访问fractional_power
local_dict = {'x': x, 'np': np, 'self': self}
return eval(expr, {"__builtins__": {}}, local_dict)
y = f(x)
# 处理标量结果 - 处理常数函数情况
if np.isscalar(y):
y = np.full_like(x, y) # 创建与x相同大小的数组,所有元素都是y
# 过滤掉无穷大和NaN值
mask = np.isfinite(y)
# 绘制函数
func_num = self.function_counter + 1
self.ax.plot(x[mask], y[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x[mask], y[mask], color=color, label=f"{func_num}: {func_str}") # 绘制函数并添加序号标签
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x[mask]) // 2 # 取中间点
## if idx < len(x[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x[mask][idx], y[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算函数: {func_str}, 错误: {str(e)}")
# 检查是否是隐函数 (F(x,y) = 0 或 F(x,y) = G(x,y))
else:
left_side = parts[0]
right_side = parts[1]
# 预处理两边的表达式
left_side = self.parse_expr(left_side)
right_side = self.parse_expr(right_side)
# 将方程转换为 F(x,y) = 0 的形式
equation = f"({left_side}) - ({right_side})"
# 创建网格点
x = np.linspace(x_min, x_max, 300)
y = np.linspace(y_min, y_max, 300)
X, Y = np.meshgrid(x, y)
try:
def f(x, y):
local_dict = {'x': x, 'y': y, 'np': np, 'self': self}
return eval(equation, {"__builtins__": {}}, local_dict)
Z = f(X, Y)
# 绘制等高线 (F(x,y) = 0)
func_num = self.function_counter + 1
contour = self.ax.contour(X, Y, Z, levels=[0], colors=[color], linewidths=linewidth)
## # 添加标签
## fmt = {0: f'{func_num}'}
## self.ax.clabel(contour, inline=True, fontsize=10, fmt=fmt)
##
## # 添加到图例
## proxy = plt.Line2D([0], [0], color=color, linestyle='-')
## self.ax.legend([proxy], [f"{func_num}: {func_str}"], loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算隐函数: {func_str}, 错误: {str(e)}")
else:
raise ValueError("无法识别的函数格式。请使用 y=f(x) 或 F(x,y)=G(x,y) 格式")
def plot_parametric_function(self, func_str, t_min, t_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 分割参数方程
if ";" not in func_str:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
equations = [eq.strip() for eq in func_str.split(";")]
if len(equations) != 2:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
x_eq = equations[0]
y_eq = equations[1]
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
x_parts = [p.strip() for p in x_eq.split("=", 1)]
y_parts = [p.strip() for p in y_eq.split("=", 1)]
if len(x_parts) != 2 or len(y_parts) != 2:
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
if x_parts[0].lower() != "x" or y_parts[0].lower() != "y":
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
# 提取表达式
x_expr = self.parse_expr(x_parts[1])
y_expr = self.parse_expr(y_parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def x_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(x_expr, {"__builtins__": {}}, local_dict)
def y_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(y_expr, {"__builtins__": {}}, local_dict)
x_vals = x_func(t)
y_vals = y_func(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x_vals) & np.isfinite(y_vals)
# 绘制参数曲线
func_num = self.function_counter + 1
line, = self.ax.plot(x_vals[mask], y_vals[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x_vals[mask], y_vals[mask], color=color, label=f"{func_num}: {func_str}")
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x_vals[mask]) // 2 # 取中间点
## if idx < len(x_vals[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x_vals[mask][idx], y_vals[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算参数方程: {func_str}, 错误: {str(e)}")
def plot_polar_function(self, func_str, t_min, t_max, color='blue'):
# 获取用户设置的线条宽度
linewidth = self.linewidth_var.get()
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) != 2 or parts[0].lower() != "r":
raise ValueError("极坐标方程应使用格式: r=f(t)")
# 提取表达式
r_expr = self.parse_expr(parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def r_func(t):
local_dict = {'t': t, 'np': np, 'self': self}
return eval(r_expr, {"__builtins__": {}}, local_dict)
r = r_func(t)
# 转换为笛卡尔坐标
x = r * np.cos(t)
y = r * np.sin(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x) & np.isfinite(y)
# 绘制极坐标线
func_num = self.function_counter + 1
line, = self.ax.plot(x[mask], y[mask], color=color, linewidth=linewidth)
## line, = self.ax.plot(x[mask], y[mask], color=color, label=f"{func_num}: {func_str}")
##
## # 在图上标记函数序号
## # 找到一个合适的点来放置标签
## idx = len(x[mask]) // 2 # 取中间点
## if idx < len(x[mask]):
## self.ax.annotate(f"{func_num}",
## xy=(x[mask][idx], y[mask][idx]),
## xytext=(10, 0),
## textcoords="offset points",
## color=color,
## fontweight='bold')
##
## # 显示图例
## self.ax.legend(loc='best')
return func_num
except Exception as e:
raise ValueError(f"无法计算极坐标方程: {func_str}, 错误: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
# 应对Matplotlib 的图形区域没有正确显示(显示不完整)
root.state('zoomed') # 设置窗口初始状态为最大化
root.after(100, lambda: root.state('normal')) # 可选 延迟 还原
root.mainloop()
--早期备份--
源码如下:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import ttk, messagebox
import sympy as sp
import re
import math
class HelpWindow:
def __init__(self, parent):
self.window = tk.Toplevel(parent)
self.window.title("使用帮助")
self.window.geometry("620x590")
self.window.resizable(True, True)
# self.window.transient(parent) # 设置它将限制了窗口的某些功能,如没有最小化等
# self.window.grab_set() # 模态窗口
# 保持窗口始终在最前面,但不阻止主窗口操作
self.window.attributes('-topmost', True)
# 创建帮助内容框架
main_frame = ttk.Frame(self.window, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text="函数画图器使用说明", font=("Arial", 14, "bold"))
title_label.pack(pady=(0, 20))
# 创建文本区域
help_text = """
函数画图器支持三种类型的函数绘制:
1. 普通函数(方程)
• 自变量和因变量分别用 x 和 y 表示
• 显式函数格式:y = f(x)
例如:y = 2x^2 + 1.3x + 1
y=abs(x^2+x-2)+x
• 隐函数格式:F(x,y) = G(x,y)
例如:(x/3)^2 + (y/2)^2 = 1
y^2-(x^3+1)=0 或 y^2=(x^3+1)
2. 参数方程
• 自变量用 t 表示,因变量用 x 和 y 表示
• 两个参数方程用英文分号分隔
• 格式:x = f(t); y = g(t)
例如:x = 3cos(t); y = 4sin(t)
x=4Cos(2t)Sin(t) ; y=4Sin(2t)Sin(t)
3. 极坐标方程
• 自变量用 t 表示,因变量用 r 表示
• 格式:r = f(t)
例如:r = 4/(1+0.5cos(t))
r=3(Sin(2t)+2Sin(3t))
这个程序支持的数学函数和数学常量是通过 NumPy 库提供的。
支持的数学函数:
sin(x), cos(x), tan(x), exp(x), log(x), sqrt(x), abs(x)
支持的常量:
pi, e
例如:y=e^x+pi/2
使用步骤:
1. 选择函数类型
2. 输入函数表达式
3. 设置坐标范围
4. 点击"绘制图像"按钮
"""
# 使用文本框显示帮助内容,允许滚动
text_frame = ttk.Frame(main_frame)
text_frame.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
help_text_widget = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set)
help_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
help_text_widget.insert(tk.END, help_text)
help_text_widget.config(state=tk.DISABLED) # 设置为只读
scrollbar.config(command=help_text_widget.yview)
# 关闭按钮
close_button = ttk.Button(main_frame, text="关闭", command=self.window.destroy)
close_button.pack(pady=10)
# 居中窗口
self.center_window()
def center_window(self):
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
class FunctionPlotter:
def __init__(self, root):
self.root = root
self.root.title("函数画图器")
self.root.geometry("1000x800")
# 设置matplotlib中文支持
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei','SimHei','DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 创建主框架
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 创建参数区
self.param_frame = ttk.LabelFrame(self.main_frame, text="参数设置", padding="10")
self.param_frame.pack(fill=tk.X, pady=5)
# 函数输入框
ttk.Label(self.param_frame, text="函数表达式:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.func_entry = ttk.Entry(self.param_frame, width=50)
self.func_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
self.func_entry.insert(0, "y=2x^2+1.3x+1")
# 函数类型选择
ttk.Label(self.param_frame, text="函数类型:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.func_type = tk.StringVar(value="普通函数")
self.type_combo = ttk.Combobox(self.param_frame, textvariable=self.func_type,
values=["普通函数", "参数方程", "极坐标方程"])
self.type_combo.grid(row=1, column=1, sticky=tk.W, pady=5)
# 坐标范围设置
range_frame = ttk.Frame(self.param_frame)
range_frame.grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=5)
ttk.Label(range_frame, text="x范围:").grid(row=0, column=0, sticky=tk.W)
self.x_min = ttk.Entry(range_frame, width=8)
self.x_min.insert(0, "-10")
self.x_min.grid(row=0, column=1, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=2, sticky=tk.W)
self.x_max = ttk.Entry(range_frame, width=8)
self.x_max.insert(0, "10")
self.x_max.grid(row=0, column=3, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="y范围:").grid(row=0, column=4, sticky=tk.W, padx=(20, 0))
self.y_min = ttk.Entry(range_frame, width=8)
self.y_min.insert(0, "-10")
self.y_min.grid(row=0, column=5, sticky=tk.W, padx=2)
ttk.Label(range_frame, text="到").grid(row=0, column=6, sticky=tk.W)
self.y_max = ttk.Entry(range_frame, width=8)
self.y_max.insert(0, "10")
self.y_max.grid(row=0, column=7, sticky=tk.W, padx=2)
# 参数方程和极坐标方程的t范围
ttk.Label(range_frame, text="t范围:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0))
self.t_min = ttk.Entry(range_frame, width=8)
self.t_min.insert(0, "0")
self.t_min.grid(row=1, column=1, sticky=tk.W, padx=2, pady=(5, 0))
ttk.Label(range_frame, text="到").grid(row=1, column=2, sticky=tk.W, pady=(5, 0))
self.t_max = ttk.Entry(range_frame, width=8)
self.t_max.insert(0, "2*pi")
self.t_max.grid(row=1, column=3, sticky=tk.W, padx=2, pady=(5, 0))
# 按钮区域
button_frame = ttk.Frame(self.param_frame)
button_frame.grid(row=3, column=0, columnspan=2, pady=10)
# 绘图按钮
self.plot_button = ttk.Button(button_frame, text="绘制图像", command=self.plot_function)
self.plot_button.pack(side=tk.LEFT, padx=5)
# 帮助按钮
self.help_button = ttk.Button(button_frame, text="使用帮助", command=self.show_help)
self.help_button.pack(side=tk.LEFT, padx=5)
# 清除按钮
self.clear_button = ttk.Button(button_frame, text="清除图像", command=self.clear_plot)
self.clear_button.pack(side=tk.LEFT, padx=5)
# 显示模式切换按钮(默认为等比例显示)
self.is_square_aspect = True
self.aspect_button = ttk.Button(button_frame, text="显示模式:等比例", command=self.toggle_aspect)
self.aspect_button.pack(side=tk.LEFT, padx=5)
# 创建绘图区
self.plot_frame = ttk.LabelFrame(self.main_frame, text="绘图区", padding="10")
self.plot_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 创建matplotlib图形
self.fig, self.ax = plt.subplots(figsize=(8, 6))
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 简短提示
tip_text = "提示: 点击“使用帮助”按钮获取详细使用说明"
self.tip_label = ttk.Label(self.main_frame, text=tip_text, foreground="gray")
self.tip_label.pack(fill=tk.X, pady=2)
# 初始化绘图区,但不立即绘图
self.init_plot()
# 强制布局更新
self.root.update_idletasks()
# 设置初始状态
if self.is_square_aspect:
self.aspect_button.config(text="显示模式:等比例")
self.ax.set_aspect('equal')
else:
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
self.fig.tight_layout()
self.canvas.draw()
## # 确保显示模式与按钮文本一致
## if self.is_square_aspect:
## self.ax.set_aspect('equal')
## else:
## self.ax.set_aspect('auto')
## self.canvas.draw()
def toggle_aspect(self):
"""在等比例和自适应显示模式之间切换"""
self.is_square_aspect = not self.is_square_aspect
if self.is_square_aspect:
# 切换到等比例模式
self.aspect_button.config(text="显示模式:等比例")
self.set_square_aspect()
else:
# 切换到自适应模式
self.aspect_button.config(text="显示模式:自适应")
self.ax.set_aspect('auto')
# 添加以下行以确保布局正确
self.fig.tight_layout()
self.canvas.draw()
def set_square_aspect(self):
"""设置绘图区域为正方形显示(等比例坐标轴)"""
try:
# 获取当前坐标范围
x_min, x_max = self.ax.get_xlim()
y_min, y_max = self.ax.get_ylim()
# 设置等比例
self.ax.set_aspect('equal')
# 确保原始范围不变
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
# 调整图形布局以适应正方形显示
self.fig.tight_layout()
# 重新绘制
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"设置正方形显示时出错: {str(e)}")
def show_help(self):
"""显示帮助窗口"""
HelpWindow(self.root)
def clear_plot(self):
"""清除当前图像"""
self.init_plot()
def init_plot(self):
"""初始化绘图区但不绘制实际图形"""
self.ax.clear()
self.ax.grid(True)
self.ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
self.ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
self.ax.set_xlim(-10, 10)
self.ax.set_ylim(-10, 10)
self.ax.set_title("点击“绘制图像”按钮以绘制函数图像")
# 应用当前的显示模式
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
# 确保布局正确
self.fig.tight_layout()
self.canvas.draw()
## def parse_expr(self, expr_str):
## """预处理表达式,替换常见的数学函数和常量"""
## # 处理大写的三角函数名称
## expr_str = re.sub(r'(?<![a-zA-Z])Sin\(', 'sin(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])Cos\(', 'cos(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])Tan\(', 'tan(', expr_str, flags=re.IGNORECASE)
##
## # 插入乘号:将 3(表达式) 类型的表达式转换为 3*(表达式)
## expr_str = re.sub(r'(\d)(\()', r'\1*\2', expr_str)
##
## # 插入乘号:将 2x 类型的表达式转换为 2*x
## expr_str = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', expr_str)
##
## # 插入乘号:将 cos(t)sin(t) 类型的表达式转换为 cos(t)*sin(t)
## expr_str = re.sub(r'(\))([a-zA-Z])', r'\1*\2', expr_str)
##
## # 插入乘号:将 2sin(x) 类型的表达式转换为 2*sin(x)
## expr_str = re.sub(r'(\d)(?=sin|cos|tan|exp|log|sqrt|abs)', r'\1*', expr_str, flags=re.IGNORECASE)
##
## # 替换^为**
## expr_str = expr_str.replace("^", "**")
##
## # 替换常见的三角函数和其他函数
## expr_str = re.sub(r'(?<![a-zA-Z])sin\(', 'np.sin(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])cos\(', 'np.cos(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])tan\(', 'np.tan(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])exp\(', 'np.exp(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])log\(', 'np.log(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])sqrt\(', 'np.sqrt(', expr_str, flags=re.IGNORECASE)
## expr_str = re.sub(r'(?<![a-zA-Z])abs\(', 'np.abs(', expr_str, flags=re.IGNORECASE)
##
## # 替换常量
## expr_str = expr_str.replace('pi', 'np.pi')
## expr_str = expr_str.replace('e', 'np.e')
##
## return expr_str
def fractional_power(self, x, numerator, denominator):
"""
安全计算分数幂,正确处理负数
"""
# 将参数转换为整数
numerator = int(numerator)
denominator = int(denominator)
# 分子为奇数且分母为奇数时,保留负号
if numerator % 2 == 1 and denominator % 2 == 1:
return np.sign(x) * np.abs(x)**(numerator/denominator)
# 其他情况,如分母为偶数或x为负数时,使用常规计算
else:
# 创建结果数组
if hasattr(x, 'shape'):
result = np.zeros_like(x, dtype=float)
# 处理正数部分
mask_positive = x >= 0
result[mask_positive] = x[mask_positive]**(numerator/denominator)
# 负数的偶分母幂为复数,在实数域中为NaN
result[~mask_positive] = np.nan
else:
if x >= 0:
result = x**(numerator/denominator)
else:
result = np.nan
return result
def parse_expr(self, expr_str):
"""预处理表达式,替换常见的数学函数和常量"""
# 处理大写的三角函数名称
expr_str = re.sub(r'(?<![a-zA-Z])Sin\(', 'sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Cos\(', 'cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])Tan\(', 'tan(', expr_str, flags=re.IGNORECASE)
# 插入乘号:将 3(表达式) 类型的表达式转换为 3*(表达式)
expr_str = re.sub(r'(\d)(\()', r'\1*\2', expr_str)
# 插入乘号:将 2x 类型的表达式转换为 2*x
expr_str = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 cos(t)sin(t) 类型的表达式转换为 cos(t)*sin(t)
expr_str = re.sub(r'(\))([a-zA-Z])', r'\1*\2', expr_str)
# 插入乘号:将 2sin(x) 类型的表达式转换为 2*sin(x)
expr_str = re.sub(r'(\d)(?=sin|cos|tan|exp|log|sqrt|abs)', r'\1*', expr_str, flags=re.IGNORECASE)
# 替换^为**
expr_str = expr_str.replace("^", "**")
# 替换常见的三角函数和其他函数
expr_str = re.sub(r'(?<![a-zA-Z])sin\(', 'np.sin(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])cos\(', 'np.cos(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])tan\(', 'np.tan(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])exp\(', 'np.exp(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])log\(', 'np.log(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])sqrt\(', 'np.sqrt(', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'(?<![a-zA-Z])abs\(', 'np.abs(', expr_str, flags=re.IGNORECASE)
# 替换常量
expr_str = expr_str.replace('pi', 'np.pi')
expr_str = expr_str.replace('e', 'np.e')
# 处理立方根:将 x**(1/3) 替换为 np.cbrt(x)
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*1\s*/\s*3\s*\)', r'np.cbrt(\1)', expr_str, flags=re.IGNORECASE)
# 处理一般分数幂 - 最后处理,避免被其他替换影响—重要!
expr_str = re.sub(r'(\b\w+\b|\([^()]+\))\*\*\s*\(\s*(\d+)\s*/\s*(\d+)\s*\)',
r'self.fractional_power(\1, \2, \3)',
expr_str, flags=re.IGNORECASE)
# 调试打印,查看最终表达式
print(f"处理后的表达式: {expr_str}")
return expr_str
def evaluate_range(self, range_str):
"""安全地评估范围表达式"""
# 处理常见的数学常量
if 'pi' in range_str.lower():
range_str = range_str.lower().replace('pi', str(np.pi))
try:
# 直接使用eval,但提供安全的命名空间
safe_dict = {'np': np, 'pi': np.pi, 'e': np.e, 'sin': np.sin, 'cos': np.cos,
'tan': np.tan, 'sqrt': np.sqrt, 'log': np.log, 'exp': np.exp}
return float(eval(range_str, {"__builtins__": {}}, safe_dict))
except Exception as e:
try:
# 如果上面失败,尝试使用sympy评估
return float(sp.sympify(range_str).evalf())
except Exception as e2:
raise ValueError(f"无法评估范围表达式: {range_str}, 错误: {str(e2)}")
def plot_function(self):
try:
self.ax.clear()
# 获取坐标范围
x_min = self.evaluate_range(self.x_min.get())
x_max = self.evaluate_range(self.x_max.get())
y_min = self.evaluate_range(self.y_min.get())
y_max = self.evaluate_range(self.y_max.get())
# 设置坐标轴
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
self.ax.grid(True)
self.ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
self.ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
func_type = self.func_type.get()
func_str = self.func_entry.get()
if func_type == "普通函数":
self.plot_regular_function(func_str, x_min, x_max, y_min, y_max)
elif func_type == "参数方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
self.plot_parametric_function(func_str, t_min, t_max)
elif func_type == "极坐标方程":
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
self.plot_polar_function(func_str, t_min, t_max)
# 根据当前模式应用相应的显示方式(删除重复的代码)
if self.is_square_aspect:
self.set_square_aspect()
else:
self.ax.set_aspect('auto')
self.canvas.draw()
except Exception as e:
messagebox.showerror("错误", f"绘图错误: {str(e)}")
import traceback
traceback.print_exc() # 打印详细错误信息到控制台
def plot_regular_function(self, func_str, x_min, x_max, y_min, y_max):
# 检查是否是显式函数 (y = f(x))
if "=" in func_str:
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) == 2 and parts[0].lower() == "y":
# 显式函数 y = f(x)
expr = parts[1]
expr = self.parse_expr(expr)
x = np.linspace(x_min, x_max, 1000)
# 使用numpy直接计算
try:
def f(x):
# 创建安全的局部命名空间,包含self以访问fractional_power方法
local_dict = {'x': x, 'np': np, 'self': self}
return eval(expr, {"__builtins__": {}}, local_dict)
y = f(x)
# 处理标量结果 - 处理常数函数情况
if np.isscalar(y):
y = np.full_like(x, y) # 创建与x相同大小的数组,所有元素都是y
# 过滤掉无穷大和NaN值
mask = np.isfinite(y)
self.ax.plot(x[mask], y[mask])
self.ax.set_title(f"函数: {func_str}")
except Exception as e:
raise ValueError(f"无法计算函数: {func_str}, 错误: {str(e)}")
# 检查是否是隐函数 (F(x,y) = 0 或 F(x,y) = G(x,y))
else:
left_side = parts[0]
right_side = parts[1]
# 预处理两边的表达式
left_side = self.parse_expr(left_side)
right_side = self.parse_expr(right_side)
# 将方程转换为 F(x,y) = 0 的形式
equation = f"({left_side}) - ({right_side})"
# 创建网格点
x = np.linspace(x_min, x_max, 300)
y = np.linspace(y_min, y_max, 300)
X, Y = np.meshgrid(x, y)
try:
def f(x, y):
local_dict = {'x': x, 'y': y, 'np': np}
return eval(equation, {"__builtins__": {}}, local_dict)
Z = f(X, Y)
# 绘制等高线 (F(x,y) = 0)
self.ax.contour(X, Y, Z, levels=[0], colors='blue')
self.ax.set_title(f"隐函数: {func_str}")
except Exception as e:
raise ValueError(f"无法计算隐函数: {func_str}, 错误: {str(e)}")
else:
raise ValueError("无法识别的函数格式。请使用 y=f(x) 或 F(x,y)=G(x,y) 格式")
def plot_parametric_function(self, func_str, t_min, t_max):
# 分割参数方程
if ";" not in func_str:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
equations = [eq.strip() for eq in func_str.split(";")]
if len(equations) != 2:
raise ValueError("参数方程应包含两个方程,用分号(;)分隔")
x_eq = equations[0]
y_eq = equations[1]
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
x_parts = [p.strip() for p in x_eq.split("=", 1)]
y_parts = [p.strip() for p in y_eq.split("=", 1)]
if len(x_parts) != 2 or len(y_parts) != 2:
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
if x_parts[0].lower() != "x" or y_parts[0].lower() != "y":
raise ValueError("参数方程应使用格式: x=f(t); y=g(t)")
# 提取表达式
x_expr = self.parse_expr(x_parts[1])
y_expr = self.parse_expr(y_parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def x_func(t):
local_dict = {'t': t, 'np': np}
return eval(x_expr, {"__builtins__": {}}, local_dict)
def y_func(t):
local_dict = {'t': t, 'np': np}
return eval(y_expr, {"__builtins__": {}}, local_dict)
x_vals = x_func(t)
y_vals = y_func(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x_vals) & np.isfinite(y_vals)
# 绘制参数曲线
self.ax.plot(x_vals[mask], y_vals[mask])
self.ax.set_title(f"参数方程: {func_str}")
except Exception as e:
raise ValueError(f"无法计算参数方程: {func_str}, 错误: {str(e)}")
def plot_polar_function(self, func_str, t_min, t_max):
# 检查格式 - 更宽松的格式检查,允许等号前后有空格
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) != 2 or parts[0].lower() != "r":
raise ValueError("极坐标方程应使用格式: r=f(t)")
# 提取表达式
r_expr = self.parse_expr(parts[1])
# 创建参数值
t = np.linspace(t_min, t_max, 1000)
try:
def r_func(t):
local_dict = {'t': t, 'np': np}
return eval(r_expr, {"__builtins__": {}}, local_dict)
r = r_func(t)
# 转换为笛卡尔坐标
x = r * np.cos(t)
y = r * np.sin(t)
# 过滤掉无穷大和NaN值
mask = np.isfinite(x) & np.isfinite(y)
# 绘制极坐标线
self.ax.plot(x[mask], y[mask])
self.ax.set_title(f"极坐标方程: {func_str}")
except Exception as e:
raise ValueError(f"无法计算极坐标方程: {func_str}, 错误: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
# 应对Matplotlib 的图形区域没有正确显示(显示不完整)
root.state('zoomed') # 设置窗口初始状态为最大化
root.after(100, lambda: root.state('normal')) # 可选 延迟 还原
root.mainloop()
特别提示
Matplotlib 的图形区域没有正确显示(显示不完整),但是当窗口最大化后又正常显示了。这是 Tkinter 和 Matplotlib 整合时常见的问题,主要原因是:
Matplotlib 图形区域需要时间完全渲染和调整
在适当的地方,强制多次更新布局
self.root.update()
self.root.after(100, self.root.update) # 延迟再次更新
或者
直接让窗口以最大化状态启动:
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
root.state('zoomed') # 以最大化状态启动
root.mainloop()
或者
你也可以在主程序中添加以下代码,来确保窗口在显示前已经完全初始化:
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
# 应对Matplotlib 的图形区域没有正确显示(显示不完整)
root.state('zoomed') # 设置窗口初始状态为最大化
root.after(100, lambda: root.state('normal')) # 可选 延迟 还原
root.mainloop()
附录
打包可执行文件,win10通过,下载地址见
https://download.csdn.net/download/cnds123/90796911