Windows Forms in 21 Tagen - Oberflächen Programmieren [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Chris Payne Übersetzung: Michael Matzer

.NET Windows Forms ■ ■ ■

Oberf berfl rflächen hen ob objektorient entiert programmieren ADO.NET, ActiveX, Multithreading, Web Services Klassenbi nbiblio liothek des .NET Framework ken kenn ennenl enlernen

Markt + Technik Verlag

Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Authorisierte Übersetzung der amerikanischen Originalausgabe: „Teach Yourself .NET Windows Forms in 21 Days“ Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Software-Bezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig auch eingetragene Marken oder sollten als solche betrachtet werden. Authorized translation from the English language edition, entiteled SAMS TEACH YOURSELF .NET Windows Forms in 21 DAYS by Chris Payne, published by Pearson Education, Inc. publishing as Sams Publishing, Copyright © 2002 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronis or mechanical, including photocopying, recording or by any Information storage retrieval system, without permisson from Pearson Education, Inc. German language edition published by PEARSON EDUCATION DEUTSCHLAND, Copyright © 2002 Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt.

10 9 8 7 6 5 4 3 2 1 05 04 03

ISBN 3-8272-6455-3

© 2003 by Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D–81829 München/Germany Alle Rechte vorbehalten Übersetzung: Michael Matzer, Waldenbruch Lektorat: Rainer fuchs, [email protected] Fachlektorat: Frank Langenau, Chemnitz Herstellung: Philipp Burkart, [email protected] Satz: reemers publishing services gmbh, Krefeld, (www.reemers.de) Einbandgestaltung: Grafikdesign Heinz H. Rauner, Gmund Druck und Verarbeitung: Bercker, Kevelaer Printed in Germany

Inhaltsverzeichnis Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wer dieses Buch lesen sollte . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorkenntnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Software für die Beispiele. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie dieses Buch aufgebaut ist . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmcode auf Begleit-CD und US-Website . . . . . . . . . . . . Konventionen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Und zu guter Letzt... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

17 17 18 18 19 20 21 21

Woche 1 – Vorschau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

23

Tag 1

Mit Windows Forms beginnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 Einführung in die Windows-Programmierung . . . . . . . . . . . . . . Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Win32-Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Was ist .NET?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .NET-Kompilierung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Common Language Runtime (CLR). . . . . . . . . . . . . . . . . . Das .NET Framework installieren. . . . . . . . . . . . . . . . . . . . . . . . Entwicklungsumgebungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Was ist .NET Windows Forms? . . . . . . . . . . . . . . . . . . . . . . . . . . Integration mit .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Windows Forms-Steuerelemente. . . . . . . . . . . . . . . . . . . . . . . . . 1.4 Die objektorientierte Vorgehensweise . . . . . . . . . . . . . . . . . . . . . 1.5 So erstellen Sie Ihre erste Anwendung . . . . . . . . . . . . . . . . . . . . 1.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

25 26 27 28 29 30 32 33 34 35 36 37 38 39 41 42 42 42 43

Tag 2

Windows Forms-Anwendungen erstellen . . . . . . . . . . . . . . . . . . . . . . . 2.1 Ein weiterer Blick auf Ihre Anwendung . . . . . . . . . . . . . . . . . . . Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

45 46 47

5

Inhaltsverzeichnis

Namensräume und Assemblies . . . . . . . . . . . . . . . . . . . . . . . . . . Die Main-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstruktoren und Destruktoren. . . . . . . . . . . . . . . . . . . . . . . . . Ein abschließender Blick auf den Programmcode . . . . . . . . . . . Das Kompilieren von Windows Forms . . . . . . . . . . . . . . . . . . . . Eine vollständige Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

49 54 58 62 63 67 74 74 75 75 75

Mit Windows Forms arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

77

2.2 2.3 2.4 2.5 2.6

Tag 3

3.1

Das objektorientierte Windows-Formular . . . . . . . . . . . . . . . . . . Mit dem Objekt Object arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . Formular-Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formular-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignisbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Nachrichtenschleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Form-Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

78 79 82 92 94 94 97 106 107 108 108 109

Menüs und Symbolleisten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

111

4.1 4.2

112 114 120 122 125 126 133 136 146 147 148 148 148

3.2

3.3 3.4 3.5

Tag 4

4.3

4.4 4.5 4.6

6

Einführung in Windows Forms-Steuerelemente . . . . . . . . . . . . Menüs hinzufügen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Menüs anpassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kontextmenüs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen. . . Symbolleisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statusleisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bildlaufleisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Inhaltsverzeichnis

Tag 5

Ereignisse in Windows Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 Was sind Ereignishandler? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignisse behandeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mehrfache Ereignisse behandeln . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Delegaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3 Ereignisse und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignishandler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzerdefinierte Ereignisse erstellen . . . . . . . . . . . . . . . . . . . . Steuerelemente sezieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzerdefinierte Delegaten erstellen . . . . . . . . . . . . . . . . . . . . 5.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

151 152 153 156 158 162 162 163 168 169 173 173 174 174 175

Tag 6

Windows Forms mit Steuerelementen erweitern . . . . . . . . . . . . . . . . . 6.1 Das Steuerelement Button. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Die Steuerelemente CheckBox und RadioButton . . . . . . . . . . . 6.3 Das Steuerelement ComboBox . . . . . . . . . . . . . . . . . . . . . . . . . . Das Kombinationsfeld instantiieren . . . . . . . . . . . . . . . . . . . . . . Die Anzeige des Steuerelements steuern . . . . . . . . . . . . . . . . . . Die ComboBox verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4 Das Steuerelement DateTimePicker. . . . . . . . . . . . . . . . . . . . . . 6.5 Das Steuerelement ImageList . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6 Das Steuerelement ListBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.7 Das Steuerelement PictureBox . . . . . . . . . . . . . . . . . . . . . . . . . . 6.8 Das Steuerelement TabControl . . . . . . . . . . . . . . . . . . . . . . . . . . 6.9 Die Steuerelemente TextBox und RichTextBox . . . . . . . . . . . . . Rich Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.10 Das Steuerelement Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.11 Das Steuerelement TreeView . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.12 Zusätzliche nützliche Steuerelemente . . . . . . . . . . . . . . . . . . . . 6.13 Das Layout steuern. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.14 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.15 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.16 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

177 179 180 183 184 186 187 189 193 195 199 200 206 208 215 218 221 222 228 229 230 230 230

7

Inhaltsverzeichnis

Tag 7

Mit Dialogfeldern arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.1 Informationen holen und anzeigen . . . . . . . . . . . . . . . . . . . . . . . 7.2 Unterschiedliche Arten von Dialogen. . . . . . . . . . . . . . . . . . . . . Modal vs. nichtmodal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Standarddialogfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 Dialogfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Dialogfelder erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . Informationen abrufen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.4 Standarddialogfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein- und Ausgabe von Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . Farben und Schriftarten wählen . . . . . . . . . . . . . . . . . . . . . . . . . Drucken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.6 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.7 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

231 232 234 234 235 235 238 244 248 248 250 253 257 258 259 259 260

Woche 1 – Rückblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

261

Projekt 1: Eine Textverarbeitung erstellen. . . . . . . . . . . . . . . . . . Über das Textverarbeitungsprojekt. . . . . . . . . . . . . . . . . . . . . . . . Was Sie benötigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Benutzeroberfläche aufbauen. . . . . . . . . . . . . . . . . . . . . . . . Einige nützliche Dinge mit Pfiff hinzufügen . . . . . . . . . . . . . . .

261 261 262 262 274

Woche 2 – Vorschau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

279

Tag 8

Datenbindung in Windows Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . .

281

8.1

282 283 284 285 287 290 290 293 296 306 307

8.2

8.3

8.4 8.5

8

Einführung in die Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . Daten aus jeder beliebigen Quelle . . . . . . . . . . . . . . . . . . . . . . . Einfache Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine Schaltfläche mit Daten verbinden . . . . . . . . . . . . . . . . . . . Das Bearbeiten gebundener Daten . . . . . . . . . . . . . . . . . . . . . . . Formulare mit Daten verbinden . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement DataGrid . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein kurzer Blick auf ADO.NET . . . . . . . . . . . . . . . . . . . . . . . . . Die Interaktion mit einem DataGrid . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Inhaltsverzeichnis

8.6

Tag 9

Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

307 307 308

ADO.NET einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

309

9.1

Was ist ADO.NET? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ADO.NET gegenüber ADO . . . . . . . . . . . . . . . . . . . . . . . . . . . . ADO.NET und XML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine Datenbank anlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine Access-Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine Datenbank in SQL Server 2000 anlegen . . . . . . . . . . . . . . SQL für die Datenbankabfrage . . . . . . . . . . . . . . . . . . . . . . . . . . Das SELECT-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das INSERT-Statement. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das UPDATE-Statement. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das DELETE-Statement. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einführung in DataSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Objektmodell von ADO.NET . . . . . . . . . . . . . . . . . . . . . . . Verbindung zur Datenbank aufnehmen und Daten abrufen. . . Daten bearbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Andere ADO.NET-Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parametrisierte Abfragen und gespeicherte Prozeduren . . . . . . . Das Objekt DataView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XML im Zusammenhang mit ADO.NET . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

310 311 313 314 314 317 319 319 322 322 323 323 325 326 329 334 338 341 342 345 346 347 347 348

MDI-Anwendungen erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.1 Einführung in die Mehrfachdokumentschnittstelle . . . . . . . . . . MDI-Menüs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.2 Eine MDI-Anwendung erstellen . . . . . . . . . . . . . . . . . . . . . . . . . Verschiedene Arten von untergeordneten Fenstern erstellen. . . 10.3 Die Besonderheiten von MDI-Formularen steuern . . . . . . . . . . Menüs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Untergeordnete Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Auf untergeordnete Steuerelemente zugreifen . . . . . . . . . . . . . . 10.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

349 350 352 353 361 362 362 365 367 372

9.2

9.3

9.4 9.5

9.6

9.7 9.8 9.9 9.10

Tag 10

9

Inhaltsverzeichnis

10.5 10.6

Tag 11

Arbeiten mit der Windows-Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . .

377 378 380 388 393 396 400 402 405 406 411 412 413 413 413

Einführung in Streams. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dateien und Verzeichnisse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dateien lesen und schreiben . . . . . . . . . . . . . . . . . . . . . . . . . . . . Binärdaten lesen und schreiben . . . . . . . . . . . . . . . . . . . . . . . . . Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement RichTextBox . . . . . . . . . . . . . . . . . . . . . . . . Das Objekt FileSystemWatcher. . . . . . . . . . . . . . . . . . . . . . . . . . Asynchrone Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Drucken unter .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Formulare Internet-fähig machen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

415

12.1

416 418 419 419 427 428 430

12.2

12.3

12.4

12.5 12.6 12.7

10

373 374 374 375

11.1 11.2

11.3 11.4 11.5 11.6 11.7 11.8

Tag 12

Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Internetprotokolle. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Austauschbare Protokolle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clients und Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anforderungen mit WebRequest senden . . . . . . . . . . . . . . . . . . Anfragen mit WebResponse verarbeiten . . . . . . . . . . . . . . . . . . . Protokollspezifische Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arbeiten mit dem Internet Explorer . . . . . . . . . . . . . . . . . . . . . . Windows Forms-Steuerelemente in den Internet Explorer einbetten . . . . . . . . . . . . . . . . . . . . . . . . . Internetsicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Authentifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Berechtigungsklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

430 433 433 434 435 436 436 436 437

Inhaltsverzeichnis

Tag 13

Grafische Anwendungen mit GDI+ . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.1 Einführung in die Grafik und Bildbearbeitung unter Windows Pinsel und Stifte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rechtecke und Bereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.2 Das Ereignis Paint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3 Text zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4 Grafiken anzeigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.5 Figuren zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Figuren ausfüllen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Figuren dauerhaft machen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Figuren umwandeln. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.6 Windows Forms beschneiden . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.8 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.9 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

439 440 442 442 443 446 450 454 460 468 470 471 473 474 475 475 475

Tag 14

ActiveX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.1 Einführung in ActiveX. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.2 ActiveX-Steuerelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das ActiveX-Steuerelement Webbrowser . . . . . . . . . . . . . . . . . . Die Typbibliotheken von Microsoft Office . . . . . . . . . . . . . . . . . 14.3 .NET-ActiveX-Steuerelemente erstellen . . . . . . . . . . . . . . . . . . . 14.4 Richtlinien für das Arbeiten mit ActiveX. . . . . . . . . . . . . . . . . . . 14.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.6 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.7 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

477 478 480 482 485 499 500 502 502 503 503 504

Woche 2 – Rückblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

505

Projekt 2: Die Textverarbeitung erweitern. . . . . . . . . . . . . . . . . . MDI hinzufügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . E/A hinzufügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Druckfunktionen hinzufügen . . . . . . . . . . . . . . . . . . . . . . . . . . .

505 505 507 511

11

Inhaltsverzeichnis

Woche 3 – Vorschau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

513

Tag 15

Webdienste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

515

15.1

Was sind Webdienste? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Simple Object Access Protocol (SOAP). . . . . . . . . . . . . . . . Andere Protokolle für Webdienste. . . . . . . . . . . . . . . . . . . . . . . . Mit ASP.NET Webdienste erstellen. . . . . . . . . . . . . . . . . . . . . . . Webmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Dienstbeschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Webdienste unter ASP.NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . Webdienste in Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . Webdienste suchen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Empfehlungen für den Einsatz von Webdiensten. . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

516 519 520 520 523 524 526 526 534 535 536 537 538 538 538

Windows-Dienste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

539

16.1 16.2 16.3

Was sind Windows-Dienste? . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einen Windows-Dienst erzeugen . . . . . . . . . . . . . . . . . . . . . . . . Dienste zum Zusammenspiel mit Windows bringen . . . . . . . . . Windows-Dienste starten und beenden. . . . . . . . . . . . . . . . . . . . Ihre Dienste überwachen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schnittstellen für Windows-Dienste . . . . . . . . . . . . . . . . . . . . . . In Protokolldateien schreiben . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

540 542 549 552 553 555 560 562 563 564 564 564

Fortgeschrittene Techniken für Steuerelemente in Windows Forms .

565

15.2

15.3 15.4 15.5 15.6 15.7 15.8

Tag 16

16.4 16.5 16.6 16.7 16.8

Tag 17

17.1 17.2

12

Noch einmal: Menüs und die Steuerelemente Listbox, ComboBox und TabControl . . . . . . . . . . . . . . . . . . . . . DrawMode festlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Steuerelemente ListBox und ComboBox erweitern . . . . . . Das Steuerelement TabControl erweitern . . . . . . . . . . . . . . . . .

566 568 570 578

Inhaltsverzeichnis

17.3 17.4 17.5 17.6

Menüs: Eine Frage des Besitzes . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

581 586 587 588 588 588

Tag 18

Benutzerdefinierte Steuerelemente in Windows Forms . . . . . . . . . . . 18.1 Wozu benutzerdefinierte Steuerelemente erzeugen? . . . . . . . . . 18.2 Benutzerdefinierte Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . Rekapitulation der Architektur für Steuerelemente . . . . . . . . . . Eigenschaften, Methoden und Ereignisse. . . . . . . . . . . . . . . . . . Ihr Steuerelement zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Alles zusammensetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.3 Benutzerdefinierte Steuerelemente in Windows Forms . . . . . . . 18.4 Benutzerdefinierte Steuerelemente aus vorhandenen erzeugen Vorhandene Steuerelemente erweitern. . . . . . . . . . . . . . . . . . . . Benutzersteuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.6 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.7 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

589 590 591 592 594 599 605 609 610 611 613 617 618 618 619 619

Tag 19

Multithreading-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19.1 Einführung in Multithreading. . . . . . . . . . . . . . . . . . . . . . . . . . . Probleme mit Threading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threading-Objekte verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . 19.2 Eine Multithreading-Anwendung erstellen . . . . . . . . . . . . . . . . . Threads miteinander synchronisieren . . . . . . . . . . . . . . . . . . . . . Threadpooling einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19.4 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19.5 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

621 622 626 627 628 635 645 652 653 654 654 654

Tag 20

Windows Forms konfigurieren und bereitstellen. . . . . . . . . . . . . . . . . 20.1 Einführung in die .NET-Konfiguration. . . . . . . . . . . . . . . . . . . . 20.2 So konfigurieren Sie Ihre Anwendungen . . . . . . . . . . . . . . . . . .

655 656 659

13

Inhaltsverzeichnis

Konfigurationsabschnitte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Abschnitt . . . . . . . . . . . . . . . . . . . . . . . . . Der Abschnitt . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Abschnitt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Abschnitt . . . . . . . . . . . . . . . . . . . Konfigurationswerte abrufen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lokalisieren Sie Ihre Anwendung . . . . . . . . . . . . . . . . . . . . . . . . Eingabehilfen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der einfache Teil der Arbeit: Bereitstellung . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

660 662 664 665 666 667 669 676 677 678 679 680 680 680

Debugging und Profiling. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.1 Debuggen in Windows Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2 JIT-Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Werkzeug DbgClr. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Werkzeug CorDbg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.3 Profiling Ihrer Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.5 Fragen und Antworten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.6 Workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

681 682 689 690 694 696 702 703 704 704 704

Woche 3 – Rückblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

705

Projekt 3: Das Textprogramm vervollständigen . . . . . . . . . . . . . . Eine Klasse für eine neue Seite erzeugen . . . . . . . . . . . . . . . . . . Der weitere Weg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

705 705 722

Anhang A. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.1 Antworten auf die Quizfragen und Übungen . . . . . . . . . . . . . . .

723 724

Stichwortverzeichnis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

809

20.3 20.4 20.5 20.6 20.7 20.8

Tag 21

14

Inhaltsverzeichnis

Inhaltsverzeichnis der Begleit-CD Bonuskapitel 1: Steuerelemente in Windows Forms. . . . . . . . . . . . . . 1.1 Die Klasse Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Die Steuerelemente Button, CheckBox und RadioButton. . . . . Das Steuerelement Button. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement CheckBox. . . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement RadioButton . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Die Steuerelemente ComboBox, ListBox und CheckedListBox Das Steuerelement ComboBox . . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement ListBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement CheckedListBox. . . . . . . . . . . . . . . . . . . . . . 1.4 Standarddialogfeld-Steuerelemente. . . . . . . . . . . . . . . . . . . . . . . Das Steuerelement ColorDialog . . . . . . . . . . . . . . . . . . . . . . . . . Dateidialogfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Menüs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ContextMenu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MainMenu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MenuItem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Das Steuerelement DataGrid . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 DateTimePicker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Bildlauffähige Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . DomainUpDown . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . NumericUpDown . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Panel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PrintPreviewDialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PropertyGrid. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TabPage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.9 ErrorProvider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10 GroupBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11 Bildlaufleisten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HScrollBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VScrollBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12 ImageList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.13 Label-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.14 ListView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.15 PictureBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1 2 11 11 12 13 13 14 16 18 19 20 20 25 25 26 26 28 32 33 35 36 39 39 40 40 42 42 43 44 45 45 45 46 47 50

15

Inhaltsverzeichnis

1.16 1.17 1.18 1.19

PrintDocument . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PrintPreviewControl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ProgressBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Textfeld-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . RichTextBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TextBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Steuerelemente der Statusleiste. . . . . . . . . . . . . . . . . . . . . . . . . . TabControl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Steuerelemente der Symbolleiste . . . . . . . . . . . . . . . . . . . . . . . . ToolTip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TrackBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TreeView-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

50 51 52 53 55 57 58 59 61 61 63 64 65

Bonuskapitel 2: Steuerelemente in ADO.NET: Eigenschaften und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Die DataSet- und verwandte Klassen . . . . . . . . . . . . . . . . . . . . . Constraint und ConstraintCollection . . . . . . . . . . . . . . . . . . . . . DataColumn und DataColumnCollection. . . . . . . . . . . . . . . . . 2.2 DataRelation und DataRelationCollection . . . . . . . . . . . . . . . . 2.3 DataRow und DataRowCollection . . . . . . . . . . . . . . . . . . . . . . . DataSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . DataTable und DataTableCollection . . . . . . . . . . . . . . . . . . . . . DataView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Verwaltete Provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDbCommand. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDBCommandBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDbConnection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDbDataAdapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDbdataReader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OleDbError und OleDbErrorCollection . . . . . . . . . . . . . . . . . . OleDbParameter und OleDbParameterCollection . . . . . . . . . . OleDbTransaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

71 72 72 73 75 77 79 81 85 86 87 88 88 90 92 93 94 96

1.20 1.21 1.22 1.23 1.24 1.25 1.26

16

Einleitung Willkommen bei .NET Windows Forms in 21 Tagen! Dieses Buch wird Sie Schritt für Schritt von den Grundlagen in Microsoft .NET Windows Forms zu recht fortgeschrittenen Themen führen. Während der nächsten 21 Tage erwartet Sie ein sehr informatives und aufregendes Abenteuer. Die Windows Forms-Technologie ist Bestandteil der .NET-Initiative Microsofts. Sie erlaubt Ihnen auf sehr einfache Weise leistungsfähige Windows-Anwendungen zu erstellen, wobei Sie fast jede beliebige Programmiersprache verwenden können, die Sie kennen. Vom einfachen Taschenrechner bis hin zu Datenbank-gestützten multithreaded Webdiensten können Sie praktisch alles erstellen (wenn Sie von Multithreading oder Datenbankzugriffen keine Ahnung haben, dann lesen Sie am besten weiter). Einfach ausgedrückt: Wenn Sie künftig Windows-Anwendungen entwickeln wollen, werden Sie Windows Forms benutzen. Während Sie sich durch die Lektionen dieses Buches arbeiten, werden Sie entdecken, auf welch einfache Art und Weise sich diese Aufgaben bewältigen lassen. Sie werden nicht nur Beispiele sehen und lernen, wie Sie diese und andere Anwendungen erzeugen, sondern werden auch ein Verständnis dafür entwickeln, das Sie in die Lage versetzt, bei jedem künftigen Windows-Programmierprojekt, das Sie vielleicht anpacken wollen, zuversichtlich an dieses Unterfangen heranzugehen.

Wer dieses Buch lesen sollte Es ist der Zweck dieses Buches, Einsteiger in Sachen Windows Forms mit dieser leistungsfähigen Technologie vertraut zu machen. »Einsteiger« ist jedoch ein recht allgemeiner Begriff. Ganz konkret sollten Sie dieses Buch verwenden, wenn Sie lernen wollen, wie Sie Windows-Anwendungen in .NET erstellen können, ganz gleich, ob Sie bereits Erfahrung mit ihrer Erstellung in einer anderen Umgebung haben oder nicht. Da aber Microsofts Zukunft auf .NET fußt, wird jeder Windows-Anwendungsentwickler früher oder später auf Windows Forms umsteigen müssen. Selbst wenn Sie Windows Forms bereits kennen, wird Ihnen dieses Buch ein unschätzbares Hilfsmittel sein, das dazu beiträgt, vergessenes Grundlagenwissen aufzufrischen und Sie auf fortgeschrittenere Projekte vorzubereiten. Wer bereits mit anderen Methoden der Anwendungsentwicklung für Windows vertraut ist, wird Windows Forms als Erleichterung empfinden, da es bislang lästige Arbeitsschritte wie

Einleitung

etwa Speicherverwaltung zu einem Kinderspiel macht. Sie werden sehen, dass viele der Kenntnisse, die Sie bereits erworben haben, sich auch weiterhin in diesem neuen Rahmenwerk anwenden lassen. Und schließlich: Wenn Sie einfach nur neugierig auf das .NET Framework sind, dient Ihnen dieses Buch als ausgezeichnete Einführung und führt Sie durch viele der zentralen Themen, die sich auf die Anwendungsentwicklung beziehen.

Vorkenntnisse Die einzigen absolut erforderlichen Voraussetzungen sind Grundkenntnisse im Umgang mit dem Betriebssystem Windows, von der Bedienung der Eingabeaufforderung bis zum Arbeiten mit Anwendungsfenstern. Die Kenntnis einer .NET-Programmiersprache wie C# oder VB .NET wird sich als äußerst hilfreich erweisen, ist aber keine Voraussetzung. Dieses Buch führt Sie rasch in die Grundlagen dieser beiden Programmiersprachen ein und stellt jedes neue Thema vor, bevor diese Sprachen verwendet werden. Wenn Sie dem Code und Text folgen können – und sowohl C# als auch VB .NET sind einfach zu lesen – dann werden Sie keine Probleme haben. Kenntnisse des .NET Frameworks sind nicht erforderlich. Dieses Buch befasst sich gründlich mit dem Framework und wird nötigenfalls auf weitere Ressourcen verweisen.

Software für die Beispiele Die einzige Software, die Sie neben Windows NT 4.0 mit Service Pack (SP) 6, Windows 2000 oder Windows XP benötigen, ist das .NET Framework und der Windows-Editor (NotePad). Das .NET Framework ist notwendig, um Windows Forms-Anwendungen erstellen zu können, und lässt sich kostenlos von Microsofts Website http://www.microsoft.com/net herunterladen. Außerdem finden Sie das .NET Framework SDK auf der Begleit-CD. Um Anwendungen zu schreiben, brauchen Sie lediglich ein Textprogramm, das einfache Textdateien (zum Beispiel mit der Endung .txt) erstellen kann. Der Windows-Editor eignet sich für diesen Zweck hervorragend. Wenn Ihnen ein anderes Textprogramm wie etwa Word, WordPad oder gar FrontPage eher behagt, dann können Sie selbstverständlich auch dieses benutzen. Visual Studio .NET wird hier nicht benötigt. In der Tat wird es im ganzen Buch recht selten verwendet. Während es zutrifft, dass Visual Studio .NET eine Menge ausgezeichneter Funktionen für die Anwendungsentwicklung bereitstellt, sind wir doch der Meinung, dass

18

Wie dieses Buch aufgebaut ist

es am besten ist, die Aufgaben »zu Fuß« zu bewältigen, bevor man sie von einer solchen Entwicklungsumgebung erledigen lässt. Auf lange Sicht werden Sie dafür dankbar sein. Um schließlich die Datenbank-bezogenen Beispiele verwenden zu können, benötigen Sie irgendeine Form von Datenbanksystem, das zu OLE DB konform ist, beispielsweise Microsoft Access oder SQL Server. Ohne diese Werkzeuge werden Sie die diesbezüglichen Beispiele nicht nutzen können.

Wie dieses Buch aufgebaut ist Das Buch ist als Serie von Lernabschnitten, als Tutorial, aufgebaut, so dass jeder Tag auf dem vorhergehenden aufbaut. Es ist sinnvoll, Schritt für Schritt vorzugehen, wenn auch nicht zwingend. Nach der Erörterung des Themas folgen jeweils ein paar Beispiele, die von Analysen begleitet werden. Jedes Kapitel endet mit Antworten auf häufig gestellte Fragen, einem kurzen Quiz und einer Übung. Diese Bestandteile sind sorgfältig ausgewählt, so dass sie Ihr Wissen erweitern und Ihre Erfahrung mit der Technologie vertiefen können. Das Buch ist in drei logische Abschnitte aufgeteilt, von denen sich jeder über eine Woche erstreckt, sowie in einen Block aus drei Anhängen: 쐽

Die erste Woche werden Sie damit verbringen, die Grundlagen von .NET und Windows Forms zu erlernen. Sie werden verstehen, was zur Erstellung einer Windows Forms-Anwendung gehört, werden herausfinden, wie man dabei vorgeht und mit den Steuerelementen umgehen lernen, die Ihre Anwendung mit Funktionen versehen. Die feste Grundlage, die Sie in dieser Woche aufbauen, hilft Ihnen in den nächsten zwei Wochen.



Während der gesamten zweiten Woche befassen Sie sich mit Technologien, die den Funktionsumfang Ihrer Windows Forms-Anwendung erweitern, angefangen mit ActiveX bis hin zu ADO.NET. Sie sind nicht unbedingt ein Teil von Windows Forms, aber Sie werden sie höchstwahrscheinlich bei der Anwendungsentwicklung benötigen.



In der dritten Woche reichen die Windows Forms-Themen schon ein gutes Stück tiefer. Sie lernen, wie Sie Ihre Anwendungen vollständig anpassen, angefangen vom Aufbau leistungsfähiger (und vermarktbarer) Steuerelemente bis hin zur Konfiguration von Installationsoptionen und Sprachen. Sie werden sich einige Techniken aneignen, mit denen Sie die Leistung Ihrer Anwendungen beträchtlich erhöhen können: von Multithreading bis hin zu Profiling.

19

Einleitung

Am Ende jeder Woche werden Sie ein wenig Zeit auf das Rekapitulieren der Lektionen verwenden und daraufhin das Wissen dieser Woche auf ein echtes Projekt anwenden. Sie werden ein Textverarbeitungsprogramm ähnlich wie Microsoft Word von Anfang an aufbauen, indem Sie es durch neu erlernte Funktionen und Technologien erweitern. Die Rekapitulation jeder Woche beruht auf den entsprechenden Lektionen. Diese Projekte bieten Ihnen die Gelegenheit, Ihre Kenntnisse, die Sie während dieser Woche erworben haben, anzuwenden und zu festigen. 쐽

Schließlich erhalten Sie mit den Anhängen eine vollständige Referenz für die in dem vorliegenden Buch behandelten Themen. Um Antworten auf die Quizfragen zu erhalten, können Sie hier nachschlagen. Damit können Sie auch Ihr Wissen über die im Buch vorgestellten Steuerelemente und Objekte auffrischen.

Programmcode auf Begleit-CD und US-Website Auf der Begleit-CD finden Sie den vollständigen Code für alle Beispiele des Buches in der amerikanischen Originalversion. Die im Buch wiedergegebenen Listings unterscheiden sich hiervon nur darin, dass Beschriftungen von sichtbaren Elementen der Benutzeroberfläche sowie die Kommentare ins Deutsche übersetzt wurden. Bei eventuellen Problemen mit der Lokalisierung Ihres eigenen Codes können Sie so durch Vergleich mit dem Originalcode die Fehlerursachen schnell einkreisen. Den Code für dieses Buch können Sie auch von der Sams Publishing-Website herunterladen: http://www.samspublishing.com/. Geben Sie die ISBN in das Suchfeld ein (0672323206 – ohne Bindestriche!) und klicken Sie auf SEARCH. Sobald der Buchtitel angezeigt wird, klicken Sie darauf und gehen auf eine Seite, wo Sie den Code herunterladen können. Auf dieser Website finden Sie gegebenenfalls auch aktualisierte bzw. fehlerbereinigte Versionen der Codebeispiele. Weitere Ressourcen finden Sie auf meiner Homepage http://www.clpayne.com. Hier finden Sie Links zu entsprechenden Informationen wie etwa Artikeln und Quellcode.

20

Konventionen

Konventionen Im Buch werden die folgenden typographischen Konventionen verwendet: 쐽

Codezeilen, Befehle, Anweisungen, Variablen und jeglicher Text, den Sie entweder eingeben oder der auf dem Bildschirm erscheint, werden in Schreibmaschinenschrift angezeigt. Benutzereingaben werden häufig in Fettschrift dargestellt.



Platzhalter in Syntaxbeschreibungen werden kursiv gedruckt. Ersetzen Sie den Platzhalter durch den tatsächlichen Dateinamen, Parameter oder das Element, das Sie verwenden möchten.



Manchmal sind Codezeilen zu lang, als dass sie auf eine einzelne Zeile auf der Seite passen. An der fehlenden Zeilennummer können Sie erkennen, wenn eine Codezeile noch zur darüber liegenden Zeile gehört. Die Analyseabschnitte nach Codelistings besprechen den Code im Detail. Hier finden Sie heraus, was ein bestimmtes Stück Code bewirkt und wie es funktioniert, sowie häufig eine Betrachtung Zeile für Zeile.



Ferner enthält dieses Buch Hinweise, Tipps und Warnungen, die Ihnen dabei helfen sollen, wichtige oder hilfreiche Informationen schneller auszumachen. Einige stellen nützliche Hilfestellungen zum effizienteren Arbeiten dar. Außerdem finden Sie nützliche Gegenüberstellungen der Art »Tun Sie dies/Tun Sie dies nicht«. Hier finden Sie Vorschläge über den richtigen Weg, eine Aufgabe zu lösen (und Warnungen vor dem falschen).

Und zu guter Letzt... Ich hoffe, Sie haben viel Spaß beim Erlernen dieser interessanten Technologie. Sie wird es Ihnen ermöglichen, mit Windows viele verblüffende Dinge, die auf dem heutigen Markt sehr gefragt sind, anzustellen. Als Leser dieses Buches sind Sie für Autor, Übersetzer, Fachlektor und Verlag der wichtigste Kritiker und Kommentator. Bitte teilen Sie uns mit, was Sie an diesem Buch positiv finden, was wir besser machen könnten oder was Ihnen sonst am Herzen liegt. Wenden Sie sich bitte an den Lektor Rainer Fuchs, [email protected], der Ihnen weiterhilft oder Ihre Anfrage weiterleitet.

21

Tag 1

Mit Windows Forms beginnen

25

Tag 2

Windows Forms-Anwendungen erstellen

45

Tag 3

Mit Windows Forms arbeiten

77

Tag 4

Menüs und Symbolleisten

111

Tag 5

Ereignisse in Windows Forms

151

Tag 6

Windows Forms mit Steuerelementen erweitern

177

Tag 7

Mit Dialogfeldern arbeiten

231

Tag 8

Datenbindung in Windows Forms

281

Tag 9

ADO.NET einsetzen

309

Tag 10

MDI-Anwendungen erstellen

349

Tag 11

Arbeiten mit der Windows-Ein-/Ausgabe

377

Tag 12

Formulare Internet-fähig machen

415

Tag 13

Grafische Anwendungen mit GDI+

439

Tag 14

ActiveX

477

Tag 15

Webdienste

515

Tag 16

Windows-Dienste

539

Tag 17

Fortgeschrittene Techniken für Steuerelemente in Windows Forms

565

Benutzerdefinierte Steuerelemente in Windows Forms

589

Tag 19

Multithreading-Anwendungen

621

Tag 20

Windows Forms konfigurieren und bereitstellen

655

Tag 21

Debugging und Profiling

681

Tag 18

W O C H E

W O C H E

W O C H E

Woche 1: Die Grundlagen Auf Ihrer Reise durch Windows Forms erlernen Sie in der ersten Woche die Grundlagen. Die ersten sieben Lektionen vermitteln Ihnen ein festes Fundament, so dass Sie in den Wochen 2 und 3 fortgeschrittenere Themen angehen können. Tag 1 mit dem Titel »Mit Windows Forms beginnen« stellt Ihnen das .NET Framework, Windows Forms und die Windows-Programmierung vor. Sie werden in dieser Lektion auch Ihre erste Anwendung sehen. An Tag 2 mit dem Titel »Windows Forms-Anwendungen erstellen« werden Sie anfangen, Ihre erste eigene Windows-Anwendung zu erzeugen und mehr darüber erfahren, wie Sie diese Anwendung kompilieren und ausführen. Tag 3 mit dem Titel »Mit Windows Forms arbeiten« ist eine sehr wichtige Lektion. Sie erfahren alles über das Objekt Form, den Hauptbestandteil jeder Windows Forms-Anwendung. Sie werden erstmals auch einen Blick darauf werfen, wie Sie Ereignisse behandeln, d.h. wie Sie Ihre Anwendung auf Benutzereingaben reagieren lassen. An Tag 4 mit dem Titel »Menüs und Symbolleisten« werden Sie von vielen der Steuerelemente im Windows Forms-Framework erfahren, die Ihnen das Leben als Entwickler sehr erleichtern. Tag 5, »Ereignisse in Windows Forms«, lehrt Sie, wie Sie auf verschiedene Geschehnisse in Ihrer Anwendung, wie etwa Mausklicks auf Schaltflächen oder Menüs, reagieren können. An Tag 6, der den Titel »Windows Forms mit Steuerelementen erweitern« trägt, werden Sie Ihr Repertoire an Windows Forms-Steuerelementen beträchtlich aufbessern: Sie lernen alle wichtigen Steuerelemente kennen, die an Tag 4 nicht behandelt wurden. Tag 7, »Mit Dialogfeldern arbeiten«, bringt Ihnen schließlich alles über eine besondere Art von Windows Forms bei. Es gibt eine ganze Reihe von Dialogfeldtypen, die in der Windows-Programmierung sehr nützlich sind. Am Ende der Woche werden Sie das Gelernte rekapitulieren und es dazu verwenden, eine funktionierende Anwendung zu erzeugen. Lassen Sie uns ohne weiteren Verzug anfangen!

24

Mit Windows Forms beginnen

1

Mit Windows Forms beginnen

Willkommen zu Tag 1, Ihrem ersten Schritt in die Welt von Windows Forms! Windows Forms als Bestandteil von Microsoft .NET ist eine neue Technologie, die es Ihnen erlaubt, Windows-Anwendungen auf schnelle und einfache Weise zu erstellen. Heute fangen Sie damit an, worum es bei Windows Forms und .NET geht, und Sie werfen einen Blick auf echten Code. Heute 쐽

entdecken Sie, was Windows-Programmierung für Entwickler bedeutet,



erfahren Sie, was .NET und Windows Forms sind,



finden Sie etwas über die Vorteile heraus, die Ihnen .NET und Windows Forms bieten,



erkunden Sie, wie die Common Language Runtime und die Microsoft Intermediate Language funktionieren,



lernen Sie, wie Sie das .NET Framework installieren,



untersuchen Sie eine einfache Windows Forms-Anwendung.

1.1

Einführung in die Windows-Programmierung

Das Wichtigste zuerst: Was ist ein Windows-Programm? Vielleicht sollten wir noch einen weiteren Schritt zurück tun und fragen: Was ist Microsoft Windows? Da Sie nun mal auf dem Weg sind, ein Entwickler zu werden, müssen Sie Windows mit anderen Augen betrachten. Wenn Sie bereits ein Programmierer sind, dann haben Sie bereits gelernt, ein Betriebssystem zu steuern. Aus der Sichtweise eines Normalbenutzers ist Microsoft Windows ein Betriebssystem, das den Computer zum Laufen bringt. Es gestattet ihm Dokumente zu erstellen und zu speichern, Berechnungen auszuführen, im Internet zu surfen, Dateien auszudrucken und so weiter. Wenn Sie dieses Buch lesen, dann behagt Ihnen diese Beschreibung höchstwahrscheinlich nicht, denn Sie wollen mehr wissen: wie alles unter der Motorhaube funktioniert und wie Sie Windows dazu bringen können, bestimmte Dinge für Sie zu erledigen. Unter seinem nett anzusehenden Äußeren ist Microsoft Windows – genau wie jedes beliebige andere Betriebssystem – eine Umgebung für Übersetzer und zuweilen auch selbst ein Übersetzer. Windows und seine Anwendungen nehmen vom Benutzer eine Eingabe entgegen und übersetzen sie in etwas, das ein Computerprozessor verstehen kann – nämlich Einsen und Nullen – und umgekehrt. Abbildung 1.1 gibt dieses mehrschichtige Paradigma wieder. Windows selbst weiß nicht, wie es jede Eingabe übersetzen soll, und das ist der Punkt, an dem Windows-Anwendungen in Aktion treten.

26

Einführung in die Windows-Programmierung

Benutzereingabe

Microsoft Windows

CPU

0101101 1001..

Abbildung 1.1: Windows fungiert als Übersetzer zwischen dem Benutzer und dem Hauptprozessor.

Ein Windows-Programm wird von einem Entwickler geschrieben und sagt Windows, was es tun soll, um auf eine Eingabe zu reagieren. Diese Eingabe könnte in einem Tastendruck bestehen, einem Mausklick oder auch einfach in einem Programmstart. Durch Verwendung einer Programmiersprache – einer Sammlung von Schlüsselwörtern, Anweisungen, Begriffen und so weiter, die für den Computer von besonderer Bedeutung sind – schreibt ein Programmierer Befehle für das Betriebssystem, so dass es weiß, wie es Eingaben in Maschinensprache zu übersetzen hat. Was ist also ein Windows-Programm? Es ist ein Befehlssatz, der Windows mitteilt, was es tun und wie es auf eine Benutzereingabe reagieren soll. Als Entwickler werden Sie solche Instruktionen schreiben – vertrauen Sie mir, es ist einfacher, als Sie glauben. Wir werden diesen Vorgang näher unter die Lupe nehmen, wenn Sie Ihre ersten Anwendungen heute und morgen auf die Beine stellen.

Programmiersprachen Wie bereits erwähnt, ist eine Programmiersprache das, womit ein Programmierer das Betriebssystem instruiert. Sie enthält bestimmte Schlüsselbegriffe und eine Syntax. Auch wenn diese Elemente für den Laien wenig Sinn ergeben mögen, entsprechen sie einer wohldefinierten Logik. Selbst ohne Erfahrung auf diesem Gebiet haben Sie wahrscheinlich schon einmal von einigen Programmiersprachen gehört: C, C++, C# (»sih scharp« ausgesprochen), Visual Basic, Pascal, Java und so weiter. Warum so viele? Jede Sprache geht das Thema ein wenig anders an und stellt dem Entwickler andere Wahlmöglichkeiten bereit; ihre Anwendungen ähneln gesprochenen Sprachen sehr. Beispielsweise kennt ein Eskimo (Inuit) zehn Ausdrucksweisen für den Begriff »Schnee«, wohingegen die englische Sprache nur wenige besitzt. In ähnlicher Weise verfügt C++ über verschiedene Möglichkeiten, »Funktionszeiger« auszudrücken, während Visual Basic nur wenige oder keine besitzt. Das bedeutet nun nicht, dass eine der beiden Sprachen mangelhaft ist, sondern dass die zwei sich aus unterschiedlichen Wurzeln entwickelt haben. Darüber hinaus verwenden die meisten Sprachen (mit den Ausnahmen, die Sie im Abschnitt »Was ist .NET?« kennen lernen werden) unterschiedliche Paradigmen, um Windows-Anwendungen zu erstellen. C- und C++-Programme verwenden häufig die

27

Mit Windows Forms beginnen

Microsoft Foundation Classes (MFC), wohingegen Visual Basic seinen eigenen Formulardesigner besitzt. Das führte dazu, dass das Erlernen einer neuen Sprache häufig auch bedeutete, eine neue Methode zur Erstellung von Windows-Anwendungen zu erlernen (was inzwischen in .NET nicht mehr der Fall ist; mehr darüber später). Programmiersprachen werden in der Regel nicht vom Betriebssystem verstanden. Vielmehr übersetzt ein Zwischenprogramm namens Compiler die Programmiersprache in Maschinensprache, also in etwas, was das Betriebssystem versteht. Man könnte theoretisch ebenfalls in Maschinensprache schreiben, doch die meisten Menschen verstehen Binärbefehle nicht. Beispielsweise dürfte die Zeichenfolge »010101111« nach Unsinn aussehen, ergibt aber für den Computer tatsächlich einen Sinn. Die Sprachen, die Sie in diesem Buch benutzen werden, sind C# und Visual Basic .NET (VB .NET) – die erste, weil sie speziell für den Einsatz in der .NET-Umgebung entworfen wurde, und die zweite, weil die meisten Anfänger diese leicht erlernbare Sprache verwenden. Es ist meistens einfach, die eine in die jeweils andere zu übersetzen. Beispielsweise zeigt das folgende Stück Code zwei Befehlszeilen: eine in C# und die andere in Visual Basic .NET; beide tun das Gleiche. C# System.Console.WriteLine("Hello World!"); VB .NET System.Console.WriteLine("Hello World!")

In diesem Fall besteht der einzige Unterschied im Semikolon am Schluss der ersten Befehlszeile. Im ganzen Buch werden Ihnen Codebeispiele in beiden Sprachen angeboten, so dass Sie das bevorzugte Beispiel wählen können. Ich halte es für eine gute Idee, wenn Sie entweder C# oder VB .NET vor dem Lesen dieses Buches kennen. Sie bekommen zwar einiges dieser Sprachen durch das Lesen mit, aber Ihnen würden viele Elemente entgehen, die Sie sonst beim separaten Erlernen dieser Programmiersprachen erhalten würden.

Win32-Programmierung Man hört häufig den Begriff »Win32«, mit dem Entwickler um sich werfen. Win32Anwendungen sind einfach Anwendungen, die für eine 32-Bit-Version von Windows (also Windows 95, 98, ME, 2000 und XP) erstellt wurden. Ältere Versionen von Windows verwendeten 16 Bit und werden daher als Win16 bezeichnet. Der Übergang von 16 zu 32 Bit stellt Ihnen viel mehr Verarbeitungsleistung zur Verfügung und gestattet Ihnen mehr Kontrolle über den Computer. Win16-Programme laufen auf einem Win32-Betriebssystem, das gilt aber nicht umgekehrt wegen beschränkter Verarbeitungsleistung und fehlender Abwärtskompatibilität.

28

Was ist .NET?

Sehr einfach ausgedrückt sind also 32 Bit besser als 16 Bit, weil Sie damit mehr in der gleichen Zeit verarbeiten können. Für einen Computerprozessor heißt 32 Bit, dass er auf 32 Bit lange Daten im Speicher zugreifen und sie verarbeiten kann, was wesentlich mehr »Platz« bedeutet als 16 Bit. In der heutigen EDV-Welt sind die meisten Anwendungen, die Sie sehen, Win32 – genau wie alle Anwendungen, die Sie in diesem Buch erstellen werden. Win16-Programme bezeichnet man als »Legacy«- oder »Vermächtnis«-Programme, weil sie noch aus der Frühzeit der modernen Computerei stammen (also bis ca. 1995). Viele Entwickler halten nichts von Legacy-Anwendungen, weil diese Programme meist recht alt sind und ihnen die modernen Verarbeitungstechniken fehlen.

1.2

Was ist .NET?

.NET ist Microsofts Initiative, eine neue Generation von Windows-Anwendungen aus der Taufe zu heben. .NET vereinfacht viele der hinderlichen Aspekte in der Windows-Programmierung zu einer einfachen Architektur und integriert dabei das Internet eng in alle Anwendungen. Während .NET ein Konzept ist, bringt das .NET Framework es zum Laufen. Sie werden das Framework bei der Arbeit mit Windows Forms ausgiebig nutzen. Einfach ausgedrückt ist das .NET Framework eine Architektur, auf deren Grundlage sowohl Internet- als auch Desktop-basierte Anwendungen erstellt und ausgeführt werden können. Für die Arbeitsweise von Windows stellt es eine völlig neue Welt dar, indem es ein standardisiertes System bereitstellt, das alle Anwendungen nutzen können, anstelle der vielen unterschiedlichen Frameworks, die heute verfügbar sind. .NET stellt für diese Anwendungen auch eine neue Umgebung bereit, in der sie ablaufen können. Das ist effizienter, sicherer und leistungsfähiger. Kurz und gut: Mit Hilfe von .NET können Sie Anwendungen leichter als je zuvor erstellen. Eines der Hauptziele von .NET besteht darin, Anwendungen oder Anwendungskomponenten über das Internet als Dienste (Services) bereitzustellen. Ein Dienst ist eine Anwendung, die über das Web zur Verfügung steht und sich direkt oder von anderen Anwendungen nutzen lässt. Diese Dienste wären für jeden verfügbar, der sie braucht; man wählt sich einfach ins Web ein und stellt die Verbindung zum Dienst her. Stellen Sie sich vor, Sie hätten nicht solche Software wie Microsoft Word auf Ihrem Computer zu installieren. Statt dessen gehen Sie online und nutzen es von der Microsoft-Website aus, genauso, als befände es sich auf Ihrem Computer. Abbildung 1.2 illustriert dieses Konzept.

29

Mit Windows Forms beginnen

Ihr Computer

Befehle senden

Internet

gelieferte Daten senden

Dienst

Abbildung 1.2: Ein Webdienst ist eine Anwendung oder Komponente, die über das Internet zum Gebrauch bereitsteht.

Dieses Paradigma erspart sowohl Endanwendern als auch Entwicklern Kopfschmerzen. Die Endanwender müssen sich keine Sorgen machen, ob ihre Installation klappen wird oder ob sie ihre Anwendung pflegen, also aktualisieren müssen. Der Entwickler muss sich nicht sorgen wegen Installationsproblemen oder der Pflege mehrerer Programmversionen auf möglicherweise Tausenden von Anwendercomputern. Er erstellt das Programm einfach und stellt es über das Internet zur Verfügung, so dass Anwender vorbeischauen können und es einsetzen, wann sie es benötigen. Wenn der Entwickler eine neue Version erstellt, stellen die Benutzer eine Verbindung nur deshalb her, um die neuen Funktionen zu nutzen. In den nächsten Abschnitten erfahren Sie mehr über die Komponenten von .NET und wie sie zusammenwirken, um Ihre Arbeit als Entwickler zu vereinfachen. Es mag zwar zunächst etwas trocken zu lesen sein, doch Sie werden diese Grundlage brauchen, bevor Sie anfangen, eigene Anwendungen zu erstellen.

.NET-Kompilierung Eine nicht auf .NET basierende Anwendung funktioniert folgendermaßen: Sie wird von einem Entwickler in einer Programmiersprache geschrieben und dann zu einer ausführbaren Anwendung kompiliert (das ist ein sehr wichtiges Konzept). Das Betriebssystem stellt die Umgebung für die Ausführung der Anwendung bereit und bietet ihr je nach Bedarf Zugriff auf seine Ressourcen. In einem Multitasking-Betriebssystem wie Windows lassen sich mehrere Anwendungen gleichzeitig ausführen, denn jede Anwendung hat ihre eigenen Betriebssystemressourcen (Speicher usw.). Normalerweise respektiert jede Anwendung die Ressourcen der anderen, so dass keine Konflikte auftreten. Jede Anwendung hat sich auch selbst zu pflegen; das heißt sie wird ausgeführt und räumt sozusagen hinter sich wieder auf, wobei sie Ressourcen wieder freigibt, sobald sie nicht mehr gebraucht werden. Das ist im Allgemeinen eine gute Arbeitsweise. Anwendungen sind üblicherweise mit sich selbst beschäftigt und kümmern sich nicht darum, was andere Anwendungen so treiben. So könnte Anwendung A beispielsweise abstürzen, während Anwendung B weiterläuft, weil die beiden sich keine Ressourcen teilen. Das Betriebssystem braucht nur minimale Ressourcen zu investieren, um die Anwendung aufrechtzuerhalten (von der Prozessorleistung einmal abgesehen). Dieses Modell hat jedoch auch ein paar Nachteile. Das Betriebssystem muss die meisten Anwendungen bei sich registrieren, damit es Einstellungen pflegen, Ressourcengrenzen

30

Was ist .NET?

bestimmen und es den Programmen erlauben kann, mit sich und anderen Anwendungen zu interagieren, wenn es nötig ist. Das kann zu einem recht komplizierten Registrierungsvorgang führen, was wiederum umfangreiche Installationsprozeduren erfordert. Da jede Anwendung ihre eigenen Ressourcen zu verwalten hat, ist es zudem dem Entwickler überlassen, diese Ressourcen sauber zu handhaben. Man muss mitunter lästigen Code schreiben, nur um dafür zu sorgen, dass Ressourcen sauber geholt und wieder freigegeben werden, so dass es zu keinen Konflikten kommt. Die Common Language Runtime (CLR: etwa »Laufzeitversion in einer gemeinsamen Sprache«) ist die .NET-Form eines Managers oder Verwalters. Die CLR verwaltet alle Aspekte derjenigen Anwendungen, die in .NET laufen. Aufgrund dessen werden die Dinge in .NET ein wenig anders gehandhabt als bislang. Wenn Sie eine .NET-Anwendung erstellen, wird sie nicht zu einem direkt ausführbaren Programm kompiliert. Vielmehr wird Ihr Quellcode in etwas übersetzt, was als Microsoft Intermediate Language (MSIL) bezeichnet wird und im Grunde nur eine andere Methode ist, Ihren Code darzustellen. Wenn Sie später Ihr Programm ausführen, wird es von einem Just-In-Time (JIT) -Compiler in Maschinencode übersetzt, welcher wiederum von der CLR ausgeführt wird. Dieser zweistufige Vorgang ist zwar langsamer als der einstufige vor der Einführung von .NET, aber nicht so viel, wie Sie jetzt annehmen würden. Der JIT kann die MSIL sehr leicht verstehen. Daher lässt sie sich effizienter kompilieren als eine traditionelle Sprache. Und zum anderen bietet dieser Vorgang einen großen Vorteil gegenüber der ersten Methode: nämlich plattformübergreifende Kompatibilität. Maschinensprachen hängen von der jeweils verwendeten Maschine ab. Computer, die mit Windows und Intel-x86-Prozessoren arbeiten, sprechen andere Sprachen als solche mit Unix. Wenn man eine Anwendung auf eine andere Plattform portieren wollte, bedeutete es daher, mindestens den Quellcode neu zu kompilieren, damit er auf der neuen Plattform lief. Meistens erforderte es aber das Neu- oder Umschreiben des Codes. MSIL ist jedoch stets MSIL, ganz gleich, auf welcher Plattform sie sich befindet. Etwas, das in MSIL auf Windows funktioniert, wird auch in MSIL auf Unix funktionieren. Es ist lediglich jeweils ein entsprechender JIT-Compiler erforderlich, der den abschließenden Schritt des Kompilierens in Maschinensprache ausführt; aber darum haben Sie sich in der Regel nicht zu sorgen. Große Unternehmen wie Microsoft bieten höchstwahrscheinlich so einen Compiler für jede Plattform, bevor Sie eine Anwendung portieren müssen. Ein weiterer Vorteil des .NET-Paradigmas gegenüber dem herkömmlichen besteht darin, dass MSIL spezielle Attribute enthält, die als Metadaten bezeichnet werden und die es einem Programmcode erlauben, sich selbst gegenüber dem Betriebssystem zu beschreiben. Man erinnere sich daran, dass traditionelle Anwendungen sich oft selbst in einer zentralen Registrierdatenbank anmelden mussten, damit das Betriebssystem sie überwachen konnte. Das ist nun nicht mehr nötig, denn alle wichtigen Informationen liegen der Anwendung bei. Zu

31

Mit Windows Forms beginnen

solchen Informationen könnte gehören, nach welchen Abhängigkeiten man suchen soll, wo Sicherheitsanforderungen zu finden sind, Versionsnummern, Autoren usw. Die Bündelung solcher Informationen ist sehr hilfreich, wie Sie sehen werden, wenn Sie mit dem Codeschreiben anfangen. Nicht jede Programmiersprache oder jeder Compiler kann MSIL und Metadaten erzeugen. Daher hat Microsoft eine neue Klasse von Compilern geschaffen, die das .NET Framework unterstützt. Dadurch können alle Ihre bevorzugten Programmiersprachen .NET unterstützen. Mit der künftigen Verbreitung von .NET dürfen Sie weitere Compiler von anderen Unternehmen erwarten. Lassen Sie uns nun mal darauf achten, was nach dem Kompilieren passiert: Das Programm wird ausgeführt.

Die Common Language Runtime (CLR) Die CLR bildet das Zentrum des .NET Frameworks. Stellen Sie sie sich wie den Hauptprozessor für Ihre Anwendungen vor; sie behandelt die gesamte Ausführung und Verwaltung Ihrer Anwendungen. Sie hat zwar eine Menge Funktionen, doch Sie brauchen vorerst nur einige davon zu kennen. Zunächst einmal steuert die CLR den Ressourcenbedarf Ihrer Anwendung. Sie teilt deren Komponenten Speicher und Prozessorzeit zu und nimmt sie nach der Nutzung wieder zurück. Dieser Vorgang wird als Garbage Collection (Müllbeseitigung) bezeichnet, also als Speicherbereinigung. Entwickler mit einer gewissen Erfahrung, besonders in C und C++, werden sich an all die lästigen Routinen erinnern, die man schreiben musste, um Speicherressourcen zu verwalten. Diese Prozedur ist zum Glück nicht mehr nötig. Als zweiten Aspekt stellt die CLR eine vereinheitlichte Architektur bereit. Ganz gleich, welche Programmiersprache Sie nutzen, können Sie sich immer auf das gleiche Framework beziehen. C++-Anwendungen können leicht Komponenten nutzen, die in VB .NET erstellt wurden, und umgekehrt. Drittens verwendet die CLR Metadaten, die zusammen mit der jeweiligen MSIL erzeugt wurden, um eine saubere Ausführung zu gewährleisten. Ein Aspekt der Metadaten ist beispielsweise, dass sie Informationen über die Umgebung enthalten, also über Komponenten, Versionen usw., die verwendet wurden, um Ihre Anwendung zu erstellen. Die CLR kann diese Daten untersuchen, und wenn das aktuelle System nicht die gleichen notwendigen Ressourcen besitzt, kann die CLR diese Komponenten automatisch aktualisieren. Code, der für die CLR geschrieben wird, ist als verwalteter Code (Managed Code) bekannt: CLR verwaltet jeden Aspekt seiner Ausführung. Code, der nicht für die CLR bestimmt ist (also Prä-.NET-Anwendungen), wird als nichtverwalteter Code (Non-Managed Code)

32

Was ist .NET?

bezeichnet. Von jetzt ab verwenden wir diese Begriffe. Nichtverwalteter Code lässt sich weiterhin in der CLR ausführen, aber dafür müssen spezielle Vorkehrungen getroffen werden (darauf kommen wir am Tag 14 zu sprechen). Abbildung 1.3 zeigt die Pfade, denen verwalteter und nichtverwalteter Code in der CLR folgen. Verwalteter Code mit MSIL Die CLR Ressourcen zuweisen JIT Kompilierung

Metadaten untersuchen Sicherheitsprüfungen Ausführung

Nichtverwalteter Code Import in .NET

Garbage Collection (Müllbeseitigung)

Abbildung 1.3: Die CLR verwaltet die komplette Ausführung von verwaltetem Code und sorgt so für Sicherheit, Kompatibilität und richtige Funktionsweise.

Das .NET Framework installieren Bevor Sie die CLR einsetzen und Windows Forms schreiben können, müssen Sie zunächst das .NET Framework Software Development Kit (SDK) installieren. Das SDK gibt es kostenlos auf www.microsoft.com/net, aber beachten Sie, dass es über 100 Mbyte groß ist und das Herunterladen mit einem 56K-Modem über sechs Stunden dauern kann. Sie können es auch auf CD-ROM bei Microsoft bestellen (VisualStudio.NET liegt es bei). Das .NET Framework SDK erfordert Windows NT 4.0 mit Service Pack (SP) 6, Windows 2000 oder Windows XP, um richtig zu laufen. Wenn Sie ein anderes Betriebssystem besitzen, haben Sie Pech: Es gelingt Ihnen vielleicht, ein paar Beispiele zum Laufen zu bringen, aber Microsoft bietet Ihnen keinen Support, wenn das nicht gehen sollte. Sobald Sie ein Exemplar des SDKs haben, führen Sie das Setup-Programm aus (Abbildung 1.4). Klicken Sie auf WEITER, um die Installation zu starten, und akzeptieren Sie die Lizenzvereinbarung. Klicken Sie noch dreimal auf WEITER und lehnen Sie sich zurück, während sich Ihnen die Tore zur .NET-Welt öffnen (in anderen Worten: während das SDK installiert wird). Nach dem Abschluss der Installation erscheint ein Verknüpfungssymbol zu einer SDKÜbersicht auf Ihrem Desktop. Diese Webseite hat Verknüpfungen, um die .NET-Musterbeispiele einzurichten (falls Sie sie in einem vorhergehenden Schritt installiert haben) wie auch Verknüpfungen zu weiterer Dokumentation.

33

Mit Windows Forms beginnen

Abbildung 1.4: Der Installationsvorgang für das .NET Framework SDK beginnt mit dem Setup-Bildschirm.

Entwicklungsumgebungen In einer Entwicklungsumgebung verrichten Sie alle Ihre Arbeiten, wie etwa Anwendungserstellung und Eingeben von Code. Für die Entwicklung mit Windows Forms brauchen Sie jedoch lediglich ein Textprogramm, um Code zu schreiben. Zusätzlich zum Textprogramm bieten viele Entwicklungsumgebungen ausgetüftelte Benutzeroberflächen, die fortgeschrittene Fähigkeiten für die Verwaltung von Code und Projekten besitzen. Diese Funktionen sind häufig nützlich, aber nicht Bedingung. Der gesamte Quellcode in diesem Buch (und auf der entsprechenden Webseite www.samspublishing.com) liegt in einfachem Text vor. Das bedeutet, dass Sie einen Texteditor wie den Windows-Editor (NotePad) als Ihre Entwicklungsumgebung nutzen können. (Die von Ihnen erzeugten Dateien werden zwar die Erweiterungen .cs oder .vb tragen, aber dennoch aus einfachem Text bestehen.) Auch Visual Studio .NET ist ein ausgezeichneter Editor für die Anwendungen, die Sie erstellen. Es zeichnet Ihren Code in Farbe aus und stellt kontextabhängige Funktionen bereit, so etwa kleine Popup-Felder, die für Sie Sätze vervollständigen. Darüber hinaus stellt Visual Studio .NET zahlreiche Assistenten zur Verfügung, die Ihnen beim Erstellen von Windows Forms-Anwendungen beistehen. Obwohl es zahlreiche für Entwickler wertvolle Funktionen in Visual Studio .NET gibt, wird sich dieses Buch nicht darauf konzentrieren. Vielmehr beschäftigt es sich mit der Windows Forms-Technologie. Schließlich besteht die beste Methode, das Programmieren von Windows Forms zu erlernen, darin, sie selbst zu erstellen, und nicht, sie von einer grafischen Entwicklungsumgebung für Sie erstellen zu lassen. Sobald Sie darin versiert sind, selbst Quellcode zu schreiben, werden Sie die Leistungsmerkmale von Visual Studio .NET umso mehr zu schätzen wissen. Für dieses Buch brauchen Sie lediglich Notepad.

34

Was ist .NET Windows Forms?

In diesem Buch legen wir für jede Tageslektion einen neuen Ordner an, damit sich alle Dinge leicht wiederfinden lassen. Zum Beispiel werden die Dateien von Tag 2 im Ordner c:\winforms\day2 abgelegt. Wenn Sie also für die Beispiele in diesem Buch (oder aus dem Download von der Sams-Website) selbst Code schreiben, erstellen Sie diese Ordner und legen die Anwendungsdateien darin ab. Wenn Sie sich an dieses Bezeichnungsschema halten, können Sie dem weiteren Vorgehen leicht folgen.

1.3

Was ist .NET Windows Forms?

Sie fragen sich vielleicht, ob die Überschrift nicht besser »Was sind Windows Forms?« lauten sollte. Nein, denn Windows Forms ist keine Sammlung von Formularen bzw. Masken, die man in Windows benutzt, sondern eine einzige Technologie, die Ihnen beim Entwerfen von Windows-Anwendungen hilft. Sie ist Microsofts neue Plattform für die Entwicklung von Windows-Anwendungen, die auf dem .NET Framework basieren. Ein Blick auf die beiden Wörter »Windows Forms«: Sie kennen ja Windows bereits als Betriebssystem, das Benutzereingaben entgegennimmt und entsprechende Arbeiten ausführt. Ein Formular (form) ist genauso wie seine Papierversion etwas, das man mit Informationen ausfüllt, um damit etwas Bestimmtes zu erreichen. Ein Papierformular lässt sich beispielsweise dazu verwenden, einen Pass zu beantragen, eine Prüfung zu absolvieren oder sich zu bewerben. Ein digitales Formular wird üblicherweise eingesetzt, um Informationen einzugeben oder um mit einer Anwendung zu interagieren. Wenn Sie schon einmal das Internet benutzt haben, haben Sie bereits etliche digitale Formulare zu Gesicht bekommen. Viele Webseiten fragen auf diese Weise Informationen vom Benutzer ab, um ihn beispielsweise für eine Dienstleistung zu registrieren. Wahrscheinlich haben Sie bereits Dutzende, wenn nicht Hunderte solcher Formulare ausgefüllt. Abbildung 1.5 zeigt ein solches Formular. Der Vorteil eines digitalen Formulars gegenüber einem Papierformular besteht darin, dass das digitale mit Ihnen interagieren kann. Sobald Sie Informationen eintragen, kann das Formular die Eingabe verarbeiten oder Ihnen weitere Auswahlmöglichkeiten anbieten. Genau auf dieser Idee fußt die Windows Forms-Technologie. Sie verwendet digitale Formulare (die ebenfalls Windows Forms genannt werden), um Daten zu sammeln. Ein Windows Form ist ein Bildschirmfenster, mit dem der Benutzer interagiert. Es lässt sich für das Sammeln oder Anzeigen von Daten verwenden, für die Ausgabe von Alarmmeldungen usw. (Bitte beachten Sie, dass wir im Folgenden den Begriff sowohl für die Technologie als auch für das eigentliche Windows Forms-Fenster (= Formular) verwenden. Das ist zulässig, denn die Technologie besteht hauptsächlich aus diesen Fenstern.)

35

Mit Windows Forms beginnen

Abbildung 1.5: Ein typisches Webformular enthält Textfelder und eine Schaltfläche.

Diese Beschreibung könnte den Eindruck erwecken, dass ein Windows Form nicht sonderlich funktionsreich sei, doch in Wirklichkeit stellt es eine Menge Leistung und Flexibilität bereit. Weil die Windows Forms-Technologie auf dem .NET Framework beruht, steht ihr eine umfangreiche Bibliothek von nützlichen Objekten und Methoden zur Verfügung, die es einem Windows Forms-Fenster gestatten, so ziemlich alles auszuführen, was Sie wünschen. Daher mag ein Windows Form einfach nur eine Schnittstelle sein, um so mit dem Benutzer zu interagieren, doch die Technologie hinter dem Formular liefert die entsprechende Leistungsfähigkeit.

Integration mit .NET .NET und die CLR bieten Ihnen eine Fülle von Funktionen, um die Anwendungsentwicklung viel einfacher zu machen. Wie bereits angesprochen, stellt die Garbage Collection sicher, dass alle unbenutzten oder übrig gebliebenen Ressourcen, die Ihre Anwendung nicht mehr braucht, auf angemessene Weise behandelt werden und so für den Gebrauch durch andere Anwendungen freigemacht werden. Die Einbindung in .NET erlaubt es Ihren Windows Forms, die umfangreiche Klassenbibliothek des .NET Framework zu nutzen. Diese enthält fast alle Objekte, die Sie jemals für Ihre Anwendungen brauchen werden, in vorgefertigter und einsatzbereiter Form. Sie brauchen eine Komponente, die auf Datenbanken zugreift? .NET stellt Ihnen mehrere zur

36

Was ist .NET Windows Forms?

Auswahl. Wie wär's mit XML-Dateien? Die hat .NET ebenfalls. Das Erlernen der Windows Forms-Programmierung besteht also zum Teil aus dem Aufbau eines möglichst großen Repertoires solcher Objekte. Das ist der Faktor, der die Experten von den Neulingen unterscheidet. Und das Beste dabei: Dieses Framework steht Ihnen zur Verfügung, gleichgültig, welche Programmiersprache Sie wählen. Apropos Datenbankzugriff: Mit Windows Forms verfügen Sie über die Leistungsfähigkeit von ADO.NET, der nächsten Generation von ActiveX Data Objects (= ADO). Diese Weiterentwicklung führt viele Leistungsmerkmale ein, die die Reichweite und Flexibilität Ihrer Anwendung erhöhen. Sie können auf vorhandene Datenquellen zugreifen oder eigene erzeugen, können unverbundene Datensätze nutzen, XML-Dateien lesen und schreiben und anderes. Mehr über diese Leistungsmerkmale erfahren Sie an Tag 9. In das .NET Framework sind auch leistungsfähige Sicherheitsmerkmale integriert. Diese gestatten es dem Betriebssystem bereits dann zu erkennen, wie sicher Ihre Anwendung ist, wenn diese noch gar nicht ausgeführt worden ist. Es kann auch unbefugte Benutzer davon abhalten, auf Ihre Anwendungen zuzugreifen, ebenso wie es autorisierte Benutzer daran hindert, unautorisierte Anwendungen wie etwa Computerviren auszuführen. Auch das Konfigurieren und Installieren Ihrer Anwendungen ist ein Kinderspiel. Die Konfiguration erfordert häufig lediglich das Erstellen einer einzigen Textdatei mit der Endung .config. Implementierung bedeutet lediglich das Überspielen Ihrer Dateien auf den Zielcomputer; die Notwendigkeit, Ihre Anwendungen zu registrieren und zu installieren, entfällt. All dies ist dank CLR und Metadaten möglich. Wie der Zusatz ».NET« andeutet, ist Windows Forms eng mit dem Internet und Web Services integriert. Web Services sind Komponenten von online laufenden Anwendungen, mit denen Sie eine Verbindung herstellen können, um so deren Funktionsumfang zu nutzen. Daher brauchen Sie diese Funktionen nicht mehr selbst zu erstellen. GDI+ (Graphical Device Interface: Schnittstelle zu Grafikgeräten) erlaubt Ihnen, die grafischen Funktionen von Windows zu nutzen, um Ihre Anwendungen für die visuelle Interaktion fit zu machen. So können Sie mit GDI+ beispielsweise Anwendungen einfärben, Grafiken laden und bearbeiten und sogar Fenster transparent machen. Mehr zu GDI+ finden Sie an Tag 13. Wir werden alle diese Funktionen in den nächsten 20 Tagen behandeln, so dass Sie nach dem Durcharbeiten dieses Buches ein Experte in der Windows Forms-Programmierung sind.

Windows Forms-Steuerelemente Eine Windows Form ist für sich allein nicht sonderlich interaktiv. Wie Sie später an diesem Tag sehen werden, ist die Zahl der Möglichkeiten dessen, was ein Benutzer mit einem Formular tun kann, recht begrenzt. Windows Forms-Steuerelemente – die es alle in der

37

Mit Windows Forms beginnen

Klassenbibliothek des .NET Framework gibt – können Ihrer Anwendung erheblich mehr Dynamik verleihen, indem sie Ihnen interaktive Objekte bereitstellen, wie zum Beispiel Schaltflächen, Textfelder, Menüs und Statusleisten. Alle Windows Forms-Steuerelemente (Controls) arbeiten in einer gemeinsamen Architektur – das macht das Erlernen neuer Steuerelemente so einfach. Jedes Steuerelement lässt sich durch seine Eigenschaften vollständig steuern. Sollte es noch keine Steuerelemente geben, die Ihnen zusagen, so können Sie leicht Ihre eigenen erstellen. Die meisten verbreiteten HTML-Steuerelemente haben in Windows Forms ein Gegenstück. Ab morgen werfen Sie einen Blick auf einige Steuerelemente, umfassender gehen wir ab Tag 4 auf dieses Thema ein.

1.4

Die objektorientierte Vorgehensweise

Das .NET Framework ist vollständig objektorientiert. Und was bedeutet das? Objektorientierte Programmierung (OOP) ist ein Programmierparadigma, das eine logische Möglichkeit zur Anwendungsentwicklung bietet; es verknüpft Programmierbegriffe mit der Realität. In echter OOP ist alles ein »Objekt«, genauso wie alles, mit dem Sie täglich interagieren, ein Objekt ist. Ihr Auto, Ihre Kaffeemaschine, Ihr Stuhl – alle sind Objekte. Jedes Objekt verfügt über Eigenschaften, die es definieren. Ihre Auto etwa hat eine Farbe, gefahrene Kilometer, die Eigenschaft »Hersteller« und so weiter. Mit all diesen Eigenschaften lässt sich ein Objekt auf eindeutige Weise beschreiben. Meistens haben Sie in der OOP mit Objekten zu tun. Ein Windows Form ist ein Objekt, ebenso wie eine ASP.NET-Seite. Objekte verfügen auch über Methoden bzw. Aktionen oder Verhaltensweisen. Ihr Auto hat Methoden anzuspringen, zu bremsen, anzuhalten usw. Ein Windows Form hat Methoden zu starten, zu schließen, in der Größe verändert zu werden, die Farbe zu ändern und viele weitere Verhaltensweisen. Abbildung 1.6 zeigt Beispiele von Objekten und einige ihrer Eigenschaften und Methoden. Darüber hinaus findet man oft Objekte, die auf dem gleichen grundlegenden Konzept basieren. Sowohl Pkws als auch Lastwagen sind Motorfahrzeuge, wenn auch im Detail verschieden. Manchmal gibt es Objekte, die auf einem anderen Objekt basieren; so etwa stammt der neue VW Käfer vom ursprünglichen Käfer ab. Dies sind Prinzipien (nämlich Schnittstellen bzw. Vererbung), die sich auch in der OOP wiederfinden. Sie bieten Ihnen die Flexibilität, um Ihre vorhandenen Objekte zu erweitern, wenn sie nicht genau Ihre Wünsche erfüllen.

38

So erstellen Sie Ihre erste Anwendung

“Auto”-Objekt Eigenschaften: Farbe, Hersteller Methoden: Start, Tür öffnen

“Scheinwerfer”-Objekt Eigenschaften: Helligkeit, Farbe Methoden: ein-/ausschalten

“Rad”-Objekt Eigenschaften: Größe, Luftdruck Methoden: Abrieb des Gummis

Abbildung 1.6: Ein Auto weist viele Eigenschaften und Methoden auf, wie auch Unterobjekte, die ihrerseits über Eigenschaften und Methoden verfügen.

Windows Forms basiert auf Prinzipien der OOP: Jeder Teil einer Windows Forms-Anwendung ist ein Objekt, vom sichtbaren Fenster bis hin zu den anklickbaren Schaltflächen, den ausfüllbaren Textfeldern und sogar bis zur Tageszeit, zu der Sie die Anwendung nutzen. Selbst wenn Sie noch nie OOP eingesetzt haben, dürfte Ihnen dies einleuchten. Solange Sie sich alles als ein selbstständiges Objekt vorstellen, werden Sie schnell lernen. Da alles in .NET ein Objekt ist, werden Sie, nachdem Sie den Umgang mit einem Objekt kennen, Bescheid wissen, wie Sie dieses Wissen auf alle übrigen Objekte übertragen. Das macht Ihr Leben als Entwickler leichter. Wir werden uns diesem Thema an Tag 3 widmen, nachdem Sie ein paar Beispiele für Objekte in Windows Forms gesehen haben.

1.5

So erstellen Sie Ihre erste Anwendung

Höchste Zeit, sich ein bisschen in Code zu vertiefen! Geben Sie den Code aus Listing 1.1 in Ihren Editor ein und speichern Sie ihn als Datei c:\winforms\day1\helloworld.vb. Denken Sie daran, im Dialogfeld SPEICHERN UNTER den Eintrag Alle Dateien im Feld DATEITYP auszuwählen und nicht TXT-Dateien. Sonst wird nämlich Ihre Datei als helloworld.vb.txt gespeichert! Listing 1.1: Ihre erste Visual Basic .NET Windows Forms-Anwendung! 1: 2: 3: 4: 5: 6: 7:

Imports System Imports System.Windows.Forms Public Class HelloWorld : Inherits Form Public Shared Sub Main() Application.Run(New HelloWorld)

39

Mit Windows Forms beginnen

8: 9: 10: 11: 12: 13: 14:

End Sub Public Sub New() Me.Text = "Hallo Welt!" End Sub End Class

Keine Sorge, wenn Sie diesen Code nicht verstehen sollten. Sie erfahren morgen, was dies bedeutet (wir untersuchen morgen auch C#-Code). Ein kurzer Überblick soll vorerst genügen: Die Zeilen 2 und 3 verwenden eine spezielle Syntax, um Ihrer Anwendung zu gestatten, die Namensräume oder Objektgruppen (aus der .NET-Klassenbibliothek) System und System.Windows.Forms zu nutzen. Das Schlüsselwort Imports referenziert diese Namensräume zwecks einfacherer Verwendung. In Zeile 4 deklarieren Sie Ihre Klasse bzw. Ihr Objekt. Die Zeilen 6-8 stellen den Startpunkt für Ihre Anwendung dar und die Zeilen 10-12 sind ein so genannter Konstruktor. In Zeile 14 schließen Sie Ihre Klasse ab. Ein Großteil dieses Codes konfiguriert Ihre Anwendung. Die einzige echte Ausführungsanweisung finden Sie in Zeile 11, die aussieht, als ob sie irgendwo in der Anwendung den Gruß »Hallo Welt!« anzeigen würde. Der nächste Schritt besteht darin, Ihre Anwendung zu MSIL zu kompilieren. Öffnen Sie ein Fenster mit einer Befehlseingabeaufforderung (START / (ALLE) PROGRAMME / ZUBEHÖR / EINGABEAUFFORDERUNG) und navigieren Sie zu dem Ordner, in welchem Sie diese Datei gespeichert haben. Geben Sie den folgenden Befehl in das Fenster ein und drücken Sie (¢): vbc /t:winexe /r:System.dll /r: System.Windows.Forms.dll helloworld.vb

Kümmern Sie sich hier noch nicht um die Syntax. Es genügt, dass Sie wissen, dass dieser Befehl eine Visual Basic .NET-Anwendung zu einer ausführbaren MSIL-Datei kompiliert. Sie sollten folgende Ausgabe sehen: C:\winforms\day1>vbc /t:winexe /r:system.dll [ic:ccc] /r: system.windows.forms.dll helloworld.vb Microsoft (R) VisualBasic.NET Compiler version 7.00.9254 for Microsoft (R) .NET CLR version 1.00.2914.16 Copyright (C) Microsoft Corp 2001. All rights reserved. C:\winforms\day1>

Falls keine Fehlermeldungen angezeigt werden, steht Ihre Anwendung zur Ausführung bereit. (Falls Sie helloworld.exe nicht finden, suchen Sie die Datei mit Ihrer Suchfunktion.) Geben Sie an der Eingabeaufforderung helloworld ein; es sollte dann ein Fenster wie in Abbildung 1.7 zu sehen sein.

40

Zusammenfassung

Abbildung 1.7: Hier sehen Sie Ihre erste Windows Forms-Anwendung namens »Hallo Welt!«

Diese Anwendung ist zwar noch nicht reich an Funktionen, beachten Sie aber bitte die Titelleiste »Hallo Welt!«. Herzlichen Glückwunsch zur Fertigstellung Ihrer ersten, wenn auch bescheidenen Windows Forms-Anwendung!

1.6

Zusammenfassung

Um Sie mit dem .NET Framework und Windows Forms bekannt zu machen, war dies heute nur eine kurze Lektion. Man muss zuerst über diese grundlegenden Themen sprechen, so dass der Code später einen Sinn ergibt, wenn man ihn sich genauer ansieht. Heute haben Sie gelernt, wie das Betriebssystem mit Ihren Anwendungen und Benutzereingaben zusammenarbeitet. Das .NET Framework ist eine neue Architektur, um damit Windows-Anwendungen zu erstellen. Damit können Sie unter Verwendung jeder beliebigen Programmiersprache eine Anwendung erstellen, wohlgemerkt: stets auf der gleichen standardisierten Architektur. Mit der Common Language Runtime (CLR) lassen sich Ihre Anwendungen zu Microsoft Intermediate Language (MSIL) kompilieren statt direkt zu einer ausführbaren Datei. MSIL enthält Metadaten, die wiederum der CLR Informationen über Ihre Anwendung, darunter Versionsnummer und Sicherheitsangaben, zur Verfügung stellen. Die Metadaten befreien Sie von der Notwendigkeit, Ihre Anwendung bei einem Betriebssystem anzumelden. Windows Forms ist als Teil von .NET ein Framework (Gerüst) für die Erstellung von Windows-Anwendungen, die die CLR nutzen. Sein Vorteil gegenüber herkömmlicher Windows-Programmierung liegt in der vereinheitlichten, objektorientierten Architektur, die die Klassenbibliothek des .NET Frameworks nutzen kann. Von Ihrer ersten Windows Forms-Anwendung haben Sie gelernt, dass der Quellcode eine einfache Textdatei ist und sich im Editor erstellen lässt. Er muss zu MSIL kompiliert werden, bevor man ihn verwenden kann; Sie haben das getan, indem Sie einen Compiler an der Eingabeaufforderung benutzten.

41

Mit Windows Forms beginnen

1.7 F

Kann ich wirklich jede gewünschte Programmiersprache benutzen, um Windows Forms zu erstellen? A

F

Fragen und Antworten

In der Theorie schon, aber in der Praxis nicht ganz. Sie brauchen einen Compiler, der Ihren Code in MSIL umwandelt und Metadaten ausgibt. Die einzigen Sprachen, die dies zur Zeit beherrschen, sind C++, C# und Visual Basic .NET, obwohl die Unterstützung für viele weitere Sprachen gerade entwickelt wird.

Was versteht man unter ASP.NET? A

1.8

ASP.NET ist eine weitere Facette von .NET. Es erlaubt Ihnen, interaktive Webseiten und Web Services zu erstellen. ASP.NET ist Windows Forms sehr ähnlich – beide haben die gleichen Begriffe und Frameworks gemeinsam –, doch ASP.NET wird verwendet, um Internetanwendungen zu schreiben, während Windows Forms für Windows-Anwendungen eingesetzt wird.

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wahr oder falsch? Windows Forms ist Win32-Programmierung. 2. Wahr oder falsch? Metadaten enthalten Angaben darüber, in welcher Umgebung eine Anwendung erstellt wurde. 3. Auf welche Weise ermöglicht MSIL plattformübergreifende Funktionsweise? 4. Wie nennt man die Weiterentwicklung von ActiveX Data Objects? 5. Wahr oder falsch? Man kann Windows Forms-Anwendungen mit jedem beliebigen Texteditor erstellen. 6. Was ist ein Windows Forms-Steuerelement? 7. Was bewirkt das Imports-Statement?

42

Workshop

Übung Was würde der folgende Code bewirken, sobald er kompiliert und ausgeführt würde? 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

Imports System Imports System.Windows.Forms Public Class MyApp : Inherits Form Public Shared Sub Main() Application.Run(New MyApp) End Sub Public Sub New() Me.Height = 100 Me.Width = 50 End Sub End Class

43

Windows FormsAnwendungen erstellen

2

Windows Forms-Anwendungen erstellen

Nachdem Sie die Grundlagen verstanden haben, ist es nun an der Zeit, sich kopfüber in die Erstellung von Windows Forms-Anwendungen zu stürzen. In der heutigen Lektion werfen Sie einen weiteren Blick auf die gestrige Anwendung und konzentrieren sich dabei auf die verschiedenen Elemente einer Windows Forms-Anwendung. Sie betrachten die Erstellung von Windows Forms mit C#, derjenigen Sprache, die viele Entwickler für Windows Forms bevorzugen. Sie untersuchen die Unterschiede im Code, den VB .NET und C# produzieren. Am Ende der heutigen Lektion werden Sie die eine Sprache leicht in die andere übersetzen können. Heute lernen Sie, 쐽

was Klassen, Assemblies und Namensräume sind,



wie Vererbung funktioniert,



auf welche Weise man die Main-Methode einsetzt,



was Konstruktoren und Destruktoren sind,



wie Sie Quellcode kompilieren,



in welcher Weise Sie alles zusammensetzen, um eine Anwendung zu erhalten.

2.1

Ein weiterer Blick auf Ihre Anwendung

Bitte erinnern Sie sich an die gestrige Zusammenfassung. Alles Weitere baut darauf auf. Nun lassen Sie uns den gestrigen Code betrachten, den Sie in Listing 2.1 finden. Listing 2.2 zeigt den gleichen Code in C#. Weil alle Windows Forms-Anwendungen das gleiche .NET Framework verwenden, sind die Quellcodes in Visual Basic .NET und C# fast identisch, abgesehen von ein paar syntaktischen Unterschieden. Alle Konzepte in dem einen Quellcode beziehen sich auch auf den anderen. Listing 2.1: Ihre erste Visual Basic .NET-Windows Forms-Anwendung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

46

Imports System Imports System.Windows.Forms Public Class HelloWorld : Inherits Form Public Shared Sub Main() Application.Run(New HelloWorld) End Sub Public Sub New() Me.Text = "Hello World!"

Ein weiterer Blick auf Ihre Anwendung

12: 13: 14:

End Sub End Class

Listing 2.2: Ihre erste C#-Windows Forms-Anwendung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

using System; using System.Windows.Forms; public class HelloWorld : Form { public static void Main() { Application.Run(new HelloWorld()); } public HelloWorld() { this.Text = "Hello World!"; } }

Beachten Sie, dass C# auf Groß-/Kleinschreibung achtet, so dass der Code genau so wie in Listing 2.2 geschrieben sein muss, sonst erhalten Sie Fehlermeldungen. Wir gehen hier nicht näher auf den Code ein, aber Ihnen werden einige Ähnlichkeiten auffallen. Es finden sich Namensräume, Klassen, Methoden und eine Main-Methode in beiden Listings. In den folgenden Abschnitten gehen wir auf diese einzelnen Begriffe ein.

Klassen Wenn Sie noch keine OOP-Sprache verwendet haben, dürfte Ihnen der obenstehende Code merkwürdig erscheinen. Lassen Sie uns zunächst Zeile 4 in beiden Listings betrachten, denn sie enthält die wichtigsten Konzepte. Diese Zeile deklariert unsere Klasse namens HelloWorld. Eine Klasse ist im Wesentlichen die Definition eines Objekts. In der Klasse bestimmen Sie die individuellen Eigenschaften sowie die Funktionen, die andere Objekte vielleicht einmal nutzen. Stellen Sie sich eine Klasse wie eine Blaupause oder einen Bauplan für ein Haus vor. Die Blaupause selbst ist nicht gerade reich an Funktionen; man kann kaum etwas damit tun. Sie legt jedoch alles fest, was das Haus benötigt: wohin die Türen und die Lampen kommen, die Zimmergröße, die Leitungen und das ganze übrige Innenleben eines Hauses. Ein Bauleiter benutzt die Blaupause bzw. einen Bauplan, um das tatsächliche Haus zu

47

Windows Forms-Anwendungen erstellen

erbauen, das ein gebrauchsfertiges Objekt darstellt: Sie können die Türen öffnen und schließen, die Wandfarbe ansehen (und ändern) usw. Ähnlich wie eine Blaupause legt eine Klasse das ganze Innenleben eines Objekts fest. In unserem Fall entspräche der Bauleiter der CLR, denn sie untersucht die Blaupausen und wandelt sie zu etwas Brauchbarem um: zu einem Objekt oder einer Anwendung. Somit können die Benutzer Ihres Objekts Methoden ausführen, die Sie festgelegt haben, und verfügbare Eigenschaften untersuchen. Kurz und gut: Sie erstellen eine Klasse, die von der CLR in etwas Nützliches umgewandelt wird. Der jeweilige Benutzer des Objekts braucht sich nicht darum zu kümmern, wie das Innere definiert ist, sondern benutzt es einfach – genau wie ein Hausbewohner im Allgemeinen nicht wissen muss, wie die Klimaanlage funktioniert, solange sie nur zuverlässig ihren Dienst versieht. Diese Abstraktion von Komplexität wird als Kapselung bezeichnet. Mit Hilfe von Klassen kapseln Sie die Einzelheiten und Implementierungsdetails, so dass sich der Benutzer nicht darum zu kümmern braucht. Abbildung 2.1 illustriert den Begriff der Kapselung. Public Class Hello … … end class

1. Was der Benutzer nicht zu sehen bekommt

2. Die CLR erzeugt das Objekt

Object

3. Was der Benutzer sieht und verwendet

Abbildung 2.1: Die CLR kapselt Ihre Klassenbaupläne in einem Objekt.

Wenn Ihnen diese Vorstellung immer noch nebulös vorkommt, sorgen Sie sich nicht. Wenn wir diese und andere Klassen untersuchen, werden Sie rasch ein grundlegendes Verständnis entwickeln. Die Wörter »Klasse« und »Objekt« werden oft synonym verwendet. Solange Sie den Unterschied kennen – eine Klasse legt die innere Funktionsweise eines Objekts fest –, ist das in Ordnung. Zurück zu Zeile 4: Wir müssen noch zwei Teile untersuchen. Das Schlüsselwort Public bedeutet, dass diese Klasse für andere Klassen zugänglich ist. Wenn Sie umgekehrt das Schlüsselwort Private benutzen, kann keine andere Klasse diese Klasse sehen oder benutzen. Wenn sie nicht öffentlich wäre, könnte sie Ihre Anwendung nicht nutzen. Es lassen sich hier ein paar weitere Schlüsselwörter verwenden, so etwa Protected oder Friend, aber wir werden diese zu gegebener Zeit betrachten.

48

Ein weiterer Blick auf Ihre Anwendung

Der letzte Bestandteil von Zeile 4, nämlich Inherits Form, ist etwas Besonderes für objektorientierte Programme (die C#-Version verwendet das Schlüsselwort Inherits nicht, sondern einfach einen Doppelpunkt). Alles in .NET ist ein Objekt (oder eine Klasse). Daher gibt es auch eine Klasse namens Form. Sie ist die Basisklasse für alle Windows FormsAnwendungen: Sie enthält Funktionen, die es gestatten, dass ein Formular angezeigt, geschlossen, bewegt und skaliert werden kann. Daher muss jede von Ihnen erstellte Windows Forms-Anwendung auf diese Form-Klasse zugreifen. Das Schlüsselwort Inherits teilt der CLR mit, dass alle Eigenschaften und Methoden in dieser Form-Klasse automatisch Ihrer Klasse HelloWorld hinzugefügt werden sollen. Das bedeutet, die Klasse HelloWorld erbt sie von der Klasse Form. Ebenso wie Sie Ihre blauen oder braunen Augen von Ihren Eltern geerbt haben, erbt die HelloWorld-Klasse von ihrer Elternklasse Form die Fähigkeit, ein Fenster am Bildschirm anzuzeigen. Die Form-Klasse erbt ihrerseits von einer anderen Klasse namens ContainerControl, welche von der Klasse Control erbt, welche ihrerseits von der Klasse Component erbt und so weiter und so fort. Allen liegt die Object-Klasse zu Grunde, die Urahnin aller .NET-Klassen. Sie enthält Eigenschaften, die alle anderen Klassen gemeinsam nutzen. Wenn Sie also wissen, wie Sie mit einem Objekt interagieren, können Sie demzufolge auch mit allen seinen Abkömmlingen interagieren. Jeder Abkömmling erbt Merkmale von allen seinen Vorgängern. Vererbung erspart uns eine ganze Menge Codeschreiben bzw. Umschreiben. Vererbung erleichtert auch das Erlernen neuer Klassen. Sofern Sie wissen, wie Sie mit ihren Basisklassen umgehen, verstehen Sie auch mit einer neuen Klasse umzugehen. An Tag 3 werden wir die Klasse Form genauer unter die Lupe nehmen. Da Sie nun über Klassen Bescheid wissen, sollten Sie auch erfahren, dass auch die Form-Klasse Public ist. Sonst wäre Ihre Klasse HelloWorld nicht in der Lage, darauf zuzugreifen. Nur nicht-Private Mitglieder der Form-Klasse werden vererbt. Nicht nur Klassen können erben, sondern auch Methoden, und zwar mit der gleichen Syntax. Klassen und Vererbung sind die zwei wichtigsten Konzepte in der OOP. Sobald Sie diese einmal verstanden haben, fällt Ihnen der Rest wesentlich leichter. Über diese Themen erfahren Sie mehr im ganzen restlichen Buch.

Namensräume und Assemblies Die Zeilen 1 und 2 in Listing 2.1 enthalten ein weiteres wesentliches Element von Windows Forms-Anwendungen: das Schlüsselwort Imports (in C# lautet es using, wie Sie in Listing 2.2 sehen). Wie Sie bereits erfahren haben, erlaubt Ihnen dieses Schlüsselwort, Sammlungen anderer Klassen in Ihrer Anwendung zu verwenden. Solche Sammlungen werden als Namensräume bezeichnet. Sie dienen dazu, miteinander verwandte Klassen zu gruppieren.

49

Windows Forms-Anwendungen erstellen

In Zeile 1 importiert die Anwendung den Namensraum System. Innerhalb des Namensraumes System befinden sich zahlreiche Objekte und Klassen, die Ihre Anwendung nutzt, selbst wenn Sie es nicht wissen. Beispielsweise sind die gebräuchlichen Datentypen Integer und String Bestandteil des Namensraumes System. Wenn Sie also eines dieser Objekte nutzen, ist es gut, den Namensraum System zu importieren (zwar eine gute Idee, aber nicht zwingend). Die Zeile 2 importiert den Namensraum System.Windows.Forms, welcher die Klasse Forms enthält. Der Forms-Namensraum ist einer von vielen im Windows-Namensraum, und dieser ist wiederum nur einer von vielen im System-Namensraum. Namensräume sind hierarchisch organisiert, um ihre Nutzung intuitiv und einfacher zu machen, insbesondere dann, wenn man schon die OOP-Grundbegriffe versteht. Ein paar der gebräuchlicheren Namensräume sind: 쐽

System



System.Data



System.Drawing



System.Net



System.Security



System.Web



System.Web.UI



System.Web.Services



System.Windows



System.Windows.Forms



System.Xml

Der Umstand, dass Namensräume hierarchisch untereinander angeordnet sind, bedeutet nicht notwendigerweise, dass der eine irgendetwas vom anderen erbt. Namensräume sind im Unterschied zu Klassen lediglich Sammlungen von Objekten – aus ihnen werden keine nutzbaren Objekte erstellt. Man verwendet sie lediglich aus organisatorischen Gründen. Natürlich können Sie leicht Ihre eigenen Namensräume erstellen. Es mag an diesem Punkt noch nicht nötig sein, aber sobald Sie komplexere Anwendungen erzeugen, die aus mehr als einer Quellcodedatei bestehen, wollen Sie sie vielleicht in einem Namensraum gruppieren. Beispielsweise so: Namespace MyNamespace Public Class MyClass : Inherits Form 'etwas mehr Code End Class End Namespace

50

Ein weiterer Blick auf Ihre Anwendung

Der restliche Code bleibt genau, wie er ist. Sie haben ihn nur mit einem Namensraum umgeben. In diesem Fall wird der Namensraum MyNamespace genannt. Sie können ihn auch vorhandenen Namensräumen hinzufügen: Namespace System.Windows.Forms

Es hängt alles davon ab, wie Sie diese Dinge gruppieren möchten. Wie gesagt, brauchen Sie Namensräume nicht unbedingt zu importieren. Dieses Vorgehen erlaubt Ihnen jedoch, weiter unten in Ihrem Code Kurzbefehle, also Abkürzungen, zu verwenden. Wenn Sie beispielsweise Zeile 2 aus Listing 2.1 entfernen würden, müssten Sie Zeile 4 von Public Class HelloWorld: Inherits Form

in Public Class HelloWorld: Inherits System.Windows.Forms.Form

ändern. Mit anderen Worten: Sie hätten dann den gesamten Namensraum-Namen für die Form-Klasse zu spezifizieren. Das Gleiche gilt für die Klasse System: Wenn Sie sie nicht importieren, würden Ihre Datentypen wie System.Integer statt nur wie Integer aussehen. Ich empfehle

Bitte beachten Sie

Importieren Sie Namensräume mit dem Schlüsselwort Imports, wenn Sie Klassen verwenden werden, die in diesem Namensraum enthalten sind; das hilft bei der Organisation und spart Zeit.

Verlassen Sie sich nicht auf die Verwendung des kompletten Namensraum-Namens für alle Ihre Objekte, selbst wenn Sie meinen, dass Ihnen das hilft, Klassen zu unterscheiden. Dieses Vorgehen bläht Ihren Code auf und erschwert es, auf Ihr Objekt in der Hierarchie zu verweisen. Gelegentliche Verwendung ist in Ordnung, aber übertreiben Sie es nicht.

Wenn Sie in Ihrer Anwendung keinen Namensraum festlegen, wird automatisch ein Standardnamensraum – auch als »Globaler Namensraum« bezeichnet –erzeugt. Er hat keinen eigentlichen Namen und dient lediglich dazu, die Klassen zu gruppieren, die nicht explizit in einem Namensraum deklariert wurden. Namensräume sind also logische Gruppierungen verwandter Klassen, aber das sagt noch lange nichts über ihre physische Position aus. Sie können diverse Quellcode-Dateien in verschiedenen Teilen Ihrer Festplatte abgelegt haben, aber alle können demselben Namensraum angehören. Um mehrere Dateien und Namensräume an einer einzigen Stelle zusammenzufassen, benutzt man Assemblies. Das sind physische Dateien auf Ihrer Festplatte, die die von Ihnen erstellten Klassen enthalten (meist mit der Endung .dll). Sie können mehrere Klassen oder Namensräume in einer einzelnen Assembly unterbringen.

51

Windows Forms-Anwendungen erstellen

Beispielsweise werden Sie in Ihrem Ordner c:\winnt\Microsoft.NET\Framework\version\ die Datei system.dll entdecken. Diese Assembly enthält die Namensräume System, System.CodeDom (und dessen Unternamensräume), System.ComponentModel, System.Net (und dessen Unternamensräume) und noch ein paar andere. Wie man sie verwendet, untersuchen wir etwas später heute im Abschnitt »Das Kompilieren von Windows Forms«. Tabelle 2.1 listet die verschiedenen .NET-Assemblies und die darin enthaltenen Namensräume auf. Assembly

Namensräume

custommarshalers.dll

Runtime.InteropServices.CustomMarshalers

mscorlib.dll

Collections, Configuration.Assemblies, Diagnostics.SymbolStore, Globalization, IO, IO.IsolatedStorage, Reflection, Reflection.Emit, Resources, alle Runtime.*-Namensräume (außer jenen in CustomMarshalers.dll, System.Runtime.Remoting.dll und System.Runtime.Serialization.Formatters.Soap.dll), Security, Security.Cryptography, Security.Cryptography.X509Certificates, Security.Permissions. Security.Policy, Security.Principal, Text, Threading

system.dll

System, CideDom, CodeDom.Compiler, Collections.Specialized, ComponentModel, ComponentModel.Design, Configuration, Diagnostics, Net, Net.Sockets, Text.RegularExpressions, Timers

system.configuration.install.dll

Configuration.Install

system.data.dll

Data, Data.Common, Data.OleDb, Data.SqlClient, Data.SqlTypes

system.design.dll

ComponentModel.Design.Serialization, Web.UI.Design, Web.UI.Design.WebControls, Windows.Forms.Design

system.directoryservices.dll

DirectoryServices

system.drawing.dll

Drawing,Drawing.Drawing2D, Drawing.Imaging, Drawing.Printing, Drawing.Text

system. drawing.design.dll

Drawing.Design

system.enterpriseser- EnterpriseServices, System.EnterpriseServices.CompensatingResourcevices.dll Manager system.management.dll Management, Management.Instrumentation system.messaging.dll

Messaging

Tabelle 2.1: NET-Assemblies und -Namensräume

52

Ein weiterer Blick auf Ihre Anwendung

Assembly

Namensräume

system.runtime. remoting.dll

Runtime.Remoting.Channels, Runtime.Remoting.Channels.Http, Runtime.Remoting.Channels.Tcp, Runtime.Remoting.MetadataServices system.Runtime.Serialization.Formatters.Soap.dll

Runtime.Serialization.Formatters.Soap system.security.dll

Security.Cryptography.Xml

system.serviceprocess.dll

ServiceProcess

system.web.dll

Web, Web.Caching, Web.Configuration, Web.Hosting, Web.Mail, Web.Security, Web.SessionState, Web.UI, Web.UI.HtmlControls, Web.UI.WebControls

system.web.services.dll

Web.Services, Web.Services.Configuration, Web.Services.Description, Web.Services.Discovery, Web.Services.Protocols

system.windows.forms.dll

Windows Forms

system.xml.dll

Xml, Xml.Schema, Xml.Serialization, Xml.Path, Xml.Xsl

(Soweit nicht anders spezifiziert, sind alle Namensräume Sub-Namensräume des Namensraums System.) Tabelle 2.1: NET-Assemblies und -Namensräume (Forts.)

Es gibt eine Menge Namensräume in Tabelle 2.1, von denen Sie manche in diesem Buch nie verwenden werden. Häufig müssen Sie aber wissen, zu welchen Assemblies sie gehören, um Ihre Anwendungen kompilieren zu können. Wenn Sie mit älteren Methoden der Windows-Programmierung vertraut sind, dürften Sie die .dll-Endung wiedererkennen, die für Dynamically Linked Libraries steht – diese DLLs sind Assemblies ähnlich. Sie stellten Objekte bereit, die von anderen Anwendungen genutzt werden konnten. DLL-Dateien werden sowohl in .NET als auch in Prä-.NET gebraucht. Denken Sie aber daran, dass .NET-DLL-Dateien in MSIL geschrieben und völlig objektorientiert sind. Prä-.NET-DLLs sind weder das eine noch das andere und leider gibt es keine Methode, um den Unterschied schon auf den ersten Blick zu erkennen. Assemblies benutzt man nicht nur für das Gruppieren von Namensräumen. Sie sind eine wichtige Komponente des .NET Frameworks, da sie die Grenzen zwischen Anwendungen darstellen, mit denen die CLR die Gültigkeitsbereiche Sicherheit, Versionierung und Metadaten erzwingt. Beim Entwickeln mit Windows Forms werden Sie Assemblies sehr häufig verwenden.

53

Windows Forms-Anwendungen erstellen

Die Main-Methode Sehen wir uns noch einmal die Zeilen 6, 7 und 8 in Listing 2.1 an: 6: 7: 8:

Public Shared Sub Main() Application.Run(New HelloWorld) End Sub

Wenn Sie wissen, was Methoden sind, haben Sie diesen Code bereits zur Hälfte verstanden. Eine Methode ist ein Codestück, das eine Funktion ausführt; sie bewirkt etwas. Code, der irgendeine Funktion ausführt, von der einfachsten Addition bis zu den kompliziertesten Threading-Routinen, muss immer in einer Methode untergebracht werden. In Visual Basic .NET gibt es zwei Methodentypen: Sub und Function. Sub macht etwas und endet dann. Function hingegen bewirkt etwas und sendet dann Daten an diejenige Instanz zurück, die die Methode aufgerufen hat. (Nichts hindert Sie, eine Function ohne Rückgabewert aufzusetzen; aber das Umgekehrte geht nicht: Eine Sub-Methode kann keine Daten zurückgeben.) In C# hingegen ist eine Methode eine Methode. Es wird keine Unterscheidung danach gemacht, ob die Methode Daten liefert oder nicht. Sie müssen in C# jedoch den Datentyp der zurückzugebenden Daten spezifizieren oder void, wenn keine Daten zurückgegeben werden. Beispielsweise sagt die Zeile 6:

public static void Main() {

in Listing 2.2 aus, dass diese Methode nichts liefert, wohingegen die folgende Codezeile einen Integer zurückgibt: 6:

public static int Main() {

Sie können automatisch ein paar Schlüsse ziehen: Sie deklarieren eine void-Funktion (sub in VB .NET), sie wird Main genannt und ist public (Public in VB .NET), was bedeutet, dass andere Klassen darauf zugreifen können. Das Schlüsselwort static (bzw. Shared) ist hingegen etwas Neues. Um es zu verstehen, müssen wir wieder zurückgehen zu Objekten und Klassen.

Objektinstanzen Aus einer einzelnen Klasse (oder Blaupause) lassen sich mehrere Objekte dieser Klasse erzeugen. Jedes einzelne Objekt bezeichnet man als eine Instanz. Das Auto-Objekt – eine wirkliche, physische, gebrauchsfertige Sache – wäre eine Instanz des Auto-Konzepts oder der Klasse. Man kann also ein Objekt haben, aber viele Instanzen: einen Mercedes SLK, einen VW Käfer oder einen Opel Tigra – all dies sind Instanzen eines Autos. Sie gleichen einander nicht ganz, aber sie alle haben das grundlegende Konzept eines Autos gemeinsam.

54

Ein weiterer Blick auf Ihre Anwendung

Abbildung 2.2 zeigt zwei Instanzen des Objekts »Standuhr«. Beachten Sie, dass sie nicht die gleiche Zeit anzeigen, doch irgendwie weiß man, dass beide Standuhren sind. Somit kann man viele Instanzen eines Objekts haben, von denen jede unterschiedliche Eigenschaften besitzt.

Standuhr A: 1:27

Standuhr B: 9:54

Abbildung 2.2: Zwei Instanzen eines Standuhr-Objekts können unterschiedliche Eigenschaften besitzen.

Das Gleiche lässt sich über OOP und Objekte sagen. Die Klasse definiert den theoretischen Begriff eines Objekts HelloWorld, erzeugt aber keine Instanzen. Es ist anderen Objekten überlassen, diese Instanzen zu erzeugen, wenn sie gebraucht werden (gleich mehr darüber). Wenn Sie vor einer Methode das Schlüsselwort Shared oder static verwenden, bedeutet dies, dass die Methode oder Eigenschaft allen Instanzen dieses Objekts zur Verfügung steht und für alle Instanzen gleich ist. Beispielsweise sind alle Autos aus Blech gefertigt und der Einfachheit halber sagen wir, dass das Blech das gleiche für jedes Auto ist. Dies wäre eine static-Eigenschaft: Die Metallart ändert sich nicht von Auto zu Auto. Alle Autos nutzen außerdem das Konzept der Farbe gemeinsam, haben aber nicht alle die gleiche Farbe; dies wäre dann nicht static. Die Schlüsselwörter sind selbsterklärend: Einzelwerte oder einzelne Methoden können Shared sein (in VB .NET), wenn alle Objekte sie gemeinsam nutzen (jedes Objekt hat nicht seine eigene Kopie), oder static (in C#), was bedeutet, dass sie sich nicht von einer Instanz zur nächsten ändern. Die Begriffe Shared und static sind im .NET Framework recht gebräuchlich. Daher ist es sehr wichtig, dass Sie ein solides Grundverständnis dafür erwerben. Für den Moment mag es genügen zu wissen, dass Ihre Main-Methode stets entweder static oder Shared sein muss.

Das Eingangstor zum Code Die Main-Methode ist auch als Einsprungspunkt zu einer Anwendung bekannt: Bei jedem Start Ihrer Anwendung wird die Main-Methode stets als erstes ausgeführt. Verfügt Ihr Programm über keine Main-Methode, wird es einfach nichts tun (man bekommt eine Fehler-

55

Windows Forms-Anwendungen erstellen

meldung wohl schon lange, bevor man es ausführen will). Die Main-Methode wird auch als letztes ausgeführt. Wenn Ihr Code mit der Ausführung fertig ist, kehrt er stets zu dieser Methode zurück. Der Main-Methode kommt also eine hohe Bedeutung zu. Entweder lässt sich der gesamte Programmcode in der Main-Methode unterbringen oder Main kann andere Methoden aufrufen. Von Main aus können Sie bestimmen, welches Formular anzuzeigen ist (viele Anwendungen können mehr als ein Windows Form-Objekt enthalten), Variablen festlegen, die von der restlichen Anwendung verwendet werden, oder beliebige weitere Vorbereitungsschritte ausführen. In unserer Main-Methode wird eine einzige Codezeile ausgeführt: Application.Run(New HelloWorld)

Diese Zeile ruft die Run-Methode des Application-Objekts (einen Teil des Namensraums System.Windows.Forms) auf. Die Run-Methode befiehlt dem Programm, in eine Nachrichtenschleife einzutreten. Das ist im Grunde die Art und Weise eines Programms, auf Eingaben vom Benutzer zu warten. Der Eintritt in eine Nachrichtenschleife ist vom Standpunkt des Benutzers aus fast immer der Startpunkt einer Anwendung. Morgen werden Sie mehr über Nachrichtenschleifen erfahren. Sie übergeben der Run-Methode eine Instanz Ihres Objekts HelloWorld. (Bitte beachten Sie, dass wir von einer Klasse zu einem Objekt übergegangen sind: Das New-Schlüsselwort erstellt eine Objektinstanz.) Das weist die Run-Methode an, Ihr Formular auf dem Bildschirm für den Dialog mit dem Benutzer anzuzeigen. Moment mal! Warum müssen wir eine Instanz der HelloWorld-Klasse an die Run-Methode übergeben? Dürfte das nicht zu einer Endlosschleife führen? Schließlich wird die MainMethode erneut ausgeführt, übergibt eine neue Instanz und so weiter. Nicht ganz. Um dies zu verstehen, müssen Sie zwischen einer Klasse und einer Anwendung unterscheiden. Wenn eine neue Instanz einer Klasse erzeugt wird, passiert nichts (es sei denn, man hätte einen Konstruktor erzeugt, aber so weit sind wir noch nicht). Wenn eine Anwendung startet, sucht sie zuerst nach einer Main-Methode, ganz gleich, wo sie diese findet oder ob sie sich in einer Klasse befindet, solange es sie nur gibt und sie in einer Klasse untergebracht ist. Main wird nur ein einziges Mal ausgeführt, wenn Ihre Anwendung gestartet wird, und danach nicht mehr. Anders ausgedrückt, wird Main bei jeder Anwendungsausführung gestartet, aber nicht immer dann, wenn eine Klasse erzeugt wird. Obwohl die Main-Methode ganz normal aussieht, führt jeder Versuch, sie von einem anderen Punkt in Ihrem Programm aus aufzurufen, zu einer Fehlermeldung, wie sie in Abbildung 2.3 zu sehen ist.

56

Ein weiterer Blick auf Ihre Anwendung

Abbildung 2.3: Eine Fehlermeldung erfolgt, wenn man versucht, die Main-Methode im Code auszuführen.

Obwohl nun Zeile 7 in Listing 2.1 Application.Run(New HelloWorld)

eine neue Instanz der Klasse HelloWorld erzeugt, wird die Main-Methode nicht erneut ausgeführt. Um die Trennung zwischen der HelloWorld-Klasse und der Main-Methode noch weiter zu unterstreichen, könnten Sie die Main-Methode in eine völlig andere Klasse stecken, und sie würde weiterhin funktionieren, wie Sie in Listing 2.3 sehen. Kurz und gut, Sie brauchen für eine Anwendung eine Main-Methode, die sich in einer Klasse befinden muss – doch diese Klasse können Sie willkürlich auswählen. Listing 2.3: Die Main-Methode lässt sich in jeder Klasse unterbringen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

Imports System Imports System.Windows.Forms Public Class HelloWorld : Inherits Form Public Sub New() Me.Text = "HelloWorld!" End Sub End Class Public Class MyMainClass Public Shared Sub Main() Application.Run(New HelloWorld) End Sub End Class

Listing 2.3 ergibt wahrscheinlich etwas mehr Sinn, denn es zeigt ganz deutlich, dass die Main-Methode und die Windows Form-Klasse zwei völlig verschiedene Einheiten sind. Die Run-Methode in Zeile 12 erzeugt eine neue Instanz Ihrer Windows Form-Klasse (HelloWorld) und bewirkt, dass sie auf dem Bildschirm angezeigt wird.

57

Windows Forms-Anwendungen erstellen

Die Methode New (oder HelloWorld in C#) im Schlussteil von Listing 2.3 wird als Konstruktor bezeichnet. Das ist eine Methode, die ausgeführt wird, sobald eine neue Instanz einer Klasse erzeugt wird. Sie hat eine »Partner«-Methode namens Destruktor. Wir sprechen im nächsten Abschnitt über diese zwei Methoden.

Konstruktoren und Destruktoren Einen Konstruktor benutzt man normalerweise, um die Instanz zu initialisieren, erforderliche Variablen anzulegen und so weiter. In Visual Basic .NET hat ein Konstruktor stets den Namen New, wie die Zeilen 10 bis 12 in Listing 2.3 zeigen: 10: 11: 12:

Public Sub New() Me.Text = "HelloWorld!" End Sub

Das ist sinnvoll. Erinnern Sie sich an Zeile 7 aus Listing 2.1, wie Sie New klassenname verwendet haben: Application.Run(New HelloWorld)

Dieser Befehl ruft die New-Methode auf. In C# nimmt der Konstruktor denselben Namen wie die Klasse selbst an, so wie in den folgenden Zeilen aus Listing 2.2: Public HelloWorld() This Text = 'Hello World!" }

Die Me-Variable in Listing 2.1 11:

Me.Text = "Hello World!"

und die this-Variable in Listing 2.2 11:

thisText = "Hello World!";

sind spezielle Schlüsselwörter, die auf das Objekt verweisen, in dem sich der Code jeweils befindet. In diesem Fall verweisen sie auf das Windows Form-Objekt HelloWorld. Auf diese Weise lässt sich leicht auf das umschließende Objekt verweisen. Zeile 11 in Listing 2.1 und 2.2 weist die Zeichenfolge »Hello World!« der Eigenschaft Text in der HelloWorld-Klasse zu, welche, wie wir gesehen haben, die Titelzeile auf dem Windows Form festlegt. Umgekehrt wird ein Destruktor immer dann aufgerufen, wenn ein Objekt der Klasse zu entsorgen ist. In Visual Basic .NET wird dies bewerkstelligt, indem das Objekt mit nothing gleichgesetzt wird: objHelloWorld = new HelloWorld() objHelloWorld = nothing

58

Ein weiterer Blick auf Ihre Anwendung

In C# erfolgt dies durch Gleichsetzung der Klasse mit null: HelloWorld objHelloWorld = newHelloWorld(); objHelloWorld = null;

In VB .NET kann man jedoch keinen eigenen Destruktor erzeugen: Immer wenn Sie Ihr Objekt mit nothing gleichsetzen, übernimmt VB .NET die Entsorgung für Sie. C# erlaubt es, Ihren eigenen Destruktor zu bestimmen, der durch den gleichen Namen wie der Konstruktor, aber mit einer Tilde davor deklariert wird: ~HelloWorld() { 'etwas verarbeiten }

Konstruktoren erben In OOP gilt die Regel, dass der Konstruktor einer Klasse stets den Konstruktor seiner übergeordneten Klasse (Basisklasse) aufrufen muss. Beispielsweise erbt die Klasse HelloWorld von der Form-Klasse. Daher muss Ihre Klasse den Konstruktor der Form-Klasse aufrufen. Dieser wiederum muss den Konstruktor seiner übergeordneten Klasse aufrufen, nämlich den von ContainerControl, und so weiter, bis Sie die Wurzel des Stammbaums erreicht haben: Object. Werfen wir einen Blick darauf, warum dies notwendig ist. Sie erinnern sich, dass, sobald ein Objekt von einem anderen (seinem übergeordneten Objekt) erbt, es auch alle Eigenschaften und Methoden des anderen erbt. Manche davon können von bestimmten Variablen abhängen, die es zu initialisieren gilt. Stellen Sie sich beispielsweise eine Klasse vor, die eine Farbe-Eigenschaft erbt, die den Hintergrund des Formulars bestimmt. Dieser Eigenschaft muss ein echter Wert zugewiesen werden, bevor man sie verwenden kann, sonst erhält man eine Fehlermeldung (schließlich lässt sich die Hintergrundfarbe nicht auf »nichts« setzen). Die Farbe-Eigenschaft würde im Konstruktor Ihrer übergeordneten Klasse initialisiert werden. Daher müssen Sie den Konstruktor der Basisklasse aufrufen, damit die Farbe-Eigenschaft eine Bedeutung erhält. Diese Abhängigkeit von Basisklassen besagt zweierlei. Erstens hängen viele Klassen, die von anderen erben, von ihren Basisklassen ab, um so Informationen zu erhalten. Und zweitens bedeutet es, dass Sie in Ihrem Konstruktor weniger Initialisierungscode schreiben müssen, weil schon vieles in den Konstruktoren der Ihrer Klasse übergeordneten Klassen erledigt worden ist. Der Aufruf des Konstruktors der Basisklasse ist in Ihrem Konstruktor als allererstes zu erledigen. In VB .NET erfolgt dies durch den Aufruf der Methode MyBase.New, in C#, indem Sie Ihren Konstruktor zum Ausführen der Methode base() veranlassen: 'VB .NET Public Sub New() MyBase.New End Sub

59

Windows Forms-Anwendungen erstellen

'C# public HelloWorld(): base() { }

In all dem Code, den Sie heute geschrieben haben (Listings 2.1 bis 2.3), fehlen diese Aufrufe, doch warum erhalten wir dann keine Fehlermeldungen? Wenn Sie in Ihrer Klasse nur einen Konstruktor haben (mehr über multiple Konstruktoren im nächsten Abschnitt), ruft die CLR automatisch den Konstruktor der Basisklasse auf. Daher können Sie diese Zeilen weglassen.

Konstruktoren überladen Manchmal möchten Sie mehr als einen Konstruktor haben. Ein nützliches Beispiel dafür bestünde darin, dass Sie dem Benutzer erlauben möchten, auf mehr als eine Weise zu initialisieren: so etwa eine, die einen Wert für eine Variable erfordert, und eine, die keinen Wert erfordert. Sie können dies durch einen Vorgang namens Überladen erledigen. Zum besseren Verständnis dieses Konzepts schauen wir uns den Code an, wie er in Listing 2.4 abgedruckt ist. Listing 2.4: Konstruktoren in C# überladen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:

using System; using System.Windows.Forms; public class HelloWorld : Form { public static void Main() { Application.Run(new HelloWorld()); } public HelloWorld(): this("Hello World!") {} public HelloWorld(string strText): base() { thisText = strText; } }

Lassen Sie uns zunächst in Zeile 12 springen. Hier sehen wir einen Konstruktor, der einen string-Wert als Parameter übernimmt. Die Text-Eigenschaft des Formulars wird in Zeile 13 auf diesen Parameter eingestellt. Schließlich ruft dieser Konstruktor mit der base()-Methode den Konstruktor der Basisklasse auf. Soweit also nichts Neues.

60

Ein weiterer Blick auf Ihre Anwendung

In Zeile 10 stoßen wir auf etwas, das wie ein weiterer Konstruktor aussieht. Doch dieser übernimmt keine Parameter und verfügt über keinen Initialisierungscode. Schauen Sie aber mal auf den Code nach dem Doppelpunkt. Er ruft this auf, von dem wir wissen, dass es eine Referenz auf das aktuelle Objekt ist, und übergibt ihm die Zeichenfolge »Hello World!«. Was bewirkt dies? Zeile 10 besagt, dass im Fall des Aufrufs dieses Konstruktors statt dessen der Konstruktor in Zeile 12 aufgerufen und ihm als Parameter die Zeichenfolge »Hello World!« übergeben werden soll. Die Ausführung geht dann zu den Zeilen 12 und 13 über, die Text-Eigenschaft wird auf »Hello World!« gesetzt. Im Grunde sagen Sie also, dass der Parameter strText optional ist: Wenn kein Parameter angegeben ist, verwendet der Konstruktor die Standardzeichenfolge »Hello World!«. Zeile 7, die einen Konstruktor aufruft, könnte also entweder so Application.Run(new HelloWorld());

oder so Application.Run(new HelloWorld("I love NY"));

aussehen. Dies ruft den jeweils passenden Konstruktor auf, je nachdem, ob Parameter übergeben wurden. Der Konstruktor in Zeile 10 könnte sogar seine eigenen Initialisierungsroutinen ausführen und braucht den Konstruktor in Zeile 12 nicht aufzurufen, wenn man nicht will. In VB .NET sieht die Syntax etwas anders aus, wie man in Listing 2.5 sehen kann. Listing 2.5: Konstruktoren in VB .NET überladen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:

Imports System Imports System.Windows.Forms Public Class HelloWorld : Inherits Form Public Shared Sub Main() Application.Run(New HelloWorld) End Sub Public Sub New() Me.New("Hello World!") End Sub Public Sub New(strText as String) Me.Text = strText End Sub End Class

61

Windows Forms-Anwendungen erstellen

In Zeile 11 verwenden Sie das Schlüsselwort Me und rufen die New-Methode auf, wobei Sie eine Standardzeichenfolge bereitstellen. Die dem Listing 2.5 zugrunde liegenden Konzepte sind die gleichen wie in Listing 2.4. Beachten Sie, dass Sie keineswegs einen Konstruktor erzeugen müssen. Wenn Sie keinen erzeugen, dann erzeugt VB .NET (oder C#) einen für Sie. Dieser automatisch generierte Konstruktor ruft nur den Konstruktor der Basisklasse auf. Konstruktoren sind nicht die einzigen Methoden, die Sie überladen können. Doch in VB .NET müssen Sie das Schlüsselwort Overloads vor jede Methode stellen, die überladen wird (Konstruktoren natürlich ausgenommen), und zwar vor dem Schlüsselwort Public.

Ein abschließender Blick auf den Programmcode Nach der Untersuchung aller Codebestandteile lassen Sie uns rekapitulieren. Listing 2.1 ist erneut in Listing 2.6 zu sehen. Listing 2.6: Ihre erste Windows Forms-Anwendung in VB .NET 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

Imports System Imports System.Windows.Forms Public Class HelloWorld : Inherits Form Public Shared Sub Main() Application.Run(New HelloWorld) End Sub Public Sub New(strText as String) Me.Text = strText End Sub End Class

Die Zeilen 1 und 2 bestimmen die Namensräume, die in Ihre Anwendung importiert werden. Die Mitglieder dieser Namensräume stehen Ihrer Anwendung in Form ihrer Kurznamen (also Form anstelle von System.Windows.Forms.Form) zur Verfügung. Zeile 4 deklariert Ihre Klasse und die Tatsache, dass sie von der Form-Klasse erbt, der Basisklasse für alle Windows Forms-Anwendungen. Zeile 6 startet die MainMethode, den Einsprungspunkt Ihrer Anwendung. Sie ruft die Run-Methode

62

Das Kompilieren von Windows Forms

auf, welche das Formular zur Darstellung auf dem Bildschirm und zum Warten auf Benutzereingaben veranlasst. Sie übergeben eine neue Instanz Ihrer Klasse, um ihr mitzuteilen, welches Formular angezeigt werden soll. Die Zeilen 10 bis 12 bilden einen Konstruktor für die HelloWorld-Klasse. In Zeile 11 weisen Sie lediglich der Text-Eigenschaft in Ihrem Formular die Zeichenfolge »Hello World!« zu. Das ändert die Titelzeile Ihrer Anwendung. Sobald Sie einmal die Grundlagen verstanden haben, können Sie erkennen, wie einfach dieser Quellcode war. Jetzt kennen Sie alle Komponenten, die für die Erstellung von Windows Forms notwendig sind, und können anfangen, Ihre eigene Windows Forms-Anwendung zu erstellen!

2.2

Das Kompilieren von Windows Forms

Der nächste Schritt bei der Anwendungserstellung besteht im Kompilieren Ihres Quellcodes zu MSIL. Die Vorgehensweise variiert je nach der verwendeten Programmiersprache, doch sie ist stets recht ähnlich. Die einfachste Kompiliermethode besteht in der Verwendung von Visual Studio .NET. VS .NET ist eine integrierte Entwicklungsumgebung (IDE), die Ihnen die Erstellung beliebiger Anwendungstypen an einem zentralen Ort erlaubt, ganz gleich welche Programmiersprache Sie benutzen. Lassen Sie uns einen kleinen Rundgang durch diese Entwicklungsumgebung machen und betrachten, wie man hier Code schreiben und kompilieren kann. Öffnen Sie VS .NET und starten ein neues Projekt, indem Sie NEU/PROJEKT aus dem DATEI-Menü auswählen. Das Dialogfeld NEUES PROJEKT (Abbildung 2.4) erscheint.

Abbildung 2.4: Das Dialogfeld NEUES PROJEKT stellt Ihnen eine VB .NET- oder C#-Anwendung zur Auswahl.

63

Windows Forms-Anwendungen erstellen

Wählen Sie mit Hilfe dieses Dialogs eine Anzahl verschiedener Optionen sowohl für C# als auch für Visual Basic .NET. Klicken Sie in der linken Fensterhälfte auf VISUAL BASICPROJEKTE und in der rechten auf WINDOWS-ANWENDUNG. Wählen Sie einen passenden Namen und einen Speicherplatz für dieses Projekt und klicken Sie auf OK. Sobald VS .NET mit der Verarbeitung fertig ist, zeigt es ein neues Projekt im PROJEKTMAP(obere rechte Ecke der VS .NET-Anwendung) an, mit diversen Dateien darunter (siehe Abbildung 2.5).

PEN-EXPLORER

Zunächst finden Sie einen Ordner namens VERWEISE, der alle Namensräume anzeigt, die Ihre Anwendung verwenden wird; beachten Sie bitte, dass sich innerhalb des Ordners (auf das Plus-Zeichen klicken, um ihn aufzuklappen) bereits einige Namensräume befinden, die für Sie vorgegeben sind. Wenn Sie mit den Statements using oder Imports einen Namensraum haben wollen, der sich nicht hier befindet, müssen Sie den Namensraum hinzufügen, indem Sie mit der rechten Maustaste auf den Ordner klicken und VERWEIS HINZUFÜGEN auswählen. Daraufhin erscheint ein Dialogfenster, das Ihnen bei der Auswahl einer Assembly hilft. Die nächste ist die Datei AssemblyInfo.vb, die verwendet wird, um Informationen über Ihre Assembly bereitzustellen. Schließlich findet sich noch eine Datei namens Form1.vb, worin Ihr Code abgelegt wird. Öffnen Sie diese Datei mit einem rechten Mausklick und der Auswahl CODE ANZEIGEN. Ihr Fenster sollte nun ähnlich aussehen wie in Abbildung 2.5.

Abbildung 2.5: Ihre leere Windows Forms-Anwendung wird im Projektmappen-Explorer angezeigt.

In VS .NET wird ein einfach zu bedienender Quellcode-Editor mitgeliefert. Er gruppiert ähnliche Dinge wie etwa Klassen, und er erlaubt Ihnen, sie auszublenden (indem Sie auf das Minus-Symbol links daneben klicken), um so die Arbeitsumgebung übersichtlicher zu

64

Das Kompilieren von Windows Forms

machen. Beachten Sie auch, dass VS .NET etwas Code für Sie erzeugt hat, den Sie in dem Code-Bereich im Windows Forms-Designer finden. Diesen Code werden wir hier nicht besprechen, aber wenn Sie mal durchblättern, dann werden Sie zumindest eine Methode wiedererkennen: den Konstruktor der Klasse. Mit dem restlichen Code integriert VS .NET Ihre Anwendung in die Entwicklungsumgebung. (Das soll nicht bedeuten, dass Sie den Code nicht löschen könnten, wenn Sie wollen.) Sie können nun Ihren Code an beliebiger Stelle zwischen den Zeilen Public Class und End Class einfügen. Sie können auch den Namen der Klasse bearbeiten. Das Eigenschaftenfenster unten rechts gestattet Ihnen die Änderung des gespeicherten Dateinamens, des Autorennamens und anderer Attribute. Sobald Sie den Code geschrieben haben, wählen Sie im Menü ERSTELLEN den Befehl ERSTELLEN, um Ihre Anwendung zu erzeugen. Befinden sich Fehler im Code, werden sie in einem Ausgabe-Fenster angezeigt. Läuft alles erfolgreich ab, dann erzeugt VS .NET eine .EXE-Datei (der Dateiname hängt vom gewählten Projektnamen ab), die Sie wie jede beliebige Anwendung ausführen können. VS .NET enthält zahlreiche Funktionen, die hier nicht vorgestellt werden. Erforschen Sie die Entwicklungsumgebung weiter und schreiben Sie Ihren Code. Wenn Sie lieber nicht mit VS .NET arbeiten wollen oder keinen Zugang dazu haben, können Sie Ihren Code mit dem jeweils nötigen Befehlszeilen-Compiler kompilieren. Das ist ein einfaches Programm, das einige Parameter übernimmt (darunter Ihre QuellcodeDatei) und eine kompilierte EXE-Datei »ausspuckt«. Der nötige Compilerbefehl ist entweder vbc.exe (für Quellcode in Visual Basic .NET) oder csc.exe (für Quellcode in C#). Für beide Methoden ist die Syntax dieselbe, daher betrachten wir hier nur die erste: vbc optionen quellcodedatei

In seiner einfachsten Form sieht Ihr Befehl wie folgt aus: vbc helloworld.vb

Tabelle 2.2 führt die gebräuchlichen Befehlszeilenoptionen auf, die Sie nutzen können. Geben Sie vbc /? oder csc /? In der Befehlszeile ein, um alle Optionen anzusehen. Option

Beschreibung

@

Spezifiziert eine Textdatei, die die für den Befehl zu verwendenden Optionen enthält. Sie müssen einen Dateinamen angeben: @dateiname.

/bugreport

Erzeugt eine Fehlerberichtsdatei. Sie müssen einen Dateinamen angeben: /bugreport:dateiname.

Tabelle 2.2: Gebräuchliche Befehlszeilen-Optionen für den Compiler

65

Windows Forms-Anwendungen erstellen

Option

Beschreibung

/debug

Gibt diverse Informationen aus. Verwenden Sie: 왘 /debug+ um Informationen auszugeben 왘

/debug- um keine Informationen auszugeben



/debug:full um alle Debugging-Informationen auszugeben (Voreinstellung)



/debug:pdbonly um lediglich die pdb-Symboldatei zu erstellen

/main

Gibt die Klasse an, die die Main-Methode enthält. Verwenden Sie /m als Kurzform.

/nologo

Unterdrückt das Microsoft-Logo.

/optioncompare

Verwenden Sie binary oder text, um anzugeben, wie alle Zeichenfolgenvergleiche in Ihrem Code durchgeführt werden sollen.

/optionexplicit

Erfordert die explizite Deklaration aller Variablen vor dem Einsatz. Verwenden Sie /optionexplicit+ oder /optionexplicit-.

/optionstrict

Erzwingt strikte Befolgung der Sprachsemantik. Verwenden Sie /optionstrict+ oder /optionstrict-.

/out

Gibt die Ausgabestandort und den Namen Ihrer kompilierten MSIL-Datei an.

/reference

Gibt zu referenzierende Assemblies in Ihrer Anwendung an (jene, die durch die Schlüsselwörter using oder Imports spezifiziert wurden). Verwenden Sie /r als Kurzform.

/target

Gibt den Typ der zu generierenden Ausgabedatei an. Verwenden Sie /t als Kurzform. Setzen Sie ein: 왘 /t:exe für eine ausführbare Konsolen-Anwendung; 왘

/t:library für eine DLL;



/t:winexe für eine ausführbare Windows-Anwendung;



/t:modul für ein Modul.

/win32icon

Gibt das zu verwendende Symbol (.ico-Datei) für das Anwendungssymbol an.

/win32resource

Gibt die zu verwendenden Ressourcen-Dateien (.res-Dateien) an.

Tabelle 2.2: Gebräuchliche Befehlszeilen-Optionen für den Compiler (Forts.)

Für unsere Anwendung verwenden wir normalerweise nur zwei Optionen: /target (Kurzform /t) und /reference (Kurzform /r). Um beispielsweise die Anwendung HelloWorld zu kompilieren, sollte das Ziel winexe sein, und wir müssen Referenzen auf die Assemblies hinzufügen, die gebraucht werden (je nach den Namensräumen, die wir importiert

66

Eine vollständige Anwendung

haben). Wenn wir auf unseren Quellcode und die Tabelle 2.1 zurückblicken, sehen Sie, dass wir die Namensräume System und System.Windows.Forms verwenden. Sie befinden sich jeweils in den Assemblies system.dll und system.windows.forms.dll. Daher sieht unsere Befehlszeile schließlich so aus: vbc /t:winexe /r:system.dll /r:system.windows.forms.dll HelloWorld.vb

Der Compiler erlaubt es Ihnen auch, mehrere Referenzen in einer einzigen Liste zu kombinieren. Ein Beispiel: vbc /t:winexe /r:system.dll,system.windows.forms.dll HelloWorld.vb

Die Assembly mscorlib.dll aus Tabelle 2.1 steht automatisch allen Anwendungen zur Verfügung: Sie ist die grundlegende Assembly für .NET. Aus diesem Grund brauchen Sie keinen expliziten Verweis dafür einzufügen. Von Ihrer Eingabeaufforderung aus veranlasst würde der Befehl die in Abbildung 2.6 sichtbare Ausgabe erzeugen.

Abbildung 2.6: Eine erfolgreiche Kompilierung führt zu diesem Ergebnis.

Welchen Weg Sie auch wählen, die Kompilierung ist ein notwendiger Schritt zu Ihrer Windows Forms-Anwendung. Im restlichen Buch verwenden wir die Befehlszeilenmethode, falls nicht anders festgelegt. Anhand der Befehle können Sie leicht bestimmen, welche Referenzen Sie Ihrem VS .NET-Projekt hinzufügen müssen.

2.3

Eine vollständige Anwendung

Da Sie nun mit allen Bestandteilen einer Windows Forms-Anwendung ausgestattet sind, können Sie mit dem Aufbau einer Applikation beginnen, die etwas nützlicher als das »Hello World«-Programm ist. Im folgenden Abschnitt werden wir einen einfachen Taschenrechner erstellen, den Sie dann an Tag 5 und 6 erweitern.

67

Windows Forms-Anwendungen erstellen

Um einen Taschenrechner zu erstellen, brauchen Sie mindestens drei Dinge (neben Windows Forms): 쐽

eine Stelle, an der der Benutzer eine Zahl bzw. Zahlen eingeben kann,



eine Stelle, an der er das Ergebnis sehen kann (möglicherweise an der gleichen Stelle),



eine oder mehrere Schaltflächen, die es ihm erlauben, mathematische Operationen auszuführen. Der Einfachheit halber beschränken wir uns hierbei auf die Addition.

Diese Komponenten werden von Windows Forms-Steuerelementen (Controls) bereitgestellt: interaktiven Elementen der Benutzeroberfläche (englisch User Interface, abgekürzt UI). Ab Tag 4 werden wir diese Steuerelemente genauer unter die Lupe nehmen, so dass wir uns hier nicht allzu lange damit aufhalten müssen. Zunächst wollen wir einmal den grundlegenden Rahmen für Ihre Anwendung erstellen. Dazu gehören alle Bestandteile, über die Sie heute etwas erfahren haben. Werfen Sie einen Blick auf Listing 2.7 (der komplette C#-Code wird später gezeigt). Listing 2.7: Das Fundament für die Taschenrechner-Anwendung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:

Imports System Imports System.Windows.Forms Namespace TYWinforms.Day2 Public Class Calculator Public Shared Sub Main() Application.Run(New Calculator) End Sub Public Sub New() End Sub End Class End Namespace

Speichern Sie diesen Code unter dem Dateinamen Listing 2.7.vb. Im Listing gibt es nichts Neues zu entdecken. Es ist sogar fast identisch mit Listing 2.1, nur dass in Zeile 4 eine Namensraum-Deklaration auftaucht. Diese teilt der CLR mit, dass diese Klasse zum Namensraum TYWinforms.Day2 gehört.

68

Eine vollständige Anwendung

Die Calculator-Klasse wird in Zeile 6 deklariert, sie erbt von der Klasse System.Windows.Forms.Form. Die Zeilen 8 bis 10 enthalten die Main-Methode, die einfach die Run-Methode aufruft und eine neue Instanz der aktuellen Klasse als Parameter übergibt. Die Zeilen 12 bis 14 enthalten eine Konstruktor-Methode, die momentan noch leer ist – in Kürze fügen wir den entsprechenden Code hinzu. In dieser Anwendung benötigen wir vier Windows Forms-Steuerelemente: zwei Textfelder (TextBox) für die Zahleneingabe, ein Bezeichnungsfeld (Label) für die Ergebnisanzeige und eine Schaltfläche (Button) für die Erledigung der Addition. Alle diese Steuerelemente gehören dem Namensraum System.Windows.Forms an, so dass Sie sie genau wie die FormKlasse verwenden können. Geben Sie in Zeile 7 Ihrer Listing 2.7.vb-Datei folgenden Code ein: Private Private Private Private

WithEvents btnAdd as Button tNumber1 as TextBox tNumber2 as TextBox lblAnswer as Label

Dieser Abschnitt deklariert vier Variablen als Steuerelemente. Das Schlüsselwort WithEvents bedeutet im Grunde, dass ein bestimmtes Steuerelement Aktionen ausführen kann. Listing 2.8 zeigt die vollständige Konstruktor-Methode. Listing 2.8: Ihre Anwendung initialisieren 12: 13: 12: 13: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:

Public Sub New() Me.btnAddYour Application Public Sub New() Me.btnAddInitializing Your Application Public Sub New() Me.btnAdd = New Button Me.tbNumber1 = New TextBox Me.tbNumber2 = New TextBox Me.lblAnswer = New Label tbNumber1.Location = New Point(0,0) tbNumber2.Location = New Point(100,0) btnAdd.Location = New Point(0,25) btnAdd.Text = "addieren" AddHandler btnAdd.Click, new EventHandler(AddressOf Add) lblAnswer.Location = New Point(0,75) Me.Controls.Add(btnAdd)

69

Windows Forms-Anwendungen erstellen

28: 29: 30: 31:

Me.Controls.Add(tbNumber1) Me.Controls.Add(tbNumber2) Me.Controls.Add(lblAnswer) End Sub

Fügen Sie diesen Code Ihrer Datei Listing 2.7.vb hinzu. Wir gehen auch hier noch nicht auf die Spezifika der einzelnen Steuerelemente ein, sondern kümmern uns nur um die Grundlagen. Die Zeilen 13 bis 16 erzeugen neue Instanzen jedes Steuerelements. Sie fragen sich vielleicht, warum die Zeilen 13 bis 16 nötig sind, wo wir doch die Variablen schon zuvor deklariert haben. Der Grund: Im vorhergehenden Codestück erzeugten wir Variablen, gaben ihnen Namen und teilten der CLR mit, um welche Variablentypen es sich handelt. Wir haben jedoch diesen Variablen keinerlei Werte zugewiesen. Das erledigen die Zeilen 13 bis 16: Sie weisen den Variablen Objekte zu (wie durch die Gleichheitszeichen angezeigt wird). Das Erzeugen eines Objekts – sei es ein Steuerelement, ein Formular oder ein anderes Objekt – ist ein zweistufiger Vorgang: Deklaration der Variablen und Zuweisung eines Wertes. Es ist jedoch möglich, beide Schritte in einer einzigen Zeile auszuführen: Dim btnAdd as Button = New Button

Oder sogar noch einfacher: Dim btnAdd as New Button

Wir werden ab jetzt diese Kurzform verwenden. Die Zeilen 18, 19, 21 und 25 machen alle das Gleiche: Sie legen die Positionen (»Locations«) für die Steuerelemente in unserem Windows Form fest. Die Eigenschaft Location übernimmt einen einzelnen Parameter des Typs Point, der die Stelle beschreibt, wo das Steuerelement platziert werden soll. Der Konstruktor für Point übernimmt wiederum zwei Parameter (eine x- und eine y-Koordinate). Man kann zwar Steuerelemente übereinander legen, aber das ist meist nicht sonderlich nützlich. Wir gehen morgen auf dieses Thema ein. Sie sollten bereits wissen, was Zeile 22 bewirkt: Sie setzt die Text-Eigenschaft der Schaltfläche auf den Wert »Addieren«. Erinnern Sie sich, dass die Text-Eigenschaft des Formulars eine Titelzeile festlegt. Die gleiche Eigenschaft für einen Button legt den Text fest, der darauf angezeigt wird. Zeile 23 bringt etwas Neues. Was sie genau bedeutet, erfahren wir an Tag 5. Für den Augenblick genügt es zu wissen, dass diese Zeile der CLR mitteilt, dass die Add-Methode (siehe Listing 2.9) ausgeführt werden soll, wenn diese Schaltfläche angeklickt wird.

70

Eine vollständige Anwendung

Die Zeilen 27-30 schließlich fügen Ihrem Formular die einzelnen Steuerelemente hinzu. Dieser Schritt ist notwendig, sonst würde die CLR nicht wissen, wo sie die Steuerelemente anzeigen soll. Sie würden nur im Speicher existieren und wertvolle Ressourcen belegen. Listing 2.9 zeigt Ihnen den Schlussteil unseres Codes: die Add-Methode. Fügen Sie diesen Code unter Zeile 31 ein, jedoch vor dem Ende der Klasse. Listing 2.9: Die Zahlen addieren 33: 34: 35:

Public Sub Add(ByVal Sender as Object, ByVal e as EventArgs) lblAnswer.Text = CStr(CInt(tbNumber1.Text) + CInt(tbNumber2.Text)) End Sub

Diese Methode nimmt zwei Parameter entgegen, die wir aber für den Augenblick ignorieren, da sie für diese einfache Anwendung nicht von großem Nutzen sind. Zeile 34 übernimmt die Werte der zwei Textfelder (unter Nutzung der jeweiligen Text-Eigenschaft), addiert sie und zeigt sie im Bezeichnungsfeld (Label) mit Hilfe von dessen Text-Eigenschaft an. Die Funktionen CStr und CInt wandeln die Benutzereingaben in Zeichenfolgen bzw. Ganzzahlen um. Das ist notwendig, weil die CLR nicht weiß, wie sie Benutzereingabewerte interpretieren soll. Beispielsweise könnte das Zeichen 9 als Zahl oder Zeichenfolge interpretiert werden, also müssen wir einen expliziten Datentyp dafür bereitstellen. Zeile 34 führt die folgenden Arbeitsschritte aus: 1. holt die Werte aus den zwei Textfeldern, 2. wandelt die Werte mit Hilfe von CInt in Integer um, 3. addiert die zwei Integer, 4. wandelt das Ergebnis in eine Zeichenfolge um, die im Bezeichnungsfeld angezeigt wird (dieser Schritt ist erforderlich, weil die Text-Eigenschaft stets eine Zeichenfolge erwartet und sich gegenüber einer Ganzzahl verweigert). Das war's auch schon. Speichern Sie den zusammengefügten Code als calculator.vb in Ihrem Verzeichnis c:\winforms\day2 und kompilieren Sie ihn unter Verwendung von VS .NET oder eines Befehlszeilen-Compilers: vbc /t:winexe /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll Calculator.vb

Dieser Befehl verwendet den Compiler von VB .NET, liefert eine ausführbare WindowsDatei und referenziert die Assemblies system.dll, system.windows.forms.dll sowie system.drawing.dll (diese wurde mit Hilfe der Tabelle 2.1 bestimmt), um die Anwendung zu kompilieren. Abbildung 2.7 zeigt eine Beispielausgabe.

71

Windows Forms-Anwendungen erstellen

Listing 2.10 zeigt den gleichen Quellcode, nur eben in C#.

Abbildung 2.7: Die Anwendung addiert erfolgreich zwei Zahlen.

Listing 2.10: Ein C#-Taschenrechner 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:

72

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.Day2 { public class Calculator : Form { private Button btnAdd; private TextBox tbNumber1; pribate TextBox tbNumber2; private Label lblAnswer; public static void Main() { Application.Run(new Calculator()); } public Calculator() { this.btnAdd = new Button(); this.tbNumber1 = new TextBox(); this.tbNumber2 = new TextBox(); this.lblAnswer = new Label(); tbNumber1.Location = new Point(0,0); tbNumber2.Location = new Point(100,0); btnAdd.Location = new Point(0,25);

Eine vollständige Anwendung

27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43:

btnAdd.Text = "Addieren"; btnAdd.Click += new EventHandler(this.Add); lblAnswer.Location = new Point(0,75); this.Controls.Add(btnAdd); this.Controls.Add(tbNumber1); this.Controls.Add(tbNumber2); this.Controls.Add(lblAnswer); } public void Add(object Sender, EventArgs e) { lblAnswer.Text = Convert.ToString(Convert.ToInt32 (tbNumber1.Text) + Convert.ToInt32(tbNumber2.Text)); } } }

Neben ein paar semantischen Unterschieden zum VB .NET-Code sollten Sie auch über andere Dinge Bescheid wissen. Alle Zeilen sind bis zur Zeile 28 miteinander identisch. Beachten Sie, dass Sie die AddHandler-Methode nicht mehr verwenden und das Schlüsselwort WithEvents aus Zeile 8 verschwunden ist. C# setzt eine andere Methode ein, um Objekte Aktionen ausführen zu lassen. Darauf kommen wir an Tag 5 zu sprechen. Andere Unterschiede treten nur noch in den Zeilen 39 und 40 auf. Statt die Funktionen CStr und CInt für die Datentypumwandlung zu verwenden, nutzen Sie nun die Methoden Convert.ToString und Convert.ToInteger. Wenn Sie mit C# vertraut sind, fragen Sie sich vielleicht, warum wir nicht den Casting-Operator für die Konvertierung von Datentypen verwendet haben, beispielsweise (string) und (int) vor den jeweiligen Variablennamen: (int)tbNumber1.Text usw. Der Grund liegt in dem Umstand, dass man in C# weder implizit noch explizit einen string in einen anderen Basisdatentyp konvertieren kann. Die Klasse System.Convert verfügt hingegen über alle nötigen Methoden, um jeden beliebigen vordefinierten Datentyp in einen anderen umzuwandeln (ToInt32, ToString, ToDouble usw.). Der Befehl zum Kompilieren ist der gleiche, nur dass Sie jetzt den C#-Compiler nutzen: csc /t:winexe /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll Calculator.cs

73

Windows Forms-Anwendungen erstellen

2.4

Zusammenfassung

Die heutige Lektion war sehr intensiv. Durch das Untersuchen eines einfachen Quellcodes haben Sie eine ganze Menge über das .NET Framework und seine Arbeitsweise gelernt, u.a. über Klassen, Namensräume, Assemblies und Methoden. Klassen sind Baupläne für Objekte. In einer Klasse definieren Sie die Eigenschaften, Methoden und Ereignisse, die ein Objektbenutzer verwenden kann. Diese offen gelegten – also für die Allgemeinheit bereitgestellten – Elemente werden mit Hilfe des Schlüsselworts public deklariert. Elemente, die Sie nicht offen legen möchten, erfordern das Schlüsselwort private. Sie haben auch etwas über Klassenvererbung gelernt. Eine Klasse kann von einer anderen erben, um so deren Funktionsumfang zu nutzen – das erspart Ihnen viel Codeschreiben. Alle Ihre Windows Forms-Klassen erben von der Klasse System.Windows.Forms.Form. Klassen verfügen über Konstruktoren und Destruktoren, also über Methoden, die die von einer Klasse verwendeten Ressourcen initialisieren und wieder freigeben. In C# hat der Konstruktor den gleichen Namen wie die Klasse; der Destruktor erhält den gleichen Namen, allerdings mit einer vorangestellten Tilde. VB .NET hingegen verfügt über keine Destruktoren und der Konstruktor trägt immer den Namen New. Namensräume sind Gruppierungen von Klassen, und Assemblies sind physische Dateien, die Namensräume gruppieren. Namen von Assemblies enden auf .dll und lassen sich von Anwendungen mit Hilfe der Option /r des Compilers referenzieren. Die Main-Methode ist stets der Anfang einer Anwendung. Sie muss sich in einer Klasse befinden, aber es ist gleichgültig, in welcher. In der Regel ruft diese Methode die Methode Application.Run auf, welche ein Formular am Bildschirm anzeigt und auf Benutzereingaben wartet. Zum Schluss haben Sie gelernt, wie man alles zusammenfügt, indem Sie eine funktionierende Windows Forms-Anwendung erstellten. Mit diesem Wissen können Sie nun beinahe jeden Bestandteil einer Windows Forms-Anwendung identifizieren und Ihre eigene Anwendung erstellen.

2.5 F

Bedeutet der Import von Namensräumen zusätzlichen Verwaltungsaufwand und somit Speicherbedarf für meine Anwendung? A

74

Fragen und Antworten

Nicht unbedingt. Die Objekte in importierten Namensräumen werden nur bei Bedarf geladen und nicht alle auf einmal. Daher benötigt die Verwendung externer Objekte zwar schon etwas (Speicher-) Ressourcen, aber nur in recht geringem Umfang.

Workshop

2.6

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wie heißt die Basisklasse, von der alle anderen Klassen erben? 2. Was bewirken die Schlüsselwörter shared und static? 3. Wahr oder falsch? Man kann vorhandene Namensräume um eigene Klassen erweitern. 4. Wahr oder falsch? Die Main-Methode wird jedes Mal ausgeführt, wenn eine neue Instanz ihrer Klasse erzeugt wird. 5. Aus welchem Grund ist der folgende Code nicht ausreichend? Was muss außerdem verwendet werden? dim MyObject as Button

6. Was stimmt mit dem folgenden Kompilierungsbefehl nicht? (Tipp: Er kann mehr als einen Fehler enthalten.) csc /type:windowsexe /r:system.dll /r:system.drawing.dll / r:system.drawing.text.dll dateiname.vb

7. Was bedeuten die folgenden Eigenschaften? Button.CanSelect Button.Cursor Form.AllowDrop Form.Visible

8. Nennen Sie drei semantische Unterschiede zwischen C# und Visual Basic .NET. 9. Wahr oder falsch? Jede Klasse muss einen Konstruktor haben.

Übung Erweitern Sie das heutige Taschenrechner-Beispiel. Fügen Sie weitere Schaltflächen hinzu, mit denen sich arithmetische Operationen ausführen lassen: Subtraktion, Multiplikation und Division. Versuchen Sie, die Anwendung sowohl in C# als auch in Visual Basic .NET zu erstellen. Verschönern Sie die Benutzeroberfläche Ihres Taschenrechners durch den großzügigen Gebrauch von Bezeichnungsfeldern (Label-Steuerelementen).

75

Mit Windows Forms arbeiten

3

Mit Windows Forms arbeiten

An den ersten zwei Tagen haben Sie eine ganze Menge über die Funktionsweise von Windows Forms-Anwendungen gelernt. Sie können jeden ihrer Bestandteile identifizieren und beschreiben, wissen, wie er mit .NET zusammenarbeitet, und Sie können auf die Verwendung derjenigen Teile schließen, die Ihnen noch unbekannt sind. Da Sie nun über das Grundlagenwissen verfügen, um Windows Forms zu erstellen, ist es an der Zeit, mehr ins Detail zu gehen. Die heutige Lektion konzentriert sich auf alle Aspekte des Objekts System.Windows.Forms.Form und zeigt Ihnen die Vielfalt an Möglichkeiten, mit denen Sie Ihre Anwendungen anpassen können. Heute lernen Sie, 쐽

wie man das Object-Objekt verwendet,



in welcher Weise Sie Layout und Aussehen eines Formulars steuern,



wie man Interaktion in Formularen kontrolliert,



was die Nachrichtenschleife ist und wie sie mit der CLR zusammenarbeitet,



wie Sie mit Tastatur- und Mauseingaben umgehen,



auf welche Weise man Formulare veranlasst, mit Drag & Drop zu arbeiten.

3.1

Das objektorientierte Windows-Formular

Nun sollten Sie langsam anfangen, sich Ihre Formulare als Objekte vorzustellen. Stellen Sie sich das Windows -Formular auf Ihrem Bildschirm als ein echtes Fenster in Ihrem Haus vor. Sie können die Farbe des Fensters (bzw. seines Rahmens) und seine Transparenz ändern, den Typ der Handgriffe und des Glases sowie die Vorhänge usw. Sie können praktisch alles, was Ihnen einfällt, mit Ihrem Fenster anstellen (vorausgesetzt, Sie verfügen über die entsprechenden Mittel). Das Gleiche gilt auch für Windows Forms. Aus der gestrigen Lektion wissen Sie, dass Sie die Titelzeile und die Größe eines Formulars ändern können. Das trifft auch auf die Titelleiste zu wie auch auf die Anordnung der Objekte im Formular, die Farbe, Transparenz und Sichtbarkeit, die Umrisslinien, die Schaltflächen usw. In Abbildung 3.1 sehen Sie nur ein paar der Layouts, die Ihr Formular annehmen kann. Daher ist es hilfreich, sich Windows Forms als generische Objekte vorzustellen, die sich in jeder gewünschten Weise bearbeiten und anpassen lassen. Sie können quasi in den »Laden« der .NET-Klassenbibliothek gehen und sich ein Form-Objekt heraussuchen. Dann schneiden Sie es sich nach Ihren Wünschen zu.

78

Das objektorientierte Windows-Formular

Abbildung 3.1: Formulare können transparent, rahmenlos und skalierbar sein.

Mit dem Objekt Object arbeiten An den Tagen 1 und 2 haben Sie mit ein paar Objekten gearbeitet, aber Sie haben noch nicht das Objekt Object untersucht, die Basisklasse aller .NET-Klassen einschließlich der Windows Forms-Formulare. Da es von jeder Klasse geerbt wird, werden Sie von jetzt ab damit umgehen, und so ist es ratsam, ein wenig mehr darüber zu erfahren, insbesondere über die Methoden, die es Ihnen zur Verfügung stellt. Object ist die allgemeinste Klasse, die Sie in .NET verwenden können. Daher wird sie verwendet, um jedes ansonsten unbekannte Objekt darzustellen. Wenn Sie beispielsweise folgendes Statement zum Erzeugen einer Variablen verwenden, weiß die CLR nicht, als welchen Typ sie sie erzeugen soll, und erzeugt lediglich ein Object: Dim MyObjectVariable

Weil Ihre Variable nur ein Objekt ist, verlieren Sie eine Menge Funktionalität, die Sie durch das Deklarieren eines expliziten Datentyps gewinnen würden. So könnten Sie etwa einem Integer einen weiteren Integer hinzufügen, aber Sie können keine Objects aneinander hängen; ungeachtet Ihrer guten Absicht würde dies zu einer Fehlermeldung führen. Daher empfiehlt es sich, stets einen spezifischen Typ anzugeben, statt ein Object als Mädchen für alles zu verwenden. Standardmäßig müssen Ihre Anwendungen stets einen Typ für Ihre Variablen deklarieren. Das verhindert den falschen Einsatz der Variablen und verbessert das Leistungsverhalten. Sie können aber diese Sicherheitsfunktion durch Deaktivieren des Merkmals Option Strict abschalten. Um Näheres darüber zu erfahren, lesen Sie bitte in Tag 2 den Abschnitt »Das Kompilieren von Windows Forms«.

79

Mit Windows Forms arbeiten

Sie werden in der Windows Forms-Programmierung viele Funktionen kennen lernen, die für Sie Objekte erzeugen. So ruft etwa die Methode ConfigurationSettings.GetConfig Konfigurationseinstellungen aus einer Datei ab. Die Funktion liefert einen Datentyp Object, weil sie nicht genau weiß, was Sie mit den zurückgegebenen Ergebnissen machen werden. Wird aber ein Object geliefert, können Sie die Ergebnisse durch Typumwandlung (Casting) in jeden gewünschten Datentyp transformieren. In C# wandeln Sie den Typ einer Variablen folgendermaßen um: myDataType myVariable; // Variable vom Typ myDataType deklarieren myVariable = (myDataType) ConfigurationSettings.GetConfig ("some setting"); // Einstellungen abrufen und in myDataType umwandeln

Das Objekt Object verfügt über fünf Hauptmethoden: Equals, ReferenceEquals, GetHashCode, GetType und ToString. Sie sind allesamt nützlich, daher wollen wir sie im Detail betrachten. Denken Sie daran, dass jedes Objekt von Object erbt, so dass diese Methoden jedem Objekt, das Sie verwenden, zur Verfügung stehen. Die Equals-Methode ist sowohl ein statisches als auch ein nicht-statisches Element. Sie können sie daher sowohl von einer bestimmten Objektinstanz aus als auch vom Typ Object selbst aus aufrufen. Listing 3.1 zeigt beispielsweise ein Codefragment, in dem sowohl statische als auch nicht-statische Versionen der Equals-Methode verwendet werden. Listing 3.1: Der Gebrauch der Equals-Methode 1: 2: 3: 4: 5: 6: 7: 8:

int intA; int intB; intA = 4; intB = 5; Console.WriteLine("{0}", intA.Equals(intB)); // nicht-statische Methode Console.WriteLine("{0}", ObjectEquals(intA, intB)); // statische Methode

Die Zeilen 7 und 9 geben beide »false« in der Befehlszeile aus, weil die Objekte intA und intB nicht gleich sind (intA ist 4, und intB ist 5). Zeile 7 ruft jedoch die Equals-Methode von einer Instanz aus auf, während Zeile 9 Equals von der Object-Klasse selbst aus aufruft. Sehr häufig werden Sie zwei Objekte miteinander vergleichen müssen, so dass sich diese Methode als nützlich erweisen dürfte. Die Console.WriteLine-Methode gibt eine Zeichenfolge in der Befehlszeile aus. Der erste Parameter besteht in der auszugebenden Zeichenfolge. Eine Zahl in Klammern bedeutet, dass die Methode auf einen der zusätzlich angegebenen Parameter verweisen sollte: {0} bedeutet also »Gib den nächsten Parameter aus«, {1} bedeutet »Gib den darauf folgenden Parameter aus« usw. Man könnte anstelle der numerischen Bezeichner auch einfach den Parameter selbst verwenden.

80

Das objektorientierte Windows-Formular

Um diese Methode verwenden zu können, müssen Sie Ihre Anwendung als reguläre ausführbare Datei kompilieren (verwenden Sie dabei die Option /t:exe). Die Methode ReferenceEquals ist ein nur-statisches Element, das der Equals-Methode ähnelt. Sie unterscheiden sich dahingehend, dass ReferenceEquals zwei Objekte miteinander vergleicht, um herauszufinden, ob sie sich in der gleichen Instanz befinden, also nicht, ob sie den gleichen Wert besitzen. Das lässt sich deswegen bewerkstelligen, weil in Computerbegriffen eine Instanz durch ihren Ort im Speicher (ihre Speicheradresse) definiert ist. Wenn also zwei Objekte auf dieselbe Speicheradresse verweisen, handelt es sich um dieselbe Instanz. Zwei verschiedene Speicheradressen können den gleichen Wert haben, aber es handelt sich nicht um dieselbe Instanz. (Beachten Sie, dass null – oder Nothing in VB .NET – stets mit derselben Instanz wie ein anderes null gleichzusetzen ist. Werfen Sie einen Blick auf Listing 3.2, das unterschiedliche Variablen miteinander vergleicht. Listing 3.2: Referenzen vergleichen 1: 2: 3: 4: 5: 6: 7: 8:

object a = null; object b = null; object c = new Object(); Console.WriteLine(Object.ReferenceEquals(a, b)); Console.WriteLine(Object.ReferenceEquals(a, c)); a = c; Console.WriteLine(Object.ReferenceEquals(a, c));

Zeile 5 liefert true, weil sich alle null-Werte auf dieselbe Instanz beziehen und einander sind. Zeile 6 hingegen gibt false bzw. unwahr zurück, weil a und c zwei unterschiedliche Objekte sind. Zeile 7 sieht aus, als würde sie a und c demselben Wert zuordnen, aber wenn Sie die eine Variable einer anderen zuweisen, setzen Sie eigentlich die Speicheradressen miteinander gleich, und daher liefert Zeile 8 true oder wahr. Falls Sie bereits über Programmiererfahrung verfügen, wissen Sie, woher der Name ReferenceEquals kommt: Diese Methode vergleicht die Speicheradressen (bzw. Referenzen) der Variablen statt ihre Werte miteinander. In den meisten Fällen genügt die Equals-Methode, aber es ist hilfreich, auch über die ReferenceEquals-Methode verfügen zu können. Die nächsten drei Methoden sind alle nicht-statisch. GetHashCode liefert einen Hash-Code für Ihr Objekt. Ein Hash-Code ist eine numerische Darstellung eines Objekts und hat darüber hinaus keine Bedeutung an sich. Die Objekte A und B können unterschiedliche

81

Mit Windows Forms arbeiten

Objekte sein, aber den gleichen Hash-Code generieren, wie etwa in Zeile 7. Sie können diesen Vorgang nicht umkehren und von der Zahl 7 auf ein Objekt schließen; Hashing ist ein Einbahnstraßen-Mechanismus. Wie ihr Name schon sagt, liefert die Methode GetType einfach nur den spezifischen Datentyp eines Objekts. Das ist sehr nützlich, wenn man nicht weiß, mit welcher Art von Daten man es zu tun hat, so etwa, wenn eine Funktion ein nicht identifizierbares Objekt liefert: string a = "hallo"; Console.WriteLine(a.GetType());

Dieses Codestück wird System.String liefern, den Datentyp der Variablen a. Beachten Sie, dass diese Methode einen Datentyp zurückgibt, nicht etwa nur den Namen des Typs: Sie liefert einen Typ und keine Zeichenfolge. Diese Methode wird häufig zum Vergleichen von Datentypen verwendet, etwa im Zusammenhang mit der eben besprochenen Methode GetConfig: myType myVariable; if (myVariable.GetType() != ConfigurationSettings.GetConfig ("mySettings").GetType()) return false;

Dieser Code prüft, ob der von der GetConfig-Methode gelieferte Typ (ein Type-Objekt) der gleiche Typ ist wie myVariable. Und in diesem Fall ist er es nicht. Zum Schluss kommen wir auf die gebräuchlichste Methode zu sprechen: ToString liefert die Zeichenfolgendarstellung eines Variablentyps. Das folgende Codefragment würde beispielsweise die Zeichenfolge "system.object" in der Befehlszeile ausgeben: Object objA = new Object(); Console.WriteLine(objA.ToString());

Beachten Sie, dass hier nicht der Wert eines Objekts ausgegeben wird, sondern der Typname. Manchmal wird die ToString-Methode für eine bestimmte Klasse überschrieben. Der string-Datentyp überschreibt beispielsweise ToString, so dass er den tatsächlichen Wert der Variablen ausgibt statt den Namen des Typs. Der folgende Code gibt die Zeichenfolge "hello world" aus: string strA = "hello world"; Console.WriteLine(strA.ToString());

Formular-Eigenschaften Das Objekt System.Windows.Forms.Form verfügt über genau 101 Eigenschaften, die es Ihnen gestatten, beinahe jeden Aspekt des Formulars zu steuern. Da dies zu viele Eigenschaften für nur einen Tag sind, lassen Sie uns nur auf ein paar der nützlicheren Aspekte einen Blick werfen, gruppiert nach Funktion.

82

Das objektorientierte Windows-Formular

Größe und Position steuern Ihnen sind bereits zwei Möglichkeiten, die Formulargröße festzulegen, bekannt: die Eigenschaften Width und Height (Breite bzw. Höhe). Diese Eigenschaften übernehmen lediglich Zahlenwerte, um die Formulargröße in Pixelwerten festzulegen. Zum Beispiel: form1.Width form1.Height = 200

Sie können die Größe auch mit Hilfe der Size-Eigenschaft und eines Size-Objekts festlegen: form1.Size = New Size(100, Form1.Size.Height) form1.Size = New Size(100, Form1.Size.Width)

Beide Methoden bewirken das Gleiche, aber meistens werden Sie die erste verwenden, da sie einfacher ist. Sie können die Größe auch abhängig von der Bildschirmgröße des Benutzers festlegen, und zwar, indem Sie das Screen-Objekt verwenden. Listing 3.3 zeigt dafür ein Beispiel. Listing 3.3: Das Festlegen der Formularhöhe abhängig von der Bildschirmgröße (in VB .NET) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

Imports System Imports System.Windows.Forms Public Class MultiForm : Inherits Form Public Sub New Me.Text = "Hauptformular" Me.Height = Screen.GetWorkingArea(Me).Height / 2 End Sub End Class Public Class StartForm Public Shared Sub Main() Application.Run(New MultiForm) End Sub End Class

Dieser Quellcode dürfte Ihnen mittlerweile vertraut sein, daher betrachten wir lediglich Zeile 7 etwas genauer. Die Methode GetWorkingArea des ScreenObjekts liefert ein Rectangle-Objekt, das für den Benutzerbildschirm steht. Sie übergeben das aktuelle Formularobjekt (mit Hilfe des Me-Schlüsselworts), um der CLR mitzuteilen, welchen Bildschirm sie verwenden soll (nur für den Fall, dass der Benutzer mehr als einen Bildschirm besitzt). Rectangle-Objekte haben wiederum Height- und Width-Eigenschaften, welche ihrerseits Integer liefern, die die

83

Mit Windows Forms arbeiten

Höhe und Breite des Bildschirms beschreiben. Danach teilen wir den Höhe-Wert durch 2. Kompilieren Sie diese Anwendung und führen Sie sie aus. Beachten Sie, dass das Fenster jetzt die halbe Höhe Ihres Bildschirms beansprucht. Um festzulegen, wo das Fenster sich öffnet, können Sie die Eigenschaften Location, DesktopLocation, Top oder Left verwenden. Die letzten beiden legen die Position der oberen linken Ecke Ihres Formulars auf dem Bildschirm fest. Sie funktionieren genauso wie die Height- und Width-Eigenschaften. Beachten Sie, dass Sie diese Eigenschaften auf eine Position festlegen können, die sich weit außerhalb des Bildschirms befindet, etwa auf Top = 9999 und Left = 9999. Die Eigenschaften Location und DesktopLocation sind einander ähnlich. Bei einem FormObjekt bewirken sie das Gleiche: Sie legen die Anfangsposition der oberen linken Ecke des Formulars fest. Die beiden folgenden Zeilen realisieren beispielsweise die gleiche Funktionalität: Form1.DesktopLocation = New Point(100,300) Form1.Location = New Point(100,300) Location ist eine Eigenschaft, die von Control, der Urgroßelternklasse von Form, geerbt

wird. Diese Eigenschaft verwendet man, um die Position eines Steuerelements innerhalb eines anderen Steuerelements festzulegen. In unserem Fall ist das beinhaltende Steuerelement für das Formular einfach der Benutzerbildschirm. Sie könnten die Location-Eigenschaft für jedes beliebige Objekt verwenden, das vom Control-Objekt erbt, etwa für die Steuerelemente TextBox oder Label (Beschriftung), um seine Position innerhalb eines anderen Objekts zu festzulegen – zum Beispiel Ihres Formulars. Die Eigenschaft DesktopLocation gilt hingegen nur für das Form-Objekt. Der Einheitlichkeit halber werden wir ab jetzt die Eigenschaft Location verwenden. Daher brauchen Sie nur eine Eigenschaft für alle Windows Forms-Objekte zu verwenden.

Das Aussehen steuern Sie wissen bereits über die Text-Eigenschaft Bescheid, die den in der Titelzeile angezeigten Text steuert. Lassen Sie uns daher ein paar Dinge betrachten, die Sie mit Schriftarten (Fonts) anstellen können. Die Eigenschaft Font legt die Schriftart fest, die im Formular benutzt wird, es sei denn, die Font-Eigenschaft eines Steuerelements hätte Priorität. Die Eigenschaft ForeColor legt die Textfarbe fest. Betrachten Sie das folgende Codefragment: Form1.Font = New Font(new FontFamily("WingDings"), 23) Form1.ForeColor = Color.Blue

Die erste Zeile erzeugt ein neues Font-Objekt (Sie merken schon, dass absolut alles in .NET ein Objekt ist, selbst Schriftarten und Farben). Es gibt eine ganze Reihe unterschiedlicher

84

Das objektorientierte Windows-Formular

Möglichkeiten, um Font-Objekte zu erzeugen (in anderen Worten: Es hat viele Konstruktoren), und dies ist lediglich eine davon. Sie legen die Schriftart fest, indem Sie das Objekt FontFamily verwenden, das vordefinierte Schriftartnamen enthält. Der zweite Parameter besteht in der Größe der Schriftart (in der Maßeinheit »Punkt«). Die zweite Zeile legt die Textfarbe mit Hilfe der Blue-Eigenschaft des Color-Objekts als blau fest. Wird dieser Code in einem Windows Forms-Formular verwendet, erzeugt er die Ausgabe, die Sie in Abbildung 3.2 sehen (Sie müssen mir glauben, dass die Farbe wirklich blau ist).

Abbildung 3.2: Aussehen und Farbe einer Schriftart werden in ihren jeweiligen Eigenschaften festgelegt.

Die Eigenschaften BackColor und BackgroundImage erlauben Ihnen, das vorgegebene Erscheinungsbild eines Formulars zu ändern. BackColor wird genauso verwendet wie ForeColor: Form1.BackColor = Color.Lachsrosa

Die Eigenschaft BackgroundImage übernimmt ein Image-Objekt als Parameter. Üblicherweise verwenden Sie die FromFile-Methode des Image-Objekts, um eine Grafik zu laden; Sie müssen einen Pfadnamen angeben, z.B. so: Form1.BackgroundImage = Image.FromFile("c:\winforms\day3\kaffeebohne.bmp") FromFile ist eine statische Methode, wie Sie wahrscheinlich schon herausgefunden haben, da Sie ja keine neue Image-Instanz erzeugen müssen, um sie verwenden zu können. Wenn wir von Abbildung 3.2 ausgehen, erhalten wir nun einen gekachelten Hintergrund, wie man in Abbildung 3.3 sehen kann.

Die Hintergrundfarbe des Label-Objekts (Bezeichnungsfeld) ist immer noch grau. An Tag 6 lernen Sie, wie Sie dies ändern können. Eine weitere Bildeigenschaft, die Sie anpassen können, ist die Icon-Eigenschaft. Das Icon oder Symbol wird in der oberen linken Ecke der Titelzeile Ihres Formulars verwendet, ebenso wie in jeder anderen Darstellung Ihrer Anwendung, wie etwa der Windows-Taskleiste. So legen Sie die Eigenschaft fest: Me.Icon = New Icon("c:\winforms\day3\VSProjectApplication.ico")

85

Mit Windows Forms arbeiten

Abbildung 3.3: Sie können eine Grafik dazu verwenden, den Hintergrund Ihres Formulars damit zu kacheln.

Sie müssen eine neue Instanz des Icon-Objekts erzeugen, wobei Sie einen gültigen Pfad zu einer Grafik angeben. Die Grafik, die Sie auswählen, muss eine Icon-Datei (.ico) sein, sonst erzeugt Ihre Anwendung eine Fehlermeldung. Das Bild des Mauszeigers lässt sich durch die Cursor-Eigenschaft steuern: Me.Cursor = Cursors.Hand

Das Cursors-Objekt verfügt über eine Menge Eigenschaften für den Standardsatz an Windows-Mauszeigern, darunter Arrow, IBeam, WaitCursor und Help. In der .NET-Dokumentation finden Sie weitere Mauszeiger. Sie können auch einen eigenen Mauszeiger aus einer Datei laden: Me.Cursor = New Cursor("Pfadname")

Die Mauszeigerdatei muss die Endung .cur tragen. Animierte Mauszeiger (jene mit der Endung .ani) werden von der CLR nicht unterstützt. Die Eigenschaft ShownInTaskBar legt fest, ob Ihre Anwendung in der Windows-Taskleiste zu sehen sein soll (dies betrifft nicht das Fenster am Bildschirm, sondern nur die Schaltfläche in der Taskleiste). Der Standardwert lautet true (wahr). Unter bestimmten Gegebenheiten möchten Sie diese Einstellung vielleicht ändern, um einen Anwender daran zu hindern, ein Formular auszuwählen. Wenn Sie etwa einen so genannten »Splash Screen« erzeugen, der etwa das Logo Ihrer Firma anzeigt, wollen Sie wahrscheinlich nicht, dass der Benutzer es auswählt (es verschwindet ja auch sofort). Setzen Sie dann einfach die Eigenschaft ShownInTaskBar auf false, und der Benutzer kann es nicht mehr aus der Taskleiste auswählen. Die Eigenschaft FormBorderStyle legt das Aussehen der Umrisslinie einer Windows FormsFormulars fest. Hauptsächlich ändern Sie diese Eigenschaft, um die Größenänderung des Formulars zu erlauben oder zu unterbinden. Manchmal betrifft das Ändern einer Umrisslinie auch das Aussehen des Formulars, wie zum Beispiel: Form1.FormBorderStyle = FormBorderStyle.Sizable

86

Das objektorientierte Windows-Formular

Die Aufzählung für FormBorderStyle (nicht zu verwechseln mit der Eigenschaft Form.FormBorderStyle) führt sieben verschiedene Typen von Umrisslinien auf, wie in Tabelle 3.1 zu sehen. (Eine Aufzählung ist einfach eine Sammlung von Eigenschaften oder Stilen.) Abbildung 3.4 zeigt eine Sammlung der unterschiedlichen Stile.

Abbildung 3.4: Es stehen sieben vordefinierte Umrisslinienstile zur Auswahl.

Stil

Beschreibung

Fixed3D

Nicht skalierbar. Dreidimensionale Umrisslinie.

FixedDialog

Nicht skalierbar. Dicke Umrisslinie.

FixedSingle

Nicht skalierbar. Dünne Umrisslinie.

FixedToolWindow

Formular mit schmalerer Titelleiste. Nützlich für die Anzeige von QuickInfos und Hilfefenstern. Nicht skalierbar. Enthält keine Maximieren- oder Minimieren-Schaltflächen.

None

Nicht skalierbar. Keine Umrisslinie.

Sizable

Skalierbar. Standardstil.

SizableToolWindow

Formular mit schmalerer Titelleiste. Nützlich für die Anzeige von QuickInfos und Hilfefenstern. Skalierbar. Enthält keine Maximieren- oder MinimierenSchaltflächen.

Tabelle 3.1: Stile für FormBorderStyle

87

Mit Windows Forms arbeiten

Zum Schluss gibt es noch drei Eigenschaften, die steuern, wie und ob Ihr Formular am Bildschirm angezeigt wird. Die Visible-Eigenschaft legt fest, ob Ihr Formular für den Benutzer sichtbar ist. Ist ein Formular nicht zu sehen, kann man nicht damit interagieren. Auf diese Weise lassen sich bestimmte Dinge vor dem Anwender verbergen, etwa wenn Sie Ihre Anwendung geöffnet lassen wollen, sie aber nicht mit der Benutzeroberfläche interferieren soll. Standardmäßig ist ein solches Formular nicht sichtbar. Um ein Formular teilweise sichtbar zu machen (mit anderen Worten: transparent), müssen Sie die Eigenschaft Visible auf true (wahr) setzen und die Eigenschaft Opacity (Undurchsichtigkeit) verwenden. Diese Eigenschaft übernimmt einen Wert zwischen 1 (= völlig undurchsichtig, also völlig sichtbar) und 0,0 (= unsichtbar bzw. völlig durchsichtig). Um ein paar wirklich interessante Transparenztechniken vorzuführen, können Sie die Eigenschaft TransparencyKey so einstellen, dass eine bestimmte Farbe durchsichtig sein soll. Der folgende Code macht beispielsweise alle grauen Flächen im Formular unsichtbar, während alles andere undurchsichtig bleibt: Form1.TransparencyKey = Color.Gray

Wenn Sie nun auch noch die BackColor des Formulars auf Grau setzen, erhalten Sie schließlich ein Formular wie das in Abbildung 3.5 zu sehende (das Formular wurde über ein Fenster mit Befehlszeilenausgabe gelegt, um den Transparenzeffekt zu verdeutlichen).

Abbildung 3.5: Verwenden Sie die Eigenschaft TransparencyKey, um nur eine bestimmte Farbe sichtbar zu machen.

Interaktivität steuern Wenn Sie eine Windows-Anwendung betrachten, dann sehen Sie an deren Fenstern die Standardmerkmale: In der oberen rechten Ecke befinden sich Schaltflächen für das Minimieren und Maximieren sowie das Schließen des Fensters, manchmal auch eine HILFESchaltfläche. Rechts unten befindet sich oft ein Anfasser, damit man mit der Maus die Fenstergröße verändern kann. Diese Systemmenüfelder sehen Sie in Abbildung 3.6.

88

Das objektorientierte Windows-Formular

Jedes Systemmenüfeld lässt sich mit Hilfe der folgenden Eigenschaften in Ihrem Formular verstecken oder anzeigen: 쐽

MaximizeBox



MinimizeBox



HelpButton



ControlBox



SizeGripStyle

Abbildung 3.6: Diese Systemmenüfelder gehören zu den Standardmerkmalen von Windows.

Die ersten vier Eigenschaften nehmen lediglich den Wert true oder false an. ControlBox legt fest, ob die vorhergehenden Schaltflächen überhaupt angezeigt werden sollen. Seien Sie hier jedoch vorsichtig: Wenn Sie ControlBox auf false setzen, könnte es sein, dass Sie Ihre Anwendung nicht mehr schließen können!

Die HILFE-Schaltfläche taucht nur auf, wenn die MINIMIEREN- und MAXIMIEREN-Schaltflächen nicht sichtbar sind. Dies ist ein Standardmerkmal von .NET.

Die Eigenschaft SizeGripStyle nimmt einen SizeGripStyle-Aufzählungswert an: Auto, Hide oder Show. Auto zeigt den Skalierungsanfasser an, falls nötig (also abhängig vom FormBorderStyle), während Hide und Show die Größenziehpunkge (Skalierungsanfasser) jeweils verbergen oder zeigen. Mit einer Anwendung sind häufig zwei besondere Tasten verbunden: (¢) und (ESC). Viele Anwendungen werden geschlossen, wenn Sie die (ESC)-Taste drücken. Sie können diese Funktionen mit den Eigenschaften AcceptButton und CancelButton steuern. Listing 3.4 zeigt in C# ein Beispiel, wie man diese Eigenschaften verwendet.

89

Mit Windows Forms arbeiten

Listing 3.4: Die Eigenschaften AcceptButton und CancelButton 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44:

90

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day3 { public class Listing34 : Form { Button btAccept = new Button(); Button btCancel = new Button(); Label lblMessage = new Label(); public Listing34() { lblMessage.Location = new Point(75,150); lblMessage.Width = 200; btAccept.Location = new Point(100,25); btAccept.Text = "OK"; btAccept.Click += new EventHandler(this.AcceptIt); btCancel.Location = new Point(100,100); btCancel.Text = "Abbrechen"; btCancel.Click += new EventHandler(this.CancelIt); this.AcceptButton = btAccept; this.CancelButton = btCancel; this.Text = "Beispiel für die Buttons OK und Abbrechen"; this.Height = 200; this.Controls.Add(lblMessage); this.Controls.Add(btAccept); this.Controls.Add(btCancel); } public void AcceptIt(Object Sender, EventArgs e) { lblMessage.Text = "Der OK-Button wurde gedrückt"; } public void CancelIt(Object Sender, EventArgs e) { lblMessage.Text = "Der Abbrechen-Button wurde gedrückt"; } } public class StartForm { public static void Main() { Application.Run(new Listing34());

Das objektorientierte Windows-Formular

45: 46: 47: 48:

} } }

Ein Großteil dieses Codes dürfte bereits vertraut aussehen, daher können wir das meiste davon überfliegen. Die Zeilen 1 bis 3 importieren die notwendigen Namensräume. Zeile 5 deklariert den Namensraum, zu dem diese Anwendung gehört; gemäß unserem Bezeichnungsschema heißt dieser Namensraum TYWinForms.Day3. Zeile 7 deklariert die Windows Forms-Formularklasse. Die Zeilen 8 bis 10 deklarieren die Steuerelemente, die wir im Formular verwenden werden. Beachten Sie, dass sie außerhalb einer Methode deklariert werden, doch innerhalb einer Klasse. Dadurch lassen sie sich von jeder Methode in der Klasse verwenden. Im Konstruktor ab Zeile 12 findet der Großteil des Geschehens statt. Der Code in den Zeilen 13 bis 22 setzt einfach ein paar Eigenschaften für unsere Steuerelemente. Beachten Sie insbesondere die Zeilen 18 und 22, die auf Methoden zeigen, die ausgeführt werden, sobald eine der beiden Schaltflächen im Formular angeklickt wird. Diese Methoden, AcceptIt und CancelIt, befinden sich in den Zeilen 33 und 37 und geben einfach nur eine Meldung im Label-Steuerelement aus. Die Zeilen 24 bis 30 stellen einige Eigenschaften ein und fügen dem Formular die Steuerelemente hinzu. Die Zeilen 24 und 25 legen die Eigenschaften AcceptButton und CancelButton für die jeweilige OK- oder Abbrechen-Schaltfläche fest. Im Wesentlichen bewirkt das Anklicken der OK- oder Abbrechen-Schaltfläche das Gleiche wie das Drücken der (¢)- oder der (ESC)-Taste. Schließlich enthalten die Zeilen 42 bis 46 eine weitere Klasse. Sie dient lediglich dazu, unsere Main-Methode aufzunehmen und die Methode Application.Run aufzurufen. Die Ausgabe der Anwendung nach dem Drücken der (ESC)-Taste wird in der Schaltfläche AcceptButton und Abbildung 3.7 gezeigt. Erhält die Abbrechen-Schaltfläche im Formular den Fokus, veranlasst das Drücken der (¢)-Taste das »Anklicken« oder Niederdrücken dieser Schaltfläche und folglich das Auslösen der CancelIt-Methode. Mit anderen Worten: Die Schaltfläche erhält die Eingabe noch vor dem eigentlichen Formular. Leider gibt es für diese missliche Tatsache keinen einfachen Ausweg. Manche Steuerelemente, wie etwa die Button- und RichTextBox-Steuerelemente, behandeln das Drücken der (¢)-Taste automatisch, wenn sie den Fokus haben, also noch bevor irgendetwas anderes ausgeführt werden kann. Sobald Sie dem Abbrechen-Button den Fokus geben (indem Sie ihn anklicken oder mit der Tabulatortaste darauf gehen), bewirkt die (¢)-Taste stets die Ausführung der CancelIt-Methode.

91

Mit Windows Forms arbeiten

Falls es Sie interessiert, können Sie diese Verhaltensweise umgehen, indem Sie das Button-Steuerelement selbst überschreiben. Wir werden dies an Tag 18 behandeln. Obwohl die AcceptButton- und CancelButton-Eigenschaften auf Button-Steuerelemente verweisen müssen, beachten Sie bitte, dass diese Buttons nicht unbedingt für den Benutzer zu sehen sein müssen. Indem man die Visible-Eigenschaft auf false setzt, macht man die Schaltflächen unsichtbar, ohne ihre Funktionalität zu verlieren. Die Eigenschaft AllowDrop schließlich legt fest, ob das Formular mit Drag & Drop-Funktionen umgehen kann, d.h. wenn ein Anwender mit der Maus ein Objekt in das Formular zieht und dort »fallen« lässt. Diese Eigenschaft kann einen true- oder false-Wert annehmen. Wie Sie Ihr Formular dazu bringen, etwas bei einem solchen Ereignis auszulösen, besprechen wir heute später im Abschnitt »Ereignisse im Formular«.

Abbildung 3.7: Das Drücken der (ESC)-Taste hat die gleiche Wirkung wie das Anklicken der ABBRECHEN-Schaltfläche.

Um eine vollständige Liste der Eigenschaften des Form-Objekts nachzuschlagen, lesen Sie Anhang B »Windows Forms-Steuerelemente«.

Formular-Methoden Das Form-Objekt verfügt ebenfalls über zahlreiche Methoden: über 57, um genau zu sein. In der Mehrzahl sind sie von den Klassen Object und Control geerbt, so dass sie den meisten Objekten, mit denen Sie in Windows Forms arbeiten, zur Verfügung stehen. Eingeteilt nach Kategorien sollen einige davon hier vorgestellt werden. Neben den hier behandelten Methoden sollten Sie auch die bereits vorgestellten ObjectMethoden wie Equals und ToString nicht vergessen. Das Form-Objekt kann auch sie nutzen.

Aspekte der Bildschirmanzeige Die ersten zwei Methoden sind Show und Hide. Diese beiden Funktionen machen Ihr Formular jeweils sichtbar oder unsichtbar, indem sie die Visible-Eigenschaft ändern. Sie verändern das Formular selbst nicht – etwa das Entfernen aus dem Speicher oder der Aufruf anderer Funktionen –, sondern nur, ob der Benutzer es sehen kann.

92

Das objektorientierte Windows-Formular

Die Close-Methode jedoch entsorgt ein Formular (und seine Steuerelemente) vollständig durch das Entfernen aus dem Speicher. Setzen Sie diese Methode ein, wenn Sie Ihre Anwendung schließen wollen oder wenn Sie einfach kein Formular mehr brauchen. Da Windows ja ein Multitasking-Betriebssystem ist, dürfen viele Fenster gleichzeitig geöffnet sein. Jedes Fenster muss um die Aufmerksamkeit des Benutzers wetteifern. Das FormObjekt verfügt über einige Methoden, die Ihnen helfen, diese Schwierigkeit zu bewältigen. Die Activate-Methode »aktiviert« Ihr Formular. Das kann zweierlei bedeuten: 쐽

Wenn Ihre Anwendung die aktive ist, also vom Benutzer gerade verwendet wird, stellt Activate das Formular in den Vordergrund der Bildschirmanzeige, so dass es über allen anderen Fenstern liegt.



Falls es nicht die aktive Anwendung ist, blinken die Titelleiste und das Symbol in der Taskleiste, um die Aufmerksamkeit des Benutzers auf sich zu lenken. Wahrscheinlich haben Sie diese Art von Blickfang schon einmal gesehen; die gebräuchlichste Verwendung findet sich bei Instant-Messaging-Anwendungen. Hat Ihnen jemand eine Instant Message (IM) gesendet, während Sie gerade in einer anderen Anwendung arbeiten, öffnet sich plötzlich das IM-Fenster im Hintergrund und seine Titelleiste blinkt.

Beim Blickfang noch etwas direkter als Activate sind die Methoden BringToFront und SendToBack. Die erste Methode stellt ein Formular vor alle anderen Fenster in der Bildschirmanzeige, so dass der Benutzer gezwungen ist, es anzusehen. Das ist nützlich, falls etwas mit Ihrer Anwendung geschieht, das die volle Aufmerksamkeit des Benutzers verlangt (beispielsweise der Erhalt einer Instant Message). Umgekehrt stellt SendToBack ein Formular hinter alle anderen Fenster auf dem Bildschirm. SendToBack wird zwar nicht häufig benutzt, ist aber für alle Fälle vorhanden. Sie können die Eigenschaft TopMost auf true setzen, um dafür zu sorgen, dass Ihr Formular immer im Vordergrund steht. Besonders nützlich ist dieses Leistungsmerkmal, wenn Alarm- oder Fehlermeldungen ausgegeben werden sollen. Schließlich funktioniert die Refresh-Methode ähnlich wie die Schaltfläche AKTUALISIEREN in Ihrem Web-Browser: Sie zeichnet alles im Formular neu und aktualisiert mitunter sogar dessen Inhalte. Wir werden näher auf diese Methode zu sprechen kommen, wenn wir GDI+ an Tag 13 durchnehmen.

Eigenschaften zurücksetzen Das Form-Objekt verfügt über eine Reihe von Reset-Methoden, mit denen Sie geänderte Eigenschaften auf ihre Vorgabewerte zurücksetzen können. Alle diese Methoden folgen dem Bezeichnungsschema ResetEigenschaft. Einige der gebräuchlicheren sind: 쐽

ResetBackColor



ResetCursor

93

Mit Windows Forms arbeiten



ResetFont



ResetForeColor



ResetText

Diese Methoden sind sehr nützlich, wenn man etwas geändert hat und alles wieder rückgängig machen muss, man aber nicht weiß oder sich nicht darum kümmert, welches jeweils der ursprüngliche Wert war. Wenn der Benutzer beispielsweise in einem Textprogramm mehrmals die Schriftart ändert, aber dann wieder zu den Standardwerten zurückkehren möchte, könnte man ResetFont einsetzen.

3.2

Ereignisbehandlung

Ein Ereignis (event) ist etwas, das als Ergebnis einer Aktion oder Aktivität passiert. Greifen Sie auf die Auto-Analogie zurück und stellen Sie sich bitte vor, dass Sie auf die Bremse treten (eine Aktion). Der Wagen hält an (ein Ereignis). Treten Sie auf das Gaspedal (Aktion), fährt der Wagen wieder an (Ereignis). Ein Ereignis ist stets die Folge einer Aktion, die stattgefunden hat. Auch Windows Forms verfügen über Ereignisse, obwohl so manches davon nicht besonders augenfällig ist. Öffnen und Schließen etwa sind zwei Ereignisse, die erfolgen, wenn Sie Ihre Anwendung starten beziehungsweise beenden. Schieben Sie Ihren Mauszeiger über das Formular, findet ein Ereignis statt, und wenn Ihr Mauszeiger es wieder verlässt, findet ein weiteres Ereignis statt. Ohne Ereignisse wären Windows Forms ziemlich fade, denn sie würden nie etwas bewirken, ganz gleich, wie sehr der Anwender das auch erzwingen wollte.

Die Nachrichtenschleife Ereignisse eignen sich in wunderbarer Weise dafür, Benutzereingaben zu handhaben. Lassen Sie uns zwei verschiedene Anwendungsmodelle betrachten – eines mit, das andere ohne Ereignisse –, bevor wir untersuchen, wie Anwendungen Ereignisse verwenden. Stellen Sie sich zunächst die ereignisgesteuerte Anwendung vor. Unter »ereignisgesteuert« versteht man, dass die Anwendung auf Ereignisse reagiert, die auf Benutzeraktionen hin erfolgen. Ohne diese Ereignisse würde die Anwendung in der Tat überhaupt nichts tun. Die Ereignisse treiben die Anwendung quasi an. In diesem Modell ist die Anwendung untätig und wartet darauf, dass etwas passiert. Sie verwendet Ereignisse als ihr Stichwort, eine Aktion auszuführen. Ein Benutzer drückt beispielsweise eine Taste. Die Anwendung merkt, dass ein Ereignis stattgefunden hat

94

Ereignisbehandlung

(Tastendruck), führt eine Aktion aus, um das entsprechende Zeichen anzuzeigen, und wartet dann auf das nächste Ereignis. Eine Anwendung, die nicht ereignisgesteuert ist, gestattet dem Benutzer nicht die gewünschte Nutzung der Anwendung, vielmehr kann er lediglich auf ihre Aufforderungen reagieren. Mit einer ereignisgesteuerten Anwendung hingegen kann der Benutzer interagieren, ganz gleich, mit welchem ihrer Teile, zu welcher Zeit oder in welcher Reihenfolge. Stellen Sie sich einmal eine Taschenrechner-Anwendung vor, die nicht ereignisgesteuert wäre. Sobald Sie die Anwendung gestartet haben, holt sie zwei Werte aus den Textfeldern, führt mathematische Berechnungen aus und gibt das Ergebnis aus. Befinden sich jedoch keine Werte in den Textfeldern, tut der Taschenrechner überhaupt nichts. Er kann nicht feststellen, ob sich eine Zahl geändert hat, da er keine Ereignisse erkennt. Jedes Mal, wenn Sie die Zahlen ändern, um einen anderen Wert zu berechnen, müssen Sie zuerst die Zahlen ändern und dann die Anwendung erneut ausführen. Klassische Active Server Pages (also ASP als Vorgänger von ASP.NET) funktionieren auf diese Weise. Eine ASP-Anwendung wird ausgeführt, sobald ein Benutzer die Seite von seinem Browser aus anfordert, dann gibt sie den nötigen HTML-Code aus und hält an. Um das, was danach passiert, kümmern sich ASPs nicht, da sie nicht auf Ereignisse zu warten brauchen. Damit eine ASP-Anwendung Benutzereingaben verarbeitet, muss der Benutzer alle Eingaben in ein Webformular eintippen, bevor er es der ASP zur Verarbeitung zuschickt. Die ASP kennt nichts anderes als das, was ihr beim Start bekannt gegeben worden ist. Abbildung 3.8 illustriert diesen Vorgang. Nicht ereignisbasiert Anwendung starten

Benutzereingaben liefern

Verarbeitung

Stop

Abbildung 3.8: Anwendungen, die nicht ereignisgesteuert sind, erfordern einen dreistufigen Prozessablauf.

Beide Modelle haben ihre Vor- und Nachteile. Sobald sie einmal gestartet sind, laufen Anwendungen, die nicht ereignisgesteuert sind, ohne Benutzereingaben ab. Ereignisgesteuerte Anwendungen erfordern hingegen üblicherweise eine Benutzereingabe, sind aber häufig reaktionsfähiger und interaktiv. Da aber Interaktion eine zwingende Eigenschaft einer jeden Windows-basierten Anwendung ist, werden alle Ihre Programme das ereignisgesteuerte Modell verwenden.

95

Mit Windows Forms arbeiten

Wie also entdeckt eine Anwendung, dass ein Ereignis stattgefunden hat? Sobald eine typische Windows-Anwendung gestartet wird, tritt sie in eine Nachrichtenschleife ein. Dies ist lediglich eine Zeitspanne, in der die Anwendung auf eine Eingabe wartet oder auf Mitteilungen vom Benutzer (einen Befehl etwa). Die Zeitspanne dauert so lange, bis die Anwendung beendet wird, so dass dies als eine Schleife (oder Zyklus) bezeichnet wird. Während dieser Schleife tut die Anwendung nichts, außer auf eine Benutzereingabe zu warten (die Spanne der Nicht-Aktivität wird Leerlauf bzw. idling genannt). Sobald eine Eingabe entgegengenommen wird, verrichtet die Anwendung ihr Werk und fällt dann wieder zurück in ihre Nachrichtenschleife. Dieser Zyklus setzt sich wieder und wieder fort, bis die Anwendung geschlossen wird. Wenn Sie etwas eingeben, ist das Windows-Betriebssystem die erste Stufe in der Verarbeitung. Windows entscheidet darüber, zu welcher Anwendung das Ereignis gehört, und schickt es ihr zu. Diese Nachrichten sind als »Windows-Nachrichten« bekannt, wovon die Bezeichnung »Nachrichtenschleife« abgeleitet wurde. Ereignisbasiert Anwendung starten

in Messageschleife eintreten

Verarbeitung

Benutzereingabe

Falls Benutzereingabe auf “Stop” lautet, dann

Stop

Abbildung 3.9: Die Nachrichtenschleife wartet auf eine Benutzereingabe.

Wenn Sie beispielsweise ein Microsoft Word-Dokument das erste Mal öffnen, passiert nichts. Word wartet auf Ihre Eingabe. Sobald Sie eine Taste drücken, findet ein Ereignis statt, eine Methode wird ausgeführt (um das Zeichen am Bildschirm anzuzeigen), dann fällt Word wieder in die Nachrichtenschleife zurück, um weiteren Input abzuwarten. Jedes Mal wenn Sie eine Taste drücken, hält die Nachrichtenschleife für einen Augenblick an, um etwas zu verarbeiten, und setzt dann das Warten fort. Abbildung 3.9 illustriert diesen Zyklus. Windows Forms sind üblicherweise die primäre Benutzeroberfläche Ihrer Anwendungen. Daher handhaben sie eine ganze Reihe unterschiedlicher Ereignisse.

96

Ereignisbehandlung

Form-Ereignisse Sie haben bereits gesehen, wie sich einige Ereignisse handhaben lassen. Im Beispiel des Taschenrechners aus der gestrigen Lektion nutzten Sie das Click-Ereignis des ButtonObjekts. Die Handhabung von Ereignissen in Windows Forms ist immer ziemlich die gleiche, ungeachtet der Objekte, denen die Ereignisse zugeordnet sind. An Tag 5 besprechen wir Ereignisse eingehender, wie man sie handhabt und welche Unterschiede zwischen verschiedenen Ereignissen bestehen. Nun lassen Sie uns aber fürs Erste einen Blick auf einige der 71 Ereignisse des Form-Objekts werfen.

Die Ausführung steuern Um Ihnen als Entwickler die größtmögliche Kontrolle über Ihre Anwendung zu verleihen, werden manche Ereignisse sowohl vor als auch nach dem Eintreten einer Aktion gestartet. Wenn Sie beispielsweise Ihr Formular schließen, findet das Closing-Ereignis direkt vor dem Schließen des Formulars statt, das Closed-Ereignis direkt danach. Diese Arten von Ereignisnamen folgen stets den Regeln für das Partizip Präsens bzw. Partizip Perfekt, daher »Closing« (»sich gerade schließend«) und »Closed« (also »geschlossen«). Nicht alle Ereignisse treten in solchen Paaren auf. Wenn Sie jedoch einen –ing-Ereignisnamen lesen, können Sie sicher sein, dass auch ein –ed-Ereignis folgt; das gilt aber nicht umgekehrt. Was ist der Grund für die Methode, zwei Ereignisse pro Aktion zu verwenden? Stellen Sie sich bitte einen großen Gemüseladen vor, der um 20:00 Uhr schließt. Fünf Minuten, bevor der Laden eigentlich zumacht, wird über die Lautsprecheranlage eine Ansage zu hören sein, die die Kunden bittet, dass sie sich zu den Kassen begeben möchten. Dieses Vor-Schließen-Ereignis dient dazu, die Kunden zu warnen, dass etwas geschehen wird. Um 20:00 Uhr schließt der Laden, und alle Kunden werden zum Verlassen des Ladens aufgefordert, alle Türen werden verriegelt. Diese zweistufige Vorgehensweise erlaubt es sowohl dem Ladeninhaber oder Filialleiter als auch den Kunden, sich auf das tatsächliche eintretende Ereignis des Ladenschlusses vorzubereiten. Bei Windows-Anwendungen verhält es sich ähnlich. Wenn z.B. ein Benutzer eine große Zahl von Änderungen an einem Word-Dokument vornimmt, dann die Anwendung mit Hilfe des SCHLIEßEN-Steuerelements schließt, aber das Speichern seines Dokuments vergisst, was würde dann geschehen? Wenn man auf das Closed-Ereignis warten würde, bevor man etwas unternähme, wäre es bereits zu spät: Das Dokument wäre schon geschlossen worden, und alle Änderungen wären verloren. In .NET jedoch wird das Closing-Ereignis ausgelöst, bevor das eigentliche Schließen des Dokuments eintritt. In diesem Fall können Sie nun den Benutzer auffordern, sein Dokument zu speichern, bevor die Änderungen verloren gehen, und dadurch die Änderungen zu erhalten. Sobald das Fenster geschlossen ist,

97

Mit Windows Forms arbeiten

wird das Closed-Ereignis ausgelöst. Danach können Sie jede Verarbeitungsmöglichkeit nutzen, die Sie benötigen (etwa dem Benutzer eine Meldung anzeigen). Lassen Sie uns einen Blick auf ein Beispiel werfen, in dem dieser zweistufige Vorgang verwendet wird. Listing 3.5 verwendet die Closing- und Closed-Ereignisse im Form-Objekt, um das obige Beispiel zu illustrieren. Listing 3.5: Die Kontrolle der Programmausführung mit Hilfe von Ereignissen in VB .NET 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:

Imports Imports Imports Imports

System System.Windows.Forms System.Drawing System.ComponentModel

Namespace TYWinForms.Day3 public class Listing35 : Inherits Form public sub New() Me.Text = "Beispiel für ein Ereignis" AddHandler Me.Closing, AddressOf Me.ClosingMsg AddHandler Me.Closed, AddressOf Me.ClosedMsg end sub public sub ClosingMsg(Sender as Object, e as CancelEventArgs) Microsoft.VisualBasic.MsgBox("Formular schließt sich") end sub public sub ClosedMsg(Sender as Object, e as EventArgs) Microsoft.VisualBasic.MsgBox("Formular ist geschlossen") end sub end class public class StartForm public shared sub Main() Application.Run(new Listing35) end sub end class End Namespace

In Zeile 4 wird ein Namensraum importiert, der noch nicht aufgetreten ist: System.ComponentModel. Er verfügt über Objekte, die Ereignisse betreffen, die man weiter unten im Code benötigt. Die Zeilen 6 bis 10 sind nichts Besonderes. Die

98

Ereignisbehandlung

Zeilen 11 und 12 verwenden die AddHandler-Methode (die Ihnen an den Tagen 1 und 2 begegnet ist), um der CLR zu befehlen, die Methoden ClosingMsg und ClosedMsg (in den Zeilen 15 und 19) auszuführen, wenn die Closing- und Closed-Ereignisse jeweils ausgelöst werden. Wir springen in Zeile 15 zur Closing-Methode, die ausgeführt wird, sobald das Closing-Ereignis ausgelöst worden ist und sich das Formular tatsächlich schließt. Sehen Sie sich zunächst die Signatur (die erste Zeile) dieser Methode an. Sie übernimmt zwei Parameter: ein Object- und ein System.ComponentModel.CancelEventArgs-Objekt. Sie wissen bereits, was das Object-Objekt ist. Der zweite Parameter ist ein spezialisiertes Objekt, das nur für Ereignisse gilt, insbesondere für Closing-Ereignisse. Es verfügt über eine besondere Eigenschaft namens Cancel, um damit den Vorgang – in diesem Fall das Schließen des Formulars – zu unterbrechen, falls dies nötig sein sollte (wie etwa im Beispiel des Textprogramms). Wenn Sie festlegen, dass das Schließen gestoppt werden soll (etwa weil der Benutzer vergessen hat, sein Dokument zu speichern), setzen Sie die Cancel-Eigenschaft auf true: e.Cancel = true

Die Anwendung wird mit dem Schließen innehalten und in die Nachrichtenschleife zurückkehren. In unserem Fallbeispiel befassen wir uns nicht damit zu verhindern, dass das Formular geschlossen wird. In Zeile 16 rufen wir vielmehr eine Methode auf, die eine Meldung auf dem Bildschirm anzeigt. Die MsgBox-Methode des Namensraums Microsoft.VisualBasic präsentiert lediglich ein Popup-Feld mit dem festgelegten Text darin. (Beachten Sie, dass wir anstatt den vollen Namen der MsgBox-Methode in Zeile 16 zu verwenden, auch den Namensraum Microsoft.VisualBasic mit Hilfe von Imports hätten importieren können.) In Zeile 19 beginnt die Methode ClosedMsg. Sie wird ausgeführt, nachdem das Formular sich geschlossen hat. Beachten Sie, dass sie als zweiten Parameter ein EventArgs-Objekt statt CancelEventArgs übernimmt. Den Grund dafür besprechen wir an Tag 5. Diese Methode wiederum ruft die MsgBox-Funktion auf, um eine weitere Meldung anzuzeigen, die den Benutzer darüber informiert, dass das Formular geschlossen wurde. Kompilieren Sie zum Schluss diesen Code, entweder mit VS .NET oder mit folgendem Befehl: vbc /t:winexe /r:system.windows.forms.dll /r:system.drawing.dll listing.3.5.vb

Sie erinnern sich bestimmt noch von Tag 2 daran, dass sich der Namensraum System.ComponentModel in der Assembly System.dll befindet. Das ist der Grund, warum wir hier keine

99

Mit Windows Forms arbeiten

neuen Assemblies referenzieren müssen. Abbildung 3.10 zeigt die Ausgabe, nachdem das Steuerelement zum Schließen des Fensters angeklickt worden ist.

Abbildung 3.10: Das Closing-Ereignis erlaubt Ihnen, das ClosedEreignis abzufangen.

Nicht alle Ereignisse folgen diesem zweistufigen Ablauf. Man findet ihn aber bei einigen Form-Objekten, die Tabelle 3.2 beschreibt. Ereignis

Beschreibung

Closed

Erfolgt beim Schließen eines Formulars

InputLanguage- Erfolgt, sobald der Benutzer die Sprache des Formulars zu ändern versucht Changed Validated

Erfolgt, sobald die Eingabe eines Steuerelements auf Gültigkeit geprüft worden ist

Tabelle 3.2: Ereignisse, die vor dem Cursor-Einsatz stattfinden

Es gibt einige wichtige Ereignisse, von deren Existenz Sie Kenntnis haben sollten, wenn Sie mit Windows Forms zu tun haben. Closed haben Sie ja bereits kennen gelernt: Es wird ausgelöst, sobald sich ein Formular schließt. Es gibt weiter ein Load-Ereignis, das sofort ausgelöst wird, noch bevor ein Formular zum ersten Mal angezeigt wird. Der Ereignishandler für dieses Ereignis ist häufig eine geeignete Stelle, um Komponenten für Ihr Formular zu initialisieren, d.h. falls Sie sie noch nicht im Konstruktor initialisiert haben sollten. Das Activated-Ereignis findet statt, sobald Ihr Formular den Fokus erhält und zur aktiven Anwendung wird – es korrespondiert mit der Activate-Methode. Dementsprechend wird

100

Ereignisbehandlung

Deactivate ausgelöst, sobald Ihr Formular deaktiviert wird, also den Fokus verliert, oder wenn eine andere Anwendung die aktive wird.

Maus- und Tastaturereignisse Maus- und Tastaturaktionen gehören zu den wichtigsten Arten von Ereignissen. Schließlich sind dies wesentliche Formen von Benutzereingaben. Daher gibt es eine ganze Reihe von Ereignissen, die mit diesen zwei Eingabegeräten verbunden sind. Lassen Sie uns mit Tastaturereignissen anfangen. Zuerst an der Reihe ist das KeyPress-Ereignis. Es erfolgt jedes Mal, wenn eine Taste gedrückt wird, ganz gleich welche (wir sehen gleich noch, wie man feststellt, welche Taste es war). Falls dieses Ereignis nicht genügend Kontrollmöglichkeiten eröffnet, gibt es noch die Ereignisse KeyDown und KeyUp, die jeweils ausgelöst werden, sobald eine Taste heruntergedrückt und dann wieder losgelassen wird. Diese Ereignisse liefern zusätzliche Informationen (etwa welche Taste gedrückt wurde), die Sie in Ihrer Anwendung nutzen können. Ihre Ereignishandler, also diejenigen Methoden, die ausgeführt werden, sobald das Ereignis ausgelöst worden ist, sind dementsprechend spezialisiert. In VB .NET erfolgt das Deklarieren dieser Handler auf die gleiche Weise, mit der Sie bereits vertraut sind: 'VB .NET AddHandler Form1.KeyPress, AddressOf methodenName AddHandler Form1.KeyDown, AddressOf methodenName AddHandler Form1.KeyUp, AddressOf methodenName

In C# müssen Sie jedoch die spezialisierten Handler berücksichtigen. Statt EventHandler in Zeile 18 von Listing 3.4 wie gewohnt zu verwenden 18:

btAccept.Click += new EventHandler(this.AcceptIt);

müssen Sie die Objekte KeyPressEventHandler und KeyEventHandler einsetzen: //C# Form1.KeyPress += new KeyPressEventHandler(methodenName) Form1.KeyDown += new KeyEventHandler(methodenName) Form1.KeyUp += new KeyEventHandler(methodenName)

Genau wie das CancelEventArgs-Objekt besitzen auch die Objekte KeyPressEventHandler und KeyEventHandler besondere Eigenschaften, die Ihrer Anwendung helfen festzustellen, welche Aktion das jeweilige Ereignis ausgelöst hat. Der KeyPressEventHandler hat zwei Eigenschaften: Handled und KeyChar. Bei der ersten Methode handelt es sich lediglich um einen true- oder false-Wert, der anzeigt, ob Ihre Methode den Tastendruck verarbeitet hat (ist das nicht der Fall, wird der Tastendruck zur Verarbeitung an das Windows-Betriebssystem geschickt). Meistens werden Sie diese Eigen-

101

Mit Windows Forms arbeiten

schaft auf true setzen, es sei denn, Sie wünschen explizit, dass das Betriebssystem diesen speziellen Tastendruck verarbeitet (wenn Sie etwa die Taste (Druck) nicht handhaben müssen, leiten Sie dies an das Betriebssystem weiter). Die Eigenschaft KeyChar liefert einfach die Taste, die gedrückt wurde. Listing 3.6 zeigt ein Beispiel in VB .NET. Listing 3.6: Verarbeitung von Tastenbetätigungen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:

Imports Imports Imports Imports

System System.Windows.Forms System.Drawing System.ComponentModel

Namespace TYWinForms.Day3 public class Listing36 : Inherits Form public sub New() Me.Text = "Tastendruck-Beispiel" AddHandler Me.KeyPress, AddressOf Me.KeyPressed end sub public sub KeyPressed(Sender as Object, e as KeyPressEventArgs) Microsoft.VisualBasic.MsgBox(e.KeyChar) e.Handled = True end sub public shared sub Main() Application.Run(new Listing36) end sub end class End Namespace

In Zeile 11 stoßen Sie auf den Ereignishandler namens KeyPressed, welcher dem Ereignis KeyPress zugeordnet ist. In Zeile 14 deklarieren Sie den Ereignishandler. Beachten Sie bitte, dass er ein KeyPressEventArgs-Objekt als Parameter übernimmt – dieser Name korrespondiert mit dem KeyPressEventHandlerObjekt, das wir weiter oben besprochen haben. In Zeile 15 zeigen Sie einfach das gedrückte Zeichen in einem Meldungsfeld an und setzen dann in Zeile 16 die Handled-Eigenschaft auf true. Abbildung 3.11 zeigt die entsprechende Bildschirmausgabe, wenn das große A gedrückt wurde ((ª) + (a)). Beachten Sie jedoch, dass lediglich Buchstaben, Zahlen und die (¢)-Taste das KeyPressEreignis auslösen. Um andere Tasten wie etwa (Strg), (Alt) und die Funktionstasten (F1) bis (F12) zu handhaben, müssen Sie die KeyUp- und KeyDown-Ereignisse verwenden. Erin-

102

Ereignisbehandlung

nern Sie sich daran, dass diese Ereignisse Handler des Typs KeyEventhandler nutzen, so dass der zweite Parameter Ihrer Ereignishandler-Methode auf KeyEventArgs lauten muss: Public Sub KeyReleased(Sender as Object, e as KeyEventArgs) 'ein wenig Code end Sub

Abbildung 3.11: Solange Ihr Formular den Fokus besitzt, führt jeder Tastendruck die KeyPressed-Methode aus.

Das Objekt KeyEventArgs verfügt über mehrere Eigenschaften, die sich als nützlich erweisen, um festzustellen, welche Taste gedrückt wurde: 쐽

Alt: Wahr- oder Falsch-Wert, der anzeigt, ob die (Alt)-Taste gedrückt wurde



Control: zeigt an, ob die (Strg)-Taste gedrückt wurde



Handled: entspricht der Handled-Eigenschaft des Objekts KeyPressEventArgs



KeyCode: der Tastaturcode für die gedrückte Taste



KeyData: die Tastendaten für die gedrückte Taste



KeyValue: der Tastaturcode für die gedrückte Taste



Modifiers: gibt Flags zurück, die anzeigen, welche Tasten und Zusatztasten (wie etwa (ª), (Strg) oder (Alt)) gedrückt wurden



Shift: zeigt an, ob die (ª)-Taste gedrückt wurde

Jede Taste auf der Tastatur hat eindeutige KeyCode-, KeyData- und KeyValue-Werte. Die Eigenschaften KeyCode und KeyValue sind üblicherweise gleich. KeyData weist bei den meisten Tasten den gleichen Wert wie die beiden anderen Eigenschaften auf, ist jedoch unter-

103

Mit Windows Forms arbeiten

schiedlich bei Zusatztasten (eine vollständige Referenz finden Sie bei der Keys- bzw. Tasten-Aufzählung in der Dokumentation des .NET Frameworks). Mausereignisse finden in einer standardisierten Reihenfolge statt: 1. MouseEnter: sobald der Mauszeiger über das Formular gelangt 2. MouseMove: wenn der Mauszeiger sich über dem Formular bewegt 3. MouseHover: wenn der Mauszeiger einfach nur über dem Formular »schwebt« (d.h. ohne Bewegung oder Mausklick) 4. MouseDown: wenn man über dem Formular eine Maustaste drückt 5. MouseUp: wenn man die Maustaste wieder löst 6. MouseLeave: wenn der Mauszeiger das Formular wieder verlässt (sich vom Formular wegbewegt) Die Ereignisse MouseEnter, MouseHover und MouseLeave liefern keine besonderen Informationen und verwenden daher den standardmäßigen Ereignishandler EventHandler und die Ereignisparameter-Objekte EventArgs. Die Ereignisse MouseMove, MouseDown und MouseUp liefern hingegen spezifische Informationen und verwenden alle die MouseEventArgsObjekte des Ereignishandlers MouseEventHandler: public Sub MouseClick(Sender as Object, e as MouseEventHandler)

Das MouseEventhandler-Objekt liefert Information wie etwa die genaue Position des Mauszeigers auf dem Bildschirm, welche Maustaste gedrückt wurde usw. Die Eigenschaften sind in Tabelle 3.3 zusammengefasst. Eigenschaft

Beschreibung

Button

Holt die Information ein, welche Maustaste gedrückt wurde (MouseButtons.Left, MouseButtons.Middle, MouseButtons.None, MouseButtons.Right, MouseButtons.XButton1 oder MouseButtons.XButton2)

Clicks

Die Anzahl von Mausklicks (ein Integerwert)

Delta

Die Anzahl von Drehungseinheiten, um die sich das Mausrad bewegt hat

X

Die x-Bildschirmkoordinate des Mauszeigers

Y

Die y-Bildschirmkoordinate des Mauszeigers

Tabelle 3.3: Eigenschaften von MouseEventHandler

104

Ereignisbehandlung

Drag & Drop Das mit Windows eingeführte Merkmal des Drag & Drop, also des Klickziehens, erlaubt es dem Anwender, eine Art Abkürzung zu nehmen, indem er mit der Maus ein Symbol auf dem Bildschirm auf eine Anwendung zieht. Diese übernimmt die Datei, die vom Symbol dargestellt wird, und verarbeitet sie. Haben Sie beispielsweise Microsoft Word geöffnet und ziehen das Symbol eines Word-Dokuments darauf, öffnet Word dieses Dokument automatisch zur Bearbeitung. Klickziehen erlaubt Ihnen auch, nur mit der Maus Dateien aus einem Ordner in einen anderen zu verschieben oder zu kopieren. Es ist recht einfach, eine Windows Forms-Anwendung für Drag & Drop fit zu machen. Als Erstes müssen Sie die Eigenschaft DragDrop des Formulars auf true setzen und dann den Code für die Ereignisse DragDrop, DragEnter, DragLeave oder DragOver schreiben. Die letzten drei Ereignisse sind den ähnlich benannten Mausereignissen sehr ähnlich; sie treten auf, wenn ein Symbol in oder aus Ihrem bzw. über Ihr Formular bewegt wird. Alle diese Ereignisse verwenden den Objekt-Handler DragEventHandler und den Ereignisparameter DragEventArgs. Wir werfen einen Blick auf die Eigenschaften dieses Parameters. Die Eigenschaft AllowedEffects ist ein Indikator, der besagt, welche Drag & DropAktionen stattfinden können. Wenn Sie etwa versuchen, eine Datei, die nur gelesen werden darf, zu ziehen und abzulegen, können Sie diese Datei nur kopieren, nicht aber verschieben. Die Aktionen werden durch die DragDropEffects-Aufzählung angezeigt: DragDropEffects.All, DragDropEffects.Copy, DragDropEffects.Link, DragDropEffects.Move, DragDropEffects.None und DragDropEffects.Scroll. Alle diese Effekte entsprechen einfachen Windows-Funktionen. Die Eigenschaft DragEventsArgs wiederum gibt den sich gerade ereignenden Effekt an. Dieser ist einer der oben aufgeführten DragDropEffects-Werte. Wenn der Benutzer etwa eine Datei zieht, während er die (Strg)-Taste gedrückt hält, wird versucht, eine Kopieroperation durchzuführen, und die Effect-Eigenschaft wird DragDropEffects.Copy anzeigen. Die Data-Eigenschaft enthält das Element, das gerade Gegenstand einer Drag & DropOperation ist. Ungeachtet seines Typs wird dieses Element vom IdataObject-Objekt dargestellt, das zahlreiche unterschiedliche Objekttypen abbilden kann. (Nähere Informationen hierzu finden Sie in der Dokumentation zum .NET Framework.) Die Eigenschaft KeyState verrät Ihnen, ob jeweils die Taste (ª), (Strg) oder (Alt) gedrückt ist, genau wie die Alt-, Control- und Shift-Eigenschaften der KeyUp- und KeyDownEreignisse. Die X- und Y-Eigenschaften schließlich sind die gleichen wie jene für Mausereignisse. Sie zeigen an, an welcher Stelle (im Koordinatensystem) sich zum Zeitpunkt des Ereignisses ein Element befand.

105

Mit Windows Forms arbeiten

Änderungsereignisse Jedes Mal wenn sich eine der Form-Eigenschaften ändert, ist damit normalerweise ein Ereignis verbunden. Wenn man beispielsweise die Text-Eigenschaft des Formulars ändert, wird das TextChanged-Ereignis ausgelöst. Meist werden diese Ereignisarten für Validierungsroutinen verwendet. Wenn Sie etwa die Zahl der in Ihrem Formular verwendbaren Schriftarten begrenzen wollen, könnten Sie einen Handler für das FontChanged-Ereignis erzeugen, der ungültige Benutzerauswahlen überschreibt. Ich werde hier nicht alle diese Ereignisse aufführen. Sie können feststellen, welche das sind, indem Sie einfach das Wort »Changed« (geändert) an die jeweiligen Eigenschaften anhängen, über die Sie heute gelesen haben: TextChanged, CursorChanged, VisibleChanged usw. Alle diese Methoden verwenden Handler vom Typ EventHandler.

3.3

Zusammenfassung

Das Basisobjekt für alle anderen Objekte ist im .NET Framework – nomen est omen – das Object-Objekt. Da es so allgemeingültig gefasst ist, wird es an verschiedenen Stellen verwendet, an denen ein spezialisiertes Objekt entweder nicht gebraucht wird oder sich nicht genauer bestimmen lässt. Object verfügt über fünf Methoden – Equals, ReferenceEquals, GetHashCode, GetType und ToString –, die alle anderen .NET-Objekte erben. Das indirekt von Object abgeleitete Objekt System.Windows.Forms.Form stellt üblicherweise den Hauptbestandteil Ihrer Windows Forms-Anwendungen dar. Es stellt den Rahmen und Hintergrund für weitere Bestandteile der Benutzerschnittstelle bereit und enthält selbst eine reiche Palette an Funktionen. Das Form-Objekt besitzt 101 Eigenschaften, mit denen Sie praktisch jeden einzelnen Aspekt der Benutzeroberfläche steuern können. Sie können Ihr Formular mit Hilfe der Opacity-Eigenschaft transparent machen, es mit der Eigenschaft FormBorderStyle nichtskalierbar machen, und Sie können die Art und Weise der Benutzerinteraktion mit Hilfe der Eigenschaften MaximizeBox, MinimizeBox, HelpButton, SizeGripStyle und ControlBox kontrollieren. Form verfügt zudem über zahlreiche Methoden wie etwa BringToFront oder Focus, die Sie zur Steuerung der Bildschirmanzeige ausführen können.

Sie haben etwas über Ereignisse und die Nachrichtenschleife erfahren, die einer Anwendung das Untätigsein erlaubt, bis ein Benutzer eine Eingabe vornimmt. Das Form-Objekt verfügt über Ereignisse, die anlässlich zahlreicher Benutzeraktivitäten ausgelöst werden, darunter Mausklick, Tastendruck und Drag & Drop.

106

Fragen und Antworten

3.4 F

Fragen und Antworten

Kann ich von einem selbst erstellten Formular erben? Wie werden dabei Eigenschaften behandelt? A

Das ist zutreffend; Sie können von jeder Klasse erben, es sei denn, sie ist mit dem Schlüsselwort NonInheritable (in C# heißt es sealed) deklariert. Der Umgang mit geerbten Eigenschaften ist häufig kompliziert. Schauen Sie sich als Beispiel folgenden Code an: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:

Imports System Imports System.Windows.Forms Imports System.Drawing Public Class ClassA : Inherits Form private lblMessage as New Label Public Sub New() lblMessage.Location = new Point(100,100) lblMessage.Height = 100 lblMessage.Width = 200 lblMessage.Text = "Hello World!" Me.Controls.Add(lblMessage) End Sub End Class Public Class ClassB : Inherits ClassA private lblMessage as New Label Public Sub New() lblMessage.Location = new Point(100,100) lblMessage.Height = 100 lblMessage.Width = 200 Me.Controls.Add(lblMessage) End Sub End Class Public Class StartForm Public Shared Sub Main() Application.Run(New ClassB) End Sub End Class

Wenn Sie diese Anwendung kompilieren und ausführen, wird das von ClassB definierte Formular angezeigt. Beachten Sie, dass in dieser Klasse nirgends die Text-

107

Mit Windows Forms arbeiten

Eigenschaft des in Zeile 18 deklarierten Bezeichnungsfelds (Label) gesetzt wird. Wenn Sie jedoch die Anwendung ausführen, enthält das Bezeichnungsfeld den Text »Hello World!« Dieser Text wird in Zeile 12 in ClassA festgelegt. All dies passiert nicht deshalb, weil das Bezeichnungsfeld von ClassB von ClassA geerbt worden wäre, sondern vielmehr, weil im Konstruktor von ClassA ein Bezeichnungsfeld mit dem fraglichen Text erstellt und dem Formular hinzugefügt wird. Erinnern Sie sich daran, dass in jedem Konstruktor als Erstes ein Aufruf an den Konstruktor seiner Basisklasse erfolgt. Daher ist zur Ausführungszeit des Konstruktors von ClassB bereits ein Bezeichnungsfeld mit den Worten »Hello World!« vorhanden. Sie können jedoch von ClassB aus dieses Bezeichnungsfeld nicht bearbeiten. Es ist nämlich in Zeile 6 als private deklariert und daher nur für ClassA zugänglich. Würden Sie es in public ändern, erhielten Sie eine Fehlermeldung, denn ClassB hätte das Bezeichnungsfeld lblMessage geerbt und Zeile 18 würde versuchen, es zu redeklarieren – das ist leider keine gültige Operation. Unterm Strich lässt sich also sagen, dass Vorsicht beim Erben eigener Klassen angebracht ist. Achten Sie ebenfalls auf den Kontext der Sicherheit, in dem Variablen deklariert werden.

3.5

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz Die Fragen 1 bis 3 beziehen sich auf das folgende Codefragment: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

108

using System; using System.Windows.Forms; using System.Drawing; public class MyForm : Form { private Label lblMessage = new Label(); public MyForm() { this.Text = "Hello World!"; } }

Workshop

12: 13: 14: 15: 16: 17:

public class StartForm { public static void Main() { Application.Run(new MyForm()); } }

1. Was würde das folgende Statement liefern, wenn man es unter Zeile 9 platzieren würde? Console.Write(this.ToString());

2. Was würde der folgende Code liefern, wenn man ihn unter Zeile 9 platzieren würde? Label lblTemp = new Label(); Console.Write(lblTemp.Equals(this.lblMessage).ToString());

3. Was würde der folgende Code liefern, wenn man ihn unter Zeile 9 platzieren würde? Label lblTemp = new Label(); Console.Write(Object.ReferenceEquals(lblTemp, this.lblMessage).ToString());

4. Wahr oder falsch? Das KeyPress-Ereignis erfordert einen Handler vom Typ KeyEventHandler. 5. Nennen Sie die fünf Eigenschaften des Objekts MouseEventArgs. 6. Schreiben Sie ein einzelnes Statement in VB .NET, das die Breite eines Formulars auf ein Drittel der Bildschirmhöhe festlegt. 7. Welche Eigenschaft kontrolliert, welche Schaltfläche aktiviert wird, sobald der Benutzer die (ESC)-Taste gedrückt hat? 8. Welches ist der Standardwert von FormBorderStyle? 9. Welche drei Ereignisse verwenden das Paradigma, dass mit einer einzelnen Aktion zwei Ereignisse verknüpft sind?

Übungen 1. Erstellen Sie in C# eine Anwendung, die alle sechs Mausereignisse überwacht. Zeigen Sie eine Meldung in einem Textfeld an, wenn die einzelnen Ereignisse auftreten. 2. Erstellen Sie in VB .NET eine Anwendung, mit der die Benutzer die Eigenschaften Text, Height, Width und Opacity durch Werteingaben in eine TextBox und das Drücken einer EINGEBEN-Schaltfläche anpassen können.

109

Menüs und Symbolleisten

4

Menüs und Symbolleisten

Mittlerweile dürften Sie mit dem Form-Objekt und dem Erstellen von Windows FormsAnwendungen vertraut sein. Sie haben bereits gelernt, wie Sie die Benutzeroberfläche verändern, indem Sie einige Steuerelemente wie etwa Label oder TextBox einfügen sowie die Eigenschaften des Formulars verändern. Schauen wir uns einmal einige trickreichere Steuerelemente an. Menüs und Symbolleisten gehören zur Standardausstattung einer Benutzeroberfläche. Praktisch jede Windows-Anwendung weist sie auf, und die Benutzer wissen sie zu bedienen. Darüber hinaus verleihen sie Ihrer Anwendung ein vollständigeres und gepflegteres Aussehen. Dieses Kapitel zeigt, wie Sie diese gebräuchlichen UI-Elemente erstellen und dem Benutzer gestatten, sie zu bedienen. Heute lernen Sie, 쐽

was Windows Forms-Steuerelemente sind,



wie Sie Ihren Formularen interaktive Menüs hinzufügen,



wie Sie Ihre Anwendung kontextabhängig machen,



wie Sie Schaltflächen in eine Symbolleiste einfügen,



wie man Informationen in der Statusleiste anzeigt,



wie Sie Ihrer Anwendung Bildlaufleisten hinzufügen.

4.1

Einführung in Windows Forms-Steuerelemente

An den Tagen 1 bis 3 haben Sie mit einigen Steuerelementen gearbeitet, doch Sie haben eigentlich nicht untersucht, was man unter einem Steuerelement versteht. Lassen Sie uns einen Schritt zurücktreten und uns ansehen, was Steuerelemente (Controls) sind und warum und wozu man sie verwendet. Für den Benutzer ist ein Windows Forms-Steuerelement ein selbstständiger Bestandteil der Benutzeroberfläche. Schaltflächen, Menüs und Listenfelder sind allesamt Steuerelemente. Manche davon, wie etwa Schaltflächen, sind interaktiv, während andere, wie etwa LabelSteuerelemente (Bezeichnungsfelder), es nicht sind. Den Benutzer kümmert es nicht, wie ein Steuerelement genau funktioniert – er kann es intuitiv nutzen, um seine Aufgaben zu erledigen. Stellen Sie sich ein Steuerelement wie eine Uhr vor. Der Benutzer sieht nur das Äußere der Uhr und daher nur äußere Eigenschaften, wie etwa die Farbe oder die Uhrzeit. Der Benutzer kann die Standuhr verschieben und Dinge darauf platzieren, denn sie ist eine physische Entität, die sich manipulieren lässt.

112

Einführung in Windows Forms-Steuerelemente

Der Entwickler hingegen betrachtet die Uhr (das Steuerelement) ein wenig anders, da er in der Lage ist, sie zu öffnen und ihr Innenleben zu manipulieren: die Zeit einstellen, sie aufziehen, die Zahnräder verändern, das Pendel in Gang setzen usw. Die »Uhr« steht immer noch für einen Teil der Benutzeroberfläche, doch sie ist auch ein Gegenstand mit Eigenschaften, Methoden und Ereignissen. Ein Steuerelement lässt sich ebenso steuern wie das Form-Objekt. Jedes Steuerelement wird ebenfalls aus einer Klasse erzeugt, die genau jenen Klassen ähnelt, die Sie erstellt haben. Das Steuerelement erbt von anderen Klassen, die sich im .NET Framework befinden. Die Standuhr präsentiert Ihnen ebenso wie Windows Forms-Steuerelemente zwei »Gesichter« (vgl. Abbildung 4.1): ein äußeres für den Benutzer, ein tiefer reichendes inneres für den Entwickler. (Es gibt sogar noch tiefer reichende. Schließlich kann der Hersteller der Zahnräder Elemente kontrollieren, auf die der Entwickler keinen Zugriff hat.) Ihnen ist bekannt, dass Forms, Objects und Steuerelemente Objekte sind, die Sie mit Hilfe ihrer Methoden und Eigenschaften manipulieren können. Wir wollen einmal unser Lieblingsobjekt, Form, mit Formular-Steuerelementen vergleichen. Forms und Steuerelemente sind einander sehr ähnlich. Sie erben beide (direkt oder indirekt) von der Control-Klasse, demzufolge teilen sie zahlreiche Eigenschaften und Methoden miteinander, so etwa ToString, Equals und GetType. Da Sie das Form-Objekt zu

verwenden wissen, kennen Sie die Grundlagen für die Verwendung aller Steuerelemente. Auch Steuerelemente werden im Namensraum System.Windows.Forms gespeichert.

Entwickler kann mit Innenleben arbeiten

Benutzer sieht Äußeres

Abbildung 4.1: Ein Steuerelement stellt ein inneres und ein äußeres Gesicht zur Schau.

Der Unterschied zwischen einem Form und einem Steuerelement besteht darin, dass ein Steuerelement in einer anderen Komponente enthalten sein muss – das ist meistens, aber nicht zwingend ein Form. Steuerelemente bilden den Schwerpunkt der Interaktion des Benutzers, denn normalerweise wird der Hintergrund einer Anwendung (der leere FormBereich) einfach ignoriert. Werfen wir einen kurzen Blick auf die möglichen Typen von Steuerelementen (wir untersuchen sie detaillierter an Tag 6):

113

Menüs und Symbolleisten



Containersteuerelement e: Eine Reihe von Steuerelementen wurde exklusiv dafür entworfen, andere Steuerelemente aufzunehmen. Steuerelemente wie etwa das PanelSteuerelemente gruppieren Elemente nicht nur, um die Benutzeroberfläche aufzuräumen, sondern um dem Entwickler ihre Bearbeitung zu erleichtern. Fügen Sie etwa einem Panel weitere Steuerelemente hinzu, können Sie deren gemeinsame Eigenschaften mit einem einzigen Befehl ändern, statt für jedes Steuerelement einen Befehl auszugeben.



Schaltflächen-Steuerelemente: Sie haben bereits mit dem Steuerelement Button gearbeitet. Diese Kategorie umfasst weitere Steuerelemente wie etwa CheckBox und RadioButton, die zwar nicht wie Schaltflächen aussehen mögen, sich aber tatsächlich so verhalten. Eine CheckBox etwa, also ein ankreuzbares Kästchen wie jene in Web-Formularen, lässt sich anklicken, verfügt über eine zugeordnete Textmeldung und kann auf einen Klick hin eine Aktion ausführen – genau wie ein regulärer Button. Diese Steuerelemente gehören zu den gebräuchlichsten in Windows Forms.



Listen-Steuerelemente: Listen-Steuerelemente werden üblicherweise verwendet, um Daten, meist aus Datenbanken, anzuzeigen. Zu dieser Kategorie gehören Steuerelemente wie die ComboBox und die ListBox. Mit ihnen zu arbeiten, ist nicht einfach, da man dabei mit Sammlungen von Daten arbeitet (im Gegensatz dazu ist es einfach, den Umgang mit ihnen zu erlernen!). Wir überspringen ihre nähere Betrachtung, bis Sie an Tag 9 lernen, wie Sie mit Daten und Datenbanken arbeiten.



Menü-Steuerelemente: Menüs gehören neben Schaltflächen zu den gebräuchlichsten Steuerelementen in einer Anwendung: von normalen Symbolleisten-Menüs bis hin zu Kontextmenüs. Der Rest dieses Kapitels beschäftigt sich mit der Funktionsweise dieser Steuerelemente.



Weitere Steuerelemente: Zu den Steuerelementen, die nicht in die obigen Kategorien passen, gehören etwa das Label-Steuerelement, Zeit- und Datum-Steuerelemente sowie das Steuerelement TreeView (Strukturansicht). Das bedeutet nicht, dass sie weniger wichtig wären, im Gegenteil: Wie Sie bereits gesehen haben, ist das Label-Steuerelement eines der gebräuchlichsten überhaupt.

Lassen Sie uns ohne weiteren Verzug lernen, wie man Menüs erstellt!

4.2

Menüs hinzufügen

Wir alle können uns etwas unter Menüs und ihrer Verwendung vorstellen, doch lassen Sie uns ein wenig rekapitulieren. Ein Menü ist eine Gruppe ähnlicher Befehle, die sich in einer direkt unter der Titelleiste befindlichen Leiste befinden. Die Titelzeile des Hauptmenüs dient lediglich der Gruppierung der Kategorien und führt ansonsten keine Funktionen

114

Menüs hinzufügen

aus. Die Unterelemente sind die eigentlichen Arbeitsbienen unter den Menüs. Manchmal können sogar Unterelemente als Menüs dienen, um so weitere Befehle zu Unterkategorien zusammenzufassen. Abbildung 4.2 zeigt ein typisches Menü.

Abbildung 4.2: Das DATEI-Menü des Internet Explorers ist typisch für ein Befehlsmenü.

In Abbildung 4.2 beachten Sie bitte die waagrechten Linien, die Menügruppen im DATEI-Menü voneinander trennen. Diese Zeilen werden Separatoren oder Trennlinien genannt und sie helfen dem Benutzer bei der Navigation in den Anwendungsmenüs. Wie Sie Trennlinien in Ihre Menüs integrieren, lernen Sie im heutigen Abschnitt »Menüs anpassen« weiter unten. In .NET werden die Menükopfzeilen durch das Objekt MainMenu dargestellt, Unterelemente jedoch durch MenuItem-Steuerelemente. Diese ähneln den Ihnen bereits bekannten Steuerelementen, also lassen Sie uns daher in etwas Code eintauchen. Listing 4.1 ist ihr erster Vorstoß ins Menü-Territorium. Listing 4.1: Beispiel für Ihr erstes Menü, geschrieben in VB .NET 1: 2: 3: 4: 5: 6: 7:

Imports System Imports System.Windows.Forms Namespace TYWinForms.Day4 public class Listing41 : Inherits Form private mnuFile as new MainMenu

115

Menüs und Symbolleisten

8: private lblMessage as new Label 9: 10: public sub New() 11: Me.Text = "Listing 4.1" 12: Me.Menu = mnuFile 13: 14: dim miFile as MenuItem = mnuFile.MenuItems.Add("&Datei") 15: miFile.MenuItems.Add(new MenuItem("&Öffnen...")) 16: 17: lblMessage.Text = "Ich liebe WinForms" 18: lblMessage.Width = 200 19: lblMessage.Height = 200 20: 21: Me.Font = new System.Drawing.Font(new  System.Drawing.FontFamily("Arial"), 15) 22: Me.Controls.Add(lblMessage) 23: End Sub 24: 25: public Shared Sub Main() 26: Application.Run(new Listing41) 27: end sub 28: end class 29: 30: End Namespace

Ab Zeile 7 erzeugen wir eine Instanz unseres ersten neuen Objekts, dem MainMenu. Dadurch erhalten wir ein Gerüst für das Einfügen weiterer Menüs. Dies und entsprechende Befehle folgen später. In Zeile 12 machen Sie Bekanntschaft mit einer neuen Eigenschaft des Form-Objekts: Menu. Menu muss stets auf ein MainMenu-Objekt zeigen, insbesondere auf das MainMenu des Formulars. Auf diese Weise können Sie so viele Menüs erzeugen, wie Sie wünschen, und dann dasjenige, das angezeigt wird, austauschen – einfach indem Sie die Menu-Eigenschaft bearbeiten. In Zeile 14 sehen Sie die erste Instanz des MenuItem-Objekts. Es lässt sich sehr vielseitig verwenden; es kann jedes Menüelement (MenuItem) darstellen, ganz gleich, ob es sich in der Menühierarchie auf oberster oder drittklassiger Ebene befindet. Sie verwenden die AddMethode des MainMenu-Objekts, um der MenuItems-Auflistung neue Menüelemente hinzuzufügen. Dieses MenuItem wird die oberste Ebene oder Kopfzeile für ein Menü bilden. In diesem Fall haben wir es »Datei« genannt, ein sehr gebräuchliches Menü in WindowsAnwendungen.

116

Menüs hinzufügen

Das kaufmännische Und-Zeichen (&, auch »Ampersand« genannt) im Menünamen bedeutet, dass das Zeichen, das unmittelbar darauf folgt, durch die Tastatur zugänglich ist. Dies wird als Zugriffstaste. In Zeile 14 folgt dem Ampersand der Buchstabe D. So kann der Benutzer das DATEI-Menü mit der Tastenkombination (Alt)+(D) öffnen. Es darf nur eine solche Tastenkombination pro Menüelement geben. Auch nach dem Erstellen eines Menüelements können Sie dessen Titelzeile noch mit Hilfe der Text-Eigenschaft ändern. In Zeile 15 fügen Sie dem in Zeile 14 erzeugten MenuItem-Objekt ein weiteres hinzu. Weil dieses MenuItem zu einem anderen MenuItem-Objekt statt zum MainMenu-Objekt gehört, wird es als Menüelement im Menü statt in der Hauptmenüleiste erscheinen. Das neue MenuItem heißt »Öffnen«. Abbildung 4.3 zeigt das Ergebnis dieses Codes.

Abbildung 4.3: Listing 4.1 erzeugt ein DATEI-Menü mit einem Menüelement namens ÖFFNEN.

Wenn Sie auf das Menüelement klicken, passiert nichts; das liegt daran, dass wir noch einen Handler für dieses Menüelement erstellen müssen. Dafür ändern wir einfach die Zeile 15 wie folgt: miFile.MenuItems.Add(new MenuItem("&Öffnen...", new EventHandler(AddressOf Me.Open_Clicked)))

oder in C# wie folgt: miFile.MenuItems.Add(new MenuItem("&Öffnen...", new EventHandler(this.Öffnen_Clicked)));

Erstellen Sie dann die Öffnen_Clicked-Methode. Fügen Sie den folgenden Code irgendwo zwischen dem Schluss der New-Methode und dem Anfang der Main-Methode ein: public sub Öffnen_Clicked(Sender as Object, e as EventArgs) Microsoft.VisualBasic.MsgBox("Öffnen-Menü angeklickt!") end sub

117

Menüs und Symbolleisten

Statt die vertraute AddHandler-Methode zu verwenden, legen Sie einfach nur die Methode fest, die nach der Menüauswahl ausgeführt werden soll – ein weiterer Parameter bei der Erstellung des Menüelements (wenn Sie wollen, könnten Sie auch AddHandler verwenden). Abbildung 4.4 zeigt das geänderte Ergebnis.

Abbildung 4.4: Das Hinzufügen eines Handlers verleiht den Menüelementen Funktionsfähigkeit.

Lassen Sie uns einen Blick auf ein weiteres Beispiel werfen, diesmal für die Erstellung von Untermenüs. Listing 4.2 zeigt die Anwendung in C#-Code. Listing 4.2: Ein erweitertes Menü in C# 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:

118

using System; using System.Windows.Forms; namespace TYWinForms.Day4 { public class Listing42 : Form { private MainMenu mnuDatei = new MainMenu(); private Label lblMessage = new Label(); public Listing42() { this.Text = "Listing 4.2"; this.Menu = mnuDatei; MenuItem miDatei = mnuDatei.MenuItems.Add("&Fenster"); miDatei.MenuItems.Add(new MenuItem("&Forms")); miDatei.MenuItems[0].MenuItems.Add(new MenuItem("&Menüs")); miDatei.MenuItems[0].MenuItems[0].MenuItems.Add(new MenuItem("&Sind")); miDatei.MenuItems[0].MenuItems[0].MenuItems.Add(new MenuItem("So")); miDatei.MenuItems[0].MenuItems[0].MenuItems[1].MenuItems.Add(new MenuItem("&Einfach wie"));

Menüs hinzufügen

20: 21: 22: 23: 24: 25: 26: 27: 28: 29:

miDatei.MenuItems[0].MenuItems[0].MenuItems[1].MenuItems.Add(new MenuItem("Kuchenbacken")); } public static void Main(){ Application.Run(new Listing42()); } } }

Bis zur Zeile 14 ist Listing 4.2 mit Listing 4.1 identisch. Ab hier wird es interessant. Die Zeilen 14 und 15 fügen dem MainMenu genau wie zuvor weitere MenuItem-Objekte hinzu. An diesem Punkt haben Sie zwei Hierarchieebenen: die Kopfzeile des Menüs (die wir 'Item A' nennen wollen) und ein einzelnes Menüelement, »Fenster« (das wir 'Item B' nennen wollen). Denken Sie daran, dass die MenuItems-Eigenschaft eine Auflistung ist: Sie können auf jedes Element in der Auflistung mit Hilfe einer Indexzahl verweisen (0 ist stets das erste Mitglied in der Auflistung). In Zeile 16 greifen Sie auf das erste Element in der MenuItems-Auflistung von Item B zu, welches wiederum ein weiteres MenuItem darstellt. Dieses MenuItem wollen wir 'Item C' nennen. Diesem fügen Sie genau wie zuvor ein weiteres MenuItem hinzu. Weil Sie das MenuItem 'Item C' hinzugefügt haben, wird es zu einem weiteren Untermenü. Jetzt verfügen wir über drei Menüebenen: Fenster, Formulare und Menüs. Um auf ein Mitglied in einer Auflistung in VB .NET zuzugreifen, benötigen Sie die Item-Eigenschaft: miDatei.MenuItems.Item(0).MenuItems.Add(new etc etc)

In Zeile 17 erweitern Sie dieses Konzept ein wenig. Sie greifen auf die MenuItems-Auflistung von Item C zu und fügen ein neues MenuItem ein, so dass Sie eine vierte Menüebene erhalten. Zeile 18 tut das Gleiche, indem sie der vierten Ebene ein weiteres MenuItem hinzufügt. Die Zeilen 19 und 20 führen diesen Vorgang fort, indem sie dem zweiten Element (wie durch den Index 1 angezeigt) weitere Untermenüs hinzufügen. Als Ergebnis unserer Bemühungen erhalten wir fünf Menüebenen, wie in Abbildung 4.5 zu sehen ist. Der Vorgang des Verschachtelns von Menüs mag zunächst ein wenig verwirrend erscheinen, aber denken Sie einfach daran, dass das erste MenuItem, das Sie einem anderen MenuItem hinzufügen, stets ein Untermenü erzeugt.

119

Menüs und Symbolleisten

Abbildung 4.5: Das Verschachteln von Menüs erzeugt einen interessanten Quellcode.

Sie können auch Untermenüs dadurch hinzufügen, dass Sie für jedes Element im Menü explizit ein neues MenuItem erzeugen. Schauen Sie sich zum Beispiel folgendes Codefragment an: MenuItem MenuItem MenuItem MenuItem …

miDatei = mnuDatei.MenuItems.Add("&Windows"); miForms = miDatei.MenuItems.Add("&Forms"); miMenüs = miFormulare.MenuItems.Add("&Menüs"); miSind = miMenüs.MenuItems.Add("Sind");

Die erste Zeile erzeugt ganz normal das Element für die Kopfzeile des Menüs. Das nächste Menüelement wird als neues MenuItem erzeugt und der MenuItems-Auflistung der vorhergehenden Ebene hinzugefügt, genau wie Sie das mit der Menükopfzeile gemacht haben. Beachten Sie, dass jede darauffolgende Zeile das neue MenuItem in die MenuItems-Auflistung der vorhergehenden Zeile hinzufügt. Beide Methoden zur Menüerzeugung erledigen diese Aufgabe effizient. Die zweite Methode weist jedoch eine Reihe von Vorteilen auf: Sie erzeugt für jedes Menüelement einen neuen Namen, so dass wir es notfalls später leicht wiederfinden können (statt seine Indexzahl zu benutzen); und der Code ist ebenfalls leichter zu lesen. Letztlich bleibt die Wahl Ihnen überlassen.

Menüs anpassen Menüs sind in höchstem Maß anpassungsfähig. Sie können Menüs zusammenfassen, visuelle Erweiterungen und Kurzbefehle hinzufügen und so weiter. Da diese Verschönerungen dem Endbenutzer eine bessere Bedienungserfahrung vermitteln, wollen wir einen Blick darauf werfen. Die einfachste Methode zur Anpassung Ihrer Menüs besteht darin, ähnliche Elemente zusammenzufassen. Das in Abbildung 4.2 gezeigte DATEI-Menü des Internet Explorers zeigt, wie sich ähnliche Menüelemente gruppieren lassen; eine Trennlinie (Separator)

120

Menüs hinzufügen

separiert eine Serie von Menüelementen von der nächsten. Das Einfügen einer Trennlinie ist problemlos und eine gute Möglichkeit, den Benutzern beim Navigieren durch die Menüs zu helfen. Sehen Sie sich bitte folgendes Stück Code an: MenuItem miDatei = mnuDatei.MenuItems.Add("&Datei"); MenuItem miÖffnen = miDatei.MenuItems.Add("&Öffnen"); MenuItem miSpeichern = miDatei.MenuItems.Add("&Speichern"); miFile.MenuItems.Add("–"); MenuItem miBeenden = miDatei.MenuItems.Add("Beenden");

Die ersten drei Zeilen fügen einem vorhandenen MainMenu-Objekt einfache Menüelemente hinzu. Um eine Trennlinie einzufügen, müssen Sie lediglich die Add-Methode aufrufen und ihr einen Gedankenstrich (–) übergeben. Die CLR versteht: Sie wollen die folgenden Menüelemente zu einer anderen Gruppe zusammenfassen und abtrennen. Diese einfache Erweiterung führt zu dem folgenden Menü in Abbildung 4.6.

Abbildung 4.6: Trennlinien helfen beim Organisieren Ihrer Menüs.

Zusätzlich zu den Zugriffstaten in den Menükopfzeilen können Sie auch Tastenkombinationen für Ihre Menüelemente einbauen. Microsoft Word verwendet beispielsweise (Strg) + (S) als Tastenkombination, um Dokumente zu speichern. Die meisten Anwendungen verwenden die Funktionstaste (F1) als Shortcut zu einem Element des HILFE-Menüs. (Strg) + (O) führt den Menübefehl DATEI/ÖFFNEN aus. MenuItem miÖffnen = miDatei.MenuItems.Add("&Öffnen", this.Öffnen_Clicked, Shortcut.CtrlO);

Der dritte Parameter, den Sie hier sehen, ist die Eigenschaft Shortcut. Sie können die Shortcut-Aufzählung verwenden, um die gewünschte Tastenkombination festzulegen. Diese Aufzählung verfügt über Werte für die meisten Tastenkombinationen, wie etwa Ctrlx wobei x eine beliebige Zahl oder ein beliebiges Zeichen sein kann; in Fx steht x für jede beliebige Zahl, und in CtrlShiftx ist x eine beliebige Zahl oder ein beliebiges Zeichen. In der .NET-Dokumentation erhalten Sie vollständige Informationen dazu.

121

Menüs und Symbolleisten

Wenn Sie während der Erstellung eines Menüelements keine Tastenkombination festlegen wollen, können Sie das auch noch später tun, indem Sie die Eigenschaft Shortcut des MenuItems nutzen: miDatei.Shortcut = Shortcut.ShiftF9

Setzen Sie die ShowShortcut-Eigenschaft auf true, wenn Sie die damit verknüpfte Tastenkombination im Menü neben dem Menüelement anzeigen lassen wollen: MiDatei.Shortcut = ShowShortcut = true

Schließlich gibt es noch ein paar einfache boolesche Eigenschaften, die Sie für jedes Menüelement anpassen können: 쐽

Enabled legt fest, ob der Benutzer das Menüelement auswählen kann oder nicht (deaktivierte Elemente erscheinen grau).



Visible zeigt an, ob der Benutzer das Element sehen kann (bedenken Sie, dass ein

Menüelement selbst dann noch per Tastenkombination aktiviert werden kann, wenn es nicht sichtbar ist; sorgen Sie dafür, dass sowohl Enabled als auch Visible auf false gesetzt sind, wenn Sie verhindern wollen, dass Benutzer ein Menüelement sehen oder auswählen). 쐽

Checked zeigt an, ob sich neben dem Menüelement ein Häkchen befindet. Das ist

nützlich, wenn Sie beispielsweise kenntlich machen wollen, ob ein Menüelement ausgewählt worden ist. 쐽

DefaultItem zeigt an, ob das Menüelement die voreingestellte Auswahl ist (Standardelemente erscheinen in fetter Schrift).

Um weitere Informationen über MenuItem-Eigenschaften zu erhalten, schlagen Sie bitte in Anhang B nach.

Kontextmenüs Wenn Sie bereits einmal auf ein Formular mit der rechten Maustaste (ein »Rechtsklick«) geklickt haben, wissen Sie, dass dann nichts passiert: kein Popup-Menü öffnet sich, wie so oft in Windows-Anwendungen. Dieses Popup-Menü nach dem Rechtsklick, welches häufig kontext-relevante Menüs zur Auswahl bereitstellt (etwa Ausschneiden, Einfügen usw.), bezeichnet man als Kontextmenü. Wenn Sie bereits einmal etwas länger mit WindowsAnwendungen gearbeitet haben, wissen Sie, dass solch ein Menü für produktives Arbeiten einfach unerlässlich ist. Zum Glück lassen sich Kontextmenüs ebenso einfach einem Formular hinzufügen wie normale Menüs. Alle Kontextmenüs werden durch das ContextMenu-Objekt dargestellt. Es ist dem MainMenuObjekt sehr ähnlich. Menüelemente im ContextMenu werden sogar als MenuItem-Objekte dargestellt. Folglich können Sie alles, was Sie bisher über Menüs gelernt haben, auch auf Kontextmenüs anwenden.

122

Menüs hinzufügen

Am einfachsten lässt sich ein Kontextmenü als Kopie des regulären Menüs anfertigen, das Sie bereits erzeugt haben. Dies können Sie mit der CloneMenu-Methode bewerkstelligen. Der folgende Code erzeugt ein neues ContextMenu-Objekt und fügt die Kopie eines vorhandenen Menüs hinzu: ContextMenu mnuContext = new ContextMenu(); Form1.ContextMenu = mnuContext; MenuItem miContext = miDatei.CloneMenu(); mnuContext.MenuItems.Add(miContext);

Sobald Sie Ihrem Formular diesen Code hinzugefügt haben, wird ein Rechtsklick ein Popup-Menü hervorbringen, das genau Ihrem DATEI-Menü entspricht. Dieses Beispiel ist das allereinfachste Kontextmenü. Der Grund jedoch, warum es »Kontext«-Menü genannt wird, liegt darin, dass die Anwendung feststellen kann, in welchem Teil des Formulars sich der Mauszeiger beim Rechtsklicken befand, und kann so für diese Stelle ein spezialisiertes Menü bereitstellen. So kann etwa ein Rechtsklick auf eine leere Fläche im Formular das DATEI-Menü aufrufen, und ein Rechtsklick auf eine TextBox könnte ein spezielles Kontextmenü mit den Befehlsoptionen AUSSCHNEIDEN und EINFÜGEN darin aufrufen. Um dieses Funktionsmerkmal bereitzustellen, verfügt jedes Windows Forms-Steuerelement (inklusive des Form-Objekts) über die Eigenschaft ContextMenu, die das Menü angibt, welches geöffnet wird, wenn das Steuerelement rechts angeklickt wird. Dies macht es Ihnen recht leicht, Ihre Kontextmenüs anzupassen. Lassen Sie uns mit Listing 4.3 ein Beispiel dafür ansehen. Listing 4.3: Angepasste Kontextmenüs 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day4 { public class Listing43 : Form { private MainMenu mnuFile = new MainMenu(); private ContextMenu ctmTextBox = new ContextMenu(); private ContextMenu ctmLabel = new ContextMenu(); private Label lblMessage = new Label(); private TextBox tbMessage = new TextBox(); public Listing43() { this.Text = "Listing 4.3 "; this.Menu = mnuFile; AddMenus();

123

Menüs und Symbolleisten

19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50:

lblMessage.Text = "Ich liebe Windows Forms "; lblMessage.Location = new Point(100,75); lblMessage.Width = 150; tbMessage.Text = "Ich liebe Windows Forms "; tbMessage.Location = new Point(75,125); tbMessage.Width = 150; this.Controls.Add(lblMessage); this.Controls.Add(tbMessage); } public void AddMenus() { MenuItem miDatei = mnuDatei.MenuItems.Add("&Datei"); MenuItem miÖffnen = miDatei.MenuItems.Add("&Öffnen"); MenuItemSpeichern = miDatei.MenuItems.Add("&Speichern"); miDatei.MenuItems.Add("-"); MenuItem miBeenden = miDatei.MenuItems.Add("Beenden"); MenuItem ctiLabel = ctmLabel.MenuItems.Add("Label-Menü"); lblMessage.ContextMenu = ctmLabel; MenuItem ctiTextBox = ctmTextBox.MenuItems.Add("Textbox-Menü"); tbMessage.ContextMenu =ctmTextBox; } public static void Main() { Application.Run(new Listing43()); } } }

In den Zeilen 1 bis 30 findet sich nichts Neues. Beachten Sie jedoch, dass die Zeilen 9 und 10 Instanzen von ContextMenu-Objekten erzeugen; wir werden sie gleich verwenden. Die Zeile 18 ruft die AddMenus-Methode auf, die in Zeile 32 beginnt. Die Zeilen 33 bis 37 erzeugen ein normales DATEI-Menü für Ihre Anwendung (Sie haben diesen Code bereits in Listing 4.2 gesehen). Zeile 39 erzeugt ein neues MenuItem für das ContextMenu-Objekt ctmLabel. Seine Beschriftung lautet »Label-Menü«. In Zeile 40 verbinden Sie dieses ContextMenu-Objekt (und somit sein untergeordnetes MenuItem-Objekt) mit dem Label-Steuerelement. Dieses Kontextmenü lässt sich nun auf dieses Label anwenden (wenn Sie wollen, könnten Sie dieses Kontextmenü auch anderen Steuerelementen zuweisen).

124

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Die Zeilen 42 und 43 erledigen das Gleiche für das TextBox-Objekt, indem sie ein Menüelement erzeugen, das die Beschriftung »Textbox-Menü« trägt. Da Sie an keiner Stelle auch dem Form-Objekt ein ContextMenu zuweisen, wird ein Rechtsklick auf eine leere Formularfläche kein Kontextmenü öffnen. Dieses Listing erzeugt das in Abbildung 4.7 gezeigte Ergebnis.

Abbildung 4.7: Wenn Sie mit der rechten Maustaste auf das Bezeichnungsfeld (Label) klicken, wird das damit verbundene Kontextmenü angezeigt.

In diesem Beispiel zeigt ein Rechtsklick auf das Textfeld das Kontextmenü »TextboxMenü« an. Wenn Sie einmal die Zeilen 42 und 43 aus dem Listing auskommentieren und es neu kompilieren, zeigt ein Rechtsklick auf das Textfeld ein ganz anderes damit verknüpftes Kontextmenü an: eines, das Befehlsoptionen für Ausschneiden und Einfügen anzeigt. Der Grund dafür ist, dass TextBox (und ebenso RichTextBox) bereits voreingestellte Kontextmenüs besitzen. Indem Sie Ihr eigenes Kontextmenü erzeugen, setzen Sie die vorhandenen Menüs außer Kraft.

4.3

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Symbolleisten, Statusleisten und Bildlaufleisten sind ebenso vertraute Bestandteile jeder Windows-Anwendung wie Menüs. Eine Symbolleiste befindet sich gewöhnlich am oberen Ende eines Anwendungsfensters, also direkt unter der Menüleiste, und enthält Schaltflächen mit Grafiken darauf, die die jeweilige Funktionalität in der Anwendung signalisieren. Word etwa hat eine Schaltfläche mit dem Bild eines geöffneten Ordners, um das Öffnen einer Datei zu bezeichnen, und ein Diskettensymbol für die Speichern-Funktion. Eine Statusleiste befindet sich am unteren Rand des Anwendungsfensters und stellt dem Benutzer Verarbeitungsinformationen zur Verfügung. Ihre einzige interaktive Funktion besteht darin, Meldungen anzuzeigen. Bildlaufleisten schließlich erlauben dem Benutzer, durch ein Formular zu blättern, falls nicht alle Steuerelemente in die vorhandene Fensterfläche passen. Abbildung 4.8 stellt alle drei Leistentypen dar.

125

Menüs und Symbolleisten

Abbildung 4.8: In den meisten Anwendungen finden sich Symbolleisten, Statusleisten und Bildlaufleisten.

Trotz ihrer Namensähnlichkeit erweisen sich die drei Steuerelemente bei der Implementierung als recht unterschiedlich. Die Symbolleiste agiert als selbstständiges Steuerelement (obwohl es weitere Steuerelemente enthält) und ist mit einem ImageList-Objekt verknüpft. Die Statusleiste hingegen agiert ähnlich einem Formular als Container für andere Steuerelemente. Die Bildlaufleiste ist wieder eine andere Angelegenheit und verfügt nur über wenige anpassbare Eigenschaften. Wir betrachten diese Steuerelemente der Reihe nach.

Symbolleisten Eine Symbolleiste besteht aus einer Sammlung von Schaltflächen (normalerweise durch Grafiken dargestellt), die Kurzbefehle (Shortcuts) zu den Funktionen Ihrer Anwendung anbieten. Wahrscheinlich arbeiten Sie bereits seit längerem mit Symbolleisten, so dass wir uns hier nicht bei konzeptionellen Details aufhalten. Die Symbolleiste und ihre Schaltfläche werden durch die Objekte ToolBar und ToolBarButton dargestellt. Diese beiden Objekte ähneln den Objekten MainMenu und MenuItem: Sie erstellen das zweite Objekt und fügen es dem ersten Objekt hinzu, die Schaltflächen also der Leiste. Lassen Sie uns einen Blick auf Listing 4.4 werfen, das Code zeigt, welcher eine einfache Symbolleiste erzeugt.

126

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Listing 4.4: Eine einfache Symbolleiste hinzufügen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:

using System; using System.Windows.Forms; namespace TYWinForms.Day4 { public class Listing44 : Form { private ToolBar tlbStandard = new ToolBar(); public Listing44() { ToolBarButton tbbOpen = new ToolBarButton(); tbbOpen.Text = "Öffnen "; tlbStandard.Buttons.Add(tbbOpen); this.Controls.Add(tlbStandard); } public static void Main() { Application.Run(new Listing44()); } } }

In Zeile 7 erzeugen Sie ein neues ToolBar-Objekt auf die gleiche Weise wie ein MainMenu-Objekt. In Zeile 15 fügen Sie es Ihrem Formular wie jedes andere Steuerelement hinzu. In Zeile 10 erstellen Sie ein neues ToolBarButton-Steuerelement: Darauf wird der Benutzer später klicken können. In Zeile 11 setzen Sie die Text-Eigenschaft so, dass das Wort »Öffnen« angezeigt wird (Sie werden gleich merken, dass diese Text-Eigenschaft nicht ganz das ist, wofür Sie sie halten). Schließlich fügen Sie das ToolBarButton-Steuerelement Ihrem ToolBarObjekt hinzu. Ohne diesen Schritt wäre Ihre Symbolleiste leer und würde in sich zusammenfallen. Kompilieren Sie diese Anwendung und führen Sie sie aus. Das Ergebnis sollte wie in Abbildung 4.9 aussehen. Wir haben zwar jetzt eine Symbolleistenschaltfläche, aber einige Dinge scheinen zu fehlen oder nicht zu stimmen. Augenfällig ist, dass sich ihr Text nicht in der Mitte befindet. Zweitens ist mit der Schaltfläche keine Grafik verknüpft. Und schließlich passiert nach einem Klick auf die Schaltfläche überhaupt nichts. Alle diese Mängel lassen sich leicht beheben.

127

Menüs und Symbolleisten

Abbildung 4.9: Listing 4.4 erzeugt eine einfache Symbolleiste mit nur einer Schaltfläche.

Die Text-Eigenschaft sieht nicht nach dem aus, was wir gewohnt sind. Die Text-Eigenschaft auf einem Button-Steuerelement zeigt eine Beschriftung im Zentrum der Schaltfläche. Die Text-Eigenschaft für den ToolBarButton hingegen wird verwendet, um die Grafik auf der Schaltfläche zu ergänzen (denken Sie mal an die ZURÜCK-Schaltfläche im Internet Explorer). Der Grund für das Erscheinen des Textes außerhalb des Zentrums ist also, dass er Platz lässt für die Grafik. Wenn wir der Schaltfläche eine Grafik zuweisen wollen, müssen wir etwas über ein neues Objekt namens ImageList lernen. Es wird meist von anderen Steuerelementen wie etwa dem ToolBar-Steuerelement dazu verwendet, Grafiken mit einer Auflistung (wie etwa ToolBarButtons) zu verknüpfen. ImageList verfügt über Methoden, um die Grafiken, die es enthält, hinzuzufügen, zu entfernen und neu zu sortieren. Nachdem Sie einer ImageList Grafiken hinzugefügt haben, werden die Schaltflächen in einer Symbolleiste auf die Grafiken mit Hilfe ihrer jeweiligen ImageIndex-Eigenschaft verweisen. Etwa so: MyToolBar.ImageList = myImageList; MyToolBarButton.ImageIndex = 1;

Wir wollen eine neue Methode namens AddToolBar verwenden, um die Erstellung unserer Symbolleiste zu bewerkstelligen. Ersetzen Sie die Zeilen 10 bis 13 in Listing 4.4 durch einen Aufruf der Methode AddToolbar();

und fügen Sie dann Ihrer Anwendung den Code, der in Listing 4.5 gezeigt wird, zwischen dem Konstruktor und der Main-Methode hinzu. Listing 4.5: Grafiken mit Symbolleistenschaltflächen verknüpfen 1: 2: 3:

128

private ImageList ilstToolbar =new ImageList(); public void AddToolbar(){

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:

ilstToolbar.Images.Add(Image.FromFile("i_open.bmp ")); ilstToolbar.Images.Add(Image.FromFile("i_maximize.bmp ")); ilstToolbar.Images.Add(Image.FromFile("i_happy.bmp ")); ToolBarButton tbbOpen =new ToolBarButton(); tbbOpen.ImageIndex =0; tbbOpen.Text ="Öffnen "; ToolBarButton tbbMaximize =new ToolBarButton(); tbbMaximize.ImageIndex =1; tbbMaximize.Text ="Maximieren "; ToolBarButton tbbHappy =new ToolBarButton(); tbbHappy.ImageIndex =2; tbbHappy.Text ="Werden Sie glücklich!"; tlbStandard.ImageList =ilstToolbar; tlbStandard.Buttons.Add(tbbOpen); tlbStandard.Buttons.Add(tbbMaximize); tlbStandard.Buttons.Add(tbbHappy); }

Zeile 1 erzeugt ein neues ImageList-Objekt. In den Zeilen 4 bis 6 fügen Sie diesem Objekt Grafiken hinzu, indem Sie die statische FromFile-Methode des Image-Objekts verwenden. (Beachten Sie, dass Sie die Namen der Dateien, die Sie oben sehen, zu solchen ändern sollten, die Sie tatsächlich besitzen!) Diese Methode lädt die angegebene Datei und übergibt sie der Images-Auflistung von ImageList. Da Sie in Listing 4.5 das Image-Objekt verwenden, sollten Sie nicht vergessen, auch den Namensraum System.Drawing zu importieren! Sie könnten etwa folgenden Code am Anfang Ihrer Datei einfügen: using System.Drawing;

Die Zeilen 8 bis 18 erzeugen drei neue ToolBarButton-Objekte, wobei die Eigenschaften für Text und ImageIndex angegeben werden. Der ImageIndex zeigt auf den Index für diejenige Grafik in der ImageList, die Sie mit der Schaltfläche verknüpfen wollen (der erste Index lautet stets 0). In Zeile 20 verknüpfen Sie schließlich die in Zeile 1 erzeugte ImageList mit dem ToolBarObjekt. Dabei verwenden Sie die ImageList-Eigenschaft des Objekts. Die Zeilen 21 bis 23 fügen einfach die neu erstellten Schaltflächen der Symbolleiste hinzu. Abbildung 4.10 zeigt das Ergebnis dieser Datei.

129

Menüs und Symbolleisten

Abbildung 4.10: Jetzt verfügt die Symbolleiste über Schaltflächen mit Symbolen und Beschriftungen.

Die Anwendung sieht schon ein wenig besser gefüllt aus. Aber es gibt noch einiges zu tun: Die Schaltflächen sollten auch eine Funktion erfüllen. Erinnern Sie sich an meine Worte, wonach die ToolBar als ein einzelnes Steuerelement agiert? Gleich werden Sie den Grund dafür erkennen. ToolBarButton-Objekte verfügen über keine Ereignisse, die man behandeln kann (von dem Disposed-Ereignis einmal abgesehen, doch dies wird von einem anderen Objekt geerbt).

Von alleine wissen sie also nicht, dass sie angeklickt wurden. Wenn man also die Aktionen steuern möchte, sobald ein Benutzer auf eine Symbolleistenschaltfläche geklickt hat, muss man sich dem ToolBar-Objekt zuwenden. Das ToolBar-Steuerelement verfügt über ein ButtonClick-Ereignis, das ausgelöst wird, sobald eine der in ihm enthaltenen ToolBarButtons angeklickt wird. Daher müssen alle Ereignisse und Aktionen über die ToolBar erfolgen. Um Ihrer ToolBar einen Handler hinzuzufügen, verwenden Sie folgenden Code: tlbStandard.ButtonClick += new ToolBarButtonClickEventhandler (this.MyToolBarHandler);

Das Listing 4.6 zeigt den MyToolBarHandler. Listing 4.6: Die Behandlung von Symbolleisten-Ereignissen in C# 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:

130

private void MyToolBarHandler(Object Sender, ToolBarButtonClickEventArgs e){ switch(tlbStandard.Buttons.IndexOf(e.Button)){ case 0: //hier folgt etwas Code; break; case 1: // hier folgt etwas Code; break; case 2: // hier folgt etwas Code;

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

11: 12: 13:

break; } }

Das Parameterobjekt ToolBarButtonClickEventArgs verfügt über eine Button-Eigenschaft, wie in Zeile 2 zu sehen ist. Sie gibt an, welche Schaltfläche angeklickt wurde. Wir verwenden die IndexOf-Eigenschaft der Buttons-Auflistung, um für jede Schaltfläche eine numerische Darstellung zurückzugeben, die wir für die Entscheidung darüber verwenden können, welcher Code auszuführen ist. Das switch-Statement (in VB .NET wird statt dessen Select Case verwendet) wertet die ihm gelieferte Bedingung gegenüber jedem einzelnen case-Element aus. Wenn die Werte übereinstimmen, wird der Code im betreffenden case-Zweig ausgeführt. IndexOf legt fest, welches Element in der Buttons-Auflistung der ToolBar mit dem im Parameterobjekt angegebenen übereinstimmt und gibt dann den Index des entsprechenden Steuerelements in der Auflistung zurück. Break ist nötig, um der CLR mitzuteilen, nach der Ausführung des case-Statements keine weiteren mehr auszuführen oder auszuwerten. Listing 4.7 zeigt den gleichen Code in VB .NET geschrieben. Listing 4.7: Die Behandlung von Symbolleisten-Ereignissen in VB .NET 1: private Sub MyToolBarHandler(Sender as Object,e as ToolBarButtonClickEventArgs) 2: Select Case tlbStandard.Buttons.IndexOf(e.Button) 3: Case 0 4: 'etwas Code ausführen 5: Case 1 6: 'etwas Code ausführen 7: Case 2 8: 'etwas Code ausführen 9: end Select 10: End Sub

Symbolleisten anpassen ToolBar- und ToolBarButton-Steuerelemente verfügen über einige Eigenschaften, die Ihnen erlauben, deren Aussehen anzupassen. Wir werfen kurz einen Blick darauf, angefangen bei den Eigenschaften des ToolBar-Steuerelements.

Die Eigenschaft Appearance gibt an, ob Ihre Symbolleistenschaltflächen als normale dreidimensionale Objekte (erhabene Schaltflächen) oder flach (Browser-Stil) erscheinen sollen. Den Unterschied versteht man am besten, indem man sich beide Stile ansieht – sie sind beide in Abbildung 4.11 zu sehen.

131

Menüs und Symbolleisten

Abbildung 4.11: Normale Symbolleistenschaltflächen sehen dreidimensional aus; flache Schaltflächen hingegen sind eben das: flach. Appearance lässt sich auf zwei Werte setzen: Normal (3D) oder Flat. Beide Werte gehören zur Aufzählung ToolBarAppearance. Das folgende Codefragment lässt die Schaltfläche flach aussehen: tlb.Standard.Appearance = ToolBarAppearance.Flat;

Funktional gesehen besteht kein Unterschied zwischen den beiden Stilen; es ist lediglich eine Angelegenheit der visuellen Vorliebe. Die AutoSize-Eigenschaft des ToolBar-Steuerelements gibt an, ob sich die ToolBar je nach der Größe der ToolBarButtons, die sie enthält, vergrößern und verkleinern soll. Dementsprechend gibt es auch eine ButtonSize-Eigenschaft, die die Größe der Schaltflächen in der ToolBar vorgibt. Die Standardgröße für eine Schaltfläche beträgt 24 (Breite) x 22 (Höhe) Pixel. Doch wenn ButtonSize nicht anderweitig angegeben ist, passen sich die Schaltflächen der größten Grafik und dem mit ihnen verknüpften Text an. Um die Begrenzung Ihrer Symbolleiste zu steuern, stehen Ihnen die Eigenschaften Divider und BorderStyle zur Verfügung. Divider gibt an, ob eine Trennlinie zwischen der Symbolleiste und anderen Steuerelementen, wie etwa einem Menü, eingezogen werden soll. BorderStyle kann BorderStyle.FixedSingle, BorderStyle.Fixed3D oder BorderStyle.None sein. Diese Eigenschaft funktioniert hier genauso wie für das Form-Objekt. Das Steuerelement ToolBarButton verfügt nur über eine Haupteigenschaft, nämlich Style, und über verschiedene weitere, die diese Eigenschaft unterstützen. Style gibt an, welche Art von Schaltfläche dem Benutzer gezeigt wird. Sie kann einen der Werte in der Aufzählung ToolBarButtonStyle annehmen:

132

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen



DropDownButton: Zeigt auf Mausklick ein Dropdown-Menü an. Verwenden Sie die Eigenschaft DropDownMenu, um ein anzuzeigendes MenuItem zuzuweisen. Verwenden Sie die DropDownArrows-Eigenschaft des ToolBar-Objekts, um anzugeben, ob ein Pfeil anzeigt wird, der auf Anklicken hin ein Menü anzeigt.



PushButton: Die standardmäßige 3D-Schaltfläche, wie sie vom jeweiligen Betriebssystem definiert wird. (Die Schaltfläche kann in manchen Windows-Versionen flach statt dreidimensional erscheinen.)



Separator: Eine Trennlinie zwischen zwei Schaltflächen.



ToggleButton: Eine Schaltfläche, die, sobald sie angeklickt wurde, wie eingedrückt aussieht und so bleibt. Verwenden Sie die Pushed-Eigenschaft, um anzugeben, ob die Schaltfläche gerade gedrückt ist. PartialPush ähnelt Pushed, nur dass die Schaltfläche

nicht gedrückt, sondern vielmehr grau eingefärbt ist. Abbildung 4.12 zeigt Beispiele für all diese Stile.

Abbildung 4.12: Hier sind die Schaltflächenstile DropDown, PushButton und ToggleButton umgesetzt.

Statusleisten In einer Windows Forms-Anwendung wird eine Statusleiste durch das StatusBar-Objekt und eine beliebige Anzahl von StatusBarPanels (Statusleistenbereiche) dargestellt. Die StatusBar sieht fast wie ein Label-Steuerelement aus: Sie verfügt über keine Umrisslinien oder Trennlinien. Daher verwendet sie StatusBarPanels, um Informationen abzutrennen und anzuzeigen. Es gibt keine Regel, die besagt, man habe einer StatusBar irgendwelche StatusBarPanels hinzuzufügen – schließlich besitzt die StatusBar eine Text-Eigenschaft, die dem Benutzer Text anzeigen wird –das Standarderscheinungsbild einer Statusleiste sieht allerdings solche Bereiche vor. Die Steuerelemente StatusBar und StatusBarPanel sind den Menü- und SymbolleistenSteuerelementen sehr ähnlich, die Sie heute kennen gelernt haben. Daher dürften Sie sie leicht verstehen können. Wir wollen wieder in etwas Code eintauchen: Listing 4.8 (für dieses Beispiel verwenden wir VB .NET) zeigt, wie eine Statusleiste hinzugefügt wird.

133

Menüs und Symbolleisten

Listing 4.8: Ihrer Anwendung eine in VB .NET geschriebene Statusleiste hinzufügen 1: Imports System 2: Imports System.Windows.Forms 3: 4: Namespace TYWinForms.Day4 5: 6: public class Listing48 : Inherits Form 7: private sbrMain as new StatusBar 8: private lblMessage as new Label 9: 10: public sub New() 11: Me.Text = "Listing 4.8 " 12: 13: AddStatusBar 14: 15: lblMessage.Text = "Ich liebe WinForms" 16: lblMessage.Width = 200 17: lblMessage.Height = 200 18: lblMessage.Font = new System.Drawing.Font(new  System.Drawing.FontFamily("Arial "), 15) 19: 20: Me.Controls.Add(lblMessage) 21: Me.Controls.Add(sbrMain) 22: 23: End Sub 24: 25: public sub AddStatusBar() 26: dim sbpName as StatusBarPanel = sbrMain.Panels.Add("Hallo, Dave!") 27: sbpName.Width = 100 28: sbpName.BorderStyle = StatusBarPanelBorderStyle.Raised 29: 30: dim sbpTime as StatusBarPanel = sbrMain.Panels.Add(" ") 31: sbpTime.Text = Datetime.Now.ToString 32: sbpTime.AutoSize = StatusBarPanelAutoSize.Spring 33: sbpTime.Alignment = HorizontalAlignment.Right 34: 35: sbrMain.ShowPanels = true 36: end sub 37: 38: public Shared Sub Main() 39: Application.Run(new Listing48) 40: end sub 41: end class 42: 43: End Namespace

134

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Das Listing demonstriert eine Reihe von Eigenschaften der Steuerelemente StatusBar und StatusBarPanel. In Zeile 7 erzeugen Sie das erste Objekt: die StatusBar. Um den Code irgendwie logisch zu sortieren, haben wir den Initialisierungscode für die Statusleiste in einer Methode namens AddStatusBar untergebracht, die in Zeile 13 aufgerufen wird. Zeile 21 fügt die Statusleiste dem Formular hinzu. Die erste Zeile der AddStatusBar-Methode (Zeile 26) erzeugt ein neues StatusBarPanelObjekt und fügt es der Panels-Auflistung der StatusBar hinzu. Zeile 27 legt die Breite des Bereichs fest und Zeile 28 setzt den BorderStyle auf Raised (einen Wert in der Aufzählung StatusBarPanelBorderStyle; die anderen Werte, die Sie verwenden können, sind None oder Sunken, welches der Standardstil ist). In Zeile 30 wird ein weiterer StatusBarPanel-Bereich erzeugt. Die Text-Eigenschaft ist zunächst auf eine leere Zeichenfolge eingestellt. In Zeile 21 setzen Sie sie mit Hilfe der DateTime.Now-Funktion auf die aktuelle Uhrzeit (verwenden Sie ToString, um die Zeit als Zeichenfolge zurückzugeben – der Standardrückgabetyp ist ein DateTime-Objekt). Zeile 32 setzt die AutoSize-Eigenschaft, welche angibt, wie sich der Bereich vergrößern bzw. verkleinern lässt, je nach seinem Inhalt und/oder der Größe des Formulars. Spring wirkt wie eine Feder, so dass der Bereich den gesamten verfügbaren Raum einnimmt, der noch nicht von anderen Bereichen belegt ist. AutoSize kann auch auf None oder Contents gesetzt sein, wodurch die Größe des Bereichs an seinen Inhalt angepasst wird. In Zeile 33 wird die Alignment-Eigenschaft verwendet, um den Inhalt des Bereichs rechtsbündig auszurichten. Dieser Wert kann Left, Center oder Right sein – die Werte stammen aus der Aufzählung HorizontalAlignment. Zum Schluss setzen Sie in Zeile 35 die ShowPanels-Eigenschaft der StatusBar auf true, damit die von Ihnen erzeugten Bereiche dem Benutzer angezeigt werden. Abbildung 4.13 zeigt, wie diese Anwendung nun aussieht. Sie können Ihrer Statusleiste so viele Bereiche hinzufügen, wie Sie wünschen. Nehmen Sie aber nicht zu viele, sonst kann sie der Benutzer nicht alle sehen. Das Steuerelement StatusBarPanel hat eine weitere wichtige Eigenschaft hinsichtlich seiner Anpassung: Icon. Die Eigenschaft lässt sich dazu nutzen, eine Grafik statt bloß Text in der Statusleiste anzuzeigen (wie zum Beispiel die Statusleiste von Word mit einem Symbol den Status Ihres Dokuments anzeigt). Diese Grafikdatei muss eine .ico-Datei sein. Durch das Hinzufügen des folgenden Codes zur AddStatusBar-Methode in Listing 4.8 erhält man einen neuen Bereich: dim sbpIcon as StausBarPanel = sbrMain.Panels.Add("") sbpIcon.AutoSize = StatusBarPanelAutoSize.Contents sbpIcon.Icon = new Icon("VSProjectApplication.ico")

135

Menüs und Symbolleisten

Abbildung 4.13: Ihre erste Statusleiste enthält eine Meldung sowie die Uhrzeit und das Datum. Event-driven Start application

Enter message loop

Do processing

User input

If user input is “stop” then

Stop

Abbildung 4.14: Die Nachrichtenschleife wartet auf eine Benutzereingabe.

Bildlaufleisten Bildlaufleisten erlauben es Ihnen (und Ihren Benutzern), sich im Formular zu bewegen, wenn die darin enthaltenen Steuerelemente zu groß sind, um in das aktuelle Fenster zu passen. Es gibt zwei Arten von Bildlaufleisten: horizontale (verschieben das Formular zwischen linkem und rechtem Rand), dargestellt durch das HScrollBar-Steuerelement, und vertikale (bewegen das Formular auf und ab, dargestellt durch VScrollBar. Bildlaufleisten lassen sich Ihrem Formular sehr leicht hinzufügen, aber es ist ein wenig schwieriger, sie zum Funktionieren zu bringen. Um zu erfahren, wie Sie Bildlaufleisten verwenden, sehen Sie sich bitte den Code in Listing 4.9 an (eine modifizierte Version von Listing 3.4).

136

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Listing 4.9: Bildlaufleisten implementieren 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day4 { public class Listing49 : Form { Button btAccept = new Button(); Button btCancel = new Button(); Label lblMessage = new Label(); VScrollBar vbarForm = new VScrollBar(); HScrollBar hbarForm = new HScrollBar(); public Listing49() { lblMessage.Location = new Point(75,150); lblMessage.Width = 200; btAccept.Location = new Point(100,25); btAccept.Text = "OK "; btAccept.Click += new EventHandler(this.AcceptIt); btCancel.Location = new Point(100,100); btCancel.Text = "Abbrechen"; btCancel.Click += new EventHandler(this.CancelIt); vbarForm.Dock = DockStyle.Right; vbarForm.Visible = false; hbarForm.Dock = DockStyle.Bottom; hbarForm.Visible = false; this.Resize += new EventHandler(this.Resized); this.AcceptButton = btAccept; this.CancelButton = btCancel; this.Text = "Beispiel für OK- und Abbrechen-Schaltflächen"; this.Height = 200; this.Controls.Add(lblMessage); this.Controls.Add(btAccept); this.Controls.Add(btCancel); this.Controls.Add(vbarForm); this.Controls.Add(hbarForm); } public void Resized(Object Sender, EventArgs e) { if (this.Height < lblMessage.Top + lblMessage.Height) {

137

Menüs und Symbolleisten

45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72:

vbarForm.Visible = true; }else { vbarForm.Visible = false; } if (this.Width < btAccept.Left + btAccept.Width) { hbarForm.Visible = true; }else { hbarForm.Visible = false; } this.Refresh(); } public void AcceptIt(Object Sender, EventArgs e) { lblMessage.Text = "Der OK-Button wurde gedrückt"; } public void CancelIt(Object Sender, EventArgs e) { lblMessage.Text = "Der Abbrechen-Button wurde gedrückt"; } } public class StartForm { public static void Main() { Application.Run(new Listing49()); } } }

Hier gibt es zwar eine Menge Code zu lesen, doch das meiste davon haben wir bereits gestern besprochen. Die Zeilen 15 bis 24 erzeugen und zeigen ein Labelund zwei Schaltflächen-Steuerelemente an. Die Zeilen 32 bis 38 fügen diese Steuerelemente dem Formular hinzu, und die Zeilen 58 bis 65 behandeln die Ereignisse der Schaltflächen. (Mehr Details finden sich nach Listing 3.4.) Die Zeilen 11 und 12 enthalten den ersten neuen Code; sie erzeugen neue VScrollBarund HScrollBar-Steuerelemente. Die Zeilen 26 bis 29 legen die Dock- und Visible-Eigenschaften fest. Da wir wissen, dass das Formular groß genug sein wird, um seine Inhalte komplett anzuzeigen, wollen wir die Bildlaufleisten zunächst einmal unsichtbar lassen. Um die Dock-Eigenschaft kümmern wir uns an Tag 6. Halten wir erst einmal fest, dass unsere Bildlaufleisten am rechten und am unteren Rand unseres Formulars angedockt sind.

138

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

In Zeile 31 behandeln Sie ein neues Ereignis des Form-Objekts: Resize. Dieses Ereignis wird natürlich ausgelöst, sobald die Größe des Formulars geändert wird. Zeile 31 besagt: Sobald dies passiert, führe die Methode Resized aus, wie in den Zeilen 43 bis 56 zu sehen. Diese Methode muss zwei Dinge tun: Erstens muss sie bei einer Größenänderung die Größe des Formulars bewerten (waagrecht wie senkrecht) und bestimmen, ob das Formular groß genug zur Darstellung seiner Inhalte ist. Ist das nicht der Fall, zeigt sie die Bildlaufleisten an. Zweitens muss sie die Refresh-Methode aufrufen, um das Formular neu zu zeichnen, so dass die gerade modifizierten Bildlaufleisten gezeichnet oder gelöscht werden können. Die Mindesthöhe des Formulars vor der Anzeige von vertikalen Bildlaufleisten liegt beim unteren Rand des untersten Steuerelements – in unserem Fall ist dies lblMessage. Die Summe der Top- und Height-Eigenschaften des Bezeichnungsfeldes ergibt die richtige Höhe. Das if-Statement in Zeile 44 führt den Vergleich zwischen der Höhe des Formulars und der Mindesthöhe aus. Ist die Formularhöhe niedriger, zeigt man die vertikale Bildlaufleiste an (Zeile 45). Umgekehrt gilt: Ist die Formularhöhe größer als die Mindesthöhe, verbergen Sie die Bildlaufleiste, wie in Zeile 47 zu sehen. Die Zeilen 50 bis 54 machen das Gleiche für die waagrechte Ausrichtung. In diesem Fall entspricht die Mindestbreite der Left-Eigenschaft des am weitesten rechts liegenden Steuerelements plus dessen Breite (Width). In Zeile 55 rufen Sie schließlich die Refresh-Methode auf. Kompilieren Sie diese Anwendung und versuchen Sie das Formular auf eine Größe zu reduzieren, die geringer ist als die minimalen Begrenzungen. Abbildung 4.14 zeigt ein typisches Beispiel.

Abbildung 4.15: Die richtigen Bildlaufleisten tauchen auf, sobald die Formulargröße geändert wird.

Beachten Sie jedoch, dass auf einen Klick auf die Bildlaufleisten hin nichts passiert! Ihr Formular lässt sich nicht wie vorgesehen bewegen. Es gibt zwei Möglichkeiten, diesen Mangel zu beheben. Die erste und einfachere Möglichkeit besteht im Setzen der AutoScoll-Eigenschaft des Formulars auf true: this.AutoScroll = true;

Ihr Formular lässt sich nun wie erwartet bewegen. Die zweite Möglichkeit besteht in einer manuellen Vorgehensweise, wobei man die Ereignisse der Bildlaufleisten behandelt. Normalerweise nutzt man diese Möglichkeit nur dann, wenn man vorher nicht weiß, wie groß ein Formular werden kann (etwa wenn ein Benutzer die Größe dynamisch steigern kann).

139

Menüs und Symbolleisten

Lassen Sie uns untersuchen, wofür der Bildlauf auszuführen ist, wenn die Bildlaufleisten angeklickt werden. Zunächst würde man vermuten, es sei das Form-Objekt, doch das träfe nicht zu. Denken Sie daran, dass die Position des Formulars auf den Desktop bezogen ist; wenn Sie also einen Bildlauf bezüglich des Formular-Objekts versuchen, bewegt es sich nur auf dem Desktop, doch seine Inhalte würden an ihrem relativen Ort (im Formular) verbleiben. Entgegen unseren Erwartungen würden sich die Steuerelemente nicht bewegen. Wir wollen aber die Steuerelemente innerhalb des Formulars bewegen, nicht aber das Formular selbst. Wenn also der Benutzer den Abwärtspfeil auf der senkrechten Bildlaufleiste anklickt, müssen sich alle Steuerelemente nach oben bewegen. Das Entsprechende gilt, wenn der Benutzer in der waagrechten Bildlaufleiste nach rechts klickt: Alle Steuerelemente müssen sich nach links bewegen. Wenn man, wie wir in unserem Beispiel, nur drei Steuerelemente im Formular hat, hält sich der Aufwand in Grenzen. Was ist aber, wenn man mehr hat? Die Neupositionierung jedes einzelnen Steuerelements wird zur Schwerstarbeit. Unser Kniff besteht in der Verwendung eines weiteren Containersteuerelements: ein Panel-Steuerelement, das alle Steuerelemente enthält, die bewegt werden müssen. Dann können Sie das PanelSteuerelement bewegen, und alle seine untergeordneten Steuerelemente werden sich entsprechend mitbewegen! Stellen Sie sich das Panel-Steuerelement als eine Art Mini-Form vor oder als Formular innerhalb eines Form-Objekts. Indem Sie die Steuerelemente in einem Panel-Steuerelement gruppieren, können Sie sie als eine Gruppe manipulieren – das ist genau das, was Sie ja beim Bildlauf erreichen wollen. Das Erzeugen des Panel-Steuerelements ist einfach; fügen Sie den folgenden Code an einer beliebigen Stelle zwischen den Zeilen 8 und 13 in Listing 4.9 ein: Panel pnlForm =new Panel();

Sie wollen sicher dafür sorgen, dass das Panel-Steuerelement die gleiche Größe besitzt wie das Formular, so dass alle Steuerelemente normal angezeigt werden. Fügen Sie dem Konstruktor folgenden Code hinzu: pnlForm.Height =this.Height; pnlForm.Width =this.Width;

Als Nächstes ersetzen Sie die Zeilen 36 bis 38 durch folgenden Code: pnlForm.Controls.Add(lblMessage); pnlForm.Controls.Add(btAccept); pnlForm.Controls.Add(btCancel);

Jetzt sind das Label- und die beiden Button-Steuerelemente nicht mehr mit dem FormObjekt verknüpft, sondern stattdessen mit dem Panel-Steuerelement. Als Nächstes fügen Sie das Panel-Steuerelement dem Konstruktor des Form-Objekts hinzu: this.Controls.Add(pnlForm);

140

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Um die Bildlaufereignisse zu behandeln, müssen Sie im Konstruktor Ereignishandler zum Scroll-Ereignis der Bildlaufleisten hinzufügen, etwa so: vbarForm.Scroll +=new ScrollEventHandler(this.HandleVScroll); hbarForm.Scroll +=new ScrollEventHandler(this.HandleHScroll);

Beachten Sie die Verwendung des Objekts ScrollEventHandler. Dies bedeutet, dass Sie ein Parameterobjekt ScrollEventArgs für Ihre Methoden verwenden. Zum Schluss müssen Sie noch die Methoden HandleVScroll und HandleHScroll erstellen, wie in Listing 4.10 gezeigt: Listing 4.10: Methoden zur Behandlung des Bildlaufs 1: 2: 3: 4: 5: 6: 7:

public void HandleVScroll(Object Sender,ScrollEventArgs e){ pnlForm.Top =0 -e.NewValue; } public void HandleHScroll(Object Sender,ScrollEventArgs e){ pnlForm.Left =0 -e.NewValue; }

Diese zwei Methoden verrichten einfache Aufgaben: Sie bewegen das Panel-Steuerelement auf- oder abwärts und nach links oder rechts, wobei die entsprechenden Top- und Left-Eigenschaften des Panel-Steuerelements verwendet werden. Die Schwierigkeit besteht nun darin, wie man herausfindet, wie weit das Panel-Steuerelement in die jeweilige Richtung bewegt werden soll. Das Panel-Steuerelement sollte stets in Bezug zu seiner Startposition bewegt werden. Wenn die Bildlaufleiste beispielsweise um x Raumeinheiten abwärts bewegt wird, sollte sich das Panel-Steuerelement von seiner ursprünglichen Position aus um x Raumeinheiten aufwärts bewegen. In diesem Fall entspricht die Anfangsposition des Panel-Steuerelements den X-Y-Koordinaten (0,0), was der Grund dafür ist, warum Sie diese Nullen in den Zeilen 2 und 6 sehen. Von dieser Anfangsposition ziehen Sie den Betrag ab, um den sich die Bildlaufleiste bewegt hat. Durch das Subtrahieren stellen Sie sicher, dass sich das Panel-Steuerelement in die entgegengesetzte Richtung des Bildlaufs bewegt (der Benutzer klickt »abwärts«, also bewegen sich die Steuerelemente aufwärts). Tabelle 4.1 zeigt ein paar beispielhafte Situationen. Aktion

Ergebnisse

1. Der Benutzer klickt den AufwärtsPfeil um zehn Einheiten (Wert der Bildlaufleiste = 10)

Das Panel-Steuerelement bewegt sich 10 Einheiten abwärts. Der neue Panel.Top-Wert lautet –10, Panel.Left = 0.

Tabelle 4.1: Beispiele für Bewegungen beim Bildlauf

141

Menüs und Symbolleisten

Aktion

Ergebnisse

2. Der Benutzer verschiebt die Bildlaufleiste bis ganz nach unten (Wert der Bildlaufleiste = Form.Height)

Das Panel-Steuerelement bewegt sich ganz nach oben. Neuer Panel.Top-Wert = 0 – Form.Height.Left = 0. (Beachten Sie, dass sich Bewegungen nicht akkumulieren, da die Bewegung nur relativ zur Ausgangsposition gemessen wird. Das heißt, der Top-Wert in diesem Schritt ist nicht –10– Form.Height.)

3. Der Benutzer verschiebt die BildDas Panel-Steuerelement bewegt sich um 20 Einheiten laufleiste um 20 Einheiten nach links. nach rechts. Top = 0 – Form.Height.Left-Wert = –20. Tabelle 4.1: Beispiele für Bewegungen beim Bildlauf (Forts.)

Und welche Einheiten benutzen nun die Bildlaufleisten? Per Voreinstellung ist die Minimum-Eigenschaft auf 0 gesetzt (dies entspricht der Ausgangsposition der Bildlaufleiste; für die senkrechte Bildlaufleiste also ganz oben, für die waagrechte ganz links). Die Vorgabe für Maximum lautet 100, was bedeutet, dass sich die Bildlaufleisten um 100 Einheiten bewegen können, nicht mehr. Sie können sowohl Minimum als auch Maximum ändern, aber der Effekt ist gleich null: Minimum entspricht immer waagrecht ganz links oder senkrecht ganz oben, und Maximum entspricht dem jeweils entgegengesetzten Ende. Die NewValue-Eigenschaft des ScrollEventArgs-Parameters bestimmt die neue Position der Bildlaufleiste (die der Benutzer durch Verschieben herbeigeführt hat). Gemäß den Zeilen 2 und 6 des Listings 4.10 sind ± 100 Einheiten auf- und abwärts, nach links oder rechts der maximale Bewegungsradius des Panel-Steuerelements. Hat ein Formular kompliziertere Anforderungen an seine Größe, müssen Sie vielleicht den Betrag erhöhen, um den sich ein Panel-Steuerelement bewegen kann. Sollte Ihr Formular also 1000 Pixel hoch sein, dann sind für Ihre vertikale Bildlaufleiste ± 100 Einheiten nicht ausreichend. Somit sollte jede Einser-Einheit der Bildlaufleiste sich um mehr als nur eine Einheit der Höhe des Panel-Steuerelements bewegen. Der folgende Code könnte die notwendige Normalisierung erzielen: int normalizedHeight = pnlForm.Height / vbarForm.Maximum; pnlForm.Top = 0 – (e.NewValue * normalizedHeight);

Wenn Sie die Zeilen 2 und 6 des Listings 4.10 entfernen, erhalten Sie das zuvor besprochene Beispiel, bei dem sich das Formular über den Desktop bewegt.

Formulare sind nicht die einzigen Objekte, die über Bildlaufleisten verfügen können; Sie können beinahe jedem gewünschten Steuerelement Bildlaufleisten hinzufügen, so etwa einer PictureBox oder sogar Schaltflächen. Dies stellt Ihnen vielfältige Entwurfsmöglichkeiten hinsichtlich der Interaktion Ihrer Anwendung mit dem Benutzer zur Verfügung.

142

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

Wir wollen noch weitere Eigenschaften betrachten, die Sie im Zusammenhang mit Bildlaufleisten nutzen können. Die Eigenschaften LargeChange und SmallChange lassen sich dazu verwenden, die Anzahl Einheiten, um die sich die Bildlaufleiste nach dem Anklicken bewegt, anzupassen. SmallChange wird meist verwendet, wenn der Benutzer auf einen der Pfeile am Ende der Bildlaufleiste klickt oder eine der (½)- bzw. (¼)-Pfeiltasten drückt. LargeChange wird benutzt, wenn die Bildlaufleiste selbst angeklickt wird oder die (Bild½)und (Bild¼)-Pfeiltasten gedrückt werden. Die Type-Eigenschaft des Parameterobjekts ScrollEventArgs teilt Ihnen mit, was tatsächlich während des Bildlaufereignisses passiert ist, also etwa eine große oder kleine Bewegung oder wohin sich die Bildlaufleiste bewegt hat. Tabelle 4.2 führt diese Werte auf, von denen alle zur Aufzählung ScrollEventType gehören. Wert

Beschreibung

EndScroll

Das Bildlauffeld (Scroll-Box; das Kästchen, das die Position der Bildlaufleiste anzeigt) hat aufgehört sich zu bewegen.

First

Das Bildlauffeld hat sich zum Minimum-Wert bewegt.

LargeDecrement

Das Bildlauffeld hat sich um den LargeChange-Wert nach oben oder nach links bewegt (die Inhalte verschieben sich dann nach unten oder nach rechts).

LargeIncrement

Das Bildlauffeld hat sich um den LargeChange-Wert nach unten oder nach rechts bewegt (die Inhalte verschieben sich dann nach oben oder nach links).

Last

Das Bildlauffeld hat sich zu dem durch Maximum angegebenen Wert bewegt.

SmallDecrement

Das Bildlauffeld hat sich um den SmallChange-Wert nach oben oder nach links bewegt (die Inhalte verschieben sich dann nach unten oder nach rechts).

SmallIncrement

Das Bildlauffeld hat sich um den SmallChange-Wert nach unten oder nach rechts bewegt (die Inhalte verschieben sich dann nach oben oder nach links).

ThumbPosition

Das Bildlauffeld selbst wurde bewegt.

ThumbTrack

Das Bildlauffeld bewegt sich noch.

Tabelle 4.2: Werte für ScrollEventType

Listing 4.11 zeigt ein kurzes Beispiel (zumindest im Hinblick auf die Komplexität) für die Verwendung dieser Werte.

143

Menüs und Symbolleisten

Listing 4.11: So stellt man den Typ des aufgetretenen Bildlaufs fest. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:

144

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day4 { public class Listing410 : Form { Label lblMessage = new Label(); VScrollBar vbarForm = new VScrollBar(); HScrollBar hbarForm = new HScrollBar(); public Listing410(){ lblMessage.Location = new Point(75,75); lblMessage.Width = 200; vbarForm.Dock = DockStyle.Right; vbarForm.Visible = true; vbarForm.Scroll += new ScrollEventHandler (this.HandleScroll); hbarForm.Dock = DockStyle.Bottom; hbarForm.Visible = true; hbarForm.Scroll += new ScrollEventHandler (this.HandleScroll); this.Text = "Beispiel für ScrollEventType-Wert"; this.Height = 200; this.Controls.Add(lblMessage); this.Controls.Add(vbarForm); this.Controls.Add(hbarForm); } public void HandleScroll(Object Sender, ScrollEventArgs e){ switch(e.Type){ case ScrollEventType.EndScroll: lblMessage.Text = "Die Scroll-Box bewegt sich nicht mehr"; break; case ScrollEventType.First: lblMessage.Text = "Die Scroll-Box befindet sich bei " + ((ScrollBar)Sender).Minimum.ToString(); break; case ScrollEventType.LargeDecrement: lblMessage.Text = "Die Scroll-Box hat sich bewegt –" + ((ScrollBar)Sender).LargeChange.ToString(); break; case ScrollEventType.LargeIncrement: lblMessage.Text = "Die Scroll-Box hat sich bewegt" + ((ScrollBar)Sender).LargeChange.ToString();

Symbolleisten, Statusleisten und Bildlaufleisten hinzufügen

43: 44: 45:

break; case ScrollEventType.Last: lblMessage.Text = "Die Scroll-Box befindet sich bei " + ((ScrollBar)Sender).Maximum.ToString(); break; case ScrollEventType.SmallDecrement: lblMessage.Text = "Die Scroll-Box hat sich bewegt –" + ((ScrollBar)Sender).SmallChange.ToString(); break; case ScrollEventType.SmallIncrement: lblMessage.Text = "Die Scroll-Box hat sich bewegt " + ((ScrollBar)Sender).SmallChange.ToString(); break; case ScrollEventType.ThumbPosition: lblMessage.Text = "Die Scroll-Box hat sich bewegt "; break; case ScrollEventType.ThumbTrack: lblMessage.Text = "Die Scroll-Box bewegt sich"; break; }

46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68:

} } public class StartForm { public static void Main(){ Application.Run(new Listing410()); } } }

Der Konstruktor dürfte recht vertraut aussehen; er erzeugt einfach die Bildlaufleisten und ein Bezeichnungsfeld, danach fügt er die Steuerelemente dem Formular hinzu. Der Scroll-Ereignishandler namens HandleScroll sieht dagegen schon wesentlich interessanter aus. Diese in Zeile 30 beginnende Methode wertet die Type-Eigenschaft des Parameters ScrollEventArgs aus. Mit Hilfe eines switch-Statements vergleicht sie den Type-Wert mit jedem der ScrollEventType-Werte, die in Tabelle 4.2 aufgeführt sind. Für jeden Einzelfall gibt sie eine entsprechende Zeichenfolge an das Label-Steuerelement aus. Zu diesem Listing sind ein paar Bemerkungen angebracht. Erstens müssen Sie die Variablen in den Zeilen 36, 39, 42, 45, 48 und 51 konvertieren. Die Variable Sender ist ein Object-Typ (wie durch die Methodendeklaration in Zeile 30 angezeigt wird). Wir wissen zwei Dinge über dieses Object: Es repräsentiert die angeklickte Bildlaufleiste (warum, verrate ich morgen) und sollte in dieser

145

Menüs und Symbolleisten

Eigenschaft die verschiedenen Bildlaufleisten -Eigenschaften enthalten, darunter Minimum, Maximum usw. Ein Objekt hat jedoch diese Eigenschaft nicht, weshalb Sie es in den richtigen Typ – ScrollBar – umwandeln müssen, damit Sie die richtigen Eigenschaft erreichen können. (Mehr dazu morgen.) Als Zweites ist festzustellen, dass Sie in ein ScrollBar- und nicht in ein VScrollBaroder HScrollBar-Steuerelement umwandeln müssen (die beiden Letzteren erben ihre Eigenschaften vom ScrollBar-Steuerelement). Daher ist es unerheblich, ob das angeklickte Steuerelement nun ein VScrollBar- oder HScrollbar-Steuerelement ist, denn alles, was wir benötigen, ist der Zugang zu den entsprechenden Eigenschaften – welche die ScrollBar-Klasse bereitstellt. (Mehr dazu morgen.) Abbildung 4.15 zeigt dieses Beispiel, nachdem die Bildlaufleiste unterhalb des Bildlauffeldes angeklickt wurde.

Abbildung 4.16: Wenn man unterhalb des Bildlauffeldes klickt, wird ein Bildlauf um den LargeChange-Wert ausgeführt.

4.4

Zusammenfassung

Sie haben heute etwas über drei wichtige und häufig eingesetzte Arten von Steuerelementen gelernt: Menüs, Symbolleisten und Bildlaufleisten. Hinsichtlich ihrer Funktion sind sie sehr ähnlich und teilen sich sogar viele ähnliche Eigenschaften, doch hinsichtlich ihrer Implementierung sind sie völlig verschieden voneinander. Ein Steuerelement in Windows Forms ist ein Objekt, das dem Benutzer eine Schnittstelle präsentiert, mit der häufig eine Interaktion möglich ist. Es gibt unterschiedliche Arten von Steuerelementen, darunter Containersteuerelemente, Menüs und Schaltflächen (Buttons). Menüs lassen sich durch die Steuerelemente MainMenu und MenuItem darstellen. Das MainMenu nimmt Menüelemente (MenuItems) auf, welche wiederum die Beschriftungen präsentieren, die der Benutzer sieht und mit denen er in Menüs interagiert. Kontextmenüs, die sich öffnen, wenn der Benutzer auf ein Steuerelement rechtsklickt, verwenden ebenfalls MenuItem-Steuerelemente, setzen aber MainMenu anstelle von ContextMenu ein.

146

Fragen und Antworten

Eine Symbolleiste ähnelt einem Menü, nur dass man ein ToolBar-Steuerelement als übergeordnetes Objekt einsetzt und die ToolBarButton-Steuerelemente als untergeordnete Steuerelemente (ToolBarButtons können sogar MenuItem-Steuerelemente als untergeordnete Steuerelemente haben, wie es bei Dropdown-Schaltflächen der Fall ist). Von den beiden kann nur das ToolBar-Steuerelement verwendbare Ereignisse haben, so dass jeder Klick auf eine Schaltfläche durch seinen Ereignishandler zu behandeln ist – üblicherweise mit einem switch- oder Select Case-Statement. Bei der Statusleiste handelt es um eine weitere Containerart. Sie verwendet die Steuerelemente StatusBar und StatusBarPanel. In der Regel sind Statusleisten nicht interaktiv, sondern zeigen lediglich Informationen an. Sowohl StatusBar als auch StatusBarPanel verfügen über Eigenschaften, die sich zur Informationsanzeige nutzen lassen, doch das normale Vorgehen sieht so aus, dass man nur das StatusBarPanel-Steuerelement als Anzeigemechanismus verwendet und das Steuerelement StatusBar als Container. Bildlaufleisten schließlich gibt es in zwei Varianten: HScrollBar und VScrollBar stellen waagrechte bzw. senkrechte Bildlaufleisten dar. Sie haben erfahren, dass es recht leicht ist, sie Ihrem Formular hinzuzufügen, dass jedoch die Behandlung ihrer Ereignisse eine sorgfältige Planung erfordert – insbesondere dann, wenn man herauszufinden versucht, für welche Elemente, um wie viele Einheiten und in welche Richtung der Bildlauf stattfinden soll. Beide Steuerelemente erben von der Basisklasse ScrollBar.

4.5 F

Gibt es ein Ereignis, das signalisiert, dass sich gleich ein Menü öffnen wird? A

F

Fragen und Antworten Ja, es gibt ein solches Ereignis: PopUp. Wenn Sie eine Aktion direkt vor dem Eintreten dieses Ereignisses ausführen lassen wollen – etwa das Verbergen oder Anzeigen von Menüelementen –können Sie für das Ereignis PopUp einen Handler erstellen.

Inwiefern unterscheidet sich die NewValue-Eigenschaft des ScrollEventArgs-Objekts von der Value-Eigenschaft? A

NewValue gibt den Wert an, um den sich die Bildlaufleiste während eines Ereignisses ändert. Value hingegen gibt den statischen Wert an. Häufig sind diese Werte identisch, manchmal aber auch nicht.

Wenn beispielsweise die Bildlaufleiste oder die Pfeile angeklickt werden, sind diese Werte identisch. Wenn jedoch das Bildlauffeld selbst mit der Maus bewegt wird, dann ist Value der vorhergehende Wert, während NewValue der aktuellen Position entspricht. Sobald das Bildlauffeld wieder losgelassen wird, nehmen beide Werte wieder den gleichen Wert an.

147

Menüs und Symbolleisten

4.6

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wahr oder falsch? Alle Steuerelemente inklusive des Form-Objekts erben von der Klasse Control. 2. Welches Objekt muss mit Symbolleisten verknüpft sein, damit Grafiken in den Symbolleistenschaltflächen angezeigt werden? 3. Wie heißen die drei optionalen Parameter für einen MenuItem-Konstruktor, und welches sind ihre Typen? 4. Welches Zeichen wird verwendet, um eine Tastenkombination für einen Buchstaben in der Beschriftung eines Menüelements bereitzustellen? 5. Schreiben Sie eine Zeile C#-Code, die ein ToolBarButton-Steuerelement namens MyFirstButton anweist, die vierte Grafik in der zugeordneten ImageList zu verwenden. 6. Wahr oder falsch? Das Ereignis, das für die Behandlung von Mausklicks auf Symbolleistenschaltflächen verwendet wird, heißt Click. 7. Welche sind die Standardwerte für die Eigenschaften Minimum, Maximum, SmallChange und LargeChange einer Bildlaufleiste?

Übung Erstellen Sie in VB .NET eine Anwendung, die ein personalisiertes Menü verwendet wie jene, die mit Microsoft Office 2000 und Windows 2000 eingeführt wurden. Diese Menüs zeigen nur die zuletzt benutzten Menüelemente, während sie die anderen verbergen und es dem Benutzer gestatten, auf einen Pfeil zu klicken, um weniger häufig ausgewählte Menüelemente anzuzeigen. Ihre Anwendung sollte ein Menü anbieten, das verborgene Menüelemente besitzt. Wird ein bestimmtes Menüelement angeklickt, sollen die verborgenen Elemente angezeigt werden. Kümmern Sie sich nicht um das Hinzufügen von Ereignishandlern für jedes Menüelement.

148

Workshop

Ihr Menü kann recht einfach sein: Es braucht sich nicht daran zu erinnern, welche Menüelemente der Benutzer am häufigsten auswählt, und es wird keine verborgenen Menüelemente anzeigen, nur weil der Mauspfeil über dem Menü schwebt. Sobald der Benutzer auf die Schaltfläche WEITERE... (More) klickt, muss er das Menü erneut öffnen, um die bislang verborgenen Elemente sehen zu können. (Sie werden sehen, wie man fortgeschrittenere Funktionen verwenden kann, wenn Sie an Tag 13 etwas über GDI+ erfahren.

149

Ereignisse in Windows Forms

5

Ereignisse in Windows Forms

Einer der wichtigsten Aspekte an einer Anwendung ist sicherlich – von der visuellen Oberfläche abgesehen – die Art und Weise, wie sie mit dem Benutzer interagiert. Wie Sie bereits wissen, wird diese Interaktion durch Ereignisse und deren Handler realisiert. Ein Ereignis ist das Resultat einer Aktion, also einer Meldung, die erzeugt wurde, um das Auftreten einer Aktion zu signalisieren. Die Aktion kann vom Benutzer oder von der Anwendung selbst verursacht worden sein. Ein Ereignishandler ist jene Methode, die reagiert, sobald ein Ereignis auftritt. Heute befassen wir uns mit Ereignissen und ihren Methoden, was sie für Ihre Anwendung bedeuten und warum man sie benötigt. Ich werde Ihnen eine neue Art von Klasse namens Delegate vorstellen, die Ihrer Anwendung bei der Ereignisbehandlung hilft. Heute lernen Sie, 쐽

was man in technischer Hinsicht unter einem Ereignishandler versteht,



wie Sie Ihre Ereignisse miteinander »verdrahten«,



wie Sie Delegaten verwenden,



wie Sie Ihre eigenen Ereignisse und Ereignisparameter erzeugen,



was jeder Parameter eines Ereignishandlers bewirkt, woher er stammt und wie man ihn einsetzt.

5.1

Was sind Ereignishandler?

Aus den vorausgegangenen Kapiteln verfügen Sie bereits über eine gewisse Erfahrung im Umgang mit Ereignissen. Sie wissen, was ein Ereignis ist und ungefähr, was ein Ereignishandler ist. Nun geht es um Letzteren. Um Ihre Anwendung zu einer Reaktion auf ein Ereignis zu veranlassen, müssen Sie einen Ereignishandler erstellen. Dieses Stück Code wird nur dann ausgeführt, wenn ein Ereignis eintritt. Wenn Sie beispielsweise Ihr Auto starten wollen, muss zwischen dem Gaspedal, dem Motor und dem Benzintank eine gewisse Verbindung bestehen, sonst erfolgt keine Aktion: es ist eine Verknüpfung nötig. Das Gleiche lässt sich für Ereignisse in .NET sagen. Eine Aktion mag erfolgen und ein Ereignis passieren, doch sofern es keine Verbindung dazwischen gibt, wird nichts stattfinden. Dementsprechend nennt man den Vorgang der Erstellung eines Ereignishandlers für ein Ereignis auch die Verknüpfung eines Ereignisses.

152

Was sind Ereignishandler?

Ereignisse behandeln Wir wollen uns zunächst eine Anwendung ansehen, die einige aus Menüelementen erzeugte Ereignisse verarbeitet, wie in Listing 5.1 zu sehen. Listing 5.1: Mit Ereignishandlern arbeiten 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: Namespace TYWinForms.Day5 6: 7: public class Listing51 : Inherits Form 8: private mnuFont as new MainMenu 9: private lblMessage as new Label 10: 11: public sub New() 12: Me.Text = "Listing 5.1 " 13: Me.Menu = mnuFont 14: 15: lblMessage.Text = "Testen von Ereignissen" 16: lblMessage.Location = new Point(75,75) 17: lblMessage.Height = 50 18: lblMessage.Width = 150 19: lblMessage.BackColor = Color.LightBlue 20: 21: dim miFont as MenuItem = mnuFont.MenuItems.Add("Font") 22: dim miTimes as MenuItem = miFont.MenuItems.Add("Times Roman ") 23: AddHandler miTimes.Click,new EventHandler(AddressOf  Me.TimesClicked) 24: 25: dim miArial as MenuItem = miFont.MenuItems.Add("Arial") 26: AddHandler miArial.Click, new EventHandler(AddressOf  Me.ArialClicked) 27: 28: dim miWing as MenuItem = miFont.MenuItems.Add("Wingdings") 29: AddHandler miWing.Click, new EventHandler(AddressOf Me.WingClicked) 30: 31: Me.Controls.Add(lblMessage) 32: End Sub 33: 34: public sub TimesClicked(Sender as Object, e as EventArgs) 35: lblMessage.Font = new Font("Times",15) 36: mnuFont.MenuItems(0).MenuItems(0).Checked = True

153

Ereignisse in Windows Forms

37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60:

mnuFont.MenuItems(0).MenuItems(1).Checked = False mnuFont.MenuItems(0).MenuItems(2).Checked = False end sub public sub ArialClicked(Sender as Object, e as lblMessage.Font = new Font("Arial", 15) mnuFont.MenuItems(0).MenuItems(0).Checked = mnuFont.MenuItems(0).MenuItems(1).Checked = mnuFont.MenuItems(0).MenuItems(2).Checked = end sub

EventArgs) False True False

public sub WingClicked(Sender as Object, e as EventArgs) lblMessage.Font = new Font("Wingdings",15) mnuFont.MenuItems(0).MenuItems(0).Checked = False mnuFont.MenuItems(0).MenuItems(1).Checked = False mnuFont.MenuItems(0).MenuItems(2).Checked = True end sub public Shared Sub Main() Application.Run(new Listing51) end sub end class End Namespace

Diese Anwendung zeigt etwas Text an und erlaubt dem Benutzer, die Schriftart – den Font – mit Hilfe eines Menüs zu ändern. Zunächst einmal die Grundelemente: Die Zeilen 1 bis 3 importieren die zu verwendenden Namensräume. In Zeile 8 und 9 werden die beiden Hauptsteuerelemente der Anwendung deklariert: ein MainMenu und ein Label. In Zeile 11 beginnt der Konstruktor für die Windows Forms-Klasse. Die Zeilen 15 bis 19 initialisieren die Eigenschaft des Label-Steuerelements, wobei sie seine Größe, Position und Text-Eigenschaften festlegen. Der nächste Codeabschnitt dürfte noch von gestern vertraut sein. Zeile 21 fügt dem MainMenu-Objekt ein Menüelement mit der Aufschrift »Font« hinzu. Dieses erste Menüelement wird die Kopfzeile des ganzen Menüs. Zeile 22 fügt dieser Kopfzeile ein Menüelement hinzu, nämlich die erste unserer zur Auswahl stehenden Schriftarten. Zeile 23 verwendet die AddHandler-Methode, um dem Click-Ereignis des Menüelements einen Ereignishandler hinzuzufügen. Das Click-Ereignis wird stets ausgelöst, wenn das zugehörige Menüelement angeklickt wird. Diese Zeile benachrichtigt die Anwendung von einem Ereignishandler namens TimesClicked, der für dieses Ereignis zu verwenden ist (mehr dazu heute im Abschnitt »Delegaten«). Die Zeilen 25 bis 29 erledigen die gleiche Aufgabe wie die Zeilen 22 und 23, nämlich weitere Menüelemente für weitere Fontoptionen zu erstellen (Arial und Wingdings) und den

154

Was sind Ereignishandler?

Click-Ereignissen dieser Menüelemente Ereignishandler zuzuweisen. Diese Zeilen ver-

knüpfen also die Ereignisse. Sobald eines dieser Menüelemente angeklickt wird, beginnt die Ausführung der jeweiligen Methode (TimesClicked, ArialClicked oder WingClicked). Dieser Ablauf ist recht simpel, obwohl der Code vielleicht ein wenig seltsam aussehen mag. Die TimesClicked-Methode in Zeile 34 ändert die Schriftart des Label-Steuerelements in Zeile 35. An Tag 3 haben wir untersucht, wie man Schriftarten mit Hilfe der Objekte Font und FontFamily ändert. Heute lernen wir eine neue Vorgehensweise. Statt das FontFamilyObjekt einzusetzen, übergeben wir einfach eine Zeichenfolge mit der Schriftart, zu der wir wechseln möchten – in diesem Fall Times. Der zweite Parameter ist wieder die anzuzeigende Schriftgröße. Um den Benutzer wissen zu lassen, welche Schriftart gerade benutzt wird, wollen wir die Menüelemente miTimes, miArial oder miWing mit Häkchen versehen, sobald sie jeweils ausgewählt sind. Die folgende Zeile bedeutet, dass das erste MenuItem, das dem MainMenuObjekt mnuFont hinzugefügt wurde, ein Häkchen erhalten soll – anders ausgedrückt, das miTimes-Menüelement: mnuFont.MenuItes(0).MenuItems(0).Checked = True

Die nächsten beiden Zeilen (37 und 38) verweisen auf die Elemente miArial und miWing (die zweiten und dritten Menüelemente, die nach dem ersten MenuItem dem MainMenuObjekt mnuFont hinzugefügt wurden). Da diese Optionen offensichtlich gerade nicht ausgewählt sind, sollten sie kein Häkchen aufweisen und ihre Checked-Werte auf False lauten: mnuFont.MenuItes(0).MenuItems(1).Checked = False mnuFont.MenuItes(0).MenuItems(2).Checked = False

Da Sie nicht wissen, welches dieser Menüelemente vorher mit Häkchen versehen war, setzen Sie beide auf False, um sicher zu gehen. Die ArialClicked- und WingClicked-Methoden in Zeile 41 bzw. 48 machen das Gleiche wie die TimesClicked-Methode, nur dass sie natürlich die Arial- bzw. Wingdings-Schriftarten anstelle von Times Roman verwenden. Zeile 55 schließlich deklariert den Eintrittspunkt zur Anwendung, nämlich die MainMethode. Sie ruft einfach die Run-Methode auf. Abbildung 5.1 zeigt diese Anwendung in Aktion. Lassen Sie uns kurz zur TimesClicked-Methode zurückkehren. Bitte beachten Sie die Signatur dieses Ereignishandlers: public sub TimesClicked(Sender as Object, e as EventArgs)

Es handelt sich hierbei um die Standardsignatur eines Ereignishandlers (anders ausgedrückt: seine Deklaration). Alle Ereignishandler weisen beinahe identische Signaturen auf. Es ändert sich jeweils nur der Methodenname (hier also TimesClicked) und in seltenen Fällen auch der abschließende Parametertyp EventArgs (Näheres dazu im Abschnitt »Delegaten«).

155

Ereignisse in Windows Forms

Abbildung 5.1: Die Schriftart ändert sich als Reaktion auf Ereignisse.

Der erste Parameter, Sender, stellt das das Ereignis erzeugende Objekt dar. In Listing 5.1 wäre dies das MenuItem-Objekt miTimes. Der zweite Parameter enthält jede nähere Information über das Ereignis. Diese Parameter werden eingesetzt, um den Ereignishandler bei seiner Aufgabe zu unterstützen. Meistens werden Sie diese Standardsignatur für alle Ihre Ereignishandler benutzen, wodurch sie nicht nur sehr leicht zu schreiben, sondern auch leicht aus bereits geschriebenem Code herauszugreifen sind. Dies war ein einfaches Beispiel, wie man Ereignishandler verwenden kann. Wir haben pro Ereignis, das uns interessierte, einen Ereignishandler erzeugt, sozusagen einen Draht pro Ereignis. Für alle anderen Ereignisse erzeugen wir keine Handler, wodurch diese praktisch ignoriert werden.

Mehrfache Ereignisse behandeln Ein sehr interessantes .NET-Merkmal besteht darin, dass ein Ereignis nicht unbedingt auch seinen eigenen Ereignishandler haben muss: Die Ereignisse können Handler gemeinsam nutzen! Eine Verknüpfung kann von einem Handler zu mehreren Ereignissen bestehen. Übertragen auf unseren Auto-Vergleich, hieße das, dass sich der Wagen auf mehrere Arten in Bewegung versetzen lässt, so etwa durch Anschieben oder durch das Loslassen der Bremse an einem abschüssigen Hügel. Da aber so viele Ereignisse einen Ereignishandler auslösen können, ist es umso wichtiger herauszufinden, welches Objekt das Ereignis erzeugt hat. An diesem Punkt kommt der Einsatz der Parameter eines Ereignishandlers unseren Wünschen entgegen. Listing 5.2 zeigt ein Beispiel eines mehrere Ereignisse verarbeitenden Ereignishandlers. Fertigen Sie von Listing 5.1 eine Kopie an und ersetzen Sie die Zeilen 34 bis 53 durch den Code in diesem Listing.

156

Was sind Ereignishandler?

Listing 5.2: Die Behandlung mehrerer Ereignisse mit nur einem Handler 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:

public sub FontClicked(Sender as Object, e as EventArgs) dim item as MenuItem if Sender is mnuFont.MenuItems(0).MenuItems(0)then lblMessage.Font = new Font("Times ", 15) elseif Sender is mnuFont.MenuItems(0).MenuItems(1)then lblMessage.Font = new Font("Arial ", 15) elseif Sender is mnuFont.MenuItems(0).MenuItems(2)then lblMessage.Font = new Font("Wingdings ", 15) end if for each item in mnuFont.MenuItems(0).MenuItems item.Checked = False next item CType(Sender,MenuItem).Checked = True end sub

Die FontClicked-Methode ersetzt die Methoden TimesClicked, ArialClicked und WingClicked aus Listing 5.1 – sie erledigt die gleiche Aufgabe, doch auf etwas andere Weise. Beachten Sie die Signatur in Zeile 1: Sie ist immer noch die gleiche Standardsignatur. In Listing 5.1 verwendeten wir keinen dieser Parameter, doch diesmal verwenden wir sie, um zu bestimmen, welches Objekt das Ereignis erzeugt hat. In Zeile 2 deklarieren Sie ein neues MenuItem-Objekt; wir werden es gleich einsetzen. Die Zeilen 4 bis 10 stellen fest, welches Menüelement das Ereignis erzeugt hat, auf das diese Methode reagiert. Zu der Reaktion gehört die Auswertung des Sender-Parameters und dessen Vergleichen mit jedem der MenuItem-Objekte, die in der Auflistung miFont.MenuItems enthalten sind. Denken Sie daran, dass Sender das Objekt darstellt, das das Ereignis erzeugt hat. Die Schriftart im Label-Steuerelement ändert sich je nach dem Wert von Sender. Wir verwenden kein Gleichheitszeichen, um den Vergleich in den Zeilen 4, 6 und 8 zu bewerten, denn der Gleichheitsoperator funktioniert nur bei einfachen Datentypen wie etwa Zahlen oder Zeichenfolgen. Bei Objekten benötigen wir hingegen das Schlüsselwort is. Im nächsten Schritt verleihen wir dem aktuellen Menüelement ein Häkchen und entfernen die Häkchen bei den restlichen Menüelementen. Zu diesem Zweck verwenden wir eine for each-Schleife wie in Zeile 12. Sie nimmt jedes MenuItem aus der Auflistung miFont.MenuItems, weist es der Variablen item, die wir in Zeile 2 erzeugten, zu und setzt die Eigenschaft Checked dieser Variablen auf False, so dass die Häkchen aller anderen

157

Ereignisse in Windows Forms

MenuItems entfernt werden. Die for each-Schleife hätte man auch durch eine einfache forSchleife bewerkstelligen können, etwa so: for i = 0 to mnuFont.MenuItems(0).MenuItems.Count – 1 mnuFont.MenuItems(0).MenuItems(i).Checked = False next i

Die for-Schleife setzt eine Variable i ein, um durch die Gesamtzahl aller Menüelemente zu iterieren. Vergessen Sie nicht, die Variable i zu deklarieren, falls Sie diese Methode verwenden. Da nun alle MenuItems häkchenlos sind, brauchen wir nur das aktuelle Menüelement mit Häkchen zu versehen. Das erfolgt in Zeile 15. Mit Hilfe der CType-Methode konvertieren wir den Sender-Parameter zu einem MenuItem-Objekt (zum näheren Verständnis verweise ich auf den Abschnitt »Ereignishandler« später in diesem Kapitel). Da Sender das aktuelle Menüelement darstellt, können wir seine Checked-Eigenschaft auf True setzen. Zum Schluss müssen Sie die Zeilen 23, 26 und 29 in Listing 5.1 so ändern, dass sie Ihren neuen Allround-Ereignishandler nutzen: 23: ... 26: ... 29: ...

AddHandler miTimes.Click, new EventHandler(AddressOf Me.FontClicked) AddHandler miArial.Click, new EventHandler(AddressOf Me.FontClicked) AddHandler miWing.Click, new EventHandler(AddressOf Me.FontClicked)

Die Ausführung des geänderten Codes produziert die gleichen Ergebnisse wie das ursprüngliche Listing 5.1. Der einzige Unterschied: Ein Ereignishandler erledigt die Arbeit von dreien. Nun sind Sie schon fast ein Profi in der Ereignis-Behandlung. Aber es gibt noch ein paar andere Puzzlestückchen, die wir noch zusammensetzen sollten. Woher stammen beispielsweise die Parameter Sender und EventArgs?

5.2

Delegaten

Ein Ereignis ist also, dies nur zur Erinnerung, eine Meldung, die ein Objekt abschickt, um zu signalisieren, dass eine Aktion stattgefunden hat. Wenn also eine Schaltfläche angeklickt wird, schickt sie eine »Ich wurde angeklickt«-Meldung an die Common Language Runtime (CLR). Jedes Objekt hat die Verantwortung, die CLR über Ereignisse zu informieren – sonst würde nie etwas passieren. Das das Ereignis auslösende Objekt – etwa die Schaltfläche in obigem Beispiel – wird als Sender (sender) bezeichnet (jetzt wissen Sie, warum Sie einen Sender-Parameter bei der

158

Delegaten

Deklaration eines Ereignishandlers verwendet haben). Das Objekt, das die Meldung über das Ereignis erhält und darauf antwortet, wird als Empfänger (receiver) bezeichnet. Da nun in .NET alles etwas flexibler gehandhabt wird, kann auch jedes Objekt ein Ereignis auslösen und jedes Objekt kann ein Ereignis empfangen. Dafür bestehen kaum Einschränkungen. Diese Flexibilität fordert allerdings ihren Preis: Der Sender weiß vorher nie genau, wer der Empfänger sein wird, noch kümmert er sich darum. Der Sender weiß lediglich, das er ein Ereignis auslöst, das durch etwas anderes verarbeitet wird. Man braucht also eine Art Vermittler, etwas, das als Mittelsmann zwischen Sender und Empfänger fungiert. Zum Glück stellt die CLR einen Delegaten (Delegierter) zur Verfügung, eine speziellen Typ für einen solchen Vermittler. Ein Delegat ist eine Klasse, die einen Verweis auf eine Methode enthalten kann. Das mag noch nicht viel Sinn ergeben. Einfach ausgedrückt, agiert ein Delegat als Zeiger auf einen EreignisAbsender. Er zeigt den Weg zum Empfänger. In einem vereinfachten Football-Spiel beginnt das Spiel, sobald der Quarterback (der Absender) den Ball besitzt. Er muss den Ball zu jemand anderem (dem Empfänger) in seiner Mannschaft weiterspielen. Wenn das Spiel beginnt, verfügt der Quarterback über eine beliebige Zahl von möglichen Empfängern. Die Entwicklung des Spiels entscheidet darüber, wer den Ball tatsächlich erhält. Der Quarterback überschaut das Spielfeld, um zu entscheiden, wem er den Ball zuspielen soll. Manchmal versperren Spieler der gegnerischen Mannschaft die Sicht auf manche Empfänger, und zuweilen kann der Quarterback nicht alle der verfügbaren Empfänger sehen. Noch kurz bevor er den Ball wirft, weiß der Quarterback nicht, wer diesen empfängt (manchmal sogar noch nach dem Wurf). In unserem vereinfachten Spiel sieht jedoch der Trainer an der Seitenlinie alles aus einem besseren Blickwinkel. Daher sieht er (besser als es der Quarterback jemals können wird), wer der Empfänger sein soll. Durch einen Funkempfänger hinten am Helm des Quarterbacks ruft der Trainer: »Wirf den Ball Nummer 66 zu!« (Wir wollen die Frage mal ignorieren, ob dies legal ist.) Daraufhin wirft der Quarterback den Ball zum Empfänger, den der Trainer angegeben hat. Abbildung 5.2 illustriert dieses Konzept. In diesem Beispiel stellt der Trainer den Delegaten dar, den Vermittler zwischen Absender und Empfänger, wobei er das Ereignis (den Ball) in die richtige Richtung lenkt. In .NET kann ein Objekt (der Absender) ein Ereignis »werfen«. Der Ereignisempfänger kann in jedem anderen Mitglied der Absendermannschaft bestehen beziehungsweise der Klasse, der das Objekt angehört. Ein Delegat leitet die Ereignismeldung zum richtigen Empfänger. Wenn Sie in den Begriffen von Football denken, ergibt das durchaus einen Sinn!

159

Ereignisse in Windows Forms

Potenzielle Empfänger (Mannschaftsmitglieder des Quarterback)

22

66

8

17 Verteidigung

“Nummer 66”

13 Quarterback Trainer

Abbildung 5.2: Ein Football-Quarterback – der hier als Absender fungiert – hat eine beliebige Anzahl von Empfängern (für seinen Ball).

Wie sieht also ein Delegat in Codeform aus? Sie sind vielleicht überrascht zu erfahren, dass Sie bereits einige verwendet haben. Sehen Sie sich folgendes Codefragment an: 'VB .NET Addhandler Button.Click, new EventHandler(AddressOf method) //C# Button.Click += new EventHandler(this.method);

Sie erinnern sich, dass die Definition eines Delegaten in einer Klasse besteht, die eine Referenz auf eine Methode enthält. Die Implementierung einer Klasse ist ein Objekt. Die Delegaten in diesem Codestück sind die EventHandler-Objekte. Die Objekte ScrollEventHandler, ToolBarClickEventHandler und KeyPressEventHandler, die Sie bereits in vorhergehenden Lektionen benutzt haben, sind ebenfalls Delegaten. Tatsächlich ist jede Klasse in .NET, die das Wort EventHandler im Namen enthält, ein Delegat. Das Codestück dient also im Endeffekt dazu, dem Click-Ereignis des Button-Steuerelements Delegaten hinzuzufügen. Sie zeigen den Weg zum Empfänger des Ereignisses, in diesem Fall dem method-Parameter, welcher nur eine Methode ist.

160

Delegaten

Zu den unterschiedlichen Implementierungen in VB .NET und C#: In C# besteht die einzige Möglichkeit, einem Ereignis einen Delegaten hinzuzufügen, in dem Operator +=. (Für jene unter Ihnen, die mit C++ und C nicht vertraut sind, sei gesagt, dass += ein Kurzbefehl für das Summieren und Zuweisen ist.) Zum Beispiel ist myInteger += 1;

das Gleiche wie myInteger = myInteger + 1;

Um einen Delegaten von einem Ereignis zu lösen, verwenden Sie den Operator -= (oder in Visual Basic .NET die RemoveHandler-Methode).

In VB .NET verfügen Sie andererseits über eine Reihe weiterer Optionen. Die erste Wahlmöglichkeit – die Sie bereits eine Weile eingesetzt haben – besteht im Gebrauch der AddHandler-Methode. Die Syntax bietet kaum Schwierigkeiten. Diese Methode fügt den Delegaten (der auf method zeigt) dem Click-Ereignis des Button-Objekts hinzu. Der Operator AddressOf ist lediglich ein Schlüsselwort, das das Programmäquivalent eines Zeigers auf die bereitgestellte Methode erzeugt. Als zweite und weniger gebräuchliche Option gestattet VB .NET, das Schlüsselwort Handles zu verwenden. Wie in Listing 5.3 zu sehen, fügen Sie einer Methodendeklaration das Handles-Schlüsselwort hinzu, gefolgt vom Ereignis, das zu verarbeiten ist. Listing 5.3: Eine weitere Möglichkeit, Ereignisse zu verknüpfen 1:

public sub MyEventHandler(Sender as Object, e as EventArgs) Handles

 btTemp.Click 2: 3:

'tu etwas end sub

Das funktioniert genauso gut wie die anderen Methoden. Ich empfehle

Bitte beachten Sie

Benutzen Sie in VB .NET möglichst immer die

Benutzen Sie die Handles-Methode bitte sparsam. Sie schränkt Ihre Möglichkeiten dadurch ein, dass man damit nur ein Ereignis pro Handler verarbeiten kann, und sie macht Code weniger gut lesbar.

AddHandler-Methode. Sie ist nicht nur die ver-

ständlichste Methode, sondern erlaubt Ihnen auch, denselben Handler für viele verschiedene Ereignisse zu verwenden. Benennen Sie Ihre Ereignishandler gemäß einer standardmäßigen Namenskonvention. Dies macht Ihren Code später viel einfacher zu lesen und zu verstehen.

161

Ereignisse in Windows Forms

5.3

Ereignisse und Objekte

Wir wollen uns nun der Implementierung von Delegaten zuwenden. Alle vordefinierten Delegaten in .NET besitzen zwei Parameter: ein Object, welches das Objekt darstellt, das das Ereignis erzeugte, und einen zweiten Parameter, der detaillierte Angaben über das erfolgte Ereignis bereitstellt. Der gebräuchlichste Delegat ist natürlich der altbekannte EventHandler. Sobald ein Ereignis auftritt, erzeugt dieser Delegat zwei Objekte (ein Object- und ein EventArgs-Objekt) und übergibt sie dem Ereignishandler oder Empfänger, der damit machen kann, was er will. Die folgenden Abschnitte befassen sich mit der Erstellung von Delegaten und benutzerdefinierten Ereignissen.

Ereignishandler Listing 5.4 zeigt einen typischen Ereignishandler in VB .NET. Er nimmt an, dass Sie bereits ein Button-Steuerelement erzeugt und ihm einen Delegaten hinzugefügt haben, der auf die HandleClick-Methode zeigt. Beachten Sie, dass das Ereignis Button.Click den standardmäßigen EventHandler-Delegaten verwendet. Listing 5.4: Ein typischer Ereignishandler 1: 2: 3:

private sub HandleClick(Sender as Object, e as EventArgs) Microsoft.VisualBasic.MsgBox(CType(Sender, Button).Text) end sub

Denken Sie daran, dass jeder Delegat zwei Objekte erzeugt und sie an den Ereignishandler weiterleitet. Dieser wiederum, egal ob er sie verwendet oder nicht, muss diese zwei Objekte als Parameter übernehmen, wie Zeile 1 zeigt. Das erste Objekt, das wir in Zeile 1 Sender genannt haben, stellt das Objekt dar, das das Ereignis erzeugt hat. In diesem Fall wäre das Objekt das zuvor erstellte Button-Objekt. Da der Delegat, der diese Methode aufruft, vom Typ EventHandler ist, ist der zweite Parameter vom Typ EventArgs. Dieser wird dazu verwendet, weitere Informationen über das Ereignis aufzunehmen, doch im Fall von EventArgs enthält er nichts; er wird nur als Platzhalter eingesetzt. Die Entwickler bei Microsoft, die für .NET und seine Klassenbibliotheken verantwortlich zeichnen, haben dankenswerterweise standardisierte Namenskonventionen verwendet. Egal wie der Name Ihres Delegaten lautet, der zweite Parameter folgt dem gleichen Namensschema. Beispiele dafür sind EventHandler und EventArgs, ScrollEventHandler und ScrollEventArgs etc.)

162

Ereignisse und Objekte

Sie könnten sich nun Folgendes fragen: Wenn der erste Parameter das das Ereignis erzeugende Objekt darstellt, warum erstellt dann nicht statt dessen der Delegat diesen Objekttyp? Zum Beispiel ein Button-Objekt in Listing 5.4 statt eines Object-Objekts? Der Grund liegt in der Flexibilität. Wenn der Delegat einen tatsächlichen Button-Typ weitergäbe, dann bräuchte man eine Menge Typen von Delegaten: einen für jeden Objekttyp, der ein Ereignis erzeugen könnte. Der Standard-EventHandler-Delegat wäre nicht ausreichend. Daher verkleidet der Delegat das Button-Objekt als ein Object. Eine weitere Analogie: Stellen Sie sich vor, Sie erhalten per Post ein Buch. Es dürfte sich in einer Schachtel befinden. Diese Schachtel stellt das Object dar. Das Transportunternehmen steckt das Buch in eine Standardschachtel, die sich für alles eignet. Das spart dem Unternehmen die Kosten für die Erzeugung einer jeweils anderen Schachtel für unterschiedliche Warentypen, die es transportiert. Sie zumindest wissen, dass sich in der Schachtel ein Buch befindet, auch wenn Sie mit der Schachtel wenig anfangen können, und lesen wollen Sie sie auch nicht. Sie müssen daher die Schachtel öffnen und das Buch herausholen. Grob gesagt, Sie transformieren die Schachtel in ein Buch. Etwas Ähnliches passiert in der Methode in Listing 5.4. Der erste Parameter wird als Object übergeben, welches über nur wenige nützliche Eigenschaften verfügt (siehe Tag 3). Wollen Sie also mit diesem ersten Parameter arbeiten, müssen Sie ihn daher erst in den notwendigen Typ umwandeln (mit anderen Worten: die Schachtel öffnen). In Zeile 2 des Listings 5.4 verwenden Sie die CType-Methode, um den ersten Parameter von einem Object in einen Button-Typ umzuwandeln, der dann über die gewünschte Text-Eigenschaft verfügt. In C# verwenden Sie den Casting-Operator. Das ist schlicht ein Klammernpaar, das den Zieltyp einklammert ((Button) Sender).Text;

Sie sollten das Absender-Objekt stets ganz explizit in den gewünschten Typ umwandeln. VB .NET kann (je nach der Komplexität des das Ereignis erzeugenden Objekttyps) diese Umwandlung automatisch ausführen, aber es wäre nicht besonders klug, sich darauf zu verlassen, dass VB .NET Ihre Arbeit verrichtet.

Benutzerdefinierte Ereignisse erstellen Mit unserem Wissen über das Wesen von Klassen, wie man sie einsetzt und wie man Ereignisse verarbeitet, wollen wir nun einen Schritt weitergehen. Im folgenden Abschnitt erzeugen wir eine eigene Klasse, die über ihre eigenen Ereignisse verfügt, für welche Sie – oder jeder andere Entwickler – Ereignishandler bereitstellen können. Da Sie noch nicht wissen, wie man seine eigenen UI-Steuerelemente erstellt, erscheint dieses Beispiel zwar etwas an den Haaren herbeigezogen, aber es ist dessen ungeachtet eine gute Lernübung (mehr über die Erstellung eigener Steuerelemente erfahren Sie an Tag 18).

163

Ereignisse in Windows Forms

In diesem Beispiel benötigen wir zweierlei: eine benutzerdefinierte Klasse, die das benutzerdefinierte Ereignis auslöst, und eine andere Klasse, die den Handler für das benutzerdefinierte Ereignis enthält (das kann eine gewöhnliche Windows Forms-Klasse sein). Wir erzeugen eine einfache Golfball-Klasse als Klasse, die unser benutzerdefiniertes Ereignis auslöst. Der Code dafür ist in Listing 5.5 zu finden. Listing 5.5: Die GlfBall-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:

using System; using System.Windows.Forms; namespace TYWinforms.Day5 { public class GolfBall { public event EventHandler Sunk; protected virtual void OnSunk(EventArgs e) { if (Sunk != null) { Sunk(this,e); } } public void Putt(int intStrength) { if (intStrength = 5) { OnSunk(new EventArgs()); } } }

In diesem Code gibt es eine Menge Neues zu entdecken. In Zeile 5 deklarieren Sie Ihre benutzerdefinierte GolfBall-Klasse. Mit dieser Klasse soll ein wirklicher Golfball nachgebildet werden. Der Einfachheit halber erzeugen wir nur eine Methode dafür, Putt, die das Einputten oder Einlochen eines Golfballs simuliert. Die Methode ist in den Zeilen 14 bis 18 zu finden (gleich mehr dazu). Logischerweise wird das einzige Ereignis nur dann ausgelöst, wenn der Golfball schließlich im Loch landet. Zeile 6 deklariert das benutzerdefinierte Ereignis für diese Klasse: Sunk (eingelocht). Beachten Sie, wie einfach diese Codezeile ist; sie besteht aus lediglich vier Wörtern. Das Ereignis wird genau wie eine Klasse deklariert, doch statt das class-Schlüsselwort zu benutzen, verwenden Sie das event-Schlüsselwort. Sie brauchen ihr keine weitere Funktionalität zu verleihen, schließlich sollen nicht Sie das Ereignis behandeln, sondern der Programmierer der Klasse. Sie müssen noch den Typ des Delegaten festlegen, der das Ereignis behandeln soll. In diesem Fall verwenden wir den Standard-EventHandler-Delegaten, obwohl wir auch unseren eigenen hätten verwenden können (mehr dazu später).

164

Ereignisse und Objekte

Die Zeilen 8 bis 12 deklarieren eine ungewöhnliche Art von Methode. Sie trägt den gleichen Namen wie unser benutzerdefiniertes Ereignis, nur dass ihr das Präfix On vorangestellt ist (z.B. OnSunk). Jedes benutzerdefinierte Ereignis muss eine entsprechende Methode OnEreignisName besitzen. Diese Methode dient nur dem einen Zweck, das Ereignis auszulösen, wie man aus Zeile 10 ersieht; keine andere Methode kann das Ereignis direkt aufrufen, sondern muss dafür über diese On-Methode gehen. Die On-Methode ist besonders interessant. Zunächst ist festzuhalten, dass sie einen Parameter des Typs EventArgs übernimmt – d.h. des Typs, der dem in Zeile 6 verwendeten Typ von EventHandler entspricht. (Sie werden gleich sehen, woher dieser Parameter stammt.) Als Nächstes ist festzuhalten, dass sie in Zeile 9 den Wert von Sunk, unserem Ereignis, auszuwerten scheint. Was bedeutet das konkret? In Zeile 6 haben Sie ein neues Ereignis deklariert, dafür aber weder eine Implementierung bereitgestellt noch eine Instanz davon erzeugt. Erinnern Sie sich daran, dass Sie von den meisten Objekten zuerst eine Instanz erzeugen müssen, bevor Sie das jeweilige Objekt benutzen können. Ein Ereignis funktioniert genauso. Indem Sie hier keine Instanz erzeugten, überlassen Sie dies dem Benutzer der GolfBall-Klasse. Ereignet sich etwas, erzeugt der Benutzer Ihrer GolfBall-Klasse eine Instanz, indem er dem Ereignis einen Delegaten zuweist. Etwa so: GolfBall myGolfBall = new GolfBall(); myGolfBall.Sunk += new EventHandler(this.eineMethode)

Bis das Ereignis einen Delegaten zugewiesen bekommt, ist es nicht instantiiert und trägt daher den Wert null (oder nothing in VB .NET). Der Zweck der On-Methode besteht darin zu prüfen, ob ein Delegat zugewiesen wurde. Dies erfolgt in Zeile 9. Falls Sunk, das Ereignis, nicht gleich null ist, dann bedeutet das, dass ein Delegat zugewiesen wurde und das Ereignis ausgelöst werden kann. Würden Sie diese Prüfung nicht vornehmen und es wäre kein Delegat zugewiesen, würden Sie beim ersten Auslösen des Ereignisses eine Fehlermeldung erhalten. Das in Zeile 6 deklarierte Ereignis verwendet einen Delegaten vom Typ EventHandler. Bekanntlich sendet der EventHandler-Delegat (wie jeder andere Delegat) zwei Parameter an den Ereignishandler: ein Object, welches das das Ereignis auslösende Objekt darstellt, und einen EventArgs-Parameter. Welches Objekt löst also unser Ereignis aus? Natürlich das GolfBall-Objekt, jene Klasse, in der sich all dieser Code befindet. Daher ist der dem Ereignis zu übergebende Parameter lediglich this, das für die aktuelle Klasse steht. Merke: Der erste Parameter zur Auslösung eines Ereignisses auf diese Weise ist stets this (oder me in VB .NET). Um mehr über den zweiten Parameter zu erfahren, müssen wir zu Zeile 14 springen. Diese Methode, Putt, führt die OnSunk-Methode unter bestimmten Bedingungen aus. Wollten Sie irgendwelche ereignisrelevanten Informationen übergeben, würden Sie sie

165

Ereignisse in Windows Forms

hier erzeugen. Da es aber für dieses Ereignis keine gibt, erzeugen wir einfach einen neuen EventArgs-Parameter und übergeben ihn an die OnSunk-Methode. Lassen Sie uns zusammenfassen. Als Erstes erzeugt der Benutzer unserer GolfBall-Klasse eine Instanz dieser Klasse und ein Delegat wird dem Sunk-Ereignis zugewiesen. Der Benutzer ruft die Putt-Methode auf, um zu versuchen, den »Golfball einzulochen«. Sobald festgestellt wurde, dass dem Putter genügend Kraft verliehen wurde (wie durch den InStrength-Parameter angegeben wird), wird die OnSunk-Methode ausgeführt, wobei ihr ein EventArgs-Parameter übergeben wird. Die OnSunk-Methode prüft wiederum, ob ein Delegat zugewiesen wurde. Für diesen Fall löst sie das Sunk-Ereignis aus und übergibt ihm eine Repräsentation des aktuellen Objekts und des EventArgs-Parameters. Wenn das Sunk-Ereignis ausgelöst wird, veranlasst der Delegat, dass der vom Benutzer zugeordnete Ereignishandler ausgeführt wird. Wir wollen uns als Nächstes einmal die Code-Seite des Benutzers der GolfBall-Klasse ansehen (im Gegensatz zu der des Endanwenders). Sie ist in Listing 5.6 zu sehen. Listing 5.6: Die Benutzerseite des GolfBall-Beispiels 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:

166

public class MainForm : Form { private Button btPutt = new Button(); private Label lblStrength = new Label(); private TextBox tbStrength = new TextBox(); private GolfBall gfBall = new GolfBall(); public MainForm(){ lblStrength.Text = "Spielstärke:"; lblStrength.Location = new Point(50,50); tbStrength.Text = 0.ToString(); tbStrength.Location = new Point(125,50); btPutt.Text = "Lochen Sie den Ball ein!"; btPutt.Location = new Point(100,100); btPutt.Click += new EventHandler(this.PuttBall); gfBall.Sunk += new EventHandler(this.BallSunk); this.Text = "Beispiel für ein angepasstes Ereignis"; this.Controls.Add(btPutt); this.Controls.Add(tbStrength); this.Controls.Add(lblStrength); } public void PuttBall(Object Sender, EventArgs e) {

Ereignisse und Objekte

27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38:

gfBall.Putt(Convert.ToInt32(tbStrength.Text)); } public void BallSunk(Object Sender, EventArgs e) { MessageBox.Show("Eingelocht!"); } public static void Main() { Application.Run(new MainForm()); } } }

Speichern Sie dieses und Listing 5.5 in einer einzigen Datei. In Listing 5.6 findet sich nichts Besonderes. In den Zeilen 2 bis 5 erzeugen Sie ein paar Steuerelemente, darunter auch eine Instanz der GolfBall-Klasse. Die Zeilen 8 bis 16 platzieren die Steuerelemente im Formular und stellen ein paar Eigenschaften ein. Die Steuerelemente erlauben es dem Benutzer, einen Spielstärke-Wert einzugeben und eine Schaltfläche anzuklicken, um den Ball »einzulochen«. In Zeile 18 fügen Sie der Sunk-Methode der GolfBall-Klasse einen Delegate hinzu, genau wie Sie das bei jeder anderen Klasse täten. Die Zeilen 20 bis 23 fügen Ihrem Formular alle Steuerelemente hinzu. Die PuttBall-Methode in Zeile 26 wird immer dann ausgeführt, wenn die in Zeile 2 deklarierte Schaltfläche angeklickt wird. Sie führt schlicht die SunkMethode der GolfBall-Klasse aus, welche bereits in Listing 5.5 in Zeile 14 deklariert worden war. Sie übergibt den Spielstärke-Wert, den der Benutzer in das Textfeld eingegeben hat. Jedes Mal wenn das Sunk-Ereignis ausgelöst wird, gelangt die BallSunk-Methode zur Ausführung. In unserem Fall wird Sunk aufgerufen, sobald die Putt-Methode mit einem Spielstärke-Wert von genau 5 aufgerufen wird. BallSunk (der Ball wurde eingelocht) ruft die Show-Methode der MessageBox-Klasse auf (die sich von der MsgBox-Funktion des Namensraums Microsoft.VisualBasic unterscheidet), um eine einfache Meldung anzuzeigen, die angibt, dass der Ball im Loch versenkt (daher »sunk«) wurde. Kompilieren Sie diese Anwendung und führen Sie sie aus. Tragen Sie verschiedene Werte in das Textfeld ein und beachten Sie, dass nichts passiert, wenn Sie auf die Schaltfläche klicken, es sei denn, Sie hätten einen Spielstärke-Wert 5 eingegeben. Das Ergebnis ist in Abbildung 5.3 zu sehen.

167

Ereignisse in Windows Forms

Abbildung 5.3: Unser benutzerdefiniertes Sunk-Ereignis in voller Aktion – der Benutzer gewinnt!

Steuerelemente sezieren Aufgrund des Wissens, über das Sie nun verfügen, können Sie eine Hypothese darüber aufstellen, wie Objekte, die Sie kennen, implementiert werden (was Ihnen helfen wird, sobald Sie Ihre eigenen Steuerelemente erstellen). Nehmen Sie das Button-Steuerelement als Beispiel. Obwohl Sie nicht wissen, wie es genau erstellt wurde, können Sie aufgrund dessen, was Sie heute gelernt haben, dennoch darauf schließen. Lassen Sie uns eine Eigenschaft, nämlich Text, und ein Ereignis, Click, genauer untersuchen (den Darstellungsvorgang wollen wir vorerst ignorieren; wir besprechen ihn an Tag 13). Wir wollen uns mit der Frage befassen, wie wir das heute Gelernte auf nützliche Objekte anwenden können. Als Erstes können Sie davon ausgehen, dass die Text-Eigenschaft eine einfache Variable ist, die wie folgt im Quellcode des Buttons deklariert wird: public string Text;

Als Nächstes muss die Button-Klasse das Click-Ereignis deklarieren, so wie im folgenden Codestück: public event EventHandler Click;

Da alle Ereignisse eine korrespondierende On-Methode besitzen müssen, deklarieren wir diese im nächsten Schritt: protected virtual void OnClick(EventArgs e) { if (Click != null) { Click(this, e); } }

Das war schon alles! In Listing 5.7 kommt alles zusammen.

168

Ereignisse und Objekte

Listing 5.7: Die extrapolierte Button-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:

using System; using System.Windows.Forms; namespace System.Windows.Forms { public class Button : ButtonBase { public string Text; public event EventHandler Click; protected virtual void OnClick(EventArgs e){ if (Click != null){ Click(this,e); } } public Button(){ // Code zur Initialisierung und Anzeige der Schaltfläche } } }

Dieser Code ist natürlich vereinfacht. Wir wissen ja, dass das Button-Steuerelement über mehr als nur eine Eigenschaft und ein Ereignis verfügt. Tatsächlich wurde das Click-Ereignis von der Control-Klasse geerbt, daher müssten wir es eigentlich nicht hier deklarieren, aber das vermittelt Ihnen eine gute Vorstellung davon, was das Erstellen von Steuerelementen bedeutet.

Benutzerdefinierte Delegaten erstellen Wenn Sie Ihre eigenen benutzerdefinierten Ereignisse erzeugen, müssen Sie häufig auch Ihre eigenen benutzerdefinierten Delegaten und EventArgs-Objekte erzeugen. Den Delegaten zu erstellen, ist einfach; das EventArgs-Objekt ist etwas komplizierter, doch Sie sind bereits in der Lage, das anzupacken. Die Deklaration des Delegaten besteht aus einer Zeile mit dem delegate-Schlüsselwort: public delegate void MyEventHandler(Object Sender, MyEventArgs e);

Diese Zeile erzeugt einen neuen Delegaten namens MyEventHandler, der sich für jedes Ihrer benutzerdefinierten Ereignis verwenden lässt. Er stellt einen EventArgs-Parameter bereit, ein benutzerdefiniertes Objekt, das wir ebenfalls zu erstellen haben (beide Objekte befolgen die Standardnamenskonventionen). Genau wie bei einem Ereignis müssen Sie weder eine Implementierung noch Code für diesen Delegaten bereitstellen; die CLR erledigt das alles für Sie.

169

Ereignisse in Windows Forms

Zum Erstellen des zweiten Parameters gehört das Erzeugen Ihrer eigenen benutzerdefinierten Klasse mit den Eigenschaften, die Ihr Ereignis benötigt, wie in Listing 5.8 demonstriert wird: Listing 5.8: Eine benutzerdefinierte EventArgs-Klasse erstellen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

using System; using System.Windows.Forms; namespace TYWinforms.Day5 { public class GolfBallEventArgs : EventArgs { public readonly int Meter; public readonly bool Save; public GolfBallEventArgs(int Meter,bool Save){ this.Meter = Meter; this.Save = Save; } } }

Diese Klasse namens GolfBallEventArgs werden wir nutzen, um einem Ereignishandler benutzerdefinierte Ereignisinformationen zukommen zu lassen. Sie erbt von der Klasse EventArgs, so dass wir keine Funktionen der Letzteren duplizieren müssen. In den Zeilen 6 und 7 deklarieren Sie zwei Eigenschaften: Meter, die angibt, aus welcher Entfernung der Golfball beim Einputten eingelocht wurde; und Save, die angibt, ob der Einputt-Schlag für Par gespeichert wurde. Diese Werte sind auf readonly (Nur-Lesen) gesetzt, so dass sie sich durch nichts anderes als durch den Konstruktor selbst ändern lassen. Dieses Vorgehen ist Standard, denn schließlich gilt: Nachdem diese Werte gesetzt sind, gibt es keinen Grund mehr, sie zu ändern. In VB .NET würden die entsprechenden Zeilen wie folgt aussehen: ReadOnly Meter as Integer ReadOnly Save as Boolean

In Zeile 9 stoßen wir auf den Konstruktor unserer GolfBallEventArgs-Klasse. Sie übernimmt zwei Parameter, nämlich Meter und Save, die mit den readonly-Eigenschaften, die gerade erstellt wurden, korrespondieren. Dieser Konstruktor dient lediglich dazu, die Parameterwerte den entsprechenden Eigenschaften der Klasse zuzuweisen. Wir wollen einmal unsere GolfBall-Klasse so ändern, dass sie diesen neuen Delegaten und das EventArgs-Objekt verwendet. Der neue Code, der auf dem modifizierten Listing 5.5 basiert, ist in Listing 5.9 zu sehen.

170

Ereignisse und Objekte

Listing 5.9: Den benutzerdefinierten Delegaten verwenden 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: 5: namespace TYWinforms.Day5 { 6: public class GolfBallEventArgs : EventArgs { 7: public readonly int Meter; 8: public readonly bool Save; 9: 10: public GolfBallEventArgs(int Meter,bool Save){ 11: this.Meter = Meter; 12: this.Save = Save; 13: } 14: } 15: 16: public delegate void GolfBallEventHandler(Object Sender,  GolfBallEventArgs e); 17: 18: public class GolfBall { 19: public event GolfBallEventHandler Sunk; 20: 21: protected virtual void OnSunk(GolfBallEventArgs e){ 22: if (Sunk != null){ 23: Sunk(this,e); 24: } 25: } 26: 27: public void Putt(int intStrength){ 28: if (intStrength == 5){ 29: Random rndGen = new Random(); 30: OnSunk(new GolfBallEventArgs(rndGen.Next(1,51),  Convert.ToBoolean(rndGen.Next(0,2)))); 31: } 32: } 33: }

Die Zeilen 5 bis 14 umfassen die GolfBallEventArgs-Klasse, die wir gerade entwickelt haben (Listing 5.8). Zeile 16 enthält die Deklaration des Delegaten, den diese Klasse verwendet. Innerhalb der GolfBall-Klasse haben nur wenige Änderungen stattgefunden. In Zeile 19 deklarieren Sie das Sunk-Ereignis mit Hilfe des GolfBallEventHandler-Delegaten anstelle von EventHandler. Auf ähnliche Weise verwenden Sie in den Zeilen 21 und 30 GolfBallEventArgs statt EventArgs. Diesmal verhält sich die Putt-Methode jedoch ein wenig anders als gewohnt.

171

Ereignisse in Windows Forms

Weil sie das GolfBallEventArgs-Objekt jetzt mit zwei Werten versorgen muss, nämlich Meter und Save, müssen wir ein paar Überstunden einlegen. Es geht uns nicht darum, wie die Werte aussehen, Hauptsache, sie werden zurückgegeben. Daher übergeben wir einfach ein paar zufällige Zahlen. In Zeile 29 erzeugen wir eine Instanz der Random-Klasse, die alle Arten von Methoden enthält, die Zufallszahlen generieren, darunter auch die von uns verwendete Methode, Next. Sie übernimmt zwei Parameter, beide Integer, die einen Wertebereich angeben, aus dem eine Zufallszahl zurückgegeben wird (Next liefert einen GanzzahlInteger, ohne Dezimalstellen). Für Meter, den ersten Parameter, wählen wir irgendeine Zahl zwischen 1 und 50 aus (im Bereich darüber dürfte es kaum jemanden geben, der aus dieser Distanz einputten kann!). Für den zweiten Parameter benötigen wir einen booleschen Wert. Dieser kann True, False, 1 oder 0 sein. Daher benutzen wir Next, um eine Zufallszahl im Bereich zwischen 0 und 1 zurückzugeben (und da Next nur Ganzzahlen zurückgibt, muss das entweder 0 oder 1 sein, aber nichts dazwischen). Mit Hilfe der Convert.ToBoolean-Methode wandeln wir die zurückgegebene Zufallszahl in einen booleschen Wert um, um sie dem GolfBallEventArgsKonstruktor zu übergeben. Wir können den Ereignishandler für das Sunk-Ereignis so ändern, dass er dieses Objekt verwendet. Ändern Sie mit Hilfe der gleichen MainForm-Klasse aus Listing 5.6 die Zeile 31 wie folgt: MessageBox.Show("Eingelocht aus " + e.Meter.ToString() + " Metern Entfernung!");

Abbildung 5.4 zeigt das Ergebnis dieser letzten Änderung.

Abbildung 5.4: Unser benutzerdefiniertes Sunk-Ereignis mit dem Objekt GolfBallEventArgs gibt dem Benutzer Anlass zum Jubeln!

172

Zusammenfassung

5.4

Zusammenfassung

Ereignisse sind ein zentraler Bestandteil einer Windows Forms-Anwendung. Ohne sie würde man nichts damit erledigen können. Wie das Ereignissystem funktioniert, wissen Sie nun sowohl aus der Perspektive des Entwicklers als auch aus der des Benutzers. Ein Ereignishandler ist diejenige Methode, die ausgeführt wird, sobald ein Ereignis stattfindet. Zur Erstellung des Handlers gehört das Verknüpfen von Ereignis und Handler mit Hilfe der AddHandler-Methode in VB .NET, oder mit dem +=-Operator in C#. Ereignishandler weisen eine Standardsignatur auf, die nur wenige Variationen zulässt. Ein Delegat ist eine Klasse, die eine Referenz auf einen Ereignishandler enthält. Er stellt die »Drähte« zwischen einem Ereignis und seinen Handlern dar. In .NET befolgen alle Delegaten das Namensschema NameEventHandler. Delegaten übergeben den Ereignishandlern zwei Parameter: ein Object-Objekt, das das ereignisauslösende Objekt darstellt, und einen EventArgs-Parameter, der zusätzliche Informationen bereitstellt. Der EventArgsParameter befolgt das gleiche Namensschema wie der Delegat. Um Ihr eigenes (benutzerdefiniertes) Ereignis zu erstellen, müssen Sie es lediglich mit Hilfe des event-Schlüsselwortes deklarieren und einen passenden Delegaten dafür wählen. Jedes Ereignis muss eine entsprechende OnEreignisName-Methode besitzen, die zur Auslösung des Ereignisses verwendet wird. Diese Methode kann ebenso prüfen, ob ein Delegat »verdrahtet« wurde, indem sie überprüft, ob das Ereignis einen Wert null (oder nothing in VB .NET) ergibt. Mit Hilfe des delegate-Schlüsselworts können Sie Ihre eigenen Delegaten auf die gleiche Weise erstellen wie ein benutzerdefiniertes Ereignis. Ein Delegat benötigt weder Implementierung noch Code – die CLR erzeugt beides für Sie. Ihre benutzerdefinierten Delegaten können jeweils einen vordefinierten EventArgs-Parameter verwenden, oder Sie können Ihren eigenen erstellen, indem Sie eine Klasse erzeugen, die vom EventArgs-Objekt erbt. Ihr benutzerdefiniertes EventArgs-Objekt muss nur die benutzerdefinierten readonlyEigenschaften bereitstellen, die Sie wollen, sowie einen Konstruktor, um diesen Eigenschaften Werte zuweisen zu können.

5.5 F

Fragen und Antworten

Woher weiß man, welche Art von Delegat man für ein Ereignis benutzen muss? A

Leider gibt es keine leichte Methode das herauszufinden. In den meisten Fällen verwendet man den Standard-EventHandler-Delegaten, so dass man damit auf der sicheren Seite ist. Ist dies aber nicht der korrekte Delegat, dann wird Ihnen normalerweise Ihr Compiler (Visual Studio .NET oder die Befehlszeilencompiler

173

Ereignisse in Windows Forms

csc.exe und vbc.exe) mitteilen, welchen Sie verwenden sollten. In diesem Buch weisen wir normalerweise auf den passenden Delegaten darauf hin.

F

Kann ich einem einzelnen Ereignis mehr als einen Delegaten zuweisen? A

Durchaus. Sie dürfen dies tun, wenn Sie wünschen, dass ein Einzelereignis mehr als eine Aktion ausführt. In unserem Vergleich mit dem Auto entspräche dies einer Ihrer Aktionen (sagen wir, Sie treten auf das Gaspedal), aber mehreren Aktionen des Wagens (bewegt sich, die Tachonadel zittert, Ausschalten des Tempomaten usw.). Das ist aber keine gebräuchliche Praxis, da normalerweise ein Ereignishandler alle gewünschten Aktionen ausführen kann. Es ist jedoch eine schlaue Möglichkeit, Ereignishandler zu »erweitern«. Mal angenommen, Sie hätten mit ShowText und ShowTextAndAlert zwei Ereignisse. Sie müssen Text in einem Bezeichnungsfeld anzeigen, doch das zweite Ereignis muss außerdem ein Meldungsfenster mit einer Warnung anzeigen. Es besteht kein Grund dafür, den Code, der den Text zur Anzeige bringt, zu duplizieren. Daher kann man beiden Ereignissen einen einzigen Delegaten zuweisen. Das ShowTextAndAlert-Ereignis würde einen weiteren Delegaten zugewiesen bekommen, der aber das Anzeigen des Meldungsfeldes veranlasst. Sobald das ShowTextAndAlert-Ereignis ausgelöst wird, würden beide Delegaten ausgeführt werden, so dass Text angezeigt und ein Meldungsfeld geöffnet wird. Die Ereignishandler werden in der Reihenfolge ausgeführt, die ihnen die Delegaten zuweisen.

5.6

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wie sieht die standardmäßige Signatur eines Ereignishandlers aus? 2. Erzeugen Sie in C# ein Ereignis namens ShowText, das den Delegaten KeyPressEventHandler verwendet. 3. Erstellen Sie einen benutzerdefinierten Delegaten, der das Objekt KeyPressEventArgs verwendet.

174

Workshop

4. Wahr oder falsch? Um Objekttypen miteinander zu vergleichen, verwendet man den Gleichheitszeichenoperator (=). 5. Welche der folgenden Aussagen ist inkorrekt? AddHandler btOne.Click, new EventHandler(AddressOf btOne_Click) public sub MyEventHandler(Sender as Object, e as EventArgs) Handles btTemp.Click Addhandler btOne.Click += new EventHandler(AddressOf btOne_Click)

6. Warum sollten die Eigenschaften eines benutzerdefinierten EventArgs-Objekts readonly sein? 7. Ist das folgende Codestück korrekt? Was sollte man falls nötig ändern? public virtual void OnMyEvent(EventArgs e) { MyEvent(this, e); }

8. An welcher Stelle (im Quellcode) muss man benutzerdefinierte Ereignisse und Delegaten deklarieren?

Übung Erstellen Sie in C# eine Taschenrechner-Anwendung wie den Windows-Taschenrechner. Er sollte Zifferntasten aufweisen, mit denen sich Zahlen eingeben lassen, sowie Funktionstasten, die Berechnungen vornehmen, wenn man darauf klickt. Verwenden Sie einen Ereignishandler für alle Zifferntasten und einen Handler für alle Funktionstasten. (Tipp: Nehmen Sie verborgene Steuerelemente, also solche, deren Visible-Eigenschaft auf False gesetzt ist – damit können Sie temporäre Variablen speichern.)

175

Windows Forms mit Steuerelementen erweitern

6

Windows Forms mit Steuerelementen erweitern

In der heutigen Lektion runden Sie Ihr wachsendes Repertoire an Windows Forms-Steuerelementen ab. Bis jetzt haben Sie nur Menüs, Bildlaufleisten, Statusleisten, Symbolleisten, Textbox, Bezeichnungsfelder und Schaltflächen als Steuerelemente zu sehen bekommen. Heute erfahren Sie etwas über Kombinationsfelder und Optionsfelder, die dem Benutzer mehrere Auswahlmöglichkeiten bereitstellen, sodann Timer, die sich zur Zeitmessung von Ereignissen verwenden lassen, und schließlich Strukturansichten (Treeviews), mit denen der Benutzer das Dateisystem erkunden kann, und viele andere Steuerelemente. Sie lernen darüber hinaus, wie man die Windows Forms-Merkmale für automatisches Layout nutzt, um eine übersichtlichere und leichter zu pflegende Benutzeroberfläche zu präsentieren. Heute lernen Sie, wie Sie 쐽

vom Benutzer Informationen einholen und sie auf logische Art und Weise anzeigen,



den Benutzer zwingen, Entscheidungen zu treffen,



Benutzereingaben formatieren,



Grafiken in Ihren Anwendungen anzeigen,



zeitmessende Anwendungen erstellen ,



viele verfügbare Aufzählungen nutzen, um Ihre Steuerelemente anzupassen,



die Positionierung Ihrer Steuerelemente steuern.

Wenn Sie die heutige Lektion durcharbeiten, sollten Sie an ein paar Schlüsselaspekte bezüglich der Windows Forms-Steuerelemente denken. Jedes Windows Forms-Steuerelement im .NET Framework erbt direkt oder indirekt von der Control-Klasse, welche wiederum von der Object-Klasse erbt. Das bedeutet, dass diese Steuerelemente eine ganze Reihe von Eigenschaften und Methoden gemeinsam haben, darunter die GetTypeMethode, die Height- und Width-Eigenschaften, das Click-Ereignis und andere, die Sie heute kennen lernen. (Um eine vollständige Aufzählung dieser gemeinsamen Mitglieder zu erhalten, schlagen Sie bitte in Anhang B nach.) All diese Steuerelemente haben auch Regeln für ihre Verwendung gemeinsam. Sie werden sehen, dass Sie nach dem Erlernen des Umgangs mit einem Steuerelement auch mit den anderen gut zurechtkommen werden. Natürlich verfügt jedes Steuerelement über seine Eigenarten und eigenen Mitglieder, aber sie folgen alle dem gleichen Muster.

178

Das Steuerelement Button

6.1

Das Steuerelement Button

Das Steuerelement Button kennen Sie ja bereits. Seine grundlegendste Eigenschaft ist Text. Sie setzt die Aufschrift, die auf der Schaltfläche angezeigt wird, um dem Benutzer einen Hinweis darauf zu geben, was die Schaltfläche bewirkt. TextAlign positioniert die Aufschrift auf der Schaltfläche und kann die folgenden Werte der ContentAlign-Aufzählung annehmen: BottomCenter, BottomLeft, BottomRight, MiddleCenter, MiddleLeft, MiddleRight, TopCenter, TopLeft und TopRight – ein Wert für jede Position auf der Schaltfläche. Die FlatStyle-Eigenschaft gestattet Ihnen die Anpassung des Aussehens der Schaltfläche. Die Eigenschaft kann einen der Werte in der FlatStyle-Aufzählung annehmen: Flat, Popup, wobei die Schaltfläche flach aussieht, bis Sie mit der Maus darüber geraten, Standard als Standardwert oder System, bei dem sich das Aussehen der Schaltfläche nach dem Standard Ihres Betriebssystems richtet (wobei dieser Wert einem der drei vorhergehenden Werte entsprechen muss). Abbildung 6.1 zeigt die ersten drei Stile sowie die Verwendung der TextAlign-Eigenschaften.

Abbildung 6.1: Beachten Sie, dass sich die Beschriftung der Schaltflächen in diesen Beispielen für FlatStyle-Werte jeweils an einer anderen Position befindet. Das ist eine Funktion der Eigenschaft TextAlign.

Mit Button-Steuerelementen können Sie Grafiken auf zweierlei Weise darstellen. Sie können entweder die Eigenschaften Image und ImageAlign verwenden oder die Eigenschaften ImageList und ImageIndex (wie bereits bei den Symbolleisten an Tag 4). ImageAlign verwendet die gleichen Werte der ContentAlign-Aufzählung wie TextAlign, wohingegen Image ein Image-Objekt übernimmt: button1.Image = Image.FromFile("C:\WinForms\Day6\MyBitmap.bmp")

Sie wissen bereits, wie man Click, das Hauptereignis eines Button-Steuerelements verarbeitet. Das folgende Codestück zeigt die Zuweisung eines Delegaten zur Click-Methode und einen exemplarischen Ereignishandler: AddHandler Button1.Click, new EventHandler(AddressOf Me.Button1_Click) ... private sub Button1_Click(Sender as Object, e as EventArgs) MessageBox.Show("Sie haben angeklickt: " & Ctype(Sender, Button).ToString end sub

Das Button-Steuerelement besitzt die Methode PerformClick, die Ihnen die Simulation eines Schaltflächen-Klicks gestattet. Sie können also das Click-Ereignis genauso auslösen,

179

Windows Forms mit Steuerelementen erweitern

als hätte ein Benutzer die Schaltfläche angeklickt. Dieser Methode müssen Sie keine Parameter bereitstellen; die CLR wird sie für Sie generieren: Button1.PerformClick

Das ist recht nützlich, wenn Sie einmal den Handler des Click-Ereignisses aufrufen müssen; durch PerformClick brauchen Sie sich nicht mehr um die Erstellung von EventArgsObjekten usw. zu kümmern.

6.2

Die Steuerelemente CheckBox und RadioButton

Die Steuerelemente CheckBox und RadioButton sind einander sehr ähnlich. Beide gestatten dem Benutzer, eine (oder mehrere) Auswahl(en) aus einer Gruppe ähnlicher Elemente zu treffen. Wahrscheinlich haben Sie diese Steuerelemente bereits einmal beim Ausfüllen eines Webformulars benutzt. Sie unterscheiden sich dadurch, dass es das Kombinationsfeld CheckBox dem Benutzer gestattet, mehr als ein Element der Angebotsliste auszuwählen, während das Optionsfeld RadioButton nur eine Auswahl gestattet. Listing 6.1 zeigt ein Beispiel für die Verwendung dieser Steuerelemente. Listing 6.1: Ein Beispiel für den Einsatz der Steuerelemente RadioButton und CheckBox 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:

180

Imports System Imports System.Windows.Forms Imports System.Drawing namespace TYWinforms.Day6 public class Listing61 : Inherits Form private lblColor as new Label private lblSex as new Label private private private private private private

chkColorBlue as new CheckBox chkColorRed as new CheckBox chkColorYellow as new CheckBox chkColorGreen as new CheckBox rbSexMale as new RadioButton rbSexFemale as new RadioButton

public sub New lblColor.Text = "Wählen Sie Ihre Lieblingsfarbe(n)" lblColor.Location = new Point(10,10) lblColor.AutoSize = True

Die Steuerelemente CheckBox und RadioButton

23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65:

chkColorBlue.Text = "Blau" chkColorBlue.Location = new Point(10,25) chkColorBlue.Width = 50 chkColorRed.Text = "Rot" chkColorRed.Location = new Point(60,25) chkColorRed.Width = 50 chkColorYellow.Text = "Gelb" chkColorYellow.Location = new Point(110,25) chkColorYellow.Width = 60 chkColorGreen.Text = "Grün" chkColorGreen.Location = new Point(170,25) chkColorGreen.Width = 60 lblSex.Text = "Wählen Sie Ihr Geschlecht" lblSex.Location = new Point(10,50) lblSex.AutoSize = True rbSexMale.Text = "Männlich" rbSexMale.Location = new Point(10,75) rbSexMale.Width = 50 rbSexFemale.Text = "Weiblich" rbSexFemale.Location = new Point(60,75) rbSexFemale.Width = 60 Me.Controls.Add(lblColor) Me.Controls.Add(lblSex) Me.Controls.Add(chkColorBlue) Me.Controls.Add(chkColorRed) Me.Controls.Add(chkColorYellow) Me.Controls.Add(chkColorGreen) Me.Controls.Add(rbSexMale) Me.Controls.Add(rbSexFemale) end sub public shared sub Main() Application.Run(new Listing61) end sub end class end namespace

181

Windows Forms mit Steuerelementen erweitern

Dieses Listing ist sehr einfach aufgebaut. Alle hier verwendeten Steuerelemente werden in den Zeilen 8 bis 16 deklariert, und die Eigenschaften Text, Location und Width werden in den Zeilen 19 bis 49 festgelegt. Die Steuerelemente werden dann dem Formular in den Zeilen 51 bis 58 hinzugefügt. Listing 6.1 produziert das in Abbildung 6.2 dargestellte Ergebnis.

Abbildung 6.2: Die Steuerelemente RadioButton und CheckBox sind in Formularen sehr nützlich. CheckBox-Steuerelemente erlauben Ihnen, jede beliebige Kombination von Kästchen im Containersteuerelement auszuwählen, welches in diesem Fall das Formular ist. RadioButtons andererseits erlauben Ihnen nur eine Auswahl je übergeordnetem Steuerelement – also pro Formular in diesem Fall. Um getrennte Gruppen von RadioButtons mit nur jeweils

einem wählbaren Wert zu erstellen, gruppieren Sie die Steuerelemente durch Positionierung auf einem Panel-Steuerelement. (Ein Panel-Steuerelement gestattet lediglich die Gruppierung von Steuerelementen; fassen Sie einfach jeden Satz von RadioButton-Steuerelementen, die denselben Wert beschreiben, auf einem separaten Panel-Steuerelement zusammen.) RadioButtons, die sich auf verschiedenen übergeordneten Steuerelementen befinden, schließen sich gegenseitig aus. Die Steuerelemente CheckBox und RadioButton haben ein paar Eigenschaften gemein. Die Checked-Eigenschaft lässt sich dazu verwenden, zu prüfen oder zu bestimmen, ob das angegebene Steuerelement ausgewählt ist. Die CheckAlign-Eigenschaft ähnelt der TextAlignEigenschaft des Button-Steuerelements. Sie prüft, wie das Auswahlfeld bezüglich des Textes ausgerichtet ist. CheckAlign verwendet ebenfalls die ContentAlignment-Aufzählung. Die AutoCheck-Eigenschaft stellt fest, ob sich das Aussehen des Steuerelements automatisch ändern sollte, wenn es angeklickt wird. Sollte also ein Häkchen erscheinen, sobald die Auswahl angeklickt wurde? Setzen Sie diese Eigenschaft auf False, wenn Sie anhand der Checked-Eigenschaft das Steuerelement selbst mit einem Häkchen versehen wollen. Der typische Fall dafür träte ein, wenn eine Art von Gültigkeitsprüfung erfolgen müsste, bevor

182

Das Steuerelement ComboBox

ein Steuerelement angeklickt werden darf (etwa bei der Gültigkeitsprüfung von Benutzereingaben). Auch Appearance ist eine vielen Steuerelementen gemeinsame Eigenschaft. Mit Hilfe von Werten aus der Appearance-Aufzählung (Button oder Normal) bestimmt sie, ob das Steuerelement normal aussieht (Voreinstellung) oder als Schaltfläche erscheint, die sich ausoder einschalten lässt. Die Funktionalität ist die gleiche, doch dies bietet dem Benutzer ein leicht verändertes Erscheinungsbild der Benutzeroberfläche. Die beiden Steuerelemente haben ein Ereignis gemein: CheckChanged. Wie der Name schon andeutet, tritt es auf, sobald das Kontrollhäkchen für das Steuerelement gesetzt oder entfernt wird. Auf Grund seiner etwas komplexeren Funktionsmerkmale verfügt das CheckBox-Steuerelement über einige weitere Eigenschaften. Die CheckState-Eigenschaft ähnelt der CheckedEigenschaft, bietet aber etwas mehr Möglichkeiten. Sie muss einen der Werte aus der CheckState-Aufzählung annehmen: Checked, Unchecked oder Indeterminate. Die ersten beiden bieten die gleiche Funktion wie die Checked-Eigenschaft (s.o.), während Indeterminate einen dritten Zustand des Steuerelements beschreibt; das Steuerelement ist mit Häkchen versehen, doch grau dargestellt, so dass es den Eindruck vermittelt, nur teilweise »angekreuzt« zu sein. Fügen Sie in Listing 6.1 nach Zeile 25 folgende Zeile hinzu und kompilieren Sie die Anwendung neu, um diesen Zustand zu sehen: chkColorBlue.CheckState = CheckState.Indeterminate

Per Voreinstellung können Sie diesen »unbestimmten« Zustand nur im Code auswählen. Wenn Sie jedoch die ThreeState-Eigenschaft auf true setzen, dann kann auch der Benutzer auf diesen Zustand umschalten. Um mit der CheckState-Eigenschaft fortzufahren: Das Ereignis CheckStateChanged wird ausgelöst, sobald diese Eigenschaft sich ändert, was doch dem Vorgang bei CheckChanged recht ähnlich ist, wenn sich die Eigenschaft Checked ändert. Da sowohl CheckBox als auch RadioButton von derselben Klasse erben wie das ButtonSteuerelement (ButtonBase), teilen sie viele der gleichen Eigenschaften: Text, TextAlign, PerformClick und FlatStyle, um nur ein paar Beispiele zu nennen.

6.3

Das Steuerelement ComboBox

Das ComboBox-Steuerelement (Kombinationsfeld) ist recht interessant; es kombiniert ein editierbares Textfeld (wie eine TextBox) mit einer Dropdown-Liste, aus der der Benutzer vorgegebene Optionen wählen kann. Wegen dieser zweifachen Funktionalität verfügt das Steuerelement über eine Reihe von Eigenschaften, um sein Aussehen zu bestimmen und ihm Wahlmöglichkeiten zuzuweisen.

183

Windows Forms mit Steuerelementen erweitern

Das Kombinationsfeld instantiieren Das Erstellen dieses Steuerelements verläuft wie bei jedem anderen auch: dim cboMyList as new ComboBox

Sie können die Eigenschaften Top, Left oder Location dazu verwenden, das Steuerelement im Formular zu positionieren, sowie die Eigenschaften Font, BackColor und andere, allen Steuerelementen zugängliche Eigenschaften, um sein Aussehen anzupassen. Sie können sogar die Text-Eigenschaft nutzen, um eine Zeichenfolge in der Textbox-ähnlichen Sektion des Steuerelements anzuzeigen. Bei den interessanteren Eigenschaften geht es jedoch um das Füllen der Dropdown-Liste. Die einfachste Möglichkeit, dem Kombinationsfeld solche Elemente hinzuzufügen, besteht in der Verwendung der Items-Auflistung und der Methoden Add oder AddRange. Die erste fügt der Liste ein einzelnes Element hinzu, die zweite ein komplettes Array von Objekten. Listing 6.2 demonstriert beide Methoden. Listing 6.2: Einer Dropdown-Liste in einer ComboBox neue Elemente hinzufügen 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: namespace TYWinforms.Day6 6: 7: public class Listing62 : Inherits Form 8: private cboMyList as new ComboBox 9: 10: public sub New 11: cboMyList.Location = new Point(75,50) 12: cboMyList.Text = "Wählen Sie ein Element aus" 13: cboMyList.Items.Add("Single Item") 14: cboMyList.Items.AddRange(new Object() {"Erster im Bereich",  "zweiter im Bereich ", "Dritter im Bereich "}) 15: 16: Me.Text = "Listing 6.2" 17: Me.Controls.Add(cboMyList) 18: end sub 19: 20: public shared sub Main() 21: Application.Run(new Listing62) 22: end sub 23: end class 24: end namespace

184

Das Steuerelement ComboBox

Wir fangen an, indem wir in den Zeilen 13 und 14 Elemente hinzufügen (gleich mehr dazu). Zeile 12 legt den Text fest, der im Textfeld des Steuerelements angezeigt wird. Dieser Text erscheint, falls kein anderes Element der Liste ausgewählt wurde. Auf diese Weise fungiert er stellvertretend als Standardwert oder -auswahl. Zeile 13 fügt der Liste mit Hilfe der Add-Methode ein einzelnes Element hinzu. Zeile 14 fügt der ComboBox ein Array hinzu. Sowohl die Add- als auch die AddRange-Methode übernehmen Object-Datentypen als Parameter. Hier erzeugen Sie Objekte vom Typ string, dem gebräuchlichsten Typ, doch Sie können leicht andere Typen hinzufügen. Wenn Sie der ComboBox wirklich einen anderen Objekttyp hinzufügen, dann stellen Sie bitte sicher, dass die Eigenschaft DisplayMember der Eigenschaft des Objekts entspricht, das Sie anzeigen wollen. So zum Beispiel die Text-Eigenschaft eines Button-Objekts: cboMyList.Items.Add(new Button()) cboMyList.DisplayMember = "Text"

Wenn Sie jedoch keine Eigenschaft für die Anzeige angeben, führt die CLR automatisch die ToString-Methode am fraglichen Objekt aus und zeigt das Ergebnis an. Beachten Sie bitte, dass Zeile 14 ein neues Object-Array erzeugt und es in derselben Zeile mit Werten füllt. Sie hätten anstelle dieses Codes auch folgende Zeilen als Alternative verwenden können, um das Gleiche zu erzielen: dim arrList as Object() = {"erstes im Bereich", "zweites im Bereich", "drittes im Bereich"} cboMyList.Items.AddRange(arrList)

Die Wahl zwischen diesen beiden Vorgehensweisen ist Ihnen überlassen. Abbildung 6.3 zeigt das Ergebnis des Listings. Statt die AddRange-Methode zu verwenden, um mehrere Werte auf einmal hinzuzufügen, können Sie auch die DataSource-Eigenschaft auf ein Objekt-Array setzen. Das folgende Codestück bewirkt das Gleiche wie Zeile 14 in Listing 6.2: cboMyList.DataSource = new Object() {"erstes im Bereich", "zweites im Bereich", "drittes im Bereich"}

Mit Hilfe von DataSource können Sie die Elemente der ComboBox leicht auf jede beliebige Auflistung von Objekten setzen. An Tag 9 werden Sie eine weitere Einsatzmöglichkeit für diese Eigenschaft im Zusammenhang mit Datenbanken kennen lernen. Wenn Sie der ComboBox mit Hilfe der Add-Methode viele Elemente hinzufügen, rufen Sie als Erstes die BeginUpdate-Methode auf: cboMyList.BeginUpdate()

185

Windows Forms mit Steuerelementen erweitern

Diese Methode zwingt die Anwendung, mit dem Aktualisieren der Oberfläche der ComboBox so lange zu warten, bis alle Elemente hinzugefügt worden sind. Das Aktualisieren der Benutzeroberfläche ist ein zeitraubender Vorgang, und wenn Sie viele Elemente auf einmal hinzuzufügen haben, könnten die Aktualisierungen Ihre Anwendung praktisch zum Stillstand bringen. Wenn Sie mit dem Hinzufügen von Elementen fertig sind, rufen Sie die EndUpdate-Methode auf: cboMyList.EndUpdate()

Abbildung 6.3: Hier können Sie das Einzelelement ebenso sehen wie die Bereichselemente, die wir unserem Beispiel für das ComboBoxSteuerelement hinzugefügt haben.

Die Anzeige des Steuerelements steuern DropDownStyle ist die erste Eigenschaft, die Sie kennen müssen, um die Anzeige der ComboBox zu steuern. Diese Eigenschaft kontrolliert das Aussehen und die Bedienung des Steuerelements und kann die Art und Weise der Interaktion des Benutzers mit der ComboBox ändern. Diese Eigenschaft muss auf einen der Werte der ComboBoxStyle-Aufzählung gesetzt

sein: 쐽

DropDown: Das ist der Standardwert. Der Benutzer kann den Textbereich bearbeiten und muss einen Pfeil anklicken, um die Dropdown-Inhalte ansehen zu können.



DropDownList: Der Benutzer kann den Textbereich nicht bearbeiten. Der DropdownBereich ist der gleiche wie beim DropDown-Wert.



Simple: Der Benutzer kann den Textbereich bearbeiten und die Dropdown-Liste wird

stets angezeigt (es gibt also keinen anzuklickenden Pfeil, um die Liste aufzuklappen, obwohl eine Bildlaufleiste vorhanden sein kann). Sobald Sie diese Eigenschaft festgelegt haben, gibt es eine Reihe weiterer Eigenschaften, mit denen sich das Aussehen des Textbereichs anpassen lässt. MaxLength legt die Anzahl derjenigen Zeichen fest, die maximal in das Textfeld eingegeben werden können (wenn

186

Das Steuerelement ComboBox

aber der Benutzer ein Element aus der Liste auswählt, das mehr Zeichen hat als MaxLength erlaubt, wird trotzdem der Gesamttext des Elements angezeigt). SelectedText gibt immer den momentan im Textbereich markierten Wert zurück. Um zu steuern, welcher Teil des angezeigten Textes markiert wird, verwenden Sie die Eigenschaften SelectionStart und SelectionLength. Die erste übernimmt einen Integer, der das Zeichen angibt, mit dem die Markierung beginnt. Die zweite Eigenschaft bestimmt, wie viele Zeichen nach der SelectStart-Position markiert werden. Die Methoden Select und SelectAll erlauben Ihnen, einen Teil oder die Gesamtheit des Textes im Textbereich zu markieren. SelectAll übernimmt keine Parameter, aber Select übernimmt zwei Integerwerte, die den zu markierenden Zeichenbereich angeben. Text zu markieren ist häufig nützlich, wenn es notwendig ist, Elemente in der ComboBox zu aktualisieren oder neue einzufügen. Auch die Dropdown-Liste verfügt über mehrere eigene Eigenschaften. DroppedDown zeigt an, ob die Dropdown-Liste gerade angezeigt wird. DropDownWidth gibt ähnlich wie MaxLength die Anzahl der Zeichen an, die in der Liste angezeigt werden können. Die Maßeinheit lautet hier allerdings Pixel statt Zeichenzahl. MaxDropDownItems gibt die Anzahl der Elemente an, die auf einmal angezeigt werden, falls der Benutzer auf die Dropdown-Liste klickt. Wenn sich in der Liste mehr Elemente befinden als MaxDropDownItems, erscheint an der Seite der Liste eine Bildlaufleiste, um dem Benutzer zu gestatten, mehr Elemente anzusehen. Zwei Eigenschaften betreffen die Höhe des Kombinationsfeldes. ItemHeight gibt die Höhe jedes Listenelements in Pixeln an. Diese Eigenschaft erlaubt keine Anpassung der Elementhöhe, nur die Betrachtung der Elemente (es sei denn, Sie überschreiben das Standardverhalten des Steuerelements, worüber Sie an Tag 18 mehr erfahren). Der Wert der Eigenschaft PreferredHeight wird vom Betriebssystem des Benutzers zugewiesen und gibt die tatsächliche Höhe des Steuerelements an. Schließlich können Sie noch die Sorted-Eigenschaft nutzen, um der ComboBox zu befehlen, die Listenelemente automatisch (alphabetisch) zu sortieren. Dies gilt sowohl für vorhandene Elemente als auch für jedes neu hinzugefügte Element. Wollen Sie diese Funktion, dann setzen Sie Sorted auf True, wenn nicht, dann auf False.

Die ComboBox verwenden Da Sie nun dem Benutzer alle Werte in der ComboBox zur Auswahl stellen, müssen Sie wissen, wie Sie die Auswahl kontrollieren und überwachen. Zwei der gebräuchlichsten ComboBox-Mitglieder sind die SelectedIndex-Eigenschaft und das SelectedIndexChangedEreignis. Listing 6.3 zeigt ein Beispiel, in dem diese Mitglieder eingesetzt werden.

187

Windows Forms mit Steuerelementen erweitern

Listing 6.3: Die Auswahl, die der Benutzer getroffen hat, überwachen 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: namespace TYWinforms.Day6 6: 7: public class Listing63 : Inherits Form 8: private cboMyList as new ComboBox 9: 10: public sub New 11: cboMyList.Location = new Point(75,50) 12: cboMyList.Text = " Wählen Sie ein Element!" 13: dim arrList as Object() = {"erstes im Bereich", "zweites im  Bereich", "drittes im Bereich "} 14: cboMyList.Items.AddRange(arrList) 15: AddHandler cboMyList.SelectedIndexChanged,  new EventHandler(AddressOf Me.HandleSelect) 16: 17: Me.Text = "Listing 6.3" 18: Me.Controls.Add(cboMyList) 19: end sub 20: 21: public sub HandleSelect(Sender as Object, e as EventArgs) 22: MessageBox.Show("Sie haben folgendes Element ausgewählt: " &  cboMyList.SelectedIndex.ToString) 23: end sub 24: 25: public shared sub Main() 26: Application.Run(new Listing63) 27: end sub 28: end class 29: end namespace

Dieses Listing ähnelt dem Listing 6.2. Die ComboBox wird in den Zeilen 13 und 14 gefüllt, wobei diesmal nur ein Array benutzt wird. In Zeile 15 wird dann dem SelectedIndexChanged-Ereignis ein Delegat zugewiesen. Er tritt nur dann in Aktion, wenn der Benutzer ein neues Listenelement auswählt. Der Handler HandleSelect für dieses Ereignis beginnt in Zeile 21. Er besteht aus einer einzigen Codezeile. Mit Hilfe der Methode MessageBox.Show zeigen Sie dem Benutzer eine Meldung an, in der Sie angeben, welches Listenelement er ausgewählt hat. Denken Sie daran, dass alle Listen in .NET nullbasiert sind (d.h. die Zählung ihrer Elemente beginnt

188

Das Steuerelement DateTimePicker

bei 0 statt bei 1), so dass bei Auswahl des zweiten Elements durch den Benutzer das Meldungsfeld »Element 1« als ausgewählt zeigen wird. Abbildung 6.4 zeigt das Ergebnis, das Listing 6.3 produziert.

Abbildung 6.4: SelectedIndex zeigt die Indexnummer des ausgewählten

Listenelements an.

In den meisten Fällen werden Ihnen diese beiden Mitglieder genügend Kontrolle über das Steuerelement geben. Es gibt noch eine Reihe weiterer nützlicher Methoden: FindString und FindStringExact. Die erste Methode sucht und liefert den Index für das erste Listenelement, das mit der angegebenen Zeichenfolge beginnt. In Listing 6.3 würde folgendes Codestück die Zahl 0 zurückgeben: cboMyList.FindString("1st") FindStringExact hingegen gibt das erste Listenelement zurück, das der (gesuchten) Zei-

chenfolge exakt entspricht. Das obige Codestück würde nichts zurückgeben, weil es keine Elemente gibt, die nur den Text »1st« enthalten. Diese Methode erweist sich als nützlich, wenn man in einer ComboBox nach einem bestimmten Wert suchen muss. Insbesondere ist sie für den Benutzer von Vorteil, wenn die Liste recht umfangreich ist. Kurzum: Die ComboBox ist ein sehr leistungsfähiges Steuerelement; Sie müssen bloß wissen, wie Sie es einsetzen.

6.4

Das Steuerelement DateTimePicker

Gemäß der Voreinstellung verhält sich das Steuerelement DateTimePicker recht ähnlich wie die ComboBox; es verfügt über einen editierbaren Textbereich und eine aufklappbare Liste. Doch besteht der Textbereich aus einem änderbaren Datum und die »Liste« aus einem interaktiven Kalender. Abbildung 6.5 zeigt ein typisches DateTimePicker-Steuerelement. Dieses Steuerelement präsentiert uns eine sehr leistungsfähige Benutzeroberfläche. Der Benutzer kann im Steuerelement das Datum ändern, indem er es direkt in den Textbe-

189

Windows Forms mit Steuerelementen erweitern

reich einträgt oder indem er eines aus dem Kalenderbereich auswählt. Der Kalenderbereich verfügt über Schaltflächen, mit deren Hilfe der Benutzer monatsweise vor und zurück springen kann. Er kann auch Monate auswählen, indem er nach einem Klick auf die Titelleiste aus der herunterklappenden Liste einen Monat auswählt. Das Beste aber kommt noch: Der DateTimePicker gestattet nur die Auswahl oder Eingabe von realen, gültigen Daten. Daher kann ein Benutzer niemals das Datum »57. Macomber 1345« eingeben, wie sehr er es auch versuchen mag.

Abbildung 6.5: Ein DateTimePicker stellt Ihnen Daten und Uhrzeiten zur Auswahl bereit.

Der DateTimePicker weist eine Reihe von Eigenschaften auf, die sich zur Anpassung des Kalenderaussehens nutzen lassen: Eigenschaft

Wird zur Einstellung von x benutzt

CalendarFont

die im Kalender benutzte Schriftart

CalendarForeColor

die Farbe des Textes

CalendarMonthBackground

die Farbe hinter dem Monat

CalendarTitleBackColor

die Farbe hinter dem Titel

CalendarTitleForeColor

die Farbe des Titeltextes

CalendarTrailingForeColor

die Farbe der im Kalender nachfolgenden Datumswerte (die nicht mehr zum aktuellen Monat gehören)

Tabelle 6.1: So gestalten Sie den Kalender.

Sie können das Erscheinungsbild ganz Ihren Wünschen anpassen. Die Format-Eigenschaft erlaubt Ihnen zudem, die Anzeigeform der Daten im Textbereich zu steuern. Diese müssen die Werte aus der DateTimePickerFormat-Aufzählung aufweisen: 쐽

Custom: ein angepasstes Anzeigeformat; verwenden Sie die CustomFormat-Eigenschaft.



Long: das vom Betriebssystem des Benutzers ausgewählte lange Datumsformat. Zum Beispiel »Mittwoch, 7. November 2001«.

190

Das Steuerelement DateTimePicker



Short: das vom Betriebssystem des Benutzers ausgewählte kurze Datumsformat. Zum Beispiel »7.11.2001«



Time: Die vom Betriebssystem des Benutzers vorgenommene Formatierung der Zeitangabe. Zum Beispiel »9:48:25«

Falls das Format auf DateTimePickerFormat.Custom eingestellt ist, können Sie die CustomFormat-Eigenschaft dazu verwenden, die Art und Weise anzupassen, wie Datum und Zeit angezeigt werden. Diese Eigenschaft benutzt speziell formatierte Zeichenfolgen, um Datums- und Zeitangaben darzustellen. Das folgende Codestück würde »01. Juni, 0930« anzeigen: dtpMyDate.CustomFormat = "dd. MMMM, HHmm"

Tabelle 6.2 führt alle verfügbaren Zeichenfolgen auf (Achtung: bei den Zeichenfolgenwerten ist die Groß-/Kleinschreibung zu berücksichtigen). Formatzeichenfolge

Beschreibung

d

Tagesangabe mit einer oder zwei Ziffern

dd

Tagesangabe mit zwei Ziffern

ddd

Drei-Zeichen-Abkürzung für Wochentag

dddd

Vollständiger Wochentagsname

h

Ein- oder Zwei-Ziffern-Ausgabe der Stunde im 12-Stunden-Format (z.B. 1 p.m.)

hh

Zwei-Ziffern-Ausgabe der Stunde im 12-Stunden-Format

H

Ein- oder Zwei-Ziffern-Ausgabe der Stunde im 24-Stunden-Format (z.B. 1300 Uhr)

HH

Zwei-Ziffern-Ausgabe der Stunde im 24-Stunden-Format

m

Ein- oder Zwei-Ziffern-Ausgabe der Minute

mm

Zwei-Ziffern-Ausgabe der Minute

M

Ein- oder Zwei-Ziffern-Ausgabe des Monats

MM

Zwei-Ziffern-Ausgabe des Monats

MMM

Drei-Zeichen-Abkürzung für Monat

MMMM

Vollständiger Monatsname

Tabelle 6.2: Datum- und Zeit-relevante Zeichenfolgen für CustomFormat

191

Windows Forms mit Steuerelementen erweitern

Formatzeichenfolge

Beschreibung

s

Ein- oder Zwei-Ziffern-Ausgabe der Sekunde

ss

Zwei-Ziffern-Ausgabe der Sekunde

t

Einbuchstabige Abkürzung für AM/PM (z.B. wird »AM« als »A« angezeigt)

tt

Zweibuchstabige Abkürzung für AM/PM

y

Ein- oder Zwei-Ziffern-Ausgabe der Jahreszahl (z.B. wird »2001« als »1« angezeigt)

yy

Die letzten beiden Ziffern der Jahreszahl

yyyy

Alle vier Ziffern der Jahreszahl

Tabelle 6.2: Datum- und Zeit-relevante Zeichenfolgen für CustomFormat (Forts.)

Die Value-Eigenschaft gibt ein DateTime-Objekt zurück, das die gerade im Steuerelement angezeigte Zeit zurückgibt. MaxDate und MinDate geben die maximalen (am weitesten in der Zukunft liegenden) und minimalen (am weitesten in der Vergangenheit liegenden) Daten zurück, die der Benutzer aus dem Kalender auswählen kann. Diese Eigenschaften übernehmen DateTime-Objekte als Werte. Das folgende Codestück setzt das MinimumDatum auf den 20. Juni 1985 fest: dtpMinDate = new DateTime(1985, 6, 20) DropDownAlignment übernimmt einen Wert aus der LeftRightAlignment-Aufzählung, um anzugeben, nach welcher Seite des Textbereichs der Kalender ausgerichtet sein soll. Mögliche Werte dafür sind LeftRightAlignment.Left (Standardwert) und LeftRightAlignment.Right.

Es gibt eine Reihe von Möglichkeiten, wie Sie dem Benutzer gestatten können, das Datum im Textbereich zu ändern. Bei der ersten Version (Standardeinstellung) kann der Benutzer den Wert eingeben. Der DateTimePicker erlaubt nur gültige Einträge (der genaue Typ kann von der Format-Eigenschaft abhängen), daher müssen Sie sich nicht um ungültige Daten kümmern. Die zweite Methode erlaubt es dem Benutzer, ein Auf-Ab-Steuerelement zu verwenden (in der Art einer kleinen Bildlaufleiste), um die Werte zu ändern. Für diese Methode müssen Sie die ShowUpDown-Eigenschaft auf True setzen. Setzt man ShowCheckBox auf True, wird ein Kontrollkästchen neben dem Datums im Textbereich angezeigt. Dadurch kann der Benutzer signalisieren, dass er ausdrücklich das angezeigte Datum auswählt. Das kann sich als nützlich erweisen, wenn Sie vom Benutzer verlangen, dass er einen Datumsbereich angibt. Indem er das Kontrollkästchen deaktiviert lässt, könnte der Benutzer ein Open-End- oder ein Startdatum angeben. Verwenden Sie die Checked-Eigenschaft, um herauszufinden, ob das Kontrollkästchen gerade angekreuzt ist.

192

Das Steuerelement ImageList

Es gibt eine paar wichtige Ereignisse im Steuerelement DateTimePicker. ValueChanged wird ausgelöst, sobald sich der Wert im Textbereich ändert, ob nun durch direkte Bearbeitung oder durch eine Auswahl aus dem Kalender. Die CloseUp- und DropDown-Ereignisse finden statt, sobald der Kalenderbereich des Steuerelements geschlossen respektive geöffnet wird. Sie könnten diese Ereignisse ausnutzen, um Gültigkeitsprüfungen auszuführen, etwa um zu sehen, ob ein ausgewähltes Datum in einen gültigen Bereich fällt, bevor Sie dem Benutzer erlauben fortzufahren.

6.5

Das Steuerelement ImageList

Wir haben das ImageList-Steuerelement bereits an Tag 4 besprochen, so dass wir uns damit nicht allzu lange aufhalten wollen. Die herausragende Eigenschaft von ImageList ist Images, welche eine Auflistung von Grafiken aufnimmt, die sich von anderen Teilen Ihrer Anwendung nutzen lassen. Um Bilder hinzuzufügen, verwenden Sie einfach die AddMethode, etwa so: private ImageList ilstToolbar = new ImageList(); ilstToolbar.Images.Add(Image.FromFile("i_open.bmp ")); ilstToolbar.Images.Add(Image.FromFile("i_maximize.bmp "));

Um Bilder zu entfernen, verwenden Sie Remove, wobei Sie den Index des zu entfernenden Bildes angeben. Es gibt nur wenige weitere Eigenschaften: ColorDepth, die zu jedem gelisteten Bild dessen Anzahl an Farben angibt (ausgedrückt als Bit-Wert: 4 Bit, 8 Bit usw.); ImageSize, die sich für die Größenänderung der Bilder einsetzen lässt; und schließlich TransparentColor, welche eine Farbe im Bild durchsichtig machen kann, wodurch Dinge hinter dem Bild zum Vorschein kommen. Listing 6.4 zeigt ein typisches Beispiel einer ImageList, die mit einem Button-Steuerelement interagiert. Listing 6.4: Die Verwendung von ImageList in C# 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day6 { public class Listing64 : Form { private ImageList ilstButton = new ImageList(); private Button btImage = new Button(); public Listing64() { ilstButton.Images.Add(Image.FromFile("i_open.bmp"));

193

Windows Forms mit Steuerelementen erweitern

13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:

ilstButton.Images.Add(Image.FromFile("i_maximize.bmp")); ilstButton.Images.Add(Image.FromFile("i_happy.bmp")); btImage.ImageList = ilstButton; btImage.ImageIndex = 0; btImage.Location = new Point(100,100); btImage.Click += new EventHandler(this.CycleImages); this.Text = "Listing 6.4"; this.Controls.Add(btImage); } public void CycleImages(Object Sender, EventArgs e) { if (btImage.ImageIndex == ilstButton.Images.Count-1) { btImage.ImageIndex = 0; } else { btImage.ImageIndex += 1; } } public static void Main() { Application.Run(new Listing64()); } } }

Die ImageList wird in den Zeilen 12 bis 14 mit Bildern gefüllt (diese Zeilen sollten auf tatsächlich vorhandene Bilder zeigen). Button verfügt wie viele Windows Forms-Steuerelemente über eine ImageList-Eigenschaft, die die Zuweisung einer ImageList zu diesem Steuerelement zulässt. Zeile 16 weist die ImageList dem Button-Steuerelement zu. Zeile 17 stellt die Schaltfläche so ein, dass sie zunächst das erste Bild in der Liste anzeigt. Zeile 18 positioniert die Schaltfläche auf dem Formular und Zeile 19 fügt dem Click-Ereignis einen Delegaten hinzu. Der Ereignishandler CycleImages (Zeile 25) ändert das auf der Schaltfläche angezeigte Bild, führt aber zuvor Prüfungen durch. Klickt der Benutzer auf die Schaltfläche, soll das nächste Bild angezeigt werden. Dies wird erreicht, indem einfach die ImageIndex-Eigenschaft nach oben gezählt (inkrementiert) wird (Zeile 29). Sobald man jedoch beim letzten Bild in der Liste angelangt ist, ändert sich das Bild mangels Vorrat nicht mehr. In diesem Fall setzt man den ImageIndex auf 0 zurück, so dass der Hochzählzyklus von neuem beginnen kann. In Zeile 26 wird eine Prüfung durchgeführt, um herauszufinden, ob der aktuelle Wert ImageIndex-Wert bereits auf dem letzten Listenwert (Count –1) angelangt ist. Ist das der Fall, startet ImageIndex wieder auf 0, andernfalls wird auf den nächsten Wert weitergezählt.

194

Das Steuerelement ListBox

Abbildung 6.6 zeigt das Ergebnis dieses Code-Beispiels.

Abbildung 6.6: Weist man einem Button eine ImageList zu, führt dies zur Anzeige von Grafiken statt Text auf der Schaltfläche.

6.6

Das Steuerelement ListBox

Das ListBox-Steuerelement (Listenfeld) ähnelt dem ComboBox-Steuerelement (Letzteres besteht aus einer Kombination von ListBox und TextBox). Ihnen gemeinsam sind solche Mitglieder wie Items, DataSource, DisplayMember, SelectedIndex, FindString usw. Sowohl ListBox als auch ComboBox sind von derselben Basisklasse abgeleitet, nämlich ListControl. Das die ListBox keine Kombination von Elementen darstellt, ist sie weitaus spezialisierter in ihren Aufgaben als die ComboBox. Daher verfügt sie über einige Mitglieder mehr. Mit Hilfe der ListBox lassen sich dem Benutzer mehrere Elemente in einer Liste anzeigen. Eine ListBox ist stets sichtbar, denn sie wird nicht wie die Dropdown-Liste in der ComboBox ausgeblendet. Und anders als dort kann der Benutzer in der ListBox mehrere Elemente auf einmal auswählen, indem er dabei einfach die (Strg)- oder (ª)-Taste gedrückt hält. Dieses Verhalten der ListBox bedingt auch eine komplexere Funktionalität mit sich. Das Beispiel in Listing 6.5 verwendet fast alle verfügbaren Eigenschaften von ListBox. Listing 6.5: Die Verwendung des ListBox-Steuerelements mit C# 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day6 { public class Listing65 : Form { private ListBox lbItems = new ListBox(); public Listing65() {

195

Windows Forms mit Steuerelementen erweitern

11: 12: 13: 14:

lbItems.Location = new Point(75,75); lbItems.MultiColumn = true; lbItems.ColumnWidth = 75; lbItems.SelectedIndexChanged += new EventHandler(this.HandleSelect); lbItems.SelectionMode = SelectionMode.MultiExtended;

15: 16: 17:

Object [] arrColors = {"Weiss", "Rot", "Orange", "Gelb", "Grün", "Blau", "Indigo", "Violett", "Schwarz"}; lbItems.Items.AddRange(arrColors);

18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:

this.Text = "Listing 6.5"; this.Controls.Add(lbItems); } public void HandleSelect(Object Sender, EventArgs e) { ListBox lbTemp = (ListBox)Sender; int i; if (lbTemp.SelectedIndices.Count == 1 ) { lbItems.BackColor = Color.FromName(lbTemp.SelectedItem.ToString()); } else { for (i = 0; i < lbTemp.SelectedIndices.Count; i++) { MessageBox.Show("Sie haben folgendes Element ausgewählt: " + lbTemp.SelectedItems[i].ToString()); } }

30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:

} public static void Main() { Application.Run(new Listing65()); } } }

Die Zeilen 11 bis 18 enthalten die meisten für uns interessanten Eigenschaften. Zeile 11 kennen Sie ja bereits; sie legt die Position des Steuerelements im Formular fest. Die ListBox kann mehrere Spalten von Elementen zugleich anzeigen. Wenn Sie MultiColumn wie in Zeile 12 auf true setzen, erzeugt die ListBox so viele Spalten wie nötig sind, um ihre Elemente aufzunehmen, damit der Benutzer keinen Bildlauf nach unten ausführen muss. (Beachten Sie, dass diese Bildlauffunktion bereits im ListBox-Steuerelement eingebaut ist; daher müssen Sie sich nicht um die betreffenden Ereignisse kümmern.) Die Abbildung 6.7 zeigt ein Beispiel für dieses Konzept.

196

Das Steuerelement ListBox

Multicolumn = true; (Spalten werden auf angemessene Weise erzeugt, so dass vertikales Scrollen nicht nötig ist) Element 1 Element 6 Element 2 Element 7 Element 3 Element 8 Element 4 Element 5

Multicolumn = false; (Voreinstellung) (Der Benutzer muss vertikal scrollen, um alle Elemente sehen zu können)

Element 1 Element 2 Element 3 Element 4 Element 5 Element 6 Element 7 Element 8

Abbildung 6.7: MultiColumn gibt an, ob sich Listenelemente horizontal oder vertikal scrollen lassen.

Zeile 13 legt einfach die Breite jeder Spalte fest, sofern MultiColumn auf true gesetzt ist. Zeile 14 weist dem SelectedIndexChanged-Ereignis einen Delegaten zu, genau wie beim gleichnamigen Ereignis in der ComboBox. Die Zeile 15 legt den SelectionMode der ListBox fest. Diese Eigenschaft bestimmt, wie der Benutzer Elemente auswählen kann (Mehrfach- oder Einzelauswahl usw.). Die möglichen Werte der SelectionMode-Aufzählung sind: 왘

MultiExtended: Der Benutzer kann mehrere Elemente mit Hilfe der (ª)-, (Strg)- und Pfeiltasten auswählen. (Dies ist die übliche Vorgehensweise,

um Mehrfachauswahl zu gestatten.) 왘

MultiSimple: Der Benutzer kann mehrere Elemente auswählen, indem er

auf eines nach dem anderen klickt. Jeder weitere Klick nimmt ein neues Element in die bestehende Auswahl auf, daher ist keine Zusatztaste wie etwa (ª) oder (Strg) nötig. 왘

None: Es können keine Elemente ausgewählt werden.



One: Es kann nur ein Element ausgewählt werden.

In den Zeilen 17 und 18 erstellen Sie schließlich ein Array von Zeichenfolgenobjekten und weisen es der ListBox mit Hilfe der AddRange-Methode zu. Beachten Sie, dass wir hier Farbennamen verwenden. Gleich werden Sie sehen, wie diese Farben eingesetzt werden. Springen wir hinunter in Zeile 24: Hier ist der Ereignishandler für das Ereignis SelectedIndexChanged zu finden. Zunächst sind in dieser Methode zwei Variablen zu erstellen: eine ListBox und ein Integer. Die erste Variable wird als

Abkürzung zum Objekt verwendet, das das Ereignis erzeugt hat. Denken Sie daran, dass die Sender-Variable zuerst in den richtigen Typ zu konvertieren ist, um sie benutzen zu können: ((ListBox)Sender).eigenschaft

Um Ihnen zu ersparen, den Typ jedes Mal konvertieren zu müssen, tun Sie das einmal in Zeile 25 und verwenden ab jetzt die neue lbTemp-Variable, die nun

197

Windows Forms mit Steuerelementen erweitern

identisch mit dem Objekt lbItems ist, das dieses Ereignis generiert hat. Dieser Vorgang ist zwar nicht unbedingt nötig, erspart aber einiges an Tipparbeit. Es gibt zwei mögliche Situationen: Der Benutzer hat ein Element markiert, und der Benutzer hat mehr als ein Element markiert. Im Einzelelement-Fall wollen wir die Hintergrundfarbe der ListBox auf die gewählte Farbe einstellen. In der Mehrfachelement-Situation können wir offensichtlich den Hintergrund der ListBox nicht auf mehrere Farben gleichzeitig setzen; statt dessen zeigen wir einfach eine Meldung an, die den Benutzer über seine Auswahl informiert (eigentlich wird nur seine letzte Auswahl in der Meldung angezeigt). In Zeile 28 benutzen Sie die SelectedIndices-Eigenschaft, die die Indizes der ausgewählten Elemente zurückgibt, ganz gleich, wie viele Elemente ausgewählt worden sind. Sollte die Anzahl der Indizes in SelectedIndices (wird durch die Count-Eigenschaft angegeben) gleich 1 sein, dann wissen Sie, dass nur ein Element ausgewählt wurde. In Zeile 29 verwenden Sie dann die FromName-Methode des Color-Objekts, um den richtigen Color-Wert je nach dem ausgewählten Element zurückzugeben. Denken Sie daran, dass die SelectedItem-Eigenschaft ein String-Objekt liefert, so dass Sie die ToString-Methode verwenden müssen, um statt dessen die Zeichenfolgen-Darstellung zu erhalten. Die Zeilen 30 bis 33 behandeln jenen Fall, bei dem es mehr als eine Auswahl gibt. Mit Hilfe der Integer-Variablen i, die Sie zuvor erzeugt haben, führen Sie eine Programmschleife durch jedes der ausgewählten Elemente aus. Die forSchleife setzt die Variable i auf 0 und fährt fort, sie zu inkrementieren (und den Code in der Schleife auszuführen), bis sie nicht mehr kleiner als die Anzahl der ausgewählten Indizes ist (welche durch SelectedIndices.Count angegeben wird). Der Code in der Programmschleife verwendet die MessageBox.ShowMethode, um eine Meldung an den Benutzer auszugeben. SelectedItems ist eine Auflistung der ausgewählten Elemente (ähnlich wie SelectedIndices eine Auflistung der Indexzahlen der ausgewählten Elemente ist). Das war's schon. Abbildung 6.8 zeigt das Ergebnis, nachdem eine einzelne Farbe ausgewählt wurde (die Hintergrundfarbe der ListBox hat sich geändert) und dann mehrere Elemente. Es gibt noch ein paar weitere Mitglieder der ListBox kennen zu lernen. ScrollAlwaysVisible lässt sich auf true setzen, damit die ListBox ständig eine senkrechte Bildlaufleiste anzeigt, selbst wenn alle Elemente auch ohne Bildlauf sichtbar wären. UseTabStops ist eine interessante Eigenschaft. Falls das Element oder der Text, den Sie in der ListBox anzeigen wollen, Tabulatorzeichen enthält, veranlasst das Setzen dieser Eigenschaft auf true die normale Anzeige dieser Tabulatoren. Ist sie jedoch auf false gesetzt,

werden die Tabulatorzeichen zu Leerzeichen umgewandelt.

198

Das Steuerelement PictureBox

Abbildung 6.8: Die ListBox gestattet die Mehrfachauswahl.

Die ListBox verfügt über eine Text-Eigenschaft, die sich von den Text-Eigenschaften aller anderen Steuerelemente unterscheidet. Wurde ein Element bereits ausgewählt, dann gibt Text den Text dieses Elements zurück. Wurden mehrere Elemente ausgewählt, gibt Text den Text des ersten ausgewählten Elements zurück. Wenn Sie jedoch versuchen, Text gleich einem Wert zu setzen, dann sucht es diesen Text und wählt dieses Element genau wie die FindStringExact-Methode aus. Der folgende Code würde beispielsweise das Element mit dem Text »Hallo Welt« auswählen (vorausgesetzt, es gibt ein Element mit diesem Text): lbTemp.Text = "Hallo Welt";

6.7

Das Steuerelement PictureBox

Das Steuerelement PictureBox ist sehr einfach. Seine einzige Aufgabe besteht in der Anzeige einer Grafik. Werfen wir einen Blick auf seine Eigenschaften. Die wahrscheinlich wichtigste ist wohl die Image-Eigenschaft. Sie gibt an, welches Bild die PictureBox anzeigen wird. Bei diesem Bild kann es sich um praktisch jede Grafikdatei handeln, so etwa im GIF-, JPG- oder BMP-Format. Zum Beispiel: dim pbMyPicture as new PictureBox pbMyPicture.Image = Image.FromFile("i_happy.bmp")

Stellen Sie auf jeden Fall sicher, dass das zu verwendende Bild eine gültige, also vorhandene Datei ist und Sie dafür die richtige Pfadangabe bereitstellen (Grafikdateien werden üblicherweise im selben Ordner wie die Anwendung gespeichert). SizeMode steuert, wie die Grafiken angezeigt werden und nimmt einen der folgenden Werte der PictureBoxSizeMode-Aufzählung an:

199

Windows Forms mit Steuerelementen erweitern



AutoSize: Die PictureBox passt sich an die Größe der enthaltenen Grafik an.



CenterImage: Das Bild wird in der Mitte der PictureBox angezeigt. Ist das Bild größer als die PictureBox, werden seine äußeren Ränder abgeschnitten.



Normal: Das Bild wird an der linken oberen Ecke der PictureBox ausgerichtet. Ist das Bild größer als die PictureBox, werden seine äußeren Ränder abgeschnitten.



StretchImage: Das Bild wird entweder gestaucht oder gestreckt, um die PictureBox

vollständig auszufüllen. An Tag 13 erfahren Sie, wie man Bildlaufleisten in Verbindung mit der PictureBox einsetzt, um in einem Bild zu navigieren, das größer als die PictureBox ist.

6.8

Das Steuerelement TabControl

Im Betriebssystem Windows findet man häufig Fenstern, die in der Art von Karteikarten gestaltet sind – Fenster mit mehreren so genannten Registerkarten (auch als Tabs oder Tabseiten bezeichnet), die bestimmte Funktionen gruppieren. Abbildung 6.9 beispielsweise zeigt das Fenster EIGENSCHAFTEN VON ANZEIGE in Windows, das über fünf Registerkarten verfügt. Ähnlich wie die ToolBar- und MainMenu-Steuerelemente besteht auch TabControl aus dem Hauptsteuerelement und mehreren untergeordneten Steuerelementen. Jede Tabseite wird durch ein TabPage-Steuerelement dargestellt. Um dem TabControl eine neue Tabseite hinzuzufügen, ruft man einfach die Add-Methode auf: TabControl tcMain = new TabControl(); tcMain.TabPages.Add(new TabPage("Tab 1");

Diese Steuerelemente sind am einfachsten mit Hilfe eines Beispiels zu verstehen. Es ist ein recht umfangreiches Beispiel, weswegen ich es auf mehrere Abschnitte aufteilen werde, um die Untersuchung zu erleichtern. Der erste Teil, das Anwendungsgerüst, ist in Listing 6.6 zu sehen. Listing 6.6: So erzeugen Sie in C# Registerkarten für Ihre Anwendung. 1: 2: 3: 4: 5: 6: 7:

200

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day6 { public class Listing66 : Form {

Das Steuerelement TabControl

8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:

private TabControl tcMain = new TabControl(); private TabPage tpGeneral = new TabPage("Allgemein"); private TabPage tpDisplay = new TabPage("Anzeige"); private TabPage tpLocation = new TabPage("Position"); private TabPage tpAdvanced = new TabPage("Extras"); private private private private private private private private

Label lbl1Name = new Label(); TextBox tb2Color = new TextBox(); Button bt2Color = new Button(); TextBox tb3Top = new TextBox(); TextBox tb3Left = new TextBox(); Button bt3Location = new Button(); TextBox tb4Tab = new TextBox(); Button bt4Tab = new Button();

public static void Main() { Application.Run(new Listing66()); } } }

Abbildung 6.9: Mit dem Steuerelement TabControl lassen sich Fenster mit Registerkarten erstellen.

201

Windows Forms mit Steuerelementen erweitern

Dieses Listing ist sehr einfach. Die passenden Namensräume werden in den Zeilen 1 bis 3 importiert. Die Klasse und die Namensräume werden in den Zeilen 5 und 7 deklariert und alle benötigten Steuerelemente in den Zeilen 8 bis 21. Zu diesen gehören das TabControl, vier TabPage- und diverse Button- und Label-Steuerelemente. Zeile 23 deklariert die Methode Main, die wie üblich lediglich die Run-Methode aufruft. Listing 6.7 zeigt den Konstruktor für diese Klasse, der zwischen die Zeilen 22 und 23 des Listings 6.6 platziert werden sollte. Listing 6.7: Beispiel für einen TabControl-Konstruktor 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34:

202

public Listing66() { // Tabseite 1 lbl1Name.Text = "Listing 6.6: Windows Forms in 21 Tagen"; lbl1Name.AutoSize = true; lbl1Name.Location = new Point(10,75); // Tabseite 2 tb2Color.Location = new Point(25,75); bt2Color.Text = "Ändern!"; bt2Color.Location = new Point(125,75); bt2Color.Click += new EventHandler(this.ChangeColor); // Tabseite 3 tb3Top.Text = this.DesktopLocation.Y.ToString(); tb3Top.Location = new Point(25,50); tb3Left.Text = this.DesktopLocation.X.ToString(); tb3Left.Location = new Point(25,75); bt3Location.Text = "Ändern!"; bt3Location.Location = new Point(125,75); bt3Location.Click += new EventHandler(this.ChangeLocation); // Tabseite 4 tb4Tab.Location = new Point(25,50); bt4Tab.Text = "Neuer Tab!"; bt4Tab.Location = new Point(75,75); bt4Tab.Click += new EventHandler(this.AddTab); tpGeneral.Controls.Add(lbl1Name); tpDisplay.Controls.Add(tb2Color); tpDisplay.Controls.Add(bt2Color); tpLocation.Controls.Add(tb3Top);

Das Steuerelement TabControl

35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49:

tpLocation.Controls.Add(tb3Left); tpLocation.Controls.Add(bt3Location); tpAdvanced.Controls.Add(tb4Tab); tpAdvanced.Controls.Add(bt4Tab); tcMain.Width = this.Width; tcMain.Height = this.Height; tcMain.TabPages.Add(tpGeneral); tcMain.TabPages.Add(tpDisplay); tcMain.TabPages.Add(tpLocation); tcMain.TabPages.Add(tpAdvanced); this.Text = "Listing 6.6"; this.Controls.Add(tcMain); }

Dieser Teil des Codes ist in Abschnitte für jede Registerkarte aufgeteilt. Unser Entwurf erfordert vier Registerkarten: »Allgemein«, die allgemeine Informationen zur Anwendung anzeigt; »Anzeige«, die dem Benutzer die Anpassung der Farben der Anwendung erlaubt; »Position«, die dem Benutzer erlaubt, die Position der Anwendung auf dem Desktop mit Programmschritten anzupassen; und schließlich »Extras«, die dem Benutzer das Hinzufügen weiterer Tabseiten zur Anwendung gestattet. Die Zeilen 2 bis 5 richten die Steuerelemente ein, die auf der ersten Tabseite erscheinen werden. Hier haben wir lediglich ein Label-Steuerelement. In den Zeilen 8 bis 12 findet die Initialisierung der zweiten Tabseite statt. Wir erzeugen eine TextBox, in der der Benutzer eine neue Farbe eintragen kann, sowie eine Schaltfläche, um den Wert zu bestätigen. (Der Ereignishandler ChangeColor ist unten in Listing 6.8 zu finden.) Die Tabseite 3, in den Zeilen 15 bis 22 zu finden, enthält zwei Textfelder (eines für den oberen Rand der Anwendung und eines für den linken) und eine Schaltfläche, um die neuen Werte übernehmen zu können. Tabseite Nummer 4 weist ein Textfeld und eine Schaltfläche auf. (Beide Ereignishandler für diese Steuerelemente finden sich unten in Listing 6.8.) Nichts Aufregendes also bis jetzt. Als Nächstes müssen wir die Steuerelemente den passenden Tabseiten hinzufügen. Dies erfolgt ähnlich wie das Hinzufügen von Steuerelementen zu einem Formular: mit Hilfe der Controls.AddMethode. Die Zeilen 31 bis 38 fügen alle vorgestellten Steuerelemente in die entsprechenden Tabseiten ein. Die Zeilen 40 bis 45 stellen die Eigenschaften für das zentrale TabControlObjekt ein. Nun müssen wir alle Tabseiten dem TabControl hinzufügen, analog dem Einfügen von Steuerelementen in Tabseiten. In Zeile 48 müssen Sie das TabControl dem Form hinzufügen.

203

Windows Forms mit Steuerelementen erweitern

Sie können die Anwendung bereits in dieser Form kompilieren (vergessen Sie jedoch nicht, zuvor die Zeilen 12, 22 und 29 in Listing 6.7 auszukommentieren – diese Zeilen würden Fehler verursachen, da wir die entsprechenden Ereignishandler noch nicht erstellt haben). Beachten Sie, dass die Registerkarten bereits voll funktionsfähig sind: Sie können darin navigieren, ohne dafür extra Code geschrieben zu haben! Wir wollen die Ereignishandler aus Listing 6.8 näher untersuchen. Dieser Code sollte nach dem Konstruktor eingefügt werden, jedoch erst hinter der Main-Methode aus Listing 6.6 (beachten Sie, dass der Code nicht unbedingt hier stehen muss; das ist lediglich ein Vorschlag). Listing 6.8: Ereignisverarbeitung mit dem TabControl 1: public void ChangeColor(Object Sender, EventArgs e) { 2: int i; 3: 4: for (i = 0; i < tcMain.TabPages.Count; i++) { 5: tcMain.TabPages[i].BackColor = Color.FromName(tb2Color.Text); 6: } 7: } 8: 9: public void ChangeLocation(Object Sender, EventArgs e) { 10: this.DesktopLocation = new Point(Convert.ToInt32 (tb3Top.Text),  Convert.ToInt32(tb3Left.Text)); 11: } 12: 13: public void AddTab(Object Sender, EventArgs e) { 14: tcMain.TabPages.Add(new TabPage(tb4Tab.Text)); 15: }

Die Zeilen 1 bis 7 umfassen die ChangeColor-Methode. Sie ist dafür zuständig, die vom Benutzer angegebene Farbe zu übernehmen und sie auf die Applikation anzuwenden. Die Methode wird ausgeführt, sobald die bt2color-Schaltfläche auf der 2. Tabseite angeklickt wird. Das Ändern der Farbe ist jedoch nicht so einfach, wie es aussieht. Man könnte die BackColor-Eigenschaft des Formulars auf die neue Farbe umstellen, doch da die Tabseiten das gesamte Formular überdecken, würde man die Farbänderung nicht sehen. Daher muss man die Farbe der Tabseiten selbst ändern. Zeile 4 verwendet eine weitere for-Schleife, um von der ersten Tabseite bis zur letzten (wie durch TabPages.Count angegeben) zu zählen. In jeder TabPage im TabControl-Steuerelement setzen Sie die BackColor-Farbe auf die vom Benutzer angegebene. Verwenden Sie die FromName-Methode des Color-Objekts, um den eingegebenen Text in einen Color-Wert umzuwandeln.

204

Das Steuerelement TabControl

Die Zeilen 9 bis 11 enthalten die ChangeLocation-Methode, die das Formular auf der Benutzeroberfläche bewegt, wobei Werte aus der 3. Tabseite berücksichtigt werden. Diese Methode ist sehr einfach: Sie benutzen lediglich die Eigenschaft DesktopLocation des Form-Objekts, um das Formular auf den benutzerdefinierten Koordinaten zu positionieren. Denken Sie daran, den Text in den Textfeldern jeweils mit Hilfe der Convert.ToInt32-Methode zu einem Integer umzuwandeln, wie in Zeile 10 zu sehen. Die AddTab-Methode in Zeile 13 ist schließlich die einfachste von allen. Sie erstellt eine neue Tabseite und fügt sie dem TabControl-Objekt hinzu. Der Text, den der Benutzer in tb4Tab angibt, wird als Titel der Tabseite verwendet. Kompilieren Sie die vollständige Anwendung und experimentieren Sie mit dem Funktionsumfang. Abbildung 6.10 zeigt das Ergebnis, nachdem eine neue Tabseite hinzugefügt wurde.

Abbildung 6.10: Mit dem TabControl-Steuerelement können Sie dynamisch weitere Tabseiten erstellen.

Es gibt noch ein paar TabControl-Eigenschaften, die Sie kennen sollten. Das TabControl hat die gleichen SelectedIndex- und SelectedIndexChanged-Mitglieder wie die Steuerelemente ComboBox und ListBox, so dass Sie damit bereits umgehen können. Zudem besitzt es eine SelectedTab-Eigenschaft, die das TabPage-Objekt zurückgibt, das gerade ausgewählt ist. Anstelle von Text für den Titel der Tabseite, wie Sie ihn in den Listings 6.6 bis 6.8 verwendet haben, kann TabControl auch Grafiken verwenden. Erstellen Sie einfach wie immer eine ImageList und weisen Sie sie der ImageList-Eigenschaft des TabControl-Steuerelements zu. Mit Hilfe der Alignment-Eigenschaft können Sie die Positionierung der Tabseiten im Formular ändern. Diese Eigenschaft übernimmt einen der Werte aus der TabAlignment-Aufzählung: Bottom, Left, Right oder Top. Eine kleine Änderung, die Sie hier vornehmen, kann Ihrer Anwendung ein völlig neues Aussehen verleihen.

205

Windows Forms mit Steuerelementen erweitern

Auch das TabControl verfügt über eine Appearance-Eigenschaft. Sie verwendet Werte aus der TabAppearance-Aufzählung und bestimmt, wie die einzelnen Tabseiten angezeigt werden sollen. Mögliche Werte sind Buttons, FlatButtons und Normal. Die ersten beiden Optionen vermitteln den visuellen Eindruck von Schaltflächen statt Registern, doch die Funktionalität ist die gleiche. Einige der Eigenschaften betreffen die Größe der Registerkarten. ItemSize holt oder setzt die Breite und Höhe der Registerkarten. In der Standardeinstellung passt sich die Registerkarte automatisch ihrem Inhalt an. Die Padding-Eigenschaft ändert den Abstand zwischen den Elementen auf der Tabseite und deren Rändern. Diese Eigenschaft übernimmt jedoch keinen Integerwert, sondern einen Point-Wert: tcMain.Padding = new Point(20,20);

Stellen Sie sich vor, Sie erzeugen ein neues Point-Objekt der Größe 20 x 20 Pixel, das auf jeder Seite des Tabseitentitels liegt. Dadurch erweitert sich die Registerkarte. Des Weiteren gibt es die SizeMode-Eigenschaft, die bestimmt, wie stark ein TabControl die Registerkarten expandieren sollte. Sie verwendet Werte aus der Aufzählung TabSizeMode: FillToRight bewirkt die Expansion bis zur Gesamtbreite des TabControl-Steuerelements, bei Fixed haben alle Registerkarten die gleiche, feste Größe und Normal legt fest, dass die Größe jede Tabseite durch die bereits genannten ItemSize- und Padding-Eigenschaften gesteuert wird. Erzeugen Sie mehr Tabseiten, als sich zugleich darstellen lassen, erscheinen auf dem TabControl Pfeile, mit deren Hilfe der Benutzer blättern kann, um alle Tabseiten zu sehen. Wenn Sie die Multiline-Eigenschaft auf true setzen, bewegen sich die Tabseiten, die normalerweise verborgen wären, in eine andere Zeile, so dass sie immer angezeigt werden – es werden so viele Zeilen erstellt wie zur Anzeige aller Tabseiten nötig sind. Um den Überblick über Ihre Steuerelemente behalten zu können, gibt es die Eigenschaften RowCount und TabCount, die, falls Multiline auf true gesetzt ist, die Anzahl der Zeilen bzw. die Gesamtzahl der Tabseiten zurückgeben. Die HotTrack-Eigenschaft schließlich veranlasst die Tabseiten, ihr Erscheinungsbild zu ändern (genauer gesagt: der Titelzeilentext ändert die Farbe), sobald die Maus darüber bewegt wird.

6.9

Die Steuerelemente TextBox und RichTextBox

Ihnen ist das TextBox-Steuerelement in diesem Buch bereits einige Male begegnet. Meistens haben Sie nur seine Text-Eigenschaft genutzt und meist brauchen Sie auch nicht mehr. Es besitzt aber noch ein paar Eigenschaften, die sich als nützlich erweisen könnten.

206

Die Steuerelemente TextBox und RichTextBox

Wir werfen einen kurzen Blick darauf, bevor ich Ihnen erzähle, was sie mit dem RichTextBox-Steuerelement zu tun haben. AcceptsReturn und AcceptsTab sind zwei Eigenschaften, die angeben, ob die (¢)- und die (ÿ_)-Tasten Zeilenumbrüche respektive Einzüge in der TextBox verursachen. Wenn der

Benutzer die Tabulatortaste drückt, geht gemäß der Voreinstellung der Fokus auf das nächste Steuerelement über und die (¢)-Taste aktiviert die Standardschaltfläche (falls es eine gibt). Indem Sie diese Eigenschaft auf true setzen, können Sie den Funktionsumfang Ihres Steuerelements erhöhen. Wenn Sie AcceptReturn gestatten, dürfen Sie damit rechnen, mehr als eine Zeile Daten zu erhalten. Tritt dies ein, dann sollten Sie Multiline auf true setzen, sonst könnte AcceptReturn nutzlos werden. Lines gibt ein Array von Zeichenfolgen zurück, das den Text aus jeder Zeile in der TextBox enthält. WordWrap gibt vor, ob der Text in die nächste Zeile weiterlaufen (also umgebrochen werden) soll, wenn die TextBox nicht groß genug ist, um ihn komplett auf einmal anzuzeigen. Ist jedoch WordWrap gleich false und Multiline gleich true, dann muss der Benutzer die (¢)-Taste drücken, um in die nächste Zeile zu gelangen. Sie können auch die Art und Weise, wie der Benutzer mit der TextBox interagiert, modifizieren. Die Eigenschaft CharacterCasing übernimmt einen Wert der CharacterCasing-Aufzählung, der angibt, wie die Großschreibung gehandhabt werden soll, wenn der Benutzer Text eingibt. Die Eigenschaft kann einen von drei Werten annehmen: Lower, welcher alle Eingaben in Kleinschreibung umwandelt; Normal, welcher Text so lässt, wie er eingegeben wird; und Upper, der jede Eingabe in Großschreibung umwandelt. Mit Hilfe von CanUndo kann der Benutzer seine letzten Aktionen rückgängig machen (für viele Benutzer ein Glücksfall!). Die Undo-Methode veranlasst die Rückgängigmachung, und ClearUndo löscht die Rückgängig-Liste, so dass der Benutzer keine Rückgängig-Aktionen mehr vornehmen kann, da die Anwendung sich nun nicht mehr »erinnern« kann, was passiert ist. Modified gibt an, ob der Text in der TextBox geändert wurde. Diese Eigenschaft wird häufig dazu verwendet, um festzustellen, ob Inhalte gespeichert werden sollten, bevor die Anwendung geschlossen wird. ReadOnly (schreibgeschützt) bedeutet, dass sich der Text nicht bearbeiten lässt. PasswordChar gibt das Zeichen an, welches zur Maskierung einer Benutzereingabe verwendet werden soll. Beim Eingeben von Kennwörtern haben Sie sicher schon oft bemerkt, dass Ihre Eingabe durch Sternchen (*) ersetzt wird. Die TextBox verfügt über eine Reihe von Methoden, die den Text behandeln. Clear löscht alle Textinhalte in der TextBox. Copy, Cut und Paste folgen alle den vertrauten WindowsOperationen Kopieren, Ausschneiden und Einfügen. AppendText hängt eine angegebene Zeichenfolge an das Ende des TextBox-Inhalts an.

207

Windows Forms mit Steuerelementen erweitern

Rich Text Das TextBox-Steuerelement eignet sich hervorragend, um Benutzereingaben zu handhaben. Sein Hauptnachteil: Es kann nur mit reinem Text oder Text ohne Formatierung umgehen. In ihrer einfachsten Form verhält sich die RichTextBox, die auch formatierten Text verarbeiten kann, wie jedes andere Steuerelement auch. Mit seinen Brüdern teilt es sich viele Mitglieder, darunter die Eigenschaften AcceptsTab, CanUndo und Multiline aus der TextBox. Doch RichTextBox bietet weit mehr Funktionen, darunter das Speichern und Laden von Dateien. Wir wollen einen Blick auf ein einfaches Beispiel für ein RichTextBox-Steuerelement werfen. Es erlaubt die Formatierung von Text. Wieder werde ich den Code aufteilen, um die Abschnitte leichter analysieren zu können. Das Listing 6.9 enthält den ersten Teil dieser Anwendung. Listing 6.9: Der Einsatz des Steuerelements RichTextBox in VB .NET 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:

208

Imports System Imports System.Windows.Forms Imports System.Drawing Namespace TYWinForms.Day6 Public Class Listing69 : Inherits Form private rtbText as New RichTextBox private intFontSize as Integer private strFontColor as String private strFontStyle as String private private private private private private private private private private private private private private private

cmnuText as New ContextMenu cmniFont as New MenuItem cmniBold as New MenuItem cmniItalics as New MenuItem cmniNormal as New MenuItem cmniFontSize as New MenuItem cmniSize8 as New MenuItem cmniSize10 as New MenuItem cmniSize12 as New MenuItem cmniSize14 as New MenuItem cmniColor as New MenuItem cmniColorRed as New MenuItem cmniColorBlue as New MenuItem cmniColorGreen as New MenuItem cmniColorBlack as New MenuItem

public shared sub Main()

Die Steuerelemente TextBox und RichTextBox

30: 31: 32: 33:

Application.Run(new Listing69) end sub End Class End Namespace

Listing 6.9 zeigt den grundlegenden Rahmen für unsere Anwendung. Die Zeilen 8 bis 27 weisen eine Reihe von Variablen auf, von denen viele Steuerelemente vom Typ ContextMenu und MenuItem sind, die der Benutzer verwenden wird, um die Schriftart im RichTextBox-Steuerelement zu ändern. Die drei Variablen in den Zeilen 9 bis 11 sind globale Variablen, die registrieren, welche Größe, Farbe und Auszeichnung die aktuelle Schriftart hat; diese Werte werden sich später als recht nützlich erweisen. Listing 6.10 zeigt den Konstruktor dieser Beispielanwendung. Listing 6.10: Der Konstruktor für das RichTextBox-Steuerelement der Anwendung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:

public sub New() intFontSize = 10 strFontColor = "Schwarz" strFontStyle = "Normal" rtbText.Dock = DockStyle.Fill rtbText.ScrollBars = RichTextBoxScrollBars.Both rtbText.ContextMenu = cmnuText cmnuText.MenuItems.Add(cmniFont) cmnuText.MenuItems.Add(cmniFontSize) cmnuText.MenuItems.Add(cmniColor) cmniFont.Text = "Font" cmniBold.Text = "Fett" cmniFont.MenuItems.Add(cmniBold) cmniItalics.Text = "Kursiv" cmniFont.MenuItems.Add(cmniItalics) cmniNormal.Text = "Normal" cmniNormal.Checked = true cmniFont.MenuItems.Add(cmniNormal) AddHandler cmniBold.Click, new EventHandler(AddressOf Me.HandleFont) AddHandler cmniItalics.Click, new EventHandler(AddressOf Me.HandleFont) AddHandler cmniNormal.Click, new EventHandler(AddressOf Me.HandleFont) cmniFontSize.Text = "Grösse" cmniSize8.Text = "8" cmniFontSize.MenuItems.Add(cmniSize8)

209

Windows Forms mit Steuerelementen erweitern

29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59:

cmniSize10.Text = "10" cmniSize10.Checked = true cmniFontSize.MenuItems.Add(cmniSize10) cmniSize12.Text = "12" cmniFontSize.MenuItems.Add(cmniSize12) cmniSize14.Text = "14" cmniFontSize.MenuItems.Add(cmniSize14) AddHandler cmniSize8.Click, new EventHandler(AddressOf Me.HandleFontSize) AddHandler cmniSize10.Click, new EventHandler(AddressOf Me.HandleFontSize) AddHandler cmniSize12.Click, new EventHandler(AddressOf Me.HandleFontSize) AddHandler cmniSize14.Click, new EventHandler(AddressOf Me.HandleFontSize) cmniColor.Text = "Farbe" cmniColorRed.Text = "Rot" cmniColor.MenuItems.Add(cmniColorRed) cmniColorBlue.Text = "Blau" cmniColor.MenuItems.Add(cmniColorBlue) cmniColorGreen.Text = "Grün" cmniColor.MenuItems.Add(cmniColorGreen) cmniColorBlack.Text = "Schwarz" cmniColorBlack.Checked = true cmniColor.MenuItems.Add(cmniColorBlack) AddHandler cmniColorRed.Click, new EventHandler(AddressOf Me.HandleFontColor) AddHandler cmniColorBlue.Click, new EventHandler(AddressOf Me.HandleFontColor) AddHandler cmniColorGreen.Click, new EventHandler(AddressOf Me.HandleFontColor) AddHandler cmniColorBlack.Click, new EventHandler(AddressOf Me.HandleFontColor) Me.Text = "Listing 6.9" Me.Font = New Font("Times New Roman", intFontSize) Me.Controls.Add(rtbText) end sub

Dies ist ein Standard-Konstruktor, doch er hat eine Menge Code zu verarbeiten. Die Zeilen 2 bis 4 setzen Anfangswerte für die Schriftstile Ihrer Anwendung fest. Die Zeilen 6 bis 8 definieren Eigenschaften für das RichTextBox-Steuerelement. Dock gibt an, wie sich das Steuerelement im Verhältnis zum Formular bewegt. ScrollBars diktiert, ob und wie Bildlaufleisten für die RichTextBox angezeigt werden sollen. Diese Eigenschaft kann einen der Werte aus der RichTextBoxScrollBar-Aufzählung annehmen:

210

Die Steuerelemente TextBox und RichTextBox



Both: Falls nötig, werden waagrechte und senkrechte Bildlaufleisten

erscheinen. 왘

ForcedBoth: Egal, ob benötigt oder nicht, es werden immer waagrechte und senkrechte Bildlaufleisten erscheinen.



ForcedHorizontal: Es wird stets eine waagrechte Bildlaufleiste angezeigt.



ForcedVertical: Es wird stets eine senkrechte Bildlaufleiste angezeigt.



Horizontal: Bei Bedarf wird eine waagrechte Bildlaufleiste angezeigt.



None: Es werden keine Bildlaufleisten angezeigt.



Vertical: Bei Bedarf wird eine senkrechte Bildlaufleiste angezeigt.

Zeile 8 weist diesem Steuerelement ein ContextMenu zu, so dass der Benutzer darauf rechtsklicken und die Schrifteigenschaften ändern kann. Die Zeilen 10 bis 12 fügen dem ContextMenu-Steuerelement Menüelemente der höchsten Hierarchieebene hinzu. Fast der ganze Rest des Listings beschäftigt sich mit den Eigenschaften für diese MenuItems. Die Zeilen 14 bis 24 initialisieren die Menüelemente, die den Schriftstil steuern (fett, kursiv oder normal). Die Zeilen 26 bis 39 initialisieren diejenigen Elemente, die sich mit der Schriftgröße befassen, und die Zeilen 41 bis 54 betreffen die Elemente, die die Schriftfarbe steuern. Beachten Sie, dass jede Codegruppe nur einen Ereignishandler verwendet: Die Zeilen 14 bis 24 benutzen HandleFont, die Zeilen 26 bis 39 HandleFontSize und die Zeilen 41 bis 54 HandleFontColor. Die Zeilen 56 bis 59 initialisieren schließlich das Formular, wobei sie die zu benutzende Standardschrift festlegen. Listing 6.11 zeigt die Ereignishandler und diverse Funktionen der Anwendung. Listing 6.11: Die Behandlung der RichTextBox-Ereignisse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

public sub HandleFont(Sender as Object, e as EventArgs) strFontStyle = CType(Sender, MenuItem).Text rtbText.SelectionFont = new Font("Times New Roman", intFontSize, ConvertToFontStyle(strFontStyle)) cmniBold.Checked = false cmniItalics.Checked = false cmniNormal.Checked = false CType(Sender, MenuItem).Checked = true end sub

211

Windows Forms mit Steuerelementen erweitern

12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45:

public sub HandleFontSize(Sender as Object, e as EventArgs) intFontSize = Convert.ToInt32(CType(Sender, MenuItem).Text) rtbText.SelectionFont = new Font("Times New Roman", intFontSize, ConvertToFontStyle(strFontStyle)) cmniSize8.Checked = false cmniSize10.Checked = false cmniSize12.Checked = false cmniSize14.Checked = false CType(Sender, MenuItem).Checked = true end sub public sub HandleFontColor(Sender as Object, e as EventArgs) strFontColor = CType(Sender, MenuItem).Text rtbText.SelectionColor = Color.FromName(strFontColor) cmniColorRed.Checked = false cmniColorBlue.Checked = false cmniColorGreen.Checked = false cmniColorBlack.Checked = false CType(Sender, MenuItem).Checked = true end sub private function ConvertToFontStyle(strStyle) as FontStyle select strStyle case "Fett" return FontStyle.Bold case "Kursiv" return FontStyle.Italic case "Normal" return FontStyle.Regular end select end function

Beachten Sie bitte zunächst, dass keine dieser Methoden eines der Ereignisse in RichTextBox direkt verarbeitet. Vielmehr behandeln wir die Ereignisse der MenuItem-Steuerelemente, die die Eigenschaften der RichTextBox entsprechend anpassen. Lassen Sie uns als Nächstes hinunter zur Funktion ConvertToFontStyle springen, die in Zeile 36 beginnt. Wie Sie aus ihrer Deklaration schließen können, gibt diese Funktion einen Wert der FontStyle-Aufzählung zurück. Wenn Sie nacheinander jedes Ereignis behandeln, werden Sie bemerken, dass es nötig wird, das vom Benutzer ausgewählte Menu-

212

Die Steuerelemente TextBox und RichTextBox

Item in einen passenden Typ umzuwandeln, den die RichTextBox verwenden kann. So kön-

nen Sie etwa mit folgendem Befehl keine Schrift erzeugen: new Font("Times New Roman", 8, "Fett")

Statt dessen müssen Sie den Wert der FontStyle-Aufzählung verwenden: new Font("Times New Roman", 8, FontStyle.Fett)

Die Funktion ConvertToFontStyle wandelt den Text »Fett« in einen FontStyle.Bold-Wert um, den Sie an anderer Stelle in Ihrem Code benutzen können. Wie genau, werden Sie gleich sehen. Wenn Sie zu Zeile 1 und der HandleFont-Methode zurückgehen, denken Sie bitte daran, dass diese Methode das Click-Ereignis für die Menüelemente cmniBild, cmniItalics und cmniNormal behandelt, die die Schriftart jeweils fett oder kursiv auszeichnen bzw. auf normal zurücksetzen. In Zeile 2 holen Sie den Text-Wert vom gerade ausgewählten MenuItem in die strFontStyle-Variable (Sie müssen die Sender-Variable in einen passenden Typ umwandeln – in diesem Fall MenuItem); dieser Wert wird »Fett« sein, »Kursiv« oder »Normal«, wie in den Zeilen 15, 17 und 19 des Listings 6.10 gezeigt wird. In Zeile 3 verwenden Sie die Eigenschaft SelectionFont der RichTextBox. Sie holt und setzt Informationen über die gerade in der RichTextBox verwendete Schriftart, insbesondere den gerade ausgewählten (oder markierten) Text. Ist momentan kein Text ausgewählt, betrifft diese Eigenschaft jeden neuen Text, den der Benutzer eingibt. Zeile 3 weist SelectionFont ein neues Font-Objekt zu, wobei die in den Variablen intFontSize und strFontStyle gesetzten Werte verwendet werden. Nun verstehen Sie, warum diese globalen Variablen wichtig sind – ohne sie würde es viel mehr Mühe bereiten, neue Schriftstile festzulegen. Ohne globale Variablen würde es sicherlich mehr Arbeit bedeuten, doch sie wäre nicht schwieriger. Wenn Sie diese globalen Variablen nicht verwenden wollen, dann ändern Sie die Zeile 3 wie folgt: rtbText.SelectionFont = new Font("Times New Roman"), rtbText.SelectionFont.Size, ConvertToFontStyle(Ctype(Sender, MenuItem).Text))

Mit anderen Worten: Die globalen Variablen spielen Platzhalter für Werte der SelectionFont-Eigenschaft und der Sender-Variablen. Lassen Sie uns kurz zur ConvertToFontStyle-Funktion zurückkehren. Bekanntlich kann man die Zeichenfolge, die vom ausgewählten Menüelement abgerufen wird, nicht direkt verwenden. Daher gibt diese Methode den richtigen Wert der FontStyle-Aufzählung zurück, um die Schrift richtig festzulegen. An Tag 4 behandelten wir Menüelemente. Wenn man zulässt, dass Menüelemente mit Häkchen versehen werden, dann muss man dafür sorgen, dass alle momentan nicht rele-

213

Windows Forms mit Steuerelementen erweitern

vanten Menüelemente kein Häkchen aufweisen. Die Zeilen 5 bis 9 entfernen alle Häkchen von den Schriftstil-Menüelementen; dann wird das passende Menüelement mit Häkchen versehen, je nachdem, was die Sender-Variable angibt. Die Methoden HandleFontSize und HandleFontColor tun praktisch das Gleiche wie HandleFont. Beide holen den Text des ausgewählten Menüelements (die Zeilen 13 respektive 25). HandleFontSize weist der SelectionFont-Eigenschaft unter Zuhilfenahme der globalen Variablen eine neue Schrift zu und versieht die Menüelemente dann mit Häkchen oder entfernt diese, je nachdem. HandleFontColor verwendet eine etwas andere Eigenschaft, nämlich SelectionColor. Diese Eigenschaft holt oder setzt die für den gerade ausgewählten Text verwendete Farbe (ist gerade keiner ausgewählt, dann für jeden neuen Text). In Zeile 26 verwenden Sie die FromName-Methode des Color-Objekts, um die Farbe zuzuweisen. In den Zeilen 28 bis 33 werden die Häkchen gesetzt bzw. entfernt. Diese Anwendung enthält eine Menge Code; tatsächlich ist sie das umfangreichste Codebeispiel, das Sie bis jetzt gesehen haben. Doch das meiste davon ist recht einfach, denn es besteht vor allem aus dem Initialisierungscode des Konstruktors. Kombinieren Sie diese drei Listings, indem Sie Listing 6.10 und 6.11 in Zeile 28 des Listings 6.9 einfügen. Abbildung 6.11 zeigt ein typisches Ergebnis aus dieser Anwendung.

Abbildung 6.11: Die voll funktionsfähige RichTextBox-Anwendung bietet dem Benutzer zahlreiche Wahlmöglichkeiten an.

Die Hauptarbeit bei der Verwendung des RichTextBox-Steuerelements besteht also vor allem in der Bereitstellung von Benutzerschnittstellen (wie Menüelementen), um Schrifteigenschaften bearbeiten zu können. Die zwei Haupteigenschaften SelectionFont und SelectionColor betreffen den gerade ausgewählten Text. Die RichTextBox besitzt viele solcher Eigenschaften, darunter SelectionAlignment, die anzeigt, wie der momentan markierte Text auszurichten ist, SelectionBullet, die festlegt, ob der Text eine Liste mit Aufzählpunkten sein soll, und schließlich SelectionIndent, die festlegt, wie groß Einrückungen sein sollen. (Um etwas über die anderen Eigenschaften zu erfahren, schlagen Sie bitte in Anhang B nach.)

214

Das Steuerelement Timer

Bevor wir zum nächsten Steuerelement übergehen, sind drei RichTextBox-Mitglieder vorzustellen, die jeder Entwickler kennen sollte. Das erste, DetectUrls, ist interessant, da seine Funktion zunehmende Bedeutung in heutigen Anwendungen erlangt. Wird diese Eigenschaft auf true gesetzt, formatiert die RichTextBox jeden Text, der wie eine Webadresse (URL) aussieht, auf solche Weise, dass er anklickbar ist – genau wie eine normale Verknüpfung in einem Webbrowser. Der fragliche Text ist blau und unterstrichen (je nach Ihren Internet-Einstellungen). Um Klicks auf solche Links handhaben zu können, müssen Sie dem LinkClicked-Ereignis der RichTextBox einen Delegaten zuweisen und einen Ereignishandler hinzufügen, so etwa: public Sub Link_Clicked(Sender As Object, e As LinkClickedEventArgs) System.Diagnostics.Process.Start(e.LinkText) end Sub

Die Start-Methode in diesem Codestück ruft den Standard-Browser des Benutzers mit dem Link, den er angeklickt hat, auf (eigentlich ruft sie die Anwendung auf, die dafür konfiguriert ist, Internet-Links zu verarbeiten, was aber meist ein Browser ist). Das zweite kennen zu lernende RichTextBox-Mitglied ist die Find-Methode. Sie durchsucht die RichTextBox nach einer angegebenen Zeichenfolge – eine sehr nützliche Funktion für jede textverarbeitende Anwendung. Diese Methode kann etliche verschiedene Parameter übernehmen, doch die gebräuchlichsten sind eine Zeichenfolge sowie eine Zeichenfolge mit einem Index für das erste Zeichen, bei dem die Suche beginnen soll. Beim letzten Mitglied handelt es sich eigentlich um zwei, die eine ähnliche Funktion haben. Rtf und SelectedRtf liefern den gesamten bzw. nur den markierten Text in der RichTextBox, wobei sie alle Formatierungen übernehmen. Die Copy-Methode, die auch in der TextBox verfügbar ist, kopiert Text, doch verlieren Sie dabei alle Formatierungen. Verwenden Sie also Rtf oder SelectedRtf, um die Formatierung beizubehalten, wenn Sie den Text an eine andere Anwendung (wie zum Beispiel Microsoft Word) übergeben wollen.

6.10 Das Steuerelement Timer Das Timer-Steuerelement ist eines der nützlichsten Steuerelemente überhaupt. Einfach ausgedrückt, können Sie damit ein Ereignis nach einem festgelegten Intervall auslösen. Bis jetzt hatten Sie keine Möglichkeit, in einer Anwendung Zeit oder Dauer festzuhalten. Denken Sie einmal an all die Einsatzmöglichkeiten von Timer: Sie können eine Anwendung erstellen, die den Benutzer alle fünf Minuten daran erinnert, sein Dokument zu speichern; Sie können Echtzeituhren und -zähler bereitstellen und eine Leistungsüberwachung einrichten. Praktisch jede interaktive Aufgabe, die Sie automatisieren wollen, erfordert das TimerSteuerelement.

215

Windows Forms mit Steuerelementen erweitern

Da er kaum etwas anderes tut, als die Zeit zu stoppen, verfügt der Timer über sehr wenige Mitglieder. Er hat nur zwei Eigenschaften: Enabled zeigt an, ob der Timer aktiv sein soll, und Interval legt die Dauer in Millisekunden fest, nach der der Timer »ticken« soll. Jeder »Tick« im Timer erzeugt ein Ereignis, das treffend einfach Tick genannt wird. Damit können Sie angepasste automatische Routinen ausführen lassen. Schließlich gibt es noch zwei interessante Methoden, nämlich Start und Stop, die einfach den Timer starten bzw. anhalten. Eine der besten Anwendungsmöglichkeiten von Timer besteht in einer Weckuhr. Listing 6.12 zeigt eine solche Anwendung. Listing 6.12: Eine Weckuhr 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: namespace TYWinForms.Day6 6: 7: public class Listing612 : Inherits Form 8: private tmrClock as New Timer 9: private tmrAlarm as New Timer 10: private lblClock as New Label 11: private intSnoozeTime as Integer 12: private intSnoozeCounter as Integer 13: 14: public sub New() 15: intSnoozeTime = 10000 '10 Sekunden 16: 17: tmrClock.Interval = 1000 18: tmrClock.Enabled = true 19: AddHandler tmrClock.Tick, new EventHandler(AddressOf  Me.UpdateClock) 20: 21: tmrAlarm.Interval = 1000 22: tmrAlarm.Enabled = true 23: AddHandler tmrAlarm.Tick, new EventHandler(AddressOf Me.CheckAlarm) 24: 25: lblClock.Width = 300 26: lblClock.Height = 150 27: lblClock.Location = new Point(0,100) 28: lblClock.Text = DateTime.Now.ToString 29: 30: Me.Text = "Listing 6.12" 31: Me.Font = new Font("Arial", 20) 32: Me.Controls.Add(lblClock)

216

Das Steuerelement Timer

33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52:

end sub public sub UpdateClock(Sender as Object, e as EventArgs) lblClock.Text = DateTime.Now.ToString end sub public sub CheckAlarm(Sender as Object, e as EventArgs) intSnoozeCounter += tmrAlarm.Interval if intSnoozeCounter = intSnoozeTime then intSnoozeCounter = 0 MessageBox.Show("Aufwachen, Schlafmütze!!!") end if end sub public shared sub Main() Application.Run(new Listing612) end sub end class end namespace

In dieser Anwendung erzeugen Sie zwei Timer-Steuerelemente: eines, um die dem Benutzer sichtbare Uhr zu aktualisieren, und eines, um den »Weckruf« zu steuern. In den Zeilen 8 bis 12 werden diese beiden Timer neben ein paar weiteren Variablen deklariert. In Zeile 15 stellen Sie im Konstruktor die Schlummerzeit ein oder die Zeit, bevor der Alarm losgeht – in unserem Fall nach 10 Sekunden (= 10.000 Millisekunden). In den Zeilen 17 bis 19 stellen Sie das Intervall des Timers auf eine Sekunde (oder 1000 Millisekunden) ein, aktivieren bzw. starten den Timer und übergeben ihm einen Ereignishandler. Die Zeilen 21 bis 23 erledigen das Gleiche für den Weckruf-Timer. Ein paar Eigenschaften für ein Label-Steuerelement, das die Uhr anzeigt, werden in den Zeilen 25 bis 28 festgelegt, und die Zeilen 30 bis 32 bestimmen ein paar allgemeine Eigenschaften für das Formular. Beachten Sie, dass Sie die DateTime.Now-Methode verwenden, um die aktuelle Zeit zurückzugeben, wie sie von der Uhr des Benutzer-Computers definiert wird. Die ToString-Methode wandelt das DateTime-Objekt in Text um, der im Label-Steuerelement angezeigt wird. Die UpdateClock-Methode in Zeile 35 wird alle 1000 Millisekunden durch den Timer tmrClock ausgeführt. Sie ruft einfach fortlaufend DateTime.Now auf, um die sichtbare Uhr zu aktualisieren. Auch CheckAlarm wird alle 1000 Millisekunden ausgeführt und bestimmt, ob das vorgegebene Schlummerintervall bereits verstrichen ist. In Zeile 40 erhöht

217

Windows Forms mit Steuerelementen erweitern

diese Methode die intSnoozeCounter-Variable, die in Zeile 42 mit der intSnoozeTime-Variablen verglichen wird. Sind beide gleich – sind also 10 Sekunden verstrichen – wird eine Meldung mit einer freundlichen Weckrufbotschaft angezeigt. Die Zählervariable wird dann auf 0 zurückgesetzt, um den Weckrufvorgang von neuem zu starten. Ansonsten passiert nichts, und der Timer tickt unverdrossen weiter. Abbildung 6.12 zeigt das Ergebnis nach zehn Sekunden.

Abbildung 6.12: Ihre einfache Weckuhr besitzt einen stillen Alarmruf.

6.11 Das Steuerelement TreeView Das Steuerelement TreeView wird für die Anzeige hierarchischer Listen von Informationen verwendet. Die gebräuchlichste Einsatzweise besteht in Oberflächen vom Typ des Windows Explorers: Sie können damit durch eine Verzeichnisstruktur blättern, wie in Abbildung 6.13 zu sehen. Eine Verzeichnisstruktur ist jedoch nicht das Einzige, das das TreeView-Steuerelement darstellen kann. Jede beliebige hierarchisch aufgebaute Informationsstruktur – Kunden- und Bestelldaten, Stammbäume, Speisekarten usw. – lässt sich mit TreeView handhaben. Jedes Element – oder Strukturknoten (node) – in der TreeView-Liste wird durch ein TreeNode-Objekt dargestellt. Natürlich kann jeder TreeNode unter sich weitere TreeNodes haben, so dass das Steuerelement seine hierarchische Charakteristik erhält. Der erste Knoten wird als Wurzel- oder Stammknoten (root node) bezeichnet. Da Sie jeden Knoten in der TreeView einzeln anlegen müssen, könnte das Anlegen dieses Steuerelements etwas mühselig sein, doch es ist zum Glück nicht schwierig. Das folgende Codestück erstellt z.B. neue Knoten einfach durch Einsatz von Programmschleifen:

218

Das Steuerelement TreeView

Abbildung 6.13: Die Benutzeroberfläche des Windows Explorers verwendet eine Strukturansicht. dim myTreeView as new TreeView For i = 0 to 20 MyTreeView.Nodes.Add(New TreeNode("Node " &&i.ToString)) For j = 0 to 20 MyTreeView.Nodes(i).Nodes.Add(New TreeNode("SubNode " &&j.ToString)) For k = 0 to 20 MyTreeView.Nodes(i).Nodes(j).Nodes.Add(New TreeNode("SubSubNode " &&k.ToString)) Next k Next j Next i

In diesem Codestück werden Schleifen durch drei verschiedene Variablen zwischen 0 und 20 geführt. Das Ergebnis ist ein dreischichtiges TreeView-Steuerelement, wobei jede Schicht über 21 Knoten verfügt. Neue Knoten lassen sich einfach mit der Nodes.AddMethode hinzufügen, wobei ein neues TreeNode-Objekt mit einer festgelegten Beschriftung übergeben wird. Auf diese Beschriftung kann man auch über die Text-Eigenschaft jedes Knotens zugreifen. Das TreeView-Steuerelement lässt sich weitgehend anpassen; jeder Aspekt der Benutzeroberfläche lässt sich steuern. Tabelle 6.3 zeigt die gebräuchlichsten Eigenschaften von TreeView.

219

Windows Forms mit Steuerelementen erweitern

Eigenschaft

Beschreibung

CheckBoxes

Gibt an, ob neben jedem Knoten Kontrollkästchen angezeigt werden sollen. Dadurch kann der Benutzer bestimmte Knoten zur Bearbeitung auswählen. Sie verwenden die Checked-Eigenschaft des TreeNode-Objekts, um zu bestimmen, ob ein Objekt mit Häkchen versehen (checked) ist.

FullRowSelect

Gibt an, ob die Markierung, die bei der Auswahl eines Knotens erscheint, die gesamte Breite des Steuerelements einnehmen oder sich auf die Beschriftung des Knotens beschränken soll.

HotTracking

Gibt an, ob sich das Aussehen des Knotens ändert, wenn sich der Mauszeiger darüber befindet (z.B. der Text sich blau färbt)

ImageList

Ein ImageList-Steuerelement, das sich dazu einsetzen lässt, Grafiken anstelle der Knotensymbole anzuzeigen. Sie verwenden dies mit der ImageIndex-Eigenschaft genau wie bei anderen Steuerelementen.

Indent

Die Anzahl an Pixeln, um die jeder untergeordnete Knoten gegenüber seinem übergeordneten Knoten eingerückt werden soll.

LabelEdit

Gibt an, ob sich die Beschriftung eines Knotens bearbeiten lässt.

Nodes

Gibt eine Auflistung von Strukturknoten (TreeNode) zurück.

PathSeparator

Das für die Abtrennung von Knoten verwendete Zeichen (mehr dazu gleich).

ShowLines

Gibt an, ob in der Strukturansicht Verbindungslinien zwischen den Knoten gezeichnet werden.

ShowPlusMinus

Gibt an, ob neben Knoten mit untergeordneten Knoten Plus- und Minuszeichen angezeigt werden sollen.

ShowRootLines

Gibt an, ob zwischen Stammknoten der Strukturansicht Verbindungslinien gezeichnet werden sollen.

Scrollable

Gibt an, ob nach Bedarf Bildlaufleisten angezeigt werden sollen.

VisibleCount

Die Anzahl der gleichzeitig sichtbaren Knoten.

TopNode

Gibt das oberste sichtbare TreeNode-Objekt zurück.

Tabelle 6.3: Die gebräuchlichsten Eigenschaften des TreeView-Steuerelements

Das TreeNode-Objekt weist ebenfalls einige nützliche Eigenschaften auf. FullPath kombiniert die Bezeichnungstexte aller Knoten, die anzusteuern sind, um den aktuellen Knoten zu erreichen. Die Angabe ist durch das PathSeparator-Zeichen (Standardwert: \) aufgeteilt. Beispielsweise sähe der FullPath, also die vollständige Pfadangabe, zum 14. Knoten,

220

Zusätzliche nützliche Steuerelemente

der zum Knoten gehört, der als 2. Knoten dem 3. Wurzelknoten untergeordnet ist, mit Hilfe des PathSeparator-Zeichens wie folgt aus: "Node2\SubNode1\SubSubNode13\"

Die Eigenschaften IsEditing, IsExpanded, IsSelected und IsVisible geben true- oder false-Werte zurück, um anzugeben, ob der aktuelle TreeNode gerade bearbeitet wird oder sichtbar, erweitert oder ausgewählt ist. Um Strukturknoten anzusteuern, stehen die Eigenschaften NextNode und NextVisibleNode sowie PrevNode und PrevVisibleNode zur Verfügung. Diese Eigenschaften geben die spezifizierten Strukturknoten zurück. Zwei nützliche TreeView-Methoden sind CollapseAll und ExpandAll, die entweder alle erweiterten Knoten reduzieren oder umgekehrt alle reduzierten Knoten erweitern. In analoger Weise verfügt TreeNode über Collapse, Expand, ExpandAll und Toggle. Letztere schaltet zwischen den Zuständen erweitert/reduziert hin und her. Das TreeView-Steuerelement weist eine Reihe von Ereignissen auf, die sowohl vor als auch nach bestimmten Aktionen ausgelöst werden. Diese Ereignisse verwenden die Präfixe Before (vor) und After (nach), so etwa in BeforeCheck, BeforeCollapse, BeforeExpand, BeforeLabelEdit und BeforeSelect (zusammen mit den entsprechenden After-Methoden).

6.12 Zusätzliche nützliche Steuerelemente In der heutigen Lektion haben wir gerade mal die Oberfläche der Windows Forms-Steuerelemente angekratzt. Wir haben die gebräuchlichsten Steuerelemente erforscht, doch es gibt insgesamt 42 Steuerelemente. Hier eine kleine Kostprobe: 쐽

CheckListBox: Ähnelt der ListBox, platziert aber neben jedem Listenelement ein kleines Kästchen. Da es sich um eine Kombination aus CheckBox und ListBox handelt,

können Sie sich die Eigenschaften in diesem Steuerelement vorstellen. 쐽

DataGrid: Sehr nützlich beim Anzeigen von Informationen aus gespeicherten Datenquellen wie etwa Datenbanken. (Mehr dazu an Tag 9.)



ProgressBar: Ähnlich den »Leisten-« Steuerelementen, die Sie an Tag 4 kennen gelernt haben. Die Fortschrittsleiste wird von links nach rechts angefüllt, um anzuzeigen, wie weit die Vollendung einer Aufgabe vorangeschritten ist.



ToolTip: Stellt eine QuickInfo dar, die erscheint, wenn der Mauszeiger über einem

Steuerelement schwebt. Windows ist voll von solchen QuickInfos, so dass dieses Steuerelement sich als recht hilfreich erweist.

221

Windows Forms mit Steuerelementen erweitern

6.13 Das Layout steuern Es ist an der Zeit zu lernen, wie man all diese Steuerelemente ordentlich auf einem Formular anordnet, denn sonst würde man ein unverständliches Durcheinander erhalten. Sie haben bereits eine Organisationsmethode kennen gelernt: das Hinzufügen von Steuerelementen zu anderen, übergeordneten Steuerelementen, wie etwa Panel- oder TabControlSteuerelementen. Mit diesen beiden lassen sich Steuerelemente zu logischen Gruppen zusammenfassen, die sowohl verwaltbar (als auch leicht darstellbar) sind. Es gibt noch andere Möglichkeiten zur Organisation von Steuerelementen. Eine davon besteht in der Dock-Eigenschaft der Control-Klasse. Mit Dock lassen sich die Position sowie die Art und Weise angeben, in der ein fragliches Steuerelement an sein Containersteuerelement angedockt ist. Indem man Werte aus der DockStyle-Aufzählung verwendet, kann man Steuerelemente dazu bringen, am oberen oder unteren Rand eines Formulars oder eines Panel-Steuerelements (wie etwa Symbolleisten oder Bildlaufleisten) zu kleben oder den Container vollständig auszufüllen. Folgende sind die DockStyle-Werte: 쐽

Bottom: Dockt das Steuerelement am unteren Rand des Containersteuerelements an.



Fill: Alle Ränder des Steuerelements werden an die entsprechenden gegenüberliegenden Seiten des Containers angedockt, wodurch das Steuerelement den Container vollständig ausfüllt, selbst wenn dessen Größe sich ändert.



Left: Dockt das Steuerelement am linken Rand des Containersteuerelements an.



None: Standardwert; das Steuerelement wird nirgendwo angedockt.



Right: Dockt das Steuerelement am rechten Rand des Containersteuerelements an.



Top: Dockt das Steuerelement am oberen Rand des Containersteuerelements an.

Eine weitere das Layout beeinflussende Eigenschaft ist Anchor. Sie ist ebenfalls in jedem Steuerelement vorhanden und ähnelt Dock insofern, als sie festlegt, welche Ränder eines Steuerelements mit dem übergeordneten Formular verknüpft sein sollen, doch im Unterschied zu Dock werden die Steuerelemente dadurch nicht an den Formularrändern angedockt. Vielmehr bleibt die angegebene Seite des Steuerelements an ihrer Position relativ zum Rand des Formulars, unabhängig von seiner Position auf dem Formular oder von der Größenänderung des Formulars. Anchor nimmt einen der Werte der AnchorStyle-Aufzählung an: Bottom, Left, None, Right oder Top. Damit ein Steuerelement seine Größe relativ zu der des Formulars ändert, setzen Sie die Verankerung auf gegenüberliegende Seiten des Formulars. Dies erfolgt in VB .NET mit dem Or-Operator und in C# mit dem Zeichen | – etwa so: textBox1.Anchor = AnchorStyles.Top Or AnchorStyles.Bottom textBox1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom;

222

Das Layout steuern

Listing 6.13 zeigt ein gutes Beispiel für die Manipulation der Eigenschaften Dock und Anchor. Listing 6.13: Das dynamische Ändern eines Steuerelementlayouts (Visual Basic .NET) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:

Imports System Imports System.Windows.Forms Imports System.Drawing Namespace TYWinForms.Day6 Public Class Listing613 : Inherits Form Private WithEvents pnlButton As New Panel Private WithEvents pnlControls As New Panel Private WithEvents gboxAnchor As New GroupBox Private WithEvents gboxDock As New GroupBox Private WithEvents btnDemo As New Button Private WithEvents rbNone As New RadioButton Private WithEvents rbTop As New RadioButton Private WithEvents rbLeft As New RadioButton Private WithEvents rbBottom As New RadioButton Private WithEvents rbRight As New RadioButton Private WithEvents rbFill As New RadioButton Private WithEvents chkTop As New CheckBox Private WithEvents chkLeft As New CheckBox Private WithEvents chkBottom As New CheckBox Private WithEvents chkRight As New CheckBox Public Sub New() rbRight.Location = New Point(8,120) rbRight.Size = New Size(72,24) rbRight.Text = "Rechts" rbNone.Location = New Point(8,24) rbNone.Size = New Size(72,24) rbNone.Text = "Kein" rbNone.Checked = True rbBottom.Location = New Point(8,96) rbBottom.Size = New Size(72,24) rbBottom.Text = "Unten" rbTop.Location = New Point(8,48) rbTop.Size = New Size(72,24) rbTop.Text = "Oben"

223

Windows Forms mit Steuerelementen erweitern

42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87:

224

rbLeft.Location = New Point(8,72) rbLeft.Size = New Size(72,24) rbLeft.Text = "Links" rbFill.Location = New Point(8,144) rbFill.Size = New Size(72,24) rbFill.Text = "Füllen" gboxAnchor.Location = New Point(16,16) gboxAnchor.Size = New Size(88,128) gboxAnchor.Text = "Verankern" gboxDock.Location = New Point(16,152) gboxDock.Size = New Size(88,176) gboxDock.Text = "Andocken" btnDemo.Size = New Size(120,24) btnDemo.Anchor = AnchorStyles.None btnDemo.Location = New Point(64,72) btnDemo.Text = "Spiel mit mir!" chkBottom.Location = New Point(8,72) chkBottom.Size = New Size(72,24) chkBottom.Text = "Unten" chkLeft.Location = New Point(8,48) chkLeft.Size = New Size(72,24) chkLeft.Text = "Links" chkTop.Location = New Point(8,24) chkTop.Size = New Size(72,24) chkTop.Text = "Oben" chkRight.Location = New Point(8,96) chkRight.Size = New Size(72,24) chkRight.Text = "Rechts" pnlButton.BorderStyle = FormBorderStyle.Fixed3D pnlButton.Dock = DockStyle.Fill pnlButton.Size = New Size(448,400) pnlButton.Text = "ButtonPanel" pnlButton.Controls.Add(btnDemo) pnlControls.BorderStyle = FormBorderStyle.Fixed3D pnlControls.Dock = DockStyle.Right pnlControls.Location = New Point(328,0)

Das Layout steuern

88: pnlControls.Size = New Size(120,400) 89: pnlControls.Text = "ControlsPanel" 90: pnlControls.Controls.Add(gboxAnchor) 91: pnlControls.Controls.Add(gboxDock) 92: 93: gboxAnchor.Controls.Add(chkRight) 94: gboxAnchor.Controls.Add(chkBottom) 95: gboxAnchor.Controls.Add(chkLeft) 96: gboxAnchor.Controls.Add(chkTop) 97: gboxDock.Controls.Add(rbBottom) 98: gboxDock.Controls.Add(rbLeft) 99: gboxDock.Controls.Add(rbNone) 100: gboxDock.Controls.Add(rbRight) 101: gboxDock.Controls.Add(rbFill) 102: gboxDock.Controls.Add(rbTop) 103: 104: Me.Text = "Listing 6.13" 105: Me.Size = New Size(800,600) 106: Me.Controls.Add(pnlButton) 107: Me.Controls.Add(pnlControls) 108: End Sub 109: 110: Private Sub AnchorClicked(Sender As Object,e As EventArgs) Handles  chkBottom.Click,chkLeft.Click,chkRight.Click,chkTop.Click 111: If chkTop.Checked Then 112: btnDemo.Anchor = btnDemo.Anchor Or AnchorStyles.Top 113: End If 114: 115: If chkLeft.Checked Then 116: btnDemo.Anchor = btnDemo.Anchor Or AnchorStyles.Left 117: End If 118: 119: If chkBottom.Checked Then 120: btnDemo.Anchor = btnDemo.Anchor Or AnchorStyles.Bottom 121: End If 122: 123: If chkRight.Checked Then 124: btnDemo.Anchor = btnDemo.Anchor Or AnchorStyles.Right 125: End If 126: End Sub 127: 128: Private Sub DockClicked(Sender As Object,e As EventArgs) Handles  rbBottom.Click,rbFill.Click,rbLeft.  Click,rbRight.Click,rbTop.Click,rbNone.Click 129: dim rbSet as RadioButton = CType(sender,RadioButton) 130:

225

Windows Forms mit Steuerelementen erweitern

131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150:

If rbSet Is rbNone Then btnDemo.Dock = DockStyle.None ElseIf rbSet Is rbTop Then btnDemo.Dock = DockStyle.Top ElseIf rbSet Is rbLeft Then btnDemo.Dock = DockStyle.Left ElseIf rbSet Is rbBottom Then btnDemo.Dock = DockStyle.Bottom ElseIf rbSet Is rbRight Then btnDemo.Dock = DockStyle.Right Else btnDemo.Dock = DockStyle.Fill End If End Sub public shared sub Main() Application.Run(New Listing613) end sub End Class End Namespace

In den Zeilen 8 bis 22 richten Sie einfach einige Steuerelemente ein, mit denen der Benutzer die Anchor- und Dock-Eigenschaften ändern kann; Sie benötigen ein Optionsfeld oder ein Kontrollkästchen für jeden der Werte in den DockStyle- und AnchorStyle-Aufzählungen (verwenden Sie Optionsfelder für DockStyle, denn es lässt sich nur jeweils ein Wert zuweisen; Kontrollkästchen werden für AnchorStyle benutzt, denn man kann ja mehrere Verankerungen zugleich haben). Beachten Sie den Gebrauch des Schlüsselwortes WithEvents: Dies teilt der CLR mit, dass jedes einzelne der Steuerelemente Ereignisse auslösen wird (in C# ist das Einbeziehen solcher Schlüsselworte nicht nötig). In den Zeilen 25 bis 107, also dem Großteil des Codes, initialisieren und platzieren wir Steuerelemente im Formular. In diesem Listing findet sich ein neues Steuerelement namens GroupBox (Gruppenfeld). Es ähnelt dem Panel-Steuerelement, weil es andere Steuerelemente enthält, doch es zeichnet einen Kasten um seine untergeordneten Steuerelemente, so dass der Benutzer weiß, dass diese Steuerelemente zusammengehören. In den Zeilen 93 bis 102 werden die einzelnen Steuerelemente diesen Gruppenfeldern hinzugefügt, welche wiederum (in den Zeilen 90 und 91) Panel-Steuerelementen hinzugefügt werden. Die Panel-Steuerelemente finden in den Zeilen 106 und 107 endlich Aufnahme in das Formular. Es gilt nur noch zwei Methoden zu untersuchen: AnchorClicked in Zeile 110 und DockClicked in Zeile 128. Sie verwenden das Schlüsselwort Handles, um der CLR mitzuteilen, auf welche Ereignisse diese Methoden angewendet werden sollen (mehr zur Ereignisbehand-

226

Das Layout steuern

lung in Windows Forms finden Sie an Tag 5). In unserem Fall handhabt die AnchorClicked-Methode alle Click-Ereignisse der Kontrollkästchen, und DockClicked tut das Gleiche für die Optionsfelder. Wir wollen zuerst AnchorClicked betrachten. Da sich Anchor-Eigenschaften kumulativ auf ein Steuerelement anwenden lassen, müssen Sie jedes einzelne Anker-Kontrollkästchen untersuchen. Ist das Kontrollkästchen für AnchorStyles.Top angekreuzt, müssen Sie den AnchorStyles.Top-Wert anwenden, und so weiter für alle weiteren Werte. Dies lässt sich leicht bewerkstelligen, indem man mit Hilfe des Or-Operators die Werte in if-Statements der Anchor-Eigenschaft einer Schaltfläche hinzufügt. Als Folge dieser Vorgehensweise wird jeder AnchorStyles-Wert nach Bedarf in der Reihenfolge der if-Statements ausgewertet und kumulativ angewendet. Mit DockClicked verhält es sich ähnlich. Doch da sich DockStyle-Werte nur einzeln anwenden lassen, müssen Sie die Stile nicht zusammen hinzufügen. Mit Hilfe von ifStatements bestimmen Sie einfach, welches Optionsfeld angeklickt wurde, und wenden den passenden DockStyle-Wert an (beachten Sie den Gebrauch des Is-Schlüsselworts: Denken Sie beim Vergleichen von Objekten daran, diesen Operator anstelle des Gleichheitszeichens (=) zu verwenden). Probieren Sie die Steuerelemente der Anwendung, die in Abbildung 6.14 zu sehen ist, aus und beachten Sie, wie die Schaltfläche dadurch beeinflusst wird. Obwohl die Eigenschaften TabStop und TabIndex das Layout von Steuerelementen nicht direkt betreffen, so gestatten sie doch das Navigieren zwischen den einzelnen Steuerelementen, so dass sie an dieser geeigneten Stelle vorgestellt werden sollen. Als WindowsBenutzer wissen Sie, dass Sie die (ÿ_)-Tasten dazu benutzen können, von einem Steuerelement zum nächsten zu gelangen. Dadurch können Sie sich ohne Maus durch eine Anwendung manövrieren. Die TabIndex-Eigenschaft gibt an, in welcher Reihenfolge bestimmte Steuerelemente angesteuert werden, wenn die (ÿ_)-Taste gedrückt wird. Ein Steuerelement mit dem TabIndex von 0 ist das erste zu markierende Steuerelement, sobald eine (ÿ_)-Taste gedrückt wird; TabIndex gleich 1 ist das nächste und so weiter. Jedes Steuerelement im Formular lässt sich auf diese Weise ansteuern. Wenn Sie die TabIndex-Werte nicht ausdrücklich zuweisen, erledigt dies die CLR für Sie, allerdings gemäß der Reihenfolge, in der das jeweilige Steuerelement dem Formular hinzugefügt wurde (und falls Sie aus Versehen eine TabIndex-Zahl überspringen, wird das die CLR ignorieren und einfach zur nächsten verfügbaren Indexzahl springen). Dies ist ein wichtiges Merkmal der Benutzeroberfläche, das man berücksichtigen muss. Es mag aber Situationen geben, in denen Sie nicht wünschen, dass der Benutzer mit der (ÿ_)-Taste auf ein Steuerelement springt. In der Regel sollten z.B. Benutzer nicht in der Lage sein, ein Label-Steuerelement zu manipulieren, daher sollte es nicht auswählbar sein. In solchen Fällen setzen Sie die TabStop-Eigenschaft auf false.

227

Windows Forms mit Steuerelementen erweitern

Abbildung 6.14: Das Modifizieren der Anchor- und Dock-Eigenschaften einer Schaltfläche.

6.14 Zusammenfassung Am besten verwenden Sie dieses umfangreiche Kapitel als eine Art Nachschlagewerk und Referenz – Sie müssen das nicht alles auswendig lernen. Sie haben mehrere Schaltflächen kennen gelernt: Button, RadioButton und CheckBox. Obwohl sie sich in ihrer Implementierung leicht unterscheiden, gestatten sie alle dem Benutzer, eine Auswahl zu treffen. Die gebräuchlichsten Mitglieder diese Steuerelemente sind die Text-Eigenschaft und das Click-Ereignis. Mit Hilfe des DateTimePicker-Steuerelements kann der Benutzer Zeiten und Daten wählen, indem er sie entweder manuell eingibt oder aus der Dropdown-Liste des Kalenders auswählt. Was uns zum nächsten Steuerelement führt: der ComboBox. Sie ist eine Kombination aus TextBox und ListBox; ein Benutzer kann Text von Hand eingeben oder Elemente aus einer Dropdown-Liste auswählen. Anders als die ComboBox erlaubt es die ListBox dem Benutzer, mehrere Elemente auf einmal auszuwählen. Dieses Steuerelement ist sehr nützlich, wenn Sie dem Benutzer mehrere Auswahlmöglichkeiten anbieten wollen. ImageList und PictureBox haben beide mit Bildern zu tun, unterscheiden sich aber stark voneinander. ImageList ist im Grunde ein Array von Grafiken, das Sie mit anderen Steuerelementen verknüpfen können, um Grafiken anzuzeigen (so wie die Schaltflächen für eine ToolBar). Eine PictureBox wird lediglich dazu verwendet, dem Benutzer eine einzelne Grafik anzuzeigen.

228

Fragen und Antworten

Mit Hilfe von TabControl können Sie Steuerelemente auf logische Weise gruppieren, indem Sie Registerkarten (Tabseiten) verwenden. Die Steuerelemente TextBox und RichTextBox bieten dem Benutzer die Möglichkeit, Text in ein Feld einzugeben. Letzteres bietet den weitaus größeren Funktionsumfang: Der Benutzer kann u.a. Schriftart, Farbe, Größe und Ausrichtung des Textes ändern. Dieses Steuerelement ähnelt WordPad. Mit Hilfe des Timer-Steuerelements lassen sich Ereignisse in bestimmten Zeitabständen auslösen. Unter anderem können Sie damit Weckuhren erstellen oder automatisierte Datensicherungsprozeduren durchführen lassen. Mit TreeView können Sie hierarchisch angeordnete Informationen in einer einfach zu bedienenden Strukturansicht anzeigen. Zum Schluss haben Sie gelernt, wie Sie das Layout Ihrer Steuerelemente in Formularen steuern, indem Sie Dock, Anchor, TabIndex und TabStop verwenden. Die ersten beiden Eigenschaften führen ähnliche Funktionen aus, der Unterschied besteht darin, dass Dock ein Steuerelement an den Rand eines Formulars bewegt, Anchor aber die Position eines Steuerelements relativ zu den Rändern des Formulars fixiert – hier ist kein Andocken an den Rand nötig.

6.15 Fragen und Antworten F

GDI+ wurde heute mehrmals erwähnt. Um was handelt es sich dabei? A

GDI+ ist die erweiterte Schnittstelle zu Grafikgeräten (Graphics Device Interface, GDI), mit deren Hilfe Sie verschiedene Grafiktypen zeichnen und bearbeiten können und die Sie in die Lage versetzt, Zeichenverfahren in Windows zu handhaben. GDI+ wurde entwickelt, um Entwicklern zu ermöglichen, sich auf die Grafikfunktionen zu konzentrieren, ohne dabei an die vielen unterschiedlichen Typen von Grafik-Hardware (wie etwa 3D-Grafikkarten) und Treiber denken zu müssen. GDI+ ist das Hauptthema an Tag 13.

F

Verwendet Microsoft Word ein RichTextBox-Steuerelement als seine Benutzeroberfläche? A

Nein. Word verwendet eine besondere, speziell dafür entwickelte Steuerelementart, die der Öffentlichkeit nicht zugänglich ist. Rich Text wird als normaler Text dargestellt, doch mit speziellem Code für die Formatierung versehen. Word benutzt für das Speichern seiner Dateien ein Binärformat, das sie nur für Word brauchbar macht. Wenn Sie also eine Textverarbeitung mit dem gleichen Funktionsreichtum wie Word erstellen wollen, müssen Sie entweder Ihr eigenes Steuerelement schreiben oder auf dem RichTextBox-Steuerelement aufbauen.

229

Windows Forms mit Steuerelementen erweitern

6.16 Workshop Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wodurch werden RadioButton-Auswahlen begrenzt? 2. Was bedeutet die folgende Datums-/Zeit-Formatzeichenfolge? "hh:MM:yy-dddd"

3. Welches Objekt ist mit dem TreeView-Steuerelement verknüpft? 4. Setzen Sie für ein Button-Steuerelement namens btHappy Eigenschaften, die es dazu veranlassen, senkrecht zu expandieren, sobald sein Containerformular seine Größe ändert. 5. Nennen Sie zehn Mitglieder, die allen heute besprochenen Steuerelementen gemeinsam sind! 6. Wie heißen die fünf Mitglieder des Timer-Steuerelements? 7. Wie fügt man dem Steuerelement TabControl Tabseiten hinzu? Beschreiben Sie dies in Worten und mit Hilfe einer Zeile Code. 8. Welche beiden Methoden sollte man ausführen, wenn man vorhat, einer ComboBox mit Hilfe der Add-Methode viele Elemente hinzuzufügen? Wenn Sie an Ihr Wissen über Windows Forms-Steuerelemente denken: Welche anderen heute vorgestellten Steuerelemente haben Ihrer Ansicht nach ebenfalls diese Methoden? 9. Wahr oder falsch? Das PictureBox-Steuerelement kann nur Bitmap- (.BMP) und GIFGrafiken anzeigen.

Übung Erstellen Sie in C# eine Anwendung, mit deren Hilfe Benutzer ihre CD-Sammlung katalogisieren und betrachten können. Sie müssen neue Künstler/Alben/Stücke in den Katalog aufnehmen und mit einer hierarchischen Liste betrachten können. (Kümmern Sie sich nicht um das Speichern der Eingaben – das besprechen wir an Tag 9, wenn es um ADO.NET geht.) Denken Sie daran, dass Alben nur Künstlern hinzugefügt werden und Stücke nur Alben.

230

Mit Dialogfeldern arbeiten

7

Mit Dialogfeldern arbeiten

Dialogfelder sind eine spezielle Art von Windows Forms, die hauptsächlich für die Interaktion mit dem Benutzer und die Anzeige von Meldungen eingesetzt wird. Während der Arbeit mit Windows haben Sie wahrscheinlich bereits Tausende davon gesehen, so etwa wenn Sie sich in Windows einloggen wollen. Dialogfelder sind ein sehr vielseitiges Werkzeug für Windows Forms und bieten zahlreiche komplexe Optionen für Ihre Anwendungen und deren Benutzer. Wir werden den ganzen Tag damit verbringen, den nicht ganz einfachen Umgang mit Dialogfeldern und den speziellen Dialogarten in Windows Forms zu erlernen. Heute erfahren Sie etwas über die verschiedenen Arten von Dialogfeldern in Windows und wie Sie 쐽

das »Ergebnis« eines Dialogfeldes bestimmen,



jedes beliebige Dialogfeld anpassen,



mit Hilfe von Dialogfeldern häufig auftretende Aufgaben in Windows bewältigen.

7.1

Informationen holen und anzeigen

Die wichtigste Aufgabe eines Dialogfeldes besteht darin, den Benutzer aufzufordern, Informationen einzugeben. Dies geschieht auf eine ganz andere Weise als bei Ihrem zentralen Windows Formular-Objekt, denn Sie können die Aufmerksamkeit des Benutzers auf recht direkte Weise gewinnen. Während Benutzer in Ihrem Formular häufig die Bitte um Information nur überfliegen, können Sie hingegen recht sicher sein, dass sie die angeforderten Informationen in einem Dialogfeld eingeben. Da Dialogfelder sich gut dazu eignen, die Aufmerksamkeit des Benutzers zu fesseln (Windows begleitet die erste Anzeige eines Dialogfeldes häufig mit einem Warnton), scheinen sie auch geradezu perfekt für das Anzeigen wichtiger Informationen zu sein. Im gestrigen Beispiel einer Weckuhr haben Sie das Meldungsfeld, einen speziellen Dialogfeldtyp, verwendet, um dem Benutzer eine Weckmeldung zukommen zu lassen. In ähnlicher Weise haben Sie in der gestrigen Übung, dem Aufbau eines Musikkatalogs, den Benutzer per Dialogfeld darauf hingewiesen, keine Objekte der falschen Kategorie zuzuordnen (etwa einen Künstler einem Album hinzuzufügen statt andersherum). Ob nun beim Anzeigen oder Einholen von Information, ein Dialogfeld schickt stets Daten zurück an das Hauptformular (mehr dazu später). Nachdem die erbetenen Informationen beschafft wurden, kann der Aufrufer des Dialogfeldes, normalerweise ein Form-Objekt, diese Informationen dazu verwenden, um einige Aktionen auszuführen. Wenn Sie beispielsweise in Word ein Dokument bearbeiten und versuchen, es ohne Speichern zu schließen, erscheint ein Dialogfeld, die Sie zum Spei-

232

Informationen holen und anzeigen

chern auffordert, wie in Abbildung 7.1 zu sehen. Je nachdem auf welche Schaltfläche der Benutzer klickt (JA, NEIN oder ABBRECHEN), führt Word die angemessene Aktion aus (also jeweils speichern, nicht speichern oder das Schließen des Dokuments abbrechen). Auf diese Weise zeigt das Dialogfeld einerseits eine Meldung an und holt auf der anderen Seite Informationen ein.

Abbildung 7.1: Dieses Dialogfeld in Microsoft Word erfordert eine Reaktion vom Benutzer.

Wie Sie heute weiter unten sehen werden, können Dialogfelder weitaus komplizierter sein, als dieses einfache Beispiel vermuten lässt. Sie bieten Ihnen als Entwickler eine Fülle von Möglichkeiten, Informationen zu beschaffen, und das Beste daran ist, dass .NET bereits die meisten komplizierten Arbeiten für Sie erledigt hat. Im Grunde ist ein Dialogfeld ein Form-Objekt, dessen FormBorderStyle-Eigenschaft auf FixedDialog gesetzt ist. (Wie Sie von Tag 3 noch wissen, bedeutet dies, dass das Fenster genau wie ein typisches Windows Forms-Fenster aussieht, nur dass sich seine Größe nicht ändern lässt.) Zusammen mit einer Eigenschaft, die als Modalität bezeichnet wird (mehr dazu später), macht dies Dialogfelder sehr effizient beim Einholen von Benutzereingaben. Der Benutzer kann nicht mit seiner Tätigkeit fortfahren, bis er das Dialogfeld beantwortet hat, um sie »aus dem Weg zu schaffen«. Obwohl es zutrifft, dass die einfachsten Dialogfelder nur etwas Text und zwei Schaltflächen für OK und ABBRECHEN aufweisen, sollte Sie dies nicht davon abhalten, dem Dialogfeld eigene Steuerelemente hinzuzufügen. Sie können sogar jedes Windows FormsSteuerelement – selbst Menüs und Statusleisten – integrieren, als ob es ein Form-Objekt wäre. Doch wozu sollte man ein Dialogfeld zu einer kompletten Anwendung ausbauen? Sicher nicht bis in Word-Dimensionen, doch vielleicht bis zur Komplexität eines Taschenrechners oder eines Temperaturumrechners (Fahrenheit in Celsius und umgekehrt). Doch letzten Endes läuft es auf Folgendes hinaus: Forms wurden dafür entwickelt, als Anwendungen zu agieren, und Dialogfelder nicht. Ich empfehle

Bitte beachten Sie

Verwenden Sie wie bisher Forms für die Anwendungserstellung, es sei denn, Sie hätten einen triftigen Grund, der dagegen spräche.

Verlassen Sie sich nicht nur auf Dialogfelder, um Benutzereingaben zu erhalten. Statt dessen sollten Sie sie nur einsetzen, um dem Benutzer etwas mitzuteilen.

233

Mit Dialogfeldern arbeiten

7.2

Unterschiedliche Arten von Dialogen

Am gebräuchlichste ist das OK/ABBRECHEN-Dialogfeld, doch es sind noch einige andere Typen verfügbar. Das .NET Framework stellt verschiedene Dialogfelder für die Mehrzahl der üblichen Windows-Aufgaben, die Benutzereingaben erfordern, zur Verfügung. Es steht Ihnen sogar frei, einen Benutzer dazu zu zwingen, auf ein Dialogfeld zu antworten, bevor er irgendetwas anderes tun kann. Wir wollen einen Blick auf die Eigenschaften werfen, die Dialogfelder steuern.

Modal vs. nichtmodal Ein modales Formular oder Dialogfeld fordert eine Eingabe vom Benutzer, bevor er mit seiner Tätigkeit in der Anwendung fortfahren kann. Ein Beispiel dafür ist die »Änderungen speichern?«-Abfrage in Word. Erscheint sie, kann man in Word nichts anderes tun als sie zu beantworten. Versucht man in das Hauptdokumentfenster zurückzugehen, erhält man einen Fehlerton und das Dialogfeld fordert erneut zur Antwort auf. Daher erfordert ein modaler Dialog eine Benutzereingabe. Modale Dialoge halten Sie zwar in der jeweiligen Anwendung auf, Sie können aber jederzeit zu einer anderen Anwendung wechseln, etwa zu Excel oder Photoshop. Ein nichtmodales Dialogfeld ist das genaue Gegenteil: Es verlangt keine Benutzereingabe, und der Benutzer kann zwischen der Anwendung und dem Dialog nach Belieben hin und her wandern. Der typische Einsatzbereich dafür ist ein Hilfe-Fenster, das sich neben der Anwendung positioniert und häufig auftretende Fragen beantwortet. Solche Dialoge verlangen nicht, dass Sie mit ihnen interagieren, obwohl Sie das natürlich tun können. Ein gutes Beispiel wäre der Microsoft Office-Assistent in Form einer Büroklammer. Es ist für den Entwickler oftmals schwieriger, mit nichtmodalen Dialogen umzugehen als mit modalen, denn er muss praktisch zwei Formulare gleichzeitig überwachen und passende Daten hin und her leiten. Bei einem modalen Dialog hingegen hat man es entweder mit dem Dialogfeld oder mit der Anwendung zu tun, nie aber mit beiden zugleich, was die Sache erleichtert. Aus diesem Grund werden nichtmodale Dialoge fast ausschließlich für die Informationspräsentation eingesetzt, aber nicht für die Datenbeschaffung.

234

Dialogfelder

Die Standarddialogfelder Standarddialogfelder (Common Dialog Boxes) dienen dazu, allgemein übliche Aufgaben vorzubereiten wie etwa das Öffnen und Speichern einer Datei, die Auswahl einer Schriftart usw. Da sie für häufige Aufgaben eingesetzt werden, sind sie an vielen Stellen anzutreffen – man denke nur an jede Anwendung, die das Öffnen einer Datei erlaubt. Sie setzt die gleiche Benutzeroberfläche für diese Aufgabe ein wie viele andere Anwendungen auch: ein Standarddialogfeld. Für den Entwickler ist dies enorm hilfreich, denn er muss für jede Anwendung, die er schreibt, keine eigenes Dialogfeld programmieren. In .NET Framework sind bereits sieben Klassen für Standarddialogfelder fix und fertig enthalten: 쐽

ColorDialog: Damit kann der Benutzer eine Farbe aus einer vorgegebenen Palette aus-

wählen (etwa um die Farbe einer Schriftart zu ändern). 쐽

FontDialog: Damit kann der Benutzer eine Schriftart auswählen.



OpenFileDialog: Damit kann der Benutzer eine zu öffnende Datei auswählen. Das

Dialogfeld öffnet die Datei nicht selbst, sondern erlaubt nur ihre Auswahl; Sie müssen in Ihrer Anwendung dafür sorgen, die ausgewählte Datei zu holen und zu öffnen; das Dialogfeld erledigt das nicht automatisch. 쐽

PageSetupDialog: Damit kann der Benutzer in Windows Dokumente formatieren (Sei-

tenränder einstellen, Papiergröße wählen usw.). 쐽

PrintDialog: Damit kann der Benutzer einen Drucker auswählen, Druckoptionen einstellen und ein Dokument drucken.



PrintPreviewDialog: Seiten- oder Druckvorschau. Damit kann der Benutzer prüfen, wie ein Dokument auf Papier aussehen wird, bevor er es ausdruckt.



SaveFileDialog: Damit kann der Benutzer Dateien in einem ausgewählten Ordner

speichern (gleicht dem »SPEICHERN UNTER...«-Dialog in Word). Diese Klassen verhalten sich genau wie andere Windows Forms-Steuerelemente oder -Dialogfelder, daher sollten Sie sie rasch erlernen können. Wir kommen heute noch auf sie zurück. Wir wollen zunächst sehen, wie man einfache Dialogfelder verwendet.

7.3

Dialogfelder

Das einfachste Dialogfeld ist die MessageBox-Klasse (Meldungsfeld). Sie verfügt über ein einziges (nicht geerbtes) Mitglied, die Show-Methode, die das Dialogfenster, das nur eine OK-Schaltfläche aufweist, auf den Bildschirm bringt:

235

Mit Dialogfeldern arbeiten

MessageBox.Show("Hallo Welt")

Diese Codezeile zeigt ein Dialogfenster mit dem Text »Hallo Welt« an sowie eine OKSchaltfläche. Für eine Reihe von Situationen reicht dieser Funktionsumfang aus. Die Show-Methode verfügt jedoch noch über einige Parameter, mit denen Sie sie Ihren Wünschen anpassen können. Sie können eine Instanz der MessageBox nicht direkt erzeugen. Der folgende Code etwa erzeugt einen Fehler: dim msgMyMessage as New MessageBox

Der einzige Weg, auf dem Sie mit der MessageBox interagieren können, besteht im Aufruf ihrer Show-Methode: MessageBox.Show("Hallo Welt!")

Tabelle 7.1 führt die verschiedenen Parameter für die Show-Methode auf, und zwar in der Reihenfolge, in der sie im Methodenaufruf stehen müssen (bis auf Nummer 2 sind alle Parameter optional). Nr. des Parameters

Typ

Beschreibung

1

IWin32Window

Das Fenster, vor dem das Dialogfeld angezeigt werden soll (z.B. Me in VB .NET oder this in C#).

2

String

Der im Dialogfenster anzuzeigende Text. Dies ist der einzige obligatorische Parameter.

3

String

Der in der Titelzeile anzuzeigende Text.

4

MessageBoxButtons

Anzahl und Typ der anzuzeigenden Schaltflächen.

5

MessageBoxIcon

Das im Dialogfenster anzuzeigende Symbol.

6

MessageBoxDefaultButton

Legt die (vorausgewählte) Standardschaltfläche fest.

7

MessageBoxOptions

Diverse Optionen für das Dialogfeld.

Tabelle 7.1: Parameter für die Methode MessageBox.Show

Sie könnten die MessageBox auf folgende Art und Weise anzeigen: MessageBox.Show(Me, "Hallo Welt", "Mein Dialogfeld") MessageBox.Show("Hallo Welt", "Mein Dialogfeld", MessageBoxButtons.OKCancel) MessageBox.Show(Me, "Hallo Welt", "Mein Dialogfeld", MessageBoxButtons.OKCancel, MessageBoxIcon.Hand)

236

Dialogfelder

Diese drei Aufrufe erzeugen die drei Dialogfelder, die Sie in Abbildung 7.2sehen. Abbildung 7.2: Sie können einem Meldungsfeld auf einfache Weise weitere (bis zu drei) Schaltflächen hinzufügen.

Die ersten drei in Tabelle 7.1 aufgeführten Parameter sind leicht zu verstehen. Die vier letzten hingegen sind Aufzählungen, die Anzeigeoptionen für das Dialogfeld angeben. Die MessageBoxButtons-Aufzählung enthält die Werte AbortRetryIgnore, OK, OKCancel, RetryCancel, YesNo und YesNoCancel. Der Zweck jeder einzelnen ist einfach herauszufinden: AbortRetryIgnore zeigt drei Schaltflächen an, die die Aufschriften BEENDEN, WIEDERHOLEN und IGNORIEREN tragen; OK zeigt einfach eine Schaltfläche mit OK an, usw. MessageBoxIcon zeigt neben dem Text im Meldungsfenster ein Symbol an. Die Werte können in Asterisk, Error, Exclamation, Hand, Information, None, Question, Stop oder Warning

bestehen. Die tatsächlich angezeigten Symbole können von Betriebssystem zu Betriebssystem variieren (in Windows sind es oftmals die gleichen). Die Abbildung 7.3 zeigt jedes dieser Symbole in Windows NT.

Abbildung 7.3: Der neunte Wert der MessageBoxIconAufzählung lautet None (keines). MessageBoxDefaultButton ist eine leicht zu erlernende Eigenschaft – sie besitzt nur drei Werte: Button1, Button2 und Button3. Die Eigenschaft legt fest, welche Schaltfläche im

Dialogfenster den voreingestellten Fokus hat. Diese Schaltfläche ist vorausgewählt, wenn das Dialogfeld angezeigt wird, und ist auch diejenige, die angeklickt wird, falls der Benut-

237

Mit Dialogfeldern arbeiten

zer die (¢)-Taste drückt. (Nochmals: Es passen maximal drei Schaltflächen in ein Meldungsfeld.) MessageBoxOptions schließlich besitzt vier nicht miteinander verwandte Werte. DefaultDesktopOnly und ServiceNotification geben an, dass das Meldungsfeld auf dem gerade

aktiven Bildschirm erscheinen soll (falls der Benutzer mehr als einen Monitor einsetzt). RightAlign gibt vor, dass der Text im Meldungsfeld rechtsbündig ausgerichtet sein soll, und RtlReading gibt an, dass der Text so formatiert sein soll, dass man ihn von rechts nach links lesen kann (was etwa in arabischen Ländern hilfreich ist). Mit Hilfe der Or- und |Operatoren können Sie diese Werte miteinander kombinieren. MessageBox.Show("Hallo Welt","Mein Dialogfeld"", MessageBoxButtons.OKCancel, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.RightAlign Or MessageBoxOptions.ServiceNotification)

Eigene Dialogfelder erstellen Die MessageBox-Klasse ist ein ausgezeichnetes vorgefertigtes Dialogsteuerelement, das eine ganze Reihe von Optionen anbietet. Doch in manchen Fällen genügt die MessageBox Ihren Ansprüchen nicht. In diesen Fällen können Sie Ihre eigene Dialogfeldklasse erstellen. Diese aufzubauen entspricht fast dem Aufbau einer Windows Forms-Klasse; man erbt von System.Windows.Forms.Form, erstellt Steuerelemente, fügt sie dem Formular hinzu und sieht auch Ereignishandler vor. Der einzige Unterschied besteht darin, dass man dem übergeordneten Formular (das das Dialogfeld anzeigt) alle notwendigen Eigenschaften zur Verfügung stellt. Lassen Sie uns als Beispiel einmal das DATEI/ÖFFNEN-Dialogfeld von Microsoft Word, das in Abbildung 7.4 zu sehen ist, auf seine Bestandteile hin untersuchen.

Abbildung 7.4: Das Dialogfeld zum Öffnen von Dateien enthält verschiedene Arten von Steuerelementen.

238

Dialogfelder

Sie vermuten sicher schon, welche Arten von Steuerelementen nötig sind, um dieses Dialogfenster aufzubauen: eine ComboBox für die Dropdown-Listen der Verzeichnisse und der Dateitypen, eine Reihe von Button-Steuerelementen für die ÖFFNEN- und ABBRECHENSchaltflächen, ein paar Label-Steuerelemente, möglicherweise ein TreeView-Steuerelement und ein paar TextBox-Steuerelemente. Sobald der Benutzer eine Datei ausgewählt hat, will die Hauptanwendung nur eines wissen: Welche Datei ist das? Daraufhin wird der ausgewählte Dateiname im Textfeld DATEINAME angezeigt. Daher muss die Hauptanwendung die Eigenschaft Text dieser TextBox wissen, damit sie die Datei angemessen behandeln kann. Um diese Eigenschaftstypen dem Hauptformular offen zu legen, müssen wir Eigenschaften erstellen (ähnlich wie die Text-Eigenschaft). Listing 7.1 zeigt eine benutzerdefinierte Dialogfeldklasse, mit der der Benutzer die Titelzeile seines Formulars ändern kann. Listing 7.1: Ihre erste Anwendung mit einem benutzerdefinierten Dialogfeld 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:

Imports System Imports System.Windows.Forms Imports System.Drawing namespace TYWinForms.Day7 public class ChangeCaption : Inherits Form private btnChange as new Button private tbCaption as new TextBox public readonly property Caption as String Get Return tbCaption.Text End Get end property public sub New tbCaption.Location = new Point(10,10) btnChange.Text = "Ändern!" btnChange.Location = new Point(125,10) AddHandler btnChange.Click, new EventHandler(AddressOf Me.Clicked) Me.Text = "Überschrift ändern" Me.Size = new Size(250,100) Me.Controls.Add(btnChange) Me.Controls.Add(tbCaption) end sub

239

Mit Dialogfeldern arbeiten

30: 31: 32: 33: 34:

private sub Clicked(Sender as Object, e as EventArgs) Me.Hide end sub end class End Namespace

Speichern Sie diese Datei als listing7.1.vb. Auf den ersten Blick sieht der Code wie jede andere Windows Forms-Anwendung aus. In den Zeilen 8 und 9 erstellen Sie zwei Steuerelemente, richten in den Zeilen 18 bis 27 Eigenschaften in einem Konstruktor ein und erstellen einen Ereignishandler in den Zeilen 30 bis 32. Der Hauptunterschied besteht darin, dass es in diesem Code keine Main-Methode gibt. Das liegt daran, dass diese Klasse niemals selbstständig ausgeführt wird – sie sollte nur in einem Formular oder einer Anwendung verwendet werden. Werfen Sie einen Blick auf Zeile 30 und den Ereignishandler. Er tut nur eines, nämlich die Hide-Methode des Formulars aufzurufen. Ohne dies würde nichts passieren, sobald der Benutzer auf die Schaltfläche »Titelzeile ändern« geklickt hat. Da wir ein modales Dialogfeld erstellen wollen, würde dies bedeuten, dass der Benutzer nicht wieder zu der ursprünglichen Anwendung zurückkehren könnte. Indem Hide aufgerufen wird, bringen wir das Dialogfeld zum Verschwinden und holen statt dessen die ursprüngliche Anwendung in den Vordergrund, so dass wieder eine Interaktion möglich ist. Beachten Sie schließlich die Zeilen 11 bis 15, die das Erstellen von Eigenschaften in einer Klasse illustrieren. Die Deklaration ähnelt der einer Funktion, verwendet aber das Schlüsselwort property statt function. Innerhalb dieser Eigenschaft finden sich zwei besondere Schlüsselwörter: Get und Set. Der Code in Get wird ausgeführt, sobald eine andere Klasse auf diese Eigenschaft zugreifen will. Set hingegen wird eingesetzt, wenn eine andere Klasse für diese Eigenschaft einen Wert setzt. In unserem Fall brauchen wir jedoch keinen Set-Teil, daher schreiben wir nur den für Get und sorgen dafür, dass die Eigenschaft als readonly (Zeile 11) markiert ist. In die Get- und Set-Teile können Sie jeden beliebigen Code einfügen; hier jedoch wollen wir nur den Wert zurückgeben, der sich in der TextBox befindet, wobei wir das Return-Schlüsselwort verwenden. In C# würde diese Eigenschaft wie in folgendem Codestück aussehen: public string Caption { get { return tbCaption.Text; } }

Beachten Sie, dass C# nicht die Schlüsselwörter property oder readonly erfordert. Wenn Sie die Eigenschaft value setzen wollen, verwenden Sie das Schlüsselwort value:

240

Dialogfelder

'VB .NET Set tbCaption.Text = value End Set //C# set { tbCaption.Text = value; }

Die einzigen public-Mitglieder in der ChangeCaption-Klasse sind der Konstruktor und die Caption-Eigenschaft. Da wir nicht wünschen, dass andere Klassen direkt auf den Rest der Mitglieder zugreifen, machen wir sie alle private. Sie können diese Vorgehensweise bei der Deklaration von public- und privateVariablen auch für die Erstellung von Eigenschaften Ihrer regulären Klassen verwenden. Wir wollen uns die Anwendung ansehen, die diese benutzerdefinierte Dialogfeldklasse verwenden soll. Listing 7.2 zeigt eine einfache Applikation, mit der der Benutzer die Titelzeile des Formulars ändern kann, indem er das ChangeCaption-Dialogfeld aus Listing 7.1 sowie dessen Caption-Eigenschaft einsetzt. Listing 7.2: Die Verwendung des benutzerdefinierten Dialogfeldes 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: namespace TYWinForms.Day7 6: 7: public class Listing72 : Inherits Form 8: private btnChange as new Button 9: 10: public sub New 11: btnChange.Text = "Überschrift ändern" 12: btnChange.Location = new Point(75,75) 13: btnChange.Size = new Size(100,75) 14: AddHandler btnChange.Click, new EventHandler(AddressOf  Me.ShowChangeBox) 15: 16: Me.Text = "Überschrift ändern" 17: Me.Controls.Add(btnChange) 18: end sub 19: 20: public sub ShowChangeBox(Sender as Object, e as EventArgs) 21: dim dlgChangeCaption as new ChangeCaption 22: dlgChangeCaption.ShowDialog

241

Mit Dialogfeldern arbeiten

23: 24: 25: 26: 27: 28: 29: 30: 31:

Me.Text = dlgChangeCaption.Caption end sub public shared sub Main() Application.Run(new Listing72) end sub end class End Namespace

Speichern Sie diese Datei unter dem Namen listing7.2.vb. Listing 7.2 zeigt eine weitere einfache Windows Forms-Klasse, nur dass diese die Hauptanwendung darstellt und daher eine Main-Methode aufweist. Die Zeilen 1 bis 18 sind typisch; sie erstellen und instantiieren das Formular und seine Steuerelemente. Erst in Zeile 20 beginnt es interessant zu werden. Die Methode ShowChangeBox behandelt das Click-Ereignis der Schaltfläche btnChange. In Zeile 21 erzeugen Sie eine neue Instanz der in Listing 7.1 erstellten ChangeCaption-Klasse. Beachten Sie, dass dieses Vorgehen dem beim Erstellen jedes anderen .NET-Objekts entspricht. In Zeile 22 rufen Sie die ShowDialog-Methode auf, die das Dialogfenster zur modalen Anzeige veranlasst.

Moment mal! In Ihrer ChangeCaption-Klasse haben Sie keine ShowDialogMethode erstellt. Wo kam sie also her? Die Klasse System.Windows.Forms.Form enthält die ShowDialog-Methode, weshalb Ihre Dialogfeldklasse sie erben konnte. Das bedeutet aber auch, dass absolut jedes Windows Form-Formular als Dialogfeld eingesetzt werden kann. Das bietet Ihnen eine Menge Flexibilität, wenn Sie Ihre Anwendungen schreiben. Nachdem Zeile 22 ausgeführt und das Dialogfeld angezeigt wurde, geht die Kontrolle über die Anwendung an das modale Dialogfeld über – bis dieses Fenster wieder geschlossen wird, kann die Hauptanwendung nichts tun. Sobald Sie etwas in das Dialogfeld eingeben und die Schaltfläche »ÄNDERN!« anklicken, geht die Kontrolle wieder an die Hauptanwendung über. Die Ausführung geht in Zeile 24 weiter, die die Caption-Eigenschaft des Dialogfeldes holt und sie der Text-Eigenschaft des aktuellen Formulars zuweist. Nun verstehen Sie, wie die Caption-Eigenschaft aus Listing 7.1 eingesetzt wird. Da diese beiden Listings in unterschiedlichen Dateien gespeichert sind, müssen Sie beide zu einer Anwendung kompilieren. Mit Hilfe von Visual Studio .NET ist dies einfach: Sorgen Sie nur dafür, dass sich beide Dateien im selben Projekt befinden und kompilieren Sie sie dann ganz normal (indem Sie ERSTELLEN aus dem ERSTELLEN-Menü auswählen).

242

Dialogfelder

Wenn Sie den Befehlszeilencompiler verwenden, geben Sie nur die beiden Dateien im Befehl an: vbc /t:winexe /r:system.windows.forms.dll /r:system.drawing.dll listing7.1.vb listing7.2.vb

Solange sich diese beiden Dateien im selben Ordner befinden, funktioniert dieser Befehl und Sie erhalten eine ausführbare Datei namens Listing7.1.exe (die Reihenfolge, die Sie für die zu kompilierenden Dateien angeben, bestimmt den Namen der ausführbaren Datei). Das war's schon. Abbildung 7.5 zeigt das Ergebnis.

Abbildung 7.5: Es ist recht einfach, ein benutzerdefiniertes modales Dialogfeld zu erstellen.

Beachten Sie, dass Sie keine besonderen Eigenschaften für Ihre benutzerdefinierte Dialogfeldklasse einrichten mussten. Indem Sie die ShowDialog-Methode aufrufen, stellt die CLR sie automatisch als modales Dialogfeld dar. Doch genau wie in einem gewöhnlichen Formular können Sie Eigenschaften beliebig modifizieren, so etwa SizeGripStyle, um den Größenziehpunkt in der unteren rechten Ecke des Formulars zu ändern, oder ControlBox, um die rechts oben verfügbaren Steuerschaltflächen festzulegen. Wollen Sie Ihr Dialogfeld hingegen nichtmodal präsentieren, rufen Sie statt ShowDialog die Methode Show auf. Der Benutzer wird zwischen Dialogfenster und Hauptanwendung hin und her wechseln können. Doch in der soeben erstellten Anwendung würde dies eine Reihe von Problemen verursachen: 쐽

Da die Ausführung im Hauptformular nicht angehalten wird, sobald die Show-Methode aufgerufen wurde, wird Zeile 24 des Listing 7.2 (wo die Titelzeile aus dem Dialogfeld geholt wird) sofort ausgeführt und die Titelzeile des Formulars wird sich in eine leere Zeichenfolge verwandeln. Denn schließlich ist bei der Dialogfelderstellung das Textfeld tbCaption leer, und Zeile 24 wird ausgeführt, bevor Sie dazukommen, Text einzutragen und auf die ÄNDERN!-Schaltfläche zu klicken.



Der Benutzer kann fortwährend auf die Schaltfläche TITELZEILE ÄNDERN im Hauptformular klicken, was dazu führt, dass viele Dialogfelder auf einmal angezeigt werden.

243

Mit Dialogfeldern arbeiten

Wenn der Benutzer Text in das tbCaption-Feld eingibt und die Schaltfläche ÄNDERN! anklickt, verschwindet in diesem Fall das Dialogfeld und sonst passiert nichts; die Titelzeile des Formulars ändert sich nicht, denn Zeile 24 wurde bereits lange vor dem Anklicken der ÄNDERN!-Schaltfläche ausgeführt. Um also die Titelzeile dennoch zu bestimmen, müssen Sie irgendwie auf die Eigenschaft tbCaption.Text des Dialogfeldes zugreifen. Dies gelingt, wenn Sie eine neue public-Eigenschaft erstellen: Public property Caption As String Get return tbCaption.Text End Get Set tbCaption.Text = value End Set End Property

Die Lösung für das zweite Problem besteht im Einsatz eines modalen Dialogfeldes. Doch dann muss der Benutzer das Dialogfeld explizit schließen, bevor ihm erlaubt ist, zum Hauptformular zurückzukehren. Sorgen Sie dafür, dass Sie nur modale Dialogfenster verwenden, wenn Sie diese Funktionsweise nutzen wollen.

Informationen abrufen Wir haben bereits gesehen, wie man mit Hilfe von Eigenschaften Daten aus einem Dialogfeld beschafft. Doch manchmal reichen Eigenschaften nicht aus, um jede gewünschte Information zu bekommen. Gelegentlich müssen Sie auch wissen, auf welche Weise der Benutzer das Dialogfeld geschlossen hat: Hat er auf die OK-Schaltfläche geklickt? Oder doch auf die ABBRECHEN-Schaltfläche? Diese Fakten, die man als das Ergebnis (result) des Dialogfeldes bezeichnet, können die Arbeitsweise Ihrer Anwendung verändern, so dass es nötig ist, auch diese Werte zu beschaffen. Die Aufzählung DialogResult enthält mehrere Werte, mit deren Hilfe Sie das Ergebnis eines Dialogfeldes bestimmen. Die ShowDialog-Methode gibt einen Wert aus dieser Aufzählung zurück, so dass Sie damit tun können, was Sie wollen, etwa so: dim objDR as DialogResult objDR = dlgChangeCaption.ShowDialog if objDR = DialogResult.OK then 'die Titelzeile ändern elseif objDR = DialogResult.No then 'nichts tun end if

244

Dialogfelder

Je nach dem vom Dialogfeld gelieferten Ergebnis können Sie eine bedingte Ausführung beginnen. Für jede Schaltfläche in einem Dialogfeld gibt es einen DialogResult-Wert (und noch mehr): 쐽

Abort/Beenden



Cancel/Abbrechen



Ignore/Ignorieren



No/Nein



None/Kein



OK



Retry/Wiederholen



Yes/Ja

Listing 7.3 zeigt ein Beispiel dafür, wie man DialogResult in einem Meldungsfeld einsetzt. Listing 7.3: Das Ergebnis eines Dialogfeldes abrufen (in C#) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day7 { public class Listing73 : Form { public Listing73() { if (MessageBox.Show("Wünschen Sie eine Vollbilddarstellung dieser Anwendung?", "Listing 7.3", MessageBoxButtons.YesNo) == DialogResult.Yes) { this.Size = Screen.GetWorkingArea(this).Size; this.DesktopLocation = new Point(0,0); } } public static void Main() { Application.Run(new Listing73()); } } }

In Zeile 9 prüfen Sie das Ergebnis der MessageBox.Show-Methode. Wenn es dem DialogResult.Yes-Wert entspricht (der Benutzer also die JA-Schaltfläche angeklickt hat), führen Sie den Code in den Zeilen 10 bis 11 aus: Er setzt die Größe des Formulars auf die Größe des Benutzerbildschirms und seine Position

245

Mit Dialogfeldern arbeiten

dort auf die obere linke Ecke. Alternativ können Sie die Zeilen 10 bis 11 durch folgenden Code ersetzen, um die Anwendung zu maximieren: this.WindowState = FormWindowState.Maximized;

Weil Sie die MessageBox mit Hilfe des Schaltflächensatzes MessageBoxButtons.YesNo angezeigt haben, weiß die CLR, dass die JA-Schaltfläche den Wert DialogReturn.Yes und die NEIN-Schaltfläche DialogResult.No zurückgeben soll. Erinnern Sie sich an das Beispiel des Word-Dialogfensters »Möchten Sie die Änderungen in [Dokumentname] speichern?«. Klicken Sie auf JA, dann gibt das Dialogfeld Dialogresult.Yes zurück und Word weiß Bescheid: Es führt den Code zum Sichern des Dokuments aus (es öffnet das Dialogfeld DATEI/SPEICHERN UNTER nur, falls es sich um eine neues Dokument handelt) und schließt dann das Fenster. Klicken Sie hingegen auf NEIN, wird DialogResult.No übergeben, das Dokument wird nicht gespeichert und Word wird beendet (sofern Sie den BEENDEN-Befehl in irgendeiner Form erteilt haben). Klicken Sie auf ABBRECHEN, lautet das Ergebnis DialogResult.Cancel und nichts passiert (das Dokument wird weder gespeichert noch geschlossen, auch Word wird nicht beendet). Das Ergebnis eines Dialogfeldes auf diese Weise abzurufen ist eine hervorragende, standardisierte Möglichkeit, um Anwendungen über Benutzeraktionen zu informieren. Was passiert jedoch, wenn Sie Ihr eigenes Dialogfeld erstellt haben? Woher weiß die CLR, dass Ihre ÄNDERN!-Schaltfläche einen Wert DialogResult.Yes zurückgeben soll? Tatsächlich ist es viel einfacher als Sie befürchten. Schaltflächen und Formulare verfügen über eine DialogResult-Eigenschaft, mit der Sie angeben können, welches Ergebnis zurückgegeben werden soll. Schauen wir uns eine modifizierte Version des benutzerdefinierten Dialogfeldes aus Listing 7.1 an, die hier in Listing 7.4 in C#-Code zu sehen ist. Listing 7.4: Eine modifiziertes benutzerdefiniertes Dialogfeld in C# 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

246

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day7 { public class ChangeCaption2 : Form { private Button btnChange = new Button(); private TextBox tbCaption = new TextBox(); public String Caption { get { return tbCaption.Text; } }

Dialogfelder

16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:

public ChangeCaption2() { tbCaption.Location = new Point(10,10); btnChange.Text = "Ändern!"; btnChange.Location = new Point(125,10); btnChange.DialogResult = DialogResult.OK; this.Text = "Überschrift ändern "; this.Size = new Size(250,100); this.Controls.Add(btnChange); this.Controls.Add(tbCaption); } } }

Von der Sprache (und der Namensänderung der Klasse in Zeile 7) einmal abgesehen, gibt es in diesem Listing nur zwei Unterschiede zum Listing 7.1. Der Ereignishandler für das Clicked-Ereignis ist jetzt verschwunden, ebenso die Zuweisung eines Delegaten zu der Schaltfläche. Das wird gleich erklärt. Der zweite Unterschied besteht im Hinzufügen der Zeile 22. Hier weisen Sie der btnChange-Schaltfläche den Wert DialogResult.OK zu. Dies bewirkt zweierlei. Erstens gibt es dem übergeordneten Formular den Wert DialogResult.OK zurück (so wie die MessageBox in Listing 7.3 DialogResult.Yes zurückgab). Wird zweitens diese Schaltfläche angeklickt, schließt die CLR das Formular automatisch für Sie. Daher brauchen Sie nicht mehr die Hide-Methode aufzurufen und demzufolge auch keinen Ereignishandler für das Clicked-Ereignis. Indem Sie also die DialogResult-Eigenschaft angegeben haben, weiß die CLR, dass diese Klasse ein Dialogfeld ist und sich entsprechend verhält. Die Hauptanwendung muss sich ebenfalls ein klein wenig ändern. Die ShowChangeBoxMethode aus Listing 7.2 muss wie im folgenden Codestück aussehen: public void ShowChangeBox(Object Sender, EventArgs e) { DialogResult objDR; ChangeCaption2 dlgChangeCaption = new ChangeCaption2(); objDR = dlgChangeCaption.ShowDialog(); if (objDR == DialogResult.OK) { this.Text = dlgChangeCaption.Caption; } this.Text = dlgChangeCaption.Caption; }

247

Mit Dialogfeldern arbeiten

(Vergessen Sie nicht, auch den Rest des Listings in C#-Code umzuwandeln.) Das Ergebnis des neuen Dialogfeldes wird in der objDR-Variablen gespeichert, die in der zweiten Zeile erstellt wurde. Dieser Wert wird dann mit DialogResult.OK verglichen, und falls sie gleichwertig sind, ändert sich die Titelzeile des Formulars. Ist das nicht der Fall, passiert nichts. Ich empfehle

Bitte beachten Sie

Weisen Sie den Schaltflächen in Ihren benutzerdefinierten Dialogfeldern DialogResult-Werte zu. Dies ist eine Standardkonvention und erleichtert Ihnen und denjenigen, die Ihre Klasse verwenden, die Verwendung Ihrer benutzerdefinierten Dialogfelder.

Verlassen Sie sich nicht auf das übergeordnete Formular, dass es Ihr Dialogergebnis interpretiert, selbst wenn Sie der Einzige sind, der es je zu sehen bekommt. Das Zuweisen von DialogResult-Werten sorgt dafür, dass Ihre Anwendung standardisierte Werte verwendet, die leicht auszuwerten sind.

7.4

Standarddialogfelder

Wie gesagt gibt es sieben Standarddialogfelder in .NET (vgl. Aufzählung oben). Sie sind bereits vorgefertigt, um das Ausführen der entsprechenden Aufgaben zu erleichtern. In den folgenden Abschnitten wollen wir etwas über ihren jeweiligen Funktionsumfang erfahren. (Nähere Informationen finden Sie in Anhang B.) Alle sieben Dialogfelder außer PrintPreviewDialog weisen folgende zwei Mitglieder auf: ShowHelp und Reset. Das erste gibt an, ob im Dialogfeld eine HilfeSchaltfläche angezeigt werden soll. Die Reset-Methode setzt alle Eigenschaften eines bestimmten Dialogs auf ihre Vorgabewerte zurück. Diese Methode erweist sich im Umgang mit diesen vielschichtigen Steuerelementen als besonders hilfreich.

Ein- und Ausgabe von Dateien Die zwei Standarddialogfelder für die Ein- und Ausgabe (E/A bzw. I/O) von Dateien sind die Steuerelemente OpenFileDialog und SaveFileDialog. Sie haben eine Reihe von Funktionen gemeinsam, weshalb die meisten ihrer Eigenschaften, die wir hier besprechen, in beiden zu finden sind. Natürlich ist ihre wichtigste Eigenschaft FileName (und FileNames), denn sie gibt an, welche Datei der Benutzer zum Öffnen oder Speichern ausgewählt hat. Ihr Hauptformular kann diesen Wert holen und die betreffende Datei nach Bedarf öffnen oder speichern (mehr dazu an Tag 11). Das Instantiieren und Anzeigen dieser Steuerelemente ist einfach:

248

Standarddialogfelder

dim dlgOpen as new SaveFileDialog dlgOpen.ShowDialog

Dieser Code ergibt das in Abbildung 7.6 dargestellte Dialogfeld.

Abbildung 7.6: Das Steuerelement für den DATEI/SPEICHERN-Dialog ist in den meisten WindowsAnwendungen zu finden.

Beide Dialogfelder geben entweder DialogResult.OK oder DialogResult.Cancel zurück, je nachdem, ob der Benutzer SPEICHERN bzw. ÖFFNEN oder ABBRECHEN angeklickt hat. Daraufhin lässt sich die FileName-Eigenschaft abfragen: if dlgOpen.ShowDialog = DialogResult.OK then Me.Text = dlgOpen.FileName end if

Im OpenFileDialog können Sie dem Benutzer erlauben, mehr als eine Datei auszuwählen, indem Sie MultiSelect auf true setzen. Die FileName-Eigenschaft nimmt dann ein Array von Zeichenfolgen auf, die diese Dateien repräsentieren. Zusätzlich zu diesen Haupteigenschaften (FileName bzw. FileNames) gibt es noch weitere, mit denen Sie die Funktionen dieser Dialogfelder anpassen können. Die Filter-Eigenschaft lässt sich dazu verwenden, die Dateitypen festzulegen, die in den Feldern SPEICHERN UNTER/DATEITYP und ÖFFNEN/DATEITYP zu sehen sind. Diese Filter begrenzen die Typen von Dateien, die der Benutzer speichern oder öffnen kann. Der Filter selbst ist eine speziell formatierte Zeichenfolge. Der folgende Code lässt nur das Öffnen von Word(.doc) und Text- (.txt) Dateien zu: dlgOpen.Filter ="Nur-Text-Dateien (*.txt)|*.txt|Word-Dokumente (*.doc)|*.doc "

Die Zeichenfolge lässt sich in folgende Bestandteile zerlegen: Beschreibung (Erweiterung) | Filterzeichenfolge

249

Mit Dialogfeldern arbeiten

Die Beschreibung ist der benutzerfreundliche Text, den der Benutzer im jeweiligen Dialogfeld angezeigt bekommt. Der senkrechte Strich (das Pipe-Symbol, | ) trennt in dieser Zeichenfolge die Felder. Die Filterzeichenfolge wird vom Dialogfeld dazu verwendet, Dateinamenendungen zu filtern; *.txt bedeutet, dass nur Dateien mit der .txt-Endung im Dialogfeld erscheinen werden. Nachfolgende Dateitypen lassen sich mit dem senkrechten Strich abtrennen. FilterIndex lässt sich verwenden, um den Standardfilter, der vorausgewählt ist, anzugeben. InitialDirectory ist eine Zeichenfolge, die das Anfangsverzeichnis angibt, in dem das

Dialogfeld mit der Anzeige beginnt. Eine Reihe von Eigenschaften fordern den Benutzer unter bestimmten Bedingungen zu Eingaben auf. Setzt man die CheckPathExists-Eigenschaft in beiden Steuerelementen auf true, dann gibt das Dialogfeld eine Warnung aus, falls der aktuell angegebene Pfad nicht existiert. Die CheckFileExists-Eigenschaft des Dialogfeldes OpenFileDialog erledigt die gleiche Aufgabe hinsichtlich einer bestimmten Datei. CreatePrompt und OverwritePrompt im SaveFileDialog ähneln einander; beim ersten wird der Benutzer gefragt, ob eine neue Datei angelegt werden soll, falls sie noch nicht existiert, und das zweite verlangt vom Benutzer eine Entscheidung, ob eine vorhandene Datei überschrieben werden soll.

Farben und Schriftarten wählen Das Steuerelement ColorDialog ist eines der am leichtesten zu erlernenden Dialog-Steuerelemente. Es besitzt nur wenige Eigenschaften, die Sie kennen müssen. Listing 7.5 zeigt ein Beispiel für den Gebrauch dieses Steuerelements beim Einstellen der Hintergrundfarbe für Ihr Formular. Listing 7.5: Die Verwendung des Steuerelements ColorDialog 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

250

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day7 { public class Listing75 : Form { public Button btColor = new Button(); public Listing75() { btColor.Text = "Farbe ändern!"; btColor.Location = new Point(100,100); btColor.Size = new Size(100,25); btColor.Click += new EventHandler(this.ChangeColor);

Standarddialogfelder

15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:

this.Controls.Add(btColor); } public void ChangeColor(Object Sender, EventArgs e) { ColorDialog dlgColor = new ColorDialog(); if (dlgColor.ShowDialog() == DialogResult.OK) { this.BackColor = dlgColor.Color; } } public static void Main() { Application.Run(new Listing75()); } } }

Dieses Listing zeigt eine einfache Anwendung mit nur einer Schaltfläche, die das Standarddialogfeld für die Farbauswahl anzeigt. Wählt der Benutzer eine Farbe in ColorDialog aus, wird diese in der Eigenschaft ColorDialog.Color abgelegt. In Zeile 22 nehmen wir diese Farbe und weisen sie dem Wert BackColor des Hauptformulars zu. Abbildung 7.7 zeigt das Ergebnis dieser Anwendung und das Dialogfeld ColorDialog.

Abbildung 7.7: Das Steuerelement ColorDialog ist ein leicht zu bedienendes Dialogfeld.

251

Mit Dialogfeldern arbeiten

Das ist schon alles, was Sie für den Einsatz von ColorDialog benötigen. Das Steuerelement hat noch weitere Funktionen, mit denen der Benutzer mehr Farben als die dargestellten auswählen kann. Wenn Sie AllowFullOpen auf true setzen, kann der Benutzer ein zusätzliches Fenster öffnen (indem er auf die Schaltfläche FARBEN DEFINIEREN klickt, die sich am unteren Rand des Dialogfeldes in Abbildung 7.7 befindet), um eigene Farben zu definieren, die er aus allen Farben auswählt, die ihm das jeweilige Betriebssystem anbietet. (Wenn Sie diese Funktion nicht gestatten wollen, setzen Sie AllowFullOpen auf false und setzen auch die Eigenschaft FullOpen, die die Schaltfläche für benutzerdefinierte Farben anzeigt, auf false.) Nachdem der Benutzer einige selbst definierte Farben ausgewählt hat, können Sie die Eigenschaft CustomColors dazu verwenden, um ein Array von Color-Objekten zurückzugeben. Das Schriftartdialogfeld FontDialog ist ein wenig vielschichtiger, denn es gibt ja eine Vielzahl unterschiedlicher Schriftstile. Die Farbe Blau ist nun mal blau, aber die Schriftart Arial kann Arial fett, kursiv oder Standard sein, und als Größen sind beispielsweise 10, 20 oder 36 Punkt möglich. Und schließlich gibt es noch unterschiedliche Schriftstile wie etwa Skript (Nicht-OEM- und ANSI-Zeichensätze), Vektorschriften, vertikale Schriften und so weiter. Wenn Sie nichts von diesen Details halten, können Sie einfach die Eigenschaft FontDialog.Font verwenden, um die vom Benutzer ausgewählte Schrift zu zurückzugeben – das haben Sie sicherlich schon vermutet: if (dlgFont.DialogResult == DialogResult.OK) { this.Font = dlgFont.Font; }

Das ist wesentlich einfacher als jedes Mal, wenn man die Schriftart ändern möchte, ein neues Font-Objekt erstellen zu müssen! Abbildung 7.8 zeigt das Standarddialogfeld für die Schriftartauswahl.

Abbildung 7.8: Das Standarddialogfeld FontDialog bietet zahlreiche Auswahlmöglichkeiten an.

252

Standarddialogfelder

Alle Felder im Schriftartdialogfeld (Unterstreichen, Größe, Zeichensätze usw.) lassen sich anpassen. Mit AllowScriptChange kann der Benutzer den Standardzeichensatz ändern, während ScriptsOnly nur Skriptschriften anzeigt. AllowSimulations, AllowVectorFonts und AllowVerticalFonts gestatten die Verwendung der angegebenen Schriftarttypen (sie sind per Vorgabe alle auf true gesetzt). FixedPitchOnly erlaubt nur die Anzeige von Schriftarten mit festem Zeichenabstand (z.B. Courier). Mit ShowColor kann der Benutzer die Farbe seiner Schrift ändern (die ColorEigenschaft gibt diese Farbe zurück) und ShowEffects zeigt die Ergebnisse der Unterstrei-

chungsoptionen. Sie können schließlich noch die Eigenschaften MaxSize und MinSize einstellen, damit der Benutzer nur im Bereich zwischen einer maximalen und minimalen Schriftgröße auswählen kann. Die Einstellung dieser Werte auf null versetzt das System in die Lage, diese Werte zu steuern – es wird normalerweise die komplette Bandbreite der Größen erlauben.

Drucken Die letzte Reihe von Standarddialogfeldern, die wir betrachten, befasst sich mit dem Seitendruck: PageSetupDialog, PrintDialog und PrintPreviewDialog. Sie sollten allen vertraut sein, die schon einmal Dokumente in Windows gedruckt haben. PageSetupDialog gestattet die Randeinstellung für ein Dokument, das Ändern der Ausrichtung (Hoch- oder Querformat ), das Ändern des eingestellten Papierformats usw.

Bevor Sie jedoch einen PageSetupDialog-Dialogfeld verwenden können, müssen Sie Ihre Einstellungen auf etwas anwenden können. Dies wird durch das Erstellen eines PageSettings-Objekts bewerkstelligt, woraufhin es der PageSettings-Eigenschaft von PageSetupDialog zugewiesen wird. Sie gibt an, auf welche Objekte diese Einstellungen angewendet werden sollen. Listing 7.6 zeigt eine Beispielanwendung, die genau dies tut. Listing 7.6: Das Einrichten einer Seite 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

using using using using

System; System.Windows.Forms; System.Drawing; System.Drawing.Printing;

namespace TYWinForms.Day7 { public class Listing76 : Form { private PageSettings objPageSettings; private Button btShowDialog = new Button(); public Listing76() {

253

Mit Dialogfeldern arbeiten

12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:

btShowDialog.Text = "Seiteneinstellungen"; btShowDialog.Click += new EventHandler(this.ShowSettings); this.Controls.Add(btShowDialog); } private void ShowSettings(Object Sender, EventArgs e) { if (objPageSettings == null) { objPageSettings = new PageSettings(); } PageSetupDialog dlgSettings = new PageSetupDialog(); dlgSettings.PageSettings = objPageSettings; dlgSettings.ShowDialog(); } public static void Main() { Application.Run(new Listing76()); } } }

Die Verwaltung von Seiteneigenschaften kann etwas kompliziert sein. Erstellen Sie hierzu zunächst ein Objekt System.Drawing.Printing.PageSettings (siehe Zeile 8; beachten Sie das neue using-Statement in Zeile 4). Dieses Objekt wird im Rest Ihrer Anwendung verwendet, daher ist es nötig, ein der ganzen Klasse zur Verfügung stehendes Objekt zu erstellen. In der ShowSettings-Methode in Zeile 18 müssen Sie dieses PageSettings-Objekt überprüfen. Ist es null (oder Nothing in VB .NET), sind diesem Dokument noch keine Eigenschaften zugewiesen worden, so dass Sie dieses Objekt (in Zeile 20) instantiieren müssen. Ist es aber nicht null, wurde das PageSettings-Objekt bereits instantiiert und es wurden Eigenschaften zugewiesen; daher wollen wir es wieder und wieder verwenden. Zeile 23 erzeugt einen neuen PageSetupDialog. In Zeile 24 wird das PageSettingsObjekt (ob neu oder bereits vorhanden) der PageSettings-Eigenschaft zugewiesen. Nun können die vom Benutzer gewählten Einstellungen gespeichert und nach Bedarf auf das Dokument angewendet werden. Die Methode ShowDialog wird in Zeile 25 aufgerufen; beachten Sie, dass Sie ihren DialogResult-Wert nicht prüfen müssen. Der Grund: Der PageSetupDialog kümmert sich selbst darum, die Einstellungen anzuwenden, je nachdem, welche Schaltfläche der Benutzer angeklickt hat. Uns kümmert es daher nicht, ob der Benutzer OK oder ABBRECHEN angeklickt hat, denn der PageSetupDialog handhabt dies intern.

254

Standarddialogfelder

Kompilieren Sie diese Anwendung und führen Sie sie aus. (Wie Sie noch von Tag 2 wissen, befindet sich der Namensraum System.Drawing.Printing in der Assembly System.drawing.dll, so dass Sie beim Kompilieren keine zusätzlichen Referenzen hinzufügen müssen.) Klicken Sie auf die Schaltfläche SEITE EINRICHTEN und ändern Sie einige Seiteneigenschaften. Schließen Sie den Dialog und öffnen ihn erneut mit der Schaltfläche. Beachten Sie, dass die vorgenommenen Änderungen sich immer noch im PageSetupDialog zeigen. Der Grund dafür ist, dass die vorgenommenen Änderungen dem PageSettingsObjekt zugewiesen wurden, welches immer wieder verwendet wird. Abbildung 7.9 zeigt das Ergebnis dieser Anwendung.

Abbildung 7.9: PageSetupDialog stellt ein Dialogfeld zur Seiteneinrichtung zur Verfügung.

Wie die anderen Standarddialogfelder, die wir heute besprochen haben, weist auch PageSetupDialog einige Eigenschaften auf, mit denen Sie das Dialogfeld anpassen können. AllowMargins, AllowOrientation, AllowPaper und AllowPrinter zeigen die Fenster an, in denen der Benutzer jeweils Ränder, Seitenausrichtung (hoch/quer), Papiergröße und Druckeinstellungen ändern kann. Wird die AllowPrinter-Einstellung geändert, benötigen Sie ein PrinterSettings-Objekt, doch dies werden wir an Tag 11 bezüglich Datei-E/A besprechen. MinMargins gibt den geringsten Seitenrand an, den Ihre Anwendung zulässt. Document gibt das PrintDocument-Objekt an, von dem man Seiteneinstellungen bezieht. Das PrintDialog-Steuerelement ist einfacher zu handhaben. Ähnlich wie der PrintSetupDialog enthält es die Eigenschaften Document und PrinterSettings, um die Einstellungen zu überwachen. Damit dieses Steuerelement funktioniert, müssen Sie der PrinterSettings-Eigenschaft ein PrinterSettings-Objekt zuweisen. Doch anders als im PageSetupDialog sollte man die DialogResult-Eigenschaft prüfen, sobald das Dialogfeld geöffnet wird. Abbildung 7.10 zeigt ein typisches DRUCKEN-Dialogfeld.

255

Mit Dialogfeldern arbeiten

Abbildung 7.10: Das Steuerelement PrintDialog stellt ein nützliches Dialogfeld für das Drucken zur Verfügung.

Das letzte Standarddialogfeld, das wir heute erörtern, ist das PrintPreviewDialog-Objekt. Es unterscheidet sich von den anderen Steuerelementen, denn anders als die anderen Standarddialogfelder erbt es nicht von der CommonDialog-Klasse. Es sieht nicht einmal wie ein Standarddialogfeld aus. Auch sein DialogResult muss nicht überwacht werden. Wir brauchen uns beim PrintPreviewDialog-Steuerelement nur um drei Eigenschaften zu kümmern. Document, die erste, gibt das Dokument für die Vorschau an. Doch Vorsicht: Diese Eigenschaft akzeptiert nicht jedes x-beliebige Dokument; sie ist sehr wählerisch. Sie müssen ihr ein PrintDocument-Objekt zuweisen, welches wiederum das Dokument repräsentiert, dessen Vorschau man sehen will. Wir werden dieses Objekt näher an Tag 11 besprechen. Die nächsten beiden Eigenschaften sind einfach. PrintPreviewControl gibt natürlich das Steuerelement PrintPreviewControl im PrintPreviewDialog-Fenster zurück. Dieses Steuerelement zeigt die eigentliche Vorschau an, denn der Rest der Schaltflächen und Steuerelemente von PrintPreviewDialog besteht aus separaten Elementen. Wenn Sie Ihr eigenes benutzerdefiniertes Druckvorschau-Dialogfeld erstellen wollen, werden Sie höchstwahrscheinlich das Steuerelement PrintPreviewDialog dafür verwenden. UseAntiAlias gibt an, ob das Vorschau-Dokument mit Antialiasing behandelt wird, das

heißt, ob die Kanten des Dokuments (genauer: der verwendeten Schriftzeichen) geglättet werden sollen, so dass keine Treppchen oder harten Kanten auftauchen. Das Antialiasing lässt die Darstellung weicher und angenehmer aussehen, natürlich auf Kosten der Schärfe. Abbildung 7.11 zeigt das Steuerelement PrintPreviewDialog in Aktion.

256

Zusammenfassung

Abbildung 7.11: Sie können das Dialogfenster PrintPreviewDialog

dazu verwenden, eine Seitenvorschau eines Dokuments zu erhalten.

7.5

Zusammenfassung

Wie Sie heute erfahren haben, können Dialogfelder weitaus vielschichtiger und funktionsreicher sein als die einfache MessageBox, die Sie bislang verwendet haben. Dialogfelder werden für das Beschaffen von Benutzereingaben eingesetzt, ob das nun ein einfaches Ja oder Nein ist oder eine Auswahl unter Schriftarten und Farben. Zunächst muss man sich merken, dass Dialogfelder modal oder nichtmodal sein können. Ein modaler Dialog hindert den Benutzer daran, zur Hauptanwendung umzuschalten, es sei denn, er antwortet zuerst auf das Dialogfeld. Nichtmodale Dialoge hingegen erlauben das Wechseln zwischen Dialogfeld und Hauptanwendung, doch können sie zu Problemen führen, da man mehr als ein Formular auf einmal steuern muss. Verwenden Sie daher nichtmodale Dialogfelder nur, wenn Sie dem Benutzer lediglich Informationen (z.B. im Hilfe-Fenster) anzeigen müssen, ohne zugleich Informationen einzuholen. Das Ihnen vertrauteste Dialogfeld ist die MessageBox (Meldungsfeld). Dieses Steuerelement besitzt nur eine Methode namens Show, die bis zu sieben Parameter übernehmen kann, um z.B. festzulegen, welches Symbol im Meldungsfeld erscheint, welche Schaltflächen verfügbar sind sowie die Titelzeile und der Text, die anzuzeigen sind. Wenn Sie mehr Funktionen benötigen, als die einfache MessageBox bereitstellt, müssen Sie Ihre eigenen Dialogfeldklassen erstellen. Man erstellt diese Klassen genau wie StandardForm-Klassen; man erbt von der Klasse System.Windows.Forms.Form und stellt Konstruktoren

257

Mit Dialogfeldern arbeiten

und Ereignishandler bereit. Sie können Werte Ihres benutzerdefinierten Dialogfeldes gegenüber dem Hauptformular offen legen, indem Sie (mit Hilfe der Get- und Set-Statements) public-Eigenschaften erzeugen. Ihre Dialogfelder können Werte zurückgeben, die angeben, auf welche Schaltfläche (wie etwa OK oder ABBRECHEN) der Benutzer zum Schließen des Dialogfeldes geklickt hat. Diese Werte befinden sich in der DialogResult-Aufzählung. Das übergeordnete Formular kann auf diese DialogResult-Werte hin prüfen, um herauszufinden, was der Benutzer mit dem Dialogfeld getan hat. Sie haben auch von der Vielzahl der Standarddialogfeld-Steuerelemente erfahren, die .NET bereitstellt. Diese Steuerelemente kapseln gebräuchliche Windows-Funktionen wie etwa die Auswahl von Schriftarten oder Farben, das Drucken und die Druckvorschau, um nur einige zu nennen. Die Themen der nächsten Woche befassen sich mit der Erhöhung des Funktionsumfangs Ihrer Anwendung, so etwa dem Hinzufügen einer Datenbank und von Grafikfähigkeiten. Vergessen Sie nicht das Bonusprojekt »Eine Textverarbeitung erstellen« am Ende dieser Woche anzusehen, bevor Sie fortfahren.

7.6 F

Fragen und Antworten

Worin besteht der Unterschied zwischen den heute erstellten Steuerelementen und öffentlichen Variablen? A

Mit beiden können Sie Daten von einer Klasse an eine andere übergeben. In Listing 7.1 hätten wir beispielsweise die Eigenschaft mit der folgenden Codezeile deklarieren können: public property Caption as String

Und solange Sie die Eigenschaft tbCaption.Text dieser Variable zuweisen, wäre die Funktionalität die gleiche. Der Unterschied in der Verwendung von Eigenschaft und öffentlichen Variablen besteht darin, dass Sie mit der Eigenschaft zusätzlichen Code in den get- und set-Statements ausführen können. Das erweist sich als nützlich, wenn Sie etwa Werte verifizieren müssen, bevor Sie sie zurückgeben: public property Caption as String Get if tbCaption.Text "" then return tbCaption.Text else return "Hier befindet sich nichts!"

258

Workshop

end if End Get End Property

Mit einer öffentlichen Variablen lässt sich keine zusätzliche Funktion ausführen. F

Wie bringe ich das Dokument dazu, in meinem Steuerelement PrintPreviewDialog zu erscheinen? A

7.7

Die Antwort erfordert Wissen über eine neue Klasse namens PrintDocument und neue Fertigkeiten, die Sie noch nicht erworben haben. Um nähere Informationen hierzu zu erhalten, lesen Sie bitte Tag 11.

Workshop

Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wahr oder falsch? Jedes Form-Objekt kann ein Dialogfeld sein. 2. Wie macht man ein Dialogfeld modal bzw. nichtmodal? 3. Wahr oder falsch? Sie können ein MessageBox-Objekt direkt instantiieren. 4. Wie heißen die sieben Parameter, die die MessageBox.Show-Methode übernehmen kann? (Führen Sie nur ihre Typen mit einer kurzen Beschreibung auf.) 5. Welche zwei Vorteile erzielen Sie, wenn Sie in Ihrer benutzerdefinierten Dialogklasse einen DialogResult-Wert zuweisen? 6. Ist das folgende Codestück korrekt? Falls nicht, nennen Sie die Fehler. public property IsClicked as string Get return blnClicked end property

7. Welche zwei Mitglieder haben fast alle Standarddialogfeld-Steuerelemente gemeinsam? Nennen Sie die Ausnahme(n). 8. Schreiben Sie eine Filterzeichenfolge für ein OpenFileDialog-Steuerelement, das Textdateien (*.txt), Bilddateien (*.gif) und Alle Dateien (*.*)anzeigt.

259

Mit Dialogfeldern arbeiten

9. Nennen Sie die Haupteigenschaften (also die Eigenschaften, die Sie am meisten interessieren, wenn der Benutzer eine Auswahl trifft) für die Steuerelemente OpenFileDialog, SaveFileDialog, ColorDialog und FontDialog.

Übung Erstellen Sie in C# eine voll funktionsfähige Anwendung mit Menüs, welche alle Standarddialogfeld-Steuerelemente verwendet. Setzen Sie auch ein nichtmodales Dialogfeld ein, um kontextabhängige Hilfeinformationen anzuzeigen. Wenn sich ein Benutzer von einem Menüelement zum nächsten bewegt, sollte sich der Inhalt dieses Dialogfeldes ändern. (Tipp: Verwenden Sie das MenuItem.Select-Ereignis, um festzustellen, über welchem Menüelement sich die Maus gerade befindet.)

260

Woche 1 – Rückblick Projekt 1: Eine Textverarbeitung erstellen Herzlichen Glückwunsch zum Überstehen der ersten Woche! Sie dürften nun mit dem Schreiben von Windows Forms-Anwendungen in C# und Visual Basic .NET vertraut sein. Ihnen dürften auch die Schwierigkeiten beim Umgang mit Ereignissen und Windows Forms-Steuerelementen bewusst sein. Mit dem bislang erworbenen Wissen sind Sie bereit, praktisch jede einfache Anwendung anzupacken, aber auch einige der vielschichtigeren. Als eine Form der Rekapitulation der vergangenen sieben Tage wird dieses Rückblickkapitel (sowie diejenigen der Wochen 2 und 3) Sie durch den Vorgang der Erstellung Ihres eigenen Textverarbeitungsprogramms führen, und zwar eines Programms, das es mit Microsoft Word aufnehmen könnte. In dieser Lektion legen Sie erst einmal das Fundament für diese Anwendung; das Bonusprojekt am Ende der zweiten Woche wird Ein- und Ausgabefunktionen für Daten und Dateien hinzufügen. Das Projekt 3 wird einige fortgeschrittenere Leistungsmerkmale integrieren. Es wird empfohlen, dass Sie diese Projekte durcharbeiten. Sie machen Sie besser mit Windows Forms-Konzepten vertraut, da Sie Situationen aus der Praxis handhaben werden.

Über das Textverarbeitungsprojekt Dieses Projekt soll Ihnen Gelegenheit geben, an einer Anwendung in jeder Phase ihrer Erstellung zu arbeiten. In dieser Lektion fangen Sie erst einmal mit einem einfachen Entwurf an, installieren die grundlegenden Funktionen und erweitern diese um peppige Zusatzelemente. Dieses Projekt dient auch dazu, Ihre Fähigkeiten zu stählen, was die Entwicklung von Windows Forms-Anwendungen anbelangt. So können Sie sich später in Ihren anderen Projekten sicherer fühlen. Am Ende der zweiten bzw. dritten Woche werden Sie zu diesem Projekt zurückkehren, um die Ergebnisse der weiteren Lektionen zu integrieren. Diese Anwendung, die wir NetWord taufen wollen, wird es dem Benutzer gestatten, Dokumente zu bearbeiten und zu speichern, Suchoperationen aus zuführen, Text zu formatieren und schließlich Dokumente zu drucken und im Web zu surfen. In dieser Lektion jedoch werden wir lediglich das grundlegende Gerüst sowie die Benutzeroberfläche erstellen.

261

Woche 1 – Rückblick

Was Sie benötigen Der wichtigste Teil jedes Textverarbeitungsprogramms ist natürlich die Hauptbedienoberfläche: der Textbereich, in dem der Benutzer Dokumente tippen und bearbeiten kann. Dreimal dürfen Sie raten, wie wir diese Komponente erstellen. Ich hoffe, Sie haben auf das Steuerelement RichTextBox getippt. Bereits an Tag 6 haben wir gesehen, dass dieses Steuerelement beinahe alle Funktionen eines Textverarbeitungsprogramms ausführen kann, vom Formatieren eines Textes bis zur Durchführung einer Suchoperation. Das Einrichten des RichTextBox-Steuerelements bedeutet jedoch nur den halben Sieg. Man muss auch Menüs hinzufügen, mit denen der Benutzer zum Beispiel Dateien öffnen und speichern, Text formatieren und Farben auswählen kann. Die Menüs sowie das RichTextBox-Steuerelement stellen in diesem Projekt die Hauptteile der Anwendung dar. Um die Anwendung noch bedienungsfreundlicher zu machen, sind noch weitere Steuerelemente nötig, so etwa eine Statusleiste und Kontextmenüs. Sie werden im Abschnitt »Einige nützliche Dinge mit Pfiff hinzufügen« weiter unten beschrieben. Obwohl die meisten Textverarbeitungsprogramme MDI-Anwendungen (Multiple Document Interface, Mehrfachdokumentschnittstelle) sind – sie können mehr als ein Dokument zugleich öffnen –, wird unsere erst einmal nur ein einzelnes Dokument anzeigen können. Auf MDI-Anwendungen kommen wir an Tag 10 zurück.

Die Benutzeroberfläche aufbauen Listing R1.1 zeigt das grundlegende Gerüst für unsere Anwendung. Listing R1.1: Die NetWord-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:

262

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.P1 { public class NetWord : Form { public NetWord() { this.Text = "NetWord v.1"; this.Size = new Size(800,600); } public static void Main() {

Woche 1 – Rückblick

13: 14: 15: 16:

Application.Run(new NetWord()); } } }

Speichern Sie dieses Listing unter dem Namen NetWord.cs. Wie Sie sehen, weist der Code außer einer Main-Methode und einem einfachen Konstruktor kaum etwas Bemerkenswertes auf. Das Kompilieren und Ausführen dieser Anwendung wird zu einer leeren Applikation wie in Abbildung R1.1 führen. Wir wollen als Nächstes die Benutzeroberfläche erstellen. Wir müssen das RichTextControl-Steuerelement hinzufügen und alle anderen Steuerelemente, die damit verknüpft sind (wie etwa Menüs). Werfen Sie einen Blick auf Listing R1.2.

Abbildung R1.1: Die bescheidenen Anfänge von NetWord

Listing R1.2: Die Document-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:

using using using using using

System; System.Windows.Forms; System.Drawing; System.Drawing.Printing; System.ComponentModel;

namespace TYWinforms.P1 { public class NetWord : Form { private RichTextBox rtbDocument = new RichTextBox(); private MainMenu mnuMain = new MainMenu(); private MenuItem mniFile = new MenuItem("Datei");

263

Woche 1 – Rückblick

13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58:

264

private private private private private private

MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem

mniOpen = new MenuItem("Öffnen..."); mniSave = new MenuItem("Speichern"); mniPageSetup = new MenuItem("Seite einrichten..."); mniPrintPreview = new MenuItem ("Druckvorschau"); mniPrint = new MenuItem("Drucken..."); mniExit = new MenuItem("Beenden");

private private private private private private

MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem

mniEdit = new MenuItem("Bearbeiten"); mniUndo = new MenuItem("Rückgängig"); mniCut = new MenuItem("Ausschneiden"); mniCopy = new MenuItem("Kopieren"); mniPaste = new MenuItem("Einfügen"); mniFind = new MenuItem("Suchen...");

private MenuItem mniFormat = new MenuItem("Format"); private MenuItem mniFont = new MenuItem("Schriftart..."); private private private private private private private private private private

Font fntCurrent = new Font("Times New Roman", 10); Color fntColor = Color.Black; FontDialog dlgFont = new FontDialog(); OpenFileDialog dlgOpen = new OpenFileDialog(); SaveFileDialog dlgSave = new SaveFileDialog(); PrintPreviewDialog dlgPrintPreview = new PrintPreviewDialog(); PageSetupDialog dlgPageSetup = new PageSetupDialog(); PrintDialog dlgPrint = new PrintDialog(); PageSettings objPageSettings = new PageSettings(); PrinterSettings objPrintSettings = new PrinterSettings();

public NetWord() { rtbDocument.Dock = DockStyle.Fill; rtbDocument.Font = fntCurrent; rtbDocument.HideSelection = false; mniOpen.Click += new EventHandler(this.FileClicked); mniSave.Click += new EventHandler(this.FileClicked); mniPageSetup.Click += new EventHandler (this.FileClicked); mniPrintPreview.Click += new EventHandler (this.FileClicked); mniPrint.Click += new EventHandler(this.FileClicked); mniExit.Click += new EventHandler(this.FileClicked); mniUndo.Click += new EventHandler(this.EditClicked); mniCut.Click += new EventHandler(this.EditClicked); mniCopy.Click += new EventHandler(this.EditClicked); mniPaste.Click += new EventHandler(this.EditClicked); mniFind.Click += new EventHandler(this.EditClicked);

Woche 1 – Rückblick

59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91:

mniFont.Click += new EventHandler (this.FormatClicked); mnuMain.MenuItems.Add(mniFile); mnuMain.MenuItems.Add(mniEdit); mnuMain.MenuItems.Add(mniFormat); mniFile.MenuItems.Add(mniOpen); mniFile.MenuItems.Add(mniSave); mniFile.MenuItems.Add("-"); mniFile.MenuItems.Add(mniPageSetup); mniFile.MenuItems.Add(mniPrintPreview); mniFile.MenuItems.Add(mniPrint); mniFile.MenuItems.Add("-"); mniFile.MenuItems.Add(mniExit); mniEdit.MenuItems.Add(mniUndo); mniEdit.MenuItems.Add("-"); mniEdit.MenuItems.Add(mniCut); mniEdit.MenuItems.Add(mniCopy); mniEdit.MenuItems.Add(mniPaste); mniEdit.MenuItems.Add("-"); mniEdit.MenuItems.Add(mniFind); mniFormat.MenuItems.Add(mniFont); this.Text = "NetWord v.1"; this.Name = "NetWord"; this.Size = new Size(800,600); this.Menu = mnuMain; this.Closing += new CancelEventHandler(this.DocumentClosing); this.Controls.Add(rtbDocument); }

Das gesamte Listing besteht lediglich aus dem Konstruktor und der Initialisierung unserer Anwendung. Inzwischen wissen Sie, dass der Großteil des von uns geschriebenen Codes (zumindest in der ersten Woche) aus der Initialisierung der Benutzeroberfläche besteht. Diesem Code kann man sehr leicht folgen. Zeile 9 erzeugt das RichTextBoxSteuerelement, das für die Hauptbenutzeroberfläche benötigt wird. Die Zeilen 11 bis 28 richten die diversen Menüs ein und in Zeile 30 bis 39 werden Einstellungen deklariert, die durchweg verwendet werden (die aktuelle Schriftart, Farbe, Druckeinstellung usw.). Die Zeilen 42 bis 90 richten einige allgemeine Eigenschaften ein und weisen den Menüelementen Delegaten zu.

265

Woche 1 – Rückblick

Es gibt hierzu noch einiges anzumerken. Erstens setzt Zeile 44 HideSelection auf false, was dazu führt, dass markierter Text im RichTextBox-Steuerelement auch dann noch als markiert zu sehen ist, wenn dieses Steuerelement nicht den Fokus hat (das wird bei der Erstellung der Suchfunktion wichtig). Zweitens weist Zeile 88 dem Closing-Ereignis des Formulars einen Delegaten zu. Wir wollen dieses Ereignis überwachen, um herauszufinden, ob sich der Inhalt vor dem Schließen des Fensters geändert hat; falls dem so ist, wollen wir dem Benutzer Gelegenheit zum Speichern seines Dokumentes geben. Wir wollen nun einen Blick auf die Ereignishandler werfen, die in Listing R1.3 zu sehen sind. Listing R1.3: Die Ereignishandler für NetWord 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:

266

private void FileClicked(Object Sender, EventArgs e) { MenuItem mniTemp = (MenuItem)Sender; switch (mniTemp.Text) { case "Öffnen...": if (dlgOpen.ShowDialog() == DialogResult.OK) { //Datei öffnen } break; case "Speichern": if (dlgSave.ShowDialog() == DialogResult.OK) { //Datei speichern } break; case "Seite einrichten...": dlgPageSetup.PageSettings = objPageSettings; dlgPageSetup.ShowDialog(); break; case "Druckvorschau": dlgPrintPreview.Document = new PrintDocument(); dlgPrintPreview.ShowDialog(); break; case "Drucken...": dlgPrint.PrinterSettings = new PrinterSettings(); if (dlgPrint.ShowDialog() == DialogResult.OK) { //drucken

Woche 1 – Rückblick

32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77:

} break; case "Beenden": this.Close(); break; } } private void EditClicked(Object Sender, EventArgs e) { MenuItem mniTemp = (MenuItem)Sender; switch (mniTemp.Text) { case "Rückgängig": rtbDocument.Undo(); break; case "Ausschneiden": if (rtbDocument.SelectedRtf != "") { rtbDocument.Cut(); } break; case "Kopieren": if (rtbDocument.SelectedRtf != "") { rtbDocument.Copy(); } break; case "Einfügen": rtbDocument.Paste(); break; case "Suchen...": FindDialog dlgFind = new FindDialog(rtbDocument); dlgFind.Show(); break; } } private void FormatClicked(Object Sender, EventArgs e) { MenuItem mniTemp = (MenuItem)Sender; switch (mniTemp.Text) { case "Schriftart...": dlgFont.ShowColor = true; if (dlgFont.ShowDialog() == DialogResult.OK) { fntCurrent = dlgFont.Font; fntColor = dlgFont.Color; rtbDocument.SelectionFont = fntCurrent; rtbDocument.SelectionColor = fntColor;

267

Woche 1 – Rückblick

78: } 79: break; 80: } 81: } 82: 83: private void DocumentClosing(Object Sender, CancelEventArgs e) { 84: if (rtbDocument.Modified) { 85: DialogResult dr = MessageBox.Show("Möchten Sie die Änderungen  speichern?", this.Name, MessageBoxButtons.YesNoCancel); 86: 87: if (dr == DialogResult.Yes) { 88: MessageBox.Show("Schade!", this.Name); 89: } else if (dr == DialogResult.Cancel) { 90: e.Cancel = true; 91: } 92: } 93: } 94: 95: public static void Main() { 96: Application.Run(new NetWord()); 97: } 98: } 99: }

Nach unseren Arbeiten an Tag 4 dürfte Ihnen ein Großteil dieses Codes bekannt vorkommen. Sie finden in Zeile 1 die Methode, die alle MenuItems im DATEI-Menü behandelt, den Ereignishandler für das BEARBEITEN-Menü in Zeile 40, denjenigen für das FORMAT-Menü in Zeile 67 und den für das Closing-Ereignis schließlich in Zeile 83. Der Löwenanteil dieses Codes wird verwendet, um genau auszuwerten, welches Menüelement angeklickt wurde. Die Zeilen 4 bis 36 dürften vertraut erscheinen: Sie sind fast identisch mit der Dialogfeld-Lektion an Tag 7. Der einzige Unterschied: Es wurde ein BEENDENMenü hinzugefügt, das die Close-Methode aufruft. Je nachdem, welches Menü angeklickt wurde, erzeugen Sie einen FileOpenDialog, einen FileSaveDialog, einen PageSetupDialog, einen PrintPreviewDialog oder einen PrintDialog. Mit Hilfe der ShowDialog-Methode zeigen Sie das jeweilige Dialogfeld an. Bei den ÖFFNEN-, SPEICHERN- und DRUCKEN-Menüelementen bewerten Sie das Ergebnis des Dialogfelds mit Hilfe der DialogResult-Aufzählung und führen eine dementsprechende Aktion aus. Die Zeilen 43 bis 63 sind sehr ähnlich und führen die Arbeitsschritte RÜCKGÄNGIG, AUSSCHNEIDEN, KOPIEREN und EINFÜGEN aus, indem sie die verschiedenen Methoden des RichTextBox-Steuerelements aufrufen. Bei den Cutund Copy-Methoden müssen Sie zunächst prüfen, ob Text markiert ist, indem

268

Woche 1 – Rückblick

Sie die Eigenschaft SelectedRtf bewerten. Das SUCHEN-Menüelement in Zeile 60 ist etwas Besonderes; es erzeugt ein benutzerdefiniertes Dialogfeld, das Sie im nächsten Abschnitt erstellen werden, und zeigt es an. Die Zeilen 70 bis 78 verhalten sich wie die anderen Fälle. Man zeigt dem Benutzer das Dialogfeld FontDialog an, um ihm die Auswahl einer Schriftart und einer Schriftfarbe zu gestatten. Diese Werte werden in den globalen Einstellungen gespeichert, die Sie in den Zeilen 30 und 31 erzeugt haben, und dann der Schriftart im RichTextBox-Steuerelement zugewiesen. Schließlich findet sich in Zeile 83 noch die DocumentClosing-Methode. Sie erinnern sich, dass das Closing-Ereignis direkt vor dem Schließen des Dokuments stattfindet, was bedeutet, dass man es zur Bewertung aller in letzter Minute geänderten Bedingungen einsetzen kann. Die Zeile 84 prüft die Modified-Eigenschaft der RichTextBox, die Ihnen verrät, ob das Dokument seit dem letzten Speichervorgang (oder in diesem Fall, seit dem letzten Öffnen) geändert wurde. Zeile 85 zeigt eine MessageBox an, die den Benutzer fragt, ob er ohne Speichern der letzten Änderungen fortfahren möchte. Je nach der Antwort tun Sie eines von drei Dingen: 왘

Sie speichern das Dokument nicht.



Sie halten die Anwendung vom Beenden ab (indem Sie die Eigenschaft CancelEventArgs.Cancel auf true setzen).



Sie speichern das Dokument (momentan nicht wirklich, aber wenigstens wissen Sie, wohin der entsprechende Code gehört).

Das ist bereits alles – wenn auch nicht für die Anwendung. Wenn Sie versuchen, diesen Code auszuführen, erhalten Sie eine Fehlermeldung, denn Sie haben bis jetzt noch nicht das Objekt FindDialog erstellt, das von Zeile 61 gefordert wird. Diese Klasse ist in Listing R1.4 zu sehen. Listing R1.4: Die FindDialog-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.P1 { public class FindDialog : Form { private Label lblFind = new Label(); private Label lblReplace = new Label(); private TextBox tbFind = new TextBox(); private TextBox tbReplace = new TextBox(); private Button btFind = new Button();

269

Woche 1 – Rückblick

12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57:

270

private Button btReplace = new Button(); private Button btReplaceAll = new Button(); private Button btCancel = new Button(); private RichTextBox rtbDoc; public FindDialog(RichTextBox rtbParentDoc): base() { rtbDoc = rtbParentDoc; lblFind.Text = "Suche: "; lblFind.Location = new Point(10,10); lblFind.Width = 50; lblReplace.Text = "Ersetze: "; lblReplace.Location = new Point(10,35); lblReplace.Width = 50; tbFind.Width = 100; tbFind.Location = new Point(60,10); tbReplace.Width = 100; tbReplace.Location = new Point(60,35); btFind.Size = new Size(75,25); btFind.Text = "Suchen"; btFind.Location = new Point(170,10); btFind.Click += new EventHandler(this.ButtonClicked); btReplace.Size = new Size(75,25); btReplace.Text = "Ersetzen"; btReplace.Location = new Point(170,35); btReplace.Click += new EventHandler(this.ButtonClicked); btReplaceAll.Size = new Size(75,25); btReplaceAll.Text = "Alle ersetzen"; btReplaceAll.Location = new Point(170,60); btReplaceAll.Click += new EventHandler(this.ButtonClicked); btCancel.Size = new Size(75,25); btCancel.Text = "Abbrechen"; btCancel.Location = new Point(170,85); btCancel.Click += new EventHandler(this.ButtonClicked); this.Text = "Suchen"; this.Size = new Size(255,140); this.MaximizeBox = false;

Woche 1 – Rückblick

58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101:

this.MinimizeBox = false; this.FormBorderStyle = FormBorderStyle.FixedDialog; this.TopMost = true; this.Controls.Add(lblFind); this.Controls.Add(lblReplace); this.Controls.Add(tbFind); this.Controls.Add(tbReplace); this.Controls.Add(btFind); this.Controls.Add(btReplace); this.Controls.Add(btReplaceAll); this.Controls.Add(btCancel); } private void ButtonClicked(Object Sender, EventArgs e) { Button btTemp = (Button)Sender; int intLocation; int intCount = 0; switch (btTemp.Text) { case "Suchen": intLocation = rtbDoc.Find(tbFind.Text, rtbDoc.SelectionStart + rtbDoc.SelectionLength, RichTextBoxFinds.None); if (intLocation != -1) { rtbDoc.SelectionStart = intLocation; rtbDoc.SelectionLength = tbFind.Text.Length; rtbDoc.Focus(); } break; case "Ersetzen": intLocation = rtbDoc.Find(tbFind.Text, rtbDoc.SelectionStart + rtbDoc.SelectionLength, RichTextBoxFinds.None); if (intLocation != -1) { rtbDoc.SelectionStart = intLocation; rtbDoc.SelectionLength = tbFind.Text.Length; rtbDoc.SelectedText = tbReplace.Text; rtbDoc.SelectionStart = intLocation; rtbDoc.SelectionLength = tbReplace.Text.Length; } break; case "Alle ersetzen": intLocation = rtbDoc.Find(tbFind.Text); while (intLocation != -1) { rtbDoc.SelectionStart = intLocation; rtbDoc.SelectionLength = tbFind.Text.Length; rtbDoc.SelectedText = tbReplace.Text;

271

Woche 1 – Rückblick

102: 103:

intCount += 1; intLocation = rtbDoc.Find(tbFind.Text, rtbDoc.SelectionStart + rtbDoc.SelectionLength, RichTextBoxFinds.None); } MessageBox.Show(intCount.ToString() + "Vorkommen ersetzt","Suchen"); break; case "Abbrechen": this.Close(); break; } }

104: 105: 106: 107: 108: 109: 110: 111: 112: 113:

} }

Dieses Listing definiert ein benutzerdefiniertes Dialogfeld, das über SUCHENund ERSETZEN-Textfelder sowie Schaltflächen für SUCHEN, ERSETZEN und ALLE ERSETZEN verfügt. Der Code dafür mag ein wenig komplizierter aussehen als in den bisherigen Arbeitsschritten, doch ist er nicht wirklich schwierig. Die Zeilen 1 bis 70 enthalten den Konstruktor- und Initialisierungscode. Hier gibt es nichts Neues, lediglich ein paar Dinge, die zu beachten wären. Erstens erfolgt die Deklaration des RichTextBox-Steuerelements in Zeile 16; diesem Steuerelement wird dann dasjenige Steuerelement zugewiesen, das an den Konstruktor in Zeile 18 übergeben wurde. Wenn wir in Listing P 1.3 zur Zeile 61 zurückgehen, sehen wir, dass wir an den Konstruktor der FindDialog-Klasse das Haupt-RichTextBox-Steuerelement übergeben. Dies wird so gehandhabt, damit Sie beim Suchen von Ihrem SUCHEN-Dialogfeld aus eine einfache Referenz auf dieses Steuerelement haben. Wie man es einsetzt, sehen Sie gleich. Als Nächstes finden Sie im Konstruktor in Zeile 18 einen Aufruf der baseMethode. Das bedeutet, dass der Konstruktor in Zeile 18 vom Konstruktor des RichTextBox-Steuerelements erben sollte und dass alle notwendigen Initialisierungsprozeduren ausgeführt werden. Da .NET dies für Sie tut, ist es im Grunde nicht nötig, dies hier einzufügen, aber es macht es für Sie deutlicher. ButtonClicked in Zeile 72 verwendet ein switch-Statement, um die angeklickte Schaltfläche zu bewerten. In Zeile 79 stoßen Sie auf die Find-Methode, die, wie Sie wissen, bestimmten Text in der RichTextBox sucht. Doch sie weist ein paar zusätzliche Parameter auf: tbFind.Text ist der zu suchende Text, rtbDoc.SelectionStart bezeichnet die Startposition für den Suchvorgang im Text (also die aktuelle Cursorposition in der RichTextBox) und RichTextBoxFinds.None gibt an, wie die Suche durchgeführt werden soll. Die Aufzählung RichTextBoxFinds enthält die Werte MatchCase (Groß-/Kleinschreibung beachten), NoHighlight

272

Woche 1 – Rückblick

(keine Markierung), None, Reverse (umgekehrt) und WholeWord (nach ganzem Wort suchen). Diese Werte lassen sich wie Anchor-Werte kombinieren: RichTextBoxFinds.MatchCase | RichTextBoxFinds.Reverse

Mit Hilfe von RichTextBoxFinds kann der Benutzer spezialisierte Suchvorgänge starten, aber im Augenblick brauchen wir uns nicht darum zu kümmern. Falls die Find-Methode den gesuchten Text nicht findet, gibt sie den Wert –1 zurück. Wir prüfen dies in Zeile 80 und falls Text gefunden wurde, markieren wir ihn mit Hilfe der Eigenschaften SelectionStart und SelectionLength. Am Schluss geben Sie der RichTextBox den Fokus zurück. (Doch weil wir in Zeile 60 für die FindDialog-Klasse TopMost auf true gesetzt haben, wird das Dialogfeld nicht verschwinden, aber die Benutzereingaben gelangen trotzdem zum RichTextBox-Steuerelement.) Bei den case-Zweigen für Suchen und Ersetzen startet die Suche hinter der aktuellen Cursorposition (SelectionStart + SelectionLength). Wird etwas Text markiert, beginnt sie direkt hinter der Markierung. Sie können dieses Verhalten durch Umstellen des zweiten Parameters der Find-Methode auf die Zahl 1 ändern oder indem Sie einfach die zweiten zwei Parameter weglassen, wie es der case-Zweig »Alle ersetzen« in Zeile 97 tut.

Abbildung R1.2: NetWord in Aktion

Die nächsten zwei Fälle, ERSETZEN und ALLE ERSETZEN, funktionieren in ähnlicher Weise. ERSETZEN verwendet die Find-Methode, markiert den angegebenen Text und ersetzt ihn mit Hilfe der SelectedText-Eigenschaft. Daraufhin markiert es das gerade ersetzte Wort. ALLE ERSETZEN tut das Gleiche, nur in einer while-Schleife, so dass man

273

Woche 1 – Rückblick

alle Vorkommen statt nur eines ersetzen kann. Durch jede Iteration der Schleife suchen Sie den angegebenen Text, markieren und ersetzen ihn, woraufhin es zum nächsten Auftreten des gesuchten Textes geht – und so weiter, bis es keine Vorkommen davon mehr gibt (also bis Find –1 zurückgibt). Anschließend wird eine MessageBox angezeigt, um dem Benutzer mitzuteilen, wie viele Textvorkommen ersetzt wurden. Ihre Anwendung ist endlich vollständig. Erledigen Sie Kompilierung und Ausführung und erfreuen Sie sich an den Früchten Ihrer Arbeit. Abbildung R1.2 zeigt das Ergebnis dieser Anwendung.

Einige nützliche Dinge mit Pfiff hinzufügen Ihre Anwendung mag ja vollständig sein, doch sie ist nicht sonderlich benutzerfreundlich. Wir wollen mit ein paar Extrafunktionen NetWord etwas mehr Leben einhauchen. Zunächst fügen wir Ihren Menüelementen Tastenkombinationen (Shortcuts) hinzu. Fügen Sie den folgenden Code in den NetWord-Konstruktor ein: mniOpen.Shortcut = Shortcut.CtrlO; mniSave.Shortcut = Shortcut.CtrlS; mniPrint.Shortcut = Shortcut.CtrlP; mniUndo.Shortcut = Shortcut.CtrlZ; mniCut.Shortcut = Shortcut.CtrlX; mniCopy.Shortcut = Shortcut.CtrlC; mniPaste.Shortcut = Shortcut.CtrlV; mniFind.Shortcut = Shortcut.CtrlF; mniOpen.ShowShortcut = true; mniSave.ShowShortcut = true; mniPrint.ShowShortcut = true; mniUndo.ShowShortcut = true; mniCut.ShowShortcut = true; mniCopy.ShowShortcut = true; mniPaste.ShowShortcut = true; mniFind.ShowShortcut = true;

Die von uns zugewiesenen Tastenkombinationen entsprechen den Standardabkürzungstasten von Windows für die betreffenden Menüoptionen. Als nächsten Schritt erstellen wir ein Kontextmenü, das auf den bereits erzeugten Menüelementen (MenuItem-Steuerelementen) basiert. Je nachdem, was der Benutzer ausgewählt hat, werden unterschiedliche Optionen erscheinen. Das Erstellen des Kontextmenüs ist einfach, doch die Bestimmung dessen, was darin erscheinen soll, ist ein wenig kniffliger. Werfen Sie einen Blick auf folgenden Code:

274

Woche 1 – Rückblick

'in der Klasse private ContextMenu cmnuDocument = new ContextMenu(); ... 'im Konstruktor cmnuDocument.MenuItems.Add(mniCut.CloneMenu()); cmnuDocument.MenuItems.Add(mniCopy.CloneMenu()); cmnuDocument.MenuItems.Add(mniPaste.CloneMenu()); cmnuDocument.MenuItems.Add("-"); cmnuDocument.MenuItems.Add(mniFont.CloneMenu()); cmnuDocument.Popup += new EventHandler(this.HandleContext); rtbDocument.ContextMenu = cmnuDocument; ... 'eine neue Methode private void HandleContext(Object Sender,EventArgs e) { if (rtbDocument.SelectionLength == 0){ cmnuDocument.MenuItems [0 ].Enabled = false; cmnuDocument.MenuItems [1 ].Enabled = false; } else { cmnuDocument.MenuItems [0 ].Enabled = true; cmnuDocument.MenuItems [1 ].Enabled = true; } }

Die erste Zeile erstellt das ContextMenu-Objekt und die nächsten Zeilen weisen ihm diverse Menüelemente zu (mit dem Kontextmenü soll der Benutzer nur ausschneiden, kopieren, einfügen und die Schriftart ändern können). Beachten Sie, dass Sie die CloneMenu-Methode verwenden, um Kopien der fraglichen Menüelemente anzufertigen. Tun Sie dies nicht, dann richten Sie in Ihren Standard-Menüelementen ein Chaos an. Sie müssen das Kontextmenü dem Kontext der Anwendung anpassen (also dem, was der Benutzer gerade tut). Dies erlaubt Ihnen das Popup-Ereignis; es wird direkt vor der Anzeige des Menüs ausgelöst. Wir wollen uns die HandleContext-Methode ansehen, die das Popup-Ereignis handhabt. Es gilt zwei einfache Fälle zu bewerten. Ist in der RichTextBox kein Text markiert, dann sollte man die Menüs AUSSCHNEIDEN und KOPIEREN abschalten (sie sind dann nämlich nutzlos). Dies erfolgt einfach durch das Setzen der Enabled-Eigenschaft auf false – die Menüelemente sind dann zwar noch zu sehen, doch der Benutzer kann sie nicht auswählen. Ist jedoch Text ausgewählt, dann sollte man dafür sorgen, dass diese zwei Menüelemente aktiviert sind; sie sind das zwar per Voreinstellung, doch wenn beim Öffnen des Kontextmenüs kein Text markiert ist, werden sie ausgeschaltet. Mit Hilfe der else-Bedingung aktivieren Sie sie einfach wieder. Lassen Sie uns zum Schluss eine Statusleiste hinzufügen, die dem Benutzer Informationen liefert, etwa die aktuelle Zeilennummer oder die aktuelle Spalte in der Zeile, wo sich der Cursor befindet. Wir brauchen also eine Statuszeile (StatusBar) und einen Statuszeilenbereich (StatusBarPanel).

275

Woche 1 – Rückblick

private StatusBar sbarMain = new StatusBar(); private StatusBarPanel spnlLine = new StatusBarPanel();

Als Nächstes initialisieren Sie diese Steuerelemente im Konstruktor: sbarMain.ShowPanels = true; sbarMain.Panels.Add(spnlLine); spnlLine.AutoSize = StatusBarPanelAutoSize.Spring; spnlLine.Alignment = HorizontalAlignment.Right;

Nun ist Ihre Statusleiste eingerichtet, doch leider macht sie noch nichts. Da Sie die aktuelle Zeile und Spalte überwachen wollen, müssen Sie die Statusleiste bei jeder Änderung in der RichTextBox aktualisieren. Hierfür eignet sich das SelectionChanged-Ereignis hervorragend; es wird bei jeder Veränderung der Cursorposition ausgelöst (durch Tippen, Mausklicks, Drücken der (¢)-Taste usw.). Fügen Sie diesem Ereignis der RichTextBox einen Delegaten hinzu: rtbDocument.SelectionChanged += new EventHandler(this.UpdateStatus);

Der erste Teil der UpdateStatus-Methode kümmert sich um das Auffinden der Zeilennummer. Das ist ein recht einfacher Vorgang und lässt sich mit dem folgenden Code bewerkstelligen: private void UpdateStatus(Object Sender,EventArgs e) { String text = rtbDocument.Text; int intLine = rtbDocument.GetLineFromCharIndex(rtbDocument.SelectionStart) + 1;

Zuerst holen Sie den in der RichTextBox enthaltenen Text, um ihn leichter bearbeiten zu können. Das RichTextBox-Steuerelement verfügt über eine elegante Methode namens GetLineFromCharIndex, die Ihnen verrät, in welcher Zeile sich der Cursor gerade befindet. Dies liefert Ihnen die richtige Zeilennummer selbst dann, wenn es keine Zeilenumbrüche gibt. Wenn etwa die erste Zeile im Steuerelement über zwei Zeilen verläuft, werden diese beiden Zeilen 1 und 2 zurückgeben. Die Lines-Eigenschaft der RichTextBox lässt sich andererseits nicht zum Zeilenzählen verwenden, weil sie keine Zeilenumbrüche berücksichtigt (sie zählt zwei umbrochene Zeilen als eine Zeile). Die Zeichenposition bzw. Spaltennummer in der aktuellen Zeile zu ermitteln, ist ein wenig schwieriger. Um festzustellen, ob man sich am Zeilenanfang befindet, kann man die GetPositionFromCharIndex-Methode der RichTextBox verwenden, welche einen Point mit den Cursorkoordinaten zurückgeben wird. Ist die x-Koordinate gleich 1, dann befinden Sie sich am Rand ganz links außen oder, mit anderen Worten, am Zeilenanfang. Das folgende Codestück führt diese Prozedur aus: int intLine = rtbDocument.SelectionStart; while (rtbDocument.GetPositionFromCharIndex(i).X > 1) --i; String StrChar = (rtbDocument.SelectionStart – i + 1).ToString();

276

Woche 1 – Rückblick

Zuerst setzen wir die Variable i auf die aktuelle Cursorposition. In einer while-Schleife verringern wir i, solange die x-Koordinate für den Point bei Zeichen i größer als 1 ist. Schließlich subtrahieren wir i von unserer Ausgangsposition, um festzustellen, wie weit die Ausgangsposition vom Zeilenanfang entfernt ist. Weisen Sie zum Schluss die sich ergebende Zeichenfolge dem Statusleistenbereich zwecks Ausgabe zu: spnlLine.Text = "Zeile " + intLine.ToString() + " Spalte " + strChar;

Abbildung R1.3 zeigt das Ergebnis. Die Projekte in Woche 2 und 3 werden diese Anwendung erweitern. Am Ende der Woche 2 werden Sie Internetfähigkeiten hinzufügen und NetWord in eine MDI-Anwendung umwandeln. Danach lernen Sie, wie Sie Ihre Anwendung installieren und sie mit anderen Windows-Anwendungen zusammenarbeiten lassen.

Abbildung R1.3: In der Statusleiste werden nun die aktuelle Zeilennummer und Cursorposition in der Zeile korrekt angezeigt.

277

Tag 1

Mit Windows Forms beginnen

25

Tag 2

Windows Forms-Anwendungen erstellen

45

Tag 3

Mit Windows Forms arbeiten

77

Tag 4

Menüs und Symbolleisten

111

Tag 5

Ereignisse in Windows Forms

151

Tag 6

Windows Forms mit Steuerelementen erweitern

177

Tag 7

Mit Dialogfeldern arbeiten

231

Tag 8

Datenbindung in Windows Forms

281

T ag 9

ADO.NET einsetzen

309

Tag 10

MDI-Anwendungen erstellen

349

Tag 11

Arbeiten mit der Windows-Ein-/Ausgabe

377

Tag 12

Formulare Internet-fähig machen

415

Tag 13

Grafische Anwendungen mit GDI+

439

Tag 14

ActiveX

477

Tag 15

Webdienste

515

Tag 16

Windows-Dienste

539

Tag 17

Fortgeschrittene Techniken für Steuerelemente in Windows Forms

565

Benutzerdefinierte Steuerelemente in Windows Forms

589

Tag 19

Multithreading-Anwendungen

621

Tag 20

Windows Forms konfigurieren und bereitstellen

655

Tag 21

Debugging und Profiling

681

Tag 18

W O C H E

W O C H E

W O C H E

Woche 2: Funktionen hinzufügen Willkommen zu Ihrer zweiten Woche in der Gesellschaft von Windows Forms! In der vorigen Woche wurden Sie in die verschiedenen Konzepte eingeführt, die bei Windows Forms erforderlich sind. Sie erfuhren, worum es sich bei Windows Forms handelt, wie man sie erstellt und wie man Windows Forms-Steuerelemente verwendet. Nun, da Sie über die Grundlagen verfügen, ist es an der Zeit, zu fortgeschritteneren Themen überzugehen. In dieser Woche lernen Sie, wie Sie Ihren Anwendungen leistungsfähige Funktionen hinzufügen, darunter Datenbankfähigkeiten und viele andere moderne Merkmale. In der dritten Woche werden Sie dann das komplexe Innenleben von Windows Forms kennen lernen und erfahren, wie Sie es nutzen können, um richtig ausgefuchste Anwendungen zu schreiben. Der Tag 8 mit dem Motto »Datenbindung in Windows Forms« lehrt Sie, wie Windows Forms entworfen sind, um mit allen Datentypen umgehen zu können, ohne auf Datenbanken zugreifen zu müssen. Diese Daten können Sie auf jede erdenkliche Weise manipulieren. An Tag 9 mit dem Titel »ADO.NET« lernen Sie, wie Sie anhand des Wissens aus Tag 8 Datenbanken in Ihre Anwendungen integrieren. Tag 10 (»MDI-Anwendungen«) konzentriert sich auf Anwendungen mit der Mehrfachdokumentschnittstelle (Multiple Document Interface), kurz MDI. Mit diesem Applikationstyp können Sie mehrere Fenster zugleich geöffnet halten. »Arbeiten mit der Windows-Ein-/Ausgabe« ist das Motto des elften Tages. Danach beherrschen Sie die Interaktion mit Windows-Dateien – also Speichern, Öffnen, Überschreiben usw. – sowie das Drucken. Wie Sie das Internet in Ihren Anwendungen nutzen, lernen Sie an Tag 12, »Formulare Internet-fähig machen«. Sie erstellen einen einfachen Webbrowser und surfen nach Herzenslust. An Tag 13 unter dem Motto »Grafische Anwendungen mit GDI+« erfahren Sie etwas über GDI+, das Paradigma für die Handhabung von Windows-Grafikaspekten. Sie erlernen nicht nur das Erstellen von »zeichnenden« Anwendungen, sondern auch, auf welche Weise sich GDI+ auf Ihre normalen Anwendungen auswirkt. Tag 14 trägt den Titel »ActiveX«, so dass Sie nun lernen, wie Sie andere Anwendungen und Komponenten mit Ihren eigenen Windows Forms-Anwendungen integrieren! Lassen Sie uns in die zweite Woche starten!

Datenbindung in Windows Forms

8

Datenbindung in Windows Forms

Die Datenbindung ist ein interessanter Begriff in .NET; mit ihrer Hilfe können Sie Daten mit den Steuerelementen in Windows Forms verknüpfen. Diese Fähigkeit erlaubt es Ihnen wiederum, diese Daten leichter anzuzeigen und zu bearbeiten. Nach der heutigen Lektion werden Sie verstehen, dass Datenbindung ein leistungsfähiger Partner sein kann. Heute lernen Sie, 쐽

was man unter Datenbindung versteht,



wie Sie Daten mit Ihren Steuerelementen verknüpfen,



was ein DataGrid ist,



wie Sie feststellen, welche Daten ein Benutzer in einem Steuerelement ausgewählt hat.

8.1

Einführung in die Datenbindung

Das Konzept der Datenbindung zu verstehen, ist manchmal nicht einfach, daher wollen wir ein paar Augenblicke dafür aufbringen. Lassen Sie uns zunächst den Begriff Daten erörtern. In Bezug auf die Datenbindung sind Daten beliebige Arten von Informationen, mit denen eine Anwendung arbeitet (oftmals als Datenmodell bezeichnet). Es könnte sich also um die in Ihrer Anwendung verwendeten Farben, um die Kundenliste in einer Datenbank oder um die Text-Eigenschaft Ihres Formulars handeln. Alle diese Beispiele stehen für Informationen, die Sie in Ihren Anwendungen verarbeiten. In den meisten Fällen will man solche Daten auch einem Benutzer präsentieren. Wenn Sie etwa einen CD-Katalog erstellen, muss der Benutzer seine eigenen Eingaben sehen können. Sie können eine solche Möglichkeit hinzufügen, indem Sie den Text-Eigenschaften diverser Steuerelemente Werte zuweisen, Kombinationsfelder und Listenfelder hinzufügen usw. Die Definition von Daten ist recht unscharf; doch eine exaktere Erklärung ist schwierig, da Daten in so vielfältiger Form vorkommen. Das ist einer der Gründe, warum Datenbindung so wichtig und leistungsfähig ist. Denn einfach ausgedrückt, erlaubt Ihnen Datenbindung, Informationen in einem Windows Forms-Steuerelement zusammenzuführen. Diese Bindung stellt eine Lese-/Schreibbeziehung zwischen den Daten und dem Steuerelement her; das Steuerelement zeigt die Daten in einer benutzerfreundlichen Weise an, und Änderungen im Steuerelement spiegeln sich in den Daten wider. Durch die Datenbindung eines Steuerelements machen Sie es praktisch zum Vehikel für die Anzeige und Bearbeitung von Daten.

282

Einführung in die Datenbindung

Inwiefern unterscheidet sich also Datenbindung von der Wertezuweisung in einem Steuerelement? Für die einfache Datenbindung besteht kaum ein Unterschied. Doch wenn Sie einmal mit komplexer Datenbindung anfangen, erspart sie Ihnen viel Mühe, wenn Sie eine Benutzeroberfläche erstellen, wobei zugleich Ihr Code schlanker und einfacher wird. Stellen Sie sich ein typisches Szenario vor, etwa den erwähnten CD-Katalog. Gibt der Benutzer Informationen zu einem neuen Album ein, lassen sich diese direkt in einem TreeView-Steuerelement anzeigen (siehe Tag 6). Dieser Prozess ist zwar einfach und schnell, doch leider verlieren Sie auf diese Weise Funktionalität. Erstens verfügen Sie über keine besonders gute Aufzeichnung der Daten, außer im TreeView-Steuerelement, welches nicht gerade der optimale Speicherort ist. Und zweitens bleiben die Daten nach der Anzeige auch weiterhin im Steuerelement. Außer durch lange und komplizierte Prozeduren lassen sie sich dort nicht mehr bearbeiten. Statt die Informationen direkt anzuzeigen, können Sie sie auch in eine Datenbank eingeben, sobald der Benutzer sie eingetippt hat. Sie verfügen dann über eine Datenaufzeichnung, die Sie nach Wunsch bearbeiten und in so vielen Steuerelementen anzeigen können, wie Sie wollen, statt in einem einzigen TreeView-Steuerelement. Innerhalb dieses Rahmens verfügen Sie nun über zwei verschiedene Objekte: die Daten und die Steuerelemente, um diese Daten anzuzeigen. Die Daten können Sie an das Steuerelement binden und das Steuerelement realisiert dann alle Aspekte der Anzeige für Sie. Die Interaktion mit dem Steuerelement berührt die Daten direkt. Dieses zweite Modell bietet mehrere Vorteile. Sie erzielen eine Trennung der Anwendungsdaten von der Benutzeroberfläche (die jeweils als die Daten- und die Präsentationsbzw. Benutzeroberflächenschicht bezeichnet werden). Haben Sie sich bereits einmal über Anwendungsentwicklungsmodelle informiert, kennen Sie schon das Dreischichtenmodell: eine Schicht für die Präsentation (in der Benutzeroberfläche), eine für die Daten und eine (in der Mitte) für die Behandlung der Logik (Verarbeitungsregeln) und der Beziehungen zwischen den Daten und der Benutzeroberfläche (User Interface, UI). Durch diese Aufteilung wird Ihre Anwendung weitaus eleganter und anspruchsvoller. Dieses Modell lässt sich einfacher erstellen und verstehen. Sollte außerdem eine der Schichten veraltet oder überflüssig werden, können Sie sie leicht austauschen, ohne Ihre gesamte Anwendung komplett neu oder umschreiben zu müssen. Die Aufteilung in Benutzeroberfläche und Datenebene sollte stets zu Ihren Zielen bei der Anwendungsentwicklung gehören. Datenbindung ist ein ausgezeichnetes Hilfsmittel bei diesem Unterfangen.

Daten aus jeder beliebigen Quelle Daten können in vielfältiger Form auftreten und in den verschiedensten Transportformen vorkommen. Ob Ihre Daten nun aus einer Datenbank stammen oder willkürlich von einem Benutzer eingegeben werden, ist gleichgültig, denn beide können an Windows

283

Datenbindung in Windows Forms

Forms-Steuerelemente gebunden werden. Diese immense Flexibilität erspart Ihnen als Entwickler eine Menge Arbeit bei der Umwandlung von Datentypen und -formen. Sie können einfach die Daten anbinden und dann gehen. XML-Daten werden beispielsweise fast wie solche aus einer relationalen Datenbank behandelt. Es ist gleichgültig, ob einige Daten aus einer Textdatei stammen und die übrigen aus Oracle oder SQL Server – beide Datentypen lassen sich für die Anzeige leicht an Steuerelemente binden. Änderungen im Steuerelement lassen sich in die XML-Datei oder die Datenbank zurückschreiben, so dass man nie Informationen verliert. Wenn Sie morgen etwas über ADO.NET lernen, werden Sie verstehen, wie diese Lese-/Schreibverbindung bei Datenbanken funktioniert.

8.2

Einfache Datenbindung

Wie bereits erwähnt brauchen Sie für die Datenbindung ein paar Daten und ein Steuerelement, um sie daran zu binden. Bei den Daten ist lediglich darauf zu achten, dass sie in einer Form vorliegen, die die IList-Schnittstelle implementiert. Schnittstellen sind ja lediglich wie eine Anforderungsliste: Sie teilen Ihnen mit, was eine Klasse alles benötigt, sagen Ihnen aber nicht, wie Sie das »Zeug« erstellen sollen. Die IList-Schnittstelle definiert die Anforderungen für jedes Objekt, das Daten enthalten kann. Über 40 Objekte verwenden IList, angefangen bei Arrays und StringCollection-Auflistungen bis hin zu etwas selteneren Objekten wie etwa EventDecriptorCollection – für gewöhnlich haben Sie keine Mühe, ein IList verwendendes Objekt zu finden. Um Daten zu binden, fügen Sie der DataBindings-Auflistung ein neues Binding-Objekt hinzu. Dieser Vorgang verläuft genau so wie das Hinzufügen neuer Steuerelemente zur Controls-Auflistung eines Formulars. Alle Windows Forms-Steuerelemente verfügen über diese Auflistung, können also alle Datenbindung nutzen. Ein Blick auf die Syntax: steuerelement.DataBindings.Add("eigenschaft", objekt, "feld")

Steuerelement ist dabei natürlich das Steuerelement, für das Sie eine Datenbindung einrichten wollen. Der erste Parameter namens eigenschaft gibt an, an welche Eigenschaft des Steuerelements Sie Daten binden wollen. Das kann je nachdem variieren, welche Daten Sie anbinden wollen. So könnten Sie etwa eine Namensliste an die Text-Eigenschaft einer Schaltfläche binden, aber eine Farbenliste an die BackColor-Eigenschaft. Datenbindung ist flexibel genug für beides. Der zweite Parameter gibt die datenquelle an. Das kann ein Array, eine Datenbank, eine StringCollection usw. sein. Der dritte Parameter gibt an, welcher Teil – oder welches »Feld« – der Datenquelle gebunden werden soll. Für ein Array ergibt diese Information wenig Sinn, denn es enthält nur willkürliche Daten. Bei einer Datenbank könnten Sie aber etwa das Feld Name einer Tabelle binden. In diesem dritten Parameter würden Sie also »Name« angeben.

284

Einfache Datenbindung

In den nächsten Abschnitten lernen Sie konkretere Beispiele für Datenbindung kennen und erfahren, wie man gebundene Daten bearbeitet.

Eine Schaltfläche mit Daten verbinden Anhand eines simplen Beispiels verwenden wir für die Datenbindung ein Array. Das Beispiel ist in Listing 8.1 zu sehen. Listing 8.1: Eine Schaltfläche mit Daten verbinden 1: 2: 3: 4: 5: 6: 7: 8:

using System; using System.Windows.Forms; using System.Drawing;

namespace TYWinforms.Day8 { public class Listing81 : Form { private Button btData = new Button(); private Color[] arrColors = {Color.Green, Color.Blue,Color.Red,  Color.Orange}; 9: 10: public Listing81() { 11: btData.Location = new Point(100,100); 12: btData.DataBindings.Add("BackColor", arrColors, ""); 13: btData.DataBindings.Add("Text", arrColors, ""); 14: 15: this.Controls.Add(btData); 16: } 17: 18: public static void Main() { 19: Application.Run(new Listing81()); 20: } 21: } 22: }

In Zeile 7 erstellen Sie das Steuerelement Button für die Datenbindung. In der Zeile darunter wird ein Array von Color-Objekten erzeugt und instantiiert: Grün, Blau, Rot und Orange. Sie haben also Ihr Steuerelement und die Daten dafür, nun können Sie sie miteinander verbinden. In Zeile 12 verwenden Sie die oben beschriebene Syntax, um das Array an die Schaltfläche zu binden. Beachten Sie die drei Parameter: Der erste gibt die Steuerelementseigenschaft für die Bindung an; da wir ein Array von ColorObjekten verwenden, binden Sie die Farben an die BackColor-Eigenschaft. Der zweite Parameter gibt die Datenquelle an: das in Zeile 8 erstellte Array. Beim

285

Datenbindung in Windows Forms

dritten Parameter handelt es sich lediglich um eine leere Zeichenfolge, da keine bestimmte Eigenschaft des Arrays gebunden werden muss (und kann); daher soll das gesamte Array angebunden werden. Sie haben nun die Farben an die Schaltfläche gebunden. Moment mal!. Zeile 13 scheint eine andere Datenbindungsprozedur aufzuweisen. Hier binden Sie das gleiche Farbenarray an die Text-Eigenschaft des Button-Steuerelements. Die CLR ruft automatisch eine ToString-Methode für die Array-Objekte auf, um die Text-Eigenschaft den Farben richtig zuzuweisen. Zum Schluss wird die Schaltfläche in Zeile 15 dem Formular hinzugefügt. Abbildung 8.1 zeigt das Ergebnis dieser Anwendung.

Abbildung 8.1: Die Datenbindung einer Eigenschaft veranlasst das Steuerelement zur Anzeige der Daten.

Was ist also geschehen? Die BackColor-Eigenschaft des Button-Steuerelements ist nun also auf die erste Farbe im Array, grün, gesetzt worden. Diese Farbe wird gesetzt, ohne dass man der BackColor-Eigenschaft manuell eine Farbe zuweisen müsste. Die Datenbindung kümmert sich selbst darum. Zweitens wird das Wort Green – das Sie sehen, wenn Sie Color.Green.ToString() aufrufen – als Beschriftung des Button-Steuerelements angezeigt. Und wieder brauchen Sie diese Eigenschaft nicht selbst zuzuweisen; das wird für Sie erledigt. Sie können noch mehr erreichen. Da das an das Button-Steuerelement gebundene Array mehr als eine Farbe enthält, können Sie alle Farben durchlaufen lassen. Zu diesem Zweck müssen Sie aber erst ein neues Objekt kennen lernen: BindingContext. In einem Steuerelement zeigt es die Beziehung zwischen Daten und der sie bindenden Steuerelementeigenschaft an. Es gibt an, welches Element in der Datenauflistung auf welche Weise angezeigt werden soll. Um seinen Gebrauch anzusehen, fügen Sie folgenden Code in das Listing 8.1 ein: //im Konstruktor btData.Click += new EventHandler(this.Cycle); ...

286

Einfache Datenbindung

//neue Methode public void Cycle(Object Sender, EventArgs e) { btData.BindingContext [arrColors ].Position += 1; }

Die erste Zeile fügt einen Ereignishandler für das Click-Ereignis hinzu. Sie benutzen dieses Ereignis, um alle Array-Elemente zu durchlaufen. Der Ereignishandler Cycle verwendet den BindingContext des Button-Steuerelements, um den gebundenen Wert zu ändern. BindingContext enthält eine Liste aller an das Steuerelement gebundenen Objekte, so dass Sie das korrekte angeben müssen; dieses referenziert das Array von Farben. Die PositionEigenschaft gibt an, welches Element gerade angezeigt wird, so dass Sie es nur um eins zu inkrementieren brauchen, um zum nächsten zu gelangen. Nun kompilieren Sie die Anwendung und klicken auf die Schaltfläche. Die Eigenschaften BackColor und Text des Button-Steuerelements ändern sich mit jedem Klick – und ohne dass Sie sich mit einer dieser Eigenschaften hätten einzeln abgeben müssen. Dieses Beispiel mag Ihnen ein wenig an den Haaren herbeigezogen erscheinen, doch wenn Sie eine Datenbank mit Tausenden von Kunden haben, können Sie sich vorstellen, wie diese einfache Datenbindungsprozedur das Entwicklerleben leichter machen kann. Zudem spiegeln sich alle im Array vorgenommenen Änderungen auch in der Schaltfläche wider. Sie könnten in Ihrer Anwendung etwa Folgendes hinzufügen: arrColors[2] = Color.Magenta

Die Schaltfläche würde sich dem ohne weiteres anpassen. Einfache Datenbindung ist also nicht schwer.

Das Bearbeiten gebundener Daten Wir wollen uns ein etwas anspruchsvolleres Beispiel ansehen, bei dem ein Kombinationsfeld verwendet wird, mit dem der Benutzer Elemente bearbeiten kann. Mit Kombinationsfeldern und anderen Listenanzeige-Steuerelementen gestaltet sich der Umgang mit den Daten etwas einfacher. Statt Datenbindungen zu entwerfen, können Sie einfach die DataSource-Eigenschaft einstellen, um die Standardeigenschaft des Steuerelements an die Standardeigenschaft der betreffenden Daten zu binden. Im Beispiel des Listings 8.2 präsentiert die Anwendung zwei Steuerelemente: ein Kombinationsfeld (ComboBox) zur Datenanzeige und eine Schaltfläche für die Datenbearbeitung. Wählt der Benutzer eine Farbe aus, kann er die Auswahl wieder ändern, indem er einen neuen Farbenwert in das Textfeld der ComboBox eintippt.

287

Datenbindung in Windows Forms

Listing 8.2: Komplexe Datenbindung 1: 2: 3: 4: 5: 6: 7: 8: 9:

using System; using System.Windows.Forms; using System.Drawing;

namespace TYWinforms.Day8 { public class Listing82 : Form { private ComboBox cboData = new ComboBox(); private Button btChange = new Button(); private Color[] arrColors = {Color.Green, Color.Blue, Color.Red,  Color.Orange}; 10: private int intLastIndex; 11: 12: public Listing82() { 13: cboData.Location = new Point(50,75); 14: cboData.DropDownStyle = ComboBoxStyle.Simple; 15: cboData.DataBindings.Add("BackColor", arrColors, ""); 16: cboData.DataSource = arrColors; 17: cboData.SelectedIndexChanged += new EventHandler(this.ChangeColor); 18: 19: btChange.Location = new Point(175,75); 20: btChange.Text = "Ändern"; 21: btChange.Click += new EventHandler(this.EditColor); 22: 23: this.Controls.Add(cboData); 24: this.Controls.Add(btChange); 25: } 26: 27: public void ChangeColor(Object Sender, EventArgs e) { 28: cboData.BindingContext[arrColors].Position = cboData.SelectedIndex; 29: intLastIndex = cboData.SelectedIndex; 30: } 31: 32: public void EditColor(Object Sender, EventArgs e) { 33: if (Color.FromName(cboData.Text).IsKnownColor) { 34: arrColors[intLastIndex] = Color.FromName (cboData.Text); 35: 36: cboData.SelectedText = Color.FromName (cboData.Text).ToString(); 37: cboData.SelectedIndex = intLastIndex; 38: } 39: } 40: 41: public static void Main() { 42: Application.Run(new Listing82()); 43: } 44: } 45: }

288

Einfache Datenbindung

Der Anfang dieses Listings ähnelt dem von Listing 8.1. In Zeile 9 erzeugen Sie ein Farben-Array und eine Schaltfläche in Zeile 8. Doch binden Sie diesmal keine Daten an die Schaltfläche, sondern vielmehr an die ComboBox (Zeile 7). In Zeile 16 setzen Sie die DataSource-Eigenschaft auf das Farben-Array. Das ist alles, was Sie für das Datenbinden tun müssen. Wenn Sie dieses Steuerelement ansehen, entsprechen die Listenelemente den Farben im Array (und der angezeigte Text entspricht der Textdarstellung dieser Farben). Um das Steuerelement etwas zu beleben, binden Sie auch die BackColor-Eigenschaft der ComboBox wie beim Button-Steuerelement in Listing 8.1. Sobald sich der ausgewählte Index ändert, soll sich auch die Farbe des Kombinationsfeldes ändern, daher fügen Sie dem Ereignis SelectedIndexChange einen Ereignishandler hinzu. In den Zeilen 19 bis 21 richten Sie das Button-Steuerelement ein. Der Benutzer wird diese Schaltfläche zur Bestätigung seiner Datenänderungen in der ComboBox verwenden. Klickt er auf die Schaltfläche, modifizieren Sie die Daten und aktualisieren nach Bedarf auch das Kombinationsfeld. Die ChangeColor-Methode – der Ereignishandler für das SelectedIndexChangedEreignis – ist sehr einfach. Mit Hilfe der BindingContext-Eigenschaft der ComboBox setzen Sie die Position auf den gerade ausgewählten Index. Als Nächstes speichern Sie den Index für den späteren Gebrauch; sobald der Benutzer etwas in das Kombinationsfeld eintippt, verliert das gerade ausgewählte seine Markierung. Das führt zu Problemen, wenn Sie versuchen Elemente zu ändern, so dass Sie eine exakte Aufzeichnung des zuletzt ausgewählten Elements bereithalten müssen. In der EditColor-Methode spielen sich die wirklich interessanten Dinge ab. Als Erstes ändern Sie das Array; Sie übernehmen die neue vom Benutzer in das Kombinationsfeld eingegebene Farbe, rufen das damit verknüpfte Color-Objekt mit Hilfe der ColorFromName-Methode ab und weisen es dem ausgewählten Index im Array zu. Klickt der Benutzer nun wieder auf dieses Element, wechselt die Hintergrundfarbe auf den neuen Wert. Beachten Sie jedoch, dass sich der im Kombinationsfeld angezeigte Text nicht ändert, selbst wenn dies der aktuelle Farbwert tut. Der Text ändert sich nicht, weil die ComboBox über keinen Mechanismus verfügt, mit dem sie Änderungen in dem in ihrer DataSourceEigenschaft festgelegten Objekt aufspüren könnte. Daher müssen Sie einfach noch einmal den Text mit Hilfe der ColorFromName-Methode aktualisieren. Zum Schluss setzen Sie das aktuell ausgewählte Element auf das letzte vom Benutzer ausgewählte Element, welches jetzt der neue Wert ist. Kompilieren Sie Listing 8.2 und probieren Sie es aus. Abbildung 8.2 zeigt das Ergebnis.

289

Datenbindung in Windows Forms

Abbildung 8.2: Die ComboBox verwendet gebundene Daten für ihre Hintergrundfarbe.

Formulare mit Daten verbinden Sie werden häufig eine Datenquelle mit einer ganzen Anwendung verknüpfen wollen. Angenommen, Sie haben eine Applikation erstellt, die nur für die Anzeige eines CD-Katalogs verwendet wird. Die Anwendung verfügt über mehrere Steuerelemente, die jeweils mit den Aspekten einer einzelnen CD verbunden sind – eine Beschriftung für den Künstler, ein Textfeld für den Albumtitel, ein Textfeld für den laufenden Musiktitel usw. Sobald der Benutzer zum nächsten Album bzw. Datensatz weiterspringt, gibt es eine Alternative: Statt für jedes Steuerelement des Formulars den Wert BindingContext.Position ändern zu müssen, können Sie ihn einfach für das Form-Objekt ändern: 'VB .NET Me.BindingContext[object].Position += 1 //C# this.BindingContext[object].Position += 1; BindingContext ändert sich dann in jedem einzelnen Steuerelement, das im Formular

angebunden ist.

8.3

Das Steuerelement DataGrid

Das leistungsstarke DataGrid-Steuerelement wurde speziell für den Einsatz bei der Datenbindung entworfen. Es ist bei der Datenanzeige in allen unterschiedlichen Formaten effizient, dabei verfügt es über eine Vielzahl von Anzeigeoptionen, die Sie zur Anpassung einer Benutzeroberfläche verwenden können. Das DataGrid verfügt sogar über eingebaute Funktionen für das Bearbeiten, Hinzufügen und Löschen von Elementen.

290

Das Steuerelement DataGrid

Die Einrichtung des Steuerelements ist einfach: Setzen Sie seine DataSource-Eigenschaft auf die Datenquelle, und schon kann's losgehen. Listing 8.3 zeigt ein einfaches Beispiel auf der Grundlage der Listings 8.1 und 8.2. Listing 8.3: Datenbindung eines DataGrid-Steuerelements 1: 2: 3: 4: 5: 6: 7: 8:

using System; using System.Windows.Forms; using System.Drawing;

namespace TYWinforms.Day8 { public class Listing83 : Form { private DataGrid dgData = new DataGrid(); private Color[] arrColors = {Color.Green, Color.Blue, Color.Red,  Color.Orange}; 9: 10: public Listing83() { 11: dgData.DataSource = arrColors; 12: dgData.Size = new Size(800,600); 13: 14: this.Size = new Size(800,600); 15: this.Controls.Add(dgData); 16: } 17: 18: public static void Main() { 19: Application.Run(new Listing83()); 20: } 21: } 22: }

Dieser Code ist fast identisch mit dem in Listing 8.1. Der Unterschied: Statt einer ComboBox verwenden Sie hier ein DataGrid. Kompilieren Sie das Listing und betrachten Sie das Ergebnis, wie es in Abbildung 8.3 zu sehen ist. Was ist nun passiert? Sie haben dem DataGrid bloß ein Array von Farben zugewiesen, also wo kamen dann die ganzen zusätzlichen Informationen her? Die in Abbildung 8.3 zu sehenden Spalten IsEmpty, A, B, IsNamedColorIsKnownColor, Name, G, R und IsSystemColor sind alles Eigenschaften des Color-Objekts. Ungeachtet seiner Farbe verfügt jedes ColorObjekt über diese Eigenschaften. Da Sie dem DataGrid eine Sammlung von Color-Objekten übergaben, nahm es an, dass Sie alle Eigenschaften angezeigt haben wollten. Hätten Sie ihm ein Array von Zeichenfolgen übergeben, würde es einfach die Länge des Textinhalts dieser Zeichenfolgen anzeigen. Tabelle 8.1 führt die gebräuchlichsten UI-Eigenschaften des DataGrid-Steuerelements auf.

291

Datenbindung in Windows Forms

Abbildung 8.3: Das DataGrid zeigt mühelos umfangreiche Datenmengen an. Eigenschaft

Beschreibung

AlternatingBackcolor

Legt die Farbe für jede zweite anzuzeigende Zeile fest (damit der Benutzer die Zeilen besser unterscheiden kann).

BackColor

Legt fest, welche Hintergrundfarbe das Datenblatt an.

BackGroundColor

Legt fest, welche Hintergrundfarbe die Fläche außerhalb des Datenblattes hat.

BorderStyle

Gibt einen der Werte der BorderStyle-Aufzählung an.

CaptionBackColor

Legt fest, welche Hintergrundfarbe der Titelbereich hat.

CaptionFont

Legt die Schriftart für den Titelbereich fest.

CaptionForeColor

Legt die Farbe der Schriftart im Titel fest.

CaptionText

Enthält den im Titel anzuzeigenden Text.

CaptionVisible

Gibt an, ob die Titelzeile sichtbar ist

ColumnHeadersVisible

Gibt an, ob die Spaltenköpfe sichtbar sind.

FlatMode

Gibt an, ob das Datenblatt im Flachmodus angezeigt wird (dieser Modus zeigt keine sichtbaren Linien zwischen Spalten- und Zeilenüberschriften an).

ForeColor

Gibt die Schriftfarbe im Datenblatt an.

GridLineColor

Gibt die Farbe der Datenblattlinien an.

GridLineStyle

Gibt einen der Werte der DataGridLineStyle-Aufzählung an: None: keine Datenblattlinien; Solid: durchgehende Datenblattlinien.

HeaderBackColor

Legt die Hintergrundfarbe der Zeilen- und Spaltenköpfe fest.

HeaderFont

Legt die Schriftart der Spaltenköpfe fest.

HeaderForeColor

Legt die Schriftfarbe der Kopfzeile fest.

Tabelle 8.1: Eigenschaften der DataGrid-Benutzeroberfläche

292

Das Steuerelement DataGrid

Eigenschaft

Beschreibung

LinkColor

Gibt die Farbe von Links an, mit deren Hilfe man zu untergeordneten Tabellen navigieren kann (mehr dazu morgen).

LinkHoverColor

Gibt die alternative Farbe von Links an, falls der Mauszeiger darüber schwebt.

ParentRowsBackColor

Legt die Hintergrundfarbe von übergeordneten Zeilen fest.

ParentRowsForeColor

Legt die Schriftfarbe von übergeordneten Zeilen fest.

ParentRowsLabelStyle

Enthält einen Wert der DataGridParentRowsLabelStyle-Aufzählung: ColumnName: der Name der übergeordneten Spalte TableName: der Name der übergeordneten Tabelle Both: beide Namen None: weder der eine noch der andere Name

ParentRowsVisible

Gibt an, ob übergeordnete Zeilen sichtbar sind.

PreferredColumnWidth

Legt die Breite angezeigter Spalten fest.

PreferredRowHeight

Legt die Höhe angezeigter Zeilen fest.

RowHeadersVisible

Gibt an, ob Titel für Zeilen sichtbar sind.

RowHeaderWidth

Legt die Breite von Zeilentitelzellen fest.

SelectionBackColor

Legt die Hintergrundfarbe einer ausgewählten Zelle fest.

SelectionForeColor

Legt die Schriftfarbe einer ausgewählten Zelle fest.

Tabelle 8.1: Eigenschaften der DataGrid-Benutzeroberfläche (Forts.)

Ein kurzer Blick auf ADO.NET Bevor Sie die Interaktion mit dem DataGrid-Steuerelement erlernen, sollten Sie noch einiges mehr über komplexe Datenstrukturen wissen. Arrays mögen ja schön und gut sein, sind aber leider nicht ganz optimal für die Speicherung und Darstellung von Daten. Sie lassen sich nicht vollständig modifizieren, und Sie können zwar einzelne Elemente im Array ändern, sie aber nicht vollständig entfernen. Wir werfen daher einen kurzen Blick auf einige der Objekte, die Teil von ADO.NET sind, insbesondere auf DataTable und seine Begleiter. Wenn Sie die morgige Lektion beginnen, haben Sie einen Vorsprung.

293

Datenbindung in Windows Forms

Eine DataTable ist eine vollständige Darstellung von Daten in einer Tabelle. Stellen Sie es sich als zweidimensionales Array mit Namen vor. Werfen Sie einen Blick auf Tabelle 8.2, die Informationen über Namen und Handorientierung bereithält. Vorname

Nachname

Handorientierung

Walter

Saravia

Rechtshänder

Eva

Payne

Rechtshänder

Joel

Andres

Linkshänder

Tabelle 8.2: Musterdaten

Diese Tabelle weist drei Datenzeilen und drei Spalten auf, die »Vorname«, »Nachname« und »Handorientierung« heißen. Aus dieser Tabelle können Sie ersehen, dass eine Spalte einen Informationstyp darstellt und eine Zeile die eigentlichen Daten (die Zeile wird im Deutschen auch als »Datensatz« bezeichnet). Eine DataTable ist genau das Gleiche. DataRow-Objekte stellen Zeilen in der DataTable dar und DataColumn-Objekte die Datentypen. Das DataGrid ist demnach optimal für die Informationsdarstellung in einer DataTable geeignet, denn es zeigt Daten im Tabellenformat an, wie Sie noch aus Abbildung 8.3 wissen. Wir wollen lernen, wie man Daten in einer DataTable speichert; diese Prozedur wird Ihnen sehr bekannt vorkommen. Sie müssen zuerst Ihre DataTable erstellen – das geht wie bei jedem anderen Objekt: //C# DataTable objMyTable =new DataTable("Meine Tabelle "); 'VB .NET dim objMyTable as New DataTable("Meine Tabelle ")

Die in Klammern stehenden Wörter stellen den Tabellennamen dar (sie werden für das spätere leichte Auffinden gebraucht). Als Nächstes erzeugen Sie DataColumn-Objekte, die jeweils einen der zu speichernden Informationstypen darstellen, und fügen Sie Ihrer DataTable hinzu. Dieser Vorgang ähnelt dem Hinzufügen von Steuerelementen zu einem Formular: //erzeugen Sie eine Spalte für Integer DataColumn ColumnA = new DataColumn(); ColumnA.DataType = System.Type.GetType("System.Int32 "); ColumnA.ColumnName = "Spalte A "; //erzeugen Sie eine Spalte für Zeichenfolgen DataColumn ColumnB = new DataColumn(); ColumnB.DataType = System.Type.GetType("System.String "); ColumnB.ColumnName ="Spalte B ";

294

Das Steuerelement DataGrid

//fügen Sie sie der Datentabelle hinzu objMyTable.Columns.Add(ColumnA); objMyTable.Columns.Add(ColumnB);

Nun verfügen Sie über eine vollständige, wenngleich etwas leere DataTable (also eine ohne Daten). Sie wird je zwei Datenelemente pro Zeile oder »Datensatz« enthalten: einen Integerwert und einen Zeichenfolgenwert. Um Daten hinzuzufügen, integrieren Sie einfach DataRows. Das folgende Stück Code zeigt eine einfache Möglichkeit die DataTable mit Daten zu füllen, indem man eine for-Schleife verwendet: DataRow myDataRow; for (int i =0; i ClientRectangle.Width-30) { ptPosition.X = ClientRectangle.Width-30; } if (ptPosition.Y > ClientRectangle.Height-30) { ptPosition.Y = ClientRectangle.Height-30; }

Dieser Code besagt Folgendes: Wenn die Position größer als die Breite des Steuerelements minus der Breite des Auges ist, setzt man die Position auf die Breite des Steuerelements minus die Breite des Auges. So kann das Auge nie über den Rand hinausrutschen. Schließlich wollten wir noch ein benutzerdefiniertes Ereignis jedes Mal auslösen, wenn sich die Augenstellung geändert hat. Der letzte Schritt besteht also im Aufruf der OnEyeMoved-Methode (man beachte das großgeschriebene O), um das Ereignis auszulösen, und danach rufen wir die Invalidate-Methode auf: OnEyeMoved(EventArgs.Empty); Invalidate();

Unsere OnPaint-Methode sind nun so aus: protected override void OnPaint(PaintEventArgs e) { e.Graphics.Clip = regClip; e.Graphics.FillEllipse(new SolidBrush(clrBlood), ClientRectangle);

604

Benutzerdefinierte Steuerelemente

e.Graphics.FillEllipse(new SolidBrush(clrIris), new Rectangle(ptPosition.X, ptPosition.Y, 30, 30)); e.Graphics.FillEllipse(Brushes.Black, new Rectangle(ptPosition.X+5, ptPosition.Y+5, 20, 20)); }

Nun haben wir zwei neue Ellipsen. Die erste stellt die Iris dar und wird mit der Farbe clrIrisColor (die der Benutzer festlegen kann) sowie mit modifizierten ptPosition-Punktwerten gezeichnet. Die zweite Ellipse stellt die Pupille dar. Sie wird innerhalb der Iris mit einem Black-Pinsel gezeichnet.

Alles zusammensetzen Bislang produzierten wir nur eine Menge kleiner Stücke des benutzerdefinierten Steuerelements. Bevor wir sie zusammensetzen, sollten wir rekapitulieren. Zunächst haben Sie Ihre Eigenschaften mit der Syntax der Eigenschaftendeklaration erzeugt. Dann haben Sie einen Timer verwendet, um die Position des Augapfels jede Zehntelsekunde zu aktualisieren. Sie haben einen benutzerdefinierten Delegaten erzeugt und lösen ein Ereignis bei jeder Augenbewegung aus. Als Nächstes wurde die Clipping-Region angelegt (nach einer genauen Untersuchung der Handles). Die Eigenschaft »Bloodshotedness« wurde mit Hilfe einer benutzerdefinierten RGB-Farbe spezifiziert. Schließlich haben Sie die Mauskoordinaten in die geeigneten Koordinaten des Steuerelements übersetzt. Listing 18.3 zeigt den vollständigen Code – holen Sie tief Luft. Listing 18.3: Ihr benutzerdefinierter Augapfel 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

using using using using

System; System.Windows.Forms; System.Drawing; System.Drawing.Drawing2D;

namespace TYWinforms.Day18.Controls { public class EyeControl : Control { private Point ptPosition; private Color clrIris; private int intBlood; private Color clrBlood; private System.Timers.Timer objTimer = new System.Timers.Timer(); private EventHandler onEyeMoved; private GraphicsPath pthClip = new GraphicsPath();

605

Benutzerdefinierte Steuerelemente in Windows Forms

15: private Region regClip; 16: 17: public Point PupilPosition { 18: get { return ptPosition; } 19: set { 20: ptPosition = value; 21: Invalidate(); 22: } 23: } 24: 25: public Color IrisColor { 26: get { return clrIris; } 27: set { 28: clrIris = value; 29: Invalidate(); 30: } 31: } 32: 33: public int BloodShotedness { 34: get { return intBlood; } 35: set { 36: intBlood = value; 37: clrBlood = Color.FromArgb(255, 255 - (int)(255*(intBlood/  100.0)), 255 - (int)(255*(intBlood/100.0))); 38: Invalidate(); 39: } 40: } 41: 42: public EyeControl() { 43: ptPosition = new Point(0,0); 44: intBlood = 0; 45: clrBlood = Color.White; 46: clrIris = Color.Blue; 47: 48: objTimer.Interval = 100; 49: objTimer.Elapsed += new  System.Timers.ElapsedEventHandler(this.Update); 50: objTimer.Enabled = true; 51: 52: this.Size = new Size(100,100); 53: this.HandleCreated += new EventHandler (this.SetBounds); 54: } 55: 56: public event EventHandler EyeMoved { 57: add { onEyeMoved += value; } 58: remove { onEyeMoved -= value; }

606

Benutzerdefinierte Steuerelemente

59: } 60: 61: private void SetBounds(Object Sender, EventArgs e) { 62: pthClip.AddEllipse(ClientRectangle); 63: regClip = new Region(pthClip); 64: } 65: 66: private void Update(Object Sender, System.Timers.ElapsedEventArgs e) { 67: ptPosition = Cursor.Position; 68: 69: ptPosition.X /= (Screen.PrimaryScreen.Bounds.Width /  ClientRectangle.Width); 70: ptPosition.Y /= (Screen.PrimaryScreen.Bounds.Height /  ClientRectangle.Height); 71: 72: if (ptPosition.X > ClientRectangle.Width-30) { 73: ptPosition.X = ClientRectangle.Width-30; 74: } 75: 76: if (ptPosition.Y > ClientRectangle.Height-30) { 77: ptPosition.Y = ClientRectangle.Height-30; 78: } 79: 80: OnEyeMoved(EventArgs.Empty); 81: Invalidate(); 82: } 83: 84: protected virtual void OnEyeMoved(EventArgs e) { 85: if (onEyeMoved != null) { 86: onEyeMoved(this, e); 87: } 88: } 89: 90: protected override void OnPaint(PaintEventArgs e) { 91: e.Graphics.Clip = regClip; 92: e.Graphics.FillEllipse(new SolidBrush(clrBlood), ClientRectangle); 93: 94: e.Graphics.FillEllipse(new SolidBrush(clrIris), new  Rectangle(ptPosition.X, ptPosition.Y, 30, 30)); 95: e.Graphics.FillEllipse(Brushes.Black, new Rectangle(ptPosition.X+5,  ptPosition.Y+5, 20, 20)); 96: } 97: } 98: }

607

Benutzerdefinierte Steuerelemente in Windows Forms

Alle internen Variablen werden in den Zeilen 8 bis 15 deklariert, darunter auch der Ausschneidebereich (Clip-Bereich), die für Iris und »Bloodshotedness« verwendeten Farben und der Ereignisdelegat. Die Zeilen 17 bis 40 sind die öffentlichen Eigenschaften, also diejenigen, die Benutzer über ihren Code setzen können. PupilPosition und IrisColor sind einfach, doch die Bloodshotedness-Eigenschaft setzt Formeln ein, um einen Prozentwert in eine richtige Farbe zu übersetzen. Jede dieser Eigenschaften ruft die Invalidate-Methode auf, um das Auge neu zeichnen zu lassen. In Zeile 42 beginnt der Konstruktor. Alle internen Variablen werden initialisiert (darunter die Size-Eigenschaft) und der Timer wird eingerichtet und gestartet. Für die Timer- und HandleCreated-Ereignisse werden Ereignishandler angegeben. Das benutzerdefinierte EyeMoved-Ereignis wird in den Zeilen 56 bis 59 deklariert. Dieser Code gleicht der Syntax der Eigenschaftendeklaration und weist einen Ereignishandler zu oder entfernt ihn aus dem Delegaten. Der Ereignishandler für das HandleCreated-Ereignis, SetBound, findet sich in den Zeilen 61 bis 64. Nachdem Ihr Steuerelement einen Handle erhalten hat, initialisieren Sie hier den Ausschneidebereich. Die Update-Methode in Zeile 66 ist der Ereignishandler für das Timer-Steuerelement. Sie ruft zunächst die aktuelle Position des Mauszeigers ab und übersetzt sie in Koordinaten, die in das Steuerelement passen. Die Serie von if-Statements in den Zeilen 72 bis 78 stellt sicher, dass Iris und Pupille niemals außerhalb des Steuerelements gezeichnet werden, indem sie die Breite und Höhe der Iris berücksichtigen. Zeile 80 ruft die OnEyeMoved-Methode auf, die lediglich das EyeMoved-Ereignis auslöst. In Zeile 84 deklariert man das OnEyeMoved-Ereignis, dessen einzige Verantwortung im Auslösen des EyeMoved-Ereignisses liegt. Die überschriebene OnPaint-Methode (Zeile 90) zeichnet das äußere Auge, die Iris und die Pupille, wobei sie die diversen Eigenschaften berücksichtigt, die zuvor gesetzt wurden. Zu diesen zählt die Röte des äußeren Auges, die Irisfarbe und die übersetzte Position von Iris und Pupille. Das hier entwickelte Steuerelement ist recht vielschichtig. Im Gegensatz dazu kann das einfachste benutzerdefinierte Steuerelement lediglich aus einer überschriebenen OnPaintMethode bestehen. Um etwas wirklich Sinnvolles zu erhalten, muss man jedoch Eigenschaften und Ereignisse einbeziehen.

608

Benutzerdefinierte Steuerelemente in Windows Forms

Auch dieses Steuerelement lässt sich sinnvoller gestalten. Beispielsweise könnte man die Iris- und Pupillenform je nach der Position des Mauszeigers ändern, so dass sie perspektivisch verkürzt und eher dreidimensional erscheint. Man könnte auch eine Blinzel- oder Glotz-Methode hinzufügen, wobei das Auge dem Mauszeiger folgen würde, bis man ihm aufzuhören befiehlt. Sobald Sie die Grundelemente der Erstellung eigener Steuerelemente verstanden haben, besteht das einzige Hindernis hinsichtlich des Funktionsumfangs in Ihrer Vorstellungskraft.

18.3 Benutzerdefinierte Steuerelemente in Windows Forms Kompilieren Sie Ihr benutzerdefiniertes Steuerelement zu einer Assembly mit folgendem Befehl: csc /t:library /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll listing18.3.cs

Der letzte Schritt besteht darin, eine normale Windows Forms-Anwendung zu erstellen, die unser Steuerelement verwendet. Sie können es in jeder beliebigen Anwendung ebenso einsetzen wie ein TextBox- oder ComboBox-Steuerelement. Listing 18.4 zeigt ein Beispiel. Listing 18.4: Das Auge einer sinnvollen Nutzung zuführen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:

using using using using

System; System.Windows.Forms; System.Drawing; TYWinforms.Day18.Controls;

namespace TYWinforms.Day18 { public class Listing184 : Form { private EyeControl objEye = new EyeControl(); public Listing184() { objEye.EyeMoved += new EventHandler(this.Go); objEye.Location = new Point(75,75); objEye.BloodShotedness = 0; this.Controls.Add(objEye); this.Text = "Listing 18.4"; } private void Go(Object Sender, EventArgs e) {

609

Benutzerdefinierte Steuerelemente in Windows Forms

20: 21: 22: 23: 24: 25: 26: 27:

this.Text = objEye.PupilPosition.ToString(); } public static void Main() { Application.Run(new Listing184()); } } }

Das Auge-Steuerelement wird in Zeile 8 erstellt. Seine Eigenschaften werden in den Zeilen 12 und 13 initialisiert und in Zeile 11 wird ein Ereignishandler zugewiesen. Die Go-Methode in den Zeilen 19 bis 21 aktualisiert lediglich die Titelzeile der Anwendung mit Hilfe der PupilPosition-Eigenschaft des Steuerelements. Abbildung 18.5 zeigt ein Beispiel für das Ergebnis.

Abbildung 18.5: Ein Spionauge, das Sie (bzw. den Mauszeiger) beobachtet

18.4 Benutzerdefinierte Steuerelemente aus vorhandenen erzeugen Zuweilen benötigt man die Flexibilität, ein Steuerelement ganz neu erstellen zu können, überhaupt nicht. Möglicherweise ist der dafür nötige Funktionsumfang bereits in einem anderen, bereits vorhandenen Steuerelement gekapselt und Ihnen fehlt nur noch das iTüpfelchen. In solchen Fällen hat man zwei Wahlmöglichkeiten: Man kann ein existentes

610

Benutzerdefinierte Steuerelemente aus vorhandenen erzeugen

Steuerelement erweitern oder ein neues durch die Kombination mehrerer vorhandener erstellen. Die erste Methode wird als Erweiterung bezeichnet, die zweite bezeichnet ein zusammengesetztes oder Benutzersteuerelement. Wir werfen einen Blick auf beide Methoden. Keine Angst: Keine der beiden ist so kompliziert wie das Erschaffen eines völlig neuen Steuerelements.

Vorhandene Steuerelemente erweitern Eines der häufig gewünschten Steuerelemente ist eine Schaltfläche, deren Beschriftung sich direkt von der Tastatur aus bearbeiten lässt. Ein Benutzer könnte also einfach Zeichen eintippen und der Text im Button-Steuerelement würde sich entsprechend ändern. Dafür jedoch brauchen Sie kein neues Steuerelement, denn die Funktion, wie man eine Schaltfläche zeichnet, gibt es ja bereits. Um die gewünschte Fähigkeit nun einzufügen, erstellen Sie eine neue Klasse, die von der vorhandenen Button-Klasse und nicht direkt von der Control-Klasse erbt. Listing 18.5 zeigt den nötigen Code. Listing 18.5: Eine benutzerdefinierte Schaltfläche 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:

using using using using

System; System.Windows.Forms; System.Drawing; System.Drawing.Drawing2D;

namespace TYWinforms.Day18.Controls { public class CustomButton : Button { private String strText; private Boolean blnEdit = false; protected override void OnMouseDown (MouseEventArgs e) { if (e.Button == MouseButtons.Right) { blnEdit = !blnEdit; strText = ""; } else { base.OnMouseDown(e); } } protected override void OnKeyPress(KeyPressEventArgs e) { if (blnEdit) { strText += e.KeyChar; this.Text = strText;

611

Benutzerdefinierte Steuerelemente in Windows Forms

24: 25: 26: 27:

} } } }

Hier benötigen wir zweierlei: eine Variable, die uns sagen kann, ob wir uns im »Bearbeiten«-Modus befinden, und eine Zeichenfolge, die die Zeichen, die der Benutzer eintippt, enthält. Die beiden Variablen werden in den Zeilen 8 und 9 deklariert. Wenn der Benutzer mit der rechten Maustaste auf die Schaltfläche klickt, wollen wir die standardmäßige Verhaltensweise überschreiben und in den Bearbeitungsmodus gehen. Das ist einfach: Die Zeile 11 überschreibt die OnMouseDownMethode. Wir verwenden die MouseEventArgs-Parameter, um zu bestimmen, welche Maustaste gedrückt wurde. War es die rechte, wird der boolesche Wert blnEdit negiert (Zeile 13) und in Zeile 14 der aktuell in der Schaltfläche befindliche Text gelöscht. War es die linke Maustaste, führen Sie lediglich die Standardverhaltensweise mit dem Aufruf von base.OnMouseDown(e) aus. Anfangs ist blnEdit auf false gesetzt und demzufolge der Bearbeitungsmodus abgeschaltet. Der Code in Zeile 13 setzt ihn auf true, sobald die Schaltfläche erstmals angeklickt wird, und zurück auf false, sobald die Schaltfläche erneut angeklickt wird. Wir müssen in der Lage sein, Tastatureingaben abzurufen. Gewöhnlich erfolgt dies mit dem KeyPress-Ereignis – so auch hier. Wir überschreiben die standardmäßige OnKeyPress-Methode in Zeile 20. Falls der Bearbeitungsmodus eingeschaltet ist, erfassen wir die Tastendrücke, sammeln sie in der Zeichenfolge strText und zeigen sie schließlich in der Schaltfläche mit Hilfe der Text-Eigenschaft an. In diesem Fall brauchen wir nicht einmal die OnPaint-Methode zu überschreiben. Setzt man die Text-Eigenschaft auf einen Wert (wie in Zeile 23), löst das Steuerelement automatisch die Paint-Methode aus und zeigt an, was auch immer in Text angegeben ist – genau das, was wir wollen. Abbildung 18.6 zeigt eine Beispielausgabe, nachdem der Benutzer etwas Text eingegeben hat.

612

Benutzerdefinierte Steuerelemente aus vorhandenen erzeugen

Abbildung 18.6: Der Benutzer kann Text direkt in die Schaltfläche (Button) eingeben.

Benutzersteuerelemente Ein neues Steuerelement durch die Kombination anderer Steuerelemente zu erzeugen, ist sogar noch einfacher, als ein vorhandenes zu erweitern: Wir brauchen keine OnPaintMethoden zu erzeugen oder zu überschreiben. Derartige Steuerelemente, die aus zwei oder mehreren anderen Steuerelementen bestehen, bezeichnet man als zusammengesetzte Steuerelemente oder (häufiger) als Benutzersteuerelemente . Ein Benutzersteuerelement stellt eine weitere Ebene der Kapselung im .NET Framework dar. Hat man eine Reihe von Steuerelementen, die man häufig gemeinsam einsetzt – etwa ein Anmeldeformular –, ist es sinnvoll, diese Steuerelemente miteinander zu einem zu kombinieren, damit man diese Komplexität bei ihrer individuellen Erstellung verbergen kann. Weil diese Aufgabe (Anmeldung) stets das Gleiche tut, lässt sich die Funktionalität auch leicht in ein benutzerdefiniertes Steuerelement hüllen. Stellen Sie sich ein Benutzersteuerelement als ein Windows Form vor, das sich innerhalb anderer Windows Forms verwenden lässt. Das Benutzersteuerelement ist im Wesentlichen ein Bestandteil der Benutzeroberfläche, der komplizierter als ein einzelnes Windows Forms-Steuerelement ist. Listing 18.6 zeigt ein einfaches Beispiel für ein Benutzersteuerelement, das ein TextBox- und ein Button-Steuerelement kombiniert. Listing 18.6: Ein Benutzersteuerelement zur Anmeldung 1: 2: 3: 4: 5: 6:

Imports System Imports System.Drawing Imports System.Windows.Forms namespace TYWinforms.Day18.Controls public class LoginForm : Inherits UserControl

613

Benutzerdefinierte Steuerelemente in Windows Forms

7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:

private lblUsername As New Label() private lblPassword As New Label() private tbUsername As New TextBox() private tbPassword As New TextBox() private btSubmit As New Button() public sub New() lblUsername.Text = "Benutzername: " lblUsername.Width = 70 lblUsername.Location = new Point(10,10) lblPassword.Text = "Kennwort: " lblPassword.Width = 70 lblPassword.Location = new Point(10,40) tbUsername.Location = new Point(80,10) tbPassword.Location = new Point(80,40) btSubmit.Text = "Abschicken" btSubmit.Location = new Point(50,70) AddHandler btSubmit.Click, new EventHandler(AddressOf Me.Register) Me.BackColor = Color.Beige Me.Size = new Size(190,100) Me.Controls.Add(lblUsername) Me.Controls.Add(lblPassword) Me.Controls.Add(tbUsername) Me.Controls.Add(tbPassword) Me.Controls.Add(btSubmit) end sub private sub Register(Sender As Object, e As EventArgs) MessageBox.Show(tbUsername.Text & " wurde registriert!") end sub end class end namespace

Auf den ersten Blick sieht dieses Listing wie das für eine normale Windows Forms-Anwendung aus, wenn auch ohne Main-Methode. Es legt in den Zeilen 7 bis 11 eine Reihe von Steuerelementen an, initialisiert sie im Konstruktor (Zeilen 14 bis 27), legt einige allgemeine Eigenschaften für die Klasse fest (Zeilen 29 bis 36) und richtet in den Zeilen 38 bis 40 für das Button.Click-Ereignis einen Ereignishandler ein. Also gleicht dieses Steuerelement einem Windows Forms-Steuerelement aufs Haar – fast.

614

Benutzerdefinierte Steuerelemente aus vorhandenen erzeugen

Beachten Sie Zeile 6. Statt von der Form-Klasse zu erben, erben wir von UserControl. Die Klasse UserControl-Klasse verhält sich sowohl wie ein Form als auch wie ein Control. Sie kann andere Steuerelemente enthalten wie etwa Form, ist aber selbst ein Steuerelement, das sich in ein Form einbauen lässt. Wenn man also weiß, wie man Windows Forms-Klassen anlegt, kann man auch Benutzersteuerelemente anlegen. Kompilieren Sie diesen Code mit dem folgenden Befehl zu einer Assembly: vbc /t:library /r:system.dll /r:system.drawing.dll /r:system.windows.forms.dll listing18.6.vb

Listing 18.7 zeigt eine sehr einfache Anwendung, die dieses Benutzersteuerelement einsetzt. Listing 18.7: Der Einsatz des Benutzersteuerelements 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:

using using using using

System; System.Windows.Forms; System.Drawing; TYWinforms.Day18.Controls;

namespace TYWinforms.Day18 { public class Listing187 : Form { private LoginForm objFrm = new LoginForm(); public Listing187() { objFrm.Location = new Point(75,75); this.Controls.Add(objFrm); } public static void Main() { Application.Run(new Listing187()); } } }

Hier findet sich nichts Ungewöhnliches, denn Listing 18.7 ist lediglich eine ganz normale Windows Forms-Anwendung, die zufällig unser LoginForm-Benutzersteuerelement nutzt. Moment mal! Listing 18.7 ist in C# geschrieben, doch Listing 18.6 in VB .NET. Das bedeutet eine weitere großartige Neuheit hinsichtlich Benutzersteuerelementen und Kapselung. Die von Ihnen erstellten Steuerelemente (Benutzersteuerelemente oder sonstige) müssen nicht in der gleichen Programmiersprache geschrieben sein wie die sie aufnehmende Anwendung. Hier sehen wir sozusagen die CLR bei der Arbeit, wie sie sprachüber-

615

Benutzerdefinierte Steuerelemente in Windows Forms

greifende Komponenten zur Zusammenarbeit bringt. Abbildung 18.7 zeigt das Ergebnis von Listing 18.7.

Abbildung 18.7: Ein Benutzersteuerelement kapselt die Komplexität seiner Steuerelementkomponenten.

Alle in Listing 18.6 deklarierten Steuerelemente waren privat. Im Allgemeinen ist dies bei Benutzersteuerelementen keine schlechte Idee, denn schließlich möchte man verhindern, dass der Endanwender in die internen Abläufe der Steuerelemente eingreift. Immerhin war das ja der Grund, warum man sie überhaupt als Benutzersteuerelement gekapselt hat. Das bedeutet jedoch nicht, dass man keine benutzerdefinierten Eigenschaften, Methoden oder Ereignisse wie die heute erzeugten benutzerdefinierten Steuerelemente erstellen kann. Vielmehr ist dies häufig notwendig. Nehmen wir etwa Listing 18.6: Statt die ClickMethode intern zu handhaben, hätte das Benutzersteuerelement ein benutzerdefiniertes Ereignis erzeugen können, das das Click-Ereignis an die umgebende Anwendung hätte weiterleiten können. Dadurch könnte der Schöpfer der Anwendung genau bestimmen, was geschehen soll, sobald der Endanwender auf die ABSCHICKEN-Schaltfläche klickt – beispielsweise die Daten in eine Datenbank speichern. Zu diesem Zweck müsste man in Listing 18.6 nur folgenden Code hinzufügen: public event Registered(Sender As Object, username as String, password as String) Protected Sub OnRegistered() RaiseEvent Registered(Me, tbUsername.Text, tbPassword.Text) End Sub

Deklarieren Sie Ihr Ereignis (in diesem Fall Registered) mit den Parametern, die Sie an die umgebende Anwendung weiterleiten wollen. Erstellen Sie dann die entsprechende OnEreignisName-Methode, die das Ereignis auslöst. VB .NET erledigt für Sie den Rest (in C# hätten Sie eine benutzerdefinierte Ereignishandler-Klasse zu erstellen, die die drei hier

616

Zusammenfassung

benutzten Parameter übernimmt, doch VB .NET tut das für Sie – weitere Informationen hierzu siehe Tag 5). Um Ihr Ereignis zu nutzen, fügen Sie den folgenden Code in den Konstruktor von Listing 18.7 ein: objFrm.Registered += new LoginForm.RegisteredEventHandler(this.RegisterMe);

Dann erstellen Sie die RegisterMe-Methode: private void RegisterMe (Object Sender, String username, String password) { MessageBox.Show ("Registriert " + username); }

Mit Benutzersteuerelementen können Sie benutzerdefinierte Schnittstellen erstellen und diese anderen Entwicklern übergeben, ohne sich sorgen zu müssen, ob sie mit Ihrem Code herumspielen und die Früchte Ihrer harten Arbeit verändern – besonders wenn Sie ein C#Programmierer sind und die anderen in VB .NET schreiben.

18.5 Zusammenfassung Heute haben Sie von drei Möglichkeiten erfahren, wie Sie Ihre Anwendungen erweitern können: benutzerdefinierte Steuerelemente erstellen, vorhandene Steuerelemente erweitern und Benutzersteuerelemente anlegen. Die Basisklasse für jedes Steuerelement ist die Klasse Control. Wenn Sie eigene Steuerelemente erstellen müssen, stellen Sie sicher, dass Sie von Control ableiten. Diese Klasse ist bereits mit zahlreichen Eigenschaften, Methoden und Ereignissen ausgestattet, die sich nutzen lassen, doch man kann leicht auch eigene erstellen, sollten die vorgefertigten die eigenen Anforderungen nicht erfüllen können. Beim benutzerdefinierten Steuerelement, das wir heute erstellt haben, erfuhren wir, dass es zuweilen schwieriger als angenommen ist, benutzerdefinierte Funktionalität zu erzeugen. Das Augapfel-Steuerelement wies eine Reihe kleinerer Probleme auf, die vorab auf dem Weg zu einem großen Ganzen gelöst werden mussten. Nach sorgfältiger Planung sollte man jedoch nicht mehr auf unerwartete Probleme stoßen. Um ein vorhandenes Steuerelement zu erweitern, muss man einfach von ihm statt direkt von der Klasse Control erben. Da alle Steuerelemente von Control abgeleitet sind, erbt man ebenfalls von Control, wenn auch indirekt. Will man die visuelle Erscheinungsweise des vorhandenen Steuerelements beibehalten, stellt man sicher, dass die base.OnPaintMethode aufgerufen wird.

617

Benutzerdefinierte Steuerelemente in Windows Forms

Benutzersteuerelemente schließlich verhalten sich genau wie Windows Forms-Klassen. Der einzige Unterschied besteht darin, dass man von UserControl erbt statt von der FormKlasse, was es ermöglicht, Benutzersteuerelemente auf Formularen zu platzieren. Die heutige Lektion unterstrich einige Hauptkonzepte des .NET Frameworks, namentlich Kapselung, Erweiterbarkeit und Wiederverwendbarkeit. Mit der Fähigkeit, eigene Steuerelemente aus anderen Steuerelementen oder ganz neu zu entwickeln, befinden Sie sich in einer ausgezeichneten Position, einzigartige Anwendungen zu erstellen.

18.6 Fragen und Antworten F

Kann ich mich benachrichtigen lassen, wenn sich eine der Eigenschaften meines Steuerelements ändert, beispielsweise wenn die PupilPosition-Eigenschaft vom Benutzer aktualisiert wird? A

Ja, das ist möglich. Ändert sich eine Eigenschaft in Ihrem Steuerelement, wird das Ereignis PropertyChanged ausgelöst. Sie können diesem Ereignis einen Ereignishandler zuordnen oder die OnPropertyChanged-Methode überschreiben, damit sie benutzerdefinierten Code ausführt, sobald sich eine Eigenschaft ändert. Das entsprechende Objekt PropertyChangedEventArgs verfügt seinerseits über eine Eigenschaft namens PropertyName, die Ihnen mitteilt, welche Eigenschaft sich geändert hat.

18.7 Workshop Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

618

Workshop

Quiz 1. Worin bestehen die vier Schritte bei der Erstellung eines benutzerdefinierten Steuerelements? 2. Welche Methode müssen Sie aufrufen, um Ihr Steuerelement neu zeichnen zu lassen? 3. Schreiben Sie in C# ein Beispiel für eine öffentliche Eigenschaft und verwenden Sie die Syntax der Eigenschaftendeklaration. 4. Schreiben Sie in VB.NET ein Beispiel für eine öffentliche Eigenschaft und verwenden Sie die Syntax der Eigenschaftendeklaration. 5. Was ereignet sich zuerst: es wird ein Handle erzeugt oder Ihr Steuerelement wird am Bildschirm angezeigt? Warum? 6. Wahr oder falsch? Man kann von einem RichTextBox-Steuerelement erben. 7. Nennen Sie drei Nutzeffekte der Erstellung von Benutzersteuerelementen.

Übung Erstellen Sie einen Thermostaten (Wärmeregler) als benutzerdefiniertes Steuerelement, das die aktuelle Temperatur in der Art eines richtigen Quecksilberthermometers anzeigt. Stellen Sie sicher, dass die Temperatur nicht über das Maximum hinausgeht oder unter Null fällt. Implementieren Sie das Steuerelement in einer Windows Forms-Anwendung und stellen Sie dem Benutzer Schaltflächen bereit, mit denen er die Temperatur steigern oder verringern kann. (Natürlich müssen Sie sich all dies ausdenken, denn Sie können ja nicht die tatsächliche Temperatur messen.)

619

MultithreadingAnwendungen

9 1

Multithreading-Anwendungen

Threading ist ein vielschichtiges Thema, mit dem man sich deshalb häufig nur in Publikationen für Fortgeschrittene beschäftigt. Doch es handelt sich dabei um ein großartiges Hilfsmittel, das die Performanz und Bedienbarkeit von Windows Forms-Anwendungen deutlich erhöhen kann und dem man daher angemessene Aufmerksamkeit zukommen lassen sollte. Ein Thread ist die Grundeinheit, in der ein Computer Befehle ausführt (also ein einzelner Verarbeitungsprozess; von engl. thread: Faden). Mehrere Threads zu haben, erlaubt es, die Geschwindigkeit des Hauptprozessors (CPU, Central Processing Unit) und den parallelen Aufbau des Betriebssystems zu nutzen, so dass Prozesse schneller und effizienter ausgeführt werden. Grob gesagt lässt sich die Leistung verdoppeln, indem man die Anzahl der Threads verdoppelt. Heute lernen Sie, worum es sich bei Threads genau handelt und was es heißt, mehr als einen gleichzeitig auszuführen. Am Ende des Tages werden Sie die Vorurteile ausgeräumt haben, die Multithreading-Anwendungen immer noch anhaften. Heute lernen Sie, 쐽

was genau ein Thread ist und was er für Ihre CPU bedeutet,



wie Threads in Windows funktionieren,



wie man neue Threads erstellt und sie verwaltet,



worum es sich bei threadübergreifendem Aufruf-Marshalling handelt,



auf welche Weise man Vorteile aus Threadpools ziehen kann,



wie man durch Synchronisierung eine sichere Kommunikation zwischen mehreren Threads ermöglicht,



mit welchen Techniken man eine vollständig multithreaded arbeitende Anwendung erstellt.

19.1 Einführung in Multithreading Bevor Sie etwas über Multithreading lernen, sollten Sie erst einmal das gute alte Threading kennen lernen und über Prozesse Bescheid wissen sowie über die Art und Weise, wie sie mit dem Betriebssystem zusammenarbeiten. Threads und Prozesse stellen die Methode des Betriebssystems dar, Aufgaben so aufzuteilen, dass sie effizienter erledigt werden können. Mit einem Prozess teilt das Betriebssystem Anwendungen voneinander ab; eine Einzelanwendung repräsentiert üblicherweise einen Einzelprozess. Ein Thread ist ein einzelner Verarbeitungsablauf. Ein einzelner Thread ist zuständig für die Verarbeitung aller Befehle in einer einzelnen Aufgabe. Technisch ausgedrückt, handelt es sich bei einem Thread um

622

Einführung in Multithreading

die elementare Ausführungseinheit, der das Betriebssystem Ressourcen zuweist. Normalerweise wird ein Thread pro Prozess erzeugt. Stellen Sie sich vor, Sie wären der Besitzer eines neuen Restaurants, das gerade eröffnet hat. Sie sind aber auch der einzige Angestellte. Kommt ein Gast herein, so sind Sie der einzige Mitarbeiter, der zur Verfügung steht, um alle notwendigen Arbeiten zu erledigen. Sie müssen eine Aufgabe beenden, bevor Sie sich der nächsten zuwenden können. Dies ist sicherlich nicht die beste Möglichkeit, die anfallende Arbeit zu erledigen. Sowohl Sie als auch Ihre Gäste leiden darunter, dass Sie sich sozusagen rar machen. In Begriffen des Computerwesens entspräche dies einer Singlethreaded-Umgebung. Es gibt nur einen Thread (Sie), um alle nötigen Aufgaben zu bewältigen. Gehen Sie einmal eine Dekade zurück, dann stoßen Sie auf ein Singlethreaded-Betriebssystem: DOS (gleichgültig, ob von Microsoft oder anderen Herstellern). In DOS konnte man nur eine Anwendung auf einmal ausführen, denn DOS besaß nur einen Thread, um sich seinen Aufgaben zu widmen. Wenn ein Thread eine Aufgabe ausführt, muss das ganze System innehalten und warten, bis er damit fertig ist, bevor der Benutzer weitermachen kann. Kurz gesagt, war DOS ein Ein-Prozess-Betriebssystem mit nur einem Thread pro Prozess. Stellen wir uns die CPU in dieser Lage vor. Der Hauptprozessor kann nur eine Aufgabe auf einmal verarbeiten (das ist oft sogar heute noch zutreffend). Tut man etwas in einer Anwendung, etwa auf eine Schaltfläche klicken, so empfängt das Betriebssystem die Benutzereingabe, übersetzt sie in etwas, das der Computer verstehen kann (Bits und Bytes) und schickt den Befehl an die CPU. Muss die CPU gerade andere Aufgaben zuerst erledigen, so hat der neue Befehl gefälligst in einer Schlange zu warten. Hat die CPU einen Befehl verarbeitet, gibt sie das Ergebnis aus und geht zum nächsten Befehl über, so lange bis es keine mehr gibt. Tut die CPU gerade nichts, wird dies als Leerlaufzustand (idle state) bezeichnet. Zurück in die Gegenwart. Prozessoren sind inzwischen derartig leistungsfähig, dass sie Befehle schneller verarbeiten als diese ankommen. CPUs, deren Taktfrequenz man früher in Megahertz oder darunter maß (MHz: 1 Million Schwingungen bzw. Taktzyklen pro Sekunde), werden nun in Gigahertz (1 Milliarde Schwingungen pro Sekunde) gemessen. Jeder Einzelbefehl kann zwischen 1 und 5 Zyklen in Anspruch nehmen (oder mehr, je nach der Kompliziertheit des Befehls), so dass dies ungefähr zwischen 200 Mio. bis 1 Mrd. Befehlen pro Sekunde entspricht. (Meine Darstellung vereinfacht CPUs in hohem Maße.) Natürlich kann man nicht 200 Mio. Befehle/Sekunde an die CPU schicken. Die Singlethreaded-Umgebung wurde entwickelt, als CPU-Geschwindigkeiten noch so eingeschränkt waren, dass Befehle, die so erteilt wurden, die CPU voll auslasteten. Hier kommt Multithreading ins Spiel. Moderne Betriebssysteme verfügen über mehr als einen Thread, um Aufgaben zu bewältigen. Windows etwa hat Hunderte von Threads gleichzeitig laufen und erzeugt je nach Bedarf weitere.

623

Multithreading-Anwendungen

Kommen wir auf unser Restaurant-Beispiel zurück. Da nun Ihr Gästekreis wächst und Sie nicht mehr alles alleine machen können, stellen Sie vier Mitarbeiter ein: einen Koch, einen Kellner, einen Kellnerlehrling und jemanden für den Gästeempfang. Nun können Sie vier (oder fünf, wenn Sie sich mitzählen) Dinge gleichzeitig erledigen, wodurch Sie mehr Gäste zufrieden stellen. Noch mehr Mitarbeiter würden die Sache für Sie noch einfacher machen (siehe Abbildung 19.1). Eine Single-threaded Umgebung

Eine Multithreaded Umgebung Befehle Befehle

Befehle

Befehle Befehle Befehle Befehle Befehle Befehle Befehle Befehle Befehle Befehle

Abbildung 19.1: Eine Multithreading-Umgebung bietet zahlreiche »Kellner« an, die Ihnen Aufgaben abnehmen.

Startet man eine Anwendung wie etwa Microsoft Word, wird ein neuer Prozess erzeugt. Auch ein neuer Thread wird angelegt, um alle Ausführungsaufgaben für diesen Prozess zu handhaben; er wird zugewiesen, um alle Word-Befehle zu verarbeiten. Währenddessen läuft das Betriebssystem unabhängig von diesem neuen Thread, damit der Rechner sauber weiterläuft. Auf diese Weise haben Sie sozusagen »die Arme frei«, um alles Anstehende zu erledigen. Starten Sie eine weitere Anwendung, beispielsweise Photoshop, werden ein weiterer Prozess und Thread erzeugt und handhaben diese Anwendung. Jetzt laufen zwei Threads gleichzeitig: der für Word und der für Photoshop. Da sie unabhängig voneinander laufen, kann man leicht zwischen ihnen hin und her wechseln. Threads mischen sich also nicht bei anderen Threads ein. Sollten sie es dennoch tun, treten unerwartete Resultate auf (siehe den Abschnitt »Probleme mit Threading«). Ein weiterer Blick auf die CPU. Zuvor führte sie einen Befehl nach dem anderen aus, dass man kaum nachkam. Nun jedoch macht die CPU zwar das Gleiche, doch da wir mehrere Prozesse (und Threads) zugleich laufen haben, wird die CPU ständig auf Trab gehalten. Jeder Thread kann der CPU seine eigenen Befehle schicken. In der oben beschriebenen Zwei-Thread-Situation (Word und Photoshop) hat die CPU zwei Datenzufuhren, die die Warteschlange für auszuführende Befehle füllen. Doch da die CPU nun Befehle so rasch ausführt, sind selbst diese zwei Zufuhren nicht genug, um sie in die Knie zu zwingen. Sehr schnell werden Befehle ausgeführt und Resultate ausgegeben. Aus diesem Grund sieht es

624

Einführung in Multithreading

so aus, als könne man mehrere Aufgaben gleichzeitig erledigen, obwohl die CPU weiterhin eine nach der anderen abarbeitet. Das Threading muss nicht mit nur einem Thread getan sein, denn so wie das Betriebssystem mehrere Prozesse haben kann, so kann auch jeder Prozess über mehrere Threads verfügen. Der Schwerpunkt der heutigen Lektion liegt nicht auf MultithreadingBetriebssystemen, sondern auf Multithreading-Anwendungen. Einer Einzelanwendung können mehrere Threads zugewiesen sein, damit ihre Effizienz erhöht wird. Wenn die CPU nämlich Zeit übrig hat, warum sollte man ihr nicht noch mehr Befehle schicken? In Microsoft Word kann man einen Thread haben, der sich um Benutzereingaben über die Tastatur und deren Bildschirmanzeige kümmert, während sich ein anderer im Hintergrund mit der Archivierung bzw. Indizierung von Dateien beschäftigt. Normalerweise müsste man mit dem Tippen innehalten und warten, bis das Archivieren beendet ist, doch auf Grund seiner Multithreading-Fähigkeiten kann Word jeder Aufgabe einen Thread zuweisen, so dass der Benutzer nicht mit dem Tippen warten muss. Abbildung 19.2 verdeutlicht dieses Konzept.

Abbildung 19.2: Mit mehreren Threads kann eine Anwendung mehrere Aufgaben gleichzeitig erledigen.

Ein alltäglicheres Beispiel dreht sich um das Internet. Websites werden zunehmend umfangreicher, so dass man länger für das Herunterladen braucht. Leider kann der Webbrowser, der einen Einzelprozess darstellt, nur »Däumchen drehen«, während er wartet, bis alle Webseiten heruntergeladen sind; diese Situation ist leider unvermeidlich. Setzt man jedoch einen Multithreading-Browser ein, können Sie von mehreren Websites gleichzeitig herunterladen, so dass Sie bei ausreichender Netzwerkbandbreite zwei (oder mehr) Seiten für den Preis von einer bekommen. Es dauert immer noch genauso lange, eine Seite herunterzuladen, doch in dieser Zeit können Sie zwei, drei oder mehr Seiten herunterladen. Ein letztes Mal zurück zum Vergleich mit dem Restaurant. Jeder Ihrer Mitarbeiter steuert nun seine eigene »Anwendung«. Der Kellner leitet ein Kellnerprogramm, der Pikkolo ein Putzprogramm usw. Angenommen, Sie stellen jeweils einen weiteren Kellner, Putzgehil-

625

Multithreading-Anwendungen

fen und Empfangskellner ein. Jetzt können zwei Kellner das Kellnerprogramm leiten. War es schon effizienter, mit mehreren Mitarbeitern ein Restaurant zu führen als mit nur einem (Ihnen), so ist es nun effizienter, mehrere Kellner die Tische bedienen zu lassen etc., solange es genügend Platz für sie gibt. Kurze Rekapitulation: Ein Thread ist ein Ausführungsvorgang, eine Einheit, die für die Befehlsausführung in einer Einzelanwendung zuständig ist. Ein Multithreaded-Betriebssystem kann mehrere Threads zugleich laufen haben, wovon jeweils einer eine Anwendung oder einen Prozess, der gerade ausgeführt wird, handhabt. Einer MultithreadedAnwendung sind mehrere Threads zugewiesen, um mehrere Aufgaben gleichzeitig in der Anwendung ausführen zu können. Da Sie nun die Vorteile kennen gelernt haben, sollen Sie auch über die Nachteile Bescheid wissen.

Probleme mit Threading Zunächst fällt einem natürlich als Nachteil ein möglicher Mangel an Ressourcen ein. Je mehr Threads man laufen hat, desto mehr Ressourcen verschlingen sie. Leider ist Ihr Computer kein unerschöpflicher Quell an Ressourcen; man ist gezwungen, innerhalb der Beschränkungen zu arbeiten, die einem CPU und Arbeitsspeicher setzen. Leider bedeutet auch das Hinzufügen eines zweiten Threads zu einer bestimmten Aufgabe nicht unbedingt, dass sich damit auch die Leistung verdoppelt. Der Leistungszuwachs entspricht nicht eins zu eins der Ressourcennutzung. Der Grund dafür ist der Bedarf des Computers an Ressourcen, nur um jeden Thread aufrechtzuerhalten. Erzeugt man also einen Thread, benötigt man nicht nur Ressourcen, um dessen Aufgaben ausführen zu können, sondern auch zusätzliche Kapazität für den Verwaltungsaufwand, um dafür zu sorgen, dass der Thread richtig läuft. Wenn die CPU außerdem zwischen Threads hin und her wechselt, muss sie zudem den Status jedes einzelnen Threads im »Gedächtnis« behalten, damit sie weiß, wo sie fortfahren soll, wenn sie zu einem bestimmten Thread zurückkehrt. Diese ganzen Beanspruchungen können mehr an Ressourcen verschlingen, als Ihnen Ihr Thread an Leistung zurückgibt. Ein weiteres Problem rührt von der Verständigung zwischen Threads her. Häufig ist es nötig, dass ein Thread sozusagen mit einem anderen Thread spricht, insbesondere dann, wenn beide der gleichen Anwendung angehören. Da jedoch Threads voneinander isoliert sind und jeder über seine eigenen Ressourcen verfügt, kann dies zum Problem werden. Threads müssen spezielle Prozeduren ausführen, um dafür zu sorgen, dass ihre Daten mit ausreichender Sicherheit über die Grenzen zwischen den Threads in fremdes Thread»Territorium« reisen können. Dies kann auf Kosten der Leistung gehen. Ein noch wichtigeres Problem besteht, wenn zwei oder mehr Threads die gleiche Datenmenge gemeinsam nutzen müssen. Angenommen, gleichzeitig laufen zwei Threads in

626

Einführung in Multithreading

Word, die auf dieselbe Datei zugreifen. Was passiert, wenn einer der Threads die Datei verändert? Dann sind die Daten, über die der andere Thread verfügt, nicht mehr gültig und müssen aktualisiert werden. Wenn die beiden Threads versuchen, zu genau der gleichen Zeit auf die Datei zuzugreifen, treten fast zwangsläufig Probleme auf und Ihre Anwendung stürzt möglicherweise ab. Aus diesem Grund können Threads ihre Ressourcen sperren. Anders ausgedrückt, wenn ein Thread eine bestimmte Ressource nutzt, dann lässt er alle anderen Threads wissen, dass sie keinesfalls versuchen sollen, auf dieselbe Ressource zuzugreifen. Auch diese Sperrungen knabbern an den Ressourcen und mindern die Leistung, so notwendig sie auch sind. Die anderen Threads sollten die Sperren beachten, müssen das aber nicht unbedingt tun. Natürlich ist der Einsatz von Threads nicht grundsätzlich falsch, wie Sie noch aus dem vorigen Abschnitt wissen. Doch die Erstellung von Multithreading-Anwendungen erfordert sorgfältige Planung und bietet oftmals mehr Vor- als Nachteile. Ich empfehle

Bitte beachten Sie

Ziehen Sie Multithreading in Betracht, wenn Sie die notwendigen Vorsichtsmaßnahmen ergreifen können, um die oben beschriebenen Probleme zu vermeiden, wenn kurze Reaktionszeiten für Ihre Anwendungen zu garantieren sind oder um Ihren Endanwendern mehr Kontrollmöglichkeiten einzuräumen, während andere Aufgaben erledigt werden.

Setzen Sie kein Multithreading ein, wenn Sie nicht im Voraus wissen, wie ressourcenhungrig Ihre Anwendung ist oder wie eingeschränkt das Zielsystem hinsichtlich seiner Verarbeitungskapazität ist.

Threading-Objekte verwenden Die wichtigste im .NET Framework verwendete Threading-Klasse ist System.Threading.Thread. Sie repräsentiert einen einzelnen Thread, der gerade im Betriebssystem läuft, und verfügt über eine Reihe von Eigenschaften und Methoden, die Ihnen Informationen darüber liefern, was gerade mit diesem Thread passiert. (Gleich mehr dazu.) Wenn man eine Anwendung startet, wird ein neuer Thread erzeugt (oder einem Threadpool entnommen) und dieser Anwendung zugewiesen. Dieser Thread bleibt während der gesamten Lebenszeit (= Laufzeit) der Anwendung für die Ausführung ihrer Aufgaben zuständig. Dieser Thread wird als Hauptthread bezeichnet, weil er in der Anwendung stets der zentrale Thread ist, gleichgültig, wie viele weitere noch erzeugt werden. Der Umgang mit einem Thread-Objekt gestaltet sich anders als alles, was wir bisher besprochen haben. Eine nähere Untersuchung erfolgt im nächsten Abschnitt, doch im Augenblick sollten wir festhalten, dass ein Thread stets etwas haben muss, das er ausführen kann;

627

Multithreading-Anwendungen

es muss ein Stück Code geben, das sich ausführen lässt, sonst tut der Thread überhaupt nichts. Dies gleicht dem Vorgang, bei dem man einem Windows Forms-Steuerelement einen Ereignishandler zuweist. Der Hauptthread einer Anwendung hat die Main-Methode, um ihn beschäftigt zu halten (die dann wiederum den Konstruktor der Anwendung usw. aufruft). Thread ist zwar die wichtigste Klasse, mit der wir uns heute befassen, doch sie hat eine ganze Reihe von Helfern. ThreadPool wird für die Einrichtung von Threadpooling benutzt: der Vorgang der gemeinsamen Nutzung von Threads durch Anwendungen. Das TimerThreadobjekt gleicht der Klasse System.Windows.Forms.Timer, mit der Sie gearbeitet haben.

Damit können Sie eine bestimmte Aufgabe auf einem anderen Thread in einem festgelegten Intervall ausführen lassen. Monitor erlaubt Ihnen, Ressourcen zu sperren, um sie gegen den Zugriff anderer Threads zu schützen. In der heutigen Lektion befassen wir uns mit all diesen Klassen, doch zunächst betrachten wir, wie man eigentlich mit einem Thread arbeitet.

19.2 Eine Multithreading-Anwendung erstellen Bevor Sie eine Anwendung erstellen, die vollständig multithreaded ist, untersuchen wir zunächst einige Eigenschaften einer Singlethreaded-Anwendung. Insbesondere zeigen wir, wie ein normaler Thread aussieht. Ab Listing 19.2 fügen Sie dann weitere Threads hinzu. Wenn man eine Windows Forms-Anwendung startet, wird bekanntlich ein so genannter Hauptthread erzeugt und der Anwendung zugewiesen. Endet der Thread, wird die Anwendung beendet. Hält er inne, pausiert auch die Anwendung. Es hilft also, wenn man sich den Thread eng mit der jeweiligen Anwendung verknüpft vorstellt. Listing 19.1 zeigt eine einfache Anwendung, die den Hauptthread untersucht. Listing 19.1: Einen Thread untersuchen 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:

628

using using using using

System; System.Windows.Forms; System.Drawing; System.Threading;

namespace TYWinforms.Day19 { public class Listing191 : Form { private Label lblInfo = new Label(); public Listing191() { lblInfo.Size = new Size(300,300);

Eine Multithreading-Anwendung erstellen

12: lblInfo.Location = new Point(10,10); 13: 14: Thread thdCurrent = Thread.CurrentThread; 15: lblInfo.Text = "Ist aktiv: " +  thdCurrent.IsAlive.ToString() + "\n"; 16: lblInfo.Text += "Ist im Hintergrund: " +  thdCurrent.IsBackground.ToString() + "\n"; 17: lblInfo.Text += "Priorität: " +  thdCurrent.Priority.ToString() + "\n"; 18: lblInfo.Text += "Threadzustand: " +  thdCurrent.ThreadState.ToString() + "\n"; 19: 20: this.Text = "Listing 19.1"; 21: this.Controls.Add(lblInfo); 22: } 23: 24: public static void Main() { 25: Application.Run(new Listing191()); 26: } 27: } 28: }

Diese Anwendung erzeugt ein Label-Steuerelement und zeigt eine Reihe von Eigenschaften des Hauptthreads an. In Zeile 14 holen Sie eine Referenz auf den Hauptthread. Ein neues Thread-Objekt namens thdCurrent wird erzeugt und der Hauptthread mit Hilfe der statischen CurrentThread-Methode der Thread-Klasse abgerufen. Die Zeilen 15 bis 18 zeigen daraufhin die Eigenschaften des Threads an. IsAlive (Zeile 15) zeigt an, ob der Thread gerade ausgeführt wird. Natürlich erwarten wir, dass der Hauptthread läuft, sobald auch die Anwendung läuft, also sollte diese Eigenschaft true liefern. IsBackground in Zeile 16 zeigt an, ob dieser Thread im Hintergrund des jeweili-

gen Betriebssystems läuft. Der Unterschied zwischen einem Vorder- und einem Hintergrundthread besteht darin, das letzterer nicht zwingend ist, um eine Anwendung am Laufen zu halten. Anders ausgedrückt, führt ein Hintergrundthread Dinge aus, die nicht für die Ausführung der Anwendung erforderlich sind (etwa die Rechtschreibprüfung in Word). Andererseits braucht eine Anwendung unbedingt einen Vordergrundthread, um laufen zu können. Da Ihr Hauptthread die Anwendung selbst repräsentiert, muss er zwingend ein Vordergrundthread sein, und IsBackground gibt daher false zurück.

629

Multithreading-Anwendungen

Einem Thread ist auch eine Priorität zugewiesen. Sie sagt dem Betriebssystem, wie viel CPU-Zeit es dem Thread im Verhältnis zu anderen Threads zuteilen soll. So sollte beispielsweise ein Thread, der seine Aufgabe schnell erledigen soll, eine möglichst hohe Priorität besitzen, damit er die CPU dazu veranlasst, ihm die meiste Zeit zu widmen. Mit der Priority-Eigenschaft lässt sich dieser Wert festlegen oder herausfinden und sie verwendet einen Wert aus der Aufzählung ThreadPriority: 왘

Highest: Threads mit der höchsten Priorität



AboveNormal: Threads mit der zweithöchsten Priorität



Normal: Typische Threads



BelowNormal: Die vierthöchste Priorität, direkt unter Normal



Lowest: Threads mit dieser Priorität werden nach allen anderen ausgeführt

Standardmäßig wird Threads die Normal-Priorität zugewiesen; unser Hauptthread macht da keine Ausnahme. Die ThreadState-Eigenschaft in Zeile 18 zeigt den Zustand an, in dem sich ein Thread gerade befindet (ähnlich wie die ServiceControllerStatus-Eigenschaft aus Tag 16). Diese Eigenschaft wird gesteuert von Werten aus der Aufzählung ThreadState: 쐽 쐽

Aborted: Der Thread wurde beendet. AbortRequested: Der Thread läuft noch, doch ein Befehl wurde erteilt, ihn zu been-

den. 쐽

Background: Bei dem Thread handelt es sich um einen Hintergrundthread.



Running: Der Thread wird gerade ausgeführt.



Stopped: Der Thread wurde beendet (ähnlich wie Aborted).



StopRequested: Dem Thread wurde eine Anforderung zur Beendigung geschickt (dieser Wert kann nur intern vom .NET Framework benutzt werden).



Suspended: Der Thread wurde unterbrochen.



SuspendRequested: Dem Thread wurde eine Anforderung zur Unterbrechung

geschickt. 쐽

Unstarted: Der Thread wurde erzeugt, aber noch nicht gestartet.



WaitSleepJoin: Der Thread ist blockiert und daher unzugänglich, bis eine bestimmte Bedingung erfüllt ist (gleich mehr dazu).

630

Eine Multithreading-Anwendung erstellen

Sie können sich denken, dass sich Ihr Hauptthread wahrscheinlich im Running-Zustand befindet, sobald die entsprechende Anwendung gestartet ist. Abbildung 19.3 zeigt das Ergebnis des Listings 19.1.

Abbildung 19.3: Die Eigenschaft eines typischen Hauptthreads.

Zu seiner Erstellung müssen Sie dem Thread etwas zum Ausführen geben, mit anderen Worten: eine Methode. Sie stellt so etwas wie den Ereignishandler für den Thread dar. Sobald ein Thread startet, wird diese Methode ausgeführt. Die Verknüpfung zwischen Thread und Methode wird von einem ThreadStart-Delegaten gehandhabt. Beispielsweise so: Thread thdNewThread = new Thread(new ThreadStart(MyMethod)); MyMethod ist die Methode, die unser neuer Thread ausführt, sobald er startet. Der ThreadStart-Delegat gleicht jedem anderen Delegaten, der ein Ereignis auf eine Methode verweist. Hätte die Thread-Klasse ein Start-Ereignis, könnte man die obige Zeile wie folgt

schreiben: Thread thdNewThread = new Thread(); thdNewThread.Start += new ThreadStart(this.MyMethod);

Doch dieses Start-Ereignis gibt es leider nicht, so dass die vorigen zwei Statements zu einem zusammengefasst werden müssen, wie in der obigen Einzelzeile zu sehen. Um den Thread zu starten, ruft man die Start-Methode auf: thdNewThread.Start();

Dieser Thread tut nichts, bis man Start aufruft.

631

Multithreading-Anwendungen

Da Sie nun mit ein paar grundlegenden Eigenschaften von Threads vertraut sind, schauen wir uns eine einfache Multithreading-Anwendung an. Listing 19.2 zeigt eine Applikation, die einen neuen Thread erzeugt. Die Anwendung demonstriert die Eigenheiten beim Umgang mit mehreren Threads in Windows Forms. Listing 19.2: Die Reihenfolge der Threadausführung 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: using System.Threading; 5: 6: namespace TYWinforms.Day19 { 7: public class Listing192 : Form { 8: private Label lblInfo = new Label(); 9: private Button btStart = new Button(); 10: 11: public Listing192() { 12: lblInfo.Size = new Size(300,100); 13: lblInfo.Location = new Point(10,75); 14: 15: btStart.Location = new Point(100,10); 16: btStart.Text = "Start!"; 17: btStart.Click += new EventHandler(this.StartIt); 18: 19: Thread.CurrentThread.Name = "Hauptthread"; 20: 21: this.Text = "Listing 19.2"; 22: this.Controls.Add(lblInfo); 23: this.Controls.Add(btStart); 24: } 25: 26: private void StartIt(Object Sender, EventArgs e) { 27: lblInfo.Text = "Thread 2 gestartet von " +  Thread.CurrentThread.Name + "...\n"; 28: 29: Thread thdTest = new Thread(new ThreadStart(DoIt)); 30: thdTest.Name = "Thread 2"; 31: 32: thdTest.Start(); 33: lblInfo.Text += Thread.CurrentThread.Name + " schläft...\n"; 34: Thread.Sleep(2000); 35: lblInfo.Text += Thread.CurrentThread.Name + " erwacht...\n"; 36: thdTest.Join(); 37:

632

Eine Multithreading-Anwendung erstellen

38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51:

lblInfo.Text += "Thread 2 endet...\n"; } private void DoIt() { Thread.Sleep(1000); MessageBox.Show(Thread.CurrentThread.Name); lblInfo.Text += "Hallo von " + Thread.CurrentThread.Name + "\n"; } public static void Main() { Application.Run(new Listing192()); } } }

Der Konstruktor in den Zeilen 11 bis 24 richtet zwei Steuerelemente ein: ein Bezeichnungsfeld (Label) und eine Schaltfläche (Button). Die Schaltfläche wird benutzt, um einen neuen Thread anzulegen. Die Name-Eigenschaft des Hauptthreads wird in Zeile 19 auf »Hauptthread« gesetzt. Dadurch können wir ihn später leichter finden. Werfen wir einen Blick auf StartIt, den Ereignishandler des Button-Steuerelements, in Zeile 26. Diese Methode ist relativ kompliziert, so dass wir fürs Erste nur die Syntax untersuchen; zur Funktionalität kommen wir gleich. Zuerst schreibt diese Methode etwas Text in das Label-Steuerelement. In Zeile 29 wird ein neuer Thread erzeugt, der die DoIt-Methode ausführen soll. Zeile 30 legt den Namen des Threads fest und in Zeile 32 wird der Thread gestartet. Im Label-Steuerelement wird etwas mehr Text angezeigt und die SleepMethode aufgerufen. Diese hält den Thread für den (in Millisekunden) angegebenen Zeitraum an. In Zeile 35 wird noch mehr Text in das Label-Steuerelement geschrieben, in Zeile 36 die geheimnisvolle Join-Methode aufgerufen und schließlich in Zeile 38 weiterer Text angezeigt. Join veranlasst den aktuellen Thread zu warten, bis ein anderer Thread mit seiner jeweiligen Aufgabe fertig ist. So wartet etwa in Zeile 36 der Hauptthread, bis Thread 2 seine Verarbeitungsaufgabe beendet hat, und fährt dann mit seiner eigenen Ausführung fort. Weil in Zeile 36 Join aufgerufen wird, muss der Hauptthread warten, bis Thread 2 seine Pflicht erfüllt hat. In diesem Fall muss er warten, bis eine MessageBox angezeigt und wieder geschlossen wird, bevor er fortfahren kann. Die DoIt-Methode in Zeile 41 ist recht einfach; sie veranlasst den aktuellen Thread, für eine Sekunde zu »schlafen«, zeigt eine MessageBox an und stellt dann etwas Text dar.

633

Multithreading-Anwendungen

Wenn Sie diese Anwendung ausführen und auf die Schaltfläche klicken, erhalten Sie die folgende Ausgabe im Bezeichnungsfeld: (1 Sekunde vergeht) (MessageBox wird mit dem Text "Thread 2" angezeigt) (Applikation wartet, bis MessageBox geschlossen ist) Thread 2 gestartet von Main Thread... Main thread schläft... Main thread erwacht... Hallo von Thread 2 Thread 2 endet...

Was geht hier vor sich? Abbildung 19.4 zeigt die theoretische Reihenfolge der Ausführung. Schaltfläche hier gedrückt Zeit (in Sekunden) 0

1

2

Schriftfeld zeigt an: “Thread 2 vom Haupt-Thread gestartet”

Meldungsfenster zeigt an: “Thread 2”

Schriftfeld zeigt an: “Haupt-Thread erwacht…”

Thread 2 gestartet

Schriftfeld zeigt an: “Hello from Thread 2”

Die Join-Methode wird ausgeführt

Thread 2 schläft 1 Sekunde Schriftfeld zeigt an: “Haupt-Thread schläft”

Schriftfeld zeigt an: “Thread 2 beendet…”

Haupt-Thread schläft 2 Sekunden

Abbildung 19.4: Wie wir uns den Ablauf der Anwendung vorstellen

Wäre das Ablaufdiagramm in Abbildung 19.4 korrekt, würden wir jedoch die folgende Bildschirmausgabe erwarten: Thread 2 gestartet von Main Thread... Main thread schläft... (1 Sekunde vergeht) (MessageBox wird mit dem Text "Thread 2" angezeigt) Hallo von Thread 2 (1 Sekunde vergeht) Main thread erwacht... Thread 2 endet...

Doch dies trifft nicht auf die tatsächlichen Ereignisse zu. Um herauszufinden, was los ist, muss man wissen, dass Windows Forms-Steuerelemente keineswegs threadsicher sind: Weil die Steuerelemente via Threads nicht sicher miteinander kommunizieren können, reagieren sie nur auf den Thread, der sie erzeugte, und auf keinen anderen. Hier kann nur der Hauptthread auf das Label-Steuerelement zugreifen. Will das ein anderer Thread tun, muss er einen Befehl an den Hauptthread schicken, was als Marshalling eines Befehls bezeichnet wird. Der Hauptthread führt daraufhin diese gemarshallten Befehle aus (sobald er

634

Eine Multithreading-Anwendung erstellen

dazu kommt). Das ist der Grund, warum der von der DoIt-Methode in das Bezeichnungsfeld geschriebene Text erst nach den Dingen angezeigt wird, die nach ihm im Zeitablauf an der Reihe waren. Moment mal. Das Meldungsfeld wurde zur richtigen Zeit angezeigt – eine Sekunde nach dem Start der Ausführung. Das liegt daran, dass das Meldungsfeld nicht im Hauptthread erzeugt wurde. Sein Besitzer ist der Thread 2 und das Meldungsfeld muss daher keine gemarshallten Befehle an den Hauptthread absetzen und darauf warten, dass diese ausgeführt werden. Dieses Thema bringt eines der kompliziertesten Probleme aufs Tapet, auf das man stößt, wenn man mit mehreren Threads zu tun hat: der Zugriff mehrerer Threads auf Objekte, die nur von einem Thread erstellt wurden. Leider gibt es keine Möglichkeit, dieses Problem zu umgehen. Alle Windows Forms-Steuerelemente sind nicht threadsicher, so dass jedes Mal, wenn ein Thread (außer dem erstellenden Thread) Zugriff auf ein Steuerelement benötigt, der Befehl gemarshallt werden muss. Damit ist der Thread der Ausführungsgewalt des Hauptthreads unterworfen ist und zwar selbst dann noch, wenn man die Priority-Eigenschaft des zweiten Threads auf Highest setzen würde. Es ist also keine gute Idee, seine Steuerelemente gleichzeitig von mehr als einem Thread manipulieren zu lassen. Ihren Threads bleibt dennoch ein großer Spielraum übrig und ein klassisches Beispiel wäre eine Anwendung für die Durchsuchung des Dateisystems. Werfen Sie einen Blick auf die Suchfunktion in Windows (START/SUCHEN). Gibt man ein paar Suchbegriffe ein und klickt dann auf die Schaltfläche STARTEN, durchsucht das Hilfsprogramm die Festplatte nach Dateien mit den gesuchten Begriffen. Die Benutzeroberfläche bleibt jedoch empfänglich für Benutzereingaben. Wäre dies jedoch ein singlethreaded Hilfsprogramm, würde die Benutzeroberfläche erstarren, während sie darauf wartet, dass der Suchvorgang beendet ist. Doch dank mehrerer Threads kann man weitere Suchbegriffe eingeben oder gar die Suche abbrechen, während sie noch im Hintergrund abläuft. Ein weiteres Beispiel ist die automatische Rechtschreibprüfung in Microsoft Word. Beim Tippen prüft sie Ihre Orthografie im Hintergrund, damit sie automatisch ein falsch geschriebenes Wort entdecken kann, ohne dass dabei die Anwendung langsamer wird oder gar »einfriert«. Auch dies ist auf mehrere Threads zurückzuführen.

Threads miteinander synchronisieren Unter Synchronisierung versteht man den Vorgang, bei dem sichergestellt wird, dass alle Threads wissen, wer gerade das »Vorfahrtsrecht« hat. Synchronisiert man etwas im wirklichen Leben, sorgt man dafür, dass jeder auf dem gleichen Stand ist. Wenn Sie z.B. Ihre Uhrzeit mit der Ihres Freundes synchronisieren (von chronos = Zeit), stellt jeder die Uhr

635

Multithreading-Anwendungen

auf den gleichen Zeitwert. Das Gleiche gilt für die Threadsynchronisierung. Während der Synchronisierung informieren sich alle Threads über den aktuellen Stand der Dinge (mit besonderer Berücksichtigung von Code- und Datenzugriff). Doch was bedeutet die Synchronisierung von Code- oder Datenzugriff eigentlich? Stellen Sie sich vor, Sie und zwei Ihrer Freunde würden einen Wagen kaufen, um ihn sich zu teilen. Doch normalerweise kann nur einer von Ihnen am Steuer sitzen, und die anderen müssen warten, bis sie an der Reihe sind. Daher müssen Sie und Ihre Freunde Ihre Fahrzeiten aufeinander abstimmen. Springen Sie zuerst auf den Fahrersitz, so teilen Sie Ihren Freunden auf diese Weise mit, dass Sie zuerst da waren und die anderen warten müssen. Wenn Sie mit Fahren fertig sind, dann teilen Sie ihnen dies mit, und die anderen können sich mit Fahren abwechseln. Diesen Vorgang kann man aus zwei Perspektiven betrachten. Im einen Fall bringen Sie die Methoden in Einklang, die zum Fahren des Wagens eingesetzt werden. Im anderen bringen Sie den Wagen selbst in Übereinstimmung. Anders ausgedrückt: Codesynchronisierung gegenüber Datensynchronisierung. Später wird auch dies ein wenig mehr Sinn ergeben. Beim Threading besteht das Ziel darin, Code auszuführen oder auf Daten bzw. ein Objekt zuzugreifen. Drei Threads – nämlich Sie und Ihre zwei Freunde – konkurrieren miteinander um die Ausführung des gleichen Programmcodes (symbolisiert durch das Fahren des Wagens). Ohne Synchronisierung würden alle drei Threads versuchen, gleichzeitig »auf den Fahrersitz zu gelangen«, mit unvorhersehbaren Folgen. Ein Drittel der Zeit würde der erste Thread laufen, während die anderen beiden um den Rest der Zeit konkurrieren würden. Sie könnten nicht sagen, welcher Thread gerade läuft. (Sie und Ihre Freunde sitzen gleichzeitig hinterm Steuer.) Noch schlimmer wäre es, wenn dieser bestimmte Code einige Variablen erreichen und modifizieren würde, von denen Ihre Anwendung abhängt (im Beispiel wäre dies der Wagen). Jeder Thread möchte diese Variablen für sich selbst reklamieren, wird sie aber nicht behalten können, was zu einer gewissen Unordnung führen dürfte und wahrscheinlich zum Absturz Ihrer Anwendung (dem Unfall des Wagens; vgl. Abbildung 19.5). Würde jeder Ihrer Freunde versuchen, den Wagen für sich zu behalten, würde dieser garantiert einen Unfall erleiden. Wenn Sie jedoch Ihre Threads aufeinander abstimmen, befehlen Sie allen Threads, darauf zu warten, bis jeder an die Reihe kommt. Der erste Thread, der den fraglichen Code erreicht, verhängt über ihn eine Sperre, und alle anderen Threads haben diese Sperre zu respektieren. Bis zur Aufhebung der Sperre kann nur der sperrende Thread den Code ausführen. Nun können Sie vorhersehbare Ergebnisse haben, denn man weiß stets, wer der Boss ist. Der Verkehrsunfall in Abbildung 19.5 wird vermieden, statt dessen sieht Ihre Lage nun eher wie in Abbildung 19.6 aus.

636

Eine Multithreading-Anwendung erstellen

Thread 2: setze x = “Eva” Thread 1: setze x = “Chris”

Thread 3: setze x = “Walter”

Abbildung 19.5: Würde man die Threads nicht synchronisieren, könnten sie versuchen, gleichzeitig auf dieselben Objekte zuzugreifen. Das würde zu einem Chaos führen.

? Sensibler Code: setze Meine Variable = x

Threading-Anfänger verwechseln häufig Synchronisierung und threadübergreifendes Marshalling von Befehlen. Bei der Synchronisierung geht es um ein Stück Code, das ausgeführt wird, oder um ein Objekt, auf das ein Thread zugreift – beides findet innerhalb eines einzelnen Threads statt. Beim Marshalling von Befehlen hingegen versucht man, auf ein Objekt oder eine Variable über mehrere Threads hinweg zuzugreifen. Während der fragliche Code versuchen könnte, auf ein bestimmtes Objekt zuzugreifen, kann dieses Objekt trotzdem einem Marshalling von Befehlen unterworfen sein; Synchronisierung und Marshalling sind zwei unterschiedliche Vorgehensweisen. Gehen Sie nicht davon aus, dass Sie die Probleme, die hinsichtlich des Marshallings von Befehlen erörtert wurden, lösen können, indem Sie eine Sperre über ein Objekt verhängen – das wird nicht funktionieren und ergibt im Grunde auch keinen Sinn. Thread 2: setze x = “Eva” Thread 1: setze x = “Chris”

Thread 3: setze x = “Walter”

Sensibler Code: setze Meine Variable = x

Abbildung 19.6: Mit Synchronisierung darf nur noch ein Thread auf einmal eintreten und alle anderen werden ausgesperrt.

Bevor wir untersuchen, wie man Threads synchronisiert (ab Listing 19.4), werfen wir kurz einen Blick auf ein Beispiel, das keinerlei Synchronisierung aufweist. Listing 19.3 erzeugt drei verschiedene Threads, die versuchen, etwas in einem Bezeichnungsfeld anzuzeigen, mit recht interessanten Ergebnissen.

637

Multithreading-Anwendungen

Listing 19.3: Threading ohne Synchronisierung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44:

638

using using using using

System; System.Windows.Forms; System.Drawing; System.Threading;

namespace TYWinforms.Day19 { public class Listing193 : Form { private Label lblInfo = new Label(); private Button btStart = new Button(); public Listing193() { lblInfo.Size = new Size(300,100); lblInfo.Location = new Point(10,75); btStart.Location = new Point(100,10); btStart.Text = "Start!"; btStart.Click += new EventHandler(this.StartIt); this.Text = "Listing 19.3"; this.Controls.Add(lblInfo); this.Controls.Add(btStart); } private void StartIt(Object Sender, EventArgs e) { lblInfo.Text = ""; Thread thdTest1 = new Thread(new ThreadStart(DoIt)); thdTest1.Name = "Thread 1"; thdTest1.Start(); Thread thdTest2 = new Thread(new ThreadStart(DoIt)); thdTest2.Name = "Thread 2"; thdTest2.Start(); Thread thdTest3 = new Thread(new ThreadStart(DoIt)); thdTest3.Name = "Thread 3"; thdTest3.Start(); } private void DoIt() { lblInfo.Text += Thread.CurrentThread.Name + " started...\n"; Thread.Sleep(100); lblInfo.Text += Thread.CurrentThread.Name + " ending...\n"; }

Eine Multithreading-Anwendung erstellen

45: 46: 47: 48: 49:

public static void Main() { Application.Run(new Listing193()); } } }

Dieses Listing gleicht Listing 19.2. Ein Bezeichnungsfeld und eine Schaltfläche werden im Konstruktor in den Zeilen 11 bis 22 angelegt, und der Ereignishandler der Schaltfläche, StartIt, beginnt in Zeile 24. Zeile 26 erzeugt einen neuen Thread und verweist ihn auf die DoIt-Methode. In Zeile 27 wird der Name des Threads auf »Thread 1« festgelegt, danach wird der Thread gestartet. Die Zeilen 30 bis 32 und 34 bis 36 wiederholen diesen Vorgang mit zwei weiteren Threads, die natürlich »Thread 2« und »Thread 3« heißen. Die DoIt-Methode in Zeile 39 ist recht einfach: Sie zeigt etwas Text im Bezeichnungsfeld an, wartet eine Zehntelsekunde und zeigt noch etwas mehr Text an. Wird nun diese Anwendung gestartet, sind die Ergebnisse unvorhersehbar. Klickt man mehrmals auf die Schaltfläche, bekommt man jedes Mal ein anderes Ergebnis. Man kann nie sagen, welcher Thread Text in das Bezeichnungsfeld schreiben wird. Abbildung 19.7 zeigt ein typisches Beispiel dafür. Eigentlich wollen wir wissen, wer beim nächsten Mal Text ausgibt, und noch lieber wäre uns, wenn einer der Threads seine Verarbeitungsaufgabe beenden würde, bevor der nächste Thread startet. Synchronisierung, übernehmen Sie! Die Lösung besteht darin, eine Sperre auf den Code in den Zeilen 40 bis 42 zu legen. Erreicht der erste Thread diesen Code, wird er eine Sperre einrichten, so dass die nächsten beiden Threads in der Warteschlange warten müssen (in der Reihenfolge ihrer Ankunft an der Sperre), bis die Sperre wieder aufgehoben wird. Auf diese Weise hat jeder Thread Zeit, den fraglichen Code vollständig auszuführen, ohne sich darum kümmern zu müssen, was die anderen Threads gerade zu tun versuchen. In .NET gibt es eine Reihe von Möglichkeiten, Threads zu synchronisieren, doch wir besprechen hier nur die vorherrschende, welche den Einsatz der Monitor-Klasse vorsieht. Diese ist recht interessant, da sie weder über Eigenschaften noch Ereignisse verfügt und man sie nicht ausdrücklich anlegen kann. Vielmehr enthält sie sechs statische Methoden, die zur Synchronisierung von Threads benutzt werden. Tabelle 19.1 führt diese Methoden auf.

639

Multithreading-Anwendungen

Abbildung 19.7: Die Reihenfolge, in der Text ausgegeben wird, lässt sich nicht vorhersagen. Methode

Beschreibung

Enter

Legt eine Sperre auf ein Objekt. Ist die Sperre bereits gesetzt, wartet der Thread, bis die Sperre verfügbar wird.

Exit

Hebt die Sperre auf einem Objekt auf.

Pulse

Benachrichtigt einen wartenden Thread von einer Änderung hinsichtlich des Sperrzustands. Pulse lässt also einen anderen Thread, der Wait aufgerufen hat, wissen, dass er nicht mehr lange warten muss.

PulseAll

Benachrichtigt alle wartenden Threads von einer Änderung hinsichtlich des Sperrzustands.

TryEnter

Versucht, eine Sperre für ein Objekt zu erhalten, doch wenn sie bereits gesetzt ist, wartet der Thread nicht und diese Methode liefert false.

Wait

Veranlasst den Verwalter einer Sperre, die Sperre aufzuheben und in die Warteschlange zurückzukehren, um dort auf den Erhalt einer weiteren Sperre zu warten. Wird benutzt, wenn ein Thread von der Ausführung eines anderen abhängt.

Tabelle 19.1: Methoden von Monitor

Die einfachste Möglichkeit, eine Sperre über Code zu verhängen, besteht im Einsatz der Enter- und Exit-Methoden auf den fraglichen Code. Ändern Sie beispielsweise die DoItMethode in Listing 19.3, dass sie wie folgt aussieht: private void DoIt() { Monitor.Enter(this);

640

Eine Multithreading-Anwendung erstellen

lblInfo.Text += Thread.CurrentThread.Name + " started...\n"; Thread.Sleep(100); lblInfo.Text += Thread.CurrentThread.Name + " ending...\n"; Monitor.Exit(this); }

Wir haben zwei neue Zeilen hinzugefügt: Monitor.Enter(this) und Monitor.Exit(this). Wenn Sie jetzt die Anwendung starten, erhalten Sie jedes Mal die gleichen Ergebnisse, wie in Abbildung 19.8 zu sehen (je nach Ihrem System können die Threads 2 und 3 in anderer Reihenfolge erscheinen, doch stets sollten ihre Ausführungszeilen gruppiert sein).

Abbildung 19.8: Die Ausführung jedes Threads wird nun zu einer Gruppe zusammengefasst.

Die Enter- und Exit-Methoden übernehmen als Parameter das Objekt, auf das Sie die Sperre legen wollen. Obwohl also die Sperre einen Codeabschnitt schützt, muss sie weiterhin auf ein echtes Objekt gelegt werden. Meistens dürfte die Variable this (oder Me in VB .NET) genügen. Man kann aber auch die Sperre nur auf das Objekt legen, das man erreichen oder modifizieren möchte. Zum Beispiel so: Monitor.Enter(lblInfo);

Sowohl C# als auch VB .NET haben ihre eigene Syntax für das Einrichten von Sperren. C# verfügt über das Schlüsselwort Lock: lock(this) { ... }

Und VB .NET besitzt das Schlüsselwort SyncLock: SyncLock (Me) ... End SyncLock

641

Multithreading-Anwendungen

Beide Methoden arbeiten auf genau die gleiche Weise wie beim Einsatz der Monitor-Klasse. Tatsächlich hängen sie sogar von dieser ab, um richtig zu funktionieren. Die unterschiedliche Syntax wird benutzt, um Benutzern früherer Versionen dieser Programmiersprachen die Migration zu erleichtern. Schauen wir uns ein etwas realistischeres Beispiel für Multithreading und Synchronisierung an. Listing 19.4 zeigt die Grundlage für eine Anwendung zum Durchsuchen von einer oder mehreren Website(s). Der Benutzer kann Suchbegriffe selbst dann eingeben, wenn der Suchvorgang gerade abläuft. Die Ergebnisse jedes Suchvorgangs werden in einem normalen Label-Steuerelement angezeigt. Listing 19.4: Eine synchronisierte Multithreading-Suchanwendung in VB .NET 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:

642

Imports Imports Imports Imports

System System.Windows.Forms System.Drawing System.Threading

namespace TYWinforms.Day19 public class Listing194 : Inherits Form private lblInfo As new Label() private tbSearch As new TextBox() private btStart As new Button() private intCounter As integer public sub New() intCounter = 0 lblInfo.Size = new Size(300,100) lblInfo.Location = new Point(10,75) tbSearch.Location = new Point(10,10) btStart.Location = new Point(120,10) btStart.Text = "Suche!" AddHandler btStart.Click, new EventHandler(AddressOf StartIt) Me.Text = "Listing 19.4" Me.Controls.Add(lblInfo) Me.Controls.Add(btStart) Me.Controls.Add(tbSearch) End Sub private sub StartIt(Sender As Object, e As EventArgs) intCounter += 1

Eine Multithreading-Anwendung erstellen

33: 34: dim thdSearch As New Thread(new ThreadStart (AddressOf Search)) 35: thdSearch.Name = "Suche " & intCounter 36: thdSearch.Start() 37: End Sub 38: 39: private sub Search() 40: ‘Suchvorgang durchführen 41: Thread.Sleep(2000) 42: 43: Monitor.Enter(Me) 44: 45: dim rnd As new Random() 46: dim i as integer 47: for i = 0 to 4 48: lblInfo.Text += Thread.CurrentThread.Name & ": " & rnd.Next &  Microsoft.VisualBasic.vbCrLf 49: next i 50: 51: Monitor.Exit(Me) 52: End Sub 53: 54: public shared sub Main() 55: Application.Run(new Listing194()) 56: End Sub 57: End Class 58: End Namespace

Der Aufbau dieses Listings ist im Grunde der gleiche wie in den Listings 19.2 und 19.3. In der Benutzeroberfläche liegen ein Bezeichnungsfeld, eine Schaltfläche und ein Textfeld vor. Der Click-Ereignishandler der Schaltfläche ist die StartIt-Methode in Zeile 31. In StartIt erhöhen Sie einen Zähler, der dem Benutzer mitteilt, wie viele Suchvorgänge bereits durchgeführt wurden (Zeile 32), und erzeugen einen neuen Thread, dessen Name auf diesem Zählerstand basiert (Zeilen 34 bis 35). Sobald die SUCHEN-Schaltfläche angeklickt wurde, wird jedes Mal ein neuer Thread erzeugt und zusammen mit der Search-Methode ab Zeile 39 gestartet. Den Suchmechanismus haben wir hier eigentlich nicht erstellt (das ist Ihre Hausaufgabe), aber in Zeile 41 legen wir den Thread für zwei Sekunden schlafen, um die Suche zu simulieren. Beachten Sie, dass dieser Befehl sich außerhalb jeglichen gesperrten Codes befindet. Befände sich dieser Code innerhalb eines Sperrbereichs, dann müsste jeder Suchvorgang so lange warten, bis der vorhergehende Suchvorgang beendet ist, damit er selbst mit der Suche beginnen könnte. Dies widerspräche aber dem Zweck einer Multithreaded-Suchan-

643

Multithreading-Anwendungen

wendung, denn wir wollen die Suchvorgänge ja parallel ausführen lassen, so dass wir eine höhere Reaktionsgeschwindigkeit der Anwendung erzielen. Die Zeilen 49 bis 51 werden nach dem Abschluss der Suchvorgänge ausgeführt und zeigen lediglich eine Meldung im Bezeichnungsfeld an, wie viele Ergebnisse geliefert wurden. Weil die Möglichkeit besteht, dass viele Threads das Label-Steuerelement auf einmal ändern wollen (und weil die Suchergebnisse gruppiert werden sollen), platzieren wir diesen Code mit Hilfe der Methode Monitor.Enter unter eine Sperre. Um die Anzahl der gelieferten Suchergebnisse zu simulieren, wird mit Hilfe der Random-Klasse eine Zufallszahl generiert und im Label-Steuerelement angezeigt. Zum Schluss wird in Zeile 51 die Exit-Methode aufgerufen. Hätten wir nicht die Sperrmechanismen eingesetzt, könnten wir nicht sicher sein, dass die Ergebnisse jedes Suchvorgangs gruppiert würden. Abbildung 19.9 zeigt ein Beispielergebnis.

Abbildung 19.9: Eine Multithreading-Suchfunktion muss das Bezeichnungsfeld, das die Suchergebnisse ausgibt, sperren können.

Es gibt außer Monitor zwei weitere gängige Methoden, Code zu sperren. Die erste basiert auf der Klasse Mutex, die ähnlich wie die Monitor-Klasse arbeitet, aber weitaus weniger Funktionsumfang bereitstellt. Zur Synchronisierung mit Mutex gehört das Übergeben eines bestimmten Mutex-Objekts zwischen einzelnen Threads; der Thread, der gerade das MutexObjekt hat, ist der einzige, dem die Ausführung von Code gestattet ist. Die zweite Methode basiert auf dem Einsatz der ReaderWriterLock-Klasse. Je nachdem, was Ihre Anwendung tun muss, setzt diese Klasse zwei verschiedene Arten von Sperren ein. Sie bietet eine Sperre für einen Writer und mehrere Reader, was bedeutet, dass nur ein einziger Thread gesperrte Objekte modifizieren kann, doch alle anderen Threads weiterhin

644

Eine Multithreading-Anwendung erstellen

darauf zugreifen können (solange sie nicht versuchen, die Objekte zu verändern). Je nach Lage der Dinge kann dieses Vorgehen die Leistung Ihrer Anwendung erhöhen. (Mehr Informationen über diese Sperrtypen finden Sie in der Dokumentation zum .NET Framework.)

Threadpooling einsetzen Ein Threadpool ist einfach eine Menge von inaktiven Threads, und Threadpooling ist der Vorgang, diese Threads einzusetzen. In einem Multithreading-Betriebssystem ist es eine Tatsache, dass ein Thread die meiste Zeit mit Nichtstun verbringt; jeder Thread wartet entweder darauf, eine Sperre auf ein Objekt legen zu können oder dass ein Ereignis eintritt. Während dieses Müßiggangs ist auch der Prozessor meist zum Nichtstun verurteilt; dennoch muss das Betriebssystem Kapazitäten für die Aufrechterhaltung des Threads reservieren, doch da dieser nichts tut, erzielt dies auch keinen Leistungszuwachs für Ihre Anwendung. Technisch ausgedrückt, erlaubt Threadpooling dem Betriebssystem, die Threads zu kontrollieren. Verfällt ein Thread in Müßiggang und der Prozessor hat nichts mehr zu tun, gestattet das Betriebssystem einem anderen Thread, an die Stelle des ersten zu rücken, um so die CPU auf Trab zu halten. Threadpooling ist ein ganz normaler Vorgang in Ihrem Betriebssystem. Man kann es für seine Anwendungen ausnutzen, so dass man als Entwickler nicht mehr selbst alle Threads einzeln verwalten muss; Erstellung, Aufrechterhaltung und Zerstörung von Threads handhabt das Betriebssystem. Man teilt diesem einfach mit, welcher Code mit einem Thread ausgeführt werden soll, und es handhabt den Prozess, einen bestimmten Thread zuzuweisen und sobald diese Aufgabe ausgeführt ist, diesen Thread wieder loszuwerden. Darin ist das Betriebssystem ziemlich effizient. Um Threadpooling in Anwendungen auszunutzen, setzt man die ThreadPool-Klasse ein. Mit Hilfe der statischen QueueUserWorkItem-Methode können Sie dem Betriebssystem sagen, dass Sie für eine bestimmte Aufgabe einen Thread benötigen. Das System handhabt alle Zuweisungs- und Ausführungsaufgaben bezüglich Threads selbst. Wenn der Thread mit seiner Arbeit fertig ist, kann er Ihren Hauptthread entsprechend benachrichtigen. Der Unterschied zwischen dem Einsatz eines ThreadPool-Objekts und normalen ThreadObjekten besteht darin, dass die Thread-Klasse mehr Flexibilität bietet. Man kann so etwa seine Threads benennen und sie mit Thread individuell identifizieren. Beim Einsatz von ThreadPool kann man eigentlich nichts direkt mit bestimmten Threads tun, doch es ist im Allgemeinen effizienter hinsichtlich der Aufrechterhaltung von Threads. Bevor wir uns ein Beispiel anschauen, müssen Sie über den Delegaten WaitCallBack Bescheid wissen, einem Helfer der ThreadPool-Klasse. WaitCallBack gleicht dem ThreadStart-Delegaten: Er verweist einen Thread auf eine auszuführende Methode. Der einzige

645

Multithreading-Anwendungen

Unterschied: Die so von WaitCallBack angezeigte Methode muss ein Objekt als Parameter übernehmen. Dieses Objekt lässt sich unter anderem dazu verwenden, einen bestimmten Thread darzustellen (mehr dazu später). Listing 19.5 zeigt ein einfaches Beispiel, wie man die ThreadPool-Klasse einsetzt. Listing 19.5: ThreadPool verwenden 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: Imports System.Threading 5: 6: namespace TYWinforms.Day19 7: public class Listing195 : Inherits Form 8: private lblInfo As new Label() 9: private btStart As new Button() 10: 11: public sub New() 12: lblInfo.Size = new Size(300,200) 13: lblInfo.Location = new Point(10,75) 14: 15: btStart.Location = new Point(100,10) 16: btStart.Text = "Start!" 17: AddHandler btStart.Click, new EventHandler(AddressOf StartIt) 18: 19: Me.Text = "Listing 19.5" 20: Me.Controls.Add(lblInfo) 21: Me.Controls.Add(btStart) 22: End Sub 23: 24: private sub StartIt(Sender As Object, e As EventArgs) 25: dim i as integer 26: 27: for i = 0 to 10 28: ThreadPool.QueueUserWorkItem(new WaitCallBack (AddressOf DoIt)) 29: next i 30: End Sub 31: 32: private sub DoIt(state As Object) 33: lblInfo.Text += "Gruss aus dem Threadpool!" &  Microsoft.VisualBasic.vbCrLf 34: End Sub 35: 36: public shared sub Main() 37: Application.Run(new Listing195())

646

Eine Multithreading-Anwendung erstellen

38: 39: 40:

End Sub End Class End Namespace

Dieses Listing verwendet das gleiche Gerüst wie alle anderen Listings heute. Nur durch die StartIt-Methode in Zeile 24 und die DoIt-Methode in Zeile 32 unterscheidet es sich etwas. In der StartIt-Methode erzeugen Sie eine einfache Schleife. Diese ruft die Methode QueueUserWorkItem elfmal auf, wobei sie jedes Mal einen WaitCallBack-Delegaten übergibt, der auf die DoIt-Methode weist. Als Ergebnis schnappt sich das ThreadPool-Objekt elf Threads, von denen jeder die jeweilige DoIt-Methode so effizient wie möglich ausführt. Diese schreibt lediglich eine Meldung in das Label-Steuerelement (Zeile 33). Abbildung 19.10 zeigt das Ergebnis dieses Listings.

Abbildung 19.10: ThreadPool reiht jeden Thread in eine Warteschlange ein und steuert ihn.

Jeder von der ThreadPool-Klasse gehandhabte Thread wird automatisch gestartet, aufrechterhalten, gestoppt und (notfalls) zerstört. Mit Hilfe der ThreadPool-Klasse kann man die Zeilen 32 bis 36 des Listings 19.4 mit nur einer Zeile ersetzen: ThreadPool.QueueUserWorkItem(new WaitCallBack(AddressOf Search))

Ihre Anwendung wird effizienter ausgeführt. Auch die erwähnten Synchronisierungstechniken gelten noch, wenn man die ThreadPool-Klasse einsetzt, so dass man nichts zusätzlich tun muss. Erinnern Sie sich an den Parameter für die DoIt-Methode in Zeile 32 des Listings 19.5? Sie können ihn für fast alles einsetzen, was Sie wollen: Es handelt sich um ein generisches

647

Multithreading-Anwendungen

Objekt, das übergeben wird, um Ihren Threads zu »helfen«. Um einen echten Wert dafür festzulegen, muss man den Aufruf von QueueUserWorkItem ein klein wenig ändern. Wir untersuchen zwei Einsatzmöglichkeiten für dieses Objekt. Erstens lässt sich das Objekt einsetzen, um einen bestimmten Thread zu identifizieren. Ändern Sie z.B. Zeile 28 so, dass sie wie folgt lautet: ThreadPool.QueueUserWorkItem(new WaitCallBack(AddressOf DoIt), i.ToString())

Wir übergeben der QueueUserWorkItem-Methode einen zusätzlichen Parameter. Da die DoIt-Methode einen Object-Datentyp erwartet, können wir etwas Beliebiges übergeben (da alle Typen in .NET von Object abgeleitet sind). i ist die Schleifenzählervariable, die wir verwendet haben, um elf Threads zu erzeugen. Diese neue Zeile übergibt eine Zeichenfolgendarstellung der aktuellen Threadanzahl in der Warteschlange. Ändern Sie Zeile 33 ein wenig: lblInfo.Text +="Hello from thread" &&state.ToString()&Microsoft.VisualBasic.vbCrLf

Der im Aufruf der QueueUserWorkItem-Methode übergebene Wert wird in den State-Parameter der DoIt-Methode geleitet. Hier rufen wir lediglich noch einmal die ToStringMethode auf, um das Object in eine Zeichenfolge umzuwandeln und eine für jeden Thread individuelle Zeichenfolge auszugeben. Die Bildschirmausgabe sieht auf Grund dieser Änderungen so aus: Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello

from from from from from from from from from from from

thread thread thread thread thread thread thread thread thread thread thread

0 1 2 3 4 5 6 7 8 9 10

Sie verfügen nun über eine etwas rudimentäre Möglichkeit, jeden einzelnen Thread zu identifizieren. Die zweite Einsatzmöglichkeit für den state-Parameter besteht darin, andere Threads davon in Kenntnis zu setzen, dass ein Thread mit seiner Ausführung fertig ist. Dazu wird die Klasse AutoResetEvent eingesetzt. AutoResetEvent bietet Methoden, die einen Thread veranlassen, so lange auf einen anderen zu warten, bis ein Flag gesetzt ist. Stellen Sie sich AutoResetEvent wie eine Art Toaster vor. Der wartende Thread muss abwarten, dass der Toast in die Höhe schießt, bis er mit seiner Aufgabe fortfahren kann. Das Listing 19.6 zeigt ein Beispiel.

648

Eine Multithreading-Anwendung erstellen

Listing 19.6: Threads in einem Threadpool benachrichtigen 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: Imports System.Threading 5: 6: namespace TYWinforms.Day19 7: public class Listing196 : Inherits Form 8: private lblInfo As new Label() 9: private btStart As new Button() 10: private objAREvent As new AutoResetEvent(false) 11: private intCounter As Integer = 10 12: 13: public sub New() 14: lblInfo.Size = new Size(300,200) 15: lblInfo.Location = new Point(10,75) 16: 17: btStart.Location = new Point(100,10) 18: btStart.Text = "Start!" 19: AddHandler btStart.Click, new EventHandler (AddressOf StartIt) 20: 21: Me.Text = "Listing 19.6" 22: Me.Controls.Add(lblInfo) 23: Me.Controls.Add(btStart) 24: End Sub 25: 26: private sub StartIt(Sender As Object, e As EventArgs) 27: dim i as integer 28: 29: for i = 0 to 10 30: ThreadPool.QueueUserWorkItem(new WaitCallBack(AddressOf DoIt),  objAREvent) 31: next i 32: 33: objAREvent.WaitOne() 34: lblInfo.Text += "Fertig!" 35: End Sub 36: 37: private sub DoIt(state As Object) 38: Thread.Sleep(2000) 39: lblInfo.Text += "Arbeite..." & Microsoft.VisualBasic.vbCrLf 40: 41: if intCounter = 0 then 42: CType(state, AutoResetEvent).Set() 43: else

649

Multithreading-Anwendungen

44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54:

Monitor.Enter(Me) intCounter = intCounter - 1 Monitor.Exit(Me) end if End Sub public shared sub Main() Application.Run(new Listing196()) End Sub End Class End Namespace

Dieses Gerüst sehen wir uns nun ein letztes Mal an. In den Zeilen 10 und 11 erzeugen wir zwei neue Variablen: ein AutoResetEvent-Objekt und einen Zähler, der angibt, wie viele Threads Sie in dieser Anwendung erzeugen. Der Parameter für den AutoResetEvent-Konstruktor zeigt den Anfangszustand des Objekts an; wird false übergeben, so heißt das, dass es noch nicht bereit ist (der Toast wurde noch nicht ausgeworfen). Die nächste Veränderung gegenüber Listing 19.5 findet sich in Zeile 30. Nun übergeben wir das neu erzeugte AutoResetEvent-Objekt der QueueUserWorkItemMethode. Die DoIt-Methode in Zeile 37 kann auf das AutoResetEvent-Objekt mit Hilfe ihres state-Parameters zugreifen. In Zeile 33 wird die WaitOne-Methode des AutoResetEvent-Objekts aufgerufen. Dies bringt den Hauptthread dazu zu warten, bis das AutoResetEvent-Objekt ihm sagt, dass er weitermachen kann (wenn der Toast ausgeworfen wird). Das Signal dazu wird gegeben, indem die Set-Methode des AutoResetEvent-Objekts aufgerufen wird (siehe nächster Absatz). Nun wird gewartet, bis alle anderen Threads ihre Ausführung beendet haben, bevor der Hauptthread fortgesetzt wird. Die StartIt-Methode zeigt im Bezeichnungsfeld etwas Text an, der angibt, dass alle Ausführungen beendet wurden. Zeile 38 legt den Thread für zwei Sekunden schlafen und etwas Text wird im Bezeichnungsfeld angezeigt (Zeile 39). Die Zeilen 41 bis 47 enthalten etwas Interessantes. Wenn alle Threads beendet wurden, wollen wir die Set-Methode des AutoResetEvent-Objekts aufrufen. Die Anzahl der aktuell laufenden Threads wird durch die intCounter-Variable angegeben. Gibt es noch laufende Threads (dann ist intCounter größer null), müssen wir intCounter jedes Mal um eins verringern, wenn ein Thread seine Arbeit beendet hat. Man sollte hier nicht den Einsatz von Synchronisierung vergessen; mehr als ein Thread greift auf die intCounter-Variable zu, so dass man einige Sperren benötigt (Zeile 44), um sicherzustellen, dass nur ein Thread nach dem anderen auf intCounter zugreift. Wird schließlich der letzte Thread erreicht, rufen wir die Set-Methode auf, und der Hauptthread aus Zeile 34 wird wieder aufgenommen.

650

Eine Multithreading-Anwendung erstellen

(Vergessen Sie bitte nicht, dass diese Anwendung immer noch dem Marshalling von Befehlen unterworfen ist, da das verwendete Label-Steuerelement nicht threadsicher ist. Daher wird im Bezeichnungsfeld erst eine Ausgabe zu sehen sein, wenn alle Threads ihre Ausführung beendet haben.) Abbildung 19.11 zeigt die Ausgabe aus Listing 19.6.

Abbildung 19.11: Die Klasse AutoResetEvent lässt sich dazu einsetzen, den Hauptthread zu kennzeichnen, nachdem alle anderen Threads ihre Ausführung beendet haben. ThreadPool bietet eine leichte und effiziente Möglichkeit, in einer Anwendung mehrere Threads einzusetzen, indem es untätige Threads nutzt. Mit der Methode ThreadPool.GetMaxThreads kann man herausfinden, wie viele Threads sich maximal gleichzeitig ausführen lassen (wie viele Threads also für sofortigen Einsatz zur Verfügung stehen). ThreadPool.GetAvailableThreads zeigt die Anzahl zusätzlicher Threads an, die man in die Warte-

schlange stellen kann, bis einem die untätigen Threads ausgehen. Ist diese Anzahl erreicht, warten Schlange stehende Elemente, bis ein weiterer Thread verfügbar wird. Ich empfehle den Einsatz des ThreadPool-Objekts, es sei denn, man benötigt die Flexibilität, die das Arbeiten mit individuellen Thread-Objekten erlaubt. Ich empfehle

Bitte beachten Sie

Verwenden Sie das ThreadPool-Objekt, wenn Sie kleine Codeblöcke effizient mit mehreren Threads ausführen müssen.

Setzen Sie ThreadPool nicht ein, wenn Sie einzelne Threads bearbeiten müssen (etwa um Prioritäten oder Namen festzulegen) oder wenn Ihre Threads ein sehr umfangreiches und kompliziertes Stück Code ausführen müssen. In solchen Fällen ist es effizienter, Threads selbst zu handhaben.

651

Multithreading-Anwendungen

Multithreaded Steuerelemente Ein multithreaded Steuerelement lässt sich ebenso leicht wie eine multithreaded Anwendung erstellen. Man legt eine neue Klasse an, die von der Klasse Control erbt, und folgt dann den Richtlinien für den Einsatz von Threads. Sie können Ihre Steuerelemente weitaus reaktionsfreudiger machen, wenn Sie mehrere Threads einsetzen. So könnten Sie etwa die Multithreading-Suchanwendung als benutzerdefiniertes Steuerelement kapseln. Anwendungen, die solche Steuerelemente verwenden, brauchen sich um Multithreading-Probleme nicht zu kümmern, denn diese Funktionalität ist ja im Steuerelement selbst gekapselt. Die Erstellung von multithreaded Steuerelementen heißt jedoch nicht, dass man die Beschränkungen, die einem threadübergreifendes Marshalling auferlegt, überwinden kann. Leider ist alles, das von Control abgeleitet ist, auf einen einzelnen Thread beschränkt, wird es in einer Anwendung eingesetzt. Alle anderen Threads müssen weiterhin Aufrufe an den erzeugenden Thread marshallen. Kurz und gut: Wenn Sie wissen, wie Sie benutzerdefinierte Steuerelemente erstellen und mehrere Threads einsetzen, können Sie bereits auch multithreaded Steuerelemente anlegen.

19.3 Zusammenfassung Wir haben heute eine Menge komplizierter Themen besprochen. Üblicherweise ist das Thema Multithreading nur fortgeschrittenen Benutzern vorbehalten, doch mit .NET und Ihrem Wissen über Windows Forms lässt sich Multithreading leicht bewältigen. Ein Thread ist eine grundlegende Ausführungseinheit in einem Betriebssystem. Das Betriebssystem reserviert Kapazitäten für die Ausführung und Aufrechterhaltung von Threads, während die CPU ihre Zeit auf mehrere Threads, die in der Anwendung laufen, verteilt. Weil der Prozessor so schnell ist, sieht es so aus, als ob man mehrere Anwendungen gleichzeitig ausführte, doch in Wahrheit führt die CPU nur eine nach der anderen aus, wenn auch sehr rasch. In .NET dreht sich Threading vor allem um die Thread-Klasse. Damit können Sie neue Threads erzeugen, Prioritäten und Namen zuweisen, Threads starten und beenden usw. Einem Thread muss ein Code zur Ausführung übergeben werden und dies erfolgt mit dem ThreadStart-Delegaten. Synchronisierung ist der Vorgang, mit dem sichergestellt wird, dass nur ein Thread ein bestimmtes Codeelement auf einmal ausführt. Üblicherweise setzt man Synchronisierung ein, wenn das fragliche Codeelement sensibel ist; das heißt, wenn es auf solche Objekte

652

Fragen und Antworten

zugreift und sie verändert, von denen die Anwendung abhängt. Für die Synchronisierung sperrt man seine Objekte mit Hilfe der Monitor-Klasse und ihrer Enter- und Exit-Methoden. Die ThreadPool-Klasse nutzt die Art und Weise aus, wie Threads arbeiten, um ihre Multithreading-Aufgaben effizienter zu erledigen. Die Hauptmethode dieser Klasse ist QueueUserWorkItem, welche einen WaitCallBack-Delegaten benutzt, um einen Thread zu starten. Sie können eine AutoResetEvent-Klasse verwenden, um Threads zu signalisieren, dass einer von ihnen seine Aufgabe beendet hat.

19.4 Fragen und Antworten F

Wie funktioniert Threading auf Multiprozessorsystemen? A

F

Verfügt Ihr Rechner über mehrere Prozessoren, so nutzen diese Threads nicht gemeinsam; vielmehr erhält jeder Prozessor einen gewissen Prozentsatz der Gesamtzahl aller Threads und arbeitet nur mit diesem Kontingent. Wie Threading im Einzelnen implementiert wird, hängt von Ihrer jeweiligen Plattform ab.

Was versteht man unter einer statischen Threadvariablen? A

Statische Threadvariablen gleichen normalen statischen Variablen. Letztere repräsentieren jeweils eine Variable oder Eigenschaft, die allgemein auf eine Klasse anwendbar ist, statt auf eine spezielle Instanz einer Klasse. Statische Threadvariablen lassen sich auf alle Threads anwenden, statt nur auf eine bestimmte Instanz eines Threads. Verwendet man eine statische Threadvariable, erhält jeder Thread, der auf diese Variable zugreift, sein eigenes Exemplar davon. Das heißt, wenn ein Thread die Variable verändert, beeinflusst dies nicht die Variable in anderen Threads. Statische Threadvariablen sind nützlich, wenn man weiß, dass eine bestimmte Variable in jedem einzelnen Thread einzigartig sein sollte. Der Einsatz statischer Threadvariablen ist effizienter als etwa neue lokale Variablen für jeden Thread zu erstellen. Sie können eine statische Threadvariable einfach dadurch deklarieren, dass Sie der Deklaration das ThreadStatic-Attribut voranstellen: [ThreadStatic] int intValue; dim intValue as integer

// C# ' VB .NET

653

Multithreading-Anwendungen

19.5 Workshop Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Schließen Sie auf drei Funktionen in Microsoft Word, die multithreaded sein könnten. 2. Was versteht man darunter, wenn man etwas als nicht threadsicher bezeichnet? 3. Aus welchem Grund kann die ThreadPool-Klasse Aufgaben effizienter ausführen? 4. Wahr oder falsch? Der Monitor.Enter-Methode muss man die this-Variable übergeben. 5. Ist der folgende Code korrekt? Monitor.Enter(this) for i = 0 to 10 lblInfo.Text += "hallo!" next i

6. Welches der folgenden Statements ist korrekt? 1. ThreadPool.QueueUserWorkItem(new ThreadStart(this.Go)); 2. ThreadPool.QueueWorkUserItem(new WaitCallBack(this.Go)); 3. ThreadPool.QueueUserWorkItem(new WaitCallBack(this.Go));

7. Was ist ThreadStart? 8. Wahr oder falsch? Ein Thread beginnt mit seiner Ausführung, sobald man einen ThreadStart-Delegaten spezifiziert hat. 9. Welches ist der standardmäßige ThreadState- und ThreadPriority-Wert für einen neu gestarteten Thread?

Übung Erstellen Sie ein Multithreading-Dateisuchprogramm. In dieser Anwendung soll ein Benutzer einen Dateinamen als Suchbegriff eingeben und auf die SUCHEN-Schaltfläche klicken können, so dass ein neuer Thread erzeugt wird. Dann wird rekursiv das Dateisystem nach dem angegebenen Dateinamen durchsucht. Zeigen Sie die Ergebnisse in einem ListBox-Steuerelement an. (Nähere Informationen zum Algorithmus für das Durchsuchen von Verzeichnissen finden Sie an Tag 11).

654

Windows Forms konfigurieren und bereitstellen

0 2

Windows Forms konfigurieren und bereitstellen

Die letzten Schritte in der Anwendungsentwicklung bestehen in der Konfiguration und Bereitstellung. Zur Konfiguration gehört sicherzustellen, dass die richtigen Einstellungen benutzt werden, dass die Versionsnummern zutreffen usw. Die Bereitstellung bzw. das Deployment besteht darin, Ihre neue Anwendung auf die Computer der Benutzer zu bringen. Die heutige Lektion wird sich intensiv mit Konfiguration und Bereitstellung befassen. Wie Sie erkennen werden, ist keines dieser Themen kompliziert, und Bereitstellung ist sogar recht einfach. Sie werden eine Reihe neuer Technologien kennen lernen, die Sie bei Ihren Vorhaben unterstützen. Darüber hinaus beschäftigt sich dieses Kapitel mit Lokalisierung und Eingabehilfen, zwei Schritten, die häufig übersehen werden. Die Lokalisierung einer Anwendung versetzt sie in die Lage, in verschiedenen Kulturen und Sprachen zu funktionieren. Eingabehilfen stellen behinderten Benutzern Hilfen zur Verfügung, mit denen auch sie Ihre Anwendung bedienen können. Heute lernen Sie, 쐽

was .config- und .resources-Dateien sind,



in welcher Weise man die ResourceManager-Klasse einsetzt,



wie man im Code die Applikationslogik von Aspekten der Benutzeroberfläche trennt,



die verschiedenen Einstellungen kennen, die Ihren Anwendungen im .NET Framework zur Verfügung stehen,



wo Sie Ihre Anwendung auf dem Computer des Benutzers installieren.

20.1 Einführung in die .NET-Konfiguration Denkt man an die Konfiguration einer Anwendung, so stellen sich die meisten Menschen Merkmale der Benutzeroberfläche vor. Wenn man beispielsweise Microsoft Word einsetzt, verstehen viele Benutzer unter »Konfiguration« das Einstellen der Schriftart, Nummerierung, Tabellen usw. Auch das Einstellen der Seitenränder und das Justieren der Druckereinstellungen mag dazu gehören. Da der Entwickler zuvor nicht weiß, welchen Drucker der Endanwender einsetzt, bleiben Aufgaben wie diese dem Benutzer überlassen. Das soll aber nicht heißen, dass sich der Entwickler keine Gedanken über Konfigurationsprobleme machen müsste. Vielmehr sollte er eine Reihe von Aspekten vorbereiten, damit die Anwendung schließlich sauber läuft. Zwei Beispiele dieser vorzubereitenden Aspekte sind Sicherheit und Versionsinformation. Häufig ist es notwendig, eine Anwendung mit standardmäßigen Konfigurationsdaten zu bündeln.

656

Einführung in die .NET-Konfiguration

Vor der .NET-Einführung gab es zwei verbreitete Methoden, Konfigurationsdaten zu liefern. Die erste bestand darin, die Windows-Registrierung zu nutzen, ein zentrales Ablagesystem für Daten, die das Betriebssystem anfordert. Diese Registrierungsdatenbank enthielt Informationen über jedes Programm auf dem Rechner, zusätzlich zu Benutzer- und Betriebssystemdaten. Installierte man eine Anwendung das erste Mal, schrieb sie Daten in die Registrierung, auf die diese dann später bei der Ausführung des Programms zurückgriff (siehe Abbildung 20.1). Bei Bedarf konnten Änderungen in die Registrierung zurückgeschrieben werden, um die Einstellungen für die Anwendung zu modifizieren. Bei diesem Vorgehen traten mehrere Probleme auf. Zum einen lässt sich die Registrierung nur relativ schwer editieren. Sie enthält einen Überfluss an (sensiblen) Informationen, die zuweilen so aufgebaut sind, dass sie sich dem unmittelbaren Verständnis entziehen. Das Ansinnen, der Benutzer solle seine eigenen Registrierungseinstellungen bearbeiten, kann man sofort vergessen. In der Mehrzahl der Fälle, in der Benutzer dies taten, richteten sie ein Chaos auf ihrem Rechner an, weil sie kritische Informationen zusammen mit nicht-kritischen Anwendungsdaten speicherten. Waren einige Einstellungen zu ändern oder neue hinzuzufügen, so hatte man zudem die Anwendung zu verändern und neu zu kompilieren – das ist nicht immer eine einfache Aufgabe, besonders dann, wenn man gerade keinen Entwickler zur Hand hat.

Abbildung 20.1: Der Registrierungs-Editor (regedit.exe) zeigt die Informationen in der WindowsRegistrierungsdatenbank vollständig an (im Bild eine Konfiguration für Word 2000).

Die zweite gängige Methode bestand im Einsatz von Konfigurationsdateien. In Windows haben diese meist die Erweiterung .ini. Diese Dateien sorgten für eine Menge Schwierigkeiten, die mit der Registrierung zusammenhingen; in der Regel waren .ini-Dateien leicht zu lesen und zu ändern. Eine Anwendung wurde mit einer oder mehreren .ini-Dateien geliefert, welche die Einstellungen enthielten, die die Anwendung benötigte. Listing 20.1 zeigt eine typische .ini-Datei für Adobe Photoshop.

657

Windows Forms konfigurieren und bereitstellen

Listing 20.1: Konfigurationsinformationen für Photoshop 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

[Startup] AppName=Adobe Photoshop 6.0 FreeDiskSpace=2441 EnableLangDlg=Y [ISUPDATE] UpdateURL=http:// [AdobePI] ProductName=Photoshop ProductType=Retail ProductVersion=6.0 ProductLanguage=Not Applicable PIVersion=1.0 PIValue=1869701

Informationen wie etwa der Anwendungsname, die Version und die Sprache sind in der .ini-Datei enthalten. Jedes Mal wenn Photoshop gestartet wird, lässt sich das Programm mit diesen Informationen richtig konfigurieren. .NET entwickelt das .ini-Konzept einen Schritt weiter und macht aus allen Konfigurationsdateien XML-Dateien. Jetzt sind nicht nur die Konfigurationseinstellungen leicht zu lesen, sondern liegen auch in einem Standardformat vor, das es Anwendungen erleichtert, solche Daten zu lesen und zu schreiben. Der Name der Konfigurationsdatei muss mit dem der zu konfigurierenden Anwendung übereinstimmen. Hieße die Anwendung etwa Listing20.1.exe, müsste die zugehörige Konfigurationsdatei Listing20.1.exe.config heißen. Wie Sie im Verlauf der letzten 19 Tage gelernt haben, müssen Anwendungen allerdings nicht unbedingt Konfigurationsdateien aufweisen (gleich mehr dazu). Listing 20.2 zeigt ein typisches Beispiel. Listing 20.2: Eine typische .config-Datei 1: 2: 3: 4: 5: 6: 7: 8: 9:

658





So konfigurieren Sie Ihre Anwendungen

Im Augenblick brauchen Sie hierzu nur zu wissen, dass .NET mehrere XML-Elemente in der .config-Datei definiert. Sie können außerdem Ihre eigenen benutzerdefinierten Konfigurationsdateien erzeugen, damit Ihre Anwendung jede Einstellung findet, die sie braucht. Auch das .NET Framework hat eine Konfigurationsdatei. Sie ist normalerweise im Verzeichnis c:\winnt\Microsoft.NET\Framework\version\CONFIG abgelegt. Sie trägt den Namen machine.config, da sie die Einstellungen enthält, die der gesamte Computer verwendet. Wenn man keine eigenen anwendungsspezifischen Einstellungen angibt, so erbt die Anwendung solche von machine.config (das ist auch der Grund, warum nicht jede Ihrer Anwendungen eine .config-Datei benötigt). Diese Datei umfasst Informationen darüber, welche Klassen sich um Sicherheitsaspekte kümmern, welche Dateien mit der ASP.NETEngine verknüpft sind, welche Sprache und Kultur der Benutzer auf seinem Computer als Standard eingetragen hat, wie Debugging zu handhaben ist usw. Die Datei machine.config ist recht umfangreich, da sie Einstellungen für jeden Aspekt im .NET Framework umfasst. (Standardmäßig sind es mehr als 750 Zeilen.) Dieses Buch geht auf die Datei nicht näher ein, aber sehen Sie die Datei ruhig einmal durch.

20.2 So konfigurieren Sie Ihre Anwendungen Bevor man mit der Erzeugung von Konfigurationsdateien beginnen kann, sollte man zuerst eine zu konfigurierende Anwendung dafür haben. Listing 20.3 zeigt die Grundstruktur für eine Anwendung mit einem INFO-Menü. Listing 20.3: Eine konfigurationsfertige Anwendung 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.Day20 { public class Listing203 : Form { private MainMenu mnuMain = new MainMenu(); private MenuItem miHelp = new MenuItem("Hilfe"); private MenuItem miAbout = new MenuItem("Info"); public Listing203() { mnuMain.MenuItems.Add(miHelp); miHelp.MenuItems.Add(miAbout); miAbout.Click += new EventHandler(this.ShowAbout);

659

Windows Forms konfigurieren und bereitstellen

17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:

this.Text = "Meine konfigurierbare Anwendung"; this.Menu = mnuMain; } private void ShowAbout(Object Sender, EventArgs e) { MessageBox.Show("TYWinforms Listing 20.3 Beispiel"); } public static void Main() { Application.Run(new Listing203()); } } }

Die Anwendung weist nur drei Steuerelemente auf: zwei MenuItems und ein MainMenu. Das INFO-Menüelement verfügt über einen Ereignishandler, der nur eine MessageBox mit einer netten Beschreibung des Programms anzeigt. Speichern Sie dieses Listing als Listing20.3.cs und kompilieren Sie es. Sie erhalten eine Datei namens Listing20.3.exe. Die entsprechende Konfigurationsdatei sollte demnach Listing20.3.exe.config heißen. Die Konfigurationsdatei muss im selben Ordner wie die ausführbare Datei liegen. War Ihre Anwendung im Verzeichnis c:\winforms\day20, so muss die Datei Listing20 .3.exe.config im selben Ordner liegen.

Konfigurationsabschnitte Die einfachste .config-Datei sieht so aus:

Das einzige erforderliche XML-Tag ist das -Element. Alle anderen Einstellungen sind – jeweils in ihren eigenen Elementen – innerhalb von platziert. So ist etwa ein weiteres gebräuchliches Konfigurationselement. Fügt man es dem vorigen Code hinzu, sieht das so aus:



660

So konfigurieren Sie Ihre Anwendungen

Bei der Erstellung dieser .config-Dateien sind einige Vorsichtsmaßnahmen zu ergreifen. Da es sich um XML-Dateien handelt, erfordern sie strikt angewandte XML-Syntax. Jedes Anfangstag zieht ein Endtag nach sich, etwa so: ... — oder —

Zweitens sind alle Abschnitte in der .config-Datei in der Camel-Schreibweise (alternierende Groß-/Kleinschreibung) zu formulieren. Dabei wird der erste Buchstabe des ersten Wortes im Abschnittsnamen klein geschrieben und alle nachfolgenden Wörter groß (falls nicht durch einen Punkt abgetrennt). Ein Beispiel: ...

...

Obwohl diese Regeln einfach sind, so bringen sie doch manchen Anfänger aus dem Konzept. Bitte schenken Sie ihnen Beachtung. Sollte einmal eine Konfigurationsdatei Ihre Anwendung zum Absturz gebracht haben, dann prüfen Sie noch einmal die Groß-/Kleinschreibung sowie die Klammern. Tabelle 20.1 führt alle Konfigurationsabschnitte auf, die für Windows Forms zur Verfügung stehen. Abschnitt

Beschreibung

Enthält Informationen über entfernte Objekte, die Ihre Anwendung in Anspruch nimmt und exponiert.

Lässt sich für die Unterbringung benutzerdefinierter Anwendungseinstellungen nutzen.

Legt die Module fest, die zur Authentifizierung von Internet-Anfragen benutzt werden.

Enthält Kanäle, über die die Anwendung mit entfernten Objekten kommuniziert.

Enthält Vorlagen für die Verarbeitung von Ereignissen aus Kanälen zu entfernten Objekten.

Definiert die Abschnitte in einer Konfigurationsdatei wie auch die Module und Assemblies, die diese Abschnitte verarbeiten.

Legt fest, wie viele Internetverbindungen höchstens zulässig sind.

Tabelle 20.1: Konfigurationsabschnitte für Windows Forms

661

Windows Forms konfigurieren und bereitstellen

Abschnitt

Beschreibung

Legt fest, ob alle Klassen, die in der .config-Datei enthalten sind, beim Anwendungsstart geladen werden sollen (um bei der Validierung der .config-Datei zu helfen).

Legt den Proxyserver fest, der für HTTP-Anforderungen an das Internet zu benutzen ist.

Enthält Kryptographie-Informationen.

Enthält Informationen über Assemblies und Garbage Collection.

Enthält Informationen über die Version des .NET Frameworks, die für Ihre Anwendung benutzt werden soll.

Legt fest, dass Informationen in Dateien geschrieben werden, sofern die Ablaufverfolgung aktiviert ist (mehr dazu morgen).

Dient als Behälter für die Abschnitte , , und .

Dient als Behälter für die Abschnitte , , und .

Legt fest, wie Windows Forms-Anwendungen zu debuggen sind.

Legt Module fest, die benutzt werden, um Internetanforderungen auszuführen.

Tabelle 20.1: Konfigurationsabschnitte für Windows Forms (Forts.)

Der Rest dieses Abschnitts erörtert die gängigsten vordefinierten Konfigurationselemente. Jedes Mal, wenn Sie eine Konfigurationsdatei ändern, müssen Sie erst die Anwendung neu starten, bevor die Änderungen wirksam werden.

Der Abschnitt Wir besprechen den -Abschnitt zuerst, weil er einer der wichtigsten ist. Wie in Tabelle 20.1 erwähnt, listet dieser Abschnitt die verschiedenen anderen Abschnitte auf und sagt, wie sie zu behandeln sind. Daher findet man für jeden nachfolgenden Abschnitt, der erörtert wird, einen Eintrag dafür im -Abschnitt in der machine.config-Datei. Zum Beispiel:

662

So konfigurieren Sie Ihre Anwendungen





Das scheint eine Menge verwirrender Code zu sein, doch wir müssen ihn nicht vollständig verstehen. Das name-Attribut jedes hier definierten Abschnitts bestimmt den Namen dieses Abschnitts. Das type-Attribut zeigt die Klasse an, die zur Behandlung aller Aspekte dieses Abschnitts benutzt wird (etwa Lesen/Schreiben), gefolgt von der Assembly, in der sich die Klasse befindet, der Version dieser Assembly und einigen ergänzenden Informationen. Müssen Sie jemals Werte aus einem bestimmten Abschnitt beschaffen, schauen Sie in das -Element, um herauszufinden, welcher Klassentyp zur Lieferung dieser Werte zu erwarten ist. Die IgnoreSectionHandler-Klasse beispielsweise liefert keinerlei

Informationen; Abschnitte, die Benutzer nicht modifizieren sollten, verwenden diese Klasse. In ähnlicher Weise setzt der -Abschnitt eine Klasse des Typs NameValueFileSectionHandler ein, die eine Gruppe von Schlüssel/Wert-Paaren liefert, die die Informationen über die fragliche Anwendung darstellen. ist im Grunde nur bei zwei Gelegenheiten nützlich: wenn Sie wissen

müssen, wie ein bestimmter Abschnitt verarbeitet wird, und wenn Sie eigene Abschnitte definieren müssen. Um Letzteres zu erledigen, brauchen Sie lediglich den Namen des eigenen Abschnitts und die Klasse, die ihn handhabt, anzugeben. So erzeugt etwa das folgende Stück Code einen neuen Abschnitt namens »meineAnwendung«, die Schlüssel/Wert-Paare an Informationen aus Ihrer .config-Datei liefert:



Der neue Abschnitt sollte folgendermaßen aussehen:

663

Windows Forms konfigurieren und bereitstellen

Kümmern Sie sich nicht um die Klassen wie etwa SingleTagSectionHandler, die hier angegeben wurden. Man benutzt sie niemals direkt und Ihre Wahl, welche nun bei der Erstellung von Abschnitten zu bevorzugen ist, dürfte meist ohne Bedeutung sein. Das heutige Unterkapitel »Konfigurationswerte abrufen« erörtert die Klassen, über die Sie Bescheid wissen sollten. Sie können Ihre eigenen Klassen erstellen, um Konfigurationsabschnitte zu verarbeiten. Das erfordert das Anlegen einer Klasse, die die Schnittstelle IconfigurationSectionHandler implementiert und XML aus Ihren .config-Dateien lesen kann. Doch dies geht über das Thema dieses Buches hinaus. Mehr Informationen finden Sie in der Dokumentation zum .NET Framework.

Der Abschnitt Der -Abschnitt ist in Windows Forms-Anwendungen am häufigsten vorzufinden. Es handelt sich um einen benutzerdefinierten Konfigurationsabschnitt, den man nutzen kann, um beliebige Informationen zu speichern. Sie haben bereits ein Beispiel dafür gesehen:



Die Konfigurationsdaten in werden in Schlüssel/Wert-Paaren angeordnet. Mit Hilfe des add-Elements können Sie je nach Bedarf so viele dieser Paare erzeugen, wie Sie brauchen. Ihre Anwendung kann leicht darauf zugreifen (mehr dazu im Unterkapitel »Konfigurationswerte abrufen«). Das obige Codestück definiert einen neuen Schlüssel namens AppName mit dem Wert Listing 20.3. Wie Sie sicher bereits vermuten, legt diese Einstellung den Namen unserer Anwendung fest. Leicht lässt sich ein weiteres Element hinzufügen, um die Versionsnummer anzugeben: ...

...

Da sich nicht viel mehr über diesen Abschnitt sagen lässt, gehen wir zum nächsten.

664

So konfigurieren Sie Ihre Anwendungen

Der Abschnitt Wollen Sie angeben, dass Ihre Windows Forms-Anwendung eine bestimmte Version des .NET Frameworks benutzt, so können Sie dies im -Abschnitt tun. Weil aber die einzige verfügbare Framework-Version die Nummer 1.0 trägt, ist vorerst von keinem großen Nutzen. Erst wenn Microsoft weitere Versionen auf den Markt gebracht hat, erhält dieser Abschnitt mehr Sinn. Angenommen, die Version 2.0 würde veröffentlicht und verfügte über viele neue Funktionen. Ihre Anwendung erstellen Sie auf der Basis dieser Version und sie enthält nun Merkmale, die in Version 1.0 nicht verfügbar waren. Im -Abschnitt können Sie nun festlegen, dass Ihre Anwendung nur ausgeführt werden darf, wenn Version 2.0 vorliegt. Wenn der Benutzer versucht, die Anwendung auszuführen, ohne dass er Version 2.0 installiert hat, lässt sich die Anwendung einfach nicht laden und zeigt eine Fehlermeldung an. Die Syntax für diesen Abschnitt lautet wie folgt:

Sie brauchen sich lediglich um die zwei Platzhalter version und safemode zu kümmern. version stellt natürlich die Version des zu verwendenden .NET Frameworks dar. Diese Angabe muss im Format vx.x.x.x vorliegen, wobei jeder x-Wert eine Zahl zwischen 0 und 65.535 ist. Safemode kann entweder true oder false sein und gibt an, ob die CLR die Windows-Registrierung nach der richtigen zu verwendenden Version durchsuchen soll. Ein Beispiel:



Beachten Sie, dass version und safemode nicht zwingend erforderlich sind. Gibt man dennoch eine version-Nummer und true für safemode an, dann muss die angegebene Versionsnummer mit der in der Windows-Registrierung übereinstimmen, sonst wird ein Fehler erzeugt.

665

Windows Forms konfigurieren und bereitstellen

Der Abschnitt Dieser Abschnitt legt nur eines fest: welches Element Fehler behandeln soll, die in einer Anwendung auftreten. Ein kurzer Blick auf die Syntax:

Das .NET Framework benutzt ja einen JIT-Compiler (vgl. Tag 1), um Anwendungen aus MSIL in Maschinencode zu kompilieren, wenn die Anwendung ausgeführt wird. Die Anwendung wird also gerade noch rechtzeitig kompiliert, damit sie ausgeführt werden kann. Ähnlich arbeitet auch ein JIT-Debugger. Er greift »gerade noch rechtzeitig« ein, um Ihnen dabei zu helfen, die Anwendung zu reparieren, sobald ein Fehler aufgetreten ist. Stoßen Sie in einer Windows Forms-Anwendung auf Fehler, dann haben Sie zwei Alternativen. Die erste Option besteht darin, nichts zu tun. Standardmäßig erhält man in Windows Forms folgende Fehlermeldung, wie in Abbildung 20.2 zu sehen.

Abbildung 20.2: Der standardmäßige Fehlerbehandlung in Windows Forms

Das in Abbildung 20.2 gezeigte Dialogfeld bietet ein wenig Aufklärung darüber, was den Fehler verursachte, lässt aber keine Aktion Ihrerseits zu. Sie können entweder fortfahren, die Anwendung zu nutzen, oder sie beenden, wobei Sie praktisch den Fehler ignorieren. Die zweite Wahlmöglichkeit besteht im Einsatz des JIT-Debuggers zur Fehlerbeseitigung. Ein JIT-Debugger erlaubt es Ihnen, einen Fehler in der Anwendung zu beheben (sofern Sie noch über den Quellcode verfügen), so dass der Fehler nicht noch einmal auftreten kann. In der Standardeinstellung hat Windows Forms das »Eingreifen« eines JIT-Debuggers zur Fehlerbeseitigung deaktiviert. Hier kommt der Konfigurationsabschnitt ins Spiel. Haben Sie im obigen Code value auf true gesetzt, dann erscheint die in Abbildung 20.2 zu sehende Meldung nicht, sondern vielmehr sehen Sie etwas wie in Abbildung 20.3. In diesem Fall wird Ihnen eine Auswahl an JIT-Debuggern geboten, um einen Fehler zu beheben.

666

So konfigurieren Sie Ihre Anwendungen

Abbildung 20.3: Wenn Sie JIT-Debuggen aktivieren, können Sie einen JIT-Debugger zur Fehlerbeseitigung einsetzen.

In der morgigen Lektion wird Debugging intensiv behandelt. Damit der Abschnitt funktioniert, müssen Sie zunächst Ihre Anwendung mit aktiviertem Debugging kompiliert haben. Die Aktivierung wird mit der Option /debug vorgenommen: vbc /t:winexe /r:system.dll /debug MeinListing.vb Dies erzeugt eine neue Datei mit der Endung .pdb. Die Datei enthält spezielle

Informationen über die fragliche Anwendung, die Debugger auswerten können.

Konfigurationswerte abrufen Nachdem Sie Ihre .config-Datei erstellt und eingerichtet haben, kann die Anwendung auf diese Konfigurationswerte leicht mit der ConfigurationSettings-Klasse zugreifen. Deren zwei Mitglieder werden hier vorgestellt. Das erste ist die GetConfig-Methode. Man setzt sie ein, indem man einen abzurufenden Konfigurationsabschnitt angibt. Der folgende Code liefert die Informationen, die im Abschnitt enthalten sind: ConfigurationSettings.GetConfig("system.windows.forms")

667

Windows Forms konfigurieren und bereitstellen

Die Klasse ConfigurationSettings ist im Namensraum System.Configuration enthalten. Diesen müssen Sie unbedingt importieren, wollen Sie ConfigurationSettings verwenden. Das Code-Statement liefert ein Objekt vom Typ System.Windows.Forms.ConfigData, das aber leider nicht sonderlich nützlich ist. Die meisten Konfigurationsabschnitte geben solche Objekte zurück. Das ist der Grund, warum der Konfigurationsabschnitt eingeführt wurde. Dieser Abschnitt liefert ein benutzerfreundliches Objekt, was uns zum zweiten Mitglied der ConfigurationSettings-Klasse führt. Die AppSettings-Eigenschaft liefert passenderweise die Einstellungen, die in enthalten sind. Statt GetConfig zu benutzen und die gelieferten Objekte umwandeln zu müssen, präsentiert AppSettings alle Daten in leicht verwendbarem Format. Erstellen Sie eine .config-Datei für die aus Listing 20.3 erzeugte Anwendung (listing20.3.exe) und taufen Sie sie listing20.3.exe.config. Fügen Sie sie dem Code hinzu, der in Listing 20.4 zu sehen ist. Listing 20.4: Die .config-Datei für Listing 20.3 1: 2: 3: 4: 5:



6:

7:

Nun ändern Sie Zeile 17 des Listings 20.3 wie folgt: this.Text = ConfigurationSettings.AppSettings["AppName"] + " version " + ConfigurationSettings.AppSettings["Version"];

Dieser Code ruft die Informationen von »AppName« und »Version« aus der .config-Datei ab und zeigt sie in der Titelzeile der Anwendung an. Ändern Sie Zeile 22 wie folgt: MessageBox.Show(ConfigurationSettings.AppSettings["Beschreibung"]);

Dieser Code zeigt die »Beschreibung«-Einstellung im INFO-Fenster an. Kompilieren Sie die Anwendung neu (und vergessen Sie nicht, den Namensraum System.Configuration zu importieren). Klicken Sie auf das INFO-Menü. Sie sollten etwas Ähnliches wie in Abbildung 20.4 sehen.

668

Lokalisieren Sie Ihre Anwendung

Abbildung 20.4: Sie können Ihre Konfigurationsdaten in Ihrer Anwendung anzeigen lassen.

Das war's schon. Verwenden Sie die AppSettings-Eigenschaft, um in eckigen Klammern (in VB .NET in runden Klammern) die Einstellung anzugeben, an der Sie interessiert sind und schon wird sie beschafft (und angezeigt, wenn Sie wollen).

20.3 Lokalisieren Sie Ihre Anwendung Lokalisierung ist der Prozess, bei dem bestimmte Aspekte je nach Publikum einer Anwendung in kulturspezifische Versionen umgewandelt werden, so etwa in französische Sprache statt englische, wenn die Anwendung in Frankreich benutzt wird. Windows pflegt die jeweiligen Ländereinstellungen eines Benutzers und eine Anwendung kann diese Einstellungen ermitteln und sich ihnen automatisch anpassen. Lokalisierung klingt nach einem komplizierten Vorgang, ist es aber nicht. Das .NET Framework macht es Ihnen leicht, die Ländereinstellungen für eine Anwendung zu ändern. Sie müssen lediglich noch die eigentlichen Informationen in die jeweils gewünschte Sprache übersetzen. Meistens sind Texte und Grafiken zu übersetzen. In Begriffen der Lokalisierung werden solche Elemente als Ressourcen bezeichnet. Sie lassen sich getrennt von der Anwendung speichern. Bei der Lokalisierung einer Anwendung brauchen Sie lediglich diese Ressourcen zu übersetzen und .NET übernimmt den Rest.

669

Windows Forms konfigurieren und bereitstellen

Lokalisierung ist ein vierstufiger Prozess: 1. Erstellen Sie in normalem Text die Ressourcendateien, die für die verschiedenen Länder gebraucht werden. 2. Konvertieren Sie die Ressourcendateien in .resource-Dateien. 3. Kompilieren Sie die Ressourcendateien zu einer Assembly und legen Sie die Assemblies in entsprechenden Verzeichnissen ab. 4. Greifen Sie aus Ihrer Anwendung auf die Ressourcendateien zu. Der erste Schritt ist der einfachste. Öffnen Sie den Windows-Editor und geben Sie den Text des Listings 20.5 ein. Listing 20.5: Ressourcen für Englisch 1: 2: 3:

Caption = TYWinforms Day 20 Application WelcomeMessage = Welcome to localization techniques! GoodbyeMessage = Leaving so soon?

Speichern Sie das Listing als Day20.txt. Dann erzeugen Sie eine Eigenschaft und einen entsprechenden Wert in Ihrer Anwendung. In unserem Fall wurden die Eigenschaften Caption, WelcomeMessage und GoodbyeMessage mit angemessenem Text erzeugt, der an verschiedenen Stellen im Programm angezeigt wird. Nun erstellen Sie eine weitere Ressource, aber diesmal in Spanisch, wie in Listing 20.6 zu sehen. Listing 20.6: Ressourcen für Spanisch 1: 2: 3:

Caption = TYWinforms Applicacion Dia 20 WelcomeMessage = ¡Bienvenidos a tecnicas de localizacion! GoodbyeMessage = Ya se va?

Speichern Sie diese Datei unter Day20.es-ES.txt. Sie sollten bei diesem Listing ein paar Aspekte beachten, zuerst den Dateinamen. Der Anfang ist identisch mit dem für Listing 20.5: »Day 20«. Das »es« ist der Code für die spanische Ländereinstellung. Einige dieser Codes zeigen Unterkulturen an. »es-GT« steht für guatemaltekisches Spanisch, »es-ES« für Spanisch, das in Spanien gesprochen wird. In Listing 20.5 wurde dieser Kulturcode weggelassen, da es sich um die Standardkultur handelte: in diesem Fall Englisch. Ist Englisch nicht der Standard für die Ländereinstellungen, dann lautet sein Kürzel »en«. Die richtige Bezeichnung dieser Dateien ist sehr wichtig. Sie werden gleich verstehen, warum. (Eine komplette Liste der Kulturcodes finden Sie in der Dokumentation des .NET Frameworks unter der CultureInfo-Klasse.)

670

Lokalisieren Sie Ihre Anwendung

Beachten Sie, dass die Eigenschaftsnamen die gleichen geblieben sind. Das erleichtert es Entwicklern, auf sie im Code zu verweisen. Lediglich der eigentliche Textwert muss geändert werden. Nun sind die Ressourcendateien in etwas zu kompilieren, das das .NET Framework gebrauchen kann. Das ist ein zweistufiger Vorgang. Der erste Schritt besteht darin, das Werkzeug Resource File Generator einzusetzen (resgen.exe), um die .txt-Dateien in .resource-Dateien umzuwandeln. Die Syntax ist einfach: resgen txt_Datei resource_Datei

In diesem Fall haben Sie zwei Befehle: resgen Day20.txt Day20.resources resgen Day20.es-ES.txt Day20.es-ES.resources

Das Ergebnis sollte wie folgt aussehen: Read in Writing Read in Writing

3 resources from 'Day20.txt' resource file... Done. 3 resources from 'Day20.es-ES.txt' resource file... Done.

Wenn Sie Visual Studio .NET einsetzen, um Anwendungen zu lokalisieren, denken Sie daran, dass die von Visual Studio .NET erzeugten Ressourcendateien nicht automatisch zu den von resgen.exe erzeugten kompatibel sind. Haben Sie sich erst einmal für eine Methode entschieden, können Sie also nicht wechseln, ohne wieder ganz von vorne anzufangen. Der nächste Schritt besteht darin, diese .resources-Dateien zu einer Assembly zu kompilieren. Die dafür empfohlene Methode sieht vor, die Standardressourcendateien (hier in Englisch) in die ausführbare Anwendung einzubetten und alle anderen Ressourcendateien zu separaten Assemblies zu kompilieren (hier für Spanisch), die als Satellitenassemblies bezeichnet werden. Wenn man der Anwendung eine neue Ressource hinzufügen muss, muss man auf diese Weise nicht die ausführbare Datei anfassen, sondern lediglich eine neue Ressource anlegen und sie in einer Assembly im richtigen Verzeichnis zusammen mit der fraglichen Anwendung speichern. Um eine Ressource in die ausführbare Datei einzubetten, verwendet man einen CompilerBefehl von der Befehlszeile (csc für C#, vbc für VB .NET) zusammen mit dem /resourceParameter.

671

Windows Forms konfigurieren und bereitstellen

Um jedoch Satellitenassemblies aus den Ressourcendateien zu erstellen, die nicht zum Standard gehören, setzt man den Assembly Linker ein (al.exe). Die Syntax für dieses Hilfsprogramm sieht wie folgt aus: al /t:library /embed:filename /culture:culture /out:out_file

Der erste Parameter, filename, stellt die .resources-Datei dar, die zu kompilieren ist. In diesem Fall soll filename Day20.es-ES.resources sein. Der zweite Parameter, culture, gibt die Kultur für eine bestimmte zu kompilierende Datei an. Das ist wichtig, denn man kann nur eine Ressourcendatei pro Assembly haben, und jede Assembly muss den gleichen Namen haben (gleich mehr dazu). Die einzige Möglichkeit, wie das .NET Framework herausfinden kann, welche Assembly zu welcher Kultur gehört, besteht also im Einsatz der culture-Eigenschaft. Der letzte Parameter, out_file, ist der Dateiname der kompilierten Assembly (eine .dll-Datei). In unserem Fall sollten Sie den folgenden Befehl verwenden: al /t:library /embed:Day20.es-ES.resources /culture:es-ES /out:Day20.resources.dll

Sie erhalten eine Datei namens Day20.resources.dll, die die Spanisch-Ressourcen enthält. Die Englisch-Ressourcen sind ja in der Datei Day20.resources abzulegen. Ein kleiner Exkurs zu den Dateinamen: Es ist bei der Lokalisierung in .NET wichtig, dass alle Ihre Dateinamen übereinstimmen. So haben etwa Ihre zwei Ressourcendateien den gleichen Anfang: Day20. Auch die Satellitenassembly teilt diese Wurzel und gleich werden Sie sehen, dass dies auch die Anwendung tun muss. .NET verlässt sich auf diese Namenskonvention, um beim Aufruf die richtigen Ressourcen finden zu können. Zusätzlich muss man auch die richtige Verzeichnisstruktur für die Ressourcen erstellen. Für jede vorhandene Satellitenassembly, die nicht dem Standard angehört, sollte man ein eigenes Verzeichnis mit dem Kulturnamen erzeugen. In unserem Fall haben Sie eine Ressourcendatei für die Kultur es-ES. Deshalb legen Sie im Ordner Ihrer Anwendung einen Unterordner namens es-ES an und legen hier Ihre kompilierte Ressourcenassembly ab. Abbildung 20.5 zeigt eine typische Verzeichnisstruktur. c:\Winforms\Day20 Day20.exe Day20.resources es-ES Day20.resources.dll Day20.es-ES.resources fr Day20.resources.dll Day20.fr.resources …

672

Abbildung 20.5: Ihre kompilierten Quelldateien sollten in den Ordnern ihrer jeweiligen Kultur abgelegt werden.

Lokalisieren Sie Ihre Anwendung

Soll eine Anwendung die Ländereinstellungen ändern, sucht sie zuerst in jedem KulturVerzeichnis nach der richtigen Ressourcenassembly. Steht keine zur Verfügung, werden die Standardressourcen verwendet. Gibt es einen Ordner, der zwar die Kultur, aber nicht die Unterkultur angibt, dann verwendet die Anwendung statt dessen die übergeordnete Kultur. Hat man beispielsweise eine Satellitenassembly und ein Verzeichnis nur für allgemeines Spanisch (es) angelegt, doch die Kultur des Benutzers ist es-ES, dann greift die Anwendung auf die es-Ressourcen zurück. Dieser Vorgang wird als Ressourcenfallback bezeichnet. Nun benötigen Sie für die Nutzung Ihrer Ressourcen nur noch eine Anwendung. Listing 20.7 zeigt eine einfache Anwendung, die Ihre kompilierte Ressourcenassembly-Datei ausliest. Listing 20.7: Ihre erste Anwendung, die die jeweilige Kultur berücksichtigt 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:

using using using using using using using using

System; System.Windows.Forms; System.Drawing; System.Resources; System.Reflection; System.Threading; System.Globalization; System.ComponentModel;

namespace TYWinforms.Day20 { public class Listing207 : Form { private ResourceManager rsmDay20; private Label lblMessage = new Label(); public Listing207() { rsmDay20 = new ResourceManager("Day20", this.GetType().Assembly); lblMessage.Text = rsmDay20.GetString("Willkommensgruss"); lblMessage.Size = new Size(300,100); this.Text = rsmDay20.GetString("Titelzeile"); this.Controls.Add(lblMessage); this.Closing += new CancelEventHandler (this.CloseMe); } private void CloseMe(Object Sender, CancelEventArgs e) { MessageBox.Show(rsmDay20.GetString ("Abschiedgruss")); } public static void Main() {

673

Windows Forms konfigurieren und bereitstellen

31: 32: 33: 34:

Application.Run(new Listing207()); } } }

Speichern Sie dieses Listing als Day20.cs. (Dieser Dateiname muss mit dem Ihrer Ressourcendateien übereinstimmen.) In den Zeilen 1 bis 8 werden eine ganze Reihe zusätzlicher Namensräume importiert. Sie sind für die Lokalisierung und einige andere Dinge im Code erforderlich. In Zeile 12 wird die ResourceManager-Klasse erstellt, die sozusagen die Schaltstelle für die Interaktion mit Ressourcen bildet. ResourceManager verfügt über diverse Methoden, die Ressourcendateien laden und die darin enthaltenen Daten holen können. In Zeile 16 wird eine Instanz des ResourceManagerObjekts angelegt. Der Konstruktor übernimmt zwei Parameter. Der erste legt die Namenswurzel der zu verwendenden Ressourcen fest. (Jetzt verstehen Sie, warum alle Dateien die gleiche Wurzel haben müssen.) Der zweite Parameter besteht aus der Hauptassembly für die Ressourcen. Meist besteht diese Assembly einfach aus Ihrer zentralen ausführbaren Datei. GetType().Assembly liefert diejenige Assembly, die die Klasse Listing207 enthält. In Zeile 18 sehen Sie, wie eine Ressource das erste Mal geholt wird. Statt den Text des Bezeichnungsfeldes auf einen statischen Wert zu setzen, benutzen Sie die GetString-Methode der ResourceManager-Klasse und geben die Eigenschaft der zu holenden Ressourcendatei an. Denken Sie noch einmal an die Listings 20.5 und 20.6, in denen die Eigenschaften Caption, WelcomeMessage und GoodbyeMessage zur Verfügung standen. Die zweite wird in Zeile 18 eingesetzt, um den im Label-Steuerelement enthaltenen Text zu bestimmen. Die Caption-Eigenschaft hingegen wird in Zeile 21 verwendet, wieder mit der GetString-Methode des ResourceManager-Objekts. Da Sie dem Benutzer bei Beendigung der Anwendung eine Meldung anzeigen wollen, hängen Sie an das Closing-Ereignis einen Ereignishandler an. Die CloseMe-Methode in Zeile 26 zeigt eine MessageBox mit der GoodbyeMessage-Eigenschaft an. Denken Sie daran, dass Sie immer noch die standardmäßige Ressourcendatei haben: Day20.resources. Sie müssen sie in die kompilierte Version von Listing 20.7 einbetten. Zu diesem Zweck benutzen Sie folgenden Befehl, um Ihre Anwendung zu kompilieren: csc /t:winexe /r:system.dll /r:system.drawing.dll /r:system.windows.forms.dll / res:Day20.resources Day20.cs

Die einzige Neuheit hier ist der /res-Parameter (der für /resource steht). Er gibt die standardmäßige Ressourcendatei an, die in die ausführbare Datei einzubetten ist. Angenommen, Ihre Kultur ist Englisch, dann sehen Sie das in Abbildung 20.6 gezeigte Ergebnis,

674

Lokalisieren Sie Ihre Anwendung

wenn Sie versuchen, Ihre Anwendung zu schließen. Die in der Titelzeile, dem LabelSteuerelement und dem Meldungsfeld zu sehenden Werte wurden aus dieser Standardressourcendatei beschafft. Wollen Sie die Anwendung in einer anderen Kultur testen, stehen Ihnen zwei Optionen zur Wahl: Schicken Sie Ihre Anwendung in ein anderes Land und installieren Sie sie dort, oder verwenden Sie die CultureInfo-Klasse. Es könnte zwar erholsam sein, nach Spanien zu reisen, doch wesentlich schneller geht es mit der CultureInfo-Klasse.

Abbildung 20.6: Ihre lokalisierte Anwendung verhält sich in ihrer jeweiligen Kultur ganz normal.

Der Hauptthread in Ihrer Anwendung (vgl. Tag 19) verfügt über eine Eigenschaft CurrentUICulture, die bestimmt, welche Kultur und somit, welche Ressourcen die Anwendung verwenden soll. Die CultureInfo-Klasse lässt sich dazu benutzen, eine bestimmte Kultur vorzugeben. Um die Spanisch-Anwendung zu testen, fügen Sie folgenden Code zwischen die Zeilen 16 und 17 ein: Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");

Dieser Code stellt die Eigenschaft CurrentUICulture des Hauptthreads mit Hilfe der CultureInfo-Klasse auf es-ES ein. Kompilieren Sie die Anwendung (mit dem /res-Parameter) neu, dann sollten Sie das in Abbildung 20.7 gezeigte Ergebnis sehen. Alle Ressourcen wurden in Spanisch umgewandelt. Wenn man die Kultur mit der CultureInfo-Klasse ändert, schaut die Anwendung nach, ob im Ordner der ausführbaren Datei ein Unterverzeichnis namens es-ES existiert. Ist es vorhanden, besorgt sich die Anwendung ihre Ressourcen von dort. Ist das aber nicht der Fall, greift die Anwendung auf die Standardkultur zurück, nämlich Englisch.

675

Windows Forms konfigurieren und bereitstellen

Abbildung 20.7: Ihre Anwendung beherrscht nun mehrere Sprachen!

Wenn Sie eine Anwendung in eine weitere Sprache übersetzen müssen, erzeugen Sie einfache eine neue .resources-Datei, kompilieren sie zu einer Satellitenassembly und legen sie im richtigen Unterverzeichnis der Anwendung ab. Sie wiederholen also die Schritte 1 bis 3 vom Anfang dieser Lektion. Sie brauchen die Anwendung nicht neu zu kompilieren (es sei denn, Sie wollen sie mit unterschiedlichen CultureInfo-Objekten testen), was es erleichtert, die Anwendung zu lokalisieren. Es wird häufig die Empfehlung gegeben, all jene Elemente einer Anwendung, die sich lokalisieren lassen, in Ressourcendateien abzulegen, selbst dann, wenn man nicht vorhat, die Anwendung in andere Länder zu liefern. Wenn also eine Wortwahl oder ein Bild in der Anwendung zu ändern ist, kann man das tun, indem man nur die Ressourcendateien bearbeitet, statt die gesamte Anwendung neu kompilieren zu müssen.

20.4 Eingabehilfen Häufig müssen Sie Ihre Anwendung an die Bedürfnisse von Menschen mit Behinderungen oder Bedienungsproblemen bei Computern anpassen. Microsoft Active Accessibility (MSAA) ist eine Technologie, die es dem Entwickler erleichtern soll, solche Leistungsmerkmale einzubinden, die dieser Zielgruppe helfen. Standardmäßig setzt bereits jedes Windows Forms-Steuerelement MSAA um; jedes Steuerelement verfügt über mehrere Eigenschaften, die spezifisch die MSAA betreffen. Bestimmte Anwendungen, die als Clientanwendungen mit Eingabehilfen bezeichnet werden, benutzen diese Eigenschaften, um Behinderten zu helfen, häufig in Form von visuellen Hinweisen oder gesprochenen Beschreibungen. Tabelle 20.2 führt diese Eigenschaften auf.

676

Der einfache Teil der Arbeit: Bereitstellung

Eigenschaft

Beschreibung

AccessibilityObject

Liefert ein AccessibleObject-Objekt, das die Eingabehilfen des betreffenden Steuerelements beschreibt.

AccessibleDefaultActionDescription

Beschreibt die Aktion, die ein betreffendes Steuerelement ausführt.

AccessibleDescription

Beschafft oder setzt eine Beschreibung, die von Eingabehilfe-Clientanwendungen benutzt wird.

AccessibleName

Gibt den Namen des Steuerelements an, den die Eingabehilfe-Clientanwendungen zu sehen bekommen.

AccessibleRole

Gibt den Typ des betreffenden Steuerelements an, etwa ein Dialogfeld oder ein MenuItem.

IsAccessible

Gibt an, ob das betreffende Steuerelement zu Eingabehilfe-Clientanwendungen konform ist.

Tabelle 20.2: Eigenschaften für Eingabehilfen

Sie finden die verschiedenen Eingabehilfen-Clients, die in Windows eingebaut sind, wenn Sie auf START, PROGRAMME, ZUBEHÖR, EINGABEHILFEN oder in die SYSTEMSTEUERUNG gehen.

20.5 Der einfache Teil der Arbeit: Bereitstellung Bereitstellung ist das bei weitem einfachste Thema in der heutigen Lektion. Bereitstellung (Deployment) meint die Installation einer Anwendung auf den Computern der Benutzer. Da im .NET Framework Anwendungen vollständig autonom sind, besteht die einzige notwendige Maßnahme für ihre Bereitstellung im Kopieren von Dateien. Jede kompilierte .NET-Anwendung enthält ja Metadaten, die dem Computer alle nötigen Informationen liefern, um die betreffende Anwendung auszuführen. Eintragungen in der Registrierung vorzunehmen, Dateien ins Windows-Verzeichnis zu speichern oder etwas dergleichen aus der Zeit vor .NET ist nunmehr unnötig. Die erste Technik zur Bereitstellung besteht im Kopieren der entsprechenden Dateien auf den Zielcomputer. Bei den heute geschriebenen Anwendungen würde dies die .exeDateien betreffen. Hinzu kommen die Erstellung aller erforderlichen Unterverzeichnisse für andere Kulturen und die Platzierung passender Satellitenassemblies in ihnen. Der Kopiervorgang kann mit den gängigen Methoden erfolgen, vom File Transfer Protocol (FTP) bis hin zum guten alten XCOPY-Befehl.

677

Windows Forms konfigurieren und bereitstellen

Eine zweite Methode sieht den Einsatz der Windows Installer-Anwendung vor. Dieses Programm verpackt alle nötigen Dateien und erzeugt eine Datei mit der Endung .msi. Windows kann die .msi-Datei nutzen, um die Anwendung zu installieren (ähnlich wie das bekannte Programm Setup.exe, das sich in vielen älteren Anwendungen findet). Der Windows Installer nimmt auch Eintragungen im HINZUFÜGEN/ENTFERNEN-Applet der Systemsteuerung (SOFTWARE-Applet) vor, mit denen sich Anwendungen leicht von der Festplatte entfernen lassen. Eine dritte Methode besteht darin, die Anwendungsdateien zu einer .CAB-Datei zu komprimieren, die als Archivdatei bezeichnet wird. Solche Archivdateien sind lediglich gepackte Versionen der ursprünglichen Dateien, die sich bei Bedarf extrahieren lassen. Nähere Informationen über den Windows Installer und den Einsatz von Archivdateien finden Sie auf Microsofts Website unter dem folgenden URL: http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdkredist.htm

20.6 Zusammenfassung Konfiguration und Bereitstellung sind meist die letzten auszuführenden Schritte in der Anwendungsentwicklung. Ihre Behandlung am Ende des Buches ist daher angemessen. Die Konfiguration von Windows Forms-Anwendungen wird normalerweise mit .configXML-Dateien gehandhabt. Diese Dateien enthalten Einstellungen, die eine Anwendung benutzt, darunter Hinweise, welche Version des .NET Frameworks zu verwenden ist und wie man Unterstützung für Debugging bietet. Um auf diese Einstellungen über die Anwendung zuzugreifen, benutzt man die ConfigurationSettings-Klasse und entweder die Eigenschaft AppSettings, die Werte aus dem Konfigurationselement liefert, oder die GetConfig-Methode, um jeden anderen Abschnitt der .config-Datei zu liefern. Eine Anwendung zu lokalisieren, bedeutet, ihre Ressourcen in verschiedene Sprachen und Kulturen zu übertragen. Dazu gehört die Erzeugung von .resources-Dateien und Satellitenassemblies für jede weitere Kultur, die man unterstützen will. Das Bezeichnungsschema für lokalisierte Anwendungen ist von großer Bedeutung: Alle Dateien müssen den gleichen Wurzelnamen tragen. Man kann die ResourceManager-Klasse einsetzen, um Werte zu holen, die in Ressourcendateien enthalten sind. Auf diese Weise lässt sich die Anwendungslogik vollständig von der Benutzeroberfläche getrennt halten. Microsoft Active Accessibility ist in alle Windows Forms-Steuerelemente eingebaut. Die Eingabehilfen helfen Benutzern, die Schwierigkeiten bei der Bedienung einer Anwendung haben, indem sie ihnen audiovisuelle Hinweise geben, wie sie sich auf dem Computer zurechtfinden können. Diese Funktion wird in jedem Steuerelement durch eine Reihe besonderer Eigenschaften unterstützt.

678

Fragen und Antworten

Bereitstellung ist der Vorgang, bei dem eine Anwendung an die Benutzer ausgeliefert wird. Das einzige Erfordernis besteht hier im Kopieren der nötigen Dateien. Das .NET Framework und die CLR handhaben alles Weitere für Sie.

20.7 Fragen und Antworten F

Wie erzeuge ich Ressourcendateien aus Bildern? A

Leider ist dies schwieriger als bei reinem Text. Sie müssen erst Ihre Bilder in angemessen formatiertes XML umwandeln, bevor Sie sie zu Assemblies kompilieren können. Das .NET Framework QuickStart-Paket enthält ein Hilfsprogramm namens ResXGen.exe. Es benutzt folgende Syntax: ResXGen /i:dateiname /o:ressourcen_name /n:name

wobei dateiname die Grafikdatei angibt, ressourcen_name die zu erzeugende Ressourcendatei (mit der Endung .resx) und name den Eigenschaftsnamen für diese Ressource (ähnlich wie heute »WelcomeMessage«). Nähere Informationen über ResXGen finden Sie in der .NET Framework-Dokumentation. F

In der Zeit vor .NET konnte man benutzerspezifische Informationen in der Registrierung ablegen. Für eine einzelne Anwendung konnte ich so für verschiedene Benutzer jeweils unterschiedliche Einstellungen realisieren. Kann ich das auch in .NET tun? A

F

Mit .config-Dateien geht das nicht. Man kann aber weiterhin benutzerspezifische Informationen in der Windows-Registrierung mit Hilfe der Registry-Klasse pflegen. Diese Klasse liefert Informationen über in der Registrierung abgelegte Daten und erlaubt auch den Schreibzugriff auf die Registrierung.

Ich bekomme nicht heraus, welche Eigenschaft ein bestimmter Handler für einen Konfigurationsabschnitt hat, oder welchen Datentyp er liefert. Ich bitte um Hilfe. A

Das Hilfsprogramm Intermediate Language Disassembler (ildasm.exe) erweist sich als nützlich, um die Inhalte einer .NET Assembly zu untersuchen (.dll- oder .exe-Dateien). Das Werkzeug verfügt über eine grafische Oberfläche, die alle Mitglieder, die in einer Assembly enthalten sind, detailliert auflistet. So können Sie herausfinden, welche Typen das Objekt liefert und welche Methoden und Eigenschaften es unterstützt. Das Tool ist häufig ein guter Ersatz für die Dokumentation des .NET Frameworks.

679

Windows Forms konfigurieren und bereitstellen

20.8 Workshop Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Welchen Vorteil bieten .config- gegenüber .ini-Dateien? 2. Was macht das Werkzeug al.exe? 3. Welche zwei Parameter erwartet das Hilfsprogramm resgen? 4. Wahr oder falsch? Eine Konfigurationsdatei für eine MyApp.exe genannte Anwendung sollte MyApp.config genannt werden. 5. Wahr oder falsch? Wenn man Ressourcendateien übersetzt, muss man auch die Eigenschaftsnamen wie auch deren Werte übersetzen. 6. Welches ist das Basiselement für alle .config-Dateien? 7. Zu welchem Zweck wird das Konfigurationselement verwendet? 8. Was versteht man unter Camel-Schreibweise und wo ist sie erforderlich? 9. Schreiben Sie eine einfache .config-Datei, die über einen -Abschnitt verfügt. Dieser Abschnitt sollte ein Element namens Color mit dem Wert Black enthalten.

Übungen 1. Nehmen Sie drei weitere Ländereinstellungen in das Beispiel aus Listing 20.6 auf: Französisch (aus Frankreich), Dänisch und Griechisch. Kümmern Sie sich nicht um das Übersetzen der eigentlichen Ausdrücke, sondern ändern Sie sie nur so, dass sie die jeweils aktuelle Kultur widerspiegeln. (In der .NET Framework-Dokumentation zur CultureInfo-Klasse finden Sie Angaben zu den Kulturcodes für diese Sprachen.) 2. Erstellen Sie eine .config-Datei, die die jeweils zu verwendende Kultur von Übung 1 angibt. So müssen Sie nicht Ihren Quellcode verändern und die Anwendung jedes Mal kompilieren, wenn Sie eine neue Kultureinstellung testen wollen. (Tipp: Nutzen Sie den -Abschnitt.)

680

Debugging und Profiling

1 2

Debugging und Profiling

Die heutige Lektion dreht sich um Debugging und Profiling. Debugging ist der Vorgang, bei dem jegliche Fehler in einer Anwendung beseitigt werden, gleichgültig, ob sie Ihnen bekannt sind oder nicht. Profiling (oder Profilerstellung) ist eine Technik, um Anwendungen hinsichtlich ihrer Leistung zu optimieren, indem man beobachtet, welche Leistung sie unter normalen Bedingungen aufbringen. Diese zwei Themen wurden zusammengefasst, weil zu ihnen ähnliche Prozesse gehören. Zu beiden Verfahren gehört die Technik, eine laufende Anwendung zu überwachen, damit man herausfinden kann, was genau »unter der Motorhaube« vor sich geht, wenn ein Benutzer die Anwendung bedient. Heute lernen Sie 쐽

die Vorteile kennen, die der Einsatz von try-catch-Blöcken im Code bietet,



wie man Programmdatenbankdateien für das Debugging erzeugt,



auf welche Weise JIT-Debugging funktioniert,



Aspekte des CLR-Debuggers und seines Befehlszeilenvetters CorDbg kennen,



die Process-Klasse einzusetzen.

21.1 Debuggen in Windows Forms Ohne Zweifel sind Sie bei der Erstellung von Windows Forms-Anwendungen auch über Fehler gestolpert. Fehler gehören zum Alltag des Entwicklers, daher muss man lernen, damit umzugehen. Häufig ist der Vorgang der Fehlerbeseitigung (also Debugging) zeitraubend und lästig; man durchsucht den Code Zeile für Zeile und zuweilen sogar Zeichen für Zeichen. Das .NET Framework erleichtert jedoch das Beheben von Fehlern. Man kann seine Software auf verschiedene Weise debuggen. Dieser Abschnitt untersucht Möglichkeiten, wie Sie die Fehlerquelle einkreisen können, während Sie noch Code schreiben. Die folgenden Abschnitte stellen eine Reihe von Werkzeugen vor, die das .NET Framework bereitstellt, um Fehler nach der Anwendungsentwicklung zu beseitigen. Zunächst ist es sinnvoll, einen Blick auf die Debugging-Strukturen zu werfen, die .NET bereitstellt. Sobald ein Fehler nach dem Kompilieren einer Anwendung auftritt, löst die CLR eine Ausnahme (Exception) aus – ein einfallsreicher Ausdruck für einen Fehler. Tritt solch ein »Bug« auf, wirft (throws) die CLR eine Ausnahme. .NET verfügt über eine Exception-Klasse (und viele davon abgeleitete Klassen), die zur Darstellung jedes auftretenden Fehlers einsetzbar ist.

682

Debuggen in Windows Forms

Das ist sinnvoll, denn nun sind Fehler wirkliche Objekte, mit denen man umgeht. Man kann daher ihre Eigenschaften herausfinden, Methoden dafür aufrufen und sogar eigene Fehler erzeugen. Zur Fehlerbehandlung in Windows Forms gehört es, diese Exception-Klassen zu finden und sie angemessen zu behandeln. Der Rest dieses Abschnitts befasst sich mit der einfachsten Methode, Exception-Klassen zu handhaben: durch den Einsatz der try-catch-Syntax des .NET Frameworks. Try-catch-Syntax tritt in Blöcken auf. Der try-Teil bedeutet, dass man Code versucht bzw. versuchsweise ausführt. Setzt man einen try-Codeblock ein, wird die CLR vorsichtiger bei der Codeausführung. Sie wird dadurch nicht langsamer, doch wenn jetzt eine Ausnahme ausgelöst wird, ist die CLR vorgewarnt und verhindert einen Programmabsturz. Dies führt uns zum catch-Block: Er fängt jede Ausnahme ab, die ausgelöst wurde. Hier kann man jede Ausnahme bearbeiten und korrigieren. Da er mit dem try-Block kombiniert wird, lassen sich nun Fehlermeldungen besonnen behandeln, statt den Benutzer dadurch aufzuregen, dass seine Anwendung abstürzt.

Die Syntax für einen try-catch-Block ist einfach: try { //sensibler Code } catch (Exception e) { //Code für die Ausnahmebehandlung }

Oder in VB .NET: Try 'sensibler Code Catch e as Exception 'Code für die Ausnahmebehandlung End Try

Unter »sensiblem Code« versteht man einen Codeabschnitt, der eine Ausnahme auslösen kann oder von dem man glaubt, dass er nicht richtig funktionieren wird. Solcher Code kann alles Mögliche umfassen: von der Initialisierung einer Variablen bis hin zum Einsatz der E/ A-Fähigkeiten von .NET, um eine möglicherweise nicht vorhandene Datei zu öffnen. Das schon erwähnte catch-Statement fängt eine bestimmte Exception ab. Die zwei Codeabschnitte oben haben ein Objekt vom Typ Exception abgefangen. Man kann dies durch jede andere Klasse ersetzen, die von Exception abgeleitet ist, etwa IOException. Einzelne Ausnahmeklassen stellen bestimmte Fehlertypen dar und können eine Reihe von Unterausnahmen haben, die es erlauben, die Fehlerquelle weiter einzukreisen. Will man etwa auf eine nicht vorhandene Datei zugreifen, wird eine IOException ausgelöst, die von der Klasse SystemException abgeleitet ist, welche wiederum von Exception abgeleitet ist (vgl. Abbildung 21.1).

683

Debugging und Profiling

System. Exception System. ApplicationException*

System.Windows.Forms.AxHost.InvalidActiveXStateException

InvalidFilterCriteriaException TargetException TargetInvocationException TargetParameterCountException System.IO.IsolatedStorage.IsolatedStorageException

*All sub-exceptions belong to the System.Reflection namespace **All sub-exceptions belong to the System namespace, unless otherwise noted ←Belongs to the System. ComponentModel namespace →+ Belongs to the System.Runtime.InteropServices namespace +++ Belongs to the System.Runtime namespace ’Belongs to the System.Security namespace ”Belongs to the System.Threading namespace

System.Runtime.Remoting.MetadataServices.SUDSGeneratorException System.SystemException**

AppDomainUnloadedException ArgumentException ArithmeticException ArrayTypeMismatchException BadImageFormatException CannotUnloadAppDomainException Design.Serialization.CodeDOMSerializerException+ LicenseException+ WarningException+ Configuration.ConfigurationException Configuration.Install.InstallException ContextMarshallException Data.DataException Data.DBConcurrencyException Data.SqlClient.SqlException DataSqlTypes.SqlTypeException Drawing.Printing.InvalidPrinterException EnterpriseServices.RegistrationException EnterpriseServices.ServicedComponentException ExecutionEngineException FormatException IndexOutOfRangeException InvalidCastException

InvalidOperationException InvalidProgramException IO.InternalBufferOverflowException IO.IOException Management.ManagementException MemberAccessException MulticastNotSupportedException NotImplementedException NotSupportedException NullReferenceException OutOfMemoryException RankException Reflection.AmbiguousMatchException Reflection.ReflectionTypeLoadException Resources.MissingManifestResourceException ExternalException++ InvalidComObjectException++ InvalidOleVariantTypeException++ MarshalDirectiveException++ SafeArrayRankMismatchException++ SafeArrayTypeMismatchException++ Remoting.RemotingException+++ Remoting.ServerException+++

Serialization.SerializationException+++ Cryptography.CryptographicException’ Policy.PolicyException’ SecurityException’ VerificationException’ XmlSyntaxException’ ServiceProcess.TimeoutException StackOverflowException SynchronizationLockException” ThreadAbortException” ThreadInterruptedException” ThreadStateException” TypeInitializationException TypeLoadException TypeUnloadedException UnauthorizedAccessException Web.Services.Protocols.SoapException Xml.Schema.XmlSchemaException Xml.XmlException Xml.XPath.XPathException Xml.Xsl.XsltException

Abbildung 21.1: Die Ausnahmenhierarchie im .NET Framework ist recht kompliziert (System.SystemException verfügt sogar

über zwei oder gar drei Unterebenen, die hier nicht angezeigt sind).

Weil das Ausnahmeklassen von .NET hierarchisch gegliedert sind, kann man einen catchBlock für eine übergeordnete Ausnahme benutzen und zugleich alle Ausnahmen abfangen, die von der übergeordneten abgeleitet sind. In den zwei Codebeispielen oben fängt man eigentlich jede Art von Ausnahme ab, weil die Exception-Klasse deren »Urvater« ist. Das bedeutet, dass man mehrere catch-Blöcke pro try-Block einsetzen kann, sollte man einmal mehrere verschiedene Ausnahmetypen behandeln müssen. Beispielsweise so: try { //sensibler Code } catch (IOException e) { //eine E/A-Ausnahme abfangen } catch (SystemException e) { //jede System-Ausnahme abfangen, darunter auch IOException und ihre gleichgeordneten Ausnahmen } catch (Exception e) { //alle Ausnahmen abfangen, einschließlich System-Ausnahmen }

684

Debuggen in Windows Forms

Sorgen Sie dafür, dass Sie beim Einsatz mehrerer catch-Blöcke vom fokussiertesten zum diffusesten vorgehen: Code, der einen ganz bestimmten Fehler behandelt, sollte also vor Code kommen, der einen allgemein gehaltenen Fehler wie etwa Exception behandelt. Da nur ein catch-Block ausgeführt wird, könnte sonst die generische Fehlerbehandlung stets vor allen anderen ausgeführt werden. Zeit für ein Beispiel. Listing 21.1 zeigt eine einfache Anwendung, die versucht, eine Textdatei zu öffnen und ihren Inhalt in einem RichTextBox-Steuerelement anzuzeigen. Listing 21.1: Eine einfache E/A-Anwendung 1: Imports System 2: Imports System.Windows.Forms 3: Imports System.Drawing 4: 5: Namespace TYWinForms.Day21 6: 7: Public Class Listing211 : Inherits Form 8: private rtbText as New RichTextBox 9: private btPush as new Button() 10: 11: public sub New() 12: rtbText.Height = 225 13: rtbText.Dock = DockStyle.Bottom 14: rtbText.ScrollBars = RichTextBoxScrollBars.Both 15: 16: btPush.Location = new Point(100,10) 17: btPush.Text = "Öffnen!" 18: AddHandler btPush.Click, new EventHandler(AddressOf Me.OpenFile) 19: 20: Me.Text = "Listing 21.1" 21: Me.Controls.Add(rtbText) 22: Me.Controls.Add(btPush) 23: end sub 24: 25: private sub OpenFile(Sender As Object, e As EventArgs) 26: rtbText.LoadFile("c:\temp\happy.txt",  RichTextBoxStreamType.PlainText) 27: end sub 28: 29: public shared sub Main() 30: Application.Run(new Listing211) 31: end sub 32: End Class 33: End Namespace

685

Debugging und Profiling

Nach Tag 11 dürfte dieses Listing recht vertraut aussehen. In den Zeilen 8 und 9 werden ein RichTextBox- und ein Button-Steuerelement erzeugt. Sie werden in den Zeilen 12 bis 18 initialisiert; das Click-Ereignis der Schaltfläche hat in Zeile 25 einen Ereignishandler, der die LoadFile-Methode des RichTextBoxSteuerelements benutzt, um die Datei c:\temp\happy.txt zu öffnen und sie dem Benutzer anzuzeigen. Ist diese jedoch nicht vorhanden und führt man die Anwendung so aus, erhält man die in Abbildung 21.2 gezeigte Fehlermeldung, falls man keinen Debugger installiert hat, oder die Fehlermeldung in Abbildung 21.3, falls man doch einen Debugger hat.

Abbildung 21.2: Die Standardroutine für die Ausnahmebehandlung lässt Ihnen die Möglichkeit, fortzufahren und den Fehler zu ignorieren oder die Anwendung zu beenden.

Abbildung 21.3: Falls Sie einen JIT-Debugger installiert haben, können Sie die Anwendung debuggen. Beachten Sie den Ausnahmetyp, der ausgelöst wird.

Wären Sie der Benutzer dieser Anwendung, dürften Sie angesichts der in Abbildung 21.2 bzw. 21.3 wiedergegebenen Meldung(en) ziemlich frustriert sein. Man muss den Fehler so abfangen, dass der Benutzer eine hilfreiche Fehlermeldung erhält, falls überhaupt. Ändern Sie die OpenFile-Methode in Zeile 25 des Listings 21.1 so ab, dass sie wie in folgendem Codestück aussieht:

686

Debuggen in Windows Forms

private sub OpenFile(Sender As Object, e As EventArgs) Try rtbText.LoadFile("c:\temp\happy.txt", RichTextBoxStreamType.PlainText) Catch ex As Exception MessageBox.Show("Diese Datei existiert nicht!") End Try end sub

Klickt der Benutzer nun auf die Schaltfläche und ist die Datei nicht vorhanden, erhält er eine Meldung wie in Abbildung 21.4.

Abbildung 21.4: Bei der Verwendung eines Try-Catch-Blocks können Sie eine weitaus benutzerfreundlichere Meldung erzeugen.

Klickt der Benutzer auf die OK-Schaltfläche, fährt die Anwendung fort, als wäre die Fehlermeldung nie ausgelöst worden – fast so, als wäre die OpenFile-Methode nie ausgeführt worden. Eine Meldung wird innerhalb des Catch-Blocks angezeigt, aber im Grunde könnte man hier alles Mögliche tun. So könnte man beispielsweise den Text abändern, um den Benutzer zu fragen, ob er die Datei erstellen möchte, falls es sie nicht gibt, und dies mit Hilfe der E/A-Fähigkeiten von .NET ausführen. Der try-catch-Block hat einen weiteren, zusätzlichen Teil: das finally-Statement. Dessen Syntax folgt der des catch-Statements. finally wird benutzt, um Code auszuführen, der immer laufen muss, gleichgültig, ob der Code in einem try-Block funktioniert oder nicht. Das folgende Codebeispiel erzeugt ein Array von Zeichenfolgen und versucht, ein FileInfo-Objekt aus jeder Zeichenfolge zu erzeugen. Ist die Datei vorhanden, wird eine Meldung mit der Angabe ihrer Länge angezeigt. Gibt es sie aber nicht, wird eine benutzerfreundliche Meldung gezeigt. Im Meldungsfeld befindet sich die Anzahl der verarbeiteten Zeichenfolgen, gleichgültig, ob ein FileInfo-Objekt erzeugt wurde oder nicht. int i = 0; FileInfo tmpFile; String[] arrFileStrings = new String[] {"c:\\temp\\happy.txt", "c:\\winforms\\day21\\listing21.1.vb"};

687

Debugging und Profiling

foreach (String strName in arrFileStrings) { try { tmpFile = new FileInfo(strName); MessageBox.Show("Länge von " + strName + ": " + tmpFile.Length.ToString()); } catch (Exception ex) { MessageBox.Show(strName + " ist keine gültige Datei!"); } finally { i++; MessageBox.Show(i.ToString() + " Dateien verarbeitet"); } }

Alle Ausnahmeklassen haben einige nützliche Eigenschaften, die zusätzliche Informationen über den betreffenden Fehler liefern. Tabelle 21.1 beschreibt die Eigenschaften der Exception-Klasse, die ja allen Unterausnahmeklassen vererbt werden. Eigenschaft

Beschreibung

HelpLink

Liefert eine optionale Datei, die mit der betreffenden Ausnahme verknüpft ist. Das Format muss ein URL sein. Zum Beispiel: file://C://TYWinforms/Day21/help.html#ErrorNum42

InnerException

Wenn eine Ausnahme eine weitere auslöst liefert diese Eigenschaft einen Hinweis auf die Ursprungsausnahme.

Message

Die benutzerfreundliche Meldung, die die Ausnahme beschreibt.

Source

Der Name der Anwendung oder des Objekts, das die Ausnahme erzeugte.

StackTrace

Die Zeichenfolgendarstellung aller Stapelrahmen in der Aufrufliste zum Zeitpunkt der Ausnahmeauslösung. Diese Eigenschaft teilt Ihnen mit, welche Methoden gerade ausgeführt wurden. (Eine Methode ruft oftmals eine weitere auf, so dass Sie nun der Spur zurück bis zur Wurzelmethode des Problems folgen können.)

TargetSite

Liefert die Methode, die die Ausnahme verursachte.

Tabelle 21.1: Eigenschaften von Exception

Statt zu warten, bis eine Fehlermeldung auftritt, können Sie auch jederzeit Ihre eigenen Fehler erzeugen, indem Sie das throw-Statement verwenden. Erzeugen Sie einfach eine neue Ausnahme vom gewünschten Typ und schon lässt sich Ihre eigene Anwendung sabotieren: throw(new Exception());

Warum sollte man so etwas tun, fragt man sich. Das tut man am ehesten, wenn man benutzerdefinierte Windows Forms-Steuerelemente erstellt hat und auch das Verhalten im Fehlerfall prüfen möchte. Das Schlüsselwort throw eignet sich ausgezeichnet für diesen

688

JIT-Debugging

Zweck. Man kann die Message- und InnerException-Eigenschaften für neu erstellte Ausnahmen festlegen, die die behandelnde Anwendung dann ebenso benutzen kann, wie Sie es bereits mit den eingebauten Ausnahmen getan haben. Im Allgemeinen sollte man einen try-catch-Block immer dann einsetzen, wenn man Code ausführt, von dem nicht sicher ist, dass er garantiert sicher ausgeführt wird, wie etwa Datenbankabfragen, Dateioperationen, Netzwerkanforderungen und Operationen, die von bestimmten Benutzereingaben abhängen. Im ganzen Buch wurden try-catch-Blöcke an den meisten Stellen, wo sie sich hätten befinden sollen, weggelassen, doch da Sie nun wissen, wie man deren Syntax benutzt, sollten Sie nicht zögern, sie einzusetzen. Ich empfehle

Bitte beachten Sie

Setzen Sie try-catch-Blöcke immer dann ein, wenn Sie Code ausführen, von dem Sie nicht sicher sind, dass er richtig ausgeführt wird.

Verlassen Sie sich nicht auf try-catch-Blöcke, um Benutzereingaben auf Gültigkeit zu prüfen. Wenn Sie dem Benutzer ein Textfenster anzeigen, in das er eine Zahl eintragen soll, ist zu prüfen, ob die Eingabe wirklich eine Zahl ist, bevor man den Code ausführt, der von der Zahl abhängt. Versuchen Sie nicht, solchen Code auszuführen, in der Hoffnung, dass der trycatch-Block ihn schon richtig behandeln werde.

21.2 JIT-Debugging Just-In-Time-Debugging ist, wie bereits erwähnt, der Vorgang, bei dem die Fehler in der Anwendung beseitigt werden, während diese gerade läuft und die Fehler nacheinander auftreten. Während try-catch-Blöcke dafür geeignet sind, Bugs zu beseitigen, bevor der Benutzer sie zu sehen bekommt, wurde JIT-Debugging dafür entworfen, den Benutzer auf die Fehler aufmerksam zu machen, damit sie beseitigt werden können. In diesem Fall ist der Benutzer natürlich der Entwickler (der einzige, der die Fehler beheben kann). Läuft die Anwendung, führt der Computer kompilierten Code aus. Dieser Code besteht aus Anweisungen, erzeugt bei der Ausführung Variablen, weist diesen Werte zu und hebt sie auch wieder auf. Alle diese Variablen werden natürlich im Speicher abgelegt. Wenn man sagt, ein Debugger wird angehängt, so ist damit gemeint, dass ein JIT-Debugger den Speicher überwacht, den die betreffende Anwendung belegt. Der Debugger kann die Anweisung und den belegten Speicher auswerten und dem Entwickler in verständlicher Form mitteilen, was los ist.

689

Debugging und Profiling

Weil der JIT-Debugger nicht das Innenleben der gerade laufenden Anwendung kennen kann (da er nicht auf deren Quellcode zugreifen kann), braucht er etwas Unterstützung, um Informationen liefern zu können. Diese Hilfe bekommt er von einer Programm-Datenbankdatei (.pdb). Sie enthält Debugging-relevante Informationen wie etwa über Methoden und Variablen im Programm. Eine .pdb-Datei zu erstellen, ist leicht: Fügen Sie den /debug-Parameter in die Befehle für den Befehlszeilencompiler ein: vbc /t:winexe /r:system.dll,... /debug temp.vb

Durch dieses Kommando wird eine neue Datei namens temp.vb erzeugt. Versucht ein JITDebugger Fehler in einer Anwendung zu beseitigen – in diesem Fall in temp.exe –, benutzt er den Inhalt der .pdb-Datei, um ihm zu helfen. Zur Aktivierung des JIT-Debuggings ist noch ein weiterer Schritt nötig. Erinnern Sie sich an .config-Dateien und das Konfigurationselement . Hierin ist auch JIT-Debugging zu aktivieren. Beispielsweise könnte die .config-Datei für temp.exe wie folgt aussehen:



Als Nächstes lernen Sie alle .NET-Werkzeuge kennen, die für das Debugging zur Verfügung stehen.

Das Werkzeug DbgClr Der Common Language Runtime Debugger (DbgClr), erlaubt das Anhängen an einen Prozess und so die Überwachung und Kontrolle der Ausführung einer Anwendung. Dieses Programm ist üblicherweise im Verzeichnis c:\Programme\Microsoft.NET\FrameworkSDK\GuiDebug abgelegt und heißt DbgClr.exe. Sie können sowohl diesen Debugger an eine bereits laufende Anwendung anhängen als auch eine neue Anwendung aus dem Debugger heraus starten. Im zweiten Fall startet man ganz von vorn und öffnet den Debugger, indem man DbgClr.exe ausführt. Sie sollten etwas sehen, das wie das in Abbildung 21.5 Gezeigte aussieht. Nachdem Sie den Debugger geöffnet haben, führen Sie die drei Schritte des DebuggingProzesses aus: 1. Öffnen Sie die zu bereinigende Anwendung oder hängen Sie den Debugger an die bereits laufende Anwendung an. 2. Setzen Sie Haltepunkte. 3. Setzen Sie den Debugger ein, um die Anwendung zu bearbeiten.

690

JIT-Debugging

Abbildung 21.5: Die Benutzeroberfläche des CLR-Debuggers ähnelt ein wenig der von Visual Studio .NET.

Vorausgesetzt, Sie haben Listing 21.1 als listing21.1.vb gespeichert, öffnen Sie die Datei bitte aus dem CLR-Debugger heraus, indem Sie in das Menü DATEI/ÖFFNEN/DATEINAME gehen und diese Datei auswählen. Nach dem Öffnen sollten Sie den Quellcode im Debugger sehen. Die Datei erscheint im PROJEKTMAPPEN EXPLORER auf der rechten Seite des Fensters. In dieser Datei können Sie den Debugging-Prozess verfolgen. Um mit der Fehlerbeseitigung zu beginnen, wählen Sie ZU DEBUGGENDES PROGRAMM aus dem DEBUGGEN-Menü aus und geben den Namen der ausführbaren Datei ein (c:\winforms\day21\listing21.1.exe). Jetzt weiß der Debugger, welche Anwendung mit dem geöffneten Quellcode verknüpft ist. Drücken Sie die Funktionstaste (F5), um die Anwendung zu starten, oder gehen Sie ins DEBUGGEN-Menü und klicken Sie auf STARTEN. Die Anwendung wird ganz normal angezeigt und der CLR-Debugger steht bereit. Ist jedoch die Anwendung bereits gestartet, gehen Sie in das Menü EXTRAS im CLRDebugger und wählen den Befehl DEBUGPROZESSE aus. Ein Listing wie in Abbildung 21.6 erscheint. Wählen Sie in der Liste Ihre Anwendung aus und klicken Sie auf ANFÜGEN. Daraufhin erscheint die Anwendung im unteren Teil des Dialogfeldes im Bereich GEDEBUGGTE PROZESSE in Abbildung 21.6. Klicken Sie auf die UNTERBRECHEN-Schaltfläche, um die Ausführung anzuhalten und den Code anzusehen. Das Anfügen an eine bereits laufende Anwendung ist sinnvoll, wenn man nicht über den entsprechenden Quellcode verfügt (wiewohl man nicht die Anwendung ohne den Quellcode ändern kann). Ein neues Fenster namens AUSGABE ist nun im CLR-Debugger verfügbar. Dieses Fenster zeigt, welche Module für die Anwendung geladen wurden. Das kann dazu beitragen, die Abhängigkeiten der Anwendung festzustellen.

691

Debugging und Profiling

Abbildung 21.6: Hängen Sie Ihrer Anwendung den Debugger durch einen Klick auf ANHÄNGEN an.

Wenn Sie sich mit einem Debugger in einen Prozess einklinken, sollten Sie sicherstellen, dass keine wichtige Anwendung läuft. Der Grund: Die Anwendung wird bei diesem Vorgang »eingefroren« und jede nicht gespeicherte Information kann verloren gehen. Ein Haltepunkt ist eine Stelle im Code, an der die Ausführung angehalten wird: Eine Pause tritt ein. Haltepunkte sind nützlich für die Fehlerbeseitigung in bestimmten Codezeilen. Wüssten Sie etwa, dass eine solche Zeile Fehler erzeugt, könnten Sie direkt davor einen Haltepunkt setzen und daraufhin den Debugger einsetzen, um die fraglichen Anweisungen und Variablen zu untersuchen. Um einen Haltepunkt zu setzen, klicken Sie auf die linke Spalte neben dem Quellcode Ihrer Datei. Ein roter Punkt sollte erscheinen, der anzeigt, dass die Ausführung in dieser Zeile stoppt. Halten Sie den Mauszeiger direkt über den Haltepunkt, um zu sehen, wo genau der Stopp passieren wird (vgl. Abbildung 21.7). Jetzt können Sie Ihre Anwendung wieder normal bedienen. Haben Sie keine Haltepunkte gesetzt und gibt es keine Fehlermeldungen, dürfte der Code so rasch ausgeführt werden, dass Sie nichts sehen können, wenn es passiert. Obwohl dies ein Hinweis ist, dass die Anwendung reibungslos funktioniert, hilft Ihnen das bei der Fehlerbeseitigung keineswegs. Um die Anwendung zu untersuchen, klicken Sie auf die DEBUGGEN BEENDEN-Schaltfläche (oder drücken (¢) + (F5)) und setzen am Anfang der OpenFile-Methode in Zeile 25 (Listing 21.1) einen Haltepunkt. Drücken Sie (F5) erneut.

692

JIT-Debugging

Abbildung 21.7: Wenn Sie in Ihrem Code einen Haltepunkt setzen, wird die Ausführung vorübergehend an der angegebenen Zeile gestoppt.

Die Anwendung startet ganz normal, doch sobald man auf die Schaltfläche klickt, hält die Anwendung an und der Debugger übernimmt die Kontrolle. Das Ausgabefenster rechts unten verändert sich, um die aktuell geladenen Dateien und Assemblies anzuzeigen. Dies verrät Ihnen alle Module, die von der Anwendung betroffen sind. Das LOKALFenster unten links (man muss eventuell auf die LOKAL-Registerkarte klicken, um es zu sehen) zeigt alle aktuellen Variablen sowie deren Werte an. Verwenden Sie das LOKALFenster, um festzustellen, ob die Variablen richtig eingestellt sind. Wenn Sie z.B. nicht herausfinden können, warum die Titelzeile nicht richtig angezeigt wird, schauen Sie im LOKALFenster unter this (Me in VB .NET), System.Windows.Forms.Form, WindowText nach und prüfen dort den eingetragenen Wert. Widerspricht er Ihren Erwartungen, wissen Sie, dass etwas vor diesem Haltepunkt schief gelaufen ist. Im LOKALFenster kann man sogar die Werte ändern, obwohl diese Änderungen nicht dauerhaft sind; man verliert sie, sobald die Fehlerbeseitigung beendet ist. Diese Information kann jedenfalls bei der Problemlösung hilfreich sein. Der CLR-Debugger lässt sich jedoch nicht als Editor missbrauchen: Man kann damit keinen Quellcode ändern. Zu diesem Zweck müssen Sie den WindowsEditor (NotePad) oder einen anderen bevorzugten Texteditor einsetzen, z.B. in Visual Studio .NET. Hat die Anwendung bei einem Haltepunkt angehalten, öffnen Sie das BEFEHLSFenster (unten rechts, oder wählen Sie FENSTER, DIREKT aus dem DEBUGGEN-Menü). In diesem Fenster können Sie Befehle Ihrer Anwendung ausführen, als ob sie sich im Quellcode

693

Debugging und Profiling

befänden. Tippen Sie in das BEFEHLSFenster MessageBox.Show("Gruss aus dem Befehlsfenster") und drücken Sie (¢). Der Debugger führt den Befehl aus und zeigt das Meldungsfeld an. Von diesem Fenster kann man praktisch jeden Befehl ausführen lassen. Als Nächstes öffnen Sie das ÜBERWACHEN-Fenster (aus dem DEBUGGEN/FENSTER-Menü während des Debugprozesses) und geben den Namen einer Variablen ein, die in Ihrer Seite benutzt wird (etwa rtbText), dann drücken Sie (¢). Nun können Sie die Variable beobachten, während Sie sich durch Ihre Anwendung bewegen, und sehen, ob ihr stets ein richtiger Wert zugewiesen wird. Tippen Sie Me in das ÜBERWACHEN-Fenster ein, können Sie alle Variablen sehen, die zu Me gehören. Diese beiden Fenster eignen sich gut dazu, jederzeit Informationen über die Anwendung zu liefern. Doch manchmal muss man die Applikation in Aktion sehen und nicht im »eingefrorenen« Zustand. Am oberen Rand des CLR-Debugger-Fensters befinden sich Schaltflächen, mit denen Sie die Ausführung der Anwendung in Echtzeit steuern können, so etwa das Springen zur nächsten Codezeile und das vollständige Beenden der Fehlerbeseitigung. Neben dem gelben Pfeil NÄCHSTE ANWEISUNG ANZEIGEN befinden sich drei Schaltflächen: EINZELSCHRITT, PROZEDURSCHRITT und AUSFÜHREN BIS RÜCKSPRUNG. Damit können Sie durch die Ausführung der Anwendung navigieren. Haben Sie etwa einen Haltepunkt auf eine Methode gesetzt, hält ein Klick auf PROZEDURSCHRITT die Ausführung bei jedem Statement in der Methode an. Das ist hilfreich, wenn man jede auszuführende Zeile sehen muss, doch es könnte etwas lästig werden, vor allem, wenn man so durch sehr viel Code »waten« muss. Wenn Sie auf einen Methodenaufruf stoßen, schickt der Debugger Sie nicht etwa zur entsprechenden Methode, sondern springt darüber. EINZELSCHRITT gleicht PROZEDURSCHRITT ein wenig, nur dass man in verschiedene Richtungen im Code verzweigen kann (etwa in andere Methoden), wenn es die Codelogik erfordert. Ein Klick auf die AUSFÜHREN BIS RÜCKSPRUNG -Schaltfläche, und die Anwendung führt die aktuelle Methode ohne anzuhalten aus, bis sie zum Beginn der nächsten Methode gelangt. Mit AUSFÜHREN BIS RÜCKSPRUNG können Sie also ganze Methoden überspringen, wenn Sie sicher wissen, dass sie in Ordnung sind. Zur Linken der SCHRITT-Schaltflächen befinden sich Steuerelemente, die wie die Steuertasten eine Rekorders aussehen: WEITER, ALLE UNTERBRECHEN, DEBUGGEN BEENDEN und NEU STARTEN. Diese Schaltflächen tun genau das, was auf ihnen steht, so dass man damit den Debug-Vorgang als Ganzes steuern kann.

Das Werkzeug CorDbg CorDbg.exe ist praktisch die Befehlszeilenversion des JIT-Debuggers DbgClr.exe. Er funkti-

oniert auf die gleiche Weise, doch statt eine grafische Benutzeroberfläche zu bedienen, muss man Befehle über die Eingabeaufforderung erteilen. Die Bedienung ist daher ein wenig unkomfortabler und das Tool wird hier nicht eingehend besprochen.

694

JIT-Debugging

Um das Programm CorDbg.exe zu starten, ohne zuerst eine Anwendung zu laden, geben Sie lediglich CorDbg in der Befehlszeile ein. Dann erteilen Sie den Befehl run anwendung name

Stellen Sie sicher, dass sich die zu prüfende Anwendung im selben Verzeichnis befindet, aus dem heraus Sie den Befehl erteilt haben. Verwendet man listing21.1.exe, sollte man etwas wie in Abbildung 21.8 zu sehen bekommen.

Abbildung 21.8: CorDbg ist ein befehlszeilengestützter Debugger, mit dem Sie der Ursache eines Problems auf den Grund gehen können.

Die Anwendung wird nun an der Main-Methode gestartet und wieder angehalten. Um zur nächsten Zeile der Ausführung zu gelangen, tippt man den Befehl si ein. So kann man sich zeilenweise durch die Anwendung bewegen, was etwas nervend ist. Ausführungszeilen sind mitunter etwas anderes als Codezeilen. Um sich zeilenweise durch Code zu bewegen, gibt man den Befehl next ein. Wenn man sich durch die Anwendung bewegt, sieht man zuweilen merkwürdige Bildschirmausgaben wie diese: (cordbg) next [0044] call (cordbg) next

dword ptr ds:[02FF17B8h]

[004a] mov (cordbg) next

ecx,ebx

[004c] call

dword ptr ds:[02FF2C18h]

Diese Befehle verraten Ihnen, was im Speicher vor sich geht, wenn die Anwendung läuft. dword ptr ist z.B. eine spezielle Anweisung, die auf eine Stelle im Arbeitsspeicher zeigt. Meistens hat Sie diese Information nicht zu interessieren, doch wenn Sie zu Ausführungsprozessen auf niederer Ebene hinuntergehen wollen, so erhalten Sie hierdurch Analysedaten. Informationen über die Befehle in CorDbg.exe finden Sie in der Dokumentation zum .NET Framework.

695

Debugging und Profiling

21.3 Profiling Ihrer Anwendung Profiling ist der Vorgang, bei dem eine Anwendung beobachtet und ihre verwendeten Ressourcen untersucht werden. Dieses Vorgehen ist sinnvoll, wenn man die Leistung einer Anwendung optimieren muss, weil man hier bemerkt, wo Engpässe auftreten und welche Codeabschnitte relativ lange brauchen, bis sie ausgeführt sind. Auf Grund dieser Informationen kann man seinen Quellcode neu strukturieren oder umschreiben, um mehr Leistung zu erzielen – eine Änderung, die stets willkommen sein dürfte. Leider steht für diese Aufgabe kein Werkzeug zur Verfügung. Doch dank der ProcessKlasse können Sie immerhin Ihr eigenes erstellen. Ein Prozess stellt auf Ihrem Computer eine ausgeführte Anwendung dar. Für die Prozesse sind Threads tätig und Prozesse werden im Windows Task-Manager angezeigt (vgl. Abbildung 21.9).

Abbildung 21.9: Der Task-Manager von Windows zeigt alle aktuell gestarteten Prozesse.

Die Process-Klasse lässt sich dazu einsetzen, jeden gerade ausgeführten Prozess zu untersuchen. Sie liefert Informationen wie etwa über die Speichermenge, die eine Anwendung beansprucht, ihren Anteil an Prozessorzeit und die Start- und Schlusszeiten einer bestimmten Methode. Somit sehen Sie nicht nur, wie die Leistung der Anwendung aussieht, sondern auch, wie sie gegenüber anderen Prozessen auf dem Computer abschneidet.

696

Profiling Ihrer Anwendung

Sehen Sie sich erst einmal eine Beispielanwendung an. Listing 21.2 verwendet die Process-Klasse zur Anzeige von statistischen Angaben über die aktuelle Anwendung. Listing 21.2: Prozesse überwachen 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: 5: using System.Diagnostics; 6: 7: namespace TYWinforms.Day21 { 8: public class Listing212 : Form { 9: private Label lblProcessor = new Label(); 10: private Label lblMemory = new Label(); 11: private Label lblTotProcessor = new Label(); 12: private Process objProcess = new Process(); 13: private System.Windows.Forms.Timer tmrProcess = new  System.Windows.Forms.Timer(); 14: 15: public Listing212() { 16: lblProcessor.Size = new Size(250,20); 17: lblProcessor.Location = new Point(10,10); 18: 19: lblMemory.Size = new Size(250,20); 20: lblMemory.Location = new Point(10,30); 21: 22: lblTotProcessor.Size = new Size(250,20); 23: lblTotProcessor.Location = new Point(10,50); 24: 25: tmrProcess.Tick += new EventHandler(this.Update); 26: tmrProcess.Interval = 500; 27: tmrProcess.Start(); 28: 29: objProcess = Process.GetCurrentProcess(); 30: 31: this.Text = ".NET Profiler"; 32: this.Controls.Add(lblProcessor); 33: this.Controls.Add(lblMemory); 34: this.Controls.Add(lblTotProcessor); 35: } 36: 37: private void Update(Object Sender, EventArgs e) { 38: objProcess.Refresh();

697

Debugging und Profiling

39: lblMemory.Text = "Speicher: " +  objProcess.PrivateMemorySize.ToString(); 40: lblProcessor.Text = "Private Prozessorzeit: " +  objProcess.PrivilegedProcessorTime.ToString(); 41: lblTotProcessor.Text = "Gesamte Prozessorzeit: " +  objProcess.TotalProcessorTime.ToString(); 42: } 43: 44: public static void Main() { 45: Application.Run(new Listing212()); 46: } 47: } 48: }

Zunächst sind wie immer die richtigen Namensräume zu importieren. In Zeile 5 importiert man den Namensraum System.Diagnostics, der die Process-Klasse enthält. In den Zeilen 9 bis 13 erstellen Sie ein paar Steuerelemente, darunter ein Process-Objekt und einen Timer. Das Objekt beobachtet noch keine Prozesse, sondern ist noch untätig. Das wird sich in Zeile 29 ändern. Im Konstruktor in den Zeilen 16 bis 23 initialisieren Sie die verschiedenen Label-Steuerelemente, die erstellt wurden. Jedes Bezeichnungsfeld zeigt einen Aspekt des zu beobachtenden Prozesses an. Der Timer ist so eingestellt, dass er jede halbe Sekunde ausgelöst wird (Zeile 26), damit Sie so die Label-Steuerele-

mente aktualisieren können; sonst würden Sie keine zutreffende Statistik erhalten. Der Tick-Ereignishandler ist die Update-Methode in Zeile 37 (gleich mehr dazu). In Zeile 29 rufen Sie die Methode GetCurrentProcess der Process-Klasse auf. Diese Zeile veranlasst das neue Process-Objekt dazu, den Prozess, der die Anwendung darstellt, zu überwachen. Sie betrachten also die Statistik der mit Listing 21.2 erstellten Anwendung. Obwohl das Beobachten des Prozesses der aktuellen Anwendung angenehm ist, so ist es doch nicht immer möglich oder sinnvoll. Man kann eine Reihe anderer Methoden der Process-Klasse benutzen, um Prozesse anderer Anwendungen zu überwachen. GetProcessById etwa beschafft einen Prozess anhand der Identifikation im Betriebssystem. (Wenn Sie die Prozess-ID [PID] nicht im Task-Manager sehen, klicken Sie auf das ANSICHT-Menü, wählen SPALTEN AUSWÄHLEN und im Auswahlfenster PID (PROZESS-ID).) Diese Methode eignet sich gut, wenn man die PID des zu beobachtenden Prozesses kennt, aber das ist nicht immer der Fall. GetProcesses liefert alle gerade im System ablaufenden Prozesse, so dass man mehrere Anwendungen zugleich beobachten kann. GetProcessesByName schließlich liefert ein Array von Processes, die dem angegebenen

698

Profiling Ihrer Anwendung

Namen entsprechen. Dieser Name ist meist der der ausführbaren Datei der Anwendung, aber ohne die Erweiterung .exe. Würde man z.B. listing21.1 .exe ausführen, könnte man ihren Prozess mit folgendem Befehl beobachten: objProcess = Process.GetProcessesByName("listing21.1")[0];

Denken Sie daran, dass diese Methode ein Array liefert. Sie müssen also angeben, welches Element im Array Sie beobachten wollen. Meist ist im Array nur ein Element vorhanden; gibt man also [0] an (oder (0) in VB .NET), dann funktioniert das gut. Der Prozess muss gerade ablaufen, sonst erhalten Sie eine Fehlermeldung (eine gute Stelle für einen try-catch-Block). Springen Sie nun nach unten zur Update-Methode. Wenn man anfangs eine der Eigenschaften der Process-Klasse anzeigt, dann holt man einen Schnappschuss dieser Eigenschaft. Anders ausgedrückt: Die Eigenschaften aktualisieren sich nicht selbst. Daher muss man die Process.Refresh-Methode aufrufen, um aktualisierte Werte zu beschaffen, wie in Zeile 38 zu sehen. Die Zeilen 39 bis 41 schließlich zeigen diverse Eigenschaften des aktuell ablaufenden Prozesses an. Abbildung 21.10 zeigt das Ergebnis, nachdem die Anwendung für einige Sekunden ausgeführt worden ist.

Abbildung 21.10: Sie können zusehen, wie die Ressourcenstatistik Ihrer Anwendung dynamisch aktualisiert wird.

Die Process-Klasse liefert etliche Informationen über eine bestimmte Anwendung, von der beanspruchten Prozessorzeit bis zur Titelzeile des aktuell geöffneten Fensters. Tabelle 21.2 führt alle nützlichen Eigenschaften von Process auf; es sind eine ganze Menge.

699

Debugging und Profiling

Eigenschaft

Beschreibung

BasePriority

Liefert die Priorität des aktuellen Prozesses (vgl. Tag 19).

ExitCode

Ein Integer, den eine Anwendung liefert, wenn sie beendet wird. Null bedeutet meist, dass das Schließen normal erfolgte; andere Werte können auf Fehler hinweisen.

ExitTime

Gibt die Zeit an, zu der der verknüpfte Prozess endete.

Handle

Liefert den Handle, der mit einem Prozess verknüpft ist.

HandleCount

Liefert die Anzahl der Handles, die mit einem Prozess verknüpft sind.

HasExited

Gibt an, ob der Prozess angehalten wurde.

Id

Gibt den Wert der Prozess-Identitätsnummer (PID) an.

MachineName

Gibt den Namen des Rechners an, auf dem der Prozess gerade läuft.

MainModule

Liefert ein ProcessModule-Objekt, das das Modul darstellt, welches die Anwendung startete (normalerweise eine .exe- oder .dll-Datei).

MainWindowHandle

Gibt den Handler für das Primärfenster des Prozesses an.

MainWindowTitle

Gibt die Titelzeile des Primärfensters des Prozesses an.

MaxWorkingSet

Gibt den maximalen Speicherplatz an, der für einen vorliegenden Prozess reserviert wird.

MinWorkingSet

Gibt den minimalen Speicherplatz an, der für einen vorliegenden Prozess reserviert wird: Das Speichervolumen, das eine Anwendung nutzt, sinkt nie unter diese Marke.

Modules

Liefert ein Array von ProcessModule-Objekten, die eine Anwendung geladen hat (z.B. .exe- und .dll-Dateien)

NonpagedSystemMemorySize Gibt den Speicherumfang an, der einem Prozess zugewiesen ist und

der nicht der Auslagerungsdatei zugeschlagen werden kann. PagedMemorySize

Gibt den Speicherumfang an, der einem Prozess zugewiesen ist und der der Auslagerungsdatei zugeschlagen werden darf.

PagedSystemMemorySize

Gibt die Menge des Systemspeichers an, die dem Prozess zugewiesen ist und die in die Auslagerungsdatei geschrieben werden kann.

PeakPagedMemorySize

Gibt die maximale Speichermenge an, die in die Auslagerungsdatei geschrieben wird, welche einem Prozess zugewiesen wurde.

Tabelle 21.2: Prozessbezogene Eigenschaften von Process

700

Profiling Ihrer Anwendung

Eigenschaft

Beschreibung

PeakVirtualMemorySize

Gibt die maximale Menge an virtuellem Speicher an, die ein Prozess verlangt hat.

PeakWorkingSet

Gibt die maximale Menge an virtuellem Speicher an, die ein Prozess zu einem Zeitpunkt verlangt hat.

PriorityBoostEnabled

Gibt an, ob dem Prozess vorübergehend eine höhere Priorität eingeräumt (und er so schneller ausgeführt) werden soll, wenn sein Hauptfenster den Fokus hat.

PriorityClass

Ein PriorityClass-Objekt, das den Prioritätsgrad darstellt, den ein Prozess besitzt wie etwa Normal oder High).

PrivateMemorySize

Gibt die Speichermenge an, die dem aktuellen Prozess zugewiesen ist, und die andere Anwendungen nicht teilen können.

PrivilegedProcessorTime

Gibt die für einen Codeverarbeitungsprozess im Betriebssystemkern verbrauchte Prozessorzeit an.

ProcessName

Der Name des aktuellen Prozesses.

ProcessorAffinity

Gibt die CPUs an, auf denen ein Prozess in einem Mehrprozessorsystem laufen kann.

Responding

Gibt an, ob die Benutzeroberfläche des aktuellen Prozesses auf Benutzereingaben reagiert (oder ob sich das Programm »aufgehängt« hat).

StandardError

Beschafft ein StreamReader-Objekt, um Fehlermeldungen der Anwendung zu lesen.

StandardInput

Holt ein StreamWriter-Objekt, das sich dazu verwenden lässt, dem Prozess Eingaben zu schicken.

StandardOutput

Holt ein StreamReader–Objekt, um Standardausgaben der Anwendung zu lesen.

StartInfo

Informationen, die mit dem Prozess verbunden sind, wenn er gestartet wird (so etwa der Name der ausführbaren Datei, die den Prozess startet).

StartTime

Der Zeitpunkt, zu dem der Prozess gestartet wurde.

Threads

Die Thread-Objekte, die mit dem aktuellen Prozess verknüpft sind.

TotalProcessorTime

Gibt die gesamte Zeit an, die die CPU für die Ausführung dieses Prozesses aufgewendet hat (also die Summe aus den Werten UserProcessorTime und PrivilegedProcessorTime).

Tabelle 21.2: Prozessbezogene Eigenschaften von Process (Forts.)

701

Debugging und Profiling

Eigenschaft

Beschreibung

UserProcessorTime

Gibt die gesamte Zeit an, die die CPU für die Ausführung von Code innerhalb der Anwendung, die von diesem Prozess repräsentiert wird, aufgewendet hat.

VirtualMemorySize

Gibt die aktuelle Größe des virtuellen Speichers des gegenwärtigen Prozesses an.

WorkingSet

Gibt an, wie viel Speicher der damit verbundene Prozess gerade beansprucht.

Tabelle 21.2: Prozessbezogene Eigenschaften von Process (Forts.)

Bei all den verschiedenen Eigenschaften ist es mitunter schwierig herauszufinden, welche man für das Profiling einsetzen soll. Das hängt im Grunde von der Art der zu erstellenden Anwendung ab. Handelt es sich etwa um ein Datenbankprogramm, will man sicher die verschiedenen speicherrelevanten Eigenschaften sehen. Ein hoher PrivateMemorySizeWert könnte bedeuten, dass man umfangreiche Datenbankinformationen im Speicher hält, was nicht unbedingt eine schlechte Idee ist. Ein hoher VirtualMemorySize-Wert bedeutet, dass sich eine große Informationsmenge, die sich im Speicher befinden sollte, noch auf der Festplatte (in der Auslagerungsdatei) befindet, was der Leistung abträglich ist. Vielleicht beschaffen Sie zu viele Daten aus der Datenbank und ein Teil davon wird auf die Festplatte ausgelagert. Eine mögliche Lösung besteht darin, nach und nach kleinere Datenmengen zu holen. Entwerfen Sie eine Anwendung mit komplizierten Berechnungen darin, so sollten Sie die verschiedenen Eigenschaften beobachten, die die Prozessorzeit betreffen. Eine 3D-CADAnwendung beispielsweise muss zahlreiche komplizierte Berechnungen der linearen Algebra ausführen, die Prozessorzeit fressen. Man kann die Eigenschaft UserProcessorTime überwachen, um herauszufinden, wie viel Prozessorzeit Ihre Berechnungen verschlingen. Außerdem lässt sich beobachten, welche Auswirkungen das Hinzufügen weiterer Threads auf die Leistung der Anwendung hat.

21.4 Zusammenfassung Debugging und Profiling sind zwei Schritte in der Anwendungsentwicklung, die leider häufig übergangen werden. Beide sind jedoch notwendig, um sicherzustellen, dass eine Anwendung stabil und effizient läuft. Mit Hilfe des .NET Frameworks lassen sich beide Schritte leicht realisieren.

702

Fragen und Antworten

Als Erstes haben Sie den try-catch-Block kennen gelernt. Damit können Sie Fehler behandeln, die nicht immer vorherzusehen sind. Solche Fehler können in Code vorkommen, der mit E/A-Operationen zu tun hat, oder in Code, der von bestimmten Benutzereingaben abhängt. Man kann schließlich nie vorhersagen, was ein Benutzer tun wird. Als Just-In-Time- (JIT-) Debugging wird der Vorgang bezeichnet, bei dem einer laufenden Anwendung ein Debugger angehängt wird, damit Sie den Ablauf ihrer Ausführung beobachten können. Der CLR-Debugger und CorDbg sind zwei JIT-Debugger, mit denen man alles sehen kann, was in einer Anwendung passiert, von Codeausführung bis hin zu Verweisen auf Speicherstellen. Profiling schließlich ist der Vorgang, bei dem die Ressourcen, die eine Anwendung bei der Ausführung beansprucht, beobachtet werden. Wenn man den Speicherbedarf und den Anteil an Prozessorzeit betrachtet, kann man herausfinden, ob und wie eine Anwendung optimiert werden sollte. Sie haben das Ende Ihrer 21 Tage erreicht. Ich hoffe, die Reise durch Windows Forms hat Ihnen gefallen. Vergessen Sie nicht, sich das letzte Bonusprojekt anzusehen, um alles, was Sie in den letzten drei Wochen gelernt haben, noch einmal Revue passieren zu lassen.

21.5 Fragen und Antworten F

Kann ich den Windows-Systemmonitor (perfmon.exe) einsetzen, um meine Anwendungen zu untersuchen? A

Natürlich. Öffnen Sie den Systemmonitor (z.B. über AUSFÜHREN im STARTMenü, wo Sie den Dateinamen eingeben) und klicken Sie im SystemmonitorFenster auf die Plus-Schaltfläche (HINZUFÜGEN) in der Symbolleiste, um dem Fenster Informationen über eine gerade ablaufende Anwendung hinzuzufügen. Bei der Option DATENOBJEKT wählen Sie .NET CLR SPEICHER. Der Befehl LEISTUNGSINDIKATOREN WÄHLEN über dem Listenfeld führt verschiedene Aspekte auf, die man beobachten kann. Im Feld INSTANZ WÄHLEN des Listenfeldes rechts daneben sollten Sie Ihre Anwendung sehen (neben mmc und _Global_), z.B. listing21.1.exe. Markieren Sie sie und klicken Sie auf HINZUFÜGEN, um diesen Aspekt der Anwendung zu überwachen. Sollte Ihnen dieses Standardverhalten nicht genügend Informationen liefern, so können Sie einen benutzerdefinierten Systemmonitor mit der PerformanceCounter-Klasse erstellen. Mit dieser Klasse lassen sich Aspekte einer Anwendung definieren, die man verfolgen will und die als Leistungsindikatoren bezeichnet werden.

703

Debugging und Profiling

21.6 Workshop Dieser Workshop soll Ihnen helfen, sich besser an die heute behandelten Begriffe zu erinnern. Es hilft Ihnen sehr, die Antworten vollständig zu verstehen, bevor Sie weiterlesen. Die Antworten zu den Quizfragen und den Übungen finden Sie in Anhang A.

Quiz 1. Wahr oder falsch? Ein catch-Block, der eine Ausnahme vom Typ IOException angibt, wird auch Ausnahmen vom Typ SystemException abfangen. 2. Ist der folgende Code richtig? Process [] objProcess = Process.GetProcessesByName ("listing21.2.exe");

3. Wie erzeugt man eine Programmdatenbankdatei? 4. Was versteht man unter einem Haltepunkt? 5. Nennen Sie drei Eigenschaften der Exception-Klasse. 6. Wie kann man eigene Fehlermeldungen auslösen?

Übung Schreiben Sie eine Anwendung, die dem Windows Task-Manager gleicht. Sie sollte den Benutzer jeden ablaufenden Prozess über ein Listenfeld auswählen lassen. In Bezeichnungsfeldern sollten die Eigenschaften Id, MainModule, PrivateMemorySize und TotalProcessorTime des Prozesses sowie die Zahl der aktiven Threads zu sehen sein. Denken Sie auch daran, try-catch-Blöcke einzusetzen!

704

Woche 3 – Rückblick Projekt 3: Das Textprogramm vervollständigen In diesem letzten Wochenrückblick bauen Sie auf die in den letzten zwei Wochenrückblicken erstellte NetWord-Anwendung auf. Beim letzten Mal machten Sie daraus eine MDIApplikation und legten eine Document-Klasse an, die jedes untergeordnete Dokument im übergeordneten Formular darstellt. Diesmal gestalten Sie den Funktionsumfang noch Wordähnlicher, indem Sie ein benutzerdefiniertes Windows Forms-Steuerelement erzeugen.

Eine Klasse für eine neue Seite erzeugen Zunächst werfen wir einen Blick auf den vorhandenen Code. Da haben wir die Datei NetWord.cs, die unverändert bleiben soll. Sie ist der Haupteinstiegspunkt für die Anwendung. Des Weiteren findet sich die Datei FindDialog.cs, die ein benutzerdefiniertes DialogfeldSteuerelement enthält, mit dem der Benutzer im Dokument nach Text suchen kann. Schließlich liegt noch die Datei Document.cs vor, die den Großteil der Benutzeroberfläche enthält, ein RichTextBox-Steuerelement, in das der Benutzer sein Dokument eingibt. Diese Klasse verfügt zudem über Menüs, um Standardfunktionen bereitzustellen, und nutzt die FindDialog-Klasse für Suchfunktionen. Soweit arbeitet die Anwendung recht ähnlich wie der Windows-Editor. Sie präsentiert einen einzelnen Dateneingabepunkt, der einfach größer wird, je mehr Text der Benutzer eingibt. Dieses Verhalten steht aber im Gegensatz zu Microsoft Word, in dem Dokumente durch tatsächliche Seiten dargestellt werden. In Word kann man seinen Text in logische Einheiten aufteilen und bündeln, die sich manipulieren lassen: zum Drucken, Durchsuchen, Setzen von Seitenrändern usw. Durch das Anlegen einer neuen Page-Klasse bauen wir eine ähnliche Funktionalität ein, um so auch die vorhandene Dokumentenschnittstelle zu ersetzen. Diese Klasse erweitert das RichTextBox-Steuerelement und stellt Funktionalität bereit, die nicht in diesem Steuerelement mitgeliefert wird. Listing R3.1 zeigt den Code für die neue Page-Klasse.

705

Woche 3 – Rückblick

Listing R3.1: Die Page-Klasse 1: using System; 2: using System.Drawing; 3: using System.Windows.Forms; 4: using System.Drawing.Printing; 5: 6: namespace TYWinforms.P3 { 7: public class Page : RichTextBox { 8: public int PageNumber; 9: private int intCharIndex; 10: private int intLineIndex; 11: public event EventHandler Filled; 12: 13: private PageSettings objPageSettings; 14: 15: public int CharIndex { 16: get { return intCharIndex; } 17: } 18: 19: public int LineIndex { 20: get { return intLineIndex; } 21: } 22: 23: public Page(Form frmParent, PageSettings objPageSettings, int  intPageNumber) { 24: intCharIndex = 1; 25: intLineIndex = 1; 26: PageNumber = intPageNumber; 27: int intWidth = (int)(.96 * (objPageSettings.Bounds.Width  (objPageSettings.Margins.Left + objPageSettings.Margins.Right))); 28: int intHeight = (int)(.96 * (objPageSettings.Bounds.Height  (objPageSettings.Margins.Top + objPageSettings.Margins.Bottom))); 29: 30: this.Parent = frmParent; 31: this.ScrollBars = RichTextBoxScrollBars.None; 32: this.Font = frmParent.Font; 33: this.objPageSettings = objPageSettings; 34: this.RightMargin = objPageSettings.Bounds.Width - (int)(.96 *  (objPageSettings.Margins.Left + objPageSettings.Margins.Right)); 35: this.Size = new Size(intWidth,intHeight); 36: } 37: 38: public void SetFont(Font fnt, Color clr) { 39: this.SelectionFont = fnt; 40: this.SelectionColor = clr;

706

Woche 3 – Rückblick

41: } 42: 43: protected virtual void OnFilled(EventArgs e) { 44: Filled(this, e); 45: } 46: 47: public void UpdatePageSettings(PageSettings objPageSettings) { 48: this.objPageSettings = objPageSettings; 49: 50: this.RightMargin = objPageSettings.Bounds.Width - (int)(.96 *  (objPageSettings.Margins.Left + objPageSettings.Margins.Right)); 51: } 52: 53: protected override void OnSelectionChanged(EventArgs e) { 54: intLineIndex = this.GetLineFromCharIndex (this.SelectionStart) + 1; 55: 56: int i = this.SelectionStart; 57: while (this.GetPositionFromCharIndex(i).X > 1) { 58: --i; 59: } 60: 61: intCharIndex = SelectionStart - i + 1; 62: 63: if (this.GetPositionFromCharIndex (this.SelectionStart).Y +  (this.SelectionFont.GetHeight()*2) >= this.ClientSize.Height) { 64: OnFilled(new EventArgs()); 65: } 66: 67: base.OnSelectionChanged(e); 68: } 69: } 70: }

In Zeile 7 sehen Sie die Deklaration der Page-Klasse, die von der RichTextBoxKlasse abgeleitet ist. Dadurch können wir (vgl. Tag 18) ein neues Steuerelement erstellen, das auf dem abgeleiteten fußt. Das ist ideal, denn wir wollen das Steuerelement nicht von Grund auf neu erstellen. Die Zeilen 8 bis 21 zeigen diverse Eigenschaften, die in der Anwendung eingesetzt werden. In Zeile 11 erstellen wir ein benutzerdefiniertes Ereignis namens Filled. Es wird immer dann ausgelöst, wenn die Grenzen einer Seite überschritten werden (entweder indem man zuviel Text eingibt oder indem man einen Bildlauf durch die Seite ausführt). Das umgebende Steuerelement kann mit diesem Ereignis herausfinden, ob eine neue Seite hinzuzufügen ist.

707

Woche 3 – Rückblick

Wir wollen die Eigenschaften CharIndex und LineIndex (in den Zeilen 15 und 19) zur Verfügung stellen, damit das umgebende Steuerelement (welches die Document-Klasse wird) auf diese Variablen zugreifen kann. Wie wichtig diese Variablen sind, werden Sie später noch sehen. Beachten Sie die Syntax der Eigenschaftendeklaration. Der Konstruktor übernimmt ab Zeile 23 ein paar Eigenschaften. Die erste, frmParent, stellt das umgebende Steuerelement dar. objPageSettings repräsentiert die Einstellungen, die durch das Containersteuerelement festgelegt wurden (z.B. Ränder, Papiergröße und Seitenausrichtung). Die letzte Eigenschaft namens intPageNumber stellt die Nummer der Seite im umgebenden Steuerelement dar. Wenn die Anwendung mehr als eine Seite enthält, lässt sich mit dieser Eigenschaft die Nummer der aktuellen Seite herausfinden. Die Klasse, die eine Instanz dieser Page-Klasse anlegt, muss beim Erstellen neuer Seiten alle drei dieser Eigenschaften liefern. Die Zeilen 24 bis 35 instantiieren verschiedene Eigenschaften des Steuerelements. Besonders interessant ist dabei die RightMargin-Eigenschaft in Zeile 34. RightMargin ist eine Eigenschaft der RichTextBox-Klasse, die festlegt, wie viel Text in eine Zeile passt – d.h. der rechte Rand des Textfensters . Dies wird dadurch bestimmt, dass man die objPageSettings-Variable untersucht, die übergeben wurde; man subtrahiert die Ränder von der Gesamtseitenbreite, um den richtigen Wert zu erhalten. Die SetFont-Methode in Zeile 38 wird vom umgebenden Objekt benutzt, um die in der neuen Page-Klasse verwendeten Schriftarten festzulegen. Sie nehmen sie in Gebrauch, wenn wir die überarbeitete Document-Klasse untersuchen. In Zeile 43 wird die OnFilled-Methode erstellt, die lediglich das erwähnte FilledEreignis ausgelöst. Die UpdatePageSettings-Methode in Zeile 47 sollte jedes Mal ausgeführt werden, wenn sich die Seiteneinstellungen geändert haben. Da diese nicht in der Page-Klasse enthalten sind (sondern in der Document-Klasse), muss diese Methode mit dem neuen Seiteneinstellungsobjekt aufgerufen werden. Das RightMargin-Mitglied wird durch die neuen Einstellungen auf ähnliche Weise aktualisiert, wie es im Konstruktor initialisiert wurde. OnSelectionChanged in Zeile 53 ist die letzte Methode in der Page-Klasse. Diese Funktion befand sich in der Document-Klasse, wurde aber hierher verschoben, um einen logischeren Ablauf zu ermöglichen. Im Wesentlichen bringt die Methode die Werte intLineIndex und intCharIndex auf den jeweils aktuellen Stand im Steuerelement. Das Containersteuerelement kann daraufhin die

708

Woche 3 – Rückblick

öffentlichen Eigenschaften LineIndex und CharIndex nutzen, um die aktuelle Cursorposition anzuzeigen. Der einzige Unterschied in dieser Methode ist das Stück in den Zeilen 63 bis 65. In der Page-Klasse brauchen wir eine Möglichkeit, das umgebende Objekt wissen zu lassen, wann eine Seite voll ist (jedes Page-Objekt repräsentiert ja eine bedruckte Seite). Dies erledigt die Prüfung in Zeile 63; sie verwendet die Methode GetPositionFromCharIndex, um die Y-Koordinate des aktuellen Zeichens festzustellen. Ist dieser Wert ausreichend hoch (im Vergleich zur Höhe des Steuerelements), bedeutet dies, dass das Steuerelement voll ist und eine neue Seite angelegt werden sollte (beachten Sie die Berücksichtigung der Schrifthöhe bei der Y-Koordinate in Zeile 63; der Grund dafür ist der, dass die YKoordinate die Position am oberen Rand des Textes liefert, wir aber den unteren Rand benötigen, um herauszufinden, ob bereits der untere Rand des Steuerelements erreicht ist). Die OnFilled-Methode löst unser benutzerdefiniertes Filled-Ereignis aus, um dem umgebenden Steuerelement mitzuteilen, dass die Seite voll ist. Speichern Sie dieses Listing als Page.cs und kompilieren Sie es mit dem folgenden Befehl: csc /t:library /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll Page.cs

Nun sind Sie bereit, die Document-Klasse so zu verändern, dass statt der normalen RichTextBox- die neue Page-Klasse eingesetzt wird. Listing R3.2 zeigt den Code für Document.cs in seiner Gesamtheit. Das Listing ist umfangreich, doch der Großteil des Codes hat sich nicht geändert, so dass wir es nicht vollständig untersuchen. Nähere Details finden Sie im Wochenrückblick 2. Listing R3.2: Die revidierte Document-Klasse 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:

using using using using using using using

System; System.Windows.Forms; System.IO; System.Drawing; System.Drawing.Printing; System.ComponentModel; System.Collections;

namespace TYWinforms.P3 { public class Document : Form { public ArrayList arrPages = new ArrayList(); public int intCurrentPage; private Panel pnlView = new Panel();

709

Woche 3 – Rückblick

15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60:

710

private private private private

ContextMenu cmnuDocument = new ContextMenu(); StatusBar sbarMain = new StatusBar(); StatusBarPanel spnlLine = new StatusBarPanel(); VScrollBar scrlbar = new VScrollBar();

private MainMenu mnuMain = new MainMenu(); private private private private private private private private private private private private private

MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem MenuItem

mniFile = new MenuItem("File"); mniSave = new MenuItem("Save"); mniPageSetup = new MenuItem ("Page Setup..."); mniPrintPreview = new MenuItem ("Print Preview"); mniPrint = new MenuItem("Print..."); mniEdit = new MenuItem("Edit"); mniUndo = new MenuItem("Undo"); mniCut = new MenuItem("Cut"); mniCopy = new MenuItem("Copy"); mniPaste = new MenuItem("Paste"); mniFind = new MenuItem("Find..."); mniFormat = new MenuItem("Format"); mniFont = new MenuItem("Font...");

private private private private

Font fntCurrent = new Font ("Times New Roman", 10); Color fntColor = Color.Black; FontDialog dlgFont = new FontDialog(); PageSetupDialog dlgPageSetup = new PageSetupDialog();

private PageSettings objPageSettings = new PageSettings(); private PrinterSettings objPrintSettings = new PrinterSettings(); private StringReader objReader; public Document(string strName) { mniSave.Click += new EventHandler(this.FileClicked); mniPageSetup.Click += new EventHandler(this.FileClicked); mniPrintPreview.Click += new EventHandler(this.FileClicked); mniPrint.Click += new EventHandler(this.FileClicked); mnuMain.MenuItems.Add(mniFile); mniFile.MergeType = MenuMerge.MergeItems; mniFile.MenuItems.Add(mniSave); mniFile.MenuItems.Add("-"); mniFile.MenuItems.Add(mniPageSetup); mniFile.MenuItems.Add(mniPrintPreview); mniFile.MenuItems.Add(mniPrint); mniFile.MergeOrder = 1;

Woche 3 – Rückblick

61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106:

mniEdit.MergeOrder = 2; mniFormat.MergeOrder = 3; mniSave.MergeOrder = 30; mniFile.MenuItems[1].MergeOrder = 35; mniPageSetup.MergeOrder = 40; mniPrintPreview.MergeOrder = 45; mniPrint.MergeOrder = 50; sbarMain.ShowPanels = true; sbarMain.Font = new Font("Arial", 10); sbarMain.Panels.Add(spnlLine); spnlLine.AutoSize = StatusBarPanelAutoSize.Spring; spnlLine.Alignment = HorizontalAlignment.Right; mniUndo.Click += new EventHandler(this.EditClicked); mniCut.Click += new EventHandler(this.EditClicked); mniCopy.Click += new EventHandler(this.EditClicked); mniPaste.Click += new EventHandler(this.EditClicked); mniFind.Click += new EventHandler(this.EditClicked); mniFont.Click += new EventHandler(this.FormatClicked); mnuMain.MenuItems.Add(mniEdit); mnuMain.MenuItems.Add(mniFormat); mniUndo.ShowShortcut = true; mniCut.ShowShortcut = true; mniCopy.ShowShortcut = true; mniPaste.ShowShortcut = true; mniFind.ShowShortcut = true; mniEdit.MenuItems.Add(mniUndo); mniEdit.MenuItems.Add("-"); mniEdit.MenuItems.Add(mniCut); mniEdit.MenuItems.Add(mniCopy); mniEdit.MenuItems.Add(mniPaste); mniEdit.MenuItems.Add("-"); mniEdit.MenuItems.Add(mniFind); mniFormat.MenuItems.Add(mniFont); cmnuDocument.MenuItems.Add(mniCut.CloneMenu()); cmnuDocument.MenuItems.Add(mniCopy.CloneMenu()); cmnuDocument.MenuItems.Add(mniPaste.CloneMenu()); cmnuDocument.MenuItems.Add("-"); cmnuDocument.MenuItems.Add(mniFont.CloneMenu());

711

Woche 3 – Rückblick

107: cmnuDocument.Popup += new EventHandler (this.HandleContext); 108: pnlView.ContextMenu = cmnuDocument; 109: 110: scrlbar.Dock = DockStyle.Right; 111: scrlbar.Scroll += new ScrollEventHandler (this.ScrollView); 112: scrlbar.Maximum = 10; 113: 114: this.Text = strName; 115: this.Font = new Font("Courier New", 12); 116: this.Size = new Size(800, 900); 117: this.Name = "NetWord"; 118: this.WindowState = FormWindowState.Maximized; 119: this.Menu = mnuMain; 120: this.Closing += new CancelEventHandler (this.DocumentClosing); 121: 122: pnlView.AutoScroll = false; 123: pnlView.BackColor = Color.DarkGray; 124: pnlView.Width = this.Width - 16; 125: pnlView.Height = this.Height - 50; 126: pnlView.Location = new Point(0,0); 127: pnlView.Font = fntCurrent; 128: 129: this.Controls.Add(scrlbar); 130: this.Controls.Add(sbarMain); 131: this.Controls.Add(pnlView); 132: AddPage(); 133: } 134: 135: private void ScrollView(Object Sender, ScrollEventArgs e) { 136: pnlView.Top = 0 - (e.NewValue * 30); 137: } 138: 139: private void DocumentCreated(Object Sender, EventArgs e) { 140: AddPage(); 141: } 142: 143: private void AddPage() { 144: int intHeight; 145: 146: intHeight = objPageSettings.Bounds.Height - (int)(.96 *  (objPageSettings.Margins.Top + objPageSettings.Margins.Bottom)); 147: 148: Page objPage = new Page(this,objPageSettings,arrPages.Count+1); 149: pnlView.Height = (arrPages.Count+1) * intHeight; 150: pnlView.Controls.Add(objPage); 151: scrlbar.Maximum = (30 * arrPages.Count+1) + 15;

712

Woche 3 – Rückblick

152: 153: objPage.Location = new Point(50,intHeight * arrPages.Count + 5); 154: objPage.Filled += new EventHandler (this.PageFilled); 155: objPage.Enter += new EventHandler (this.PageGotFocus); 156: objPage.SelectionChanged += new EventHandler (this.UpdateStatus); 157: 158: arrPages.Add(objPage); 159: objPage.Focus(); 160: } 161: 162: private void PageFilled(Object Sender, EventArgs e) { 163: if (intCurrentPage == arrPages.Count) { 164: AddPage(); 165: } else { 166: ((Page)arrPages[intCurrentPage]).Focus(); 167: } 168: } 169: 170: private void PageGotFocus(Object Sender, EventArgs e) { 171: intCurrentPage = ((Page)Sender).PageNumber; 172: } 173: 174: private void UpdateStatus(Object Sender, EventArgs e) { 175: Page tmpPage = (Page)arrPages[intCurrentPage-1]; 176: 177: spnlLine.Text = "Page " + intCurrentPage.ToString() + " Line " +  tmpPage.LineIndex.ToString() + " Char " + tmpPage.CharIndex.ToString(); 178: } 179: 180: private void HandleContext(Object Sender, EventArgs e) { 181: if (((Page)arrPages[intCurrentPage-1]).SelectionLength == 0) { 182: cmnuDocument.MenuItems[0].Enabled = false; 183: cmnuDocument.MenuItems[1].Enabled = false; 184: } else { 185: cmnuDocument.MenuItems[0].Enabled = true; 186: cmnuDocument.MenuItems[1].Enabled = true; 187: } 188: } 189: 190: private void FileClicked(Object Sender, EventArgs e) { 191: MenuItem mniTemp = (MenuItem)Sender; 192: PrintDocument pd; 193: String strText; 194: 195: switch (mniTemp.Text) { 196: case "Save":

713

Woche 3 – Rückblick

197: FileInfo filTemp = new FileInfo(this.Text); 198: if (filTemp.Extension == ".rtf") { 199: ((Page)arrPages[intCurrentPage-1]).SaveFile(this.Text,  RichTextBoxStreamType.RichText); 200: } else { 201: ((Page)arrPages[intCurrentPage-1]).SaveFile(this.Text,  RichTextBoxStreamType.PlainText); 202: } 203: ((Page)arrPages[intCurrentPage-1]). Modified = false; 204: break; 205: case "Page Setup...": 206: dlgPageSetup.PageSettings = objPageSettings; 207: dlgPageSetup.ShowDialog(); 208: 209: foreach (Page tmpPage in arrPages) { 210: tmpPage.UpdatePageSettings (objPageSettings); 211: } 212: 213: break; 214: case "Print Preview": 215: strText = ""; 216: 217: foreach (Page tmpPage in arrPages) { 218: strText += tmpPage.Text; 219: } 220: 221: objReader = new StringReader(strText); 222: 223: pd = new PrintDocument(); 224: pd.DefaultPageSettings = objPageSettings; 225: pd.PrintPage += new PrintPageEventHandler (this.PrintIt); 226: 227: PrintPreviewDialog dlgPrintPreview = new  PrintPreviewDialog(); 228: dlgPrintPreview.Document = pd; 229: 230: dlgPrintPreview.ShowDialog(); 231: break; 232: case "Print...": 233: PrintDialog dlgPrint = new PrintDialog(); 234: dlgPrint.PrinterSettings = new PrinterSettings(); 235: if (dlgPrint.ShowDialog() == DialogResult.OK) { 236: strText = ""; 237: 238: foreach (Page tmpPage in arrPages) { 239: strText += tmpPage.Text;

714

Woche 3 – Rückblick

240: } 241: 242: objReader = new StringReader(strText); 243: 244: pd = new PrintDocument(); 245: 246: pd.DefaultPageSettings = objPageSettings; 247: pd.PrintPage += new PrintPageEventHandler (this.PrintIt); 248: pd.Print(); 249: } 250: break; 251: case "Exit": 252: this.Close(); 253: break; 254: } 255: } 256: 257: private void PrintIt(Object Sender, PrintPageEventArgs e) { 258: Font fntPrint = this.Font; 259: int count = 0; 260: float yPos = 0; 261: float lpp = e.MarginBounds.Height /  fntPrint.GetHeight(e.Graphics); 262: float fltTopMargin = e.MarginBounds.Top; 263: float fltLeftMargin = e.MarginBounds.Left; 264: String strLine = null; 265: 266: while (count < lpp && ((strLine = objReader.ReadLine()) != null)) { 267: yPos = fltTopMargin + (count * fntPrint.GetHeight(e.Graphics)); 268: 269: e.Graphics.DrawString(strLine, fntPrint, Brushes.Black,  fltLeftMargin, yPos, new StringFormat()); 270: 271: count++; 272: } 273: 274: if (strLine != null) { 275: e.HasMorePages = true; 276: } else { 277: e.HasMorePages = false; 278: } 279: } 280: 281: private void EditClicked(Object Sender, EventArgs e) { 282: MenuItem mniTemp = (MenuItem)Sender;

715

Woche 3 – Rückblick

283: Page tmpPage = (Page)arrPages[intCurrentPage-1]; 284: 285: switch (mniTemp.Text) { 286: case "Undo": 287: tmpPage.Undo(); 288: break; 289: case "Cut": 290: if (tmpPage.SelectedRtf != "") { 291: tmpPage.Cut(); 292: } 293: break; 294: case "Copy": 295: if (tmpPage.SelectedRtf != "") { 296: tmpPage.Copy(); 297: } 298: break; 299: case "Paste": 300: tmpPage.Paste(); 301: break; 302: case "Find...": 303: FindDialog dlgFind = new FindDialog(this); 304: dlgFind.Show(); 305: break; 306: } 307: } 308: 309: private void FormatClicked(Object Sender, EventArgs e) { 310: MenuItem mniTemp = (MenuItem)Sender; 311: 312: switch (mniTemp.Text) { 313: case "Font...": 314: dlgFont.ShowColor = true; 315: dlgFont.Font = this.Font; 316: if (dlgFont.ShowDialog() == DialogResult.OK) { 317: fntCurrent = dlgFont.Font; 318: fntColor = dlgFont.Color; 319: this.Font = fntCurrent; 320: this.ForeColor = fntColor; 321: ((Page)arrPages[intCurrentPage - 1]).SetFont(fntCurrent,  fntColor); 322: } 323: break; 324: } 325: } 326: 327: private void DocumentClosing(Object Sender, CancelEventArgs e) {

716

Woche 3 – Rückblick

328: if (((Page)arrPages[intCurrentPage-1]).Modified) { 329: DialogResult dr = MessageBox.Show("Do you want to save the  changes?", this.Name, MessageBoxButtons.YesNoCancel); 330: 331: if (dr == DialogResult.Yes) { 332: FileClicked(mniSave, new EventArgs()); 333: } else if (dr == DialogResult.Cancel) { 334: e.Cancel = true; 335: } 336: } 337: } 338: } 339: }

Als Erstes ist festzustellen, dass alle Verweise auf das RichTextBox-Steuerelement getilgt sind. Statt dessen finden sich Referenzen auf unsere neue PageKlasse. Wir benötigen einen Mechanismus, um in diesem Dokument einzelne Seiten speichern zu können. Dies lässt sich mit einem ArrayList-Objekt durchführen, das in Zeile 11 deklariert wird (und der Namensraum des Objekts, System.Collections, wird in Zeile 7 importiert). Wenn wir ein neues Page-Objekt anlegen, fügen wir es diesem Array hinzu. Wenn wir auf die jeweils aktuelle Seite zugreifen müssen, benutzen wir das Array und die intCurrentPage-Variable (gleich mehr davon). Die nächste Änderung tritt in Zeile 14 auf, wo wir ein neues Panel-Steuerelement erzeugen. Dieses Paneel soll jede neue Seite, die erzeugt wird, aufnehmen. Seine Eigenschaften werden im Document-Konstruktor initialisiert. Zeile 18 erzeugt ein neues VScrollBar-Objekt, das für den Bildlauf benutzt wird. Das Panel-Steuerelement verfügt über eine AutoScroll-Eigenschaft, die uns Bildlaufleisten zur Verfügung stellt, doch sie hat auch Nachteile. Wenn man sich z.B. im Panel-Steuerelement von einer Seite zur nächsten bewegt und AutoScroll auf true gesetzt ist, ist der Bildlauf der Page-Objekte viel zu groß– dadurch wird es schwierig, den Überblick über die aktuelle Texteingabestelle zu behalten. Daher implementieren wir benutzerdefinierte Bildlaufleisten, die tun, was wir wollen. Die VScrollBar wird in den Zeilen 110 bis 112 initialisiert, und ihr Ereignishandler, ScrollView, befindet sich in Zeile 135. Diese Methode verschiebt einfach unser Panel-Steuerelement um einen skalierten Betrag. Der letzte Teil des Konstruktors ruft die AddPage-Methode (Zeile 132) auf. AddPage enthält einen Teil der wichtigsten neuen Funktionen. Sie erzeugt ein neues Page-Objekt (Zeile 148; beachten Sie bitte die Übergabe der nötigen

717

Woche 3 – Rückblick

Variablen). Zeile 153 legt die Platzierung einer neuen Seite im Panel-Steuerelement fest. Dazu zählt sie einfach die vorhandenen Page-Objekte und setzt dementsprechend die oberste Koordinate. In den Zeilen 154 bis 156 werden Ereignishandler den Ereignissen Filled, Enter und SelectionChanged zugewiesen. Wir fügen das eben angelegte Page-Objekt dem arrPages-Array und dem Panel-Steuerelement hinzu und verleihen ihm den Fokus. Die Größe des Panel-Steuerelements ist zu ändern, um die neue Seite aufzunehmen (Zeile 149), und auch die Maximum-Eigenschaft der VScrollBar wird angepasst (Zeile 151). Denken Sie an die Bildlaufleiste in Microsoft Word: Fügt man neue Seiten hinzu, schrumpft die Bildlaufleiste kontinuierlich (da der Maximalwert der Bildlaufleiste laufend wächst). Wird die Seitengrenze überschritten, so ist dies der Auslöser unseres benutzerdefinierten Ereignisses Filled. Dessen Ereignishandler PageFilled (Zeile 162) fügt entweder eine neue Seite hinzu (wenn es sich aktuell um die letzte Seite handelt) oder überträgt den Fokus auf die nächste Seite im Dokument. Beachten Sie die Verwendung der Variablen arrPages und intCurrentPage in Zeile 166. PageGotFocus, der Ereignishandler für das Enter-Ereignis, findet sich in Zeile 170. Er aktualisiert die Variable intCurrentPage. Der Ereignishandler für das SelectionChanged-Ereignis, UpdateStatus, wurde in Zeile 174 geschrieben. Damit können wir nun auf die Eigenschaft von Page in Zeile 177 zugreifen. Wieder ist auf den Einsatz der Variablen arrPages und intCurrentPage in Zeile

175 zu achten. Der übrige Code zwischen Zeile 180 und Zeile 339 hat sich nur geringfügig verändert. Alle Verweise auf das RichTextBox-Steuerelement wurden durch Referenzen auf ein PageObjekt ersetzt. Die Fälle »Print Preview« (Druckvorschau) und »Print« in den Zeilen 214 und 232 wurden geändert, um durch alle Page-Objekte im Dokument zu blättern, wobei sie den Text jeder Seite einsammeln, bevor sie ihn dem StringReader-Objekt in den Zeilen 221 und 242 übergeben. Die FormatClicked-Methode in Zeile 309 verweist nun auf die neue Page.SetFont-Methode, die wir in Listing P3.1 angelegt haben. Der Aufruf der FindDialog-Klasse in Zeile 303 hat sich leicht geändert, um eine neue Variable an den Konstruktor zu übergeben. (Dies wird in den nächsten Abschnitten erklärt.) Die FindDialog-Klasse ist so abzuändern, dass sie jedes Page-Objekt durchsucht statt nur ein einzelnes RichTextBox-Steuerelement wie zuvor. Um diese Funktionalität zu erreichen, sind zwei spezifische Änderungen notwendig: den Konstruktor von FindDialog so modifizieren, dass er auf die Document-Klasse statt auf sein RichTextBox-Steuerelement verweist, und die Methoden für Suchen, Ersetzen und Alle ersetzen so anpassen, dass sie jedes PageObjekt durchsuchen.

718

Woche 3 – Rückblick

Zunächst ändern Sie den Konstruktor wie folgt: private Document docParent; public FindDialog(Form frmDocument):base(){ docParent =(Document)frmDocument; ...

Nun nimmt die docParent-Variable eine Referenz auf unsere Haupt-Document-Klasse auf, welche wiederum ihrerseits einen Verweis auf alle Page-Objekte enthält. Listing R3.3 zeigt die Änderungen am ButtonClicked-Ereignishandler, der für die eigentliche Suchen- und Ersetzen-Funktionalität zuständig ist. Listing R3.3: Suchen und Ersetzen in Page-Objekten 1: 2: 3: 4: 5: 6: 7: 8:

private void ButtonClicked(Object Sender, EventArgs e) { Button btTemp = (Button)Sender; int intLocation; int intCount = 0;

switch (btTemp.Text) { case "Suchen": for (int i = docParent.intCurrentPage-1; i <  docParent.arrPages.Count; i++) { 9: 10: rtbDoc = (RichTextBox)docParent.arrPages[i]; 11: 12: if (i != docParent.intCurrentPage-1) { 13: rtbDoc.Focus(); 14: rtbDoc.SelectionStart = 0; 15: } 16: 17: intLocation = rtbDoc.Find(tbFind.Text, rtbDoc.SelectionStart +  rtbDoc.SelectionLength, RichTextBoxFinds.None); 18: if (intLocation != -1) { 19: rtbDoc.SelectionStart = intLocation; 20: rtbDoc.SelectionLength = tbFind.Text.Length; 21: rtbDoc.Focus(); 22: break; 23: } 24: } 25: break; 26: case "Ersetzen": 27: for (int i = docParent.intCurrentPage-1; i <  docParent.arrPages.Count; i++) { 28: 29: rtbDoc = (RichTextBox)docParent.arrPages[i];

719

Woche 3 – Rückblick

30: 31: if (i != docParent.intCurrentPage-1) { 32: rtbDoc.Focus(); 33: rtbDoc.SelectionStart = 0; 34: } 35: 36: intLocation = rtbDoc.Find(tbFind.Text, rtbDoc.SelectionStart +  rtbDoc.SelectionLength, RichTextBoxFinds.None); 37: if (intLocation != -1) { 38: rtbDoc.SelectionStart = intLocation; 39: rtbDoc.SelectionLength = tbFind.Text.Length; 40: rtbDoc.SelectedText = tbReplace.Text; 41: rtbDoc.SelectionStart = intLocation; 42: rtbDoc.SelectionLength = tbReplace.Text.Length; 43: } 44: } 45: break; 46: case "Replace All": 47: for (int i = 0; i < docParent.arrPages.Count; i++) { 48: 49: rtbDoc = (RichTextBox)docParent.arrPages[i]; 50: 51: if (i != docParent.intCurrentPage-1) { 52: rtbDoc.Focus(); 53: rtbDoc.SelectionStart = 0; 54: } 55: 56: intLocation = rtbDoc.Find(tbFind.Text); 57: while (intLocation != -1) { 58: rtbDoc.SelectionStart = intLocation; 59: rtbDoc.SelectionLength = tbFind.Text.Length; 60: rtbDoc.SelectedText = tbReplace.Text; 61: intCount += 1; 62: intLocation = rtbDoc.Find(tbFind.Text,  rtbDoc.SelectionStart + rtbDoc.SelectionLength, RichTextBoxFinds.None); 63: } 64: } 65: MessageBox.Show(intCount.ToString() + " Ersetzungen  ausgeführt","Suchen"); 66: break; 67: case "Abbrechen": 68: this.Close(); 69: break; 70: } 71: }

720

Woche 3 – Rückblick

Der meiste Code hat sich nicht geändert. Im Grunde gibt es nur einen neuen Codeabschnitt, welcher jedoch an drei Stellen verwendet wird. In Zeile 8 findet sich eine for-Schleife, die durch jedes Page-Objekt in der Auflistung Document.arrPages iteriert. Jedes Page-Objekt wird vorübergehend der rtbDoc-Variablen in Zeile 10 zugewiesen. Diese temporäre Variable wird genau wie beim letzten Mal zum Suchen nach Text verwendet. Man kann jedoch ein RichTextBox-Steuerelement nur dann durchsuchen, wenn es den Eingabefokus besitzt. Daher geben wir in den Zeilen 12 bis 15 jedem Page-Objekt den Fokus, falls es ihn noch nicht hat. Dieser Code wird für den »Ersetzen«-Fall ab Zeile 27 wiederholt und ebenso für den Fall »Alle ersetzen« ab Zeile 47. Kompilieren Sie die revidierten Document- und FindDialog-Klassen (wie auch den Rest der Anwendung) mit folgendem Befehl: csc /t:winexe /r:system.dll /r:system.drawing.dll /r:system.windows.forms.dll / r:Page.dll NetWord.cs FindDialog.cs Document.cs

Dieser Befehl ähnelt den anderen, die Sie in den vorherigen Wochenrückblicken kennen gelernt haben. Sie rufen damit den C#-Compiler auf, wobei Sie auf die notwendigen Assemblies und die Quellcodedateien verweisen (beachten Sie den Verweis auf die Datei Page.dll). Abbildung R3.1 zeigt das Ergebnis dieser Anwendung nach der Erstellung einiger Seiten.

Abbildung R3.1: Ihre NetWordAnwendung verhält sich nun sehr ähnlich wie Microsoft Word.

721

Woche 3 – Rückblick

Probieren Sie die Anwendung aus. Gerät man ans Ende einer Seite, wird automatisch eine neue kreiert und die Cursorposition verschoben. Man kann Dokumente drucken (und sie in der Vorschau begutachten) – sie werden im Druck genau wie am Bildschirm erscheinen (eine Bildschirmseite entspricht also einer Druckseite). Meinen Glückwunsch zur neuen Textverarbeitung!

Der weitere Weg Obwohl nun eine komplette Textverarbeitung vorliegt, lassen sich noch einige Dinge daran tun, um NetWord effizienter und funktionsreicher zu machen. Die Art und Weise, wie man sich von einer Seite zur nächsten bewegt, entspricht nicht genau der in Word. Konkret: Man kann nicht hoch oder hinunter zur nächsten Seite springen, ohne explizit auf diejenige Seite zu klicken (in Word kann man einfach den Auf- bzw. Abwärts-Pfeil anklicken). Das wäre ein leicht zu behebendes Problem. Die Seitenzählfunktion funktioniert vorerst für neue Dokumente, doch öffnet man eine vorhandene Datei, so wird diese Datei nicht automatisch paginiert. Die Seitenzählung funktioniert bei normalem Inhalt, doch wenn man umfangreiche Textmengen einkopiert, wird dieser Text nicht korrekt paginiert. Vielmehr wird er am unteren Rand der aktuellen Seite angehängt, welche sich einfach ausdehnt, um den neuen Text aufzunehmen. Zudem lassen sich momentan keine Seiten löschen. In seiner aktuellen Implementierung kann NetWord nur reinen Text sowie RTF-Dateien bearbeiten. Doch mit Ihrer Kenntnis der Serialisierung (Tag 11 über Windows-Ein-/Ausgabe) können Sie Ihre eigenen Dateitypen erstellen. Auf diese Weise lässt sich ein Dokument mit intakten Seiten (in Page-Objekten) speichern, was Ihnen den erwähnten Prozess der neuerlichen Seitenzählung erspart, wenn man sie öffnet. Unter Verwendung von ActiveX (Tag 14) könnten Sie sogar NetWord einsetzen, um Word-Dokumente zu öffnen. NetWord könnte weitaus mehr Funktionen hinsichtlich der Bearbeitung von Dokumenten anbieten, so etwa die Fähigkeit, Text zu zentrieren oder rechtsbündig auszurichten, Zeilenabstände zu ändern, Aufzählungszeichen und nummerierte Listen einzufügen usw. Dieser Funktionsumfang befindet sich bereits im RichTextBox-Steuerelement, so dass man NetWord ohne weiteres damit ausstatten kann, indem man lediglich die entsprechende Benutzeroberfläche bereitstellt. Während man einige dieser Funktionen leicht integrieren kann, verlangen andere jedoch ein wenig Einfallsreichtum und technisches Wissen. Mit den in den letzten 21 Tagen erworbenen Kenntnissen können Sie diese Aufgaben jedoch zweifellos bewältigen. Danke, dass Sie die letzten 21 Tage mit uns verbracht haben.

722

Anhang A

2 2

Anhang A

A.1

Antworten auf die Quizfragen und Übungen

Tag 1 Quiz 1. Wahr oder falsch? Windows Forms ist Win32-Programmierung. Wahr. In diesem Buch besprechen wir keine Win16-Programmierung. 2. Wahr oder falsch? Metadaten enthalten Angaben darüber, in welcher Umgebung eine Anwendung erstellt wurde. Wahr. 3. Auf welche Weise ermöglicht MSIL plattformübergreifende Funktionsweise? MSIL ist eine plattformunabhängige Sprache, so dass sie sich leicht auf jedes Betriebssystem übertragen lässt. Ein JIT-Compiler wird dann eingesetzt, um die MSIL in die jeweilige Maschinensprache zu übertragen: ein wirtschaftlicherer Vorgang als für jede zu unterstützende Plattform die Anwendung neu zu schreiben oder zu kompilieren. 4. Wie nennt man die Weiterentwicklung von ActiveX Data Objects? ADO.NET. 5. Wahr oder falsch? Man kann Windows Forms-Anwendungen mit jedem beliebigen Texteditor erstellen. Wahr, solange die Dateien im Nur-Text-Format gespeichert werden. 6. Was ist ein Windows Forms-Steuerelement? Ein Windows Forms-Steuerelement ist ein Objekt, das dem Benutzer eine Schnittstelle bereitstellt, die häufig zur Interaktion fähig ist. 7. Was bewirkt das ImportsStatement? Es versetzt eine Anwendung in die Lage, die Objekte oder eine Auflistung von Objekten in anderen Namensräumen zu nutzen.

Übung Was würde der folgende Code bewirken, sobald er kompiliert wäre und ausgeführt würde? 1: 2:

724

Imports System Imports System.Windows.Forms

Antworten auf die Quizfragen und Übungen

3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

Public Class MyApp : Inherits Form Public Shared Sub Main() Application.Run(New MyApp) End Sub Public Sub New() Me.Height = 100 Me.Width = 50 End Sub End Class

Dies erzeugt ein Windows Form und verändert seine Größe auf 100 x 50 Pixel.

Tag 2 Quiz 1. Wie heißt die Basisklasse, von der alle anderen Klassen erben? Die Object-Klasse. 2. Was bewirken die Schlüsselwörter shared und static? Diese Schlüsselwörter zeigen an, dass ein Mitglied ein fundamentaler Klassenbestandteil ist, der in Instanzen der Klassen vorhanden ist. Während beispielsweise verschiedene Autoinstanzen unterschiedliche Farben haben können, so haben sie doch wohl alle den gleichen Metalltyp gemeinsam. 3. Wahr oder falsch? Man kann vorhandene Namensräume um eigene Klassen erweitern. Wahr. 4. Wahr oder falsch? Die Main-Methode wird jedes Mal ausgeführt, wenn eine neue Instanz ihrer Klasse erzeugt wird. Falsch. Die Main-Methode wird nur einmal ausgeführt, nämlich dann, wenn die Anwendung gestartet wird. 5. Aus welchem Grund ist der folgende Code nicht ausreichend? Was muss außerdem verwendet werden? dim MyObject as Button

725

Anhang A

Er ist nicht ausreichend, weil man damit nur eine Variable des Typs Button deklariert; man weist der Variablen keinen Wert zu, was zu erledigen ist, bevor man sie einsetzen kann. Um die Variable zu benutzen, ist das New-Schlüsselwort einzusetzen: dim MyObject as New Button

oder dim MyObject as Button = New Button

6. Was stimmt mit dem folgenden Kompilierungsbefehl nicht? (Tipp: Er kann mehr als einen Fehler enthalten.) csc /type:windowsexe /r:system.dll /r:system.drawing.dll / r:system.drawing.text.dll dateiname.vb

Es gibt keinen Typ windowsexe. Verwenden Sie statt dessen winexe. Es gibt auch keine Assembly system.drawing.text.dll. Der Namensraum system.drawing.text ist in der Assembly system.drawing.dll enthalten. Außerdem verwendet diese Codezeile den C#-Compiler, um eine VB .NET-Datei zu kompilieren, wie an der Erweiterung der Quellcodedatei zu erkennen ist. 7. Was bedeuten die folgenden Eigenschaften? Button.CanSelect: Gibt an, ob die Schaltfläche vom Benutzer ausgewählt werden

kann. Button.Cursor: Gibt das Symbol an, das benutzt wird, wenn sich der Mauszeiger über der Schaltfläche befindet. Form.AllowDrop: Gibt an, ob dieses Formular Drag & Drop verarbeiten kann. Form.Visible: Ist das Formular für den Benutzer sichtbar?

8. Nennen Sie drei semantische Unterschiede zwischen C# und Visual Basic .NET. Variablentypen kommen in C# vor Variablennamen. Am Ende jeder Zeile muss ein Semikolon gesetzt werden. C# beachtet die Groß-/Kleinschreibung. 9. Wahr oder falsch? Jede Klasse muss einen Konstruktor haben. Das ist eine Fangfrage. Es trifft zu, dass jede Klasse einen Konstruktor haben muss, aber man muss keinen explizit selbst erzeugen. Die CLR erstellt nämlich automatisch einen, wenn er noch nicht vorhanden ist.

726

Antworten auf die Quizfragen und Übungen

Übung Erweitern Sie das heutige Taschenrechner-Beispiel. Fügen Sie weitere Schaltflächen hinzu, mit denen sich arithmetische Operationen ausführen lassen: Subtraktion, Multiplikation und Division. Versuchen Sie, die Anwendung sowohl in C# als auch in Visual Basic .NET zu erstellen. Verschönern Sie die Benutzeroberfläche Ihres Taschenrechners durch den großzügigen Gebrauch von Bezeichnungsfeldern (Label-Steuerelementen). Der Code in C#: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.Day2 { public class Calculator : Form { private Button btnAdd; private Button btnSubtract; private Button btnMultiply; private Button btnDivide; private TextBox tbNumber1; private TextBox tbNumber2; private Label lblAnswer; private Label lblNum1; private Label lblNum2; public static void Main() { Application.Run(new Calculator()); } public Calculator() { this.btnAdd = new Button(); this.btnSubtract = new Button(); this.btnMultiply = new Button(); this.btnDivide = new Button(); this.tbNumber1 = new TextBox(); this.tbNumber2 = new TextBox(); this.lblAnswer = new Label(); this.lblNum1 = new Label(); this.lblNum2 = new Label(); this.Width = 325; this.Height = 150; this.Text = "My Calculator";

727

Anhang A

37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82:

728

tbNumber1.Location = new Point(100,0); tbNumber2.Location = new Point(100,25); btnAdd.Location = new Point(0,75); btnAdd.Text = "+"; btnAdd.Click += new EventHandler(this.Add); btnSubtract.Location = new Point(75,75); btnSubtract.Text = "-"; btnSubtract.Click += new EventHandler(this.Subtract); btnMultiply.Location = new Point(150,75); btnMultiply.Text = "*"; btnMultiply.Click += new EventHandler(this.Multiply); btnDivide.Location = new Point(225,75); btnDivide.Text = "/"; btnDivide.Click += new EventHandler(this.Divide); lblNum1.Text = "Enter number 1: "; lblNum1.Location = new Point(0,0); lblNum2.Text = "Enter number 2: "; lblNum2.Location = new Point(0,25); lblAnswer.Location = new Point(0,55); this.Controls.Add(btnAdd); this.Controls.Add(btnSubtract); this.Controls.Add(btnMultiply); this.Controls.Add(btnDivide); this.Controls.Add(tbNumber1); this.Controls.Add(tbNumber2); this.Controls.Add(lblAnswer); this.Controls.Add(lblNum1); this.Controls.Add(lblNum2); } public void Add(object Sender, EventArgs e) { lblAnswer.Text = Convert.ToString(Convert.ToInt32(tbNumber1.Text) + Convert.ToInt32(tbNumber2.Text)); } public void Subtract(object Sender, EventArgs e) { lblAnswer.Text = Convert.ToString(Convert.ToInt32(tbNumber1.Text) Convert.ToInt32(tbNumber2.Text));

Antworten auf die Quizfragen und Übungen

83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95:

} public void Multiply(object Sender, EventArgs e) { lblAnswer.Text = Convert.ToString(Convert.ToInt32(tbNumber1.Text) * Convert.ToInt32(tbNumber2.Text)); } public void Divide(object Sender, EventArgs e) { lblAnswer.Text = Convert.ToString(Convert.ToInt32(tbNumber1.Text) / Convert.ToInt32(tbNumber2.Text)); } } }

Der Code in VB .NET: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:

Imports System Imports System.Windows.Forms Imports System.Drawing namespace TYWinforms.Day2 Public class Calculator : Inherits Form private WithEvents btnAdd as Button private WithEvents btnSubtract as Button private WithEvents btnMultiply as Button private WithEvents btnDivide as Button private tbNumber1 as TextBox private tbNumber2 as TextBox private lblAnswer as Label private lblNum1 as Label private lblNum2 aS Label Public shared Sub Main() Application.Run(new Calculator) End Sub Public Sub New() Me.btnAdd = new Button() Me.btnSubtract = new Button() Me.btnMultiply = new Button() Me.btnDivide = new Button() Me.tbNumber1 = new TextBox() Me.tbNumber2 = new TextBox() Me.lblAnswer = new Label() Me.lblNum1 = new Label() Me.lblNum2 = new Label()

729

Anhang A

32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77:

730

Me.Width = 325 Me.Height = 150 Me.Text = "My Calculator" tbNumber1.Location = new Point(100,0) tbNumber2.Location = new Point(100,25) btnAdd.Location = new Point(0,75) btnAdd.Text = "+" AddHandler btnAdd.Click, new EventHandler (AddressOf Add)

btnSubtract.Location = new Point(75,75) btnSubtract.Text = "-" AddHandler btnSubtract.Click, new EventHandler(AddressOf Subtract) btnMultiply.Location = new Point(150,75) btnMultiply.Text = "*" AddHandler btnMultiply.Click, new EventHandler (AddressOf Multiply) btnDivide.Location = new Point(225,75) btnDivide.Text = "/" AddHandler btnDivide.Click, new EventHandler (AddressOf Divide) lblNum1.Text = "Enter number 1: " lblNum1.Location = new Point(0,0) lblNum2.Text = "Enter number 2: " lblNum2.Location = new Point(0,25) lblAnswer.Location = new Point(0,55) Me.Controls.Add(btnAdd) Me.Controls.Add(btnSubtract) Me.Controls.Add(btnMultiply) Me.Controls.Add(btnDivide) Me.Controls.Add(tbNumber1) Me.Controls.Add(tbNumber2) Me.Controls.Add(lblAnswer) Me.Controls.Add(lblNum1) Me.Controls.Add(lblNum2) End Sub Public Sub Add(ByVal Sender as Object, ByVal e as EventArgs) lblAnswer.Text = CStr(Cint(tbNumber1.Text) + Cint(tbNumber2.Text))

Antworten auf die Quizfragen und Übungen

78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92:

End Sub Public Sub Subtract(ByVal Sender as Object, ByVal e as EventArgs) lblAnswer.Text = CStr(Cint(tbNumber1.Text) – Cint(tbNumber2.Text)) End Sub Public Sub Multiply(ByVal Sender as Object, ByVal e as EventArgs) lblAnswer.Text = CStr(Cint(tbNumber1.Text) * Cint(tbNumber2.Text)) End Sub Public Sub Divide(ByVal Sender as Object, ByVal e as EventArgs) lblAnswer.Text = CStr(Cint(tbNumber1.Text) / Cint(tbNumber2.Text)) End Sub End Class End Namespace

Tag 3 Quiz Die Fragen 1 bis 3 beziehen sich auf das folgende Codefragment: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:

using System; using System.Windows.Forms; using System.Drawing; public class MyForm : Form { private Label lblMessage = new Label(); public MyForm() { this.Text = „Hello World!"; } } public class StartForm { public static void Main() { Application.Run(new MyForm()); } }

1. Was würde das folgende Statement liefern, wenn man es unter Zeile 9 platzieren würde? Console.Write(this.ToString()); MyForm, Text: Hello World!

731

Anhang A

2. Was würde der folgende Code liefern, wenn man ihn unter Zeile 9 platzieren würde? Label lblTemp = new Label(); Console.Write(lblTemp.Equals(this.lblMessage).ToString()); False.

3. Was würde der folgende Code liefern, wenn man ihn unter Zeile 9 platzieren würde? Label lblTemp = new Label(); Console.Write(Object.ReferenceEquals(lblTemp, this.lblMessage).ToString()); False.

4. Wahr oder falsch? Das KeyPress-Ereignis erfordert einen Handler vom Typ KeyEventHandler. Falsch. Die KeyPress-Ereignisse übernehmen einen KeyPressEventHandler. 5. Nennen Sie die fünf Eigenschaften des Objekts MouseEventArgs. Button, Clicks, Delta, X und Y.

6. Schreiben Sie ein einzelnes Statement in VB .NET, das die Breite eines Formulars auf ein Drittel der Bildschirmhöhe festlegt. Me.Width = Screen.GetWorkingArea(Me).Height / 3

7. Welche Eigenschaft kontrolliert, welche Schaltfläche aktiviert wird, sobald der Benutzer die (ESC)-Taste gedrückt hat? CancelButton.

8. Welches ist der Standardwert von FormBorderStyle? Sizable.

9. Welche drei Ereignisse verwenden das Paradigma, dass mit einer einzelnen Aktion zwei Ereignisse verknüpft sind? Closed, InputLanguageChanged und Validated.

Übung 1. Erstellen Sie in C# eine Anwendung, die alle sechs Mausereignisse überwacht. Zeigen Sie eine Meldung in einem Textfeld an, wenn die einzelnen Ereignisse auftreten. 2. Erstellen Sie in VB .NET eine Anwendung, mit der die Benutzer die Eigenschaften Text, Height, Width und Opacity durch Werteingaben in eine TextBox und das Drücken einer EINGEBEN-Schaltfläche anpassen können.

732

Antworten auf die Quizfragen und Übungen

Zu 1: 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: using System.ComponentModel; 5: 6: namespace TYWinForms.Day3 { 7: 8: public class Exercise1 : Form { 9: private Label lblMessage = new Label(); 10: 11: public Exercise1() { 12: lblMessage.Width = this.Width; 13: lblMessage.Height = this.Height; 14: 15: this.Text = "Mouse events Example"; 16: this.Width = 800; 17: this.Height = 600; 18: this.MouseEnter += new EventHandler(this.MouseEntered); 19: this.MouseHover += new EventHandler(this.MouseHovered); 20: this.MouseLeave += new EventHandler(this.MouseLeft); 21: this.MouseMove += new MouseEventHandler(this.MouseMoved); 22: this.MouseDown += new MouseEventHandler(this.MouseClicked); 23: this.MouseUp += new MouseEventHandler(this.MouseReleased); 24: 25: this.Controls.Add(lblMessage); 26: } 27: 28: public void MouseEntered(Object Sender, EventArgs e) { 29: lblMessage.Text += "Mouse entered\r\n"; 30: } 31: 32: public void MouseHovered(Object Sender, EventArgs e) { 33: lblMessage.Text += "Mouse hovered\r\n"; 34: } 35: 36: public void MouseLeft(Object Sender, EventArgs e) { 37: lblMessage.Text += "Mouse left\r\n"; 38: } 39: 40: public void MouseMoved(Object Sender, MouseEventArgs e) { 41: lblMessage.Text += "Mouse moved: x=" + e.X + ", y=" + e.Y +  "\r\n"; 42: } 43:

733

Anhang A

44: 45: 46: 47: 48: 49:  "\r\n"; 50: 51: 52: 53: 54: 55: } 56: }

public void MouseClicked(Object Sender, MouseEventArgs e) { lblMessage.Text += "Button clicked: " + e.Button + "\r\n"; } public void MouseReleased(Object Sender, MouseEventArgs e) { lblMessage.Text += "Button released: x=" + e.X + ", y=" + e.Y + } public static void Main() { Application.Run(new Exercise1()); }

Zu 2. Der Code für die Anwendung sieht wie folgt aus: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:

734

Imports Imports Imports Imports

System System.Windows.Forms System.Drawing System.ComponentModel

Namespace TYWinForms.Day3 public class Exercise2 : Inherits Form private btSubmit as new Button private tbText as new TextBox private tbHeight as new TextBox private tbWidth as new TextBox private tbOpacity as new TextBox private lblText as new Label private lblHeight as new Label private lblWidth as new Label private lblOpacity as new Label public sub New() Me.Text = "Event Example" Me.Width = 800 Me.Height = 600 btSubmit.Location = new Point(50, 150) btSubmit.Text = "Submit" AddHandler btSubmit.Click, AddressOf Me.HandleIt lblText.Location = new Point(25,25) lblText.Text = "Text: " tbText.Location = new Point(75,25)

Antworten auf die Quizfragen und Übungen

31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75:

tbText.Text = Me.Text lblHeight.Location = new Point(25,50) lblHeight.Text = "Height: " tbHeight.Location = new Point(75,50) tbHeight.Text = Me.Height lblWidth.Location = new Point(25,75) lblWidth.Text = "Width: " tbWidth.Location = new Point(75,75) tbWidth.Text = Me.Width lblOpacity.Location = new Point(25,100) lblOpacity.Text = "Opacity: " tbOpacity.Location = new Point(75,100) tbOpacity.Text = Me.Opacity Me.Controls.Add(btSubmit) Me.Controls.Add(tbText) Me.Controls.Add(tbHeight) Me.Controls.Add(tbWidth) Me.Controls.Add(tbOpacity) Me.Controls.Add(lblText) Me.Controls.Add(lblHeight) Me.Controls.Add(lblWidth) Me.Controls.Add(lblOpacity) end sub public sub HandleIt(Sender as Object, e as EventArgs) Me.Text = tbText.Text Me.Height = CInt(tbHeight.Text) Me.Width = CInt(tbWidth.Text) Me.Opacity = CInt(tbOpacity.Text) end sub end class

public class StartForm public shared sub Main() Application.Run(new Exercise2) end sub end class End Namespace

735

Anhang A

Tag 4 Quiz 1. Wahr oder falsch? Alle Steuerelemente inklusive des Form-Objekts erben von der Klasse Control. Wahr. 2. Welches Objekt muss mit Symbolleisten verknüpft sein, damit Grafiken in den Symbolleistenschaltflächen angezeigt werden? Das ImageList-Objekt. 3. Wie heißen die drei optionalen Parameter für einen MenuItem-Konstruktor, und welches sind ihre Typen? Der erste Parameter ist eine Zeichenfolge, die die Aufschrift (oder Text-Eigenschaft) des MenuItem-Steuerelements darstellt. Der 2. Parameter legt den Ereignishandler fest, der auszuführen ist, sobald das MenuItem angeklickt wurde; es handelt sich um ein EventHandler-Objekt. Der 3. Parameter ist ein Wert aus der ShortCut-Aufzählung, der die Tastenkombination festlegt, die für den Zugriff auf das Menüelement benutzt werden kann. 4. Welches Zeichen wird verwendet, um eine Tastenkombination für einen Buchstaben in der Beschriftung eines Menüelements bereitzustellen? Das kaufmännische Und-Zeichen (&, auch Ampersand genannt). 5. Schreiben Sie eine Zeile C#-Code, die ein ToolBarButton-Steuerelement namens MyFirstButton anweist, die vierte Grafik in der zugeordneten ImageList zu verwenden. myFirstButton.ImageIndex = 3;

6. Wahr oder falsch? Das Ereignis, das für die Behandlung von Mausklicks auf Symbolleistenschaltflächen verwendet wird, heißt Click. Falsch. Das Ereignis heißt ButtonClick und gehört zum ToolBar-Objekt. 7. Welche sind die Standardwerte für die Eigenschaften Minimum, Maximum, SmallChange und LargeChange einer Bildlaufleiste? Jeweils 0, 100, 1 und 10.

Übung Erstellen Sie in VB .NET eine Anwendung, die ein personalisiertes Menü verwendet wie jene, die mit Microsoft Office 2000 und Windows 2000 eingeführt wurden. Diese Menüs zeigen

736

Antworten auf die Quizfragen und Übungen

nur die zuletzt benutzten Menüelemente, während sie die anderen verbergen und es dem Benutzer gestatten, auf einen Pfeil zu klicken, um weniger häufig ausgewählte Menüelemente anzuzeigen. Ihre Anwendung sollte ein Menü anbieten, das verborgene Menüelemente besitzt. Wird ein bestimmtes Menüelement angeklickt, sollen die verborgenen Elemente angezeigt werden. Kümmern Sie sich nicht um das Hinzufügen von Ereignishandlern für jedes Menüelement. Ihr Menü kann recht einfach sein: Es braucht sich nicht daran zu erinnern, welche Menüelemente der Benutzer am häufigsten auswählt, und es wird keine verborgenen Menüelemente anzeigen, nur weil der Mauspfeil über dem Menü schwebt. Sobald der Benutzer auf die Schaltfläche WEITERE... (More) klickt, muss er das Menü erneut öffnen, um die bislang verborgenen Elemente sehen zu können. (Sie werden sehen, wie man fortgeschrittenere Funktionen verwenden kann, wenn Sie an Tag 13 etwas über GDI+ erfahren. Der Code für die Anwendung sieht wie folgt aus: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:

Imports System Imports System.Windows.Forms Namespace TYWinForms.Day4 public class Exercise1 : Inherits Form private mnuFile as new MainMenu private miFile as new MenuItem("File") private miOpen as new MenuItem("Open...") private miClose as new MenuItem("Close") private miPrintPreview as new MenuItem("Print Preview") private miPrint as new MenuItem("Print...") private miProperties as new MenuItem("Properties") private miSendTo as new MenuItem("Send To...") private miExit as new MenuItem("Exit") private WithEvents miMore as new MenuItem("More items...") public event PopUp as EventHandler public sub New() Me.Text = "Exercise 1" Me.Menu = mnuFile CreateMenus() End Sub public sub CreateMenus() mnuFile.MenuItems.Add(miFile)

737

Anhang A

31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67:

738

miFile.MenuItems.Add(miOpen) miFile.MenuItems.Add(miClose) miFile.MenuItems.Add("-") miFile.MenuItems.Add(miPrintPreview) miFile.MenuItems.Add(miPrint) miFile.MenuItems.Add("-") miFile.MenuItems.Add(miSendTo) miFile.MenuItems.Add(miProperties) miFile.MenuItems.Add("-") miFile.MenuItems.Add(miExit) miFile.MenuItems.Add("-") miFile.MenuItems.Add(miMore) miPrintPreview.Visible = false miProperties.Visible = false miSendTo.Visible = false miFile.MenuItems(5).Visible = false AddHandler miMore.Click, new EventHandler(AddressOf Me.ShowMenu) end sub public sub ShowMenu(Sender as Object, e as EventArgs) miPrintPreview.Visible = true miProperties.Visible = true miSendTo.Visible = true miFile.MenuItems(5).Visible = true miFile.MenuItems(10).Visible = false miMore.Visible = false end sub public Shared Sub Main() Application.Run(new Exercise1) end sub end class End Namespace

Antworten auf die Quizfragen und Übungen

Tag 5 Quiz 1. Wie sieht die standardmäßige Signatur eines Ereignishandlers aus? public sub HandlerName(Object as Sender, e as EventArgs)

2. Erzeugen Sie in C# ein Ereignis namens ShowText, das den Delegaten KeyPressEventHandler verwendet. public event KeyPressEventHandler ShowText;

3. Erstellen Sie einen benutzerdefinierten Delegaten, der das Objekt KeyPressEventArgs verwendet. public delegate void CustomEventHandler(Object Sender, KeyPressEventArgs e);

4. Wahr oder falsch? Um Objekttypen miteinander zu vergleichen, verwendet man den Gleichheitszeichenoperator (=). Falsch. Das Gleichheitszeichen wird nur bei einfachen Datentypen eingesetzt. Für Objekte sollten Sie den is-Operator benutzen. 5. Welche der folgenden Aussagen ist inkorrekt? AddHandler btOne.Click, new EventHandler(AddressOf btOne_Click) public sub MyEventHandler(Sender as Object, e as EventArgs) Handles btTemp.Click Addhandler btOne.Click += new EventHandler(AddressOf btOne_Click)

Das dritte Statement ist falsch, denn es versucht, sowohl C#- als auch VB .NET-Syntax zu kombinieren. 6. Warum sollten die Eigenschaften eines benutzerdefinierten EventArgs-Objekts readonly sein? Der Ereignishandler, also die das benutzerdefinierte EventArgs-Objekt empfangende Methode, sollte nicht in der Lage sein, diese Eigenschaften zu verändern. Sie sollten vom Ereignis erzeugt werden, um dem Handler ergänzende Informationen zu liefern. 7. Ist das folgende Codestück korrekt? Was sollte man falls nötig ändern? public virtual void OnMyEvent(EventArgs e) { MyEvent(this, e); }

Erstens muss der Zugriffsmodifizierer von public auf protected geändert werden; nur die Klasse, in der sich diese Methode befindet, sollte ihn ausführen können und public

739

Anhang A

würde dies jeder Klasse erlauben. Zweitens sollte es eine Prüfung geben, um herauszufinden, ob ein Delegat zugewiesen wurde: if (MyEvent != null) { MyEvent(this, e); }

8. An welcher Stelle (im Quellcode) muss man benutzerdefinierte Ereignisse und Delegaten deklarieren? Ein Ereignis muss innerhalb derjenigen Klasse deklariert werden, die das Ereignis auslösen soll, jedoch außerhalb jeglicher Methodendeklaration. Ein Delegat kann sowohl inner- als auch außerhalb einer Klasse deklariert werden.

Übung Erstellen Sie in C# eine Taschenrechner-Anwendung wie den Windows-Taschenrechner. Er sollte Zifferntasten aufweisen, mit denen sich Zahlen eingeben lassen, sowie Funktionstasten, die Berechnungen vornehmen, wenn man darauf klickt. Verwenden Sie einen Ereignishandler für alle Zifferntasten und einen Handler für alle Funktionstasten. (Tipp: Nehmen Sie verborgene Steuerelemente, also solche, deren Visible-Eigenschaft auf False gesetzt ist – damit können Sie temporäre Variablen speichern.) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:

740

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinforms.Day5 { public class Calculator : Form { private TextBox tbNumber = new TextBox(); private TextBox tbHiddenNumber = new TextBox(); private TextBox tbHiddenOperator = new TextBox(); private TextBox tbHiddenAnswer = new TextBox(); private private private private private private private private private private

Button Button Button Button Button Button Button Button Button Button

btZero = new Button(); btOne = new Button(); btTwo = new Button(); btThree = new Button(); btFour = new Button(); btFive = new Button(); btSix = new Button(); btSeven = new Button(); btEight = new Button(); btNine = new Button();

Antworten auf die Quizfragen und Übungen

24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69:

private private private private private

Button Button Button Button Button

btAdd = new Button(); btSubtract = new Button(); btMultiply = new Button(); btDivide = new Button(); btEquals = new Button();

public Calculator() { tbNumber.Location = new Point(210,10); tbNumber.Width = 100; tbHiddenNumber.Visible = false; tbHiddenOperator.Visible = false; tbHiddenAnswer.Visible = false; btZero.Text = "0"; btZero.Location = new Point(10,125); btZero.Click += new EventHandler(this.NumberClick); btOne.Text = "1"; btOne.Location = new Point(10,100); btOne.Click += new EventHandler(this.NumberClick); btTwo.Text = "2"; btTwo.Location = new Point(85,100); btTwo.Click += new EventHandler(this.NumberClick); btThree.Text = "3"; btThree.Location = new Point(160,100); btThree.Click += new EventHandler(this.NumberClick); btFour.Text = "4"; btFour.Location = new Point(10,75); btFour.Click += new EventHandler(this.NumberClick); btFive.Text = "5"; btFive.Location = new Point(85,75); btFive.Click += new EventHandler(this.NumberClick); btSix.Text = "6"; btSix.Location = new Point(160,75); btSix.Click += new EventHandler(this.NumberClick); btSeven.Text = "7"; btSeven.Location = new Point(10,50); btSeven.Click += new EventHandler(this.NumberClick);

741

Anhang A

70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115:

742

btEight.Text = "8"; btEight.Location = new Point(85,50); btEight.Click += new EventHandler(this.NumberClick); btNine.Text = "9"; btNine.Location = new Point(160,50); btNine.Click += new EventHandler(this.NumberClick); btAdd.Text = "+"; btAdd.Location = new Point(160,125); btAdd.Click += new EventHandler(this.OperatorClick); btSubtract.Text = "-"; btSubtract.Location = new Point(235,100); btSubtract.Click += new EventHandler(this.OperatorClick); btMultiply.Text = "*"; btMultiply.Location = new Point(235,75); btMultiply.Click += new EventHandler(this.OperatorClick); btDivide.Text = "/"; btDivide.Location = new Point(235,50); btDivide.Click += new EventHandler(this.OperatorClick); btEquals.Text = "="; btEquals.Location = new Point(235,125); btEquals.Click += new EventHandler(this.OperatorClick);

this.Text = "Calculator"; this.Width = 325; this.Height = 200; this.Controls.Add(btZero); this.Controls.Add(btOne); this.Controls.Add(btTwo); this.Controls.Add(btThree); this.Controls.Add(btFour); this.Controls.Add(btFive); this.Controls.Add(btSix); this.Controls.Add(btSeven); this.Controls.Add(btEight); this.Controls.Add(btNine); this.Controls.Add(btAdd); this.Controls.Add(btSubtract); this.Controls.Add(btMultiply); this.Controls.Add(btDivide);

Antworten auf die Quizfragen und Übungen

116: this.Controls.Add(btEquals); 117: this.Controls.Add(tbNumber); 118: this.Controls.Add(tbHiddenOperator); 119: this.Controls.Add(tbHiddenNumber); 120: this.Controls.Add(tbHiddenAnswer); 121: } 122: 123: public void NumberClick(Object Sender, EventArgs e) { 124: if (tbNumber.Text != "" & tbNumber.Text != "0" &  tbHiddenAnswer.Text != "1") { 125: tbNumber.Text += ((Button)Sender).Text; 126: } else { 127: tbNumber.Text = ((Button)Sender).Text; 128: tbHiddenAnswer.Text = ""; 129: } 130: } 131: 132: public void OperatorClick(Object Sender, EventArgs e) { 133: int intAnswer = 0; 134: 135: if (tbNumber.Text != "" & ((Button)Sender).Text != "=") { 136: tbHiddenNumber.Text = tbNumber.Text; 137: tbHiddenOperator.Text = ((Button)Sender).Text; 138: tbNumber.Text = "0"; 139: } else if (tbNumber.Text != "" & ((Button)Sender).Text == 140: switch(tbHiddenOperator.Text) { 141: case "+": 142: intAnswer = Convert.ToInt32 (tbHiddenNumber.Text)  Convert.ToInt32(tbNumber.Text); 143: break; 144: case "-": 145: intAnswer = Convert.ToInt32 (tbHiddenNumber.Text)  Convert.ToInt32(tbNumber.Text); 46: break; 147: case "*": 148: intAnswer = Convert.ToInt32 (tbHiddenNumber.Text)  Convert.ToInt32(tbNumber.Text); 149: break; 150: case "/": 151: intAnswer = Convert.ToInt32 (tbHiddenNumber.Text)  Convert.ToInt32(tbNumber.Text); 152: break; 153: } 154: tbNumber.Text = intAnswer.ToString(); 155: tbHiddenAnswer.Text = "1"; 156: tbHiddenNumber.Text = "";

"=") {

+

-

*

/

743

Anhang A

157: 158: 159: 160: 161: 162: 163: 164: 165:

tbHiddenOperator.Text = ""; } } public static void Main() { Application.Run(new Calculator()); } } }

Tag 6 Quiz 1. Wodurch werden RadioButton-Auswahlen begrenzt? Der Benutzer kann nur ein RadioButton-Steuerelement pro Containersteuerelement auswählen. 2. Was bedeutet die folgende Datums-/Zeit-Formatzeichenfolge? "hh:MM:yy-dddd"

Zwei-Ziffern-Stundenanzeige, gefolgt von einem Doppelpunkt und dem Zwei-ZiffernMonat, wiederum gefolgt von einem Doppelpunkt und einer Zwei-Ziffern-Jahreszahl, gefolgt von einem Gedankenstrich und dem ausgeschriebenen Wochentag. 3. Welches Objekt ist mit dem TreeView-Steuerelement verknüpft? Das TreeNode-Objekt, welches zur Darstellung jedes Knotens in der Strukturansicht benutzt wird. 4. Setzen Sie für ein Button-Steuerelement namens btHappy Eigenschaften, die es dazu veranlassen, senkrecht zu expandieren, sobald sein Containerformular seine Größe ändert. btHappy.Anchor = AnchorStyles.Top Or AnchorStyles.Bottom

5. Nennen Sie zehn Mitglieder, die allen heute besprochenen Steuerelementen gemeinsam sind! Die Eigenschaften Anchor, Dock, BackColor, Location, Font und Text, die Methoden GetType und Refresh sowie die Ereignisse Click und Resize. 6. Wie heißen die fünf Mitglieder des Timer-Steuerelements? Enabled, Interval, Tick, Start und Stop.

744

Antworten auf die Quizfragen und Übungen

7. Wie fügt man dem Steuerelement TabControl Tabseiten hinzu? Beschreiben Sie dies in Worten und mit Hilfe einer Zeile Code. Man verwendet TabPage-Objekte, um weitere Tabseiten hinzuzufügen. Im folgenden Beispiel sei ein TabControl-Steuerelement namens MyTabControl gegeben: MyTabControl.TabPages.Add(New TabPage("Label text"));

8. Welche beiden Methoden sollte man ausführen, wenn man vorhat, einer ComboBox mit Hilfe der Add-Methode viele Elemente hinzuzufügen? Wenn Sie an Ihr Wissen über Windows Forms-Steuerelemente denken: Welche anderen heute vorgestellten Steuerelemente haben Ihrer Ansicht nach ebenfalls diese Methoden? Die BeginUpdate-Methode wird benutzt, um das Steuerelement am Aktualisieren seiner Anzeige zu hindern, bis die EndUpdate-Methode aufgerufen wird. Neben der ComboBox sind auch die ListBox- und TreeView-Steuerelemente in der Lage, diese Methoden einzusetzen. 9. Wahr oder falsch? Das PictureBox-Steuerelement kann nur Bitmap- (.BMP) und GIFGrafiken anzeigen. Falsch. Das PictureBox-Steuerelement kann diese ebenso anzeigen wie viele andere Formate, beispielsweise .JPG- und .ICO-Dateien.

Übung Erstellen Sie in C# eine Anwendung, mit deren Hilfe Benutzer ihre CD-Sammlung katalogisieren und betrachten können. Sie müssen neue Künstler/Alben/Stücke in den Katalog aufnehmen und mit einer hierarchischen Liste betrachten können. (Kümmern Sie sich nicht um das Speichern der Eingaben – das besprechen wir an Tag 9, wenn es um ADO.NET geht.) Denken Sie daran, dass Alben nur Künstlern hinzugefügt werden und Stücke nur Alben. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:

using System; using System.Windows.Forms; using System.Drawing; namespace TYWinForms.Day6 { public class CDCatalog : Form { private TreeView tvList = new TreeView(); private TextBox tbName = new TextBox(); private Button btArtist = new Button(); private Button btAlbum = new Button(); private Button btSong = new Button(); public CDCatalog() { tvList.Location = new Point(10,10);

745

Anhang A

16: tvList.Size = new Size(200,550); 17: tvList.LabelEdit = true; 18: 19: tbName.Location = new Point(250,10); 20: tbName.Width = 150; 21: 22: btArtist.Location = new Point(225,40); 23: btArtist.Text = "Add Artist"; 24: btArtist.Click += new EventHandler(this.AddArtist); 25: 26: btAlbum.Location = new Point(300,40); 27: btAlbum.Text = "Add Album"; 28: btAlbum.Click += new EventHandler(this.AddAlbum); 29: 30: btSong.Location = new Point(375,40); 31: btSong.Text = "Add Song"; 32: btSong.Click += new EventHandler(this.AddSong); 33: 34: this.Text = "CD Catalog"; 35: this.Size = new Size(800,600); 36: this.Controls.Add(tvList); 37: this.Controls.Add(tbName); 38: this.Controls.Add(btArtist); 39: this.Controls.Add(btAlbum); 40: this.Controls.Add(btSong); 41: } 42: 43: public void AddArtist(Object Sender, EventArgs e) { 44: if (tbName.Text == "") { 45: MessageBox.Show("You forgot to enter a name"); 46: return; 47: } 48: 49: tvList.Nodes.Add(new TreeNode(tbName.Text)); 50: tbName.Text = ""; 51: } 52: 53: public void AddAlbum(Object Sender, EventArgs e) { 54: if (tbName.Text == "") { 55: MessageBox.Show("You forgot to enter a name"); 56: return; 57: } 58: 59: if (tvList.SelectedNode != null &  tvList.Nodes.Contains(tvList.SelectedNode)) { 60: tvList.SelectedNode.Nodes.Add(new TreeNode (tbName.Text));

746

Antworten auf die Quizfragen und Übungen

61: tbName.Text = ""; 62: tvList.ExpandAll(); 63: } else { 64: MessageBox.Show("You must first select an artist"); 65: } 66: } 67: 68: public void AddSong(Object Sender, EventArgs e) { 69: if (tbName.Text == "") { 70: MessageBox.Show("You forgot to enter a name"); 71: return; 72: } 73: 74: if (tvList.SelectedNode != null &  tvList.Nodes.Contains(tvList.SelectedNode.Parent)) { 75: tvList.SelectedNode.Nodes.Add(new TreeNode (tbName.Text)); 76: tbName.Text = ""; 77: tvList.ExpandAll(); 78: } else { 79: MessageBox.Show("You must first select an album"); 80: } 81: } 82: 83: public static void Main() { 84: Application.Run(new CDCatalog()); 85: } 86: } 87: }

Tag 7 Quiz 1. Wahr oder falsch? Jedes Form-Objekt kann ein Dialogfeld sein. Wahr. 2. Wie macht man ein Dialogfeld modal bzw. nichtmodal? Durch Aufruf der Methode ShowDialog bzw. durch Aufruf der Methode Show. 3. Wahr oder falsch? Sie können ein MessageBox-Objekt direkt instantiieren. Falsch. Man kann nur auf die statische Show-Methode dieses Objekts zugreifen.

747

Anhang A

4. Wie heißen die sieben Parameter, die die MessageBox.Show-Methode übernehmen kann? (Führen Sie nur ihre Typen mit einer kurzen Beschreibung auf.) 왘

Iwin32Window: Das Fenster, vor dem das Meldungsfeld angezeigt wird



String: Der im Meldungsfeld anzuzeigende Text



String: Die Titelzeile des Fensters



MessageBoxButtons: Die Schaltflächen, die im Meldungsfeld erscheinen sollen



MessageBoxIcon: Das anzuzeigende Symbol



MessageBoxDefaultButton: Die vorausgewählte Schaltfläche



MessageBoxOptions: Diverse nicht damit verknüpfte Eigenschaften

5. Welche zwei Vorteile erzielen Sie, wenn Sie in Ihrer benutzerdefinierten Dialogklasse einen DialogResult-Wert zuweisen? Es liefert dem übergeordneten Formular einen DialogResult-Wert und schließt das Dialogfeld automatisch, wenn der Benutzer eine Schaltfläche auswählt (man muss die Hide-Methode nicht aufrufen). 6. Ist das folgende Codestück korrekt? Falls nicht, nennen Sie die Fehler. public property IsClicked as string Get return blnClicked end property

Wird nur das Get-Statement benutzt, muss die Eigenschaft als readonly markiert werden. Außerdem fehlt das End Get-Statement. 7. Welche zwei Mitglieder haben fast alle Standarddialogfeld-Steuerelemente gemeinsam? Nennen Sie die Ausnahme(n). ShowHelp und Reset. Diese Mitglieder sind Bestandteil aller Standarddialogfeld-Steuerelemente, mit Ausnahme des PrintPreviewDialog-Steuerelements.

8. Schreiben Sie eine Filterzeichenfolge für ein OpenFileDialog-Steuerelement, das Textdateien (*.txt), Bilddateien (*.gif) und Alle Dateien (*.*) anzeigt. "Text Dateien (*.txt)|*.txt|GIF-Bilder (*.gif)|*.gif|Alle Dateien (*.*)|*.*"

9. Nennen Sie die Haupteigenschaften (also die Eigenschaften, die Sie am meisten interessieren, wenn der Benutzer eine Auswahl trifft) für die Steuerelemente OpenFileDialog, SaveFileDialog, ColorDialog und FontDialog. Jeweils Filename, Filename, Color und Font.

748

Antworten auf die Quizfragen und Übungen

Übung Erstellen Sie in C# eine voll funktionsfähige Anwendung mit Menüs, welche alle Standarddialogfeld-Steuerelemente verwendet. Setzen Sie auch ein nichtmodales Dialogfeld ein, um kontextabhängige Hilfeinformationen anzuzeigen. Wenn sich ein Benutzer von einem Menüelement zum nächsten bewegt, sollte sich der Inhalt dieses Dialogfeldes ändern. (Tipp: Verwenden Sie das MenuItem.Select-Ereignis, um festzustellen, über welchem Menüelement sich die Maus gerade befindet.) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:

using using using using

System; System.Windows.Forms; System.Drawing; System.Drawing.Printing;

namespace TYWinforms.Day7 { public class Exercise1 : Form { PageSettings objPageSettings = new PageSettings(); MainMenu mnuMain = new MainMenu(); MenuItem mniFile = new MenuItem("File"); MenuItem mniOpen = new MenuItem("Open"); MenuItem mniSave = new MenuItem("Save"); MenuItem mniPageSetup = new MenuItem("Page Setup"); MenuItem mniPrintPreview = new MenuItem("Print Preview"); MenuItem mniPrint = new MenuItem("Print"); MenuItem mniEdit = new MenuItem("Edit"); MenuItem mniFont = new MenuItem("Font"); MenuItem mniColor = new MenuItem("Color"); HelpDialog dlgHelp = new HelpDialog(); public Exercise1() { mnuMain.MenuItems.Add(mniFile); mnuMain.MenuItems.Add(mniEdit); mniFile.MenuItems.Add(mniOpen); mniFile.MenuItems.Add(mniSave); mniFile.MenuItems.Add("-"); mniFile.MenuItems.Add(mniPageSetup); mniFile.MenuItems.Add(mniPrintPreview); mniFile.MenuItems.Add(mniPrint); mniEdit.MenuItems.Add(mniFont); mniEdit.MenuItems.Add(mniColor); mniOpen.Click += new EventHandler(this.FileMenuClick); mniSave.Click += new EventHandler(this.FileMenuClick);

749

Anhang A

38: mniPageSetup.Click += new EventHandler(this.FileMenuClick); 39: mniPrintPreview.Click += new EventHandler(this.FileMenuClick); 40: mniPrint.Click += new EventHandler(this.FileMenuClick); 41: mniFont.Click += new EventHandler(this.EditMenuClick); 42: mniColor.Click += new EventHandler(this.EditMenuClick); 43: 44: mniOpen.Select += new EventHandler(this.DisplayHelp); 45: mniSave.Select += new EventHandler(this.DisplayHelp); 46: mniPageSetup.Select += new EventHandler(this.DisplayHelp); 47: mniPrintPreview.Select += new EventHandler(this.DisplayHelp); 48: mniPrint.Select += new EventHandler(this.DisplayHelp); 49: mniFont.Select += new EventHandler(this.DisplayHelp); 50: mniColor.Select += new EventHandler(this.DisplayHelp); 51: 52: this.Menu = mnuMain; 53: this.Text = "Exercise 1"; 54: } 55: 56: public void FileMenuClick(Object Sender, EventArgs e) { 57: MenuItem mniTemp = (MenuItem)Sender; 58: 59: switch (mniTemp.Text) { 60: case "Open": 61: OpenFileDialog dlgOpen = new OpenFileDialog(); 62: if (dlgOpen.ShowDialog() == DialogResult.OK) { 63: //open file 64: } 65: break; 66: case "Save": 67: SaveFileDialog dlgSave = new SaveFileDialog(); 68: if (dlgSave.ShowDialog() == DialogResult.OK) { 69: //save file 70: } 71: break; 72: case "Page Setup": 73: PageSetupDialog dlgPageSetup = new PageSetupDialog(); 74: dlgPageSetup.PageSettings = objPageSettings; 75: dlgPageSetup.ShowDialog(); 76: break; 77: case "Print Preview": 78: PrintPreviewDialog dlgPrintPreview = new  PrintPreviewDialog(); 79: dlgPrintPreview.Document = new PrintDocument(); 80: dlgPrintPreview.ShowDialog(); 81: break; 82: case "Print":

750

Antworten auf die Quizfragen und Übungen

83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128:

PrintDialog dlgPrint = new PrintDialog(); dlgPrint.PrinterSettings = new PrinterSettings(); if (dlgPrint.ShowDialog() == DialogResult.OK) { //print } break; } } public void EditMenuClick(Object Sender, EventArgs e) { MenuItem mniTemp = (MenuItem)Sender; switch (mniTemp.Text) { case "Font": FontDialog dlgFont = new FontDialog(); if (dlgFont.ShowDialog() == DialogResult.OK) { this.Font = dlgFont.Font; } break; case "Color": ColorDialog dlgColor = new ColorDialog(); if (dlgColor.ShowDialog() == DialogResult.OK) { this.BackColor = dlgColor.Color; } break; } } public void DisplayHelp(Object Sender, EventArgs e) { MenuItem mniTemp = (MenuItem)Sender; if (!dlgHelp.Visible) { dlgHelp.Show(); } switch (mniTemp.Text) { case "Open": dlgHelp.HelpText = break; case "Save": dlgHelp.HelpText = break; case "Page Setup": dlgHelp.HelpText = break; case "Print Preview": dlgHelp.HelpText = break;

"Open a file";

"Save this file";

"Change page settings";

"Preview the document before printing";

751

Anhang A

129: case "Print": 130: dlgHelp.HelpText = "Print the current document"; 131: break; 132: case "Font": 133: dlgHelp.HelpText = "Change the current font face, color,  size, etc"; 134: break; 135: case "Color": 136: dlgHelp.HelpText = "Change the background color of the  application"; 137: break; 138: } 139: } 140: 141: public static void Main() { 142: Application.Run(new Exercise1()); 143: } 144: } 145: 146: public class HelpDialog : Form { 147: private Label lblMessage = new Label(); 148: 149: public String HelpText { 150: get { return lblMessage.Text; } 151: set { lblMessage.Text = value; } 152: } 153: 154: public HelpDialog() { 155: lblMessage.Size = new Size(100,100); 156: lblMessage.Text = "Help screen"; 157: 158: this.Controls.Add(lblMessage); 159: this.BackColor = Color.Yellow; 160: this.Size = new Size(100,100); 161: this.FormBorderStyle = FormBorderStyle.FixedToolWindow; 162: } 163: } 164: }

752

Antworten auf die Quizfragen und Übungen

Tag 8 Quiz 1. Schreiben Sie ein Statement, das einem Textfeld-Steuerelement namens tbOne eine neue Datenbindung hinzufügt. Binden Sie an die Text-Eigenschaft das Array arrOne. tbOne.DataBindings.Add("Text", arrOne, "");

2. Ist das folgende Statement für das Textfeld aus Frage 1 korrekt? tbOne.BindingContext.Position += 1;

Nein, Sie müssen den bestimmten Bindungskontext so festlegen, dass er sich laufend erhöht: tbOne.BindingContext[arrOne].Position += 1;

3. Wahr oder falsch? Man kann Daten an ein Form-Objekt binden. Wahr. Solche Daten anzubinden, ist häufig eine einfache Methode, eine Anwendung mit Daten zu verknüpfen. 4. Welches Ereignis wird oft verwendet, um die HitTest-Methode aufzurufen, und aus welchem Grund? Dafür wird oft das MouseDown-Ereignis benutzt. Es liefert spezifische Koordinaten, wo sich der Mausklick ereignete. 5. Wie würde die Anzeige der Zahl "458.34e+04" aussehen, wenn man die Formatzeichenfolge "d3" darauf anwendet? Das ist eine Fangfrage. Die Anzeige würde "4583400" lauten. Wenn man einen Präzisionswert in der Formatzeichenfolge mit weniger Ziffern als im Originalwert angibt, wird die Genauigkeit ignoriert. 6. Welcher Eigenschaft des DataGrid-Steuerelements fügen Sie DataGridTableStyleObjekte hinzu? Der TableStyles-Eigenschaft. 7. Welcher Eigenschaft des DataGrid-Steuerelements fügen Sie DataGridColumnStyleObjekte hinzu? Schon wieder eine Fangfrage. Man fügt keine DataGridColumnStyle-Objekte einem DataGrid-Steuerelement hinzu, sondern fügt sie der GridColumnStyles-Eigenschaft des DataGridTableStyle-Objekts hinzu.

753

Anhang A

8. Angenommen, Sie haben eine DataTable namens dtData mit zwei Spalten namens ColA und ColB. Erzeugen Sie eine neue, leere Zeile. DataRow myRow myRow = dtRow.NewRow(); dtData.Rows.Add(myRow);

9. Erzeugen Sie die ColA-Spalte der in Frage 8 erwähnten DataTable als einen Integer-Typ. DataColumn colA = new DataColumn(); colA.DataType = System.Type.GetType("System.Int32"); ColA.ColumnName = "ColA"; dtData.Columns.Add(colA);

Übung Erstellen Sie eine DataGrid und DataTable verwendende Anwendung, in der der Benutzer seine Kontoführungsinformationen – wie in Quicken oder Microsoft Money – eingeben kann. Gestatten Sie nicht, dass das DataGrid manuell editiert wird, sondern stellen Sie vielmehr dem Benutzer andere Steuerelemente für die Eingabe der Informationen bereit, die dann im DataGrid beim Bestätigen bzw. Absenden angezeigt werden. Um dieses Beispiel einfach zu halten, gestatten Sie momentan nur Abbuchungen. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:

754

using using using using

System; System.Windows.Forms; System.Drawing; System.Data;

namespace TYWinforms.Day8 { public class Exercise1 : Form { private DataGrid dgMoney = new DataGrid(); private DataTable dtMoney = new DataTable("Money"); private private private private private private private private private private

Panel pnlAdd = new Panel(); TextBox tbNum = new TextBox(); TextBox tbDate = new TextBox(); TextBox tbPayee = new TextBox(); TextBox tbAmount = new TextBox(); Label lblNum = new Label(); Label lblDate = new Label(); Label lblPayee = new Label(); Label lblAmount = new Label(); Button btSubmit = new Button();

public Exercise1() { //create columns

Antworten auf die Quizfragen und Übungen

24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69:

DataColumn colNum = new DataColumn(); colNum.DataType = System.Type.GetType ("System.Int32"); colNum.ColumnName = "Check Number"; dtMoney.Columns.Add(colNum); DataColumn colAmount = new DataColumn(); colAmount.DataType = System.Type.GetType ("System.Double"); colAmount.ColumnName = "Amount"; dtMoney.Columns.Add(colAmount); DataColumn colPayee = new DataColumn(); colPayee.DataType = System.Type.GetType ("System.String"); colPayee.ColumnName = "Payee"; dtMoney.Columns.Add(colPayee); DataColumn colDate = new DataColumn(); colDate.DataType = System.Type.GetType ("System.DateTime"); colDate.ColumnName = "Date"; dtMoney.Columns.Add(colDate); //create styles DataGridTableStyle dgtStyle = new DataGridTableStyle(); dgtStyle.MappingName = "Money"; DataGridTextBoxColumn dgcStyle = new DataGridTextBoxColumn(); dgcStyle.MappingName = "Check Number"; dgcStyle.ReadOnly = true; dgcStyle.HeaderText = "Check No."; dgcStyle.Width = 100; dgtStyle.GridColumnStyles.Add(dgcStyle); dgcStyle = new DataGridTextBoxColumn(); dgcStyle.MappingName = "Date"; dgcStyle.ReadOnly = true; dgcStyle.HeaderText = "Date"; dgcStyle.Width = 100; dgtStyle.GridColumnStyles.Add(dgcStyle); dgcStyle = new DataGridTextBoxColumn(); dgcStyle.MappingName = "Payee"; dgcStyle.ReadOnly = true; dgcStyle.HeaderText = "Payee"; dgcStyle.Width = 452; dgtStyle.GridColumnStyles.Add(dgcStyle); dgcStyle = new DataGridTextBoxColumn();

755

Anhang A

70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115:

756

dgcStyle.MappingName = "Amount"; dgcStyle.ReadOnly = true; dgcStyle.HeaderText = "Amount"; dgcStyle.Width = 100; dgcStyle.Format = "c"; dgtStyle.GridColumnStyles.Add(dgcStyle); //create add form lblNum.Text = "Check No."; lblNum.Location = new Point(10,250); tbNum.Location = new Point(75,245); lblDate.Text = "Date"; lblDate.Location = new Point(600,250); tbDate.Location = new Point(675,245); lblPayee.Text = "Pay To: "; lblPayee.Location = new Point(10,280); tbPayee.Location = new Point(75,275); tbPayee.Width = 400; lblAmount.Text = "Amount"; lblAmount.Location = new Point(600,280); tbAmount.Location = new Point(675,275); btSubmit.Text = "Enter"; btSubmit.Location = new Point(675,310); btSubmit.Click += new EventHandler (this.AddRecord); pnlAdd.Size = new Size(800,500); pnlAdd.Dock = DockStyle.Bottom; pnlAdd.BackColor = Color.Tan; pnlAdd.Controls.Add(tbNum); pnlAdd.Controls.Add(tbDate); pnlAdd.Controls.Add(tbPayee); pnlAdd.Controls.Add(tbAmount); pnlAdd.Controls.Add(lblNum); pnlAdd.Controls.Add(lblDate); pnlAdd.Controls.Add(lblPayee); pnlAdd.Controls.Add(lblAmount); pnlAdd.Controls.Add(btSubmit); dgMoney.Dock = DockStyle.Top; dgMoney.Size = new Size(800,300); dgMoney.CaptionText = "Checkbook Register"; dgMoney.BackgroundColor = Color.White;

Antworten auf die Quizfragen und Übungen

116: dgMoney.TableStyles.Add(dgtStyle); 117: dgMoney.DataSource = dtMoney; 118: 119: this.Size = new Size(800,600); 120: this.Text = ".NET Check Book Register"; 121: this.Controls.Add(dgMoney); 122: this.Controls.Add(pnlAdd); 123: } 124: 125: private void AddRecord(Object Sender, EventArgs e) { 126: if (tbPayee.Text == "" | tbAmount.Text == "") { 127: MessageBox.Show("You forgot to enter a payee or amount.","Check  Register"); 128: } else { 129: DataRow newRow = dtMoney.NewRow(); 130: 131: newRow[0] = Convert.ToInt32(tbNum.Text); 132: newRow[1] = Convert.ToDouble(tbAmount.Text); 133: newRow[2] = tbPayee.Text; 134: newRow[3] = Convert.ToDateTime(tbDate.Text); 135: 136: dtMoney.Rows.Add(newRow); 137: 138: tbNum.Text = ""; 139: tbDate.Text = ""; 140: tbAmount.Text = ""; 141: tbPayee.Text = ""; 142: } 143: } 144: 145: public static void Main() { 146: Application.Run(new Exercise1()); 147: } 148: } 149: }

757

Anhang A

Tag 9 Quiz 1. Schreiben Sie ein SELECT-Statement, das aus der tblUsers-Tabelle nur solche Datensätze holt, in denen der Wert für das Feld UserID zwischen 5 und 10 liegt. Sie können dieses Statement auf zwei verschiedene Arten formulieren: SELECT * FROM tblUsers WHERE UserID < 11 and UserID > 4

oder SELECT * FROM tblUsers WHERE UserID BETWEEN 5 AND 10

2. Welche Parameter fordert das SqlCommandBuilder-Objekt für seinen Konstruktor? Das SqlCommandBuilder-Objekt erfordert eine Instanz eines SqlDataAdapter-Objekts. 3. Ein SqlCommandBuilder erzeugt SQL-Befehle nur dann automatisch, wenn ein Primärschlüssel existiert. Warum? Die erzeugten Befehle müssen eine WHERE-Klausel enthalten, die die Zahl der geänderten Datensätze auf die richtigen eingrenzt. Die Verwendung eines Primärschlüssels ist ein Weg, um sicherzustellen, dass nur die richtigen Datensätze geändert werden. Das folgende Statement könnte mehr als einen Datensatz verändern: DELETE FROM tblUsers WHERE NACHNAME = 'Payne'

Statt dessen will man sicherstellen, dass nur ein einziger Datensatz betroffen ist – der Primärschlüssel, der ja für jeden Datensatz eindeutig sein muss, sorgt dafür. Der folgende Befehl wird garantiert nur einen einzigen Datensatz löschen: DELETE FROM tblUsers WHERE UserID = 1

4. Wahr oder falsch? Meist können Sie die Vorsilbe Sql durch OleDb ersetzen, um die Objekte im OLE DB-Provider nutzen zu können. Wahr. Beispielsweise wird SqlDataAdapter zu OleDbDataAdapter und aus SqlConnection wird OleDbConnection. 5. Ist es ausreichend, die DataSource-Eigenschaft eines DataGrid-Steuerelements auf ein gefülltes DataSet zu setzen? Nein. Man muss außerdem die DataMember-Eigenschaft auf die DataTable einstellen, die die anzuzeigenden Informationen enthält. 6. Schreiben Sie für ein gegebenes SqlCommand-Objekt namens objCmd ein Statement, das einen Parameter namens @Geburtsdatum mit einem Wert von 1/7/01 hinzufügt.

758

Antworten auf die Quizfragen und Übungen

Sie können das Statement auf zweierlei Weise formulieren: objCmd.Parameters.Add("@Geburtsdatum", SqlDbType.varChar, 10).Value = "1/7/ 01";

oder: objCmd.Parameters.Add("@Geburtsdatum", SqlDbType.DateTime, 10).Value = "1/7/ 01";

7. Nennen Sie die Methode, die veranlasst, dass Änderungen sofort in ein DataSet geschrieben werden. Dies ist die EndCurrentEdit-Methode des BindingContext-Objekts.

Übung Erstellen Sie eine Anwendung, mit der der Benutzer SQL-Statements in ein Textfeld eingeben kann. Sollte das Statement Ergebnisse liefern, legen Sie sie in einem DataSet ab und gestatten Sie dessen Bearbeitung. Kümmern Sie sich nicht darum, ob die Abfrage im richtigen Format eingegeben wird; Fehlerprüfungen werden erst an Tag 21 durchgenommen. 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: using System.Data; 5: using System.Data.SqlClient; 6: 7: namespace TYWinforms.Day9 { 8: public class Exercise1 : Form { 9: private DataGrid dgData = new DataGrid(); 10: private TextBox tbCommand = new TextBox(); 11: private Button btExecute = new Button(); 12: private DataSet objDS = new DataSet(); 13: private String strConn; 14: 15: public Exercise1() { 16: strConn = "Initial Catalog=TYWinforms;DataSource=localhost;User  ID=sa"; 17: 18: tbCommand.Location = new Point(150,75); 19: tbCommand.Multiline = true; 20: tbCommand.Height = 50; 21: tbCommand.Width = 300; 22: 23: btExecute.Location = new Point(475,75); 24: btExecute.Text = "Execute!"; 25: btExecute.Click += new EventHandler (this.Execute); 26:

759

Anhang A

27: dgData.Dock = DockStyle.Bottom; 28: dgData.Size = new Size(800,400); 29: dgData.Enabled = false; 30: dgData.CurrentCellChanged += new EventHandler(this.UpdateData); 31: 32: this.Size = new Size(800,600); 33: this.Text = "Exercise 1"; 34: this.Controls.Add(dgData); 35: this.Controls.Add(tbCommand); 36: this.Controls.Add(btExecute); 37: } 38: 39: private void Execute(Object Sender, EventArgs e) { 40: if (tbCommand.Text != "") { 41: SqlConnection objConn = new SqlConnection(strConn); 42: 43: SqlDataAdapter objCmd = new SqlDataAdapter(tbCommand.Text,  objConn); 44: objDS.Clear(); 45: int intRows = objCmd.Fill(objDS, "Query"); 46: 47: if (intRows > 0) { 48: dgData.ResetBindings(); 49: dgData.DataSource = objDS; 50: dgData.DataMember = "Query"; 51: dgData.Enabled = true; 52: } else { 53: dgData.Enabled = false; 54: } 55: } 56: } 57: 58: public void UpdateData(Object Sender, EventArgs e) { 59: SqlConnection objConn = new SqlConnection(strConn); 60: SqlDataAdapter objCmd = new SqlDataAdapter(tbCommand.Text,  objConn); 61: SqlCommandBuilder objBuilder = new SqlCommandBuilder(objCmd); 62: 63: objCmd.Update(objDS, "Query"); 64: } 65: 66: public static void Main() { 67: Application.Run(new Exercise1()); 68: } 69: } 70: }

760

Antworten auf die Quizfragen und Übungen

Tag 10 Quiz 1. Welche Eigenschaft müssen Sie im übergeordneten MDI-Formular einstellen, um es zu einer MDI-Anwendung zu machen? Man muss IsMdiContainer auf true setzen. 2. Welche Eigenschaft müssen Sie im untergeordneten MDI-Dokument einstellen, um es zum Bestandteil einer MDI-Anwendung zu machen? Man setzt die MdiParent-Eigenschaft des untergeordneten Dokuments auf das übergeordnete Formular. Das muss im übergeordneten Formular selbst erfolgen. 3. Über welche drei Werte verfügt die Aufzählung MdiLayout? MdiLayout.Cascade, MdiLayout.TileHorizontal und MdiLayout.TileVertical.

4. Wahr oder falsch? Sie müssen erst die Show-Methode aufrufen, bevor Sie ein untergeordnetes MDI-Formular anzeigen können, wenn es im Konstruktor seines übergeordneten Formulars erzeugt wird. Falsch. Man braucht die Show-Methode nur aufzurufen, wenn das untergeordnete Dokument außerhalb des Konstruktors angelegt wird. 5. Der folgende Code veranlasst das TextBox-Steuerelement tbText nicht dazu, das gesamte Formular auszufüllen: TextBox tbText = new TextBox(); tbText.Dock = DockStyle.Fill;

Warum nicht? Damit sich ein TextBox-Steuerelement in der Höhe mehr als über den Vorgabewert ausdehnt, muss man auch die Multiline-Eigenschaft auf true setzen: tbText.Multiline = true;

6. Wie findet man heraus, ob ein untergeordnetes MDI-Fenster aktiv ist? Man bewertet die ActiveMdiChild-Eigenschaft und findet heraus, ob sie mit null (Nothing in VB .NET) bewertet wird. 7. Sie haben drei Menüelemente in einem untergeordneten Dokument erzeugt, aber sie werden im übergeordneten Dokument nicht richtig angezeigt. Nennen Sie drei Dinge, die man zur Behebung dieses Problems prüfen sollte.

761

Anhang A

Setzt man einen anderen MenuMerge-Wert als Remove ein, sollte man dafür sorgen, dass die Menüelemente sowohl in der übergeordneten als auch in den untergeordneten Klassen zugewiesen sind. Man muss dafür sorgen, dass die MergeOrder-Werte übereinstimmen (die Menüs, die sich verbinden/miteinander verschmelzen/ersetzen sollen, haben gleiche MergeOrderWerte). Prüfen Sie besser doppelt, ob Sie die richtigen MenuMerge-Werte benutzen. Sie könnten aus Versehen Remove in der untergeordneten Klasse benutzen, im Glauben, dass dies das übergeordnete Menüelement entfernt, doch in Wahrheit entfernt dies das untergeordnete Menüelement. 8. Wahr oder falsch? Der MergeOrder-Wert muss sich schrittweise erhöhen lassen; Sie können keine Werte in der Reihenfolge überspringen. Falsch. Der tatsächliche Wert von MergeOrder ist gleichgültig, wenn man Menüelemente anordnet; nur auf relative Werte kommt es an.

Übung Erstellen Sie eine MDI-Version der gestrigen Anwendung für die Ausführung von SQL-Statements. Erzeugen Sie ein benutzerdefiniertes Dialogfeld, das ein Textfeld anzeigt, in dem der Benutzer seine Abfrage eingeben kann. Exercise1.cs: 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: 5: namespace TYWinforms.Day10 { 6: public class Exercise1 : Form { 7: private MainMenu mnuMain = new MainMenu(); 8: private int intCounter = 0; 9: 10: public Exercise1() { 11: MenuItem miQuery = mnuMain.MenuItems.Add("Query"); 12: MenuItem miNew = miQuery.MenuItems.Add("New"); 13: miNew.Click += new EventHandler(this.HandleMenu); 14: 15: MenuItem miWindow = mnuMain.MenuItems.Add("&Window"); 16: miWindow.MenuItems.Add("Cascade", new  EventHandler(this.ArrangeWindows)); 17: miWindow.MenuItems.Add("Tile Horizontal", new  EventHandler(this.ArrangeWindows));

762

Antworten auf die Quizfragen und Übungen

18: miWindow.MenuItems.Add("Tile Vertical", new  EventHandler(this.ArrangeWindows)); 19: miWindow.MdiList = true; 20: 21: QueryDialog dlgQuery = new QueryDialog(); 22: this.AddOwnedForm(dlgQuery); 23: dlgQuery.TopMost = true; 24: dlgQuery.Show(); 25: 26: this.Size = new Size(800,600); 27: this.Text = "Exercise 1"; 28: this.Menu = mnuMain; 29: this.IsMdiContainer = true; 30: } 31: 32: private void ArrangeWindows(Object Sender, EventArgs e) { 33: MenuItem miTemp = (MenuItem)Sender; 34: 35: switch (miTemp.Text) { 36: case "Cascade": 37: this.LayoutMdi(MdiLayout.Cascade); 38: break; 39: case "Tile Horizontal": 40: this.LayoutMdi(MdiLayout.TileHorizontal); 41: break; 42: case "Tile Vertical": 43: this.LayoutMdi(MdiLayout.TileVertical); 44: break; 45: } 46: } 47: 48: private void HandleMenu(Object Sender, EventArgs e) { 49: intCounter++; 50: 51: DataDocument doc = new DataDocument ("Query Results " +  intCounter.ToString()); 52: doc.MdiParent = this; 53: doc.Show(); 54: } 55: 56: public static void Main() { 57: Application.Run(new Exercise1()); 58: } 59: } 60: }

763

Anhang A

QueryDialog.cs: 1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: 5: namespace TYWinforms.Day10 { 6: public class QueryDialog : Form { 7: private TextBox tbCommand = new TextBox(); 8: private Button btExecute = new Button(); 9: 10: public QueryDialog() { 11: tbCommand.Location = new Point(10,10); 12: tbCommand.Multiline = true; 13: tbCommand.Height = 50; 14: tbCommand.Width = 250; 15: 16: btExecute.Location = new Point(10,75); 17: btExecute.Text = "Execute!"; 18: btExecute.Click += new EventHandler(this.Execute); 19: 20: this.Size = new Size(300,150); 21: this.Text = "Query Executor"; 22: this.Controls.Add(tbCommand); 23: this.Controls.Add(btExecute); 24: } 25: 26: private void Execute(Object Sender, EventArgs e) { 27: if (tbCommand.Text != "") { 28: if (this.Owner.ActiveMdiChild != null) { 29: 30:  ((DataDocument)this.Owner.ActiveMdiChild).Execute(tbCommand.Text); 31: } 32: } 33: } 34: } 35: }

DataDocument.cs: 1: 2: 3: 4: 5: 6: 7:

764

using using using using using

System; System.Windows.Forms; System.Drawing; System.Data; System.Data.SqlClient;

namespace TYWinforms.Day10 {

Antworten auf die Quizfragen und Übungen

8: public class DataDocument : Form { 9: private DataGrid dgData = new DataGrid(); 10: private DataSet objDS = new DataSet(); 11: private String strConn; 12: private String strQuery; 13: 14: public DataDocument(string strName) { 15: strConn = "Initial Catalog=TYWinforms;DataSource=localhost;  User ID=sa"; 16: 17: dgData.Dock = DockStyle.Fill; 18: dgData.Enabled = false; 19: dgData.CurrentCellChanged += new EventHandler(this.UpdateData); 20: 21: this.WindowState = FormWindowState.Maximized; 22: this.Text = strName; 23: this.Controls.Add(dgData); 24: } 25: 26: public void Execute(string strQuery) { 27: this.strQuery = strQuery; 28: 29: SqlConnection objConn = new SqlConnection(strConn); 30: 31: SqlDataAdapter objCmd = new SqlDataAdapter(strQuery, objConn); 32: objDS.Clear(); 33: int intRows = objCmd.Fill(objDS, "Query"); 34: 35: if (intRows > 0) { 36: dgData.ResetBindings(); 37: dgData.DataSource = objDS; 38: dgData.DataMember = "Query"; 39: dgData.Enabled = true; 40: } else { 41: dgData.Enabled = false; 42: } 43: } 44: 45: public void UpdateData(Object Sender, EventArgs e) { 46: SqlConnection objConn = new SqlConnection(strConn); 47: SqlDataAdapter objCmd = new SqlDataAdapter(strQuery, objConn); 48: SqlCommandBuilder objBuilder = new SqlCommandBuilder(objCmd); 49: 50: objCmd.Update(objDS, "Query"); 51: } 52: } 53: }

765

Anhang A

Tag 11 Quiz 1. Worin besteht der Unterschied zwischen den Read- und Peek-Methoden? Sowohl die Read- als auch die Peek-Methoden liefern das nächste Zeichen in einem Strom, doch Peek bewegt dabei nicht den Zeiger im Strom. Daher liefern nachfolgende Peek-Aufrufe stets das gleiche Zeichen, wohingegen Read-Aufrufe den Zeiger bewegen und nachfolgende Zeichen liefern. 2. Was ist eine rekursive Funktion und wann erweist sie sich als hilfreich? Eine rekursive Funktion ruft sich selbst immer wieder neu auf. Solch eine Funktion setzt man ein, wenn man nicht im Voraus weiß, wie oft etwas ausgeführt werden muss, beispielsweise in einer Verzeichnisstruktur. 3. Worin besteht der Unterschied zwischen synchroner und asynchroner Arbeitsweise? Synchrone Arbeitsweise erfordert, dass alles andere in bestimmter Reihenfolge und synchron ausgeführt wird, wohingegen bei asynchroner Arbeitsweise die Ausführung aufgeteilt werden kann, um so gleichzeitig ausgeführt zu werden. Das erspart Zeit. 4. Zu welchem Namensraum gehört die Druckfunktionalität? System.Drawing.Printing.

5. Welche sind die vier Ereignisse des FileSystemWatcher-Objekts? Created, Changed, Renamed und Deleted.

6. Welche sind die Werte der FileMode-Aufzählung ? Append, Create, CreateNew, Open, OpenOrCreate und Truncate.

Übung Modifizieren Sie Listing 11.1 so, dass die TreeView-Knoten jedes Mal dynamisch erstellt werden, wenn ein Benutzer einen Knoten expandiert, anstatt alle während der Initialisierungsphase der Anwendung erstellen zu lassen. Fügen Sie außerdem eine benutzerdefinierte Druckfunktion hinzu, um die sichtbaren Inhalte des TreeView-Steuerelements drucken zu können. (Tipp: Sie müssen die Inhalte der Strukturansicht erst in einer passenden Variablen sammeln, um sie drucken zu können.) 1: 2: 3:

766

using System; using System.Windows.Forms; using System.Drawing;

Antworten auf die Quizfragen und Übungen

4: using System.IO; 5: using System.Drawing.Printing; 6: 7: namespace TYWinforms.Day11 { 8: public class Exercise1 : Form { 9: private TreeView tvFiles = new TreeView(); 10: private Label lblInfo = new Label(); 11: private MainMenu mnuMain = new MainMenu(); 12: private String strList = ""; 13: private StringReader objPrintReader; 14: 15: public Exercise1() { 16: tvFiles.Dock = DockStyle.Left; 17: tvFiles.Width = 200; 18: tvFiles.AfterSelect += new TreeViewEventHandler(this.DisplayInfo); 19: tvFiles.BeforeExpand += new  TreeViewCancelEventHandler(this.ExpandThread); 20: 21: lblInfo.Size = new Size(150,150); 22: lblInfo.Location = new Point(210,10); 23: lblInfo.Text = "Select a file or\ndirectory"; 24: 25: PopulateList("c:\\", tvFiles.Nodes.Add("c:\\")); 26: 27: MenuItem miFile = new MenuItem("File"); 28: MenuItem miPrint = new MenuItem("Print"); 29: miPrint.Click += new EventHandler (this.PrintClicked); 30: 31: mnuMain.MenuItems.Add(miFile); 32: miFile.MenuItems.Add(miPrint); 33: 34: this.Menu = mnuMain; 35: this.Text = "Exercise 1"; 36: this.Size = new Size(800,600); 37: this.Controls.Add(tvFiles); 38: this.Controls.Add(lblInfo); 39: } 40: 41: private void PopulateList(String strPath, TreeNode currNode) { 42: DirectoryInfo dir = new DirectoryInfo(strPath); 43: TreeNode nodeSubDir; 44: 45: foreach (DirectoryInfo d in dir.GetDirectories()) { 46: nodeSubDir = currNode.Nodes.Add(d.Name); 47: nodeSubDir.Nodes.Add(""); 48: }

767

Anhang A

49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94:

768

foreach (FileInfo f in dir.GetFiles("*.*")) { currNode.Nodes.Add(f.Name); } } private void ExpandThread(Object Sender, TreeViewCancelEventArgs e) { if (e.Node.Nodes[0].Text == "") { TreeNode tempNode = e.Node; String strFullName = e.Node.Text; while (tempNode.Parent != null) { strFullName = tempNode.Parent.Text + "\\" + strFullName; tempNode = tempNode.Parent; } e.Node.Nodes[0].Remove(); PopulateList(strFullName, e.Node); } } private void DisplayInfo(Object Sender, TreeViewEventArgs e) { TreeNode tempNode = e.Node; String strFullName = tempNode.Text; while (tempNode.Parent != null) { strFullName = tempNode.Parent.Text + "\\" + strFullName; tempNode = tempNode.Parent; } if (File.Exists(strFullName)) { FileInfo obj = new FileInfo(strFullName); lblInfo.Text = "Name: " + obj.Name; lblInfo.Text += "\nSize: " + obj.Length; lblInfo.Text += "\nAccessed: " + obj.LastAccessTime.ToString(); } else { DirectoryInfo obj = new DirectoryInfo (strFullName); lblInfo.Text = "Name: " + obj.Name; lblInfo.Text += "\nAttributes: " + obj.Attributes.ToString(); lblInfo.Text += "\nAccessed: " + obj.LastAccessTime.ToString(); } } private void PrintClicked(Object Sender, EventArgs e) { //Assemble string of nodes strList = "";

Antworten auf die Quizfragen und Übungen

95: TreeNode nodeTemp = tvFiles.TopNode; 96: 97: for (int i = 0; i < tvFiles.GetNodeCount(true); i++) { 98: /*pad the string with spaces for easier 99: reading */ 100: strList += "".PadRight(GetLevel(nodeTemp)*2) + nodeTemp.Text +  "\n"; 101: 102: nodeTemp = nodeTemp.NextVisibleNode; 103: if (nodeTemp == null) break; 104: 105: nodeTemp.EnsureVisible(); 106: } 107: 108: PrintDocument pd = new PrintDocument(); 109: pd.PrintPage += new PrintPageEventHandler (this.pd_PrintPage); 110: objPrintReader = new StringReader(strList); 111: pd.Print(); 112: } 113: 114: /* Returns an integer indicating how many levels deep 115: the specified node is in the treeview */ 116: private int GetLevel(TreeNode objNode) { 117: int intLevel = 0; 118: TreeNode nodeTemp = objNode; 119: 120: while (nodeTemp.Parent != null) { 121: intLevel++; 122: nodeTemp = nodeTemp.Parent; 123: } 124: 125: return intLevel; 126: } 127: 128: private void pd_PrintPage(Object Sender, PrintPageEventArgs e) { 129: Font fntPrint = this.Font; 130: int count = 0; 131: float yPos = 0; 132: float lpp = e.MarginBounds.Height /  fntPrint.GetHeight(e.Graphics); 133: float fltTopMargin = e.MarginBounds.Top; 134: float fltLeftMargin = e.MarginBounds.Left; 135: String strLine = null; 136: 137: while (count < lpp && ((strLine = objPrintReader. ReadLine()) !=  null)) {

769

Anhang A

138: yPos = fltTopMargin + (count * fntPrint.GetHeight(e.Graphics)); 139: 140: e.Graphics.DrawString(strLine, fntPrint, Brushes.Black,  fltLeftMargin, yPos, new StringFormat()); 141: 142: count++; 143: } 144: 145: if (strLine != null) { 146: e.HasMorePages = true; 147: } else { 148: e.HasMorePages = false; 149: } 150: } 151: 152: public static void Main() { 153: Application.Run(new Exercise1()); 154: } 155: } 156: }

Tag 12 Quiz 1. Wahr oder falsch? Um dem Server POST-Informationen zu schicken, verwendet man das WebResponse-Objekt. Falsch. Man benutzt eine WebRequest-Methode und ruft GetRequestStream auf, um POST-Daten zu senden.

2. Nennen Sie die vier Bestandteile eines URI. Das Protokoll, der Servername, der Verzeichnispfad und eine optionale Abfragezeichenfolge. 3. Welchen Typs muss der Inhalt sein, um Formularinformationen posten zu können? application/x-www-form-urlencoded

4. Was ist das HttpWebRequest-Objekt? Das HttpWebRequest-Objekt, ein Helfer des WebRequest-Objekts, gestattet feiner abgestimmte Kontrolle über eine HTTP-Anforderung. Man kann sein WebRequest-Objekt in ein HttpWebRequest umwandeln, um benutzerdefinierte HTTP-Header und andere HTTP-Einstellungen festzulegen.

770

Antworten auf die Quizfragen und Übungen

5. Welche sind die drei Typen der Windows-Authentifizierung? Basis-, Digest- und integrierte Windows-Authentifizierung.

Übung Erstellen Sie eine Anwendung, die eine Webseite (Ihrer Wahl) untersucht, deren Bilder parst (in HTML beginnen Grafiken mit dem Tag