Simulation des schiefen Wurfs

1. Aufgabe

Viele interessante Bewegungen (Kugelstoß, Speerwurf, Kanonenkugel usw.) werden physikalisch betrachtet durch eine Wurfparabel beschrieben. Im Prinzip beobachten wir dabei den Abwurf eines Körpers (Ball, Kugel, etc.) mit einer bestimmten Abwurfgeschwindigkeit v0 in eine bestimmte Richtung, die durch einen Winkel α mit der Horizontalen beschrieben wird. Der Einfachheit halber vernachlässigen wir den Luftwiderstand. In dieser Aufgabe betrachten wir eine C#-Desktop-Anwendung zur Simulation des schiefen Wurfs, deren Oberflächenanteile in XAML samt zugehörigen Logik-Anweisungen in C# zu erstellen sind. Für die Strukturierung der Anwendung legen wir das Model-View-Controller (MVC)-Entwurfsmuster zu Grunde.

Ihre physikalischen Grundkenntnisse für diese Aufgabe dürfen Sie getrost auf zwei Formeln reduzieren, für deren Verständnis Sie zumindest einen Blick auf Abbildung 1 werfen sollten:

Grundlagen zur Wurfparabel des schiefen Wurfs.

Abbildung 1. Grundlagen zur Wurfparabel des schiefen Wurfs.


Um eine Wurfparabel wie in Abbildung 1 gezeigt darstellen zu können, gelten in Abhängigkeit von der Abwurfhöhe y0, der Abwurfgeschwindigkeit v0 und dem Abwurfwinkel α die folgenden beiden Gleichungen x(t) und y(t) für den horizontalen und vertikalen Bewegungsverlauf:

x(t) = v0 * cos(α) * t

und

y(t) = y0 + v0 * sin(α) * t - 1/2 * g * t2

In der zweiten Bewegungsgleichung dürfen Sie die Erdbeschleunigung (Gravitationskonstante) g mit dem Wert 9,81 gleichsetzen.

Als Basis der Simulation konzipieren wir nun eine Klasse TrajectoryModel (engl. trajectory: Wurfparabel), die die physikalischen Grundlagen des schiefen Wurfs kapselt und zum Zwecke einer grafischen Simulation auch ein geeignetes Ereignis bereitstellt. Alles Weitere entnehmen Sie bitte Tabelle 1:

Element

Schnittstelle und Beschreibung

Eigenschaft StartHeight

public double StartHeight { get; set; }

Die Eigenschaft StartHeight dient zum Setzen der Abwurfhöhe eines Wurfs.

Eigenschaft StartVelocity

public double StartVelocity { get; set; }

Die Eigenschaft StartVelocity dient zum Setzen der Abwurfgeschwindigkeit eines Wurfs.

Eigenschaft StartAngle

public double StartAngle { get; set; }

Die Eigenschaft StartAngle dient zum Setzen des Abwurfwinkels eines Wurfs.

Methode Start

public void Start();

Startet die Simulation eines schiefen Wurfs. Es wird in entsprechenden zeitlichen Abständen das Ereignis PositionChanged ausgelöst (siehe weiter unten). Auf diese Weise lässt sich der räumliche und zeitliche Verlauf einer Wurfparabel verfolgen.

Ereignis PositionChanged

public event PositionChangedHandler PositionChanged;

Das Ereignis PositionChanged wird durch den delegate-Typ PositionChangedHandler definiert:

public delegate void PositionChangedHandler (Point p, double elapsed);

Pro Eintreffen eines PositionChanged-Ereignisses erhält man eine Ortsangabe (Position des Flugkörpers, Variable vom Typ Point) und eine Zeitangabe (Variable elapsed vom Typ double) übermittelt. Der Wert der elapsed-Variablen ist in der Zeiteinheit Sekunden zu sehen.

Tabelle 1. Öffentliche Elemente der Klasse TrajectoryModel.


Die Beschreibung der Start-Methode in Tabelle 1 bedarf noch einer Ergänzung. Da in gewissen zeitlichen Abständen das PositionChanged-Ereignis auszulösen ist, bedient sich das Modell den Hilfestellungen eines DispatcherTimer-Objekts. Hierbei stellt sich die Frage, wann das DispatcherTimer-Objekt, das die einzelnen PositionChanged-Ereignisse auslöst, zu stoppen ist? Da ein Betrachten von Simulationswerten mit negativen Höhenwerten sinnlos ist, legen wir fest, dass wir das DispatcherTimer-Objekt anhalten, wenn die Bewegungsgleichung y(t) negative Werte annimmt!

Hinweis:

Für die Eigenschaft StartAngle in Tabelle 1 sind Werte im Bereich von 0° bis 90° zulässig. Anders formuliert: Wir haben es mit Winkelangaben in der Darstellungsart „Gradmaß“ zu tun. Natürlich ist das Gradmaß für Werte im Bereich von 0° bis 360° definiert. Größere Winkel als 90° ergeben für einen Abwurfwinkel aber keinen Sinn.

Die mathematischen Funktionen Math.Sin und Math.Cos aus der .NET-Klassenbibliothek sind für das „Bogenmaß“ ausgelegt. Zum Abbilden der Bewegungsgleichungen x(t) und y(t) in C# Anweisungen müssen Sie folglich Abwurfwinkel, die im Gradmaß vorliegen, in das Bogenmaß umwandeln. Die Umrechnungsformel dafür ist sehr einfach:

αBogenmaß = αGradmaß * π / 180

bzw.

αBogenmaß = αGradmaß * 3,1415926 / 180,

wenn wir die Zahl π durch den Näherungswert 3,1415926 ersetzen.

Beispiel: Für einen Startwinkel von 30° berechnet sich das Bogenmaß zu

30 * 3,1415926 / 180 = 0.5235988

Für die Realisierung einer Ansicht des schiefen Wurfs entwerfen Sie ein WPF-Custom-Control-Steuerelement und leiten dieses von der Basisklasse Canvas ab. Geben Sie der Klasse den Namen TrajectoryViewControl. Ein Beispiel für die Oberfläche eines TrajectoryViewControl-Objekts finden Sie in Abbildung 2 vor:

Eine Ansicht des schiefen Wurfs.

Abbildung 2. Eine Ansicht des schiefen Wurfs.


Im Prinzip besitzt die Ansicht als einzige Aufgabe nur das Zeichnen der Flugbahn des Flugkörpers. Zu diesem Zweck besitzt sie die Eigenschaft BallPosition, die kontinuierlich dann aufzurufen ist, wenn sich die Position des Flugkörpers verändert hat. Ebenfalls wollen wir beim Auswählen der Simulationsparameter die Höhe des Flugkörpers in der Ansicht simultan verstellen können. Dazu gibt es die Eigenschaft BallHeight (Tabelle 2):

Element

Schnittstelle und Beschreibung

Eigenschaft BallPosition

public Point BallPosition { get; set; }

Die Eigenschaft BallPosition dient zum Setzen der aktuellen Position des Flugkörpers während eines schiefen Wurfs.

Eigenschaft BallHeight

public double BallHeight { get; set; }

Die Eigenschaft BallHeight dient zum Setzen der Höhe des Flugkörpers. Während eines Wurfs sollte diese Eigenschaft nicht verändert werden.

Tabelle 2. Öffentliche Elemente der Klasse TrajectoryViewControl.


Die Implementierung des Zeichnens der Flugbahn in der TrajectoryViewControl-Klasse ist nicht ganz einfach. Die Koordinaten eines Punktes (Klasse Point) beziehen sich auf ein mathematisches Koordinatensystem mit dem Ursprung links unten. Canvas-Steuerelemente (wie die meisten Steuerelemente gängiger grafischer Klassenbibliotheken) haben ihren Ursprung in der linken oberen Ecke. Bzgl. der vertikalen Ausrichtung eines Ellipse-Objekts auf dem Canvas-Steuerelement zur Visualisierung eines Flugkörpers müssen Sie also eine geeignete Umrechung der mathematischen in grafische Koordinaten vornehmen. Die Auswirkungen des Model-View-Controller-Entwurfsmusters werden an dieser Stelle deutlich erkennbar: Mathematische Grundlagen sind Sache des Modells, grafische Visualisierungen gehören zur Ansicht. Da ein Modell unterschiedliche Ansichten besitzen kann, obliegt die Umrechnung mathematischer Koordinaten der jeweiligen Ansicht.

Um den Aufwand in der Realisierung der Ansicht in Grenzen zu halten, dürfen Sie bzgl. des schiefen Wurfs maximale Werte in der waagrechten und senkrechten Ausrichtung zu Grunde legen. In meiner Musterrealisierung habe ich horizontal 25m und vertikal 15m festgelegt.

Für die Realisierung des Controllers entwerfen Sie ein WPF-User-Control. Geben Sie der realisierenden Klasse den Namen TrajectorySettingsControl. Im Prinzip setzt sich das Steuerelement aus einer Reihe von WPF-Standard-Steuerelementen wie Slider-, TextBox- und Label-Objekten zusammen, um die drei Simulationsparameter Abwurfhöhe, Abwurfgeschwindigkeit und Abwurfwinkel komfortabel einstellen zu können. Eine mögliche Gestaltung der Oberfläche des Simulationscontrollers zeigt Abbildung 3:

Oberfläche eines Simulation-Controllers.

Abbildung 3. Oberfläche eines Simulation-Controllers.


Beachte: Den Simulationsparametern liegen gewisse Wertebereiche zu Grunde (Abwurfhöhe 0 bis maximal 15m, Abwurfgeschwindigkeit 0 bis 100 m/s2, Abwurfwinkel 0° bis 90°). Achten Sie bei der Umsetzung des Controllers darauf, dass die jeweiligen Slider-Objekte den entsprechenden Wertebereich des Simulationsparameters einhalten.

Damit das TrajectorySettingsControl-Steuerelement die eingestellten Simulationsparameter publizieren kann, genügt es, die TrajectorySettingsControl-Klasse mit entsprechenden Eigenschaften auszustatten. Für das Konzept dieser Aufgabe sind Ereignisse im Simulations-Controller nicht erforderlich. Die naheliegende Definition dreier Eigenschaften StartHeight, StartVelocity und StartAngle im TrajectorySettingsControl-Steuerelement finden Sie in Tabelle 3 vor:

Eigenschaft

Schnittstelle und Beschreibung

Eigenschaft StartHeight

public double StartHeight { get; set; }

Die Eigenschaft StartHeight dient zum Setzen der Abwurfhöhe eines Wurfs.

Eigenschaft StartVelocity

public double StartVelocity { get; set; }

Die Eigenschaft StartVelocity dient zum Setzen der Abwurfgeschwindigkeit eines Wurfs.

Eigenschaft StartAngle

public double StartAngle { get; set; }

Die Eigenschaft StartAngle dient zum Setzen des Abwurfwinkels eines Wurfs.

Tabelle 3. Öffentliche Elemente der Klasse TrajectorySettingsControl.


Hinweis:

Wenn Sie die Eigenschaft StartHeight aus Tabelle 3 als .NET-DependencyProperty realisieren, können sich Ansichten mittels WPF-Databinding mit dieser Eigenschaft verschalten.

Integrieren Sie die drei Komponenten TrajectoryModel, TrajectoryViewControl und TrajectorySettingsControl geeignet in einer WPF-Desktopanwendung. Die Verschaltung des PositionChanged-Ereignisses vom TrajectoryModel-Objekt mit der TrajectoryViewControl-Ansicht nehmen Sie im Codebehind-Anteil des Hauptfensters vor. Das Ergebnis der Integration sollte in etwa wie in Abbildung 4 aussehen:

Integration der Simulation in einer WPF-Desktopanwendung.

Abbildung 4. Integration der Simulation in einer WPF-Desktopanwendung.


Im Anschluss an die Integration der Gesamtanwendung ergänzen Sie die Darstellung der Wurfparabel des schiefen Wurfs. Alle Positionsangaben, die während des schiefen Wurfs durch PositionChanged-Ereignisse übermittelt werden, sind mit Line-Objekten miteinander zu verbinden. Bei hinreichend vielen PositionChanged-Ereignissen pro Sekunde können wir auf diese Weise mit dem bloßen Auge nicht mehr wahrnehmen, dass eine Wurfparabel, wie wir sie beispielsweise in Abbildung 5 vorfinden, ausschließlich aus Linien besteht!

Darstellung der Wurfparabel.

Abbildung 5. Darstellung der Wurfparabel.

2. Lösung

Quellcode: Siehe auch https://github.com/peterloos/Wpf_SchieferWurf.git.

Wir starten in Listing 1 mit der Implementierung der Modelklasse TrajectoryModel. Für die Berechnung der einzelnen Punkte des schiefen Wurfs in Abhängigkeit vom zeitlichen Verlauf kommt eine Instanz der Klasse DispatcherTimer zum Zuge. Der Vorteil dieser Klasse liegt darin, dass alle Methoden, die in Folge eines Eintretens des PositionChanged-Ereignisses aufgerufen werden, die Hilfestellungen der Dispatcher-Klasse nicht in Anspruch nehmen müssen. Sie werden automatisch im Kontext des UI-Threads ausgeführt.

001: namespace Wpf_TrajectorySimulation
002: {
003:     using System;
004:     using System.Windows;
005:     using System.Windows.Threading;
006: 
007:     public delegate void PositionChangedHandler(Point p, double elapsed);
008: 
009:     public class TrajectoryModel
010:     {
011:         private const double Gravity = 9.81;  // m/s2
012: 
013:         private double startHeight;
014:         private double startVelocity;
015:         private double startAngleDegree;      // Startwinkel im Gradmaß
016:         private double startAngleRadian;      // Startwinkel im Bogenmaß
017: 
018:         private const int TimerInterval = 10; 
019:         private double TimeDelta = 1.0 / 100.0; 
020: 
021:         private DispatcherTimer timer;
022:         private double t;   // elapsed time
023: 
024:         public event PositionChangedHandler PositionChanged;
025: 
026:         // c'tor
027:         public TrajectoryModel ()
028:         {
029:             this.startHeight = 0.0;
030:             this.startVelocity = 0.0;
031:             this.startAngleDegree = 0.0;
032: 
033:             // timer setup
034:             this.timer = new DispatcherTimer();
035:             this.timer.Tick += this.TrajectoryTimerTick;
036:             this.timer.Interval = TimeSpan.FromMilliseconds(TimerInterval);
037:         }
038: 
039:         // properties
040:         public double StartHeight
041:         {
042:             set
043:             {
044:                 this.startHeight = 0.0;
045:                 if (value >= 0.0 && value <= 15.0)
046:                 {
047:                     this.startHeight = value;
048:                 }
049:             }
050: 
051:             get
052:             {
053:                 return this.startHeight; 
054:             }
055:         }
056: 
057:         public double StartVelocity
058:         {
059:             set
060:             {
061:                 this.startVelocity = 0.0;
062:                 if (value >= 0.0 && value <= 100.0)
063:                 {
064:                     this.startVelocity = value;
065:                 }
066:             }
067: 
068:             get
069:             {
070:                 return this.startVelocity;
071:             }
072:         }
073: 
074:         public double StartAngle
075:         {
076:             set
077:             {
078:                 this.startAngleDegree = 0.0;
079:                 this.startAngleRadian = 0.0;
080: 
081:                 if (value >= 0.0 && value <= 90.0)
082:                 {
083:                     this.startAngleDegree = value;
084:                     this.startAngleRadian =
085:                         this.ConvertDegreesToRadians(this.startAngleDegree);
086:                 }
087:             }
088: 
089:             get
090:             {
091:                 return this.startAngleDegree;
092:             }
093:         }
094: 
095:         // public methods
096:         public void Start()
097:         {
098:             this.t = 0.0;
099:             this.timer.Start();
100:         }
101: 
102:         // private helper methods
103:         private void TrajectoryTimerTick(Object sender, EventArgs e)
104:         {
105:             Point p = this.CalcPosition(t);
106: 
107:             if (p.Y < 0)
108:             {
109:                 this.timer.Stop();
110:             }
111: 
112:             this.t += TimeDelta;
113: 
114:             if (this.PositionChanged != null)
115:                 this.PositionChanged.Invoke(p, this.t);
116:         }
117: 
118:         private double CalcX(double t)
119:         {
120:             return this.startVelocity * Math.Cos(this.startAngleRadian) * t;
121:         }
122: 
123:         private double CalcY(double t)
124:         {
125:             return
126:                 this.startHeight
127:                 + (this.startVelocity * Math.Sin(this.startAngleRadian) * t)
128:                 - 0.5 * Gravity * t * t;
129:         }
130: 
131:         private Point CalcPosition(double t)
132:         {
133:             return new Point(this.CalcX(t), this.CalcY(t));
134:         }
135: 
136:         private double ConvertDegreesToRadians(double degrees)
137:         {
138:             return (Math.PI / 180) * degrees;
139:         }
140:     }
141: }

Beispiel 1. Modellklasse TrajectoryModel.


Das TrajectorySettingsControl-Steuerelement können wir ebenfalls schnell abhandeln. Die StartHeight-Eigenschaft wurde als DependencyProperty-Objekt ausgelegt. Darauf gehen wir bei der Betrachtung des Hauptfensters näher ein. Da das TrajectorySettingsControl-Steuerelement als WPF-User Control realisiert wurde, ist die Implementierung auf eine XAML- und eine Codebehind-Datei in Listing 2 und Listing 3 aufgeteilt:

01: <UserControl
02:     x:Class="Wpf_TrajectorySimulation.TrajectorySettingsControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     d:DesignHeight="100" d:DesignWidth="400">
09:     
10:     <UniformGrid Rows="2" Columns="3">
11:         <DockPanel LastChildFill="True" Margin="5">
12:             <Label DockPanel.Dock="Left"
13:                 VerticalContentAlignment="Center">Abwurfwinkel:</Label>
14:             <TextBox Name="TextBoxLaunchAngle" IsEnabled="False"></TextBox>
15:         </DockPanel>
16:         
17:         <DockPanel LastChildFill="True" Margin="5">
18:             <Label DockPanel.Dock="Left"
19:                 VerticalContentAlignment="Center">Abwurfhöhe:</Label>
20:             <TextBox Name="TextBoxLaunchHeight" IsEnabled="False"></TextBox>
21:         </DockPanel>
22:         
23:         <DockPanel LastChildFill="True" Margin="5">
24:             <Label DockPanel.Dock="Left"
25:                 VerticalContentAlignment="Center">Abwurfgeschwindigkeit:</Label>
26:             <TextBox Name="TextBoxLaunchVelocity" IsEnabled="False"></TextBox>
27:         </DockPanel>
28:         
29:         <Slider Name="SliderStartAngle"
30:                 Foreground="Blue" LargeChange="1" SmallChange="1"
31:                 Minimum="0" Maximum="90" TickPlacement="BottomRight"
32:                 TickFrequency="5" ValueChanged="Slider_ValueChanged"
33:                 Margin="5"/>
34:         
35:         <Slider Name="SliderStartHeight" Foreground="Blue"
36:                 IsSnapToTickEnabled="True"
37:                 Minimum="0" Maximum="10"
38:                 TickPlacement="BottomRight"
39:                 TickFrequency="0.5" ValueChanged="Slider_ValueChanged"
40:                 Margin="5"/>
41:         
42:         <Slider Name="SliderStartVelocity" Foreground="Blue"
43:                 Minimum="0" Maximum="100"
44:                 TickPlacement="BottomRight"
45:                 TickFrequency="5" ValueChanged="Slider_ValueChanged"
46:                 Margin="5" />
47:     </UniformGrid>
48: </UserControl>

Beispiel 2. Steuerelement TrajectorySettingsControl: XAML.


01: namespace Wpf_TrajectorySimulation
02: {
03:     using System;
04:     using System.Windows;
05:     using System.Windows.Controls;
06: 
07:     public partial class TrajectorySettingsControl : UserControl
08:     {
09:         private double startVelocity;
10:         private double startAngle;
11: 
12:         public static readonly DependencyProperty StartHeightProperty;
13: 
14:         // static c'tor
15:         static TrajectorySettingsControl()
16:         {
17:             StartHeightProperty = DependencyProperty.Register(
18:                 "StartHeight",   // name of property
19:                 typeof(double),  // type of property
20:                 typeof(TrajectorySettingsControl),   // type of property owner
21:                 new PropertyMetadata(0.0)            // metadata
22:             );
23:         }
24: 
25:         // c'tor
26:         public TrajectorySettingsControl()
27:         {
28:             this.InitializeComponent();
29: 
30:             this.SliderStartAngle.Value = 45.0;
31:             this.SliderStartHeight.Value = 5.0;
32:             this.SliderStartVelocity.Value = 10.0;
33:         }
34: 
35:         // properties
36:         public double StartHeight
37:         {
38:             get { return (double) this.GetValue(StartHeightProperty); }
39:             set { this.SetValue(StartHeightProperty, value); }
40:         }
41: 
42:         public double StartVelocity
43:         {
44:             get { return this.startVelocity; }
45:         }
46: 
47:         public double StartAngle
48:         {
49:             get { return this.startAngle; }
50:         }
51: 
52:         private void Slider_ValueChanged(
53:             Object sender, RoutedPropertyChangedEventArgs<double> e)
54:         {
55:             if (sender == this.SliderStartAngle)
56:             {
57:                 this.TextBoxLaunchAngle.Text = String.Format("{0:00.00}",
58:                     this.SliderStartAngle.Value);
59: 
60:                 this.startAngle = this.SliderStartAngle.Value;
61:             }
62:             else if (sender == this.SliderStartHeight)
63:             {
64:                 this.TextBoxLaunchHeight.Text = String.Format("{0:00.00}",
65:                     this.SliderStartHeight.Value);
66: 
67:                 this.StartHeight = this.SliderStartHeight.Value;
68:             }
69:             else if (sender == this.SliderStartVelocity)
70:             {
71:                 this.TextBoxLaunchVelocity.Text = String.Format("{0:00.00}",
72:                     this.SliderStartVelocity.Value);
73: 
74:                 this.startVelocity = this.SliderStartVelocity.Value;
75:             }
76:         }
77:     }
78: }

Beispiel 3. Steuerelement TrajectorySettingsControl: Codebehind.


Die von mir realisierte View der Anwendung ist als WPF-Custom Control ausgelegt. Es genügt damit eine C#-Datei zur Implementierung. In Listing 4 finden wir eine Klasse TrajectoryViewControl vor, die die Canvas-Klasse spezialisiert:

001: namespace Wpf_TrajectorySimulation
002: {
003:     using System;
004:     using System.Windows;
005:     using System.Windows.Controls;
006:     using System.Windows.Media;
007:     using System.Windows.Shapes;
008: 
009:     public class TrajectoryViewControl : Canvas
010:     {
011:         private const double MaxSizeX = 25;  // unit 'meter'
012:         private const double MaxSizeY = 15;  // unit 'meter'
013: 
014:         private const double BaseUnits = 20;
015:         private const double CanvasWidth = (MaxSizeX + 2) * BaseUnits;
016:         private const double CanvasHeight = (MaxSizeY + 2) * BaseUnits;
017: 
018:         private const int BallDiameter = 12;
019:         private Ellipse ball;
020: 
021:         private int lastChild;
022:         private double lastX;
023:         private double lastY;
024: 
025:         public static readonly DependencyProperty BallHeightProperty;
026: 
027:         // static c'tor
028:         static TrajectoryViewControl()
029:         {
030:             BallHeightProperty = DependencyProperty.Register(
031:                 "BallHeight",                   // name of property
032:                 typeof(double),                 // type of property
033:                 typeof(TrajectoryViewControl),  // type of property owner
034:                 new PropertyMetadata(0.0, OnBallHeightPropertyChanged)
035:             );
036:         }
037: 
038:         // c'tor
039:         public TrajectoryViewControl()
040:         {
041:             this.Background = Brushes.LightGray;
042: 
043:             // set fixed size of this control
044:             this.Height = CanvasHeight;
045:             this.Width = CanvasWidth;
046: 
047:             // draw coordinates
048:             this.DrawCoordinateAxes();
049: 
050:             // prepare projectile
051:             this.ball = new Ellipse();
052:             this.ball.Fill = Brushes.Black;
053:             this.ball.Height = BallDiameter;
054:             this.ball.Width = BallDiameter;
055:             this.DrawBallAbsolute(
056:                 BaseUnits - BallDiameter / 2,
057:                 CanvasHeight - BaseUnits - BallDiameter / 2);
058:             this.Children.Add(this.ball);
059: 
060:             // store current number of children controls for later control reset
061:             this.lastChild = this.Children.Count;
062:             this.lastX = -1;
063:             this.lastY = -1;
064:         }
065: 
066:         // properties
067:         public bool ShowTrajectory { get; set; }
068: 
069:         public double BallHeight
070:         {
071:             get { return (double) this.GetValue(BallHeightProperty); }
072:             set { this.SetValue(BallHeightProperty, value); }
073:         }
074: 
075:         public Point BallPosition
076:         {
077:             set
078:             {
079:                 this.DrawBall(value);
080:             }
081:         }
082: 
083:         // public interface
084:         public void Clear()
085:         {
086:             this.Children.RemoveRange(this.lastChild, this.Children.Count - this.lastChild);
087: 
088:             this.ShowTrajectory = false;
089:             this.BallPosition = new Point() { X = 0.0, Y = this.BallHeight };
090: 
091:             this.lastX = -1;
092:             this.lastY = -1;
093:         }
094: 
095:         // private helper methods
096:         private static void OnBallHeightPropertyChanged(
097:             DependencyObject d, DependencyPropertyChangedEventArgs e)
098:         {
099:             TrajectoryViewControl control = (TrajectoryViewControl)d;
100: 
101:             if (e.NewValue != null)
102:             {
103:                 control.DrawBall(new Point() { X = 0.0, Y = (double)e.NewValue });
104:             }
105:         }
106: 
107:         private void DrawBall(Point p)
108:         {
109:             double x = BaseUnits + p.X * BaseUnits;
110:             double y = CanvasHeight - BaseUnits - p.Y * BaseUnits;
111: 
112:             this.DrawBallAbsolute(x - BallDiameter / 2, y - BallDiameter / 2);
113: 
114:             if (this.ShowTrajectory)
115:             {
116:                 if (this.lastX == -1 && this.lastY == -1)
117:                 {
118:                     // just store coordinates, if it's first point
119:                     this.lastX = x;
120:                     this.lastY = y;
121:                 }
122:                 else
123:                 {
124:                     Line segment = new Line();
125:                     segment.X1 = this.lastX;
126:                     segment.Y1 = this.lastY;
127:                     segment.X2 = x;
128:                     segment.Y2 = y;
129:                     segment.Stroke = Brushes.Red;
130:                     segment.StrokeThickness = 2;
131:                     this.Children.Add(segment);
132: 
133:                     this.lastX = x;
134:                     this.lastY = y;
135:                 }
136:             }
137:         }
138: 
139:         private void DrawBallAbsolute(double x, double y)
140:         {
141:             this.ball.SetValue(Canvas.LeftProperty, x);
142:             this.ball.SetValue(Canvas.TopProperty, y);
143:         }
144: 
145:         private void DrawCoordinateAxes()
146:         {
147:             // draw coordinate axes
148:             Line xAxes = new Line();
149:             xAxes.X1 = BaseUnits;
150:             xAxes.Y1 = 1 * BaseUnits + MaxSizeY * BaseUnits;
151:             xAxes.X2 = BaseUnits + (MaxSizeX * BaseUnits);
152:             xAxes.Y2 = 1 * BaseUnits + MaxSizeY * BaseUnits;
153:             xAxes.Stroke = Brushes.Black;
154:             xAxes.StrokeThickness = 2;
155:             this.Children.Add(xAxes);
156: 
157:             Line yAxes = new Line();
158:             yAxes.X1 = BaseUnits;
159:             yAxes.Y1 = 1 * BaseUnits + MaxSizeY * BaseUnits;
160:             yAxes.X2 = BaseUnits;
161:             yAxes.Y2 = BaseUnits;
162:             yAxes.Stroke = Brushes.Black;
163:             yAxes.StrokeThickness = 2;
164:             this.Children.Add(yAxes);
165: 
166:             // draw units of coordinate axes
167:             for (int i = 1; i < MaxSizeX; i++)
168:             {
169:                 Line unit = new Line();
170:                 unit.X1 = BaseUnits + i * BaseUnits;
171:                 unit.Y1 = BaseUnits + MaxSizeY * BaseUnits - 3;
172:                 unit.X2 = BaseUnits + i * BaseUnits;
173:                 unit.Y2 = BaseUnits + MaxSizeY * BaseUnits + 3;
174:                 unit.Stroke = Brushes.Black;
175:                 unit.StrokeThickness = 2;
176:                 this.Children.Add(unit);
177:             }
178: 
179:             for (int i = 1; i < MaxSizeY; i++)
180:             {
181:                 Line unit = new Line();
182:                 unit.X1 = BaseUnits - 3;
183:                 unit.Y1 = BaseUnits + i * BaseUnits;
184:                 unit.X2 = BaseUnits + 3;
185:                 unit.Y2 = BaseUnits + i * BaseUnits;
186:                 unit.Stroke = Brushes.Black;
187:                 unit.StrokeThickness = 2;
188:                 this.Children.Add(unit);
189:             }
190:         }
191:     }
192: }

Beispiel 4. Steuerelement TrajectoryViewControl.


Abschließend gehen wir in Listing 5 und Listing 6 auf das Hauptfenster der Anwendung ein.

01: <Window
02:     x:Class="Wpf_TrajectorySimulation.MainWindow"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:local="clr-namespace:Wpf_TrajectorySimulation"
06:     Title="Der schiefe Wurf (Physik / Mechanik)"
07:     Height="500" Width="700">
08: 
09:     <DockPanel LastChildFill="True">
10:         <UniformGrid DockPanel.Dock="Bottom" Rows="1"  Columns="2" Margin="5" >
11:             <UniformGrid Rows="1" Columns="2">
12:                 <Button Name="ButtonStart" Click="Button_Click" Margin="5">Start</Button>
13:                 <Button Name="ButtonClear" Click="Button_Click" Margin="5">Clear</Button>
14:             </UniformGrid>           
15:             <DockPanel LastChildFill="True">
16:                 <Label DockPanel.Dock="Left" VerticalContentAlignment="Center">Zeit:</Label>
17:                 <TextBox Name="TextBoxTimeElapsed" IsEnabled="False" Margin="5"></TextBox>
18:             </DockPanel>
19:         </UniformGrid>
20:         <local:TrajectorySettingsControl
21:             x:Name="TrajectorySettings"
22:             DockPanel.Dock="Bottom">
23:         </local:TrajectorySettingsControl>
24:         <local:TrajectoryViewControl
25:             x:Name="TrajectoryView"
26:             BallHeight="{Binding ElementName=TrajectorySettings, Path=StartHeight}">
27:         </local:TrajectoryViewControl>
28:     </DockPanel>
29: </Window>

Beispiel 5. Hauptfenster MainWindow: XAML.


Die Zeile 26 von Listing 5 verdient eine besondere Beachtung. Die Ansicht (Steuerelement TrajectoryViewControl) besitzt eine Eigenschaft BallHeight. Mit dieser Eigenschaft wird die Höhe des Balls eingestellt – ein Vorgang, der immer dann graphisch beachtet werden soll, wenn im TrajectorySettingsControl-Steuerelement der entsprechende Schiebebalken bewegt wird. Mit Hilfe der WPF-Datenbindung kann man beide Eigenschaften bequem miteinander verbinden – wenn die Ursprungseigenschaft eine Dependency-Property ist. Die Datenbindung in Zeile 26 hat also zur Folge, dass Änderungen im TrajectorySettingsControl-Steuerelement unmittelbar Änderungen im TrajectoryViewControl-Steuerelement (hier: Höhe das Balls wird verändert) nach sich ziehen.

01: namespace Wpf_TrajectorySimulation
02: {
03:     using System;
04:     using System.Windows;
05: 
06:     public partial class MainWindow : Window
07:     {
08:         private TrajectoryModel model;
09: 
10:         public MainWindow()
11:         {
12:             this.InitializeComponent();
13: 
14:             this.model = new TrajectoryModel();
15:             this.model.PositionChanged += this.Model_PositionChanged;
16:         }
17:         private void Button_Click(Object sender, RoutedEventArgs e)
18:         {
19:             if (sender == this.ButtonStart)
20:             {
21:                 this.TrajectoryView.Clear();
22:                 this.TrajectoryView.ShowTrajectory = true;
23: 
24:                 this.model.StartAngle = this.TrajectorySettings.StartAngle;
25:                 this.model.StartHeight = this.TrajectorySettings.StartHeight;
26:                 this.model.StartVelocity = this.TrajectorySettings.StartVelocity;
27:                 this.model.Start();
28:             }
29:             else if (sender == this.ButtonClear)
30:             {
31:                 this.TrajectoryView.Clear();
32:             }
33:         }
34: 
35:         private void Model_PositionChanged(Point p, double t)
36:         {
37:             this.TrajectoryView.BallPosition = p;
38:             this.TextBoxTimeElapsed.Text = String.Format("{0:00.00} sec.", t);
39:         }
40:     }
41: }

Beispiel 6. Hauptfenster MainWindow: Codebehind.