Bewegte geometrische Figuren

1. Aufgabe

Threads sind das probate Hilfsmittel für Anwendungen, um eine Oberfläche mit bewegten grafischen Figuren zu gestalten. Wir betrachten zu diesem Zweck eine Anwendung, die unterschiedliche geometrische Figuren (Rechtecke, Kreise und Polygone) mit wechselnden Positionen anzeigt. Die Positionen der Figuren sind mit Zufallszahlen zu berechnen, der zeitliche Abstand von einem Positionswechsel zum nächsten sollte unterschiedlich groß sein. Ein Schnappschuss der Anwendung könnte beispielsweise wie in Abbildung 1 gezeigt aussehen.

Bewegte geometrische Figuren.

Abbildung 1. Bewegte geometrische Figuren.


Ziel dieser Aufgabe sollte es unter anderem sein, Threads mit Hilfe der Schaltflächen kontrolliert zu starten und zu beenden. Auch sollte nicht übersehen werden, dass beim Schließen der Anwendung noch laufende Threads zu beenden sind.

2. Lösung

Nachfolgender Lösungsvorschlag zu dieser Aufgabe ist deklarativ verfasst. Für die Oberfläche steht eine XAML-Datei zur Verfügung, die mit sechs Schaltflächen das Starten und Stoppen der Rechteck-, Polygon- und Kreisanimationen ermöglicht (Listing 1):

01: <Window x:Class="WpfFlyingShapes.MainWindow"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="Flying Shapes ..." Height="350" Width="500">
05:     <DockPanel LastChildFill="True">
06:         <UniformGrid  Columns="3" Rows="2" DockPanel.Dock="Top">
07: 
08:             <Button Name="ButtonRectangleStartNeu"
09:                     Click="Button_Click">Start Rectangle ...</Button>
10:             <Button Name="ButtonPolygonStartNeu"
11:                     Click="Button_Click">Start Polygon ...</Button>
12:             <Button Name="ButtonCircleStartNeu"
13:                     Click="Button_Click">Start Circle ...</Button>
14: 
15:             <Button Name="ButtonRectangleStopNeu"
16:                     Click="Button_Click">Stop Rectangle ...</Button>
17:             <Button Name="ButtonPolygonStopNeu"
18:                     Click="Button_Click">Stop Polygon ...</Button>
19:             <Button Name="ButtonCircleStopNeu"
20:                     Click="Button_Click">Stop Circle ...</Button>
21:         
22:         </UniformGrid>
23:         <Canvas Name="CanvasShapes"></Canvas>
24:     </DockPanel>
25: </Window>

Beispiel 1. XAML-Anteil einer einfachen Animation mit bewegten Rechtecken, Polygonen und Kreisen.

 

Damit fehlt noch die Code-Behind-Datei, siehe Listing 2:

001: namespace WpfFlyingShapes
002: {
003:     internal delegate void SetCoordinateHandler(Shape s, Point p);
004: 
005:     public partial class MainWindow : Window
006:     {
007:         // drawing utilities
008:         private Ellipse circle;
009:         private Rectangle rect;
010:         private Polygon poly;
011: 
012:         // threading utilities
013:         private Thread threadRectangle;
014:         private Thread threadPolygon;
015:         private Thread threadCircle;
016: 
017:         private Random random;
018: 
019:         // c'tor
020:         public MainWindow()
021:         {
022:             this.InitializeComponent();
023: 
024:             this.random = new Random();
025: 
026:             // setup rectangle shape
027:             this.rect = new Rectangle();
028:             this.rect.Height = 40;
029:             this.rect.Width = 50;
030:             this.rect.Stroke = Brushes.DarkRed;
031:             this.rect.StrokeThickness = 8;
032:             this.rect.Fill = Brushes.LightGray;
033:             this.rect.RadiusX = 3;
034:             this.rect.RadiusY = 3;
035: 
036:             // setup circle shape
037:             this.circle = new Ellipse();
038:             this.circle.Height = 50;
039:             this.circle.Width = 50;
040:             this.circle.Stroke = Brushes.DarkGreen;
041:             this.circle.StrokeThickness = 8;
042:             this.circle.Fill = Brushes.LightGray;
043: 
044:             // setup polygon shape
045:             this.poly = new Polygon();
046:             this.poly.Points.Add(new Point(10, 10));
047:             this.poly.Points.Add(new Point(50, 50));
048:             this.poly.Points.Add(new Point(10, 50));
049:             this.poly.Points.Add(new Point(50, 10));
050:             this.poly.Stroke = Brushes.DarkBlue;
051:             this.poly.StrokeThickness = 8;
052:             this.poly.Fill = Brushes.LightGray;
053: 
054:             this.ButtonRectangleStopNeu.IsEnabled = false;
055:             this.ButtonPolygonStopNeu.IsEnabled = false;
056:             this.ButtonCircleStopNeu.IsEnabled = false;
057:         }
058: 
059:         // event handler
060:         private void Button_Click(Object sender, RoutedEventArgs e)
061:         {
062:             if (sender == this.ButtonRectangleStartNeu)
063:             {
064:                 this.CanvasShapes.Children.Add(this.rect);
065: 
066:                 // animate rectangle
067:                 ParameterizedThreadStart ts =
068:                     new ParameterizedThreadStart(this.FlyingShape);
069:                 this.threadRectangle = new Thread(ts);
070:                 this.threadRectangle.Priority = ThreadPriority.BelowNormal;
071:                 this.threadRectangle.Start(this.rect);
072: 
073:                 // enable rsp. disable controlling buttons
074:                 this.ButtonRectangleStartNeu.IsEnabled = false;
075:                 this.ButtonRectangleStopNeu.IsEnabled = true;
076:             }
077:             else if (sender == this.ButtonPolygonStartNeu)
078:             {
079:                 this.CanvasShapes.Children.Add(this.poly);
080: 
081:                 // animate polygon
082:                 ParameterizedThreadStart ts =
083:                     new ParameterizedThreadStart(this.FlyingShape);
084:                 this.threadPolygon = new Thread(ts);
085:                 this.threadPolygon.Priority = ThreadPriority.BelowNormal;
086:                 this.threadPolygon.Start(this.poly);
087: 
088:                 // enable rsp. disable controlling buttons
089:                 this.ButtonPolygonStartNeu.IsEnabled = false;
090:                 this.ButtonPolygonStopNeu.IsEnabled = true;
091:             }
092:             else if (sender == this.ButtonCircleStartNeu)
093:             {
094:                 this.CanvasShapes.Children.Add(this.circle);
095: 
096:                 // animate circle
097:                 ParameterizedThreadStart ts =
098:                     new ParameterizedThreadStart(this.FlyingShape);
099:                 this.threadCircle = new Thread(ts);
100:                 this.threadCircle.Priority = ThreadPriority.BelowNormal;
101:                 this.threadCircle.Start(this.circle);
102: 
103:                 // enable rsp. disable controlling buttons
104:                 this.ButtonCircleStartNeu.IsEnabled = false;
105:                 this.ButtonCircleStopNeu.IsEnabled = true;
106:             }
107:             else if (sender == this.ButtonRectangleStopNeu)
108:             {
109:                 // terminate thread
110:                 this.threadRectangle.Abort();
111:                 this.threadRectangle = null;
112: 
113:                 this.CanvasShapes.Children.Remove(this.rect);
114: 
115:                 // enable rsp. disable controlling buttons
116:                 this.ButtonRectangleStartNeu.IsEnabled = true;
117:                 this.ButtonRectangleStopNeu.IsEnabled = false;
118:             }
119:             else if (sender == this.ButtonPolygonStopNeu)
120:             {
121:                 // terminate thread
122:                 this.threadPolygon.Abort();
123:                 this.threadPolygon = null;
124: 
125:                 this.CanvasShapes.Children.Remove(this.poly);
126: 
127:                 // enable rsp. disable controlling buttons
128:                 this.ButtonPolygonStartNeu.IsEnabled = true;
129:                 this.ButtonPolygonStopNeu.IsEnabled = false;
130:             }
131:             else if (sender == this.ButtonCircleStopNeu)
132:             {
133:                 // terminate thread
134:                 this.threadCircle.Abort();
135:                 this.threadCircle = null;
136: 
137:                 this.CanvasShapes.Children.Remove(this.circle);
138: 
139:                 // enable rsp. disable controlling buttons
140:                 this.ButtonCircleStartNeu.IsEnabled = true;
141:                 this.ButtonCircleStopNeu.IsEnabled = false;
142:             }
143:         }
144: 
145:         private void FlyingShape(Object s)
146:         {
147:             try
148:             {
149:                 while (true)
150:                 {
151:                     // calculate next shape position
152:                     int x = this.random.Next(
153:                         (int)this.CanvasShapes.ActualWidth - 50);
154:                     int y = this.random.Next(
155:                         (int)this.CanvasShapes.ActualHeight - 50);
156: 
157:                     Point p = new Point(x, y);
158:                     this.SetLocation((Shape)s, p);
159: 
160:                     // suspend thread
161:                     int pause = 300 + this.random.Next(500);
162:                     Thread.Sleep(pause);
163:                 }
164:             }
165:             catch (Exception) { }
166:         }
167: 
168:         protected override void OnClosing(CancelEventArgs e)
169:         {
170:             base.OnClosing(e);
171: 
172:             // stop thread procedures
173:             if (this.threadRectangle != null)
174:                 this.threadRectangle.Abort();
175:             if (this.threadPolygon != null)
176:                 this.threadPolygon.Abort();
177:             if (this.threadCircle != null)
178:                 this.threadCircle.Abort();
179:         }
180: 
181:         // private helper method
182:         private void SetLocation(Shape s, Point p)
183:         {
184:             if (this.Dispatcher.CheckAccess())
185:             {
186:                 // running on this control's UI thread
187:                 s.SetCurrentValue(Canvas.LeftProperty, p.X);
188:                 s.SetCurrentValue(Canvas.TopProperty, p.Y);
189:             }
190:             else
191:             {
192:                 // running on a different, non-UI thread
193:                 SetCoordinateHandler h =
194:                     new SetCoordinateHandler(this.SetLocation);
195:                 this.Dispatcher.BeginInvoke(h, s, p);
196:             }
197:         }
198:     }
199: }

Beispiel 2. Code-Behind-Datei einer einfachen Animation mit bewegten Rechtecken, Polygonen und Kreisen.

 

In Zeile 182 von Listing 2 finden Sie eine Methode SetLocation vor, die ein beliebiges Shape-Objekt an einer bestimmten Position platziert. Da die Positionsänderungen einerseits Änderungen am UI-Interface des Steuerelements vornehmen, der Zugriff aber im Kontext eines beliebigen Threads erfolgen kann, wurden diese konform zum WPF-Regelwerk für UI und Multithreading mit Hilfe eines Dispatcher-Objekts implementiert. In Zeile 184 wird mittels der CheckAccess-Methode zunächst ermittelt, in welchem Thread-Kontext der Zugriff erfolgt. Darf direkt auf das Steuerelement zugegriffen werden, kann in den Zeilen 187 und 188 die Methode SetCurrentValue direkt aufgerufen werden. Andernfalls muss ein Umweg über die BeginInvoke-Methode des Dispatcher-Objekts eingeschlagen werden (Zeile 195). Interessant dabei ist, dass mittels BeginInvoke dieselbe Methode SetLocation in den Primärthread der Anwendung eingeschleust wird.