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。其他提到的模块要么是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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

学习&认知实践爱好者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值