Skip to content

Commit 0ddfe47

Browse files
Merge pull request #60 from jinwandalaohu66/submission/script_mndbzbxy
2 parents 6cb97e7 + 8abae28 commit 0ddfe47

2 files changed

Lines changed: 315 additions & 2 deletions

File tree

script_library/index.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"format_version": 1,
3-
"data_version": 41,
4-
"updated": "2026-03-29T13:08:52.907Z",
3+
"data_version": 42,
4+
"updated": "2026-03-30T15:16:31.856Z",
55
"announcement": null,
66
"categories": [
77
{
@@ -1005,6 +1005,29 @@
10051005
"updated": null,
10061006
"status": "active",
10071007
"lines": 130
1008+
},
1009+
{
1010+
"id": "script_mndbzbxy",
1011+
"name": "函数绘制器",
1012+
"name_en": "函数绘制器",
1013+
"desc": "输入函数解析式可以直接生成函数图象",
1014+
"desc_en": "输入函数解析式可以直接生成函数图象",
1015+
"category": "basic",
1016+
"file": "scripts/basic/script_mndbzbxy.py",
1017+
"thumbnail": null,
1018+
"version": 1,
1019+
"file_type": "py",
1020+
"author": "Nemo",
1021+
"author_en": "Nemo",
1022+
"tags": [
1023+
"community"
1024+
],
1025+
"requires": [],
1026+
"min_app_version": "1.5.0",
1027+
"added": "2026-03-30",
1028+
"updated": null,
1029+
"status": "active",
1030+
"lines": 290
10081031
}
10091032
]
10101033
}
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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

Comments
 (0)