WPF 中的钟摆及其相应的振荡






4.88/5 (7投票s)
一篇说明如何模拟钟摆的文章。
前言
本文的重点是说明如何构建一个模拟物理现象(即钟摆)的 WPF 应用程序。为此,我们将需要构建一个 C# 库,在我们的项目中将其引用为 DLL。所提供的代码旨在说明一种数学技术,以实现此钟摆模拟。因此,如果您能容忍对物理学进行简短的解释,我们将继续构建应用程序。然而,在这里,最重要的是要明确,本文中的代码示例很大程度上基于 Jack Xu 所著的 WPF 图形学书籍中的教学内容。这位 Windows 技术作者写了大量的书籍,Jack Xu 展示了如何使用一个类库,该库成功地使用 C# 代码基于非常复杂的积分——Runge-Kutta 方法——创建了一个四阶微积分库。松散地说,微积分的主题,无论多么广泛,都分为两部分:积分和微分。将其与正弦和余弦函数结合起来,当它们与绘图函数链接时会循环,这使得这些示例(除其他外)成为可能。对这些主题感兴趣的学生应该努力理解微积分中的积分和微分是逆运算。但是,例如,.NET Framework BCL 中包含的正弦和余弦函数可以与几种图形函数结合使用,并通过 for 循环控制结构绘制线条。
物理学专业的学生不可避免地会接触到微分方程,并求解偏微分方程和常微分方程。许多物理现象可以用常微分方程 (ODE) 来描述。例如,当一个弹丸在空中飞行时,它会受到空气动力学的阻力,而空气动力学的阻力是物体速度的函数。某些乐器的声音是弦张力的函数。再考虑一个弹簧-质量系统。在这个系统中,有两个力作用在质量上:弹性恢复力,它与质量的位移成正比,以及阻尼力,它与速度成正比。描述该系统的运动方程也是一组常微分方程,无法直接求解。然而,当无法得到解析闭式解时,有许多技术可以用来求解 ODE。一种技术称为 Runge-Kutta 方法。现在,在不详细解释该技术工作原理的情况下,我们可以看一些 C# 代码,这些代码首先定义了一个委托函数,该函数接受一个 double
数组 x
和一个 double
时间变量 t
作为输入参数。
using System;
using System.Windows;
namespace Swing
{
public class ODESolver
{
public delegate double Function(
double[] x, double t);
public static double[] RungeKutta4(
Function[] f, double[] x0, double t0, double dt)
{
int n = x0.Length;
double[] k1 = new double[n];
double[] k2 = new double[n];
double[] k3 = new double[n];
double[] k4 = new double[n];
double t = t0;
double[] x1 = new double[n];
double[] x = x0;
for (int i = 0; i < n; i++)
k1[i] = dt * f[i](x, t);
for (int i = 0; i < n; i++)
x1[i] = x[i] + k1[i] / 2;
for (int i = 0; i < n; i++)
k2[i] = dt * f[i](x1, t + dt / 2);
for (int i = 0; i < n; i++)
x1[i] = x[i] + k2[i] / 2;
for (int i = 0; i < n; i++)
k3[i] = dt * f[i](x1, t + dt / 2);
for (int i = 0; i < n; i++)
x1[i] = x[i] + k3[i];
for (int i = 0; i < n; i++)
k4[i] = dt * f[i](x1, t + dt);
for (int i = 0; i < n; i++)
x[i] +=
(k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) / 6;
return x;
}
}
}
可以使用 /t:library 开关在命令行上编译此文件,或者在 Visual Studio 中使用 C# 类库项目将其作为类文件进行编译。在构建 WPF 应用程序时,我们将引用此 DLL。我们想模拟钟摆的运动。当钟摆从其静止平衡位置移开时,它会受到重力引起的恢复力,该力会将其加速回平衡位置。释放后,恢复力与钟摆的质量相结合,使其在平衡位置附近振荡,来回摆动。一个完整周期(一次左摆和一次右摆)的时间称为周期。钟摆以特定的周期摆动,该周期(主要)取决于其长度。这意味着我们将使用它来模拟这个模型。
请查看下图。底部左侧的窗格显示了一个末端悬挂着重物的字符串。底部右侧的窗格显示了摆角随时间的变化。此外,还有几个 TextBox 字段,允许您输入质量、字符串长度、阻尼系数、初始角度和初始角度速度。Start 按钮启动钟摆模拟器,Stop 按钮停止模拟,Reset 按钮停止模拟并将钟摆恢复到初始位置。因此,让我们启动 Expression Blend 或 Visual Studio,创建一个名为 Swing 的新项目,添加对 ODESolver.dll 的引用,添加一个新的 WPF 窗口,或者将 MainWindow
重命名为 Pendulum
,然后查看 XAML。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Swing.Pendulum"
x:Name="Window"
Title="Swing
Out"
Width="640"
Height="480" Background="MediumPurple">
<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="20"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="Margin" Value="2"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,2,2,5"/>
<Setter Property="Width" Value="70"/>
<Setter Property="TextAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="2"/>
<Setter Property="Width" Value="75"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Window.Foreground>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="Black" Offset="1"/>
</LinearGradientBrush>
</Window.Foreground>
<StackPanel Margin="10">
<StackPanel Orientation="Horizontal">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="14.667" FontFamily="Times New Roman"
FontWeight="Bold">Mass:</TextBlock>
<TextBox Name="tbMass" Text="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="Times New Roman"
FontWeight="Bold" FontSize="14.667">Length:</TextBlock>
<TextBox Name="tbLength" Text="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="Times New Roman" FontWeight="Bold"
FontSize="14.667">Damping:</TextBlock>
<TextBox Name="tbDamping" Text="0.1"/>
</StackPanel>
</StackPanel>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="Times New Roman" FontWeight="Bold"
FontSize="14.667">Theta0:</TextBlock>
<TextBox Name="tbTheta0" Text="45"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="Times New Roman" FontWeight="Bold"
FontSize="14.667">Alpha0:</TextBlock>
<TextBox Name="tbAlpha0" Text="0"/>
</StackPanel>
</StackPanel>
<StackPanel Margin="70,0,0,10">
<Button Click="btnStart_Click" Content="Start"/>
<Button Click="btnStop_Click" Content="Stop"/>
<Button Click="btnReset_Click" Content="Reset"/>
</StackPanel>
<StackPanel Margin="70,40,0,0">
<TextBlock Name="tbDisplay" FontSize="16"
Foreground="Black" FontFamily="Tahoma"
FontWeight="Bold">Stopped
</TextBlock>
</StackPanel>
</StackPanel>
<Separator Margin="0,10,0,10"></Separator>
<Viewbox Stretch="Fill">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Canvas Name="canvasLeft" Grid.Column="0"
Width="280" Height="170">
<Rectangle Fill="DarkGoldenrod" Width="50"
Height="10" Canvas.Left="115"
Canvas.Top="10"/>
<Line Name="line1" X1 ="140" Y1="20"
X2="140" Y2="150" Stroke="Red"/>
<Path Fill="Blue">
<Path.Data>
<EllipseGeometry x:Name="ball" RadiusX="10"
RadiusY="10" Center="140,150"/>
</Path.Data>
</Path>
</Canvas>
<Canvas Name="canvasRight" Grid.Column="1"
ClipToBounds="True" Width="280"
Height="170">
<Line X1="10" Y1="0" X2="10" Y2="170"
Stroke="Gray" StrokeThickness="1"/>
<Line X1="10" Y1="85"
X2="280" Y2="85"
Stroke="Gray" StrokeThickness="1"/>
<TextBlock TextAlignment="Left"
Canvas.Left="10" FontFamily="Times New Roman"
FontWeight="Bold" FontSize="14.667">theta
</TextBlock>
<TextBlock TextAlignment="Left" Canvas.Left="248.51"
Canvas.Top="89.5" FontFamily="Times New Roman"
FontWeight="Bold" FontSize="14.667"
Margin="0">time
</TextBlock>
</Canvas>
</Grid>
</Viewbox>
</StackPanel>
</Window>
按下 Start 按钮时,将从相应 TextBox 字段中的值获取质量、字符串长度、阻尼系数以及初始位置和速度的输入值。同时,将事件处理程序 StartAnimation
附加到静态 CompositionTarget.Rendering
事件。这是后台代码文件。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Swing
{
public partial class Pendulum : Window
{
private double PendulumMass = 1;
private double PendulumLength = 1;
private double DampingCoefficient = 0.5;
private double Theta0 = 45;
private double Alpha0 = 0;
double[] xx = new double[2];
double time = 0;
double dt = 0.03;
Polyline pl = new Polyline();
double xMin = 0;
Double yMin = -100;
double xMax = 50;
double yMax = 100;
public Pendulum()
{
InitializeComponent();
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
PendulumMass = Double.Parse(tbMass.Text);
PendulumLength = Double.Parse(tbLength.Text);
DampingCoefficient = Double.Parse(tbDamping.Text);
Theta0 = Double.Parse(tbTheta0.Text);
Theta0 = Math.PI * Theta0 / 180;
Alpha0 = Double.Parse(tbAlpha0.Text);
Alpha0 = Math.PI * Alpha0 / 180;
tbDisplay.Text = "Starting...";
if (canvasRight.Children.Count > 4)
canvasRight.Children.Remove(pl);
pl = new Polyline();
pl.Stroke = Brushes.Red;
canvasRight.Children.Add(pl);
time = 0;
xx = new double[2] { Theta0, Alpha0 };
CompositionTarget.Rendering += StartAnimation;
}
private void StartAnimation(object sender, EventArgs e)
{
// Invoke ODE solver:
ODESolver.Function[] f =
new ODESolver.Function[2] { f1, f2 };
double[] result = ODESolver.RungeKutta4(
f, xx, time, dt);
// Display moving pendulum on screen:
Point pt = new Point(
140 + 130 * Math.Sin(result[0]),
20 + 130 * Math.Cos(result[0]));
ball.Center = pt;
line1.X2 = pt.X;
line1.Y2 = pt.Y;
// Display theta - time curve on canvasRight:
if (time < xMax)
pl.Points.Add(new Point(XNormalize(time) + 10,
YNormalize(180 * result[0] / Math.PI)));
// Reset the initial values for next calculation:
xx = result;
time += dt;
if (time > 0 && Math.Abs(result[0]) < 0.01 &&
Math.Abs(result[1]) < 0.001)
{
tbDisplay.Text = "Stopped";
CompositionTarget.Rendering -= StartAnimation;
}
}
private void btnReset_Click( object sender, RoutedEventArgs e)
{
PendulumInitialize();
tbDisplay.Text = "Stopped";
if (canvasRight.Children.Count > 4)
canvasRight.Children.Remove(pl);
CompositionTarget.Rendering -= StartAnimation;
}
private void PendulumInitialize()
{
tbMass.Text = "1";
tbLength.Text = "1";
tbDamping.Text = "0.1";
tbTheta0.Text = "45";
tbAlpha0.Text = "0";
line1.X2 = 140;
line1.Y2 = 150;
ball.Center = new Point(140, 150);
}
private void btnStop_Click( object sender, RoutedEventArgs e)
{
line1.X2 = 140;
line1.Y2 = 150;
ball.Center = new Point(140, 150);
tbDisplay.Text = "Stopped";
CompositionTarget.Rendering -= StartAnimation;
}
private double f1(double[]xx, double t)
{
return xx[1];
}
private double f2(double[] xx, double t)
{
double m = PendulumMass;
double L = PendulumLength;
double g = 9.81;
double b = DampingCoefficient;
return -g * Math.Sin(xx[0]) / L - b * xx[1] / m;
}
private double XNormalize(double x)
{
double result = (x - xMin) *
canvasRight.Width / (xMax - xMin);
return result;
}
private double YNormalize(double y)
{
double result = canvasRight.Height - (y - yMin) *
canvasRight.Height / (yMax - yMin);
return result;
}
}
}
获得新的角度和速度值后,您将更新显示移动的钟摆以及屏幕上显示的角度随时间变化的屏幕。接下来,您将当前解设置为下一轮模拟的初始值。当摆角和角速度非常小,以至于钟摆几乎不再摆动时,您可以使用以下语句通过分离 StartAnimation
事件处理程序来停止动画:
CompositionTarget.Rendering -= StartAnimation;
您可以通过更改质量、阻尼系数、初始字符串角度和初始角度速度的值来玩 Pendulum Simulator,并观察它们对钟摆运动的影响。
参考文献
Jack Xu 实用 WPF 图形编程