Preface
This focus of this article is to illustrate how to build a WPF application that simulates a Physics phenomena, namely a pendulum. To do this, we will have to build a C# library to reference as a DLL in our project. The code presented is meant to illustrate a mathematical technique to enable this pendulum simulation. So, if you can bear with a brief explanation about Physics, we will proceed and build the application. It is, howver, of paramount importance here to make it clear that the code examples of this article are largely based on on the teachings of the WPF Graphics book, as written by Jack Xu. This particular Windows technology author has a plethora of books Jack Xu demonstrated the use of a class library that sucessfully uses C# code to create a fourth-order Calculus library based on the very complicated integral called the Runge Khutta. Stated loosely, the subject of calculus, however extensive, is divided into two parts: the integral and the derivative differential. Combining that with the use of sin and cosine functions that loop when linked to the draw functions enable these examples (amongst other things. The interested student of these topics should try to grasp that integrals and derivatives in Calculus are inverse operations. But for instance, the sin and cosine functions contained the .NET Framework BCL can be used with several of the graphics functions to draw lines using the for loop control structure.
The student of Physics will have inevitably tackled differential equations, solving both partial and ordinary equations. Many Physics phenomena can be described in terms of ordinary differential equations (ODEs). For instance, if a projectile is flying through the air, it will be subject to the force of aerodynamic drag, which is a function of the object's velocity. The sounds of certain musical instruments are a function of the tension of the string. Also consider a spring-mass system. In this system, there are two forces acting on the mass: elastic recovery force, which is proportional to the displacement of the mass, and the damping force, which is proportional to its velocity. The equations of motion describing this system are also a set of ordinary differential equations, which can't be directly solved either. There are, however, a number of techniques that can be used to solve ODEs when an analytically closed form of solution is impossible. One technique is called the Runge-Kutta method. Now, without going into a long, drawn-out explanation of how this technique works, we can look at some C# code that first defines a delegate function that takes a double
array x
and a double
time variable t
as its input parameters.
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;
}
}
}
This file can be compiled on the command line using the /t:library switch or as a class file in a C# Class Library project using Visual Studio. We are going to reference this DLL when we build our WPF app. We want to simulate the motion of a pendulum. When a pendulum is displaced from its resting equilibrium position, it is subject to a restoring force due to gravity that will accelerate it back toward the equilibrium position. When released, the restoring force combined with the pendulum's mass causes it to oscillate about the equilibrium position, swinging back and forth. The time for one complete cycle, a left swing and a right swing, is called the period. A pendulum swings with a specific period which depends (mainly) on its length. This means that we are going to use it to simulate this model.
Examine the image below. A string with a mass hanging on one end is displayed in the bottom-left pane. The bottom-right pane shows how the swing angle changes with time. In addition, there are several TextBox fields that allow you to input the mass, string length, damping coefficient, initial angle, and initial angle velocity. A Start button begins the pendulum simulator, a Stop button stops the simulation, and a Reset button stops the simulation and returns the pendulum to its initial position. So let's fire up either Expression Blend or Visual Studio, start a new project called Swing, add a reference to ODESolver.dll, add a new WPF window, or rename the MainWindow
to Pendulum
, and take a look at the 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>
When the Start button is pressed, the input values for the mass, string length, damping coefficient, and initial position and velocity are obtained from the values inside their corresponding TextBox fields. At the same time, the event handler StartAnimation
is attached to the static CompositionTarget.Rendering
event. Here is the code-behind file:
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)
{
ODESolver.Function[] f =
new ODESolver.Function[2] { f1, f2 };
double[] result = ODESolver.RungeKutta4(
f, xx, time, dt);
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;
if (time < xMax)
pl.Points.Add(new Point(XNormalize(time) + 10,
YNormalize(180 * result[0] / Math.PI)));
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;
}
}
}
Once the new values of angle and velocity are obtained, you update the screen that shows the moving pendulum and the swing angle as a function of time on the screen. Next, you set the current solution as the initial values for the next round simulation. When the swing angle and angle velocity are so small that the pendulum almost doesn't swing, you can stop the animation by detaching the StartAnimation
event handler using the statement:
CompositionTarget.Rendering -= StartAnimation;
You can play around with the Pendulum Simulator by changing the values of the mass, damping coefficient, initial string angle, and initial angle velocity, and watch their effects on the motion of the pendulum.
References
Jack Xu Practical WPF Graphics Programming