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。其他提到的模块要么是Python的标准库(如re、 fractions),通常是随Python一起提供的。
修订版
运行截图:

源码如下
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 re
import traceback
import os
import sys
# ---------- 资源路径定位(打包 EXE 必备) ----------
def resource_path(relative_path):
""" 获取打包后的资源绝对路径,兼容 PyInstaller 单文件打包模式 """
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# ---------- 帮助窗口 ----------
class HelpWindow:
def __init__(self, parent):
self.window = tk.Toplevel(parent)
self.window.title("使用帮助")
self.window.geometry("650x620")
self.window.resizable(True, True)
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, 15))
help_text = """
函数画图器支持三种类型的函数绘制:
1. 普通函数(方程)
• 自变量和因变量分别用 x 和 y 表示
• 显式函数格式:y = f(x)
例如:y = 2x^2 + 1.3x + 1
y = ln(x) + arcsin(x)
y = floor(x) + sgn(x)
• 隐函数格式:F(x,y) = G(x,y)
例如:xy = 1 (双曲线)
1/4x^2 + 1/4xy + 1/3y^2 - 3/8x + 1/9y - 2 = 0 (斜椭圆)
(x/3)^2 + (y/2)^2 = 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)
x = 3cos(t);y = 4sin(t) (支持中文分号)
3. 极坐标方程
• 自变量用 t 表示,因变量用 r 表示
• 格式:r = f(t)
例如:r = 4/(1+0.5cos(t))
r=3(sin(2t)+cos(2t))
--------------------------------------------------
智能输入友好:
• 完美支持中文全角符号(如 () ; = × ÷ + - ^ π),输入法无需刻意切换!
• 支持省略乘号,如 2x, xy, 5sin(2x)。
--------------------------------------------------
支持的数学常量与函数(大小写通用):
【常量】
• pi (π), e (自然常数)
【基本三角与反三角函数】
• 正弦/余弦/正切: sin(x), cos(x), tan(x)
• 反正弦/反余弦/反正切: arcsin(x) [或 asin(x)], arccos(x) [或 acos(x)], arctan(x) [或 atan(x)]
【双曲函数】
• 双曲正弦/余弦/正切: sinh(x), cosh(x), tanh(x)
【指数与对数函数】
• 指数函数: exp(x) (即 e^x)
• 自然对数: ln(x) 或 log(x)(注意:按计算机科学常规,此处log指以e为底的自然对数)
• 常用对数(以10为底): lg(x) 或 log10(x)
【其他常用函数】
• 平方根: sqrt(x)
• 绝对值: abs(x)
• 向下/向上取整: floor(x), ceil(x)
• 符号函数: sign(x) 或 sgn(x)
【分数幂处理】
• 完美支持诸如 x^(1/3) 或 x^(2/3) 等分数幂,在负数定义域内图像依然完整。
--------------------------------------------------
使用步骤:
1. 选择函数类型
2. 输入函数表达式
3. 设置坐标范围(范围输入框内同样支持输入数学常数如 2*pi)
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.strip())
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.15 设计:Wang KeJun")
# 核心优化:绑定窗口关闭协议,防止关闭后后台进程残留
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 使用 resource_path 确保打包后图标不丢失
try:
self.root.iconbitmap(resource_path('函数图像2_256x256.ico'))
except Exception:
pass
self.root.geometry("1000x800")
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=1/(log10(x+1)-x)")
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)
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)
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)
# ---------- 函数列表(Listbox + 双向滚动条) ----------
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_listbox = tk.Listbox(list_container, height=10, width=40, selectmode=tk.SINGLE)
# 配置垂直和水平双向滚动条
v_scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.func_listbox.yview)
h_scrollbar = ttk.Scrollbar(list_container, orient=tk.HORIZONTAL, command=self.func_listbox.xview)
self.func_listbox.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
# 网格布局精确定位
self.func_listbox.grid(row=0, column=0, sticky="nsew")
v_scrollbar.grid(row=0, column=1, sticky="ns")
h_scrollbar.grid(row=1, column=0, sticky="ew")
list_container.grid_rowconfigure(0, weight=1)
list_container.grid_columnconfigure(0, weight=1)
# 删除与清除按钮
list_btn_frame = ttk.Frame(func_list_frame)
list_btn_frame.pack(fill=tk.X, pady=(5, 0))
self.del_func_btn = ttk.Button(list_btn_frame, text="删除所选", command=self.delete_selected_func)
self.del_func_btn.pack(side=tk.LEFT, padx=2)
self.clear_list_btn = ttk.Button(list_btn_frame, text="清空列表", command=self.clear_plot)
self.clear_list_btn.pack(side=tk.LEFT, padx=2)
# ---------- 绘图区 ----------
self.plot_frame = ttk.LabelFrame(self.main_frame, text="绘图区", padding="10")
self.plot_frame.pack(fill=tk.BOTH, expand=True, pady=5)
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)
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.colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
# 存储函数条目
self.func_entries = []
self.function_counter = 0
if self.is_square_aspect:
self.ax.set_aspect('equal')
else:
self.ax.set_aspect('auto')
self.fig.tight_layout()
self.canvas.draw()
try:
self.root.state('zoomed')
except tk.TclError:
self.root.geometry("1200x850")
# ---------- 核心优化:安全关闭窗口并强制回收内存 ----------
def on_closing(self):
""" 彻底销毁图形引擎和 Tkinter 窗口,防止后台进程残留 """
try:
plt.close('all') # 物理释放所有 Matplotlib 构图资源
except Exception:
pass
self.root.destroy() # 销毁 Tkinter 主窗口
sys.exit(0) # 强制杀死 Python 进程
# ---------- 核心辅助:全角转半角智能转换器 ----------
def normalize_input(self, text):
"""将用户意外输入的全角/中文数学符号无缝转换为标准的英文半角符号"""
char_map = {
';': ';',
'=': '=',
'(': '(',
')': ')',
'+': '+',
'-': '-',
'—': '-',
'–': '-',
'*': '*',
'×': '*',
'/': '/',
'÷': '/',
'^': '^',
',': ',',
'。': '.',
'π': 'pi'
}
for cn_char, en_char in char_map.items():
text = text.replace(cn_char, en_char)
return text
def update_grid(self):
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_listbox.delete(0, tk.END)
self.func_entries.clear()
self.function_counter = 0
self.canvas.draw()
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, artists, color):
if func_type == "普通函数":
desc = f"函数 {self.function_counter}: {func_str}"
elif func_type == "参数方程":
desc = f"参数方程 {self.function_counter}: {func_str}"
else:
desc = f"极坐标方程 {self.function_counter}: {func_str}"
pos = self.func_listbox.size()
self.func_listbox.insert(tk.END, desc)
self.func_listbox.itemconfig(pos, {'fg': color})
self.func_entries.append({
'desc': desc,
'artists': artists,
'color': color
})
def delete_selected_func(self):
selection = self.func_listbox.curselection()
if not selection:
messagebox.showinfo("提示", "请先在右侧列表中点击选中要删除的函数。")
return
idx = selection[0]
entry = self.func_entries[idx]
for artist in entry['artists']:
try:
artist.remove()
except Exception:
pass
self.func_listbox.delete(idx)
del self.func_entries[idx]
self.canvas.draw()
if not self.func_entries:
self.ax.set_title("点击“绘制图像”按钮以绘制函数图像")
self.canvas.draw()
def save_figure(self):
try:
if not self.func_entries:
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'\bln\b', 'log', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\blg\b', 'log10', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\bsgn\b', 'sign', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\basin\b', 'arcsin', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\bacos\b', 'arccos', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\batan\b', 'arctan', expr_str, flags=re.IGNORECASE)
# 1. 插入乘号:独立变量 x, y 紧跟其他变量、数字或左括号 (解决 xy, yx, xsin(x) 等问题)
expr_str = re.sub(r'([xy])\s*(?=[a-zA-Z0-9\(])', r'\1*', expr_str, flags=re.IGNORECASE)
# 2. 插入乘号:独立变量 t 紧跟其他变量、数字或左括号 (排除 tan 和 tanh 函数被拆碎)
expr_str = re.sub(r'\bt\s*(?=[a-zA-Z0-9\(])(?!an)', r't*', expr_str, flags=re.IGNORECASE)
# 3. 插入乘号:常量 pi 和 e 紧跟其他变量或左括号 (安全排除 exp)
expr_str = re.sub(r'\bpi\s*(?=[a-zA-Z0-9\(])', r'pi*', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\be\s*(?=[yt\(]|x(?!p\())', r'e*', expr_str, flags=re.IGNORECASE)
# 4. 核心修复:插入乘号:使用 \b 确保只有独立的数字紧跟左括号才补全乘号,防止 log10(x) 变成 log10*(x)
expr_str = re.sub(r'\b(\d+)\s*(\u0028)', r'\1*\2', expr_str)
# 5. 数字紧跟字母(排除科学计数法)
expr_str = re.sub(r'(\d)\s*(?=[a-zA-Z_])(?![eE][+-]?\d)', r'\1*', expr_str)
# 6. 右括号紧跟字母或左括号
expr_str = re.sub(r'(\))\s*(?=[a-zA-Z_\(])', r'\1*', expr_str)
# 7. 替换 ^ 为 Python 的幂运算符 **
expr_str = expr_str.replace("^", "**")
# 8. 常量 π 和 e(单词边界)
expr_str = re.sub(r'\bpi\b', 'np.pi', expr_str, flags=re.IGNORECASE)
expr_str = re.sub(r'\be\b', 'np.e', expr_str, flags=re.IGNORECASE)
# 9. 添加 np. 前缀到数学函数 (优化匹配顺序:将 log10 放在 log 之前,防止前缀漏匹配)
funcs_pattern = r'\b(sin|cos|tan|arcsin|arccos|arctan|sinh|cosh|tanh|exp|log10|log|sqrt|abs|floor|ceil|sign)\b\s*(?=\()'
expr_str = re.sub(funcs_pattern, lambda m: 'np.' + m.group(1).lower(), expr_str, flags=re.IGNORECASE)
# 10. 核心优化:匹配支持一级嵌套括号的底数,使 (sin(x))**(1/3) 等复杂底数也能完美触发 np.cbrt 算法
expr_str = re.sub(r'(\b\w+\b|\((?:[^()]+|\([^()]*\))*\))\*\*\s*\(\s*1\s*/\s*3\s*\)', r'np.cbrt(\1)', expr_str, flags=re.IGNORECASE)
# 11. 核心优化:匹配支持一级嵌套括号的底数
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 denominator % 2 == 1:
if numerator == 0:
return np.ones_like(x, dtype=float) if hasattr(x, 'shape') else 1.0
return (np.sign(x) ** numerator) * np.abs(x) ** (numerator / denominator)
else:
if hasattr(x, 'shape'):
result = np.zeros_like(x, dtype=float)
mask_positive = x > 0
result[mask_positive] = x[mask_positive] ** (numerator / denominator)
if numerator == 0:
result[x == 0] = 1.0
else:
result[x == 0] = 0.0
result[x < 0] = np.nan
return result
else:
if x > 0:
return x ** (numerator / denominator)
elif x == 0:
return 1.0 if numerator == 0 else 0.0
else:
return np.nan
def evaluate_range(self, range_str):
"""解析范围字符串(支持全角智能转换、基本常量和简单运算)"""
range_str = self.normalize_input(range_str)
range_str = re.sub(r'\bpi\b', str(np.pi), range_str, flags=re.IGNORECASE)
range_str = re.sub(r'\be\b', str(np.e), range_str, flags=re.IGNORECASE)
safe_dict = {
'np': np,
'sin': np.sin, 'cos': np.cos, 'tan': np.tan,
'arcsin': np.arcsin, 'arccos': np.arccos, 'arctan': np.arctan,
'sinh': np.sinh, 'cosh': np.cosh, 'tanh': np.tanh,
'exp': np.exp, 'log': np.log, 'log10': np.log10,
'ln': np.log, 'lg': np.log10,
'sqrt': np.sqrt, 'abs': np.abs, 'floor': np.floor, 'ceil': np.ceil,
'sign': np.sign
}
try:
return float(eval(range_str, {"__builtins__": {}}, safe_dict))
except Exception as e:
raise ValueError(f"无法评估范围表达式: {range_str},错误: {str(e)}")
# ---------- 绘图主入口 ----------
def plot_function(self):
try:
# 1. 读取输入并进行全角中文符号智能归一化
func_str = self.func_entry.get().strip()
func_str = self.normalize_input(func_str)
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())
if x_min >= x_max:
messagebox.showerror("范围错误", "x 最小值必须小于最大值")
return
if y_min >= y_max:
messagebox.showerror("范围错误", "y 最小值必须小于最大值")
return
if func_type in ["参数方程", "极坐标方程"]:
t_min = self.evaluate_range(self.t_min.get())
t_max = self.evaluate_range(self.t_max.get())
if t_min >= t_max:
messagebox.showerror("范围错误", "t 最小值必须小于最大值")
return
else:
t_min = t_max = None
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
self.ax.grid(True)
self.function_counter += 1
color = self.colors[(self.function_counter - 1) % len(self.colors)]
linewidth = self.linewidth_var.get()
with np.errstate(all='ignore'):
if func_type == "普通函数":
artists = self.plot_regular_function(func_str, x_min, x_max, y_min, y_max, color, linewidth)
elif func_type == "参数方程":
artists = self.plot_parametric_function(func_str, t_min, t_max, color, linewidth)
else:
artists = self.plot_polar_function(func_str, t_min, t_max, color, linewidth)
# 添加到列表
self.add_function_to_list(func_str, func_type, artists, color)
# 更新标题
if len(self.func_entries) == 1:
self.ax.set_title("函数图像")
else:
self.ax.set_title("多函数比较图像")
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:
err_msg = f"绘图错误: {str(e)}"
messagebox.showerror("错误", err_msg)
traceback.print_exc()
# ---------- 具体绘图函数 ----------
def plot_regular_function(self, func_str, x_min, x_max, y_min, y_max, color, linewidth):
artists = []
if "=" not in func_str:
raise ValueError("无法识别的函数格式。请使用 y=f(x) 或 F(x,y)=G(x,y) 格式")
parts = [p.strip() for p in func_str.split("=", 1)]
if len(parts) == 2 and parts[0].lower() == "y":
# 显式函数
expr = self.parse_expr(parts[1])
x = np.linspace(x_min, x_max, 1000)
try:
def f(x):
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)
# 将非有限值转为 nan
y = np.where(np.isfinite(y), y, np.nan)
# 核心修复:渐近线突变断开算法(阈值优化为 1.2 倍)
if len(y) > 1:
with np.errstate(invalid='ignore'):
y_diff = np.abs(np.diff(y))
# 判定发生跳变:相邻点差值大于视口高度的 1.2 倍
threshold = (y_max - y_min) * 1.2
bad_mask = y_diff > threshold
# 物理断开:将跳变位置后一个点设为 nan
y[1:][bad_mask] = np.nan
line, = self.ax.plot(x, y, color=color, linewidth=linewidth)
artists.append(line)
return artists
except Exception as e:
raise ValueError(f"无法计算显式函数: {func_str}, 错误: {str(e)}")
else:
# 隐函数
left_side = self.parse_expr(parts[0])
right_side = self.parse_expr(parts[1])
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)
contour = self.ax.contour(X, Y, Z, levels=[0], colors=[color], linewidths=linewidth)
# 兼容 Matplotlib 3.8+ 移除了 collections 属性的问题
if hasattr(contour, 'collections'):
artists.extend(contour.collections)
else:
artists.append(contour)
return artists
except Exception as e:
raise ValueError(f"无法计算隐函数: {func_str}, 错误: {str(e)}")
def plot_parametric_function(self, func_str, t_min, t_max, color, linewidth):
if ";" not in func_str:
raise ValueError("参数方程应包含两个方程,用分号(;)或(;)分隔。格式: x=f(t); y=g(t)")
equations = [eq.strip() for eq in func_str.split(";")]
if len(equations) != 2:
raise ValueError("参数方程应包含两个方程,用分号(;)或(;)分隔。格式: x=f(t); y=g(t)")
x_eq, y_eq = equations[0], 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
x_vals = np.where(np.isfinite(x_vals), x_vals, np.nan)
y_vals = np.where(np.isfinite(y_vals), y_vals, np.nan)
# 二维空间跳变检测
if len(x_vals) > 1:
with np.errstate(invalid='ignore'):
dx = np.diff(x_vals)
dy = np.diff(y_vals)
dist = np.sqrt(dx**2 + dy**2)
diag = np.sqrt((self.ax.get_xlim()[1] - self.ax.get_xlim()[0])**2 +
(self.ax.get_ylim()[1] - self.ax.get_ylim()[0])**2)
bad_mask = dist > (diag * 1.2) # 阈值同步优化为 1.2 倍
x_vals[1:][bad_mask] = np.nan
y_vals[1:][bad_mask] = np.nan
line, = self.ax.plot(x_vals, y_vals, color=color, linewidth=linewidth)
return [line]
except Exception as e:
raise ValueError(f"无法计算参数方程: {func_str}, 错误: {str(e)}")
def plot_polar_function(self, func_str, t_min, t_max, color, linewidth):
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
x = np.where(np.isfinite(x), x, np.nan)
y = np.where(np.isfinite(y), y, np.nan)
# 二维空间跳变检测
if len(x) > 1:
with np.errstate(invalid='ignore'):
dx = np.diff(x)
dy = np.diff(y)
dist = np.sqrt(dx**2 + dy**2)
diag = np.sqrt((self.ax.get_xlim()[1] - self.ax.get_xlim()[0])**2 +
(self.ax.get_ylim()[1] - self.ax.get_ylim()[0])**2)
bad_mask = dist > (diag * 1.2) # 阈值同步优化为 1.2 倍
x[1:][bad_mask] = np.nan
y[1:][bad_mask] = np.nan
line, = self.ax.plot(x, y, color=color, linewidth=linewidth)
return [line]
except Exception as e:
raise ValueError(f"无法计算极坐标方程: {func_str}, 错误: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = FunctionPlotter(root)
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
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()
4192

被折叠的 条评论
为什么被折叠?



