Uwe Klappert
Coding For Fun mit C#
Liebe Leserin, lieber Leser, ich freue mich, dass Sie sich für dieses Coding for Fun-Buch entschieden haben. Sie sind Programmierer aus Berufung und Ihnen macht es Spaß, Aufgaben programmierend zu lösen? Dann sind Sie hier genau richtig! Dieses Buch bietet Ihnen sechs ungewöhnliche Programmierprojekte an, die Sie (Programmiererfahrung vorausgesetzt) ganz einfach nachvollziehen können. Die Projekte haben nur das eine Ziel: Sie zu unterhalten und zum Knobeln und Ausprobieren anzuregen. Wir versprechen Ihnen keine schlanken Lösungen, keine Snippets, die sie beruflich verwenden könnten, sondern reinen Programmierspaß. Dabei sind die Projekte so unterschiedlich und abwechslungsreich wie Aprilwetter: Eine Poker-Runde gefällig? Abendstimmung mit Sternenhimmel oder lieber Wecker-Tool? Oder wollten Sie schon immer den DAX manipulieren? Legen Sie einfach los. Die aktuelle und für die Beispiele benötigte Visual Studio Express Edition 2010 und der Beispielcode liegen auf der Buch-DVD für Sie bereit. Noch eine Anmerkung in eigener Sache: Dieses Buch wurde mit großer Sorgfalt geschrieben, begutachtet, lektoriert und produziert. Doch kein Buch ist perfekt. Sollte also etwas nicht so funktionieren, wie Sie es erwarten, dann scheuen Sie sich nicht, sich mit mir in Kontakt zu setzen. Ihre freundlichen Fragen und Anmerkungen sind jederzeit willkommen. Viel Freude beim Lesen und Programmieren wünscht
Judith Stevens-Lemoine Lektorat Galileo Computing
[email protected] www.galileocomputing.de Galileo Press · Rheinwerkallee 4 · 53227 Bonn
Auf einen Blick 1
Einleitung ............................................................................
9
2
Die Sache mit .NET .............................................................
13
3
Ausgeschlafen – das Wecker-Tool ......................................
41
4
Alles Täuschung oder was? Herr Hermann und sein Gitter ............................................
69
5
Mit Argusaugen – der nächtliche Sternenhimmel ..............
101
6
Garantiert ungefährlich – Manipulationen am DAX ...........
141
7
Im Labyrinth des Minotaurus .............................................
187
8
Pokern .................................................................................
235
A
Visual C# 2010 Express .......................................................
327
Der Name Galileo Press geht auf den italienischen Mathematiker und Philosophen Galileo Galilei (1564–1642) zurück. Er gilt als Gründungsfigur der neuzeitlichen Wissenschaft und wurde berühmt als Verfechter des modernen, heliozentrischen Weltbilds. Legendär ist sein Ausspruch Eppur se muove (Und sie bewegt sich doch). Das Emblem von Galileo Press ist der Jupiter, umkreist von den vier Galileischen Monden. Galilei entdeckte die nach ihm benannten Monde 1610. Gerne stehen wir Ihnen mit Rat und Tat zur Seite:
[email protected] bei Fragen und Anmerkungen zum Inhalt des Buches
[email protected] für versandkostenfreie Bestellungen und Reklamationen
[email protected] für Rezensions- und Schulungsexemplare Lektorat Judith Stevens-Lemoine Fachgutachten Thomas Theis, Monschau Korrektorat Friederike Daenecke, Zülpich Cover Barbara Thoben, Köln Coverillustration Graham Geary, Boston Typografie und Layout Vera Brauner Herstellung Frauke Kaiser Satz Typographie & Computer, Krefeld Druck und Bindung Bercker Graphischer Betrieb, Kevelaer Dieses Buch wurde gesetzt aus der Linotype Syntax Serif (9,25/13,25 pt) in FrameMaker. Gedruckt wurde es auf chlorfrei gebleichtem Offsetpapier.
Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. ISBN
978-3-8362-1484-1
© Galileo Press, Bonn 2010 1. Auflage 2010 Das vorliegende Werk ist in all seinen Teilen urheberrechtlich geschützt. Alle Rechte vorbehalten, insbesondere das Recht der Übersetzung, des Vortrags, der Reproduktion, der Vervielfältigung auf fotomechanischem oder anderen Wegen und der Speicherung in elektronischen Medien. Ungeachtet der Sorgfalt, die auf die Erstellung von Text, Abbildungen und Programmen verwendet wurde, können weder Verlag noch Autor, Herausgeber oder Übersetzer für mögliche Fehler und deren Folgen eine juristische Verantwortung oder irgendeine Haftung übernehmen. Die in diesem Werk wiedergegebenen Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. können auch ohne besondere Kennzeichnung Marken sein und als solche den gesetzlichen Bestimmungen unterliegen.
Inhalt 1
Einleitung .......................................................................................
9
1.1 1.2 1.3
Grundlegendes zu diesem Buch .......................................................... Abseits von Klassen und Methoden ................................................... Eine notwendige Frage: Die Zielgruppe? ............................................
10 10 11
2
Die Sache mit .NET .......................................................................
13
2.1
2.9
Intelligent, aber nicht unergründlich – das .NET-Framework .............. 2.1.1 Sprachgewaltig: die Laufzeitumgebung .................................. 2.1.2 Ein Meer von Klassen – die Base Class Library (BCL) .............. Zwischengeschoben – der IL-Code ..................................................... 2.2.1 Kein Fabelwesen – der Jitter .................................................. Ein Raum ohne Tür – der Namensraum .............................................. Ein Wort wie ein Kosename – Assembly ............................................. Die »digitale Müllabfuhr« – der Garbage Collector .............................. Das Salz in der Suppe – Steuerelemente ............................................. .NET und kein Ende? .......................................................................... Aus der Art geschlagen – die Sprache C# ............................................ 2.8.1 Musik im Namen ................................................................... 2.8.2 Ein Kessel Buntes – die Ursprünge der .NET-Sprache C# ........ 2.8.3 Ein heikler Punkt – Pointer .................................................... 2.8.4 Ein geheimnisvoller Verein – Delegates .................................. Ereignisbehandlung ............................................................................
14 14 15 17 18 20 21 24 27 29 29 30 31 31 35 39
3
Ausgeschlafen – das Wecker-Tool ............................................
41
3.1 3.2
Als der Computer die Zeit entdeckte .................................................. Die Entwicklung der Bedienoberfläche ............................................... 3.2.1 Anlegen eines neuen Projekts ................................................ 3.2.2 Die benötigten Steuerelemente ............................................. 3.2.3 Ein EventHandler für das Beenden der Anwendung ... ........... 3.2.4 Implementierung und Test der Zeitanzeige ............................ 3.2.5 Von der Zeitanzeige zur Implementierung einer Uhr .............. 3.2.6 Zahlen für das »NumericUpDown«-Control ............................ 3.2.7 Mehr Lärm als Melodie – »tada« und »ir« ............................... 3.2.8 In einem Aufwasch – Einstellen der Weckzeit und Auslösen des Wecktons ......................................................... 3.2.9 Stop and Go – das Beenden des Wecktons ............................ Hätten Sie’s gewusst? .........................................................................
42 45 47 50 52 53 55 57 58
2.2 2.3 2.4 2.5 2.6 2.7 2.8
3.3
60 64 68 5
Inhalt
4 4.1
Alles Täuschung oder was? Herr Hermann und sein Gitter ...................................................
69
4.4
Die Entdeckung der geheimnisvollen Punkte ...................................... 4.1.1 Kaffeestunde beim Optiker – woher die Punkte kommen ...... Entwicklung der Benutzeroberfläche .................................................. 4.2.1 Was beabsichtigt ist .............................................................. 4.2.2 Die beteiligten Controls ......................................................... Entwicklung der Programmierlogik .................................................... 4.3.1 Viel Aufwand für den Hintergrund ......................................... 4.3.2 Zeichnung und Positionierung der Gitterquadrate .................. 4.3.3 Der Schalter für das Hermann-Gitter ...................................... 4.3.4 Ganz schön blass geworden – die Regelung des Alpha-Werts .......................................................................... 4.3.5 Aus groß mach klein – Skalierung der Quadrate durch ein TrackBar-Steuerelement ........................................................ 4.3.6 Die Verhältnisse auf den Kopf gestellt – Umkehrung der Farben ................................................................................... Hätten Sie’s gewusst? .........................................................................
5
Mit Argusaugen – der nächtliche Sternenhimmel ................. 101
5.1 5.2
5.5
Wie alles begann – Stippvisite in Padua .............................................. Subtil und verspielt – wo wir hin wollen ............................................. 5.2.1 Weitere Anforderungen an die Anwendung ........................... Entwicklung der Benutzeroberfläche .................................................. Entwicklung der Programmierlogik ..................................................... 5.4.1 Ein Klasse mit Ambitionen – Creating .................................... 5.4.2 Zurück zur Klasse »Form1« .................................................... 5.4.3 Sterne der 1. Kategorie .......................................................... 5.4.4 Sterne der 2. Kategorie .......................................................... 5.4.5 Zu guter Letzt – ein Stern der 3. Kategorie ............................. 5.4.6 Verzögertes Schließen der Anwendung .................................. 5.4.7 Aufgehoben ist nicht aufgeschoben – die Klasse »DelayTime« .......................................................................... Hätten Sie’s gewusst? .........................................................................
6
Garantiert ungefährlich – Manipulationen am DAX .............. 141
6.1
Im Schmelztiegel des großen Geldes .................................................. 141 6.1.1 Xetra – das elektronische Hirn der Börse ................................ 143 6.1.2 Am Puls der Wirtschaft – der DAX ......................................... 143
4.2
4.3
5.3 5.4
6
69 71 72 72 73 76 77 79 82 83 86 94 99
101 103 105 105 108 111 118 119 122 129 134 134 140
Inhalt
6.2 6.3
6.5
Was angedacht ist .............................................................................. Entwicklung der Benutzeroberflächen ................................................ 6.3.1 Das Fenster »Data« ................................................................ 6.3.2 Das Fenster »DAX« ................................................................ Entwicklung der Programmierlogik ..................................................... 6.4.1 Das große Zeichnen – die Klasse »Chart« ............................... Hätten Sie’s gewusst? .........................................................................
7
Im Labyrinth des Minotaurus ..................................................... 187
7.1
7.5
Und dann kam Dijkstra ... .................................................................. 7.1.1 Ein Quäntchen Graphentheorie ............................................. 7.1.2 Dijkstra in Worten ................................................................. 7.1.3 Listenplätze ........................................................................... »Verworrene« Absichten .................................................................... 7.2.1 »Schwaches Knotenkriterium« ............................................... 7.2.2 Nullsummenspiel ................................................................... 7.2.3 Wie es weiter geht ................................................................ Entwicklung der Benutzeroberfläche .................................................. 7.3.1 Ein Fall für sich – das »TableLayoutPanel« .............................. 7.3.2 Labels am laufenden Band ..................................................... 7.3.3 Drei Buttons und ein Textfeld ................................................ Entwicklung der Programmierlogik ..................................................... 7.4.1 Gute Eigenschaften ................................................................ 7.4.2 Definition der Knoten im EventHandler »button1_Click()« .................................................................. 7.4.3 Ausblenden der Knoten im EventHandler »button2_Click()« .................................................................. 7.4.4 Die Schaltfläche zum kürzesten Weg ..................................... Hätten Sie’s gewusst? .........................................................................
8
Pokern ............................................................................................. 235
8.1
Die Hand am Colt – Five Card Draw ................................................... 8.1.1 Die Regeln beim Five Card Draw ........................................... 8.1.2 Gewichtete Hände ................................................................. Draw Poker Light – unser Spiel ........................................................... 8.2.1 Die Wahl des Gegners ........................................................... 8.2.2 Erzwungener Tausch .............................................................. 8.2.3 Die Frage des Geldes ............................................................. Entwicklung der Benutzeroberfläche .................................................. 8.3.1 Vom Ordner zur Bedienoberfläche .........................................
6.4
7.2
7.3
7.4
8.2
8.3
144 146 146 150 151 159 184
188 189 191 192 192 194 195 195 196 196 198 200 201 202 206 209 210 234
236 237 238 240 240 241 241 241 242
7
Inhalt
8.4
8.5 8.6
Entwicklung der Programmierlogik ..................................................... 8.4.1 Addition und Subtraktion – der Ereignisbehandler »numericUpDown1_ValueChanged()« .................................... 8.4.2 PictureBoxen aufgelistet – die Methode »pictureboxList()« .... 8.4.3 Einsatz des Kartengebers ....................................................... 8.4.4 Basisarbeit – die Klasse »kartenListe« ..................................... 8.4.5 Zurück in der Klassendatei »Poker.js« .................................... 8.4.6 Tauschgeschäfte 1 – EventHandling für fünf PictureBoxen ...... 8.4.7 Tauschgeschäfte 2 – die Klasse »tauscheKarten« .................... 8.4.8 Tauschgeschäfte 3 – das finale Ereignis .................................. 8.4.9 Reduzierte Menge – die Klasse »kartenKombinationen« ........ 8.4.10 Wie viel auf dem Spiel steht – die Struktur »wertigkeitHand« .................................................................. 8.4.11 Schnelles Plätzetauschen – die Klasse »permutKarten« ........... 8.4.12 Gewonnen oder verloren – die Klasse »evaluiereHand« .......... 8.4.13 Letzte Schritte im EventHandler »tauschen_Click()« ............... 8.4.14 Die »Konditionierung« des Spiels ........................................... 8.4.15 Mit »this« und »Close()« zum geschlossenen Fenster .............. Hätten Sie‘s gewusst? ......................................................................... Zum guten Schluss .............................................................................
A
Visual C# 2010 Express ............................................................... 327
A.1
Anwendungen, die mit Visual C# 2010 Express erstellt werden können .................................................................................. Reduzierter Funktionsumfang ............................................................. Neues bei Visual C# 2010 Express ...................................................... Der Weg zu Visual C# 2010 Express ................................................... A.4.1 Was ist ein ISO-Image? .......................................................... A.4.2 Brennen einer Visual C# 2010 Express-CD ............................. A.4.3 Die Alternative – virtuelle Festplatte ...................................... Installation von Visual C# 2010 Express ............................................. Das Prinzip der integrierten Entwicklungsumgebung .......................... A.6.1 Projekte mit System ............................................................... A.6.2 Quell- versus Entwurfsmodus ................................................ A.6.3 Unverzichtbar – das »Eigenschaften«-Fenster ......................... Veröffentlichung einer Anwendung ....................................................
A.2 A.3 A.4
A.5 A.6
A.7
253 254 256 256 259 270 274 276 283 286 289 294 307 315 322 324 324 325
327 328 329 330 331 331 334 335 336 337 338 338 339
Index ............................................................................................................ 343
8
Wenn Baumeister Gebäude bauten, so wie Programmierer Programme schreiben, dann würde der erste Specht, der vorbeikommt, die Zivilisation zerstören. (Verfasser unbekannt)
1
Einleitung
»Coding for Fun mit C#« kann Sie, liebe Leserinnen und Leser, leider nicht in die Situation versetzen, ein Programm zu entwickeln, das New Yorks riesige Ampelschar koordiniert. Auch werden Sie am Ende der Lektüre nicht befähigt sein, Klassen, Methoden und Eigenschaften zu entwickeln, die im sinnvollen Zusammenspiel das erste Vorhandensein Ihres Geburtsjahrs im Nachkommabereich der unendlichen, weil irrationalen Zahl Pi ermitteln. Und auch die immerhin 500 000 Zeilen lange, jedoch keineswegs fehlerfreie Flugsoftware des betagten Space Shuttle (die in HAL, einer FORTRAN-ähnlichen Sprache implementiert ist) werden Sie nach dem Durcharbeiten der Lektüre nicht so ohne Weiteres auf C# portieren können. Das alles ist auch nicht unbedingt Ihre Erwartung? Gut so, denn das angenehm Knifflige eines programmiertechnischen Problems wächst nicht zwangsläufig mit der Dimension der definierten Aufgabe. Natürlich verfällt dieses Buch auch nicht ins Gegenteil. Aus dem zerstörerischen Specht wird nicht klammheimlich eine Biene werden. Die Mauern der Zivilisation stürzen nicht ein, weil Sie womöglich genötigt werden, wild darauflos zu programmieren. Ihr Wohlergehen ist demnach nicht gefährdet. Und vielleicht, ja vielleicht empfinden Sie nach sorgfältigem Durcharbeiten eines, mehrerer oder gleich aller Beispiele erst recht ein gewisses Unverständnis gegenüber dem sicherlich übertriebenen Zitat zu Beginn dieses Vorworts. Der kleine Anleser dürfte Sie darüber hinaus eher an die Zeiten bester »Spaghetti-Codierung« erinnern, als das Objekt der gesamte Quellcode und alles irgendwie eine – zumindest aus heutiger Sicht – schmunzelwürdige Klasse für sich war. Bei uns wird es beträchtlich strukturierter zugehen, was sich allein schon aus der Sprache ergibt, die zum Einsatz kommt: C#.
9
1
Einleitung
1.1
Grundlegendes zu diesem Buch
Praxisorientierte IT-Literatur folgt im Wesentlichen zwei Richtungen. Die eine mündet im Entwicklungsprozess eines ausführlichen Praxisbeispiels, die andere in mehreren, thematisch voneinander abgegrenzten Programmen. Beide Ansätze haben sowohl ihre didaktische Berechtigung als auch nicht zu unterschätzende Vorteile. »Coding for Fun mit C#« verwirklicht das zweite Konzept. Inhaltlich wohlunterscheidbare, vollkommen unabhängige Praxisbeispiele geben Ihnen die Möglichkeit, sich entweder erfolgreich auf gleich mehreren oder auf ausgesuchten Feldern zu versuchen. Terra incognita bleibt da nichts, wenn Sie das nicht wollen. Ganz gleich, ob Sie gern ein Wecker-Tool entwickeln, das angenäherte Abbild des abendlichen Sternenhimmels implementieren oder sich auf dem Bildschirm Punkte ansehen möchten, die Ihnen das Auge (selbst das fehlerfreie) vorgaukelt; diesbezüglich und darüber hinaus bleibt Ihre Suche nach Interessantem nicht erfolglos. Und schließlich: Ein wenig Fantasie, gepaart mit der Fähigkeit zur Extrapolation, vorausgesetzt, kann jedes der Praxisbeispiele auf eigene Szenarien übertragen werden. Sie müssen keine DAX-Kurve mögen. Irgendetwas wird sich aber sicher finden, das Sie gerne als Chart dargestellt sähen. Vielleicht können Sie sogar auf diese Weise darstellen, wie oft Sie bei jedem Kapitel gedacht haben: »Das hätte ich aber anders gemacht.« Es stimmt ja auch! Doch bitte: Viele Wege führen nach Rom – auch Programmierwege. Ferner wurden die Beispiele mit dem Anspruch gewählt, dass die Codierung möglichst einfach nachvollziehbar sein soll. Dass dieser Anspruch zuweilen auf Kosten dessen geht, was man als Programmierästhetik bezeichnen könnte, räume ich an dieser Stelle (mit der Bitte um Nachsicht) ein. In dem Zusammenhang erinnere ich mich jedoch gerne an die Bemerkung eines Kommilitonen, nach der nicht für jede Addition eine eigene Klasse entwickelt werden muss. Er hatte recht, obgleich ich ihm damals nicht so ohne Weiteres beipflichten mochte.
1.2
Abseits von Klassen und Methoden
Ihr literarischer Neuerwerb ist kein Fachbuch. Wenigstens keines im klassischen Sinne. Alles Wissensnotwendige erfahren Sie natürlich dennoch, eingeleitet durch kurzweilig gehaltene Informationen, die das jeweilige Programmiervorhaben einleiten. So schadet es nicht, wenn Sie beispielsweise etwas von der lange zurückliegenden Existenz eines computerähnlichen Geräts erfahren, das in einer ganz eigenen Beziehung zur Zeit stand. Auch wenn technische Welten Ihren und meinen Computer von jenem Exponat der Frühzeit trennen, so ist der Abstand
10
Eine notwendige Frage: Die Zielgruppe?
zwischen den am Abendhimmel sichtbaren Fixsternen und Ihren Augen noch viel größer. Wie sich Galileo den vermeintlich unendlichen Weiten näherte – selbst darüber erfahren Sie mehr.
1.3
Eine notwendige Frage: Die Zielgruppe?
Die Zielgruppe des Buches sind … vor allem Sie. Warum? Weil Sie ein grundlegendes Interesse an den sogenannten .NET-Sprachen (über den Begriff wird noch zu schreiben sein) im Allgemeinen und der .NET-Sprache C# im Speziellen haben. Weil Sie bereits mit dem Besuch einer einführenden Vorlesung, eines Seminars oder/und durch literaturgestütztes Selbststudium einen Schritt weitergegangen sind. Gefolgt von einem weiteren Schritt, indem Sie einfach angefangen haben zu programmieren. On the fly! Weil Ihnen das Prinzip der objektorientierten Programmierung im Zusammenhang mit C# aus unerfindlichen Gründen reizvoll erscheint. Durch Letzteres wird Ihnen natürlich (und sicher zu Recht) unterschwellig unterstellt, mehr über objektorientiertes Programmieren zu wissen, als einführende Beispiele zur Beschreibung einer Klasse in der Regel vermitteln. Die zum Objekt verkommene Katze werden Sie demnach in diesem Buch ebenso vergebens suchen wie die in private oder öffentliche Methoden gebannten Gewohnheiten des schnurrenden Vierbeiners. Über Derartiges sind Sie zwar nicht erhaben, gleichwohl fachlich weit hinaus. Es ist also an der Zeit, die Katze aus dem Sack zu lassen.
11
1.3
Künstliche Intelligenz ist gar nichts – verglichen mit natürlicher Dummheit. (Verfasser unbekannt)
2
Die Sache mit .NET
Auch nach über sieben Jahren ist .NET (zum Zeitpunkt der Niederschrift dieses Textes war Version 4.0 aktuell) oder, um es korrekt zu formulieren, die .NETTechnologie, ein kleines Mysterium geblieben. Doch leider ist ohne .NET nicht gut mit C-Sharp (C#) programmieren, und ohne einen Fundus an Grundwissen braust Ihr sicher vorhandener Elan an den Möglichkeiten des mächtigen Frameworks vorbei. Framework? Damit ist natürlich das .NET-Framework gemeint, und was es damit auf sich hat, darum werden wir uns im Folgenden kümmern. Stiefmütterlich behandelt werden dagegen Webservices sowie der .NET Enterprise Server. Beide spielen für die Entwicklung der Sie erwartenden Praxisbeispiele nämlich keine Rolle und somit auch nicht für Ihren literarischen Neuerwerb. Abgesehen davon ist das .NET-Framework primärer Bestandteil von .NET, und so betrachtet, kann es nicht falsch sein, mit .NET zunächst das .NET-Framework zu verbinden oder die beiden Begriffe sogar synonym zu verwenden. In der Zusammenfassung noch einmal: Bestandteile der viel gepriesenen .NETTechnologie sind: 왘
.NET-Framework
왘
Webservices
왘
.NET Enterprise Server
Für uns ist, wie ich bereits erwähnt habe, lediglich das .NET-Framework von Belang. Würden wir Webservices und .NET Enterprise Server mit ins Boot nehmen, hielten Sie ein praxisorientiertes Buch über die .NET-Technologie in den Händen. Trösten Sie sich: Das .NET-Framework allein füllt mühelos die Seiten kiloschwerer, mitunter schwer verdaulicher Fachbücher. Dasselbe komplexe Thema betreffend, begnügen wir uns mit vergleichweise wenigen Seiten, denn weniger ist manchmal (allerdings nicht immer) mehr. Vor allem dann, wenn die Intention des Buches eine etwas andere ist.
13
2
Die Sache mit .NET
Trivial wird es auf den folgenden Seiten gleichwohl nicht. Es geht nicht anders! Denn Sie sollten zumindest im Groben wissen, auf welcher softwaretechnischen Grundlage die Praxisbeispiele aufgesetzt sind. Schließlich unternehmen Sie mit Ihrem hypothetischen Wunschauto auch keine Probefahrt, ohne sich für die Raffinessen des vierrädrigen Objekts zu interessieren (und gegebenenfalls zu begeistern). Alles werden wir gleichwohl nicht besprechen können. Einige der wichtigsten Konzepte dagegen schon.
2.1
Intelligent, aber nicht unergründlich – das .NET-Framework
Die Frage, was unter dem .NET-Framework primär zu verstehen ist, führt beinahe zwangsläufig zu anderen, nicht immer tauglichen Begriffen: 왘
konzeptionelle Plattform
왘
Softwareplattform
왘
Klassenbibliothek
왘
Programmiermodell
All das haben Sie mit Sicherheit schon irgendwo gelesen. Und nichts von dem ist selbstredend falsch (schlechtestenfalls ist es unvollständig). Fragen wir deshalb anders: Was ist der springende Punkt beim .NET-Framework? Oder gibt es sogar mehrere?
2.1.1
Sprachgewaltig: die Laufzeitumgebung
Das .NET-Framework ist bedeutend mehr als eine Klassenbibliothek (und sei diese auch noch so groß). Im Wort Framework steckt schließlich neben Frame auch das Wort Work. Hier arbeitet notwendigerweise etwas, nämlich die Laufzeitumgebung (englisch Runtime Environment), genauer gesagt eine programmiersprachenunabhängige Laufzeitumgebung, die als Common Language Runtime oder kurz CLR bezeichnet wird. Die können Sie sich getrost als Computer im Computer (eine Art virtuelle Maschine also) vorstellen. Abbildung 2.1 zeigt schematisch den Aufbau der CLR.
14
Intelligent, aber nicht unergründlich – das .NET-Framework
Base Class Library (BCL-Support)
COM Marshaler
Thread Support
Type Checker
Exeption Manager
Debug Engine
Security Engine
IL to Native Computer
Code Manager
Garbage Collector
Class Loader
Abbildung 2.1
2.1.2
Vereinfachter schematischer Aufbau der Common Language Runtime (CLR)
Ein Meer von Klassen – die Base Class Library (BCL)
Ganz oben in Abbildung 2.1entdecken Sie die Base Class Library (BCL). So oft der Name auch in allen möglichen und schematischen Darstellungen zu finden ist, so wenig aktuell ist er in Wahrheit. Microsoft selbst spricht lieber von der .NET Framework Class Library (FCL), was sogleich die Frage aufwirft, wo in der Abkürzung das .NET abgeblieben ist. NFCL wäre genauer, allein schon deshalb, weil es neben dem .NET-Framework auch andere Frameworks gibt, nicht zuletzt den ewigen Konkurrenten Java mit seinem etablierten Runtime Environment (JRE). Anscheinend steckt jedoch mehr hinter der namentlichen Verwirrung, gibt es doch im Hause Microsoft an der Entwicklung des .NET-Frameworks beteiligte Programmierer (auf einen werden wir noch zurückkommen), die die BCL als Teil der FCL betrachten. Demnach wird also zwischen BCL und FCL unterschieden. Und wenn wir schon mal dabei sind, Konfusion zu erzeugen: Bei Ihrer Beschäftigung mit .NET wird Ihnen auch die Abkürzung CL begegnen, womit nichts anderes als die BCL gemeint ist. Schließen wir Frieden mit dem Namen- und Abkürzungswirrwarr, denn ändern lässt es sich gerade in einer wissenschaftlich-technischen Umgebung nicht, die von Anglizismen und Abkürzungen nur so wimmelt.
15
2.1
2
Die Sache mit .NET
Ganz gleich, ob Sie Windows-, Webanwendungen oder Webservices entwickeln möchten, an der Klassenbibliothek des .NET-Framework ist nur schwer vorbeizukommen. Nach was, glauben Sie, werden Sie im Installationsverzeichnis des .NET-Frameworks (in der Regel C:\WINDOWS\Microsoft.NET\Framework\) im Falle der, sagen wir, BCL suchen müssen? Nach einem oder gar mehreren Ordnern, gefüllt mit zahlreichen, unzusammenhängenden Klassendateien, in womöglich noch alphabetischer Reihenfolge? So wird es – vielleicht – irgendwann angefangen haben, als sich zur Laufzeit eines Programms im Wesentlichen nichts anderes vollzog als der Lauf des Programms. Klassenbibliotheken können (und das ist in der Regel der Fall) auch zur Laufzeit eingebunden werden (vereinzelt spricht man auch, und nicht frei von unfriedlichen Assoziationen, vom Nachladen der Bibliothek), womit wir es dann mit einer Dynamic Link Library (DLL) zu tun hätten, die Sie an der gleichlautenden, wenngleich zumeist kleingeschriebenen Dateiendung (.dll) erkennen. Statisch oder dynamisch? Faustischen Ursprungs ist die Frage zwar nicht, allerdings auch nicht unbedeutend. Programme, die auf DLLs zurückgreifen, sind zumeist kleiner als jene, die sich mit einer statischen Bibliothek begnügen (solche Programme werden tatsächlich noch geschrieben) – einfach weil der Code auf mehrere Dateien verteilt werden kann. Dagegen erfolgt das Hinzubinden einer statischen Bibliothek zum Programm zur Übersetzungszeit, mit der leicht zu erahnenden Konsequenz, dass die ausführbare Datei (.exe) vergrößert wird. Dafür wird nichts nachgeladen.
Im Kontext der BCL sind zwei Dateien (und aus mehr Files besteht die Base Class Library erstaunlicherweise nicht) mit der Endung .dll von einiger Bedeutung: 왘
system.dll
왘
mscorlib.dll
Beide Dateien sind mit im Mittel 3,5 MB wahrlich keine Leichtgewichte, was bei der Anzahl der in ihnen enthaltenen Klassen auch nicht verwundert. Gezählt habe ich sie nicht. Es werden einige Tausend sein, deren Aufgaben sich gleichwohl gut gruppieren lassen. Die beiden Dateien system.dll und mscorlib.dll enthalten: 왘
Basisdatentypen
왘
IO-Funktionen (Input/Output)
왘
Netzwerkfunktionen
왘
Reflection-API
16
Zwischengeschoben – der IL-Code
왘
COM-Interoperabilität
왘
Remoting
왘
Sicherheitskonfiguration
왘
Threading
왘
Zugriff auf Teile des Windows-Systems
2.2
Zwischengeschoben – der IL-Code
Die Unterstützung der Base Class Library (siehe Abbildung 2.1) ist Teil der Laufzeitumgebung (CLR), ohne die es schwierig wird, den in einer beliebigen Programmiersprache entwickelten Code auszuführen. Früher war mit dem Wechsel der Programmiersprache zwangsläufig ein Wechsel der Laufzeitumgebung verbunden, woran sich im Grunde nichts geändert hat – bis auf die erfolgreiche Sache mit .NET, wo eine Laufzeitumgebung gleich mehrere Sprachen bedient (nämlich die sogenannten .NET-Sprachen, u. a. auch das von uns gewählte C#). Ein intelligentes Konzept, fürwahr, nur: Warum funktioniert es so gut? Es hilft nichts, wir müssen noch weiter ins Kellergewölbe des .NET-Frameworks vordringen. Verstaubtes erwartet Sie dort nicht, wohl aber eine weitere, softwaretechnische Raffinesse. Eine stattliche Hürde Das Problem ist schlicht, einen in C# entwickelten Programmcode gleichermaßen für die Laufzeitumgebung des .NET-Frameworks verständlich zu machen, wie die potenziell in Visual Basic.NET oder einer anderen .NET-Sprache (z. B. C++.NET) entwickelten Sourcen. Die CLR spricht nur eine Sprache, .NET-Sprachen dagegen gibt es gleich mehrere. Zum Glück! Was es leider auch gibt, ist ein Problem.
Um zu verhindern, dass sich die Laufzeitumgebung an der Mehrsprachigkeit des .NET-Frameworks nachhaltig »verschluckt«, wird der Runtime Environment ein Zwischencode serviert, der IL- oder MSIL-Code genannt wird. (IL steht für Intermediate Language, MSIL für Microsoft Intermediate Language.) Anders formuliert: Die CLR weiß nichts vom relativen Sprachreichtum der Codierung, die ausgeführt werden soll. Die Laufzeitumgebung befasst sich lediglich mit dem ILCode. Bleibt die Frage zu klären, was der ominöse IL-Code eigentlich ist. Kurze Antwort: Es ist Bytecode. Und der besteht aus nichts anderem als aus Befehlen für eine virtuelle Maschine. Das Schöne am Bytecode (der auch P-Code genannt wird) ist die Unabhängigkeit vom Prozessor, was so formuliert ein wenig geschönt ist, verhält es sich doch eher so, dass der Bytecode vom physischen Prozessor nicht verstanden werden kann. Prozessorunabhängigkeit? So kann man es
17
2.2
2
Die Sache mit .NET
also auch nennen. Was die CPU Ihres und meines Computers versteht, sind Maschinenbefehle (man spricht auch von Native Code), die wiederum vom Typ des Prozessors abhängig sind. Dennoch ist das Thema Zwischencode damit nicht abgehandelt, denn nur wenig ist so einfach, wie es – dem Titel dieses Buches geschuldet – beschrieben werden sollte. Kurz mal verschnaufen, und dann folgt ein vermeintlich überflüssiger Satz: Die Übersetzung einer C#-Anweisung in den IL-Code (Zwischencode) übernimmt der Compiler. Im Falle der Sprache C# verbirgt sich der Compiler in der Datei csc.exe, die Sie, abhängig vom Ort der Installation, irgendwo in den unergründlichen Tiefen des .NET-Frameworks finden. Wenn es aber mehrere .NET-Sprachen, einen Zwischencode und eine Laufzeitumgebung gibt, muss es notwendigerweise auch mehrere Compiler geben, genau für jede der .NET-Sprachen einen. Dem ist auch so. Metadaten Neben dem IL-Code generieren die Compiler spezielle Informationsdaten. Diese sogenannten Metadaten beinhalten Informationen über Typen, Elemente oder Verweise, die für das C#-Programm auf die ein oder andere Art wichtig sind. Schließlich muss die Laufzeitumgebung beispielsweise wissen, wann eine Klasse oder ein anderes Programmelement geladen werden soll. Metadaten werden zur Laufzeit des Programms ausgelesen. Dazu bedarf es spezieller Klassen, die unter dem Namen Reflection API bekannt sind.
Selbst wenn es keine Metadaten gäbe: Fühlen Sie sich nicht zu sehr an Ihre Beschäftigung mit dem klassischen, in Native Code übersetzenden Compiler erinnert! Denn die hier angesprochenen »Compiler« kompilieren nicht in den Maschinencode, sondern in den IL-Code, P-Code, Bytecode oder welchen Namen Sie auch immer dem begabten Kind geben mögen. Auf alle Fälle ist der Zwischencode eben nicht für einen physikalischen Prozessor bestimmt.
2.2.1
Kein Fabelwesen – der Jitter
Die Weiterverarbeitung des IL-Codes könnte durchaus eine Art Interpreter erledigen, der den Code einliest, analysiert und schlussendlich ausführt. Der Nachteil eines Interpreters? Die Geschwindigkeit. Der Vorteil des interpretierten Codes: die weitgehende Unabhängigkeit von der Architektur des Rechners (zumindest dann, wenn der Interpreter selbst in C oder einem C-Dialekt geschrieben ist). Als Alternative käme der Einsatz eines klassischen Compilers in Betracht. Der Vorteil der Kompilierung: eine höhere Geschwindigkeit, denn Native Code ist nicht langsam. Der Nachteil? Tja, überlegen Sie! Maschinencode ist prozessorabhängig, somit erführe das .NET-Framework einen merklichen Ruck in Richtung
18
Zwischengeschoben – der IL-Code
Plattformabhängigkeit. Genau die gilt es zu vermeiden, mehr noch als eine langsame Ausführungsgeschwindigkeit. Eine Software, für die erst das Motherboard und/oder der Prozessor ausgetauscht werden müssen, wird nur schwerlich den Weg aus den Regalen des Herstellers finden. (Bei Computerspielen sieht das schon anders aus: Das nächste Weihnachtsfest kommt bestimmt.) Was bleibt, ist die Vereinigung der Vorteile von Interpreter und Compiler. Was dabei herausgekommen ist, wird umgangssprachlich Jitter genannt. Großzügig betrachtet, leitet sich der Begriff aus dem sperrig klingenden Just-In-Time-Compiler (kurz JIT-Compiler) ab. Wenn Sie sich den als Flasche vorstellen, ist in dieser allerdings beträchtlich mehr Compiler als Interpreter vorhanden. Drehen Sie das Behältnis um, um die Geister herauszulassen, tritt auch sogleich der Compiler in Aktion, genauer betrachtet allerdings nur dann, wenn Ihr Programm tatsächlich ausgeführt wird. Das Just-In-Time geht allerdings noch weiter: Ein Programm besteht (auch im weiteren Sinne) aus Modulen. Wird lediglich das Modul A verwendet, ist die Übersetzung des Moduls B (zunächst) überflüssig. Demnach ist nur das aktuell Benötigte zu kompilieren – zur Laufzeit und gerade noch rechtzeitig. Eben just in time.
Quellcode (Sourcecode)
Compiler (z.B. csc.exe)
IL-Code zuzüglich Metadaten
JIT-Compiler
Native Code Abbildung 2.2 Vom Quellcode zum Native Code – die Schritte bis zur Ausführung eines C#-Programms
19
2.2
2
Die Sache mit .NET
Vergessen Sie jedoch nicht: Der Ausgangspunkt des Kompilats war der Zwischencode. Von dem, was Sie in Kürze ins Editorfenster Ihrer Entwicklungsumgebung (Visual C# 2010 Express Edition) eingeben werden, sind wir relativ weit abgekommen. Daher ist es jetzt Zeit, den Weg zurück zu dem anzutreten, was spätestens ab dem nächstem Kapitel Ihre Aufgabe sein soll: zum Programmieren. Um genau zu sein: zum Programmieren mit C#. Zuvor sehen wir uns den Weg vom Quellcode zum Native Code in einer grobschematischen Darstellung (siehe Abbildung 2.2) an.
2.3
Ein Raum ohne Tür – der Namensraum
Die Geburtsstunde von .NET (Microsoft begann die Entwicklung in den späten 1990er-Jahren) ist nicht mit der Geburtsstunde der Namensräume gleichzusetzen, existiert das Konzept des Namensraums (engl. namespace) doch schon länger. Ferner sind Namensräume keine Domäne von Programmiersprachen; XML als Markup-Sprache nutzt Namensräume genauso wie UML als Modellierungssprache. Beide tun dies allerdings auf andere Art. Eigentlich geht es bei der Einrichtung von Namensräumen weniger um Ordnung als darum, die Möglichkeit von Verwechslungen zu minimieren. Schließlich besteht durchaus die Möglichkeit, zwei Klassen mit unterschiedlichen Aufgaben denselben Namen zu geben. Ferner können durch die Einrichtung von Namensräumen die Klassen eines Programms ausgezeichnet strukturiert werden, u. a. nach Aufgabengebieten. Beispielsweise wäre es überlegenswert, die Klasse, die für das Zeichnen zweier sich überlappender Kreise zuständig ist, einem anderen Namensraum zuzuweisen als jene, die die Größe der Überschneidungsfläche berechnet. Erstere würde in die Kategorie »Montagsmaler« fallen, Letztere wäre unter »fortgeschrittene mathematische Verfahren« zu verbuchen. Klassen In einem Namensraum werden weder Methoden noch Variablen deklariert, sondern primär Klassen. Ist eine statische (damit wir uns die Instanziierung sparen) Klasse MeineKlasse in einem Namensraum MeinNamensraum organisiert, sind damit natürlich auch die Elemente der Klasse (Methoden, Variablen etc.) dem entsprechenden Namensraum »zugewiesen«. Auf den Punkt gebracht: Für alles, was nicht im noblen Stande einer Klasse ist, existieren nur indirekt Namensräume – zumindest dann, wenn es um programmiersprachliche Namensräume geht. Doch gibt es Ausnahmen. Eine davon ist Ihnen bereits begegnet: die Struktur.
20
Ein Wort wie ein Kosename – Assembly
Die Gültigkeit der Klasse MeineKlasse ist auf den Namensraum MeinNamensraum beschränkt. Wir sollten nicht vergessen, der Klasse MeineKlasse noch eine Methode zu spendieren; die nennen wir einfach TuWas(). Wir entwickeln ein Szenario, in dem Sie aus einer selbst geschriebenen Methode MeineMethode() heraus die Methode TuWas() aufrufen möchten. Diese ist Teil der Klasse MeineKlasse, deren Gültigkeit wiederum auf den Namensraum MeinNamensraum beschränkt ist. Das hatten wir schon, okay. Wie kommen wir nun aber an die Methode TuWas()? Ein Namensraum hat keine Türen. Folgendes Listing gibt die Antwort: public void MeineMethode() { MeinNamensraum.MeineKlasse.TuWas(); }
Der Aufruf der Methode TuWas() gelingt, indem wir der Methode sowohl den Klassennamen als auch den Namen des Namensraums voranstellen, jeweils getrennt durch den leidlich bekannten Punktoperator (.). Schauen wir uns einige Namensräume mit wichtigen Typen im .NET-Framework an. Da wären: 왘
Windows.System.Forms, der klassische WindowsForms-Steuerelemente wie Button, CheckBox etc. zur Verfügung stellt.
왘
System.Web beinhaltet Klassen und Interfaces zur Client-Server-Kommunika-
tion. 왘
System.IO bietet elementare Funktionalitäten für Schreib- und Lesezugriffe.
왘
System.Data stellt Klassen zur Verwaltung von Daten aus unterschiedlichen
Datenquellen zur Verfügung. 왘
Windows.System.Controls kapselt Steuerelemente der Windows Presentation Foundation (WPF), wie beispielsweise Canvas, ListBox oder TextBox.
2.4
Ein Wort wie ein Kosename – Assembly
Konzeptionell betrachtet, existiert zwischen einem konventionell gefertigten Haus und einer .NET-Framework-Anwendung eine Gemeinsamkeit: Beide bestehen aus Bausteinen. Im Falle von .NET werden die Bausteine als Assemblys bezeichnet, ein melodisches, im Ursprung auf das beträchtlich steifer klingendere Assemblierung zurückgehendes Wort, was auf Deutsch so viel wie Baugruppe heißt.
21
2.4
2
Die Sache mit .NET
Eine Assembly besteht aus einer Auflistung von Typen und Ressourcen, die so erstellt wurden, dass ein Zusammenspiel möglich wird und darüber hinaus eine logisch funktionale Einheit gebildet wird. Dabei könnte das Aufgaben- und Funktionsspektrum einer Assembly nicht größer sein: 왘
Eine Assembly enthält Code, der von der Common Language Runtime ausgeführt wird, wobei jede Assembly nur über einen Einstiegspunkt verfügt. Dabei gelten als Einstiegspunkt die Schlüsselbegriffe Main DllMain WinMain
왘
Eine Assembly bildet eine sicherheitsrelevante Schranke dergestalt, dass sowohl das Anfordern einer Berechtigung als auch die Erteilung der Berechtigung durch die Assembly geschieht (ein weiteres Indiz dafür, wie genau es .NET mit Fragen der Sicherheit nimmt).
왘
Eine Assembly bildet eine typbezügliche Grenze, was einigermaßen nichtssagend klingt, weswegen uns ein Beispiel weiterhelfen muss: Wird ein Typ Beispieltyp in den Gültigkeitsbereich einer Assembly FirstAssembly.dll geladen, ist der Typ ein anderer, als wenn er in den Gültigkeitsbereich der Assembly SecondAssembly.dll geladen wird – was dem Konzept der Namensräume nicht unähnlich ist.
왘
Eine Assembly bildet eine Schranke für den Gültigkeitsbereich von Verweisen. Teil jeder Assembly ist das sogenannte Assembly-Manifest (siehe Abbildung 2.3), das Assembly-Metadaten enthält. Diese werden für das Auflösen von Typen ebenso verwendet wie für die Bereitstellung von Ressourcen (beispielsweise JPG- oder Ressourcendateien sowie Bitmaps). Die Assembly gibt Typen und Ressourcen an, die außerhalb der Assembly verfügbar gemacht werden dürfen.
왘
Eine Assembly stellt die kleinste, verwendbare versionierte Einheit der Laufzeitumgebung dar, wobei eine Assembly von der Common Language Runtime in verschiedenen Versionen verwendet werden kann. Die Typen und Ressourcen einer Assembly bilden eine Einheit, deren Version jener der Assembly entspricht, die die Typen und Ressourcen beinhaltet.
왘
Eine Assembly bildet eine Einheit bezüglich der parallelen Ausführung, also dem Ausführen unterschiedlicher Versionen einer Assembly.
Abbildung 2.3 soll den Aufbau einer Assembly veranschaulichen, wobei es sich um eine Einzeldatei-Assembly (englisch Single-File-Assembly) handelt, bei der alle Elemente einer Assembly physisch in einer Datei (TestAssembly.dll) gruppiert sind.
22
Ein Wort wie ein Kosename – Assembly
TestAssembly.dll Assemblymanifest Typmetadaten MSIL-Code Ressourcen Abbildung 2.3
Schematischer Aufbau einer Einzeldatei-Assembly
Daneben existieren auch Mehrfachdatei-Assemblys (englisch Multi-File-Assemblies), die nicht vom Dateisystem selbst, sondern vom Assembly-Manifest verbunden werden. Die CLR behandelt die verschiedenen Dateien der Assembly im Weiteren als logische Einheit. Bei einer Multi-File-Assembly müssen das AssemblyManifest und der Einsprungspunkt in ein und demselben Modul liegen. Auch eine Assembly besitzt eine Identität Die körperlichen Merkmale Ihrer eigenen Identität werden im Personalausweis eingetragen sein, so wie auch Ihre Anschrift. Die Identifikation einer Assembly geschieht über die Elemente 왘
Assembly-Name
왘
Versionsnummer
왘
Kultur, also die länderspezifischen Festlegungen (standardmäßig das Benutzergebietsschema des Betriebssystems)
왘
Zu dem kommen Informationen über den sogenannten starken Namen, der weniger ein Name im Buchstabensinne als eine 128-Byte-Zahl ist, die aus zwei großen Primärzahlen besteht, die miteinander multipliziert worden sind.
All diese Informationen finden Sie, neben anderen, im Assembly-Manifest, dem »Personalausweis einer Assembly«. Zweifelsohne sind Sie auch ohne einen (gültigen) »Perso« existenzfähig, wogegen eine Assembly ohne ihr Manifest keine wäre.
Natürlich stellt sich sofort die Frage, wann eine Einzeldatei-Assembly und wann eine Mehrfachdatei-Assembly erstellt werden sollte. Um das interessante Thema Assembly nicht ausufern zu lassen, wollen wir uns mit zwei Anwendungsfällen für Mehrfachdatei-Assemblys begnügen. Solche können Sie beispielsweise erstellen, wenn es darum geht, 왘
das Herunterladen einer .NET-Framework-Anwendung zu beschleunigen. Konkret legen Sie selten verwendete Typen in einem eigenen Modul ab, das dann nur bei Bedarf herunterzuladen wäre.
왘
Module zu kombinieren, die in verschiedenen .NET-Sprachen geschrieben sind.
23
2.4
2
Die Sache mit .NET
2.5
Die »digitale Müllabfuhr« – der Garbage Collector
Dass Sicherheit bei .NET großgeschrieben wird, gehört zu den echten Pluspunkten der Technologie. Nicht jedes Programmmodul sollte auf jede Ressource zugreifen dürfen. Das damit einhergehende Einschränken von Rechten wird uns noch in programmiernahem Zusammenhang (bei der .NET-Sprache C# ...) beschäftigen. Im Regelfall werden Programme von .NET ständig überwacht. Genauer gesagt, von der .NET-Laufzeitumgebung als – mit einer Einschränkung – stets präsenter Kontrollinstanz. Selbst die Vorgänge im zweiten »Heiligtum« des Computers, dem Arbeitsspeicher, entgehen dem wachen Auge der Laufzeitumgebung nicht. Wird im Speicher mit Ressourcen schluderig verfahren, d. h., erfolgt keine Freigabe nicht mehr benötigter Segmente durch das Programm (womit diese Speicherstellen für die Nutzung des Programms drastisch formuliert »tot« wären), tritt der sogenannte Garbage Collector (die Speicherbereinigung) als eine Art digitale Müllabfuhr auf den Plan. Allerdings nicht allein. Gemeinsam mit dem C#-Compiler wird der Code hinsichtlich der Frage analysiert, welche Verweise auf das Objekt weiterhin verwendet werden können und welche nicht. Im letzteren Falle erfolgt der Anstoß zum Aufruf des Destruktors. Exkurs: Die Zerstörung von Objekten durch den Destruktor Wird das Objekt – die Instanz – einer Klasse erzeugt, geht dem der Aufruf des Konstruktors voraus. Ob der standardisiert oder frei definiert ist, ist in dem Zusammenhang nicht von Belang. Die Lebenszeit des Objekts währt nur so lange, wie der Destruktor, als »Gegenmittel« zum Konstruktor, nicht aufgerufen wird. Der Destruktor enthält nicht selten Anweisungen zur Freigabe der vom Objekt beanspruchten Ressourcen. Das können auch fremde Ressourcen wie z. B. eine Datenbankverbindung sein, die in einer Referenz des Objekts vorgehalten wird. Aufgerufen wird der Konstruktor leider nicht durch kritisches Nachdenken des Programms über sich selbst. Vielmehr muss mindestens eine von zwei klar definierten Bedingungen erfüllt werden: 왘
Der Gültigkeitsbereich der Objektvariablen wurde verlassen.
왘
An die Objektreferenz wurde null zugewiesen.
Prima, ist eine oder sind gar beide Voraussetzungen erfüllt, erfolgt also der Aufruf des Destruktors. Danach geht es dem Speicher besser, weil »Objektleichen« entfernt wurden. So einfach funktioniert das jedoch nicht, ist der tatsächliche Aufruf des Destruktors doch selbst dann zeitlich relativ unbestimmt, wenn der Gültigkeitsbereich einer Objektvariablen verlassen und/oder an die Objektreferenz null zugewiesen wurde. »Programmiersprachlich« betrachtet, existiert danach zwar kein Objekt mehr, im Speicher dagegen dümpelt der digitale Müll auch weiterhin vor sich hin.
24
Die »digitale Müllabfuhr« – der Garbage Collector
Ist hinsichtlich dessen die externe Triggerung des Destruktors durch den Garbage Collector nicht eine famose Sache? Nein, ist sie nicht. Das Prinzip ist pfiffig und gut gemeint – mehr aber auch nicht. Sicher, das liest sich ein wenig wie das schulmeisterlich strenge Wort, doch sind mit dem Garbage Collector in der Tat einige nicht von der Hand zu weisende Ungereimtheiten verbunden, die sich durchaus unter dem gerne gebrauchten Oberbegriff »Performance-Verlust« zusammenfassen lassen. Zwar sind die Algorithmen, die zur Suche nach sogenannten Memory Leaks (also Speicherlecks) eingesetzt werden, von durchaus akzeptabler Geschwindigkeit (was auch immer hier das Kriterium sein mag), das ändert jedoch kaum etwas daran, dass im Hinterstübchen der Common Language Runtime ein Garbage Collector genanntes Etwas langsam, aber stetig auf die Bremse der Ausführungsgeschwindigkeit tritt. Abgesehen vom zeitlich Unbestimmten beim Aufruf des Destruktors, ließe sich Pro Garbage Collector natürlich der Hinweis auf Speicherbereiche anbringen, deren Freigabe allzu gern mal vergessen wird – mit dem bekannten Worst-CaseSzenario eines mit zunehmender Laufzeit immer langsamer werdenden Computers (inklusive eines möglichen Absturz des Programms). All das ist richtig und sollte vermieden werden. Nur: Wo bitte bleibt das Vertrauen in den Entwickler und seine Kunst? Das Zerstören nicht mehr benötigter Objekte zwecks Speicherbereinigung gehört genauso zum objektorientierten Handwerkszeug wie das Erstellen eines Objekts. In die Fahrschule sind Sie schließlich auch nicht nur deshalb gegangen, um zu lernen, wie ein Auto in Bewegung gesetzt wird. Zum Hornberger Schießen wird die automatische Speicherbereinigung im Zusammenhang mit Systemen, die unter Echtzeitbedingungen laufen (sogenannte Echtzeitsysteme, englisch Real Time System). Echtzeitsysteme Echtzeitsysteme haben es schwerer als konventionelle. Während es bei konventionellen Systemen »lediglich« darauf ankommt, ein richtiges Ergebnis zu liefern, wird bei Echtzeitsystemen ein solches Ergebnis innerhalb eines wohldefinierten Zeitintervalls erwartet. Nachrangig ist dabei die Größe des Intervalls, das von einigen Millisekunden bis hin zu Tagen reichen kann.
Sucht das System nämlich nach freizugebenden Speichersegmenten, ist das Programm so lange blockiert, wie der Vorgang nicht abgeschlossen ist. Überlegen Sie sich die Konsequenzen, wenn es beispielsweise um vollautomatisierte Produktionsstraßen geht! Jede Zehntelsekunde wird benötigt. Der Garbage Collector be-
25
2.5
2
Die Sache mit .NET
nötigt für seinen »Rundgang« durch die Codedateien mehr Zeit. Während dieser ist das System von seinen regulären Aufgaben gezwungenermaßen entbunden. Das Echtzeitsystem wird zum blockierten System, der Garbage Collector zur Vollbremsung. Wann der Garbage Collector nach verwaisten Objekten sucht, kann nicht vorhergesagt werden (was eine seltsam anmutende Gemeinsamkeit mit einem »ungetriggerten Destruktor« darstellt). Dass er irgendwann in Aktion tritt, ist dagegen sicher. Das passiert nämlich spätestens dann, wenn die Speicherressourcen weitgehend aufgebraucht sind. Auf eine Meldung à la »Es ist nicht mehr genügend Arbeitsspeicher vorhanden …« lässt es die automatische Speicherbereinigung nicht ankommen. Da können Sie ganz zuversichtlich sein. Es stellt sich die Frage, was vorher geschieht. Trotz funktionaler Wichtigkeit des Garbage Collectors genießt er als selbstständige Ausführungseinheit (im Englischen spricht man von einem Thread) keine erhöhte Priorität. Selbst die Speicherbereinigung hat demnach widerstandslos zu warten, bis die Anwendung ruht, wodurch Rechenleistung zum Zwecke der Speicherverwaltung freigegeben werden kann. Der Garbage Collector also darauf angewiesen, von dem Programm, das untersucht werden soll, (indirekt) Rechenzeit zugewiesen zu bekommen, worauf er bei hoch beanspruchten Programmen womöglich lange warten kann. Gibt es einen Ausweg aus dem Dilemma? Ja, indem Sie im C#-Programmcode die automatische Speicherverwaltung durch die nicht sonderlich schwierige Zeile GC.Collect();
selbst aufrufen. Was genau aufgerufen wird, ist die statische Methode Collect() der Klasse GC, die Sie im Namensraum System finden. Damit erzwingen Sie eine sofortige Garbage Collection, mit der das System gezwungen wird, so viel Speicher wie möglich freizugeben – ein im Grundsatz recht rabiates Vorgehen, das einigermaßen gut überlegt sein will. Einen chirurgischen Eingriff stellt die Arbeit des Garbage Collectors nicht unbedingt dar. So viel sollte inzwischen klar geworden sein. Entweder wird der Speicher im Rahmen des programmseitig Möglichen bereinigt – oder überhaupt nicht. Zum Schluss des Abschnitts habe ich doch noch einige lobende Worte für den schwer – und vielleicht nicht unbedingt zu Recht – gescholtenen Garbage Collector. Für diese muss ich allerdings ein wenig ausholen. Auch abseits seiner Aufgabe ist ein Objekt nicht gleich Objekt. Entscheidend ist der Augenblick seiner Erstellung. Sie und ich sind ein Objekt der Gattung Mensch, irgendwann geboren und somit zu einer bestimmten Generation gehö26
Das Salz in der Suppe – Steuerelemente
rend. In unserem Falle spricht man gleichwohl auch von Lebensjahren, bei digitalen Objekten im .NET-Kontext von einer Generationszahl als ein gegebenenfalls von der Implementierung definiertes Maß für die Lebensdauer eines Objekts. Null als Generationszahl wird für das zuletzt erstellte Objekt festgelegt, ältere Objekte erhalten eine dementsprechend höhere, bis hin zur höchsten Generationszahl. Die Größe der maximalen Generationszahl kann mittels der MaxGeneration-Eigenschaft der Klasse GC ermittelt werden. Diese Zahl, die der Collect()-Methode als Wert übergeben wird, bewirkt die Freigabe von Objekten bis einschließlich der höchsten Generationszahl, die unter dem Einfluss der automatischen Speicherbereinigung gleichwohl kein Fixum ist. Denn der Garbage Collector kennt die bis hier beschriebenen Zusammenhänge und weiß sie ausgesprochen intelligent zu nutzen. Auch vor dem Hintergrund der Annahme (die man kritisch hinterfragen kann), dass neuerer Speicher dringender einer Bereinigung bedarf als älterer.
2.6
Das Salz in der Suppe – Steuerelemente
Im Englischen heißen Steuerelemente Controls, und Sie werden im Verlaufe des Buches einige näher kennenlernen. Steuerelemente sind unverzichtbar, denn das, was wir bis jetzt in Sachen .NET-Framework besprochen haben, nützt Ihnen im praktischen Umgang, zumindest im Sinne einer wirklichen Arbeitserleichterung, wenig. Zu Beginn eines konkreten Projekts wird es um die funktionale Gestaltung einer Bedienoberfläche gehen, auf der Bedienelemente systematisch und optisch ansprechend anzuordnen sind, ähnlich wie Sie das von komfortablen, womöglich noch interaktiven Websites her kennen. Auch Webapplikationen können Sie mit dem .NET-Framework entwickeln, wobei sich prinzipiell dieselbe Frage stellt: Woher den exemplarischen Button, das Label, die CheckBox, kurz die Steuerelemente nehmen, wenn nicht selber programmieren? Um welche Steuerelemente es geht, können Sie Tabelle 2.1 entnehmen, in der einige wichtige Windows-Forms-Steuerelemente namentlich genannt und von der Funktion her beschrieben sind. Wohlgemerkt: Dies sind WindowsForms-Steurelemente.
27
2.6
2
Die Sache mit .NET
Name des Steuerelements
Funktion
Button
Darstellung einer Standardschaltfläche, auf die der Benutzer zur Ausführung von Aktionen klicken kann
BindingNavigator
Darstellung einer Benutzeroberfläche zum Bearbeiten und Navigieren durch Steuerelemente, die an Daten gebunden sind
CheckBox
Darstellung einer aktivierten oder deaktivierten Bedingung
CheckedListBox
Darstellung einer Liste von Elementen, wobei jedes Element der Liste mit einem Kontrollkästchen versehen ist
ComboBox
Darstellung von Daten in einem Dropdown-Kombinationsfeld
ContextMenuStrip
Darstellung eines Kontextmenüs (in älteren .NET-FrameworkVersionen lediglich als ContextMenu-Steuerelement bezeichnet)
DataGrid
Darstellung tabellarischer Daten aus einem Dataset. Zusätzlich ermöglicht das DataGrid-Steuerelement die Aktualisierung der Datenquelle.
DataGridView
Darstellung eines erweiterbaren Systems für die Anzeige und Bearbeitung von Tabellendaten
DateTimePicker
Tabellarische Darstellung von Datums- und Zeitangaben, aus denen ein Benutzer ein Element auswählen kann
DomainUpDown
Darstellung einer Textzeichenfolge, aus der Benutzer auswählen können und die durchsucht werden kann
FlowLayoutPanel
Darstellung eines Bereichs, dessen Inhalt dynamisch horizontal und vertikal verändert werden kann
GroupBox
Darstellung einer für andere Steuerelemente erkennbaren Gruppierung
HScrollBar VScrollBar
Die beiden Controls ermöglichen das horizontale (HScrollBar) bzw. vertikale (VScrollBar) Scrollen durch eine Elementliste oder – allgemeiner formuliert – durch eine große Anzahl von Informationen.
Label
Darstellung eines nicht zu bearbeitenden Textes
LinkLabel
Implementierung eines Hyperlinks
ListBox
Ermöglicht die Auswahl einer oder mehrerer Elemente aus einer vordefinierten Liste.
ListView
Dem Windows Explorer ähnliche Darstellung von Symbolen
MaskedTextBox
Einschränkung des Formats von Benutzereingaben in einem Formular
Tabelle 2.1 Wichtige Windows-Forms-Steuerelemente und ihre Funktion (Auflistung in alphabetischer Reihenfolge)
28
Aus der Art geschlagen – die Sprache C#
2.7
.NET und kein Ende?
Der kleine Exkurs in die ungeliebte Theorie brachte Ihnen eine ebenso interessante wie komfortable Softwaretechnologie hoffentlich ein Stück näher. Das hohe Lied auf die .NET-Philosophie habe ich dennoch nicht gesungen, muss doch .NET weder einen globalen Praxiseinsatz erfahren noch übermäßig beworben werden. Dafür ist der Erfolg zwischenzeitlich zu groß. Bei .NET sind bis zur Ausführung von Maschinencode vergleichsweise viele Schritte notwendig, von denen einige der Kontrolle durch die Laufzeitumgebung unterliegen. Auf den Punkt gebracht: .NET liegt weder eine unaufwendige Architektur zugrunde noch ist das Framework eine übermäßig schnelle Angelegenheit (was allerdings ausdrücklich nicht im Sinne von »langsam« verstanden werden sollte). Letzten Endes wird man auch bei .NET zwischen Komplexität der Aufgabe und dem dafür in Kauf zu nehmenden Aufwand abwägen müssen. Käme man beispielsweise auf die originelle Idee, einen Hardwaretreiber auf Grundlage der .NET-Plattform zu entwickeln, entstünde zweifelsohne ein grobes Missverhältnis – abgesehen davon, dass einem solchen ».NET-Treiber« nicht unbedingt zu trauen wäre. Nach .NET und seinem Framework soll nun endlich aber C# an die Reihe kommen. Hier allerdings fangen wir anders an.
2.8
Aus der Art geschlagen – die Sprache C#
Geständnisse sollte man nicht auf die lange Bank schieben, weniger noch, wenn eine Unterlassung Inhalt des Geständnisses ist: .NET versus COM Sollte bei Ihnen auf den vergangenen Seiten der Eindruck entstanden sein, mit C# ließen sich lediglich .NET-Komponenten entwickeln, ist dies nur insofern richtig, als dass COM (Component Object Model), die .NET-Vorgänger-Technologie, ebenfalls durch C# bedient werden kann, was allerdings nur selten geschieht. Deshalb ist die Entwicklung klassischer COM-Komponenten weder »out« noch eine auf an der Historie orientierten Programmierer beschränkte Angelegenheit. Sehen Sie die Dinge bitte dennoch so, wie sie sind: Überholt wurde COM von .NET mit einiger Geschwindigkeit, entsprechend hoch ist der Abstand zwischen den Technologien. Übrigens: Auch COM ist ein ausschließliches Erzeugnis der Redmonder Softwareschmiede Microsoft.
29
2.8
2
Die Sache mit .NET
2.8.1
Musik im Namen
Versuchen Sie, mit einer Standardtastatur ein Kreuz auf Ihren Bildschirm zu »zaubern«. Ein wenig verärgert, mögen Sie vielleicht denken: »Wozu existiert ein Plus-Zeichen?« Stimmt, nur ist das nicht gemeint, sondern jenes Kreuz, das einen Stammton um einen Halbton erhöht. Und so sieht das Kreuz aus der Welt der Noten aus: $. Direkt eingeben lässt sich das Erhöhungszeichen (im internationalen Zeichencodierungssystem Unicode finden Sie es unter U+266F) nicht, wohl aber das ähnlich aussehende Raute-Zeichen (#). Anderenfalls würden Sie im Verlauf des Buches nicht mit C#, sondern mit C arbeiten. Der Freude am Programmieren wäre damit kein Abbruch getan, und der Bezug zur Musik wäre schneller hergestellt – auch deswegen, weil C nicht nur der Urvater der mächtigen C-Programmierfamilie ist, sondern auch ein überaus beanspruchter Stammton. Erhöht man diesen Ton nun um unseren halben Ton, erhält man das erwähnte C. Im Lande Mozarts, Bachs und Beethovens heißt er Cis, im Englischen dagegen C sharp. Und da Englisch die Weltsprache der IT-Kundigen (und die Muttersprache des Branchenriesen Microsoft) ist, sprechen auch wir statt von Cis von C sharp. Das klingt gut, hat einen intelligenten Hintergrund und verleugnet die Musik, als wohl schönste der schönen Künste, nicht. War es das zu diesem Thema? Nein. Abgekupfert – Derivate Die Beliebtheit einer Programmiersprache lässt sich auch an der Anzahl vorhandener Derivate ablesen. C# hat einige Nachahmer gefunden, sogar im riesigen Hause des Sprachurhebers (Microsoft) selbst. Was einmal klingt, kann auch mehrfach klingen, am besten noch in unterschiedlichen Tönen, was im Kontext unserer kleinen Namensforschung natürlich nicht wörtlich zu nehmen ist. Schön klingt sie dennoch, ich meine die Programmiersprache Polyphonic C#, bei der es sich genau betrachtet allerdings nicht um ein Derivat, sondern um eine Erweiterung handelt. Andere C#-Derivate sind u. a.: 왘
Vala
왘
Metaphor
왘
Multiprocessor C# (MC C#)
왘
eXtensible C# (XC#)
왘
Sing#
왘
Spec#
30
Aus der Art geschlagen – die Sprache C#
2.8.2
Ein Kessel Buntes – die Ursprünge der .NET-Sprache C#
Fern aller mitunter philosophisch angehauchten Grundsatzdebatten sind .NET und C# für nicht wenige Freunde des .NET-Frameworks gedanklich genauso untrennbar miteinander verwoben, wie es Kreide und Tafel für den gestrengen Lehrer sind. C# ist elegant, typsicher (Typverletzungen werden spätestens zur Laufzeit erkannt) und nicht zuletzt objektorientiert. Vor allem jedoch ist C# –anders. Anders deswegen, weil die Sprache ein Konglomerat aus verschiedenen Hochsprachen darstellt. Im Einzelnen wären zu nennen: 왘
C
왘
C++
왘
Java
왘
Smalltalk
왘
Modula 2
왘
Delphi
Dazu kommt noch die Datenbankabfragesprache SQL. Primär orientiert sich C# allerdings an Mitgliedern der C-Familie (C, C++), was es Programmierern mit fachlichem Schwerpunkt auf den beiden Cs besonders leicht macht, auf das dritte C (C#) umzusteigen. »Anders« ist aber auch der Name des Mannes, der als Architekt primär für das Design der Sprache C# verantwortlich zeichnet: Anders Hejlberg. Microsoft war nie besonders zimperlich, wenn es darum ging, Spitzenleute von anderen namhaften Softwareschmieden abzuwerben, in diesem Falle von Borland. Und wer da im Jahre 1996 dem Schwergewicht der IT-Branche ins ausgedehnte Netz ging, war nicht irgendein Programmierer, war doch eben jener Anders Hejlberg der Urheber des »Pascal-All-in-one-Systems« (Editor, Compiler, Debugger in einem) Turbo Pascal, mit dem auch ich während meines Studiums (gerne) gearbeitet habe. Erschienen ist Turbo Pascal allerdings bereits zehn Jahre zuvor. Damals verwies die Entwicklungsumgebung, besonders der Compiler, nicht zuletzt aufgrund seiner beeindruckenden Geschwindigkeit alles bisher Dagewesene auf die hinteren Ränge. Übrigens: Hejlsberg ist darüber hinaus einer der Entwickler des .NET-Systems und Däne.
2.8.3
Ein heikler Punkt – Pointer
Fragen Sie einen eingefleischten C++-Programmierer, auf welches der Sprachkonzepte er entweder niemals oder nur allzu gerne verzichten würde, bekommen Sie vielleicht (und hinter vorgehaltener Hand) zur Antwort: Zeiger. Abgesehen vom grundsätzlich Schwierigen beim Einsatz von Zeigern, scheiden sich die gelehrten
31
2.8
2
Die Sache mit .NET
Geister nicht zuletzt in Hinblick auf die Sicherheitsaspekte. Das ist nicht so bei C#, denn dort gibt es keine Zeiger, wenigstens nicht durchgängig. Grundsätzlich versucht man den Einsatz von Zeigern bei .NET zu vermeiden. Werden Zeiger dennoch eingesetzt, spricht man so abschreckend wie naheliegend von unsicherem Code, der aus Bereichen mit eingeschränkten Rechten verbannt ist und ohne die Zuweisung erweiterter Rechte erst gar nicht zur Ausführung kommt. Ja, und in der Tat ist das .NET-Framework eine einigermaßen »rechthaberische Angelegenheit«. Um vermeintlich oder tatsächlich unsicheren Code zu produzieren, brauchen Sie den C#-Modifizierer unsafe, der damit zu einem Teil der Deklaration einer Klasse, einer Struktur, eines Interfaces oder eines Delegate wird. Womit die Einsatzgebiete des Modifizierers unsafe mitnichten ausgeschöpft wären, lässt er sich doch gleichermaßen in die Deklaration eines Felds, einer Methode, einer Eigenschaft, eines Ereignisses, Indexers, Operators, Instanzkonstruktors bzw. -destruktors integrieren. Allgemein ermöglicht die unsafe-Anweisung die Verwendung eines unsicheren Kontexts innerhalb eines Blocks. Alles, was sich im Block befindet, wird als unsicherer Kontext angesehen. Punkt. Was wollen Sie mehr? Zum Beispiel ein kleines, halbwegs einprägsames Beispiel? Gut, versuchen wir es mit dem Folgenden: public unsafe class Example { public char* C; public char* o; public char* d; public char* i; public char* n; public char* g; } Listing 2.1
Deklaration einer Klasse unter Einbeziehung des Modifizierers »unsafe«
Der Modifizierer unsafe in der Deklaration der Klasse Example bewirkt, dass deren gesamter Bereich, also alles, was sich innerhalb der geschweiften Klammern befindet, zum unsicheren Kontext erklärt wird. Und in dem haben wir es mit gleich sechs öffentlichen (public) Variablen vom Typ »Zeiger auf char« (char*) zu tun. Das Schlüsselwort unsafe ist selbsterklärend und missverständlich zugleich (desgleichen die Formulierung unsicherer Kontext). Abgesehen davon, dass Sie vermutlich bei Ihrer Arbeit mit .NET sehr genau wissen, was Sie tun, bedeutet un-
32
Aus der Art geschlagen – die Sprache C#
safe keineswegs das unbedingte Vorhandensein unsicherer Codeabschnitte. Erst im Zusammenhang mit der .NET-Laufzeitumgebung (CLR) machen die vermeintlichen Unsicherheiten im Code Sinn.
Zäumen wir das Pferd von der anderen Seite auf: Wie sieht es denn bei sicherem Code aus? Separat ausgewiesen braucht der nicht zu werden, ein Modifizierer safe existiert im Wortschatz der .NET-Sprache C# nicht (oder?). Sicherer Code ist deshalb sicher, weil er unter der Kontrolle der Laufzeitumgebung steht, weniger weil er per se sicher wäre. Beispielsweise verhindert die Laufzeitumgebung das Überschreiben von Array-Grenzen. Unsicherer Code genießt eine solche Protektion nicht. So einiges ist da möglich, bis hin zum »locker fröhlichen« Überschreiben ordentlich reservierter Speicherbereiche. Den Anwender »freut« es zuletzt. Strukturierung ist alles Erinnern Sie sich an einen nahen Verwandten der Klasse? Natürlich: die Struktur. Wird eine Struktur angelegt, erhält man einen komplexen Datentyp. Von diesem – ganz gleich wie komplex er nun wirklich ist – können Sie Variablen deklarieren, genauso wie Sie es von den elementaren Datentypen (int, char, float etc.) her kennen.
Na denn: Schreiben wir obiges Beispiel für eine Struktur desselben Namens um (Example): public unsafe struct Example { public char* C; public char* o; public char* d; public char* i; public char* n; public char* g; } Listing 2.2
Deklaration einer Struktur unter Einbeziehung des Modifizierers »unsafe«
Dasselbe Wort – unsafe – muss noch einmal vorkommen. Als Compiler-Direktive nämlich. Fehlt die Direktive, d. h., versuchen Sie, einen mit unsafe ausgewiesenen, unsicheren Code zu kompilieren, ohne das entlarvende Wort auch dem CSCCompiler (csc.exe) mitzugeben, verweigert dieser mit einer deutlichen Fehlermeldung den Dienst. Als Nutzer einer integrierten Entwicklungsumgebung (IDE) werden Sie in diese Verlegenheit jedoch nicht unbedingt kommen. Allerdings bleibt es Ihnen nicht erspart, der Entwicklungsumgebung mitzuteilen, dass der Compiler unsicheren Code im Projekt vorfinden wird. Alles Weitere erledigt dann die IDE. Dazu ist es notwendig, im Projektmappen-Explorer mit der
33
2.8
2
Die Sache mit .NET
rechten Maustaste auf das aktuelle Projekt zu klicken, um anschließend im Kontextmenü die Option Eigenschaften auszuwählen. Was Sie im Code-Editor daraufhin erwartet, ist die Eigenschaftenseite des Projekts, auf der Sie im rechtsseitigen Menü Erstellen wählen. Im Formular muss das Kontrollkästchen Unsicheren Code zulassen gesetzt werden (siehe Abbildung 2.4).
Abbildung 2.4 Über das grafische Interface der Entwicklungsumgebung (IDE) wird dem Compiler mitgeteilt, dass er unsicheren Code zu kompilieren hat.
Ein wenig aufwendiger gestaltet sich die Angelegenheit, wenn Ihnen der Sinn nach »manuellem Bedienen« des Compilers steht, also beispielsweise über die Kommandozeile des Betriebssystems. Im folgenden Beispiel wird der Compiler angewiesen, die Quellcodedatei test.cs für den unsicheren Modus zu kompilieren: csc /unsafe test.cs
Für Windows-Applikationen (und solche werden Sie erstellen) ist die Frage zeigerarithmetischer Codierung weniger relevant als für eine Web-Applikation; dessen ungeachtet sollte das Thema trotzdem nicht gemieden werden. Zudem: Vielleicht hegen Sie ja Sympathien für Zeiger. Ein wirklich großer Stein wird Ihnen vom .NET-Framework, ungeachtet aller Sicherheitsfragen, nicht in den Weg gelegt, geschweige denn von C# selbst. Dort können u. a. folgende Typen Zeigertypen sein:
34
Aus der Art geschlagen – die Sprache C#
왘
sbyte, byte, short, ushort, int, uint, long, ulog, char, float, double, decimal und bool
왘
enum-Typ (beliebig)
왘
ein beliebiger Zeigertyp
Ob Sie Zeiger einsetzen oder nicht, liegt natürlich bei Ihnen – genauso wie die Bewertung der Tatsache, dass Zeiger nicht vom bereits besprochenen (siehe Abschnitt 2.5) Garbage Collector überwacht werden. Der kennt weder Zeigertypen noch die Daten, auf die sie verweisen. Ein Fazit im Spaß Programmieren Sie unsicher, und Sie bewegen sich mit Zeigern auf der sicheren Seite.
2.8.4
Ein geheimnisvoller Verein – Delegates
Mit dem scherzhaft gemeinten, die Verhältnisse dreist auf den Kopf stellenden Fazit oben könnte das Kapitel über Zeiger abgeschlossen werden, gäbe es bei .NET nicht ein zeigerähnliches Sprachkonzept, dem die Beschränkung auf unsichere Codebereiche erspart bleibt. Gemeint sind sogenannte Delegates. Das klingt ein wenig nach einer Bedrohung aus der hintersten Ecke des Sonnensystems, ist jedoch eine durchweg friedliche, wenngleich nicht unbedingt triviale Angelegenheit. Und wirklich: Einfach zu verstehen ist das Konzept der Delegates nicht. Versuchen wir die Annäherung in kleinen Schritten. Delegates (dt. Delegierte) sind Stellvertreter, zumindest in der ursprünglichen Bedeutung des Wortes. Von politischen Delegierten bis apostolischen Legaten existieren unter dem Himmelszelt eine ganze Menge Stellvertreter. Softwaretechnisch betrachtet, sind Delegates jedoch mehr als »nur« Stellvertreter (mit meist eingeschränkten Befugnissen), nämlich Referenzen auf Methoden, was Sie womöglich und nicht zu Unrecht an Funktionszeiger bei C++ bzw. C erinnert. Bei einer kurzen, klaren Erinnerung sollte es allerdings bleiben, bieten Delegates doch einen größeren Funktionsumfang als die programmiersprachlichen Vorbilder, wird doch auf ungleich mehr als lediglich Methoden verwiesen. Genauer gesagt, verweisen Delegates auf: 왘
Instanzmethoden eines Typs sowie die Zielobjekte, die der Methode zugeordnet werden können
왘
statische (static) Methoden
Legen wir nach: Ein Delegate gilt als über seinem ersten Argument geschlossen, wenn er auf eine statische Methode und ein Zielobjekt verweist, das dem ersten Parameter der Methode zugeordnet werden kann.
35
2.8
2
Die Sache mit .NET
Von elementarer Wichtigkeit bei Delegates ist: 왘
deren unbedingte Typsicherheit
왘
der objektorientierte Ansatz
Allein mit diesen beiden Punkten wird dem klassischen Funktionszeiger bereits der Rang abgelaufen. Was Delegates mit Funktionen bzw. Methoden gemeinsam haben, ist das Vorhandensein einer Signatur. Alle Funktionen, auf die ein Delegate zeigen soll, müssen einer im Delegate festgelegten Signatur entsprechen. Wohlgemerkt: der Signatur. Nichts sonst. Einen Methodenrumpf werden Sie bei der Deklaration eines Delegate nicht finden. Damit Sie mir glauben, schauen Sie sich die folgende Deklaration des Delegate MeinDelegate an. Zuvor aber noch dies: Delegates werden zur Laufzeit des Programms erstellt, was es ermöglicht, das Verhalten eines Programms dynamisch zu ändern. Damit besteht zum Beispiel die Möglichkeit, den Delegate in Abhängigkeit von einer Bedingung zu generieren. Doch zurück zur Deklaration: public delegate void MeinDelegate(int buchseiten);
Auch wenn es allzu selbstverständlich erscheint: Dass die Sichtbarkeit des Delegate MeinDelegate auf public gesetzt ist und der Rückgabetyp void lautet, spielt nur eine sekundäre Rolle, genauso wie der exemplarische Parameter buchseiten vom naheliegenden Basistyp int. Entscheidend ist die Existenz einer Methode mit identischer Signatur, was selbstredend nicht bedeutet, dass die Methode denselben Namen wie der Delegate besitzt. Etwas anderes, was uns gleichwohl schneller zurück zum Delegate führt: Natürlich wissen Sie um die Bedeutung des Wortes Instanz im Kontext der Programmierung. Sinnbildlich gesprochen ist eine Instanz die Kopie einer Klasse. Wird eine Klasse instanziiert, ist damit nichts anderes gemeint, als dass von einer zunächst x-beliebigen Klasse eine Kopie (mit anderem Namen) erstellt wird. Mit dieser Kopie arbeiten Sie, während Ihnen der Zugriff auf die originäre Klasse nicht selten verwehrt bleibt. Wenn wir schon mal dabei sind: Nicht zuletzt im Hinblick auf das Vorhandensein der .NET-Klassenbibliothek ist die Tatsache, dass Klassen in der Regel nicht direkt verwendet werden können, durchaus zu verstehen (siehe dazu den folgenden Info-Kasten).
36
Aus der Art geschlagen – die Sprache C#
Besuch in einer Bibliothek In Ihrer Heimatstadt gibt es wahrscheinlich eine größere Bibliothek, die Sie als wissbegieriger Mensch womöglich (und trotz der Existenz des allgegenwärtigen World Wide Web) nutzen. Sie leihen Bücher oder schmökern in denen vor Ort. Vielleicht fertigen Sie auch Kopien einzelner Seiten, Kapitel oder Passagen an (oder lassen sich zu eigenem Schreiben inspirieren). Eines jedoch tun Sie gewiss nicht: Veränderungen am literarischen Gegenstand selbst vornehmen (schließlich wollen Sie wiederkommen dürfen).
Klassen, Instanzen, Bücher! Ihre Gedanken bewegen sich zweifelsohne in der richtigen Richtung: Wie so vieles im hochkomplexen ».NET-Gebälk« geht auch ein Delegate zunächst auf eine Klasse zurück, namentlich auf die Klasse Delegate (die sich im Namensraum System befindet). Sogleich spinnen Sie den Faden munter weiter: »Ich erstelle eine Instanz der Klasse Delegate und erhalte einen zur freien Verfügung stehenden Delegate, der dann auf Methoden und/oder Objekte verweist. Prima!« Beispielsweise und in altbekannter Manier: Delegate MeinDelegate = new Delegate();
In den Laboratorien Microsofts (vielleicht sogar auf Anraten von Herrn Hejlsberg selbst – wir wissen es nicht) wurde jedoch ein etwas anderer Weg beschritten. Ersparen wir aber dem, was enthüllt werden soll, den Trommelwirbel. Die Aufgabe der Klasse Delegate ist eine eher indirekte. Betrachtet wird Delegate nämlich nicht als Delegate-Typ, sondern als Klasse zum Ableiten von Dele-
gate-Typen, was bedeutend mehr als ein gradueller Unterschied ist. Delegate ist lediglich die Basisklasse für Delegate-Typen. Nebenbei bemerkt, existiert in der .NET-Bibliothek eine ganze Reihe von Basisklassen. Eine der bekanntesten ist die Klasse Form, mit Sitz im Namensraum System.Windows.Forms. Multicast-Delegate Im Zusammenhang mit Delegates steht die Klasse Delegate nicht allein da, leistet doch MulticastDelegate (die Sie ebenfalls im Namensraum System finden) prinzipiell dasselbe. Allerdings ist MulticastDelegate mächtiger als es Delegate ist, können in der Aufrufliste des Delegate doch gleich mehrere Elemente (auch im Sinne von einzelnen Delegates) vorhanden sein. Das alles klingt, als hätten Sie beim Programmieren die Wahl, entweder Delegate oder MulticastDelegate zu verwenden. Oberflächlich betrachtet ist das auch so. Verwenden Sie allerdings die Klasse MulticastDelegate, sitzt damit auch die Klasse Delegate mit im Boot des Projekts, wird die funktional erweiterte Klasse MulticastDelegate doch von Delegate abgeleitet – Stichwort: Vererbungshierarchie.
37
2.8
2
Die Sache mit .NET
Sie selbst können weder von der Klasse Delegate noch von MulticastDelegate ableiten bzw. eine Instanz erstellen. Das bleibt dem System sowie dem – in unserem Falle – C#-Compiler sowie diversen Tools vorbehalten. In der Deklaration eines Delegate bewirkt das Schlüsselwort delegate, dass der Compiler eine Klasse erzeugt, die von System.Delegate abgeleitet ist. Kommt MulticastDelegate ins Spiel, wird der Compiler durch das Schlüsselwort delgate zur Erzeugung einer von MulticastDelegate abgeleiteten Klasse veranlasst. Auch hier gilt: Dem Entwickler sind quasi die Hände gebunden. Das primär Erforderliche erledigt der Compiler. Ebenfalls ist es Ihnen nicht erlaubt, von einem Delegate-Typ einen neuen Typ abzuleiten (von der Möglichkeit, zu erben, ganz zu schweigen). Auf dieser Ebene kann ein Delegate-Typ nichts anderes als der einmal definierte Delegate-Typ sein. Wie also soll man es machen? Am besten richtig. Vollkommen falsch ist obiges Listing nicht, werden Delegates doch in der Tat wie auch Objekte über den hinlänglich bekannten Operator new erstellt (obwohl es sich bei Delegates streng genommen um Datenstrukturen handelt). Es gibt aber folgenden wichtigen Unterschied: Den Klassennamen Delegate suchen Sie in der Anweisung zur Erzeugung eines Delegate vergebens. Allgemein lautet die Syntax zur Erzeugung eines Delegate:
= new (<Methode>);
Auch wenn es Ihnen klar sein dürfte: steht nicht für die Klasse Delegate, sondern für den Typ des Delegate. <Methode> bezeichnet den Namen einer Methode, die – es wurde bereits erwähnt
– der Signatur des Delegate entsprechen sollte. (Umgekehrt kann man es allerdings auch betrachten, d. h., die Signatur des Delegate hat der Signatur jener Methode zu entsprechen, auf die verwiesen werden soll.) Erweitert kann der Methodenname durch den Klassennamen und den Namensraum werden. Schließlich kann die Methode, auf die verwiesen werden soll, Teil einer Klasse sein (Instanzmethode), die sich wiederum in einem ganz anderen Namensraum befindet. Natürlich drängt sich die Frage auf, wofür das Sprachmittel Delegate eigentlich gut sein soll. Kurze Antwort vorweg: für gleich mehreres. Begnügen wir uns im Weiteren jedoch mit dem, was sich wie ein roter Faden durch den Praxisteil des Buches ziehen wird: Ereignisse. Auch für die sind Delegates beinahe unentbehrlich. Im letzten Abschnitt dieser kompakten Theorie-Einführung sollen die Ereignisse ihre Schatten vorauswerfen.
38
Ereignisbehandlung
2.9
Ereignisbehandlung
Sie klicken auf einen Button, auf ein Dropdown-Listenfeld, auf einen ImageButton. Sie fokussieren mit dem Cursor ein Textfeld, setzen einen RadioButton oder eine CheckBox und so weiter. Kurzum: Im Zuge Ihrer Arbeit mit einer Windowsoder Web-Applikation lösen Sie mehr als ein Ereignis (englisch Event) mehrfach aus (oft wird auch davon gesprochen, ein Ereignis zu »feuern«) Vom Auslösen eines Ereignisses leben zumindest benutzerorientierte Programme, weniger jene, die sich beispielsweise über Stunden mit einer numerischen Berechnung beschäftigen. Ereignisse müssen jedoch 왘
abgefangen und
왘
behandelt werden.
Letzter Punkt führt zum Begriff der Ereignisbehandlung, genauer zu einer Ereignisbehandlungsroutine (englisch Event Handler, etwas eingedeutscht Eventhandler). Einen Eventhandler stellen nur ganz bestimmte, Eventsink genannte Klassen zur Verfügung. Im Praxisteil werden Ihnen einige Eventhandler begegnen. Die meisten der abkürzend auch als Handler bezeichneten Ereignisbehandlungsroutinen besitzen folgende Signatur: void Mein_EventHandler(object sender, EventArgs e);
Zwei nicht unbedingt selbsterklärende Argumente werden der Methode Mein_ EventHandler() mitgegeben: 왘
sender ist eine Referenz auf das Objekt, das das Ereignis auslöst. Nicht selten
handelt es sich bei solchen Objekten um Steuerelemente, sprich Controls. Für eine auf Grundlage von .NET entwickelte Applikation (ganz gleich ob, es eine Web- oder Standalone-Applikation ist), sind Controls ungefähr so elementar wie die Farben auf der Palette des Malers. 왘
e bezeichnet eine Referenz auf ein Objekt, das zusätzliche ereignisbeschreibende Parameter enthalten kann.
Damit hätten Sie den anstrengendsten Abschnitt des Buches hinter sich gebracht.
39
2.9
Arbeit tendiert dazu, den zur Verfügung gestellten Zeitraum auszufüllen. (Parkinson)
3
Ausgeschlafen – das Wecker-Tool
Vermutlich kennen Sie das morgendliche »Drama«: Zur viel zu frühen Stunde kreischt oder piept es auf dem Nachtschränkchen. Der Arm wandert einigermaßen unkoordiniert zum vermuteten Standort der ungebetenen Geräuschquelle. Im besten Fall trifft Ihr Finger die Stopp-Taste, im schlechtesten Fall landet das lärmende Ding auf dem Boden – wo es fleißig weitertönt. Das, was Sie einigermaßen unsanft Morpheus‘ Reich entrissen hat, wird ein mechanischer Wecker oder ein Radiowecker sein, vielleicht auch nur die Weckfunktion des allgegenwärtigen Handys oder schlicht Ihre digitale Armbanduhr (die wir noch genauer unter die Lupe nehmen werden). Ein Notebook war es mit einiger Sicherheit nicht, von einem Desktop-PC ganz zu schweigen. Der steht natürlich auch bei Ihnen auf dem Schreibtisch – der wiederum seinen Platz nicht unbedingt im Schlafzimmer hat. Doch ehrlich gestanden: Wer von uns »PC-Getreuen« hat nicht schon mal ein Nickerchen vor dem Monitor gehalten? Ich selbst bevorzugt als Student, wenn morgens um drei der verd... Debugger immer noch Fehlermeldungen spie wie der kleine Grisu das Feuer. Nun ja, dann wurde die Tastatur halt dezent zur Seite geschoben und der rauchig müde Kopf in die umständlich auf der Schreibtischplatte verschränkten Arme gelegt. Geholfen hat es nur selten (am wenigsten dem Denkvermögen), eher war die Aufwachprozedur eine auch körperlich schmerzhafte Angelegenheit, was nicht hätte sein müssen, wenn, ja wenn mich der Computer zuvor geweckt hätte. Schaffen wir für den Fall vergleichbarer Situationen Abhilfe, indem wir ein Wecker-Tool entwickeln. Abbildung 3.1 zeigt Ihnen, wie die Eingabe- und Anzeigemaske aussehen sollen. Darüber hinaus kann Ihnen das kleine Tool wertvolle Dienste beim zyklischen Lernen oder Arbeiten (als wäre Lernen nicht auch Arbeit) leisten: Stellen Sie einfach unter Einstellen der Weckzeit die Dauer der beabsichtigten »mentalen Betriebsamkeit« ein, klicken Sie den Button Stellen an, und danach geht – was auch immer – los. 41
3
Ausgeschlafen – das Wecker-Tool
Abbildung 3.1
Eingabe- und Anzeigemaske des Wecker-Tools
Wurde das Intervall im Wortsinne abgearbeitet, ist es Zeit für eine wohlverdiente Pause, nach deren Ende der Wecker erneut gestellt werden kann und so weiter. Primär geht es beim Wecker-Tool um Zeit. (Wann geht es nicht darum?) Die natürlich digitale Anzeige derselben, zumeist in der rechten unteren Ecke des Monitors (auf der Windows-eigenen Startleiste) ist für Sie wie auch für mich vollkommen selbstverständlich. Gleichwohl wird es Sie vielleicht interessieren, wie der Computer zur Darstellung der Zeit kommt, seit wann es das Feature gibt und wie es ganz früher um das Verhältnis zwischen Computer und Zeit bestellt war. Werfen wir zunächst also einen möglichst neugierigen Blick in die Vergangenheit. Lange werden wir in der allerdings nicht bleiben können.
3.1
Als der Computer die Zeit entdeckte
Geht man 387 Jahre zurück, müssten die zwei Hauptwörter in der Überschrift eigentlich ihre Plätze tauschen. Entdeckt hat nämlich nicht der Computer die Zeit, sondern eher die Zeit den Computer. Zumindest dann, wenn wir uns die legitime Freiheit erlauben, den Begriff »Computer« zunächst von seiner Fähigkeit abzukoppeln, Berechnungen auf Grundlage des binären Zahlensystems durchzuführen. Mehr als eine knackige Kaffeebeilage – Leibniz 1703 erfand der Leipziger Philosoph, Mathematiker, Physiker, Historiker, Politiker, Diplomat und Bibliothekar Gottfried Wilhelm Leibniz (1646–1716) die Darstellung von Zahlen im Dualsystem, das zur Grundlage der »digitalen Revolution« wurde.
42
Als der Computer die Zeit entdeckte
Historisch betrachtet, ist der Begriff »digitaler Computer« ebenso wenig ein »weißer Schimmel«, wie die Formulierung »analoger Computer« Unsinn ist. Anderenfalls würde der Astronom, Geograf und Orientalist Wilhelm Schickard (1592– 1635) nicht als der »Vater der Computerära« gelten. Um dazu zu werden, brauchte Schickard im Jahre 1623 im Wesentlichen lediglich diverse aus dem Uhrmacherhandwerk entliehene Zahnräder, die, intelligent ineinandergreifend, schließlich zu dem wurden, was als »rechnende Uhr« in die Geschichte technischer Erfindungen eingehen sollte. Die Maschine beherrschte zwar in der »Grundausstattung« nicht einmal das kleine Einmaleins, immerhin jedoch zwei Grundrechenarten, nämlich Addition und Subtraktion – und das sogar im sechsstelligen Bereich. Im Falle komplexerer Aufgaben wurde das Gerät um Napier‘sche Rechenstäbchen ergänzt (die Sie sich getrost als miniaturisierte Stäbe zur Anzeige des Wasserstandes vorstellen können). War plötzlich eine Glocke zu hören, dann weniger, um den Meister an den Mittagstisch zu locken, als um auf den Speicherüberlauf aufmerksam zu machen. Auch den gab es in der »rechnenden Uhr« bereits. Mehr über Schickard und sein beachtenswertes Werk erfahren Sie unter http://www.iser-uni-erlangen.de/aktuelles/ Schickard/index.html. Schauen Sie mal vorbei. Es würde ihn vielleicht gefreut haben. Ein Herr namens Kepler Schickards Erfindung gilt als der erste mechanische Rechner der Neuzeit – dem keineswegs die praktische Nutzanwendung versagt blieb. Ich bin zwar kein Wissenschaftshistoriker, allerdings physikalisch vorbelastet, weshalb ich gerne die Frage stelle, welche physikalischen Gesetze der (auch) Astronom Johannes Kepler (1571–1630) ohne die »Vier-Spezies-Maschine« (Gattungsbegriff für mechanische Rechner) hinterlassen hätte. Die hat er nämlich für seine astronomischen Berechnungen verwendet. Anscheinend mit Erfolg, können sich seither doch Generationen von Schülern der Oberstufe und Physikstudenten an den keplerschen Gesetzen (besonders dem zweiten und dritten) erfreuen – oder auch nicht ...
In den frühen 1980iger-Jahren – wir haben einen riesigen Schritt durch die Geschichte der rechnenden Maschinen gemacht – sah die Welt der Computer beträchtlich anders aus. Auch weil dank in Serie gefertigter (somit endlich erschwinglicher) Mikroprozessoren, vor allem aber durch die Arbeit des amerikanischen Elektronikingenieurs Ed Roberts (als Erfinder des ersten Tischrechners, des Altair 880; Roberts war zudem einer der ersten Arbeitgeber eines gewissen Bill Gates), der Computer zum beinahe profanen Allerweltsgerät mit eigenem Kürzel (PC), jedoch ohne integrierte Uhr geworden war. Das Manko mit der Uhr wurde im Jahre 1986 von Big Blue, sprich IBM (für International Business Machines), höchstselbst durch die Einführung des AT-Systems
43
3.1
3
Ausgeschlafen – das Wecker-Tool
korrigiert, wobei sich die Advanced Technology (AT) in weit mehr als dem Vorhandensein einer Uhr ausdrückte (es gab höhere Rechenleistung, höhere Festplattenund Diskettenkapazität etc.). Immerhin gibt es seitdem eine Zeitanzeige im PC, batteriegestützt und als Teil der Hauptplatine – ein überlebensfähiges Konzept, wie sich schnell zeigen sollte. Was es beim guten alten AT nicht gab, war ein grafisches Betriebssystem à la Windows (oder Linux oder OS2 oder, oder ...). Und damit setzen wir zum letzten Schritt hin zum modernen Personal Computer an. Erinnern Sie sich an den Begriff Echtzeit? Diese wurde im Umfeld des Garbage Collectors bereits erwähnt (als es darum ging, dass die Garbage Collection als automatische Speicherbereinigung für Echtzeitsysteme nicht unbedingt geeignet ist). Hier begegnet uns das Wort abermals, denn die in das Mainboard, also in die Hauptplatine des Personal Computers, integrierte Uhr (man spricht auch von einer Hardware-Uhr) firmiert unter dem Namen Echtzeit-Uhr, im Englischen Real Time Clock oder – bestens abgekürzt – RTC. Das »Echtzeitige« der Uhr drückt sich in dem aus, was sie misst: die physikalische Zeit nämlich – in strenger Abgrenzung zur logischen Zeit, bei der es vereinfacht gesagt um ein spezielles, auf beispielsweise verteilte Systeme abgestimmtes Zeitmodell geht, in dem stur (wissenschaftlich: monoton) »den Berg herauf« gezählt wird. Ganz gleich ob Rolex oder No-Name: Ein prinzipieller Unterschied zwischen einer digitalen Armbanduhr und einer Hardware-Uhr besteht nicht. Auch beim Timer-Chip der Hauptplatine existieren Taktgeber und Zähler, wobei der Zähler bei jedem Takt des Frequenzgebers erhöht wird. Die Bauart des Zählers ist nicht unabhängig von der des Taktgebers. Die einfache Formel könnte ungefähr so lauten: Je willkürlicher die Frequenz des Taktgebers gewählt ist, desto aufwendiger muss der Zähler gefertigt sein – was es selbstredend zu vermeiden gilt. Nach dem Anschalten des PCs übernimmt das BIOS (Basic Input/Output System), eine Art hardwarenahes Minimalprogramm, von der RTC die Uhrzeit (und das Datum). Beides wird dem Betriebssystem zur Verfügung gestellt, das seinerseits die Informationen selbstständig weiterschreibt. Ergo ist das, was Sie auf der Windows-Startleiste gelegentlich in Augenschein nehmen, nicht die von der RTC generierte Systemzeit, sondern eine kleine, wenngleich angenehme Zugabe des Betriebssystems. Besagte Zugabe wird als Software-Uhr bezeichnet, die lediglich aus einem Zähler besteht. Das Taktsignal erhält die Software-Uhr als Folge eines regelmäßigen Interrupts. Daraufhin wird vom Betriebssystem eine Interrupt-Routine ausgeführt,
44
Die Entwicklung der Bedienoberfläche
in der sich nicht nur das Weiterstellen der Software-Uhr erledigt, sondern beispielsweise auch das Prozess-Scheduling, also die Zeitplanerstellung für das Betriebssystem. Software-Uhr und Hardware-Uhr werden beim Booten des PCs über das bereits erwähnte BIOS abgeglichen, während beim Herunterfahren zuweilen ein Zurücksetzen der Hardware-Uhr auf den Wert der Software-Uhr erfolgt (übrigens mitunter auch dann, wenn es nicht unbedingt angebracht ist).
3.2
Die Entwicklung der Bedienoberfläche
Wie möchten Sie die praktische Arbeit beginnen? Mein Vorschlag: Beginnen wir mit der Festlegung der Anforderungen. Daraus soll aber kein langweiliges, die Aktualität rasch verlierendes Pflichtenheft resultieren, wohl aber Klarheit darüber, was realisiert werden soll (Abbildung 3.2 lässt ja bereits einiges vermuten). Folgendes müssen wir nun umsetzen: 왘
ein Hauptmenü (einziger Eintrag: Anwendung) 왘
왘
mit einem Submenü, dessen ebenfalls einziger Eintrag Beenden lautet
ein virtuelles Display (für das sich getrost auch »schwarzes Rechteck« schreiben ließe). Auf dem soll Folgendes angezeigt werden: 왘
Stunden, Minuten, Sekunden, im Format (Beispiel): 18:14:28.
왘
ein kurzer, prägnanter Satz, dessen Kernaussage die eingestellte Weckzeit ist. Beispiel: Sie werden um 18:30 geweckt! (siehe Abbildung 3.2).
Abbildung 3.2
Display-Anzeige, nachdem die Uhrzeit eingestellt wurde
45
3.2
3
Ausgeschlafen – das Wecker-Tool
왘
ein ebenso kurzer wie prägnanter Satz, dessen zentrale Aussage die ist, dass das Schläfchen für Sie vorbei ist; Beispiel: Die Pflicht ruft ... (siehe Abbildung 3.3).
Abbildung 3.3 왘
Display-Anzeige, nachdem die Weckzeit erreicht ist und das Wecksignal ertönt
einen Kalender, in dem Sie den Tag auswählen können, an dem Sie wünschen, geweckt zu werden. Der Kalender macht Sinn, denn stellen Sie sich vor, Sie überkommt um 23:55 Uhr die Müdigkeit (siehe Abbildung 3.4).
Abbildung 3.4
Aufgeklappter Kalender des Wecker-Tools
왘
einen Radiobutton. Genauer sind es deren zwei, gedacht zur Auswahl der Weckmelodie. Eine kleine Überschrift (Auswahl der Melodie) soll die Einstellungsmöglichkeiten einleiten.
왘
eine Schaltfläche (Stellen) zur Aktivierung des »Software-Weckers«. Das Einstellen der Weckzeit erfolgt getrennt nach Stunden und Minuten. Ein auf die Sekunde genaues Wecken erschien mir übertrieben. Ihnen steht es natürlich
46
Die Entwicklung der Bedienoberfläche
frei, das Wecker-Tool (auch) dahingehend zu erweitern. Auch hier soll eine kleine Überschrift (Einstellen der Weckzeit) die Möglichkeit zur Einstellung einleiten. 왘
eine Schaltfläche (Stop) zur Unterbrechung des Wecktons. Wird der Button betätigt, soll der Begleittext auf dem virtuellen Display ausgeblendet werden.
Abbildung 3.5 Warnmeldung in dem Fall, dass die eingestellte Weckzeit vor der Systemzeit liegt
Falls Sie versehentlich Ihr Wecker-Tool auf eine Zeit einstellen, die vor der aktuellen liegt, soll sich auch noch ein kleines Fenster mit der Meldung Möchten Sie tatsächlich in der Vergangenheit geweckt werden? öffnen. Zunächst allerdings gilt es etwas anderes einzustellen: ein neues Projekt nämlich. Spätestens an dieser Stelle sollten Sie die Entwicklungsumgebung Visual C# 2010 Express Edition geöffnet haben.
3.2.1
Anlegen eines neuen Projekts
Zu der in Abbildung 3.6 gezeigten Dialogmaske Neues Projekt gelangen Sie über Hauptmenü Datei 폷 Neues Projekt. Im Editorfeld Name geben Sie – wenn Sie mögen – Wecker_Tool ein. Wählen Sie aber vorher unter den verschiedenen Standardanwendungstypen Windows Forms-Anwendung aus! Nachdem Sie auf den Button Ok geklickt haben, wird einige Sekunden später (unter möglicherweise einigem Rumoren der Festplatte, je nachdem, wie viel Arbeitsspeicher es in Ihrem Computer gibt) das neue Projekt erstellt und unter anderem die Datei Form1.cs im Entwurf-Modus geöffnet.
47
3.2
3
Ausgeschlafen – das Wecker-Tool
Abbildung 3.6 Die Dialogmaske »Neues Projekt« zum Anlegen eines neuen Projekts in der Visual C# 2010 Express Edition
Auch wenn Ihnen das programmiertechnische Vorerzeugnis der Entwicklungsumgebung grundlegend bekannt ist, gestatten Sie mir dennoch einen »Abriss« dessen, was Sie beim Öffnen der Datei Form1.cs im Quelle-Modus erwartet. Gemessen an der Zahl der generierten Zeilen ist das zwar nicht viel, gleichwohl von elementarer Wichtigkeit. using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Linq; System.Text; System.Windows.Forms;
namespace Wecker_Tool { public partial class Form1 : Form { public Form1()
48
Die Entwicklung der Bedienoberfläche
{ InitializeComponent(); } } } Listing 3.1
Vorerzeugter Quellcode beim Erstellen des Projekts »Wecker_Tool«
Die ersten sechs Zeilen im obigen Listing bestehen aus using-Direktiven zur Einbindung grundlegender Namensräume (auf die ich später noch genauer eingehen werde). Im Block des Namensraums Wecker_Tool (der identisch mit dem Projekttitel ist) erwartet Sie sodann die öffentliche (public) Klasse Form1, ein Erbe der »Formular-Ur-Klasse« Form, als Grundlage jedweder WindowsForms-Anwendung. Eine Klasse für mehrere Teile Über das Schlüsselwort partial wurde die Deklaration der Klasse Form1 standardmäßig so realisiert, dass verschiedene Teile der Klassenimplementierung in verschiedenen Modulen oder Quelldateien der Anwendung untergebracht werden können. Warum ein solches Vorgehen mitunter sinnvoll ist? Die Erklärung würde – Sie lieben die Formulierung sicher genauso wie ich – »an dieser Stelle zu weit führen«. Wichtiger ist auch, wie es mit den partiellen Typdeklarationen weitergeht: Die einzelnen Dateien bzw. Module werden kompiliert, um die kombinierte Typimplementierung erstellen zu können. Das funktioniert wirklich, sogar mit Strukturen. Versuchen Sie dagegen, beispielsweise ein Interface mit partial zu deklarieren, ernten Sie böses »Compiler-Gegrummel«. An dem Punkt hört der Spaß nämlich auf – zumindest für den Compiler.
Weiter im Listing-Text! Als Nächstes sehen Sie die öffentliche Methode Form1(). Zu der gibt es kaum etwas anzumerken, zum Inhalt des Methodenrumpfes dagegen einiges. In ihm wird die Methode InitializeCompent() aufgerufen. Der Name ist bedingt selbsterklärend, verschweigt allerdings geflissentlich, was der Methode im Verlauf des Projekts aufgebürdet wird. Ein Einblick gefällig? Dann öffnen Sie die Datei Form1.Designer.cs durch einen Doppelklick auf den Dateinamen im Projektmappen-Explorer. Der folgende, zunächst recht übersichtliche Code dürfte Sie erwarten: private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Text = "Form1"; } Listing 3.2
Elementare Festlegungen in der Methode »InitializeComponent()«
49
3.2
3
Ausgeschlafen – das Wecker-Tool
Schnell ist es jedoch um die Übersichtlichkeit geschehen. Denn was immer Sie via Drag&Drop aus der Toolbox auf die durch Form1 dargestellte Oberfläche ziehen – initialisiert und von den Eigenschaften her belegt werden die Steuerelemente im Rumpf der Methode InitializeComponent(), was geradezu nach manuellen Eingriffen schreit. Bitte tun Sie das nicht! Wenn Sie die Initialisierungsmethode im Editor der Entwicklungsumgebung ändern, kann das gut gehen. Geht es schief, »verlieren« Sie im schlechtesten Falle sämtliche Controls – und womöglich die Nerven. Änderung des Klassennamens Bevor wir weitermachen: Ändern Sie bitte den Namen der Datei Form1.cs in cTime.cs. Ein Muss ist das zwar nicht, wohl aber ein sinnvolles Kann. Unser Formelement sollte zumindest auf das hinweisen, was später auf ihm dargestellt wird (es geht uns um Zeit). Wenn Ihnen der Name nicht gefällt, können Sie natürlich auch einen anderen wählen. Sie ändern ist den Dateinamen über 왘
einen Klick auf die Datei Form1.cs im Projektmappen-Explorer,
왘
das Drücken der rechten Maustaste und
왘
die anschließende Wahl der Option Umbenennen (im Kontextmenü).
Genauso wie beim »altehrwürdigen« Windows Explorer können Sie nun den neuen Namen editieren, womit es dann ... leider nicht getan wäre. Hat sich doch ein Fenster geöffnet, in dem Sie gefragt werden: »Möchten Sie auch alle Verweise auf das Codeelement Form1 umbenennen?« – Ja, das wollen Sie.
3.2.2
Die benötigten Steuerelemente
Abgesehen von der Nachrichtenbox, die mithilfe der Klasse MessageBox (Namensraum System.Windows.Form) realisiert wird (und die kein klassisches Steuerelement darstellt), brauchen Sie für die Realisation der Oberfläche zunächst eine Handvoll Controls. Diese werden Ihnen jetzt nicht in epischer Breite nahegebracht, wohl aber in Form von Tabelle 3.1. Bezeichnung des Steuerelements
Anzahl
Namen
MenuStrip
1
menuStrip1
Panel
3
displayPanel* panel1 panel2
Tabelle 3.1
50
Zur Entwicklung der Anzeige- und Bedienoberfläche erforderliche Steuerelemente
Die Entwicklung der Bedienoberfläche
Bezeichnung des Steuerelements
Anzahl
Namen
DateTimePicker
1
dateTimePicker1
Label
5
label1 label2 label3 message_label* static_clocklabel*
RadioButton
2
melodie1_radioButton* melodie2_radioButton*
Button
2
stellen_Button* stop_Button*
NumericUpDown
2
hours_numericUpDown* minutes_numericUpDown*
Tabelle 3.1
Zur Entwicklung der Anzeige- und Bedienoberfläche erforderliche Steuerelemente
Die Bezeichnung der Steuerelemente finden Sie in der Toolbox der Entwicklungsumgebung (Registerkarte Standard) wieder. Jedem im Projekt verwendeten Control wird vom Entwicklungssystem unter der Eigenschaft Name ein Default-Wert zugewiesen, der sich meist aus dem Namen des Controls, ergänzt um eine Ziffer, zusammensetzt. Beispielsweise heißen sie label1, label2, label3. Demnach geschieht hier nichts weiter als eine langweilige Nummerierung. Den Default-Wert können Sie beibehalten, Sie können ihn aber auch über das Eigenschaften-Fenster ändern. Das habe ich – teilweise – getan: In der Tabelle sind in der Spalte Namen jene Einträge »made by author«, die nicht mit einer Ziffer enden. Beispiel: minutes_numericUpDown. Zusätzlich wurden die Namen noch mit einem Sternchen markiert (das natürlich weder im Eigenschaften-Fenster noch im Programmcode auftauchen wird – eine Fehlermeldung weniger). Abbildung 3.7 zeigt Ihnen die Anordnung der benötigten Steuerelemente in der Entwurfsansicht. Zählen Sie die abgebildeten Controls ruhig ab, um sie mit der Summe der Steuerelemente in der Tabelle zu vergleichen. Sie stimmt eh nicht überein – aus folgendem Grund: Der Fokus in Abbildung 3.7 steht auf dem displayTime-Control. Hinter dem verbergen sich noch – unsichtbar – static_clocklabel und message_label. static_clocklabel realisiert auf GUI-Ebene (Graphical User Interface) die Darstellung der Uhrzeit, message_label die Anzeige der kleinen Begleittexte (Die Pflicht ruft!).
51
3.2
3
Ausgeschlafen – das Wecker-Tool
Abbildung 3.7
Oberfläche des Wecker-Tools in der Entwurfsansicht
Nachdem Sie die Controls auf dem Formular angeordnet und die Eigenschaften über das Eigenschaften-Fenster mit den entsprechenden Werten versehen haben, geht es ans Programmieren.
3.2.3
Ein EventHandler für das Beenden der Anwendung ...
... sollte wohl eher am Ende der Programmierung stehen. Das tut er aber nicht, weil das Theoriekapitel mit einigen einführenden Bemerkungen über Ereignisbehandlung abgeschlossen wurde. Zwischenzeitlich bei der Programmierung der Logik angekommen, machen wir beim Eventhandling einfach weiter. Abgesehen davon, brauchen wir bereits jetzt, wo noch keine einzige zeitliche Aspekte betreffende Codezeile geschrieben ist, eine Ereignisbehandlungsroutine. Die soll dafür sorgen, dass Ihre Anwendung »sauber«, also über den Submenüeintrag Beenden, zu schließen ist. Die Codedatei cTime.cs um eine solche Methode zu ergänzen, ist eine wahre Herkules-Aufgabe: Sie rufen cTime.cs im Entwurf-Modus auf, klicken zweimal im menuStrip1-Control auf den Eintrag Beenden und ... nichts weiter: Wie von Geisterhand öffnet sich cTime.cs, ergänzt um den gewünschten Eventhandler. In diesem entdecken Sie die Argumente e und sender wieder, leider jedoch keine Logik, die das eigentliche Schließen der Anwendung übernimmt. Was der Eventhandler darstellt, ist lediglich ein Rahmen, dessen Inhalt als Konsequenz eines Ereignisses auszuführen ist. Hier kommt uns Close() als Member der »Königsklasse« Form (durch die ein Formular als Grundlage einer Benutzeroberfläche erst dar-
52
Die Entwicklung der Bedienoberfläche
stellbar wird – nicht vergessen!) wie gerufen. Der Aufruf der Methode (im Eventhandler beendenToolStripMenuItem_Click()) bewirkt das Schließen des Formulars – hoffentlich. Nein, ganz gewiss, doch sollten wir uns durch ein Voranstellen des C#-Schlüsselwortes this absichern, verweist das beliebte Keyword doch auf die aktuell verwendete Instanz (der Klasse Form). Somit schreiben Sie: private void beendenToolStripMenuItem_ Click(object sender, EventArgs e) { this.Close(); }
3.2.4
Implementierung und Test der Zeitanzeige
Kein Wecker ohne Anzeige der Uhrzeit. Ihr Wecker-Tool bildet da natürlich keine Ausnahme. Wie das Betriebssystem an die Uhrzeit kommt, konnten Sie einige Seiten zuvor lesen. Nun sollte auch die Anwendung »Wecker-Tool« an Informationen zur Zeit kommen. Mit einer Klasse? Leider nicht. Eher eine Struktur führt uns zum Ziel, namentlich die DateTime-Struktur (im Namensraum System), die schon vom Namen her kein Geheimnis aus ihrer Aufgabe macht. Die Struktur selbst ist einigermaßen nutzlos, nicht aber ihre vornehmste Eigenschaft Now, die ein Objekt der Struktur abruft. Dieses Objekt ist auf Datum und Zeit des Betriebssystems, somit auf die Software-Uhr, festgelegt. Das ist gut, wenngleich für sich genommen zumindest hier nicht zielführend, fehlt doch eine weitere Eigenschaft, die beispielsweise die Stunde liefert. Die Eigenschaft heißt auch genauso wie das, was ermittelt werden soll: Hour. Eine Struktur und zwei Eigenschaften, verknüpft mit zwei Punktoperatoren, und das Ganze sieht dann folgendermaßen aus: DateTime.Now.Hour
Analog die Minuten: DateTime.Now.Minutes
Und schließlich die Sekunden: DateTime.Now.Second
Der Typ der von den Eigenschaften Hour, Minutes und Second bereitgestellten Informationen ist für die Übergabe an ein Label-Control eher ungeeignet. Hinzu kommt das für die Darstellung der Zeit definierte Format (siehe Abschnitt 3.2). Die Typ- und Formatfrage beantworten wir in einer längeren Codezeile: string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second);
53
3.2
3
Ausgeschlafen – das Wecker-Tool
Was sich in diesem Einzeiler abspielt, dürfte Ihnen vermutlich klar sein. Falls nicht, ist hier die Erklärung: Die Klasse Convert (aus dem Namensraum System) konvertiert einen Basistyp in einen anderen Basistyp, der in unserem Falle string heißt. Dabei hilft der Klasse ihre Methode ToString(), deren Argument aus der Verknüpfung von Stunden-, Minuten- und Sekundenanzeige einschließlich zweier Doppelpunkte (das ist unser Zeitformat) besteht. Sie sehen das »Monster« von Argument hier nochmals explizit hingeschrieben: DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now. Second
Nun haben wir eine gebrauchsfähige Zeitanzeige – mit der wir nicht wissen, wohin. Die in dtime gespeicherte Zeitanzeige übergeben wir dem static_clocklabel genannten Label-Control zur Anzeige. Das funktioniert nur unter Einbeziehung der Label-Eigenschaft text, durch die die Text-Eigenschaft des LabelSteuerelements festgelegt oder abgerufen wird. Wie sieht das in strenger C#-Notation aus? So: static_clocklabel.Text = dtime;
Damit wäre die digitale Zeitanzeige implementiert. Hier sehen Sie noch einmal den vollständigen (Mini-)Code: string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second); static_clocklabel.Text = dtime; Listing 3.3 Implementierung der digitalen Zeitanzeige über die »DateTime«-Struktur sowie das »static_clocklabel«
Testen können Sie die Zeitanzeige natürlich auch. Dazu gibt es zwei Möglichkeiten: 왘
Sie »packen« obigen Zweizeiler in den Rumpf der öffentlichen Methode cTime().
왘
Sie generieren den EventHandler cTime_Load_1().
Es wäre schön, wenn Sie sich für die zweite Möglichkeit entscheiden würden, cTime_Load() benötigen wir nämlich eh. Den Handler zu erzeugen ist darüber hi-
naus eine Angelegenheit von Sekunden: Klicken Sie zweimal im Entwurf-Modus der Datei cTime.cs auf das Formular (jedoch bitte nicht auf ein Control!), und die Methode wird erzeugt. In diese »packen« Sie bitte (vorübergehend!) den besprochenen Zweizeiler zur Zeitanzeige: private void cTime_Load(object sender, EventArgs e) {
54
Die Entwicklung der Bedienoberfläche
string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second); static_clocklabel.Text = dtime; } Listing 3.4 Load()«
Bereitstellung der Zuweisungen zur Zeitanzeige im EventHandler »cTime_
Starten Sie die Anwendung mit (Shortcut (F5)) oder ohne Debugging ((Strg)+(F5)). Beim erstmaligen Laden wird zuerst alles ausgeführt, was sich im Rumpf des Eventhandlers cTime_Load() befindet. Versuchen Sie es! Das virtuelle Display des Wecker-Tools dürfte nicht mehr dunkel bleiben. Die Sache hat jedoch einen Haken.
3.2.5
Von der Zeitanzeige zur Implementierung einer Uhr
Berechtigterweise wird sich Ihre Begeisterung in Grenzen halten, hat sich die Zeitanzeige doch tatsächlich als leider einmalige Angelegenheit entpuppt. Man könnte auch schreiben: Die Uhr steht. Hier einen Ansatz zu finden, ist leichter als dessen Umsetzung in einem Programm. Sie müssen erreichen, dass das, was Sie zu Testzwecken im Rumpf des Eventhandlers cTime_Load()untergebracht haben, im Sekundentakt angestoßen, sprich, zur Ausführung gebracht wird. Dazu brauchen Sie eine Klasse, die den Takt angibt. Genauer gesagt muss diese Klasse einen Mechanismus bereitstellen, der eine Methode im Sekundenintervall ausführt. Ein Besuch im Namensraum System.Threading lässt Sie schnell fündig werden: Dort residiert die Klasse Timer, von der natürlich zunächst eine Instanz gebildet werden muss: static Timer static_clockTimer = new Timer();
In welchem Intervall (ausgedrückt in Millisekunden) die noch unbekannte Methode ausgeführt wird, legen Sie mit der Eigenschaft Interval der Klasse Timer fest: static_clockTimer.Interval = 1000;
Jetzt ist es Zeit für ein Ereignis, das dann eintritt, wenn die angegebene Anzahl von Millisekunden erreicht ist: static_clockTimer.Tick += new EventHandler(static_clockTimer_Check);
Danach schlägt die Sekunde der Methode static_clockTimer_Check(), die – das soll bereits hier verraten werden – nichts anderes enthält als das, was Sie wenige
55
3.2
3
Ausgeschlafen – das Wecker-Tool
Abschnitte zuvor zum Zwecke des Testens in den Rumpf des Eventhandlers cTime_Load()eingefügt haben: private void static_clockTimer_Check(object sender, EventArgs e) { string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second); static_clocklabel.Text = dtime; } Listing 3.5 So stellen Sie die Zuweisungen zur Zeitanzeige im Eventhandler »static_ clockTimer_Check()« bereit.
Jetzt fehlt uns noch eine Art »Zündschlüssel« für den static_clockTimer, den wir in der Methode Start() der Timer-Klasse finden. Demnach: static_clockTimer.Start();
Somit haben wir die erforderlichen Codefragmente beisammen: static Timer static_clockTimer = new Timer(); private void cTime_Load(object sender, EventArgs e) { static_clockTimer.Interval = 1000; static_clockTimer.Tick += new EventHandler(static_clockTimer_Check); static_clockTimer.Start(); } private void static_clockTimer_Check(object sender, EventArgs e) { string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second); static_clocklabel.Text = dtime; } Listing 3.6 Implementierung einer funktionsfähigen Uhr unter Einbeziehung einer Instanz der »Timer«-Klasse (»static_clockTimer«)
56
Die Entwicklung der Bedienoberfläche
3.2.6
Zahlen für das »NumericUpDown«-Control
Vom Auf-Ab-Steuerelement (auch mit Windows-Drehfeld ließe sich NumericUpDown-Control übersetzen – sogar laut Microsoft) existieren auf der Oberfläche zwei: eines für die Auswahl der Stunden (hours_numericUpDown), das andere für die Minuten (minutes_numericUpDown). Per Default ist der minimale Wert der Controls auf 0, der maximale auf 100 festgelegt. Das können Sie nicht nur ändern, das sollten Sie auch, denn der Tag hat bekanntlich 24 Stunden (was so nicht ganz stimmt) und die Minute bekanntlich 60 Sekunden. Zwei Möglichkeiten zur Änderung stehen zur Wahl: 왘
im Eigenschaften-Fenster über die Festlegung der Werte für die Eigenschaften Minimum und Maximum
왘
per Programmierung
Programmieren möchten Sie mithilfe dieses Buches, also werden wir die Werte besagter Eigenschaften »manuell« im Code festlegen. Die Eigenschaften Minimum und Maximum sind in erster Linie Member der Klasse NumericUpDown (Namensraum: Systems.Windows.Forms), was sogleich den Punkto-
perator als Verknüpfung zwischen Objektname und Eigenschaft auf den Plan ruft. Fangen wir mit hours_numericUpDown an. Die Stunden werden von 0 bis 23 heraufgezählt, und dekrementiert wird von 23 bis 0: hours_numericUpDown.Maximum = 23; hours_numericUpDown.Minimum = 0;
Für minutes_numericUpDown wird ein Wertebereich von 0 bis 59 gewählt: minutes_numericUpDown.Maximum = 59; minutes_numericUpDown.Minimum = 0;
Derart codiert, zeigen beide Windows-Drehfelder bei Ausführung des Programms 0 an. Obgleich es nicht zwingend ist (schließlich lassen sich nun die zur Weckzeitfestlegung nötigen Werte einstellen, ohne dass Sie sich durch unnötige klicken müssen), können wir das besser. Indem wir die Eigenschaft Value nutzen, wird im Drehfeld ein Wert abgerufen, der beim Start des Programms anstelle der unspektakulären 0 zu sehen ist. Es fragt sich bloß, welcher das ist! Genau genommen sind es zwei, nämlich: Stunde und Minute – im Sinne einer einmaligen Uhrzeit, die Ihnen in den Auf-Ab-Steuerelementen angezeigt wird. Gut, es war kein glücklicher, aber den Fall hatten wir bereits (siehe Abschnitt 3.2.4 – die »stehende Uhr«). Hier genügt uns DateTime.Now.Hour bzw. DateTime.Now.Minute für das, was der Eigenschaft Value zuzuweisen wäre:
57
3.2
3
Ausgeschlafen – das Wecker-Tool
hours_numericUpDown.Value = DateTime.Now.Hour; minutes_numericUpDown.Value = DateTime.Now.Minute;
Unterbringen ließen sich die sechs Zeilen Code in der Methode cTime(). Die Reihenfolge der Einzeiler können Sie frei wählen. Mein Vorschlag wäre: public cTime() { InitializeComponent(); hours_numericUpDown.Value = DateTime.Now.Hour; hours_numericUpDown.Maximum = 23; hours_numericUpDown.Minimum = 0; minutes_numericUpDown.Value = DateTime.Now.Minute; minutes_numericUpDown.Maximum = 59; minutes_numericUpDown.Minimum = 0; } Listing 3.7
3.2.7
Intervall- und Zeitbelegung der beiden Windows-Drehfelder
Mehr Lärm als Melodie – »tada« und »ir«
Nachdem ich die circa vierzig WAV-Dateien unter C:\WINDOWS\Media in Augenschein genommen hatte, verging mir schnell die Lust, nach zwei geeigneten Klängen für das Wecker-Tool zu suchen – möglichst schrecklich sollte es allerdings schon klingen. Auch deshalb wurde die Entscheidung schnell getroffen: tada.wav und ir_inter.wav. Das sind also die »Melodien«, die Ihnen zugemutet werden. In einer Endlosschleife abgespielt, hört sich tada.wav ein wenig an wie die musikalische Untermalung einer sich unaufhörlich auf die Erde zu bewegenden Sonne. Apokalypse in Zeiten von .NET. Und was ist mit ir_inter.wav? Solange sind die Zeiten noch nicht her, als sich am Ende des vinylhaltigen Tonträgers die Diamantnadel in einer der Rillen verfing. Sollte in dieser Rille ein pausbackiger Bläser eigentlich abschließend noch einmal in sein Horn stoßen, so tut er es bei ir_ inter.wav gleich mehrfach. Auch davon sollten Sie wach werden. Zu den Melodien gäbe es Alternativen, zum WAV-Format dagegen nicht, denn die für die Wiedergabe zuständige Klasse SoundPlayer versteht nichts anderes. Sie finden sie im Namensraum System.Media – den das Projekt noch nicht kennt. Exakter formuliert, geht es darum, die im Namensraum System.Media vorhandenen Typen (in unserem Falle eben jene Klasse SoundPlayer) für das Projekt Wecker-Tool zuzulassen. Anderenfalls »droht« Qualifizierung (an sich nichts Schlechtes), was kurz gesagt bedeutet, dass dem C#-Compiler deutlich gemacht werden muss, dass diese und keine andere Klasse gemeint ist.
58
Die Entwicklung der Bedienoberfläche
Als Sie das Projekt angelegt haben, konnten Sie im Kopf der Datei Form1.cs acht using-Direktiven entdecken, mit denen die Zulassung acht elementarer Namensräume bewirkt wird – zu denen System.Media leider nicht zählt. Das ist Pech, wenngleich kein Drama und darüber hinaus schnell behoben: Ergänzen Sie einfach in der Datei cTime.cs die Liste der using-Direktiven um eine weitere und letzte, nämlich: using System.Media;
Fertig! Übrigens: Im übertragenen Sinne fällt System.Media eher in die Kategorie »mittelprächtige Studentenbude«, existieren im Raum neben der SoundPlayerKlasse doch lediglich noch die Klassen SystemSound und System.Sounds. Zwei Instanzen der Klasse SoundPlayer werden benötigt: eine für Melodie 1, die andere für ... nein, das schreibe ich jetzt nicht. Stattdessen fügen Sie jetzt bitte in den Block der Klasse cTime den folgenden Zweizeiler ein: SoundPlayer sound1
= new SoundPayer(@"C:\Projekte\Wecker_Tool\ Sounds\tada.wav");
SoundPlayer sound2
= new SoundPlayer(@"C:\Projekte\Wecker_Tool\ Sounds\ir_inter.wav");
Listing 3.8
Zwei Objekte vom Typ »SoundPlayer« werden erzeugt.
Wie unschwer zu erkennen ist, handelt es sich bei dem, was den Konstruktoren an Wert übergeben wird, um die absoluten Pfade hin zu den Sounddateien tada.wav und ir_inter.wav. Wenn Sie das allzu wörtlich nehmen, womöglich noch ohne sich die beiden Initialisierungen genauer angesehen zu haben, ist Ihnen die nächste Beschwerde des Debuggers sicher. Bei der kann man nur schreiben: Willkommen zurück in der Welt von C. Und da Teile der Pfade nicht als Escape-Sequenzen interpretiert werden, muss ein spezielles Zeichen her. Erst dann ist für den Debugger klar: Es geht um Zeichenfolgenliterale (Anführungszeichen reichen in diesem Fall nicht). Dafür müssen Sie den Pfaden jeweils ein @-Zeichen voranstellen. Unter Laufzeitbedingungen zu testen gibt es hier nichts, fehlt doch die Implementierung für das Stellen des Weckers ebenso wie das den Weckton schlussendlich auslösende Ereignis, dessen Ursprung gleichwohl nicht der Button Stellen ist. Einen neuen Ordner im Projektverzeichnis anlegen Auch aus einem anderen Grunde können Sie nicht testen. Im Projektverzeichnis des Wecker-Tools existiert kein Ordner sounds. Vier Schritte schaffen Abhilfe:
59
3.2
3
Ausgeschlafen – das Wecker-Tool
1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Stammverzeichnis. 2. Wählen Sie im Kontextmenü zuerst Hinzufügen, anschließend Neuer Ordner. 3. Benennen Sie den neuen Ordner um. 4. Kopieren Sie die Dateien tada.wav und ir_inter.wav in den neu erstellten Ordner.
3.2.8
In einem Aufwasch – Einstellen der Weckzeit und Auslösen des Wecktons
Was sollte sich nach Betätigen des Buttons Stellen programmintern zunächst abspielen? Eine im Sekundenintervall durchgeführte Prüfung, ob es eine Identität zwischen Systemzeit (auch hinsichtlich des Datums) und eingestellter Weckzeit gibt (die es natürlich niemals geben würde, läge die eingestellte Weckzeit vor der Systemzeit). Mit anderen Worten: Sie brauchen noch einmal die Timer-Klasse. Fügen Sie bitte unterhalb der Zeile, in der die erste Instanz der Timer-Klasse erstellt wurde, folgenden Code zur erneuten Instanziierung der Klasse Timer ein: static Timer clockTimer = new Timer();
Einen Eventhandler für den Button Stellen gibt es in der Datei cTime.cs nicht. Noch nicht, denn nachdem Sie im Entwurf-Modus derselben Datei zweimal auf den Button Stellen geklickt haben, ist der Ereignishandler vorhanden: private void stellen_button_Click(object sender, EventArgs e){}
Weiter oben haben Sie sich zwecks Implementierung einer (zunächst »statischen«) Uhrzeit der Struktur DateTime bedient. Auch die brauchen wir ein zweites Mal, allerdings in erweiterter Form, geht es doch darum, aus dem Kalender (dateTimePicker1) das Datum zu holen sowie aus den beiden Windows-Drehfeldern hours_numericUpDown und minutes_numericUpDown die eingestellte Weckzeit abzurufen. So sieht der Code aus, den Sie dazu in den Rumpf des Eventhandlers stellen_ button_Click() einfügen: DateTime dt = new DateTime(dateTimePicker1.Value.Year, dateTimePicker1.Value.Month, dateTimePicker1.Value.Day, Convert.ToInt32(hours_numericUpDown.Value), Convert.ToInt32(minutes_numericUpDown.Value), Convert.ToInt32(DateTime.Now.Second)); Listing 3.9
60
Ein »Kessel Buntes« für den Konstruktor »DateTime()«
Die Entwicklung der Bedienoberfläche
An der Initialisierung ist nichts Weltbewegendes – bis auf eine Kleinigkeit: Wir müssen konvertieren. Ganz gleich, auf welcher Zeile der Überladungsliste Sie Ihren Finger stoppen, das Format der dem Konstruktor übergebenen Werte ist fast ausschließlich Int32 oder – the next generation of personal computers – Int64. Dumm gelaufen, denn bei diesen Werttypen spielt weder hous_numericUpDown noch minutes_numericUpDown so ohne Weiteres mit. Nun geht es ans Vergleichen, an ein if-else-Konstrukt und an das »In-SchwungBekommen« des zweiten Timers. Denn was wir wollen, ist klar: 왘
Wir wollen überprüfen, ob die eingestellte Weckzeit (inklusive Datum) kleiner gleich der Systemzeit ist.
왘
Ist die eingestellte Weckzeit größer als die Systemzeit, soll 왘
das Intervall des Timers festgelegt werden,
왘
der Timer auf einen Eventhandler bezogen werden und
왘
der Timer gestartet werden.
Übersetzt in C#-Notation, sehen obige Anforderungen wie folgt aus: if (dt <= DateTime.Now) { MessageBox.Show("Möchten Sie tatsächlich in der Vergangenheit geweckt werden?"); } else { clockTimer.Interval = 1000; clockTimer.Tick += new EventHandler(clockTimer_check); clockTimer.Start(); } Listing 3.10
Alles eine Frage der Zeit ...
Der Code kann unterhalb der Instanziierung der Struktur DateTime in den Rumpf des Eventhandlers stellen_button_Click() eingefügt werden, der damit komplett wäre: private void stellen_button_Click(object sender, EventArgs e) { DateTime dt = new DateTime(dateTimePicker1.Value.Year, dateTimePicker1.Value.Month, dateTimePicker1.Value.Day, Convert.ToInt32(hours_numericUpDown.Value), Convert.ToInt32(minutes_numericUpDown.Value), Convert.ToInt32(DateTime.Now.Second));
61
3.2
3
Ausgeschlafen – das Wecker-Tool
if (dt <= DateTime.Now) { MessageBox.Show("Möchten Sie tatsächlich in der Vergangenheit geweckt werden?"); } else { clockTimer.Interval = 1000; clockTimer.Tick += new EventHandler(clockTimer_check); clockTimer.Start(); } } Listing 3.11
Überschaubar – Ereignisbehandlung in der Methode »stellen_button_Click()«
Nun fehlt noch die Ereignisbehandlungsroutine clockTimer_check(), deren primäre Aufgabe das Auslösen des Wecksignals in Abhängigkeit von der gewählten Melodie ist. Über ein weiteres if-else-Konstrukt werden die Systemzeit und die Weckzeit miteinander verglichen. Schon wieder! Nur kommt es hier (abgesehen davon, dass beides vom Datentyp string sein sollte) zudem auf die Identität der Formate (Beispiel: 11:40) an. Zunächst erhalten wir die Systemzeit: string systemzeit = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute);
Nicht viel anders sieht es bei der Weckzeit aus: string weckzeit = Convert.ToString(hours_ numericUpDown.Value + ":" + minutes_numericUpDown.Value );
Als Nächstes übergeben wir der Text-Eigenschaft des message_label-Controls die Information zur gewählten Weckzeit: message_label.Text = "Sie werden um " + weckzeit + " geweckt!";
Ist die Weckzeit erreicht, erhält die Text-Eigenschaft als Wert einen neuen Text: message_label.Text = "Die Pflicht ruft...";
Jetzt gilt es zu klären, welcher der nervtötenden Klänge ausgewählt wurde. Abhängig davon kommt entweder sound1 oder sound2 zur Ausführung. Genauer gesagt ist es die Methode PlayLooping() als Member der Klasse SoundPlayer, die für das unendliche Abspielen der Dateien sorgt: if (melodie1_radioButton.Checked) { sound1.PlayLooping(); }
62
Die Entwicklung der Bedienoberfläche
else { sound2.PlayLooping(); }
Hier sehen Sie die Implementierung von Zeitvergleich, Textdarstellung und endlosem Abspielen in der Zusammenfassung: if (systemzeit == weckzeit) { message_label.Text = "Die Pflicht ruft..."; if (melodie1_radioButton.Checked) { sound1.PlayLooping(); } else { sound2.PlayLooping(); } } Listing 3.12 Implementierung von Zeitvergleich, Textdarstellung und endlosem Abspielen der gewählten Melodie
Der Eventhandler clockTimer_check() sieht vollständig so aus: private void clockTimer_check(object sender, EventArgs e) { string systemzeit = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute); string weckzeit = Convert.ToString(hours_numericUpDown.Value + ":" + minutes_numericUpDown.Value ); message_label.Text = "Sie werden um " + weckzeit + " geweckt!"; if (systemzeit == weckzeit) { message_label.Text = "Die Pflicht ruft..."; if (melodie1_radioButton.Checked) { sound1.PlayLooping(); } else {
63
3.2
3
Ausgeschlafen – das Wecker-Tool
sound2.PlayLooping(); } } } Listing 3.13 Implementierung der Auslösung des Wecktons im Ereignishandler »clockTimer_check()«
3.2.9
Stop and Go – das Beenden des Wecktons
Ausgehend vom Eventhandler stop_button_Click(), den Sie sicher schon erstellt haben (er sorgt für den Button Stop und das, was man mit ihm anstellen kann), ist das folgende Listing weitgehend selbsterklärend. Das mit dem »selbsterklärend« ist eine einigermaßen beliebte Phrase, die ich aber nicht deshalb verwende, weil mir zwischenzeitlich die Freude am Schreiben vergangen wäre, sondern weil ich Ihnen im letzten Abschnitt (Abschnitt 3.3, »Hätten Sie’s gewusst?«) ein kleines Rätsel an die Hand geben möchte, das genau diesen Code betrifft. In dem Code geht es nicht um die Zeile Message_label.Text = ""; , die trivialerweise dafür sorgt, dass nach Betätigung des Buttons Stop der Text Die Pflicht ruft ... vom virtuellen Display des Wecker-Tools verschwindet. Schließlich wird der Eigenschaft Text des Label-Steuerelements message_label nichts weiter als ein Leerstring zugewiesen. Sehen Sie sich zunächst den Code zum Beenden des Wecktons an: private void stop_button_Click(object sender, EventArgs e) { message_label.Text = ""; clockTimer.Stop(); if (melodie1_radioButton.Checked) { sound1.Stop(); } else { sound2.Stop(); } } Listing 3.14
64
Code zum Beenden des Wecktons im Eventhandler »stop_button_Click()«
Die Entwicklung der Bedienoberfläche
Damit wäre die Codierung der Logik für das Wecker-Tool erledigt. Da wir den Code Stück für Stück entwickelt haben, ist es, so denke ich, in Ihrem Sinne, wenn Ihnen der Code noch einmal komplett präsentiert wird: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Media; namespace Wecker_Tool { public partial class cTime : Form { SoundPlayer sound1 = new SoundPlayer(@"C:\Projekte\Wecker_Tool\ Sounds\tada.WAV"); SoundPlayer sound2 = new SoundPlayer(@"C:\Projekte\Wecker_Tool\ Sounds\ir_inter.WAV"); static System.Windows.Forms.Timer static_clockTimer = new System.Windows.Forms.Timer(); static System.Windows.Forms.Timer clockTimer = new System.Windows.Forms.Timer(); private void cTime_Load(object sender, EventArgs e) { static_clockTimer.Interval = 1000; static_clockTimer.Tick += new EventHandler(static_clockTimer_check); static_clockTimer.Start(); } private void static_clockTimer_check(object sender, EventArgs e) { string dtime = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second); static_clocklabel.Text = dtime; }
65
3.2
3
Ausgeschlafen – das Wecker-Tool
public {
cTime()
InitializeComponent(); hours_numericUpDown.Value = DateTime.Now.Hour; hours_numericUpDown.Maximum = 23; hours_numericUpDown.Minimum = 0; minutes_numericUpDown.Value = DateTime.Now.Minute; minutes_numericUpDown.Maximum = 59; minutes_numericUpDown.Minimum = 0; } private void stellen_button_Click(object sender, EventArgs e) { DateTime dt = new DateTime(dateTimePicker1.Value.Year, dateTimePicker1.Value.Month, dateTimePicker1.Value.Day, Convert.ToInt32(hours_numericUpDown.Value), Convert.ToInt32(minutes_numericUpDown.Value), Convert.ToInt32(DateTime.Now.Second)); if (dt <= DateTime.Now) { MessageBox.Show("Möchten Sie tatsächlich in der Vergangenheit geweckt werden?"); } else { clockTimer.Interval = 1000; clockTimer.Tick += new EventHandler(clockTimer_check); clockTimer.Start(); } } private void clockTimer_check(object sender, EventArgs e) { string systemzeit = Convert.ToString(DateTime.Now.Hour + ":" + DateTime.Now.Minute); string weckzeit = Convert.ToString(hours_numericUpDown.Value + ":" + minutes_numericUpDown.Value ); message_label.Text = "Sie werden um " + weckzeit + " geweckt!";
66
Die Entwicklung der Bedienoberfläche
if (systemzeit == weckzeit) { message_label.Text = "Die Pflicht ruft..."; if (melodie1_radioButton.Checked) { sound1.PlayLooping(); } else { sound2.PlayLooping(); } } } private void stop_button_Click(object sender, EventArgs e) { message_label.Text = ""; clockTimer.Stop(); if (melodie1_radioButton.Checked) { sound1.Stop(); } else { sound2.Stop(); } } private void beendenToolStripMenuItem_Click(object sender, EventArgs e) { this.Close(); } } } Listing 3.15
Die komplette codemäßige Umsetzung der Anforderung
67
3.2
3
Ausgeschlafen – das Wecker-Tool
3.3
Hätten Sie’s gewusst?
Im Rumpf des Handlers stop_button_Click() wird zweimal die Methode Stop() bemüht: einmal, um den Timer clockTimer zu stoppen, das andere Mal, abhängig von der Wahl der Melodie, um das Abspielen derselben zu beenden. Dass es sich bei Stop() um zwei Methoden handelt, darum geht es hier nicht, wohl aber um drei Fragen: 왘
Was würde geschehen, wenn der Timer clockTimer nicht mittels der Methode Stop() beendet würde?
왘
Was würde geschehen, wenn die beiden Soundplayer (sound1 oder sound2) nicht gestoppt würden?
Die dritte Frage ist die interessanteste: 왘
Ist die Reaktion des Programms auf einen nicht gestoppten Timer dieselbe wie auf einen nicht gestoppten Soundplayer?
Der Sache wäre durch Auskommentierung (// oder /**/) der betreffenden Codezeilen und anschließendes Starten des Programms natürlich leicht auf die Spur zu kommen. Mogeln gilt allerdings nicht. Die Antwort finden Sie auch nicht einzig im Eventhandler des Stop-Buttons. Besser, Sie rekapitulieren das Zusammenspiel der einzelnen Methoden (es sind ja nicht viele). Trotzdem: Es ist nicht wirklich schwer, auf die Antworten zu kommen, denn nach diesem Praxisbeispiel soll es schließlich ein weiteres geben. Und auch bei dem hätte ich Sie gerne dabei.
68
Auch wenn die Biene einen gestreiften Rücken hat, ist sie noch lange kein Tiger. (Chinesisches Sprichwort)
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Wo findet sich das schon im täglichen Alltagserleben? Regelmäßig, in zwei Dimensionen angeordnete schwarze Quadrate, die vermeintlich mit Punkten ausgestattet sind, die auf den Kreuzungen der frei gebliebenen Streifen liegen. Und nicht nur dort: Selbst am Rand, wo die vertikalen Streifen des Gitters auf einen horizontalen Streifen bzw. wo horizontale Streifen auf einen vertikalen Streifen treffen, sind – wenngleich deutlich schwächer – Punkte erkennbar. Das Dumme an der Sache? Auf dem Objekt selbst existiert kein einziger Punkt. Und nehmen Sie einen solchen Punkt länger ins Visier, verschwindet er (siehe Abbildung 4.1).
Abbildung 4.1
Das Hermann-Gitter
Kennen Sie einen Ort, an dem obige Seherfahrung regelmäßig ohne Ihr Wollen auf Sie wartet? Dann schreiben Sie mir!
4.1
Die Entdeckung der geheimnisvollen Punkte
Auch Physiologieprofessor Ludimar Hermann (1838–1914) wäre vielleicht nicht auf das seltsame Phänomen gestoßen, wenn er mit anderem als den Naturwissenschaften sein Geld verdient hätte. Irgendwann im Jahre 1870 blätterte er, warum auch immer, in der deutschen Übersetzung der Tyndall’schen Vorlesungen über den Schall.
69
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Abbildung 4.2
Der Physiologe Ludimar Hermann
Auf Seite 169 stieß er auf die Cladni’schen Klangfiguren, sechsundzwanzig an der Zahl, regelmäßig neben- und übereinander angeordnet, und jede der Figuren war vor einem schwarzen quadratischen Hintergrund abgebildet. Der war in diesem Fall der springende Punkt, und Punkte (wenngleich verwaschene) waren auch das, was Hermann an den Kreuzungen der weißen Streifen sah – zu seiner wahrscheinlichen Freude. An dieser Stelle aber sollte er am besten selbst zu Wort kommen (Auszug aus »Eine Erscheinung des simultanen Contrastes« von Ludimar Hermann): »Die Erscheinung sieht ein jeder, der darauf aufmerksam gemacht wird; für meine Augen scheint sie ganz besonders intensiv zu sein, denn mir erschien sie von Anfang an so auffallend, dass ich mich immer wundere, wenn jemand, dem ich das Blatt zeige, sie nicht ohne weiteres bemerkt.« Den nächsten Abschnitt würde ich Ihnen (genauso mir selbst) gern ersparen, ich kann es aus Gründen der Vollständigkeit jedoch nicht. Ich versuche im Folgenden, hochkomplexe physiologische Vorgänge inhaltlich zu verdichten und in einem programmiersprachenorientierten Buch zu platzieren. Der Schiffbruch ist wahrscheinlich vorprogrammiert. Mir Ihrer rettenden Hand sicher und gewiss, wage ich es dennoch.
70
Die Entdeckung der geheimnisvollen Punkte
4.1.1
Kaffeestunde beim Optiker – woher die Punkte kommen
Die Netzhaut des Menschen besteht aus über 100 Millionen Rezeptoren, deren Signale in sogenannten rezeptiven Feldern (im Weiteren RF genannt) zu funktionalen Einheiten zusammengefasst werden. Diese sind entscheidend beim Versuch, die Punkte des Hermann-Gitters zu verstehen. Rezeptoren senden Informationen, die im Sehnerv (Nervus Opticus) zusammenlaufen – Biologie Oberstufenkurs. Fällt auf ein solch kreisförmiges RF ein winziger »Lichtfleck«, wird die zugehörige Faser des Sehnervs unter der Bedingung ihre Aktivität verringern, dass das rezeptive Feld am Rande vom Licht getroffen wurde. In diesem Falle spricht man von einer lateralen Hemmung (der Faser des Sehnervs). Anderes Szenario: Diesmal wird das RF mittig getroffen. Reaktion des Faser: Verstärkung ihrer Aktivität. Drittes und entscheidendes Szenario: Das gesamte, rezeptive Feld wird getroffen, also nicht nur vereinzelte Punkte irgendwo an der Peripherie oder im Zentrum. Würde nur das getroffen, wäre im Sinne einer normalen Lichtempfindung alles okay. Belichtet wurde jedoch alles. In der Folge sind Teile der Sehfaser gehemmt. Theoretisch ist dort für uns die Welt grau und verwaschen. Beim Betrachten des Hermann-Gitters erfolgt die Belichtung der rezeptiven Zelle in einer ganz speziellen Weise, die davon abhängt, auf welche Stelle des Hermann-Gitters der Betrachter blickt. Die »schönsten« Punkte befinden sich auf den Kreuzungen der weißen Streifen (Hermann selbst sprach von »Straßen«). Keinen einzigen Punkt werden Sie dagegen auf einem Streifen irgendwo zwischen zwei Quadraten finden. Aus guten Grund. Stellen Sie sich vor, Ihren Augen würden zwei rezeptive Zellen entnommen. Wir idealisieren! Eine rezeptive Zelle soll auf eine der Kreuzungen, die andere in einigem Abstand rechts oder links daneben gelegt werden (siehe Abbildung 4.3).
Abbildung 4.3 Von der Position des rezeptiven Feldes (idealisiert als Kreis) hängt seine Belichtung ab.
71
4.1
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Betrachten Sie zuerst den rechten Kreis: Ist der weiße Streifen gerade so breit wie das Zentrum des RF, werden große Teile der Randgebiete von den schwarzen Quadraten belichtet. Und zwar schlecht, weil Schwarz sich außerordentlich wenig zur Reflexion von Lichtstrahlen eignet. Dementsprechend gering ist die laterale Hemmung im RF. Als Konsequenz erscheinen die Streifen dem Auge so, wie sie vom Urheber des Gitters gedacht sind: weiß. Anders stellt sich die Situation des RF an der Kreuzung dar (der linke Kreis), wo der Schwarzanteil augenscheinlich geringer ist. Im Umkehrschluss ist die Belichtung stärker, mit der Konsequenz einer auch stärkeren Hemmung. So viel zur Entstehung der Punkte.
4.2
Entwicklung der Benutzeroberfläche
Das Hermann-Gitter wollen wir simulieren. Dies wäre ein ehrenhaftes Ansinnen – wenn es nicht so unsinnig wäre. Denn entweder existiert ein Hermann-Gitter, und damit auch die beschriebene Kontrasttäuschung, oder es existiert nicht.
4.2.1
Was beabsichtigt ist
Sehr wohl lassen sich Variationen des »Urgitters« simulieren und damit einhergehend die Stärke des Effekts. Gezeigt werden soll, dass der Kontrasteffekt (bedingt) abhängig ist 왘
vom Verhältnis zwischen der Breite der Streifen, genauer gesagt von der Fläche der Kreuzungen und der Fläche der Quadrate. Je kleiner das Verhältnis ist, umso schwächer ist der Effekt. Das gilt auch dann, wenn die Farben umgekehrt werden, d. h., wenn weiße Quadrate auf schwarzem Hintergrund erscheinen. Der Kontrasteffekt ist unter den Bedingungen zwar immer noch schwach, gleichwohl – zumindest bei genauerer Betrachtung – ein wenig stärker.
왘
von der Stärke des Kontrasts an sich. Dafür werden wir die Möglichkeit implementieren, das Schwarze der Quadrate in Richtung dunkler und heller Grautöne zu verändern.
Auch kombinieren sollen sich die Variationen lassen, bis hin zu dem Punkt, an dem der Kontrasteffekt (fast) vollständig verschwunden ist. Das heißt genauer gesagt: bis hin zu dem Punkt, an dem der Kontrast zwischen Quadrat- und Streifenfarbe minimal und gleichzeitig das Verhältnis zwischen Quadrat- und Kreuzungsfläche maximal ist.
72
Entwicklung der Benutzeroberfläche
4.2.2
Die beteiligten Controls
Lassen Sie sich durch Maxima, Minima, Kontraste und Kontrasteffekte nicht irre machen. Bevor das möglicherweise aber doch passiert, fangen wir am besten zunächst an. Stichwort: Benutzeroberfläche. Klicken Sie im Hauptmenü der Entwicklungsumgebung auf Datei und im Kontextmenü auf Neues Projekt. Die Dialogmaske Neues Projekt erscheint auf der Bildfläche. Dort entscheiden Sie sich – natürlich – unter Von Visual Studio installierte Vorlagen für Windows Forms-Anwendung. Im Editorfeld Name tragen Sie Hermann_Gitter ein (siehe Abbildung 4.4 ).
Abbildung 4.4 Anlegen eines neuen Projekts, »Hermann_Gitter«, in der Dialogmaske »Neues Projekt«
Nächstes Stichwort: Steuerelemente. Der Einkaufszettel mit dem, was Sie bitte aus der Toolbox an Controls besorgen, liest sich folgendermaßen: 왘
3 × Label
왘
1 × Panel
왘
2 × Button
왘
1 × TextBox
왘
1 × TrackBar
왘
1 × ComboBox 73
4.2
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Abbildung 4.5 können Sie sowohl die Position der Controls auf der WindowsForm entnehmen als auch die Belegung der Text-Eigenschaft der Steuerelemente – und der WindowsForm selbst (Hermann-Gitter(v.1.0)). Ferner ist Ihnen aufgefallen, dass sich das Formelement nicht maximieren lässt. Zu Recht schreiben Sie das der auf False gesetzten MaximizeBox-Eigenschaft zu. Zur Dimensionierung der WindowsForm lässt sich Folgendes feststellen: Width: 661 Height: 681
Abbildung 4.5
Entwurfsansicht der Hermann-Gitter-Simulation – doch wo ist das Gitter?
Der Abbildung ist ferner zu entnehmen, dass 왘
왘
74
die Enabled-Eigenschaft der Controls 왘
textBox1,
왘
comboBox1,
왘
trackBar1
왘
sowie Button2 auf False gesetzt ist. Starten Sie die Anwendung später, soll zuerst kein Hermann-Gitter erscheinen. Somit macht es Sinn, die Controls zu deaktivieren, die zur Variation und Anzeige der veränderbaren Parameter verwendet werden.
In der ComboBox (comboBox1) gibt es nun fixe Einträge, und zwar neun an der Zahl. Ihr Startwert beträgt 25, der Endwert 255. Folglich beträgt die Schritt-
Entwicklung der Benutzeroberfläche
weite 25. Wie das Steuerelement comboBox1 an genannte Werte kommt, ist schnell skizziert: 1. Öffnen Sie die Datei Form1.cs im Entwurfmodus. 2. Setzen Sie den Fokus auf das Control ComboBox1. 3. Klicken Sie oberhalb des Steuerelements auf den winzigen Pfeil. Es öffnet sich die Dialogmaske ComboBox-Aufgaben, in der Sie den Link Einträge bearbeiten auswählen. 4. Die Einträge bearbeiten Sie im nun geöffneten Zeichenfolgen-Editor. Noch etwas unsicher? Dann schauen Sie sich bitte einmal Abbildung 4.6 an.
Abbildung 4.6 Festlegung der ComboBox-Einträge im »Zeichenfolgen-Editor« der Entwicklungsumgebung
Nicht zu vergessen die TrackBar (trackBar1). Das ist unser Schieberegler. Per Default ist er horizontal orientiert. Des Weiteren existieren standardmäßig lediglich zehn Stufen zur Verstellung. Wir benötigen zwölf. Um die Orientierung und die Anzahl der Stufen festzulegen, nutzen wir zwei Eigenschaften: Orientation und Maximum, die mit vertical beziehungsweise 12 zu belegen sind. Abgesehen von Fragen der Farbgestaltung (die ich als ausgewiesener Nicht-Designer gerne Ihnen überlasse), sind wir damit im Besitz einer Benutzeroberfläche. Diese funktioniert zwar (noch) nicht, doch ist spätestens hier der Anlass gekommen, Sie über die Aufgabe einiger Controls zu informieren.
75
4.2
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Zunächst: Die Position des Schiebereglers (Skalierung der Quadratabstände) wird in der TextBox (textBox1) ausgegeben. Das Control trackBar1 steuert die Breite der Streifen zwischen den Quadraten, und zwar durch eine einheitliche Skalierung der Quadratflächen, nicht etwa durch Zusammenrücken der Quadrate. Steht der Regler unten (Position 0), ist die Fläche der Quadrate minimal, der Quadratabstand maximal und damit auch die Fläche der Kreuzung maximal. Beim erstmaligen Einblenden des Hermann-Gitters (über den Button Hermann-Gitter einblenden) ist genau das die Situation. Diese lässt sich außer mit der TrackBar auch über die ComboBox verändern, in der Sie einen Wert (den sogenannten Alpha-Wert) auswählen, mit dem das Schwarz der Quadrate zusehends verblasst. ComboBox und TrackBar sind selbstredend nicht unabhängig voneinander. Die Skalierung der Quadrate muss vor dem Hintergrund dessen geschehen, ob ein Alpha-Wert gesetzt ist und wenn ja, welcher. An dem Punkt werde ich Ihnen das Programmiererleben absichtlich ein wenig schwer machen – um Ihnen Möglichkeiten aufzuzeigen, abseits zahlreicher Optionen ein Control zu konfigurieren. Habe ich etwas vergessen? Ja, den umgekehrten Fall: Wenn Sie zuerst die Quadrate skalieren, um anschließend am Alpha-Wert zu »drehen«, müssen die eingestellten Größenverhältnisse (Quadratfläche-Kreuzungsfläche) natürlich erhalten bleiben. So viel (vielleicht zu viel?) zu dem, was wir wollen. Bitte öffnen Sie die Datei Form1.cs im Quelle-Modus. Dann geht’s los.
4.3
Entwicklung der Programmierlogik
Höchst unspektakulär beginnen wir mit der Deklaration zweier Variablen, deren Wichtigkeit Sie trotzdem nicht unterschätzen sollten. Beide sind vom Elementartyp int, und weil das Private nicht selten besser als die Öffentlichkeit ist, wird die Sichtbarkeit auf private festgelegt (eine Begründung, mit der selbst der fähigste Informatikstudent in Windeseile durch jede Fachprüfung fallen würde): private int cindex; private int index;
Die Steigerung folgt auf dem Fuße: Wir erzeugen ein privates Objekt der Klasse vom Typ GS durch den Konstruktor GS(): private GS ground = new GS();
76
Entwicklung der Programmierlogik
Dass die drei Deklarationen im Kopfbereich der Klasse Form1 »fein säuberlich« untereinander geschrieben werden, macht auch deshalb Sinn, weil die beiden int-Variablen (cindex und index) teilweise in einem Zusammenhang mit der Klasseninstanz ground stehen. Doch lassen Sie sich überraschen! So viel vorweg: Hinter cindex verbirgt sich der Alpha-Wert und hinter index die Laufvariable zur Skalierung der Quadrate. Weiter geht es allerdings zunächst woanders: beim Hintergrund. Gut, wenn man über den Bescheid weiß.
4.3.1
Viel Aufwand für den Hintergrund
Eine Methode wird benötigt. Privat soll sie sein. Nennen wir die Methode sinnigerweise Background_Paint(). In ihr wird zwar kein Wert zurückgegeben (void), wohl aber hat Background_Paint() ein gewichtiges Argument, nämlich bc, zu verarbeiten. bc ist vom Typ Color, wobei Color keine Klasse, sondern eine Struktur ist (im Na-
mensraum System.Drawing). Klasse ist Color dennoch, wird durch den komplexen Typ doch eine ARGB-Farbe (Alpha (!), Rot, Grün, Blau) dargestellt. Die Methodensignatur sieht so aus: void Background_Paint(Color bc){}
Farben sind wichtig! Ohne Farbe ist ein Pinsel wenig, auch der, den wir in Gestalt der Klasse SolidBrush finden (sie ist im selben Namensraum wie die Color-Struktur organisiert). Ein Objekt der Klasse SolidBrush wird über den Konstruktor der Klasse SolidBrush erzeugt. Dem geben wir etwas mit auf den Weg der Erzeugung – die Variable bc nämlich: SolidBrush brush = new SolidBrush(bc);
Prima! Nun haben wir einen Pinsel, aber noch nichts, auf dem er seine Farbe hinterlassen könnte. Für sich genommen, hilft das Formelement uns nicht weiter. Sehr wohl aber die Klasse Graphics (abermals in System.Drawing), die eine GDI+-Zeichnungsoberfläche (das Kürzel GDI steht für Graphics Device Interface) kapselt. Trivial ist die Angelegenheit nicht. Es geht im Weiteren nämlich nicht darum, ein Objekt der Klasse Graphics über den Aufruf des Konstruktors, also auf klassischem Wege, zu erzeugen (new Graphics()). Stattdessen soll, als eine von mindestens vier vergleichsweise aufwendigen Möglichkeiten, ein Graphics durch Aufruf der CreateGraphics()-Methode zurückgegeben werden (wobei das Gra-
77
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
phics den Namen formG tragen soll). Abgesehen von this, als einem auf die aktuelle Instanz der Klasse Form verweisenden Schlüsselwort, liest sich das dann so: Graphics formG = this.CreateGraphics();
Eine »normale« Methode ist demnach der Schlüssel zum Erhalt eines Graphics. Stimmt, bloß existiert (natürlich?) keine Methode CreateGraphics() unter den zahlreichen Membern der Klasse Graphics. Wo aber dann? In der Klasse Control. Sollten Sie bislang ein wenig an der Sinnhaftigkeit von Namensräumen gezweifelt haben (vielleicht, weil ich sie nicht gut genug erläutert habe), so tun Sie es in Kürze womöglich nicht mehr. Die Klasse Control existiert nämlich zweimal. Beide Versionen sind sehr wichtig – und unverwechselbar: dank zweier, getrennter Namensräume. Unsere Control-Klasse residiert im Namensraum System.Windows.Forms, die andere in System.Web.UI, was schon das ein oder andere aussagt. Durch Control werden im Wesentlichen WindowsForms-Steuerelemente definiert, die eine visuelle Darstellung haben. Unter den Membern der Klasse entdecken wir dann auch jenes zum Erstellen der Graphics: CreateGraphics(). Zurück zur Klasse Graphics. Diese enthält eine Methode namens FillRectangle(), die in gleich mehreren Argumentkonstellationen »betrieben« werden
kann. Demnach zählt FillRectangle() zu den überladenen Methoden. Wir entscheiden uns für die Version, in der als Argumente der Pinsel brush sowie ein Rechteck übergeben werden. Hier kommt die Rectangle-Struktur (System.Drawing) wie gerufen. Die ersten beiden der insgesamt vier in der Struktur gespeicherten Zahlen kennzeichnen die Position der oberen linken Ecke des Rechtecks, die beiden letzten bestimmen die Höhe und Breite. Somit schreiben wir: formG.FillRectangle(brush, new Rectangle(26,8,298,320));
Tieferes Nachdenken über die Zahlenwerte lohnt nicht. Es geht lediglich darum, die Hintergrundfläche so zu dimensionieren und zu positionieren, dass die linke Hälfte der WindowsForm fast vollständig mit einer (von zwei möglichen) Hintergrundfarben ausgefüllt ist. Zwei Objekte werden verwendet: brush vom Typ SolidBrush sowie formG als Graphics. Die Klasse SolidBrush erbt von Brush. Wird Brush nicht mehr benötigt, können durch den Aufruf der Methode Dispose()(als Member der Klasse SolidBrush) die vom Brush-Objekt verwendeten Speicherressourcen freigegeben werden, was in der Umsetzung so aussieht: brush.Dispose();
78
Entwicklung der Programmierlogik
Was bei SolidBrush ein relativ unbestimmtes Kann ist, ist bei Graphics schlicht ein Muss. Dort müssen Sie die vom Graphics-Objekt (formG) verwendeten Ressourcen freigeben: formG.Dispose();
Damit wäre die Methode Background_Paint() implementiert. So sieht sie aus: private void Background_Paint(Color bc) { SolidBrush brush = new SolidBrush(bc); Graphics formG = this.CreateGraphics(); formG.FillRectangle(brush, new Rectangle(26, 8, 298, 320)); brush.Dispose(); formG.Dispose(); } Listing 4.1
4.3.2
Ohne Hintergrund geht wenig – die Methode »Background_Paint()«
Zeichnung und Positionierung der Gitterquadrate
Die Methode zum Zeichnen des Hintergrunds habe ich Background_Paint() genannt. Da ist es nur billig, die zum Zeichnen des Gitters erdachte Methode Gitter_Paint() zu nennen. Für die gibt es allerdings ein paar Argumente mehr. Doch kommen wir am besten gleich zur Signatur: void Gitter_Paint(int ind, int cin, Color co){}
Auch bei Gitter_Paint() wird nichts zurückgegeben, die Privatheit der Methode bleibt ebenfalls gewahrt. Und was die int-Variablen ind und cin anbelangt: Gerne dürfen Sie sich an cindex und index erinnert fühlen. Bliebe noch co vom Typ Color: co ist einer von nunmehr zwei »konstruktiven Beiträgen« zur Herstellung eines digitalen Pinsels. Für den Pinsel zum Zeichnen des Hintergrunds brauchten wir dem Konstruktor nur die Variable der Farbe zu übergeben. Doch jetzt geht es um mehr. Auch ein Gitterquadrat ist nur die Spezialform eines Rechtecks. Wie ein solches gezeichnet wird, konnten Sie im letzten Abschnitt in »quälender« Ausführlichkeit lesen, weswegen in diesem Abschnitt das Rad nicht neu erfunden werden muss. Dennoch sind wir in Sachen grafikorientierter Programmierbeigaben noch nicht über den Berg, denn im Gegensatz zum Rechteck des Hintergrunds, der nur Schwarz und Weiß kennt, können die Quadrate des Gitters gleich mehrere Farben (im Sinne der stufenweisen Abschwächung der Basisfarbe Schwarz) annehmen. Hilfe bekommen wir von der vierfach überladenen Methode FromArgb() der Color-Struktur. In der »Zwei-Argumente-Version« wird FromArgb() ein Int32-
79
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Wert (der in Wahrheit auf 8 Bit beschränkt ist) sowie eine Color-Struktur übergeben. Sie haben richtig gelesen: Einer Color-Struktur-Methode wird als Argument eine Color-Struktur übergeben. Das ist nicht halb so ungewöhnlich, wie es vielleicht verwirrend klingt, interessanter ist darüber hinaus das erste Argument. Denn was sich hinter dem Int32-Wert verbirgt, ist nichts weniger als der variable Alpha-Wert, mithin die sogenannte A-Eigenschaft einer Farbe. Wenn Sie jetzt über Color customColor = Color.FromArgb(cin, co);
eine aus A-Eigenschaft (die Variable cin) und Basisfarbe (co) bestehende, veränderbare Farbe definieren, die Sie dem Konstruktor der »Pinselklasse« SolidBrush als Wert übergeben, erhalten Sie: SolidBrush brush1 = new Solid Brush(customColor);
Nachdem wir mit Graphics formGraphics = this.CreateGraphics();
für eine weitere Graphics gesorgt haben, können wir theoretisch unter Verwendung der FillRectangle()-Methode ein Quadrat zeichnen – eines von 132 benötigten Quadraten. Zweifelsfrei: Es geht aufwärts mit uns, und das noch mehr, wenn wir die ersten beiden Werte der Rectangle-Struktur (als eines von zwei Argumenten der FillRectangle()-Methode) als klassische i-j-Laufvariablen festlegen. i bezeichnet die x-Position der rechten oberen Quadratecke, j die y-Position. Solange i und j mit Werten versorgt sind werden fleißig Quadrate gezeichnet, sowohl in horizontaler als auch in vertikaler Richtung. Bloß skaliert werden sie nicht. Bitte ziehen Sie abschließend noch einmal die Rectangle-Struktur heran. Vier Werte werden in ihr gespeichert: zwei für die Position der linken oberen Quadratecke (abgegolten mit den Laufvariablen i und j), zwei für die Kantenlängen. Wenn Sie später auf die Schaltfläche Hermann-Gitter einblenden klicken, geschieht genau das, und zwar mit der kleinsten Quadratfläche. Genauer gesagt mit 15px. Werden die Quadrate mit dem Schieberegler skaliert, fügen Sie dem Startwert 15px einen weiteren Wert hinzu, der in der Variablen ind als erstes Argument der Methode Gitter_Paint() gespeichert ist. So fügt sich eins zum anderen. Die Methode FillRectangle() wäre damit komplett: formGraphics.FillRectangle(brush1, new Rectangle(j, i, 15 + ind, 15 + ind));
Abgesehen von der Methode Dispose(), anzuwenden auf brush1 und formGraphics, fehlen noch Kontrollstrukturen für die Laufvariablen i und j, für die in
80
Entwicklung der Programmierlogik
der Struktur kein Startwert festgelegt wurde. Wir verwenden dazu zwei ineinander verschachtelte for-Schleifen. for (int i = 10; i <= 320; i = i + 27) { for (int j = 30; j <= 320; j = j + 27) { } } Listing 4.2
Verschachtelte »for«-Schleifen zur Erzeugung von 132 Quadraten
Auch hier gilt: Halten Sie nicht lange damit auf, wie diese Zahlenwerte wohl zustande gekommen sind; sie sind mehr oder weniger durch Versuch und Irrtum entstanden. Das Prinzip ist wichtiger. Für den vertikalen Aufbau des Gitters ist die innere Schleife zuständig (die auch zuerst ausgeführt wird) und für den horizontalen Aufbau die äußere. Zusammengefasst, bewirken die for-Schleifen nichts anderes als das Verschieben der Position der linken oberen Quadratecke. Und jede neue Position entspricht einem Quadrat. Hier sehen Sie die vollständige Methode Gitter_Paint(): private void Gitter_Paint(int ind, int cin, Color co) { for (int i = 10; i <= 320; i = i + 27) { for (int j = 30; j <= 320; j = j + 27) { Color customColor = Color.FromArgb(cin, co); SolidBrush brush1 = new Solid Brush(customColor); Graphics formGraphics = this.CreateGraphics(); formGraphics.FillRectangle(brush1, new Rectangle(j, i, 15 + ind, 15 + ind)); brush1.Dispose(); formGraphics.Dispose(); } } } Listing 4.3
Ohne Gitter kein Hermann-Gitter – die Methode »Gitter-Paint()«
81
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
4.3.3
Der Schalter für das Hermann-Gitter
Wenn Sie den Rumpf der Ereignisbehandlungsroutine für die Schaltfläche Hermann-Gitter einblenden durch einen linksseitigen Doppelklick auf das Control button1 erstellt haben, geht es zunächst darum, vier Steuerelemente zu aktivieren. Die Klassen ComboBox, TrackBar, Button und TextBox (jeweils im Namensraum System.Windows.Forms zu finden) besitzen alle die Eigenschaft Enabled, die, mit true festgelegt, zur Aktivierung des Controls führt: comboBox1.Enabled = true; trackBar1.Enabled = true; button2.Enabled = true; textBox1.Enabled = true;
Eingangs, d. h. nach Betätigung des Schalters, sind die Quadrate nicht skaliert. Der Schieberegler wurde nicht betätigt. Die TextBox Stufe sollte 0 anzeigen: textBox1.Text = «0«;
Ein wirklich schwarzes Quadrat bekommen wir nur mit einem maximalen Wert der int-Variable cindex: cindex = 255;
Da die Umkehrung der Farbe keine Auswirkung auf die Größe der Quadrate haben darf, wird der Variablen index der Wert 0 zugewiesen: index = 0;
Dann geht es ans Zeichnen des Hintergrunds, wofür in Abschnitt 4.3.1 die Methode Background_Paint(Color bc) entwickelt wurde. Die Variable bc (vom Typ Color) wird mit Color.White festgelegt. Die Methode kann nun aufgerufen werden: Background_Paint(Color.White);
Es folgt der Aufruf der Methode Gitter-Paint(), der als Argumente cindex, index sowie Color.Black übergeben werden: Gitter_Paint(index, cindex, Color.Black);
Acht Zeilen Code, und das Rumpfinnere des Eventhandlers button1_Click() wäre komplett. Hier sehen Sie das Resultat: private void button1_Click(object sender, EventArgs e) { comboBox1.Enabled = true;
82
Entwicklung der Programmierlogik
trackBar1.Enabled = true; button2.Enabled = true; textBox1.Enabled = true; textBox1.Text = «0«; cindex = 255; index = 0; Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } Listing 4.4 Wie das Hermann-Gitter eingeblendet wird – die Ereignisbehandlungsroutine »button1_Click()«
Nach erfolgreichem Start mit ((F5)) oder ohne Debugging ((Strg)+(F5)) und einem Klick auf den Button Hermann-Gitter einblenden erwartet Sie der Anblick aus Abbildung 4.7.
Abbildung 4.7 Hermann-Gitter-Variation – abgeschwächter Kontrasteffekt, da das Verhältnis zwischen Kreuzungsfläche und Quadratfläche klein ist.
4.3.4
Ganz schön blass geworden – die Regelung des Alpha-Werts
Dank des letzten Abschnitts sind wir immerhin so weit, das Hermann-Gitter über den Schalter Hermann-Gitter einblenden darstellen zu können. Um den schwarzen Quadraten »scheibchenweise« die Schwärze zu nehmen, sollten wir zuerst dem Steuerelement comboBox1 einen anständigen Eventhandler spendieren. Viel Auswahl haben Sie da nicht, am wenigsten, wenn Sie im Entwurfsmodus zweimal
83
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
auf comboBox1 klicken. Dann ist er nämlich da, der Ereignisbehandler comboBox1_ SelectedIndexChanged(object sender, EventArgs e). Ihn gilt es mit Leben zu füllen. Auf das Ausgewählte lässt sich über die Text-Eigenschaft der ComboBox-Klasse bequem zugreifen: comboBox1.Text
Das ist gut, wenngleich unzureichend. Die ComboBox-Einträge liegen als Text vor. Dagegen ist die cindex-Variable (als Speicherort des Alpha-Werts) vom Typ int – was so bleiben soll. Folglich kommen wir nicht darum herum, das Ergebnis von comboBox1.Text in den Basistyp int zu konvertieren. Das ist eine Aufgabe für die Klasse Convert (Namespace: System). Deren mächtig überladener Funktion ToInt16() wird der angeklickte Eintrag übergeben: cindex = Convert.ToInt16(comboBox1.Text);
Was der ComboBox ihr comboBox1.Text ist, ist der TrackBar ihr trackBar1.Value. Ist der Schiebregler nämlich an einer bestimmten Position, wird ein Wert geliefert – vom Typ int. Analog zur ComboBox könnten wir also schreiben: index = trackBar1.Value;
Wo auch immer sich der Schieberegler im Bezug auf die Skala befindet – der betreffende int-Wert, gespeichert in der int-Variablen index, sorgt nach dem Aufruf der Methode Gitter-Paint(int ind, int cin, Color co)dafür, dass die ComboBox notwendigerweise Kenntnis darüber erhält, für welche Quadratgröße der neue Alpha-Wert gilt. Wie Sie sehen, ist dieser Satz dreierlei: zu lang, verwirrend – und inhaltlich richtig. Nichtsdestoweniger hat die Angelegenheit einen Haken. Im Prototyp der Anwendung Hermann-Gitter erfolgte auch die Regelung der Quadratgröße über eine ComboBox – was mir schnell missfiel. Als ich die ComboBox durch eine TrackBar ersetzte, gefiel mir das Resultat noch viel weniger. Bei Stufe 12 des Schiebereglers wurden die Quadrate schlechterdings zu groß, was sich eindrucksvoll im Verschwinden der weißen Streifen zeigte. Aus 132 kleinen Quadraten wurde ein großes. Punkte ade! Vermutlich war ich in Gedanken schon im Feierabend und habe deswegen das Programm nicht an der richtigen Stelle geändert. Anstatt nämlich im Ausdruck formGraphics.FillRectangle(brush1, new Rectangle(j, i, 15 + ind, 15 + ind));
den Startwert der Quadratkantenlänge (oder die Stufen der TrackBar) um 1 zu reduzieren, habe ich den Positionswert des Schiebereglers durch den Wert 2 dividiert. Dabei ist es geblieben. Somit:
84
Entwicklung der Programmierlogik
index = (trackBar1.Value)/2;
Was bleibt, ist die Methode zum Zeichnen des Hintergrunds sowie die Methode, um die Quadrate aufzurufen: Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black);
Hier sehen Sie noch einmal die Ereignisbehandlungsroutine hinter der ComboBox Alpha-Wert: private void comboBox1_ SelectedIndexChanged(object sender, EventArgs e) { cindex = Convert.ToInt16(comboBox1.Text); index = (trackBar1.Value)/2; Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } Listing 4.5 Regelung des Alpha-Werts in der Ereignisbehandlungsroutine »comboBox1_ SelectedIndexChanged()«
Auch hier können Sie testen, beispielsweise durch Auswahl des kleinsten AlphaWerts (siehe Abbildung 4.8).
Abbildung 4.8 Beinahe vollständig verschwundener Kontrasteffekt aufgrund von a) kleinem Verhältnis zwischen Quadratfläche und Fläche der Kreuzungen sowie b) nicht ausreichendem Kontrast zwischen Quadratfarbe und Streifenfarbe
85
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
4.3.5
Aus groß mach klein – Skalierung der Quadrate durch ein TrackBar-Steuerelement
Auch das gewichtige TrackBar-Steuerelement lässt sich im Entwurfsmodus der Seite Form1.cs problemlos zweimal anklicken. Tun Sie das, und der Ereignishandler trackBar1_Scroll(object sender, EventArgs e) ist Ihrer. Und auch hier gilt es, den Rumpf der Routine zu füllen. Bitte kehren Sie also in den Quellmodus zurück. Gerade im Eventhandler der TrackBar brauchen wir den (aus genannten Gründen halbierten) Positionswert des Schiebereglers. Hier ist er noch einmal: index = trackBar1.Value/2;
Die Skalierung der Quadrate sollte mit dem Wissen erfolgen, ob und wenn ja welcher Alpha-Wert in der ComboBox Alpha-Wert angeklickt wurde. Die relevante Zeile lautet: cindex = ground.combo(comboBox1.Text);
Was nun folgt, rechtfertigt ein weiteres Unterkapitel. Schnell werden Sie merken, warum. Es geht um eine programmiertechnische Übertreibung, die wir uns an dieser Stelle erlauben. Eine kleine Serviceklasse Zu Beginn der Klasse Form1 wurde über private GS ground = new GS();
eine private Instanz der Klasse GS erzeugt. Das ist gut. Weniger gut ist, dass in der Projektmappe »Hermann_Gitter« bislang keine gleichnamige Klasse existiert. Noch schlimmer kommt es in der Ereignisbehandlungsroutine trackBar1_ Scroll(object sender, EventArgs e). Dort wurde in der Zeile cindex = ground.combo(comboBox1.Text);
der Rückgabewert der Methode combo(), die ein Member der Klasse GS ist, der Variablen cindex (vom Typ int) zugewiesen. Wenn es keine Klasse GS gibt, sieht das bei der Methode combo() vermutlich nicht viel anders aus. Was ist mit dem Argument comboBox1.Text der Methode combo()? Die Aufgabe der Klasse GS ist es, mit der Methode combo() – abhängig davon, ob und was unter Alpha-Wert ausgewählt ist – einen Wert für die Variable cindex zu generieren. Unterbrechen wir hier die Erörterung der (zugegebenermaßen etwas hochtrabend) »Serviceklasse« genannten Klasse GS, die erst dann wieder Sinn macht,
86
Entwicklung der Programmierlogik
wenn wenigstens der Klassenrumpf vorhanden ist. Hier ist schnell Abhilfe geschaffen: 1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf Projektmappe »Hermann-Gitter«. 2. Wählen Sie im Kontextmenü erst Hinzufügen, anschließend (im Untermenü) Neues Projekt aus. Es öffnet sich die Dialogmaske Neues Projekt hinzufügen. 3. Unter Vorlagen entscheiden Sie sich für Klassenbibliothek, und im Editorfeld Name wäre Gitter_Service einzutragen (siehe Abbildung 4.8). Mit der Festlegung des Projekttitels wird gleichzeitig der Name des Namespace festgelegt. Er lautet also Gitter_Service. 4. Klicken Sie einmal auf Ok, und warten Sie dann gespannt auf das, was sich im Projektmappen-Explorer tut.
Abbildung 4.9
Hinzufügen einer Klassenbibliothek als neues Projekt
Im Projektmappen-Explorer existiert fortan ein neues Projekt, das aus Eigenschaften, Verweisen und, last but not least, aus der Klasse Class1.cs besteht.
87
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Womit gestartet wird Die Entwicklungsumgebung hat es sich gespart, die Datei Programm.cs zu erzeugen. Folglich gibt es keine Methode Main() (die, nebenbei bemerkt, wohl das stärkste Indiz für den C-Ursprung der Sprache C# ist), keinen Einstiegspunkt für die Laufzeitumgebung und somit keine Möglichkeit, die Datei Class1.cs direkt auszuführen – worauf sich auch verzichten lässt. Schließlich haben wir ein ausführbares Projekt Hermann_Gitter, mit dem gestartet werden kann. Dazu klicken Sie bitte mit der rechten Maustaste auf das Projekt Hermann_Gitter. Das Kontextmenü enthält die Option Als Startprojekt festlegen. Klicken Sie bitte auf diesen Eintrag.
Ändern Sie zunächst den Klassennamen Class1 in GS. Klicken Sie dazu mit der rechten Maustaste auf die Datei Class1.cs, wählen Sie im Kontextmenü Umbenennen aus, und tun Sie genau das dann auch. Von der Änderung des Namens sollen auch Verweise betroffen sein, was Sie durch einen Klick auf den Button Ja der diesbezüglichen Meldung bestätigen. Anschließend öffnen Sie die Datei GS.cs im Codeeditor. Erwarten sollte Sie folgendes Codegerüst: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Gitter_Service { public class GS { } } Listing 4.6 »GS.cs«
»using«-Direktiven, Namespace und vorerzeugter Klassenrumpf in der Datei
Erste Auffälligkeit: Auch hier stehen vor dem vorerzeugten Klassenrumpf (nicht zu vergessen der übergeordnete namespace Gitter_Service) using-Direktiven – von denen Sie alle außer using System getrost »streichen« können (aber nicht müssen). Zweite Auffälligkeit: Der Konstruktor fehlt, was mit public GS() { }
schnell behoben ist.
88
Entwicklung der Programmierlogik
Für den Fall der Fälle Einen Konstruktor (als unbedingte Voraussetzung zum Erzeugen eines Klassenobjekts) in der »Grundversion« zu erstellen, ist manchmal einfacher, als ihn zu verstehen. Doch eigentlich müssen Sie gar nichts tun, erzeugt der Compiler doch automatisch für Klassen ohne eigenen Konstruktor einen Ersatz- bzw. Standardkonstruktor. Dennoch ist es die schönere Variante, die Aufgabe selbst zu erledigen – finde ich zumindest.
Der Methode combo() soll ein Wert, gespeichert in der Variablen cb, vom Typ string übergeben werden – eben das, was in der ComboBox Alpha-Wert ausgewählt ist. Um nichts anderes als den Alpha-Wert geht es. Zurückgeliefert wird ein in der Variablen cd gespeicherter Wert, der vom Typ int ist. Auch hinter, besser gesagt in der Variablen cd verbirgt sich der Alpha-Wert. Zwischen cb und cd muss irgendetwas passiert sein. Werfen wir zunächst jedoch einen Blick auf die Methodensignatur: int combo(string cb){ }
Über cindex = ground.combo(comboBox1.Text);
erfolgt die Speicherung dessen, was die Methode combo(comboBox1.Text) zurückgibt, in der Variablen cindex, mit der die Methode zum Zeichnen der Quadrate aufgerufen wird. Wurde unter Alpha-Wert nichts ausgewählt, hat die Methode combo() einen Leerstring zu verarbeiten, was in dem Augenblick unangenehm wird, in dem Sie ihn konvertieren müssen. Und wir müssen konvertieren. Nämlich so: cd = Convert.ToInt16(cb);
Übergeben Sie ToInt16() einen Leerstring, handeln Sie sich eine unbehandelte FormatException ein. Fangen Sie stattdessen genau diesen Fall – zuvor – in einer if-else-Struktur ab, vielleicht so: if (cb == "") { cd = 255; } else { cd = Convert.ToInt16(cb); }
Die komplette Methode sieht so aus:
89
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
public int combo(string cb) { if (cb == "") { cd = 255; } else { cd = Convert.ToInt16(cb); } return cd; } Die Methode combo()
Listing 4.7
Hier sehen Sie die vollständige Klasse GS: using System; namespace Gitter_Service { public class GS { private int cd; public int combo(string cb) { if (cb == "") { cd = 255; } else { cd = Convert.ToInt16(cb); } return cd; } } } Listing 4.8
90
Die selbst entworfene Serviceklasse »GS«
Entwicklung der Programmierlogik
Bliebe das Erstellen der Serviceklasse GS, im Sinne einer Assembly Gitter_Service.dll. Klicken Sie dazu im Projektmappen-Explorer auf Gitter_Service und im Kontextmenü auf Erstellen. Zuvor jedoch sollten Sie überprüft haben, an welchem Ort die erstellte Assembly abgelegt wird. Idealerweise wäre das der Ordner bin im Projekt Gitter_Service. Der Ordner existiert, bloß sehen Sie ihn vermutlich nicht, weil in der Projektmappe Hermann_Gitter nicht alle Dateien eingeblendet sind. Das korrigieren Sie rasch durch einen Klick auf die mittlere Schaltfläche im Projektmappen-Explorer (Tooltip: Alle Dateien Anzeigen). Klicken Sie nun mit der rechten Maustaste auf das Projekt Gitter_Service. Allerdings wählen Sie diesmal die Option Eigenschaften, woraufhin sich der Reiter Gitter_Service öffnet. Hier geht es um den Menüpunkt Erstellen und im Weiteren um das Editorfeld Ausgabepfad, in dem bin\Debug\ zu lesen sein sollte (siehe Abbildung 4.10).
Abbildung 4.10
Festlegung des Ausgabepfads für die Assembly »Gitter_Service.dll«
Jetzt können Sie das Projekt erstellen, starten können Sie es nicht. Denn zurzeit hat das WindowsForm-Projekt Hermann_Gitter keine Kenntnis über Inhalte des Projekts Gitter_Service, geschweige denn weiß das zuerst erstellte Projekt von der Existenz der Assembly Gitter_Service.dll im Projektverzeichnis bin\Debug\. Woher auch? Die gemeinsame Projektmappe »Hermann_Gitter« genügt nicht, um die Projektgrenzen zu öffnen. Vielmehr muss im Ordner Verweis des Projekts Hermann_Gitter ein Verweis auf die unter bin\Debug\ liegende Assembly Gitter_ Servive.dll eingefügt werden (ein sogenannter Assembly-Verweis). Die Schritte im Einzelnen:
91
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
1. Rechtsklicken Sie im Projekt Hermann_Gitter auf Verweise. 2. Im Kontextmenü wählen Sie Verweis hinzufügen. Es öffnet sich eine Dialogmaske desselben Namens, in der Sie auf den Reiter Durchsuchen klicken. 3. Im Dateisystem »hangeln« Sie sich bis zum Ort der Assembly Gitter_Service.dll vor (C:\Projekte\Gitter_Service\bin\Debug; siehe Abbildung 4.11).
Abbildung 4.11 Auswahl der Assembly-Datei »Gitter_Service.dll« im Reiter »Durchsuchen« der Dialogmaske »Verweis hinzufügen«
4. Die Wahl der Assembly Gitter_Service.dll bestätigen Sie durch einen Klick auf den Button Ok. Im Ordner Verweise des Projekts Hermann_Gitter wurde die Liste der Verweise um den Verweis Gitter_Service erweitert (siehe Abbildung 4.12). Die gute Nachricht: So und nicht anders soll es sein. Die schlechte Nachricht lautet: Es fehlt noch eine Kleinigkeit. Zwar hat das Projekt Hermann-Gitter zwischenzeitlich Kenntnis von der Existenz der Assembly Gitter_Service.dll erhalten – mehr aber auch nicht. Nichts und niemand hat der »Hauptklasse« Form1 den Gebrauch des Namensraums Gitter_Service »verordnet« – bis jetzt. Indem Sie in der Datei Form1.cs die Liste der using-Direktiven um die Anweisung using Gitter_Services;
ergänzen, beheben Sie nicht nur das Versäumnis, Sie schließen auch die Entwicklungsarbeit an der Serviceklasse GS ab. Kehren wir also zum Eventhandler trackBar1_Scroll() der Klasse Form1 zurück.
92
Entwicklung der Programmierlogik
Abbildung 4.12
Verweis auf den Namensraum »Gitter_Service« im Projekt »Hermann_Gitter«
TrackBar und TextBox sind derart synchronisiert, dass im Endergebnis der Positionswert (0–12) des Reglers unter Stufe dargestellt wird. Der Positionswert ist ein int-Wert; TextBox-Ausgaben dagegen setzen das Format string voraus. Schon einmal hatten wir ein solches Problem mittels der Convert-Klasse gelöst. Konvertiert wird diesmal allerdings mit der Methode ToString(), der wir als Wert den Ausdruck trackBar1.Value übergeben: textBox1.Text = Convert.ToString(trackBar1.Value);
Abschließend werden auch hier die Methoden für den Hintergrund und die Quadrate aufgerufen. Fertig wäre der Eventhandler: private void trackBar1_Scroll(object sender, EventArgs e) { index = trackBar1.Value/2; cindex = ground.combo(comboBox1.Text); textBox1.Text = Convert.ToString(trackBar1.Value); Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } Listing 4.9
Die Ereignisbehandlung hinter dem Schieberegler »trackBar1«
93
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Ein weiterer Testlauf wird zeigen, dass TextBox, TrackBar sowie ComboBox tatsächlich gekoppelt sind. Starten Sie die Anwendung. Skalieren Sie bei einem mittleren Alpha-Wert von 125 die Quadrate maximal (Stufe: 12). Das Ergebnis sehen Sie in Abbildung 4.13.
Abbildung 4.13 Maximale Quadratfläche, minimale Streifenbreite. Die Punkte sind trotz verringertem Kontrast (Alpha-Wert = 125) zu erkennen.
4.3.6
Die Verhältnisse auf den Kopf gestellt – Umkehrung der Farben
Um die Farben umzukehren, erstellen Sie zunächst und notwendigerweise den Eventhandler für button2, was sich übrigens auch manuell erledigen lässt. Das ist allerdings ein wenig aufwendig, gehen Sie also wie gehabt vor: Zweifacher »Linksklick« auf das betreffende Control (button2), und die Klasse Form1 hat Zuwachs in Gestalt einer weiteren Methode (button2_Click()) bekommen. Trösten Sie sich: Danach ist Schluss. Nach einem Klick auf die Schaltfläche Farbumkehr soll genau das geschehen, was auf dem Button steht: Die Farben werden umgekehrt. Aus weißen Streifen werden schwarze, aus schwarzen Quadraten weiße. Mehr nicht. In diesem »Modus« soll weder die Skalierung der Quadratflächen durch die TrackBar möglich sein noch eine Variation des Alpha-Werts in der ComboBox erfolgen. Die betreffenden Elemente der Benutzeroberfläche (TrackBar, ComboBox, Editorfeld) stehen demnach nicht zur Verfügung, sollten also für die Dauer der Farbumkehr deaktiviert bzw. auf null gesetzt werden:
94
Entwicklung der Programmierlogik
comboBox1.Enabled = false; trackBar1.Enabled = false; textBox1.Enabled = false;
Die Stellung des Schiebereglers (trackBar1) bei der Farbumkehr auf null zu setzen, ist dank der Eigenschaft Value der Klasse TrackBar ein Kinderspiel: trackBar1.Value = 0;
Ist der Schieberegler sozusagen auf dem Nullpunkt, sollte es der TextBox auch nicht besser gehen: textBox1.Text = "0";
Um das geforderte Weiß der Quadrate zu realisieren, wird der volle cindexsprich Alpha-Wert erwartet (anderenfalls würde auch das Weiß in ein Grau übergehen): cindex = 255;
Quadratflächen (somit auch die Streifenbreite) bleiben von der Farbumkehr unberührt. Deshalb verwenden wir einen minimalen index-Wert: index = 0;
Der Hintergrund des Hermann-Gitters soll schwarz sein, deshalb schreiben wir in bekannter Manier: Background_Paint(Color.Black);
Zum Schluss werden die Quadrate gezeichnet – und diesmal sind sie weiß: Gitter_Paint(index, cindex, Color.White);
Damit wäre die Ereignisbehandlungsroutine button2_Click() vollständig: private void button2_Click(object sender, EventArgs e) { comboBox1.Enabled = false; trackBar1.Enabled = false; textBox1.Enabled = false; trackBar1.Value = 0; textBox1.Text = "0";
cindex = 255; index = 0;
95
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
Background_Paint(Color.Black); Gitter_Paint(index, cindex, Color.White); } Listing 4.10
Implementierung der Farbumkehr im EventHandler »button2_Click()«
Abbildung 4.14 Farbumkehr bei kleinstmöglicher Quadratfläche. Schauen und beurteilen Sie selbst.
Mit der Implementierung der letzten Methode ist auch die Klasse Form1 vollständig. Sie soll der Vollständigkeit halber noch einmal wiedergegeben werden: using using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Linq; System.Text; System.Windows.Forms; Hermann_Gitter;
namespace Hermann_Gitter { public partial class Form1 : Form {
96
Entwicklung der Programmierlogik
private int cindex; private int index; private GS ground = new GS(); public Form1() { InitializeComponent(); } private void Background_Paint(Color bc) { SolidBrush brush = new SolidBrush(bc); Graphics formG = this.CreateGraphics(); formG.FillRectangle(brush, new Rectangle(26, 8, 298, 320)); brush.Dispose(); formG.Dispose(); } private void Gitter_Paint(int ind, int cin, Color co) { for (int i = 10; i <= 320; i = i + 27) { for (int j = 30; j <= 320; j = j + 27) { Color customColor = Color.FromArgb(cin, co); SolidBrush brush1 = new SolidBrush(customColor); Graphics formGraphics = this.CreateGraphics(); formGraphics.FillRectangle(brush1, new Rectangle(j, i, 15 + ind, 15 + ind)); brush1.Dispose(); formGraphics.Dispose(); } } } private void button1_Click(object sender, EventArgs e) {
97
4.3
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
comboBox1.Enabled = true; trackBar1.Enabled = true; button2.Enabled = true; textBox1.Enabled = true; textBox1.Text = «0«; cindex = 255; index = 0; Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { cindex = Convert.ToInt16(comboBox1.Text); index = (trackBar1.Value)/2; Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } private void trackBar1_Scroll(object sender, EventArgs e) { index = trackBar1.Value/2; cindex = ground.combo(comboBox1.Text); textBox1.Text = Convert.ToString(trackBar1.Value); Background_Paint(Color.White); Gitter_Paint(index, cindex, Color.Black); } private void button2_Click(object sender, EventArgs e) { comboBox1.Enabled = false; trackBar1.Enabled = false; textBox1.Enabled = false; trackBar1.Value = 0; textBox1.Text = "0"; cindex = 255; index = 0;
98
Hätten Sie’s gewusst?
Background_Paint(Color.Black); Gitter_Paint(index, cindex, Color.White); } } } Listing 4.11
4.4
»Form1« – die um diverse Funktionalitäten erweiterte »Hauptklasse«
Hätten Sie’s gewusst?
Programmierer sind kreative Zeitgenossen. Im eher künstlerischen Sinne noch schöpferischer sind Webdesigner mit ihren hoch performanten Bildbearbeitungsund Zeichenprogrammen, deren virtuoser Gebrauch mir schon so manchen bewundernden Blick abgerungen hat. Ein Vertreter der Zunft (der sich seit geraumer Zeit ganz eigene Gedanken zum Hermann-Gitter macht) klopft Ihnen mit diebischem Ausdruck im Gesicht auf die Schulter. Verdächtig höflich bittet er Sie, vor einem Computer Platz zu nehmen. Im Weiteren werden Sie aufgefordert, ein Bildbearbeitungsprogramm zu starten, mit dem Sie den Täuschungscharakter der Punkte des Hermann-Gitters beweisen sollten. Quod erat demonstrandum. Um welches Grafikprogramm es sich handelt, ist irrelevant. In beinahe jeder Werkzeugleiste existiert nämlich ein Standardfeature, mit dem Sie den Beweis sehr leicht führen können. Weitere Tipps würden unweigerlich zur richtigen Lösung führen, weswegen ich Sie zunächst sich selbst und Ihrem fruchtbaren Nachdenken überlasse. Sie haben die Lösung bereits gefunden, stimmt’s? Dann auf zur zweiten und letzten Knobelei: Auch Punkte, die es nicht gibt, können zum Verschwinden gebracht, zumindest abgeschwächt werden. Die »Simulation« des Hermann-Gitters, bei der Größenverhältnisse und Farben sowie Farbkombinationen variiert wurden, hat es zumindest ansatzweise gezeigt. Was wäre, wenn Sie plötzlich all das nicht mehr dürften, aber dennoch die Punkte verschwinden sollen? Augen zu und durch? Der Sehapparat muss »eingeschaltet« bleiben. Die Variationen erfolgen am Gitter. Ihnen hier eine massive Hilfestellung zu verweigern, wäre unfair. Abgesehen davon, dass es etliche Möglichkeiten zur Variation des »Urgitters« gibt, existieren auch mehrere Optionen zum Unterdrücken des berühmten Kontrasteffekts. Uns geht es nur um eine, bei der
99
4.4
4
Alles Täuschung oder was? Herr Hermann und sein Gitter
왘
erstens (außer der des Gitters) keine einzige Gerade mehr existiert (1.Tipp) und
왘
zweitens die Seiten der Quadrate durch eine elementare trigonometrische Funktion ersetzt sind, die – 5 Zeichen lang – mit dem gleichen Buchstaben beginnt wie sie endet (2.Tipp).
Ein letzter Tipp: Vielleicht schnuppern Sie gerne Meeresluft. Im Meer gibt es, mal hoch, mal weniger hoch ... Das sollte Ihnen genügen, um die verwaschenen grauen Flecken der ursprünglichen Täuschung zu neutralisieren – was zudem eine wunderbare Programmieraufgabe wäre. Experimentieren Sie, auch wenn es knifflig anmutet! Die Klasse Graphics hält auch Methoden zur Darstellung unterschiedlicher Linien bereit (im Grunde war das der vierte Tipp).
100
Die Tage werden unterschieden, aber die Nacht hat einen einzigen Namen. (Elias Canetti)
5
Mit Argusaugen – der nächtliche Sternenhimmel
Canettis Zitat legt die Vermutung nahe, dass sich auch der große Schriftsteller und Aphoristiker (den Nobelpreis für Literatur erhielt er 1981) für die wundersame Schönheit des Weltalls zu begeistern wusste. Eine Schönheit, die sich – einen weitgehend wolken- bzw. dunstfreien Himmel vorausgesetzt – selbst dann schon erschließt, wenn man sie mit bloßem Auge betrachtet.
5.1
Wie alles begann – Stippvisite in Padua
Er hatte schon etwas Gravitätisches an sich, der Galileo Galilei (1564–1642). Auf alten Zeichnungen sehen Sie ihn in einem langen Gewand, mit imponierendem Bartwuchs, das Fernrohr in der Hand. Ein Naturforscher durch und durch, will man meinen, wenngleich ein zur Zeit seines Wirkens nicht unumstrittener. Umstritten war er weitgehend zu Unrecht, wie wir heute wissen. Als sich Galileo Galilei Anfang des 17. Jahrhunderts (genau war es 1609) daran machte, das Fernrohr des Holländers Jan Lippershey nachzubauen, kamen zunächst reguläre, d. h. auf maximal vierfache Vergrößerung beschränkte Linsen zum Einsatz. Dem naturwissenschaftlichen Tausendsassa genügte das nicht. So machte sich Galilei kurzerhand daran, selbst Linsen zu schleifen, Linsen, mit denen schließlich eine bis zu dreiunddreißigfache Vergrößerung erzielt wurde. Diese Tätigkeit dürfte dem wissensdurstigen Italiener im wörtlichen Sinne nicht nur neue Ansichten, sondern auch neue Einsichten vermittelt haben (siehe Abbildung 5.1). Die Präsentation von Galileo Galileis Weiterentwicklung des Lippershey'schen Fernrohres vor der Signoria, der damaligen Regierung Venedigs, gilt als Gedenktag der Astronomie (was ich Ihnen auch deshalb gerne mitteile, weil ich dieses Kapitel im Jahr der Astronomie, 2009, geschrieben habe, das zum Gedenken an eben jenes Jahr 1609 ausgerufen wurde).
101
5
Mit Argusaugen – der nächtliche Sternenhimmel
Abbildung 5.1 Verblüffende Übereinstimmung – links: Federzeichnung Galileis, rechts: eine zeitgenössische teleskopische Aufnahme der Mondoberfläche
Fragen wissenschaftlicher Urheberschaft waren schon zu Galileis Zeiten ein heißes Eisen. Hier kann dem Meister selbst zumindest ein halber Vorwurf gemacht werden, weil er Venedigs Vorstehern schlicht verschwieg, dass die Urfassung (heute würde man Prototyp sagen) seiner Erfindung von einem anderen, eben jenem Jan Lippershey, stammte. Gegen das, was ihm (vor allem vom Klerus) noch blühen sollte, war die Reaktion der Verantwortlichen nichts, beließ man es gnädigerweise doch dabei, dem Inhaber des Lehrstuhls für Mathematik (in Padua, wo er auch zeitweilig lebte) das zum Dank für seine technische Errungenschaft verdoppelte Professorengehalt ordentlich zu kürzen. Trotzdem, und warum auch nicht: Galilei betrieb seine Himmelsbeobachtungen weiter. Ob er es unverdrossen tat, wissen wir nicht. Erfolgreich war er auf jeden Fall, nicht zuletzt, weil seine Beobachtungen zum ersten Mal eine Überprüfung theoretischer Ansätze erlaubte – u. a. jener Theorien von Johannes Kepler, der kurz nach Galilei allerdings ein eigenes Fernrohr entwarf. Was Galilei von den meisten anderen zeitgenössischem Astronomen unterschied, war die untrügliche Sicherheit, aus dem Beobachteten die richtigen Schlüsse zu
102
Subtil und verspielt – wo wir hin wollen
ziehen. Um mit Brecht, der Galilei ein eigenes Stück widmete, zu sagen: »Er glotzte nicht, sondern er sah.«
5.2
Subtil und verspielt – wo wir hin wollen
Was tut sich des Nachts vor Ihren nach Sternen Ausschau haltenden Augen, besonders dann, wenn Sie kurze Zeit vorher aus einer vergleichsweise hellen Umgebung gekommen sind? Die absonderliche Antwort lautet: Einige Sterne werden Ihnen vorenthalten. Wenigsten so lange, bis sich das mitunter recht träge Auge an die gewaltige Dunkelheit gewöhnt hat. Bis das Dämmerungssehen vollends »eingeschaltet« ist, kann immerhin bis zu einer Stunde vergehen! Zeit, die wir leider nicht haben. Im Rahmen dessen, was kosmologisch, im Bezug auf Sie als interessierten Beobachter möglich ist, und abzüglich atmosphärischer Einflüsse werden umso mehr Sterne sichtbar, je länger Sie in den nächtlichen Himmel blicken. Diesen Effekt wollen wir versuchen im Zeitraffer durch ein Programm ansatzweise nachzuahmen, wobei die Betonung beinahe zwingend auf dem Wort »ansatzweise« liegen muss. Das Flackern vor allem horizontnaher Gestirne hat übrigens so gut wie nichts mit der Arbeitsweise des Sehapparats zu tun. Der Szintillation genannte Effekt entsteht durch Dichte- und Temperaturschwankungen der Erdatmosphäre. Ebenso leistet die Unruhe in den Luftschichten einen Beitrag zur Szintillation. Auch die Szintillation, also der rasche Wechsel von Farbe, Helligkeit und Größe der Sterne soll – zumindest für eine einzige der schönen »Himmelslaternen« – simuliert werden. Und nicht nur das, wir werden den Stern durch ein virtuelles Spiegelteleskop betrachten, gleichwohl durch ein älteres, bei dem es noch keine adaptive Optik zum Ausgleich atmosphärischer Störungen gibt. Wie leistungsfähig Spiegelteleskope sind, beweist vor allem eines: Hubble. Die vom berühmten NASA/ESA-Weltraumteleskop gelieferten Bilder sind an bizarrer Ästhetik kaum zu überbieten. Als ein Beispiel zeigt Abbildung 5.2 die Kollision zweier Galaxien, unvorstellbar weit entfernt von unserer eigenen, der Milchstraße. In Abbildung 5.2 ist oben rechts ein Stern weiß eingerahmt. Nicht etwa, weil er irgendwie besonders wäre (im All existiert weder wörtlich noch im übertragenen Sinne etwas Ausgezeichnetes). Vielmehr geht es um Hubble selbst, dessen Technik ungewollt dem Stern etwas verleiht, was er in Wahrheit nicht hat: Zacken. Und die sind auch nur auf der Abbildung unbewegt.
103
5.2
5
Mit Argusaugen – der nächtliche Sternenhimmel
Abbildung 5.2 Kollision zweier Spiralgalaxien – aufgenommen vom HubbleWeltraumteleskop (April 2008)
Auch unserer Stern soll zackig sein. Doch wo kommen die Zacken her? Auch Hubble zählt, wie fast alle modernen Teleskope, zur Kategorie der Spiegelteleskope. Deren prinzipieller Aufbau besteht aus einem als Objektiv dienenden Hohlspiegel sowie aus dem sogenannten Fang- oder Sekundärspiegel. Während bei einem Linsenteleskop der Brennpunkt hinter dem Objektiv liegt (wo sich schließlich auch das erwartungsvolle Auge befindet), liegt er beim Spiegelteleskop vor dem Objektiv, also mitten im Strahleneingang des Hohlspiegels. Ungünstiger geht es kaum! Das Licht dort hinaus zu bekommen, ist Aufgabe des Fangspiegels. Wie er das macht, ist in dem Zusammenhang so sekundär wie der Spiegel selbst. Entscheidend sind die Streben, mit denen der Fangspiegel am Teleskop befestigt ist und die für einfallendes Licht unnatürliche Hindernisse darstellen. Ein Beugungsmuster entsteht: unsere Zacken.
104
Entwicklung der Benutzeroberfläche
Auf den folgenden Seiten erwartet Sie die programmiertechnische Nachbildung 왘
von Sternen, die sichtbar sind, bevor das Auge sich ausreichend angepasst hat. Meist sind das besonders helle und/oder erdnahe Gestirne (gemessen am »kosmischen Einheitsmaß«, dem Lichtjahr) . Wir nennen sie Sterne der 1. Kategorie.
왘
des Effekts, dass mit zunehmender Gewöhnung des Auges an die Dunkelheit mehr und mehr Sterne sichtbar werden – diese nennen wir Sterne der 2. Kategorie.
왘
eines flackernden Sterns mit zwei animierten Zacken. Den rechnen wir den Sternen der 3. Kategorie zu.
5.2.1
Weitere Anforderungen an die Anwendung
Einen Eingriff ins kosmische Geschehen leisten wir uns. Auch wenn es grotesk erscheint: Die Anzahl der Sterne, die sich erst mit der Zeit dem Auge zu erkennen geben (Sterne der 2. Kategorie) bestimmen Sie. Ob Sie das durch ein DropdownListenfeld, eine Combo- oder ListBox tun, spielt dabei keine Rolle. Hauptsache, es kann gewählt werden. Demokratie im Weltraum sozusagen. Die netteste Dynamik spiegelt die Anwendung eher als Fußnote wieder. Abermals Sterne der 2. Kategorie betreffend, wird bei jedem stellaren »Neuzugang« dessen Position im Sternenfenster in zwei separaten Textboxen (x- und y-Position) dargestellt. Werden im geplanten Abstand von 600 Millisekunden Sterne eingeblendet, gibt es viel Bewegung in den Boxen. Benutzerdefiniert kann die Anwendung erst dann geschlossen werden, wenn der Letzte der Sterne zweiter Kategorie eingeblendet ist. Erst dann soll ein kleines Menü zur Verfügung stehen. Wird auf den Eintrag Space-Window schliessen geklickt, verschwindet das Fenster erst, wenn nach einer gewissen Verzögerung kein Stern mehr zu sehen ist und die letzten beiden Textbox-Einträge gelöscht sind. Zumindest unter optischen Aspekten geht es eher darum, die Anwendung herunterzufahren, als darum, sie im Hauruckverfahren zu schließen.
5.3
Entwicklung der Benutzeroberfläche
Im Schnelldurchgang: Klicken Sie im Hauptmenü der Visual C# 2010 Express Edition auf Datei. Im Kontextmenü entscheiden Sie sich für die Option Neues Projekt. Eine gleichnamige Dialogmaske wird geöffnet, in der Sie unter Von Visual Studio installierte Vorlagen den Eintrag WindowsForms-Anwendung wählen. Wie in Abbildung 5.3 tragen Sie unter Name »Space_Impressions« ein. Wenn Sie diese Angaben mit OK bestätigen, werden die benötigten Dateien generiert, auf die Sie bequem im Projektmappen-Explorer zugreifen können. 105
5.3
5
Mit Argusaugen – der nächtliche Sternenhimmel
Abbildung 5.3
Anlegen eines neuen Projekts in der Dialogmaske »Neues Projekt«
Wenn Sie die Datei Form1.cs im Entwurfmodus öffnen, zeigt sie die WindowsForm mit Default-Festlegungen der Eigenschaften. Ein paar von denen müssen wir im Eigenschaften-Fenster ändern. Viel Arbeit ist es nicht! 왘
Legen Sie die Text-Eigenschaft von Form1 auf Space-Window fest. So soll unser Fenster zu den Sternen nämlich heißen.
왘
Legen Sie die Width-Eigenschaft auf 1037 und die Height-Eigenschaft auf 553 fest.
왘
Legen Sie die MaximizeBox-Eigenschaft auf False fest. Space-Window soll nur eine definierte Größe haben (schließlich können Sie Ihr Wohnzimmerfenster auch nicht skalieren). Damit das auch tatsächlich gewährleistet ist, 왘
ändern Sie zusätzlich noch die FormBorderStyle-Eigenschaft von Sizable auf FixedSingle.
Das hätten wir. Abgesehen davon, dass das Fenster selbst auch ein Control ist, werden natürlich weitere Steuerelemente benötigt. Welche das sind, verrät Ihnen Abbildung 5.4.
106
Entwicklung der Benutzeroberfläche
Abbildung 5.4
Entwurfsansicht der Benutzeroberfläche
Gleich unter dem Fenstertitel angeordnet, liegt ein MenuStrip-Control (menuStrip1) mit einem Submenü (Space-Window schliessen). Das Menü selbst soll nicht anders als Menü heißen. Was die am unteren Ende des Fensters angeordneten sieben Controls anbelangt, so braucht die Ausrichtung nicht pixelgenau zu erfolgen. Mit Coding for Fun in C# müssen Sie weder den Projektleiter noch einen Design-Dozenten gnädig und milde stimmen. Von mir ganz zu schweigen. Gleichwohl wäre es schön, würden die Steuerelemente auf einer Linie aufgereiht. Der einfachste Weg dorthin ist der, im Eigenschaften-Fenster unter Location die Y-Eigenschaft sämtlicher Controls auf denselben Wert festzulegen (in meinem Fall war das 489). Tipp Eine ruhige Hand vorausgesetzt, können Sie die Positionierung der Controls auch mithilfe der Orientierungslinien der Entwicklungsumgebung vornehmen. Die treten automatisch in Erscheinung, wenn Sie mit der Maus ein Steuerelement über die WindowsForm bewegen.
Auch drei Label-Controls sind mit von der Partie, deren Text-Eigenschaft Sie auf 왘
xPos,
왘
yPos sowie
왘
Anzahl einzublendender Sterne
festlegen. Die Schrift sollte gut zu sehen sein, weswegen unter Font der Eigenschaft Bold ein True zuzuweisen ist. Schriftfarben sind weitgehend Geschmacksfragen. Bei der Entwicklung des Praxisbeispiels stand mir der Sinn nach einem dunklen Rot. Den müssen Sie nicht teilen. Was auch immer Sie für eine Farbe suchen, suchen Sie im Eigenschaften-Fenster des entsprechenden Labels nach der
107
5.3
5
Mit Argusaugen – der nächtliche Sternenhimmel
Eigenschaft ForeColor. Im Dropdown-Listenfeld können Sie zwischen Web- und Systemfarben wählen. DarkRed entdeckte ich auf dem Reiter Web. Die Width-Eigenschaft der beiden TextBox-Controls legen Sie bitte auf den Wert 50 fest, womit wir beim Steuerelement ComboBox angekommen wären. Das erste Mal ist es nicht, dass Sie in diesem Buch angehalten werden, eine ComboBox mit Werten zu füllen. Beim Herman-Gitter aus Kapitel 4 geschah das über den Eintrag Einträge bearbeiten im Menü ComboBox-Aufgaben. Diesmal wählen wir den Weg über das Eigenschaften-Fenster der ComboBox (comboBox1). Das Prinzip ist dasselbe, der Weg zum Zeichenfolgen-Editor ist allerdings um ein Fenster kürzer. Klicken Sie dazu im Eigenschaften-Fenster auf den »dreigepunkteten« Button hinter dem Eintrag Items. Schon steht der Editor bereit. Tragen Sie die Werte 10, 30, 60, 70, 90, 110, 130 ein, und bestätigen Sie mit Ok.
5.4
Entwicklung der Programmierlogik
Erschrecken Sie nicht! In diesem Anwendungsbeispiel werden wir erstmalig Delegates einsetzen. Und freuen Sie sich, wenn Ihnen schnell klar wird, wie überflüssig das ist. Es schadet aber auch nicht, wenn Sie wissen, wie eine Methode eher indirekt, über die Adresse aufzurufen ist. Hinzu kommt: Durchgängig werden beim Sternenfenster Delegates nicht verwendet. Traditionelle Methodenaufrufe kommen demnach auch und in überwiegender Mehrheit vor. Lediglich auf zwei Methoden werden Delegates angesetzt. Und noch ist es nicht so weit. An die Arbeit! Im Head-Bereich der Klasse Form1 soll es uns zunächst darum gehen, drei Objekte zu erzeugen, und zwar Objekte von Klassen. Im Einzelnen handelt es sich um folgende Klassen: 왘
DelayTime
왘
StopTimer
왘
Creating
Beteiligt an der Objekterzeugung sind Konstruktoren desselben Namens (natürlich!). Und ansonsten wissen Sie ja, wie das geht. Ach ja: Legen Sie die Sichtbarkeit der Objekte bitte auf private fest. Demnach schreiben Sie: private DelayTime delTime = new DelayTime(); private StopTimer stTimer = new StopTimer(); private Creating dr = new Creating();
108
Entwicklung der Programmierlogik
Bitte kommentieren Sie die ersten beiden Zeilen zunächst aus (//)! Bereits nach wenigen Seiten werden Sie in die Verlegenheit kommen, die Anwendung zu testen – was schiefgehen wird, wenn Objekte von Klassen erzeugt werden sollen, die es nicht gibt. Jede der Klassen finden Sie später im Namensraum Space_Impressions. Wo Sie die Klassen nicht finden werden, ist in der Klassendatei Form1.cs. Bei unserem kleinen Space-Projekt wollen wir uns nämlich ein wenig Übersichtlichkeit und Struktur erarbeiten, was ab einem bestimmten Umfang an Code auch Sinn macht. Zudem gilt auch oder gerade in der .NET-Welt das eherne Gesetz der Programmierung: Oberfläche und Funktion sind zu trennen. – Ja, auch ich ignoriere das zuweilen. Ferner erzeugen wir zwei Objekte der Timer-Klasse. Diese Klasse ist im Namensraum System.Threading zu finden und stellt einen Mechanismus zum intervallmäßigen Ausführen einer Methode bereit. Auch bei der Klasse Timer geht es nicht gut ohne den Einsatz eines Konstruktors: private Timer starsTimer = new Timer(); private Timer animateTimer = new Timer();
Was die beiden privaten Objekte unterscheidet, ist die Art der Sterne, die von den Instanzen der Klassen timer bedient werden. Die sukzessive einzublendenden Hintergrundsterne (Sterne der 2. Kategorie) sind Aufgabe des Objekts starsTimer, während animateTimer für die Animation eines einzigen Sterns sorgt. Damit das nicht alles in stupider Regelmäßigkeit erfolgt, benötigen wir ein Objekt der Klasse Random (Namensraum System): private Random sr = new Random();
Zufälle, die keine sind Die Klasse Random ist der C#-Beitrag zum hinlänglich bekannten »Zufallsgenerator«, der natürlich nichts wirklich Zufälliges an sich hat. Es handelt sich um einen Pseudozufallsgenerator, das heißt: Zahlen werden entsprechend im Algorithmus festgelegter, statistischer Anforderungen hinsichtlich der Häufigkeit ihres Vorkommens erzeugt.
Dann machen wir uns an die Deklaration bzw. Initialisierung diverser Variablen, die allesamt vom Elementartyp int sind: private int n = 1; private int l = 0; private int s, f, x, y, w, h, r, g, b;
Wofür wir die zahlreichen int-Variablen benötigen, werden Sie im Verlauf des Projekts erfahren. Auch hier gilt: Keine Bange!
109
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Im Konstruktor der Klasse Form1 (Form1()) fügen Sie unterhalb der Initialisierungsmethode InitializeComponent() folgende Zeile ein: comboBox1.SelectedIndex = 0;
Hinter dem Einzeiler steckt nicht viel. Es geht lediglich darum, über die SelectedIndex-Eigenschaft der ComboBox-Klasse für die Auswahl einen Default-Wert
festzulegen. Der Index 0 entspricht dem Eintrag 10, d. h., es werden zeitverzögert 10 Sterne eingeblendet, solange der Benutzer keinen anderen Wert eingibt. Als Nächstes erstellen Sie den Eventhandler für den Button Space-Window Open. Klicken Sie dazu im Entwurfsmodus der Datei Form1.cs zweimal auf besagte Schaltfläche, und das Methodengerüst steht: private void button1_Click(object sender, EventArgs e) { } Listing 5.1
Behandlung des Click-Ereignisses im EventHandler »button1_Click()«
Sie können im Rumpf des Handlers die Zeile button1.Enabled = false;
eintragen. Wenn die Enabled-Eigenschaft des Objekts button1 auf false gesetzt wird, bewirkt das die Deaktivierung der Schaltfläche Space-Window Open, sobald der Klick erfolgt ist. Ein zweiter Eventhandler wird gebraucht: jener hinter dem Menüeintrag SpaceWindow schliessen: private void spaceWindowSchließenToolStripMenuItem_ Click(object sender, EventArgs e) { } Listing 5.2 Ereignis
Der EventHandler für das »spaceWindowSchließenToolStripMenuItem_Click«-
Den haben Sie sicher auf dieselbe Weise, d. h. durch zweimaliges Anklicken des Eintrags Space-Window schliessen erzeugt – im Entwurfsmodus der Klassendatei Form1.cs versteht sich. Hier verlassen wir zunächst die Klasse Form1, um uns der Erstellung einer benutzerdefinierten Klasse zu widmen.
110
Entwicklung der Programmierlogik
5.4.1
Ein Klasse mit Ambitionen – Creating
Nichts ist einfacher, als »Space_Window« eine Klassendatei zu spendieren, was Sie am Ende des Projekts gleich dreimal getan haben werden. Wir beginnen mit der Klasse, die den wichtigsten Part und den selbsterklärendsten Namen hat: Creating. Methoden der Klasse Creating zeichnen 왘
einen Hintergrund mit Farbverlauf, wobei absichtlich kein »gefadetes« Image zum Einsatz kommt
왘
Sterne der Kategorie 1, 2 und 3
왘
Linien
Das ist eine Menge Arbeit für die Klasse Creating. Schaffen wir zunächst die Voraussetzungen: Im Projektmappen-Explorer klicken Sie mit der rechten Maustaste auf Space_Window. Weiter geht es mit Hinzufügen und einem abschließenden Klick auf Klasse. Was sich daraufhin öffnet, zeigt Abbildung 5.5 genauso wie den Eintrag im Editorfeld Name.
Abbildung 5.5 Festlegung des Dateinamens in der Dialogmaske »Neues Element hinzufügen – Space_Window«
Creating.cs ist also der Name der ersten von drei Klassendateien, die Sie mit einem Klick auf den Button Hinzufügen erzeugen. Damit wäre auf Ebene der Vorerzeugung alles getan. Doppelklicken Sie auf die Datei, und im Hauptfenster der Entwicklungsumgebung erwartet Sie Folgendes: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
111
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
namespace Space_Impressions { class Creating { } } Listing 5.3
Vorerzeugter Inhalt der Klassendatei »Creating.cs«
Das sieht recht ordentlich aus, doch weiß weder die Entwicklungsumgebung noch das .NET-Framework etwas von Ihren weiteren Absichten. Immerhin fallen Ihnen einige using-Direktiven ins Auge, der Namensraum Space_Impressions sowie die Klasse Creating selbst. Nach einem ersten Code-Refresh sollte sich das Ganze dann so präsentieren: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Space_Impressions { public class Creating { public Creating() { } } } Listing 5.4
Die öffentliche Klasse »Creating« mit öffentlichem Konstruktor »Creating()«
Damit das Zeichnen funktioniert, müssen der Klasse zwei Namensräume bekannt gemacht werden, weswegen die using-Direktiven um folgende Anweisungen zu ergänzen sind: using System.Drawing; using System.Drawing.Drawing2D;
Dem Erstellen von Methoden steht damit nichts mehr im Wege.
112
Entwicklung der Programmierlogik
Hintergrundarbeit Dabei geht es nicht um den Einsatz nur einer Farbe (naheliegenderweise Schwarz), sondern um zwei Farben: Schwarz und ein leuchtendes Blau (im schicken Englisch: LightBlue), die durch das Programm so miteinander verwoben sind, dass ein Farbverlauf entsteht. Geht es um das Zeichnen von Grafik und Text in WindowsForms, führt nur wenig an der Klasse Graphics (Namensraum System.Drawing) vorbei. Mit besagter Klasse stellt das .NET-Framework eine Programmierschnittstelle namens GDI+ (Graphic Device Interface) bereit. Ohne die wird es schwierig, sich auf einem Formelement künstlerisch zu betätigen (abgesehen von der Verwendung eines PictureBox-Controls). Es ist ein bisschen so, als würden Sie versuchen, eine PowerPoint-Präsentation in die Luft zu projizieren. Dessen ungeachtet, ist es nicht unbedingt naheliegend, der Methode – die wir nebenbei bemerkt fading()nennen wollen – zum Zeichnen des Hintergrunds einen Parameter vom Typ Graphics zu übergeben. Bevor das erörtert wird, zeige ich Ihnen hier die Methodensignatur: public void fading(Graphics g){} fading() als öffentlicher Member der gleichfalls öffentlichen Klasse Creating
wird in der Klasse aufgerufen, die für die Darstellung des Sternenfensters zuständig ist: Form1. In dieser wird gezeichnet. Nirgendwo sonst. Und Form1 ist auch der Ort, an dem, ungeachtet anderer Möglichkeiten, ein Graphics-Objekt zur Weitergabe an die Methode fading() erzeugt werden sollte. Zunächst aber finden Erzeugungen im Rumpf der Methode fading() statt. Den Anfang macht ein Objekt der Struktur Rectangle: Rectangle rcf = new Rectangle(0, 0, 1037, 553);
Mit der Bestimmung der Koordinaten der linken oberen Ecke (0, 0) sowie der Größenverhältnisse (1037, 553) ist alles gesagt. Der Hintergrund soll die gesamte Fläche des Formelements ausfüllen. Deswegen sind die Rechteck- und die Formelement-Größe identisch. Völlig ungeachtet dessen, ob eine Graphics-Zeichenoberfläche existiert – ein Pinselobjekt der Klasse LinearGradientBrush (Namensraum System.Drawing. Drawing2D) können Sie immer erzeugen: LinearGradientBrush fBrush = new LinearGradientBrush(rcf, Color.Blac k, Color.LightBlue, LinearGradientMode.Vertical);
113
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Zugegeben, der Pinsel fBrush ist borstig, und »borstig« ist auch das, was bei der Erzeugung der Instanz fBrush dem Konstruktor an Parametern zu übergeben ist: 왘
die Objektvariable rcf
왘
Wichtig sind natürlich Black und LightBlue als Farben des Übergangs. Diese werden als Eigenschaften der Color-Struktur übergeben. Die Reihenfolge ist ebenfalls wichtig. Der Übergang soll von LightBlue (Color.LightBlue) nach Black (Color.Black) erfolgen, getreu der nicht unberechtigten Annahme, dass es nach oben hin immer dunkler wird (obgleich es im Weltall selbst kein Oben und Unten gibt).
왘
Die Richtung des linearen Farbverlaufs soll vertikal sein. Leider existiert in der LinearGradientBrush-Klasse keine gleichnamige Eigenschaft, wohl aber gibt es in der Enumeration LinearGradientMode (Namensraum System.Drawing.Drawing2D – ja, auch Enumerationen sind in Namensräumen organisiert) eine Konstante Vertical. Auf die kann höchst einfach zugegriffen werden: LinearGradientMode.Vertical.
Im Besitz eines Pinsels zu sein, ist eine Sache. Mit ihm ein Rechteck zu zeichnen, ist eine andere. Eines von zahlreichen Membern der Graphics-Klasse ist die vierfach überladene Methode Rectangle(). Wir übergeben der Methode zwei Parameter: den Pinsel fBrush und rcf als Objekt der Rectangle-Struktur. g.FillRectangle(fBrush, rcf);
Wenn der durch g (als Variable vom Typ Graphics) definierte Zeichenbereich nicht mehr benötigt wird, muss er freigegeben werden, um den Arbeitsspeicher zu entlasten. Da hilft uns Dispose() als weiteres Member der Graphics-Klasse auf die Sprünge: g.Dispose();
Damit wäre die Methode fading() komplett: public void fading(Graphics g) { Rectangle rcf = new Rectangle(0, 0, 1037, 553); LinearGradientBrush fBrush = new LinearGradientBrush(rcf, Color.Black, Color.LightBlue, LinearGradientMode.Vertical); g.FillRectangle(fBrush, rcf); g.Dispose(); } Listing 5.5
114
Zuständig für einen (hoffentlich) ansprechenden Farbverlauf: die Methode »fading()«
Entwicklung der Programmierlogik
Das Zeichnen der Sterne Kommen wir – endlich – zum Zeichnen der Sterne. Sinnigerweise lautet der Name der Methode, die genau das leistet, paintStars(). Die Methode ist öffentlich, und sie gibt nichts zurück. Dagegen ist das, was paintStars() an Parametern erhält (obgleich es im Wesentlichen bekannt ist), alles andere als nichts. Ein Blick auf die zeilenfüllende Signatur zeigt: public void paintStars(Graphics g, Color c, int xpos, int ypos, int width, int height){}
Erwartet werden neben einer weiteren Graphics-Zeichenoberfläche g und einer Color-Struktur c Angaben zu Positionen (xpos, ypos) sowie zu Dimensionen (width, height). Damit sind wir im Rumpf der Methode angekommen, und auch hier macht eine Rectangle-Struktur den Anfang: Rectangle rcs = new Rectangle(xpos, ypos, width, height);
Hintergrund, Sterne, Zacken – in jedem Fall brauchen wir einen Pinsel: SolidBrush sbrush = new SolidBrush(c);
Doch irgendetwas stimmt nicht, schließlich sind Sterne nicht rechteckig, sondern eher rund, zumindest dann, wenn die Sichtweise zweidimensional ist. Es läuft also auf eine weitere Methode hinaus: FillEllipse() (natürlich ein Member der Klasse Graphics) füllt das Innere einer Ellipse. Eine Ellipse ist durch ein umschließendes Rechteck definiert, das wiederum durch rcs als instanziierte RectangleStruktur angegeben ist. Hinzu kommt der Pinsel sbrush. Damit wären wir am Ende, was im Falle der Methode paintStars() so aussieht: g.FillEllipse(sbrush, rcs);
Es muss nicht eigens erwähnt werden, wie durch ein Gleichsetzen der Inhalte von width und height später aus der Ellipse ein Kreis wird. So entstehen Sterne (wenn es in der Natur auch so einfach wäre)! Das Endergebnis sieht so aus: public void paintStars(Graphics g, Color c, int xpos, int ypos, int width, int height) { Rectangle rcs = new Rectangle(xpos, ypos, width, height); SolidBrush sbrush = new SolidBrush(c); g.FillEllipse(sbrush, rcs); } Listing 5.6
Eine Methode für alle Sterne – »paintStars()«
115
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Schlanke Linie Immer einfacher scheinen die Methoden der Klasse Creating zu werden. Was die dritte und letzte an Parametern aufnimmt, sind (neben dem bekannten g und c) vier int-Variablen. Die ersten beiden geben die Koordinaten des Linienanfangs an, die letzten beiden das Ende: public void paintLine(Graphics g,Color c, int p1x, int p1y, int p2x, int p2y){}
So unverzichtbar ein Pinsel für Hintergrund und Stern ist, so notwendig ist der Stift zum Zeichnen einer Linie: Pen linePen = new Pen(c);
Die Klasse Pen finden Sie am selben Ort wie Brush, im Namensraum System.Drawing nämlich. Und genauso wie dem Konstruktor der Brush-Klasse eine ColorStruktur übergeben wurde, tun Sie das auch bei Pen. Es gibt also wenig Neues im Code. Wenn Sie sich abschließend noch einmal unter den Membern der Klasse Graphics umsehen, entdecken Sie die Methode DrawLine(). Was die erwartet, können Sie an fünf Fingern abzählen: ein Pen-Objekt (linePen) und zwei mal zwei Koordinaten: g.DrawLine(linePen, p1x, p1y, p2x, p2y);
So sieht das Endergebnis aus: public void paintLine(Graphics g,Color c, int p1x, int p1y, int p2x, int p2y) { Pen linePen = new Pen(c); g.DrawLine(linePen, p1x, p1y, p2x, p2y); } Listing 5.7
Zum Strahlen gebracht – die Methode »paintLine()«
Die wichtigste Klasse des Projekts wäre somit erstellt. Schauen Sie: using using using using using
System.Collections.Generic; System.Linq; System.Text; System.Drawing; System.Drawing.Drawing2D;
namespace Space_Impressions
116
Entwicklung der Programmierlogik
{ public class Creating { public Creating() { } public void fading(Graphics g) { Rectangle rcf = new Rectangle(0, 0, 1037, 553); LinearGradientBrush fBrush = new LinearGradientBrush(rcf, Color.Black, Color.LightBlue, linearGradientMode.Vertical); g.FillRectangle(fBrush, rcf); g.Dispose(); } public void paintStars(Graphics g, Color c, int xpos, int ypos, int width, int height) { Rectangle rcs = new Rectangle(xpos, ypos, width, height); SolidBrush sbrush = new SolidBrush(c); g.FillEllipse(sbrush, rcs); } public void paintLine(Graphics g, Color c, int p1x, int p1y, int p2x, int p2y) { Pen linePen = new Pen(c); g.DrawLine(linePen, p1x, p1y, p2x, p2y); } } } Listing 5.8
Pinsel, Farben und Formen – die Klasse »Creating«
Öffnen wir das Sternenfenster.
117
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
5.4.2
Zurück zur Klasse »Form1«
Zunächst testen wir den Hintergrund. Zu Beginn der Form1-Klasse haben wir ein Objekt (dr) der Klasse Creating erstellt. Creating selbst existiert in der Klassendatei Creating.cs. Member der Klasse ist u. a. die Methode fading(), der ein Parameter vom Typ Graphics zu übergeben ist. Seit Abschnitt 5.4.1 wissen Sie um die Zusammenhänge. Tragen Sie nun im Rumpf des Eventhandlers button1_Click() folgende Zeile ein, in der mithilfe der CreateGraphics()-Methode ein Graphics-Zeichenobjekt instanziiert wird: Graphics grf = this.CreateGraphics();
In der nächsten Zeile findet sich grf als Argument der Methode fading() wieder: dr.fading(grf);
Abschließend zerstören Sie das Objekt grf mit: grf.Dispose();
Starten Sie die Anwendung mit ((F5)) ohne Debug ((Strg)+(F5)). Abbildung 5.6 zeigt das, was Sie nach einem Klick auf die Schaltfläche Space-Window Open erwarten sollte.
Abbildung 5.6
118
Von Blau nach Schwarz – der Hintergrund
Entwicklung der Programmierlogik
5.4.3
Sterne der 1. Kategorie
Sterne der 1. Kategorie sind jene, die nicht mit sichtbarer Verzögerung im Sternenfenster erscheinen. Namensgebend für die zu entwickelnde Methode ist der Umstand, dass Sterne der 1. Kategorie keinen Timer benötigen. Werfen Sie bitte zunächst einen kurzen Blick auf die Signatur der Methode nonTimerStars(): void nonTimerStars(){}
Analog zum Vorgehen beim Testen des Hintergrunds erzeugen wir auch hier zunächst ein Graphics-Zeichenobjekt (es hat sogar denselben Namen): Graphics gr = this.CreateGraphics();
Schnüren wir für die Variablen r, g, b ein Initialisierungspäckchen: r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255);
Sie vermuten richtig: Ja, es geht um Farbanteile! Und in der Tat steht r für Rot, g für Grün und b für Blau. Doch während r ein fester Wert zugewiesen wird (250), ist der Inhalt von g und b abhängig von den »Launen« des »Zufallsgenerators« Random. Das heißt, nicht ganz, denn die Werte wechseln innerhalb wohldefinierter Grenzen. Wenn diese Werte dem Random-Member Next() übergeben werden, sorgen sie schon für einen Mix, der sich durch das Zusammenspiel der Farbanteile zwischen Weiß, Gelb- und Orangetönen bewegt. Dies sind die Farben unserer Sterne. Lassen Sie bei alledem bitte Folgendes nicht außer Acht: sr ist ein Objekt der Klasse Random. Das haben wir am Anfang der Klasse Form1 vereinbart. Mit den angegebenen, teils »zufälligen« Farbwerten initialisieren wir über die Methode FromArgb(), die ein Member der Struktur Color ist, die Variable co: Color co = Color.FromArgb(r, g, b);
Vier weitere Initialisierungen folgen, die allesamt vom Objekt sr der Klasse Random abhängig sind. Dasselbe Spiel wie oben. Nur sind sozusagen in der Methode Next() die Grenzen verschoben: x y w h
= = = =
sr.Next(0, 1000); sr.Next(0, 500); sr.Next(2, 5); w;
Die oben gezeigten Initialisierungen definieren eine pseudozufällige Position (x, y für die rechte obere Kante) sowie die gleichfalls pseudozufällige Größe eines
119
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Rechtecks. Bei dem ist die Breite w gleich der Höhe h. Somit haben wir es mit einem Quadrat zu tun. All das erinnert an die Methode paintStars(), die in der Klasse Creating (von der dr als Objekt instanziiert wurde) auf ihren Einsatz wartet. Im Wesentlichen geschieht in der Methode nonTimerStars() nämlich nichts anderes, als dass für paintStars() die erforderlichen Werte bereitgestellt werden. So steht dem Aufruf des »großen Zeichenmeisters« nichts mehr im Wege: dr.paintStars(gr, co, x, y, w, h);
Rufen Sie nonTimerStars() bitte nicht im Eventhandler des Buttons Space-Window einblenden auf. Obgleich es programmiertechnisch richtig ist, wäre, mit einem gezeichneten Stern, das Ergebnis eher nüchtern. Wo er sich im Fenster befände und welche Größe und Farbe er besäße – ich könnte es Ihnen nicht sagen. Aber etwas anderes verrate ich Ihnen: Wie Sie mithilfe eines netten Features der Entwicklungsumgebung rasch zur Vermehrung der Sterne kommen (wir brauchen nämlich 30): 1. Markieren Sie in der Methode nonTimerStars() jede Zeile nach jener zur Erzeugung des Graphics-Zeichenobjekts. Zur Kontrolle: Es sind 9, Leerzeilen nicht eingerechnet. 2. Nach einem Rechtsklick wählen Sie im Menü Umschliessen mit. Abbildung 5.7 zeigt, was sich daraufhin öffnet. 3. Doppelklicken Sie auf den Listeneintrag while. Damit sind die neun relevanten Zeilen im Rumpf einer while-Schleife gekapselt.
Abbildung 5.7
120
In C# kann man Code mit vielem umschließen ...
Entwicklung der Programmierlogik
Zwei Kleinigkeiten fehlen: 1. Ändern Sie die Bedingung der while-Schleife von true auf s <= 30. 2. Inkrementieren Sie nach Aufruf der paintStars()-Methode die Laufvariable s (s++;). Die Arbeit an der Methode nonTimerStars() endet mit der Zerstörung des Zeichenobjekts gr (was außerhalb der while-Schleife geschieht): gr.Dispose();
Die Methode nonTimerStars() in Gänze: private void nonTimerStars() { Graphics gr = this.CreateGraphics(); while (s <= 30) { r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255); Color co = Color.FromArgb(r, g, b); x y w h
= = = =
sr.Next(0, 1000); sr.Next(0, 500); sr.Next(2, 5); w;
dr.paintStars(gr, co, x, y, w, h); s++; } gr.Dispose(); } Listing 5.9
In einem Rutsch – Einblenden von dreißig Sternen der 1. Kategorie
Rufen Sie nun im Eventhandler des einzigen Buttons, unterhalb der Routine fading(), die Methode nonTimerStars()auf. Starten Sie die Anwendung. Abbildung 5.8 zeigt, was Sie nach einem Klick auf die Schaltfläche Space-Window Open erwartet. Natürlich wird bei Ihnen die Konstellation der Sterne eine andere sein, genau wie die Farb- und Größenverteilung.
121
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Abbildung 5.8
Zur Laufzeit-Darstellung von Sternen der 1. Kategorie
Machen wir uns jetzt auf zu den Sternen der 2. Kategorie, also jenen, die erst nach einer gewissen Zeit für das Auge sichtbar werden, das sich erst an die Dunkelheit gewöhnt.
5.4.4
Sterne der 2. Kategorie
Vom Standpunkt der Programmierung aus betrachtet, ist der primäre Unterschied zwischen Sternen der 1. und 2. Kategorie der, dass Letztere über einen im Intervall aufgerufenen Eventhandler realisiert werden. Dafür wird das eingangs erstellte Timer-Objekt starsTimer verwendet. Hier sehen Sie die Signatur des Handlers (oder wie es im sperrigen Deutsch heißt: des Ereignisbehandlers): void starsTimer_Tick(object sender, EventArgs e){}
Fügen Sie das Gerüst der Methode in Form1.cs ein! Sollte Ihnen dabei jene Zeile ins Auge fallen, in der ein Objekt der Klasse StopTimer erzeugt wird, ist das gut. Zu der kommen wir nämlich jetzt. Einen Riegel vorgeschoben – die Klasse »StopTimer« Das Kerngeschäft der Klasse StopTimer ist der Vergleich. Verglichen wird die im Dropdown-Listenfeld ausgewählte Anzahl Hintergrundsterne mit der Häufigkeit, mit der die Ereignisbehandlungsmethode starsTimer_Tick() bislang ausgeführt wurde. Den Anstoß dazu gibt das Timer-Objekt starsTimer.
122
Entwicklung der Programmierlogik
Den Vergleich selbst realisiert natürlich weniger die Klasse als eine Methode, um die wir uns erst kümmern müssen. Zunächst jedoch gilt es auch in dieser Klasse, eine andere Methode bereitzustellen, nämlich jene, die auch Konstruktor genannt wird. Im Verbund – vorerzeugter Klassenrumpf zuzüglich Konstruktor – erhalten wir damit in der Klassendatei StopTimer.cs: using using using using using
System; System.Collections.Generic; System.Linq; System.Text; System.Windows.Forms;
namespace Space_Impressions { public class StopTimer { public StopTimer() { } } } Listing 5.10
Der Konstruktor »StopTimer()« im Rumpf der gleichnamigen Klasse
Wird nun verdächtig schnell die Ebene der Klasse StopTimer verlassen, um sich stattdessen, oberhalb der Klasse StopTimer, im Namensraum Space_Impression nach einem schattigen Plätzchen umzusehen, ahnen Sie womöglich den Grund: die Deklaration eines Delegate. Hier ist sie: public delegate int timerst(int k, ComboBox cb);
Für timerst ist zweierlei wichtig: eine Variable k vom Typ int und eine andere, cb, vom Typ ComboBox. Eine Ebene tiefer, und wir sind zurück im Rumpf der Klasse StopTimer. Dort, gleich zu Beginn, soll ein Objekt tst des Delegate timerst erzeugt werden, wobei dem Konstruktor die Adresse der zu implementierenden Methode übergeben wird: public timerst tst = new timerst(tSto);
Kommen wir nun zur Methode tSto: Wen wundert es, dass diese einen int-Wert zurückgeben soll – siehe die Deklaration des Delegate timest? Und wen wundern die beiden Argumente der Methode, von denen eines, j, vom Typ int ist, während das andere, cob, vom komplexeren Typ ComboBox ist (siehe auch hier die De-
123
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
klaration des Delegate)? So viel zur Signatur der Methode tSto(), die damit folgendermaßen aussieht: static int tSto(int j, ComboBox cob){}
Das Innenleben der Methode tSto(int j, ComboBox cob) ist schon ein wenig bewegt, wofür es zwei Gründe gibt: 왘
Bei jedem Aufruf des Eventhandlers starsTimer_Tick()wird auch die Methode tSto() (durch die Hintertür, d. h. über das Delegate-Objekt tst) ausgeführt.
왘
Bei jedem Aufruf des Eventhandlers starsTimer_Tick() liefert die Methode tSto() (gleichfalls durch die Hintertür, also über das Delegate-Objekt tst) einen int-Rückgabewert.
Ist »das Maß voll«, d. h., ist die Anzahl schrittweise eingeblendeter Sterne gleich dem, was im Dropdown-Listenfeld Anzahl einzublendender Sterne ausgewählt wurde, wird 1 zurückgeliefert, anderenfalls 0. Natürlich läuft die Anforderung auf eine if-else-Struktur hinaus, die bereits hier, verankert im Rumpf der Methode tSto(), wiedergegeben werden soll: public static int tSto(int j, ComboBox cob) { if (j == Convert.ToInt16(cob.Text)) { return 1; } else { return 0; } } Listing 5.11
1 oder 0, das ist hier die Frage ... Oder doch nicht?
Über die Eigenschaft Text der ComboBox-Klasse wird das Ausgewählte abgerufen. Verlieren Sie dabei nicht den Ursprung der Variablen cob aus den Augen. Das war, besser gesagt ist nämlich die Klasse ComboBox. Demnach heißt es jetzt: cob.Text
Der anvisierte Vergleich des Inhalts der int-Variablen j (Anzahl der bereits schrittweise eingeblendeten Sterne) mit dem Ausgewählten ist ohne eine Konvertierung schwierig. Schließlich liegen die Inhalte der ComboBox im Format string
124
Entwicklung der Programmierlogik
vor. Hier schafft die Methode ToInt16() der Klasse Convert (aus dem Namensraum System) Abhilfe: Convert.ToInt16(cob.Text)
Nun kann im 600-Millisekunden-Takt verglichen werden. Zuvor sollten Sie jedoch unter die Liste vorerzeugter using-Direktiven noch Folgendes setzen: using System.Windows.Forms;
Warum? Überlegen Sie – vielleicht während Sie die Klasse StopTimer im Endzustand betrachten: using using using using using
System; System.Collections.Generic; System.Linq; System.Text; System.Windows.Forms;
namespace Space_Impressions { public delegate int timerst(int k, ComboBox cb); public class StopTimer { public timerst tst = new timerst(tSto); public StopTimer() { } public static int tSto(int j, ComboBox cob) { if (j == Convert.ToInt16(cob.Text)) { return 1; } else { return 0; } } } } Listing 5.12 Hier wird die Vorarbeit zum Stoppen des Timers »starsTimer« geleistet – die Klasse »StopTimer()«.
125
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Und wieder zurück zur Klasse »Form1« In der Klasse Form1 geht es uns im Weiteren verstärkt um den Handler starsTimer_Tick(). Bedenkenlos und ohne Erklärung können nachstehende Codezeilen zur Verwendung bekannt gemacht werden: Graphics gr = this.CreateGraphics(); r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255); Color co = Color.FromArgb(r, g, b); x y w h
= = = =
sr.Next(0, 1000); sr.Next(0, 500); sr.Next(1, 4); w;
dr.paintStars(gr, co, x, y, w, h); Listing 5.13
Nichts Neues unter dem Himmelszelt
Alles ist wie gehabt: Erzeugung eines Graphics-Objekts (gr), (pseudozufällige) Initialisierung der Farbwerte r, g, b, Vereinbarung einer Color-Struktur, Initialisierung von Positions- und Größenwerten des Rechtecks, Zeichnen der Sterne. Im Intervall von 600 Millisekunden wird die Ereignisbehandlungsmethode ausgeführt. Jeder Durchlauf entspricht einem Stern, der irgendwo im Fenster mit irgendeiner Farbe (abhängig von den in Next() definierten Grenzen) auftaucht. Um die Position des eingeblendeten Sterns anzuzeigen, ist nichts weiter erforderlich, als die Werte der Variablen x und y ins String-Format zu konvertieren und das Ergebnis den Textboxen xPos/yPos über deren Text-Eigenschaft gewissermaßen zuzuweisen: textbox1.Text = Convert.ToString(x); textbox2.Text = Convert.ToString(y);
Als Nächstes tragen Sie die Zeile l = stTimer.tst(n++, comboBox1);
in den Rumpf des Eventhandlers starsTimer_Tick() ein. Zugegebenermaßen sieht diese Zeile etwas kryptisch aus. Wir klären das! Es geht um die Methode tSto() (ein öffentliches Member der Klasse StopTimer), die überprüft, ob so viele Hintergrundsterne eingeblendet wie ausgewählt wurden. Abhängig davon gibt die Methode 1 oder 0 zurück. So weit, so gut.
126
Entwicklung der Programmierlogik
Das alles passiert – nur nicht im besagten Eventhandler. In dem geschieht anderes, nämlich der Aufruf der Methode tsto() über das signaturgleiche DelegateObjekt tst (int-Typ, ComboBox-Typ). Dem werden sowohl das Inkrement der int-Variable n (n++) als auch das Objekt comboBox1 mitgegeben. Letzteres ändert sich nicht, doch bei jedem Ausführen des Handlers erhöht sich der Wert von n, was viel (um nicht zu sagen alles) über die Zahl der eingeblendeten Sterne aussagt. Irgendwann ist Schluss mit neuen Sternen, weil 1 zurückgegeben wurde: Die Bedingung im naheliegenden if-Konstrukt ist erfüllt, der Timer kann gestoppt und das Menü eingeblendet werden: if (l == 1) { starsTimer.Stop(); menuStrip1.Visible = true; } Listing 5.14
Alle Sterne der 2. Kategorie, und das Menü ist in Sicht.
Hier sehen Sie den vollständig codierten Eventhandler: private void starsTimer_Tick(object sender, EventArgs e) { Graphics gr = this.CreateGraphics(); r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255); Color co = Color.FromArgb(r, g, b); x y w h
= = = =
sr.Next(0, 1000); sr.Next(0, 500); sr.Next(1, 4); w;
dr.paintStars(gr, co, x, y, w, h); Textbox1.Text = Convert.ToString(x); Textbox2.Text = Convert.ToString(y); l = stTimer.tst(n++, comboBox1); if (l == 1) { starsTimer.Stop();
127
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
menuStrip1.Visible = true; } } Listing 5.15
Ereignisbehandlung für Sterne der 2. Kategorie
Der Handler nützt wenig ohne einen Mechanismus, der ihn regelmäßig anstößt. Zwar haben wir mit starsTimer eine Instanz der Klasse Timer, doch sonst haben wir nichts. Anders, als in anderen Abschnitten angefangen wurde, beginnen wir trotzdem nicht: Eine Methode muss her! Die Routine retardingStars() erwartet nichts, und sie gibt auch nichts zurück. Über die Intervall-Eigenschaft der Timer-Klasse wird der Zeitraum zwischen den Ticks des Zeitgebers (Timers) festgelegt: starsTimer.Interval = 600;
Ein »Tick« ist im Sinne eines Ereignisses zu verstehen. Das Tick-Ereignis ist Member der Klasse Timer, genauso wie Methoden, Eigenschaften, ja auch Interfaces Klassenmember sein können. Das nur nebenbei bemerkt. Je nachdem, wie aufmerksam Sie bislang gelesen haben, dürfte die nächste Zeile schnell einiges an Unklarheit verlieren: starsTimer.Tick += new EventHandler(starsTimer_Tick);
Richtig: Es geht um einen Delegate, genauer um den EventHandler-Delegate. Der ist in dem Sinne eher speziell, als dass durch EventHandler eine Ereignisbehandlungsmethode für ein Ereignis dargestellt wird, das keine Daten generiert. Welches Ereignis das ist? Sehr einfach: Tick. Um das Ereignis der Ereignisbehandlungsmethode starsTimer_Tick() zuordnen zu können, wird dem Tick-Ereignis eine Instanz des Delegate EventHandler hinzugefügt (new EventHandler(starsTimer_Tick)). Abschließend geht es darum, den Timer durch starsTimer.Start();
zu starten. public void retardingStars() { starsTimer.Interval = 600; starsTimer.Tick += new EventHandler(starsTimer_Tick); starsTimer.Start(); } Listing 5.16
128
Die Methode zur Steuerung des Timer-Objekts »starsTimer« – »retardingStars()«
Entwicklung der Programmierlogik
Um auch Sterne der 2. Kategorie zu testen, rufen Sie lediglich die Methode retardingStars() im Eventhandler button1_Click() auf. Danach können Sie die Anwendung mit oder ohne Debugging starten. Wer weiß! Vielleicht macht sich eines schönen Tages ein kluger Mann daran, animierte Bildschirmfotos zu entwickeln. Bis dahin allerdings müssen Sie mit Abbildung 5.9 vorlieb nehmen:
Abbildung 5.9 Sterne der 1. und 2. Kategorie (Achten Sie auf die Einträge in den beiden Textboxen sowie in der ComboBox.)
5.4.5
Zu guter Letzt – ein Stern der 3. Kategorie
Das vermeintlich Schwierigste kommt zum Schluss: ein Stern, der flackert und strahlt. Über den physikalischen Ursprung der Effekte habe ich zu Beginn des Kapitels einiges gesagt. Was Sie nicht wissen, ist, wie die vermeintliche Unruhe des Sterns in Ihr Programm umzusetzen ist. Vorweggenommen: Schwer ist es nicht. Denn prinzipiell geht es nur darum, bereits vorhandene Methoden halbwegs pfiffig zu arrangieren. Wie bei den Sternen der 2. Kategorie spielt sich auch hier das Wesentliche in einem Eventhandler ab: private void animateTimer_Tick(object sender, EventArgs e){}
Auch in dem wird zunächst eine Graphics-Zeichenoberfläche erzeugt, die r-, g-, bVariablen (pseudozufällig) initialisiert sowie eine Color-Struktur bereitgestellt. Kalter Kaffee, so was schreibt man nicht in einem ordentlichen Buch, doch denke ich, das Folgende ist für Sie zwischenzeitlich genau das:
129
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Graphics gr = this.CreateGraphics(); r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255); Color co = Color.FromArgb(r, g, b);
Bis hierhin ging es um die Pflicht. Was folgt, ist die Kür. Doch auch die nächsten fünf Zeilen werden Sie kaum fordern. Zunächst: f = sr.Next(0, 6);
Abhängig von dem, was in f nach jeder Ausführung des Eventhandlers gespeichert ist, werden der Stern und seine beiden Strahlen entweder »so oder so« dargestellt, was so oder so natürlich nicht so stehen bleibt. Doch warten Sie’s ab! Als Nächstes sind Position und Dimension des Sterns fix zu initialisieren: x y w h
= = = =
500; 200; 7; w;
Die Werte für x, y sollen zugleich als Orientierungspunkt für die Anfangskoordinaten des Strahls im Bezug auf den Stern dienen. Das ist umso einfacher, als dass der Strahl von links oben nach rechts unten diagonal durch jenes Quadrat verläuft, in dem der Pinsel sbrush den Sternenkreis zeichnet. Gesagt, getan, und nun muss von x und y, als dem Koordinatenpaar der linken oberen Quadratecke, ein identischer Wert abgezogen werden. Wir wählen zunächst 10. Für den Endpunkt des Strahls gebe ich Ihnen der Einfachheit halber sofort das Wertepaar an: 515, 215. Was folgt, ist der erstmalige Aufruf der Methode paintLine(): dr.paintLine(gr, Color.Black, x-10, y-10, 515, 215);
Erstaunlicherweise wird als Linienfarbe Schwarz übergeben (Color.Black). Genauso rabenschwarz stellt sich der Stern dar (was ihn noch lange nicht zu einem schwarzen Loch macht): dr.paintStars(gr, Color.Black, x-2, y-2, w+3, h+3);
Und nicht nur das. Die Position des Sterns ist nach links oben verschoben (x-2, y-2 ), und zugleich ist die Dimension vergrößert (w+3, h+3). Ich glaube, es wird Zeit, die Katze aus dem Sack zu lassen: 왘
130
Grundsätzlich erfolgt die Animation des Sterns durch wechselweises Einblenden zweier Sterne von geringfügig unterschiedlicher Dimension. Das erklärt – für den größeren der beiden – die Argumente w+3 bzw. h+3.
Entwicklung der Programmierlogik
왘
Idealisiert als Kreis, wird der Stern innerhalb eines definierten Quadrats gezeichnet. Ein kleiner Stern wird in ein kleines Quadrat gezeichnet, sein etwas größeres Pedant in ein entsprechend größeres Quadrat (w+3 bzw. h+3). Damit die auszutauschenden Sterne einen identischen Mittelpunkt haben, muss das größere Quadrat gegenüber dem kleineren Quadrat verschoben sein. Das erklärt im obigen Aufruf der Methode paintStars() die Argumente x-2, y-2.
왘
Jedes Mal, wenn der Handler animateTimer_Tick() (der gleichwohl noch nicht komplett ist) zur Ausführung kommt, entscheidet die Zufallsvariable f (= sr.Next(0, 6)) darüber, in welcher Dimension der Stern zu zeichnen ist. Dabei verhindert die bedingte Zufälligkeit der Entscheidung ein regelmäßiges Blinken des Sterns und sorgt für das gewünschte Flackern (was wir betrachten, ist schließlich kein behäbiges Leuchtturmfeuer).
왘
Was für den Stern gilt, gilt gleichermaßen für seine Zacken. Das Prinzip ist exakt dasselbe: ein »aperiodisches« Auswechseln von Groß und Klein, hier: von langen und kurzen Zacken, gleichfalls im 30-Millisekunden-Takt des Timer-Objekts animateTimer.
Was bleibt, ist das Rätsel der schwarzen Farbe. Vor dessen Lösung betrachten Sie bitte folgenden Codeauszug. Wenn Sie die Punkte aufmerksam gelesen haben, dürften Sie jetzt keine grundlegenden Fragen mehr haben. Doch Vorsicht! if (f < 5) { dr.paintStars(gr, co, x-2, y-2, w+3, h+3); dr.paintLine(gr, co, x-10, y-10, 515, 215); } else { dr.paintStars(gr, co, x, y, w, h); dr.paintLine(gr, co, x-2, y-2, 508, 208); } Listing 5.17
»Aperiodischer« Austausch von Linien und Sternen unterschiedlicher Dimension
Die Angelegenheit schreit zu dem, was Thema des Projekts ist: zum Himmel. Mit obigem Code allein würde im Wortsinne nämlich nichts bewegt. Flackern kann der zackige Stern erst dann, wenn nach jedem Aufruf zunächst ein geometriegleicher schwarzer Hintergrund zur Verfügung gestellt wird. Spielen wir das Ganze mit drei Methodenaufrufen des Handlers animateTimer_Tick() durch: 1. Erster Aufruf: Zunächst erfolgt die Darstellung eines schwarzen Sterns mit Zacken. Auf den wird abhängig vom Inhalt der Variablen f ein weiteres Exemplar gezeichnet.
131
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
2. Nach 30 Millisekunden erfolgt der zweite Aufruf: Durch abermaliges Zeichnen des schwarzen Sterns wird der vorherige, helle Stern »gelöscht«. Desgleichen die beiden Zacken. Ein neuer Stern kann – entweder groß oder klein – gezeichnet werden. 3. Dritter Aufruf: Der zuletzt gezeichnete Stern wird über den schwarzen Hintergrund »gelöscht«. Ein neuer Stern kann – entweder groß oder klein – gezeichnet werden. Und so weiter. Das alles passiert im erwähnten Abstand von 30 Millisekunden. Die Abfolge ist demnach sehr schnell. Jetzt wissen Sie auch, wie »virtuelle Szintillation« entstehen kann. Weil es so erschöpfend langatmig war, zeige ich hier noch einmal den Eventhandler animateTimer_Tick(): private void animateTimer_Tick(object sender, EventArgs e) Graphics gr = this.CreateGraphics(); r = 250; g = sr.Next(242, 255); b = sr.Next(7, 255); Color co = Color.FromArgb(r, g, b); f x y w h
= = = = =
sr.Next(0, 6); 500; 200; 7; w;
dr.paintLine(gr, Color.Black, x-10, y-10, 515, 215); dr.paintStars(gr, Color.Black, x-2, y-2, w+3, h+3); if (f < 5) { dr.paintStars(gr, co, x-2, y-2, w+3, h+3); dr.paintLine(gr, co, x-10, y-10, 515, 215); } else { dr.paintStars(gr, co, x, y, w, h); dr.paintLine(gr, co, x-2, y-2, 508, 208);
132
{
Entwicklung der Programmierlogik
} } Listing 5.18
Zahlenwirrwar im EventHandler »animateTimer_Tick()«
Die folgende Methode zur Steuerung der Timer-Instanz animateTimer ist analog zur Routine retardingStars() zu verstehen. Denn ganz gleich, worauf ein Timer-Objekt angesetzt wird, das Intervall muss festgelegt, Instanzen des Delegate EventHandler müssen erzeugt werden. Zum Schluss gilt es, den Timer zu starten. Sie kennen das Prozedere: private void animateStar() { animateTimer.Interval = 30; animateTimer.Tick += new EventHandler(animateTimer_Tick); animteTimer.Start(); } Listing 5.19
Die Methode zur Steuerung des Timer-Objekts »animateTimer«: »animateStar()«
Okay, richten wir unser virtuelles Teleskop aus: 1. Rufen Sie die Methode animateStar() im EventHandler button1_Click() auf. 2. Starten Sie die Anwendung, wahlweise ohne ((F5)) oder mit Debugging ((Strg)+(F5)). Abbildung 5.10 zeigt einen Schnappschuss dessen, was das Teleskop sieht.
Abbildung 5.10
Ein flackernder, strahlender Stern der 3. Kategorie
133
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Bis hierhin haben wir alles getan, um Sterne im Space-Window darzustellen. Es ist an der Zeit, das Fenster zu schließen.
5.4.6
Verzögertes Schließen der Anwendung
Über den Menüeintrag Space-Window schliessen soll die Anwendung verzögert geschlossen werden. Der zugehörige Eventhandler spaceWindowSchließenToolStripMenuItem_Click() existiert schon eine ganze Weile. Was in ihm umgesetzt werden soll, ist das Folgende: 1. Zuerst wird das Timer-Objekt starsTimer angehalten. 2. Unmittelbar danach werden sämtliche Sterne dadurch ausgeblendet, dass ein neuer Hintergrund eingeblendet wird. 3. Ab jetzt muss gewartet werden. Zuerst 1500 Millisekunden lang. Dann werden die Textboxen xPos und yPos auf den Eintrag Leerstring zurückgesetzt. Nichts ist mehr zu lesen. 4. Weitere 1500 Millisekunden später erfolgt das Schließen des Fensters sowie die Zerstörung des Graphics-Zeichenobjekts grf. Auch im Eventhandler des Menüeintrags Space-Window schliessen brauchen wir das Objekt, genauer gesagt beim Aufruf der »Hintergrundmethode« fading(). All das funktioniert noch nicht, nicht zuletzt, weil es an der Möglichkeit fehlt, die Programmausführung zu verzögern. Der nächste Abschnitt schafft Abhilfe.
5.4.7
Aufgehoben ist nicht aufgeschoben – die Klasse »DelayTime«
Es ist nicht schade, dass im Sprachumfang der Sprache C# ein Delay-Befehl (oder eine Methode) zur Verzögerung von Ausführungen fehlt. Wirklich nicht? Nein, denn erstens ist Windows ein Multitasking-System (!), und zweitens gilt: Do it yourself! Zwei Möglichkeiten existieren: 왘
Sie schreiben eine eigene Hilfsroutine delay(time).
왘
Sie nutzen die Sleep()-Methode der Klasse Thread (aus dem Namensraum System.Threading).
Einfacher ist die zweite Option – weswegen wir uns für die erste entscheiden. Unsinn, es geht um mehr: Auch wenn es für das Sternenfenster weitgehend irrelevant ist – wenn Sie Sleep()benutzen, ist die komplette Anwendung für einige Sekunden zur Untätigkeit verdammt. Mögliche Eingaben? Fehlanzeige! Der gesamte Prozess inklusive sämtlicher Funktion der GUI, »duselt« vor sich hin. Wir duseln nicht.
134
Entwicklung der Programmierlogik
Denn längst schon haben Sie eine Klassendatei DelayTime.cs erstellt, in der die Entwicklungsumgebung wiederum einige (auch überflüssige) using-Direktiven, den Namensraum Space_Impressions sowie eine leere Klasse DelayTime erzeugt hat. Da wir hier das Puristische im Programmierhandwerk hochhalten, darf im Klassenrumpf der Konstruktor DelayTime() genauso wenig fehlen (obgleich es trotzdem funktioniert, würde doch automatisch eine Art Standardkonstruktor generiert), wie unter den using-Direktiven auf using System.Windows.Forms
zu verzichten ist. Ob Sie sich auch das bereits gedacht haben, weiß ich nicht. Wenn nicht, fügen Sie die Direktive bitte ein. Einzufügen, beispielsweise im Namensraum Space_Impressions, wäre auch die Deklaration eines Delegate, für den nur die Angabe der Millisekunden (gespeichert in der int-Variablen z) wichtig ist. Anders gesagt, wird durch folgende Zeile vereinbart, dass sich der Delegate del auf eine Methode bezieht, die einen Parameter vom Typ int erwartet. Übrigens: Sie dürfen mir gern schreiben, wenn Ihnen das Delegate grammatikalisch richtiger erscheint. Ich konnte das »Rätsel« der richtigen Schreibweise nicht lösen. Wie auch immer: public delegate void del(int z);
Innerhalb der Klasse DelayTime wäre nun eine Instanz auf den Delegate zu erzeugen: public del dl = new del(delay);
Durch die Anweisung wird eine Objektvariable mit dem klanglosen Namen dl deklariert. Was die Variable aufnimmt, ist eine Instanz des Delegate del. Was beim Aufruf des Konstruktors del() übergeben wird, ist die Adresse der Methode delay(). Das – obgleich ich es bereits mehrfach erwähnt habe – ist wichtig, weshalb eine abermalige Wiederholung gerechtfertigt ist: Dem Konstruktor wird nicht die Methode selbst, sondern lediglich deren Adresse übergeben. Was auch immer ein Delegate ist, der eine Methode über den Namen aufruft, ein echter Delegate ist er jedenfalls nicht. Wenn Sie anderer Meinung sind: Die Einladung steht! Abgesehen davon, dass der Objektvariablen dl zu Gefallen die Sichtbarkeit der Methode delay() auf static festgelegt werden sollte, liegt »das Schlimmste« in Sachen Delegate hinter und die Signatur der Methode vor Ihnen:
135
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
static void delay(int time){}
Zum Thema Parameter gibt es nicht viel anzumerken. In der int-Variablen time wird die Anzahl an Millisekunden hinterlegt, um die verzögert werden soll. Im Methodenrumpf deklarieren wir darüber hinaus eine Variable t, die gleichfalls vom Typ int ist. Auch sie hat etwas mit Zeit zu tun. Zeit, die sie bekommt, und zwar vom Systemzeitgeber des Computers (Sie erinnern sich noch an das Wecker-Tool?). Genauer gesagt handelt es sich um die seit dem Start des Systems verstrichene Zeit in Millisekunden. Dazu ist zweierlei notwendig: eine Klasse und deren Eigenschaft. Die Klasse heißt Environment (aus dem Namensraum System), die Eigenschaft heißt TickCount. Das Verbindende zwischen beiden ist der Punktoperator: t = Environment.TickCount;
Nachfolgendes Codefragment stimmt Sie vielleicht für wenige Augenblicke nachdenklich, besonders hinsichtlich der Bedingung in der while-Schleife: while ((Environment.TickCount - t) < time) //Hier soll etwas passieren ... und passieren ... und passieren ...
Natürlich liefert die Subtraktion der verstrichenen Millisekunden einen Wert ungleich 0, wird doch bei jedem Schleifendurchlauf Environment.TickCount ausgeführt, und jedes Mal ist der Wert höher. Von dem abzuziehen ist der Inhalt von t. Der wiederum ändert sich nicht, da die Zuweisung außerhalb der Schleife erfolgt (und die Methode delay() nur einmal ausgeführt wird). Demnach ist nach jedem Durchlauf das Ergebnis der Subtraktion größer, was so lange weiter geht, bis die Bedingung nicht mehr erfüllt, d. h. die gewünschte Verzögerung gleich der Subtraktion ist. Dann ist das Spiel zu Ende. Selbstredend soll bis dahin in der while-Schleife eine Anweisung ausgeführt werden. Um hier abzukürzen: Statische Methoden und Eigenschaften zum Verwalten einer WindowsForms-Anwendung stellt die Klasse Application (Namensraum System.Windows.Forms) bereit. Für unser Anliegen ist das Member DoEvents() relevant. Es ist eine überaus emsige Methode, die alle Windows-Meldungen verarbeitet, die sich zum Zeitpunkt der Ausführungen in der Warteschlange befinden. Weitgehend unabhängig davon, welche Ereignisse sich in der Warteschlange befinden, geschieht das so lange, wie die Bedingung der while-Schleife nicht erfüllt ist. Und solange wird die Ausführung des Programms auch unterbrochen. Einsatz mit Bedacht Leider kann DoEvents() reguläre Abläufe nicht nur verzögern, sondern auch dafür sorgen, dass Code ungewollt erneut durchlaufen wird. Geben Sie also Obacht, wenn Sie eigene Versuche mit der Methode starten.
136
Entwicklung der Programmierlogik
Hier sehen Sie die vollständige Methode delay(): static void delay(int time) { int t; t = Environment.TickCount; while ((Environment.TickCount - t) < time) Application.DoEvents(); } Listing 5.20
Die Funktion »delay()« – manchmal lohnt es sich zu warten.
Und hier noch einmal die Klasse DelayTime: using using using using using
System; System.Collections.Generic; System.Linq; System.Text; System.Windows.Forms;
namespace Space_Impressions { public delegate void del(int z); public class DelayTime { public del dl = new del(delay); public DelayTime() { } static void delay(int time) { int t; t = Environment.TickCount; while ((Environment.TickCount - t) < time) Application.DoEvents(); } } } Listing 5.21
Verzögert beendet – die Klasse »DelayTime« macht’s möglich.
137
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
Die Einträge im Rumpf des EventHandlers Nachdem wir im letzten Kapitel die Klasse DelayTime erstellt haben, können wir beginnen, den Rumpf des Eventhandlers spaceWindowSchließenToolStripMenuItem_Click() zu füllen. Zuerst erzeugen wir eine Graphics-Zeichenoberfläche. Für Sie ist das zwischenzeitlich ein alter Hut: Graphics grf = this.CreateGraphics();
Anschließend stoppen wir das Timer-Objekt animateTimer, was auch nicht unbedingt der Gipfel der Programmierkunst ist: animateTimer.Stop();
Nächster Punkt: Wir zeichnen einen neuen Hintergrund (ohne Gestirne), d. h., wir übergeben das Objekt grf an das Creating-Klassenmember fading(): dr.fading(grf);
Am Anfang von Abschnitt 5.4 wurde ein Objekt delTime der Klasse DelayTime erzeugt. Die Klasse existiert nunmehr, und in ihr ist der Delegate dl vorhanden, über den die Klassenmethode delay() aufgerufen wird: delTime.dl(1500);
1500 Millisekunden später wird über die Text-Eigenschaft der Textbox-Klasse (die Sie in welchem Namensraum finden?) den Objekten textBox1 und textbox2 ein Leerstring übergeben: textBox1.Text = ""; textBox2.Text = "";
Jetzt folgt ein erneuter Aufruf des Klassenmembers delay() (über den Delegate dl): delTime.dl(1500);
Ab jetzt schlagen der Anwendung die letzten Sekunden: Nach 1500 Millisekunden wird nämlich das Fenster Space-Window geschlossen: this.Close();
Was bleibt, ist die Zerstörung der Graphics-Zeichenfläche grf: grf.Dispose();
Der vollständige Eventhandler spaceWindowSchließenToolStripMenuItem_Click (object sender, EventArgs e) sieht so aus : private void spaceWindowSchließenToolStripMenuItem_ Click(object sender, EventArgs e)
138
Entwicklung der Programmierlogik
{ Graphics grf = this.CreateGraphics(); animateTimer.Stop(); dr.fading(grf); delTime.dl(1500); Textbox1.Text = ""; Textbox2.Text = ""; delTime.dl(1500); this.Close(); grf.Dispose(); } Listing 5.22 Schön der Reihe nach – das Schließen der Anwendung im EventHandler »spaceWindowSchließenToolStripMenuItem_Click()«
Abbildung 5.11 zeigt den Zustand der Anwendung kurze Zeit bevor sich das Fenster schließt. Das Menü ist eingeblendet, die Sterne sind indirekt ausgeblendet, die Textboxen zurückgesetzt.
Abbildung 5.11 Der Zustand der Anwendung wenige Augenblicke vor dem Schließen des Fensters – kein Stern und keine Positionsangabe ist mehr zu sehen.
139
5.4
5
Mit Argusaugen – der nächtliche Sternenhimmel
5.5
Hätten Sie’s gewusst?
Zwei Fragen hätte ich an Sie. Betroffen sind zunächst Sterne der 2. Kategorie, also jene, die mit einer regelmäßigen Verzögerung von 600 Millisekunden eingeblendet werden. Exemplarisch für den x-Wert wird deren Position über textBox1.Text = Convert.ToString(x);
auch optisch bestimmt (Ausgabe der Textboxen xPos/yPos). Ist das wirklich so, oder könnten Sie mich der Schluderei vor dem Feierabend überführen? Ich gebe Ihnen die Antwort: Ja, allerdings nur in einem »minderschweren« Fall. Eine einzige Brücke schlage ich Ihnen: Erinnern Sie sich an die geometrische Hilfsfigur, mit der ein Stern, stilisiert als Kreis, gezeichnet wird? In der liegt die Lösung. Sterne der 1. und 2. Kategorie werden pseudozufällig im Space-Window positioniert. Während mit 30 die Anzahl der Sterne 2. Kategorie fest vorgegeben ist, liegt es an Ihnen, wie viele Sterne zeitverzögert (die Sache mit dem Auge!) am Firmament auftauchen. Ist auf beides Verlass? Könnten regelmäßig 30 Sterne der 1. Kategorie und (angenommene) 50 Sterne – der 2. Kategorie abgezählt werden? Die Antwort ist Nein. Doch warum nicht? Werfen Sie gedanklich mehrfach hintereinander einen Würfel. Was kann abgesehen davon, dass die gewünschte Zahl nicht oben liegt, passieren?
140
Es stimmt, dass Geld nicht glücklich macht. Allerdings meint man damit das Geld der anderen ... (Georg Bernhard Shaw)
6
Garantiert ungefährlich – Manipulationen am DAX
»Wir kommen zur Börse ...« – So oder ähnlich heißt es an fünf Abenden in der Woche, quer durch die Landschaft optischer Nachrichtenkanäle. Schon erscheint eine dezent lächelnde, adrett gekleidete Dame oder ein in schicken Zwirn gehüllter Herr und kurze Zeit später, geheimnisvoll wie der heilige Gral, die Kurve des DAX, gefolgt von dem Versuch einer plausibel klingenden Erklärung, warum diese an eben diesem Tage keinen anderen als den gezeigten Verlauf nehmen konnte. Und wir, die etwas ratlos schauenden, lauschenden Zuschauer, ahnen: Was sich auf dem Parkett der Frankfurter Börse (als Deutschlands wichtigster Wertpapierbörse) abspielt, ist eine Wissenschaft für sich, in die einzuführen mir schlicht die Kompetenz fehlt. Folgen können Sie mir trotzdem. Denn zweifelsohne finden wir einen Weg, um uns unentdeckt in das schicke, am Börsenplatz 4 gelegene Gebäude (siehe Abbildung 6.1) zu mogeln.
6.1
Im Schmelztiegel des großen Geldes
Heftig gestikulierende, hemdsärmelige Mannen, die sich, das Telefon fest in die Halsbeuge geklemmt, per Handzeichen verständigen und per Zuruf Geschäfte abschließen, wie es scheint, dem Herzinfarkt näher als der unverzichtbaren Kaffeetasse. Spätestens seit Tom Wolfes Welterfolg Fegefeuer der Eitelkeiten (nicht zu vergessen die gleichnamige Verfilmung mit Tom Hanks in der Rolle des geldschweren Brokers Sherman McCoy) stellt sich der Laie die Börse als einen Ort vor allem menschlicher Turbulenzen (und Abgründe) vor. Doch stimmt das Bild bereits seit einigen Jahren nicht mehr. Zwar sind die großen Wertpapierhandelsplätze, von New York über Frankfurt bis Tokio, nach wie vor nicht unbedingt Oasen kontemplativer Ruhe und Besinnlichkeit, gleichwohl hat der moderne Wertpapierhändler mittlerweile mindestens einen Monitor vor der Nase (siehe Abbildung 6.2), der das Sitzenbleiben »erzwingt«.
141
6
Garantiert ungefährlich – Manipulationen am DAX
Abbildung 6.1 Architektonisches Glanzstück aus der Wilhelminischen Epoche – die Frankfurter Wertpapierbörse
Abbildung 6.2 Eine runde Angelegenheit – die Parkettbörse im Computerzeitalter (Urheber »Dontworry«)
Oder glaubten Sie allen Ernstes, die Computerisierung der Zivilgesellschaft hätte ausgerechnet vor der Börse halt gemacht? Mitnichten. Darf ich also vorstellen ...
142
Im Schmelztiegel des großen Geldes
6.1.1
Xetra – das elektronische Hirn der Börse
Xetra (Exchange Elektronic Trading) ist ein von der Deutschen Börse AG betriebenes, elektronisches Handelssystem, dessen Zentralrechner in Frankfurt am Main stehen. Vor allem über das Internet können sich weltweit Clientrechner mit den Servern des Xetra-Systems verbinden. Das hört sich einfacher an, als es ist, dürfen doch nur an der Börse zugelassene Händler dort (direkt oder indirekt) auch Handel treiben. Der Rest bleibt im wahrsten Sinne des Wortes vor der Tür – mit oder ohne Xetra. Denn auch, wenn Sie sich von auswärts ins Xetra-System einloggen, benötigen Sie eine Zulassung. Ein neuer Personalausweis ist einfacher zu bekommen. Man staune: Mehr als dreiviertel des gesamten Aktienhandels an den deutschen Wertpapierbörsen werden zwischenzeitlich über die Handelsplattform Xetra abgewickelt, Tendenz steigend. Durch Xetra, bei dem Kauf- und Verkaufsaufträge elektronisch gegenübergestellt werden, wurde der klassische Parketthandel à la Tom Wolfe (die sogenannte Präsenzbörse) mehr und mehr verdrängt, was einer von einer ganzen Reihe von Kritikpunkten war, die dem System bei seiner Einführung vor zwölf Jahren entgegengehalten wurden. Traditionalisten finden sich demnach auch unter Börsianern. Warum auch nicht? Doch dürfte deren Stand zusehends schwerer werden. Vollkommen unrecht haben jene Stimmen nämlich nicht, die im Frankfurter Handelsparkett nur noch eine mondäne Fernsehkulisse sehen.
6.1.2
Am Puls der Wirtschaft – der DAX
Auf Basis der Xetra-Kurse und durch Xetra selbst erfolgt – und zwar sekündlich (!) – auch die Berechnung des DAX, zum letzten Mal um 17:30 Uhr am jeweiligen Handelstag. Dann ist Xetra-Handelsschluss, was nicht das Ende des Börsentages an sich bedeutet. Von 17:45 Uhr bis 20:00 Uhr geht es nämlich munter weiter – allerdings nur noch auf dem Parkett im großen Handelssaal der Frankfurter Börse. Auch in den zweieinhalb Stunden nach dem Xetra-Handelsschluss werden DAX-Stände berechnet, allerdings »nur« im Minutentakt. Darüber hinaus bekommt das kapitalstarke Kind am Abend einen anderen Namen: L-DAX, ausgesprochen Late-DAX. Um Abschnitt 6.2 zusammenfassend vorwegzunehmen: In diesem Kapitel wollen wir eine DAX-Kurve nachahmen, so wie sie im Verlauf eines x-beliebigen Börsentags entstehen (oder nicht entstehen) könnte. Das ist Grund für mich genug, das Wagnis einzugehen, Ihnen den Deutschen Aktienindex (DAX) zu erklären, den es seit dem 1. Juli 1988 gibt. Was der DAX widerspiegelt, ist die Kursentwicklung der 30 wichtigsten deutschen Aktien, hinter denen in der Tat namhafte Unternehmen stehen. Von »A« wie Addidas bis »V« wie Volkswagen werden gleichwohl nur börsennotierte Un-
143
6.1
6
Garantiert ungefährlich – Manipulationen am DAX
ternehmen zur Bildung des DAX zugelassen, deren Umsatzstärke die Deutsche Börse AG ebenso überzeugt wie die Anzahl handelbarer Aktien. Eine Überprüfung der Zusammensetzung des Index erfolgt jährlich, im Gegensatz zur Gewichtung der Firmen, die im Rhythmus von drei Monaten – ebenfalls durch die Deutsche Börse AG (sowie durch in- und ausländische Banken) – auf den Prüfstand kommt. Vom US-amerikanischen Dow-Jones-Index entscheidet sich der Deutsche Aktienindex durch die leistungsorientierte Art der Berechnung (Performance-Index), wobei sich das Gewicht einer Aktie nach dem Anteil an der gesamten Kapitalisierung der im Index enthaltenen Werte bemisst. Exemplarisch erläutert, dürfte die »A AG« mit 5 handelbaren Aktien und einer Marktkapitalisierung von 10 Euro für den DAX eine weniger große Rolle spielen als die »B AG« mit lediglich 2 handelbaren Aktien, jedoch einer Marktkapitalisierung von 100 Euro jährlich. Ist das so? Was die handelbaren Aktien anbelangt, so spricht die Börse jedenfalls so kryptisch wie bildhaft auch von Aktien im Streubesitz. Im Grunde ist der DAX eine kleine Familie, zu der neben dem bereits erwähnten L-DAX u. a. noch der M- und der SDAX (mit 50 Werten) sowie der TecDax, in den 30 technologie-orientierte Aktien eingehen, gehören. Die Bedeutung von vor allem »Vater DAX« erklärt sich aus dessen hoher Repräsentanz, werden durch die 30 Standardwerte doch rund 75 Prozent des gesamten Grundkapitals börsennotierter Inlandsaktiengesellschaften sowie etwa 85 Prozent der in deutschen Beteiligungspapieren getätigten Börsenumsätze vertreten. Ein starkes Stück!
6.2
Was angedacht ist
Ein Fenster genügt uns diesmal nicht. Am indirekt hin- und hergeschobenen Kapital der Börse liegt es ebenso wenig wie an unseren Fähigkeiten, eher daran, dass wir es nicht wollen. Schließlich ist es nicht uninteressant, zu sehen, wie Eingaben und Festlegungen in einer WindowsForm getätigt werden, während die Weiterverarbeitung, vor allem die Visualisierung der Daten im Sinne einer (auch »abenteuerlichen«) DAX-Kurve, dagegen in einer zweiten WindowsForm erfolgt. Womit wir bei der Frage nach den Anforderungen an das Programm sind. Wir müssen hier zwischen den Anforderungen an die Darstellung der 왘
DAX-Kurve sowie der
왘
Benutzeroberflächen
144
Was angedacht ist
unterscheiden. Kommen wir zunächst zum zweiten Punkt. Wie gesagt, existieren zwei WindowsForms. Die eine trägt den Namen Data, die andere heißt Chart. Über eine Registerkarte stellt Data zwei Ebenen (Panels) für Eingaben bereit: 왘
DAX-Punkte
왘
Optionen
Auf dem Reiter DAX-Punkte haben Sie von einschließlich 9:00 bis einschließlich 17:00 Uhr die Möglichkeit, zu jeder vollen Stunde, also in demnach neun Drehfeldern (NumericUpDown-Control) den DAX-Stand einzustellen, wobei der Index von 5100 bis 5180 Punkte, in 10-Punkte-Schritten, variiert werden kann, aber nicht variiert werden muss. Ist nämlich in keinem Drehfeld ein Wert gewählt worden, ist der Kurvenverlauf tendenziell indifferent, d. h., an jeder Stundenmarke sind 5110 Punkte abzulesen. Demnach erklimmt der DAX im gesamten Zeitraum des virtuellen Handels (wobei wir uns die Freiheit nehmen, bereits um 17:00 Uhr Feierabend zu machen) weder einen möglichen atemberaubenden Punktestand noch fällt er von dem zurück ins besorgniserregende Punktetief. Der DAX plätschert unmotiviert vor sich hin. Ein langweiliger Tag an der Börse. Rien ne va plus! Nicht Ihre Sache sind die zwischen den vollen Stunden erreichten DAX-Stände. Die bestimmt das Programm. Mit einer ganz wesentlichen Einschränkung: Wurden um 16:00 Uhr hypothetische 5100 Punkte berechnet und eine Stunde später 5120 Punkte, können die dazwischen liegenden Werte nur +/– 25 Punkte über und unter dem späteren Wert liegen. Auch deshalb vermag es der programmgenerierte Anteil der DAX-Kurve nicht, der von Ihnen vorgegebenen, groben Entwicklung des Akienindex entgegenzuwirken. Kurz gesagt: Es ist an Ihnen, der Deutschen Wirtschaft am Tage x eine Befindlichkeit zu geben. Zur Registerkarte Optionen: Einige Fernsehsender begnügen sich nicht damit, nach Handelsschluss die Kamera dorthin zu schwenken, wo der DAX auf einer riesigen Schautafel dargestellt ist, nach der Devise: »Seht her und staunt, hört vor allem aber den Reportern gut zu...« Über die Spanne der Handelszeit baut sich die Kurve doch erst auf. Genau das wird vereinzelt im Zeitraffer wiedergegeben. Auch wir machen das so. Segment für Segment entsteht die Kurve. Wie schnell das geschieht, bestimmen Sie in einer von drei ComboBox-Controls (Verzögerung der Chart-Segmente). Zur Wahl stehen 300, 400, 500, 600, 700 und 800 Millisekunden. Die zweite ComboBox (Breite der DAX-Kurve) können Sie, innerhalb der Werte 6, 7, 8 und 9, variabel halten, je nachdem, ob sich der DAX fett oder eher »filigran« präsentieren soll. Dasselbe gilt für die Farbe. Schwarz, Rot und Blau werden zwar zur Abwechslung in RadioButtons selektiert, doch geht es auch hier um
145
6.2
6
Garantiert ungefährlich – Manipulationen am DAX
nichts weiter als um den Geschmack – und den Spaß am Verändern optischer Programmausgaben. Zur dritten und letzten ComboBox: Okay, auch das ist eine Frage des Geschmacks, doch zählt Opacity für mich zu den interessanten Eigenschaften der Klasse Form. Es hat schon etwas, die Durchlässigkeit eines Formulars zu variieren. Genau das soll in der ComboBox Transparenz des DAX-Formulars geschehen: eine variable Wertbindung der Eigenschaft Opacity. Die möglichen Werte sind 60, 80 und 100 %. Unabhängig vom Grad der Formulardurchsichtigkeit besteht die Möglichkeit, über eine CheckBox (Hintergrundbild aktivieren) ein Hintergrundbild einoder auszublenden.
6.3
Entwicklung der Benutzeroberflächen
Auch im vierten Praxisbeispiel steht zu Beginn die Einrichtung eines neuen Projekts. Der Weg dorthin ist Ihnen bekannt. Geben Sie also in der Dialogmaske Neues Projekt im Editorfeld Name »DAX_SIM« ein. Etwas anderes als eine WindowsForms-Anwendung entwickeln wollen wir eh nicht. Klicken Sie auf die Schaltfläche Ok, und lehnen Sie sich anschließend für wenige Augenblicke im Stuhl zurück. Lauschen Sie dem (hoffentlich leisen) Knistern der Festplatte – und fertig sind die Vorerzeugnisse, die Sie im Projektmappen-Explorer einsehen können: angefangen bei der Projektmappe über das Stammverzeichnis DAX_SIM bis hin zur Klassendatei Program.cs in der die »Königsmethode« Main(), als Einstiegspunkt des .NET-Frameworks, für die Ausführbarkeit der Software sorgt. Benennen Sie die Datei Form1.cs in Data.cs um. Dazu ist nichts weiter erforderlich als mit der rechten Maustaste auf die Datei zu klicken, und im Kontextmenü die Option Umbenennen auszuwählen. Das auch alle Verweise umbenannt werden sollten, bestätigen Sie im Hinweisfenster durch einen Klick auf den Button Ja.
6.3.1
Das Fenster »Data«
Das Fenster Data sollte auch so benannt werden. Legen Sie deshalb die Text-Eigenschaft auf den gleichlautenden Wert fest. Weiter oben im EigenschaftenFenster entdecken Sie MaximizeBox und MinimizeBox. Setzen Sie beides bitte auf False, denn wir müssen das Formular Data weder maximieren noch minimieren. Allerdings werden wir es ein wenig skalieren, weswegen Sie unter dem Knoten Size Width mit 728 und Height mit 344 belegen. Dann ist es gut.
146
Entwicklung der Benutzeroberflächen
Langgestreckt – der Button Positionieren Sie, in minimalem Abstand zur Begrenzung des Fensters Data, ein Button-Control in die linke untere Formularecke. Die relevanten Einstellungen sind: 왘
Name: chartButton
왘
BackColor: Red
왘
Unter dem Knoten Font: 왘
Bold: True
왘
ForeColor: ControlLightLight
왘
Unter dem Knoten Size:
왘
왘
Width: 240
왘
Height: 28
Text: Zum DAX-Koordinatensystem
Nicht unbeliebt – Registerkarten Auch wenn es unter dem Aspekt der Oberflächenentwicklung kaum anders zu machen ist, das Schöne am TabControl ist, dass es auch im Entwurfmodus funktioniert. Über jedes Control kann das nicht gesagt werden. Zum weiteren Vorgehen: Ziehen Sie aus der Toolbox (Kategorie Alle Windows Forms) ein TabControl-Steuerelement auf das Formular Data. Belassen Sie den Fokus dort, wo er ist, denn ohne Fokus kann die Position von tabConrol1 nur im Eigenschaften-Fenster des Controls geändert werden – was wir zur Abwechslung nicht tun wollen. In dem Augenblick, zu dem sich der Mauscursor über dem Pfeilkreuz des TabControls befindet, nimmt der Cursor dessen Gestalt an. Drücken Sie bitte jetzt die linke Maustaste, und ziehen Sie tabControl1 so weit wie möglich in die linke obere Ecke des Formulars. Belassen Sie den Fokus noch einen Moment auf dem TabControl. Unschwer sind sechs winzige Quadrate zu erkennen, die das Control begrenzen. Benutzen Sie diese, um via Maus (halten Sie die linke Taste gedrückt) die 왘
Länge des Steuerelements an die Länge der WindowsForm Data anzupassen und
왘
die Breite des Steuerelements bis zur Oberkante des Buttons chartButton zu vergrößern.
147
6.3
6
Garantiert ungefährlich – Manipulationen am DAX
Benennung der Registerkarten Standardmäßig werden die beiden Registerkarten des TabControls tabPage1 bzw. tabPage2 genannt, wohinter nicht die Text-Eigenschaft der Klasse TabControl steckt. Zwar existiert bei TabControl eine Eigenschaft Text, gleichwohl nur als Teil der Infrastruktur, d. h. ohne Bedeutung für das Control. Die eigentliche Aufgabe der TabControl-Klasse ist die Verwaltung einer Gruppe von Registerkarten, wobei die wiederum von der TabPage-Klasse dargestellt werden. Was schlussendlich eine komfortable Brücke zwischen TabControl und TabPage schlägt, ist die TabPages-Eigenschaft der TabControl-Klasse. In der Praxis sieht das dann so aus: 왘
Klicken Sie im Eigenschaften-Fenster von tabControl1 auf den Button mit den drei Punkten neben dem Eintrag TabPages. Daraufhin öffnet sich der Tab-Pages-Auflistungs-editor.
왘
Links, unter Member, sind die beiden beteiligten Objekte (tabPage1 und tabPage2) aufgelistet, rechts befindet sich das zugehörige EigenschaftenFenster, in dem u. a. die Text-Eigenschaft der TabPage-Klassen mit den beiden Texten DAX-Punkte und Optionen belegt werden kann.
Noch eine Überschriftenebene tiefer geht es mit der Gestaltung der Registerkarten weiter. Die Registerkarte »DAX-Punkte«
Trotz im doppelten Wortsinne erschöpfender Erklärungen lässt Ihnen Ihr Buch auch jede Freiheit zu grafischer Gestaltung. Niemand zwingt Sie, neun NumericUpDown- und elf Label-Controls in einem TableLayoutPanel mit dem Ziel zu gruppieren, das Ergebnis in der unteren Hälfte eines GroupBox-Controls (Ihre Eingaben) anzuordnen. Tun Sie es dennoch, dann bitte in dieser Reihenfolge: 왘
GroupBox
왘
TableLayoutPanel
왘
Labels
왘
NumericUpDowns
Wie Steuerelemente positioniert und dimensioniert werden, wurde exemplarisch für das TabControl beschrieben. Und ansonsten ist Ihnen die Existenz der Textund ForeColor-Eigenschaft auch bei GroupBox- und Label-Controls längst bekannt.
148
Entwicklung der Benutzeroberflächen
Was bleibt, sind neun Drehfelder (NumericUpDown1 bis NumericUpDown9), bei denen drei Eigenschaften im Kontext des Geplanten (siehe Abschnitt 6.2) wichtig sind: Maximum, Minimum und Value. Hier die Belegung der Eigenschaften: 왘
Maximum: 5180
왘
Minimum: 5100
왘
Value: 5110
Abbildung 6.3 Entwurfsansicht der Registerkarte »DAX-Punkte« (beachten Sie, dass der Fokus auf dem Steuerelement »tabControl1« steht).
Die Registerkarte »Optionen«
Das Label- und das GroupBox-Control (Ihre Einstellungen) sind ebenfalls auf dem Panel Optionen vertreten, zusammen mit den bereits gelieferten Informationen bezüglich der Anpassung von Textfarbe, Text und Größe. Neu im Verbund der Oberfläche sind eine CheckBox, drei RadioButtons (rbSchwarz, rbRot, rbBlau) sowie drei ComboBoxen, die, wie auch immer sie angeordnet werden, natürlich nur im Falle der ComboBoxen mit Einträgen auszustatten sind. Drei Wege führen zum Ziel einer gefüllten ComboBox, von denen zwei im Zeichenfolgen-Editor münden (die Einstellwerte sind auf Seite 6 zu finden). Sie können den Zeichenfolgen-Editor wie folgt öffnen: 왘
im Eigenschaften-Fenster der fokussierten ComboBox durch einen Klick rechts neben dem Eintrag Items
왘
im Menü ComboBox-Aufgaben. Um es zu öffnen, klicken Sie auf den winzigen Pfeil über der fokussierten ComboBox.
149
6.3
6
Garantiert ungefährlich – Manipulationen am DAX
Abbildung 6.4
6.3.2
Entwurfsansicht der Registerkarte »Optionen«
Das Fenster »DAX«
Das primäre Charakteristikum der zu generierenden Klasse Charts ist deren »formidabler Charakter«, denn in was außer einem Clientrechteck sollte die DAXKurve gezeichnet werden? Eine WindowsForm existiert bereits; auf Codeebene ausgedrückt durch die Klasse Data, eine nicht unbedingt genügsame Erbin der mächtigen Klasse Form. Das Projekt DAX_SIM um eine WindowsForm zu ergänzen, ist auf Grundlage des Projektmappen-Explorers genauso schnell erledigt wie durch den Gebrauch des Hauptmenüs. Im Hauptmenü klicken Sie einfach auf Projekt und anschließend auf Windows Form hinzufügen. Was folgt, ist das Übliche: Sie tragen den Namen »Chart« im Editorfeld Name ein und klicken abschließend beherzt auf den Button Hinzufügen. Fertig. Nach neuerlichem Gegrummel der Festplatte öffnet sich die Datei Chart.cs im Entwurfsmodus – mit dem wir schnell fertig werden. Legen Sie im Projektmappen-Explorer die 왘
Text-Eigenschaft auf DAX, die
왘
Width-Eigenschaft auf 937 und Height auf 720
fest. Auch hier kann das Fenster zur Laufzeit so bleiben, wie es ist. Setzen Sie also die Minimize- und die Maximize-Eigenschaft auf False, genauso wie Sie es bei der Form Data – hoffentlich – getan haben. Als Hintergrundfarbe schlage ich ... nein, ich schlage nichts vor. Entscheiden Sie die Belegung der Eigenschaft BackColor. Gleichwohl existieren drei Einschränkungen, die Rot, Schwarz und Blau betreffen. Genannte Farben sind meinerseits bereits verplant. Sorry.
150
Entwicklung der Programmierlogik
Zweimal »greifen« Sie in die Toolbox der Entwicklungsumgebung. Es geht um ein Label- und ein Button-Control, die – zunächst – irgendwo auf der WindowsForm dax zu positionieren wären. Das Label dient zur Aufnahme des Datums, der Button zum Starten der DAX-Animation. Legen Sie die relevanten Eigenschaften für das Label-Control wie folgt fest: 왘
Name-Eigenschaft: dateLabel
왘
BackColor-Eigenschaft: Info, ForeColor-Eigenschaft: Desktop
왘
AutoSize: False;
왘
Width-Eigenschaft: 57, Height-Eigenschaft: 20
왘
X-Eigenschaft: 525, Y-Eigenschaft: 55
Beim Steuerelement vom Typ Button wäre lediglich die Text-Eigenschaft mit Start sowie die Position des Controls (unter dem Knoten Location) festzulegen: 왘
X-Eigenschaft: 816
왘
Y-Eigenschaft: 657
Da sie derart spartanisch mit Steuerelementen ausgestattet ist, lohnt ein Screenshot der WindowsForm Chart nicht.
6.4
Entwicklung der Programmierlogik
Zwei Windows mit zusammen drei Benutzeroberflächen (zwei Registerkarten im Fenster Data, zuzüglich dem Fenster Chart) sind nun mit Logik zu speisen. Data.cs haben Sie bereits im Entwurfsmodus geöffnet. Mit den diversen, von der Visual C# 2010 Express-Edition geleisteten Vorerzeugnissen sind Sie vertraut. Trotzdem gestalten wir den Anfang der Programmierarbeit vergleichsweise einfach. Auswahl per Default – die Methode »Defaults()« Unter anderem wurden drei ComboBoxen, ebenso viele RadioButtons sowie eine CheckBox entsprechend Abbildung 6.4 plaziert. Nach dem Programmstart soll auf dem Reiter Optionen 왘
in der ComboBox Verzögerung der Chart-Segmente (comboBox1) der Wert 500 Millisekunden und
왘
in der ComboBox Breite der Dax-Kurve (comboBox2) der Wert 6
bereits ausgewählt sein.
151
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Innerhalb der 3er-Gruppe RadioButtons (Farbe der DAX-Kurve) ist per Default das RadioButton-Objekt rbSchwarz gesetzt, desgleichen die einzige CheckBox, Hintergrundbild aktivieren (checkBox1). Im Falle des »Dreigestirns« aus ComboBoxen kann die Umsetzung ruhigen Gewissens über die SelectedIndex-Eigenschaft der ComboBox-Klasse erfolgen: comboBox1.SelectedIndex = 2; comboBox2.SelectedIndex = 2; comboBox3.SelectedIndex = 2;
Sowohl die Klasse RadioButton als auch CheckBox verfügen über die Eigenschaft Checked. Wenn diese mit dem booleschen Wert true belegt wird, führt sie sogleich zum Ziel – das so aussieht: rbSchwarz.Checked = true; checkBox1.Checked = true;
Nunmehr gibt es fünf Zeilen – fünf Zeilen, die Sie in den Rumpf der privaten, benutzerdefinierten Methode Defaults() (vom Rückgabetyp void, denn was sollte auch zurückgeben werden?) einfügen: private void Defaults() { comboBox1.SelectedIndex = 2; comboBox2.SelectedIndex = 2; comboBox3.SelectedIndex = 2; rbSchwarz.Checked = true; checkBox1.Checked = true; } Listing 6.1 Ein bisschen was zur Einstimmung – Eigenschaftenfestlegungen in der Methode »Defaults()«
Genauso wie die zur Unterstützung des Designentwurfs verwendete Methode InitializeComponent() (Data.Designer.cs) im Konstruktor der Klasse Data zur Ausführung kommt, soll auch Defaults() dort aufgerufen werden: public Data() { InitializeComponent(); Defaults(); } Listing 6.2
152
Aufruf der Methode »Defaults()« im Konstruktor der Klasse »Data«
Entwicklung der Programmierlogik
Starten Sie die Anwendung mit (F5) oder ohne Debug mit (Strg)+(F5). Wechseln Sie in der WindowsForm Data auf die Registerkarte Optionen. Abbildung 6.5 zeigt den aktuellen Stand der Entwicklung.
Abbildung 6.5
Darstellung der Registerkarte »Optionen« zur Laufzeit
Eine Hommage an die Umständlichkeit – die Methode »fileContent()« Legen Sie sich bitte irgendwo im Dateisystem eine Textdatei Data.txt an. Diese Datei fungiert als Datenbankersatz. Es ist nicht etwa so, dass unsere kleine DAXNachahmung ohne Datenhaltung nicht auskäme. Es geht darum, Sie mit elementaren Dateioperationen bekannt zu machen, sprich: mit dem Lesen und Schreiben. Entsprechende Klassen sind im Namensraum System.IO (das Kürzel IO steht natürlich für Input/Output) organisiert. Und wir sind organisiert genug, um im geöffneten Klassenfile Data.cs unter die letzte der using-Direktiven eine allerletzte zu setzen: using System.IO;.
Zunächst erschöpft sich unsere Absicht darin, die Data.txt mit dem zu füllen, was in den neun NumericUpDown-Controls (per Default oder vom Benutzer) ausgewählt wurde. Das Gewählte repräsentiert den Punktestand des DAX zu jeder vollen Stunde. Die Logik dahinter nimmt die private Methode fileContent() auf, deren Rückgabetyp ebenfalls void ist: private void fileContent(){}
153
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Deklarationen sind aller Programmierung Anfang, so auch hier: string writePoints, strWrite; writePoints, als die erste der beiden string-Variablen, erhält den Pfad zur beschreibenden Textdatei: writePoints = "C:/Data.txt";
Kommen wir nun zur zweiten als string deklarierten Variablen: strWrite. Ihr übergeben Sie einen String aus neun DAX-Punktzahlen, wobei die DAX-Stände durch Kommata zu trennen sind: strWrite = ( numericUpDown1.Text.ToString() numericUpDown2.Text.ToString() numericUpDown3.Text.ToString() numericUpDown4.Text.ToString() numericUpDown5.Text.ToString() numericUpDown6.Text.ToString() numericUpDown7.Text.ToString() numericUpDown8.Text.ToString() numericUpDown9.Text.ToString() );
+ + + + + + + +
"," "," "," "," "," "," "," ","
+ + + + + + + +
Das Monstrum an Initialisierung leistet sich gleich neunmal hintereinander die Methode ToString(), was neunmal hintereinander ebenso wenig zwingend wie falsch ist. Einerseits wird nämlich über die Text-Eigenschaft der NumericUpDownKlasse der ausgewählte Wert abgerufen, der günstigerweise bereits im erforderlichen string-Format vorliegt. Andererseits erbt jedes Objekt in C# die Methode ToString(), die eine Zeichenfolgedarstellung des Objekts zurückgibt. ToString() hält es noch auf einer anderen Ebene mit Objekten, gehört die Methode doch zur Klasse Object (Namensraum: System), deren Aufgabe eben die StringDarstellung von Objekten ist. Sie merken: Alles dreht sich um Objekte. Wenn es darum geht, Zeichen einer bestimmten Codierung (standardmäßig UTF8) in einen Stream zu schreiben, empfiehlt sich der Gebrauch der Klasse StreamWriter aus dem bereits bekannt gemachten Namensraum System.IO. Im Folgenden erzeugen wir ein StreamWriter-Objekt, wobei wir dem Konstruktor die Variable writePoints übergeben: StreamWriter streamWriter = new StreamWriter(writePoints);
Die gewählten neun DAX-Punkte sind in der string-Variablen strWrite gespeichert, mit der die Methode Write() (ein Member der Klasse StreamWriter) arbeitet: StreamWriter.Write(strWrite);
154
Entwicklung der Programmierlogik
Es klingt paradox, doch müssen das StreamWriter-Objekt bzw. der Stream mithilfe der Close()-Methode geschlossen werden, um sicherzustellen, dass alle Daten ordnungsgemäß im Stream ankommen: streamWriter.Close();
Ursächlich für die Regelung sind Exceptions, die erst in dem Augenblick greifen, wo der Stream durch die Close()-Methode geschlossen werden soll, was zum Beispiel dann schiefgeht, wenn die Kapazität der Festplatte nicht (mehr) ausreicht oder es Probleme mit der Codierung gibt (EncoderFallbackException). Querverbindungen Übrigens: Die Methoden Write() und Close() stehen im Klassenverbund der .NETFramework-Klassenbibliothek (BCL) nicht allein auf weiter Flur. Beide Methoden überschreiben Pendants der Klasse TextWriter, die ebenfalls im Namensraum System.IO organisiert ist und zu den abstrakten Klassen zählt. Eine Instanz von TextWriter ist also »nur schwer« zu erstellen. Sehen Sie es so: TextWriter fungiert für StreamWriter als Basisklasse – kein Einzelfall im »unendlichen Klassenmeer« des Frameworks. Bevor es zur nächsten Methode geht, sehen Sie hier noch einmal fileContent() in seiner ganzen zweifelhaften Pracht: private void fileContent() { string writePoints, strWrite writePoints = "C:/Data.txt"; strWrite = ( numericUpDown1.Text.ToString() numericUpDown2.Text.ToString() numericUpDown3.Text.ToString() numericUpDown4.Text.ToString() numericUpDown5.Text.ToString() numericUpDown6.Text.ToString() numericUpDown7.Text.ToString() numericUpDown8.Text.ToString() numericUpDown9.Text.ToString() );
+ + + + + + + +
"," "," "," "," "," "," "," ","
+ + + + + + + +
StreamWriter streamWriter = new StreamWriter(writePoints); streamWriter.Write(strWrite); streamWriter.Close(); } Listing 6.3
Ein Datenstrom wird erzeugt – die Methode »fileContent()«
Weiter geht’s ...
155
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Drei Methoden und ein Handler Wenn Sie es nicht bereits getan haben, doppelklicken Sie jetzt im Entwurfsmodus der Klasse Data auf die Schaltfläche Zum DAX-Koordinatensystem. Zweifelsohne sollte hinter dem Button nämlich ein Handler stehen. Dessen Rumpf ist einleitend mit dem Aufruf der Methode fileContent() zu füllen: fileContent();
Wichtig Die Erzeugung des Objekts Chart, hinter dem, bildhaft gesprochen, die gleichnamige Klasse steht, erfolgt anderes als bei der Klasse Data nicht in der Methode Main(), sondern im EventHandler der Schaltfläche Zum DAX-Koordinatensystem.
Instanziieren wir die Klasse Chart: Chart ch = new Chart( comboBox1.Text, comboBox2.Text, comboBox3.Text, rbSchwarz, rbRot, rbBlau, checkBox1 );
Beachten Sie, dass der Konstruktor der Klasse Chart bislang nicht existiert. Fehlermeldungen sollten unter diesem Aspekt ignoriert werden. Wie auch immer ist der Konstruktor reich an Parametern. Genau sind es derer sieben. Sieben Parameter, unter denen keiner ist, der nichts mit den Auswahlmöglichkeiten auf der Registerkarte Optionen zu tun hätte – Informationen, die der Klasse Chart nicht vorenthalten werden sollten. (Unterscheiden Sie die Parameter danach, ob sie Objekte oder Eigenschaften von Objekten sind.) Weiter unten, im Abschnitt »Stretching für den Konstruktor«, geht es genau an dem Punkt weiter. Ein Objekt vom Typ Form zu erzeugen (Charts ist Erbe der Klasse Form, somit ist das Chart-Objekt ch direkt vom Typ Chart und indirekt vom Typ Form), ist nicht gleichbedeutend mit dem Aufruf der WindowsForm Charts. Show(), ein Member der Klasse Form, schafft Abhilfe: ch.Show();
Hinweis Die Darstellung eines Steuerelements entspricht dem Festlegen der Visible-Eigenschaft des entsprechenden Controls auf den Wert true. Das heißt, nach Aufruf der Methode Show() gibt die Visible-Eigenschaft den Wert true zurück.
Hier noch einmal der vollständige Eventhandler button1_Click():
156
Entwicklung der Programmierlogik
private void button1_Click(object sender, EventArgs e) { fileContent(); Chart ch = new Chart( comboBox1.Text, comboBox2.Text, comboBox3.Text, rbSchwarz, rbRot, rbBlau, checkBox1 ); ch.Show(); } Listing 6.4 Nichts als Methodenaufrufe im EventHandler »button1_Click(object sender, EventArgs e)«
Damit wäre die Klasse Data fertig implementiert: using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Linq; System.Text; System.Windows.Forms; System.IO;
namespace DAX_SIM { public partial class Data : Form { public Data() { InitializeComponent(); Defaults(); }
private void Defaults() { comboBox1.SelectedIndex = 2; comboBox2.SelectedIndex = 2; comboBox3.SelectedIndex = 2; rbSchwarz.Checked = true; checkBox1.Checked = true;
157
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
} private void fileContent() { string writePoints, strWrite; writePoints = «C:/Data.txt”; strWrite = (numericUpDown1.Text + numericUpDown2.Text.ToString() + numericUpDown3.Text.ToString() + numericUpDown4.Text.ToString() + numericUpDown5.Text.ToString() + numericUpDown6.Text.ToString() + numericUpDown7.Text.ToString() + numericUpDown8.Text.ToString() + numericUpDown9.Text.ToString());
"," "," "," "," "," "," "," ","
+ + + + + + + +
StreamWriter streamWriter = new StreamWriter(writePoints); streamWriter.Write(strWrite); streamWriter.Close(); } private void button1_Click(object sender, EventArgs e) { fileContent(); Chart ch = new Chart( comboBox1.Text, comboBox2.Text, comboBox3.Text, rbSchwarz, rbRot, rbBlau, checkBox1 ); ch.Show(); } } } Listing 6.5
158
»Durchgangsbahnhof« für allerlei Daten – die Klasse »Data«
Entwicklung der Programmierlogik
6.4.1
Das große Zeichnen – die Klasse »Chart«
Am Anfang der Klasse Chart (die Sie im Quellmodus geöffnet haben) stehen ein paar Deklarationen und die Erzeugung eines alten Bekannten. Mit dem geht es los: private DateTime dt = DateTime.Now;
Mit der Eigenschaft Now der DateTime-Struktur wird ein DateTime-Objekt abgerufen. Worauf dt als Objekt festgelegt ist, ist die aktuelle Zeit auf dem lokalen Rechner. Später soll diese Zeitangabe im Zusammenspiel mit einer nicht zum ersten Mal verwendeten Methode für die Verzögerung des Programms an wohldefinierter Stelle sorgen. Abgesehen von einer per Programmierung möglichen Alternative, könnten Sie sich anderenfalls den DAX auch in der Zeitung ansehen (womit keine unterschwellige Kritik an Printmedien ausgedrückt ist – im Gegenteil). Ohne Kommentar – eine private Variable vom Typ int: private int dl;
Ferner eine private Variable vom Typ float: private float wd;
Stopp! Eine float-Variable gab es bislang noch nicht. Zumindest nicht in diesem Buch. Auch auf die Gefahr hin, auf Ihrerseits bereits erkundetes Terrain vorzustoßen: In float halten 32-Bit-Gleitkommawerte mit einem ungefähren Bereich von ±1.5 × 10-45 bis ±3,4 × 1038 und einer Genauigkeit von 7 Stellen Einzug. Einzug hält ferner eine Variable vom Typ string: private string op;
Zum Schluss: Deklaration dreier Variablen vom privaten Typ RadioButton: private RadioButton schwarz; private RadioButton rot; private RadioButton blau;
Alternativ können Sie sich auch zwei Zeilen sparen: private RadioButton schwarz, rot, blau;
Stretching für den Konstruktor Eingedenk der »beeindruckenden« sieben Parameter, mit denen im Eventhandler button1_Click() der Konstruktor Chart() der gleichlautenden Klasse aufgerufen wurde, dürfte die Signatur des Konstuktors für Sie nichts Schwieriges bergen:
159
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
public Chart(string d, string e, string o, RadioButton s, RadioButto n r, RadioButton b, CheckBox v){}
Die Parameter (wie alles, was im Weiteren im Rumpf des Konstruktors entwickelt werden soll) dienen zur 왘
erweiterten Darstellung – Hintergrundbild aktiviert ja/nein, Durchlässigkeit des Formulars der WindowsForm Chart (erzeugt durch die Klasse Chart als Erbin der Basisklasse Form – nicht vergessen!)
왘
Vorbereitung einer benutzerdefinierten Darstellung (Breite, Farbe, Verzögerung der Segmente) des DAX
왘
Anzeige des Datums
Der Rumpf des Konstruktors beginnt mit: InitializeComponent();
Beginnend mit dl, geht es weiter mit der Initialisierung der eingangs deklarierten Variablen dl, wd, schwarz, rot, blau. Der Inhalt der int-Variablen dl soll das in d Abgelegte sein – was zunächst nicht funktioniert. Denn d ist vom Typ string, was wiederum so sein muss, wird im Abschnitt »Drei Methoden und ein Handler« dem Konstruktor Chart() zuerst die unter Verzögerung der DAX-Segmente getroffene Wahl (ComboBox1.Text) übergeben. Folglich heißt es abermals: Konvertierung. Wieder die Klasse Convert, wieder der Methodenmember ToInt16(): this.dl = Convert.ToInt16(d);
this Bitte sehen Sie es mir nach, dass das Chart-Klassenmember dl wie auch die folgenden Member über das C#-Schlüsselwort this gesondert gekennzeichnet sind. Ich leugne es ja nicht: Die Gefahr, dass Variablen durch ähnlich lautende ausgeblendet werden, besteht nicht. Der Fall läge anders, würde beispielsweise dem Klassenmember dl der Inhalt einer gleichlautenden Variablen zugewiesen. Zum Beispiel so: this.dl = dl;
Machen Sie es besser ... Von der Konvertierung in eine Gleitkommazahl einfacher Genauigkeit abgesehen, passiert in der folgenden Zeile Ähnliches: this.wd = Convert.ToSingle(e);
160
Entwicklung der Programmierlogik
Als Nächstes wird dem Konstruktor das in der ComboBox Transparenz des DAXFormulars (ComoBox3) Ausgewählte übergeben, mit dem das Member op zu initialisieren ist. Da beide Variablen vom Typ string sind, erübrigt sich eine Konvertierung: this.op = o;
Im Member op ist die ausgewählte Durchlässigkeit des Formulars Chart gespeichert. Im Konstruktor der namensgleichen formularerzeugenden Klasse arbeiten wir derzeit. Über den Aufruf des Konstruktors wiederum erfolgt die Erzeugung des Chart-Objekts, und weil das alles so ist, können wir gleich hier, im Rumpf des Konstruktors, die Frage der Durchlässigkeit der WindowsForm (die Eigenschaft Opacity) klären: if (op == "60 %") { this.Opacity = 0.60; } else if (op == "80 %") { this.Opacity = 0.80; } else if (op == "100 %") { this.Opacity = 1.0; }
Damit wäre der dritte Parameter in der Liste des Konstruktors auch abgehandelt. Die nächsten drei Parameter sind trivial und ebenso der darauf folgende. Zunächst jedoch werden die Variablen s, r und b (vom Typ RadioButton) den Variablen schwarz, rot, blau (ebenfalls vom Typ RadioButton) zugewiesen: this.schwarz = s; this.rot = r; this.blau = b;
Die Darstellung des Datums auf der Oberfläche Chart ist unsere nächste Aufgabe. dt, als eingangs vereinbarte DateTime-Struktur, kann dem Label-Objekt dateLabel weder im originären Format noch in Gänze zugewiesen werden. Das heißt
konkret: Wir müssen erneut das Convert-Member ToString() einsetzen, zuzüglich der Methode Substring(), die das Ergebnis bis auf den Datumsanteil »zusammenstreicht«:
161
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
this.dateLabel.Text = (Convert.ToString(dt)).Substring(0,10);
Falls Ihnen bislang nur Bekanntes begegnet ist (was ja durchaus sein kann), geht es nun an hoffentlich unbekanntem Ort weiter. Ressourcen
Bitte tun Sie Folgendes: 왘
Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Stammverzeichnis DAX_SIM. Wählen Sie im Kontextmenü Hinzufügen. Weiter geht es mit Neues Element.
왘
Daraufhin öffnet sich die Dialogmaske Neues Element hinzufügen DAX_ SIM. Unter Vorlagen klicken Sie Ressourcendatei an. Im Editorfeld Name kann auch etwas anderes stehen als das bereits eingetragene Resources.resx (siehe Abbildung 6.6).
왘
Was auch immer Sie in Sachen Name tun, vergessen Sie nicht, abschließend auf den Button Hinzufügen zu klicken. Sonst war die Mühe, die keine war, vergebens.
Abbildung 6.6
162
Hinzufügen einer Ressourcendatei im Fenster »Neues Element hinzufügen«
Entwicklung der Programmierlogik
What’s new? Auf Klassenebene gibt der Projektmappen-Explorer, Registerkarte Klassen, die schnellste Antwort: Der Neuzugang ist direkt zu erkennen. Im Namensraum DAX_SIM.Properties wurde nämlich eine Klasse Resources erstellt, die – vereinfachend gesagt – die programmiersprachliche Entsprechung dessen ist, was sich im Weiteren in der Ressourcendatei Resources.resx abspielt. Von der Existenz der Datei konnten Sie sich bereits auf der Registerkarte Projektmappe des Projektmappen-Explorers im Ordner Properties überzeugen. Ferner enthält er die Klassendatei Resources.Designer.cs mit der erwähnten Klasse Resources (dient zur Aufteilung einer Klasse auf mehrere Klassendateien). Zudem wurde im Stammverzeichnis DAX_SIM der Ordner Resources angelegt. Füllen wir den mit der Kopie eines Bildes: 왘
Doppelklicken Sie auf die Datei Resources.resx, die sich daraufhin in – im Wortsinne – Form einer Dialogmaske öffnet.
왘
Wählen Sie unter Ressource hinzufügen den Eintrag Vorhandene Datei hinzufügen.
왘
Im Fenster Vorhandene Datei zu Ressourcen hinzufügen »hangeln« Sie sich zum Ort einer Bilddatei durch, die Sie für geeignet halten, zum Hintergrund des Formulars Chart zu werden. Bestätigen Sie Ihre Auswahl durch einen Klick auf den Button Öffnen.
Miniaturisiert ist in Resources.resx das zu sehen, was Sie als Bild ausgewählt haben (meine Auswahl sehen Sie in Abbildung 6.7).
Abbildung 6.7
Die Ressource »Boerse_01_KMJ2« in der Ressourcendatei »Resources.resx«
Damit wären wir mit verhaltenem Getöse auf die Bühne der Programmierung, genauer im Rumpf des Konstruktors Chart(), zurückgekehrt, in dem die folgende Zeile alles außer selbsterklärend ist. Am wenigsten erklärt sich das von selbst, was rechts vom Gleichheitszeichen steht:
163
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
this.BackgroundImage = global::DAX_SIM.Properties.Resources.Boerse_01_KMJ2;
Womit wir noch kein Wort über den doppelten Doppelpunkt (::) verloren hätten, der einen besonders einprägsamen Namen trägt: Namespace-Aliasqualifizierer. Als wäre das nicht schon schlimm genug, ist der Operator global (global), wodurch unser Namespace-Aliasqualifizierer berechtigt ist, seine Suche nach Bezeichnern auf globale statt auf Aliasnamensräume zu beschränken. In globalen Namensräumen finden Typen eine Heimat, die keinem speziellen Namensraum zugeordnet sind. Bei DAX_SIM.Properties handelt es sich um einen solch globalen Namensraum. In dem existiert die Klasse Resources, in der wiederum eine spezielle Methode (Accessormethode) enthalten ist, die letztendlich die Ressource Boerse_01_KMJ2 zurückgibt, hinter der sich die Kopie des gewählten Bildes verbirgt. Die Länge und die Breite des Bildes werden nicht selbstverständlich mit den Maßen des Fensters Chart übereinstimmen. Im ungünstigsten Fall ist das Bild zu klein. Dann müssen Sie entweder einen Bildbearbeiter bemühen oder die BackgroundImageLayout-Eigenschaft der Klasse Form entsprechend belegen. Vom Standpunkt der Eigenschaft betrachtet, kann nur das festgelegt (oder abgerufen werden), was sich in der ImageLayout-Enumeration an Membern findet. Es sind fünf, von denen wir nur eines brauchen: Stretch. this.BackgroundImageLayout = ImageLayout.Stretch;
Sofern Sie gegen ein Hintergrundbild als Dauereinrichtung nichts einzuwenden haben, kann die Arbeit am Konstruktor der Klasse Chart beendet werden. Was schade um den Parameter v wäre, in den beim Aufruf des Konstruktors das CheckBox-Objekt checkBox1 (Hintergrundbild aktivieren) eingeht. Ob checkBox1 gesetzt ist oder nicht, lässt sich erwähntermaßen über die Checked-Eigenschaft der CheckBox-Klasse ermitteln – was sich wunderbar als Bedingung einer if-Anweisung eignet. Ist die Bedingung erfüllt, d. h., existiert ein Häkchen in der Box, gibt es ein »gestretchtes« (gedehntes), als Ressource eingerichtetes Hintergrundbild. Anderenfalls bleibt alles beim Alten: if (v.Checked) { this.BackgroundImage = global::DAX_SIM.Properties.Resources.Boerse_01_KMJ2; this.BackgroundImageLayout = ImageLayout.Stretch; }
164
Entwicklung der Programmierlogik
Der Konstruktor Chart() in Gänze: public Chart(string d, string e, string o, RadioButton s, RadioButton r, RadioButton b, CheckBox v) { InitializeComponent(); this.dl = Convert.ToInt16(d); this.wd = Convert.ToSingle(e); this.op = o; if (op == "60 %") { this.Opacity = 0.60; } else if (op == "80 %") { this.Opacity = 0.80; } else if (op == "100 %") { this.Opacity = 1.0; } this.schwarz = s; this.rot = r; this.blau = b; this.dateLabel.Text = (Convert.ToString(dt)).Substring(0,10); if (v.Checked) { this.BackgroundImage = global::DAX_SIM.Properties.Resources.Boerse_01_KMJ2; this.BackgroundImageLayout = ImageLayout.Stretch; } } Listing 6.6
Logisch – der Konstruktor »Chart()«
Die Koordinaten des Kapitals Eine DAX-Kurve ohne Bezugssystem ist weder besonders gut zu lesen noch zu interpretieren.
165
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Verwechseln ist menschlich Ordinate und Abszisse beim Bezugssystem! Wer hat die beiden Begriffe nicht schon einmal verwechselt? Die meisten Leute sprechen lieber von der y- und x-Achse. Weil aber die y-Achse im Regelfall nach oben zeigt, kann man »davon ausgehen«, dass auch die Ordinate nach oben zeigt. Zweimal »O«, und fertig ist die schmale Eselsbrücke. Und die ist sogar tragfähig.
Werden unregelmäßig schwankende Werte anderer Herkunft grafisch aufgetragen und verbunden (beispielsweise Temperatur- oder Druckwerte), verhält es sich nicht anders, wobei es keine Rolle spielt, ob Werte auch negativ werden können, was beim DAX nicht der Fall ist. Der kann – wie sich die Börsianer zuweilen ausdrücken – zwar negativ reagieren, was aber nicht gleichbedeutend mit einem negativen DAX-Stand ist. –5143 Punkte um 16:35 Uhr? Beim besten Willen weiß ich nicht, wie es dazu kommen konnte. Sie? Zur Programmierung: Wir werden jetzt unter Verwendung der bekannten Klasse Graphics eine Zeichenoberfläche für das Bezugssystem des DAX bereitstellen, wobei wir, im Vergleich zu früheren Abschnitten, anders vorgehen. CreateGraphics(), als die objekterzeugende Methode der Klasse Control, bleibt nämlich zugunsten des Eventhandlers OnPaint() außen vor. Ein Blick auf die Signatur: protected override void OnPaint(PaintEventArgs paintEvent){}
Die Methode gibt nichts zurück (void), was bei Ereignisbehandlern keine Seltenheit ist. PaintEvent, vom Typ PaintEventArgs, wird als einziger Parameter übergeben. Die Aufgabe der Klasse PaintEventArgs (aus dem Namensraum System.Windows.Forms) ist das Bereitstellen von Daten für das Paint-Ereignis. Ausgelöst wird Paint durch OnPaint() selbst, womit die Methode weniger ein Ereignisbehandler als ein Ereignisauslöser ist – was man so sehen kann, aber nicht so sehen muss. Ihren »ständigen Sitz« hat die OnPaint()-Methode in der Basisklasse Control, durch die optische Steuerelemente definiert werden (also solche Steuerelemente, die auf einer Oberfläche auch zu sehen sind). Jedes WindowsForm-Steuerelement muss das von Control geerbte Paint-Ereignis überschreiben, ergo auch die Methode OnPaint(), die das Paint-Ereignis sendet (so viel zu der Frage, warum OnPaint() ein override vorangestellt ist). Da die Darstellung des abgeleiteten Controls unbekannt ist, enthält OnPaint() keinerlei Zeichenlogik. Gewissermaßen ist das unser Glück, dreht es sich im spe-
166
Entwicklung der Programmierlogik
ziellen Fall doch mitnichten um ein darzustellendes Control. Wozu also der Aufwand? Es geht um die Klasse PaintEventArgs. Sie hat zwei Eigenschaften, und eine von ihnen ruft die zum Zeichnen verwendete Grafik ab, wobei ein Graphics-Objekt den Eigenschaftswert darstellt: Graphics g = paintEvent.Graphics;
Obige Zeile ist die erste im Rumpf der Routine OnPaint(). Und es wird nicht die letzte sein. Als Nächstes empfiehlt sich ein Zeichenstift. Ein Objekt der Klasse Pen hilft uns auf die Sprünge. Dem Konstruktor wird über die Blue-Eigenschaft der Struktur Color gleich die passende Farbe übergeben: Pen pen = new Pen(Color.Blue);
Jetzt geht es ans Zeichnen der blauen Umrandung des Koordinatensystems: g.DrawRectangle(pen, 100, 100, 820, 500);
An DrawRectangle(), ein Methodenmember der Graphics-Klasse, wurden der Stift pen, ein Koordinatenpaar (100, 100) sowie die Maße für Breite und Höhe übergeben. Nächste Bereitstellung: ein Pinsel. Auch der Konstruktor der Klasse SolidBrush empfängt eine Systemfarbe. Diesmal ist es die »Farbe« der Reinheit: SolidBrush brush = new SolidBrush(Color.White);
Der Pinsel brush füllt das Rechteck aus: g.FillRectangle(brush, 101, 101, 819, 499);
Eine kleine Ablenkung gefällig? Dann erzeugen Sie bitte zunächst einen weiteren Pinsel (brush2). Den tauchen wir in rote Farbe: SolidBrush brush2 = new SolidBrush(Color.Red);
Des Weiteren ist eine Überschrift für das Koordinatensystem angezeigt. Ja, natürlich, auf ein Label-Control mehr wäre es nicht angekommen. Doch geht es auch anders: FontStyle style = FontStyle.Bold;
Der Variablen style vom Typ FontStyle (die Enumeration finden Sie im Namensraum: System.Dawing) wurde das FontStyle-Enumerationsmember Bold zugewiesen. Nicht mehr und nicht weniger. Was folgt, ist die Instanziierung der
167
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Klasse Font mit der Schriftartenfamilie, Schrifthöhe und eben jener mit FontStyle.Bold initialisierten Variablen style: Font arial = new Font(new FontFamily("Arial"), 30, style);
Beachten Sie, wie der Konstruktor Font() zu seiner Schriftfamilie kommt. Die wird über den Konstruktor einer anderen Klasse – FontFamily – definiert. Schwierig ist das alles nicht, gleichwohl ein wenig aufwendig. Doch weiter im Text! Ein String – DAX – soll gezeichnet werden. Wie geht das? Natürlich: mit Graphics! Und immer wieder verwenden wir die Klasse Graphics mit ihrem unerschöpflichen Methodenreservoir. Die nächste Routine heißt DrawString(). Sie können sich gewiss denken, was der Routine zu übergeben ist: die zu zeichnende Zeichenfolge, ein Font- und ein Brush-Objekt sowie ein Koordinatenpaar: g.DrawString("DAX", arial, brush2, 360, 30);
Die Ordinate mit einer Erklärung zu beschriften, schenken wir uns. Unter der Abszisse allerdings sollte Time stehen. Abgesehen von den geänderten Farb- und Größenwerten, gleicht das Vorgehen ziemlich genau dem, was bei der Zeichenfolge »DAX« getan wurde. Demnach schreiben Sie jetzt: SolidBrush brush3 = new SolidBrush(Color.Black); FontStyle style2 = FontStyle.Bold; Font arial2 = new Font(new FontFamily("Arial"), 16, style2); g.DrawString("Time", arial2, brush3, 430, 650);
Wesentlich ändert sich das Prinzip auch dann nicht, wenn entlang der x-Achse (Abszisse) Uhrzeiten aufgetragen werden. Legen Sie also einen Schriftstil via FontStyle-Enumeration fest, wobei diesmal allerdings das Enumerationsmember Regular zum Zuge kommt, soll die Schrift doch eine normale, d. h. weder fett noch unter- oder durchgestrichen sein. Anschließend erzeugen Sie ein Font-Objekt über den Konstruktor und seine bekannten Parameter – dasselbe Spiel, mit (fast) gleichem Ausgang: FontStyle style3 = FontStyle.Regular; Font arial3 = new Font(new FontFamily("Arial"), 12, style3);
Trotzdem fehlt etwas: der Pinsel. Und doch fehlt er nicht, weil der Pinsel, den wir brauchen, weiter oben bereits erzeugt wurde. Neunmal muss brush3 eine schwarz kolorierte Textzeichenfolge (für jede Uhrzeit eine) auf Grundlage des Font-Objekts arial3 sowie zweier Koordinaten (X/Y) zeichnen: g.DrawString( "9h00", arial3, brush3, 90, 620); g.DrawString("10h00", arial3, brush3, 175, 620); g.DrawString("11h00", arial3, brush3, 260, 620);
168
Entwicklung der Programmierlogik
g.DrawString("12h00", g.DrawString("13h00", g.DrawString("14h00", g.DrawString("15h00", g.DrawString("16h00", g.DrawString("17h00",
arial3, arial3, arial3, arial3, arial3, arial3,
brush3, brush3, brush3, brush3, brush3, brush3,
345, 430, 515, 600, 685, 770,
620); 620); 620); 620); 620); 620);
Verlaufen sollen Sie sich im Zahlenwirrwarr des Neunzeilers natürlich nicht, deshalb folgen hier zwei Orientierungshilfen (die übergangen werden können, wenn Ihnen das Zustandekommen der Zahlenpaare klar ist): 왘
Die y-Position ist mit jeweils 620 pt für alle Uhrzeiten identisch. Dem Wert liegt keine Berechnung zugrunde, wohl aber eine Orientierung an der Position und Dimension des eingangs gezeichneten Rechtecks. Wenige Punkte unterhalb des Rechtecks sollen die Zeitwerte platziert werden.
왘
Auf einer imaginären x-Achse, in »Reih‘ und Glied« angeordnet, beträgt der Abstand zwischen den Uhrzeiten 85 pt.
Das Vorgehen beim Anordnen der DAX-Punkte entlang der y-Achse des blau umrandeten Rechtecks ist identisch. Während hier jedoch die x-Position unveränderlich mit jeweils 45 pt vorgegeben ist, beträgt der vertikale Abstand zwischen den Textzeichenfolgen regelmäßig 50 pt: g.DrawString("5.100", g.DrawString("5.110", g.DrawString("5.120", g.DrawString("5.130", g.DrawString("5.140", g.DrawString("5.150", g.DrawString("5.160", g.DrawString("5.170", g.DrawString("5.180",
arial3, arial3, arial3, arial3, arial3, arial3, arial3, arial3, arial3,
brush3, brush3, brush3, brush3, brush3, brush3, brush3, brush3, brush3,
45, 45, 45, 45, 45, 45, 45, 45, 45,
540); 490); 440); 390); 340); 290); 240); 190); 140);
Damit sind Ordinate und Abszisse markiert: die y-Achse mit neun DAX-Ständen (5.100 bis 5.180 Punkte), die x-Achse im Sinne einer Zeitleiste von 9:00 bis 17:00 Uhr. Verlängert man die horizontalen und vertikalen Markierungen, entsteht ein Gitter. Zunächst neun horizontale Linien: g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen,
100, 100, 100, 100, 100, 100, 100,
150, 200, 250, 300, 350, 400, 450,
920, 920, 920, 920, 920, 920, 920,
150); 200); 250); 300); 350); 400); 450);
169
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
g.DrawLine(pen, 100, 500, 920, 500); g.DrawLine(pen, 100, 550, 920, 550);
Dann geht es in die Vertikale: g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen,
190, 275, 360, 445, 530, 615, 700, 785, 870,
600, 600, 600, 600, 600, 600, 600, 600, 600,
190, 275, 360, 445, 530, 615, 700, 785, 870,
100); 100); 100); 100); 100); 100); 100); 100); 100);
Zum Schluss rufen Sie über das base-Schlüsselwort die überschriebene Methode aus der Basisklasse auf: base.OnPaint(paintEvent);
Bevor die Routine OnPaint() aus allen Nähten platzt, werfen Sie einen abschließenden Blick darauf: protected override void OnPaint(PaintEventArgs paintEvent) Graphics g = paintEvent.Graphics; Pen pen = new Pen(Color.Blue); g.DrawRectangle(pen, 100, 100, 820, 500); SolidBrush brush = new SolidBrush(Color.White); g.FillRectangle(brush, 101, 101, 819, 499); SolidBrush brush2 = new SolidBrush(Color.Red); FontStyle style = FontStyle.Bold; Font arial = new Font(new FontFamily("Arial"), 30, style); g.DrawString("DAX", arial, brush2, 360, 30); SolidBrush brush3 = new SolidBrush(Color.Black); FontStyle style2 = FontStyle.Bold; Font arial2 = new Font(new FontFamily("Arial"), 16, style2); g.DrawString("Time", arial2, brush3, 430, 650); FontStyle style3 = FontStyle.Regular; Font arial3 = new Font(new FontFamily("Arial"), 12, style3); g.DrawString( "9h00", arial3, brush3, 90, 620; g.DrawString("10h00", arial3, brush3, 175, 620);
170
{
Entwicklung der Programmierlogik
g.DrawString("11h00", g.DrawString("12h00", g.DrawString("13h00", g.DrawString("14h00", g.DrawString("15h00", g.DrawString("16h00", g.DrawString("17h00",
arial3, arial3, arial3, arial3, arial3, arial3, arial3,
brush3, brush3, brush3, brush3, brush3, brush3, brush3,
260, 345, 430, 515, 600, 685, 770,
g.DrawString("5.100", g.DrawString("5.110", g.DrawString("5.120", g.DrawString("5.130", g.DrawString("5.140", g.DrawString("5.150", g.DrawString("5.160", g.DrawString("5.170", g.DrawString("5.180",
arial3, arial3, arial3, arial3, arial3, arial3, arial3, arial3, arial3,
brush3, brush3, brush3, brush3, brush3, brush3, brush3, brush3, brush3,
45, 45, 45, 45, 45, 45, 45, 45, 45,
g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen,
100, 100, 100, 100, 100, 100, 100, 100, 100,
150, 200, 250, 300, 350, 400, 450, 500, 550,
920, 920, 920, 920, 920, 920, 920, 920, 920,
150); 200); 250); 300); 350); 400); 450); 500); 550);
g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen, g.DrawLine(pen,
190, 275, 360, 445, 530, 615, 700, 785, 870,
600, 600, 600, 600, 600, 600, 600, 600, 600,
190, 275, 360, 445, 530, 615, 700, 785, 870,
100); 100); 100); 100); 100); 100); 100); 100); 100);
620); 620); 620); 620); 620); 620); 620);
540); 490); 440); 390); 340); 290); 240); 190); 140);
base.OnPaint(paintEvent); } Listing 6.7 Unverzichtbar für die Darstellung des Deutschen Aktienindex (DAX) – das Bezugssystem
Starten Sie abermals die Anwendung, wahlweise mit ((F5)) oder ohne Debug ((Strg)+(F5)). Nachdem Sie im WindowsForm Data auf die Schaltfläche DAX-Ko-
171
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
ordinatensystem anzeigen geklickt haben, sollte – abgesehen von einem anderen Datum – der Anblick aus Abbildung 6.8 zu sehen sein.
Abbildung 6.8
Die Laufzeitansicht mit aktiviertem Hintergrundbild
Ein alter Bekannter – die Methode »Delay()« Die Entscheidung, den Verlauf des DAX (mit variabler Geschwindigkeit) sukzessive aufzubauen, bedingt es, einzelne Programmabschnitte verzögert auszuführen. Auch im letzten Kapitel, in dem es um Sterne ging, war das Teil der Aufgabe, die schließlich mit der Delay()-Methode gelöst wurde. Zur Erinnerung: private void delay(int time) { int t = Environment.TickCount; while ((Environment.TickCount - t) < time) Application.DoEvents(); } Listing 6.8
172
Wenige Zeilen mit einiger Wirkung – die Methode »delay()«
Entwicklung der Programmierlogik
Falls Sie das letzte Kapitel nicht gelesen haben, ist hier die Methode delay() im »Schleudergang«: Die Anzahl der Millisekunden, um die verzögert werden soll, ist in der int-Variablen time hinterlegt. Im Methodenrumpf deklariert ist eine Variable t, die gleichfalls vom Typ int ist. Die Initialisierung von t erfolgt mit der Zeit, die seit dem Start des Systems verstrichen ist – und fortschreitet. Die Bedingung der while-Schleife: Bei jedem Schleifendurchlauf wird Environment.TickCount ausgeführt, mit einem sich stetig erhöhenden Ergebnis, von dem
der Inhalt von t abzuziehen ist. t, außerhalb der Schleife initialisiert, bleibt konstant (auch weil die Methode nur einmal zur Ausführung kommt). Infolgedessen erhöht sich nach jedem Durchlauf das Ergebnis der Subtraktion. Ist die Bedingung nicht mehr erfüllt, sprich: ist die gewünschte Verzögerung gleich der Subtraktion, ist Feierabend für die Schleife while. Bis dahin schlägt die Stunde der Methode DoEvents(), einem Member der Klasse Application (die Sie, nebenbei bemerkt, auch in der Main()-Methode finden). DoEvents() verarbeitet alle Windows-Meldungen, die sich zum Zeitpunkt der Ausführung in der Warteschlange befinden. Welche das sind, ist für uns irrelevant. Entscheidend ist, dass DoEvents() so lange vor sich hin werkelt, wie die Bedingung der while-Schleife nicht erfüllt ist. Solange DoEvents() aber arbeitet, ist die Ausführung des Programms unterbrochen. Kommata unerwünscht – die Methode »readFileContent()« Abbildung 6.9 zeigt einen möglichen Inhalt der Datei Data.txt, bei dem wir eines nicht gebrauchen können: Kommata.
Abbildung 6.9
Möglicher Inhalt der Datei »Data.txt«
Aus dem Textfile neun Zahlen zu extrahieren, ist die primäre Aufgabe der privaten, in Charts.cs stehenden Methode readFileContent(),deren Signatur wie folgt aussieht: string[] readFileContent(){}
Was die Routine liefert, ist ein acht-elementiges Array vom Typ string, das durch den Ausdruck
173
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
string[] posVal = new string[8];
erzeugt wird. Weiter geht es mit dem Datentyp string: Hier wird der exemplarische Pfad der Datei Data.txt in der Variablen getDataPath gespeichert: string getDataPath = "C:/Data.txt";
Und eine letzte string-Variable: string readF;
Auch wenn es nur eine Zeile gibt – zum Lesen von Zeilen mit Informationen aus einer Textdatei eignet sich die StreamReader-Klasse (aus dem Namensraum System.IO) hervorragend. Der Konstruktor instanziiert eine neue Klasse für den in getDataPath gespeicherten Dateinamen bzw. Pfad: StreamReader stream = new StreamReader(getDataPath);
Um die Datei Data.txt in einem Vorgang bis zum Ende lesen zu können, brauchen wir das StreamReader-Member ReadToEnd(), dessen Rückgabe der string-Variablen readF zuzuweisen ist: readF = stream.ReadToEnd();
Am Ende steht das Schließen des Objekts stream (Close()) sowie des Streams selbst. Alle dem StreamReader zugrunde liegenden Ressourcen werden freigegeben: stream.Close();
Erst jetzt kommen wir an den Punkt, die neun Zahlen von den dazwischen liegenden Kommata zu trennen und das Ergebnis (neun Teilzeichenfolgen) im stringArray posVal abzulegen. Wenn zum Extrahieren von Teilzeichenfolgen entsprechende Methoden der String-Klasse nicht ausreichen, empfiehlt es sich auf, die Klasse Regex (System.Text.RegularExpressions) auszuweichen, die einen regulären Ausdruck darstellt. (Wurde der Namensraum im Kopf der Datei Chart.cs bekannt gemacht?) Obgleich reguläre Ausdrücke (die keine Erfindung der Sprache C# sind) ein probates Mittel für u. a. die Umgestaltung und/oder Splittung von Zeichenfolgen darstellen, bedeuten sie zugleich einen Rückfall in die Untiefen kaum verständlicher Kryptik, weswegen das Kind ein eher ungeliebtes ist. Sie und auch ich, wir kommen allerdings mit dem Ausdruck ,\s*
und einem blauen Auge davon. Für sich selbst genommen nützt die Zeichenfolge wenig. Erst im Zusammenspiel mit dem Regex-Member Split(), das einen String
174
Entwicklung der Programmierlogik
(in dem Falle readF) an den Fundstellen des regulären Ausdrucks (Kommata) trennt, wird sie interessant. Beachten Sie dabei zweierlei: Split() erwartet zwei string-Argumente. Folglich muss der reguläre Ausdruck in Anführungszeichen gesetzt und mit einem Zeichenfolgeliteral (@) versehen werden. Dies wäre eine verzichtbare Ergänzung, gäbe es im regulären Ausdruck keine Escapesequenz (\): posVal = Regex.Split(readF, @",\s*");
Schauen wir uns die Funktion readFileContent() in Gänze an: private string[] readFileContent() { string[] posVal = new string[8]; string getDataPath = "C:/Data.txt"; string readF; StreamReader stream = new StreamReader(getDataPath); readF = stream.ReadToEnd(); stream.Close(); posVal = Regex.Split(readF, @",\s*"); return posVal; } Listing 6.9
Trennung von Wichtigem und Unwichtigem in der Methode »readFileContent()«
Am Ende der readFileContent()-Methode steht ein »gewöhnliches« Array aus strings, aus dem im folgenden Abschnitt eigene Schlüsse gezogen werden. Positionsbestimmung Auch die private Routine GetPositions() liefert ein Array zurück – ein Array aus int-Elementen. Dies ist ein besonderes Array aus int-Elementen, nämlich eines, das wiederum aus Arrays besteht. Damit ist kein »klassisches« zweidimensionales Array gemeint, sondern ein Array aus Arrays oder, um einen anderen Terminus zu gebrauchen, ein verzweigtes Array. Trotzdem ist die Signatur der Methode GetPositions()simpel: int[][] GetPositions(){}
Das ist genauso simpel wie die Definition zweier, eindimensionaler Arrays. Beide besitzen die Länge 9. Das erste jedoch enthält Strings, das zweite int-Werte:
175
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
string[] pVal = new string[9]; int[] yPos = new int[9];
Kaum des Schreibens wert ist die Deklaration einer int-Variablen: int py; pVal nimmt das auf, was readFileContent() zurückgibt, womit zugleich der Me-
thodenaufruf angekündigt ist: pVal = readFileContent();
Zum Auslesen und Füllen eines Arrays verwenden wir ganz klassisch die forSchleife. Ohne Sie verschrecken zu wollen, nehme ich den Code (in dem beides und noch mehr geschieht) vorweg: for (int i = 0; i <= 8; i++) { py = Convert.ToInt16(pVal[i]); yPos[i] = py - (4550 +((Convert.ToInt16(pVal[i].Substring(2)) / 10) * 60)); }
Zwei Zeilen gibt es im Rumpf dieser Schleife. Was die erste mit der zweiten gemeinsam hat, ist die Konvertierung der in pVal enthaltenen Array-Elemente ins int-Format. Wie schon bei vergangenen Projekten wurden auch hier die Klasse Convert sowie das Member ToInt16() eingesetzt. In der zweiten Zeile allerdings wird vor der Konvertierung eine Teilzeichenfolge des Array-Elements gebildet: Substring(2) (Member der String-Klasse) ruft den Teilstring ab der String-Position 2 ab. Weiter im Vergleich der beiden Zeilen: In der ersten Zeile wird das string-Array pVal über seinen Index i ausgelesen. In der zweiten Zeile wird das int-Array yPos über denselben Index gefüllt. Gefüllt mit was? Mit dem Ergebnis der Umrechnung ausgewählter DAX-Punkte (die 9 Drehfelder im Formular Data) in die y-Koordinate. Denn was nützt es uns zu wissen, dass der DAX kurz vor Xetra-Handelschluss auf schlappen 5100 Punkten steht, wenn das Programm nicht weiß, welcher Pixelposition im Bezugssystem der Wert entspricht? Was schlussendlich also im Array yPos enthalten ist, sind 9 in Koordinatenpunkte umgerechnete DAX-Stände. Egal, ob sie hoch oder niedrig sind – es ist einfach, die über den Handelstag verteilten Stände des Deutschen Aktienindex der entsprechenden Uhrzeit auf der Abszisse des Koordinatensystems (der x-Achse) zuzuordnen. Die Werte existieren
176
Entwicklung der Programmierlogik
nämlich bereits. Und Sie wissen auch, wo: Jede vertikale Linie des Gitters trifft die x-Achse an einer wohldefinierten Stelle. Doch Vorsicht, vergessen Sie zweierlei nicht: 왘
Auch die linke vertikale Begrenzung des Koordinatensystems ist mit einer Uhrzeit assoziiert (siehe Abbildung 6.8).
왘
Bei uns ist um 17h00 Handelsschluss. Folglich brauchen im Sinne der Uhrzeit die letzten beiden vertikalen Linien keiner x-Position mehr zugeordnet zu werden (siehe auch hierzu Abbildung 6.8).
Im folgenden Neunzeiler werden neun int-Arrays mit jeweils 2 Elementen vereinbart. Das erste Element ist die Position der DAX-Werte auf der x-Achse. Das zweite Element ist die Position des DAX-Wertes, gespeichert an der Indexposition i im Array yPos: int[] int[] int[] int[] int[] int[] int[] int[] int[]
ko k1 k2 k3 k4 k5 k6 k7 k8
= = = = = = = = =
{ { { { { { { { {
100, 190, 275, 360, 445, 530, 615, 700, 785,
yPos[0] yPos[1] yPos[2] yPos[3] yPos[4] yPos[5] yPos[6] yPos[7] yPos[8]
}; }; }; }; }; }; }; }; };
Somit sind wir im Besitz der Koordinaten für jeden von Ihnen gewählten oder nicht gewählten DAX-Stand (vergessen Sie nicht die Default-Belegung der neun Drehfelder – Sie müssen den Aktienindex nicht manipulieren). Womit ich genauso unzufrieden bin wie Sie. Indem wir jedoch mit den neun int-Arrays ein verzweigtes Array initialisieren, ist das als Rückgabe der Methode GetPositions() Vereinbarte in trockenen Tüchern: int[][] posg = {ko, k1, k2, k3, k4, k5, k6, k7, k8};
Also: return posg;
Hier sehen Sie die Methode GetPositions() in der Gesamtdarstellung: private int[][] GetPositions() { string[] pVal = new string[9]; int[] yPos = new int[9]; int py;
177
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
pVal = readFileContent(); for (int i = 0; i <= 8; i++) { py = Convert.ToInt16(pVal[i]); yPos[i] = py - (4550 + ((Convert.ToInt16(pVal[i].Substring(2)) / 10) * 60)); } int[] int[] int[] int[] int[] int[] int[] int[] int[]
ko k1 k2 k3 k4 k5 k6 k7 k8
= = = = = = = = =
{ { { { { { { { {
100, 190, 275, 360, 445, 530, 615, 700, 785,
yPos[0] yPos[1] yPos[2] yPos[3] yPos[4] yPos[5] yPos[6] yPos[7] yPos[8]
}; }; }; }; }; }; }; }; };
int[][] posg = {ko, k1, k2, k3, k4, k5, k6, k7, k8}; return posg; } Listing 6.10
Die Methode »GetPositions()« – Arrays, wohin das Auge blickt
Keine Frage, mit uns geht es aufwärts! Linien am laufenden Band – so entsteht die DAX-Kurve Wenn es im Folgenden gilt, die Methode DAXPainting() (Zugriffsmodifizierer : private; Rückgabetyp: void; keine zu übergebenden Parameter) zu entwickeln, geschieht das selbstredend auch vor dem Hintergrund dessen, was auf der Registerkarte Optionen (im Formular Data) im Bezug auf die Darstellung der Kurve ausgewählt wurde. Zunächst jedoch ein paar einfache Deklarationen: int xs, ys, xe, ye;
Daran schließt sich das Erzeugen einer weiteren Graphics-Zeichenoberfläche an, und zwar über die CreateGraphics()-Methode der Basisklasse Control: Graphics chartGraphics = this.CreateGraphics();
178
Entwicklung der Programmierlogik
Ignorieren Sie nicht das Schlüsselwort this, schließlich muss dem Programm klar sein, wo die Zeichenoberfläche eingerichtet werden soll. Zeichnen ohne Farbe ist eine fruchtlose Angelegenheit, weshalb eine ColorStruktur, cl, Sinn macht: Color cl = new Color();
Es gibt drei Farben, in denen der DAX dargestellt werden kann: Schwarz, Rot, Blau (siehe Abschnitt 6.2) . Die gleichlautenden, zu Beginn der Klasse Chart deklarierten Variablen sind vom Typ RadioButton. Die Eigenschaft Checked der gleichnamigen Klasse beantwortet die Frage, ob das Steuerelement gesetzt ist oder nicht. Das genügt, lässt sich doch über ein schnödes if-else-if-Konstrukt, in Abhängigkeit vom gesetzten RadioButton, prima die in cl abzuspeichernde Farbe bestimmen: if (schwarz.Checked) { cl = Color.Black; } else if (rot.Checked) { cl = Color.Red; } else if (blau.Checked) { cl =Color.Blue; }
Ähnlich wie bei der Programmierung des Bezugssystems muss auch hier ein Zeichenstift vorhanden sein, wobei dem Konstruktor der zwischenzeitlich hinlänglich bekannten Pen-Klasse das Color-Objekt cl (und damit die ausgewählte Farbe) zu übergeben wäre: Pen chartPen = new Pen(cl);
Allerdings sollte der Stift chartPen noch hinsichtlich seiner Breite näher spezifiziert werden, denn auch die Breite ist ja über die Benutzeroberfläche auszuwählen. Die Width-Eigenschaft der Pen-Klasse wird mit der Variable wd belegt, die im Konstruktor der Chart-Klasse mit dem Eintrag initialisiert wurde, der unter Breite der DAX-Kurve ausgewählt wurde: chartPen.Width = wd;
179
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
So viel zur Erfassung der Registerkarte Optionen durch das Programm. Als Nächstes benötigen wir das, was durch GetPostion() zurückgegeben wird. GetPosition() fungiert hier als Lieferant der Koordinaten, die in einem verzweigten Array gespeichert sind. int[][] posgesamt = GetPositions();
Dem Zufallsgenerator Random und seiner eher bedingten Zufälligkeit sind Sie bereits an anderer Stelle begegnet. Auch deshalb bedarf die folgende Instanziierung keiner Erklärung: Random rd = new Random();
Nicht jeder Programmierer empfindet ein und dasselbe Listing auf dieselbe Weise. Was der eine als logisch und selbsterklärend erachtet (nicht selten im Brustton selbstgefälliger Überzeugung), ist für den anderen eine harte Nuss: for (int i = 0; i <= 7; i++) { xs = posgesamt[i][0]; ys = posgesamt[i][1]; xe = posgesamt[i+1][0]; ye = posgesamt[i+1][1]; Point[] points = { new Point(xs,ys), new Point(xs+5, rd.Next(ye-25,ye+25)), new Point(xs+10, rd.Next(ye-25,ye+25)), new Point(xs+15, rd.Next(ye-25,ye+25)), new Point(xs+20, rd.Next(ye-25,ye+25)), new Point(xs+25, rd.Next(ye-25,ye+25)), new Point(xs+30, rd.Next(ye-25,ye+25)), new Point(xs+35, rd.Next(ye-25,ye+25)), new Point(xs+40, rd.Next(ye-25,ye+25)), new Point(xs+45, rd.Next(ye-25,ye+25)), new Point(xs+50, rd.Next(ye-25,ye+25)), new Point(xs+55, rd.Next(ye-25,ye+25)), new Point(xs+60, rd.Next(ye-25,ye+25)), new Point(xs+65, rd.Next(ye-25,ye+25)), new Point(xs+70, rd.Next(ye-25,ye+25)), new Point(xs+75, rd.Next(ye-25,ye+25)), new Point(xs+80, rd.Next(ye-25,ye+25)), new Point(xe,ye) };
180
Entwicklung der Programmierlogik
chartGraphics.DrawLines(chartPen, points); delay(dl); } Listing 6.11
Unter die Motorhaube der DAX-Darstellung geschaut
In salopper Bildhaftigkeit gesprochen: Die aufgeblasene for-Schleife ist der Motor der DAX-Darstellung, und wir können nicht umhin, diesen Motor in seine Einzelteile zu zerlegen. Doch seien Sie guter Dinge: Selbst im Triebwerk eines Ferraris spielen nur Kolben die Hauptrolle. Die erste Zeile im Rumpf der for-Schleife ist folgende Zuweisung: xs = posgesamt[i][0];
Was geschieht beim Durchlaufen der for-Schleife? Einfache Antwort: Neun x-Koordinaten von genauso vielen DAX-Punkten werden ausgelesen. Zweite Frage? Wie geht das genau vonstatten? Antwort: Zunächst durchläuft der Index i das Unterarray (ko,..., k8), von dem jeweils das erste von zwei Elementen ([0]), sprich die x-Koordinate , ausgelesen und der Variablen xs zugewiesen wird. Zweite Zeile: ys = posgesamt[i][1];
Die Variable ys verrät das Ziel der Reise: die neun y-Koordinaten. Nichts anderes kann es sein, wird doch in den neun Unterarrays jeweils die Indexposition 1 angesprochen. Dort sitzen die y-Werte. Wiederholen wir das mit den x-Koordinaten. Nicht, weil es so schön war, sondern weil eine kleine Variation ansteht: xe = posgesamt[i+1][0];
Hier werden die neun Unterarrays nicht ab der Indexposition i durchlaufen, sondern erst ab Position i+1. Analog geschieht das für die y-Werte: ye = posgesamt[i+1][1];
Wir haben also zwei Koordinatenpaare, xs/ys sowie xe/ye, und somit die Möglichkeit, diese als Startpunkt (xs/ys) und Endpunkt (xe/ye) eines Segments der DAX-Kurve einzusetzen – multipliziert mit neun, denn zwischen 9:00 und 17:00 Uhr sind neun Segmente zeitverzögert aufzubauen.
181
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Etwas fehlt dem Projekt noch, sollen doch zwischen Start- und Endpunkt der DAX-Segmente weitere, diesmal vom System generierte DAX-Stände zu sehen sein. Im Folgenden wird ein eindimensionales Array (points) aus 18 Punktstrukturen erzeugt: Point[] points { new new new new new new new new new new new new new new new new new new
= Point(xs,ys), Point(xs+5, rd.Next(ye-25,ye+25)), Point(xs+10, rd.Next(ye-25,ye+25)), Point(xs+15, rd.Next(ye-25,ye+25)), Point(xs+20, rd.Next(ye-25,ye+25)), Point(xs+25, rd.Next(ye-25,ye+25)), Point(xs+30, rd.Next(ye-25,ye+25)), Point(xs+35, rd.Next(ye-25,ye+25)), Point(xs+40, rd.Next(ye-25,ye+25)), Point(xs+45, rd.Next(ye-25,ye+25)), Point(xs+50, rd.Next(ye-25,ye+25)), Point(xs+55, rd.Next(ye-25,ye+25)), Point(xs+60, rd.Next(ye-25,ye+25)), Point(xs+65, rd.Next(ye-25,ye+25)), Point(xs+70, rd.Next(ye-25,ye+25)), Point(xs+75, rd.Next(ye-25,ye+25)), Point(xs+80, rd.Next(ye-25,ye+25)), Point(xe,ye)
}; Listing 6.12
Erzeugung eines eindimensionalen Arrays aus 18 Punktstrukturen
Jede der 18 Point-Strukturen stellt ein geordnetes Paar x- und y-Koordinaten (als ganze Zahlen) dar, durch das ein Punkt in einem zweidimensionalen Bezugssystem definiert ist. Das, was in der ersten und letzten der 18 Strukturen gespeichert ist, geht auf Ihr Konto, die 16 Strukturen dazwischen sind – zumindest, was die relevanten y-Koordinaten betrifft – der Scheinzufälligkeit des Random-Objekts rd geschuldet. Das Array points an sich repräsentiert eines von acht DAX-Segmenten, die zwischen neun definierten Uhrzeiten liegen. Ohne eine Methode, die Linien zeichnet, läuft der bis hierhin betriebene Aufwand ins Nichts. Die Formulierung ist ungenau. Vielmehr ist ein Mechanismus erforderlich, der eine Reihe von Liniensegmenten zeichnet, die durch ein Array
182
Entwicklung der Programmierlogik
von Point-Strukturen definiert sind. Jetzt müssen Sie bitte gut aufpassen, ist mit dem Begriff Liniensegment doch der siebzehnte Teil eines DAX-Segments gemeint! Auf den Punkt gebracht, sind wir bislang lediglich in der Lage, ein einziges, aus siebzehn Segmenten bestehendes DAX-Segment zu zeichnen, beispielsweise zwischen 9h00 und 10h00 Uhr. In C# formuliert, sieht das so aus: chartGraphics.DrawLines(chartPen, points);
An DrawLines(), als Member der Klasse Graphics, lässt sich nicht rütteln. Ist Pen und Points vereinbart (chartPen bzw. points), wird, ausgehend vom Laufindex i=0, das erste DAX-Segment (zwischen 9h00 und 10h00 gelegen) gezeichnet. Es folgt eine Pause von dl= ... was auch immer Sie unter Verzögerung der ChartSegmente eingestellt haben: delay(dl);
Anschließend geht es mit der Erhöhung des Index i um den Wert 1 weiter – mit der Konsequenz, dass die DAX-Segment-Grenzen um eine Stunde nach hinten verschoben werden. Anders formuliert: Der Endpunkt des vorherigen Segments wird zum Startpunkt des darauf folgenden Segments (10h00 bis 11h00). Zwecks Freigabe von Speicherressourcen sollte am Ende der Methode DAXPainting() die Zerstörung des Graphics-Objekts chartsGraphic stehen: chartGraphics.Dispose();
Das wichtigste Ereignis zum Schluss Doppelklicken Sie im Entwurfsmodus der Datei Chart.cs auf den Button Start. Wozu und weshalb? Sie wissen es. Sie wissen auch, was im Rumpf des generierten Eventhandlers button1_Click() einzutragen ist. Nichts weiter als die Methode DAXPainting()nämlich, ohne die nichts in Sachen DAX-Kurve geht. Das Ergebnis: private void button1_Click(object sender, EventArgs e) { DAXPainting(); } Listing 6.13
Genügsamer EventHandler – »button1_Click()«
Starten Sie das Programm über (Strg)+(F5), ausnahmsweise also ohne vorherigen Debug. Versuchen Sie spaßeshalber, die Entwicklung des DAX, so wie sie am 26.11.2009 vermeintlich geschah, über die neun Drehfelder der Data-Oberfläche in etwa so nachzubilden, wie Abbildung 6.10 es zeigt.
183
6.4
6
Garantiert ungefährlich – Manipulationen am DAX
Abbildung 6.10
6.5
Vielversprechend hatte der Handelstag begonnen – doch dann ...
Hätten Sie’s gewusst?
Nach über 40 Seiten mitunter anstrengender Lektüre dürfte Ihnen kaum der Sinn nach einem Autor stehen, der Ihnen inhaltsbezogen auf die sprichwörtliche Pelle rückt. Doch um – hoffentlich frei von jeder Hybris – mit dem viel zitierten Luther zu sprechen: Hier stehe ich und kann nicht anders. Gut, der Schreib- und Programmiertätigkeit geschuldet, sitze ich zwar, doch kann ich trotzdem nicht anders, denn eines (neben anderem) dürfte Ihnen vor allem in Abschnitt 6.4.1, »Das große Zeichnen – die Klasse »Chart««, unangenehm aufgefallen sein. Über die Dauer des (um eine halbe Stunde gekürzten) Xetra-Handelstags können Sie den Wert des DAX zu neun definierten Zeiten innerhalb von definierten Grenzen bestimmen. Das waren also neun DAX-Stände, die, in neun Koordinaten umgerechnet (genauer trifft das natürlich nur für die y-Koordinate zu), schließlich in ein Objekt vom Typ verzweigtes Array verpackt wurden: int[][] posg = {ko, k1, k2, k3, k4, k5, k6, k7, k8};
184
Hätten Sie’s gewusst?
So weit zu dem, was noch stimmig ist. Wie aber komme ich wenige Abschnitte später dazu, posg in einer for-Anweisung auszulesen, deren Laufindex lediglich von 0 bis 7 zählt? Adam Riese auf das eherne Tun des Programmierers übertragen, müsste die Konsequenz eine Unterschlagung des DAX-Standes sein, der um 17:00 Uhr bestimmt wird – was nicht passiert. Überlegen Sie, doch bitte ohne Hilfe des Debuggers. Zumindest indirekt würde der Ihnen die Antwort geben, auf die Sie zweifelsohne auch selbst kommen.
185
6.5
Ein Labyrinth verwirrt wirklich – manche schon durch seine Schreibweise. (Hans Horst Skupy)
7
Im Labyrinth des Minotaurus
Der eigentliche Erfolg der Labyrinthe (griechisch für die Einzahl: labyrinthos) besteht in ihrer Versinnbildlichung. Wer hat nicht schon vom »Labyrinth des Lebens gehört«, in dem sich selbst der rechtschaffenste, der wohlmeinendste Mensch irgendwann verirrt hätte? Das gern gebrauchte Bild hat einen Haken: Es stimmt nicht. Zumindest stimmt es dann nicht, wenn man die Definition für ein Labyrinth im weiteren Sinne zu Rate zieht, in der von verschlungenen Wegen ohne Verzweigungen die Rede ist, die unter regelmäßigen Richtungswechseln zum Mittelpunkt führen. Entscheidend an den verschlungenen Wegen ohne Verzweigungen ist nicht die Schwierigkeit, den Mittelpunkt bzw. den Ausgang zu finden, sondern ihn nicht zu finden (obgleich es nicht unmöglich ist). Dem Mutigen allerdings, der ohne Ariadnefaden in ein Labyrinth im engeren Sinne gerät, sei von Herzen der Erfolg gewünscht, einen Ausgang zu finden, existieren im System doch Wegverzweigungen, Kreuzungen genauso wie verzweiflungsfördernde Sackgassen, und all das ist unter Umständen en masse vorhanden. Wenn Sie einmal in einem solchen Wegesystem gefangen sind, spielt es keine Rolle, wo Sie sich tatsächlich befinden: Irrgarten heißt der Ort Ihrer Pein nämlich im Deutschen, ein Begriff, der sich nebenbei bemerkt gegenüber dem »Labyrinth des Lebens« nie richtig durchsetzen konnte. So finden sich (zumindest am Tag der Niederschrift dieses Textes) bei Google 293.000 Treffer für das »Labyrinth des Lebens«, wogegen nur ganze 49.000 Treffer auf den »Irrgarten des Lebens« entfallen. Richtig: Es ist zweitrangig. In der Theseus-Sage ließ der kretische König Minos (ein Sohn des Zeus) von Baumeister Daidalos (dessen tragisch berühmter Sohn Ikarus ein Problem mit der Sonne bekam) für den Minotaurus ein labyrinth-ähnliches Gefängnis errichten, anstatt ihn zu töten. Sein Leben aushauchen musste das Mensch-Stier-Wesen dennoch, denn Theseus, von der schönen Königstochter Ariadne (!) mit einem aufgerollten Faden ausgestattet, wagte sich ins Labyrinth und vor den Rachen des Minotaurus.
187
7
Im Labyrinth des Minotaurus
Dank des abgespulten Fadens fand der kühne Recke nach begangener »Heldentat« prompt auch wieder hinaus, im Gegensatz zu Minotaurus, der, sich mit sonderbaren Pillen aus Pech und Haaren todbringend den Magen verdarb. Bis heute gilt der bekannte Ariadnefaden als einziger Hinweis, dass mit dem Labyrinth des Daidalos ein verzweigtes Gangsystem, somit ein Irrgarten gemeint ist (siehe Abbildung 7.1 für die englische Version der »altgriechischen Haftanstalt«).
Abbildung 7.1
7.1
Skizze des Irrgartens von Hampton Court
Und dann kam Dijkstra ...
Als am 07.08.2002 heise online (http://www.heise.de) den Tod von Edsger Wybe Dijkstra vermeldete, dürfte mehr als ein Informatiker, vor allem der älteren Generation, seine ganz persönliche Schweigesekunde eingelegt haben. Flapsig formuliert, war der zeitlich und qualitativ erste Programmierer der Niederlande nämlich »nicht ohne« – auch nicht seine flotten Sprüche. Ob die Programmierung mit Basic Gehirnschäden verursacht (»Programming in Basics causes brain damage«: O-Ton Dijkstra), müssen bei Bedarf die Neurochirurgen klären. Während die – im wörtlichen Sinne – Schwimmunfähigkeit eines Unterseebootes dem technisch orientierten Menschen genauso klar sein dürfte wie die Denkunfähigkeit des Computers ("The question of whether Machines Can Think ... is about as relevant as the question of whether Submarines Can Swim."). Hier bedurfte es nicht der Einlassung des markigen Informatik-Professors. So umstritten Dijkstras Bemerkungen noch heute sind, so unbestritten sind seine Verdienste, allen voran der 1968 in den Communications of the ACM veröffentlichte Aufsatz »GOTO statement considered harmful«. Fortan gab es Spaghetti nur noch auf dem Teller, war mit diesem Aufsatz doch die strukturierte Programmierung à la while-do-until (und natürlich if) geboren.
188
Und dann kam Dijkstra ...
Auch im neuen Jahrzehnt finden sich obige Schlüsselwörter in beinahe jedem hochsprachlichen Programm zu beinahe jedem Szenario – mit Sicherheit aber (und sei es nur in abgeleiteter Form) in der Software für Routenplaner und Navigationsinstrumente, womit uns der halbe Schulterschluss zu Theseus gelungen wäre. Notgedrungen kam dieser im Labyrinth genauso wenig ohne Faden aus, wie der im Straßennetz rollende Mensch ohne »Navi« auszukommen scheint. Dieses Gerät gäbe es möglicherweise ohne Dijkstra nicht, setzt die zugehörige Software doch nichts Geringeres als den Algorithmus zur Berechnung kürzester Pfade zwischen einem Startknoten und einem beliebigen Knoten in einem kantengewichteten Graphen um. Wer der Erfinder des Algorithmus ist? Dijkstra.
7.1.1
Ein Quäntchen Graphentheorie
In grober Unkenntnis Ihrer fachlichen Vorbelastung kann ich Sie nur bitten, den Dijkstra-Algorithmus weder zu überschätzen noch zu unterschätzen. Sie sollten allerdings auf jeden Fall ungefähr wissen, was ein Graph ist. Ein Graph besteht aus: 왘
Knoten
왘
Kanten
Zeigen die Kanten eine Richtung an, so spricht man von einem gerichteten Graphen. Abbildung 7.2 zeigt einen mehr oder weniger typischen Vertreter mit sieben Knoten (A, B, C, D, E und F) und acht Kanten:
C A
B E D G
Abbildung 7.2
F
Beispiel für einen gerichteten Graphen
Hinweis Erlauben Sie mir, auch beim Schreiben kurze Wege zu suchen: Wenn ich im Weiteren Dijkstra schreibe, ist damit der Dijkstra-Algorithmus gemeint.
189
7.1
7
Im Labyrinth des Minotaurus
Abbildung 7.3 ist ein schönes Beispiel für einen azyklischen Graphen, mit dem wir gut beraten sind, geht es doch lediglich darum, von einem Startknoten A zu einem Zielknoten F zu kommen. Ein labyrinthischer Rundwanderweg ist nicht angezeigt, die Gewichtung der Kanten, im Sinne einer Distanz, dagegen schon. Hier sehen Sie denselben Graphen mit beispielhaft gewichteten – bzw. um einen anderen Ausdruck zu bemühen – bewerteten Kanten.
C
3
A
2
B
3
7
E
4 6
D G Abbildung 7.3
2
1
F
Gerichteter Graph mit gewichteten Kanten
Mehr Graphentheorie brauchen wir nicht, um Dijkstra exemplarisch anwenden zu können und das Kommende zu verstehen. Aufwand sollte allerdings bereits hier vermieden werden, weswegen ich Sie bitte, den gerichteten, kantengewichteten Graphen in Abbildung 7.3 genauer in Augenschein zu nehmen: 왘
Über den Knoten A steigen Sie in den Irrgarten ein, und F als Zielknoten repräsentiert den Ausgang. An Knoten B kommen Sie sowieso vorbei, weswegen Sie B als Startknoten definieren und sich A schenken können.
왘
Knoten G ist hier völlig überflüssig: Von dem können Sie zwar starten, Sie können aber nicht zu ihm gelangen – siehe Pfeilrichtung.
왘
Einmal bei E angekommen, ist der Ausgang in Gestalt des Knotens F nicht weit. Von E nach F führt ein Weg. Das hat folgende Konsequenz: Der Endknoten F könnte aus Dijkstra herausgehalten werden.
Abbildung 7.4 zeigt das Ergebnis unserer Bemühungen.
190
Und dann kam Dijkstra ...
C
3
3
B
7
E
4 6
D Abbildung 7.4
Der auf vier Knoten und vier Kanten reduzierte Graph
Auf zu Dijkstra.
7.1.2
Dijkstra in Worten
Das Prinzip hinter Dijkstra ist, ausgehend von einem definierten Startknoten die kürzestmöglichen Wege hin zu allen anderen Knoten zu ermitteln. (Im Falle von Abbildung 7.4 ist Knoten B der Startknoten). Längere Wege werden kategorisch ausgeschlossen. Der Algorithmus lässt sich wie folgt gliedern: 왘
Weise jedem Knoten die beiden Attribute Distanz und Vorgänger zu.
왘
Initialisiere die Distanz im Startknoten mit 0, in allen anderen Knoten mit .
왘
Solange noch Knoten existieren, die nicht besucht wurden, wähle unter ihnen jenen Knoten mit minimaler Distanz aus, und: 왘
markiere/speichere, dass dieser Knoten bereits besucht wurde.
왘
berechne für alle noch unbesuchten Nachbarknoten die Summe des jeweiligen Kantengewichts und der Distanz im aktuellen Knoten.
왘
Ist dieser Wert für einen Knoten kleiner als die im Knoten gespeicherte Distanz, aktualisiere die Distanz, und lege den aktuellen Knoten als Vorgänger fest.
Einfach oder nicht einfach? Entscheiden Sie. Der letzte Schritt, der auch als Update bezeichnet wird, ist jedenfalls die zentrale Idee des Algorithmus. Im nächsten Abschnitt werden wir uns dem anhand eines sehr einfachen Beispiels noch weiter nähern. Welches Beispiel gemeint ist? Natürlich der Graph in Abbildung 7.4.
191
7.1
7
Im Labyrinth des Minotaurus
7.1.3
Listenplätze
Der Algorithmus arbeitet mit zwei Listen. Die eine nennen wir Opt (für Optimum), die andere Rest. Am Ende der Arbeit enthält Opt alle Knoten, die zum kürzesten Weg zwischen B und E beitragen, genauer gesagt die Knoten, deren kürzester Weg bereits bestimmt ist. Am Anfang enthält Opt lediglich den Knoten B, von dem gestartet wird: Opt={B}
Abgesehen davon, ist der Abstand von B zu sich selbst mit 0 mehr als überschaubar. In die Liste Rest packen wir alle Nachbarknoten von B. Das sind 3 an der Zahl, nämlich C, D und E: Rest={C,D,E}
Von Interesse ist lediglich der Knoten C, aus einem einfachen Grund: C liegt E am nächsten (Kantengewicht 3). Schön für C, ist dem Knoten doch ab sofort ein »Ehrenplatz« in der Liste Opt sicher. Damit gilt: Opt ={B,C} Rest={D,E}
Um hier ein Zwischenergebnis zu formulieren: Der kürzeste Weg von B nach E führt definitiv über den Knoten C. Und mit dem machen wir weiter, enthält doch auch C Nachbarknoten – falsch: Lediglich ein Nachbarknoten existiert: E, der damit der Liste Rest entnommen und Opt zugefügt werden kann. Nebenbei bemerkt ist E auch noch der Zielknoten – ja, und es stimmt tatsächlich: Wir sind fertig. Das Ergebnis sieht so aus: Opt ={B,C,E} Rest={D}
Dann gute Reise von B nach E! Gleichwohl, vergessen Sie nicht: Zu B müssen Sie erst einmal kommen, und wenn Sie bei E angekommen sind, geht die Reise noch ein Stück weiter, nämlich zum wirklichen Zielknoten F.
7.2
»Verworrene« Absichten
Gibt es, von »Hello World« auf der Kommandozeile abgesehen, in Sachen Programmentwicklung etwas Langweiligeres als ein durch gleich große Quadrate zusammengesetztes Labyrinth, bei dem ein verstohlener Seitenblick genügt, um gedanklich aus ihm herauszukommen? Abbildung 7.5 zeigt also nichts Aufregendes:
192
»Verworrene« Absichten
Spätestens nach einem zweiten Blick ist klar, dass hier zwei Wege zum Ziel führen – und Dijkstra soll den kürzeren von beiden finden.
Abbildung 7.5
Überschaubar verwirrend – unser Irrgarten
Trotzdem kann unser Labyrinth – mit einigem Wohlwollen – als solches durchgehen, sind echte Labyrinthe (pardon: Irrgärten) wie erwähnt doch primär durch die Möglichkeit definiert, sich darin zu verirren. Wie auch immer, Dijkstra wird uns helfen, den sprichwörtlichen Faden nicht zu verlieren, vorausgesetzt, dass wir an der Programmierung des Algorithmus nicht verzweifeln. Unsere Absichten gipfeln in einer »konzeptionellen Unverfrorenheit erster Ordnung«, die zu erklären ich mir Mühe gebe. Es geht um die optische Vermischung zweier Ebenen, und zwar der Ebene des Labyrinths, so wie Abbildung 7.5 es darstellt, und der Ebene des Graphen, der die Abstands- und Wegverhältnisse im Labyrinth repräsentiert. Genauer gesagt, werden im Labyrinth 왘
eine gewisse Anzahl an Quadraten als Knoten definiert und durch einen Buchstaben sowie eine Umrandung entsprechend ausgewiesen.
왘
die Anzahl der zwischen den »Quadratknoten« vorhandenen Quadrate als Gewichtung der Kanten zwischen den Knoten betrachtet. Ein Beispiel: Zwi-
193
7.2
7
Im Labyrinth des Minotaurus
schen dem mit A gekennzeichneten Quadrat und Quadrat B liegen drei Quadrate. Eine imaginäre Kante zwischen A und B bekäme demnach eine glatte 3 als Bewertung bzw. Gewichtung. Schauen Sie sich Abbildung 7.6 an. So wird es später aussehen, nachdem Sie auf die Schaltfläche Knoten definieren geklickt haben.
Abbildung 7.6
7.2.1
Labyrinth mit eingeblendeten Knoten
»Schwaches Knotenkriterium«
Elf Knoten (von A bis K) werden definiert. Die Frage ist, nach welchen Kriterien die Festlegung der Knoten erfolgt. Vorweg und zu Ihrem und meinem Glück: Es gibt nur ein Kriterium. Abgesehen vom Zielknoten K besitzen alle als Knoten definierten Quadrate eine nicht unerhebliche Gemeinsamkeit, die als Rätsel zu verpacken (siehe Abschnitt 7.5) womöglich starker Tobak wäre. Womit ich Sie selbstredend nicht unterschätzt haben will – im Gegenteil. Dennoch lässt sich die Gemeinsamkeit in einem einzigen Satz formulieren: Außer dem Knoten K ist jeder der zehn Knoten im Verbund der Quadrate so positio-
194
»Verworrene« Absichten
niert, dass sich an mindestens zwei der senkrecht aufeinander stehenden Seiten des Quadrats weitere Quadrate anschließen. Anders gesagt ist jeder der so definierten Knoten der Ort, an dem zwingend (zumindest unter der Prämisse des Ziels) ein Richtungswechsel erfolgt. Ignorieren Sie den, stehen Sie alsbald vor einer Mauer, einer Hecke oder was auch immer geeignet ist, Ihnen den Weg zu versperren. Nehmen Sie als Beispiel J. Diesen Knoten erreichen Sie »von oben« (der Vorgängerknoten trägt die Bezeichnung H). Am Knoten selbst führt der Weg weiter entlang »der Horizontalen«, nämlich in Richtung Endknoten K, wo Sie das Herz einer hübschen Adelsdame oder/und eine Blaskapelle erwartet.
7.2.2
Nullsummenspiel
Sie haben richtig gesehen und darüber hinaus aus dem Gesehenen ebenso richtige Schlüsse gezogen: Denn definitiv existieren in Abbildung 7.6 mit Null gewichtete, gleichwohl imaginäre Kanten. Diese sind sogar in der Überzahl, was uns genauso wenig irritiert wie die Tatsache an sich. Tabelle 7.1 gibt einen Überblick, zwischen welchen Knoten nullgewichtete Kanten liegen. Nullgewichtete Kanten CD DG GH FE EI IH Tabelle 7.1
Nullgewichtete Kanten im Verbund der Knoten
Dijkstra lässt sich jedenfalls auch mit nullgewichteten Kanten »betreiben« – ob das sinnvoll ist oder nicht müssen Sie entscheiden. (Schwieriger wird es mit negativen Kanten, unlösbar ist das Problem gleichwohl nicht.)
7.2.3
Wie es weiter geht
Neben der oben erwähnten Schaltfläche Knoten definieren benötigen wir noch eine zweite (Knoten ausblenden) und eine dritte und letzte (Berechnung des kürzesten Weges). Klicken Sie auf diese, werden unter den elf Knoten die Buch-
195
7.2
7
Im Labyrinth des Minotaurus
staben jener Knoten rot dargestellt, durch die eine minimale Wegstrecke beschrieben ist. So lässt sich, gerade im Zusammenhang mit der Zeitverzögerung, recht schön verfolgen, wo entlang im Labyrinth auf kürzestem Wege das Ziel bzw. der Zielknoten zu erreichen ist. In abgewandelter Form (quasi als kleine Zugabe) findet das Ganze ein zweites Mal statt, und zwar in einer gewöhnlichen, wenngleich ausgedehnten TextBox, wo die wegbildenden Buchstaben mittig, in fettem, »bold-gestyltem« Rot und im selben zeitlichen Abstand angezeigt werden.
7.3
Entwicklung der Benutzeroberfläche
Auf Ebene der Controls besteht der gezeigte Irrgarten aus nichts weiter als 36, in einem TableLayoutPanel-Container (Kategorie Container in der Toolbox) angeordneten Label-Steuerelementen, die Sie allesamt mit Width: 100 und Height: 100 (Kategorie Size im Eigenschaften-Fenster) skalieren. Hochgerechnet auf die Länge und Breite des Containers TableLayoutPanel ergibt sich für Width: 600, für Height: 600 – natürlich, schließlich operieren wir mit Quadraten. Aber der Reihe nach, und überhaupt: Die Grundlage auch dieses Beispiels ist eine WindowsForm als Basis eines neuen Projekts. Rufen Sie also über das Hauptmenü Datei 폷 Neu 폷 Projekt zunächst die Dialogmaske Neues Projekt auf. Wählen Sie im mittleren Segment des Fensters Windows Forms-Anwendung. Unter Name tragen Sie »Labyrinth« ein. Wo auch immer Sie die Dateien abgelegt haben möchten, der Button Durchsuchen führt Sie zum Dateisystem und die Schaltfläche Ok zu einem vorerzeugten Projekt namens Labyrinth. Öffnen Sie die Datei Labyrinth.cs im Entwurfsmodus.
7.3.1
Ein Fall für sich – das »TableLayoutPanel«
Seine Möglichkeiten kann das TableLayoutPanel-Steuerelement (auch Container sind Steuerelemente) erst im Kontext hochentwickelter Layouts, die sich zur Laufzeit geänderten Proportionen anpassen, in Gänze ausspielen. Darin ist der eigentliche Sinn des aus einem Raster und Zeilen bestehenden TableLayoutPanels zu sehen – abgesehen von der Möglichkeit, auf die Schnelle ein Labyrinth zu kreieren. Irgendwie findet das TableLayoutPanel schon zu seiner WindowsForm Labyrinth. Am einfachsten dadurch, dass Sie auf der Registerkarte Container der Toolbox einmal auf TableLayoutPanel und ein zweites Mal in den rechten oberen Bereich des Formulars klicken.
196
Entwicklung der Benutzeroberfläche
Drag&Drop ist nicht alles. Geht die Positionierung schief, erinnern Sie sich an die Möglichkeit, ein Steuerelement 왘
zu fokussieren (einfacher Klick auf das Control) und
왘
über sein Richtungskreuz zu positionieren.
Fokussieren werden Sie eh müssen. Es sei denn, Sie gehen, um zur Dialogmaske Spalten- und Zeilenstile (siehe Abbildung 7.7) zu gelangen, den Weg über das Eigenschaften-Fenster von tablelLayoutPanel1, in dem Sie unter der Kategorie Size sowohl Width als auch Height auf 600 festlegen. Hinter den Einträgen Rows und Columns verbirgt sich jeweils ein und dasselbe »Spalten-und Zeilenstile«-Fenster. Desgleichen hinter dem Menüeintrag Zeilen und Spalten bearbeiten des TableLayoutPanel-Aufgaben-Menüs. Da allerdings wären wir wieder beim fokussierten tablelLayoutPanel1 angelangt, dessen rechtszeigender Pfeil nichts für schwache Sehnerven ist. Hinter ihm finden Sie das TableLayoutPanel-Aufgaben-Menü.
Abbildung 7.7 Die Dialogmaske »Spalten- und Zeilenstile« des Containers »tableLayoutPanel1«
Im Formular Spalten- und Zeilenstile läuft alles auf die Formatierung von Spalten und Zeilen hinaus. Das Vorgehen ist identisch, weswegen im Folgenden lediglich auf die Spaltenstile näher eingegangen wird (Dropdown-Listenfeld Anzeigen).
197
7.3
7
Im Labyrinth des Minotaurus
Standardmäßig enthält das TableLayoutPanel-Control zwei Spalten (und vier Reihen). Benötigt werden sechs Spalten und sechs Reihen. Eine Schaltfläche Hinzufügen existiert im Formular, die dieses Manko behebt, wenn Sie sie viermal anklicken. In der GroupBox Grössentyp geht es uns um Pixel, aktivieren Sie also den RadioButton Absolut. Von Column1 bis Column6 sollte im Drehfeld Pixel jeweils der Wert 100 eingestellt sein. Wiederholen Sie das Prozedere für die benötigten Reihen, klicken Sie abschließend auf den Button OK, und das Drama kann beginnen.
7.3.2
Labels am laufenden Band
Wir haben nun 36 Label-Controls, die in 36 Tabellenzellen einzufügen sind. Diesen 36 Labels müssen mindestens vier und höchstens fünf Eigenschaften (von denen es zwei in sich haben) zugewiesen werden. 왘
Die 36 Labels akzeptieren erst dann die Festlegungen Width: 100, Height: 100 als Größenangaben, wenn zuvor die Dock-Eigenschaft auf Fill gesetzt wurde. Fill, als Member der Enumeration DockStyle (aus dem Namensraum System.Windows.Forms), bewirkt, dass alle Ränder des untergeordneten Steuerelements (in unserem Falle eines Labels) an das übergeordnete Steuerelement (tableLayoutPanel1) angedockt werden, woraus eine Größenanpassung des Labels resultiert – vereinfacht gesprochen.
왘
Bei den 36 Labels ist die Anchor-Eigenschaft, gerade im Zusammenspiel mit dem übergeordneten Steuerelement TableLayoutPanel (tableLayoutPanel1), nicht unabhängig von der Dock-Eigenschaft, und zwar weder im Entwurf noch zur Laufzeit. Zum gleichen Zeitpunkt kann jeweils nur eine Eigenschaft festgelegt werden, wobei sich grundsätzlich die beiden Eigenschaften wechselseitig ausschließen. Operieren Sie dennoch im Entwurf mit Fill als Dock-Eigenschaft und Top, Left als Anchor-Eigenschaft – auf dass es gelinge.
Relevant wird das Beschriebene natürlich erst, nachdem das erste Label in die erste Tabellenzelle (von links oben aus betrachtet) eingefügt wurde. Seitens der Entwicklungsumgebung erfolgt die »Nummerierung«, sprich die Festlegung der Name-Eigenschaft von label1 bis label36. Vernünftigerweise dürfte Ihnen der Gedanke an Copy & Paste als Alternative zur »Standleitung« (36 Label-Controls) zwischen WindowsForm und Toolbox gekommen sein. Und in der Tat, es funktioniert. Beachten Sie allerdings Folgendes: Wenn Sie mit der rechten Maustaste auf ein bereits platziertes label1 klicken, um im Kontextmenü Kopieren zu wählen, kann es in dem Moment problematisch
198
Entwicklung der Benutzeroberfläche
werden, in dem Sie das kopierte Control via Einfügen in die jeweils nächste freie Zelle einfügen wollen. Eingefügt wird zweifelsohne, die Frage ist nur wohin, d. h. in welche Zelle des TableLayoutPanel. Kurzum: Auch wenn es länger dauert, machen Sie sich die Mühe, 36 Label-Controls aus der Toolbox in die entsprechenden Zellen zu befördern Stück für Stück. Als Nächstes geht es um die Farben der fröhlichen Label-Schar. Zwei Systemfarben (Window und HotTrack) sowie zwei Webfarben (DarkOrange und Maroon) kommen zum Einsatz. Hinter Window verbirgt sich ein schnödes Weiß. HotTrack kennzeichnet ein dunkles Blau, DarkOrange ein nicht wirklich dunkles Orange und Maroon irgendetwas zwischen Rot und Lila. Sie werden sehen. Tabelle 7.2 zeigt, welches Label über die BackColor-Eigenschaft (EigenschaftenFenster) mit welcher der genannten Farben zu verknüpfen ist. HotTrack
DarkOrange X
label1
X
label36 label6
X
label8 – label10
X
label12
X
label15
X
label19
X
label23
X
label25–label26
X
label28–label30
X
label31
X
Tabelle 7.2
Maroon
36 Labels und ihre »BackColor«-Eigenschaft
Wo aber taucht in der Tabelle die Systemfarbe Window auf? Eine gleichnamige Spalte existiert augenscheinlich nicht. Sollte es eine Spalte Window geben? Nein. Nachdem Sie nämlich die BackColor-Eigenschaft genannter Labels mit HotTrack, DarkOrange oder Maroon belegt haben, bleiben 20 Labels farbenmäßig sozusagen unbesetzt (gleichwohl ist deren Hintergrund grau). Das genau sind die Labels, deren BackColor-Eigenschaft mit Window zu belegen ist. Hinweis Ein Klick auf das mit Eigenschaften zu belegende Label »fokussiert« das zugehörige Eigenschaften-Fenster – was für (beinahe) jedes Control gilt.
199
7.3
7
Im Labyrinth des Minotaurus
Jetzt legen wir die nächste Eigenschaft fest: Text. Löschen Sie im EigenschaftenFenster außer für label1 und label36 den Default-Wert der Text-Eigenschaft (der in der Regel identisch dem Wert der Name-Eigenschaft ist). Setzen Sie bei label1 die Name-Eigenschaft auf Start und bei label36 auf Ziel. Start und Ziel sind am besten mittig auf dem jeweiligen Label zu positionieren, deshalb setzen Sie bitte bei beiden Labeln die TextAlign-Eigenschaft auf MiddleCenter. Noch einmal zur Farbgestaltung: Setzen Sie am Start die Eigenschaft ForeColor auf ControlText (Systemfarbe) und am Ziel auf den zwischenzeitlich bekannten Wert Window. Übrigens: Hinter ControlText können Sie zu Recht Schwarz als Farbe vermuten. Damit wäre die Implementierung des Labyrinths abgeschlossen. Abgesehen vom übergeordneten Steuerelement WindowsForm, bringt (F5) (Start mit Debugging) bzw. (Strg)+(F5) (Start ohne vorherigen Debug) Sie zu dem, was Abbildung 7.8 zeigt.
7.3.3
Drei Buttons und ein Textfeld
Abbildung 7.8 geht vier Schritte weiter – erster Schritt: ein Button-Control; zweiter und dritter Schritt: dasselbe. Nur im vierten und letzten Schritt ist uns an der Gestaltung eines TextBox-Steuerelements gelegen. Bei jedem der vier Controls müssen Sie im Eigenschaften-Fenster der Entwicklungsumgebung unter der Kategorie Size die Width-Eigenschaft auf 290 festlegen. Was die Height-Eigenschaft anbelangt, fällt textBox1 mit 20 statt 29 für die drei Buttons (button1, button2, button3) aus dem zum Glück nur sprichwörtlichen Rahmen. An einem zweiten Punkt sind TextBox und Button als elementare Controls leider noch nicht auf einer Linie: Während beim Button-Control die TextAlign-Eigenschaft per Default auf Center gesetzt ist, wird im TextBox-Control standardmäßig der Text links ausgerichtet. Ändern Sie die TextAlign-Eigenschaft von Left auf Center, und das »kleine Malheur« ist behoben. Die zeitverzögerte Einblendung der Knoten, die zum kürzesten Weg beitragen, sollte auch dann zu sehen sein, wenn die Size-Eigenschaft (Kategorie Font) unverändert bleibt. Ändern Sie stattdessen unter derselben Kategorie die Eigenschaft Bold auf True. Und wenn Sie schon einmal dabei sind: Rot ist nicht nur die Farbe der Lehrer. Setzen Sie ForeColor auf Red, und die Knoten werden in einem schönen Rot angezeigt.
200
Entwicklung der Programmierlogik
Abbildung 7.8
7.4
Die WindowsForm Labyrinth in der Entwurfsansicht
Entwicklung der Programmierlogik
Die Umsetzung von Dijkstra in C#-Code kann auf mehreren, mehr oder weniger aufwendigen Wegen gelingen. Mit zehn Zeilen werden Sie gleichwohl nicht auskommen (mit zwanzig auch nicht). Unser Weg führt Sie zu 왘
generischen Listen (sogenannten Generics),
왘
Dictionarys (die ebenfalls zu den Generics zählen),
왘
der Verknüpfung generischer Listen mit Dictionarys
sowie zu der hoffentlich interessanten Erfahrung, wie minimalistisch (im Sinne weniger Eigenschaften) eine Klasse angelegt sein kann. Zigmal haben Sie im Laufe der Lektüre dieses Buches auf die eine oder andere Weise (am schnellsten über das Eigenschaften-Fenster des jeweiligen Controls) Eigenschaften mit Werten belegt. Im nächsten Abschnitt werden Sie lernen (wenn Sie es nicht schon können), wie das Ganze vom Standpunkt der Klasse betrachtet und gehandhabt wird.
201
7.4
7
Im Labyrinth des Minotaurus
7.4.1
Gute Eigenschaften
Den Sinn und Zweck von Eigenschaften im Einzelnen zu erläutern ist Aufgabe einschlägiger Theoriebücher zur .NET-Sprache C#, in denen, nicht selten mit beneidenswertem Geschick, genau das und noch mehr geleistet wird. Trotzdem ... Eigenschaften stellen eine ausgezeichnete Möglichkeit dar, um in einer Klasse auf private-Felder lesend und schreibend zuzugreifen, ohne den Umweg über die öffentlichen (public) Methoden gehen zu müssen, die für die notwendige operative Sicherheit sorgen. Eigenschaften reduzieren somit u. a. die Länge des Codes. Zwar nicht beträchtlich, aber immerhin. Eine Eigenschaft setzt sich aus zwei Teilen zusammen: 왘
einem als private deklarierten Datenelement
왘
einem methodenähnlichen Konstrukt, das, mit der Sichtbarkeit public ausgewiesen, aus wiederum zwei Teilen besteht: 왘
einem get-Teil, in dem der Code zum Abfragen (Leseeigenschaft) des privaten Datenelements untergebracht wird
왘
einem set-Teil als Ort der Codierung zum Setzen des privaten Feldes. Hier in der Schreibeigenschaft kann der Wert der privaten Variablen verändert werden.
Unverzichtbar Ohne Eigenschaften gäbe es kein Eigenschaften-Fenster in der Entwicklungsumgebung, und ohne Eigenschaften-Fenster gäbe es viel weniger Komfort im täglichen Programmiergeschäft.
Kommen wie zurück zur Praxis, in der man – vielleicht – am besten lernt. Eine Klasse für elf Knoten Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Stammverzeichnis Labyrinth. Im Kontextmenü wählen Sie Hinzufügen, im Weiteren Neues Element. Im farbig schönen Fenster Neue Elemente/Projektmappenelemente hinzufügen klicken Sie auf C#-Klasse und geben »Knoten.cs« in das Editorfeld Name ein. Hinzufügen müssen Sie die Settings schon, dafür sorgt ein Klick auf den Button Hinzufügen. Öffnen Sie die Datei Knoten.cs, in der zunächst eine private string-Variable – _name – zu vereinbaren wäre: private string _name;
202
Entwicklung der Programmierlogik
Stichwort Konstruktor: Als Parameter erwartet der Konstruktor die Variable name, die ebenfalls vom Typ string sein muss. Im Rumpf des Konstruktors weisen Sie die Variable name dem privaten Feld _name zu: public Knoten(string name) { this._name = name; }
Die im Folgenden definierte Eigenschaft Name bezieht sich auf die eingangs deklarierte private Variable _name: public string Name { get { return this._name; } set { this._name = value;} }
Später kann man über den Namen (Name) der Eigenschaft wie auf ein ganz normales Feld (bislang wurde der Terminus Feld eher stiefmütterlich behandelt – ich fürchte, das wird auch so bleiben) auf die Variable _name lesend und schreibend zugreifen. Zwingend erforderlich ist die Großschreibung beim Namen der Eigenschaft nicht, gleichwohl wird durch die stillschweigende Vereinbarung verdeutlicht, welche Eigenschaft zu welchem Feld gehört. Obligatorisch dagegen ist die grundsätzliche Gleichheit zwischen Eigenschaften- und Variablennamen, eine Gleichheit, die es übrigens auch zwischen dem Typ des privaten Felds und dem Typ der Eigenschaft geben sollte. Stellen Sie sich vor, die Klasse Knoten wäre Teil der mächtigen .NET-FrameworkKlassenbibliothek. Dann gäbe es prinzipiell ein Eigenschaften-Fenster mit einem Eintrag Name, hinter den Sie jeden Buchstaben von einschließlich A bis einschließlich K im Sinne eines schreibenden (set) und lesenden (get) Zugriffs setzen könnten. Ja, liebe Leser, um die Knotennamen geht es, um die Knotennamen im Zusammenspiel mit Dijkstra. Noch war alles Vorarbeit für das »große Finale«. Diese Vorarbeit ist allerdings, zumindest bezogen auf die Klasse Knoten, jetzt abgeschlossen. Das Ergebnis sieht so aus: using System; using System.Collections.Generic;
203
7.4
7
Im Labyrinth des Minotaurus
using System.Linq; using System.Text; namespace Labyrinth { public class Knoten { private string _name; public Knoten(string name) { this._name = name; } public string Name { get { return this._name; } set { this._name = value; } } } } Listing 7.1
Eine Eigenschaft, die genügt – in der Klasse »Knoten«
Eine weitere Klasse erwartet Sie – jene für die Kanten. Eine Klasse für die Kanten Erstellen Sie eine weitere Klasse, genannt Kanten. Wenn Sie die Datei Kanten.cs geöffnet haben, sind drei private Variablen zu deklarieren: private Knoten _startknoten; private Knoten _zielknoten; private int _distanz;
Freuen Sie sich nicht zu früh. Vom Start (A) zum Zielknoten (K) ist die Distanz xyz zurückzulegen? Falsch ist das nicht. Trotzdem geht es in der Klasse Kanten um alle relevanten Distanzen, also beispielsweise auch um den Weg von Knoten G zu Knoten H. Der ist mit einem Kantengewicht von 0 recht kurz (zwischen den beiden Rechtecken liegt kein weiteres Rechteck, was gleichwohl nicht ignoriert werden sollte).
204
Entwicklung der Programmierlogik
Das mit den Variablen _startknoten (vom Typ Knoten), _zielknoten (vom Typ Knoten) und _distanz (ein magerer int-Wert) wäre damit geklärt. Kommen wir zum Konstruktor Kante(): public Kante(Knoten startknoten, Knoten zielknoten, int distanz) { this._startknoten = startknoten; this._zielknoten = zielknoten; this._distanz = distanz; }
Nun definieren Sie drei Eigenschaften, die drei privat deklarierten Feldern (_startknoten, _zielknoten, _distanz) entsprechen. Beginnen wir mit der Eigenschaft Startknoten: public Knoten Startknoten { get { return _startknoten; } set { _startknoten = value; } }
Analog werden die Eigenschaften Zielknoten und Distanz definiert. Und weil das alles nicht sonderlich schwierig ist, wird Ihnen die vollständige Klasse Kanten gleich hier präsentiert: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Labyrinth { class Kante { private Knoten _startknoten; private Knoten _zielknoten; private int _distanz; public Kante(Knoten startknoten, Knoten zielknoten, int distanz) { this._startknoten = startknoten; this._zielknoten = zielknoten; this._distanz = distanz;
205
7.4
7
Im Labyrinth des Minotaurus
} public Knoten Startknoten { get { return _startknoten; } set { _startknoten = value; } } public Knoten Zielknoten { get { return _zielknoten; } set { _zielknoten = value; } } public int Distanz { get { return _distanz; } set { _distanz = value; } } } } Listing 7.2
Zwei Eigenschaften mehr in der Klasse »Kante«
Themenwechsel, denn bislang ist nichts vorhanden, was im Formular Labyrinth für angezeigte, also definierte Knoten sorgt.
7.4.2
Definition der Knoten im EventHandler »button1_Click()«
Klicken Sie in der Klassendatei Labyrinth.cs zweimal hintereinander auf die Schaltfläche Knoten definieren. Die Konsequenz ist Ihnen bekannt: Der Eventhandler button1_Click() wird erstellt. In dem Eventhandler müssen Sie im Wesentlichen nichts anderes tun, als die Text-, TextAligment- sowie die BorderStyle-Eigenschaft der Labels zu verändern, die in Abbildung 7.6 als Knoten definiert sind. Erst ein Buchstabe in Verbindung mit einer dunklen Umrandung identifiziert ein Label als Knoten, wobei der Buchstabe auf des Labels goldener Mitte anzuordnen ist. Nehmen wir die Metamorphose, beispielhaft und nicht unüberlegt, an Knoten B (label5) vor: label5.Text = "B";
206
Entwicklung der Programmierlogik
Dem ist, bis auf die Festlegung der Schriftfarbe über die ForeColor-Eigenschaft, nichts hinzuzufügen: label5.ForeColor = Color.Black;
Beachten Sie dennoch: Bei label36 sollte der Text weiß dargestellt sein. Rötlich auf Rot macht sich schlecht. Die nächste Zeile: label5.TextAlign = ContentAlignment.MiddleCenter;
Die Textausrichtung im Label geschieht über die TextAlign-Eigenschaft der Klasse Label – was nicht weiter verwundert. Die Werte der Eigenschaft allerdings finden sich in der ContentAlignment-Enumeration (Namenraum System.Drawing), die ganz allgemein über ihre Enumerationsmember (neben MiddleCenter existieren acht weitere) die Ausrichtung von Inhalt auf einer Zeichenoberfläche regelt: Nächste Zeile: Label5.BorderStyle = BorderStyle.FixedSingle;
Es riecht nach Enumeration! Noch dazu nach einer, die denselben Namen trägt wie die Eigenschaft. Trotzdem ist BorderStyle nur eine weitere Enumeration (im Namensraum System.Windows.Forms), eine durch die der Rahmen eines Steuerelements zu bestimmen ist. Für die Kennzeichnung eines Labels als Knoten genügt eine einfache Linie, somit das Enumerationsmember FixedSingle. Hier sehen Sie den vollständigen Handler button1_Click(): private void button1_Click(object sender, EventArgs e) { label1.Text = "A"; label1.ForeColor = Color.Black; label1.BorderStyle = BorderStyle.FixedSingle; label5.Text = "B"; label5.ForeColor = Color.Black; label5.BorderStyle = BorderStyle.FixedSingle; label5.TextAlign = ContentAlignment.MiddleCenter; label13.Text = "C"; label13.ForeColor = Color.Black; label13.BorderStyle = BorderStyle.FixedSingle; label13.TextAlign = ContentAlignment.MiddleCenter;
207
7.4
7
Im Labyrinth des Minotaurus
label14.Text = "D"; label14.ForeColor = Color.Black; label14.BorderStyle = BorderStyle.FixedSingle; label14.TextAlign = ContentAlignment.MiddleCenter; label16.Text = "E"; label16.ForeColor = Color.Black; label16.BorderStyle = BorderStyle.FixedSingle; label16.TextAlign = ContentAlignment.MiddleCenter; label17.Text = "F"; label17.ForeColor = Color.Black; label17.BorderStyle = BorderStyle.FixedSingle; label17.TextAlign = ContentAlignment.MiddleCenter; label20.Text = "G"; label20.ForeColor = Color.Black; label20.BorderStyle = BorderStyle.FixedSingle; label20.TextAlign = ContentAlignment.MiddleCenter; label21.Text = "H"; label21.ForeColor = Color.Black; label21.BorderStyle = BorderStyle.FixedSingle; label21.TextAlign = ContentAlignment.MiddleCenter; label22.Text = "I"; label22.ForeColor = Color.Black; label22.BorderStyle = BorderStyle.FixedSingle; label22.TextAlign = ContentAlignment.MiddleCenter; label33.Text = "J"; label33.ForeColor = Color.Black; label33.BorderStyle = BorderStyle.FixedSingle; label33.TextAlign = ContentAlignment.MiddleCenter; label36.Text = "K"; label36.ForeColor = Color.White; label36.BorderStyle = BorderStyle.FixedSingle; } Listing 7.3 Die goldene Mitte – Textfestlegung und Ausrichtung der Labels im EventHandler »button1_Click()«
208
Entwicklung der Programmierlogik
Bestimmt werden Sie es bemerkt haben: Bei label1 (Startknoten A) und label36 (Zielknoten K) findet keine explizite Wertbindung der TextAlign-Eigenschaft statt. Es scheint nur so, denn nur zu gut erinnern Sie sich an Abschnitt 7.3.2, wo die TextAlign-Eigenschaft bereits via Eigenschaften-Fenster vergeben wurde. Was eingeblendet wird, sollte auch wieder ausgeblendet werden können. Davon handelt der nächste Abschnitt.
7.4.3
Ausblenden der Knoten im EventHandler »button2_Click()«
Zu Beginn gilt auch hier: Erstellen Sie den Eventhandler button2_Click(). In der Routine geht es um exakt dieselben Labels, deren Text-, BorderStyle- und TextAlign-Eigenschaften im Handler button1_Click() an die Anforderung definierter Knoten angepasst wurden (siehe Abschnitt 7.4.2). Jetzt läuft das ganze »Spiel« rückwärts ab – mit einer Ausnahme: TextAlign. In Abschnitt 7.3.2 haben wir über das Eigenschaften-Fenster für label1 und label36 die Eigenschaft bereits auf MiddleCenter gesetzt, woran in der Routine button1_Click() nicht gerüttelt wurde. Allen anderen als Knoten definierten Labels wird über die Text-Eigenschaft ein Leerstring (««) zugewiesen. Damit hätte sich die Behandlung der TextAlign-Eigenschaft erübrigt. Wenn auf einem Label nichts weiter zu lesen ist – was sollte ausgerichtet oder positioniert werden? Anders sieht das im Falle des Rahmens aus (BorderStyle), den es im Urzustand des Labyrinths bei keinem der 11 am Knotengeschehen beteiligten Labels geben darf. Der Enumeration BorderStyle sind Sie bereits begegnet, nicht jedoch dem Enumerationsmember None. Dieses wird zur Beseitigung der Rahmen benötigt. Auch sollte dafür gesorgt werden, dass Start (label1) und Ziel (label36) auf die ursprünglichen Farben, also Schwarz und Weiß, zurückgesetzt werden. An der entsprechenden Belegung der ForeColor-Eigenschaft werden Sie demnach nicht vorbeikommen. Freuen Sie sich! Das war es nämlich bereits. Ein Blick auf den Code: private void button2_Click(object sender, EventArgs e) { label1.Text = "Start"; label1.ForeColor = Color.Black; label1.BorderStyle = BorderStyle.None; label5.Text = ""; label5.BorderStyle
= BorderStyle.None;
209
7.4
7
Im Labyrinth des Minotaurus
label13.Text = ""; label13.BorderStyle
= BorderStyle.None;
label14.Text = ""; label14.BorderStyle
= BorderStyle.None;
label16.Text = ""; label16.BorderStyle
= BorderStyle.None;
label17.Text = ""; label17.BorderStyle
= BorderStyle.None;
label20.Text = ""; label20.BorderStyle
= BorderStyle.None;
label21.Text = ""; label21.BorderStyle
= BorderStyle.None;
label22.Text = ""; label22.BorderStyle
= BorderStyle.None;
label33.Text = ""; label33.BorderStyle
= BorderStyle.None;
label36.Text = "Ziel"; label36.ForeColor = Color.White; label36.BorderStyle = BorderStyle.None; } Listing 7.4 Click()«
Als wäre nichts gewesen – Ausblenden der Knoten im EventHandler »button2_
Testen Sie die Anwendung auf eine von zwei bekannten Weisen. button2_ Click() ist allerdings so wenig fehlerträchtig, dass Sie sich ein Debugging sparen und gleich die Taste (F5) drücken können. Auch wenn noch nichts animiert ist, sind Knoten, die ein- und ausgeblendet werden können, besser als nichts.
7.4.4
Die Schaltfläche zum kürzesten Weg
Die Logik hinter dem Button Berechnung des kürzesten Weges wird im Wesentlichen von mächtigen Listen dominiert, in denen es nicht minder bewegt zugeht wie im Labyrinth selbst. Elemente werden eingefügt, Elemente werden entnommen, auf Elemente wird zugegriffen.
210
Entwicklung der Programmierlogik
Gleichwohl ist Dijkstra noch einige Seiten entfernt. Und obgleich der Algorithmus vernünftigerweise in einer eigenen Klasse (Dijkstra) residiert, wird hier, im Rumpf des Eventhandlers button3_Click() (den Sie sicher erstellt haben ...) fleißig Vorarbeit geleistet – bis zu dem Punkt, an dem es gilt, die Dijkstra-Klasse zu instanziieren. Dann fügt sich das eine ins andere, denn zwei von drei der Listen, die Sie im Folgenden implementieren, werden dann dem Konstruktor der Klasse Dijkstra als Parameter übergeben. Das zur Einstimmung! Mehr als digitale Wörterbücher – Dictionarys Theoretisch könnte sich die Erklärung zur Dictionary(TKey, TValue)-Klasse auf die Information beschränken, dass diese für die Zuordnung von einem Satz von Schlüsseln zu einem Satz von Werten sorgt. Punkt. Genüge getan wäre der interessanten und im wahrsten Sinne des Wortes eigenartigen Klasse damit aus gleich mehreren Gründen nicht. Dictionary(TKey, TValue) (die im Namensraum System.Collections.Generic
zu finden ist) ist nämlich etwas Besonderes. Sie ist auch deswegen besonders, weil über den Schlüssel TKey der Wert (TValue) beinahe in O(1)-Geschwindigkeit abgerufen werden kann (genauer gesagt wird von einer O(1)-Operation gesprochen). Was immer das auch genau ist, es ist nicht langsam. Die Geschwindigkeit hat ihren Grund. Gleichwohl ist sie nicht konstant und von der Qualität des Hashalgorithmus des Typs abhängig, der einige Textzeilen weiter für TKey angegeben werden wird. Vorweg: Den Zuschlag bekommt der Typ string. Übrigens: Die gesamte Klasse Dictionary(TKey, TValue) ist in Form einer Hashtabelle implementiert – das ist der Grund für die Geschwindigkeit. Schneller Exkurs – Hashverfahren Das Hashverfahren geht von der pfiffigen Idee aus, die Position eines Objekts in einer Tabelle durch schiere Berechnung zu ermitteln. Darauf zielt auch der verwendete Algorithmus ab, der naheliegenderweise als Hashalgorithmus bezeichnet wird. So können Sie sich das Durchsuchen der gesamten Tabelle nach dem verlangten Objekt sparen. Das Verfahren gipfelt in der Verwendung sogenannter Hashtabellen (englisch Hash Tables oder Hash Maps) die, primär für das Handling größerer Datenmengen gedacht, das Auffinden einzelner Elemente mithilfe einer speziellen Indexstruktur ermöglicht Hashtabellen werden auch als Streuwerttabellen bezeichnet. Das ist ein gut klingender Begriff, den sich der Interessierte merken sollte.
Der Gebrauch eines Dictionarys legt die Erzeugung eines Objekts vom Typ Dictionary(TKey, TValue) nahe. Besonderheit hin, Besonderheit her – die Objekt-
211
7.4
7
Im Labyrinth des Minotaurus
erzeugung geschieht auf klassischem Wege, ganz gewöhnlich über den Konstruktor der Klasse: Dictionary<string, Label> _textboxItems = new Dictionary<string, Label>();
Label? Ja, im Dictionary _textboxItems ist TValue vom Typ Label, und Tkey ist (wie bereits erwähnt wurde) vom Typ string. Wozu das Ganze? Sie müssen nur noch wenige Abschnitte lesen, bis Ihnen Dijkstra eine Buchstabenfolge liefert – den durch Knoten ausgedrückten, kürzesten Weg zwischen Start (Knoten A) und Ziel (Knoten K). Sind die Knoten ermittelt, sind es die zugehörigen Labels noch lange nicht. Das ist Grund genug, sich eines Mechanismus zu bedienen, der elf Knoten (aus denen x Knoten für den kürzesten Weg herausgerechnet werden) mit elf Labels verknüpft. Noch ist das Dictionary _textboxItems ein »leeres Objekt«. Dieser Zustand ist auf einfachste Weise, nämlich über die Klassenmethode Add(), zu ändern. Add() erwartet zweierlei: einen Schlüssel (Knoten) und einen Wert (Label). Ein Beispiel (Startknoten A) gebe ich Ihnen: _textboxItems.Add("A", label1);
So schnell kann es gehen! Hier sehen Sie noch einmal das gesamte Dictionary, von der Erzeugung des Objekts _textboxItems bis zum Befüllen mit ... wie vielen Wert-Schlüssel-Paaren (TKey, TValue)? Dictionary<string, Label> _textboxItems = new Dictionary<string, Label>(); _textboxItems.Add("A", _textboxItems.Add("B", _textboxItems.Add("C", _textboxItems.Add("D", _textboxItems.Add("E", _textboxItems.Add("F", _textboxItems.Add("G", _textboxItems.Add("H", _textboxItems.Add("I", _textboxItems.Add("J", _textboxItems.Add("K", Listing 7.5
212
label1); label5); label13); label14); label16); label17); label20); label21); label22); label33); label36);
Paarweise – Knoten und zugehörige Labels im Dictionary »_textboxItems«
Entwicklung der Programmierlogik
An Dictionarys erstaunt auch, was in denen so alles als Wert (TValue) durchgeht: zum Beispiel ein Klassenobjekt – hier ein Objekt der Klasse Knoten. Was die Schlüssel anbelangt, die sind auch im zweiten Dictionary vom Typ string. Erzeugen wir ein Dictionary(TKey, TValue)-Objekt mit dem Namen _dictKnoten: Dictionary<string, Knoten> _dictKnoten = new Dictionary<string, Knoten>();
Auch hier werden 11 Wert-Schlüssel-Paare benötigt: 11 Schlüssel, die aus den Buchstaben der als Knoten definierten Labels bestehen, sowie 11 Knoten-Objekterzeugungen. Dabei ist dem Konstruktor der Wert des Schlüssels TKey zu übergeben. _dictKnoten.Add("A", new Knoten("A"));
Die Endfassung des Dictionarys _dictKnoten sieht so aus: Dictionary<string, Knoten> _dictKnoten = new Dictionary<string, Knoten>(); _dictKnoten.Add("A", _dictKnoten.Add("B", _dictKnoten.Add("C", _dictKnoten.Add("D", _dictKnoten.Add("E", _dictKnoten.Add("F", _dictKnoten.Add("G", _dictKnoten.Add("H", _dictKnoten.Add("I", _dictKnoten.Add("J", _dictKnoten.Add("K", Listing 7.6
new new new new new new new new new new new
Knoten("A")); Knoten("B")); Knoten("C")); Knoten("D")); Knoten("E")); Knoten("F")); Knoten("G")); Knoten("H")); Knoten("I")); Knoten("J")); Knoten("K"));
Jedem Knoten sein Knoten-Objekt – das Dictionary »_dictKnoten«
Behalten Sie das Dictionary _dictKnoten im Auge und _textboxItems im Hinterkopf. Quadrate zählen für die Klasse »List(T)« Mit den aufgelisteten Wert-Schlüssel-Paaren können Sie ein Dictionary als Spezialfall einer generischen Liste betrachten. Kommen wir zum »generischen Normalfall«, zur Klasse List(T), deren Zuhause ebenfalls der Namensraum System.Collections.Generic ist.
213
7.4
7
Im Labyrinth des Minotaurus
Auch List(T) typisiert Objekte recht stark. Auf diese Objekte kann schließlich über einen Index zugegriffen werden. Darüber hinaus stellt die List(T)-Klasse ein Sammelsurium an Methoden zum Sortieren, Durchsuchen und Bearbeiten der Liste bereit. Hier brauchen wir nur eine, nämlich Add(), die ohne ein Objekt der zugehörigen Klasse »wie bestellt und nicht abgeholt« im Namensraum steht. Im Gegensatz zur Add()-Methode der Dictionary(TKey, TValue)-Klasse, die zwei Parameter erwartet, begnügt sich Add() »Numero zwei« mit einem: dem am Ende der Liste anzufügenden Objekt, das in unserem Falle vom Typ Kante ist, also von jener in Abschnitt 7.4.1 erstellten, benutzerdefinierten Klasse. So erzeugen Sie das Objekt vom Typ List(): List _kanten = new List();
Der Objektname _kanten sagt, um was es geht: Kanten. Abschnitt 7.2 stellte die diesbezüglichen Verhältnisse erst auf den Kopf und stellte anschließend klar, was das heißt: Wie viele Quadrate auch immer zwischen zwei als Knoten definierten Quadraten liegen, für uns drückt sich in der Anzahl die Gewichtung der Kanten aus, bis hin zum – sinnigen oder unsinnigen – Nullgewicht, bei dem zwischen zwei sozusagen quadrierten Knoten keine weiteren Quadrate existieren (siehe Abschnitt 7.2.2 und Abbildung 7.6). Hinweis Von hier an bis zum Ende des Projekts werden die bislang entwickelten Codefragmente wie die Teile eines Puzzles zusammengefügt. Besser noch ließe sich an ein Getriebe denken, in dem bewegte Zahnräder funktional ineinandergreifen. – Nein, ich schneide weder auf noch versuche ich, Zeilen zu schinden. Die augenzwinkernde Warnung hat ihre Gründe. Doch lesen Sie selbst.
Betrachten Sie nachstehende Zeile – doch tun Sie das bitte nicht »einfach so«. Hinter dem komplexen Einzeiler verbirgt sich nämlich mehr: _kanten.Add(new Kante(_dictKnoten["A"], _dictKnoten["B"], 3));
Es geht hier nicht um die Methode Add(), mit der die Liste _kanten gefüllt wird. Die ist ein alter Hut. Es geht um den Parameter der Routine, um die Erzeugung eines Objekts vom Typ Kante, jener Klasse, deren Konstruktor Kante() dreierlei erwartet: 왘
zwei Objekte vom Typ Knoten
왘
ein Objekt vom Typ int für die zwischen einem beliebigen Start- und einem beliebigen Zielknoten verlaufende Kante
214
Entwicklung der Programmierlogik
Der Wert des dritten Parameters (Kantengewicht) für die exemplarische Kante zwischen Knoten A und B beträgt 3; es sei denn, Sie zählen zwischen A und B mehr als drei Quadrate, wovon nicht auszugehen ist. Leider hält im ersten Aufruf der Add()-Methode (da es elf mögliche Kanten gibt, werden zehn weitere folgen) der Konstruktor Kante() für die ersten beiden Parameter (die Knoten-Objekte für den Start- und den Zielknoten) zwei unangenehme Überraschungen bereit: _dictKnoten["A"] _dictKnoten["B"]
Unser Dictionary _dictKnoten ist als array-ähnliches Etwas »verkleidet«. Grundsätzlich falsch ist die Analogie nicht. Denn genauso, wie Sie über den Ausdruck meinArray[2];
Zugriff auf das dritte (!) Element im Array meinArray (von welchem Typ auch immer das Array ist) erhalten, ist mit dem Ausdruck _dictKnoten["A"];
auf den Wert zuzugreifen, der im Dictionary _dictKnoten mit dem Schlüssel A assoziiert ist. Und das wäre: new Knoten("A")
Damit wird dem Konstruktor Knoten() mit _dictKnoten["A"] und _dictKnoten["B"] (nicht zu vergessen der int-Wert 3 als Kantengewicht zwischen den Knoten A und B) genau das übergeben, was per definitionem (siehe Abschnitt 7.4.1) übergeben werden muss: zwei Knoten-Objekte. Vollständig implementiert, sieht die generische Liste _kanten folgendermaßen aus: List _kanten = new List(); _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new _kanten.Add(new Listing 7.7
Kante(_dictKnoten["A"], Kante(_dictKnoten["A"], Kante(_dictKnoten["B"], Kante(_dictKnoten["C"], Kante(_dictKnoten["D"], Kante(_dictKnoten["E"], Kante(_dictKnoten["F"], Kante(_dictKnoten["G"], Kante(_dictKnoten["H"], Kante(_dictKnoten["I"], Kante(_dictKnoten["J"],
_dictKnoten["B"], _dictKnoten["C"], _dictKnoten["F"], _dictKnoten["D"], _dictKnoten["G"], _dictKnoten["I"], _dictKnoten["E"], _dictKnoten["H"], _dictKnoten["J"], _dictKnoten["H"], _dictKnoten["K"],
3)); 1)); 1)); 0)); 0)); 0)); 0)); 0)); 1)); 0)); 2));
Beinahe wie gehabt – das Befüllen der generischen Liste »_kanten«
215
7.4
7
Im Labyrinth des Minotaurus
Eine Liste fehlt uns noch, denn wo Kanten aufgelistet werden, sind Knoten nicht weit. Es geht auch anders Zunächst: Erzeugen Sie eine Liste _knoten, deren Elemente aus Objekten vom Typ Knoten bestehen: List _knoten = new List();
Knoten existieren bereits – im Dictionary _dictKnoten. Entscheidend ist, was wir aus dem Wörterbuch benötigen: die Ausdrücke zum Erzeugen der Knoten-Objekte nämlich, oder anderes ausgedrückt: die Werte (TValue). Welche Schlüssel zu den elf Werten gehören, interessiert uns zur Abwechslung nicht. Über die Values-Eigenschaft der Dictionary(TKey, TValue)-Klasse erfolgt der Abruf sämtlicher im Dictionary enthaltener Werte, was ohne den Einsatz einer foreach-Anweisung schwierig wird. Vor allem dann, wenn mit den Werten (den Knoten-Objekten) etwas geschehen soll. Durch die folgende foreach-Anweisung werden elf Knoten-Objekte dem Wörterbuch _dictKnoten entnommen und über die Add()-Methode in die Liste _knoten eingefügt: foreach (Knoten n in _dictKnoten.Values) { _knoten.Add(n); }
Hier sehen Sie das Minimalprogramm komplett: List _knoten = new List(); foreach (Knoten n in _dictKnoten.Values) { _knoten.Add(n); } Listing 7.8
Ein »Zeilensparprogramm« – »foreach« dient zum Füllen der Liste »_knoten«.
Mehr können wir im Augenblick für die Klasse Labyrinth nicht tun. Die Klasse »Dijkstra« Das Grundgerüst für die Klasse Dijkstra ist auf bekanntem Wege schnell erstellt. Alternativ können Sie in dem schicken Entwicklungswerkzeug Visual C# 2010
216
Entwicklung der Programmierlogik
Express Edition auch über das Hauptmenü mit Projekt 폷 Klasse hinzufügen zum objektorientierten Vorerzeugnis gelangen. Auch Bekanntes wird in der Dijkstra-Klasse zu realisieren sein, unter anderem Dictionarys, Listen und Eigenschaften, wobei Ihnen klar sein sollte, worauf es letzten Endes ankommt: auf das Zusammenspiel der Klassen(-module) nämlich. Bei welchem objektorientierten Programm ist das nicht der Fall? Bestellte Felder
Starten wir das Befüllen des Klassenrumpfes mit der Deklaration eines – natürlich – privaten Feldes vom Typ List, mithin mit einer generischen Liste, deren Objekte vom Typ Knoten sind oder sein sollen: private List _knoten;
Die nächste Aktion ist nicht viel anders: Deklarieren Sie eine private Variable _kanten vom Typ einer Liste, deren Objekte vom Typ Kante sind: private List _kanten;
Auch in die folgende private Variable hält ein Generic Einzug, dessen Objekte wieder vom Typ Knoten sind. Der Name der Variablen ist _rest. Nach Abschnitt 7.1.3 taucht das Wort – kleingeschrieben und mit einem Underscore versehen – zum zweiten Mal in diesem Kapitel auf. Ob sich der Kreis langsam schließt? Schauen Sie: private List _rest;
In der »Abteilung Wörterbuch« findet sich zunächst _distanz: private Dictionary<string, double> _distanz;
Privat deklariert, bietet die Variable Platz für ein Dictionary, dessen Schlüssel (TKey) vom Typ string, die assoziierten Werte dagegen vom Typ double sind. Ähnlich gelagert ist der Fall des Dictionarys _opt. Der Unterschied besteht im Typ der Werte (TValue): Hier ist das der Typ Knoten. Es ließe sich auch schreiben Opt Knoten; was die Deklaration womöglich plausibler macht: private Dictionary<string, Knoten> _opt;
Es nimmt kein Ende – Kanten und Knoten im Konstruktor
Bereits im Konstruktor der Klasse Dijkstra gelingt uns der Schulterschluss mit der Name-Eigenschaft der Klasse Knoten. Zunächst jedoch gibt es in Dijkstra() noch anderes zu erledigen. Zum Beispiel:
217
7.4
7
Im Labyrinth des Minotaurus
this.Kanten = kanten; this.Knoten = knoten;
Im Weiteren wird eine leere Instanz (Rest) der List-Klasse erzeugt, wobei die Elemente der Liste vom Typ Knoten sein sollen (ja, irgendwann hat es mit Knoten und Kanten doch ein Ende ...): Rest = new List();
Anschließend erfolgt die Erzeugung eines Dictionarys Opt; TKey ist vom Typ string, und die Werte (TValue) sollen Knoten-Objekte sein: Opt = new Dictionary<string, Knoten>();
Eine letzte Liste (Distanz), gleichfalls realisiert als Dictionary, wird generiert: Distanz = new Dictionary<string, double>();
Gerne darf Sie das Kommende an den Abschnitt »Es geht auch anders« erinnern, wo am Ende die Liste _knoten unter Einsatz der foreach-Anweisung (und natürlich der Add()-Methode) mit Inhalten des Dictionarys _dictKnoten gefüllt wurde, wobei es nur um die Werte, sprich um das Erzeugen der Knoten-Objekte ging. Nichts anderes hat uns seinerzeit interessiert. Als einer von zwei dem Konstruktor Dijkstra() übergebenen Parameter liegt die Liste _knoten so geschrieben (bislang wurde noch kein Objekt der Klasse Dijkstra erzeugt) zwischenzeitlich vor. Das ist gut für die – noch – leeren Listen Rest, Opt und Distanz, existiert doch ein Reservoir, um die Leere zu füllen. Genau das wird hiermit getan: foreach (Knoten n in Knoten) { Opt.Add(n.Name, null); Rest.Add(n); Distanz.Add(n.Name, double.MaxValue); }
Sehen wir uns zunächst den ersten Ausdruck im Block der foreach-Anweisung an: Opt.Add(n.Name, null);
Zweierlei ist wichtig: 왘
218
Während die Liste Knoten ursprünglich Objekterzeugungen der Klasse Knoten enthält, bestehen die Schlüssel des Dictionarys Opt aus dem, was die NameEigenschaft der Klasse Knoten (Lesezugriff) liefert: nämlich aus den Buchsta-
Entwicklung der Programmierlogik
ben, die einen Knoten repräsentieren – zum Beispiel A, von dem ausgehend wir das Abenteuer Irrgarten wagen. Damit erschöpft sich unser Interesse, das heißt, dem Wörterbuch Opt werden als Werte (TValue) lediglich so viele null-Verweise übergeben, wie es Knoten gibt.
왘
Zum zweiten Ausdruck: Rest.Add(n);
Was passiert? Schnodderig formuliert, »wandert« der Inhalt der Liste Knoten in die Liste Rest. Mehr passiert hier nicht. Kommen wir zum dritten und letzten Ausdruck: Distanz.Add(n.Name, double.MaxValue);
Was passiert? Die Schlüssel betreffend passiert nichts anderes als das, was im Wörterbuch Opt geschieht. Was allerdings die Werte betrifft, so wird diesmal eine obere Schranke übergeben, wie sie durch das MaxValue-Feld der Struktur double gegeben ist. public Dijkstra(List kanten, List knoten) { this.Kanten = kanten; this.Knoten = knoten; Rest = new List(); Opt = new Dictionary<string, Knoten>(); Distanz = new Dictionary<string, double>(); foreach (Knoten n in Knoten) { Opt.Add(n.Name, null); Rest.Add(n); Distanz.Add(n.Name, double.MaxValue); } } Listing 7.9 »Dijkstra«
Ein kleiner Vorgeschmack auf das Kommende – der Konstruktor der Klasse
Als Erholung gedacht – Definition der Eigenschaften
Wie und zu welchem Zweck Eigenschaften definiert werden, konnten Sie in Abschnitt 7.4.1 lesen. Während wir mitten in der Implementierung der Klasse Dijkstra stecken, stellt sich uns die Frage, welche der eingangs als private dekla-
219
7.4
7
Im Labyrinth des Minotaurus
rierten Felder mit einer Eigenschaft verknüpft werden sollen. Die Antwort: alle. Das heißt: _knoten, _kanten, _rest, _distanz und _opt. Als ein Beispiel für die generische Liste _knoten wird Ihnen hiermit die korrespondierende Eigenschaft präsentiert: public List Knoten { get { return _knoten; } set { _knoten = value; } }
Auch hier bin ich mir sicher und gewiss, dass Sie die Definition der vier verbleibenden Eigenschaften ohne meine Hilfe bewerkstelligen werden. Das Prinzip ist immer dasselbe. Auf Distanz gegangen – die Methode »erhalteKnotenMitGeringsterDistanz()«
Wir werden nun vier Methoden entwickeln, deren sperrig-hölzerne Namen zugleich und im wörtlichen Sinne Programm sind: 왘
erhalteKnotenMitGeringsterDistanz()
왘
erhalteNachbarn()
왘
erhalteDistanzZwischen()
왘
berechneKnoten()
Entscheidend ist die Methode berechneKnoten(), die eine Liste mit jenen Knoten zurückliefert, die den kürzesten Weg bilden. Entscheidend wiederum am Verbund der vier Methoden ist genau diese Reihenfolge, denn ohne die ersten drei Methoden, die in berechneKnoten() aufgerufen werden, würde höchstens unser Frust berechnet. Ferner bitte ich Sie, mir zu glauben: Die Reihenfolge der Methoden wurde nicht willkürlich gewählt. Einleitend erfolgt die Berechnung des Knotens mit der geringsten Distanz, der Einzug in die Variable kleinsteDistanz hält, die vom Typ Knoten ist. Genau jener Knoten mit der kleinsten Distanz wird methodenseitig zurückgegeben. Schauen Sie sich die Methode an, die nichts enthält, was im Projekt Labyrinth nicht schon angesprochen worden wäre – bis vielleicht auf diese Zeile: Knoten kleinsteDistanz = null;
Lokale Variablen dürfen im Sinne der Zuweisung nicht leer ausgehen, auch dann nicht, wenn die Variable auf einen Referenztyp »abzielt«. Dann hilft nur ein Nullverweis. Bei einem Werttyp (zum Beispiel int) sollten Sie das gleichwohl nicht versuchen!
220
Entwicklung der Programmierlogik
public Knoten erhalteKnotenMitGeringsterDistanz() { double distanz = double.MaxValue; Knoten kleinsteDistanz = null; foreach (Knoten n in Rest) { if (Distanz[n.Name] < distanz) { distanz = Distanz[n.Name]; kleinsteDistanz = n; } } return kleinsteDistanz; } Listing 7.10
Die Methode »erhalteKnotenMitGeringsterDistanz()«
Im folgenden Abschnitt sehen wir uns die zweite Methode an. »Auf gute Nachbarschaft« – die Methode »erhalteNachbarn()«
Für Dijkstra ist es nicht unbedeutend zu wissen, welche Knoten in unmittelbarer Nachbarschaft des Knotens liegen, der im letzten Kapitel als der Knoten mit der geringsten Distanz ermittelt wurde. Wen wundert es da, wenn die Methode erhalteNachbarn() in der finalen Methode berechneKnoten() mit dem aufgerufen wird, was die Routine erhalteKnotenMitGeringsterDistanz() liefert: public List erhalteNachbarn(Knoten n) { List nachbarn = new List(); foreach (Kante e in Kanten) { if (e.Startknoten.Equals(n) && Rest.Contains(n)) { nachbarn.Add(e.Zielknoten); } } return nachbarn; } Listing 7.11
Die Methode »erhalteNachbarn()«
221
7.4
7
Im Labyrinth des Minotaurus
Zwischen zwei Knoten – die Methode »erhalteDistanzZwischen()«
Was die Methode erhalteDistanzZwischen() tut, sagt der Name in wirklich eindeutiger Weise: Distanzen berechnen, und zwar von einem Startknoten zum Zielknoten. Gleichfalls aufgerufen in der Methode berechneKnoten(), werden als Parameter kleinsteDistanz sowie die Nachbarknoten übergeben. public double erhalteDistanzZwischen(Knoten o, Knoten d) { foreach (Kante e in Kanten) { if (e.Startknoten.Equals(o) && e.Zielknoten.Equals(d)) { return e.Distanz; } } return 0; } Listing 7.12
Die Methode »erhalteDistanzZwischen()«
Kommen wir nun zur letzten und wichtigsten Methode – dann haben Sie es geschafft. Auf kurzen Wegen unterwegs – die Methode »berechneKnoten()«
Betrachten Sie zunächst die Methode berechneKnoten() in Gänze: public List berechneKnoten(Knoten start, Knoten ziel) { List pfad = new List(); Distanz[start.Name] = 0; while (Rest.Count > 0) { Knoten w = erhalteKnotenMitGeringsterDistanz(); foreach (Knoten v in erhalteNachbarn(w)) { double alt = Distanz[w.Name] + erhalteDistanzZwischen(w, v); if (alt < Distanz[v.Name]) {
222
Entwicklung der Programmierlogik
Distanz[v.Name] = alt; Opt[v.Name] = w; } } Rest.Remove(w); } pfad.Insert(0, ziel); while (Opt[ziel.Name] != null) { ziel = Opt[ziel.Name]; pfad.Insert(0, ziel); } return pfad; } Listing 7.13
Die Methode »berechneKnoten()«
Was passiert mit dem in start gespeicherten Startknoten A, was mit dem Zielknoten K (Parameter ziel)? Weiter geht es für start im Rumpf der ersten while-Schleife, in der die drei zuvor implementierten Methoden aufgerufen werden – und zwar mehr als einmal. Das Zauberwort heißt Iteration, und ohne Iteration können Sie Dijkstra allein deshalb nicht umsetzen, weil sich die Frage nach Nachbarknoten und kürzesten Wegen bei jedem Knoten neu stellt, der den kürzesten Weg zum Vorgänger (ausgehend vom Startknoten A) hat. Das ist wichtig, um die Routine berechne Knoten() zu verstehen. Was in der ersten while-Anweisung geschieht, ist prinzipiell nichts anderes als das, was wir in Abschnitt 7.1.3 für den absolut simplen Fall des Graphen aus Abbildung 7.4 durchgespielt haben: die sukzessive Entleerung der Liste Rest (Rest.Remove(w), mit Remove() als elementausschließender Methode der List-Klasse) zugunsten der Liste Opt. Die Liste Opt ist für uns relevant, auch wenn es noch Ungetanes gibt, was Aufgabe der zweiten while-Schleife ist! Zunächst wird über die Insert()-Methode der List-Klasse (von der zu Beginn der Methode berechneKnoten() eine Instanz namens pfad erstellt wurde) der Zielknoten K an der Indexposition 0 eingefügt: pfad.Insert(0, ziel);
Jetzt geht es los, wir haben sozusagen ein Ziel. Solange nämlich die Bedingung
223
7.4
7
Im Labyrinth des Minotaurus
Opt[ziel.Name] != null
erfüllt ist (und ist sie erfüllt, sind alle im Wörterbuch Opt enthaltenen WertSchlüssel-Paare verarbeitet), erfolgt die Zuweisung des mit Opt[ziel.Name] assoziierten Werts an die Variable ziel, die wiederum abermals an der Indexposition 0 der Liste pfad ihren Platz findet. So baut sich die Liste pfad auf. Hier ist die vollständige Klasse Dijkstra in der »Draufsicht«: class Dijkstra { private List _knoten; private List _kanten; private List _rest; private Dictionary<string, double> _distanz; private Dictionary<string, Knoten> _opt; public Dijkstra(List kanten, List knoten) { this.Kanten = kanten; this.Knoten = knoten; Rest = new List(); Opt = new Dictionary<string, Knoten>(); Distanz = new Dictionary<string, double>(); foreach (Knoten n in Knoten) { Opt.Add(n.Name, null); Rest.Add(n); Distanz.Add(n.Name, double.MaxValue); } } public List Knoten { get { return _knoten; } set { _knoten = value; } } public List Kanten { get { return _kanten; } set { _kanten = value; } }
224
Entwicklung der Programmierlogik
public List Rest { get { return _rest; } set { _rest = value; } } public Dictionary<string, double> Distanz { get { return _distanz; } set { _distanz = value; } } public Dictionary<string, Knoten> Opt { get { return _opt; } set { _opt = value; } } public Knoten erhalteKnotenMitGeringsterDistanz() { double distanz = double.MaxValue; Knoten kleinsteDistanz = null; foreach (Knoten n in Rest) { if (Distanz[n.Name] < distanz) { distanz = Distanz[n.Name]; kleinsteDistanz = n; } } return kleinsteDistanz; } public List erhalteNachbarn(Knoten n) { List nachbarn = new List(); foreach (Kante e in Kanten) { if (e.Startknoten.Equals(n) && Rest.Contains(n)) { nachbarn.Add(e.Zielknoten); }
225
7.4
7
Im Labyrinth des Minotaurus
} return nachbarn; } public double erhalteDistanzZwischen(Knoten o, Knoten d) { foreach (Kante e in Kanten) { if (e.Startknoten.Equals(o) && e.Zielknoten.Equals(d)) { return e.Distanz; } } return 0; } public List berechneKnoten(Knoten start, Knoten ziel) { List pfad = new List(); Distanz[start.Name] = 0; while (Rest.Count > 0) { Knoten w = erhalteKnotenMitGeringsterDistanz(); foreach (Knoten v in erhalteNachbarn(w)) { double alt = Distanz[w.Name] + erhalteDistanzZwischen(w, v); if (alt < Distanz[v.Name]) { Distanz[v.Name] = alt; Opt[v.Name] = w; } } Rest.Remove(w); } pfad.Insert(0, ziel);
226
Entwicklung der Programmierlogik
while (Opt[ziel.Name] != null) { ziel = Opt[ziel.Name]; pfad.Insert(0, ziel); } return pfad; } } Listing 7.14
Hoffentlich kein gordischer Knoten – die Klasse »Dijkstra«
Dijkstra in Aktion Kehren wir zur Klassendatei Labyrinth.cs zurück, in der wir bis zum finalen Test der Anwendung Labyrinth verbleiben werden. Im Eventhandler Button3_ Click() instanziieren wir unter Einbeziehung der Listen _kanten und _knoten die mühsam erstellte Klasse Dijkstra: Dijkstra algo = new Dijkstra(_kanten, _knoten);
Hier, im Eventhandler des Buttons Berechnung des kürzesten Weges, ist die Methode berechneKnoten() von Belang. Aufgerufen wird die Routine mit _dict Knoten["A"],_dictKnoten["K"], Ausdrücke hinter denen sich – noch einmal – nichts anderes als Objekterzeugungen der Klasse Knoten verbergen. Was die Routine liefert, weisen wir der Variablen pfad vom Typ List zu: List pfad = algo.berechneKnoten(_dictKnoten["A"], _dictKnoten["K"]);
Das, was die Liste pfad beinhaltet, ist der Weg zum Ziel, genauer gesagt zum Zielknoten K, ausgedrückt durch die relevanten Knoten, die zunächst als Objekte bzw. deren Erzeugung vorliegen. Das nützt uns nichts. Werfen Sie einen Blick auf den folgenden Code: foreach (Knoten n in pfad) { textBox1.Text = n.Name; delay(1000); _textboxItems[n.Name].Text = n.Name; _textboxItems[n.Name].ForeColor = Color.Red; }
227
7.4
7
Im Labyrinth des Minotaurus
Ihnen dürfte Folgendes klar sein: Die Elemente der Liste pfad müssen ausgelesen werden, was beinahe zwangsläufig zum erneuten Einsatz der foreach-Anweisung führt, in der es um n vom Typ Knoten geht. Ihnen ist das nicht neu. Klar ist Ihnen auch, dass zweierlei bedient werden muss: 왘
das TextBox-Control textBox1 als der Ort, an dem die relevanten Knoten zeitverzögert angezeigt werden
왘
die als Knoten definierten Labels, von denen freilich nur jene relevant sind, die zum kürzesten Weg beitragen.
Machen wir den schweren Sack zu. Die Text-Eigenschaft der Klasse TextBox enthält als Wert das, was unser lesender Zugriff auf die Name-Eigenschaft von n liefert: textBox1.Text = n.Name;
Es folgt eine kurze Pause: delay(1000);
Und ein Hinweis: »Wiederholungstäter« Sowohl in Kapitel 5, »Mit Argusaugen – der nächtliche Sternenhimmel«, als auch bei unserem Besuch an der Börse (Kapitel 6, »Garantiert ungefährlich – Manipulationen am DAX«) wurde die Methode Delay() vorgestellt. Mindestens eines der Projekte werden Sie sicher durchgearbeitet haben, weswegen ich mir abermalige Erörterungen zur Delay()-Methode spare. Einverstanden?
Kommen wir zu den Labels. Irgendwann habe ich Sie gebeten, das Wörterbuch _textboxItems im Hinterkopf zu behalten. Das brauchen wir jetzt – zum Beispiel im folgenden Zusammenhang: _textboxItems[n.Name].Text = n.Name;
Gleich zweimal wird lesend (!) auf die Name-Eigenschaft von n (Typ Knoten) zugegriffen: 왘
linksseitig in Verbindung mit dem Dictionary _textboxItems. Dort fungiert n.Name als Schlüssel zum Aufruf der verknüpften Werte. Das sind unsere »Knoten-Labels«.
왘
Die Text-Eigenschaft der relevanten Labels wird mit dem bewertet, was der Lesezugriff auf die Eigenschaft Name n liefert – den Knotennamen.
Die letzte Zeile im Rumpf der foreach-Anweisung sieht so aus: _textboxItems[n.Name].ForeColor = Color.Red;
228
Entwicklung der Programmierlogik
Nur die Eigenschaft hat sich verändert. Aus Text wurde eine Textfarbe (ForeColor), und aus einem über n.Name zugewiesenen Buchstaben wird ein Member
der Color-Struktur: Red. Bevor Sie testen, ist hier noch einmal der vollständige Inhalt der Klassendatei Dijkstra.cs: using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Linq; System.Text; System.Windows.Forms;
namespace Labyrinth { public partial class Labyrinth : Form { public Labyrinth() { InitializeComponent(); } private void delay(int time) { int t = Environment.TickCount; while ((Environment.TickCount - t) < time) Application.DoEvents(); } private void button1_Click(object sender, EventArgs e) { label1.Text = "A"; label1.ForeColor = Color.Black; label1.BorderStyle = BorderStyle.FixedSingle; label5.Text = "B"; label5.ForeColor = Color.Black; label5.BorderStyle = BorderStyle.FixedSingle; label5.TextAlign = ContentAlignment.MiddleCenter; label13.Text = "C"; label13.ForeColor = Color.Black;
229
7.4
7
Im Labyrinth des Minotaurus
label13.BorderStyle = BorderStyle.FixedSingle; label13.TextAlign = ContentAlignment.MiddleCenter; label14.Text = "D"; label14.ForeColor = Color.Black; label14.BorderStyle = BorderStyle.FixedSingle; label14.TextAlign = ContentAlignment.MiddleCenter; label16.Text = "E"; label16.ForeColor = Color.Black; label16.BorderStyle = BorderStyle.FixedSingle; label16.TextAlign = ContentAlignment.MiddleCenter; label17.Text = "F"; label17.ForeColor = Color.Black; label17.BorderStyle = BorderStyle.FixedSingle; label17.TextAlign = ContentAlignment.MiddleCenter; label20.Text = "G"; label20.ForeColor = Color.Black; label20.BorderStyle = BorderStyle.FixedSingle; label20.TextAlign = ContentAlignment.MiddleCenter; label21.Text = "H"; label21.ForeColor = Color.Black; label21.BorderStyle = BorderStyle.FixedSingle; label21.TextAlign = ContentAlignment.MiddleCenter; label22.Text = "I"; label22.ForeColor = Color.Black; label22.BorderStyle = BorderStyle.FixedSingle; label22.TextAlign = ContentAlignment.MiddleCenter; label33.Text = "J"; label33.ForeColor = Color.Black; label33.BorderStyle = BorderStyle.FixedSingle; label33.TextAlign = ContentAlignment.MiddleCenter; label36.Text = "K"; label36.ForeColor = Color.White; label36.BorderStyle = BorderStyle.FixedSingle; } private void button2_Click(object sender, EventArgs e) { label1.Text = "Start"; label1.ForeColor = Color.Black;
230
Entwicklung der Programmierlogik
label1.BorderStyle = BorderStyle.None; label5.Text = ""; label5.BorderStyle = BorderStyle.None; label13.Text = ""; label13.BorderStyle = BorderStyle.None; label14.Text = ""; label14.BorderStyle = BorderStyle.None; label16.Text = ""; label16.BorderStyle = BorderStyle.None; label17.Text = ""; label17.BorderStyle = BorderStyle.None; label20.Text = ""; label20.BorderStyle = BorderStyle.None; label21.Text = ""; label21.BorderStyle = BorderStyle.None; label22.Text = ""; label22.BorderStyle = BorderStyle.None; label33.Text = ""; label33.BorderStyle = BorderStyle.None; label36.Text = "Ziel"; label36.ForeColor = Color.White; label36.BorderStyle = BorderStyle.None; } private void button3_Click(object sender, EventArgs e) { Dictionary<string, Label> _textboxItems = new Dictionary<string, Label>(); _textboxItems.Add("A", _textboxItems.Add("B", _textboxItems.Add("C", _textboxItems.Add("D", _textboxItems.Add("E", _textboxItems.Add("F", _textboxItems.Add("G",
label1); label5); label13); label14); label16); label17); label20);
231
7.4
7
Im Labyrinth des Minotaurus
_textboxItems.Add("H", _textboxItems.Add("I", _textboxItems.Add("J", _textboxItems.Add("K",
label21); label22); label33); label36);
Dictionary<string, Knoten> _dictKnoten = new Dictionary<string, Knoten>(); _dictKnoten.Add("A", _dictKnoten.Add("B", _dictKnoten.Add("C", _dictKnoten.Add("D", _dictKnoten.Add("E", _dictKnoten.Add("F", _dictKnoten.Add("G", _dictKnoten.Add("H", _dictKnoten.Add("I", _dictKnoten.Add("J", _dictKnoten.Add("K",
new new new new new new new new new new new
Knoten("A")); Knoten("B")); Knoten("C")); Knoten("D")); Knoten("E")); Knoten("F")); Knoten("G")); Knoten("H")); Knoten("I")); Knoten("J")); Knoten("K"));
List _kanten = new List(); _kanten.Add(new Kante(_dictKnoten["A"], _dictKnoten["B"], _kanten.Add(new Kante(_dictKnoten["A"], _dictKnoten["C"], _kanten.Add(new Kante(_dictKnoten["B"], _dictKnoten["F"], _kanten.Add(new Kante(_dictKnoten["C"], _dictKnoten["D"], _kanten.Add(new Kante(_dictKnoten["D"], _dictKnoten["G"], _kanten.Add(new Kante(_dictKnoten["E"], _dictKnoten["I"], _kanten.Add(new Kante(_dictKnoten["F"], _dictKnoten["E"], _kanten.Add(new Kante(_dictKnoten["G"], _dictKnoten["H"], _kanten.Add(new Kante(_dictKnoten["H"], _dictKnoten["J"], _kanten.Add(new Kante(_dictKnoten["I"], _dictKnoten["H"], _kanten.Add(new Kante(_dictKnoten["J"], _dictKnoten["K"],
3)); 1)); 1)); 0)); 0)); 0)); 0)); 0)); 1)); 0)); 2));
List _knoten = new List();
232
Entwicklung der Programmierlogik
foreach (Knoten n in _dictKnoten.Values) { _knoten.Add(n); } Dijkstra algo = new Dijkstra(_kanten, _knoten); List pfad = algo.pfadZu(_dictKnoten["A"], _dictKnoten["K"]); foreach (Knoten n in path) { textBox1.Text = n.Name; delay(1000); _textboxItems[n.Name].Text = n.Name; _textboxItems[n.Name].ForeColor = Color.Red; } } } } Listing 7.15
Die Klassendatei »Labyrinth.cs« in der Endversion
Abbildung 7.9
Zur Laufzeit – das Projekt »Labyrinth«
233
7.4
7
Im Labyrinth des Minotaurus
Debuggen Sie die Anwendung mit (Strg)+(F5). Wenn der Debugger keine Einwände hat, sollte das Labyrinth-Fenster angezeigt werden. Definieren Sie nun elf Knoten über die Schaltfläche Knoten definieren. Lassen Sie im Labyrinth den kürzesten Weg von A nach K berechnen. Abbildung 7.9 verrät meine Ungeduld. Bis zum Zielknoten K wollte ich mit dem Screenshot nämlich nicht warten. Anderenfalls stünde ein K in der TextBox. Trotzdem habe auch ich den Weg hinaus gefunden.
7.5
Hätten Sie’s gewusst?
Dank Dijkstra und Ihnen wissen wir jetzt: Der kürzeste Weg vom Start zum Ziel führt über die Knoten A, C, D, G, H links um das T-förmige, blaue Hindernis herum. Auch ohne mannigfaltige Eigenschaftenbelegung war die Programmierung nicht unbedingt eine Angelegenheit von zehn Minuten, allein schon wegen der Implementierung der Listen und Dictionarys. Glauben Sie mir bitte trotzdem: Um Dijkstra in die sprichwörtliche, gleichwohl nicht wortwörtlich zu nehmende Irre zu führen, genügt es, im Programmcode zwei int-Werte auszutauschen: Welche Werte können ausgetauscht werden, und an welcher Stelle im Programm? Vieles auf den vorangegangenen Seiten handelte von den Knoten des gerichteten, azyklischen, kantengewichteten Graphen. In Abschnitt 7.4.2, »Definition der Knoten im EventHandler »button1_Click()«, wurden 11 Knoten im Labyrinth definiert und auf »Knopfdruck« dargestellt. Stillschweigend werden Sie mir unterstellt haben keinen Knoten übersehen zu haben, der die in Abschnitt 7.2.1, »Schwaches Knotenkriterium«, erklärte Bedingung erfüllt. Einen Knoten vergessen habe ich zwar in der Tat nicht, allerdings habe ich einen möglichen Knoten nicht verwendet. Welches der sechsunddreißig Labels hätte das »schwache Knotenkriterium« ebenfalls erfüllt? Erinnern Sie sich an Abschnitt 7.1.1, »Ein Quäntchen Graphentheorie«! Deswegen wurde das Label x nicht in den Stand eines Knotens erhoben.
234
»Du brauchst fünf Minuten, um Poker zu lernen, aber ein ganzes Leben, um es zu beherrschen.« (Doyle Brunson)
8
Pokern
Diesmal hätte er Nein sagen sollen! Sympathisch, wie der gefallene Leinwandheld (wie immer ganz er selbst – Steve McQueen) selbst in der Stunde seiner größten Niederlage noch war, wollte er jedoch nicht Nein sagen. Und so kam es, wie von der Dramaturgie gefordert: Eric Stoners Münze berührte die Mauer. Endlich konnte auch der namenlose Straßenjunge über das stadtbekannte, Cincinnati Kid genannte Poker-Ass triumphieren. Da war es erst wenige Filmminuten her, dass sich der charismatische, exzellent pokernde Stoner vom Altmeister des Five Card Stud, Lancy Howard, in einem quälend langen Marathon auf ein durchschnittliches Maß herunterspielen lassen musste. Gewissermaßen war Kids Niederlage so großartig wie der ganze, in den entscheidenden Szenen wie ein subtiles Kammerspiel wirkende Film (er wurde 1965 nach einer Romanvorlage von Richard Jessup gedreht). Denn gegen einen aus Assen und Zehnen zusammengesetzten Straight Flush des Pokerkönigs Howard war Eric Stoners Full House beinahe so leer, wie es nachher in seinem Leben zugehen sollte.
Abbildung 8.1
Selbst der König kann sich nur noch abwenden ...
235
8
Pokern
Bevor es weitergeht Was auch immer Sie aus dem Projekt »Pokerspiel« ableiten – auch dieser Klassiker der Kartenspiele zählt zur Kategorie Glücksspiel, dem bekanntlich die Gefahr des ruinösen Süchtigwerdens innewohnt. Spielen Sie trotzdem, doch behalten Sie den Deckel auf der Haushaltskasse. Rechtzeitig auszusteigen ist besser, als am Ende abzusteigen.
Zu Recht erwarten Sie nun eine kurze Einführung in zumindest eine von ungezählten Pokervarianten.
8.1
Die Hand am Colt – Five Card Draw
Five Card Draw gehört zur Kategorie des Draw Pokers, das weithin als die älteste Variante angesehen wird. Ganz gleich vor welchem Wildwestfilm Sie sitzen, schwenkt die Kamera auf einen Pokertisch, auf Schnapsflasche, Colt und auf Zigarren, die ins Diffuse des Salons hinein dampfen, ist in neun von zehn cineastischen Fällen eine Five Card Draw-Runde im Gang – und die genretypische Knallerei nicht weit.
Abbildung 8.2
236
Verdecktes (Un-)Glück – fünf Pokerkarten beim Five Card Draw
Die Hand am Colt – Five Card Draw
Draw Poker ist die einzig (mir bekannte) Pokervariante, bei der der Spieler die Karten des Gegners nicht zu Gesicht bekommt, was den Adrenalinspiegel zusätzlich in die Höhe treibt. Vielleicht ist das einer der Gründe, warum Five Card Draw lange auch das beliebteste Pokerspiel war. Glücksfördernd mag auch die Möglichkeit sein, in mehreren Setzrunden Karten gegen unbekannte Karten zu tauschen (siehe Abschnitt 8.1.1), natürlich mit dem Ziel, aus der eigenen Hand mehr als eine High Card (siehe Abbildung 8.1) herauszuholen. Schließlich ging es zur Zeit des William Frederick Cody (besser bekannt als Buffalo Bill) vor allem um bare Dollars. Und wenn die nicht reichten, ging es um Kopf und Kragen ... Auch wir spielen Draw Poker, um genau zu sein: Five Card Draw. Sehen wir uns das Regelwerk an.
8.1.1
Die Regeln beim Five Card Draw
Im Verlauf zumeist mehrerer Pokerrunden wird jeder Spieler mindestens einmal zum Kartengeber, auch Dealer genannt. Dabei erfolgt der Wechsel des Gebers im Uhrzeigersinn, mithin rechts herum. Drücken kann sich vor dem Job niemand. Bevor die Karten ausgeteilt werden, erfolgt anhand sogenannter Blindeinsätze (Antes) die Bildung des Pots; eine Angelegenheit, die höchst geregelt über die Spielfläche geht: 왘
Der Spieler links vom Dealer eröffnet das fröhliche Sätzen mit einem sogenannten Small Blind.
왘
Dann ist der zweite, links vom Geber sitzende Spieler an der Reihe. Der allerdings tätigt einen Big Blind; somit hat sein Einsatz höher zu sein.
Nun werden jedem Spieler – verdeckt – fünf Karten ausgeteilt. Sitzen Sie zufällig links neben dem Spieler, der den Big Blind gesetzt hat, ist es an Ihnen, die erste Einsatzrunde zu eröffnen. Sie, wie auch alle anderen, können dabei mitgehen, erhöhen oder aussteigen. Nach der ersten Einsatzrunde darf jeder Spieler null bis fünf Karten ablegen und ziehen – sprich: tauschen. Entscheidet sich ein Spieler gegen das Ablegen einer oder mehrerer Karten und auch gegen den Tausch, ist allen anderen klar: Er oder sie setzt aus. Nachdem jeder entweder ausgesetzt oder Karten ausgetauscht hat, beginnt die zweite, letzte und entscheidende Einsatzrunde. Nach der kommt es zum Showdown, und der Spieler mit dem besten Blatt gewinnt den Pot.
237
8.1
8
Pokern
Sonderfall »Split Pot« Besitzen zwei oder mehrere Spieler ein gleich starkes Blatt und ist der Showdown demnach unentschieden ausgegangen, wird der Pot unter den betreffenden Spielern brüderlich aufgeteilt. Der Pokerspielkundige spricht von einem Split Pot. Interessanter ist die Frage der spielbeeinflussenden Kartenkombinationen, die bei allen Pokervarianten identisch sind. Auch ihre Rangfolge ist bei allen Varianten dieselbe. Doch lesen Sie selbst.
8.1.2
Gewichtete Hände
Beim Pokern ist unter dem Begriff Hand eine Kartenkombination im Allgemeinen sowie die beste vom Spieler zu nutzende Kombination im Speziellen zu verstehen. Können zwei Spieler eine identische Kartenkombination auf den Tisch legen, entscheidet die Höhe der einzelnen Karten über Sieg oder Niederlage. Grundlage ist die folgende absteigende Wertigkeit: Ass, König, Dame, Bube, 10, 9, 8, 7, 6, 5, 4, 3, 2. Ebenfalls in der Wertigkeit absteigend (Straight Flush ist die bestmögliche Kartenkombination), gibt Tabelle 8.1 einen Überblick über die relevanten Kombinationen. Farben, die keine sind Für das Verständnis von Tabelle 8.1 ist es wichtig, zu wissen, dass bei Spielkarten (ausgenommen die vielfarbigen Karten) der Begriff Farbe die vier Symbole, namentlich Schaufel (Pik), Kreuz, Herz, Karo bezeichnet. Auf jeder der 52 Karten ist ein solches Symbol abgebildet. Name der Hand Bedeutung
Beispiel
Entscheidungskriterium
Straight Flush
Straße, bei der die Karten von einheitlicher Farbe sind
Höchste Karte, hier KaroBube
Four of A Kind
Vier Karten mit gleichem Wert
Höhe des Vierlings zuzüglich der Beikarte
Full House
Ein Drilling und ein Paar
Höhe des Drillings zuzüglich der Höhe des Paars
Flush
Fünf Karten identischer Farbe
Höhe der einzelnen Karten
Tabelle 8.1
238
Verwirrende Vielfalt – spielentscheidende Kartenkombinationen beim Pokerspiel
Die Hand am Colt – Five Card Draw
Name der Hand Bedeutung Straight
Beispiel
Entscheidungskriterium
Straße, bei der die Karten von unterschiedlicher Farbe sind
Höchste Karte, hier HerzSieben
Three of A Kind Drei Karten mit identischem Wert
Höhe des Drillings und der Beikarten
Two Pair
Zwei Paare
Wert der Paare und der Beikarten
One Pair
Zwei Karten mit gleichem Wert
Höhe des Paares und der restlichen drei Karten
High Card
Keine der obigen Kombinationen
Höhe der einzelnen Karten
Tabelle 8.1
Verwirrende Vielfalt – spielentscheidende Kartenkombinationen beim Pokerspiel
Sonderfall »Royal Flush« Ein Royal Flush ist ein Straight Flush mit einem Ass als höchster Karte. Somit war Lancy Howards letzte Hand eher ein Royal Flush als ein Straight Flush. Ein Royal Flush ist ausgesprochen selten, seltener noch war die Kombination, mit der der alternde Pokerkönig schlussendlich triumphierte. Für die Kartenverteilung wurde später eine Wahrscheinlichkeit von etwa 1 zu 45 Millionen (!) errechnet.
Bleiben wir beim Thema Wahrscheinlichkeiten. Kombinationen und Wahrscheinlichkeiten bei 5 aus 52 Karten Die Anzahl möglicher Kombinationen für eine definierte Hand verhält sich proportional zur Wahrscheinlichkeit, die Hand auch tatsächlich zu erhalten. Tabelle 8.2 bestätigt den Zusammenhang. Hand
Kombinationen
Wahrscheinlichkeit
Straight Flush
36
0,0014 %
Four of A Kind
624
0,0240 %
Full House
3.744
0,144 %
Flush
5.108
0,197 %
Straight
10.200
0,392 %
Three of A Kind
54.912
2,11 %
Tabelle 8.2
Was möglich ist, ist noch lange nicht sehr wahrscheinlich..
239
8.1
8
Pokern
Hand
Kombinationen
Wahrscheinlichkeit
Two Pair
123.552
4,75 %
One Pair
1.098.240
42,3 %
High Card
1.302.540
50,1 %
Tabelle 8.2
Was möglich ist, ist noch lange nicht sehr wahrscheinlich.. (Forts.)
Wenn Sie die Werte in der Spalte Kombinationen summieren, ergeben sich 2.598.956 mögliche Kartenkombinationen. Noch einmal – der Royal Flush Es existieren lediglich 4 Möglichkeiten, aus 52 Karten einen Royal Flush zu kombinieren – was einer Wahrscheinlichkeit von ungefähr 0,00015 % entspricht. Nicht sehr ermutigend ...
8.2
Draw Poker Light – unser Spiel
Das »Light« im Spielnamen ist unausweichlich, denn ein Pokerspiel zu entwickeln ist kein einfaches Vorhaben. Zum Beispiel arbeiten Online-Pokerspiele, hinter denen oft voluminöse Datenbanken werkeln, immer öfter mit hochentwickelten, mitunter sogar patentierten, evolutionären bzw. lernbasierten Algorithmen. Von denen würde jeder einzelne mühelos ein Buch der Kategorie »Schwer verdauliche Kost« füllen. Kurzum: Wir müssen Kompromisse machen. Der Warnung zweiter Teil Hüten Sie sich vor Online-Pokerspielen, bei denen um echtes Geld gespielt wird. Sie wissen nicht, ob die beteiligten Algorithmen nicht darauf »getrimmt« sind, den Gegner mit versteckter Regelmäßigkeit verlieren zu lassen.
8.2.1
Die Wahl des Gegners
Teil der Kompromisslinie ist nicht die Wahl des Gegners, gegen den Sie spielen werden. Das ist nämlich die Bank. Zwar handelt es sich nicht um die Bank von England, doch ist auch das erdachte Institut ehrenwert, in den Einsätzen moderat und dem Wohle der Kundschaft aufs Höchste verpflichtet [...]. Die Bank selbst spielt – vielleicht gerade deswegen – indirekt. Das heißt, wie viel Sie verlieren oder gewinnen, bemisst sich nicht nach dem Wert der Hand in Re-
240
Entwicklung der Benutzeroberfläche
lation zu den Karten des kreditgebenden Gegners. Entscheidend ist einzig die Wertigkeit der fünf Pokerkarten, die sich nach einem Tausch auf der virtuellen Spielfläche befinden. Dies ist nicht »made by author«, sondern wird von einigen (Freeware-)Pokerspielen konzeptionell in vergleichbarer Weise umgesetzt.
8.2.2
Erzwungener Tausch
Bei »Draw Poker Light« allerdings müssen Sie mindestens eine Karte tauschen. Des Weiteren haben Sie – selbstverständlich – nicht die Möglichkeit des »Rücktauschs«. Ist eine Karte einmal zum Tausch ausgewählt (wie das vonstatten geht, verrät Abschnitt 8.4.6), bleibt es dabei – selbst dann, wenn durch einen vorschnellen Klick eine geringer bewertete Hand oder nichts daraus geworden ist.
8.2.3
Die Frage des Geldes
Spieler und Bank bringen jeweils 5000 $ Eigenkapital mit – ein erkleckliches Sümmchen. Dahingegen kann, von 10 $ ausgehend, die Höhe des Spieleinsatzes 50 $ (Schrittweite 10 $) nicht überschreiten. Wie viel auch immer Ihnen der Nervenkitzel wert ist – das Institut verdoppelt den Betrag im Pot durch einen Einsatz in derselben Höhe. Somit kann der Geldtopf höchstens bis zur 100-$-Marke gefüllt sein, während sich die Eigenkapitaldecke der Kontrahenten um jeweils 50 $ verdünnt hat. Trotzdem kann es schnell im Ruin enden, denn jetzt kommt ein Multiplikator ins Spiel: Von High Card bis Royal Flush erhöht sich der Betrag durch Multiplikation mit einem der Hand zugeordneten Wert. Den resultierenden Betrag erhält der Gewinner. Der Verlierer bekommt denselben Betrag abgezogen. Kann keine spielrelevante Hand ermittelt werden, ist das Geld im Pot mit dem Faktor 50 zu multiplizieren. Sind im Pot zum Beispiel 40 $ und konnte der Spieler keine entsprechende Hand auf den Tisch legen, schrumpft sein Guthaben um 2000 $, während sich die Bank über denselben Betrag freuen darf. Übrigens: Gesetzt wird erst, nachdem die Karten aufgenommen sind. Mithin gibt es keine Blindeinsätze.
8.3
Entwicklung der Benutzeroberfläche
Wenn Sie über das Hauptmenü mit Datei 폷 Projekt ein neues Projekt anlegen, entscheiden Sie sich in der Dialogmaske Neues Projekt für eine WindowsForms-Anwendung.
241
8.3
8
Pokern
Im Editorfeld Name könnte beispielsweise »Poker Game« stehen, genauso wie im Schreibfeld Projektmappe (der Default-Eintrag im Dropdown-Listenfeld Lösung 폷 Neue Projektmappe erstellen kann beibehalten werden). Es ist kein konzeptioneller Sündenfall, wenn Projekt und Projektmappe denselben Namen tragen. Projekte gehören – zumindest bei mir – in einen C:\-Pfad-Ordner Projekte. Ist das bei Ihnen auch so, tragen Sie den Pfad unter Ort ein. Anderenfalls suchen Sie sich den Weg zum Projekt über das Dateisystem, das hinter der Schaltfläche Durchsuchen liegt (im zugehörigen Formular lässt sich auch ein neuer Ordner anlegen).
Abbildung 8.3
Projektrelevante Festlegungen in der Dialogmaske »Neues Projekt«
Klicken Sie auf Ok.
8.3.1
Vom Ordner zur Bedienoberfläche
Auch das letzte Projekt ist nicht frei von generischen Listen und »wildem Jonglieren« mit Listenelementen. Zuerst geht es um jene Liste, in der 52 Karten untergebracht sind. Darauf folgt eine weitere Liste mit fünf Karten, die eine mehr oder weniger ertragreiche Hand bilden.
242
Entwicklung der Benutzeroberfläche
Wir sind noch nicht so weit. Doch sind 52 Spielkarten gleichbedeutend mit 52 Images; und jedes Bild zeigt die Vorderseite einer Karte. Für die brauchen wir einen Ort, der im Projektordner, genauer gesagt unterhalb des Stammverzeichnisses Draw Poker, gefunden werden sollte. Ein wenig Vorarbeit Zunächst erstellen wir den Folder und Subfolder und bringen sie in einem Ordner Images (und dort im Unterordner Karten) unter – auch wenn das mit Oberflächengestaltung nichts zu tun hat: 왘
Klicken Sie dazu im Projektmappen-Explorer mit der rechten Maustaste auf das Stammverzeichnis Poker Game.
왘
Wählen Sie im Kontextmenü bitte die Option Hinzufügen aus, und klicken Sie im nächsten Menü Neuer Ordner an.
Ein neuer Ordner wird erstellt. Ändern Sie dessen Default-Namen bitte in Images. Die nächste Runde: Diesmal klicken Sie jedoch den neu erstellten Ordner an, in dem es einen Klick weiter ein weiteren Ordner geben muss. Den nennen Sie Karten. Auch abseits des Unterordners bleibt der Ordner Images nicht leer. Wenn wir im übernächsten Abschnitt nämlich sieben PictureBox-Steuerelemente platzieren, zeigen zwei ein Image mit eher dekorativem Motiv. Daneben brauchen wir ein weiteres Bild – für die Kartenrückseite. WindowsForm in Form gebracht Unser kleines Pokerspiel benötigt Platz auf dem Bildschirm, von dem der größte Teil gleichwohl nicht auf die fünf Spielkarten entfällt. Eher sind wir bemüht, »Draw Poker Light« ein »spiele-typisches Erwachsenengesicht« zu geben: ein irgendwie nettes, unser Bemühen ansatzweise widerspiegelndes Design. Im Eigenschaften-Fenster der WindowsForm Poker (das zugehörige DropdownListenfeld sollte den Eintrag Poker System.Windows.Forms.Form zeigen), tragen Sie in der Kategorie Size für Width den Wert 1297 ein und für Height den Wert 898. Zur Festlegung weiterer Eigenschaften der WindowsForm Poker: 왘
Legen Sie die BackColor-Eigenschaft (Kategorie Darstellung) auf Black fest.
왘
Zur Minimierung des Fensters besteht ebenso wenig Grund wie für eine Maximierung. Legen Sie deshalb (in der Kategorie: Fensterstil) Folgendes fest):
243
8.3
8
Pokern
왘
MaximizeBox: False
왘
MinimizeBox: False
Die Möglichkeit zur Skalierung sollte ebenfalls nicht bestehen: Legen Sie (in der Kategorie Darstellung) die FormBorderStyle-Eigenschaft auf das Enumerationsmember FixedSingle fest. Weiter geht es mit den verwendeten Controls. Wobei Ihnen klar sein sollte, dass auch die WindowsForm Poker – obgleich eher im weiteren Sinne – den Steuerelementen zuzurechnen ist. Der Klassenbeste Die meisten Steuerelementklassen im Namensraum System.Windows.Forms sind von der Klasse Control abgeleitet. Control stellt Basisfunktionen für fast alle in einer WindowsForm verwendeten Steuerelemente bereit. Die Form-Klasse selbst ist ebenfalls eine Erbin der Basisklasse Control, wenngleich – und das ist einer der Unterschiede zu »waschechten« Steuerelementen – nicht unmittelbar.
Die Anordnung der verwendeten Controls im Formular Abbildung 8.4 führt Sie in den segmentären Aufbau des Pokerspiels ein. Abhängig von den Segmenten und ihren Funktionen werden die benötigen Controls auf der Bedienoberfläche angeordnet.
Steuerungssegment
Hintergrundsegment
Kartensegment Abbildung 8.4
Anzeigesegment
Funktionsbereiche des Spiels «Draw Poker Light”
Sie bekommen es mit vier Segmenten zu tun: 왘
Abgesehen von einem Spiele-Logo wird im Steuerungsegment sowohl der Einsatz getätigt als auch das Geben, Aufnehmen und Tauschen der fünf Karten.
왘
Die Anzeige des Kartendecks erfolgt im Kartensegment, wo auch die Möglichkeit besteht, Karten zum Tausch auszuwählen (wie, das verrät Ihnen Abschnitt 8.4.6).
왘
Im Anzeigesegment ist die Höhe des Einsatzes und davon abhängig das Guthaben der Kontrahenten (Spieler versus Bank) angezeigt. Ist eine Runde beendet,
244
Entwicklung der Benutzeroberfläche
d. h., sind die Karten bewertet, erfolgt ebenfalls die Anzeige des Guthabens von Spieler und Bank. 왘
One oder Two Pair, Hight Card oder was auch immer – wer Geld verliert oder gewinnt, das wird zusätzlich in der rechten oberen Ecke des Hintergrundsegments angezeigt. Dies geschieht vor dem Hintergrund irgendeines ansprechenden Bildes. Von dem hat das Hintergrundsegment seinen wohlklingend sperrigen Namen.
In den folgenden Abschnitten werden die vier Segmente getrennt behandelt. Kartensegment
Fünf PictureBoxen (pictureBox1 bis pictureBox5) werden im Kartensegment angeordnet: für jede Karte eine Box. Diese Boxen sind in ihren Abmessungen selbstredend gleich: Legen Sie bitte in der Kategorie Layout des jeweiligen Eigenschaften-Fensters die Width-Eigenschaft der PictureBox auf 125 und die Height-Eigenschaft auf 195 fest. Einer realen Spielkarte entsprechen die Maße zwar nicht, doch lässt sich auch mit verkleinerten Karten vorzüglich verlieren. Spielkarten überlappen sich öfter, als dass sie – Kante an Kante – nebeneinander angeordnet werden. Daran orientieren wir uns bei der Belegung der Koordinaten X und Y: Name-Eigenschaft
X-Koordinate
Y-Koordinate
pictureBox1
413
603
PictureBox2
501
603
PictureBox3
587
603
pictureBox4
674
603
pictureBox5
771
603
Tabelle 8.3
Positionierung der fünf PictureBoxen über die X- und Y-Koordinate
Aktiviert werden die PictureBoxen später dynamisch. Deshalb müssen Sie die Enabled-Eigenschaft (Kategorie Verhalten) jeweils auf den booleschen Wert False setzen. Abbildung 8.5 illustriert die fünf PictureBoxen in der Entwurfsansicht. Zur Laufzeit wären keine Begrenzungen erkennbar, da wir die auf None gesetzte FormBoderStyle-Eigenschaft nicht geändert haben. Ein Pokertisch, selbst dann, wenn er virtuell und nicht rund ist, ist schließlich kein Kaufhausparkplatz, auf dem weiße
245
8.3
8
Pokern
Rechtecke aufgezeichnet sind, damit die Kunden ordentlich parken (was nicht immer hilft). Übersehen Sie bitte nicht die Fokussierung auf pictureBox1.
Abbildung 8.5
Fünf überlappend angeordnete PictureBox-Controls in der Entwurfsansicht
Im nächsten Segment gibt es mehr zu tun. Steuerungssegment
Greifen Sie in die »Schublade« Container der Toolbox. Ziehen Sie ein GroupBoxSteuerelement auf die Oberfläche der Anwendung. Das »ewige Thema« Eigenschaften handeln wir auch beim Control groupBox1 tabellarisch ab (siehe Tabelle 8.4). Kategorie
Eigenschaft
Bewertung
Darstellung
BackColor
Chocolate
Darstellung
Text
Draw Poker
Darstellung
Font
Layout
Layout
왘
Size
8,9
왘
Bold
True
Location 왘
X
11
왘
Y
12
Size 왘
Width
357
왘
Height
786
Tabelle 8.4 Festlegung projektrelevanter Eigenschaften beim GroupBox-Control (»groupBox1«)
Entnehmen Sie der Toolbox eine weitere PictureBox (pictureBox6). Sie soll ein aufdringlich prangendes Spiele-Logo aufnehmen. Skalieren Sie sie auf:
246
Entwicklung der Benutzeroberfläche
왘
Width: 180
왘
Height: 185
Positionieren Sie die PictureBox gemäß folgender Koordinaten oben mittig in groupBox1: 왘
X: 75
왘
Y: 48
Zum Fenster Ressource auswählen aus Abbildung 8.6 gelangen Sie am schnellsten über das Eigenschaften-Fenster des Controls pictureBox6. Unter der Kategorie Darstellung entdecken Sie die Image-Eigenschaft des Steuerelements, hinter der ein Button mit drei Punkten zum Öffnen des Fensters einlädt. In diesem Fenster geht es uns um den RadioButton Lokale Ressource, der, einmal gesetzt, die Schaltfläche Importieren aktiviert, hinter der das Dateisystem (Dialogmaske Öffnen) ruht. Klicken Sie sich durch das Ordnergestrüpp bis zum Projektordner Images vor. Was auch immer für ein Bild im gleichnamigen Folder liegt – ein Klick auf OK »enabled« die Vorschau im Fenster Ressource auswählen. Ein abschließender Klick auf OK verknüpft das ausgewählte Image mit pictureBox6.
Abbildung 8.6
Auswahl einer lokalen (Image-)Ressource im Formular »Ressource auswählen«
Zur Steuerung des Spiels werden neben einem NumericUpDown-Control (auch Drehfeld genannt) drei Buttons (der Kategorie Allgemeine Steuerelemente der
247
8.3
8
Pokern
Toolbox) benötigt. Sie sollen bündig untereinander angeordnet werden und unterschiedliche (Text-)Farben und Aufschriften bekommen. Über dem Drehfeld soll ein kleiner Text stehen (Einsatz in $:), der ausgehend von einem schnöden Label-Control mit folgenden Eigenschaften zu versehen ist (die Sie allesamt unter der Kategorie Darstellung finden): Eigenschaft
Bewertung
BackColor
Chocolate
Font
9,1
Font 왘
Bold
True
ForeColor
PeachPuff
Text
Einsatz in $
Tabelle 8.5
Festlegung der Eigenschaften beim Steuerelement »label2«
Die Festlegung der relevanten Eigenschaften beim Control numericUpDown1 sind Tabelle 8.6 zu entnehmen: Kategorie
Eigenschaft
Bewertung
Darstellung
Value
10
Layout
Location
Size
Verhalten
왘
X
23
왘
Y
575
Size 왘
Width
120
왘
Height
24
Enabled
False
Tabelle 8.6 Relevante Eigenschaften und deren Festlegung beim Steuerelement »numericUpDown1«
Tabelle 8.7 zeigt die Text-, BackColor- und ForColor-Eigenschaften für die Schaltflächen. Text-Eigenschaft
BackColor-Eigenschaft
ForeColor-Eigenschaft
Geben
SaddleBrown
ButtonFace
KartenAufnehmen
ButtonShadow
ActiveCaptionText
Tauschen
Red
ButtonFace
Tabelle 8.7 Festlegung der »Text«-, »BackColor«- und der »ForeColor«-Eigenschaft der drei Schaltflächen
248
Entwicklung der Benutzeroberfläche
Wie bei den Karten im Kartensegment legen Sie auch bei dem schmucken Buttontriplett identische Werte für die Eigenschaften Width (291) und Height (43) fest. Tabelle 8.8 gibt Auskunft darüber, wo die Buttons zu positionieren sind. Sie sehen hier auch, wie die Name-Eigenschaft (Kategorie Entwurf) der drei Schaltflächen zu bewerten ist: Name-Eigenschaft
X-Koordinate
Y-Koordinate
geben
23
618
aufnehmen
23
667
tauschen
23
716
Tabelle 8.8
Positionierung der drei Button-Controls über die X- und Y-Koordinate
Setzen Sie abschließend bei den Buttons Aufnehmen und Tauschen die Enabled-Eigenschaft auf False.
Abbildung 8.7 zeigt das Steuerungssegment so, wie es zur Laufzeit des Programms aussehen sollte.
Abbildung 8.7
Das Steuerungssegment zur Laufzeit
249
8.3
8
Pokern
Anzeigesegment
Anzuzeigen ist der Spielstand im Anzeigesegment. Dort, wo die Tragödie ungebremster Zockerei nur allzu augenfällig ist, sollte der Rahmen stimmen und eine Überschrift nicht fehlen. Kurzum: Nehmen Sie ein zweites GroupBox-Steuerelement (groupBox2) aus der Toolbox, und positionieren Sie es rechts neben dem Kartensegment. Geht das auch ein wenig genauer? Ja: 왘
X: 922
왘
Y: 597
Das Steuerelement sollte folgende Dimensionen haben: 왘
Width: 358
왘
Height: 201
Als Text (Text-Eigenschaft, unter der Kategorie Darstellung) wählen Sie Spiel gegen die Bank – was man als Aufforderung oder anders verstehen kann.
Positionieren Sie als Nächstes fünf Label-Controls im Containerelement groupBox2. Bezüglich der Koordinaten werden Sie von Tabelle 8.9 auf dem Lau-
fenden gehalten. Dort sehen Sie auch, welche Werte Sie der Text-Eigenschaft geben müssen: Name-Eigenschaft
Text-Eigenschaft
X-Koordinate
Y-Koordinate
label3
Spieler
32
47
label4
Bank
265
47
label5
5000$
31
131
label6
5000$
264
131
label8
Einsatz
137
70
Tabelle 8.9
Positionierung der fünf Label-Controls über die X- und Y-Koordinate
Orientiert an der Name-Eigenschaft, werden die übrigen Eigenschaften wie folgt belegt (siehe Tabelle 8.10): Name-Eigenschaft
Size-Eigenschaft (Font) ForeColor-Eigenschaft
label3
10
Red
label4
10
Red
label5
12
White
label6
12
White
label8
12
Red
Tabelle 8.10
250
Belegung der »Size«- und der »ForeColor«-Eigenschaft
Entwicklung der Benutzeroberfläche
Zwei grafische Dollarbündel, die im Ordner Images des Projekts zu finden sind, sollen für ein wenig Auflockerung im etwas textlastigen Anzeigesegment sorgen. Beide Bilder sind identisch und haben eine Größe von: 왘
Width: 48
왘
Height: 46
Dementsprechend sind auch die beiden PictureBox-Controls (pictureBox8/ pictureBox9) bemessen. Die erste Box positionieren Sie bitte zwischen label3 und label5, und zwar genau bei: 왘
x: 35
왘
y: 70
Die Koordinaten der zweiten PictureBox seien mit 왘
x: 269
왘
y: 70
angegeben. Jetzt müssen Sie nur noch über den »Drei-Punkt-Button« hinter der jeweiligen BackgrundImage-Eigenschaft die Dialogmaske Ressource auswählen öffnen und die beiden Bilder über den Button Importieren als lokale Ressource einbinden. Abbildung 8.8 zeigt das Anzeigesegment im Entwurfsmodus (Fokussierung auf pictureBox8).
Abbildung 8.8
Das Anzeigesegment in der Entwurfsansicht
Hintergrundsegment
Vor dem großartig weiträumigen Panorama einer nordamerikanischen Urlandschaft lässt es sich vielleicht besser pokern als in einem schlecht beleuchteten, stickigen Salon. Was durch pictureBox6 im Kleinen realisiert wurde, ist im Großen Aufgabe einer weiteren PictureBox (pictureBox7), die Sie bitte rechts neben der GroupBox
251
8.3
8
Pokern
Draw Poker positionieren. Tabelle 8.11 zeigt, welche Eigenschaften Sie der PictureBox zuweisen. Kategorie
Eigenschaft
Layout
Location
Layout
Bewertung
왘
X
374
왘
Y
12
Size 왘
Width
905
왘
Height
560
Tabelle 8.11 »pictureBox7« – Festlegung projektrelevanter Eigenschaften im Hintergrundsegment
An pictureBox7 haben Sie gelernt, wie einfach es ist, ein PictureBox-Steuerelement mit einem Bild Ihrer Wahl statisch zu verknüpfen. (Im Fall der Kartenhintergründe erfolgt die Verknüpfung gleichwohl dynamisch.) Tipp Seien Sie kreativ. Im Netz der Netze existiert genügend freies, gemeinfreies oder nur mit geringer Beschränkung versehenes Bildmaterial. Es muss nicht – wie in meinem eigenen Projekt geschehen – der berühmte John Ford Point (benannt nach dem gleichnamigen Regisseur) im Monument Valley sein. Auch ein anderes Motiv gibt dem Pokerspiel »Draw Poker Light« ein eigenes Erscheinungsbild.
Die Realisierung der dynamischen Textanzeige geschieht über ein gewöhnliches statisches Label (in meinem Falle label7), das Sie der Toolbox entnehmen und bei 왘
X: 766
왘
Y: 41
anordnen. Legen Sie im Eigenschaften-Fenster des Label-Controls unter der Kategorie Darstellung im Knotenpunkt Font die Size-Eigenschaft auf 13 fest. Die Eigenschaft ForeColor setzen Sie bitte auf Red. Damit wäre auch das Kapitel Hintergrundsegment erledigt. Allerdings ist die Gestaltung der Benutzeroberfläche damit noch nicht abgeschlossen. Was sonst noch zu tun ist
Außerhalb der in Abbildung 8.4 skizzierten Oberflächensegmente ordnen Sie noch einen kleinen Button (Width: 133, Height: 23) zum Schließen des Fensters
252
Entwicklung der Programmierlogik
(Wert der Text-Eigenschaft: Spiel beenden) an. Genauer gesagt, sitzt er unterhalb des Anzeigesegments bei: 왘
X: 1147
왘
Y: 804
Abbildung 8.9 gewährt Ihnen einen Blick auf die vollständig eingerichtete Benutzeroberfläche. Es ist übrigens vollkommen in Ordnung, wenn Sie andere Vorstellungen umsetzen und das beschriebene Vorgehen lediglich als grobe Richtschnur nutzen.
Abbildung 8.9
8.4
Das Pokerspiel »Draw Poker Light« in der Entwurfsansicht
Entwicklung der Programmierlogik
Benennen Sie im Projektmappen-Explorer den Dateinamen Form1.cs in Poker.cs um. Im Quellmodus der Datei geht es alsbald im Stil einer Liste weiter. Zuvor jedoch sind einige private, globale Felder zu vereinbaren. Das erste Feld ist vom Typ einer generischen Liste, deren Elemente wiederum vom Typ PictureBox sind: private List pb;
Auch die beiden nächsten Felder sind generischen Ursprungs, die Elemente allerdings gehen auf einen String zurück:
253
8.4
8
Pokern
private List<string> kliste, getauscht;
Die vierte Variable zählt zum Image-Typ: private Image im;
Zu instanziieren gibt es natürlich auch etwas, nämlich die Klasse tauscheKarten: private tauscheKarten tk = new tauscheKarten();
In der Klassenbibliothek des .NET-Frameworks suchen Sie tauscheKarten vergebens, nicht aber in Ihrem Buch. In Abschnitt 8.4.7 werden Sie auch für den Fall fündig, dass Sie bereits jetzt wissen möchten, wie fünf Karten auf Gleichheit geprüft und anschließend getauscht werden. Wie auch immer: Kommentieren Sie bitte die Zeile im Quellcode zunächst aus (//). Anderenfalls würden die Sie erwartenden Testläufe nicht funktionieren. Zum guten Schluss initialisieren wir das Grundkapital der Kontrahenten in einer int-Variablen.
Für den Spieler: private int ks = 5000;
Für die Bank: private int kb = 5000;
8.4.1
Addition und Subtraktion – der Ereignisbehandler »numericUpDown1_ValueChanged()«
Wir lassen Deklarationen und Initialisierungen jetzt hinter uns. Weiter geht es mit der Behandlung des ValueChanged-Ereignisses, das vom NumericUpDow-Control (numericUpDown1) ausgelöst wird. Zum Ereignisbehandler des Steuerelements gelangen Sie durch einen Doppelklick auf das Control. Wurde im Drehfeld Einsatz in $ ein Betrag gewählt, reduziert sich das Startguthaben des Spielers (angezeigt in label5) um denselben Betrag. Vom in ks gespeicherten int-Wert ist der Wert abzuziehen, den die Value-Eigenschaft der Klasse NumericUpDown liefert. ks - numericUpDown1.Value
Was auch immer das Ergebnis der Subtraktion ist – wir müssen ins gefällige string-Format konvertieren. Anderenfalls bekommen wir Probleme mit der Text-Eigenschaft der Klasse Label. Unbekannt ist Ihnen weder die Klasse Convert
254
Entwicklung der Programmierlogik
noch das Klassenmember ToString(). Wir hängen noch ein Dollarzeichen ($) an und sind damit fertig: label5.Text = Convert.ToString(ks - numericUpDown1.Value)+"$";
Nicht nur das Guthaben des Spielers reduziert sich um die gewählte Einsatzhöhe, sondern auch das der Bank: label6.Text = Convert.ToString(kb - numericUpDown1.Value)+"$";
Der Spieler setzt hypothetische 10 $, die Bank »verabredungsgemäß« denselben Betrag. Damit sind 20 $ im Pot (dargestellt durch label9): label9.Text = Convert.ToString(numericUpDown1.Value * 2)+ "$";
Hier sehen Sie den vollständigen Handler: private void numericUpDown1_ValueChanged(object sender, EventArgs e) { label5.Text = Convert.ToString(ks - numericUpDown1.Value) +"$"; label6.Text = Convert.ToString(kb - numericUpDown1.Value) +"$"; label9.Text = Convert.ToString(numericUpDown1.Value * 2) +"$"; } Listing 8.1
Der EventHandler »numericUpDown1_ValueChanged()«
Testen Sie das Anzeigesegment. Setzen Sie zuvor die im Eigenschaften-Fenster von numericUpDown1 die Enabled-Eigenschaft auf True. Wenn Sie das Spiel mit ((F5)) oder ohne ((Strg)+(F5)) Debug starten und im Drehfeld 50 $ auswählen, sollte auch bei Ihnen unter Spiel gegen die Bank der Stand aus Abbildung 8.10 zu sehen sein.
Abbildung 8.10
Das Anzeigesegment zur Laufzeit
255
8.4
8
Pokern
8.4.2
PictureBoxen aufgelistet – die Methode »pictureboxList()«
Was passiert denn schon Großartiges in der Methode pictureboxList(), die, obwohl sie auch ohne Argumente glücklich wäre, eine generische Liste zurückgibt, die mit fünf PictureBoxen gefüllt ist? Nichts, außer dass über List picb = new List();
ein Objekt (picb) der List(T)-Klasse erzeugt und mithilfe der Add()-Methode (einem Member der Klasse List(T)) mit Elementen gefüllt wird: private List pictureboxList() { List picb = new List(); picb.Add(pictureBox1); picb.Add(pictureBox2); picb.Add(pictureBox3); picb.Add(pictureBox4); picb.Add(pictureBox5); return picb; } Listing 8.2
Der andere Weg – »PictureBox«-Controls in einer generischen Liste
Jetzt passen Sie auf: Der Augenblick des Kartengebers ist gekommen. Schnell liegt die Hand auf dem Tisch.
8.4.3
Einsatz des Kartengebers
Im Eventhandler geben_Click(), den Sie durch einen Doppelklick auf den Button Geben erzeugen, dominiert eine simple for-Schleife, doch erst, nachdem die zuvor erstellte Methode pictureboxList() notwendigerweise aufgerufen wurde: pb = pictureboxList();
Zurückgegeben wird eine mit PictureBox-Elementen gefüllte Liste, deren Speicherung in der Variablen pb erfolgt. Der mit 0 initialisierte Schleifenindex i hat selbstredend fünf Karten abzudecken (später: aufzudecken), was nur in Verbindung mit einem entsprechenden Image gelingt. Dieses Bild zeigt die Rückseite der Pokerkarten. Zu Beginn der Klasse Poker (die als »Träger« der WindowsForm eine Erbin der Klasse Form ist) wurde auch die Variable im vom Typ Image deklariert. Die Klasse
256
Entwicklung der Programmierlogik
Image, die im bereits mehrfach besuchten Namensraum System.Drawing organisiert ist, erspart uns die Erzeugung eines Objekts im, ist Image doch von grundlegender Bedeutung. Vor allem aber ist die Klasse abstrakt. Und wenn Sie jemals auf die Idee gekommen sein sollten, eine abstrakte Klasse zu instanziieren ... Unter Umständen bleibt Ihr Bemühen erfolglos.
Um aus einer angegebenen Datei ein Image erzeugen zu können, brauchen Sie die zweifach überladene Methode FromFile(), die Sie unter den Membern der Image-Klasse finden. Im einfachsten Fall ist der Routine der Name des Bildes zu übergeben, gerne auch mit dem absoluten Pfad, so wie hier: im = Image.FromFile(@"C:\Projekte\Draw_Poker\PokerGame\Images\ cbg.gif");
Wichtig Ignorieren Sie das dem Zeichenfolgeliteral vorangestellte @-Zeichen nicht, am wenigsten beim Programmieren. Anderenfalls weiß der Debugger von nicht erkannten EscapeSequenzen (\) zu berichten.
Im Kontext der mit Spannung erwarteten Hand ist das zwischenzeitlich vorhandene Image vor allem für die Image-Eigenschaft der fünf PictureBoxen von Belang, die samt und sonders mit im zu bewerten sind: pb[i].Image = im;
Wichtig Welchen Wert zwischen (einschließlich) 0 und (einschließlich) 4 der Laufindex i auch immer einnimmt – das, was Sie über pb[i] erhalten, ist die jeweilige PictureBox mit allen Eigenschaften, die in der Klasse PictureBox zur Verfügung stehen.
Das gilt auch für die Location-Eigenschaft, deren Eigenschaftswert einer PointStruktur (Namensraum System.Drawing) entspricht. Strukturen sind Werttypen, woran Point nichts ändert. Beim Zugriff auf die Location-Eigenschaft wird eine Kopie der Koordinaten der linken oberen Ecke zurückgegeben. Versuchen wir es: pb[i].Location = new Point(413 + (i * 87), 603);
Augenscheinlich existiert auch rechts vom Gleichheitszeichen der Index i. Aus guten Grund, sollen die Karten der Hand doch versetzt, wenngleich überlappend auf dem virtuellen Tisch liegen. Der Wert 603, als y-Wert der Koordinaten, bleibt über alle fünf Karten hinweg konstant, wohingegen sich die x-Position in der Größenordnung 87 ändert. Ein mathematisches Gesellenstück ist die Zahl nicht, doch
257
8.4
8
Pokern
immerhin: Die Karten überlappen sich. Das sieht zwar nicht ganz klassisch aus, weil es auf einem echten Pokertisch weniger aufgeräumt zugeht, aber Sie wissen, was gemeint ist. Mit einer Verzögerung von 200 Millisekunden sollen die Karten vor des Spielers Nase flattern. Am Ende der for-Schleife steht die Methode delay(), die wir jetzt im bereits dritten oder vierten Projekt verwenden: static void delay(int time) { int t; t = Environment.TickCount; while ((Environment.TickCount - t) < time) Application.DoEvents(); } Listing 8.3
Ein alter Bekannter – die Methode »Delay()«.
Und der Methodenaufruf: delay(200);
Nach der schließenden geschweiften Klammer (im Rumpf der for-Schleife gibt es nichts mehr zu tun) schreiben wir noch eine letzte Zeile: aufnehmen.Enabled = true;
Solange die Karten nicht auf dem Tisch liegen, ist das Aufnehmen derselben schwierig. Deshalb kommt es erst am Ende des Eventhandlers geben_Click() zur Aktivierung der Schaltfläche Aufnehmen. Der vollständige Ereignisbehandler geben_Click() sieht so aus: private void geben_Click(object sender, EventArgs e) { pb = pictureboxList(); for (int i = 0; i < 5; i++) { im = Image.FromFile(@"C:\Projekte\Draw_Poker\Poker Game\Images\cbg.gif"); pb[i].Image = im; pb[i].Location = new Point(413 + (i * 87), 603);
258
Entwicklung der Programmierlogik
delay(200); } aufnehmen.Enabled = true; } Listing 8.4
Schnelle Abfolge – Ausgabe von fünf Karten im EventHander »geben_Click()«
Testen Sie das angefangene Poker-Game-Programm mit ((F5)) oder ohne vorherigen Debug ((Strg)+(F5)). Lassen Sie sich über den Button Geben fünf verdeckte Karten aushändigen (siehe Abbildung 8.11).
Abbildung 8.11
Zur Laufzeit – fünf verdeckte, überlappende Karten im Kartensegment
Aufnehmen können Sie das Blatt gleichwohl nicht. Dafür fehlen uns noch eine Klasse und einige nicht unwichtige Überlegungen im Vorfeld.
8.4.4
Basisarbeit – die Klasse »kartenListe«
Derart von Schnaps umwölkt konnte kein Pokerspieler im Wilden Westen sein, als dass er es nicht gemerkt hätte, wenn er zwei identische Karten vor der Nase hatte – wo auch immer die herkamen. Da hieß es dann nicht »Full House«, sondern, mit gefährlich verengten Augen an den Dealer gewandt, »Ich glaube, du solltest jetzt besser mal aufstehen ...«
259
8.4
8
Pokern
Sie und ich, obgleich wir sicher nüchtern und ebenso nüchtern mit Fragen der Programmierung befasst sind, müssen überlegen, wie in »Draw Poker Light« die riskante Peinlichkeit identischer Karten zu vermeiden ist. Natürlich läuft es auf die zufallerzeugende Klasse Random hinaus. Sie ist in diesem Buch ein alter Bekannter, der allerdings nicht davor gefeit ist, aus einem Pool mit 52 verschiedenen Karten mindestens zweimal dieselbe zu ziehen. Bevor wir gleich für den Pool sorgen, müssen wir noch die Klasse kartenListe schreiben, deren Gerüst Sie bitte über das Hauptmenü mit Projekt und einem Klick auf die Option Klasse hinzufügen anlegen. Im Fenster Neues Element hinzufügen 폷 Poker Game tragen Sie im Eingabefeld Name bitte kartenListe ein (siehe Abbildung 8.12).
Abbildung 8.12 Einrichtung einer neuen Klasse (»kartenListe«) in der Dialogmaske »Neues Element hinzufügen 폷 Poker Game«
Als Nächstes kommt die Implementierung des unverzichtbaren Konstruktors an die Reihe, den Sie auch mit dem Hinzufügen-Button in das Programm aufnehmen Obwohl wir noch ein wenig unter dem Eindruck der aufgepeppten Konstruktorenschaft des Labyrinth-Kapitels stehen, begnügen wir uns zunächst mit der »Basisversion«:
260
Entwicklung der Programmierlogik
public kartenListe() { }
Drei Methoden werden implementiert, aber zu Beginn der Klasse wird trotzdem nichts deklariert, weswegen es gleich mit der Erstellung des Kartenpools weitergeht. Die Methode »RandomKarten()« Der Methodenname ist ... nun ja, er ist, wie er ist, und das gilt auch für das Argument i vom Typ int. Wie Sie auch den Rückgabetyp string, in dem eine von fünf erwarteten Karten zu speichern ist, bitte einfach mal so hinnehmen. Ein Blick auf die Signatur zeigt: string randomKarten(int i){}
Ein besserer Ort als eine generische Liste fällt mir für 52 Karten nicht ein. Bei denen geht es nicht um den Wert, sondern um den Namen, weshalb die Elemente der Liste gleich dem Rückgabetyp der Methode sein sollten: List<string> _randomk = new List<string>();
Um Elemente in eine generische Liste einzufügen, ist die Methode Insert() (ein Member der Klasse List(T)) auch dann eine gute Wahl, wenn sie 52-mal aufgerufen wird – was an dieser Stelle nicht geschieht, wohl aber am Schluss des Kapitels, wo die Klasse kartenListe in Gänze aufgelistet ist. Hier begnügen wir uns mit einer Herz-Acht, die an der Indexposition 0 ihren Platz in der Liste _randomk findet: _randomk.Insert(0, "herzacht");
Vielleicht noch ein Ass mit Herz: _randomk.Insert(1, "herzass");
Wie in Abschnitt 8.4.3 kann auch hier über _randomk[i]
auf das Element – die Karte – an der in i gespeicherten Indexposition zugegriffen werden. Dies ist eine gewollt und notwendigerweise zufällige Angelegenheit, die zu prüfen ist.
261
8.4
8
Pokern
Dem Dealer in die Karten geschaut – die Methode »pruefeKarten()« Stellen Sie sich fünf Karten vor, die einer Methode – hier ist es pruefeKarten() – als Argumente übergeben werden. pruefeKarten(), die eine öffentliche Methode ist, soll einen booleschen Wert, mithin true oder false, zurückgeben. Die Signatur sieht so aus: Boolean pruefeKarten(string k1, string k2, string k3, string k4, string k5){}
Im Methodenrumpf müssen Sie zuerst eine Variable zaehler als int-Typ deklarieren und mit 0 initialisieren – ein überschaubares Problem: int zaehler = 0;
Auch die zu prüfenden Karten halten in eine generische Liste Einzug, die Sie zunächst erzeugen müssen: List<string> _crListe = new List<string>();
Da wir Add() und Insert() im Wechsel einsetzen, ist die Reihe diesmal an Add(): _crListe.Add(k1); _crListe.Add(k2); _crListe.Add(k3); _crListe.Add(k4); _crListe.Add(k5);
Es lässt sich gut an. Grund genug, um weiterzudenken: Ordentlich in einer Liste platziert, gibt es unter den Membern der Klasse List() sicher eine Methode, die die Elemente der Liste auf etwaige Gleichheiten überprüft (schließlich geht es um .NET). Exemplarisch gefragt: Kommt die in k1 gespeicherte Karte innerhalb des Objekts _crListe mehrfach vor? Sie werden keine Methode finden, die Ihnen die Antwort gibt. Wir sind auf uns allein gestellt. Trotzdem ist die Liste nützlich. Für jede der fünf Zufallszahlen muss obige Frage explizit beantwortet werden. Existiert nur eine Karte mehr als einmal, führt sich »Draw Poker Light« ad absurdum. Das ist ein erster Ansatz, können wir doch mittels einer foreach-Anweisung das, was auch immer wir tun, für jedes (string-)Objekt in der Liste _crListe tun: foreach (string st in _crListe){}
Der zweite Ansatz sieht so aus: In den Rumpf der foreach-Anweisung betten wir eine for-Anweisung ein. Jetzt stehen wir hier:
262
Entwicklung der Programmierlogik
foreach (string st in _crListe) { for (int index = 0; index < _crListe.Count; index++) { } }
Auf zwei Wegen wird auf Karten der Liste _crListe zugegriffen: foreach wählt den eher direkten Weg, for den indirekten, also jenen, der über den Index führt. Dabei liefert die Count-Eigenschaft der List(T)-Klasse die Anzahl vorhandener Objekte. Verwechseln Sie das nicht mit dem maximalen Wert, den index annehmen kann. Der ist nämlich 4 und nicht 5, weil natürlich der Laufindex mit 0 startet und somit bei 4 endet (worauf Sie im Verlauf des Projekts immer wieder stoßen werden). Ein wenig kommt uns die Klasse List(T) allerdings entgegen, existiert doch die Methode Equals(), die auf Gleichheit zwischen einem angegebenen Objekt und dem aktuellen, in der Liste befindlichen Objekt prüft. Ausgehend von einem ifKonstrukt soll die Zählvariable zaehler jedes Mal inkrementiert werden (zaehler++), wenn die Prüfung auf Gleichheit erfolgreich war: if (_crListe[index].Equals(st)) { zaehler++; }
Bis zu diesem Punkt wurde der sprichwörtliche Faden abgerollt, jetzt rollen wir ihn auf: index sei 0, doch ist auch in der Variablen st die Karte an Indexposition 0 gespeichert (dort beginnt foreach die Arbeit). Im Falle der durch Equals()geprüften Gleichheit »springt« der Zähler auf 1. Solange es um st an Indexposition 0 geht, sollte sich der Zähler nicht noch einmal rühren, nicht bei index=1, nicht bei index=2 usw. Für st an Indexposition 0 wäre damit die for-Schleife für alle Indizes abgearbeitet, sodass sich bestenfalls und exemplarisch sagen lässt: Herz-Ass existiert in der Liste _crListe genau einmal. Jetzt liefert die foreach-Anweisung das zweite Element. Wieder geht in for der Laufindex index auf Wanderschaft, und wieder – um einen »Checkpoint« herauszunehmen – wird geprüft, ob das zweite Listenelement (das in st gespeichert ist) gleich dem Element ist, das an Indexposition 3 (_crListe[3]) liegt. Und so geht es weiter, bis foreach das letzte Listenelement geliefert und der Methode Equals() zur Prüfung übergeben hat.
263
8.4
8
Pokern
Schauen Sie: foreach (string st in _crListe) { for (int index = 0; index < _crListe.Count; index++) { if (_crListe[index].Equals(st)) { zaehler++; } } } Listing 8.5
»for« und »foreach« im geprüften Zusammenspiel
Welcher Wert sollte am Ende in der int-Variablen zaehler abgespeichert werden? 5, nichts anderes können wir akzeptieren, anderenfalls gibt die Methode pruefeKarten() ein sattes false zurück: if (zaehler > 5) { return false; } else { return true; }
Die vollständig implementierte Routine pruefeKarten() sieht so aus: public Boolean pruefeKarten(string k1, string k2, string k3, string k4, string k5) { int zaehler = 0; List<string> _crListe = new List<string>(); _crListe.Add(k1); _crListe.Add(k2); _crListe.Add(k3); _crListe.Add(k4); _crListe.Add(k5); foreach (string st in _crListe) { for (int index = 0; index < _crListe.Count; index++) {
264
Entwicklung der Programmierlogik
if (_crListe[index].Equals(st)) { zaehler++; } } } if (zaehler > 5) { return false; } else { return true; } } Listing 8.6
Die Methode »pruefeKarten()«
Etwas zu prüfen, ist eine Sache, das womöglich negative Resultat abzuändern, ist eine andere. Bezogen auf das Problem identischer Karten genügt es unter Umständen nicht, die Methode pruefeKarten() nur einmal aufzurufen. Doch sehen Sie selbst ... So lange, bis es passt – die Methode »erstelleKartenliste()« Vorweg: erstelleKartenliste()ist die Methode, die in Kürze im Ereignisbehandler der Schaltfläche Aufnehmen aufzurufen ist. Über etwaige Argumente der Methode (es gibt keine) sagt das nichts aus, wohl aber darüber, was die Routine zurückgibt: naheliegenderweise eine Liste mit fünf Karten. Die Signatur der Methode sieht so aus: List<string> erstelleKartenliste(){}
Erstellen wir am Rumpfanfang fünf string-Variablen, ka1 bis ka5: string ka1, ka2, ka3, ka4, ka5;
Darauf folgt ein Objekt ra vom Typ Random – das an derselben Stelle auch erzeugt wird: Random ra = new Random();
Mit true oder false sind wir immer noch nicht fertig. Deshalb schreiben wir: Boolean res = false;
265
8.4
8
Pokern
Tue das, wenn etwas anderes nicht ist; mache jenes so lange, bis die Bedingung xyz erfüllt ist; tue nichts, wenn andere auch nichts tun – »Algorithmensprache« ist so technokratisch wie schrecklich. Drei Sätze werden Sie dennoch aushalten: 왘
Entnimm dem Kartenpool fünf zufällige Karten.
왘
Lasse die Karten auf etwaige Gleichheiten überprüfen.
왘
Wiederhole den Vorgang so lange, bis fünf verschiedene Karten vorliegen.
Genau das leistet folgendes Codefragment: do { ka1 ka2 ka3 ka4 ka5
= = = = =
randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52));
res = pruefeKarten(ka1, ka2, ka3, ka4, ka5); } while (res == false);
Beide zuvor erstellten Methoden kommen im Anweisungsblock der do-while-Ablaufsteuerung zum Einsatz: randomKarten() fünfmal, pruefeKarten() dagegen nur einmal, aufgerufen mit den in ka1 bis ka5 gespeicherten 5 aus 52 Karten. Ist die Bedingung nicht mehr erfüllt, d. h., ist der Rückgabewert der pruefeKarten()-Methode true, bricht der »Zufallskartendauerlauf« mit 5 wohlunterscheidbaren Karten ab – ganz wie gewollt, ganz wie beabsichtigt. Jetzt müssen wir noch die Karten in einer weiteren, generischen Liste unterbringen: List<string> _kl = new List<string>(); Add() oder Insert(), auch bei dieser Liste ist das die Frage. Wir wählen Insert(): _kl.Insert(0, _kl.Insert(1, _kl.Insert(2, _kl.Insert(3, _kl.Insert(4,
ka1); ka2); ka3); ka4); ka5);
Jede Karte hat ihren Platz in der Liste _kl. Und die geben wir, am Ende der Methode angekommen, zurück: return _kl;
266
Entwicklung der Programmierlogik
Hier ist der »Panoramablick« auf die Methode erstelleKartenliste(): public List<string> erstelleKartenliste() { string ka1, ka2, ka3, ka4, ka5; Random ra = new Random(); Boolean res = false; do { ka1 ka2 ka3 ka4 ka5
= = = = =
randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52));
res = pruefeKarten(ka1, ka2, ka3, ka4, ka5); } while (res == false); List<string> _kl = new List<string>(); _kl.Insert(0, _kl.Insert(1, _kl.Insert(2, _kl.Insert(3, _kl.Insert(4,
ka1); ka2); ka3); ka4); ka5);
return _kl; } Listing 8.7
Die Methode »erstelleKartenliste()«
Vollständig implementiert, sieht die Klasse kartenListe folgendermaßen aus: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Poker_Game { class kartenListe { public kartenListe() { }
267
8.4
8
Pokern
public string randomKarten(int i) { List<string> _randomk = new List<string>(); _randomk.Insert(0, "herzacht"); _randomk.Insert(1, "herzass"); _randomk.Insert(2, "herzbube"); _randomk.Insert(3, "herzdame"); _randomk.Insert(4, "herzdrei"); _randomk.Insert(5, "herzfuenf"); _randomk.Insert(6, "herzkoenig"); _randomk.Insert(7, "herzneun"); _randomk.Insert(8, "herzsechs"); _randomk.Insert(9, "herzsieben"); _randomk.Insert(10,"herzvier"); _randomk.Insert(11,"herzzehn"); _randomk.Insert(12,"herzzwei"); _randomk.Insert(13,"karoacht"); _randomk.Insert(14,"karoass"); _randomk.Insert(15,"karobube"); _randomk.Insert(16,"karodame"); _randomk.Insert(17,"karodrei"); _randomk.Insert(18,"karofuenf"); _randomk.Insert(19,"karokoenig"); _randomk.Insert(20,"karoneun"); _randomk.Insert(21,"karosechs"); _randomk.Insert(22,"karosieben"); _randomk.Insert(23,"karovier"); _randomk.Insert(24,"karozehn"); _randomk.Insert(25,"karozwei"); _randomk.Insert(26,"kreuzacht"); _randomk.Insert(27,"kreuzass"); _randomk.Insert(28,"kreuzbube"); _randomk.Insert(29,"kreuzdame"); _randomk.Insert(30,"kreuzdrei"); _randomk.Insert(31,"kreuzfuenf"); _randomk.Insert(32,"kreuzkoenig"); _randomk.Insert(33,"kreuzneun"); _randomk.Insert(34,"kreuzsechs"); _randomk.Insert(35,"kreuzsieben"); _randomk.Insert(36,"kreuzvier"); _randomk.Insert(37,"kreuzzehn"); _randomk.Insert(38,"kreuzzwei"); _randomk.Insert(39,"schaufelacht"); _randomk.Insert(40,"schaufelass");
268
Entwicklung der Programmierlogik
_randomk.Insert(41,"schaufelbube"); _randomk.Insert(42,"schaufeldame"); _randomk.Insert(43,"schaufeldrei"); _randomk.Insert(44,"schaufelfuenf"); _randomk.Insert(45,"schaufelkoenig"); _randomk.Insert(46,"schaufelneun"); _randomk.Insert(47,"schaufelsechs"); _randomk.Insert(48,"schaufelsieben"); _randomk.Insert(49,"schaufelvier"); _randomk.Insert(50,"schaufelzehn"); _randomk.Insert(51,"schaufelzwei"); return _randomk[i]; } public Boolean pruefeKarten(string k1, string k2, string k3, string k4, string k5) { int zaehler = 0; List<string> _crListe = new List<string>(); _crListe.Add(k1); _crListe.Add(k2); _crListe.Add(k3); _crListe.Add(k4); _crListe.Add(k5); foreach (string st in _crListe) { for (int index = 0; index < _crListe.Count; index++) { if (_crListe[index].Equals(st)) { zaehler++; } } } if (zaehler > 5) { return false; }
269
8.4
8
Pokern
else { return true; } } public List<string> erstelleKartenliste() { string ka1, ka2, ka3, ka4, ka5; Random ra = new Random(); Boolean res = false; do { ka1 ka2 ka3 ka4 ka5
= = = = =
randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52)); randomKarten(ra.Next(52));
res = pruefeKarten(ka1, ka2, ka3, ka4, ka5); } while (res == false); List<string> _kl = new List<string>(); _kl.Insert(0, _kl.Insert(1, _kl.Insert(2, _kl.Insert(3, _kl.Insert(4,
ka1); ka2); ka3); ka4); ka5);
return _kl; } } } Listing 8.8
8.4.5
Für eine saubere Hand – die Klasse »kartenListe«
Zurück in der Klassendatei »Poker.js«
Dank des Eventhandlers geben_Click() liegen fünf Karten auf dem Tisch. Vom Standpunkt der Programmierung betrachtet, ist das zwar nicht richtig, doch sehen wir es trotzdem so: Unter den ausgegebenen Karten gibt es nicht einmal zwei, die gleich sind. Dafür sorgt die Klasse kartenListe, genauer gesagt die Methode erstelleKartenliste().
270
Entwicklung der Programmierlogik
Groß ist der Unterschied nicht zwischen dem, was sich im Ereignisbehandler des Buttons Geben und dem der Schaltfläche Aufnehmen tut. Im Rumpf des Handlers, den Sie fix durch einen Doppelklick auf den Button Aufnehmen erstellen, geht es mit der Instanziierung der Klasse kartenListe los: kartenListe kl = new kartenListe();
Das Klassenmember kartenListe() gibt eine Liste mit fünf Karten zurück, deren Speicherung in der Variablen kliste erfolgt: kliste = kl.erstelleKartenliste();
Vergessen wir nicht jene Methode, über die wir an unsere PictureBoxen herankommen: pb = pictureboxList();
Sie werden sich denken können, was nun folgt: eine for-Anweisung (zur Klärung der Hintergründe siehe Abschnitt 8.4.3). Doch etwas ist anders, geht es im Rumpf der Schleife doch eher darum, das mit fünf PictureBoxen verknüpfte Kartenrückseitenimage zugunsten fünf verschiedener Kartenvorderseitenimages auszutauschen. Das heißt: Wir müssen mithilfe der Dispose()-Methode zunächst das bestehende Image zerstören: pb[i].Image.Dispose();
Betreiben wir Flickschusterei. Wo? In der Image-Klassenmethode FromFile(), deren Argument natürlich auch hier primär aus dem Dateinamen des relevanten Bildes besteht. In Wahrheit sind es fünf Bilder (für eine Hand), die in der Liste hinterlegt sind, die erstelleKartenliste()bereitstellt. Flickschustern müssen wir deshalb werden, weil kliste keine Dateinamenextensions vorhält. Und wir dürfen es tun, weil es zum Handwerkszeug jedes Programmierers gehört, ein ausgedehntes Literal aus Teilzeichenfolgen und Variablen entstehen zu lassen – was hiermit getan ist: im = Image.FromFile(@"C:\Projekte\Draw_Poker\Poker Game\Images\Karten\" + kliste[i] + ".gif");
Von i=0 bis i=4 vervollständigt der Ausdruck kliste[i] den Weg zum Image für jede PictureBox. Den Rest kennen Sie: Weisen Sie die Variable im an die ImageEigenschaft der in pb[i]versteckten PictureBox zu; weisen Sie der Location-Eigenschaft über die Point-Struktur Werte zu und schreiben Sie schließlich: pb[i].Enabled = true;
Nein, das kennen Sie nicht. Doch wie sinnvoll ist es, aus einer mit der Rückseite nach oben liegenden Kartenhand jene Karten auszuwählen, die getauscht werden sollen? Im 200 Millisekunden-Takt (am Ende des Handlers tritt die buchbekannte
271
8.4
8
Pokern
Methode delay() abermals auf den Plan) dreht die Routine aufnehmen_Click() die Karten um. Auch programmseitig kann erst danach getauscht werden, d. h., für alles Weitere sind die Karten, mit einer auf true gesetzten Enabled-Eigenschaft, bestens vorbereitet. Damit gibt es im Rumpf der for-Anweisung nichts mehr zu tun, wohl aber außerhalb. Nach ungefähr einer Sekunde wissen Sie, was die vor Ihnen liegende Hand taugt. Die Vorgänge Geben und Aufnehmen, initiiert über die gleichnamigen Schaltflächen, sind abgeschlossen. Damit können die beiden Buttons zunächst deaktiviert und kann das Drehfeld aktiviert werden: geben.Enabled = false; aufnehmen.Enabled = false; numericUpDown1.Enabled = true;
Jetzt ist Ihr Einsatz gefragt, der nur dann getätigt werden kann, wenn auch das Drehfeld Einsatz in $ aktiviert ist: numericUpDown1.Enabled = true;
Hier sehen Sie die Operationen im Eventhandler aufnehmen_Click()in Gänze: private void aufnehmen_Click(object sender, EventArgs e) { kartenListe kl = new kartenListe(); kliste = kl.erstelleKartenliste(); pb = pictureboxList(); for (int i = 0; i < 5; i++) { pb[i].Image.Dispose(); im = Image.FromFile(@"C:\Projekte\Draw_Poker\Poker Game\Images\Karten\" + kliste[i] + ".gif"); pb[i].Image = im; pb[i].Location = new Point(413 + (i * 87), 603); pb[i].Enabled = true; delay(200); } geben.Enabled = false; aufnehmen.Enabled = false; numericUpDown1.Enabled = true; } Listing 8.9
272
Der Eventhandler »aufnehmen_Click()«
Entwicklung der Programmierlogik
Gerade hier empfiehlt sich ein Probelauf. Richtig programmiert werden Sie haben, doch stimmen die Überlegungen zur Frage identischer Karten? Stimmt das vor allem in Abschnitt 8.4.4 umgesetzte Konzept, der Algorithmus en miniature? Ein »Härtetest«: 왘
Setzen Sie vorläufig die Enabled-Eigenschaft des Buttons Geben wieder auf true.
왘
Reduzieren Sie den Kartenpool, indem Sie in der Methode erstelleKartenliste() (Klasse kartenListe) 52 durch 7 ersetzen.
왘
Starten Sie die Anwendung über den Hotkey (Strg)+(F5).
Lassen Sie sich so oft wie gewünscht aus dem reduzierten Pool von nunmehr sieben Karten fünf auf den Tisch liegen. Im Wesentlichen ändern sollte sich lediglich die Reihenfolge der Karten, von gleichen Karten ganz zu schweigen (siehe Abbildung 8.13).
Abbildung 8.13
Zur Laufzeit nur Herzen im Kartensegment
Tauchen wider aller Erwartung ungebetene Zwillinge (oder gar Drillinge) in der Hand auf, gönnen Sie bitte trotzdem dem Dealer seine Unversehrtheit. Eine gepfefferte E-Mail an mich tut es auch.
273
8.4
8
Pokern
8.4.6
Tauschgeschäfte 1 – EventHandling für fünf PictureBoxen
Ihr Programm muss wissen, wie viele und welche der fünf Karten zum Austausch vorgesehen sind. Doch auch Sie sollten, allein aus Gründen des Spielkomforts, nicht in die Situation kommen, sich die ausgewählten Karten merken zu müssen. Offline- wie Online-Pokerspiele beschreiten hier unterschiedliche Wege, angefangen von schnöden Buttons, die über die angezeigten Karten gesetzt werden (Halten, Tauschen etc.), über aufgelegte »Farbfilter« bis hin zu einer Karte, die, ebenfalls als Folge eines Click-Ereignisses, sich plötzlich einige Pixel über den anderen befindet. Wir setzen die dritte Möglichkeit um, was einfacher ist, als Sie vielleicht ahnen. Denn wozu gibt es die Location-Eigenschaft der PictureBox-Klasse? Und wozu gibt es die Möglichkeit, durch einen Doppelklick auf pictureBox1 bis pictureBox5 entsprechende Ereignisbehandler zu generieren? Das sollten Sie spätestens hier getan haben. Hinweis Hinterfragen Sie den schwingenden Ton der Selbstverständlichkeit! Denn beileibe nicht jedes Control reagiert auf ein Click- oder OnClick-Ereignis. Was gibt Ihnen also Aufschluss über erlaubte Ereignisse? Die Steuerelementklasse selbst. Übrigens, Ereignisse gehören genauso zu den Membern einer Klasse wie Methoden und/oder Eigenschaften.
Nehmen wir den ersten von fünf Handlern, pictureBox1_Click(). Tragen Sie im Rumpf Folgendes ein: pictureBox1.Location = new Point(413, 590);
Geändert (um 13 Pixel) hat sich lediglich die y-Koordinate. An der x-Koordinate der ersten PictureBox dagegen ändert sich nichts, was auch für die restlichen vier PictureBoxen gilt: PictureBox2.Location pictureBox3.Location pictureBox4.Location pictureBox5.Location
= = = =
new new new new
Point(500, Point(587, Point(674, Point(761,
590); 590); 590); 590);
Hat der Spieler eine Karte angeklickt, geht es im wörtlichen Sinne nach oben, womit die Karte für den Tausch »markiert« ist. Ebenso wenig zwingend wie unsinnig ist es, im Anschluss die angeklickte PictureBox für die Verarbeitung weiterer Click-Ereignisse zu sperren: pictureBox1.Enabled = false;
Mindestens eine Karte muss getauscht werden. Das ist nur bei einer aktivierten Schaltfläche Tauschen möglich:
274
Entwicklung der Programmierlogik
tauschen.Enabled = true;
Was die Ereignisbehandlung der PictureBoxen anbelangt, sind wir zunächst fertig. Das Zwischenergebnis, exemplarisch für pictureBox1, sieht so aus: private void pictureBox1_Click(object sender, EventArgs e) { pictureBox1.Location = new Point(413, 590); pictureBox1.Enabled = false; tauschen.Enabled = true; } Listing 8.10
Wenig Arbeit im EventHandler »pictureBox1_Click()«
War der eventuell gewünschte Debug ((F5), anderenfalls drücken Sie (Strg)+(F5)) erfolgreich, können Sie Herz-Dame, Herz-Bube oder was auch immer Ihnen tauschenswert erscheint mit einem Klick auf die linke Maustaste »springen« lassen. Solange wir allerdings nicht beim Eventhandler des Buttons Tauschen angekommen sind, werden die Spielkarten angehoben bleiben (siehe Abbildung 8.14).
Abbildung 8.14 angehoben.
Laufzeitansicht – die Karten Herz-Dame und der Herz-Bube wurden
275
8.4
8
Pokern
Und noch einmal: Die fünf Behandlungsroutinen sind damit nicht komplett. Erst nach Abschluss des nächsten Abschnitts kann die Implementierung komplettiert werden. In dem Abschnitt geht es um die Erstellung einer Klasse, mit der – vor allem unter dem Gesichtspunkt zu vermeidender Identitäten – bis zu fünf Karten getauscht werden können.
8.4.7
Tauschgeschäfte 2 – die Klasse »tauscheKarten«
Erzeugen Sie über die beschriebene Benutzerführung (siehe Abschnitt 8.4.4) der Entwicklungsumgebung das Gerüst für eine neue Klasse. Deren Name soll tauscheKarten sein. Damit ist die Aufgabe der aus drei öffentlichen (public) Routinen bestehenden Klasse bereits bekannt. Wir beginnen damit, eine private Instanz der in Abschnitt 8.4.4 entwickelten Klasse kartenListe zu bilden: private kartenListe kl = new kartenListe();
Implementiert in der Methode randomKarten(), enthält kl den Kartenpool, aus dem bis zu fünf Karten entnommen werden. Demnach ist eine private Instanz der Klasse Random angezeigt: private Random ra = new Random();
Bis hierhin gibt es keinerlei Schwierigkeiten, was natürlich auch bei der Bereitstellung des Standardkonstruktors so bleibt: public tauscheKarten() { }
So weit das Vorgeplänkel. Zwei Listen und ein Gleichheitsfall – die Methode »pruefeTausch()« Der Button Geben brachte Ihnen fünf Karten auf den Bildschirm (siehe Abschnitt 8.4.3), die über die Schaltfläche Aufnehmen aufgenommen wurden (siehe Abschnitt 8.4.5). Stellen Sie sich nun ein einfaches Szenario und Ihre Vorsicht vor. Denn nur eine Karte wollen Sie tauschen, vielleicht weil das Blatt bereits ein Paar (One Pair) oder gar einen Drilling (Three of a Kind) zeigt. Welche beiden Fälle müssen zwingend abgefangen werden? 왘
276
Aus dem Pool wurde ausgerechnet die Karte gezogen, die ausgetauscht werden soll. Faktisch findet kein Tausch, wohl aber der große Ärger statt.
Entwicklung der Programmierlogik
왘
Zwar ist die neue Karte ungleich der zum Tausch vorgesehenen, doch existieren fortan zwei identische Karten.
Die Signatur der Methode pruefeTausch() sieht so aus: Boolean pruefeTausch(int b, List<string> _l,List<string> _li){}
Die Methode pruefeTausch() gibt einen booleschen Wert zurück und erwartet drei Parameter: Der erste (b) ist vom Typ int, die beiden anderen (_l und _li) sind vom Typ List<String>. Hinter b verbirgt sich nichts anderes als die angeklickte PictureBox. _l nimmt die Kartenliste nach dem Tausch auf, _li jene vor dem Tausch. Analog zur Methode pruefeKarten() – und nicht minder trivial – erfolgen die Vereinbarung und die Initialisierung eines Zählers: int zaehler = 0;
Hier haben wir beispielsweise die erste PictureBox (pictureBox1) angeklickt, und es wurde eine 0 übergeben. Der dazu gehörende Auftrag: In der Kartenliste soll die an Indexposition 0 liegende Karte getauscht werden. Über _l[b]
lässt sich die neue (alte?) Karte für b=0 ermitteln. Davon haben wir nichts. Weder in Bezug auf den ersten »Unglücksfall« (die neue Karte ist gleich der getauschten) noch im Hinblick auf den zweiten. Doch gibt es mit _li eine zweite Liste, die ursprüngliche nämlich. In der lässt sich über _li[i]
ermitteln, welche Karte an der Indexposition i liegt, woraus sich genauso wenig schließen lässt. Unter der Bedingung allerdings, dass auch i=0 ist, kann über ein if-Konstrukt ermittelt werden, ob der erste Fall eingetreten ist: if (_l[b] == _li[i]) { zaehler++; }
Liefert _l an der Stelle b dieselbe Karte wie _li an Position i, »springt« der Zähler von 0 auf 1. Die Prüfung ist fehlgeschlagen und damit auch der Tausch. Ist die Bedingung nicht erfüllt, d. h., liegen an der definierten Indexposition (0) unterschiedliche Karten, wurde erfolgreich getauscht. Gäbe es den zweiten Fall nicht,
277
8.4
8
Pokern
wäre, abgesehen von true oder false als zu Recht vermuteter Rückgabe, die Arbeit im Rumpf der Methode pruefeTausch() getan. Auch wenn die Variable zaehler für b=i=0 (»mathematische Schreibweise«) auf dem Default-Wert geblieben ist – läuft i über den gesamten Indexbereich, kann sich das schnell ändern. Denn nichts sagt uns, dass die erfolgreich getauschte Karte unter den vier anderen nicht bereits existiert. Also läuft es auch in der zweiten Prüfmethode auf eine for-Anweisung hinaus: for (int i = 0; i < _li.Count; i++) { if (_l[b] == _li[i]) { zaehler++; } }
Lieber Leser, liebe Leserin, vergessen Sie das zuvor Geschriebene. Eine erste Prüfung fand nie statt. Bringen Sie unter der Bedingung den Laufindex i auf Trab. Und diesmal wollen Sie die zweite Karte (vielleicht Herz-Bube) tauschen. Verwirrenderweise gilt somit: b=1;
Die Karte hinter _l[b] darf es in der ursprünglichen Liste _li an keiner Indexposition geben, mithin hat am Ende des Durchlaufs zaehler=0; ,
zu gelten. Dann und nur dann wurden beide Fälle erfolgreich geprüft. Das heißt, _l[b] erbrachte eine Karte, die sowohl von _li[i=1] (»mathematische Schreib-
weise«) als auch von allen anderen Karten der ursprünglichen Liste verschieden ist. Nunmehr ist die Methode pruefeTausch()vollständig, und so sieht sie aus: public Boolean pruefeTausch(int b,
List<string> _l, List<string> _li)
{ int zaehler = 0; for (int i = 0; i < _li.Count; i++) { if (_l[b] == _li[i]) { zaehler++;
278
Entwicklung der Programmierlogik
} } if (zaehler == 0) { return true; } else { return false; } } Listing 8.11
Die Methode »pruefeTausch()«
Kommen wir zum nächsten Schritt ... Endlich wird getauscht – die Methode »tauscheKarten()« Es stimmt: Irgendwo sollte die Methode pruefeTausch() zum Einsatz kommen, sprich aufgerufen werden. Am besten dort, wo der Kartentausch tatsächlich stattfindet. Kein anderer Ort als die Methode tauscheKarten() kann das sein – zumindest bei uns. Die Methode gibt eine Liste zurück, nämlich jene mit den getauschten Karten. Gleichwohl empfängt die Routine eine Liste, vollgestopft mit fünf Karten, die noch nicht geändert wurden. Daneben erwartet tauscheKarten() eine int-Variable, pb, in der die Nummer der angeklickten PictureBox gespeichert ist. Die Signatur sieht so aus: List<string> kartenTausch(int pb, List<string> _tkl){}
Deklariert und mit einem Leerstring initialisiert (kein Einzelfall in unserem Projekt), sei ein Feld vom Typ string: string neueKarte = "";
Darauf folgt eine Boolean-Variable Res, die mit false initialisiert ist: Boolean Res = false;
Empfängt die zuvor erstellte Methode pruefeKarten() neben der manipulierten Liste (die in _l gespeichert ist) auch die ursprüngliche Liste (_li), fehlt uns etwas, wird kartenTausch() doch lediglich mit der ursprünglichen Liste _tkl aufgerufen.
279
8.4
8
Pokern
Kopieren Sie die Seite eines Buches, ist das Ergebnis in der Regel so flach wie die kopierte Seite selbst. Erstellen Sie dagegen eine sogenannte flache Kopie einer generischen Liste, ist das nur möglich, wenn die Elemente der Liste einem Werttyp entsprechen – was bei string zum Glück der Fall ist. Der Kopiervorgang selbst ist schnell erledigt: List<string> _ckl = new List<string>(_tkl);
Damit wurde für eine zweite Liste gesorgt. Noch sind _tkl und _ckl inhaltsgleich. Um es vorwegzunehmen: Die Änderung erfolgt in _tkl, während _ckl unangetastet bleibt. Entnehmen Sie dem Kartenpool eine Karte. Ein Objekt der Klasse kartenListe haben wir eingangs erstellt, desgleichen ein Objekt der Random-Klasse. Eine string-Variable (neueKarte) zur Aufnahme der Karte haben wir auch. Also denn: neueKarte = kl.randomKarten(ra.Next(52));
Noch ist in der Liste _tkl alles beim Alten. Enthält pb beispielsweise den Wert 2 (demnach wurde pictureBox3 angeklickt), führt _tkl[pb]
zu der Karte, die an Indexposition 2 liegt. Eine einfache Zuweisung bewerkstelligt schließlich den Austausch: _tkl[pb] = neueKarte;
Fortan residiert an Indexposition 2 die in neueKarte hinterlegte Karte. Jetzt kann geprüft werden: res = pruefeTausch(pb , _tkl, _ckl);
Als es um die Prüfung der Kartenliste ging (siehe Abschnitt 8.4.4), half uns eine do-while-Anweisung. Bei den getauschten Karten hilft uns nichts anderes: do { neueKarte = kl.randomKarten(ra.Next(52)); _tkl[pb] = neueKarte; Res = pruefeTausch(pb , _tkl, _ckl); } while (Res == false);
In Worte gefasst: Solange die Methode pruefeTausch() der Variablen res ein false übergibt, wird aus dem Kartenpool eine Karte gezogen und an der Index-
280
Entwicklung der Programmierlogik
position pb der Liste _tkl eingefügt. Irgendwann glückt der Tausch unter den skizzierten Bedingungen. Dann bleibt der Methode nichts anderes übrig als die Rückgabe der Liste: return _tkl;
Hier sehen Sie noch einmal die Methode kartenTausch(): public List<string> kartenTausch(int pb, List<string> _tkl) { string neueKarte = ""; Boolean res = false; List<string> _ckl = new List<string>(_tkl); do { neueKarte = kl.randomKarten(ra.Next(52)); _tkl[pb] = neueKarte; res = pruefeTausch(pb , _tkl, _ckl); } while (res == false); return _tkl; } Listing 8.12
Die Methode »kartenTausch()«
Damit wäre die Klasse tauscheKarten fertig implementiert. Hier ist das Ergebnis: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Poker_Game { class tauscheKarten { private kartenListe kl = new kartenListe(); private Random ra = new Random(); public tauscheKarten() { }
281
8.4
8
Pokern
public Boolean pruefeTausch(int b,
List<string> _l, List<string> _li)
{ int zaehler = 0; for (int i = 0; i < _li.Count; i++) { if (_l[b] == _li[i]) { zaehler++; } } if (zaehler == 0) { return true; } else { return false; } } public List<string> kartenTausch(int pb, List<string> _tkl) { string neueKarte = ""; Boolean res = false; List<string> _ckl = new List<string>(_tkl); do { neueKarte = kl.randomKarten(ra.Next(52)); _tkl[pb] = neueKarte; res = pruefeTausch(pb , _tkl, _ckl); } while (res == false); return _tkl; } } } Listing 8.13
282
Getauscht und unterschieden – die Klasse »tauscheKarten«
Entwicklung der Programmierlogik
Auch die Methode kartenTausch() sollte irgendwo aufgerufen werden. Davon handelt der nächste Abschnitt. Aufruf der Methode »kartenTausch()«
Merkwürdig ist es schon, die Methode kartenTausch() im Rumpf der Handler pictureBox1_Click() bis pictureBox5_Click() (siehe Abschnitt 8.4.6) aufzurufen. Nicht, dass es keinen Sinn macht, schließlich geht es bei der Ereignisbehandlung nicht zuletzt darum, den Tausch der Karte tatsächlich durchzuführen. Allerdings gibt die Methode eine Liste mit getauschten Karten zurück. Im Ereignisbehandler der PictureBox ist das unwichtig. Das folgende Listing verwirklicht für pictureBox1 somit im Eventhandler pictureBox1_Click() den Aufruf der Routine. Dabei müssen Sie kartenTausch() neben dem int-Wert 0 als sträflich einfacher Kennzeichnung dessen, was angeklickt wurde, die in Abschnitt 8.4.4 entwickelte und im Eventhandler aufnehmen_Click() erhaltene Kartenliste kliste übergeben: private void pictureBox1_Click(object sender, EventArgs e)
{
getauscht = tk.kartenTausch(0, kliste); pictureBox1.Location = new Point(413, 590); pictureBox1.Enabled = false; tauschen.Enabled = true; } Listing 8.14
Aufruf der Methode »kartenTausch()« im EventHandler von »pictureBox1«
In den restlichen vier Eventhandlerns muss das erste Argument der Methode kartenTausch() natürlich angepasst, d.h. mit 1, 2, 3, 4 angebenen werden. Erinnern Sie sich, wo das Feld getauscht (vom Typ List<string>) innerhalb der Poker-Klasse deklariert wurde? Auf jeden Fall geschah das außerhalb der beteiligten Routinen, mithin global. Deshalb können im Ereignisbehandler der Schaltfläche Tauschen die in getauscht enthaltenen Elemente ausgelesen werden. Genau das geschieht im nächsten Abschnitt.
8.4.8
Tauschgeschäfte 3 – das finale Ereignis
Wirklich final ist das Ereignis nicht, ist doch lediglich die Anzeige getauschter und/oder nicht getauschter Karten als Reaktion auf den angeklickten Button Tauschen gemeint. Mithin geht es um ein aus fünf Karten bestehendes Kartendeck
283
8.4
8
Pokern
(eine Hand – siehe Abschnitt 8.1.2), dessen spielbetreffende Güte alsbald zu überprüfen ist. Der gesunde Programmiererverstand sagt Ihnen: Die Logik hinter dem Button Tauschen kann keine grundlegend andere sein als die der beiden ersten Schaltflächen. Dessen ungeachtet, muss es einen entscheidenden Unterschied geben. Es gibt ihn: im = Image.FromFile(@"C:\Projekte\Draw_Poker\Poker mages\Karten\" + getauscht[i] + ".gif");
Der implementierte Eventhandler tauschen_Click() sieht so aus: private void tauschen_Click(object sender, EventArgs e) { pb = pictureboxList(); for (int i = 0; i < 5; i++) { pb[i].Image.Dispose(); pb[i].Image = null; im = Image.FromFile(@"C:\Projekte\Draw_Poker\Poker Game\Images\Karten\" + getauscht[i] + ".gif"); pb[i].Image = im; pb[i].Location = new Point(413+(i * 87), 603); delay(200); pb[i].Enabled = false; } tauschen.Enabled = false; geben.Enabled = true; } Listing 8.15
Der EventHandler »tauschen_Click()«
So weit im Rumpf des Eventhandlers tauschen_Click()vorangekommen, können wir auch die Methode kartenTausch() unter erschwerten Bedingungen testen. Test der Routine »kartenTausch()« Beim dritten Button setzen Sie alles auf eine Karte – und sogar im wörtlichen Sinne. Reduzieren Sie den Kartenpool abermals, indem Sie in der Methode er-
284
Entwicklung der Programmierlogik
stelleKartenliste() (Klasse kartenListe) 52 durch 6 (und nicht durch 7 wie beim ersten Test) ersetzen. Das Gleiche tun Sie in der Methode kartenTausch() der Klasse tauscheKarten().
Starten Sie die Anwendung »Draw Poker Light« in der Debugger-Umgebung ((F5)). Klicken Sie erst auf den Button Geben, dann auf die Schaltfläche Aufnehmen. Fünf Karten liegen auf dem Tisch, die aus einem Pool mit sechs Karten gewählt wurden. (Bei 0 beginnend, kann Random.Next(6) maximal eine 5 übergeben, nicht aber eine 6!) Das weitere Vorgehen: 왘
Notieren Sie sich die angezeigten Karten. Beispiel: Herz-Acht, Herz-Fünf, Herz-Dame, Herz-Bube, Herz-Drei. Folglich wurde dem Pool kein Herz-Ass entnommen.
왘
Klicken Sie auf die Herz-Acht (siehe Abbildung 8.15) und im Anschluss auf die Schaltfläche Tauschen.
Um das Ergebnis vorwegzunehmen: Unabhängig davon, welche Karte getauscht wurde, sollte die neue Karte ein Herz-Ass zeigen (siehe Abbildung 8.16). Keine andere Karte kann vergeben werden.
Abbildung 8.15
Die Karte Herz-Acht wurde zwecks Tausch angehoben ...
Abbildung 8.16
... und gegen ein Herz-Ass eingetauscht.
285
8.4
8
Pokern
Um den Ereignisbehandler tauschen_Click() vervollständigen zu können, sind weitere Klassen notwendig. Den Anfang macht jene, in der nichts anderes als mögliche Kartenkombinationen – Hände – implementiert werden. Dahinter jedoch steckt mehr als ein Mustervergleich, den Sie vermutlich erwarten.
8.4.9
Reduzierte Menge – die Klasse »kartenKombinationen«
Warum in der Klasse kartenKombinationen die Menge möglicher Kartenkombinationen reduziert werden kann – hier wird es noch nicht verraten. Genauso wenig verrate ich Ihnen jetzt den Weg, auf dem Sie zu einem vorerzeugten Klassenrumpf nebst Namensraum und using-Direktiven gelangen. Den kennen Sie nämlich. Falls nicht, klicken Sie im Hauptmenü Ihrer Entwicklungsumgebung auf Projekt. Dort finden Sie einen kleinen »Wegweiser«. Nachdem Sie die Datei kartenKombinationen.cs geöffnet haben, geht es (und das nicht zum ersten Mal) mit der Implementierung des Standardkonstruktors weiter: public kartenKombinationen() { }
Daneben benötigen wir eine einzige Methode – und ein weiteres Unterkapitel. Die Methode »kartenKom()« Die Methode kartenKom() gibt ein geschachteltes string-Array zurück, ein Array also, dessen Elemente wiederum aus Arrays bestehen, deren sechs Elemente vom Elementartyp string sind. Ein Blick auf die Signatur: string[][] kartenKom(){}
Im Rumpf der Methode ist nichts weiter als ein gleichfalls verschachteltes Array, kartenK, zu vereinbaren. Entscheidend bei unserem Array aus Arrays (so lautet eine andere, nicht besser klingendere Bezeichnung) sind die 6 Elemente der Unterarrays, die aus 왘
dem Namen der Kartenkombination (siehe Abschnitt 8.1.2) einerseits und
왘
fünf exemplarischen Karten andererseits bestehen.
왘
Ein Beispiel:
new string[]{"Royal Flush","herzass","herzkoenig", "herzdame", "herzbube","herzzehn"}
286
Entwicklung der Programmierlogik
Hier setzt sich, als eine von vier Möglichkeiten, der höchst unwahrscheinliche Royal Flush aus Herz-Ass, Herz-König, Herz-Dame, Herz-Bube sowie einer HerzZehn zusammen. Ein weiteres Beispiel – diesmal für einen Straight Flush: new string[]{"Straight Flush ,"herzdrei","herzfuenf", "herzacht", "herzdame","herzbube"}
Eine von über dreißig Möglichkeiten (siehe Abschnitt 8.1.2) wurde implementiert. Packen wir als nächstes Royal- und Straight Flush in das übergeordnete Array kartenK: string[][] kartenK = { new string[]{"Royal Flush","herzass","herzkoenig", "herzdame", "herzbube","herzzehn"}, new string[]{"Straight Flush ,"herzdrei","herzfuenf", "herzacht", "herzdame","herzbube"} };
Und natürlich ist es die Auflistung kartenK, die schlussendlich zurückzugeben ist: return kartenK;
Am Beispiel von zehn repräsentativen Händen sieht die Methode kartenKom()folgendermaßen aus: public string[][] kartenKom() { string[][] kartenK = { new string[]{"Royal Flush","herzass","herzkoenig", "herzdame", "herzbube","herzzehn"}, new string[]{"Straight Flush ,"herzdrei","herzfuenf", "herzacht", "herzdame","herzbube"} new string[]{"For Of A Kind","herzass","kreuzass", "schaufelass", "karoass","kreuzsechs"}, new string[]{"Full House","herzkoenig","kreuzkoenig", "karokoenig", "kreuzneun","karoneun"}, new string[]{"Flush","herzdrei","herzfuenf", "herzacht", "herzneun","herzkoenig"}, new string[]{"Straight","herzsieben","kreuzacht", "karoneun", "herzzehn","kreuzbube"}, new string[]{"Three Of A Kind","kreuzdame","herzdame", "schaufeldame", "herzacht","kreuzvier"},
287
8.4
8
Pokern
new string[]{"Two Pair","karobube","schaufelbube", "kreuzacht", "schaufelacht","schaufelass"}, new string[]{"One Pair","schaufelsechs","herzsechs", "herzdrei", "karosieben","karozwei"}, new string[]{"Hight Card","herzkoenig","kreuzbube", "kreuzacht", "karosieben","schaufeldrei"} }; return kartenK; } Listing 8.16
Repräsentative Hände in der Methode »kartenKom()«
Die »vollständig« implementierte Klasse kartenKombinationen sieht so aus: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Poker_Game { public class kartenKombinationen { public kartenKombinationen() { } public string[][] kartenKom() { string[][] kartenK = { new string[]{"Royal Flush","herzass","herzkoenig", "herzdame", "herzbube","herzzehn"}, new string[]{"Straight Flush","herzdrei", "herzfuenf","herzacht", "herzdame","herzbube"}, new string[]{"For Of A Kind","herzass","kreuzass", "schaufelass", "karoass","kreuzsechs"}, new string[]{"Full House","herzkoenig","kreuzkoenig", "karokoenig", "kreuzneun","karoneun"}, new string[]{"Flush","herzdrei","herzfuenf", "herzacht", "herzneun","herzkoenig"}, new string[]{"Straight","herzsieben","kreuzacht", "karoneun", "herzzehn","kreuzbube"},
288
Entwicklung der Programmierlogik
new string[]{"Three Of A Kind","kreuzdame","herzdame", "schaufeldame", "herzacht","kreuzvier"}, new string[]{"Two Pair","karobube","schaufelbube", "kreuzacht", "schaufelacht","schaufelass"}, new string[]{"One Pair","schaufelsechs","herzsechs", "herzdrei", "karosieben","karozwei"}, new string[]{"Hight Card","herzkoenig","kreuzbube", "kreuzacht", "karosieben","schaufeldrei"} }; return kartenK; } } } Listing 8.17
Die Klasse »kartenKombinationen«
Im Array kartenK der Methode kartenKom() wurde für jede der zehn spielrelevanten Hände eine mögliche Kartenkombination erstellt. Zehn von – ausgenommen der Royal Flush mit vier Variationsmöglichkeiten – 2.598.956 Kombinationen wären damit programmseitig abgefangen. Zeit, sich auf die Schulter zu klopfen? Eindeutig nein, denn versuchen Sie einmal, mit vier Sandkörnern eine Sandburg zu bauen ... Am Ende des übernächsten Abschnitts (Abschnitt 8.4.11) werden es gleichwohl mehr geworden sein, mehr Sandkörner – und mehr Kartenkombinationen. Hinweis Spielrelevante Kartenkombinationen in einem verzweigten Array zu »horten« stellt eine von mehreren Möglichkeiten der »fixen Händeverdrahtung« dar. Alternativ können Sie auch eine Datenbank, ein .txt-File, eine generische Liste oder was auch immer einsetzen: »Draw Poker Light« lädt Sie bereits jetzt zur Weiterentwicklung ein.
Weiter geht es – quasi als Kontrast zu den ewigen Klassen – mit einer einfachen Struktur.
8.4.10 Wie viel auf dem Spiel steht – die Struktur »wertigkeitHand« Ob eine Klasse oder eher eine Struktur implementiert werden sollte, ist mit der Frage vergleichbar, wann Sie, als potenzieller Besitzer zweier Autos, den größeren Wagen besser in der Garage lassen. Geht es doch auch in der Programmierung um Ökonomie, um Ressourcen und um Aufwand, der bei Referenztypen (wozu ja auch Klassen zählen) im Hinblick auf die Wahl des Speichers ungleich
289
8.4
8
Pokern
größer ist als bei Werttypen (beispielsweise bei der zu implementierenden Struktur wertigkeitHand). Doch noch immer halten Sie kein klassisches Lehrbuch in den Händen, in dem für Begriffe wie Heap und Stack mehr Platz wäre. Merken Sie sich einfach: Die Verwaltung des Heap-Speichers ist gegenüber der Stack-Verwaltung aufwendiger. Genau dort, auf dem Heap nämlich, »landen« aber unsere Klassenobjekte, weshalb der Aufwand, den der Heap verursacht, in einem ausgewogenen Verhältnis zur Größe des Objekts stehen sollte. Werttypen und ihre Objekte werden dagegen im einfacher konzipierten Stackspeicher organisiert, wobei die Speicherung direkt in den entsprechenden Variablen erfolgt. Daneben werden Strukturen 왘
mit dem Schlüsselwort struct vereinbart,
왘
wobei keine Konstruktoren ohne Argumente (Standardkonstruktor) definiert werden dürfen.
Im Laufe Ihrer Arbeit mit diesem Buch mussten Sie oft Strukturen instanziieren, wobei (hoffentlich) nicht der Eindruck entstanden ist, dass eine Struktur auch vererbt werden könne: Instanzbildung ja, Vererbung eindeutig nein. Auch bei Strukturen ist es sinnvoll, sie in eine gesonderte Datei zu schreiben. Sowohl das .NET-Framework als auch die Entwicklungsumgebung (hier die Visual C# 2010 Express Edition) sind allerdings ein wenig auf Klassen fixiert. Das heißt: Unter den zahlreichen neuen Elementen, die Sie dem Projekt hinzufügen können, suchen Sie eine Strukturvorlage bedauerlicherweise vergeblich. Was tun? Sehr einfach: Legen Sie sich auf bekanntem Wege (siehe Abschnitt 8.4.4) eine Klasse – wertigkeitHand – an. Ersetzen Sie in der erzeugten Datei wertigkeitHand.cs das Schlüsselwort Class durch das Wort struct. Fertig ist das Strukturgerüst, dem Sie einleitend ein privates Feld na vom Typ string spendieren: private string na;
Bei der Erzeugung des struct-Objekts müssen Sie einen Parameter n vom Typ string übergeben. Im Rumpf des Konstruktors wird n dem Feld na zugewiesen (dem das C#-Schlüsselwort this vorangestellt wird): public wertigkeitHand(string n) { this.na = n; }
290
Entwicklung der Programmierlogik
n, somit na, enthält für den Fall die Zeichenfolge Verloren!, dass nach dem
Tausch der Karten nichts als eine vergleichsweise wertlose Hand herausgekommen ist. Anderenfalls enthält es den Namen der Hand (Flush, Straight, Full House etc.). Punkte für zehn Hände – die Methode »wHand()« Die Information, ob und wenn ja durch welche Hand dem Spieler das Glück beschieden ist, hält Einzug in die Routine wHand(). In ihr werden Straight oder Royal Flush, Three of A Kind oder On Pair mit Faktoren assoziiert, die der Wertigkeit entsprechen. Demzufolge haben wir es mit einem int-Typ als Rückgabewert zu tun. Parameter erwartet die Methode indes keine. Somit im Methodenrumpf angekommen, vereinbaren wir zunächst das Folgende: int p = 50;
In dieser simplen Zuweisung zeigt sich bereits des Spielers mögliches Unglück. Da sie mit dem Literal Verloren! erzeugt wurde, gibt es in der Methode wHand() nichts als 50-mal das, was im Pot ist, zu verlieren. Allerdings geschieht das erst später, nachdem wir das Wörterbuch des Pokerns auch in unserem Programm aufgeschlagen haben. Die generische Klasse Dictionary(TKey, TValue) ist ein pfeilschneller SchlüsselWert-Abruf aus dem Namensraum System.Collection.Generics (in dem auch die bereits eingesetzte List(T)-Klasse residiert)! Erinnern Sie sich an das letzte Projekt, wie frappierend einfach es war, ein Objekt der Dictionary(TKey, TValue)Klasse zu erzeugen und anschließend mithilfe der Add()-Methode Schlüssel-WertPaare einzufügen? Ein Projekt weiter, ist TKey vom Typ string, was nicht anderes sein kann, denn aus Sicht des Programms besteht ein Royal Flush aus nichts weiter als einer schnöden Zeichenfolge. In »Draw Poker Light« ist die kaum zu erreichende Königshand 50 Punkte wert, womit wir beim Typ für TValue wären: int. Summa summarum: Dictionary<string, int> _punkteh = new Dictionary<string, int>(); _punkteh.Add("Royal Flush", 50); _punkteh.Add("Straight Flush", 45); _punkteh.Add("For of A Kind", 40); _punkteh.Add("Full House", 35); _punkteh.Add("Flush", 30); _punkteh.Add("Straight", 25);
291
8.4
8
Pokern
_punkteh.Add("Three of A Kind", 20); _punkteh.Add("Two Pairs", 15); _punkteh.Add("One Pair", 10); _punkteh.Add("Hight Card", 5);
Das hätten wir. Was im Weiteren passiert, habe ich bereits angedeutet. Abhängig von dem, was in na gespeichert ist, liefert return entweder p oder den im Dictionary _punkteh gespeicherten Wert der erhaltenen Hand über den Ausdruck _punkteh[na]
zurück. Dabei versteht sich der abermalige Gebrauch einer if-else-Struktur beinahe von selbst: if (na != "Verloren!") { return _punkteh[na]; } else { return p; }
Hier sehen Sie noch einmal die Methode wHand(): public int wHand() { int p = 50; Dictionary<string, int> _punkteh = new Dictionary<string, int>(); _punkteh.Add("Royal Flush", 50); _punkteh.Add("Straight Flush", 45); _punkteh.Add("For of A Kind", 40); _punkteh.Add("Full House", 35); _punkteh.Add("Flush", 30); _punkteh.Add("Straight", 25); _punkteh.Add("Three of A Kind", 20); _punkteh.Add("Two Pairs", 15); _punkteh.Add("One Pair", 10); _punkteh.Add("Hight Card", 5); if (na != "Verloren!") { return _punkteh[na];
292
Entwicklung der Programmierlogik
} else { return p; } } Listing 8.18
Die Methode »wHand()«
Hier sehen Sie die komplette Struktur wertigkeitHand: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Poker_Game { struct wertigkeitHand { private string na; public wertigkeitHand(string n) { this.na = n; } public int wHand() { int p = 50; Dictionary<string, int> _punkteh = new Dictionary<string, int>(); _punkteh.Add("Royal Flush", 50); _punkteh.Add("Straight Flush", 45); _punkteh.Add("For of A Kind", 40); _punkteh.Add("Full House", 35); _punkteh.Add("Flush", 30); _punkteh.Add("Straight", 25); _punkteh.Add("Three of A Kind", 20); _punkteh.Add("Two Pairs", 15); _punkteh.Add("One Pair", 10); _punkteh.Add("Hight Card", 5);
293
8.4
8
Pokern
if (na != "Verloren!") { return _punkteh[na]; } else { return p; } } } } Listing 8.19
Die Struktur »wertigkeitHand()«
Der folgende Abschnitt beantwortet die Frage nach Sinn und Zweck repräsentativer Kartenkombinationen als Alternative zur riesigen Menge möglicher Kombinationen.
8.4.11 Schnelles Plätzetauschen – die Klasse »permutKarten« Ich sollte aufhören, es dort spannend machen zu wollen, wo die Spannung möglicherweise nicht gerechtfertigt ist. Ohnehin verrät die Überschrift, worum es geht: Permutationen, also die Variation der Anordnung einer Menge durch Vertauschen der Elemente. In unserem Fall sind das fünf Spielkarten einer mehr oder weniger gewichtigen Hand. Eine Fakultät ohne Eingang Bereits seit einigen Buchseiten (genauer gesagt seit Abschnitt 8.4.7) existiert eine generische Liste (_tkl), die mit fünf Spielkarten gefüllt ist. Wichtig Für alles Weitere ist es vollkommen unerheblich, wie viele und welche der Pokerkarten getauscht wurden. Entscheidend ist die Option, in der Liste die Reihenfolge der Karten variieren zu können, und noch entscheidender ist die Frage, wie oft das maximal geschehen kann, ohne Wiederholungen zu produzieren. Die Antwort enthält neben dem Buchstaben »n« ein Ausrufezeichen: n!
Nennen wir die Dinge beim Namen: n Fakultät. n gleich 5 gesetzt, lässt sich die Fakultät leicht berechnen: 1 × 2 × 3 × 4 × 5 = 120;
294
Entwicklung der Programmierlogik
Demnach existieren 120 Möglichkeiten, die Reihenfolge der Karten in der Liste _tkl zu vertauschen. Strenger formuliert, beträgt die Gesamtzahl der Permutationselemente der Ordnung n=5 satte 120. Nein, in den Mathematikunterricht hat es uns trotzdem nicht verschlagen. Betrachten Sie Abbildung 8.17. Der Screenshot zeigt die Konsolenausgabe der Klasse, die wir in Kürze erstellen werden. Das Testprogramm berechnete die Permutationen eines string-Arrays mit den Elementen herzbube, herzacht, herzkoenig, herzfuenf, herzdrei.
Abbildung 8.17 Mit System vertauscht – fünf Karten der Startreihenfolge »herzacht«, »herzbube«, »herzdrei«, »herzfuenf«, »herzkoenig«
Wichtig Zu erkennen sind lediglich 25 von 120 Permutationselementen, was weniger einer fehlerhaften Programmierung als der Skalierung des Ausgabefensters zuzuschreiben ist. Doch glauben Sie mir bitte: Ausgegeben werden tatsächlich 120 von 120 möglichen Kombinationen.
Wichtiger jedoch ist: Das erste Permutationselement (herzacht, herzbube, herzdrei herzfuenf, herzkoenig), stellt die alphabetisch sortierte Version der Aus-
gangsliste dar. Das ist eine von zwei Bedingungen, die der Algorithmus stellt. Und das ist eine gute Überleitung zum nächsten Unterkapitel.
295
8.4
8
Pokern
Der Algorithmus Auch ohne Sortierung bestünde das Ergebnis aus Permutationselementen, allerdings wären es deutlich weniger als 120. Zusätzlich verfälscht würde die Ausgabe durch identische Karten. Das ist die zweite Bedingung, für deren Erfüllung die Klassen erstelleKartenliste und tauscheKarten gleichwohl sorgen. Was aber tut ein derart wählerischer Algorithmus? Er sucht. Genauer gesagt sucht er das lexikalische Nachfolgepermuationselement (eine zugegeben grauenvolle Bezeichnung). Er tut dies immer und immer wieder, bis 120 Permutationselemente gefunden sind. Das impliziert selbstredend einen interaktiven Ansatz, ohne den nur wenige Algorithmen zur Berechnung der Permutationselemente auskommen (mir persönlich ist kein einziger bekannt, aber vielleicht Ihnen). Die Schritte im Einzelnen: 왘
Bestimmt werden die durch Indizes ausgedrückten Positionen zweier Elemente: eines linken und eines rechten.
왘
Im Anschluss tauschen die beiden Karten ihre Positionen in der Liste.
왘
Abschließend werden alle Elemente rechts vom linken Index umgekehrt.
Damit wäre das lexikalische Nachfolgeelement ermittelt. Stören Sie sich nicht an dem Begriff, dem am besten über die Systematik beizukommen ist, mit der ein Nachfolgeelement ins Netz geht. Das Vorgehen sei exemplarisch am Beispiel der Liste erläutert, von der ausgehend die in Abbildung 8.17 gezeigten Permuationselemente ermittelt wurden. Auf geht’s! Hier ist noch einmal die alphabetisch sortierte Ausgangsliste (nebenbei bemerkt ein Flush in Herzfarbe): {herzacht, herzbube, herzdrei, herzfuenf, herzkoenig}
Der erste Schritt hin zur geregelten Nachfolge: Suchen wir einen rechten und einen linken Index. Die Indizes verweisen auf Karten, deren Positionen in der Liste zu tauschen sind. Die Ermittlung des linken Index beginnt beim vorletzten Element, also bei herzfuenf. Weiter geht es in linker Richtung bis zu dem ersten Element, das lexikalisch/alphabetisch kleiner als das aktuelle Element ist. Hier ist das herzdrei. Womit wir den ersten – den linken – Index mit i=2
ermittelt hätten.
296
Entwicklung der Programmierlogik
Die Ermittlung des rechten Index führt zunächst zu dem Element, das am weitesten rechts liegt: herzkoenig. Weiter geht es nach links bis zum ersten Element, das größer als jenes Element ist, das hinter dem linken Index steht. Auf welche Karte wir treffen? herzfuenf. Womit wir auch dem rechten Index auf die Spur gekommen wären: i=3.
Jetzt können die Karten hinter i=2 und i=3 im Sinne eines Zwischenergebnisses getauscht werden: {herzacht, herzbube, herzfuenf, herzdrei, herzkoenig}
Im letzten Schritt werden alle Karten, die rechts vom linken Index stehen, der Reihenfolge nach umgekehrt. Aus herzdrei, herzkoenig
wird demnach: herzkoenig, herzdrei.
Das Endergebnis: {herzacht, herzbube, herzfuenf, herkoenig, herzdrei}
Das prinzipielle Vorgehen werden wir jetzt programmieren müssen. Umsetzung des Algorithmus Ihrerseits ist alles Notwendige bereits veranlasst, d. h., Sie haben ein Vorerzeugnis der Klasse permutKarten mithilfe der Dialogführung in der Entwicklungsumgebung generiert. Mit der gleichnamigen Klassendatei (permutKarten.cs) vis à vis geht es mit der Deklaration einer globalen, privaten Variable vom Typ Array of String (string[]) weiter: private string[] pel;
Des ewig gleichen Deklarierens unterschiedlicher Felder müde und überdrüssig, definieren wir zur willkommenen Abwechslung ein Feld vom Typ der Klasse, die wir just implementieren: private permutKarten ergebnis;
Schon ist’s wieder vorbei mit der Abwechslung: Nun müssen wir fünf private Variablen bereitstellen, die samt und sonders vom wahnsinnig aufregenden Urvater aller Datentypen –int- sind: private int links, rechts, i, j, laenge;
297
8.4
8
Pokern
Auch die Erzeugung einer generischen Liste mit string-Elementen ist zwischenzeitlich nichts mehr, was Sie hinter dem Ofen hervorlocken könnte: private List<string> pList = new List<string>();
Selbst den Konstruktor, obgleich er mit einem Parameter und zwei Zeilen versehen ist, verbuchen Sie unter »Alles schon mal da gewesen«: public permutKarten(string[] perliste) { this.pel = perliste; this.laenge = perliste.Length; }
Trotzdem sei folgender Hinweis erlaubt. Hinweis: Was die Zuweisung des Parameters perliste an die string[]-Variable pel betrifft: Natürlich hätte sich die Länge des Arrays perliste damit auch über pel.Length ermitteln lassen. Übrigens erlaubt die Length-Eigenschaft der Array-Klasse ausschließlich den lesenden Zugriff. Helfershelfer – die Methode »permutList()«
Die kleine Routine tut nichts anderes, als eines von 120 möglichen Permutationselementen, die im string-Array pel gespeichert sind, in die generische Liste pList zu überführen. PermutList() erwartet keine Argumente, wohl aber eine Rückgabe in Form der Liste pList. Das ist beinahe zu wenig, um noch explizit die Signatur hinschreiben zu wollen. Dennoch: List<string> permutList(){}
Es ist kein leichtes Unterfangen, ohne ein Schleifenkonstrukt den mehrmaligen Aufruf der Methode Add() zu vermeiden. Wir wählen die for-Schleife: for (int i = 0; i < laenge; i++) { pList.Add(pel[i]); }
Von Indexposition 0 bis 4 werden die fünf Pokerkarten über die Add()-Methode der generischen Liste pList hinzugefügt. Nach getaner Arbeit folgt noch die return-Anweisung: return pList;
298
Entwicklung der Programmierlogik
Hier sehen Sie noch einmal permutList() in vollständiger Form: public List<string> permutList() { for (int i = 0; i < laenge; i++) { pList.Add(this.pel[i]); } return pList; } Listing 8.20
Die Methode »permutList()«
Um einiges interessanter, merkwürdiger und aufwendiger gestaltet sich die Implementierung der Methode, die für den lexikalischen Nachfolger der aufgereihten Karten sorgt. Auch ohne diese Methode ließe sich mit permutKarten arbeiten. Doch bekämen Sie nichts anderes zurück als fünf Karten in der ursprünglichen, d. h. alphabetisch sortierten Reihenfolge – gemessen an der Anforderung ein Ding der Unmöglichkeit. Geregelte Nachfolge – die »Methode Nachfolger()«
Ein Blick genügt, um festzustellen, dass es bereits bei der Signatur mit den Merkwürdigkeiten losgeht: permutKarten Nachfolger(){}
Implementiert in der Klasse permutKarten, ist der Typ der Rückgabe vom Typ permutKarten (global, deklariert im Kopf der Klasse permutKarten, siehe dort das Feld ergebnis). Argumente erwartet die Routine indes keine. Ist die Rückgabe der Routine vom Typ permutKarten, ist die Existenz eines Objekts vom selben Typ zumindest nicht sinnlos. Anders gesagt sollte die Klasse permutKarten wie folgt instanziiert werden: ergebnis = new permutKarten(this.pel);
Ihnen ist klar, dass dies mitnichten an dieser Stelle zum ersten Mal passiert. Anderenfalls gäbe es in pel keine einzige Pokerkarte. Irgendwo muss es also eine erste Instanzbildung der Klasse permutKarten geben. Am besten sollte dies außerhalb der Klasse geschehen, und zwar dort, wo der Konstruktor mit einer wohlsortierten Kartenliste aufgerufen werden kann. Natürlich: Im Programm gibt es die Stelle, an der wir bald ankommen.
299
8.4
8
Pokern
Erreicht ist dagegen bereits der Punkt, wo der int-Variablen links etwas zuzuweisen ist: nämlich die um 2 reduzierte Anzahl an Elementen in der Liste pel: links = ergebnis.laenge-2;
Fünf Karten enthält die Liste pel. Abzüglich 2, lautet der in links gespeicherte Wert 3. Den verstehen wir fortan als Indexposition, womit klar ist, dass pel[3] auf die vorletzte Karte zeigt, jene also, bei der wir die Suche nach dem linken, auszutauschenden Listenelement starten. Hinweis Sie haben recht! pel enthält fünf Elemente, eine Tatsache, die sich nicht ändert, geht es in Draw Poker doch um lediglich fünf Karten. Mithin steht die vorletzte Karte felsenfest an der Indexposition 3, ein int-Wert, mit dem wir das globale Feld links auch direkt hätten initialisieren können. Doch wer weiß: Vielleicht steht Ihnen der Sinn danach, Permutationselemente für Listen mit variabler Länge zu ermitteln. Dann sollten wir besser bei links=ergebnis.lanege-2
bleiben. Einverstanden?
Für ewig und alle Zeiten festgelegt, ist die in links gespeicherte Zahl damit allerdings nicht. Im Gegenteil. Abhängig vom Ergebnis einer Bedingung ist links zu dekrementieren. Wörtlich lautet die aus zwei Teilen bestehende Bedingung: Dekrementiere den Inhalt der Variablen links so lange, wie der Ausdruck pel[links]
einen alphabetisch größeren Wert repräsentiert als pel[links+1].
Der zweite Teil der Bedingung lautet: Dekrementiere links, solange links >= 1
ist. Beide Bedingungen, verknüpft mit einem AND-Operator (&&), finden sich im Kopf einer while-Schleife wieder, in dem zunächst mehr Fragen aufgeworfen als beantwortet werden: while ((ergebnis.pel[links].CompareTo(ergebnis.pel[links + 1])) > 0 && (links >= 1)) { links--; }
Genauso, wie sich mittels Punktoperator auf das private, globale Feld laenge zugreifen lässt (ergebnis.laenge), erhält die Instanz ergebnis als permutKartenObjekt über
300
Entwicklung der Programmierlogik
ergebnis.pel[links]
Zugriff auf das an Indexposition links stehende Element der Liste pel. Nicht anders ist der Ausdruck ergebnis.pel[links+1]
zu erklären. Dieser Ausdruck, als Argument der Methode CompareTo() übergeben, führt erstmalig zum Begiff der Schnittstelle (englisch Interface), denn die Routine CompareTo() ist Member einer Schnittstelle mit dem Namen IComparable (im Namensraum System organisiert). Hinweis Eine namensgleiche Methode findet sich auch in der Struktur Boolean (die ebenfalls im Namensraum System organisiert ist). Was wäre das .NET-Framework ohne Namensräume als erstklassiges Konzept zur Vermeidung von Verwechslungen?
Durch das Interface IComparable wird eine allgemeine Vergleichsmethode definiert, die von einer Klasse oder einem Werttyp für die Erstellung einer typspezifischen, vergleichenden Methode zu implementieren ist. Wobei der Begriff Vergleichsmethode nichts anderes als eben eine einzige Methode meint. Über die hinaus existiert im Interface IComparable nämlich kein weiteres Member. Der folgende Ausdruck verknüpft die Instanz der Klasse permutKarten (ergebnis) mit dem string-Array pel und dieses wiederum mit der Methode CompareTo(): ergebnis.pel[links].CompareTo(ergebnis.pel[links + 1])
Als Argument erwartet CompareTo()ein Element der Liste pel. Ausgedrückt durch ergebnis.pel[links + 1], erhält die Routine genau das, nämlich die Pokerkarte an der Indexposition links+1. Die muss alphabetisch/lexikalisch kleiner sein als jene an der Indexposition links. In diesem Falle gibt die Routine einen Wert größer 0 zurück. Anderenfalls, d. h., gilt pel[links] < pel[links+1]
dann ist der zurückgegebene Wert kleiner 0. Zuzüglich der zweiten Bedingung (links>=1) wäre damit der Augenblick zum Abbruch der while-Schleife gekommen. Das Ergebnis wäre eine im lokalen Feld links gespeicherte Indexposition – die auf was zeigt? Auf das linke Element, auf eine von zwei Pokerkarten, die ausgetauscht werden sollen. Irgendwann muss es gut sein mit der Suche nach lexikalischen Nachfolgeelementen, denn mehr als 120 darf es nicht geben. Auch die Abbruchbedingung besteht
301
8.4
8
Pokern
aus zwei Teilen, wobei der erste eine 0 – und weiter nach links geht es nicht – als Indexposition, mithin die erste Pokerkarte erwartet: links==0
Zeigt der Index auf die erste Spielkarte, gibt die Methode Nachfolger() einen Nullverweis statt des permutKlassen-Objekts ergebnis zurück: return null;
Ausgezeichnet, die Suche ist beendet – bei Permutationselement Numero 24. Lediglich 96 mögliche Variationen der Reihenfolge von fünf Karten fehlen. Die Ironie beiseite gelassen, ist mit dem Resultat die Notwendigkeit einer zweiten Bedingung zumindest verdeutlicht. Ist die Karte an Indexposition links+1 alphabetisch größer als jene an Indexposition links, könnte ein weiteres Permutationselement die Folge sein, nicht aber wenn links==0 und die Spielkarte an Position links+1 kleiner als jene an Indexposition 0 ist. Die zweite Bedingung sieht formal ausgedrückt so aus: this.pel[links].CompareTo( this.pel[links + 1]) > 0
Und hübsch im Kopf einer if-Bedingung verstaut, liest sich das so: if ((links == 0) && (this.pel[links].CompareTo(this.pel[links + 1])) > 0) return null;
Was für die Suche nach dem linken Element bereits umgesetzt wurde, lässt sich leicht auf die rechte Pokerkarte übertragen. Zunächst bestimmen wir die Indexposition für das letzte Element in der Liste (wo die Suche nach der zweiten Pokerkarte, die getauscht werden soll, beginnt): rechts = ergebnis.laenge-1;
In der while-Anweisung soll die erste Pokerkarte aufgespürt werden, die alphabetisch größer als jene am linken Index (links) ist. Somit spielt sich der Vergleich zwischen pel[links] und pel[rechts] ab. Solange ergebnis.pel[links].CompareTo(ergebnis.pel[rechts]) > 0
gilt, wurde keine Karte gefunden, die größer als die am linken Index ist. Der in rechts gespeicherte Wert muss demzufolge dekrementiert werden – bis die Bedingung im Kopf der while-Schleife erfüllt ist:
302
Entwicklung der Programmierlogik
while (ergebnis.pel[links].CompareTo(ergebnis.pel[rechts]) > 0) { rechts--; }
Jetzt, da wir im Besitz eines linken und eines rechten Index sind, kommt als Nächstes das Vertauschen der zugehörigen Pokerkarten an die Reihe, eine vergleichsweise einfache Angelegenheit. Wir beginnen mit der Zwischenspeicherung der linken, durch ergebnis.pel[links]
identifizierbaren Spielkarte in einer lokalen, temporären Variable vom Typ string: string temp = ergebnis.pel[links];
Deklaration und Initialisierung erfolgten in einem Rutsch. Jetzt initialisieren wir ergebnis.pel[links]
mit der am rechten Index (ergebnis.pel[rechts]) liegenden Pokerkarte: ergebnis.pel[links] = ergebnis.pel[rechts];
So können wir das nicht lassen, enthält die Liste pel doch doppelte Elemente. Genauer gesagt, liegt an der Indexposition links die gleiche Karte wie an der Indexposition rechts. Man ist versucht, »worst case« zu schreiben. Allerdings enthält die Variable tmp die linke Pokerkarte, die im letzten Schritt ergebnis.pel[rechts]
zuzuweisen ist: ergebnis.pel[rechts] = temp;
Damit hätten die Kartendinge ihre beabsichtigte Ordnung: Salopp formuliert, wurden links und rechts vertauscht. Jetzt steht die Umkehrung der Pokerkarten ab Indexposition links an. Welche Karte die Position besetzt, ist dabei unerheblich. Umzudrehen ist alles, was sich rechts von Indexposition links befindet. Unter der Prämisse lässt sich eine weitere Indexposition, i, klar definieren, nämlich jene, bei der die Umkehrung der Listenelemente beginnt: i = links+1;
Bei irgendeinem Index muss das Vertauschen der Elemente allerdings auch enden, am besten bei der letzten Pokerkarte. Für die lässt sich prima die Indexposition j angeben: 303
8.4
8
Pokern
j = ergebnis.laenge-1;
Solange i<j
ist, wird 왘
die Pokerkarte an der Indexposition i in der temporären Variable temp gespeichert: temp = ergebnis.pel[i];
die Pokerkarte an Indexposition j der inkrementierten Indexposition i (i++) zugewiesen: ergebnis.pel[i++] = ergebnis.pel[j]; 왘
die in temp gespeicherte Pokerkarte der dekrementierten Indexposition j (j--) zugewiesen: ergebnis.pel[j--] = temp;
Abhängig von der genannten Bedingung ist damit die Umkehrung der Karten realisiert. Zuzüglich der while-Schleife ergibt sich damit: while (i < j) { temp = ergebnis.pel[i]; ergebnis.pel[i++] = ergebnis.pel[j]; ergebnis.pel[j--] = temp; }
Die Rückgabe der Methode Nachfolger() besteht aus einem Objekt ergebnis vom Typ der Klasse permutKarten, in der die Methode Nachfolger() implementiert ist: return ergebnis;
Hier sehen Sie noch einmal die zugegeben etwas verzwickte Klasse permutKarten(): public permutKarten Nachfolger() { ergebnis = new permutKarten(this.pel); links = ergebnis.laenge - 2; while ((ergebnis.pel[links].CompareTo( ergebnis.pel[links + 1])) > 0 && (links >= 1))
304
Entwicklung der Programmierlogik
{ links--; } if ((links == 0) && (this.pel[links].CompareTo( this.pel[links + 1])) > 0) return null; rechts = ergebnis.laenge - 1; while (ergebnis.pel[links].CompareTo( ergebnis.pel[rechts]) > 0) { rechts--; } string temp = ergebnis.pel[links]; ergebnis.pel[links] = ergebnis.pel[rechts]; ergebnis.pel[rechts] = temp; i = links + 1; j = ergebnis.laenge - 1; while (i < j) { temp = ergebnis.pel[i]; ergebnis.pel[i++] = ergebnis.pel[j]; ergebnis.pel[j--] = temp; } return ergebnis; } Listing 8.21
Die Methode »Nachfolger()«
Wichtig Auch wenn nur schwer ein anderer Eindruck entstanden sein kann: Die Methode Nachfolger() generiert in der Tat nicht mehr als ein lexikalisches Nachfolgeelement, sprich
ein Element aus der 120 Elemente (5!=120) starken Menge der Permutationselemente einer Liste mit fünf Pokerkarten.
Hier ist das Herzstück des Projekts Poker Game in der Gesamtansicht:
305
8.4
8
Pokern
using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace Poker_Game { class permutKarten { private private private private
string[] pel; permutKarten ergebnis; int links, rechts, i, j, laenge; List<string> pList = new List<string>();
public permutKarten(string[] perliste) { this.pel = perliste; this.laenge = perliste.Length; } public List<string> permutList() { for (int i = 0; i < laenge; i++) { pList.Add(pel[i]); } return pList; } public permutKarten Nachfolger() { ergebnis = new permutKarten(this.pel); links = ergebnis.laenge - 2; while ((ergebnis.pel[links].CompareTo( ergebnis.pel[links + 1])) > 0 && (links >= 1)) { --links; } if ((links == 0) && (this.pel[links].CompareTo( this.pel[links + 1])) > 0)
306
Entwicklung der Programmierlogik
return null; rechts = ergebnis.laenge - 1; while (ergebnis.pel[links].CompareTo( ergebnis.pel[rechts]) > 0) { --rechts; } string temp = ergebnis.pel[links]; ergebnis.pel[links] = ergebnis.pel[rechts]; ergebnis.pel[rechts] = temp; i = links + 1; j = ergebnis.laenge - 1; while (i < j) { temp = ergebnis.pel[i]; ergebnis.pel[i++] = ergebnis.pel[j]; ergebnis.pel[j--] = temp; } return ergebnis; } } } Listing 8.22
Die Klasse »permutKarten()«
8.4.12 Gewonnen oder verloren – die Klasse »evaluiereHand« An oberster Stelle im Rumpf der vorerzeugten Klasse evaluiereHand sei ein fünfelementiges, eindimensionales string-Array definiert: private string[] arraykl = new string[5];
Anschließend erfolgt die Deklaration eines verschachtelten Arrays, das, wie Sie zu Recht vermuten, ebenfalls vom Typ string ist: private string[][] kartenArray;
307
8.4
8
Pokern
Wir deklarieren fleißig weiter: Jetzt kommt eine generische Liste (Elementtyp string) an die Reihe: private List<string> pList;
Ferner ist ein Feld kk vom Referenztyp kartenKombinationen (siehe Abschnitt 8.4.9) zu vereinbaren: private kartenKombinationen kk;
Auch die in Abschnitt 8.4.11 implementierte Klasse permutKarten ist alsbald zu instanziieren. Deshalb schreiben wir: private permutKarten p;
Und natürlich brauchen wir den »Feld-Wald-und-Wiesen-Konstruktor«: public evaluiereHand() { }
Auf der Suche nach identischen Karten in identischer Reihenfolge geht es in der nächsten Methode um den Vergleich zwischen dem Inhalt zweier Listen. Identitätssuche – die Methode »vergleicheKarten()« Routiniert vergleicht vergleicheKarten() jede der in vkl gespeicherten fünf Pokerkarten mit jeder spielrelevanten Kartenkombination, die im verzweigten Array kartenKom enthalten ist. Dabei existiert in den Unterarrays von kartenKom jeweils ein Element mehr als in der übergebenen Liste vkl, und zwar der Name der Kombination. Gleichwohl wird genau dieser Name zurückgegeben, weshalb der Rückgabewert der Routine vergleicheKarten() vom Typ string sein sollte. Etwas kürzer macht es die Methodensignatur: string vergleicheKarten(List<string> vkl){};
Weiter geht es mit der Erzeugung eines Objekts kk vom Typ der Klasse kartenKombinationen(): kk = new kartenKombinationen();
Damit lässt sich die Methode kartenKom() aufrufen und der Rückgabewert (vom Typ string[][]) dem typgleichen Feld kartenArray zuweisen: kartenArray = kk.kartenKom();
308
Entwicklung der Programmierlogik
Zum Schluss folgt die Deklaration einer lokalen string-Variablen nachricht, die mit einem Leerstring zu initialisieren ist: string nachricht = "";
Die Reduzierung der Aufgabe könnte folgendermaßen aussehen: 왘
Vergleiche die in vkl gespeicherten fünf Pokerkarten mit den im ersten Unterarray gespeicherten Karten.
왘
Ist der Inhalt der Liste vkl gleich dem Inhalt des Unterarrays, initialisiere die string-Variable nachricht mit dem ersten Element des ersten Unterarrays.
왘
Die Variable nachricht ist zurückzugeben.
Hier sehen Sie die Lösung: if ( kartenArray[0][1] kartenArray[0][2] kartenArray[0][3] kartenArray[0][4] kartenArray[0][5]
== == == == ==
vkl[0] vkl[1] vkl[2] vkl[3] vkl[4]
&& && && &&
) { nachricht = kartenArray[0][0]; return nachricht; }
Erweitert sei die Aufgabe auf alle Unterarrays, die im verschachtelten Array kartenArray enthalten sind. Das heißt, die Elemente der generischen Liste vkl sollen notwendigerweise mit allen Unterarrays verglichen werden. Und so sieht die erweiterte Lösung aus: for (int i = 0; i < kartenArray.Length; i++) { if ( kartenArray[i][1] kartenArray[i][2] kartenArray[i][3] kartenArray[i][4] kartenArray[i][5]
== == == == ==
vkl[0] vkl[1] vkl[2] vkl[3] vkl[4]
&& && && &&
)
309
8.4
8
Pokern
{ hand = kartenArray[i][0]; return nachricht; } }
Kann kein Treffer erzielt werden, d. h., existiert für kein Unterarray eine Übereinstimmung mit den in vkl enthaltenen Listen, erfolgt eine »zwangsweise« Zuweisung des Zeichenfolgenliterals Verloren! an die lokale string-Variable nachricht. Dann ist die Routine der Überbringer schlechter Nachrichten: return nachricht = "Verloren!";
Damit wäre die Methode vergleicheKarten() vollständig: public string vergleicheKarten(List<string> vkl) { kk = new kartenKombinationen(); kartenArray = kk.kartenKom(); string nachricht = ""; for (int i = 0; i < kartenArray.Length; i++) { if ( kartenArray[i][1] kartenArray[i][2] kartenArray[i][3] kartenArray[i][4] kartenArray[i][5]
== == == == ==
vkl[0] vkl[1] vkl[2] vkl[3] vkl[4]
&& && && &&
) { nachricht = kartenArray[i][0]; return nachricht; } } return nachricht = "Verloren!"; } Listing 8.23
Die Methode »vergleicheKarten()«
Kommen wir zur zweiten Methode.
310
Entwicklung der Programmierlogik
Durchlauferhitzer – die Methode »eH()« Werfen wir zunächst einen schnellen Blick auf die Signatur der öffentlichen (public) Methode eH(): string eH(List<string> kl){}
Der Rückgabewert ist vom Typ string, der im Methodenkopf definierte Parameter kl ist vom Typ einer generischen Liste, die string-Elemente enthält. Eine Etage tiefer finden wir uns sogleich im Rumpf der quirligen Routine wieder. Dort erfolgt zunächst die Definition einer lokalen Variablen r vom Typ string, wobei auch r mit einem Leerstring zu initialisieren ist. Auf den kommt es nicht an, sehr wohl aber darauf, die lokale Variable überhaupt zu initialisieren: string r = "";
Die Erzeugung eines kartenPermut-Objekts kann nicht anders geschehen als durch den Aufruf des Konstruktors permutKarten() (new permutKarten()). Der ist eindeutig nicht vom »Typ Standard«, sondern erwartet ein eindimensionales string-Array – das wir nicht haben! Vorhanden ist allerdings der Aufrufparameter kl vom Typ List<string>. Ebenso vorhanden ist Ihre lebhafte Erinnerung an die generische Klasse List(T), unter deren umfangreicher Memberschaft die dreifach überladene Methode CopyTo() existiert. In der »Urfassung« (Ein-Argument-Aufruf) kopiert CopyTo() die gesamte, in kl gespeicherte Liste in ein kompatibles, eindimensionales Array, wobei der Kopiervorgang am Anfang des Zielarrays beginnt. So sieht’s aus: kl.CopyTo(arraykl);
Noch immer lässt sich keine Instanz der Klasse permutKarten erzeugen. Zumindest nicht, wenn Ihnen daran gelegen ist, 120 Permutationselemente (mithin die volle Breitseite möglicher Variationen im Reihenfolgeschema) zu erhalten. Gewährleistet ist das nur, wenn arraykl als alphabetisch sortierte Liste im Kopf des Konstruktors permutKarten() ankommt. Weiter oben wurde bereits darüber berichtet. Glücklicherweise müssen wir keine Sortierroutine schreiben. Schließlich existiert in der Klasse Array das ordentliche Mitglied Sort(). Diese Routine gibt sich im einfachsten Falle mit einem Array – unserem Array arraykl – zufrieden: Array.Sort(arraykl);
Endlich! Ein Objekt der Klasse permutKarten kann erzeugt werden:
311
8.4
8
Pokern
p = new permutKarten(arraykl);
Ausgehend von einer while-Schleife, die sich (abgesehen von einer weiteren Bedingung) so lange »weiterdreht«, wie es unterschiedliche Permutationselemente gibt (p != null), geschieht im Rumpf der altgedienten Kontrollstruktur Folgendes: 왘
Aufruf der Methode PermutList(), deren Rückgabeliste (eines von 120 möglichen Permutationselementen) in der List<string>-Variablen pList abgespeichert wird: pList = p.PermutList();
왘
Übergabe der Variablen pList an die Methode vergleicheKarten(), deren string-Rückgabewert der Variablen r zuzuweisen ist: r = vergleicheKarten(pList);
왘
Enthält r etwas anderes als das Zeichenfolgenliteral Verloren! (mithin fand in der Routine vergleicheKarten() ein erfolgreicher Mustervergleich statt), beendet das C#-Schlüsselwort break die weitere Ausführung der Schleife: if (r != "Verloren!") { break; }
왘
Anderenfalls sorgt die Anweisung p = p.Nachfolger();
für die Lieferung des nächsten, in pList zu speichernden Permutationselements. Die while-Schleife kann weiter ausgeführt werden. Hier ist noch einmal die while-Schleife – diesmal als zusammenhängende, gestrenge »C#-Formalie«: while (p != null) { pList = p.PermutList(); r = vergleicheKarten(pList); if (r != "Verloren!") { break; } p = p.Nachfolger(); }
312
Entwicklung der Programmierlogik
Hinweis Je schneller die Ausführung der while-Schleife beendet ist, desto besser ist das. Unter dem Aspekt der Performance liegt der eindeutig schlechteste Fall nämlich dann vor, wenn alle 120 Permutationselemente, sprich Kartenreihenfolgen, mit dem Inhalt der Liste kartenKom verglichen werden müssen.
Nicht vergessen werden darf natürlich: return r;
Damit wäre auch die zweite von zwei Klassenmethoden vollständig implementiert: public string eH(List<string> kl) { string r = ""; kl.CopyTo(arraykl); Array.Sort(arraykl); p = new permutKarten(arraykl); while (p != null) { pList = p.PermutList(); r = vergleicheKarten(pList); if (r != "Verloren!") { break; } p = p.Nachfolger(); } return r; } Listing 8.24
Die Routine »eH()«
Die Gesamtansicht der Klasse evaluiereHand: using using using using
System; System.Collections.Generic; System.Linq; System.Text;
313
8.4
8
Pokern
namespace Poker_Game { class evaluiereHand { private private private private private
string[] arraykl = new string[5]; string[][] kartenArray; List<string> pList; kartenKombinationen kk; permutKarten p;
public evaluiereHand() { } public string vergleicheKarten(List<string> vkl) { kk = new kartenKombinationen(); kartenArray = kk.kartenKom(); string nachricht = ""; for (int i = 0; i < kartenArray.Length; i++) { if ( kartenArray[i][1] kartenArray[i][2] kartenArray[i][3] kartenArray[i][4] kartenArray[i][5]
== == == == ==
vkl[0] vkl[1] vkl[2] vkl[3] vkl[4]
&& && && &&
) { nachricht = kartenArray[i][0]; return nachricht; } } return nachricht = "Verloren!"; } public string eH(List<string> kl) { string r = ""; kl.CopyTo(arraykl); Array.Sort(arraykl);
314
Entwicklung der Programmierlogik
p = new permutKarten(arraykl); while (p != null) { pList = p.permutList(); r = vergleicheKarten(pList); if (r != "Verloren!") { break; } p = p.Nachfolger(); } return r; } } } Listing 8.25
Die Klasse »evaluiereHand«
8.4.13 Letzte Schritte im EventHandler »tauschen_Click()« Weiter geht die Entwicklungsarbeit im Eventhandler tauschen_Click() der Klassendatei Poker.cs. Der Rumpf des Handlers wurde bereits so weit mit Code gefüllt, dass angehobene Karten getauscht werden können. Jetzt müssen wir die Logik um jene Teile ergänzen, die notwendig sind, um Ausgaben im Anzeige- und Hintergrundsegment erzeugen zu können. Ein schneller Einstieg: 왘
Wechseln Sie in den Quellmodus der Datei Poker.cs.
왘
Klicken Sie im rechten Dropdown-Listenfeld auf den Eintrag tauschen_ Click(object sender, EventArgs e). Ein automatischer Bildlauf zur ausgewählten Routine erfolgt (siehe Abbildung 8.18).
Im Rumpf des Eventhandlers, oberhalb der Zeile pb = pictureboxList();
(siehe Abschnitt 8.4.2), geht es um die Vereinbarung der int-Variablen mult: int mult = 0;
315
8.4
8
Pokern
Abbildung 8.18 Auswahl der Routinen und Felder im Dropdown-Listenfeld der Entwicklungsumgebung
Ferner wird eine string-Variable st vereinbart, die mit einem Leerstring initialisiert ist: string st = "";
Nicht ganz unwichtig ist die in Abschnitt 8.4.12 implementierte Methode evaluiereHand, die hier zu instanziieren ist: evaluiereHand evaluiere = new evaluiereHand();
Ob und welche Hand auf dem Tisch liegt, darüber gibt die Methode eH() als Member der Klasse evaluiereHand Auskunft. Zu übergeben ist der Routine die global deklarierte Liste getauscht. Als Rückgabe kommt entweder die Zeichenfolge Verloren! oder eine Hand in Frage. Was auch immer die Rückgabe ist – wenn Sie sie der Variablen st übergeben, erklärt sich nachfolgende Zeile von selbst: st = evaluiere.eH(getauscht);
Erzeugen wir ein Objekt der Klasse wertigkeitHand durch Übergabe der Variable st an den Konstruktor: wertigkeitHand wh = new wertigkeitHand(st);
Die Klassenmethode ermittelt, mit welchem Wert der im Pot befindliche Betrag zu multiplizieren ist. Den Multiplikator übergeben Sie der Variablen mult: mult = wh.wHand();
316
Entwicklung der Programmierlogik
Pause! Alle Informationen sind vorhanden, um Anzeigen im Hintergrund- und Anzeigeelement triggern zu können. Wir bleiben zunächst im Ereignisbehandler tauschen_Click() und setzen die Arbeit hinter der for-Schleife fort. Ein if-else-Konstrukt klärt die Frage, ob in st etwas anderes als die Zeichenfolge Verloren! abgespeichert ist. Wenn dem so ist, erhöht sich das Guthaben des Spielers (ks) um den Betrag im Pot, multipliziert mit mult. Konvertieren Sie das Ergebnis mit ToString() (einem Member der Klasse Convert) ins string-Format, und weisen Sie es anschließend der Text-Eigenschaft von label5 als Wert zu. label5.Text = Convert.ToString(ks + (numericUpDown1.Value * 2 * mult))+ "$";
Der Gewinn des einen ist der Verlust des anderen – hier der Bank, die künftig exakt denselben Betrag weniger in ihren Tresoren hat: label6.Text = Convert.ToString(kb - numericUpDown1.Value * 2 * mult))+ "$";
Im Label des Hintergrundsegments (label7) erscheint der Name der Hand, ergänzt um den Text Du hast gewonnen... label7.Text = st + "! -Du hast gewonnen...";
Mit »Draw Poker Light« können Sie mehr als eine Runde pokern, demzufolge wird der Button Tauschen öfter angeklickt. Entsprechend müssen die global deklarierten Felder ks und kb (für den Kontostand der Kontrahenten) bei jedem Klick aktualisiert werden. Es geht hier nur um Mathematik, d. h., es findet keine Konvertierung ins string-Format statt. Werte vom Elementartyp int sind nicht so ohne Weiteres mit einem Dezimalwert zu multiplizieren. Dessen ungeachtet, liegt der im Drehfeld Einsatz in $ eingestellte Betrag (den Sie über die Value-Eigenschaft der NumericUpDown-Klasse abrufen können) als decimal-Typ vor. Konvertieren wir also. Exemplarisch für das Guthaben des Spielers (ks) ließe sich Folgendes schreiben: ks = ks + Convert.ToInt16(numericUpDown1.Value) * 2 * mult;
Das können wir besser, nämlich so: ks += Convert.ToInt16(numericUpDown1.Value) * 2 * mult;
Analog für die um denselben Betrag geschröpfte Bank (kb) schreiben wir: kb -= Convert.ToInt16(numericUpDown1.Value) * 2 * mult;
317
8.4
8
Pokern
Damit ist der if-Block der if-else-Anweisung komplett. Abgesehen von umgekehrten Vorzeichen (das Geld geht an die Bank) und einer veränderten Ausgabe im Hintergrundsegment, ist im else-Block dasselbe zu tun. Werfen Sie einen Blick auf die vervollständigte if-else-Struktur: if (st != "Verloren!") { label5.Text = Convert.ToString(ks + (numericUpDown1.Value * 2 * mult))+ "$"; label6.Text = Convert.ToString(kb - (numericUpDown1.Value * 2 * mult))+ "$"; label7.Text = st + "! -Du hast gewonnen..."; ks += Convert.ToInt16(numericUpDown1.Value) * 2 * mult; kb -= Convert.ToInt16(numericUpDown1.Value) * 2 * mult; } else { label5.Text = Convert.ToString(ks - (numericUpDown1.Value * 2 * mult))+ "$"; label6.Text = Convert.ToString(kb +(numericUpDown1.Value * 2 * mult))+ "$"; label7.Text = "Spieler du hast verloren – Geld geht an die Bank"; ks -= Convert.ToInt16(numericUpDown1.Value) * 2 * mult; kb += Convert.ToInt16(numericUpDown1.Value) * 2 * mult; } Listing 8.26 Initialisierung der spielstandabhängigen Anzeigen im Hintergrund- und Anzeigesegment
Kommen wir noch einmal auf den Ereignisbehandler tauschen_Click() zu sprechen. Die neu hinzugekommenen Passagen sind gegenüber den »Altteilen« aus Abschnitt 8.4.8 fett abgehoben: private void tauschen_Click(object sender, EventArgs e) { int mult = 0; string st = ""; evaluiereHand evaluiere = new evaluiereHand(); st = evaluiere.eH(getauscht); wertigkeitHand wh = new wertigkeitHand(st);
318
Entwicklung der Programmierlogik
mult = wh.wHand(); pb = pictureboxList(); for (int i = 0; i < 5; i++) { pb[i].Image.Dispose(); pb[i].Image = null; im = Image.FromFile(@"C:\Projekte\Draw_Poker\ Poker Game\Images\Karten\" + getauscht[i] + ".gif"); pb[i].Image = im; pb[i].Location = new Point(413+(i * 87), 603); delay(200); pb[i].Enabled = false; } if (st != "Verloren!") { label5.Text = Convert.ToString(ks + (numericUpDown1.Value * 2 * mult))+ "$"; label6.Text = Convert.ToString(kb (numericUpDown1.Value * 2 * mult))+ "$"; label7.Text = st + "! -Du hast gewonnen..."; ks += Convert.ToInt16(numericUpDown1.Value) * 2 * mult; kb -= Convert.ToInt16(numericUpDown1.Value) * 2 * mult; } else { label5.Text = Convert.ToString(ks (numericUpDown1.Value * 2 * mult))+ "$"; label6.Text = Convert.ToString(kb + (numericUpDown1.Value * 2 * mult))+ "$"; label7.Text = "Spieler du hast verloren -Geld geht an die Bank!"; ks -= Convert.ToInt16(numericUpDown1.Value) * 2 * mult; kb += Convert.ToInt16(numericUpDown1.Value) * 2 * mult; }
319
8.4
8
Pokern
tauschen.Enabled = false; geben.Enabled = true; } Listing 8.27
Endlich komplett – der Ereignisbehandler »tauschen_Click()«
Der Test – Provokation eines Royal- oder Straight Flush Nähern wir uns einem erweiterten Testlauf über die zwei Hände mit den geringsten Kombinationsmöglichkeiten (und Wahrscheinlichkeiten): Royal- und Straight Flush. Der in erstelleKarten (Abschnitt 8.4.4) implementierte Kartenpool soll auf die Farbe Herz reduziert werden. Demnach sind im Generic _randomk lediglich die ersten 13 Kartenelemente von Belang. Zweimal ist das Argument der Next()-Methode (die Klasse Random) von 52 auf 13 zu ändern: 왘
einmal in der Routine pruefeKarten() der Klasse erstelleKartenliste
왘
zum anderen in der Methode kartenTausch() der Klasse tauscheKarten
Sie kennen das Prinzip von den ersten beiden Testfällen. Beim dritten allerdings gibt es noch etwas anderes zu tun, das weniger den Royal als den Straight Flush betrifft. Öffnen Sie die Klassendatei kartenKombinationen.cs. Das erste Unterarray aus dem string[][]-Array kartenK (implementiert in der Routine kartenKom()) stellt einen von vier möglichen Royal Flushs dar, noch dazu den, den wir benötigen. Stichwort Herz: new string[]{"Royal Flush","herzass","herzkoenig", "herzdame", "herzbube","herzzehn"},
Beim Straight Flush dagegen gibt es mehr zu tun. Zwar existiert bereits ein Unterarray, benötigt werden jedoch acht. So viele Möglichkeiten haben Sie nämlich, je Farbe einen Straight Flush zu legen. Ergänzen Sie das Array kartenK unterhalb des ersten und einzigen Straight Flush um folgende Unterarrays: new string[]{"Straight Flush","herzdrei","herzvier","herzfuenf", "herzsechs","herzsieben"}, new string[]{"Straight Flush","herzvier","herzfuenf", "herzsechs", "herzsieben","herzacht"}, new string[]{"Straight Flush","herzfuenf","herzsechs",
320
Entwicklung der Programmierlogik
"herzsieben", "herzacht","herzneun"}, new string[]{"Straight Flush","herzsechs","herzsieben", "herzacht", "herzneun","herzzehn"}, new string[]{"Straight Flush","herzsieben","herzacht", "herzneun", "herzzehn","herzbube"}, new string[]{"Straight Flush","herzacht","herzneun", "herzzehn", "herzbube","herzdame"}, new string[]{"Straight Flush","herzneun","herzzehn", "herzbube","herzdame","herzkoenig"},
Danach gehen Sie wie folgt vor: 왘
Starten Sie die Anwendung »Draw Poker Light« mit ((F5)) oder ohne Debug ((Strg)+(F5)).
왘
Lassen Sie sich über die Geben-Schaltfläche fünf Karten aushändigen. Nehmen Sie das Kartendeck auf (Button Aufnehmen), um abschließend zwischen ein und fünf Karten zu tauschen.
왘
Pokern Sie so lange, bis im Hintergrundsegment der Text Straight Flush! – Du hast gewonnen ... erscheint.
Betrachten Sie Abbildung 8.19 und die Ausschnittvergrößerung in Abbildung 8.20. Der gezeigte Straight Flush entstand durch Tausch der – von links aus betrachtet – zweiten Pokerkarte.
Abbildung 8.19
Ein Straight Flush zur Laufzeit
321
8.4
8
Pokern
Um die illustrierte Hand auf Grundlage eines reduzierten Kartenpools zu erhalten, waren circa fünf Minuten notwendig. Bei anderen Testläufen ging es schneller, bei keinem langsamer. Hinweis Der Screenshot in Abbildung 8.19 wurde mit nicht initialisiertem Anzeigesegment geschossen. Einzig der Text im Hintergrundsegment bewertet die Güte der Hand.
Abbildung 8.20 zeigt das Kartensegment im »Großformat«.
Abbildung 8.20
Von Herz-Sechs bis Herz-Zehn – ein Straight Flush!
Auch mein Straight Flush kann auf 120 mögliche Arten gelegt werden. Bezogen auf unser Spiel sind 120 Variationen einer Startreihenfolge für einen (in numerischer Reihenfolge aufgezählt) aus Herz-Sechs, Herz-Sieben, Herz-Acht, Herz-Neun und Herz-Zehn bestehenden Straight Flush möglich. Noch anders formuliert: Abbildung 8.20 könnte auf Grundlage derselben Karten etwas vollkommen anderes zeigen. Trotzdem würde im Hintergrundsegment der Benutzeroberfläche der Schriftzug Straight Flush! – Sie haben gewonnen ... erscheinen. Im verzweigten Array kartenK dagegen ist die hoch bewertete (siehe Abschnitt 8.4.10) Kartenkombination nur in einer möglichen Variante (und nicht notwendigerweise in eben der numerisch sortierten) als Unterarray vertreten. Ohne die Bildung der möglichen Permutationen müsste im verzweigten Array kartenK allein ein solcher Straight Flush durch genauso viele Unterarrays repräsentiert werden, wie es für eine Menge von fünf Pokerkarten Möglichkeiten gibt, die Reihenfolge der Karten zu verändern.
8.4.14 Die »Konditionierung« des Spiels Im Abbildung 8.21 zeigt sich allerdings ein Widerspruch.
322
Entwicklung der Programmierlogik
Abbildung 8.21
Trotz Flush verloren ...
Hinweis Auch der Screenshot in Abbildung 8.21 wurde mit nicht initialisiertem Anzeigesegment »geschossen«. Einzig der Text im Hintergrundsegment bewertet die Güte der Hand.
Im Kartensegment steht ein mittelprächtiger Flush, und im Anzeigesegment steht der Text Spieler du hast verloren! –Geld geht an die Bank. Ein Blick in die Klassendatei kartenKombinationen.cs bringt Licht ins Dunkel dieser Diskrepanz: Dem verzweigten Array kartenKom fehlt es an einem Unterarray, dessen Elemente identisch mit den in Abbildung 8.21 gezeigten Karten sind. Zur Verdeutlichung holen wir das betreffende Unterarray hervor: new string[]{"Flush","kreuzdrei","kreuzfuenf", "kreuzacht", "kreuzneun","kreuzkoenig"}
Hier hilft auch die Permutation nicht. In Abbildung 8.21 kann die Reihenfolge der die Hand bildenden Karten nach Belieben variiert werden, die Karten jedoch bleiben dieselben. Die Klasse evaluiereHand (genauer gesagt, die beteiligten Methoden vergleicheKarten() und eH()) gleicht jede der 120 möglichen Kombinationen gegen die im Unterarray angeordneten Karten ab. Leider erfolglos. Der Zuschlag geht an die Bank. Wie ungerecht!
323
8.4
8
Pokern
»Draw Poker Light« in Richtung möglichst vieler Treffer zu konditionieren, geht natürlich mit einer sukzessiven Aufstockung des Arrays kartenKom einher. Ausgehend von einem zunächst reduzierten Kartenpool, müssen in kartenKom lediglich die Hände nachgetragen werden, bei denen unberechtigterweise die Bank zum Gewinner der Runde ausgerufen wird. Dabei hat es der versierte Pokerspieler leichter als der Laie, der womöglich die Hand vor Augen nicht sieht. Doch was, bedeutet hier »lediglich«? Unser Spiel fit für sämtliche Kartenkombinationen zu machen, ist allein aus Performance-Gründen illusorisch. Gleichwohl: Hat kartenKom es auf 100 Hände gebracht, fängt »Draw Poker Light« über die Permutation immerhin 12.000 Kombinationen ab. Beenden wir das Spiel ...
8.4.15 Mit »this« und »Close()« zum geschlossenen Fenster Die Behandlung des Click-Ereignisses ist im Falle der Schaltfläche Fenster schliessen eine gleichsam platz- wie zeitsparende Angelegenheit. Denn einzig die Methode Close()kommt zum Einsatz: Das Ergebnis: private void button1_Click(object sender, EventArgs e) { this.Close(); } Listing 8.28
Er wartet auf das Spielende – der EventHandler »button1_Click()«
Was bleibt, ist eine Frage.
8.5
Hätten Sie‘s gewusst?
Ganz gleich ob Romancier, Biograf oder Fachautor – als Mensch, der Texte produziert, sollte man sich seine Worte sorgfältig überlegen, worum ich mich zumindest bemüht habe. Ganz besonders habe ich mich bemüht, als ich Ihnen absichtlich verschwieg, wie nahe der kleine Spiele-Ansatz vor dem Programmabsturz ist. Gemeint sind die Testläufe, bei denen die Anzahl der Karten im Kartenpool (die Liste _randomk) reduziert wurde. Beim zweiten Testlauf (siehe Abschnitt 8.4.8, »Tauschgeschäfte 3 – das finale Ereignis«) bis hinunter auf 6 als int-Parameter der Next()-Methode stand es auf einmal Spitz auf Knopf, konnten doch nur noch aus einem Pool von sechs Karten (ausgehend von Startindex 0) fünf Karten gewählt
324
Zum guten Schluss
werden. Folglich blieb eine Karte übrig. Dann ging es ans Tauschen der Pokerkarten. Der Fortgang der Geschichte ist Ihnen bekannt. Kurz zusammengefasst: Es funktionierte. Welcher int-Wert wäre sowohl in der Methode erstelleKartenliste() (Member der Klasse kartenListe) als auch in kartenTausch() (aus der Klasse tausche Karten) höchstens der Routine Next() als Argument zu übergeben, um einen Programmabsturz herbeizuführen? Schneller ist die zweite Frage gestellt: Warum stürzt die Anwendung ab? Nähern Sie sich der Antwort über den Kartenpool, um sich anschließend an des Programmierers liebsten Fehler zu erinnern. Orientieren Sie sich an der Methode karten Tausch() als einem von zwei Membern der Klasse tauscheKarten.
8.6
Zum guten Schluss
Börsenguru, Sternenfänger, Irrläufer oder Pokerkönig – zu nichts von alledem hat Ihr Buch Sie werden lassen. Vom Sternenfänger abgesehen, ist das ein Glück. Coding for fun mit C# wollte auch anderes, nämlich aufzeigen, wie ein Szenario auch umgesetzt werden kann. Nicht beabsichtigt war die Beschreibung des einen richtigen Weges, den es gerade im Programmiergeschäft nicht gibt. Wir haben uns auch nicht auf den Aspekt der Performance konzentriert, von optischen Fragen ganz zu schweigen, bei denen es am Ende eh auf den persönlichen Geschmack ankommt. Wie Sie gesehen haben, existieren zig Wege im Labyrinth der Möglichkeiten für ein und dieselbe Programmieraufgabe. Gerade die Vielfalt möglicher Realisationen macht die Entwicklung von Programmen so interessant. An deren Anfang steht allerdings immer das Problem, einen halbwegs gescheiten Ansatz zu finden. Danken möchte ich dem Team des Galileo Verlags, allen voran Frau StevensLemoine, für die unproblematische Zusammenarbeit, die selbst dann noch unproblematisch blieb, als ich in den Untiefen der Formatvorlage zu versinken drohte. Vor allem aber sei meiner Lektorin für die Möglichkeit gedankt, ein Computerbuch zu schreiben, das heißt, neben Quellcodes, Screenshots und schematischen Darstellung auch beschreibenden, erzählenden Elementen Platz zu geben. Das ist ein nicht selbstverständliches Entgegenkommen. Gegenüber jemandem, der ebenso gerne programmiert wie er gelegentlich zur Feder greift, war damit alles gesagt. Beurteilen Sie, ob es sich gelohnt hat ...
325
8.6
A
Visual C# 2010 Express
Visual C# 2010 Express als Entwicklungsumgebung für Studenten, Programmiereinsteiger und Hobbyprogrammierer zu bezeichnen, trifft es nur am Rande, denn dafür steckt in dem Tool zu viel. Zu viel an Komfort, zu viel an Performance, zu viel an Möglichkeiten, mit denen sich auch komplexere Programmiervorhaben umsetzen lassen. Ferner darf die entwickelte Software ohne Einschränkungen weitergegeben werden – eine Erlaubnis, die selbst bei gewerblichen Zwecken nicht endet. Hier ist der Softwaregigant Microsoft gewaltig über seinen kommerziellen Schatten gesprungen. Wir danken es ihm mit eifriger Nutzung.
Abbildung A.1 Schlichte Eleganz – das Logo der Entwicklungsumgebung Visual C# 2010 Express
Kurzum: Es muss nicht immer der »große Bruder« Visual Studio 2010 sein, umso weniger, als dass die Express-Editionen (die es auch in anderen, spezialisierten Ausführungen, namentlich C++, Basic und Web Developer gibt) »Pro bono«-Produkte, sprich kostenlos zu haben sind. In den Anfangstagen der abgespeckten Studio-Version sollte der Nulltarif nur für einen begrenzten Zeitraum gelten. Erst als auch dem letzten Redmonder die Beliebtheit des Produkts klar geworden war, wurde der portemonnaie-freundliche Tarif ins Zeitlose ausgedehnt – was hoffentlich so bleibt. Hinweis Alle im Buch vorgestellten Projekte können selbstverständlich auch mit Visual Studio 2010 nachprogrammiert, geändert und/oder ergänzt werden.
A.1
Anwendungen, die mit Visual C# 2010 Express erstellt werden können
Dass sich mit dem Entwicklungswerkzeug Windows- bzw. WindowsForms-Anwendungen erstellen lassen, ist Ihnen nicht entgangen. Denn abgesehen von einer minimalisierten Klassenbibliothek (die wir exemplarisch im Startprojekt Wecker Tool eingerichtet haben), haben wir die Umsetzung der kleinen Programmierauf-
327
A
Visual C# 2010 Express
gaben primär auf WinForms aufgesetzt. Warum auch nicht, wird doch nach wie vor das Gros clientseitiger Anwendungen auf Basis von WindowsForms entwickelt. Es bleibt Ihnen unbenommen, eine Oberfläche mittels der auf XML basierenden Extensible Application Markup Language (kurz XAML) deklarativ zu beschreiben und mit würzigem C#-Code zu garnieren. Auch mit Visual C# 2010 Express sind schließlich Applikationen unter dem dicht gedeckten Dach der WPF (Windows Presentation Foundation) möglich, einem Grafik-Framework, mit dem Sie sich viel Arbeit im Umgang mit XAML ersparen können. Darüber hinaus ist die WPF auch Teil des .NET-Framework 4.0. WPF-Anwendungen, die ein wenig an JAVA-Anwendungen erinnern, sind interessant, wirken filigran und elegant – ein De-factoStandard im Clientbereich ist damit allerdings noch nicht definiert. Was wäre die Entwicklung von Benutzeroberflächen ohne Steuerelemente? Mühsam. Das .NET-Framework bringt eine ganze Reihe Controls mit, von denen Sie einige auf den vergangenen Buchseiten kennengelernt haben. Darüber hinaus erlaubt die Entwicklungsumgebung die Erstellung von Benutzersteuerelementen, wahlweise im WinForms- oder WPF-Gewand. Last but noch least verschließen sich auch die aktuellsten Express-Editionen dem Klassiker der Programmierung nicht: der Konsolenanwendung. Selbst in Zeiten, in denen ohne grafisches Interface nichts mehr zu gehen scheint, ist es nie falsch, einige der Programmausgaben zunächst über die altehrwürdige Konsole zu schicken. Beim letzten Projekt – Draw Poker Light – haben wir genau das getan.
A.2
Reduzierter Funktionsumfang
Was Sie mit Visual C# Express nicht realisieren können, sind Anwendungen für mobile Geräte, wie beispielsweise PDAs (Personal Digital Assistants). Des Weiteren fehlen den Express-Editionen Werkzeuge wie das Remote-Debugging, die Anbindung an ein Quellcode-Verwaltungssystem (z. B. die Freeware Mercurial, erhältlich unter http://mercurial.selenic.com) und die Teamfähigkeit. Außerdem gibt es keinen Ressourceneditor und (zunächst) keine Möglichkeit, MFC-Projekte zu realisieren, also solche Projekte, die auf Grundlage der Microsoft Foundation Classes arbeiten. Hier ist allerdings noch nicht aller Tage Programmierabend. Programme, die die Ressourcendatei afxres.h nutzen (in der u. a. vordefinierte Symbole für Menüs enthalten sind), können nämlich durchaus kompiliert werden – zumindest dann, wenn statt afxres.h die Windows-Basisdatei windows.h eingebunden wird.
328
Neues bei Visual C# 2010 Express
Warnung Der Griff in die Trickkiste ist beliebt, doch legen Sie sich nicht mit EULA an! So ungern Endbenutzer-Lizenzverträge gelesen werden – auch im End User License Agreement von Visual C# 2010 Express ist klar geregelt, was Sie dürfen und was nicht. Tatsache ist: Ihr Tool ist ein .NET-orientiertes Entwicklungswerkzeug, mit dem die MFC wenig zu tun hat.
A.3
Neues bei Visual C# 2010 Express
Selbstredend unterstützt Visual C# 2010 Express das gleichfalls neue .NET-Framework 4.0. Beinahe genauso selbstverständlich war das neue Startfenster (siehe Abbildung A.2).
Abbildung A.2
Das Startfenster des neuen Visual C# 2010 Express
Eine echte Neuigkeit dagegen ist die Oberfläche, im Besonderen der Codeeditor der sympathischen Express-Familie. Hier wurde zugunsten der WPF auf einen WinForms-Ansatz komplett verzichtet. Dies war eine beinahe drakonische Maßnahme, die das Erscheinen der Final Release maßgeblich verzögert hat, gab es, vor allem bei den diversen Vollversionen, vonseiten der Beta-Tester doch mitunter erhebliche Beschwerden: zu wenig Geschwindigkeit, zu hoher Ressourcenver-
329
A.3
A
Visual C# 2010 Express
brauch. Ungeachtet dessen: Schick ist die neue Oberfläche. Ob allerdings die Geschwindigkeit der Werkzeuge mittlerweile genügt – eine einhellige Antwort wird es nicht gegeben. Zu unterschiedlich sind die Ansprüche. Probieren Sie es aus! Per Default ist Visual C# 2010 so konfiguriert, dass nur relevante Menüs eingeblendet und fortgeschrittene Optionen ausgeblendet sind, wovon Microsoft sich eine verbesserte User Experience, sprich Benutzerfreundlichkeit verspricht. Das Ausgeblendete lässt sich natürlich problemlos über die Menüpunkte Tools 폷 Einstellungen auch wieder einblenden. Sollte es bei Ihnen mehrere Computermonitore geben, können Sie sich der Unterstützung der neuen Visual Studio 2010 Express-Produkte sicher sein. Ganz gleich, ob Sie C# ewige Programmiertreue schwören oder Ihr Glück auch mit anderen .NET-Sprachen (C++, Basic etc.) versuchen wollen – keines der Tools verschluckt sich an einem Zimmer voller Bildschirme.
A.4
Der Weg zu Visual C# 2010 Express
Die erste Station auf Ihrem Weg zu Visual C# 2010 Express ist die Webadresse http://www.microsoft.com/germny/express/, die im Downloadbereich mit gleich zwei Möglichkeiten zum Bezug der begehrten Software aufwartet: 왘
Installation eines circa 3 MB großen Web Installers (.exe-Datei), der nach erfolgreicher Installation – eine schnelle Datenleitung vorausgesetzt – das Express Tool online installiert
왘
Download einer circa 700 MB großen ISO-Datei (gemeinhin auch als ISOImage bezeichnet). Auch hier sollte eine einigermaßen schnelle Leitung zwischen Ihnen und Microsofts Downloadserver bestehen.
Sollten Sie Vorbehalte gegen den zuweilen recht nervigen Vorgang einer Registrierung hegen, sind Sie mit der Online-Installation schlecht beraten. Denn Ihnen bleiben nur 30 Tage zur Nutzung des Produkts, danach muss ein Registrierungsformular ausgefüllt werden, was nur möglich ist, wenn Sie im Besitz eines Live ID Kontos sind (Näheres unter http://home.live.com). Das bleibt Ihnen beim Herunterladen des ISO-Image erspart. Nicht erspart bleibt Ihnen vielleicht die Frage nach dem technischen Hintergrund einer ISO-Datei (Dateiendung ISO). Die ist nicht bei jedem beliebt, auch weil viele den vermeintlichen Aufwand des Brennens scheuen. Ich selbst bildete lange Zeit keine Ausnahme.
330
Der Weg zu Visual C# 2010 Express
A.4.1
Was ist ein ISO-Image?
Ein ISO-Image ist die exakte Kopie, somit ein identisches Abbild, des Inhalts und des Formats einer DVD bzw. CD. Dabei kommt es weniger auf den Inhalt als auf das Format an. ISO selbst steht für International Organization for Standardization, den gestrengen »Wächterrat«, der auch für die Normung des Formats runder Datenträger (und keinesfalls nur für Schrauben und Muttern) zuständig ist. Strukturiert in ISO 9660, ist in besagter Norm ein Dateisystem genau definiert, beispielsweise hinsichtlich der Länge der Dateinamen. Bei der Erstellung eines ISO-Abbildes wird das Dateisystem unverändert und zeitnah kopiert. In der Folge bleiben Berechtigungen und Metadaten in Gänze erhalten. Daher bringt unser ISO-Image alles mit, was notwendig ist, um den eigentlichen Inhalt erfolgreich brennen zu können – ein Vorgang, bei dem trotzdem Fallstricke lauern ...
A.4.2
Brennen einer Visual C# 2010 Express-CD
Bereits die Wahl des Mediums kann schiefgehen. Eine CD+RW kauft sich zuweilen leichter als eine einfache CD+R. Trotzdem: Entscheiden Sie sich besser für die Variante, bei der Daten unveränderlich auf den Träger geschrieben werden. Denn nicht selten gibt es allein deswegen Ärger, weil ISO-Images auf RW-Medien gebrannt wurden. Und noch etwas: Preisgünstige Rohlinge können nach vermeintlich geglücktem Brennen zu den merkwürdigsten Problemen führen. Versuchen Sie erst gar nicht, die heruntergeladene ISO-Datei als gewöhnliche Daten-CD zu brennen. Funktionieren würde es womöglich – nur hätten Sie nichts davon. Ein ISO-Image darf nicht als Datei auf CD gebrannt werden, die ISO-Datei ist die CD, wenngleich eine in Gestalt einer Datei. Überspitzt formuliert wird durch den Brennvorgang eine virtuelle CD in eine reale, d. h. physisch vorhandene überführt. Bei den meisten Brennprogrammen, allen voran Nero (http://www.nero.com), existiert dafür eine eigene Option, die Sie unter der Rubrik Kopieren und Sichern finden. Anschließend klicken Sie auf Image auf Disk brennen (siehe Abbildung 9.1).
331
A.4
A
Visual C# 2010 Express
Abbildung A.3
Die Option »Image auf Disk brennen« der Nero-Dialogmaske
In der nächsten Dialogmaske – Imagedateien brennen – können Sie alle Festlegungen beibehalten, bis auf jene im Dropdown-Listenfeld Schreibgeschwindigkeit. 24× (3600 KB/s) ist eine gute Schreibleistung, nur leider kann zu schnelles Brennen auch Schreibfehler verursachen. Deshalb sollten Sie vorsichtshalber einen oder gleich mehrere Gänge zurückschalten (siehe Abbildung A.4), selbst dann, wenn Sie auf ein installierbares Visual C# 2010 Express geringfügig länger warten müssen. Wer Nero nicht mag (verständlich, wenngleich die Software bedeutend besser als ihr Namenspatron ist) oder ein eher auf ISO-Belange gemünztes Tool bevorzugt, der kann auf die Freeware ImgBurn (http://www.imgburn.com) ausweichen. Abbildung A.5 stellt eine von zwei (!) Startmasken vor. Die Bedienung von ImgBurn ist weitgehend intuitiv. Verwechseln Sie bitte trotzdem nicht die Option Write image file to disk mit Create image file from disk. Ein ISO-Image haben Sie bereits heruntergeladen ...
332
Der Weg zu Visual C# 2010 Express
Abbildung A.4 So reduzieren Sie die Schreibgeschwindigkeit im Dropdown-Listenfeld »Schreibgeschwindigkeit«.
Abbildung A.5
Gebrannt wird auch hier – ImgBurn
333
A.4
A
Visual C# 2010 Express
A.4.3
Die Alternative – virtuelle Festplatte
Für die Ungeduldigen oder jene, die sich partout nicht mit dem Brennen einer ISO-Datei anfreunden können, besteht die Möglichkeit, das Image als virtuelle Festplatte einzubinden. Auf ein virtuelles Laufwerk kann genauso zugegriffen werden wie auf ein physikalisches, einzig ein spezielles Tool ist nötig. Hier haben Sie die Wahl zwischen den DeamonTools (http://www.daemon-tools.cc) und dem etwas martialisch anmutenden IsoBuster, der unter http://www.IsoBuster.com zum kostenlosen Download bereitsteht. Wie ist das weitere Vorgehen? Beispielsweise können Sie Folgendes tun: 왘
Klicken Sie mit der rechten Maustaste auf das heruntergeladene ISO-Image, und wählen Sie im Menü die Option Extrahieren. Im linken Fensterteil zeigt sich, worum geht: um eine virtuelle CD, deren softwarerelevante Daten im rechten Teil des Hauptfensters angezeigt werden (siehe Abbildung A.6).
Abbildung A.6
334
Für die ganz Eiligen – IsoBuster 2.7
Installation von Visual C# 2010 Express
왘
Im zweiten geöffneten Fenster (Ordner suchen) wählen Sie den Bestimmungsordner aus, in den die Dateien von Visual C# 2010 Express zu extrahieren sind.
왘
Nach einem abschließenden Klick auf die Schaltfläche Ok wird der Vorgang gestartet.
Abbildung A.7
A.5
»Bestimmungsort auswählen« in der Dialogmaske »Ordner suchen«
Installation von Visual C# 2010 Express
Wenn entweder eine ISO-gebrannte CD im CD-Player des Computers liegt oder eine virtuelle Festplatte unter Windows vorhanden ist, können Sie die Installation durch einen Doppelklick auf die Datei setup.hta starten. Die Installation besticht nicht gerade durch eine überhöhte Geschwindigkeit, was allein wegen der großen Datenmenge auch nicht zu erwarten ist. Sollten Sie also noch etwas zu erledigen haben ... Wichtig Die Installationsroutine schließt die Installation von .NET-Framework 4.0 ein, ohne das Visual C# 2010 Express faktisch nutzlos ist (siehe Abbildung A.8).
335
A.5
A
Visual C# 2010 Express
Abbildung A.8
A.6
Die Visual C# 2010-Installationsroutine in Aktion
Das Prinzip der integrierten Entwicklungsumgebung
Jedes Tool, das zur Visual Studio 2010-Produktfamilie gehört (somit auch Visual C# Express), nutzt eine integrierte Entwicklungsumgebung, kurz IDE (Integrated Development Environment), die sich aus mehreren Komponenten zusammensetzt: 왘
Menüleiste
왘
Standardsymbolleiste
왘
diversen, wahlweise anzuordnenden Toolfenstern, die darüber hinaus ganz nach Belieben ein- und ausgeblendet werden können
왘
Editorbereich
Hinweis Die Verfügbarkeit der einzelnen Features ist u. a. von dem Dateityp abhängig, der gerade bearbeitet wird. Arbeiten Sie beispielsweise in einer Ressourcendatei (Dateiendung .res), ist es Ihnen nicht möglich, auf die Steuerelemente der Toolbox zuzugreifen (siehe Abbildung A.9).
336
Das Prinzip der integrierten Entwicklungsumgebung
Abbildung A.9
A.6.1
Steuerelemente in der Toolbox
Projekte mit System
Sowohl die Projektmappe als auch der Projektordner enthalten Verweise, Dateien, Ordner und Datenbankverbindungen als elementare Bausteine (auch) eines Visual C# 2010-Projekts. Die Projektmappe kann mehrere Projekte, mithin mehrere Projektordner enthalten. Angezeigt werden die Elemente im Projektmappen-Explorer (siehe Abbildung A.10), in dem Sie 왘
Dateien zur Bearbeitung öffnen
왘
neue Dateien dem Projekt hinzufügen sowie Projektmappen-, Projekt- und Dateieigenschaften anzeigen lassen können (mit einem Rechtsklick und durch Auswahl der Option Eigenschaften, genauso wie Sie es vom Windows Explorer gewohnt sind)
337
A.6
A
Visual C# 2010 Express
Abbildung A.10
A.6.2
Der »Projektmappen-Explorer« (am Beispiel des Projekts »Poker Game«)
Quell- versus Entwurfsmodus
Der Projektmappen-Explorer ist auch der Ort, an dem Sie auswählen, ob eine auf Grundlage einer WindowsForms erstellte Klassendatei im Entwurfs- oder im Quellmodus angezeigt werden soll. Standardmäßig führt ein Doppelklick auf die Datei zum Designer, sprich in den Entwurfsmodus (siehe Abbildung A.11).
Abbildung A.11
A.6.3
Entwurfsansicht der WinForms-Datei »Form1.cs«
Unverzichtbar – das »Eigenschaften«-Fenster
Während sich im Fall einer einfachen Klassendatei (Dateiendung .cs) die Eigenschaften auf zumeist wenige Optionen reduzieren lassen (z. B. Dateiname, unter der Rubrik Sonstiges), gestaltet sich das Eigenschaften-Fenster bei Steuerelementen ungleich umfangreicher.
338
Veröffentlichung einer Anwendung
Abbildung A.12 zeigt das Eigenschaften-Fenster eines PictureBox-Controls, in dem Sie z. B. in den Rubriken Darstellung, Daten, Entwurf, Layout und Verhalten Ihr Steuerelement frei konfigurieren können.
Abbildung A.12 (»pictureBox6«)
Das »Eigenschaften«-Fenster eines PictureBox-Steuerelements
Zu den Eigenschaften des jeweiligen Controls gelangen Sie auf zwei Wegen: 왘
Klicken Sie in der Entwurfsansicht einmal auf das zu konfigurierende Control.
왘
Wählen Sie im Dropdown-Listenfeld des Eigenschaften-Fensters das entsprechende Steuerelement aus.
A.7
Veröffentlichung einer Anwendung
Natürlich möchten Sie, dass das Resultat Ihres produktiven Schaffens zu einer »auf Knopfdruck« startenden Anwendung wird. Hier wartet die IDE mit dem Webpublishing-Assistent auf – erreichbar über das Hauptmenü, Menüpunkt Erstellen –, der die Anwendung 왘
auf einer Website,
왘
einem FTP-Server oder
왘
in einer Datei
veröffentlicht.
339
A.7
A
Visual C# 2010 Express
Abbildung A.13 zeigt die Eingangsmaske (Wo möchten Sie die Anwendung veröffentlichen?).
Abbildung A.13
Die Eingangsmaske des Webpublishing-Assistenten
왘
Der Button Durchsuchen hilft Ihnen, im Editorfeld Veröffentlichungsort für diese Anwendung angeben den korrekten Pfad einzustellen. Danach geht es mit einem Klick auf den Button Weiter zum nächsten Schritt. Notwendig ist das nicht, doch gehe ich hier von einer CD-ROM bzw. DVD-ROM als dem Ort aus, von dem aus die Installation erfolgt.
왘
In der Dialogmaske Wie werden Benutzer die Anwendung installieren? aktivieren Sie den RadioButton Von einer CD-ROM bzw. DVD-ROM (siehe Abbildung A.14). Klicken Sie auf die Schaltfläche Weiter.
왘
In der nächsten Dialogmaske kann eine Quelle auf Updates überprüft werden. Ist das gewünscht, setzen Sie den RadioButton Die Anwendung überprüft folgenden Speicherort auf Updates. Anschließend wäre der Ort des Updates einzustellen. Dies kann ebenfalls ein FTP-Server, eine Website oder eine lokale Datei sein (siehe Abbildung A.15).
왘
In der letzten Dialogmaske Veröffentlichung kann gestartet werden (siehe Abbildung A.16) wird es nach einem Klick auf den Button Fertig stellen schließlich ernst.
340
Veröffentlichung einer Anwendung
Abbildung A.14 Auswahl des Installationsmediums in der Dialogmaske »Wie werden Benutzer die Anwendung installieren?«
Abbildung A.15
Auswahl optionaler Update-Möglichkeiten
Danach finden Sie zwei Dateien an dem Ort, der unter Veröffentlichungsort für diese Anwendung angeben angegeben wurde. Die erste ist eine gewöhnliche Setup-Datei, die zweite ein sogenanntes Application Manifest.
341
A.7
A
Visual C# 2010 Express
Abbildung A.16
Die »Veröffentlichung kann gestartet werden«.
Am Beispiel des Projekts DAX_SIM zeigt Abbildung A.17 das Setup- und Application-Manifest-Icon auf dem Desktop.
Abbildung A.17
Setup- und Application-Manifest-Icon
Application Manifest Das Application Manifest ist zunächst nichts anderes als eine XML-Datei, die Informationen über die Anwendung und deren Abhängigkeit von anderen Anwendungen enthält. Die Datei kann problemlos mit einem Editor geöffnet werden.
Selbstverständlich kann die Anwendung via Setup auch lokal installiert werden. Soll es nach wie vor eine CD oder DVD sein, müssen die Dateien natürlich auf einen geeigneten Rohling gebrannt werden. Viel Spaß mit Visual C# 2010 Express!
342
Index @-Zeichen 59
A Accessormethode 164 Add() 212, 215, 256, 262, 298 Adresse 108, 135 A-Eigenschaft 80 Algorithmus 25, 109, 189, 240, 295 Aliasnamensraum 164 Alpha-Wert 76, 95 Anchor-Eigenschaft 198 AND-Operator 300 Anwendung 134 Application Manifest 341 Application-Klasse 136, 173 Arbeitsspeicher 24, 26 Architektur 18 ARGB-Farbe 77 Argument 35 Array 173, 175, 215 Array-Klasse 311 Assembly 91 Assemblierung 21 Assembly-Manifest 22, 23 Assembly-Metadaten 22 Assembly-Name 23 Assembly-Verweis 91 AT Advanced Technologie 44 AT-System 43 Auf-Ab-Steuerelement NumericUpDown.Control 57 Aufrufliste 37 Aufrufparameter 311 Ausführungsgeschwindigkeit 19, 25 Automatische Speicherbereinigung Garbage Collector 25
B BackColor-Eigenschaft 150, 199, 243 BackgroundImageLayout-Eigenschaft 164 Base Class Library BCL 15
base-Schlüsselwort 170 Basisdatentyp 16 Basisklasse 37, 155, 160, 244 Basistyp 54 BCL-Support Base Class Library 17 Bedienelement 27 Bedienoberfläche 27, 244 Benutzer 28 Benutzereingabe 28 Benutzerführung 276 Benutzergebietsschema 23 Benutzersteuerelement 328 Berechtigung 22 Betriebssystem 23, 34, 44 Bezeichner 164 Bibliothek 16 Bilddatei 163 Bildschirmfoto 129 BindingNavigator 28 BIOS 45 Basic Input/Output System 44 Bitmap 22 Block 32 Blue-Eigenschaft 167 Bold-Enumerationsmember 167 Boolean 279 Boolean-Struktur 301 BorderStyle-Eigenschaft 206, 244 break 312 Brush-Klasse 116 Brush-Objekt 168 Button 21, 39, 51 Standardschaltfläche 28 Bytecode 18 P-Code 17
C C-Dialekt 18 C# 31 C#-Anweisung 18 C#-Compiler 24, 38, 58 C#-Derivat 30 C#-Klasse 202
343
Index
C++ 31 C_ò 29 Canvas 21 CheckBox 21, 39, 152 Bedingung 28 CheckBox-Control 146 CheckBox-Objekt 164 Checked-Eigenschaft 152, 164 CheckedListBox Kontrollkästchen 28 CL BCL 15 Class 290 Click-Ereignis 274, 324 Clientrechteck 150 Client-Server-Kommunikation 21 Close() 155, 174, 324 CLR 14, 17, 23 Codeedititor 329 Color-Struktur 77, 114, 129, 167, 229 ComboBox Dropdown-Kombinationsfeld 28 ComboBox-Aufgaben 149 ComboBox-Control 108, 145 ComboBox-Klasse 84 COM-Interoperabilität 17 COM-Komponente 29 Common Language Runtime 22, 25 CompareTo() 301 Compiler 18, 31, 38, 49, 89 Compiler-Direktive 33 Component Object Model COM 29 Computer 42 Container 196 ContentAlignment-Enumeration 207 ContextMenu-Steuerelement 28 ContextMenuStrip Kontextmenü 28 Control 78, 82, 106, 167, 328 Steuerelement 27 Control-Klasse 166, 244 Convert-Klasse 93, 125, 160, 254 Copy and Paste 198 CopyTo() 311 Count-Eigenschaft 263 CPU Prozessor 18 CreateGraphics() 118, 166
344
CSC-Compiler 33 C-Sharp C# 13 Cursor 39
D DataGrid DataGrid-Steuerelement 28 DataGridView Tabellendaten 28 Datei 22 Dateiname 253, 331 Dateinamenextension 271 Dateioperation 153 Dateisystem 23, 153, 196, 242, 247, 331 Dateityp 336 Daten 21, 28, 128, 155, 166 Datenbank 289 Datenbankersatz 153 Datenbankverbindung 24, 337 Datenelement 202 Datenhaltung 153 Datenquelle 21, 28 Datenstruktur 38 DateTime-Objekt 159 DateTimePicker 28, 51 DateTime-Struktur 53, 159, 161 Debug 153, 321 Debugger 31, 59, 185 Debugging 55 Default-Wert 51 Deklaration 32, 36, 135, 217 Delay-Befehl 134 DelayTime 108 Delegate 32, 35, 36, 108, 123, 135, 138 Delegatentyp 37, 38 Delegate-Objekt 127 Delegate-Objekt tst 124 Delegate-Schlüsselwort 38 Delphi 31 Derivat 30 Designentwurf 152 Designer 338 Destruktor 24 Dictionary 201, 216, 217 Dictionary(TKey, TValue)-Klasse 211, 291 Dictionary(TKey, TValue)-Objekt 213
Index
die TapePages-Eigenschaft 148 Dijkstra-Algorithmus 189 Dispose() 114, 271 DllMain 22 Dock-Eigenschaft 198 DockStyle-Enumeration 198 DoEvents() 136, 173 DomainUpDown Textzeichenfolge 28 do-while-Ablaufsteuerung 266 do-while-Anweisung 280 Drag&Drop 50, 197 Drawing 108 DrawLine() 116 DrawLines() 183 DrawRectangle() 167 DrawString() 168 Drehfeld 149, 272 NumericUpDown-Steuerelement 254 Dropdown-Listenfeld 39, 122, 242, 243, 315 Dropdownlistenfeld 339 Dualsystem 42 Dynamic Link Library DLL 16
E Echtzeit 44 Echtzeitbedingung Echtzeitsystem 25 Echtzeitsystem 26 Editor 31 Editorbereich 336 Eigenschaft 32, 50, 87, 114, 128, 164, 202, 203, 220 Eigenschaften-Fenster 201, 243 Einsprungspunkt 23 Einstiegspunkt 22, 88, 146 Einzeldatei-Assembly 23 Single-File-Assembly 22 Element 18, 28, 37 Elementartyp 76 Enabled-Eigenschaft 74, 245 EncoderFallbackException 155 Entwicklungsumgebung 31, 88, 111, 198 Entwurfsmodus 106, 196 Environment-Klasse 136 Equals() 263
Ereignis 32, 38, 59 Ereignisbehandlung 39, 283 Ereignisbehandlungsmethode 122, 128 Ereignisbehandlungsroutine 52 Ereignishandler 86, 166 Escape-Sequenz 59 Escapesequenz 257 Event Ereignis 39 EventHandler Ereignisbehandlungsroutine 39 EventHandler-Delegate 128 EventHandling 52 EventSink 39 Exception 155
F Feld 32, 203, 220 Fensterstil 243 Festplatte 146 FillEllipse() 115 FixedSingle 244 FixedSingle-Enumerationsmember 207 float-Variable 159 FlowLayoutPanel 28 FontFamily-Klasse 168 Font-Klasse 168 Font-Objekt 168 FontStyle-Enumeration 167 for-Anweisung 185, 271, 278 foreach-Anweisung 216, 218, 228, 262 ForeColor 108 ForeColor-Eigenschaft 148, 207 Form 37 Form_Klasse 150 Format 331 FormatException 89 Formatierung 197 FormBorderStyle-Eigenschaft 106 Formelement 50, 74, 113 Form-Klasse 146, 160, 164, 244 Formular 28, 34, 52, 146, 147 for-Schleife 81, 176, 256, 298 Framework 29 .NET-Framework 13, 155 Frequenzgeber 44 FromArgb() 119 FromFile() 257, 271
345
Index
FTP-Server 339 Funktionszeiger 36 Delegate 35
G Garbage Collector 25, 26, 35, 44 Speicherbereinigung 24 GDI Graphics Device Interface 77 GDI+ 113 GDI+-Zeichnungsoberfläche 77 Generationszahl 27 Generic 201, 217 get 202 Gleitkommazahl 160 global-Schlüsselwort 164 Grafik-Framework 328 Graph 189 Graphentheorie 190 Graphic Device Interface GDI 113 Graphics-Klasse 77, 166 Graphics-Objekt 113, 126, 167 Graphics-Zeichenoberfläche 113, 129, 138 Graphics-Zeichenobjekt 118, 134 GroupBox 28, 198 GroupBox-Control 148 GroupBox-Steuerelement 246, 250 GUI Graphical User Interface 51 Gültigkeitsbereich 24
H Handler Ereignisbehandlungsroutine 39 Hardwaretreiber 29 Hardware-Uhr Echtzeit-Uhr 44 Hash Map 211 Hash Table 211 Hashalgorithmus 211 Hashtabelle 211 Hashverfahren 211 Hauptmenü 45 Hauptplatine 44 Heap 290
346
Hintergrundbild 146 Hochsprache 31 HotTrack 199 Hour-Eigenschaft 53 HScrollBar Elementliste 28
I IBM International Business Maschines 43 Icomparable-Schnittstelle 301 IDE Integrated Development Environment 336 Integrierte Entwicklungsumgebung 33 if-Anweisung 164 if-else-Struktur 89, 124, 292 IL-Code 18 ImageButton 39 Image-Eigenschaft 247, 257, 271 Image-Klasse 257 ImageLayout-Enumeration 164 Index 176, 181, 214 Indexer 32 Indexposition 181, 223 Infrastruktur 148 Initialisierung 59, 61, 154, 160 Initialisierungsmethode 50, 110 InitializeComponent() 152, 160 Inkrement 127 Insert() 223, 261 Installationsverzeichnis 16 Instanz 24, 36, 38, 86, 109, 128, 155, 300 Instanzkonstruktor 32 Instanzmethode 35, 38 int-Array 176 Interface 21, 32, 49, 128, 328 Intermediate Language IL 17 Internationales Zeichencodierungssystem Unicode 30 Internet 143 Interpreter 18 Interrup 44 Interruptroutine 44 Interval-Eigenschaft 55 int-Typ 262, 291 IO Input/Output 153
Index
IO-Funktion Input/Output 16 ISO 9660 331 ISO Datei 331 ISO Image 330 ISO-Abbild 331 Iteration 223
J Java 31 Java-Anwendung 328 JIT-Compiler Just-In-Time-Compiler 19 Jitter Just-In-Time-Compiler 19
K Kanten 189 keine Escapesequenz 175 Klasse 20, 21, 24, 32, 33, 36, 39, 87, 289 Programmelement 18 Klassenbibliothek 14, 16, 327 Klassendatei 111, 135, 146, 163, 297, 338 Klassenimplementierung 49 Klassenmember 128 Klassenobjekt 89, 213, 290 Klassenrumpf 87, 88 Knoten 189 Kommandozeile 34, 192 Kompilat 20 Kompilierung 18 Komplexer Datentyp 33 Konsole 328 Konsolenanwendung 328 Konsolenausgabe 295 Konstruktor 24, 59, 88, 108, 135, 152, 154, 159, 203, 212, 260, 311 Kontrollstruktur 312 Konvertierung 124, 160 Kultur Länderspezifische Festlegungen 23
L Label 28, 51 Label-Control 148 Label-Klasse 254
Label-Objekt 161 Label-Steuerelement 196, 252 Laufindex 185 Laufvariable 77, 121 Laufzeit 16, 18, 19, 25, 31, 36, 150, 196, 198 Laufzeitbedingung 59 Laufzeitumgebung 17, 18, 22, 29, 33 Leerstring 64, 89, 138, 279, 309 Leseeigenschaft 202 Lesezugriff 21, 218 LinearGradientBrush-Klasse 114 LinearGradientMode-Enumeration 114 LinkLabel Hyperlink 28 Linux 44 List(T)-Klasse 213, 256, 261, 263, 291 ListBox 21 Liste 28 Liste 28, 161, 242 List-Klasse 218, 223 ListView Windows Explorer 28 Literal 271 Location 151 Location-Eigenschaft 257, 271, 274
M Main 22 Main() 88, 146, 156, 173 Mainboard Hauptplatine 44 Maschinenbefehl Native Code 18 Maschinencode 18, 29 MaskedTextBox Format 28 Mauscursor 147 Maxima-Eigenschaft 75 MaximizeBox 146 MaximizeBox-Eigenschaft 74, 106 MC C# Multiprodessor C# 30 Mehrfachdatei-Assembly 23 Multi-File-Assembly 23 Member 148 Menü 330 Menüleiste 336
347
Index
menuStrip 50 MenuStrip-Control 107 Metadaten 331 Informationsdaten 18 Metaphor 30 Methode 20, 32, 35, 38, 128 Methodenaufruf 108 Methodenkopf 311 Methodenrump 36 MFC Microsoft Foundation Classes 328 Microprozessor 43 Microsoft Intermediate Language MSIL 17 Minimalprogramm 44 MinimizeBox 146 Modul 23, 49 Modula 2 31 MulticastDelegate 37 Multitasking-System 134
N Name-Eigenschaft 198 Namensraum 20, 21, 22, 38, 164 Namespace 20 Namespace 87 Namespace-Aliasqualifizierer 164 Native Code 18 Maschinencode 20 .NET 17, 22, 29 .NET Framework Class Library FCL 15 .NET-Bibliothek 37 .NET-Framework 16, 17, 18, 27, 31, 32, 113, 146, 290, 301, 328 .NET-Framework-Anwendung 21, 23 .NET-Framework-Klassenbibliothek 155 .NET-Klassenbibliothek 36 NET-Framework-Klassenbibliothek 203 .NET-Komponente 29 .NET-Laufzeitumgebung 24, 33 .NET-Plattform 29 .NET-Sprache 11, 17, 23 .NET-System 31 .NET .NET-Technologie 13 Netzwerkfunktion 16 new-Operator 38
348
Next() 126, 320 None-Enummerationsmember 209 Now-Eigenschaft 53, 159 Nullverweis 220 NumericUpDow-Control 254 NumericUpDown 51 NumericUpDown-Control Drehfeld 145 NumericUpDown-Klasse 57, 154, 254
O O(1)-Operation 211 Object-Klasse 154 Objekt 11, 24, 26, 108, 154, 211 Objekterzeugung 108, 212 Objektreferenz 24 Objektvariable 24, 114, 135 OnClick-Ereignis 274 Onlineinstallation 330 OnPaint() 166, 167 Opacity-Eigenschaft 146, 161 Operator 32, 164 Orientation-Eigenschaft 75 OS2 44 override-Schlüsselwort 166
P Paint-Ereignis 166 PaintEvent 166 PaintEventArgs-Klasse 166, 167 PaintEventArgs-Typ 166 Panel 50 Registerkarte 145 Parameter 35, 39, 74, 114, 156, 159, 166, 277, 290 Paramter 311 partial-Schlüsselwort 49 P-Code 18 PDA Personal Digital Assistant 328 Pen-Klasse 116, 167 Permutation 294 Permutationselement 295 Personal Computer 44 Pfad 59, 154 Physikalischer Prozessor 18 PictureBox-Control 113
Index
PictureBox-Klasse 274 PictureBox-Steuerelement 243, 252 Plattformabhängigkeit 19 Point-Struktur 182, 257, 271 Polyphonic C# 30 private-Feld 202 Programmcode 17 Programmiermodell 14 Programmiersprache 17, 20 Projekordner 337 Projektmappe 146, 242, 337 Projektmappen-Explorer 33, 50, 91, 105, 150, 337 Prozess 134 Prozessor 17 Prozess-Scheduling Zeitplanerstellung 45 Punktoperator 21, 57, 300
Q Qualifizierung 58 Quellcode 20 Quellcode-Verwaltungssystem 328
R RadioButton 39, 46, 51, 198, 247 RadioButton-Control 145 RadioButton-Klasse 152 RadioButton-Objekt 152 Random 109 Random-Klasse 119, 180, 260, 265, 276, 280 Rautezeichen 30 ReadToEnd() 174 Real-Time-System Echtzeitsystem 25 Rechenleistung 26, 44 Rectangle() 80, 114 Rectangle-Struktur 78, 113 Referenz 24, 35, 39 Referenztyp 220, 289, 308 Reflection API 18 Reflection-API 16 Regex-Klasse 174 Registerkarte 145, 151, 196 Regular-Enumerationsmember 168 Remote-Debugging 328
Remoting 17 Resources-Klasse 163 Ressource 22, 24, 79, 164, 174, 289 Ressourcendatei 22, 162, 328 Ressourceneditor 328 RTC Real Time Clock 44 Rückgabewert 308 Runtime Environment 17 JRE 15 Laufzeitumgebung 14 RW-Medium 331
S Schaltfläche 46 Schieberegler 95 Schlüssel 211 Schlüssel-Wert-Paar 291 Schlüsselwort 32 Schnittstelle Interface 301 Schreibeigenschaft 202 Schreibgeschwindigkeit 332 Schriftartenfamilie Schrifthöhe 168 SelectedIndex-Eigenschaft 152 sender 39 set 202 Show() 156 Sicherheitskonfiguration 17 Sichtbarkeit 36, 76, 135 Signatur 36, 159 Sing# 30 Size-Eigenschaft 200 Sleep() 134 Smaltalk 31 Softwareplattform 14 Software-Uhr 44, 53 SolidBrush-Klasse 77, 167 Sort() 311 SoundPlayer-Klasse 58 Spec# 30 Speicher 24, 289 Speicherbereich 33 Speicherbereinigung 25, 26 Speicherlecks Memory Leak 25 Speicherressource 26, 78, 183
349
Index
Speichersegment 25 Speicherstelle 24 Speicherüberlauf 43 Speicherverwaltung 26 Split() 174 SQL Datenbankabfragesprache 31 Stack 290 Stackverwaltung 290 Stammverzeichnis 146, 162, 163, 202, 243 Standardanwendungstyp 47 Standardkonstruktor 135, 276, 286, 290 Konstruktor 89 Standardsysmbolleiste 336 Starker Name 23 Startleiste 42 Steuerelement 50, 73, 156, 166, 244, 339 Control 39 Steuerelementklasse 244, 274 StopTimer 108 Stream 154, 155 StreamReader-Klasse 174 StreamWriter-Klasse 154 StreamWriter-Objekt 154 Stretch 164 Streuwerttabelle 211 string-Array 174, 286, 295, 298, 307, 311 string-Element 298 string-Format 254 String-Klasse 174, 176 string-Variable 154, 202, 265 struct 290 struct-Objekt 290 Struktur 20, 32, 33, 49, 289 Strukturvorlage 290 Submenü 45 Substring() 161 System 38 System.Sounds-Klasse 59 Systemfarbe 167 SystemSound-Klasse 59 Systemzeit 44, 62 Systemzeitgeber 136
T TabControl-Klasse 148 Tabelle 211
350
TableLayoutPanel 196 TableLayoutPanel-Aufgaben-Menü 197 TableLayoutPanel-Container TableLayoutPanel 196 TableLayoutPanel-Steuerelement 148 TabPage-Klasse 148 Taktgeber 44 Taktsignal 44 TapControl-Steuerelement 147 Tape-Pages-Auflistungs-Editor 148 Teilzeichenfolge 176 TextAlign-Eigenschaft 200, 207 TextBox 21, 196 TextBox-Control 108, 228 Textdatei 154 Text-Eigenschaft 54, 62, 200 Textfeld 39 TextWriter-Klasse 155 Textzeichenfolge 168 this 53, 290 this-Schlüsselwort 160 Thread Selbstständige Ausführungseinheit 26 Threading 17 Thread-Klasse 134 TickCount-Eigenschaft 136 Tick-Ereignis 128 Timer 55 Timer-Chip 44 Timer-Klasse Timer 56 ToInt16() 125, 160 Toolbox 50, 51, 147, 196, 246 ToString() 154, 161, 255 TrackBar-Control Schieberegler 75 TrackBar-Steuerelement 86 Turbo Pascal 31 Typ 18, 21, 22, 35, 38 Typdeklaration 49 Typimplementierung 49 Typsicherheit 36 Typverletzung 31
U Überladungsliste 61 UML Modellierungssprache 20
Index
unsafe 33 C#-Modifizierer 32 unsafe-Anweisung 32 Unsicherer Code 32 Unsicherer Kontext 32 Unterarray 181, 286, 308, 322 using-Direktive 49, 59, 112, 153, 286 UTF-8 154
V ValueChanged-Ereignis 254 Value-Eigenschaft 57 Values-Eigenschaft 216 Variable 20 Vererbungshierachie 37 Vergleichsmethode 301 Versionsnummer 23 Verteilte Systeme 44 Verweis 18, 22, 24, 87, 88, 92, 337 Virtuelle Festplatte 334 Virtuelle Maschine 14 Visible-Eigenschaft 156 Visual Basic.NET 17 Visualisierung 144 void 153 von Array-Grenze 33 Vorlage 162 Vorlagen 87 VScrollBar Elementliste 28
W WAV-Datei 58 Web Installer 330 Webanwendung 16 Web-Applikation 34, 39 Webpublishing-Assistent 339 Webservice 16 Website 339 Webapplikation 27 Wert 211 Wert-Schlüssel-Paar 212 Werttyp 257, 280, 301 while-do-until 188 while-Schleife 121, 136, 173, 223, 300, 301, 312 Width-Eigenschaft 179
Window 199 Windows 44 Windows Explorer 50, 337 Windows Forms-Anwendung 47, 196 Windows Presentation Foundation 21 Windows-Applikation 34 Windows-Drehfeld NumericUpDown-Control 57 WindowsForm 74, 107, 113, 144, 160, 196 WindowsForm -Steuerelement 166 WindowsForms-Anwendung 105, 136, 146, 241, 327 Windows-Forms-Steuerelement 27 WindowsForms-Steuerelement 21, 78 Windows-Meldung 136, 173 Windows-Startleiste Startleiste 44 Windows-Steuerelement 21 WinForm 328 WinMain 22 World Wide Web 37 WPF Windows Presentation Foundation 328 WPF-Anwendung 328 Write() 154
X XAML Extensible Application Markup Language 328 XC# eXtensible C# 30 Xetra Exchange Elektronic Trading 143 XML 328 Markup-Sprache 20 XML-Datei 342
Z Zähler 44 Zeichen 154 Zeichenbereich 114 Zeichenfolge 174, 291 Zeichenfolgedarstellung 154 Zeichenfolgeliteral 175, 257 Zeichenfolgen-Editor 75, 108, 149
351
Index
Zeichenfolgenliteral 59, 310 Zeichenlogik 166 Zeichennoberfläche 166 Zeichenoberfläche 179 Zeiger 32 Pointer 31
352
Zeigertyp 34 Zielobjekt 35 Zugriffsmodifizierer 178 Zwischencode IL-Code 18