1+ import tkinter as tk
2+ from tkinter import messagebox
3+ import numpy as np
4+ import matplotlib .pyplot as plt
5+ from matplotlib .backends .backend_tkagg import FigureCanvasTkAgg
6+ from matplotlib .figure import Figure
7+ from matplotlib .ticker import MultipleLocator , AutoLocator
8+ import math
9+ import re
10+
11+ # ---------------------------- 表达式预处理(支持省略乘号) ----------------------------
12+ def preprocess_expr (expr : str ) -> str :
13+ """将数学表达式转换为Python可eval的形式,自动插入*号"""
14+ expr = expr .replace ('π' , 'math.pi' ).replace ('e' , 'math.e' )
15+ expr = expr .replace ('^' , '**' )
16+ # 插入隐式乘号:数字与字母/括号之间、字母与字母/括号之间、括号与数字/字母之间
17+ pattern = r'(?<=[0-9])(?=[a-zA-Z(])|(?<=[a-zA-Z)])(?=[0-9a-zA-Z(])'
18+ expr = re .sub (pattern , '*' , expr )
19+ return expr
20+
21+ # ---------------------------- 主应用类 ----------------------------
22+ class FunctionPlotter :
23+ def __init__ (self , master ):
24+ self .master = master
25+ master .title ("函数图像绘制器" )
26+ master .geometry ("500x700" )
27+ master .configure (bg = "#f0f0f0" )
28+ master .minsize (450 , 600 )
29+
30+ self .mode = 'linear' # 'linear' 或 'quadratic'
31+ self .animation = None
32+ self .current_line = None
33+ self .fig = None
34+ self .ax = None
35+ self .canvas = None
36+
37+ self .create_widgets ()
38+ self .init_plot ()
39+
40+ def create_widgets (self ):
41+ # ---------- 顶部模式切换按钮 ----------
42+ top_frame = tk .Frame (self .master , bg = "#f0f0f0" , pady = 10 )
43+ top_frame .pack (fill = tk .X )
44+
45+ self .btn_linear = tk .Button (
46+ top_frame , text = "📈 一次函数" , font = ("微软雅黑" , 12 , "bold" ),
47+ bg = "#e0e0e0" , fg = "#333" , padx = 20 , pady = 5 , relief = tk .RAISED ,
48+ command = lambda : self .switch_mode ('linear' )
49+ )
50+ self .btn_linear .pack (side = tk .LEFT , expand = True , fill = tk .X , padx = 10 )
51+
52+ self .btn_quadratic = tk .Button (
53+ top_frame , text = "📉 二次函数" , font = ("微软雅黑" , 12 , "bold" ),
54+ bg = "#e0e0e0" , fg = "#333" , padx = 20 , pady = 5 , relief = tk .RAISED ,
55+ command = lambda : self .switch_mode ('quadratic' )
56+ )
57+ self .btn_quadratic .pack (side = tk .RIGHT , expand = True , fill = tk .X , padx = 10 )
58+
59+ self .update_button_style ()
60+
61+ # ---------- 输入区域 ----------
62+ input_frame = tk .Frame (self .master , bg = "#f0f0f0" , pady = 10 )
63+ input_frame .pack (fill = tk .X , padx = 15 )
64+
65+ tk .Label (input_frame , text = "函数表达式:y=" , font = ("微软雅黑" , 10 ), bg = "#f0f0f0" ).pack (anchor = tk .W )
66+ self .entry = tk .Entry (input_frame , font = ("Consolas" , 12 ), bg = "white" )
67+ self .entry .pack (fill = tk .X , pady = (5 ,10 ))
68+ self .entry .insert (0 , "x+1" )
69+
70+ self .plot_btn = tk .Button (
71+ input_frame , text = "🎨 绘制图像" , font = ("微软雅黑" , 10 , "bold" ),
72+ bg = "#4caf50" , fg = "white" , padx = 10 , pady = 5 ,
73+ command = self .start_plot_animation
74+ )
75+ self .plot_btn .pack ()
76+
77+ # ---------- 数学符号面板 ----------
78+ symbol_frame = tk .LabelFrame (self .master , text = "数学符号库" , font = ("微软雅黑" , 9 ), bg = "#f0f0f0" , padx = 5 , pady = 5 )
79+ symbol_frame .pack (fill = tk .X , padx = 15 , pady = 5 )
80+
81+ symbols = ['x' , 'x^2' , 'x^3' , '+' , '-' , '*' , '/' , '(' , ')' , 'π' , 'e' ,
82+ 'sin' , 'cos' , 'tan' , 'sqrt' , '^' , '**' ]
83+ row , col = 0 , 0
84+ for sym in symbols :
85+ btn = tk .Button (
86+ symbol_frame , text = sym , font = ("Consolas" , 9 ), width = 5 , relief = tk .GROOVE ,
87+ command = lambda s = sym : self .insert_symbol (s )
88+ )
89+ btn .grid (row = row , column = col , padx = 2 , pady = 2 , sticky = "ew" )
90+ col += 1
91+ if col >= 6 :
92+ col = 0
93+ row += 1
94+ for i in range (6 ):
95+ symbol_frame .columnconfigure (i , weight = 1 )
96+
97+ # ---------- 状态栏 ----------
98+ self .status = tk .Label (self .master , text = "就绪" , bd = 1 , relief = tk .SUNKEN , anchor = tk .W , bg = "#e0e0e0" )
99+ self .status .pack (side = tk .BOTTOM , fill = tk .X )
100+
101+ def init_plot (self ):
102+ """初始化 matplotlib 图形,设置坐标轴样式和固定刻度间隔为1"""
103+ self .fig = Figure (figsize = (6 , 4 ), dpi = 100 , facecolor = 'white' )
104+ self .ax = self .fig .add_subplot (111 )
105+ self .canvas = FigureCanvasTkAgg (self .fig , master = self .master )
106+ self .canvas .get_tk_widget ().pack (fill = tk .BOTH , expand = True , padx = 15 , pady = 10 )
107+
108+ self .ax .grid (True , linestyle = '--' , alpha = 0.7 )
109+ self .ax .axhline (0 , color = 'k' , linewidth = 0.8 )
110+ self .ax .axvline (0 , color = 'k' , linewidth = 0.8 )
111+ self .ax .set_xlabel ('x' , fontsize = 10 )
112+ self .ax .set_ylabel ('y' , fontsize = 10 )
113+ self .ax .set_title ('函数图像' , fontsize = 12 )
114+
115+ # x轴固定刻度间隔1,范围-10~10
116+ self .ax .set_xlim (- 10 , 10 )
117+ self .ax .xaxis .set_major_locator (MultipleLocator (1 ))
118+ self .ax .xaxis .set_minor_locator (MultipleLocator (0.5 ))
119+
120+ # y轴暂用自动,绘图时会根据数据重新调整
121+ self .ax .yaxis .set_major_locator (AutoLocator ())
122+ self .fig .tight_layout ()
123+ self .canvas .draw ()
124+
125+ def update_button_style (self ):
126+ if self .mode == 'linear' :
127+ self .btn_linear .config (bg = "#4caf50" , fg = "white" , relief = tk .SUNKEN )
128+ self .btn_quadratic .config (bg = "#e0e0e0" , fg = "#333" , relief = tk .RAISED )
129+ else :
130+ self .btn_quadratic .config (bg = "#4caf50" , fg = "white" , relief = tk .SUNKEN )
131+ self .btn_linear .config (bg = "#e0e0e0" , fg = "#333" , relief = tk .RAISED )
132+
133+ def switch_mode (self , mode ):
134+ if self .mode == mode :
135+ return
136+ self .mode = mode
137+ self .update_button_style ()
138+ if mode == 'linear' :
139+ default_expr = "2x+3"
140+ else :
141+ default_expr = "x^2+2x+1"
142+ self .entry .delete (0 , tk .END )
143+ self .entry .insert (0 , default_expr )
144+ self .status .config (text = f"已切换到{ '一次' if mode == 'linear' else '二次' } 函数模式" )
145+ self .entry .config (bg = "#ffffcc" )
146+ self .master .after (200 , lambda : self .entry .config (bg = "white" ))
147+
148+ def insert_symbol (self , symbol ):
149+ pos = self .entry .index (tk .INSERT )
150+ text = self .entry .get ()
151+ new_text = text [:pos ] + symbol + text [pos :]
152+ self .entry .delete (0 , tk .END )
153+ self .entry .insert (0 , new_text )
154+ self .entry .icursor (pos + len (symbol ))
155+ self .entry .focus_set ()
156+
157+ def evaluate_expression (self , expr_str , x_vals ):
158+ """计算表达式值,返回y数组,若全部无效则返回None"""
159+ try :
160+ processed = preprocess_expr (expr_str )
161+ namespace = {
162+ 'math' : math ,
163+ 'np' : np ,
164+ 'sqrt' : math .sqrt ,
165+ 'sin' : math .sin ,
166+ 'cos' : math .cos ,
167+ 'tan' : math .tan ,
168+ 'pi' : math .pi ,
169+ 'e' : math .e
170+ }
171+ code = compile (f'lambda x: { processed } ' , '<string>' , 'eval' )
172+ f = eval (code , namespace )
173+ y_vals = []
174+ for x in x_vals :
175+ try :
176+ y = f (x )
177+ if isinstance (y , complex ) or not np .isfinite (y ):
178+ y = np .nan
179+ y_vals .append (y )
180+ except :
181+ y_vals .append (np .nan )
182+ y_arr = np .array (y_vals )
183+ if np .all (np .isnan (y_arr )):
184+ messagebox .showerror ("无效表达式" , "表达式在所有x上均无有效值,请检查输入。" )
185+ return None
186+ return y_arr
187+ except Exception as e :
188+ messagebox .showerror ("表达式错误" , f"解析表达式出错:\n { str (e )} " )
189+ return None
190+
191+ def start_plot_animation (self ):
192+ """启动绘图动画,确保曲线正常显示"""
193+ # 停止旧动画
194+ if self .animation is not None :
195+ try :
196+ self .animation .event_source .stop ()
197+ except :
198+ pass
199+ self .animation = None
200+
201+ expr = self .entry .get ().strip ()
202+ if not expr :
203+ messagebox .showwarning ("空表达式" , "请输入函数表达式" )
204+ return
205+
206+ # 生成x点并计算y值
207+ x_vals = np .linspace (- 10 , 10 , 200 )
208+ y_vals = self .evaluate_expression (expr , x_vals )
209+ if y_vals is None :
210+ return
211+
212+ # 清除旧曲线
213+ if self .current_line is not None :
214+ self .current_line .remove ()
215+ self .current_line = None
216+
217+ # 动态调整y轴范围(过滤无效点)
218+ valid_mask = ~ np .isnan (y_vals )
219+ if np .any (valid_mask ):
220+ y_min = np .nanmin (y_vals [valid_mask ])
221+ y_max = np .nanmax (y_vals [valid_mask ])
222+ y_range = y_max - y_min
223+ if y_range < 1e-6 :
224+ y_min -= 1
225+ y_max += 1
226+ else :
227+ y_min -= 0.1 * y_range
228+ y_max += 0.1 * y_range
229+ self .ax .set_ylim (y_min , y_max )
230+ # 设置y轴刻度间隔为1(如果范围太大则自动调整)
231+ if y_max - y_min <= 20 :
232+ self .ax .yaxis .set_major_locator (MultipleLocator (1 ))
233+ else :
234+ self .ax .yaxis .set_major_locator (MultipleLocator (5 ))
235+ self .ax .yaxis .set_minor_locator (MultipleLocator (1 ))
236+ else :
237+ self .ax .set_ylim (- 5 , 5 )
238+
239+ # 创建空曲线(占位)
240+ self .current_line , = self .ax .plot ([], [], 'b-' , linewidth = 2 , label = f'y = { expr } ' )
241+ self .ax .legend (loc = 'upper right' )
242+ self .canvas .draw ()
243+
244+ # 动画参数
245+ self .total_points = len (x_vals )
246+ self .x_data = x_vals
247+ self .y_data = y_vals
248+ self .step = 0
249+
250+ # 动画更新函数
251+ def animate (i ):
252+ self .step = min (self .total_points , self .step + 5 )
253+ if self .step >= 1 :
254+ self .current_line .set_data (self .x_data [:self .step ], self .y_data [:self .step ])
255+ # 强制刷新画布
256+ self .canvas .draw_idle ()
257+ self .canvas .flush_events ()
258+ if self .step >= self .total_points :
259+ self .animation .event_source .stop ()
260+ self .status .config (text = "绘图完成" )
261+ return self .current_line ,
262+
263+ from matplotlib .animation import FuncAnimation
264+ # 帧数 = 总点数/5 + 1
265+ frames = self .total_points // 5 + 1
266+ self .animation = FuncAnimation (
267+ self .fig , animate , frames = frames ,
268+ interval = 30 , repeat = False , blit = False
269+ )
270+ self .status .config (text = "正在绘制动画..." )
271+
272+ # 备用机制:如果动画因故未显示,强制重绘一次(极少情况)
273+ def fallback_draw ():
274+ if self .current_line is not None and len (self .current_line .get_xdata ()) == 0 :
275+ self .current_line .set_data (self .x_data , self .y_data )
276+ self .canvas .draw ()
277+ self .status .config (text = "已绘制完整曲线" )
278+ self .master .after (500 , fallback_draw ) # 0.5秒后若仍无数据则直接画全
279+
280+ # 动画结束后启用按钮(粗略延时)
281+ def enable_btn ():
282+ self .plot_btn .config (state = tk .NORMAL )
283+ self .plot_btn .config (state = tk .DISABLED )
284+ self .master .after (2000 , enable_btn )
285+
286+ # ---------------------------- 主程序 ----------------------------
287+ if __name__ == "__main__" :
288+ root = tk .Tk ()
289+ app = FunctionPlotter (root )
290+ root .mainloop ()
0 commit comments