Unendlich viele Nachkommastellen der Zahl Pi

1. Aufgabe

In der Mathematik kommt der Kreiszahl Pi (π = 3,1415926...) eine wichtige Bedeutung zu: Sie beschreibt das Verhältnis zwischen dem Umfang eines Kreises und seinem Durchmesser. π ist eine irrationale Zahl und deshalb nicht als Bruch zweier ganzer Zahlen darstellbar. Sie besitzt unendlich viele Nachkommastellen, die nicht periodisch sind und deshalb auch nie ganz genau bestimmt werden können, wie der folgende minimale Versuch erahnen lässt:

π = 3,14159 26535 89793 23846 26433 83279 50288 41971 69399 37510
      58209 74944 59230 78164 06286 20899 86280 34825 34211 70679 ...

Eine Zahl, die von ihrer praktischen Bedeutung so nah und einfach erscheint und dann doch so unerreichbar ist, war seit ihrer Entdeckung vor über dreitausend Jahren für die Menschen schon immer von einem Hauch von Mystik umwittert. Man wollte diese geheimnisvolle Zahl so genau wie möglich verstehen und ergründen. Im 16. Jahrhundert konnte der deutsche Mathematiker Ludolf von Ceulen immerhin schon die ersten 35 Stellen nach dem Komma bestimmen. Und auch heute noch liefern sich Mathematiker ein Kopf-an-Kopf-Rennen um den π-Nachkommastellenrekord. Seit September 1999 kennt man schon 206 Milliarden Nachkommastellen dieser Zahl. Den Aufwand, um diese Fülle an Ziffern zu berechnen, machte sich Yasuma Kanada von der Universität in Tokio. Drei Jahre später gelang es ihm, auf einem Hitachi-Supercomputer über 1,2 Billionen Nachkommastellen von π zu berechnen. Sein Computer brauchte dafür über 400 Stunden! Diese Leistung wird von Ihnen in dieser Klausur nicht verlangt! Der Schwerpunkt liegt statt dessen auf der nicht-blockierenden Visualisierung einer beliebig großen Anzahl von Nachkommastellen von π!

Im Mittelpunkt dieser Klausur stehen drei oberflächenbasierte Komponenten, die jede auf ihre eigene Weise mit der Zahl π in Zusammenhang stehen. Um diese Komponenten möglichst ansehnlich in einem Window-Container integrieren zu können, spezialisieren sie alle ein Gruppenfeld-Steuerelement (Container-Klasse GroupBox):

  • Gruppenfeld-Steuerelement PiToolbar:

    Komponente zur Parametrierung der maximal zu berechnenden Anzahl von Nachkommastellen von π sowie zur Visualisierung einer aktiven Berechnung.

  • Gruppenfeld-Steuerelement PiDisplay:

    Komponente zur Anzeige von π in einem TextBox-Steuerelement. Die Anzahl der Nachkommastellen kann beliebig groß sein.

  • Gruppenfeld-Steuerelement PiExtraordinarySequence:

    Komponente, um eine bestimmte Ziffernfolge in den Nachkommastellen von π zu suchen.

Alle drei Komponenten setzen auf der Hilfsklasse PiCalculator auf:

  • Klasse PiCalculator:

    Hilfsklasse zur einfachen Berechnung einer beliebig großen Anzahl von Nachkommastellen von π. Die PiCalculator-Klasse besitzt keine Oberfläche.

Prinzipiell besteht der Clou dieser Aufgabe darin, dass

  • eine Klasse PiCalculator beliebig viele Nachkommastellen von π berechnen kann, aber keinerlei Kenntnisse von der Gestaltung einer Oberfläche besitzt.

  • drei WPF-Steuerelemente PiToolbar, PiDisplay und PiExtraordinarySequence unterschiedliche Facetten in der Berechnung und Visualisierung der Nachkommastellen von π anzeigen, diese aber so gut wie keine Kenntnisse vom Rechenvorgang des Kalkulators besitzen.

Das bedeutet wiederum, dass Kalkulator und Steuerelemente auf Basis standardisierter C#-Mechanismen Daten austauschen müssen, um Zwischenmeldungen und Ergebnisse des Kalkulators anzuzeigen. In dieser Aufgabe sind dies Ereignisse und Properties, die an geeigneten Stellen im Entwurf der Logik- und UI-Klassen vorhanden sein müssen. Alles Details hierzu sind in den nachfolgenden Entwurfsempfehlungen beschrieben.

Da ein Teil der Berechnungen in dieser Aufgabe sehr zeitaufwändig sein kann, ist das Hilfsmittel des Multithreadings einzusetzen, um die Bedienfähigkeit der Anwendung nicht zu gefährden. Langwierige Berechnungen sind in den Kontext von nebenläufigen Arbeitsthreads auszulagern.

Als Hilfsmittel darf die Klasse PiCalculator eine Klasse NineDigitsOfPi in Anspruch nehmen, die von Fabrice Bellard im Jahre 1997 geschrieben wurde. Mit den Methoden dieser Klasse lassen sich beliebig viele Nachkommastellen von π berechnen. Die zu Grunde liegenden Algorithmen dieser Klasse sind nicht trivial, ihr Quellcode wird deshalb zur Verfügung gestellt (Datei NineDigitsOfPiAt.cs). Für diese Klausur ist die einzige öffentliche Klassenmethode StartingAt wichtig (Tabelle 1):

Methode

Beschreibung

StartingAt

public static int StartingAt (int n);

Der Parameter n spezifiziert, ab welcher Position nach dem Komma die Nachkommastellen von π zu berechnen sind. Als Resultat werden immer neun Dezimalziffern auf einmal (in einer int-Variablen) zurückgeliefert. Beispiel: Ein Aufruf von

NineDigitsOfPi.StartingAt (15);

liefert das Resultat 238462643, vergleiche dazu die Darstellung von π in der Einleitung!

Tabelle 1. Öffentliche Methode der Klasse NineDigitsOfPi.



Achtung

Die Methode StartingAt muss immer mit einem Aktualparameter größer Null aufgerufen werden!


Mit Hilfe der Klasse NineDigitsOfPi ist nun die Klasse PiCalculator zu erstellen, deren Methoden in Tabelle 2 näher beschrieben sind. Bitte beachten Sie: Diese Klasse ist von der Klasse System.Object abzuleiten, sie besitzt also keine Oberfläche. Auf die drei Komponenten mit grafischer Oberfläche kommen wir erst in den nachfolgenden Abschnitten zu sprechen:

Element

Beschreibung

Methode Start

public void Start();

Stößt die Berechnung von π als Folge einzelner Fragmente, bestehend aus neun Dezimalziffern, an. Die Anzahl der insgesamt zu berechnenden Nachkommastellen ist durch die Eigenschaft Precision vorgegeben.

Ein Aufruf von Start nimmt nur den Auftrag zur Berechnung von π entgegen, die Methode kehrt sofort zum Aufrufer zurück. Die eigentliche Arbeit ist innerhalb des Objekts in einer privaten Hilfsmethode im Kontext eines oder mehrerer Threads durchzuführen. Die Anzahl der Threads, die sich an der Berechnung beteiligen, wird durch die Eigenschaft NumThreads festgelegt.

Methode Stop

public void Stop();

Bricht einen vorangegangenen Aufruf zur Berechnung von π vorzeitig ab, sofern dieser Auftrag noch aktiv ist. Das Ereignis PiCalculationDone ist auszulösen, siehe dazu die Beschreibung weiter unten.

Eigenschaft Precision

public int Precision { get; set; }

Die Eigenschaft legt die Anzahl der Nachkommastellen von π fest, die zu berechnen sind.

Eigenschaft NumThreads

public int NumThreads { set; }

Die Eigenschaft legt die Anzahl der Threads fest, die sich an der Berechnung von π beteiligen.

Ereignis PiGotFragment

public event PiGotFragmentHandler PiGotFragment;

Die Berechnung der Nachkommastellen von π nimmt längere Zeit in Anspruch. Pro berechneter Gruppe von neun Dezimalziffern wird das PiGotFragment-Ereignis ausgelöst. Der zu Grunde liegende Delegate-Typ PiGotFragmentHandler lautet

public delegate void PiGotFragmentHandler (int tid, int offset, int fragment);

Der Parameter fragment beschreibt das Fragment von 9 Ziffern der Nachkommastellen von π, die an der Position offset vorzufinden sind. Logischerweise sollte dieses Ereignis zu einem bestimmten Offset nur einmal ausgelöst werden, um Doppelberechnungen im Kalkulator zu vermeiden. Der Parameter tid (thread id) gibt an, in welchem Threadkontext die Berechnung erfolgte.

Ereignis PiCalculationDone

public event PiCalculationDoneHandler PiCalculationDone;

Das Ende der – möglicherweise parallelen – Berechnungen der Nachkommastellen von π (siehe auch Eigenschaft Precision) wird durch das Ereignis PiCalculationDone mitgeteilt. Der zu Grunde liegende Delegate-Typ PiCalculationDoneHandler lautet

public delegate void PiCalculationDoneHandler ();

Offensichtlich benötigt dieses Ereignis keine zusätzlichen Parameter.

Tabelle 2. Elemente der Klasse PiCalculator.


Beachte: Entscheiden Sie, ob die Klasse PiCalculator bei ihrer Realisierung die Synchronisationsmechanismen des Multithreadings einsetzen muss oder nicht!

Das Gruppenfeld-Steuerelement PiToolbar stellt eine logische Gruppierung einzelner Steuerelemente (wie Schaltflächen, Schiebebalken oder Fortschrittsanzeige) dar, um die im letzten Abschnitt vorgestellte Klasse PiCalculator möglichst komfortabel bedienen zu können (Abbildung 1):

Oberfläche der PiToolbar-Komponente.

Abbildung 1. Oberfläche der PiToolbar-Komponente.


Um die genaue Funktionsweise des PiToolbar-Gruppenfelds zu verstehen, genügt es, die Zuordnung der einzelnen Elemente der Hilfsklasse PiCalculator zu den Steuerelementen des Gruppenfelds zu betrachten, siehe dazu Tabelle 3:

Gruppenfeld: Steuerelement

Hilfsklasse PiCalculator

Schaltfläche „Start

Aufruf von Methode Start am PiCalculator-Objekt.

Schaltfläche „Stop

Aufruf von Methode Stop am PiCalculator-Objekt.

Schiebebalken „Precision

Setzen des Attributs Precision am PiCalculator-Objekt.

Auswahlfeld „Threads

Setzen des Attributs NumThreads am PiCalculator-Objekt.

Fortschrittsbalken

Abbildung des Ereignisses PiGotFragment. Zusammen mit der Eigenschaft Precision ist ein prozentualer Wert ermittelbar, wie weit die Berechnung fortgeschritten ist.

Tabelle 3. Gegenüberstellung von Klasse PiCalculator und Steuerelement PiToolbar.


Der Fortschrittsbalken sollte im Steuerelement PiToolbar natürlich mit der Zugriffsklasse private versehen sein. Man kann über das PiToolbar-Steuerelement also nicht direkt auf den Balken zugreifen. Aus diesem Grunde ergänzen wir das Steuerelement um eine Methode PerformStep, mit deren Hilfe der Balken bei einer laufenden Berechung angepasst werden kann (Tabelle 4):

Methode

Beschreibung

PerformStep

public void PerformStep();

Pro Aufruf von PerformStep passt das Steuerelement den Fortschrittsbalken an den Zustand der laufenden Berechnung an. Das das Steuerelement in seinen (privaten) Instanzvariablen die Information zur Anzahl der zu berechnenden Nachkommastellen besitzt, ist dies mit einer sehr einfachen Berechnung möglich.

Tabelle 4. Öffentliche Methode PerformStep des PiToolbar-Steuerelements.


Wir vervollständigen das Steuerelement mit einer Eigenschaft Status und einem Ereignis Action. An Hand der Eigenschaft Status kann man abfragen, ob aktuell eine Berechnung der Nachkommastellen aktiv ist oder nicht. Das Action-Ereignis dient dem Zweck, „Aktionen“ wie das Starten oder Anhalten einer Berechnung interessierten Clients des Steuerelements mitzuteilen (Tabelle 5):

Element

Beschreibung

Eigenschaft Status

public ToolbarStatus Status { set; get; };

Dient zum Lesen und Schreiben des aktuellen Status einer Nachkommastellenberechnung mit dem Aufzählungstyp ToolbarStatus:

public enum ToolbarStatus { Idle, Active };

In Abhängigkeit vom Wert der Eigenschaft sollte innerhalb des Gruppenfeld-Steuerelements ein Label-Steuerelement in den Farben Grün (Status aktiv) bzw. Rot (Status inaktiv) angezeigt werden.

Ereignis Action

public event ToolbarActionsEventHandler Action;

Mit dem Action-Ereignis werden Aktionen, die im PiToolbar-Steuerelement ausgelöst werden, an registrierte Clients weitergereicht. Das Ereignis selbst ist vom Typ

public delegate void ToolbarActionsEventHandler (ToolbarActions action);

Der Parameter action wiederum wird durch einen Aufzählungstyp ToolbarActions definiert, der zwei Aktionen zum Starten und Anhalten beschreibt:

public enum ToolbarActions { Start, Stop };

Tabelle 5. Weitere öffentliche Elemente des PiToolbar-Steuerelements.


Zum Anzeigen der berechneten Nachkommastellen in einem PiCalculator-Objekt gibt es das Steuerelement PiDisplay. Wie die PiToolbar-Komponente ist auch das PiDisplay-Steuerelement als Gruppenfeld zu entwerfen (Abbildung 2):

Oberfläche der PiDisplay-Komponente.

Abbildung 2. Oberfläche der PiDisplay-Komponente.


Um möglichst passgenau dieses Steuerelement mit einem PiCalculator-Objekt verschalten zu können, ergänzen Sie das PiDisplay-Steuerelement um die Elemente aus Tabelle 6:

Element

Beschreibung

Eigenschaft Precision

public int Precision { set; }

Die Eigenschaft informiert das Steuerelement über die Anzahl der Nachkommastellen von π, die zu berechnen sind. Siehe dazu die Methode Init.

Methode Init

public void Init();

Um in der Anzeige, speziell beim Einsatz von mehreren Threads, hervorzuheben, welche Nachkommastellen (Fragmente mit 9 Ziffern) in der Anzeige noch fehlen, kann man vor dem Start der Berechnung alle zu berechnenden Stellen in der Anzeige mit einem Unterstrich hervorheben.

Methode Clear

public void Clear();

Löscht den Inhalt des im Steuerelement vorhandenen TextBox-Objekts.

Methode SetFragmentAt

public void SetFragmentAt (int offset, int fragment);

Fügt in der Anzeige des Steuerelements ein berechnetes Fragment ein. Der Wert von fragment ist in eine Zeichenkette der Länge 9 umzuwandeln. Dabei müssen möglicherweise fehlende führende Nullen hinzugefügt werden. Anschließend ist das so berechnete String-Objekt im TextBox-Objekt des PiDisplay-Steuerelements an der Stelle offset einzufügen.

Hinweis: Die Unterstützung dieser Aufgabenstellung durch die Klasse TextBox ist nicht sehr umfangreich. Sie erreichen das Ziel der Aufgabenstellung nur mit recht einfachen, bisweilen auch recht ineffizienten Aktionen am TextBox-Objekt.

Tabelle 6. Zusätzliche Elemente der PiDisplay-Komponente.


Neben der Eigenschaft von π, die Berechnung einer Kreisfläche durchführen zu können, gilt diese Zahl auch als besonders gute Zufallszahl. Unter einer guten Folge von Zufallszahlen versteht man eine, in der innerhalb der gesamten Zahlenfolge keine Teilzahlenfolge mehrere Male auftaucht.

Zum Suchen einer bestimmten Teilzahlenfolge erstellen Sie nun das Gruppenfeld-Steuerelement PiExtraordinarySequence. In diesem Steuerelement geben Sie zunächst in einem Textfeld die zu suchende Zeichenfolge ein, im Beispiel aus Abbildung 3 ist dies die Folge „567“. Durch Aktivieren der „Allow Searching“-Checkbox wird die im PiCalculator-Objekt aktuell stattfindende Berechung von Pi nach dieser Teilzahlenfolge durchsucht. Wird sie gefunden, ist ihr Abstand zum Anfang in einem Beschriftungselement auszugeben. Wird die Teilzahlenfolge nicht gefunden, gibt das Steuerelement am Ende der Berechnung den Hinweis Not found im Beschriftungselement aus.

Oberfläche der PiExtraordinarySequence-Komponente.

Abbildung 3. Oberfläche der PiExtraordinarySequence-Komponente.


Auf Grund der konzeptionellen Trennung von Berechnung und Darstellung der Zahl Pi ist dieses Steuerelement nicht ganz trivial zu implementieren. Zum einen muss es an einem Kalkulator-Objekt „lauschen“, welche Fragmente bereits berechnet wurden. Dabei müssen diese lückenlos vorliegen. Denn nur dann lassen sich diese zu einer einzigen Zeichenkette aller bereits berechneten Nachkommastellen zusammenfügen, um so schließendlich nach der gesuchten Teilzeichenkette suchen zu können.

In der Gestaltung der öffentlichen Schnittstelle der Klasse PiExtraordinarySequence haben Sie freie Wahl. Bitte achten Sie aber darauf, dass – wie bei den Steuerelementen zuvor – das Datenobjekt (Klasse PiCalculator) im Hauptfensterobjekt residiert und an das PiExtraordinarySequence-Steuerelement Informationen und Aktionen über öffentliche Eigenschaften und Methoden weitergereicht werden.

Frage: Welche Indizes erhalten Sie in Ihrer Anwendung für die beiden Teilzahlenfolge „789“ und „99999“? Tipp: Die Teilzahlenfolge „567“ befindet sich an der 467.-ten Stelle nach dem Komma.

Das Zusammenspiel der drei Steuerelemente PiToolbar, PiExtraordinarySequence und PiDisplay finden Sie in Abbildung 4 vor. Natürlich benötigen Sie auch ein Objekt der PiCalculator-Klasse. Dieses legen Sie am besten im Konstruktor des Hauptfensters an. Dort können Sie gleichfalls die Ereignisse dieses Objekts mit den Eigenschaften der Visualisierungs-Objekte adäquat verschalten.

Oberfläche der Pi Calculator-Anwendung.

Abbildung 4. Oberfläche der „Pi Calculator“-Anwendung.


Bemerkung: Bei einer großen Anzahl von zu berechnenden Nachkommastellen werden Sie feststellen, dass sich die Performance der Klassenmethode NineDigitsOfPi sehr stark verschlechtert. Überprüfen Sie an Ihrer Realisierung, inwieweit der Einsatz von mehreren Threads zu einer Verbesserung des Laufzeitverhaltens Ihrer Anwendung führt.

2. Lösung

Die Grundlage dieser Aufgabe, die Klassenmethode StartingAt, finden Sie im Netz zum Beispiel unter NineDigitsOfPiAt.cs. vor. Damit können wir gleich zur Realisierung der Klasse PiCalculator gemäß ihrer Spezifikation aus Tabelle 2 in Listing 1 übergehen. Hauptaufgabe dieser Klasse ist die Implementierung des Logik-Anteils dieser Aufgabe, oder damit im Umkehrschluss ausgedrückt: Diese Klasse besitzt keinerlei Oberflächenanteile.

Natürlich muss die PiCalculator-Klasse mit ihrer Umwelt kommunizieren können, spätestens dann, wenn es um die Visualisierung gewisser Resultate geht. Zu diesem Zweck gibt es in C# das Sprachmittel der Ereignisse. Dieses gelangt in den Zeilen 28 und 29 (Definition) zum Einsatz:

001: namespace WpfPiCalculatorAdvanced
002: {
003:     public delegate void PiGotFragmentHandler(int tid, int offset, int fragment);
004:     public delegate void PiCalculationDoneHandler ();
005: 
006:     public class PiCalculator
007:     {
008:         // calculation utils
009:         private int precision;
010:         private int next;
011: 
012:         // threading utils
013:         private int numThreads;
014:         private Thread[] workers;
015:         private bool active;
016: 
017:         // c'tor
018:         public PiCalculator()
019:         {
020:             this.precision = 0;
021:             this.next = 1;
022:             this.numThreads = 1;
023:             this.active = false;
024:             this.workers = null;
025:         }
026: 
027:         // events
028:         public event PiGotFragmentHandler PiGotFragment;
029:         public event PiCalculationDoneHandler PiCalculationDone;
030: 
031:         // properties
032:         public int NumThreads
033:         {
034:             set
035:             {
036:                 this.numThreads = value;
037:             }
038:         }
039: 
040:         public int Precision
041:         {
042:             get
043:             {
044:                 return this.precision;
045:             }
046: 
047:             set
048:             {
049:                 this.precision = value;
050:             }
051:         }
052: 
053:         // public interface
054:         public bool Start()
055:         {
056:             if (this.workers != null)
057:                 return false;
058: 
059:             Task.Factory.StartNew(() => { this.CalcPiAsyncExtended (this.numThreads); });
060:             return true;
061:         }
062: 
063:         public bool Stop()
064:         {
065:             if (this.workers == null)
066:                 return false;
067: 
068:             this.active = false;
069:             return true;
070:         }
071: 
072:         private void CalcPiAsyncExtended(int numThreads)
073:         {
074:             this.next = 1;
075:             this.active = true;
076: 
077:             this.workers = new Thread[numThreads];
078:             ThreadStart ts = new ThreadStart(this.CalcPiAsyncExtendedHelper);
079: 
080:             for (int i = 0; i < this.workers.Length; i++)
081:             {
082:                 this.workers[i] = new Thread(ts);
083:                 this.workers[i].Priority = ThreadPriority.Lowest;
084:                 this.workers[i].Start();
085:             }
086: 
087:             // wait for end of workers
088:             for (int i = 0; i < this.workers.Length; i++)
089:                 this.workers[i].Join();
090: 
091:             // calculation done, fire event
092:             if (this.PiCalculationDone != null)
093:                 this.PiCalculationDone.BeginInvoke(null, null);
094: 
095:             // using thread array reference as a guard
096:             this.workers = null;
097:         }
098: 
099:         // private helper methods
100:         private void CalcPiAsyncExtendedHelper()
101:         {
102:             int max;
103:             int next;
104: 
105:             Monitor.Enter(this);
106:             {
107:                 // retrieve precision (number of digits to compute)
108:                 max = this.precision;
109: 
110:                 // retrieve next candidate
111:                 next = this.next;
112: 
113:                 // increment current candidate for next available client
114:                 this.next += 9;
115:             }
116:             Monitor.Exit(this);
117: 
118:             while (next < max)
119:             {
120:                 if (!this.active)
121:                     break;
122: 
123:                 // compute fragment
124:                 int fragment = NineDigitsOfPi.StartingAt(next);
125:                 if (this.PiGotFragment != null)
126:                 {
127:                     int tid = Thread.CurrentThread.ManagedThreadId;
128:                     this.PiGotFragment.BeginInvoke(tid, next, fragment, null, null);
129:                 }
130: 
131:                 Monitor.Enter(this);
132:                 {
133:                     // retrieve next offset
134:                     next = this.next;
135: 
136:                     // increment current offset for next available calculation client
137:                     this.next += 9;
138:                 }
139:                 Monitor.Exit(this);
140:             }
141:         }
142:     }
143: }

Beispiel 1. Realisierung der Klasse PiCalculator.


Zu beachten in Listing 1 ist: Je nach Anzahl der Threads, die sich an der Berechnung von Nachkommastellen beteiligen, wird die Methode CalcPiAsyncExtendedHelper (Zeilen 100 bis 141) im Kontext mehrerer Threads aufgerufen. Aus diesem Grund ist der Zugriff auf Instanzvariablen (hier: Variable next) vor dem parallelen Zugriff zu schützen.

Wir fahren in Listing 2 und Listing 3 mit dem Controller-Teil der Anwendung fort:

01: <UserControl
02:     x:Class="WpfPiCalculator.PiToolbarControl"
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" d:DesignHeight="80" d:DesignWidth="600">
08:     
09:     <GroupBox Header="Toolbar">
10:         <StackPanel>
11:             <DockPanel LastChildFill="True" Margin="2">
12:                 
13:                 <Button
14:                     DockPanel.Dock="Left" Width="60" Name="ButtonStart"
15:                     Click="Button_Click" Margin="2"
16:                     x:FieldModifier="private">Start</Button>
17:                 
18:                 <Label
19:                     Name="LabelActive" DockPanel.Dock="Left" Width="60"
20:                     Background="Green" Margin="2"/>
21:                 
22:                 <Button
23:                     DockPanel.Dock="Left" Width="60" Name="ButtonStop"
24:                     Click="Button_Click" Margin="2" x:FieldModifier="private">Stop</Button>
25:                                 
26:                 <ComboBox
27:                     Name="ComboBoxNumberThreads" DockPanel.Dock="Right"
28:                     Width="60" VerticalAlignment="Center"
29:                     SelectionChanged="ComboBoxNumberThreads_SelectionChanged"
30:                     x:FieldModifier="private"
31:                     Margin="2"/>
32:                 
33:                 <Label
34:                     DockPanel.Dock="Right"
35:                     VerticalAlignment="Center"
36:                     Width="60"
37:                     Margin="2"
38:                     Content="Threads:"/>
39: 
40:                 <TextBox
41:                     Name="TextBoxPrecision" DockPanel.Dock="Right" Width="40"
42:                     IsEnabled="False" Margin="4" x:FieldModifier="private">1000</TextBox>
43: 
44:                 <ScrollBar
45:                     Name="ScrollBarPrecision" Orientation="Horizontal"
46:                     Minimum="1" Maximum="10000" Value="1000"
47:                     ValueChanged="ScrollBarPrecision_ValueChanged"
48:                     Margin="2" x:FieldModifier="private"/>
49:             </DockPanel>
50:             
51:             <ProgressBar
52:                 Name="ProgressBarCalulation" Height="20" Minimum="0"
53:                 Maximum="100" Value="0" x:FieldModifier="private"
54:                 Margin="4,2,4,2" />
55:             
56:         </StackPanel>
57:     </GroupBox>
58: </UserControl>

Beispiel 2. Realisierung des Steuerelements PiToolbarControl: XAML-Direktiven.


Beachten Sie an den XAML-Direktiven in Listing 2 wie auch in den nachfolgenden XAML-Dateien: Prinzipiell sind benannte Steuerelemente (also Steuerelemente mit dem Attribut Name="...") mit dem Attribut x:FieldModifier="private" versehen, also in ihrer Klasse privat deklariert. Dies hat zunächst einmal den Sinn, dass man die Innereien des Steuerelements zu einem späteren Zeitpunkt ändern kann, ohne dass Benutzer dieses Steuerelements ihre Software ändern und neu übersetzen müssen. Wie erfolgt stattdessen der Zugriff auf Informationen, die in den privaten (internen) Steuerelementen residieren? Zu diesem Zweck sind geeignete Eigenschaften, Methoden und Ereignisse zu definieren und implementieren. Bei geschickter Definition zeichnen sie sich eben vor allem dadurch aus, dass bei Änderungen an der Oberfläche des umgebenden Steuerelements sie nicht geändert werden müssen. Die öffentliche Schnittstelle des Steuerelements bleibt auf diese Weise unverändert.

Speziell im Beispiel des PiToolbarControl-Steuerelements reden wir hier von den drei Eigenschaften Status, Precision und NumThreads und den beiden Methoden Init und PerformStep. Änderungen im Steuerelement (die zum Beispiel durch Bedienung der einzelnen, inneren Steuerelemente eintreten) werden durch Ereignisse nach außen gereicht. Im vorliegenden Beispiel ist dies das Ereignisse Action (sprich: Eine Aktion ist eingetreten und sollte von einem umgebenden Container-Objekt weiter verarbeitet werden).

001: using WpfPiCalculatorAdvanced;
002: 
003: namespace WpfPiCalculator
004: {
005:     public enum ToolbarActions { Start, Stop };
006:     public enum ToolbarStatus { Idle, Active };
007: 
008:     public delegate void ToolbarActionsEventHandler (ToolbarActions action);
009: 
010:     public partial class PiToolbarControl : UserControl
011:     {
012:         private ToolbarStatus status;
013:         private int numThreads;
014:         private int precision;
015:         private int steps;
016: 
017:         // events
018:         public event ToolbarActionsEventHandler Action;
019: 
020:         // c'tor
021:         public PiToolbarControl()
022:         {
023:             this.InitializeComponent();
024: 
025:             this.status = ToolbarStatus.Idle;
026: 
027:             this.numThreads = 1;
028:             this.steps = 0;
029: 
030:             this.ComboBoxNumberThreads.Items.Add("1");
031:             this.ComboBoxNumberThreads.Items.Add("2");
032:             this.ComboBoxNumberThreads.Items.Add("4");
033:             this.ComboBoxNumberThreads.Items.Add("8");
034:             this.ComboBoxNumberThreads.Items.Add("16");
035:             this.ComboBoxNumberThreads.SelectedIndex = 0;
036: 
037:             this.ScrollBarPrecision.Value = 1000;
038:         }
039: 
040:         // public properties
041:         public ToolbarStatus Status
042:         {
043:             set
044:             {
045:                 if (this.Dispatcher.CheckAccess())
046:                 {
047:                     this.status = value;
048:                     this.LabelActive.Background =
049:                         (this.status == ToolbarStatus.Idle) ? Brushes.Green : Brushes.Red; ;
050:                 }
051:                 else
052:                     this.Dispatcher.BeginInvoke((Action)(() => { this.Status = value; }));
053:             }
054: 
055:             get
056:             {
057:                 return this.status;
058:             }
059:         }
060: 
061:         public int Precision
062:         {
063:             get
064:             {
065:                 return this.precision;
066:             }
067:         }
068: 
069:         public int NumThreads
070:         {
071:             get
072:             {
073:                 return this.numThreads;
074:             }
075:         }
076: 
077:         // public interface
078:         public void Init()
079:         {
080:             this.steps = 0;
081:             this.ProgressBarCalulation.Value = 0.0;
082:         }
083: 
084:         public void PerformStep()
085:         {
086:             this.steps ++;
087: 
088:             double percentage = ((9 * this.steps) / (double) this.precision) * 100.0;
089: 
090:             if (this.Dispatcher.CheckAccess())
091:                 this.ProgressBarCalulation.Value = percentage;
092:             else
093:                 this.Dispatcher.BeginInvoke (
094:                     (Action)(() => { this.ProgressBarCalulation.Value = percentage; })
095:                 );
096:         }
097: 
098:         // private event handler
099:         private void Button_Click(Object sender, RoutedEventArgs e)
100:         {
101:             if (this.Action != null)
102:             {
103:                 if (sender == this.ButtonStart)
104:                 {
105:                     this.Action(ToolbarActions.Start);
106:                 }
107:                 else if (sender == this.ButtonStop)
108:                 {
109:                     this.Action(ToolbarActions.Stop);
110:                 }
111:             }
112:         }
113: 
114:         private void ScrollBarPrecision_ValueChanged (Object sender, RoutedPropertyChangedEventArgs<double> e)
115:         {
116:             this.precision = (int) this.ScrollBarPrecision.Value;
117:             this.TextBoxPrecision.Text = precision.ToString();
118:         }
119: 
120:         private void ComboBoxNumberThreads_SelectionChanged (Object sender, SelectionChangedEventArgs e)
121:         {
122:             switch (this.ComboBoxNumberThreads.SelectedIndex)
123:             {
124:                 case 0:
125:                     this.numThreads = 1;
126:                     break;
127:                 case 1:
128:                     this.numThreads = 2;
129:                     break;
130:                 case 2:
131:                     this.numThreads = 4;
132:                     break;
133:                 case 3:
134:                     this.numThreads = 8;
135:                     break;
136:                 case 4:
137:                     this.numThreads = 16;
138:                     break;
139:                 default:
140:                     this.numThreads = 1;
141:                     break;
142:             }
143:         }
144:     }
145: }

Beispiel 3. Realisierung des Steuerelements PiToolbarControl: Codebehind-Anweisungen.


Es folgt in Listing 4 und Listing 5 der View-Teil der Anwendung:

01: <UserControl
02:     x:Class="WpfPiCalculator.PiDisplayControl"
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="300" d:DesignWidth="500">
09:     
10:     <GroupBox Header="Display" >
11:         <DockPanel LastChildFill="True" >
12:             <StackPanel DockPanel.Dock="Top" Margin="2">
13:                 <Button
14:                     Name="ButtonClear" Width="100" VerticalAlignment="Center"
15:                     Margin="2" Content="Clear" Click="Button_Click"
16:                     x:FieldModifier="private"/>
17:             </StackPanel>
18:             <TextBox
19:                 Name="TextBoxPiDisplay"
20:                 AcceptsReturn="True" 
21:                 TextWrapping="Wrap"
22:                 VerticalScrollBarVisibility="Visible"
23:                 FontFamily="Consolas" FontSize="14"
24:                 Background="AliceBlue"
25:                 Margin="4,0,2,4"
26:                 x:FieldModifier="private"/>
27:         </DockPanel>
28:     </GroupBox>
29: </UserControl>

Beispiel 4. Realisierung des Steuerelements PiDisplayControl: XAML-Direktiven.


01: namespace WpfPiCalculator
02: {
03:     internal delegate void UpdateTextBoxFragmenHandler(int offset, int fragment);
04: 
05:     public partial class PiDisplayControl : UserControl
06:     {
07:         private int precision;
08: 
09:         // c'tor
10:         public PiDisplayControl()
11:         {
12:             this.InitializeComponent();
13:             this.precision = 100;
14:         }
15: 
16:         // properties
17:         public int Precision
18:         {
19:             set
20:             {
21:                 this.precision = value;
22:             }
23:         }
24: 
25:         // public interface
26:         public void Clear()
27:         {
28:             this.TextBoxPiDisplay.Text = "";
29:         }
30: 
31:         public void Init()
32:         {
33:             this.TextBoxPiDisplay.Text = "3.";
34:             for (int i = 0; i < this.precision; i++)
35:                 this.TextBoxPiDisplay.AppendText("_");
36:         }
37: 
38:         public void SetFragmentAt(int offset, int fragment)
39:         {
40:             if (this.Dispatcher.CheckAccess())
41:             {
42:                 String s = this.TextBoxPiDisplay.Text;
43:                 String head = s.Substring(0, 1 + offset);
44:                 String tmp = String.Format("{0:D9}", fragment);
45: 
46:                 String tail = "";
47:                 if (2 + offset + 9 <= s.Length)
48:                     tail = s.Substring(1 + offset + 9);
49: 
50:                 this.TextBoxPiDisplay.Text = head + tmp + tail;
51:             }
52:             else
53:             {
54:                 UpdateTextBoxFragmenHandler d =
55:                     new UpdateTextBoxFragmenHandler(this.SetFragmentAt);
56:                 this.Dispatcher.BeginInvoke(d, offset, fragment);
57:             }
58:         }
59: 
60:         // event handler
61:         private void Button_Click(Object sender, RoutedEventArgs e)
62:         {
63:             if (sender == ButtonClear)
64:             {
65:                 this.Clear();
66:             }
67:         }
68:     }
69: }

Beispiel 5. Realisierung des Steuerelements PiDisplayControl: Codebehind-Anweisungen.


Beim PiDisplayControl-Steuerelement ist zu erwähnen, dass das TextBox-Steuerelement sich nicht gerade durch einen großen Komfort bzgl. seiner Funktionalität auszeichnet. Das „Einpflanzen“ einer Teilzeichenkette in der Mitte eines vorhandenen Textes ist nur mit eher brachialen Zugriffsfunktionen möglich (vorhanden Text komplett wegschmeißen, neuen Text zusammenbauen und dem TextBox-Steuerelement zuweisen). Die Methode SetFragmentAt (Zeilen 38 bis 58 von Listing 5) ist bzgl. ihrer Realisierung daher etwas schlicht ausgefallen.

In Listing 6 und Listing 7 können wir von einem kombinierten View- und Controller-Teil der Anwendung sprechen:

01: <UserControl x:Class="WpfPiCalculator.PiExtraordinarySequenceSearcherControl"
02:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
05:              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
06:              mc:Ignorable="d" d:DesignWidth="682">
07:     <GroupBox Header="Extraordinary Sequence Searcher" >
08:         <DockPanel LastChildFill="True" Margin="2">
09:             <CheckBox
10:                 Name="CheckBoxSearchingActive" DockPanel.Dock="Left"
11:                 VerticalAlignment="Center" Content="Allow Searching" IsChecked="False"
12:                 Checked="CheckBoxSearchingActive_Checked"
13:                 Unchecked="CheckBoxSearchingActive_Checked" Margin="2"
14:                 x:FieldModifier="private"/>
15:             
16:             <TextBox
17:                 DockPanel.Dock="Left" IsEnabled="False"
18:                 Margin="2">Enter Search String</TextBox>
19:             
20:             <TextBox
21:                 Name="TextBoxSearchString" Width="100"
22:                 DockPanel.Dock="Left" IsEnabled="True"
23:                 TextChanged="TextBoxSearchString_TextChanged"
24:                 Margin="2" x:FieldModifier="private"/>
25:             
26:             <TextBox
27:                 DockPanel.Dock="Left" IsEnabled="False"
28:                 Margin="2">Found:</TextBox>
29:             
30:             <TextBox
31:                 Name="TextBoxSearcherResult" DockPanel.Dock="Left"
32:                 IsEnabled="False" Margin="2"
33:                 x:FieldModifier="private"/>
34:         </DockPanel>
35:     </GroupBox>
36: </UserControl>

Beispiel 6. Realisierung des Steuerelements PiExtraordinarySequenceSearcherControl: XAML-Direktiven.


001: namespace WpfPiCalculator
002: {
003:     public partial class PiExtraordinarySequenceSearcherControl : UserControl
004:     {
005:         private bool enabled;
006:         private String searchString;
007:         private List<String> fragments;
008: 
009:         // c'tor
010:         public PiExtraordinarySequenceSearcherControl()
011:         {
012:             this.InitializeComponent();
013:             this.searchString = "";
014:             this.fragments = new List<String>();
015:         }
016: 
017:         // properties
018:         public bool IsActive
019:         {
020:             get
021:             {
022:                 return this.enabled;
023:             }
024:         }
025: 
026:         public int Precision
027:         {
028:             set
029:             {
030:                 // calculate number of fragments
031:                 int capacity = value / 9 + 1;
032: 
033:                 this.fragments.Clear();
034:                 this.fragments = new List<String>(capacity);
035: 
036:                 // setup list of empty fragments
037:                 for (int i = 0; i < capacity; i++)
038:                     this.fragments.Add(null);
039:             }
040:         }
041: 
042:         // private event handler
043:         private void CheckBoxSearchingActive_Checked(Object sender, RoutedEventArgs e)
044:         {
045:             this.enabled = (bool) this.CheckBoxSearchingActive.IsChecked;
046:             Console.WriteLine("Checked: {0}", this.enabled);
047: 
048:             this.TextBoxSearchString.Text = "";
049:             this.TextBoxSearcherResult.Text = "";
050:         }
051: 
052:         private void TextBoxSearchString_TextChanged(Object sender, TextChangedEventArgs e)
053:         {
054:             this.searchString = this.TextBoxSearchString.Text;
055:             this.TextBoxSearcherResult.Text = "";
056:         }
057: 
058:         // public interface
059:         public void NextFragment (int offset, int fragment)
060:         {
061:             if (this.searchString == "")
062:                 return;
063: 
064:             int index = (offset - 1) / 9;
065:             StringBuilder pi = new StringBuilder("");
066: 
067:             // update internal list of fragments
068:             // (read-write access must be thread-safe !!!)
069:             Monitor.Enter(this);
070:             this.fragments[index] = String.Format("{0:D9}", fragment);
071: 
072:             // compute so far calculated representation of pi
073:             int tmp = 0;
074:             while (this.fragments[tmp] != null)
075:                 tmp++;
076: 
077:             for (int i = 0; i < tmp; i++)
078:                 pi.Append(this.fragments[i]);
079:             Monitor.Exit(this);
080: 
081:             // search substring
082:             int pos = pi.ToString().IndexOf(this.searchString);
083:             if (pos >= 0)
084:             {
085:                 if (this.Dispatcher.CheckAccess())
086:                     this.TextBoxSearcherResult.Text =
087:                         "Result: " + (pos + 1).ToString();
088:                 else
089:                 {
090:                     this.Dispatcher.BeginInvoke (
091:                         (Action)(() =>
092:                         {
093:                             this.TextBoxSearcherResult.Text =
094:                                 "Result: " + (pos + 1).ToString();
095:                         })
096:                     );
097:                 }
098:             }
099:         }
100:     }
101: }

Beispiel 7. Realisierung des Steuerelements PiExtraordinarySequenceSearcherControl: Codebehind-Anweisungen.


Die Suche nach einer Teilziffernfolge in den Nachkommastellen mag für die Wissenschaftler eine interessante Fragestellung sein. Auf Basis der vorliegenden Aufgabe und im Speziellen beim Einsatz der Klassenmethode StartingAt kommt sie etwas ungelegen. Trotzdem – oder vielleicht gerade deshalb – wollte ich diese Fragestellung in der Aufgabe belassen. Herausragendes Merkmal der StartingAt-Methode ist ja gerade ihre Eigenschaft, 9 Ziffern an einer beliebigen Nachkommastelle berechnen zu können. Dies heißt anders formuliert: Bis zum bisherigen Verlauf der Aufgabe gab es keinerlei Veranlassung, alle Nachkommastellen von Pi an einem Stück aufzuheben. Das PiDisplayControl-Steuerelement war so konzipiert, dass es – in Abhängigkeit von der Berechnungsreihenfolge – bestimmte Neuner-Gruppen von Ziffern anzeigen kann, aber diese nicht am Stück in der richtigen Reihenfolge vorliegen müssen. Der parallele Ansatz verfolgte ja gerade die Strategie, dass die Neuner-Gruppen in unabhängiger Reihenfolge berechnet werden.

Bei der Suche nach einer Teilziffernfolge in den Nachkommastellen kommen wir nun nicht umhin, alle vorliegenden Fragmente zu einer lückenlosen Folge von Nachkommaziffern zusammenzufügen (soweit die Informationen hierzu vorab berechnet wurden). Aus diesem Grund muss sich das PiExtraordinarySequenceSearcherControl (indirekt über das Hauptfenster) am PiGotFragment-Ereignis des Kalkulators anmelden, um mit dessen Hilfe eine zusammenhängende Zeichenkette von Nachkommaziffern zu ermitteln.

Mit diesen Vorbereitungen ist die Aufgabenstellung nun recht einfach geworden. Die IndexOf-Methode aus der Klasse String begibt sich auf die Suche nach der gewünschten Teilziffernfolge; ein Rückgabewert größer-gleich Null verkündet die Position des ersten Funds. Ein negativer Wert gibt an, dass die Teilziffernfolge nicht vorliegt.

01: <Window
02:     x:Class="WpfPiCalculator.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:WpfPiCalculator"
06:     Title="Pi Calculator" Height="500" Width="725">
07:     
08:     <DockPanel LastChildFill="True">
09:         <local:PiToolbarControl
10:             x:Name="PiToolbar" x:FieldModifier="private"
11:             DockPanel.Dock="Top"/>
12:         
13:         <local:PiExtraordinarySequenceSearcherControl
14:             x:Name="PiExtraordinarySequenceSearcher"
15:             x:FieldModifier="private"
16:             DockPanel.Dock="Bottom"/>
17:             
18:         <local:PiDisplayControl
19:             x:Name="PiDisplay" x:FieldModifier="private"/>
20:     </DockPanel>
21: 
22: </Window>

Beispiel 8. Realisierung des Hauptfensters MainWindow: XAML-Direktiven.


01: using WpfPiCalculatorAdvanced;
02: 
03: namespace WpfPiCalculator
04: {
05:     public partial class MainWindow : Window
06:     {
07:         private PiCalculator calculator;
08: 
09:         public MainWindow()
10:         {
11:             this.InitializeComponent();
12: 
13:             // create (single) calculator
14:             this.calculator = new PiCalculator();
15: 
16:             // wiring to events of calculator and toolbar
17:             this.calculator.PiGotFragment +=
18:                 new PiGotFragmentHandler (this.PiCalculator_PiGotFragment);
19:             this.calculator.PiCalculationDone +=
20:                 new PiCalculationDoneHandler (this.PiCalculator_PiCalculationDone);
21:             this.PiToolbar.Action +=
22:                 new ToolbarActionsEventHandler (this.PiToolbar_Action);
23:         }
24: 
25:         // event handler methods
26:         private void PiToolbar_Action(ToolbarActions action)
27:         {
28:             if (action == ToolbarActions.Start)
29:             {
30:                 if (this.PiToolbar.Status == ToolbarStatus.Active)
31:                     return;
32: 
33:                 this.PiToolbar.Status = ToolbarStatus.Active;
34:                 this.PiToolbar.Init();
35: 
36:                 this.PiDisplay.Precision = this.PiToolbar.Precision;
37:                 this.PiDisplay.Init();
38: 
39:                 this.PiExtraordinarySequenceSearcher.Precision = this.PiToolbar.Precision;
40: 
41:                 this.calculator.Precision = this.PiToolbar.Precision;
42:                 this.calculator.NumThreads = this.PiToolbar.NumThreads;
43:                 this.calculator.Start();
44:             }
45:             else if (action == ToolbarActions.Stop)
46:             {
47:                 this.calculator.Stop();
48:             }
49:         }
50: 
51:         private void PiCalculator_PiGotFragment(int tid, int offset, int fragment)
52:         {
53:             this.PiToolbar.PerformStep();
54: 
55:             this.PiDisplay.SetFragmentAt(offset, fragment);
56: 
57:             if (this.PiExtraordinarySequenceSearcher.IsActive)
58:                 this.PiExtraordinarySequenceSearcher.NextFragment(offset, fragment); 
59:         }
60: 
61:         private void PiCalculator_PiCalculationDone()
62:         {
63:             this.PiToolbar.Status = ToolbarStatus.Idle;
64:         }
65:     }
66: }

Beispiel 9. Realisierung des Hauptfensters MainWindow: Codebehind-Anweisungen.


Beim Hauptfenster gibt es im Wesentlichen zwei Aspekte zu beachten. Auf Grund der Modularisierung der Oberfläche der Aufgabenstellung in Teil-Komponenten ist der XAML-Anteil des Hauptfensters sehr kurz und übersichtlich geraten. Offensichtlich erfüllen die Teil-Komponenten ihren Pflichtanteil, eigenständige und kapselbare Teilaufgaben vollständig zu übernehmen.

Zur Kommunikation zwischen den Teil-Komponenten bedarf es des Auswertens von eingetretenen Ereignissen. Dazu sind – im Konstruktor des Hauptfensters – an allen notwendigen Ereignissen Methoden angemeldet. Im Rumpf dieser Methoden werden Informationen an andere Komponenten weitergeleitet – sei es durch das Setzen bestimmter Eigenschaften oder durch den Aufruf von Methoden.

Beachten Sie bitte auch: Sieht man einmal von den (privaten) Ereignis-Handlermethoden im Hauptfenster ab, dann werden Sie im Codebehind-Anteil keine weiteren Anweisungen vorfinden! Diese sind – wie schon bei der Betrachtung der Oberflächen – in den jeweiligen Komponenten vor Ort abgelegt. Auf diese Weise sind Änderungen und Erweiterungen einer komplexen Anwendung auf eine einfache Art und Weise möglich: Dezentral (und damit gekapselt) an isolierten Stellen der einzelnen Teil-Komponenten.