Python实现数学函数画图器

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学习&实践爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值