python中学物理实验模拟:瞬间推力与摩擦力作用下的物体运动
下面程序通过图形用户界面允许用户设置物体的质量、推力、冲击时间和摩擦系数,模拟物体在推力作用下的运动过程,并实时显示物体的位置 - 时间曲线。用户可以通过 “推动” 按钮启动模拟,“归位” 按钮重置模拟。同时,程序考虑了摩擦系数为 0 的特殊情况,提供了相应的提示信息。
运行情况:
源码如下:
import tkinter as tk
from tkinter import ttk, messagebox
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.ticker import MultipleLocator
class MotionSimulator:
def __init__(self, root):
self.root = root
self.root.title("瞬间推力与摩擦力作用下的物体运动")
self.root.geometry("900x780") # 减小窗口宽度
# 设置中文支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 初始化物体和动画参数
self.block_size = 40
self.velocity = 0
self.position = 0
self.animation = None
self.is_moving = False
self.time_data = []
self.position_data = []
self.line = None
self.animation_id = None
self.zero_friction_warning_shown = False
# 配置网格布局
root.grid_columnconfigure(0, weight=1) # 控制面板
root.grid_columnconfigure(1, weight=4) # 图表区域权重增加为4倍
root.grid_rowconfigure(0, weight=3)
root.grid_rowconfigure(1, weight=2)
# 创建控制面板 - 减小宽度
self.control_frame = tk.Frame(root, padx=8, pady=8, width=220, height=400)
self.control_frame.grid(row=0, column=0, sticky="nsew")
self.control_frame.grid_propagate(False)
# 创建图表区域
self.figure_frame = tk.Frame(root, padx=10, pady=10)
self.figure_frame.grid(row=0, column=1, sticky="nsew")
# 创建物体动画区域 - 增加高度
self.bottom_frame = tk.Frame(root, padx=10, pady=10, height=250)
self.bottom_frame.grid(row=1, column=0, columnspan=2, sticky="nsew")
self.bottom_frame.grid_propagate(False)
# 添加质量输入
tk.Label(self.control_frame, text="质量 (kg):").pack(anchor=tk.W)
self.mass_var = tk.StringVar(value="1.0")
mass_entry = ttk.Entry(self.control_frame, textvariable=self.mass_var, width=8)
mass_entry.pack(anchor=tk.W, pady=(0, 5))
# 添加推力输入
tk.Label(self.control_frame, text="推力 (N):").pack(anchor=tk.W)
self.force_var = tk.StringVar(value="50.0")
force_entry = ttk.Entry(self.control_frame, textvariable=self.force_var, width=8)
force_entry.pack(anchor=tk.W, pady=(0, 5))
# 添加冲击时间输入
tk.Label(self.control_frame, text="冲击时间 (s):").pack(anchor=tk.W)
self.impulse_time_var = tk.StringVar(value="0.1")
impulse_time_entry = ttk.Entry(self.control_frame,
textvariable=self.impulse_time_var,
width=8)
impulse_time_entry.pack(anchor=tk.W, pady=(0, 5))
# 添加摩擦系数滑动条
tk.Label(self.control_frame, text="摩擦系数 μ:").pack(anchor=tk.W)
self.mu_frame = tk.Frame(self.control_frame)
self.mu_frame.pack(fill=tk.X, pady=(0, 5))
self.mu_var = tk.DoubleVar(value=0.1)
self.mu_scale = ttk.Scale(self.mu_frame, from_=0.0, to=0.5, orient=tk.HORIZONTAL,
variable=self.mu_var, length=120) # 减小滑块长度
self.mu_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.mu_spinbox = ttk.Spinbox(self.mu_frame, from_=0.0, to=0.5, increment=0.01,
textvariable=self.mu_var, width=5) # 减小宽度
self.mu_spinbox.pack(side=tk.LEFT, padx=(5, 0))
# 摩擦力显示
self.friction_var = tk.StringVar(value="摩擦力: 0 N")
tk.Label(self.control_frame, textvariable=self.friction_var).pack(anchor=tk.W, pady=(0, 5))
# 添加按钮 - 并排显示
button_frame = tk.Frame(self.control_frame)
button_frame.pack(fill=tk.X, pady=(5, 5))
self.push_button = ttk.Button(button_frame, text="推动", command=self.push_object)
self.push_button.pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True)
self.reset_button = ttk.Button(button_frame, text="归位", command=self.reset_simulation)
self.reset_button.pack(side=tk.LEFT, fill=tk.X, expand=True)
# 添加公式说明区域 - 使用Text组件带滚动条
self.formula_frame = tk.Frame(self.control_frame, relief=tk.GROOVE, bg="#f9f9f9")
self.formula_frame.pack(anchor=tk.W, pady=(5, 0), fill=tk.BOTH, expand=True)
# 添加滚动条
scrollbar = tk.Scrollbar(self.formula_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 使用Text组件
self.formula_text = tk.Text(
self.formula_frame,
wrap=tk.WORD,
yscrollcommand=scrollbar.set,
bg="#f9f9f9",
padx=5,
pady=3,
height=6, # 初始显示6行
width=20, # 固定宽度20字符
font=("Courier New", 9), # 等宽字体更紧凑
state="disabled" # 设置为只读
)
self.formula_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.formula_text.yview)
# 创建图表
self.figure = plt.Figure(figsize=(6, 3))
self.ax = self.figure.add_subplot(111)
self.ax.set_xlabel('时间 (s)')
self.ax.set_ylabel('位置 (m)')
self.ax.set_title('物体运动情况')
# 设置网格和刻度
self.ax.grid(True, linestyle='--', alpha=0.7)
self.ax.xaxis.set_major_locator(MultipleLocator(1))
self.ax.yaxis.set_major_locator(MultipleLocator(10))
self.ax.xaxis.set_minor_locator(MultipleLocator(0.5))
self.ax.yaxis.set_minor_locator(MultipleLocator(5))
self.ax.grid(which='minor', linestyle=':', alpha=0.4)
# 调整图表边距
self.figure.subplots_adjust(left=0.10, right=0.95, top=0.95, bottom=0.12) # 减小左边距
self.canvas = FigureCanvasTkAgg(self.figure, master=self.figure_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 创建物体动画区域
self.canvas_width = 800
self.canvas_height = 220
self.simulation_canvas = tk.Canvas(self.bottom_frame,
width=self.canvas_width,
height=self.canvas_height,
bg="white")
self.simulation_canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 初始化物体
self.block_x = 10
self.block = self.simulation_canvas.create_rectangle(
self.block_x, 50,
self.block_x + self.block_size, 50 + self.block_size,
fill="olive", outline="black"
)
# 绘制刻度尺
self.draw_ruler()
# 添加木棒对象
self.stick = self.simulation_canvas.create_rectangle(
0, 0, 0, 0,
fill="brown", outline="black", state="hidden"
)
# 更新摩擦力显示
self.mu_var.trace_add("write", self.update_friction_display)
self.update_friction_display()
# 初始化公式说明
self.reset_simulation()
def draw_ruler(self):
# 获取物体右侧位置
block_right = self.block_x + self.block_size # 应该等于50
# 绘制水平线 - 覆盖整个画布
y_pos = 100
self.simulation_canvas.create_line(10, y_pos + self.block_size/2,
self.canvas_width - 10, y_pos + self.block_size/2,
fill="blue", width=1, tags="ruler")
# 固定长度的刻度尺 - 70米
max_length_meters = 70
max_length_pixels = max_length_meters * 10 # 每米10像素
# 绘制固定长度的刻度尺 - 从物体右侧位置开始
# 每10像素一个小刻度,每50像素一个中等刻度,每100像素一个大刻度
for i in range(0, max_length_pixels, 10):
x_pos = block_right + i
if i % 100 == 0: # 大刻度
tick_height = 8
self.simulation_canvas.create_line(
x_pos, y_pos + self.block_size/2,
x_pos, y_pos + self.block_size/2 + tick_height,
fill="black", width=1.5, tags="ruler")
# 添加数字标签(米)
self.simulation_canvas.create_text(
x_pos, y_pos + self.block_size/2 + 15,
text=str(i // 10), fill="black", font=("Arial", 8), tags="ruler")
elif i % 50 == 0: # 中等刻度
tick_height = 5
self.simulation_canvas.create_line(
x_pos, y_pos + self.block_size/2,
x_pos, y_pos + self.block_size/2 + tick_height,
fill="black", width=1, tags="ruler")
else: # 小刻度
tick_height = 3
self.simulation_canvas.create_line(
x_pos, y_pos + self.block_size/2,
x_pos, y_pos + self.block_size/2 + tick_height,
fill="black", width=0.5, tags="ruler")
# 在刻度尺0位置添加红色标记,明确表示这是0点
self.simulation_canvas.create_line(
block_right, y_pos + self.block_size/2 - 10,
block_right, y_pos + self.block_size/2 + 15,
fill="red", width=1.5, tags="ruler"
)
self.simulation_canvas.create_text(
block_right, y_pos + self.block_size/2 + 25,
text="0", fill="red", font=("Arial", 9, "bold"), tags="ruler"
)
def update_friction_display(self, *args):
try:
mass = float(self.mass_var.get())
mu = self.mu_var.get()
gravity = 9.8
friction = mu * mass * gravity
self.friction_var.set(f"摩擦力: {friction:.2f} N")
except ValueError:
self.friction_var.set("摩擦力: 计算错误")
def push_object(self):
if self.is_moving:
return
try:
mass = float(self.mass_var.get())
initial_force = float(self.force_var.get())
mu = self.mu_var.get()
except ValueError:
return
# 禁用按钮,防止多次点击
self.push_button.configure(state="disabled")
# 重置标志
self.zero_friction_warning_shown = False
# 显示推动木棒
self.show_push_stick()
def show_push_stick(self):
# 设置木棒的初始位置(物体左侧)
stick_width = 40
stick_height = 10
block_x = self.simulation_canvas.coords(self.block)[0] # 获取物体当前X坐标
# 计算木棒位置 - 初始位置在物体左侧一段距离
stick_x = block_x - stick_width - 10
stick_y = 50 + self.block_size/2 - stick_height/2 # 与物体中心对齐
# 更新木棒位置并显示
self.simulation_canvas.coords(
self.stick,
stick_x, stick_y,
stick_x + stick_width, stick_y + stick_height
)
self.simulation_canvas.itemconfigure(self.stick, state="normal")
# 设置木棒推动动画
self.animate_stick(stick_x, stick_y, stick_width, stick_height)
def animate_stick(self, stick_x, stick_y, stick_width, stick_height, step=0):
if step < 10: # 推动动画步骤
# 计算每一步木棒移动的距离
move_distance = 2
# 更新木棒位置
self.simulation_canvas.coords(
self.stick,
stick_x + move_distance * step, stick_y,
stick_x + stick_width + move_distance * step, stick_y + stick_height
)
# 继续下一步动画
self.root.after(30, lambda: self.animate_stick(stick_x, stick_y, stick_width, stick_height, step + 1))
else:
# 推动完成后,隐藏木棒
self.simulation_canvas.itemconfigure(self.stick, state="hidden")
# 然后开始物体的运动
self.start_object_motion()
def start_object_motion(self):
try:
mass = float(self.mass_var.get())
initial_force = float(self.force_var.get())
impulse_time = float(self.impulse_time_var.get())
mu = self.mu_var.get()
except ValueError:
self.push_button.configure(state="normal") # 如果出错,重新启用按钮
return
# 重置数据
# 正确计算初速度:v = F·t/m (冲量/质量)
impulse = initial_force * impulse_time # 冲量 = 力 × 时间
self.velocity = impulse / mass # 速度变化 = 冲量/质量
self.position = 0 # 这里是物理位置,从0开始
self.time_data = [0]
self.position_data = [0]
self.is_moving = True
# 清除之前的线条
if self.line:
self.line.remove()
# 处理摩擦系数为0的特殊情况
if mu <= 0.0001: # 近似为0的情况
estimated_max_distance = 70 # 限制为70米,与刻度尺一致
else:
# 计算理论最大位移:s = v²/(2·μ·g)
estimated_max_distance = (self.velocity ** 2) / (2 * mu * 9.8)
# 限制最大位移不超过70米
estimated_max_distance = min(estimated_max_distance, 70)
# 设置更合理的y轴刻度范围
y_max = max(10, estimated_max_distance * 1.2) # 确保至少显示10米,最大不超过70米的120%
y_max = min(y_max, 70) # 确保不超过70米
# 重置坐标轴 - 更合理的范围
self.ax.set_xlim(0, 10)
self.ax.set_ylim(0, y_max)
# 更新公式说明标签显示当前计算结果
formula_text = "当前计算结果:\n"
formula_text += f"• 初速度 = {self.velocity:.2f} m/s\n"
formula_text += f" (F={initial_force}N, t={impulse_time}s, m={mass}kg)\n\n"
if mu > 0.0001:
formula_text += f"• 预计最大位移 = {estimated_max_distance:.2f} m\n"
formula_text += f" (v0={self.velocity:.2f}m/s, μ={mu})\n\n"
else:
formula_text += "• 无摩擦力,理论上会无限运动\n\n"
formula_text += "公式说明:\n"
formula_text += "• 瞬间推力(冲量): I = F·Δt\n"
formula_text += "• 初速度: v0 = F·t/m\n"
formula_text += "• 最大位移: s = v0^2/(2·μ·g)"
#self.formula_label.config(text=formula_text)
self.formula_text.config(state="normal")
self.formula_text.delete(1.0, tk.END)
self.formula_text.insert(tk.END, formula_text)
self.formula_text.config(state="disabled")
# 绘制新的线条
self.line, = self.ax.plot(self.time_data, self.position_data, 'b-', linewidth=1)
# 启动动画
self.animate()
# 恢复按钮状态
self.push_button.configure(state="normal")
def animate(self):
if not self.is_moving:
return
try:
mass = float(self.mass_var.get())
mu = self.mu_var.get()
except ValueError:
return
# 计算摩擦力
gravity = 9.8
friction_force = mu * mass * gravity
# 计算加速度 (负的,因为摩擦力阻碍运动)
acceleration = -friction_force / mass
# 更新时间、速度和位置 (时间步长0.05秒)
dt = 0.05
new_time = self.time_data[-1] + dt
# 摩擦系数为0或近似为0时特殊处理
if mu <= 0.0001:
new_velocity = self.velocity # 速度不变
# 检查物体是否将要超出画布
new_position = self.position_data[-1] + new_velocity * dt
canvas_limit = 70 # 限制为70米
if new_position >= canvas_limit and not self.zero_friction_warning_shown:
# 停止动画
self.is_moving = False
# 显示永恒运动的警告对话框
self.zero_friction_warning_shown = True
messagebox.showinfo("永恒运动", "由于没有摩擦力,物体将永远运动下去!在现实世界中,物体最终会因为空气阻力等因素停下来。")
return
else:
# 更新速度 (不能低于0)
new_velocity = max(0, self.velocity + acceleration * dt)
# 如果速度变为0或接近0,停止运动
if new_velocity < 0.01: # 使用小阈值判断速度接近0
self.is_moving = False
# 确保最后一个位置是静止位置
new_position = self.position_data[-1]
# 更新数据以显示最终状态
self.time_data.append(new_time)
self.position_data.append(new_position)
self.velocity = 0
# 更新图表最终状态
self.line.set_data(self.time_data, self.position_data)
self.canvas.draw_idle()
return
# 更新位置 - 使用更精确的运动学公式
new_position = self.position_data[-1] + self.velocity * dt + 0.5 * acceleration * dt * dt
# 更新数据
self.time_data.append(new_time)
self.position_data.append(new_position)
self.velocity = new_velocity
# 更新图表
self.line.set_data(self.time_data, self.position_data)
# 更智能地调整x轴范围
if new_time > self.ax.get_xlim()[1]:
self.ax.set_xlim(0, new_time * 1.2)
# 限制y轴最大值为70m或当前位置的120%,确保与刻度尺一致
current_y_max = self.ax.get_ylim()[1]
if new_position > current_y_max * 0.8: # 如果位置超过当前最大值的80%
new_y_max = min(70, new_position * 1.2) # 限制最大值为70米
self.ax.set_ylim(0, new_y_max)
self.canvas.draw_idle()
# 获取物体右侧位置(初始值)
block_right = self.block_x + self.block_size
# 计算画布上的位置 - 物体右侧对应物理位置0点
scaled_position = block_right + new_position * 10 # 缩放以适应画布
# 检查是否超出画布边界
if scaled_position > self.canvas_width - 10:
# 如果物体即将超出画布
self.is_moving = False
# 根据摩擦系数提供不同的提示信息
if mu <= 0.0001:
messagebox.showinfo("永恒运动", "由于没有摩擦力,物体将永远运动下去!在现实世界中,物体最终会因为空气阻力等因素停下来。")
else:
messagebox.showinfo("提示", "物体已到达画布边界")
return
# 更新物体位置 - 确保物体右侧对应目前位置
self.simulation_canvas.coords(
self.block,
scaled_position - self.block_size, 50, # 左侧坐标
scaled_position, 50 + self.block_size # 右侧坐标
)
# 继续动画
if self.is_moving:
self.animation_id = self.root.after(50, self.animate)
def reset_simulation(self):
# 停止动画
if self.animation_id:
self.root.after_cancel(self.animation_id)
self.animation_id = None
self.is_moving = False
self.zero_friction_warning_shown = False # 重置警告标志
# 重置位置数据
self.velocity = 0
self.position = 0
# 确保木棒隐藏
self.simulation_canvas.itemconfigure(self.stick, state="hidden")
# 确保按钮可用
self.push_button.configure(state="normal")
# 重置图表
if self.line:
self.line.remove()
self.line = None
self.time_data = []
self.position_data = []
self.ax.set_xlim(0, 10)
self.ax.set_ylim(0, 10)
self.canvas.draw_idle()
# 重置物体位置
self.simulation_canvas.coords(
self.block,
self.block_x, 50,
self.block_x + self.block_size, 50 + self.block_size
)
# 清除并重绘刻度尺
self.simulation_canvas.delete("ruler") # 删除所有带有"ruler"标签的项目
self.draw_ruler()
# 更新公式说明标签
formula_text = "公式说明:\n"
formula_text += "• 初速度: v0 = F·t/m\n"
formula_text += " (F=推力, t=冲击时间, m=质量)\n\n"
formula_text += "• 最大位移: s = v0^2/(2·μ·g)\n"
formula_text += " (v0=初速度, μ=摩擦系数, g=9.8m/s²)\n\n"
formula_text += "• 无摩擦时(μ=0): 物体保持匀速运动"
#self.formula_label.config(text=formula_text)
self.formula_text.config(state="normal")
self.formula_text.delete(1.0, tk.END)
self.formula_text.insert(tk.END, formula_text)
self.formula_text.config(state="disabled")
if __name__ == "__main__":
root = tk.Tk()
app = MotionSimulator(root)
root.mainloop()