Programmer's Choice
Michael Kofler
Visual Basic .NET Grundlagen, Programmiertechniken, Windows-Programmierung
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich.
Die Informationen in diesem Buch 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 Autor können für fehlerhafte Angaben und deren Folgen weder juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag, Herausgeber und Autor dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
10 9 8 7 6 5 4 3 2 1 04 03 02 ISBN 3-8273-1982-X © 2002 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH Martin-Kollar-Straße 10–12, D-81829 München/Germany – Alle Rechte vorbehalten Einbandgestaltung: Christine Rechl, München Titelbild: Abutilon. © Karl Blossfeldt Archiv – Ann und Jürgen Wilde, Zülpich / VG Bild-Kunst Bonn, 2002 Lektorat: Irmgard Wagner, Planegg,
[email protected] Korrektorat: Andrea Stumpf, München Satz: Michael Kofler, Graz, www.kofler.cc Druck und Verarbeitung: Bercker, Kevelaer Printed in Germany
Inhaltsübersicht Vorwort Formales
19 23
I
Einführung
1 2 3
Hello World Das .NET-Universum Von VB6 zu VB.NET
II
Grundlagen
4 5 6 7
Variablen- und Objektverwaltung Prozedurale Programmierung Klassenbibliotheken und Objekte anwenden Objektorientierte Programmierung
III
Programmiertechniken
8 9 10 11 12
Zahlen, Zeichenketten, Datum und Uhrzeit Aufzählungen (Arrays, Collections) Dateien und Verzeichnisse Fehlersuche und Fehlerabsicherung Spezialthemen
IV
Windows-Programmierung
557
13 14 15 16 17 18
Windows.Forms – Einführung Steuerelemente Gestaltung von Benutzeroberflächen Grafikprogrammierung (GDI+) Drucken Weitergabe von Windows-Programmen (Setup.exe)
559 575 709 831 959 1003
Anhang A B C D E
Abkürzungsverzeichnis Glossar Dateikennungen Inhalt der CD-ROM Quellenverzeichnis Stichwortverzeichnis
27 29 49 77
113 115 161 185 211
287 289 355 383 471 505
1023 1025 1029 1035 1037 1039 1041
Inhaltsverzeichnis Vorwort
19
Formales
23
I
EINFÜHRUNG
27
1
Hello World
29
1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7
Hello World (Konsolenversion) Hello World (Windows-Version) Bedienung der Entwicklungsumgebung Layout der Entwicklungsumgebung Menüs und Symbolleisten Tastenkürzel Online-Hilfe Codeeingabe Befehlsfenster Defaulteinstellungen für neue Projekte
30 37 41 41 42 43 43 44 45 47
2
Das .NET-Universum
49
2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.7.1 2.7.2 2.7.3
Wozu .NET? Das .NET-Framework Architektur Sicherheitsmechanismen .NET und das Internet Programmiersprachen (C# versus VB.NET) Entwicklungsumgebungen Microsoft-Entwicklungsumgebungen SharpDevelop Ohne Entwicklungsumgebung arbeiten
50 53 55 61 65 67 69 70 71 74
3
Von VB6 zu VB.NET
77
3.1 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5
Fundamentale Neuerungen in VB.NET Unterschiede zwischen VB6 und VB.NET Variablenverwaltung Sprachsyntax Bearbeitung von Zahlen und Zeichenketten Windows-Anwendungen und Steuerelemente Grafikprogrammierung und Drucken
78 79 80 84 86 87 100
8
Inhaltsverzeichnis
3.2.6 3.2.7 3.2.8 3.2.9 3.3
Umgang mit Verzeichnissen und Dateien Fehlerabsicherung und Debugging Datenbank- und Internetprogrammierung Sonstiges Der Migrationsassistent
II
GRUNDLAGEN
4
Variablen- und Objektverwaltung
4.1 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.3 4.4 4.4.1 4.4.2 4.4.3 4.4.4 4.5 4.5.1 4.5.2 4.5.3 4.6 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5
Umgang mit Variablen Deklaration von Variablen Objektvariablen Variablenzuweisungen Option Explicit und Option Strict Syntaxzusammenfassung Variablentypen Ganze Zahlen (Byte, Short, Integer, Long) Fließ- und Festkommazahlen (Single, Double, Decimal) Datum und Uhrzeit (Date) Zeichenketten (String) Objekte Weitere .NET-Datentypen Konstanten Enum-Aufzählungen Syntax und Anwendung Enum-Kombinationen (Flags) Interna (System.Enum-Klasse) Syntaxzusammenfassung Felder Syntax und Anwendung Interna und Programmiertechniken (System.Array-Klasse) Syntaxzusammenfassung Interna der Variablenverwaltung Speicherverwaltung Garbage collection Speicherbedarf Variablen- bzw. Objekttyp feststellen (GetType) Variablen- und Objektkonvertierung (casting)
101 101 103 106 109
113 115 116 116 119 123 125 126 127 127 128 129 129 130 131 132 134 134 135 136 138 139 139 141 145 146 147 148 151 153 157
Inhaltsverzeichnis
9
5
Prozedurale Programmierung
5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.4
Verzweigungen (Abfragen) If-Then-Else Select-Case IIf, Choose, Switch Syntaxzusammenfassung Schleifen For-Next-Schleifen For-Each-Schleifen Do-Loop-Schleifen Syntaxzusammenfassung Prozeduren und Funktionen Syntax Lokale und statische Variablen Rekursion Parameterliste Syntaxzusammenfassung Operatoren
161
6
Klassenbibliotheken und Objekte anwenden
6.1 6.1.1 6.1.2 6.1.3 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.3 6.3.1 6.3.2 6.3.3
Schnelleinstieg Miniglossar Beispiel 1 – Textdatei erzeugen und löschen Beispiel 2 – Dateiereignisse empfangen Verwendung der .NET-Bibliotheken Verweise auf Bibliotheken einrichten Klassennamen verkürzen mit Imports Das System-Wirrwarr Shared- und Instance-Klassenmitglieder Objektbrowser Tipps zur Bedienung Deklaration von Schlüsselwörtern Objektbrowser-Icons
186 186 187 190 193 193 194 197 198 203 203 205 208
7
Objektorientierte Programmierung
211
7.1 7.2 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5
Elemente eines Programms Klassen, Module, Strukturen Klassen Klassenvariablen und -konstanten (fields) Methoden Eigenschaften Shared-Klassenmitglieder
162 162 162 163 164 165 165 166 166 167 167 167 170 172 173 180 181
185
212 217 217 220 222 230 236
10
Inhaltsverzeichnis
7.3 7.3.1 7.3.2 7.4 7.4.1 7.4.2 7.4.3 7.5 7.5.1 7.5.2 7.6 7.6.1 7.6.2 7.6.3 7.7 7.8 7.9 7.9.1 7.9.2 7.10
Module und Strukturen Module Strukturen Vererbung Syntax Basisklasse erweitern und ändern LinkedList-Beispiel Schnittstellen (interfaces) Syntax und Anwendung IDisposable-Schnittstelle Ereignisse und Delegates Ereignisse Delegates Delegates und Ereignisse kombinieren Attribute Namensräume Gültigkeitsbereiche (scope) Gültigkeitsbereich definieren Defaultgültigkeit Syntaxzusammenfassung
III
PROGRAMMIERTECHNIKEN
8
Zahlen, Zeichenketten, Datum und Uhrzeit
8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.1.5 8.1.6 8.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.2.7 8.3 8.3.1
Zahlen Notation von Zahlen Rundungsfehler bei Fließkommazahlen Division durch null und der Wert unendlich Arithmetische Funktionen Zahlen runden und andere Funktionen Zufallszahlen Zeichenketten Grundlagen Methoden zur Bearbeitung von Zeichenketten Vergleich von Zeichenketten Interna Unicode, Eurozeichen Zeichenketten effizient zusammensetzen (StringBuilder) Syntaxzusammenfassung Datum und Uhrzeit Methoden zur Bearbeitung von Daten und Zeiten
239 239 242 245 246 248 252 256 257 259 262 263 267 270 272 274 277 277 279 281
287 289 290 290 291 292 293 294 296 298 298 301 305 308 309 312 315 319 321
Inhaltsverzeichnis
11
8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2
Beispiele Syntaxzusammenfassung Konvertierung zwischen Datentypen Automatische und manuelle Konvertierung Konvertierungsfunktionen Spezialmethoden .NET-Formatierungsmethoden .NET-Formatierungsgrundlagen Zahlen formatieren Daten und Zeiten formatieren VB-Formatierungsmethoden Zahlen formatieren Daten und Zeiten formatieren
326 328 331 331 334 338 339 340 343 347 349 350 352
9
Aufzählungen (Arrays, Collections)
355
9.1 9.2 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2
Einführung Klassen- und Schnittstellenüberblick Programmiertechniken Elemente einer Aufzählung einzeln löschen Elemente einer Aufzählungen sortieren Schlüssel einer Aufzählung sortieren Datenaustausch zwischen Aufzählungsobjekten Multithreading Syntaxzusammenfassung Schnittstellen Klassen
10
Dateien und Verzeichnisse
10.1 10.2 10.3 10.3.1 10.3.2 10.3.3 10.3.4 10.3.5 10.3.6 10.3.7 10.3.8 10.4 10.4.1 10.4.2 10.5
Einführung und Überblick Klassen des System.IO-Namensraums Laufwerke, Verzeichnisse, Dateien Informationen über Verzeichnisse und Dateien ermitteln Alle Dateien und Unterverzeichnisse verarbeiten Manipulation von Dateien und Verzeichnissen Spezielle Verzeichnisse, Dateien und Laufwerke ermitteln Bearbeitung von Datei- und Verzeichnisnamen Beispiel – Backup automatisieren Beispiel – Verzeichnisse synchronisieren Syntaxzusammenfassung Standarddialoge Dateiauswahl Verzeichnisauswahl Textdateien lesen und schreiben
356 359 364 365 366 371 373 376 378 378 380
383 384 386 389 390 391 397 402 405 406 407 409 416 416 419 421
12
Inhaltsverzeichnis
10.5.1 10.5.2 10.5.3 10.5.4 10.5.5 10.5.6 10.5.7 10.6 10.6.1 10.6.2 10.6.3 10.6.4 10.6.5 10.7 10.7.1 10.7.2 10.7.3 10.8 10.9 10.9.1 10.9.2 10.9.3 10.9.4 10.10
Codierung von Textdateien Textdateien lesen (StreamReader) Textdateien schreiben (StreamWriter) Beispiel – Textdatei erstellen und lesen Beispiel – Textcodierung ändern Zeichenketten lesen und schreiben (StringReader und StringWriter) Syntaxzusammenfassung Binärdateien lesen und schreiben FileStream BufferedStream (FileStream beschleunigen) MemoryStream (Streams im Arbeitsspeicher) BinaryReader und -Writer (Variablen binär speichern) Syntaxzusammenfassung Asynchroner Zugriff auf Dateien Programmiertechniken Beispiel – Vergleich synchron/asynchron Beispiel – Asynchroner Callback-Aufruf Verzeichnis überwachen Serialisierung Grundlagen Beispiel – String-Feld serialisieren Beispiel – Objekte eigener Klassen serialisieren Beispiel – LinkedList serialisieren IO-Fehler
422 423 427 429 431 432 433 435 436 441 443 444 446 449 450 452 454 456 458 459 463 465 467 468
11
Fehlersuche und Fehlerabsicherung
471
11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.2.3 11.2.4
Fehlerabsicherung Ausnahmen (exceptions) Try-Catch-Syntax Programmiertechniken On-Error-Syntax Fehlersuche (Debugging) Grundlagen Fehlersuche in der Entwicklungsumgebung Debugging-Anweisungen im Code Fehlersuche in Windows.Forms-Programmen
472 472 479 483 488 490 490 492 497 500
12
Spezialthemen
12.1 12.2 12.2.1 12.2.2 12.3
Ein- und Ausgabeumleitung (Konsolenanwendungen) Systeminformationen ermitteln System.Environment-Klasse System.Management-Bibliothek (WMI) Sicherheit
505 506 507 508 511 514
Inhaltsverzeichnis
12.4 12.5 12.5.1 12.5.2 12.5.3 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.7.3
Externe Programme starten Externe Programme steuern (Automation) Grundlagen Beispiel – Daten aus einer Excel-Datei lesen Beispiel – RichTextBox mit Word ausdrucken Multithreading Grundlagen Programmiertechniken Synchronisierung von Threads API-Funktionen verwenden (Declare) Grundlagen API-Viewer Beispiele
IV
WINDOWS-PROGRAMMIERUNG
13
Windows.Forms – Einführung
13.1 13.1.1 13.1.2 13.2 13.3
Einführung Kleines Glossar Einführungsbeispiel Elementare Programmiertechniken Windows.Forms-Klassenhierarchie
14
Steuerelemente
14.1 14.1.1 14.1.2 14.1.3 14.1.4 14.2 14.2.1 14.2.2 14.2.3 14.2.4 14.2.5 14.3 14.3.1 14.3.2 14.3.3 14.4 14.4.1
Einführung Steuerelemente – Überblick Microsoft.VisualBasic.Compatibility.VB6-Steuerelemente ActiveX-Steuerelemente (alias COM-Steuerelemente) Tipps zur Verwendung der Toolbox Gemeinsame Eigenschaften, Methoden und Ereignisse Aussehen Größe, Position, Layout Eingabefokus, Validierung Sonstiges Syntaxzusammenfassung Buttons Gewöhnliche Buttons Auswahlkästchen (CheckBox) Optionsfelder (RadioButton) Textfelder Label
13
516 519 519 524 526 528 528 530 539 546 546 552 554
557 559 560 560 561 569 573
575 576 576 578 578 579 581 581 584 589 591 593 596 596 598 599 600 600
14
14.4.2 14.4.3 14.4.4 14.5 14.5.1 14.5.2 14.6 14.6.1 14.6.2 14.6.3 14.6.4 14.6.5 14.6.6 14.6.7 14.6.8 14.7 14.7.1 14.7.2 14.8 14.8.1 14.8.2 14.8.3 14.8.4 14.8.5 14.9 14.9.1 14.9.2 14.9.3 14.10 14.10.1 14.10.2 14.10.3 14.10.4 14.10.5 14.10.6 14.11 14.11.1 14.11.2 14.11.3 14.12 14.12.1 14.12.2 14.12.3
Inhaltsverzeichnis
LinkLabel TextBox RichTextBox Grafikfelder PictureBox ImageList Listenfelder ListBox CheckedListBox ComboBox ListView ListView-Beispielprogramm TreeView TreeView-Beispielprogramm DataGrid Datums- und Zeiteingabe MonthCalender DateTimePicker Schiebe- und Zustandsbalken, Drehfelder HScrollBar, VScrollBar TrackBar ProgressBar NumericUpDown DomainUpDown Gruppierung von Steuerelementen GroupBox Panel TabControl (Dialogblätter) Spezielle Steuerelemente Splitter Timer ToolTip HelpProvider ErrorProvider NotifyIcon Programmiertechniken Schleife über alle Steuerelemente Steuerelemente dynamisch einfügen Steuerelementfelder Neue Steuerelemente programmieren Vererbte Steuerelemente Steuerelemente zusammensetzen UserControl-Beispiel
601 603 608 610 610 611 612 612 624 625 628 642 649 656 660 662 662 663 665 665 666 667 668 669 669 670 670 672 674 674 678 679 682 686 688 690 690 691 694 696 697 700 701
Inhaltsverzeichnis
15
Gestaltung von Benutzeroberflächen
15.1 15.1.1 15.1.2 15.1.3 15.1.4 15.2 15.2.1 15.2.2 15.2.3 15.2.4 15.2.5 15.2.6 15.2.7 15.2.8 15.2.9 15.2.10 15.3 15.3.1 15.3.2 15.4 15.4.1 15.4.2 15.5 15.5.1 15.5.2 15.6 15.6.1 15.6.2 15.6.3 15.6.4 15.6.5 15.7 15.8 15.9 15.10 15.11 15.12 15.12.1 15.12.2 15.12.3 15.12.4
Formularspezifische Eigenschaften, Methoden und Ereignisse Aussehen Position und Größe Verwaltung von Formularen und Steuerelementen Ereignisreihenfolge Formularinterna Nachrichtenschleife (message loop, DoEvents) Windows Form Designer Code Ereignisprozeduren Formular dynamisch erzeugen Vererbung bei Formularen Windows-XP-Optik Multithreading Betriebssysteminformationen ermitteln Bildschirmauflösung (DPI) Lokalisierung von Windows-Anwendungen Verwaltung mehrerer Fenster Fenster als modale Dialoge anzeigen Mehrere gleichberechtigte Fenster öffnen MDI-Anwendungen Programmiertechniken MDI-Fenster andocken Standarddialoge Einfache Dialogboxen (MessageBox, MsgBox) Standarddialoge (OpenFileDialog, ColorDialog) Menüs Der Menüeditor Anwendung und Programmierung Menüs bei MDI-Anwendungen Kontextmenüs Menüeinträge selbst zeichnen (owner-drawn menu) Symbolleiste (ToolBar) Statusleiste (StatusBar) Tastatur Maus Zwischenablage Drag&Drop Programmiertechniken Beispiel – Symbolleiste verschieben Beispiel – Datei-Drop aus dem Windows-Explorer Drag&Drop zwischen Listenfeldern
15
709 710 710 712 713 715 718 720 722 726 727 729 730 733 745 747 751 757 758 761 768 768 772 777 777 779 780 780 782 785 786 789 795 801 804 809 814 817 818 821 823 825
16
Inhaltsverzeichnis
16
Grafikprogrammierung (GDI+)
16.1 16.1.1 16.1.2 16.1.3 16.2 16.2.1 16.2.2 16.2.3 16.2.4 16.2.5 16.2.6 16.3 16.3.1 16.3.2 16.3.3 16.3.4 16.3.5 16.3.6 16.3.7 16.4 16.4.1 16.4.2 16.4.3 16.4.4 16.4.5 16.4.6 16.4.7 16.5 16.5.1 16.5.2 16.5.3 16.5.4 16.5.5 16.5.6 16.5.7 16.5.8 16.5.9 16.5.10 16.5.11
Einführung Ein erstes Beispiel Grafik-Container (Form, PictureBox) Dispose für Grafikobjekte Elementare Grafikoperationen Linien, Rechtecke, Vielecke, Ellipsen, Kurven (Graphics-Klasse) Farben (Color-Klasse) Linienformen (Pen-Klasse) Füllmuster (Brush-Klassen) Koordinatensysteme und -transformationen Syntaxzusammenfassung Text ausgeben (Font-Klassen) Einführung TrueType-, OpenType- und Type-1-Schriftformate Schriftarten und -familien Schriftgröße Schriftattribute, Textformatierung Font-Auswahldialog Syntaxzusammenfassung Bitmaps, Icons und Metafiles Graphics- versus Image- versus Bitmap-Klasse Bitmaps in Formularen darstellen Bitmaps manipulieren Durchsichtige Bitmaps Icons Metafile-Dateien Syntaxzusammenfassung Interna und spezielle Programmiertechniken Zeichen- und Textqualität Grafikobjekte zusammensetzen (GraphicsPath) Umgang mit Regionen und Clipping Interna zu den Paint- und Resize-Ereignissen Rechteck-Auswahl mit der Maus (Rubberbox) Bitmap-Grafik zwischenspeichern (AutoRedraw) Flimmerfreie Grafik (Double-Buffer-Technik) Scrollbare Grafik Einfache Animationseffekte Bitmap mit Fensterinhalt erzeugen und ausdrucken Bitmap über Byte-Feld adressieren
831 832 833 835 836 840 840 850 853 856 859 863 867 867 868 868 871 880 890 892 894 894 896 900 907 910 912 914 915 915 918 919 922 929 932 944 946 948 951 954
Inhaltsverzeichnis
17
Drucken
17.1 17.2 17.2.1 17.2.2 17.2.3 17.2.4 17.2.5 17.3 17.4 17.4.1 17.4.2 17.4.3 17.4.4 17.5 17.5.1 17.5.2 17.5.3
Überblick Grundlagen PrintDocument-Steuerelement PrintDialog- und PageSetupDialog-Steuerelement PrintPreviewDialog-Steuerelement Druckereigenschaften und Seitenlayout Syntaxzusammenfassung Beispiel – Mehrseitiger Druck Beispiel – Inhalt eines Textfelds ausdrucken Codegerüst PrintDocument-Ereignisprozeduren Hilfsfunktionen Verbesserungsideen Fortgeschrittene Programmiertechniken Drucken ohne Steuerelemente Eigene Seitenvorschau Beispielprogramm
18
Weitergabe von Windows-Programmen (Setup.exe)
18.1 18.2 18.3 18.3.1 18.3.2 18.4 18.4.1 18.4.2 18.4.3 18.4.4 18.4.5 18.4.6
Einführung Installationsprogramm erstellen (Entwicklersicht) Installation ausführen (Kundensicht) Installation eines .NET-Programms Installation des .NET-Frameworks Installationsprogramm für Spezialaufgaben Grundeinstellungen eines Setup-Projekts Startmenü, Desktop-Icons Benutzeroberfläche des Installationsprogramms Start- und Weitergabebedingungen Dateityp registrieren Einträge in der Registrierdatenbank durchführen
ANHANG A B C D E
Abkürzungsverzeichnis Glossar Dateikennungen Inhalt der CD-ROM Quellenverzeichnis Stichwortverzeichnis
17
959 960 961 961 965 969 970 974 977 981 982 983 987 991 992 992 996 997
1003 1004 1005 1008 1008 1010 1011 1012 1014 1015 1017 1020 1021
1023 1025 1029 1035 1037 1039 1041
Vorwort Wenn Sie gerade beginnen, sich mit der neuen Welt von .NET im Allgemeinen und mit VB.NET im Besonderen auseinanderzusetzen, dann geht es Ihnen wahrscheinlich so ähnlich wie mir vor einem Dreivierteljahr: Es gibt überwältigend viele Informationen, die Zahl der neuen Paradigmen, Konzepte, Klassen scheint unendlich zu sein und wo immer man sich einliest, stellen sich mehr neue Fragen, als bestehende beantwortet werden.
Was ist .NET? Ich stelle Ihnen zu der Frage, was .NET nun wirklich ist, drei mögliche Antworten zur Auswahl: •
Einfach ein neues Kürzel der Microsoft-Marketing-Abteilung ohne konkreten Inhalt.
•
Ein neuer Versuch von Microsoft, das Internet auch serverseitig zu erobern. (Clientseitig ist das mit dem Internet Explorer schon gelungen. Aber nach wie vor laufen mehr als doppelt so viele Websites auf Unix-/Linux-Systemen als auf Windows-Systemen.)
•
Eine neue, objektorientierte Programmierschnittstelle zu fast allen Betriebssystemfunktionen.
Alle drei Antworten sind teilweise richtig, aber am ehesten trifft meiner Meinung nach der letzte Punkt zu: Der Kern von .NET ist eine neue Klassenbibliothek, die einen komfortablen, konsistenten Zugang zu nahezu allen Betriebssystemfunktionen ermöglicht. (Natürlich will die .NET-Initiative noch viel mehr. Microsoft hat ganze Bücher veröffentlicht, um zu erklären, was es mit .NET beabsichtigt, worin sich .NET von früheren Technologien unterscheidet etc. Ich möchte hier aber nicht die Versprechungen der Marketing-Abteilung wiedergeben – die finden Sie auch unter http://www.microsoft.com.)
Wozu VB.NET? Die .NET-Bibliotheken nützen Konzepte der objektorientierten Programmierung, die in dieser Form weder in Visual Basic 6 noch in C++ zur Verfügung standen. Damit die .NETBibliotheken überhaupt eingesetzt werden können, hat Microsoft gleich zwei neue Programmiersprachen geschaffen: Während sich C# primär an C-, C++- und Java-Programmierer richtet, ist VB.NET für die zahlreichen Visual-Basic-Programmierer gedacht, die die neuen .NET-Funktionen nutzen möchten. Die beiden Sprachen sind fast gleichwertig und unterscheiden sich primär durch ihre Syntax. VB.NET gewinnt seine Daseinsberechtigung also zuerst einmal dadurch, dass es die .NETBibliotheken für Visual-Basic-Programmierer zugänglich macht. Darüber hinaus bietet VB.NET im Vergleich zu VB6 aber auch eine Fülle neuer Merkmale, auf die viele Programmierer schon seit Jahren gewartet haben:
20
Vorwort
•
VB.NET ist jetzt eine vollwertige, objektorientierte Programmiersprache. VB.NET unterstützt echte Vererbung, Schnittstellen, Attribute, Delegates (eine neue Art von Funktionszeigern) etc.
•
VB.NET kann ohne Einschränkungen dazu verwendet werden, um Multithreading-Anwendungen zu entwickeln. (Das sind Anwendungen, bei denen mehrere Programmteile quasi parallel ausgeführt werden.)
•
Die in VB.NET zur Verfügung stehenden Komponenten und Bibliotheken sind entrümpelt worden und viel konsistenter zu verwenden als früher.
•
VB.NET ist erstmals auch zur Entwicklung von Internet-Anwendungen geeignet (ASP.NET). Das hat Microsoft natürlich schon bei früheren VB-Versionen versprochen. Die damals vorgestellten Konzepte überzeugten aber nicht und fanden nur geringe Verbreitung. Durchgesetzt hat sich stattdessen ASP, eine Art Script-Code, der inkompatibel zur VB6-Entwicklungsumgebung ist. Das neue ASP.NET verbindet die Vorteile von ASP mit denen von VB.NET. Damit können dynamische Webseiten nun direkt in der VB.NET-Entwicklungsumgebung programmiert werden.
VB.NET ist allerdings nicht der Nachfolger von VB6, d.h., es ist nicht VB7, wie es sich viele Programmierer erwartet haben. VB.NET ist vielmehr eine von Grund auf neue Programmiersprache! Wenn Sie VB6-Vorkenntnisse haben, wird Ihnen natürlich einiges vertraut vorkommen, intern ist aber wirklich alles neu: Das beginnt mit neuen Variablentypen, neuen Steuerelementen, einer neuen Art, Code zu verwalten und zu kompilieren, und endet bei einer unüberschaubaren Fülle neuer Bibliotheken und einer neuen Entwicklungsumgebung. Was sich in den vergangenen zehn Jahren und sechs Versionen an VB-Wildwuchs angesammelt hat, wurde neu geordnet, zum Teil durch bessere Konzepte ersetzt, zum Teil aber auch einfach gestrichen. Das Ergebnis ist eine ohne jede Einschränkung gut durchdachte Programmierumgebung, die zur Entwicklung von Anwendungen fast jeden Typs geeignet ist. Daraus folgt leider auch: VB.NET ist inkompatibel zu VB6. Vorhandener Code kann weder weiterentwickelt noch gewartet noch (mit vertretbarem Aufwand) migriert werden. Damit hat sich VB6 – eine der populärsten Programmiersprachen, die es je unter Windows gab – als Sackgasse herausgestellt. Microsoft hat hier eine ebenso unpopuläre wie mutige Entscheidung getroffen: die Kompatibilität wurde zugunsten neuer und durchwegs besserer Konzepte geopfert. (So viel Mut kann man sich freilich nur leisten, wenn man eine marktbeherrschende Position innehat ...)
Ein Blick in die Zukunft Als ich das erste Mal von den .NET-Plänen Microsofts gehört habe, war ich ziemlich skeptisch, und mit dieser Skepsis habe ich im Herbst 2001 auch mit der Arbeit an diesem Buch begonnen. Dabei wurde ich in fast jeder Hinsicht positiv überrascht: VB.NET läuft sehr stabil (obwohl es ein Version-1-Produkt ist), viele Konzepte wirken gut durchdacht, die Bibliotheken und Komponenten bieten viel mehr Möglichkeiten als bisher und sind zugleich konsistent in ihrer Anwendung. Die mitgelieferte Online-Dokumentation ist viel-
Vorwort
21
leicht nicht perfekt, aber immerhin sehr umfassend und besser als vieles, was ich für andere Programmiersprachen in letzter Zeit gelesen habe. Kurz und gut, mit .NET ist Microsoft tatsächlich ein großer Wurf gelungen! Ich bin überzeugt davon, dass die Zukunft der Windows-Programmierung .NET heißen wird. Nicht jeder wird sofort umsteigen, aber in wenigen Jahren wird die überwiegende Mehrheit aller Windows-Entwickler sicherlich .NET einsetzen. VB.NET ist natürlich nicht die einzige Programmiersprache, um .NET zu nutzen, aber es ist meiner Ansicht nach die geeignetste und am leichtesten zu erlernende.
Dieses Buch Dieses Buch versucht, einen umfassenden und detaillierten Einstieg in die VB.NET-Programmierung zu geben. Im Vordergrund stehen dabei weniger enzyklopädische Aufzählungen (die finden Sie ohnedies in der Online-Hilfe) von Klassen oder Methoden, sondern der Blick fürs Ganze, die Herstellung von Zusammenhängen und die Präsentation praxisnaher Lösungsansätze. Die zentrale Fragestellung lautet also nicht, welche Funktionen es gibt, sondern wie alltägliche Probleme aus der Programmierpraxis gelöst werden können. Inhaltlich ist das Buch in vier Teile gegliedert: •
Die einleitenden Kapitel dienen als Schnelleinstieg und geben eine Referenz der wichtigsten Änderungen für VB6-Anwender.
•
Der Grundlagenteil führt in die Syntax von VB.NET, in die objektorientierte Programmierung und – damit verbunden – in die Konzepte der Objektverwaltung unter .NET ein.
•
Die folgenden Kapitel stellen Programmiertechniken vor, die für jede Art von VB.NETAnwendung benötigt werden: Methoden zur Konvertierung und Formatierung von Basisdatentypen, Methoden zum Zugriff auf Dateien, Verfahren zur Absicherung von Programmcode, Klassen zur Verwaltung von Aufzählungen etc. Auch fortgeschrittene Techniken wie der (manchmal noch immer erforderliche) Aufruf von API-Funktionen oder Multithreading kommen nicht zu kurz.
•
Windows-Programmierung lautet schließlich die Devise im vierten Teil, der immerhin fast das halbe Buch einnimmt: Die behandelten Themen reichen von den Windows.Forms-Grundlagen über die Vorstellung der wichtigsten Steuerelemente bis hin zur Grafikprogrammierung und dem Ausdruck von Dokumenten. Damit Sie Ihre Programme problemlos weitergeben können, werden auch die neuen Möglichkeiten von SetupProjekten vorgestellt.
So wie Microsoft mit VB.NET eine Programmiersprache geschaffen hat, die sich an professionelle Anwender richtet, so orientiert sich auch diese Buch an dieser Zielgruppe. Ich setze voraus, dass Sie bereits programmieren können. Sie brauchen keine Vorkenntnisse in VB6 oder einem anderen VB-Dialekt, aber Sie sollten zumindest wissen, was eine Variable, eine Schleife und eine Prozedur ist. (Wenn das nicht der Fall ist, sollten Sie für die ersten Schritte zu einem anderen Buch greifen. Nach ein paar Wochen werden Sie den Bedarf
22
Vorwort
nach Informationen mit mehr Tiefgang verspüren – dann ist der richtige Zeitpunkt für dieses Buch gekommen.)
Viel Erfolg! Ich wünsche Ihnen, dass Ihnen mit diesem Buch der VB.NET-Einstieg gelingt, dass Sie die ersten Hürden rasch überwinden und danach wie ich Spaß an den vielen neuen Möglichkeiten gewinnen, die Ihnen VB.NET bietet!
Michael Kofler, Juni 2002 http://www.kofler.cc
PS: Manche Leser kennen sicher auch mein Buch über VB6 (Programmiertechniken, Datenbanken, Internet): Damals habe ich versucht, einen umfassenden Einstieg in die gesamte Breite von VB6 zu geben. Bei VB.NET hat sich das aber als unmöglich herausgestellt. Deswegen werden die Themen Datenbanken, Internet und XML nicht in diesem Buch, sondern in einem eigenen Band behandelt. Genauere Informationen über Inhalt und Erscheinungstermin finden Sie auf meiner Website. Der Vorteil dieser Zweiteilung besteht darin, dass ich trotz der viel größeren Themenfülle meinem Schreibstil treu bleiben kann und die einzelnen Themen so detailliert beschreiben kann, wie es mir notwendig erscheint.
Formales
TIPP
Dieses Buch ist zwar in deutscher Sprache verfasst, es enthält aber eine Menge englischer Begriffe. Das hat damit zu tun, dass die Originaldokumentation zu VB.NET natürlich englisch ist und dass ich die krampfhafte Übersetzung von Fachausdrücken eher vermeide. (Die Online-Hilfe zu VB.NET folgt da einem anderen Ansatz; dort ist wirklich fast alles eingedeutsch. Ich bin aber der Ansicht, dass die Klarheit und Eindeutigkeit des Texts darunter leidet.) Als zusätzliche Hilfe für Querleser befindet sich im Anhang ein Abkürzungsverzeichnis und ein Glossar!
Voraussetzungen/Versionen Dieses Buch geht davon aus, dass Sie zumindest VB.NET Standard besitzen. Diese Entwicklungsumgebung ist ein Produkt von Microsoft, das käuflich erworben werden muss. (Die Entwicklungsumgebung befindet sich also nicht auf der beiliegenden CD-ROM! Informationen über die unterschiedlichen Versionen der VB.NET- und VS.NET-Entwicklungsumgebungen finden Sie in Abschnitt 2.7.) Alle Beispielprogramme für dieses Buch wurden unter der folgenden Umgebung getestet: Windows 2000 deutsch mit Service Pack 2 VS.NET Enterprise Architect deutsch (final version) mit Service Pack 1
Beispielprogramme, Beispielcode Ziel der Beispielprogramme ist es nicht, fertige Anwendungen zu präsentieren, sondern bestimmte im Buch erklärte Programmiertechniken zu veranschaulichen. Aus diesem Grund gibt es in diesem Buch nicht wenige lange, sondern viele, überwiegend sehr kurze Beispiele. Aus Platzgründen sind bei Programmlistings generell nur die für den jeweiligen Abschnitt interessanten Passagen abgedruckt. Den vollständigen Code finden Sie auf der beiliegenden CD. Um Ihnen bei der Suche nach den Beispieldateien zu helfen, finden Sie am Beginn jedes Programmlistings einen Kommentar der Art 'Beispiel grafik\intro. Das bedeutet, das sich der Code im Verzeichnis grafik\intro auf der CD befindet. Im Code sind die Namen von Prozeduren durch fette Schrift hervorgehoben, um Ihnen die Orientierung zu erleichtern. Darüber hinaus fehlt bei Ereignisprozeduren von WindowsProgrammen meist die Parameterliste.
24
Formales
Statt Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
wird nur Private Sub Button1_Click(...) Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
abgedruckt, um die Listings kompakter und übersichtlicher zu halten. Das erscheint mir deswegen zweckmäßig, weil an Windows-Ereignisprozeduren ohnedies immer dieselben zwei Parameter übergeben werden: sender mit dem zugrunde liegenden Objekt und e mit ereignisspezifischen Daten. (Details zu Windows-Ereignisprozeduren werden in Abschnitt 13.1.2 beschrieben.)
Verweise auf die Online-Hilfe
TIPP
In diesem Buch finden Sie zahllose Verweise auf die Online-Hilfe. Diese Verweise sehen wie Webadressen aus, beginnen aber mit ms-help://. Da es möglich ist, dass sich die Adressen in der Zukunft ändern können, wird in den Verweisboxen immer auch ein Suchbegriff angegeben, der zu dem Hilfethema führt. Sie können die Hilfetexte übrigens nicht nur im Hilfebrowser von Visual Studio, sondern auch im Internet Explorer lesen. Die im Buch angegebenen Hilfeadressen beziehen sich auf die deutsche Version von Visual Studio .NET. Wenn Sie die englische Version der Online-Hilfe installiert haben, müssen Sie aus allen Hilfeverweisen die fünf Zeichen .1031 entfernen. (Diese Zeichen geben an, dass Sie die deutsche Version der Hilfe lesen möchten.) Statt durch ms-help://MS.VSCC/MS.MSDNVS.1031/vbcmn/html/vborilegacyactivexcntrlref.htm
wird der entsprechende englische Text also durch ms-help://MS.VSCC/MS.MSDNVS/vbcmn/html/vborilegacyactivexcntrlref.htm
TIPP
aufgerufen.
Sie müssen die Links übrigens nicht mühsam abtippen. Auf der beiliegenden CD befindet sich die Datei Links.html, die alle Web- und Hilfe-Links dieses Buchs enthält (geordnet nach Kapiteln). Bei den Hilfe-Links ist jeweils die deutsche und die englische Version enthalten. Wie lange die Links gültig bleiben, kann ich natürlich nicht versprechen ...
Formales
25
Verweise auf Knowledge-Base-Artikel Sie werden in diesem Buch auch Verweise auf so genannte Knowledge-Base-Artikel (kurz KB-Artikel) finden. Dabei handelt es sich um Artikel in einer Art Microsoft-Hilfedatenbank. Diese Artikel werden durch eine sechsstellige Zahl mit einem vorangestellten Q bezeichnet. Q301264 meint also den KB-Artikel 301264. Sie finden derartige Artikel auf der Microsoft-Website oder in der MSDN-Library. Die Webadresse von KB-Artikeln hat sich in der Vergangenheit immer wieder geändert. Zurzeit sieht die Adresse für den Artikel Q301264 so aus: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301264
.NET im Internet Die vielleicht beste Informationsquelle bei diffizilen Problemen der .NET-Programmierung sind die diversen Newsgruppen, die über den News-Server msnews.microsoft.com zur Verfügung gestellt werden. Einige Beispiele für derartige Gruppen sind: microsoft.public.de.german.entwickler.dotnet.vb (deutsch) microsoft.public.dotnet.languages.vb (englisch) microsoft.public.dotnet.framework.windowsforms (english)
Zum Lesen der aktuellen Beiträge in den Newsgruppen können Sie Outlook Express verwenden. Zur Suche nach alten Artikeln ist dagegen http://groups.google.com am besten geeignet. Dabei geben Sie als zusätzlichen Suchbegriff einfach dotnet ein. Wenn Sie also Probleme damit haben, eine Bitmap als GIF-Datei zu speichern, suchen Sie beispielsweise nach dotnet gif export. Darüber hinaus gibt es natürlich eine Menge Websites, die sich intensiv mit VB.NET oder .NET auseinandersetzen. Links auf derartige Seiten finden Sie auf meiner Website unter http://www.kofler.cc/vbnet.html.
Teil I
Einführung
1
Hello World
Ein erstes Beispiel (genau genommen sind es sogar zwei) soll Sie in die VB.NET-Welt einführen. Dabei geht es weniger um konkrete Programmiertechniken (für die ist in den folgenden Kapiteln noch genug Platz) als vielmehr darum, die Entwicklungsumgebung anhand konkreter Beispiele kennen zu lernen. Das Kapitel endet dann auch mit einigen Tipps zur Bedienung der Entwicklungsumgebung. Voraussetzung für dieses Kapitel ist die Installation von VB.NET bzw. VS.NET, damit Ihnen die Entwicklungsumgebung zur Verfügung steht. Wie Sie im nächsten Kapitel lernen werden, ist die VB.NET-Programmierung theoretisch auch ohne eine Entwicklungsumgebung von Microsoft möglich. Dieses Buch geht aber davon aus, dass Sie mit VB.NET bzw. VS.NET arbeiten. 1.1 1.2 1.3
Hello World (Konsolenversion) Hello World (Windows-Version) Bedienung der Entwicklungsumgebung
30 37 41
30
1 Hello World
1.1
Hello World (Konsolenversion)
Projekttyp auswählen Jedes neue Projekt beginnt damit, dass Sie die Entwicklungsumgebung starten und mit DATEI|NEU|PROJEKT ein neues Projekt starten. Es erscheint dann der in Abbildung 1.1 gezeigte Dialog, in dem Sie zwischen einer Menge Projekttypen auswählen können. Die überwiegende Mehrheit aller Projekte dieses Buchs sind Visual-Basic-Projekte, wobei die Vorlagen WINDOWS-ANWENDUNG oder KONSOLENANWENDUNG zum Einsatz kommen. Das hier vorgestellte Beispielprogramm ist eine KONSOLENANWENDUNG. Das bedeutet, dass das Programm keine eigenen Fenster verwendet, sondern alle Texteingaben und -ausgaben in einem Konsolenfenster (dem ehemaligen DOS-Fenster) durchführt.
Abbildung 1.1: Dialog zum Beginn eines neuen Projekts
Beachten Sie in Abbildung 1.1 auch die Eingabefelder NAME und SPEICHERORT: Der Speicherort gibt an, in welchem Verzeichnis das Projektverzeichnis gespeichert wird. NAME gibt den Projektnamen an. Dieser Name wird gleich mehrfach verwendet: •
für das Verzeichnis, in dem alle Projektdateien gespeichert werden;
•
für die Namen der Projektdateien (*.vbproj für das VB.NET-Projekt sowie *.sln für die Projektmappe, die auch mehrere Projekte enthalten kann);
•
für den Namen der resultierenden Programmdatei (*.exe, wird in der Entwicklungsumgebung bei den Projekteigenschaften als Assemblyname bezeichnet):
1.1 Hello World (Konsolenversion)
•
31
für den so genannten Stammnamensraum, der ebenfalls Teil der Projekteigenschaften ist. Dieser Stammnamensraum bestimmt den vollständigen Klassennamen aller Klassen, die Sie in Ihrem Projekt erstellen. Der Stammnamensraum ist vorerst unwichtig und spielt erst dann eine Rolle, wenn Sie eigene Klassenbibliotheken programmieren (siehe Kapitel 7 und speziell Abschnitt 7.8). Schon jetzt wichtig ist allerdings der Umstand, dass der Stammnamensraum nicht im Konflikt mit Klassennamen stehen sollte, die Sie in Ihrem Programm einsetzen. Beispielsweise verwendet dieses Beispielprogramm die Klasse Console, um Ausgaben durchzuführen. Sie sollten deswegen dem Beispielprogramm nicht den Namen Console geben!
Mit den Einstellungen aus Abbildung 1.1 wird das neue Projekt also im Verzeichnis D:\code\vb.net\hello-world\hello-console\ erzeugt. Die Entwicklungsumgebung besteht darauf, dass sich jedes Projekt in einem eigenen Verzeichnis befindet. Die resultierenden Dateien werden etwas weiter unten kurz beschrieben – siehe Abbildung 1.6.
Die Entwicklungsumgebung Nach der Auswahl des Projekttyps gelangen Sie in die Entwicklungsumgebung. Das Aussehen der Entwicklungsumgebung kann durch zahllose Einstellmöglichkeiten variiert werden, weswegen die Fensteranordnung nicht unbedingt so aussehen muss wie in Abbildung 1.2. Entscheidend sind vorerst nur zwei Fenster: der Projektmappen-Explorer (rechts oben) und das Codefenster (links oben). Den Projektmappen-Explorer können Sie bei Bedarf mit ANSICHT|PROJEKTMAPPENEXPLORER öffnen. Das Fenster gibt Auskunft über die zum Projekt gehörenden Dateien. Von dort können Sie durch einen Doppelklick das Codefenster öffnen. Der Code befindet sich bei Konsolenanwendungen per Default in der Datei Module1.vb. (Sie können die Datei ganz einfach im Projektmappen-Explorer umbenennen, wenn Sie möchten.)
Programmcode (Konsolenanwendungen) Bei neuen Projekten enthält das Codefenster nur ein minimales Codegerüst, das im Regelfall bereits ausreicht, um das Programm zu kompilieren. (Das Programm erfüllt dann aber natürlich keine Aufgaben.) Bei Konsolenanwendungen ist das Codegerüst besonders kurz und besteht aus nur vier Zeilen: Module Module1 Sub Main() End Sub End Module
32
1 Hello World
Abbildung 1.2: Die Entwicklungsumgebung
Die Programmausführung beginnt und endet mit Main. Main ist wiederum die einzige Prozedur des Moduls Module1. (VB.NET-Code muss sich immer in einem Modul oder in einer Klasse befinden.) Um das in Abbildung 1.5 dargestellte Hello-World-Programm zu realisieren, müssen Sie Main mit Code füllen: ' Beispiel hello-world\hello-console Option Strict On Module Module1 Sub Main() Console.WriteLine("Hello Console!") Console.WriteLine() 'leere Zeile Console.WriteLine("{0}, das heutige Datum ist: {1}!", _ Environment.UserName, Now.ToLongDateString()) Console.WriteLine() Console.WriteLine("Return drücken, um das Programm zu beenden ..") Console.ReadLine() End Sub End Module
1.1 Hello World (Konsolenversion)
33
Es ist nicht sinnvoll, hier alle Hintergründe des Codes zu beschreiben – das würde zu tief in die Welt der Klassen, Objekte, Methoden etc. führen. Einen geeigneten Einstieg in dieses Thema gibt Kapitel 6. Damit Sie aber zumindest eine grobe Vorstellung davon haben, was in dem kurzen Programm vor sich geht und warum es funktioniert, folgt nun eine kurze Beschreibung der im Code vorkommenden Schlüsselwörter. Option Strict On bewirkt, dass der Compiler eine genaue Überprüfung durchführt, ob alle Variablen deklariert sind und ob bei Zuweisungen bzw. beim Aufruf von Methoden oder Prozeduren immer die richtigen Datentypen angegeben werden. Option Strict kann viele Flüchtigkeitsfehler vermeiden helfen (siehe auch Abschnitt 4.1.4). Console ist eine Klasse, die allen Anwendungen automatisch zur Verfügung steht. Die beiden wichtigsten Methoden dieser Klasse sind Write- und ReadLine. Mit WriteLine schreiben Sie eine Zeichenkette in das Konsolenfenster. WriteLine erwartet als ersten Parameter eine Zeichenkette. Diese Zeichenkette darf die Spezialcodes {0}, {1} etc. enthalten – dann werden
an diese Stelle die weiteren optionalen Parameter eingesetzt. Das Beispielprogramm verwendet diese Art der Formatierung, um den Benutzernamen und das Datum auszugeben. (Die Angabe von Formatzeichenketten wird ausführlich in Abschnitt 8.5 beschrieben. Der von WriteLine eingesetzte Mechanismus geht auf die Methode String.Format zurück und steht bei vielen weiteren .NET-Methoden zur Verfügung.) ReadLine ist das Gegenstück zu WriteLine. Es erwartet eine mit Return abgeschlossene Eingabe, die im Konsolenfenster durchgeführt wird. ReadLine liefert die eingegebene Zeichenkette zurück. Sehr oft wird ReadLine aber wie im obigen Beispiel dazu verwendet, das Programmende so lange zu verzögern, bis der Anwender Return drückt. Die resultierende
Eingabe wird dabei gar nicht ausgewertet – es geht nur darum, zu verhindern, dass das Programm sofort nach den Konsolenausgaben endet und das Ergebnis daher nur für ein paar Sekundenbruchteile am Bildschirm zu sehen ist. Environment ist eine weitere Klasse, die allen Programmen zur Verfügung steht. Sie hilft dabei, verschiedene Informationen des Betriebssystems, des aktuellen Benutzers etc. zu ermitteln. Hier wird die Methode UserName eingesetzt, um den Namen des eingeloggten Benutzers festzustellen. Weitere Environment-Methoden werden in Abschnitt 12.2 vorgestellt. Now ist eine VB.NET-spezifische Eigenschaft, die das aktuelle Datum samt Uhrzeit liefert. Now gibt ein Objekt des Klasse Date zurück. Um ein derartiges Objekt in eine Zeichenkette der Form Mittwoch, 12. Juni 2002 umzuwandeln, stellt die Date-Klasse die Methode ToLongDateString zur Verfügung.
Kommentare: Kommentare werden mit dem Zeichen ' eingeleitet und reichen bis an das Ende der Zeile.
VERWEIS
Mehrzeilige Anweisungen: Wenn Sie eine Anweisung über mehrere Zeilen verteilen möchten (meistens deswegen, um den Code übersichtlicher anzuzeigen), müssen Sie die jeweils vorangehende Zeile mit einem Leerzeichen und dem Zeichen _ abschließen. Wie der Programmcode für Hello-Console aus objektorientierter Sicht zu verstehen ist (d.h., was ein Modul ist, warum die Codeausführung mit Main beginnt etc.) wird in Abschnitt 7.1 beschrieben.
34
1 Hello World
Codeeingabe (IntelliSense) Bei der Codeeingabe werden Sie feststellen, dass die Entwicklungsumgebung den Code automatisch einrückt (und zwar per Default um vier Zeichen pro Einrückebene, während die Listings dieses Buchs aus Platzgründen nur um zwei Zeichen pro Ebene eingerückt sind). Des Weiteren hilft Ihnen die Entwicklungsumgebung bei der Codeeingabe durch so genannte IntelliSense-Funktionen: •
Bei der Eingabe von Schlüsselwörtern werden alle für ein bestimmtes Objekt bzw. für eine Klasse zur Auswahl stehenden Eigenschaften, Methoden etc. in einer Liste anzeigt. Nach der Eingabe der ersten Zeichen reicht Tab zur Vervollständigung des Namens. Diese Funktion funktioniert meistens gut. Es gibt aber Fälle, bei denen die Funktion Schlüsselwörter nicht zu kennen glaubt, obwohl diese sehr wohl verwendet werden dürfen. Lassen Sie sich davon nicht irritieren! (Für Insider: Das Fehlverhalten betrifft meist Eigenschaften oder Methoden von vererbten Klassen bzw. von Klassen, die Schnittstellen realisieren.) Manchmal erscheint die IntelliSense-Liste nicht automatisch. In solchen Fällen lässt sich die Funktion mit Strg+Leertaste zur Zusammenarbeit bewegen.
•
Bei der Eingabe von Parametern zeigt die Entwicklungsliste in einem kleinen gelben Fenster die Parameterliste an (siehe Abbildung 1.3). Diese Funktion wäre an sich auch sehr praktisch, wenn sie ein bisschen besser funktionieren würde. Das erste Problem besteht darin, dass einzelne Methoden oft eine ganze Menge unterschiedlicher Syntaxvarianten kennen, je nachdem, wie viele Parameter übergeben werden und in welchem Typ diese Parameter übergeben werden. WriteLine kennt beispielsweise 18 verschiedene Varianten. Die IntelliSense-Funktion erkennt aber nur selten die Variante, die für Ihren Code gerade die richtige ist. In Abbildung 1.3 zeigt die Entwicklungsumgebung beispielsweise Variante 14 für einen String-Parameter an. Dabei ist offensichtlich, dass zumindest drei Parameter übergeben werden. Sie können nun die angezeigte Parameterliste einfach ignorieren (oder mit Esc ausschalten) oder mit den Cursortasten die richtige Variante suchen (siehe Abbildung 1.4). Das zweite Problem besteht darin, dass die Parameterliste oft unerwartet erscheint und Sie dabei hindert, den Cursor mit den Tasten ↑ oder ↓ in die nächste Zeile ober- bzw. unterhalb zu bewegen. Dazu müssen Sie das IntelliSense-Fenster zuerst mit Esc schließen. Sie werden bald feststellen, dass Esc zu den wichtigsten Tasten bei der Codeeingabe zählt.
Wenn Sie möchten, können Sie die IntelliSense-Funktion auch abschalten: EXTRAS|OPTIONEN, Dialogblatt TEXTEDITOR|BASIC, Optionen ANWEISUNGSABSCHLUSS.
1.1 Hello World (Konsolenversion)
35
Abbildung 1.3: Die IntelliSense-Funktion der Entwicklungsumgebung zeigt oft die falsche Parameterliste an
Abbildung 1.4: Das ist die richtige Parameterliste für diesen Aufruf von WriteLine
Bei der VB.NET-Codeeingabe spielt die Groß- und Kleinschreibung von Schlüsselwörtern und Variabeln keine Rolle. Der Editor passt Ihre Eingabe automatisch an die Schreibweise an, die bei der Definition des Schlüsselworts bzw. der Variable verwendet wurde.
Programm ausführen Um das Programm auszuführen, drücken Sie einfach auf den blauen Pfeil-Button (STARTEN) in der Standardsymbolleiste bzw. führen DEBUGGEN|STARTEN aus. Das Programm wird dazu zuerst kompiliert. Wenn dabei Fehler auftreten, werden diese angezeigt und müssen korrigiert werden. (Die meisten Fehler erkennt die Entwicklungsumgebung schon vor dem Kompilieren und kennzeichnet die betroffenen Zeilen durch eine rote gewellte Linie.)
Abbildung 1.5: Das Programm Hello Console
Wenn keine Fehler aufgetreten sind, erscheint das Programm in einem eigenen Fenster (siehe Abbildung 1.5). Noch eine Anmerkung zu dieser Abbildung: Normalerweise wird der Text in Konsolenfenstern in weißer Schrift auf schwarzem Hintergrund dargestellt. Da das weder am Bildschirm noch in einem Buch besonders augenfreundlich ist, habe ich die Farben für alle Bildschirmabbildungen geändert. Dazu klicken Sie den Fenstertitel mit der rechten Maustaste an und ändern im Eigenschaftsdialog FARBEN die entsprechenden Einstellungen.
36
1 Hello World
Projekt- und Quellcodedateien Bis jetzt kamen Sie in der Entwicklungsumgebung nur mit der Codedatei Module1.vb in Kontakt. Tatsächlich hat die Entwicklungsumgebung aber eine ganze Reihe weiterer Dateien erzeugt (siehe Abbildung 1.6), die hier kurz beschrieben werden: Module1.vb
VB.NET-Quellcode
AssemblyInfo.vb
VB.NET-Quellcode mit Informationen über die Programmversion, den Entwickler, die Firma, das Copyright etc.; die Angabe dieser Informationen ist optional
projname.vbproj
VB.NET-Projektdatei mit Informationen darüber, aus welchen Dateien das Projekt besteht, welche Einstellungen gelten, welche Verweise auf zusätzliche Bibliotheken eingerichtet wurden etc.; viele Einstellungen dieser Datei können durch den EIGENSCHAFTENDialog zum Projekt eingestellt werden
projname.vbproj.user
Ergänzung zur VB.NET-Projektdatei, enthält benutzerspezifische Einstellungen
projname.sln
VS.NET-Projektmappe mit Informationen darüber, welche Projekte zur Mappe gehören; bei einfachen Anwendungen enthält die Datei nur einen Verweis auf projname.vbproj; prinzipiell ist es aber möglich, innerhalb einer Projektmappe mehrere Projekte zu verwalten (die sogar mit unterschiedlichen Programmiersprachen ausgeführt werden können)
projname.suo
Ergänzung zu projname.sln, enthält benutzerspezifische Einstellungen
bin\*
das zur Ausführung geeignete Kompilat des Programms
obj\*
temporäre Dateien, die während des Kompilierens erzeugt werden
Neben den Quellcode- und Konfigurationsdateien erzeugt die Entwicklungsumgebung beim Kompilieren das ausführbare Programm. Zum Kompilieren wird das temporäre Verzeichnis obj\debug (für die Debug-Version) bzw. obj\release (für die Endversion des Programms, das an den Kunden weitergeben werden soll) verwendet. Das Endergebnis, d.h. die ausführbare Datei projname.exe sowie eventuell zusätzliche Debugging-Informationen zur Fehlersuchen (Datei projname.pdb), werden anschließend in das Verzeichnis bin kopiert. Dieser Vorgehensweise mutet vielleicht ein wenig kompliziert an, sie hat aber Vorteile bei komplexen Projekten, weil dann nur die Dateien neu kompiliert werden müssen, die sich geändert haben. Allerdings befinden sich immer mindestens zwei Kopien des ausführbaren Programms im Projektverzeichnis. Am besten ist es, wenn Sie das obj-Verzeichnis einfach ignorieren und nur das bin-Verzeichnis berücksichtigen!
1.2 Hello World (Windows-Version)
37
Abbildung 1.6: Die Quelldateien des Beispielprogramms
1.2
Hello World (Windows-Version)
Die Entwicklung eines Windows-Programms beginnt damit, dass Sie als Projekttyp WINDOWS-ANWENDUNG angeben. Die Entwicklungsumgebung erzeugt damit ein neues Projekt, in das es ein erstes (noch leeres) Formular samt dem dazugehörigen Verwaltungscode einfügt. In der Entwicklungsumgebung wird das leere Fenster sichtbar (Form1.vb).
Steuerelemente einfügen Der erste Schritt zu dem in Abbildung 1.8 dargestellten Beispielprogramm besteht darin, in das Fenster die erforderlichen Steuerelemente einzufügen. (Steuerelemente sind Bedienungs- bzw. Anzeigeelemente eines Windows-Programms, z.B. Buttons, Listenfelder etc.). Das Beispielprogramm benötigt nur drei Steuerelemente, zwei Label-Felder zur Darstellung kurzer Texte sowie einen Button, um das Programm zu beenden. Um Steuerelemente einzufügen, öffnen Sie das Toolbox-Fenster (ANSICHT|TOOLBOX). Die Toolbox zeigt nur dann Steuerelemente an, wenn im Arbeitsbereich der Entwicklungsumgebung ein Formular angezeigt wird. Wenn die Toolbox nach kurzer Zeit automatisch wieder ausgeblendet ist, fixieren Sie das Fenster vorübergehend durch einen Klick auf die Pin-Up-Nadel. Damit verkleinert sich zwar Ihr Arbeitsbereich, aber dafür können Sie komfortabel arbeiten, ohne ständig auftauchenden und wieder verschwindenden Fenstern nachzujagen. In der Toolbox klicken Sie zuerst das gewünschte Steuerelement an und zeichnen dann im Formular mit der Maus einen Rahmen, der die gewünschte Größe angibt. Die Entwicklungsumgebung fügt das Steuerelement an der Stelle des Rahmens ein.
38
1 Hello World
Abbildung 1.7: Die VB.NET-Benutzeroberfläche beim Entwurf eines Windows-Programms
Eigenschaften einstellen Der zweite Schritt besteht darin, die Eigenschaften des Formulars und der Steuerelemente einzustellen. Dazu klicken Sie das betreffende Objekt an. Im Eigenschaftsfenster (ANSICHT| EIGENSCHAFTSFENSTER) werden nun alle Eigenschaften dieses Objekts angezeigt, wobei von den Defaultwerten abweichende Einstellungen durch eine fette Schrift hervorgehoben sind. Beim Beispielprogramm Hello Windows müssen Sie nur die Text-Eigenschaften des Buttons sowie des Formulars einstellen (mit den Texten Ende bzw. Hello Windows). Alle anderen Voreinstellungen der Entwicklungsumgebung können so bleiben, wie sie sind.
Ereignisprozeduren einfügen Der Programmfluss von Windows-Programmen wird durch Ereignisse bestimmt. Ereignisse sind z.B. eine Mausbewegung, das Anklicken eines Buttons oder das Laden des Programms. Programmcode wird immer nur dann ausgeführt, wenn das Programm ein Ereig-
1.2 Hello World (Windows-Version)
39
nis feststellt und es eine dazu passende Ereignisprozedur gibt. (Anders als bei Konsolenanwendungen gibt es keine Main-Prozedur.) Hello Windows soll auf zwei Ereignisse reagieren: Beim Laden des Programms soll es in den
beiden Labelfeldern den Benutzernamen und das aktuelle Datum anzeigen. Beim Anklicken von ENDE soll das Programm beendet werden. Um die zwei entsprechenden Ereignisprozeduren einzufügen, führen Sie zuerst einen Doppelklick auf den Button aus. Damit fügt die Entwicklungsumgebung selbstständig eine noch leere Button1_Click-Prozedur in den Code ein und wechselt in das Codefenster. Dort geben Sie in dieser Prozedur als einzige Anweisung Close an. (Damit wird das Fenster geschlossen und das Programm beendet.) Wechseln Sie zurück in das Formularfenster und führen Sie nun einen Doppelklick im Innenbereich des Formulars aus. Die Entwicklungsumgebung fügt damit eine leere Form1_Load-Prozedur ein, in die Sie die beiden folgenden Zeilen einfügen: Label1.Text = "Benutzer: " + Environment.UserName Label2.Text = "Datum: " + Now.ToLongDateString()
HINWEIS
Damit erreichen Sie, dass beim Programmstart die beiden Label-Texte mit dem Benutzernamen und dem Datum initialisiert werden. Einzelne Objekte eines Windows-Programms kennen normalerweise mehrere Ereignisse. Load und Click waren die Defaultereignisse des Formulars bzw. des Buttons und konnten deswegen komfortabel per Doppelklick in den Code eingefügt werden. Bei allen anderen Ereignissen müssen Sie zum Einfügen im Codefenster zuerst im linken Listenfeld das Objekt (Steuerelement) auswählen und dann im rechten Listenfeld das gewünschte Ereignis.
Programmcode Der gesamte Programmcode für das Hello-Windows-Projekt besteht aus weit mehr als den beiden Ereignisprozeduren, die Sie selbst eingegeben haben. Die folgenden Zeilen zeigen die Struktur des Codes. Dabei sehen Sie, dass sich der gesamte Code innerhalb einer Klasse befindet, die das Fenster gewissermaßen beschreibt. Der größte Teil des Codes ist allerdings normalerweise gar nicht sichtbar. Es handelt sich dabei um den Block Vom Windows Form Designer generierter Code. Dieser Block kann in der Entwicklungsumgebung durch Anklicken des +-Zeichens auseinander geklappt werden, wenn Sie sich für die Details interessieren. Der Code enthält Anweisungen, um das Fenster mit seinen Steuerelementen und allen im Eigenschaftsfenster getätigten Einstellungen zu erzeugen und bei Programmende wieder zu schließen. Bei einem noch leeren WindowsProjekt ist der Codeblock ca. 35 Zeilen lang; er wächst, je mehr Steuerelemente Sie in das Fenster einfügen und je mehr Eigenschaften Sie einstellen. Im Regelfall können Sie diesen Codeblock einfach ignorieren. Er dient einfach dazu, eine Verbindung zwischen der internen Programmlogik und der nach außen hin sichtbaren Entwicklungsumgebung (dem so genannten Windows Form Designer) herzustellen. Auf
40
1 Hello World
keinen Fall sollten Sie den Code ändern, solange Sie nicht genau verstehen, wofür die einzelnen Anweisungen verantwortlich sind. ' Beispiel hello-world\ Option Strict On Public Class Form1 Inherits System.Windows.Forms.Form [ ... Vom Windows Form Designer generierter Code ... ] Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Label1.Text = "Benutzer: " + Environment.UserName Label2.Text = "Datum: " + Now.ToLongDateString() End Sub
VERWEIS
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Close() End Sub End Class
Eine ausführlichere Einführung in die Windows-Programmierung folgt in Kapitel 13. Im Mittelpunkt des Kapitels steht ein etwas komplexeres Beispielprogramm. Eine detaillierte Beschreibung des Codeblocks Vom Windows Form Designer generierter Code finden Sie in Abschnitt 15.2.2.
Programm ausführen Wie bei einer Konsolenanwendung kann das fertige Programm mit STARTEN ausgeführt werden. Sie sehen das Ergebnis Ihrer Mühe in Abbildung 1.8. (Auf der beiliegenden CDROM finden Sie das gesamte Beispielprojekt im Verzeichnis hello-world\hello-windows.)
Abbildung 1.8: Das Programm Hello Windows
1.3 Bedienung der Entwicklungsumgebung
1.3
41
Bedienung der Entwicklungsumgebung
Der Platz reicht nicht aus, um hier alle Bedienungsdetails der VB.NET- bzw. VS.NET-Entwicklungsumgebung zu beschreiben. Und selbst wenn es mehr Platz gäbe, könnte dieser sicherlich besser genutzt werden, statt auf vielen Seiten die Menüs, Symbolleisten und Fenster der Entwicklungsumgebung aufzuzählen: Im Großen und Ganzen ist die Entwicklungsumgebung intuitiv zu bedienen, und Sie werden nach wenigen Tagen auch ohne langatmige Erklärungen damit zurecht kommen.
VERWEIS
Daher beschränkt sich dieser Abschnitt auf die Konfigurationsmöglichkeiten sowie auf einige Aspekte, die nicht sofort ins Auge springen bzw. bei denen es wesentliche Unterschiede im Vergleich zu VB6 gibt. Einzelne Aspekte der Entwicklungsumgebung werden in den Kapiteln behandelt, in die sie thematisch passen, der Objektbrowser also beispielsweise in Kapitel 6, das den Umgang mit .NET-Bibliotheken beschreibt. Objektbrowser: Abschnitt 6.3 Debugging-Elemente (Überwachungsfenster, Threadfenster etc.): Abschnitt 11.2 Fenster zur Bearbeitung von Setup-Projekten: Kapitel 18
1.3.1
Layout der Entwicklungsumgebung
Die Entwicklungsumgebung setzt sich aus Dutzenden von Fenstern zusammen, von denen die meisten an einem der vier Ränder des Hauptfensters angedockt sind. Die eigentlichen Dokumentenfenster (Programmcode, Formulare etc.) werden in dem verbleibenden Innenbereich angezeigt (siehe z.B. Abbildung 1.7). Das ermöglicht nur dann ein komfortables Arbeiten, wenn Sie einen ausreichend großen Monitor besitzen. Wenn Sie mit dem Standardlayout nicht zufrieden sind, können Sie die Position, Größe und das Docking-Verhalten fast aller Fenster selbst konfigurieren. Meine Experimente endeten allerdings immer wieder damit, dass ich reumütig zum Defaultlayout zurückgekehrt bin (EXTRAS|OPTIONEN|UMGEBUNG|FENSTERLAYOUT ZURÜCKSETZEN). So groß die Konfigurationsvielfalt in der Theorie ist, so vielfältig sind leider auch die Probleme in der Praxis, sobald man vom Defaultlayout abweicht. Besonders oft hatte ich das Problem, dass sich einzelne Fenster nicht mehr öffnen ließen (z.B. das EIGENSCHAFTS-Fenster oder das THREADING-Fenster zur Fehlersuche). Auch der Versuch, die Dokumentenfenster als MDI-Fenster anzuzeigen (EXTRAS|OPTIONEN|UMGEBUNG|MDI-UMGEBUNG), ist gescheitert: Die ganze Entwicklungsumgebung ist darauf nicht ausgerichtet, merkt sich keine Fenstergrößen, platziert mehr oder weniger willkürlich andere Fenster in den Vordergrund etc. Fazit: Man kann mit der Entwicklungsumgebung in der Defaultumstellung gut arbeiten. Die Umgebung ist aber (noch) zu wenig ausgereift, um auch andere Layouts ordentlich zu
42
1 Hello World
unterstützen. Sie sparen Zeit und Nerven, wenn Sie das Layout so lassen, wie es ist, und sich mit den kleinen Unzulänglichkeiten abfinden.
1.3.2
Menüs und Symbolleisten
Wie ein Menü zu bedienen ist, brauche ich an dieser Stelle sicher nicht zu erklären. Ebenso ist eine Beschreibung der zahlreichen Menükommandos hier nicht sinnvoll. Dennoch erscheint es angebracht, hier einige nicht ganz selbstverständliche Hinweise zusammenzufassen: •
Das Menü ist kontextabhängig! Viele Menükommandos stehen nur dann zur Verfügung, wenn gerade ein bestimmtes Fenster oder ein bestimmter Fensterbereich der Entwicklungsumgebung gerade aktiv ist. Zum Teil muss in diesem Fenster(bereich) sogar noch ein ganz bestimmtes Element aktiviert sein! Ein besonders frappierendes Beispiel ist das häufig benötigte Kommando PROJEKT|EIGENSCHAFTEN, mit dem globale Eigenschaften des Projekts verändert werden können. Dieses Kommando ist nur zugänglich, wenn das Fenster PROJEKTMAPPEN-EXPLORER aktiv ist und darin ein Projekt ausgewählt ist. Zum Teil sind diese kontextabhängigen Menüeinträge ganz praktisch, weil Sie vermeiden, dass das Menü noch unübersichtlicher ist als dies ohnedies schon der Fall ist. Manchmal wirkt die Entwicklungsumgebung aber auch schlicht unlogisch. Fazit: Senden Sie mir bitte keine E-Mails der Form: Lieber Herr Kofler, auf Seite 345 schreiben Sie, man könne mit Projekt|Eigenschaften die Default-Importe verändern. Bei mir gibt es diesen Menüeintrag aber nicht!
•
Auch die Symbolleisten und andere Elemente der Entwicklungsumgebung sind kontextabhängig: Was für das Menü gilt, gilt auch für andere Elemente der Entwicklungsumgebung. Symbolleisten tauchen auf und verschwinden, je nachdem, was Sie gerade tun bzw. welches Fenster aktiv ist. In der Toolbox sind nur dann Steuerelemente zu finden, wenn gerade ein Formularfenster geöffnet ist. Die Debugging-Fenster stehen erst zur Verfügung, wenn Sie tatsächlich ein Programm ausführen etc.
•
Menüs und Symbolleisten können verändert werden: Mit EXTRAS|ANPASSEN|SYMBOLLEISTE können Sie das Hauptmenü und die Symbolleisten ein- und ausschalten. (Das Hauptmenü gilt intern ebenfalls als Symbolleiste, kann aber nicht ausgeschaltet werden.) Solange der Dialog aktiv ist, können Sie Menükommandos und Buttons zwischen den Symbolleisten verschieben bzw. kopieren (Strg-Taste). Eine schier endlose Liste aller zur Verfügung stehenden Kommandos finden Sie im Dialogblatt BEFEHLE. (Übrigens ist die Vorgehensweise bei der Veränderung von Menüs und Symbolleisten exakt dieselbe wie beim Office-Paket.)
1.3 Bedienung der Entwicklungsumgebung
1.3.3
43
Tastenkürzel
Die Benutzeroberfläche kann durch unzählige Tastenkürzel sehr effizient bedient werden – wenn man die Kürzel einmal erlernt hat. Bevor Sie sich damit auseinander setzen, sollten Sie sich zuerst für eine der möglichen Defaultbelegungen entscheiden. Zur Auswahl stehen mehrere Schemas: Standardeinstellung, Visual Basic 6, VS6, VC++2 oder VC++6. Das Schema kann beim ersten Start der Entwicklungsumgebung sowie im Dialogblatt EXTRAS|OPTIONEN|UMGEBUNG|TASTATUR ausgewählt werden. Darüber hinaus kann jedem der zahllosen Kommandos der Benutzeroberfläche ein eigenes Tastenkürzel zugewiesen werden. Die so veränderte Tastaturbelegung wird dann in einem eigenen Tastaturschema gespeichert. Alle Einstellungen erfolgen im gerade erwähnten Dialogblatt (siehe Abbildung 1.9).
Abbildung 1.9: Tastaturkonfiguration für die Entwicklungsumgebung
1.3.4
Online-Hilfe
In die Online-Hilfe gelangen Sie am einfachsten mit F1. Nach Möglichkeit führt F1 kontextabhängig zum richtigen Thema. Insbesondere sollte F1 zur Beschreibung der Klasse, Methode, Eigenschaft etc. führen, an der sich der Cursor im Codefenster gerade befindet. Wenn das nicht klappt (was manchmal vorkommt), sollten Sie in den Objektbrowser wechseln (ANSICHT|ANDERE FENSTER), dort das Schlüsselwort suchen und die Hilfe vom Objektbrowser aus öffnen.
TIPP
44
1 Hello World
Falls Sie die Visual-Basic-Tastenkürzel verwenden, gelangen Sie mit Shift+F2 besonders rasch in den Objektbrowser bzw. zur Definition eines Schlüsselworts im Code. Diese Tastenkombination sucht im Objektbrowser das Schlüsselwort, über dem sich der Cursor gerade befindet. Esc führt zurück ins Codefenster.
Neben diesem herkömmlichen Hilfesystem bietet die Entwicklungsumgebung auch ein dynamisches Hilfesystem. Wenn Sie dieses Hilfefenster anzeigen (HILFE|DYNAMISCHE HILFE), versucht die Entwicklungsumgebung passend zur aktuellen Arbeit die richtigen Hilfethemen anzuzeigen. Während der ersten ein bis zwei Wochen mit VB.NET ist das ganz praktisch, danach beginnt es zu nerven und der Neuigkeitswert sinkt gegen null. Der Umfang der Hilfe ist wirklich phantastisch. Es gibt fast kein Thema, das dort nicht behandelt wird. Allerdings ist es manchmal recht schwierig, zur richtigen Seite zu gelangen. Machen Sie sich also mit den Suchmöglichkeiten vertraut – es lohnt sich. Meist sieht der Suchvorgang bei mir so aus:
TIPP
• Die Suche beginnt mit ANSICHT|NAVIGATION|SUCHEN, wo ich ein bis drei möglichst konkrete Stichwörter eingebe. • Dann sortiere ich die Suchergebnisse nach dem SPEICHERORT, um auf diese Weise möglichst rasch die interessanten von den uninteressanten Ergebnissen zu trennen. • Wenn ich einen einigermaßen hilfreichen Eintrag gefunden habe, verwende ich den Button INHALT SYNCHRONISIEREN (ein blauer Doppelpfeil) in der Symbolleiste des Hilfefensters. Damit sehe ich, wo in der verzweigen Hierarchie der Hilfethemen ich mich befinde. Oft sind auch die benachbarten Seiten hilfreich.
1.3.5
Codeeingabe
Codeeinrückung
TIPP
Die Einrückung von Code erfolgt in der Regel automatisch. (Die einzige Ausnahme sind mehrzeilige Anweisungen, deren Zeilenenden durch das Zeichen _ gekennzeichnet sind.) Die Parameter für die automatische Einrückung können mit EXTRAS|OPTIONEN|TEXTEDITOR|BASIC|TABSTOPPS eingestellt werden. (Beispielsweise verwendet der gesamte in diesem Buch abgedruckte Code eine Einrücktiefe von nur zwei Zeichen statt der Defaulteinstellung von vier Zeichen.) Wenn der Editor bei Änderungen der Codestruktur mit der automatischen Einrückung durcheinander kommt, markieren Sie einfach die ganze Codepassage und drücken Tab. Damit wird der gesamte Bereich neu (und korrekt) eingerückt.
1.3 Bedienung der Entwicklungsumgebung
45
#-Kommandos (Direktiven) #-Kommandos beginnen mit dem Zeichen #. Es handelt sich dabei nicht um VB.NETKommandos, sondern um Kommandos, die vom Compiler bzw. vom Editor ausgewertet werden. Sie dienen beispielsweise dazu, manche Codeteile je nach Abhängigkeit einer Konstante zu berücksichtigen oder Codeteile zu einer Gruppe zusammenzufassen, die vollständig zusammengeklappt werden kann. #-Kommandos #Region "name" ... code #End Region
gruppiert Code zu einer Region, die zusammengeklappt werden kann. So definierte Regionen werden beim Laden eines Projekts automatisch zusammengeklappt. #Region wird beispielsweise dazu verwendet, den
automatisch erzeugten Code von Windows-Formularen auszublenden (Vom Windows Form Designer generierter Code). Mehr Informationen zu diesem automatisch erzeugten Code finden Sie in Abschnitt 15.2.2. #Const constname = "xy"
definiert eine Konstante. Die Konstante ist nur für andere #-Kommandos sichtbar und gilt nur für die jeweilige Codedatei.
#If ausdruck1 Then ... code1 #ElseIf ausdruck2 Then ... code2 #Else ... code3 #End If
definiert eine Verzweigung. Der Compiler berücksichtigt nur den Codeabschnitt, dessen zugeordneter Ausdruck zutrifft. In den Ausdrücken können selbst definierte Konstanten sowie die drei vordefinierten Konstanten Config, Debug und Trace ausgewertet werden.
#ExternalSource(filename, n) [error] #End External Source
stellt eine Verbindung zu einer externen Datei her (z.B. bei *.aspx-Dateien). Das Kommando ist nur für den internen Einsatz durch die Entwicklungsumgebung gedacht. n gibt eine Zeilennummer in der externen Datei an. error kann optional angeben, in welcher Zeile der externen Datei ein Fehler aufgetreten ist.
1.3.6
Debug und Trace (jeweils True/False) sowie alle weiteren
Konstanten können im Eigenschaftsdialog des Projekts eingestellt werden.
Befehlsfenster
Das Befehlsfenster kann über das Menü ANSICHT|ANDERE FENSTER geöffnet werden. Während der Fehlersuche (d.h., während ein Programm unterbrochen ist) können in diesem Fenster einfache Kommandos ausgeführt werden. Insbesondere können Sie mit ?name den Inhalt einer Variablen darstellen, mit name=123 den Wert einer Variablen ändern etc. Allerdings bietet das Befehlsfenster bei weitem nicht so viele Möglichkeiten wie das aus VB6 vertraute Direktfenster. Insbesondere kann bei ? immer nur ein Ausdruck angegeben wer-
46
1 Hello World
den. Des weiteren ist es nicht möglich, komplexe Kommandos auszuführen (etwa eine Schleife). Das Befehlsfenster kennt zwei Modi: einen so genannten unmittelbaren Modus und den Befehlsmodus. •
Unmittelbarer Modus: Dieser Modus ist per Default aktiv. Sie erkennen diesen Modus daran, dass in der Titelleiste des Befehlsfensters der Text UNMITTELBAR angezeigt wird. (Falls gerade der Befehlsmodus aktiv ist, gelangen Sie mit dem Kommando immed zurück in den unmittelbaren Modus.) Im unmittelbaren Modus können Sie einfache VB-Kommandos ausführen, selbst definierte Funktionen oder Prozeduren aufrufen sowie mit ? den Inhalt einer Variablen ausgeben. (Die aus VB6 geläufige Schreibweise ?x,y,z zur Ausgabe mehrerer Variablen ist allerdings nicht mehr möglich.)
•
Befehlsmodus: Um in den Befehlsmodus zu wechseln, führen Sie das Kommando >cmd aus. (Das Zeichen > muss ebenfalls eingegeben werden!) Sie erkennen den Befehlsmodus daran, dass von nun an jeder Eingabezeile das Zeichen > vorangestellt wird. Im Befehlsmodus können Sie Kommandos zur Steuerung der Entwicklungsumgebung ausführen. Beispielsweise öffnet das Kommando Datei.NeueDatei den Dialog, um dem Projekt eine neue Datei hinzuzufügen. (Die Kommandos müssen in der Sprache der Entwicklungsumgebung eingegeben werden. Die zur Auswahl stehenden Kommandos finden Sie mit EXTRAS|OPTIONEN im Dialogblatt UMGEBUNG|TASTATUR.) Neben den ausgeschriebenen Kommandos gibt es einige vordefinierte Abkürzungen für besonders wichtige Kommandos. Beispielsweise setzt bp in der gerade aktuellen Zeile des Codefensters einen Haltepunkt (break point) bzw. entfernt ihn wieder. Eine Liste der zur Auswahl stehenden Abkürzungen finden Sie in der Hilfe (suchen Sie nach vordefinierte Befehls-Aliase). Sie können Befehle zur Steuerung der Entwicklungsumgebung übrigens auch im unmittelbaren Modus eingeben, wenn Sie das Zeichen > voranstellen.
TIPP
Jetzt bleibt nur noch die Frage offen, was dieser neue Befehlsmodus eigentlich bringt: Meiner Ansicht nach nicht viel. Die meisten Kommandos sind über das Menü einfacher zugänglich (und auch das Menü kann per Tastatur gut bedient werden). Der Befehlsmodus ermöglicht zwar auch die Ausführung von Kommandos, die im Menü nicht enthalten sind – aber das ist eine eher exotische Anwendung für echte Freaks. (Wer ein im Menü fehlendes Kommando häufig per Tastatur ausführen will, kann das Kommando ja ohne weiteres in das Menü einbauen: ANSICHT|SYMBOLLEISTEN|ANPASSEN.) Wenn Sie im Befehlsfenster VB-Kommandos eingeben möchten, aber jede Eingabe mit Der Befehl xy ist ungültig quittiert wird, sind Sie wahrscheinlich versehentlich in den Befehlsmodus geraten. Führen Sie zum Wechsel in den unmittelbaren Modus das Kommando immed aus!
1.3 Bedienung der Entwicklungsumgebung
1.3.7
47
Defaulteinstellungen für neue Projekte
Im Verzeichnis Programme\Microsoft Visual Studio .NET\Vb7\VBWizards gibt es für jeden Projekttyp (z.B. Windows-Anwendungen, Konsolenanwendungen etc.) ein eigenes Verzeichnis. Das Unterverzeichnis Template enthält wiederum einige Dateien mit diversen Defaulteinstellungen, die Sie verändern können (erstellen Sie vorher ein Backup!). Wenn Sie beispielsweise die Defaulteinstellungen für Windows-Anwendungen verändern möchten, werfen Sie einen Blick in das folgende Verzeichnis. (Wenn Sie mit der englischen VS.NETVersion arbeiten, lautet die Sprachnummer 1033.) Programme\Microsoft Visual Studio .NET\Vb7\VBWizards\WindowsApplication\Templates\1031
Das Verzeichnis enthält drei Dateien: •
Form.vb enthält eine Codeschablone für das Formular des neuen Programms.
•
AssemblyInfo.vb enthält einige Felder betreffend Copyright, Firmennamen etc. Wenn Sie wollen, dass bei jedem neuen Projekt Ihr Name (oder Ihr Firmenname) eingetragen wird, verändern Sie diese Felder.
•
WindowsApplication.vbproj enthält schließlich einige allgemeine Defaulteinstellungen. (Diese Einstellungen finden Sie in der Entwicklungsumgebung im Dialog PROJEKT|EIGENSCHAFTEN. Damit dieser Menüpunkt zur Auswahl steht, müssen Sie im PROJEKTMAPPEN-EXPLORER den Namen Ihres Projekts anklicken.)
Wenn Sie beispielsweise möchten, dass für alle neuen Windows-Anwendungen die Option Option Strict On gilt (Hintergrundinformationen zu dieser nützlichen Option finden Sie in Abschnitt 4.1.4), dann laden Sie die Datei WindowsApplication.vbproj in einen Texteditor und fügen die fett markierte Zeile ein. (Lassen Sie die anderen Einstellungen unverändert!)
HINWEIS
<Build> <Settings ... OptionStrict = "On" >
Die obige Einstellung gilt nur für Windows-Anwendungen (nicht aber für die zahlreichen anderen VB.NET-Projekttypen)! Wenn Option Strict On beispielsweise auch für Konsolenanwendungen gelten soll, müssen Sie auch deren Schablone ändern (Verzeichnis ConsoleApplication). Es scheint leider keine Möglichkeit zu geben, Option Strict global für alle Projekttypen zu aktivieren.
2
Das .NET-Universum
VB.NET basiert auf dem .NET-Framework, also den neuen .NET-Klassenbibliotheken und den dazugehörenden Werkzeugen. Viele .NET-Details und -Interna werden bei der VB.NET-Programmierung von der Entwicklungsumgebung verborgen, dennoch ist es natürlich zweckmäßig, wenn Sie zumindest eine ungefähre Vorstellung haben, was hinter den Kulissen vor sich geht. Dieses Kapitel will einen Einstieg in das .NET-Universum geben. Es beschreibt die Hintergründe, die zur Entwicklung von .NET führten, stellt einige zugrunde liegende Techniken vor, gibt einen Überblick über die zur Auswahl stehenden Entwicklungswerkzeuge etc. Bevor die weiteren Kapiteln die Details der VB.NET-Programmierung erläutern, geht es hier um den Blick aufs Ganze. 2.1 2.2 2.3 2.4 2.5 2.6 2.7
Wozu .NET? Das .NET-Framework Architektur Sicherheitsmechanismen .NET und das Internet Programmiersprachen (C# versus VB.NET) Entwicklungsumgebungen
50 53 55 61 65 67 69
50
2 Das .NET-Universum
2.1
Wozu .NET?
Vor .NET beruhte die Programmentwicklung unter Windows primär auf der für C- und C++-Programmierer konzipierten MFC (Microsoft Foundation Class Library) sowie auf dem für C++ und VB6 gedachten COM (Component Object Model). Beide Konzepte waren bei ihrer Einführung modern und stellten einen Fortschritt im Vergleich zu vorhandenen anderen Technologien dar. Im Laufe der Zeit tauchten aber eine Reihe von Problemen auf, die das Leben für Programmierer zunehmend mühsam machten: •
DLL-Hell: COM-Bibliotheken werden bei der Installation eines Programms in Form von DLL-Dateien in das Windows-Systemverzeichnis kopiert. Falls sich dort bereits ältere Versionen derselben Bibliothek befanden, wurden diese überschrieben. Allerdings waren die neuen Bibliotheken zum Teil in (winzigen) Details inkompatibel, so dass nach der Installation eines neuen Programms oft ältere Programme, die sich auf ältere Versionen einer COM-Bibliothek verließen, nicht mehr liefen. Dieses Problem wird mit dem einprägsamen Begriff DLL-Hell bezeichnet.
•
Speicherverwaltung: Bei COM ist der Programmierer dafür verantwortlich, nicht mehr benötigte Objekte explizit freizugeben. Bei einzelnen Objekten ist das nicht weiter schwierig. Wenn aber mehrere Objekte (womöglich zirkulär) aufeinander verweisen, kann es schlicht unmöglich sein, diese Objekte aus dem Speicher zu entfernen. Daher verbrauchten viele Programme aufgrund nicht freigegebener Objekte zunehmend mehr Speicher, je länger sie liefen.
•
Sicherheit: Bei herkömmlichen *.exe-Dateien gilt das Motto: Alles oder nichts. Sobald ein Programm läuft, hat es uneingeschränkten Zugriff auf alle Betriebssystemfunktionen. (Bei Windows NT/2000/XP ist der Zugriff durch die Datei- und Systemzugriffsrechte etwas limitiert, aber das hilft auch nichts, wenn ein Programm von Administrator gestartet wird). Daraus ergeben sich natürlich massive Sicherheitsprobleme.
•
Konsistenz, Objektorientierung: Das COM-Konzept ist mittlerweile etwa zehn Jahre alt, und viele Bibliotheken tragen in sich auch den Ballast der letzten zehn Jahre. Das führt dazu, dass die Bibliotheken in sich und im Vergleich mit anderen Bibliotheken vollkommen inkonsistent sind. Vergleichbare Operationen werden in unterschiedlichen Bibliotheken auf unterschiedliche Weise durchgeführt. Der Einarbeitungsaufwand für den Programmierer ist dementsprechend groß. Außerdem haben sich in den vergangenen zehn Jahren die Anforderungen an eine moderne, objektorientierte Klassenbibliothek stark gewandelt. COM-Bibliotheken entsprechen diesen Anforderungen meist nicht mehr.
Diese Probleme sind so grundlegend, dass eine Korrektur durch ein Update oder Bugfix unmöglich ist. Die Wartung des auf COM basierenden Codes stellte sich als zunehmend schwierig bis unmöglich dar. Aus diesen (und vielen anderen Gründen) hat sich Microsoft für eine Neuentwicklung der gesamten Infrastruktur für Programmentwickler entschlossen. Diese Infrastruktur umfasst neue Klassenbibliotheken, neue Mechanismen zum Austausch von Objekten zwischen verschiedenen Anwendungen, neue Compiler, einen neuen Zwischencode für ausführbare
2.1 Wozu .NET?
51
Programme, eine neue Entwicklungsumgebung, neue Programmiersprachen und zu guter Letzt eine neue Dokumentation. Dieses Mammutprojekt, das unter dem Kürzel .NET präsentiert wurde und wird, nahm mehrere Jahre in Anspruch.
Vorteile von .NET .NET verspricht, alle oben genannten Probleme zu lösen. Die DLL-Hell wird durch eine intelligentere Installation von Bibliotheken vermieden, die auch eine Parallelinstallation unterschiedlicher Versionen erlaubt, ohne dass diese sich im Weg sind. (Am einfachsten geht das dadurch, dass Bibliotheken in das Programmverzeichnis installiert werden.) Generell ist die Weitergabe von .NET-Programmen viel einfacher als die von COM-Programmen. Es ist nicht mehr notwendig, Bibliotheken in die Registrierdatenbank einzutragen. In vielen Fällen reicht es, die *.exe- und *.dll-Dateien einfach zu kopieren. (Vorausgesetzt wird dabei natürlich, dass am Zielrechner das .NET-Framework bereits installiert ist.) Das Problem der Speicherverwaltung wird dadurch gelöst, dass sich nicht mehr der Programmierer um die Freigabe von Objekten kümmert, sondern eine im Hintergrund laufende garbage collection. Das ist ein Prozess, der nicht mehr benutzte Objekte erkennt und aus dem Speicher entfernt. Die Sicherheitsprobleme werden dadurch gemindert, dass bei der gesamten Neukonzeption der .NET-Bibliotheken verschiedene Sicherheitsebenen beachtet wurden. Bestimmte Operationen dürfen nur dann ausgeführt werden, wenn die .NET-Sicherheitseinstellungen dies erlauben. Die Einstellung der Rechte, die .NET-Programme haben dürfen, erfolgt ähnlich wie beim Internet Explorer. Das erste Kriterium ist die Herkunft eines Programms. Aus dem Internet geladene Programme haben also weniger Rechte als von der lokalen Festplatte gestartete Programme. (Ob die neuen Sicherheitsmechanismen halten, was Microsoft zurzeit verspricht, muss sich natürlich erst erweisen!) Die neuen Klassenbibliotheken bieten mehr Konsistenz und deutlich bessere Möglichkeiten zur Lösung vieler (alltäglicher) Probleme. (Dass das Einlesen in die neuen Bibliotheken oft genauso lange dauert wie die umständliche Lösung eines bestimmten Problems mit alten Technologien, steht auf einem anderen Blatt.) Ein weiterer Vorteil von .NET besteht darin, dass das System weitgehend sprachunabhängig ist. Microsoft unterstützt neben den Sprachen C# und VB.NET auch C++ und J# (eine Java-ähnliche Programmiersprache). Drittanbieter unterstützen eine Reihe weiterer Programmiersprachen. Das .NET-Framework stellt durch vorgegebene Datentypen sicher, dass Komponenten problemlos zusammenarbeiten und Daten austauschen können, egal, mit welcher Sprache sie entwickelt wurden. Auch die Anforderungen zur Ausführung von .NET-Programmen sind unabhängig davon, mit welcher Sprache sie erstellt wurden. (Es kommen also immer dieselben Bibliotheken zum Einsatz.) Java-Programmierer werden beim Einlesen in die .NET-Dokumentation und speziell bei der Anwendung der Programmiersprache von C# vertraute Merkmale feststellen. Vieles, was .NET jetzt bietet, ist also gar nicht so neu, sondern stand Java-Programmierern in ähnlicher Form schon länger zur Verfügung. .NET ist sicherlich nicht einfach eine Kopie von
52
2 Das .NET-Universum
Java, aber es ist durchaus ein weiterer Beweis für die Fähigkeit Microsofts, gute Ideen der Konkurrenz aufzugreifen und zu adaptieren.
Nachteile von .NET Natürlich hat Microsoft schon bisher bei der Einführung jeder neuen Technologie das Blaue vom Himmel versprochen. Das hat sich auch mit .NET nicht geändert. So gut die Ideen sind, die hinter .NET stehen, so sehr ist doch eine gewisse Skepsis, ein gewisser Realismus angebracht! (Wenn man hin und wieder eine von Microsoft organisierte Entwicklerkonferenz besucht, kann dieser Realitätssinn ja leicht verloren gehen ...) Was momentan gegen .NET spricht, ist der Umstand, dass die .NET-Klassenbibliothek einfach noch zu neu ist. Diese Bibliothek umfasst Millionen Zeilen von Code. Trotz des mehrjährigen Betatests wäre es naiv zu glauben, dass dieser Code fehlerfrei ist. Zudem ist die Dokumentation vieler Funktionen – vor allem solcher, die sich etwas tiefer in der .NETBibliothek verstecken – noch ziemlich lückenhaft. (Es ist wohl jedes Schlüsselwort irgendwie beschrieben, aber oft bleiben der Kontext bzw. die vorgehesehene Technik für die konkrete Anwendung schleierhaft.) Ein weiteres Argument gegen .NET ist dessen Windows-Abhängigkeit. Zwar gibt es Versprechungen, zumindest Teile der .NET-Klassenbibliothek auch unter Unix/Linux zur Verfügung zu stellen, und zum Teil auch von Microsoft unabhängige Open-Source-Projekte, die in diese Richtung gehen; aber wieweit diese Projekte erfolgreich sein werden, bleibt abzuwarten. Ich wage zu bezweifeln, dass wirklich eine Kompatibilität erreicht wird, die so weit geht, dass ein unter Windows entwickeltes Programm ohne Änderungen auch unter Linux läuft. (Vor Jahren gab es auch Versprechungen, den Internet Explorer, die ActiveX-Technologie etc. auf Unix/Linux zu portieren. Auch wenn der gute Wille durchaus da war und nun eine alte IE-Version tatsächlich für einige Unix-Derivate zur Verfügung steht, blieben diese Portierungsversuche in einem sehr unvollkommenen Stadium stecken und waren letztlich nicht erfolgreich.) Ein riesiges Problem im Zusammenhang mit .NET ist die Wartung alten Codes. Die Durchführung eines neuen Projekts mit .NET mag eine Herausforderung sein, was die Einarbeitung in die neuen Konzepte betrifft, wirft aber ansonsten nicht mehr Probleme auf als sonst üblich. .NET ist aber grundsätzlich ungeeignet, vorhandenen COM-Code weiterzuentwickeln. Um die zum Teil riesigen VB6-Projekte zu warten, die in den letzten Jahren entstanden sind, müssen Sie weiter VB6 verwenden! .NET enthält zwar hochkomplexe Kompatibilitätsmechanismen zwischen COM und .NET (Schlagwort interoperabilty), aber sobald Sie in einem .NET-Programm auf COM zurückgreifen, verlieren Sie den Großteil der Vorteile von .NET. Es ist Microsoft somit nicht gelungen, einen plausible Migrationsweg anzubieten. Problematisch ist die Weitergabe von .NET-Programmen. Damit diese ausgeführt werden können, muss am Rechner des Kunden das .NET-Framework installiert werden. Das dafür erforderliche Installationsprogramm ist mehr als 20 MByte groß; auf der Festplatte beansprucht das .NET-Framework sogar deutlich mehr Platz. Windows 95 wird gar nicht mehr unterstützt, Windows NT4 nur, wenn alle aktuellen Service-Packs installiert sind etc.
2.2 Das .NET-Framework
53
Unklar ist schließlich die Situation für Programmierer, die basierend auf .NET InternetAnwendungen entwickeln möchten (ASP.NET, Web-Services). Das Problem stellt hier weniger die Technologie an sich dar, sondern die Tatsache, dass es noch sehr wenige Internet-Service-Provider gibt, die .NET unterstützen. Wenn Sie den Webserver also nicht selbst verwalten (was nur bei großen Firmen sinnvoll ist) und daher auf einen externen Server angewiesen sind, müssen Sie zuerst einmal einen Internet-Service-Provider finden, dem Sie so weit vertrauen, dass dieser die zugrunde liegende Technik und die Wartung der Server (hinsichtlich Sicherheits-Updates) wirklich im Griff hat. .NET ist ja nicht nur für Programmierer Neuland, sondern auch für alle, die darauf basierende Dienste anbieten. Lassen Sie sich von diesen Nachteilen aber nicht allzusehr abschrecken: Viele Probleme sind nicht grundlegender, sondern zeitlicher Natur, und werden sich in den nächsten ein bis zwei Jahren gewissermaßen von selbst lösen. .NET wird ausreifen (das erste Service Pack gab es schon, bevor dieses Buch fertig war), das .NET-Framework wird auf immer mehr Rechnern standardmäßig installiert sein, Internet-Service-Provider und andere Software-Firmen werden sich auf .NET einstellen etc. Ich erwarte, dass .NET das Fundament für die Software-Entwicklung unter Windows für die kommenden Jahre sein wird, und ich habe den Eindruck, dass es ein gutes Fundament ist. Aber spätestens in zehn Jahren wird sicher wieder alles neu gemacht. Dann wird Microsoft plötzlich die Nachteile von .NET in die Welt posaunen, um so die Vorteile des nächsten Technologieschritts zu vermarkten. Schon jetzt ist vorauszusehen, dass sich die Slogans bis zum Verwechseln denen ähneln werden, die jetzt .NET bewerben ...
2.2
Das .NET-Framework
Der Begriff .NET-Framework bezeichnet die Summe aller Bibliotheken und Komponenten, die erforderlich sind, um .NET-Programme auszuführen. Zum .NET-Framework gehört unter anderem ein so genannter Just-in-time-Compiler, der MSIL-Code in Maschinencode übersetzt (Details folgen im nächsten Abschnitt), sowie diverse Administrationswerkzeuge, mit denen beispielsweise die .NET-Sicherheitseinstellungen verändert werden können. Das .NET-Framework SDK (Software Development Kit) enthält zusätzlich zum .NET-Framework alle Entwicklerwerkzeuge, um VB.NET- oder C#-Programme zu entwickeln, eine Menge Beispiele sowie eine umfassende Dokumentation zum .NET-Framework. Das .NET-Framework ist (mit oder ohne SDK) kostenlos bei http://www.microsoft.com erhältlich und befindet sich auch auf der CD-ROM zum Buch. Wenn Sie nun aber glauben, dass Sie alles kostenlos bekommen, irren Sie (natürlich): Die VB.NET- bzw. VS.NET-Entwicklungsumgebung ist nicht Bestandteil des SDKs! Die im SDK enthaltenen Werkzeuge sind durchwegs Kommandozeilentools, die mühsam anzuwenden sind. Wenn Sie VB.NET-Programme effizient und komfortabel entwickeln möchten, einen Designer zum Zusammenstellen von Formularen oder einen Debugger bei der Fehlersuche einsetzen möchten etc., benötigen Sie eine Version der Entwicklungsumgebung (siehe Abschnitt 2.7).
54
2 Das .NET-Universum
Überblick über die wichtigsten .NET-Bibliotheken Wichige Klassenbibliotheken des .NET-Frameworks mscorlib.dll System.Array, System.Collections.*, System.Console, System.Environment, System.IO.*, System.Math, System.Random, System.Math, System.Runtime.InteropServices.*, System.Runtime.Serialization.*, System.Security.*, System.Text.*, System.Threading.*, System.Type
Das ist die wohl wichtigste Bibliothek. Sie enthält unter anderem alle Basisdatentypen (Double, String) sowie Methoden für grundlegende Operationen (Dateizugriff, Verwaltung von Feldern, Multithreading etc.). Jedes .NET-Programm nutzt automatisch diese Bibliothek.
System.dll System.IO.*, System.Net.*, System.Net.Sockets.*, System.Security.Permissions.*, System.Text.RegularExpressions.*, System.Timers.*
Die Bibliothek enthält weitere Basisklassen, z.B. zur Nutzung verschiedener Netzwerkprotokolle.
System.Data.dll System.Data.*, System.Data.OleDb.*, System.Xml.*
Die Bibliothek enthält Klassen zur Datenbankprogrammierung (ADO.NET).
System.Drawing.dll System.Drawing.*, System.Drawing.Imaging.*, System.Drawing.Printing.*, System.Drawing.Text.*
Die Bibliothek enthält Klassen zur Grafikprogrammierung und zum Drucken (GDI+).
System.Management.dll System.Management.*
Die Bibliothek gibt Zugriff auf die Funktionen der Windows Management Instrumentation (WMI).
System.Web.dll System.Web.*, System.Web.Mail.*, System.Web.Security.*, System.Web.SessionState*, System.Web.UI.*, System.Web.UI.HtmlControls.*, System.Web.UI.WebControls.*
Die Bibliothek enthält Klassen zur Programmierung von InternetAnwendungen (ADO.NET).
System.Windows.Forms.dll System.Windows.Forms.*
Die Bibliothek enthält Klassen zur Windows-Programmierung.
System.Xml.dll System.Xml.*, System.Xml.Schema.*, System.Xml.Serialization.*, System.Xml.XPath.*, System.Xml.Xsl.*
Die Bibliothek enthält Klassen zur Bearbeitung von XML-Daten.
Microsoft.VisualBasic.dll Microsoft.VisualBasic.*
Die Bibliothek enthält Klassen, die eine Grundkompatibilität zu VB6 herstellen.
2.3 Architektur
55
Die Tabelle auf der Vorseite gibt eine kurze (und natürlich unvollständige!) Inhaltsangabe für die wichtigsten .NET-Bibliotheken. Bei jeder Bibliothek werden jeweils ihr Name sowie die wichtigsten darin vorkommenden Klassennamen angegeben. (* bedeutet, dass es mehrere Klassen gibt, die mit der gleichen Namenskombination beginnt.) Die DLL-Dateien der Bibliotheken befinden sich im Verzeichnis Windows\Microsoft.NET\Framework\versionsnummer. Die Summe aller dieser Bibliotheken wird manchmal auch als Framework Class Library (FCL) oder Base Class Library (BCL) bezeichnet.
Voraussetzungen zur Ausführung von .NET-Programmen Damit .NET-Programme auf einem Rechner ausgeführt werden können, muss das .NETFramework installiert sein. Dazu ist wiederum ein einigermaßen aktuelles Betriebssystem erforderlich. Windows 95 wird explizit nicht mehr unterstützt, Windows NT 4 nur, wenn alle aktuellen Service-Packs installiert sind. Weitere Details zu den Versionsvoraussetzungen finden Sie in Abschnitt 18.3.2.
Voraussetzungen zur Entwicklung von .NET-Programmen Die Entwicklung von .NET-Programmen setzt das .NET-Framework SDK voraus. Das SDK kann nur unter Windows NT 4.0 SP6a, 2000 oder XP installiert werden. Windows 95, 98 und ME werden nicht unterstützt. Noch größer sind die Anforderungen, wenn Sie Internet-Anwendungen entwickeln möchten (ADO.NET, Web-Services). Dann brauchen Sie Zugang zu einem Rechner, auf dem die Internet Information Services (IIS) laufen. Das setzt wiederum die Windows-Versionen NT, 2000 oder XP Professional voraus. Windows XP Home kann die IIS dagegen nicht ausführen. Die IIS müssen nicht notwendigerweise am selben Rechner laufen, d.h., Sie können auch unter Windows XP Home entwickeln, wenn Sie Zugang zu einem anderen Rechner mit den IIS haben.
2.3
Architektur
VERWEIS
Der Platz reicht hier nicht aus, um einen vollständigen Überblick über die Technik hinter .NET zu geben. Stattdessen werden hier die wichtigsten Begriffe vorgestellt, die die Architektur von .NET beschreiben. Das sollte als erster Einstieg ausreichen und Ihnen helfen, vertiefende Literatur leichter zu verstehen. In der Online-Hilfe finden Sie eine ausführlichere Beschreibung der .NET-Architektur, wenn Sie nach Einblicke in .NET Framework suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconinsidenetframework.htm
56
2 Das .NET-Universum
MSIL (Microsoft Intermediate Language) .NET-Programme werden grundsätzlich nicht zu Maschinencode kompiliert, sondern zu einem Zwischencode mit dem Namen MSIL. Zusammen mit dem MSIL-Code werden außerdem Metadaten gespeichert, die alle im Programm vorkommenden Klassen, Methoden etc. beschreiben. Das ist besonders bei Bibliotheken praktisch, weil sich diese gewissermaßen selbst beschreiben. Der Compiler kann außerdem exakt überprüfen, ob Methoden mit den richtigen Parametern aufgerufen werden etc. Erst bei der Ausführung eines Programms wird dieses vom JIT-Compiler (siehe den nächsten Abschnitt) in Maschinencode umgewandelt. MSIL ist vergleichbar mit dem Java-Bytecode oder mit dem P-Code früherer VB-Versionen. Ein wesentliche Vorteil, der für die Verwendung eines Zwischencodes spricht, ist die Plattformunabhängigkeit. Theoretisch könnte ein .NET-Programm also auch auf einem AppleRechner ausgeführt werden; tatsächlich scheitert das allerdings (noch) daran, dass das gesamte .NET-System, also insbesondere die Bibliotheken und der JIT-Compiler, momentan nur für Windows zur Verfügung steht. Wenn Sie sich den MSIL-Code eines .NET-Programms ansehen möchten, können Sie dazu das Programm ildasm.exe verwenden (siehe Abbildung 2.1), das Sie im Verzeichnis Programme\Microsoft Visual Studio .NET\FrameworkSDK\Bin finden. Mit dem Programm können Sie auch die Struktur von Bibliotheken erforschen. Dazu laden Sie eine beliebige .NET-Bibliothek; das Programm zeigt dann ähnlich wie der Objektbrowser der VB.NETEntwicklungsumgebung alle verfügbaren Klassen, deren Methoden und Eigenschaften etc. an (siehe auch Abschnitt 6.3).
Abbildung 2.1: Der MSIL-Disassembler
2.3 Architektur
57
Ein großer Nachteil des MSIL-Konzepts besteht darin, dass es denkbar einfach ist, den Code eines kompilierten .NET-Programms zu rekonstruieren. Im Internet steht dazu das kostenlose Programm Anakrino zur Verfügung. Damit können Sie ein beliebiges Programm laden. Das Programm übersetzt den MSIL-Code in beinahe lesbaren C#-Code. (Es fehlen eigentlich nur die Namen lokaler Variablen.) http://www.saurik.com/net/exemplar/
Abbildung 2.2 zeigt die Prozedur Form1_Load des Programms Hello Windows aus Abschnitt 1.2. Die Abbildung sollte klar machen, dass Sie in .NET-Programmen nichts verbergen können. (Im Prinzip galt das schon immer, aber so einfach war es noch nie, fremden Code zu lesen.) Ob Verschlüsselungsalgorithmen oder Kopierschutzfunktionen – der Code muss so programmiert sein, dass auch die Kenntnis des Codes den Schutzmechanismus nicht beeinträchtigen kann! Anakrino kann wie ildasm.exe auch auf die .NET-Bibliotheken angewendet werden. Wenn Sie wissen wollen, wie die ConcatArray-Methode der String-Klasse funktioniert, zeigt Anakrino Ihnen den kompletten C#-Code an. (Allerdings funktioniert das nicht für Methoden, die extern, also außerhalb der eigentlichen .NET-Bibliothek, realisiert sind. Gerade die .NET-Basisbibliotheken greifen zum Teil intensiv auf diverse Betriebssystemfunktionen zurück, die nicht als .NET-Code vorliegen.)
Abbildung 2.2: Anakrino
JIT (Just-in-Time-Compiler) Sobald Sie ein .NET-Programm (also die *.exe-Datei) ausführen, wird automatisch ein Justin-Time-Compiler gestartet. Der übersetzt die gerade benötigten Teile des MSIL-Code in Maschinencode, der auf dem vorliegenden Rechner optimal ausgeführt werden kann. Da die Übersetzung erst zu diesem Zeitpunkt erfolgt, ist es möglich, den Maschinencode für
58
2 Das .NET-Universum
den vorliegenden Prozessor zu optimieren. Wieweit das tatsächlich der Fall ist und ob sich dadurch wirklich Geschwindigkeitsvorteile ergeben, kann ich allerdings nicht abschätzen. Wichtig ist, dass der JIT-Compiler nicht einfach das ganze Programm sofort kompiliert, sondern immer nur die gerade benötigten Methoden. Deswegen sieht es so aus, als würden selbst große Programme praktisch verzögerungsfrei gestartet.
HINWEIS
Ein entscheidender Vorteil des JIT-Compilers besteht darin, dass während der Codeumwandlung eine Menge Sicherheitsüberprüfungen durchgeführt werden können: ob der MSIL-Code korrekt ist, ob keine unerlaubten Speicherzugriffe erfolgen, ob bei der Übergabe von Parametern an andere Bibliotheken die richtigen Typen verwendet werden etc. Dabei werden die .NET-Sicherheitseinstellungen beachten (siehe Abschnitt 2.4). Der JITCompiler stellt also einen wichtigen Teil des .NET-Sicherheitskonzeptes dar. Die Bibliotheken des .NET-Frameworks werden übrigens bereits bei dessen Installation von MSIL-Code zu Maschinencode kompiliert. Damit ist sichergestellt, dass diese sehr häufig benötigten Bibliotheken ohne Verzögerungen sofort zur Verfügung stehen.
Managed code versus unmanaged code Managed code (verwalteter Code) bedeutet, dass sich .NET um die Speicherverwaltung der
Objekte kümmert. Speicher für nicht mehr benötigte Objekte wird automatisch durch eine so genannte garbage collection wieder freigegeben (siehe auch Abschnitt 4.6.2). Managed code bietet darüber hinaus verschiedene Vorteile beim Debugging und im Hinblick auf die Sicherheit. In VB.NET erstellter MSIL-Code gilt dank der Ausführung durch die CLR immer als managed. Wenn Sie aber von Ihrem VB.NET-Programm COM-Komponenten nutzen oder API-Funktionen aufrufen, dann wird dieser Code unmanaged ausgeführt. .NET ist also nicht verantwortlich dafür, ob durch COM-Komponenten oder durch die API-Funktionen reservierter Speicher ordnungsgemäß wieder freigegeben wird.
Safe code versus unsafe code Safe code bedeutet, dass das Programm nicht direkt den Speicher manipulieren darf. VB.NET-Programme enthalten ausschließlich safe code, weil VB.NET keine Möglichkeiten bietet, über Zeiger auf den Speicher zuzugreifen. In C# sieht es anders aus: Dort kann eine Prozedur (Funktion) als unsafe deklariert werden; innerhalb dieser Prozedur darf das Programm dann Zeiger verwenden. Unsafe code kann bei manchen Anwendungen zu effizienterem Code führen. Er hat aber
den Nachteil, dass bei einem Programmierfehler die Gefahr besteht, dass Daten unzulässig überschrieben werden, was zu einer Korruption von Daten bzw. zu einem Absturz des Programms führen kann (daher die Bezeichnung unsafe). Aus diesem Grund darf unsafe code nur in der höchsten .NET-Sicherheitsstufe ausgeführt werden. (Anders formuliert: Die Verwendung von unsafe code reduziert die Anwendbarkeit eines Programms.)
2.3 Architektur
59
Assemblies, Global Assembly Cache (GAC) Vereinfacht ausgedrückt ist eine Assembly ein Programm (*.exe) bzw. eine Bibliothek (*.dll). Die exakte Beschreibung einer Assembly ist etwas komplizierter: Eine Assembly ist eine .NET-Ausführungseinheit. Das bedeutet, dass es sich um ausführbaren Code handelt (MSIL), der aber durchaus auf mehreren Dateien verteilt sein darf. Darüber hinaus enthält eine Assembly auch Metadaten, die den Inhalt beschreiben (verfügbare Klassen, Methoden, Typen etc.). Optional kann eine Assembly auch Ressourcen umfassen, also nicht ausführbare Daten (Text, Bilder, Sounddateien etc.). Selbst wenn eine Assembly aus mehreren Dateien besteht, bildet sie dennoch eine logische Einheit, die nur als Ganzes weitergegeben werden kann und die nur einen einzigen Einsprungpunkt hat. Viele Sicherheitsmechanismen gelten auf Assembly-Ebene. Der Datenaustausch innerhalb einer Assembly ist effizienter als über sie hinaus. Eine gesamte Assembly hat eine einheitliche Versionsnummer. Assemblies stellen einen Teil der .NET-Lösung für das so genannte DLL-Hell-Problem aus der COM-Vergangenheit dar: .NET unterstützt die parallele Installation unterschiedlicher Assembly-Versionen. Jedes .NET-Programm gibt intern an, auf welche Versionen externer Assemblies es zugreift. .NET kümmert sich darum, dass die richtige Version verwendet wird. (Es ist also durchaus möglich, dass zwei .NET-Programme, die gleichzeitig ausgeführt werden, unterschiedliche Versionen einer an sich gleichen .NET-Bibliothek nutzen.) Die Assemblies, die primär dafür gedacht sind, dass sie von vielen .NET-Anwendungen genutzt werden sollen, werden in dem so genannten global assembly cache (kurz GAC) installiert. Äußerlich ist der GAC einfach ein Verzeichnis (z.B. C:\WINNT\assembly). Es enthält unter anderem alle .NET-Basisbibliotheken. Beim Einfügen von Assemblies in den GAC werden spezielle Überprüfungen durchgeführt, um eine möglichst hohe Sicherheit zu gewährleisten.
HINWEIS
Selbst entwickelte Bibliotheken, die nur für das eine oder andere Programm eingesetzt werden, gehören normalerweise nicht in den GAC, sondern einfach in dasselbe Verzeichnis wie die *.exe-Datei des Programms. (Microsoft möchte mit dieser Empfehlung offensichtlich vermeiden, dass der GAC in wenigen Jahren genauso überfüllt ist wie jetzt das Windows-Systemverzeichnis.) Wenn Sie sich das Verzeichnis C:\WINNT\assembly im Windows-Explorer ansehen, sieht es so aus, als würden sich die Dateien direkt darin befinden. Das ist aber nicht der Fall – der Explorer stellt nur eine vereinfachte Sicht dar. In Wirklichkeit beginnen an dieser Stelle eine Menge Unterverzeichnisse, die unter anderem die Parallelinstallation unterschiedlicher Versionen einer Bibliothek ermöglichen. Wenn Sie sich die tatsächliche Verzeichnisstruktur ansehen möchten, können Sie z.B. das Programm EINGABEAUFFORDERUNG dazu verwenden.
60
2 Das .NET-Universum
Common Language Runtime (CLR) Der Begriff Common Language Runtime fasst zusammen, was zur Ausführung von .NET-Programmen erforderlich ist: Dazu zählen im Wesentlichen zwei Dinge: •
der JIT-Compiler sowie andere Tools, um MSIL-Code zu starten und in Maschinencode umzuwandeln, und
•
die .NET-Klassenbibliotheken.
Indem das .NET-Framework auf einem Rechner installiert wird, steht die CLR auf diesem Rechner zur Verfügung. (Beachten Sie, dass hier nicht das .NET-Framework SDK gemeint ist. Bei der CLR geht es ausschließlich um die Ausführung von .NET-Programmen, nicht um deren Entwicklung.) Die Besonderheit der CLR besteht darin, dass sie sprachunabhängig ist (daher common!): Egal, ob ein .NET-Programm mit VB.NET, C# oder einer beliebigen anderen .NET-Programmiersprache entwickelt wurde – die CLR reicht für die Ausführung aus. (Im Gegensatz dazu erforderten früher VB6-Programme eine andere Runtime-Umgebung als Programme, die mit Visual C++ entwickelt wurden!)
Common Type System (CTS)
VERWEIS
Das Common Type System (bzw. das allgemeine Typensystem laut Online-Hilfe) ist eine formale Beschreibung der Datentypen, die von .NET grundsätzlich unterstützt werden. Dazu zählen Wert- und Verweistypen, unterschiedliche Klassentypen (z.B. vererbbare oder nicht vererbbare), Strukturen, Felder (arrays), Aufzählungen (enums) etc. Es ist nicht erforderlich, dass jede .NET-Programmiersprache alle Elemente des CTS unterstützt. Die von VB.NET unterstützten Teile des CTS werden in diesem Buch vor allem in den Kapiteln 4 (Variablen- und Objektverwaltung) und 7 (Objektorientierte Programmierung) beschrieben.
Common Language Specification (CLS) Da nicht jede .NET-Programmiersprache alle Elemente der CTS unterstützt, definiert die Common Language Specification eine Teilmenge des CTS: Diese Teilmenge muss von jeder Programmiersprache unterstützt werden, die .NET-tauglich ist. In der Praxis ist die CLS insofern wichtig, als jede Bibliothek, die der CLS entspricht, von allen .NET-Programmiersprachen uneingeschränkt verwendet werden kann. Die CLS ist also nichts anderes als eine Aufzählung von Regeln, die eine Bibliothek einhalten muss, damit sie als CLS-kompatibel bezeichnet werden kann. Die Regeln betreffen ausschließlich die nach außen hin sichtbare Schnittstelle der Bibliothek, also die öffentlichen Methoden und Eigenschaften von Klassen. Was hinter dieser Schnittstelle vor sich geht, ist für die CLS nicht relevant.
2.4 Sicherheitsmechanismen
61
HINWEIS
Zahlreiche Bücher behaupten, dass mit VB.NET erzeugte Bibliotheken automatisch CLS-kompatibel sind. Das ist falsch. Auch wenn VB.NET als Standardvariablentypen nur CLS-kompatible Typen unterstützt (siehe Abschnitt 4.2.6), können CLSinkompatible Typen (z.B. UInt16, UInt32 etc.) sehr wohl verwendet werden – und zwar auch bei der Definition von öffentlichen Klassenmitgliedern. Was vielleicht noch schlimmer ist: Der VB.NET-Compiler gibt nicht einmal eine Warnung von sich, wenn Sie eine CLS-inkompatible Bibliothek erstellen. In der Online-Hilfe zum Attribut CLSCompliant können Sie dazu lesen: Der aktuelle Microsoft Visual Basic-Compiler generiert bewusst keine CLS-Kompatibilitätswarnungen. Künftige Versionen des Compilers werden diese Warnung allerdings ausgeben. Eine Begründung, warum der Compiler keine deartigen Warnungen ausgibt, fehlt freilich. Der C#Compiler ist dem VB.NET-Compiler in diesem Punkt auf jeden Fall überlegen. ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfsystemclscompliantattributeclasstopic.htm
2.4
Sicherheitsmechanismen
Das .NET-Framework und insbesondere die .NET-Bibliotheken implementieren ein ebenso feinmaschiges wie komplexes (um nicht zu sagen kompliziertes) Sicherheitskonzept. Vor der Ausführung jeder einzelnen Methode wird überprüft, ob das Programm bzw. der Benutzer, der das Programm ausführt, dazu ausreichende Rechte hat. Wenn das nicht der Fall ist, tritt ein Fehler auf. Welche Rechte ein Programm hat, hängt von mehreren Faktoren ab: •
Gewöhnliche Benutzerrechte: Bei den Windows-Versionen NT, 2000 und XP haben Benutzer je nach Login und Konfiguration unterschiedliche Rechte. Im Regelfall hat der Administrator uneingeschränkte Rechte, während alle anderen Benutzer nur ihre eigenen Dateien lesen und verändern, die Windows-Verzeichnisse aber nicht verändern dürfen etc. Neben diesen offensichtlichen Punkten betreffen die Login-abhängigen Rechte aber auch Details, die im normalen Betrieb seltener auffallen: Das Recht, andere Prozesse zu manipulieren, das Recht, Netzwerkverbindungen zu erstellen, das Recht, auf Windows-Ressourcen zuzugreifen, Daten in der Registrierdatenbank zu lesen und zu verändern etc. Diese Rechte wirken sich natürlich auf alle Operationen aus, die durch ein .NET-Programm durchgeführt werden. (Insofern ist der .NET-Sicherheitsmechanismus nicht neu; dieselben Einschränkungen galten auch schon für herkömmliche Programme.)
•
Herkunft des .NET-Programms: .NET unterscheidet zwischen Programmen von der lokalen Festplatte (am sichersten), aus dem lokalen Netzwerk (Intranet), aus dem Internet (am unsichersten) und von explizit als vertrauenswürdig bezeichneten Orten. Falls Sie sich schon einmal die Sicherheitseinstellungen des Internet Explorers angesehen haben, werden Ihnen diese vier Zonen auf Anhieb bekannt vorkommen.
62
2 Das .NET-Universum
Für jede dieser vier Zonen gelten unterschiedliche Defaultrechte. Diese Rechte steuern die so genannte Codezugriffssicherheit, also welche .NET-Methoden ausgeführt werden dürfen oder nicht. Die Codezugriffssicherheit ist eine wesentliche Neuerung im .NET-Sicherheitssystem. .NET-intern werden die Zonen durch so genannte Codegruppen beschrieben. Die für eine Codegruppe zulässigen Rechte werden wiederum durch Berechtigungssätze ausgedrückt. (Ein Berechtigungssatz ist einfach eine Aufzählung von Rechten.) •
ACHTUNG
.NET-Sicherheitskonfiguration: Das .NET-Sicherheitssystem kann auf drei Ebenen konfiguriert werden: für eine ganze Organisation (z.B. auf allen Rechnern einer Firma), für einen Rechner oder für einen Benutzer. Als Entwickler sollten Sie bedenken, dass die Frage im Allgemeinen nicht lautet, ob ein Programm ausgeführt werden darf oder nicht, sondern welche Teile des Programms ausgeführt werden dürfen! Oft kann ein Programm ohne weiteres gestartet werden; irgendwann während der Ausführung versucht das Programm dann, eine Methode aufzurufen, die das .NET-Framework als nicht zulässig betrachtet. Es tritt nun ein Fehler (eine SecurityException) auf; wenn das Programm gegenüber diesem Fehler nicht abgesichert ist (siehe Kapitel 11), wird es beendet. Besonders unangenehm ist der Umstand, dass Sie derartige Probleme nicht bemerken, wenn Sie wie viele Entwickler unter Windows als Administrator arbeiten und sich Ihre Programmdateien auf der lokalen Festplatte befinden. Dann haben Sie bei der probeweisen Ausführung Ihrer Programme beinahe uneingeschränkte Rechte, und alles funktioniert wunderbar. Erst wenn Sie die Programmausführung mit eingeschränkten Rechten ausprobieren, werden Sie feststellen, wo Ihr Programm auf Sicherheitsprobleme stoßen kann.
Konfiguration des Sicherheitssystems Das .NET-Sicherheitssystem wird durch drei XML-Dateien gesteuert, die die Codezugriffsrechte auf drei Ebenen steuern. (Die im Folgenden angegebenen Pfade sind natürlich abhängig von der Windows-Installation und der .NET-Version. WINNT bezeichnet das Windows-Verzeichnis, Dokumente und Einstellungen das Verzeichnis für Benutzerdaten. v1.0.3705 ist die Build-Nummer der .NET-Framework-Version.) •
Unternehmensebene (Organisation, Enterprise): WINNT\Microsoft.NET\Framework\v1.0.3705\config\enterprisesec.config
Defaulteinstellung: jeder darf alles •
Rechnerebene (Computer): WINNT\Microsoft.NET\Framework\v1.0.3705\config\security.config
Die Defaulteinstellung ist zonenabhängig:
2.4 Sicherheitsmechanismen
63
Lokaler Rechner (Arbeitsplatz): jeder darf alles (Berechtigungssatz FullTrust) Intranet: eingeschränkte Rechte (Berechtigungssatz LocalIntranet) Vertrauenswürdige Sites: noch stärkere Einschränkungen (Berechtigungssatz Internet) Internet: alles ist verboten (Berechtigungssatz Nothing) •
Benutzerebene: Dokumente und Einstellungen\benutzer\Anwendungsdaten\Microsoft\ CLR Security Config\v1.0.3705\security.config
Defaulteinstellung: jeder darf alles Auf einer oberen Ebene eingeschränkte Rechte können auf den unteren Ebenen weiter eingeschränkt, aber nicht mehr erweitert werden. Die Defaulteinstellung bewirkt, dass alle Zugriffsrechte durch die Rechnerebene bestimmt werden und nur auf dieser Ebene erweitert werden können. Ein gewöhnlicher Benutzer kann sich damit selbst nicht mehr Rechte zuweisen, als der Administrator des Rechners auf Computer-Ebene zulässt. Zur Einstellung der Rechte ist das .NET-Konfigurationsprogramm vorgesehen. Dieses Programm wird mit SYSTEMSTEUERUNG|VERWALTUNG|MICROSOFT .NET FRAMEWORK KONFIGURATION gestartet (siehe Abbildung 2.3). Die direkte Veränderung der Rechte mit diesem Programm setzt allerdings noch ein detaillierteres Verständnis der Mechanismen zur Zuordnung der Rechte voraus als hier vermittelt werden konnte. Eine rudimentäre Veränderung der Rechte erlaubt aber der in den folgenden Absätzen beschriebene Assistent.
Einstellung der Zonensicherheit Das größte Problem am .NET-Sicherheitssystem sehe ich in dem Umstand, dass das System viel zu komplex ist. So wie es unzählige Windows-Anwender gibt (ich nehme mich selbst nicht aus), die unter Windows NT/2000/XP grundsätzlich als Administrator arbeiten, weil es zu umständlich ist, einen sicheren Account einzurichten, der gleichzeitig ausreichend Rechte für die alltägliche Arbeit gibt, so werden sich auch die meisten (Privat)Anwender nicht mit den Details der .NET-Sicherheit beschäftigen. Immerhin bietet das .NET-Sicherheitssystem zumindest in Firmennetzen die Möglichkeit, die Sicherheitseinstellungen zentral zu administrieren. Microsoft hat dieses Problem anscheinend vorhergesehen und stellt einen einfachen Assistenten zur Verfügung, mit dem die Zonensicherheit relativ grob konfiguriert werden kann (siehe Abbildung 2.3). Um den Assistenten zu starten, führen Sie SYSTEMSTEUERUNG|VERWALTUNG|MICROSOFT .NET FRAMEWORK KONFIGURATION aus und klicken dann DIE ZONENSICHERHEIT ANPASSEN an. Sie können nun die gewünschte Vertrauensebene für die vier Zonen Arbeitsplatz, Lokales Intranet, Internet und Vertrauenswürdige Sites angeben. Diese Einstellung kann ein gewöhnlicher Anwender nur für sich selbst, der Administrator aber auch für den ganzen Rechner durchführen. (Welche Adressen als vertrauenswürdig gelten, können Sie übrigens im Optionsdialog des Internet Explorers einstellen.) Das große Problem mit dem Assistenten besteht darin, dass er dazu verleitet, bei SecurityException-Problemen einfach die Vertrauensebene zu erhöhen. Wenn also die Ausführung
64
2 Das .NET-Universum
eines Programms aus dem Intranet aufgrund unzureichender Rechte scheitert, deklariert der Administrator das Intranet im Assistenten als voll vertrauenswürdig. Dann läuft zwar das Programm, aber auch alle anderen .NET-Programme aus dem Intranet haben nun uneingeschränkte Rechte (soweit diese nicht durch den Login beschränkt werden). Das gesamte, ausgeklügelte Sicherheitssystem ist damit ad absurdum geführt und alles ist so unsicher wie eh und je. Und genau so wird es wohl auch kommen – eben weil eine korrekte und sichere Konfiguration so kompliziert ist, dass eine vollständige Erklärung ein ganzes Kapitel, wenn nicht ein ganzes Buch füllen würde.
VERWEIS
Abbildung 2.3: Einstellung der .NET-Zonensicherheit
Einführende Informationen dazu, wie Sicherheitsverletzungen im Programmcode abgesichert bzw. getestet werden können, gibt Abschnitt 12.3. Weitere Informationen zum Thema Sicherheit finden Sie in der Online-Hilfe, wenn Sie nach Sichern von Anwendungen bzw. nach Konfigurieren der Sicherheitsrichtlinien suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconsecuringyourapplication.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconsecuritypolicyconfiguration.htm
2.5 .NET und das Internet
2.5
65
.NET und das Internet
Wenn man die Berichterstattung über .NET in den Medien verfolgt hat, konnte man leicht den Eindruck gewinnen, .NET habe ausschließlich mit dem Internet zu tun. Das ist falsch! .NET bietet zahllose Verbesserungen für jede Art der Anwendungsentwicklung, unabhängig davon, ob herkömmliche Windows-Anwendungen, neue Komponenten oder InternetAnwendungen entwickelt werden sollen. Microsoft hätte sich sicher nicht so viel Mühe gegeben, riesige .NET-Bibliotheken zur Windows- und Grafikprogrammierung zu entwickeln, wenn es einzig um die Programmentwicklung für das Internet ginge. Richtig ist aber natürlich, dass .NET unter dem Gesichtspunkt entwickelt wurde, die Entwicklung gerade von Internet-Anwendungen wesentlich zu verbessern. Auch wenn dieses Buch den Schwerpunkt bei VB.NET-Grundlagen und der herkömmlichen Windows-Programmierung setzt (die Themen Datenbanken und Internet werden in einem eigenen Buch beschrieben), sind daher einige Worte zum Thema Internet angebracht (und sei es nur, um eine Erklärung für die Begriffe zu geben, auf die Sie in der sonstigen Literatur unweigerlich stoßen).
ASP.NET ASP.NET ist der Nachfolger von ASP (Active Server Pages). ASP ermöglicht die Entwicklung dynamischer Webseiten auf der Basis von VBScript-Code. Obwohl ASP technologisch gesehen eine schreckliche (weil unsichere und ineffiziente) Technologie ist, hat sie sich in einem Ausmaß durchgesetzt, das vermutlich nicht einmal Microsoft erwartet hat. Der Grund besteht darin, dass ASP einfach und flexibel ist. ASP.NET ist nun der Nachfolger zu ASP und zeichnet sich durch viele Vorteile aus. Die beiden wichtigsten sind: •
ASP.NET-Code kann nun mit der VB.NET-Entwicklungsumgebung entwickelt werden.
•
ASP.NET-Code wird kompiliert. (ASP-Code wurde interpretiert, was deutlich ineffizienter ist.)
Im Vergleich zu den in VB6 eingeführten WebClasses (die in VB.NET nicht mehr unterstützt werden), zeichnet sich ADO.NET vor allem dadurch aus, dass fertige Lösungen einfach durch ein Kopieren der Dateien in ein Verzeichnis installiert werden können (xcopyInstallation). Vom Standpunkt des Entwicklungskomforts und der angebotenen Möglichkeiten ist ASP.NET ein Meilenstein (nicht nur im Vergleich zu ASP, sondern auch zu konkurrierenden Technologien wie PHP). Der größte Nachteil besteht darin, dass ASP.NET eine teure Technologie ist: Als Internet-Server muss ein Windows-Server-Betriebssystem mit den Internet Information Services eingesetzt werden, in der Regel in Kombination mit dem SQL Server als Datenbank. Die Lizenzgebühren für einen derartigen Server sind hoch, insbesondere im Vergleich zu einem LAMP-System (Linux + Apache + MySQL + PHP/Perl). Insofern bietet sich ASP.NET eher für große und professionelle Websites an, bei denen die Lizenzkosten nur eine untergeordnete Rolle spielen. Die kostenlos verfügbare Entwicklungsumgebung ASP.NET Web Matrix (http://www.asp.net/webmatrix/) ermöglicht zwar ein kostengünstiges Ausprobieren von ASP.NET, mindert aber die Weitergabekosten nicht.
66
2 Das .NET-Universum
Web-Services Web-Services ermöglichen eine standardisierte Kommunikation zwischen Internet-Servern über das HTTP-Protokoll. Im Gegensatz zu ADO.NET erfolgt der Datenaustausch aber nicht im HTML-, sondern im XML-Format. Web-Services sind beispielsweise dann praktisch, wenn ein Internet-Server keine fertigen Webseiten, sondern lediglich Daten anbietet (z.B. Börsenkurse). An sich sind Web-Services eine gute Idee. Wirklich neu daran ist aber nur die Standardisierung des Datenaustauschs. Schon bisher konnten Daten über das HTTP-Protokoll oder über andere Protokolle über das Internet ausgetauscht werden. Neben Microsoft bieten auch andere Firmen (Sun, IBM etc.) Technologien an, die ähnliche Funktionen wie Microsofts Web-Services bieten. Da diese Technologien zum Teil auf denselben Standards basieren (XML, SOAP), sind sie sogar miteinander kompatibel; wieweit der Datenaustausch dann auch in der Praxis funktioniert, wird sich erst dann zeigen, wenn Web-Services einen größeren Stellenwert im Internet einnehmen.
.NET MyServices Zu den ersten konkreten Anwendungen von Web-Services zählt .NET MyServices: Die Grundidee dieses Systems (das früher den Namen Hailstorm hatte) besteht darin, dass kundenspezifische Daten – Name, Adresse, optional sogar Kreditkartendaten etc. – zentral bei Microsoft gespeichert werden. Wenn der Kunde nun bei einem .MyServices-Kooperationspartner etwas bestellen möchte, würden so viele Daten weitergegeben, dass eine (hoffentlich sichere) Transaktion möglich wird. Der Vorteil für den Kunden besteht darin, dass es nicht mehr notwendig ist, vor jedem Internet-Kauf eine Registrierung samt der Angabe von Adresse, Kreditkarteninformationen etc. durchzuführen. Ein zentraler .MyServices-Login würde ausreichen, egal ob bei der Firma X ein Flugticket, bei der Firma Y ein Buch oder beim Broker Z ein paar Aktien gekauft werden sollen. Der Vorteil für .MyServices-Kooperationspartner (also für Firmen): Sie könnten auf ein standardisiertes Konzept zum Austausch dieser sicherheitskritischen Daten zurückgreifen. Zudem würde die Hemmschwelle für Internet-Einkäufe reduziert: Wer sich einmal bei MyServices angemeldet hat, kann sofort einkaufen. Die Idee klingt prinzipiell faszinierend und praxisnah, allerdings schrillten bei allen Datenschützern die Alarmglocken: Die zentrale Speicherung derart wichtiger Daten ausgerechnet bei einem Unternehmen mit Monopolcharakter erscheint wenig erstrebenswert. Bedenklich stimmt auch, dass das Konzept .NET MyServices laut einem Bericht der New York Times (April 2002) offensichtlich auf geringe Resonanz bei anderen Firmen gestoßen ist. Es bleibt also abzuwarten, ob .NET MyServices ein Erfolg beschert sein wird. http://www.microsoft.com/myservices/
.NET Passport Die .NET-Passport-Funktion ist ein zentrales Login-Service und insofern der erste Schritt zu .NET MyServices. .NET Passport ermöglicht es, nach einem zentralen Login auf mehre-
2.6 Programmiersprachen (C# versus VB.NET)
67
re unterschiedliche Websites (die natürlich mit .NET Passport koopieren müssen) zuzugreifen. Passport ist im Gegensatz zu MyServices bereits Realität, weil beispielsweise jeder Hotmail-Login via Passport erfolgt. Bis jetzt gibt es aber fast keine Microsoft-unabhängigen Websites, die einen Passport-Login akzeptieren.
2.6
Programmiersprachen (C# versus VB.NET)
Zur .NET-Programmierung stehen Ihnen – zumindest wenn man den Microsoft-Versprechungen trauen darf – Dutzende von Programmiersprachen zur Auswahl. Die zwei wichtigsten .NET-Sprachen sind aber ohne jeden Zweifel VB.NET und C#. Welche dieser Sprachen ist nun besser? Da Sie dieses Buch lesen und nicht ein C#-Buch, vermute ich, dass die Entscheidung für Sie schon gefallen ist. Wenn Sie aber noch unschlüssig sind, hier einige Anmerkungen: •
Die beiden Sprachen sind zu 99 Prozent gleichwertig. Die Syntax sieht zwar anders aus, aber Sie können dieselben Dinge erzielen. (Die verbleibenden Unterschiede folgen gleich.)
•
Sie können beide Sprachen zur Entwicklung eines Programms mischen. Dazu muss das Programm aus mehreren Teilprojekten zusammengesetzt werden. Damit kann theoretisch jeder Entwickler seine Lieblingssprache verwenden. In der Praxis wird das aber kaum sinnvoll sein, weil die Wartung des Codes natürlich umso komplizierter wird, je mehr Sprachen das Projekt einsetzt.
C#-Vorteile •
C# unterstützt das Überladen von Operatoren, Integerzahlen ohne Vorzeichen und die direkte Dokumentierung von Quellcode. (Alle drei Merkmale werden für künftige Versionen auch für VB.NET versprochen.)
•
C# bietet die Möglichkeit, unsafe code zu entwickeln, um über Zeigern direkt auf den Speicher zuzugreifen. Das kann in manchen Anwendungen Geschwindigkeitsvorteile mit sich bringen.
•
Microsoft-intern wird C# offensichtlich favorisiert. Viele Microsoft-Projekte werden momentan mit C# durchgeführt. Daher ist zu erwarten, dass Fehler in C# früher entdeckt und behoben werden als in VB.NET. Bemerkenswert ist auch, dass die OnlineHilfe zur .NET-Klassenbibliothek viel mehr C#- als VB.NET-Beispiele enthält.
•
Microsoft hat C# zur Standardisierung durch ein unabhängiges Gremium freigegeben (durch die ECMA, das ist die European Computer Manufacturer's Association). Das macht es anderen Software-Herstellern möglich, ebenfalls einen C#-Compiler zu entwickeln. Es bleibt aber abzuwarten, ob es tatsächlich je möglich sein wird, ein normales C#-Programm ohne größere Änderungen auf einer anderen Plattform zu kompilieren. Das Mono-Projekt (http://www.go-mono.net/) versucht momentan, Teile des .NET-Frameworks als kostenlose Software für Linux zu entwickeln. Das Projekt umfasst unter
68
2 Das .NET-Universum
anderem einen C#-Compiler, der grundsätzlich bereits läuft, sowie wichtige .NET-Bibliotheken. Allerdings sind nur kleine Teile der .NET-Klassenbibliothek ebenfalls der ECMA zur Standardisierung übergeben werden. Die Portierung der .NET-Bibliotheken bewegt sich daher auf unsicherem Boden. •
C# kann anders als VB.NET dazu verwendet werden, so genannten unsafe code zu entwickeln (etwa zum direkten Speicherzugriff durch pointer). Das bietet manchmal die Möglichkeit, besonders systemnahen und effizienten Code zu gestalten.
•
Wer bisher mit C, C++, Java oder PHP programmiert hat, wird sich schließlich mit der C#-Syntax leichter tun als mit der von VB.NET.
•
Schließlich verspricht das C von C# Professionalität, während das Basic in VB.NET von vielen weiterhin so gedeutet wird, dass es sich hier um eine Programmiersprache für Kinder handelt ... Dieses Mißverständnis wird aus den Köpfen mancher Leute sicher nie verschwinden.
VB.NET-Vorteile •
VB.NET unterstützt optionale Parameter mit Defaultwerten sowie das Schlüsselwort With, mit dem der Objektzugriff im Code in manchen Fällen vereinfacht werden kann. (Beide Merkmale fehlen in C#.)
•
VB.NET-Code ist unabhängig von der Groß- und Kleinschreibung.
•
Der VB.NET-Editor bietet deutlich mehr Komfort als der C#-Editor: Die automatische Codeeinrückung bei Änderungen der Programmstruktur funktioniert viel besser, die Groß- und Kleinschreibung wird automatisch richtig gestellt, fehlerhafter Code wird meist schon während der Eingabe erkannt (weil der Code im Hintergrund kompiliert wird) etc. Das Gegenargument, dass der Tippaufwand bei VB.NET größer ist als bei C#, ist zwar richtig, aber irrelevant. Haben Sie schon mal überlegt, wie groß der Zeitaufwand für das Eingeben von 100 Zeilen Code ist? Und wie groß der Aufwand ist, um diesen Code zu entwicken, die darin enthaltenen Fehler zu suchen? Die Eingabezeit ist dagegen vollkommen vernachlässigbar!
•
Manche Einschränkungen in VB.NET gegenüber C# kommen der Sicherheit, Kompatibilität und Stabilität der resultierenden Programme zugute. Insofern kann man es durchaus als Vorteil betrachten, dass VB.NET keine Integer-Datentypen ohne Vorzeichen unterstützt (diese Datentypen sind nicht CLS-kompatibel) oder dass VB.NET die Verwendung von Zeigern grundsätzlich verbietet.
•
Für VB.NET spricht die ausführlichere, besser lesbare Syntax. Das macht das Lesen von fremdem Code deutlich einfacher als bei C# und ist ein wesentlicher Vorteil, wenn Code von unterschiedlichen Personen bearbeitet wird.
•
Wer bereits Erfahrungen mit VB1-6, VBA, VBScript, WSH oder ASP hat – und das sind Millionen von Programmierern! – wird sich in VB.NET-Code ohne große Umstellungsprobleme zurechtfinden. In diesem Zusammenhang hilft vor allem die VB.NET-Run-
2.7 Entwicklungsumgebungen
69
VERWEIS
time-Bibliothek, die zahlreiche aus VB vertraute Funktionen unter VB.NET zur Verfügung stellt: die String-Funktionen Left, Mid und Right, Konstanten wie vbCrLF, Hilfsfunktionen wie IIf etc. (Grundsätzlich können Sie diese Bibliothek auch in C# nutzen, das ist aber unüblich.) Im Internet sind zurzeit zwei Konverter von C# zu VB.NET zu finden. Konverter in die umgekehrte Richtung scheint es noch nicht zu geben. http://www.kamalpatel.net http://www.aspalliance.com/aldotnet/examples/translate.aspx
Fazit Es gibt momentan keine zwingenden Argumente, die belegen, dass die eine oder andere Sprache besser ist als die andere. Es lässt sich auch noch nicht abschätzen, ob die Mehrheit der Programmierer eher zu C# oder zu VB.NET tendiert. Verwenden Sie also die Sprache, die Ihnen sympatischer ist. (Und wenn Sie dieses Buch lesen möchten: Verwenden Sie VB.NET!)
2.7
Entwicklungsumgebungen
Wie in Abschnitt 2.2 bereits erwähnt wurde, sind grundsätzlich alle Werkzeuge, die zur Entwicklung von VB.NET-Programmen unbedingt erforderlich sind, kostenlos verfügbar. Das Problem besteht nur darin, dass diese Werkzeuge so umständlich zu bedienen sind, dass sofort der Wunsch nach einer komfortablen Entwicklungsumgebung aufkommt.
HINWEIS
Microsoft bietet eine ausgezeichnete, aber leider relativ teure Entwicklungsumgebung im Rahmen seiner VB.NET- bzw. VS.NET-Produkte an. Wer Geld sparen will, kann stattdessen die kostenlose Entwicklungsumgebung SharpDevelop einsetzen oder eben ganz ohne Entwicklungsumgebung arbeiten. Dieser Abschnitt stellt alle drei zurzeit aktuellen Optionen kurz vor. (Möglicherweise wird es in Zukunft noch mehr kommerzielle oder kostenlose Alternativen geben.) Beachten Sie, dass jede .NET-Programmentwicklung – egal ob mit oder ohne Entwicklungsumgebung – Windows NT, 2000 oder XP erfordert! Die Beispiele dieses Buchs setzen voraus, dass Sie mit der Entwicklungsumgebung von Microsoft arbeiten. VB.NET Standard reicht für die meisten Beispiele aus, professionelle Programmierer sind mit VS.NET Professional aber sicherlich besser beraten.
70
2 Das .NET-Universum
2.7.1
Microsoft-Entwicklungsumgebungen
Eine Microsoft-Entwicklungsumgebung für VB.NET (siehe Abbildung 1.2 im vorigen Kapitel) können Sie im Rahmen einer ganzen Reihe von Versionen erwerben: •
VB.NET-Standard: Diese Minimalversion kann nur für VB.NET-Projekte verwendet werden, nicht aber für C#. (Für C#-Entwickler gibt es eine eigene Version C#-Standard.) Ich konnte diese Version selbst leider weder testen, noch fand ich auf den Seiten von http://www.microsoft.com eine wirklich präzise Beschreibung, welche Komponenten von VS.NET Professional (siehe unten) nun mit VB.NET Standard mitgeliefert werden. Laut Berichten aus verschiedenen Newsgruppen unterliegt die Standardversion aber massiven Einschränkungen, was die Unterstützung verschiedener Projekttypen betrifft. Insbesondere ist es nicht möglich, mit VB.NET-Standard eigene Klassenbibliotheken zu entwickeln. Alle Projekte müssen zu *.exe-Dateien kompiliert werden. *.dll-Kompilate werden nicht unterstützt.
•
VS.NET Professional: Für die meisten professionellen Entwickler ist das die am besten geeignete Version. Es werden prinzipiell alle Projekttypen unterstützt, d.h., es können sowohl VB.NET- als auch C#- und C++-Projekte durchgeführt werden. Im Vergleich zur Standardversion wird unter anderem der Migrationsassistent, das Datenbankberichtsystem Crystal Reports und ein umfassenderes Hilfesystem mitgeliefert.
•
VS.NET Enterprise: Diese Version enthält alle Komponenten von VS.NET Professional, zusätzlich den Visual Studio Analyzer, Visual Source Safe sowie Entwicklerlizenzen für Microsoft Windows 2000 Advanced Server, SQL Server 2000, Exchange Server, Host Integration Server und Commerce Server.
•
VS.NET Enterprise Architect: Hier bestehen die weiteren Ergänzungen unter anderem aus einer Visio-2002-Version zum interaktiven Datenbankdesign und einer Entwicklerversion des Microsoft BizTalk Servers.
Die mitgelieferte Entwicklungumgebung ist im Prinzip bei allen Versionen die gleiche. Die Unterschiede betreffen nur die Anzahl der in die Umgebung integrierten Designer, die mitgelieferten Zusatztools, den Umfang der Dokumentation, eventuell das Angebot für kostenlose Support-Anfragen etc.
TIPP
Beachten Sie, dass Sie VS.NET auch im Rahmen eines MSDN-Abonnements erwerben können. Das ist dann empfehlenswert, wenn Sie an einem Gesamtpaket verschiedener Microsoft-Produkte interessiert sind. Es ist zu hoffen, dass es demnächst auch preisreduzierte VB.NET- bzw. VS.NETVersionen für Schüler, Studenten und generell für den akademischen Bereich geben wird. Für den US-Markt gibt es bereits derartige Angebote; im deutschen Sprachraum habe ich bis zur Manuskriptabgabe allerdings noch keine vergleichbaren Versionen entdecken können.
VERWEIS
2.7 Entwicklungsumgebungen
71
Wenn Sie VS.NET schon besitzen, finden Sie in der Online-Hilfe hier eine Beschreibung der verschiedenen VS.NET-Versionen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbgrfvisualbasicstandardeditionfeatures.htm
Im Internet finden Sie einen relativ ausführlichen Produktvergleich hier: http://msdn.microsoft.com/vstudio/howtobuy/choosing.asp
2.7.2
SharpDevelop
SharpDevelop ist eine in C# programmierte Entwicklungsumgebung für C#- und VB.NETProjekte (siehe Abbildung 2.4). Zum Zeitpunkt der Manuskriptabgabe lag das Programm in einer bereits brauchbaren, aber nicht ganz stabilen Betaversion 0.88b vor. (Diese Version finden Sie auch auf der CD zu diesem Buch.) http://www.icsharpcode.net/OpenSource/SD/default.asp
Die vielleicht größte Besonderheit von SharpDevelop besteht darin, dass es im Rahmen der GNU Public License (GPL) kostenlos und samt Quellcode zur Verfügung steht. Die wichtigste Auflage der GPL besteht darin, dass Sie eine weiterentwickelte Version von SharpDevelop nur dann verbreiten dürfen, wenn Sie Ihre Änderungen am Quellcode kostenlos zur Verfügung stellen. (Mit anderen Worten: Sie dürfen SharpDevelop nicht zu einer kommerziellen Entwicklungsumgebung ausbauen, wenn Sie nicht bereit sind, alle Änderungen am Code kostenlos zur Verfügung zu stellen.)
Abbildung 2.4: VB.NET-Programmentwicklung mit SharpDevelop
72
2 Das .NET-Universum
Was kann SharpDevelop? Zu den Grundaufgaben einer Entwicklungsumgebung gehört die Verwaltung von Projektund Codedateien, die bereits ausgezeichnet funktioniert. Die Codeeingabe wird durch eine farbige Syntaxhervorhebung und durch einige Assistenten unterstützt, mit denen Codeschablonen eingegeben werden können. SharpDevelop kann nicht nur zur Eingabe von C#- und VB.NET-Code verwendet werden, sondern unterstützt unter anderem auch die Formate HTML und XML. Beim Kompilieren des Codes zeigt das Programm eine Fehlerliste an. Per Doppelklick können Sie in die betreffende Zeile springen.
Was kann SharpDevelop (noch) nicht? Im Vergleich zur Microsoft-Entwicklungsumgebung fehlen SharpDevelop noch viele Funktionen. Die folgende Liste basiert auf der SharpDevelop-Version 0.88b und nennt nur die wichtigen Punkte: •
Es gibt keinen Designer zum Entwurf von Windows.Forms-Formularen.
•
Es gibt keinen Debugger.
•
F1 führt nur zur Online-Hilfe von SharpDevelop, nicht aber zum Hilfetext des gerade
unter dem Cursor befindlichen Schlüsselworts. •
Es gibt keinen Objektbrowser.
•
Code wird nicht automatisch eingerückt.
•
Fehler werden erst beim Kompilieren erkannt, nicht schon bei der Eingabe.
Hello Console Um das VB.NET-Beispielprogramm aus Abschnitt 1.1 in SharpDevelop zu realisieren, sind zwei (fett hervorgehobene) Änderungen am Code erforderlich. Zum einen bewirkt Imports Microsoft.VisualBasic, dass die Klassen der VB-Bibliothek verwendet werden können, ohne dass jedes Mal Microsoft.VisualBasic vorangestellt werden muss. Zum anderen muss bei jeder Methode aus dieser Bibliothek der Klassenname vorangestellt werden (DateAndTime.Now statt Now, Strings.Left statt Left etc.). Option Strict On Imports System Imports Microsoft.VisualBasic Module Module1 Sub Main() Console.WriteLine("Hello Console!") Console.WriteLine() 'leere Zeile Console.WriteLine("{0}, das heutige Datum ist: {1}!", _ Environment.UserName, DateAndTime.Now.ToLongDateString) End Sub End Module
VERWEIS
2.7 Entwicklungsumgebungen
73
Vielleicht fragen Sie sich, warum der Code nicht ohne Änderungen verwendet werden kann. Der Grund besteht darin, dass sich die Microsoft-Entwicklungsumgebung um so genannte Defaultimporte kümmert und das (undokumentierte) Attribut <StandardModule> auswertet. Deswegen muss bei Methoden aus Microsoft.VisualBasic der Klassenname nicht angegeben werden. Diese beiden Besonderheiten sind in Abschnitt 6.2 beschrieben.
Hello Windows Erheblich aufwendiger als bei Konsolenanwendungen ist die Portierung von VB.NETCode von Windows-Anwendungen. Neben den Importen und Referenzen auf Bibliotheken, die von Hand eingefügt werden müssen, unterscheiden sich SharpDevelop und die Microsoft-Entwicklungsumgebung auch darin, wie Windows.Forms-Programme gestartet werden. Microsoft fügt dazu den Code Application.Run(New formname()) ein (siehe Abschnitt 15.2), was bei SharpDevelop nicht der Fall ist. Außerdem fehlt in SharpDevelop der Designer zum Entwurf von Formularen. Natürlich kann diese Arbeit von Hand erledigt werden, indem in den New-Konstruktor der erforderliche Code zum Erzeugen von Buttons etc. eingefügt wird (siehe Abschnitt 15.2.2) – aber wirklich Spaß macht das nicht. Wenn Sie es dennoch versuchen möchten, sollten Ihnen die folgenden Zeilen bei ersten Experimenten helfen. Imports System Imports System.Drawing Imports System.Windows.Forms Public Class MainForm Inherits Form Friend WithEvents Button1 As Button Public Shared Sub Main() Application.Run(New MainForm()) End Sub Public Sub New() MyBase.New() Me.Text = "Hello Windows" Me.ClientSize = New Size(224, 104) ' Button Me.Button1 = New Button() Me.Button1.Location = New Point(112, 72) Me.Button1.Size = New Size(104, 24) Me.Button1.Text = "Ende" Me.Controls.Add(Button1) End Sub
74
2 Das .NET-Universum
HINWEIS
Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles Button1.Click Me.Close() End Sub End Class
SharpDevelop 0.88b hat offensichtlich noch ein Problem beim Kompilieren von Windows-Anwendungen: Zwar kann bei den PROJEKTOPTIONEN angegeben werden, dass der Code als Windows-Programm kompiliert werden soll; SharpDevelop ignoriert diese Option aber und kompiliert das Programm dennoch zu einem Konsolenprogramm. Deswegen wird während der Programmausführung auch ein Konsolenfenster angezeigt, solange das Fenster offen ist.
2.7.3
Ohne Entwicklungsumgebung arbeiten
Wenn Sie die ersten Experimente ganz ohne eine Entwicklungsumgebung machen möchten, sollten Sie als Erstes die Umgebungsvariable PATH so einstellen, dass diese auch auf das Verzeichnis mit den .NET-Tools (Compiler etc.) zeigt. Dazu starten Sie das Modul SYSTEMSTEUERUNG|SYSTEM|ERWEITERT|UMGEBUNGSVARIABLEN und bearbeiten dort PATH. Die .NET-Tools befinden sich im Verzeichnis C:\Windows\Microsoft.NET\Framework\v1.0.3705 (überprüfen Sie die Versionsnummer!).
Abbildung 2.5: Einstellung der PATH-Variablen
2.7 Entwicklungsumgebungen
75
Anschließend starten Sie das Programm START|ZUBEHÖR|EINGABEAUFFORDERUNG und wechseln in das Verzeichnis, in dem sich Ihre VB.NET-Quelltextdatei befindet. (Diese Datei können Sie mit einem beliebigen Editor erstellen.) Zum Kompilieren führen Sie einfach die folgende Anweisung aus (siehe Abbildung 2.6): vbc /target:exe dateiname.vb
Abbildung 2.6: VB.NET-Programm kompilieren und ausführen
Der Compiler erstellt daraus die Datei dateiname.exe, die Sie danach sofort ausprobieren können. Wenn beim Kompilieren Fehler auftreten, werden Sie davon (jeweils mit Zeilennummer) informiert und müssen diese beheben. Im Code müssen Sie die gleichen Details wie bei SharpDevelop beachten (siehe den vorigen Abschnitt). Außerdem müssen Sie beim Kompilieren alle Bibliotheken angeben, die Ihr Programm nutzt (außer mscorlib.dll). Um ein Windows-Programm zu kompilieren, müssen Sie den Compiler wie folgt ausführen. Entscheidend ist, dass Sie bei der Option /target winexe angeben und dass Sie mit der Option /reference alle erforderlichen Bibliotheken angeben. Die Anweisung muss in einer Zeile erfolgen und wurde hier nur aus Platzgründen auf drei Zeilen verteilt.
VERWEIS
vbc /target:winexe /reference:system.dll,system.drawing.dll,system.windows.forms.dll mainform.vb vbc.exe kann durch unzählige weitere Optionen gesteuert werden, die in der Online-Hilfe beschrieben werden. Suchen Sie einfach nach vbc.exe! ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/valrfvbcompileroptionslistedbycategory.htm
3
Von VB6 zu VB.NET
Dieses Kapitel richtet sich an VB6-Programmierer, die gezielt nach Informationen suchen, die die Unterschiede zwischen VB6 und VB.NET betreffen. So viel vorweg: Es hat sich derart viel geändert, dass die Weiterverwendung vorhandenen Codes so gut wie unmöglich ist. Auch der am Ende des Kapitels kurz beschriebene Migrationsassistent kann da nicht wirklich weiterhelfen. VB.NET ist eine neue Programmiersprache mit vielen neuen Funktionen und Vorzügen, aber sie ist ungeeignet, um alten VB6-Code zu warten oder zu migrieren. 3.1 3.2 3.3
Fundamentale Neuerungen in VB.NET Unterschiede zwischen VB6 und VB.NET Der Migrationsassistent
78 79 109
78
3 Von VB6 zu VB.NET
3.1
Fundamentale Neuerungen in VB.NET
VB.NET zeichnet sich durch viele Neuerungen aus, die von grundlegender Natur und nicht einfach Ergänzungen oder Änderungen gegenüber VB6 sind. Dieser Abschnitt fasst die wichtigsten dieser Neuerungen zusammen. (Im nächsten Abschnitt folgen dann Details über die kleinen und großen Inkompatibilitäten zwischen VB6 und VB.NET.) •
Die .NET-Klassenbibliothek: Die wohl größte Neuerung für Visual-Basic-Programmierer betrifft genau genommen gar nicht VB.NET, sondern das darunter liegende Betriebssystem. Durch die .NET-Klassenbibliothek können fast alle Betriebssystemfunktionen über eine einheitliche objektorientierte Schnittstelle genutzt werden. Die Bibliothek enthält Tausende von Klassen und schier unendlich viele Eigenschaften, Methoden, Ereignisse etc. (auf jeden Fall mehr, als Sie jemals nutzen können). Der Zugriff auf die Klassen erfolgt in der Schreibweise System.klasse.subklasse. Die folgende Liste gibt ein paar Beispiele, die lediglich die riesige Bandbreite verdeutlichen sollen, die durch diese Bibliothek abgedeckt wird. System.Data: die neue Datenbankbibliothek ADO.NET System.Drawing: Grafikfunktionen (GDI) System.IO: Zugriff auf Dateien System.OperatingSystems: Informationen über die Version des laufenden Betriebssystem System.Security: Sicherheitsfunktionen, Zugriffsrechte, Kryptographie System.Text.RegularExpression: Bearbeitung von Zeichenketten mit regulären Ausdrücken System.Web: Internet-Programmierung (ASP.NET) System.Web.Services: Internet-Programmierung (Web-Services) System.Windows.Forms: Windows-Programmierung (Fenster, Steuerelemente)
Viele Begriffe, die Sie aus der Werbung für .NET vermutlich schon kennen (ADO.NET, ASP.NET, Web-Services etc.) sind also nichts anderes als Funktionen, die die .NETKlassenbibliothek zur Verfügung stellt und die Sie mit VB.NET nutzen können. •
Namensräume: Wie aus der obigen Liste deutlich wird, bestehen Klassennamen aus mehreren Teilen und können ziemlich lang werden. Zur Organisation der Klassen verwendet Microsoft so genannte Namensräume. Ein Namensraum fasst mehrere Klassen zusammen, deren Name mit denselben Schlüsselwörtern beginnt. Damit Sie bei der Nutzung von Klassen nicht jedes Mal den gesamten Klassennamen angeben müssen, können Sie mit Imports einzelne Namensräume als Default einrichten. Das bewirkt, dass der Compiler bei der Auflösung von Klassennamen automatisch die entsprechenden Namensräume durchsucht.
•
Objektorientierte Programmierung: In VB.NET können Sie Klassen nicht nur anwenden, sondern auch selbst programmieren. VB.NET bietet dazu jetzt weitgehend dieselben Möglichkeiten, die Sie von anderen Programmiersprachen (C++, Java) vielleicht schon kennen: Vererbung, Schnittstelle, Attribute, Delegates (eine Art von Funktionszeigern) etc. Eine ausführliche Erklärung dieser Begriffe folgt in Kapitel 7.
3.2 Unterschiede zwischen VB6 und VB.NET
79
•
Konsolenanwendungen: VB.NET ermöglicht erstmals (wie seit jeher die Programmiersprache C) die Erstellung von so genannten Konsolenanwendungen. Derartige Programme werden in einer Textkonsole (dem ehemaligen DOS-Fenster) ausgeführt. Konsolenanwendungen eignen sich besonders gut für Beispielprogramme, weil es nur einen minimalen Overhead gibt. Ein noch so einfaches Windows-Programm besteht dagegen aus mindestens 50-60 Zeilen Code, die zwar von der Entwicklungsumgebung automatisch erstellt werden, am Anfang aber dennoch für Verwirrung sorgen.
•
Multithreading: Multithreading bedeutet, dass verschiedene Teile eines Programms quasi gleichzeitig ausgeführt werden können. Auch VB.NET unterstützt diese aus anderen Programmiersprachen schon lange bekannte Funktion nun.
•
Entwicklungsumgebung: Zu VB.NET gibt es eine vollkommen neue Entwicklungsumgebung (die einheitlich für alle .NET-Programmiersprachen ist). VB6-Umsteiger brauchen eventuell ein paar Tage, um sich an die Neuerungen zu gewöhnen, aber im Wesentlichen ist die neue Entwicklungsumgebung gelungen und läuft stabil. Einzige Voraussetzung: Ein großer Monitor!
•
Compiler, MSIL: Auch hinter den Kulissen hat sich viel getan. VB.NET wird jetzt immer kompiliert, d.h., es gibt (anders als bei VB6) keinen Interpreter mehr. Das ist nicht immer ein Vorteil – gerade bei der Fehlersuche war der Interpreter von VB6 ausgesprochen praktisch, weil Änderungen im laufenden Programm möglich waren. Neu ist auch die Art, wie Code kompiliert wird: VB6 erzeugte wahlweise so genannten P-Code (der von einem Runtime-Interpreter ausgeführt wird) oder echten Maschinencode. VB.NET erzeugt Code in der MSIL (Microsoft Intermediate Language). Das ist wie PCode ein Zwischencode, der nicht unmittelbar ausgeführt werden kann. Anders als PCode wird MSIL vor der Ausführung aber durch einen Just-in-time-Compiler kompiliert, so dass es keine Geschwindigkeitsnachteile gibt.
3.2
Unterschiede zwischen VB6 und VB.NET
VERWEIS
Dieser Abschnitt fasst die wichtigsten Unterschiede zwischen VB6 und VB.NET zusammen. Dabei handelt es sich aber keineswegs um eine vollständige Referenz. Einige weitere Informationsquellen finden Sie, wenn Sie in der Hilfe nach Änderungen an Visual Basic suchen. http://www.msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvb600/html/vb6tovbdotnet.asp ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vacondifferencesbetweenvb6andvb7.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconProgrammingElementsChangesInVB7.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/ vboriIntroductionToVisualBasic70ForVisualBasicVeterans.htm
Wenn Sie einen noch ausführlicheren und systematischeren Vergleich zwischen VB6 und VB.NET suchen, empfehle ich Ihnen das Buch The .NET Languages von Brian Bischof wärmstens. (Das Buch berücksichtigt außerdem noch C#, was aber kein Nachteil ist – ganz im Gegenteil!)
80
3.2.1
3 Von VB6 zu VB.NET
Variablenverwaltung
Allgemeines VB6
VB.NET
Dim: Dim gilt immer für eine
Dim gilt für den Block, in dem es definiert ist; wenn Dim innerhalb einer Schleife eingesetzt
gesamte Prozedur oder Funktion.
wird, kann die so deklarierte Variable auch nur innerhalb der Schleife verwendet werden. Dim: Mit Dim x, y, z As Long wurden x Dim x, y, z As Long deklariert alle angegebenen und y als Variant-Variable deklariert, Variablen als Long. z dagegen als Long-Variable. Dieses
Verhalten war unlogisch, aber man hat sich mit der Zeit daran gewöhnt. Dim: Eine Initialisierung der
Variablen ist nicht möglich. DefInt, DefDbl etc.: Mit diesen
Kommandos ist es möglich, den Defaultvariablentyp einzustellen (z.B. DefInt A-Z). Option Explicit: Diese Option kann
am Beginn jeder Codedatei eingestellt werden. Sie steuert, ob Variablen vor ihrer Verwendung deklariert werden müssen.
Dim ermöglicht nun auch eine Initialisierung der Variable, z.B. Dim i1 As Integer = 3. DefInt und vergleichbare Kommandos gibt es nicht mehr. Als Defaultvariablentyp gilt Object (der Nachfolger des Variant-Typs).
Die Option kann weiterhin im Programmcode eingestellt werden. Dabei muss nun zusätzlich das Schlüsselwort On oder Off verwendet werden, um die gewünschte Einstellung zu verdeutlichen. Während die Option-Einstellungen im Programmcode nur die jeweilige Codedatei gelten, gibt es nun auch Defaulteinstellungen, die für das gesamte Projekt gelten. Diese können mit PROJEKT|EIGENSCHAFTEN eingestellt werden. Per Default gilt: Option Explicit On
Typenkonvertierung: VB6 führt generell eine automatische Typenkonvertierung durch, etwa wenn Sie integervar = "3" ausführen. Dieser Automatismus ist bequem, verbirgt aber manchmal Programmierfehler.
Per Default ist alles beim Alten geblieben. Es gibt aber eine neue Option Option Strict. Die Einstellung erfolgt wie bei Option Explicit (siehe oben). Per Default gilt: Option Strict Off
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
81
VB.NET Wenn die Option aber aktiviert wird, führt VB.NET nur mehr solche Umwandlungen automatisch durch, die unbedenklich sind und mit Sicherheit ohne Datenverlust durchgeführt werden können (etwa von Long zu Double). Alle anderen Umwandlungen werden bereits bei der Entwicklung des Programms mit einem Syntaxfehler quittiert. Sie sind dann gezwungen, eine geeignete Funktion zur Umwandlung selbst einzufügen. Eine Nebenwirkung von Option Explicit On besteht darin, dass alle Variablen explizit mit Typ deklariert werden müssen. Der Vorteil besteht darin, dass Sie auf diese Weise eine (nicht unerhebliche) Fehlerquelle ausschließen.
Set: Mit diesem Kommando werden
Objektzuweisungen durchgeführt (Set obj = anderesobjekt).
Objektzuweisungen erfolgen jetzt einfach durch den Operator =. Das Kommando Set ist nicht mehr erforderlich und existiert deswegen nicht mehr. (Wenn Sie es versehentlich eingeben, wird es von der Entwicklungsumgebung sofort wieder entfernt.)
Variablentypen VB6
VB.NET
Variablen versus Objekte: VB6 unterscheidet streng zwischen gewöhnlichen Variablen und Objekten. Objekte zeichnen sich dadurch aus, dass es hierfür Methoden und Eigenschaften gibt.
Bei VB.NET gilt jede Variable als Objekt. Selbst auf Variablen für elementare Datentypen (z.B. Integer, Date, String) können zahlreiche Methoden angewandt werden. Beispielsweise liefert intvar.MaxValue den größten Wert, der in dieser Variable gespeichert werden darf. strvar.Length liefert die Anzahl der gespeicherten Zeichen.
Variant-Variablentyp: Dieser
Den Datentyp Variant gibt es nicht mehr. Der Defaultdatentyp heißt nun Object und hat teilweise ähnliche Eigenschaften wie Variant.
Variablentyp gilt als Defaultvariablentyp.
82
3 Von VB6 zu VB.NET
VB6
VB.NET
Integer-Variablentypen:
Die Bedeutung von Integer und Long hat sich geändert:
Integer-Zahlen sind 2 Byte lang. Long-Zahlen sind 4 Byte lang.
Integer-Zahlen sind jetzt 4 Byte lang. Long-Zahlen sind jetzt 8 Byte lang.
Als Ersatz für 2-Byte-Integerzahlen gibt es den neuen Datentyp Short (2 Byte, für Werte zwischen -32.768 bis 32.767). Single- und Double-Variablentypen:
Single und Double kennen nun auch die
Bei ungültigen Berechnungen, z.B. 1/0.0, Sqr(-1) und Log(-1), treten Fehler auf.
Sonderwerte plus und minus unendlich sowie not a number. Ungültige Fließkommaberechnungen führen normalerweise nicht mehr zu einem Fehler, sondern resultieren in den erwähnten Sonderwerten. Diese Werte können Sie mit den Methoden Double.IsNaN(x) oder Single.IsNegativeInfinity(s) feststellen. Gerade bei der Entwicklung mathematischer Anwendungen ist dieses Verhalten sehr gewöhnungsbedürftig, weil offensichtliche Fehler oft verborgen bleiben.
Decimal- und Currency-Datentypen: Currency: Festkommazahlen, 15
Stellen vor, 4 nach dem Komma. Platzbedarf 4 Byte. Kennzeichnung mit @.
Den Datentyp Currency gibt es nicht mehr. Dafür ist Decimal nun ein eigenständiger Datentyp mit ansonsten unveränderten Eigenschaften. Mit @ gekennzeichnete Variablen gelten nun automatisch als Decimal-Variablen.
Decimal: Festkommazahlen mit 28 Stellen, als Variant-Untertyp
realisiert. Platzbedarf 12 Byte. String-Datentyp: Mit Dim s As String * 10 können Zeichenkettenvariablen
konstanter Länge definiert werden.
Zeichenkettenvariablen mit vorgegebener Länge gibt es nicht mehr. Für diesen Zweck können Char-Felder eingesetzt werden. Darüber hinaus gibt es zahlreiche Änderungen, die den Umgang mit Zeichenketten betreffen (siehe Abschnitt 3.2.3).
Date-Datentyp: Die interne
Darstellung erfolgte durch einen Double-Wert (8 Byte).
Die interne Darstellung erfolgt nun durch einen Long-Wert (ebenfalls 8 Byte). Eine direkte Addition von Daten und Zeiten mit + ist nicht mehr möglich. Dafür gibt es zahlreiche neue Methoden zur Bearbeitung derartiger Variablen (z.B. datevar.Add).
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
83
VB.NET Zur Konvertierung zwischen Date-Variablen und dem VB6-kompatiblen Fließkommaformat gibt es die neuen Methoden To- und FormOADate.
Konstanten True und False: True hat den Wert -1, False 0.
Hier hat sich nichts geändert. (In der ersten BetaVersion von VB.NET galt True=1, aber in der zweiten wurde diese Änderung wieder rückgängig gemacht.)
Felder und Aufzählungen VB6
VB.NET
Option Base: Diese Anweisung bestimmt, ob ein mit Dim x(3) deklariertes Feld von x(0) bis x(3)
Die Anweisung Option Base gibt es nicht mehr. Die Indizes eines mit Dim x(n) deklarierten Felds reichen immer von 0 bis n.
reicht (Defaulteinstellung), oder von x(1) bis x(3) (Option Base 1). Dim x(-5 To 7): In VB6 besteht die
VB.NET bietet diese Möglichkeit nicht mehr. Der Möglichkeit, bei der Feldindex reicht immer von 0 bis n. Dimensionierung von Feldern sowohl den unteren als auch den oberen Grenzwert des Indexbereichs anzugeben. Datenfelder (Array): In VariantVariablen können so genannte Datenfelder gespeichert werden, die mit Array sehr komfortabel initialisiert werden können.
Datenfelder und das Schlüsselwort Array sind zusammen mit dem Variant-Datentyp abgeschafft worden. Um mehrere Elemente eines Felds gleichzeitig zu initialisieren, ist jetzt eine neue Schreibweise möglich: Dim myarray As Integer = {1, 2, 3}
Aufzählungen: In VB6 können Sie Aufzählungen mit den beiden Klassen Collection (VB-intern) und Dictionary (Scripting-RuntimeBibliothek) verwalten.
Unter VB.NET stehen eine VB6-kompatible Collection-Klasse noch immer zur Verfügung (Microsoft.VisualBasic.Collection). Darüber hinaus stellt die .NET-Bibliothek aber gleich eine ganze Sammlung neuer Collection-Klassen zur Verfügung, die viel leistungsfähiger sind als die aus VB6 vertrauten Aufzählungen. Die neuen Aufzählklassen werden ausführlich in Kapitel 9 vorgestellt. Eine syntaxkompatible Entsprechung von Dictionary gibt es allerdings nicht.
84
3 Von VB6 zu VB.NET
Eigene Datenstrukturen VB6
VB.NET
Type: Mit Type – End Type können
Type wird nicht mehr unterstützt. Stattdessen gibt es nun Structure – End Structure zur Bildung von
sehr einfach Strukturen aus elementaren Datentypen zusammengesetzt werden.
3.2.2
eigenen Datenstrukturen. Strukturen bieten zwar viel mehr Möglichkeiten, sind aber inkompatibel zu Type.
Sprachsyntax
Im Bereich der objektorientierten Programmierung gibt es zahllose neue Schlüsselwörter, die unter anderem erstmals eine echte Vererbung bei der Definition von Klassen ermöglichen. Die Beschreibung dieser Merkmale füllt das gesamte Kapitel 7. Daher beschränkt sich die folgende Tabelle auf die prozeduralen Sprachelemente. VB6
VB.NET
While-Wend-Schleifen: Derartige Schleifen können mit While bedingung – Wend formuliert werden.
Die Syntax lautet nun Do While bedingung – Loop.
Gleichnamige Prozeduren: Es besteht keine Möglichkeit, zwei gleichnamige Prozeduren zu deklarieren.
VB.NET erlaubt die Deklaration gleichnamiger Prozeduren, wenn diese anhand der Parameterliste eindeutig identifizierbar sind.
Aufruf von Prozeduren und Methoden: Parameter von Prozeduren und Methoden ohne Rückgabewert müssen nicht in Klammern gesetzt werden.
Parameter von Prozeduren und Methoden müssen nun in Klammern gestellt werden. (Die Entwicklungsumgebung fügt die Klammern selbstständig ein, wenn Sie es vergessen.)
Parameterübergabe: Per Default werden Parameter mit ByRef an Prozeduren übergeben, können also verändert werden.
Per Default erfolgt die Parameterübergabe nun mit ByVal. Änderungen der Parameter in der Prozedur haben daher keine Auswirkung auf Variablen außerhalb der Prozedur.
Optionale Parameter: Bei optionalen Bei der Deklaration optionaler Parameter muss Variant-Parametern können Sie mit nun ein Defaultwert angegeben werden. IsMissing IsMissing testen, ob ein Wert steht nicht mehr zur Verfügung. übergeben wurden. Die Angabe von Defaultwerten ist optional.
3.2 Unterschiede zwischen VB6 und VB.NET
85
VB6
VB.NET
Funktionsergebnis zurückgeben: Das Resultat einer Funktion wird durch die Zuweisung funktionsname=ergebnis bestimmt.
Die aus VB6 vertraute Syntax ist in VB.NET weiterhin möglich. Darüber hinaus kennt VB.NET nun aber das neue Schlüsselwort Return, mit dem das Funktionsergebnis angegeben werden kann. (Durch Return wird die Funktion gleichzeitig beendet. Return kann auch dazu verwendet werden, ein Unterprogramm zu beenden. Es hat dann dieselbe Wirkung wie Exit Sub.)
Statische Prozeduren: Wenn der Deklaration einer Prozedur das Schlüsselwort Static vorangestellt wird, gelten alle darin enthaltenen Variablen als statisch (behalten also ihren Wert bei mehreren Aufrufen der Prozedur).
In VB.NET kann Static zwar weiterhin zur Deklaration einzelner Variablen verwendet werden, nicht aber zur Deklaration ganzer Prozeduren.
And- und Or-Operatoren: Bei a Or b bzw. a And b wird b auch dann ausgewertet, wenn a wahr ist (Or) bzw. falsch ist (And). Wenn es darum geht, einen Wahrheitswert zu ermitteln, ist das sinnlos, weil die Auswertung von b in diesem Fall nichts mehr am Ergebnis ändern kann.
And und Or verhalten sich aus
Imp- und Eqv-Operatoren: Mit den Operatoren Imp und Eqv können die Implikation (wenn a wahr ist, muss auch b wahr sein) und die Äquivalenz von Wahrheitswerten getestet werden.
Die Operatoren Imp und Eqv stehen nur noch als Methoden der Bibliothek Microsoft.VisualBasic.Compatibility.dll zur Verfügung. Da die Dokumentation von der Verwendung dieser Bibliothek abrät, sollten Sie folgende Ersatzkonstruktionen verwenden:
Kompatibilitätsgründen weiterhin so wie bisher. Es gibt aber immerhin die neuen Operatoren AndAlso bzw. OrElse, die sich so verhalten, wie man das von logischen Operatoren aus allen anderen Programmiersprachen kennt.
a Imp b entspricht (Not a) Or b a Eqv b entspricht a = b
Set-Operator: Der Set-Operator wird Set gibt es nicht mehr, Objekte werden nun mit = zur Zuweisung von Objekten zugewiesen. verwendet (z.B. Set obj1 = obj2). Operatoren +=, -= etc.: VB6 kennt keine derartigen Operatoren.
In VB.NET können Sie Variablen durch x+=1, y-=2, z*=wert, s+="abc" bearbeiten. Das verringert den Schreibaufwand (und die Gefahr von Tippfehlern). Die von C bekannten Operatoren ++, -- etc. kennt VB.NET weiterhin nicht.
86
3.2.3
3 Von VB6 zu VB.NET
Bearbeitung von Zahlen und Zeichenketten
Arithmetische Funktionen VB6
VB.NET
Arithmetische Funktionen wie Sin, Cos etc. stehen ohne weiteres zur Verfügung.
Arithmetische Funktionen sind Teil der Systems.Math-Klasse. Statt Sin(x) müssen Sie jetzt Math.Sin(x) angeben. Außerdem haben einige Funktionen einen anderen Namen erhalten.
Bearbeitung von Daten und Zeiten VB6
VB.NET
Addition von Zeiten: Wegen der internen Darstellung durch DoubleWerte können Zeiten einfach mit dem Operator + addiert werden.
Die interne Darstellung von Date-Variablen hat sich geändert. Eine direkte Addition von Zeiten ist nicht mehr möglich, dafür gibt es aber diverse Add-Methoden. Ansonsten gibt es aber kaum Kompatibilitätsprobleme, d.h., alle aus VB6 vertrauten Funktionen funktionieren weiterhin. Darüber hinaus stehen zahllose neue Funktionen zur Verfügung.
Bearbeitung von Zeichenketten Obwohl sich der Umgang mit Zeichenketten auf den ersten Blick kaum geändert hat, gibt es fundamentale Änderungen unter der Oberfläche. VB6
VB.NET
Zeichenketten sind veränderlich. Das bedeutet, dass eine Zeichenkette verändert werden kann, ohne dass sich ihr Ort im Speicher ändert.
Zeichenketten sind unveränderlich. Bei jeder Änderung (s = s + "abc") wird eine neue Zeichenkette erzeugt und an einem anderen Ort im Speicher abgelegt (auch dann, wenn sich durch die Veränderung die Länge der Zeichenkette nicht ändert).
3.2 Unterschiede zwischen VB6 und VB.NET
87
VB6
VB.NET
Funktionen zur Bearbeitung von Zeichenketten: VB stellt eine Fülle von Funktionen bzw. Kommandos zur Manipulation von Zeichenketten zur Verfügung (Left, Mid, Len etc.).
Diese Funktionen stehen weiterhin zur Verfügung. Daneben gibt es aber eine Menge weiterer Funktionen, die als Eigenschaften oder Methoden von String-Variablen oder auch als eigenständige Funktionen verschiedener Namensräume genutzt werden können (System.String, System.Text.RegularExpression etc.). Bei der Verwendung dieser Funktionen ist zu beachten, dass das erste Zeichen mit dem Index 0 angesprochen wird, während bei herkömmliche VB-Funktionen der Index 1 gilt.
Variant- versus String-Funktionen: VB stellt fast alle Funktionen zur Bearbeitung von Zeichenketten doppelt zur Verfügung: Beispielsweise dient Left zur Bearbeitung von Variant-Variablen, Left$ zur Bearbeitung von String-Variablen.
Alle Zeichenkettenfunktionen, deren Name mit $ endet, wurden eliminiert. Verwenden Sie stattdessen die gleichnamigen Funktionen ohne $.
Funktion String: Diese Funktion ermöglicht die Vervielfältigung von Zeichenketten. String("ab", 3) liefert "ababab".
Diese Funktion gibt es nicht mehr. Wenn Sie nur ein einzelnes Zeichen (und nicht eine Zeichenkette) vervielfältigen möchten, können Sie New String oder StrDub verwenden: s = New String("x"c, 3) s = StrDup(3, "x")
Funktionen LenB, LeftB etc.: Diese Kommandos ermöglichen ab VB4 eine byteorientierte Bearbeitung von Zeichenketten.
3.2.4
Diese Funktionen gibt es ebenfalls nicht mehr. Sie können aber Zeichenketten mit den Methoden aus System.Text.Encoding in ein Byte-Feld umwandeln (siehe Abschnitt 8.2.5).
Windows-Anwendungen und Steuerelemente
Windows-Fenster und die meisten Steuerelemente basieren auf der neuen Windows.FormsBibliothek. Die Konsequenz lautet kurz gefasst, dass zwar vieles so aussieht wie früher, aber nur wenig noch so funktioniert. Neue Steuerelement-, Eigenschafts- und Methodennamen erschweren den Umstieg zusätzlich. Die folgende Beschreibung von Änderungen ist keinesfalls vollständig, sondern beschränkt sich auf die auffälligsten Details (bzw. auf Punkte, die am häufigsten Umstellungsschwierigkeiten verursachen).
88
3 Von VB6 zu VB.NET
Formulare (Fenster) VB6
VB.NET
Show vbModal: Mit dieser Methode
Statt Show vbModal muss nun die Methode ShowDialog verwendet werden, die dieselbe Wirkung hat, aber einen Ergebniswert zurückgibt. Dieser Wert kann durch die Eigenschaft DialogResult eingestellt werden.
kann ein Formular modal angezeigt werden, d.h., der Rest des Programms ist blockiert, bis dieses Formular wieder geschlossen wird. Programmende: Das Programm endet automatisch, wenn das letzte offene Fenster geschlossen wird.
Das Programm endet automatisch, wenn das Startfenster geschlossen wird. (Alle anderen offenen Fenster werden dann ebenfalls geschlossen. Tipps, wie Sie ein anderes Verhalten erzielen können, finden Sie in Abschnitt 15.3.
Schriftarten: VB6 unterstützt alle unter Windows zur Auswahl stehenden Schriftarten.
VB.NET (genau genommen das .NET-Grafiksystem GDI+) unterstützt nur noch TrueTypebzw. OpenType-Schriftarten. Die VB6Defaultschrift MS Sans Serif wird nicht mehr unterstützt. Verwenden Sie stattdessen Arial oder die namensähnliche Schrift Microsoft Sans Serif. (Details zum Umgang mit Schriften finden Sie in Abschnitt 16.3.)
Standardsteuerelemente In VB.NET gibt es die Unterscheidung zwischen Standard- und Zusatzsteuerelementen nicht mehr. Alle Steuerelemente sind gleichberechtigt. In diesem Abschnitt wird aber dennoch die aus VB6 bekannte Zweiteilung beibehalten, um den Abschnitt übersichtlicher zu halten. (Die weiteren Kapitel zu diesem Thema halten sich dann an die neue Kategorisierung der Steuerelemente.) VB6
VB.NET
CommandButton
Der CommandButton heißt nun einfach Button.
DriveListBox, DirListBox und FileListBox: Diese Steuerelemente
Die Steuerelemente werden unter .NET nicht mehr offiziell unterstützt, stehen aber als Teil der Bibliothek microsoft.visualbasic.compatibility weiterhin zur Verfügung. Die Dokumentation rät allerdings davon ab, diese Bibliothek in neuen Projekten zu verwenden.
zeigen Listenfelder mit Laufwerken, Verzeichnissen und Dateien an.
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
89
VB.NET
Frame: Das Steuerelement fasst
Statt des Frame-Steuerelements stehen zwei neue mehrere andere Steuerelemente (z.B. Steuerelemente zur Auswahl: GroupBox sieht aus Optionsfelder) zu einer Gruppe und funktioniert wie Frame. Panel kann im zusammen. Gegensatz zu Frame nicht beschriftet werden, kann dafür aber mit Scroll-Balken ausgestattet werden, um einen beliebig großen Innenbereich zu verwalten. Label: Der angezeigte Text wird über Der Text wird nun (wie bei allen anderen die (Default-)Eigenschaft Caption Steuerelementen) durch die Eigenschaft Text
gesteuert.
eingestellt.
ListBox, ComboBox: Die beiden
Der Zugang auf die Listenelemente erfolgt nun über die Items-Eigenschaft, die auf ein Objekt der Klasse ComboBox.- bzw. ListBox.ObjectCollection verweist. Listeneinträge werden mit Items.Add(...) oder Insert(...) eingefügt.
Steuerelemente dienen zur Anzeige von (ausklappbaren) Listen. Als Listeneinträge sind nur Zeichenketten zulässig, die über List(n) gelesen bzw. verändert werden können. Optional kann zu jedem Listenelement über die Aufzähleigenschaft ItemData(n) ein Variant-Objekt mit Zusatzinformationen gespeichert werden.
Image, PictureBox: Das Image-
Steuerelement dient zur Anzeige unveränderlicher Bitmaps. Im PictureBox-Steuerelement können dagegen eigene Grafiken dargestellt werden. Das PictureBox-Steuerelement kann auch als Container für andere Steuerelemente verwendet werden.
Als Listeneinträge sind beliebige Objekte erlaubt, wobei die Textrepräsentation (ToString-Methode) angezeigt wird. Da zusammen mit diesen Objekten beliebige Zusatzinformationen gespeichert werden können, ist DataItem nicht mehr erforderlich und wird nicht mehr unterstützt. Die Portierung von VB6-Code mit DataItem erfordert aber eine grundlegende Umstellung des Codes (Deklaration einer eigenen Klasse zur Speicherung der Zusatzinformationen). Es gibt nur noch das PictureBox-Steuerelement, das sowohl zur Grafikprogrammierung als auch zur Anzeige von Bitmaps verwendet werden kann. Die Grafikprogrammierung hat sich aber grundlegend gegenüber VB6 geändert. Einerseits gibt es zahllose neue Methoden, andererseits wird die praktische AutoRedraw-Eigenschaft nicht mehr unterstützt. Das PictureBox-Steuerelement ist nicht mehr als Container verwendbar. (Stattdessen können Sie das Panel- oder das GroupBox-Steuerelement verwenden.)
90
3 Von VB6 zu VB.NET
VB6
VB.NET
Menüs: Zur Gestaltung eigener Menüs bietet VB6 einen vollkommen veralteten und nicht intuitiven Editor an.
Die Verwaltung von Menüs hat sich intern wie extern stark verändert. Intern dienen die Klassen MainMenu, ContextMenu und MenuItem zur Verwaltung der Menüeinträge. Extern steht zur Gestaltung von Menüs nun endlich ein zeitgemäßer Editor zur Verfügung. Ziemlich ärgerlich bei der Programmierung ist der Umstand, dass die MenuItem-Klasse weder eine Name- noch eine Tag-Eigenschaft kennt. Eine große Enttäuschung ist auch das optische Erscheinungsbild: Menüs mit kleinen Icons, wie sie z.B. im Office-Paket seit Jahren üblich sind, können weiterhin nur mit großem Programmieraufwand realisiert werden.
OptionButton: Das Defaultereignis
(dessen Ereignisprozedur bei einem Doppelklick in der Entwicklungsumgebung eingefügt wird), lautet Click. Es wird bei einem Mausklick aufgerufen, also immer dann, wenn diese Option aktiviert wird. Timer: Das Steuerelement löst in
regelmäßigen Abständen ein Ereignis aus.
Der OptionButton heißt nun RadioButton. Das Defaultereignis lautet nun CheckedChanged und wird bei jeder Veränderung aufgerufen (also auch dann, wenn die Option deaktiviert wird, weil eine andere Option ausgewählt wurde!).
In VB.NET gibt es mehrere Möglichkeiten, Ereignisse periodisch auszulösen. Das TimerSteuerelement unterscheidet sich von der VB6Version vor allem dadurch, dass Interval=0 den Ereignisaufruf nicht stoppt, sondern als Interval=1 interpretiert (d.h. ein Ereignisaufruf pro Millisekunde!). Um die Timer-Ereignisse zu stoppen, muss Enabled auf False gesetzt werden.
Zusatzsteuerelemente VB6
VB.NET
Common Controls: Die drei CommonControls-Bibliotheken
Viele dieser Steuerelemente gibt es weiterhin, sie sind nun aber wie alle anderen Steuerelemente Teil der Windows.Forms-Bibliothek. Viele Steuerelemente haben neue Namen bekommen.
enthalten eine ganze Reihe von Zusatzsteuerelementen.
3.2 Unterschiede zwischen VB6 und VB.NET
91
VB6
VB.NET
CommonDialog: Mit diesem
Das CommonDialog-Steuerelement gibt es in seiner bisherigen Form nicht mehr. Dafür gibt es eine ganze Reiher neuer Steuerelemente (z.B. OpenFileDialog, FontDialog etc.), die dieselben Aufgaben übernehmen. Allerdings haben sich die Namen vieler Eigenschaften und Methoden geändert, so dass alter Code nicht mehr verwendet werden kann.
Steuerelement können in VB6 Auswahldialoge (für Dateien, Schriftarten, Drucker etc.) dargestellt werden.
DataGrid und MS[H]FlexGrid: Diese
Steuerelemente ermöglichen die Darstellung von Tabellen, wobei die Daten wahlweise von einer Datenbank stammen können.
Das vollkommen veränderte DataGridSteuerelement ersetzt gleichermaßen das alte DataGrid sowie MS[H]FlexGrid. Wie alle datenbanktauglichen .NET-Steuerelemente kann DataGrid ausschließlich mit ADO.NETDatenquellen umgehen, nicht aber mit ADO-, RDO- oder DAO-Datenquellen
DataList und DataCombo: Diese
In .NET sind bereits die Basissteuerelemente Steuerelemente ermöglichen die datenbanktauglich, d.h., die neue ListBox ersetzt Darstellung von Listen, die aus einer DataList und ComboBox ersetzt DataCombo. Datenbank stammen dürfen. DataReport: Dieses vollkommen
In .NET ist DataReport wieder verschwunden, unausgereifte Steuerelement war der stattdessen vertraut Microsoft nun wieder auf die Versuch Microsofts, ein eigenes Funktionen von Crystal Reports (Steuerelement Steuerelement für DatenbankCrystalReportViewer). berichte zu entwickeln. ImageList: Das Steuerelement dient
zur Aufbewahrung von mehreren gleich großen Bitmaps, die dann in anderen Steuerelementen (z.B. ListView, TreeView) angezeigt werden.
RichTextBox: Mit der Methode SelPrint kann der zuvor markierte
Text ausgedruckt werden.
Das Steuerelement funktioniert im Prinzip wie bisher, allerdings ist es nicht mehr möglich, mit Namen auf die Bitmaps zuzugreifen (d.h., die aus VB6 vertraute Key-Eigenschaft ist verschwunden). Stattdessen müssen Indexnummern verwendet werden (die sich aber ändern, wenn eine Bitmap aus dem Steuerelement entfernt wird). Der einzige Kommentar, der mir dazu einfällt: idiotisch! Das Steuerelement unterstützt keinen Ausdruck mehr. Nahezu alle Eigenschaften haben neue Namen.
92
3 Von VB6 zu VB.NET
VB6
VB.NET
StatusBar: Mit dem Steuerelement
Das Steuerelement existiert weiterhin, verwendet aber ein vollkommen neues Objektmodell. Die automatische Darstellung von Zeit, Datum, CapsLock und Überschreibmodus ist dem .NETUmstieg leider zum Opfer gefallen. (Der CapsLock-Status kann überhaupt nur durch den Aufruf der API-Funktion GetKeyState ermittelt werden. Hier gibt es noch Lücken im .NETFramework.)
kann eine Statuszeile angezeigt werden. Innerhalb dieser Zeile können unter anderem die Uhrzeit, das Datum, der CapsLock-Zustand und der Überschreibmodus angezeigt werden, wobei sich das Steuerelement selbst um die Aktualisierung dieser Werte kümmert. TabStrip, SSTab: Mit beiden
Steuerelementen können mehrblättrige Dialoge gebildet werden. (Das MultiPageSteuerelement der MS-FormsBibliothek bietet eine dritte Möglichkeit, die unter VB6 aber nur mit Einschränkungen funktioniert.) ToolBar: Mit dem Steuerelement
kann eine Symbolleiste definiert werden.
Das neue TabControl-Steuerelement vereint alle positiven Eigenschaften der drei nebenstehend erwähnten Steuerelemente: Es ist einfach zu bedienen, der Innenbereich jeder Dialogseite kann mit Scroll-Balken ausgestattet werden etc. Dass es damit aber auch zu allen drei genannten Steuerelementen inkompatibel ist, versteht sich gewissermaßen von selbst. Das Steuerelement existiert weiterhin, verwendet aber ein vollkommen neues Objektmodell. Im Vergleich zu VB6 gibt es als einziges neues Merkmal so genannte Dropdown-Buttons, nach deren Anklicken ein Menü erscheint. Dafür gibt es aber leider gleich mehrere Einschränkungen: Den einzelnen Buttons fehlt die Key-Eigenschaft, die bei der Identifizierung in der ClickEreignisprozedur hilfreich war. (Als Ersatz können Sie die Eigenschaft Tag verwenden.) Es gibt keine Möglichkeit mehr, andere Steuerelemente in die Symbolleiste einzufügen. Der Anwender hat im laufenden Programm keine Möglichkeit, die Symbolleiste zu verändern (d.h., es gibt keinen Ersatz für die VB6-Eigenschaft AllowCustomize).
Bei einer Reihe von Steuerelementen haben sich nicht nur viele Eigenschaften und Methoden geändert, sondern auch der Name des Steuerelements. Die folgende Tabelle hilft bei der Suche.
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
VB.NET
DTPicker
DateTimePicker
MonthView
MonthCalender
Slider
TrackBar
UpDown
NumericUpDown
93
Nicht mehr bzw. nur mehr eingeschränkt unterstützte Steuerelemente Grundsätzlich können die meisten herkömmlichen COM- bzw. ActiveX-Steuerelemente auch in VB.NET-Programmen verwendet werden. Insofern stehen also fast alle Steuerelemente weiterhin zur Verfügung. Allerdings ist die Verwendung von ActiveX-Komponenten in VB.NET mit Nachteilen verbunden: es gibt Sicherheitseinschränkungen, die Effizienz ist geringer als bei reinen .NET-Programmen und bisweilen treten Kompatibilitätsprobleme zwischen ActiveX und .NET auf. Aus diesen Gründen werden mit VB.NET fast keine COM- bzw. ActiveX-Steuerelemente mitgeliefert. Die einzigen Ausnahmen sind die Steuerelemente MSChart, Masked Edit und DBGrid (Details siehe in der folgenden Tabelle). Wenn Sie die anderen aus VB6 vertrauten Steuerelemente unter VB.NET nutzen möchten, muss entweder VB6 am selben Rechner installiert sein oder Sie müssen alle erforderlichen *.ocx- und *.dll-Dateien in das WindowsSystemverzeichnis kopieren (was aber fehleranfällig ist). Die ActiveX-Zusatzsteuerelemente müssen zur Verwendung in der Entwicklungsumgebung lizenziert sein. Das ist automatisch der Fall, wenn VB6 am selben Rechner installiert ist. Andernfalls muss die Datei extras\vb6 controls\vb6controls.reg ausgeführt werden. Diese Datei befindet sich auf der VS.NET-CD und enthält die Lizenzschlüssel für die Registrierdatenbank. Die folgende Liste zählt unter VB6 gebräuchliche Steuerelemente auf, zu denen es keine .NET-kompatible Version gibt. VB6
VB.NET
Masked Edit: Das Steuerelement ist eine Variante zur TextBox, mit der
Das Steuerelement steht in unveränderter Form als ActiveX-Steuerelement zur Verfügung einzelne Zeichen der Texteingabe (Name Microsoft Masked Edit Control, Datei besonders formatiert, verifiziert oder Windows\System32\msmask32.ocx). Bevor Sie das maskiert werden können. Steuerelement verwenden können, müssen Sie es selbst in die Toolbox einfügen. MSChart: Mit dem Steuerelement
Das Steuerelement steht ebenfalls als ActiveXkönnen Geschäftsdiagramme erstellt Steuerelement zur Verfügung (Microsoft Chart werden. Control, mschart20.ocx).
94
3 Von VB6 zu VB.NET
VB6
VB.NET
DBGrid: Dieses Steuerelement
Merkwürdigerweise steht auch dieses sehr alte Steuerelement als ActiveX-Steuerelement zur Verfügung. Es wird allerdings nicht automatisch installiert. Falls Sie das Steuerelement unter VB.NET verwenden möchten, kopieren Sie die *.ocx- und *.dll-Dateien aus dem Verzeichnis extras\vb6 controls der VS.NET-CD in das Windows-Systemverzeichnis. Anschließend fügen Sie das Steuerelement in die Toolbox ein.
stammt aus VB5 und dient zur tabellarischen Darstellung von Datenbankdaten. (Es wurde in VB6 durch DataGrid abgelöst.)
Datenbanksteuerelemente: VB6 kannte eine ganze Reihe spezieller Datenbanksteuerelemente für ADO.
Bei .NET gibt es keine Unterscheidung mehr zwischen gewöhnlichen und Datenbanksteuerelementen. Alle Steuerelemente, bei denen dies sinnvoll ist, sind automatisch datenbanktauglich (Eigenschaft DataBinding). Allerdings wird als Datenquelle nur ADO.NET unterstützt. Außerdem sind einige aus VB6 bekannte Datenbanksteuerelemente ersatzlos gestrichen worden (AdoDC, DataRepeater, DataReport).
Internet-Steuerelemente: Die Steuerelemente Inet, WinSock, MAPIMessage und MAPISession helfen bei der Herstellung von HTTP/TCP/UDP-Verbindungen und beim Versenden von E-Mails.
Diese Steuerelemente gibt es nicht mehr. Vergleichbare Funktionen finden Sie aber in den Namensräumen der .NET-Bibliothek (z.B. System.Net and System.Net.Sockets, System.Web.Mail).
WebBrowser: Dieses Steuerelement
Das ActiveX-Steuerelement kann wie alle anderen ActiveX-Steuerelemente weiterhin verwendet werden. Es gibt aber keine .NET-konforme Möglichkeit, den Internet Explorer zu steuern.
stammt aus einer Bibliothek, die zusammen mit dem Internet Explorer installiert wird. Das Steuerelement konnte in VB6 dazu verwendet werden, um HTMLDokumente anzuzeigen. MS-Forms-Steuerelemente: Diese Steuerelemente gehören ebenfalls nicht direkt zu VB6, sondern werden als Bestandteil des Internet Explorers bzw. des Office-Pakets mitgeliefert. Sie können unter VB6 mit Einschränkungen verwendet werden.
Eine Verwendung unter .NET ist ebenfalls nur mit Einschränkungen möglich, weil die MS-FormsSteuerelemente auf ActiveX-Technologie basieren und zum Teil Kompatibilitätsprobleme auftreten. Die Steuerelemente bieten aber ohnedies kaum Funktionen, die die normalen .NETSteuerelemente nicht ebenfalls bieten.
3.2 Unterschiede zwischen VB6 und VB.NET
95
VB6
VB.NET
Windowless-Steuerelemente: Das sind besonders ressourcensparende Versionen der Standardsteuerelemente.
.NET unterstützt offensichtlich keine WindowlessSteuerelemente. (Auf jeden Fall gibt taucht der Begriff windowless in der gesamten .NETFramework-Dokumentation nur ein einziges Mal auf und da in einem anderen Kontext.)
Sonstige Steuerelemente: Adodc,
Diese Steuerelemente stehen unter VB.NET nicht mehr zur Verfügung. Zum Teil ist das ein echter Verlust an Funktionen (z.B. bei MSComm), zum Teil gibt es aber äquivalente Funktionen in den bereits erwähnten .NET-Steuerelementen oder aber in verschiedenen .NET-Bibliotheken.
Animation, CoolBar, DataRepeater, FlatScrollBar, Image, ImageCombo, Line, MMControl, MSComm, OLEFeld, PictureClip, Shape und SysInfo
Unsichtbare Steuerelemente In VB6 gibt es Steuerelemente, die eigentlich gar keine richtigen Steuerelemente sind. Derartige Steuerelemente können nicht direkt in ein Formular eingefügt werden und bleiben daher auch bei der Programmausführung im Formular unsichtbar. (Manche dieser Steuerelemente zeigen sich bei ihrer Anwendung immerhin durch eigene Dialogboxen.) Beispiele für derartige Steuerelemente sind das CommonDialog-Steuerelement zur Darstellung von Auswahldialogen, das ImageList-Steuerelement zur Verwaltung von Bitmap-Dateien oder das WinSock-Steuerelement zur Herstellung einer TCP- oder UDP-Netzwerkverbindung zwischen zwei Programmen etc. In VB.NET scheint es derartige Steuerelemente ebenfalls zu geben. So finden Sie in der Toolbox Steuerelemente wie OpenFileDialog, FontDialog, PrintDocument etc., die ebenfalls nicht direkt in ein Formular eingefügt werden können und somit im Formular unsichtbar bleiben. Im Gegensatz zu VB6 handelt es sich dabei aber um ganz gewöhnliche Klassen, die nur von der Entwicklungsumgebung so behandelt werden, als wären sie Steuerelemente. Das nimmt Ihnen die Arbeit ab, ein Objekt dieser Klasse selbst zu erzeugen (und vermindert die Umstiegsprobleme). Anders als in VB6 können Sie die VB.NET-Pseudosteuerelemente aber auch ohne den Umweg über ein Formular direkt per Code erzeugen und anwenden. Manche aus VB6 vertrauten unsichtbaren Steuerelemente stehen in VB.NET nicht mehr zur Verfügung. Beispielsweise gibt es kein direktes Gegenstück zum WinSock-Steuerelement. Vergleichbare Funktionen stehen unter .NET aber sehr wohl zur Verfügung (nämlich durch die Klassen System.Net.* und System.Net.Sockets.*).
Neue Steuerelemente Mit VB.NET sind manche Steuerelemente verschwunden, aber es gibt auch einige neue. Die folgende Liste zählt die wichtigsten davon auf:
96
•
3 Von VB6 zu VB.NET
CheckedListBox: Diese Variante zur ListBox stattet jedes Listenelement mit einem Kon-
trollkästchen aus. Das Steuerelement eignet sich damit hervorragend, wenn Optionen aus einer umfangreichen Liste ausgewählt werden sollen. •
DomainUpDown: Das Steuerelement ermöglicht die Auswahl einer Zeichenkette aus einer vorgegebenen Liste. Die Funktionsweise ist ähnlich wie bei einer ComboBox, allerdings gibt es zwei Pfeilbuttons zur Auswahl des vorigen oder nächsten Listeneintrags.
•
ErrorProvider: Das Steuerelement zeigt neben einem Steuerelement mit einem Eingabefehler ein kleines rotes Icon mit einem Ausrufezeichen an.
•
HelpProvider: Das Steuerelement ermöglicht das automatische Anzeigen eines Hilfetexts, wenn der Anwender F1 drückt.
•
LinkLabel: Das Steuerelement enthält einen Link, z.B. eine Web- oder E-Mail-Adresse oder einen Link auf eine lokale Datei. Beim Anklicken erscheint der Standard-Browser bzw. ein Mail- oder News-Programm.
•
NotifyIcon: Das Steuerelement ermöglicht die Anzeige eines Icons im Icon-Bereich der Taskbar. Das ist vor allem für Hintergrundprogramme praktisch, deren Benutzerober– fläche normalerweise unsichtbar ist.
•
Panel: Das Steuerelement dient als Container für andere Steuerelemente. Anders als GroupBox enthält es keine Beschriftung und ist durchsichtig. Dafür kann der Innenbe-
reich mit Scroll-Balken ausgestattet werden. •
Splitter: Das Steuerelement dient als Fensterteiler. Es wird als verschiebbare Linie (bzw. als schmaler Balken) zwischen zwei angedockten Steuerelementen angezeigt (wie beim Windows-Explorer zwischen dem Verzeichnisbaum und der Detailansicht der einzelnen Dateien).
•
ToolTip: Das Steuerelement verwaltet die ToolTip-Texte aller anderen Steuerelemente im
Formular. •
Steuerelemente zur Steuerung eines Ausdrucks: Die Klasse PrintDocument sowie die Steuerelemente PrintDialog, PageSetupDialog, PagePreviewDialog sowie PagePreviewControl helfen dabei, den Code zum Ausdruck von Dokumenten mit einer ansprechenden Benutzeroberfläche zu versehen.
•
Sonstige neue Komponenten: Dieser Abschnitt beschreibt nur Steuerelemente zur Windows-Programmierung. Darüber hinaus bietet .NET in der Toolbox aber zahlreiche weitere neue Komponenten, die z.B. zur Entwicklung von Server-, Datenbank- oder ASP.NET-Anwendungen geeignet sind. Bei vielen dieser Komponenten handelt es sich um gewöhnliche .NET-Klassen. Ihre Platzierung in der Toolbox erleichtert sowohl die Suche als auch die Anwendung ein klein wenig, weil die Initialisierung (Dim x As New klassenname) und Einstellung von Eigenschaften quasi automatisch – d.h. in einem von der Entwicklungsumgebung erzeugten Codeabschnitt – erfolgt.
3.2 Unterschiede zwischen VB6 und VB.NET
97
Geänderte Eigenschaften, Methoden, Ereignisse Der Platz in diesem Buch reicht nicht aus, um sämtliche geänderten Eigenschaften, Methoden und Ereignisse aller Steuerelemente aufzulisten. Die Grundformel lautet leider: Mindestens die Hälfte aller Namen von Schlüsselwörtern haben sich geändert; in vielen Fällen hat sich auch das Verhalten, das Datenformat etc. geändert! Dieser Abschnitt beschränkt sich daher auf einige fundamentale Änderungen bzw. Neuerungen, die für die meisten Steuerelemente relevant sind: •
Defaulteigenschaften: In VB6 konnte die wichtigste Eigenschaft eines Steuerelements ohne explizite Nennung verwendet werden. Text1="abc" veränderte etwa die TextEigenschaft einer Textbox. Die .NET-Steuerelemente kennen allerdings generell keine Defaulteigenschaften mehr. Statt Text1="abc" müssen Sie jetzt Text1.Text="abc" schreiben.
•
Positionierung von Steuerelementen: Um Steuerelemente bei einer Änderung der Fenstergröße neu zu positionieren, musste in VB6 die Form_Resize-Ereignisprozedur verwendet werden. In VB.NET ist das aufgrund der neuen Eigenschaften Anchor und Dock nur noch ganz selten erforderlich. Anchor ermöglicht es, den Abstand eines Steuerelements zu den Fensterrändern zu fixieren. Per Default ist dies nur für den linken und oberen Abstand der Fall, d.h., die Steuerelemente verhalten sich wie gewohnt. Indem auch die beiden anderen Ränder einbezogen werden, sind aber Steuerelemente möglich, die sich ohne Zusatzcode automatisch an die Fenstergröße anpassen bzw. die am rechten oder unteren Fensterrand scheinbar kleben bleiben.
Eine ähnliche Wirkung hat Dock: Damit kann jedes Steuerelement an einen der vier Ränder eines Fensters gebunden (angedockt) werden. Es nimmt dort automatisch die volle Fensterbreite bzw. -höhe an. •
HelpXxx-Eigenschaften: In VB6 war (fast) jedes Steuerelement mit einigen HelpXxxEigenschaften ausgestattet, die angaben, welches Hilfedokument durch F1 angezeigt
werden sollte. In VB.NET fehlen diese Eigenschaften, dafür gibt es ein neues HelpProvider-Steuerelement, das die Verbindung zwischen den Steuerelementen herstellt. Die Anwendung des HelpProviders ist nicht wesentlich komplizierter, allerdings ist es nicht mehr möglich, ein konkretes Hilfedokument durch dessen ID-Nummer zu identifizieren (d.h., es gibt kein Analogon zur VB6-Eigenschaft HelpContextID). Der Umstand, dass mit VB.NET eine im Vergleich zu VB6 beinahe unveränderte Version des HTML-Help-Workshops (Copyright 1998!) mitgeliefert wird, beweist, dass Microsoft das Thema eigene Hilfe auch in .NET stiefmütterlich behandelt. Die einzigen, die sich darüber freuen dürfen, sind die Anbieter so genannter Help-Authoring-Systeme ... •
Tag-Eigenschaft/Vererbung: In VB6 bot die Tag-Eigenschaft der meisten Steuerelemente die Möglichkeit, zusammen mit dem Steuerelement eine Zeichenkette zu speichern.
98
3 Von VB6 zu VB.NET
In VB.NET existiert diese Eigenschaft weiterhin. Sie kann nun Objekte beliebiger Klassen speichern. In vielen Fällen ist eine Verwendung von Tag aber gar nicht mehr notwendig. Viel eleganter ist es, durch Vererbung einfach ein neues Steuerelement zu deklarieren, das zusätzliche Eigenschaften und Methoden zur Verwaltung interner Daten hat. •
ToolTip-Eigenschaft: In VB6 hatten die meisten Steuerelemente eine ToolTip-Eigenschaft, mit der ein Infotext eingestellt werden konnte. Dieser Text erscheint automatisch in einer kleinen gelben Box, wenn die Maus eine Weile über dem Steuerelement verharrt.
In VB.NET fehlt die ToolTip-Eigenschaft bei den meisten Steuerelementen, dafür gibt es aber ein neues ToolTip-Steuerelement. Um ToolTips anzuzeigen, muss für jedes Steuerelement die Anweisung tooltipObject.SetToolTip(steuerelement, "mein Tooltip-Text") ausgeführt werden. Mit anderen Worten: ToolTips werden weiterhin unterstützt, die Initialisierung ist aber ungleich komplizierter als bisher und kann nur per Code erfolgen (nicht im Eigenschaftsfenster). Irgendein Vorteil dieser Änderung ist nicht zu erkennen. •
Steuerelementfelder: VB6 bot die Möglichkeit, mehreren Steuerelementen denselben Namen zu geben. Diese Steuerelemente konnten weiterhin über eine Indexnummer unterschieden werden. Der Vorteil dieser Vorgehensweise: Die Steuerelemente konnten mit gemeinsamen Ereignisprozeduren ausgestattet werden, in Schleifen gemeinsam manipuliert werden etc. In VB.NET sind Steuerelementfelder in dieser Form leider ersatzlos verschwunden. Zwar können Sie weiterhin über Controls auf alle Steuerelemente eines Formulars zugreifen und per Code mehrere Steuerelemente mit einer gemeinsamen Ereignisprozedur ausstatten – aber die Eleganz und Einfachheit von VB6 ersetzt das nicht. (Tipps, wie Sie mehrere gleichartige Steuerelemente effizient verwalten, finden Sie in Abschnitt 14.11.)
MDI-Anwendungen MDI-Anwendungen (zur Realisierung des multiple document interface) werden im Prinzip wie in VB6 unterstützt, auch wenn die Methoden und Eigenschaften zur Verwaltung der Fenster neue Namen bekommen haben. Insbesondere wird das Hauptfenster nun durch IsMdiContainer = True gekennzeichnet; bei den Subfenstern muss MdiParent auf das Hauptfenster verweisen. Vollkommen geändert hat sich allerdings die Menüverwaltung bei MDI-Anwendungen: Nach wie vor können sowohl das Haupt- als auch das Subfenster mit eigenen Menüs ausgestattet werden, und nach wie vor wird das Menü ausschließlich im Hauptfenster angezeigt. Damit enden die Gemeinsamheiten aber schon. Nun zu den Unterschieden: •
In VB6 war es so, dass das Hauptfenstermenü nur so lange angezeigt wurde, bis zumindest ein Subfenster mit einem eigenen Menü geöffnet wird. Ab diesem Zeitpunkt wurde im Hauptfenster nur noch das Menü des (gerade aktiven) Subfensters angezeigt.
3.2 Unterschiede zwischen VB6 und VB.NET
•
99
In .NET werden die Menüs von Haupt- und Subfenster dagegen kombiniert: Im Hauptfenster werden zuerst die Einträge des Hauptfensters, daran anschließend die Einträge des gerade aktiven Subfensters angezeigt.
Änderungen bei anderen Aspekten der Windows-Programmierung VB6
VB.NET
Tastatureingaben: In VB6 besteht die Möglichkeit, in der KeyPressProzedur den Code des eingegebenen Zeichens zu verändern (also beispielsweise ein x durch ein y zu ersetzen).
Diese Möglichkeit gibt es in .NET in dieser Form leider nicht mehr. Dasselbe Ziel kann über Umwege aber dennoch erreicht werden (z.B. durch die direkte Veränderung der TextEigenschaft des Steuerelements oder unter Zuhilfenahme von SendKeys.)
Tastatureingaben simulieren: Zur Simulation von Tastatureingaben steht die SendKeys-Methode zur Verfügung.
Statt SendKeys("xxx") müssen Sie nun SendKeys.Send("xxx") ausführen. Durch einen optionalen Parameter können Sie eine sofortige Verarbeitung der Eingabe erreichen.
Zwischenablage: VB6 stellt zum Zugriff auf die Zwischenablage das Clipboard-Objekt zur Verfügung.
Die Clipboard-Klasse von .NET ist zwar vollständig inkompatibel mit dem aus VB6 vertrauten Clipboard-Objekt, bietet aber im Wesentlichen dieselben Möglichkeiten.
Drag&Drop: In VB6 gibt es verschiedene Drag&Drop-Varianten. Drag&Drop zum Verschieben ganzer Steuerelemente innerhalb des Programms, automatisches OLEDrag&Drop zum Verschieben von Daten aus Steuerelementen sowie manuelles OLE-Drag&Drop, mit dem der gesamte Prozess des Datenaustauschs kontrolliert werden konnte.
Mit .NET ist auch hier alles anders geworden. Die drei links beschriebenen Varianten sind zu einem Drag&Drop-Verfahren vereinheitlicht worden, das am ehesten dem manuellen OLE-Drag&Drop entspricht. Diese Vereinheitlichung ist an sich erfreulich. Positiv ist auch der Umstand, dass der Datenaustausch nun dieselben Mechanismen wie beim Umgang mit der Zwischenablage verwendet. Mit dem neuen Drag&Drop lassen sich allerdings nicht alle Effekte der drei ehemaligen Drag&Drop-Varianten erzielen. Beispielsweise funktioniert Drag&Drop innerhalb eines Textfelds nicht mehr. Auch der Programmieraufwand ist zum Teil höher als bisher.
100
3.2.5
3 Von VB6 zu VB.NET
Grafikprogrammierung und Drucken
VB6
VB.NET
VB6 bietet zur Grafikprogrammierung einige einfache Steuerelemente (Shapes) sowie einige rudimentäre Methoden (Cls, Line, Circle etc.). Soweit Sie mit diesen Elementen das Auslangen finden, ist die Grafikprogrammierung relativ einfach.
Die Grafikprogrammierung in VB.NET ist zu 100 Prozent inkomaptibel zu VB6. Die vertrauten Steuerelemente funktionieren anders bzw. es gibt sie nicht mehr.
Sehr oft ist es aber erforderlich, auf GDI-Funktionen des Betriebssystems auszuweichen – und dann wird die Grafikprogrammierung mühsam. (GDI steht für Graphics Device Interface.)
Dafür stehen Ihnen in VB.NET zahllose System.Drawing-Klassen zur Verfügung, die das Fundament für das neue Grafiksystem GDI+ bilden (das zum alten GDI natürlich ebenfalls vollständig inkompatibel ist). VB.NET bietet damit neue Möglichkeiten zur Grafikprogrammierung, die in praktisch jeder Hinsicht besser sind als die von VB6 – mehr dazu in Kapitel 16. Allerdings vergessen Sie am besten vorher alles, was Sie bisher zum Thema Grafikprogrammierung wussten!
Drucken VB6
VB.NET
Die Druckerunterstützung in VB6 ist miserabel. Die Print-Klasse ist für professionelle Anwendungen zu wenig leistungsstark; die für Datenbankanwendungen mitgelieferte Version von Crystal Reports vollkommen veraltet und ADO-inkompatibel.
VB.NET stellt in dieser Hinsicht eine große Verbesserung dar. Die unselige Print-Klasse gibt es nicht mehr (und keiner wird ihr eine Träne nachweinen). Stattdessen gibt es nun die PrintDocument-Klasse, die den Druckvorgang über vier Ereignisse steuert, zahlreiche weitere Klassen zur Verwaltung von Druckern und ihren Eigenschaften sowie einige neue Steuerelemente zur Auswahl des Druckers, zur Einstellung des Seitenlayouts und zur Durchführung einer Seitenvorschau. Die mit VS.NET Professional und Enterprise mitgelieferte Version von Crystal Reports ist aktuell und erstmals ordentlich in die Entwicklungsumgebung integriert.
PrintForm: Mit dieser Methode
PrintForm steht leider nicht mehr zur Verfügung.
können Sie in VB6 das Formular als Bitmap ausdrucken.
Wie Sie das Formular dennoch ausdrucken können, zeigt ein Beispielprogramm in Abschnitt 16.5.10.
3.2 Unterschiede zwischen VB6 und VB.NET
3.2.6
101
Umgang mit Verzeichnissen und Dateien
VB6
VB.NET
Zum Zugriff auf Verzeichnisse und Dateien können wahlweise die aus früheren VB-Versionen vertrauten Kommandos (FileCopy, Open etc.) oder die in VB6 eingeführte FSOBibliothek verwendet werden.
Zum Umgang mit Verzeichnissen und Dateien sind die System.IO-Klassen vorgesehen, die in Kapitel 10 ausführlich beschrieben werden. Daneben können aber sowohl die alten Dateikommandos als auch die Objekte der FSOBibliothek (COM) weiterhin benutzt werden. Die VB6-Kommandos Get und Put haben neue Namen bekommen (FileGet und FilePut), funktionieren aber sonst wie gewohnt. App.Path zur Ermittlung des aktuellen
Verzeichnisses steht nicht mehr zur Verfügung. Dafür gibt es aber diverse neue Wege, um das aktuelle Verzeichnis und andere spezielle Verzeichnisse (das Windows-Verzeichnis etc.) zu ermitteln – siehe Abschnitt 10.3.4. ChDir ändert nur das aktuelle
ChDir ändert nun auch das Laufwerk (wenn dieses
Verzeichnis, nicht aber das Laufwerk.
angegeben wird). Im Gegensatz zu früher ist es nicht mehr erforderlich, dazu auch ChDrive auszuführen.
3.2.7
Fehlerabsicherung und Debugging
On Error: Die aus VB6 vertrauten On-Error-Anweisungen stehen in VB.NET weiterhin zur Verfügung. Besser lesbaren Code erhalten Sie aber, wenn Sie die neue Try-Catch-Konstruktion zur Fehlerabsicherung verwenden. (Es ist nicht möglich, On Error und Try Catch innerhalb einer Prozedur zu kombinieren.) Ausnahmen (exceptions): Auch wenn es On Error noch immer gibt – hinter den Kulissen hat sich viel geändert. Insbesondere werden Fehler nun durch so genannten Exceptions zwischen verschiedenen Programmteilen weitergereicht. Exceptions basieren auf einer neuen .NET-Klasse; die davon abgeleiteten Objekte helfen, die Natur des Fehlers genau zu beschreiben. Debugger: Im Zuge der Erneuerung der Entwicklungsumgebung wurden natürlich auch alle Debugging-Elemente erneuert. Die meisten Bedienungselemente – etwa zur Verwaltung von Haltepunkten, zum Analysieren von Variablen etc. – sind aber ganz ähnlich wie bisher zu bedienen und wurden in vielen Details verbessert. Eine der wichtigsten Neuerungen besteht im Thread-Fenster, das auch in Multithreading-Programmen die Fehlersuche möglich macht.
102
3 Von VB6 zu VB.NET
Direktfenster: Einer der wenigen Punkte, wo Sie bei der neuen Debugging-Umgebung Einbußen im Vergleich zu VB6 hinnehmen müssen, ist das Direktfenster. Dieses Fenster heißt jetzt Befehlsfenster (ANSICHT|ANDERE FENSTER) und steht leider nur noch in einer sehr eingeschränkten Form zur Verfügung. Zwar können Sie noch immer Variablen anzeigen und verändern, allerdings lässt der PrintOperator ? nur noch die Angabe eines einzigen Ausdrucks zu. Komplexe Operationen (etwa das Ausführen einer Schleife, um alle Elemente eines Felds anzuzeigen) sind nicht mehr möglich. Die automatische Vervollständigung von Eigenschaften und Methoden (Intellisense) funktioniert ebenfalls nicht mehr. Generell stehen viele Funktionen erst dann zur Verfügung, wenn gerade ein Programm ausgeführt wird (das momentan unterbrochen ist). Dafür kennt das Befehlsfenster jetzt einen neuen Befehlsmodus. In diesem Modus können Sie Befehle zur Steuerung der Entwicklungsumgebung ausführen. Beim Schreiben dieses Buchs habe ich diese neue Funktion allerdings kein einziges Mal benötigt. Codeänderungen während des Debuggings: Dramatische Veränderungen gibt es leider, was die Veränderung des Codes während der Programmausführung betrifft. Anders als in VB6 ist es nicht mehr möglich, beim Auftreten eines Fehlers den Code zu ändern und das Programm dann fortzusetzen. Stattdessen muss das Programm beendet und anschließend neu gestartet werden. Das Fehlen dieses VB6-Features zählt zu den größten Quellen von Ärger und Wut unter eingefleischten VB6-Fans. Entsprechende Diskussionen in den vb.net-Diskussionsforen haben epische Längen erreicht. Selbst Microsoft hat zugegeben, dass dies ein Rückschritt sei und dass es Überlegungen gäbe, dieses Feature in kommenden Versionen wieder einzuführen. Ob Microsoft diesen edlen Vorsatz verwirklichen kann, ist allerdings nicht so sicher: VB.NET ist zu einer echte Compiler-Sprache geworden, was viele Vorteile, aber auch manche Nachteile mit sich bringt. Und ein Nachteil besteht eben darin, dass Änderungen in einem bereits compilierten Programm an sich unmöglich sind, ohne das Programm zu verändern. Wenn Microsoft das Unmögliche doch zustande bringt, dann nur durch massive Veränderungen innerhalb der .NET-Infrastruktur und insbesondere im MSIL-Compiler; derartige Änderungen wären vermutlich mit neuen Inkompatibilitäten verbunden, und die kann sich Microsoft nach der mühsamen Migration von VB6 zu VB.NET in naher Zukunft eigentlich nicht leisten. Meine pragmatische Empfehlung lautet daher: Gewöhnen Sie sich ein exakteres Programmieren an! VB6 hatte bei vielen VB-Programmierern den Effekt, dass Code recht flott geschrieben und getestet wurde; wenn er dann – beinahe erwartungsgemäß – nicht funktionierte, wurde er im laufenden Programm optimiert. (Man kann diese Art der Programmentwicklung auch mit rapid prototyping umschreiben – dann klingt es weniger negativ.)
3.2 Unterschiede zwischen VB6 und VB.NET
VERWEIS
3.2.8
103
Datenbank- und Internet-Programmierung
Datenbank- und Internet-Anwendungen sind aus Platzgründen nicht Thema dieses Buchs, sondern werden in einem eigenen Buch beschrieben. Aus diesem Grund sind die in diesem Abschnitt zusammengestellten Informationen eher knapp und grundsätzlich gehalten. Informationen über den Inhalt und den Erscheinungstermin des neuen Buchs finden Sie auf meiner Website unter http://www.kofler.cc.
Von ADO zu ADO.NET Die große Neuerung von VB6 im Datenbankbereich war die Datenbankbibliothek ADO (ActiveX Data Objects). Während die mit VB6 ursprünglich mitgelieferte Version noch unausgegoren war, hat sich das im Laufe mehrere Service Packs und neuer ADO-Versionen allmählich zum Besseren gewandt und ADO ist gewissermaßen erwachsen geworden. Mit VB.NET gehört ADO allerdings schon wieder zum alten Eisen. Die neue Zauberformel lautet nun ADO.NET. Obwohl das Kürzel ADO noch immer ActiveX-Technologie vermuten lässt, ist ADO.NET eine komplette, .NET-konforme Neuentwicklung. ADO.NET ist Teil des .NET-Frameworks und wird über zahlreiche Klassen der Bibliothek System.Data verwendet. Einmal abgesehen davon, dass sich fast alle Namen von Schlüsselwörtern geändert haben und zahllose neue Funktionen dazugekommen sind, hat auch ein fundamentaler Paradigmenwechsel stattgefunden: •
Bei ADO war es so, dass zuerst eine Datenbankverbindung hergestellt wurde und diese dann für längere Zeit geöffnet blieb, um Datenbankabfragen auszuführen, neue Daten in der Datenbank zu speichern etc.
•
ADO.NET ist dagegen verbindungslos. Wenn Daten bearbeitet werden sollen, wird kurzzeitig eine Verbindung zur Datenbank hergestellt und die Daten werden an den Client übertragen. Anschließend wird die Verbindung sofort wieder beendet. Nun können die Daten am Client bearbeitet werden. Wenn irgendwelche Änderungen gespeichert werden sollen, dann muss neuerlich eine Verbindung hergestellt werden.
Beide Vorgehensweisen haben Vor- und Nachteile. Der Vorteil von ADO besteht darin, dass Änderungen in der Datenbank beinahe sofort auch im Client-Programm bemerkbar werden (das Recordset-Objekt kann also aktualisiert werden). Wenn mehrere Anwender gleichzeitig gemeinsame Daten bearbeiten, kann das eine wesentliche Hilfe sein. (Wenn Anwender A den letzten freien Platz für einen Flug reserviert hat, bemerkt Anwender B dies sofort – vorausgesetzt die Anwendung wurde entsprechend programmiert.)
104
3 Von VB6 zu VB.NET
Der Vorteil von ADO.NET besteht darin, dass mit Datenbankverbindungen viel sparsamer umgegangen wird. Derartige Verbindungen sind ein kostbares Gut; sie beanspruchen am Datenbank-Server Speicher und Rechenzeit. Je kürzer Datenbankverbindungen aufrechterhalten werden, desto mehr Anwender können gleichzeitig auf die Datenbank zugreifen und desto effizienter können diese Zugriffe verarbeitet werden. (Für das obige Beispiel einer Flugreservierung bedeutet das, dass Anwender B beim Versuch, den letzten Flug ebenfalls zu reservieren, eine Fehlermeldung erhält – also zu einem viel späteren Zeitpunkt als bei einem entsprechenden ADO-Programm.) Vereinfacht könnte man sagen, dass das Programmiermodell von ADO.NET dem Gedanken des Internets näher kommt und sich daher besonders gut zur Programmierung von Internet-Anwendungen (ASP.NET, Webservices) eignet. ADO spielt seine Vorteile dagegen eher dann aus, wenn herkömmliche, interaktive Windows-Anwendungen entwickelt werden sollen. Microsoft sieht ADO.NET nicht als Ersatz für ADO, sondern als Ergänzung. Und tatsächlich kann die ADO-Bibliothek wie jede ActiveX-Bibliothek unter VB.NET weiter verwendet werden. Wenn Sie nun aber versuchen, ADO in Ihren VB.NET-Programmen zu nutzen, werden Sie rasch bemerken, dass die grundsätzliche Kompatibilität Ihnen kaum weiterhilft: So sind die neuen Windows.Forms-Steuerelemente grundsätzlich nicht ADO-kompatibel. (Und wie bereits erwähnt, ist ADO speziell für gewöhnliche Windows-Anwendungen geeignet.) Die unter VB6 übliche Schreibweise recordsetobject!spaltenname ist nicht mehr zulässig; stattdessen müssen Sie jetzt recordsetobject.Fields("spaltenname") schreiben. Die Liste der Probleme ließe sich fortsetzen, das Fazit lautet aber kurz und bündig: Wenn Sie bei ADO bleiben möchten (dafür gibt es bei bestimmten Anwendungsszenarien gute Gründe), sollten Sie in den meisten Fällen auch gleich bei VB6 bleiben. VB.NET bietet für Datenbankentwickler viele neue und tolle Funktionen – aber diese Funktionen stehen nur dann zur Verfügung, wenn ADO.NET eingesetzt wird. ADO-Anwender gewinnen durch den Umstieg auf VB.NET dagegen so gut wie nichts, müssen aber eine Menge Kompatibilitätsprobleme in Kauf nehmen.
Entwicklung von dynamischen Webseiten (ASP.NET) Beginnend mit VB4 hat Microsoft mit jeder neuen Version eine neue Technologie eingeführt, so dass unter VB6 schließlich die folgenden Technologien unterstützt wurden. •
ActiveX-Dokumente
•
ActiveX-Steuerelemente
•
DHTML-Anwendungen
•
WebClasses (IIS-Anwendungen)
•
ASP-Seiten
Wirklich durchgesetzt haben sich davon einzig ASP-Seiten – und ausgerechnet die hatten eigentlich wenig mit dem klassischen VB zu tun. ASP-Seiten enthalten nämlich nur VBScript-Code, der in der VB6-Entwicklungsumgebung weder entwickelt noch ausgeführt
3.2 Unterschiede zwischen VB6 und VB.NET
105
werden kann. (Allerdings können in ASP-Seiten ActiveX-Komponenten genutzt werden, die wiederum mit VB6 entwickelt werden können.) In VB.NET gibt es keine einzige der fünf genannten Technologien mehr! Stattdessen wurde mit ASP.NET eine weitere Technologie eingeführt, die die Vorteile von herkömmlichen ASP-Seiten mit denen von WebClasses verbindet (siehe auch Abschnitt 2.5). Mit ASP.NET ist Microsoft ohne Zweifel ein großer Wurf gelungen, nur dürfen Sie eben nicht erwarten, dass Sie auch nur eine einzige Zeile Code aus einem mit VB6 entwickelten Webprojekt übernehmen können – ganz egal, auf welche der vielen Technologien Sie gesetzt haben.
Web-Services Web-Services sind eine weitere Neuerung in .NET. Sie ermöglichen eine standardisierte Kommunikation zwischen Internet-Servern über das HTTP-Protokoll, aber ohne die Verwendung von HTML-Dokumenten. (Die Daten werden vielmehr im XML-Format ausgetauscht.)
XML-Funktionen XML ist ein strukturiertes Textformat, das vor allem zum Austausch von Daten im Internet eingesetzt wird. Die .NET-Bibliothek System.XML bietet zahllose Klassen zum Erzeugen und Verarbeiten von XML-Daten. Diese Klassen können Sie natürlich auch für herkömmliche Anwendungen oder für Datenbankprogramme einsetzen.
HTML-Dokumente anzeigen, E-Mails verwalten In VB6 konnten Sie mit dem vom Internet Explorer stammenden WebBrowser-Steuerelement HTML-Dokumente anzeigen und mit den MAPISession- bzw. MAPIMessage-Steuerelementen E-Mails versenden. Unter VB.NET können Sie das WebBrowser-Steuerelement als ActiveX-Steuerelement weiterverwenden, es gibt aber keine eigene .NET-Variante davon. Die MAPI-Steuerelemente wurden ersatzlos gestrichen. Methoden zum Versenden von E-Mails finden Sie in den Klassen System.Web.Mail.* (Bibliothek System.Web).
Low-Level-Programmierung Wenn Sie Daten direkt auf Basis der Protokolle HTTP, FTP, TCP, UDP etc. austauschen möchten, standen Ihnen dazu in VB6 die Steuerelemente WinSock und Inet zur Verfügung. Diese Steuerelemente gibt es nicht mehr, dafür enthält die .NET-Bibliothek aber zahllose Klassen, die zum selben Zweck viel mehr und vor allem viel besser durchdachte Funktionen zur Verfügung stellen. Alten Code können Sie freilich auch hier nicht einmal ansatzweise übernehmen.
106
3.2.9
3 Von VB6 zu VB.NET
Sonstiges
VB6
VB.NET
App-Objekt: Die verschiedenen Eigenschaften dieses globalen Objekts geben Auskunft über das laufende Programm (Name, Verzeichnis etc.)
Das Objekt gibt es nicht mehr. Ein Teil der Informationen kann aber mit den Eigenschaften und Methoden verschiedener .NET-Klassen ermittelt werden, z.B. System.Reflection, System.Environment und System.Windows.Forms.Application.
API-Funktionen: Fast alle Betriebssystemfunktionen können in VB-Programmen benutzt werden. Allerdings müssen sie vorher mit Declare deklariert werden.
Declare steht weiterhin zur Verfügung, wird aber
An API-Funktionen werden grundsätzlich ANSI-Zeichenketten übergeben, selbst dann, wenn die API-Funktionen in der Lage wären, Unicode-Zeichenketten zu verarbeiten.
viel seltener gebraucht, weil fast alle Betriebssystemfunktionen nun über die .NETKlassenbibliothek zur Verfügung stehen. Wenn der Aufruf von API-Funktionen aber unvermeidlich ist, sind große Änderungen im Vergleich zu VB6-Code erforderlich: zum einen, weil sich alle elementaren Datentypen geändert haben (Long, Integer), zum anderen, weil Types durch Structures mit einer anderen Syntax ersetzt wurden. Wesentlich verbessert hat sich der Umgang mit Zeichenketten. VB.NET ist nun in der Lage, Zeichenketten im Unicode-Zeichensatz an die API-Funktion zu übergeben. ANSI-Zeichenketten werden weiterhin unterstützt (je nachdem, wie die API-Funktion mit Declare deklariert wird).
DDE: DDE steht für Dynamic Data Exchange und ermöglichte die Steuerung externer Programme. DDE wird allerdings nur noch von wenigen, ziemlich alten WindowsProgrammen unterstützt und hat daher eine geringe Bedeutung.
DDE wird von VB.NET nicht mehr unterstützt.
DoEvents: Dieses Schlüsselwort
DoEvents steht in Windows-Anwendungen
bewirkt in VB6, dass die Ausführung der aktuellen Ereignisprozedur vorübergehend unterbrochen wird, um andere Ereignisse (z.B. Mausklicks) festzustellen und zu verarbeiten.
weiterhin zur Verfügung, muss nun aber mit Application.DoEvents aufgerufen werden. In vielen Fällen bieten die neuen Möglichkeiten zur Multithreading-Programmierung bessere (effizientere) Wege, um im Hintergrund länger andauernde Operationen durchzuführen, ohne die Bedienbarkeit von Programmen einzuschränken.
3.2 Unterschiede zwischen VB6 und VB.NET
107
VB6
VB.NET
Interne Projektverwaltung: Ein VBProjekt besteht aus relativ wenigen Dateien, im einfachsten Fall aus einer Projektdatei (*.vbp) sowie einigen Formular- und Codedateien (*.frm und *.bas).
Bei jedem noch so kleinen VB.NET-Projekt werden unzählige Dateien und mehrere Verzeichnisse erzeugt. Aus diesem Grund besteht die Entwicklungsumgebung darauf, jedes Projekt in einem neuen Verzeichnis zu speichern (was natürlich auch schon unter VB6 empfehlenswert war). Dieses Verzeichnis muss beim Erstellen eines Projekts angegeben werden. Wenn Sie diesen Teil des Dialogs nicht beachten, erzeugt die Entwicklungsumgebung selbst ein neues Verzeichnis (in Eigene Dateien\Visual StudioProjekte).
Setup-Programm: In VB6 gibt es einen Installationsassistenten (den package and deployment wizard, kurz PDW), um Setup-Programme für VB6-Programme zu entwickeln.
Auch VB.NET bietet vergleichbare Funktionen, die aber in jeder Hinsicht vollkommen anders organisiert sind. Das gilt sowohl für die Konfiguration (für die es eigene Projekttypen gibt) als auch für die interne Logik der Weitergabe (die nun auf dem Windows Installer basiert).
COM- und ActiveX-Kompatiblität Fast ein Jahrzehnt war OLE alias COM alias ActiveX das vorherrschende Objektmodell im Microsoft-Weltbild. Mit .NET hat sich das geändert. Jetzt heißt die neue Devise: Mit .NET wird alles besser, ActiveX ist alt, überholt und ungeeignet. Dieselben Argumente, mit denen vor ein paar Jahren ActiveX beworben wurden, sind jetzt als Werbeargumente für .NET zu hören. Dabei bestreite ich keineswegs, dass .NET jede Menge Vorteile hat. Tatsache ist aber auch, dass es beinahe unendlich viel Code gibt, der auf ActiveX basiert. Wieweit ist VB.NET also kompatibel zu ActiveX? ActiveX-Steuerelemente und -Bibliotheken: Diese Komponenten können unter VB.NET weiterhin verwendet werden. Sobald Sie einen Verweis auf eine derartige Komponente einrichten, erzeugt die Entwicklungsumgebung automatisch eine so genannte wrapperBibliothek, die die Schnittstelle zwischen .NET und COM herstellt. In der Praxis hat es bei den meisten Experimenten mit ActiveX-Komponenten kleinere Probleme gegeben, die aber meist mit etwas Experimentieren zu lösen waren. ActiveX-Automation: Manche ActiveX-/COM-/OLE-Programme können durch Automation von außen gesteuert werden (z.B. alle Programme aus dem Microsoft-Office-Paket). In VB.NET können Sie diesen Steuerungsmechanismus wegen der COM-Kompitibilität weiterhin nutzen. Dabei gibt es allerdings einige Einschränkungen: •
Bevor die Klassenbibliothek eines externen Programms verwendet werden kann, muss eine so genannte Wrapper-Bibliothek erstellt werden, die sich um den Übergang zwischen .NET und COM kümmert. Die .NET-Entwicklungsumgebung kümmert sich automatisch um die Erzeugung dieser Wrapper-Bibliothek. Leider gibt es dabei bei
108
3 Von VB6 zu VB.NET
manchen Bibliotheken arge Probleme, d.h., Klassen fehlen, Typen werden nicht richtig zugeordnet etc. In solchen Fällen ist es das Beste, late binding einzusetzen. (Dazu muss Option Strict Off verwendet werden!) •
Da das OLE-Steuerelement unter VB.NET nicht mehr zur Verfügung steht, kann das zu steuernde Programm nicht mehr innerhalb eines Fensters des VB.NET-Programms angezeigt werden. (Da diese Funktion in VB6 meistens auch nicht richtig funktioniert hat, ist der Verlust des OLE-Steuerelements leicht zu verschmerzen.)
VERWEIS
Runtime.Interop: Wenn es Kompatibilitätsprobleme zwischen COM und .NET gibt, dann helfen eventuell die Klassen System.Runtime.Interop.* weiter.
Zum Thema COM- und .NET-Kompatibilität sind (bis jetzt) zwei englische Bücher erschienen: .NET and COM: The Complete Interoperability Guide von Adam Nathan und COM and .NET Interoperability von Andrew Troelsen. Der Umfang der beiden Bücher (zusammen 2500 Seiten) sollte klar machen, dass es zu diesem Thema mehr zu schreiben gibt als diesen kurzen Abschnitt. Wenn Sie also COM-Probleme mit .NET haben, sollten diese Bücher Ihre erste Anlaufstelle sein.
VB6-Kompatibilität Es sollte mittlerweile klar geworden sein, dass VB.NET in sehr vielen Punkten inkompatibel zu VB6 ist. Das bedeutet aber nicht, dass sich Microsoft nicht bemüht hätte, zumindest ein gewisses Maß an Kompatibilität zu erreichen. Dafür sind vor allem zwei Bibliotheken verantwortlich, von denen die erste offiziell unterstützt wird, die zweite dagegen nur für besondere Notfälle gedacht ist. •
Die Bibliothek microsoft.visualbasic.dll ist integraler Bestandteil aller VB.NET-Projekte (und kann theoretisch auch von anderen Programmiersprachen verwendet werden). Sie stellt im Namensraum Microsoft.VisualBasic eine Menge aus VB6 vertraute Methoden und Klassen zur Verfügung. Dazu zählen beispielsweise Zeichenkettenfunktionen (Left, Right etc.), herkömmliche Funktionen zum Dateizugriff, Funktionen zur Bearbeitung von Zahlen, Daten und Zeiten, vordefinierte Konstanten (vbCrLf, vbTab etc.), Konvertierungsfunktionen etc.
•
Die Bibliothek microsoft.visualbasic.compatibility.dll stellt im Namensraum Microsoft.VisualBasic.Compatibility.VB6 diverse Funktionen und Steuerelemente zur Verfügung, die in dieser Form in VB.NET nicht mehr existieren. Die Bibliothek ist nur als Hilfsmittel für den Migrationsassistenten gedacht. Die Dokumentation rät ausdrücklich davon ab, diese Bibliothek auch in neuen VB.NET-Projekten einzusetzen, weil die Bibliothek von künftigen VB.NET-Versionen unter Umständen nicht mehr unterstützt wird. Aus diesem Grund wird in diesem Buch auf eine Beschreibung der in dieser Bibliothek enthaltenen Schlüsselwörter verzichtet.
3.3 Der Migrationsassistent
3.3
109
Der Migrationsassistent
VB.NET ist eine neue Programmiersprache, deren einzige Gemeinsamkeit mit VB6 die Basissyntax ist. Auf beinahe 30 Seiten habe ich nun die wichtigsten Unterschiede zwischen VB6 und VB.NET aufgezählt, und ich betone nochmals, dass diese Aufzählung alles andere als komplett ist. Es sind wohl Marketing-Gründe, weswegen Microsoft dennoch das Unmögliche versucht und mit VS.NET Professional und Enterprise einen Migrationsassistenten mitliefert. Jedem Programmierer, der VB6 und VB.NET auch nur ein bisschen kennt, muss klar sein, dass eine automatische Codemigration nur in Ausnahemfällen gelingen kann. (Derartige Ausnahmefälle sind Algorithmen und Klassenbibliotheken, die weder eine Benutzeroberfläche haben noch irgendwelche ActiveX-Bibliotheken nutzen. Ich habe in den vielen Jahren, die ich mit VB schon programmiere, kein einziges derartiges Projekt erstellt.) Daher sollten Sie keine falschen Erwartungen an den Assistenten haben: Im Regelfall wird dieser Assistent kein lauffähiges VB.NET-Projekt liefern, sondern Code, der noch voller Fehler steckt, die Sie selbst beheben müssen. Das setzt ein gutes Wissen sowohl über VB6 als auch über VB.NET voraus. (Die trivialen Dinge – etwa die Konvertierung der Integerdatentypen – gelingen dem Assistenten erwartungsgemäß gut. Aber bei vielen Funktionen, bei denen eine Portierung aufwendig ist, macht der Assistent nicht einmal den Versuch einer Konvertierung. Dazu zählen z.B. alle Grafikausgaben.) Die Frage ist auch, welches Ziel die Konvertierung haben soll: Wenn es darum geht, vorhandenen VB6-Code weiter zu warten, ist eine Portierung auf VB.NET zwecklos. Es wird Tage, wenn nicht Wochen dauern, bis Sie ein größeres Projekt zuerst zum Laufen gebracht haben und es dann so lange ausführlich getestet haben, bis Sie sicher sind, dass es zumindest so fehlerfrei wie vorher läuft. Da ist es sinnvoller, zur Wartung und auch zur Realisierung einzelner neuer Funktionen weiterhin VB6 zu verwenden. Wenn Sie dagegen an einem neuen Projekt arbeiten und die eigentlichen Vorteile von .NET nutzen möchten, ist es vernünftiger, den Code von Grund auf neu zu entwickeln (auch wenn Sie schon einmal ein vergleichbares VB6-Projekt realisiert haben). Der Migrationsassistent ist naturgemäß nicht in der Lage, Code so umzuprogrammieren, dass statt der zahlreicher ActiveX-Komponenten gleichwertige bzw. bessere .NET-Bibliotheken eingesetzt werden. Über so viel Intelligenz verfügen nur Sie als Programmierer! Kurz und gut: So sehr ich die Leistung der Programmierer würdige, die den Migrationsassistenten entwickelt haben, so sehr bezweifle ich, dass es irgendein reales Anwendungsszenario für das Programm gibt. Das Programm versagt bereits bei fast allen meiner ziemlich trivialen Beispielprogramme aus dem VB6-Buch – wie soll das Programm dann mit realen (und sehr viel kompexeren) Anwendungen zurechtkommen?
110
3 Von VB6 zu VB.NET
VERWEIS
Weitere Informationen zum Migrationsassistenten finden Sie in der Online-Hilfe (suchen Sie nach Aktualisieren von Anwendungen) und im Internet: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vboriUpgradingApplications.htm http://www.devx.com/free/hotlinks/2002/ednote022002/ednote022002.asp
Auf den folgenden Seiten finden Sie grundsätzliche Informationen zur Migration von VB6 nach VB.NET: http://msdn.microsoft.com/vbasic/techinfo/articles/upgrade/guide.asp http://www.msdn.microsoft.com/library/en-us/dnvb600/html/vb6tovbdotnet.asp
Voraussetzungen VB6 muss am selben Rechner installiert sein. Der Grund hierfür besteht darin, dass der Migrationsassistent auf alle Steuerelemente und Bibliotheken zugreifen muss, die im jeweiligen VB6-Programm eingesetzt werden. Diese Voraussetzung wird am einfachsten durch eine VB6-Installation erfüllt. Ich habe auf meinem Rechner VB6 nach VB.NET installiert, ohne dass es zu Problemen gekommen ist. Nach Möglichkeit sollte aber dennoch die umgekehrte Reihenfolge vorgezogen werden.
•
Der Migrationsassistent wird nur mit den Professional- und Enterprise-Versionen mitgeliefert. Das Argument Microsofts lautet hierfür, dass sich VB.NET Standard an Einsteiger richtet, die mit dem Migrationsassistenten sicherlich überfordert sind und die im Übrigen auch keinen alten VB6-Code besitzen.
HINWEIS
•
In den VB.NET-Newsgruppen war mehrfach zu lesen, dass der Migrationsassistent anfänglich funktionierte, sich dann aber nicht mehr starten ließ. Abhilfe brachte erst ein neuerliches Ausführen des VS.NET-Installationsprogramms, um den Assistenten neu zu installieren. Ich kann aus eigener Erfahrung nichts dazu sagen – bei mir ließ sich der Assistent stets problemlos starten.
Anwendung Die Anwendung des Assistenten ist denkbar einfach: Sie öffnen in der Entwicklungsumgebung ein altes VB6-Projekt (also die *.vbp-Projektdatei). Damit wird der Assistent automatisch gestartet. Der Assistent fragt, in welches Verzeichnis er das neue Projekt speichern soll (siehe Abbildung 3.1) und beginnt dann mit der Konvertierung des Codes (die bei umfangreichen Projekten und langsamen Rechnern sehr lange dauern kann).
3.3 Der Migrationsassistent
111
Abbildung 3.1: Der Migrationsassistent
Das resultierende VB.NET-Projekt wird anschließend geöffnet. Sie können sich nun im Fenster AUFGABENLISTE sowie im Migrationsreport (der in HTML-Form Teil des Projekts ist, siehe Abbildung 3.2) über die Punkte informieren, die bei der Konvertierung Probleme bereitet bzw. Fehler verursacht haben.
Abbildung 3.2: Der Migrationsreport informiert über Fehler, die bei der Konvertierung des Projekts aufgetreten sind
Teil II
Grundlagen
4
Variablen- und Objektverwaltung
Nach einer Einführung in den Umgang mit Variablen (Deklaration, Zuweisung, Verwendung von Objektvariablen) beschreibt das Kapitel ausführlich die grundlegenden .NET-Datentypen, also beispielsweise Integer, Double, Date und String. Anschließend wird der Umgang mit Konstanten und Feldern beschrieben. Das Kapitel endet mit einem Ausflug in die Interna der Variablen-, Objekt- und Speicherverwaltung, wobei sich dieser Abschnitt eher an fortgeschrittene VB.NET-Programmierer richtet. 4.1 4.2 4.3 4.4 4.5 4.6
Umgang mit Variablen Variablentypen Konstanten Enum-Aufzählungen Felder Interna der Variablenverwaltung
116 127 132 134 139 146
VERWEIS
Dieses Kapitel stellt wirklich nur eine Einführung dar! Die folgende Liste gibt an, wo Sie weitere Detailinformationen finden: Lokale Variablen und Prozedurparameter: Abschnitt 5.3 Objekte nutzen: Kapitel 6 Eigene Klassen definieren: Kapitel 7 Eigene Datenstrukturen definieren (Structure): Abschnitt 7.3 Gültigkeitsbereiche von Variablen (scope): 7.9 Umgang mit Zahlen, Daten und Zeichenketten: Kapitel 8 Aufzählungen (Collections): Kapitel 9
116
4 Variablen- und Objektverwaltung
4.1
Umgang mit Variablen
4.1.1
Deklaration von Variablen
Variablen müssen vor ihrer ersten Verwendung mit Dim deklariert werden. Dieses Kommando kennt unglaublich viele Syntaxvarianten, die aber erst im weiteren Verlauf dieses Buchs beschrieben werden. Im einfachsten Fall deklariert Dim x, y die Variablen x und y. VB.NET verwendet dabei Object als Defaultdatentyp. In derartigen Variablen können Sie alle erdenklichen Daten (Zahlen, Zeichenketten etc.) speichern, ohne dass Sie sich über den Datentyp Sorgen machen müssen. Dim x, y ist unzulässig, wenn Sie Option Strict On verwenden (siehe Abschnitt 4.1.4). Per Default ist diese Option in VB.NET-Programmen aber nicht aktiv. Dim x, y As Integer deklariert die beiden Variablen explizit als Integer-Variablen (32 Bit mit
Vorzeichen). Der Vorteil gegenüber der Deklaration ohne die Angabe eines expliziten Datentyps besteht darin, dass Sie nun in x und y nicht versehentlich andere Daten speichern können. Außerdem bestehen aufgrund der expliziten Typangabe bessere Möglichkeiten zur Syntaxkontrolle, der resultierende Code wird effizienter, und der Platzbedarf im Speicher ist ein wenig geringer. Das folgende Miniprogramm deklariert die beiden Variablen x und y, weist ihnen Werte zu und zeigt diese Werte dann in einer Hinweisbox an. Module Module1 Sub Main() Dim x, y As Integer x = 3 y = x + 1 MsgBox("x=" & x & " End Sub End Module
y=" & y)
HINWEIS
Per Default müssen in VB.NET Variablen vor ihrer ersten Verwendung deklariert werden. Es ist üblich, alle Variablendeklarationen am Beginn einer Prozedur (hier also am Beginn von Main) durchzuführen, das ist aber keine Bedingung. Auch die elementaren Variablentypen werden intern als Objekte betrachtet. Daher sind die beiden Anweisungen Dim x As Integer und Dim x As New Integer gleichwertig.
Variablendeklaration mit Kennzeichnungszeichen Für die wichtigsten Variablentypen gibt es spezielle Kennzeichner, die eine Kurzschreibweise bei der Deklaration ermöglichen: So können Sie statt Dim x As Integer auch die Kurz-
4.1 Umgang mit Variablen
117
schreibweise Dim x% verwenden. (Ausführlichere Informationen über diese und einige weitere Variablentypen finden Sie im nächsten Abschnitt.) Kennzeichner
Bezeichnung
Platzbedarf
Zahlenbereich
% & @ ! # $
Integer Long Decimal Single Double String
4 Byte 8 Byte 12 Byte 4 Byte 8 Byte 10 + 2*n Byte
-2.147.483.648 bis 2.147.483.647 63 63 -2 bis 2 -1 ±9,99E27 mit 28 Stellen ±3,4E38 mit 8 Stellen ±1,8E308 mit 16 Stellen bis zu 2.147.483.647 Unicode-Zeichen
Die beiden Deklarationsformen können allerdings nicht nach Belieben gemischt werden. Insbesondere dürfen die Kurzschreibweisen nur alleine oder nach As-Anweisungen angegeben werden, nicht aber davor.
HINWEIS
Dim x%, y& Dim b1, b2 As Byte, x%, y& Dim x%, y&, b1, b2 As Byte
'ok 'ok 'Syntaxfehler!
Die VB.NET-Dokumentation rät von der Verwendung von Kennzeichnern bei der Deklaration von Variablen ab. Derartige Kürzel vermindern die Lesbarkeit von Code. Allerdings sparen Sie oft (gerade bei der Deklaration von Parametern) eine Menge Platz, was der Übersichtlichkeit sehr zugute kommt. Letztlich ist es also eine persönliche Entscheidung (oder eine des Entwicklerteams), ob diese Kurzschreibweise angewandt wird oder nicht.
Variablennamen Variablennamen müssen mit einem Buchstaben oder einem Unterstrich beginnen. Die weiteren Zeichen dürfen auch Zahlen enthalten. Es sind auch Sonderzeichen der jeweiligen Sprache erlaubt. Da Programmcode aber per Default mit nur einem Byte pro Zeichen in Dateien gespeichert wird (ANSI), kann die Verwendung von Zeichen außerhalb des USASCII-Zeichensatz Probleme verursachen. Es ist daher eine gute Idee, auf die deutschen Zeichen äöü und ß in Variablennamen zu verzichten. Informationen über die maximale Länge von Zeichenketten habe ich nicht gefunden, Experimente haben aber ergeben, dass diese größer als 256 Zeichen sein darf. Variablennamen sollten nicht mit den Namen von VB.NET-Schlüsselwörtern übereinstimmen. Wenn dies unvermeidlich ist, können Sie den Variablennamen in eckige Klammern stellen. Beispielsweise ist Dim [Next] As Integer zulässig, obwohl Next ein Schlüsselwort zur Formulierung von Schleifen ist.
118
4 Variablen- und Objektverwaltung
Groß- und Kleinschreibung
TIPP
Grundsätzlich spielt in VB.NET die Groß- und Kleinschreibung von Variablen keine Rolle. variable und varIABle und Variable sind also gleichwertig. Der Codeeditor passt die Schreibweise aller Variablen an die Schreibweise an, die bei der Deklaration verwendet wurde. Wenn Sie die Schreibweise einer Variable in der Dim-Anweisung verändern, ändert der Codeeditor nicht automatisch alle anderen Zeilen, in denen die Variable vorkommt. Wenn Sie das möchten, markieren Sie einfach den gesamten Text mit Strg+A und klicken dann Tab an. Damit wird nicht nur der gesamte Code richtig eingerückt, sondern auch die Schreibweise aller Variablen synchronisiert.
Defaultwerte von nicht initialisierten Variablen Neu deklarierte Variablen erhalten automatisch einen Startwert, der aus der folgenden Tabelle hervorgeht. Variablentyp
Startwert
numerische Variablen
0
Boolean-Variablen
False
String-Variablen
Nothing
Bemerkungen
Vorsicht: Nicht initialisierte String-Variablen enthalten tatsächlich Nothing, auch wenn man oft den Eindruck gewinnt, sie enthielten "". Der Grund für diese Missverständnisse ist die VB.NET-Runtime: Diese interpretiert nicht initialisierte String-Variablen wie eine leere Zeichenkette ""! Len(s) liefert daher 0. s="" liefert (eigentlich inkorrekt) True. s Is Nothing und IsNothing(s) liefern korrekt True.
Verwenden Sie die nicht intialisierte String-Variable dagegen im Kontext von .NET-Methoden (z.B. s.Length), tritt ein Fehler auf. Char-Variablen
0
Vorsicht: Die Entwicklungsumgebung, d.h. der Editor, das Kommandofenster und die Überwachungsfenster, zeigen bei nicht initialisierten Char-Variablen Nothing an. Das ist abermals falsch! System.Convert.ToInt16(c) beweist, dass c wirklich ein
Zeichen mit dem Code 0 enthält.
4.1 Umgang mit Variablen
Variablentyp
Startwert
119
Bemerkungen IsNothing(c) funktioniert korrekt und liefert False. c Is Nothing kann für Char-Variablen nicht ausgewertet
werden. Date-Variablen
#12:00:00 AM#
Vorsicht: Diese Zeitangabe entspricht bei deutscher Formatierung 00:00:00 (und nicht etwa 12 Uhr mittags)! VB.NET zeigt diesen Datumswert als reine Zeitangabe an. d.Date ist leer. d.Year, d.Month und d.Day bzw. d.ToString ergeben aber, dass intern das Datum 1.1. des Jahres 0001 gespeichert ist. d.Ticks enthält 0.
Object-Variablen
Nothing
Objektvariablen werden im nächsten Abschnitt beschrieben.
Wertzuweisung (Initialisierung) bei der Deklaration Variablen können unmittelbar bei der Deklaration initialisiert werden. Die erforderliche Syntax sieht so aus: Dim i1 As Integer = 1 Dim i2 As Integer = 2, i3 As Integer = 3 Dim i4% = 4, i5% = 5
Manche Datentypen und alle gewöhnlichen Klassen (Details folgen im nächsten Abschnitt) sehen zur Initialisierung von Variablen den New-Operator vor.
VERWEIS
Dim d As New Date(2001, 12, 31) Dim s1 As New String("a", 3)
'd1 enthält das Datum 31.12.2001 's1 enthält "aaa"
Was passiert, wenn Sie a=b ausführen? Wird in a eine Kopie von b gespeichert oder ein Verweis auf b? Bevor diese Frage beantwortet werden kann, müssen Sie zunächst Objektvariablen kennen lernen. Die Antwort folgt dann in Abschnitt 4.1.3.
4.1.2
Objektvariablen
Was sind Objekte? Ein Kapitel zur Variablenverwaltung ohne die Berücksichtigung von Objekten wäre unvollständig. Allerdings werden Objekte und die Grundzüge objektorientierter Programmierung erst in den folgenden Kapiteln ausführlich beschrieben. Daher ist hier ein Vorgriff unvermeidbar. (Wenn die Beschreibung der Nomenklatur an dieser Stelle zu schnell geht, muss ich Sie auf die nachfolgenden Kapitel vertrösten.)
120
4 Variablen- und Objektverwaltung
Klasse (Typ): Eine Klasse beschreibt die Eigenschaften eines Objekts. Die Klasse ist gewissermaßen der Bauplan für ein Objekt. In der .NET-Klassenbibliothek sind mehrere Tausend Klassen definiert, die bei der Organisation und Verwaltung aller möglichen Dinge helfen: Es gibt Klassen zur Beschreibung der Eigenschaften einer Datei (z.B. System.IO.FileInfo), Klassen zur Darstellung eines Fensters (z.B. System.Windows.Forms.Form), Klassen für alle Steuerelemente, die im Fenster angezeigt werden (z.B. System.Windows.Forms.Button), Klassen zur Erzeugung von Zufallszahlen (System.Random) etc. Darüber hinaus können Sie selbst eigene Klassen definieren. Statt Klasse wird manchmal auch der Begriff Typ verwendet, vor allem in der Zusammensetzung Werttype bzw. Referenztyp. Gemeint sind damit Klassen mit bestimmten Merkmalen – siehe unten. Vererbung: Klassen können ihre Eigenschaften und Methoden an andere Klassen gewissermaßen vererben. Das bedeutet, dass mehrere von einer Basisklasse abgeleitete Klassen dieselben Grundeigenschaften und -methoden haben. Vererbung ist ein wichtiges Werkzeug bei der Programmierung eigener Klassen. Objekte: Objekte sind konkrete Realisierungen von Klassen. In Variablen speichern Sie daher Objekte, nicht Klassen. Wenn die folgende Zeile ausgeführt wird, dann enthält die Variable myRandomObjekt ein Objekt der Klasse System.Random. Dim myRandomObject As New System.Random()
Werttypen (ValueType) versus Referenztypen Viele Programmiersprachen differenzieren zwischen gewöhnlichen Variablen (z.B. für Integer-Zahlen) und Objektvariablen. Nicht so VB.NET: Hier ist jede Variable eine Objektvariable! Wenn hier und in vielen anderen Büchern dennoch manchmal zwischen Variablen und Objektvariablen differenziert wird, dann bezieht sich diese Unterscheidung auf den Typ der zugrunde liegenden Klasse. Die .NET-Bibliothek teilt nämlich alle Klassen in zwei ganz wesentliche Typen ein: •
Werttypen: Zu dieser Gruppe zählen unter anderem alle elementaren Datentypen (z.B. Integer, Double etc.) mit der Ausnahme von String. Das entscheidende interne Merkmal besteht darin, dass sie von der Klasse ValueType abgeleitet sind. (Daraus resultiert auch die Bezeichnung ValueType-Klassen bzw. ValueType-Objekte.) Neben den elementaren Datentypen gibt es eine ganze Reihe von Klassen und Datenstrukturen in der .NET-Bibliothek, die ebenfalls von ValueType abgeleitet sind. Zumeist handelt es sich dabei um eher kleine Klassen (klein hinsichtlich des Speicherbedarfs, aber auch klein hinsichtlich der angebotenen Funktionen). Zwei Beispiele sind System.Drawing.Rectangle und System.Drawing.Color. Auch die in VB.NET definierten Strukturen (Structure ... End Structure) und Aufzählungen (Enum) werden von ValueType abgeleitet.
4.1 Umgang mit Variablen
•
121
Referenztypen: Zu dieser Gruppe zählen alle Klassen, die nicht von ValueType abgeleitet sind. Die Bezeichnung Referenztypen (im Englischen reference types oder reference classes) resultiert daraus, dass Objekte dieser Klassen als Referenz (als Zeiger) in Variablen gespeichert bzw. an Prozeduren oder Methoden übergeben werden. Zu den Referenztypen zählt die Mehrheit aller Klassen der .NET-Bibliothek, z.B. System.Array, System.IO.FileInfo, System.Drawing.Bitmap und System.Windows.Forms.Button, um willkürlich vier Beispiele herauszugreifen. ValueType-Klassen sind so gesehen nur eine (sehr wichtige) Ausnahme. Beachten Sie, dass auch Felder (die intern durch die Klasse System.Array verwaltet werden) zu den Referenztypen zählen – selbst dann, wenn deren Elemente oft Werttypen sind (z.B. Integer-Zahlen).
Die Motivation für die Trennung zwischen Wert- und Referenztypen lautet kurz und einfach: Effizienz. Damit der objektorientierte Ansatz von .NET konsequent verfolgt werden kann, müssen alle Daten Objekte sein. Durch die Behandlung als Objekte entsteht aber ein hoher Overhead, der bei einfachen Daten (z.B. bei Integer-Zahlen) in keinem Verhältnis zum Nutzen stehen würde. Deswegen muss es für den Compiler einen Weg geben, mit manchen Objekten (eben mit den ValueType-Objekten) so umzugehen, als wären es nur einfache Werte. Objekte, die von Wert- bzw. von Referenztypen abgeleitet sind, unterscheiden sich in ihrer Verwendung ganz erheblich: bei Variablenzuweisungen, bei der Übergabe als Parameter an eine Prozedur oder Methode, bei der internen Speicherverwaltung etc. In diesem und in den folgenden Kapiteln wird immer wieder auf diesen Unterschied hingewiesen. Daher ist es wichtig, dass Sie zwischen Wert- und Referenztypen differenzieren können! Wie können Sie aber feststellen, welchem Typ eine bestimmte Klasse angehört? Am besten verwenden Sie dazu den Objektbrowser, den Sie mit ANSICHT|ANDERE FENSTER öffnen. Dort suchen Sie die Klasse, die Sie interessiert, und klicken dann das Pluszeichen um, um die übergeordneten Klassen zu ermitteln (also die Klassen, die durch Vererbung implementiert sind). Abbildung 4.1 zeigt, dass die Klasse System.Drawing.Color von der ValueTypeKlasse abgeleitet ist. Damit ist klar, dass Color ein Werttyp ist.
Abbildung 4.1: Die Klasse System.Drawing.Color ist eine ValueType-Klasse
VERWEIS
122
4 Variablen- und Objektverwaltung
Ein übersichtliches Schema der .NET-Datentypen samt der Unterscheidung zwischen Wert- und Referenztypen finden Sie in der Online-Dokumentation, wenn Sie nach Übersicht Typensystem suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconcommontypesystemoverview.htm
Deklaration von Objektvariablen Wenn Sie eine Variable deklarieren, die auf ein bestimmtes Objekt verweisen soll, ändert sich an der Syntax nichts: Statt des Datentyps geben Sie jetzt den Klassennamen ein. Dim myRandomObject As System.Random
Die Variable myRandomObject ist damit allerdings noch leer! Bevor Sie Methoden der System.Random-Klasse nutzen können, müssen Sie das Objekt erst erzeugen. myRandomObject = New System.Random()
Im Regelfall werden Sie diese beiden Anweisungen mit Dim As New in einer einzigen Zeile vereinen: Dim myRandomObject As New System.Random()
Noch kompakter wird die Zeile, wenn Sie auf die Angabe von System verzichten (was im Regelfall möglich ist – mehr dazu in Abschnitt 6.2.) Dim myRandomObject As New Random()
Bei vielen Klassentypen können an die New-Methode Parameter übergeben werden. Die folgende Anweisung erzeugt ein Objekt der Klasse System.IO.DirectoryInfo. Das Objekt wird gleich mit dem Pfad des aktuellen Verzeichnis initialisiert.
VERWEIS
Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".")
Elementare Variablentypen wie Integer, String sind so genannte Werttypen und stellen einen Sonderfall dar. Zwar betrachtet VB.NET auch solche Variablen intern als Objekte, dennoch können diese Variablen sofort nach der Deklaration verwendet werden. Das Schlüsselwort New ist zwar erlaubt (und manchmal zur Initialisierung auch nützlich – siehe unten), es ist aber nicht erforderlich. Hintergrundinformationen über die Interna der Variablenverwaltung und insbesondere über den Unterschied zwischen Wert- und Referenztypen finden Sie in Abschnitt 4.6.
Objektvariablen ohne Typangabe Wenn Sie im Vorhinein nicht wissen, wie Sie eine Objektvariable verwenden werden, können Sie die Variable sehr allgemein als Object deklarieren. Sie können diese Variable dann im weiteren Verlauf nach Belieben verwenden. Durch myObject=3 wird in myObject ein Inte-
4.1 Umgang mit Variablen
123
ger-Wert gespeichert. Durch New System.Random wird ein Objekt des Typs System.Random gespeichert etc. Dim myObject As Object myObject = 3 myObject = New System.Random()
'myObject verweist jetzt auf eine 'Integer-Variable 'myObject verweist jetzt auf ein 'Objekt der Klasse System.Random
Diese Vorgehensweise ist allerdings mit Nachteilen verbunden: Erstens weiß die Entwicklungsumgebung nicht, wie Sie die Variable verwenden werden, und kann daher bei der Codeeingabe nicht die für ein Objekt relevanten Methoden und Eigenschaften vorschlagen. Zweitens weiß auch der Compiler nicht, wie Sie die Variable verwenden, und kann daher nur eine eingeschränkte Syntaxkontrolle durchführen. Drittens kann es passieren, dass Sie der Variable versehentlich einen Wert zuweisen und so unbeabsichtigt den Objekttyp verändern. (Dabei kommt es zu keinem Fehler. Probleme kann es aber später geben, wenn Sie Objekteigenschaften oder -methoden einsetzen, die es für den neuen Objekttyp gar nicht gibt.) Und zu guter Letzt ist der Code etwas langsamer – aber dieses Argument ist wahrscheinlich das unwichtigste.
VERWEIS
Fazit: Geben Sie bei der Deklaration nach Möglichkeit exakt den Daten- oder Objekttyp an! Sie ersparen sich damit eine oft langwierige Fehlersuche. (Lesen Sie auch den Abschnitt zu Option Explicit und Option Strict etwas weiter unten!) In Abschnitt 4.6.4 werden verschiedene Wege vorgestellt, mit denen Sie den Typ einen Variable bestimmen können. Am vielseitigsten ist dabei die Methode GetType.
4.1.3
Variablenzuweisungen
VERWEIS
Was passiert, wenn Sie a = b ausführen? Diese Frage klingt trivial, aber wenn es wirklich so trivial wäre, gäbe es dazu natürlich keinen eigenen Abschnitt. Das Ergebnis einer Variablenzuweisung hängt nämlich von der Art der Variablen ab. Variablen zur Speicherung von Werttypen verhalten sich anders als solche für Referenztypen. Dieser Abschnitt setzt voraus, dass a und b für denselben Daten- bzw. Objekttyp deklariert wurden. Wenn das nicht der Fall ist, kommt es bei der Zuweisung zu einer automatischen Konvertierung der Daten. Diese gelingt allerdings nur in Sonderfällen (z.B. wenn a eine Double- und b eine Integer-Variable ist). Wenn eine verlustfreie Typumwandlung dagegen nicht möglich ist, kommt es zu einer Fehlermeldung. Mehr Informationen zum Thema der automatischen und manuellen TypenKonvertierung erhalten Sie in Abschnitt 8.4.
124
4 Variablen- und Objektverwaltung
Werttypen (ValueType-Variablen) Bei Werttypen, d.h. bei den meisten elementaren Datentypen (Integer, Double, Date etc.), wird durch a = b eine Kopie der Daten erstellt und zugewiesen. Dim Dim a = b =
a As Integer = 1 b As Integer = 2 b 'jetzt ist a=2 und b=2 3 'jetzt ist a=2 und b=3
Referenztypen (herkömmliche Objektvariablen) Bei Referenztypen (deren Klasse nicht von ValueType abgeleitet ist) wird durch a = b dagegen ein Link (eine Referenz) auf das Objekt zugewiesen, also keine Kopie! Im folgenden Beispiel wird in a und b jeweils ein Button-Objekt gespeichert. Derartige Objekte dienen normalerweise zur Darstellung von Buttons in einem Fenster. Die Zuweisung a = b bewirkt hier, dass nun beide Variablen auf denselben Button verweisen. Durch die Veränderung der Eigenschaft b.Text ändert sich nun auch a.Text (weil ja a und b auf dasselbe Objekt verweisen)! Dim a As Dim b As a.Text = b.Text = a = b b.Text =
New Windows.Forms.Button() New Windows.Forms.Button() "a" "b" 'a und b zeigen jetzt auf denselben Button (.Text="b") "x" 'damit wird .Text für a und für b verändert!
VERWEIS
Wenn Sie bei gewöhnlichen Objekten eine Kopie der Daten benötigen, müssen Sie a = b.Clone() ausführen. Allerdings steht die Methode Clone nicht für alle Klassen zur Verfügung. Wenn es Clone nicht gibt, besteht keine Möglichkeit, ein Objekt zu kopieren. (Die Button-Klasse sieht kein Clone vor.) Vielleicht fragen Sie sich, was mit dem ursprünglich für a erzeugten Button passiert: Es gibt nun ja keine Variable mehr, die darauf verweist. Deswegen wird das Objekt nach einiger Zeit automatisch aus dem Speicher entfernt. Dieser Prozess wird garbage collection genannt und ist in Abschnitt 4.6.2 beschrieben.
String-Variablen String-Variablen zur Speicherung von Zeichenketten stellen einen Sonderfall dar. Zwar zählt String zu den elementaren Datentypen, es handelt sich aber intern dennoch um einen Referenztyp. (Die String-Klasse ist nicht von ValueType abgeleitet.) Und trotzdem verhalten sich String-Variablen wie Werttypen!
4.1 Umgang mit Variablen
125
Das hat folgenden Grund: String-Objekte gelten als unveränderlich (immutable). Das bedeutet, dass bei jeder Veränderung einer Zeichenkette ein vollkommen neues String-Objekt erzeugt wird. Aus diesem Grund betrifft eine Änderung immer nur eine Variable (auch wenn vorher zwei Variablen auf dasselbe String-Objekt verwiesen haben). Dim Dim a = b =
a As b As b "x"
String = "a" String = "b" 'a und b verweisen nun auf "b" 'a verweist weiterhin auf "b"; 'b verweist auf das neue String-Objekt "x"
Geradezu verblüffend ist das Verhalten, wenn Sie nun auch a die Zeichenkette "x" zuweisen: Dann verweisen a und b wieder auf dasselbe String-Objekt (d.h., der Objektvergleich a Is b liefert True). a = "x"
'a und b verweisen nun auf dasselbe String-Objekt "x"
Dieses Verhalten gilt allerdings nicht immer. Wenn Sie zuerst a="12" und dann b="1" und b+="2" ausführen, dann enthalten zwar a und b dieselbe Zeichenkette "12", aber intern verweisen die beiden Variablen auf unterschiedliche Objekte (weil der Compiler hier nicht erkennen konnte, dass die Ergebnisse der Zuweisungen gleichartige Objekte sein würden).
4.1.4
Option Explicit und Option Strict
Option Explicit: In VB.DOT müssen per Default alle Variablen mit Dim deklariert werden, bevor sie verwendet werden können. Der Grund für dieses Verhalten ist die projektweite Defaulteinstellung für Option Explicit. Falls Sie diese Einstellung verändern möchten (dazu gibt es aber keinen vernünftigen Grund!), markieren Sie im PROJEKTMAPPEN-EXPLORER Ihr Projekt und stellen Sie dann in PROJEKT|EIGENSCHAFTEN|ALLGEMEINE EIGENSCHAFTEN|ERSTELLEN das Listenfeld OPTION EXPLICIT auf AUS. Sie können Option Explicit auch getrennt für einzelne Codedateien einstellen: Option Explicit On|Off Option Strict: Diese Option hat auf ersten Blick nichts mit der Deklaration von Variablen zu tun, sondern damit, ob VB.NET (wie VB6) versucht, Umwandlungen zwischen verschiedenen Datentypen automatisch durchzuführen. Dies ist in der Defaulteinstellung der Fall. Wenn Sie Option Strict aktivieren (entweder projektweit mit PROJEKT|EIGENSCHAFTEN oder für eine einzelne Codedatei mit Option Strict On), müssen Sie bei allen Umwandlungen, die das Risiko eines Datenverlusts in sich bergen, explizit eine Konvertierungsfunktion angeben. (Weitere Information zur Konvertierung zwischen Datentypen finden Sie in Abschnitt 8.4.)
Gleichsam eine Nebenwirkung von Option Strict On besteht darin, dass Dim x ohne explizite Typenangabe nicht mehr erlaubt ist. Sie müssen bei jeder Variablen den gewünschten Typ angeben (wobei As Object natürlich weiterhin zulässig bleibt).
TIPP
4 Variablen- und Objektverwaltung
Wenn Sie Wert auf exakten Code legen, sollten Sie generell beide Optionen, also auch Option Strict, aktivieren. Sie ersparen sich dadurch eine Menge Zeit bei der Suche von Fehlern, die durch Tippfehler (Option Explicit) oder durch die unbeabsichtigte Typenkonvertierung (Option Strict) entstehen können.
VERWEIS
126
Die Defaulteinstellung Option Strict Off für neue Projekte kann in der Entwicklungsumgebung nicht verändert werden. (Die Einstellung durch PROJEKT|EIGENSCHAFTEN gilt immer nur für das aktuelle Projekt.) Abschnitt 1.3.7 zeigt aber, wo Sie die Konfigurationsdateien für neue Projekte finden und wie Sie Option Strict dort für alle neuen Projekte eines bestimmten Typs (z.B. für alle Windows-Anwendungen) auf On setzen können.
Option-Strict-Beispiel Die folgenden Zeilen sehen so aus, als würden sie zur aktuellen Zeit in d1 eine Stunde hinzuaddieren. Tatsächlich kommt es bei der Durchführung der Addition aber zu einem Fehler. (In VB6 funktionierte dieser Code tatsächlich. In VB.NET sind derartige Additionen aber nicht zulässig.) Dim d1, d2, d3 As Date d1 = Now d2 = #1:00:00 AM# d3 = d1 + d2 'Fehler in VB.NET!
Was hat nun dieser Code mit Option Strict zu tun? VB.NET betrachtet + hier als einen Operator, um zwei Zeichenketten zu verknüpfen! d1 und d2 werden entsprechend der Ländereinstellung automatisch in Zeichenketten umgewandelt und aneinandergefügt. Der Fehler tritt erst auf, weil die ebenfalls automatische Rückkonvertierung der Zeichenkette in ein Datum für die Zuweisung an d3 scheitert. Die aus d1+d2 resultierende Zeichenkette lautet beispielsweise "29.11.2002 16:23:2101:00:00", wenn der Code am 29.11.2002 um 16:23 im deutschen Sprachraum ausgeführt wurde. (Genau genommen ist die Ländereinstellung des Betriebssystem für das Ergebnis entscheidend.) Wenn Sie Option Strict On verwenden, erkennt die Entwicklungsumgebung sofort, dass hier Probleme auftreten werden. Ohne Option Strict tritt der Fehler dagegen erst bei der tatsächlichen Ausführung des Codes auf.
4.1.5
Syntaxzusammenfassung
Umgang mit Variablen Option Explicit On
bewirkt, dass jede Variable deklariert werden muss. (Diese Einstellung gilt per Default.)
4.2 Variablentypen
127
Umgang mit Variablen Option Strict On
bewirkt, dass nur solche Typenkonvertierungen automatisch durchgeführt werden, die ohne Datenverlust durchgeführt werden können. (Diese Einstellung gilt per Default nicht.)
Dim x As objekttyp [= wert]
deklariert (und initialisiert) eine Variable.
Dim x% [=wert]
Kurzschreibweise für einige Variablentypen.
Dim x As New objekttyp()
deklarierit und initialisiert eine Objektvariable.
x = wert
weist einer Variablen einen Wert zu.
x = New objekttyp()
erzeugt ein neues Objekt der Klasse objekttyp.
x1 = x2
führt eine Variablen- oder Objektzuweisung durch. Wenn x1 und x2 Variablen für Werttypen sind (z.B. Integer), dann wird in x1 eine Kopie von x2 gespeichert. Sind x1 und x2 dagegen Objektvariablen für Referenztypen, enthält x1 nach der Zuweisung einen Verweis auf den Inhalt von x2.
4.2
Variablentypen
VERWEIS
Dieser Abschnitt gibt einen Überblick über die elementaren Variablentypen (Datentypen) von VB.NET. Genau genommen handelt es sich dabei um .NET-Klassen wie System.Boolean, System.Byte etc., die unter VB.NET aber zum Teil unter andern Namen verwendet werden können. (Beispielsweise lautet die VB.NET-Bezeichnung für System.Int16 einfach Short.) Dieser Abschnitt stellt die elementaren Datentypen nur kurz vor. Eine ausführliche Beschreibung der vielen Methoden, die es zur Bearbeitung, Formatierung und Konvertiertung von Zahlen, Daten und Zeichenketten gibt, finden Sie in Kapitel 8.
4.2.1
Ganze Zahlen (Byte, Short, Integer, Long)
Die folgende Tabelle fasst die in VB.NET vorgesehenen Datentypen zur Speicherung ganzer Zahlen zusammen. Die in der ersten Spalte angegebenen Zeichen können als Kurzschreibweise zur Kennzeichnung des Datentyps verwendet werden. Die beiden folgenden Deklarationen sind deswegen gleichwertig: Dim i As Integer Dim i%
128
4 Variablen- und Objektverwaltung
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zahlenbereich
Boolean
System.Boolean
1 Byte
True oder False
Byte
System.Byte
1 Byte
0 bis 255
Short
2 Byte
-32.768 bis 32.767
System.Int32
4 Byte
-2.147.483.648 bis 2.147.483.647
&
System.Int64
8 Byte
-2 bis 2 -1
VERWEIS
System.Int16
% Integer Long
63
63
Bitte beachten Sie, dass sich die Angaben für den Platzbedarf auf die eigentlichen Daten beziehen. Je nach Anwendung (z.B. wenn die Daten in einer als Object deklarierten Variable gespeichert werden) kann der tatsächliche Platzbedarf deutlich größer sein. Diese Interna der Variablenverwaltung werden in Abschnitt 4.6 beschrieben.
4.2.2
Fließ- und Festkommazahlen (Single, Double, Decimal)
VORSICHT
Der Defaultdatentyp für Fließkommazahlen und Fließkommaberechnungen ist Double. Dieser Datentyp kann auch intern am effizientesten verarbeitet werden. Single sollte nur dann eingesetzt werden, wenn der Platzbedarf eine wichtige Rolle spielt (etwa bei riesigen Feldern). Die Division x = 1.0 / 0.0 löst (anders als bei ganzen Zahlen) keinen Fehler aus! Stattdessen ist das Resultat einer derartigen Division der Wert Double.NegativeInfinity oder Double.PositiveInfinity. Einige weitere Informationen zu diesem interessanten Aspekt von Single- und Double-Zahlen sind in Abschnitt 8.1.3 beschrieben.
Der Datentyp Decimal eignet sich insbesondere zur Speicherung von Werten, bei denen keine Rundungsfehler auftreten dürfen. Decimal-Variablen sind daher besonders gut geeignet, wenn Geldbeträge verarbeitet werden sollen. Die Genauigkeit beträgt 28 Stellen, wobei die Position des Kommas variabel ist. (Wenn eine Zahl 10 Stellen vor dem Komma beansprucht, stehen für den Nachkommaanteil noch 18 Stellen zur Verfügung.) Decimal-Variablen sind aber auch mit Nachteilen verbunden: Rechenoperationen werden viel langsamer als mit Double-Variablen ausgeführt, der Speicherbedarf ist größer, und anders als bei Single und Double ist keine Exponentialdarstellung zulässig. Deswegen ist
der zulässige Zahlenbereich viel kleiner. Der bis VB6 zur Verfügung stehende Datentyp Currency wird von VB.NET nicht mehr unterstützt (verwenden Sie Decimal!).
4.2 Variablentypen
129
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zahlenbereich
@
Decimal
System.Decimal
12 Byte
±9,99E27 mit 28 Stellen
#
Double
System.Double
8 Byte
±1,8E308 mit 16 Stellen
!
Single
System.Single
4 Byte
±3,4E38 mit 8 Stellen
4.2.3
Datum und Uhrzeit (Date)
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zeitbereich
Date
System.DateTime
8 Byte
1.1.0001 00:00:00 bis 31.12.9999 23:59:59
Anders als in VB6, wo Daten und Zeiten als Double-Wert gespeichert werden, erfolgt die interne Repräsentierung nun durch einen Long-Wert. Dieser Wert gibt die Anzahl so genannter Ticks zu je 100 ns (Nanosekunden) an, die seit dem 1.1.0001 00:00 vergangen sind. Mit datevar.Ticks können Sie diesen internen Wert auslesen. System.DateTime sieht eigentlich einen Zeitbereich vom 1.1.0001 bis zum 31.12.9999 vor. Die VB.DOT-Dokumentation spricht hingegen vom 1.1.100 als kleinstmöglichem Datum. Diese Aussage resultiert allerdings nicht aus den internen Möglichkeiten von DateTime, sondern aus der Art und Weise, wie VB.DOT mit zweistelligen Jahrenszahlen umgeht. Wenn Sie ein zweistelliges Datum zuweisen (z.B. datevar=#12/31/15#), wird die Jahreszahl automatisch in 1930 bis 2029 umgewandelt (beim gewählten Beispiel also 2015). Ein Sonderfall ist die Zuweisung einer Uhrzeit ohne Datumsangabe: In diesem Fall verwendet auch VB als Datum dem 1.1.0001.
4.2.4
Zeichenketten (String)
HINWEIS
Zeichenketten werden intern generell im Unicode-Format gespeichert (also mit zwei Byte pro Zeichen). Das ermöglicht die Speicherung fast aller ausländischer Sonderzeichen (auch für asiatische Zeichensätze, die mehr als 256 Zeichen umfassen). Per Default speichert die Entwicklungsumgebung VB.NET-Code in ANSI-Dateien (ein Byte pro Zeichen, Codierung entsprechend der Codeseite 1252). Wenn Sie im Code Unicode-Zeichen verwenden möchten, die sich außerhalb dieser Codeseite befinden, müssen Sie den Programmcode im Unicode-Format abspeichern. Dazu führen Sie DATEI|ERWEITERTE SPEICHEROPTIONEN aus und wählen einen der zur Auswahl stehenden Unicodes aus.
130
4 Variablen- und Objektverwaltung
Unverständlich ist in diesem Zusammenhang, dass VB.NET-Code weiterhin in ANSI-Dateien (ein Byte pro Zeichen) gespeichert wird. Mit anderen Worten: VB.NET-Programme kommen zwar intern gut mit Unicode zurecht, aber im Programmcode sind Sie auf einen Ein-Byte-Zeichensatz beschränkt. Daher ist es unmöglich, in Ihrem Code einer Zeichenkette beliebige Unicode-Zeichen zuzuweisen. Ebenso kann es Probleme geben, wenn Programmcode zwischen Ländern mit einem unterschiedlichen Zeichensatz ausgetauscht wird. Eine weitere Besonderheit bei Zeichenketten besteht darin, dass es sich hierbei um unveränderliche (immutable) Objekte handelt. Wenn Sie x = x + "abc" ausführen, wird eine neue Zeichenkette gebildet und die alte Zeichenkette verworfen. (Die alte Zeichenkette wird automatisch aus dem Speicher entfernt.) Das gilt selbst dann, wenn sich die Länge der Zeichenkette nicht ändert oder wenn sich die Zeichenkette verkürzt.
HINWEIS
$
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Inhalt
Char
System.Char
2 Byte
ein Unicode-Zeichen
String
System.String
10 + 2*n Byte bis zu 2.147.483.647 Unicode-Zeichen
Wenn Char-Variablen leer sind, haben sie den Wert Chr(0). Leere String-Variablen liefern im Gegensatz dazu eine leere Zeichenkette!
4.2.5
Objekte
Wenn Sie mit Dim eine Variable deklarieren, ohne explizit einen Typ anzugeben (das ist nur möglich, wenn Option Strict Off gilt), verwendet VB.NET Object als Defaulttyp. In ObjectVariablen können beliebige Daten gespeichert werden – Zahlen, Zeichenketten, Daten und Zeiten sowie Objekte aller .NET-Klassen. Object ist daher der allgemeingültigste Variablentyp von Visual Basic. Wenn Sie sich keine Gedanken über den richtigen Datentyp machen möchten und mit Option Strict Off arbeiten (siehe Abschnitt 4.1.4), können Sie daher immer Object-Variablen verwenden. Visual Basic kümmert sich nun selbst darum, intern den richtigen Variablentyp einzusetzen. Der Preis dieser Bequemlichkeit ist aber hoch: Ihr Programm wird sowohl ineffizient als auch fehleranfällig sein (d.h., Sie werden viele Fehler erst bemerken, wenn Sie das Programm tatsächlich ausführen).
Obwohl Object auf den ersten Blick ähnliche Eigenschaften wie der aus VB6 vertraute Variablentyp Variant hat, ist er intern ganz anders realisiert. Es handelt sich dabei um die Überklasse (System.Object), von der alle anderen Variablentypen (und generell alle .NET-Klassen) abgeleitet sind. Die Object-Variable besteht im Wesentlichen aus einem Zeiger (pointer) auf die tatsächlichen Daten. Insofern beträgt der Speicherbedarf für eine noch nicht initialisierte Object-Variable nur vier Byte. Sobald die Variable tatsächlich benutzt wird, kommt dazu aber noch der Speicherbedarf für die tatsächlichen Daten.
4.2 Variablentypen
131
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Object
System.Object
4 Byte für den Zeiger (plus n Byte für die Daten)
In diesem Buch werden Sie kaum auf Object-Variablen stoßen. Stattdessen sind Objektvariablen fast immer exakt deklariert. Die folgende Anweisung deklariert dir beispielsweise als Variable, in der ein Objekt der Klasse System.IO.DirectoryInfo gespeichert werden kann. Dim dir As IO.DirectoryInfo
4.2.6
Weitere .NET-Datentypen
Bei den von VB.NET direkt unterstützten Datentypen handelt es sich um die so genannten CLS-Datentypen. CLS steht für Common Language Specification und beschreibt einen Standard für .NET-Bibliotheken. Eine CLS-konforme Bibliothek hat den Vorteil, dass sie von allen .NET-Programmiersprachen genutzt werden kann. Das setzt unter anderem voraus, dass die Schnittstelle dieser Bibliothek nur die CLS-Datentypen verwendet. Neben den CLS-Datentypen kennt .NET aber eine Reihe weiterer Datentypen. Sie finden diese Datentypen in der System-Klasse der mscorlib-Bibliothek. (Diese Bibliothek steht in allen VB.NET-Programmen immer zur Verfügung.) Die folgende Tabelle zählt lediglich die interessantesten Datentypen auf. .NET-Datentypen, die von VB.NET nicht direkt unterstützt werden System.GUID
128-Bit-Integerzahlen zur Speicherung von globally unique identifier
System.SByte
8-Bit-Integerzahlen mit Vorzeichen
System.TimeSpan
Zeitspannen (relative Zeitangaben, siehe Abschnitt 8.3.1)
System.UInt16, .UInt32 und .UInt64 16-, 32- oder 64-Bit-Integerzahlen ohne Vorzeichen
Wie das folgende Beispiel beweist, erlaubt VB.NET durchaus die Deklaration derartiger Variablen: Dim i1, i2 As System.UInt32
Auch eine gegenseitige Zuweisung (i1=i2) ist möglich. Aber sobald Sie versuchen, den Variablen einen Wert zuzuweisen (i1=3), gibt es Probleme. VB beklagt sich, dass es den Datentyp Integer nicht in System.UInt32 konvertieren kann. Dieses Problem kann noch relativ leicht umgangen werden: Mit der Funktion System.Convert.ToUInt32 können die meisten Datentypen zu UInt32 konvertiert werden. (Natürlich gibt es auch äquivalente Funktionen für UInt16 und UInt64.) i1 = Convert.ToUInt32(100)
132
4 Variablen- und Objektverwaltung
Wenn Sie nun als Nächstes versuchen, eine einfache arithmetische Operation durchzuführen (z.B. i2=i1-1), gibt es neuerlich Probleme: der Minusoperator von VB ist nicht in der Lage, einen UInt32-Wert (den Inhalt von i1) und einen Int32-Wert (1) miteinander zu verknüpfen. Zwar können Sie auch dieses Problem umgehen, aber Sie erkennen jetzt sicher, dass die Verwendung von Datentypen, die VB nicht explizit unterstützt, wenig Freude bereitet. (Ganz abgesehen davon liefert diese Subtraktion nur dann korrekte Ergebnisse wenn i1 innerhalb des Int32-Zahlenbereichs liegt – Sie haben also im Vergleich zu gewöhnlichen Integer-Variablen nichts gewonnen.)
HINWEIS
i2 = Convert.ToUInt32(System.Convert.ToInt32(i1) - 1)
C# kann im Gegensatz zu VB.NET problemlos mit UIntxx-Variablen umgehen. Gerüchten zufolge soll VB.NET das in der nächsten Version auch können.
Auch die Anwendung der anderen Datentypen aus der obigen Tabelle ist prinzipiell möglich, in der Praxis aber mit ähnlich hohem Konvertierungsaufwand verbunden. Beispiele für die Verwendung von TimeSpan-Variablen finden Sie auch in Abschnitt 8.3.1, wo es um den Umgang mit Daten und Zeiten geht.
4.3
Konstanten
Konstanten selbst definieren Mit Const können Sie Konstanten deklarieren. Die Syntax von Const entspricht weitgehend der von Dim. Der Unterschied besteht darin, dass die so deklarierten Konstanten unveränderlich sind. Const const1 As Integer = 3, const2 As Double = 4.56
Eine merkwürdige Eigenheit von selbst definierten Konstanten besteht darin, dass Sie beim Testen von Programmen in der Entwicklungsumgebung nicht sichtbar sind. Konstanten können weder im Befehlsfenster verwendet noch in Überwachungsfenstern angezeigt werden.
Vordefinierte VB-Konstante In VB.NET stehen zahllose vordefinierte Konstanten zur Verfügung. Die folgende Tabelle ist alles andere als komplett. Sie soll lediglich als erste Orientierungshilfe dienen.
4.3 Konstanten
133
Konstante
Verwendung
Definiert in
True=-1, False=0
enthalten Wahrheitswerte.
VB.NET-Sprachdefinition
Nothing
gibt an, dass die Variable nicht initialisiert (also leer) ist.
VB.NET-Sprachdefinition
vbXxx (z.B. vbCrLF, vbTab, vbYes etc.)
erhöhen die Kompatibilität mit VB6.
Microsoft.VisualBasic.Constants
Back, Cr, CrLf, FormFeed, Lf, NewLine, NullChar, Quote, Tab, VerticalTab
dienen zur Zusammensetzung von Zeichenketten (siehe Abschnitt 8.2).
Microsoft.VisualBasic.ControlChars
(Teil der VB.NET-Runtime)
(Teil der VB.NET-Runtime)
Beachten Sie, dass viele Definitionen doppelgleisig sind: sowohl ControlChars.Tab als auch vbTab enthalten den Code 9 (ein Tabulatorzeichen), sowohl MsgBoxResult.Abort als auch vbAbort enthalten den Wert 3 etc. Es ist eine Geschmacksfrage, welche Konstanten Sie verwenden.
VERWEIS
Der Vorteil der vbXxx-Konstanten besteht darin, dass diese in VB-Programmen unmittelbar verwendet werden können. Bei allen anderen Konstanten muss jeweils die dazugehörende Klasse angegeben werden (ControlChars für Char-Konstanten, MsgBoxResult und MsgBoxStyle für Konstanten zum Aufruf und zur Auswertung von MsgBox etc.) Eine Beschreibung aller Konstanten in Microsoft.VisualBasic finden Sie, wenn Sie in der Hilfe nach Konstanten Enumerationen suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/vaoriconstvba.htm
.NET-Konstanten Neben den hier beschriebenen VB-spezifischen Konstanten gibt es in den .NET-Klassenbibliotheken natürlich unzählige weitere Konstanten. Diese sind sehr oft als so genannte Enum-Aufzählungen realisiert (siehe den folgenden Abschnitt), was ihre Anwendung erleichtert. Generell müssen Sie bei der Verwendung solcher Konstanten den gesamten Klassennamen angeben, also z.B. IO.FileAttributes.Compressed für die Konstante Compressed (die eines der vielen möglichen Dateiattribute beschreibt).
134
4 Variablen- und Objektverwaltung
HINWEIS
4.4
Enum-Aufzählungen Beachten Sie bitte, dass der Begriff Aufzählung zweideutig ist. Er wird einerseits dazu verwendet, um die hier beschriebenen Enum-Konstrukte zu benennen. (Das sind Gruppen von Konstanten.) Andererseits meint Aufzählung oft auch ein Objekt des System.Collections-Namensraums. Damit können Sie Listen verwalten, in die Sie jederzeit Elemente einfügen und wieder löschen können. Diese Art von Aufzählungen werden in Kapitel 9 beschrieben.
4.4.1
Syntax und Anwendung
Mit der Anweisung Enum name – End Enum können Sie eine ganze Gruppe von Konstanten definieren. Die folgenden Zeilen demonstrieren die Syntax: Enum myColors As Integer Red 'automatisch Wert 0 Green 'automatisch Wert 1 Blue = 10 'Wert 10 Yellow 'automatisch Wert 11 End Enum mycolors gilt nun wie ein neuer Datentyp. Sie können also eine Variable vom Typ mycolors deklarieren: Dim col As myColors
Wenn Sie mit Option Strict arbeiten, können Sie dieser Variable ausschließlich die im EnumBlock definierten Konstanten zuweisen: col = myColors.Green col = 0
'OK 'nicht erlaubt, falls Option Strict On
Auch bei Vergleichen können Sie ausschließlich die Enum-Konstanten verwenden: If col = myColors.Red Then ... End If
Der Vorteil eines Enum-Blocks besteht also darin, dass die so definierten Konstanten nur im Kontext von entsprechenden Enum-Variablen verwendet werden können. Das erleichtert die Codeeingabe (der Editor schlägt automatisch die zulässigen Konstanten vor) und vermindert die Gefahr durch die versehentliche Verwendung von (für die jeweilige Variable oder Datenstruktur) ungeeigneten Konstanten.
4.4 Enum-Aufzählungen
135
Enum-Konstrukte können innerhalb von Modulen, Klassen und Strukturen sowie auf
äußerster Ebene im Code (also außerhalb anderer Konstrukte) definiert werden. Es ist aber nicht möglich, eine Enum-Aufzählung innerhalb einer Prozedur zu definieren. Zahlenwert eines Enum-Elementes ermitteln: Nach Möglichkeit sollten Sie im Programmcode im Umgang mit Enum-Aufzählungen ausschließlich die definierten Konstanten verwenden (für Zuweisungen, Vergleiche etc.). Sollte Sie aus irgendeinem Grund dennoch den Zahlenwert benötigen, können Sie diesen jederzeit mit den VB-Konvertierungsfunktionen ermitteln (also etwa CInt(col) oder CInt(myColors.Red)). Enum-Datentyp: Als Datentyp für die Konstanten kommen Byte, Short, Integer oder Long in Frage. Laut Dokumentation muss der Datentyp in der ersten Zeile des Enum-Blocks angegeben werden, wenn Option Strict verwendet wird. Tatsächlich ist die Angabe des Datentyps aber anscheinend immer optional, wobei Integer als Defaultdatentyp gilt.
Automatische Werte: Sie können jedem Enum-Element einen beliebigen (ganzzahligen) Wert zuweisen. Wenn Sie das nicht tun, verwendet VB.NET per Default 0 für das erste Element, 1 für das zweite etc. Nach den expliziten Wertzuweisungen setzt VB.NET automatisch mit n+1 fort.
HINWEIS
Es ist prinzipiell erlaubt, dass zwei Enum-Elemente denselben Wert enthalten (auch wenn das selten sinnvoll ist). Passen Sie auf, dass das nicht unbeabsichtigt passiert! Da dies syntaktisch erlaubt ist, kommt es zu keinem Fehler. Enum choices As Integer good = 10 'Wert 10 medium 'automatisch Wert 11 bad 'automatisch Wert 12 horrible = 12 'ebenfalls 12! End Enum
4.4.2
Enum-Kombinationen (Flags)
Manchmal wollen Sie in Enum-Variablen nicht einen einzelnen Zustand, sondern eine Kombination von Zuständen speichern. Ein typisches .NET-Beispiel hierfür ist System.IO. FileAttributes: Damit werden mehrere Attribute von Dateien gleichzeitig ausgedrückt (z.B. Hidden und ReadOnly). Wenn Sie selbst eine derartige Enum-Klasse deklarieren möchten, müssen Sie der Definition voranstellen. Die eckigen Klammern bedeuten, dass es sich beim Inhalt um ein so genanntes Attribut handelt, das der nachfolgenden Deklaration zusätzliche Eigenschaften verleiht. Hier wird als Attribut Flags verwendet. Intern bewirkt das, dass die Enum-Klasse zusätzliche Eigenschaften der Klasse System.FlagsAttribute erhält. (Was Attribute eigentlich sind, wird in Abschnitt 7.7 erklärt.)
136
4 Variablen- und Objektverwaltung
Bei der Definition der Konstanten müssen Sie darauf achten, dass jede Konstante eine Zweierpotenz ist (1, 2, 4, 8, 16, 32 etc.). Damit stellen Sie sicher, dass auch jede Kombination von Konstanten eindeutig ist. Sie können bei der Zuweisung der Konstanten auch hexadezimale Werte verwenden (&H1, &H2, &H4, &H8, &H10, &H20 etc.). Das folgende Beispiel zeigt die Definition der Enum-Klasse myPrivileges, mit der Zugriffsrechte verwaltet werden (z.B. für ein Dateisystem, eine Datenbank etc.). Es ist damit beispielsweise möglich, jemanden den Lesezugriff sowie die Ausführung von Programmen zu erlauben, aber Veränderungen zu verbieten. Enum myPrivileges As Integer ReadAccess = 1 WriteAccess = 2 Execute = 4 Delete = 8 End Enum
Bei der Verwendung von myPrivileges-Variablen können Sie die verschiedenen Zustände mit Or verknüpfen. (Beachten Sie, dass die mathematisch ebenfalls korrekte Verknüpfung der Konstanten durch + bei Option Strict nicht zulässig ist!) Dim priv As myPrivileges priv = myPrivileges.ReadAccess Or myPrivileges.Execute ToString liefert nun eine Zeichenkette, die die Kombination aller Enum-Konstanten ausdrückt. (Ohne das Flags-Attribut bei der Enum-Deklaration würde das nicht funktionieren!) s = priv.ToString
' s = "ReadAccess, Execute"
Wenn Sie testen möchten, ob eine Variable eine bestimmte Konstante enthält, können Sie nicht mehr einfach einen Vergleich mit = durchführen (weil dieser Vergleich natürlich False liefert, wenn die Variable eine Kombination von Konstanten enthält). Stattdessen müssen Sie den Vergleich wie das folgende Beispiel mit And formulieren. If (priv And myPrivileges.Execute) <> 0 Then ... End If
Beachten Sie, dass die Klammern und der Ausdruck <> 0 erforderlich sind, falls Sie mit Option Strict arbeiten. (a And b liefert einen Integer-Ausdruck, If erwartet aber einen BooleanAusdruck!)
4.4.3
Interna (System.Enum-Klasse)
Intern sind Enum-Konstrukte von der .NET-Klasse System.Enum abgeleitet. Das bedeutet insbesondere, dass Sie alle Eigenschaften und Methoden von System.Enum auf Enum-Konstanten und -Variablen anwenden können. Im Folgenden finden Sie hierfür einige Beispiele.
HINWEIS
4.4 Enum-Aufzählungen
137
Dieser Abschnitt geht auf einige Interna der Verwaltung von Enum-Konstrukten ein. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon etwas Erfahrung mit VB.NET haben.
Namen (Zeichenketten) von Enum-Elementen ermitteln: Wenn Sie bei einem gegebenen Enum-Wert den Namen des entsprechenden Elements wissen möchten, verwenden Sie einfach die Methode ToString: Dim s As String, col As myColors col = myColors.Green s = col.ToString 's = "Green"
Alle Namen einer Enum-Aufzählung ermitteln: Die folgende Schleife verwendet GetNames, um alle in myColors definierten Namen anzuzeigen (also "Red", "Green", "Blue" und "Yellow"). Beachten Sie, dass die Kurzschreibweise Enum.GetNames nicht zulässig ist (obwohl System sonst fast immer weggelassen werden kann), weil Enum ein VB.NET-Schlüsselwort ist. GetNames erwartet als Parameter ein Type-Objekt, das den Datentyp beschreibt. Ein derartiges Objekt wird hier mit col.GetType() erzeugt. Dim all() As String, s As String, col As myColors all = System.Enum.GetNames(col.GetType()) For Each s In all Console.WriteLine(s) Next
Enum-Element aus Zeichenkette erzeugen: Die Methode Parse wertet die angegebene Zeichenkette aus und liefert als Ergebnis ein Enum-Objekt, das der Zeichenkette entspricht. Die Definition von Parse gibt an, dass die Methode ein Objekt des Typs Object zurückgibt. Daher muss CType verwendet werden, um eine Umwandlung in ein myColors-Objekt durchzuführen. (CType wird in Abschnitt 4.6.5 beschrieben.) Die folgende Anweisung entspricht col = myColors.Red. Dim col As myColors col = CType(System.Enum.Parse(col.GetType(), "Red"), myColors)
Per Default unterscheidet Parse zwischen Groß- und Kleinschreibung. Wenn Sie das nicht möchten, müssen Sie als dritten Parameter True übergeben: col = CType(System.Enum.Parse(col.GetType(), "rEd", True), myColors) Parse kommt auch mit Enum-Kombinationen zurecht. In der Zeichenkette müssen die einzelnen Namen durch Kommas getrennt werden. Die folgende Anweisung entspricht priv = myPrivileges.ReadAccess Or myPrivileges.WriteAccess. Dim priv As myPrivileges priv = CType(System.Enum.Parse(priv.GetType(), _ "ReadAccess, WriteAccess"), myPrivileges)
138
4 Variablen- und Objektverwaltung
Testen, ob ein Enum-Wert gültig ist: Wenn Sie wissen möchten, ob ein beliebiger Wert einer Enum-Konstante entspricht, können Sie dies mit IsDefined feststellen. Diese Methode liefert True oder False. If System.Enum.IsDefined(col.GetType(), 17) Then ...
Beachten Sie, dass IsDefined für Enum-Kombinationen (siehe oben) ungeeignet ist! IsDefined(priv, 10) würde eigentlich der Kombination WriteAccess Or Delete entsprechen, liefert aber False!
4.4.4
Syntaxzusammenfassung
Aufzählungen deklarieren und verwenden Enum aufz As Byte/Short/Integer/Long element1 [ = wert1] element2 [ = wert2] ... End Enum
deklariert eine Aufzählung. Den Elementen werden automatisch durchlaufende Zahlen zugewiesen, wenn Sie nicht explizit eigene Werte angeben.
Enum komb_aufz As ... element1 = 1 element2 = 2 element3 = 4 ... End Enum
deklariert eine Aufzählung, deren Elemente kombiniert werden können. Die Elemente müssen dazu Zweierpotenzen enthalten.
Dim aufz_obj As aufz
deklariert aufz_obj als Variable der Aufzählung.
Eigenschaften und Methoden der Klasse System.Enum System.Enum.GetNames(aufz.GetType())
liefert ein Zeichenkettenfeld, das die Namen aller Enum-Konstanten enthält.
System.Enum.IsDefined(aufz.GetType(), n)
testet, ob n ein gültiger Wert einer EnumKonstante von aufz ist.
aufz_obj = CType(System.Enum.Parse( _ col.GetType(), s), aufz)
wertet die Zeichenkette s aus und liefert die entsprechende Enum-Konstante von aufz.
4.5 Felder
4.5
139
Felder
VERWEIS
Felder kommen immer dann zum Einsatz, wenn Sie mehrere gleichartige Daten (z.B. Zeichenketten, Integer-Zahlen etc.) effizient verwalten möchten. Felder sind nicht die einzige Möglichkeit zur Verwaltung von Daten. Die .NET-Bibliothek bietet eine ganze Gruppe so genannter Collection-Klassen, die für Spezialanwendungen effizienter sind als Felder. Damit können Sie beispielsweise assoziative Felder bilden (bei denen ein beliebiges Objekt und nicht eine Integer-Zahl als Index gilt), komfortabel Elemente einfügen und löschen etc. Einen Überblick über die wichtigsten Collection-Klassen sowie Tipps zu ihrer Anwendung gibt Kapitel 9.
4.5.1
Syntax und Anwendung
Felder werden in VB.NET ganz ähnlich wie Variablen deklariert. Der einzige Unterschied besteht darin, dass an den Variablennamen oder an den Variablentyp ein Klammernpaar angehängt werden muss, um so zu kennzeichnen, dass es sich um ein Feld handelt. Dim a() As Integer Dim a As Integer()
'Elementzahl noch unbekannt 'gleichwertige Alternative
Im Regelfall geben Sie auch gleich die Anzahl der Elemente an. In diesem Fall muss die Anzahl im ersten Klammernpaar angegeben werden. Eine Besonderheit von Visual Basic besteht darin, dass Dim b(3) ein Feld mit vier Elementen deklariert: a(0), a(1), a(2) und a(3). Der Zugriff auf einzelne Elemente erfolgt in der Form feldname(indexnummer). Dim b(3) As Integer b(0) = 17 b(1) = 20 b(2) = b(0) + b(1) b(3) = -7
'eindimensionales Feld, vier Elemente
Sie können Felder auch direkt bei der Deklarierung initialisieren. Bei dieser Syntaxvariante dürfen Sie allerdings keine Elementzahl angeben – VB.NET entscheidet selbst, wie viele Elemente erforderlich sind. (Beim folgenden Beispiel hat c drei Elemente, c(0), c(1) und c(2).) Dim c() As Integer = {7, 12, 39}
Wenn Sie ein Feld an eine Methode übergeben, können Sie das Feld auch dynamisch beim Aufruf der Methode übergeben. Die folgende Zeile führt die Methode AddRange für ein ListBox-Steuerelement aus. Diese Methode erwartet ein beliebiges Feld als Parameter. ListBox1.Items.AddRange(New String() {"a", "b", "c"})
140
4 Variablen- und Objektverwaltung
Felder neu dimensionieren Eine Besonderheit von VB.NET besteht darin, dass Sie Felder mit ReDim nachträglich neu dimensionieren können. Wenn Sie dabei das optionale Schlüsselwort Preserve angeben, bleibt der Inhalt des bisherigen Felds erhalten. (Beachten Sie bitte, dass ReDim ein verhältnismäßig zeitaufwendiger Vorgang ist. Wenn Sie eine große Anzahl von Elementen verwalten, wobei die genaue Anzahl von vornherein nicht feststeht, sollten Sie entweder auf eine der in Kapitel 9 beschriebenen Collection-Klassen zurückgreifen oder die Feldanzahl nur in großen Schritten erhöhen.) ReDim a(7) a(5) = 1232 ReDim Preserve a(12)
Mehrdimensionale Felder Mehrdimensionale Felder werden einfach dadurch erzeugt, dass Sie bei Dim mehrere Indizes angeben. Beim folgenden Feld reicht der erste Index von 0 bis 3, der zweite von 0 bis 4, der dritte von 0 bis 5. Insgesamt hat das Feld somit 4*5*6=120 Elemente. Dim d(3, 4, 5) As Integer ReDim kann auch auf mehrdimensionale Felder angewendet werden, allerdings darf dabei nur die Größe der äußersten Dimension verändert werden. (Nach Dim a(3,4,5) dürfen Sie also ReDim a(3,4,6) ausführen, nicht aber ReDim a(4,4,5).)
Feldgröße ermitteln Die Eigenschaft feld.Rank liefert die Anzahl der Dimensionen. (d.Rank liefert 3.) Für jede Dimension kann der zulässige Indexbereich mit feld.GetLowerBound(n) und feld.GetUpperBound(n) ermittelt werden, wobei n die Dimension ist. (Für die erste Dimension gilt n=0!) Die Gesamtzahl aller Elemente kann mit feld.Length ermittelt werden. Die folgenden Zeilen zeigen die Initialisierung eines dreidimensionalen Felds. For i = 0 To d.GetUpperBound(0) For j = 0 To d.GetUpperBound(1) For k = 0 To d.GetUpperBound(2) d(i, j, k) = i * 100 + j * 10 + k Next Next Next
Statt GetUpper/LowerBound können Sie auch die VB-Schlüsselwörter UBound bzw. LBound verwenden. Dabei müssen Sie aber beachten, dass die erste Dimension nun mit n=1 ausgedrückt wird.
4.5 Felder
141
HINWEIS
For i = 0 To UBound(d, 1) For j = 0 To UBound(d, 2) For k = 0 To UBound(d, 3) ...
Bei Feldern, die Sie selbst mit Dim erzeugt haben, können Sie auf die Auswertung von GetLowerBound verzichten: VB.NET-Felder beginnen immer mit dem Index 0. Bei Feldern, die von anderen Bibliotheken stammen, ist eine Auswertung aber sehr wohl sinnvoll, weil .NET grundsätzlich auch Felder mit einem von 0 abweichenden Startindex unterstützt.
For-Each-Schleifen Eine besonders einfache Form, alle Elemente eines Felds zu durchlaufen, bilden die in Abschnitt 5.2.2 vorgestellten For-Each-Schleifen. Die Schleifenvariable muss dabei denselben Datentyp wie das Feld aufweisen. (Durch die Anweisung Console.WriteLine wird der Inhalt von i in einem Konsolenfenster angezeigt.) Dim c() As Integer = {7, 12, 39} Dim i As Integer For Each i In c Console.WriteLine(i) Next
Felder löschen Mit Erase können Sie alle Elemente eines Felds löschen und den so reservierten Speicher wieder freigeben. Erase a, b, c, d
HINWEIS
4.5.2
Interna und Programmiertechniken (System.Array-Klasse)
Dieser Abschnitt beschreibt einige Interna bei der Verwaltung von Feldern. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon etwas Erfahrung mit VB.NET haben.
142
4 Variablen- und Objektverwaltung
Intern sind alle Felder von der .NET-Klasse System.Array abgeleitet. Das bedeutet, dass Sie alle Eigenschaften und Methoden von System.Array zur Bearbeitung von Feldern anwenden können. Die im vorigen Abschnitt vorgestellten Eigenschaften bzw. Methoden GetUpper/LowerBound, Length und Rank sind dafür einfache Beispiele. Die folgenden Abschnitte beschreiben einige weitergehende Möglichkeiten. Beachten Sie bitte, dass die Methoden von System.Array zum Teil direkt auf Feldvariablen angewendet werden können (z.B. feld.Rank), zum Teil aber das Feld als Parameter erwarten (z.B. Array.Referse(feld)).
Feldelemente löschen Clear setzt eine vorgegebene Anzahl von Elementen auf 0, Nothing oder False (je nach Datentyp des Elements). Bei einem Integer-Feld entspricht Array.Clear(f, 7, 2) den Anweisungen f(7)=0 und f(8)=0.
Felder kopieren Mit Clone können Sie eine vollständige Kopie eines Felds erzeugen. Clone liefert allerdings ein Object-Feld, das mit CType in ein Integer-Feld umgewandelt werden muss. Die folgenden Zeilen zeigen die Anwendung der Methode. Wenn Sie anschließend b(3) auswerten, enthält dieses Element erwartungsgemäß den Wert 5. (CType wird in Abschnitt 4.6.5 beschrieben.) Dim a(10) As Integer Dim b() As Integer a(3) = 5 b = CType(a.Clone, Integer())
Wenn Sie nicht einfach alle Elemente kopieren möchten, können Sie die Methode Copy zu Hilfe nehmen. Im folgenden Beispiel werden aus dem Feld c sechs Elemente nach d kopiert, wobei das Kopieren in c beim Element 2 und in d beim Element 0 beginnt. Die CopyAnweisung entspricht also d(0)=c(2), d(1)=c(3), d(2)=c(4) etc. Im Konsolenfenster werden daher die Werte 2, 3, 4, 5, 6 und 7 ausgegeben. Dim c(10) As Integer Dim d(5) As Integer Dim i As Integer For i = 0 To 10 c(i) = i Next Array.Copy(c, 2, d, 0, 6) For i = 0 To 5 Console.WriteLine(d(i)) Next
HINWEIS
4.5 Felder
143
Wenn Sie ein Feld kopieren, das Objekte von Referenztypen enthält (keine ValueType-Daten), dann wird ein so genanntes shallow copy durchgeführt. Das bedeutet, dass nur die Referenzen kopiert werden, dass aber von den Objekten selbst keine Kopie erstellt wird. Beide Felder verweisen dann auf dieselben Objekte.
Reihenfolge der Elemente vertauschen Array.Reverse dreht die Reihenfolge der Elemente um. Das letzte Element eines eindimensionalen Felds wird damit zum ersten (und umgekehrt). For i = 0 To 9 e(i) = i Next Array.Reverse(e)
'nun gilt e(0)=9, e(1)=8 etc.
Felder sortieren und durchsuchen Mit Array.Sort können Sie das als Parameter übergebene Feld sortieren. Durch optionale Parameter kann auch nur ein Teil des Felds sortiert werden. Ein Sortieren ist nur möglich, wenn die im Feld gespeicherten Objekte (z.B. Zahlen, Zeichenketten) vergleichbar sind. Wenn Sie ein bestimmtes Element in einem Feld suchen, müssen Sie in der Regel alle Elemente durchlaufen. Wenn das Feld aber bereits sortiert ist, können Sie die Suche ganz wesentlich beschleunigen, indem Sie die Methode BinarySearch zu Hilfe nehmen. Diese Methode benötigt beispielsweise zur Suche in einem Feld mit 1000 Einträgen nur maximal zehn Vergleichsvorgänge. BinarySearch liefert als Ergebnis entweder die positive Indexnummer des gefunden Elements oder eine negative Nummer, wenn das Element nicht gefunden wurde. ' Beispiel variablen\felder Sub sort_array() Dim i As Integer Dim n As String 'einige VB-Autoren ... Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"} ' sortieren Array.Sort(s) ' suchen i = Array.BinarySearch(s, "Dan Appleman") Console.WriteLine("Suche nach Dan Appleman: Index = " + _ i.ToString + " Element = " + s(i)) End Sub
VERWEIS
144
4 Variablen- und Objektverwaltung
Optional können Sie sowohl an Sort als auch an BinarySearch ein Objekt übergeben, dessen Klasse die Schnittstelle Collections.IComparer realisiert: Dann werden bei jedem Vergleichsvorgang zwei Elemente des Felds an die Compare-Funktion dieser Klasse übergeben. Die Funktion vergleicht die beiden Elemente und liefert als Ergebnis -1, 0 oder 1, je nachdem, ob das erste Objekt kleiner, gleich oder größer als das zweite war. Auf diese Weise können Sie die eingebaute Sort-Methode mit beliebigen Vergleichskriterien verbinden. Beispielsweise könnten Sie das obige String-Feld dann nach Familiennamen sortieren. Hintergrundinformationen und Beispiele zum individuellen Sortieren von Feldern und Aufzählungen finden Sie in Abschnitt 9.3.2.
Asymmetrische Felder Mit Dim können Sie nur symmetrische Felder erzeugen, also beispielspielsweise ein Feld mit 10*10 Elementen. In manchen, überwiegend mathematischen Anwendungen wären aber asymmetrische Felder sinnvoll, bei denen die Anzahl der Elemente in jeder Zeile (in jedem Segment) variiert. Das folgende Beispiel zeigt, wie eine Matrix verwaltet werden kann, bei der in der Zeile n jeweils n Elemente gespeichert werden können (in der ersten Zeile also ein Element, in der zweiten Zeile zwei etc.). Dazu wird arr als ein Feld deklariert, dessen Elemente den Typ Array haben, also selbst wieder Felder enthalten dürfen. Diese Felder werden in einer Schleife mit der Methode CreateInstance erzeugt. An die Methode müssen der gewünschte Datentyp sowie die Anzahl der Elemente übergeben werden. Leider haben mit CreateInstance erzeugte Felder zwei wesentliche Nachteile: Erstens können die Feldelemente nicht mit arr(i,j) angesprochen werden. Stattdessen müssen die Elemente mit arr(i).GetValue(j) gelesen bzw. mit arr(i).SetValue(val, j) verändert werden. Zweitens ist das Feld intern immer ein Object-Feld. Wenn Sie ValueType-Daten (Werttypen) speichern, werden die Elemente intern durch boxing in Objekte umgewandelt. Daher ist die Verwaltung asymmetrischer Felder relativ langsam, womit das hier vorgestellte Verfahren für mathematische Algorithmen leider ungeeignet ist. ' Beispiel variablen\felder Sub asymetric_array() Const n As Integer = 5 Dim i, j As Integer ' Feld deklarieren Dim arr(n - 1) As Array For i = 0 To n - 1 arr(i) = Array.CreateInstance(i.GetType(), i + 1) Next
4.5 Felder
145
' Feld initialisieren For i = 0 To n - 1 For j = 0 To i arr(i).SetValue(i * 100 + j, j) Next Next ' Feldinhalt anzeigen For i = 0 To n - 1 For j = 0 To i Console.Write(arr(i).GetValue(j).ToString + " Next Console.WriteLine() Next End Sub
")
Das Unterprogramm liefert folgendes Ergebnis im Konsolenfenster: 0 100 200 300 400
101 201 301 401
4.5.3
202 302 402
303 403
404
Syntaxzusammenfassung
Felder deklarieren und verwenden Dim f(7) As datentyp
deklariert ein eindimensionales Feld mit acht Elementen, die mit feld(0) bis feld(7) angesprochen werden.
Dim f(n, m, o, p) As datentyp
deklariert ein vierdimensionales Feld.
ReDim [Preserve] f(n)
verändert die Größe des Felds (optional unter Erhaltung des Inhalts).
Erase f
löscht das Feld.
LBound(f,n) UBound(f, n)
ermittelt den minimalen bzw. maximalen Index des Felds für die Dimension n (mit n=0 für die erste Dimension).
146
4 Variablen- und Objektverwaltung
Eigenschaften und Methoden der Klasse System.Array Array.BinarySearch(f, suchobj) Array.BinarySearch(f, suchobj, icompareobj)
durchsucht das Feld f nach dem Eintrag suchobj. Die Methode setzt voraus, dass das Feld sortiert ist, und liefert als Ergebnis die Indexnummer des gefundenen Eintrags. Optional kann eine eigene Vergleichsmethode angegeben werden.
Array.Clear(f, n, m)
setzt m Elemente beginnend mit f(n) auf 0, Nothing oder False (je nach Datentyp des Elements).
f2 = CType(f1.Clone, datentyp())
weist f2 eine Kopie von f1 zu.
Array.Copy(f1, n1, f2, n2, m)
kopiert m Elemente vom Feld f1 in das Feld f2, wobei n1 der Startindex in f1 und n2 der in f2 ist.
f = Array.CreateInstance(type, n [,m [,o]])
erzeugt ein Feld der Größe (n,m,o), wobei in den einzelnen Elementen Objekte des Typs type gespeichert werden können.
f.GetLowerBound(n) f.GetUpperBound(n)
ermittelt den minimalen bzw. maximalen Index des Felds für die Dimension n (mit n=0 für die erste Dimension).
f.GetValue(n [,m [,o]])
liefert das Element f(n, m, o).
f.Length
ermittelt die Gesamtzahl der Elemente des Felds.
f.Rank
gibt die Anzahl der Dimensionen an.
Array.Reverse(f)
vertauscht die Reihenfolge der Elemente des Felds.
f.SetValue(data, n [,m [,o]])
speichert in f(n, m, o) den Wert data.
Array.Sort(f [ ,icompareobj ] )
sortiert f (unter Anwendung der Vergleichsfunktion des ICompare-Objekts).
4.6
Interna der Variablenverwaltung
Dieser Abschnitt geht auf einige Interna der Variablenverwaltung und insbesondere der Speicherverwaltung ein. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon einige Erfahrung mit VB.NET haben.
4.6 Interna der Variablenverwaltung
4.6.1
147
Speicherverwaltung
Wie bereits in Abschnitt 4.1.2 beschrieben, ist in VB.NET jede Variable eine Objektvariable. Um aber elementare Datentypen wie Integer oder Double bzw. einfache Datenstrukturen möglichst effizient verwalten zu können, unterscheidet .NET zwischen Wert- und Referenztypen (bzw. zwischen ValueType-Klassen und gewöhnlichen Klassen). Diese Unterscheidung hat einen ganz wesentlichen Einfluss darauf, wie die Daten im Speicher verwaltet werden: •
Gewöhnliche Objekte (für Referenztypen) werden in einem eigenen Speicherbereich, dem so genannten heap, gespeichert. Das ist deswegen sinnvoll, weil derartige Objekte meistens eine variable Größe haben und weil Objekte typischerweise dynamisch während des Programms erzeugt und wieder gelöscht werden. Um die Speicherverwaltung im heap kümmert sich die .NET-Runtime-Bibliothek. Nicht mehr benötigte Objekte werden bei einer so genannten garbage collection (wörtlich: Müllabfuhr) automatisch wieder freigegeben. Einige Feinheiten der garbage collection werden im nächsten Abschnitt beschrieben. Entscheidend für Sie als Programmierer(in) ist an dieser Stelle nur: Sie brauchen sich im Regelfall nicht selbst um die Speicherverwaltung zu kümmern. Bei Prozeduraufrufen werden in Parametern Zeiger (Referenzen) auf die Daten übergeben (nicht die Daten selbst). Ebenso wird bei einer Variablenzuweisung (obj1 = obj2) nur ein neuer Zeiger (eine neue Referenz) auf das Objekt eingerichtet. obj1 und obj2 zeigen nun also auf dasselbe Objekt, dessen Daten sich im heap befinden.
•
VERWEIS
Gewöhnliche Objekte und ValueType-Daten verhalten sich vollkommen unterschiedlich, wenn sie mit ByVal an eine Prozedur übergeben werden! Lesen Sie unbedingt auch den diesbezüglichen Abschnitt 5.3.4!
TIPP
ValueType-Daten (für Werttypen wie Integer) werden dagegen direkt in Datenstrukturen gespeichert (allocated inline a structure). Bei Prozeduraufrufen werden die Daten in den Stack kopiert. Bei einer Variablenzuweisung (obj1 = obj2) werden die Daten kopiert.
Wenn Sie feststellen möchten, ob eine Objektvariable Referenz- oder ValueType-Daten enthält, werten Sie einfach obj.GetType().IsValueType aus!
Boxed value types Aus Effizienzgründen werden ValueType-Objekte also anders behandelt als gewöhnliche Objekte. Es gibt aber Fälle, wo ValueType-Objekte auch intern wie gewöhnliche Objekte dargestellt werden müssen – beispielsweise, wenn ein Integer-Wert in einer als Object deklarierten Variable, einem Object-Feld oder einer Collection gespeichert wird.
148
4 Variablen- und Objektverwaltung
Dazu wird am heap Speicher für eine Zwischenschicht (einen so genannten wrapper) reserviert. Die Daten des ValueType-Objekts werden dorthin kopiert. Dieser Vorgang wird als boxing bezeichnet, die resultierenden Daten boxed value types. Die Rückverwandlung in gewöhnliche ValueType-Daten heißt dementsprechend unboxing.
4.6.2
Garbage collection
Der Begriff garbage collection (wörtlich übersetzt: Müllabfuhr) bezeichnet das Aufräumen des Speichers. Die garbage collection wird automatisch ausgeführt, wobei Sie als Proramierer normalerweise keinen keinen Einfluss haben, wann das passiert. Vielmehr beobachtet die .NET-Bibliothek die Nutzung von Objekten und den Speicherbedarf und entscheidet selbst, wann der Speicher von nicht mehr benötigten Objekten befreit werden muss. Beachten Sie, dass die garbage collection ausschließlich für den heap gilt, auf dem Objekte von Referenztypen gespeichert werden. ValueType-Daten sind von einer garbage collection nicht betroffen.
Dispose-Methode, IDisposable-Schnittstelle Zu den Werbeversprechungen von .NET zählt, dass Sie sich nun nicht mehr um die Speicherverwaltung zu kümmern brauchen. Nicht mehr benötigte Objekte werden automatisch aus dem Speicher entfernt, ohne dass Sie die Objekte explizit löschen müssen. Wie bei vielen Werbeversprechen ist das aber nur die halbe Wahrheit. Die ganze Wahrheit ist, dass die automatische garbage collection nur für solche Objekte zufriedenstellend funktioniert, deren Klassen die Schnittstelle IDisposeable nicht implementieren. In diese Gruppe fallen zwar viele .NET-Klassen, aber bei weitem nicht alle.
VERWEIS
Daneben gibt es zahlreiche Klassen, die die Schnittstelle IDisposable implementieren und daher die Methode Dispose unterstützen. (Was eine Schnittstelle ist, wird erst in Abschnitt 7.5 ausführlich beschrieben. Die Kurzfassung: Eine Schnittstelle definiert eine Reihe von Eigenschaften und Merkmalen, die eine bestimmte Klasse auszeichnen. Die Formulierung eine Klasse x implementiert eine Schnittstelle y bedeutet, dass die Klasse x neben diversen eigenen Eigenschaften und Methoden auch alle Eigenschaften und Methoden von y kennt.) Eine Liste vieler, wenn auch nicht aller .NET-Klassen, die Dispose kennen, finden Sie in der Online-Hilfe bei der Beschreibung der IDisposable-Schnittstelle. Zu den dort genannten Klassen zählen unter anderem viele Klassen zum Umgang mit Dateien (System.IO.*), die meisten Klassen zur Grafikprogrammierung (System.Drawing.*), die meisten Klassen zur Windows-Programmierung (System.Windows.Forms.*) etc. ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemIDisposableClassTopic.htm
Mit der IDisposable-Schnittstelle sind vor allem Klassen ausgestattet, deren Objekte viel Speicherplatz konsumieren (es also wichtig ist, dass dieser Speicher sofort, und nicht irgendwann, freigegeben wird) oder deren Objekte knappe Ressourcen beanspruchen (z.B. Datenbankverbindungen).
4.6 Interna der Variablenverwaltung
149
Im Zusammenhang mit Dispose gibt es drei Überlebensregeln: •
Wenn eine Klasse die Dispose-Methode kennt, dann muss sie ausgeführt werden, wenn ein selbst erzeugtes Objekt dieser Klasse nicht mehr benötigt wird! Dim bm As New Drawing.Bitmap(100, 100) 'Bitmap mit 100*100 Pixel ... Bitmap bearbeiten, anzeigen, speichern etc. bm.Dispose() 'Speicher wieder freigeben
•
Der Code von Prozeduren, die IDisposable-Objekte verwenden, muss so abgesichert werden, dass Dispose auch dann ausgeführt wird, wenn ein Fehler auftritt. (Das gilt insbesondere dann, wenn Sie Klassenbibliotheken programmieren, die beim Auftreten eines Fehlers nicht gleich beendet werden.)
•
Dispose darf nicht für Objekte verwendet werden, die als Parameter einer Ereignispro-
zedur übergeben werden. Der Grund besteht darin, dass das Objekt unter Umständen von der Prozedur, die das Ereignis ausgelöst hat, noch benötigt wird. Es drängt sich nun die Frage auf: Was passiert, wenn auf die Ausführung von Dispose vergessen wird? Eine allgemeingültige Antwort ist leider nicht möglich, weil die Folgen stark von der Natur der Klasse abhängen. Die folgenden Punkte nennen zwei mögliche Konsequenzen: •
Ihr Programm benötigt mehr Speicher als notwendig und wird deswegen ineffizient. Speicherproblem treten vor allem dann auf, wenn Sie eine Menge Objekte, die viel Speicher beanspruchen, in kurzer Zeit erzeugen, verwenden und dann nicht durch Dispose freigeben. Durch eine garbage collection werden zwar auch solche Objekte früher oder später wieder aus dem Speicher entfernt, bei denen Dispose vergessen wurde, aber manchmal ist später eben schon zu spät: Da beansprucht Ihr Programm vielleicht schon einige Hundert Megabyte RAM und zwingt zur langsamen Auslagerung von RAM auf die Festplatte. Immerhin ist es beruhigend zu wissen, dass das Vergessen von Dispose im Regelfall keinen anhaltenden Speicherverlust (memory leak) verursacht, sondern nur die Effizienz der Speicherverwaltung (unter Umständen erheblich) beeinträchtigt.
•
VERWEIS
Ihr Programm beansprucht wertvolle Ressourcen (etwa den Zugriff auf eine Datei oder Datenbank). Während dieser Zeit sind andere Programme (unter Umständen sogar das eigene Programm!) blockiert. Bei vielen System.IO-Klassen zum Zugriff auf Dateien und Verzeichnisse wird statt Dispose üblicherweise die Methode Close ausgeführt – siehe auch Kapitel 10. Eine ausführliche Diskussion zur Verwendung von Dispose für Grafikobjekte finden Sie in Abschnitt 16.1.3. Darüber hinaus wird Dispose auch im Kontext mit einer ganzen Reihe weiterer Klassen beschrieben – werfen Sie einen Blick in das Stichwortverzeichnis beim Eintrag Dispose!
VERWEIS
150
4 Variablen- und Objektverwaltung
Natürlich können Sie auch eigene Klassen mit der IDisposable-Schnittstelle ausstatten; dann müssen Sie die entsprechende Dispose-Methode selbst programmieren. Konkrete Tipps, wie eine derartige Prozedur aussehen könnte, gibt Abschnitt 7.5.2.
Garbage collection manuell auslösen Normalerweise kümmert sich .NET selbstständig darum, eine garbage collection durchzuführen, wenn dies notwendig erscheint. In seltenen Fällen kann es sinnvoll sein, die garbage collection manuell auszuführen – z.B. wenn Sie wissen, dass Ihr Programm in den nächsten Sekunden keine zeitkritischen Operationen durchführen muss. Dazu führen Sie einfach GC.Collect() aus. Beachten Sie, dass die garbage collection in einem eigenen Thread – also quasi parallel zum Hauptprogramm – ausgeführt wird. Die Collect-Methode initiiert diesen Vorgang nur; anschließend wird das Hauptprogramm sofort fortgesetzt. Wenn Sie warten möchten, bis die garbage collection abgeschlossen ist, müssen Sie anschließend noch GC.WaitForPendingFinalizers() ausführen.
Speicherverbrauch ermitteln Mit GC.GetTotalMemory() können Sie den heap-Speicherbedarf ermitteln. An die Methode müssen Sie True oder False übergeben, je nachdem, ob Sie auf das Ende der durch GetTotalMemory ausgelösten garbage collection warten möchten oder nicht. Der so ermittelte Wert hat allerdings wenig mit dem tatsächlich vom Programm beanspruchten Speicher zu tun. Insbesondere gibt es Objekte, bei denen zwar einige Verwaltungsinformationen am heap, weitere Daten aber in anderen Speicherbereichen abgelegt werden (z.B. Bitmaps). Eine Reihe anderer Speicherbedarfparameter können Sie aus den Eigenschaften eines Diagnostics.Process-Objekts entnehmen. Dazu müssen Sie mit GetCurrentProcess ein ProcessObjekt erzeugen. Anschließend können Sie dessen Eigenschaften auslesen. (Diese Eigenschaften geben eine statische Momentaufnahme wieder. Die Speicherwerte des Objekts verändern sich nur, wenn die Methode Refresh ausgeführt wird.) Mit PrivateMemorySize können Sie den Speicherbedarf des Programms ermitteln soweit der Speicher nicht gemeinsam von mehreren Programmen genutzt wird. Beachten Sie, dass PrivateMemorySize nur unter Windows NT/2000/XP ermittelt werden kann. Unter Windows 98/ME kommt es zu einem Fehler. Dim pr As Diagnostics.Process Dim n1, n2 As Integer pr = Process.GetCurrentProcess() n1 = pr.PrivateMemorySize n2 = pr.PeakVirtualMemorySize ... pr.Dispose()
4.6 Interna der Variablenverwaltung
151
HINWEIS
Mit dem in Abbildung 4.2 dargestellten Beispielprogramm variablen\garbage-collection können Sie die Mechanismen der Speicherverwaltung experimentell erforschen. Mit den verschiedenen Buttons können Sie verschiedene speicherintensive Operationen durchführen (die zum Teil im nächsten Abschnitt beschrieben werden). Dabei werden zwar nie durch Dispose Objekte freigegeben, es wird aber einmal pro Sekunde GetTotalMemory ausgeführt. Dadurch wird regelmäßig eine oberflächliche garbage collection ausgeführt. Mit einem eigenen Button können Sie eine tiefgreifende garbage collection auslösen. Eine garbage collection kann unterschiedlich gründlich durchgeführt werden: Durch GetTotalMemory wird offensichtlich nur eine schnelle, aber eben auch etwas schlampige garbage collection ausgelöst. GC.Collect(n) limitiert die garbage collection auf n Stufen. Erst GC.Collect() führt die garbage collection so gründlich wie möglich durch.
Abbildung 4.2: Speicherbedarf ermitteln
Das Verhalten des Programms ist bisweilen rätselhaft: Beispielsweise wird durch das Erzeugen zahlreicher Bitmaps kaum heap-Speicher beansprucht, dafür aber eine Menge sonstiger Speicher. Der Speicher wird nach einer Weile durch eine automatische garbage collection wieder freigegeben. Der heap-Speicher, der durch das Deklarieren eines großen Felds beansprucht wird, wird beinahe sofort automatisch wieder freigegeben. In diesem Fall bleibt aber der durch die Eigenschaft PrivateMemorySize ermittelte Speicherbedarf auf einem relativ hohem Niveau, das auch durch eine explizite garbage collection nicht oder zumindest nur teilweise verkleinert werden kann.
4.6.3
Speicherbedarf
Einzelne Variablen: Die in Abschnitt 4.2 angegebenen Werte für den Platzbedarf elementarer .NET-Datentypen sind mit Vorsicht zu genießen. Sie gelten nur, wenn die Daten in entsprechend deklarierten Variablen gespeichert werden (also ein Integer-Wert in einer Integer-Variable). Wenn dagegen ein Integer-Wert in einer als Object deklarierten Variable gespeichert wird, werden die Daten als boxed value types dargestellt, wodurch sich ein
152
4 Variablen- und Objektverwaltung
zusätzlicher Speicherbedarf von vermutlich acht Byte ergibt. (Dieser Wert ist nicht dokumentiert, geht aber aus Experimenten mit großen Object-Feldern hervor.) Damit nicht genug: Auch wenn zur Speicherung der eigentlichen Daten einer Boolean-, Byte-, Short- und Char-Variablen nur ein bzw. zwei Byte erforderlich sind, werden tatsächlich meist vier Byte reserviert. Der Grund besteht diesmal darin, dass sich dann für den Variablenzugriff so genannte 32-Bit-Aligned-Adressen ergeben, was die Geschwindigkeit der Codeausführung steigert. Das hat zur Folge, dass zur Speicherung eines Byte-Werts je nach Anwendung bis zu zwölf Byte notwendig sind. Felder: Dennoch ist auch die Behauptung, eine Byte-Variable würde nur ein Byte Speicherplatz beanspruchen, richtig. Wenn Sie nämlich ein großes Byte-Feld deklarieren, beansprucht jedes Element dieses Felds tatsächlich nur ein Byte (hier also insgesamt 10 MB). Dazu kommen noch ein paar Byte zur Verwaltung des Felds, aber bei großen Feldern ist der Overhead jetzt wirklich vernachlässigbar. Dim bytearray( 10000000) As Byte
'10 MByte
Object-Felder: Bei der Deklaration eines Object-Felds werden vier Byte pro Element reserviert. (Diese vier Byte dienen nicht zur Speicherung von Daten, sondern zur Speicherung des Verweises auf die Daten!) Das folgende Feld beansprucht daher ca. 40 MB. Dim myarray(10000000) As Object
'40 MByte
Wenn Sie nun in einer Schleife jedes Feldelement mit einem Boolean-Wert belegen (True), werden die entsprechenden Boolean-Objekte erzeugt und in myarray Verweise darauf eingerichtet. For i = 0 To 10000000 myarray(i) = True Next
Wie groß ist nun der gesamte Speicherbedarf? Es sind beachtliche 160 MB! Dieser Betrag ergibt sich aus den bereits erwähnten 40 MB für myarray sowie aus zwölf Byte für jedes einzelne Feldelement. (Da in jedem einzelnen Element eines Object-Felds ein anderer Objekt- oder Datentyp gespeichert werden könnte, muss auch bei jedem einzelnen Element der gesamte objektorientierte Overhead gespeichert werden.) Bei einem Standard-PC, der mit mindestens 256 MByte RAM ausgestattet ist, spielt es keine große Rolle, ob eine Boolean-Variable nun ein oder zwölf Byte beansprucht. Wenn Sie aber mit großen Feldern zu tun haben, spielt die Wahl des richtigen Datentyps eine wichtige Rolle. String-Felder: Laut Dokumentation beansprucht eine String-Variable zehn Byte plus zwei Byte pro gespeichertem Zeichen. Diese Angabe lässt sich aber nur schwer messen. Daher wurden einige Experimente mit großen String-Feldern durchgeführt. Nicht initialisierte Elemente eines String-Felds beanspruchen ebenso wie nicht initialisierte Object-Elemente nur vier Byte. Das folgende Feld belegt daher vorerst nur ca. 40 MByte. Dim s(10000000) As String
'40 MByte
4.6 Interna der Variablenverwaltung
153
Überraschenderweise erhöht auch die folgende Schleife den Speicheraufwand nicht nennenswert. Es wird nämlich nur eine einzige Zeichenkette mit dem Inhalt "x" erzeugt! Alle zehn Millionen String-Variablen verweisen darauf. (Das ist deswegen möglich, weil Zeichenketten als unveränderliche Objekte gelten. Sobald der Inhalt eines beliebigen Elements dieses Felds verändert wird, wird automatisch Platz für die neue Zeichenkette reserviert.) For i = 0 To 10000000 s(i) = "x" Next
'noch immer 40 MByte
Wenn Sie dagegen allen Elementen des Felds zufällige Zeichenketten (jeweils nur ein Zeichen lang) zuweisen, steigt der Speicherverbrauch dramatisch auf ca. 240 MByte an – also auf 24 Byte pro Feldelement. Das ist deutlich mehr, als die VB.NET-Dokumentation vermuten lässt (zehn Byte plus zwei Byte pro Zeichen). Dim r As New Random() For i = 0 To 10000000 s(i) = Chr(r.Next(65, 90)) Next
'ca. 240 MByte 'zufälliger Buchstabe zwischen A und Z
Zum Abschluss noch der Speicheraufwand, wenn etwas längere Zeichenketten verwendet werden: For i = 0 To 10000000 'ca. 280 MByte s(i) = "a" + Chr(r.Next(65, 85)) Next For i = 0 To 10000000 'ca. 320 MByte s(i) = "abcd" + Chr(r.Next(65, 85)) Next
Eine plausible Begründung für den nicht der Dokumentation entsprechenden Speicherverbrauch kann ich leider nicht anbieten. Das einzige Fazit kann eigentlich nur lauten, der Dokumentation nicht einfach blind zu vertrauen, sondern dann und wann ein kleines Experiment durchzuführen.
4.6.4
Variablen- bzw. Objekttyp feststellen (GetType)
Dieser Abschnitt stellt einige Methoden vor, mit denen Sie den Typ einer Variable feststellen können. (Zu diesem Abschnitt gibt es das Beispielprogramm variablen\var-types, das aber aus Platzgründen und wegen des geringen Informationswerts nicht abgedruckt ist. Sie können das Beispielprogramm als Ausgangspunkt für eigene Experimente mit den hier vorgestellten Methoden verwenden.) In diesem Zusammenhang ist bemerkenswert, dass in VB.NET jede Variable bzw. jedes Objekt zur Laufzeit (also bei der Programmausführung) weiß, welchen Datentyp sie enthält bzw. von welcher Klasse es abgeleitet ist. Die hier vorgestellten Funktionen helfen nur beim Ermitteln dieser Informationen.
154
4 Variablen- und Objektverwaltung
TypeName Die Visual-Basic-Methode TypeName liefert eine Zeichenkette mit dem Variablentyp (den Klassennamen) der angegebenen Variable. Für Dim i As Integer liefert TypeName(i) die Zeichenkette Integer. Bei noch nicht initialisierten reference-Objekten liefert TypeName als Ergebnis "Nothing". Bei initialisierten reference-Objekten liefert TypeName den tatsächlichen Datentyp. Wenn Sie also Dim o As Object und dann o=1.5 ausführen, liefert TypeName das Ergebnis "Double". Bei Objekten, deren Klassenname aus mehreren Teilen zusammengesetzt ist, liefert TypeName nur den letzten Teil (also z.B. "FileInfo" bei einem Objekt der Klasse System.IO.FileInfo).
TypeOf obj Is className In manchen Fällen ist der Operator TypeOf eine Alternative zu TypeName. TypeOf muss immer mit Is kombiniert werden. Die Syntax sieht so aus: Dim o As Object o = ... If TypeOf o Is System.IO.FileInfo Then ... End If
Die obige Abfrage gilt dann als erfüllt, wenn o ein Objekt der Klasse System.IO.FileInfo oder einer davon abgeleiteten Klasse enthält. Wenn o noch nicht initialisiert ist (also Nothing enthält), ist die TypeOf-Abfrage nicht erfüllt. Der Vorteil von TypeOf besteht darin, dass der Typenvergleich viel schneller ausgeführt wird als durch If TypeName(o)="System.IO.FileInfo". Allerdings kann TypeOf ausschließlich für Referenztypen eingesetzt werden, nicht aber für Werttypen (ValueType-Klassen). Bei Objekten, deren Klasse aufgrund von Vererbung auf anderen Basisklassen aufbaut, kann TypeOf auch zum Test dieser Basisklassen verwendet werden. Das folgende Beispiel basiert auf einem Objekt der Klasse System.IO.FileInfo, deren Klassenhierarchie hier dargestellt wird. Klassenhierarchie für System.IO.FileInfo Object └─ MarshalByRefObject
│ └─ IO.FileSystemInfo └─ IO.FileInfo
.NET-Basisklasse Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für DirectoryInfo und FileInfo Informationen über Dateien ermitteln
Aufgrund dieser Hierarchie sind alle vier folgenden If-Abfragen erfüllt.
4.6 Interna der Variablenverwaltung
155
Dim o As Object o = New IO.FileInfo("c:\readme.txt") If TypeOf o Is Object Then ... If TypeOf o Is MarshalByRefObject Then ... If TypeOf o Is IO.FileSystemInfo Then ... If TypeOf o Is IO.FileInfo Then ...
VERWEIS
Da jede Klasse von der Basisklasse Object abgeleitet ist, liefert TypeOf o Is Object immer True! Mehr Informationen zu TypeName und TypeOf finden Sie in der Hilfe, wenn Sie nach Bestimmen des Objekttyps suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vbconDiscoveringClassObjectBelongsTo.htm
IsArray, IsDate, IsNumeric, IsReference etc. VB.NET kennt eine Reihe von IsXxx-Methoden, die bei der Klassifizierung von Objekten bzw. Daten helfen. Methoden zur Objektklassifizierung (Klasse Microsoft.VisualBasic.Information) IsArray(var)
liefert True, wenn die Variable ein initialisiertes Feld enthält. (Das Feld muss mit der Elementzahl deklariert sein. Für Dim var() As Short liefert die Methode False.)
IsDate(var)
liefert True, wenn die Variable als Date-Variable deklariert ist oder wenn es sich um eine Zeichenkette handelt, die in der aktuellen Ländereinstellung in ein Datum oder in eine Zeit umgewandelt werden kann.
IsError(var)
liefert True, wenn var ein Objekt einer Klasse ist, die von System.Exception abgeleitet ist. (Derartige Objekte werden in VB.NET zur Darstellung von Fehlern verwendet – siehe Kapitel 11.)
IsNothing(var)
liefert True, wenn var ein nicht initialisiertes reference-Objekt ist. Bei Variablen für Werttypen liefert IsNothing immer False (d.h., auch IsNothing(0) oder IsNothing("") liefert False).
IsNumeric(var)
liefert True, wenn var als Boolean, Byte, Short, Integer, Long, Decimal, Single oder Double deklariert ist, oder wenn var eine Zeichenkette enthält, die als Zahlenwert interpretiert werden kann (z.B. "123").
IsReference(var)
liefert True, wenn die Variable ein initialisiertes reference-Objekt enthält. Wenn var ein Objekt eines Werttyps (ValueType-Objekt) oder Nothing enthält, liefert die Methode False.
156
4 Variablen- und Objektverwaltung
GetType Wenn Ihnen die oben beschriebenen Methoden zu wenig Informationen geben, können Sie mit der Methode GetType ein Objekt der Klasse System.Type ermitteln. Dim t As Type t = variable.GetType()
Das Objekt t gibt nun Auskunft über unzählige Details der zugrunde liegende Klasse: t.name liefert beispielsweise die Kurzform des Klassennamens, t.FullName den vollständigen Klassennamen, t.AssemblyQualifiedName gibt Informationen über die Bibliothek, aus der die Klasse stammt, t.IsValueType gibt an, ob es sich um ein gewöhnliches Objekt oder um einen Werttyp handelt, etc.
VERWEIS
Statt mit var.GetType() können Sie ein Type-Objekt für eine bestimmte Klasse auch durch Type.GetType("klassenname") ermitteln. Type.GetType("Integer") liefert ein Type-Objekt, das die Integer-Klasse beschreibt. Die Type-Klasse kennt Dutzende weitere Eigenschaften und Methoden, mit denen Sie alle nur erdenklichen Informationen über die Klasse ermitteln können. Beispielsweise können Sie damit feststellen, welche Methoden es für die Klasse gibt, welche Ereignisse unterstützt werden etc. Bei der Auswertung dieser Informationen helfen die zahlreichen Klassen des System.Reflection-Namensraums. Beinahe die einzige Information, die Sie leider nicht ermitteln können, ist der Variabelname. Ein Objekt kann keine Informationen darüber, wie die Variable heißt, die auf das Objekt verweist. (Dafür gibt es viele Gründe. Einer besteht ganz einfach darin, dass mehrere Variablen auf ein- und dasselbe Objekt verweisen können.
' Beispiel variablen\var-types Dim fi As New IO.FileInfo("c:\datei1.txt") Dim t As Type = fi.GetType() Console.WriteLine("t.Name: " + t.Name) Console.WriteLine("t.FullName: " + t.FullName) Console.WriteLine("t.AssemblyQualifiedName: " + _ t.AssemblyQualifiedName)
Durch die obigen Anweisungen werden im Konsolenfenster die folgenden Informationen ausgegeben: t.Name: FileInfo t.FullName: System.IO.FileInfo t.AssemblyQualifiedName: System.IO.FileInfo, mscorlib, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
HINWEIS
4.6 Interna der Variablenverwaltung
157
In VB.NET kann GetType auch in der Form GetType(Integer) oder GetType(IO.FileInfo) verwendet werden. GetType liefert damit ein Type-Objekt des angegebenen Typs bzw. der angegebenen Klasse zurück. GetType gilt hier nicht als Methode, sondern als Operator.
4.6.5
Variablen- und Objektkonvertierung (casting)
Der Begriff casting bezeichnet die Umwandlung von Daten zwischen verschiedenen Klassen bzw. Datentypen. In VB.NET ist dafür die Funktion CType vorgesehen, deren Syntax folgendermaßen aussieht: obj2 = CType(obj1, klassenname) CType wird nur dann fehlerfrei ausgeführt, wenn eine Typumwandlung tatsächlich mög-
lich ist. Grundsätzlich gibt es dabei zwei Fälle: •
Wenn der ursprüngliche Objekttyp und die neue Klasse fundamental voneinander abweichen, muss eine Umrechnung bzw. Umwandlung in den neuen Typ durchgeführt werden. Das ist nur dann möglich, wenn die Ausgangsklasse eine entsprechende Umwandlungsmethode vorsieht. Das ist nur bei den elementaren Datentypen der Fall. Deswegen können Sie beispielsweise eine Integer-Zahl in eine Zeichenkette umwandeln. Dim i As Integer = 1000 Dim s As String s = CType(i, String)
•
Sehr oft muss der Typ gar nicht geändert werden – es liegt schon der richtige Typ vor. Allerdings ist die Variable als Object deklariert, weswegen der Compiler nicht weiß, welche Eigenschaften und Methoden zur Verfügung stehen. (Im laufenden Programm weiß das Objekt sehr wohl, welcher Klasse es angehört, aber das nützt dem Compiler nichts.) In solchen Fällen ist obj2 = CType(obj1, klasse) nur eine Hilfe für den Compiler, mit der er den Code überprüfen kann. Durch CType werden die Daten an sich aber nicht verändert, weswegen diese (eigentlich fiktive) Umwandlung auch nicht mehr Zeit kostet als eine gewöhnliche Variablenzuweisung.
Am einfachsten ist dieser zweite Fall anhand eines Beispiels zu verstehen. Die Prozedur write_duplicate hat die Aufgabe, den doppelten Wert des übergebenen Parameters auszugeben. Der Parameter ist als Object deklariert. Innerhalb der Prozedur wird nun mit TypeOf getestet, ob es sich beim Parameter um einen Integer-Wert handelt. Wenn das der Fall ist, wird mit CType eine Typumwandlung durchgeführt. Damit weiß der Compiler nun, dass i eine Integer-Variable ist, und kann i*2 berechnen. Wenn der Parameter eine Zeichenkette enthält, wird diese analog per CType einer String-Variablen zugewiesen. Anschließend kann die Zeichenkette durch s+s verdoppelt werden.
158
4 Variablen- und Objektverwaltung
Sub write_duplicate(ByVal obj As Object) Dim i As Integer Dim s As String If TypeOf obj Is Integer Then i = CType(obj, Integer) Console.WriteLine(i * 2) ElseIf TypeOf obj Is String Then s = CType(obj, String) Console.WriteLine(s + s) Else Console.WriteLine("invalid type") End If End Sub
Da der Parameter von write_duplicate als Object deklariert ist, kann jeder beliebige Wert an die Prozedur übergeben werden. write_duplicate(3) write_duplicate("abc") write_duplicate(0.4)
Als Ausgabe erhalten Sie im Konsolenfenster die folgenden drei Zeilen:
VERWEIS
6 abcabc invalid type
Wenn Sie im Stichwortverzeichnis unter CType nachsehen, finden Sie dort einige Verweise zu Beispielen, die eine reale Anwendung dieser Funktion zeigen. Allerdings sind zum Verständnis der Beispiele oft viele Hintergrundinformationen erforderlich, weswegen hier ein einfaches (aber unrealistisches) Beispiel präsentiert wurde.
CType und die Objekthierarchie Bei Objekten, deren Klassen von anderen Basisklassen abgeleitet sind, kann CType eine Umwandlung in all diese Klassen durchführen. Zur Verdeutlichung dieses Umstands wird nochmals ein Objekt der Klasse IO.FileInfo verwendet. (Die Vererbungshierarchie dieser Klasse ist im vorigen Abschnitt in einem Diagramm dargestellt.) In obj1 wird ein Objekt der Klasse IO.FileInfo gespeichert. Wegen der Klassenhierarchie können die folgenden drei Zuweisungen mit CType problemlos durchgeführt werden. (Anschließend verweisen alle vier Variablen auf dasselbe Objekt!) Dim obj1, obj2 As Object Dim fsi As IO.FileSystemInfo Dim fi As IO.FileInfo
4.6 Interna der Variablenverwaltung
159
obj1 = New IO.FileInfo("c:\readme.txt") fsi = CType(obj1, IO.FileSystemInfo) fi = CType(obj1, IO.FileInfo) obj2 = CType(fi, Object)
Obwohl also alle vier Variablen auf dasselbe Objekt verweisen, akzeptiert der Compiler die Verwendung von Eigenschaften und Methoden zur Bearbeitung des Objekts nur bei den Variablen, die die richtige Klasse aufweisen. Die Methode Exists ist für die Klasse IO.FileSystemInfo deklariert, die Methode DirectoryName für IO.FileInfo. Exists kann daher sowohl auf fsi als auch auf fi angewendet werden, DirectoryName dagegen nur auf fi.
VERWEIS
Console.WriteLine(fsi.Exists()) Console.WriteLine(fi.Exists()) Console.WriteLine(fi.DirectoryName)
Einige Beispiele zu diesem Abschnitt finden Sie im Beispielprogramm variablen\objektkonvertierung, das hier aber aus Platzgründen nicht abgedruckt ist. Weitere Informationen zur automatischen und expliziten Konvertierung zwischen verschiedenen Daten- und Objekttypen gibt Abschnitt 8.4. Der Schwerpunkt dieses Abschnitts liegt bei der Umwandlung zwischen Basisdatentypen (Zahlen, Zeichenketten, Datum und Uhrzeit). Dort lernen Sie die VB.NET-Funktionen CBool, CByte, CChar, CDate sowie eine ganze Menge weiterer .NET-Methoden kennen.
5
Prozedurale Programmierung
Dieses Kapitel liefert eine kompakte Beschreibung der prozeduralen Sprachelemente von VB.NET. Dazu zählen insbesondere die Kommandos zur Bildung von Schleifen, Abfragen und Prozeduren. Das Kapitel wird mit einer Referenz aller Operatoren abgeschlossen.
VERWEIS
5.1 5.2 5.3 5.4
Verzweigungen (Abfragen) Schleifen Prozeduren und Funktionen Operatoren
162 165 167 181
Objektorientierte Sprachelemente werden in den Kapiteln 6 und 7 behandelt. Eine zumeist gut verständliche Sprachdefinition finden Sie auch in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
162
5 Prozedurale Programmierung
5.1
Verzweigungen (Abfragen)
5.1.1
If-Then-Else
Die folgenden Zeilen zeigen anhand eines Beispiels die Struktur von If-Then-Else-Konstruktionen. Dim a As Double = Rnd() If a < 0.1 Then MsgBox("a ist kleiner 0.1") ... beliebig viele weitere Kommandos ElseIf a < 0.5 Then MsgBox("a ist größer gleich 0.1, aber kleiner 0.5") ElseIf a < 0.9 Then MsgBox("a ist größer gleich 0.5, aber kleiner 0.9") Else MsgBox("a ist größer gleich 0.9") End If
In der einfachsten Form besteht die If-Konstruktion nur aus der Abfrage, dem Block nach Then und schließlich End If. Die ElseIf-Blöcke und der Else-Block sind optional. In einfachen Fällen kann eine If-Abfrage in nur einer einzigen Zeile formuliert werden. Dazu wird unmittelbar nach Then das auszuführende Kommando angegeben. Es ist kein End If erforderlich (und bei dieser Syntaxvariante auch gar nicht erlaubt). If a < 0.1 Then MsgBox("a ist kleiner 0.1")
Formulierung von Bedingungen If erwartet als Bedingung einen Wahrheitswert, also True oder False. Wenn Sie einen Vergleich angeben (z.B. a=3 oder b>4), dann liefert der Vergleich diesen Wahrheitswert. Natürlich können Sie auch mehrere Teilbedingungen mit logischen Operatoren miteinander verbinden: If (a < 0.1 And b > 3) Or c=5 Then ...
Wenn Sie dagegen einfach eine Variable angeben, dann testet VB.NET, ob dessen Wert 0 (False) bzw. ungleich 0 (True) ist. Der folgende Then-Block wird also immer ausgeführt, wenn a ungleich 0 ist. If a Then ...
5.1.2
Select-Case
Alternativ zur If-Verzweigung kennt VB.NET auch die Select-Case-Konstruktion, mit der sich manche Abfragen übersichtlicher (lesbarer) formulieren lassen. Abermals vorweg ein Beispiel:
5.1 Verzweigungen (Abfragen)
163
Dim n As Integer = CInt(Rnd() * 100) Select Case n Case 1 MsgBox("n ist 1") Case 2, 3, 4, 5 MsgBox("n liegt zwischen 2 und 5") Case 6 To 50 MsgBox("n liegt zwischen 6 und 50") Case Is < 70 MsgBox("n liegt zwischen 51 und 69") Case Else MsgBox("n ist größer gleich 70") End Select
Mit Select Case wird der auszuwertende Ausdruck angegeben. Dabei muss es sich um einen der elementaren Datentypen handeln (alle Zahlentypen sowie Char, String, Date oder Object). In jeder Case-Bedingung kann ein bestimmter Wert, eine Aufzählung von Werten oder ein Wertbereich (a To b) angegeben werden. Des weiteren kann mit Is ein beliebiger Vergleichsoperator angegeben werden. (Is TypeOf datentyp ist allerdings nicht erlaubt.) Wenn in Select Case eine Zeichenkette angegeben wird, erfolgt der Zeichenkettenvergleich gemäß der Option-Compare-Einstellung (siehe Abschnitt 8.2.3). Per Default bedeutet das, dass zwischen Groß- und Kleinschreibung differenziert wird. Sobald eine zutreffende Case-Bedingung gefunden wird, wird der darauf folgende Codeblock ausgeführt. Anschließend wird die gesamte Konstruktion verlassen. (Es wird also maximal ein Case-Codeblock ausgeführt.)
5.1.3
IIf, Choose, Switch
Die Methode IIf(a, b, c) aus der Interaction-Klasse des Microsoft.VisualBasic-Namensraum entspricht in etwa dem C-Konstrukt a ? b : c: Wenn a zutrifft, liefert die Funktion den Wert von b, sonst c. Dim x As Double = Rnd() Dim s As String s = IIf(x > 0.5, "abc", "def") Choose weist eine ähnliche Syntax wie If auf: Im ersten Parameter wird ein Indexwert angegeben, der zeigt, welchen der folgenden Parameter Choose zurückgeben soll. Wenn der Indexwert kleiner als 1 oder größer als der größtmögliche Index ist, liefert die Funktion als Ergebnis Nothing. Bei Fließkommazahlen werden die Nachkommastellen ignoriert. Choose(1, "a", "b", "c") Choose(2.8, "a", "b", "c") Choose(0, "a", "b", "c") Choose(4, "a", "b", "c")
'liefert 'liefert 'liefert 'liefert
"a" "b" Nothing Nothing
164
5 Prozedurale Programmierung
Eine dritte Variante für die Formulierung einzeiliger Auswahlentscheidungen stellt die Methode Switch dar. In Switch(a,x,b,y,..) wird getestet, ob a ungleich 0 (also True) ist. Wenn ja, liefert Switch als Ergebnis x, andernfalls wird der Test für b wiederholt etc. Ist keine der in jedem zweiten Parameter formulierten Bedingungen wahr, liefert die Funktion als Ergebnis Nothing.
HINWEIS
Switch(1, "a", 1, "b") Switch(0, "a", 1, "b") Switch(0, "a", 0, "b")
'liefert "a" 'liefert "b" 'liefert Nothing
Unabhängig vom Ergebnis der Bedingung werden bei IIf, Choose und Switch alle Parameter ausgewertet. Daher werden bei IIf(True, funktion1, funktion2) sowohl funktion1 als auch funktion2 berechnet, obwohl IIf eigentlich nur den Wert von funktion1 benötigen würde. Dieses Verhalten ist weder besonders logisch noch effizient, stellt aber die Kompatibilität mit VB6 sicher.
5.1.4
Syntaxzusammenfassung
Verzweigungen mit If-Then-Else If bedingung Then kommando
einzeilige Kurzform
If bedingung Then kom1 Else kom2
einzeilige Kurzform mit Else
If bedingung Then ... ElseIf bedingung Then ... Else ... End If
mehrzeilige Variante beliebig viele Kommandos optional, beliebig oft optional mehrzeilige Variante beliebig viele Kommandos
Verzweigungen mit Select-Case Select Case ausdruck Case bedingung1 ... Case bedingung2 ... Case Else ... End Select
beliebig viele Fälle
optional
5.2 Schleifen
165
Case-Bedingungen wert wert1, wert2, wert3 wert1 To wert2 Is operator vergleichswert
Einzelwert (oder Zeichenkette) Aufzählung Wertebereich allgemeiner Vergleich, z.B. Is < 3
Fallunterscheidungen mit IIf, Choose und Switch (Namensraum Microsoft.VisualBasic) IIf(bedingung, ausdruck1, ausdruck2)
die Bedingung entscheidet, welchen Ausdruck IIf als Ergebnis liefert
Choose(index, ausd1, ausd2, ausd3 ...)
der erste Ausdruck wirkt wie ein Index
Switch(bed1, ausd1, bed2, ausd2 ...)
die erste wahre Bedingung entscheidet über den Ergebnisausdruck
5.2
Schleifen
5.2.1
For-Next-Schleifen
Die einfachste Schleifenform ist die klassische For-Next-Schleife. Die folgenden Zeilen zeigen die prinzipielle Syntax. Normalerweise wird die Schleifenvariable mit jedem Durchlauf um 1 erhöht. Sie können aber mit Step einen beliebigen anderen Wert angeben (auch einen negativen!). Bei Next können Sie optional die Schleifenvariable angeben. Das ist zwar nicht notwendig, fördert bei verschachtelten Konstruktionen aber die Lesbarkeit. Dim i As Integer For i = 1 To 10 [ Step 2 ] ... Next [ i ]
Intern wird am Beginn der Schleife getestet, ob die Schleifenvariable bereits größer als der Endwert ist (bzw. kleiner bei einem negativen Step-Wert). In diesem Fall wird die Schleife sofort abgebrochen. Der Schleifenkörper wird dann kein einziges Mal durchlaufen. Jedes Mal, wenn die Programmausführung Next erreicht, wird die Schleifenvariable um 1 bzw. um den Step-Wert erhöht. Anschließend wird der Test wiederholt, ob der Zielwert bereits über- bzw. unterschritten wurde. Wenn das nicht der Fall ist, wird die Schleife nochmal durchlaufen. (Eine Konsequenz dieser Vorgehensweise besteht darin, dass die Schleifenvariable am Ende der obigen Schleife den Wert 11 enthält!) Die Schleife kann vorzeitig mit Exit For abgebrochen werden. (Exit For bewirkt, dass der Code nach dem dazugehörenden Next-Kommando fortgesetzt wird.)
166
5 Prozedurale Programmierung
5.2.2
For-Each-Schleifen
Bei For-Each-Schleifen durchläuft die Schleifenvariable alle Elemente des angegebenen Felds bzw. der Aufzählung. (Der Begriff Aufzählung meint genau genommen ein Objekt, dessen Klasse die IEnumerable-Schnittstelle implementiert. Was das bedeutet, wird in Kapitel 9 ausführlich beschrieben.) Die folgende Schleife verdeutlicht die Syntax. Der Datentyp der Schleifenvariable muss mit dem des Felds bzw. der Aufzählung übereinstimmen. Die Schleife kann vorzeitig mit Exit For abgebrochen werden. Dim i As Integer Dim arr() As Integer = {12, 7, 4} For Each i In arr Console.WriteLine(i) Next
TIPP
For-Each-Schleifen sind im Regelfall nicht dazu geeignet, die Elemente einer Auf-
zählung zu löschen (weil dadurch die Aufzählung durcheinander kommt). Stattdessen sollten Sie eine For-Next Schleife verwenden, die von coll.Count-1 bis 0 herunterzählt und coll(i) löscht.
5.2.3
Do-Loop-Schleifen
Es gibt insgesamt fünf Möglichkeiten, Do-Loop-Schleifen zu bilden, von denen hier zwei dargestellt sind. Die erste Schleife gibt die Werte 1 bis 5 aus, die zweite die Werte 1 bis 4. Dim i As Integer = 1 Do While i < 5 Console.WriteLine(i) i += 1 Loop
Der wesentliche Unterschied zwischen der Formulierung der Schleifenbedingung bei Do oder bei Loop besteht darin, dass der Schleifenkörper im zweiten Fall immer einmal durchlaufen wird. Im ersten Fall kann es dagegen sein, dass die Bedingung von Anfang an nicht erfüllt ist und das Innere der Schleife daher nie durchlaufen wird. Dim j As Integer = 1 Do Console.WriteLine(j) j += 1 Loop Until j > 5
Die anderen Schleifenvarianten bestehen darin, die Do-Bedingung mit Until bzw. die LoopBedingung mit While zu bilden oder eine Schleife ganz ohne Bedingung zu bilden. Die da-
5.3 Prozeduren und Funktionen
167
raus resultierende Endlosschleife kann mit Exit Do abgebrochen werden. (Es ist übrigens nicht zulässig, sowohl bei Do als auch bei Loop eine Bedingung anzugeben.)
5.2.4
Syntaxzusammenfassung
Schleifen For var = start To ende [Step schrittweite] ... [Exit For] Next [var]
durchläuft die Schleife, bis var größer als ende ist (bzw. kleiner, wenn mit Step eine negative Schrittweite angegeben wird).
For Each var In feld ... [Exit For] Next [var]
durchläuft die Schleife, bis var alle Elemente des Felds oder der Aufzählung angenommen hat.
Do [While bedingung oder Until bedingung] ... [Exit Do] Loop
durchläuft die Schleife, bis die WhileBedingung erfüllt bzw. die UntilBedingung nicht mehr erfüllt ist.
oder Do ... [Exit Do] Loop [While bedingung oder Until bedingung]
5.3
Prozeduren und Funktionen
5.3.1
Syntax
Prozedurale Programmiersprachen (zu denen auch VB.NET zählt) zeichnen sich dadurch aus, dass der Programmcode in kleinen, voneinander getrennten Programmteilen angeordnet wird. Diese Programmteile (Prozeduren) können sich gegenseitig aufrufen und dabei Parameter übergeben. Es gibt zwei Typen von Prozeduren: •
Unterprogramme sind Prozeduren, die kein Ergebnis zurückgeben.
•
Funktionen sind Prozeduren, die als Ergebnis einen Wert zurückgeben. Dabei kann der Rückgabewert einen beliebigen Datentyp haben. (Funktionen können also auch Objekte zurückgeben.) VB.NET-Funktionen bieten keine direkte Möglichkeit, mehrere Werte zurückzugeben. Wenn Sie das wollen, müssen Sie entweder ein Objekt oder eine Struktur mit mehreren Eigenschaften zurückgeben oder die Parameter der Funktion mit ByRef deklarieren und im Code verändern. (Details zur Verwendung von ByRef folgen etwas weiter unten.)
VERWEIS
168
5 Prozedurale Programmierung
Im Kontext einer Klasse werden Prozeduren zu Methoden. (Eine Methode ist also nichts anderes als eine Prozedur, die in einer Klasse formuliert wird.)
Bei Konsolenanwendungen (bzw. bei VB.NET-Programmen, die aus nur einem Modul bestehen) hat die Prozedur mit dem Namen Main eine besondere Bedeutung: Die Programmausführung beginnt mit dieser Prozedur. Wenn Main zu Ende ist, endet auch das Programm. (Bei Windows-Anwendungen beginnt die Programmausführung dagegen per Default damit, dass ein Objekt eines Fensters erzeugt und ausgeführt wird. Hier endet die Programmausführung mit dem Schließen des Fensters. Aber auch bei Windows-Anwendungen können Sie die Projekteigenschaften so einstellen, dass das Programm mit Main beginnt.)
Deklaration von Prozeduren Prozeduren werden mit Sub name bzw. mit Function name eingeleitet. Sie enden mit End Sub bzw. mit End Function. Bei der Deklaration von Funktionen muss der Datentyp des Rückgabewerts angegeben werden. Das erfolgt wie bei Variablen durch den Nachsatz As datentyp. Die folgende Funktion liefert als Ergebnis also einen Integer-Wert. Function myFunc() As Integer ... End Function
Wie bei Variablen ist bei der Deklaration auch eine Kurzschreibweise mit Zeichen wie % (Integer) oder $ (String) zulässig: Function myFunc%()
Prozeduren können mit einer (in Klammern deklarierten) Parameterliste ausgestattet werden. Parameter werden wie Variablen deklariert, also jeweils mit der Angabe des Datentyps. Die folgende Funktion erwartet als Parameter einen Integer-Wert. (Details zum Umgang mit Parametern folgen etwas weiter unten in einem eigenen Abschnitt.) Function myFunc(ByVal y As Integer) As Integer
Prozeduren werden im Regelfall bis zu ihrem Ende ausgeführt. Sie können aber auch vorzeitig beendet werden: Unterprogramme mit Exit Sub oder Return, Funktionen mit Exit Function oder Return. Bei Funktionen gibt es zwei Möglichkeiten, den Rückgabewert anzugeben: Üblicherweise wird das Ergebnis als Parameter an Return übergeben. Die andere Variante besteht darin, eine Zuweisung an den Funktionsnamen durchzuführen. (Wenn die Funktion myFunc heißt, können Sie den Rückgabewert beispielsweise durch myFunc = 7 einstellen.) Wenn die Funktion durch Exit Function oder End Function beendet wird, ohne dass vorher ein Rückgabewert eingestellt wurde, dann wird ein nicht initialisiertes Objekt des jeweiligen Datentyps übergeben (0 bei Zahlen, "" bei Zeichenketten, Nothing bei Objekten etc.)
5.3 Prozeduren und Funktionen
169
Es ist nicht möglich, innerhalb einer Prozedur eine andere Prozedur zu deklarieren. (Die Deklaration von Prozeduren darf also nicht verschachtelt werden.)
Aufruf von Prozeduren Prozeduren werden einfach durch die Nennung ihres Namens aufgerufen. Die Entwicklungsumgebung fügt an den Namen automatisch ein Klammernpaar an. Falls an die Prozedur Parameter übergeben werden, müssen diese innerhalb dieser Klammern angegeben werden. Bei Funktionen wird der Rückgabewert üblicherweise ausgewertet (z.B. in einer Bedingung oder in einer Zuweisung). Das ist aber nicht erforderlich, falls Sie aus irgendeinem Grund nicht am Rückgabewert interessiert sind. Daher sind die zwei folgenden Zeilen zum Aufruf von myFunc beide zulässig. myFunc(3) If myFunc(7) > 12 Then ...
Bei Unterprogrammen darf der Aufruf auch mit Call erfolgen. (Call ist ein Relikt von VB6.) Die beiden folgenden Aufrufe sind daher gleichwertig. myProc() Call myProc()
Ein erstes Beispiel Die folgenden Zeilen zeigen die Definition der beiden Prozeduren myProc und myFunc sowie deren Aufruf durch die Prozedur Main. Das Programm ist eine Konsolenanwendung, d.h., der gesamte Code ist von Module Module1 und End Module eingeschlossen. (Diese zwei Zeilen werden in diesem Buch normalerweise nicht abgedruckt. Eine Erklärung, was ein Modul eigentlich ist, folgt in Kapitel 7. Vorerst reicht es aus, wenn Sie wissen, dass Code von Konsolenanwendungen innerhalb von Module-Anweisungen angegeben werden muss. Die Entwicklungsumgebung sieht diese Anweisungen ohnedies automatisch vor.) Das Ergebnis des Programms wird im Konsolenfenster sichtbar: Dort werden die Zeichenkette abc und die Zahl 24 angezeigt, bis Sie durch Return das Programm beenden. ' Beispiel prozedurale-programmierung\prozeduren Module Module1 Sub Main() Dim x As Integer myProc() x = myFunc(7) Console.WriteLine(x) Console.ReadLine() 'auf Return warten End Sub
170
5 Prozedurale Programmierung
Sub myProc() Console.WriteLine("abc") End Sub Function myFunc(ByVal y As Integer) As Integer Return (y + 1) * 3 End Function End Module
Zugriff auf Variablen Innerhalb einer Prozedur kann auf alle Variablen zugegriffen werden, die auf Modul- oder Klassenebene deklariert werden. Es ist aber nicht möglich, auf Variablen zuzugreifen, die in einer anderen Prozedur deklariert sind. Module Module1 Dim var1 As Integer Sub Main() Dim var2 As Integer myProc1() End Sub
VERWEIS
Sub myProc1() var1 = 7 'ok var2 = 3 'Syntaxfehler, auf var2 kann nicht zugegriffen werden End Sub End Module
Die Frage, wo im Code auf welche Variable, Prozedur, Methode etc. zugegriffen werden kann, ist weitaus komplexer, als es hier den Anschein hat. In Abschnitt 7.9 – nach der Präsentation der objektorientierten Sprachelemente von VB.NET – wird das Thema noch einmal aufgegriffen und dann umfassender behandelt. An dieser Stelle wird vorausgesetzt, dass das Programm aus nur einem einzigen Modul oder aus nur einer einzigen Klasse besteht, wie dies bei einfachen Konsolenanwendungen oder bei Windows-Anwendung mit nur einem Fenster der Fall ist.
5.3.2
Lokale und statische Variablen
Innerhalb von Prozeduren können ebenfalls Variablen deklariert werden. Diese Variablen werden oft als lokal bezeichnet, weil sie getrennt von Variablen außerhalb der Prozedur verwaltet werden und nur innerhalb der Prozedur verwendet werden können. (Parameter von Prozeduren werden übrigens wie lokale Variablen behandelt.)
5.3 Prozeduren und Funktionen
171
Es ist zulässig, je eine Variable innerhalb und außerhalb einer Prozedur mit demselben Namen zu deklarieren. Lokale Variablen werden bei jedem Aufruf der Prozedur neu initialisiert (d.h., sie merken sich ihre bisherigen Zustände nicht). Wenn Sie das folgende Miniprogramm ausführen, wird zweimal der Wert 0 und dann einmal der Wert 5 ausgegeben. (i innerhalb von Main wird also durch den Code in myProcVar nicht verändert.) Sub Main() Dim i = 5 myProcVar() myProcVar() Console.WriteLine(i) End Sub Sub myProcVar() Dim i As Integer Console.WriteLine(i) i = 2 End Sub
Statische Variablen Mit Static können Variablen innerhalb einer Prozedur als statisch definiert werden. Das bedeutet, dass sie ihren Wert nach der Rückkehr der Prozedur behalten und beim nächsten Aufruf noch 'wissen', welchen Wert sie zuletzt hatten. Wenn bei der Deklaration eine Zuweisung angegeben wird (wie im folgenden Beispiel), dann wird diese Zuweisung nur beim ersten Aufruf durchgeführt. Wenn Sie das folgende Miniprogramm ausführen, werden die Werte 1, 2 und 3 ausgegeben. Sub Main() myProcStatic() myProcStatic() myProcStatic() End Sub
VORSICHT
Sub myProcStatic() Static i As Integer = 1 Console.WriteLine(i) i += 1 End Sub
Das Schlüsselwort static der Programmiersprache C# hat eine vollkommen andere Bedeutung als das hier beschriebene VB.NET-Schlüsselwort Static! static von C# wird zur Beschreibung gemeinsamer Klassenvariablen bzw. zur Beschreibung von Methoden verwendet, die ohne ein Objekt verwendet werden können. static hat damit dieselbe Bedeutung wie das VB.NET-Schlüsselwort Shared.
172
5.3.3
5 Prozedurale Programmierung
Rekursion
Prozeduren können sich selbstverständlich gegenseitig aufrufen. Das bedeutet, dass Sie z.B. innerhalb von myProc die Funktion myFunc nutzen können etc. Ein Sonderfall derartiger Aufrufe besteht dann, wenn eine Prozedur sich selbst aufruft. Das ist nicht nur wie im folgenden Beispiel zur Berechnung mancher mathematischer Funktionen nützlich, sondern generell zur Verarbeitung hierarchischer Datenstrukturen. Wenn Sie beispielsweise alle Verzeichnisse des Dateisystems durchlaufen möchten, verwenden Sie im Regelfall eine rekursive Prozedur, die sich selbst für alle Unterverzeichnisse aufruft. (Werfen Sie einen Blick in das Stichwortverzeichnis! Dort finden Sie unter dem Stichwort Rekursion eine ganze Reihe von Querverweisen auf Rekursionsbeispiele.) Die folgende Beispielfunktion berechnet die Fakultät einer Zahl. Diese mathematische Funktion ist definiert als das Produkt aller Zahlen zwischen 1 und der angegebenen Zahl. Die Fakultät von 5 beträgt also 1*2*3*4*5=120. Der Aufruf myFuncRec(5) bewirkt, dass in der If-Abfrage 5 * myFuncRec(4) aufgerufen wird, dann 4 * myFuncRec(3), dann 3 * myFuncRec(2) und dann 2 * myFuncRec(1). Durch myFuncRec(1) wird einfach der Long-Wert zurückgegeben. Anschließend lösen sich die Aufrufe rückwärts wieder auf. (Wenn Sie mit dem Konzept der Rekursion nicht vertraut sind, können Sie den Code mit DEBUGGEN|EINZELSCHRITT auch Zeile für Zeile ausführen.) ' Beispiel prozedurale-programmierung\prozeduren Function myFuncRec(ByVal n As Long) As Long If n <= 1 Then Return 1L Else Return n * myFuncRec(n - 1) End If End Function
Beschränkung der Rekursionstiefe Intern müssen bei jedem Prozeduraufruf die Rücksprungadresse sowie alle lokalen Variablen und Parameter im so genannten Stack gespeichert werden (weil diese Werte bei jedem Aufruf voneinander unabhängig sind). Das bedeutet, dass der Speicherbedarf für rekursive Prozeduren erheblich sein kann, wenn die Rekursionstiefe (also die Anzahl der ineinander verschachtelten Aufrufe) sehr groß ist. Da der Stack-Speicher limitiert ist, ist auch die Rekursionstiefe begrenzt. Wenn Sie beispielsweise die folgende Testprozedur aufrufen, tritt nach ca. 130.000 Aufrufen der Fehler StackOverflowException auf. Sub myFuncRecTest() Static i As Integer i += 1 myFuncRecTest() End Sub
5.3 Prozeduren und Funktionen
173
Bei dieser Variante tritt der Fehler schon nach ca. 20.000 Aufrufen auf, weil der Speicherbedarf pro Aufruf aufgrund der fünf Double-Variablen viel größer ist. Sub myFuncRecTest() Static i As Integer Dim a, b, c, d, e As Double i += 1 myFuncRecTest() End Sub
5.3.4
Parameterliste
Die einführenden Beispiele haben bereits gezeigt, dass Prozeduren mit Parametern ausgestattet werden können. Dabei gibt es ziemlich viele Sonderfälle, weswegen sich dieser Abschnitt ausschließlich mit dem Aufbau der Parameterliste und der Übergabe von Parametern beschäftigt. Grundsätzlich müssen bei der Deklaration alle Parameter samt Datentyp angegeben werden. Die Entwicklungsumgebung fügt das Schlüsselwort ByVal automatisch ein, wenn Sie das vergessen haben sollten. Sub myProcPara1(ByVal x As Integer, ByVal y As Double, _ ByVal z As String) ...
Beim Aufruf der Prozedur müssen alle Parameter angegeben werden, wobei Sie auch auf den richtigen Datentyp achten müssen. (VB.NET führt – wenn möglich – automatisch Typumwandlungen durch. Wenn Sie Option Strict verwenden, werden aber nur solche Umwandlungen durchgeführt, die verlustfrei sind – also etwa von Integer zu Double, aber nicht umgekehrt.) myProcPara1(1, 2.5, "abc") myProcPara1(1, 2, "abc") myProcPara1(1.5, 2, "abc") myProcPara1(1.5, 2, 2) myProcPara1("1", 2.5, "abc") myProcPara1(#12/31/2002#, 2.5, "abc")
'ok 'ok 'Syntaxfehler bei Option Strict 'Syntaxfehler bei Option Strict 'Syntaxfehler bei Option Strict 'immer Syntaxfehler
Gleichnamige Prozeduren VB.NET erlaubt die Deklaration mehrerer gleichnamiger Prozeduren, wenn diese anhand der Parameterliste eindeutig unterscheidbar sind. Die Deklaration der drei folgenden Prozeduren ist daher zulässig. Sub myProcPara1(ByVal x As Integer, ByVal y As Integer) ... Sub myProcPara1(ByVal x As Integer, ByVal y As Double) ... Sub myProcPara1(ByVal x As Integer, ByVal y As Double, _ ByVal z As String) ...
174
5 Prozedurale Programmierung
Die folgende vierte Deklaration wäre dagegen unzulässig, weil sie nicht von der zweiten unterschieden werden kann. Sub myProcPara1(ByVal a As Integer, ByVal b As Double)
HINWEIS
VB.NET entscheidet anhand der übergebenen Parameter, welche Prozedur aufgerufen wird. Durch myProcPara1(1, 2) würde die erste Variante, durch myProcPara1(1, 2.7) dagegen die zweite Variante aufgerufen. Der Mechanismus zur Verwaltung gleichnamiger Prozeduren wird auch als Überladung (overloading) bezeichnet. Dieser Mechanismus funktioniert auch dann, wenn manche Prozeduren als Unterprogramme, andere als Funktionen deklariert werden. Optional können Sie allen gleichnamigen Prozeduren das Schlüsselwort Overloads voranstellen, um so auf den hier eingesetzten Mechanismus hinzuweisen. (Wenn Sie eine gleichnamige Prozedur durch Overloads kennzeichnen, müssen Sie alle Prozeduren so kennzeichnen!) Overloads kann auch bei der Definition vererbter Klassen verwendet werden – siehe Abschnitt 7.4.
ByVal versus ByRef Per Default werden Parameter mit ByVal als Wertparameter an die Prozedur übergeben. Das bedeutet im Regelfall, dass Veränderungen, die innerhalb der Prozedur an Parametern durchgeführt werden, nicht auf die Variablen übertragen werden, die zum Aufruf verwendet wurden. (Die Ausnahmen zum Regelfall folgen unten!) Als Alternative zu ByVal können Sie Parameter mit ByRef als Rückgabeparameter deklarieren. Das bedeutet, dass die Prozedur Variablen (oder auch Eigenschaften von Objekten) verändern kann, die als Parameter angegeben wurden. Am einfachsten ist dieser Unterschied anhand eines Beispiels zu verstehen. Die beiden Prozeduren myProcByVal und myProcByRef unterscheiden sich nur durch die Schlüsselwörter ByVal bzw. ByRef. Beide Prozeduren verändern die übergebenen Parameter: x wird um eins vergrößert, an s wird die Zeichenkette "x" angehängt, und das Objekt di (das ein Verzeichnis des Festplatte beschreibt) wird durch ein anderes Objekt desselben Typs ersetzt. Beide Prozeduren werden nun einmal aufgerufen, wobei als Parameter die Variablen a, b und c übergeben werden. Beim ersten Aufruf werden diese Variablen nicht verändert, beim zweiten dagen sehr wohl! Im Konsolenfenster erscheinen daher die folgenden Ausgaben: Zuerst 3, abc, C:\, danach 4, abcx und D:\code\vb.net\prozedurale-programmierung\prozeduren\bin.
5.3 Prozeduren und Funktionen
' Beispiel Sub Main() Dim a As Dim b As Dim c As
175
prozedurale-programmierung\prozeduren Integer = 3 String = "abc" New IO.DirectoryInfo("c:")
myProcByVal(a, b, c) Console.WriteLine(a) Console.WriteLine(b) Console.WriteLine(c.FullName) myProcByRef(a, b, c) Console.WriteLine(a) Console.WriteLine(b) Console.WriteLine(c.FullName) End Sub Sub myProcByVal(ByVal x As Integer, ByVal s As String, _ ByVal di As IO.DirectoryInfo) x += 1 s += "x" di = New IO.DirectoryInfo(Environment.CurrentDirectory) End Sub Sub myProcByRef(ByRef x As Integer, ByRef s As String, _ ByRef di As IO.DirectoryInfo)
TIPP
x += 1 s += "x" di = New IO.DirectoryInfo(Environment.CurrentDirectory) End Sub
Wenn Sie Variablen mit ByVal an die Prozedur übergeben möchten, obwohl die Prozedur mit ByRef deklariert ist, stellen Sie die Parameter bei der Übergabe einfach in Klammern. Wenn Sie im obigen Beispiel myProcByRef((a), (b), (c)) aufrufen, bleiben a, b und c unverändert.
ByVal versus ByRef – Interna und Sonderfälle Leider ist die Logik von ByVal und ByRef nicht ganz so einfach, wie das obige Beispiel den Anschein hat. Wenn Sie beispielsweise ein Objekt oder ein Feld mit ByVal übergeben, können Sie dessen Eigenschaften bzw. Elemente sehr wohl verändern! Dieser Effekt lässt sich anschaulich mit einem einfachen Windows-Programm demonstrieren: Beim Klick auf einen Button wird automatisch die Click-Ereignisprozedur aufgerufen. Diese übergibt das Objekt des Buttons an eine Prozedur. Dort wird die Eigenschaft Left des
176
5 Prozedurale Programmierung
Buttons verändert. Obwohl die Parameterübergabe mit ByVal erfolgt, wird das ButtonObjekt bleibend verändert! Der Button wandert um 10 Pixel nach rechts. ' Beispiel prozedurale-programmierung\byval-parameter Private Sub Button1_Click(...) Handles Button1.Click myProcByValue(Button1) 'Unterprogrammaufruf End Sub
VERWEIS
Sub myProcByValue(ByVal btn As Button) btn.Left += 10 End Sub
Die Logik des Beispiels und insbesondere die Wirkung der ByVal-Übergabe des Buttons an die Prozedur myProcByValue sollte auf Anhieb verständlich sein, auch wenn Sie noch keine Kenntnis über die Windows.Forms-Programmierung haben. Detaillierte Informationen zur Programmierung von Windows-Anwendungen folgen ab Kapitel 13.
Um zu verstehen, was hier vor sich geht, ist eine kurze Erklärung erforderlich, was bei der Parameterübergabe hinter den Kulissen passiert.
ByRef-Parameter Dieser Fall ist ganz leicht zu verstehen. Hier wird ein Zeiger auf die Daten an die Prozedur übergeben. Jede Veränderung des Parameters wirkt sich unmittelbar auf die zugrunde liegende Variable aus. (Eine Veränderung ist nur möglich, wenn Sie direkt eine Variable übergeben – z.B. durch myProcByRef(a, b). Wenn Sie dagegen einen errechneten Ausdruck oder eine Konstante übergeben – z.B. myProcByRef(a+2, 7) –, können die Ausgangsdaten natürlich nicht verändert werden. Das hat aber nichts mit ByRef oder ByVal zu tun, sondern gilt immer.)
ByVal-Parameter Hier wird intern wiederum zwischen zwei Varianten unterschieden: •
Parameter für Werttypen (ValueType-Klassen): Bei allen elementaren Datentypen außer String und Object, bei selbst definierten Strukturen sowie bei einigen einfachen Strukturen aus der .NET-Bibliothek wird tatsächlich eine Kopie der Daten übergeben.
•
Parameter für Referenztypen: Bei gewöhnlichen Objekten wird dagegen eine Kopie des Zeigers auf die Daten an die Prozedur übergeben.
Die sich daraus ergebenden Konsequenzen sind leider ein wenig unübersichtlich: •
Bei Werttypparametern sind grundsätzlich keine bleibenden Änderungen möglich, d.h., ByVal funktioniert so, wie man es erwartet. Der Grund: In der Prozedur wird nur eine Kopie der Daten verändert.
5.3 Prozeduren und Funktionen
177
•
Für selbst definierte Datenstrukturen gilt im Prinzip die obige Aussage. (Datenstrukturen sind ja von ValueType abgeleitet.) Allerdings kann ein Element einer Struktur auf ein gewöhnliches Objekt verweisen. Dessen Eigenschaften können sehr wohl geändert werden!
•
Bei String-Daten sowie bei anderen Objekten, die als unveränderlich (immutable) gelten, sind ebenfalls keine bleibenden Änderungen möglich. Der Grund: In der Prozedur wird beim Versuch einer Änderung automatisch ein neues Objekt erzeugt. Das ursprüngliche Objekt außerhalb der Prozedur wird davon nicht beeinflusst.
•
Bei allen anderen gewöhnlichen Objekten (Referenztypen) sowie bei Feldern können Sie Eigenschaften oder Elemente verändern. Sie können das Objekt aber nicht durch ein neues ersetzen. Der Grund: An die Prozedur wurde eine Kopie des Zeigers auf die Daten übergeben. Eine Veränderung dieser Kopie (durch die Zuweisung eines neuen Objekts) hat wie bei String-Daten keinen Einfluss auf die Ausgangsdaten. Es ist aber sehr wohl möglich, Eigenschaften des Objekts zu verändern. Der Zeiger auf die Daten wird dadurch ja nicht beeinflusst.
Felder An Prozeduren können auch Felder übergeben werden. Innerhalb der Prozedur können Sie mit For Each alle Elemente durchlaufen. Wenn Sie auf die Elemente über Indizes zugreifen möchten, müssen Sie mit Rank die Dimension des Feldes und mit GetLowerBound und GetUpperBound die Indexgrenzen innerhalb einer bestimmten Dimension ermitteln. Das folgende Beispiel testet, ob es sich um ein eindimensionales Feld handelt. Wenn das der Fall ist, werden alle Elemente um eins vergrößert. (Es werden also die Werte 2, 3 und 4 ausgegeben.) ' Beispiel prozedurale-programmierung\prozeduren Sub Main() Dim a() As Integer = {1, 2, 3} myProcArray(a) Console.WriteLine(a(0) & " " & a(1) & " " & a(2)) End Sub Sub myProcArray(ByRef x() As Integer) Dim i As Integer If x.Rank <> 1 Then Exit Sub For i = x.GetLowerBound(0) To x.GetUpperBound(0) x(i) += 1 Next End Sub
178
5 Prozedurale Programmierung
Egal, ob der Feldparameter in der Prozedur mit ByRef oder ByVal deklariert ist, die Feldelemente können in jedem Fall in der Prozedur verändert werden (siehe oben). Dennoch sind die beiden Schlüsselwörter nicht gleichwertig! Wenn Sie beispielsweise durch ReDim die Größe des Felds verändern, wirkt sich diese Veränderung nur bei ByRef aus, nicht aber bei ByVal. Bei Funktionen können Felder auch als Rückgabeparameter verwendet werden, wie das folgende Beispiel beweist. myFuncArray(n) liefert ein Integer-Feld mit n Elementen, wobei die Elemente mit 0, 1, 2 etc. initialisiert sind. Function myFuncArray(ByVal n As Integer) As Integer() Dim a(n - 1) As Integer Dim i As Integer For i = 0 To n - 1 a(i) = i Next Return a End Function
Optionale Parameter Normalerweise müssen beim Aufruf einer Prozedur alle bei der Deklaration angegebenen Parameter übergeben werden. Es besteht aber die Möglichkeit, mit Optional einige Parameter als optional zu kennzeichnen. Dabei muss für jeden Parameter ein Defaultwert angegeben werden. Wenn der Parameter beim Aufruf nicht angegeben wird, dann wird stattdessen der Defaultwert eingesetzt. Innerhalb der Prozedur gibt es keine Möglichkeit festzustellen, ob ein Parameter gar nicht übergeben wurde oder ob (vielleicht zufällig) genau der Defaultwert übergeben wurde.
HINWEIS
Sub myProcOptional(ByVal x As Integer, _ Optional ByVal y As Integer = 0, _ Optional ByVal z As String = "") ...
In vielen Fällen können Sie statt optionaler Parameter auch mehrere unterschiedliche Prozedurdeklarationen verwenden, die sich durch die Parameteranzahl unterscheiden. Das vermindert zwar die Flexibilität beim Aufruf der Prozedur ein wenig, dafür ist aber klar, welcher Parameter angegeben wurde und welcher nicht.
Variable Parameteranzahl Optionale Parameter haben den Nachteil, dass ihre Anzahl vorgegeben ist. Indem Sie den letzten Parameter einer Prozedur mit ParamArray deklarieren, können Sie die Parameteranzahl variabel machen. Das ParamArray-Feld muss mit ByVal deklariert werden. Es ist nicht möglich, gleichzeitig optionale Parameter und ParamArray einzusetzen.
5.3 Prozeduren und Funktionen
179
Die größte Flexibilität erzielen Sie, wenn Sie ParamArray mit dem Datentyp Object kombinieren – dann können die übergebenen Parameter jeden beliebigen Typ annehmen. Das erfordert aber auch eine entsprechende Auswertung des Datentyps innerhalb der Prozedur. Sub myProcParamArray(ByVal x As Integer, _ ByVal ParamArray y() As Object) Dim i As Integer Console.WriteLine("x = " + x.ToString) For i = 0 To y.GetUpperBound(0) Console.WriteLine("y(" + i.ToString + ") hat den Typ " + _ y(i).GetType.Name) Next End Sub myProcParamArray gibt im Konsolenfenster für alle übergebenen Parameter den Datentyp
an. Die folgenden Zeilen zeigen einen möglichen Prozeduraufruf und die dazugehörende Ausgabe. myProcParamArray(1, 2, 3.0, "abc", New Random()) x = 1 y(0) hat y(1) hat y(2) hat y(3) hat
den den den den
Typ Typ Typ Typ
Int32 Double String Random
Prozeduraufruf mit benannten Parametern Wenn eine Prozedur mehrere optionale Parameter hat, beim Aufruf aber nur wenige Parameter angegeben werden, wird der Code rasch unübersichtlich. Das lässt sich anhand eines Beispiels leicht demonstrieren: Die folgende Prozedur ist eine Schablone zum Zeichnen eines Kreis- oder Ellipsenbogens. (Der Code zum tatsächlichen Zeichnen der Ellipse fehlt, es geht hier nur um die Parameterübergabe.) Sub myProcEllipse(ByVal x As Single, ByVal y As Single, _ ByVal r1 As Single, Optional ByVal r2 As Single = -1, _ Optional ByVal startangle As Single = 0, _ Optional ByVal endangle As Single = 360) If r2 < 0 Then r2 = r1 ' Ellipse zeichnen ... End Sub
Im einfachsten Fall (um einen Kreis zu zeichnen) können Sie die Prozedur so aufrufen: myProcEllipse(100, 100, 50)
Wenn Sie nur einen Halbkreis zeichnen möchten (von 0 bis 180 Grad), sieht der Aufruf so aus:
180
5 Prozedurale Programmierung
myProcEllipse(100, 100, 50, , , 180)
Auf ersten Blick ist nicht zu erkennen, welche Bedeutung der vierte Parameter hat. Wesentlich klarer wird der Code, wenn Sie den Parameternamen angeben und eine Zuweisung mit := durchführen. myProcEllipse(100, 100, 50, endangle:=180)
Wenn Sie möchten, können Sie auch alle Parameter benennen: myProcEllipse(x:=100, y:=100, r1:=50, endangle:=180)
Das hat den Vorteil, dass Sie nun nicht mehr an eine bestimmte Reihenfolge gebunden sind. myProcEllipse(r1:=50, endangle:=180, x:=100, y:=100)
5.3.5
Syntaxzusammenfassung
Prozedurdefinition Sub name(parameterlist) ... End Sub
deklariert die Prozedur name. Die Prozedur kann mit Exit Sub oder mit Return vorzeitig verlassen werden.
Function name(parameterlist) As datentyp ... name = ergebnis Return ergebnis End Function
deklariert die Funktion name. Die Funktion kann mit Exit Function oder mit Return vorzeitig verlassen werden.
Deklaration eines Parameters ByVal para1 As datentyp
deklariert einen Wertparameter.
ByRef para2 As datentyp
deklariert einen Referenzparameter (Übergabe eines Zeigers).
ByVal/ByRef para3() As datentyp
deklariert einen Parameter zur Übergabe eines Felds.
Optional ByVal/ByRef para3 As datentyp
deklariert einen optionalen Parameter.
ByVal ParamArray para() As datentyp
deklariert ein Parameterfeld (beliebige Parameteranzahl).
Prozeduraufruf name(para1, para2)
führt die Prozedur name aus.
name( (para1), (para2))
wie oben, übergibt para1 und para2 aber mit ByVal.
5.4 Operatoren
181
Prozeduraufruf name(para1:=wert1, para2:=wert2)
wie oben, die Parameterübergabe erfolgt aber mit so genannten benannten Parametern.
ergebnis = name(para1, para2)
wertet bei einem Funktionsaufruf das Ergebnis aus.
5.4
Operatoren
Die Anwendung der meisten Operatoren bedarf keiner langen Erklärung – die Syntaxreferenz am Ende dieses Abschnitts sollte ausreichen. Vorweg wird nur auf einige Besonderheiten hingewiesen. Zuweisungsoperatoren: VB.NET kennt die von C bekannten Zuweisungsoperatoren wie +=, -= etc. Damit ist a += 1 also eine Kurzschreibweise für a = a + 1. (Die Schreibweise a++ ist in VB.NET dagegen nicht zulässig.) Verkettung von Zeichenketten: Hierfür stehen zwei Operatoren zur Auswahl. + kann wirklich nur mit Zeichenketten umgehen und verbindet etwa "ab"+"cd" zu "abcd". & kommt auch mit Zahlen zurecht und wandelt diese automatisch in Zeichenketten um. "12" & 3 liefert "123". (Das funktioniert auch, wenn Option Strict verwendet wird.) Logische bzw. binäre Operatoren: Die Operatoren And, Or, Xor und Not können sowohl auf Wahrheitswerte als auch auf ganze Zahlen angewendet werden. Im zweiten Fall wird die binäre Repräsentation der Zahlen miteinander verknüpft. (12 entspricht binär 1010. 7 entspricht 0111. Das Ergebnis 12 Or 7 ergibt sich daraus, dass im binären Ergebnis überall dort eine 1 steht, wo eine der beiden Ausgangszahlen eine 1 hat – hier also binär 1111, was dezimal 15 ist.) If x > 4 And y>10 Then ...
'wie ausgeführt, wenn beide 'Bedingungen erfüllt sind
n = 12 Or 7
'liefert 15
Ein Nachteil von And und Or bei der Auswertung von Wahrheitswerten besteht darin, dass in jedem Fall beide Operanden ausgewertet werden. Das ist nicht nur unnötig langsam, sondern kann auch zu Fehlern führen. Aus diesem Grund stehen auch die Operatoren AndAlso bzw. OrElse zur Verfügung: •
Bei a AndAlso b wird b nur dann ausgewertet, wenn a=True gilt. (Wenn dagegen a=False gilt, dann ist a And b auf jeden Fall False, ganz egal, was b ist.)
•
Bei a OrElse b wird b nur dann ausgewertet, wenn a=False gilt. (Wenn dagegen a=True gilt, dann liefert a Or b auf jeden Fall True, unabhängig vom Wert von b.)
Sonstige Operatoren: AddressOf liefert die Adresse einer Prozedur Delegate-Objekt. Das ist beispielsweise notwendig, um Ereignisprozeduren einzurichten (siehe Abschnitt 7.6).
182
5 Prozedurale Programmierung
TypeOf kann in der Konstruktion If TypeOf o Is c verwendet werden, um zu überprüfen, ob o ein Objekt der Klasse c ist. GetType(c) liefert ein Type-Objekt der angegebenen Klasse. (GetType erfordert Klammern und sieht nicht aus wie ein Operator – aber die VB.NET-Hilfe besteht darauf, dass es einer ist. Beachten Sie aber, dass es auch eine Methode GetType gibt, die in der Form o.GetType() angewendet wird!) TypeOf und GetType sind in Abschnitt 4.6.4
beschrieben. Operatorenhierarchie: Dass nicht alle Operatoren gleichberechtigt sind, wissen Sie seit der Grundschule. Bei a + b * c wird zuerst die Multiplikation ausgeführt, dann die Addition. Wenn Sie eine andere Reihenfolge brauchen, müssen Sie Klammern setzen. In VB.NET gilt darüber hinaus die folgende Hierarchie: •
Arithmetische Operatoren und Verknüpfungsoperatoren
•
Vergleichsoperatoren
•
Logische Operatoren
VERWEIS
Die zuerst genannten Operatoren werden zuerst ausgeführt: If a + b > c And d entspricht also If ((a+b) > c) And d. Eine vollständige Tabelle mit der Hierarchie aller Operatoren finden Sie, wenn Sie in der Hilfe nach Operatorvorrang Visual Basic suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/vagrpoperatorprecedence.htm
Syntaxzusammenfassung Zuweisungen =
weist einer Variablen oder einem Objekt einen Wert zu (z.B. a=3).
:=
weist einem benannten Parameter einen Wert zu.
+= -=
vergrößert bzw. verkleinert eine Variable um den angegebenen Wert (z.B. a+=1)
*= /=
multipliziert bzw. dividiert eine Variable um den angegebenen Wert.
\=
speichert in der Variable den Rest einer ganzzahligen Division. (a\=3 entspricht a = a Mod 3).
+= &=
fügt einer Zeichenkette eine andere Zeichenkette hinzu.
Verknüpfung von Zeichenketten +
verknüpft Zeichenketten.
&
verknüpft beliebige Datentypen, sofern sie in Zeichenketten umgewandelt werden können.
5.4 Operatoren
183
Arithmetische Operatoren -
ist das negatives Vorzeichen.
+ - * /
führt die Grundrechnungsarten durch.
^
potenziert Zahlen. (2^3 liefert 8.)
\
führt eine ganzzahlige Division für ganze Zahlen durch. (8 \ 3 liefert 2.)
Mod
liefert den Rest einer Division mit einem ganzzahligen Ergebnis. (8.3 Mod 3.1 liefert 2.1. Dieses Ergebnis ergibt sich aus 8.3/3.1=2.67. Der Wert 2.67 wird abgerundet zu 2. Nun wird 8.3 - 2 * 3.1 ermittelt: Ergebnis: 2.1.)
Vergleichsoperatoren =
testet auf Gleichheit.
<>
testet auf Ungleichheit.
< <=
testet, ob der erste Wert kleiner als der zweite ist (bzw. kleiner-gleich).
> >=
testet, ob der erste Wert größer als der zweite ist (bzw. größer-gleich).
Is
testet, ob zwei Objekte gleich sind.
Like
führt einen Mustervergleich für Zeichenketten durch. ("abcd" LIKE "?b*" liefert True.)
Logische Operatoren And
verknüpft zwei (Wahrheits-)Werte durch logisches Und.
AndAlso
funktioniert wie And, allerdings wird b bei a AndAlso b nur dann ausgewertet, wenn a=True gilt. AndAlso kann nur für Wahrheitswerte verwendet werden.
Or
verknüpft zwei (Wahrheits-)Werte durch logisches Oder.
OrElse
funktioniert wie Or, allerdings wird b bei a OrElse b nur dann ausgewertet, wenn a=False gilt. OrElse kann nur für Wahrheitswerte verwendet werden.
Xor
verknüpft zwei (Wahrheits-)Werte durch logisches Exklusiv-Oder.
Sonstige Operatoren AddressOf fn
ermittelt die Adresse einer Funktion, Prozedur oder Methode.
GetType(c)
liefert ein Type-Objekt für die angegebene Klasse (z.B. GetType(String)).
TypeOf x
testet, ob x ein Objekt einer bestimmten Klasse ist. TypeOf darf nur im Rahmen der Konstruktion If TypeOf x Is abc Then ... eingesetzt werden. x muss ein Objekt eines Referenztyps enthalten, kein ValueType-Objekt!
184
5 Prozedurale Programmierung
Andere Sonderzeichen im Programmcode ' Kommentar
leitet einen Kommentar ein.
#Region
leitet eine Direktive für den Editor oder Compiler ein.
_
gibt an, dass die Programmzeile fortgesetzt wird. (Vor _ muss ein Leerzeichen stehen!)
"abc"
gibt eine Zeichenkette an (String-Datentyp).
"a"C
gibt ein einzelnes Zeichen an (Char-Datentyp).
#12/31/2002#
gibt ein Datum an (optional mit Zeitangabe).
&H123
gibt eine hexadezimale Zahl an.
&O123
gibt eine oktale Zahl an.
123@ oder 123D
gibt eine Decimal-Zahl an.
123! oder 123F
gibt eine Single-Zahl an.
123# oder 123R
gibt eine Double-Zahl an.
123& oder 123L
gibt eine Long-Zahl an.
123% oder 123I
gibt eine Integer-Zahl an.
123S
gibt eine Short-Zahl an.
( ... )
ändert die Abarbeitungsreihenfolge, z.B. (a Or b) And c oder (a+1) * 3).
x(n)
greift auf das n-te Element des Felds x() zu.
{1, 2, 3}
zählt Elemente auf (z.B. Dim a() As Integer = {1, 2, 3}).
[name]
gibt einen Variablen- oder Prozedurnamen an, der denselben Namen wie ein VB.NET-Schlüsselwort hat.
gibt ein Attribut an.
6
Klassenbibliotheken und Objekte anwenden
Ob Sie Ihre eigenen Projekte mit VB.NET objektorientiert konzipieren oder eher traditionell aus Prozeduren und Funktionen aufbauen, bleibt Ihnen überlassen. Auf jeden Fall müssen Sie lernen, mit den durch VB.NET vorgegebenen Klassenbibliotheken zu arbeiten. Ob Sie nun ein einfaches Formular mit Steuerelementen erstellen, mit System.IO-Objekten eine Textdatei erzeugen oder Methoden zur Bearbeitung von Zeichenketten (also String-Objekten!) anwenden – immer haben Sie es mit dem objektorientierten Ansatz von .NET zu tun. Ziel dieses Kapitels ist es, Ihnen die Welt der Objekte nahezubringen. Damit meine ich nicht nur die Erklärung der Nomenklatur (Klasse, Objekt, Methode, Eigenschaft, Ereignis etc.), sondern auch den Umgang mit dem Objektbrowser sowie die Kunst, die umfassende Online-Dokumentation richtig zu lesen.
VERWEIS
6.1 6.2 6.3
Schnelleinstieg Verwendung der .NET-Bibliotheken Objektbrowser
186 193 203
Dieses Kapitel beschränkt sich auf die Nutzung von Objekten, die durch VB.NET bzw. durch die .NET-Bibliotheken vorgegeben sind. Sie können mit VB.NET natürlich aber auch eigene Klassen mit Eigenschaften, Methoden etc. programmieren. Detaillierte Informationen zur objektorientierten Programmierung mit VB.NET finden Sie im nächsten Kapitel. Eine zumeist gut verständliche Sprachdefinition finden Sie auch in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
186
6.1
6 Klassenbibliotheken und Objekte anwenden
Schnelleinstieg
Dieser Abschnitt führt anhand von zwei Beispielen in die objektorientierte Programmierung ein – zumindest so weit es um die Nutzung von vorgegebenen Klassen aus der .NETBibliothek geht. Als Grundlage für die beiden Beispiele dienen verschiedene Klassen, die den Zugriff auf das Dateisystem ermöglichen. Diese Klassen werden im Detail zwar erst in Kapitel 10 vorgestellt, aber Sie werden die Beispiele auch ohne die dort vermittelten Details verstehen.
6.1.1
Miniglossar
Dieses Miniglossar soll Ihnen beim Verständnis der nachfolgenden Beispielprogramme helfen. Die Kurzbeschreibungen der wichtigsten Begriffe aus der objektorientierten Programmierung sind natürlich recht theoretisch und abstrakt – aber anhand der Beispiele in den nächsten Abschnitten sollte die Bedeutung dann rasch klar werden. (Ein ausführlicheres Glossar finden Sie übrigens im Anhang dieses Buchs.) Klasse: Eine Klasse beschreibt die Eigenschaften eines Objekts. Die Klasse ist gewissermaßen der Bauplan für ein Objekt. In der .NET-Klassenbibliothek sind mehrere Tausend Klassen definiert. Damit ist die Klassenbibliothek eine wichtige Grundlage für eigene Programme. Objekte: Objekte sind konkrete Realisierungen von Klassen. In Variablen speichern Sie daher Objekte, nicht Klassen. Objekte müssen explizit mit dem New-Operator erzeugt werden, entweder bei der Deklaration in der Form Dim var As New xyz() oder zu einem späteren Zeitpunkt durch eine Zuweisung in der Art var = New xyz(). Die Variable var verweist dann auf das Objekt der Klasse xyz. Manchmal wird ein Objekt auch als Instanz einer Klasse bezeichnet. Methoden: Methoden sind mit Prozeduren oder Funktionen vergleichbar. Der Unterschied besteht darin, dass sie normalerweise auf ein Objekt angewendet werden, entweder um Daten des Objekts zu ermitteln oder um das Objekt zu bearbeiten. Es gibt auch besondere Methoden (so genannte Shared-Methoden), die direkt auf eine Klasse angewendet werden können. Derartige Methoden dienen meist zur Ermittlung von Informationen oder zur Durchführung von Operationen, die unabhängig von einem konkreten Objekt sind. Eigenschaften: Eigenschaften sind mit Variablen vergleichbar. Der Unterschied besteht abermals darin, dass Eigenschaften auf ein Objekt oder eine Klasse angewendet werden müssen. Eigenschaften dienen dazu, Daten eines Objekts zu lesen oder zu verändern. (Es gibt auch ReadOnly-Eigenschaften, die nur gelesen werden können.) Ereignisse: Ereignisse erleichtern die Kommunikation zwischen einem Objekt und dem Programm, das das Objekt erzeugt hat. Ereignisse ermöglichen es, dass bei bestimmten Änderungen des Objektinhalts automatisch eine Prozedur aufgerufen wird. (Das erspart die aus manchen Programmiersprachen bekannten Warteschleifen, in denen periodisch überprüft wird, ob sich bestimmte Daten eines Objekts mittlerweile verändert haben.)
6.1 Schnelleinstieg
187
Namensräume: Die meisten Klassennamen der .NET-Bibliothek sind ziemlich lang – z.B. System.Windows.Forms.Button. Die mehrteiligen Klassennamen ermöglichen es, inhaltlich zusammengehörende Klassen zu Gruppen zusammenzufassen. Eine derartige Gruppe ist System.Windows.Forms. Sie enthält alle zur Windows-Programmierung erforderlichen Klassen. Der offizielle Begriff für derartige Gruppen lautet Namensraum (name space).
6.1.2
Beispiel 1 – Textdatei erzeugen und löschen
Beim hier vorgestellten Beispielprogramm handelt es sich um eine Konsolenanwendung (siehe Abbildung 6.1). Als einziges Programm in diesem Buch ist es mit Zeilennummern abgedruckt, damit eindeutig auf einzelne Zeilen hingewiesen werden kann. Wenn Sie das Programm selbst eingeben, dürfen Sie die Zeilennummern natürlich nicht mit eingeben! (Einfacher ist es ohnedies, das Programm von der beiliegenden CD zu laden.)
Abbildung 6.1: Beispielprogramm zur objektorientierten Programmierung
Die Programmausführung beginnt und endet mit der Prozedur Main, die (der einzige) Bestandteil des Moduls Module1 ist. Dieser Programmaufbau ist charakteristisch für die meisten Konsolenanwendungen. In den Zeilen 6-9 werden mehrere Variablen deklariert. fname und txt dienen zur Speicherung von Zeichenketten. In sw soll später ein Objekt der Klasse System.IO.StreamWriter gespeichert werden. Die Kurzschreibweise IO.StreamWriter ist deswegen zulässig, weil der Compiler bei allen Klassen automatisch System voranstellt, wenn es die Klasse unter dem angegebenen Namen nicht erkennt. (Das Voranstellen von System ist Teil des so genannten Importmechanismus, der in Abschnitt 6.2.2 noch näher beschrieben wird.) Die drei im Beispielprogramm vorkommenden System.IO.*-Klassen sind Teil der Bibliothek mscorlib.dll, die allen .NET-Programmen automatisch zur Verfügung steht. Es sind daher keine besonderen Vorbereitungen erforderlich, damit die Klassen verwendet werden können. (.NET stellt noch viel mehr Bibliotheken zur Verfügung. Wenn Sie Klassen daraus benutzen möchten, müssen Sie in der Regel zuerst einen Verweis auf die Bibliothek einrichten – siehe Abschnitt 6.2.1.) Alle drei System.IO.*-Klassen befinden sich im Namensraum System.IO.
188
01 02 03 04 05 06 07 08 09
6 Klassenbibliotheken und Objekte anwenden
' Beispiel oo-programmierung\intro Option Strict On Module Module1 Sub Main() ' diverse Variablen deklarieren Dim fname, txt As String Dim sw As IO.StreamWriter Dim fi As IO.FileInfo Dim sr As IO.StreamReader
HINWEIS
In Zeile 11 wird der Dateiname einer temporären Datei ermittelt. Dazu wird die Methode GetTempFileName der Klasse System.IO.Path verwendet. Bei dieser Methode handelt es sich um eine so genannte Shared-Methode. Das bedeutet, dass es nicht erforderlich (und in diesem Fall auch gar nicht möglich) ist, zuerst ein Objekt der Klasse System.IO.Path zu erzeugen, um die Methode dann darauf anzuwenden. (Shared-Methoden werden in Abschnitt 6.2.4 genauer behandelt.) Aus unerfindlichen Gründen liefert GetTempFileName den Dateinamen in einer Kurzschreibweise, die gewährleistet, dass jeder einzelne Datei- oder Verzeichnisname maximal acht Zeichen lang ist. Diese Kurzschreibweise wurde beim Übergang von DOS bzw. Windows 3.1 zu Windows 95 eingeführt und hatte damals durchaus ihre Berechtigung. Aber warum auch die .NET-Klassenbibliothek noch zum seligen DOS kompatibel sein muss, dass weiß allein Microsoft. (Davon, dass eine DOSkompatible .NET-Version geplant ist, habe ich auf jeden Fall noch nichts gehört ...)
Zeile 12 verwendet die Methode WriteLine der Klasse System.Console, um eine Zeile in das Konsolenfenster zu schreiben. Diese Methode nimmt beliebig viele Parameter entgegen. Der erste Parameter ist zumeist eine Zeichenkette, die auch Formatplatzhalter für die weiteren Parameter enthalten darf. {0} bedeutet, dass an dieser Stelle der Inhalt des nächsten Parameters in Textform ausgegeben werden soll. Auch WriteLine ist eine Shared-Methode – deswegen ist es nicht notwendig, vorher ein Console-Objekt zu erzeugen. 10 11 12
' Dateinamen für temporäre Datei ermitteln fname = IO.Path.GetTempFileName() Console.WriteLine("Temporäre Datei: {0}", fname)
In Zeile 14 wird zum ersten Mal ein Objekt erzeugt: Dazu wird an den so genannten NewKonstruktor der Klasse System.IO.StreamWriter ein Dateiname übergeben. Diese Klasse hilft beim Schreiben von Textdateien. Der Konstruktor liefert als Ergebnis ein Objekt dieser Klasse. sw gilt nun als Objektvariable, d.h., sie verweist auf das Objekt und ermöglicht so deren Bearbeitung. (Sollte es aus irgendeinem Grund nicht möglich sein, die Datei zu erstellen, würde in dieser Zeile ein Fehler auftreten.) In den Zeilen 15-16 wird die Methode WriteLine auf das StreamWriter-Objekt angewendet. Damit werden zwei Textzeilen in der Datei gespeichert. In Zeile 17 wird die Datei durch die Methode Close geschlossen. Damit wird das StreamWriter-Objekt aus dem Speicher entfernt. (Bei manchen Klassen gibt es statt Close ein Dispose-
6.1 Schnelleinstieg
189
Methode, um das Objekt zu löschen. Die überwiegende Mehrheit der Klassen kennt aber weder Close noch Dispose. Bei derartigen Klassen gibt es keine Möglichkeit, den durch das Objekt belegten Speicher explizit freizugeben. Stattdessen erkennt die so genannte garbage collection, die ständig im Hintergrund ausgeführt wird, wenn das Objekt nicht mehr benötigt wird, und entfernt es dann automatisch aus dem Speicher.)
HINWEIS
13 14 15 16 17
' Textdatei erzeugen sw = New IO.StreamWriter(fname) sw.WriteLine("eine Zeile Text") sw.WriteLine("noch eine Zeile") sw.Close()
Wenn Sie längere Variablennamen verwenden (was in realen Programmen der Regelfall sein sollte, um eine bessere Lesbarkeit zu gewährleisten), ist es bisweilen lästig, diesen Variablennamen jedes Mal voranzustellen, wenn Sie eine Methode oder Eigenschaft auf das Objekt anwenden möchten. Um Ihnen diese Tipparbeit zu ersparen, können Sie mit With das Objekt fixieren. Allen Methoden und Eigenschaften, die sie auf das Objekt anwenden möchten, brauchen Sie nun nur noch einen Punkt voranstellen. With sw .WriteLine("eine Zeile Text") .WriteLine("noch eine Zeile") .Close() End With With kann nur für Objekte, nicht aber für Klassen verwendet werden. With Console
ist daher nicht erlaubt (auch wenn das bisweilen durchaus praktisch wäre). In Zeile 19 wird ein Objekt der Klasse System.IO.FileInfo erzeugt. Diese Klasse dient dazu, Informationen über eine Datei zu ermitteln. Das FileInfo-Objekt wird hier dazu verwendet, um festzustellen, ob die Datei überhaupt existiert und wie groß diese Datei ist. Diese Informationen werden mit den Eigenschaften Exists und Length ermittelt, die auf das Objekt angewendet werden. Die Ergebnisse werden mit der schon vertrauten Methode Console.WriteLine im Konsolenfenster angezeigt. 18 19 20 21
' Informationen über die erzeugte Datei anzeigen fi = New IO.FileInfo(fname) Console.WriteLine("Die Datei existiert: {0}", fi.Exists) Console.WriteLine("Dateilänge: {0}", fi.Length)
In Zeile 23 wird ein Objekt der Klasse System.IO.StreamReader erzeugt. Diese Klasse ist das Gegenstück zu StreamWriter und hilft beim Lesen von Textdateien. Zeile 23 beweist, dass neue Objekte nicht nur durch den New-Konstruktor erzeugt werden können; hier ist es vielmehr die Methode OpenText der FileInfo-Klasse, die als Ergebnis ein neues Objekt liefert. Auf das StreamReader-Objekt wird nun die Methode ReadToEnd angewendet. Diese Methode liest die Datei bis zu ihrem Ende und liefert das Ergebnis als Zeichenkette, die in der
190
6 Klassenbibliotheken und Objekte anwenden
Variablen txt zwischengespeichert wird. Der Inhalt der Variable wird mit Console.WriteLine im Konsolenfenster ausgegeben. (Es wäre natürlich ebenso möglich gewesen, Zeile 24 und 27 zu kombinieren: Console.WriteLine(sr.ReadToEnd()).) 22 23 24 25 26 27
' Inhalt der erzeugten Datei anzeigen sr = fi.OpenText() txt = sr.ReadToEnd() sr.Close() Console.WriteLine("Inhalt der Datei:") Console.WriteLine(txt)
In Zeile 29 wird die temporäre Datei wieder gelöscht. Dazu wird auf das (noch immer vorhandene) FileInfo-Objekt die Methode Delete angewendet. 28 29
' Datei löschen fi.Delete()
Damit das Konsolenfenster nicht verschwindet, bevor Sie die Textausgaben lesen können, wird mit der Methode ReadLine der Klasse System.Console auf die Eingabe einer Textzeile gewartet. ReadLine würde eigentlich die eingegebene Zeichenkette zurückgeben – aber die ist für das Programm uninteressant. Der einzige Sinn von ReadLine besteht hier darin, dass mit dem Programmende so lange gewartet werden soll, bis der Anwender Return drückt. Zeile 32 ist also ein Beispiel für den Aufruf einer Methode, die zwar einen Rückgabewert hat, der aber ignoriert wird. 30 31 32 33 34
' Programmende Console.WriteLine("Return drücken") Console.ReadLine() End Sub End Module
6.1.3
Beispiel 2 – Dateiereignisse empfangen
Das zweite Beispielprogramm überwacht das temporäre Verzeichnis des Anwenders, der das Programm startet. Jedes Mal, wenn sich in diesem Verzeichnis irgendetwas ändert (d.h., wenn eine Datei erzeugt, geändert, gelöscht oder umbenannt wird), wird dies im Konsolenfenster angezeigt (siehe Abbildung 6.2). Return beendet das Progamm. Um das Programm zu testen, starten Sie am besten dieses Programm und dann das im vorigen Abschnitt präsentierte Programm. Letzteres erzeugt innerhalb des temporären Verzeichnisses eine neue Datei und löscht diese etwas später wieder. (Wenn Sie das introevent-Programm länger laufen lassen, werden Sie feststellen, dass im temporären Verzeichnis laufend Dateien geändert werden.)
VORSICHT
6.1 Schnelleinstieg
191
Dieses Programm funktioniert nur, wenn Sie es unter Windows NT/2000/XP ausführen. (Nur diese Betriebssystemversionen können mit der hier eingesetzten .NETKlasse System.IO.FileSystemWatcher korrekt kommunizieren.) Dass die Dateinamen trotz dieser Betriebssystemanforderung in einer DOSkompatiblen 8+3-Schreibweise angezeigt werden, ist eine Besonderheit der System.IO-Klassen, die sich leider nicht abstellen lässt.
Abbildung 6.2: Ereignisse im temporären Verzeichnis des Anwenders
Die Main-Prozedur des Programms zeichnet sich durch prägnante Kürze aus: In der ersten Zeile wird ein neues Objekt der Klasse System.IO.FileSystemWatcher erzeugt. Dabei wird an den New-Konstruktor das Verzeichnis übergeben, das überwacht werden soll. Als Testobjekt wird das temporäre Verzeichnis verwendet, das mit der Shared-Methode GetTempPath der Klasse System.IO.Path ermittelt wird. In der nächsten Zeile wird die Eigenschaft EnableRaisingEvents auf True gesetzt. Damit löst das FileSystemWatcher-Objekt nun bei jeder Änderung innerhalb des angegebenen Verzeichnisses ein Ereignis aus. Die zwei weiteren Zeilen dienen dazu, dass das Programm erst durch die Eingabe von Return beendet wird. Der eigentlich interessante Teil des Programms befindet sich freilich außerhalb von Main. Zum einen wird die Variable fsw zur Speicherung des FileSystemWatcher-Objekts nicht innerhalb von Main deklariert, sondern auf Modulebene. Das ist erforderlich, damit das zusätzliche Schlüsselwort WithEvents angegeben werden darf. Dieses Schlüsselwort erleichtert die Deklaration von Ereignisprozeduren zum Empfang von Ereignissen. Die FileSystemWatcher-Klasse kennt fünf verschiedene Ereignisse, von denen im Beispielprogramm vier ausgewertet werden. Zum Empfang der Ereignisse müssen so genannte Ereignisprozeduren in den Code eingefügt werden. Diese Prozeduren werden automatisch aufgerufen, sobald ein Ereignis auftritt. Die Entwicklungsumgebung hilft Ihnen beim Einfügen von Ereignisprozeduren: Wählen Sie zuerst im linken Listenfeld den Namen einer mit WithEvents deklarierten Objektvariablen aus und suchen Sie dann im rechten Listenfeld ein Ereignis aus: Die Entwicklungsumgebung fügt dann die Zeilen Public Sub ereignisname ... und End Sub in den Code ein. Nun müssen Sie nur noch die Zeilen dazwischen eingeben.
192
6 Klassenbibliotheken und Objekte anwenden
An Ereignisprozeduren werden üblicherweise zwei Parameter übergeben, die Sie innerhalb der Prozedur auswerten können. sender gibt an, woher das Ereignis stammt. Beim Beispielprogramm ist das FileSystemWatcher die Ereignisquelle, d.h., sender verweist auf dieses Objekt. e enthält ereignisspezifische Daten. Bei den hier vorgestellten Ereignissen enthält e unter anderem den Namen der Datei oder des Verzeichnisses, die bzw. das das Ereignis ausgelöst hat (e.FullPath). Beim Renamed-Ereignis können Sie auch den ursprünglichen Namen ermitteln (e.OldFullPath) ' Beispiel oo-programmierung\intro-event Module Module1 Dim WithEvents fsw As IO.FileSystemWatcher Sub Main() fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath) fsw.EnableRaisingEvents = True Console.WriteLine("Return beendet das Programm") Console.ReadLine() End Sub Public Sub fsw_Changed(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Changed Console.WriteLine("Datei {0} hat sich geändert", e.FullPath) End Sub Public Sub fsw_Created(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Created Console.WriteLine("Datei {0} wurde erzeugt", e.FullPath) End Sub Public Sub fsw_Deleted(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Deleted Console.WriteLine("Datei {0} wurde gelöscht", e.FullPath) End Sub Public Sub fsw_Renamed(ByVal sender As Object, _ ByVal e As System.IO.RenamedEventArgs) Handles fsw.Renamed
VERWEIS
Console.WriteLine("Datei {0} wurde umbenannt in {1}", _ e.OldFullPath, e.FullPath) End Sub End Module
VB.NET kennt zwei unterschiedliche Mechanismen zur Aktivierung von Ereignisprozeduren. In diesem Beispielprogramm wurde die WithEvents-Variante demonstriert. Wie Sie Ereignisse auch ohne WithEvents empfangen können, erklärt Abschnitt 7.6.
6.2 Verwendung der .NET-Bibliotheken
6.2
193
Verwendung der .NET-Bibliotheken
Ganz egal, welches Programm Sie in VB.NET entwickeln möchten – die Verwendung der .NET-Bibliotheken ist unumgänglich. Selbst elementare Merkmale der Sprache VB.NET sind in diesen Bibliotheken definiert (z.B. Datentypen wie Double, Integer oder String). Die Verzahnung zwischen VB.NET und den .NET-Bibliotheken geht so weit, dass es schlicht unmöglich ist, ein Programm ohne die beiden Basisbibliotheken mscorlib und system zu entwickeln. Da die große Bedeutung von .NET-Bibliotheken also auf der Hand liegt, gibt dieser Abschnitt eine Menge Tipps zur richtigen und effizienten Nutzung dieser Bibliotheken.
6.2.1
Verweise auf Bibliotheken einrichten
In VB.NET-Programmen können Sie nur die Klassen, Strukturen und Methoden solcher Bibliotheken nutzen, auf die Sie in Ihrem Programm verweisen. Per Default ist das (bei einer VB.NET-Konsolenanwendung) für die folgenden Bibliotheken der Fall: •
Microsoft Visual Basic.NET Runtime (Microsoft.VisualBasic.dll)
•
Basisklassenbibliothek (mscorlib.dll) mit der Definition der .NET-Datentypen sowie mit zahllosen Basisklassen und -Methoden.
•
System-Klassenbibliothek (System.dll) mit weiteren Grundfunktionen, z.B. zur Nutzung
unterschiedlicher Netzwerk- und Internet-Protokolle •
System.Data-Klassenbibliothek (System.Data.dll) zum Datenbankzugriff
•
System.XML-Klassenbibliothek (System.Xml.dll) zum Umgang mit XML-Daten
Von diesen fünf Bibliotheken stehen die ersten zwei in VB.NET-Projekten immer zur Verfügung. (Es ist nicht möglich, die Verweise darauf zu entfernen.) Alle weiteren Bibliotheken werden im Projekmappen-Explorer angezeigt und können dort entfernt werden (siehe Abbildung 6.3).
Abbildung 6.3: Verweise auf Bibliotheken im Projektmappen-Explorer
194
6 Klassenbibliotheken und Objekte anwenden
Bei Windows-Projekten kommen zwei weitere Defaultbibliotheken hinzu: •
System.Drawing-Klassenbibliothek (System.Drawing.dll) mit den Grafikklassen (GDI+)
•
System.Windows.Forms-Klassenbibliothek
(System.Windows.Forms.dll)
mit
zahllosen
Klassen zur Windows-Programmierung Wenn Sie darüber hinaus Klassen nutzen möchten, die in anderen Bibliotheken definiert sind, müssen Sie vorher mit PROJEKT|VERWEIS HINZUFÜGEN einen Verweis auf die entsprechende Bibliothek einrichten. (Beachten Sie, dass Sie mit dem VERWEISE-Dialog nicht nur .NET-Bibliotheken, sondern auch herkömmliche COM-Bibliotheken mit Ihrem Projekt verbinden können.)
6.2.2
Klassennamen verkürzen mit Imports
Mit Ausnahme einiger elementarer Funktionen befinden sich alle weiteren Schlüsselwörter, die Sie in Ihrer täglichen Programmierung benötigen, in diversen Bibliotheken.
HINWEIS
Ein Beispiel sind die arithmetischen Funktionen (Sin, Cos etc.): Diese Funktionen stehen nicht ohne weiteres zur Verfügung. Der Ausdruck Sin(x) liefert nur die Fehlermeldung, dass Sin unbekannt ist. Stattdessen lautet die neue Schreibweise System.Math.Sin(x). Die Funktion Sin (die nach der exakten Objektnomenklatur eigentlich eine Methode ist) wird durch die Bibliothek System.Math zur Verfügung gestellt. Statt System.Math.Sin(x) funktioniert auch Math.Sin(x). Der Grund besteht darin, dass System per Default vorangestellt wird (siehe den nächsten Abschnitt Import-Einstellungen in den Projekteigenschaften).
Wenn Sie in einem Programm häufig arithmetische Funktionen einsetzen, werden Sie es bald satt haben, ständig System.Math oder Math voranzustellen (ganz abgesehen davon, dass der Code damit fast unleserlich wird). Das muss aber auch gar nicht sein: Die Lösung für das Problem lautet Imports System.Math. Damit stehen alle in dieser Bibliothek definierten Funktionen direkt zur Verfügung. Die Sinusfunktion kann also wieder in der Form Sin(x) verwendet werden. Genau genommen importiert Imports keine Funktionen, Bibliotheken etc., sondern fügt einfach nur den angegebenen Namensraum (namespace) an. Wenn Sie ein Schlüsselwort eingeben, sucht die Entwicklungsumgebung in allen derartigen Namensräumen, ob es das Schlüsselwort darin finden kann. Die Imports-Anweisung muss am Beginn einer Codedatei (am Beginn eines Moduls) angegeben werden, und zwar vor der Deklaration von Prozeduren, Klassen etc. Die Anweisung gilt für den gesamten Code der Datei (aber nicht für das gesamte Projekt).
6.2 Verwendung der .NET-Bibliotheken
195
Imports System.Math Module Module1 Sub Main() Dim d1, d2 As Double d1 = 1.5 d2 = Sin(d1) MsgBox("Der Sinus von " & d1 & " beträgt " & d2 & ".") End Sub End Module
Import-Einstellungen in den Projekteigenschaften Neben den Imports-Anweisungen, die immer nur für eine Codedatei gelten, können Sie auch globale Importe durchführen. Um die Importe anzusehen bzw. zu verändern, klicken Sie im Projektmappen-Explorer den Projektnamen an. Anschließend finden Sie entsprechende Einstellmöglichkeit im Dialogblatt PROJEKT|EIGENSCHAFTEN|ALLGEMEINE EIGENSCHAFTEN|IMPORTE (siehe Abbildung 6.4). Um dem Projekt einen zusätzlichen Namensraum hinzuzufügen, geben Sie dessen Namen im Textfeld NAMESPACE ein und klicken dann den Button IMPORT HINZUFÜGEN an.
Abbildung 6.4: Globaler Namensraum-Import für das gesamte Projekt
HINWEIS
196
6 Klassenbibliotheken und Objekte anwenden
Es gibt weder einen Auswahldialog zur Eingabe des Namensraums, noch erfolgt eine Kontrolle, ob der Namensraum korrekt geschrieben wurde. Wenn Sie also irrtümlich Systems.Math (statt korrekt System.Math) angeben, aktzeptiert der Dialog diese Einstellung ohne Fehlermeldung. Die Verwendung der System.Math-Schlüsselwörter klappt aber natürlich nicht, bis Sie den Tippfehler entdeckt haben.
Abbildung 6.4 zeigt die Default-Importe für VB-Konsolenprojekte. Je nach Projekttyp gelten aber andere Importe! Bei Windows-Anwendungen sind beispielsweise auch System.Drawing und System.Windows.Forms aktiviert. Intern werden diese Einstellungen in der Projektdatei (Endung *.vbproj) gespeichert.
TIPP
Die Defaulteinstellungen (je nach Projekttyp) sind der Grund, weswegen Sie in Programmen viele Schlüsselwörter unmittelbar nutzen können, obwohl diese Schlüsselwörter in den unterschiedlichsten Bibliotheken, Klassen und Namensräumen definiert sind. Natürlich tritt manchmal auch das umgekehrte Problem auf: Sie haben sich daran gewöhnt, dass Sie bestimmte Schlüsselwörter ohne Imports-Anweisungen in Ihren Programmen nutzen können. In einem anderen Projekt funktioniert das dann plötzlich nicht mehr. Die Ursache ist fast immer, dass in den Projekteigenschaften andere Defaulteigenschaften gelten. Aus diesem Grund verwenden die meisten Beispiele dieses Buchs keine Importe außer den Defaultimporten der Entwicklungsumgebung.
Doppeldeutigkeiten (Namenskonflikte) In unterschiedlichen Namensräumen können gleichnamige Klassen oder Methoden definiert sein. Wenn Sie unter Zuhilfenahme von Importen die Kurzschreibweise verwenden, kann es zu Doppeldeutigkeiten kommen, die der Compiler nicht automatisch korrekt auflösen kann. Beispielsweise kennt die Klasse System.Windows.Forms.Form die Eigenschaft Left, die die xKoordinate des linken Fensterrands enthält. Dem steht die Methode Left aus der Klasse Microsoft.VisualBasic.Strings gegenüber. Wenn Sie nun im Formularcode eines Windows-Programms die Zeichenkettenmethode Left wie gewohnt unmittelbar verwenden möchten (s = Left("abcd", 2)), kommt es zu einem Fehler: Der Compiler beklagt sich darüber, dass die Eigenschaft Left gar keine Parameter
akzeptiert. (Offensichtlich ist der Compiler davon überzeugt, dass Sie die Windows-Eigenschaft Left meinen.) Um dieses Problem zu umgehen, müssen Sie den Namen der gewünschten Left-Methode exakter angeben (also z.B. mit s = Strings.Left("abcd", 2)).
Alias Sie können mit Imports auch eine Abkürzung, also einen so genannten Alias definieren. Die beiden folgenden Zeilen demonstrieren das Konzept. (Wirklich sinnvoll ist diese Vorge-
6.2 Verwendung der .NET-Bibliotheken
197
hensweise natürlich nur bei längeren Namensräumen und wenn Sie Namenskonflikte durch gewöhnliche Importe vermeiden möchten.) Imports mymath = System.Math d2 = mymath.Sin(d1)
Manchmal klappt der Zugriff auf Methoden auch ohne Imports Warum können manche Methoden direkt verwendet werden, während anderen der Klassennamen vorangestellt werden muss? Beispielsweise können Sie die Zeichenkettenfunktion Microsoft.VisualBasic.Strings.Left ohne weiteres in der Form Left(...) verwenden, während Sie der Mathematikfunktion System.Math.Sin zumindest Math voranstellen müssen (also Math.Sin(...)).
VERWEIS
Das Verhalten von Sin entspricht dem Regelfall, der für alle Klassen der .NET-Standardbibliothek gilt. Left ist insofern eine Ausnahme, als es nicht in den .NET-Bibliotheken definiert ist, sondern in einer VB-Zusatzbibliothek Microsoft.VisualBasic. Diese Bibliothek verwendet zur Deklaration mancher Klassen das Attribut <StandardModuleAttribute>. Dieses Attribut ist nicht dokumentiert, es bewirkt aber offensichtlich, dass die Klasse wie ein Modul betrachtet wird, dessen globale Methoden unmittelbar – ohne Nennung des Klassennamens – verwendet werden können. Offensichtlich soll dieses Attribut eine etwas höhere Kompatibilität zu VB6 erzielen. In Abschnitt 7.3.1 finden Sie genaue Informationen darüber, was Module sind und wodurch sie sich von Klassen unterscheiden. Eine Erklärung, was Attribute sind, folgt in Abschnitt 7.7.
6.2.3
Das System-Wirrwarr
Aus dem Namen System.begriff1.begriff2.begriff3 geht leider nicht hervor, ob begriffn eine Klasse, ein Namensraum oder eine Methode ist, wo das Schlüsselwort definiert ist und wie es in einem VB.NET-Programm verwendet werden kann. Die folgenden Beispiele illustrieren das Problem. Es geht jeweils um die Frage, wie ein Objekt der betreffenden Klasse in einem VB.NET-Programm genutzt werden kann: •
System.Random: Diese Klasse ist Teil der Bibliothek mscorlib.dll. Diese Bibliothek steht in VB.NET-Programmen immer zur Verfügung.
Innerhalb der mscorlib-Bibliothek sind mehrere so genannte Namensräume definiert. Random ist eine Klasse des Namensraums System. Ein Objekt der Klasse System.Random kann ohne weiteres mit Dim myobj As Random deklariert und verwendet werden, weil einerseits die mscorlib-Bibliothek in jedem VB.NET-Programm zur Verfügung steht und weil andererseits System zu den Defaultimporten zählt (daher die Kurzschreibweise Random statt System.Random).
198
•
6 Klassenbibliotheken und Objekte anwenden
System.Text: Hierbei handelt es sich nicht um eine Klasse, sondern um einen Namens-
raum der CLR, der selbst Klassen enthält. Es ist daher weder möglich noch sinnvoll, ein Objekt der Klasse System.Text zu erzeugen. •
System.Text.UnicodeEncoding: Die Encoding-Klasse ist eine der in System.Text enthaltenen Klassen. Die Anwendung ist ähnlich unkompliziert wie bei System.Random. Als Kurzschreibweise ist Text.UnicodeEncoding zulässig (wegen des System-Defaultimports).
•
System.Text.RegularExpression: Wenn Sie nun glauben, dass RegularExpression einfach eine weitere Klasse des System.Text-Namensraums wäre, irren Sie! System.Text.RegularExpression ist vielmehr ein weiterer Namensraum, der in der .NET-Bibliothek System definiert ist (Datei System.dll). Auch diese Bibliothek steht per Default in allen VB.NET-
Programmen zur Verfügung. •
System.Text.RegularExpression.Regex: Die Regex-Klasse ist in System.Text.RegularExpression definiert. Da die System-Bibliothek per Default in allen VB.NET-Programmen zur Verfügung steht, kann ein Regex-Objekt ohne weiteres erzeugt werden. Als Kurzschreibweise ist Text.RegularExpression.Regex zulässig.
•
System.Web.Mail.MailMessage: Die MailMessage-Klasse ist im Namensraum System.Web.Mail der .NET-Bibliothek System.Web (Datei System.Web.dll) deklariert. Per Default ist in
TIPP
VB.NET-Projekten kein Verweis auf diese Bibliothek eingerichtet. Um die Klasse verwenden zu können, müssen Sie daher zuerst diesen Verweis einrichten. Anschließend ist die Kurzschreibweise Web.Mail.MailMessage zulässig. Wenn Sie diese – nur in den ersten Tagen verwirrenden – Hintergründe selbst erforschen möchten, sollten Sie sich mit dem Objektbrowser anfreunden (siehe Abschnitt 6.3).
6.2.4
Shared- und Instance-Klassenmitglieder
Wenn Sie die Online-Hilfe zu einer beliebigen .NET-Klasse durchlesen, werden Sie bei jeder Klasse eine Members-Aufzählung finden. Dabei handelt es sich um eine Tabelle mit allen Methoden, Eigenschaften, Operatoren und anderen Schlüsselwörtern dieser Klasse. Diese Schlüsselwörter sind in verschiedene Gruppen gegliedert. Die folgenden Beispiele sollen Ihnen dabei helfen, die Dokumentation richtig zu lesen und zu verstehen.
Beispiel – Klassenmitglieder von System.DateTime System.DateTime ist eine Klasse der System-Bibliothek von .NET. Diese Klasse beschreibt einerseits den Visual-Basic-Datentyp Date. (Jede Date-Variable ist also genau genommen ein Objekt der System.DateTime-Klasse!) Andererseits stellt diese Klasse eine Menge Methoden, Eigenschaften etc. zur Verfügung, die auch losgelöst von Date-Variablen
verwendet werden können.
6.2 Verwendung der .NET-Bibliotheken
199
VERWEIS
Suchen Sie im Hilfesystem nach DateTime-Members oder verwenden Sie die folgende Adresse: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfsystemdatetimememberstopic.htm
Hier geht es nur um die Grundlagen der Anwendung von Klassenbibliotheken. Die DateTime-Klasse wird dabei nur als Beispiel verwendet. Wenn Sie genaue Informationen über die tatsächliche Anwendung der DateTime-Klassenmitglieder suchen, werfen Sie einen Blick in Abschnitt 8.2, wo der Umgang mit Daten und Zeiten detailliert beschrieben wird.
Wenn Sie sich die Dokumentation zu System.DateTime im Hilfesystem ansehen (siehe Abbildung 6.5), finden Sie dort eine lange Tabelle mit Schlüsselwörtern. Die folgende Tabelle nennt jeweils nur ein Mitglied aus jeder Kategorie. Beispiel: Ausgewählte Mitglieder der System.DateTime-Klasse Öffentliche Konstruktoren
New(y, m, d)
erzeugt ein neues System.DateTime-Objekt und intialisiert es mit dem Datum d.m.y: Dim d As New Date(2001, 12, 31)
Ein Konstruktor dient dazu, ein neues Objekt zu erzeugen. In VB.NET verwenden Sie den Konstruktor mit New. Bei manchen Konstruktoren können Sie dabei Parameter übergeben, um das neue Objekt gleich zu initialisieren. Öffentliche Felder MaxValue
liefert das größte zulässige Datum, das mit der System.DateTime-Klasse verarbeitet werden kann. Felder werden in diesem Buch als Klassenvariablen bezeichnet, um Konfusion mit Feldern (im Sinne des englischen Worts array) zu vermeiden. Sie enthalten oft Konstanten, die spezifisch für die Klasse, aber unabhängig vom jeweiligen Objekt sind (z.B. den größten und kleinsten Wert, der in der Klasse gespeichert werden kann, oder eine Naturkonstante wie System.Math.Pi).
Öffentliche Eigenschaften
Now
liefert die aktuelle Zeit (samt Datum). Eigenschaften können wie Klassenvariablen verwendet werden (auch wenn sie intern ganz anders realisiert sind). Sie geben Auskunft über die im Objekt gespeicherten Daten. Manche Eigenschaften können auch verändert werden, andere können nur gelesen werden (ReadOnly).
200
6 Klassenbibliotheken und Objekte anwenden
Beispiel: Ausgewählte Mitglieder der System.DateTime-Klasse Öffentliche Methoden
IsLeapYear(n)
Öffentliche Operatoren
Subtraction(d1, d2)
ermittelt die Zeitspanne zwischen zwei Daten. (Beachten Sie, dass Sie den Operator in dieser Form nicht in VB.NET nutzen können. Sie müssen stattdessen die Methode Subtract verwenden.)
Geschützte Felder, Eigenschaften, Methoden etc.
Finalize()
wird automatisch aufgerufen, wenn das DateTime-Objekt aus dem Speicher entfernt wird.
gibt an, ob n ein Schaltjahr ist oder nicht. Methoden dienen zur Bearbeitung von Objekten.
Geschützte Klassenmitgleider stehen Ihnen normalerweise nicht zur Verfügung. (Die einzige Ausnahme besteht darin, dass Sie Code für eine abgeleitete Klasse entwickeln. Weitere Informationen zu diesem Thema finden Sie im nächsten Kapitel.)
Abbildung 6.5: Die Beschreibung der DateTime-Klassenmitglieder in der Online-Hilfe
6.2 Verwendung der .NET-Bibliotheken
201
Shared- versus Instance-Mitglieder Im Hilfetext sind manche Schlüsselwörter durch ein gelbes Icon in der Form des Buchstaben S gekennzeichnet. Dieses S steht für Shared (VB.NET) bzw. static (C#). Es gibt an, ob es sich bei dem Schlüsselwort um ein Shared- oder um ein Instance-Mitglied der Klasse handelt. Der Unterschied zwischen diesen beiden Gruppen ist ausgesprochen wichtig für die korrekte Anwendung von Methoden. •
Shared-Schlüsselwörter können sowohl eigenständig als auch als Methoden bzw. Eigenschaften von System.DateTime-Objekten verwendet werden. Aus diesem Grund können Sie die Eigenschaft Now sowohl als Element der Variablen d verwenden als auch als eigenständige Eigenschaft der Klasse System.DateTime. Dim d, e As Date e = d.Now e = System.DateTime.Now e = DateTime.Now
'd und e sind Objekte der 'System.DateTime-Klasse 'd mit aktueller Zeit belegen 'd mit aktueller Zeit belegen 'Kurzschreibweise
Beachten Sie bitte, dass die drei Zuweisungen absolut gleichwertig sind. Obwohl es so aussieht, als würde der Ausdruck d.Now in irgendeiner Form d auswerten, ist dies bei Shared-Schlüsselwörtern nicht der Fall! Sie sollten sich angewöhnen, Shared-Schlüsselwörter immer nur den Klassennamen, nicht aber eine Objektinstanz voranzustellen. Zwar ist der dafür erforderliche Tippaufwand meist etwas höher, aber dafür ist Ihr Code viel besser zu verstehen. In den .NET-Klassen dominieren unter den Shared-Mitgliedern die Methoden. Da derartige Methoden verwendet werden können, ohne vorher ein Objekt der Klasse zu erzeugen, entsprechen sie in ihrer Anwendung eher herkömmlichen Funktionen als Methoden. Die folgenden Zeilen geben einige Beispiele für den Aufruf von Shared-Methoden aus unterschiedlichen Klassen. Dim b As Boolean, s As String, x As Double Dim ar() As String = {"xy", "abc", "123"} b = Date.IsLeapYear(2004) 'testet, ob 2004 ein Schaltjahr ist s = IO.Path.ChangeExtension("c:\name.txt", "bak") '--> c:\name.bak Array.Sort(ar) 'sortiert die Elemente des Felds ar x = Math.Sqrt(7) 'berechnet die Quadratwurzel von 7
•
Instance-Schlüsselwörter können dagegen nur verwendet werden, wenn sie auf ein konkretes Objekt angewendet werden. Dim d As Date d = d.AddDays(2)
'entspricht d = d + [zwei Tage]
Soweit es sich (wie bei Date) nicht um ValueType-Klassen handelt, muss das Objekt vor der Verwendung mit New erzeugt werden. In den folgenden Zeilen wird zuerst ein neues Objekt der Klasse System.Collections.Hashtable erzeugt. Anschließend wird darauf die Add-Methode angewandt, um die Geburtstage von zwei Personen zu speichern. ht("Gerhard") ermittelt das Geburtsdatum von Gerhard, wobei das eigentlich eine Kurz-
202
6 Klassenbibliotheken und Objekte anwenden
schreibweise für ht.Item("Gerhard") ist. ht.Count ermittelt, wie viele Einträge das Hashtable-Objekt enthält. (Was eine Hashtable ist, erfahren Sie in Kapitel 9.) Dim n As Integer, d As Date Dim ht As New Collections.Hashtable() ht.Add("Gerhard", #4/27/1978#) ht.Add("Susanne", #7/3/1967#) d = CType(ht("Gerhard"), Date) 'd = #4/27/1978# n = ht.Count 'n = 2
HINWEIS
Beachten Sie, dass die Klassifizierung in Shared- und Instanced-Schlüsselwörtern nicht immer logisch und bisweilen sogar vollkommen inkonsequent ist. Beispielsweise gilt die SortMethode von System.Array als Shared (also Array.Sort(ar)), während die äquivalente Sort-Methode von System.Collections.ArrayList ein Instanced-Schlüsselwort ist (also alist.Sort()). Im Objektbrowser können Sie den Typ von Klassenmitgliedern leider nur mit Mühe erkennen: bei Shared-Schlüsselwörter enthält die Deklaration das Schlüsselwort Shared (siehe Abbildung 6.7 im nächsten Abschnitt). Alle Mitglieder, die im Objektbrowser nicht als Shared gekennzeichnet sind, sind Instance-Mitglieder.
Verwendung von Instance-Klassenmitgliedern ohne Objektvariable Oben habe ich gerade erklärt, dass Instanced-Schlüsselwörter nur im Kontext einer Objektvariablen verwendet werden können. Im Regelfall bedeutet das also, dass Sie zuerst ein Objekt der entsprechenden Klasse deklarieren (Dim myobj As New klassenname()) und dann dessen Eigenschaften oder Methoden anwenden (myobj.MethodeXy). Die folgenden Zeilen erzeugen ein Objekt der Klasse System.Random und wenden darauf die Methode Next an, um eine Zufallszahl zu erzeugen. Dim i As Integer Dim myrand As New Random() i = New Random().Next 'i enthält eine Zufallszahl
Wenn Sie eine bestimmte Methode aber nur ein einziges Mal benötigen, können Sie auf die Objektvariable auch verzichten. Die beiden folgenden Zeilen sind zu den drei Zeilen oben gleichwertig. Die Zufallszahl (nicht das Random-Objekt!) wird in der Variablen i gespeichert. Das Random-Objekt wird hingegen sofort wieder verworfen. Dim i As Integer i = New Random().Next
'i enthält eine Zufallszahl
Vor allem Programmierer, die mit Java oder C++ vertraut sind, wird auch diese Vorgehensweise vertraut sein. Empfehlenswert ist sie aber nur, wenn Sie die Methode (hier Next) wirklich nur einmal benötigen. Wird die Methode dagegen in einer Schleife angewendet, sollten Sie das Random-Objekt unbedingt in einer Variablen speichern. (Andernfalls muss mit jedem Schleifendurchgang ein neues Random-Objekt erzeugt werden. Auch wenn sich VB.NET darum kümmert, dass diese Objekte automatisch wieder aus dem Speicher entfernt werden, kostet das Erzeugen und Beseitigen von Objekten unnötig Zeit.)
6.3 Objektbrowser
6.3
203
Objektbrowser
Ein unverzichtbares Hilfsmittel zur Erforschung der riesigen Klassenbibliotheken ist der Objektbrowser (ANSICHT|ANDERE FENSTER|OBJEKTBROWSER, siehe Abbildung 6.6). Dieser Dialog liefert Informationen über die Verwendung von Methoden, Eigenschaften und Konstanten, die in den Klassen verschiedener Bibliotheken deklariert sind. Die Grundfunktion ist einfach: Im linken Dialogteil können Sie in einem hierarchischen Listenfeld zuerst eine Bibliothek (z.B. Microsoft VB.NET Runtime oder mscorlib), dann einen so genannten Namensraum und schließlich eine Klasse auswählen. Rechts werden dann alle für diese Klasse verfügbaren Schlüsselwörter (Methoden, Eigenschaften, Konstanten etc.) angezeigt. Sobald Sie eines dieser Schlüsselwörter mit der Maus anklicken, liefert der untere Dialogteil Informationen über alle Parameter des Schlüsselworts. Manchmal liefert der Browser sogar kurze Informationen über den Zweck des Schlüsselworts.
6.3.1
Tipps zur Bedienung
Gerade Einsteiger wenden sich manchmal schnell frustriert vom Objektbrowser ab, weil Sie von der der Informationsfülle überwältigt werden. Immerhin beweist der Objektbrowser, dass Ihnen unter VB.NET tatsächlich Tausende von Methoden, Eigenschaften etc. zur Verfügung stehen (von denen Sie natürlich nur einen verschwindenden Bruchteil tatsächlich brauchen). Während der Objektbrowser bis VB6 eine relativ übersichtliche Hilfe war, kann es nun schon passieren, dass man in den vielen Hierarchieebenen verloren geht, bevor man das gewünschte Element findet. Es lohnt sich aber, sich mit dem Dialog vertraut zu machen! Vielleicht helfen Ihnen dabei die folgenden Tipps weiter: •
Eine elegante Möglichkeit, diesen Dialog zu öffnen, besteht darin, ein Objekt oder eine Methode im Programmcode mit der rechten Maustaste anzuklicken und dann den Kontextmenüeintrag GEHE ZU DEFINITION auszuführen. Sie ersparen sich damit die oft langwierige Suche in der verschachtelten Objekthierarchie. (Falls Sie die VB-Tastenkürzel verwenden, führt Shift+F2 direkt in den Objektbrowser.)
•
Der Objektbrowser kann mit Esc geschlossen werden. Esc bietet damit den schnellsten Weg zurück ins Codefenster.
•
Per Default zeigt der Objektbrowser alle Schlüsselwörter in alphabetischer Reihenfolge an. Über das Kontextmenü können Sie aber auch angeben, dass die Schlüsselwörter im linken Dialogbereich PER OBJEKTTYP und im rechten Bereich PER MEMBERTYP sortiert werden. Das bedeutet, dass im linken Dialogbereich zuerst alle Klassen, dann alle Interfaces, dann alle Strukturen etc. angezeigt werden; im rechten Dialogbereich werden zuerst alle Methoden, dann die Eigenschaften etc. angezeigt. Mit anderen Worten: zusammengehörende Schlüsselwörter werden gruppiert. (Innerhalb dieser Gruppen werden die Schlüsselwörter natürlich weiterhin alphabetisch sortiert.)
204
6 Klassenbibliotheken und Objekte anwenden
•
Wenn Sie innerhalb des Objektbrowsers Text eingeben, wird das erste Schlüsselwort gesucht, das mit diesen Buchstaben beginnt. Das ermöglicht vor allem bei umfangreichen Namensräumen wie Windows.Forms oft einen rascheren Zugriff als per Maus.
•
Der Objektbrowser bietet eine Suchmöglichkeit, mit der Sie nach Schlüsselwörtern suchen können.
•
Bei vielen Klassen und Schlüsselwörtern führt F1 direkt zum Hilfetext. (F1 funktioniert im Objektbrowser zuverlässiger als im Codefenster.)
•
Mit Strg+C können Sie das Schlüsselwort in die Zwischenablage kopieren (um es anschließend in den Programmcode einzufügen). Dabei wird der vollständige Objektname kopiert (bei Abbildung 6.6 also Microsoft.VisualBasic.Strings).
•
Der Objektbrowser enthält nur Klassen von Bibliotheken, die zurzeit in Ihr Projekt eingebunden sind. Wenn Sie zusätzliche Bibliotheken einbinden möchten, führen Sie PROJEKT|VERWEIS HINZUFÜGEN aus oder klicken den Button ANPASSEN an (der dieselbe Wirkung hat).
Abbildung 6.6: Der Objektbrowser
6.3 Objektbrowser
6.3.2
205
Deklaration von Schlüsselwörtern
Wenn Sie im Objektbrowser eine Klasse im linken Bereich oder ein Klassenmitglied im rechten Bereich anklicken, wird im grauen Bereich darunter die exakte Deklaration des Schlüsselworts in der Syntax von VB.NET angezeigt (Public Shared Sub Sort(...) in Abbildung 6.7).
Abbildung 6.7: Sort ist eine Shared-Methode der Klasse System.Array
Sobald Sie einmal gelernt haben, die Informationen richtig zu interpretieren, gibt Ihnen diese Zeile genaue Auskunft darüber, wie Sie die Klasse, Methode etc. in Ihren Programmen einsetzen können. Alle Schlüsselwörter, die in der Deklarationszeile vorkommen, werden ausführlich im nächsten Kapitel beschrieben. Eine kompakte Referenz finden Sie in Abschnitt 7.10. Vorweg eine kleine Orientierungshilfe zu den wichtigsten Begriffen: •
Public, Private und Protected geben den Gültigkeitsbereich an. Bei der gewöhnlichen Anwendung können Sie nur Public-Schlüsselwörter nützen. (Protected-Schlüsselwörter stehen zur Verfügung, wenn Sie Klassen vererben.)
•
Class, Module und Structure sind verschiedene Varianten von Klassen. Structure-Klassen sind immer Werttypen (ValueType-Klassen).
•
Sub und Function bezeichnet Methoden, Property eine Eigenschaft, Dim und Const eine Klassenvariable bzw. Konstante, Event ein Ereignis. ReadOnly-Eigenschaften können nur gelesen, nicht verändert werden.
•
Shared gibt an, dass das Schlüsselwort ohne Objektinstanz verwendet werden kann (siehe Abschnitt 6.2.4).
•
[Not]Overridable gibt an, ob das Schlüsselwort durch Vererbung verändert werden kann. Für die gewöhnliche Anwendung spielt das keine Rolle.
206
6 Klassenbibliotheken und Objekte anwenden
•
MustOverride bzw. MustInherit bedeuten, dass das Schlüsselwort nicht unmittelbar genutzt werden kann (sondern in vererbten Klassen verändert werden muss).
•
ByVal und ByRef geben an, wie Parameter übergeben werden (siehe Abschnitt 5.3.4).
•
Inherits gibt an, von welcher Basisklasse die Klasse abgeleitet ist.
Abbildung 6.7 zeigt, dass Sort eine Methode (Sub) der Klasse System.Array ist. Die Methode ist öffentlich zugänglich (Public). Sie kann in der Form Array.Sort(...) verwendet werden (Shared). Als Parameter muss ein Objekt des Typs System.Array (also ein beliebiges Feld) übergeben werden, das dann sortiert wird.
Abgeleitete Schlüsselwörter (Vererbung) Wenn Sie im Objektbrowser die Eigenschaften oder Methoden einer Klasse betrachten, gewinnen Sie vielleicht den Eindruck, dass manche in der Online-Dokumentation oder in diesem Buch beschriebene Schlüsselwörter ganz einfach fehlen. Der Grund dafür besteht meist darin, dass die von Ihnen betrachtete Klasse von einer anderen Klasse abgeleitet ist und von der übergeordneten Klasse manche Schlüsselwörter geerbt hat. Werfen Sie beispielsweise einen Blick auf die Schlüsselwörter der Klasse DirectoryInfo (Bibliothek mscorlib, Namensraum System.IO, siehe Abbildung 6.8): Diese Klasse gibt Auskunft über die Eigenschaften eines Verzeichnisses (z.B. C:\WinNT4\System32). Die Eigenschaft Name enthält den Namen des Verzeichnisses (z.B. "System32"). Es scheint aber keine Eigenschaft zu geben, die den gesamten Verzeichnisnamen (also inklusive dem Laufwerk und den Unterverzeichnissen gibt). Im unteren Bereich des Objektbrowsers sehen Sie, dass die Klasse DirectoryInfo von System.IO.FileSystemInfo abgeleitet ist (DirectoryInfo Inherits FileSystemInfo, siehe Abbildung 6.8). Sie können nun zu dieser Klasse springen und deren Mitglieder lesen. Noch einfacher ist es aber, die DirectoryInfo-Klasse im Objektbrowser aufzuklappen. Sie gelangen zuerst zu Basen und Schnittstellen und dann zu allen übergeordneten Klassen, von denen DirectoryInfo abgeleitet ist. Dort entdecken Sie die vermisste Eigenschaft FullName (siehe Abbildung 6.9). Dass die Eigenschaft FullName auch für Objekte des Typs DirectoryInfo zur Verfügung steht, ist eine Konsequenz des Mechanismus der Vererbung. Beachten Sie, dass derartige Vererbungsmechanismen auch über mehrere Ebenen funktionieren! Im konkreten Fall enthalten die übergeordneten Klassen MarshalByRefObject und Object weitere Klassenmitglieder, die aber nur bei der internen Objektverwaltung hilfreich sind. Diesselbe Information finden Sie übrigens auch in der Online-Hilfe. Wenn Sie einen Blick in den Hilfetext zur DirectoryInfo-Klasse werfen, sehen Sie, dass dort auch alle abgeleiteten Schlüsselwörter aufgelistet sind (siehe Abbildung 6.10).
6.3 Objektbrowser
Abbildung 6.8: Die Klasse System.IO.DirectoryInfo
Abbildung 6.9: Die übergeordnete Klasse System.IO.FileSystemInfo
207
208
6 Klassenbibliotheken und Objekte anwenden
Abbildung 6.10: Der Hilfetext zu System.IO.DirectoryInfo
6.3.3
Objektbrowser-Icons
Alle Klassen, Methoden, Funktione etc. werden im Objektbrowser durch Icons symbolisiert. Diese Icons sehen anfänglich alle gleich aus, aber mit der Zeit werden Sie merken, dass sie eine wichtige Orientierungshilfe darstellen. Die erste Tabelle enthält die Icons, die im linken Bereich des Objektbrowsers angezeigt werden (auf der Objektseite). Assembly
.NET-Bibliothek (z.B. Microsoft VB Runtime, mscorlib, System.Data)
Namespace
Namensraum (z.B. System.IO, Microsoft.VisualBasic)
Class
Klasse (z.B. System.Math, Microsoft.VisualBasic.Strings)
Module
Modul (z.B. Module1() mit Main() in einem eigenen Programm)
Structure
CLS-kompatibler Datentyp (z.B. System.Integer, System.String)
Structure
CLS-inkompatibler Datentyp bzw. Datenstruktur (z.B. System.UInt16, .TimeSpan)
Enum
Konstantenaufzählung (z.B. Microsoft.VisualBasic.MsgBoxStyle)
6.3 Objektbrowser
209
Interface
Schnittstelle (eine abstrakte Definition von Methoden und Eigenschaften, die von anderen Klassen implementiert werden; z.B. System.IFormatProvider; eine Klasse, die diesem Interface entspricht, ist etwa System.Globalization.CultureInfo)
Delegate
Beschreibung der Parameter einer Funktion oder Methode (beispielsweise beschreibt System.Windows.Forms.KeyEventHandler die Ereignisprozedur zu den Ereignissen KeyUp und KeyDown)
Objektbrowser-Icons (Mitglieder) Diese Tabelle enthält die Icons, die im rechten Bereich des Objektbrowsers angezeigt werden (auf der Mitgliederseite). Es handelt sich dabei um die Elemente von Klassen, Strukturen, Aufzählungen (Enums) etc. Sub / Function Methode (z.B. Add für Objekte der Klasse System.DateTime) Property
Eigenschaft (z.B. DayOfWeek für Objekte der Klasse System.DateTime)
Constant
Konstante (z.B. System.Math.Pi)
Dim / Const
Klassenvariable oder -konstante (z.B. Microsoft.Visual Basic.Constants.vbNullChar, System.Guid.Empty)
Gültigkeit von Schlüsselwörtern Die in den beiden vorigen Tabellen vorgestellten Icons gelten in dieser Form, wenn das Schlüsselwort öffentlich zugänglich ist. Häufig sind Klassen, Methoden etc. aber so deklariert, dass sie nur innerhalb des aktuellen Projekts oder nur bei einer Vererbung der Klasse verwendet werden können. (Ausführliche Hintergrundinformationen zu den Gültigkeitsbereichen von Schlüsselwörtern finden Sie in Abschnitt 7.9.) Diese Gültigkeitsebenen werden im Objektbrowser durch eine Erweiterung der Icons durch eine Raute, einen Schlüssel oder ein Vorhängeschloss dargestellt. Die folgende Tabelle zeigt diese Erweiterungen am Beispiel des Icons für Klassenvariablen. Analog gelten diese Erweiterungen aber auch für Eigenschaften, Methoden etc. Public Friend Protected Private
öffentliche Klassenvariable; der Zugriff auf die Variable ist immer möglich Friend-Klassenvariable; der Zugriff ist nur innerhalb des Projekts
möglich, in dem die Klasse deklariert ist Protected-Klassenvariable; der Zugriff ist nur in vererbten Klassen
möglich Private-Klasenvariable; der Zugriff ist nur innerhalb des
Klassencodes möglich, der die Klasse beschreibt
7
Objektorientierte Programmierung
Während sich das vorige Kapitel mit der Nutzung von Klassen, Objekten, Methoden und Eigenschaften beschäftigt hat, geht es in diesem Kapitel darum, selbst Klassen zu programmieren, mit Methoden und Eigenschaften auszustatten etc. Dabei werden natürlich auch Themen wie Vererbung, Schnittstellen (interfaces) Attribute etc. behandelt. Selbst wenn Sie vorerst nicht vorhaben, eigene Klassen zu programmieren, lohnt sich ein Überfliegen dieses Kapitels. Von allgemeinem Interesse ist beispielsweise der Abschnitt über die Gültigkeitsbereiche von Variablen, Prozeduren, Methoden etc. (scope). Dieses Thema wird deswegen erst am Ende dieses Kapitels behandelt, weil vorher viele der dort genannten Begriffe noch unbekannt wären. 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10
Elemente eines Programms Klassen, Module, Strukturen Module und Strukturen Vererbung Schnittstellen (interfaces) Ereignisse und Delegates Attribute Namensräume Gültigkeitsbereiche (scope) Syntaxzusammenfassung
212 217 239 245 256 262 272 274 277 281
212
7.1
7 Objektorientierte Programmierung
Elemente eines Programms
VERWEIS
Dieser Abschnitt gibt eine erste, beispielorientierte Einführung in die Welt der Module, Klassen etc. Seien Sie beruhigt, systematische Informationen darüber, was Module, Klassen etc. sind und wie sie sich unterscheiden, folgen in den weiteren Abschnitten des Kapitels noch zuhauf! Eine zumeist gut verständliche Sprachdefinition von VB.NET finden Sie in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen. Dort finden Sie auch eine Beschreibung aller objektorientierten Schlüsselwörter. Diese gleichsam offizielle Sprachreferenz ist eine gute Ergänzung zu diesem Kapitel. ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
Hello World als Modul Die denkbar einfachste Variante liegt bei einer Konsolenanwendung im Stil von Hello World vor. Der gesamte Code befindet sich in einer einzigen Datei module1.vb. Darin ist das Modul Module1 definiert. Es enthält eine Prozedur, nämlich Main. Die Programmausführung beginnt und endet mit dieser Prozedur. (Der Startpunkt eines Programms kann in den Projekteigenschaften eingestellt werden. Das ist dann wichtig, wenn ein Programm aus mehreren Modulen besteht.) Das Konsolenprojekt oo-programmierung\hello-world besteht genau genommen aus viel mehr Dateien, die von der Entwicklungsumgebung automatisch erzeugt werden.
HINWEIS
• AssemblyInfo.vb enthält Meta-Informationen über das Programm (Copyright, Versionsnummer etc.). • hello-world.vbproj enthält eine Liste aller Codedateien sowie alle Projekteigenschaften. hello-world.vbproj.user kann zusätzliche benutzerspezifische Ergänzungen erhalten. • hello-world.sln enthält die Liste der Projekte sowie alle Eigenschaften der Projektmappe. (Einfache VB.NET-Projektmappen enthalten nur ein einziges Projekt, aber die Entwicklungsumgebung kann auch mehrere Projekte gemeinsam verwalten.) In diesem Abschnitt geht es aber nur um den reinen Code, soweit er von Ihnen selbst erstellt wird.
7.1 Elemente eines Programms
213
' Beispiel oo-programmierung/hello-world-console ' Datei module1.vb Module Module1 Sub Main() Console.WriteLine("Hello world (module)!") Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub End Module
Hello World als Klasse Im Zeitalter der objektorientierten Programmierung ist ein Modul eigentlich etwas Altmodisches, gewissermaßen ein Relik aus alten (Visual-)Basic-Zeiten. So verwundert es denn nicht, dass C# gar keine Module kennt. Selbstverständlich können Sie Hello World auch in VB.NET als Klasse realisieren. Dabei führen mehrere Wege zum Ziel: •
Sie können ein neues Projekt als Konsolenanwendung starten, die Moduldefinition löschen und stattdessen die folgenden Zeilen eingeben. Anschließend ändern Sie in den Projekteigenschaften das Startobjekt zu Class1.
•
Sie können ein neues Projekt auch als Klassenbibliothek starten. In der Codedatei geben Sie abermals den folgenden Code ein und ändern dann in den Projekteigenschaften den Ausgabetyp zu KONSOLENANWENDUNG.
•
Sie können die unten angegebene Klasse auch einfach beim bereits vorhandenen Projekt hello-world-console in die Codedatei einfügen. (Es ist also erlaubt, in einer Codedatei mehrere Klassen oder Module zu definieren.) Damit das Programm durch die Main()-Prozedur von Class1 gestartet wird, müssen Sie in den Projekteigenschaften als Startobjekt Class1 angeben.
Wichtig bei den folgenden Zeilen ist die Kennzeichnung der Prozedur Main mit Shared. Erst dadurch kann Main ausgeführt werden, ohne dass vorher ein Objekt der Klasse Class1 erzeugt wird. Class Class1 Shared Sub Main() Console.WriteLine("Hello world (class)!") Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub End Class
214
7 Objektorientierte Programmierung
Das Ergebnis ist in jedem Fall dasselbe: Die Programmausführung startet und endet mit Class1.Main(). Wenn Sie also einen Widerwillen gegen Module haben, können Sie alles auch mit Klassen erreichen. Daraus ergibt sich kein Vorteil, es erleichtert aber C#- oder JavaProgrammierer die Vorstellung darüber, was ein Modul eigentlich ist (siehe auch Abschnitt 7.3.1).
Programmstart bei Windows-Anwendung Während Konsolenanwendungen generell mit Main beginnen – egal, ob sich Main nun in einem Modul oder in einer Klasse befindet –, ist der Start von Windows-Programmen eine viel komplexere Angelegenheit. Der Normalfall besteht darin, dass VB.NET beim Kompilieren die folgende Anweisung zum Anzeigen des ersten Fensters des Programms einfügt: System.Windows.Forms.Application.Run(New formname())
Das bewirkt, dass ein Objekt der Klasse formname erzeugt wird. (Dieses Objekt wird am Bildschirm als Fenster sichtbar.) Gleichzeitig wird eine so genannte Nachrichtenschleife eingerichtet, die Ereignisse (z.B. Tastatureingaben) feststellt und an das Programm weiterleitet. Das Programm endet, wenn das Startfenster geschlossen wird. (Im Detail wird der Start von Windows-Programmen in Abschnitt 15.2 beschrieben.)
Codedateien Ein Programm kann sich aus beliebig vielen *.vb-Codedateien zusammensetzen. Innerhalb jeder Codedatei können wiederum beliebig viele Klassen und Module definiert werden. Für die Gültigkeitsbereiche von Modulen, Prozeduren, Variablen etc. spielt es keine Rolle, in welcher Datei sich der Code befindet. Um eine neue Codedatei hinzuzufügen, führen Sie PROJEKT|MODUL HINZUFÜGEN oder PROJEKT|KLASSE HINZUFÜGEN aus. (Die beiden Kommandos sind absolut gleichwertig. Der Unterschied besteht darin, dass als Dateiname einmal ModuleN.vb vorgeschlagen wird, das andere Mal ClassN.vb. Außerdem enthält die neue Datei einmal eine leere Modulschablone, das andere Mal eine leere Klassenschablone. Das sollte aber nicht darüber hinwegtäuschen, dass Sie in jeder dieser Dateien nach Belieben Klassen und Module definieren dürfen und dass der Compiler beide Dateien vollkommen gleichwertig behandelt.) Direkt in der Codedatei können Sie die folgenden Konstrukte starten: Namespace, Module, Class, Structure, Enum, Interface
Hingegen können Deklarationen von Variablen, Konstanten, Prozeduren, Methoden, Eigenschaften und Ereignissen nur innerhalb eines Moduls, einer Klasse oder einer Struktur vorgenommen werden.
Projekteigenschaften Mit dem in Abbildung 7.1 dargestellten Dialog kommen Sie normalerweise erst dann in Berührung, wenn Sie das Defaultverhalten von VB.NET-Projekten ändern möchten. (Per Default beginnt die Programmausführung bei Konsolenanwendungen mit der Prozedur
7.1 Elemente eines Programms
215
Main des ersten Moduls, bei Windows-Anwendungen mit dem Anzeigen des ersten Fens-
ters.)
TIPP
Aber auch wenn Sie mit dem Defaultverhalten durchaus zufrieden sind, empfiehlt sich die Auseinandersetzung mit diesem Dialog. Sie lernen dann die Hintergründe eines VB.NETProgramms ein wenig besser verstehen. Deswegen werden in den folgenden Punkten einige Einstellmöglichkeiten des Dialogs kurz beschrieben. Um den Dialog zu öffnen, klicken Sie am besten den Projektnamen im PROJEKTMAPPEN-EXPLORER mit der rechten Maustaste an. Das Kontextmenü EIGENSCHAFTEN führt zum Dialog. Das Hauptmenükommando PROJEKT|EIGENSCHAFTEN funktioniert dagegen nur dann, wenn das Projekt vorher im im PROJEKTMAPPEN-EXPLORER markiert wird.
•
ASSEMBLYNAME: Dieser Punkt gibt an, wie die resultierende Programmdatei heißen soll. An diesen Namen wird noch .exe oder .dll angehängt. (Ein Assembly ist – ein wenig vereinfacht ausgedrückt – die aus einem Projekt resultierende Programm- oder Bibliotheksdatei. Beachten Sie, dass eine Assembly bei komplexen Projekten aber auch aus mehreren Dateien bestehen kann.)
•
AUSGABETYP: Dieser Punkt bestimmt die Art des Assembly. Zur Auswahl stehen KONSOLENANWENDUNG, WINDOWS-ANWENDUNG oder KLASSENBIBLIOTHEK. (Die unzähligen Pro-
jekttypen, die zu Beginn eines neuen Projekts zur Auswahl stehen, können also auf diese drei Varianten reduziert werden.) Die drei Varianten unterscheiden sich vor allem durch das Startverhalten voneinander: KONSOLENANWENDUNGEN werden normalerweise mit der Prozedur Main gestartet, WINDOWS-ANWENDUNGEN durch Application.Run(New formname()), KLASSENBIBLIOTHEKEN über-
haupt nicht. (Sie können nur von anderen Projekten verwendet bzw. getestet werden, indem dort eine Objektinstanz einer der Klassen erzeugt wird.) Ein weiterer Unterschied besteht darin, dass KLASSENBIBLIOTHEKEN zu *.dll-Dateien kompiliert werden, die beiden anderen Typen zu *.exe-Dateien. •
STARTOBJEKT: Mit diesem Listenfeld können Sie entweder SUB MAIN oder den Namen einer Klasse als Startobjekt auswählen. Im ersten Fall dürfen alle Module und Klassen des Projekts nur eine einzige Main()-Prozedur enthalten. Die Programmausführung beginnt mit dieser Prozedur.
Im zweiten Fall (Auswahl einer Klasse) wird bei Konsolenanwendungen die MainProzedur dieser Klasse gestartet. Bei Windows-Anwendungen wird eine Instanz der Klasse (die von Windows.Forms.Form abgeleitet sein muss) an Application.Run übergeben. •
STAMM-NAMESPACE: Dieses Feld bestimmt den Defaultnamensraum für das gesamte Projekt. Dieser Name ist nur dann von Bedeutung, wenn es sich bei dem Projekt um eine Klassenbibliothek handelt. In diesem Fall gibt der Namensraum an, wie im Projekt definierte Klassen von außen angesprochen werden. Wenn Sie also in Ihrem Projekt die Klasse Class1 definiert haben und der Defaultnamensraum lautet hello_world, dann kann die Klasse von externen Projekten unter dem Namen hello_world.Class1 angesprochen
216
7 Objektorientierte Programmierung
werden. (Innerhalb des Projekts besteht die Möglichkeit, weitere Unternamensräume mit dem Schlüsselwort Namespace zu definieren – siehe Abschnitt 7.8.) •
Sonstiges: In den anderen Blättern des Eigenschaftsdialogs können Sie eine ganze Menge weiterer Eigenschaften einstellen. Sie betreffen unter anderem die Genauigkeit, mit der Variablendeklarationen durchgeführt werden müssen (Option Explicit, Option Strict), das Icon der Programmdatei, Defaultimporte (siehe Abschnitt 6.2.2), Kompilier- und Debugging-Optionen etc. Diese Einstellungen sind in diesem Kapitel aber nicht von Interesse.
HINWEIS
Abbildung 7.1: Einstellung der Projekteigenschaften
Die Defaulteinstellungen für ASSEMBLYNAME und STAMM-NAMESPACE werden vom Projektnamen übernommen, den Sie beim Start eines neuen Projekts angeben. Im NAMESPACE-Namen werden gegebenenfalls unzulässige Sonderzeichen durch _ ersetzt. Achten Sie darauf, dass der Projektname nicht mit dem Namen einer .NET-Klasse oder eines .NET-Namenraums übereinstimmt – sonst gibt es Probleme beim Zugriff auf diese Klasse! Diese Probleme beheben Sie, indem Sie im Eigenschaftsdialog den NAMESPACE-Namen ändern.
7.2 Klassen, Module, Strukturen
7.2
217
Klassen, Module, Strukturen
Dieser Abschnitt führt in die Programmierung von Klassen, Modulen und Strukturen ein. Klassen stehen deswegen an erster Stelle, weil sie das universellste Konstrukt darstellen. Module und Strukturen haben ähnliche Eigenschaften und werden auf ähnliche Weise definiert (programmiert), ihr Funktionsumfang im Vergleich zu Klassen ist aber eingeschränkt. Sobald Sie verstehen, was Klassen sind und welche Merkmale sie haben, wird es Ihnen leicht fallen, auch das Konzept von Modulen und Strukturen zu erkennen.
7.2.1
Klassen
Am Beginn steht die Frage: Wozu Klassen? Nur wenn Sie wissen, wozu Sie Ihr Projekt in Klassen organisieren, kann diese Organisation auch gelingen. Im Wesentlichen helfen Klassen bei zwei Dingen: erstens, den Code in sinnvolle Einheiten zu gliedern; und zweitens, immer wiederkehrende Aufgaben so zu kapseln, dass sie sowohl im aktuellen Projekt als auch in anderen Projekten wiederverwendet werden können. Klassen sind sozusagen die Grundlage für ein modernes Code-Recycling: Statt also ein wiederkehrendes Problem immer wieder neu zu lösen, können Sie für diese Aufgabe eine Klasse entwickeln und diese dann in verschiedenen Projekten entweder als Code (durch simples Einfügen) oder als Bibliothek (durch eine Referenz) nutzen.
HINWEIS
Damit dieses Konzept erfolgreich ist, sollten Sie sich Zeit für die richtige Organisation der Klasse nehmen, d.h. für die Überlegung, durch welche Eigenschaften und Prozeduren Sie die Funktionen der Klasse nach außen hin zugänglich machen, wie die Daten intern verwaltet werden etc. Dabei können Sie durchaus die Erfahrungen, die Sie mit der Anwendung der zahllosen .NET-Bibliotheken bereits gemacht haben, in das Design mit einfließen lassen. Und vergessen Sie nicht, Ihre Klasse ordentlich zu dokumentieren! Die ganze Idee der Wiederverwendung von Code scheitert oft daran, dass es weniger Arbeit bereitet, eine Funktion ein zweites Mal neu zu implementieren als nachzuvollziehen, wie eine bereits vorhandene Klasse eingesetzt werden kann. Mit diesen beiden Absätzen schließe ich das Thema Klassendesign aus Platzgründen auch schon wieder ab. Es gibt zahllose exzellente Bücher zu den Themen Entwurfsmuster (design patterns) und Modellierung (UML, Unified Modeling Language), die sich diesem Thema in aller Ausführlichkeit widmen. In diesem Kapitel geht es lediglich darum, Ihnen die Syntaxelemente von VB.NET zu beschreiben. Mit diesem Wissen sollten Sie dann in der Lage sein, die Design-Tipps aus anderen Büchern zur objektorientierten Programmierung in VB.NET umsetzen.
Was sind Klassen und woraus bestehen sie? Eine Klasse ist die abstrakte Beschreibung (der Bauplan) eines objektorientierten Datentyps. Die Schnittstelle nach außen (also zur Anwendung der Klasse) wird in erster Linie
218
7 Objektorientierte Programmierung
durch Eigenschaften und Methoden hergestellt. Viele Klassen bieten auch den direkten Zugang auf Datenfelder und Konstanten. Einige Klassen kennen darüber hinaus Ereignisse, die beispielsweise dann ausgelöst werden, wenn sich Daten auf eine bestimmte Weise ändern.
Definition von Klassen Auf der Codeebene wird die Definition einer Klasse durch Class name eingeleitet und durch End Class abgeschlossen. Innerhalb dieses Blocks werden die Elemente der Klasse definiert – also Variablen (alias Datenfelder), Prozeduren (alias Methoden), Eigenschaften etc. Class Class1 Private x As Integer Public y As Integer Private Sub p() ... Sub m() ... Property e() As Integer ... End Class
'eine 'eine 'eine 'eine 'eine
interne Klassenvariable öffentliche Klassenvariable interne Prozedur von außen zugängliche Methode von außen zugängliche Eigenschaft
VERWEIS
Entscheidend beim Entwurf der Klasse ist die Überlegung, welche Elemente der Klasse nur für die intere Programmierung innerhalb der Klasse gedacht sind und welche Elemente extern zur Verfügung stehen sollen. Dabei müssen Sie auf die korrekte Deklaration achten. Beispielsweise gelten Variablen per Default als Private und können nur intern verwendet werden, während Prozeduren (Methoden) und Eigenschaften per Default als Public gelten und von außen hin verwendet werden können. Die möglichen Gültigkeitsebenen von Variablen, Prozeduren etc. und die Schlüsselwörter zur entsprechenden Deklaration (Private, Friend, Protected, Public) werden in Abschnitt 7.9 ausführlich beschrieben.
Anwendung von Klassen (Objekte) Wenn Sie die Definition einer Klasse abgeschlossen haben, können Sie die Klasse anwenden. Im Regelfall erzeugen Sie dazu ein Objekt dieser Klasse: Dim o As New Class1() o.y = 3 o.e = 4 o.m()
'Objekt der Klasse Class1 erzeugen 'öffentliche Klassenvariable nutzen 'Eigenschaft zuweisen 'Methode aufrufen
VERWEIS
7.2 Klassen, Module, Strukturen
219
Die Nutzung von Klassen – d.h. der Umgang mit Objekten – wird ausführlich in den Kapiteln 4 und 6 zur Variablenverwaltung und zur Anwendung von Klassenbibliotheken und Objekten beschrieben.
Verschachtelung von Klassen Klassendefinitionen können ineinander verschachtelt werden (siehe die folgende Schablone). Das bewirkt, dass Sie Class2 innerhalb von Class1 unmittelbar verwenden können. In anderen Klassen können Sie Class2 dagegen nur unter dem vollständigen Namen Class1.Class2 ansprechen. (Unter diesem Namen scheint Class2 auch im Objektkatalog auf.) Class Class1 ' Class2 nutzen Dim c As New Class2() ' ... weiterer Code für Class1 Class Class2 ' ... weiterer Code für Class2 End Class End Class ' Class1.Class2 nutzen Class Class3 Dim c As New Class1.Class2() ' ... weiterer Code für Class3 End Class
LinkedList-Beispiel
HINWEIS
Damit dieses Kapitel nicht vollkommen abstrakt bleibt, wird zur Beschreibung der Elemente einer Klasse (Methoden, Eigenschaften etc.) ein durchgängiges Beispiel verwendet. Ziel der Klasse LinkedList ist es, eine Liste von Zeichenketten so zu verwalten, dass an einer beliebigen Stelle innerhalb der Liste Elemente eingefügt und wieder entfernt werden können. Intern verweist jedes Objekt auf das nächste bzw. vorangehende Objekt. Um die Verwaltung der Liste zu vereinfachen, wird die Klasse mit Methoden wie Insert oder Remove ausgestattet. Beachten Sie bitte, dass dieses Beispiel in erster Linie didaktischer Natur ist. Wenn es Ihnen darum geht, eine Liste von Zeichenketten effizient zu verwalten, sollten Sie nicht diese Beispielklasse, sondern die viel leistungsfähigere Klasse Collections.ArrayList verwenden (siehe Kapitel 9)!
220
7 Objektorientierte Programmierung
7.2.2
Klassenvariablen und -konstanten (fields)
Wenn Sie innerhalb einer Klasse Variablen oder Konstanten öffentlich deklarieren (mit Public), können deren Werte direkt gelesen und verändert werden. Nach außen hin wirken solche Variablen wie Eigenschaften, aber im Objektbrowser wird unmissverständlich klar, dass es sich um Klassenvariablen bzw. -konstanten handelt. (Der Unterschied zu richtigen Eigenschaften wird in Abschnitt 7.2.4 beschrieben.) Natürlich können Sie Variablen bzw. Konstanten auch mit Private deklarieren – dann können Sie auf die Variablen nur im Code innerhalb der Klasse (also z.B. in einer Methode) zugreifen. Class Class1 Public data As Integer Private internaldata As String ... End Class
Syntaktisch ist es auch erlaubt, eine Variable als ReadOnly zu deklarieren (z.B. Public ReadOnly abc As Integer = 3). Praktisch ist das aber selten sinnvoll – Sie können derartige Variablen weder innerhalb noch außerhalb der Klasse verändern (außer durch die Zuweisung im Rahmen der Deklaration). Daher ist es klarer, derartige Variablen gleich als Konstante zu deklarieren.
LinkedList-Beispiel Die Grundidee des LinkedList-Beispiels, das in den folgenden Abschnitten schrittweise erweitert wird, ist einfach: Jedes Element einer derartigen Liste wird durch ein eigenes LinkedList-Objekt dargestellt. In der Minimalvariante lässt sich eine derartige Datenstruktur durch eine Klasse darstellen, die aus nur drei Klassenvariablen besteht: Value enthält die zu speichernde Zeichenkette. Next- und PreviousItem verweisen entweder auf nachfolgende bzw. vorausgehende LinkedList-Elemente, sie enthalten Nothing, wenn das Objekt am Anfang bzw. Ende der Liste steht.
HINWEIS
' Beispiel oo-programmierung\linkedlist1 Class LinkedList Public NextItem As LinkedList Public PreviousItem As LinkedList Public Value As String End Class
Grundsätzlich wäre es auch möglich, die gesamte Liste nicht durch viele, sondern durch ein einziges LinkedList-Objekt darzustellen. Die Verwaltung der Elemente würde dann innerhalb der Liste erfolgen. Das ist mit diversen Vor- und Nachteilen verbunden. Der Hauptvorteil der hier gewählten Variante besteht darin, dass das Konzept einfach verständlich ist und sich didaktisch gut darstellen lässt. Die Effizienz steht hier nicht an erster Stelle.
7.2 Klassen, Module, Strukturen
221
Anwendung der LinkedList-Klasse So einfach die Definition der Klasse ist, so umständlich ist deren Anwendung. Die folgenden Zeilen zeigen, wie vier LinkedList-Objekte erzeugt und initialisiert werden, so dass in jedem Element ein Wort eines kurzen Satzes gespeichert ist. Durch die Schleife am Ende von Main wird die gesamte Liste im Konsolenfenster ausgegeben. Dort können Sie den Text "Das ist ein Satz." lesen. Abbildung 7.2 zeigt die interne Darstellung der Liste. Sub Main() ' einen kurzen Satz in Form von ' LinkedList-Elementen formulieren Dim o1 As New LinkedList() Dim o2 As New LinkedList() Dim o3 As New LinkedList() Dim o4 As New LinkedList() o1.Value = "Das" o1.NextItem = o2 o2.Value = "ist" o2.PreviousItem = o1 o2.NextItem = o3 o3.Value = "ein" o3.PreviousItem = o2 o3.NextItem = o4 o4.Value = "Satz." o4.PreviousItem = o3 ' Satz ausgeben Dim item As LinkedList = o1 While Not item Is Nothing Console.Write("{0} ", item.Value) item = item.NextItem End While End Sub
Nothing
Value=“Das“
Value=“ist“
Value=“ein“
NextItem
NextItem
NextItem
NextItem
PreviousItem
PreviousItem
PreviousItem
PreviousItem
Abbildung 7.2: Vier miteinander verknüpfte LinkedList-Objekte
Value=“Satz.“ Nothing
222
7 Objektorientierte Programmierung
7.2.3
Methoden
Aus Anwendersicht dienen Methoden dazu, bestimmte Operationen mit einem Objekt durchzuführen. Aus der Sicht der Programmiererin einer Klasse ist eine Methode aber einfach eine Prozedur bzw. Funktion innerhalb einer Klasse, die öffentlich zugänglich ist. Wenn die Methode als Funktion formuliert wird, liefert sie einen Rückgabewert, sonst nicht. (In einer Klasse können selbstverständlich auch Prozeduren enthalten sein, die nur intern verwendet werden und nach außen hin unzugänglich sind.)
VERWEIS
Class Class1 ' Methode xy mit Rückgabewert Public Function xy(ByVal n As Integer) As String ... End Function ' Methode z ohne Rückgabewert Public Sub z(ByVal n As Integer) ... End Sub End Class
Ein Thema für sich ist die sinnvolle Namensgebung (natürlich nicht nur bei Methoden, sondern auch bei Klassen, Variablen, Eigenschaften etc.) In der Online-Dokumentation finden Sie eine recht hilfreiche Sammlung von Regeln, denen gemäß Sie eigene Klassen, Eigenschaften, Methoden etc. benennen sollten. Suchen Sie nach den Richtlinien für die Benennung: ms-help://MS.VSCC/MS.MSDNVS.1031/cpgenref/html/cpconnamingguidelines.htm
Me-Schlüsselwort Mit dem Schlüsselwort Me können Sie innerhalb des Klassencodes auf die aktuelle Instanz der Klasse zugreifen. Wenn die Anwenderin der Klasse also obj.methode() ausführt, dann verweist Me innerhalb von Public Sub methode() auf obj. Allzuoft werden Sie Me nicht brauchen, weil der Zugriff auf Objektvariablen, -methoden, -eigenschaften etc. innerhalb des Klassencodes ohnedies problemlos möglich ist. Me.variable und variable sind also gleichwertig. Me ist aber dann wichtig, wenn Sie eine Instanz des Objektes als Ergebnis zurückgeben oder als Parameter an eine andere Methode übergeben möchten.
New-Methode (Konstruktur) Eine Sonderrolle nehmen Methoden mit dem Namen New ein. Diese Methode ist nicht zum Aufruf in der Form obj.New(...) gedacht, sondern zur Erzeugung und Initialisierung einer neuen Objektinstanz der Klasse in der Form Dim obj As New klasse(...) bzw. obj = New klasse(...).
7.2 Klassen, Module, Strukturen
223
HINWEIS
Es ist nicht zwingend erforderlich, eine eigene Klasse mit New auszustatten. Auch wenn Sie sich dagegen entscheiden, können Sie neue Objekte durch Dim obj As New klasse() erzeugen. Allerdings ist es dann nicht möglich, eine Initialisierung durchzuführen. Wenn Sie eine eigene New-Methode mit Parametern zur Initialisierung angeben, steht der Defaultkonstruktor ohne Parameter nicht mehr zur Verfügung und Sie müssen auch diese Methode selbst programmieren (gegebenenfalls einfach durch die beiden Zeilen Public Sub New() und End Sub). Beachten Sie, dass es in VB.NET zulässig ist, mehrere Methoden mit dem gleichen Namen zu definieren, wenn sie sich durch ihre Parameterliste eindeutig unterscheiden. (Das gilt auch für gewöhnliche Prozeduren – siehe Abschnitt 5.3.4.)
Finalize-Methode (Destruktor) Das Gegenstück zu New ist die Methode Finalize. Diese Methode wird im Rahmen der garbage collection automatisch ausgeführt, wenn das Objekt aus dem Objektspeicherraum (heap) entfernt wird.
VERWEIS
Da für die Klasse Object bereits eine Default-Finalize-Methode vorgesehen ist und alle eigenen Klassen automatisch von Object abgeleitet sind, ist es im Regelfall nicht erforderlich, dass Sie eine eigene Finalize-Methode angeben! Eine eigene Finalize-Methode ist nur dann notwendig, wenn beim Entfernen eines Objekts aus dem Speicher auch Datenbankverbindungen, offenen Dateien etc. geschlossen werden müssen. In solchen Fällen sollten Sie für die Klasse auch die IDisposable-Schnittstelle implementieren. Hintergründe zur .NET-Verwaltung des Objektspeicherplatzes sind in Abschnitt 4.6 beschrieben. Beachten Sie, dass Sie Finalize nicht selbst aufrufen dürfen, sondern den Aufruf von Finalize der garbage collection überlassen müssen. Beachten Sie auch, dass Sie keinen Einfluss auf die Reihenfolge haben, in der nicht mehr benötigte Objekte aus dem Speicher entfernt werden. Wenn Sie das Entfernen von Objekten aus dem Speicher selbst in die Hand nehmen möchten, müssen Sie für die Klasse die IDisposable-Schnittstelle implementieren und für das Objekt die Methode Dispose ausführen. Die Vorgehensweise ist in Abschnitt 7.5.2 beschrieben.
Sie müssen Finalize mit den Schlüsselwörtern Protected Overrides deklarieren. Des weiteren müssen Sie innerhalb des Finalize-Codes MyBase.Finalize() aufrufen. Eine minimale Schablone für eine eigene Finalize-Methode sieht damit so aus. Protected Overrides Sub Finalize() ... eigener Code MyBase.Finalize() 'Finalize der zugrunde liegenden Klasse (Object) End Sub
VERWEIS
224
7 Objektorientierte Programmierung
Protected bedeutet, dass die Prozedur außerhalb des Klassencodes nicht aufgerufen werden darf. Overrides bedeutet, dass die Methode die Finalize-Methode der Basisklasse Object überschreibt. MyBase ermöglicht es, innerhalb einer Klasse auf gleich-
namige Schlüsselwörter einer Basisklasse zuzugreifen. Alle drei Schlüsselwörter werden in Abschnitt 7.4 noch näher vorgestellt. (Dort geht es um das Thema Vererbung.)
Finalize-Beispiel Im folgenden Beispiel werden 1000 Objekte einer einfachen Klasse erzeugt. Durch jede Zuweisung o = New Class1(...) wird die zuletzt in der Variablen o gespeicherte Objektinstanz ungültig. Damit kann das Objekt jederzeit durch eine garbage collection gelöscht werden. Es ist allerdings nicht vorherbestimmbar, wann die nächste garbage collection tatsächlich beginnt und in welcher Reihenfolge die Objekte aus dem Speicher entfernt werden. Um das zu ergründen, wird in der Finalize-Prozedur die Objektnummer im Konsolenfenster ausgegeben. Die Ausgabe des Programms sieht dann aus wie in Abbildung 7.3.
Abbildung 7.3: Ausgaben des Finalize-Beispielprogramms
Der Code des Programms ist einfach zu verstehen. Die neuen Objekte werden mit New erzeugt, wobei durch StrDup eine unterschiedlich lange Zeichenkette und mit i ein durchlaufender Zähler übergeben wird. (Die Zeichenkette hat nur den Sinn, den Speicherver-
7.2 Klassen, Module, Strukturen
225
brauch der Objekte künstlich zu vergrößern, um so .NET hin und wieder zu einer garbage collection zu motivieren.) Threading.Thread.Sleep(1000) bewirkt, dass das Programm eine Sekunde lang nichts tut. Während dieser Zeit kommt es mit großer Wahrscheinlichkeit zu einer weiteren garbage collection. Module Module1 Sub Main() Dim i As Integer Dim o As Class1 For i = 1 To 1000 o = New Class1(StrDup(i, "x"), i) Console.WriteLine("Created object {0}. ", i) Next Threading.Thread.Sleep(1000) Console.WriteLine("Return drücken") Console.ReadLine() Threading.Thread.Sleep(100) End Sub End Module Class Class1 Public data As String Public counter As Integer Public Sub New(ByVal s As String, ByVal i As Integer) data = s counter = i End Sub Protected Overrides Sub Finalize() Console.Write("Finalize counter={0}. ", counter) MyBase.Finalize() End Sub End Class
LinkedList-Beispiel In seiner zweiten Version wird das LinkedList-Beispiel schon wesentlich interessanter. Eine Reihe von Methoden machen sowohl das Initialisieren neuer Objekte als auch die Verwaltung von Listen deutlich einfacher. Die Grundidee besteht darin, die beiden Variablen nextItem und previousItem mit Private zu deklarieren und somit eine direkte (und fehleranfällige) Manipulation dieser beiden Zeiger auf nachfolgende bzw. vorausgehende Objekte zu unterbinden. Dafür helfen nun verschieden Methoden, weitere LinkedList-Objekte an ein bereits vorhandenes Objekt anzuhängen bzw. davon wieder zu entfernen. Ein wesentlicher Vorteil dieser Vorgehensweise besteht darin, dass es mit den neuen Methoden unmöglich ist, mehrere LinkedList-Objekte zirkulär zu verbinden. Durch eine direkte Veränderung von next- und previousItem wäre das dagegen sehr einfach möglich.
226
7 Objektorientierte Programmierung
Derartige Kreisverweise sind in der Praxis aber fast immer unerwünscht und würden eine Menge Zusatzcode erfordern, um mögliche Endlosschleifen bei der Auswertung der Listen auszuschließen. Der Konstruktor New ermöglicht es, ein neues LinkedList-Objekt zu erzeugen, wobei optional eine Zeichenkette zur Initialisierung des Objekts übergeben werden kann. Wenn der Konstruktor ohne Parameter aufgerufen wird, wird innerhalb der Klasse Me.New("") ausgeführt, d.h. eine leere Zeichenkette an die zweite New-Variante übergeben. Beachten Sie insbesondere den Einsatz des Schlüsselworts Me, das auf die aktuelle Instanz eines Objekts verweist und den Aufruf der Methode New innerhalb des Klassencodes ermöglicht. ' Beispiel oo-programmierung\linkedlist2 Class LinkedList Private nextItem As LinkedList Private previousItem As LinkedList Public Value As String ' Konstrukturen Public Sub New() Me.New("") End Sub Public Sub New(ByVal s As String) nextItem = Nothing previousItem = Nothing Value = s End Sub ... weitere Methoden End Class
Die beiden Methoden AddAfter und AddBefore erzeugen ein neues LinkedList-Objekt und fügen es vor bzw. nach dem aktuellen Objekt in die Liste ein. Als Parameter muss der gewünschte Inhalt des Objekts angegeben werden. Die Methoden kümmern sich um die korrekte Einstellung der previous- und nextItem-Eigenschaften, und zwar sowohl für das neu eingefügte Objekt als auch für die bereits vorhandene Liste. Am schwersten zu verstehen ist wahrscheinlich der dreizeilige If-Block, der in beiden Methoden vorkommt. Bei AddAfter wird durch die Abfrage getestet, ob es nach Me ein weiteres Listenelement gibt. Wenn das der Fall ist, verweist dieses momentan zurück auf Me. Durch das Einfügen des neuen Elements newitem muss es aber künftig zurück auf newitem verweisen. Genau das bewirkt nextItem.previousItem = newitem. (In AddBefore kümmert sich der If-Block analog um die Vorwärtsverweise des vorangehenden Objekts.)
7.2 Klassen, Module, Strukturen
227
' neues Element hinter dem vorhandenen Element einfügen Public Function AddAfter(ByVal s As String) As LinkedList Dim newitem As New LinkedList(s) newitem.previousItem = Me newitem.nextItem = nextItem If Not IsNothing(nextItem) Then nextItem.previousItem = newitem End If nextItem = newitem Return newitem End Function ' neues Element vor dem vorhandenen Element einfügen Public Function AddBefore(ByVal s As String) As LinkedList Dim newitem As New LinkedList(s) newitem.nextItem = Me newitem.previousItem = previousItem If Not IsNothing(previousItem) Then previousItem.nextItem = newitem End If previousItem = newitem Return newitem End Function
Ganz ähnlich sieht die Logik von Remove aus, um ein Element aus der Liste zu entfernen: Hier geht es zuerst darum, die Variable nextItem des vorangehenden Listenelements bzw. die Variable previousItem des nachfolgenden Listenelements so zu korrigieren, dass diese beiden Listenelemente nun direkt aufeinander verweisen (und nicht mehr auf das bisher dazwischenliegende Objekt, das durch Remove aus der Liste entfernt werden soll). Die IfTests sind notwendig, weil das zu löschende Objekt ja auch am Ende der Liste stehen bzw. ein isoliertes LinkedList-Element sein kann. Remove endet damit, dass die Variablen nextItem und previousItem gelöscht werden. Damit bleibt ein isoliertes LinkedList-Objekt übrig (wobei sein Inhalt – also .Value – noch immer
vorhanden ist). Sofern es im Programm keinen Verweis mehr auf das Objekt gibt, wird es nach einer Weile durch die automatische garbage collection aus dem Speicher entfernt. ' Element aus Liste entfernen Public Sub Remove() ' nextItem-Link des vorigen Eintrags richtig stellen If Not IsNothing(previousItem) Then If IsNothing(nextItem) Then previousItem.nextItem = Nothing Else previousItem.nextItem = nextItem End If End If
228
7 Objektorientierte Programmierung
' previousItem-Link des nächsten Eintrags richtig stellen If Not IsNothing(nextItem) Then If IsNothing(previousItem) Then nextItem.previousItem = Nothing Else nextItem.previousItem = previousItem End If End If ' Verweise des Elements löschen nextItem = Nothing previousItem = Nothing End Sub
Da die Variablen nextItem und previousItem nun als Private deklariert sind, gibt es keine Möglichkeit mehr, auf die nachfolgenden bzw. vorangehendenen Listenelemente zuzugreifen. Die beiden Methoden GetNext bzw. GetPrevious beheben diesen Mangel. (Wie der nächste Abschnitt zeigen wird, könnte dieselbe Funktion auch durch zwei Read-OnlyEigenschaften Next und Previous erreicht werden. Generell gilt, dass in manchen Fällen die Entscheidung zwischen einer Methode und einer Eigenschaft eine reine Geschmacksfrage ist.) ' nächstes/voriges Element der Liste ermitteln Public Function GetNext() As LinkedList Return nextItem End Function Public Function GetPrevious() As LinkedList Return previousItem End Function
Von anderen Klassen sind Sie es gewohnt, dass Sie mit obj.ToString() den Inhalt des Objekts in Textform ermitteln können. Für ein LinkedList-Objekt funktioniert ToString automatisch, weil die Klasse (wie alle Klassen) automatisch von der Klasse Object abgeleitet ist. Allerdings liefert die Defaultimplementierung von ToString nur den Klassennamen. Damit ToString auch für LinkedList das erwartete Resultat liefert, muss die Defaultimplementierung überschrieben werden. Genau das bewirkt das Schlüsselwort Overrides. Dank ToString können Sie bei der Anwendung der Klasse nun Console.Write(llobj) schreiben. Die Write-Methode wertet automatisch ToString aus und zeigt die Zeichenkette des Objekts im Konsolenfenster an. Public Overrides Function ToString() As String Return Value End Function
Für Testzwecke ist es praktisch, wenn nicht nur ein einzelnes Element, sondern gleich eine ganze Liste von LinkedList-Objekten ausgegeben werden kann. Genau dabei hilft die Methode ItemsText. Per Default liefert die Methode die Zeichenkette des aktuellen Objekts sowie maximal neun nachfolgender Objekte, wobei die Zeichenketten durch ein Leer-
7.2 Klassen, Module, Strukturen
229
zeichen voneinander getrennt werden. Durch die beiden optionalen Parameter max und delimitor können Sie die Anzahl der Zeichenketten und das Trennzeichen steuern. Public Function ItemsText(Optional ByVal max As Integer = 10, _ Optional ByVal delimitor As String = " ") As String Dim i As Integer Dim tmp As String Dim item As LinkedList = Me While (Not IsNothing(item)) And (i < max) tmp += item.Value + delimitor item = item.GetNext() i += 1 End While Return tmp End Function
Anwendung der LinkedList-Klasse Mit diesem schon recht reichen Satz an Methoden lässt sich schon ganz gut experimentieren. In den drei ersten Zeilen von Main wird dieselbe LinkedList-Kette wie im vorigen Abschnitt zusammengesetzt. Beachten Sie, dass AddAfter jeweils ein LinkedList-Element zurückgibt, auf das dann die nächste AddAfter-Methode angewendet wird. Die zweite Codezeile ist daher eine Kurzfassung der folgenden Zeilen: ' Beispiel oo-programmierung\linkedlist2 Dim a, b, c As LinkedList a = ll1.AddAfter("ist") b = a.AddAfter("ein") c = b.AddAfter("Satz.")
Statt mit AddAfter kann dieselbe LinkedList-Kette natürlich auch mit AddBefore zusammengesetzt werden. Diese Rückwärtsformulierung ist aber weniger inituitiv. Die verbleibenden Zeilen zeigen die Anwendung von GetNext und Remove. Zuerst wird nach dem Wort ist ein zusätzliches Listenelement mit dem Text neuer eingefügt (woraus sich der Satz "Das ist ein neuer Satz" ergibt). Das neue Listenelement wird anschließend wieder entfernt. Sub Main() ' einen kurzen Satz in Form von ' LinkedList-Elementen formulieren ' liefert 'Das ist ein Satz.' Dim ll1 As New LinkedList("Das") ll1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") Console.WriteLine(ll1.ItemsText())
230
7 Objektorientierte Programmierung
' liefert ebenfalls 'Das ist ein Satz.' ' ll1 --> Satz ' ll2 --> Das Dim ll2 As LinkedList ll1 = New LinkedList("Satz.") ll2 = ll1.AddBefore("ein").AddBefore("ist").AddBefore("Das") Console.WriteLine(ll2.ItemsText()) ' ll3 --> neuer ' liefert 'Das ist ein neuer Satz.' Dim ll3 As LinkedList ll3 = ll2.GetNext().GetNext().AddAfter("neuer") Console.WriteLine(ll2.ItemsText()) ' liefert wieder 'Das ist ein Satz.' ll3.Remove() Console.WriteLine(ll2.ItemsText()) End Sub
7.2.4
Eigenschaften
Eigenschaften sehen nach außen hin wie Klassenvariablen aus. Intern handelt es sich aber um ein Paar von Prozeduren. Diese Prozeduren werden beim Lesen oder Verändern der Eigenschaft ausgewertet. Die Syntax von Eigenschaften lässt sich am einfachsten anhand eines Beispiels beschreiben. Die Eigenschaft wird mit Property name As klasse eingeleitet. Wenn Sie jetzt in der Entwicklungsumgebung Return drücken, fügt sie automatisch die Codeschablone für die Get- und Set-Teile ein. Der Get-Teil der Eigenschaft ist für das Auslesen der Eigenschaft zuständig (also beispielsweise x = obj.prop). Dieser Teil muss mit einer Return-Anweisung enden, die den Wert der Eigenschaft zurückgibt. Der Set-Teil wird bei einer Veränderung der Eigenschaft ausgeführt (beispielsweise obj.prop = "abc"). An die Set-Prozedur wird der Parameter Value übergeben, der denselben Datentyp wie die gesamte Eigenschaft hat und den zuzuweisenden Wert enthält. Die Get- und SetBlöcke können jeweils vorzeitig durch Exit Property verlassen werden. Die folgenden Beispielzeilen zeigen, wie die nach außen hin unzugängliche Klassenvariable privatevar durch die Prozedur prop gelesen und verändert werden kann. Class Class1 Private privatevar As String Public Property prop() As String Get ... Return privatevar End Get
7.2 Klassen, Module, Strukturen
231
Set(ByVal Value As String) ... privatevar = Value End Set End Property End Class
Eigenschaften versus Klassenvariablen Statt der obigen Eigenschaft prop und der privaten Klassenvariable privatevar hätten Sie einfach eine öffentliche Variable prop deklarieren können (Public prop As String). Die Anwenderin der Klasse hätte keinen Unterschied gemerkt, aber Sie hätten etwas Zeit für die Programmierung der Eigenschaft gespart und wären zudem mit effizienterem Code belohnt worden. Wozu also Eigenschaften? •
Der wichtigste Vorteil einer Eigenschaft besteht darin, dass bei jedem Zugriff und bei jeder Veränderung Code ausgeführt wird. Damit haben Sie als Programmiererin der Klasse volle Kontrolle über jeden Zugriff. Das können Sie dazu ausnützen, Eigenschaften erst beim Lesen dynamisch zu errechnen, um bei jeder Veränderung eine Validätskontrolle durchzuführen etc.
•
Auf Eigenschaften basierende Klassen sind im Regelfall einfacher durch Vererbung zu erweitern.
Read-Only-Eigenschaften Eine Sonderform von Eigenschaften sind solche Eigenschaften, die nur gelesen, aber nicht verändert werden können. Bei der Deklaration werden solche Eigenschaften mit dem zusätzlichen Schlüsselwort ReadOnly gekennzeichnet. Der Set-Teil entfällt. Class Class1 Public ReadOnly Property prop() As String Get Return ... End Get End Property End Class
Analog zu ReadOnly existiert auch das Schlüsselwort WriteOnly, um Eigenschaften zu deklarieren, die nur verändert, aber nicht gelesen werden können. In diesem Fall entfällt der Get-Teil. WriteOnly-Eigenschaften sind aber sehr unüblich. Es ist leider nicht möglich, dieselbe Eigenschaft als Read- und WriteOnly-Eigenschaft mit unterschiedlichen Gültigkeitsebenen zu deklarieren (z.B. Public ReadOnly und Private WriteOnly).
232
7 Objektorientierte Programmierung
Eigenschaften mit Parametern Eigenschaften können ebenso wie Methoden mit Parametern ausgestattet werden (obwohl das in der Praxis eher selten vorkommt). Die folgenden Zeilen zeigen die grundsätzliche Syntax: Class Class1 Public Property para(ByVal n As Integer) As String Get Return ... End Get Set(ByVal Value As String) ... End Set End Property End Class
Defaulteigenschaften VB.NET bietet die Möglichkeit, eine Eigenschaft durch das Schlüsselwort Default als Defaulteigenschaft zu kennzeichnen. Es muss sich dabei um eine Eigenschaft mit Parametern handeln. Der Vorteil einer Defaulteigenschaft besteht darin, dass diese Eigenschaft nicht genannt werden muss. Statt obj.def(3) können Sie einfach obj(3) schreiben, was in manchen Fällen intuitiver ist. Beispielsweise kann auf eigene Aufzählungen ähnlich wie auf die Elemente eines Feldes zugegriffen werden. Class Class1 Public Default Property def(ByVal n As Integer) As String ... Get/Set wie bisher End Property End Class
LinkedList-Beispiel Die LinkedList-Klasse wird in der dritten Version dieses Beispiels um eine Reihe zusätzlicher Eigenschaften erweitert. Die Klassenvariablen und Methoden bleiben im Vergleich zur vorigen Version unverändert. Mit der Eigenschaft Count kann die Gesamtzahl der Elemente einer LinkedList-Kette ermittelt werden. Count funktioniert unabhängig vom Startpunkt, d.h., es werden alle vorangehenden und nachfolgenden Elemente durchlaufen, bis Nothing erreicht wird. (Bei sehr langen Ketten ist Count eine ineffiziente Eigenschaft! Eine effizientere Realisierung wäre nur möglich, wenn alle Listenelemente in einem einzigen Objekt verwaltet würden, d.h. nur bei einem vollkommen anderen Design der LinkedList-Klasse.) Count ist eine ReadOnly-Eigenschaft, d.h., sie kann nur gelesen, aber nicht verändert werden.
7.2 Klassen, Module, Strukturen
233
' Beispiel oo-programmierung\linkedlist3 Class LinkedList Private nextItem As LinkedList 'wie in Beispiel linkedlist2 Private previousItem As LinkedList Public Value As String [... methoden ...] 'wie in Beispiel linkedlist2 Public ReadOnly Property Count() As Integer Get Dim n As Integer = 1 Dim ll As LinkedList = Me.previousItem While Not IsNothing(ll) n += 1 ll = ll.previousItem End While ll = Me.nextItem While Not IsNothing(ll) n += 1 ll = ll.nextItem End While Return n End Get End Property ... weitere neue Eigenschaften End Class
Die Eigenschaften Next und Previous haben dieselbe Funktion wie die im vorigen Abschnitt vorgestellten Methoden GetNext bzw. GetPrevious: Sie liefern das nächste bzw. vorangehende Element der LinkedList-Kette (oder Nothing, wenn es keine weiteren Elemente mehr gibt). Next muss in eckige Klammern gestellt werden, weil es mit dem VB.NET-Schlüsselwort Next übereinstimmt. ' Zugriff auf das folgende bzw. vorhergehende Element Public ReadOnly Property [Next]() As LinkedList Get Return nextItem End Get End Property Public ReadOnly Property Previous() As LinkedList Get Return previousItem End Get End Property
234
7 Objektorientierte Programmierung
HINWEIS
Next und GetNext() bzw. Previous und GetPrevious() erfüllen dieselbe Aufgabe. Ist es
nun sinnvoller, diese Aufgabe durch eine Methode oder durch eine Eigenschaft zu realisieren? Eine schlüssige Antwort auf diese Frage gibt es leider nicht. Sehr oft kann eine bestimmte Funktion sowohl durch eine Eigenschaft als auch durch eine Methode erreicht werden. Für welche Variante Sie sich beim Design der Klasse entscheiden, ist eher eine Geschmacksfrage.
Der Code für die Eigenschaft Item ist schon etwas komplexer. Diese Eigenschaft ermöglicht den Zugriff auf das n-te nachfolgende bzw. vorausgehende Element in der Form obj.Item(3) oder obj.Item(-1). Da Item als Defaulteigenschaft deklariert ist, sind auch die Kurzschreibweisen obj(3) bzw. obj(-1) zulässig. Der Get-Teil zum Lesen eines Objekts ist einfach: Für n=0 liefert die Eigenschaft einfach Me zurück. Wenn n positiv ist, wird die Kette n Mal nach vorne verfolgt. Wenn dabei das Ende der Kette erreicht wird, liefert die Eigenschaft (ohne Fehlermeldung) Nothing zurück, sonst das gefundene Element. Analog wird bei negativem n das entsprechende vorausgehende Element ermittelt. Der Set-Teil zum Verändern eines Objekts wird aufgerufen, wenn Sie ein Element einer Kette verändern möchten: obj(3) = newobj. Die Prozedur wurde so implementiert, dass das newobj von seiner bisherigen Position in die neue Position verschoben wird. (Grundsätzlich wäre es auch denkbar gewesen, die Item-Eigenschaft so zu realisieren, dass der Inhalt des Objekts kopiert wird. Das wäre einfacher gewesen: Nach den einleitenden Codezeilen hätte die Anweisung olditem.Value = newitem.Value ausgereicht.)
VORSICHT
Die Set-Prozedur beginnt mit einem Test, ob das zu ändernde Objekt überhaupt existiert. Wenn das nicht der Fall ist, wird ein IndexOutOfRangeException-Fehler ausgelöst. Andernfalls wird das einzufügende Objekt durch newitem.Remove aus seiner bisherigen Position herausgelöst und durch die Veränderung seiner next- und previousItem-Variablen in die neue Position eingefügt. Soweit es an der neuen Position vorausgehende bzw. nachfolgende Elemente gibt, werden auch deren previous- und nextItem-Variablen richtig gestellt. Schließlich wird das Element, das sich bisher an der Position des neuen Elements befand, durch die Zuweisung von Nothing an previous- und nextItem zu einem isolierten LinkedListObjekt gemacht. (Wenn es keine Verweise mehr auf das Objekt gibt, wird es bei der nächsten Gelegenheit durch die garbage collection aus dem Speicher entfernt.) Wenn Sie obj(0) = newobj ausführen, verweist obj danach nicht mehr auf den Beginn der Kette, sondern auf das ehemalige Element obj(0), das jetzt isoliert ist. Auf den Beginn der Kette müssen Sie jetzt mit newobj zugreifen.
Default Public Property Item(ByVal n As Integer) As LinkedList Get Dim i As Integer Dim ll As LinkedList = Me
7.2 Klassen, Module, Strukturen
If n = 0 Then Return Me If n > 0 Then For i = 1 To n ll = ll.nextItem If ll Is Nothing Then Return Nothing Next Return ll Else For i = 1 To -n ll = ll.previousItem If ll Is Nothing Then Return Nothing Next Return ll End If End Get Set(ByVal Value As LinkedList) Dim olditem, newitem As LinkedList newitem = Value olditem = Me(n) ' es ist nicht möglich, ein gar nicht existierendes ' Objekt zu ändern If IsNothing(olditem) Then Throw New IndexOutOfRangeException() Exit Property End If ' olditem aus seiner bisherigen Position herauslösen newitem.Remove() ' Verweise des neuen Elements auf seine Nachbarn einrichten newitem.nextItem = olditem.nextItem newitem.previousItem = olditem.previousItem ' Verweise auf das neue Element einrichten If Not IsNothing(olditem.previousItem) Then olditem.previousItem.nextItem = newitem End If If Not IsNothing(olditem.nextItem) Then olditem.nextItem.previousItem = newitem End If ' Verweise des alten Elements entfernen olditem.nextItem = Nothing olditem.previousItem = Nothing End Set End Property
235
236
7 Objektorientierte Programmierung
Anwendung der LinkedList-Klasse Die folgenden Zeilen zeigen die Anwendung der neuen Eigenschaften. test1 ist der Ausgangspunkt für den schon aus früheren Beispielen bekannten Mustersatz, dessen Elemente gezählt, gelesen und verändert werden. test2 zeigt auf den Beginn einer Kette mit den Elementen "0", "1" etc. Durch die Zuweisung test2(3)=test2(7) wird das vierte Element der Kette durch das achte ersetzt. Die Kette wird dadurch um ein Element kürzer. ' Beispiel oo-programmierung\linkedlist3 Sub Main() Dim test1 As New LinkedList("Das") test1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") Console.WriteLine(test1.ItemsText()) '--> 'Das ist ein Satz.' Console.WriteLine(test1.Count) '--> 4 Console.WriteLine(test1.Next) '--> 'ist' Console.WriteLine(test1(2)) '--> 'ein' test1(2) = New LinkedList("EIN NEUER") Console.WriteLine(test1.ItemsText()) '--> 'Das ist EIN NEUER Satz.' Dim test2 As New LinkedList("0") Dim ll As LinkedList = test2 Dim i As Integer For i = 1 To 10 ll = ll.AddAfter(i.ToString) Next Console.WriteLine(test2.ItemsText(20)) '--> '0 1 2 3 4 5 6 7 8 9 10' test2(3) = test2(7) Console.WriteLine(test2.ItemsText(20)) '--> '0 1 2 7 4 5 6 8 9 10' End Sub
7.2.5
Shared-Klassenmitglieder
Klassenvariablen, Methoden und Eigenschaften können durch das zusätzliche Schlüsselwort Shared gekennzeichnet werden. Das bewirkt, dass diese Klassenmitglieder ohne eine Instanz der Klasse – also ohne dass vorher ein Objekt erzeugt wird – verwendet werden können (siehe auch Abschnitt 6.2.4). Im Regelfall wird Shared nur für öffentliche Klassenmitglieder verwendet (Public Shared). Die Deklaration privater Klassenmitglieder als Shared ist zwar zulässig, aber nur in seltenen Fällen sinnvoll.
HINWEIS
7.2 Klassen, Module, Strukturen
237
Das Gegenstück zu Shared (VB.NET) lautet in C# static! Das bedeutet auch, dass Static (VB.NET) und static (C#) zwei Schlüsselwörter mit vollkommen unterschiedlicher Wirkung sind. Static (VB.NET) kann zur Deklaration von Variablen in Prozeduren verwendet werden; es bewirkt, dass die Variablen in der Prozedur ihren Wert zwischen zwei Aufrufen behalten. static (C#) hat dieselbe Wirkung wie das hier beschriebene VB-NET-Schlüsselwort Shared.
Shared-Klassenvariablen Als Public Shared deklarierte Klassenvariablen haben eine ähnliche Wirkung wie globale Variablen innerhalb eines Moduls. Jedes Programm, das die Klasse nutzen kann, kann eine derart deklarierte Variable sofort lesen bzw. verändern. Dabei ist aber Vorsicht geboten! Alle Instanzen dieser Klasse teilen sich die als Shared deklarierte Variable! (Insofern ist die Variable also eine gemeinsame Variable aller Objekte dieser Klasse.) Das kann unter Umständen ungewollte Konsequenzen haben. Im folgenden Beispielprogramm ist die Variable x als globale Variable (Shared) der Klasse Class1 deklariert. In Module1.Main werden zwei Objekte dieser Klasse erzeugt (o1 und o2). Zur Initialisierung von x und y wird der New-Konstruktor verwendet. Anschließend werden x und y für beide Objekte ausgegeben. Das Ergebnis sieht so aus: o1.x=3 o2.x=3
o1.y=2 o2.y=4
Falls Sie o1.x=1 erwartet hätten, hier die Begründung: Class1.x ist eine gemeinsame Variable aller Instanzen (Objekten) dieser Klasse. Bei der Initialisierung von o2 wird x=3 ausgeführt – und damit gilt x=3 auch für alle anderen Objekte der Klasse. ' Beispiel oo-programmierung/shared-variable Module Module1 Sub Main() Dim o1 As New Class1(1, 2) Dim o2 As New Class1(3, 4) Console.WriteLine("o1.x={0} o1.y={1}", o1.x, o1.y) Console.WriteLine("o2.x={0} o2.y={1}", o2.x, o2.y) Console.WriteLine("Return drücken") Console.ReadLine() End Sub End Module
238
7 Objektorientierte Programmierung
Class Class1 Public Shared x As Integer Public y As Integer Sub New(ByVal x As Integer, ByVal y As Integer) Me.x = x Me.y = y End Sub End Class
Shared-Methoden Bei Shared-Methoden sind weniger Komplikationen zu erwarten. Derartige Methoden werden üblicherweise eingesetzt, •
um ein neues Objekt der aktuellen Klasse auf eine andere Weise als durch den NewKonstruktor zu erzeugen,
•
um Methoden zur Verfügung zu stellen, die nicht nur auf die aktuelle Instanz eines Objekts angewendet werden können, sondern auch auf andere Objekte (eventuell sogar auf Objekte einer anderen, verwandten Klasse),
•
um eine Sammlung von Hilfsfunktionen in Form einer Klasse zu kapseln. (Die Klasse Path aus dem Namensraum System.IO gibt dafür ein gutes Beispiel. Die darin enthaltenen Methoden dienen zur Manipulation von Verzeichnis- und Dateinamen und sind durchwegs als Shared deklariert. Dieser Klasse fehlt der New-Operator, d.h., es ist nicht nur nicht sinnvoll, sondern gar nicht möglich, ein Objekt dieser Klasse zu erzeugen.)
Wenn Sie selbst Shared-Methoden programmieren, müssen Sie im Code der Methode beachten, dass Me auf Nothing verweisen kann und dass sämtliche Klassenvariablen leer bzw. nicht initialisiert sein können.
Syntaxvarianten beim Aufruf von Shared-Methoden Der größte Nachteil von Shared-Methoden besteht darin, dass diese Methoden in zwei Formen aufgerufen werden können: entweder, indem der vollständige Name der Methode angegeben wird, oder, indem eine Variable der Klasse vorangestellt wird. Unter Anwendung der unten deklarierte Struktur vector3d gibt es also zwei Wege, die Vektoren v1 und v2 zu addieren und das Ergebnis in v3 zu speichern: Dim v1, v2, v3, v4 As vector3d v3 = vector3d.Add(v1, v2) 'Variante 1 v3 = v4.Add(v1, v2) 'Variante 2, gleichwertig
Bei Variante 2 kann statt v4 eine beliebige Variable des Typs vector3d verwendet werden, ohne dass sich das Ergebnis ändert! Genau darin liegt aber das Problem: Beim Lesen des Codes gewinnt man den Eindruck, die Addition hätte in irgendeiner Weise etwas mit v4 zu tun, vielleicht im Sinne von v3 = v4 + v1 + v2. Bei einer entsprechenden Deklaration von Add wäre eine derartige Addition ja durchaus möglich.
7.3 Module und Strukturen
239
Aus Sicht der Klassenprogrammiererin lässt sich dagegen nicht viel tun. Die einzige Möglichkeit besteht darin, derartige Prozeduren nicht in einer Klasse oder Struktur, sondern in einem Modul zu definieren (was aber ebenfalls mit vielen Nachteilen und einem Verlust an innerer Logik verbunden ist). Aus Sicht der Anwenderin der Klasse kann man nur empfehlen, generell und immer nur die erste Syntaxvariante zum Aufruf von Shared-Methoden oder -Eigenschaften zu verwenden. ' Beispiel oo-programmierung\structure-test Public Structure vector3d Public x As Double Public y As Double Public z As Double Public Shared Function Add(ByVal sum1 As vector3d, _ ByVal sum2 As vector3d) As vector3d
VERWEIS
Dim tmp As vector3d = sum1 tmp.Add(sum2) Return tmp End Function End Structure
Das obige Beispiel ist Teil des Structure-Beispiels aus Abschnitt 7.3.2. Shared-Methoden können also gleichermaßen in Klassen und in Strukturen definiert werden.
7.3
Module und Strukturen
Module und Strukturen sind Sonderfälle von Klassen, die sich durch diverse Einschränkungen von den eigentlichen Klassen unterscheiden. Dieser Abschnitt beschreibt die Unterschiede, wobei vorausgesetzt wird, dass Sie bereits eine Vorstellung über das Konzept von Klassen haben.
7.3.1
Module
Module sind Codeblöcke, innerhalb derer Deklarationen von Variablen, Prozeduren, Eigenschaften etc. durchgeführt werden können. Sie werden mit Module eingeleitet und enden mit End Module. Module Module1 Dim x As Integer Sub testme() .. End Sub End Module
240
7 Objektorientierte Programmierung
Module werden im Regelfall zur Erfüllung der folgenden Aufgaben eingesetzt: zur Deklaration global verfügbarer Konstanten, Variablen und Prozeduren (siehe unten),
•
als Startpunkt für ein Programm (Prozedur Main()),
•
zur Formulierung von Code, der losgelöst von Objektinstanzen ausgeführt werden kann (z.B. für die zentrale Steuerung des Programmstarts und seines Endes) und
•
wenn Code losgelöst von den Konzepten objektorientierter Programmierung formuliert werden soll. HINWEIS
•
Der Begriff Modul ist doppeldeutig. In diesem Kapitel und generell im Zusammenhang mit VB.NET meint ein Modul eine Sonderform einer Klasse. In der .NETNomenklatur (und insbesondere als Klasse von System.Reflection) bezeichnet ein Modul allerdings eine Datei einer Assembly.
Module versus Klassen Im Vergleich zu einer Klasse stellt ein VB.NET-Modul einfach nur eine Sonderform dar, die sich überwiegend durch Einschränkungen unterscheidet: •
Es ist nicht möglich, eine Instanz eines Moduls zu erzeugen. Demzufolge gibt es auch keinen New-Konstruktor. (Es spricht natürlich nichts dagegen, innerhalb eines Moduls eine Prozedur mit dem Namen New zu deklarieren. Zu empfehlen ist dies allerdings nicht, weil diese Prozedur nur Verwirrung stiften würde. New gilt immer als Private und kann nicht als Public deklariert werden.)
•
Alle Prozeduren innerhalb eines Moduls entsprechen Shared-Methoden bzw. -Eigenschaften einer Klasse, können also aufgerufen werden, ohne dass eine Objektinstanz erforderlich ist.
•
Module können weder anderen Klassen erben noch selbst vererbt werden.
•
Module können keine Schnittstellen implementieren.
•
Module müssen direkt auf unterster Ebene in der Codedatei angegeben werden. Module können nicht verschachtelt werden, und Module können auch nicht innerhalb von Klassen definiert werden. Umgekehrt ist es aber zulässig, innerhalb eines Moduls eine Klassendefinition durchzuführen.
Globale Variablen und Prozeduren Der einzige Punkt, in dem Module Klassen überlegen sind, besteht in der Möglichkeit, Variablen oder Prozeduren so global zu definieren, dass sie ohne die zusätzliche Angabe des Modulnamens verwendet werden können. Das bedeutet, dass Variablen, die in Modulen als Public oder Friend deklariert werden, in allen anderen Klassen und Modulen
7.3 Module und Strukturen
241
unmittelbar verwendet werden. Ebenso können alle in Modulen definierten Prozeduren, sofern sie nicht als Private gekennzeichnet sind, in allen anderen Klassen und Modulen aufgerufen werden. (Prozeduren gelten per Default als Public.) ' Deklaration der globalen Variable x und ' der globalen Prozedur global_proc Module Module1 Friend x As Integer = 4 Friend Sub global_proc() ' Code für test_proc End Sub End Module
VERWEIS
Eine detaillierte Beschreibung der Gültigkeitsebenen (des so genannten scopes) von Variablen, Prozeduren etc. sowie der Schlüsselwörter Private, Public und Friend finden Sie in Abschnitt 7.9.
HINWEIS
' globale Variable x auswerten, Prozedur global_proc aufrufen Class Class2 Sub test_proc() Console.WriteLine(x) global_proc() End Sub End Class
Wenn in zwei Modulen derselbe Variablen- oder Prozedurname verwendet wird, dann muss in anderen Modulen oder Klassen der Modulname mit angegeben werden – also etwa die Variablenbezeichnung Module2.x.
Vielleicht ist der Unterschied zwischen Modulen und Klassen noch nicht ganz klar: Sie können auch in Klassen globale Variablen oder Prozeduren (die dann Methoden heißen) definieren. Dazu müssen Sie aber das Schlüsselwort Shared verwenden, damit auf die Variablen bzw. Prozeduren sofort zugegriffen werden kann, ohne vorher eine Instanz des Objekts zu erzeugen. Und im Unterschied zu Modulen müssen bei der Nutzung dieser Variablen oder Prozeduren nun immer auch der Klassenname angegeben werden – also Class1.x oder Class1.global_proc(). (Die folgenden Zeilen entsprechen dem obigen Beispiel.) ' Deklaration der globalen Klassenvariable x und ' der globalen Methode global_proc Class Class1 Friend x As Integer = 4 Friend Shared Sub global_proc() ' Code für test_proc End Sub End Class
242
7 Objektorientierte Programmierung
VORSICHT
' globale Klassenvariable x auswerten, ' globale Methode global_proc aufrufen Class Class2 Sub test_proc() Console.WriteLine(Class1.x) Class1.global_proc() End Sub End Class
Die Verwendung von Klassenvariablen, die als Shared deklariert sind, kann unerwartete Nebenwirkungen haben (siehe auch Abschnitt 7.2.5). Verwenden Sie Shared nur, wenn Sie sich über die Bedeutung dieses Schlüsselworts im Klaren sind!
7.3.2
Strukturen
Strukturen sind Codeblöcke, in denen Deklarationen von Variablen, Prozeduren, Eigenschaften etc. durchgeführt werden können. Sie werden mit Structure eingeleitet und enden mit End Structure. (Anders als in VB6 und in vielen anderen Programmiersprachen dürfen Strukturen Code enthalten! Strukturen dienen also nicht mehr einfach dazu, ein paar elementare Variablen zu einer Gruppe zusammenzufassen!) Strukturen werden üblicherweise dazu verwendet, einfache Datenstrukturen zu definieren. Einfach meint in diesem Zusammenhang, dass der Speicheraufwand für die Daten gering ist und dass die Methoden bzw. Eigenschaften zur Initialisierung bzw. Verarbeitung der Datenstruktur in der Regel nur ganz grundlegende (oft simple) Aufgaben erledigen. Structure Structure1 Private x As Double Public y As Double Sub m() ... Property e() As Integer ... End Structure
'ein internes Element der Struktur 'ein öffentliches Element der Struktur 'eine Methode für die Struktur 'eine Eigenschaft
Strukturen versus Klassen Im Vergleich zu einer Klasse stellt eine Struktur eine Sonderform dar. Die folgenden Punkte fassen die wichtigsten Unterschiede zusammen. •
Strukturen werden automatisch von ValueType abgeleitet, d.h., es handelt sich immer um Werttypen. Daraus folgt, dass die Daten von Strukturen am Stack (und nicht im so genannten heap für Objekte) gespeichert werden. Das macht Strukturen deutlich effizienter als Klassen, solange kleine Datenmengen verwaltet werden. (Eine ausführliche Diskussion der zahlreichen Unterschiede zwischen gewöhnlichen Klassen und Werttypen finden Sie in den Abschnitten 4.1.2 und 4.6.)
7.3 Module und Strukturen
243
Beachten Sie, dass es laut Objektbrowser Strukturen der .NET-Bibliotheken gibt, die nicht von ValueType abgeleitet sind. Beispielsweise bezeichnet der Objektbrowser String als eine Struktur, die direkt von Object abgeleitet ist. Im Gegensatz dazu ist die Hilfe zu String der Ansicht, dass es sich bei String um eine NotInheritable Public Class handelt. Diese Information erscheint glaubwürdiger und stimmt mit der Auswertung des TypeObjekts einer String-Variablen überein. Strukturen dürfen wie Klassen mit eigenen New-Konstrukturen ausgestattet werden. Im Gegensatz zu Klassen müssen diese Konstruktoren aber Parameter aufweisen. Der Defaultkonstruktor New ohne Parameter, der alle Elemente der Struktur mit ihren Defaultwerten initialisiert (also Zeichenketten mit "", Zahlen mit 0 etc.), kann nicht durch einen eigenen parameterlosen Konstruktor überschrieben werden.
•
Strukturen können weder andere Klassen oder Strukturen erben, noch können sie selbst vererbt werden. (Strukturen gelten als NotInheritable.) Strukturen können aber wie Klassen Schnittstellen implementieren.
•
Die Klassenvariablen von Strukturen dürfen keine Instanzen der eigenen Struktur enthalten. (Daher ist es unmöglich, die LinkedList-Klasse aus dem vorherigen Abschnitt als Struktur zu definieren: Dort sind die beiden Klassenvariablen nextItem und previousItem selbst vom Typ LinkedList deklariert und verweisen auf andere LinkedList-Objekte. Genau das ist bei Strukturen nicht möglich.) VERWEIS
•
Weitere Details finden Sie in der Hilfe, wenn Sie nach Strukturen und Klassen suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconStructuresAndClasses.htm
vector3d-Beispiel Im folgenden Beispiel wird die Struktur vector3d zur Verwaltung dreidimensionaler Vektoren definiert. Jeder Vektor besteht aus den drei Double-Elementen x, y und y, die öffentlich zugänglich sind. Darüber hinaus ist die Struktur mit einer Reihe von Methoden und Eigenschaften ausgestattet. Der Konstruktur New erleichtert die Initialisierung neuer vector3d-Variablen. Als Parameter müssen die Werte für x, y und y übergeben werden. Die Methode ToString zeigt den Inhalt der Zeichenkette in der Form [1; 2; 3] an. Die Read-Only-Methode Length errechnet die Länge des Vektors (indem die Wurzel aus der Summe der Quadrate der Einzelkomponenten ermittelt wird). Scale multipliziert eine vector3d-Struktur mit einem Faktor. Beachten Sie, dass es grundsätzlich zwei Möglichkeiten gibt, Methoden wie Scale zu deklarieren. Die eine besteht darin,
wie im hier vorliegenden Beispiel die Strukturvariable direkt zu verändern. Der Aufruf erfolgt in der Form vec.Scale(2). Die andere Variante besteht darin, dass Scale als eine Funktion deklariert wird, die die Strukturvariable unverändert lässt und stattdessen das Ergebnis der Operation zurückgibt. Die Anwendung der Methode würde dann so aussehen: v1 = v1.Scale(2) oder v2 = v1.Scale(3). Ein Vorteil der zweiten Variante besteht darin, dass auch
244
7 Objektorientierte Programmierung
Verkettungen der Form v2 = v1.Add(2).Scale(4) möglich sind. Wie so oft ist es aber eine Geschmacksfrage, welche Vorgehensweise intuitiver erscheint. Sie sollten die einmal gewählte Variante aber konsequent einsetzen! Für die Methode Add liegen zwei alternative Deklaration vor. Die Variante mit einem Parameter verändert die als Parameter übergebene Struktur: v1.Add(v2) entspricht also v1 = v1 + v2. Die Variante mit zwei Parametern ist als Shared deklariert und kann losgelöst von einer Struktur zur Addition zweier Vektoren verwendet werden: v1 = vector3d.Add(v2, v3). Innerhalb der Add-Prozedur wird dazu eine temporäre Variable verwendet, deren Inhalt als Ergebnis zurückgegeben wird. ' Beispiel oo-programmierung\structure-test Public Structure vector3d Public x As Double Public y As Double Public z As Double Public Sub New(ByVal x As Double, ByVal y As Double, _ ByVal z As Double) Me.x = x Me.y = y Me.z = z End Sub Public Overrides Function ToString() As String Return "[" + x.ToString + "; " + y.ToString + "; " + _ z.ToString + "]" End Function Public ReadOnly Property Length() As Double Get Return Math.Sqrt(x ^ 2 + y ^ 2 + z ^ 2) End Get End Property Public Sub Scale(ByVal factor As Double) x *= factor y *= factor z *= factor End Sub Public Sub Add(ByVal sum As vector3d) x += sum.x y += sum.y z += sum.z End Sub
7.4 Vererbung
245
Public Shared Function Add(ByVal sum1 As vector3d, _ ByVal sum2 As vector3d) As vector3d Dim tmp As vector3d = sum1 tmp.Add(sum2) Return tmp End Function End Structure
Anwendung der vector3d-Struktur Die Anwendung der vector3d-Struktur ist ausgesprochen intuitiv, wie die folgenden Zeilen beweisen. ' Beispiel oo-programmierung\structure-test Sub Main() Dim v1 As New vector3d(1, 2, 3) Dim v2, v3 As vector3d v1.Add(New vector3d(1, 0, 0)) v2 = v1 v2.Scale(2.5) v3 = vector3d.Add(v1, v2) Console.WriteLine("v1 = {0}", v1) Console.WriteLine("v2 = {0}", v2) Console.WriteLine("v3 = {0}", v3) Console.WriteLine("v3.Length = {0}", v3.Length) End Sub
Im Konsolenfenster wird das folgende Ergebnis angezeigt: v1 = [2; 2; v2 = [5; 5; v3 = [7; 7; v3.Length =
7.4
3] 7,5] 10,5] 14,4308696896618
Vererbung
Vererbung bedeutet, dass eine Klasse die Klassenmitglieder (Variablen, Methoden, Prozeduren) einer anderen Klasse übernimmt. Vererbung wird dazu verwendet, um redundanten Code zu vermeiden. Beispielsweise können Sie zwei Spezialklassen von einer Basisklasse ableiten. Alle gemeinsamen Merkmale werden nur einmal in der Basisklasse definiert. Im Code der Spezialklassen brauchen Sie sich dann nur noch um deren Besonderheiten zu kümmern. Was hier so einfach klingt, ist in der Realität meist doch mit erheblichem Aufwand verbunden. Wenn eine Klasse nicht von Anfang an unter dem Gesichtspunkt einer späteren Er-
246
7 Objektorientierte Programmierung
weiterung (Vererbung) entwickelt wurde, kann jede Änderung größere Probleme mit sich bringen (die oft Mängel im Design der Ausgangsklasse sichtbar machen).
7.4.1
Syntax
Im Klassencode wird Vererbung durch die Inherits-Anweisung durchgeführt. Inherits gibt an, welche Klasse die neue Klasse erbt. Bei der Basisklasse kann es sich sowohl um eine selbst definierte Klasse als auch um eine der zahllosen Klassen der .NET-Bibliotheken handeln. (Sie können also auch bereits vorhandene Klassen erweitern.) Inherits muss am Beginn einer Klassendefinition angegeben werden. ' Beispiel oo-programmierung\vererbung-intro Class class1 Public x As Integer Public y As Integer Public Sub m1() y = 2 * x End Sub End Class Class class2 Inherits class1 Public z As Integer Public Sub m2() z = x + y End Sub End Class
Verwendung vererbter Klassen Die Verwednung vererbter Klassen unterscheidet sich nicht von der gewöhnlicher Klassen. Wenn Sie ein Objekt der Klasse class2 erzeugen, dann stehen Ihnen anschließend alle öffentlichen Klassenvariablen, Methoden und Eigenschaften zur Verfügung, die in class1 und class2 gemeinsam deklariert sind (also obj.m1(), obj.m2(), obj.x, obj.y und obj.z). Objekte abgeleiteter Klassen dürfen auch in Variablen gespeichert werden, die für die Basisklasse deklariert sind (aber nicht umgekehrt). Im folgenden Beispiel werden die Variablen obj1a und obj2a jeweils mit einem neuen Objekt der Klassen class1 und class2 initialisiert. Anschließend wird ein Verweis auf obj2a in obj1b gespeichert. Dieser Verweis wird in der nächsten Zuweisung in obj2b gespeichert. Dabei ist eine Objektumwandlung durch CType erforderlich, weil der Compiler annimmt, dass obj1b ein Objekt des Typs class1 enthält. Wie die folgenden Konsolenausgaben beweisen, verweisen sowohl obj1b als auch obj2b auf ein class2-Objekt. Das Beispiel zeigt einmal mehr, dass jedes Objekt weiß, welcher Klasse es angehört – vollkommen unabhängig davon, wie die Variable deklariert ist.
7.4 Vererbung
247
Dim obj1a, obj1b As class1 Dim obj2a, obj2b As class2 obj1a = New class1() obj2a = New class2() obj1b = obj2a obj2b = CType(obj1b, class2) Console.WriteLine("TypeName(obj1b)=" + TypeName(obj1b)) Console.WriteLine("TypeName(obj2b)=" + TypeName(obj2b))
Die Ausgabe des Programms sieht so aus: TypeName(obj1b)=class2 TypeName(obj2b)=class2
Beachten Sie, dass es umgekehrt unmöglich ist, ein class1-Objekt in einer class2-Variable zu speichern. Die Zuweisung obj2b = obj1a wird bereits vom Compiler als ungültig zurückgewiesen. Wenn Sie es mit obj2b = CType(obj1a, class2) versuchen, akzeptiert zwar der Compiler den Code, aber nun tritt der Fehler bei der Ausführung aus (InvalidCastException).
Mehrfachvererbung Das Konzept der Vererbung kann verschachtelt werden: Sie können also zuerst eine Klasse A deklarierien, dann B von A ableiten und dann C von B. C enthält schließlich alle Klassenmitglieder von A und B.
VERWEIS
Von dieser Verschachtelung abgesehen gibt es aber keine Möglichkeit, dass eine Klasse von mehreren anderen Klassen gleichzeitig erbt. Innerhalb einer Klassendefinition ist daher nur eine einzige Inherits-Anweisung zulässig. Statt auf Vererbung können Sie in manchen Fällen auch auf Schnittstellen zurückgreifen. Schnittstellen bieten zwar nicht den Komfort der Vererbung, geben aber die Möglichkeit, in einer Klasse die Merkmale mehrerer unterschiedlicher Schnittstellen zu realisieren.
Defaultvererbung Alle Klassen sind per Default von Object abgeleitet, alle Strukturen von ValueType. Das kann weder bei Klassen noch bei Strukturen geändert werden: Bei Klassen können Sie zwar Inherits überklasse angeben, aber auch diese Überklasse ist zwangsläufig selbst wieder von Object abgeleitet; und bei Strukturen kann Inherits gar nicht verwendet werden.
MustInherit und NotInheritable Am Beginn der Klassendefinition (also vor Class name ...) können die Schlüsselwörter MustInherit oder NotInheritable vorangestellt werden:
248
•
7 Objektorientierte Programmierung
MustInherit bedeutet, dass die Klasse in dieser Form nicht unmittelbar verwendet
werden kann, d.h., dass es nicht möglich ist, ein Objekt dieser Klasse zu erzeugen. Die Klasse kann ausschließlich als Basisklasse bei der Definition anderer Klassen verwendet werden. •
NotInheritable bedeutet, dass die Klasse gewöhnlich angewendet werden kann, dass es aber nicht möglich ist, neue Klassen davon abzuleiten (zu vererben). NotInheritable verhindert also eine weitere Vererbung.
7.4.2
Basisklasse erweitern und ändern
Das Ziel von Vererbung besteht immer darin, eine vorhandene Basisklasse in irgendeiner Form zu erweitern bzw. zu verändern. Eine Erweiterung um zusätzliche Klassenvariablen, Methoden oder Eigenschaften ist nicht weiter schwierig: Wie das Einführungsbeispiel am Beginn dieses Abschnitts gezeigt hat, fügen Sie dazu einfach den Code für die neuen Schlüsselwörter in die Klasse ein. Bei einem Objekt der Klasse class2 stehen damit die Klassenvariablen x, y und z sowie die Methoden m1 und m2 zur Verfügung.
Overloads, Overrides und Shadows Deutlich komplexer ist die Veränderung bereits vorhandener Schlüsselwörter: Hier bedarf es klarer Regeln, ob und in welchem Ausmaß eine bereits vorhandenes Schlüsselwort durch eine neues, gleichnamiges Schlüsselwort ersetzt werden kann. VB.NET kennt drei unterschiedliche Mechanismen, um vorhandene Schlüsselwörter zu ersetzen. Diese Mechanismen sehen auf ersten Blick sehr ähnlich aus, unterscheiden sich aber in der internen Handhabung durch den Compiler. •
Overrides: Overrides ersetzt eine Methode oder Eigenschaft der Basisklasse. Die neue Methode oder Eigenschaft muss sowohl dieselben Parameter als auch dieselbe Gültigkeitsebene (z.B. Private, Public) aufweisen. Overrides kann nur verwendet werden, wenn das Schlüsselwort in der Basisklasse als Overridable oder MustOverride deklariert ist. Per Default gelten eigene Methoden und Eigenschaften als NotOverridable. Jede mit Overrides deklarierte Methode oder Eigenschaft gilt automatisch selbst wieder als Overridable, ohne dass dies explizit angegeben
wird. MustOverride-Deklarationen dienen nur zur Angabe der Parameterliste. Es darf aber weder ein Code für die Prozedur angegeben werden, noch darf die Deklaration mit End Sub/Function/Property abgeschlossen werden.
•
Overloads: Auch Overloads ersetzt eine Methode oder Eigenschaft der Basisklasse. Dabei kommt derselbe Überladungsmechanismus zum Einsatz, der es auch erlaubt, mehrere gleichnamige Prozeduren innerhalb eines Moduls oder einer Klasse zu definieren (siehe Abschnitt 5.3.4).
Die Überladung gilt allerdings nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Wenn Sie die Methode m1 überladen, dann wird durch obj.m1() die
7.4 Vererbung
249
neue Methode ausgeführt, aber durch CType(obj, basisklasse).m1() die Methode der Basisklasse! Overloads kann nicht verwendet werden, wenn sich bei einer Methode oder Eigenschaft
nur der Typ des Rückgabewerts (aber nicht die Parameterliste) verändert. •
Shadows: Shadows verdeckt alle gleichnamigen Schlüsselwörter der Basisklasse. Dazu ist nicht erforderlich, dass das Basisschlüsselwort und das neue Schlüsselwort denselben Typ, dieselbe Gültigkeitsebene oder dieselben Parameter aufweist. Shadows kann auch bei der Deklaration von Variablen oder Konstanten verwendet werden.
Mit Shadows können Sie beispielsweise eine Klassenvariable der Basisklasse A durch eine Eigenschaft der abgeleiteten Klasse B ersetzen. Wenn in B auch nur eine einzige Methode m1 mit Shadows deklariert ist, dann verdeckt diese Methode alle in A deklarierten Methoden m1. Wie Overloads gilt auch Shadows nur solange, wie der Compiler dem Objekt die richtige Klasse zuordnet. Jetzt stellt sich natürlich die Frage, welche dieser drei Varianten die Beste ist. Wenn Sie eine eigene Klassenbibliothek erstellen und somit Einfluss auf die Basisklassen haben, ist die Kombination aus Overridable (Basisklasse) und Overrides (abgeleitete Klasse) die im Sinne der objektorientierten Programmierung beste Lösung. Overrides bietet als einzige der drei Varianten die Sicherheit, dass die ursprüngliche Eigenschaft oder Methode auf keinen Fall mehr zugänglich ist (auch nicht, wenn ein Objekt durch CType in ein Objekt der Basisklasse umgewandelt wird). Overloads bietet sich dann an, wenn Sie eine fremde Klasse vererben und eine einzelne Eigenschaften oder Methoden verändern möchten, die in der Basisklasse als NotOveridable gilt. Shadows sollte nur zum Einsatz kommen, wenn Sie den Typ oder die Gültigkeitsebene
eines Schlüsselworts bzw. den Typ eines Rückgabewerts ändern möchten. Einen Sonderfall stellen New-Konstruktoren dar: Diese werden in der neuen Klasse prinzipiell ohne zusätzliche Kennzeichner deklariert. Es ist weder notwendig noch zulässig, einen New-Konstruktor mit Overridable zu kennzeichnen.
Zugriff auf Eigenschaften und Methoden der Basisklasse Innerhalb des Klassencodes können Sie mit den drei Schlüsselwörter Me, MyBase und MyClasse auf die aktuelle Objektinstanz verweisen. Die drei Schlüsselwörter unterscheiden sich dadurch, welche Eigenschaften oder Methoden damit aufgerufen werden. •
Me: Dieses aus früheren Beispielen schon vertraute Schlüsselwort verweist auf die Instanz der Klasse, die gerade bearbeitet wird. (Wenn Sie die Methode obj.m1() aufrufen, dann verweist Me innerhalb des Codes der Klasse m1 auf obj.) Das Schlüsselwort kann verwendet werden, wenn eine Eigenschaft oder Methode eine Instanz der Klasse zurückgeben soll. Me.methode ruft ebenso wie einfach methode die zur Objektinstanz passende Methode auf. (Die Anwendung von Me ist daher in den meisten Fällen optional.)
250
•
7 Objektorientierte Programmierung
MyBase: MyBase verweist wie Me auf die aktuelle Objektinstanz. Durch MyBase.schlüsselwort können Sie aber Schlüsselwörter der abgeleiteten Basisklasse nutzen. Insbesondere können Sie durch MyBase.New den Konstruktor und durch MyBase.Finalize den Destruk-
tor der Basisklasse aufrufen. MyBase.methode ruft eine Methode der Basisklasse auf, selbst dann, wenn es in der aktu-
ellen Klasse eine gleichnamige Neudefinition gibt. •
MyClass: Auch MyClass verweist auf die aktuelle Objektinstanz. Anders als bei Me werden durch MyClass in jedem Fall die unmittelbar in der Klasse definierten Methoden
oder Eigenschaften aufgerufen. MyClass.methode ruft die in der aktuellen Klasse definierte Methode auf, selbst dann, wenn aufgrund des Objekttyps eigentlich eine Overrides-Methode oder -Eigenschaft einer abgeleiteten Klasse aufgerufen werden müsste. (Zu MyClass gibt es in der Online-
VERWEIS
Hilfe ein gut verständliches Beispiel.) Grundsätzlich können Sie in der abgeleiteten Klasse nur auf solche Klassenvariablen, Eigenschaften und Methoden der Basisklasse zugreifen, die dort als Public oder Protected deklariert sind, nicht aber auf solche, die mit Private deklariert sind. Protected bedeutet, dass die Schlüsselwörter zwar von außen unzugänglich sind,
dass sie aber in vererbten Klassen verwendet werden dürfen. Einen ausführlichen Überblick über die Gültigkeitsebenen von Schlüsselwörtern gibt Abschnitt 7.9. Dort werden die vier Schlüsselwörter Private, Protected, Friend und Public miteinander verglichen.
Beispiel Das folgende Beispiel erfüllt keine konkrete Aufgabe, sondern demonstriert die verschiedenen Mechanismen, um vorgegebene Methoden in einer abgeleiteten Klasse zu verändern. (Dieselben Mechanismen können auch für Eigenschaften angewendet werden.) Ausgangspunkt des Beispiels ist die Klasse a mit den Methoden m1, m2 und m3. Jede dieser Methoden steht in zwei Varianten zur Verfügung, einmal ohne Parameter und einmal mit einem Integer-Parameter. Die Klasse b erbt a. m1 wird mittels Shadows durch eine neue Version ersetzt. Das neue m1 ersetzt sowohl m1() als auch m1(Integer)! Das bedeutet, dass die Methode m1(Integer) für Objekte der Klasse b nicht mehr zugänglich ist. (Es bestünde aber natürlich die Möglichkeit, in der Klasse b auch m1(Integer) neu zu implementieren.) m2 wird mittels Overloads durch eine neue Version ersetzt. m2(Integer) wird dadurch nicht
berührt und bleibt weiter zugänglich. m3 wird schließlich mittels Overrides durch eine neue Version ersetzt. Bei der Deklaration muss auch Overloads angegeben werden, weil in a mehrere gleichnamige Varianten von m3 definiert sind. m3(Integer) bleibt weiter zugänglich.
7.4 Vererbung
251
' Beispiel oo-programmierung\overloads_overrides_shadows Class a Public Sub m1() Console.WriteLine("a.m1") End Sub Public Sub m1(ByVal x As Integer) Console.WriteLine("a.m1(x)") End Sub Public Sub m2() Console.WriteLine("a.m2") End Sub Public Sub m2(ByVal x As Integer) Console.WriteLine("a.m2(x)") End Sub Public Overridable Sub m3() Console.WriteLine("a.m3") End Sub Public Overridable Sub m3(ByVal x As Integer) Console.WriteLine("a.m3(x)") End Sub End Class Class b Inherits a Public Shadows Sub m1() Console.WriteLine("b.m1") End Sub Public Overloads Sub m2() Console.WriteLine("b.m2") End Sub Public Overloads Overrides Sub m3() Console.WriteLine("b.m3") End Sub End Class
In Main wird ein neues Objekt der Klasse b erzeugt. Der Aufruf der Methoden m1 bis m3 ohne Parameter führt erwartungsgemäß zu den Ausgaben b.m1, b.m2 und b.m3. Interessanter ist das Ergebnis, wenn die Variable mit CType in ein Objekt des Typs a umgewandelt wird: Bei Shadows und Overloads wird nun die alte Version von m1 bzw. m2 aufgerufen. Einzig die Wirkung von Overrides ist so nachhaltig, dass trotz der Objektumwandlung weiterhin die neue Methode aufgerufen wird.
252
7 Objektorientierte Programmierung
Module Module1 Sub Main() Dim obj As New b() obj.m1() ' obj.m1(123) ist nicht zugänglich obj.m2() ' obj.m2(123) ist zugänglich obj.m3() ' obj.m3(123) ist zugänglich CType(obj, a).m1() CType(obj, a).m2() CType(obj, a).m3() End Sub End Module
Das Ergebnis des Programms sieht so aus: b.m1 b.m2 b.m3 a.m1 a.m2 b.m3
7.4.3
LinkedList-Beispiel
Die Ausgangsidee für dieses Beispiel ist eigentlich recht einfach: Aus der in diesem Kapitel vorgestellten LinkedList-Klasse soll eine neue LinkedListTime-Klasse abgeleitet werden. Die Neuerung dieser Klasse besteht darin, dass in zwei privaten Klassenvariablen jeweils das Datum und die Uhrzeit der letzten Änderung der Daten bzw. des letzten Lesezugriffs gespeichert werden. Diese Daten können mit den beiden neuen Methoden GetLastWrite bzw. GetLastAccess gelesen werden.
VERWEIS
Auf den ersten Blick klingt die Aufgabenstellung so, als ließe sich die neue Klasse LinkedListTime in ein paar Minuten programmieren. Tatsächlich betrug der Aufwand mehrere Stunden und erforderte ca. 120 Codezeilen für die neue Klasse (gegenüber 190 Zeilen für die Basisklasse, die in einigen Details verändert werden musste, um eine Erweiterung überhaupt möglich zu machen). Weitere Vererbungsbeispiele finden Sie in den Abschnitten 14.12.1 (Steuerelemente vererben) und 15.2.5 (Fenster bzw. Formulare vererben).
7.4 Vererbung
253
LinkedList-Änderungen Die meisten Änderungen in der Ausgangsklasse sind kosmetischer Natur: Die Daten des Objekts werden nun in den privaten Klassenvariablen _nextItem, _previousItem und _Value gespeichert. Der Zugriff auf diese Variablen erfolgt durch die Eigenschaften nextItem, previousItem und Value. Außerdem wurden bei einigen Eigenschaften und Methoden die Deklarationen um das Schlüsselwort Overrideable ergänzt. In den folgenden Zeilen sind aus Platzgründen nur die Deklarationen aller Schlüsselwörter der Klasse abgedruckt. ' Beispiel oo-programmierung\linkedlist4, Datei linkedlist.vb Class LinkedList ' Klassenvariablen Private _nextItem As LinkedList Private _previousItem As LinkedList Private _value As String ' Methoden Public Sub New(ByVal s As String) Public Function AddAfter(ByVal s As String) As LinkedList Public Function AddBefore(ByVal s As String) As LinkedList Public Sub Remove() Public Function GetNext() As LinkedList Public Function GetPrevious() As LinkedList Public Overrides Function ToString() As String Public Function ItemsText(Optional ByVal max As Integer = 10, _ Optional ByVal delimitor As String = " ") As String ' Eigenschaften Protected Property previousItem() As LinkedList Protected Property nextItem() As LinkedList Public Overridable Property Value() As String Public ReadOnly Property Count() As Integer Public ReadOnly Property [Next]() As LinkedList Public ReadOnly Property Previous() As LinkedList Default Public Property Item(ByVal n As Integer) As LinkedList End Class
LinkedListTime-Klasse Der Code der LinkedListTime-Klasse beginnt mit der Inherits-Anweisung und der Deklaration der beiden neuen Klassenvariablen _lastAccess und _lastWrite.
254
7 Objektorientierte Programmierung
' Beispiel oo-programmierung\linkedlist4, Datei linkedlisttime.vb Class LinkedListTime Inherits LinkedList ' zwei neue Klassenvariablen Private _lastAccess As Date Private _lastWrite As Date ... weitere Methoden und Eigenschaften End Class
Neue oder geänderte Methoden für LinkedListTime Der neue Konstruktor New greift auf den New-Konstruktor der Basisklasse zurück. Außerdem wird der Eigenschaft lastWrite die aktuelle Uhrzeit zugewiesen. (Der Code zu lastWrite folgt etwas weiter unten. Kurz gefasst wird die Uhrzeit sowohl in _lastAccess als auch in _lastWrite gespeichert.) Public Sub New(ByVal s As String) MyBase.New(s) lastWrite = Now End Sub
Die beiden Methoden GetLastAccess und GetLastWrite geben öffentlichen Lesezugriff auf _lastAccess und _lastWrite. Public Function GetLastAccess() As Date Return _lastAccess End Function Public Function GetLastWrite() As Date
... wie GetLastAccess
Die von LinkedList vertrauten Methoden AddAfter/-Before und GetNext/-Previous mussten für LinkedListTime wegen des geänderten Rückgabetyps neu implementiert werden. Aus eben diesem Grund muss bei der Deklaration Shadows verwendet werden. (Overrides oder Overloads können hier nicht eingesetzt werden.) Public Shadows Function AddAfter(ByVal s As String) As LinkedListTime Dim newitem As New LinkedListTime(s) newitem.previousItem = Me newitem.nextItem = Me.nextItem Me.nextItem = newitem Return newitem End Function Public Shadows Function AddBefore(...) ... wie AddAfter Public Shadows Function GetNext() As LinkedListTime Return nextItem End Function Public Shadows Function GetPrevious() ... wie GetNext
7.4 Vererbung
255
Die Methoden Remove, ToString und ItemsText der Basisklasse brauchen nicht verändert zu werden. Sie funktionieren unverändert auch für LinkedListTime-Objekte.
Neue oder geänderte Eigenschaften für LinkedListTime Die Eigenschaften lastWrite und lastAccess vereinfachen den klasseninternen Zugriff auf _lastWrite und -Access. Bemerkenswert ist vor allem der Set-Teil von lastWrite, der auch _lastAccess verändert. Protected Property lastWrite() As Date Get Return _lastWrite End Get Set(ByVal Value As Date) _lastWrite = Value _lastAccess = Value End Set End Property Protected Property lastAccess() As Date ... wie lastWrite
Die neue Version der Value-Eigenschaft unterscheidet sich von der Basisvariante dadurch, dass bei jedem Zugriff auch die Eigenschaft lastWrite bzw. lastAccess geändert wird. Public Overrides Property Value() As String Get lastAccess = Now Return MyBase.Value End Get Set(ByVal Value As String) MyBase.Value = Value lastWrite = Now End Set End Property previous und nextItem sind nur für die klasseninterne Verwaltung gedacht. Ihre Aufgabe besteht darin, das vorausgehende bzw. nachfolgende LinkedListTime-Objekt zu ermitteln. Da MyBase.previousItem als Rückgabewert ein LinkedList-Objekt liefert, muss dieses in ein LinkedListTime-Objekt umgewandelt werden. (Das funktioniert natürlich nur dann, wenn MyBase.previousItem tatsächlich ein LinkedListTime-Objekt ist! Solange ein LinkedListTime-
Objekt nur mit den hier vorgestellten Methoden bearbeitet wird, ist das sichergestellt.) Protected Shadows Property previousItem() As LinkedListTime Get Return CType(MyBase.previousItem, LinkedListTime) End Get Set(ByVal Value As LinkedListTime) MyBase.previousItem = Value End Set
256
7 Objektorientierte Programmierung
End Property Protected Shadows Property nextItem() ... wie previousItem
Die öffentlich zugänglichen Eigenschaften Next und Previous greifen auf die gerade vorgestellten Eigenschaften zurück. Public Shadows ReadOnly Property [Next]() As LinkedListTime Get Return nextItem End Get End Property Public Shadows ReadOnly Property Previous() ... wie Next Item kann auf MyBase.Item zurückgreifen, muss aber im Get-Teil mit CType eine Typum-
wandlung durchführen. Default Public Shadows Property Item(ByVal n As Integer) _ As LinkedListTime Get Return CType(MyBase.Item(n), LinkedListTime) End Get Set(ByVal Value As LinkedListTime) MyBase.Item(n) = Value End Set End Property
Als einzige Eigenschaft kann Count unverändert genutzt werden.
7.5
Schnittstellen (interfaces)
VERWEIS
Schnittstellen (englisch interfaces) dienen dazu, gemeinsame Merkmale unterschiedlicher Klassen oder Strukturen zu definieren. Eine Schnittstelle besteht lediglich aus einer simplen Aufzählung von Schablonen (also Deklarationen ohne Code) für Methoden, Eigenschaften und Ereignisse. Klassen, die eine Schnittstelle implementieren, müssen für alle Schablonen realen Code angeben. Der Sinn von Schnittstellen besteht darin, dass Objekte unterschiedlicher Klassen, die alle eine bestimmte Schnittstelle realisieren, einheitlich behandelt werden können. Eine sehr anschauliche Demonstration der Anwendungsmöglichkeiten von Schnittstellen geben die Klassen des System.Collections-Namensraums, die in Kapitel 9 beschrieben werden.
7.5 Schnittstellen (interfaces)
7.5.1
257
Syntax und Anwendung
Am besten ist das Konzept und die Syntax von Schnittstellen anhand eines einfachen Beispiels zu verstehen: In den folgenden Zeilen wird die Schnittstelle ILen definiert.
Definition einer Schnittstelle Schnittstellen werden mit Interface name eingeleitet und enden mit End Interface. Die Namen von Schnittstellen beginnen überlicherweise mit dem Buchstaben I. Das einzige Merkmal dieser Schnittstelle besteht darin, dass jede Klasse oder Struktur die Methode Len zur Verfügung stellen muss. Diese Methode muss als Ergebnis eine DoubleZahl liefern, die Rückschluss auf die Länge oder Größe der Daten gibt. Innerhalb der Schnittstelle wird daher einfach eine Schablone für die Methode Len angegeben, und zwar ohne die Angabe der Gültigkeitsebene (also ohne Private, Public etc.), ohne konkreten Code für die Realisierung der Methode und ohne End Sub/Function/Property. ' Beispiel oo-programmierung\interfaces-intro Interface ILen Function Len() As Double End Interface
Schnittstellen können andere Schnittstellen erben. Sie können also am Beginn einer Schnittstelle Inherits Ixy angeben, sofern Ixy eine andere Schnittstelle ist. Die resultierende neue Schnittstelle enthält dann auch alle Merkmale der abgeleiteten Schnittstelle. (Anders als bei Klassen ist es sogar möglich, eine Schnittstelle von mehreren anderen Schnittstellen abzuleiten.)
Implementierung (Realisierung) einer Schnittstelle Die beiden Strukturen vector2d und vector3d realisieren die ILen-Schnittstelle. Dazu muss die Schnittstelle am Beginn der Struktur mit Implements angegeben werden. (Mehrere Schnittstellen können entsprechend durch mehrere Implements-Anweisungen genannt werden. Bei Klassen müssen die Implements-Anweisungen unmittelbar einer eventuellen Inherits-Anweisung folgen.) Der Deklaration der Methode Len muss Implements ILen.Len nachgestellt werden, um den Zweck dieser Methode zu verdeutlichen. Der VB.NET-Compiler sollte eigentlich wie der C#-Compiler auch ohne den Implements-Zusatz in der Lage sein, den Zusammenhang zur Schnittstelle herzustellen. Die VB.NET-Syntax schreibt den eigentlich unsinnigen Implements-Nachsatz aber vor. Das macht es möglich, der Methode einen anderen Namen zu geben als den, der durch die Schnittstelle vorgegeben ist – also beispielsweise Function xy() As Double Implements ILen.Len. In der Praxis stiftet das aber meist nur Verwirrung. Die einzig sinnvolle Anwendung besteht darin, in einer Klasse gleichnamige Methoden unterschiedlicher Schnittstellen zu realisieren.
258
7 Objektorientierte Programmierung
Structure vector2d Implements ILen Public x, y As Double Public Function Len() As Double Implements ILen.Len Return Math.Sqrt(x ^ 2 + y ^ 2) End Function End Structure Structure vector3d Implements ILen Public x, y, z As Double
HINWEIS
Public Function Len() As Double Implements ILen.Len Return Math.Sqrt(x ^ 2 + y ^ 2 + z ^ 2) End Function End Structure
Wenn Sie eine Schnittstelle implementieren, müssen Sie alle Eigenschaften und Methoden dieser Schnittstelle implementieren. In manchen Fällen kann es sein, dass das nicht sinnvoll ist; dann müssen Sie in Ihre eigene Klasse zumindestens die Prozedurdefinition für die Eigenschaft oder Methode angeben, ohne diese Definition mit Code zu füllen. Eventuell sollten Sie beim Aufruf der Eigenschaft oder Methode eine NotImplementedException auslösen, um dem Anwender der Klasse klar zu machen, dass diese Eigenschaft bzw. Methode nicht zur Verfügung steht.
Objekte einer gemeinsamen Schnittstelle verarbeiten Die folgenden Zeilen zeigen die Deklaration und Initialisierung je eines Objekts der Klassen vector2d und -3d. Beide Objekte werden dann an die Prozedur WriteLen übergeben, die die Länge des Vektors in einem Konsolenfenster anzeigt. Die Besonderheit von WriteLen besteht darin, dass dessen Parameter x mit dem Typ ILen deklariert ist. Damit kann ein Objekt einer beliebigen Klasse übergeben werden, vorausgesetzt die Klasse unterstützt die ILen-Schnittstelle. (Innerhalb der Prozedur können nur die durch ILen deklarierten Eigenschaften und Methoden genutzt werden. ILen beschreibt also den gemeinsamen Nenner aller Klassen mit ILen-Schnittstelle.) ' Beispiel oo-programmierung\interfaces-intro Sub Main() Dim v2 As vector2d Dim v3 As vector3d v2.x = 3 : v2.y = 2 v3.x = 4 : v3.y = 5 : v3.z = 6 WriteLen(v2) WriteLen(v3) End Sub
7.5 Schnittstellen (interfaces)
259
Sub WriteLen(ByVal x As ILen) Console.WriteLine(x.Len) End Sub
Schnittstellen versus Vererbung Die Implementierung einer Schnittstelle und die Vererbung einer Klasse sieht auf den ersten Blick sehr ähnlich aus. Tatsächlich gibt es aber fundamentale Unterschiede: •
Eine Klasse darf nur von einer anderen Basisklasse vererbt werden, kann aber beliebig viele Schnittstellen realisieren.
•
Vererbter Code für Methoden oder Eigenschaften kann meist ohne Veränderung genutzt werden (sofern die neue Klasse für die betreffende Eigenschaft oder Methode nicht neue Funktionen realisieren will). Im Gegensatz dazu müssen die durch eine Schnittstelle vorgegebenen Methoden oder Eigenschaften in jeder Klasse vollkommen neu implementiert werden. Schnittstellen dienen daher nicht zur Wiederverwendung von Code, sondern lediglich dazu, den Kommunikationsmechanismus für mehrere unterschiedliche Klassen zu vereinheitlichen.
•
Schnittstellen können auch von Strukturen realisiert werden, während Vererbung nur für Klassen zur Verfügung steht.
7.5.2
IDisposable-Schnittstelle
Die .NET-Bibliotheken enthalten zahllose Schnittstellen, deren Implementierung in eigenen Klassen sinnvoll sein kann: •
IComparable ermöglicht den Vergleich zweier Objekte (siehe auch Abschnitt 9.3.2). Die Schnittstelle ist auch dann wichtig, wenn Objekten sortiert werden sollen.
•
IClonable ermöglicht es, eine identische Kopie eines Objekts zu erzeugen.
•
IConvertible ermöglicht es, das Objekt in einen anderen Typ umzuwandeln.
•
IFormatable ermöglicht es, das Objekt mit ToString auf unterschiedliche Weise in Text-
form darzustellen. •
IEnumerable ermöglicht es, alle Elemente einer Aufzählung mit For-Each zu durchlaufen (siehe Abschnitt 9.2).
•
ISerializable ermöglicht die Steuerung der (De-)Serialisierung eigener Klassen (siehe Abschnitt 10.9).
Dieses Kapitel beschränkt sich allerdings auf ein Beispiel für eine einzige Schnittstelle, nämlich IDisposable. Diese Schnittstelle hilft dabei, eine Objektinstanz kontrolliert durch die Methode Dispose zu löschen. Die Schnittstelle sollte für eigene Klassen realisiert werden, wenn diese überdurchschnittlich viel Speicher beanspruchen, Dateien oder Daten-
260
7 Objektorientierte Programmierung
VERWEIS
bankverbindungen öffnen oder auf eine andere Weise Systemressourcen binden. In so einem Fall ist es nicht sinnvoll, darauf zu warten, dass die garbage collection irgendwann das Objekt wieder freigibt. Stattdessen wird diese Verantwortung an die Anwenderin der Klasse übertragen, die Dispose ausführen muss, sobald das Objekt nicht mehr benötigt wird. Weitere Informationen zu diesem Thema finden Sie bei der Beschreibung der Object.Finalize-Methode und wenn Sie nach Dispose implementieren suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemObjectClassFinalizeTopic.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconimplementingdisposemethod.htm
Zur Implementierung der Disposable-Schnittstelle müssen Sie lediglich die Dispose-Methode zur Verfügung stellen. Dabei sollten Sie aber einige Dinge beachten: •
Der Code sollte so formuliert werden, dass ein mehrfacher Aufruf von Dispose zu keinen Fehlern führt.
•
Am Ende von Dispose sollte GC.SuppressFinalize(Me) ausgeführt werden. Damit erreichen Sie, dass für das bereits gelöschte Objekt auf den automatischen Finalize-Aufruf durch die garbage collection verzichtet wird. (Das erhöht die Effizienz der garbage collection.)
•
Falls die eigene Klasse von einer anderen Klasse abgeleitet ist, die selbst die IDisposableSchnittstelle realisiert, muss in der eigenen Dispose-Methode MyBase.Dispose() ausgeführt werden, um auch alle Ressourcen der Basisklasse freizugeben.
•
Falls die Anwenderin der Klasse den Dispose-Aufruf vergisst, sollte sich die FinalizeMethode um die Aufräumarbeiten kümmern.
•
Falls die Klasse eine Close-Methode kennt, sollte diese einfach Dispose aufrufen. (Damit hat obj.Close() dieselbe Wirkung wie obj.Dispose().)
Aus diesen Anforderungen ergibt sich die folgende Schablone, die Sie zur Erstellung eigener Dispose-Methoden verwenden können. Die eigentliche Aufräumarbeit erfolgt in der Prozedur cleanup, die sowohl von Finalize als auch von Dispose aufgerufen wird. Die Variable clean stellt sicher, dass der Code nur einmal ausgeführt wird. Class Class1 Implements IDisposable Private clean As Boolean = False ' Finalize gibt es nur für den Fall, 'dass auf obj.Dispose() vergessen wird Protected Overrides Sub Finalize() cleanup() MyBase.Finalize() End Sub
7.5 Schnittstellen (interfaces)
261
Overridable Overloads Sub Dispose() Implements IDisposable.Dispose cleanup() ' nur wenn die Basisklasse ebenfalls IDisposable implementiert: ' MyBase.Dispose() GC.SuppressFinalize(Me) End Sub Private Sub cleanup() If clean = True Then Exit Sub 'alles schon erledigt clean = True ... Aufräumarbeiten End Sub End Class
IDisposeable-Beispiel Mit einem Objekt der Klasse AppendTextToFile können Sie eine Textdatei öffnen und an diese Text hinzufügen (Methode WriteLine). Innerhalb der Beispielklasse wird dazu ein IO.StreamWriter-Objekt verwendet (siehe Abschnitt 10.5.3). Objekte der AppendTextToFile-Klasse können durch Close oder Dispose aus dem Speicher entfernt werden. Innerhalb der Klasse wird dazu die Methode cleanup ausgeführt, um das StreamWriter-Objekt zu schließen. Wenn die Anwenderin den Aufruf von Dispose oder Close vergisst, wird durch die garbage collection die Methode Finalize aufgerufen, die dann ebenfalls cleanup aufruft. Die Variable clean stellt sicher, dass auch bei einen irrtümlichen doppelten Aufruf von Close oder Dispose kein Fehler auftritt. ' Beispiel oo-programmierung\idisposable-test Class AppendTextToFile Implements IDisposable Private clean As Boolean = False Private sw As IO.StreamWriter Public Sub New(ByVal filename As String) sw = New IO.StreamWriter(filename, True) End Sub Public Sub WriteLine(ByVal s As String) sw.WriteLine(s) sw.Flush() 'Zeile sofort physikalisch speichern End Sub ' Finalize gibt es nur für den Fall, ' dass obj.Dispose() oder obj.Close() vergessen wird Protected Overrides Sub Finalize() cleanup() MyBase.Finalize() End Sub
262
7 Objektorientierte Programmierung
' Objekt dezidiert schließen Public Sub Close() Dispose() End Sub Overridable Overloads Sub Dispose() Implements IDisposable.Dispose cleanup() GC.SuppressFinalize(Me) End Sub ' Aufräumarbeiten Private Sub cleanup() If clean = True Then Exit Sub 'alles schon erledigt clean = True sw.Close() End Sub End Class
Um die Klasse zu testen, wird in Main zweimal die Prozedur WriteALine aufgerufen. In dieser Prozedur wurde attf.Close auskommentiert. Es müsste daher eigentlich zu einem Fehler kommen, weil die temporäre Datei nach dem ersten Aufruf von WriteALine nicht geschlossen wurde. Dieser Fehler tritt aufgrund der künstlich ausgelösten garbage collection (die zu einem Aufruf von attf.Finalize() führt) nicht auf. Sub Main() WriteALine() GC.Collect() GC.WaitForPendingFinalizers() WriteALine() End Sub
'garbage collection auslösen 'wartet auf das Ende der gc
Sub WriteALine() Dim attf As New AppendTextToFile(IO.Path.GetTempPath() + "test.txt") attf.WriteLine(Now.ToLongTimeString) ' attf.Close() End Sub
7.6
Ereignisse und Delegates
Ereignisse bieten eine Möglichkeit, mit der ein Objekt Informationen über einen geänderten Zustand, geänderte Daten, neue Eingaben, Fehler etc. weitergeben kann. Dazu wird beim Empfänger des Ereignisses eine zuvor vereinbarte Prozedur aufgerufen. Delegates stellen einen Mechanismus zur Verfügung, um Prozeduren, Funktionen oder
Methoden aufzurufen, von denen nur die Adresse bekannt ist. Das ermöglicht es beispielsweise, einen Funktionszeiger an eine Prozedur zu übergeben und die Funktion dort aufzurufen.
7.6 Ereignisse und Delegates
263
Ereignisse und Delegates werden hier (und in den meisten anderen .NET-Büchern) gemeinsam beschrieben, weil Ereignisse intern auf Delegates aufbauen. Ereignisse sind gewissermaßen nur eine Spezialanwendung von Delegates. Ereignisse haben aber den Vorteil, dass Sie sich nicht mit der Komplexität des Delegates-Mechanismus auseinandersetzen müssen.
7.6.1
Ereignisse
Deklaration eines Ereignisses Ereignisse können in Klassen, Modulen, Strukturen und in Schnittstellen mit dem Schlüsselwort Event deklariert werden. Die Deklaration enthält keinen Code und gibt nur den Namen des Ereignisses und die Datentypen der Parameter der Ereignisprozedur an. (Üblicherweise wird als erster Parameter eine Objektinstanz der Klasse übergeben; die weiteren Parameter enthalten ereignisspezifische Daten.) Class Class1 Public Event ValueChanged(ByVal obj As Object, _ ByVal data As Integer) ... End Class
Aufruf eines Ereignisses Innerhalb der Klasse, in der das Ereignis deklariert wurde, kann es durch RaiseEvent ausgelöst werden. RaiseEvent ValueChanged(Me, 123)
Ereignisse empfangen Es gibt zwei Möglichkeiten, ein Ereignis zu empfangen, das durch RaiseEvent ausgelöst wurde: entweder, indem die Objektvariable mit WithEvents deklariert und eine dazu passende Ereignisprozedur mit Handles angegeben wird, oder, indem die Ereignisprozedur mit AddHandler angegeben wird. Bei Ereignissen, die in Strukturen oder Modulen (nicht in Klassen) deklariert sind, bietet AddHandler die einzige Möglichkeit, das Ereignis zu empfangen. Ereignisse, die nicht empfangen werden, verfallen. Wenn also innerhalb einer Klasse RaiseEvent ausgeführt wird, aber keine Prozedur eingerichtet wurde, um das Ereignis zu verarbeiten, kommt es zu keiner Reaktion (auch zu keinem Fehler).
WithEvents und Handles Der einfachste Weg, Ereignisse eines bestimmten Objekt zu empfangen, besteht darin, die Objektvariable auf Modul- oder Klassenebene (also nicht innerhalb einer Prozedur!) mit WithEvents zu deklarieren. Wenn Sie möchten, können Sie das Objekt dabei gleich erzeu-
264
7 Objektorientierte Programmierung
gen (As New Class1(...)) – andernfalls müssen Sie das zu einem späteren Zeitpunkt mit obj1 = New Class1(...) nachholen. Auf jeden Fall können Sie nur dann Ereignisse empfangen, wenn das Objekt auch existiert! Dim WithEvents obj1 As Class1 obj1 = New Class1(...)
Der zweite Schritt besteht nun, die dazugehörende Ereignisprozedur in den Code einzufügen. Dabei hilft Ihnen die Entwicklungsumgebung: Wählen Sie zuerst im linken Listenfeld des Codefensters die Objektvariable aus und dann im rechten Listenfeld das gewünschte Ereignis. Die Entwicklungsumgebung fügt dann eine Prozedurdeklaration nach dem folgenden Schema in den Code ein: Public Sub obj1_ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueChanged ' ... hier müssen Sie Ihren Code einfügen End Sub
Selbstverständlich können Sie die Deklaration für die Ereignisprozedur auch per Tastatur einfügen. Dabei müssen Sie auf die korrekte Parameterliste achten (sie muss der Event-Deklaration entsprechen). Außerdem muss der eigentlichen Deklaration Handles variablenname.ereignisname folgen, damit VB.NET weiß, mit welchem Ereignis die Prozedur verbunden werden soll. (Der Name der Ereignisprozedur – hier als obj1_ValueChanged – ist dagegen irrelevant.)
AddHandler und RemoveHandler Die zweite Variante zum Empfang von Ereignissen besteht darin, die Objektvariable auf gewöhnliche Art (ohne WithEvents) zu deklarieren und dann mit AddHandler die Adresse der Prozedur anzugeben, die das Ereignis verarbeiten soll. Die Adresse ermitteln Sie mit dem Operator AddressOf. Dim obj2 As New Class1() AddHandler obj2.ValueChanged, AddressOf eventproc
Bei der Deklaration der Ereignisprozedur muss wie bei der ersten Variante auf die korrekte Parameterliste geachtet werden. Der Handles-Zusatz ist dagegen nicht erforderlich. Die Entwicklungsumgebung gibt hier keine Hilfe bei der Deklaration dieser Prozedur. Public Sub eventproc(ByVal obj As Object, ByVal data As Integer) ' ... hier müssen Sie Ihren Code einfügen End Sub
Mit RemoveHandler kann eine Ereignisprozedur wieder deaktiviert werden: RemoveHandler obj2.ValueChanged, AddressOf eventproc
Die Vorgehensweise mit AddHandler hat eine Reihe von Vorteilen:
7.6 Ereignisse und Delegates
265
Sie können auf diese Weise auch Ereignisse empfangen, die in Modulen oder Strukturen ausgelöst werden.
•
Der Ort, an dem die Objektvariablen deklariert werden, spielt keine Rolle. (WithEventsVariablen müssen dagegen als Modul- oder Klassenvariablen deklariert werden.)
•
Die Verbindung zwischen dem Ereignis eines Objekts und der Prozedur erfolgt dynamisch (also während der Programmausführung). Das ist vor allem dann praktisch, wenn während des Programms laufend neue Objekte erzeugt werden, deren Ereignisse verarbeitet werden sollen.
•
Eine Ereignisprozedur kann zur Verarbeitung beliebig vieler Ereignisse (unterschiedlicher Objekte) eingesetzt werden, sofern die Parameterliste der Ereignisse identisch ist.
•
Es ist möglich, mit AddHandler für ein Ereignis (eines Objekts) mehrere Ereignisprozeduren anzugeben. Diese werden der Reihe nach aufgerufen.
VERWEIS
•
Besonders häufig mit Ereignissen zu tun haben Sie es bei der Programmierung von Windows-Anwendungen: dort gilt jeder Mausklick, jede Menüauswahl, jede Änderung der Fenstergröße als Ereignis. Es verwundert daher nicht, dass das Verarbeiten von Ereignissen in den Kapiteln zur Windows-Programmierung einen großen Stellenwert hat. Weitere Informationen finden Sie hier: Einführung, Ereignisprozeduren in den Code einfügen: Abschnitt 13.1.2 Ereignisse bei dynamisch erzeugten Steuerelementen: Abschnitt 14.11.2 Ereignisreihenfolge: Abschnitt 15.1.4 Interna der Ereignisverwaltung: Abschnitt 15.2
Beispiel Die Klasse Class1 besteht aus einer privaten Klassenvariable _x, die über die öffentliche Eigenschaft X gelesen und verändert werden kann. Bei jeder Veränderung von X wird das Ereignis ValueChanged ausgelöst. Als Parameter werden ein Verweis auf das Objekt (also Me) sowie der aktuelle Inhalt von _x übergeben. 'Beispiel oo-programmierung\event-intro Class Class1 Private _x As Integer Public Event ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Public Property X() As Integer Get Return _x End Get
266
7 Objektorientierte Programmierung
Set(ByVal Value As Integer) _x = Value RaiseEvent ValueChanged(Me, Value) End Set End Property Public Sub Clear() X = 0 End Sub End Class
In Module1.Main werden die beiden Prozeduren sub1 und sub2 aufgerufen, die die beiden Mechanismen zur Ereignisverwaltung demonstrieren. sub1 greift auf die Modulvariable obj1 zurück, die mit WithEvents deklariert ist. Deren Ereignisse werden durch obj1_ValueChanged verarbeitet. Module Module1 Dim WithEvents obj1 As Class1 Sub Main() sub1() 'Ereignisaufruf mit WithEvents demonstrieren sub2() 'Ereignisempfang mit AddHandler demonstrieren End Sub Sub sub1() obj1 = New Class1() obj1.X = 3 obj1.X = 4 obj1.Clear() End Sub Public Sub obj1_ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueChanged Console.WriteLine( _ "Ereignis class1.ValueChanged (WithEvents), data={0}", data) End Sub ' weitere Prozeduren in Module1 ... End Module
In sub2 wird eine weitere Variable der Klasse Class1 erzeugt. Diesmal wird die Ereignisprozedur ValueChanged_handler durch AddHandler mit dem Ereignis verbunden. Sub sub2() Dim obj2 As New Class1() AddHandler obj2.ValueChanged, AddressOf ValueChanged_handler obj2.X = 1 obj2.X += 2 obj2.Clear() End Sub
7.6 Ereignisse und Delegates
267
Public Sub ValueChanged_handler(ByVal obj As Object, _ ByVal data As Integer) Console.WriteLine( _ "Ereignis class1.ValueChanged (AddHandler), data={0}", data) End Sub
Das Beispielprogramm liefert folgende Ausgabe im Konsolenfenster: Ereignis Ereignis Ereignis Ereignis Ereignis Ereignis
class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged
7.6.2
Delegates
(WithEvents), (WithEvents), (WithEvents), (AddHandler), (AddHandler), (AddHandler),
data=3 data=4 data=0 data=1 data=3 data=0
Ein Delegate ist ein Objekt der Klasse System.Delegate, in dem die Adresse einer Prozedur oder Methode und (optional) der Verweis auf ein Objekt gespeichert werden. Delegates helfen dabei, Funktionen, Unterprogramme oder Methoden aufzurufen, von denen nur die Adresse bekannt ist. Insofern bieten Delegates eine ähnliche Möglichkeit wie die Verwendung von Funktionszeigern (function pointers) in manchen herkömmlichen Programmiersprachen (z.B. C). Da innerhalb des Delegate-Objekts nicht nur die Adresse, sondern auch Kontextinformationen gespeichert werden, ist es ausgeschlossen, dass eine Prozedur mit einer nicht dazu passenden Parameterliste aufgerufen wird. Delegates stellen also einen Sicherheitsmechanismus dar, der Kompatibilitätsprobleme beim Aufruf von Prozeduren von vorneherein vermeidet. Intern basiert jeder Aufruf einer Ereignisprozedur auf Delegates. Der Mechanismus von Delegates ist aber allgemeingültiger als der von Ereignissen und ermöglicht darüber hinausgehende Anwendungen.
Syntax Bevor eine Prozedur durch die Angabe ihrer Adresse aufgerufen werden kann, sind mehrere Schritte erforderlich. Als Erstes wird mit Delegate Sub oder Delegate Function eine Schablone für die aufzurufende Prozedur definiert. Diese Schablone dient lediglich dazu, die Typen der Parameter sowie (bei Funktionen) den Typ des Rückgabewerts zu spezifizieren. Delegate Function mydelegate(ByVal s As String) As String mydelegate kann nun wie eine neue Klasse (wie ein neuer Typ) verwendet werden. Sie können also Variablen diesen Typs deklarieren. Dim func1 As mydelegate
268
7 Objektorientierte Programmierung
In func1 können Sie nun unter Anwendung von AddressOf die Adresse einer beliebigen Funktion speichern, die der Parameterliste von mydelegate entspricht. AddressOf liefert nicht einfach eine Adresse, sondern ein Delegate-Objekt. Wenn Sie AddressOf auf eine Methode (nicht auf eine Prozedur) anwenden, dann enthält das von AddressOf erzeugte Delegate-Objekt auch einen Verweis auf die Objektinstanz, auf die die Methode angewendet werden soll. func1 = AddressOf my_string_function
Die Deklaration von my_string_function muss dem folgenden Muster entsprechen: Function my_string_function(ByVal s As String) As String ... Code der Funktion End Function
Jetzt können Sie die Methode Invoke auf die Variable func anwenden, um die Funktion aufzurufen. An Invoke müssen Sie die bei der Delegate-Definition angegebenen Parameter übergeben. Bei Funktionen liefert Invoke auch den Rückgabewert (ebenfalls im Typ der DelegateDefinition). Dim s As String s = func3.Invoke("abc")
Derselbe Mechanismus funktioniert natürlich nicht nur für Funktionen oder Prozeduren, sondern auch für die Methoden einer Klasse.
Beispiel Im Mittelpunkt dieses Beispiels steht das Unterprogramm WriteStrings. An diese Prozedur wird ein String-Feld sowie eine Funktion zur Verarbeitung von Zeichenketten übergeben. Diese Prozedur ist durch die Delegate-Deklaration string_delegate deklariert. Sie erwartet als Parameter eine Zeichenkette und liefert als Ergebnis wieder eine Zeichenkette zurück. WriteStrings wendet nun diese Funktion auf jedes Element des String-Felds an und schreibt die Resultate in das Konsolenfenster. Mit anderen Worten: An WriteStrings kann eine beliebige, einparametrige Funktion zur Verarbeitung von Zeichenketten übergeben werden. In Main() wird WriteStrings dreimal aufgerufen. Als String-Feld wird jeweils eine Aufzählung von Buchautoren übergeben. Als Funktion zur Bearbeitung der Zeichenketten wird mit Hilfe des Delegate-Mechanismus einmal die Funktion first_word, einmal die Funktion last_word und einmal die VB.NET-Funktion Strings.StrReverse übergeben. ' Beispiel oo-programmierung\delegates-intro Module Module1 Sub Main() Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"}
7.6 Ereignisse und Delegates
269
Dim func1, func2, func3 As string_delegate func1 = AddressOf first_word func2 = AddressOf last_word func3 = AddressOf Strings.StrReverse WriteStrings(s, func1) WriteStrings(s, func2) WriteStrings(s, func3) End Sub Sub WriteStrings(ByVal s As String(), ByVal func As string_delegate) Dim item, tmp As String For Each item In s tmp = func.Invoke(item) Console.Write("{0} ", tmp) Next Console.WriteLine() End Sub ' Delegate für eine Funktion, die einen String als Parameter ' erwartet und einen String zurückgibt Delegate Function string_delegate(ByVal s As String) As String ' zwei Funktionen zur String-Verarbeitung Function first_word(ByVal s As String) As String Dim pos As Integer s = Trim(s) pos = InStr(s, " ") If pos > 0 Then Return (Left(s, pos - 1)) Else Return s End If End Function Function last_word(ByVal s As String) As String ... ähnlicher Code wie in first_word End Function End Module
Das Programm liefert folgendes Ergebnis im Konsolenfenster: Holger Frank Dan Brian Gary Jonathan Andrew Schwichtenberg Eller Appleman Bischof Cornell Morrison Troelsen grebnethciwhcS regloH rellE knarF namelppA naD fohcsiB nairB llenroC yraG nosirroM nahtanoJ nesleorT werdnA
270
7.6.3
7 Objektorientierte Programmierung
Delegates und Ereignisse kombinieren
Normalerweise merken Sie nichts davon, dass jeder Ereignisaufruf intern auf Delegates beruht, und es braucht Sie auch gar nicht zu kümmern. Sie können aber eine Delegate-Deklaration dazu verwenden, um mehrere Ereignisse, die sich durch dieselbe Parameterliste auszeichen, einheitlich zu deklarieren. Der Vorteil dieser Vorgehensweise besteht darin, dass Ihre Ereignisse mit Sicherheit zueinander kompatibel sind, dass die Deklaration der Ereignisse übersichtlicher wird und Sie redundanten Code sparen. Die Grundidee besteht darin, zuerst eine Delegate-Deklaration durchzuführen, um darin die Parameterliste für das Ereignis anzugeben. Bei der nachfolgenden Deklaration der Ereignisse können Sie dann auf die Delegate-Deklaration zurückgreifen. Public Delegate Sub mydelegate(ByVal obj As Object, _ ByVal data As Integer, ...) Public Event myEvent1 As mydelegate Public Event myEvent2 As mydelegate Public Event myEvent3 As mydelegate ...
Sie können natürlich auch durch die .NET-Klassenbibliothek vordefinierte Delegates verwenden, um Ereignisse zu deklarieren, die mit vertrauten .NET-Ereignissen kompatibel sind. Die folgende Zeile definiert ein Ereignis, das mit den MouseEvent-Ereignissen aus der Windows.Forms-Bibliothek kompatibel ist. Public Event myevent As Windows.Forms.MouseEventHandler
Der Aufruf und die Verarbeitung von derart deklarierten Ereignissen unterscheidet sich nicht von gewöhnlichen Ereignissen. (Es sei hier aber erwähnt, dass die .NET-Bibliothek unterschiedliche Unterklassen zu System.Delegate kennt: MulticastDelegate, AsyncCallback, EventHandler, CrossAppDomainDelegate etc. Damit können verschiedene Sonderformen beim Aufruf von Funktionen und Ereignisprozeduren realisiert werden. In diesem Buch fehlt allerdings der Platz, um auf die Besonderheiten dieser Klassen einzugehen.)
Beispiel Die Klasse Class1 sieht ähnlich aus wie im Event-Beispiel aus Abschnitt 7.6.1. Der Unterschied besteht darin, dass nun je nachdem, ob _x vergrößert oder verkleinert wird, das Ereignis ValueIncreased oder ValueDecreased ausgelöst wird. (Wenn der Wert sich bei einer Zuweisung nicht ändert, kommt es zu gar keinem Ereignis.) Anstatt bei der Deklaration der beiden Ereignisse jedes Mal die Parameterliste anzugeben, wurde die Delegate-Prozedur valuechanged_delegate deklariert und sozusagen als Muster für die Parameterliste angegeben. ' Beispiel oo-programmierung\delegates-events Class Class1 Private _x As Integer Public Delegate Sub valuechanged_delegate(ByVal obj As Object, _ ByVal data As Integer)
7.6 Ereignisse und Delegates
271
Public Event ValueIncreased As valuechanged_delegate Public Event ValueDecreased As valuechanged_delegate Public Property X() As Integer Get Return _x End Get Set(ByVal Value As Integer) Dim oldx As Integer = _x _x = Value If Value > oldx Then RaiseEvent ValueIncreased(Me, Value) ElseIf Value < oldx Then RaiseEvent ValueDecreased(Me, Value) End If End Set End Property Public Sub Clear() X = 0 End Sub End Class
Die folgenden Zeilen zeigen, wie die ValueDecreased- und -Increased-Ereignisse verarbeitet werden können. Module Module1 Dim WithEvents obj1 As Class1 Sub Main() obj1 = New Class1() obj1.X = 3 obj1.X = -1 obj1.Clear() End Sub Public Sub obj1_ValueDecreased(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueDecreased Console.WriteLine( _ "Ereignis class1.ValueDecreased, data={0}", data) End Sub Public Sub obj1_ValueIncreased(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueIncreased Console.WriteLine( _ "Ereignis class1.ValueIncreased, data={0}", data) End Sub End Module
272
7 Objektorientierte Programmierung
Das Programm liefert folgendes Ergebnis im Konsolenfenster: Ereignis class1.ValueIncreased, data=3 Ereignis class1.ValueDecreased, data=-1 Ereignis class1.ValueIncreased, data=0
7.7
Attribute
Attribute bieten die Möglichkeit, zusätzliche Merkmale einer Klasse, eines Parameters, einer Prozedur etc. anzugeben. Dazu gleich ein paar Beispiele: •
Wenn Sie eine Aufzählung (Enum) mit dem Attribut ausstatten, können die Elemente der Aufzählung durch Or kombiniert werden.
•
Wenn Sie eigene Steuerelemente programmieren, können Sie durch diverse -Attribute angeben, welche Eigenschaften im Eigenschaftsfenster angezeigt werden, wie die Eigenschaften gruppiert werden sollen etc.
•
Bei Prozeduren können Sie durch das Attribut eine zeilenweise Ausführung durch den Debugger verhindern.
•
Bei Klassen oder Strukturen erreichen Sie durch das Attribut <Serializable()>, dass deren Inhalt serialisiert werden kann (etwa zur Übertragung der Daten in oder aus einer Datei bzw. über eine Netzwerkverbindung).
•
In der Datei AssemblyInfo.vb, die zu jedem VB.NET-Projekt gehört, werden mit Attributen diverse Zusatzinformationen angegebenen, die die Assembly beschreiben (also die resultierenden Programmdatei).
Intern handelt es sich bei Attributen um Klassen, die von System.Attribute abgeleitet sind.
Wozu Attribute? Auf den ersten Blick mag es so erscheinen, als wären Attribute eine Spezialform von Klasseneigenschaften. Das ist aber keineswegs der Fall! Die Anwendungsmöglichkeiten von Attributen liegen ganz woanders: •
Attribute stehen nicht nur für Klassen, sondern für fast alle VB.NET-Sprachkonstrukte zur Verfügung. Daher können Sie mit Attributen nicht nur Zusatzeigenschaften von Klassen einstellen, sondern auch besondere Eigenschaften von Variablen, Eigenschaften, Methoden, Aufzählungen, Assemblies etc.
•
Attribute werden als Teil der so genannten Metadaten eines Programms bzw. einer Bibliothek gespeichert. (Die Metadaten beschreiben die äußeren Merkmale aller Klassen, Methoden, Eigenschaften einer ausführbaren .NET-Programmdatei. Der Objektbrowser ist ein Werkzeug, um diese Metadaten in einer verständlichen Form anzuzeigen. Sie können diese Metadaten mit den Klassen des System.Reflection-Namensraum auslesen.)
7.7 Attribute
273
VERWEIS
Aus diesem Grund können Attribute losgelöst von der Programmausführung ausgewertet werden. Beispielsweise haben der Compiler, der Debugger und die Entwicklungsumgebung Zugriff auf die Attribute. Es überrascht daher kaum, dass es eine ganze Reihe von .NET-Attributen gibt, die zur Steuerung des Compilers, Debuggers etc. dienen. In den .NET-Bibliotheken sind zahllose Attribute definiert, mit denen Sie diverse Spezialeffekte erzielen können. Es gibt allerdings keine gesammelte Dokumentation über alle zur Verfügung stehenden Attribute. Einen ersten Überblick geben die Informationen im Rahmen der VB.NET-Sprachdefinition in der Online-Hilfe. Noch viel mehr Attribute entdecken Sie, wenn Sie im Objektbrowser nach Klassen suchen, die mit ...Attribute enden. ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconExamplesOfCustomAttributeUsage.htm
Konkrete Anwendungsbeispiele für Attribute in diesem Buch finden Sie mit Hilfe des Stichwortverzeichnis (Schlagwort Attribute). Insgesamt war es für die in diesem Buch vorgestellten Beispiele nur recht selten erforderlich, Attribute einzusetzen.
Attribute angeben Attribute werden in VB.NET in spitze Klammern gesetzt und der eigentlichen Deklaration vorangestellt. Bei vielen Attributen reicht einfach deren Nennung, um das Attribut zu aktivieren; bei manchen Attributen müssen Sie darüber hinaus Parameter in der Form para:=wert angeben. Die folgenden Zeilen definieren eine Konstantenaufzählung, bei der die einzelnen Konstanten bitweise kombiniert werden dürfen (siehe auch Abschnitt 4.4.2). Enum myPrivileges As Integer ReadAccess = 1 WriteAccess = 2 Execute = 4 End Enum
Die meisten Attributnamen enden mit Attribute. Diese Endung muss allerdings nicht angegeben werden. (Aus diesem Grund finden Sie das Flags-Attribut im Objektbrowser oder in der Online-Hilfe nur unter den Namen FlagsAttribute!) Ein Sonderfall sind Attribute für die Assembly bzw. für eine einzelne Datei der Assembly (ein .NET-Modul): Diese Attribute werden in der Form bzw. <Module: Name()> angegeben.
Eigene Attribute definieren Sie können auch selbst Attribute definieren. Im einfachsten Fall müssen Sie dazu nur eine eigene Klasse von System.Attributes ableiten. Die folgenden Zeilen definieren ein neues Attribut, mit dem Sie eine Kommentarzeichenkette angeben können.
274
7 Objektorientierte Programmierung
' Beispiel oo-programmierung\attributes-intro Public Class CommentAttribute Inherits Attribute Public Text As String End Class
Bei der Anwendung dieses Attributs können Sie die öffentlichen Klassenelemente in der Form para:=wert initialisieren. _ Class class1 ... Klassencode End Class
Attribute auswerten Wenn Sie feststellen möchten, mit welchen selbst definierten Attributen eine Klasse ausgestattet ist, ermitteln Sie mit GetType das Type-Objekt zur Beschreibung der Klasse. Darauf wenden Sie die Methode GetCustomAttributes an. Diese Methode liefert ein Object-Feld zurück. Mit TypeOf können Sie den tatsächlichen Objekttyp feststellen und gegebenenfalls zur Auswertung eine Umwandlung in die eigenen CommentAttribute-Klasse durchführen. ' Beispiel oo-programmierung\attributes-intro Sub Main() Dim x As New class1() Dim obj As Object For Each obj In x.GetType().GetCustomAttributes(False) Console.WriteLine(obj.ToString) If TypeOf obj Is CommentAttribute Then Console.WriteLine(" Text={0}", _ CType(obj, CommentAttribute).Text) End If Next End Sub
7.8
Namensräume
Wie Sie sicher schon festgestellt haben, sind die Namen der .NET-Klassen aus vielen, jeweils durch Punkte getrennten Teilen zusammengesetzt: System.IO.FileInfo, System.Windows.Forms.Button etc. (Vielleicht ist die Abkürzung .NET für dotnet ja eine Referenz auf die Allgegenwart des Punkts?) Der erste Teil des Klassennamens wird als Namensraum bezeichnet. Bei den beiden Beispielen lautet der Namensraum also System.IO bzw. System.Windows.Forms.
HINWEIS
7.8 Namensräume
275
Beachten Sie, dass der eigentliche Klassenname ebenfalls mehrteilig sein kann! Bei System.Windows.Forms.ListView.ListViewItemCollection lautet der Klassenname ListView.ListViewItemCollection, der Namensraum ist abermals nur System.Windows.Forms.
Grundsätzlich wäre es natürlich auch möglich gewesen, allen Klassen einteilige Namen zu geben – also nur FileInfo, Button etc. Die mehrteiligen Namen haben aber Vorteile: •
Es besteht die Möglichkeit, gleichnamige Klassen in unterschiedlichen Namensräumen zu definieren, ohne dass es dabei zu Konflikten kommt.
•
Zusammengehörende Klassen können in einem Namensraum gruppiert werden. Angesichts der Tatsache, dass die .NET-Klassenbibliothek bereits mehrere Tausend Klassen enthält, vergrößert diese Möglichkeit zur hierarchischen Gliederung die Übersicht ganz erheblich.
Soweit es um die Anwendung von .NET-Klassen geht, müssen Sie bei der Deklaration von Variablen oder Parametern entweder den vollständigen Klassennamen angeben oder Sie müssen auf die in Abschnitt 6.2.2 ausführlich beschriebene Anweisung Imports zurückgreifen, um den Tippaufwand zu minimieren.
HINWEIS
Namensräume haben nichts mit Bibliotheken, Programmen oder Assemblies zu tun: So können unterschiedliche Bibliotheken und Klassen für denselben Namensraum zur Verfügung stellen. (Beispielsweise enthalten die Bibliotheken mscorlib.dll und System.dll beide Klassen für den Namensraum System.IO.) Umgekehrt kann ein Programm bzw. eine Bibliothek durchaus Klassen in mehreren Namensräumen zur Verfügung stellen. Beispielsweise sind die in mscorlib.dll enthaltenen Klassen auf mehr als 30 Namensräume verteilt. Der Objektbrowser gruppiert alle Klassen zuerst nach Bibliotheken, dann nach Namensräumen und zuletzt nach den darin enthaltenen Klassen, Aufzählungen etc. Diese Hierarchie ist willkürlich. Der Objektbrowser könnte ebensogut als erste Hierarchieebene den Namensraum wählen und alle dafür verfügbaren Klassen anzeigen, unabhängig davon, aus welcher Bibliothek die Klassen stammen.
Namensraum für eigene Klassen definieren Bei einem eigenen Projekt ergibt sich der Defaultnamensraum für alle Klassen und Module per Default aus dem Projektnamen. Die Entwicklungsumgebung trägt diesen Namen automatisch in das Feld STAMMNAMESPACE des Projekteigenschaftsdialogs ein, wobei für den Namensraum ungültige Zeichen durch andere Zeichen ersetzt oder ganz entfernt werden.
VORSICHT
276
7 Objektorientierte Programmierung
Verwenden Sie nach Möglichkeit keinen in der .NET-Bibliothek vorkommenden Namen einer Klasse oder eines Namensraums als Projektnamen! Wenn Sie Ihr Projekt Button nennen und dann versuchen, die Button-Klasse der Windows.Forms-Bibliothek zu verwenden, gibt es unweigerlich Probleme!
Für den Namensraum gelten im Wesentlichen dieselben Regeln wie für Variablennamen: Erlaubte Zeichen sind alle Buchstaben und Zahlen sowie das Zeichen _. Der Stammnamensraum darf auch aus mehreren Teilen zusammengesetzt werden – etwa abc.def. Nicht zulässig sind die meisten Sonderzeichen, darunter auch der Bindestrich. Wenn Sie im Projekt myprojekt eine Klasse class1 definieren, so lautet deren vollständiger Name myprojekt.class1. Dieser Name ist allerdings nur dann relevant, wenn das Projekt eine Klassenbibliothek ist, die Sie in einem anderen Programm nutzen möchten. Innerhalb des Projekts können Sie auf alle darin definierten Klassen unmittelbar zugreifen. Mit den Anweisungen Namespace name und End Namespace bilden Sie eine neue Namensraumuntergruppe. Der angegebene Name gilt als Ergänzung zum Stammnamensraum, wobei der Punkt zur Trennung der Namensteile nicht angegeben werden darf. NamespaceAnweisungen müssen direkt auf Dateiebene vorgenommen werden. (Namespace darf also nicht innerhalb einer Klasse oder eines Moduls verwendet werden.) Namespace-Anweisungen dürfen ineinander verschachtelt werden.
Beispiel Die folgenden Zeilen demonstrieren die Anwendung der Namespace-Anweisung. Als Stammnamensraum für das Projekt wurde bei den Projekteigenschaften mynamespace angegeben. Beachten Sie insbesondere, dass sich class3 und class4 im selben Namensraum befinden! Innerhalb des Projekts können die Klassen unter den Namen class1, abc.class2 etc. angesprochen werden. Wenn das Projekt dagegen zu einer Bibliothek (DLL-Datei) kompiliert wird und in einem anderen Programm genutzt wird, dann lauten die Klassennamen mynamespace.class1, mynamespace.abc.class2 etc. (Wenn Sie möchten, können Sie diese langen Namen auch innerhalb des Projekts verwenden.) ' Beispiel oo-programmierung\namespace-intro ' der Stammnamensraum lautet mynamespace Module Module1 Sub Main() Dim x1 As New class1() 'oder mynamespace.class1 Dim x2 As New abc.class2() 'oder mynamespace.abc.class2 Dim x3 As New abc.efg.class3() Dim x4 As New abc.efg.class4() Console.WriteLine(x1.GetType.FullName) 'liefert mynamespace.class1 End Sub End Module Class class1 End Class
7.9 Gültigkeitsbereiche (scope)
277
Namespace abc Class class2 End Class Namespace efg Class class3 End Class End Namespace End Namespace Namespace abc.efg Class class4 End Class End Namespace
7.9
Gültigkeitsbereiche (scope)
VERWEIS
Unter welchen Umständen können Sie in Klasse A auf eine Variable in Klasse B zugreifen? Wie kann eine Methode der Klasse C vom Modul D genutzt werden? Dieser Abschnitt gibt Antwort auf diese Fragen, stellt die Schlüsselwörter zur Erweiterung bzw. zur Einschränkung des Gültigkeitesbereichs vor und gibt an, welche Einstellungen per Default gelten. Einen ausgezeichneten (englischen) Artikel zu diesem Thema finden Sie auf den Microsoft-Entwicklerseiten, wenn Sie nach Variable and Method Scope suchen: http://msdn.microsoft.com/library/en-us/dndotnet/html/methodscope.asp
7.9.1
Gültigkeitsbereich definieren
Zur Definition des Gültigkeitsbereichs eines VB.NET-Konstrukts (Variable, Prozedur, Methode, Eigenschaft etc.) können Sie der Deklaration eines der folgenden Schlüsselwörter voranstellen. •
Private bedeutet, dass das Konstrukt (Variable, Prozedur, Methode, Eigenschaft etc.) nur innerhalb der Gültigkeitsebene verwendet werden kann, in der das Konstrukt definiert ist.
Wenn also eine Variable mit Private Dim a innerhalb einer Klasse deklariert ist, dann ist diese Variable von außen hin vollkommen unzugänglich. •
Protected hat fast diesselbe Wirkung wie Private. Der einzige Unterschied besteht darin, dass das Konstrukt auch in vererbten Klassen zugänglich ist.
Wenn Sie also in Klasse X die Anweisung Protected b ausführen und dann Klasse Y von X ableiten, können Sie im Code der Klasse Y auf b zugreifen.
278
•
7 Objektorientierte Programmierung
Friend bedeutet, dass das Konstrukt innerhalb des aktuellen Projekts generell zugänglich ist, aber nicht nach außen hin. Friend ist vor allem bei der Programmierung von
Klassenbibliotheken wichtig. Wenn Sie in der Klasse X die Anweisung Friend c ausführen, können Sie in allen anderen Klassen des aktuellen Projekts darauf zugreifen. Das gilt sowohl für den Code vererbter Klassen als auch für alle Objekte der Klasse X, die in irgendwelchen anderen Klassen oder Modulen verwendet werden. Wenn Sie die aus dem Projekt resultierende Klassenbibliothek aber in einem anderen Projekt einsetzen, ist die Variable c dort unzugänglich (als wäre sie mit Private deklariert). •
Protected Friend kombiniert die Funktionen von Protected und Friend.
•
Public macht das Konstrukt global zugänglich, also sowohl innerhalb des aktuellen
HINWEIS
Projekts als auch außerhalb (wenn das Projekt als Klassenbibliothek eingesetzt wird). Beachten Sie, dass Codedateien keinen Einfluss auf die Gültigkeitsbereiche von VB.NET-Konstrukten haben. Für den Zugriff auf Variablen, den Aufruf von Prozeduren etc. spielt es keine Rolle, ob Klasse A in ClassA.vb und Klasse B in ClassB.vb definiert sind oder ob beide Klassen in derselben Datei definiert sind. Ebenso ist der Dateiname der Codedateien unerheblich.
Beispiel Ausgangspunkt für die folgenden Überlegungen ist die Klasse ClassX. ' Beispiel oo-programmierung\protected-var Class ClassX Private a As Integer Protected b As Integer Friend c As Integer Protected Friend d As Integer Public e As Integer End Class
Wenn Sie im selben Projekt ein Objekt der Klasse ClassX erzeugen, können Sie auf die Variablen c, d und e zugreifen. Dim ox As New ClassX()
'hier zugänglich: ox.c, ox.d, ox.e
Wenn Sie im selben Projekt die Klasse ClassY von ClassX ableiten, können Sie darin auf die Variablen b, c, d und e zugreifen.
7.9 Gültigkeitsbereiche (scope)
279
Class ClassY Inherits ClassX Sub do_something() ' hier zugänglich: b, c, d und e End Sub End Class
Wenn Sie in einem anderen Projekt die Klassenbibliothek mit der Definition von ClassX nutzen und dort eine Objekt dieser Klasse erzeugen, können Sie nur auf die Variable e zugreifen. ' in einem externen Projekt Dim ox As New ClassX() 'hier zugänglich: ox.c, ox.d, ox.e
Wenn Sie in einem externen Projekt ClassX vererben, können Sie auf die Variablen b, d und e zugreifen. ' in einem externen Projekt Class ClassZ Inherits ClassX Sub do_something() ' hier zugänglich: b, d und e End Sub End Class
7.9.2
Defaultgültigkeit
Sie können Variablen, Funktionen, Eigenschaften etc. auch ohne Angabe eines der obigen Schlüsselwörter deklarieren. Das ist der Regelfall in vielen VB.NET-Programmen. Die folgenden Tabellen geben die Defaultgültigkeit diverser VB.NET-Konstrukte in Abhängigkeit vom Deklarationsort an. Beachten Sie insbesondere, dass Variablen in Modulen und Klassen anders behandelt werden als in Strukturen! Defaultgültigkeitsbereich in Codedateien Module
Friend Module
Class
Friend Class
Structure
Friend Struct
Enum
Friend Enum
Defaultgültigkeitsbereich in Modulen Class/Structure/Enum
Public Class/Structure/Enum Class
Function/Sub
Public NotOverridable Function/Sub
Event/Delegate
Public Event/Delegate
Dim/Const
Private Dim/Const
280
7 Objektorientierte Programmierung
Defaultgültigkeitsbereich in Klassen Class/Structure/Enum
Public Class/Structure/Enum
Function/Sub/Property
Public NotOverridable Function/Sub/Property
Event/Delegate
Public Event/Delegate
Dim/Const
Private Dim/Const
Defaultgültigkeitsbereich in Strukturen Class/Structure/Enum
Public Class/Structure/Enum
Function/Sub/Property
Public NotOverridable Function/Sub/Property
Event/Delegate
Public Event/Delegate
Dim/Const
Public Dim/Const
TIPP
Bei der Deklaration von Variablen und Konstanten innerhalb von Prozeduren, Methoden oder Eigenschaften dürfen keine Gültigkeitsbezeichnungen angegeben werden. Die Variablen bzw. Konstanten gelten als lokal, d.h., sie können nur innerhalb der Prozedur verwendet werden. (Variablen können optional als Static deklariert werden. Das bedeutet, dass sie ihren Inhalt zwischen Prozeduraufrufen behalten und dass ihr Inhalt im Code und nicht wie sonst üblich am Stapelspeicher gespeichert wird. Static wird in Abschnitt 5.3.2 näher vorgestellt.) Wie können Sie die Defaultgültigkeit von VB.NET-Konstrukten feststellen? Die naheliegende Antwort wäre vielleicht die Online-Hilfe – aber dort suchen Sie lang und wahrscheinlich vergeblich. Es geht aber viel einfacher: Geben Sie im Codefenster die Deklaration einer Variablen, Prozedur etc. ohne Gültigkeitsbezeichner an und werfen Sie dann einen Blick in die Klassenansicht oder in den Objektbrowser. Dort können Sie die vollständige (interne) Deklaration des Schlüsselworts herausfinden. (Bei der Klassenansicht müssen Sie dazu die Maus über das Icon des Schlüsselworts bewegen – dann erscheint die Deklaration des Schlüsselworts in einem kleinen, gelben ToolTip-Fenster.) Zum Experimentieren können Sie das hier nicht abgedruckte Beispielprogramm ooprogrammierung\scope verwenden. Das Programm erfüllt keine konkrete Aufgabe, enthält aber unzählige, ineinander verschachtelte Deklarationen von Modulen, Klassen etc.
7.10 Syntaxzusammenfassung
7.10
281
Syntaxzusammenfassung
Konstrukte der objektorientierten Programmierung Class name End Class
deklariert eine Klasse.
Module name End Module
deklariert ein Modul.
Structure name End Structure
deklariert eine Datenstruktur.
Enum name End Enum
deklariert eine Konstantengruppe (Aufzählung).
Interface name End Interface
deklariert eine Schnittstelle.
Inherits basisklasse Inherits basisschnittstelle
gibt an, dass die Klasse von basisklasse vererbt wird. Inherits muss am Beginn einer Klassendefinition angegeben werden. Inherits kann auch am Beginn einer Schnittstellendeklaration
angegeben werden. Die neue Schnittstelle erbt damit alle Merkmale der Basisschnittstelle. Bei Schnittstellen ist sogar eine Vererbung mehrerer Basisschnittstellen möglich (während Klassen nur von einer Basisklasse erben können). Implements schnittstelle
gibt an, dass die Klasse oder die Struktur die angegebene Schnittstelle implementiert. Implements muss am Beginn einer Klassen- oder Strukturdefinition angegeben werden (aber nach Inherits).
stellt der Deklaration eine Attributangabe voran.
Elemente von Klassen Dim/Const x
deklariert eine Klassenvariable bzw. Konstante.
Sub/Function m(...)
deklariert eine Prozedur (privat) oder eine Methode (öffentlich).
Property p() As String Get ... Return wert End Get Set(Value As String) ... End Set End Property
deklariert eine Eigenschaft. Der Get-Teil wird ausgeführt, wenn die Eigenschaft p gelesen wird, der Set-Teil, wenn sie verändert wird. Dabei wird der neue Wert im Parameter Value übergeben. Wenn die Eigenschaft als ReadOnly deklariert ist, entfällt der Set-Teil. Analog entfällt der Get-Teil, wenn die Eigenschaft als WriteOnly deklariert wurde.
282
7 Objektorientierte Programmierung
Elemente von Klassen ... Implements interface.member
gibt an, dass die voranstehende Eigenschaft oder Methode bzw. das Ereignis zur Realisierung einer Schnittstelle dienen.
Zugriff auf die aktuelle Instanz innerhalb des Klassencodes Me
verweist auf die aktuelle Instanz der Klasse.
MyBase
verweist ebenfalls auf die aktuelle Instanz, ermöglicht aber die Verwendung von Schlüsselwörtern der Basisklasse.
MyClass
verweist ebenfalls auf die aktuelle Instanz, erzwingt aber in jedem Fall die Verwendung von Eigenschaften oder Methoden, die in der aktuellen Klasse definiert sind (selbst dann, wenn aufgrund des Objekttyps eigentlich in einer abgeleiteten Klasse definierte Overrides-Eigenschaften oder -Methoden aufgerufen werden müssten).
Namensraum Namespace abc End Namespace
ergänzt den Namensraum um .abc. (Als Ausgangspunkt gilt der Stammnamensraum aus den Projekteigenschaften.) Die Einstellung gilt für alle darin deklarierten Module, Klassen etc.
Imports name
gibt an, dass bei Deklarationen von Variablen, Parametern etc. der Klassenname im angegebenen Namensraum gesucht werden soll. (Beachten Sie, dass weitere Importe in den Projekteigenschaften eingestellt werden können.)
Ereignisse und Delegates Ereignisse Event ev(parameter)
deklariert ein Ereignis (z.B. innerhalb des Codes einer Klasse).
Delegate Sub deleg(parameter) Event ev As deleg
deklariert ein Ereignis, wobei die Parameterliste nicht direkt, sondern durch eine Delegate-Definition angegeben wird.
RaiseEvent ev(argumente)
löst ein Ereignis aus (ebenfalls innerhalb der Klasse).
Dim WithEvents obj1 _ As New class1() Sub name1(parameter) _ Handles obj1.ev
name1 verarbeitet das Ereigniss ev des Objekts obj1 der Klasse class1. Die Prozedur wird jedes Mal aufgerufen, wenn innerhalb der Klasse RaiseEvent ausgeführt wird.
7.10 Syntaxzusammenfassung
283
Ereignisse Dim obj2 As New class1() AddHandler obj2.ev, _ AddressOf name2 Sub name2(parameter)
name2 verarbeitet das Ereignis ev des Objekts obj2 der Klasse class1. Diese Vorgehensweise ist eine syntaktische
RemoveHandler obj2.ev, _ AddressOf name2
deaktiviert die Ereignisprozedur name2 für das Ereignis obj2.ev.
Alternative zur obigen Variante.
Delegates Delegate Sub d1(parameter) Delegate Function d2(para) _ As typ
deklariert eine Delegate-Klasse zum Aufruf einer Prozedur oder Funktion mit den angegebenen Parametern und Rückgabewerten.
Dim ptr As d2
deklariert ptr als Variable für ein Objekt der Delegate-Klasse d2.
ptr = AddressOf func
speichert in ptr den Zeiger auf die Funktion func. (Genau genommen wird nicht einfach ein Zeiger, sondern ein Delegate-Objekt gespeichert.)
result = ptr.Invoke(parameter)
ruft die Funktion func auf.
Optionale Kennzeichner Optionale Kennzeichner zur Deklaration des Gültigkeitsbereichs Private
beschränkt die Gültigkeit auf das Klasseninnere.
Protected
erlaubt den Zugriff nur in vererbten Klassen.
Friend
erlaubt den Zugriff nur im Code desselben Projekts. Friend kann mit Protected kombiniert werden.
Public
macht das Element öffentlich.
Die folgenden Kennzeichner können nur für bestimmte Elemente einer Klasse verwendet werden, beispielsweise Default nur für Eigenschaften. Das oder die Elemente sind in der zweiten Spalte fett hervorgehoben. Optionale Kennzeichner von Konstrukten und Elementen Default
gibt an, dass die Eigenschaft die Defaulteigenschaft der Klasse ist. Das ermöglicht einen Zugriff auf diese Eigenschaft ohne explizite Nennung.
284
7 Objektorientierte Programmierung
Optionale Kennzeichner von Konstrukten und Elementen MustInherit
gibt an, dass die Klasse nicht direkt genutzt werden kann (dass also kein Objekt dieser Klasse erzeugt werden kann). Die Klasse kann damit ausschließlich als Basisklasse für die Definition einer anderen Klasse dienen.
MustOverride
gibt an, dass die Eigenschaft oder Methode nicht direkt verwendet werden kann, sondern in einer abgeleiteten Klasse durch eine eigene Implementierung mit Overrides ersetzt werden muss.
NotInheritable
gibt an, dass diese Klasse nicht vererbt werden kann.
NotOverridable
gibt an, dass die Eigenschaft oder Methode in einer abgeleiteten Klasse nicht durch Overrides ersetzt werden darf.
Overloads
gibt an, dass die Eigenschaft oder Methode eine von der Basisklasse vorgegebene Eigenschaft oder Methode ersetzt. Die Ersetzung gilt nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Overloads kann nicht verwendet werden, wenn sich der Rückgabetyp ändert.
Overridable
gibt an, dass die Eigenschaft oder Methode durch eine vererbte Klasse ersetzt werden darf.
Overrides
gibt an, dass die Eigenschaft oder Methode eine von der Basisklasse vorgegebene Eigenschaft oder Methode ersetzt. Overrides kann nur verwendet werden, wenn das Basisschlüsselwort als Overridable gekennzeichnet ist, wenn die Datentypen der Parameter bzw. des Rückgabewerts unverändert bleiben und die Gültigkeitsebene nicht erweitert wird.
ReadOnly
gibt an, dass die Eigenschaft oder Klassenvariable nur gelesen, aber nicht verändert werden darf.
Shadows
gibt an, dass ein Klassenmitglied in einer vererbten Klasse alle gleichnamigen Klassenmitglieder der Basisklasse überdeckt. Die Überdeckung gilt nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Shadows erlaubt auch die Überdeckung unterschiedlicher Klassenmitglieder (z.B., dass eine Eigenschaft eine Variable überdeckt).
Shared
gibt an, dass die Klassenvariable zwischen allen Objekten der Klasse geteilt wird bzw. dass die Eigenschaft oder Methode ohne eine Instanz der Klasse verwendet werden kann.
Static
gibt an, dass die Prozedurvariable bei mehrfachen Prozeduraufrufen ihren Wert behalten soll.
7.10 Syntaxzusammenfassung
Optionale Kennzeichner von Konstrukten und Elementen WriteOnly
gibt an, dass die Eigenschaft nur verändert, aber nicht gelesen werden darf.
285
Teil III
Programmiertechniken
8
Zahlen, Zeichenketten, Datum und Uhrzeit
Dieses Kapitel beschreibt ausführlich den Umgang mit Zahlen, Zeichenketten, Datum und Uhrzeit. Vielleicht fragen Sie sich, was es über dieses Thema viel zu schreiben gibt. Aber die Fülle neuer Datentypen und eine Unzahl von Methoden, die bei der Manipulation, Umwandlung und Formatierung helfen, fordern auch beim Programmierer ihren Tribut: nicht weil die Anwendung schierig wäre, sondern weil es gilt, den Überblick zu bewahren und für eine bestimmte Aufgabe die richtige Methode auszuwählen. Das Kapitel geht auch auf Typenkonvertierung ein, die VB.NET in manchen Fällen automatisch durchführt und die Sie ansonsten selbst initiieren müssen. Das Thema ist zwar bei den in der Kapitelüberschrift genannten Datentypen besonders wichtig, spielt aber auch bei allen anderen Datentypen und Klassen eine große Rolle. 8.1 8.2 8.3 8.4 8.5 8.6
Zahlen Zeichenketten Datum und Uhrzeit Konvertierung zwischen Datentypen .NET-Formatierungsmethoden VB-Formatierungsmethoden
290 298 319 331 339 349
290
8 Zahlen, Zeichenketten, Datum und Uhrzeit
8.1
Zahlen
8.1.1
Notation von Zahlen
Fließkommazahlen: Bei Fließkommazahlen müssen Sie im Programmcode immer einen Punkt zur Dezimaltrennung verwenden (also 3.1415 statt 3,1415). Sehr große oder sehr kleine Fließkommazahlen können Sie im Programmcode in der wissenschaftlichen Notation angeben, also etwa 3.1E10, wenn Sie 3.1*1010 meinen. Visual Basic ersetzt diese Eingabe dann automatisch durch 31000000000. Nur bei wirklich großen Zahlen belässt Visual Basic es bei der Dezimalnotation (3.1E+50). Hexadezimale Schreibweise: Normalerweise interpretiert Visual Basic Zahlen natürlich im dezimalen System. Nur wenn einer Zahl &H oder &O vorangestellt wird, betrachtet Visual Basic diese Zahl als hexadezimal bzw. oktal. Dim i = i = i = i =
i As Integer, s As String &H10 'i = 16 &HFFF0 'i = 65520 &HFFFFFFF0 'i = -16 &O10 'i = 8
Für Konvertierungen in die umgekehrte Richtung dienen die Funktionen Hex und Oct. Sie liefern Zeichenketten von Zahlen in hexadezimaler bzw. oktaler Schreibweise (ohne vorangestelltes &H bzw. &O). s = Hex(100) s = Hex(-100) s = Oct(100)
's = "64" 's = "FFFFFF9C" 's = "144"
Literale Ganze Zahlen im Programmcode gelten immer als Integer-Zahlen, Fließkommazahlen immer als Double-Zahlen. Sie können Zahlen aber auch explizit einen Datentyp zuweisen, indem Sie einen Buchstaben hintanstellen. So gilt 23L etwa als Long-Zahl. Besonders deutlich bemerken Sie den Unterschied, wenn Sie negative Zahlen mit Hex in die hexadezimale Schreibweise umwandeln: Hex(-3) liefert FFFFFFFD (Integer). Hex(-3S) liefert FFFD (Short). Hex(-3L) liefert FFFFFFFFFFFFFFFD (Long).
Sie können den Datentyp natürlich mit TypeName überprüfen: TypeName(3!) liefert beispielsweise Single.
8.1 Zahlen
291
Literale zur Kennzeichnung von Datentypen C
Char (einzelnes Unicode-Zeichen, z.B. "x"c)
D oder @
Decimal (Festkommazahl mit 28 Stellen Genauigkeit)
F oder !
Single (Fließkommazahl mit 8 Stellen Genauigkeit)
I oder %
Integer (32-Bit-Integer mit Vorzeichen, Default bei ganzen Zahlen)
L oder &
Long (64-Bit-Integer mit Vorzeichen)
R oder #
Double (Fließkommazahl mit 16 Stellen Genauigkeit, Default bei
Fließkommazahlen) S
8.1.2
Short (16-Bit-Integer mit Vorzeichen)
Rundungsfehler bei Fließkommazahlen
Prinzipbedingt treten bei den Datentypen Double und Single immer Rundungsfehler auf. Diese Fehler resultieren aus der internen Darstellung der Zahlen und sind nichts, was Sie Microsoft vorwerfen können. (Die Rundungsfehler treten nur im Bereich der Rechengenauigkeit auf – im folgenden Beispiel an der 16ten Nachkommastelle.) Rundungsfehler lassen sich nur verhindern, indem andere Datentypen eingesetzt werden – je nach Anwendung z.B. Integer, Long, Decimal. Vergessen Sie aber nicht, dass Berechnungen mit Decimal-Zahlen viel langsamer sind als mit Double-Zahlen! Im folgenden Beispielprogramm wird zweimal eine Schleife von -1 bis 1 mit einer Schrittweite von 0.1 durchlaufen. Dabei wird als Schleifenvariable zuerst eine Decimal, dann eine Double-Variable verwendet. Abbildung 8.1 beweist, dass im zweiten Fall im Bereich um 0 offensichtliche Rundungsfehler auftreten.
Abbildung 8.1: Rundungsfehler bei der Verwendung von Double-Variablen
292
8 Zahlen, Zeichenketten, Datum und Uhrzeit
HINWEIS
' Beispielprogramm zahlen-zeichenketten/rechengenauigkeit Option Strict On Module Module1 Sub Main() Dim dec As Decimal, dbl As Double ' Schleife mit Decimal-Variable Console.WriteLine("------ mit Decimal -----") For dec = -1 To 1 Step 0.1D Console.Write(dec & " ") Next Console.WriteLine() Console.WriteLine() ' Schleife mit Double-Variable Console.WriteLine("------ mit Double -----") For dbl = -1 To 1 Step 0.1 Console.Write(dbl & " ") Next End Sub End Module
Beachten Sie, dass in der ersten Schleife die Schrittweite 0.1 mit dem Literal D als Decimal-Zahl gekennzeichnet wurde. Vergessen Sie das, kann es sein, dass bereits an dieser Stelle Rundungsfehler auftreten! (Wenn Sie wie der Autor Option Strict verwenden, erinnert Sie die Entwicklungsumgebung bzw. der Compiler an solche kleinen Ungenauigkeiten.)
8.1.3
Division durch null und der Wert unendlich
Die Datentypen Single und Double (nicht aber Decimal) kennen die Werte unendlich bzw. minus unendlich. Bei einer Division durch null (x = y / 0.0) tritt deswegen kein Fehler auf! Stattdessen enthält x nun Double.NegativeInfinity bzw. Double.NegativeInfinity. Diese Werte können mit den Methoden IsInfinity, IsNegativeInfinity bzw. IsPositiveInfinity festgestellt bzw. als -1.#INF bzw. 1.#INF ausgegeben werden. Die folgende Tabelle fasst diese und einige weitere Eigenschaften und Methoden zusammen, die zur Verarbeitung von unendlich bzw. NaN (not a number). Um zu testen, ob die Double-Variable x einen ungültigen Wert enthält, müssen Sie Double.IsNaN(x) ausführen. (Die näher liegende Form x.IsNaN() ist leider nicht vorgesehen.)
8.1 Zahlen
293
Eigenschaften und Methoden von System.Single und System.Double Epsilon
enthält die kleinste darstellbare Zahl, die noch größer als 0 ist (bei Double ca. 4.94065645841247E-324).
MaxValue
enthält die größte darstellbare Zahl (bei Double ca. 1.7976931348623157E+308)
MinValue
enthält die kleinste darstellbare Zahl.
NaN
gibt einen Wert an, der zur Repräsentierung irregulärer Zustände verwendet wird (not a number, -1.#IND).
NegativeInfinity
gibt den Wert minus unendlich an (-1.#INF).
PositiveInfinity
gibt den Wert unendlich an (1.#INF).
IsInfinity
testet, ob der Wert unendlich enthält (egal mit welchem Vorzeichen).
IsNaN
testet, ob es sich um einen ungültigen Wert handelt.
IsNegativeInfinity
testet, ob der Wert negativ unendlich ist.
IsPositiveInfinity
testet, ob der Wert positiv unendlich ist.
8.1.4
Arithmetische Funktionen
Arithmetische Funktionen sind Teil der Systems.Math-Klasse. Statt Sin(x) in VB6 heißt es daher nun Math.Sin(x). Wenn Sie häufig arithmetische Funktionen einsetzen, sollten Sie die Anweisung Imports System.Math verwenden: dann können Sie arithmetische Funktionen wieder wie in VB verwenden. Alle Math-Funktionen erwarten Double-Parameter und liefern Double-Ergebnisse. System.Math – Arithmetische Funktionen und Konstanten E
Eulersche Zahl e (2.71828182845905)
Pi
Kreisteilungszahl π
Abs(x)
Absolutbetrag
Acos(x), Asin(x), Atan(x)
Arcussinus, -cosinus, -tangens
Atan2(x, y)
Arcustangens zu x/y
Cos(x), Sin(x), Tan(x)
Sinus, Cosinus, Tangens
Cosh(x), Sinh(x), Tanh(x)
hyperbolische Funktionen
Exp(x)
Exponentialfunktion (e )
Log(x)
natürlicher Logarithmus zur Basis e
Log10(x)
Logarithmus zur Basis 10
x
294
8 Zahlen, Zeichenketten, Datum und Uhrzeit
System.Math – Arithmetische Funktionen und Konstanten Log(x, b)
Logarithmus zur Basis b
Pow(x, y)
berechnet x (entspricht in VB.NET x^y)
Sign(x)
Signum-Funktion (liefert 1 bei positiven Zahlen, 0 bei 0, -1 bei negativen Zahlen)
Sqrt(x)
Quadratwurzel
TIPP
In der Klasse Microsoft.VisualBasic.Financial stehen einige finanzmathematische Funktionen zur Verfügung. Diese Funktionen können direkt verwendet werden (also ohne vorangestelltes Math).
VORSICHT
y
Anders als in VB6 und vielen anderen Programmiersprachen führen ungültige Berechnungen nicht zu einem Fehler. Math.Sqrt(-1) oder Math.Log(-1) liefern als Ergebnis den Double-Wert -1.#IND. Dieser Wert entspricht dem Zustand not a number, der mit Double.IsNaN(x) festgestellt werden kann.
8.1.5
Zahlen runden und andere Funktionen
VB enthält zwei eigene Rundungsfunktionen: Fix und Int. Fix schneidet einfach den Nachkommaanteil ab. Int verhält sich bei positiven Zahlen gleich, rundet aber bei negativen Zahlen ab. Die Besonderheit dieser beiden Funktionen besteht darin, dass sie für verschiedene Datentypen definiert sind. Wenn Sie an Int beispielsweise einen Double-Parameter übergeben, liefert diese Funktion auch das Ergebnis als Double-Zahl. Übergeben Sie dagegen einen Decimal-Parameter, ist auch das Ergebnis vom Typ Decimal. Microsoft.VisualBasic.Conversion-Methoden Fix(x)
schneidet den Nachkommaanteil ab: Fix(2.9) liefert 2. Fix(-2.9) liefert -2.
Int(x)
rundet zur nächst kleinern Zahl ab: Int(2.9) liefert 2. Int(-2.1) liefert -2.
Die meisten anderen Funktionen, die zum Runden und für vergleichbare Zwecke geeignet sind, befinden sich in System.Math. Diese Funktionen erwarten durchweg Double-Parameter und liefern Double-Ergebnisse. Math.Floor entspricht im Verhalten exakt Int. Der einzige wesentliche Unterschied liegt bei den Datentypen: Floor erwartet einen Double-Parameter und liefert auch das Ergebnis als Double-Zahl. Math.Ceiling funktioniert so ähnlich wie Floor, rundet aber immer auf.
8.1 Zahlen
295
Die einzige Funktion, die wirklich im Sinne des Sprachgebrauchs rundet, ist Math.Round. Hier wird bei einen Nachkommaanteil kleiner 0,5 abgerundet, bei einem Nachkommaanteil größer 0,5 dagegen aufgerundet. Auf den ersten Blick eigentümlich ist das Verhalten allerdings bei einem Nachkommaanteil von genau 0,5: Dort rundet Round zur nächsten geraden (!) Zahl: 1.5 wird ebenso wie 2.5 zu 2 gerundet. (Dieses Verhalten entspricht also nicht ganz der Schulmathematik, in der bei 0,5 immer aufgerundet wird. Der Vorteil von Round besteht aber darin, dass die Summe der Fehler, die beim Runden vieler gleichverteilter Zahlen entsteht, gegen 0 geht.) System.Math – Sonstige Funktionen Ceiling(x)
rundet zur nächstgrößeren ganzen Zahl auf: Ceiling(0.1) liefert 1. Ceiling(-2.9) liefert -2. Ceiling(3) liefert unverändert 3.
Floor(x)
rundet zur nächstkleineren ganzen Zahl ab: Floor(0.1) liefert 0. Floor(-2.1) liefert -3. Floor(3) liefert unverändert 3.
IEEERemainder(x, y)
liefert den Rest einer ganzzahligen Division, also x Mod y; genau genommen wird zuerst Q=x/y berechnet, wobei Q dann zur nächsten ganzen Zahl auf- oder abgerundet wird; IEEERemainder liefert dann x-Q*y als Ergebnis. IEEERemainder(12, 5) liefert 2. IEEERemainder(-12, 5) liefert -2. IEEERemainder(12, -5) liefert 2. IEEERemainder(-12, -5) liefert -2. IEEERemainder(12.1, 5) liefert 2.1 = 12.1 - 2 * 5. IEEERemainder(12, 5.1) liefert 1.8 = 12 - 2 * 5.1.
Min(x ,y), Max(x, y)
liefert die kleinere bzw. größere von zwei Zahlen. Es ist nicht möglich, mehr als zwei Argumente zu übergeben.
Round(x, n)
rundet auf die angegebene Stellenanzahl: Round(3.14159, 3) liefert 3.142.
Die VB-Funktionen CByte, CShort, CInt und CLng sind eigentlich nicht zum Runden von Zahlen gedacht, sondern zur Umwandlung zwischen verschiedenen Datentypen (siehe auch Abschnitt 8.4). Dabei wird wie mit Round gerundet. Im Unterschied zu Round liefern die Funktionen aber keine Double-Zahlen als Ergebnis, sondern jeweils den entsprechenden Datentyp.
296
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-Konvertierungsfunktionen CByte(x)
wandelt x in eine Byte-Zahl um und rundet dabei wie Round: CByte(2.5) liefert 2. CByte(2.51) liefert 3. CByte(3.49) liefert 3. CByte(3.5) liefert 4.
CShort(x)
wie oben, liefert aber eine Short-Zahl.
CInt(x)
wie oben, liefert aber eine Integer-Zahl.
CLng(x)
wie oben, liefert aber eine Long-Zahl.
8.1.6
Zufallszahlen
TIPP
Grundsätzlich gibt es zwei Möglichkeiten, Zufallszahlen zu erzeugen: Entweder setzen Sie die aus VB6 vertrauten Funktionen ein (Microsoft.VisualBasic.VBMath), oder Sie verwenden die neuen .NET-Methoden (System.Random). Beide hier beschriebenen Methoden liefern keine echten Zufallszahlen, sondern nur Pseudozufallszahlen, die anhand relativ einfacher mathematischer Modelle erzeugt werden. Für fortgeschrittene Anwendungen sind diese Zahlen allerdings zu wenig zufällig. Bessere Zufallszahlen liefern die Methoden von System.Security.Crypthography. Die Anwendung dieser Methoden ist allerdings etwas komplizierter; außerdem dauert die Erzeugung von Zufallszahlen dann deutlich länger.
VB-Zufallszahlen Rnd liefert eine 8-stellige Zufallszahl (Single) zwischen 0 (inklusive) und 1 (exklusive). Um Zufallszahlen in einem bestimmten Bereich zu erhalten, müssen Sie mit Rnd weiterrech-
nen: a + Rnd * (b-a) Int(a + Rnd * (b-a+1))
'liefert Zufallszahlen zwischen 'a (inklusive) und b (exklusive) 'liefert ganze Zufallszahlen 'zwischen a (inkl.) und b (inkl.)
Wenn Sie vermeiden möchten, daß Ihr Visual-Basic-Programm nach jedem Start die gleiche Abfolge von Zufallszahlen generiert, dann müssen Sie zum Programmstart das Kommando Randomize ausführen (entweder ohne Parameter oder mit einem pseudo-zufälligen Parameter, der sich etwa aus der Uhrzeit und dem Datum ergibt).
8.1 Zahlen
297
Microsoft.VisualBasic.VBMath-Methoden Rnd()
liefert eine Single-Zufallszahl.
Rnd(x)
liefert für x=0 nochmals dieselbe Zufallszahl, für x<0 immer dieselbe Zufallszahl, wobei x als Startwert für den Zufallszahlengenerator verwendet wird, und für x>0 die nächste Zufallszahl (wie Rnd()).
Randomize
initialisiert den Zufallszahlengenerator mit einem zufälligen Startwert.
Randomize(x)
initialisiert den Zufallszahlengenerator mit dem Startwert x.
.NET-Zufallszahlen Die mscorlib-Bibliothek (Datei mscorlib.dll) stellt in der Klasse System.Random einige Methoden zur Erzeugung von Zufallszahlen zur Verfügung. Damit Sie diese Methoden verwenden können, müssen Sie vorher eine Objekt des Typs Random erzeugen. Die in der folgenden Tabelle genannten Methoden müssen auf dieses Objekt angewandt werden. Random produziert bei jedem Programmstart andere Zufallszahlen. Aus diesem Grund ist eine mit Randomize vergleichbare Methode nicht erforderlich. (Wenn Sie immer wieder dieselben reproduzierbaren Zufallszahlen verwenden möchten – etwa um eine Programmfunktion zu testen –, müssen Sie mit den oben beschriebenen VB-Funktionen arbeiten.)
System.Random-Methoden Next()
liefert eine Integer-Zufallszahl zwischen 0 (inklusive) und 2147483647 (exklusive).
Next(n)
liefert eine Integer-Zufallszahl zwischen 0 (inklusive) und n (exklusive). n muss selbst eine Integer-Zahl größer 0 sein. Next(4) liefert also Zufallszahlen zwischen 0 und 3.
Next(n1, n2)
liefert eine Integer-Zufallszahl zwischen n1 (inklusive) und n2 (exklusive). Next(7, 12) liefert also Zufallszahlen zwischen 7 und 11.
NextBytes(bytearray())
füllt das Byte-Feld mit Zufallsdaten.
NextDouble()
liefert eine Double-Zufallszahl zwischen 0 (inklusive) und 1 (exklusive).
Das folgende Miniprogramm schreibt zehn Zufallszahlen zwischen 1 und 100 (jeweils inklusive) in ein Konsolenfenster.
298
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Module Module1 Sub Main() Dim i As Integer Dim myrand As New Random() For i = 1 To 10 Console.WriteLine(myrand.Next(1, 101)) Next Console.WriteLine("Drücken Sie eine Taste.") Console.ReadLine() End Sub End Module
8.2
Zeichenketten
8.2.1
Grundlagen
VB.NET kennt zwei Datentypen zum Umgang mit Zeichenketten: In Char-Variablen kann ein einzelnes Zeichen und in String-Variablen können Zeichenketten beinahe beliebiger 31 Länge (bis zu 2 Zeichen) gespeichert werden. In beiden Fällen verwendet VB.NET intern Unicode zur Codierung der Zeichen (zwei Byte pro Zeichen).
Notation Zeichenketten werden zwischen zwei Hochkommas eingeschlossen. Wenn Sie das Zeichen " selbst in einer Zeichenkette speichern möchten, wird die Zuweisung ein wenig unübersichtlich: Sie müssen " verdoppeln: Wenn Sie s1 = """abc""efg""" ausführen, enthält s1 anschließend die Zeichenkette "abc"efd". Dim s = s = s =
s As String "abc" "a""bc" """abc"""
'Zeichenkette abc 'Zeichenkette a"bc 'Zeichenkette "abc"
Konvertierung zwischen Char und String Die Umwandlung von Char zu String ist immer problemlos. Den umgekehrten Fall, also etwa die Zuweisung charvariable = stringvariable, akzeptiert der VB.NET-Kompiler dagegen nur bei Option Strict Off. Wenn Sie mit Option Strict On arbeiten, müssen Sie CChar einsetzen. Die folgenden Zeilen geben einige Beispiele, wie die Zuweisung korrekt durchgeführt werden kann. (Die letzte Variante ist am langsamsten.)
8.2 Zeichenketten
Dim c = c = c = c =
299
c As Char, s As String CChar(s) s.Chars(0) GetChar(s, 1) Convert.ToChar(Left(s, 1))
Initialisierung String-Variablen können direkt bei der Deklaration initialisiert werden. Die folgenden Beispiele zeigen einige Syntaxvarianten: Dim Dim Dim Dim Dim Dim
s1 s2 s3 s4 s5 s6
As As As As As As
String = "abc" String = Space(5) New String("a"c, 10) String = StrDup(3, "x") String = LSet("abc", 10) String = RSet("abc", 10)
's1 's2 's3 's4 's5 's6
= = = = = =
"abc" " " "aaaaaaaaaa" "xxx" "abc " " abc"
Space liefert einfach die angegebene Anzahl von Leerzeichen zurück. New String(c, n) liefert ein Zeichenkette, die n Mal das im ersten Parameter angegebene Zeichen enthält. Als Parameter muss ein Char-Zeichen übergeben werden, d.h., es ist nicht möglich, eine Zeichenkette zu vervielfältigen. (Wenn Sie mit Option Strict On arbeiten, müssen Sie den Parameter wie im obigen Beispiel mit dem Literal c als Char-Zeichen kennzeichnen!) StrDup hat dieselbe Funktion wie NewString, akzeptiert aber auch eine normale Zeichen-
kette (von der aber nur das erste Zeichen berücksichtigt wird!). Beachten Sie, dass die Parameterreihenfolge gegenüber New String vertauscht ist. LSet und RSet kopiert eine Zeichenkette in eine Variable und fügt dann so viele Leerzeichen am Ende bzw. am Beginn der neuen Zeichenkette ein, dass diese eine vorgegebene Länge erreicht. Besonders praktisch ist das bei Zahlen, die rechtsbündig in Zeichenketten gespeichert werden sollen (oder müssen): Module Module1 Sub Main() Dim i As Integer, s(9) As String For i = 0 To 9 s(i) = RSet((5 ^ i).ToString, 10) Console.WriteLine("s(" & i & ")=" & s(i)) Next End Sub End Module
Das Programm demonstriert gleichzeitig die wichtige Methode ToString, die in VB.NET auf beinahe jedes Objekt angewandt werden kann – auch auf geklammerte arithmetische oder logische Ausdrücke. Das Programm liefert folgendes Ergebnis:
300
8 Zahlen, Zeichenketten, Datum und Uhrzeit
s(0)= s(1)= s(2)= s(3)= s(4)= s(5)= s(6)= s(7)= s(8)= s(9)=
1 5 25 125 625 3125 15625 78125 390625 1953125
Die Verkettungsoperatoren + und & Mehrere Zeichenketten können mit + zusammengesetzt werden. "ab"+"cd" liefert also "abcd". Noch universeller ist der Operator &, der Daten in anderen Typen (Zahlen, Datum und Uhrzeit) automatisch in Zeichenketten umwandelt. "ab" & 1/3 ergibt damit "ab0,333333333333333". Wie bei anderen Datentypen sind auch bei Zeichenketten die Operatoren =+ und =& zulässig, um einer String-Variablen eine Zeichenkette hinzuzufügen:
VORSICHT
Dim s As String = "abc" s += "efg" 's enthält jetzt "abcefg" s &= 1 / 2 's enthält jetzt "abcefg0,5"
Bei der Umwandlung von Zahlen, Daten und Zeiten in Zeichenketten durch & oder &= wird automatisch die gültige Landeseinstellung berücksichtigt. Das Programm verhält sich daher unterschiedlich, je nachdem, wo es ausgeführt wird. Wenn Sie das nicht möchten, müssen Sie die Konvertierung explizit mit Funktionen wie Str durchführen. (Mehr Informationen zum Thema Konvertierung gibt Abschnitt 8.4.)
Vordefinierte Zeichenketten (Konstanten) Einige oft benötigte Zeichenketten sind als Konstanten vordefiniert – und das gleich doppelt: einmal in Microsoft.VisualBasic.Constants und ein zweites Mal in Microsoft.VisualBasic.ControlChars. Die Konstanten sind dann praktisch, wenn mehrzeilige Zeichenketten (etwa für das Textfeld) oder Tabellen gebildet werden. Ob Sie lieber die aus VB6 vertrauten vbXxx-Konstanten verwenden oder die (mit etwas mehr Tippaufwand verbundenen) neuen ControlChars-Konstanten, ist eine reine Geschmacksfrage.
8.2 Zeichenketten
301
.Constants
.ControlChars
Inhalt
Verwendung
vbBack
ControlChars.Back
Chr(8)
Backspace-Zeichen
vbCr
ControlChars.Cr
Chr(13)
Wagenrücklauf (Carriage Return)
vbCrLf
ControlChars.CrLf
Chr(13)+Chr(10)
Zeilenumbruch unter Windows
vbFormFeed
ControlChars.FormFeed
Chr(12)
neue Seite
vbLF
ControlChars.LF
Chr(10)
neue Zeile (Line Feed)
vbNewLine
ControlChars.NewLine
Chr(13)+Chr(10)
unter Windows wie ControlChars.CrLf
vbTab
ControlChars.NullChar
Chr(0)
Zeichen mit dem Code 0
ControlChars.Quote
Chr(34)
Anführungszeichen "
ControlChars.Tab
Chr(9)
Tabulator
vbVerticalTab ControlChars.VerticalTab Chr(11)
vertikaler Tabulator
Alle Konstanten, die nur ein Zeichen enthalten, sind Char-Konstanten. Lediglich bei [vb]CrLf und [vb]NewLine handelt es sich um String-Konstanten. [vb]NewLine enthalten die Codes zur Markierung einer neuen Zeile. Die Zeichenkette hat je nach Rechner einen unterschiedlichen Wert (vbCrLf unter Windows) und wird dann interessant, falls Visual Basic einmal auch für andere Betriebssysteme zur Verfügung stehen sollte. (Dieselbe Information kann auch mit Environment.NewLine ermittelt werden.)
8.2.2
Methoden zur Bearbeitung von Zeichenketten
Eigenschaften und Methoden zur Bearbeitung von Zeichenketten gibt es wie Sand am Meer. •
Zum einen stehen alle aus VB6 bekannten Methoden (Left, Mid etc.) weiterhin per Default zur Verfügung (Klasse Microsoft.VisualBasic.Strings).
•
Zum anderen enthält die .NET-Bibliothek mscorlib.dll zahllose neue Eigenschaften und Methoden, die auf Char- und String-Variablen angewandt werden können. Diese Schlüsselwörter sind Klassenmitglieder von System.String bzw. System. Char.
Die am häufigsten eingesetzten Methoden werden auf den folgenden Seiten vorgestellt. Einen weitgehenden Überblick geben die Syntaxtabellen am Ende dieses Abschnitts. (Einige Methoden, deren Anwendung unter VB.NET selten sinnvoll sind, werden aus Platzgründen nicht beschrieben.)
302
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VORSICHT
Es gibt zwei große Unterschiede zwischen den beiden Gruppen: • Der Startindex zum Zugriff auf Zeichen ist unterschiedlich: Herkömmliche Zeichenkettenmethoden verarbeiten das erste Zeichen einer Zeichenkette mit dem Index eins: Left(s, 1) liefert also das erste Zeichen der String-Variablen s. Die neuen .NET-Methoden verwenden dagegen den Index null: s.Chars(1) liefert das zweite Zeichen! • Nicht initialisierte Zeichenketten werden unterschiedlich interpretiert: Herkömmliche Zeichenkettenmethoden betrachten nicht initialisierte Zeichenketten so, als enthielten sie "". Len(s) liefert in einem derartigen Fall einfach 0. Die neuen .NET-Methoden (z.B. s.Length) verursachen in diesem Fall dagegen einen Fehler.
Herkömmliche Methoden zur Bearbeitung von Zeichenketten Die drei wichtigsten Methoden sind Left, Mid und Right: Left(s,n) ermittelt die n ersten Zeichen, Right(s,n) die n letzten Zeichen der Zeichenkette. Mid(s,n) liefert alle Zeichen ab dem n-ten Zeichen, Mid(s,n,m) liefert ab dem n-ten Zeichen m Zeichen. Mid kann auch als Befehl verwendet werden, um einige Zeichen einer Zeichenkette zu verändern. In allen Methoden wird das erste Zeichen mit n=1 angesprochen (nicht n=0).
TIPP
Dim s As String s = "abcdef" Mid(s, 3)="12"
's enthält jetzt "ab12e"
In Windows-Programmen müssen Sie statt Left und Right die etwas umständlichere Schreibweise Strings.Left bzw. Strings.Right verwenden, um einen Konflikt mit den Fenstereigenschaften Left und Right zu vermeiden.
Len ermittelt die Anzahl der Zeichen einer Zeichenkette. (Das Ergebnis von Len ist nicht die Anzahl der Bytes!) Len kann auch für alle anderen elementaren VB-Variablentypen verwendet werden und liefert in den meisten Fällen die Anzahl der Bytes, die zur Speicherung der eigentlichen Daten benötigt werden. (Intern benötigt VB aber unter Umständen deutlich mehr Speicher, wie bereits in Abschnitt 4.6.3 ausgeführt worden ist.) UCase wandelt alle Buchstaben in Großbuchstaben um, LCase liefert Kleinbuchstaben. Trim eliminiert die Leerzeichen am Anfang und Ende der Zeichenkette, LTrim und RTrim arbeiten nur auf jeweils einer Seite. (Genau genommen entfernen die Trim-Methoden nicht nur Leerzeichen, sondern so genannten white space. Dazu zählen auch Tabulator-, Zeilen-
trennzeichen und eine Reihe anderer Sonderzeichen. Die vollständige Liste finden Sie in der Online-Dokumentation, wenn Sie im Index nach white space suchen.) Zum Suchen einer Zeichenkette in einer anderen steht die Methode InStr zur Verfügung. Die Methode ermittelt die Position, an der die gesuchte Zeichenkette zum ersten Mal gefunden wird. InStr("abcde", "cd") liefert beispielsweise 3. Wenn die Suche erfolglos bleibt, gibt die Methode den Wert 0 zurück. Optional kann in einem Parameter angegeben wer-
8.2 Zeichenketten
303
den, an welcher Position die Suche begonnen wird. InStr berücksichtigt Option Compare (siehe Abschnitt 8.2.3), sofern nicht durch einen weiteren optionalen Parameter das gewünschte Vergleichsverhalten vorgegeben wird. InstrRev funktioniert wie Instr, durchsucht die Zeichenkette aber von hinten. Beispielsweise liefert InstrRev("abcababc","ab") den Wert 6. StrReverse dreht eine Zeichenkette einfach um (das erste Zeichen wird zum letzten). s = StrReverse("abcde")
'liefert "edcba"
Split zerlegt eine Zeichenkette in ein String-Feld. Dabei kann im zweiten Parameter ein beliebiges Trennzeichen angegeben werden. (Per Default wird " " verwendet.) Mit einem weiteren Parameter können Sie die Anzahl der Elemente limitieren. Dim a As String, b() As String, c As String a = "abc efg" b = Split(a) 'liefert b(0)="abc", b(1)="efg"
Die Umkehrmethode zu Split lautet Join und setzt die einzelnen Zeichenketten wieder zusammen. c = Join(b)
'liefert c="abc efg"
Eine Hilfe bei der Verarbeitung des aus Split resultierenden Felds bietet Filter: Die Methode erwartet im ersten Parameter ein eindimensionales Feld mit Zeichenketten und im zweiten Parameter eine Suchzeichenkette. Das Ergebnis ist ein neues Feld mit allen Zeichenketten, in denen die Suchzeichenkette gefunden wurde. Die zulässigen Indizes des Ergebnisfelds können mit UBound und LBound ermittelt werden. Dim x(), y() As String x = Split("abc:ebg:hij", ":") y = Filter(x, "b") 'liefert y(0)="abc", y(1)="ebg" Replace ersetzt in einer Zeichenkette einen Suchausdruck durch einen anderen Ausdruck. Komplexe Suchmuster werden zwar nicht unterstützt, aber für einfache Anwendungen reicht Replace aus. Im folgenden Beispiel werden Kommas durch Punkte ersetzt. s = Replace("12,3 17,5 18,3", ",", ".") Str und Format wandeln Zahlen in Zeichenketten um. Val liefert den Wert einer
VERWEIS
Zahl. Diese und andere Umwandlungsmethoden werden im nächsten Abschnitt ausführlicher beschrieben. Einfache Kommandos zur Ein- und Ausgabe von Zeichenketten sind beispielsweise MsgBox, Windows.Forms.MessageBox und InputBox: MsgBox bzw. die neue Methode MessageBox zeigen die angegebene Zeichenkette in einer Dialogbox an, die mit OK quittiert werden kann. InputBox ermöglicht die Eingabe von Zeichenketten, wobei ein beschreibender Text und eine Defaulteingabe als Parameter übergeben werden können. Alle drei Kommandos werden in Abschnitt 15.5 vorgestellt.
304
8 Zahlen, Zeichenketten, Datum und Uhrzeit
.NET-Methoden zur Bearbeitung von Zeichenketten Unter den .NET-Methoden (System.String.*) befinden sich einige recht praktische Hilfen, um eine Zeichenkette in einer anderen zu finden. So testet etwa s.EndsWith("efgh"), ob s mit den vier angegebenen Buchstaben endet. Analog überprüft s.StartsWith("abc"), ob s mit den drei Buchstaben "abc" endet. An s.IndexOfAny wird ein Char-Feld übergeben. Die Methode sucht nun nach der ersten Stelle in s, das mit einem beliebigen Zeichen aus dem Feld übereinstimmt. Wenn keines der Zeichen gefunden werden kann, liefert die Methode -1 zurück. (Die Variante LastIndexOfAny funktioniert ebenso, beginnt die Suche aber von hinten. An beide Methoden kann der Suchbereich durch zwei weitere, optionale Parameter eingeschränkt werden.) Dim Dim Dim n =
s As String = "ab,cd.ef:g" c() As Char = {"."c, ","c, ":"c} n As Integer s.IndexOfAny(c) 'n enthält 2
Auch zur Bearbeitung von Zeichenketten wartet .NET mit einigen Methoden auf, die VB bisher fehlten: Ausgesprochen praktisch ist etwa Insert, um eine Zeichenkette in eine andere an einer beliebigen Position einzufügen. Als erster Parameter wird die Position angegeben, an der mit dem Einfügen begonnen wird. Dim s1 As String = "abcdef", s2 As String, s3 As String s2 = s1.Insert(2, "XYZ") 's2 enthält "abXYZcdef" Remove entfernt einige Zeichen aus einer der Zeichenketten: s3 = s2.Remove(5, 2)
's3 enthält "abXYZef"
Manchmal sollen Zeichenketten anhand eines einfachen Zahlenwerts identifiziert werden – etwa wenn die Zeichenkette als Schlüssel für einen raschen Zugriff in einer Aufzählung oder in einer Datenbank dienen soll. Die Methode GetHashCode ist dabei eine große Hilfe. Sie liefert einen Integer-Wert, der aus dem Inhalt der gesamten Zeichenkette berechnet wird.
HINWEIS
Beachten Sie bitte, dass der hash-Wert nicht eindeutig ist! Es kann vorkommen, dass zwei unterschiedliche Zeichenketten zufällig denselben hash-Wert liefern. (Es wäre ja ein Wunder, wenn in einer 32-Bit-Zahl der gesamte Inhalt einer beliebig langen Zeichenkette ausgedrückt werden könnte – und Wunder sind in der Informatik selten.) GetHashCode liefert für eine bestimmte Zeichenkette immer wieder denselben hashWert. Mit anderen Worten: wenn zwei String-Variablen denselben Inhalt haben, ist auch ihr hash-Wert derselbe. Nur die umgekehrte Schlussfolgerung ist nicht zuläs-
sig. Interessante Hintergrundinformationen zu GetHashCode finden Sie auch bei der Dokumentation zu System.Object.GetHashCode: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemObjectClassGetHashCodeTopic.htm
VERWEIS
8.2 Zeichenketten
305
Für fast jedes .NET-Objekt steht die Methode ToString zur Verfügung. Damit kann der Inhalt des Objekts (oder zumindest eine Beschreibung, und sei es nur über den Datentyp) in Form einer Zeichenkette ausgedrückt werden. Selbst auf beliebige Ausdrücke können Sie ToString anwenden: (a+3>5).ToString liefert "True" oder "False" (je nach Inhalt von a). (1/3).ToString liefert im deutschen Sprachraum "0,333333333333333".
Weitere Informationen zu ToString finden Sie im Zusammenhang mit den anderen Konvertierungsmethoden in Abschnitt 8.4.
.NET-Methoden zur Bearbeitung von einzelnen Zeichen Auch für den Datentyp System.Char gibt es zahllosen Methoden. Da man mit einem einzelnen Zeichen nicht so viel anstellen kann wie mit einer ganzen Zeichenkette, beschränken sich die meisten dieser Methoden darauf, Informationen über die Art des Zeichens zu geben. Beispielsweise liefert IsDigit(c) das Ergebnis True oder False, je nachdem, ob c eine Ziffer enthält oder nicht. Die wichtigsten IsXxx-Methoden sind in der Syntaxzusammenfassung am Ende dieses Abschnitts aufgezählt.
8.2.3
Vergleich von Zeichenketten
Zeichenketten können mit den Operaten =, < und > ohne Probleme verglichen werden. Der Vergleich wird allerdings binär durchgeführt (d.h., es wird die binäre Repräsentierung der Zeichenketten miteinander verglichen). Wenn Sie auf dieser Basis eines binären Vergleichs einen Sortieralgorithmus programmieren, gelten Großbuchstaben kleiner als Kleinbuchstaben, deutsche Sonderzeichen kleiner als alle ASCII-Zeichen etc. Zahlen werden entsprechen ihrer Zeichen (und nicht entsprechend ihres Werts) sortiert. Für einige Testzeichenketten gilt somit die folgende Ordnung: 100 < 27 < ABC < Abcd < Barenboim < Bär < Bären < abc < bar < bärtig < Ärger Wenn Sie am Begin der Codedatei die Option Compare Text einfügen, führt VB.NET Zeichenkettenvergleiche etwas intelligenter aus: Groß- und Kleinbuchstaben werden als gleichwertig betrachtet, deutsche Sonderzeichen werden mit den entsprechenden Buchstaben gleichgesetzt (A=Ä etc.). Es gilt nun die folgende Ordnung: 100 < 27 < ABC < Abcd < abc < Ärger < bar < Bär < Bären < Barenboim < bärtig
StrComp (VB-Methode) Beachten Sie, dass Option Compare Text alle Zeichenkettenvergleiche der gesamten Textdatei betrifft und diese spürbar verlangsamt! Wenn Sie die Groß- und Kleinschreibung sowie ausländische Zeichen nur bei einzelnen Vergleichen korrekt berücksichtigen möchten, sollten Sie die Methode StrComp einsetzen. Bei dieser Methode können Sie das gewünschte Vergleichsverfahren in einem optionalen dritten Parameter angegeben. StrComp liefert 0
306
8 Zahlen, Zeichenketten, Datum und Uhrzeit
zurück, wenn beide Zeichenketten gleich sind, -1, wenn die erste kleiner ist als die zweite, und 1, wenn die erste größer ist.
HINWEIS
ergebnis = StrComp(s1, s2) 'Vergleich je nach Option Compare ergebnis = StrComp(s1, s2, CompareMethod.Binary) ergebnis = StrComp(s1, s2, CompareMethod.Text)
Ein einfaches Beispielprogramm, das Sortierungsvarianten auf der Basis von StrComp und String.Compare demonstriert, finden Sie im Verzeichnis zahlen-zeichenketten\text-compare-sort. Beachten Sie bitte, dass es zum Sortieren von Feldern effizientere Verfahren gibt – siehe Abschnitt 9.3.2!
String.Compare (.NET-Methode) Ähnlich wie StrComp vergleicht die Methode String.Compare zwei Zeichenketten und liefert liefert 0, wenn beide Zeichenketten gleich sind, einen Wert kleiner 0, wenn die erste kleiner ist als die zweite, und einen Wert größer 0, wenn die erste größer ist. Beispielsweise liefert String.Compare("a", "b") das Ergebnis -1. Damit enden die Ähnlichkeiten zu StrComp aber. Großbuchstaben gelten für String.Compare größer als ihr entsprechender Kleinbuchstabe, aber kleiner als der nächste Buchstabe: Es gilt also "a"<"A"<"b". Auch ausländische Zeichen werden (zumindest aus deutscher Sicht) korrekt behandelt: Es gilt "a"<"ä"<"b". Mit dem oben erwähnten Beispielprogramm ergibt sich folgende Ordnung: 100 < 27 < abc < Abc < ABC < Ärger < bar < Bär < Bären < Barenboim < bärtig Zur Standardform gibt es zwei wichtige Varianten: Mit dem optionalen dritten Parameter können Sie angeben, ob Groß- und Kleinbuchstaben gleichwertig behandelt werden sollen. String.Compare("a", "A", True) liefert 0 statt bisher -1. Im vierten Parameter kann ein Globalization.CulturInfo-Objekt übergeben werden, das die gewünschte Sortierordnung angibt. Wenn Sie beispielsweise Zeichenketten entsprechend der schwedischen Ordnung sortieren möchten, verwenden Sie folgenden Code: Dim cultInfo As New Globalization.CultureInfo("sv-SE") If String.Compare(s1, s2, True, cultInfo) > 0 Then ...
Mit dem oben erwähnten Beispielprogramm ergibt sich nun folgende Ordnung. (In Schweden gilt im Wesentlichen dieselbe Sortierordnung wie in Deutschland, allerdings werden die Buchstaben å, ä und ö nach z eingeordnet.) 100 < 27 < abc < Abc < ABC < bar < Barenboim < Bär < Bären < bärtig < Ärger Die Zeichenketten zur Initialisierung von CultureInfo-Objekten für anderen Sprachen können Sie in der Online-Dokumentation nachlesen (z.B. "de" für den deutschen Sprachraum, "en" für den englischen Sprachraum etc.).
8.2 Zeichenketten
307
Mustervergleich (Like) Mit dem Vergleichsoperator Like können Sie testen, ob eine Zeichenkette einem bestimmten Muster entspricht, beispielsweise: If s Like "*1.*" Then ... Like berücksichtigt Option Text (siehe oben), d.h., nur bei Option Text Text werden Groß- und Kleinbuchstaben gleichwertig und ausländische Zeichen korrekt berücksichtigt. Die folgende Tabelle beschreibt die Platzhalter für die Musterzeichenkette.
Platzhalter für das Like-Muster *
beliebig viele beliebige Zeichen
!
genau ein beliebiges Zeichen
#
eine Ziffer
[abc]
eines der Zeichen a, b oder c
[a-f]
eines der Zeichen a bis f
[!abc]
keines der Zeichen a, b oder c
[!a-f]
keines der Zeichen a-f
Abschließend einige Beispiele (durchgeführt bei Option Text Binary, also der VB-Defaulteinstellung für Zeichenkettenvergleiche):
VERWEIS
"x" Like "X" "a" Like "ä" "Visual Basic" Like "Bas" "Visual Basic" Like "*Bas*" "Visual Basic" Like "Vis*" "basic" Like "[a-f][a-f]*" "basic" Like "*[a-f][a-f]" "basic" Like "[!a-f]*" "3.1415927" Like "3.#" "3.1415927" Like "3.#*"
False False False True True True False False False True
Erheblich vielseitigere Möglichkeiten zum Mustervergleich bieten die Methoden aus System.Text.RegularExpression. Diese Methoden sind allerdings auch viel komplizierter in der Anwendung. Weitere Informationen und Anwendungsbeispiele finden Sie hier: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconcomregularexpressions.htm http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301264
308
8 Zahlen, Zeichenketten, Datum und Uhrzeit
8.2.4
Interna
Die wohl ungewöhnlichste Eigenschaft von String-Variablen besteht darin, dass Zeichenketten als unveränderliche Objekte gelten. Jedes Mal, wenn Sie eine Zeichenkette verändern, wird intern die bisherige Zeichenkette als ungültig markiert und Speicher für eine neue Zeichenkette reserviert. Programme, die Zeichenketten häufig ändern, werden daher überdurchschnittlich langsam ausgeführt. Mehr Effizienz für derartige Fälle bietet die StringBuilder-Klasse, die in Abschnitt 8.2.6 beschrieben wird. Die Tatsache, dass Zeichenketten unveränderlich sind, hat aber auch einige überraschende Auswirkungen auf die interne Verwaltung. Im folgenden Beispiel werden a und b als String-Variablen deklariert. Nach den Zuweisungen a="abc" und b=a haben nicht nur beide Variablen denselben Inhalt (das war ja zu erwarten), auch a Is b liefert True! Intern verweisen beide Variablen also auf denselben Ort im Speicher. Die Zeichenkette "abc" befindet sich also nur einmal im Speicher, obwohl es zwei Variablen gibt. Was passiert nun, wenn b nun durch eine weitere Zuweisung geändert wird? Ändert sich damit auch a (nachdem ja beide Variablen auf dieselbe Zeichenkette verweisen)? Die Antwort lautet zum Glück nein. Durch b="xy" wird eine neue Zeichenkette erzeugt (Zeichenketten sind ja unveränderlich)! Intern verweist b nun auf die neue Zeichenkette, während a unverändert auf die bisherige Zeichenkette zeigt. Das folgende Beispielprogramm zahlen-zeichenketten\string-test demonstriert diesen Sachverhalt. Das Ergebnis des Programms ist in Abbildung 6.3 zu sehen.
VERWEIS
Module Module1 Sub Main() Dim a As String = "abc" Dim b As String b = a Console.WriteLine("a=" & a & " b=" & b) Console.WriteLine("a = b: " & (a = b).ToString) Console.WriteLine("a Is b: " & (a Is b).ToString) Console.WriteLine() b = "xy" Console.WriteLine("a=" & a & " b=" & b) Console.WriteLine("a = b: " & (a = b).ToString) Console.WriteLine("a Is b: " & (a Is b).ToString) End Sub End Module
Einige Anmerkungen zu dem nicht immer ohne weiteres nachvollziehbaren Speicherbedarf von String-Variablen finden Sie in Abschnitt 4.6.
8.2 Zeichenketten
309
Abbildung 8.2: Ergebnis des Programms string-test
Nicht initialisierte String-Variablen (Nothing)
VORSICHT
Nicht initialisierte String-Variablen enthalten den Inhalt Nothing. Allerdings verarbeitet die VB.NET-Runtime derartige Zeichenketten, als würden sie die Zeichenkette "" enthalten. Das ist ein VB-Spezifikum, das weder für andere .NET-Sprachen gilt noch für .NET-Methoden, die in VB.NET-Programmen genutzt werden. Aus diesem Grund liefert Len(s) für die nicht initialisierte String-Variable s den Wert 0, s="" wird als True ausgewertet. Wenn Sie versuchen, nicht intialisierte String-Variablen im Kontext von .NET-Methoden zu verwenden (z.B. s.Length), tritt ein Fehler auf. Wenn Sie feststellen möchten, ob eine String-Variable "" enthält oder nicht initialisiert ist, müssen Sie zum Vergleich s Is Nothing oder IsNothing(s) einsetzen. s = Nothing ist für diesen Test ungeeignet, weil es sowohl bei leeren als auch bei nicht initialisierten Zeichenketten True liefert!
8.2.5
Unicode, Eurozeichen
Schon seit VB4 wird der Inhalt von Zeichenketten gemäß Unicode dargestellt. Unicode ist ein Code, der zur Repräsentation jedes Zeichens zwei Byte (65536 Möglichkeiten) vorsieht. Das ermöglicht es, nicht nur alle europäischen Zeichen, sondern auch die meisten anderen auf dieser Welt bekannten Zeichen einheitlich zu verarbeiten. (Allerdings gibt es nur wenige Schriftarten, die die gesamte Breite von Unicode abdecken. Aber an dieser Stelle geht es um die interne Darstellung von Zeichenketten, nicht um die Darstellung aller möglichen Zeichen am Bildschirm.)
Unicode-Grundlagen Unicode beschreibt an sich nur die Zuordnung zwischen Zahlencodes und Zeichen. Beispielsweise ist der Buchstabe A dem Code 65 zugeordnet, das Eurozeichen € dem Code 8364. In VB.NET können Sie die Methoden AscW und ChrW einsetzen, um eine Umwandlung zwischen Codes und Zeichen durchzuführen:
310
Dim c = n = n =
8 Zahlen, Zeichenketten, Datum und Uhrzeit
c As Char, n As Integer ChrW(65) ' c enthält "A"c AscW("B") ' n enthält 66 AscW("€") ' n enthält 8364
Unicode beschreibt allerdings nicht, wie die Codes tatsächlich gespeichert werden. Dazu gibt es wiederum mehrere Möglichkeiten, die als Unicode-Formate bezeichnet werden. Windows und .NET verwenden intern meist UTF-16 (Unicode Transformation Format, 16-bit encoding form). Bei diesem Format werden einfach zwei aufeinander folgende Byte verwendet, um ein Unicode-Zeichen zu speichern. Der größte Vorteil von UTF-16 besteht darin, dass alle Zeichen einheitlich behandelt werden. Der offensichtliche Nachteil ist, dass bei der Speicherung von Texten mit ausschließlich westeuropäischen Zeichen jedes zweites Byte 0 enthält, was natürlich eine Platzverschwendung ist. Innerhalb von UTF-16 gibt es nochmals zwei Varianten, je nachdem, in welcher Reihenfolge die Bytes pro Zeichen gespeichert werden. Auf Rechnern mit Intel-Prozessoren wird beispielsweise das niedrigwertige Byte zuerst gespeichert. (Das bedeutet, dass das Zeichen "A" durch die Byte-Codes 65 und 0 abgebildet wird.) Eine Variante zu UTF-16 ist das vor allem unter Unix/Linux populäre UTF-8. Hier ist die Anzahl der Bytes vom Code abhängig. ASCII-Zeichen (Code < 128) werden mit nur einem Byte gespeichert, andere Zeichen mit zwei bis vier Zeichen. UTF-8 hat zwei Vorteile: Erstens ist ausgeschlossen, dass der Byte-Code 0 innerhalb einer Zeichenkette auftritt (das vermeidet eine Menge Probleme für die Programmiersprache C, die annimmt, dass Zeichenketten mit dem Zeichen 0 enden). Zweitens ist die Speicherung englischer Texte deutlich kompakter als bei UTF-16. .NET unterstützt auch UTF8. Beim Lesen und Schreiben von Textdateien mit den Klassen System.IO.StreamReader bzw. -Writer gilt UTF-8 sogar als Defaultformat.
VORSICHT
Neben UTF-8 und UTF-16 gibt es einige weitere Codierungsmöglichkeiten, die aber relativ selten genutzt werden, z.B. UTF-7 (verwendet bei jedem Byte nur die ersten sieben Bits) und UTF-32 (vier Bytes für jedes Zeichen für einen erweiterten 32-Bit-UnicodeZeichensatz). Unter Windows und speziell in der .NET-Dokumentation ist es üblich, UTF-16 einfach als Unicode zu bezeichnen. Das stiftet bisweilen Verwirrung, weil Unicode allein ja noch keine Informationen darüber gibt, wie die einzelnen Zeichencodes nun tatsächlich zu Bytes angeordnet werden.
ASCII Falls Sie statt des Unicodes den ASCII-Code verwenden möchten, können Sie statt AscW und ChrW die (aus älteren VB-Versionen vertrauten) Methoden Asc und Chr verwenden. Dann ist allerdings bei allen Zeichen mit einem Code > 128 Vorsicht geboten. Beispielsweise liefert Asc("€") den Code 128, Chr(128) entsprechend das Zeichen "€". Asc und Chr sind also zum Unicode inkompatibel.
8.2 Zeichenketten
311
Codierung auflösen oder ändern Normalerweise brauchen Sie sich nicht allzu viele Gedanken darüber machen, wie VB.NET Zeichenketten nun intern codiert. Wirklich interessant wird es aber, wenn Sie Texte bzw. Textdateien aus verschiedenen Quellen und mit unterschiedlicher Codierung verarbeiten sollen. In solchen Fällen helfen die Klassen des System.Text-Namensraum weiter. Sie finden dort unter anderen Klassen für die Formate ASCII, Unicode (UTF-16), UTF-8 und UTF-7. Die Methoden dieser Klassen ermöglichen unter anderem eine Konvertierung zwischen String-Zeichenketten und Byte-Feldern. Das folgende Programm verwendet ein Objekt der Klasse Text.UnicodeEncoding, um die Byte-Codes der Zeichenkette "ABC€" zu ermitteln. Die eigentliche Umwandlung der Zeichenkette in ein Byte-Feld erfolgt mit GetBytes. Dim i As Integer Dim s As String = "ABC€" Dim uni_enc As New System.Text.UnicodeEncoding() Dim b As Byte() = uni_enc.GetBytes(s) For i = 0 To b.Length - 1 Console.Write(b(i) & " ") Next
Das Programm liefert folgendes Ergebnis: 65 0 66 0 67 0 172 32
VERWEIS
Die beiden letzten Codes ergeben mit 172+32*256=8364 den Unicode des Eurozeichens. Eine Einführung in das Thema Zeichenketten-Codierung finden Sie in der Hilfe auf der Seite Codieren von Basistypen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconencodingbasetypes.htm
TIPP
Hinweise zur Speicherung von Text in Dateien finden Sie in Abschnitt 10.5.
Während Zeichenketten VB.NET-intern durch Unicode abgebildet werden, speichert die Entwicklungsumgebung den Programmcode per Default in einem 8-BitCode gemäß der aktuellen Landeseinstellung. Wenn Sie auch im Programmcode beliebige Unicode-Zeichen verwenden möchten, müssen Sie auch den Programmcode als Unicode abspeichern. Dazu führen Sie DATEI|ERWEITERTE SPEICHEROPTIONEN aus und wählen einen der zur Auswahl stehenden Unicodes aus.
312
8 Zahlen, Zeichenketten, Datum und Uhrzeit
8.2.6
Zeichenketten effizient zusammensetzen (StringBuilder)
Wie bereits mehrfach erwähnt, sind Zeichenketten in VB.NET unveränderlich (immutable). Bei jeder String-Operation, die die Zeichenkette verändert (selbst wenn sich dabei die Länge der Zeichenkette nicht verändert oder kleiner wird), wird die alte Zeichenkette verworfen und eine neue erzeugt. (Die alte Zeichenkette wird nach einer Weile automatisch aus dem Speicher entfernt.) Diese Vorgehensweise hat zweifelsohne gute Gründe, aber es gibt Anwendungen, bei denen der daraus resultierende Code geradezu unglaublich langsam wird. Das hinsichtlich der Effizienz schlimmste (aber oft vorkommende) Szenario ist eine Schleife dieser Form: Dim s As String, i As Integer For i = 1 To 10000 s += "die nächste Zeile" + vbCrLf Next i
HINWEIS
Hier wird mit jedem Schleifendurchgang eine neue Zeichenkette erzeugt. Im Verlauf der Schleife müssen daher 10000 neue Objekte im Speicher angelegt und 9999 Objekte entsorgt werden. Darüber hinaus müssen mit jedem Schleifendurchgang größere Zeichenketten von der alten in die neue Variable kopiert werden. Deswegen steigt der Zeitverbrauch dieses Programms mit der Schleifengröße nicht linear, sondern quadratisch an – ein in der Informatik sehr unbeliebter Umstand. In VB6 gab es dieses Problem bei einer Schleife dieses Muster auch schon. Dort bestand die einfachste Abhilfe darin, der Variablen s zuerst eine sehr lange Zeichenkette zuzuweisen und diese dann mit Mid(s, n, m) = ... zu verändern. Dazu war ein bisschen Verwaltungsaufwand erforderlich (etwa um die Größe des bereits genutzten Teils der Zeichenkette auszurechnen), aber der resultierende Code war um Größenordnungen schneller als bei der gewöhnlichen Vorgehensweise. In VB.NET wird aber auch durch Mid(...)=... jedes Mal eine neue Zeichenkette erzeugt, weswegen der ganze Aufwand umsonst ist.
Die StringBuilder-Klasse Zum Glück gibt es in VB.NET eine recht einfache Abhilfe: Die Text.StringBuilder-Klasse ermöglicht es, Zeichenketten zusammenzusetzen, wobei die Zeichenkette möglichst am selben Ort im Speicher bleibt. (Das funktioniert mit Einschränkungen sogar dann, wenn die Zeichenkette größer wird, weil die Größe von StringBuilder-Objekten intern immer ein bisschen überdimensioniert wird, um etwas Spielraum für Vergrößerungen zu lassen.) Der entscheidende Nachteil von StringBuilder-Objekten gegenüber normalen Zeichenketten besteht darin, dass zu ihrer Bearbeitung nur sehr wenige, eher elementare Methoden zur
8.2 Zeichenketten
313
Verfügung stehen. Die folgenden Programmzeilen demonstrieren beispielhaft die wichtigsten Methoden: Dim sb As New System.Text.StringBuilder("abc") 'sb enthält "abc" sb.Append("abc") 'sb enthält "abcabc" sb.Insert(3, "12") 'sb enthält "abc12abc" sb.Remove(3, 4) 'sb enthält "abcc" sb.Replace("c", "X") 'sb enthält "abXX"
Für komplexere Bearbeitungsschritte müssen Sie den StringBuilder-Inhalt mit ToString in eine normale Zeichenkette umwandeln, manipulieren und dann wieder in einer StringBuilder-Variablen speichern. Da es keine Möglichkeit gibt, den aktuellen Inhalt einer StringBuilder-Variablen durch einen neuen Inhalt zu ersetzen, sieht der erforderliche Code recht umständlich aus: Dim s As String s = UCase(sb.ToString) sb.Length=0 sb.Append(s)")
's enthält "ABXX" 'sb enthält "" 'sb enthält "ABXX"
Mit Length können Sie wie bei Zeichenketten die Anzahl der Zeichen ermitteln. Neu im Vergleich zu String-Variablen ist, dass Sie Length auch ändern können. Bei einer Verkleinerung wird die Zeichenkette entsprechend verkürzt. Das Verhalten bei einer Vergrößerung ist unzureichend dokumentiert (oder funktioniert nicht richtig) – sehen Sie davon ab!
VERWEIS
Capacity gibt an, wie groß die Zeichenkette in der StringBuilder-Variablen werden darf, ohne dass neuer Speicher angefordert werden muss. Capacity ist meistens ein bisschen (und manchmal viel) größer als Length, damit Vergrößerungen der Zeichenkette effizient durchgeführt werden können. Selbstverständlich darf die Zeichenkette durch Append auch über das aktuelle Maß von Capacity hinaus vergrößert werden – dann wird automatisch neuer Speicher angefordert und Capacity entsprechend vergrößert.
Zur Zusammensetzung von Zeichenketten können Sie statt Text.StringBuilder auch die Klasse IO.StringWriter verwenden. Intern wird dabei ebenfalls ein StringBuilderObjekt verwendet, nach außen hin verhält sich StringWriter aber wie die StreamWriter-Klasse zum Schreiben einer Textdatei. Sie können die Zeichenkette daher mit Write- oder WriteLine-Methoden zusammensetzen, was in manchen Anwendungen intuitiver ist. Die StringWriter-Klasse wird kurz in Abschnitt 10.5.6 beschrieben. Die hier angegebenen Geschwindigkeitsvorteile gegenüber normalen Zeichenketten gelten auch für StringWriter.
Beispiel Das folgende Beispiel beweist, dass sich der Einsatz der StringBuilder-Klasse wirklich lohnt. Ziel des Programms ist es, einen Zeichenkette mit 5000 Zeilen zu bilden. Jede Zeile enthält das Ergebnis der Berechnung 1/zeilennummer (siehe Abbildung 8.3). Der StringBuilder-Code ist mehr als 500 Mal schneller als der gewöhnliche Code! Das Verhältnis verbessert sich
314
8 Zahlen, Zeichenketten, Datum und Uhrzeit
übrigens weiter zugunsten der StringBuilder-Variante, wenn die Anzahl der Schleifendurchgänge vergrößert wird. (Der Code zur Durchführung der Zeitmessung wird in Abschnitt 8.3.2 näher erläutert.)
Abbildung 8.3: Programm zur Demonstration der StringBuilder-Klasse
' Beispiel zahlen-zeichenketten\stringbuilder Option Strict On Imports System.Console Module Module1 Const loopsize As Integer = 5000 Sub Main() Dim s1, s2 As String Dim starttime, totaltime As Long Write("Variante 1 ohne StringBuilder: ") starttime = Now s1 = variante1() totaltime = Now.Subtract(starttime) WriteLine(Format(totaltime.TotalSeconds, _ "Zeit: #.##### Sekunden")) WriteLine("Inhalt von s2:") WriteLine(Left(s1, 100) + " ...") WriteLine() Write("Variante 2 mit StringBuilder: ") starttime = Now s2 = variante2() totaltime = Now.Subtract(starttime) WriteLine(Format(totaltime.TotalSeconds, _ "Zeit: #.##### Sekunden"))
8.2 Zeichenketten
315
WriteLine("Inhalt von s1:") WriteLine(Left(s2, 100) + " ...") WriteLine("Drücken Sie Return") ReadLine() End Sub Function variante1() As String ' herkömmlich Dim tmp As String Dim i As Integer For i = 1 To loopsize tmp += "Zeile " & i & ": 1/i=" & 1 / i tmp += vbCrLf Next Return tmp End Function ' mit System.Text.StringBuilder Function variante2() As String Dim sb As New System.Text.StringBuilder() Dim nextline As String Dim i As Integer For i = 1 To loopsize nextline = "Zeile " & i & ": 1/i=" & 1 / i & vbCrLf sb.Append(nextline) Next Return sb.ToString End Function End Module
8.2.7
Syntaxzusammenfassung
Zeichenkettenmethoden aus Microsoft.VisualBasic.Strings stehen per Default zur Verfügung. Methoden der String-Klasse müssen dagegen in der Form String.Methode oder stringvariable. Methode aufgerufen werden (es sei denn, Sie fügen am Beginn des Codes Imports String ein.) Beachten Sie auch, dass die VB-Methoden den Index 1 für das erste Zeichen einer Zeichenkette verwenden, die .NET-Methoden dagegen den Index 0. Deklaration, Initialisierung, Verknüpfung Dim s As String [= "abc"]
deklariert s als String-Variable.
Dim c As Char
deklariert c als Char-Variable.
s = "abc"
weist s die Zeichenkette abc zu.
s = "ab""cd"
weist s die Zeichenkette ab"cd zu.
s = Space(n)
weist s eine aus n Leerzeichen bestehende Zeichenkette zu.
316
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Deklaration, Initialisierung, Verknüpfung s1 = LSet(s2, n)
speichert s2 in s1 und fügt daran so viele Leerzeichen an, dass s1 insgesamt n Zeichen lang ist.
s1 = RSet(s2, n)
wie LSet, die Leerzeichen befinden sich nun aber am Beginn der Zeichenkette (nicht am Ende).
s1 + s2
verbindet zwei Zeichenketten.
s&n
verbindet Zeichenketten und andere Datentypen.
s1 += s2
Kurzschreibweise für s1 = s1 + s2.
s1 &= s2
Kurzschreibweise für s1 = s1 & s2.
Herkömmliche Zeichenkettenmethoden Mitglieder von Microsoft.VisualBasic.Strings Left(s, n)
liefert die ersten n Zeichen von s.
Right(s, n)
liefert die letzten n Zeichen von s.
Mid(s, n)
liefert alle Zeichen ab dem n-ten.
Mid(s, n1, n2)
liefert n2 Zeichen ab dem n1-ten.
Mid(s1, n) = s2
verändert einige Zeichen in s1.
Len(s)
liefert die Anzahl der Zeichen.
UCase(s)
wandelt s in Großbuchstaben um.
LCase(s)
wandelt s in Kleinbuchstaben um.
Trim(s)
entfernt Leerzeichen (white space) am Anfang und am Ende.
LTrim(s)
entfernt Leerzeichen am Anfang.
RTrim(s)
entfernt Leerzeichen am Ende.
StrReverse(s)
dreht die Reihenfolge der Zeichen um.
StrDup(n, "x")
vervielfältigt das Zeichen "x" n-Mal.
Split(s, "x")
zerlegt s an den Stellen des Zeichens "x" und liefert das Ergebnis als String-Feld.
Join(sarray, "x")
setzt das String-Feld sarray zu einer einzigen Zeichenkette zusammen, wobei die Teile durch das Zeichen "x" getrennt werden.
Filter(sarray, "abc")
liefert ein String-Feld mit all den Elementen von sarray, die die Zeichenkette "abc" enthalten.
Replace(s, "x", "y")
ersetzt in s alle "x" durch "y".
8.2 Zeichenketten
317
.NET-Zeichenkettenmethoden Wie alle .NET-Klassen enthalten auch die String- und die Char-Klassen zwei grundsätzlich unterschiedliche Typen von Mitgliedern: Shared- und Instance-Mitglieder. Kurz gesagt besteht der Unterschied darin, dass Shared-Mitglieder unmittelbar verwendet werden können (z.B. String.Format(...)), während Instance-Mitglieder als Methoden eines String- oder CharObjekts angewendet werden (z.B. s.EndsWith(...)). Eine detaillierte Beschreibung dieses Unterschieds finden Sie in Abschnitt 6.2.4. In den Syntaxboxen sind Instance-Mitglieder dadurch gekennzeichnet, dass ihnen s oder c als Platzhalter für eine String- oder Char-Variable vorangestellt werden. Wichtige Mitglieder der System.String-Klasse Compare(s1, s2 [,...] )
vergleicht s1 mit s2; Details siehe Syntaxbox Vergleich.
CompareOrdinal(s1, s2)
führt einen Binärvergleich von s1 und s2 durch.
Concat(array())
verbindet die als String-, Char- oder Object-Feld übergebenen Zeichenketten zu einer neuen Zeichenkette.
s.EndsWith(s1)
testet, ob die letzten Zeichen von s mit s1 übereinstimmen.
s.GetHashCode()
liefert einen Integer-Wert zur Identifizierung des Inhalts.
s.IndexOf(s1)
liefert die Position, an der s1 innerhalb von s gefunden wurde (oder -1).
s.IndexOfAny(c())
liefert die Position, an der ein beliebiges der Zeichen aus dem Char-Feld c in s gefunden wurde (oder -1).
s.Insert(n, s1)
fügt s1 an der Position n in s ein und liefert die resultierende Zeichenkette.
s.Length()
liefert die Anzahl der Zeichen von s.
s.LastIndexOf(s1)
wie IndexOf, sucht aber von hinten.
s.LastIndexOfAny(c())
wie IndexOfAny, sucht aber von hinten.
s.Remove(n, m)
entfernt m Zeichen ab der Position n und liefert die resultierende Zeichenkette.
s.Substring(n, m)
liefert m Zeichen ab der Position n.
s.StartsWith(s1)
testet, ob die ersten Zeichen von s mit s1 übereinstimmen.
Wichtige Mitglieder der System.Char-Klasse IsControl(c)
testet, ob c ein Kontrollzeichen ist.
IsDigit(c)
testet, ob c eine Ziffer ist.
IsLetter(c)
testet, ob c ein Buchstabe ist.
IsLetterOrDigit(c)
testet, ob c eine Ziffer oder ein Buchstabe ist.
IsLower(c)
testet, ob c ein Kleinbuchstabe ist.
IsNumber(c)
testet, ob c eine Ziffer einer hexadezimalen Zahl ist (0-9, a-f, A-F).
318
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Wichtige Mitglieder der System.Char-Klasse IsPunctuation(c)
testet, ob c ein Interpunktionszeichen ist (. , : ; ! ? etc.).
IsUpper(c)
testet, ob c ein Großbuchstabe ist.
IsWhiteSpace(c)
testet, ob c ein Trennzeichen zwischen Worten ist (Leerzeichen, Tabulatorzeichen etc.).
Vergleich von Zeichenketten Vergleich Option Compare Text
behandelt Klein- und Großbuchstaben in dieser CodeDatei gleichwertig (gilt nur für VB-, nicht für .NETMethoden!).
StrComp(s1, s2 [,method})
vergleicht s1 mit s2; Ergebnis -1, 0 oder 1.
String.Compare(s1, s2)
vergleicht s1 mit s2; Ergebnis -1, 0 oder 1.
String.Compare(s1, s2, True)
wie oben, behandelt Groß- und Kleinbuchstaben aber gleichwertig.
String.Compare(s1, s2, True, culture)
wie oben, aber unter Berücksichtigung der Sortierordnung eines Landes (culture ist ein Objekt der Klasse System.Globalization.CultureInfo).
s Like muster
führt einen Mustervergleich durch.
StringBuilder System.Text.StringBuilder-Klasse sb.Append(s)
fügt sb die Zeichenkette s hinzu.
sb.AppendFormat(format, obj)
fügt sb die gemäß format formatierte Zeichenkette hinzu.
sb.Capacity
liefert die Größe des reservierten Speichers für die Zeichenkette (in Zeichen).
sb1.Equals(sb2)
testet, ob die beiden StringBuilder identische Zeichenketten enthalten.
sb.Insert(pos, s)
fügt die Zeichenkette s an der Position pos ein.
sb.Insert(pos, s, n)
fügt die Zeichenkette s n-Mal ein.
sb.Length
liefert die tatsächliche Länge der Zeichenkette.
sb.Remove(pos, len)
entfernt len Zeichen ab der Position pos.
sb.Replace(s1, s2)
ersetzt in sb alle Zeichenketten s1 durch s2.
s = sb.ToString()
liefert eine gewöhnliche Zeichenkette.
8.3 Datum und Uhrzeit
8.3
319
Datum und Uhrzeit
Um ein Datum oder eine Uhrzeit zu speichern, verwenden Sie eine Variable des Typs Date (entspricht System.DateTime). Intern werden Zeitangaben in einer Long-Zahl gespeichert, die angibt, wie viele Ticks zu je 100 ns seit dem 1.1.0001 00:00:00 vergangen sind.
HINWEIS
In VB6 wurden Daten und Zeiten intern durch Double-Zahlen ausgedrückt. CDbl(d) lieferte die zugeordnete Fließkommazahl und erleichterte so jede Art von Datumsund Zeitberechnungen. Aufgrund des neuen internen Formats ist die Anwendung von CDbl nun nicht mehr möglich. Es gibt aber neue Methoden zur VB6-kompatiblen Umwandlung in Double-Werte: doublevar = datevar.ToOADate() doublevar = doublevar + 1 'einen Tag hinzufügen datevar = DateTime.FromOADate(doublevar)
Diese Vorgehensweise ist in VB.NET allerdings möglichst zu vermeiden. Neue Methoden zur Addition von Daten und Zeiten werden in Abschnitt 8.3.1 vorgestellt. Weitere Konvertierungsmethoden finden Sie in Abschnitt 8.4.
VERWEIS
Zwei mit Zeit und Datum verwandte Themen werden nicht in diesem Kapitel behandelt: • Mit der Methode Threading.Thread.Sleep(n) können Sie die Programmausführung für eine vorgegebene Zeit in Millisekunden unterbrechen. Die Methode eignet sich vor allem zur Programmierung von Warteschleifen. • Mit dem Timer-Steuerelement können Sie in Windows-Anwendungen eine Prozedur automatisch in regelmäßigen Abständen aufrufen. Das Steuerelement und seine Anwendung wird in Abschnitt 14.10.2 vorgestellt.
Initialisierung von Date-Variablen, Notation von Daten und Zeiten Nicht initialisierte Date-Variablen enthalten den Zeitpunkt 1.1.0001 00:00:00 (d.h. d.Ticks=0). Sie können aber bereits bei der Deklaration verschiedene New-Varianten einsetzen, um die Variable zu initialisieren. Die beiden folgenden Zeilen zeigen die zwei wichtigsten Varianten. Dim d1 As New Date(2002, 12, 31) Dim d2 As New Date(2002, 12, 31, 23, 59, 59)
'31.12.2002 00:00:00 '31.12.2002 23:59:59
Eine weitere Möglichkeit besteht darin, ein Datum oder eine Uhrzeit zwischen zwei #-Zeichen einzuschließen. Die Zeitangabe muss dabei in der amerikanischen Schreibweise erfolgen. (Der Grund dafür ist einfach: Ihr Programmcode muss unabhängig von der Ländereinstellung funktionieren.)
320
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Dim d3 As Date = #12/31/2002# Dim d4 As Date = #12/31/2002 11:59:59 PM# Dim d5 As Date = #11:59:59 PM#
'31.12.2002 00:00:00 '31.12.2002 23:59:59 ' 1. 1.0001 23:59:59
Wenn Ihnen die amerikanische Schreibweise zuwider ist, können Sie auch die beiden Methoden DateSerial und TimeSerial verwenden. Es ist auf diese Weise allerdings nicht möglich, Datum und Zeit anzugeben. Sie können die Zeitangaben auch nicht mit dem Operator + addieren (mehr zu Berechnungen mit Daten und Zeiten siehe etwas weiter unten). d1 = DateSerial(2002, 12, 31) d2 = TimeSerial(23, 59, 59)
'31.12.2002 00:00:00 ' 1. 1.0001 23:59:59
Eine Besonderheit von DateSerial und TimeSerial besteht darin, dass es mit ungültigen Daten intelligent umgeht. Die Uhrzeit 8:70 wird als 8:00 plus 70 Minuten interpretiert und ergibt daher 9:10. d1 = TimeSerial(8, 70, 0)
' 1. 1.0001
9:10:00
Das Datum 31.2.2002 wird in den 3.3. umgerechnet. (Der Februar hat 2002 nur 28 Tage – also ist die Angabe um drei Tage zu groß.) d1 = DateSerial(2002, 12, 31)
' 3. 3.2002 00:00:00
Das Datum 1.14.2002 wird als 1.2.2003 interpretiert. (Da das Jahr nur zwölf Monate hat, sind also zwei Monate zu viel.) d1 = DateSerial(2002, 12, 31)
' 1. 2.2003 00:00:00
Das tolerante Verhalten von Time- und DateSerial ist ausgesprochen praktisch, wenn Sie Berechnungen durchführen möchten (etwa um ausgehend von einem gegebenen Datum einen Monat in die Zukunft zu rechnen): d1 = DateSerial(d1.Year, d1.Month+1, d1.Day)
Recht oft wird es vorkommen, dass Sie nicht eine konkrete Zeit, sondern die aktuelle Zeit in einer Date-Variablen speichern möchten. Abermals gibt es mehrere Möglichkeiten: Die in Microsoft.VisualBasic.DateAndTime definierte Eigenschaft Now liefert die aktuelle Zeit. Today liefert nur das Datum, aber mit der Uhrzeit 00:00:00. TimeOfDay funktioniert gerade umgekehrt und liefert nur die Zeit mit dem Datum 1.1.0001. (Dieser Abschnitt wurde am 29.11.2001 um ca. 9:15 verfasst. Daraus resultieren die folgenden Zeitangaben.) d1 = Now d2 = Today d3 = TimeOfDay
'29.11.2001 9:14:34 '29.11.2001 00:00:00 ' 1. 1.0001 9:14:34
Now und Today stehen aber auch als Methoden von System.DateTime zur Verfügung. Diese Methoden können sowohl für sich als auch auf ein beliebiges DateTime-Objekt angewandt
werden. Daher funktionieren auch die folgenden (gleichwertigen) Zuweisungen: d1 = d1.Now d1 = DateTime.Now
'29.11.2001 '29.11.2001
9:14:34 9:14:34
8.3 Datum und Uhrzeit
321
Schon interessanter ist die DateTime-Methode UtcNow: Sie liefert die so genannte universal time, coordinated. Das ist die Zeit, die intern in allen (insbesondere mit dem Internet verbundenen) Computern verwendet wird. (Es ist erforderlich, dass im internationalen Austausch eine einheitliche Zeit verwendet wird. Wer sollte sonst noch wissen, welche Datei aktueller ist, eine, die um 18:00 Ortszeit in München gespeichert wurde, oder eine, die um 12:30 Ortszeit in New York gespeichert wurde? Für diesen Zweck wird die englische Greenwich Mean Time (GMT) verwendet. UtcNow berücksichtigt die eingestellte Zeitzone, um aus der lokalen Zeit die UTC zu ermitteln.) d4 = DateTime.UtcNow
8.3.1
'29.11.2001
8:14:34
Methoden zur Bearbeitung von Daten und Zeiten
In VB.NET stehen unzählige Methoden zur Auswahl, um den Inhalt von Date-Variablen zu bearbeiten. Diese Methoden sind an zwei Orten definiert: •
Einerseits gibt es die überwiegend schon aus VB6 vertrauten Methoden, die in Microsoft.VisualBasic.DateAndTime definiert sind. Diese Methoden können unmittelbar verwendet werden (z.B. DateSerial).
•
Andererseits können Sie auf die neuen .NET-Methoden zurückgreifen, die Teil von System.DateTime sind. Diese Methoden können zum Teil für sich aufgerufen werden (wie die gerade vorhin vorgestellte Methode DateTime.UtcNow), zum Teil müssen Sie auf Date-Variablen angewandt werden (z.B. d.AddDays).
VERWEIS
Methoden zur Umwandlung von Zeiten in Zeichenketten (etwa der Form 23. November 01) werden zusammen mit zahllosen anderen Konvertierungsmethoden in Abschnitt 8.4 beschrieben.
HINWEIS
Eine Referenz der wichtigsten Schlüsselwörter finden Sie in der Syntaxzusammenfassung am Ende dieses Abschnitts. Im Folgenden werden einige ausgewählte Methoden anhand von Beispielen vorgestellt. Dabei gelten d, d1, d2 etc. als Platzhalter für Date-Variablen.
Das gesamte Buch geht davon aus, dass Sie Daten auf der Basis des bei uns üblichen gregorianischen Kalenders verarbeiten. .NET kommt grundsätzlich auch mit einer Menge anderer Kalender zurecht (beispielsweise dem hebräischen, islamischen, japanischen und koreanischen Kalender). Diese Kalender sind im Namensraum System.Globalization als eigene Klassen definiert, die jeweils eine Menge Methoden zur Umrechnung zwischen verschiedenen Daten bieten.
322
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Extraktion von Detailinformationen Die VB-Methoden Hour, Minute und Second extrahieren die Anzahl der Stunden, Minuten und Sekunden aus der angegebenen Zeit. Year, Month, Day und WeekDay stellen die analogen Methoden für das Datum dar. Wenn WeekDay ohne zusätzliche Parameter verwendet wird, liefert die Funktion 1 für Sonntag, 2 für Montag etc. und schließlich 7 für Samstag. Durch den optionalen zweiten Parameter kann jeder beliebige andere Wochentag als Starttag angegeben werden. n = Hour(d)
Alle oben angegebenen VB-Methoden stehen auch als .NET-Eigenschaften zur Verfügung, so dass auch die folgende Schreibweise zulässig ist. (Statt Weekday(d) müssen Sie allerdings d.DayOfWeek schreiben.) Es ist eine reine Geschmackfrage, welche Schreibweise Ihnen sympatischer erscheint. Die .NET-Schreibweise erspart oft unübersichtliche Klammern. n = d.Hour
Im Reservoir der DateTime-Eigenschaften finden sich auch einige weitere Methoden, die es in dieser Form bisher nicht in VB gab: So liefert d.DayOfYear die Nummer des Tages (1366). Und alle, denen Sekunden zu wenig genau sind, können mit d.MilliSecond die Millisekunden (1/1000 Sekunden) ermitteln. Noch genauer ist nur noch d.Ticks, das die Anzahl der Ticks (100 ns, also 1/10000000 Sekunden) vom 1.1.0001 bis zu d zurückgibt. (Beachten Sie, dass Ticks im Plural steht, alle anderen Methoden dagegen im Singular!) In Date-Variablen sind ja normalerweise Datum und Zeit gemischt. Am einfachsten können Sie die beiden Komponenten mit Date und TimeOfDay aufspalten. Date liefert das reine Datum (mit der Zeit 00:00:00) zurück, TimeOfYear die Uhrzeit ohne Datum. Bei der Anwendung von TimeOfDay müssen Sie allerdings darauf achten, dass diese Methode ein Objekt des Typs System.TimeSpan zurückliefert. Derartige Objekte benötigen Sie beispielsweise, wenn Sie eine Berechnung mit der Methode Add durchführen möchten (siehe etwas weiter unten): Dim ts As TimeSpan ts = d1.TimeOfDay
Mit DateTime.DaysInMonth(2002, 3) können Sie ermitteln, wie viele Tage der März 2002 hat. Es gibt aber erstaunlicherweise keine Methode, die die Anzahl der Monatstage für das aktuelle Datum ermittelt. Die erforderliche Schreibweise ist ein wenig umständlich: n = DateTime.DaysInMonth(d.Year, d.Month)
Wenn Sie wissen möchten, ob 2012 ein Schaltjahr ist, führen Sie DateTime.IsLeapYear(2012) aus. DatePart ermittelt die Anzahl der Perioden für einen bestimmten Zeitpunkt: Bei Jahren wird vom Jahr 0 aus gerechnet, bei Quartalen, Monaten, Wochen, Kalenderwochen ("ww") und Tagen ("y") vom 1.1. des Jahres, bei Monatstagen ("w") vom ersten Tag des Monats, bei Wochentagen ("d") vom ersten Tag der Woche (ohne optionale Parameter ist das der Sonntag) und bei Stunden von 0:00, bei Minuten und Sekunden von der letzten vollen Stunde oder Minute. DatePart erfüllt also in den meisten Fällen dieselbe Aufgabe wie die schon erwähnten Methoden Year, Month, Day, Weekday etc.
8.3 Datum und Uhrzeit
n n n n
= = = =
DatePart("m", DatePart("y", DatePart("d", DatePart("w",
323
Now) Now) Now) Now)
'Anzahl der 'Anzahl der 'Anzahl der 'Anzahl der 'inkl. So.)
Monate seit dem 1.1. Tage seit dem 1.1. Monatstage (für diesen Monat) Wochentage (für diese Woche
Statt der Zeichenketten "m", "y" etc. können auch die DateInterval-Konstanten verwendet werden, beispielsweise: n = DatePart(DateInterval.Month, Now)
'Anzahl der Monate seit 'dem 1.1.
Mit DateDiff können Sie ermitteln, wie viele Zeitintervalle sich zwischen zwei Daten oder Zeiten befinden. Das Intervall wird wie bei DatePart durch eine Zeichenkette angegeben. Die Online-Hilfe beschreibt im Detail, wie die Methode rechnet. (Im Regelfall wird einfach auf das jeweilige Intervall zurückgerechnet. Die Zeitdifferenz vom 31.1. zum 1.2. gilt deswegen als ganzer Monat, während die viel längere Zeitdifferenz vom 1.1. zum 31.1. keinen Monat ergibt. DateDiff muss also mit großer Vorsicht angewendet werden!) DateDiff("m", Today, DateSerial(2010, 1, 1))
'Anzahl der Monate bis 'zum 1.1.2010
Der Datentyp System.TimeSpan VB.NET sieht zur Speicherung von Daten und Zeiten nur den Datentyp Date (entspricht System.DateTime) vor. Genau genommen eignet sich dieser Datentyp allerdings nur dazu, um in großer Genauigkeit einen bestimmten Zeitpunkt zu speichern (z.B. 12:30 am 31.12. 2002). Was aber tun, wenn Sie eine Zeitspanne (zwei Tage, eine halbe Stunde) ausdrücken möchten? Die Lösung für die meisten Probleme bietet der .NET-Datentyp System.TimeSpan, der speziell zur Speicherung von Zeitspannen vorgesehen ist. Wie DateTime wird die Zeitspanne intern als Long-Wert gespeichert, wobei die Zeitspanne ebenfalls in der Einheit Ticks (100 ns) ausgedrückt wird. In TimeSpan-Variablen lassen sich Zeiträume von mehr als 29.000 Jahren speichern – damit ist der Wertebereich noch größer als bei DateTimeVariablen.
ANMERKUNG
Allerdings zählt TimeSpan nicht zu den offiziell von VB.NET unterstützten Variablentypen, weswegen seine Anwendung manchmal ein wenig umständlich ist. Dennoch helfen TimeSpan-Variablen bei einer Vielzahl von Operationen. In VB6 bestand die Lösung übrigens darin, dass auch zur Verarbeitung von Zeitspannen Date-Variablen verwendet wurden. Um eine Zeitspanne von zwei Stunden zu speichern, wurde d einfach #2:00:00# zugewiesen. Damit wurde zwar eigentlich der Zeitpunkt 31.12.1899 2:00:00 gespeichert, aber für die interne Darstellung von Zeiten galt der 31.12.1899 mit dem Wert 0 als Startpunkt der Zeit. Aus diesem Grund konnte man d auch als Zeitspanne seit 31.12.1899 0:00:00 interpretieren, und VB6 ließ tatsächlich entsprechende Berechnungen zu.
324
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Per Default enthalten TimeSpan-Variablen die Zeitspanne 0. Durch verschiedene NewVarianten können TimeSpan-Variablen gleich bei der Deklaration initialisiert werden. Die folgenden Zeilen zeigen einige Beispiele: Dim Dim Dim Dim
ts1 ts2 ts3 ts4
As As As As
TimeSpan New TimeSpan(5) New TimeSpan(2, 30, 0) New TimeSpan(7, 6, 0, 0)
' ' ' '
0 5 2 7
(per Default) Ticks (500 ns) Stunden und 30 Minuten Tage und 6 Stunden
Nach der Initialisierung sind es vor allem verschiedene FromXxx-Methoden, die dabei helfen, einer TimeSpan-Variablen einen bestimmten Wert zu geben. FromDays(3) liefert beispielsweise eine Zeitspanne von drei Tagen. Besonders attraktiv ist dabei, dass an die FromXxx-Methoden Double-Zahlen übergeben werden können. Daher ist es leicht, auch Bruchteile von Zeiten zuzuweisen. ts1 = TimeSpan.FromDays(3) ts2 = TimeSpan.FromSeconds(0.3)
' 3 Tage ' 0,3 Sekunden
Noch flexibler (aber etwas langsamer) ist die Parse-Methode, die als Parameter eine Zeichenkette erwartet. ts1 = TimeSpan.Parse("3.12:30:00")
' 3 Tage, 12 Stunden, 30 Minuten
TimeSpan-Objekte können auch von Date-Variablen abgeleitet werden: Dim d1, d2 As Date ts1 = d1.TimeOfDay ts2 = d1.Subtract(d2)
' Zeitanteil von d1 (ohne Datum) ' Zeitspanne zwischen d1 und d2
Der Inhalt von TimeSpan-Objekten kann wie bei Date-Variablen mit den Eigenschaften MilliSeconds, Seconds, Minutes etc. ausgelesen werden. Darüber hinaus gibt es aber noch eine Reihe von TotalXxx-Eigenschaften (z.B. TotalSeconds, TotalMinutes). Der Unterschied zwischen den beiden wird aus einem Beispiel klar: Dim n1, n2 As Integer, x1, x2 As Double ts = TimeSpan.Parse("0:03:12") ' 3 Minuten, 12 Sek n1 = ts.Seconds ' enthält 12 n2 = ts.Minutes ' enthält 3 x1 = ts.TotalSeconds ' enthält 192 x2 = ts.TotalMinutes ' enthält 3,2
Sobald Sie einmal ein TimeSpan-Objekt haben, können Sie mit den Methoden Add, Negate und Subtract einfache Rechenoperationen durchführen: ts3 = ts1.Add(ts2) ts1 = ts1.Negate() ts3 = ts1.Subtract(ts2)
' entspricht ts3 = ts1 + ts2 ' entspricht ts1 = -ts2 ' entspricht ts3 = ts1 - ts2
Der eigentliche Motivation, ein TimeSpan-Objekt zu erzeugen, besteht aber zumeist darin, dass Sie diese Objekte auch für Rechenoperationen mit Date-Variablen verwenden können – mehr dazu im nächsten Abschnitt.
8.3 Datum und Uhrzeit
325
Berechnungen (Addition, Subtraktion) Wie der vorige Abschnitt zum Datentyp TimeSpan schon vermuten lässt, ist die Durchführung von Zeitberechnungen nicht ganz so trivial, wie man es sich vielleicht wünschen würde. Die Operatoren + und - können leider nicht dazu verwendet werden, einfache Zeitberechnungen durchzuführen. Mit den folgenden Zeilen können Sie in VB6 die Zeit in einer Stunde ausrechnen. In VB.NET erhalten Sie dagegen nur eine Fehlermeldung! (Mit Option Strict können Sie das Programm gar nicht kompilieren. Eine genaue Analyse des Fehlers finden Sie in Abschnitt 4.1.4.) Dim d1, d2, d3 As Date d1 = Now d2 = #1:00:00 AM# d3 = d1 + d2 'Fehler in VB.NET!
Statt + und - müssen Sie daher spezielle Methoden einsetzen, von denen in diesem Abschnitt die wichtigsten vorgestellt werden. Die VB-Methode DateAdd eignet sich dazu, zu einem Datum oder zu einer Uhrzeit ein oder mehrere Zeitintervalle zu addieren. Das Intervall wird in Form eine Zeichenkette angegeben: "yyyy" zum Addieren von Jahren, "q" für Quartale, "m" für Monate, "ww" für Wochen, "y", "w" oder "d" für Tage, "h" für Stunden, "n" für Minuten und "s" für Sekunden. Der zweite Parameter gibt an, wie oft das Intervall addiert werden soll. (Mit negativen Zahlen können Sie auch rückwärts rechnen. Es sind allerdings nur ganze Intervalle möglich, halbe oder viertel Stunden müssen Sie in Minuten rechnen.) Der dritte Parameter enthält die Ausgangszeit: d = DateAdd("yyyy", 1, Now) d = DateAdd("h", -2, Now)
'Datum und Zeit in einem Jahr 'Datum und Zeit vor zwei Stunden
Wenn sich durch die Addition ungültige Daten ergeben (etwa der 31.4., nachdem zum 31.3. ein Monat addiert wurde), ermittelt Visual Basic den ersten gültigen Tag vorher (30.4.). Beachten Sie, daß sich DateSerial hier anders verhält und aus DateSerial(2001,4,31) den 1.5.01 macht! Eine Alternative zu DateAdd sind die .NET-Methoden AddDays, AddHours etc.: Der wesentliche Vorteil dieser Methoden gegenüber DateAdd besteht darin, dass die meisten Methoden (nicht: AddMonths und AddYears) als Parameter eine Double-Zahl akzeptieren. Damit können Sie also ganz einfach eineinhalb Stunden addieren: d1 = d2.AddHours(1.5)
' entspricht d1 = d2 + 1,5 Stunden
Wenn Sie eine Zeit subtrahieren möchten, übergeben Sie einfach einen negativen Parameter. Übrigens können Sie die .NET-Methoden auch direkt auf Now oder auf andere Schlüsselwörter anwenden, die ein Date-Objekt zurückgeben: d1 = Now.AddMinutes(-1)
' entspricht d1 = jetzt vor drei Minuten
Noch allgemeingültiger als AddDays, -Hours etc. sind die Methode Add und Subtract. Damit können Sie wirklich eine beliebige Zeit addieren bzw. subtrahieren. Der wesentliche
326
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Nachteil besteht darin, dass als Parameter nur ein TimeSpan-Objekt erlaubt ist (siehe den vorigen Abschnitt). Dim ts As TimeSpan ts = TimeSpan.Parse("0:03:30") ' 3 Minuten, 30 Sek d2 = d1.Add(ts1) ' entspricht d2 = d1 + ts1 d3 = d1.Subtract(ts2) ' entspricht d2 = d1 - ts1
8.3.2
Beispiele
Einen Monat zu einem Datum addieren Wie immer führen viele Wege zum Ziel. DateAdd ist eine relativ intelligente Methode. Sie berechnet das Datum in einem Monat, d.h., aus dem 15.3. wird der 15.4.. Wenn das Ergebnis der Addition ein ungültiges Datum ist (etwa der 30.2.), liefert DateAdd automatisch ein früheres Datum (also den 28.2. oder 29.2., je nachdem, ob es sich um ein Schaltjahr handelt oder nicht). DateAdd("m", 1, d)
'Datum in einem Monat
Der folgende Ausdruck scheint auf den ersten Blick dieselbe Funktion wie DateAdd zu erfüllen. In den meisten Fällen ist das auch der Fall. DateSerial verhält sich allerdings bei ungültigen Daten anders und macht aus dem 30.2. den 1.3. oder 2.3. DateSerial(d.Year, d.Month + 1, d.Day)
'Datum in einem Monat
Als dritte und wahrscheinlich übersichtlichste Variante bietet sich die .NET-Methode AddMonths an. Sie liefert dieselben Ergebnisse wie DateAdd: d.AddMonths(1)
'Datum in einem Monat
Datum des Monatsendes ermitteln Im folgenden Ausdruck wird aus einem vorgegebenem Datum der 0-te Tag des Folgemonats berechnet. DateSerial betrachtet den Tag 0 wie 'gestern' und liefert als Ergebnis das Datum des letzten Tags des Vormonats. DateSerial(d.Year, d.Month +1, 0)
'Datum zum Monatsletzten
Wenn Sie die Anzahl der Tage des Monats wissen möchten, können Sie an den obigen DateSerial-Ausdruck .Day anhängen. Eine andere Möglichkeit bietet die .NET-Methode DaysInMonth: DateTime.DaysInMonth(d.Year, d.Month)
'Anzahl der Tage des Monats
Auch die folgende Schreibweise ist gültig, wenngleich sie weniger intuitiv ist. (DaysInMonth wird dabei als Methode auf d angewandt. d wird aber gar nicht direkt berücksichtigt, entscheidend sind nur die beiden Parameter. Immerhin ist der Tippaufwand etwas geringer.) d.DaysInMonth(d.Year, d.Month)
'Anzahl der Tage des Monats
8.3 Datum und Uhrzeit
327
Zeitdifferenz in Jahren (Altersberechnung) Zur Berechnung von Zeitdifferenzen ist eigentlich die Methode DateDiff vorgesehen. Diese Methode hat aber ihre Tücken: Sie nimmt nur auf das unmittelbare Intervall Rücksicht. Für die Berechnung des Alters (aus Geburtsdatum und aktuellem Datum) ist das nicht ausreichend, neben dem Geburtsjahr müssen auch Tag und Monat berücksichtigt werden. Dim alter As Integer alter ? DateDiff("yyyy", d1, Today) 'liefert falsches Alter
In der Formel unten wird die reine Differenz der Jahre um ein Jahr korrigiert, wenn das Datum d2 kleiner ist als das Datum, das sich aus dem Jahr d2 sowie Tag und Monat von d1 ergibt. d1 = DateSerial(1967, 10, 31) 'Geburtsdatum d2 = Today 'heute alter = d2.Year - d1.Year - _ IIf(d2 < DateSerial(d2.Year, d1.Month, d1.Day), 1, 0)
Übrigens hilft bei der Altersberechnung auch ein TimeSpan-Objekt nicht weiter. Dieses bietet zwar im Regelfall tolle Möglichkeiten zur Auswertung von Zeiträumen, aber diese Eigenschaften enden mit Days bzw. TotalDays. Größere Zeiträume (Month, Year) können nicht ausgewertet werden (ganz einfach deswegen, weil Monate und Jahre unterschiedlich lang sind).
Zeitmessung Oft wollen Sie die Zeit messen, die die Ausführung eines Programmteils benötigt. Dazu gibt es natürlich viele Möglichkeiten, die alle darauf basieren, den Start- und Endzeitpunkt mit Now festzustellen und in irgendeiner Form die Differenz auszurechnen. Recht elegant ist die folgende Variante auf der Basis einer TimeSpan-Variablen: Dim starttime As Date Dim totaltime As TimeSpan starttime = Now ... ' hier befindet sich der zu messende Code totaltime = Now.Subtract(starttime) Console.WriteLine(Format(totaltime.TotalSeconds, _ "Zeit: #.##### Sekunden"))
Am Beginn der Zeitmessung wird die Startzeit in starttime gespeichert. Zum Ende der Messung wird die Startzeit mit Subtract von Now subtrahiert. Daraus resultiert ein TimeSpan-Objekt. Dessen Eigenschaft TotalSeconds gibt als Double-Zahl die Sekunden (samt Bruchteilen) in ausreichender Genauigkeit an.
328
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VERWEIS
8.3.3
Syntaxzusammenfassung
Die Syntax der wichtigsten Methoden zur Umwandlung von Daten in Zeichenketten folgt in Abschnitt 8.4.
VB-Methoden (Klasse Microsoft.VisualBasic.DateAndTime) Aktuelle Zeit, Initialisierung Now
liefert das aktuelle Datum und die aktuelle Uhrzeit.
Timer
liefert die Sekunden seit Mitternacht als Double-Wert.
TimeOfDay
liefert die aktuelle Uhrzeit (mit dem Datum 1.1.0001).
Today
liefert das aktuelle Datum.
DateSerial(y,m,d)
setzt das Datum aus Jahr, Monat und Tag zusammen (mit der Uhrzeit 00:00:00).
TimeSerial(h, m, s)
setzt die Uhrzeit aus Stunde, Minute und Sekunde zusammen (mit dem Datum 1.1.0001).
Extraktion von Daten Year(d)
liefert das Jahr (1-9999).
Month(d)
liefert den Monat (1-12).
Day(d)
liefert den Tag (1-31).
Weekday(d)
liefert den Wochentag (1 für Sonntag bis 7 für Samstag).
Hour(d)
liefert die Stunde (0-23).
Minute(d)
liefert die Minute (0-59).
Second(d)
liefert die Sekunde (0-59).
DatePart(p, d)
liefert die Anzahl der Perioden p bis zurzeit d.
Zeitberechnungen DateAdd(p, n, d)
liefert d plus n Perioden p. (n darf auch negativ sein.)
DateDiff(p, d1, d2)
liefert die Anzahl der Perioden p zwischen d1 und d2.
Angabe der Periode p in DateAdd, DateDiff und DatePart "yyyy"
oder
DateInterval.Year
Jahre
"q"
oder
DateInterval.Quarter
Quartale
oder
DateInterval.Month
Monate
"m"
8.3 Datum und Uhrzeit
329
Angabe der Periode p in DateAdd, DateDiff und DatePart oder
DateInterval.WeekOfYear
Wochen (1-51)
"y"
oder
DateInterval.DayOfYear
Tage im Jahr (1-366)
"d"
oder
DateInterval.Day
Monatstage (1-31)
"w"
oder
DateInterval.Weekday
Wochentage (1-7)
"h"
oder
DateInterval.Hour
Stunden
"n"
oder
DateInterval.Minute
Minuten
"s"
oder
DateInterval.Second
Sekunden
HINWEIS
"ww"
Bei den drei DateXxx-Methoden können Sie durch optionale Parameter angeben, welcher Tag der Woche als erster Tag gilt (per Default der Sonntag) und wie die erste Kalenderwoche ermittelt wird (per Default ist das jene Woche, in der der 1. Januar zu liegen kommt; es gibt aber auch andere Definitionen).
.NET-Methoden und Eigenschaften (Klasse System.DateTime) Initialisierung New Date(y, m, d)
initialisiert ein Date-Objekt mit dem angegebenen Datum.
New Date(y, m, d, h, mm, d)
initialisiert ein Date-Objekt mit dem angegebenen Datum und der angegebenen Zeit.
FromFileTime(n)
wandelt die Dateizeit n in ein Date-Objekt um. (n ist eine Long-Zahl mit der Anzahl von Ticks seit dem 1.1.1601 UTC.)
Parse(s)
versucht die Zeichenkette s entsprechend der geltenden Ländereinstellung als Datum/Uhrzeit zu interpretieren und liefert als Ergebnis ein Date-Objekt.
UTCNow
liefert die aktuelle UTC-Zeit (Greenwich-Zeit).
Extraktion von Daten d.Date
liefert das reine Datum von d (also mit der Uhrzeit 00:00:00).
d.Year
liefert das Jahr (1 bis 9999).
d.Month
liefert den Monat (1-12).
d.Day
liefert den Monatstag (1-31).
d.DayOfWeek
liefert den Wochentag (0 für Sonntag, 1-6 für Montag bis Samstag).
d.DayOfYear
liefert den Tag im Jahr (1-366).
330
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Extraktion von Daten d.TimeOfDay
liefert die reine Zeit (ohne das Datum); als Datentyp wird System.TimeSpan verwendet.
d.Hour
liefert die Stunde (0-23).
d.Minute
liefert die Minute (0-59).
d.Second
liefert die Sekunde (0-59).
d.Millisecond
liefert den Millisekundenanteil der Zeit (0-999).
d.Ticks
liefert die interne Darstellung der Zeit als Long-Zahl. Die Maßeinheit beträgt 100 ns (Nanosekunden).
d.ToUniversalTime()
wandelt d in die UTC-Zeit (Greenwich-Zeit) um.
IsLeapYear(y)
gibt an, ob y ein Schaltjahr ist.
DaysInMonth(y, m)
gibt an, wie viele Tage das m-te Monat im Jahr y hat.
Berechnungen d.Add(ts)
liefert d + ts (wobei ts ein TimeSpan-Objekt sein muss).
d.AddTicks(n) d.AddMilliSeconds(x) d.AddSeconds(x) d.AddMinutes(x) d.AddHours(x) d.AddDays(x) d.AddMonths(n) d.AddYears(n)
liefert d + n Mal der angegebenen Zeitspanne bzw. d+x Mal der angegebenen Zeitspanne, wobei n ein Long-Wert und x ein Double-Wert ist.
d.Subtract(ts)
liefert d - ts (wobei ts ein TimeSpan-Objekt sein muss).
d1.Subtract(d2)
liefert d1 - d2. Das Ergebnis ist ein TimeSpan-Objekt.
.NET-Methoden und -Eigenschaften (Klasse System.TimeSpan) Initialisierung FromTicks(n) FromMilliSeconds(n) FromSeconds(n) FromMinutes(n) FromHours(n) FromDays(n)
liefert eine Zeitspanne von n Zeiteinheiten; bei Ticks ist n ein Long-Parameter, bei allen anderen Parametern ein Double-Parameter.
Parse("d.hh:mm:ss.f")
liefert eine Zeitspanne der angegebenen Zeit (bestehend aus Tagen, Stunden, Minuten, Sekunden und Sekundenbruchteilen). d und f sind optional.
8.4 Konvertierung zwischen Datentypen
331
Initialisierung d.TimeOfDay
liefert den Zeitanteil einer Date-Variablen als TimeSpanObjekt.
d1.Subtract(d2)
liefert die Zeitspanne zwischen zwei Date-Zeitpunkten als TimeSpan-Objekt.
Extraktion von Daten ts.MilliSeconds ts.Seconds ts.Minutes ts.Hours ts.Days
gibt den Anteil der Sekunden, Minuten, Stunden etc. der Zeitspanne an. Die gesamte gespeicherte Zeitspanne ergibt sich aus der Summe dieser Zeiten (d.h., MilliSeconds reicht von 0-999, Seconds und Minutes reichen von 0-59, Hours von 0-23).
ts.Ticks ts.TotalMilliSeconds ts.TotalSeconds ts.TotalMinutes ts.TotalHours ts.TotalDays
drückt die gesamte Zeitspanne in Sekunden, Minuten, Stunden etc. aus. Ticks liefert das Ergebnis als Long-Zahl, alle anderen Methoden liefern Double-Werte.
Berechnungen, Vergleiche ts1.Add(ts2)
liefert ts1+ts2.
Compare(ts1, ts2)
vergleicht ts1 mit ts2. Liefert -1, 0 oder 1, je nachdem, ob ts1 kleiner, gleich oder größer als ts2 ist.
ts1.Negate()
liefert -ts1.
ts1.Subtract(ts2)
liefert ts1-ts2.
8.4
Konvertierung zwischen Datentypen
Dieser Abschnitt fasst die zahllosen Methoden zusammen, die bei der Umwandlung von Daten in verschiedene Datentypen (Zahlen, Daten, Zeichenketten etc.) helfen. Einen großen Stellenwert nimmt dabei auch die Formatierung von Zahlen, Daten und Zeiten ein (wenn Sie etwa das Datum 31.12.2002 in die Zeichenkette 31. Dezember 02 umwandeln möchten).
8.4.1
Automatische und manuelle Konvertierung
Visual Basic wurde gewissermaßen berühmt dafür, mit welcher Eleganz es automatisch Konvertierungen zwischen den unterschiedlichsten Datentypen durchführte: Zahlen in Zeichenketten und wieder zurück, Daten und Zeiten in Fließkommazahlen etc.
332
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Zwar sind diese Automatismen in VB.NET nicht mehr ganz so stark ausgeprägt (insbesondere dann nicht, wenn Sie mit Option Strict arbeiten, was sehr zu empfehlen ist – siehe Abschnitt 4.1.4), aber noch immer geht vieles wie von Zauberhand, was in anderen Programmiersprachen eine explizite Konvertierung erfordert. Das gilt vor allem für alle Umwandlungen, die garantiert ohne Verlust durchgeführt werden können – etwa von Integer zu Double oder von Short zu Long. Object-Variablen: Besonders verführerrisch ist die automatischen Typkonvertierung natürlich bei Object-Variablen, die nach Bedarf einen beliebigen Datentyp annehmen kann. Dim o = o = o =
o As Object "xx" 'o ist eine String-Variable 3 'o ist jetzt eine Integer-Variable 3.14 'o ist jetzt eine Double-Variable
Konvertierung zwischen verschiedenen Zahlentypen: Die Short-Variable s wird mit dem Wert 3 initialisiert. Anschließend wird der Inhalt von s in der Double-Variablen x gespeichert. Dabei kommt es zu einer Konvertierung von Short zu Double, die VB.NET ohne weiteres durchführt. Dim s As Short, x As Double s = 3 x = s 'Konvertierung Short --> Double
Dennoch sollten Sie sich nicht einfach blind darauf verlassen, dass VB.NET schon errät, was Sie sich beim Programmieren gedacht haben (nehmen wir mal an, Sie haben sich überhaupt etwas gedacht). Bereits bei der Umkehrung der obigen Zuweisung kann es zu Problemen kommen: Die VB.NET-Entwicklungsumgebung akzeptiert die Anweisung s=x nur dann, wenn Sie Option Strict nicht verwenden. In diesem Fall rundet es 3.63 zur nächsten ganzen Zahl und speichert den Wert 4 in s. x = 3.63 s = x
Die nächsten Zeilen zeigen die Problematik der Typumwandlung noch deutlicher. Die Zahl 345678 überschreitet den Zahlenbereich für Short. Deswegen kommt es bei der Ausführung des Codes zu einem Fehler (System.OverflowException). x = 345678 s = x
'Fehler (System.OverFlowException)
Wenn Sie mit Option Strict arbeiten, akzeptiert die Entwicklungsumgebung (bzw. der Compiler) die Zuweisung s=x nicht. Damit das Programm überhaupt getestet werden kann, müssen Sie die Konvertierungsfunktion CShort einsetzen. Am Problem, dass 345678 zu groß für eine Short-Variable ist, ändert natürlich auch das nichts, d.h., bei der Programmausführung tritt weiterhin ein Fehler auf. Option Strict verhindert also keine Überlaufprobleme, aber es zwingt, die richtigen Konvertierungsfunktionen einzusetzen. (Und spätestens an dieser Stelle tauchen im Hinterkopf erste Fragen auf: Was passiert, wenn x zu groß ist? Soll ich s vielleicht besser als Integer deklarieren? Was, wenn x selbst dann noch zu groß ist?)
8.4 Konvertierung zwischen Datentypen
x = 345678 s = CShort(x)
333
'Fehler (System.OverFlowException)
Integer-Multiplikation: Ein weiteres interessantes Problem zeigen die beiden folgenden Zeilen. Zwar ist l als Long-Variable hier richtig dimensioniert, aber VB.NET betrachtet die beiden Konstanten 100000 und 200000 als Integer-Zahlen und führt daher auch die Multiplikation mit Integer-Arithmetik aus. Dabei tritt ein Überlauf auf, der bereits in der Entwicklungsumgebung festgestellt wird. Die Zeile wird daher als fehlerhaft gekennzeichnet. (In diesem Fall versagt also die automatische Typumwandlung von VB.NET. Natürlich könnte VB.NET derartige Multiplikationen immer mit Long-Arithmetik durchführen, aber das ist in den meisten Fällen nicht notwendig, dafür aber vier Mal langsamer.) Dim l As Long l = 100000 * 200000
'Fehler, weil 100000 * 200000 zu groß ist
Die einfachste Abhilfe besteht darin, eine der beiden Zahlen mit dem nachgestellten Buchstaben L als Long zu kennzeichnen. Damit wird die gesamte Multiplikation im LongZahlenbereich ausgeführt. Dieselbe Wirkung bei etwas größerem Tippaufwand erzielen Sie, wenn Sie eine der beiden Zahlen mit CLng explizit in das Long-Format umwandeln.
HINWEIS
l = 100000 * 200000L l = 100000 * CLng(200000)
'ok 'ok
Zur Erinnerung: VB.NET betrachtet ganze Zahlen im Programmcode generell als Integer, Fließkommazahlen generell als Double, Zeichenketten (auch bei einem einzelnen Zeichen!) generell als String. Wenn Sie möchten, dass VB.NET intern einen anderen Datentyp verwendet, müssen Sie diesen durch ein nachgestelltes Literalzeichen (z.B. L für Long, C für Char) angeben. Die in VB.NET vorgesehenen Literale sind am Beginn dieses Kapitels zusammengefasst.
Zeichenkettenverknüpfung mit & und +: Beim String-Verknüpfungsoperator ist die automatische Typkonvertierung gewissermaßen inhärent eingebaut. Dieser Automatismus funktioniert auch dann, wenn Sie mit Option Strict arbeiten: Option Strict On Dim s As String Dim d As Date = Now Dim x As Double = 1 / 3 Dim i As Integer = 27 s = "abc " & d & " " & x & " " & i s enthält nun beispielsweise die Zeichenkette "abc 01.12.2001 18:09:43 0,333333333333333 27" (abhängig davon, wann das Programm ausgeführt wird und welche Spracheinstellungen gelten).
Wenn Sie statt des Zeichens & das Zeichen + verwenden, wird es komplizierter. Das Programm lässt sich nun nur noch ausführen, wenn Sie Option Strict Off verwenden. In diesem Fall versucht VB.NET, alle Konvertierungen selbstständig durchzuführen. Das geht allerdings schief: Das Programm setzt zuerst "abc " + d + " " zu einer Zeichenkette zusammen
334
8 Zahlen, Zeichenketten, Datum und Uhrzeit
(z.B. "abc 01.12.2001 18:09:43 "). Den nächste Operator + interpretiert es allerdings als Additionsoperator für Double-Zahlen (wegen + x). Es versucht deshalb, die bisher gebildete Zeichenkette ebenfalls in eine Double-Zahl umzuwandeln, was aber zu einem Fehler führt (System.InvalidCastException). Das Beispiel beweist einmal mehr, wie gefährlich es ist, sich auf die automatische Typenkonvertierung blind zu verlassen. VB.NET errät eben nicht immer, was Sie vom Programm erwarten. Option Strict Off Dim s As String Dim d As Date = Now Dim x As Double = 1 / 3 Dim i As Integer = 27 s = "abc " + d + " " + x + " " + i
VORSICHT
Landessprachliche Besonderheiten Generell werden bei allen Konvertierungen von oder zu Zeichenketten die Landeseinstellungen der Systemeinstellung beachtet! Im deutschen Sprachraum werden daher Fließkommazahlen mit einem Komma dargestellt, Daten und Zeiten gemäß den deutschen Gepflogenheiten formatiert etc. Dieses Defaultverhalten von VB.NET kann natürlich zu großen Problemen führen (etwa beim Import oder Export von Dateien), sobald Ihr Programm woanders ausgeführt wird.
8.4.2
Konvertierungsfunktionen
CInt, CShort, CStr etc. Für explizite Typkonvertierungen mit minimalen Tippaufwand und größtmöglicher Effizienz sieht VB.NET die Funktionen CBool, CByte, CChar, CDate, CDbl, CDec, CInt, CLng, CObj, CShort, CSng und CStr vor. Damit werden alle elementaren VB.NET-Datentypen abgedeckt. Als Parameter dürfen an sich beliebige Daten an die Funktionen übergeben werden. Allerdings gibt es eine Reihe von Fehlermöglichkeiten und Sonderfällen: •
Wenn die Konvertierung den Wertebereichs des jeweiligen Datentyps überschreitet, tritt ein OverflowException-Fehler auf.
•
Ebenso tritt ein Fehler auf, wenn eine Umwandlung unmöglich ist (z.B. CDate("xx")).
•
Bei der Umwandlung leerer oder nicht initialisierter Zeichenketten in Zahlen liefern die Funktionen generell 0.
•
Die Umwandlung von Char-Zeichen in Zahlen (um den Code zu ermitteln) ist unmöglich – dazu müssen Sie die VB-Funktion Asc oder AscW verwenden.
•
Die Funktionen CByte, CShort, CInt und CLng runden wie Math.Round (siehe auch Abschnitt 8.1.5). Bei einem Dezimalanteil kleiner 0.5 wird abgerundet; wenn dieser Anteil
8.4 Konvertierung zwischen Datentypen
335
größer als 0.5 ist, wird aufgerundet. Bei 0.5 wird bei geraden Zahlen ab-, bei ungeraden Zahlen aufgerundet. •
Die Umwandlungsfunktionen berücksichtigen die aktuelle Landeseinstellung. Im deutschen Sprachraum wird daher CDate("1. Oktober") akzeptiert (das resultierende Datum ist der 1.10. des aktuellen Jahres). Entsprechend liefert CStr(2.5) die Zeichenkette "2,5" (also mit einem Komma zur Dezimaltrennung).
Falls Sie im Objektbrowser nach den CXxx-Funktionen suchen, wird Ihre Suche vergeblich sein. Es handelt sich bei diesen Funktionen nämlich nicht um richtige Methoden, sondern um Anweisungen an den Compiler. Dieser ersetzt die Funktionen durch Inline-Code, um auf diese Weise einen möglichst effizienten Code zu produzieren. (Inline-Code bedeutet, dass es in den meisten Fällen nicht zum Aufruf einer richtigen Funktion oder einer Methode kommt. Vielmehr werden die Anweisungen zur Typumwandlung direkt in den Code eingebaut. Ausnahmen sind allerdings aufwendige Umwandlungen für Daten oder Zeiten. Hier wird natürlich auf externe Funktionen zurückgegriffen.)
CType (Casting-Methode) Statt CLng(x) können Sie auch CType(x, Long) ausführen. Beide Anweisungen werden exakt gleich ausgeführt. (Genau genommen werden alle oben aufgezählten Funktionen CBool, CByte, CChar etc. durch CType ersetzt.)
VERWEIS
Als Datentyp darf auch ein Feld angegeben werden. Beispielsweise liefert CType(obj, String()) ein String-Feld. (Das funktioniert nur dann, wenn obj zwar als Variable des Typs Object deklariert ist, tatsächlich aber auf ein String-Feld verweist.) CType wird oft eingesetzt, wenn die Parameter von Methoden oder Ereignisprozeduren bzw. die Elemente von Aufzählungen oder Felder mit dem Typ Object dekla-
riert sind, tatsächlich aber andere Objekte anderer Klassen enthalten. Hintergrundinformationen zu dieser Anwendung von CType finden Sie in Abschnitt 4.6.5, zahlreiche weitere Beispiele, wenn Sie im Stichwortverzeichnis unter CType nachsehen.
Val und Str (Klasse Microsoft.VisualBasic.Conversion) Unter VB.NET stehen zwei weitere Methoden zur Verfügung, die man als Konvertierungsfunktionen betrachten kann. Val versucht, eine Zeichenkette als Zahl zu interpretieren, also den Wert der Zeichenkette zu ermitteln. Falls das nicht möglich ist, liefert Val den Wert 0. Val liefert als Ergebnis eine Double-Zahl. Die folgenden Zeilen demonstrieren die Anwendung von Val. Dim x = x = x = x =
x As Double Val("abc") Val("3") Val("3,5") Val("3.5")
'x=0 'x=3 'x=3 'x=3.5
336
8 Zahlen, Zeichenketten, Datum und Uhrzeit
x = Val("3.5e7") x = Val("12 + 3") x = Val("31.12.2001")
'x=35000000 'x=12 'x=31.12
Str ist das Gegenstück zu Val: Die Methode wandelt eine Zahl in eine Zeichenkette um. An Str darf kein Datum (also keine Date-Variable) übergeben werden. Beachten Sie bitte, dass
die resultierende Zeichenkette bei positiven Zahlen mit einem Leerzeichen beginnt. Verwenden Sie gegebenenfalls Trim, um dieses Leerzeichen zu entfernen.
HINWEIS
Dim s as String s = Str(3.123)
's = " 3.123"
Die große Besonderheit von Val und Str besteht darin, dass diese Methoden die Landeseinstellungen ignorieren und stattdessen den internationalen Gepflogenheiten entsprechen. Insbesondere werden Fließkommazahlen immer mit einem Dezimalpunkt dargestellt. Je nach Anwendung kann das ein Vor- oder ein Nachteil sein. Sie sollten den Unterschied aber auf jeden Fall kennen.
ToString-Methode (Konvertierung in Zeichenketten) Alle .NET-Klassen kennen die Methode ToString. Diese Methode wandelt den Inhalt eines Objekts in eine Zeichenkette um. Das funktioniert selbst für elementare Datentypen. Beispielsweise liefert (1 / 3).ToString die Zeichenkette "0,333333333333333". Eine Menge weiterer Informationen zu ToString gibt Abschnitt 8.5, wo es um Methoden zur Formatierung von Zahlen und Zeichenketten geht.
.NET-Konvertierungsmethoden (Klasse System.Convert) Natürlich enthält auch .NET zahllose Konvertierungsmethoden. Die bei weitem meisten derartigen Methoden sind in der Klasse System.Convert definiert. Insbesondere enthält diese Klasse die Methoden ToBoolean, ToByte etc. (für jeden elementaren Datentyp), wobei auch als Parameter wiederum fast jeder Datentyp erlaubt ist. Wenn Sie eine Konvertierung von Long nach Decimal durchführen möchten, setzen Sie die Methoden ToDecimal(val As Long) ein. (Die Auswahl der richtigen ToDecimal-Funktion erfolgt natürlich automatisch. Der Compiler entscheidet sich anhand des übergebenen Datentyps, welche der zahlreichen ToDecimal-Methoden zum Einsatz kommt.) Die Methoden verhalten sich wie die VB-Funktionen CByte, CDate etc.: Leere bzw. nicht initialisierten Zeichenketten werden zur Zahl 0 bzw. zum Datum 1.1.0001 (mit der Zeit 0:00:00) umgewandelt. Zahlen und Daten werden entsprechend der Landeseinstellungen konvertiert. Beachten Sie aber, dass bei weitem nicht jede der mögliche Kombinationen Ausgangsdatentyp → Zieldatentyp tatsächlich unterstützt wird! So gibt es etwa die Methoden ToDateTime(n As Integer), d.h., die Methode kann syntaktisch korrekt im Code verwendet werden. Aber
8.4 Konvertierung zwischen Datentypen
337
jeder Versuch, eine Zahl in ein Datum zu verwandeln, führt generell zum Fehler System.InvalidCastException. Dim s = d = x =
s As String, d As Date, x As Double Convert.ToString(1 / 3) ' s = "0,3333333333333333" Convert.ToDateTime("31.12.2002") ' d = #12/31/2002# Convert.ToDouble("1,75") ' x = 1.75
Interpretation von Zeichenketten (Parse-Methode) Wenn Sie Zeichenketten in andere Datentypen umwandeln möchten, können Sie entweder die Convert.ToXxxDatentyp-Methoden oder XxxDatentyp.Parse verwenden. Parse kommt aber mit mehr Sonderfällen zurecht und basiert intern auf aufwendigeren Algorithmen. Dim d As Date, x As Double d = Date.Parse("31.12.2002") x = Double.Parse("12,34")
' d = #12/31/2002# ' x = 12,34
Syntaxzusammenfassung VB-Konvertierungsfunktionen und -methoden CBool, CByte, CChar, CDate, CDbl, CDec, CInt, CLng, CObj, CShort, CSng und CStr
wandeln den übergebenen Parameter in den entsprechenden Datentyp (Boolean, Byte etc.) um.
CType(obj, typ)
wandelt obj in ein Objekt der Klasse typ um. Wenn die Umwandlung nicht möglich ist, tritt ein Fehler auf.
s = Str(x)
wandelt die Zahl x in eine Zeichenkette um (wobei als Dezimaltrennung immer ein Punkt verwendet wird). Bei positiven Zahlen beginnt s mit einem Leerzeichen.
x = Val(s)
interpretiert den Inhalt der Zeichenkette s als Zahl (wobei abermals ein Punkt zur Dezimaltrennung verwendet werden muss) und liefert als Ergebnis einen Double-Wert.
.NET-Konvertierungsmethoden (Klasse System.Convert) ToBoolean, ToByte, ToChar, ToDateTime, ToDecimal, ToDouble, ToInt16, ToInt32, ToInt64, ToSByte, ToSingle, ToString, ToUInt16, ToUInt32 und ToUInt64
wandeln den übergebenen Parameter in den entsprechenden Datentyp (Boolean, Byte etc.) um. Allen Methoden muss Convert. vorangestellt werden.
x.ToString
wandelt (soweit dies möglich ist) jeden .NET-Datentyp in eine Zeichenkette um.
338
8 Zahlen, Zeichenketten, Datum und Uhrzeit
.NET-Konvertierungsmethoden (Klasse System.Convert) x.Parse(s)
interpretiert eine Zeichenkette als Wert des Typs x. Beispielsweise interpretiert x=Double.Parse(s) die Zeichenkette s als Fließkommazahl. Parse ist flexibler als die ToXxx-Methoden. Beispielsweise kommt d=Date.Parse(s) mit mehr Sonderfällen zurecht als d=Convert.ToDateTime(s).
8.4.3
Spezialmethoden
Die folgenden Tabellen fassen – geordnet nach Themen – diverse Spezialmethoden zur Konvertierung zusammen. Konvertierung Fließkommazahlen → Integerzahlen (Details siehe Abschnitt 8.1.5) n = CShort/CInt/CLong(x)
rundet je nach Nachkommaanteil auf oder ab (wie Round).
n = Convert.ToInt16/32/64(x)
rundet je nach Nachkommaanteil auf oder ab (wie Round).
n = CLong(Fix(x))
schneidet den Nachkommaanteil ab.
n = CLong(Int(x))
rundet immer zur nächstkleineren Zahl.
n = CLong( Math.Ceiling(x))
rundet immer zur nächstgrößeren Zahl.
n = CLong(Math.Floor(x))
rundet immer zur nächstkleineren Zahl.
n = CLong(Math.Round(x, y))
rundet je nach Nachkommaanteil auf y Stellen auf oder ab.
Konvertierung String → Char (Details siehe Abschnitt 8.2.1) c = Convert.ToChar(s)
wandelt das in der String-Variablen s enthaltene Zeichen in ein Char-Zeichen um. Achtung, das funktioniert nur, wenn s genau ein Zeichen enthält. (Andernfalls tritt ein Fehler auf!)
c = CChar(s)
liefert das erste Zeichen von s. Wenn s leer ist, liefert CChar ein Zeichen mit dem Code 0.
c = GetChar(s, n)
liefert das n-te Zeichen von s. (n=1 für das erste Zeichen!)
c = s.Chars(n)
liefert das n-te Zeichen von s. (n=0 für das erste Zeichen!)
carray = s.ToCharArray()
liefert ein Char-Feld, das jedes einzelne Zeichen von s enthält.
carray = s.ToCharArray(n, m)
wie oben, das resultierende Feld enthält aber nur m Zeichen ab dem n-ten Zeichen.
8.5 .NET-Formatierungsmethoden
339
Encoding, Decoding (Unicode und ASCII, Details siehe Abschnitt 8.2.5) c = Chr(n)
liefert das Zeichen (Datentyp Char) mit dem ASCII-Code n.
c = ChrW(n)
liefert das Zeichen (Datentyp Char) mit dem Unicode n.
n = Asc(c/s)
liefert den ASCII-Code zum Zeichen c bzw. zum ersten Zeichen von s.
n = AscW(c/s)
liefert den Unicode zum Zeichen c bzw. zum ersten Zeichen von s.
Dim uni_enc As New liefert ein Byte-Feld, das die Bytes (Unicode) der System.Text.UnicodeEncoding() Zeichenkette s enthält. Dim b As Byte() = uni_enc.GetBytes(s)
Konvertierung Datum ↔ Zahlen (Details siehe Abschnitt 8.3) n = d.Ticks
liefert Datum und Zeit als Long-Zahl (DateTime → Long). n enthält die Anzahl von Ticks (100 ns) seit dem 1.1.0001.
Dim d As New DateTime (n)
initialisiert ein neues Datum aus einer Long-Zahl (Long → DateTime).
d = DateTime.MinValue. AddTicks(n)
wandelt eine Long-Zahl in ein Datum um (Long → DateTime).
n = d.ToFileTime
wandelt ein Datum in die Zeitdarstellung von Dateien um (DateTime → Long). n enthält die Anzahl von Ticks seit dem 1.1.1601 UTC.
d = DateTime.FromFileTime(n)
wandelt die Zeitdarstellung von Dateien in ein Datum um (Long → DateTime).
x = d.ToOADate
wandelt ein Datum in die VB6-Darstellung um (DateTime → Double).
d = DateTime.FromOADate(x)
wandelt einen VB6-Double-Wert in ein Datum um (Double → DateTime).
8.5
.NET-Formatierungsmethoden
Relativ oft kommt es vor, dass Sie eine Zahl oder ein Datum schön formatieren möchten – etwa in der Form 31. Dezember 2002. Vielleicht fragen Sie sich, warum derartige Methoden gerade hier beschrieben werden. Der Grund ist einfach: Es handelt sich um eine logische Fortsetzung des vorherigen Abschnitts zu den Konvertierungsmethoden. Die Formatierung von Zahlen und Daten ist einfach ein Sonderfall der Konvertierung von Zahlen oder Daten zu Zeichenketten. Wie so oft gibt es auch bei den Formatierungsmethoden zwei parallele Implementierungen: Sie können wahlweise auf die (aus VB6) vertrauten VB-Methoden oder auf die
340
8 Zahlen, Zeichenketten, Datum und Uhrzeit
neuen .NET-Methoden zurückgreifen. Diese beiden Varianten unterscheiden sich in erster Linie durch die Formatierungszeichen, die das gewünschte Format beschreiben. Wenn Sie noch mit keiner der beiden Varianten vertraut sind, ist es vermutlich sinnvoller, gleich die .NET-Methoden zu erlernen, die sich dann auch in C# oder anderen .NET-Sprachen mühelos einsetzen lassen. Die VB-Methoden sind zwar zum Teil einfacher zu nutzen, bieten aber oft auch weniger Flexibilität. Im Folgenden finden Sie zuerst einen knappen Überblick über das .NET-Formatierungskonzept und dann eine Referenz der wichtigsten Formatierungscodes. Abschnitt 8.6 beschreibt dann die entsprechenden VB-Methoden.
8.5.1
.NET-Formatierungsgrundlagen
Formatierung mit ToString .NET verfolgt einen objektorientierten Ansatz zur Formatierung von Zahlen und Daten. Ausgangspunkt des Formatierungskonzepts ist die Methode ToString, die auf alle elementaren .NET-Datentypen und -Objekte angewandt werden kann. Wenn an ToString keine zusätzlichen Parameter übergeben werden, erfolgt die Konvertierung gemäß eines vom Datentyp abhängigen Defaultformats und unter Anwendung der gültigen Landeseinstellungen. s = Now.ToString
'liefert "31.12.2002 16:51:54"
Optional können an ToString bei den meisten Datentypen ein oder zwei Parameter übergeben werden. Der erste Parameter enthält eine Zeichenkette, die das gewünschte Format beschreibt. Für Daten ist als Formatzeichen beispielsweise "s" vorgesehen, um Daten entsprechend der ISO-Norm 8601 so zu formatieren, dass die resultierenden Zeichenketten sortiert werden können. "D" gilt als Kürzel zur ausführlichen Formatierung von Daten. s = Now.ToString("s") s = Now.ToString("D")
'liefert "2002-12-31T16:54:22" 'liefert "Dienstag, 31. Dezember 2002"
Landesspezifische Formatierung Als zweiten optionalen Parameter können Sie an ToString ein IFormatProvider-Objekt übergeben. IFormatProvider ist selbst keine eigene Klasse, sondern nur eine Schnittstelle. Es gibt drei .NET-Klassen, die diesem Interface entsprechen: CultureInfo mit landesspezifischen Einstellungen, DateTimeFormatInfo mit speziellen Formatierungsangaben für Daten sowie NumberFormatInfo mit Angaben zur Formatierung von Zahlen. Alle drei Klassen sind im Namensraum System.Globalization definiert. Das folgende Beispiel zeigt, wie ein Objekt der Klasse CultureInfo mit den italienischen Landeseinstellungen dazu verwendet wird, um ein Datum ausführlich zu formatieren. Dim cult As New Globalization.CultureInfo("it-IT") s = Now.ToString("D", cult) 'liefert "martedì 31 dicembre 2002"
8.5 .NET-Formatierungsmethoden
341
Landesunabhängige Formatierung Vielleicht wollen Sie Zahlen oder Daten ohne Berücksichtigung der aktuellen oder anderer Ländereinstellungen ausgeben (etwa zur Speicherung in Dateien, die international verwendet werden können): In diesem Fall können Sie zur Initialisierung eines CultureInfoObjekt die Eigenschaft InvariantCulture verwenden. Dieses Objekt enthält landesunabhängige Optionen zur Formatierung. (Landesunabhängig bedeutet in der Praxis englisch.) Die folgenden Zeilen formatieren das aktuelle Datum unabhängig von der gerade geltenden Landeseinstellung in einer international einheitlichen Art und Weise. Dim cult As Globalization.CultureInfo = _ Globalization.CultureInfo.InvariantCulture s = Now.ToString("D", cult) 'liefert "Tuesday, 31 December 2002"
Formatierung mit Format Statt ToString können Sie zur Formatierung auch die Methode String.Format verwenden. Die allgemeine Syntax lautet dabei: String.Format("myformat", ausdruck1 [, a2 [, a3]]). Eine Formatzeichenkette beschreibt also die Formatierung von bis zu drei weiteren Parametern. (Noch mehr Parameter können in Form eines Object-Felds übergeben werden.) Die Formatierungszeichenkette ist im Grunde gleich aufgebaut wie bei ToString. Der wesentliche Unterschied besteht darin, dass die Zeichenkette auch beliebigen Text enthalten darf und dass klar sein muss, welches Format für welchen Parameter verwendet werden soll. Aus diesem Grund muss der eigentliche Formatcode folgendermaßen geklammert werden: {n:code}. Dabei bezeichnet n die Nummer des Parameters (0 für der ersten), code den gewünschten Formatierungscode. Die folgenden Beispiele illustrieren die Syntax. (Das Format "d" stellt Zahlen in dezimaler Schreibweise dar.) s = String.Format("{0:D}", Now) '--> "Dienstag, 31. Dezember 2002" s = String.Format("abc {0:d} efg {1:d}", 3, 7) '--> "abc 3 efg 7" s = String.Format("abc {1:d} efg {0:d}", 3, 7) '--> "abc 7 efg 3"
Je nach Format kann zusätzlich angegeben werden, wie viele Stellen zur Formatierung verwendet werden sollen. Die allgemeine Syntax lautet dann {n,m:code} (wobei m die Stellenanzahl ist). Auch dazu ein Beispiel (beachten Sie die Anzahl der Leerzeichen vor der Ziffer 3!): s = String.Format("abc {0,7:d} efg", 3)
'-->
"abc
3 efg"
Wenn die Stellenanzahl m negativ angegeben wird, werden die Leerzeichen nach der formatierten Zeichenkette (statt davor) eingefügt: s = String.Format("abc {0,-7:d} efg", 3)
'-->
"abc 3
efg"
TIPP
342
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Sie können in der Formatierungszeichenkette auch ganz auf eine Formatangabe verzichten. In diesem Fall werden die Daten einfach mit ToString ohne besondere Formatoptionen (also mit dem ToString-Default) in Zeichenketten umgewandelt. Für einfache Ausgaben ist diese Kurzschreibweise meistens ausreichend. Dazu noch ein Beispiel: String.Format("abc {0} efg", 3) liefert "abc 3 efg".
Falls Sie zusätzliche (landesspezifische) Formatierungsoptionen verwenden möchten, können Sie ein IFormatProvider-Objekt als ersten Paramter an String.Format übergeben, also beispielsweise String.Format(cult, "{0:D}", Now). Neben String.Format gibt es unzählige weitere .NET-Methoden, die dieselben Parameter verarbeiten – beispielsweise Console.Write[Line] oder IO.TextWriter. Write[Line]. Es ist also selten erforderlich, zuerst mit String.Format eine entsprechende Zeichenkette zu bilden und diese dann an eine andere Methode zu übergeben; oft kann die Formatierung direkt mit der Ausgabemethode durchgeführt werden. Das verhilft zu einem kompakteren Code. Die beiden folgenden Zeilen Console.WriteLine("{0:D}", Now) Console.WriteLine("abc {0:d} efg {1:d}", 3, 7)
führen zu dieser Ausgabe im Konsolenfenster: Dienstag, 31. Dezember 2002 abc 3 efg 7
Beachten Sie aber, dass diese Format-Schreibweise leider nicht für die Write[Line]-Methoden der Debug-Klasse verwendet werden kann. .NET-Formatierungsmethoden obj.ToString
verwendet das Defaultformat ("") und die Sprache entsprechend der Systemeinstellungen.
obj.ToString("format")
verwendet zur Formatierung das angegebene Format.
obj.ToString("format", iform)
wie oben, berücksichtigt zusätzliche Formatoptionen. (Zulässige IFormatProvider-Parameter sind CultureInfo-, DateTimeFormatInfo- und NumberFormatInfo-Objekte.)
String.Format("format", obj1, obj2, obj3 ...)
verwendet zur Formatierung das angegebene Format. Der Formatcode muss in der Schreibweise {n} oder {n:code} oder {n,m:code} angegeben werden. Dabei gibt n die Parameternummer an (beginnend mit 0), m die maximale Stellenanzahl der Zeichenkette und code die gewünschte Formatierung.
String.Format(iform, "format", obj1, obj2 ...)
wie oben, berücksichtigt aber zusätzliche Formatoptionen (z.B. Ländereinstellungen).
8.5 .NET-Formatierungsmethoden
8.5.2
343
Zahlen formatieren
Dieser Abschnitt stellt eine Menge Codes zur Formatierung von Zahlen vor. Diese Codes können folgendermaßen angewendet werden (hier für den Code e): Dim s As String, x As Double = Math.PI s = x.ToString("e3") 's = "3,142e+000" s = String.Format("{0:e3}", x) 's = "3,142e+000"
Die Formate c bis p können für alle numerischen Datentypen verwendet werden. Bei einigen Formaten kann durch eine nachgestellte Zahl die Anzahl der Stellen hinter dem Komma angegeben werden. Per Default werden im Regelfall zwei Nachkommastellen verwendet. Die folgende Tabelle zeigt, wie die Double-Zahlen 123456789 und -0,0000123 formatiert werden (bei deutscher Ländereinstellung und mit € als Währungssymbol). Vordefinierte Formatcodes zur Formatierung von Zahlen (Methode String.Format) c
123.456.789,00 € / -0,00 €
Währungsformat (currency)
c3
123.456.789,000 € / -0,000 €
Währungsformat mit drei Nachkommastellen
e
1,234568e+008 / -1,230000e-006
wissenschaftliches Format (exponential)
e3
1,235e+008 / 1,230e-005
wissenschaftliches Format mit drei Nachkommastellen
E
1,234568E+008 / -1,230000E-006
wie oben, aber Exponentialschreibweise mit E statt mit e
f
123456789,00 / -0,00
Festkommaformat (fixed-point)
f3
123456789,000 / -0,000
Festkommaformat mit drei Nachkommastellen
g
123456789 / -1,23e-06
allgemeines Format, möglichst kompakte Darstellung von Zahlen (general)
n
123.456.789,00 / -0,00
Format mit Tausendertrennung (number)
n3
123.456.789,000 / -0,000
Format mit Tausendertrennung mit drei Nachkommastellen
p
12,345,678,900,00% / -0,00%
Prozentzahlen (Achtung: der Wert 1 wird als 100 % dargestellt!)
p3
12,345,678,900,00% / -0,001%
Prozentzahlen mit drei Nachkommastellen
r
123456789 / -1,23E-06
Format, das ein verlustfreies Wiedereinlesen (z.B. mit CDbl) der Daten ermöglicht (roundtrip). Allerdings ist auch dieses Format von der Landeseinstellung abhängig und daher für internationale Anwendungen ungeeignet! Das Format ist ausschließlich für Single- und Double-Zahlen gedacht.
344
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Die Formate d und x können ausschließlich für Integerzahlenformate verwendet werden (nicht aber für Single, Double und Decimal!). Bei beiden Formaten kann die gewünschte Stellenanzahl angegeben werden – in diesem Fall werden entsprechend viele Nullen eingefügt. Die folgende Tabelle zeigt, wie die Integerzahlen i1=123456789 und i2=-123 formatiert werden. Vordefinierte Formatcodes für Integerzahlen (Byte, Short, Integer, Long) d
123456789 / -123
dezimales Format
d5
123456789 / -00123
dezimales Format mit fünf Stellen
x
75bcd15 / ffffff85
hexadezimales Format
Einzelcodes zur individuellen Formatierung von Zahlen Wenn Ihnen die vordefinierten Formate nicht ausreichen, können Sie die Formatzeichenkette auch ganz selbst bilden. .NET sieht dazu eine Menge Codes vor, die sowohl für Fließkomma- als auch für Integerzahlen verwendet werden dürfen. Einzelcodes zur Formatierung von Zahlen (Methode String.Format) 0
Platzhalter für eine Zahl bzw. für eine Stelle; nichtsignifikante Nullen werden durch das Zeichen 0 dargestellt
#
Platzhalter für eine Zahl bzw. für eine Stelle; nichtsignifikante Nullen werden durch Leerzeichen ersetzt
.
Dezimalpunkt (das tatsächlich verwendete Zeichen hängt von der Landeseinstellung ab!)
,
Tausendertrennung (wie oben); das Zeichen bewirkt gleichzeitig eine Division durch 1000, so dass die Zahl 1234567890 mit dem Formatcode "#,," zu "1235" wird.
%
Prozentzeichen (wie oben); das Zeichen bewirkt gleichzeitig eine Multiplikation mit 100, so dass die Zahl 0,753 mit dem Formatcode "0%" zu "75%" wird.
E+0 e+0
Exponentialdarstellung mit den Zeichen E oder e; das Vorzeichen des Exponenten wird immer angezeigt (1E+3 oder 1E-3); die Anzahl der 0en bestimmt die Stellenanzahl des Exponenten (d.h., das Format e+000 führt zu "1e+003")
E-0 e-0
wie oben, ein positives Vorzeichen des Exponenten wird aber nicht angezeigt (1E3 oder 1E-3).
fp;fn;f0
ermöglicht die Angabe von drei unterschiedlichen Formaten für positive Zahlen, negative Zahlen und für 0; Achtung, bei negativen Zahlen wird das Vorzeichen entfernt (d.h., die Zahl -1 wird durch das Format 0;0 als 1 dargestellt)!
8.5 .NET-Formatierungsmethoden
345
Die folgenden Zeilen liefern einige Beispiele für die Anwendung der obigen Codes bei deutscher Landeseinstellung. Um den Code möglichst kompakt zu halten, wurde die unübliche (aber durchaus zulässige) Schreibweise zahl.ToString gewählt. Dim s = s = s =
s As String 123.456.ToString("0") 123.456.ToString("0.00") 123.456.ToString("0000.0000")
's = "123" 's = "123,46" 's = "0123,4560"
s s s s
123.456.ToString("#") 0.0123.ToString("#") 0.0123.ToString("#.###") 0.0123.ToString("0.###")
's 's 's 's
= = = =
= = = =
"123" "" ",012" "0,012"
s = 1234.ToString("#,#,#") s = 123456789.ToString("#,#,#") s = 1234.ToString("0,000,000")
's = "1.234" 's = "123.456.789" 's = "0.001.234"
s = 0.0123.ToString("0%") s = 0.0123.ToString("0.00%")
's = "1%" 's = "1,23%"
s = 123456789.ToString("0e+00") s = 123456789.ToString("000e+00") s = 123456789.ToString("0000E-00")
's = "1e+08" 's = "123e+06" 's = "1235E05"
s =-1.ToString("0;(0);0")
's = "(1)"
Noch eine kurze Erklärung zum letzten Beispiel: Damit wird erreicht, das positive Zahlen und 0 normal dargestellt werden, negative Zahlen dagegen in Klammern und ohne Vorzeichen, wie dies bisweilen im englischen Sprachraum üblich ist.
Text und Sonderzeichen in der Formatzeichenkette In der Formatzeichenkette dürfen auch beliebige andere Zeichen enthalten sein. Diese werden einfach unverändert angezeigt. s = 123.ToString("abc#efg")
's = "abc123efg"
Etwas komplizierter wird es, wenn Sie Formatierungszeichen als Text anzeigen möchten (z.B. das Zeichen #). Dazu gibt es zwei Schreibweisen: Sie können dem betreffenden Zeichen das Zeichen \ voranstellen oder Sie können die Sonderzeichen zwischen Apostrophe stellen. Auch hierfür zwei Beispiele: s = 123.ToString("#") s = 123.ToString("'###' # '###'")
's = "#123" 's = "### 123 ###"
Landesunabhängige Formatierung von Zahlen Unabhängig davon, welche Formatcodes Sie verwenden, wird bei der Formatierung von Zahlen immer auch die Landeseinstellung berücksichtigt (z.B. bei der Auswahl der Zeichen zur Dezimal- und zur Tausendertrennung). Wenn Sie das vermeiden möchten, über-
346
8 Zahlen, Zeichenketten, Datum und Uhrzeit
geben Sie an die Formatmethode ein zusätzliches CultureInfo-Objekt mit den InvariantCulture-Einstellungen. Dim s As String, x As Double = 1/7 Dim cult As Globalization.CultureInfo = _ Globalization.CultureInfo.InvariantCulture s = x.ToString("r", cult) 's = "0.14285714285714285" s = String.Format(cult, "{0:r}", x) 's = "0.14285714285714285"
Dezimaltrennzeichen und Tausendertrennzeichen feststellen Wenn Sie wissen möchten, welche Zeichen am aktuellen Computer zur Dezimal- bzw. zur Tausendertrennung eingestellt sind, können Sie den folgenden Code zu Hilfe nehmen. Dim decimalpoint, groupseparator As Char decimalpoint = String.Format("{0:0.0}", 0).Chars(1) groupseparator = String.Format("{0:0,000}", 1000).Chars(1)
Formatcodes ausprobieren Um die verschiedenen Formatcodes rasch auszuprobieren, können Sie ein Programm nach dem folgenden Muster verwenden. Sub Main() Dim x1 As Double = 123456789 Dim x2 As Double = -0.00000123 Dim i As Integer, formstr As String Dim myformat() As String = {"c", "e", "f", "g", "n", "p", "r"} For i = 0 To myformat.Length - 1 formstr = myformat(i) + ": {0:" + myformat(i) + "} / {1:" + _ myformat(i) + "}" Console.WriteLine(formstr, x1, x2) Next i End Sub
Mehr Flexibilität bietet ein kleines Windows-Programm, in dem Sie in zwei Textfeldern eine Fließkommazahl und eine Formatzeichenkette eingeben können. Das Programm zeigt bei jeder Änderung die resultierende formatierte Zeichenkette sofort an (siehe Abbildung 8.4). Sie finden das Programm auf der beiliegenden CD (Verzeichnis zahlen-zeichenketten\ formattest). Auf einen Abdruck des gesamten Codes wird hier verzichtet. Die entscheidenden Prozedur, die die beiden Eingabefelder auswertet und die Ergebniszeichenkette ermittelt, sieht folgendermaßen aus: ' in zahlen-zeichenketten\formattest\form1.vb Private Sub UpdateResultLabel() Dim x As Double
8.5 .NET-Formatierungsmethoden
347
Try 'Fehlerabsicherung, siehe Kapitel 11 x = CDbl(txtNumber.Text) lblResult.Text = _ String.Format("{0:" + Trim(txtFormat.Text) + "}", x) Catch lblResult.Text = "error" End Try End Sub
Abbildung 8.4: Programm zum Testen von Formatcodes
8.5.3
Daten und Zeiten formatieren
Die folgende Tabelle zählt die vordefinierten Codes zur Formatierung von Datum und Zeit auf (zusammen mit Beispielen, die für die deutsche Ländereinstellung gelten). Die Codes können folgendermaßen angewendet werden (hier für den Code F): s s s s
= = = =
Now.ToString("F") String.Format("{0:F}", Now) Now.ToString("HH:mm:ss") String.Format("{0:HH:mm:ss}", Now)
Vordefinierte Formatcodes für Datum und Uhrzeit (Methode String.Format) d
03.12.2001
Datum kurz (date)
D
Montag, 3. Dezember 2001
Datum lang
f
Montag, 3. Dezember 2001 21:42
Datum lang plus Zeit (full)
F
Montag, 3. Dezember 2001 21:42:06
Datum lang plus Zeit lang
g
03.12.2001 21:42
Datum kurz plus Zeit kurz (general)
G
03.12.2001 21:42:06
Datum kurz plus Zeit lang
m
03 Dezember
Tag plus Monat (month)
r
Mon, 03 Dec 2001 21:42:06 GMT
Datum gemäß RFC1123 (Internet-Standard)
s
2001-12-03T21:42:06
Datum gemä ISO-8601-Standard (sortable)
348
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Vordefinierte Formatcodes für Datum und Uhrzeit (Methode String.Format) t
21:42
Zeit kurz (time)
T
21:42:06
Zeit lang
u
2001-12-03 21:42:06Z
wie s, aber laut Dokumentation universal time statt lokaler Zeit
U
Montag, 3. Dezember 2001 20:42:06
Variante zu u
y
Dezember 2001
Monat und Jahr (year)
Einige der folgenden Zeichen repräsentieren für sich allein ein komplettes Datumsformat (siehe Tabelle oben). Erst in Kombination mit anderen Formatierungszeichen werden sie als Einzelcodes interpretiert. Einzelcodes für Uhrzeit (Methode String.Format) f bis fffffff
123
Sekundenbruchteile (ein bis sieben Stellen, also von Zehntelsekunden bis 100 ns)
ss
59
Sekunden (00-59)
m
59
Minute (0-59) ohne vorangestellte 0
mm
59
Minute (00-59) mit vorangestellter 0
h
12
Stunde (1-12) ohne vorangestellte 0
hh
12
Stunde (1-12) mit vorangestellter 0
H
23
Stunde (0-23) ohne vorangestellte 0
HH
23
Stunde (0-23) mit vorangestellter 0
t
A
A oder P entsprechend AM oder PM (nur wenn AM/PM in der Ländereinstellung vorgesehen ist)
tt
AM
AM oder PM (nur bei entsprechender Ländereinstellung)
z
+1
Zeitzone (relativ zu GMT) ohne vorangestellte 0
zz
+01
Zeitzone mit vorangestellter 0
zzz
+01:00
Zeitzone vierstellig
Einzelcodes für Datum (Methode String.Format) ddd
Mon
Abkürzung für Monatstag
dddd
Montag
Monatstag voll ausgeschrieben
d
31
Monatstag (1-31) ohne vorangestellte 0
dd
31
Monatstag (01-31) mit vorangestellter 0
8.6 VB-Formatierungsmethoden
349
Einzelcodes für Datum (Methode String.Format) M
12
Monat (1-12) ohne vorangestellte 0
MM
12
Monat (1-12) mit vorangestellter 0
MMM
Dez
Abkürzung für den Monatsnamen
MMMM Dezember
Monatsname voll ausgeschrieben
y
1
Jahr zweistellig ohne vorangestellte 0
yy
01
Jahr zweistellig mit vorangestellter 0
yyy
2001
Jahr vierstellig
gg
n. Chr.
Zeitperiode oder Zeitära (nur, wenn dies für den eingestellten Kalender vorgesehen ist!)
8.6
VB-Formatierungsmethoden
Die zentrale VB-Methode zur Formatierung von Zahlen und Daten heißt einfach Format (definiert in der Klasse Microsoft.VisualBasic.Strings). Die allgemeine Syntax dieser Funktion lautet: Format(ausdruck, "myformat"). Beachten Sie, dass die hier beschriebene Methode Strings.Format nicht kompatibel mit der Methode String.Format aus dem vorigen Abschnitt ist! Damit wird der Ausdruck o entsprechend den Formatangaben durch myformat in eine Zeichenkette umgewandelt. Wie die beiden folgenden Beispiele zeigen, kann als myformat entweder eine vordefinierte String-Konstante oder eine Zeichenkette mit Formatzeichen übergeben werden: s = Format(Now, "General Date") s = Format(Now, "dd-mm-yyyy")
'liefert "31.12.2002 15:15:34" 'liefert "31-12-2002"
Die Methoden FormatNumber, FormatCurrency, FormatPercent und FormatDateTime sind vereinfachte Varianten zu Format. Zwar sind die Formatierungsmöglichkeiten stark eingeschränkt, dafür ist die Steuerung aber einfacherer. Alle vier Format-Methoden berücksichtigen die Systemeinstellung (Landeseinstellung, Währungssymbol etc.). Es besteht keine Möglichkeit, die Formatierung landesunabhängig durchzuführen. (Für diesem Zweck müssen Sie die unten beschriebenen .NET-Methoden einsetzen.) VB-Formatierungsmethoden (Klasse Microsoft.VisualBasic.Strings) Format(obj, "format")
formatiert das angegebene Objekt entsprechend der Formatierungszeichenkette.
FormatNumber(x, ...)
formatiert eine Zahl entsprechend den Angaben durch die weiteren Parameter.
FormatCurrency(x, ...)
formatiert eine Zahl als Geldbetrag.
350
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-Formatierungsmethoden (Klasse Microsoft.VisualBasic.Strings) FormatPercent(x, ...)
formatiert eine Zahl als Prozentwert entsprechend den Angaben durch die weiteren Parameter.
FormatDateTime(d, format)
formatiert ein Datum oder eine Uhrzeit entsprechend dem (optionalen) Formatparameter.
8.6.1
Zahlen formatieren
FormatNumber FormatNumber formatiert die übergebene Zahl bei deutschen Systemeinstellungen per Default mit zwei Nachkommastellen und mit Punkten als Tausenderseparatoren. Daher liefert FormatNumber(123456.789) die Zeichenkette "123.456,79". Durch eine Reihe optionaler Parameter kann die Formatierung beeinflusst werden: s = FormatNumber(x, n, leading, parent, group)
•
x ist die zu formatierende Zahl (Datentyp Object, d.h. es dürfen Zahlen in allen Variab-
lentypen übergeben werden). •
n gibt die Anzahl der Nachkommastellen an (per Default 2).
•
leading gibt an, ob nicht signifikante Nullen dargestellt werden sollen (",23" oder "0,23"). Mögliche Einstellungen sind Tristate.True, Tristate.False oder Tristate.UseDefault. Per Default gilt UseDefault. Das bedeutet, dass das Verhalten durch die Systemeinstel-
lung gesteuert wird. Im deutschen Sprachraum werden nicht signifikante Nullen üblicherweise angezeigt. •
parent gibt an, ob negative Zahlen in Klammern gestellt werden sollen ("-1" oder "(1)"). Es gelten dieselben Einstellmöglichkeiten wie bei leading.
•
group gibt an, ob Tausenderseparatoren verwendet werden sollen. Abermals stehen dieselben Einstellmöglichkeiten wie bei leading zur Auswahl.
Einige Beispiele illustrieren die Anwendung von FormatNumber. s = FormatNumber(123456.789) s = FormatNumber(123456.789, 0) s = FormatNumber(123456.789, 0, , , TriState.False)
's = "123.456,79" 's = "123.457" 's = "123457"
FormatCurrency FormatCurrency funktioniert wie FormatNumber, fügt aber am Ende der Zeichenkette noch das durch die Systemeinstellung bestimmte Währungssymbol hinzu. FormatCurrency( 12345.678) liefert "12.345,68 €". Alle Parameter sind wie bei FormatNumber einzustellen.
8.6 VB-Formatierungsmethoden
351
FormatPercent FormatPercent formatiert die übergebene Zahl als Prozentwert. Das heißt, dass die Zahl mit Hundert multipliziert wird und mit zwei Nachkommastellen und einem Prozentzeichen dargestellt wird. FormatPercent(0.756) liefert im deutschen Sprachraum die Zeichenkette "75,60%". Die Methode kann mit denselben optionalen Parametern wie bei FormatNumber gesteuert werden. s = FormatPercent(x, n, leading, parent, group)
Noch einige Beispiele: s s s s
= = = =
FormatPercent(0.75678) FormatPercent(0.75678, 0) FormatPercent(0.0001234) FormatPercent(0.0001234, 4, TriState.False)
's 's 's 's
= = = =
"75,68%" "76%" "0,01%" ",0123%"
VB-spezifische Codes für Format Grundsätzlich können dieselben Kürzel wie bei den .NET-Formatierungsmethoden verwendet werden. Diese Kürzel werden hier nicht nochmals aufgezählt – Sie finden Sie in Abschnitt 8.5.2. Das folgende Beispiel zeigt die Anwendung des Formatcodes e für die Exponentialschreibweise. s = Format(123456, "e")
's = "1,23E+02"
Darüber hinaus kennt Format ein paar weitere Codes, deren Hauptvorteil darin besteht, dass ihre Bedeutung unmittelbar klar ist. (Dafür ist der Tippaufwand größer.) s = Format(123456, "Scientific")
's = "1,23E+02"
Die folgende Tabelle zeigt die Anwendung dieser Formate auf die Double-Zahlen 123456789 und -0,0000123. VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) Standard
123.456.789,00 / -0,00
Format mit Tausendertrennung (number)
General Number
123456789 / -1,23e-06
allgemeines Format, möglichst kompakte Darstellung von Zahlen
Currency
123.456.789,00 € / -0,00 €
Währungsformat
Scientific
1,2E+08 / -1,23E-05
wissenschaftliches Format (exponential)
Fixed
123456789,00 / -0,00
Festkommaformat
Percent
12,345,678,900,00% / -0,00%
Prozentzahlen (Achtung: der Wert 1 wird als 100 % dargestellt!)
True/False
False / False
liefert False für Zahlen ungleich 0 und True für 0
Yes/No
Ja / Ja
liefert in der Landessprache Ja für Zahlen ungleich 0 und Nein für 0
352
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) On/Off
8.6.2
Ein / Aus
liefert in der Landessprache Ein für Zahlen ungleich 0 und Aus für 0
Daten und Zeiten formatieren
FormatDateTime FormatDateTime formatiert die Date-Variable per Default in der Form "31.12.2002 16:55:37". An den optionalen zweiten Parameter kann eine der folgenden Konstanten übergeben werden (jeweils mit Beispiel): DateFormat.GeneralDate DateFormat.LongDate DateFormat.ShortDate DateFormat.LongTime DateFormat.ShortTime
31.12.2002 16:55:37 Dienstag, 31. Dezember 2002 31.12.2002 16:55:37 16:55
Das folgende Beispiel demonstriert die Anwendung von FormatDateTime. s = FormatDateTime(Now, DateFormat.ShortTime)
's = "16:55"
VB-spezifische Codes für Format Grundsätzlich können bei Format dieselben Kürzel wie bei den .NET-Formatierungsmethoden verwendet werden. Diese Kürzel werden hier nicht nochmals aufgezählt – siehe Abschnitt 8.5.3! Das folgende Beispiel zeigt die Anwendung des Formatcodes d (kurzes Datumsformat). s = Format(Now, "d")
's = "31.12.2002"
Darüber hinaus kennt Format ein paar weitere Codes, deren Hauptvorteil darin besteht, dass ihre Bedeutung unmittelbar klar ist. (Dafür ist der Tippaufwand größer.) s = format(Now, "Medium Date")
's = "Dienstag, 31. Dezember 2002"
Die folgende Tabelle fasst die VB-spezifischen Codes zusammen und gibt jeweils ein Beispiel, das mit deutschen Landeseinstellungen ermittelt wurde. VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) General Date
31.12.2002 16:55:37
Datum und Zeit
Long Date
Dienstag, 31. Dezember 2002
Datum ausführlich
Medium Date
Dienstag, 31. Dezember 2002
Datum mittel
Short Date
31.12.2002
Datum kompakt
Long Time
16:55:37
Zeit ausführlich
Medium Time
16:55:37
Zeit mittel
8.6 VB-Formatierungsmethoden
353
VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) Short Time
16:55
Zeit kompakt
VB-Hilfsmethoden zur Formatierung von Daten und Zeiten Neben den Format-Methoden enthält die VB-Bibliothek auch einige Hilfsmethoden in der Klasse DateAndTime. Alle Methoden liefern Datum- und Zeitangaben gemäß der aktuellen Ländereinstellung. Methoden der Klasse Microsoft.VisualBasic.DateAndTime (siehe Abschnitt 8.3.1) DateString
liefert das aktuelle Datum (ohne Zeit) als Zeichenkette.
MonthName(n)
liefert den Monatsnamen (für n=1 bis 12).
MonthName(n, True)
liefert den Monatsnamen als Abkürzung (drei Buchstaben).
TimeString
liefert die aktuelle Uhrzeit als Zeichenkette.
WeekdayName(n)
liefert den Wochentag als Zeichenkette (n=1 für Sonntag).
WeekdayName(n, True)
liefert den Wochentag als Abkürzung (zwei Buchstaben)
9
Aufzählungen (Arrays, Collections)
Die .NET-Bibliothek enthält in den Namensräum System.Collections und System.Collections.Specialized eine große Anzahl von Klassen, die bei der Organisation von Aufzählungen helfen, beispielsweise ArrayList, SortedList, DictionaryBase etc. Damit können Sie assoziative Felder erzeugen, sortierte Listen effizient verwalten etc. Dieses Kapitel beschreibt, wie Sie von diesen Klassen abgeleiteten Objekten auswerten (z.B. wenn Sie derartige Aufzählungen als Ergebnis einer .NETMethode erhalten) und wie Sie die Klassen selbst zur Organisation Ihrer Daten einsetzen können.
VERWEIS
9.1 9.2 9.3 9.4
Einführung Klassen- und Schnittstellenüberblick Programmiertechniken Syntaxzusammenfassung
356 359 364 378
Der Umgang mit gewöhnlichen Feldern (die intern von System.Array abgeleitet sind), wird im Kapitel zum Thema Variablenverwaltung in Abschnitt 4.5 behandelt.
356
9.1
9 Aufzählungen (Arrays, Collections)
Einführung
Die einfachste Möglichkeit, mehrere Werte, Zeichenketten oder Objekte in einer Gruppe zu verwalten, bieten die in Abschnitt 4.5 bereits vorgestellten Felder. Felder haben aber den Nachteil, dass die Elementzahl bereits im Voraus bekannt sein muss und dass der Zugriff ausschließlich über einen Integer-Index erfolgen kann. (Wenn diese Einschränkungen für Sie kein Problem darstellen, sollten Sie wegen der großen Effizienz einfach bei Feldern bleiben.) Für viele Anwendungsfälle stellen Felder allerdings nicht die optimale Lösung dar. Deswegen gibt es im Namensraum System.Collections in mscorlib.dll sowie im Namensraum System.Collections.Specialized in System.dll eine ganze Reihe von so genannten Aufzählungsklassen, die die Verwaltung von Daten je nach Anwendung stark vereinfachen können. (Beachten Sie, dass System.Collections bei allen VB.NET-Projekttypen ein Defaultimport ist. Daher funktioniert Dim ht As Hashtable, obwohl Sie System.Collections.Hashtable meinen.) Damit Sie eine erste Vorstellung bekommen, wozu Aufzählungen eingesetzt werden können, beginnt dieses Kapitel mit einigen einfachen Beispielen.
ArrayList-Beispiel Zu den einfachsten Aufzählungen gehört ArrayList. Diese Klasse hat ähnliche Eigenschaften wie ein gewöhnliches Feld. Der Hauptunterschied besteht darin, dass Objekte nach Belieben eingefügt und wieder entfernt werden dürfen. ArrayList wird sehr oft dann eingesetzt, wenn eine (im Vorhinein noch unbekannte Anzahl) von Objekten oder Zeichenketten eingelesen und anschließend sortiert werden soll. In den folgenden Zeilen werden mit der Methode GetFiles alle Dateien im Verzeichnis c:\ ermittelt. (GetFiles liefert ein Feld von IO.FileInfo-Objekten – siehe Abschnitt 10.3.) Die Namen der Verzeichnisse werden mit Add in das ArrayList-Objekt eingefügt. Sort sortiert die Liste. Anschließend werden die Namen in einer For-Each-Schleife im Konsolenfenster ausgegeben. ' Beispiel aufzaehlungen\intro Sub arraylist_sample() Dim ar As New ArrayList() Dim fi As IO.FileInfo Dim o As Object ' alle Namen der Dateien in c:\ in die ArrayList eintragen For Each fi In New IO.DirectoryInfo("c:\").GetFiles() ar.Add(fi.Name) Next
9.1 Einführung
357
' ArrayList sortieren ar.Sort() ' ArrayList ausgeben For Each o In ar Console.WriteLine(o) Next End Sub
Die wenigen Zeilen demonstrieren bereits einige elementare Eigenschaften vieler CollectionKlassen: Vor der Verwendung der Klasse muss ein entsprechendes Objekt mit New erzeugt werden. (Bei ArrayList können Sie als optionalen Parameter die voraussichtliche Anzahl der Elemente angeben. Die tatsächliche Elementzahl darf sowohl größer als auch kleiner sein. Die Angabe einer sinnvollen Startgröße kann aber die interne Verwaltung ein bisschen effizienter machen, weil dann seltener eine automatische Anpassung an die erforderliche Elementzahl notwendig ist.) Elemente können mit Add eingefügt und bei Bedarf durch RemoveAt(n) oder Remove(obj) wieder entfernt werden. Die Elemente der Aufzählung können Sie mit Sort sortieren, mit Reverse auf den Kopf stellen etc. BinarySearch ermöglicht eine sehr effiziente Suche nach einzelnen Elementen. Der Zugriff auf die Aufzählungselemente erfolgt durch arraylistobj(n) oder mit For-EachSchleifen. Dabei müssen Sie beachten, dass Sie die Elemente immer mit dem Typ Object erhalten, unabhängig davon, welche Daten Sie tatsächlich gespeichert haben. Gegebenenfalls müssen Sie mit CType eine Umwandlung in den ursprünglichen Objekttyp vornehmen.
Hashtable-Beispiel Die Hashtable-Klasse bietet das, was in anderen Programmiersprachen oft als assoziatives Feld bezeichnet wird. Das bedeutet, dass Datenpaare verwaltet werden, die jeweils aus dem Schlüssel für den Datenzugriff und dem eigentlichen Datenobjekt bestehen. Das ermöglicht es wie im folgenden Beispiel, Zeichenketten für den Objektzugriff zu verwenden. Mit Add werden vier Objekte unterschiedlichen Typs in die Aufzählung eingefügt, wobei als Schlüssel die Zeichenketten "eins", "zwei" etc. verwendet werden. Der Zugriff auf einzelne Elemente kann nun in der Form ht(schlüsselobjekt) erfolgen. ht("vier") liefert daher ein Objekt des Typs System.Random. In der For-Each-Schleife werden alle Elemente der Aufzählung durchlaufen, wobei sowohl der Schlüssel als auch der Inhalt im Konsolenfenster ausgegeben werden. Beachten Sie, dass die Schleifenvariable ein Objekt des Typs DictionaryEntry sein muss. (Ein DictionaryEntry-Objekt liefert mit den Eigenschaften Key und Value die beiden Komponenten des Datenpaars.)
358
9 Aufzählungen (Arrays, Collections)
' Beispiel aufzaehlungen\intro Sub hashtable_sample() Dim ht As New Hashtable() Dim dictEntry As DictionaryEntry ht.Add("eins", ht.Add("zwei", ht.Add("drei", ht.Add("vier",
1) 23.5) "eine Zeichenkette") New Random())
Console.WriteLine("Element ht(""drei""): " + ht("drei").ToString) For Each dictEntry In ht Console.WriteLine("Key: " + dictEntry.Key.ToString + _ "Daten: " + dictEntry.Value.ToString) Next End Sub
.NET-Aufzählungen auswerten Zahlreiche .NET-Klassen verwenden intern ebenfalls die in diesem Kapitel beschriebenen Aufzählungsklassen. Daher überrascht es wenig, dass viele .NET-Methoden als Ergebnis auch derartige Aufzählungen zurückgeben. Eine Besonderheit besteht allerdings darin, dass solche Methoden oft nicht mit einem Klassentyp wie ArrayList oder Hashtable deklariert sind, sondern mit einer Schnittstelle wie IDictionary oder IList. Beispielsweise liefert die Methode Environment.GetEnvironmentVariables eine Liste aller Namen der Umgebungsvariablen des Betriebssystems samt deren Inhalt. Diese Liste wird als IDictionary-Objekt zur Verfügung gestellt. Was haben nun diese Schnittstellen mit den Aufzählungsklassen zu tun? Da die CollectionsKlassen zum Teil sehr ähnlich sind, haben die .NET-Entwickler versucht, den gemeinsamen Nenner dieser Klassen in Schnittstellen zu formulieren. Beispielsweise realisieren sowohl die ArrayList- als auch die Hashtable-Klasse die ICollection-Schnittstelle. Deswegen können Sie in beiden Fällen mit Count die Anzahl der Elemente feststellen oder mit CopyTo die Elemente in ein gewöhnliches Feld kopieren. Schnittstellen helfen also dabei, den Zugriff auf unterschiedliche Collections-Objekte möglichst einheitlich zu gestalten. Wenn Sie wie im konkreten Beispiel mit einer .NET-Methode konfrontiert sind, dessen Ergebnisobjekt mit einer Schnittstelle deklariert ist, dann können Sie zur Auswertung der Daten alle Methoden und Eigenschaften dieser Schnittstelle verwenden. (Für die wichtigsten Schnittstellen finden Sie eine Referenz dieser Schlüsselwörter in der Syntaxzusammenfassung am Ende dieses Kapitels.) In der Praxis sieht der resultierende Code meist genau gleich aus, als würde er ein gewöhnliches Collection-Objekt auswerten. Die unten abgedruckte For-Each-Schleife entspricht in ihrer Struktur exakt dem vorigen Hashtable-Beispiel.
9.2 Klassen- und Schnittstellenüberblick
359
' Beispiel aufzaehlungen\intro Sub read_idictionary() Dim entry As DictionaryEntry For Each entry In Environment.GetEnvironmentVariables() Console.WriteLine("{0} = {1}", _ entry.Key.ToString, entry.Value.ToString) Next End Sub
HINWEIS
Es ist unmöglich, ein Objekt direkt von einer Schnittstelle zu erzeugen. Daher kann GetEnvironmentVariables genau genommen auch kein IDictionary-Objekt liefern. Aber indem der Typ der Rückgabedaten von GetEnvironmentVariable mit IDictionary deklariert ist, weiß der Compiler und wissen Sie, dass zumindest alle Eigenschaften und Methoden der IDictionary-Schnittstelle zur Verfügung stehen. Die Bezeichnung IDictionary-Objekt ist insofern also nicht ganz korrekt, aber eben auch nicht ganz falsch (und sicherlich einfacher zu verstehen, als wenn ich jedes Mal schreibe: ein Objekt, dessen Klasse die IDictionary-Schnittstelle realisiert). Falls es Sie interessiert, von welcher Klasse dieses Objekt intern nun tatsächlich abgeleitet ist, können Sie das mit TypeName(Environment.GetEnvironmentVariables) feststellen. Bei GetEnvironment handelt es sich um ein Objekt der Klasse Hashtable.
9.2
Klassen- und Schnittstellenüberblick
Wie in der Einführung bereits erwähnt wurde, spielen Schnittstellen eine wichtige Rolle bei der Definition der Merkmale, die eine bestimmte Klasse unterstützen. Bevor dieser Abschnitt daher einen Überblick über die Merkmale der wichtigsten Collections-Klassen gibt, müssen Sie vorher zumindest die wichtigsten Schnittstellen kennen lernen. (Soweit nicht anders angegeben, sind alle in diesem Abschnitt genannten Schnittstellen und Klassen im Namensraum System.Collections der mscorlib.dll definiert.)
Schnittstellenüberblick •
IEnumerable: Diese Schnittstelle ermöglicht es, dass alle Elemente einer Aufzählung komfortabel in einer For-Each-Schleife angesprochen werden können. (Intern liefert dazu die Methode GetEnumerator ein IEnumerator-Objekt, das bei der Ausführung der For-Each-Schleife ausgewertet wird.)
Beachten Sie, dass der Datentyp der Elemente der For-Each-Schleife von der konkreten Realisierung von GetEnumerator abhängt! In vielen Fällen liefert die For-Each-Schleife einfach Object-Daten; bei allen Klassen, die die IDictionary-Schnittstelle realisieren, sollte die Schleifenvariable aber mit dem Typ DictionaryEntry deklariert werden; bei Aufzählungen auf Basis der StringCollection-Klasse bekommen Sie dagegen String-Daten etc.
360
•
9 Aufzählungen (Arrays, Collections)
ICollection: Diese Schnittstelle ermöglicht die Ermittlung der Elementzahl (Count) und
das Kopieren der Elemente in ein einfaches Feld. •
IList: Diese Schnittstelle stellt Methoden zur Verfügung, die den Zugriff und die Verwaltung einfacher Listen ermöglichen (Add, Remove, Contains etc.) Besonders wichtig ist die Eigenschaft Item(n) zum Zugriff auf ein Element der Aufzählung. Item(n) wird selten
explizit verwendet, weil VB.NET diese Eigenschaft intern verwendet, wenn Sie auf Aufzählungselemente in der Form aufzählungsobjekt(n) zugreifen. •
IDictionary: Diese Schnittstelle ist eine Alternative zu IList und hilft bei der Verwaltung
von Datenpaaren. Der entscheidende Unterschied besteht darin, dass der Elementzugriff nun über beliebige Schlüsselobjekte erfolgt (wobei intern ein Hash-Code zu Hilfe genommen wird). Die Schnittstellen stehen in einem hierarchischen Zusammenhang, der aus der folgenden Hierarchiebox hervorgeht. Beispielsweise realisiert jede IDictionary-Schnittstelle automatisch auch die Schnittstellen ICollection und IEnumerable. Hierarchie der System.Collections-Schnittstellen IEnumerable └─ ICollection ├─ IDictionary └─ IList
For-Each-Unterstützung Count, CopyTo assoziativer Zugriff auf Elemente, Add und Remove Indexzugriff (Integer) auf Elemente, Add und Remove
Die folgende Tabelle gibt für die wichtigsten Aufzählungsklassen an, welche der vier obigen Schnittstellen implementiert werden. IEnumerable
ICollection
IList
+ +
+ +
+ +
+ +
+ +
+
HybridDictionary
+ +
+ +
+ +
StringDictionary
+
NameValueCollection
+ +
+ +
+
Array ArrayList StringCollection Hashtable ListDictionary
SortedList
IDictionary
+
Klassenüberblick Wenn Sie Objekte in einer Aufzählung verwalten möchten, können Sie dazu nicht direkt eine Schnittstelle verwenden. Vielmehr müssen Sie auf Klassen zurückgreifen, die diese Schnittstellen realisieren. Dieser Abschnitt stellt die wichtigsten derartigen Klassen vor.
9.2 Klassen- und Schnittstellenüberblick
361
Um das Bild abzurunden, werden in diesem Abschnitt auch drei Möglichkeiten außerhalb der Collections-Klassen genannt: gewöhnliche Felder, Datenverwaltung mit ADO.NET sowie XML-Dokumente. •
Felder: Die in Abschnitt 4.5 bereits beschriebenen Felder sind intern von der Klasse System.Array abgeleitet. Sie bieten die effizienteste Möglichkeit, große Datenmengen von Werttypen (ValueType-Klassen) zu speichern – also z.B. eine Million Integerzahlen. Der Zugriff auf die Elemente erfolgt über einen numerischen Index. Felder werden im angegebenen Datentyp deklariert (z.B. String). Es besteht die Möglichkeit, mehrdimensionale Felder zu verwalten. Nachteile: Felder müssen im Voraus in der richtigen Größe dimensioniert werden. Eine nachträgliche Änderung ist zwar möglich, aber relativ langsam. Es ist nicht möglich, Elemente einzufügen oder zu entfernen.
•
ArrayList: Diese Klasse weist ähnliche Eigenschaften wie ein normales Feld auf. Der
wesentliche Vorteil besteht darin, dass die Aufzählung automatisch wächst oder schrumpft, wenn Elemente mit Add oder Insert eingefügt bzw. mit Remove[At] entfernt werden. Der Zugriff erfolgt über einen Index, also aufzählung(n). Die Aufzählung kann sortiert werden, sortierte Aufzählungen können anschließend sehr effizient durchsucht werden. Im Vergleich zu Feldern bietet die Klasse Methoden wie InsertRange, RemoveRange oder GetRange, um mehrere Elemente gleichzeitig einzufügen, zu löschen oder in ein neues ArrayList-Objekt zu kopieren. (Der Vorteil dieser Methoden liegt vor allem in der höheren Geschwindigkeit.) Nachteile: Wie fast alle Collection-Klassen sind die Elemente der Aufzählung als Object deklariert. Das ist ideal zur Speicherung von Referenztyp-Objekten. Allerdings müssen elementare Datentypen wie Integer oder Double (und generell alle ValueType-Daten) durch ein so genanntes boxing in richtige Objekte umgewandelt werden. Alle CollectionKlassen sind daher im Vergleich zu Feldern ineffizient, wenn große ValueType-Datenmengen verwaltet werden müssen. •
StringCollection: Wenn Sie nicht allgemeine Objekte, sondern nur Zeichenketten verwalten möchten, können Sie statt ArrayList ein Objekt der Klasse Collections.Specialized.StringCollection verwenden. Diese Klasse ist speziell für Zeichenketten optimiert und
daher ein wenig effizienter. Nachteile: Im Gegensatz zu ArrayList bietet StringCollection keine Sortier- und Suchmethoden. •
Hashtable: Diese Klasse ermöglicht es, so genannte assoziative Felder zu erzeugen. Das sind Felder, bei denen als Schlüssel für den Elementzugriff nicht ein Integer-Index,
sondern ein beliebiges Objekt (z.B. eine Zeichenkette) verwendet werden kann. Zur Verwaltung der Elemente wird der so genannte hash-Wert der Schlüsselobjekte verwendet. Intern wird dazu die Methode GetHashCode ausgeführt, die automatisch für jede Klasse zur Verfügung steht. GetHashCode liefert bei den elementaren Datentypen (Zahlen, Zeichenketten etc.) gut gleichverteilte Werte.
362
9 Aufzählungen (Arrays, Collections)
Bei selbst erstellten Klassen liefert die Methode allerdings lediglich eine durchlaufende Nummer. Damit ist zwar sichergestellt, dass jedes Objekt einer Klasse einen eindeutigen hash-Wert hat, aber von Gleichverteilung ist jetzt keine Rede mehr. Außerdem liefern zwei Objekte mit exakt denselben Daten unterschiedliche hash-Werte. (Im Regelfall wäre es sinnvoller, wenn Objekte mit denselben Daten auch denselben hash-Wert hätten.) Wenn Sie also Objekte eigener Klassen als Schlüssel in einer Hashtable verwenden, sollte die Klasse eine eigene GetHashCode-Methode zur Verfügung stellen, um eine effiziente Datenverarbeitung sicherzustellen. Wie bei ArrayList können Elemente mit Add eingefügt und mit Remove entfernt werden. Beim Einfügen von Elementen in eine Hashtable muss darauf geachtet werden, dass derselbe Schlüssel nicht zweimal verwendet wird. Mit der Methode ContainsKeys kann getestet werden, ob ein bestimmter Schlüssel bereits im Einsatz ist. Nachteile: Im Vergleich zu ArrayList besteht keine Möglichkeit, die Elemente der Aufzählung zu sortieren oder nach Elementen zu suchen. Auch die effizienten XxxRangeKommandos fehlen. Es gibt keine Möglichkeit, über einen numerischen Index auf die Elemente zuzugreifen. •
ListDictionary: Wenn Sie überwiegend sehr kleine Aufzählungen verwalten (bis zu zehn Elementen) und dabei Wert auf maximale Effizienz legen, bietet die Klasse Collections.Specialized.ListDictionary beinahe dieselben Eigenschaften wie Hashtable. Der Vorteil
besteht darin, dass der Verwaltungsaufwand geringer ist. •
HybridDictionary: Wenn Sie sich nicht zwischen Hashtable und ListDictionary entscheiden können, ist Collections.Specialized.HybridDictionary die richtige Wahl. Diese Klasse beginnt als ListDictionary, wechselt intern aber zu einer Hashtable, wenn die Elementzahl stark steigt.
•
StringDictionary: Die Klasse Collections.Specialized.StringDictionary ist eine weitere Variante zu Hashtable. Diesmal besteht die Besonderheit darin, dass sowohl der Schlüssel als
auch die Daten ausschließlich Zeichenketten sein können. Der Vorteil besteht abermals in der etwas höheren Effizienz. •
NameValueCollection: Diese Klasse ist eine Sonderform von StringDictionary. Abermals müssen Schlüssel und Daten jeweils Zeichenketten sein. Die Besonderheit besteht darin, dass der Schlüssel nicht eindeutig sein muss, d.h., zu einer Schlüsselzeichenkette können mehrere Datenzeichenketten gespeichert werden.
•
SortedList: Diese Klasse vereint die Merkmale von gewöhnlichen Feldern und der Hashtable-Klasse. Wie bei Hashtable gibt es also einen assoziativen Zugriff auf die gespeicher-
ten Objekte. Darüber hinaus besteht aber die Möglichkeit, auf die einzelnen Elemente und Schlüssel über eine Indexnummer zuzugreifen. Dazu werden alle Elemente automatisch bei jedem Einfüge- bzw. Löschvorgang nach ihren Schlüsseln sortiert. (Bei der New-Methode kann ein IComparer-Objekt angegeben werden, dessen Compare-Methode zum Sortieren verwendet wird.) Nachteil: Intern werden dazu zwei Felder für die Elemente und die Schlüsseln verwaltet, d.h., der Verwaltungsaufwand (vor allem beim Einfügen und Entfernen von Ele-
9.2 Klassen- und Schnittstellenüberblick
363
menten) ist deutlich größer als bei der Hashtable-Klasse. Dies gilt insbesondere für Aufzählungen mit sehr vielen Elementen. •
DataSet (ADO.NET): Auch die Datenbankbibliothek ADO.NET und insbesondere die Klasse DataSet bieten viele Möglichkeiten zur Verwaltung von Daten. ADO.NET ist dann optimal, wenn Sie sehr große Datenmengen in einer externen Datenbank speichern möchten und bei Zugriffen nicht immer die gesamte Datenmenge, sondern nur einzelne Teile lesen möchten.
Nachteile: ADO.NET ist erheblich komplizierter anzuwenden als die in diesem Kapitel beschriebenen Collection-Klassen. Wirklich optimal ist die Anwengung nur in Kombination mit einem externen Datenbanksystem (z.B. SQL-Server oder die Jet-Engine). •
XmlDocument: Diese Klasse aus dem Namensraum System.XML ermöglicht es, ein XML-
Dokument im Speicher abzubilden. Sie können diese Klasse natürlich zum Lesen und Schreiben von XML-Dokumenten verwenden – Sie können die in XML vorgesehenen Mechanismen aber auch zur Verwaltung von Daten einsetzen. Der entscheidende Vorteil gegenüber den Collections-Klassen besteht darin, dass sich XML ausgezeichnet zur Darstellung hierarchischer Zusammenhänge eignet.
HINWEIS
Nachteile: XML bringt eine Menge Overhead mit sich, der sich sowohl im vergleichsweise hohen Speicherbedarf als auch in der relativ geringen Verarbeitungsgeschwindigkeit negativ bemerkbar macht. Die Anwendung der XmlDocument-Klasse ist daher nur sinnvoll, wenn Sie die spezifischen XML-Merkmale wirklich benötigen (oder wenn Sie ohnedies vorhaben, die Daten in einer XML-Datei zu speichern). Neben den aufgezählten Collections-Klassen gibt es noch ein paar Sonderformen, auf deren genauere Beschreibung hier verzichtet wird: Queue ermöglicht die Realisierung einfacher Warteschlangen. Stack realisiert einen first-in-first-out-Puffer (FIFO). BitArray hilft dabei, ein effizientes (Platz sparendes) Feld für Bitwerte zu bilden. BitVector32 bietet ähnliche Funktionen wie BitArray, ist aber auf 32 Bit beschränkt.
VERWEIS
Wenn es Ihnen anhand dieser Beschreibung und den folgenden Beispielen schwer fällt, sich für die richtige Collection-Klasse zu entscheiden, sollten Sie sich als Erstes mit den beiden Klassen ArrayList und Hashtable anfreunden. Für mindestens 90 Prozent aller Anwendungen reichen diese beiden Klassen aus. Einen guten Überblick über die Merkmale der verschiedenen Klassen gibt die Übersichtstabelle in Abschnitt 9.4.2. Hilfreich ist auch die Seite Auswählen einer Auflistungsklasse in der Online-Hilfe: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconselectingcollectionclass.htm
Dort finden Sie eine systematische Anleitung zur Auswahl der richtigen Klasse anhand mehrerer Entscheidungsfragen.
364
9 Aufzählungen (Arrays, Collections)
Aufzählungen selbst programmieren Die oben aufgezählten Collection-Klassen können unmittelbar eingesetzt werden. Es kann aber sein, dass deren Eigenschaften Ihrer Anwendung nicht optimal entsprechen – dann müssen Sie selbst eine Aufzählungsklasse programmieren. Damit Sie auch in diesem Fall das Rad nicht neu erfinden müssen, stehen Ihnen einige XxxBase-Klassen zur Auswahl, auf die Sie Ihren Code aufbauen können. •
CollectionBase: Die Klasse hilft bei der Programmierung von Aufzählungen, die die IList-Schnittstelle realisieren (wie ArrayList).
•
ReadOnlyCollectionBase: Die Klasse erleichtert die Programmierung von Read-OnlyAufzählungen auf das Basis von IList. Im Vergleich zu CollectionBase müssen viel weni-
ger Methoden selbst programmiert werden. •
DictionaryBase: Die Klasse hilft bei der Verwaltung assoziativer Felder auf der Basis der IDictionary-Schnittstelle (wie Hashtable).
•
NameObjectCollectionBase: Damit können Sie eine Klasse bilden, bei der der Elementzugriff wahlweise über einen Index oder über einen String-Schlüssel erfolgt. Intern werden die Schlüsseleinträge sortiert (wie SortedList).
TIPP
VERWEIS
Trotz dieser Basisklassen ist die Programmierung eigener Aufzählungen ziemlich aufwendig. Insbesondere müssen alle Methoden für den Elementzugriff neu implementiert werden, um die Verwendung der korrekten Datentypen sicherzustellen. Um einen komfortablen For-Each-Zugriff auf die Elemente zu ermöglichen, muss des Weiteren eine eigene Klasse programmiert werden, die die IEnumerator-Schnittstelle realisiert.
9.3
Beispiele für die Programmierung eigener Aufzählungsklassen finden Sie unter der folgenden Adresse: http://www.wintellect.com/resources/newsletters/feb2002.asp
Falls Sie SharpDevelop installiert haben (siehe Abschnitt 2.7.2), können Sie dessen Assistenten dazu verwenden, um den Code einer eigenen Collection-Klasse für einen bestimmten Datentyp bequem und rasch zu erzeugen (DATEI|NEU, Kategorie VB, Schablone GETYPTE VB COLLECTION). Den resultierenden Code können Sie anschließend in Ihr VB.NET-Projekt kopieren.
Programmiertechniken
Grundsätzlich hätte ich diesen Abschnitt damit beginnen können, die wichtigsten Schlüsselwörter aller Collections-Schnittstellen und -Klassen im Detail zu beschreiben. Es erschien mir aber sinnvoller, den Platz stattdessen zu nutzen, um einige konkrete Programmiertechniken vorzustellen – z.B. welche Möglichkeiten es gibt, Elemente einer Aufzählung zu
9.3 Programmiertechniken
365
VERWEIS
sortieren. Eine Referenz der wichtigsten Schlüsselwörter samt einer knappen Beschreibung finden Sie in der Syntaxzusammenfassung im nächsten Abschnitt. Bei den meisten Methoden (soweit sie nicht ohnedies im Rahmen der folgenden Beispiele vorgestellt wurden) wird die Anwendung nicht schwer fallen. Wie der Inhalt eines gesamten Felds oder eines Collection-Objekts dank Serialisierung effizient in einer Datei gespeichert bzw. von dort wieder geladen werden kann, erklärt Abschnitt 10.9.
9.3.1
Elemente einer Aufzählung einzeln löschen
Um alle Elemente einer Aufzählung zu löschen, steht bei den meisten Aufzählungsklassen die Methode Clear zur Verfügung. Falls Sie diese Methode nicht anwenden möchten (etwa weil Sie vor dem Löschen jedes Element noch bearbeiten möchten), müssen Sie die Aufzählung mit einer Schleife durchlaufen. Dabei ist aber Vorsicht geboten! Der naheliegende Ansatz, einfach alle Elemente in einer For-Each-Schleife zu löschen, führt zu einem Fehler. Der Grund: Die IEnumerator-Schnittstelle funktioniert nur, solange die Anzahl der Elemente nicht verändert wird. (Das gilt nicht nur für Änderungen innerhalb der For-Each-Schleife, sondern auch für mögliche simultane Änderungen durch andere Threads!) Bei Aufzählungen, deren Elemente über einen Integer-Index angesprochen werden können (z.B. ArrayList-Objekte auf der Basis der IList-Schnittstelle), bieten sich folgende Möglichkeit an. (Statt list.RemoveAt(list.Count-1) funktioniert auch list.RemoveAt(0). Die hier vorgestellte Variante kommt der internen Organisation der Daten aber eher entgegen.) ' Beispiel aufzaehlungen\intro Sub delete_items_list(ByVal list As IList) While list.Count > 0 list.RemoveAt(list.Count-1) End While End Sub
Bei Aufzählungen auf der Basis von IDictionary-Schnittstelle können Sie wie in den folgenden Zeilen zuerst alle Schlüsselobjekte in ein Feld kopieren und dann alle Elemente dieses Felds durchlaufen. Allerdings muss sichergestellt sein, dass sich die Aufzählung während der Ausführung der Schleife nicht ändert. Sub delete_items_dict(ByVal dict As IDictionary) Dim i As Integer Dim k(dict.Count - 1) As Object ' kopiert alle Schlüssel in das Feld k dict.Keys.CopyTo(k, 0) For i = dict.Count - 1 To 0 Step -1 dict.Remove(k(i)) Next End Sub
366
9 Aufzählungen (Arrays, Collections)
9.3.2
Elemente einer Aufzählungen sortieren
Wenn Sie die Elemente einer Aufzählung sortieren möchten, kommt als einzige CollectionsKlasse ArrayList in Frage. Eine Alternative zu ArrayList bilden gewöhnliche Felder, deren Klasse Array ebenfalls eine Sort-Methode kennt. (Bei der Klasse SortedList werden dagegen nicht die eigentlichen Daten, sondern die Schlüssel sortiert – siehe den nächsten Abschnitt!) Bei den folgenden Zeilen wird zur Initialisierung der ArrayList an den New-Konstruktor ein String-Feld übergeben. Zum Sortieren wird einfach die Methode Sort angegeben. ' Beispiel aufzaehlung\sort_data Dim al As New ArrayList( _ New String() {"abc", "ABC", "Abcd", "bar", _ "Bär", "Bären", "Barenboim", _ "bärtig", "27", "100", "Ärger"}) al.Sort()
Wenn Sie sich die Elemente der sortierten ArrayList ansehen, ergibt sich folgende Reihenfolge: 100
27
abc
ABC
Abcd
Ärger
bar
Bär
Bären
Barenboim
bärtig
Sort sortiert per Default im deutschen Sprachraum so, dass es Groß- und Kleinschreibung als fast gleichwertig betrachtet: Ein kleines a wird vor einem großen A sortiert, a und A aber beide vor b etc. Auch deutsche Sonderzeichen werden korrekt berücksichtigt. (Hinter-
gründe über den Vergleich von Zeichenketten finden Sie in Abschnitt 8.2.3.)
IComparable-Schnittstelle Das Sortieren durch Sort funktioniert nur dann, wenn die in der Aufzählung enthaltenen Objekte die IComparable-Schnittstelle unterstützen. Für die Basisdatentypen wie Integer, String oder Date ist das automatisch der Fall. Wenn Sie in einer Aufzählung aber Objekte einer selbst definierten Klasse sortieren möchten, müssen Sie diese Klasse entweder selbst mit dieser Schnittstelle ausstatten, oder Sie müssen an Sort ein Objekt übergeben, das eine IComparer-Schnittstelle realisiert (siehe die nächste Überschrift). Die folgenden Zeilen definieren das Objekt vector2d, das zur Speicherung von zweidimensionalen Vektoren dient. Der Großteil des Codes sollte ohne weitere Erklärung verständlich sein: New ermöglicht eine bequeme Initialisierung des Objekts; Len berechnet die Länge des Vektors; ToString gibt das Objekt in Textform aus. Besonders von Interesse ist hier die Implementierung der IComparable-Schnittstelle: Dazu muss die Klasse die Methode CompareTo zur Verfügung stellen. Die Methode vergleicht das Objekt mit einem zweiten Objekt derselben Klasse. Im Beispiel erfolgt der Vergleich entsprechend der Länge der beiden Vektoren, wobei auf die CompareTo-Methode der Double-Klasse zurückgegriffen wird. Falls das Vergleichsobjekt von einer anderen Klasse abstammt, wird eine ArgumentException ausgelöst.
9.3 Programmiertechniken
367
' Beispiel aufzaehlung\sort_data Class vector2d Implements IComparable Dim x As Double Dim y As Double ' New-Konstruktor Public Sub New() Me.New(0, 0) End Sub Public Sub New(ByVal x As Double, ByVal y As Double) Me.x = x Me.y = y End Sub ' berechnet die Länge des Vektors Public Function Len() As Double Return Math.Sqrt(x ^ 2 + y ^ 2) End Function ' ToString-Methode Public Overrides Function ToString() As String Return "(" + Me.x.ToString + "; " + Me.y.ToString + ")" End Function ' CompareTo-Methode der IComparable-Schnittstelle Overridable Function CompareTo( _ ByVal obj As Object) _ As Integer Implements IComparable.CompareTo If TypeOf obj Is vector2d Then Return Me.Len().CompareTo(CType(obj, vector2d).Len()) Else Throw New ArgumentException() End If End Function End Class
Am schwierigsten zu verstehen ist wahrscheinlich die folgende Zeile des Programmcodes: Return Me.Len().CompareTo(CType(obj, vector2d).Len()) Me.Len() liefert einen Double-Wert, der die Länge der aktuellen Objektinstanz liefert. Dieser Wert soll mit der Länge von obj verglichen werden. Obwohl bereits überprüft wurde, dass obj ein Objekt der Klasse vector2d ist, betrachtet der Compiler obj als Object (weil der Parameter ja so deklariert ist). obj.Len() ist deswegen unzulässig, weil die Klasse Object keine Methode Len kennt. obj muss also mit CType(obj, vector2d) in ein vector2d-Objekt umgewandelt werden. (Diese Typenkonvertierung ist nur für den Compiler wichtig. Tatsächlich enthält obj ja bereits Daten im richtigen Typ. CType(obj, vector2d) verursacht daher keinen Rechenaufwand.) Auf
368
9 Aufzählungen (Arrays, Collections)
das Ergebnis von CType kann nun die Len-Methode angewendet werden, die einen DoubleWert liefert. Me.Len().CompareTo vergleicht somit zwei Double-Werte. Auf der Basis dieser Klasse stellt es nun kein Problem dar, mehrere vector2d-Objekte zu erzeugen, zu sortieren und mit der (nicht abgedruckten) Prozedur print_ilist in einem Konsolenfenster auszugeben. Sub sort_icomparable() Dim ar As New ArrayList() ar.Add(New vector2d(3, 4)) ar.Add(New vector2d(2.9, 4.1)) ar.Add(New vector2d(3.1, 3.9)) ar.Add(New vector2d()) ar.Sort() print_ilist(ar) End Sub
Das Beispielprogramm liefert das folgende Ergebnis: (0; 0)
(3,1; 3,9)
(3; 4)
(2,9; 4,1)
IComparer-Schnittstelle
HINWEIS
Wenn die durch CompareTo vorgegebene Defaultordnung beim Sortieren nicht Ihren Wünschen entspricht oder wenn Sie Objekte von Klassen sortieren möchten, die die IComparableSchnittstelle nicht unterstützen, dann können Sie an Sort ein Objekt übergeben, dessen Klasse die Schnittstelle Collections.IComparer realisiert. Verwechseln Sie IComparer und IComparable nicht! IComparer ist eine externe Schnittstelle, deren Objekt an Sort übergeben wird. Der Vergleich erfolgt durch die Methode Compare, an die zwei Objekte übergeben werden. Im Gegensatz dazu ist IComparable eine Schnittstelle, die innerhalb der Klasse definiert wird. Die Methode CompareTo vergleicht die konkrete Objektinstanz (also Me) mit einem externen Objekt der gleichen Klasse.
Das Ziel des folgenden Beispiels besteht darin, mehrere Zeichenketten mit Namen in der Form "Stephen King" entsprechend der Familiennamen zu sortieren. Um das zu ermöglichen, wurde die Klasse CompareByLastName definiert. Dazu muss die Klasse die Methode Compare anbieten, an die zwei Object-Parameter übergeben werden. Da in diesem Beispiel nur Zeichenketten sortiert werden, können die Objekte mit CType in Zeichenketten umgewandelt werden. Die Funktion last_name_first überprüft nun, ob der Name ein Leerzeichen enthält. Wenn das der Fall ist, wird der letzte Teil des Namens an den Anfang gestellt. Die Funktion macht also aus "John F. Kennedy" die Zeichenkette "Kennedy John F.". Damit enthalten s1 und s2 zwei Namen, bei denen der Familienname am Anfang steht.
9.3 Programmiertechniken
369
Die resultierenden Zeichenketten können nun einfach mit String.Compare verglichen werden. (Diese Methode liefert -1, 0 oder 1, je nachdem, wie die alphabetische Reihenfolge der Zeichenketten ist. An String.Compare kann mit optionalen Parametern angegeben werden, welche Sortierordnung beim Vergleich berücksichtigt werden soll – siehe Abschnitt 8.2.3.) Class CompareByLastName Implements Collections.IComparer Overridable Function Compare( _ ByVal obj1 As Object, ByVal obj2 As Object) As Integer _ Implements IComparer.Compare Dim s1, s2 As String s1 = last_name_first(CType(obj1, String)) s2 = last_name_first(CType(obj2, String)) Return String.Compare(s1, s2) End Function Private Function last_name_first(ByVal s As String) As String Dim pos As Integer Dim lastname, firstname As String s = Trim(s) pos = InStrRev(s, " ") If pos > 0 Then lastname = Trim(Mid(s, pos + 1)) firstname = Trim(Left(s, pos - 1)) If lastname <> "" Then Return lastname + " " + firstname Else Return s End If Else Return s End If End Function End Class
Zum Test dieser Klasse werden einige Zeichenketten mit den Autorennamen verschiedener VB.NET-Bücher initialisiert. Um diese Namen nach den Vornamen zu sortieren, reicht ein einfacher Aufruf der Sort-Methode. Zum Sortieren nach Familiennamen muss beim Aufruf von Sort ein Objekt der Klasse CompareByLastName übergeben werden. Für die Ausgabe der Aufzählungselemente im Konsolenfenster wird die nicht abgedruckte Prozedur print_ilist verwendet.
370
9 Aufzählungen (Arrays, Collections)
Sub sort_icomparer() ' mehrere Zeichenketten in ein String-Feld und in eine ' ArrayList einfügen Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"} Dim al As New ArrayList(s) Console.WriteLine("unsortiert:") print_ilist(al) al.Sort() Console.WriteLine("normal sortiert:") print_ilist(al) Console.WriteLine("nach Familiennamen sortiert:") al.Sort(New CompareByLastName()) print_ilist(al) End Sub
Das Ergebnis des Programms sieht folgendermaßen aus: unsortiert: Holger Schwichtenberg Frank Eller Dan Appleman Gary Cornell Jonathan Morrison Andrew Troelsen
Brian Bischof
normal sortiert: Andrew Troelsen Brian Bischof Dan Appleman Frank Eller Gary Cornell Holger Schwichtenberg Jonathan Morrison
VERWEIS
nach Familiennamen sortiert: Dan Appleman Brian Bischof Gary Cornell Frank Eller Jonathan Morrison Holger Schwichtenberg Andrew Troelsen
Sie finden in diesem Buch noch eine ganze Reihe weiterer Beispiele für das Sortieren unterschiedlicher Daten: Werfen Sie einen Blick in das Stichwortverzeichnis bei den Einträgen sortieren und ICompare-Schnitstelle.
Nach Elementen suchen Wenn Sie ein bestimmtes Element in einer Aufzählung suchen, müssen Sie in der Regel alle Elemente durchlaufen. Wenn die Aufzählung aber bereits sortiert ist, können Sie die Suche ganz wesentlich beschleunigen, indem Sie die Methode BinarySearch zu Hilfe nehmen. Diese Methode benötigt beispielsweise zur Suche in einem Feld mit 1000 Einträgen nur maximal zehn Vergleichvorgänge.
9.3 Programmiertechniken
371
Im einfachsten Fall wird an die Methode ein eindimensionales, durch Sort sortiertes Feld bzw. eine Aufzählung sowie der Suchbegriff übergeben. BinarySearch liefert dann entweder die positive Indexnummer des gefunden Elements oder eine negative Nummer, wenn das Element nicht gefunden wurde; in diesem Fall können Sie das Vorzeichen umdrehen und gelangen dann zum nächstliegenden größeren oder kleineren Eintrag im Feld. Dim i As Integer i = al.BinarySearch("Dan Appleman") Console.WriteLine("Suche nach Dan Appleman: Index = " + _ i.ToString + " Element = " + al(i).ToString)
Wenn Sie das Feld mit einer eigenen Vergleichsmethode sortiert haben, müssen Sie auch an BinarySearch ein IComparer-Objekt übergeben: i = al.BinarySearch("Dan Appleman", New CompareByLastName())
Sortieren und Suchen bei Feldern Grundsätzlich stehen die Methoden Sort und BinarySearch auch für Felder zur Verfügung. Unverständlicherweise sieht aber die Syntax anders aus: Während bei ArrayList die Methode einfach nach dem Objektnamen angegeben wird, müssen hier Felder als Parameter der Methode übergeben werden: Dim i As Integer Dim s() As String = {...} Dim ar As New ArrayList(s) Array.Sort(s) Array.Sort(s, icompobj) i = Array.BinarySearch(s, "xy") i = Array.BinarySearch(s, "xy", icobj)
9.3.3
'entspricht ar.Sort() ' ar.Sort(icompobj) ' ar.BinarySearch("xy") ' ar.BinarySearch("xy", icobj)
Schlüssel einer Aufzählung sortieren
Im vorigen Abschnitt wurde gezeigt, wie die Elemente eines Felds oder einer ArrayList sortiert werden können. Bei Aufzählungsklassen, die direkt auf der IDirectory-Schnittstelle basieren, ist ein Sortieren grundsätzlich unmöglich, weil es keinen durchlaufenden Index zum Objektzugriff gibt. Eine Sortiermöglichkeit bietet nur die Spezialklasse SortedList. Jedes Mal, wenn Sie in ein Objekt dieser Klasse ein Element einfügen oder eines löschen, wird die Reihenfolge der Elemente reorganisiert. Sortiert werden dabei nicht die eigentlichen Daten, sondern die Schlüssel der Datenpaare. Dabei wird per Default die CompareTo-Methode der Objekte verwendet. Alternativ können Sie beim Erzeugen des SortedList-Objekts auch ein IComparerObjekt übergeben, dessen Compare-Methode dann ein individuelles Sortieren ermöglicht (siehe auch den vorigen Abschnitt).
372
9 Aufzählungen (Arrays, Collections)
Beispiel Das folgende Beispiel ermittelt unter Zuhilfenahme der System.Drawing.KnownColor-Aufzählung alle im Grafiksystem GDI bekannten Farben. Dazu ganz kurz einige Hintergrundinformationen: KnowColor enthält für mehr als 150 vordefinierte Farbcodes (einfache Integer-Zahlen). Aus diesen Konstanten können mit der Methode Drawing.Color.FromKnownColor Objekte des Typs Color ermittelt werden. (Mehr Informationen zur Verwendung von Farben finden Sie in Kapitel 16.) Die erste For-Each-Schleife hat also den Zweck, für alle bekannten Farben deren Namen (ein String-Objekt) und die Farbe an sich (ein Color-Objekt) zu ermitteln. Diese beiden Informationen werden mit Add in das SortedList-Objekt eingefügt, wobei der Farbname mit LCase in Kleinbuchstaben umgewandelt wird. Die folgenden Zeilen zeigen einige Möglichkeiten zur Auswertung. Mit sl("farbname") erhalten Sie ein Color-Objekt. Da SortedList aber allgemeingültig für den Datentyp Object deklariert ist, muss mit CType explizit eine Typumwandung durchgeführt werden, damit auch der Compiler erkennt, dass das Objekt die Klasse Color und nicht Object hat. Wenn Sie auf die Elemente in sl mit GetByIndex bzw. GetKey über einen Index zugreifen, sind die Elemente automatisch nach ihrem Namen geordnet. ' Beispiel aufzaehlungen\sort_key ' das Beispiel setzt eine Referenz auf die Bibliothek ' System.Drawing voraus Sub sortedlist_sample() Dim i As Integer Dim colorName As String Dim kc As Drawing.KnownColor Dim col As Drawing.Color Dim sl As New SortedList() ' bildet eine SortedList aller Farbnamen mit den ' dazugehörenden Color-Objekten For Each colorName In System.Enum.GetNames(kc.GetType()) kc = CType(System.Enum.Parse(kc.GetType(), colorName), _ Drawing.KnownColor) col = Drawing.Color.FromKnownColor(kc) sl.Add(LCase(colorName), col) Next ' Auswertung der SortedList Console.WriteLine("sl enthält {0} Farben", sl.Count) col = CType(sl("red"), Drawing.Color) Console.WriteLine("Farbe red: R/G/B = {0}/{1}/{2}", _ col.R, col.G, col.B)
9.3 Programmiertechniken
373
' die alphabetisch ersten zehn Farben For i = 0 To 9 col = CType(sl.GetByIndex(i), Drawing.Color) colorName = CStr(sl.GetKey(i)) Console.WriteLine("Farbe {0}: R/G/B = {1}/{2}/{3}", _ colorName, col.R, col.G, col.B) Next End Sub
Im Konsolenfenster erscheint das folgende Ergebnis: sl enthält 167 Farben Farbe red: R/G/B = 255/0/0 Farbe activeborder: R/G/B = 212/208/200 Farbe activecaption: R/G/B = 10/36/106 Farbe activecaptiontext: R/G/B = 255/255/255 Farbe aliceblue: R/G/B = 240/248/255 Farbe antiquewhite: R/G/B = 250/235/215 Farbe appworkspace: R/G/B = 255/255/255 Farbe aqua: R/G/B = 0/255/255 ...
9.3.4
Datenaustausch zwischen Aufzählungsobjekten
Es gibt verschiedene Wege, um Daten von einer Aufzählung in eine andere oder in ein Feld zu kopieren bzw. um neue Zugriffsformen auf einmal gespeicherte Daten zu erlangen. Dieser Abschnitt stellt die wichtigsten dieser Wege vor.
Daten in ein Feld kopieren Fast alle Aufzählungsklassen implementieren die ICollection-Schnittstelle. Damit steht die Methode CopyTo zur Verfügung, mit der Sie die Elemente aus einer Aufzählung in ein Feld kopieren können. In der einfachsten Form kopiert CopyTo alle Elemente in ein Feld, das vorher entsprechend groß dimensioniert werden muss. Als Parameter müssen ein ausreichend großes Feld und ein Startindex (meist 0) angegeben werden. Manche Klassen stellen darüber hinaus mehrere CopyTo-Varianten zur Verfügung, mit denen die Anzahl der zu kopierenden Elemente limitiert werden kann. Dim al As New ArrayList() al.Add ... Dim obj(al.Count - 1) As Object al.CopyTo(obj, 0)
Beim Kopieren versucht CopyTo, die Objekte automatisch in den Datentyp des Felds zu kopieren. Wenn das nicht gelingt, kommt es zu einem Fehler. (Wenn sich die Ausgangsdaten in einer Hashtable befinden, werden DictionaryEntry-Objekte kopiert. Das Feld muss daher mit Object oder mit DictionaryEntry deklariert werden.) Wenn die Elemente der Auf-
374
9 Aufzählungen (Arrays, Collections)
zählung auf Referenztypen verweisen, werden nicht die Objekte selbst kopiert, sondern die Verweise auf die Objekte. (Das ist ein so genanntes shallow copy.)
Daten in eine Aufzählung kopieren Auch für den Datentransport in die umgekehrte Richtung sind bei den meisten Klassen Methoden vorgesehen. Bei einer Reihe von Collections-Objekten kann an New ein ICollectionObjekt übergeben werden. Damit werden die so angegebenen Elemente bereits bei der Erzeugung der Aufzählungsobjekts dorthin kopiert. Dim s() As String = {"a", "bB", "ccCC", "d"} Dim al As New ArrayList(s)
Mit der Methode AddRange können weitere Elemente eingefügt werden, die abermals aus einem ICollection-Objekt (also z.B. aus einem Feld) stammen. al.AddRange(s)
Unveränderliche ArrayList-Objekte erzeugen Die ArrayList-Methoden FixedSize und ReadOnly liefern in ihrer Größe bzw. in ihrem Inhalt unveränderliche ArrayList- bzw. IList-Objekte. Die Methoden werden überwiegend dann verwendet, wenn Sie in einer eigenen Funktion oder Methode eine Aufzählung als Ergebnis zurückgeben, aber vermeiden möchten, dass der Adressat der Daten diese verändert. ' al ist ein ArrayList-Objekt Dim readonly_list As ArrayList readonly_list = ArrayList.ReadOnly(al)
Die Ausgangsdaten, die ebenfalls in Form eines ArrayList- oder IList-Objekts vorliegen müssen, werden dabei nicht kopiert. Stattdessen wird eine neue Zwischenschicht (ein so genannter wrapper) zum Zugriff auf die Daten erzeugt. Wenn Sie nun via readonly_list auf die Aufzählung zugreifen, können Sie weder Elemente hinzufügen oder löschen, noch können Sie Objekte ändern. Hingegen ist die Aufzählung via al sehr wohl noch veränderlich! Wenn Sie also al.Remove("a") ausführen, verschwindet das Objekt sowohl aus al als auch aus readonly_list!
ArrayList-Adapter Mit der Methode ArrayList.Adapter können Sie um ein beliebiges IList-Objekt eine ArrayListVerwaltungsschicht legen. Das bedeutet, dass Sie alle ArrayList-Methoden auf das IList-Objekt anwenden können, soweit das Objekt dies zulässt. (Hier liegt eine wesentliche Grenze des Verfahrens: Sie können zwar mit ArrayList.Adapter ein ArrayList-Objekt aus einem gewöhnliches Feld bilden, die ArrayList-Methoden Add und Remove bleiben Ihnen aber weiterhin verwehrt, weil Felder als IList-Objekte mit FixedSize=True realisiert sind. Die Elementzahl kann also auch über die ArrayList-Schicht nicht verändert werden.)
9.3 Programmiertechniken
375
Das in Abbildung 9.1 dargestellte Beispielprogramm ist ein Vorgriff auf die Kapitel zur Windows-Programmierung. Es verwendet das in Abschnitt 14.6.1 vorgestellte ListBoxSteuerelement, mit dem eine Liste in einem Fenster angezeigt werden kann. Dieses Steuerelement bietet zwar die Möglichkeit, seine Listenelemente alphabetisch zu sortieren, Sie haben aber keinen Einfluss auf die dabei eingesetzte Vergleichsmethode. (Sie können also kein eigenes IComparer-Objekt angeben.) Diese Einschränkung kann mit einem ArrayListAdapter umgangen werden.
Abbildung 9.1: Die Elemente des Listenfelds wurden mit ArrayList.Sort sortiert
Der Programmcode ist nur verständlich, wenn Sie schon einmal ein Windows-Programm gesehen haben – aber unter dieser Voraussetzung sollte es keine Verständnisprobleme geben. Die Programmausführung beginnt (nach einigen Initialisierungsarbeiten) in Form_Load. Dort werden in das Listenfeld einige Namen eingefügt. Außerdem wird mit ArrayList.Adapter ein ArrayList-Objekt erzeugt, das auf die Liste verweist. Durch das Anklicken des Buttons wird die Prozedur Button1_Click ausgeführt. Dort wird die Sort-Methode auf das ArrayList-Objekt angewendet. Als Parameter wird ein Objekt der Klasse CompareByLastName übergeben. Damit werden die Listeneinträge nicht einfach alphabetisch sortiert, sondern unter Anwendung der Familiennamen. ' Beispiel aufzaehlungen\arraylist_adapter Dim al As ArrayList Private Sub Form1_Load(...) Handles MyBase.Load ListBox1.Items.AddRange(New String() _ {"Holger Schwichtenberg", ... "Andrew Troelsen"}) al = ArrayList.Adapter(ListBox1.Items) End Sub Private Sub Button1_Click(...) Handles Button1.Click al.Sort(New CompareByLastName()) End Sub Class CompareByLastName ... wie in Abschnitt 9.3.2
376
9 Aufzählungen (Arrays, Collections)
VERWEIS
9.3.5
Multithreading
Dieser Abschnitt setzt voraus, dass Sie sich schon ein wenig in das Thema Multithreading eingelesen haben. Multithreading bezeichnet die nahezu gleichzeitige Ausführung mehrere Programmteile in eigenen Teilprozessen (Thread). Weitere Informationen finden Sie in Abschnitt 12.6.
Wenn Sie in Multithreading-Anwendungen auf Collections-Objekte zugreifen, tritt das Problem auf, dass der Datenzugriff und insbesondere die Veränderung von Daten nicht Thread-sicher ist. Wenn ein Thread die Aufzählung verändert, während in einem zweiten Thread eine Schleife durchlaufen wird, kommt es zu einem Fehler. Die Prozedur threading_error demonstriert diesen Fehler. Im Start-Thread des Programms wird mit Do-Loop eine Endlosschleife ausgeführt. Innerhalb dieser Schleife werden mit ForEach alle Elemente der Aufzählung al durchlaufen. Die Endlosschleife wird circa zwei Mal pro Sekunde durch den Aufruf der Prozedur addItemEverySecond unterbrochen. Diese Prozedur wird in einem eigenen Thread ausgeführt und fügt eine Zufallszahl zwischen 0 und 1000 in die Aufzählung ein. Das Programm endet nach ein paar Sekunden mit dem Fehler InvalidOperationException, der dadurch ausgelöst wird, dass das ArrayList verändert wird, während die For-Each-Schleife ausgeführt wird. ' Beispiel aufzaehlungen\multi-threading Dim al As New ArrayList() Sub threading_error() ' addItemEverySecond alle 200 ms aufrufen Dim timerDelegate As New Threading.TimerCallback( _ AddressOf addItemEverySecond) Dim tm As New Threading.Timer(timerDelegate, Nothing, 0, 200) Dim obj As Object Do For Each obj In al Console.Write("{0} ", obj) Next Console.WriteLine() Loop End Sub Sub addItemEverySecond(ByVal status As Object) al.Add(CInt(Rnd() * 1000)) End Sub
9.3 Programmiertechniken
377
SyncRoot-Eigenschaft Zum Glück ist es einfach, diesen Fehler zu vermeiden: Sie müssen nur jeden Zugriff auf die Aufzählung, der eventuell problematisch sein könnte, synchronisieren. Dazu kapseln Sie sowohl die For-Each-Schleife als auch die Add-Methode durch die SyncLock-Anweisung. Als Parameter geben Sie dabei die SyncRoot-Eigenschaft an, die bei allen von ICollection abgeleiteten Klassen zur Verfügung steht. SyncRoot liefert ein Objekt, das explizit zur Synchronisierung von Aufzählungen vorgesehen ist. Um zu vermeiden, dass diese neue Version des Programms nun endlos läuft, wird das Programm beendet, wenn die Aufzählung mehr als 15 Elemente enthält. Natürlich hat diese Vorgehensweise auch einen Nachteil: Die Synchronisierung verlangsamt den Zugriff der beiden Threads auf die Aufzählung. Wenn die Aufzählung gerade durch einen Thread blockiert ist, muss der andere Thread auf das Ende der Blockade warten. Sub threading_without_error() ' addItemEverySecond alle 200 ms aufrufen Dim timerDelegate As New Threading.TimerCallback( _ AddressOf addItemEverySecondThreadSafe) Dim tm As New Threading.Timer(timerDelegate, Nothing, 0, 200) Dim obj As Object Do SyncLock al.SyncRoot For Each obj In al Console.Write("{0} ", obj) Next End SyncLock Console.WriteLine() If al.Count > 15 Then tm.Change(0, Threading.Timeout.Infinite) 'Timer-Thread tm.Dispose() 'abschalten Console.WriteLine("Return drücken") Console.ReadLine() Exit Sub 'Programmende End If Loop End Sub Sub addItemEverySecondThreadSafe(ByVal status As Object) SyncLock al.SyncRoot al.Add(CInt(Rnd() * 1000)) End SyncLock End Sub
378
9 Aufzählungen (Arrays, Collections)
9.4
Syntaxzusammenfassung
9.4.1
Schnittstellen
IEnumerable- und IEnumerator-Schnittstelle Zur Anwendung der Schnittstellen IEnumerable und IEnumerator formulieren Sie einfach eine For-Each-Schleife für das Aufzählungsobjekt. Mit den Interna dieser Schnittstellen müssen Sie sich nur dann beschäftigen, wenn Sie selbst Klassen programmieren möchten, die diese Schnittstelle realisieren. Collections.IEnumerable-Schnittstelle GetEnumerator()
liefert ein Objekt mit IEnumerator-Schnittstelle.
Collections.IEnumerator-Schnittstelle Current
verweist auf das aktuelle Objekt (Typ Object).
MoveNext()
geht weiter zum nächsten Objekt. Liefert False, wenn das Ende der Aufzählung erreicht ist.
Reset()
geht zurück an den Start.
Collections.IDictionaryEnumerator-Schnittstelle Entry
verweist auf das aktuelle Objekt (Typ Collections.DictionaryEntry).
Key
verweist auf den aktuellen Schlüssel (Object).
Value
verweist auf die aktuellen Daten (Object).
ICollection-Schnittstelle Collections.ICollection-Schnittstelle CopyTo(f, n)
kopiert die Elemente der Aufzählung in ein eindimensionales Feld beginnend mit f(n).
Count
liefert die Anzahl der Elemente in der Aufzählung.
IsSynchronized
gibt an, ob die Aufzählung synchroniziert ist. (Das bedeutet, dass sichergestellt ist, dass kein anderer Thread die Daten ändert.) IsSynchronized liefert bei fast allen Collection-Objekten False.
SyncRoot
liefert ein Objekt, das (mit SyncLock aufzählung.SyncRoot) zur Synchronisierung der Aufzählung verwendet werden kann.
9.4 Syntaxzusammenfassung
379
IList- und IDictionary-Schnittstelle Beachten Sie, dass sämtliche Methoden der Schnittstellen IList und IDictionary optional sind. Sie können sich also nicht darauf verlassen, dass Sie bei einer Klasse mit diesen Schnittstellen tatsächlich eine bestimmte Methode ausführen können. Im Regelfall geben IsFixedSize und IsReadOnly Auskunft über die Merkmale der Klasse, aber selbst diese beiden Eigenschaften sind optional. (Eigentlich führen diese Schnittstellen die Idee von Schnittstellen ad absurdum. Eine Schnittstelle ist ja ein Vertrag, bestimmte Methoden zur Verfügung zu stellen. Wenn aber alle Regeln des Vertrags mit der Klausel "wenn xy implementiert ist, dann ..." beginnen, fragt man sich, wozu es überhaupt einen Vertrag gibt.) Die Eigenschaft collobj.Item(x) verwenden Sie normalerweise nicht explizit. Stattdessen ist in VB.NET wie bei Feldern die Kurzschreibweise collobj(x) üblich. Collections.IList-Schnittstelle Add(obj)
fügt ein Objekt ein.
Clear()
löscht alle Elemente der Aufzählung.
Contains(obj)
testet, ob das Objekt bereits in der Aufzählung enthalten ist.
IndexOf(obj)
ermittelt den Index des Objekts. Liefert -1, wenn das Objekt nicht enthalten ist.
IsFixedSize
gibt an, ob die Anzahl der Elemente der Aufzählung unveränderlich ist. In diesem Fall stehen Add und Remove[At] nicht zur Verfügung.
IsReadOnly
gibt an, ob einzelne Elemente der Aufzählung verändert werden dürfen.
Item(n)
liefert das Objekt an der Indexposition n.
RemoveAt(n)
entfernt das Objekt an der Indexposition n aus der Aufzählung.
Remove(obj)
entfernt das angegebene Objekt aus der Aufzählung.
Collections.IDictionary-Schnittstelle Clear, Contains, IsFixedSize, IsReadOnly, Remove
funktionieren wie bei IList.
Add(keyobj, dataobj)
fügt das Objekt dataobj mit dem Schlüssel keyobj in die Aufzählung ein.
GetEnumerator()
liefert ein Objekt mit IDictionaryEnumerator-Schnittstelle. In For-Each-Schleifen werden DictionaryEntry-Objekte durchlaufen.
Item(keyobj)
liefert das Datenobjekt zum Schlüssel keyobj.
Keys
liefert ein ICollection-Objekt mit allen Schlüsselobjekten.
Values
liefert ein ICollection-Objekt mit allen Datenobjekten.
380
9.4.2
9 Aufzählungen (Arrays, Collections)
Klassen
Die folgende Tabelle gibt einen Überblick über die wichtigsten Klassen und ihre Merkmale. Schlüssel Daten Datentyp eindeutig sortieren Datentyp einfügen sortieren Array
Integer
+
beliebig
ArrayList
Integer
Object
StringCollection
Integer
+ +
Hashtable
Object
ListDictionary
Object
+ +
Object Object
+ +
HybridDictionary
Object
+
Object
+
StringDictionary
String
+
String
NameValueCollection
String
String
+ +
SortedList
Object / Integer
+
Object
+
CollectionBase
Integer
+
beliebig
+
ReadOnlyCollectionBase
Integer
beliebig
DictionaryBase
beliebig
+ +
NameObjectCollectionBase
String / Integer
+
String
+
+
+ + +
beliebig
+
beliebig
+
+
(+)
ArrayList-Klasse Die Klasse ArrayList realisiert die Schnittstellen IEnumerable, ICollection und IList. Die folgende Tabelle beschreibt nur solche Eigenschaften bzw. Methoden, die nicht ohnedies durch die Schnittstellen vorgegeben sind. Beim Erzeugen eines neuen Objekts durch New kann optional die voraussichtliche Anzahl der Aufzählungselemente angegeben werden. Collections.ArrayList-Klasse BinarySearch
ermöglicht eine sehr effiziente Suche nach Elementen, wenn die Aufzählung vorher sortiert wird.
Capacity
gibt an, wie viele Elemente die Aufzählung enthalten kann, bevor sie automatisch vergrößert wird.
Count
gibt an, wie viele Elemente die Aufzählung tatsächlich enthält.
GetRange
liefert eine Teilliste der Aufzählung.
9.4 Syntaxzusammenfassung
381
Collections.ArrayList-Klasse InsertRange
fügt mehrere Elemente, die als ICollection-Objekt angeben werden, in die Aufzählung ein.
LastIndex
sucht das letzte Element der Aufzählung, das ein bestimmtes Objekts enthält bzw. darauf verweist.
RemoveRange
entfernt mehrere Elemente gleichzeitig.
ArrayList.Repeat
liefert ein ArrayList-Objekt, das ein angegebenes Objekt n-Mal enthält.
Reverse
dreht die Reihenfolge der Elemente der Liste um.
Sort
sortiert die Elemente (optional unter Angabe eines IComparer-Objekts zur Durchführung des Objektvergleichs).
Synchronized
liefert eine synchronisierte Version des ArrayList-Objekts.
ToArray
kopiert die Elemente in ein Feld.
TrimToSize
gibt nicht benötigten Speicher für weitere Elemente frei.
Hashtable-Klasse Die Klasse Hashtable realisiert die Schnittstellen IEnumerable, ICollection und IDictionary. Darüber hinaus gibt es nur wenige wichtige Methoden. Collections.Hashtable-Klasse Contains
testet, ob das angegebene Objekt in der Aufzählung gespeichert ist.
ContainsKey
testet, ob der angegebene Schlüssel bereits in Verwendung ist.
SortedList-Klasse Die Klasse SortedList unterstützt unter anderem die Schnittstellen IEnumerable, ICollection und IDictionary. Die folgende Tabelle beschreibt die wichtigsten Methoden, die darüber hinausgehen. Collections.SortedList-Klasse GetByIndex
liefert das Objekt zur angegebenen Indexnummer.
GetKey
liefert den Schlüssel zur Indexnummer.
GetKeyList
liefert ein IList-Objekt mit allen Schlüsseln.
GetValueList
liefert ein IList-Objekt mit allen Datenelementen.
IndexOfKey
liefert die Indexnummer zum angegebenen Schlüssel.
382
9 Aufzählungen (Arrays, Collections)
Collections.SortedList-Klasse IndexOfValue
liefert die Indexnummer des ersten Elements, das das angegebene Objekt enthält.
RemoveAt
entfernt das Objekt mit der angegebenen Indexnummer.
10 Dateien und Verzeichnisse Im Mittelpunkt dieses Kapitels steht der .NET-Namensraum System.IO. Er enthält zahlreiche Klassen, mit denen Sie komfortabel den Verzeichnisbaum durchlaufen, Verzeichnisse erstellen und löschen sowie Dateien in verschiedenen Formaten (binär, Unicode-Text, ASCII-Text) lesen und verändern können. Das Kapitel geht auch auf einige IO-Besonderheiten ein, etwa auf asynchrone Dateioperationen, auf die Speicherung (Serialisierung) von Objekten und auf die Überwachung des Dateisystems. 10.1 10.2 10.3 10.4 10.5 10.6 10.7 10.8 10.9 10.10
Einführung und Überblick Klassen des System.IO-Namensraums Laufwerke, Verzeichnisse, Dateien Standarddialoge Textdateien lesen und schreiben Binärdateien lesen und schreiben Asynchroner Zugriff auf Dateien Verzeichnis überwachen Serialisierung IO-Fehler
384 386 389 416 421 435 449 456 458 468
384
10.1
10 Dateien und Verzeichnisse
Einführung und Überblick
Dieser Abschnitt gibt einen ersten Überblick über die zahlreichen Varianten (Bibliotheken, Klassen, Steuerelemente), die beim Umgang mit Dateien und Verzeichnissen helfen.
Kommandos und Bibliotheken zum Zugriff auf Dateien •
VB-Kommandos: Am Anfang (und damit meine ich Visual Basic seit Version 1!) gab es zum Zugriff auf Dateien und Verzeichnisse eine Reihe von an sich eigenständigen Kommandos wie CurDir, ChDir, FileCopy, Open, Read und Write. Das Konzept dieser Kommandos erinnert vielfach an die aus DOS-Zeiten vertrauten Kommandos. Diese Kommandos stehen aus Kompatibilitätsgründen innerhalb der Klasse Microsoft.VisualBasic.FileSystem noch immer zur Verfügung. (Falls Sie Get und Put vermissen: diese beiden Kommandos wurden in FileGet und FilePut umbenannt.) Der vielleicht einzige Grund, diese Kommandos noch immer einzusetzen, sind so genannte Random-Access-Dateien. Das sind Dateien, in denen Datensätze mit vorgegebener Länge relativ effizient gespeichert werden können. Allerdings empfiehlt Microsoft schon seit Jahren und mit vielen guten Gründen, derartige Daten in einer Datenbank zu speichern.
•
FSO (File Scripting Objects): In VB6 propagierte Microsoft die FSO-Bibliothek als Lösung aller Probleme, die im Umfeld des Dateizugriffs auftreten können. Tatsächlich bot die FSO-Bibliothek einen eleganten Zugriff auf das Dateisystem. Der größte Mangel bestand darin, dass Binärdateien weder ordentlich ausgelesen noch selbst erzeugt werden konnten. Die FSO-Bibliothek (Microsoft Scripting Runtime, Datei scrrun.dll) kann natürlich auch in VB.NET weiterhin benutzt werden. Allerdings handelt es sich um eine COM-Bibliothek, d.h., Sie nehmen mit der Nutzung den Overhead in Kauf, der durch die Kompatibilitätsschicht zwischen .NET und COM entsteht. Außerdem bietet die FSO-Bibliothek keine Vorteile gegenüber System.IO, wenn man mal davon absieht, dass bereits erworbenes Know-how weiter genutzt werden kann.
•
System.IO: Im Mittelpunkt dieses Kapitels steht die aktuellste Lösung, die Microsoft für den Zugriff auf Dateien anbietet, nämlich die System.IO-Klassenbibliothek. System.IO ist gewissermaßen der Nachfolger der FSO-Bibliothek. Gegenüber der FSO-Bibliothek bietet System.IO eine Menge Neuerungen: den Binärzugriff auf Dateien, Ereignisse zur Überwachung der Dateiaktivität, die Unterscheidung zwischen synchronen (Threadsicheren) und asynchronen Operationen, die Unterstützung verschiedener Codierungen (insbesondere Unicode UTF8) etc. Genau genommen ist System.IO keine eigenständige Bibliothek, sondern ein Bestandteil (Namensraum) von zwei Bibliotheken. Die meisten Objekte sind in der mscorlib-Bibliothek (Datei mscorlib.dll) enthalten, einige weniger oft benötigte Objekte in der systemBibliothek (Datei System.dll).
10.1 Einführung und Überblick
385
Das System.IO-Objektmodell ist grundsätzlich inkompatibel zur FSO-Bibliothek (auch wenn es einige Ähnlichkeiten gibt). FSO-Erfahrung hilft also beim Erlernen des System.IO-Objektmodells, erspart aber nicht einen gewissen Einarbeitungsaufwand. •
VERWEIS
Zugriff auf Active-Directory-Objekte: System.IO hilft nur beim Zugriff auf ein herkömmliches Dateisystem. Wenn Sie auf Objekte in einem Active Directory oder einem anderen Verzeichnisdienst (LDAP, IIS, NDS) zugreifen möchten, können Sie dazu die System.DirectoryServices-Bibliothek zu Hilfe nehmen (die in diesem Buch aber nicht beschrieben wird). Nicht behandelt werden in diesem Buch auch Klassen zum Lesen und Schreiben von XML-Dateien. Sie finden diese Klassen im Namensraum System.XML. XML wird in meinem zweiten VB.NET-Buch zu Datenbanken und Internet ausführlich beschrieben.
Steuerelemente und Komponenten •
OpenFileDialog und SaveFileDialog: Genau genommen handelt es sich hierbei nicht um
gewöhnliche Steuerelemente, sondern um im Formular unsichtbare Objekte, mit deren Methoden Sie einen eigenständigen Dialog zur Dateiauswahl anzeigen können. Die Dialogklassen sind Teil der System.Windows.Forms-Bibliothek. Ihre Anwendung wird in Abschnitt 10.4.1 beschrieben. •
FileSystemWatcher: Auch hierbei handelt es sich nicht wirklich um ein Steuerelement, sondern um eine ganz gewöhnliche Klasse aus dem System.IO-Namensraum, die bei Windows-Anwendungen über die Toolbox (Gruppe Komponenten) in das Programm eingefügt werden kann. Ein FileSystemWatcher-Objekt ermöglicht es, Änderungen im Dateisystem sehr einfach zu verfolgen. Es löst dazu bei jeder Veränderung ein Ereignis aus. Informationen zur Anwendung des Steuerelements finden Sie in Abschnitt 10.8.
•
DirectoryEntry und DirectorySearcher: Diese beiden Klassen sind ebenfalls über die Toolbox (Gruppe Komponenten) zugänglich. Sie helfen dabei, auf Objekte in einem Active Directory zuzugreifen bzw. diese zu finden. Die Steuerelemente sind Teil der System.DirectoryServices-Bibliothek, die in diesem Buch nicht beschrieben wird.
•
DriveListBox, DirListBox und FileListBox: Die seit VB1 vertrauten Steuerelemente zei-
gen in Listenfeldern alle Laufwerke des Rechners, deren Verzeichnisse und Dateien an. Diese Steuerelemente werden unter .NET nicht mehr offiziell unterstützt und daher auch in der Toolbox nicht angezeigt. Mit TOOLBOX ANPASSEN können Sie die Steuerelemente aber manuell aktivieren (Dialogblatt .NET-FRAMEWORK-KOMPONENTEN, AssemblyName Microsoft.VisualBasic.Compatibility). In diesem Buch wird auf eine Beschreibung verzichtet.
386
10 Dateien und Verzeichnisse
10.2
Klassen des System.IO-Namensraums
HINWEIS
Dieser Abschnitt gibt einen Überblick über die System.IO-Klassen und ihre Anwendung. Im Detail werden die meisten Klassen dann im Verlauf der weiteren Abschnitte vorgestellt. Wenn Sie sich die System.IO-Klassen im Objektbrowser ansehen, beachten Sie bitte, dass die Klassen auf zwei gleichnamige Namensräume verteilt sind: System.IO in mscorlib (Datei mscorlib.dll) sowie System.IO in system (also der system-Klassenbibliothek System.dll).
Dateien und Verzeichnisse bearbeiten IO.File und IO.Directory: Diese Klassen enthalten Methoden, um Dateien bzw. Verzeichnisse direkt zu bearbeiten, ohne vorher Objekte zu erzeugen. Beispielsweise können Sie mit IO.File.Move("C:\name1", "C:\name2") eine Datei umbenennen. IO.FileInfo und IO.DirectoryInfo: Die von diesen Klassen abgeleiteten Objekte dienen in
erster Linie dazu, Informationen über Dateien und Verzeichnisse zu ermitteln (Größe, Datum der letzten Änderung, alle Dateien innerhalb eines Verzeichnisses etc.). Sie können die Objekte aber auch direkt bearbeiten (kopieren, löschen etc.). IO.Path: Diese Klasse enthält eine Reihe von Methoden, die bei der Manipulation von Datei- und Verzeichnisnamen helfen. Beispielsweise können Sie mit GetPathRoot das Laufwerk eines Verzeichnispfades extrahieren, mit Combine zwei Verzeichnisse aneinanderfügen etc. GetTempPath und GetTempFileName liefern das temporäre Verzeichnis bzw.
einen Dateinamen für eine temporäre Datei.
Dateien lesen und schreiben IO.StreamReader und IO.StreamWriter: Mit diesen Klassen können Sie Textdateien komfortabel lesen und schreiben. Per Default gilt dabei das Unicode-UTF8-Format, es kann aber auch eine andere Codierung gewählt werden. Der Dateizugriff ist rein sequentiell, es ist daher nicht möglich, die Schreib- oder Leseposition unmittelbar zu beeinflussen. IO.FileStream: Diese Klasse ermöglicht den Low-Level-Zugriff auf Dateien. Sie können damit einzelne Bytes lesen bzw. (über-)schreiben. Dabei können Sie die Lese- bzw. Schreibposition (den Dateizeiger) jederzeit verändern. Die Lese- und Schreiboperationen können wahlweise synchron oder asynchron erfolgen. IO.BinaryReader und IO.BinaryWriter: Diese Klassen ermöglichen wie FileStream den Binärzugriff auf Dateien. Allerdings unterstützen die Read- und Write-Methoden die meisten
.NET-Datentypen, so dass Sie sich nicht um jedes einzelne Byte zu kümmern brauchen.
10.2 Klassen des System.IO-Namensraums
387
Dateisystem überwachen IO.FilesystemWatcher: Mit dieser Klasse können Sie ein Verzeichnis des Dateisystems über-
wachen (nur unter Windows NT/2000/XP). Jedes Mal, wenn innerhalb dieses Verzeichnisses eine Datei verändert wird, kommt es zum automatischen Aufruf einer Ereignisprozedur.
Fehlerbehandlung IO.*Exception: Diese Klassen beschreiben einige Fehler (Exceptions), die bei der Bearbeitung von System.IO-Objekten auftreten können. Neben diesen Fehlern gibt es aber noch weitere
Fehlermöglichkeiten (siehe auch Abschnitt 10.10).
Sonstige IO.FileSystemInfo: Die Klassen FileInfo und DirectoryInfo sind von FileSystemInfo abgeleitet.
Diese Klasse stellt einige gemeinsame Methoden und Eigenschaften zur Verfügung. IO.TextReader und IO.TextWriter: Das sind die Basisklassen für StringReader bzw. -Writer sowie StreamReader. Diese Klassen stellen einige gemeinsame Eigenschaften zur Verfügung, werden aber üblicherweise nicht direkt verwendet. IO.StringReader und IO.StringWriter: Ähnlich wie StreamReader und StreamWriter können Sie mit den Methoden Text lesen bzw. schreiben. Der wesentliche Unterschied besteht darin, dass als Datenquelle bzw. als Ziel nun Zeichenketten statt Dateien gelesen bzw. geschrieben werden. IO.Stream: Das ist die Basisklasse für FileStream, BufferedStream, MemoryStream sowie einige weitere Stream-Klassen außerhalb des System.IO-Namensraums. IO.BufferedStream: Diese Klasse optimiert wiederholte Lese- oder Schreibvorgänge aus bzw. in IO.Stream-Objekten. IO.MemoryStream: Diese Klasse funktioniert wie FileStream; allerdings wird der Datenstrom im Arbeitsspeicher verwaltet (statt in einer Datei).
Objekthierarchie im System.IO-Namensraum Die folgenden Diagramme zeigen, wie die Klassen voneinander abgeleitet sind. Die in diesem Kapitel beschriebenen System.IO-Klassen sind fett hervorgehoben.
388
10 Dateien und Verzeichnisse
Dateien und Verzeichnisse bearbeiten Object ├─ Directory ├─ File ├─ MarshalByRefObject
│ │ │ ├─ Component │ │ └─ FileSystemWatcher │ └─ FileSystemInfo │ ├─ DirectoryInfo │ └─ FileInfo └─ Path
.NET-Basisklasse Verzeichnis unmittelbar bearbeiten Datei unmittelbar bearbeiten Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für Komponenten Dateisystem überwachen Basisklasse für DirectoryInfo und FileInfo Verzeichnisse ermitteln Informationen über Dateien ermitteln Hilfsfunktionen zur Ermittlung und Bearbeitung von Datei- und Verzeichnisnamen
Textdateien Object └─ MarshalByRefObject
│ ├─ TextReader │ ├─ StreamReader │ └─ StringReader └─ TextWriter ├─ StreamWriter └─ StringWriter
.NET-Basisklasse Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für StreamReader und StringReader Textdateien lesen Informationen aus Zeichenkette lesen Basisklasse für StreamWriter und StringWriter Textdateien schreiben Informationen in Zeichenkette schreiben
Binärdateien Object ├─ BinaryReader ├─ BinaryWriter └─ MarshalByRefObject
│ └─ Stream ├─ FileStream ├─ BufferedStream │ └─ MemoryStream
.NET-Basisklasse .NET-Datentypen aus Binärdateien lesen .NET-Datentypen in Binärdateien speichern Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für Buffered-, File- und MemoryStream Bytes in/aus Binärdateien speichern/lesen wiederholte Lese- oder Schreibzugriffe bei IO.Stream-Objekten optimieren wie FileStream, Daten werden aber im Arbeitsspeicher verwaltet
Von IO.Stream sind übrigens nicht nur die drei IO-Klassen BufferedStream, FileStream und MemoryStream abgeleitet, sondern auch Klassen anderer Namensräume (z.B. Net.Sockets.NetworkStream und Security.Cryptography.CryptoStream).
10.3 Laufwerke, Verzeichnisse, Dateien
10.3
389
Laufwerke, Verzeichnisse, Dateien
Im Mittelpunkt dieses Abschnitts stehen vier System.IO-Klassen: File: ermöglicht die Bearbeitung einer Datei (öffnen, kopieren, verschieben, löschen etc.). FileInfo: liefert genaue Informationen über eine Datei. Directory: ermöglicht die Bearbeitung eines Verzeichnisses. DirectoryInfo: liefert genaue Informationen über ein Verzeichnis.
HINWEIS
FileInfo und DirectorInfo sind beide von der Klasse FileSystemInfo abgeleitet. Diese
Klasse wird selten direkt verwendet, sie stellt aber diverse Eigenschaften und Methoden zur Verfügung, die sowohl für FileInfo als auch für DirectorInfo angewendet werden können. Beachten Sie, dass diese gemeinsamen Schlüsselwörter im Objektbrowser nicht bei den Klassen FileInfo und DirectorInfo aufscheinen, sondern bei FileSystemInfo!
File und FileInfo bzw. Directory und DirectoryInfo weisen ähnlich lautende Eigenschaften und
Methoden auf und scheinen sich auf den ersten Blick zu entsprechen. Das täuscht aber: FileInfo und DirectoryInfo dienen primär dazu, eine konkrete Datei bzw. ein konkretes Verzeichnis zu bearbeiten. Dazu muss vorher mit myfile = New IO.FileInfo("name") bzw. mit mydir = New IO.DirectoryInfo ein entsprechendes Objekt erzeugt werden. In der Folge kann dieses Objekt mit Methoden und Eigenschaften bearbeitet werden (z.B. myfile. Length, um die Größe der Datei zu ermitteln).
•
Die Methoden der File- bzw. Directory-Klassen können dagegen eingesetzt werden, ohne vorher ein Objekt zu erzeugen. (Es ist nicht einmal möglich, ein Objekt dieser Klassen zu erzeugen, d.h., New steht für File und Directory nicht zur Verfügung.) Beispielsweise können Sie eine Datei mit IO.File. Copy("alter name", "neuer name") kopieren.
VERWEIS
•
Man kann den Unterschied zwischen File/Directory und FileInfo/DirectoryInfo auch in der Nomenklatur der objektorientierten Programmierung ausdrücken: Alle Mitglieder der FileInfo bzw. DirectoryInfo sind so genannte Instance-Mitglieder, die erst dann verwendet werden können, wenn vorher ein Objekt erzeugt wurde. Die Mitglieder von File bzw. Directory sind dagegen Shared-Mitglieder, die immer zur Verfügung stehen. Hintergründe zur Unterscheidung zwischen Shared- und Instance-Mitgliedern finden Sie in Abschnitt 6.2.4.
Auch wenn die Namenserweiterung-Info auf einen inhaltlichen Unterschied zwischen den Klassen hinzuweisen scheint, ist der Unterschied rein formal. Inhaltlich überlappen sich die Klassen weitestgehend. Beispielsweise können Sie das Datum der letzten Änderung einer Datei sowohl mit IO.File.GetLastAccessTime("name") als auch mit myFileInfoObject. LastAccessTime ermitteln. Sie können eine Datei sowohl mit IO.File.Delete("name") als auch mit myFileInfoObject.Delete löschen.
390
10 Dateien und Verzeichnisse
10.3.1 Informationen über Verzeichnisse und Dateien ermitteln Als Ausgangspunkt für erste Experimente kann das aktuelle Verzeichnis dienen, das mit dem Namen "." angesprochen wird. Die folgende Zeile erzeugt daher ein DirectoryInfoObjekt dieses Verzeichnisses:
HINWEIS
Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".")
Wenn ein Verzeichnis, dessen Namen Sie an New IO.DirectoryInfo übergeben, nicht existiert, wird der Code dennoch ohne Fehlermeldung ausgeführt und ein DirectoryInfo-Objekt erzeugt! Ob es das Verzeichnis tatsächlich gibt, müssen Sie mit dir.Exists testen. (Das Gleiche gilt auch für FileInfo-Objekte.) Falls Sie ein Verzeichnis neu erstellen möchten, sieht der Code so aus: Dim dir As IO.DirectoryInfo = _ IO.Directory.CreateDirectory("c:\test1")
Nun können Sie über das dir-Objekt verschiedene Eigenschaften des Verzeichnisses ermitteln, beispielsweise: dir.Name dir.FullName
HINWEIS
dir.LastWriteTime dir.Attributes
'Name des Verzeichnisses (z.B. "bin") 'vollständiger Name (z.B. ' "D:\vb.net\test\test2\bin") 'Zeitpunkt der letzten Änderung 'Eigenschaften (z.B. Directory, siehe unten)
Alle Eigenschaften und Methoden von System.IO erwarten bzw. liefern Verzeichnisse ohne ein abschließendes \-Zeichen! Ein Verzeichnis wird also korrekt in der Form "C:\verzeichnis" angegeben (nicht "C:\verzeichnis\"). Die einzige Ausnahme von dieser Regel ist das Wurzelverzeichnis eines Laufwerks: dieses wird mit einem abschließenden \-Zeichen angegeben, also "C:\". Falls Sie Code schreiben möchten, der nicht nur unter Windows, sondern auch unter anderen Betriebssystemen läuft, sollten Sie statt des Zeichens "\" die Konstante IO.Path.PathSeparator verwenden (siehe Abschnitt 10.3.5).
Ganz ähnlich sieht die Vorgehensweise aus, wenn Sie einige Eigenschaften einer Datei ermitteln möchten. Mit der folgenden Anweisung erzeugen Sie ein FileInfo-Objekt für die Datei C:\Winnt\Notepad.exe. (Wenn Sie das Beispiel ausprobieren, verwenden Sie den Namen einer existierenden Datei.) Dim fil As New IO.FileInfo("c:\winnt\notepad.exe")
Nun können Sie sich mit Exists vergewissern, ob die Datei tatsächlich existiert, mit Length die Größe der Datei (in Bytes) ermitteln etc.:
10.3 Laufwerke, Verzeichnisse, Dateien
ACHTUNG
fil.Exists fil.Length fil.Extension
391
'True, wenn die Datei existiert 'Größe der Datei (in Byte) 'Dateikennung, z.B. ".exe"
Viele Eigenschaften der DirectoryInfo- und FileInfo-Objekte sind statisch. Mit anderen Worten: Wenn dass Objekt erzeugt wird, werden Eigenschaften wie Exists, Length, Attributes etc. gelesen. Wenn sich die Datei oder das Verzeichnis auf der Festplatte nun ändert (z.B. weil die Datei gelöscht wird), bleiben die Eigenschaften unverändert! dir.Exists liefert noch immer True. Sie müssen die Methode Refresh ausführen, um diese Eigenschaften auf den aktuellen Stand zu bringen!
Datei- und Verzeichnisattribute Die Attributes-Eigenschaft der File- und DirectoryInfo-Objekte bedarf einer etwas ausführlicheren Erklärung: Sie liefert eine Kombination von FileAttributes-Konstanten zurück: Archive, Compressed, Device, Directory, Encrypted, Hidden etc. Attributes.ToString liefert eine Zeichenkette, in der die Attribute aneinandergereiht sind: s = fil.Attributes.ToString
'z.B. s = "ReadOnly, Archive"
Ob das Verzeichnis ein bestimmtes Merkmal hat, können Sie so testen: If (fil.Attributes And IO.FileAttributes.Compressed) <> 0 Then ...
Eine Liste mit allen Attributen finden Sie in der Syntaxzusammenfassung (Abschnitt 10.3.8).
10.3.2 Alle Dateien und Unterverzeichnisse verarbeiten Bis jetzt wurden nur solche Eigenschaften und Methoden von FileInfo bzw. DirectoryInfo verwendet, die Informationen unmittelbar zur jeweiligen Datei bzw. zum Verzeichnis liefern. Oft wollen Sie aber alle Dateien oder alle Unterverzeichnisse eines Verzeichnisses bearbeiten. Dabei helfen die Methoden GetFiles bzw. GetDirectories der DirectoryInfo-Klasse, die im Mittelpunkt dieses Abschnitts stehen.
Alle Dateien eines Verzeichnisses ermitteln Die Methode dir.GetFiles liefert ein Feld von FileInfo-Objekten des DirectoryInfo-Objektes dir. Die folgende Schleife schreibt die Namen aller Verzeichnisse der Festplatte C: in ein Konsolenfenster. Dim dir As New IO.DirectoryInfo("c:\") Dim fil As IO.FileInfo For Each fil In dir.GetFiles() Console.WriteLine(fil.FullName) Next
392
10 Dateien und Verzeichnisse
HINWEIS
Wenn Sie nicht alle Dateien ermitteln möchten, übergeben Sie an GetFiles einfach das gewünschte Suchmuster. Beispielsweise findet GetFiles("*.txt") alle .txt-Dateien im Verzeichnis. GetFiles bietet allerdings keine Möglichkeiten, nur solche Dateien zu ermitteln, die bestimmte Eigenschaften oder Attribute aufweisen (etwa alle schreibgeschützten Dateien). Durch GetFiles wird ein Feld erzeugt, das zum Zeitpunkt der Erzeugung den Zustand des Dateisystems wiederspiegelt. Das Feld ist aber statisch. Es kann also passieren, dass während der Zeit, in der Sie in einer Schleife das Feld verarbeiten, neue Dateien erzeugt bzw. Dateien gelöscht werden!
Dateien sortieren: Bei ersten Tests mit GetFiles – z.B. unter Windows 2000 mit NTFS-Dateisystem – kann man den Eindruck gewinnen, dass die Methode die Dateien grundsätzlich in sortierter Reihenfolge liefert (binärer Vergleich). Diese Eigenschaft von GetFiles ist aber nicht dokumentiert, und tatsächlich zeigen Tests mit einem Netzwerkverzeichnis, dass die Dateien dort plötzlich in willkürlicher Reihenfolge geliefert werden. Wenn Sie also eine sortierte Liste der Dateinamen benötigen, müssen Sie sich selbst um die Sortierung kümmern! Das folgende Beispiel demonstriert, wie Sie das am leichesten bewerkstelligen können: Sie verwenden zum Sortieren die Methode Array.Sort. Entscheidend ist, dass Sie an diese Methode eine Klasse übergeben müssen, die den Vergleich zwischen zwei Dateien durchführt. Diese Klasse muss die Schnittstelle Collections.ICompare und deren Methode Compare implementieren. (Hintergrundinformationen zu Array.Sort und der ICompare-Schnittstelle finden Sie in Abschnitt 9.3.2.) ' Beispiel dateien\dateien-sortieren Option Strict On Module Module1 Sub Main() Dim dir As New IO.DirectoryInfo("c:\") Dim files As IO.FileInfo() Dim fil As IO.FileInfo files = dir.GetFiles() 'sortieren Array.Sort(files, New myFileComparer()) 'im Konsolenfenster ausgeben For Each fil In files Console.WriteLine(fil.FullName) Next Console.WriteLine("Drücken Sie Return!") Console.ReadLine() End Sub
10.3 Laufwerke, Verzeichnisse, Dateien
393
' Klasse zum Dateinamenvergleich von FileInfo-Objekten Class myFileComparer Implements Collections.IComparer Overridable Function Compare( _ ByVal obj1 As Object, ByVal obj2 As Object) As Integer _ Implements IComparer.Compare Dim fil1, fil2 As IO.FileInfo fil1 = CType(obj1, IO.FileInfo) fil2 = CType(obj2, IO.FileInfo) Return String.Compare(fil1.Name, fil2.Name) End Function End Class End Module
Abbildung 10.1: Beispielprogramm für das Sortieren von Dateien
Alle Unterverzeichnisse eines Verzeichnisses ermitteln GetDirectories funktioniert exakt wie GetFiles, liefert aber ein Feld von DirectoryInfo-Elementen, mit deren Hilfe Sie auf alle Unterverzeichnisse zugreifen können.
Alternativen zu DirectoryInfo.GetFiles() und .GetDirectories() Anstatt die Dateien und Unterverzeichnisse eines Verzeichnisses getrennt zu ermitteln, können Sie auch beide Gruppen mit einer einzigen Methode der DirectoryInfo-Klasse ermitteln, nämlich mit GetFileSystemInfos. Sie erhalten damit als Ergebnis ein FileSystemInfoFeld. Die einzelnen Elemente des Felds sind aber FileInfo oder DirectoryInfo-Objekte. Den tatsächlichen Objekttyp können Sie mit TypeName oder .GetType.FullName feststellen. Sie können auch mit .Attributes testen, ob es sich um ein Verzeichnis handelt oder nicht. Statt der drei GetXxx-Methoden der DirectoryInfo-Klassen können Sie auch die Methoden GetFiles, GetDirectories und GetFileSystemEntries der Directory-Klasse verwenden. An diese Methoden müssen Sie den Pfad des Startverzeichnisses als Zeichenkette übergeben. Die Methoden liefern keine Objektfelder als Resultat, sondern Felder mit den Zeichenketten der Dateien bzw. Verzeichnisse.
394
10 Dateien und Verzeichnisse
Dim s As String For Each s In IO.Directory.GetFileSystemEntries("c:\") Console.WriteLine(s) Next
Verzeichnisbaum rekursiv durchlaufen Immer wieder kommt es vor, dass Sie alle Dateien eines ganzen Verzeichnisbaums verarbeiten möchten – sei es, dass Sie eine bestimmte Datei suchen, sei es, dass Sie alle Dateien verändern oder dass Sie den Gesamtplatzbedarf ermitteln möchten. Den besten Weg bietet hierfür eine rekursive Funktion, die – ausgehend von einem Startverzeichnis – für alle Unterverzeichnisse aufgerufen wird. Die folgenden Zeilen geben ein Muster für eine derartige Funktion. Sub processDirectory(ByVal dir As IO.DirectoryInfo) Dim subdir As IO.DirectoryInfo Dim file As IO.FileInfo ' Debug.WriteLine("Verzeichnis: " + dir.FullName) ' alle Dateien des aktuellen Verzeichnisses bearbeiten For Each file In dir.GetFiles() ... Code zur Bearbeitung der Dateien Next ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In dir.GetDirectories() processDirectory(subdir) Next End Sub
Die Prozedur kann beispielsweise mit processDirectory(New IO.DirectoryInfo("C:\")) gestartet werden (um alle Dateien und Verzeichnisse von Laufwerk C: zu bearbeiten).
Beispielprogramm: Anzahl und Größe aller Dateien ermitteln Das folgende Programm basiert auf dem obigen Muster processDirectory. Das Programm ermittelt die Anzahl der Dateien und Verzeichnisse sowie den Gesamtplatzbedarf aller Dateien im Laufwerk C:. Außerdem wird für jedes Verzeichnis im Konsolenfenster angezeigt, wie viele Dateien und Verzeichnisse darin enthalten sind und wie groß der Platzbedarf der unmittelbar enthaltenen Dateien ist (siehe Abbildung 10.2). Beachten Sie, dass das Programm nicht den tatsächlichen Platzbedarf ermittelt, sondern einfach die Größe der Dateien addiert (FileInfo.Length). Der tatsächliche Platzbedarf ist in der Regel etwas größer, weil Dateien blockweise gespeichert werden, diese Blöcke aber nur dann vollständig gefüllt werden, wenn die Dateigröße zufällig mit der Blockgröße übereinstimmt. Außerdem beanspruchen auch Verzeichnisse etwas Platz auf der Festplatte. Andererseits kann der wahre Platzbedarf kleiner sein, wenn Dateien oder ganze Verzeichnisse komprimiert sind (nur bei einem NTFS-Dateisystem).
10.3 Laufwerke, Verzeichnisse, Dateien
395
Abbildung 10.2: Die ersten und die letzten Ergebniszeilen des Beispielprogramms verzeichnisbaum
Im folgenden Programmlisting wurde aus Platzgründen der Code für die Zeiterfassung entfernt (siehe Abschnitt 8.3.2). ' dateien\verzeichnisbaum Dim totalDirCount, totalFileCount, totalFileSize As Long Dim totalFileErrors, totalDirErrors As Long Sub Main() processDirectory(New IO.DirectoryInfo("C:\")) ' Zusammenfassung Console.WriteLine() Console.WriteLine("Anzahl der Verzeichnisse: {0}", totalDirCount) Console.WriteLine("Anzahl der Dateien: {0}", totalFileCount) Console.WriteLine("Gesamtgröße aller Dateien: {0:d} MB", _ CInt(totalFileSize / 1024 ^ 2)) Console.WriteLine("Verzeichnisfehler: {0}", totalDirErrors) Console.WriteLine("Dateifehler: {0}", totalFileErrors) Console.WriteLine() Console.WriteLine("Drücken Sie Return!") Console.ReadLine() End Sub processDirectory ist im Vergleich zum obigen Muster vor allem durch die zweistufige Fehlerabsicherung aufgebläht: Der erste Try-Block für die Schleife For Each file In dir.GetFiles() kommt dann zur Geltung, wenn der Zugriff auf ein ganzes Verzeichnis nicht möglich ist. Die wahrscheinlichste Ursache sind fehlende Zugriffsrechte (insbesondere dann, wenn Sie das Programm nicht mit Administrator-Rechten ausführen).
396
10 Dateien und Verzeichnisse
Der zweite Try-Block dient speziell für die Anweisung fileSize += file.Length. Auch hier ist die wahrscheinlichste Fehlerursache, dass Sie auf die betroffene Datei nicht zugreifen dürfen (und daher nicht einmal seine Größe ermitteln können). Vereinzelt kann aber auch eine IO.PathTooLongException die Fehlerursache sein (also ein zu langer Dateiname).
VERWEIS
Falls Fehler auftreten, werden diese mit Debug.WriteLine dokumentiert. (Sie können sich die Fehlermeldungen im der Entwicklungsumgebung im Ausgabefenster ansehen.) Einführende Informationen zur Fehlerabsicherung von Dateizugriffen finden Sie in Abschnitt 10.8. Eine allgemeine Einführung in die Fehlerabsicherung mit VB.NET (also eine Beschreibung des Try-Catch-Konzepts) gibt Abschnitt 11.1.
Ansonsten sollte der Code auf Anhieb verständlich sein. Die Variablen fileSize, fileCount und dirCount dienen zum Sammeln der Informationen über das aktuelle Verzeichnis. Die Variable indent misst die Rekursionstiefe von processDirectory. Diese Information wird für die Ausgabe der Zeilen im Konsolenfenster ausgewertet (so dass die Verzeichnisse entsprechend der Verzeichnisstruktur eingerückt sind, siehe Abbildung 10.2). Sub processDirectory(ByVal dir As IO.DirectoryInfo) Dim fileSize, fileCount, dirCount As Long Static indent As Integer Dim subdir As IO.DirectoryInfo Dim file As IO.FileInfo ' Einrückung für Konsolenausgabe indent += 1 Try ' wenn bereits in der nächsten Zeile ein Fehler auftritt, ' kann wahrscheinlich auf das gesamte Verzeichnis nicht ' zugegriffen werden (z.B. UnauthorizedAccessException) For Each file In dir.GetFiles() ' wenn in den beiden folgenden Zeilen ein Fehler auftritt, ' kann auf eine einzelne Datei nicht zugegriffen werden Try fileCount += 1 fileSize += file.Length Catch e As Exception ' Problem beim Zugriff auf eine Datei totalFileErrors += 1 Debug.WriteLine("Fehler: " + TypeName(e) + ": " + e.Message) Debug.WriteLine(" In Verzeichnis: " + dir.FullName) Debug.WriteLine(" Bei Datei: " + file.Name) End Try Next
10.3 Laufwerke, Verzeichnisse, Dateien
397
' Informationen über das aktuelle Verzeichnis angeben Console.WriteLine( _ "{0}{1}: {2} Verz., {3} Dateien, Platzbedarf {4} kB", _ Space((indent - 1) * 2), dir.Name, _ dir.GetDirectories().Length, fileCount, CInt(fileSize / 1024)) ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In dir.GetDirectories() dirCount += 1 processDirectory(subdir) Next Catch e As Exception ' Problem beim Zugriff auf ein Verzeichnis totalDirErrors += 1 Debug.WriteLine("Fehler: " + TypeName(e) + ": " + e.Message) Debug.WriteLine(" In Verzeichnis: " + _ IO.Path.Combine(dir.Parent.FullName, dir.Name)) End Try ' Gesamtergebnis addieren totalDirCount += dirCount totalFileCount += fileCount totalFileSize += fileSize ' Einrückung für Konsolenausgabe indent -= 1 End Sub
Erwarten Sie übrigens keine Geschwindigkeitswunder von dem Programm. Auf meinem Rechner erfordert das Durcharbeiten von C: (ca. 80000 Dateien, ca. 6 GByte Daten) beim ersten Durchlauf mehr als eine Minute. Wird das Programm anschließend nochmals gestartet, liefert es bereits nach etwa 15 Sekunden das Endergebnis. Der Grund für diese deutliche Geschwindigkeitssteigerung liegt darin, dass beim zweiten Mal fast alle Dateiinformationen im lokalen Zwischenspeicher des Betriebssystems liegen und daher nur vergleichsweise wenige Zugriffe auf die Festplatte erforderlich sind. (Beim zweiten Durchlauf beansprucht übrigens auch die Bildschirmausgabe einen nennenswerten Anteil der Rechenzeit. Wenn Sie darauf verzichten, wird das Programm nochmals erheblich schneller.)
10.3.3 Manipulation von Dateien und Verzeichnissen Bis jetzt beschränkten sich die Beispiele dieses Kapitels auf das Ermitteln von Informationen. Selbstverständlich können Sie aber Dateien und Verzeichnisse auch ändern, d.h. kopieren, verschieben, neu anlegen und wieder löschen. Wie bereits in der Einleitung dieses Kapitels ausgeführt wurde, bietet der System.IO-Namensraum dazu grundsätzlich zwei Möglichkeiten:
398
10 Dateien und Verzeichnisse
•
Sie können entweder ein Objekt der Klassen FileInfo oder DirectoryInfo erzeugen und dieses dann mit Methoden wie Delete oder Copy bearbeiten.
•
Oder Sie können die Methoden der Klassen File oder Directory verwenden, wobei Sie die zu bearbeitende Datei bzw. das Verzeichnis als Zeichenkette angeben.
Welche der beiden Varianten vorzuziehen ist, hängt primär davon ab, ob es aus dem bisherigen Code heraus bereits ein FileInfo- oder DirectoryInfo-Objekt gibt. Wenn das nicht der Fall ist, sind die File- oder Directory-Methoden vorzuziehen (klarerer und kompakterer Code). Ein weiteres Kriterium kann der Rückgabewert sein: So liefert File.Copy keinen Rückgabewert, während myFileInfoObject.Copy ein neues FileInfo-Objekt für die kopierte Datei zurückgibt. Die folgenden Absätze stellen exemplarisch Methoden aller vier Klassen vor. Eine vollständige Referenz finden Sie in der Syntaxzusammenfassung.
Neue Dateien und Verzeichnisse erzeugen Um ein neues Verzeichnis zu erzeugen, verwenden Sie am einfachsten IO.Directory.CreateDirectory. Falls das Verzeichnis schon existiert, wird es nicht verändert. Die Methode liefert ein DirectoryInfo-Objekt zurück: ' Beispielprogramm dateien/manipulation ' Verzeichnis erzeugen Dim dir As IO.DirectoryInfo = IO.Directory.CreateDirectory("c:\test1")
Wenn Sie das DirecotryInfo-Objekt nicht benötigen, können Sie den Rückgabewert einfach ignorieren und ersparen sich dann die Deklaration der Variablen dir. IO.Directory.CreateDirectory("c:\test1")
Ungleich mehr Möglichkeiten gibt es, eine Datei zu erzeugen – abhängig davon, was Sie in der Folge mit der Datei tun möchten: Create liefert als Rückgabewert ein FileStream-Objekt, das eine sehr vielseitige Nutzung der Datei (auch in Binärform) ermöglicht. CreateText liefert dagegen ein StreamWriter-Objekt, dessen Verwendung sich vor allem zum Schreiben von Textdateien anbietet. Schließlich können Sie noch Open verwenden und im Modusparameter IO.FileMode.Create, .CreateNew oder .OpenOrCreate angeben (damit nicht nur eine vorhandene Datei geöffnet, sondern gegebenenfalls auch eine neue erzeugt wird). Open liefert ein FileStream-Objekt. ' Dateien zur Bearbeitung erzeugen Dim fs1, fs2 As IO.FileStream Dim sw As IO.StreamWriter fs1 = IO.File.Create("c:\test1\test1.bin") fs2 = IO.File.Open("c:\test1\test2.bin", IO.FileMode.OpenOrCreate) sw = IO.File.CreateText("c:\test1\test3.txt")
Auch hier können Sie natürlich den Rückgabewert ignorieren, falls Sie nur eine leere Datei erzeugen, diese aber nicht bearbeiten möchten. Beachten Sie aber, dass die neuen Dateien nun als geöffnet gelten! Damit die Dateien vor dem Ende des Programms (z.B. an einer
10.3 Laufwerke, Verzeichnisse, Dateien
399
anderen Stelle im Code) auch bearbeitet werden können, müssen sie vorher geschlossen werden. Dazu hängen Sie an Create, CreateText oder Open die Methode Close an.
VERWEIS
VERWEIS
' Dateien nur erzeugen IO.File.Create("c:\test1\test1.bin").Close() IO.File.Open("c:\test1\test2.bin", IO.FileMode.OpenOrCreate).Close() IO.File.CreateText("c:\test1\test3.txt").Close()
Detailliertere Informationen zum Lesen und Schreiben von Textdateien (Klassen StreamReader und StreamWriter) sowie zum Umgang mit allgemeinen Dateien (Binärdateien, FileStream und verwandte Klassen) liefern die Abschnitte 10.5 und 10.6.
Per Default öffnet Create und Open die Dateien so, dass bis zum Schließen kein anderer Benutzer (kein anderes Programm) darauf zugreifen kann, nicht einmal lesend. CreateText ist etwas liberaler und lässt einen parallelen Lesezugriff zu, nicht aber einen Schreibzugriff. Wenn Sie explizit angeben möchten, ob andere Programme auf die geöffnete oder neu erstellte Datei zugreifen dürfen, müssen Sie zum Öffnen/Erstellen die Methode Open mit allen vier Parametern verwenden: IO.File.Open("name", modus, access, share). Genaue Informationen zu den Open-Parametern finden Sie in Abschnitt 10.6.1 im Zusammenhang mit dem FileStream-Objekt.
Dateien und Verzeichnisse kopieren, verschieben und löschen Mit Copy (File-Objekt) bzw. CopyTo (FileInfo-Objekt) können Sie eine Datei kopieren. Move bzw. MoveTo verschiebt eine Datei an einen anderen Ort bzw. gibt der Datei einen neuen Namen. Die beiden Methoden stehen auch für Verzeichnisse zur Verfügung. (Dagegen gibt es keine Möglichkeit, ganze Verzeichnisse zu kopieren.) IO.File.Copy("c:\test1\test3.txt", "c:\test1\test4.txt") IO.File.Move("c:\test1\test4.txt", "c:\test1\test5.txt") Delete löscht schließlich die angegebene Datei bzw. das leere Verzeichnis. Wenn Sie ein Verzeichnis inklusive des gesamten Inhalts löschen möchten, müssen Sie im optionalen zweiten Parameter True übergeben (siehe zweites Beispiel). IO.File.Delete("c:\test1\test5.txt") IO.Directory.Delete("c:\test1", True) Delete akzeptiert keine Muster. Wenn Sie beispielsweise alle *.bmp-Dateien in einem Verzeichnis löschen möchten, müssen Sie diese mit GetFiles("*.bmp") ermitteln und dann einzeln löschen. Die andere Möglichkeit besteht darin, das VB-Kommando Kill (Namensraum Microsoft.VisualBasic.FileSystem) einzusetzen, das auch Muster akzeptiert.
400
10 Dateien und Verzeichnisse
Dateien und Verzeichnisse in den Papierkorb verschieben Wenn Sie Dateien oder Verzeichnisse mit Delete löschen, dann ist diese Löschoperation endgültig. Oft wäre es praktischer, eine Datei oder ein Verzeichnis in den Papierkorb zu verschieben – aber die .NET-Klassenbibliothek bietet hierfür leider noch keine Möglichkeiten. Das Betriebssystem bietet diese Möglichkeit aber sehr wohl; aber um diese Funktion auch in VB.NET nutzen zu können, müssen Sie auf die in VB.NET eigentlich verpönten API-Aufrufe zurückgreifen. Die hier vorgestellte Funktion SafeDelete erwartet eine oder mehrere Zeichenketten oder ein String-Feld. Die Parameter geben an, welche Dateien oder Verzeichnisse in den Papierkorb befördert werden sollen. Vor dem Löschen erscheint automatisch eine Sicherheitsabfrage, ob die Dateien wirklich gelöscht werden sollen (siehe Abbildung 10.3).
Abbildung 10.3: Sicherheitsabfrage beim Löschen von Dateien
' Beispielprogramm dateien\safedelete ' Dateien und Verzeichnisse in den Papierkorb befördern Option Strict On ' SafeDelete testen Sub Main() ' ein Verzeichnis und zwei Dateien erzeugen IO.Directory.CreateDirectory("c:\test1") IO.File.Create("c:\test1\test1.bin").Close() IO.File.Create("c:\test1\test2.bin").Close() ' alles in den Papierkorb befördern SafeDelete.SafeDelete("c:\test1") End Sub SafeDelete greift auf die Betriebssystemfunktion SHFileOperation zurück. (Eine genaue Beschreibung der Declare-Syntax finden Sie in Abschnitt 12.7.) An diese Funktion wird in der Datenstruktur SHFILEOPSTRUCT eine Zeichenkette mit allen Dateinamen übergeben
(getrennt jeweils durch ein 0-Byte, abgeschlossen durch zwei 0-Bytes).
10.3 Laufwerke, Verzeichnisse, Dateien
401
Als gewünschte Operation wird Delete mit der Undo-Option angegeben (Strukturelemente wFunc und pFlags). Die Datenstruktur ist mit dem Attribut StructLayout aus dem Namensraum Runtime.InteropServices deklariert. Die Parameter des Attributs bewirken, dass die Elemente der Struktur in der angegebenen Reihenfolge angeordnet werden. (Diese Reihenfolge darf also nicht durch den VB.NET-Compiler verändert werden.) SafeDelete ist mit den dazugehörenden Deklarationen in eine Klasse gleichen Namens gekapselt. Nach außen hin ist nur die Funktion SafeDelete zugänglich. Die Funktion liefert als Ergebnis 0, wenn alles funktioniert hat, andernfalls einen Fehlercode ungleich 0. Class SafeDelete ' Konstanten für SHFileOperation Protected Const FO_DELETE As Integer = &H3 Protected Const FOF_ALLOWUNDO As Integer = &H40 ' Struktur für SHFileOperation _ Protected Structure SHFILEOPSTRUCT Dim hWnd As Integer Dim wFunc As Integer Dim pFrom As String Dim pTo As String Dim fFlags As Short Dim fAborted As Boolean Dim hNameMaps As Integer Dim sProgress As String End Structure ' Deklaration für die API-Funktion SHFileOperation Protected Declare Unicode Function SHFileOperation _ Lib "shell32.dll" (ByRef lpFileOp As SHFILEOPSTRUCT) As Integer ' nach außen hin zugängliche Funktion SafeDelete Shared Function SafeDelete(ByVal ParamArray files() As String) _ As Integer Dim fileOp As SHFILEOPSTRUCT fileOp.hWnd = 0 fileOp.wFunc = FO_DELETE fileOp.pFrom = String.Join(vbNullChar, files) + _ vbNullChar + vbNullChar fileOp.fFlags = FOF_ALLOWUNDO Return SHFileOperation(fileOp) End Function End Class
402
10 Dateien und Verzeichnisse
Zugriffsrechte von Dateien und Verzeichnissen ändern Sofern ein VB.NET-Programm auf einem Betriebssystem mit NTFS-Dateisystem ausgeführt wird, können die Zugriffsrechte von Dateien und Verzeichnissen sehr exakt eingestellt werden. Die erforderlichen Klassen (insbesondere FileIOPermission) und Methoden befinden sich in den Namensräumen System.Security und System.Security.Permissions, die in diesem Buch aber nicht beschrieben werden.
10.3.4 Spezielle Verzeichnisse, Dateien und Laufwerke ermitteln Dieser Abschnitt beschreibt verschiedene Wege, um besondere Verzeichnisse (z.B. das aktuelle Verzeichnis, das Windows-Verzeichnis, das temporäre Verzeichnis etc.) zu ermitteln. Dazu müssen Sie Methoden aus den verschiedensten .NET-Klassen (IO.DirectoryInfo, IO.Path, Environment, Reflection.Assembly etc.) einsetzen.
Aktuelles Verzeichnis ermitteln Für jedes Programm gilt ein Verzeichnis als das aktuelle Verzeichnis (per Default das Verzeichnis, von dem aus das Programm gestartet wurde). Sie können dieses Verzeichnis sehr einfach mit der VB-Methode CurDir ermitteln. Diese Methode liefert eine Zeichenkette mit dem aktuellen Verzeichnis zurück. Wenn Sie lieber .NET-Methoden einsetzen möchten, ermitteln Sie das Verzeichnis mit der Eigenschaft Environment.CurrentDirectory: Dim s As String = Environment.CurrentDirectory
Einen dritten Weg bietet die Klasse System.IO.DirectoryInfo: Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".") s = dir.FullName
Falls Sie das Objekt dir anschließend nicht mehr benötigen, bietet sich die folgende Kurzform an (deren Nachteil allerdings darin besteht, dass der Code nicht besonders gut lesbar ist): Dim s As String = New IO.DirectoryInfo(".").FullName
Aktuelles Verzeichnis ändern Um das aktuelle Verzeichnis zu ändern, verwenden Sie entweder die Methode IO.Directory.SetCurrentDirectory oder das VB-Kommando ChDir. Beide Kommandos verändern gegebenenfalls auch das aktuelle Laufwerk. (In VB6 war dies bei ChDir nicht der Fall. Dort musste zusätzlich auch ChDrive ausgeführt werden.) IO.Directory.SetCurrentDirectory("c:\meinverzeichnis")
10.3 Laufwerke, Verzeichnisse, Dateien
403
Sowohl SetCurrentDirectory als auch ChDir funktionieren auch für Netzwerkverzeichnisse: IO.Directory.SetCurrentDirectory("\\mars\data")
Verzeichnis des laufenden Programms ermitteln: Mit dem nicht gerade besonders kurzen Ausdruck Reflection.Assembly.GetExecutingAssembly().Location können Sie den vollständigen Namen des gerade ausgeführten Programms ermitteln (z.B. "D:/code/vb.net/test/test2/ bin/test2.exe"). Um daraus das Verzeichnis zu ermitteln, können Sie die Methode IO.Path.GetDirectoryName zu Hilfe nehmen. Dim exename, exedir As String exename = Reflection.Assembly.GetExecutingAssembly().Location exedir = IO.Path.GetDirectoryName(exename)
Im Regelfall können Sie sich diese Arbeit ersparen, weil das Programmverzeichnis per Default mit dem aktuellen Verzeichnis übereinstimmt. Es kann aber sein, dass Sie das aktuelle Verzeichnis verändert haben, oder dass das Programm von einem anderen Verzeichnis aus gestartet wurde. In diesen Fällen weichen das aktuelle Verzeichnis und das Programmverzeichnis voneinander ab.
Temporäres Verzeichnis ermitteln Es gibt mehrere Möglichkeiten, das Verzeichnis für temporäre Dateien zu ermitteln. Die folgenden Programmzeilen zeigen einige Varianten. Die zweite und dritte Variante wertet jeweils die Umgebungsvariable temp aus (siehe auch Abschnitt 12.2.1).
HINWEIS
Dim s = s = s =
s As String IO.Path.GetTempPath() Environment.GetEnvironmentVariable("temp") Environ("temp")
Beachten Sie, dass das temporäre Verzeichnis – anscheinend aus Kompatibilitätsgründen – manchmal noch immer in der mit Windows 95 eingeführten 8+3-Kurzschreibweise angegeben ist. Beispielsweise liefern alle drei obigen Kommandos auf meinem Rechner s = "C:\DOKUME~1\ADMINI~1\LOKALE~1\Temp". Zum Glück verhalten sich die System.IO-Methoden demgegenüber tolerant, d.h., Verzeichnisse in dieser Schreibweise werden akzeptiert. Es scheint aber keinen einfachen Weg zu geben, der von dieser Schreibweise zu einem Verzeichnisnamen ohne Abkürzungen führt.
Namen für eine temporäre Datei ermitteln Wenn Sie eine temporäre Datei anlegen möchten, können Sie einen geeigneten Namen sehr einfach mit GetTempFileName auf die folgende Weise ermitteln: s = IO.Path.GetTempFileName()
404
10 Dateien und Verzeichnisse
Beachten Sie, dass GetTempFileName die temporäre Datei auch gleich erzeugt (mit einer Länge von null Bytes)!
Windows-Verzeichnis ermitteln Auch zur Ermittlung des Windows-Verzeichnisses bestehen mehrere Möglichkeiten. Bei der ersten und zweiten Variante wird jeweils die Umgebungsvariable windir ausgewertet. Bei der dritten Variante wird zuerst das Windows-Systemverzeichnis und dann dessen übergeordnetes Verzeichnis ermittelt. Diese Variante setzt also voraus, dass das Systemverzeichnis immer ein Unterverzeichnis des Windows-Verzeichnisses ist (was bei allen bisherigen Windows-Versionen der Fall war). Eine .NET-Methode, die das WindowsVerzeichnis direkt ermittelt, scheint es nicht zu geben. s = Environment.GetEnvironmentVariable("windir") s = Environ("windir") s = New IO.DirectoryInfo(Environment.SystemDirectory).Parent.FullName
Windows-Systemverzeichnis ermitteln In diesem Fall führt die Eigenschaft Environment.SystemDirectory direkt zum Ziel: s = Environment.SystemDirectory
Andere spezielle Verzeichnisse Die Methode Environment.GetFolderPath hilft dabei, diverse andere spezielle Verzeichnisse zu ermitteln. Die folgenden Zeilen geben zwei Beispiele: zuerst wird das Installationsverzeichnis für Programme ermittelt (z.B. "C:\Programme"), dann das Verzeichnis mit den persönlichen Dateien des Benutzers (z.B. "C:\Dokumente und Einstellungen\Administrator\ Eigene Dateien"). s = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) s = Environment.GetFolderPath(Environment.SpecialFolder.Personal)
Eine Referenz aller derartigen Verzeichnisse finden Sie in der Syntaxzusammenfassung. Wenn Sie sich rasch einen Überblick verschaffen möchten, können Sie auch die folgenden Zeilen ausführen. (Wenn Sie Probleme haben, den Code zu verstehen, werfen Sie bitte einen Blick in Abschnitt 4.4. Dort wird der Umgang mit Enum-Aufzählungen ausführlich erläutert.) Dim s As String Dim fld As Environment.SpecialFolder For Each s In System.Enum.GetNames(fld.GetType) fld = CType(System.Enum.Parse(fld.GetType, s), _ Environment.SpecialFolder) Console.WriteLine("{0}: {1}", s, Environment.GetFolderPath(fld)) Next
10.3 Laufwerke, Verzeichnisse, Dateien
405
Am Rechner verfügbare Laufwerke ermitteln Die am Rechner verfügbaren Laufwerke können Sie mit Environment.GetLogicalDrives oder mit IO.DirectoryGetLogicalDrives ermitteln. Beide Methoden liefern ein String-Feld mit den Namen der Laufwerke (also "A:\", "C:\", "D:\" etc.).
VERWEIS
Dim s As String Dim drvs As String() = Environment.GetLogicalDrives For Each s In drvs Console.WriteLine(s) Next
Wenn Sie mehr Informationen benötigen – z.B. welches der Laufwerke Festplatten, CD-ROMs etc. sind –, wird es leider ziemlich kompliziert. Sie müssen dann auf die Klassen der System.Management-Bibliothek zurückgreifen, die in Abschnitt 12.2.2 ganz kurz vorgestellt wird.
10.3.5 Bearbeitung von Datei- und Verzeichnisnamen Die Klasse System.IO.Path enthält eine Reihe von Methoden, mit denen Sie Zeichenketten bearbeiten können, die Datei- oder Verzeichnisnamen enthalten. GetFileName, GetFileNameWithoutExtension, GetPathRoot, GetDirectoryName und GetExtension helfen dabei, aus einem vollständigen Dateinamen dessen Kurzform (ohne Verzeichnisse), das Laufwerk, das Verzeichnis und die Dateikennung zu extrahieren. Dim full, file1, file2, folder, drive, extension As String ' Name des Programms, z.B. "D:/vb.net/test/test2/bin/test2.exe" full = Reflection.Assembly.GetExecutingAssembly.Location file1 = IO.Path.GetFileName(full) ' "test2.exe" file2 = IO.Path.GetFileNameWithoutExtension(full) ' "test2" drive = IO.Path.GetPathRoot(full) ' "D:\" folder = IO.Path.GetDirectoryName(full) ' "D:/vb.net/test/test2/bin" extension = IO.Path.GetExtension(full) ' ".exe" GetFullPath liefert den vollständigen Dateinamen. Die einzig sinnvolle Anwendung dieser
Methode ergibt sich dann, wenn Sie als Parameter einen Dateinamen ohne Verzeichnisse übergeben. In diesem Fall verbindet GetFullPath den Dateinamen mit dem aktuellen Verzeichnis. ChangeExtension ändert die Dateikennung. Das ist beispielsweise praktisch, wenn Sie eine
Sicherungskopie einer Datei erstellen möchten. Falls der ursprüngliche Dateiname noch keine Kennung enthält, wird diese hinzugefügt. Dim backup As String backup = IO.Path.ChangeExtension(full, ".bak") ' backup = "D:/vb.net/test/test2/bin/test2.bak"
406
10 Dateien und Verzeichnisse
Die Eigenschaften PathSeparator, VolumeSeparatorChar, InvalidPathChars etc. geben an, welche Zeichen in Dateinamen (nicht) verwendet werden können. Eine komplette Referenz dieser Eigenschaften gibt die Syntaxzusammenfassung. Die Eigenschaften sind vor allem dann interessant, falls die .NET-Umgebung in Zukunft auch unter anderen Betriebssystemen zur Verfügung stehen sollte. Beispielsweise werden unter Unix Verzeichnisse mit / statt mit \ getrennt. Durch die konsequente Anwendung der IO.Path-Eigenschaften und Methoden können Sie mögliche Portabilitätsprobleme vermeiden.
10.3.6 Beispiel – Backup automatisieren Meine Backup-Strategie beim Schreiben dieses Buchs bestand im Wesentlichen darin, dass ich einmal täglich die Textdatei auf eine zweite Festplatte kopiert habe (in der Hoffnung, dass nicht beide Festplatte gleichzeitig kaputt gehen würden). Um zusätzlich Sicherheit zu gewinnen, habe ich beim Kopieren nicht jedes Mal denselben Dateinamen verwendet, sondern einen Namen, der auch das Datum umfasst (also z.B. vbnet-2002-01-31.doc). Der Vorteil dieser Vorgehensweise besteht darin, dass ich so beispielsweise auch ein ganzes Kapitel rekonstruieren kann, wenn ich dieses irrtümlich gelöscht hätte (ganz einfach, indem ich das Kapitel aus einer älteren Backup-Version entnehme). Natürlich habe ich dieses Backup nicht manuell durchgeführt, sondern mit einem kleinen VB.NET-Programm, das ich jeden Tag vor dem Ausschalten des Rechners ausgeführt habe. Der Code beweist, dass auch mit wenigen Zeilen eine sinnvolle Aufgabe erfüllt werden kann. ' Beispiel dateien\backup Sub Main() Backup("d:\text\vbnet\vbnet.doc", "n:\bak\vbnet") End Sub Sub Backup(ByVal oldfilename As String, ByVal backupdir As String) ' filename: die zu sichernde Datei ' backupdir: das Verzeichnis mit den Backup-Dateien Dim newfilename, newfullname As String ' der neue Dateiname ohne Pfad newfilename = _ IO.Path.GetFileNameWithoutExtension(oldfilename) + _ String.Format("{0:-yyyy-MM-dd}", Today) + _ IO.Path.GetExtension(oldfilename) ' der neue Dateiname mit Pfad newfullname = IO.Path.Combine(backupdir, newfilename)
10.3 Laufwerke, Verzeichnisse, Dateien
407
' kopieren, evt. schon vorhandene Backup-Datei überschreiben Try IO.File.Copy(oldfilename, newfullname, True) Catch e As Exception MsgBox("Beim Backup ist ein Fehler aufgetreten: " & e.Message) End Try End Sub
10.3.7 Beispiel – Verzeichnisse synchronisieren Manchmal besteht der Wunsch, zwei Verzeichnisse (Verzeichnisbäume) miteinander zu synchronisieren. Das klassische Beispiel dafür ist die wechselseitige Bearbeitung von Dateien auf zwei unterschiedlichen Rechnern (z.B. Bürorechner und Notebook). Eine Synchronisation kann aber auch bei der effizienten Durchführung eines Backups eines ganzen Verzeichnisbaums helfen. Die einfachste Form der Synchronisation bestünde darin, einfach den gesamten Verzeichnisbaum vom Ursprungsort zum Zielort zu kopieren. Wenn sich aber nur einige wenige Dateien in einem riesigen Verzeichnisbaum geändert haben, ist diese Vorgehensweise ineffizient. Der folgende Algorithmus kopiert daher nur alle neuen bzw. veränderten Dateien von einem Quell- in ein Zielverzeichnis. Wenn das Zielverzeichnis leer ist, werden alle Dateien und Verzeichnisse kopiert. Damit kann der Algorithmus also auch dazu verwendet werden, um einen ganzen Verzeichnisbaum einfach zu kopieren. Beachten Sie bitte die folgenden Einschränkungen des Programms: •
Die Synchronisiation erfolgt nur einseitig. Wenn sich manche Dateien im Quellverzeichnis, andere im Zielverzeichnis verändert haben, wäre eine Synchronisation in beide Richtungen erforderlich.
•
Zur Feststellung, ob sich eine Datei verändert hat, wird ausschließlich das Datum der letzten Änderung verwendet. Das bedeutet, dass das Programm nur dann zuverlässig funktioniert, wenn das Zeitsystem im Quell- und im Zielverzeichnis übereinstimmt. (Wenn sich Quell- und Zielverzeichnis auf unterschiedlichen Rechnern befinden, sollte das unbedingt kontrolliert werden.)
•
Dateien, die im Quellverzeichnis gelöscht wurden, werden im Zielverzeichnis aus Sicherheitsgründen nicht ebenfalls gelöscht. Das führt allerdings dazu, dass auf einem Rechner gelöschte Dateien nach zwei Synchronisationsvorgängen (einmal in die eine, dann in die andere Richtung) wieder auftauchen.
•
Wenn das Zielverzeichnis nicht existiert, wird es erzeugt. Das funktioniert aber nur, wenn zumindest das Unterverzeichnis des Zielverzeichnisses bereits existiert. (Wenn das Zielverzeichnis c:\abc\def\ghi lautet, muss also zumindest c:\abc\def bereits existieren.)
•
Das Programm ist in keiner Weise gegen mögliche Fehler abgesichert.
408
10 Dateien und Verzeichnisse
Programmcode Der Programmcode beginnt in Main mit dem Aufruf von Synchronize. An dieses Unterprogramm werden die Namen des Quell- und Zielverzeichnisses übergeben. Synchronize testet, ob das Quellverzeichnis existiert und ruft dann die Funktion SynchronizeDirectory auf. Dort werden zuerst alle Dateien des aktuellen Verzeichnisses mit dem Zielverzeichnis verglichen; fehlende oder veraltete Dateien werden kopiert. Anschließend wird SynchronizeDirectory rekursiv für alle Unterverzeichnisse aufgerufen. Die Funktion liefert die Anzahl der kopierten Dateien zurück, die summiert wird. ' Beispiel dateien\synchronisation Sub Main() Synchronize("c:\test1", "c:\test2") End Sub Sub Synchronize(ByVal source As String, ByVal dest As String) Dim n As Integer Dim sourceDir As IO.DirectoryInfo ' testen, ob Quellverzeichnis existiert; falls nicht: Abbruch If IO.Directory.Exists(source) = False Then MsgBox("Quellverzeichnis " + source + " existiert nicht.") Exit Sub Else sourceDir = New IO.DirectoryInfo(source) End If ' Dateien und Verzeichnisse rekursiv kopieren n = SynchronizeDirectory(sourceDir, dest) MsgBox(n.ToString + " Dateien wurden kopiert bzw. aktualisiert.") End Sub Function SynchronizeDirectory(ByVal sourceDir As IO.DirectoryInfo, _ ByVal destDirName As String) As Integer Dim subdir, destDir As IO.DirectoryInfo Dim sourceFile As IO.FileInfo Dim destFileName As String Dim changedFiles As Integer ' testen, ob Zielverzeichnis überhaupt existiert; ' gegebenenfalls erzeugen destDir = New IO.DirectoryInfo(destDirName) If destDir.Exists = False Then destDir.Create() ' alle Dateien des Quellverzeichnisses bearbeiten ' falls Datei im Zielverzeichnis nicht existiert, ' oder wenn sie älter ist: kopieren bzw. überschreiben
10.3 Laufwerke, Verzeichnisse, Dateien
409
For Each sourceFile In sourceDir.GetFiles() destFileName = IO.Path.Combine(destDir.FullName, _ sourceFile.Name) If IO.File.Exists(destFileName) = False OrElse _ sourceFile.LastWriteTime > _ IO.File.GetLastWriteTime(destFileName) Then sourceFile.CopyTo(destFileName, True) IO.File.SetLastWriteTime(destFileName, sourceFile.LastWriteTime) changedFiles += 1 End If Next ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In sourceDir.GetDirectories() changedFiles += SynchronizeDirectory( _ subdir, IO.Path.Combine(destDirName, subdir.Name)) Next Return changedFiles End Function
10.3.8 Syntaxzusammenfassung Informationen über Dateien und Verzeichnisse ermitteln Die Klassen DirectoryInfo und FileInfo sind beide von der Klasse FileSystemInfo abgeleitet. Deswegen gibt es eine Menge gemeinsamer Eigenschaften und Methoden, die im ersten Syntaxkasten beschrieben werden. Spezifische Schlüsselwörter der beiden Klassen folgen in den beiden weiteren Syntaxkästen. System.IO.DirectoryInfo und .FileInfo-Klassen Attributes
gibt die Datei- bzw. Verzeichnisattribute an. Der Wert setzt sich aus der Kombination von IO.FileAttributesKonstanten zusammen (siehe nächste Überschrift).
CreationTime
gibt den Zeitpunkt an, zu dem das Verzeichnis bzw. die Datei erzeugt wurde (Date-Objekt).
Exists
gibt an, ob das angegebene Verzeichnis bzw. die Datei tatsächlich existiert.
Extension
gibt die Dateikennung an (z.B. ".bmp").
FullName
gibt den vollständigen Namen an (inklusive Laufwerk und allen Verzeichnissen).
LastAccessTime
gibt den Zeitpunkt des letzten Lesezugriffs an.
LastWriteTime
gibt den Zeitpunkt der letzten Veränderung an.
410
10 Dateien und Verzeichnisse
System.IO.DirectoryInfo und .FileInfo-Klassen Name
gibt den Datei- oder Verzeichnisnamen ohne Unterverzeichnisse an.
Spezifische Schlüsselwörter der System.IO.FileInfo-Klasse Directory
liefert ein DirectoryInfo-Objekt des Verzeichnisses, in dem sich die Datei befindet.
DirectoryName
liefert eine Zeichenkette mit dem Verzeichnis, in dem sich die Datei befindet.
Length
liefert die Größe der Datei (in Byte).
Spezifische Schlüsselwörter der System.IO.DirectoryInfo-Klasse GetDirectories()
liefert alle Unterverzeichnisse als DirectoryInfo-Feld.
GetDirectories("*.abc")
wie GetDirectories(), liefert aber nur Unterverzeichnisse, die der angegebenen Maske entsprechen.
GetFiles()
liefert alle Dateien als FileInfo-Feld.
GetFiles("*.abc")
wie GetFiles(), liefert aber nur die Dateien, die der angegebenen Maske entsprechen.
GetFileSystemInfos()
liefert alle Unterverzeichnisse und Dateien in Form eines FileSystemInfo-Felds.
GetFileSystemInfos("*.abc")
wie GetFileSystemInfos(), liefert aber nur Dateien und Verzeichnisse, die der Maske entsprechen.
Parent
liefert das übergeordnete Verzeichnis als DirectoryInfoObjekt.
Root
liefert das Wurzelverzeichnis (z.B. C:\ oder \\mars\data) als DirectoryInfo-Objekt.
Datei- und Verzeichnisattribute Die folgende Tabelle fasst die möglichen Verzeichnis- und Dateiattribute zusammen. Beachten Sie, dass manche Attribute nur dann zur Verfügung stehen, wenn ein NTFS-Dateisystem vorliegt (Windows NT, 2000, XP). System.IO.FileAttributes-Aufzählung Archive
die Datei wurde durch ein Backup-Programm archiviert und seither nicht mehr verändert.
Compressed
die Datei ist komprimiert.
10.3 Laufwerke, Verzeichnisse, Dateien
411
System.IO.FileAttributes-Aufzählung Device
es handelt sich nicht um eine normale Datei, sondern um ein so genanntes Device, das den direkte Zugriff auf die Hardware ermöglicht.
Directory
es handelt sich nicht um eine Datei sondern um ein Verzeichnis.
Encrypted
die Datei ist verschlüsselt.
Hidden
die Datei ist versteckt.
Normal
es handelt sich um eine gewöhnliche Datei.
NotContentIndexed
die Datei ist nicht Teil eines Index.
Offline
die Datei ist nur über ein Netzwerk/Internet zugänglich, momentan besteht aber keine Verbindung.
ReadOnly
die Datei darf nicht verändert werden.
ReparsePoint
die Datei enthält zusätzliche, benutzerspezifische Informationen.
SparseFile
die Datei besteht überwiegend aus 0-Bytes.
System
es handelt sich um eine Systemdatei.
Temporary
es handelt sich um eine temporäre Datei.
Dateien öffnen bzw. neu erzeugen System.IO.File-Methoden AppendText("filename")
liefert ein StreamWriter-Objekt, mit dessen Hilfe Sie an die bereits vorhandene Datei Text anfügen können.
Create("name")
erzeugt bzw. überschreibt die angegebene Datei und liefert ein FileStream-Objekt zur weiteren Bearbeitung.
CreateText("name")
wie oben, liefert aber ein ein StreamWriter-Objekt zum Schreiben der Datei.
Open("name", mode, [access [,share]])
öffnet die Datei name und liefert ein FileStream-Objekt zur weiteren Bearbeitung. mode gibt an, wie die Datei geöffnet wird (IO.FileModeAufzählung). access gibt den Zugriffsmodus an (IO.FileAccessAufzählung). share gibt an, ob ein Mehrfachzugriff zulässig ist (IO.FileSharing-Aufzählung).
OpenRead("name")
öffnet die vorhandene Datei und liefert ein FileStreamObjekt zum Lesen der Daten.
412
10 Dateien und Verzeichnisse
System.IO.File-Methoden OpenText("name")
öffnet die vorhandene Textdatei und liefert ein StreamReader-Objekt zum Lesen der Daten.
OpenWrite("name")
öffnet die vorhandene Datei und liefert ein FileStreamObjekt zum Lesen und Schreiben von Daten.
System.IO.FileInfo-Methoden liefert ein StreamWriter-Objekt, um am Ende der Datei Text hinzuzufügen.
Create()
erzeugt bzw. überschreibt die Datei und liefert ein FileStream-Objekt zum Schreiben der Datei.
CreateText()
wie oben, liefert aber ein StreamWriter-Objekt zum Schreiben der Textdatei.
Open(mode, [access [,share]])
siehe File.Open.
OpenRead(), OpenWrite()
siehe File.OpenRead() bzw. File.OpenWrite().
OpenText()
siehe File.OpenText().
VERWEIS
AppendText()
Eine Referenz der Aufzählungen IO.FileMode, IO.FileAccess und IO.FileSharing finden Sie in der Syntaxreferenz zum FileStream-Objekt in Abschnitt 10.6.4.
Verzeichnisse neu erzeugen System.IO.Directory-Methoden CreateDirectory("name")
erzeugt das Verzeichnis und liefert ein DirectoryInfo-Objekt.
System.IO.DirectoryInfo-Methoden Create()
erzeugt das Verzeichnis. Wenn das Verzeichnis bereits existiert, bleibt es unverändert.
CreateSubdirectory("name")
erzeugt ein Unterverzeichnis zum aktuellen Verzeichnis und liefert ein neues DirectoryInfo-Objekt zur weiteren Bearbeitung.
10.3 Laufwerke, Verzeichnisse, Dateien
413
Vorhandene Dateien und Verzeichnisse kopieren, verschieben, löschen System.IO.File-Methoden Copy("name1", "name2")
kopiert die Datei name1. Falls name2 schon existiert, tritt ein Fehler auf.
Copy("name1", "name2", True)
kopiert die Datei name1. Falls name2 schon existiert, wird diese Datei überschrieben.
Delete("name")
löscht die Datei.
Exists("name")
testet, ob die Datei existiert.
Move("name1", "name2")
benennt name1 in name2 um. Quelle und Ziel dürfen sich auch auf unterschiedlichen Verzeichnissen bzw. Laufwerken befinden, dann wird die Datei verschoben.
System.IO.FileInfo-Methoden CopyTo("zielname")
kopiert die Datei zu zielname. Falls zielname schon existiert, tritt ein Fehler auf. Die Methode liefert ein neues FileInfo-Objekt, das auf die neue Datei verweist.
CopyTo("zielname", True)
wie oben, aber zielname wird gegebenenfalls überschrieben.
Delete()
löscht die Datei.
MoveTo("zielname")
benennt die Datei um bzw. verschiebt sie in ein anderes Verzeichnis oder Laufwerk. Das FileInfo-Objekt verweist anschließend auf zielname.
System.IO.Directory-Methoden Delete("name")
löscht das Verzeichnis, wenn es leer ist.
Delete("name", True)
löscht das Verzeichnis mit seinem gesamten Inhalt (auch Unterverzeichnisse).
Exists("name")
testet, ob das Verzeichnis existiert.
Move("name1", "name2")
verschiebt das Verzeichnis bzw. benennt es um.
System.IO.DirectoryInfo-Methoden Delete()
löscht das Verzeichnis, wenn es leer ist.
Delete(True)
löscht das Verzeichnis samt Inhalt.
MoveTo("zielname")
benennt das Verzeichnis um bzw. verschiebt es in ein anderes Verzeichnis oder Laufwerk. Das DirectoryInfoObjekt verweist anschließend auf zielname.
414
10 Dateien und Verzeichnisse
Spezielle Verzeichnisse ermitteln Microsoft.VisualBasic.FileSystem-Klasse CurDir()
liefert das aktuelle Verzeichnis als Zeichenkette.
ChDir("c:\test")
ändert das aktuelle Verzeichnis und Laufwerk.
ChDrive("d:")
ändert das aktuelle Laufwerk. Damit wird das für dieses Laufwerk zuletzt aktuelle Verzeichnis wieder zum aktuellen Verzeichnis.
Environ("temp")
liefert das temporäre Verzeichnis.
Environ("windir")
liefert das Windows-Verzeichnis.
System.IO-Namensraum DirectoryInfo(".").FullName
ermittelt das aktuelle Verzeichnis.
Directory. _ SetCurrentDirectory("c:\test")
ändert das aktuelle Verzeichnis und Laufwerk.
System.IO.Path-Klasse GetTempPath()
liefert den Namen des temporären Verzeichnisses.
GetTempFileName()
liefert den Namen einer temporären Datei, die zu diesem Zweck als leere Datei erzeugt wird.
System.Reflection-Namensraum Assembly. _ GetExecutingAssembly. _ Location
liefert den vollständigen Namen (inklusive Laufwerk und Verzeichnissen) des laufenden VB.NET-Programms.
System.Environment-Klasse CurrentDir
liefert das aktuelle Verzeichnis.
GetEnvironmentVariable(var)
liefert das temporäre Verzeichnis (var="temp") bzw. das Windows-Verzeichnis (var="windir").
GetFolderPath(fld)
liefert ein spezielles Verzeichnis. fld ist eine Konstante der System.Environment.SpecialFolder-Aufzählung (siehe nächste Box).
GetLogicalDrives
liefert ein String-Feld mit den Namen aller Laufwerke des Rechners (z.B. "C:\").
10.3 Laufwerke, Verzeichnisse, Dateien
415
System.Environment.SpecialFolder-Aufzählung für GetFolderPath ApplicationData
persönliche Anwendungsdaten
CommonApplicationData
allgemeine Anwendungsdaten
CommonProgramFiles
gemeinsame Dateien (C:\Programme\Gemeinsame Dateien)
Cookies
Cookies-Verzeichnis
DesktopDirectory
Desktop-Verzeichnis
Favorites
Favoriten des Internet Explorers
History
Verlauf des Internet Explorers
InternetCache
Cache des Internet Explorers
LocalApplicationData
lokale Anwendungsdaten
Personal
persönliche Dateien (C:\Dokumente und Einstellungen\user\Eigene Dateien)
ProgramFiles
Installationsverzeichnis für Programme
Programs
Verzeichnis des Startmenüs PROGRAMME
Recent
zuletzt genutzte Dokumente
SendTo
Verzeichnis der SENDEN-AN-Programme
StartMenu
Verzeichnis des Startmenüs
Startup
Autostart-Verzeichnis
System
Windows-Systemverzeichnis
Templates
Vorlagenverzeichnis
Bearbeitung von Datei- und Verzeichnisnamen (IO.Path) System.IO.Path-Klasse – Datei- und Verzeichnisnamen bearbeiten ChangeExtension(s, ext)
ändert die Kennung des Dateinamens.
GetDirectoryName(s)
liefert das Laufwerk und das Verzeichnis.
GetExtension(s)
liefert die Dateikennung (z.B. ".bmp").
GetFileName(s)
liefert den Dateinamen ohne Verzeichnisse.
GetFileNameWithoutExtension(s)
wie GetFileName, es wird aber auch die Dateikennung entfernt.
GetFullPath(s)
verbindet einen ohne Verzeichnis angegebenen Dateinamen mit dem aktuellen Verzeichnis.
GetPathRoot(s)
liefert das Laufwerk (z.B. "C:\" oder "\\mars\data").
HasExtension(s)
testet, ob der Dateiname eine Kennung hat.
416
10 Dateien und Verzeichnisse
System.IO.Path-Klasse – Sonderzeichen DirectorySeparatorChar
liefert das Zeichen zur Trennung von Verzeichnissen: Windows: "\" Apple: ":" Unix: "/"
AltDirectorySeparatorChar
gibt eine (nur für den System.IO-Namensraum!) zulässige Alternative zu DirectorySeparatorChar an: Windows: "/" Apple: "/" Unix: "\"
InvalidPathChars
liefert ein Char-Feld mit Zeichen, die nicht in Dateinamen vorkommen dürfen. Unter Windows sind das die Zeichen "<", ">", "|", das Hochkomma " sowie 0-Codes. (Die Online-Dokumentation nennt weitere Zeichen, die aber in InvalidPathChars nicht enthalten sind.)
PathSeparator
liefert das Zeichen zur Trennung mehrerer Namen voneinander. Unter Windows ist das das Zeichen ";".
VolumeSeparatorChar
liefert das Zeichen zur Angabe von Laufwerken. Windows: ":" Apple: ":" Unix: "/"
10.4
Standarddialoge
VERWEIS
Immer wieder wird es in Windows-Programmen vorkommen, dass der Anwender in einem Dialog einen Dateinamen auswählen soll, beispielsweise um eine Datei zu laden oder um eigene Daten in einer (eventuell neuen) Datei zu speichern. Die Bibliothek System.Windows.Forms.dll enthält zu diesem Zweck fertige Dialoge, die Thema dieses Abschnitts sind. OpenFileDialog und SaveFileDialog gehören zu einer ganzen Gruppe so genannter Standarddialoge, die alle im Namensraum System.Windows.Forms definiert sind. All-
gemeine Informationen zu diesen Standarddialogen finden Sie in Abschnitt 15.5.
10.4.1 Dateiauswahl Um diese Dialoge in einem eigenen Windows-Programm anzuwenden, fügen Sie OpenFileDialog oder SaveFileDialog aus der Toolbox in Ihr Formular ein. Die resultierenden Objekte OpenFileDialog1 bzw. SaveFileDialog1 (die Sie natürlich umbenennen können) wenden Sie dann entsprechend dem folgenden Muster an:
10.4 Standarddialoge
417
' Beispiel dateien\open-save-dialog Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei laden; der Dateiname befindet sich in ' OpenFileDialog1.FileName End If End Sub Private Sub Button2_Click(...) Handles Button2.Click If SaveFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei speichern; der Dateiname befindet sich in ' SaveFileDialog1.FileName End If End Sub
Abbildung 10.4: Standarddialog zur Dateiauswahl
Open- und SaveFileDialog sind von der Basisklasse FileDialog abgeleitet. Bevor Sie den Dialog mit ShowDialog aufrufen, können Sie eine Menge Eigenschaften einstellen, um den Aus-
wahlprozess zu steuern (siehe Syntaxzusammenfassung). Der Unterschied zwischen den beiden Dialogen besteht darin, dass der Anwender bei OpenFileDialog nur existierende Dateien auswählen darf, während bei SaveFileDialog auch der Name einer noch nicht existierenden Datei angegeben werden darf. Wenn mit SaveFileDialog eine vorhandene Datei
418
10 Dateien und Verzeichnisse
ausgewählt wird, erscheint eine Sicherheitsabfrage, ob diese Datei überschrieben werden soll. Beachten Sie, dass die Methode ShowDialog den Dialog nur anzeigt, die Datei aber anschließend weder lädt noch speichert. Dafür sind Sie selbst verantwortlich. Den Dateinamen können Sie der Eigenschaft FileName entnehmen.
Mehrfachauswahl Beim OpenFileDialog können Sie auch mehrere Dateien gleichzeitig auswählen. Dazu setzen Sie vor dem Anzeigen des Dialogs die Eigenschaft Multiselect auf True. Nach der Auswahl können Sie mit FileNames auf ein String-Feld zugreifen, das die einzelnen Dateinamen enthält.
Verwendung in Konsolenanwendungen Die Dialoge können auch in Konsolenanwendungen eingesetzt werden. Dazu müssen Sie einen Verweis auf die Bibliothek System.Windows.Forms.dll einrichten (da diese Bibliothek per Default bei Konsolenanwendungen nicht zur Verfügung steht). Außerdem müssen Sie ein Objekt der Klassen Open- oder SaveFileDialog erstellen: ' Beispiel dateien\open-file-console Sub Main() Dim ofd As New Windows.Forms.OpenFileDialog() Console.WriteLine("Wählen Sie einen Dateinamen aus!") If ofd.ShowDialog() = Windows.Forms.DialogResult.OK Then Console.WriteLine("Dateiname: {0}", ofd.FileName) Else Console.WriteLine("Dateiauswahl wurde abgebrochen.") End If Console.WriteLine("Return drücken") Console.ReadLine() End Sub
Syntaxzusammenfassung Windows.Forms.OpenFileDialog und .SaveFileDialog – Methoden und Eigenschaften ShowDialog()
zeigt den Dialog an und liefert DialogResult.OK, wenn die Dateiauswahl ordnungsgemäß beendet wird.
AddExtension
gibt an, ob an den ausgewählten Dateinamen automatisch eine Typerweiterung angefügt werden soll, wenn der Benutzer keine angibt. Die Erweiterung wird aus Filter entnommen.
10.4 Standarddialoge
419
Windows.Forms.OpenFileDialog und .SaveFileDialog – Methoden und Eigenschaften CheckFileExists
gibt an, ob nur bereits existierende Dateinamen ausgewählt werden dürfen (per Default False).
CheckPathExists
gibt an, ob Dateien nur aus schon existierenden Verzeichnissen ausgewählt werden dürfen (per Default True).
FileName
enthält den vollständigen Dateinamen.
FileNames
enthält ein String-Feld mit allen ausgewählten Dateinamen (nur bei einer Mehrfachauswahl).
Filter
enthält einen oder mehrere Filter für die Dateitypen. Die Zeichenkette muss nach dem folgenden Muster zusammengesetzt werden: "text1|filter1|text2|filter2", also beispielsweise "Textdateien|*.txt".
InitialDirectory
gibt das Startverzeichnis für die Auswahl an.
Multiselect
gibt an, ob mehrere Dateien zugleich ausgewählt werden dürfen.
Title
gibt an, welcher Text im Fenstertitel des Dialogs angezeigt werden soll.
ValidateNames
gibt an, ob nur gültige Windows-Dateinamen akzeptiert werden sollen (per Default True).
10.4.2 Verzeichnisauswahl Die offiziellen Standarddialoge eignen sich zwar zur Auswahl einer bestehenden oder neuen Datei, nicht aber zur Auswahl eines Verzeichnisses. In manchen Anwendungen ist aber die Auswahl eines Verzeichnisses sehr wohl erforderlich (beispielsweise wenn ein Verzeichnis angegeben werden soll, aus dem Dateien gelesen bzw. in dem Dateien gespeichert werden sollen). Versteckt in den Tiefen der .NET-Bibliothek erfüllt die Klasse Windows.Forms.Design.FolderNameEditor.FolderBrowser aus der Bibliothek System.Design.dll genau diese Aufgabe: ShowDialog zeigt einen Dialog an, in dem Sie ein bestehendes Verzeichnis auswählen bzw. ein neues Verzeichnis erstellen können. Die Eigenschaft ReturnPath enthält anschließend den
VORSICHT
Verzeichnisnamen. Die .NET-Dokumentation zu FolderNameEditor.FolderBrowser enthält folgenden Hinweis: This type supports the .NET Framework infrastructure and is not intended to be used directly from your code. Mit anderen Worten: Auch wenn der hier vorgestellte Code funktioniert, ist nicht sicher, dass dies auch für künftige .NET-Versionen gilt.
420
10 Dateien und Verzeichnisse
Definition der neuen Klasse FolderBrowserDialog Bevor diese Klasse wie die anderen in diesem Abschnitt vorgestellten Standarddialoge verwendet werden kann, ist allerdings etwas Vorarbeit erforderlich. Im folgenden Beispielprogramm wird die neue Klasse FolderBrowserDialog definiert, die von FolderNameEditor.FolderBrowser abgeleitet ist. Das ist erforderlich, weil FolderNameEditor.FolderBrowser als Protected gilt und nicht unmittelbar verwendet werden kann. (Die Idee des Beispiels stammt übrigens von Frank Eller und wurde als Beitrag der News-Gruppe microsoft.public.dotnet.languages.csharp am 15.8.2001 publiziert.) Der Code für die neue Klasse FolderBrowserDialog enthält keine Besonderheiten. Die Klasse ist von FolderNameEditor abgeleitet. Intern wird ein FolderNameEditor.FolderBrowser-Objekt zur Anzeige des Dialogs verwendet. Nach außen hin sind nur die Eigenschaften Description und ReturnPath sowie die Methode ShowDialog zugänglich. ' ' ' '
Beispiel benutzeroberflaeche\folderbrowser Klassendatei folderbrowserdialog.vb dieser Code setzt voraus, dass ein Verweis auf die .NET-Bibliothek System.Design.dll eingerichtet wird
Public Class FolderBrowserDialog Inherits Windows.Forms.Design.FolderNameEditor Private fb As New _ Windows.Forms.Design.FolderNameEditor.FolderBrowser() Private fbReturnPath As String Public Description As String Public ReadOnly Property ReturnPath() As String Get Return fbReturnPath End Get End Property Public Function ShowDialog() As DialogResult Dim result As DialogResult fb.Description = Me.Description fb.StartLocation = FolderBrowserFolder.MyComputer result = fb.ShowDialog() If (result = DialogResult.OK) Then Me.fbReturnPath = fb.DirectoryPath Else Me.fbReturnPath = String.Empty End If Return result End Function End Class
10.5 Textdateien lesen und schreiben
421
Anwendung der Klasse FolderBrowserDialog Die Anwendung der Klasse FolderBrowserDialog erfolgt ähnlich wie bei den anderen Standarddialogen: Der einzige Unterschied besteht darin, dass die Entwicklungsumgebung FolderBrowserDialog nicht als Steuerelement erkennt und dass Sie ein Objekt dieser Klasse deswegen selbst erzeugen müssen. Den Dialog zeigen Sie mit ShowDialog an; anschließend können Sie der Eigenschaft ReturnPath den Verzeichnisnamen entnehmen. ' Beispiel benutzeroberflaeche\folderbrowser ' Formulardatei form1.vb Private Sub Button1_Click(...) Handles Button1.Click Dim fb As New FolderBrowserDialog() fb.Description = "Verzeichnis mit den Quelldateien auswählen" If fb.ShowDialog() = DialogResult.OK Then Label1.Text = fb.ReturnPath Else Label1.Text = "Auswahl wurde abgebrochen ..." End If End Sub
Abbildung 10.5: Standarddialog zur Verzeichnisauswahl
10.5
Textdateien lesen und schreiben
Mit den Methoden der Klassen StreamReader und StreamWriter können Sie Textdateien komfortabel lesen und schreiben. Der Dateizugriff ist rein sequentiell, es ist daher nicht möglich, die Schreib- oder Leseposition unmittelbar zu beeinflussen.
422
10 Dateien und Verzeichnisse
10.5.1 Codierung von Textdateien Wenn Sie mit Windows in Westeuropa arbeiten, erwarten Sie im Regelfall, dass die Zeichen in Textdateien mit dem ANSI-Zeichensatz (Codeseite 1252) codiert sind. Diese Dateien werden dann oftmals als ASCII-Dateien bezeichnet, was genau genommen aber falsch ist: ASCII definiert nur die Zeichen mit Codes von 0-127. ANSI lässt dagegen Codes bis 255 zu und ermöglicht so auch die Darstellung vieler Sonderzeichen in Europa verbreiteteter Sprachen (inklusive äöüß). Sobald Sie aber über die Grenzen Westeuropas hinaussehen, wird es komplizierter: So sind in Osteuropa andere Codeseiten üblich, die mit ANSI inkompatibel sind. Und im asiatischen Sprachraum reicht ein Byte pro Zeichen sowieso nicht aus. Der Ausweg aus diesem Dilemma heißt Unicode – also ein Zeichensatz, mit dem fast alle weltweit vorkommenden Zeichen codiert werden können (siehe auch Abschnitt 8.2.5). Allerdings gibt es mehrere Möglichkeiten, einzelne Unicode-Zeichen intern darzustellen (UTF-7, -8, -16), so dass die Bezeichnung Unicode-Datei noch nicht eindeutig ist. Per Default verwenden StreamReader und StreamWriter die Unicode-UTF-8-Codierung. StreamReader und -Writer kommen natürlich auch mit einigen weiteren Textcodierungen zurecht (z.B. ASCII ANSI, UTF-16), das muss dann aber explizit angegeben werden. (Beispiele folgen im Verlauf dieses Abschnitts.) Die UTF-8-Codierung bedeutet, dass ASCII-Zeichen mit einem Byte pro Zeichen, alle anderen Unicode-Zeichen aber mit zwei bis vier Byte codiert werden. Solange eine UTF-8Textdatei ausschließlich US-Zeichen enthält, ist sie nicht von einer ASCII-Datei zu unterscheiden.
TIPP
Auch wenn dieses Dateiformat wahrscheinlich eine große Zukunft hat (weil es auch unter Unix/Linux gebräuchlich ist), gibt es momemtan unter Windows nur relativ wenige Programme, die mit solchen Dateien umgehen können. Dazu zählen unter anderem die aktuellen Versionen von Microsoft Word sowie der Editor notepad.exe (z.B. jene Version, die mit Windows 2000 mitgeliefert wird). Word 2000 kann reine Textdateien in verschiedenen Unicode-Varianten speichern (DATEI|SPEICHERN UNTER, Dateityp CODIERTE TEXTDATEI). Bisweilen hat Word 2000 aber Probleme, derartige Dateien wieder zu öffnen. Diese Probleme treten vor allem bei ganz kurzen Dateien auf, wo es schwierig ist, die Codierung korrekt zu erkennen. Zeichen außerhalb des ASCII-Zeichensatzes werden dann falsch angezeigt. Bei längeren Dateien erkennt Word die Codierung meist selbstständig bzw. zeigt einen Konvertierungsdialog an, in dem Sie die gewünschte Codierung auswählen können.
Wie kann die Codierung einer Textdatei erkannt werden? Bei Textdateien mit einer Codierung mit einem Byte pro Zeichen gibt es in der Regel keine Kennzeichnung der Codierung. Damit also ein Austausch von Textdateien zwischen
10.5 Textdateien lesen und schreiben
423
verschiedenen Personen oder Programmen gelingt, muss der Empfänger wissen, welche Codierung der Sender verwendet hat! Etwas besser sieht es bei Unicode-Dateien aus: Dort ist für einige Codierungsvarianten eine Kennzeichnung durch die ersten Bytes vorgesehen. Genau genommen dient diese Kennzeichnung nur dazu, um die Bytereihenfolge anzugeben (byte order mark, kurz BOM). Diese kann bei UTF-16- und UTF-32-Dateien je nach Prozessorarchitektur variieren. Bei UTF-8-Dateien ist eine derartige Kennzeichnung dagegen nicht erforderlich, weil die Bytereihenfolge unabhängig von der Prozessorarchitektur ist. Dennoch sind auch für UTF-8Dateien drei Kennzeichnungsbytes vorgesehen, die aber optional sind. (In der Praxis werden Sie überwiegend auf UTF-8-Dateien ohne Kennzeichnung stoßen! Beachten Sie auch, dass es nicht üblich ist, einzelne Zeichenketten durch ein BOM zu kennzeichnen – etwa wenn Sie Unicode-Zeichenketten in einer Tabelle einer Datenbank speichern.) Die folgende Tabelle gibt die hexadezimalen Codes der ersten Bytes für die wichtigsten Unicode-Varianten an: FF FE FE FF 00 00 FE FF FF FE 00 00 EF BB BF
Unicode UTF-16 Unicode UTF-16 Big-Endian (umgekehrte Byte-Reihenfolge) Unicode UTF-32 Unicode UTF-32 Big-Endian (umgekehrte Byte-Reihenfolge) Unicode UTF-8 (optionale Kennzeichnung, fehlt oft!)
10.5.2 Textdateien lesen (StreamReader) Unicode-Textdatei öffnen Die folgenden Zeilen zeigen einige Möglichkeiten, um eine vorhandene Unicode-Textdatei zu öffnen. In jedem Fall ist das Ergebnis ein StreamReader-Objekt, mit dessen Hilfe die Datei dann gelesen werden kann. (Bei der letzten Variante bezieht sich New auf ein IO.FileInfoObjekt, auf das anschließend die Methode OpenText angewendet wird.) Dim sr As IO.StreamReader sr = New IO.StreamReader("c:\test\test1.txt") sr = New IO.StreamReader(io_stream_object) sr = IO.File.OpenText("c:\test\test2.txt") sr = New IO.FileInfo("c:\test\test3.txt").OpenText()
Bei allen drei Varianten werden die ersten Bytes der Datei ausgewertet, um die UnicodeVariante zu erkennen. Dateien, die als UTF-8- und UTF-16-Dateien gekennzeichnet sind (siehe Tabelle oben), werden korrekt behandelt. Alle anderen Dateien werden so verarbeitet, als wären sie gemäß UTF-8 codiert. Wenn die Datei in einer anderen Codierung vorliegt, führt dies natürlich zu fehlerhaften Ergebnissen. Das Beispielprogramm dateien\unicode\textdatei-anzeigen (Abbildung 10.6) demonstriert, was passiert, wenn die von StreamReader verwendete Defaultcodierung UTF-8 und die tatsächliche Codierung einer Textdatei nicht übereinstimmen. Nach dem Start des Programms können Sie eine Textdatei auswählen. Dabei haben Sie die Wahl zwischen meh-
424
10 Dateien und Verzeichnisse
reren Beispieldateien, die alle denselben Text enthalten – aber in unterschiedlichen Codierungen. Die Datei wird zur Gänze geladen und im Textfeld angezeigt.
Abbildung 10.6: StreamReader interpretiert die ANSI-Datei fälschlich als UTF-8-Datei und verschluckt deswegen alle deutschen Sonderzeichen
Die folgenden Zeilen zeigen die Ereignisprozedur zum Laden der Textdatei. Die Dateiauswahl erfolgt mit dem OpenFileDialog-Steuerelement. Anschließend wird ein StreamReaderObjekt erzeugt und mit der Methode ReadToEnd die gesamte Datei gelesen. Welche Codierung StreamReader intern verwendet, können Sie mit der Eigenschaft CurrentEncoding ermitteln. Diese Eigenschaft liefert als Ergebnis ein Objekt der Klasse Text.Encoding. Beachten Sie, dass CurrentEncoding erst dann ausgewertet werden darf, nachdem die ersten Zeichen der Datei gelesen wurden! Erst dann versucht StreamReader, die Codierung automatisch zu erkennen. Beachten Sie auch, dass die von StreamReader eingesetzte Codierung keineswegs immer korrekt ist (was Abbildung 10.6 ja beweist). ' Beispielprogramm dateien\unicode\textdatei-anzeigen Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim sr As New IO.StreamReader(OpenFileDialog1.FileName) TextBox1.Text = sr.ReadToEnd() ' Infos über Datei anzeigen LabelFileName.Text = OpenFileDialog1.FileName LabelCode.Text = sr.CurrentEncoding.EncodingName sr.Close() End If End Sub
10.5 Textdateien lesen und schreiben
425
Textdatei in eine anderen Codierung (z.B. ANSI) öffnen Wenn Sie Textdateien mit einer anderen Codierung als UTF-8 öffnen möchten, müssen Sie zum Öffnen den StreamReader-Konstruktur (also New) verwenden und die gewünschte Codierung durch ein Text.Encoding-Objekt angeben. Das Encoding-Objekt für ANSI erhalten Sie mit GetEncoding(1252). (Die Nummer 1252 bezeichnet die Codeseite für ANSI-Latin-1.) Dim enc As Text.Encoding = Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader("c:\test\test1.txt", enc)
Wenn Sie statt ANSI eine andere Codierung verwenden möchten, müssen Sie vorher ein entsprechendes Text.Encoding-Objekt erzeugen. Für die wichtigsten von .NET unterstützten Codierungen können Sie dabei die folgende Kurzschreibweise verwenden. (Das Ergebnis von Text.Encoding.Default ist abhängig vom Betriebssystem und von den Ländereinstellungen.) Dim enc enc enc enc enc enc
enc As Text.Encoding = Text.Encoding.ASCII = Text.Encoding.Default = Text.Encoding.UTF7 = Text.Encoding.UTF8 = Text.Encoding.Unicode = Text.Encoding.BigEndianUnicode
'US-ASCII (7 Bit) 'Windows-Default-Codepage 'UTF-7 'UTF-8 'UTF-16 'UTF-16 Big Endian
Für alle anderen Codierungen muss GetEncoding(n) oder GetEncoding("name") verwendet werden. Dabei gibt n die Nummer der Codeseite an. Alternativ kann auch deren Windows-interner Name angegeben werden (z.B. "Windows-1252"). Die folgenden Zeilen zeigen einige Beispiele: enc enc enc enc
= = = =
Text.Encoding.GetEncoding(850) Text.Encoding.GetEncoding(1252) Text.Encoding.GetEncoding(28591) Text.Encoding.GetEncoding(28605)
'OEM Multilingual Latin 1 (DOS) 'ANSI Latin-1 'ISO 8859-1 Latin-1 'ISO 8859-15 Latin-9
Eine Liste mit den am Rechner installierten Codeseiten finden Sie in der Systemsteuerung im Dialog LÄNDEREINSTELLUNGEN|ALLGEMEIN|ERWEITERT (siehe Abbildung 10.7).
Inhalt der Datei lesen Um eine Datei vollständig zu lesen, verwenden Sie die Methode ReadToEnd: Dim allLines As String allLines = sr.ReadToEnd()
Wenn Sie eine Datei zeilenweise lesen möchten, können Sie den folgenden Code als Muster verwenden (sr ist ein StreamReader-Objekt). Das Ende der Datei erkennen Sie daran, dass ReadLine als Ergebnis Nothing liefert. Seien Sie aber vorsichtig bei der Formulierung der Schleife! Wenn Sie das Schleifenende mit s = Nothing feststellen, bricht die Schleife bereits bei der ersten leeren Zeile ab! Der Test muss vielmehr IsNothing(s) oder s Is Nothing lauten (weil VB.NET Nothing und "" als gleichwertig betrachtet).
426
10 Dateien und Verzeichnisse
Abbildung 10.7: Am lokalen Rechner verfügbare Codeseiten
Dim line As String Do line = sr.ReadLine() If IsNothing(line) Then Exit Do Console.WriteLine(line) Loop
Natürlich können Sie auch einzelne Zeichen lesen. Read() liefert den Code eines Zeichens oder -1, wenn das Ende der Datei erreicht ist. (Damit auch -1 zurückgegeben werden kann, liefert Read einen Integer-Wert und nicht – wie Sie vielleicht erwarten würden – einen CharWert.) Peek funktioniert wie Read, allerdings bleibt der Dateizeiger dabei unverändert. Das Zeichen kann also anschließend mit Read gelesen werden. Die folgende Schleife schreibt jedes einzelne Zeichen einer Datei in das Konsolenfenster. While sr.Peek() <> -1 Console.Write(ChrW(sr.Read())) End While
Die Syntaxvariante Read(c, pos, n) ermöglicht es, n Zeichen in das Char-Feld c() zu lesen. pos gibt an, wo in c() die Zeichen eingefügt werden sollen. (pos gibt nicht die Stelle an, von der die Zeichen aus der Datei gelesen werden! Die Textdatei wird immer sequentiell gelesen.)
10.5 Textdateien lesen und schreiben
427
Read liefert die Zahl der tatsächlich gelesenen Zeichen zurück. Dieser Wert kann kleiner als n sein, wenn das Ende der Datei erreicht wird.
Datei schließen Wenn Sie die Datei fertig gelesen haben, sollten Sie darauf achten, das StreamReader-Objekt so rasch wie möglich mit Close zu schließen. Damit können andere Benutzer oder Programme wieder auf die Datei zugreifen. (Verlassen Sie sich nicht darauf, dass die Datei automatisch geschlossen wird, wenn das StreamReader-Objekt – z.B. am Ende einer Prozedur – nicht mehr gültig ist. Aufgrund der neuen Speicherverwaltung von VB.NET wird ein Objekt nicht sofort gelöscht, wenn es nicht mehr gültig ist, sondern zu einem unbestimmten Zeitpunkt!)
10.5.3 Textdateien schreiben (StreamWriter) Das Gegenstück zu StreamReader ist erwartungsgemäß die Klasse StreamWriter. Damit können Sie sowohl neue Textdateien erzeugen als auch Text an vorhandene Dateien anfügen. Per Default verwendet StreamWriter UTF-8, wobei neue Dateien nicht mit Kennzeichnungsbytes ausgestattet werden. Auf Wunsch können Sie aber natürlich auch eine andere Codierung auf der Basis eines Text.Encoding-Objekts verwenden.
Neue UTF-8-Textdateien erzeugen Es gibt eine ganze Reihe von Syntaxvarianten, um eine neue Textdatei zu erzeugen. Sollte es bereits eine Datei mit dem angegebenen Namen geben, wird diese ohne Rückfrage überschrieben (also gelöscht). Dim sw As IO.StreamWriter sw = New IO.StreamWriter("c:\test\test1.txt") sw = New IO.StreamWriter(io_stream_object) sw = IO.File.CreateText("c:\test\test2.txt") sw = New IO.FileInfo("c:\test\test3.txt").CreateText()
Vorhandene Textdatei öffnen, um Text anzufügen Wenn die Datei bereits existiert und Sie Text am Ende der Datei anfügen möchten, verwenden Sie eine der folgenden Anweisungen, um ein StreamWriter-Objekt zu erzeugen: sw = New IO.StreamWriter("c:\test\test1.txt", True) sw = IO.File.AppendText("c:\test\test2.txt") sw = New IO.FileInfo("c:\test\test3.txt").AppendText()
Andere Codierung als UTF-8 verwenden Wenn Sie eine andere Codierung als UTF-8 verwenden möchten, müssen Sie die gewünschte Codierung als dritten Parameter bei New StreamWriter angeben. (Wie Sie Text.En-
428
10 Dateien und Verzeichnisse
coding-Objekte erzeugen können, wurde bereits im vorigen Abschnitt beschrieben.) Der
zweite Parameter gibt an, ob Dateien an eine vorhandene Datei hinzugefügt werden sollen (True) oder ob die Datei neu erzeugt werden soll (False).
HINWEIS
Dim enc As Text.Encoding = Text.Encoding.GetEncoding(1252) 'ANSI sw = New IO.StreamWriter("c:\test\test1.txt", False, enc) 'neue Datei sw = New IO.StreamWriter("c:\test\test2.txt", True, enc) 'anfügen
Intern verwendet VB.NET zur Darstellung von Zeichenketten immer Unicode. Beim Speichern einer Zeichenkette in einer Textdatei werden alle Zeichen entsprechend der gewählten Codierung in Bytecodes umgewandelt. Wenn ein UnicodeZeichen in der Codierung nicht vorgesehen ist (z.B. ein deutsches Sonderzeichen bei der Codierung Text.Encoding.ASCII), wird es beim Speichern automatisch durch ein Fragezeichen ersetzt (oder durch einen speziellen Code für unbekannte Zeichen, sofern ein derartiger Code im Zeichensatz vorgesehen ist).
Kennzeichnung von Unicode-Dateien StreamWriter verwendet zwar per Default UTF-8, kennzeichnet neue Dateien aber nicht mit den drei BOM-Bytes (EF BB BF). Wenn Sie das möchten, erzeugen Sie die neue Datei unter Angabe eines Text.Encoding.UFT8-Objekts: Dim enc As Text.Encoding = Text.Encoding.UTF8 sw = New IO.StreamWriter("c:\test\test1.txt", False, enc)
UTF-16-Dateien (also Dateien auf der Basis von Text.Encoding.Unicode oder .BigEndianUnicode) werden in jedem Fall durch die zwei BOM-Bytes FF FE bzw. FE FF gekennzeichnet.
Text schreiben Mit den Methoden Write und WriteLine können Sie Text in der Datei speichern. Die Methoden unterscheiden sich nur dadurch, dass WriteLine noch ein oder zwei Bytes zur Kennzeichnung des Zeilenendes anfügt. (Diese Codes können mit NewLine ermittelt und bei Bedarf auch verändert werden. Per Default verwendet StreamWriter die unter Windows üblichen Codes 13 und 10.) Sowohl bei Write als auch bei WriteLine gibt es unzählige Syntaxvarianten, dank derer fast alle elementaren .NET-Datentypen direkt als Parameter übergeben werden können. Per Default werden bei der Umwandlung in Zeichenketten die aktuellen Ländereinstellungen berücksichtigt. sw.WriteLine(1/3)
'schreibt "0,3333333333333333" in die Datei
In der Variante Write[Line]("format", x, y ...) werden die Parameter x, y etc. entsprechend der im ersten Parameter übergebenen Zeichenkette formatiert (siehe auch Abschnitt 8.5). Write[Line] bietet allerdings keine Syntaxvariante, mit der die Landeseinstellung für die Formatierung von Zahlen und Daten verändert werden kann. Wenn Sie das möchten, müssen Sie auf String.Format zurückgreifen:
10.5 Textdateien lesen und schreiben
429
' schreibt z.B. "venerdì 4 gennaio 2002" in die Datei Dim cult As New Globalization.CultureInfo("it-IT") 'italienisch sw.WriteLine(String.Format(cult, "{0:D}", Now))
Die mit Write[Line] übergebenen Daten werden nicht sofort bei jeder Änderung physikalisch auf der Festplatte gespeichert, sondern aus Geschwindigkeitsgründen für einige Zeit zwischengespeichert. Um zu erreichen, dass die Daten tatsächlich sofort gespeichert werden, führen Sie die Methode Flush aus oder setzen die Eigenschaft AutoFlush auf True.
Datei schließen Wie bei StreamReader sollten Sie auch StreamWriter-Objekte so rasch wie möglich mit Close schließen, um die Datei für andere Programme oder Benutzer wieder freizugeben.
10.5.4 Beispiel – Textdatei erstellen und lesen Das folgende Beispielprogramm erzeugt in der Prozedur WriteTextFile zuerst das Verzeichnis C:\test (sofern dieses noch nicht existiert) und dann die Datei C:\test\test1.txt. Darin werden drei Zeilen Text gespeichert. In der Prozedur ReadTextFile wird diese Datei neu eingelesen und mit MsgBox angezeigt (siehe Abbildung 10.8). DeleteTextFile löscht schließlich (nach einer Rückfrage) das gesamte Verzeichnis C:\test.
Abbildung 10.8: Die Dialogbox zeigt den Inhalt der Datei c:\test\test1.txt
Wenn Sie ein Programm zur hexadezimalen Darstellung von Dateien besitzen, können Sie sich Byte für Byte ansehen, wie die Datei intern aussieht (siehe Abbildung 10.9 mit dem Freeware-Hexeditor XVI32). Deutlich zu sehen sind die drei UTF-8-Kennzeichnungsbytes sowie die Codierung der Zeichen äöü bzw. € durch zwei bzw. drei Bytes. Sowohl Write- als auch ReadTextFile verwenden dasselbe Text.Encoding-Objekt zur Codierung des Texts. Versuchen Sie probeweise, die erste Codezeile in Main so zu verändern, dass die ASCII-Codierung verwendet wird (Text.Encoding.ASCII). Sie werden feststellen, dass die deutschen Sonderzeichen und das Eurozeichen dann fehlen. Die Datei enthält stattdessen Fragezeichen.
430
10 Dateien und Verzeichnisse
Abbildung 10.9: Ansicht von c:\test\test1.txt mit einem Hexeditor
' Beispiel dateien\textdateien Dim enc As Text.Encoding Sub Main() enc = Text.Encoding.UTF8 WriteTextFile() ReadTextFile() DeleteTextFile() End Sub
HINWEIS
Sub WriteTextFile() ' Verzeichnis c:\test erzeugen Dim dir As New IO.DirectoryInfo("c:\test") If Not dir.Exists Then dir.Create() ' neue Textdatei c:\test\test1.txt erzeugen Dim sw As IO.StreamWriter sw = New IO.StreamWriter("c:\test\test1.txt", False, enc) sw.WriteLine("bla bla äöü €") sw.WriteLine("zweite Zeile") sw.WriteLine("dritte Zeile") sw.Close() End Sub
Beachten Sie bitte, dass das Programm ohne die obige Anweisung sw.Close nicht funktionieren würde! Zwar verliert die Variable sw am Ende der Prozedur ihre Gültigkeit. Das StreamWriter-Objekt wird aber erst bei der nächsten garbage collection tatsächlich aus dem Speicher entfernt (und Sie wissen nicht, wann diese stattfindet). Bis dahin gilt die Datei als offen. In ReadTextFile würde daher beim Versuch, die Datei zu öffnen, ein Fehler auftreten!
10.5 Textdateien lesen und schreiben
431
Sub ReadTextFile() ' Textdatei c:\test\test1.txt lesen Dim sr As New IO.StreamReader("c:\test\test1.txt", enc) MsgBox(sr.ReadToEnd) sr.Close() End Sub Sub DeleteTextFile() ' Verzeichnis c:\test löschen If MsgBox("Soll das Verzeichnis c:\test vollständig gelöscht " & _ "werden?", MsgBoxStyle.YesNo) = MsgBoxResult.Yes Then IO.Directory.Delete("c:\test", True) End If End Sub
10.5.5 Beispiel – Textcodierung ändern Sie können die StreamReader- und StreamWriter-Klasse auch dazu verwenden, um die Codierung einer Textdatei zu ändern. Dazu verwenden Sie ein StreamReader-Objekt, um die Datei zu lesen, und ein StreamWriter-Objekt, um die Datei unter einem anderen Namen zu speichern. Die folgende Prozedur demonstriert die Vorgehensweise anhand einer Konvertierung von UTF-8 (enc1) nach ANSI (enc2). Beachten Sie bitte, dass dabei Unicode-Zeichen, die im ANSI-Zeichensatz nicht vorgesehen sind, durch Fragezeichen ersetzt werden! ' Beispiel dateien\textdateien Sub ConvertTextFile() Dim enc1 As Text.Encoding = Text.Encoding.UTF8 Dim enc2 As Text.Encoding = Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader("c:\test\test1.txt", enc1) Dim sw As New IO.StreamWriter("c:\test\test2.txt", False, enc2) sw.Write(sr.ReadToEnd()) sw.Close() sr.Close() End Sub
Die Anweisung sw.Write(sr.ReadToEnd()) ist zwar für den Programmierer praktisch, aber in der Ausführung nicht unbedingt optimal: Die zu konvertierende Datei wird dadurch vollständig in den Arbeitsspeicher geladen. Das funktioniert nur bei kleinen Dateien gut. Bei großen Dateien muss die Datei dagegen stückweise gelesen und geschrieben werden. Die folgenden Zeilen zeigen dafür einen entsprechenden Algorithmus, der ein Char-Feld als Pufferspeicher verwendet. Mit Read werden bis zu 65536 Zeichen in dieses Feld gelesen. Mit Write werden anschließend die tatsächlich gelesenen Zeichen (Variable n) gespeichert.
432
10 Dateien und Verzeichnisse
Sub ConvertTextFile() Dim enc1, enc2, sr, sw ... wie oben Const buffersize As Integer = 65536 Dim buffer(buffersize) As Char Dim n As Integer Do n = sr.Read(buffer, 0, buffersize) sw.Write(buffer, 0, n) Loop Until n < buffersize sw.Close() sr.Close() End Sub
10.5.6 Zeichenketten lesen und schreiben (StringReader und StringWriter) Zu den in den vorigen Abschnitten vorgestellten Klassen StreamReader und -Writer gibt es zwei analoge Klassen StringReader und -Writer. Der wesentliche Unterschied besteht darin, dass Sie damit nicht Dateien, sondern Zeichenketten lesen oder schreiben können. Beispielsweise können Sie diese Klassen dazu verwenden, um Daten, die Sie sonst in einer temporären Datei zwischenspeichern würden, in einer String-Variablen zwischenzuspeichern. Der offensichtliche Vorteil besteht darin, dass dazu kein Festplattenzugriff erforderlich ist – die Vorgehensweise ist daher (zumindest bei kleinen Datenmengen) deutlich effizienter. Gleichzeitig sind nur minimale Änderungen am Code erforderlich, weil die meisten Eigenschaften und Methoden äquivalent sind.
StringReader Mit den folgenden zwei Zeilen wird zuerst eine Zeichenkette initialisiert und auf deren Basis dann das StringReader-Objekt strr erzeugt. Aus diesem Objekt können Sie nun wie bei einem StreamReader-Objekt mit den Methoden Read, ReadLine oder ReadToEnd einzelne Zeichen, Zeilen oder den gesamten Inhalt lesen. Beachten Sie, dass das StringReader-Objekt statisch ist. Eine nachträgliche Veränderung der Variablen s hat keinerlei Einfluss mehr auf dieses Objekt! Dim s As String = "abcdefg" + vbCrLf + "zweite Zeile" + vbCrLf Dim strr As New IO.StringReader(s) Console.WriteLine(strr.ReadLine())
StringWriter Einem StringWriter-Objekt liegt intern ein (bereits in Abschnitt 8.2.6 vorgestelltes) StringBuilder-Objekt zugrunde. Insofern bietet die Klasse StringWriter einfach eine weitere Möglichkeit, Zeichenketten sehr viel effizienter als durch s = s + "abc" zusammenzusetzen. Dazu verwenden Sie die Methoden Write oder WriteLine.
10.5 Textdateien lesen und schreiben
433
Dim strw As New IO.StringWriter() strw.WriteLine("erste Zeile") strw.WriteLine("zweite Zeile")
Sie können vom StringWriter-Objekt jederzeit mit den Methoden GetStringBuilder oder ToString ein StringBuilder-Objekt oder eine gewöhnliche Zeichenkette ableiten. Console.Write(strw.ToString)
Wie bei einem StreamWriter-Objekt können Sie das Schreiben mit Close abschließen. Das bedeutet, das weitere Veränderungen nicht mehr möglich sind. Der Inhalt der StringWriterObjekts bleibt aber erhalten, d.h., GetStringBuilder und ToString funktionieren weiterhin. Wenn Sie den Inhalt des StringWriter-Objekts dagegen löschen möchten, müssen Sie die Methode Dispose ausführen.
10.5.7 Syntaxzusammenfassung Codierungs-Objekt erzeugen System.Text.Encoding-Objekt erzeugen Dim enc As Text.Encoding enc = Text.Encoding.ASCII enc = Text.Encoding.UTF7 enc = Text.Encoding.UTF8 enc = Text.Encoding.Unicode enc = Text.Encoding.GetEncoding(n)
erzeugt ein Text.Encoding-Objekt für den angegebenen Zeichensatz.
Textdateien lesen System.IO.StreamReader-Objekt erzeugen New IO.StreamReader("name.txt")
öffnet die Textdatei.
New IO.StreamReader("name.txt", enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
IO.File.OpenText("name.txt")
öffnet die Textdatei.
New IO.FileInfo("name.txt").OpenText()
öffnet ebenfalls die Textdatei.
System.IO.StreamReader-Klasse – Textdatei lesen Close()
schließt die Datei.
CurrentEncoding
liefert ein Text.Encoding-Objekt, das die Codierung angibt.
434
10 Dateien und Verzeichnisse
System.IO.StreamReader-Klasse – Textdatei lesen Peek()
liefert das nächste Zeichen und liefert dessen Code oder -1, ohne den Dateizeiger weiter zu bewegen.
Read()
liest das nächste Zeichen und liefert dessen Code oder -1.
Read(c, 0, n)
liest maximal n Zeichen in das Char-Feld c.
ReadLine()
liefert die nächste Zeile (oder Nothing, wenn das Dateiende erreicht ist).
ReadToEnd()
liefert den gesamten Text bis zum Ende der Datei.
Textdateien schreiben System.IO.StreamWriter-Objekt erzeugen New IO.StreamWriter("name.txt")
erzeugt eine neue Textdatei.
New IO.StreamWriter("name.txt", False, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
New IO.FileInfo("name.txt").CreateText()
erzeugt eine neue Textdatei.
New IO.StreamWriter(io_stream_object)
leitet das Objekt von IO.Stream ab.
New IO.StreamWriter("name.txt", True)
ergänzt eine vorhandene Textdatei.
New IO.StreamWriter("name.txt", True, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
IO.File.AppendText("name.txt")
ergänzt eine vorhandene Textdatei.
New IO.FileInfo("name.txt").AppendText()
ergänzt eine vorhandene Textdatei.
System.IO.StreamWriter-Klasse – Textdateien schreiben Close()
schließt die Datei.
Flush()
speichert alle Änderungen auf der Festplatte.
NewLine
gibt die Zeichen zum Zeilenwechsel an bzw. verändert sie.
Write[Line](x)
speichert den Inhalt von x im Textformat in der Datei.
Write[Line](c, pos, n)
speichert n Zeichen beginnend bei der Position pos aus dem Char-Feld c in der Datei.
10.6 Binärdateien lesen und schreiben
10.6
435
Binärdateien lesen und schreiben
Die im vorigen Abschnitt besprochenen Textdateien haben den Vorteil, dass sie (einen geeigneten Editor vorausgesetzt) problemlos gelesen werden können. Der Inhalt einer Datei kann daher auch ohne ein Spezialprogramm leicht festgestellt werden. Bei Binärdateien werden Daten dagegen in ihrer internen Darstellung gespeichert. Beispielsweise wird eine Double-Fließkommazahl nicht mehr in der Form "123.456789012345" gespeichert, sondern durch die hexadezimalen Codes 69 4C FB 07 3C DD 5E 40. Das hat Vor- und Nachteile: Auf der einen Seite sind Binärdateien meist deutlich kleiner. Zudem ist das Lesen und Schreiben von Daten effizienter, weil keine Umwandlungen zwischen unterschiedlichen Darstellungsformaten erforderlich sind. Auf der anderen Seite können derartige Dateien nur noch mit einem Programm gelesen werden, das den exakten internen Aufbau der Datei kennt. Für den Austausch von Daten zwischen unterschiedlichen Programmen ist das oft nicht förderlich. Zum Lesen und Schreiben von Binärdateien bietet der System.IO-Namensraum zwei prinzipielle Möglichkeiten: •
Die FileStream-Klasse ermöglicht einen Dateizugriff auf unterster Ebene. Mit davon abgeleiteten Objekten können Sie einzelne Bytes (oder ganze Gruppen von Bytes) lesen und schreiben. Für die Interpretation der Daten sind Sie selbst verantwortlich. Die FileStream-Klasse bietet eine fast vollständige Kontrolle über den Lese- bzw. Schreibprozess: Sie können die Lese- bzw. Schreibposition innerhalb der Datei jederzeit verändern und so die Daten in beliebiger Reihenfolge lesen oder schreiben, Sie können in derselben Datei lesen und schreiben, Sie können zwischen synchronem und asynchronem Zugriff wählen (siehe Abschnitt 10.7) etc.
•
Die beiden Klassen BinaryReader und -Writer ermöglichen es, die meisten .NET-Basisdatentypen unmittelbar zu lesen und schreiben. Beispielsweise können Sie damit eine Double-Variable speichern, ohne sich Gedanken über die interne Darstellung dieser Variablen zu machen. Dieser Vorteil wird allerdings durch andere Einschränkungen gegenüber der FileStream-Klasse erkauft.
Daneben beschreibt dieser Abschnitt noch zwei weitere Klassen, die mit der FileStreamKlasse verwandt sind: •
MemoryStream bietet im Wesentlichen dieselben Methoden und Eigenschaften wie FileStream, der Datenstrom wird aber im Arbeitsspeicher verwaltet. MemoryStream eignet
sich daher vor allem als Ersatz für temporäre Dateien. (Es ist möglich, ein vorhandenes MemoryStream-Objekt zu einem späteren Zeitpunkt mit einem FileStream-Objekt zu
speichern.) •
BufferedStream ist eine Hilfsklasse für alle von IO.Stream abgeleiteten Klassen. Die Klasse
hilft dabei, wiederholte Lese- bzw. Schreibzugriffe durch die Verwendung eines (größeren) Zwischenspeichers zu optimieren.
436
10 Dateien und Verzeichnisse
10.6.1 FileStream Als Stream wird in der Informatik generell ein Fluss von Daten bezeichnet, ganz unabhängig davon, woher oder wohin die Daten fließen. Es verwundert deswegen nicht, dass es in der .NET-Bibliothek ein allgemeines IO.Stream-Objekt gibt, von dem dann FileStream abgeleitet ist, um den Datenfluss aus oder in Dateien zu steuern. Von Stream ist aber z.B. auch Net.Sockets.NetworkStream abgeleitet (Datenfluss im Netzwerk).
FileStream-Objekt erzeugen Der New-Konstruktor sieht gleich eine ganze Reihe von Syntaxvarianten vor, um ein neues FileStream-Objekt zu erzeugen: Dim fs As IO.FileStream fs = New IO.FileStream("filename", fs = New IO.FileStream("filename", fs = New IO.FileStream("filename", fs = New IO.FileStream("filename",
modus) modus, access) modus, access, sharing) modus, access, sharing, buffersize)
modus ist ein Element der IO.FileMode-Aufzählung. Der Parameter gibt an, ob eine vorhandene Datei geöffnet oder eine neue Datei erzeugt werden soll (z.B. IO.FileMode.Create, Open, OpenOrCreate, Append etc.). access ist ein Element der IO.FileAccess-Aufzählung. Der Parameter bestimmt, ob die Datei für den Lese- und/oder Schreibzugriff geöffnet werden soll (IO.FileAccess.Read, Write oder ReadWrite). Per Default wird die Datei zum Lesen und Schreiben geöffnet. Wenn Sie die Datei nicht verändern brauchen, sollten Sie unbedingt Read angeben, um Zugriffskonflikte
mit anderen Programmen möglichst zu vermeiden. sharing ist ein Element der IO.FileShare-Aufzählung. Der Parameter bestimmt, ob und wie
andere Programme auf dieselbe Datei zugreifen dürfen (während die Datei von Ihrem Programm noch geöffnet ist): Die Defaulteinstellung IO.FileShare.None blockiert die Datei für jeden anderen Zugriff. IO.FileShare.Read erlaubt anderen Prozessen, die Datei zu lesen, sie aber nicht zu verändern. IO.FileShare.Write bzw. ReadWrite erlauben auch einen schreibenden Zugriff. (Experimente mit diesem Parameter haben allerdings gezeigt, dass dieser nicht immer so funktioniert, wie er dokumentiert ist. Weitere Informationen folgen gleich unter der Überschrift Sharing-Probleme.) buffersize gibt schließlich die Größe des Puffers an, der Dateizugriffe zwischenspeichert. Die
VERWEIS
Defaultgröße des Puffers ist nicht dokumentiert und möglicherweise auch betriebssystemabhängig. Die Vorgabe einer eigenen Puffergröße ist nur in Ausnahmefällen sinnvoll. FileStream-Objekte können auch mit den Methoden Create, Open, OpenRead und OpenWrite der Objekte IO.File und IO.FileInfo erzeugt werden. Eine Syntaxzusammenfas-
sung dieser Methoden finden Sie in Abschnitt 10.3.8.
10.6 Binärdateien lesen und schreiben
437
Daten lesen und schreiben Es gibt nur zwei Methoden, um Daten aus einem FileStream zu lesen: ReadByte() liest ein einzelnes Byte. Die Methode liefert als Ergebnis allerdings einen Integer-Wert – entweder den Wert des Datenbytes oder -1, wenn das Ende der Datei erreicht ist. Die Methode Read ermöglicht es, mehrere Bytes auf einmal in eine beliebige Position innerhalb eines ByteFelds zu lesen. Diese Methode liefert die Anzahl der gelesenen Bytes zurück. (Sie erkennen das Dateiende daran, dass weniger Bytes gelesen als angefordert wurden.) Auch die Methoden zum Schreiben von Daten sind spartanisch: WriteByte schreibt ein einzelnes Byte. Write speichert eine angegebene Anzahl von Bytes aus einem Byte-Feld. Mit beiden Write-Methoden können auch vorhandene Daten einer Datei überschrieben werden.
Dateizeiger verändern Zu den besonderen Eigenschaften der FileStream-Klasse zählt die Möglichkeit, die Position des Dateizeigers zu verändern. (Der Dateizeiger zeigt auf das nächste Byte, das gelesen bzw. geschrieben oder überschrieben wird.) Die aktuelle Position kann mit Position ermittelt werden. (Die Zählung beginnt wie üblich bei 0. Diese Position nimmt der Dateizeiger normalerweise auch unmittelbar nach dem Öffnen einer Datei ein. Die einzige Ausnahme besteht dann, wenn die Datei mit modus = IO.FileMode.Append geöffnet wurde. In diesem Fall zeigt der Dateizeiger auf das Ende der Datei.) Mit Seek kann die Position verändert werden. An die Methode werden zwei Parameter übergeben: Der erste Parameter gibt an, um wie viele Bytes der Zeiger verstellt werden soll. Dieser Wert darf auch negativ sein. Der zweite Parameter muss ein Element der IO.SeekOrigin-Aufzählung sein. Der Wert bestimmt die Startposition für die Bewegung. Die drei Möglichkeiten sind: Dateianfang, Dateiende oder die aktuelle Position. fs.Seek(0, IO.SeekOrigin.Begin) fs.Seek(3, IO.SeekOrigin.Current) fs.Seek(-3, IO.SeekOrigin.End)
'Dateizeiger zurück an den Beginn 'Dateizeiger drei Bytes vorbewegen 'Dateizeiger drei Bytes vor 'das Dateiende bewegen
Die Länge der Datei (d.h. die größtmögliche Position des Dateizeigers) kann mit Length ermittelt werden.
Änderungen ausführen, Datei schließen Wenn Sie Daten mit Write[Byte] speichern bzw. verändern, werden diese Änderungen aus Effizienzgründen nicht sofort physikalisch auf der Festplatte durchgeführt. Sie können eine sofortige Speicherung aber jederzeit mit der Methode Flush erreichen. Wie bei den anderen in diesem Kapitel vorgestellten IO-Objekten zum Dateizugriff sollten Sie auch bei FileStream darauf achten, das Objekt so rasch wie möglich mit Close zu schließen. Damit können andere Benutzer oder Programme wieder auf die Datei zugreifen. (Verlassen Sie sich nicht darauf, dass die Datei automatisch geschlossen wird, wenn das Objekt – z.B. am Ende einer Prozedur – nicht mehr gültig ist. Aufgrund der neuen
438
10 Dateien und Verzeichnisse
Speicherverwaltung von VB.NET wird ein Objekt nicht sofort gelöscht, sondern zu einem unbestimmten Zeitpunkt!)
Beispiel – Binärdatei lesen und schreiben Das folgende Beispielprogramm ermittelt mit IO.Path.GetTempFileName einen temporären Dateinamen, erzeugt diese Datei und speichert dort zehn Bytes. Abbildung 10.10 zeigt den Inhalt der Datei im Hex-Editor XVI32. Der Dateizeiger wird nun zum ersten Mal zurück an den Beginn gestellt, um die Daten mit einer Schleife Byte für Byte auszulesen. Anschließend wird der Dateizeiger ein zweites Mal zurückgestellt, um die Daten nun auf einmal in ein Byte-Feld zu übertragen.
Abbildung 10.10: Der Inhalt der binären Testdatei
' Beispiel dateien\filestream Sub main() Dim i As Byte Dim data As Integer Dim bytes() As Byte Dim filename As String = IO.Path.GetTempFileName Dim fs As New IO.FileStream(filename, IO.FileMode.Create) ' 10 Bytes in der Datei speichern For i = 0 To 9 fs.WriteByte(i) Next ' Dateizeiger zurück an den Anfang der Datei ' Daten Byte für Byte lesen, bis das Ende der Datei erreicht wird fs.Seek(0, IO.SeekOrigin.Begin)
10.6 Binärdateien lesen und schreiben
439
Do data = fs.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine() ' Dateizeiger nochmals zurück an den Anfang der Datei ' alle Daten als Byte-Block lesen fs.Seek(0, IO.SeekOrigin.Begin) ReDim bytes(CInt(fs.Length - 1)) fs.Read(bytes, 0, CInt(fs.Length)) For Each i In bytes Console.Write(i & " ") Next Console.WriteLine() ' Datei schließen, Programmende fs.Close() Console.WriteLine("Drücken Sie Return") Console.ReadLine() Debug.WriteLine(filename) End Sub
Sharing-Probleme (gemeinsamer Zugriff auf Dateien) Wenn man der Dokumentation zu IO.FileSharing-Aufzählung folgt, sollte es eigentlich kein Problem sein, mit zwei FileStream-Objekten auf dieselbe Datei zuzugreifen. (Das wird in der Praxis selten erforderlich sein. Auf diese Weise kann aber rasch mit nur einem Programm getestet werden, ob der gemeinsame Zugriff mehrerer Programme oder Benutzer auf eine Datei prinzipiell funktioniert.) Mit den folgenden Zeilen sollte mit fs1 eine temporäre Datei erzeugt und beschrieben werden. Mit fs2 sollte dann die Datei gelesen werden (ohne vorher fs1 zu schließen.) Das Experiment scheiterte allerdings schon in Zeile fs2 = New IO.FileStream(...) mit einer Fehlermeldung (IO.IOException) wegen eines Konflikts beim Dateizugriff. Dim i As Byte Dim fs1, fs2 As IO.FileStream Dim filename As String = IO.Path.GetTempFileName ' fs1 erzeugt die Datei und schreibt zehn Bytes fs1 = New IO.FileStream(filename, IO.FileMode.Create, _ IO.FileAccess.Write, IO.FileShare.Read) For i = 0 To 9 : fs1.WriteByte(i) : Next
440
10 Dateien und Verzeichnisse
' nun mit fs2 dieselbe Datei lesen fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read, IO.FileShare.Read) ...
Mit einem zweiten Test wird zuerst eine Datei mit dem FileStream-Objekt fs1 erzeugt. fs1 wird anschließend geschlossen. Danach versuchen zwei weitere FileStream-Objekte fs2 und fs3 gleichzeitig, diese Datei zu lesen. Das klappt – allerdings nur dann, wenn beide Objekte mit access = IO.FileAccess.Read geöffnet wurden. Dim filename As String = IO.Path.GetTempFileName Dim fs1, fs2, fs3 As IO.FileStream Dim i As Byte Dim data As Integer ' fs1 erzeugt die Datei und schreibt zehn Bytes; ' die Datei wird danach geschlossen fs1 = New IO.FileStream(filename, IO.FileMode.Create) For i = 0 To 9 : fs1.WriteByte(i) : Next fs1.Close() ' fs2 öffnet die Datei im Lesezugriff fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) Do data = fs2.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine() ' fs3 öffnet die Datei ebenfalls fs3 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) Do data = fs3.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine()
Fazit: Auch wenn die Dokumentation zu IO.FileShare.Write bzw. ReadWrite vermuten lässt, dass ein gleichzeitiger Schreibzugriff durch zwei Prozesse möglich ist, ist dies in der Praxis nicht (zumindest nicht immer) der Fall. Ein gleichzeitiger Lesezugriff ist möglich, erfordert aber nicht die Angabe eines sharing-Parameters.
10.6 Binärdateien lesen und schreiben
441
Lock und Unlock Die FileStream-Klasse sieht die Methoden Lock und Unlock vor, um einen Bytebereich einer Datei vorübergehend für jeden Zugriff durch andere Prozesse zu blockieren. Nachdem ein gemeinsamer Schreibzugriff nicht gelungen ist (siehe oben), wurden die beiden Methoden nur im Hinblick auf einen gemeinsamen Lesezugriff getestet. Dabei hat sich gezeigt, dass der Zugriff mit den Methoden zwar tatsächlich gesteuert werden kann, dass die durch Lock ausgelöste Blockierung aber für die gesamte Datei oder zumindest für einen größeren Bereich der Datei gilt (eventuell für die Puffergröße), nicht nur für den angegebenen Bytebereich. fs1 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) fs1.Lock(5, 2) 'blockiert zwei Bytes ab der Position 5 ' das folgende Kommando löst einen Zugriffsfehler aus, ' obwohl die ersten Bytes eigentlich nicht blockiert sind fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read)
Fazit: Die Parameter von Lock und Unlock gaukeln eine Genauigkeit bei der Blockierung von Dateien vor, die in der Praxis nicht erzielbar ist. Stattdessen gilt das Motto: Alles oder nichts.
10.6.2 BufferedStream (FileStream beschleunigen) Die BufferedStream-Klasse hilft, die Effizienz beim Umgang mit anderen IO.Stream-Objekten in bestimmten Fällen zu steigern. Dazu werden Schreibvorgänge zuerst im Arbeitsspeicher zwischengespeichert, bevor sie tatsächlich durchgeführt werden. Bei Lesevorgängen werden entsprechend schon im Voraus große Datenmengen gelesen, die dann sofort zur Verfügung stehen. Die Anwendung eines BufferedStream empfiehlt sich laut Dokumentation dann, wenn sehr viele Lese- oder Schreibvorgänge hintereinander durchgeführt werden, es aber nur selten zu einem Wechsel zwischen Lese- und Schreibvorgängen kommt. Zum Erzeugen eines BufferedStream-Objekts muss ein anderes, bereits existierendes IO.Stream-Objekt (oder ein Objekt einer davon abgeleiteten Klasse) im New-Konstruktor angegeben werden. Diese Vorgehensweise bietet sich natürlich besonders bei FileStreamObjekten an. (Bei einem MemoryStream können Sie durch einen Zwischenpuffer nichts beschleunigen, weil sich die Daten ohnedies schon im Arbeitsspeicher befinden.) Anschließend wenden Sie die Read- oder Write-Methoden auf das BufferedStream-Objekt anstatt auf Ihr ursprüngliches Objekt an. Die Verwendung des BufferedStream-Objekts erfordert also fast keine Veränderung an bereits bestehendem FileStream-Code. Dim fs As New IO.FileStream(...) Dim bs As New IO.BufferedStream(fs) bs.WriteByte(...) ... fs.Close() 'schließt sowohl fs als auch bs
442
10 Dateien und Verzeichnisse
Beispiel Natürlich werden auch bei normalen FileStream-Objekten Lese- und Schreibvorgänge zwischengespeichert. (Es wäre unglaublich langsam, wenn bei jedem einzelnen Byte, das gelesen oder geschrieben wird, ein Festplattenzugriff erforderlich wäre.) Insofern hatte ich ursprünglich gewisse Zweifel, ob die Verwendung eines BufferedStream-Objektes tatsächlich eine Geschwindigkeitssteigerung bringen würde. Das folgende Testprogramm (siehe Abbildung 10.11) beweist aber, dass tatsächlich eine Zugriffsoptimierung stattfindet. Das Programm erzeugt zwei ca. 10 MByte große temporäre Dateien. Die erste Datei wird auf der Basis eines FileStream-Objekts erstellt, die zweite unter Zuhilfenahme eines BufferedStream-Objekts. In beiden Fällen werden die Daten Byte für Byte mit WriteByte gespeichert. Bei diesem einfachen Test ist die BufferedStream-Variante um ca. 25 Prozent schneller.
Abbildung 10.11: Geschwindigkeitsvergleich zwischen FileStream und BufferedStream
' Beispiel dateien\bufferedstream Sub main() Dim starttime As Date Dim time1, time2 As TimeSpan Const nr_of_bytes As Integer = 10000000 Dim i As Integer ' Daten ohne BufferedStream speichern Dim fs1 As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) starttime = Now For i = 0 To nr_of_bytes - 1 fs1.WriteByte(CByte(i Mod 16)) Next fs1.Close() time1 = Now.Subtract(starttime) ' Daten mit BufferedStream speichern Dim fs2 As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) starttime = Now Dim bs As New IO.BufferedStream(fs2) For i = 0 To nr_of_bytes - 1 bs.WriteByte(CByte(i Mod 16)) Next
10.6 Binärdateien lesen und schreiben
443
fs2.Close() ' damit ist auch bs geschlossen! time2 = Now.Subtract(starttime) ' Ergebnis anzeigen, temporäre Dateien löschen Console.WriteLine("FileStream: {0} Sekunden", time1) Console.WriteLine("BufferedStream: {0} Sekunden", time2) IO.File.Delete(fs1.Name) IO.File.Delete(fs2.Name) Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub
10.6.3 MemoryStream (Streams im Arbeitsspeicher) MemoryStream verwaltet einen Stream im Arbeitsspeicher. Da die Klasse ebenfalls von IO.Stream abgeleitet ist, stehen die meisten von FileStream bekannten Eigenschaften und Methoden auch für MemoryStream-Objekte zur Verfügung.
Im einfachsten Fall, d.h., wenn das Objekt mit New IO.MemoryStream() erzeugt wurde, erfolgt die Speicherverwaltung automatisch. Die zahlreichen New-Konstruktor-Varianten ermöglichen es aber auch, in der Größe oder im Inhalt unveränderliche Streams auf der Basis von Byte-Feldern zu erzeugen. Dim ms As New IO.MemoryStream() ms.WriteByte(...)
Mit WriteTo können Sie den Inhalt eines MemoryStream-Objekts in ein anderes Stream-Objekt übertragen. Wenn Sie dabei als Parameter ein FileStream-Objekt angeben, können Sie ein MemoryStream-Objekt in einer Datei speichern. Mit den folgenden Zeilen wird zuerst ein MemoryStream-Objekt erzeugt, um darin zehn Bytes zu speichern. Anschließend wird der Inhalt des Objekts in einer temporären Datei gespeichert. Dim i As Integer Dim ms As New IO.MemoryStream() For i = 1 To 10 ms.WriteByte(CByte(i)) Next Dim fs As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) ms.WriteTo(fs)
Wenn Sie das gesamte MemoryStream-Objekt in einer anderen Form weiterbearbeiten möchten, können Sie es mit der Methode ToArray in ein Byte-Feld kopieren.
444
10 Dateien und Verzeichnisse
10.6.4 BinaryReader und -Writer (Variablen binär speichern) Mit Objekten der beiden Binary-Klassen können Sie binäre Daten lesen oder schreiben. Der wesentliche Vorteil gegenüber FileStream besteht in den reichhaltigen Read- und WriteMethoden: Mit BinaryWriter.Write können Sie die meisten elementaren .NET-Datentypen direkt in einer Datei speichern (darunter Byte, Short, Integer, Long, Single, Double, Decimal sowie Char und String). Anders als bei FileStream brauchen Sie sich also nicht mehr mit einzelnen Bytes zu plagen, sondern können ganze Variablen unmittelbar speichern bzw. lesen.
TIPP
BinaryReader bietet eine ganze Kollektion entsprechender ReadXxx-Methoden, z.B. ReadByte, ReadInt16, ReadInt32 etc. Falls während des Leseversuchs das Ende der Datei erreicht wird, kommt es zu einem EndOfStream-Fehler.
Aus schwer nachvollziehbaren Gründen wird der elementare Datentyp Date von den Binary-Objekten nicht unterstützt. Die einfachste Abhilfe besteht darin, d.Ticks zu speichern (das ist ein Long-Wert) und später ReadInt64 zu verwenden, um die Daten wieder einzulesen. Das Beispielprogramm demonstriert diese Vorgehensweise.
Die BinaryReader- bzw. BinaryWriter-Klassen sind zwar in der Objekthierarchie nicht von der Klasse IO.FileStream abgeleitet, verwenden aber intern ein derartiges Objekt. Aus diesem Grund muss vor der Erzeugung eines BinaryReader- oder BinaryWriter-Objekts zuerst ein FileStream-Objekt zur Verfügung stehen. Dim fs As New IO.FileStream("name.bin", IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs) bw.Write(x) ...
Im Gegensatz zu FileStream-Objekten können Sie mit BinaryXxx-Objekten nicht wechselweise Daten lesen und schreiben. Eine weitere Einschränkung gilt nur für BinaryReader: Es ist nicht möglich, die Leseposition (den Dateizeiger) durch Seek zu verändern.
Umgang mit Zeichenketten BinaryReader und -Writer berücksichtigen beim Lesen bzw. Schreiben von Char- und StringVariablen die Codierung, die beim Erzeugen des BinaryWriter-Objekts angegebenen wurde. Wenn die Codierung nicht angegeben wird, verwendet BinaryWriter per Default
UTF8. Das bedeutet, dass je nach Zeichen unterschiedlich viele Bytes zur Codierung verwendet werden!
10.6 Binärdateien lesen und schreiben
445
Dim enc As Text.Encoding = New Text.UnicodeEncoding() Dim fs As New IO.FileStream(filename, IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs, enc) BinaryWriter speichert zudem bei String-Variablen die Anzahl der Zeichen. Das hilft BinaryReader später, die richtige Länge der Zeichenkette zu erkennen. Die Zeichenanzahl wird
dabei auf eine besondere Weise codiert: Werte bis zu 127 werden in einem einzigen Byte gespeichert. Größere Werte werden auf mehrere Bytes verteilt, wobei in jedem Byte nur sieben Bits für die Codierung der Zahl genutzt werden. Das achte Bit gibt an, ob im nächsten Byte weitere Bits folgen. Auf diese Weise können beliebig große ganze Zahlen gespeichert werden, ohne jedes Mal vier Bytes zu beanspruchen.
Beispielprogramm Das folgende Beispielprogramm erzeugt mit IO.Path.GetTempFileName einen temporären Dateinamen. Dieser Name wird zuerst dazu benutzt, um eine neue Datei zu erzeugen und darin eine Double-Zahl, zwei Zeichenketten und die aktuelle Zeit zu speichern. Anschließend werden diese Daten wieder aus der Datei extrahiert und mit MsgBox zur Kontrolle angezeigt. Abbildung 10.12 zeigt den Inhalt der temporären Datei im Hex-Editor XVI32. Es ist offensichtlich, dass der Inhalt dieser Datei nur noch für den Computer verständlich ist.
Abbildung 10.12: Der Inhalt der temporären Datei, dargestellt in einem Hex-Editor
' Beispiel dateien\binaryreader Sub Main() Dim filename As String = IO.Path.GetTempFileName WriteBinaryData(filename) ReadBinaryData(filename) End Sub
446
10 Dateien und Verzeichnisse
Sub WriteBinaryData(ByVal filename As String) Dim x As Double = 1 / 7 Dim s1 As String = "abcäöü€" Dim s2 As String = "blabla" Dim d As Date = Now Dim fs As New IO.FileStream(filename, IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs) bw.Write(x) bw.Write(s1) bw.Write(s2) bw.Write(d.Ticks) bw.Close() End Sub Sub ReadBinaryData(ByVal filename As String) Dim x As Double, s1, s2 As String Dim fs As New IO.FileStream(filename, IO.FileMode.Open) Dim br As New IO.BinaryReader(fs) Dim d As Date x = br.ReadDouble() s1 = br.ReadString() s2 = br.ReadString() d = New Date(br.ReadInt64()) MsgBox("x=" & x & vbCrLf & _ "s1=" & s1 & vbCrLf & _ "s2=" & s2 & vbCrLf & _ "d=" & d) br.Close() End Sub
10.6.5 Syntaxzusammenfassung FileStream System.IO.FileStream-Objekt erzeugen New IO.FileStream(filename, modus [,access [,sharing [,buffersize]]])
erzeugt ein FileStream-Objekt. modus (IO.FileMode-Aufzählung) gibt an, ob die Datei
neu erzeugt werden soll. access (IO.FileAccess-Aufzählung) gibt an, ob die Daten
gelesen oder geschrieben werden sollen. sharing (IO.FileShare-Aufzählung) gibt an, ob andere
Prozesse Zugriff auf die Daten haben.
10.6 Binärdateien lesen und schreiben
447
System.IO.FileStream-Klasse – Binärdateien lesen und schreiben Close()
schließt die Datei.
Flush()
speichert alle offenen Änderungen in der Datei.
Length
liefert die Länge der Datei.
Lock(pos, n)
blockiert n Bytes ab Position n für jeden Zugriff durch andere Prozesse.
Name
liefert den Namen der Datei.
Position
liefert die Position des Dateizeigers (0, wenn der Zeiger auf das erste Byte zeigt).
Read(b, offset, n)
liest n Bytes in das Byte-Feld b. Die Daten werden beginnend mit b(offset) in das Feld kopiert.
ReadByte()
liest ein Byte und liefert einen entsprechenden IntegerWert oder -1, wenn das Dateiende erreicht ist.
Seek(pos, start)
verändert die Position des Dateizeigers um pos Bytes. start (IO.SeekOrigin-Aufzählung) gibt die Startposition an.
Unlock(pos, n)
gibt n Bytes ab Position n für den Zugriff durch andere Prozesse wieder frei.
Write(b, offset, n)
speichert n Bytes, die aus dem Byte-Feld b beginnend mit b(offset) gelesen werden.
WriteByte(b)
speichert den Byte-Wert b.
System.IO.SeekOrigin-Aufzählung – Startposition für Seek() Begin
legt die Position relativ zum Dateianfang fest.
Current
ändert die Position relativ zur aktuellen Position.
End
legt die Position relativ zum Dateiende fest.
System.IO.FileMode-Aufzählung – Modus beim Öffnen von Dateien Append
öffnet die Datei, um an ihrem Ende Daten anzufügen. Es können keine Daten gelesen werden. Die Datei muss bereits existieren.
Create
erzeugt bzw. überschreibt die Datei.
CreateNew
erzeugt eine neue Datei. Die Datei darf noch nicht existieren.
Open
öffnet die angegebene Datei. Die Datei muss bereits existieren.
OpenOrCreate
öffnet die angegebene Datei oder erzeugt sie neu.
Truncate
öffnet die angegebene Datei und löscht ihren Inhalt. Die Datei muss bereits existieren.
448
10 Dateien und Verzeichnisse
System.IO.FileAccess-Aufzählung – Zugriffmodus auf Dateien Read
öffnet die Datei nur zum Lesen.
ReadWrite
öffnet die Datei zum Lesen und zum Schreiben.
Write
öffnet die Datei nur zum Schreiben.
System.IO.FileShare-Aufzählung – Sharing-Modus beim Dateizugriff None
verwehrt allen anderen Prozessen den Zugriff auf die Datei, bis diese geschlossen wird (Default bei Schreibzugriffen).
Read
erlaubt anderen Prozessen, die Datei gleichzeitig zu lesen (Default bei Lesezugriffen).
ReadWrite
erlaubt anderen Prozessen, die Datei gleichzeitig zu lesen und zu verändern.
Write
erlaubt anderen Prozessen, die Datei gleichzeitig zu verändern.
MemoryStream System.IO.MemoryStream-Klasse – Besondere Eigenschaften und Methoden Capacity
liefert die Größe des internen Pufferspeichers.
ToArray()
liefert den Inhalt des Streams als Byte-Feld.
WriteTo(stream)
speichert den Inhalt in einem anderen Stream-Objekt.
BinaryReader und -Writer System.IO.BinaryReader und System.IO.BinaryReader-Objekt erzeugen New IO.BinaryReader(filestream)
erzeugt aus einem FileStream-Objekt ein BinaryReaderObjekt.
New IO.StreamReader(filestream, enc) wie oben, aber unter Anwendung eines bestimmten Zeichensatzes (Text.Encoding). New IO.BinaryWriter(filestream)
erzeugt aus einem FileStream-Objekt ein BinaryWriterObjekt.
New IO.BinaryWriter(filestream, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes (Text.Encoding).
10.7 Asynchroner Zugriff auf Dateien
449
System.IO.BinaryReader-Klasse – Binärdateien lesen Close()
schließt die Datei.
PeekChar()
liest ein Zeichen, ohne den Dateizeiger zu verändern.
Read()
liest ein Zeichen und liefert einen Integer-Wert oder -1, wenn das Dateiende erreicht ist.
ReadByte(), ReadChar() etc.
liest eine Variable des angegebenen Typs (soviele Bytes, wie zur Speicherung des Datentyps erforderlich sind). Falls das Dateiende erreicht wird, tritt ein EndOfStream-Fehler auf.
System.IO.BinaryWriter-Klasse – Binärdateien schreiben Close()
schließt die Datei.
Flush()
speichert alle offenen Änderungen auf dem Datenträger.
Seek(n, origin)
verändert die Position des Dateizeigers.
Write(x)
speichert die in x enthaltenen Daten in binärer Form. x muss einer der elementaren .NET-Datentypen sein (außer Boolean und Date).
10.7
Asynchroner Zugriff auf Dateien
Normalerweise erfolgt der Zugriff auf Dateien synchron. Das bedeutet, dass das Programm nach der Anweisung fs.Read(...) erst dann fortgesetzt wird, nachdem die angeforderten Daten gelesen wurden. Ebenso wird das Programm nach fs.Write(...) erst fortgesetzt, nachdem die Daten tatsächlich gespeichert wurden. (Aus Geschwindigkeitsgründen werden Lese- und Schreibzugriffe intern gepuffert, so dass nicht jeder Schreibvorgang auch sofort physikalisch auf der Festplatte ausgeführt wird.) Alle bisherigen Beispiele sind stillschweigend davon ausgegangen, dass der Datenzugriff synchron erfolgt. Ein asynchroner Zugriff bedeutet dagegen, dass Sie nach der Aufforderung, bestimmte Daten zu lesen oder zu schreiben, sofort mit anderen Kommandos weiterarbeiten können. Die asynchrone Dateioperation erfolgt in einem eigenen Thread, der zu diesem Zweck automatisch geschaffen wird. Nach Abschluss des Lese- oder Schreibvorgangs kann eine so genannte Callback-Prozedur zur Verständigung aufgerufen werden. Asynchrone Dateioperationen machen auf der einen Seite die Programmierung deutlich komplizierter, weil Sie sich nicht mehr darauf verlassen können, dass die gelesenen Daten sofort zur Verfügung stehen bzw. dass geschriebene Daten tatsächlich erfolgreich gespeichert werden konnten. Auf der anderen Seite bieten sie aber die Möglichkeit, Programme effizienter zu machen. Wenn Sie bei einem Programm DATEI|SPEICHERN asynchron implementieren, kann der Benutzer sofort weiterarbeiten, auch wenn das Speichern vielleicht
450
10 Dateien und Verzeichnisse
einige Sekunden dauert. Wenn Sie bei einem Programm Daten asynchron lesen, können Sie in der Zeit, bis die Daten verfügbar sind, andere Arbeiten erledigen.
Voraussetzungen •
Grundsätzlich sind asynchrone Dateioperationen nur bei großen Dateien sinnvoll und auch nur dann, wenn große Datenmengen auf einmal gelesen oder geschrieben werden. (Sind diese Voraussetzungen nicht erfüllt, sind asynchrone Operationen wegen des größeren Verwaltungsaufwands oft wesentlich langsamer als synchrone!)
•
Je nach Betriebssystem werden asynchrone Dateioperationen womöglich gar nicht unterstützt. Laut Dokumentation wird New auch dann fehlerfrei ausgeführt, die unten beschriebenen Methoden BeginWrite bzw. BeginRead werden aber dennoch synchron ausgeführt. Ich habe asynchrone Dateioperationen unter Windows 2000 getestet, und dort haben sie gut funktioniert.
•
Asynchrone Operationen sind nur für binäre Datenströme vorgesehen (Klassen Stream, FileStream etc.). Die Methoden BeginWrite und BeginRead können nur Byte-Felder verarbeiten.
10.7.1 Programmiertechniken Asynchrones FileStream-Objekt erzeugen Bevor asynchrone Operationen überhaupt möglich sind, muss ein FileStream-Objekt im asynchronen Modus erzeugt werden. Der dazu vorgesehene New-Konstruktor verlangt ziemlich viele Parameter: Dim fs As New IO.FileStream(filename, createmode, accessmode, _ sharemode, buffersize, True)
Die drei Parameter createmode, accessmode und sharemode wurden bereits in Abschnitt 10.6.1 beschrieben. buffersize gibt die Größe des Puffers an, der beim Lesen bzw. Schreiben verwendet werden soll. Dieser Puffer muss mindestens 64 kByte betragen, andernfalls werden die Dateioperationen im synchronen Modus ausgeführt.
Daten asynchron schreiben und lesen Um nun Daten asynchron in der Datei zu speichern, verwenden Sie statt Write die Methode BeginWrite. Die zu schreibenden Daten müssen als eindimensionales Bytefeld übergeben werden. Die Parameter start und n geben an, ab welcher Position innerhalb des Felds und wie viele Bytes gespeichert werden sollen. Die beiden weiteren Parameter können die Adresse einer Callback-Prozedur und ein Objekt zur Identifizierung des Schreibvorgangs enthalten (siehe unten).
10.7 Asynchroner Zugriff auf Dateien
451
BeginWrite wird im Gegensatz zu synchronen Schreibmethoden sofort beendet. Als Ergebnis erhalten Sie ein Objekt, das die Schnittstelle IAsyncResult realisiert. Mit dessen Eigenschaft IsCompleted können Sie ganz einfach überprüfen, ob der Schreibvorgang bereits abgeschlossen ist. Darüber hinaus liefert die Eigenschaft AsyncWaitHandle ein Threading.WaitHandle-Objekt, dessen Methoden bei Bedarf eine Synchronisierung des Lese- oder
Schreib-Threads ermöglichen. Während das Programm nun im Hintergrund die Daten in die Datei überträgt, können Sie in Ihrem Programm andere Dinge erledigen. (Falls dabei Fehler auftreten können, müssen Sie eine Fehlerabsicherung durchführen, damit der Schreibvorgang sicher zu Ende geführt werden kann!) Um den Programmfluss wieder zu synchronisieren, führen Sie EndWrite aus, wobei Sie das IAsyncResult-Objekt übergeben. EndWrite wartet, bis der Schreibvorgang tatsächlich zu Ende ist. Dim result As IAsyncResult result = fs.BeginWrite(byte_array, start, n, Nothing, Nothing) ... andere Dinge erledigen fs.EndWrite(result) 'auf das Ende des Schreibvorgangs warten
Analog zu Begin-/EndWrite gibt es auch die Methoden BeginRead und EndRead. Damit können Sie Daten asynchron in ein Byte-Feld einlesen. Beachten Sie, dass Sie auf das Feld mit den Ergebnisdaten erst nach EndRead zugreifen können! Wenn Sie die Daten sofort benötigen, sollten Sie eine synchrone Schreibmethode verwenden.
Asynchrone Callbacks Statt mit EndRead/-Write auf das Ende der asynchronen Operation zu warten, können Sie sich davon auch durch eine so genannte Callback-Prozedur verständigen lassen. Eine Callback-Prozedur ist mit einer Ereignisprozedur vergleichbar, die automatisch aufgerufen wird. Diese Prozedur muss folgendermaßen deklariert werden (wichtig ist der Parameter des Typs IAsyncResult). ' wird zum Ende der asynchronen Operation aufgerufen Public Sub my_callback_func(ByVal ar As IAsyncResult) ... End Sub
Beim Aufruf von BeginWrite oder -Read übergeben Sie nun im vierten Parameter die Adresse dieser Prozedur. Im fünften Parameter können Sie ein beliebiges Objekt data angeben, das dann an die Callback-Prozedur weitergegeben wird. Dieses Objekt übernimmt zwei Aufgaben: es kann innerhalb der Callback-Prozedur helfen, die Quelle des Aufrufs zu identifizieren (falls Sie die Callback-Prozedur für verschiedene asynchrone Operationen in Ihrem Programm verwenden), und es kann dazu verwendet werden, um bestimmte Arbeiten zu erledigen, mit denen Sie warten müssen, bis die asynchrone Operation zu Ende ist.
452
10 Dateien und Verzeichnisse
' fs ist ein IO.FileStream-Objekt, siehe oben result = fs.BeginWrite(byte_array, start, n, _ AddressOf my_callback_func, data)
Innerhalb der Callback-Funktion können Sie über ar auf das oben schon beschriebene Objekt der Schnittstelle IAsyncResult zugreifen. Neu ist, dass Sie nun über die Eigenschaft ar.AsyncState auf das Objekt zugreifen können, dass Sie im fünften Parameter von BeginWrite oder -Read übergeben haben.
Fehlerabsicherung Wenn bereits als Reaktion auf die Methode BeginRead oder -Write ein Fehler auftritt, können Sie davon ausgehen, dass die asynchrone Operation gar nicht gestartet wurde. (In diesem Fall kommt es daher auch nicht zum Aufruf von Callback-Prozeduren.)
VERWEIS
Komplizierter wird es, wenn während der asynchronen Operation ein Fehler auftritt. In diesem Fall wird die entsprechende Exception erst ausgelöst, wenn Sie EndRead oder -Write ausführen. Allgemeine Grundlageninformationen zur asynchronen Programmierung (also losgelöst von den hier beschriebenen IO-Anwendungen) finden Sie in der OnlineHilfe, wenn Sie nach Einschließen asynchroner Aufrufe suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconasynchronousprogramming.htm
10.7.2 Beispiel – Vergleich synchron/asynchron Das folgende Beispielprogramm wird durch die beiden Prozeduren write_file_async und write_file_sync dominiert. write_file_async schreibt ein Byte-Feld asynchron in eine Datei, wobei ein Buffer von 256 kByte verwendet wird. Während des asynchronen Schreibvorgangs werden in der Prozedur do_some_work eine Menge Sinuswerte berechnet. write_file_sync erfüllt dieselbe Aufgabe, schreibt die Datei aber synchron und muss daher zuerst auf das Ende des Schreibvorgangs warten, bevor es do_some_work ausführen kann. Um die beiden Methoden zu vergleichen, werden write_file_[a]sync je fünf Mal ausgeführt und die Laufzeiten im Konsolenfenster angezeigt. (Achtung, dabei werden vorübergehend zwei temporäre Dateien mit einem Gesamtspeicherbedarf von 500 MByte erstellt!) Abbildung 10.13 beweist zweierlei: Einerseits, dass die asynchrone Vorgehensweise in diesem Fall wirklich deutlich effizienter ist, andererseits, dass die Zeiten relativ stark variieren können (abhängig davon, wie rasch es dem Betriebssystem gelingt, die Dateien auf die Festplatte zu schreiben). Es kann sogar vorkommen, dass eine asynchrone Operation länger dauert als eine synchrone, im Durchschnitt ist die asynchrone Variante aber deutlich effizienter.
10.7 Asynchroner Zugriff auf Dateien
Abbildung 10.13: Messzeiten für das synchrone und asynchrone Schreiben von Dateien
' Beispiel dateien\async-test Module Module1 Const nr_of_bytes As Integer = 1024 * 1024 * 50 ' 50 MByte Const buffer_size As Integer = 1024 * 256 '256 kByte Sub main() Dim i As Integer ' Byte-Feld mit Zufallsdaten füllen Dim b(nr_of_bytes) As Byte Dim rndm As New System.Random() rndm.NextBytes(b) ' zwei temporäre Dateien erzeugen Dim fname1 As String = IO.Path.GetTempFileName() Dim fname2 As String = IO.Path.GetTempFileName() ' Byte-Feld speichern For i = 1 To 5 write_file_async(fname1, b) Next For i = 1 To 5 write_file_sync(fname2, b) Next ' Dateien löschen IO.File.Delete(fname1) IO.File.Delete(fname2) End Sub
453
454
10 Dateien und Verzeichnisse
Sub write_file_async(ByVal fname As String, ByVal b_array As Byte()) Dim starttime As Date = Now Dim result As IAsyncResult Dim fs As IO.FileStream fs = New IO.FileStream(fname, IO.FileMode.Append, _ IO.FileAccess.Write, IO.FileShare.None, buffer_size, True) result = fs.BeginWrite(b_array, 0, b_array.Length, _ Nothing, Nothing) do_some_work() fs.EndWrite(result) 'auf Ende der asynchronen Operation warten fs.Close() Console.WriteLine("async: " + Now.Subtract(starttime).ToString) End Sub Sub write_file_sync(ByVal fname As String, ByVal b_array As Byte()) Dim starttime As Date = Now Dim fs As IO.FileStream fs = New IO.FileStream(fname, IO.FileMode.Append) fs.Write(b_array, 0, b_array.Length) do_some_work() fs.Close() Console.WriteLine("sync: " + Now.Subtract(starttime).ToString) End Sub ' sinnlose Berechnungen durchführen ... Sub do_some_work() Dim i As Integer Dim x As Double For i = 0 To nr_of_bytes \ 5 x += Math.Sin(i) Next End Sub End Module
10.7.3 Beispiel – Asynchroner Callback-Aufruf Das zweite Beispiel ist eine Variation des ersten. Abermals wird eine große Datei asynchron gespeichert und währenddessen eine Berechnung durchgeführt. Neu ist, dass zum Ende des Speichervorgangs die Prozedur async_callback_func aufgerufen wird. Abbildung 10.14 veranschaulicht die Gleichzeitigkeit der beiden Operation.
10.7 Asynchroner Zugriff auf Dateien
455
Abbildung 10.14: Während der Berechnung erscheint die Meldung, dass der asynchrone Schreibvorgang beendet ist
Auf einen Abdruck des gesamten Beispielprogramms wird diesmal aus Platzgründen verzichtet; die meisten Details sehen ohnedies ganz ähnlich aus wie im vorigen Beispiel. Entscheidend ist der Aufruf von BeginWrite, wobei als Parameter die Adresse von async_callback_func sowie das FileStream-Objekt übergeben werden. Das ermöglicht es, in der Callback-Prozedur die Dateigröße anzuzeigen. ' Beispiel dateien\async-callback Sub main() ... fs = New IO.FileStream(fname, IO.FileMode.Append, _ IO.FileAccess.Write, IO.FileShare.None, buffer_size, True) result = fs.BeginWrite(b_array, 0, b_array.Length, _ AddressOf async_callback_func, fs) ... End Sub ' Callback-Prozedur Public Sub async_callback_func(ByVal ar As IAsyncResult) Dim fs As IO.FileStream = CType(ar.AsyncState, IO.FileStream) Console.WriteLine("asynchroner Schreibvorgang beendet: {0}", _ ar.IsCompleted) Console.WriteLine("Dateigröße in Bytes: {0}", fs.Length) End Sub
456
10 Dateien und Verzeichnisse
10.8
Verzeichnis überwachen
HINWEIS
Die Klasse io.FileSystemWatcher ermöglicht es, ein Verzeichnis des Dateisystems zu überwachen. Jedes Mal, wenn sich in diesem Verzeichnis eine Datei ändert, wird ein Ereignis ausgelöst. Die Klasse ist im Gegensatz zu den anderen IO-Klassen in der Bibliothek System.dll definiert. Diese Bibliothek steht wie mscorlib.dll allen VB.NET-Programmen automatisch zur Verfügung. Die FileSystemWatcher-Klasse kann nur unter Windows NT/2000/XP genutzt werden! An die Ereignisprozeduren werden aber trotz dieser Betriebssystemanforderung Dateinamen in der DOS-kompatiblen 8+3-Schreibweise übergeben. Warum das so ist, weiß allein Microsoft ...
Zur Anwendung der FileSystemWatcher-Klasse definieren Sie mit WithEvents auf Moduloder Klassenebene eine Variable des Typs FileSystemWatcher. Anschließend erzeugen Sie ein neues Objekt dieser Klasse, wobei Sie an den New-Konstruktor den Namen des Verzeichnisses übergeben, das Sie überwachen möchten. Durch EnableRaisingEvents=True starten Sie den Ereignisfluss. Es werden nun in Ihrem Programm enthaltene Ereignisprozeduren für die FileSystemWatcher-Variable aufgerufen, wobei aus den Eigenschaften des Parameters e unter anderem der Name der geänderten Datei ermittelt werden kann. Die FileSystemWatcher-Klasse sieht fünf Ereignisse vor: Created, Change, Renamed, Deleted und Error. Die ersten vier Ereignisse beziehen sich auf die Dateien im überwachten Verzeichnis. Das Error-Ereignis tritt auf, wenn der interne Puffer zur Verwaltung der Dateiereignisse überschritten wird (siehe Überschrift Pufferüberlauf). Module Module1 Dim WithEvents fsw As IO.FileSystemWatcher Sub Main() fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath()) fsw.EnableRaisingEvents = True Console.WriteLine("Return beendet das Programm") Console.ReadLine() End Sub
VERWEIS
Public Sub fsw_Changed(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Changed Console.WriteLine("Datei {0} hat sich geändert", e.FullPath) End Sub End Module
Ein vollständiges Anwendungsbeispiel für die FileSystemWatcher-Klasse finden Sie im Beispielprogramm oo-programmierung\intro-event, das in Abschnitt 6.1.3 beschrieben wird.
10.8 Verzeichnis überwachen
457
Überwachungsbereich modifizieren Per Default überwacht FileSystemWatcher alle Dateien, die sich direkt im angegebenen Verzeichnis befinden. Wenn Sie auch alle Unterverzeichnisse überwachen möchten, müssen Sie IncludeSubdirectories auf True setzen. Wenn Sie nur bestimmte Dateien (z.B. "*.gif") überwachen möchten, können Sie die FilterEigenschaft mit einem entsprechenden Muster einstellen. Die Defaulteinstellung lautet "*.*". Schließlich können Sie mit NotifyFilters angeben, welcher Art die Veränderungen sein sollen, die Sie beobachten möchten. Per Default werden Änderungen am Namen und am Inhalt beobachtet. Sie können aber beispielsweise auch Veränderungen an den Sicherheitseinstellungen oder an den Dateiattributen verfolgen. Die Einstellung der Eigenschaft erfolgt durch eine Or-Kombination von Konstanten aus der NotifyFilters-Aufzählung.
Pufferüberlauf Wenn mit FileSystemWatcher eine Überwachung eingerichtet wird, wird jede Änderung am Dateisystem in einem Puffer zwischengespeichert. Die einzelnen Einträge des Puffers werden dann der Reihe nach durch die Aufrufe von Ereignisprozeduren abgearbeitet. Wenn plötzlich sehr viele Dateien erzeugt, geändert oder gelöscht werden und die FileSystemWatcher-Ereignisprozeduren nicht schnell genug verarbeitet werden können, reicht die Größe des Zwischenpuffers nicht aus. In diesem Fall tritt das Error-Ereignis auf, an das ein Objekt der Klasse InternalBufferOverflowException übergeben wird. (Das Objekt kann mit e.GetException() ausgewertet werden.) Durch diesen Fehler wird der Puffer gelöscht und die Überwachung beendet. Um neuerlich Ereignisse empfangen zu können, muss ein neues FileSystemWatcher-Objekt erzeugt werden. Dabei ist es sinnvoll, mit InternalBufferSize einen größeren Puffer zu wählen. Die Angabe des Puffers erfolgt in Byte und sollte ein Vielfaches von 4096 betragen. Die Defaultgröße beträgt 8192 Byte. Die folgende Prozedur zeigt eine mögliche Reaktion auf den Fehler.
TIPP
' Beispiel oo-programmierung\intro-event Public Sub fsw_Error(ByVal sender As Object, _ ByVal e As System.IO.ErrorEventArgs) Handles fsw.Error Console.WriteLine(e.GetException.GetType.FullName + vbCrLf + _ e.GetException.Message) fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath()) fsw.InternalBufferSize = 4096 * 16 fsw.EnableRaisingEvents = True End Sub
Achten Sie darauf, dass Sie nicht mehr überwachen, als unbedingt notwendig ist. Nutzen Sie die Eigenschaften Filter, NotifyFilter und IncludeSubdirectories, um den Geltungsbereich des FileSystemWatcher-Objekts einzuschränken.
458
10 Dateien und Verzeichnisse
Syntaxzusammenfassung System.IO.FileSystemWatcher – Eigenschaften EnableRaisingEvents
aktiviert den Aufruf der Ereignisprozeduren (True/False).
Filter
gibt ein Muster für die zu überwachenden Dateinamen an (per Default "*.*")
IncludeSubdirectories
gibt an, ob auch Unterverzeichnisse überwacht werden sollen (per Default False).
InternalBufferSize
bestimmt die Größe des Zwischenspeichers in Byte.
NotifyFilter
gibt an, welcher Art die Änderungen an der Datei sein sollen (z.B. NotifyFilters.Size).
System.IO.FileSystemWatcher – Ereignisse Changed
gibt an, dass der Inhalt einer Datei geändert wurde.
Created
gibt an, dass eine neue Datei erzeugt wurde.
Deleted
gibt an, dass eine Datei gelöscht wurde.
Error
es ist ein Fehler bei der Verwaltung des FileSystemWatcherZwischenspeichers aufgetreten.
Renamed
gibt an, dass eine Datei umbenannt wurde.
10.9
Serialisierung
Der schwer zu übersetzende Fachausdruck serialization bezeichnet die Umwandlung eines Objekts in eine Binär- oder Textform (XML), die anschließend über ein Netzwerk an einen anderen Rechner übertragen oder in einer Datei gespeichert werden kann. Die Rückverwandlung in Objekte wird Deserialisierung (englisch de-serialization) genannt. Der Begriff Serialisierung bezieht sich darauf, dass die im allgemeinen Fall hierarchische Verknüpfung verschiedener Objekte in eine serielle Form, also gewissermaßen in eine flache Darstellung umgewandelt werden muss. Für die Serialisierung und Deserialisierung gibt es eine ganze Reihe von Anwendungen: •
Übertragung von Objekten via Netzwerk: damit können Prozesse auf unterschiedlichen Rechnern Objekte austauschen.
•
Speicherung von Objekten in einer Datei: damit können Objekte persistent gespeichert und zu einem späteren Zeitpunkt wieder geladen werden. (Diese Form der Anwendung ist auch der Grund, weswegen Serialisierung in diesem Kapitel beschrieben wird.)
•
Speicherung von Objekten in einer Datenbank.
10.9 Serialisierung
459
VERWEIS
Dieser Abschnitt gibt nur eine Einführung in das Thema Serialisierung. Eine ausführlichere Beschreibung finden Sie in der Hilfe, wenn Sie nach Serialisieren von Objekten suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpovrserializingobjects.htm
Wenn Sie sich speziell für den Datenaustausch zwischen Prozessen auf unterschiedlichen Rechnern interessieren, müssen Sie sich auch in das Thema .NET Remoting einlesen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconnetremotingoverview.htm
10.9.1 Grundlagen Serialisierungsformate Im Zusammenhang mit Serialisierung bezeichnet der Begriff Formatierung die Umwandlung von Objekten zu einem Byte-Strom. Die folgende Tabelle zählt die drei in der .NETBibliothek vorgesehenen Formatierungsklassen auf. Damit können Sie Objekte in einem Binärformat, in einem XML-Format oder in einem SOAP-Format (de-)serialisieren. Formatierungsklassen zum (De-)Serialisieren von Objekten Runtime.Serialization.Formatters.Binary.BinaryFormatter
Bibliothek mscorlib.dll
Xml.Serialization.XmlSerializer
Bibliothek System.Xml.dll
Runtime.Serialization.Formatters.Soap.SoapFormatter
Bibliothek System.Runtime.Serialization.Formatters.Soap.dll
XML steht für Extensible Markup Language und bezeichnet ein Textformat zur Darstellung beinahe beliebiger (auch hierarchischer) Daten. SOAP steht für Simple Object Access Protocol und beschreibt ein standardisiertes Format zum Austausch von Objekten zwischen verschiedenen Prozessen. SOAP basiert auf XML, d.h., die Objekte werden in Form von XMLDateien oder -Datenströmen weitergegeben. Natürlich stellt sich jetzt sofort die Frage, welches Format Sie einsetzen sollen. Die folgenden Punkte sollen bei der Beantwortung dieser Frage helfen. •
Wenn es Ihnen darum geht, die Daten möglichst kompakt und effizient zu serialisieren, ist BinaryFormatter die beste Wahl. Allerdings ist dieses Format nur innerhalb der .NETWelt verständlich.
•
Wenn Sie dagegen Wert auf offene Standards legen und Daten mit Programmen austauschen möchten, die selbst nicht auf den .NET-Bibliotheken basieren, sind SoapFormatter und XmlSerializer besser geeignet. Bei beiden Varianten sind die resultierenden Datenströme aber wesentlich größer als beim Binärformat, die Verarbeitung dauert entsprechend länger.
460
•
10 Dateien und Verzeichnisse
BinaryFormatter ist am systemnächsten. Laut Online-Hilfe wird bei der (De-)Serialisierung die Typintegrität beibehalten (type fidelity im englischen Orginal). Was das bedeutet,
beschreibt die Online-Hilfe leider nicht. Praktische Tests ergaben, dass sich mit dem BinaryFormatter manche .NET-Objekte serialisieren und wieder deserialisieren lassen, bei denen SoapFormatter und XmlSerializer nur Fehlermeldungen liefern (z.B. ein Drawing.Font-Objekt). •
BinaryFormatter und SoapFormatter kommen mit Rekursion zurecht. Der XmlSerializer
kann dagegen nur dann miteinander verknüpfte Objekte (de-)serialisieren, solange es dabei keine zirkuläre Verweise gibt. •
BinaryFormatter und SoapFormatter setzen voraus, dass selbst definierte Klassen mit dem Attribut <Serializable()> ausgestattet sind, damit davon abgeleitete Objekte serialisiert werden können. XmlSerializer funktioniert auch ohne diese Voraussetzung, dafür muss die Klasse aber explizit als Public deklariert werden. (Wenn Sie das vergessen, liefert der New-Konstruktor von XmlSerializer nur eine irreführende Fehlermeldung.)
•
HINWEIS
Die Anwendung und Steuerung von BinaryFormatter und SoapFormatter erfolgt weitgehend identisch. Daher ist es meist problemlos möglich, zwischen diesen beiden Formaten zu wechseln. Dagegen stellt XmlSerializer eine vollkommen eigenständige und in vielen Details inkompatible Implementierung dar. Dieser Abschnitt bezieht sich überwiegend auf BinaryFormatter und SoapFormatter. XmlSerializer wird nur am Rande behandelt. Beachten Sie, dass Sie vor der Verwendung des SoapFormatters einen Verweis auf die Bibliothek System.Runtime.Serialization.Formatters.Soap.dll einrichten müssen!
Objekt serialisieren Um ein Objekt zu serialisieren, benötigen Sie ein Formatter-Objekt. Anschließend übergeben Sie an dessen Serialize-Methode ein IO.Stream-Objekt einer Datei oder einer Netzwerkverbindung sowie das zu serialisierende Objekt: Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() binform.Serialize(streamobj, dataobj)
Selbstverständlich können Sie nun mit binform auch mehrere Objekte hintereinander serialisieren. (Sie müssen aber bei der Deserialisierung darauf achten, dass Sie die Objekte in der gleichen Reihenfolge wieder auslesen.) Statt der BinaryFormatter-Klasse können Sie ebenso gut eine SoapFormatter-Klasse verwenden. Auch der Einsatz der XmlSerializer-Klasse ändert nicht viel an der Vorgehensweise, allerdings muss als New-Parameter der Klassentyp des zu speichernden Stammobjekts
10.9 Serialisierung
461
übergeben werden. (dataobj darf aber durchaus auf Objekte anderer Klassen verweisen, die ebenfalls serialisiert werden.) Dim xmlform As New Xml.Serialization.XmlSerializer(dataobj.GetType()) xmlform.Serialize(streamobj, dataobj)
Objekt deserialisieren Bevor Sie ein Objekt deserialisieren können, benötigen Sie abermals ein Objekt der Klassen BinaryFormatter, SoapFormatter oder XmlSerializer, das wie vorhin erzeugt wird. Darauf wenden Sie die Methode Deserialize an. Diese Methode liefert die Daten als Object zurück, weswegen Sie mit CType eine Konvertierung durchführen müssen: dataobj = CType(binform.Deserialize(streamobj), dataclass)
HINWEIS
Eigene Klassen (de-)serialisieren Alle weiteren Informationen in diesem Abschnitt beziehen sich explizit nur auf Binary- und SoapFormatter! Die Serialisierung eigener Klassen mit XmlSerializer wird durch andere Attribute gesteuert, die hier aber nicht beschrieben werden.
Damit ein Objekt einer selbst definierten Klasse (de-)serialisiert werden kann, muss die ganze Klasse mit dem Attribut <Serializable()> gekennzeichnet werden. .NET speichert nun bei der Serialisierung automatisch alle Klassenvariablen (unabhängig davon, ob diese Public oder Private sind). Bei der Serialisierung werden die Klassenvariablen direkt ausgelesen, d.h., es werden dazu keine Eigenschaften oder Methoden eingesetzt. Bei der Deserialisierung wird ein neues Objekt erzeugt, wobei alle Klassenvariabeln rekonstruiert werden. Dabei wird weder einer der New-Konstruktoren ausgeführt, noch werden irgendwelche Eigenschaften oder Methode der Klasse benutzt. Wenn Sie möchten, dass einzelne Klassenvariablen nicht serialisiert werden, können Sie diese als kennzeichnen. <Serializable()> Class class1 Public a As Integer Public b As String ... End Class
ISerializable-Schnittstelle Wenn Ihnen die automatische (De-)Serialisierung zu wenig Flexibilität bietet, können Sie die Sache auch selbst in die Hand nehmen. Dazu müssen Sie in Ihrer Klasse die Runtime.Serialization.ISerializable-Schnittstelle implementieren. Diese Schnittstelle muss zusätzlich zum Attribut <Serializable()> angegeben werden.
462
10 Dateien und Verzeichnisse
Die ISerializable-Schnittstelle bewirkt, dass nun automatisch gar keine Daten mehr (de-)serialisiert werden. Stattdessen müssen Sie sich selbst um die Serialisierung in der Methode GetObjectData und um die Deserialisierung in einem zusätzlichen New-Konstruktor kümmern. Die folgenden Zeilen geben ein Codegerüst an: <Serializable()> Class class1 Implements Runtime.Serialization.ISerializable ' Deserialisierung Public Sub New( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) a = info.GetInt32("a") b = info.GetString("b") ... End Sub ' Serialisierung Public Overridable Overloads Sub GetObjectData( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) _ Implements Runtime.Serialization.ISerializable.GetObjectData info.AddValue("a", a) info.AddValue("b", b) ... End Sub End Class
An GetObjectData wird ein Objekt der Klasse SerializationInfo-Objekt übergeben. Mit dessen Methode AddValue übergeben Sie alle zu serialisierenden Daten (meist Klassenvariablen). Dabei muss jede Variable durch eine eindeutige Zeichenkette gekennzeichnet werden. Im zusätzlichen New-Konstruktor erfolgt der umgekehrte Vorgang: Mit diversen GetXxxMethoden können Sie die Daten aus dem SerializationInfo-Objekt wieder extrahieren. Die Get-Methoden stehen für alle elementaren Datentypen zur Verfügung, z.B. GetString, GetSingle etc. Bei Integerzahlen gilt: GetInt16 für Short, GetInt32 für Integer, GetInt64 für Long. Um Objekte einer beliebigen Klasse zu extrahieren, müssen Sie die Methode GetValue verwenden. An diese Methode müssen Sie im zweiten Parameter den zu erwartenden Klassentyp übergeben. Außerdem müssen Sie die resultierenden Daten, die von GetValue als Object zurückgegeben werden, mit CType in ein Objekt der entsprechenden Klasse umwandeln. c = CType(info.GetValue("c", GetType(klassenname)), klassenname)
10.9 Serialisierung
463
10.9.2 Beispiel – String-Feld serialisieren Das folgende Beispielprogramm erzeugt ein String-Feld zufälliger Größe und Inhalts. Dieses Feld wird in eine temporäre Datei serialisiert und dann von dort wieder neu eingelesen. Zur Kontrolle wird der Inhalt des Felds vorher und nachher angezeigt. Außerdem wird die Serialialisierungsdatei im Konsolenfenster angezeigt (siehe Abbildung 10.15).
Abbildung 10.15: Beispielprogramm zur Serialisierung eines String-Felds
Das Programm erzeugt in der Prozedur create_array ein zufälliges String-Feld und zeigt dieses mit show_array am Bildschirm an. Das Feld wird dann mit save_array_xxx in einem von drei Formaten gespeichert und anschließend mit load_array_soap_xxx wieder eingelesen. ' Beispiel dateien\serialize-array Sub Main() Dim s() As String Dim fname As String = IO.Path.GetTempFileName() Dim fs As IO.FileStream ' Feld mit Zufallsdaten erzeugen und anzeigen create_array(s) show_array(s) ' Array speichern Console.WriteLine("Temporäre Datei: {0}", fname) fs = New IO.FileStream(fname, IO.FileMode.Open)
464
10 Dateien und Verzeichnisse
save_array_soap(fs, s) fs.Close() ' Array löschen und neu laden Erase s fs = New IO.FileStream(fname, IO.FileMode.Open) load_array_soap(fs, s) fs.Close() show_array(s) ' Serialisierungsdatei anzeigen Dim sr As New IO.StreamReader(fname) Console.WriteLine("Der Inhalt der Serialisierungsdatei:") Console.WriteLine(sr.ReadToEnd()) sr.Close() IO.File.Delete(fname) End Sub Sub save_array_binary(ByVal fs As IO.FileStream, ByVal s As String()) Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() binform.Serialize(fs, s) End Sub Sub save_array_soap(ByVal fs As IO.FileStream, ByVal s As String()) Dim soapform As New _ Runtime.Serialization.Formatters.Soap.SoapFormatter() soapform.Serialize(fs, s) End Sub Sub save_array_xml(ByVal fs As IO.FileStream, ByVal s As String()) Dim xmlform As New Xml.Serialization.XmlSerializer(s.GetType()) xmlform.Serialize(fs, s) End Sub Sub load_array_binary(ByVal fs As IO.FileStream, ByRef s As String()) Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() s = CType(binform.Deserialize(fs), String()) End Sub Sub load_array_soap(ByVal fs As IO.FileStream, ByRef s As String()) ' analog zu load_array_binary(...) ... End Sub Sub load_array_xml(ByVal fs As IO.FileStream, ByRef s As String()) Dim xmlform As New _ Xml.Serialization.XmlSerializer(GetType(String())) s = CType(xmlform.Deserialize(fs), String()) End Sub
10.9 Serialisierung
465
10.9.3 Beispiel – Objekte eigener Klassen serialisieren Im Beispielprogramm werden zwei Objekte data1 und data2 erzeugt, in einen MemoryStream serialisiert und von dort in get1 und get2 deserialisiert. Anschließend zeigt das Beispielprogramm die Klassenvariablen der vier Variablen an (siehe Abbildung 10.2).
Abbildung 10.16: Beispielprogramm zur (De-)Serialisierung eigener Klassen
Die Klasse class1 besteht aus vier Klassenvariablen a, b, c und d für verschiedene Datentypen. c enthält ein Objekt der Klasse System.Random, um so auch die Serialisierung nicht ganz trivialer Daten zu demonstieren. Die New-Konstruktoren dienen zur Initialisierung, WriteToConsole schreibt den Inhalt von a bis d in das Konsolenfenster. Die gesamte Klasse ist mit dem Attribut Serializable gekennzeichnet. ' Beispiel dateien\serialize-test <Serializable()> Class class1 Public a As Integer Public b As String Public c As Random Public d As Date Public Sub New() Me.New(0, "", New Random()) End Sub Public Sub New(ByVal a As Integer, ByVal b As String, _ ByVal c As Random) Me.a = a : Me.b = b : Me.c = c : d = Now End Sub Public Sub WriteToConsole(ByVal name As String) Console.Write(name + ": ") Console.WriteLine( _ "a={0}, b={1}, Zufallszahl c.Next()={2}, d={3}", _ a, b, c.Next(), d) End Sub End Class class2 ist von class1 vererbt und realisiert außerdem die Schnittstelle ISerializable. Die beiden
elementaren Konstruktoren rufen einfach den Konstruktor der Basisklasse auf. Neu ist der dritte Konstruktor, der wegen der ISerializable-Schnittstelle bei der Deserialisierung aufge-
466
10 Dateien und Verzeichnisse
VERWEIS
rufen wird. In dieser Prozedur werden die Klassenvariablen a bis d initialisiert. a bis c werden aus dem SerializationInfo-Objekt extrahiert. d wird dagegen mit der aktuellen Zeit initialisiert (um so zu zeigen, dass Sie durch die ISerializable-Schnittstelle Flexibilität bei der Initialisierung gewinnen). Damit gibt class2.d den Zeitpunkt an, zu dem das Objekt deserialisiert wurde, während class1.d den Zeitpunkt angibt, zu dem das Objekt zum ersten Mal erzeugt wurde. Für die Serialisierung ist die Methode GetObjektData verantwortlich. Dort werden a bis d in das SerializationInfo-Objekt übertragen. Ein weiteres Beispiel für die Realisierung der ISerializable-Schnittstelle finden Sie in Abschnitt 11.1.1, wo die Programmierung einer eigenen Exception-Klasse erklärt wird.
'Klasse mit ISerializable-Schnittstelle <Serializable()> Class class2 Inherits class1 Implements Runtime.Serialization.ISerializable Public Sub New() MyBase.New() End Sub Public Sub New(ByVal a As Integer, ByVal b As String, _ ByVal c As Random) MyBase.New(a, b, c) End Sub ' Deserialisierung Public Sub New( _ ByVal info As System.Runtime.Serialization.SerializationInfo, _ ByVal context As System.Runtime.Serialization.StreamingContext) a b c d End
= info.GetInt32("a") = info.GetString("b") = CType(info.GetValue("c", GetType(Random)), Random) = Now Sub
' Serialisierung Public Overridable Overloads Sub GetObjectData( _ ByVal info As System.Runtime.Serialization.SerializationInfo, _ ByVal context As System.Runtime.Serialization.StreamingContext) _ Implements Runtime.Serialization.ISerializable.GetObjectData info.AddValue("a", a) info.AddValue("b", b) info.AddValue("c", c) End Sub End Class
10.9 Serialisierung
467
Bei der Anwendung der Klassen gibt es wenig Besonderheiten. Die Serialisierung erfolgt in einen MemoryStream, um das Erzeugen einer temporären Datei zu vermeiden. Vor der Deserialisierung legt das Programm eine Pause von drei Sekunden ein, um die unterschiedliche Behandlung der Klassenvariable d in class1 und class2 zu demonstrieren. ' Beispiel dateien\serialize-test Sub Main() Dim i As Integer Dim ms As New IO.MemoryStream() Dim frmt As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() ' Objekte erzeugen, serialisieren und ihren Inhalt anzeigen Dim data1 As New class1(123, "abc", New Random()) Dim data2 As New class2(456, "xxx", New Random()) frmt.Serialize(ms, data1) frmt.Serialize(ms, data2) Console.WriteLine("3 Sekunden Pause ...") Threading.Thread.Sleep(3000) ' Objekte deserialisieren Dim get1 As class1, get2 As class2 ms.Seek(0, IO.SeekOrigin.Begin) get1 = CType(frmt.Deserialize(ms), class1) get2 = CType(frmt.Deserialize(ms), class2) ' Quelldaten und Ergebnisse anzeigen Console.WriteLine("Ausgangsdaten:") data1.WriteToConsole("data1") data2.WriteToConsole("data2") Console.WriteLine("Deserialisierte Daten:") get1.WriteToConsole("get1") get2.WriteToConsole("get2") End Sub
10.9.4 Beispiel – LinkedList serialisieren In Kapitel 7 wurde zur Erläuterung des objektorientierten Programmierens die Klasse LinkedList vorgestellt. Das folgende Beispiel beweist, dass sich eine Kette von LinkedListObjekten problemlos mit Binary- oder SoapFormatter serialisieren lässt. (XmlSerializer ist dazu ungeeignet, weil die Vor- und Rückverweise der LinkedList-Objekte zirkulär sind.) Die einzige Änderung, die dazu an der LinkedList-Klasse erforderlich ist, ist das <Serializable()>Attribut.
468
10 Dateien und Verzeichnisse
' Beispiel dateien\serialize-linkedlist Module Module1 Sub Main() Dim ms As New IO.MemoryStream() Dim frmt As New _ Runtime.Serialization.Formatters.Soap.SoapFormatter() Dim test1, test2 As LinkedList ' test1: Testkette zusammenstellen, serialisieren test1 = New LinkedList("Das") test1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") frmt.Serialize(ms, test1) 'test2: Testkette deserialisieren ms.Seek(0, IO.SeekOrigin.Begin) test2 = CType(frmt.Deserialize(ms), LinkedList) ' Testausgabe Console.WriteLine("test1:") Console.WriteLine(test1.ItemsText()) Console.WriteLine("test2:") Console.WriteLine(test2.ItemsText()) End Sub End Module
'liefert 'Das ist ein Satz.' 'liefert 'Das ist ein Satz.'
<Serializable()> Class LinkedList ... wie oo-programmierung\linkedlist3 End Class
10.10 IO-Fehler Wenn Sie (per Programmcode) mit Dateien hantieren, kann beinahe alles schief gehen: •
Sie greifen auf eine Datei zu, die es nicht mehr gibt (etwa weil die Diskette, CD-ROM, DVD etc. aus dem Laufwerk entfernt wurde).
•
Sie dürfen eine Datei nicht ändern (oder nicht einmal lesen), weil der Benutzer, der Ihr Programm ausführt, keine ausreichenden Zugriffsrechte hat.
•
Sie können auf eine Datei nicht zugreifen, weil diese von einem anderen Programm verwendet wird.
•
Die Diskette, Festplatte etc. ist voll, während Sie versuchen, eine Datei zu schreiben.
•
Der Benutzer gibt ungültige Datei- oder Verzeichnisnamen an.
•
Während der Bearbeitung einer Datei innerhalb eines lokalen Netzwerks gibt es Netzwerkprobleme.
10.10 IO-Fehler
469
VERWEIS
Diese Liste ließe sich natürlich noch fortsetzen – aber ich denke, die angeführten Punkte reichen bereits aus, um die Notwendigkeit einer guten Fehlerabsicherung zu begründen. Dieser Abschnitt gibt einen Überblick über die wichtigsten Fehler (Exceptions), die beim Umgang mit Dateien auftreten können. Ein konkretes Beispiel zur Absicherung eines einfachen IO-Programms gibt Abschnitt 10.3.2. Allgemeine Informationen darüber, wie eine Fehlerabsicherung mit Catch-Try durchgeführt werden kann, gibt Kapitel 11.
Fehlertypen (System.IO-Exceptions) Im System.IO-Namensraum sind die folgenden (in der Hierarchie fett hervorgehobenen) Exception-Klassen zur Information über Fehler vorgesehen. Die meisten der Fehler sind selbst erklärend. Hierarchie der Exception-Klassen
ACHTUNG
Exception └─ SystemException ├─ IOException │ ├─ DirectoryNotFoundException │ ├─ EndOfStreamException │ ├─ FileLoadException │ ├─ FileNotFoundException │ └─ PathTooLongException └─ InternalBufferOverflowException
Basisklasse für alle Exceptions Basisklasse für alle .NET-Exceptions Basisklasse für einige System.IO-Exceptions Verzeichnis existiert nicht. Ende der Datei wurde erreicht. Assembly-Datei kann nicht geladen werden. Datei existiert nicht. Dateiname ist zu lang (siehe unten). Pufferüberlauf (definiert in System.dll)
Beachten Sie, dass bei der Bearbeitung von Dateien auch andere Fehler auftreten können, z.B. UnauthorizedAccessException oder SecurityException, wenn Sie keine Zugriffsrechte haben, um eine Datei oder ein Verzeichnis zu bearbeiten!
Probleme mit zu langen Dateinamen Die folgenden Zeilen resultieren aus Problemen mit dem Beispielprogramm verzeichnisbaum (siehe Abschnitt 10.3.2). Dieses Programm ermittelt die Größe jeder einzelnen Datei einer Festplatte. Dabei traten bei manchen Dateien Probleme auf, die aus einem zu langen Dateinamen resultierten. (Der Vollständigkeit halber sei noch erwähnt, dass es der Internet Explorer war, der diese Dateien im Cache-Verzeichnisse Content.IE5 erzeugt hatte.) Der Hintergrund des Problems besteht darin, dass die gesamte Länge eines vollständigen Dateinamens (inklusive Laufwerks- und Verzeichnisangabe) 260 Zeichen nicht überschreiten darf. Dieses Limit wird durch System.IO vorgegeben, nicht durch das Dateisystem!
470
10 Dateien und Verzeichnisse
Die Eigenschaft FullName liefert den vollständigen Namen einer Datei oder eines Verzeichnisses. Wird dabei das 260-Zeichenlimit überschritten, tritt bei der Auswertung von FullName der Fehler System.IO.PathTooLongException auf. Falls sowohl das Verzeichnis als auch die Datei als eigenständige Objekte vorliegen (z.B. dir und file), können Sie sich damit behelfen, dass Sie den vollständigen Dateinamen mit IO.Path.Combine zusammensetzen: Dim dir As New IO.DirectoryInfo("ein verzeichnis") Dim file As IO.FileInfo Dim reallyFullName As String For Each file In dir.GetFiles("*.abc") Try reallyFullName = file.FullName Catch e As IO.PathTooLongException reallyFullName = IO.Path.Combine(dir.FullName, file.Name) End Try Console.WriteLine(reallyFullName) Next
Wenn Sie Probleme erwarten, können Sie natürlich auch auf den Try/Catch-Block verzichten und immer Combine verwenden. Der obige Code setzt allerdings voraus, dass nur file.FullName Probleme bereitet, dir.FullName aber fehlerfrei ausgeführt wird. Theoretisch kann bereits dir.FullName einen Fehler verursachen (nämlich dann, wenn bereits die aneinander gefügten Verzeichnisnamen zu lange sind), in der Praxis ist das aber relativ unwahrscheinlich. Aber selbst wenn es Ihnen gelingt, mit dem obigen Code den vollständigen Dateinamen zu ermitteln, haben Sie damit noch nicht viel gewonnen. Bei Dateien, deren Name 260 Zeichen überschreitet, besteht keine Möglichkeit, auch nur einfache Informationen (etwa die Dateigröße) zu ermitteln. Allerdings tritt in diesem Fall ein anderer Fehler auf: System.IO. FileNotFoundException. Kurz gefasst bedeutet das leider, dass Sie mit System.IO nicht alle Dateien bearbeiten können, die sich in Ihrem Dateisystem befinden.
11 Fehlersuche und Fehlerabsicherung Eine grundlegende Tatsache des Programmierens lautet leider: Jedes Programm enthält Fehler. Wie Sie Fehler in eigenen Anwendungen entdecken bzw. wie Sie Ihre Programme gegen Fehler absichern, ist Thema dieses Kapitels. Zur Fehlerabsicherung sieht VB.NET ein Konstrukt vor, das aus den Anweisungen Try, Catch und End Try besteht. Damit steht auch VB-Programmierern ein strukturierter Weg zur Absicherung ihres Codes zur Verfügung. Zur Fehlersuche bietet die Visual-Studio-Entwicklungsumgebung die besten Voraussetzungen: Sie können Variablen beobachten, den Programmcode schrittweise (Zeile für Zeile) ausführen, zwischen mehreren Threads wechseln etc. 11.1 11.2
Fehlerabsicherung Fehlersuche (Debugging)
472 490
472
11.1
11 Fehlersuche und Fehlerabsicherung
Fehlerabsicherung
Wie so oft in VB.NET gibt es auch zur Fehlerabsicherung zwei Wege. Der eine stammt aus den Zeiten von VB6 und basiert auf den diversen On-Error-xxx-Varianten. Allerdings ist dieses Konzept veraltet und führt oft zu unübersichtlichem Code. Neu in VB.NET ist die zweite Variante mit den Kommandos Try und Catch. Dieses Konzept ermöglicht es, den Code für die Fehlerbehandlung etwas besser zu strukturieren. Dieses Buch konzentriert sich auf den neuen Weg; die On-Error-Syntax wird nur kurz in Abschnitt 11.1.4 beschrieben. Am Beginn des Kapitels steht aber die Beschreibung von so genannten Ausnahmen (exceptions), die die Basis des gesamten Fehlermanagements unter VB.NET ist.
11.1.1 Ausnahmen (exceptions) Wenn Sie Code ausführen, der einen Fehler enthält, tritt dabei eine so genannte Ausnahme (exception) auf. Wenn also in diesem Kapitel von Fehlern die Rede ist, sind genau genommen immer Ausnahmen gemeint. Eine exception bewirkt, dass die normale Programmausführung unterbrochen wird und (soweit vorhanden) Code zur Fehlerbehandlung ausgeführt wird. Wenn die aktuelle Prozedur nicht gegen Fehler abgesichert ist, wird sie verlassen und die Information über den Fehler (ein Exception-Objekt) an die nächst höhere Ebene innerhalb der Aufrufhierarchie weitergegeben.
Defaultreaktion auf Fehler, Fehlerdialoge Wenn Sie das folgende Miniprogramm ausführen, tritt ein Division-durch-0-Fehler auf (System.DivideByZeroException). Sub Main() Dim a, b, c As Integer a = b \ c End Sub
Abbildung 11.1: Fehlerhaftes Programm in der Entwicklungsumgebung ausführen
11.1 Fehlerabsicherung
473
VERWEIS
In der Entwicklungsumgebung erscheint bei der Ausführung der in Abbildung 11.1 dargestellte Dialog. Mit UNTERBRECHEN wird der Debugger aktiviert und die fehlerhafte Zeile markiert. Sie können nun den Fehler analysieren (siehe Abschnitt 11.2). Wenn Sie dagegen auf WEITER drücken, wird das Programm beendet. (Warum der Button nicht klarer mit ENDE beschriftet wurde, weiß nur das Microsoft-Entwicklungsteam.) Per Default erscheint beim Auftreten jedes Fehlers, der nicht durch On Error oder Try-Catch abgesichert ist, der in Abbildung 11.1 dargestellte Dialog. Dieses Verhalten kann aber durch den Dialog DEBUGGEN|AUSNAHMEN verändert werden. In diesem Dialog können Sie erreichen, dass das Programm bei bestimmten Fehlern in jedem Fall sofort unterbrochen wird (auch wenn der Fehler abgesichert ist) oder dass der Fehler übergangen wird. Weitere Informationen zu diesem Dialog und anderen Debugging-Elementen der Entwicklungsumgebung folgen in Abschnitt 11.2.2.
Wenn Sie das obige Programm außerhalb der Entwicklungsumgebung ausführen, sieht der Fehlerdialog ein wenig anders aus (siehe Abbildung 11.2): Hier wird der Anwender gefragt, ob er (soweit verfügbar) einen Debugger zur Analyse des Problems starten möchte. JA startet den Debugger, NEIN beendet das fehlerhafte Programm. Auch dieser Dialog ist also für den Normalanwender, der von Programmierung keine Ahnung hat und den Begriff Debugger wahrscheinlich gar nicht kennt, wenig hilfreich. Sie sollten also alles tun, um zu vermeiden, dass der Endanwender einen derartigen Dialog je zu Gesicht bekommt.
Abbildung 11.2: Fehlerhaftes Programm außerhalb der Entwicklungsumgebung ausführen
474
11 Fehlersuche und Fehlerabsicherung
Wenn bei Windows-Anwendungen ein Fehler nicht unmittelbar in Ihrem eigenen Code auftritt, sondern innerhalb der Windows.Forms-Bibliothek, dann wird der in Abbildung 11.3 dargestellte Fehlerdialog angezeigt, der schon sehr viel aufschlussreicher ist (zumindest für Programmierer). Dabei ist es egal, ob es sich um einen internen Fehler oder einen von Ihnen verursachten Fehler handelt. In seltenen Fällen können Sie das Programm hier sogar mit WEITER fortsetzen, meist führt aber WEITER wie BEENDEN zum sofortigen Programmende.
Abbildung 11.3: Windows.Forms-Fehlermeldung
Exception-Eigenschaften Jede Ausnahme wird durch ein Objekt beschrieben, dessen Klasse von der Klasse System.Exception abgeleitet ist. Der folgende Baum zeigt die Hierarchie einiger der ExceptionKlassen. Hierarchie der Exception-Klassen System.Exception ├─ System.ApplicationException
│ └─ System.SystemException │ ├─ System.DivideByZeroException ├─ ... └─ System.IO.IOException │ ├─ System.IO.FileNotFoundException └─ ...
Basisklasse für alle exceptions Basisklasse für alle benutzerdefinierten exceptions Basisklasse für alle exceptions der .NET-Bibliotheken Division-durch-0-Fehler zahllose weitere Fehler Basisklasse für alle exceptions des System.IO-Namensraums Datei-nicht-gefunden-Fehler weitere System.IO-Fehler
11.1 Fehlerabsicherung
475
Die Aufgabe all dieser Objekte besteht darin, den Fehler und seine Ursache bzw. den Weg seiner Entstehung so exakt wie möglich zu beschreiben. Die folgende Tabelle fasst die wichtigsten Eigenschaften und Methoden zusammen, die bei allen Exception-Objekten zur Verfügung stehen. System.Exception-Klasse – Eigenschaften und Methoden GetBaseException()
liefert das Exception-Objekt, das am Beginn einer Kette von einander auslösenden Fehlern steht. (GetBaseException verfolgt InnerException bis an den Anfang zurück. Wenn der aktuelle Fehler nicht durch einen anderen Fehler ausgelöst wurde, liefert GetBaseException einfach Nothing.)
HelpLink
verweist auf einen zum Fehler passenden Hilfetext.
InnerException
falls der Fehler durch einen anderen Fehler ausgelöst wurde, verweist InnerException auf den vorangegangenen Fehler.
Message
enthält die Fehlerbeschreibung.
Source
gibt das Programm an, in dem der Fehler aufgetreten ist. (Per Default enthält Source einfach den Assembly-Namen.)
StackTrace
gibt an, wie es zum Aufruf der Prozedur bzw. der Methode kam, in dem der Fehler ausgelöst worden ist. (Der Inhalt von StackTrace entspricht den Informationen, die in der Entwicklungsumgebung im Fenster AUFRUFLISTE angezeigt werden.) Das folgende Beispiel gibt an, dass der Fehler in der Prozedur sub1 aufgetreten und dass sub1 von main aufgerufen worden ist. In der Praxis ist die Verschachtelung meist viel höher; die Prozedurkette enthält oft auch Prozeduren oder Methoden, die innerhalb der .NETBibliothek definiert sind. at myown_exception.Module1.sub1(String p1, Int32 p2) in D:\code\vb.net\fehler\myown-exception\Module1.vb:line 16 at myown_exception.Module1.Main() in D:\code\vb.net\fehler\myown-exception\Module1.vb:line 5
TargetSite
verweist auf ein Objekt der Klasse System.Reflection.MethodBase, das detaillierte Informationen über die Prozedur bzw. die Methode gibt, in der der Fehler aufgetreten ist. (Über das MethodBase-Objekt können Sie z.B. feststellen, welche Parameter die Methode kennt, welchen Typ diese Parameter haben etc. Die Methode ToString liefert einfach die Deklaration der Prozedur in C#-Syntax (z.B. Void sub1(System.String, Int32)).
Die von Exception abgeleiteten Klassen kennen zum Teil spezifische Zusatzeigenschaften, deren Inhalt dem Fehlertyp entspricht. Beispielsweise gibt es bei System.IO.FileNotFoundException die zusätzliche Eigenschaft FileName; sie enthält den Namen der Datei, die nicht gefunden wurde.
TIPP
476
11 Fehlersuche und Fehlerabsicherung
Im Regelfall haben Sie es also nicht mit einem Objekt der Klasse System.Exception zu tun, sondern mit einem Objekt einer davon abgeleiteten Klasse. Den Namen dieser Klasse können Sie mit e.GetType().Name ermitteln (wenn e ein allgemeines ExceptionObjekt ist).
Fehler (exceptions) selbst auslösen Der Großteil dieses Kapitels beschäftigt sich mit der Frage, wie Sie auf Fehler reagieren können, die während der Programmausführung unerwartet aufgetreten sind. Sie können Fehler (exceptions) aber auch explizit selbst auslösen. Das ist beispielsweise dann sinnvoll, wenn Sie eine eigene Klasse entwickelt haben und den Code für die einzelnen Methoden und Eigenschaften gegen unzulässige Parameter absichern möchten. Um einen Fehler auszulösen, verwenden Sie das VB.NET-Schlüsselwort Throw. Als Parameter übergeben Sie ein Exception-Objekt. Throw New Exception()
Durch Throw wird der normale Programmfluss unterbrochen. Wenn Throw innerhalb eines Try-Blocks ausgeführt wird, wird das Programm im dazugehörenden Catch-Block fortgesetzt (Details folgen im nächsten Abschnitt). Wenn der Code in der aktuellen Prozedur dagegen nicht abgesichert ist, wird die Prozedur beendet. Das Exception-Objekt wird an die übergeordnete Prozedur weitergegeben (also dorthin, wo die fehlhafte Prozedur ausgeführt wurde). Ist der Aufruf auch dort nicht durch Try abgesichert, wandert das ExceptionObjekt bis an den Ausgangspunkt der Aufrufliste. Fehlt auch dort eine Fehlerbehandlung, erscheint der am Beginn dieses Kapitels dargestellte Fehlerdialog und das Programm wird beendet. In der Regel werden Sie nicht ein allgemeines Exception-Objekt verwenden, sondern ein Objekt einer Exception-Klasse, die Ihrem Fehler besser entspricht. Um beispielsweise anzugeben, dass an einen Parameter Nothing übergeben wurde, obwohl ein konkretes Objekt erforderlich gewesen wäre, können Sie mit der folgenden Zeile eine ArgumentNullException auslösen: Throw New ArgumentNullException() ArgumentNullException ist eine der vielen durch die .NET-Bibliotheken vorgegebenen
Fehlerklassen. Zahllose weitere derartige Klassen finden Sie, wenn Sie mit dem Objektbrowser nach Klassen suchen, deren Name Exception enthält. Auf diese Weise finden Sie z.B. ArithmeticException, ConfigurationException, InvalidCastException, NotImplementedException, NullReferenceException oder SecurityException. Jede Exception-Klasse kennt eine Defaultfehlerbeschreibung (z.B. Wert darf nicht Null sein bei der ArgumentNullException-Klasse). Um statt dieses Defaulttexts einen eigenen Text anzugeben, können Sie bei den meisten Exception-Klassen an den New-Konstruktor genauere Informationen über die Art des Fehlers übergeben. Der bzw. die New-Parameter hängen von der jeweiligen Exception-Klasse ab.
11.1 Fehlerabsicherung
477
An den Konstruktor von ArgumentOutOfRangeException können beispielsweise zwei Zeichenketten übergeben werden, von denen die erste den fehlerhaften Parameter beschreibt und die zweite angibt, welche Regel verletzt wurde. Im folgenden Beispiel akzeptiert der Parameter p2 keine negative Zahlen. Die Klasse verwendet die beiden New-Parameter, um daraus die Fehlerbeschreibung zu bilden. Der Aufruf von sub1("abc", -10) führt zu einer Exception, deren Message-Eigenschaft dann so aussieht: p2 darf nicht negativ sein. Übergeben wurde -10. Parametername: p2, Prozedur sub1, Modul Module1 Public Sub sub1(ByVal p1 As String, ByVal p2 As Integer) If p2 < 0 Then Throw New ArgumentOutOfRangeException( _ "p2, Prozedur sub1, Modul Module1", _ "p2 darf nicht negativ sein. Übergeben wurde " + _ p2.ToString + ".") End If ... End Sub
Eigene Exception-Klassen Wenn die vorgegebenen Exception-Klassen Ihren Anforderungen nicht entsprechen, können Sie selbst eine Exception-Klasse programmieren. Um den Normen der .NET-Fehlerverwaltung zu entsprechen, sollte diese Klasse von System.ApplicationException abgeleitet sein. Bei der Implementierung einer eigenen Exception-Klasse müssen Sie zwei Details beachten: Sie müssen eigene Versionen für alle New-Konstruktoren der ApplicationException-Klasse schreiben.
•
Damit Fehler zwischen unterschiedlichen Rechnern übertragen werden können (was in verteilten Anwendungen manchmal notwendig ist), muss jede Exception-Klasse die Schnittstelle ISerializable implementieren. Wenn Sie in Ihrer Exception-Klasse eigene Daten speichern, müssen Sie sich selbst um deren (De-)Serialisierung kümmern. Für die Serialisierung ist die Methode GetObjectData verantwortlich, für die Deserialisierung einer der New-Konstruktoren.
VERWEIS
•
Einen lesenswerten Artikel (The Well-Tempered Exception von Eric Gunnerson) zur Programmierung eigener Exception-Klassen finden Sie hier: http://msdn.microsoft.com/library/en-us/dncscol/html/csharp08162001.asp
Alle Beispiele sind allerdings als C#-Code angegeben. Das folgende Beispiel stellt die Klasse MyOwnException vor, die sich von der gewöhnlichen Exception-Klasse durch die zusätzliche Eigenschaft ErrorTime unterscheidet. Über diese Eigenschaft kann der genaue Fehlerzeitpunkt festgestellt werden. Intern wird dieser Zeitpunkt in der Variablen _errortime gespeichert. Diese Variable wird durch die drei gewöhnlichen New-Konstruktoren mit Now initialisiert. Wird das Objekt dagegen durch den vier-
478
11 Fehlersuche und Fehlerabsicherung
ten New-Konstruktor durch eine Serialisierung erzeugt, wird der Wert von _errortime aus dem SerializationInfo-Objekt gelesen. Für die Serialisierung ist GetObjectData verantwortlich. ' Beispiel fehler\myown-exception <Serializable()> Class MyOwnException Inherits ApplicationException Private _errortime As Date ' Defaultkonstruktor Public Sub New() Me.New("MyOwnException") ' Defaultfehlertext End Sub ' Konstruktor mit Fehlertext Public Sub New(ByVal message As String) MyBase.New(message) _errortime = Now End Sub ' Konstruktor mit Fehlertext und Angabe einer vorausgegangenen ' Exception Public Sub New(ByVal message As String, ByVal inner As Exception) MyBase.New(message, inner) _errortime = Now End Sub ' Objekt durch Deserialisierung erzeugen Public Sub New( _ ByVal info As Runtime.Serialization.SerializationInfo, ByVal context As Runtime.Serialization.StreamingContext) MyBase.New(info, context) _errortime = info.GetDateTime("errortime") End Sub ' Objekt serialisieren Public Overrides Sub GetObjectData( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) MyBase.GetObjectData(info, context) info.AddValue("errortime", _errortime) End Sub ' Zeitpunkt des Fehlers ermitteln Public ReadOnly Property ErrorTime() As Date Get Return _errortime End Get End Property End Class
11.1 Fehlerabsicherung
479
11.1.2 Try-Catch-Syntax Um zu vermeiden, dass ein unerwartet auftretender Fehler zur Anzeige eines Fehlerdialogs und zum anschließenden Programmende führt, können Sie Ihren Code durch eine Try-Catch-Konstruktion absichern. In der einfachsten Form sieht die Syntax so aus: Try [Code, der eventuell einen Fehler auslösen könnte] Catch [Code, um auf den Fehler zu reagieren] End Try
Durch diese Konstruktion werden die Codezeilen zwischen Try und Catch ausgeführt. Wenn dabei kein Fehler auftritt, wird die Konstruktion anschließend verlassen und die nächste Anweisung nach End Try ausgeführt. Tritt hingegen irgendwo im Try-Block ein Fehler auf, wird dieser Block verlassen und der Catch-Block ausgeführt. Dieser Block eignet sich beispielsweise dazu: •
um Informationen über den Fehler in einer eigenen Dialogbox anzuzeigen,
•
um den Fehler in eine Protokolldatei einzutragen,
•
um eine E-Mail mit einem Fehlerbericht zu versenden,
•
um Objekte aus dem Speicher zu entfernen oder vergleichbare Aufräumarbeiten durchzuführen,
•
um eine neue Exception auszulösen, die Informationen über den Fehler an eine übergeordnete Prozedur weitergibt (z.B. in einer Klassenbibliothek),
•
um das Programm geordnet zu beenden.
HINWEIS
Wenn die Prozedur oder das Programm im Catch-Block nicht beendet wird, wird die Programmausführung anschließend fortgesetzt, als wäre nie ein Fehler aufgetreten. Der Fehler (die exception) gilt durch die Ausführung des Catch-Codes also automatisch als behoben bzw. als ausreichend verarbeitet – vollkommen unabhängig davon, was Ihr Programm im Catch-Code tatsächlich tut. Die Try-Catch-Syntax setzt voraus, dass innerhalb der Konstruktion zumindest ein Catch- oder Finally-Block (siehe unten) angegeben wird. Es ist aber erlaubt, dass der Catch-Block leer ist (d.h., die nächste Catch folgende Zeile lautet End Try). Das bewirkt, dass der Fehler ohne Fehlermeldung einfach ignoriert wird und die Codeausführung nach End Try fortgesetzt wird, als wäre nichts gewesen. Es liegt auf der Hand, dass eine derartige Absicherung selten sinnvoll ist und im Gegenteil das Auftreten von Fehlern nur verschleiert (ähnlich wie durch On Error Resume Next in VB6).
VORSICHT
480
11 Fehlersuche und Fehlerabsicherung
Fehler, die innerhalb des Catch-Blocks auftreten, sind durch die Try-Catch-Konstruktion nicht abgesichert! Stellen Sie also beim Programmieren sicher, dass Ihr CatchCode garantiert fehlerfrei ausgeführt wird, oder sichern Sie diesen Code zur Not durch eine verschachtelte Try-Catch-Konstruktion ab. (Allzu weit sollten Sie diese Art der Verschachtelung aber nicht treiben ...)
Differenzierte Reaktion auf Fehler In der obigen Form können Sie im Catch-Block nicht auf das Exception-Objekt zugreifen, das den Fehler beschreibt. Dieses Objekt benötigen Sie aber, wenn Sie eine differenzierte Fehlerbehandlung realisieren möchten. Dabei hilft die folgende Syntaxvariante: Try [Code, der eventuell einen Fehler auslösen könnte] Catch e As Exception [Code, um auf den Fehler zu reagieren] End Try
Nun können Sie innerhalb des Catch-Blocks über die lokale Variable e auf alle Eigenschaften des zum Fehler gehörenden Exception-Objekts zugreifen. Die folgenden Zeilen geben dazu ein konkretes Beispiel. Dim txt As String Dim sr As IO.StreamReader Try sr = New IO.StreamReader("diese_datei_gibt_es_nicht.txt") txt = sr.ReadToEnd() sr.Close() Console.WriteLine(txt) Catch e As Exception ' Datei schließen If Not IsNothing(sr) Then sr.Close() ' Informationen über den Fehler anzeigen Console.WriteLine("Exception-Name: " + e.GetType.Name) Console.WriteLine("Fehlertext: " + e.Message) Console.WriteLine("Fehlerort: " + e.StackTrace) End Try
Als weitere Syntaxvariante können mehrere Catch-Blöcke für unterschiedliche ExceptionKlassen angegeben werden. Wenn ein Fehler auftritt, werden alle Catch-Anweisungen der Reihe nach überprüft, bis eine Catch-Anweisung gefunden wird, die auf den Fehler zutrifft.
11.1 Fehlerabsicherung
481
Try sr = New IO.StreamReader("diese_datei_gibt_es_nicht.txt") txt = sr.ReadToEnd() sr.Close() Console.WriteLine(txt) Catch e As IO.FileNotFoundException ' Datei wurde nicht gefunden ... Catch e As Exception ' ein anderer Fehler ist aufgetreten ... End Try
Optional können Sie jede Catch-Anweisung mit When bedingung ergänzen. Dadurch wird der nachfolgende Codeblock nur ausgeführt, wenn die Bedingung erfüllt ist.
HINWEIS
Beachten Sie, dass Sie unbedingt zuerst spezifische und erst dann allgemeine Exception-Klassen angeben müssen (also in der Vererbungshierarchie der Exception-Klassen zuerst die abgeleiteten und erst dann die Basisklassen)! Da jede Exception-Klasse von System.Exception abgeleitet ist, trifft Catch e As Exception auf jeden Fehler zu. Wenn Catch e As Exception also am Beginn der Catch-Alternativen steht, trifft diese Variante immer zu und die anderen Catch-Alternativen werden gar nicht mehr berücksichtigt. Catch e As Exception sollte daher am Ende der Catch-Variante angegeben werden. Damit gilt der letzte Catch-Block als Sammelbecken für alle Fehler, deren genauen
Typ Sie nicht vorhergesehen haben bzw. die Sie nicht separat berücksichtigen wollten. Wenn innerhalb der Try-Catch-Konstruktion ein Fehler auftritt, der keiner der CatchVarianten entspricht, gilt dieser Fehler als unbehandelt und es kommt zur Anzeige der am Beginn dieses Kapitels abgebildeten Defaultfehlermeldung und zum anschließenden Programmende.
Finally-Block Am Ende der Try-Catch-Konstruktion kann ein Finally-Block angegeben werden. Der darin enthaltene Code wird sowohl nach der fehlerfreien Ausführung des Try-Blocks als auch nach dem Auftreten eines behandelten Fehlers im Anschluss an den Catch-Block abgearbeitet. Wenn dagegen ein unbehandelter Fehler auftritt, wird die gesamte Try-Catch-Konstruktion verlassen und zumeist auch der Finally-Block ignoriert.
482
11 Fehlersuche und Fehlerabsicherung
Try [Code, der eventuell einen Fehler auslösen könnte] Catch ... [Code, um auf den Fehler zu reagieren] Finally [Code, der immer ausgeführt wird] End Try
Syntaktisch gesehen ist der Finally-Block weitgehend sinnlos. Sie können den Code des Finally-Blocks ebenso gut nach End Try angeben, also: Try [Code, der eventuell einen Fehler auslösen könnte] Catch ... [Code, um auf den Fehler zu reagieren] End Try [Code, der immer ausgeführt wird]
HINWEIS
Die Berechtigung des Finally-Blocks besteht in erster Linie wohl darin, dass logisch zusammengehörender Code damit vollständig innerhalb der Try-Catch-Konstruktion angeordnet werden kann; das kann die Lesbarkeit des Programms ein wenig verbessern. Es gibt einige wenige Exceptions, die vom gewöhnlichen Verhalten abweichen. Dazu zählt die Threading.ThreadAbortException: Wenn diese Exception innerhalb eines TryBlocks auftritt, wird der Finally-Block selbst dann ausgeführt, wenn die Exception nicht in einem Catch-Block abgefangen wird bzw. wenn es gar keinen Catch-Block gibt. Weitere Informationen zu dieser Exception folgen in Abschnitt 12.6.2.
Syntaxzusammenfassung Try-Catch-Syntax Try ...
führt die Codezeilen bis zur ersten Catch-Anweisung aus. Wenn dabei kein Fehler auftritt, wird der (optionale) Finally-Block ausgeführt.
Catch e As xxxException [When cond] falls im Try-Block ein Fehler des Typs xxxException ... aufgetreten ist (und die optionale Bedingung cond
erfüllt ist), wird zuerst dieser Block und dann der Finally-Block ausgeführt. Über die lokale Variable e kann auf das Exception-Objekt zugegriffen werden. Catch [e As Exception] ...
berücksichtigt alle Fehler, die bisher nicht explizit behandelt wurden.
Finally ...
wird in jedem Fall ausgeführt, unabhängig davon, ob ein behandelter Fehler aufgetreten ist oder nicht.
End Try
beendet die Try-Catch-Konstruktion.
11.1 Fehlerabsicherung
483
11.1.3 Programmiertechniken Minimalabsicherung für Konsolenanwendungen Try/Catch gilt auch für alle untergeordneten Prozeduren, die Sie aufrufen. Wenn es sich
beim folgenden Programm um eine Konsolenanwendung handelt, deren gesamter Code über myFunction aufgerufen wird, so ist das gesamte Programm abgesichert – wenn auch nur notdürftig. Sobald in myFunction oder in irgendeiner anderen Prozedur, die von dort aus ausgerufen wurde, ein Fehler auftritt, wird der Catch-Block von Main ausgeführt. Dort können Sie dann eine Fehlermeldung anzeigen und das Programm geordnet beenden. Sub Main() Try myFunction() Catch MsgBox("error") End Try End Sub
Absicherung von eigenen Klassen Schon wesentlich aufwendiger ist es, eine selbst programmierte Klasse abzusichern. Es gibt leider keine Möglichkeit, eine zentrale Fehlerbehandlungsprozedur einzurichten, die automatisch immer dann aufgerufen wird, wenn in irgendeinem Codeabschnitt der Klasse ein nicht abgesicherter Fehler auftritt. Daher müssen Sie jede Prozedur (Methode, Eigenschaft etc.) der Klasse einzeln absichern, was bei umfangreichen Klassen natürlich sehr mühsam ist.
Absicherung von Windows-Programmen Jedes Formular (Fenster) eines Windows-Programms ist eine eigene Klasse. Daher gibt es keinen Unterschied zwischen der Absicherung eines Windows-Programms und der einer Klasse: Sie müssen jede Prozedur (und insbesondere jede Ereignisprozedur, die als Reaktion auf Benutzereingaben, Mausklicks etc. aufgerufen wird) explizit absichern. Damit Sie nicht immer wieder denselben Code zur Fehlerabsicherung in den Catch-Blöcken der diversen Prozeduren angeben müssen, können Sie eine eigene Prozedur zur Fehlerbehandlung aufrufen. ' jede (Ereignis-)Prozedur einzeln absichern Sub prozedur1, 2, 3, 4 ...() Try ... normaler Code Catch e As Exception myErrorCode(e) End Try End Sub
484
11 Fehlersuche und Fehlerabsicherung
ACHTUNG
Function myErrorCode(e As Exception) As Integer .. Code zur Fehlerabsicherung und evt. zum Programmende MsgBox("error") End Function
Je nach Anwendung können Sie dem Anwender in myErrorCode die Möglichkeit geben, das Programm zu beenden oder zu versuchen, es fortzusetzen. Wenn das Programm in myErrorCode nicht beendet wird, wird es bei der Zeile End Try in der den Fehler verursachenden Prozedur fortgesetzt. Das ist aber nicht immer sinnvoll. Ein differenzierteres Verhalten können Sie beispielsweise erreichen, indem Sie die weitere Programmausführung vom Rückgabewert der myErrorCode-Funktion abhängig machen.
Fehlerabsicherung absichern Wenn innerhalb eines Catch-Blocks ein weiterer Fehler auftritt, gilt dieser Fehler als ganz normaler Fehler, der zu einer Programmunterbrechung führt! Sie können aber natürlich wahlweise den Catch-Block durch eine weitere Try-Konstruktion (Variante 1) oder den gesamten Try-Block zweifach absichern (Variante 2). ' Variante 1 Try ... normaler Code Catch Try ... Code zur Fehlerbehandlung Catch ... Code, wenn in der Fehlerbehandlung ein Fehler auftritt End Try End Try ' Variante 2 Try Try ... normaler Code Catch ... Code zur Fehlerbehandlung End Try Catch ... Code, wenn in der Fehlerbehandlung ein Fehler auftritt End Try
11.1 Fehlerabsicherung
485
Absicherung von IDisposable-Objekten Wenn Sie Objekte erzeugen, deren Klasse die IDisposable-Schnittstelle implementieren, sollten Sie bei der Fehlerabsicherung darauf achten, dass das Objekt ordnungsgemäß aus dem Speicher entfernt wird. Die folgenden Zeilen geben hierfür ein Beispiel. (Sowohl die Bitmap- als auch die Graphics-Klasse sind IDisposable-Klassen.) Dim bm As Drawing.Bitmap Dim gr As Drawing.Graphics Try bm = New Drawing.Bitmap(100, 100) gr = Drawing.Graphics.FromImage(bm) gr.DrawLine(Drawing.Pens.Black, 0, 0, 50, 50) Catch If Not IsNothing(gr) Then gr.Dispose() If Not IsNothing(bm) Then bm.Dispose() End Try
Exception-Objekt modifizieren oder weitergeben Was ist eigentlich das Ziel der Fehlerabsicherung? Bei interaktiven Programmen soll zumeist eine verständliche Fehlermeldung angezeigt werden. Außerdem sollte dem Anwender eine Möglichkeit gegeben werden, das Programm entweder fortzusetzen oder es zumindest kontrolliert zu beenden. Wenn Sie dagegen eigene Klassen entwickeln, die möglicherweise von nichtinteraktiven Programmen genutzt werden sollen (z.B. von Server-Diensten), sieht die Zielsetzung ganz anders aus: Soweit der Fehler innerhalb des Fehlerbehandlungscodes nicht umgangen werden kann, soll eine möglichst präzise Information über den Fehler in Form eines Exception-Objekts an das Programm zurückgegeben werden, das die Klasse nutzt. Dabei gibt es wiederum zwei Möglichkeiten, wie ein bereits aufgetretener Fehler innerhalb des Fehlerabsicherungscodes weitergegeben werden kann: Entweder indem das ExceptionObjekt durch Throw unverändert neu ausgelöst wird; oder indem ein neues Exception-Objekt erzeugt wird, das zusätzliche Kontextinformationen zum Fehler angibt und via InnerException auf den ursprünglichen Fehler verweist. Das folgende Beispiel veranschaulicht beide Vorgehensweisen. class1 ist eine Klasse, in der zwei Integer-Werte x und y gespeichert werden. Die Methode quot berechnet den Quotienten der beiden Zahlen. Wenn dabei eine Division durch 0 auftritt, wird der Sonderfall x=0 und y=0 berücksichtigt: In diesem Fall wird statt eines Fehlers das Resultat 1 zurückgegeben. In allen anderen Fällen wird der Fehler durch Throw einfach neu ausgelöst. (Wenn in quot ein anderer Fehler auftritt, der durch Catch DivideByZeroException nicht erfasst wird, wird dieser Fehler automatisch an die aufrufende Stelle im Code weitergegeben, ohne dass hierfür eigener Code erforderlich ist.)
486
11 Fehlersuche und Fehlerabsicherung
' Beispiel fehler\modify-exception Class class1 Public x, y As Integer Public Sub New(ByVal x As Integer, ByVal y As Integer) Me.x = x Me.y = y End Sub Public Function quot() As Integer Try Return x \ y Catch e As DivideByZeroException If x = 0 And y = 0 Then ' für diese Klasse gilt: 0 / 0 = 1 Return 1 Else ' Exception neuerlich auslösen Throw e End If End Try End Function End Class class2 hat dieselbe Aufgabe wie class1 und unterscheidet sich nur durch die Fehlerabsicherung: Der Sonderfall x=0 und y=0 wird hier bereits vor der Division kontrolliert und führt
(ohne einen Fehler auszulösen) zur Rückgabe von 1. Sollte dagegen bei der Integerdivision x \ y ein beliebiger Fehler auftreten, dann wird ein neues Exception-Objekt erzeugt und der Fehler mit Throw ausgelöst. Dabei werden als Message-Eigenschaft des Objekts einige Kontextinformationen übergeben: der Ort des Fehlers und der Inhalt von x und y. Außerdem wird das ursprüngliche Exception-Objekt als zweiter New-Parameter übergeben. Es kann dann aus der InnerException-Eigenschaft des neuen Exception-Objekts gelesen werden. Class class2 '[ ... x, y und New() wie in class1 ...] Public Function quot() As Integer Try ' für diese Klasse gilt: 0 / 0 = 1 If x = 0 And y = 0 Then Return 1 Return x \ y Catch e As Exception ' Exception neuerlich auslösen Dim msg As String msg = String.Format( _ "Fehler in Methode class2.quot. x={0}, y={1}", x, y) Throw New Exception(msg, e) End Try End Function End Class
11.1 Fehlerabsicherung
487
Die folgenden Zeilen zeigen die (abgesicherte) Anwendung der beiden Klassen. Zur Anzeige der Fehlertexte wird die Funktion AllExceptionMessages verwendet, die mit InnerException alle Fehler bis zur ursprünglichen Exception durchläuft. Module Module1 Sub Main() Dim a As New class1(0, 0) Console.WriteLine("a.quot() = {0}", a.quot())
'kein Fehler
Dim b As New class1(1, 0) Try Console.WriteLine("b.quot() = {0}", b.quot()) Catch e As Exception Console.WriteLine(AllExceptionMessages(e)) End Try Dim c As New class2(1, 0) Try Console.WriteLine("c.quot() = {0}", c.quot()) Catch e As Exception Console.WriteLine(AllExceptionMessages(e)) End Try End Sub Public Function AllExceptionMessages(ByVal e As Exception) As String Dim msg As String Dim indent As Integer Dim innerex As Exception = e.InnerException msg = e.GetType().Name + ": " + e.Message While Not IsNothing(innerex) indent += 2 msg += vbCrLf + Space(indent) msg += innerex.GetType().Name + ": " + innerex.Message innerex = innerex.InnerException End While msg += vbCrLf Return msg End Function End Module
Das Beispielprogramm liefert folgendes Ergebnis: a.quot() = 1 DivideByZeroException: Es wurde versucht, durch null zu teilen. Exception: Fehler in Methode class2.quot. x=1, y=0 DivideByZeroException: Es wurde versucht, durch null zu teilen.
488
11 Fehlersuche und Fehlerabsicherung
11.1.4 On-Error-Syntax
HINWEIS
Zur Fehlerabsicherung können Sie statt Try-Catch-Konstruktionen auch die aus VB6 stammende On-Error-Anweisungen verwenden. Der Nachteil des On-Error-Konzepts besteht darin, dass es auf Goto-Kommandos basiert, also auf Sprüngen im Code. Das führt oft zu einem schwer nachvollziehbaren Codeablauf, weswegen Sie nach Möglichkeit auf OnError-Anweisungen verzichten sollten. Innerhalb einer Prozedur können Sie entweder On Error oder Try-Catch zur Fehlerabsicherung verwenden, aber nicht beide Konstruktionen gleichzeitig!
Das Grundkonzept von On Error ist in der Prozedur sub1 demonstriert: Die Anweisung On Error Goto label bewirkt, dass das Programm beim Auftreten eines Fehlers beim angegebenen Sprunglabel fortgesetzt wird. (Ein Sprunglabel wird durch einen Text mit einem nachfolgenden Doppelpunkt gekennzeichnet. Der Label muss sich innerhalb der Prozedur befinden.) Im Fehlerbehandlungscode können die Details des Fehlers aus einem Objekt der Klasse Microsoft.VisualBasic.ErrObject gelesen werden. Dieses Objekt ist über die Funktion Err zugänglich. Err.Number liefert eine VB6-kompatible Fehlernummer. (Err.GetException liefert übrigens das zum Fehler passende Exception-Objekt.)
Die Prozedur kann nun im Fehlerbehandlungscode verlassen werden – damit gilt der Fehler als behandelt. Sie können aber auch Resume ausführen, um die Prozedur an einem beliebigen weiteren Label fortzusetzen. (Dabei ist Vorsicht geboten: Wenn der Fehler nun abermals auftritt, gerät das Programm in eine Endlosschleife.) Als Variante zu Resume können Sie auch Resume Next ausführen, um das Programm in der Zeile nach der fehlerhaften Zeile fortzusetzen. ' Beispiel Sub sub1() Dim a, b, c As Integer On Error GoTo errorcode Console.WriteLine("in sub1:") tryagain: a = b \ c Console.WriteLine("a={0}", a) Exit Sub errorcode: ' Fehlerbehandlung Console.WriteLine(Err.Description) c = 1 Resume tryagain End Sub
11.1 Fehlerabsicherung
489
Noch verpönter als On Error Goto ist in VB-kritischen Kreisen die Anweisung On Error Resume Next. Diese Anweisung bewirkt, dass das Auftreten von Fehlern einfach ignoriert wird. Stattdessen wird das Programm mit der nächsten Zeile fortgesetzt. Wenn Sie möchten, können Sie am Ende des kritischen Blocks Err überprüfen, ob in der Zwischenzeit ein Fehler aufgetreten ist. Sub sub2() Dim a, b, c As Integer On Error Resume Next Console.WriteLine("in sub2:") a = b \ c Console.WriteLine("a={0}", a) ' Fehlerbehandlung If Not IsNothing(Err) Then Console.WriteLine(Err.Description) End If End Sub
Syntaxzusammenfassung On-Error-Syntax On Error Resume Next
ignoriert Fehler und setzt das Programm mit der nächsten Anweisung fort.
On Error Goto label
setzt das Programm beim Auftreten eines Fehlers an der Sprungmarke label fort.
Resume label
setzt das Programm bei der Sprungmarke label fort. Resume kann nur innerhalb des Fehlerbehandlungscodes ausgeführt werden.
Resume Next
setzt das Programm mit der nächsten Anweisung nach der fehlerhaften Anweisung fort. Auch Resume kann nur innerhalb des Fehlerbehandlungscodes ausgeführt werden.
Err
verweist auf ein Objekt der Klasse ErrObject, das Informationen über den Fehler enthält.
Err.GetException()
verweist auf das Exception-Objekt des Fehlers.
Err.Number
liefert eine VB6-kompatible Fehlernummer.
490
11.2
11 Fehlersuche und Fehlerabsicherung
Fehlersuche (Debugging)
Der Begriff Debugging bezeichnet die Suche nach Fehlern in einem Programm. Debugging betreiben Sie immer dann, wenn Ihr Programm nicht so funktioniert, wie Sie es erwarten, und Sie auf der Suche nach den Ursachen sind. Dieser Abschnitt gibt einen Überblick über die zu diesem Zweck vorgesehenen Hilfsmitteln in VB.NET und über den in die Entwicklungsumgebung integrierten Debugger. (Der Debugger ist kein isolierter Teil der Entwicklungsumgebung, sondern ein integrierter Bestandteil, der beispielsweise das zeilenweise Ausführen von Code ermöglicht.)
11.2.1 Grundlagen Debug- oder Release-Kompilat Die Debugging-Eigenschaften eines Programms hängen stark davon ab, ob es als Debugoder als Release-Kompilat erstellt wird. Wenn Sie Ihr Programm in der Entwicklungsumgebung erstellen, wird per Default die Debug-Variante erzeugt. Zwischen Debug- und ReleaseVariante können Sie mit ERSTELLEN|KONFIGURATIONSMANAGER, in den Projekteigenschaften oder mit in der Standardsymbolleiste wählen. Dieser Abschnitt setzt voraus, dass Sie zur Fehlersuche die Debug-Variante verwenden. Durch welche Eigenschaften sich das Debug- und Release-Kompilat unterscheiden, können Sie im Dialogblatt KONFIGURATIONSEIGENSCHAFTEN|ERSTELLEN der Projekteigenschaften einstellen (siehe Abbildung 11.4). Per Default unterscheidet sich ein Debug-Kompilat in folgenden Punkten von einem Release-Kompilat: •
Zusammen mit dem Kompilat wird eine zusätzliche Datei name.pdb erzeugt, die Debugging-Informationen enthält. Diese Informationen ermöglichen es dem in der Entwicklungsumgebung integrierten Debugger, Unterbrechungen bei der Programmausführung einer bestimmten Zeile zuzuordnen. Darüber hinaus können Sie das Programm im Debugger zeilenweise ausführen etc. Die Debugging-Datei ist also eine Grundvoraussetzung dafür, dass Sie den Debugger zur Fehlersuche verwenden können.
•
Die Konstante Debug wird vordefiniert. Diese Konstante kann durch #If debug Then ... ausgewertet werden, beispielsweise um einen Codeblock nur dann auszuführen, wenn das Programm in der Debug-Konfiguration kompiliert wurde. Gleichzeitig können alle Methoden und Eigenschaften der Klasse Debug verwendet werden. (Bei Release-Kompilaten werden alle Debug.Xxx-Anweisungen einfach ignoriert.)
Daneben können aber eine Reihe weitere Eigenschaften unterschiedlich eingestellt werden, z.B. das Verzeichnis, in dem das Kompilat erstellt wird, vordefinierte Konstanten etc.
TIPP
11.2 Fehlersuche (Debugging)
491
Im Konfigurationsmanager können Sie neben Debug und Release weitere Konfigurationen (Profile) definieren und deren Eigenschaften dann im Projekteigenschaftendialog einstellen.
Abbildung 11.4: Die Debug-Konfiguration im Projekteigenschaftendialog
Programmausführung unterbrechen
VERWEIS
Wenn Sie ein Programm in der Entwicklungsumgebung ausführen, können Sie die Ausführung mit DEBUGGEN|UNTERBRECHEN oder mit Strg+Untbr unterbrechen. Bei Konsolenanwendungen hat Strg+C dieselbe Wirkung. Diese beiden Tastenkombinationen sind beispielsweise dann praktisch, wenn sich Ihr Programm in einer Endlosschleife befindet. Bei Release-Kompilaten bewirken Strg+Untbr und Strg+C ein sofortiges Programmende. Es scheint keine Möglichkeit zu geben, Strg+Untbr oder Strg+C durch Catch-Try abzufangen. Zu Programmunterbrechungen kommt es auch, wenn im Programm ein unbehandelter Fehler (eine Exception) auftritt oder wenn im Programm ein Haltepunkt gesetzt ist. Exceptions wurden im vorigen Abschnitt dieses Kapitels ausführlich beschrieben, Informationen zu Haltepunkten folgen etwas weiter unten.
492
11 Fehlersuche und Fehlerabsicherung
Code verändern Wie in Kapitel 3 bereits beschrieben wurde, unterscheidet sich VB.NET in einem ganz elementaren Punkt von den Vorgängerversionen VB1 bis VB6 und VBA: Es ist nicht mehr möglich, in einem unterbrochenen Programm Code zu verändern. (Sie können die Entwicklungsumgebung zwar so einstellen, dass Änderungen erlaubt sind, diese werden aber erst wirksam, wenn das Programm neu kompiliert und gestartet wird.)
11.2.2 Fehlersuche in der Entwicklungsumgebung Haltepunkte Indem Sie eine Programmzeile am linken Rand anklicken, setzen Sie in dieser Zeile einen so genannten Haltepunkt (break point). In der Entwicklungsumgebung werden Haltepunkte durch einen roten Kreis neben der Programmzeile angezeigt. Wenn die mit einem Haltepunkt versehene Zeile bei der Programmausführung erreicht wird, wird das Programm unterbrochen. Sie haben nun die Möglichkeit, den Inhalt einzelner Variablen zu analysieren oder das Programm (eventuell nur zeilenweise) fortzusetzen. Neben einfachen Haltepunkten kennt die Entwicklungsumgebung auch Haltepunkte, die nur dann berücksichtigt werden, wenn eine bestimmte Bedingung zutrifft, oder nur dann, wenn die Zeile n Mal erreicht wird. Derartige Zusatzbedingungen können im Dialog HALTEPUNKTEIGENSCHAFTEN eingestellt werden. Dieser Dialog ist über das Kontextmenü des Haltepunkts oder über das Haltepunktfenster erreichbar. Das Haltepunktfenster (siehe Abbildung 11.5) hilft bei der Verwaltung aller Haltepunkte.
TIPP
Abbildung 11.5: Das Haltepunktfenster zur Verwaltung aller Haltepunkte
Wenn Haltepunkte in der Entwicklungsumgebung mit einem Fragezeichen angezeigt werden (Tooltip-Text: Der Haltepunkt wird momentan nicht erreicht. Für dieses Dokument wurden keine Symbole geladen.), dann haben Sie Ihr Programm in der Release-Konfiguration kompiliert. Haltepunkte werden aber nur in der Debug-Konfiguration unterstützt. Stellen Sie also auf die Debug-Konfiguration um und kompilieren Sie Ihr Programm neu!
11.2 Fehlersuche (Debugging)
493
Programm zeilenweise ausführen Nach einer Unterbrechung können Sie das Programm zeilenweise (also Anweisung für Anweisung) fortsetzen. Dazu können Sie entweder die Kommandos des DEBUGGEN-Menüs, die dort angegebenen Tastenkürzel oder die Buttons der DEBUGGEN-Symbolleiste verwenden. Die folgende Aufzählung beschreibt die verschiedenen Kommandos, die dabei zur Auswahl stehen: •
STARTEN/FORTSETZEN: setzt die Programmausführung unbegrenzt fort (bis der nächste
Fehler auftritt bzw. der nächste Haltepunkt erreicht wird). •
EINZELSCHRITT: führt nur die nächste Anweisung aus.
•
PROZEDURSCHRITT: führt ebenfalls nur die nächste Anweisung aus. Wenn es sich dabei
um den Aufruf einer selbst definierten Methode oder Prozedur handelt, wird diese Prozedur vollständig ausgeführt (und nicht Anweisung für Anweisung wie bei EINZELSCHRITT). •
AUSFÜHRUNG BIS RÜCKSPRUNG: führt die Prozedur bis an ihr Ende aus. Die Programmaus-
führung wird bei der nächsten Anweisung nach dem Aufruf der Prozedur wieder unterbrochen. •
AUSFÜHREN BIS CURSOR: führt die Prozedur aus, bis die Anweisung an der aktuellen Cursorposition erreicht ist.
TIPP
Obwohl es im Menü der Entwicklungsumgebung und in den Symbolleisten wirklich nicht an Kommandos mangelt, die man nie braucht, fehlt per Default ein Teil der hier beschriebenen Kommandos. Das können Sie mit EXTRAS|ANPASSEN aber schnell ändern. Das BEFEHLE-Dialogblatt enthält die vollständige Liste aller Debugging-Kommandos, die Sie einfach per Drag&Drop in das Menü einbauen können.
TIPP
Darüber hinaus können Sie mit NÄCHSTE ANWEISUNG FESTLEGEN den Punkt für die Programmfortsetzung beliebig festlegen. (Sie können also beispielsweise den Cursor an das Ende einer Schleife setzen, dort die NÄCHSTE ANWEISUNG FESTLEGEN und das Programm dann an dieser Stelle durch EINZELSCHRITT fortsetzen.)
In der Praxis werden Sie die oben beschriebenen Kommandos meistens per Tastatur ausführen. Leider sind die Tastenkürzel stark von der Konfiguration der Entwicklungsumgebung abhängig, weswegen ich hier auf eine Referenz verzichte. Die gerade gültigen Tastenkürzel werden im DEBUGGEN-Menü angezeigt.
Befehlsfenster Während das Programm unterbrochen ist, können Sie im Befehlsfenster (ANSICHT|ANDERE FENSTER|BEFEHLSFENSTER) einfache VB-Kommandos ausführen. Das Fenster eignet sich inbesondere dazu, um Prozeduren aufzurufen oder den Wert einer Variablen mit ? anzuzeigen.
VERWEIS
494
11 Fehlersuche und Fehlerabsicherung
VB-Kommandos können nur dann ausgeführt werden, wenn sich das Befehlsfenster im so genannten unmittelbaren Modus befindet. Wenn das nicht der Fall ist, gelangen Sie mit dem Kommando immed in diesen Modus (siehe auch Abschnitt 1.3.6).
Überwachungsfenster Während das Programm unterbrochen ist, wollen Sie normalerweise wissen, welchen Inhalt die gerade aktuellen Variablen haben. Die Entwicklungsumgebung stellt gleich eine ganze Reihe von Fenstern zur Auswahl, um diese Informationen zu ermitteln. Diese Fenster können mit DEBUGGEN|FENSTER|NAME geöffnet werden: •
AUTO: Das Fenster (siehe Abbildung 11.6) zeigt die Inhalte aller Variablen an, die in der aktuellen Zeile und in den umgebenden Anweisungen benutzt werden.
•
LOKAL: Dieses Fenster zeigt alle Variablen an, die innerhalb der aktuellen Prozedur lokal definiert sind. (Dazu zählen auch alle Parameter der Prozedur.)
•
ME: Das Fenster zeigt nur das Objekt Me an, das auf die aktuelle Objektinstanz verweist.
•
ÜBERWACHEN 1 bis 4: In diese vier Fenster können manuell einzelne Variablen zur Über-
wachung eingefügt werden. (Dazu klicken Sie die Variable mit der rechten Maustaste an und führen das Kommando ÜBERWACHUNG HINZUFÜGEN aus.) •
SCHNELLÜBERWACHUNG: Dieser Dialog zeigt nur den Inhalt einer einzigen Variablen an. Der Dialog kann durch Anklicken der Variablen mit der rechten Maustaste über ein Kontextmenükommando geöffnet werden.
Alle Fenster zeichnen sich durch einige gemeinsame Merkmale aus: So werden die Inhalte von Variablen, die sich durch die letzte Anweisung geändert haben, rot dargestellt. Zahlenwerte können per Kontextmenü wahlweise dezimal oder hexadezimal angezeigt werden. Vor Objekten wird ein Pluszeichen angezeigt, mit dem das Objekt auseinandergeklappt werden kann. Damit können die Eigenschaften von Objekten angezeigt werden. Wenn diese auf andere Objekte verweisen, wiederholt sich die Vorgehensweise, so dass ein ganzer Objektbaum angezeigt werden kann. (Das wird allerdings rasch sehr unübersichtlich.)
Aufrufliste Das Fenster AUFRUFLISTE zeigt an, wie die aktuelle Codeposition erreicht worden ist. In Abbildung 11.7 wurde beispielsweise in Module1.Main die Methode ItemsText eines Objekts der Klasse LinkedList ausgeführt. Innerhalb des Codes dieser Methode wurde wiederum die Eigenschaft Value gelesen (was im Aufruffenster als get_Value dargestellt wird).
11.2 Fehlersuche (Debugging)
495
Abbildung 11.6: Das Auto-Fenster mit den Variablen, die in der aktuellen Anweisung und den umliegenden Anweisungen benutzt werden
Durch einen Doppelklick innerhalb der Aufrufliste können Sie den aktuellen Gültigkeitsbereich (Kontext) verändern. Wenn Sie den Kontext beispielsweise auf Main() schalten, können Sie feststellen, welchen Inhalt die Variablen in Main hatten, als die Methode ItemsText ausgeführt worden ist. (Das Programm kann aber unabhängig vom Kontext nur in der höchsten Ebene der Aufrufliste fortgesetzt werden.) Bei Ereignisprozeduren von Windows-Anwendungen oder bei rekursiven Algorithmen kann die Aufrufliste sehr lang sein. Außerdem kann die Aufrufliste Prozedur- und Methodenaufrufe von .NET-Klassen enthalten. Derartige Einträge sind in grauer Schrift dargestellt. Der dazugehörige Code kann dann nur als Assembler-Code angezeigt werden; dieser Code ist aber nur interessant, wenn Sie über ein umfassendes Assembler- und .NETHintergrundwissen verfügen.
Abbildung 11.7: Die Aufrufliste zeigt die Hierarchie der Prozedur- und Methodenaufrufe an
496
11 Fehlersuche und Fehlerabsicherung
Threads-Fenster Wenn Sie Multithreading-Anwendungen erstellen, können Sie im Threads-Fenster zwischen den Threads Ihres Programms wechseln und das Programm gezielt in einem bestimmten Thread fortsetzen. Außerdem können Sie dort einen Thread sperren. Das bedeutet, dass dieser Thread nicht mehr ausgeführt wird, bis die Sperre aufgehoben wird.
Ausnahmen-Fenster Per Default wird das Programm bei jeder nicht abgesicherten Ausnahme unterbrochen. Dieses Verhalten können Sie im Dialog DEBUGGEN|AUSNAHMEN verändern. Dort können Sie für jede Exception-Klasse bzw. für jede Gruppe derartiger Klassen (z.B. für alle System.IOFehler) festlegen, wie sich der Debugger verhalten soll, wenn ein Fehler auftritt. Dabei gibt es folgende Varianten: •
Beim Auftreten des Fehlers kann das Programm sofort unterbrochen werden. (Das ist die Defaulteinstellung für die Gruppe Common Language Runtime Exceptions.)
•
Der Fehler wird vorerst ignoriert.
•
Es gilt die Einstellung der übergeordneten Gruppe. (Das ist die Defaulteinstellung für fast alle Fehler.)
Sofern der Fehler beim Auftreten ignoriert wurde (und nur dann), kann in der zweiten Optionsgruppe eingestellt werden, wie sich der Debugger verhalten soll, wenn der Fehler nicht durch On-Error oder Catch-Try abgesichert wurde: Der Debugger wird jetzt gestartet. (Das ist die Defaulteinstellung für die Gruppe Common Language Runtime Exceptions.)
•
Der Fehler wird weiterhin ignoriert.
•
Es gilt die Einstellung der übergeordneten Gruppe. (Das ist die Defaulteinstellung für fast alle einzelnen Fehler.) HINWEIS
•
In den obigen Punkten wurden auch die Defaulteinstellungen für die meisten Exceptions beschrieben. Beachten Sie aber, dass es vereinzelte Ausnahmen gibt! Eine davon ist Threading.ThreadAbortException. Dort sind beide Optionsfelder auf WEITER voreingestellt (siehe auch Abschnitt 12.6.2).
Falls Ihnen die Tragweite dieses Dialogs noch nicht ganz klar sein sollte: Wenn Sie Common Language Runtime Exceptions anklicken und eine der Optionen verändern, gilt diese Einstellung für alle vordefinierten .NET-Fehler. Wenn Sie angeben, dass sofort beim Auftreten eines Fehlers der Debugger gestartet werden soll (wie dies in Abbildung 11.8 für System.ArgumentExceptions der Fall ist), dann erfolgt immer, wenn dieser Fehler auftritt, sofort eine Programmunterbrechung. Das gilt auch dann, wenn der Fehler eigentlich abgesichert wäre; und auch dann, wenn der Fehler in einer .NET-Bibliothek (und nicht in Ihrem Programm) auftritt!
TIPP
11.2 Fehlersuche (Debugging)
497
Manchmal tritt ein Fehler nicht unmittelbar in Ihrem Code auf, sondern in einer .NET-Bibliothek. (Es kann trotzdem sein, dass Sie an dem Fehler direkt oder indirekt schuld sind, weil Sie einen falschen Parameter übergeben haben, ein noch benötigtes Objekt gelöscht haben etc.) Derartige Fehler sind sehr schwer zu lokalisieren. Versuchen Sie in solchen Fällen, einfach beim Auftreten aller Fehler sofort in den Debugger zu springen. Dazu markieren Sie Common Language Runtime Exceptions und aktivieren die erste Option. Manchmal gelingt es mit dieser Radikalmaßnahmen die Ursache des Problems zu erkennen.
Abbildung 11.8: Einstellung der Reaktion auf Fehler
11.2.3 Debugging-Anweisungen im Code Sie können in Ihren Programmcode verschiedene Anweisungen einbauen, die bei der Fehlersuche helfen: •
Stop unterbricht die Programmausführung und führt zurück in die Entwicklungsumgebung. Dort können Sie das Programm mit DEBUGGEN|WEITER fortsetzen.
498
11 Fehlersuche und Fehlerabsicherung
Stop hat eine ähnliche Wirkung wie ein Haltepunkt, lässt sich aber bisweilen einfacher steuern, etwa in Kombination mit If-Abfragen: If i = 5 Then Stop.
Bei Release-Kompilaten führt Stop ebenfalls zu einer Unterbrechung. Der Anwender kann an dieser Stelle entweder einen Debugger starten oder das Programm fortsetzen (Button NEIN). •
Die Debug-Klasse stellt eine Reihe von Eigenschaften und Methoden zur Verfügung, die bei der Anzeige von Debugging-Informationen in der Entwicklungsumgebung hilfreich sind. Die Ausgaben können während oder nach der Programmausführung im Ausgabefenster angesehen werden (ANSICHT|ANDERE FENSTER|AUSGABEFENSTER). Bei Release-Kompilaten werden Debug-Methoden ignoriert.
•
Zwischen den Zeilen #If Debug Then ... und #End Debug können Sie Code einbauen, der nur dann ausgeführt wird, wenn das Programm als Debug-Kompilat erstellt wird (siehe auch Abschnitt 1.3.5).
Debug-Ausgaben
HINWEIS
Mit den Methoden Debug.Write und WriteLine können Sie Informationen über den Zustand Ihres Programms in das Ausgabefenster schreiben. Als Parameter kann entweder eine Zeichenkette oder ein Objekt übergeben werden, bei dem dann die ToString-Methode ausgewertet wird. Die Methoden WriteIf und WriteLineIf funktionieren wie Write[Line], allerdings erfolgt die Ausgabe nur, wenn die im ersten Parameter angegebene Bedingung erfüllt ist. Debug.Write[Line] ist im Gegensatz zu Console.Write[Line] leider nicht in der Lage, Formatanweisungen zu interpretieren. Debug.WriteLine("abc = {0}", abc) führt also nicht zu der von Console.WriteLine vertrauten Ausgabe. Sie müssen die Ausgabe stattdessen in der Form Debug.WriteLine("abc = " + abc.ToString()) durchführen.
Die Methode Indent bewirkt, dass alle weiteren Textausgaben eingerückt werden. Wenn Indent mehrfach ausgeführt wird, erhöht sich das Außmaß der Einrückung. Unindent reduziert das Maß der Einrückung. IndentLevel gibt an, um wie viele Stufen (Ebenen) Text momentan eingerückt wird. IndentSize gibt an, um wie viele Zeichen der Text pro IndentEbene eingerückt wird (per Default 4).
Debug-Ausgaben umleiten Per Default erfolgen die Debug-Ausgaben nur in das Ausgabefenster der Entwicklungsumgebung. Mit Listener.Add können Sie weitere TextWriterTraceListener-Objekte angeben. An die New-Methode dieser Klasse müssen Sie ein IO.Stream-Objekt übergeben. Auf diese Weise können Sie die Debug-Ausgaben beispielsweise auch im Konsolenfenster (Console.Out) anzeigen oder in einer Datei speichern: Dim fname As String = IO.Path.GetTempFileName() Debug.Listeners.Add(New TextWriterTraceListener(Console.Out))
11.2 Fehlersuche (Debugging)
499
Debug.Listeners.Add( _ New TextWriterTraceListener(New IO.StreamWriter(fname)))
Assertion Mit der Methode Assert (to assert: behaupten, erklären, Anspruch oder Recht geltend machen) können Sie eine beliebige Bedingung testen. Debug.Assert(Not IsNothing(parameter), "fehlermeldung")
Wenn die Bedingung nicht (!) erfüllt ist, wird die in Abbildung 11.9 dargestellte Nachrichtenbox angezeigt. Mit den drei überdurchschnittlich unsinnig beschrifteten Buttons können Sie das Programm anschließend beenden, debuggen oder fortsetzen. Gleichzeitig wird eine Information über den festgestellten Fehlerzustand ins Ausgabefenster bzw. in die entsprechende Datei geschrieben. Assert-Anweisungen eignen sich beispielsweise dazu, um die Parameter von Prozeduren oder Methoden zu überprüfen oder um während der Programmentwicklung andere unzulässige Zustände festzustellen. Assert-Fehlermeldungen sind keine Exceptions und können daher nicht durch Catch-Try abgefangen werden. Assert-Anweisungen werden in ReleaseKompilaten ignoriert.
Abbildung 11.9: Assertion-Nachricht
Mit der Methode Fail können Sie eine Assertion-Fehlermeldung ausgeben. Im Unterschied zu Assert gibt es bei Fail keinen Parameter für die Bedingung, so dass die Fehlermeldung immer angezeigt wird.
Syntaxzusammenfassung Debug-Klasse – Methoden und Eigenschaften Assert(bedingung [, text])
zeigt eine Assertion-Fehlermeldung an, wenn die Bedingung nicht (!) erfüllt ist. Anschließend kann das Programm beendet oder fortgesetzt werden.
Close()
schließt Dateien für Debug-Ausgaben.
Fail(text)
zeigt eine Assertion-Fehlermeldung an.
500
11 Fehlersuche und Fehlerabsicherung
Debug-Klasse – Methoden und Eigenschaften Flush()
speichert noch gepufferte Ausgaben in den Ausgabedateien.
Indent()
rückt alle weitere Ausgaben um eine Ebene ein.
IndentLevel
gibt an, um wie viele Ebenen Ausgaben eingerückt werden.
IndentSize
gibt an, um wie viele Zeichen Ausgaben pro Ebene eingerückt werden.
Listeners
verweist auf die Liste aller TraceListener-Objekte, an die Fehlermeldungen geschrieben werden (per Default nur das Ausgabefenster).
Unindent()
reduziert die Einrückung für weitere Ausgaben.
Write[Line](text)
schreibt den Text in das Ausgabefenster.
Write[Line]If(bedingung, text)
schreibt den Text in das Ausgabefenster, wenn die angegebene Bedingung erfüllt ist.
11.2.4 Fehlersuche in Windows.Forms-Programmen
HINWEIS
Bei Windows-Programmen kann es vorkommen, dass beim Auftreten eines Fehlers weder die tatsächliche Fehlerursache angegeben noch die Zeile gekennzeichnet wird, die den Fehler verursacht hat. Noch schlimmer: In seltenen Fällen kommt es trotz eines Fehlers zu gar keiner Fehlermeldung; die Ausführung der Prozedur wird einfach abgebrochen. In so einem Fall ist es schon recht schwierig, auch nur festzustellen, dass überhaupt ein Fehler vorliegt (geschweige denn, diesen auch zu finden). Ich hatte mehrfach das Problem, dass sich einzelne Debugging-Fenster einfach nicht öffnen ließen. Grundsätzlich gilt, dass viele Fenster erst dann geöffnet werden, wenn gerade ein Programm unterbrochen ist. Wenn diese Voraussetzung erfüllt ist und es dennoch nicht klappt, kann es sein, dass die Entwicklungsumgebung mit der Fensteranordnung so durcheinander gekommen ist, dass das Fenster nicht geöffnet werden kann. Abhilfe brachte der Button FENSTERLAYOUT ZURÜCKSETZEN im ersten Dialogblatt von EXTRAS|OPTIONEN. (Damit verlieren Sie alle eigenen Einstellungen der Entwicklungsumgebung, die die Fensteranordnung betreffen.)
Beispiel 1 Ein Windows-Programm mit einem PictureBox-Steuerelement enthält die folgende (fehlerhafte) Ereignisprozedur PictureBox1_Paint:
11.2 Fehlersuche (Debugging)
501
' Beispiel fehler\gdi-debugging Public Class Form1 Inherits System.Windows.Forms.Form [ ... Vom Windows Form Designer generierter Code ...] Private Sub PictureBox1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics gr.DrawLine(Pens.Red, 0, 0, 100, 100) gr.Dispose() 'Achtung, Fehler! End Sub End Class
Wenn das Programm ausgeführt wird, tritt in der ersten Zeile (Public Class Form1) der Fehler System.ArgumentException auf. Was ist passiert? Das Problem ist die Zeile gr.Dispose, mit dem das aus dem e-Parameter stammende Graphics-Objekt gelöscht wird. Das ist nicht erlaubt, weil Sie das Objekt nicht selbst erzeugt haben und es intern offensichtlich noch benötigt wird. Der Fehler tritt aber erst nach dem Ende der Ereignisprozedur im internen Code der .NET-Klassen auf und kann daher nicht lokalisiert werden. Es scheint keine Möglichkeit zu geben, derartige Fehler gezielt zu suchen. Beim vorliegenden Beispiel fällt der Verdacht natürlich sofort auf PictureBox1_Paint (weil das die einzige Ereignisprozedur des Programms ist), bei größeren Anwendungen müssen Sie sich aber wohl auf Ihre Intuition verlassen. Wenn Sie in PictureBox1_Paint einen Haltepunkt setzen und das Programm Zeile für Zeile ausführen, werden Sie bemerken, dass der Fehler unmittelbar nach dem Aufruf der Dispose-Methode auftritt.
Beispiel 2 Das zweite Beispiel entstand beim Versuch, Drag&Drop für Listenfelder zu realisieren (siehe auch Abschnitt 15.12.4). Um den Fehler nachzuvollziehen, klicken Sie im rechten Listenfeld einen Wochentag an und verschieben den Eintrag – ohne die Maustaste dazwischen loszulassen! – in das linke Listenfeld mit den Farben. Es passiert nichts, d.h., der Wochentag wird nicht in das andere Listenfeld verschoben. Es tritt aber auch keine Fehlermeldung auf. Beenden Sie das Programm, führen Sie DEBUGGEN|AUSNAHMEN aus und aktivieren Sie für alle Ausnahmen die Option WENN DIE AUSNAHME AUSGELÖST WIRD|IN DEN DEBUGGER SPRINGEN. Nun führen Sie exakt den gleichen Vorgang aus wie zuvor. Diesmal tritt in der Zeile n = ListBox2.SelectedIndices(i) der Fehler IndexOutOfRangeException auf. Wie ein Blick in das Aufruffenster beweist (siehe Abbildung 11.11), ist dieser Fehler nicht im VB-Code aufgetreten, sondern in der Windows.Forms-Bibliothek bei der Ausführung einer internen GetEntryObject-Methode.
502
11 Fehlersuche und Fehlerabsicherung
Mit anderen Worten: An diesem Fehler sind nicht Sie schuld, sondern die Programmierer der .NET-Bibliotheken. (Der Ereignisfluss bei dieser Drag&Drop-Operation bringt offensichtlich die interne Verwaltung der ausgewählten Elemente des Listenfelds durcheinander. Es kann natürlich sein, dass dieser Fehler bei künftigen .NET-Versionen behoben wird und sich dann nicht mehr reproduzieren lässt.)
Abbildung 11.10: Drag&Drop-Beispielprogramm
' Beispiel fehler/drag-and-drop-debugging Private Sub ListBox1_DragDrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListBox1.DragDrop Dim i, n As Integer If e.Data.GetDataPresent(ListBox1.GetType()) Then ' ausgewählte Einträge kopieren und löschen If ListBox2.SelectedIndices.Count > 0 Then For i = ListBox2.SelectedIndices.Count - 1 To 0 Step -1 n = ListBox2.SelectedIndices(i) 'Fehler! ListBox1.Items.Add(ListBox2.Items(n)) ListBox2.Items.Remove(ListBox2.Items(n)) Next End If End If End Sub
11.2 Fehlersuche (Debugging)
Abbildung 11.11: Die Aufrufliste beim Auftreten des Fehlers
503
12 Spezialthemen Dieses Kapitel gibt eine Einführung in eine Reihe von Spezialthemen bzw. in etwas exotischere Klassen der .NET-Bibliothek. Ob Sie Informationen über das Betriebssystem bzw. seine Umgebungsvariablen ermitteln, API-Funktionen aufrufen, eigene Programme durch Multithreading effizienter strukturieren oder fremde Programme starten oder per Automation steuern möchten – dieses Kapitel hilft Ihnen bei den ersten Schritten. 12.1 12.2 12.3 12.4 12.5 12.6 12.7
Ein- und Ausgabeumleitung (Konsolenanwendungen) Systeminformationen ermitteln Sicherheit Externe Programme starten Externe Programme steuern (Automation) Multithreading API-Funktionen verwenden (Declare)
506 507 514 516 519 528 546
506
12.1
12 Spezialthemen
Ein- und Ausgabeumleitung (Konsolenanwendungen)
Zirka zwei Drittel aller Beispielprogramme dieses Buchs sind Konsolenanwendungen. Der Grund dafür ist einfach: Die Struktur derartiger Programme ist leicht zu verstehen, der Overhead zur Demonstration eines einfachen Effekts sehr gering. Die Grundprinzipien von Konsolenanwendungen und die beiden Methoden Console.Write[Line] und .Read[Line] wurden in Abschnitt 1.1 bereits einleitend vorgestellt.
Standardein- und ausgabe An dieser Stelle geht es um eine seltener genutze Funktion der Klasse Console (Namensraum System, Bibliothek mscorlib.dll). Wie in C-Programmen schon seit rund 20 Jahren, können Sie nun endlich auch in VB.NET auf die Standardein- und ausgabekanäle zugreifen. Das ermöglicht die Entwicklung von Konsolenprogrammen, die wie unter Unix das Ergebnis eines anderen Programms verarbeiten. Falls Ihnen die Möglichkeiten der Standardeingabe bzw. -ausgabe unbekannt sind, sollten Sie zuerst einige Experimente in einem Eingabeaufforderungsfenster durchführen: dir /b
zeigt eine Liste aller Dateien im aktuellen Verzeichnis an. (Die Option /b bewirkt, dass dir nur die Namen angibt, nicht aber andere Zusatzinformationen.) dir /b > datei.txt
speichert diese Liste in der Datei datei.txt. Das Zeichen > bewirkt, dass die Standardausgabe vom Konsolenfenster in eine Datei umgeleitet wird. dir /b | sort /r > datei.txt
erzeugt ebenfalls eine Liste aller Dateien. Die Standardausgabe wird durch das Zeichen | an das Programm sort weitergegeben. sort sortiert die Dateien wegen der Option /r in umgekehrter Reihenfolge und schreibt das Ergebnis an den Standardausgabekanal. Von dort werden Sie wegen > wieder in eine Datei umgeleitet.
Standardein- und ausgabe in Konsoleanwendungen In Konsolenanwendungen liest Read[Line] Text aus dem Standardeingabekanal, während Write[Line] Text an den Standardausgabekanal schreibt. Darüber hinaus bieten Console.In bzw. .Out einen direkten Zugriff auf die zugrunde liegenden TextReader- bzw. TextWriterObjekte der Standardkanäle. (Die in Abschnitt 10.5 beschriebenen Eigenschaften und Methoden von StreamReader und StreamWriter können daher auch auf In bzw. Out angewendet werden). Mit SetIn bzw. SetOut können Sie die Standardkanäle woandershin umleiten, so dass beispielsweise alle Eingaben des Programms nicht mehr interaktiv erwartet werden, sondern aus einer Datei gelesen werden.
12.2 Systeminformationen ermitteln
507
Das folgende Beispiel zeigt ein Konsolenprogramm, das zeilenweise Eingaben aus dem Standardeingabekanal liest und diese Zeichenketten dann in umgekehrter Reihenfolge wieder ausgibt. (Dieses Programm würde auch funktionieren, wenn Sie auf In und Out verzichten und die gewöhnlichen Read- bzw. WriteLine-Methoden der Console-Klasse verwenden. Dank In und Out stehen Ihnen noch eine Menge zusätzlicher Programmiermöglichkeiten zur Verfügung, die in diesem sehr einfachen Beispiel aber nicht benötigt wurden.) ' Beispiel spezial\reversetext Sub Main() Dim line As String Do line = Console.In.ReadLine() If IsNothing(line) Then Exit Do Console.Out.WriteLine(StrReverse(line)) Loop End Sub
Um das Programm auszuprobieren, kompilieren Sie es, öffnen ein Eingabeaufforderungsfenster, wechseln in das bin-Verzeichnis des Programms und führen dann die in Abbildung 12.1 dargestellten Kommandos aus.
Abbildung 12.1: Ein- und Ausgabeumleitung mit Konsolenanwendungen
12.2
Systeminformationen ermitteln
Dieser Abschnitt stellt einige Eigenschaften und Methoden der Klassen System.Environment und Microsoft.VisualBasic.Interaction vor und gibt eine erste Einführung in die Bibliothek System.Management. Diese Klassen sind dabei behilflich, diverse system- und programmspezifische Informationen zu ermitteln: die Kommandozeile des laufenden Programms, die Umgebungsvariablen des Betriebssystems, die Pfade spezieller Verzeichnisse (z.B. des Windows-Systemverzeichnisses), den Rechnernamen, den aktuellen Benutzernamen, die Liste der an den Rechner angeschlossenen Laufwerke etc.
VERWEIS
508
12 Spezialthemen
Systeminformationen, die spezifisch für die Windows-Programmierung sind (also z.B. die Anzahl der angeschlossenen Monitore, deren Auflösung etc.) können mit den Windows.Forms-Klassen SystemInformation und Screen ermittelt werden. Diese Klassen werden in Abschnitt 15.2.8 kurz vorgestellt.
12.2.1 System.Environment-Klasse Die Klasse Environment (Namensraum System, Bibliothek mscorlib.dll) gibt Zugriff auf zahlreiche Basiseinstellungen des Betriebssystems bzw. der Umgebung, in der das aktuelle Programm ausgeführt wird. Alle Methoden und Eigenschaften können verwendet werden, ohne vorher ein Objekt dieser Klasse zu erzeugen. (Ein Teil der Informationen kann alternativ auch mit den Methoden der Klasse Microsoft.VisualBasic.Interaction ermittelt werden. Der Vorteil besteht im geringeren Tippaufwand.)
Umgebungsvariablen ermitteln Durch das Betriebssystem sind eine Reihe so genannter Umgebungsvariablen (environment variables) definiert. Diese Variablen geben Auskunft über das laufende Betriebssystem, über die Anzahl der Prozessoren, über einige wichtige Verzeichnisse, über Pfade zu Programmen, über Pfade zu verschiedenen Bibliotheken etc. Bei der Installation von Programmen werden dem System manchmal neue Umgebungsvariablen hinzugefügt. Sie können sich diese Variablen in der Systemsteuerung ansehen (SYSTEM|ERWEITERT|UMGEBUNGSVARIABLEN). Unter VB.NET können Sie Systemvariablen wahlweise mit den Methode Environ oder Environment.GetEnvironmentVariable auslesen. Die folgenden Zeilen liefern jeweils den Pfad zum Windows-Verzeichnis. Dim s As String s = Environ("windir") s = Environment.GetEnvironmentVariable("windir")
Eine Aufzählung aller Umgebungsvariablen erhalten Sie mit GetEnvironmentVariables. Die Methode liefert ein Collections.IDictionary-Objekt zurück. Die folgende Schleife zeigt die Auswertung dieser Aufzählung. Dim entry As DictionaryEntry For Each entry In Environment.GetEnvironmentVariables Console.WriteLine("{0} = {1}", _ entry.Key.ToString, entry.Value.ToString) Next
12.2 Systeminformationen ermitteln
509
Kommandozeile auswerten Wenn Ihr Programm mit START|AUSFÜHREN oder aus einem Eingabeaufforderungsfenster ('DOS-Fenster') heraus gestartet wird, können Parameter an das Programm übergeben werden. Diese Parameter werden als Kommandozeile bezeichnet. Dasselbe gilt auch für Programme, die durch einen Doppelklick auf ein damit verbundenes Dokument aus dem Windows Explorer starten. (Wenn Sie also eine *.doc-Datei per Doppelklick öffnen, wird winword.exe gestartet und der Dateiname in der Kommandozeile übergeben.) Diesen Mechanismus können Sie auch für eigene Programme nutzen, wenn Sie diese mit einem eigenen Dateityp verbinden. Ein entsprechendes Beispiel finden Sie in Abschnitt 18.4.5, wo es weniger um die einfache Auswertung der Kommandozeile geht, als vielmehr um die Registrierung eines neuen Dateityps bei der Installation des Programms. Den Inhalt der Kommandozeile können Sie wahlweise mit der Methode Command oder mit der Eigenschaft Environment.CommandLine ermitteln. Beachten Sie, dass CommandLine gleichsam als ersten Parameter den Namen des Programms enthält und die tatsächlichen Parameter erst im Anschluss daran folgen. Wenn Sie die Parameter einzeln auslesen möchten, liefert Environment.GetCommandLineArgs ein String-Feld; das erste Element dieses Felds ist wiederum der Name des aktuellen Programms, erst die folgenden Elemente sind die tatsächlichen Parameter.
VERWEIS
Spezielle Verzeichnisse ermitteln Mit der Methode Environment.CurrentDirectory können Sie das aktuelle Verzeichnis ermitteln. SystemDirectory liefert das Windows-Systemverzeichnis, GetFolderPath(..) liefert den Pfad zu weiteren speziellen Verzeichnissen. All diese Eigenschaften bzw. Methoden werden in Abschnitt 10.3.4 beschrieben.
Benutzername, Rechnername, OS-Version Die folgenden Eigenschaften der System.Environment-Klasse bedürfen keiner langen Erklärung, weswegen Sie hier nur Syntaxboxen mit einer Zusammenfassung der Eigenschaften finden. System.Environment-Klasse – Rechnerspezifische Informationen MachineName
liefert den Rechnernamen in einer Arbeitsgruppe (NetBIOS-Name).
NewLine
gibt an, welche Zeichen zur Zeilentrennung in Textdateien verwendet werden (unter Windows Chr(13)+Chr(10)).
OSVersion
liefert Informationen über das Betriebssystem (als System.OperatingSystem-Objekt).
510
12 Spezialthemen
System.Environment-Klasse – Rechnerspezifische Informationen TickCount
liefert die Anzahl der Ticks (100 ns), seit der Rechner gestartet wurde.
UserDomainName
liefert den Rechnernamen in einer Domain (stimmt meist mit HostName überein).
System.Environment-Klasse – loginspezifische Informationen UserName
Benutzername (Login-Name)
UserInteractive
gibt an, ob ein interaktiver Login vorliegt (True/False).
System.Environment-Klasse – programmspezifische Informationen Version
gibt Informationen über die Version des laufenden Programms an (als System.Version-Objekt).
WorkingSet
gibt die Menge des Speichers an, der dem Prozess zugeordnet ist (in Byte).
Betriebssystemversion ermitteln Abschließend noch ein Beispiel zur Ermittlung der Version des laufenden Betriebssystems. Environment.OSVersion liefert ein OperatingSystem-Objekt zurück. Dessen Eigenschaft Platform gibt den Betriebssystemtyp an (in Form eines Elements der PlatformID-Aufzählung). Die möglichen Werte sind im Kasten unten zusammengefasst. Die Eigenschaft Version des OSVersion-Objekts liefert ein System.Version-Objekt. Dessen Eigenschaften Major, Minor und Revision geben die Bestandteile der internen Versionsnummer des Betriebssystems an. (Wenn das Betriebssystem die Versionsnummer 5.3.7 hat, dann gilt Major=5, Minor=3 und Revision=7.) Build enthält eine weitere interne Versionsnummer. (Sie wird jedes Mal um eins erhöht, wenn Microsoft das gesamte Betriebssystem neu kompiliert.) Es gibt leider keine eigene Eigenschaft, die die Nummer des installierten Service-Packs angibt. Sie können grundsätzlich aus der Build-Nummer auf die SP-Nummer schließen, es scheint aber leider keine zentrale Tabelle zu geben, die die Zuordnung zwischen Betriebssystemversion, Build-Nummer und SP-Nummer angibt. Die folgenden Zeilen schreiben die wichtigsten Betriebssysteminfos in ein Konsolenfenster. Dim os As OperatingSystem = Environment.OSVersion Dim ver As Version = os.Version Console.WriteLine(os.Platform.ToString) Console.WriteLine( _ "Major: {0} Minor: {1} Revision: {2} Build: {3}", _ ver.Major, ver.Minor, ver.Revision, ver.Build)
12.2 Systeminformationen ermitteln
511
Bei Windows 2000 SP 2 liefert das Miniprogramm folgendes Ergebnis: Win32NT Major: 5
Minor: 0
Revision: 0 Build: 2195
System.OperatingSystem-Klasse Platform
gibt in Form von Aufzählungselementen den Betriebssystemtyp an. In Frage kommen zurzeit: PlatformID.Win32S: 16-Bit-Windows-Version mit 32-Bit-
Emulationsschicht PlatformID.Win32Windows: Windows 95, 98, ME PlatformID.Win32NT: Windows NT, 2000, XP Version
liefert ein System.Version-Objekt.
System.Version-Klasse Build
Build-Nummer (durchlaufende Nummer, je höher, desto aktueller)
Major
Hauptversionsnummer (3 für Windows NT 3.x, 4 für Windows 9x, ME und NT 4, 5 für Windows 2000, XP und .NET-Server)
Minor
Unterversionsnummer (0 für Windows 95, NT 4, 2000, 1 für Windows XP und .NET-Server, 10 für Windows 98, 51 für Windows NT 3.51 90 für Windows ME)
Revision
Revisionsnummer
12.2.2 System.Management-Bibliothek (WMI) Die System.Management-Bibliothek bietet im gleichnamigen Namensraum zahlreiche Klassen zur Systemverwaltung. Genau genommen helfen diese Klassen, die Funktionen der Windows Management Instrumentation (kurz WMI) zu nutzen. Damit können Sie verschiedene Performance-Parameter ermitteln (CPU-Auslastung etc.), Benutzer und Passwörter verwalten, verschiedene Server-Dienste steuern (SQL-Server, IIS etc.), die an den Rechner angeschlossenen Laufwerke ermitteln, Netzwerkparameter lesen und verändern etc. Der Platz reicht hier nicht aus, um auf die System.Management-Bibliothek ausführlich einzugehen. Das in diesem Abschnitt vorgestellte Beispiel soll Ihnen aber 'Lust machen, sich selbst ein wenig in das Thema einzulesen. Beachten Sie, dass Sie einen Verweis auf die Bibliothek einrichten müssen, bevor Sie deren Klassen verwenden können!
VERWEIS
512
12 Spezialthemen
Ausführliche Informationen zur Nutzung dieser Bibliothek finden Sie in der Hilfe: Suchen Sie einfach nach den Begriffen Verwalten Anwendungen Verwendung WMI. ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconmanagingapplicationsusingwmi.htm
Eine gute Einführung in WMI und in die Verwendung der System.Management-Bibliothek gibt auch das .NET-Klassenhandbuch von Holger Schwichtenberg und Frank Eller (siehe Quellenverzeichnis im Anhang).
Beispiel – Informationen über die Laufwerke des Rechners ermitteln WMI-intern werden alle über den Computer verfügbaren Informationen in einer Art Datenbank verwaltet. Die Besonderheit besteht darin, dass Sie diese Informationen wie durch Datenbankabfragen auslesen können. Dazu benötigen Sie ein Objekt der Klasse ManagementObjectSearcher, um die Abfrage zu formulieren und mit Get auszuführen. Als Ergebnis erhalten Sie eine ManagementObjectCollection-Aufzählung, die eine Reihe von ManagementObject-Objekten enthält. Diese Objekte verweisen wiederum auf PropertyDataObjekte mit den eigentlichen Daten. Das folgende Beispielprogramm ermittelt alle am Rechner verfügbaren Laufwerke. Diese Informationen befinden sich in der WMI-Datenbank in einer Win32_LogicalDisk-Tabelle. Das Programm ermittelt alle Elemente dieser Tabelle und zeigt wiederum alle Eigenschaften der gefundenen Elemente in einem Konsolenfenster an (siehe Abbildung 12.2). Die TryCatch-Konstruktion ist erforderlich, weil manche Eigenschaften nicht mit ToString in Zeichenketten umgewandelt werden können. Diese Eigenschaften werden einfach übersprungen. Für die praktische Anwendung dieses Beispiels ist vor allem die DriveType-Eigenschaft interessant, die ohne die System.Management-Bibliothek leider nicht ermittelt werden kann. Laut der WMI-Dokumentation sind folgende Werte vorgesehen:
HINWEIS
0 1 2 3 4 5 6
unbekannt Datenträger ohne Wurzelverzeichnis (no root directory, was immer das bedeutet) Diskette (oder ein anderer, nicht fest angeschlossener Datenträger) lokale (Fest-)Platte Netzwerkverzeichnis CD-/DVD-Laufwerk RAM-Disk System.Management ist eine eigene Bibliothek und nicht Teil von System.dll oder mscorlib.dll! Deswegen müssen Sie die Bibliothek explizit mit PROJEKT|VERWEIS HINZUFÜGEN in Ihr Projekt aufnehmen, damit Sie die System.Management-Klassen nützen
können!
12.2 Systeminformationen ermitteln
Abbildung 12.2: Alle verfügbaren Informationen über die Laufwerke A:, C:, D: etc.
' Beispiel spezial\laufwerke ' dieses Programm setzt voraus, dass Sie einen Verweis ' auf die System.Management-Bibliothek einrichten Sub Main() Dim mos As Management.ManagementObjectSearcher Dim moc As Management.ManagementObjectCollection Dim mo As Management.ManagementObject Dim pd As Management.PropertyData mos = New Management.ManagementObjectSearcher( _ "SELECT * FROM Win32_LogicalDisk") moc = mos.Get() For Each mo In moc Console.WriteLine("------------") For Each pd In mo.Properties Try Console.WriteLine(pd.Name + " = " + pd.Value.ToString) Catch End Try Next Console.ReadLine() Next moc.Dispose() mos.Dispose() End Sub
513
514
12 Spezialthemen
12.3
Sicherheit
ACHTUNG
In Abschnitt 2.4 wurden die Grundkonzepte der .NET-Sicherheit kurz skizziert. Dieser Abschnitt greift das Thema nochmals auf und beschreibt, wann es in VB.NET-Programmen zu Problemen aufgrund mangelnder Zugriffsrechte kommen kann und wie sich mit diesen Problemen umgehen lässt. Ich habe bereits in Abschnitt 2.4 darauf hingewiesen, dass die vollständige Beschreibung der .NET-Sicherheitsmechanismen und ihrer Steuerung durch VB.NETCode zumindest ein ganzes Kapitel füllen könnte (wenn man noch auf Themen wie Krypthographie eingeht, auch ein ganzes Buch). An dieser Stelle fehlt dazu aber der Platz. Ich betone also ausdrücklich, dass das in diesem Buch vermittelte Wissen zum Thema Sicherheit bestenfalls ein Ausgangspunkt für eigene Experimente und zur weitergehenden Lektüre sein kann!
Einige Beispiele für sicherheitskritische Methoden bzw. Kommandos Sicherheitskritischen Methoden sind durchaus nicht immer ohne weiteres erkennbar. Die folgende Liste zählt einige Kommandos bzw. Methoden auf, die bei der Ausführung ohne ausreichende Rechte zu Fehlern (SecurityExceptions) führen: •
Mit dem aus VB6-Zeiten stammenden Kommando End wird das laufende Programm sofort beendet. Das Kommando wirkt vollkommen harmlos, aber es erfordert das Recht SecurityPermissionFlag.UnmanagedCode. Wenn dieses Recht nicht gegeben ist, führt End zu einem Fehler!
•
Mit New IO.DirectoryInfo("c:\").GetFiles() ermitteln Sie eine Liste aller Dateien, die sich auf der Festplatte C: im Wurzelverzeichnis befinden. Die Methode GetFiles darf allerdings nur ausgeführt werden, wenn Sie das Recht Security.Permissions.FileIOPermission besitzen. Ähnliche Probleme können bei allen System.IO-Methoden auftreten.
•
Der Aufruf von API-Funktionen und die Verwendung von COM-Bibliotheken bzw. die Steuerung von COM-Programmen (Automation) führt bei unzureichenden Rechten zu einer SecurityException.
•
Die Methode Graphics.FromHwnd zur Ermittlung des Windows-Handle eines Fensters führt bei unzureichenden Rechten ebenfalls zu einer SecurityException.
Die Liste ließe sich fast beliebig fortführen. Zum Teil sind die erforderlichen Rechte bei der Dokumentation der Klasse bzw. Methode beschrieben, aber leider ist das nicht immer der Fall. Es ist daher eine gute Idee, die Programmausführung mit eingeschränkten .NETRechten einfach auszuprobieren!
12.3 Sicherheit
515
SecurityExceptions durch Catch/Try abfangen Unzureichende Rechte lösen normalerweise eine Security.SecurityException aus. Derartige Fehler können durch Try – Catch abfangen werden. Dim fi() As IO.FileInfo Try fi = New IO.DirectoryInfo("c:\").GetFiles() Catch e As Security.SecurityException Console.WriteLine(e.Message) End Try
Zugriffsrechte testen Die Basisrechte werden durch die Klassen des Namensraums Security.Permissions verwaltet. Wenn Sie überprüfen möchten, ob Ihr Programm ein bestimmtes Recht hat, erzeugen Sie ein Permission-Objekt und führen dann die Methode Demand aus. Wenn es dabei zu einem Fehler kommt, wissen Sie, dass Sie das angeforderte Recht nicht haben. Die folgenden Zeilen testen, ob Dateien aus dem Verzeichnis C:\ gelesen werden dürfen. Dazu wird ein FileIOPermission-Objekt erzeugt, wobei die gewünschte Operation und der Pfadname als Parameter an New übergeben werden. Dim fp As New _ Security.Permissions.FileIOPermission( _ Security.Permissions.FileIOPermissionAccess.Read, "c:\") Try fp.Demand() Catch e As Security.SecurityException Console.WriteLine(e.Message) End Try
Programmausführung von Rechten abhängig machen Bei manchen Programmen ist es zwecklos, dass diese überhaupt gestartet werden können, wenn sie in der Folge aufgrund unzureichender Rechte ihre Aufgabe ohnedies nicht erfüllen können. Um einen Rechtetest komfortabel beim Programmstart durchzuführen, gibt es zu jeder Permission-Klasse eine PermissionAttribute-Variante. Das entsprechende Attribut muss als Assembly-Attribut im Programmcode angegeben werden. Die folgenden Zeilen testen nochmals, ob Dateien aus dem Verzeichnis C:\ gelesen werden dürfen. Beachten Sie, dass die Parameter des Attributs nicht ganz mit denen der FileIOPermission-Klasse übereinstimmen.
516
12 Spezialthemen
12.4
Externe Programme starten
Dieser Abschnitt beschreibt einige Möglichkeiten, ein anderes (externes) Programm zu starten und eventuell auch wieder zu beenden bzw. auf sein Ende zu warten. Dabei kommen vor allem Methoden aus zwei Klassen zur Anwendung: •
Die Interaction-Klasse aus dem Namensraum Microsoft.VisualBasic bietet einige grundlegende und schon aus VB6 bekannte Möglichkeiten, andere Programme zu starten.
•
Mit der Process-Klasse aus dem Namensraum System.Diagnostics können Sie darüber hinaus eine Menge Informationen über das aktuellen Programm ermitteln und neue Programme starten. (Jedes Programm wird von einem so genannten Prozess ausgeführt, der wiederum aus mehreren Teilprozessen (Threads) bestehen kann. Multithreading wird in Abschnitt 12.6 beschrieben.)
VERWEIS
Manche Programme können von VB.NET nicht nur gestartet, sondern auch gesteuert werden. Dabei kommt der Mechanismus Automation zur Anwendung, der im nächsten Abschnitt behandelt wird. Wenn Sie Automation einsetzen möchten, müssen Sie zum Programmstart Get- oder CreateObject verwenden.
TIPP
Der Namensraum System.Diagnostics ist Teil der Bibliothek System.dll, die jedem VB.NET-Programm zur Verfügung steht. Die Diagnostics-Klassen werden unter anderem auch von den Debugging-Komponenten der Entwicklungsumgebung genutzt.)
Auf der CD finden Sie im Verzeichnis spezial\start-program ein kleines Beispiel, das die unten beschriebenen Programmiertechniken demonstriert. Auf den Abdruck des Beispiels wurde aus Platzgründen verzichtet.
Programm starten Die Methode Shell der Interaction-Klasse startet ein anderes Programm. Als einziger Parameter muss der Programmname als Zeichenkette übergeben werden. Bei Programmen, die sich im Windows-Verzeichnis befinden, reicht der reine Name (z.B. "notepad.exe"). Dasselbe gilt für Programme, die sich in Verzeichnissen der Umgebungsvariablen PATH befinden. Wenn diese Voraussetzung nicht erfüllt ist, muss der vollständige Pfad angegeben werden (also z.B. "C:\Programme\Opera\Opera.exe"). Zusammen mit dem Programmnamen können auch Parameter angegeben werden. Beispielsweise startet das folgende Kommando den Texteditor und zeigt darin die Datei c:\readme.txt an. Dim processid As Integer processid = Shell("notepad.exe c:\readme.txt")
12.4 Externe Programme starten
517
Shell kennt drei optionale Parameter:
•
Style gibt an, wie das Programm geöffnet werden soll – beispielsweise in einem bildschirmfüllenden Fenster mit Eingabefokus (AppWinStyle.MaximizedFocus). Per Default gilt die Einstellung AppWinStyle.MinimizedFocus.
•
Wait gibt an, ob mit der Fortsetzung des VB.NET-Programms so lange gewartet werden soll, bis das aufgerufene Programm beendet wird (per Default False). Beachten Sie, dass
das VB.NET-Programm dadurch wirklich vollständig blockiert wird. Bei WindowsProgrammen funktioniert dann nicht einmal das Neuzeichnen des Fensters. •
Timeout gibt an, wie lange maximal auf das Programmende gewartet werden soll. Timeout wird nur berücksichtigt, wenn Wait:=True gilt. Die Zeitangabe erfolgt in Millisekun-
den. Die Defaulteinstellung lautet -1, d.h., es wird endlos gewartet. Wenn der Programmstart gelingt, liefert Shell als Ergebnis die Prozessnummer des Programms. Diese Nummer kann später z.B. dazu verwendet werden, um das Programm AppActivate in den Vordergrund zu bringen.
Laufendendes Programm aktivieren Mit der Methode AppActivate (Klasse Interactive) können Sie ein laufendes Programm bzw. Fenster aktivieren, wenn Sie entweder dessen Prozessnummer kennen (z.B. von einem früheren Shell-Aufruf) oder den Text, der in der Titelzeile des Fensters angezeigt wird. AppActivate endet mit einem ArgumentException-Fehler, wenn der Prozess nicht gefunden werden kann. Sie sollten den Aufruf daher durch Catch-Try absichern.
HINWEIS
AppActivate(processid)
Das durch AppActivate aktivierte Programm erhält zwar den Eingabefokus, seine Fensterposition wird aber nicht verändert. Wenn das Programm momentan verkleinert ist und nur als Icon in der Taskleiste angezeigt wird, wird es durch AppActivate nicht sichtbar.
Programm zum Anzeigen oder Bearbeiten einer Datei öffnen Mit der Methode Start (Klasse Diagnostics.Process) können Sie ein Dokument öffnen. Die Besonderheit von Start im Vergleich zu Shell besteht darin, dass als Parameter nur der Dateiname des Dokuments übergeben werden muss. Start ermittelt selbstständig das erforderliche Programm. Beispielsweise startet die folgende Zeile Adobe Acrobat und zeigt darin die angegebene PDF-Datei an. (Wenn der Dateiname wie hier ohne Pfad angegeben wird, wird die Datei im selben Verzeichnis gesucht, in dem sich die Programmdatei befindet.) Diagnostics.Process.Start("datei.pdf") Start akzeptiert als Parameter auch Web- und E-Mail-Adressen und öffnet dann den
Default-Webbrowser bzw. das Default-E-Mail-Programm.
518
12 Spezialthemen
Diagnostics.Process.Start("http://www.kofler.cc") Diagnostics.Process.Start("mailto:[email protected]") Start funktioniert natürlich nur dann, wenn der Datei ein Programm zugeordnet werden kann und dieses auch installiert ist. Wenn die Dateikennung dagegen unbekannt ist, löst Start einen Win32Exception-Fehler aus.
Wenn durch Start ein neues Programm gestartet wird, liefert die Methode als Ergebnis ein Process-Objekt zurück. Es kann aber auch vorkommen, dass das Programm schon vorher gelaufen ist (z.B. ein Webbrowser) und dass nur ein neues Fenster geöffnet wurde, um darin eine weitere Webseite anzuzeigen. In diesem Fall liefert Start als Ergebnis Nothing. (Das Ergebnis Nothing bedeutet also durchaus nicht, dass das Dokument nicht angezeigt wurde!) Sie können nun das Process-Objekt dazu verwenden, um Informationen über das laufende Programm zu ermitteln bzw. um dieses zu beenden. Die folgenden Zeilen bewirken, dass die Datei readme.txt nur fünf Sekunden lang angezeigt wird. Anschließend wird der Editor durch CloseMainWindow zum Programmende aufgefordert. (Wenn die Datei in der Zwischenzeit geändert wurde, erscheint dann ein Dialog zum Speichern, in dem der Anwender das Programm auch fortsetzen kann. Wenn der Anwender die Datei geschlossen hat, bevor CloseMainWindow aufgerufen wurde, kommt es zu einem Fehler.) Dim pr As Diagnostics.Process pr = Diagnostics.Process.Start("readme.txt") Threading.Thread.Sleep(5000) pr.CloseMainWindow()
Die folgenden Zeilen zeigen den Einsatz der Methode WaitForExit. Damit wartet das VB.NET-Programm, bis das gestartete Programm geschlossen wird.
VERWEIS
Dim pr As Diagnostics.Process pr = Diagnostics.Process.Start("readme.txt") pr.WaitForExit()
Weitere Beispiele für die Anwendung der Start-Methode finden Sie in Abschnitt 14.4.2 bei der Beschreibung des LinkLabel-Steuerelements.
VERWEIS
Programm durch SendKeys steuern Eine sehr primitive Form, ein anderes Programm zu steuern, bietet die Simulation von Tastatureingaben mit der Methode SendKeys. Eine derartige Steuerung ist aber von den Tastenkürzeln des Programms abhängig (und damit meist auch von der Sprache des Programms) und generell sehr fehleranfällig. Nähere Informationen zu SendKeys erhalten Sie in Abschnitt 15.9.
12.5 Externe Programme steuern (Automation)
12.5
519
Externe Programme steuern (Automation)
12.5.1 Grundlagen Der Begriff Automation bezeichnet die Möglichkeit, ein fremdes Programm zu steuern. Der Automation-Mechanismus basiert zwar auf COM-Technologie (alias ActiveX, alias OLE), kann aber auch mit VB.NET wegen dessen COM-Kompatibilität genutzt werden. Die Grundidee von Automation besteht darin, dass das VB.NET-Programm eine Verbindung zum Programm herstellt, das gesteuert werden soll. (Dieser Verbindungsaufbau kann z.B. auf der Basis einer vorhandenen Datei erfolgen.) Das Ergebnis ist ein Objekt, über dessen Methoden und Eigenschaften das externe Programm dann gesteuert werden kann. Wenn Ihnen das alles recht theoretisch erscheint, hier einige Beispiele: Dank Automation können Sie mit einem VB.NET-Programm Word starten, dort einen Text einfügen, formatieren und schließlich ausdrucken; Sie können eine Excel-Datei öffnen und daraus einige Zellen lesen, in Ihrem VB.NET-Programm verarbeiten, das Ergebnis wieder in einer anderen Zelle eintragen und die Excel-Datei dann speichern; Sie können Access starten und damit einen Datenbankbericht ausdrucken etc. Automation kann natürlich nur für solche Programme verwendet werden, die diesen Mechanismus unterstützen. Das ist unter anderem für alle aktuellen Office-Komponenten der Fall. Wenn Sie Programme weitergeben, die Automation nutzen, muss das zu steuernde Programm auch beim Anwender installiert sein (möglichst in derselben Version). Was in der Theorie toll klingt, führt in der Praxis leider zu schier endlosen Problemen. Obwohl Microsoft das Konzept von Automation schon 1994 mit Excel 5 realisierte, ist das Konzept nie richtig ausgereift: Automation ist abhängig von der am Rechner installierten Version des Programms, von dessen Sprache etc. Oft passiert es, dass nach dem Ende eines Programms, das Automation nutzt, eine Instanz des zu steuernden Programms (also z.B. Excel) weiterläuft und sich nur noch mit dem Task-Manager beenden lässt. Kurz und gut – es gibt 1000 Gründe, warum Automation in der Praxis selten so funktioniert, wie es eigentlich sollte. Eine Grundvoraussetzung für jede Automation-Anwendung besteht darin, dass Sie das Objektmodell des zu steuernden Programms kennen. Diese Hürde ist nicht zu unterschätzen. Beispielsweise kennt Excel ca. 150 Klassen mit weit über 1000 Methoden und Eigenschaften, deren Beschreibung ein ganzes Buch füllt. (Ich habe selbst ein derartiges Buch geschrieben und weiß, wovon ich spreche.) Zu diesen Problemen, die gar nichts mit .NET zu tun haben, kommen die grundsätzlichen Schwierigkeiten der .NET/COM-Kompatibilität hinzu, die in diesem Buch nicht einmal angedeutet werden. (Damit Sie sich eine Vorstellung von der Komplexität dieses Themas machen: die beiden bisher zu diesem Thema erschienenen englischen Bücher von Andrew Troelsen und Adam Nathan füllen zusammen 2500 Seiten!)
HINWEIS
12 Spezialthemen
Die obigen Absätze sollen Sie nicht davon abhalten, Automation selbst auszuprobieren, sondern sollen lediglich falsche bzw. überzogene Erwartungen verhindern. Mit etwas Experimentierfreude können Automation-Lösungen dann durchaus gelingen. (Senden Sie mir aber bitte keine E-Mails, wenn Sie auf Automation-Probleme stoßen! Ich kann in solchen Fällen nicht weiterhelfen und Ihnen insbesondere das Experimentieren nicht abnehmen.)
HINWEIS
520
In VB6 bestand die Möglichkeit, das zu steuernde Programm innerhalb des VB-Programms in einem OLE-Steuerelement sichtbar zu machen. Diese Möglichkeit gibt es in VB.NET nicht mehr.
Verwendung der Klassenbibliothek Das besondere Kennzeichen eines Programms, das sich durch Automation steuern lässt, besteht darin, dass es eine Klassenbibliothek zur Verfügung stellt (ähnlich wie die .NETBibliotheken, die Sie täglich nutzen). Diese Klassenbibliothek gibt Zugriff auf alle Objekte des Programms und ermöglicht somit die Steuerung. (Um es an einem Beispiel zu illustrieren: Bei Excel können Sie mit dem Worksheet-Objekt auf einzelne Tabellenblätter zugreifen, mit dem Range-Objekt auf Zellen oder Zellbereiche etc.) Damit Sie eine derartige Klassenbibliothek in Ihrem VB.NET-Programm nutzen können, müssen Sie einen Verweis auf die Bibliothek einrichten (im Projektmappenexplorer oder mit PROJEKT|VERWEISE). Sie finden die Klassenbibliothek unter den COM-Bibliotheken – die Bibliothek für Excel z.B. unter dem Namen MicrosoftExcel n.n Object Library. Über den Objektbrowser können Sie die Klassen dann ansehen (siehe Abbildung 12.3). VBA-Programmierern wird auffallen, dass viele Klassen, die in der VBA-Entwicklungsumgebung als Klassen dargestellt werden, von der .NET-Entwicklungsumgebung als Schnittstellen bezeichnet werden. Das hat aber keinen spürbaren Effekt auf die Programmierung. Nach der Auswahl der Bibliothek erstellt die Entwicklungsumgebung eine so genannte Wrapper-Bibliothek (z.B. Interop.Excel), die die Schnittstelle zwischen .NET und COM bildet. Bei Programmen wie Excel mit einer ziemlich großen Klassenbibliothek dauert die Erzeugung der Wrapper-Bibliothek ziemlich lang. Das wäre an sich noch kein Problem, wenn die resultierenden Bibliotheken dann vollständig wären. Das ist aber leider nicht immer der Fall. Beispielsweise fehlt nach dem Import der Excel-9-Bibliothek (entspricht Excel 2000) ein Großteil der rund 150 Klassen, was die weitere Programmierung fast unmöglich macht. Experimente mit der Excel-5-Bibliothek waren auch nicht erfolgreicher. (Diese Bibliothek ist kompatibel zum Objektmodell von Excel 5 und wird aus Kompatibilitätsgründen ebenfalls mit Excel 2000 mitgeliefert.) Diesmal klappte zwar der Import der meisten Klassen, dafür fehlten aber alle Konstanten. Darüber hinaus liefern viele Eigenschaften oder Methoden der Wrapper-Bibliothek als Ergebnis Object, obwohl sie im Original (also unter Excel) ganz andere Typen liefern.
12.5 Externe Programme steuern (Automation)
521
VERWEIS
Tests mit der Klassenbibliothek von Word 2000 führten zu keinen offensichtlichen Problemen. (Allerdings kenne ich die Word-Klassenbibliothek weniger gut als die von Excel. Es ist also nicht auszuschließen, dass ich diverse Word-Probleme übersehen habe.) Office XP alias Office 2002 stand mir für meine Tests leider nicht zur Verfügung, so dass ich auch dazu nichts sagen kann. Microsoft plant, die vollkommen unbefriedigende Import-Funktion für COM-Bibliotheken zumindest für seine eigenen Office-Programme dadurch zu lösen, dass es so genannte primary interop assemblies (PIAs) zur Verfügung stellt. Dabei handelt es sich um vorgefertigte Wrapper-Bibliotheken, die als offizielle Schnittstelle verwendet werden sollen, um Konflikte zwischen verschiedenen selbst erstellten WrapperBibliotheken zu vermeiden. Es ist zu hoffen, dass diese offiziellen Wrapper-Bibliotheken dann auch komplett sind. Bis zum Abgabetermin für dieses Buch habe ich leider keine derartigen PIAs für Office-Komponenten gefunden. Weitere Informationen finden Sie hier: http://msdn.microsoft.com/library/en-us/dndotnet/html/whypriinterop.asp
Abbildung 12.3: Einige Klassen zur Word-Automation
Late binding Wenn der Import der Klassenbibliothek wie bei Excel 2000 gänzlich scheitert, müssen Sie eben ohne einen Verweis auf diese Bibliothek arbeiten und late binding verwenden. late binding bedeutet, dass Sie Variablen des Typs Object verwenden und darauf Methoden und Eigenschaften anwenden. Der Compiler kann aber nicht überprüfen, ob es diese Methoden oder Eigenschaften überhaupt gibt. Deswegen lässt er diese Frage offen. Die Verbindung
522
12 Spezialthemen
zwischen Objekt und Methode oder Eigenschaft wird erst dann hergestellt, wenn das Programm ausgeführt wird – daher die Bezeichnung late binding. (Das Gegenteil von late binding wird übrigens als early binding bezeichnet und ist unter VB.NET der Normalfall.) late binding hat zwei wesentliche Nachteile: Erstens ist die Codeentwicklung sehr mühsam, weil die IntelliSense-Funktion nicht funktioniert und Sie ständig selbst im Objektkatalog nachsehen müssen, welche Klasse welche Eigenschaften oder Methoden kennt, welche Parameter und Rückgabewerte es hierfür gibt etc. Zweitens kann der Compiler die Korrektheit des Codes nicht überprüfen. Tippfehler (z.B. ein falscher Methodenname) treten daher erst bei der Ausführung des Programms auf.
TIPP
Um diese Nachteile zu umgehen, ist es am besten, den Code zuerst in einer anderen Programmiersprache (z.B. in VB6 oder in der VBA-Entwicklungsumgebung) zu entwickeln und den fertigen Code dann in das VB.NET-Programm einzufügen. Ganz ohne Änderungen ist das aber meist auch nicht möglich, weil die Namen von Klassen, Konstanten etc. bisweilen unter VB.NET anders sind. VB.NET unterstützt late binding nur dann, wenn Option Strict Off gilt! Mit Option Strict On kann ausschließlich early binding verwendet werden.
Verbindung zum Programm herstellen Es gibt zwei Möglichkeiten, um eine Verbindung zum Programm herzustellen, das gesteuert werden soll: •
Mit CreateObject("bibliothek.objekt") erzeugen Sie ein neues Objekt, z.B. ein WordDokument ("word.document") oder eine Excel-Tabelle ("excel.sheet"). Falls das Programm noch nicht läuft, wird es dazu gestartet. Dim doc As Object 'late binding doc = CreateObject("Word.Document") Dim doc As Word.Document 'early binding doc = CType(CreateObject("Word.Document"), Word.Document)
•
Mit GetObject(dateiname) öffnen Sie eine Datei und erhalten ebenfalls ein Objekt. GetObject startet bei Bedarf automatisch das richtige Programm (also z.B. Word, um die Datei "beispiel.doc" zu öffnen. Dim wb As Object 'late binding wb = CreateObject("C:\test\excel.doc") Dim wb As Excel.Workbook 'early binding wb = CType(CreateObject("C:\test\excel.doc"), Excel.Workbook)
12.5 Externe Programme steuern (Automation)
523
Die Methoden Create- und GetObject sind in der Interaction-Klasse von Microsoft.VisualBasic definiert. Welcher Klasse die zurückgegebenen Objekte angehören, hängt von der Klassenbibliothek des Programms ab. Welche Zeichenketten an CreateObject übergeben werden dürfen, hängt davon ab, wie die Programme in der Registrierdatenbank registriert sind. (Die Tabelle mit allen zulässigen Objektnamen können Sie mit regedit.exe ermitteln: HKEY_LOCAL_Machine|Software|Classes.) Durch Get- oder CreateObject gestartete Programme sind normalerweise unsichtbar. Die Microsoft-Office-Komponenten können über objekt.Application.Visible=True sichtbar gemacht werden. Bei Programmen anderer Hersteller kann es andere Mechanismen geben.
Verbindung zum Programm trennen Bisweilen komplizierter als der Verbindungsaufbau ist es, sich von dem zu steuernden Programm wieder zu trennen, wenn es nicht mehr benötigt wird. Wenn Sie nicht aufpassen, läuft das (womöglich unsichtbare) Programm weiter. Um das Programm explizit zu beenden, führen Sie üblicherweise object.Application.Quit() aus. Das Programm wird dadurch aber keineswegs tatsächlich sofort beendet. Es läuft vielmehr (mindestens) so lange weiter, solange es in ihrem VB.NET-Programm noch Verweise auf irgendein Objekt des Programms gibt. Wie lange es derartige Verweise gibt, hängt davon ab, wann diese durch eine garbage collection aus dem Speicher entfernt werden. Wenn Sie möchten, dass das externe Programm möglichst schnell beendet wird, dann sollten Sie folgenden Weg einschlagen: Achten Sie zum einen darauf, dass alle Variablen, die auf Objekte des Programms verweisen, entweder nicht mehr gültig sind (weil sie in einer Prozedur deklariert sind, die nicht mehr läuft) oder explizit auf Nothing gesetzt werden (also doc = Nothing). Lösen Sie zum anderen die garbage collection explizit zweimal hintereinander aus. (Fragen Sie mich nicht, warum gerade zweimal. Der Tipp stammt aus einem News-Gruppenbeitrag und hat sich in der Praxis bewährt.)
HINWEIS
doc.Application.Quit() doc = Nothing GC.Collect() GC.WaitForPendingFinalizers() GC.Collect() GC.WaitForPendingFinalizers()
'Programm zum Ende auffordern 'Objektvariablen löschen 'garbage collection auslösen 'auf das Ende der gc warten 'garbage collection nochmals auslösen 'auf das Ende der gc warten
Es kann sein, dass Word, Excel etc. schon läuft, wenn Sie Get- oder CreateObject ausführen. Dann wird das neue Objekt in der schon laufenden Instanz erzeugt. In diesem Fall sollten Sie das Programm nicht durch Quit beenden. Das Problem besteht allerdings darin, dass es in VB.NET nicht ohne weiteres zu erkennen ist, ob das Programm schon läuft. Daher sollten Sie vor der Ausführung von Quit überprüfen, ob im Programm noch andere Dokumente geöffnet sind. (In Excel können Sie das mit Application.Workbooks.Count tun.)
524
12 Spezialthemen
VERWEIS
Weitere Informationen In der Online-Hilfe wird das Thema Automation weitgehend ignoriert. Dafür gibt es aber gute Beiträge in der Knowledge-Base. Die folgende Aufzählung zählt drei einführende Artikel auf, eine Menge weitere finden Sie, wenn Sie im KnowledgeBase-Suchformular nach automate .NET oder automation .NET suchen. http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301982 (VB.NET + Excel) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q302814 (Excel-Ereignisse empfangen) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301656 (VB.NET + Word)
12.5.2 Beispiel – Daten aus einer Excel-Datei lesen Wegen der oben schon beschriebenen Probleme beim Versuch, die Excel-Klassenbibliothek zu importieren, verwendet das Beispielprogramm late binding. Das Programm öffnet die Datei sample.xls im Verzeichnis spezial\automation-excel, liest die Zellen [A1] bis [C3] aus und zeigt die Werte im Konsolenfenster an, trägt in [A4] die aktuelle Zeit ein und schließt die Datei dann wieder. Falls danach keine weitere Excel-Datei geöffnet ist (Workbooks.Count = 0), wird Excel durch Quit beendet. (Die Abfrage ist deswegen wichtig, weil Excel ja möglicherweise schon vor dem Programm gestartet wurde und dann nicht willkürlich beendet werden soll.) Excel endet allerdings erst nach zwei garbage collections, die alle Verweise auf das Excel-Objekt aus dem Speicher räumen.
Abbildung 12.4: Excel-Daten auslesen
12.5 Externe Programme steuern (Automation)
' Beispiel spezial\automation-excel Option Strict Off Sub Main() ' Excel-Datei bearbeiten process_xl_file() ' Excel beenden GC.Collect() 'garbage GC.WaitForPendingFinalizers() 'auf das GC.Collect() 'garbage GC.WaitForPendingFinalizers() 'auf das ' Programmende Console.WriteLine("Return drücken") Console.ReadLine() End Sub
525
collection auslösen Ende der gc warten collection nochmals auslösen Ende der gc warten
Sub process_xl_file() Dim i, j As Integer Dim xl, wb, ws As Object Dim fname As String fname = IO.Path.Combine(Environment.CurrentDirectory, _ "..\sample.xls") wb = GetObject(fname) xl = wb.Application ' xl.Visible = True 'wenn Sie sehen wollen, was vor sich geht ' wb.NewWindow() ws = wb.Sheets(1) For i = 1 To 3 For j = 1 To 3 Console.WriteLine("Zelle in Zeile {0} / Spalte {1} ={2}", _ i, j, ws.Cells(i, j).Value) Next Next ws.Cells(4, 1).Value = Now ' wb.Windows(wb.Windows.Count).Close() wb.Save() wb.Close() If xl.Workbooks.Count = 0 Then xl.Quit() End Sub
526
12 Spezialthemen
12.5.3 Beispiel – RichTextBox mit Word ausdrucken Die in Abschnitt 14.4.4 vorgestellte RichTextBox hat einen gravierenden Nachteil: man kann damit nichts ausdrucken. Das folgende Beispielprogramm umgeht dieses Problem: Es kopiert den gesamten Inhalt eines derartigen Steuerelements in ein neues Word-Dokument, stattet das Dokument mit einer Kopfzeile samt Seitenzähler aus und zeigt es in der Vorschau an (siehe Abbildung 12.5). Der Anwender kann das Dokument nun bequem ausdrucken und muss Word dann selbst beenden.
Abbildung 12.5: Text aus RichTextBox mit Word ausdrucken
Das Programm setzt voraus, dass ein Verweis auf die Bibliothek Microsoft.Word eingerichtet wurde. Mit CreateObject wird ein neues Objekt der Klasse Word.Document erzeugt. Der folgende Code, um den Inhalt der Zwischenablage einzufügen, das Dokument mit einer Kopfzeile auszustatten und in der Seitenvorschau anzuzeigen, wurde unter Word mit der Makroaufzeichnung entwickelt und dann in den VB.NET-Code eingebaut. Die meisten Änderungen betrafen die Konstanten, die in Word direkt verwendet werden können, in VB.NET aber nur bei genauer Angabe der Aufzählung, in der sie definiert sind. Damit nicht jeder Anweisung doc.Application vorangestellt werden muss, wurde With eingesetzt.
12.5 Externe Programme steuern (Automation)
527
' Beispiel spezial\automation-word Private Sub Button1_Click(...) Handles Button1.Click Dim doc As Word.Document ' Inhalt der RichTextBox in die Zwischenablage kopieren RichTextBox1.SelectAll() RichTextBox1.Copy() ' neues Word-Dokument erzeugen; dieses ist automatisch ' das aktive Dokument doc = CType(CreateObject("Word.Document"), Word.Document) With doc.Application .Selection.Paste() .ActiveWindow.ActivePane.View.Type = Word.WdViewType.wdPrintView .ActiveWindow.ActivePane.View.SeekView = _ Word.WdSeekView.wdSeekCurrentPageHeader .Selection.TypeText( _ Text:="Kopfzeile" & vbTab & vbTab & _"Seite ") .Selection.Fields.Add(Range:=.Selection.Range, _ Type:=Word.WdFieldType.wdFieldPage) .ActiveWindow.ActivePane.View.SeekView = _ Word.WdSeekView.wdSeekMainDocument ' Druckvorschau .Visible = True .ActiveDocument.PrintPreview() AppActivate(.ActiveWindow.Caption) End With End Sub
Wenn Sie möchten, dass der Text sofort und ohne Rückfrage ausgedruckt wird ohne dass Word dabei sichtbar wird, ersetzen Sie die drei letzten Zeilen durch den folgenden Code. Damit wird das Dokument gedruckt. Wenn der Ausdruck abgeschlossen ist, wird das Dokument durch Close geschlossen. Die CType-Konstruktion ist erforderlich, weil VB.NET aus nicht ganz nachvollziehbaren Gründen zwei Close-Methoden erkennt und nicht weiß, welche es einsetzen soll. Wenn es anschließend keine offenen Dokumente in Word gibt, wird das Programm durch Quit beendet (wobei derselbe Namenskonflikt wie bei Close auftritt). .ActiveDocument.PrintOut(False) 'im Vordergrund drucken CType(doc, Word._Document).Close( _ Word.WdSaveOptions.wdDoNotSaveChanges) If .Documents.Count = 0 Then CType(.Application, Word._Application).Quit() End If
528
12 Spezialthemen
12.6
Multithreading
12.6.1 Grundlagen
HINWEIS
Ein Thread ist ein Teilprozess eines Programms. Ein gewöhnliches Programm besteht aus nur einem einzigen Thread zur Ausführung des Programms. Multithreading bedeutet, die Programmausführung auf mehrere Threads zu verteilen; diese Threads werden dann quasi parallel ausgeführt. Neben den von Ihnen verwalteten Threads gibt es noch mindestens zwei weitere Threads, die von den .NET-Bibliotheken benötigt werden. Diese dienen unter anderem dazu, regelmäßig den Speicher von nicht mehr benötigten Objekten durch eine garbage collection freizugeben. Deswegen zeigt der Task-Manager selbst bei einer einfachen Konsolenanwendungen mindestens drei Threads an. Auf die durch .NET vorgegebenen Threads haben Sie im Regelfall keinen direkten Einfluss. Wenn in diesem Abschnitt von Threads die Rede ist, sind deshalb nur die vom Hauptprogramm verwalteten Threads gemeint.
Wie parallel die Ausführung wirklich ist, hängt auch von Ihrer Hardware ab. Auf einem gewöhnlichen PC mit einer CPU kann immer nur ein Thread ausgeführt werden. Bei einem Multithreading-Programm wechselt das Betriebssystem aber automatisch alle paar Millisekunden zwischen den Threads, so dass der Eindruck der Gleichzeitigkeit entsteht. Nur auf einem Rechner mit mehreren CPUs ist es theoretisch möglich, dass zwei Threads wirklich gleichzeitig ausgeführt werden.
Multithreading-Modelle Es gibt verschiedene Arten des Multithreadings. Der Großteil der .NET-Bibliothek sowie gewöhnliche VB.NET-Anwendungen (inklusive Konsolenanwendungen) verwenden so genanntes free threading (freies Threading); dort ist das Erstellen neuer Threads und der Wechsel zwischen den Threads problemlos möglich. Jeder Thread ist selbstständig. Es besteht aber auch die Möglichkeit, mehrere Threads in so genannte apartments zu gruppieren. Soweit sich mehrere Threads im selben Apartment befinden, vereinfacht sich dadurch der gegenseitige Zugriff auf Objekte (d.h., ein Thread kann Methoden eines Objekts nützen, das in einem anderen Thread erstellt wurde etc. ). Es gibt zwei Apartment-Modelle: •
Windows.Forms-Programme basieren auf dem Singlethread-Apartment-Modell (STA). Allerdings ergeben sich daraus Einschränkungen, wenn ein neuer (eigener) Thread, der sich außerhalb des apartments befindet, auf Fenster und Steuerelemente zugreifen will (siehe Abschnitt 15.2.7).
•
Für Server-Anwendungen empfiehlt sich die Verwaltung eines so genannten ThreadPools unter Zuhilfenahme der ThreadPool-Klasse. Dabei werden die Threads in einem
12.6 Multithreading
529
Multithread-Apartment (MTA) verwaltet. Diese Technik wird in diesem Buch allerdings
nicht beschrieben.
Wozu Multithreading? Je nach Anwendung kann Multithreading verschiedene Vorteile mit sich bringen: •
Bei Server-Anwendungen, in denen ein Programm gleichzeitig auf unterschiedliche Datenanfragen (üblicherweise aus dem Netzwerk) antworten soll, kann Multithreading die Effizienz steigern. Der Grund: Zur Beantwortung jeder Anfrage müssen üblicherweise Daten aus einer Datei oder aus einer Datenbank gelesen werden. Dabei treten meistens kleine Verzögerungen auf, die ein Single-Threaded-Programm untätig abwarten müsste. Bei einem Multithreading-Programm findet bei einer derartigen Wartezeit automatisch ein Thread-Wechsel statt, so dass ein anderer Teilprozess mit der Beantwortung einer anderen Anfrage beginnen kann. Beachten Sie, dass diese Darstellung ein wenig vereinfacht ist. (Die Idee sollte aber klar werden.) Tatsächlich ist die Entwicklung effizienter Server-Anwendungen eine ziemlich diffizile Sache: Wie werden die Anfragen auf unterschiedliche Threads verteilt? Welche Thread-Anzahl führt zu einer optimalen Effizienz? (Mit der Zahl der Threads steigt auch der Verwaltungs-Overhead.)
•
Bei interaktiven Programmen (z.B. bei einer gewöhnlichen Windows-Anwendung) kann durch Multithreading ein höherer Benutzerkomfort erreicht werden. Beispielsweise kann eine Berechnung in einem eigenen Thread gestartet werden, während der Haupt-Thread für die Benutzeroberfläche weiterläuft. Damit wird die Benutzeroberfläche nicht durch die Berechnung blockiert. (Denselben Effekt haben Sie beispielsweise, wenn Sie in Word ein Dokument im Hintergrund ausdrucken und währenddessen ein anderes Dokument bearbeiten oder wenn Sie in Outlook E-Mails versenden und zugleich eine neue verfassen etc.)
ACHTUNG
Beachten Sie, dass Multithreading ein Programm nicht generell schneller macht – im Gegenteil! Multithreading bringt einigen Overhead mit sich, sowohl auf Betriebssystemebene durch die Thread-Wechsel als auch in Ihrem Programm, in dem die Threads miteinander kommunizieren und sich oft auch synchronisieren müssen. Wenn Sie also eine Aufgabe möglichst rasch erledigen möchten, ist der herkömmliche Single-Threaded-Lösungsansatz der effizienteste. Die Programmierung stabiler Multithreading-Anwendungen ist relativ kompliziert, insbesondere dann, wenn mehrere Threads gemeinsame Daten ändern müssen (was sich nur selten vermeiden lässt). Erschwerend kommt hinzu, dass die Fehlersuche in Multithreading-Anwendungen mühsam ist, dass eventuelle Fehler möglicherweise nur sehr selten auftreten und daher schwerer zu finden sind. Programmieren Sie also nicht einfach los, sondern lesen Sie sich vorher ausführlich in die Materie ein! (Dieser Abschnitt stellt nur eine erste Einführung dar!)
530
12 Spezialthemen
Wie eingangs bereits erwähnt, kann dieses Buch nur eine Einleitung in das Thema Multithreading geben. Weitere Basisinformationen finden Sie in der Hilfe, wenn Sie nach Multithreading in Visual Basic suchen:
VERWEIS
ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconThreadingInVisualBasic.htm
Wenn Sie sich speziell für die Komponentenprogrammierung interessieren, suchen Sie am besten nach Multithreading in Komponenten: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconScalabilityMultithreadingInComponents.htm
Wenn Sie sich für Hintergründe und fortgeschrittene Programmiertechniken interessieren (z.B. für die Verwaltung von Thread-Pools), suchen Sie nach Threads und Threading. Sie werden auf eine ausführliche Beschreibung des .NET-Threading-Modells stoßen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconthreading.htm
12.6.2 Programmiertechniken Neue Threads starten Fast alle Klassen zur Verwaltung von Multithreading-Anwendungen befinden sich im Namensraum System.Threading der Standardbibliothek mscorlib.dll. Um eine Prozedur in einem neuen Thread auszuführen, müssen Sie zuerst ein neues Thread-Objekt erzeugen. Dabei geben Sie als Parameter die Adresse der auszuführenden Prozedur oder Methode an. Die Prozedur darf keine Parameter haben. Funktionen bzw. Methoden mit einem Rückgabewert sind ebenfalls nicht zulässig. Anschließend führen Sie für das Objekt die Methode Start aus. Dim mythread As New Threading.Thread(AddressOf myprocedure) mythread.Start() Start übergibt den neuen Thread an das Betriebssystem. Dieses bestimmt, wann der Thread
tatsächlich gestartet wird. (Das ist meist nicht sofort der Fall, sondern erst nach ein paar Millisekunden.) Der neue Thread läuft nun parallel zum Hauptprogramm, d.h., die Programmausführung wechselt alle paar Millisekunden zwischen dem Hauptprogramm und der Prozedur myprocedure. Damit verlangsamt sich natürlich sowohl das Hauptprogramm als auch die Prozedur, weil die beiden Programmteile sich ja nun die verfügbare CPU-Zeit teilen müssen. Wenn eines der beiden Programmteile vorübergehend blockiert ist (etwa weil es auf das Öffnen einer Datei, die Herstellung einer Datenbankverbindung oder eine Benutzereingabe wartet), wird der andere Programmteil während dieser Zeit fast ungehindert ausgeführt.
12.6 Multithreading
531
Einführungsbeispiel Abbildung 12.6 zeigt die Ausgaben eines ersten, sehr einfachen Multithreading-Beispiels: Sowohl das Hauptprogramm als auch die Prozedur sub1, die in einem eigenen Thread ausgeführt wird, durchlaufen eine lange Schleife und geben während dieser Zeit 200 Mal den Buchstaben M (main) bzw. 1 (sub1) im Konsolenfenster aus. main wartet am Ende der Schleife durch Join darauf, bis auch die Schleife des zweiten Threads zu Ende ist.
Abbildung 12.6: Ein erstes Multithreading-Programm
' Beispiel spezial\multithread-intro Const max_loop As Integer = 1000 * 1000 * 100 Const interval As Integer = max_loop \ 200 Sub Main() Dim i As Integer Dim mythread As New Threading.Thread(AddressOf sub1) mythread.Start() For i = 0 To max_loop If i Mod interval = 0 Then Console.Write("M") Next mythread.Join() 'auf das Ende von mythread warten Console.WriteLine(vbCrLf + "Return drücken") Console.ReadLine() End Sub Sub sub1() Dim i As Integer For i = 0 To max_loop If i Mod interval = 0 Then Console.Write("1") Next End Sub
Periodischer Prozeduraufruf Anstatt eine Prozedur einmal in einem eigenen Thread zu starten, besteht auch die Möglichkeit, dies periodisch zu tun. Das ist vor allem dann sinnvoll, wenn die Prozedur eine Kontroll- oder Protokollierungsaufgabe übernehmen soll, die sehr rasch erledigt werden kann. Zu diesem Zweck müssen Sie zuerst ein Objekt der Delegate-Klasse TimerCallback erzeugen und dabei die Adresse der aufzurufenden Prozedur oder Methode angeben.
532
12 Spezialthemen
Dim mytimerDelegate As New Threading.TimerCallback( _ AddressOf method1)
Um die periodischen Aufrufe zu starten, erzeugen Sie ein Objekt der Timer-Klasse. Mit state wird ein beliebiges Objekt (oder Nothing) angegeben, das bei jedem Aufruf an die Prozedur oder Methode übergeben wird. n1 gibt an, nach wie vielen Millisekunden die Prozedur zum ersten Mal ausgeführt werden soll. (0 bedeutet, so schnell wie möglich.) n2 gibt das Intervall an, alle wie viel Millisekunden die Prozedur aufgerufen werden soll. Die Einstellungen n1 und n2 können später durch die Change-Methode des Timer-Objekts verändert werden. Dim tm As New Threading.Timer(mytimerDelegate, state, n1, n2)
Die aufzurufende Methode muss folgendermaßen deklariert werden. (Achten Sie auf die Parameterliste! Anders als bei einfachen Thread-Aufrufen ist hier die Übergabe eines Objekts zwingend vorgesehen.)
HINWEIS
Der periodische Prozeduraufruf erfolgt in einem Thread mit der Eigenschaft IsBackground = True. Deswegen enden dieser Thread und somit auch die periodischen Aufrufe automatisch mit dem Ende des Programms.
VERWEIS
Sub method1(ByVal state As Object)
Ein Beispiel für die Anwendung der Threading.Timer-Klasse finden Sie in Abschnitt 9.3.5. Beachten Sie, dass es speziell für Windows-Anwendungen eine eigene TimerKlasse gibt, die in Abschnitt 14.10.2 beschrieben wird.
Kommunikation zwischen Hauptprogramm und Thread An die mit mythread.Start() gestartete Prozedur oder Methode können keine Parameter übergeben werden. Ebensowenig kann die Prozedur nicht (wie eine Funktion) eine Ergebnis an das Hauptprogramm zurückliefern. Daher müssen andere Wege zur Kommunikation gesucht werden. Der einfachste Weg, an einen Thread Daten zu übergeben und später im Hauptprogramm Ergebnisse zu empfangen, bietet die Definition einer eigenen Klasse: Um eine Operation in einem neuen Thread zu starten, erzeugen Sie zuerst ein Objekt dieser Klasse und übergeben die Startdaten mittels Klassenvariablen oder Eigenschaften und Methoden. Dann verwenden Sie den neuen Thread, um eine Methode der Klasse auszuführen. Am Ende der Methode erfolgt die Rückmeldung an das Hauptprogramm über eine Ereignisprozedur. Dieser Mechanismus wird durch das folgende Beispiel veranschaulicht.
12.6 Multithreading
533
' Beispiel spezial\multithread-event Module Module1 Dim WithEvents calc As class1 Sub Main() Dim i As Integer Dim mythread As Threading.Thread calc = New class1() calc.data = 123 mythread = New Threading.Thread(AddressOf calc.method1) mythread.Start() ' ... Hauptprogramm fortsetzen End Sub Public Sub calc_Done(ByVal obj As Object, ByVal result As Integer) _ Handles calc.Done Console.WriteLine("calc_Done: result={0}", result) ' ... Ergebnis verarbeiten End Sub End Module Class class1 Public data As Integer Public result As Integer Public Event Done(ByVal obj As Object, ByVal result As Integer) Public Sub method1() ' ... eine komplizierte Berechnung result = data - 1 RaiseEvent Done(Me, result) End Sub End Class
Thread-Ausführung vorübergehend unterbrechen (Sleep) Wenn Sie den aktuellen Thread für einige Zeit unterbrechen möchten, können Sie dazu die Sleep-Methode verwenden. Als Parameter geben Sie die gewünschte Zeit in Millisekunden an. ' 5 Sekunden warten Threading.Thread.Sleep(5000) Sleep kann auch in gewöhnlichen (Single-Threaded-)Programmen verwendet werden und bewirkt dann, dass das gesamte Programm während der angegebenen Zeit ruht. Sleep ist
auf jeden Fall eventuellen Warteschleifen vorzuziehen, weil es keine CPU-Zeit verbraucht und stattdessen die Rechenzeit anderen Threads bzw. anderen Programmen des Computers zur Verfügung stellt.
HINWEIS
534
12 Spezialthemen
Sleep gilt immer für den gerade aktiven Thread! Wenn Sie im Einführungsbeispiel in main die Anweisung mythread.Sleep(1000) ausführen, dann gilt Sleep dennoch für main (nicht für mythread!).
Auf das Ende eines anderen Threads warten Wenn Sie im aktuellen Code darauf warten möchten oder müssen, bis ein anderer Thread mythread zu Ende ist, führen Sie mythread.Join() aus.
Threads vorübergehend anhalten (Suspend und Resume) Mit mythread.Suspend() können Sie die Ausführung eines Threads vorübergehend stoppen. mythread.Resume() setzt die Ausführung zu einem späteren Zeitpunkt fort. Ein Thread kann sich selbst in den Haltezustand versetzen (Threading.Thread.CurrentThread.Suspend()). Das ist aber nur dann ratsam, wenn sichergestellt ist, dass ein anderer Thread den ruhenden Thread wieder aufweckt.
Threads beenden oder abbrechen (Abort) Ein Thread endet automatisch, wenn das Ende der gestarteten Prozedur oder Methode erreicht wird. Um den aktuell laufenden Thread zu beenden, brauchen Sie also nur Exit Sub auszuführen. Von außen kann ein Thread durch mythread.Abort() beendet werden. Abort bewirkt, dass im betroffenen Thread eine ThreadAbortException ausgelöst wird. Außerdem wird der ThreadStatus des Threads auf AbortRequested gesetzt. An die Abort-Methode kann optional ein beliebiges Objekt übergeben werden, das bei der Auswertung des ThreadAbortException-Objekts aus der Eigenschaft ExceptionState entnommen kann. Das Objekt kann beispielsweise dazu dienen, den Thread über die Gründe des Abbruchs zu informieren. Die Reaktion eines Threads auf eine ThreadAbortException ist aus mehreren Gründen ungewöhnlich: •
Wenn der Code des Threads nicht abgesichert ist (wenn es also keine Try-Catch-Konstruktion gibt), wird der Thread ohne Fehlermeldung still beendet. Das Gesamtprogramm wird fortgesetzt (sofern es noch andere Vordergrund-Threads gibt). Das ist insofern untypisch, als nicht abgesicherte Exceptions im Allgemeinen zu einer Fehlermeldung und dann zum Programmende führen. Das außergewöhnliche Verhalten auf eine ThreadAbortException kann auch im Dialog DEBUGGEN|AUSNAHMEN nachvollzogen werden: Dort lautet die Einstellung (im Unterschied zu anderen Exceptions), dass das Programm ohne Fehlermeldung fortgesetzt werden soll.
•
Die ThreadAbortException kann wie jede andere Exception durch Try-Catch abgefangen werden. Am Ende der Try-Konstruktion wird der Fehler aber neuerlich ausgelöst und
12.6 Multithreading
535
der Thread somit trotz der Try-Konstruktion beendet. Sie können somit ein geordnetes Ende der Prozedur erreichen, offene Ressourcen schließen etc., aber im Gegensatz zu gewöhnlichen Exceptions gilt der Fehler durch die Try-Konstruktion nicht als behoben. Wenn Sie einen Thread trotz dieses merkwürdigen Verhaltens nach einer ThreadAbortException fortsetzen möchten, müssen Sie innerhalb des Catch-Blocks die Methode Threading.Thread.CurrentThread.ResetAbort() ausführen. ResetAbort darf nur dann verwendet werden, wenn für Ihr Programm erweiterte Sicherheitseinstellungen gelten (was oft nicht der Fall ist). Generell ist es nur in sehr seltenen Fällen sinnvoll, einen Thread trotz Abort-Aufforderung fortzusetzen. •
Der Finally-Block einer Try-Konstruktion wird auf jeden Fall ausgeführt, selbst dann, wenn die ThreadAbortException nicht durch Catch abgefangen wird (d.h., wenn es in der Try-Konstruktion gar keinen Catch-Block gibt oder wenn es nur Catch-Blöcke für andere Fehler gibt). Beachten Sie, dass ResetAbort im Finally-Block nicht mehr wirksam ist.
Wenn der Code eines Threads Dispose-Objekte erzeugt, Datenbankverbindungen herstellt, Dateien öffnet etc., sollte (zumindest) eine Fehlerabsicherung in der folgenden Form vorliegen: Sub method1() Try ... der eigentliche Code Finally ... Aufräumarbeiten, die in jedem Fall ausgeführt werden (auch dann, wenn eine ThreadAbortException auftritt) End Try End Sub
HINWEIS
mythread.Abort bewirkt also nicht, dass der betreffende Thread sofort beendet wird! Zum einen wird die ThreadAbortException erst dann tatsächlich ausgelöst, wenn die
Ausführung des betreffenden Threads aufgrund eines Thread-Wechsels fortgesetzt wird – und bis dahin können einige Millisekunden verstreichen. Zum anderen liegt es ja nun in der Verantwortung des Threads, wie es auf die ThreadAbortException reagiert. Es kann beispielsweise sein, dass die erforderlichen Aufräumarbeiten (z.B. zum Schließen einer Datei) einige Zeit in Anspruch nehmen. Aus diesem Grund ist es fast immer empfehlenswert, das tatsächliche Thread-Ende mit Join abzuwarten, also: mythread.Abort() mythread.Join()
Programmende Ein Multithreading-Programm läuft normalerweise so lange, bis alle Threads beendet sind. Wenn Sie also in einem Konsolenprogramm in main einige neue Threads starten und main dann verlassen, läuft das Programm so lange weiter, bis der letzte der Threads beendet ist.
536
12 Spezialthemen
VORSICHT
Wenn Sie ein geordnetes Programmende in main erreichen möchten, können Sie dort mit Join auf das Ende der anderen Threads warten. Radikaler ist es, die anderen Threads durch Abort gewaltsam zu beenden. Schließlich besteht die Möglichkeit, die neuen Threads gleich beim Start als Hintergrund-Threads zu kennzeichnen (mythread.IsBackGround = True). Das bewirkt, dass mit dem Ende des letzten Vordergrund-Threads alle Hintergrund-Threads automatisch durch Abort beendet werden. (Per Default gelten der Start-Thread sowie alle neuen Threads als Vordergrund-Threads.) Solange es unterbrochene Vordergrund-Threads gibt (Suspend), kann das Programm nicht beendet werden. Es darf daher auf keinen Fall passieren, dass als einziger Thread ein unterbrochener Thread übrig bleibt. (Dasselbe gilt natürlich auch für mehrere unterbrochene Threads.) Das Programm befindet sich dann in einer Sackgasse, aus der es nicht mehr herausfindet: Die unterbrochenen Threads warten darauf, dass irgendwo Resume ausgeführt wird, aber es gibt im Programm gar keine Stelle mehr, an der noch Code ausgeführt wird.
Thread-Eigenschaften Jedes Thread-Objekt ist mit einer Reihe von Eigenschaften ausgestattet, die Auskunft über den Zustand des Threads geben und zum Teil auch eine Veränderung ermöglichen. Priority gibt an, welche Priorität der Thread relativ zu anderen hat. Per Default ist jeder Thread mit ThreadPriority.Normal eingestuft. Durch die Einstellungen Below- oder AboveNormal kann ein Thread entsprechend seiner Wichtigkeit benachteiligt oder bevorzugt werden. Wenn die CPU voll ausgelastet wird, wird einem derartigen Thread daher weniger oder mehr Rechenzeit zugeordnet. CurrentCulture verweist auf ein CultureInfo-Objekt, dessen Inhalt unter anderem die Metho-
den zur Formatierung von Zahlen, Daten und Zeiten beeinflusst. Normalerweise übernehmen alle neu gestarteten Threads die Einstellungen des Start-Threads, aber es ist prinzipiell auch möglich, unterschiedliche Threads mit unterschiedlichen internationalen Einstellungen auszuführen.
VERWEIS
ThreadState gibt den aktuellen Zustand des Threads an. Mögliche Werte sind die Konstanten der ThreadState-Aufzählung, z.B. Running, Aborted etc. IsAlive liefert die Quintessenz von ThreadState, nämlich False, wenn ThreadState=Unstarted, Stopped oder Aborted, sonst True.
In der Online-Hilfe finden Sie ausführliche Informationen darüber, unter welchen Umständen ein Thread welchen Zustand einnimmt, wenn Sie nach Statuswerte der Threadaktivität suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconthreadactivitystates.htm
TIPP
12.6 Multithreading
537
Am einfachsten ist der Zugriff auf diese Eigenschaften natürlich über eine Variable mit einem Thread-Objekt (mythread.ThreadState etc.). Wenn Sie auf den gerade aktiven Thread zugreifen möchten (den, der die aktuelle Codeanweisung ausführt), können Sie dazu Threading.Thread.CurrentThread verwenden.
Liste aller Threads ermitteln Wenn Sie wissen möchten, welche Threads es im aktuellen Programm gibt, können Sie die Threads-Eigenschaft der Klassse Process des Namensraums System.Diagnostics auswerten (Bibliothek System.dll). Vorher müssen Sie mit GetCurrentProcess das Process-Objekt des aktuellen Programms ermitteln. Die Threads-Aufzählung liefert ProcessThread-Objekte, die weit mehr Eigenschaften aufweisen als die Thread-Objekte des Threading-Namensraum. Die folgenden Zeilen zeigen beispielhaft die Auswertung. Dim pr As Diagnostics.Process = _ Diagnostics.Process.GetCurrentProcess() Dim tr As Diagnostics.ProcessThread For Each tr In pr.Threads Console.WriteLine("id={0} starttime={1}", tr.Id, tr.StartTime) Next
Debugging
TIPP
Wenn die Programmausführung durch Stop, durch einen Haltepunkt oder durch einen Fehler in einem der Threads unterbrochen und der Debugger angezeigt wird, dann werden damit alle Threads unterbrochen. Im Threads-Fenster (DEBUGGEN|FENSTER|THREADS) ist der zuletzt aktive Thread markiert. Sie können dort in einen anderen Thread wechseln. Es erleichtert die Fehlersuche erheblich, wenn Sie jedem Thread beim Erzeugen einen Namen geben (myThread.Name="..."). Der Name wird im Threads-Fenster angezeigt.
Beachten Sie, dass die Entwicklungsumgebung bzw. der Debugger das Laufzeitverhalten von Multithreading-Programmen beeinflusst! Wenn Sie das Programm außerhalb der Entwicklungsumgebung starten, erfolgt der Thread-Wechsel bisweilen deutlich rascher als innerhalb der Entwicklungsumgebung. Wenn Sie Multithreading-Anwendungen entwickeln, sollten Sie das Programm also unbedingt auch außerhalb der Entwicklungsumgebung ausführlich testen!
538
12 Spezialthemen
VERWEIS
Beispiele Dieser Abschnitt hat lediglich Multithreading-Grundlagen vermittelt. Einige konkrete Anwendungen finden Sie im Stichwortverzeichnis unter Multithreading. Werfen Sie insbesondere einen Blick in die Abschnitte 9.3.5 und 15.2.7! Dort geht es um die korrekte Verwendung von Aufzählungsobjekten und um die Gestaltung von Multithreading-Windows-Programmen. Beachten Sie, dass auch jede asynchrone Operation (z.B. das asynchrone Lesen oder Schreiben von Dateien, siehe Abschnitt 10.7) intern auf Multithreading basiert.
Syntaxzusammenfassung System.Threading.Thread-Klasse – Methoden Abort( [obj] )
fordert den Thread dazu auf zu enden. Im Thread kommt es dadurch zu einer ThreadAbortException, wobei die optionalen Daten obj übergeben werden.
Join()
wartet auf das Ende des Threads.
ResetAbort()
setzt die Ausführung des Threads trotz einer ThreadAbortException fort.
Resume()
setzt einen unterbrochenen Thread fort.
Sleep()
unterbricht die Ausführung des gerade aktiven Threads für eine bestimmte Zeit. (Während dieser Zeit können andere Threads ausgeführt werden.)
Start()
startet den Thread.
Suspend()
hält die Ausführung eines Threads vorübergehend an. Der Thread kann mit Resume fortgesetzt werden.
System.Threading.Thread-Klasse – Eigenschaften ApartmentState
gibt an, ob der Thread Teil eines in einem Single- oder Multithread-Apartments (STA oder MTA) ist.
CurrentCulture
gibt das CultureInfo-Objekt des Threads an, das unter anderem Einfluss auf Konvertierungs- und Formatierungsmethoden hat (z.B. die Darstellung von Datum und Uhrzeit).
CurrentThread
verweist auf das Thread-Objekt des gerade laufenden Threads.
IsAlive
gibt an, ob der Thread grundsätzlich aktiv ist (ThreadState ungleich Unstarted, Stopped oder Aborted).
12.6 Multithreading
539
System.Threading.Thread-Klasse – Eigenschaften IsBackground
gibt an, ob der Thread ein Hintergrund-Thread ist. In diesem Fall endet er automatisch mit dem Hauptthread.
Name
gibt den Namen des Threads an (per Default leer).
Priority
gibt die Priorität des Threads an (per Default ThreadPriority.Normal).
ThreadState
gibt den aktuellen Zustand des Threads durch eine Konstante der ThreadState-Aufzählung an (z.B. Running, Stopped, Aborted, WaitSleepJoin).
12.6.3 Synchronisierung von Threads Wenn mehrere Threads auf gemeinsame Daten zugreifen müssen und zumindest einer der Threads die Daten auch verändern muss, sind Probleme im wörtlichen Sinne vorprogrammiert. Es kann vorkommen, dass ein Thread unterbrochen wird, während er die Daten ändert und nun ein anderer Thread die unvollständig veränderten Daten zu lesen versucht: Das Ergebnis sind korrupte Daten (die meist unmittelbar zu Fehlern führen) oder falsche Daten (was viel schlimmer ist, weil dieser Fall oft lange unbemerkt bleibt). Beachten Sie, dass der Thread-Wechsel vom Betriebssystem durchgeführt wird und zu jedem Zeitpunkt erfolgen kann, selbst während einer ganz elementaren Operation (z.B. x += 1)! Um derartige Probleme zu vermeiden, müssen die Threads synchronisiert werden. Das bedeutet, dass ein Thread mit dem Zugriff auf gemeinsame Daten warten muss, bis die anderen Threads fertig sind. Den einfachsten Weg bietet hierfür das VB-Konstrukt SyncLock: Damit wird ein Objekt angegeben, auf das ein Programmteil den alleinigen Zugriff beansprucht. Wenn das Objekt bei der Ausführung von SyncLock bereits durch einen anderen Thread blockiert ist, muss der aktuelle Thread warten, bis das Objekt wieder freigegeben wird. Sobald das Objekt frei ist, bekommt der aktuelle Thread den alleinigen Zugriff auf das Objekt. Nun müssen also alle anderen Threads warten. SyncLock data ... Code, der das Objekt data verändert End SyncLock
Mit SyncLock muss ein Objekt eines Referenztyps angegeben werden. ValueType-Variablen für elementare Datentypen wie Integer oder für Strukturen sind also ungeeignet. StringVariablen sind nur dann geeignet, wenn die Zeichenkette nicht geändert wird. (Bei jeder Veränderung wird ein neues String-Objekt erzeugt, womit der Schutz hinfällig wird.) Mit SyncLock kann nur ein Objekt angegeben werden. Wenn innerhalb der SyncLock-Codes mehrere Objekte bearbeitet bzw. verändert werden, dann müssen sich alle Codeteile auf ein gemeinsames Objekt für SyncLock einigen. (SyncLock ist ja kein Schutz gegen Veränderungen, sondern ein Schutz gegen die gleichzeitige Ausführung von Code. Insofern ist es
540
12 Spezialthemen
ganz egal, welches Objekt mit SyncLock angegeben wird, solange es nur immer dasselbe ist.)
HINWEIS
VORSICHT
.NET-intern verwendet SyncLock die Monitor-Klasse. Das Objekt wird durch Monitor.Enter(obj) gesperrt und durch Monitor.Exit(obj) wieder freigegeben. Die Online-Hilfe rät explizit davon ab, SyncLock auf Fenster oder Steuerelemente von Windows.Forms-Anwendungen anzuwenden. Sie riskieren dadurch einen deadlock (also einen Zustand, bei dem sich zwei Threads gegenseitig so blockieren, dass keiner mehr fortgesetzt werden kann). Lesen Sie zur Multithreading-Programmierung von Windows.Forms-Programmen unbedingt Abschnitt 15.2.7.
Vor allem wenn mehrere Threads im Spiel sind, wird eine derartige Synchronisierung zunehmend ineffizient. Sie sollten daher darauf achten, den Objektzugriff nicht länger als unbedingt erforderlich durch SyncLock zu blockieren. In manchen Fällen reicht die Sicherheit, dass einfache Operationen (z.B. das Vergrößern oder Verkleinern eines Zählers) nicht durch einen Thread-Wechsel unterbrochen werden. Zu diesem Zweck stellt die Interlock-Klasse die Methoden Increment, Decrement, Exchange und CompareExchange für einige elementare Datentypen zur Verfügung. Die Verwendung dieser Methoden ist deutlich effizienter als SyncLock.
Synchronisierungsbeispiel Das folgende Beispiel illustriert die Problematik. Als gemeinsame Daten dienen die Elemente des Integer-Felds data. Im main wird die Prozedur changearray in einem eigenen Thread gestartet. In dieser Prozedur werden unzählige Male zwei zufällige Elemente des Felds ausgewählt; eines dieser Elemente wird um einen zufälligen Wert erhöht, ein zweites um denselben Wert verkleinert. Die Summe aller Feldelemente beträgt daher immer 0.
Abbildung 12.7: Fehler aufgrund mangelnder Thread-Synchronisation
12.6 Multithreading
541
Solange der changearray-Thread läuft (mythread.IsAlive), wird in main immer wieder die Prozedur calc_sum aufgerufen. Diese Prozedur berechnet die Summe der Feldelemente. Wenn das Ergebnis ungleich 0 ist, wird der Wert ausgegeben. Abbildung 12.7 beweist, dass dieser Fall bei einem Testdurchlauf 21 Mal aufgetreten ist – eine Fehlerwahrscheinlichkeit von 0,0063 Prozent. (Das Ergebnis sieht natürlich jedes Mal ein wenig anders aus – mal mit mehr, mal mit weniger Fehlern. Es sollte aber aber klar sein, wie klein die Chance ist, einen derartigen Fehler durch Debugging klar nachzuvollziehen.) ' Beispiel spezial\multithread-syncerror Const max_loop As Integer = 1000 * 1000 * 10 Const arr_size As Integer = 1000 Dim data(arr_size - 1) As Long Dim error_counter As Long Sub Main() Dim counter As Integer Dim starttime As Date = Now Dim mythread As New Threading.Thread(AddressOf changearray) mythread.Start() While mythread.IsAlive counter += 1 calc_sum() End While Console.WriteLine("Summe wurde {0} Mal berechnet", counter) Console.WriteLine("Dabei traten {0} Fehler auf", error_counter) Console.WriteLine("Zeit: {0}", Now.Subtract(starttime)) End Sub ' vergrößert ein Element des Feldes um tmp, ' reduziert ein anderes Element ebenfalls um tmp Sub changearray() Dim i As Integer Dim index1, index2 As Integer Dim tmp As Integer Dim rand As New Random() For i = 0 To max_loop tmp = rand.Next(100) index1 = rand.Next(arr_size) index2 = rand.Next(arr_size) ' SyncLock data data(index1) += tmp '(1) data(index2) -= tmp '(2) ' End SyncLock Next End Sub
542
12 Spezialthemen
' die Summe des Felds sollte immer 0 sein Sub calc_sum() Dim i As Integer Dim sum As Long ' SyncLock data For i = 0 To arr_size - 1 sum += data(i) Next 'End SyncLock If sum <> 0 Then Console.WriteLine("Summe: {0}", sum) error_counter += 1 End If End Sub
Die Fehlerursache sollte leicht verständlich sein: Wenn der changearray-Prozess genau zwischen den Anweisungen (1) und (2) unterbrochen wird und nun main die Feldsumme berechnet, ergibt sich eine Summe ungleich 0. Die Lösung des Problems ist im Code bereits angedeutet: Die beiden Anweisungen zur Änderung der Feldelemente sowie die Schleife zur Berechnung der Summe müssen durch SyncLock gekapselt werden. Damit gehören die Rechenfehler der Vergangenheit an. Der dramatische Anstieg der Rechenzeit auf ein Mehrfaches hat übrigens weniger mit dem Locking-Overhead zu tun als damit, dass die relativ zeitaufwendige Summenberechnung wegen der SyncLock-Anweisung gegenüber dem kurzen SyncLock-Block in changearray bevorzugt wird und unverhältnismäßig viel Rechenzeit zugeordnet bekommt. Aus diesem Grund wird die Summe sehr viel öfter als bisher berechnet.
Konflikte durch Mehrfachausführung von Code Was passiert, wenn ein und diesselbe Prozedur von mehreren Threads gleichzeitig ausgeführt wird? Die Dokumentation zu dieser Frage ist eher spärlich, weswegen ich ein bisschen experimentiert habe. Dabei hat sich gezeigt, dass lokale Variablen, die innerhalb einer Prozedur deklariert werden, tatsächlich lokal sind – also auch lokal für jeden Thread. Im folgenden Beispielprogramm wird die Prozedur sub1 durch zwei Threads beinahe gleichzeitig gestartet. Die Daten für die Schleifenvariablen i und j und die beiden Zähler k und x werden für jeden Thread an unterschiedlichen Orten im Speicher abgelegt. Daher kommen sich die beiden Threads nicht in die Quere und laufen vollkommen unabhängig voneinander ab. ' Beispiel spezial\multithread-conflicts Const loopsize As Integer = 1000 * 1000 * 10 Sub Main() Dim counter As Integer Dim starttime As Date = Now Dim mythread1 As New Threading.Thread(AddressOf sub1)
12.6 Multithreading
543
Dim mythread2 As New Threading.Thread(AddressOf sub1) mythread1.Name = "1" mythread2.Name = "2" mythread1.Start() mythread2.Start() mythread1.Join() mythread2.Join() ... Fortsetzung siehe etwas weiter unten End Sub Sub sub1() Dim i, j, k As Integer Dim x As Double For i = 1 To 10 For j = 1 To loopsize k += 1 x += Math.Sin(k) Next Console.WriteLine("thread={0}: i={1}, k={2}", _ Threading.Thread.CurrentThread.Name, i, k) Next End Sub
Thread-sichere Klassen und Methoden Bei der Beschreibung der .NET-Klassen wird immer wieder darauf hingewiesen, welche Methoden der Klasse Thread-sicher sind und welche nicht. Was bedeutet das aber? Thread-sicher bedeutet, dass Methode desselben Objekts von mehreren Threads quasi gleichzeitig ausgeführt werden dürfen, ohne dass es dabei zu Konflikten kommt. Eine kleine Variation des obigen Beispiels beweist, dass die Thread-Sicherheit durchaus nicht selbstverständlich ist. In den Threads mythread3 und -4 wird jeweils die Methode sub1 desselben Objekts der Klasse class1 ausgeführt. i, j, k und x sind nun Klassenvariablen, die von allen Threads gemeinsam genutzt werden. Aus diesem Grund kommt es nun zu Zugriffskonflikten; das Programm liefert nun eine ziemlich wirre Bildschirmausgabe (siehe Abbildung 12.8). Natürlich werden Schleifenvariablen üblicherweise auch bei Methoden von Klassen lokal definiert und bereiten dann keine Probleme. Aber bei den meisten Klassen gibt es Klassenvariablen, die von Methoden und Eigenschaften gemeinsam verwendet werden; welche Probleme dadurch entstehen können, wird durch das folgende Beispielprogramm demonstriert. (Dieselben Probleme würden natürlich auch dann auftreten, wenn Sie im vorigen Beispiel die die Zeilen mit der Variablendeklaration außerhalb von sub1 durchgeführt hätten. Die Probleme haben also nichts mit der Frage Modul versus Klasse zu tun, sondern damit, ob es gemeinsam genutzte Variablen gibt.)
544
12 Spezialthemen
' Beispiel spezial\multithread-conflicts Sub Main() ... Fortsetzung von oben Dim myobj As New class1() Dim mythread3 As New Threading.Thread(AddressOf myobj.sub1) Dim mythread4 As New Threading.Thread(AddressOf myobj.sub1) mythread3.Name = "3" mythread4.Name = "4" mythread3.Start() mythread4.Start() mythread3.Join() mythread4.Join() End Sub Class class1 Dim i, j, k As Integer Dim x As Double Sub sub1() For i = 1 To 10 For j = 1 To loopsize k += 1 x += Math.Sin(k) Next Console.WriteLine("thread={0}: i={1}, k={2}", _ Threading.Thread.CurrentThread.Name, i, k) Next End Sub End Class
Abbildung 12.8: Die Klassenvariablen i, j und k werden von meheren Threads gleichzeitig benutzt
12.6 Multithreading
545
Schlussfolgerungen für die Programmierung eigener Klassen Wenn Sie selbst Klassen programmieren möchten, die Thread-sicher sind, müssen Sie jede Methode der Klasse, die auf gemeinsame Klassenvariablen zugreift, durch SyncLock Me absichern. (Shared-Methoden, die keine gemeinsamen Daten verwenden, müssen dagegen nicht abgesichert werden.) Class class1 Dim ... Sub sub1() SyncLock Me ... Code mit Zugriff auf gemeinsame Klassenvariablen End SyncLock End Sub End Class
VERWEIS
Diese Vorgehensweise ist allerdings insofern unbefriedigend, weil die vielen SyncLock-Anweisungen die Anwendung der Klasse in Multithreading-Anwendungen sehr ineffizient machen und unter Umständen Deadlock-Probleme verursachen können. (Ein Deadlock ist so ziemlich das Schlimmste, was in einer Multithreading-Anwendung passieren kann: Thread A wartet, dass Thread B Daten freigibt, und umgekehrt. Das ist eine Pattsituation, aus der es keinen Ausweg gibt.) Aus diesem Grund sind die meisten .NET-Klassen nicht Thread-sicher und es bleibt dem Anwender der Klasse überlassen, die Methoden so aufzurufen, dass es keine Konflikte geben kann. Weitere Informationen zum Thema Thread-Sicherheit finden Sie, wenn Sie nach Entwurfsrichtlinien für das Threading oder nach Threadsichere Komponenten suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpgenref/html/cpconthreadingdesignguidelines.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconThread-SafeComponents.htm
Schlussfolgerungen für Anwender von nicht Thread-sicheren Methoden Wenn Sie in einer Multithreading-Anwendung nicht Thread-sichere Methoden anwenden (und das ist der Regelfall!), müssen Sie darauf achten, dass es ausgeschlossen ist, dass unterschiedliche Threads gleichzeitig Methoden oder Eigenschaften eines gemeinsamen Objekts ausführen. Die einfachste Lösung besteht darin, für jeden Thread eigene Objekte zu erzeugen. Wenn das nicht möglich ist und die Threads auf gemeinsame Objekte zugreifen müssen, dann müssen Sie vor dem Aufruf derartiger Methoden die bereits beschriebenen Synchronisationsmechanismen anwenden (also SyncLock).
546
12 Spezialthemen
12.7
API-Funktionen verwenden (Declare)
12.7.1 Grundlagen API steht für Application Programming Interface und bezeichnet die Programmierschnittstelle des Betriebssystems. Diese Schnittstelle besteht aus einer riesigen Zahl von Funktionen, die sich in diversen DLL-Dateien befinden. (Beispielsweise enthält gdi32.dll grundlegende Grafikfunktionen.) DLL ist die Kurzform für Dynamic Link Libraries und bezeichnet Bibliotheken, die bei Bedarf vom Betriebssystem geladen werden. In den Zeiten vor .NET waren API-Funktionen der Schlüssel zu zahllosen Zusatzfunktionen, die in VB6 (oder auch in anderen Programmiersprachen) nicht direkt zur Verfügung standen. Es gab nur wenige professionelle VB6-Programme, die ohne den Aufruf irgendwelcher API-Funktionen realisiert werden konnten. Mit .NET hat sich das grundlegend geändert: Fast alle Betriebssystemfunktionen können nun komfortabel über .NET-Bibliotheken genutzt werden. Damit hat der direkte Aufruf von API-Funktionen ganz wesentlich an Bedeutung verloren. Leider sind die .NET-Bibliotheken aber noch unvollständig, und die eine oder andere Betriebssystemfunktion kann nach wie vor nur als API-Funktionen genutzt werden. Außerdem gibt es unzählige VB6Programme, bei deren .NET-Portierung es oft einfacher ist, eine API-Funktionen wie bisher aufzurufen, als nach der äquivalenten .NET-Klasse zu suchen. Dieser Abschnitt vermittelt nur einen ersten Einstieg in das Thema API-Funktionen. Weitere Informationen finden Sie in der Hilfe, wenn Sie nach Verwenden nicht verwalteter DLL-Funktionen bzw. nach Exemplarische Vorgehensweise: Aufrufen von Windows-APIs suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconconsumingunmanageddllfunctions.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconCallingWindowsAPIs.htm
VERWEIS
Eine Fülle brauchbarer API-Informationen (allerdings für VB6) finden Sie hier im Internet: http://www.allapi.net/
Gute API-Bücher für VB-Programmierer gibt es momentan ebenfalls nur für VB6. Besonders empfehlenswert sind Hardcore Visual Basic von Bruce McKinney und Visual Basic Programmer's Guide to the Win32 API von Dan Appleman. Ob es je API-Bücher für VB.NET geben wird, ist wegen der sinkenden Bedeutung der API-Funktionen eher zweifelhaft. Einen wie immer fundierten Einstieg bietet Dan Appleman in seinem Buch Moving to VB.NET (immerhin 30 Seiten zu APIFunktionen, mit einigen ziemlich fortgeschrittenen Beispielen). Viele Nebenaspekte, die im Zusammenhang mit API-Funktionen zu beachten sind, werden auch in den Wälzern .NET and COM: The Complete Interoperability Guide von Adam Nathan und COM and .NET Interoperability von Andrew Troelsen behandelt.
12.7 API-Funktionen verwenden (Declare)
547
API-Funktionen verwenden Bevor eine API-Funktion in einem VB.NET-Programm aufgerufen werden kann, muss sie mit all ihren Parametern durch Declare deklariert werden. Dabei treten in der Praxis einige Probleme auf: •
Die Syntax von Declare ist relativ unübersichtlich – nicht zuletzt deshalb, weil die Syntax seit ihrer Einführung (Windows 3.1!) immer wieder erweitert werden musste, um der Weiterentwicklung der Betriebssystemfunktionen Rechnung zu tragen.
•
Es ist nicht immer einfach, die richtige API-Funktion für eine konkrete Aufgabe in der Dokumentation zu finden. Alle API-Funktionen sind in der MSDN-Hilfe dokumentiert, die im Rahmen von VS.NET installiert wird. Ein erster Startpunkt für die Suche sollte das Hilfeverzeichnis MSDN-LIBRARY|WINDOWS-ENTWICKLUNG|WIN32-API sein.
•
Wenn das gelungen ist, muss die Parameterliste an die Syntax bzw. die Merkmale von VB.NET angepasst werden. (API-Funktionen sind eigentlich für den Aufruf durch die Programmiersprache C vorgesehen und sind entsprechend dokumentiert.) Besonders schwierig ist es, C-Datenstrukturen in VB.NET nachzubilden. Es gibt zahllose Attribute, mit denen der VB.NET-Compiler angewiesen werden muss, bestimmte Optimierungen nicht durchzuführen, die Reihenfolge von Strukturelementen im Speicher nicht zu verändern etc.
Sobald die Declare-Anweisung richtig durchgeführt ist, können Sie die API-Funktion wie eine gewöhnliche Prozedur oder Methode aufrufen. Falls dabei ein Fehler auftritt, können Sie Err.LastDLLError Informationen über den Fehler entnehmen. Beachten Sie, dass API-Funktionen als unmanaged code ausgeführt werden müssen. (Die API-Funktionen sind ja Teil von Bibliotheken, die außerhalb der .NET-Welt stehen.) Deswegen kommen die diversen .NET-Sicherheitsmechanismen nicht zum Tragen. Und aus diesem Grund dürfen API-Funktionen nur dann aufgerufen werden, wenn Ihr Programm in der höchsten .NET-Sicherheitsstufe ausgeführt wird.
Einführungsbeispiel Bevor die Syntax von Declare etwas ausführlicher beschrieben wird, gibt das folgende Beispiel eine erste Vorstellung, wie die Anwendung von API-Funktionen aussieht. Das Beispiel ermittelt den Pfad des Windows-Verzeichnisses (den Sie natürlich viel bequemer mit Environ("windir") ermitteln können). Dazu wird die Funktion GetEnvironmentVariable aus der Kernel32-Bibliothek eingesetzt. An diese Funktion muss im ersten Parameter der Name der gesuchten Variable übergeben werden. An den zweiten Parameter wird die StringVariable übergeben, in die das Ergebnis eingetragen wird. Der dritte Parameter gibt die maximal zulässige Länge der Zeichenkette an. Die Ergebnisvariable winpath muss vor dem Aufruf der Funktion mit Leerzeichen initialisiert werden. Nach dem Aufruf muss die Zeichenkette auf die tatsächliche Länge verkürzt werden. (GetEnvironmentVariable beendet die Zeichenkette mit einem 0-Zeichen, das von VB.NET aber nicht als Ende einer Zeichenkette erkannt wird.)
548
12 Spezialthemen
' Beispiel spezial\api-intro Private Declare Auto Function GetEnvironmentVariable Lib "kernel32" _ (ByVal lpName As String, ByVal lpBuffer As String, _ ByVal nSize As Integer) As Integer Sub Main() Dim pos As Integer Dim winpath As String = Space(256) GetEnvironmentVariable("windir", winpath, 255) winpath = Left(winpath, InStr(winpath, Chr(0)) - 1) Console.WriteLine("Windows-Verzeichnis: " + winpath) Console.WriteLine("Return drücken") Console.ReadLine() End Sub
Declare-Syntax Die Kurzfassung der Syntax sieht folgendermaßen aus (je nachdem, ob die API-Funktion einen Rückgabewert liedert oder nicht): Declare [Auto|Ansi|Unicode] Sub name Lib "lib.dll" _ [Alias "name2"] (parameterliste) Declare [Auto|Ansi|Unicode] Function name Lib "lib.dll" _ [Alias "name2"] (parameterliste) As datentyp Declare-Anweisungen müssen in Modulen oder Klassen durchgeführt werden (nicht auf Codedateiebene, nicht innerhalb von Prozeduren). Declare kann ein Gültigkeitsbezeichner vorangestellt werden (z.B. Public, Private etc.).
In den Zeiten von Windows 3.1 und Windows 95 verarbeiteten alle API-Funktionen ausschließlich ANSI-Zeichenketten. Bei neueren Versionen vollzog Microsoft dann den Schritt hin zu Unicode. Aus Kompatibilitätsgründen musste der ANSI-Zeichensatz aber weiter unterstützt werden. Deswegen gibt es von vielen API-Funktionen zwei Versionen, deren Name sich durch den Endbuchstaben unterscheidet: nameA verarbeitet ANSI-Zeichenketten, nameW verarbeitet Unicode-Zeichenketten. (W steht für wide und bezieht sich darauf, dass für die Darstellung jedes Zeichens zwei Byte erforderlich sind.) Die optionalen Schlüsselwörter Ansi (gilt per Default), Auto und Unicode geben an, welche Version der API-Funktion aufgerufen werden soll und ob VB.NET beim Aufruf automatisch eine Umwandlung zwischen Unicode (dem Defaultformat aller VB.NET-Zeichenketten) und ANSI durchführt. •
Ansi: Es wird die ANSI-Version der API-Funktion aufgerufen. An den Funktionsnamen wird bei Bedarf automatisch der Buchstabe -A angefügt. VB.NET-Zeichenketten werden vor dem Aufruf von Unicode zu ANSI konvertiert, nach dem Aufruf zurück zu Unicode. Ansi gilt aus Kompatibilitätsgründen zu VB6 als Defaulteinstellung.
12.7 API-Funktionen verwenden (Declare)
•
549
Unicode: Es wird die Unicode-Version der API-Funktion aufgerufen. An den Funktionsnamen wird der Buchstabe -W angefügt. Eine Konvertierung der Zeichenketten ist
nicht erforderlich. •
Auto: Es wird je nach Betriebssystem die ANSI-Version (Windows 98/ME) oder die
Unicode-Version verwendet (Windows NT/2000/XP). Das ist insofern praktisch, als die Unicode-Variante zwar effizienter ist, aber manche API-Funktionen unter Windows 98/ME nach wie vor nur in der ANSI-Variante zur Verfügung stehen. Lib gibt den Namen der Bibliothek an, in der sich die DLL-Funktion befindet (z.B. "kernel32.dll"). Die Bibliothek wird im Windows-Verzeichnis, im Windows-Systemverzeichnis und in dem Verzeichnis gesucht, in dem sich die gerade ausgeführte *.exe-Datei befindet.
Mit dem optionalen Alias-Schlüsselwort können Sie den tatsächlichen API-Funktionsnamen angeben. Damit können Sie Namenskonflikte vermeiden (wenn die API-Funktion denselben Namen wie eine bereits vorhandene VB.NET-Funktion hat oder wenn der APIFunktionsname unter VB.NET nicht gültig ist). Die folgende Zeile bewirkt, dass durch den VB.NET-Code abc(...) die API-Funktion GetEnvironmentVariableA aufgerufen wird. Declare Sub abc Lib "kernel32" Alias GetEnvironmentVariableA (...)
Parameterübergabe Die Angabe der Parameterliste erfolgt in derselben Syntax wie bei der Deklaration von VB.NET-Prozeduren, -Methoden oder -Funktionen. Das Kunststück besteht aber darin, die richtige Entsprechung zwischen den Parametern in C-Notation (laut API-Dokumentation) und der VB.NET-Syntax zu finden. Generell müssen Parameter meistens als Wertparameter übergeben werden (also mit ByVal deklariert werden). Nur wenn die API-Funktionen einen Zeiger auf eine Zahl oder auf ein einzelnes Zeichen erwarten muss ByRef verwendet werden. Achtung, Zeichenketten werden immer mit ByVal übergeben! Zahlen: Die Übergabe von Zahlen bereitet selten Probleme. Die folgende Tabelle gibt einige wichtige C- bzw. API-Datentypen und ihre VB.NET-Entsprechung an. C / Win32API
VB.NET
BYTE
ByVal Byte
short, SHORT, WORD
ByVal Short
USHORT
ByVal UInt16 oder ByVal Short
int, long, INT, LONG, DWORD
ByVal Integer
ULONG
ByVal UInt32 oder ByVal Integer
float
ByVal Single
double
ByVal Double
LPSHORT, LPWORD etc. (LP steht für long pointer, daher ByRef!)
ByRef datentyp oder ByVal IntPtr
550
12 Spezialthemen
C / Win32API
VB.NET
short *, word *, int * etc.
ByRef datentyp oder ByVal IntPtr
(* meint in C für einen Zeiger) Zeiger: Viele API-Funktionen erwarten Zeiger (pointer, handle) auf betriebssysteminterne Datenstrukturen (z.B. auf einen so genannten device context, der ein Grafikobjekt beschreibt). Grundsätzlich handelt es sich dabei um Integer-Daten. Wenn der Zeiger mit einer .NET-Methode ermittelt wird, die IntPtr als Ergebnis zurückgibt, muss IntPtr auch bei der Deklaration verwendet werden. (Ein entsprechendes Beispiel finden Sie am Ende dieses Abschnitts.) Zeichenketten: API-Funktionen erwarten 0-terminierte Zeichenketten und geben auch solche Zeichenketten zurück. Bei der Übergabe von Zeichenketten an API-Funktionen ist das kein Problem – VB.NET kümmert sich darum, dass die API-Funktionen die Zeichenketten so erhalten, dass sie damit umgehen können. Bei der Rückgabe von Zeichenketten ist aber Vorsicht geboten. Erstens muss die StringVariable schon vor dem Aufruf eine Zeichenkette enthalten, die so groß ist, dass die größtmögliche Ergebniszeichenkette der API-Funktion darin gespeichert werden kann. Die APIFunktion überschreibt die vorhandene Zeichenkette durch die zurückgegebenen Daten. (Wenn die vorgegebene Zeichenkette zu kurz ist, schneidet VB.NET das API-Ergebnis ohne Fehlermeldung ab.) Zweitens müssen Sie bei der Weiterverarbeitung der Zeichenkette die C-typische 0-Terminierung beachten. Das bedeutet, dass das Ende einer Zeichenkette durch ein 0-Zeichen ausgedrückt wird. Wenn Sie also eine Zeichenkette vor dem Aufruf mit 256 Leerzeichen gefüllt haben (s = Space(256)) und die API-Funktion dorthin eine Ergebniszeichenkette von nur 20 Zeichen speichert, liefert Len(s) nach wie vor 256! Das liegt daran, dass für VB.NET ein 0-Zeichen wie jedes andere Zeichen gilt. Mit der folgenden Anweisung (die bereits im Einführungsbeispiel zur Anwendung gekommen ist) wandeln Sie die API-Zeichenkette in eine unter VB.NET übliche Zeichenkette um. s = Left(s, InStr(s, Chr(0)) - 1)
Wenn Sie häufig API-Funktionen aufrufen, bietet es sich an, eine kleine VB.NET-Funktion zur Zeichenkettenkonvertierung einzusetzen. s = C2VB(s) Function C2VB(ByVal s As String) As String Dim pos As Integer pos = InStr(s, Chr(0)) If pos = 0 Then Return "" Return Left(s, pos - 1) End Function
12.7 API-Funktionen verwenden (Declare)
551
Datenstrukturen: Wenn API-Funktionen als Parameter eine Datenstruktur erwarten, besteht das Problem darin, eine äquivalente Datenstruktur mit VB.NET nachzubilden. Dabei ist entscheidend, dass sich die einzelnen Elemente der Datenstruktur exakt an der Position im Speicher befinden, an der die API-Funktion – vertrauend auf die Regeln der Programmiersprache C – diese erwartet. In VB.NET müssen Sie Datenstrukturen durch Structure – End Structure bilden. Dabei entscheidet normalerweise der VB.NET-Compiler, wie er die Daten im Speicher organisiert. Um ein C-konformes Speicherlayout zu erzwingen, müssen Sie das Attribut StructLayout mit den Parametern LayoutKind.Sequential und Pack:=1 verwenden. Das bedeutet, dass die Elemente der Struktur in der von Ihnen angegebenen Reihenfolge im Speicher abgelegt werden und dass so genanntes single-byte-packing verwendet wird. Das bedeutet, dass Mehrbyteparameter an jedem beliebigen Byte beginnen dürfen. (Normalerweise werden Short-Daten nur an geraden Adressen und Integer-Daten nur an Adressen abgelegt, die ein Vielfaches von Vier betragen.) Das Attribut StructLayout ist im Namensraum Runtime.InteropServices definiert. Imports System.Runtime.InteropServices <StructLayout(LayoutKind.Sequential, Pack:=1)> _ Structure structname Dim hWnd As Integer Dim wFunc As Integer ... End Structure
VERWEIS
Ein weiteres Problem besteht darin, dass manche API-Datenstrukturen Zeichenketten oder Felder mit fixer Länge verwenden. Beides ist in VB.NET in Strukturen nicht vorgesehen. Das Problem kann umgangen werden, indem einzelne Elemente der Struktur mit dem Attribut MarshalAs deklariert werden, das ebenfalls im Namensraum Runtime.InteropServices definiert ist. Ein Beispiel für die Deklaration einer Datenstruktur mit dem Attribut StructLayout finden Sie in Abschnitt 10.3.3.
Konstanten: Viele API-Funktionen erwarten in einzelnen Parametern numerische Konstanten. Das Problem liegt hier weder bei der Deklaration des Parameters noch bei der Übergabe der Werte an die Funktion, sondern darin, den Wert der Konstante festzustellen. In der Dokumentation ist nämlich normalerweise nur der Name, nicht aber der Wert der Konstanten zu finden. Um die Konstanten zu ermitteln, können Sie entweder den im nächsten Abschnitt beschriebenen API-Viewer einsetzen, oder Sie müssen die entsprechende C-Header-Datei suchen, in der die Konstanten definiert sind. Beispielsweise finden Sie die Konstanten der GDI32-Bibliothek in der Datei Programme\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include\WinGDI.h. (Diese Datei befindet sich nur dann auf Ihrem Rechner, wenn Sie bei der Visual-Studio-Installation die Visual-C++-Klassenbibliotheken mitinstalliert haben!)
552
12 Spezialthemen
API-Deklaration mit DllImportAttribut Als Variante zu Declare können API-Funktionen auch wie gewöhnliche Funktionen mit Function – End Function deklariert werden, wenn das Attribut Runtime.InteropServices.DllImport vorangestellt wird. Innerhalb der Funktion braucht kein Code angegeben zu werden, weil ja ohnedies eine externe API-Funktion aufgerufen wird. Die folgenden Zeilen deklarieren nochmals die aus dem Einführungsbeispiel schon vertraute Funktion GetEnvironmentVariable. Im ersten Parameter von DllImport wird der Bibliotheksname angegeben. EntryPoint gibt den internen Funktionsnamen an. Das angehängte W bedeutet, dass die Unicode-Version (wide) der Funktion verwendet werden soll. Entsprechend gibt CharSet an, dass Zeichenketten beim Transport zwischen VB.NET und der APIFunktion im Unicode-Zeichensatz belassen werden sollen. Die DllImport-Schreibweise bietet im Wesentlichen dieselben Funktionen wie Declare, die Syntax ist aber noch unübersichtlicher. Insofern spricht nichts dagegen, bei Declare zu bleiben. Die Kenntnis von DllImport ist dennoch wertvoll, wenn Sie C#-Code lesen: Dort gibt es keine Declare-Anweisung, d.h., alle API-Funktionen müssen via DllImport definiert werden. Imports System.Runtime.InteropServices _ Private Function GetEnvironmentVariable(ByVal lpName As String, _ ByVal lpBuffer As String, ByVal nSize As Integer) As Integer
TIPP
End Function
Wenn Ihnen die Beschreibung mancher Declare-Syntaxelemente in der Online-Hilfe vage erscheint, suchen Sie in der Hilfe das äquivalente DllImport-Schlüsselwort! Sie werden mit einer deutlich präziseren Beschreibung belohnt.
12.7.2 API-Viewer Der vorige Abschnitt hat wahrscheinlich klar gemacht, dass es mühsam sein kann, die Deklaration für API-Funktionen, Konstanten und Strukturen zusammenzustellen. Mit VB6 wurde deswegen das Programm API-Viewer mitgeliefert, das Zugriff auf eine Datenbank vordefinierter Declare-, Const- und Structure-Anweisungen gab. Mit VB.NET ist dieses Programm aber verschwunden. Pramod Kumar Singh hat sich die Mühe gemacht, eine VB.NET- und C#-taugliche Version dieses Programms zu entwickeln. Sie finden das Programm (samt VB.NET-Quelltext) im Internet, wenn Sie nach API Viewer for VB.NET suchen. Zuletzt wurde das Programm hier zum Download angeboten: http://www.freevbcode.com/ShowCode.Asp?ID=3639
Nach dem Programmstart müssen Sie die Datei win32api.txt öffnen. Diese von Microsoft zusammengestellte Datei enthält Declare-Anweisungen und Konstantendefinitionen in der
12.7 API-Funktionen verwenden (Declare)
553
Syntax von VB6. Die Datei wird mit dem API-Viewer normalerweise nicht mitgeliefert. Wenn Sie VB6 noch auf Ihrem Rechner installiert haben, finden Sie diese Datei im Verzeichnis Programme\Microsoft Visual Studio\Common\Tools\Winapi. Eine aktualisierte Version befindet sich laut News-Gruppenberichten im Microsoft-Platform-SDK-Paket. Sie finden dieses 900 MByte große Paket im Download-Bereich von www.microsoft.com. Ich habe den Riesen-Download allerdings nicht durchgeführt und kann nicht garantieren, dass win32api.txt wirklich Teil des Pakets ist.
TIPP
Egal welche Version von win32api.txt Sie nun verwenden – nach dem Laden der Datei können Sie im API-Viewer nach Funktionsdeklarationen, Konstanten und Datenstrukturen suchen. Das Programm wandelt die Deklarationen in die VB.NET-Syntax um und Sie können den Code dann über die Zwischenablage in Ihr Programm kopieren. Sie finden den API Viewer for VB.NET (Stand Juni 2002) und die Datei win32api.txt (die mit VB6 gelieferte Version, Stand Juni 1998) auch auf der beiliegenden CD im Verzeichnis spezial\win32api.
Abbildung 12.9: Der API-Viewer
HINWEIS
554
12 Spezialthemen
Erwarten Sie sich von dem Programm keine Wunder! Abbildung 12.9 zeigt beispielsweise, dass der Parameter hdc der Funktion GetDeviceCaps als Integer deklariert ist. Beim folgenden Beispielprogramm wird sich herausstellen, dass dieser Parameter in Wirklichkeit als IntPtr deklariert werden muss. Derartige Fehler gibt es leider zuhauf. (Die Fehler werden nicht durch das Programm API-Viewer verursacht, sondern durch die zugrunde liegende Datei win32api.txt.) Ein weiteres Problem besteht darin, dass viele moderne API-Funktionen (die erst nach Windows 95/NT eingeführt wurden) in der Datei ganz fehlen. Trotz dieser Mängel ist das Programm in vielen Fällen eine wertvolle Hilfe.
VERWEIS
12.7.3 Beispiele Ein weiteres API-Beispiel finden Sie in Abschnitt 10.3.3. Dort wird die Funktion SHFileOperation eingesetzt, um eine Datei in den Papierkorb zu löschen.
Bildschirmauflösung ermitteln Das Windows-Programm api-resolution verwendet die Funktion GetDeviceCaps aus der Grafikbibliothek GDI32, um die aktuelle Bildschirmauflösung in Pixeln zu ermitteln. (Diese Information könnte natürlich viel einfacher mit den Klassen Windows.Forms.Screen oder Windows.Forms.SystemInformation ermittelt werden, siehe Abschnitt 15.2.8.) An diese Funktion muss im ersten Parameter ein so genannter Device Context übergeben werden. Dabei handelt es sich um einen Zeiger auf ein Objekt, das das Fenster beschreibt. Im zweiten Parameter muss eine Nummer übergeben werden, die angibt, welche Information ermittelt werden soll. In der Online-Dokumentation ist die Syntax dieser Funktion so dargestellt: int GetDeviceCaps(HDC hdc, int nIndex)
Mit Declare wird diese Funktion nun für den Aufruf unter VB.NET deklariert. Dabei wird für den Parameter hdc der Datentyp IntPtr verwendet. Das ist notwendig, weil die etwas weiter unten beschriebene Methode GetHdc ein Objekt des Typs IntPtr liefert. Aus Sicht der API-Funktion gibt es keinen Unterschied zwischen Integer und IntPtr – beides sind einfach 32-Bit-Zahlen. Der VB.NET-Compiler unterscheidet aber sehr wohl zwischen den beiden Datentypen und vermeidet so eine irrtümliche Fehlanwendung von Adressen. (Generell müssen API-Parameter, die als handle bezeichnet werden, häufig mit IntPtr deklariert werden, wenn der zu übergebende Zeiger vorher mit .NET-Funktionen ermittelt wird.)
12.7 API-Funktionen verwenden (Declare)
555
' Beispiel spezial\api-resolution Private Declare Function GetDeviceCaps Lib "gdi32.dll" _ (ByVal hdc As IntPtr, ByVal nIndex As Integer) As Integer Private Const HORZRES = 8 'Horizontal width in pixels Private Const VERTRES = 10 'Vertical width in pixels
Beim Aufruf der Funktion besteht das größte Problem darin, den erforderlichen HDC-Parameter anzugeben. Dieser steht in Windows.Forms-Programmen nämlich (anders als in VB6) nicht mehr unmittelbar zur Verfügung. Stattdessen muss mit CreateGraphics ein Graphics-Objekt für das aktuelle Fenster erzeugt werden. Anschließend kann mit der Methode GetHdc der interne handle ermittelt werden. Dieser Zeiger muss später mit ReleaseHdc wieder freigegeben werden. Ebenso muss das Graphics-Objekt mit Dispose wieder freigegeben werden. Private Sub Form1_Load(...) Handles MyBase.Load Dim width, height As Integer Dim gr As Graphics = CreateGraphics() Dim hdc As IntPtr = gr.GetHdc() width = GetDeviceCaps(hdc, HORZRES) height = GetDeviceCaps(hdc, VERTRES) gr.ReleaseHdc(hdc) gr.Dispose() Label1.Text = "Bildschirmauflösung: " + _ width.ToString + "*" + height.ToString + " Pixel" End Sub
Abbildung 12.10: Die Bildschirmauflösung wird mit einer API-Funktion ermittelt
Größe einer komprimierten Datei ermitteln Windows NT/2000/XP bietet bei Verwendung des NTFS-Dateisystems die Möglichkeit, Dateien zu komprimieren. Derartige Dateien benötigen dann auf der Festplatte weniger Platz. Die Eigenschaft Length der Klasse IO.FileInfo gibt bei solchen Dateien immer die unkomprimierte Größe an. (Das ist durchaus sinnvoll, weil die Datei beim Lesen ja automatisch dekomprimiert wird.) Wenn Sie nun aber wissen möchten, wie viel Platz die komprimierte Datei auf der Festplatte beansprucht, müssen Sie die API-Funktion GetCompressedSize einsetzen. Das folgende Beispielprogramm demonstriert die Anwendung dieser Funktion (siehe Abbildung 12.11).
556
12 Spezialthemen
Beachten Sie, dass GetCompressedSize nur unter den genannten Betriebssystemen zur Verfügung steht!
Abbildung 12.11: Das Beispielprogramm zeigt die tatsächliche und die komprimierte Größe einer Bitmap-Datei
Die Funktion GetCompressedFileSize befindet sich in der Bibliothek kernel32. Sie erwartet als ersten Parameter den Dateinamen. Das Ergebnis wird in zwei Teilen zurückgegeben: Der Rückgabewert enthält die ersten 32 Bit der Dateigröße. (Das reicht für Dateien bis zu 4 GByte.) Die zweiten 32 Bit werden in die im zweiten Parameter übergebene Integer-Variable geschrieben. Beachten Sie, dass der zweite Parameter als ByRef deklariert werden muss. Es wird hier also ein Zeiger auf einen Integer-Wert übergeben. (Der API-Viewer liefert hier fälschlich ByVal.) ' Beispiel spezial\api-getcompressedsize Private Declare Auto Function GetCompressedFileSize Lib "kernel32" _ (ByVal lpFileName As String, ByRef lpFileSizeHigh As Integer) _ As Integer Private Sub Button1_Click(...) Handles Button1.Click Dim fname As String Dim fi As IO.FileInfo Dim sizelow, sizehigh As Integer Dim sizetotal As Long If OpenFileDialog1.ShowDialog = DialogResult.OK Then fname = OpenFileDialog1.FileName fi = New IO.FileInfo(fname) Label1.Text = "Datei: " + fname Label2.Text = "Größe: " + FormatNumber(fi.Length, 0) + " Byte" sizelow = GetCompressedFileSize(fname, sizehigh) sizetotal = sizelow + sizehigh * CLng(2 ^ 32) Label3.Text = "Komprimierte Größe: " + _ FormatNumber(sizetotal, 0) + " Byte" End If End Sub
Teil IV
Windows-Programmierung
13 Windows.Forms – Einführung Windows.Forms ist der Teil der .NET-Bibliothek, der für die Anzeige und Verwaltung von Fenstern (Dialogen, Formularen) und den darin eingebetteten Steuerelementen zuständig ist. Jedes VB.NET-Programm, das mit einer Windows-Benutzeroberfläche ausgestattet werden soll, basiert auf Windows.Forms.
Dieses Kapitel gibt eine erste Einführung in die Erstellung eigener Formulare sowie einen Überblick über die dabei zur Anwendung kommenden Bibliotheken, Klassen und Steuerelemente. Außerdem beschreibt das Kapitel die Anwendung der Entwicklungsumgebung zum Design von Formularen. Die beiden folgenden Kapitel geben dann einen Überblick über die wichtigsten zur Verfügung stehenden Steuerelemente sowie eine Menge Details und Interna zur Gestaltung eigener Benutzeroberflächen (Verwaltung mehrerer Fenster, Menüs und Symbolleisten etc.) 13.1 13.2 13.3
Einführung Elementare Programmiertechniken Windows.Forms-Klassenhierarchie
560 569 573
560
13.1
13 Windows.Forms – Einführung
Einführung
Im Mittelpunkt dieses und der folgenden beiden Kapitel steht die Bibliothek Systems.Windows.Forms. Diese Bibliothek stellt im gleichnamigen Namensraum unzählige Klassen zur Verwaltung von Fenstern und Steuerelementen zur Verfügung. (Die Bibliothek enthält noch einige weitere Namensräume, die aber von untergeordneter Bedeutung sind.) Wenn Sie ein neues Projekt des Typs WINDOWS-ANWENDUNG entwickeln, wird automatisch ein Verweis auf diese Bibliothek eingerichet. Außerdem gilt System.Windows.Forms als Default-Import, weswegen Klassen aus diesem Namensraum im Code verwendet werden können, ohne dass jedes Mal Windows.Forms vorangestellt werden muss.
13.1.1 Kleines Glossar Die drei Begriffe Fenster, Formulare und Dialoge sind – zumindest was die Interna angeht – gleichbedeutend. In jedem Fall ist das am Bildschirm sichtbare Fenster von der Klasse Form abgeleitet – ganz egal, ob es nun als Dialog, als Eingabeformular oder für einen beliebigen anderen Zweck verwendet wird. Eine Sonderstellung nimmt manchmal der Begriff Dialog ein: Ein Fenster wird üblicherweise dann als Dialog bezeichnet, wenn es den Rest des Programms blockiert. (Das heißt, dass die Arbeit im Hauptprogramm erst dann wieder fortgesetzt werden kann, wenn die Eingabe im Dialog beendet ist.) Aber auch hierzu gibt es Ausnahmen: Beispielsweise darf der Dialog zum SUCHEN UND ERSETZEN in den meisten Programmen geöffnet bleiben, während Sie im Hauptprogramm weiterarbeiten. (Das gilt z.B. für Microsoft Word oder für die VB.NET-Entwicklungsumgebung.) Exakter ist in solchen Fällen die Bezeichnung modaler oder nichtmodaler Dialog. Modal bedeutet, dass der Dialog das Hauptprogramm blockiert. Nichtmodal bedeutet, dass der Dialog als gleichberechtigtes zweites Fenster agiert. Steuerelemente sind die Bedienelemente in einem Fenster. Zu Steuerelementen zählen z.B. Buttons, Kontrollkästchen, Optionsfelder, Textfelder, Schiebebalken etc. Während des Programmentwurfs steht ein komfortabler Editor (Designer) zur Verfügung, um Steuerelemente auf einem Fenster zu platzieren. (Sie können neue Steuerelemente aber auch dynamisch im laufenden Programm durch ein paar Zeilen zusätzlichen Code erzeugen, wenn Sie das möchten.) Was Eigenschaften und Methoden sind, wissen Sie ja schon. Mit Eigenschaften können Sie Merkmale eines Objekts lesen und zum Teil verändern. Mit Methoden können Sie das Objekt bearbeiten. Da sowohl ein Fenster als auch die darin enthaltenen Steuerelemente intern (natürlich) Objekte sind, können auch sie durch zahllose Eigenschaften und Methoden gesteuert werden. Die einzige Neuerung im Zusammenhang mit Steuerelementen und Formularen besteht darin, dass Sie in vielen Fällen ein sofortiges visuelles Feedback bekommen: Wenn Sie also die Eigenschaft BackColor eines Fensters verändern, ändert sich unmittelbar auch die Farbe des Fensterhintergrunds. Neu ist auch, dass eine Menge (aber nicht alle) Eigenschaften während der Programmentwicklung sehr komfortabel über das Fenster EIGENSCHAFTEN voreingestellt werden können.
13.1 Einführung
561
VERWEIS
Ereignisse sollten Sie ebenfalls schon kennen (siehe Kapitel 7), aber möglicherweise haben Sie noch wenig praktische Erfahrungen damit gemacht. Das wird sich jetzt rasch ändern, denn der gesamte Programmfluss eines Windows-Programms ist durch Ereignisse gesteuert. Wenn beispielsweise ein Anwender Ihres Programms einen Button anklickt, wird für diesen Button ein Click-Ereignis ausgelöst. In einer Ereignisprozedur können Sie darauf reagieren. (Die Vorgehensweise wird im folgenden Beispiel demonstriert.) In diesem Kapitel werden Steuerelemente einfach eingesetzt, ohne detailliertes Hintergrundwissen über deren Natur, Eigenschaften, Methoden etc. zu vermitteln. Diese Informationen werden aber im folgenden Kapitel nachgereicht.
13.1.2 Einführungsbeispiel Ziel des Einführungsbeispiels ist es, einen minimalistischen Texteditor zu programmieren. Das Programm besteht aus einem Fenster, das ein Textfeld und zwei Buttons enthält. Mit den beiden Buttons kann der Text aus einer ANSI-Datei geladen bzw. wieder gespeichert werden.
Steuerelemente in das Formular einfügen Der Programmentwurf beginnt damit, dass Sie ein neues VB.NET-Projekt des Typs WINDOWS-ANWENDUNG starten. Nun sollten Sie die Entwicklungsumgebung so einstellen, dass sowohl das Formular im Design-Modus als auch die Fenster bzw. Registrierkarten TOOLBOX und EIGENSCHAFTEN sichtbar sind (siehe auch Abbildung 13.1). Zum Einfügen der Steuerelemente markieren Sie das gewünschte Steuerelement zuerst in der Toolbox. Anschließend zeichnen Sie mit der Maus einen Rahmen an der Position im Fenster, an der das Steuerelement eingefügt werden soll. Das Steuerelement erscheint dort, sobald Sie die Maustaste loslassen. (Eine alternative Vorgehensweise besteht darin, das Steuerelement durch einen Doppelklick in der Toolbox einzufügen. Die Entwicklungsumgebung entscheidet dann selbst über Ort und Größe des Steuerelements. Anschließend verschieben Sie das Steuerelement an die gewünschte Stelle.) Für das hier vorgestellte Beispielprogramm benötigen Sie fünf Steuerelemente: zwei Buttons, ein Textfeld (TextBox) sowie je ein OpenFileDialog- und SaveFileDialog-Steuerelement. Die beiden letzten Steuerelemente stellen insofern einen Spezialfall dar, als sie nicht direkt im Formular angezeigt werden, sondern in einem eigenen Bereich unterhalb des Formulars. (Dieser Bereich ist für unsichtbare Steuerelemente vorgesehen.)
Einstellung der Eigenschaften Der nächste Schritt besteht darin, die Eigenschaften der Steuerelemente sowie des Formulars im Fenster EIGENSCHAFTEN einzustellen. Die im EIGENSCHAFTEN-Fenster angezeigten Einstellungen gelten für das im Formulardesign gerade aktive (angeklickte) Steuerelement.
562
13 Windows.Forms – Einführung
Sie können dieses Steuerelement auch mit dem Listenfeld des EIGENSCHAFTEN-Fensters auswählen. Die Eigenschaften bestimmen das Aussehen und Verhalten der Steuerelemente. Welche Eigenschaften zur Auswahl stehen, hängt vom Steuerelement ab. (Eine Menge Eigenschaften werden im nächsten Kapitel beschrieben. Dort finden Sie auch einen Überblick über gemeinsame Eigenschaften, die fast alle Steuerelemente besitzen.) Für das Beispielprogramm wurden folgende Eigenschaften verändert: •
Für das Fenster (Form-Objekt): Text = "Einfacher Texteditor"
•
Für die beiden Buttons: Text="ANSI-Datei laden" bzw. Text="ANSI-Datei speichern"
•
Für das Textfeld: Text="" (damit das Steuerelement beim Programmstart leer ist) MultiLine=True (damit im Textfeld mehrere Zeilen angezeigt werden können) WordWrap=False (damit lange Zeilen nicht auf mehrere Zeilen verteilt werden) ScrollBars=Both (damit bei langen Texten Schiebebalken angezeigt werden) AcceptTabs=True (damit mit Tab ein Tabulatorzeichen eingegeben werden kann) Anchor=Top, Bottom, Left, Right (damit das Steuerelement sich automatisch an die Größe des Fensters anpasst)
Abbildung 13.1: Entwurf des Fensters des Texteditors in der Entwicklungsumgebung
13.1 Einführung
563
Die beiden letzten Eigenschaften bedürfen einer etwas ausführlicheren Erklärung: Tab dient normalerweise dazu, den Eingabefokus von einem Steuerelement zum nächsten zu bewegen. Das macht es aber unmöglich, in einem Textfeld ein Tabulatorzeichen einzugeben. AcceptTabs behebt diesen Mangel.
HINWEIS
Die Anchor-Eigenschaft gibt an, welche Kanten des Steuerelements mit den Kanten des Fensters verbunden sind. Normalerweise ist das nur für Top und Left der Fall. Das bedeutet, dass das Steuerelement bei einer Größenänderung Position und Größe behält. Wenn auch die rechte und die untere Kante verankert werden, dann bleibt der Abstand zwischen dem Steuerelement und dem rechten bzw. unteren Fensterrand konstant. Dazu wird das Steuerelement automatisch mit dem Fenster vergrößert bzw. verkeinert. Bevor Sie die Anchor-Eigenschaft einstellen, müssen Sie das Textfeld relativ zur Fenstergröße im Formular-Designer richtig positionieren. Normalerweise sollten Sie den Steuerelementen aussagekräftige Namen geben. Dazu ändern Sie die Name-Eigenschaft des jeweiligen Steuerelements. Beispielsweise wären für die beiden Buttons die Bezeichnungen ButtonOK und ButtonCancel aussagekräftiger (statt der Defaultbezeichnungen Button1 und Button2). Beachten Sie, dass Sie die Steuerelemente umbenennen müssen, bevor Sie mit der Codeeingabe beginnen! (Nachträgliche Änderungen werden im Code nicht ausgeführt, d.h., Sie müssen dann den Code an den geänderten Namen anpassen.)
Sie können das Programm jetzt bereits starten (siehe Abbildung 13.2) – es erfüllt aber vorerst noch keine sinnvolle Aufgabe. Wenn Sie die beiden Buttons anklicken, passiert nichts. Text können Sie bereits eingeben, aber noch nicht speichern.
Abbildung 13.2: Aussehen des Fensters bei der Ausführung des Programms
Tipps zur effizienten Bedienung des Eigenschaftsfensters •
Im Eigenschaftsfenster sind alle Eigenschaften, die nicht die Defaulteinstellung enthalten, fett hervorgehoben.
•
Die Eigenschaften können im Eigenschaftsfenster wahlweise alphabetisch oder nach Gruppen geordnet dargestellt werden. Zwischen diesen beiden Modi kann mit den Buttons im linken oberen Eck des Eigenschaftsfenster gewechselt werden. Die alphabetische Anordung erleichtert oft die Suche nach einer bestimmten Eigenschaft.
564
13 Windows.Forms – Einführung
•
Wenn Sie eine Einstellung zurücksetzen möchten, klicken Sie die Eigenschaft mit der rechten Maustaste an und führen das Kontextmenükommando RESET aus.
•
Falls Sie die Entwicklungsumgebung so konfiguriert haben, dass das Eigenschaftsfenster normalerweise nicht angezeigt wird (AUTOMATISCH IM HINTERGRUND), dann lohnt es sich während des Formularentwurfs, das Fenster mit der Pinnadel vorübergehend anzudocken.
•
Im Eigenschaftsfenster können Sie auch gemeinsame Eigenschaften mehrerer Steuerelemente verändern. Dazu müssen Sie die Steuerelemente gemeinsam markieren – entweder mit Strg + Mausklick, oder indem Sie mit der Maus einen Rahmen über alle zu markierenden Steuerelemente zeichnen.
•
Wenn Sie mehrere gleichartige Steuerelemente benötigen, ist es oft am einfachsten, zuerst ein Steuerelement einzufügen, dessen Eigenschaften einzustellen und das Steuerelement dann mit KOPIEREN/EINFÜGEN (Strg+C, Strg+V) zu vervielfältigen.
•
Wenn Sie im Eigenschaftsfenster eine Eigenschaft markieren, gelangen Sie mit F1 direkt zum dazugehörenden Hilfetext.
Bei der Ausrichtung mehrerer Steuerelemente (z.B. Einstellung eines gleichmäßigen Abstands) hilft die LAYOUT-Symbolleiste. Wenn Sie diese Symbolleiste wie ich aus Platzgründen per Default nicht anzeigen lassen, vergessen Sie nicht, dass es sie gibt und dass sie durchaus praktisch ist! Beachten Sie auch, dass Sie mehrere Steuerelemente gemeinsam markieren können. Anschließend betreffen Änderungen der Position oder Größe alle Steuerelemente.
Ereignisprozeduren Was dem Programm jetzt noch fehlt, ist der Programmcode zum Laden und Speichern einer Datei. Grundsätzlich wird der Code zu Formularen ereignisorientiert verarbeitet. Die Reaktion auf Benutzereingaben (Mausklicks, Tastatureingaben etc.) sind Ereignisse. Wenn es zu einem Ereignis eine dazugehörende Ereignisprozedur gibt, wird diese automatisch ausgeführt. Es gibt drei Methoden, um das Gerüst einer Ereignisprozedur in den Code einzufügen: •
Am bequemsten und effizientesten ist ein Doppelklick auf das jeweilige Steuerelement im Designmodus. Die Entwicklungsumgebung zeigt dann automatisch den Programmcode zum Formular an und füg dort das Gerüst der Prozedur zum Defaultereignis ein. Besonders gut funktioniert das bei den Buttons: Durch einen Doppelklick wird die Click-Ereignisprozedur eingefügt. (Im Regelfall ist das die einzige Ereignisprozedur eines Buttons, die Sie brauchen.)
•
Wenn Sie eine Prozedur zu einem anderen Ereignis als dem Defaultereignis schreiben möchten, wechseln Sie in das Codefenster zum Formular. (Falls dieses Fenster nicht schon offen ist, können Sie es am schnellsten über das Kontextmenü CODE ANZEIGEN des Designfensters öffnen.)
13.1 Einführung
565
Dort wählen Sie zuerst im linken Listenfeld das gewünschte Steuerelement, dann im rechten Listenfeld den Ereignisnamen aus (siehe Abbildung 13.3). Um eine Ereignisprozedur für das Fenster einzufügen, wählen Sie links den Listeneintrag BASISKLASSENEREIGNISSE aus. •
Natürlich können Sie den Code auch einfach über die Tastatur eingeben. Das ist aber mühsam, weil Sie sowohl die Parameterliste als auch den Ereignistyp (Handles XyEvent) exakt angeben müssen.
Abbildung 13.3: Codegerüst für eine Ereignisprozedur einfügen
Das Codegerüst sieht wie im folgenden Muster aus: Der Name der Prozedur ergibt sich aus dem Steuerelement- und dem Ereignisnamen. Im Anschluss an die Parameterliste gibt das Schlüsselwort Handles an, auf welches Ereignis die Prozedur reagiert. Die Parameterliste sieht immer sehr ähnlich aus: •
sender enthält einen Verweis auf das Steuerelement, für das das Ereignis verarbeitet wird. Im Regelfall ist eine Auswertung von sender nicht erforderlich. sender ist aber
dann praktisch, wenn mehrere Steuerelemente mit einer Ereignisprozedur verbunden werden (siehe Abschnitt 14.11.2). In diesem Fall kann sender mit CType in ein Steuerelementobjekt umgewandelt werden, z.B. so: Dim btn As Button If TypeOf sender Is Button Then btn = CType(sender, Button) ...
•
e enthält ereignisspezifische Daten. Der Objekttyp hängt vom Ereignis ab. Bei Ereignissen ohne ereignisspezifische Daten ist e ein Objekt der Klasse System.EventArgs. In diesem Fall kann e einfach ignoriert werden.
566
13 Windows.Forms – Einführung
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click ... End Sub
Aus Platzgründen und um eine höhere Übersichtlichkeit zu erzielen, werden die beiden Parameter von Ereignisprozeduren in diesem Buch meist nicht angegeben. Stattdessen wird das Listing in einer verkürzten Form entsprechend dem folgenden Muster dargestellt.
VORSICHT
HINWEIS
Private Sub Button1_Click(...) Handles Button1.Click ... End Sub
Für VB6-Kenner sieht die obige Deklaration redundant aus: Sowohl aus dem Prozedurnamen als auch aus dem Handles-Nachsatz scheint hervorzugehen, für welches Ereignis die Prozedur gedacht ist. Dabei ist aber Vorsicht angebracht: Anders als in VB6 ist in VB.NET der Prozedurname vollkommen gleichgültig. Sie können die Prozedur also von Button1_Click in Xyz umbennen – und sie funktioniert weiterhin unverändert. Entscheidend dafür, auf welches Ereignis die Prozedur reagiert, ist einzig das nach dem Schlüsselwort Handles angegebene Ereignis!
Wenn Sie ein Steuerelement aus einem Formular entfernen, bleiben die zum Steuerelement gehörenden Ereignisprozeduren erhalten. Allerdings wird der Nachsatz Handles steuerelementname.ereignisname entfernt, weil dieser Nachsatz syntaktisch falsch ist, solange es das Steuerelement gar nicht gibt. Wenn Sie nun das Steuerelement wieder einfügen (mit dem ursprünglichen Namen), wird der Handles-Nachsatz nicht mehr wiederhergestellt! Das bedeutet, dass die noch vorhandenen Ereignisprozeduren nicht mehr mit dem Steuerelement verbunden sind und daher wirkungslos bleiben. Abhilfe: Der Handles-Nachsatz muss manuell wieder hinzugefügt werden.
Datei laden und speichern Wenn es einmal gelungen ist, das Codegerüst für die Ereignisprozeduren zu füllen, muss nur noch der eigentliche Code eingefügt werden. Für das vorliegende Beispiel soll in den beiden Button-Ereignisprozeduren eine Datei geladen bzw. gespeichert werden. Zur Dateiauswahl werden vorgefertige Standarddialoge verwendet, die über die Steuerelemente Open- bzw. SaveFileDialog zur Verfügung steht. Die Anwendung dieser Dialoge sowie die Programmiertechniken zum Lesen bzw. Schreiben wurde bereits in Kapitel 10 beschrieben.
13.1 Einführung
567
Vom Standpunkt der Windows.Forms-Programmierung ist der Aufruf der Dialoge durch ShowDialog sowie die Auswertung des Rückgabewerts (Vergleich mit DialogResult.OK) interessant. Nach dem Laden der Datei wird einfach die Text-Eigenschaft des TextBox-Steuerelements geändert. Dadurch wird der gelesene Text im Textfeld angezeigt. Analog wird beim Speichern einfach der Inhalt des Textfelds ebenfalls aus der Text-Eigenschaft gelesen. Bemerkenswert ist noch die Variable textHasChanged, die auf Klassenebene deklariert wird. Diese Variable wird dazu verwendet, um aufzuzeichnen, ob sich der Text seit dem Laden bzw. Speichern der Datei geändert hat. Die Variable wird beim Programmende ausgewertet (siehe nächste Überschrift). Außer dem hier abgedruckten Code enthält die Formulardatei noch eine ganze Menge Zeilen, die automatisch von der Entwicklungsumgebung eingefügt wurden (Vom Windows Form Designer generierter Code). Dieser Code erfüllt zwei Aufgaben: •
Erstens speichert er alle Einstellungen, die während des Formularentwurfs vorgenommen wurden (Namen der eingefügten Steuerelemente, deren Position und Größe, ihre Eigenschaften etc.). Dieser Code wird bei jedem Wechsel zwischen der Code- und der Design-Ansicht ausgewertet bzw. aktualisiert.
•
Zweitens enthält der Code alle Anweisungen, damit die Steuerelemente beim Start des Programms sichtbar werden und beim Schließen des Formulars wieder aus dem Speicher freigegeben werden.
Nähere Informationen zu dem automatisch erzeugten Code finden Sie in Abschnitt 15.2.2. ' Beispiel windows.forms\intro Public Class Form1 Inherits System.Windows.Forms.Form [... Vom Windows Form Designer generierter Code ...] Dim textHasChanged As Boolean ' ANSI-Datei laden Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim enc As System.Text.Encoding = _ System.Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader(OpenFileDialog1.FileName, enc) TextBox1.Text = sr.ReadToEnd() sr.Close() textHasChanged = False End If End Sub
568
13 Windows.Forms – Einführung
' ANSI-Datei speichern unter ... Private Sub Button2_Click(...) Handles Button2.Click SaveFileDialog1.FileName = OpenFileDialog1.FileName If SaveFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim enc As System.Text.Encoding = _ System.Text.Encoding.GetEncoding(1252) Dim sw As New IO.StreamWriter(SaveFileDialog1.FileName, _ False, enc) sw.Write(TextBox1.Text) sw.Close() textHasChanged = False End If End Sub
Sicherheitsabfrage beim Programmende Mit den beiden obigen Ereignisprozeduren ist das Programm bereits funktionsfähig. Die zwei folgenden Ereignisprozeduren machen das Programm aber noch ein wenig benutzerfreundlicher. Die Ereignisprozedur TextBox1_TextChanged wird immer dann aufgerufen, wenn sich im Textfeld etwas ändert. Darin wird die Variable textHasChanged auf True gesetzt. Somit kann das Programm jederzeit feststellen, ob der aus einer Datei geladene Text geändert wurde. Die Ereignisprozedur Form1_Closing wird automatisch ausgeführt, bevor das Fenster geschlossen werden soll (insbesondere, nachdem ein Benutzer den X-Button zum Schließen des Fensters angeklickt hat). Falls der Text zu diesem Zeitpunkt nicht gespeicherte Änderungen enthält, wird mit MsgBox ein kleiner Dialog angezeigt: Soll der Text vor dem Programmende gespeichert werden? Je nachdem, für welche der drei Antworten JA, NEIN oder ABBRUCH sich der Anwender entscheidet, wird das Programmende durch e.Cancel = True widerrufen oder der Text noch gespeichert. Dazu wird einfach die Button2_Click-Prozedur aufgerufen, wobei als Parameter zweimal Nothing übergeben wird. Private Sub TextBox1_TextChanged(...) Handles TextBox1.TextChanged textHasChanged = True End Sub Private Sub Form1_Closing(...) Handles MyBase.Closing ' Sicherheitsabfrage Dim result As MsgBoxResult If textHasChanged Then result = MsgBox("Soll der Text vor dem Programmende " + _ "gespeichert werden?", MsgBoxStyle.YesNoCancel)
13.2 Elementare Programmiertechniken
569
If result = MsgBoxResult.Cancel Then ' doch kein Programmende e.Cancel = True ElseIf result = MsgBoxResult.Yes Then Button2_Click(Nothing, Nothing) ' falls das scheitert: ebenfalls kein Programmende If textHasChanged Then e.Cancel = True End If End If End Sub End Class
13.2
Elementare Programmiertechniken
Fenster/Formulare/Dialoge anzeigen Wenn Sie ein Programm entwickeln, das aus einem einzigen Formular bzw. Fenster besteht, ist es keine große Kunst, das Fenster anzuzeigen: Es erscheint beim Programmstart automatisch und verschwindet zum Programmende ebenso automatisch. (Genau genommen ist es umgekehrt: Wenn das Fenster geschlossen wird, endet automatisch auch das laufende Programm.) Was bei einem einzigen Fenster ganz einfach ist, wird bei mehreren Fenstern komplizierter: •
Wann soll das Programm enden? Wenn das Hauptfenster geschlossen wird? Oder wenn das letzte offene Fenster geschlossen wird?
•
Soll das Hauptfenster bedienbar bleiben, während ein Dialogfenster sichtbar ist?
Bei Programmen, die aus einem Hauptfenster und mehreren Dialogen bestehen, sieht die gängigste Vorgehensweise folgendermaßen aus: In einer Ereignisprozedur des Hauptfensters – z.B. als Reaktion auf einen Mausklick oder eine Menüauswahl – wird der Dialog auf die folgende Weise dargestellt. ShowDialog bewirkt, dass das Hauptprogramm blockiert ist, bis der Dialog beendet wurde. (Der Dialog wird in diesem Fall als modal bezeichnet.) Falls der Dialog einen Rückgabewert liefert, liefert ShowDialog diesen zurück. Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New FormXyz() frm.ShowDialog() oder result = frm.ShowDialog() ... Eingabe auswerten frm.Dispose() End Sub FormXyz ist in diesem Fall der Name des Dialogformulars (per Default also Form1, Form2 etc.). Intern gilt jedes Formular als eine eigene, selbst definierte Klasse. Durch New erzeugen Sie ein neues Objekt dieser Klasse, und durch ShowDialog machen Sie das Objekt
570
13 Windows.Forms – Einführung
VERWEIS
(das Formular) sichtbar. Dispose entfernt das Objekt anschließend wieder aus dem Speicher. Statt ShowDialog können Sie auch die Methode Show verwenden. Das zweite Fenster ist dann nicht modal, sondern kann (fast) gleichberechtigt zum ersten Fenster verwendet werden. Die Methode Show sowie eine Menge anderer Mechanismen zur Anzeige, Verwaltung und zum Datenaustausch zwischen mehreren Fenstern werden in Abschnitt 15.3 detailliert vorgestellt.
Fenster bzw. Programm beenden Wenn ein VB.NET-Programm aus nur einem Fenster besteht, wird das Programm automatisch beendet, wenn das Fenster vom Anwender geschlossen wird. Um das Fenster per Programmcode zu schließen (z.B. als Reaktion auf das Anklicken des ENDE-Buttons), führen Sie Me.Close aus. In der Folge werden die Ereignisse Closing und Closed ausgelöst (siehe unten). Anschließend wird das Fenster mit all seinen Steuerelementen aus dem Speicher entfernt (d.h., es wird automatisch Dispose ausgeführt). Um ein Fenster nur vorübergehend unsichtbar zu machen, führen Sie Me.Hide aus. Das Form-Objekt kann dann später mit frmobject.Show wieder angezeigt werden. Dieses Szenario ist nur bei Programmen mit mehreren Fenstern oder Modulen sinnvoll und wird ebenfalls in Abschnitt 15.3 behandelt.
Standarddialoge Manchmal ist es gar nicht notwendig, einen eigenen Dialog zu erstellen. Für immer wieder vorkommende Aufgaben – etwa zur Auswahl einer Datei oder einer Schriftart – gibt es so genannte Standarddialoge. Der wohl am häufigsten eingesetzte Standarddialog ist die Nachrichtenbox, die mit MsgBox (nur VB.NET) oder mit MsgBox.Show (alle .NET-Programmiersprachen) dargestellt werden kann. Standarddialoge werden in Abschnitt 15.5 ausführlicher vorgestellt.
Grafik in Formularen und Steuerelementen In Formularen können nicht nur Steuerelemente dargestellt werden, sondern auch Grafikausgaben durchgeführt werden. Dazu verwenden Sie eine Paint-Ereignisprozedur, die automatisch immer dann aufgerufen wird, wenn ein Teil des Fensters neu gezeichnet werden muss. (Sie müssen Ihre Grafikausgaben also immer wieder wiederholen, nachdem Teile des Fensters verdeckt waren. Das Programm merkt sich Ihre Grafikkommandos nicht.) Die folgenden Zeilen zeigen, wie Sie eine rote Linie vom linken oberen Eck zum Punkt (100,100) zeichnen. Die Einheit des Koordinatensystems ist per Default Pixel. Bemerkenswert an der kurzen Prozedur ist der Umstand, dass hier zum ersten Mal ein Parameter einer Ereignisprozedur verwendet wird: Der Parameter e ist bei Paint-Ereignissen ein Ob-
13.2 Elementare Programmiertechniken
571
jekt der Klasse PaintEventArgs. Dessen Eigenschaft Graphics verweist auf ein Objekt der Graphics-Klasse, mit dessen Hilfe sämtliche Grafikausgaben durchgeführt werden. Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint e.Graphics.DrawLine(Pens.Red, 0, 0, 100, 100) End Sub
Grafikausgaben können nicht nur direkt im Fenster, sondern auch in vielen Steuerelementen durchgeführt werden. Die vielen Details der Grafikprogrammierung werden im umfangreichen Kapitel 16 beschrieben.
Initialisierungsarbeiten (Load-Ereignis) Oftmals möchten Sie vor dem Erscheinen eines Fensters Variablen oder Steuerelemente initialisieren. Der geeignet Ort ist die Load-Ereignisprozedur des Formulars. Diese Prozedur wird unmittelbar vor dem Erscheinen des Formulars am Bildschirm ausgeführt. (Load ist das Defaultereignis von Formularen. Daher können Sie das Codegerüst für die Prozedur einfach durch einen Doppelklick auf das Formular im Design-Modus einfügen.)
Aufräumarbeiten (Closed-Ereignis)
HINWEIS
Falls Sie nach dem Schließen des Fensters Aufräumarbeiten durchführen möchten, ist die Closed-Ereignisprozedur der richtige Ort. (Die im Fenster enthaltenen Steuerelemente werden übrigens automatisch durch Dispose freigegeben. Darum kümmert sich der von der Entwicklungsumgebung eingefügte Code – siehe auch Abschnitt 15.2). Vor dem Close-Ereignis tritt das im Beispielprogramm bereits angewendete ClosingEreignis ein. Es drückt die Absicht des Anwenders aus, das Fenster zu schließen. Zu diesem Zeitpunkt kann das noch verhindert werden (e.Cancel=True).
Programmzugriff auf Fenstereigenschaften, Steuerelemente etc. Innerhalb des Codes zu einem Formular können Sie auf die Eigenschaften und Methoden des Formulars unmittelbar zugreifen. Darüber hinaus können Sie auf alle im Formular enthaltenen Steuerelemente direkt zugreifen. Left=10 verändert daher die Eigenschaft Left des Formulars. Button1.Left=10 verändert die Eigenschaft Left des Steuerelements Button1. Wenn Sie möchten, können Sie beim Zugriff auf Eigenschaften, Methoden und Steuerelemente das Schlüsselwort Me voranstellen (also Me.Left=10 bzw. Me.Button1.Left=10). Die Bedeutung des Codes ändert sich dadurch nicht, manche Programmierer empfinden den Code dann aber als klarer und besser lesbar. In den Beispielen dieses Buchs wird Me meistens verwendet.
572
13 Windows.Forms – Einführung
Spezialeffekte für Fenster Dieser Abschnitt beschreibt ganz kurz, wie Sie einige manchmal benötigte Spezialeffekte erzielen können. Mehr Details zu den hier erwähnten Eigenschaften gibt Abschnitt 15.1. •
Startposition des Fensters: Per Default entscheidet das Betriebssystem darüber, wo das Fenster erscheint. Wenn Sie eine andere Position erzielen möchten, verändern Sie die Eigenschaft StartPosition. Bei der Einstellung Manual werden die Koordinatenangaben der Eigenschaft Location berücksichtigt. (Beachten Sie, dass die Taskleiste bei manchen Rechnern am linken Bildschirmrand platziert ist. Daher ist eine x-Koordinate von 0 ungünstig!) Sie können die Startposition auch in Form_Load durch die Methode SetDesktopLocation einstellen.
•
Startgröße des Fensters: Per Default ist das Fenster beim Programmstart so groß wie im Design-Modus in der Entwicklungsumgebung. Der einfachste Weg, die Fenstergröße zu ändern, besteht demnach darin, die Größe im Design-Modus zu ändern. Wenn es aber aus irgendeinem Grund erforderlich ist, dass das Fenster im Design-Modus und im laufenden Programm unterschiedlich groß sein soll, können Sie die gewünschte Größe in Form_Load mit der Methode SetDesktopBounds angeben. (Gleichzeitig müssen Sie auch die Position angeben, die Sie aus der Eigenschaft mit DesktopLocation lesen können. Die folgende Zeile ändert die Außengröße des Fensters auf 500*100 Punkte, belässt die Position aber unverändert. Me.SetDesktopBounds(Me.DesktopLocation.X, Me.DesktopLocation.Y, _ 500, 100)
Falls Sie möchten, dass das Fenster beim Programmstart in maximaler Größe angezeigt wird, brauchen Sie sich nicht mit SetDesktopBounds zu plagen. Stattdessen stellen Sie einfach WindowState auf Maximized. •
Unveränderliche Fenstergröße: Wenn Sie möchten, dass die Fenstergröße nicht geändert werden darf, stellen Sie die Eigenschaft FormBorderStyle auf FixedSingle. (Auch die Einstellungen FixedDialog, Fixed3D und FixedToolWindow erzielen den gewünschten Effekt, verändern aber außerdem das Aussehen des Fensters.)
•
Fenster ohne Rahmen (Splash-Fenster): Wenn das Fenster ohne Rahmen (also insbesondere ohne Titelleiste) angezeigt werden soll, stellen Sie FormBorderStyle auf None. Beachten Sie, dass das Fenster nun weder verschoben noch geschlossen werden kann! Diese Einstellung ist im Regelfall nur für so genannte Splash-Fenster sinnvoll, die während des Programmstarts angezeigt werden.
•
Fenster immer ganz oben halten: Damit das Fenster immer über allen anderen Fenstern angezeigt wird, verwenden Sie die Einstellung TopMost=True.
•
Durchscheinende Fenster: Wenn Ihr Fenster durchscheinend aussehen soll, stellen Sie die Eigenschaft Opacity auf einen Wert kleiner 1, beispielsweise auf 0,5. (0 würde das Fenster vollkommen unsichtbar machen – das ist natürlich nicht sinnvoll.) Beachten Sie, dass dieser Effekt nur von Windows 2000/XP unterstützt wird.
13.3 Windows.Forms-Klassenhierarchie
573
•
Halb-durchsichtige Fenster: Einen ähnlichen Effekt können Sie erzielen, wenn Sie mit der Eigenschaft TransparencyKey eine im Fenster vorkommende Farbe als durchsichtig deklarieren: Dann erscheint das Fenster an allen Stellen, an denen diese Farbe vorkommt, vollkommen durchsichtig. (Vorsicht: An diesen Stelle kann die Maus nicht verwendet werden!) Für den oben vorgestellten Texteditor können Sie z.B. die Farbe Weiß einstellen: Dann können Sie durch den Text durchsehen! Auch dieser Effekt wird nur unter Windows 2000/XP unterstützt.
•
Nicht-rechteckige Fenster: Dieses Buch geht davon aus, dass Ihre Fenster rechteckig sind. Wenn Sie bereit sind, einigen Mehraufwand zu investieren, können Sie aber auch Fenster in einer beliebigen anderen Form erzeugen. Eine Anleitung im Internet finden Sie, wenn Sie auf den Microsoft-Developer-Seiten nach Shaped Windows Forms and Controls suchen: http://msdn.microsoft.com/library/en-us/dv_vstechart/html/ vbtchShapedWindowsFormsControlsInVisualStudioNET.asp
VERWEIS
Interna der Formularverwaltung Um die Funktionsweise von Formularen, aber auch des Formular-Designers richtig verstehen zu können, sind noch einige Detailinformationen darüber erforderlich, wie Eigenschaftseinstellungen im Code gespeichert werden, wie Steuerelemente in das Form-Objekt eingefügt werden, wo die Codeausführung beginnt und wo sie endet etc. Diese Hintergründe werden in Abschnitt 15.2 ausführlich beschrieben. Dort finden Sie unter anderem auch eine Erklärung, welche Funktion der von der Entwicklungsumgebung eingefügte Code hat (Vom Windows Form Designer generierter Code).
13.3
Windows.Forms-Klassenhierarchie
Formulare und viele Steuerelemente besitzen jeweils gemeinsame Ereignisse, Methoden und Eigenschaften. Warum das so ist, verstehen Sie sofort, wenn Sie einen Blick auf den folgenden Kasten mit der Hierarchie der wichtigsten Windows.Forms-Klassen werfen. (Beachten Sie insbesondere die Position der Klasse Form innerhalb der Hierarchie! Diese Klasse ist für die Darstellung von Fenstern verantwortlich. Beachten Sie auch, dass der Windows.Forms-Namensraum Hunderte von Klassen enthält und dieser Überblick daher alles andere als vollständig ist!)
574
13 Windows.Forms – Einführung
Klassenhierarchie im System.Windows.Forms-Namensraum (Teil 1) Object └─ MarshalByRefObject └─ Component
│ ├─ CommonDialog │ └─ Control └─ ...
.NET-Basisklasse Objekt als Referenz an andere Rechner weitergeben Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Standarddialoge (z.B. ColorDialog, FileDialog, FontDialog etc.) Basisklasse für fast alle Steuerelemente
Klassenhierarchie im System.Windows.Forms-Namensraum (Teil 2) ...
└─ Control │ ├─ ButtonBase │ ├─ ScrollableControl │ │ │ └─ ContainerControl │ │ │ ├─ Form │ └─ UserControl │ └─ TextBoxBase
Basisklasse für alle Steuerelemente; direkt abgeleitet sind z.B. Label, PictureBox, ScrollBar, GroupBox Basisklasse für Button-Steuerelemente (z.B. Button, CheckBox, RadioButton) Basisklasse für Steuerelemente, die automatisches Scrolling des Inhalts erlauben (z.B. Panel) Basisklasse zur Verwaltung des Eingabefokus für die enthaltenen Steuerelemente Klasse für Formlare, Fenster und Dialoge; davon Basisklasse für eigene Steuerelemente; abgeleitet ist z.B. PrintPreviewDialog Basisklasse für Steuerelemente zur Texteingabe (z.B. TextBox, RichTextBox)
14 Steuerelemente Dieses Kapitel gibt zuerst einen Überblick über die mit VB.NET mitgelieferten Steuerelemente. Nach einer Beschreibung einiger gemeinsamer Merkmale werden die einzelnen Steuerelemente im Detail vorgestellt. Dabei habe ich besonderen Wert auf die Präsentation von Programmiertechniken gelegt, die bei der Durchführung häufig benötigter Operationen helfen (z.B. die Validierung von Texteingaben und die Verwaltung und Sortierung von Listenelementen) Das Kapitel endet mit einigen Spezialthemen. Hierbei geht es z.B. um die effiziente Verwaltung gleichartiger Steuerelemente (das, was in VB6 Steuerelementfelder waren), um das dynamische Einfügen neuer Steuerelemente per Code und um die Entwicklung eigener Steuerelemente. 14.1 14.2 14.3 14.4 14.5 14.6 14.7 14.8 14.9 14.10 14.11 14.12
Einführung Gemeinsame Eigenschaften, Methoden und Ereignisse Buttons Textfelder Grafikfelder Listenfelder Datums- und Zeiteingabe Schiebe- und Zustandsbalken, Drehfelder Gruppierung von Steuerelementen Spezielle Steuerelemente Programmiertechniken Neue Steuerelemente programmieren
576 581 596 600 610 612 662 665 669 674 690 696
576
14.1
14 Steuerelemente
Einführung
14.1.1 Steuerelemente – Überblick In VB6 gab es eine Unterscheidung zwischen Standard- und Zusatzsteuerelementen. Außerdem gab es die besonders ressourcenschonenden Windowless-Steuerelemente. In VB.NET gibt es diese Unterscheidung nicht mehr. Alle Steuerelemente basieren auf derselben Technologie.
Windows.Forms-Steuerelemente Die folgenden Steuerelemente sind Teil des Systems.Windows.Forms-Namensraums und stehen in der Registrierkarte WINDOWS FORMS der Toolbox zur Auswahl. Sie stellen also gewissermaßen die Standardsteuerelemente von .NET dar. Elementare Windows.Forms-Steuerelemente Buttons
Button, CheckBox, RadioButton
Textfelder
Label, LinkLabel, TextBox, RichTextBox
Grafik
PictureBox (in vielen anderen Steuerelementen können aber
ebenso eine Bitmap dargestellt bzw. Grafikmethoden ausgeführt werden) Bitmap-Container
ImageList
Listenfelder
ListBox, CheckedListBox, ComboBox, ListView
Hierarchische Listen
TreeView
Tabellenfeld
DataGrid
Eigenschaftsfeld
PropertyGrid (zeigt die Eigenschaften eines beliebigen Objekts an;
das Steuerelement entspricht dem Eigenschaftsfenster der Entwicklungsumgebung und wird in diesem Buch nicht beschrieben) Zeit, Datum
DateTimePicker, MonthCalender
Gruppierung
GroupBox, Panel, TabControl (Dialogblätter)
Bildlaufleisten
HScrollBar, VScrollBar, Trackbar
Drehfeld
DomainUpDown, NumericUpDown
Zustandsanzeige
Progressbar
Fensterteiler
Splitter
Zeitgeber
Timer
Infotext anzeigen
ToolTip
Fehlerindikator
ErrorProvider
Hilfefenster anzeigen
HelpProvider
Programmindikator
NotifyIcon
14.1 Einführung
577
Windows.Forms-Steuerelemente zur Gestaltung einer Benutzeroberfläche (Kapitel 15) Menüs
MainMenu, Menu, MenuItem, ContextMenu
Symbolleiste
ToolBar
Statusleiste
StatusBar
Standarddialoge
OpenFileDialog, SaveFileDialog, FontDialog, ColorDialog
Windows.Forms-Steuerelemente zum Ausdruck eines Dokuments (Kapitel 17) PrintDocument (Namensraum System.Drawing.Printing)
Drucker einstellen
PrintDialog
Seite einstellen
PageSetupDialog
Seitenvorschau
PrintPreviewDialog, PrintPreviewControl
Datenbankberichte
CrystalReportViewer
HINWEIS
Ausdruck steuern
Die mit VS.NET nur aus Kompatibilitätsgründen mitgelieferten ActiveX-Steuerelemente Microsoft Chart Control und Microsoft Masked Edit Control sind gegenüber VB6 unverändert. Ihre Anwendung in neuen Projekten wird nicht mehr empfohlen. Die Steuerelemente werden deswegen in diesem Buch nicht beschrieben. Eine Referenz der Eigenschaften, Methoden etc. finden Sie in der Hilfe, wenn Sie nach Referenz älteren ActiveX suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcmn/html/vborilegacyactivexcntrlref.htm
Abbildung 14.1: Die Windows.Forms-Steuerelemente in der Toolbox
Datenbanksteuerelemente Viele der oben aufgezählten Steuerelemente können auch als so genannte gebundene Steuerelemente verwendet werden, um die Daten einer ADO.NET-Datenbankabfrage darzustellen. Da die Datenbankfunktionen bereits in den Standardsteuerelementen integriert
578
14 Steuerelemente
sind, gibt es in VB.NET keine eigenen Datenbanksteuerelemente mehr. Die Datenbankanwendung der Steuerelemente wird in diesem Buch allerdings aus Platzgründen nicht beschrieben. Stattdessen finden Sie entsprechende Informationen in einem zweiten VB.NET-Band speziell zum Thema Datenbanken und Internet (siehe http://www.kofler.cc).
14.1.2 Microsoft.VisualBasic.Compatibility.VB6Steuerelemente
VORSICHT
Der in der Überschrift genannte Namensraum (Bibliothek Microsoft.VisualBasic.Compatibility.dll) enthält eine Reihe von Steuerelementen, die in VB.NET nicht oder nicht mehr in dieser Form zur Verfügung stehen. Der Namensraum enthält unter anderm die Steuerelemente Drive-ListBox, DirListBox und FileListBox sowie eine Reihe von XxxArray-Steuerelementen, mit denen die nicht mehr unterstützten Steuerelementfelder für die wichtigsten Standardsteuerelemente nachgebildet werden können. Der Namensraum wird vom Migrationsassistenten verwendet, um ansonsten nicht portierbaren VB6-Code in VB.NET umzuwandeln. Grundsätzlich ist es aber natürlich möglich, die in Microsoft.VisualBasic.Compatibility.VB6 enthaltenen Steuerelemente auch in neuen VB.NET-Projekten zu verwenden. Die Dokumentation rät davon aber ausdrücklich ab, da der Namensraum von zukünftigen VB.NET-Versionen möglicherweise nicht mehr unterstützt wird.
14.1.3 ActiveX-Steuerelemente (alias COM-Steuerelemente) Herkömmliche Zusatzsteuerelemente (in der Nomenklatur von VB6), die auch als ActiveXoder als COM-Steuerelemente bezeichnet werden, können in .NET weiterhin verwendet werden.
TIPP
Sobald ein COM-Steuerelement in ein Formular eingefügt wird, erzeugt die Entwicklungsumgebung zwei DLL-Bibliotheken (so genannte interop-Bibliotheken) im bin-Verzeichnis des Projekts, die für die Kommunikation zwischen dem COM-Steuerelement und .NET verantwortlich sind. Das COM-Steuerelement wird dabei von der Windows.Forms-Klasse Control abgeleitet und kann wie ein normales .NET-Steuerelement verwendet werden: seine Eigenschaften können im Eigenschaftsfenster verändert werden, die Ereignisse stehen im Codefenster zur Auswahl etc. Bemerkenswert ist, dass die Steuerelemente sogar mit einigen .NET-Eigenschaften ausgestattet sind (z.B. Anchor und Dock). Um den ActiveX-spezifischen Eigenschaftsdialog des Steuerelements zu öffnen, klicken Sie im unteren Ende des Eigenschaftsfensters den Link ACTIVEX-EIGENSCHAFTEN an. Sie können auf diese Weise einige Eigenschaften des Steuerelements einstellen, die im gewöhnlichen Eigenschaftsfenster nicht verändert werden können.
HINWEIS
14.1 Einführung
579
Bei ActiveX-Steuerelementen gibt es Design- und Run-time-Lizenzen. Damit ein ActiveX-Steuerelement in der Entwicklungsumgebung verwendet werden kann, muss auf dem lokalen Rechner die Design-Lizenz installiert sein. Diese liegt vor, die VB6 am Rechner installiert wurde. Wenn das nicht der Fall ist, können Sie die Datei extras\vb6 controls\vb6controls.reg durch Doppelklick ausführen. Die Datei enthält die Design-Lizenzen für die VB6-Steuerelemente in Form von Einträgen für die Registrierdatenbank. (Sie finden die Datei vb6controls.reg auf der VS.NET-Installations-CD.)
Kompatibilität Ich habe die Verwendung von COM-Steuerelementen unter .NET zu wenig intensiv getestet, um seriöse Aussagen über die zu erwartenden Kompatibilitätsprobleme oder über Schwierigkeiten bei der Weitergabe derartiger Projekte machen zu können. Dass die Migration vollkommen problemlos verläuft, ist allerdings nicht anzunehmen: Beispielsweise habe ich die Verwendung des CheckBox-Steuerelements aus der MS-Forms2.0-Bibliothek ausprobiert. Dabei hat sich herausgestellt, dass ausgerechnet die absolut elementare Value-Eigenschaft fehlt. Die Suche im Objektbrowser hat mich dann auf die neue Methode get_Value gebracht. Diese Methode liefert allerdings ein Object als Ergebnis. Weitere Tests ergaben, dass mit CBool eine Umwandlung in True oder False möglich ist. Die folgende Ereignisprozedur verändert den Text des CheckBox-Steuerelements je nachdem, in welchem Zustand sich das Steuerelement gerade befindet. ' Beispiel steuerelemente\com-steuerelemente Private Sub AxCheckBox1_Change(...) Handles AxCheckBox1.Change If CBool(AxCheckBox1.get_Value()) = True Then AxCheckBox1.Caption = "CheckBox wurde ausgewählt" Else AxCheckBox1.Caption = "CheckBox ist nicht ausgewählt" End If End Sub
Einige wenige ActiveX-Steuerelementen sind definitiv inkompatibel zu .NET und können überhaupt nicht verwendet werden. Dazu zählen UpDown, ssTab und Coolbar. Für alle drei Steuerelemente gibt es aber .NET-Alternativen, so dass der Verlust verschmerzbar ist.
14.1.4 Tipps zur Verwendung der Toolbox In der Toolbox wird in mehreren so genannten Registrierkarten eine Defaultauswahl von Steuerelementen angezeigt. Für die Windows-Programmierung enthält die in Abbildung 14.1 dargestellte Registierkarte die wichtigsten Steuerelemente. Grundsätzlich gibt es zwei Darstellungsweisen innerhalb der Toolbox: Per Default werden die Steuerelemente als Liste dargestellt, d.h. jede Zeile enthält ein Icon und den Namen des Steuerelements. Wenn Sie sich an die Icons gewöhnt haben, können Sie das Fenster über
580
14 Steuerelemente
ein Kontextmenükommando wie in Abbildung 14.1 auf die platzsparende Iconansicht umstellen. Die Icons sind innerhalb der Toolbox geordnet. Die Ordnung sieht zwar auf ersten Blick willkürlich aus, aber im Regelfall sind zusammenpassende Steuerelemente in Gruppen angeordnet. Wenn Sie möchten, können Sie die Steuerelemente mit der Maus verschieben und so die Reihenfolge verändern. Per Kontextmenü können Sie die Steuerelemente auch alphabetisch sortieren.
Steuerelemente in die Toolbox einfügen Neben den in der Toolbox bereits enthaltenen Steuerelementen können Sie mit dem Kontextmenü TOOLBOX ANPASSEN weitere Elemente einfügen. Im Einfügedialog (siehe Abbildung 14.2) haben Sie die Wahl zwischen installierten COM- und .NET-Steuerelementen. Wenn Sie das gewünschte Steuerelement nicht finden (was z.B. bei selbst programmierten Steuerelementen die Regel ist), können Sie mit DURCHSUCHEN nach der DLL-Datei suchen, die das Steuerelement enthält.
Abbildung 14.2: Dialog zum Einfügen eines zusätzlichen Steuerelements in die Toolbox
Das Steuerelement wird in die gerade aktive Registrierkarte eingefügt. Sie können die Steuerelemente aber per Maus zwischen unterschiedlichen Registrierkarten verschieben bzw. überhaupt neue Registrierkarten einfügen, um Ordnung in die Steuerelementlisten zu bekommen. Die Veränderungen an der Toolbox gelten für die gesamte Entwicklungsumgebung (also nicht nur für das aktuelle Projekt).
HINWEIS
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
581
Wenn Sie ein Steuerelement von der Toolbox in ein Formular einfügen, fügt die Entwicklungsumgebung automatisch auch einen Verweis auf die Bibliothek hinzu, die das Steuerelement enthält. (Sie können das im Objektkatalog oder im Projektmappen-Explorer kontrollieren.) Wenn Sie das Steuerelement wieder aus dem Formular löschen, wird der Bibliotheksverweis dagegen nicht automatisch entfernt.
14.2
Gemeinsame Eigenschaften, Methoden und Ereignisse
Dieser Abschnitt beschreibt gemeinsame Eigenschaften, Methoden und Ereignisse, die bei vielen Steuerelementen zur Verfügung stehen. (Aber natürlich gilt: Nicht jede der in diesem Abschnitt beschriebenen Eigenschaften steht für jedes Steuerelement zur Verfügung!) Viele der hier erwähnten Schlüsselwörter können auch für das Form-Objekt verwendet, das das zugrunde liegende Formular beschreibt. Um hier eine ebenso langweilige wie unübersichtliche Aufzählung zu vermeiden, wurden die Schlüsselwörter in thematischen Gruppen zusammengefasst. Eine alphabetische Referenz der erwähnten Schlüsselwörter finden Sie in der Syntaxzusammenfassung am Ende des Abschnitts.
14.2.1 Aussehen Das Aussehen von Steuerelementen wird sehr stark durch das Aussehen des zugrunde liegenden Formulars beeinflusst! Wenn Sie beispielsweise die Hintergrundfarbe des Formulars ändern, dann nehmen automatisch auch alle Steuerelemente diese Hintergrundfarbe an, sofern nicht explizit eine andere Farbe eingestellt wurde. Dasselbe Verhalten gilt auch für eine Reihe weiterer Eigenschaften (z.B. für die Schriftart).
Text Mit der Eigenschaft Text geben Sie die Beschriftung des Steuerelements an. Einige Steuerelemente stellen zu lange Texte per Default in mehreren Zeilen dar. Bei vielen Steuerelementen kann die Positionierung und Ausrichtung des Texts (z.B. links oben in einem Button) durch TextAlign eingestellt werden. Die Schriftart und -größe wird durch die Eigenschaft Font eingestellt. Dabei sind nur TrueType- und OpenType-Schriftarten zulässig. Beachten Sie auch, dass Font-Objekte nicht verändert werden können. Wenn Sie im laufenden Programm beispielsweise die Schriftart von normal auf fett umstellen möchten, müssen Sie ein neues Font-Objekt erzeugen. Das folgende Beispiel demonstriert die richtige Vorgehensweise. (Beachten Sie auch, dass das alte Font-Objekt durch Dispose freigegeben werden sollte.)
582
14 Steuerelemente
VERWEIS
Private Sub Button1_Click(...) Handles Button1.Click Dim fnt As New Font(Button1.Font, FontStyle.Bold) Button1.Font.Dispose() Button1.Font = fnt End Sub
Eine Menge weiterer Informationen zum Umgang mit Font-Objekten finden Sie in Abschnitt 16.3.
VORSICHT
.NET verwendet intern zur Darstellung aller Zeichenketten den Unicode-Zeichensatz. Das gilt natürlich auch für Steuerelemente. Ob die Steuerelemente allerdings tatsächlich Unicode-kompatibel sind, hängt auch von ihrer Darstellung durch das Betriebssystem ab! Wenn Ihr Programm unter Windows 98/ME ausgeführt wird (Windows 95 wird ja überhaupt nicht unterstützt), dann kann es laut Dokumentation bei den folgenden Steuerelementen Unicode-Darstellungsprobleme geben: TabControl, ListView, TreeView, DateTimePicker, MonthCalendar, TrackBar, ProgressBar, ImageList, ToolBar und StatusBar. Weitere Informationen finden Sie, wenn Sie in der Online-Hilfe nach Codierung Globalisierung Windows Forms suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconUnicodeCharacterDisplayInWFCProjects.htm
Bilder, Grafik In den meisten Steuerelementen können auch Bilder dargestellt werden, wobei es mehrere Varianten gibt: •
Im Normalfall wird die Bilddatei durch das Anklicken der Image-Eigenschaft im Eigenschaftsfenster geladen. (Als Bilddatei sind nicht nur Bitmaps, sondern auch Icons und WMF/EMF-Dateien zulässig!) Bilder können ebenso wie Texte durch ImageAlign innerhalb des Steuerelements positioniert werden.
•
Wenn es im Formular einen ImageList-Container gibt (siehe Abschnitt 14.5.2), dann können Sie auf eine der dort enthaltenen Bitmaps über die beiden Eigenschaften ImageList und ImageIndex verweisen. Abermals dient ImageAlign zur Positionierung des Bilds.
•
Schließlich können Sie mit BackGroundImage ein Hintergrundmuster angeben. Im Gegensatz zu den beiden ersten Varianten wird das Bild nun periodisch wiederholt, um so den gesamten Hintergrund des Steuerelements mit einem Muster zu füllen.
Manche Steuerelementen stellen schließlich das Paint-Ereignis zur Verfügung. Damit können Sie im Steuerelement beliebige Grafikausgaben machen. Das eignet sich insbesondere zur Gestaltung von ansonsten leeren Steuerelementen (z.B. für einen Button ohne Text). Hintergründe zur Paint-Ereignisprozedur und zur Grafikprogrammierung werden in Ka-
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
583
pitel 16 vermittelt. Ein Beispiel für eine Paint-Ereignisprozedur bei einem Button finden Sie in Abschnitt 14.3.1.
Farben Die Hintergrundfarbe von Steuerelementen kann mit BackColor, die Vordergrundfarbe (Textfarbe) mit ForeColor eingestellt werden. Beachten Sie aber, dass viele Anwender die Defaultfarben bevorzugen und dass zu viele Farben Ihr Programm unübersichtlich machen können.
HINWEIS
Wenn Sie die Farbe per Code verändern möchten, können Sie die Windows-Standardfarben der SystemColors-Aufzählung entnehmen (Namensraums System.Drawing). Darüber hinaus stehen Ihnen zahlreiche weiter Farben in der Form Color.White, Color.Black etc. zur Auswahl. Eigene Farben können Sie mit Color.FromArgb(rot, grün, blau) erzeugen. Details zum Umgang mit Farben finden Sie in Abschnitt 16.2.2. Eine Veränderung der Windows-Standardfarben können Sie durch das SystemColorsChanged-Ereignis feststellen. Das ist aber nur in wenigen Spezialanwendungen erforderlich. (Jedes Programm verwendet beim Start automatisch die gerade aktuellen Windows-Standardfarben. Eine dynamische Anpassung während der Programmausführung ist nur in seltenen Fällen notwendig.)
Umrandung Einige Steuerelemente können mit der Eigenschaft BorderStyle mit einem einfachen oder einem dreidimensionalen Rand ausgestattet werden.
Sichtbarkeit Ob ein Steuerelement sichtbar ist oder nicht, wird durch Visible gesteuert. Im Regelfall sind natürlich alle Steuerelemente sichtbar. In manchen Fällen kann es aber sinnvoll sein, einige Steuerelemente nur bei Bedarf anzuzeigen. Statt Visible direkt zu ändern, können Sie dazu auch die Methoden Hide und Show verwenden. Enable hat nur bedingt mit der Sichtbarkeit zu tun: Durch Enable=False deaktivieren Sie das Steuerelement. Es wird nun grau dargestellt, ist also gewissermaßen noch halb sichtbar. Es kann aber nicht mehr verwendet werden. Auch Enable ist per Default auf True gestellt. Es kann aber für die Anwender Ihres Programms hilfreich sein, wenn im Kontext gerade nicht einsetzbare Steuerelemente durch Enable=False deutlich markiert sind.
Manchmal besteht der Wunsch, Steuerelemente durchsichtig zu machen: Beispielsweise soll ein Beschriftungsfeld (Label) das Hintergrundmuster des Formulars nicht überdecken. Dazu stellen Sie einfach die Hintergrundfarbe BackColor auf Transparent. (Sie finden diese Farbe im Eigenschaftsfenster als erste Farbe der WEB-Gruppe.)
584
14 Steuerelemente
Änderungen in Steuerelementen (z.B. Label1.Text=...) werden im Regelfall erst am Ende der Ereignisprozedur sichtbar, in der die Änderung durchgeführt wird. Wenn in der Prozedur eine länger andauernde Berechnung ausgeführt wird, kann eine sofortige Aktualisierung durch Update erreicht werden. Noch umfassender ist die Wirkung von Refresh – die Methode bewirkt ein komplettes Neuzeichnen des Steuerelements (unabhängig davon, ob dies erforderlich ist oder nicht). Wenn nur Teile des Steuerelements neu gezeichnet werden sollen, können Sie dazu Invalidate verwenden.
Maus Das Aussehen der Maus wird durch die Eigenschaft Cursor bestimmt. Wenn diese Eigenschaft verändert wird, nimmt die Maus ein anderes Aussehen an, solange sie sich über dem Steuerelement befindet. Die Cursors-Klasse stellt einige Defaultformen für die Maus zur Auswahl.
3-D-Aussehen
VERWEIS
Steuerelemente werden üblicherweise mit einem leicht dreidimensionalen Aussehen dargestellt. Bei manchen Steuerelementen kann durch FlatStyle=Flat aber auch ein flaches, zweidimensionales Aussehen erreicht werden. Wenn Sie möchten, dass Steuerelemente unter Windows XP in der dort üblichen neuen Optik erscheinen, müssen Sie die Einstellung FlatStyle=System verwenden. Außerdem muss das Programm mit Version 6 der Bibliothek comctl32.dll verbunden werden, die zurzeit nur unter Windows XP zur Verfügung steht. Weitere Details zu diesem Vorgang folgen in Abschnitt 15.2.6.
VERWEIS
14.2.2 Größe, Position, Layout Positions- und Größenangaben erfolgen in der Regel durch Objekte der Strukturen Point, Size und Rectangle. Diese Strukturen werden in Abschnitt 16.2 im Rahmen der Grafikprogrammierung vorgestellt. Ihre Verwendung sollte aber auf Anhieb klar sein. Aus Eigenschaften wie X, Y, Width und Height können die Details der Position bzw. Größen entnommen werden.
Position: Die Position des linken oberen Ecks eines Steuerelements wird wahlweise durch Location (Point-Objekt) bzw. durch Left und Top bestimmt. Alle drei Eigenschaften sind veränderlich. Right und Bottom geben die Position des rechten unteren Ecks an. Diese beiden Eigen-
schaften können allerdings nur gelesen werden.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
585
Größe: Die Größe wird durch Size (Size-Objekt) oder durch Width und Height bestimmt. Position und Größe: Bounds verweist auf ein Rectangle-Objekt, das sowohl die Position als auch die Größe enthält. Innenmaße: Alle bisher angegebenen Eigenschaften bezogen sich auf die Außenmaße des Steuerelements. Bei manchen Steuerelementen gibt es davon abweichende Innenmaße, die um den Rand verkleinert sind und den Bereich des Steuerelements angeben, der tatsächlich genutzt werden kann (z.B. für Grafikausgaben oder als Container für andere Steuerelemente). Die Innenmaße können aus ClientSize (Size-Objekt) oder ClientRectangle (Rectangle-Objekt) entnommen werden. Bei ClientRectangle sind die Eigenschaften X und Y immer 0, weil das Innenkoordinatensystem innerhalb des Steuerelements bei (0,0) beginnt. Bei Steuerelementen, die nicht zwischen Innen- und Außenmaß unterscheiden, liefern Size und ClientSize einfach dieselben Ergebnisse. Größe und Position ändern: Zur Veränderung von Größe oder Position können Sie die meisten der oben angegebenen Eigenschaften ändern bzw. ein neues Objekt zuweisen. Beispielsweise versetzt die folgende Anweisung Button1 an die Position (10,10) und stellt gleichzeitig Breite und Höhe mit 100*50 Punkten ein. Button1.Bounds = New Rectangle(10, 10, 100, 50)
Die nächste Zeile setzt die Innengröße mit 100*50 Punkten fest. Button1.ClientSize = New Size(100, 50)
Alternativ können Sie die Außengröße auch mit SetBounds festlegen. Größe automatisch ändern: Manche Steuerelemente stellen die Eigenschaft AutoSize zur Verfügung. Wenn diese Eigenschaft auf True gesetzt wird, passt sich die Größe des Steuerelements automatisch an ihren Inhalt an. Das ist vor allem zur Darstellung von Bildern praktisch, kann aber in manchen Fällen auch für Text nützlich sein.
Steuerelemente verankern Mit der Anchor-Eigenschaft kann der Abstand zwischen dem Rand des Steuerelements und dem dazugehörenden Fensterrand fixiert werden. Das bedeutet, dass sich der Abstand bei einer Änderung der Fenstergröße nicht ändert. Je nachdem, an wie vielen Rändern das Steuerelement verankert ist, wird dazu die Position oder die Größe des Steuerelements verändert. Das Konzept ist anhand einiger Beispiele (und anhand von Abbildung 14.3) leicht zu verstehen: •
Per Default sind Steuerelemente links oben verankert (d.h. Anchor = AnchorStyles.Left Or AnchorStyles.Top oder salopper formuliert: Anchor=Left,Top). Bei einer Veränderung der Fenstergröße bleibt das Steuerelement unverändert.
•
Die Einstellung Anchor=Top,Right bewirkt, dass das Steuerelement bei einer horizontalen Vergrößerung des Fensters mit nach außen wandert.
586
14 Steuerelemente
•
Die Einstellung Anchor=Top,Left,Right bewirkt, dass das Steuerelement horizontal an beiden Seiten verankert ist. Bei einer Änderung der Fensterbreite ändert sich auch die Breite des Steuerelements.
•
Anchor=Top,Bottom,Left,Right bewirkt, dass das Steuerelement an allen Rändern veran-
kert ist. Es ändert sowohl seine Breite als auch seine Höhe synchron mit einer Änderung der Fenstergröße. •
Anchor=None bewirkt, dass das Steuerelement zentriert in der Mitte des Fensters angezeigt wird. Die Steuerelemente behalten dabei ihre ursprüngliche Größe. Wenn mehrere Steuerelemente so eingestellt sind, bleiben auch die relativen Abstände zwischen den Steuerelementen erhalten. (Dieses Verhalten für Anchor=None ist allerdings nicht dokumentiert.)
Abbildung 14.3: Die beiden Fenster demonstrieren die Funktion der Anchor-Eigenschaft
HINWEIS
Beachten Sie, dass die Größe einzelner Steuerelemente bei einer starken Verkleinerung des Fensters auf 0 schrumpfen kann bzw. dass sich einzelne Steuerelemente überlappen können (siehe Abbildung 14.4). Dabei kommt es zu keinen Fehlermeldungen, aber die Funktionstüchtigkeit des Programms ist beeinträchtigt. Dieses Problem können Sie umgehen, wenn Sie eine minimale Fenstergröße vorgeben (Eigenschaft MinimumSize des Fensters). Die Einstellung der Anchor-Eigenschaft gilt relativ zum Rand des Containers, in dem sich das Steuerelement befindet. Normalerweise dient das Formular als Container. Daneben gibt es aber auch eine Reihe von Steuerelementen (Panel, GroupBox, TabControl), die ebenfalls Steuerelemente aufnehmen können.
TIPP
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
587
Sie können die Funktion der Anchor-Eigenschaft bereits im Entwurfsmodus ausprobieren: Ändern Sie einfach die Fenstergröße. Die Steuerelemente sollten ihre Größe automatisch anpassen.
Abbildung 14.4: Anchor-Probleme bei einer zu starken Verkleinerung des Fensters
Steuerelemente andocken Mit der Dock-Eigenschaft können Sie ein Steuerelement an einen Fensterrand andocken (Dock = Left, Right, Bottom oder Top). Das Steuerelement ist damit ganz am Rand festgeklebt. Außerdem nimmt das Steuerelement automatisch die gesamte Fensterbreite bzw. -höhe an (je nachdem, wo es angedockt wird). Mit Dock=Fill erreichen Sie, dass das Steuerelement den gesamten zur Verfügung stehenden Raum einnimmt, der nicht bereits von anderen angedockten Steuerelementen beansprucht wird. Beim Formular können Sie mit der Eigenschaft DockPadding angeben, wie groß der Abstand zwischen dem Rand und den angedockten Steuerelementen sein soll. Per Default ist der Abstand 0. Grundsätzlich ist es möglich, mehrere Steuerelemente in einem Fenster anzudocken. In der Praxis funktioniert das allerdings nur dann zufriedenstellend, wenn alle Steuerelemente nur horizontal oder nur vertikal angedockt werden. Wenn Sie mehrere Steuerelemente auf einer Seite nebeneinander andocken möchten, bestimmt die Einfügereihenfolge die Position (d.h., welches Steuerelement ganz am Rand, welches etwas weiter eingerückt ist etc.). Nachträglich können Sie die Reihenfolge gedockter Steuerelemente durch die Kontextmenükommandos IN DEN HINTERGRUND oder IN DEN VORDERGRUND verändern. Wenn Sie Steuerelemente sowohl horizontal als auch vertikal andocken möchten, besteht die beste Strategie darin, zuerst die Steuerelemente für eine Ausrichtung (vertikal oder horizontal) anzudocken. In den verbleibenden Leerraum fügen Sie ein Panel-Steuerelement ein und stellen dessen Dock-Eigenschaft auf Fill. Nun fügen Sie alle weiteren Steuerelemente in das Panel ein. Für diese Steuerelemente bestimmt die Dock-Eigenschaft die Platzierung innerhalb des Panels. Auf diese Weise können Sie beinahe beliebig komplexe Layouts er-
588
14 Steuerelemente
reichen. (Das Panel-Steuerelement ist ein Container für andere Steuerelemente. Im laufenden Programm ist es unsichtbar.) Das in Abbildung 14.5 dargestellte Beispielprogramm zeigt ein etwas komplexeres Layout (das aber durchaus realistisch ist und das Sie bei vielen E-Mail-Clients bzw. vergleichbaren Programmen wiederfinden werden). Die Steuerelemente wurden in der folgenden Reihenfolge in das Fenster eingefügt: •
ButtonBar-Steuerelement, Dock=Top
•
StatusBar-Steuerelement, Dock=Bottom
•
Panel-Steuerelement, Dock=Fill
•
TreeView-Steuerelement in Panel1, Dock=Left
•
ein weiteres Panel-Steuerelement in Panel1, Dock=Fill
•
ListView-Steuerelement in Panel2, Dock=Top
•
TextBox-Steuerelement in Panel2, Dock=Fill
VERWEIS
Abbildung 14.5: Die beiden Fenster demonstrieren die Funktion der Dock-Eigenschaft
Bei vielen Anwendungen mit gedockten Steuerelementen hat der Anwender die Möglichkeit, die Breite bzw. Höhe der gedockten Steuerelemente durch eine bewegliche Linie zu verändern. In .NET-Programmen können Sie diesen Effekt mit dem Splitter-Steuerelement erzielen, das in Abschnitt 14.10.1 beschrieben wird.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
589
14.2.3 Eingabefokus, Validierung Eingabefokus Ein Steuerelement besitzt den so genannten Eingabefokus, wenn es Tastatureingaben entgegennehmen kann. Pro Formular kann immer nur ein Steuerelement den Eingabefokus besitzen. Der Eingabefokus kann mit den Cursortasten, mit Alt-Tastenkürzeln (siehe unten), mit Tab sowie per Mausklick verändert werden. Bei einem Fokuswechsel tritt zuerst für das Steuerelement, das den Fokus bisher hatte, ein
HINWEIS
Leave-Ereignis auf, anschließend für das Steuerelement, das den Fokus nun erhält, ein Enter-Ereignis. Ob ein Steuerelement gerade den Fokus besitzt, können Sie mit der Eigenschaft ContainsFocus feststellen.
Neben den Enter- und Leave-Ereignissen gibt es auch die Ereignisse GotFocus und LostFocus. Die Dokumentation empfiehlt aber, diese Ereignisse nicht zu nutzen und stattdessen auf Enter und Leave zurückzugreifen.
Tabulaturreihenfolge (Aktivierreihenfolge) Wie gerade erwähnt, kann der Eingabefokus auch durch Tab verändert werden. (Wenn Sie möchten, dass ein Steuerelement nicht per Tab aktiviert werden kann, setzen Sie TabStop auf False.) Die Reihenfolge, mit der die Steuerelemente durch Tab angesprungen werden, wird durch die Eigenschaft TabIndex gesteuert. Das erste Steuerelement für die Tabulatorreihenfolge hat TabIndex=0, das zweite 1 etc. Zur Einstellung der Tabulatorreihenfolge verwenden Sie am besten das Menükommando ANSICHT|AKTIVIERREIHENFOLGE. Damit wird bei jedem Steuerelement die aktuelle TabIndex-Nummer angezeigt. Um die Reihenfolge zu ändern, klicken Sie die Steuerelemente einfach der Reihe nach an. Anschließend deaktiveren Sie ANSICHT|AKTIVIERREIHENFOLGE wieder.
Abbildung 14.6: Einstellung der Tabulatorreihenfolge
590
14 Steuerelemente
Alt-Tastaturabkürzungen Der Eingabefokus kann auch durch Alt-Tastenkürzel verändert werden. Derartige Tastenkürzel werden dadurch angezeigt, dass der betroffene Buchstabe in der Beschriftung des Steuerelements unterstrichen ist (also beispielsweise ABBRUCH, um den Button mit Alt+A auszuwählen). Um für ein Steuerelement ein Tastenkürzel zu definieren, geben Sie einfach in der Text-Eigenschaft vor dem betreffenden Buchstaben ein &-Zeichen an (also Text="&Abbruch"). Wenn Sie in einem Steuerelement das &-Zeichen anzeigen möchten, müssen Sie es im Text zweimal hintereinander angeben (z.B. Text="Drag && Drop") oder die Möglichkeit zur Definition von Tastenkürzeln durch UseMnemonic=False ganz deaktivieren. Bei Steuerelementen, für die kein Tastenkürzel definiert werden kann (z.B. TextBox), können Sie ein Label-Feld zur Beschriftung voranstellen und dieses mit einem Tastenkürzel ausstatten. Das Label-Feld kann den Eingabefokus selbst nicht erhalten und gibt ihn an das nächste Steuerelement in der Tabulatorreihenfolge weiter. Die Reaktion auf das Alt-Kürzel hängt vom Steuerelementtyp ab. Die meisten Steuerelemente erhalten einfach nur den Eingabefokus. Bei Buttons, Optionsfeldern und Auswahlkästchen wird das Steuerelement außerdem ausgewählt, d.h., das Alt-Kürzel hat diesselbe Wirkung wie ein Anklicken mit der Maus.
Fokus per Programmcode verändern Nicht nur die Anwender können den Fokus verändern (per Tastatur oder Maus), Sie können den Fokus auch per Programmcode auf ein anderes Steuerelement richten. Dazu führen Sie für das entsprechende Steuerelement einfach die Focus-Methode aus: Textbox1.Focus()
Die Methode liefert True oder False zurück, je nachdem, ob der Fokuswechsel geglückt ist oder nicht. (Der Fokuswechsel kann scheitern, wenn er von dem Steuerelement, das den Fokus momentan enthält, in der Validating-Ereignisprozedur verhindert wird.)
Validierung Es gibt verschiedene Zeitpunkte, zu denen Sie überprüfen können, ob Benutzereingaben im Steuerelement bzw. im gesamten Dialog korrekt sind. (Welche von diesen drei Varianten die beste ist, hängt von der jeweiligen Anwendung ab.) •
Bei jeder Änderung der Daten im Steuerelement: Dazu sehen viele Steuerelemente Changed-Ereignisse vor, z.B. TextChanged bei einem TextBox-Steuerelement.
•
Bei einem Fokuswechsel: Dazu ist das Ereignis Validating vorgesehen. Falls ein Eingabefehler festgestellt wird, kann durch e.Cancel=True der Fokuswechsel verhindert werden. Falls die Validating-Prozedur erfolgreich beendet wird (ohne e.Cancel=True), tritt danach ein Validated-Ereignis auf. Es kann dazu verwendet werden, eventuell im ValidatingCode dargestellte Fehlermeldungen, Farbveränderungen etc. rückgängig zu machen.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
•
591
Beim Beenden des Dialogs: In diesem Fall hängt der Ort für den Validierungscode von der Logik des Dialogs bzw. Fensters ab. Bei einfachen Dialogen eignet sich die Ereignisprozedur der OK-Buttons.
VERWEIS
Die Validating- und Validated-Ereignisse können durch die Einstellung CausesValidation = False unterbunden werden. Ein einfaches Beispiel für die Anwendung des Validating-Ereignisses zur Überprüfung, ob eine Zahleneingabe in einer TextBox korrekt durchgeführt wurde, finden Sie in Abschnitt 14.4.3. Die Anwendung des ErrorProvider-Steuerelements als Fehlerindikator wird in Abschnitt 14.10.5 demonstriert.
Reihenfolge der Ereignisse Wenn Sie mit Tab oder auf eine andere Weise den Fokus von steuerelement1 in steuerelement2 setzen, dann treten für die beiden Steuerelemente folgende Ereignisse in der hier angegebenen Reihenfolge auf: •
steuerelement1_Leave
•
steuerelement1_Validating
•
steuerelement1_Validated
•
steuerelement2_Enter
Der Fluss der Ereignisse kann in der Validating-Ereignisprozedur durch e.Cancel=True gestoppt werden. In diesem Fall bleibt der Fokus in steuerelement1.
14.2.4 Sonstiges Tastatur und Maus: Fast alle Steuerelemente können Tastatur- und Mauseingaben verarbeiten. Diese Eingaben lösen eine ganze Menge von Ereignissen aus (KeyDown, -Press, -Up sowie MouseEnter, -Leave, -Down, -Up, -Move und -Hover), die in den Abschnitten 15.9 und 15.10 beschrieben werden. Das Aussehen der Maus kann mit Cursor eingestellt werden. MousePosition und -Buttons geben Auskunft über die aktuelle Mausposition und den Zustand der Maustasten, ModifierKeys enthält außerden den Zustand von Alt, Shift etc. Die Methoden PointToScreen und PointToClient führen eine Umrechnung zwischen lokalen Koordinaten und absoluten Bildschirmkoordinaten durch. Jedes einzelne Steuerelement kann mit der Eigenschaft ContextMenu mit einem eigenen Menü ausgestattet werden, das erscheint, wenn die rechte Maustaste gedrückt wird. Datenbankanwendung (gebundene Steuerelemente): Die meisten Steuerelemente können mit so genannten Datenquellen verbunden werden. Derartige Datenquellen (z.B. DataTable- oder DataView-Objekte) stellen im Regelfall eine Verbindung zu einer Datenbank her.
592
14 Steuerelemente
Wenn die Eigenschaften DataSource und DataBinding des Steuerelements so eingestellt werden, dass sie auf die Datenquelle verweisen, werden im Steuerelement automatisch die Daten aus der Datenbank angezeigt (und können dort sogar verändert werden). Eigenschaften der Entwicklungsumgebung: Im Eigenschaftsfenster werden auch die Eigenschaften Locked und Modifiers angegeben. Dabei handelt es sich aber nicht um Eigenschaften des Steuerelements, sondern um Zusatzinformationen, die nur für die Verwaltung der Steuerelemente durch die Entwicklungsumgebung relevant sind. Locked=True bewirkt, dass das Steuerelement in der Entwicklungsumgebung vor Veränderungen geschützt ist. Modifiers gibt an, wie das Steuerelement innerhalb der Formularklasse deklariert werden soll. Per Default werden die Steuerelemente als Friend deklariert
und können daher per Code von anderen Formularen angesprochen werden (siehe auch Abschnitt 15.2.2). Verwaltung von Steuerelementen: Jedes Steuerelement befindet sich entweder direkt im Formular oder in einem anderen Steuerelement, das als Container funktioniert. Parent verweist auf das direkt übergeordnete Steuerelement, TopLevelControl auf den Container an der Spitze der Hierarchie (überlicherweise ein Form-Objekt). Bei Containern verweist Controls auf die enthaltenen Steuerelemente. GetChildAtPoint ermittelt, welches Steuerelement sich an einer bestimmten Position befindet. Bei jedem Steuerelement können Sie mit Name dessen Namen feststellen. Das kann vor allem dann interessant sein, wenn mehrere Steuerelemente diesselbe Ereignisprozedur nutzen (siehe Abschnitt 14.11.3). Zugriff auf Steuerelemente in Multithreading-Anwendungen: Generell darf nur der Haupt-Thread auf Steuerelemente zugreifen. In einer Multithreading-Anwendung müssen Sie die Methoden BeginInvoke, Invoke und EndInvoke verwenden. Beispiele für die Anwendung von Invoke finden Sie in den Abschnitten 15.2.7 und 16.5.6. Zusatzinformationen speichern: Manchmal besteht der Wunsch, zusammen mit dem Steuerelement Zusatzinformationen zu verwalten. Dazu steht die Tag-Eigenschaft zur Verfügung, in der ein beliebiges Objekt gespeichert werden kann. (Da Tag den Typ Objekt hat, müssen Sie beim Auslesen eventuell CType-Funktionen zur Konvertierung in den tatsächlichen Objekttyp verwenden.)
TIPP
Tag steht vor allem aus historischen Gründen (Kompatibilität mit VB6) zur Verfü-
gung. Übersichtlicheren Code erzielen Sie im Regelfall, indem Sie ein neues Steuerelement von einem vorhandenen Steuerelement ableiten und es mit Zusatzeigenschaften oder -methoden ausstatten. Ein vergleichbares Beispiel (Ableitung einer ListViewItem-Klasse) finden Sie in Abschnitt 14.6.4.
RightToLeft: In zahlreichen Ländern dieser Erde wird Text nicht von links nach rechts ge-
schrieben, sondern in die umgekehrte Richtung. Falls Sie internationale Software entwickeln, können Sie mit der Eigenschaft RightToLeft zahlreiche Steuerelemente an diese Gepflogenheit anpassen.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
593
VERWEIS
Accessibility: Mit den Eigenschaften AccessibleName, -Description und -Role können Sie den Namen, die Bedeutung und die Funktion eines Steuerelement in Ihrem Programm beschreiben. Diese Informationen sind insbesondere für Benutzer mit eingeschränktem Sehvermögen hilfreich. Eine Menge Hintergrundinformationen und Tipps, wie Programme gestaltet werden können, damit sie auch von körperlich behinderten Benutzern genutzt werden können, finden Sie in der Online-Hilfe (MSDN), wenn Sie nach Microsoft Active Accessibility suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/dnacc/html/msaa.htm
14.2.5 Syntaxzusammenfassung Gemeinsame Eigenschaften für System.Windows.Forms-Steuerelemente AccessibleDescription
beschreibt das Steuerelement (für sehbehinderte Benutzer).
AccessibleName
benennt das Steuerelement.
AccessibleRole
beschreibt die Funktion des Steuerelements.
Anchor
gibt an, an welchen Seiten der Abstand zwischen dem Steuerelementrand und dem Rand des Formulars (bzw. übergeordneten Steuerelements) konstant bleiben soll.
AutoSize
gibt an, ob die Größe des Steuerelements durch seinen Inhalt bestimmt werden soll.
BackColor
bestimmt die Hintergrundfarbe des Steuerelements. Mit BackColor=Transparent können durchsichtige Steuerelemente erreicht werden.
Bounds
gibt die Außenposition und -größe an (Rectangle-Objekt).
BorderStyle
gibt an, ob das Steuerelement umrandet werden soll.
Bottom
gibt die Y-Position des unteren Rands an (read-only).
CausesValidation
gibt an, ob die Validating- und Validated-Ereignisse bei einem Fokuswechsel ausgelöst werden sollen.
ClientRectangle
gibt die Innengröße und -position an (Rectangle-Objekt).
ClientSize
gibt die Innengröße an (Size-Objekt).
ContainsFocus
gibt an, ob das Steuerelement gerade den Fokus besitzt.
ContextMenu
verweist auf das Kontextmenü zum Steuerelement.
Controls
verweist auf die enthaltenen Steuerelemente (wenn das Steuerelement ein Container ist).
Cursor
bestimmt das Aussehen des Maussymbols.
Dock
gibt an, ob und an welchem Rand des Fensters bzw. Containers das Steuerelement angedockt ist.
594
14 Steuerelemente
Gemeinsame Eigenschaften für System.Windows.Forms-Steuerelemente Enable
aktiviert das Steuerelement. (Enable=False bewirkt eine graue Darstellung; das Steuerelement kann dann nicht mehr verwendet werden.)
FlatStyle
gibt an, ob die Steuerelemente flach (ohne 3D-Optik) dargestellt werden sollen.
Font
gibt die Schriftart für den Text an.
ForeColor
bestimmt die Vordergrundfarbe des Steuerelements.
Height
gibt die Außenhöhe an.
Image
gibt das im Steuerelement angezeigte Bild an.
ImageAlign
gibt die Positionierung des Bilds an (z.B. rechts unten).
Left
gibt die X-Position des linken Rands an.
Location
gibt die Position des linken oberen Ecks an (Point-Objekt).
ModifierKeys
liefert den aktuellen Zustand der Tasten Shift, Strg, Alt etc.
MouseButtons
liefert den aktuellen Zustand der Maustasten.
MousePosition
liefert die aktuelle Mausposition als Point-Objekt in Bildschirmkoordinaten.
Name
liefert den Namen des Steuerelements.
Parent
verweist auf das übergeordnete Steuerelement bzw. das Formular oder den Container.
Size
gibt die Außengröße an (Size-Objekt).
Visible
gibt an, ob das Steuerelement sichtbar ist.
Right
gibt die X-Position des rechten Rands an (read-only).
RightToLeft
gibt an, dass Texteingaben von rechts nach links erfolgen sollen.
TabStop
gibt an, dass das Steuerelement mit Tab ausgewählt werden kann.
TabIndex
gibt die Tabulator-Reihenfolge an.
Text
gibt den im Steuerelement angezeigten Text an. Wenn im Text das Zeichen & angegeben wird, wird dieses Zeichen nicht angezeigt. Stattdessen gilt der folgende Buchstabe als Tastenkürzel.
TextAlign
gibt die Positionierung und Ausrichtung des Texts an (z.B. oben zentriert).
Top
gibt die Y-Position des oberen Rands an (read-only).
TopLevelControl
verweist auf den Basiscontainer, in dem sich die Steuerelemente befinden und dessen Parent-Eigenschaft Nothing enthält. Normalerweise ist das ein Form-Objekt.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
595
Gemeinsame Eigenschaften für System.Windows.Forms-Steuerelemente UseMnemonic
gibt an, ob das Zeichen & in der Text-Eigenschaft dazu dient, Alt-Tastenkürzel zu definieren. Per Default ist das der Fall (UseMnemonic=True).
Width
gibt die Außenbreite an.
Gemeinsame Methoden für System.Windows.Forms-Steuerelemente BeginInvkoe
führt eine Prozedur asynchron aus. BeginInvoke wird nur in Multithreading-Anwendungen benötigt.
BringToFront
platziert das Steuerelement über allen anderen Steuerelementen.
EndInvoke
beendet die mit BeginInvoke gestartete asynchrone Ausführung einer Prozedur.
Focus
versucht den Eingabefokus in das Steuerelement zu setzen und liefert True oder False, je nachdem, ob der Fokuswechsel gelungen ist oder nicht.
GetChildAtPoint
ermittelt das Steuerelement, das sich an einer bestimmten Koordinatenposition befindet.
Hide
macht das Steuerelement unsichtbar (Visible=False).
Invalidate
bewirkt das Neuzeichnen eines Teils des Steuerelements.
Invoke
löst die synchrone Ausführung einer Prozedur im Windows.Forms-Haupt-Thread aus. Invoke wird nur bei Multithreading-Anwendungen benötigt.
PointToClient
rechnet Bildschirmkoordinaten in das lokale Koordinatensystem des Steuerelements um.
PointToScreen
ist die Umkehrung zu PointToClient.
Refresh
bewirkt ein Neuzeichnen des Steuerelements (und aller eventuell darin enthaltenen Steuerelemente).
Select
aktiviert das Steuerelement und setzt den Eingabefokus dorthin.
SetBounds
verändert die Position und Größe des Steuerelements.
Show
macht das Steuerelement sichtbar (Visible=True).
Update
führt noch nicht ausgeführte Zeichenoperationen sofort aus.
Gemeinsame Ereignisse für System.Windows.Forms-Steuerelemente Click
gibt an, dass das Steuerelement angeklickt wurde.
DoubleClick
gibt an, dass das Steuerelement durch einen Doppelklick angeklickt wurde.
596
14 Steuerelemente
Gemeinsame Ereignisse für System.Windows.Forms-Steuerelemente Enter
bedeutet, dass das Steuerelement den Eingabefokus erhält.
GotFocus
entspricht Enter, ist aber nur für interne Zwecke vorgesehen.
Leave
bedeutet, dass das Steuerelement den Eingabefokus verliert.
LostFocus
entspricht Leave, ist aber wie GotFocus nur für interne Zwecke vorgesehen.
KeyXxx
wird bei verschiedenen Tastaturereignissen ausgelöst (siehe Abschnitt 15.9).
MouseXxx
wird bei diversen Mausereignissen wie Bewegung oder Mausklick ausgelöst (siehe Abschnitt 15.10).
Paint
gibt an, dass Teile des Steuerelements neu gezeichnet werden müssen.
SystemColorsChanged
gibt an, dass sich die Windows-Standardfarben verändert haben.
Validating
gibt an, dass das Steuerelement den Eingabefokus verliert. Die Prozedur kann dazu verwendet werden, die Richtigkeit einer Eingabe in einem Steuerelement zu kontrollieren.
Validated
gibt an, dass die Validating-Ereignisprozedur erfolgreich beendet wurde (also ohne e.Cancel=True).
14.3
Buttons
Die in diesem Abschnitt vorgestellten Steuerelemente Button, CheckBox und RadioButton erfüllen nicht nur ähnliche Aufgaben, sie sind auch intern von der gemeinsamen Klasse ButtonBase abgeleitet. Deswegen weisen sie eine Menge gemeinsamer Eigenschaften auf.
14.3.1 Gewöhnliche Buttons Gestaltung: FlatStyle gibt an, ob der Button normal, flach oder als so genannter PopupButton gestaltet werden soll. (Letzeres bedeutet, dass sein Rahmen markiert wird, wenn die Maus über den Button bewegt wird.) Die Button-Klasse sieht keine Möglichkeit vor, einen Umschalt-Button zu erzeugen (also einen Button, der durch einmaliges Anklicken gedrückt und durch nochmaliges Klicken wieder losgelassen wird). Derartige Buttons können Sie mit der CheckBox-Klasse erzeugen, wenn Sie Appearance=Button einstellen.
14.3 Buttons
597
Ereignisse: Das entscheidende und im Regelfall einzig relevante Ereignis eines Buttons lautet Click. Es wird aufgerufen, nachdem ein Button mit der Maus angeklickt oder per Tastatur (mit der Leertaste) aktiviert wurde. Rückgabewert: Wenn ein Fenster als modaler Dialog verwendet wird, kann beim Schließen ein Rückgabewert (z.B. DialogResult.OK oder .Cancel) übergeben werden. Zur Einstellung des Rückgabewerts führen Sie in einer Ereignisprozedur des Dialogs Me.DialogResult=... aus. Noch bequemer ist es, die Button-Eigenschaft DialogResult einzustellen. Damit wird durch das Anklicken des Buttons die DialogResult-Eigenschaft des Formulars geändert. (Das Schlüsselwort DialogResult ist insofern ein wenig verwirrend, als es gleichermaßen die DialogResult-Aufzählung, eine Form- und eine Button-Eigenschaft bezeichnet.)
Default-Buttons In jedem Formular können zwei so genannte Default-Buttons definiert werden. Der so genannte AcceptButton kann durch Return ausgewählt werden, der CancelButton durch Esc. Der jeweilige Button braucht dazu nicht den Eingabefokus zu besitzen. Um einen Default-Button zu definieren, fügen Sie den Button ganz gewöhnlich in das Formular ein. Anschließend wählen Sie den Button bei der Einstellung der Eigenschaft AcceptButton oder CancelButton des Formulars (!) aus. Im Eigenschaftsfenster wird bei diesen beiden Eigenschaften ein Listenfeld mit allen Buttons im Formular angezeigt. Um es nochmals zu betonen: AcceptButton und CancelButton sind Eigenschaften des Formulars, nicht des Buttons!
HINWEIS
AcceptButton und CancelButton funktionieren nur dann wie erwartet, wenn sich der Eingabefokus nicht gerade in einem Steuerelement befindet, das Return oder Esc als
Eingabe aktzeptiert. Ersters ist für eine ganze Reihe von Steuerelementen der Fall (z.B. für andere Buttons oder für mehrzeilige Textfelder). Deswegen kann das folgende Programm nur per Esc beendet werden, während Return wirkungslos bleibt (es sei denn, der OK-Button hat gerade den Eingabefokus). Beachten Sie auch, dass Accept- bzw. CancelButton nicht zu einem automatischen Schließen des Formulars führen! Es kommt lediglich zu einem automatischen Aufruf der Click-Ereignisprozedur des jeweiligen Steuerelements. Dort können Sie dann das Fenster mit Close schließen. Durch Return bzw. Esc kommt es nicht zu einem Fokuswechsel. Der Eingabefokus bleibt in dem gerade aktiven Steuerelement.
Beispielprogramm Das Beispielprogramm erfüllt lediglich die Aufgabe, verschiedene Formen der Button-Gestaltung zu demonstrieren. Die Grafik im Button im rechten oberen Eck (Button5) wird mit einer kleinen Paint-Ereignisprozedur erzeugt. In diesem wurde auf eine Beschriftung des Buttons verzichtet. (Details zur Grafikprogrammierung folgen in Kapitel 16.) Bei den fünf
598
14 Steuerelemente
großen Buttons erfolgt keine Reaktion auf das Anklicken. OK und ABBRUCH (ButtonOK und ButtonCancel) beenden das Programm.
Abbildung 14.7: Sieben unterschiedlich gestaltete Buttons
' Beispiel steuerelemente\buttons Private Sub Button5_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles Button5.Paint Dim gr As Graphics = e.Graphics Dim radius As Integer Dim w As Integer = Button5.ClientSize.Width Dim h As Integer = Button5.ClientSize.Height For radius = 10 To 40 Step 3 gr.DrawEllipse(Pens.Blue, _ w / 2.0F - radius, h / 2.0F - radius, 2 * radius, 2 * radius) Next End Sub Private Sub ButtonOK_Click(...) Handles ButtonOK.Click Me.Close() End Sub Private Sub ButtonCancel_Click(...) Handles ButtonCancel.Click Me.Close() End Sub
14.3.2 Auswahlkästchen (CheckBox) Das CheckBox-Steuerelement dient dazu, Auswahlkästchen im Formular zu realisieren (siehe Abbildung 14.8). Jedes Auswahlkästchen kann unabhängig von allen anderen ausgewählt werden. Mit Appearance=Button erreichen Sie, dass das Kästchen als UmschaltButton dargestellt wird. Mit FlatStyle=True erzielen Sie eine flache Optik. Mit CheckAlign und TextAlign können Sie die Position des Kästchens und des dazugehörenden Texts ändern. (Per Default werden sowohl das Kästchen als auch der Text links und vertikal mittig ausgerichtet.)
14.3 Buttons
599
Bei jeder Änderung des Auswahlkästchens (also auch beim Deaktivieren!) kommt es zum Ereignis CheckedChange. Dort geben zwei Eigenschaften Auskunft über den aktuellen Zustand des Steuerelements: •
CheckState kann drei Zustände einnehmen: Checked (ausgewählt), Unchecked (nicht ausgewählt) oder Indeterminate (unbestimmt, wie in CheckBox2 in Abbildung 14.8). Der Zustand Indeterminate kann nur eintreten, wenn die Eigenschaft ThreeState den Wert True enthält.
•
Checked ist einfacher auszuwerten und enthält False, wenn das Kästchen nicht ausgewählt wurde, sonst True. (Vorsicht: Das bedeutet, dass auch Indeterminate=True gilt!)
Normalerweise wird der Zustand der Steuerelemente beim Anklicken automatisch verändert. Wenn Sie das nicht möchten, müssen Sie die Eigenschaft AutoCheck auf False setzen. Allerdings müssen Sie jetzt das Click-Ereignis auswerten und CheckState selbst verändern.
VERWEIS
Abbildung 14.8: Verschiedene Darstellungsvarianten von Auswahlkästchen und Optionsfeldern
Wenn Sie nicht eine einzelne CheckBox, sondern eine ganze Liste solcher Steuerelemente benötigen, können Sie auch auf die Steuerelemente CheckedListBox, ListView und TreeView zurückgreifen (siehe Abschnitt 14.6.2, 14.6.4 und 14.6.6).
14.3.3 Optionsfelder (RadioButton) Optionsfelder verhalten sich im Wesentlichen wie Auswahlkästchen. Die Eigenschaften Appearance, CheckAlign, FlatStyle und Checked haben dieselbe Bedeutung wie bei Auswahlkästchen. CheckState und ThreeState stehen dagegen nicht zur Verfügung, weil Optionsfelder keinen undefinierten Zustand einnehmen können. Die Besonderheit von Optionsfeldern besteht darin, dass immer nur eines ausgewählt sein kann. Sobald ein anderes Optionsfeld angeklickt wird, wird das bisher ausgewählte deaktiviert. (Das ist gleichzeitig die einzige Möglichkeit, ein Optionsfeld zu deaktivieren.
600
14 Steuerelemente
Anders als bei Auswahlkästchen werden Optionsfelder durch ein zweites Anklicken nicht wieder deaktiviert.) Beachten Sie, dass das CheckedChange-Ereignis wie bei Kontrollkästchen auch dann eintritt, wenn eine Option dadurch deaktiviert wird, dass ein anderes Optionsfeld angeklickt wurde. Wenn Sie in einem Formular mehrere Gruppen von Optionsfeldern benötigen, die unabhängig voneinander ausgewählt werden können, dann müssen Sie die Optionsfelder durch GroupBox-Felder gruppieren. (Prinzipiell können Sie zum Gruppieren auch das unsichtbare Panel-Steuerelement verwenden, allerdings geben Sie Ihren Anwendern dadurch kein visuelles Feedback, welche Optionsfelder zusammengehören. GroupBox und Panel werden im weiteren Verlauf dieses Kapitels noch vorgestellt.)
14.4
Textfelder
Das folgende Diagramm zeigt, wie die in diesem Abschnitt vorgestellten Steuerelemente voneinander abgeleitet sind. Klassenhierarchie im System.Windows.Forms-Namensraum ...
└─ Control ├─ Label │ └─ LinkLabel │ └─ TextBoxBase ├─ TextBox └─ RichtTextBox
Basisklasse für alle Steuerelemente Text anzeigen Links ins Internet bzw. zu Dateien anzeigen (z.B. Button, CheckBox, RadioButton) Basisklasse für TextBox und RichTextBox Text eingeben beliebig formatierten Text eingeben
14.4.1 Label Zum Label-Steuerelement gibt es nicht viel zu sagen: Es dient zur Beschriftung anderer Elemente im Formular. Der Beschriftungstext wird durch die Text-Eigenschaft eingestellt. TextAlign steuert die Ausrichtung des Texts. Zu langer Text wird normalerweise automatisch auf mehrere Zeilen verteilt. Wenn Sie AutoSize auf True setzen, wird der Text dagegen einzeilig dargestellt. Das Label-Steuerelement nimmt dann automatisch die erforderliche Größe (Breite) an.
Mit BorderStyle können Sie das Steuerelement mit einem zwei- oder dreidimensionalen Rand ausstatten. Wenn Sie möchten, dass das Steuerelement mit Ausnahme des Texts durchsichtig ist, verwenden Sie als Hintergrundfarbe Transparent (Eigenschaft BackColor).
14.4 Textfelder
601
Obwohl das Label-Steuerelement selbst keinen Eingabefokus erhalten kann, besteht die Möglichkeit, durch das Zeichen & im Text ein Alt-Tastenkürzel zu definieren. Bei der Eingabe des Tastenkürzels wird der Fokus an das nächste Steuerelement in der Tabulaturreihenfolge weitergeben. Das ist praktisch, um Tastenkürzel auch für solche Steuerelemente zu ermöglichen, für die selbst keine Tastenkürzel definiert werden (d.h. Steuerelemente wie TextBox, die die Eigenschaft UseMnemonic nicht besitzen).
14.4.2 LinkLabel Das LinkLabel-Steuerelement ist vom Label-Steuerelement abgeleitet. Es vereinfacht den Aufruf von Webseiten, das Versenden von E-Mails bzw. die Anzeige von Dokumenten per Mausklick. Anders als das Label-Steuerelement kann LinkLabel mit Tab direkt ausgewählt werden. Wenn das Steuerelement den Eingabefokus hat, hat Return dieselbe Wirkung wie das Anklicken des Steuerelements. Das Steuerelement sieht auch Tastenkürzel vor (UseMnemonic hat per Default den Wert True), diese scheinen aber nicht zu funktionieren. Im Eigenschaftsfenster geben Sie mit der Text-Eigenschaft jenen Text an, der im Label angezeigt werden soll. Per Default wird dieser Text in blauer Schrift angezeigt. Je nach der Einstellung für den Internet Explorer wird der Text immer unterstrichen oder nur dann, wenn sich die Maus über dem Steuerelement befindet, oder gar nicht. Per Default übernimmt das Steuerelement die Logik vom Internet Explorer. Über die Eigenschaft LinkBehavior können Sie per Code auch eine andere Einstellung erreichen. LinkBehavior kann allerdings nicht im Eigenschaftsfenster eingestellt werden. Während sich die Maus über dem Steuerelement befindet, erscheint sie außerdem als Zeigefinger.
Abbildung 14.9: Verschiedene Label- und LinkLabel-Varianten
Wenn Sie den Label anklicken, passiert allerdings vorerst gar nichts. Stattdessen tritt ein LinkClicked-Ereignis auf. In der Ereignisprozedur können Sie nun eine Webseite, ein lokales Dokument etc. anzeigen. .NET bietet für diesen Zweck die Methode System.Diagnostics.Process.Start an, an die Sie eine Zeichenkette mit dem Link übergeben müssen. Für die drei wichtigsten Anwendungen (Webadressen, E-Mail-Adressen, lokale Dateien) sollte die Zeichenkette mit dem Link wie in den folgenden Mustern aussehen:
602
14 Steuerelemente
•
http://www.kofler.cc
•
mailto:[email protected] oder mailto:[email protected]?subject=LinkLabel&body=bla bla
•
file:readme.txt (Dateiname im selben Verzeichnis wie die *.exe-Datei) oder file:../readme.txt (Dateiname relativ zum *.exe-Verzeichnis) C:\readme.txt (absoluter Dateiname)
Es gibt nun zwei Vorgehensweisen, eine einfache und eine komplizierte. Die einfache Variante geht davon aus, dass im LinkLabel-Steuerelement nur ein einziger Link dargestellt wird. In diesem Fall sieht die LinkClicked-Ereignisprozedur wie das folgende Muster aus. Die Start-Methode versucht das dem Dateityp zugeordnete Programm zu starten (Notepad bei einer *.txt-Datei , AcrobatReader bei einer *.pdf-Datei etc.). Die Try-Catch-Konstruktion vermeidet Fehlermeldungen, wenn die Datei nicht gefunden wird. Die Zuweisung LinkVisited=True bewirkt, dass der angeklickte Link (wie in einem Browser) die Farbe ändert. (Die Farben vor, während und nach dem Anklicken können natürlich ebenfalls eingestellt werden, und zwar mit LinkColor, ActiveLinkColor und VisitedLinkColor.) ' Beispiel steuerelemente\labels Private Sub LinkLabel3_LinkClicked(...) Handles LinkLabel3.LinkClicked Try Diagnostics.Process.Start("c:\readme.txt") LinkLabel3.LinkVisited = True Catch ... Fehlermeldung etc. End Try End Sub
Die komplizierte Variante ist in der Online-Dokumentation zur LinkLabel-Klasse beschrieben. Sie ermöglicht es, einem LinkLabel-Steuerelement mehrere Links zuzuordnen. So können Sie beispielsweise erreichen, dass beim Anklicken der ersten vier Zeichen des LinkLabel-Tests ein anderer Link verwendet wird als beim Anklicken der nächsten vier Zeichen. (Allzu oft wird das nicht sinnvoll sein ...) Damit das funktioniert, müssen Sie während der Initialisierung des Programms (z.B. in Form1_Load) die Links-Klasse des Steuerelements initialisieren. Dazu fügen Sie mit Links.Add den gewünschten Link hinzu, wobei Sie in den beiden ersten Parametern von Add
angeben, für welchen Zeichenbereich der Link gilt. Im folgenden Beispiel verweisen die ersten vier Zeichen des Label-Texts auf meine Website, die nächsten vier Zeichen des Labels auf die Microsoft-Website. Der Rest des Labels ist passiv. Private Sub Form1_Load(...) Handles MyBase.Load LinkLabel1.Links.Add(0, 4, "http://www.kofler.cc") LinkLabel1.Links.Add(4, 4, "http://www.microsoft.com") End Sub
14.4 Textfelder
603
Nun ändert sich auch der Code in der LinkClicked-Ereignisprozedur, damit der richtige Link verwendet wird. Dabei wird der Ereignisparameter e ausgewertet. e.Link verweist auf ein Link-Objekt, das unter anderem die Zeichenkette mit der Adresse enthält. Private Sub LinkLabel1_LinkClicked(ByVal sender As Object, _ ByVal e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) _ Handles LinkLabel1.LinkClicked Try ' Link aufrufen Diagnostics.Process.Start(e.Link.LinkData.ToString()) ' Link in der 'visited'-Farbe anzeigen LinkLabel1.Links(LinkLabel1.Links.IndexOf(e.Link)).Visited = True Catch ... Fehlermeldung etc. End Try End Sub
14.4.3 TextBox Das TextBox-Steuerelement enthält eine Menge Funktionen, mehr als auf den ersten Blick erkennbar sind. Grundsätzlich dient es zur Texteingabe. Die Verwaltung von Tastatureingaben erfolgt automatisch, wobei auch alle gängigen Kommandos zum Markieren, Kopieren, Ausschneiden und Einfügen von Text unterstützt werden. (Sie brauchen sich also nicht um die Kommunikation mit der Zwischenablage zu kümmern.)
Abbildung 14.10: Verschiedene Gestaltungsvariante des TextBox-Steuerelements
TextBox ist ein vielgestaltiges Steuerelement, wie Abbildung 14.10 beweist. MultiLine gibt an, ob im Steuerelement mehrere Zeilen Text dargestellt bzw. eingegeben werden können. ScrollBars gibt an, ob das Feld mit einem vertikalen und/oder horizontalen Schiebebalken ausgestattet werden soll. WordWrap bestimmt das Verhalten bei langen Textzeilen: per Default werden diese auf mehrere umbrochen. WordWrap=False verhindert das – aber dann sollte das Steuerelement mit einem horizontalen Schiebebalken ausgestattet werden. Bor-
604
14 Steuerelemente
derStyle gibt an, wie das Steuerelement umrandet werden soll (per Default durch einen 3D-Rand). TextAlign gibt an, wie der Text innerhalb des Steuerelements ausgerichtet wer-
den soll (linksbündig, rechtsbündig oder zentriert). Texteingabe: Per Default kann der im Steuerelement angezeigt Text verändert werden. Wenn Sie das nicht möchten, setzen Sie ReadOnly auf True. (Damit ändert sich auch die Hintergrundfarbe von Weiß nach Grau.) Wenn der eingegebene Text ausschließlich in Groß- oder Kleinbuchstaben angezeigt werden soll, erreichen Sie das durch eine Veränderung der Eigenschaft CharacterCasing. Wenn das Textfeld zur Passworteingabe verwendet werden soll, können Sie mit PasswordChar ein Zeichen angeben, das statt des Texts angezeigt wird. Normalerweise ignoriert das Steuerelement die Tasten Tab und Return (nur bei der einzeiligen Variante), weil damit üblicherweise ein Fokuswechsel zwischen den Steuerelementen bzw. die Auswahl des Defaultbuttons möglich ist. Wenn Sie diese Zeichen direkt eingeben möchten, verändern Sie einfach die Eigenschaften AcceptsReturn und AcceptsTab. Anders als in den TextBox-Versionen von VB1-6 ist die Textlänge nun grundsätzlich nicht mehr auf 32767 Zeichen limitiert. Allerdings ist die Eigenschaft MaxLength auf diesen Wert voreingestellt (aus mir unerfindlichen Gründen). Wenn Sie das Steuerelement also dazu verwenden möchten, umfangreiche Textdateien zu editieren, müssen Sie MaxLength auf 0 setzen (und so die Längenkontrolle abschalten). Generell gilt MaxLength nur für Texteingaben per Tastatur; wenn Sie den Text per Programmcode ändern, dürfen Sie unabhängig von MaxLength beliebig lange Zeichenketten einfügen.
Programmierung Text auslesen: Den eingegebenen Text können Sie natürlich aus der Eigenschaft Text entnehmen (und über diese Eigenschaft auch ändern). Bei mehrzeiligem Text ist die Eigenschaft Lines praktisch: Sie gibt Zugriff auf ein String-Feld, das die einzelnen Textzeilen enthält. Allerdings können Sie dieses Feld nur lesen, nicht verändern. TextLength gibt Auskunft über die Länge des Texts (gemessen in Zeichen). Die Anzahl der Zeilen können Sie via Lines ermitteln: TextBox1.Lines().GetUpperBound(0)+1. (Die Methode GetUpperBound ermittelt den größten zulässigen Index des Felds.)
Markierung: Die Eigenschaft SelectedText enthält den markierten Text. Wenn Sie einen Textbereich per Programmcode markieren möchten, verwenden Sie dazu die Eigenschaften SelectionStart (gibt die Startposition gemessen in Zeichen an) und SelectionLength. Die Methode Select bietet eine weitere Möglichkeit, Text zu markieren. Wenn gerade kein Text markiert ist, gibt SelectionStart die Cursorposition an; SelectionLength ist dann 0. Wenn Sie mit SelectionStart die Cursorposition verändern, sollten Sie die Methode ScrollToCaret ausführen, um sicherzustellen, dass der Cursor anschließend auch sichtbar ist. Alles markieren: Beim Programmstart gilt der gesamte Text in allen Steuerelementen als markiert. Wenn Sie das nicht möchten, können Sie in Form1_Load die Eigenschaft SelectionLength auf 0 setzen.
14.4 Textfelder
605
Wenn Sie möchten, dass des gesamte Text jedes Mal markiert wird, wenn das Feld den Eingabefokus erhält, fügen Sie die folgende Ereignisprozedur in Ihr Programm ein: Private Sub TextBox1_Enter(...) Handles TextBox1.Enter TextBox1.SelectAll() End Sub
Wenn die Anwender Ihres Programms zu einem späteren Zeitpunkt Strg+A drücken, um damit den gesamten Text manuell zu markieren, reagiert das Steuerelement hingegen nicht. Aber auch dieser Mangel lässt sich durch eine kleine Ereignisprozedur beheben. Dabei wird die Methode SelectAll verwendet, um den gesamten Text zu markieren. (Hintergrundinformationen zur Auswertung von Tastaturereignissen werden in Abschnitt 15.9 beschrieben.) Private Sub TextBox3_KeyDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyEventArgs) _ Handles TextBox3.KeyDown If e.KeyCode = Keys.A And e.Control Then TextBox3.SelectAll() End If End Sub
Text einfügen: Um Text einzufügen, können Sie einfach die Text-Eigenschaft ändern, z.B.: TextBox1.Text += "neue Zeile" + vbCrLf
Alternativ können Sie auch die Methode AppendText verwenden, die den Text aber ebenfalls wie die obige Anweisung am Ende einfügt. Um Text an der aktuellen Cursorposition einzufügen, verändern Sie einfach die SelectedText-Eigenschaft. Markierter Text wird dadurch überschrieben. Wenn Sie das vermeiden möchten, müssen Sie den Cursor zuerst an das Ende der Markierung setzen und SelectionLength dann auf 0 stellen. TextBox1.SelectionStart += TextBox1.SelectionLength TextBox1.SelectionLength = 0 TextBox1.SelectedText = "abc" + vbCrLf
Text kopieren, ausschneiden, einfügen: Für diese Standardoperationen stellt das Steuerelement die Methoden Copy, Cut und Paste zur Verfügung. Mit Undo kann die letzte Operation rückgängig gemacht werden. Textänderungen verfolgen: Neben den Standardereignissen kennt das TextBox-Steuerelement das TextChanged-Ereignis. Es wird nach jeder Änderung des Texts aufgerufen. Wenn es nur darum geht, festzustellen, ob sich der Inhalt des Steuerelements seit einem bestimmten Zeitpunkt geändert hat, können Sie die Modified-Eigenschaft auswerten. Diese Eigenschaft wird durch jede Benutzereingabe, die den Text verändert, auf True gesetzt. Direkte Zuweisungen an die Text-Eigenschaft verändern Modified nicht, wohl aber Veränderungen via AppendText oder SeletedText. Sie können Modified auch explizit auf False zurücksetzen (beispielsweise, wenn der Text gerade in einer Datei gespeichert wurde).
606
14 Steuerelemente
Validierung von Texteingaben Häufig wollen Sie Texteingaben sofort verifizieren, um Fehleingaben schon zu einem möglichst frühen Zeitpunkt zu vermeiden. Dazu können Sie die TextChanged- oder die Validating-Ereignisprozedur verwenden. Das Ereignis TextChanged tritt bei jeder Veränderung der Eingabe auf, Validating dagegen erst beim Versuch, den Eingabefokus aus dem Steuerelement herauszubewegen. Insofern ist die Validating-Ereignisprozedur in den meisten Fällen der bessere Ort für die Eingabekontrolle. Wenn Sie in dieser Prozedur e.Cancel = True ausführen, dann kommt es wieder zu einem Fokuswechsel. (Validating tritt übrigens nach dem Leave-Ereignis auf. Die Reihenfolge der Ereignisse ist in Abschnitt 14.2.3 zusammengefasst.) Falls die Validating-Prozedur erfolgreich ausgeführt wird (also nicht mit e.Cancel = True beendet wird), tritt anschließend ein Validated-Ereignis auf. Die Ereignisprozedur kann dazu verwendet werden, um eventuell noch sichtbare Fehlerindikatoren zu entfernen oder Berechnungen auf der Basis der zuvor kontrollierten Eingaben durchzuführen. In vielen Fällen – wie im folgenden Beispielprogramm – ist eine Auswertung des Ereignisses nicht erforderlich. Ein Anwendungsbeispiel für das Ereignis finden Sie in Abschnitt 14.4.3.
Beispielprogramm Im folgenden Beispielprogramm sollen Sie zwei Zahlen eingeben. Das Programm berechnet automatisch das Produkt der beiden Zahlen und zeigt dieses in einem Label-Feld an (siehe Abbildung 14.11). Die Besonderheit des Programms besteht darin, dass in den beiden Textfeldern wirklich nur Zahlen eingegeben werden dürfen. Bei einer Fehleingabe kann das Steuerelement nicht verlassen werden, und es erscheint eine Fehlermeldung. Der Programmcode ist einfach zu verstehen: Bei jeder Veränderung in einer der beiden TextBox-Steuerelemente wird mit IsNumeric getestet, ob sich beide Texte in Zahlen um-
wandeln lassen. Wenn das möglich ist, wird das Ergebnis der Multiplikation berechnet und in LabelResult dargestellt. ' Beispiel steuerelemente\textbox-validation Private Sub TextBox1_TextChanged(...) Handles TextBox1.TextChanged CalcResult() End Sub Private Sub TextBox2_TextChanged(...) Handles TextBox2.TextChanged CalcResult() End Sub Private Sub CalcResult() If IsNumeric(TextBox1.Text) AndAlso IsNumeric(TextBox2.Text) Then LabelResult.Text = (Double.Parse(TextBox1.Text) * _ Double.Parse(TextBox2.Text)).ToString Else LabelResult.Text = "" End If End Sub
14.4 Textfelder
607
Die eigentliche Validierung der Eingabe erfolgt nur beim Versuch, ein Textfeld zu verlassen. (Das Validating-Ereignis tritt auch dann auf, wenn der Anwender versucht, das Programm zu beenden!) Um die zweimalige Formulierung desselben Codes zu vermeiden, wurde die Validierung in die Prozedur ValidateTextBox ausgelagert. ' Valididierung der Eingabe durchführen Private Sub TextBox1_Validating(..., _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles TextBox1.Validating ValidateTextBox(TextBox1, e) End Sub Private Sub TextBox2_Validating(...) Handles TextBox2.Validating ValidateTextBox(TextBox2, e) End Sub Private Sub ValidateTextBox(ByVal txtbox As TextBox, _ ByVal e As System.ComponentModel.CancelEventArgs) If IsNumeric(txtbox.Text) = False Then txtbox.SelectAll() MsgBox("Eingabefehler! Geben Sie bitte eine Zahl ein.") e.Cancel = True End If End Sub
VERWEIS
Abbildung 14.11: Automatische Eingabekontrolle bei einer Texteingabe
In Abschnitt 14.10.5 wird dieses Beispiel in veränderter Form aufgegriffen. Dort wird ein ErrorProvider-Steuerelement verwendet, um die Eingabefehler anzuzeigen. Diese Programmvariante ist insofern benutzerfreundlicher, als das Programm auch bei einer Fehleingabe beendet werden kann.
608
14 Steuerelemente
14.4.4 RichTextBox Das RichTextBox-Steuerelement (kurz RTF-Feld) unterscheidet sich vom gewöhnlichen TextBox-Steuerelement dadurch, dass einzelne Textpassagen in unterschiedlichen Textformatierungen (Schriftart, Schriftgröße, Absatzformate etc.) dargestellt werden können (siehe Abbildung 14.12).
Abbildung 14.12: Verschiedene Textformate in einem RichTextBox-Steuerelement
Bevor auf die Programmierung eingegangen wird, ist eine kurze Erklärung notwendig, wie das RTF-Feld die Daten intern verwaltet. Das Steuerelement unterstützt eine Teilmenge des Rich Text Formats. Das ist ein Textformat, das alle Textformatinformationen ebenfalls in Textform ausdrückt. Die ersten Zeilen des in Abbildung 14.12 dargestellten Texts sehen als interner RTF-Code folgendermaßen aus. (Die Einrückungen habe ich durchgeführt, um den Text lesbarer zu gestalten.) {\rtf1\ansi\ansicpg1252\deff0\deflang1033 {\fonttbl{\f0\fswiss\fcharset0 Arial;} {\f1\fswiss\fprq2\fcharset0 Arial;} {\f2\fmodern\fprq1\fcharset0 Courier New;} {\f3\fnil\fcharset2 Symbol;}} {\colortbl ;\red0\green128\blue0;\red255\green0\blue0;} \viewkind4\uc1\pard\f0 \fs20 Normaler Text, \fs28 gro\'dfer Text\fs20 , \fs16 kleiner Text\fs20 , \i kursiver Text\i0 , \b fetter Text\b0 , \cf1\f1 gr\'fcner Text\cf0\f0 , \cf2\f1 roter Text\cf0\f0 . \par\par Sonderzeichen: \'e4\'f6\'fc\'df\'80\par ...
Das Rich Text Format wird auch von den meisten Textverarbeitungsprogrammen sowie von dem mit allen Windows-Versionen mitgelieferten Programm WordPad unterstützt.
14.4 Textfelder
609
Textzugriff Im Regelfall werden Sie mit RTF-Codes nichts zu tun haben. Sie greifen über die TextEigenschaft auf den normalen Text (ohne Formatinformationen) zu bzw. verwenden SelectedText, um den gerade markierten Text zu lesen oder zu verändern. Die Einstellung der Schriftart, der Farbe und der Textausrichtung erfolgt über Eigenschaften und Methoden wie SelectionFont, SelectionColor, SelectionAlignment etc. Für manche Anwendungen kann es nützlich sein, direkt auf die Formatcodes zuzugreifen. In diesem Fall verwenden Sie statt Text bzw. SelectedText die Eigenschaften RTF bzw. SelectedRTF, die den Text im RTF-spezifischem Format enthalten. Wenn Sie Formatinformationen direkt verändern möchten, ist allerdings eine ausgezeichnete Kenntnis der RTF-Codes und ihrer Bedeutung erforderlich. (Suchen Sie in der Online-Hilfe nach RTF specification!) Der Zugriff auf die aktuelle Cursorposition und den markierten Text erfolgt wie beim gewöhnlichen Textfeld durch die drei Eigenschaften SelectionStart, SelectionLength und SelectedText. Die Cursorposition ergibt sich aus SelectionStart+SelectionLength.
Formatierung von Text Die Formatierung von Text erfolgt generell in zwei Schritten. Zuerst muss der zu formatierende Text markiert werden. Das kann sowohl vom Anwender des Programms als auch per Programmcode (SelectionStart, SelectionLength) erfolgen. Anschließend können die Formatierungsattribute über diverse SelectionXxx-Eigenschaften ausgelesen werden. Beachten Sie, dass die Schriftart bzw. dessen Attribute nur geändert werden können, indem SelectionFont ein neues Font-Objekt zugewiesen wird. Damit wird die Schrift des gesamten markierten Texts einheitlich neu eingestellt. Umgekehrt sind die Verhältnisse nicht immer so eindeutig: Wenn der markierte Text in verschiedenen Schriftarten formatiert ist, dann liefert SelectionFont meist das Ergebnis Nothing, manchmal aber leider ein Font-Objekt mit irreführende Angaben, z.B. mit einer falschen Schriftgröße (meist 13)!
Text laden und speichern Mit LoadFile wird eine RTF-Datei aus einer Datei oder einem Stream geladen. Optional können Sie das gewünschte Textformat angeben: RichTextBoxStreamType.RichText
RTF-Text mit OLE-Objekten (Defaulteinstellung)
RichTextBoxStreamType.PlainText
ANSI-Text ohne Formatierung
RichTextBoxStreamType.UnicodePlainText
UTF-16-Text ohne Formatierung
RichTextBox1.LoadFile(("..\test-document.rtf")
Zum Speichern des Texts verwenden Sie die analoge Methode SaveFile. Per Default wird der Text als RTF-Datei gespeichert. Sie können das gewünschte Textformat in einem optionalen Parameter angeben, wobei zusätzlich zu den beiden obigen Einstellungen zwei weitere möglich sind:
610
14 Steuerelemente
RichTextBoxStreamType.RichNoOleObjs
RTF-Text ohne eingebettete OLE-Objekte
RichTextBoxStreamType.TextTextOleObjs
ANSI-Text ohne Formatierung, wobei versucht wird, OLE-Objekte in Textform darzustellen
Text suchen Mit Find können Sie innerhalb des Texts suchen. Die Methode liefert als Ergebnis die Position, an der das Suchmuster gefunden wurde. Optional können Sie auch ab einer bestimmten Position oder rückwärts suchen, nur ganze Wörter suchen etc.
VERWEIS
Text drucken Es gibt leider keine .NET-Methode, um den Inhalt einer RichTextBox auszudrucken. Falls am lokalen Rechner Microsoft Word installiert ist, können Sie via Automation den Inhalt des Steuerelements über die Zwischenablage dort einfügen und ausdrucken. Ein konkretes Beispiel finden Sie in Abschnitt 12.5.3.
14.5
Grafikfelder
Dieser Abschnitt beschreibt zwei vollkommen unterschiedliche Steuerelemente, die mit der Darstellung bzw. Verwaltung von Grafik zu tun haben. Das PictureBox-Feld dient dazu, eine Grafik in einem Steuerelement anzuzeigen. Das Steuerelement kann auch als Darstellungsobjekt für eigene Grafiken dienen, die in der Paint-Ereignisprozedur gezeichnet werden. Im Gegensatz dazu ist das ImageList-Steuerelement unsichtbar. Es dient als Container für mehrere Bitmaps gleicher Größe, die in anderen Steuerelementen dargestellt werden.
14.5.1 PictureBox Im PictureBox-Steuerelement können Sie vorhandene Grafikdateien dargestellen. Dazu werden im Eigenschaftsfenster die Eigenschaften Image und ImageAlign eingestellt. Wenn Sie Image per Programmcode verändern möchten, sieht die Vorgehensweise beispielsweise so aus: PictureBox1.Image = New Bitmap("dateiname.bmp")
Alternativ oder in Ergänzung zu dem durch Image definierten Hintergrund können Sie in der Paint-Ereignisprozedur Grafikmethoden ausführen und so eine eigene Grafik zeichnen. Die Ereignisprozedur wird jedes Mal aufgerufen, wenn Teile des PictureBox-Steuerelements neu zu zeichnen sind. Eine Menge Details zum Paint-Ereignis sowie viele Tipps und
14.5 Grafikfelder
611
HINWEIS
Beispiele zur Grafikprogrammierung finden Sie in Kapitel 16, weswegen hier auf Beispiele verzichtet wird. Die meisten in diesem Kapitel vorgestellten Steuerelemente kennen die ImageEigenschaft und das Paint-Ereignis, eignen sich also ebenfalls als Grafik-Container. Die Besonderheit des PictureBox-Steuerelements besteht gewissermaßen darin, dass es außer als Grafik-Container keine weitere Funktionen erfüllt.
14.5.2 ImageList Im ImageList-Steuerelement können Sie viele Bitmaps und Icons speichern. Die Bitmaps werden üblicherweise bereits während des Programmentwurfs geladen. Dazu öffnen Sie im Eigenschaftsfenster den Dialog IMAGE-AUFLISTUNGS-EDITOR (Eigenschaft Images). Mit ADD können Sie nun eine Bitmap nach der anderen einfügen. (Das Einfügen mehrerer Bitmaps gleichzeitig ist nicht möglich.)
HINWEIS
Beim Einfügen werden die Bitmaps auf eine einheitliche Größe (Eigenschaft ImageSize) und Farbtiefe (ColorDepth) skaliert. Es ist nicht möglich, Bitmaps unterschiedlicher Größe zu speichern. Per Default beträgt die Größe nur 16*16 Pixel und die Farbtiefe 8 Bit (also 256 Farben). Sie können im Eigenschaftsfenster natürlich andere Werte einstellen – die Qualität von einmal geladenen Bitmaps verbessert sich dadurch aber nicht mehr! Wenn Sie die Größe nachträglich ändern möchten, müssen Sie alle Bitmaps entfernen und neu laden ... Vergessen Sie also nicht, die Eigenschaften vor dem Einfügen der Bitmaps auf geeignete Werte zu stellen!
Per Code können die Bitmaps mit ImageList1.Images(n) angesprochen werden. n ist dabei eine Indexnummer. Es ist leider nicht möglich, den Bildern einen Namen zu geben, was den Zugriff per Code natürlich übersichtlicher machen würde. Ebenso wenig ist es möglich, Indexnummer und Bild zu verbinden. Wenn Sie bei einer Liste mit 20 Bildern das dritte enfernen, ändert sich bei allen weiteren Bildern die Indexnummer! Es ist nicht möglich, per Code ein vorhandenes Bild zu verändern. Sie können aber mit Images.Add und .Remove[At] Bilder einfügen und löschen. (Beachten Sie, dass sich beim
Löschen die Indexnummern aller Bilder ändern, die eine höhere Indexnummer als das gelöschte Bild haben.) Ein Beispiel für die dynamische Veränderung eines Bilds in einem ImageList-Steuerelement finden Sie in Abschnitt 15.7. Das ImageView-Steuerelement selbst ist unsichtbar. Das bedeutet, dass auch alle darin gespeicherten Bitmaps vorerst unsichtbar sind. Das Steuerelement wird meistens in Kombination mit anderen Steuerelementen eingesetzt. Hierfür kommen viele Steuerelemente in Frage, beispielsweise ListView, TreeView, TabControl und ToolBar. Die ImageList-Bitmaps werden dann in diesen Steuerelementen angezeigt. Zwei ausführliche Anwendungsbeispiele finden Sie in den Abschnitten 14.6.4 bis 14.6.7.
612
14 Steuerelemente
14.6
Listenfelder
Dieser Abschnitt beschreibt sechs Listenfelder, die sich in Aussehen, Anwendung und Programmierung deutlich unterscheiden. Das folgenden Hierarchiediagramm hilft bei der Einordnung der Steuerelemente innerhalb der Windows.Forms-Hierarchie. (Die Klassen der Listenfelder sind fett hervorgehoben.) Klassenhierarchie im System.Windows.Forms-Namensraum ...
VERWEIS
└─ Control ├─ DataGrid ├─ ListControl │ ├─ ListBox │ │ └─ CheckedListBox │ └─ ComboBox ├─ ListView └─ TreeView
Basisklasse für alle Steuerelemente Tabellenfeld Basisklasse für ListBox und ComboBox gewöhnliches Listenfeld Listenfeld mit Auswahlkästchen Listenfeld zum Ausklappen mehrspaltiges Listenfeld (Windows-95-Optik) hierarchisches Listenfeld
Neben den hier vorgestellten Listenfeldern gibt es einige weitere Steuerelemente, die eigentlich in diese Kategorie passen würden, aber an anderen Stellen in diesem Kapitel vorgestellt werden: • Das DateTimePicker-Steuerelement (siehe Abschnitt 14.7.2) sieht wie eine ComboBox aus, ermöglicht aber die komfortable Auswahl eines Datums oder einer Uhrzeit. • Das DomainUpDown-Steuerelement (siehe Abschnitt 14.8.5) hat ebenfalls große Ähnlichkeiten mit der ComboBox, ist aber mit zwei Pfeilbuttons zur Auswahl des Listenelements ausgestattet.
14.6.1 ListBox Aussehen MultiColumn gibt an, ob die Listeneinträge in einer einzigen Spalte untereinander oder in
mehreren Spalten nebeneinander angezeigt werden sollen. Im ersten Fall (per Default) wird das Steuerelement mit einem vertikalen, sonst mit einem horizontalen Schiebebalken ausgestattet (siehe Abbildung 14.13). Außerdem können Sie nun die gewünschte Spaltenbreite (in Pixel) mit ColumnWidth einstellen. Unabhängig von der Einstellung von MultiColumn ist es aber nicht möglich, mehrspaltige Listeneinträge anzuzeigen (also z.B. ein Listenelement, das aus den Spalten Name und Adresse besteht). Für diesen Zweck müssen Sie ein ListView-Steuerelement einsetzen.
14.6 Listenfelder
613
Wenn Sie in einem Listenfeld grafische Einträge (und nicht nur einfache Texte) darstellen möchten, müssen Sie die DrawMode-Eigenschaft ändern und DrawItem- und MeasureItemEreignisprozeduren zur Verfügung stellen. Die Vorgehensweise wird etwas weiter unten erläutert.
Abbildung 14.13: Vier ListBox-Steuerelemente
Die Eigenschaft TopIndex gibt die Indexnummer des ersten sichtbaren Eintrags im Listenfeld an. Durch die Veränderung dieser Eigenschaft können Sie das Listenfeld per Code scrollen.
Verwaltung der Listenelemente Die Initialisierung des Steuerelements kann wahlweise während des Programmentwurfs und per Code erfolgen. Während des Programmentwurfs klicken Sie im Eigenschaftsfenster die Eigenschaft Items an und gelangen so in ein Textfenster, in dem Sie die Texte für die Listeneinträge komfortabel eingeben können. Bei der Initialisierung per Code gibt es wiederum zwei Varianten: Sie können die Listeneinträge entweder direkt über die Items-Eigenschaft verändern oder mit DataSource eine Datenquelle angeben. Hier wird zuerst die Items-Variante beschrieben. Informationen zur DataSource-Variante folgen etwas weiter unten. Bei der Items-Variante können Sie Listeneinträge wahlweise mit Items.Add oder Items.Insert einfügen. Bei Insert müssen Sie den Ort (Index) angeben, an dem in der Liste das Element eingefügt werden soll. Um viele Einträge möglichst effizient einzufügen, sollten Sie Items.AddRange verwenden oder die etwas weiter unten beschriebenen Methoden Begin- und EndUpdate einsetzen. Items.Clear löscht eine eventuell bereits vorhandene Liste.
614
14 Steuerelemente
Private Sub Form1_Load(...) Handles MyBase.Load Dim i As Integer ListBox1.Items.Clear() For i = 1 To 10 ListBox1.Items.Add("Eintrag " + i.ToString) Next End Sub
Die Einfügemethoden akzeptieren jeden beliebigen Objekttyp. Im Listenfeld wird die Zeichenkette des Objekts angezeigt (ToString-Methode). Beachten Sie, dass Items(n) ein Objekt des allgemeinen Typs Object zurückgibt. Wenn Sie wie im obigen Beispiel einfache Zeichenketten als Listenelemente verwenden, liefert CStr( Items(n)) das Listenelement als String-Objekt. Wenn Sie dagegen Objekte einer beliebigen Klasse myOwnClass gespeichert haben, müssen Sie die Konstruktion CType(Items(n), myOwnClass) verwenden, um eine Umwandlung von Object zu myOwnClass durchzuführen. Die Eigenschaft Sorted gibt an, ob die Listenelemente automatisch alphabetisch sortiert werden sollen. (Wenn Sie eine andere Sortierordnung wünschen, können Sie das über ein ArrayList-Objekt veranlassen, das Sie mit ArrayList.Adapter(listboxobj.Items) erzeugen. Die Vorgehensweise ist in Abschnitt 9.3.4 beschrieben.) Intern erfolgt die Verwaltung der Listenelemente durch ein Objekt der Klasse ListBox.ObjectCollection, das über die bereits erwähnte Eigenschaft Items angesprochen wird. Die Klasse ListBox.ObjectCollection realisiert die Schnittstellen ICollection, IEnumerable und IList (siehe auch Kapitel 9). Auf einzelne Listenelemente kann mit Items(n) zugegriffen werden.
Effiziente Items-Initialisierung Das Einfügen sehr vieler Einträge in die Liste ist ein relativ aufwendiger Prozess, insbesondere wenn Sorted=True gilt. Um derartige Operationen möglichst effizient auszuführen, sollten Sie Sorted vorübergehend auf False stellen und vorher BeginUpdate und anschließend EndUpdate ausführen. Sie vermeiden dadurch unnötige Bildschirmaktualisierungen. (Eventuell sollten Sie mit Me.Cursor = Cursors.WaitCursor die Maus vorübergehend als Sanduhr darstellen, um dem Anwender ein Feedback zu geben, dass gerade etwas geschieht.) Die folgenden Zeilen fügen alle Dateinamen aus dem Windows-System-Verzeichnis möglichst effizient in eine ListBox ein. Dim di As New IO.DirectoryInfo(Environment.SystemDirectory) Dim fi As IO.FileInfo ListBox1.Sorted = False ListBox1.BeginUpdate() ListBox1.Items.Clear() For Each fi In di.GetFiles() ListBox1.Items.Add(fi.Name) Next ListBox1.Sorted = True ListBox1.EndUpdate()
14.6 Listenfelder
615
DataSource-Initialisierung Wenn Sie sich nicht via Items um jedes einzelne Listenelement kümmern möchten, können Sie der Liste mit DataSource ein Objekt zuweisen, das die IList-Schnittstelle implementiert. Diese Vorgehensweise wird üblicherweise in Datenbankanwendungen gewählt, eignet sich aber auch zur Darstellung lokaler Objekte, die sich beispielsweise in einer ArrayList befinden. Damit das Listenfeld weiß, welche Daten es anzeigen soll, muss mit DisplayMember der Name einer Eigenschaft der ArrayList-Objekte angegeben werden. Optional können Sie mit ValueMember eine zweite Eigenschaft angeben: Wenn Sie dann einen Listeneintrag auswählen, enthält ListBox1.SelectedValue den entsprechenden Wert. Am einfachsten ist das alles anhand eines Beispiels zu verstehen (siehe Abbildung 14.14). In Form1_Load wird das ArrayList-Objekt mybooks mit mehreren Objekten der selbst definierten Klasse Book initialisiert. Anschließend wird mybooks als DataSource angegeben. Im Listenfeld sollen die Buchtitel angezeigt werden – daher wird DisplayMember auf "title" gestellt. Zur Weiterverarbeitung der Listenauswahl ist aber die ISBN praktischer – daher wird ValueMember auf "isbn" gestellt. title und isbn sind Eigenschaften der Book-Klasse (siehe unten). Beachten Sie, dass die ListBox-Eigenschaft Sorted bei der DataSource-Variante nicht verwendet werden kann. Sie können zum Sortieren die Sort-Methode der ArrayList verwenden (müssen dann aber ein IComparer-Objekt angeben, das die Objektvergleiche durchführt).
Abbildung 14.14: Eine ListBox mit Daten aus einem ArrayList-Objekt
' Beispiel steuerelemente\listbox-arraylist Dim mybooks As New ArrayList() Dim initialized As Boolean = False Private Sub Form1_Load(...) Handles MyBase.Load mybooks.Add(New Book("Linux", "Kofler Michael", "3827318548")) mybooks.Add(New Book("MySQL", "Kofler Michael", "3827317622")) mybooks.Add(New Book("VB.NET", "Kofler Michael", "382731982X")) ListBox1.DataSource = mybooks ListBox1.DisplayMember = "title" ListBox1.ValueMember = "isbn" initialized = True End Sub
616
14 Steuerelemente
Nach der Auswahl eines Listenelements wird der Inhalt des Book-Objekts in einer MsgBox angezeigt. Private Sub ListBox1_SelectedIndexChanged(...) Handles _ ListBox1.SelectedIndexChanged Dim bk As Book If initialized = False Then Exit Sub If ListBox1.SelectedItem Is Nothing Then Exit Sub bk = CType(ListBox1.SelectedItem, Book) MsgBox("Titel: " + bk.title + " Autoren: " + bk.authors + _ " ISBN " + bk.isbn) End Sub
Die zugrunde liegende Book-Klasse sieht folgendermaßen aus und bedarf keiner weiteren Erklärung. Private Class Book ' interne Daten Private isbn_ As String Private title_ As String Private authors_ As String ' New-Konstruktur Sub New(ByVal t As String, ByVal a As String, ByVal i As String) isbn_ = i title_ = t authors_ = a End Sub ' Eigenschaften Property title() As String Get Return title_ End Get Set(ByVal Value As String) title_ = Value End Set End Property Property authors() As String Property isbn() As String End Class
'wie bei title() 'wie bei title()
14.6 Listenfelder
617
Listenauswahl Die Eigenschaft SelectionMode gibt an, wie der Anwender einen oder mehrere Listeneinträge auswählen kann. Die Defaulteinstellung lautet One – damit kann nur ein einziger Listeneintrag ausgewählt werden. Die beiden anderen möglichen Einstellungen lauten MultiSimple und MultiExtended. (Es handelt sich dabei jeweils um Elemente der SelectionModeAufzählung.) Diese beiden Einstellungen erlauben eine Mehrfachauswahl: •
Bei MultiSimple kann der Anwender einfach die gewünschten Elemente per Mausklick auswählen. Ein nochmaliger Klick deaktiviert das Element wieder.
•
Bei MultiExtended funktioniert die Mehrfachauswahl nur bei gleichzeitigem Drücken der Strg-Taste. Dafür können ganze Bereiche der Liste mit Shift plus Maustaste ausgewählt werden.
Listenauswahl auswerten Bei jeder Veränderung der Listenauswahl tritt ein SelectedIndexChanged-Ereignis auf. (Ein TextChanged-Ereignis tritt dagegen nicht auf, obwohl sich die Text-Eigenschaft sehr wohl ändert!) An die SelectedIndexChanged-Ereignisprozedur werden zwar keine Parameter übergeben, aber dafür gibt es zahlreiche ListBox-Eigenschaften, die bei der Auswertung der Listenauswahl helfen. Bei der Auswertung müssen zwei Fälle unterschieden werden: Darf nur ein einziges Listeelement ausgewählt werden (SelectionMode=One) oder ist auch eine Mehrfachauswahl möglich? Die für die beiden Varianten zutreffenden Eigenschaften sind in den folgenden Syntaxboxen kurz beschrieben.
ListBox-Steuerelement bei einer einfachen Auswahl auswerten SelectedIndex
gibt die Indexnummer des ausgewählten Listeneintrags an. Wenn kein Element ausgewählt ist, enthält SelectedIndex den Wert -1.
SelectedItem
verweist auf das Objekt, das dem ausgewählten Listeneintrag entspricht. Beachten Sie, dass das Ergebnis den Typ Object hat und normalerweise mit CType in den tatsächlichen Datentyp umgewandelt werden muss. Wenn kein Element ausgewählt ist, enthält SelectedItem den Wert Nothing.
Text
enthält die Zeichenkette des ausgewählten Listeneintrags. Wenn kein Element ausgewählt ist, enthält Text eine leere Zeichenkette.
618
14 Steuerelemente
ListBox-Steuerelement bei einer Mehrfachauswahl auswerten GetSelected(n)
stellt fest, ob das n-te Listenelement ausgewählt ist.
SelectedIndices
verweist auf ein Objekt der Klasse ListBox.SelectedIndexCollection. Diese Klasse realisiert die Schnittstellen ICollection, IEnumerable und IList. Das Objekt enthält die Indizes aller ausgewählten Listeneinträge. SelectedIndices(n) liefert den Index des n-ten ausgewählten Listeneintrags. SelectedIndices.Count liefert die Anzahl der ausgewählten Listeneinträge. SelectedIndices.Contains(n) testet, ob der Listeneintrag n ausgewählt wurde.
SelectedItems
verweist auf ein Objekt der Klasse ListBox.SelectedObjectCollection, die sich durch dieselben Eigenschaften wie SelectedIndexCollection auszeichnet. Allerdings enthält das Objekt nicht die Indizes der Listeneinträge, sondern die eigentlichen Objekte (die Sie zur Bearbeitung mit CType in den tatsächlichen Datentyp umwandeln sollten).
Bei der Auswertung der ausgewählten Einträge sollten Sie einige Dinge beachten: •
Es kann immer auch vorkommen, dass gar kein Listeneintrag ausgewählt ist! Sie erkennen diesen Zustand durch die Auswertung von SelectedXxx.Count. Dieser Fall tritt insbesondere unmittelbar nach dem Programmstart ein. Gegebenenfalls erreichen Sie mit ListBox1.SelectedIndex=0 in Form1_Load, dass der erste Eintrag automatisch ausgewählt wird.
•
Wenn Sie die ausgewählten Listeneinträge entfernen möchten, müssen Sie die Schleife über SelectedIndices mit negativer Schrittweite formulieren, weil sonst die SelectedXxxCollection durcheinander kommt. (Eine For-Each-Schleife ist für diesen Zweck nicht geeignet. Ein Beispiel für die richtige Formulierung einer For-Next-Step-1-Schleife folgt etwas weiter unten.)
•
Der Zugriff auf SelectedIndices bzw. -Items in MouseXxx-Ereignisprozeduren (z.B. MouseDown) sollte nach Möglichkeit vermieden werden. Es ist dort nicht immer klar, ob ein gerade angeklickter Eintrag bereits in die Aufzählung der ausgewählten Listeneinträge aufgenommen wurde oder nicht. (Führen Sie die Auswertung nach Möglichkeit in der SelectedIndexChanged-Ereignisprozedur durch oder in Prozeduren, die von den Mausereignissen des Listenfelds vollkommen losgelöst sind.) Ebenso traten massive Probleme mit diesen beiden Eigenschaften auf, nachdem ich in MouseDown- oder MouseMove-Ereignisprozeduren eine Drag&Drop-Operation auslöste
(siehe auch Abschnitt 15.12.4). Irgendwie brachte das die innere Logik zur Verwaltung der SelectedIndices bzw. SelectedItems-Eigenschaften durcheinander.
14.6 Listenfelder
619
Wenn Sie per Code den n-ten Listeneintrag (de)selektieren möchten, können Sie dazu die Methode SetSelected(n, True/False) verwenden. Um alle Einträge zu deselektieren, führen Sie ClearSelected aus. Anschließend ist kein Listenelement mehr ausgewählt, SelectedIndex enthält den Wert -1.
Beispiel zur Auswertung einer Mehrfachauswahl Das folgende Beispielprogramm (siehe Abbildung 14.15) fügt alle bekannten Farben in eine ListBox ein. Anschließend können Sie mehrere Einträge auswählen und per Buttonklick in das darunter liegende Textfeld kopieren oder aus der Liste entfernen.
Abbildung 14.15: Mehrfachauswahl auswerten
' Beispiel steuerelemente\listbox-multiselect ' Listenfeld initialisieren Private Sub Form1_Load(...) Handles MyBase.Load Dim all As String(), s As String Dim kc As KnownColor all = System.Enum.GetNames(kc.GetType()) For Each s In all ListBox1.Items.Add(s) Next End Sub ' ausgewählte Listeneinträge kopieren Private Sub Button1_Click(...) Handles Button1.Click Dim o As Object TextBox1.Clear() For Each o In ListBox1.SelectedItems TextBox1.AppendText(o.ToString + " ") Next End Sub
620
14 Steuerelemente
' ausgewählte Listeneinträge löschen Private Sub Button2_Click(...) Handles Button2.Click Dim i, n As Integer TextBox1.Clear() If ListBox1.SelectedIndices.Count > 0 Then For i = ListBox1.SelectedIndices.Count - 1 To 0 Step -1 n = ListBox1.SelectedIndices(i) ListBox1.Items.Remove(ListBox1.Items(n)) Next End If End Sub
Listenelemente selbst zeichnen (owner-drawn ListBox) Normalerweise kümmert sich das ListBox-Steuerelement selbst um die Darstellung der Listenelemente. Allerdings kann auf diese Weise pro Listenelement nur ein einfacher Text mit einer einheitlichen Schrift angezeigt werden. Wenn Sie eine grafische Gestaltung der Listeneinträge realisieren möchten (z.B. Darstellung von Bitmaps, Verwendung unterschiedlicher Schriftarten, Farben etc.), müssen Sie die Eigenschaft DrawMode auf OwnerDrawFixed (gleichbleibende Höhe der Listenelemente, Eigenschaft ItemHeight) oder OwnerDrawVariable (variable Höhe) setzen. Allerdings müssen Sie jetzt Prozeduren für das Ereignis DrawItem und gegebenenfalls auch für MeasureItem schreiben. Die DrawItem-Ereignisprozedur dient erwartungsgemäß zur Darstellung eines einzelnen Listeneintrags, wobei ausgewählte Einträge invers (oder auf eine andere Weise hervorgehoben) gezeichnet werden müssen. An die Ereignisprozedur werden mit dem Parameter (Klasse DrawItemEventArgs) alle zum Zeichnen erforderlichen Daten übergeben. Back- und ForeColor berücksichtigen bereits den Zustand des Listeneintrags, d.h., die Farben sind automatisch invertiert, wenn der Listeneintrag ausgewählt ist. Den Hintergrund des Steuerelements können Sie komfortabel auch mit der Methode e.DrawBackground zeichnen. Eigenschaften der Klasse DrawItemEventArgs e.Graphics
Objekt zur Anwendung der Grafikmethoden
e.Bounds
Zeichenbereich (als Rectangle-Objekt)
e.BackColor
Hintergrundfarbe
e.ForeColor
Vordergrundfarbe
e.Font
Defaultschriftart für das Steuerelement
e.Index
Indexnummer des Listeneintrags
e.State
Zustand des Listeneintrags (z.B. DrawItemState.Selected, wenn der Eintrag ausgewählt ist)
Die MeasureItem-Ereignisprozedur wird nur aufgerufen, wenn DrawMode = OwnerDrawVariable gilt. (Bei DrawMode=OwnerDrawFixed wird die Höhe des Listeneintrags aus der Eigenschaft ListBox.ItemHeight entnommen.)
14.6 Listenfelder
621
Die MeasureItem-Prozedur wird für jedes Listenelement einmal aufgerufen. In der Prozedur müssen Sie in den Parametern e.ItemHeight und ItemWidth angeben, wir groß der Platzbedarf für das Listenelement ist. Diese Daten werden bis zum Programmende gespeichert, d.h., die ListBox geht davon aus, dass sich der Platzbedarf pro Listenelement nach dem Programmstart nicht mehr ändert. Sie können aber selbstverständlich die Liste erweitern (ListBox.Items.Add etc.) – dann wird die MeasureItem-Ereignisprozedur automatisch für das neue Listenelement aufgerufen. An die Ereignisprozedur wird der Parameter e der Klasse MeasureItemEventArgs übergeben. Die folgende Tabelle fasst die Eigenschaften dieser Klasse zusammen. Eigenschaften der Klasse MeasureItemEventArgs e.Graphics
Objekt zur Anwendung der Grafikmethoden
e.Index
Indexnummer des Listeneintrags
e.ItemWidth
die Breite des Listeneintrags (Rückgabewert)
e.ItemHeight
die Höhe des Listeneintrags
Beispiel Das Beispielprogramm demonstriert die Initialisierung und Verwaltung der in Abbildung 14.13 dargestellten ListBox-Steuerelemente. ListBox1 und -2 werden in der Entwicklungsumgebung initialisiert. Bei ListBox3 und -4 kümmert sich Form1_Load darum: ListBox3 wird mit den Namen der am Rechner verfügbaren Schriftarten initialisiert, ListBox4 mit Objekten der Klasse myownClass. (Diese Beispielklasse soll lediglich demonstrieren, wie Sie beliebige Objekte anstatt einfacher Zeichenketten in einer ListBox verwenden können.) ' Beispiel steuerelemente\listboxes Public Class myOwnClass Inherits Object Public x As Integer Public y As Integer Public z As Integer Public Sub New(ByVal x As Integer, ByVal y As Integer, _ ByVal z As Integer) Me.x = x Me.y = y Me.z = z End Sub Public Overrides Function ToString() As String Return x.ToString + "-" + y.ToString + "-" + z.ToString End Function End Class
622
14 Steuerelemente
Public Class Form1 ... Private Sub Form1_Load(...) Handles MyBase.Load Dim i As Integer Dim myobj As myOwnClass Dim ff As FontFamily ' ListBox3 initialisieren For Each ff In FontFamily.Families ListBox3.Items.Add(ff.Name) ff.Dispose() Next ' ListBox4 initialisieren For i = 1 To 30 myobj = New myOwnClass(i, 2 * i, 3 * i) ListBox4.Items.Add(myobj) Next End Sub
Jedes Mal, wenn in Form1_Load die Add-Methode für ListBox3 ausgeführt wird, kommt es automatisch zum Aufruf der MeasureItem-Prozedur. Dort wird ein Font-Objekt erzeugt, das dem Namen des Listeneintrags entspricht. Anschließend wird mit MeasureString berechnet, wie groß der Platzbedarf zur Darstellung der Zeichenkette ist. Dabei wird die Schriftgröße der Defaultschriftart des Steuerelements berücksichtigt. Die Try-Catch-Konstruktion ist für die Fälle vorgesehen, bei denen eine Schriftart im Defaultstil FontStyle.Regular nicht verfügbar ist. (Auf meinem Rechner gab es z.B. bei der Schriftart Monotype Corsiva Probleme, die nur kursiv zur Verfügung steht.) Private Sub ListBox3_MeasureItem(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MeasureItemEventArgs) _ Handles ListBox3.MeasureItem Dim fnt As Font Dim itemvalue As String = ListBox3.Items(e.Index).ToString Dim sizf As SizeF Try fnt = New Font(itemvalue, ListBox3.Font.SizeInPoints) Catch fnt = New Font("Arial", ListBox3.Font.SizeInPoints) End Try sizf = e.Graphics.MeasureString(itemvalue, fnt) e.ItemHeight = CInt(sizf.Height) e.ItemWidth = CInt(sizf.Width) fnt.Dispose() End Sub
14.6 Listenfelder
623
Die DrawItem-Ereignisprozedur ähnelt der MeasureItem-Prozedur. Der Unterschied besteht darin, dass der Listeneintrag diesmal tatsächlich mit DrawString gezeichnet wird. (Vorher muss mit DrawBackground der Hintergrund neu gezeichnet werden. Anders als in PaintEreignisprozeduren geschieht das nicht automatisch.) Private Sub ListBox3_DrawItem(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DrawItemEventArgs) _ Handles ListBox3.DrawItem Dim fnt As Font Dim itemvalue As String = ListBox3.Items(e.Index).ToString Dim br As Brush br = New SolidBrush(e.ForeColor) Try fnt = New Font(itemvalue, e.Font.SizeInPoints) Catch fnt = New Font("Arial", e.Font.SizeInPoints) End Try e.DrawBackground() e.Graphics.DrawString(itemvalue, fnt, br, _ RectangleF.op_Implicit(e.Bounds)) br.Dispose() fnt.Dispose() End Sub
Mit OK (Button1) beendet der Anwender das Programm. Vorher werden in einer MsgBox alle ausgewählten Objekte angezeigt. Private Sub Button1_Click(...) Handles Button1.Click Dim obj As Object Dim s As String s = "ListBox1: " + ListBox1.Text + vbCrLf s += "ListBox2: " + ListBox2.Text + vbCrLf s += "ListBox3: " + ListBox3.Text + vbCrLf s += "ListBox4: " For Each obj In ListBox4.SelectedItems s += CType(obj, myOwnClass).ToString + " " Next s += vbCrLf MsgBox("Die folgenden Einträge wurden ausgewählt:" + vbCrLf + s) Me.Close() End Sub
VERWEIS
624
14 Steuerelemente
Das Zeichnen von Listeneinträgen setzt voraus, dass Sie mit den diversen Grafikmethoden der Klasse Graphics umgehen können. Diese Methoden – z.B. DrawString, MeasureString etc. – werden in Kapitel 16 beschrieben. Dort finden Sie auch ausführliche Informationen zum Umgang mit Schriftarten (Klassen Font, FontFamily etc.). Ein weiteres Beispiel für die Programmierung eines ListBox-Steuerelements mit selbst gezeichneten Elementen finden Sie hier: http://www.gotdotnet.com/quickstart/howto/doc/WinForms/WinFormsOwnerDrawListBox.aspx
14.6.2 CheckedListBox Das CheckedListBox-Steuerelement ist von der gewöhnlichen ListBox abgeleitet. Der wesentliche Unterschied besteht darin, dass vor jedem Listeneintrag ein Auswahlkästchen angezeigt wird. Damit ist eine Mehrfachauswahl visuell besser nachvollziehbar als beim gewöhnlichen Listenfeld (siehe Abbildung 14.16). Wenn die Auswahlkästchen in einer 3DOptik dargestellt werden sollen, setzen Sie ThreeDCheckBoxes auf True.
Abbildung 14.16: Das CheckedListBox-Steuerelement
Im Defaultzustand ist die Bedienung des Steuerelements etwas irritierend, weil ein Auswahlkästchen erst dann verändert werden kann, wenn vorher das Listenelement ausgewählt wurde. (Das bedeutet, dass das Listenelement zweimal angeklickt werden muss!) Abhilfe schafft CheckOnClick=True. (Warum die Eigenschaft nicht von Anfang an diesen Zustand hat, ist unerklärlich.) Die Auswertung der ausgewählten Listenelemente erfolgt über die Eigenschaften CheckedIndices und CheckedItems, die auf Objekte der Klassen CheckedListBox.CheckedIndexCollection und CheckedListBox.CheckedItemCollection verweisen. Die beiden Klassen haben dieselben Eigenschaften wie die im vorigen Abschnitt behandelten ListBox.SelectedXxxCollection-Klassen. Im Vergleich zur gewöhnlichen ListBox gibt es zwei Einschränkungen: Die einzig zulässige Einstellung für SelectionMode lautet One. (Eine Mehrfachauswahl wäre nicht sinnvoll, weil diese nun ja durch die Auswahlkästchen erzielt werden kann.) Darüber hinaus steht die Eigenschaft DrawMode nicht zur Verfügung, d.h., es ist unmöglich, die Listenelemente selbst zu zeichnen.
14.6 Listenfelder
625
14.6.3 ComboBox Das ComboBox-Steuerelement entspricht in seinen Eigenschaften und bei der Programmierung weitgehend der ListBox, weswegen hier nur die Unterschiede beschrieben werden. Deren wichtigster besteht in der Darstellungsform: Während bei einer ListBox immer eine (vorgegebene) Anzahl von Listenelementen sichtbar ist, ist bei der ComboBox normalerweise nur eine einzelne Zeile mit dem gerade ausgewählten Eintrag sichtbar. Erst durch Anklicken der Pfeilbuttons wird die Auswahlliste sichtbar. Eine prinzipbedingte Einschränkung gegenüber der ListBox besteht darin, dass eine Mehrfachauswahl unmöglich ist.
Abbildung 14.17: Drei Varianten einer ComboBox
Aussehen Die ComboBox steht in drei Varianten zur Verfügung, die mit der Eigenschaft DropDownStyle eingestellt werden: •
DropDownStyle=DropDown (Defaulteinstellung): Bei dieser Einstellung wird der Name des Steuerelements klar: Es verhält sich nämlich wie eine Kombination aus einem Textund einem Listenfeld. Der Anwender kann also einen Eintrag aus der Liste auswählen, er kann aber auch über die Tastatur einen beliebigen anderen Text eingeben.
Beim Programmstart wird kein Listenelement ausgewählt. Sie sollten daher entweder die Text-Eigenschaft auf "" stellen oder beim Programmstart die Anweisung ComboBox1.SelectedIndex = 0 ausführen, um das erste Listenelement auszuwählen. •
DropDownStyle=DropDownList: Bei dieser Einstellung kann nur eines der vorgegebenen
Listenelemente ausgewählt werden. Die Auswahl kann nun auch über die Tastatur erfolgen. (Die Eingabe von A bewirkt, dass der erste Listeneintrag mit dem Anfangsbuchstaben a ausgewählt wird.) Beim Programmstart wird automatisch das erste Listenelement ausgewählt. •
DropDownStyle=Simple: Funktionell verhält sich die ComboBox wie bei der DropDown-Ein-
stellung, optisch sieht das Steuerelement aber wie ein normales Textfeld aus. Insbesondere fehlt der Pfeilbutton zum Ausklappen der Liste. (Die Listenelemente müssen mit den Cursortasten ausgewählt werden.)
626
14 Steuerelemente
Das Steuerelement kann in dieser Form nicht intuitiv bedient werden, weswegen die Simple-Einstellung in der Praxis wohl nie vorkommen wird.
HINWEIS
Per Default ist die ausgeklappte Liste maximal acht Einträge lang und so breit wie das zusammengeklappte Steuerelement. Mit den Eigenschaften MaxDropDownItems (Einheit Zeilen) und DropDownWidth (Pixel) können Sie die Maße aber vergrößern, was sich vor allem bei umfangreichen Listen empfiehlt. Ob die Liste gerade ausgeklappt ist oder nicht, können Sie mit der Eigenschaft DroppedDown feststellen und verändern. Das ComboBox-Steuerelement hat einen (offiziell bestätigten) Bug: Es ist nicht möglich, im Textbereich des Steuerelements mehrere Zeichen mit der Maus zu markieren. Die Markierung kann nur per Tastatur (Shift + Cursor) erfolgen. Es ist zu erwarten, dass dieser Fehler in einem zukünftigen Service Pack behoben wird.
Auswertung der Auswahl bzw. Texteingabe Wenn der Anwender einen Listeneintrag auswählt, treten die Ereignisse TextChanged und SelectedIndexChanged auf (in dieser Reihenfolge). Wenn dagegen bei einer ComboBox des Typs DropDown über die Tastatur ein neuer Text eingegeben wird, tritt nur das SelectedTextChanged-Ereignis auf. Zur Auswertung des gewählten Listeneintrags verwenden Sie die folgenden Eigenschaften: ComboBox-Steuerelement auswerten SelectedIndex
gibt die Indexnummer des ausgewählten Listeneintrags an. Wenn kein Element ausgewählt ist, enthält SelectedIndex den Wert -1. Vorsicht: SelectedIndex ändert sich bei einer Texteingabe nicht (ComboBox-Variante DropDown)! Daher kann es passieren, dass die Eigenschaft Text eine ganz andere Zeichenkette enthält als das zuletzt ausgewählte Listenelement.
SelectedItem
verweist auf das Objekt, das dem ausgewählten Listeneintrag entspricht. Wenn kein Element ausgewählt ist, enthält SelectedItem den Wert Nothing. Vorsicht: Wie SelectedIndex ändert sich auch SelectedItem nicht, wenn ein Text per Tastatur eingegeben wird. SelectedItem verweist dann weiterhin auf das Objekt des zuletzt ausgewählten Listeneintrags, während Text schon einen ganz anderen Text enthält.
Text
enthält die Zeichenkette des ausgewählten Listeneintrags bzw. den eingegebenen Text.
14.6 Listenfelder
627
Wie aus der obigen Beschreibung bereits hervorgeht, wird die Auswertung dadurch erschwert, dass die beiden Selected-Eigenschaften nicht synchron mit Text sind. Um festzustellen, ob es sich bei der Zeichenkette in Text um eine eigene Eingabe oder um einen ausgewählten Listeneintrag handelt, müssen Sie den folgenden Vergleich durchführen: If ComboBox1.Text = _ CType(ComboBox1.SelectedItem, objtype).ToString() Then ...
Automatische Vervollständigung (Auto-Complete) Wenn Sie im Adressfeld des Internet Explorers einige Buchstaben eingeben, erscheint automatisch eine Dropdown-Liste, wobei der erste passende Eintrag gleich markiert ist. Ein weiteres praktisches Merkmal der IE-ComboBox besteht darin, dass die Größe der Dropdown-Liste mit der Maus eingestellt werden kann. Um es kurz zu machen: Die hier beschriebene .NET-ComboBox kennt leider keine dieser beiden Merkmale in derselben Form wie bei der IE-ComboBox. Es gibt aber immerhin eine eingeschränkte Auto-Complete-Funktion: Wenn die Dropdown-Liste sichtbar ist, wird darin der erste passende Listeneintrag zur Eingabe in der Textbox angezeigt. Mit den Cursortasten können Sie dann direkt ein Listenelement auswählen. Die hier abgedruckte TextChanged-Ereignisprozedur bewirkt, dass die Drop-Down-Liste bei jeder Eingabe automatisch eingeblendet wird. Die initialized-Abfrage verhindert, dass das schon während der Initialisierung passiert, was zumindest unter Windows 2000 zu einem Darstellungsfehler führt. In der tmp-Variablen wird der Inhalt von Text zwischengespeichert, weil durch DropDown=True der erste passende Listeneintrag gleich in das Textfeld übernommen wird. Das stört aber die weitere Eingabe.
Abbildung 14.18: AutoComplete-Funktion für eine ComboBox
' Beispiel steuerelemente\combobox-autocomplete Dim initialiazed As Boolean = False Private Sub Form1_Load(...) Handles MyBase.Load ' ComboBox mit Daten füllen ' ... ComboBox1.Text = "" initialiazed = True End Sub
628
14 Steuerelemente
HINWEIS
Private Sub ComboBox1_TextChanged(...) Handles ComboBox1.TextChanged Dim tmp As String If initialiazed AndAlso Not (ComboBox1.DroppedDown = True) Then tmp = ComboBox1.Text ComboBox1.DroppedDown = True ComboBox1.Text = tmp ComboBox1.SelectionStart = Len(tmp) End If End Sub
Die hier vorgestellte Lösung ist einfach, aber leider nicht perfekt. Ein großer Mangel besteht darin, dass Sie keinen Einfluss auf die Suche haben. (Wenn Sie in der IEComboBox den Begriff goo eingeben, erscheint in der Dropdown-Liste z.B. die dazugehörende Adresse http://www.google.com.) Die Programmierung einer perfekteren Lösung scheitert momentan unter anderem darin, dass in der KeyPress-Ereignisprozedur e.Handled=True nicht funktioniert, Sie daher Tastatureingaben also nicht per Code unterdrücken und selbst verarbeiten können. (Das ist ein bekannter Bug der ComboBox, der vielleicht in einem künftigen Service Pack korrigiert wird.)
14.6.4 ListView Sowohl das ListBox- als auch das ListView-Steuerelement dienen dazu, auswählbare Listen darzustellen. Trotz dieser offensichtlichen Wesensähnlichkeit trennen die beiden Steuerelemente Welten – sowohl im Aussehen als auch bei der Programmierung. ListView ermöglicht die Darstellung mehrspaltiger Listen, wobei jedes Listenelement mit einer kleinen und einer großen Bitmap ausgestattet werden kann. Die Liste kann dann in den vier vom Windows-Explorer vertrauten Darstellungsformen angezeigt werden (d.h. als einfache Liste mit kleinen oder großen Bitmaps, als mehrspaltige Liste sowie als Detailliste – siehe auch Abbildung 14.19).
Klassenüberblick Die hohe Flexibilität bei der Darstellung der Liste wird durch ein komplexes Klassenmodell erkauft, das die Intialisierung und Verwaltung der Liste recht kompliziert macht. Die folgende Tabelle fasst die alle sehr ähnlich klingenden Klassen zusammen und gibt an, über welche Eigenschaften deren Objektinstanzen miteinander verbunden sind. (Die Tabelle macht deutlich, dass .NET-Klassennamen auch einen Punkt enthalten dürfen.)
14.6 Listenfelder
629
ListView-Klassen (System.Windows.Forms-Namensraum) ListView
ist die Klasse des Steuerelements. CheckedItems → ListView.CheckedListViewItemCollection CheckedIndices → ListView.CheckedIndexCollection Colums → ListView.ColumnHeaderCollection Columns(n) → ColumnHeader FocusedItem → ListViewItem Items → ListView.ListViewItemCollection Items(n) → ListViewItem SelectedItems → ListView.SelectedListViewItemCollection SelectedIndices → ListView.SelectedIndexCollection
ListViewItem
beschreibt einen Listeneintrag. Die Eigenschaften Text, Font, Back- und ForeColor geben an, wie der Text angezeigt werden soll. SubItems → ListViewItem.ListViewSubItemCollection SubItems(n) → ListViewSubItem
ListView.ListViewItemCollection
ermöglicht den Zugriff auf alle Listeneinträge und insbesondere das Einfügen und Entfernen von Einträgen.
ListViewSubItem
enthält die Detailinformationen zum Listeneintrag (also die Texte, die bei der Detailansicht ab der zweiten Spalte angezeigt werden).
ListViewItem.ListViewSubItemCollection
ermöglicht den Zugriff auf alle ListViewSubItemEinträge eines Listeneintrags. (Achtung: SubItems(0) liefert den Text der ersten, nicht der zweiten Spalte!)
ColumnHeader
beschreibt einen Spaltentitel für die Detailansicht. Die Eigenschaften Text, TextAlign und Width geben den Spaltentitel, die Ausrichtung und die Breite an.
ListView.ColumnHeaderCollection
ermöglicht den Zugriff auf alle ColumnHeaderObjekte des Steuerelements.
ListView.SelectedListViewItemCollection
verweist auf die ListViewItem-Objekte, die momentan ausgewählt sind.
ListView.CheckedListViewItemCollection
verweist auf die die ListViewItem-Objekte, deren Auswahlkästchen angeklickt sind.
ListView.SelectedIndexCollection
enthält die Indexnummern der ausgewählten ListViewItem-Objekte.
ListView.CheckedIndexCollection
enthält die Indexnummern aller ListViewItemObjekte, deren Auswahlkästchen angeklickt sind.
630
14 Steuerelemente
Abbildung 14.19: Darstellungsformen des ListView-Steuerelements
Initialisierung des Steuerelements Für erste Experimente mit dem Steuerelement möchten Sie wahrscheinlich einfach ein paar Listeneinträge einfügen und das Steuerelement anzeigen. Das Einfügen der Daten kann wahlweise in der Entwicklungsumgebung oder per Code erfolgen. Initialisierung in der Entwicklungsumgebung: Üblicherweise beginnen Sie damit, dass Sie View=Details einstellen (Detailansicht) und dann mit dem Dialog für die die ColumnsEigenschaft die Spalten für das Steuerelement definieren (Beschriftung, Ausrichtung, Breite). Dazu fügen Sie mit HINZUFÜGEN leere Spalten ein und stellen anschließend die Eigenschaften Text, TextAlign und Width ein. Die Items-Eigenschaft führt zu einem ähnlichen Dialog, in dem Sie einzelne Listeneinträge hinzufügen können. Die Text-Eigenschaft gibt den Text für die erste Spalte der Detailansicht an. Falls Sie Texteinträge für mehrere Spalten angeben möchten, führt die SubItemsEigenschaft zu einem weiteren Dialog. Falls Sie nicht vorhaben, die Detailansicht von ListView zu nutzen, können Sie auf die Initialisierung von Columns und SubItems verzichten. Generell ist die manuelle Eingabe der Listeneinträge umständlich und nicht intuitiv. In der Praxis werden Sie vermutlich (wenn überhaupt) nur die Columns-Eigenschaft in der Entwicklungsumgebung einstellen und den Rest der Intialisierung per Code erledigen.
14.6 Listenfelder
631
Initialisierung per Code: Die folgenden Zeilen demonstrieren die Initialisierung einer Liste mit drei Spalten und zwei Listeneinträgen. Mit Clear werden eventuell schon vorhandene Spalten bzw. Listeneinträge gelöscht. Die Add-Methoden liefern jeweils das erzeugte Objekt zurück, so dass weitere Eigenschaften (z.B. die Farbe) eingestellt werden können. Die Texte für die Spalten zwei bis n werden über die SubItems-Eigenschaft des ListViewItemObjekts eingestellt. Dim lvitem As ListViewItem With ListView1 .Columns.Clear() .Columns.Add("spalte 1", 120, HorizontalAlignment.Left) .Columns.Add("spalte 2", 60, HorizontalAlignment.Right) .Columns.Add("spalte 3", 60, HorizontalAlignment.Right) .Items.Clear() lvitem = .Items.Add("listeneintrag 1") lvitem.ForeColor = Color.Red lvitem.SubItems.Add("50") 'Spalte 2 zu Listeneintrag 1 lvitem.SubItems.Add("x") 'Spalte 3 zu Listeneintrag 1 lvitem = .Items.Add("listeneintrag 2") lvitem.SubItems.Add("30") 'Spalte 2 zu Listeneintrag 2 lvitem.SubItems.Add("y") 'Spalte 3 zu Listeneintrag 2 End With
TIPP
Wenn Sie den Code ansehen, gewinnen Sie vielleicht den Eindruck, dass SubItems(0) den Text der zweiten Spalte, SubItems(1) den Text der zweiten Spalte bezeichnet etc. Das ist falsch: Bevor Sie SubItems.Add zum ersten Mal ausführen, existiert SubItems(0) bereits und enthält den Text der ersten Spalte. lvitem.Text und lvitem.SubItems(0).Text liefern daher denselben Text! SubItems(1) bezieht sich also auf die zweite Spalte! Wenn Sie im laufenden Programm größere Veränderungen an den Listenelementen durchführen, sollten Sie zu Beginn die Methode BeginUpdate und zum Abschluss EndUpdate ausführen. Sie vermeiden damit, dass das Steuerelement bei jeder einzelnen Veränderung am Bildschirm aktualisiert wird, was langsam ist und ein störendes Flackern verursacht. Wenn Sie zahlreiche Listeneinträge besonders effizient einfügen möchten, können Sie dazu die AddRange-Methode verwenden. Diese Methode erwartet als Parameter ein Feld von ListViewItem-Objekten (die Sie also vorweg erzeugen müssen).
Bitmaps für die Listeneinträge Mit der oben durchgeführten Initialisierung werden im Steuerelement ausschließlich Texte angezeigt. Wenn Sie die Listeneinträge mit Bildern verschönern möchten, wird es ein bisschen komplizierter: Die Bilder werden nämlich nicht direkt vom ListView-Steuerelement verwaltet, sondern durch zwei ImageList-Steuerelemente. Die Grundidee besteht darin, dass zusammen mit jedem Listenelement nur die Indexnummer einer Bitmap gespei-
632
14 Steuerelemente
chert wird (Eigenschaft ImageIndex des ListViewItem-Objekts). Die eigentlichen Bitmaps befinden sich aber in einem ImageList-Steuerelement. Wenn Sie alle vier Darstellungsformen unterstützen möchten (siehe die übernächste Überschrift), benötigen Sie Bitmaps in zwei Größen. Üblich sind Bitmaps mit 16*16 bzw. 32*32 Pixel. Zur Speicherung dieser Bitmaps müssen Sie zwei getrennte ImageList-Steuerelemente verwenden. Die Verbindung zwischen den beiden ImageList-Steuerelemente und dem ListView-Steuerelement erfolgt über die Eigenschaften Large- und SmallImageList, die Sie üblicherweise im Eigenschaftsfenster einstellen. Anhand eines praktischen Beispiels ist die Vorgehensweise am einfachsten zu verstehen: Das Beispielprogramm dieses Abschnitts verwendet drei Bitmaps: eines für Verzeichnisse, eines für Dateien und eines für den Wechsel in das übergeordnete Verzeichnis (also für das Verzeichnis ..). Der erste Schritt besteht darin, Bitmaps für diese Dateien in zwei verschiedenen Größen zu erstellen und in einem Verzeichnis zu speichern (siehe Abbildung 14.22). Für das vorliegende Beispiel wurden die Bitmaps einfach aus Screenshots des WindowsExplorer extrahiert und mit dem mit allen Windows-Versionen mitgelieferten Programm mspaint.exe zur richtigen Größe zugeschnitten. Für eigene Anwendungen können Sie dieselbe Vorgehensweise wählen, eigene Bitmaps zeichnen oder auf die mitgelieferten Bitmaps zurückgreifen (Verzeichnis Programme\Microsoft Visual Studio .NET\Common7\Graphics).
Abbildung 14.20: Die Bitmaps zum Beispielprogramm für diesen Abschnitt
Im zweiten Schritt fügen Sie Ihrem Formular zwei ImageList-Steuerelemente ein. Um Verwirrung zu vermeiden, sollten Sie die Steuerelemente von ImageList1 und -2 in ImageListSmall und -Large umbennen. Bei ImageListLarge müssen Sie außerdem die Bitmap-Auflösung auf 32*32 Pixel vergrößern (bevor Sie Bitmaps einfügen!). Schließlich müssen Sie die beiden ImageList-Steuerelemente mit dem ListView-Steuerelement verbinden (d.h., Sie müssen die Eigenschaften Large- und SmallImageList einstellen). Um die Bitmaps in die ImageList-Steuerelemente einzufügen, aktivieren Sie das Steuerelement und öffnen über das Eigenschaftsfenster (Images-Eigenschaft) den IMAGE COLLECTION EDITOR. Mit HINZUFÜGEN fügen Sie nun der Reihe nach die Bitmaps ein (siehe Abbildung
14.6 Listenfelder
633
14.21). Es ist leider nicht möglich, mehrere Bitmaps gleichzeitig einzufügen. Achten Sie darauf, dass die Reihenfolge der Bitmaps in beiden ImageList-Steuerelementen dieselbe ist!
Abbildung 14.21: Bitmaps in das ImageList-Steuerelement einfügen
Damit im ListView-Steuerelement Bitmaps angezeigt werden, müssen Sie nun noch bei jedem Listeneintrag die Bitmap-Nummer angeben. Damit beim ersten Listeneintrag die Datei-Bitmap angezeigt wird, führen Sie folgende Anweisung aus: ListView1.Items(0).ImageIndex = 2
Sie können den gewünschten ImageIndex-Wert auch gleich beim Erstellen des Listeneintrags durch Add angeben: ListView1.Items.Add("listeneintrag 1", 2)
Verwaltung der Listendaten (ListViewItem-Klasse ableiten) Anders als die in den vorigen Abschnitten beschriebenen ListBox-Varianten kann ListView in den Listenelementen – also in den ListViewItem- und ListViewSubItem-Klassen – nur gewöhnliche Zeichenketten speichern (keine Objekte). Sehr oft ist es aber notwendig, für jeden Listeneintrag außer den Zeichenketten für die einzelnen Spalten auch Zusatzinformationen zu verwalten. Dazu gibt es zwei Möglichkeiten, eine einfache und eine, die den Prinzipien der objektorientierten Programmierung besser entspricht. Der einfache Weg: In der Tag-Eigenschaft der ListViewItem-Klasse kann ein Verweis auf ein beliebiges Objekt gespeichert werden. Wenn Sie also wie beim Beispielprogramm zu diesem Abschnitt in den einzelnen Listeneinträgen Dateinamen anzeigen, wäre es naheliegend, mit jedem Listeneintrag ein FileInfo-Objekt zu speichern. Nichts leichter als das: ListView1.Items(0).Tag = myFileinfoObject
634
14 Steuerelemente
Üblicherweise werden Sie diese Initialisierung dann durchführen, wenn Sie mit Add ein neues ListViewItem-Objekt erzeugen. Die folgenden Zeilen deklarieren auch gleich den Text für zwei weitere Spalten des ListViewItem: Dim lvitem As ListViewItem ' 2 ist die Indexnummer für die Datei-Bitmap lvitem = ListView1.Items.Add(myFileinfoObject.Name, 2) ' in der zweiten Spalte: Dateigröße lvitem.SubItems.Add(myFileinfoObject.Length.ToString) ' in der dritten Spalte: Datum der letzten Änderung lvitem.SubItems.Add(myFileinfoObject.LastWriteTime.ToString) lvitem.Tag = myFileinfoObject
Da Tag den Typ Object hat, müssen Sie beim Zugriff CType verwenden, um Tag in den gewünschte Objekttyp zu verwandeln. (Vorher sollten Sie sicherstellen, dass Tag tatsächlich ein entsprechendes Objekt enthält und nicht etwa Nothing!) myFileInfoObject = CType(ListView1.Items(0).Tag, IO.FileInfo)
Der objektorientierte Weg: Die andere Variante besteht darin, eine neue Klasse bilden, die von der ListViewItem-Klasse abgeleitet ist, die aber zusätzliche Eigenschaften besitzt, um die erforderlichen Zusatzinformationen zu speichern. Diese Variante bietet deutlich mehr Flexibilität: So können Sie die neue Klasse mit eigenen Methoden zur Initialisierung, Verwaltung etc. ausstatten. Im folgenden Beispiel wird die Klasse mySuperItem mit der Zusatzeigenschaft FileInfo definiert, um darin ein IO.FileInfo-Objekt zu speichern. Um den Umgang mit der Klasse so komfortabel wie möglich zu machen, wird das Objekt mit einer eigenen New-Methode ausgestattet, die nicht nur die neue Eigenschaft, sondern auch diverse andere ListViewItemEigenschaften initialisiert. (Was hier wie initialisiert wird, hängt natürlich stark von der Gestaltung des ListView-Steuerelements ab. Der folgende Code entspricht der Initialisierung, die auch beim obigen Beispiel vorgenommen wurde.) Class mySuperItem Inherits ListViewItem Public FileInfo As IO.FileInfo
'die zusätzliche Eigenschaft
Public Sub New(ByVal fi As IO.FileInfo) Me.Text = fi.Name Me.ImageIndex = 2 'Index-Nummer für Datei-Bitmap Me.SubItems.Add(fi.Length.ToString) Me.SubItems.Add(fi.LastWriteTime.ToString) Me.FileInfo = fi Me.Tag = fi End Sub End Class
14.6 Listenfelder
635
Um einen neuen Listeneintrag einzufügen, verwenden Sie weiterhin ListView1.Items.Add. Allerdings übergeben Sie dieser Methode nun ein Objekt des Typs mySuperItem. (Add erwartet eigentlich ein Objekt der Klasse ListViewItem – aber jedes davon abgeleitete Objekt ist natürlich auch erlaubt.) ListView1.Items.Add(New mySuperItem(myFileinfoObject))
Wenn Sie zu einem späteren Zeitpunkt auf die FileInfo-Eigenschaft des mySuperItem-Objekts zugreifen möchten, müssen Sie wieder CType verwenden. ListView1.Items(n) liefert nämlich definitionsgemäß ein ListViewItem-Objekt, das erst in ein mySuperItem-Objekt umgewandelt werden muss:
VERWEIS
myFileInfoObject = CType(ListView1.Items(0), mySuperItem).FileInfo
In der Online-Hilfe finden Sie ein weiteres Beispiel für eine Klasse, die von TreeNode abgeleitet wird (zur Verwendung in einem TreeView-Steuerelement): ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbtsksubclassinglistitemortreenode.htm
Erscheinungsformen der Liste Grundsätzlich kennt das Steuerelement vier Erscheinungsformen, die durch die Eigenschaft View gesteuert werden (siehe auch Abbildung 14.19): •
View=LargeIcons: Listeneinträge werden durch ein großes Icon und einen darunter an-
geordneten Text dargestellt. Lange Texte werden nach Möglichkeit auf mehrere Zeilen verteilt oder sonst verkürzt. •
View=SmallIcon und List: Jeder Listeneintrag wird durch ein kleines Icon und einem daneben angeordneten Text dargestellt. Bei SmallIcon werden so viele Spalten nebeneinan-
der dargestellt, wie im Steuerelement Platz finden. Wenn die Listeneinträge nicht alle Platz finden, erscheint ein vertikaler Scroll-Balken. Bei List wird dagegen die Anzahl der Einträge pro Spalte durch die Steuerelementgröße limitiert. Wenn nicht alle Listeneinträge gleichzeitig angezeigt werden können, erscheint ein horizontaler Scroll-Balken. •
View=Details: In dieser Darstellungsform werden zu jedem Listeneintrag auch die Detailinformationen angezeigt.
SmallIcon-Fehler: Die Darstellungsform View=SmallIcon funktioniert nur bei der ersten Darstellung der Liste. Wenn Sie die Liste später löschen (Methode Clear) und neu aufbauen,
gelingt es dem Steuerelement nicht mehr, ordentliche Spalten zu bilden (siehe Abbildung 14.22). Bis zum Erscheinen einer hoffentlich fehlerbereinigten .NET-Version gibt es zwei gleichermaßen umständliche Wege, um den Fehler zu umgehen: Der eine besteht darin, Clear zu vermeiden und die Listeneinträge einzeln zu löschen:
636
14 Steuerelemente
While ListView1.Items.Count > 0 ListView1.Items(ListView1.Items.Count - 1).Remove() End While
Die andere Variante besteht darin, die Clear-Methode wie bisher zu verwenden, aber nach dem Einfügen der neuen Einträge die View-Eigenschaft vorübergehend zu ändern. Diese Kommandos sollten vor der Methode EndUpdate ausgeführt werden, um ein Flickern am Bildschirm zu vermeiden. If ListView1.View = View.SmallIcon Then ListView1.View = View.List ListView1.View = View.SmallIcon End If
Abbildung 14.22: Darstellungsfehler bei View=SmallIcon
Gestaltung- und Bedienungsdetails Eine Menge Details, die die Gestaltung und Bedienung des Steuerelement betreffen, können durch diverse ListView-Eigenschaften gesteuert werden. Die folgende Aufzählung gibt einen Überblick. •
AllowColumnReorder=True ermöglicht es dem Anwender, die Reihenfolge der Spalten
zu verändern (nur bei der Detailansicht). •
CheckBoxes=True bewirkt, dass bei jedem Listeneintrag ein Auswahlkästchen angezeigt
wird. In manchen Anwendungsfällen ermöglichen Sie damit eine bessere bzw. übersichtlichere Mehrfachauswahl. Leider sind die Auswahlkästchen überdurchschnittlich hässlich (siehe Abbildung 14.23). Eine 3D-Darstellung ist nicht möglich. Die Kästchen werden am unteren Ende der Bitmap ausgerichtet. Wenn das Bild die Bitmap nicht vollständig ausfüllt, sollte der leere Teil der Bitmap oben (nicht unten) sein. Wenn Ihnen die Defaultauswahlkästchen nicht gefallen oder wenn der Anwender bei jedem Listeneintrag zwischen mehr als zwei Zuständen auswählen soll, können Sie Ihrem Formular ein weiteres ImageList-Steuerelement hinzufügen, das Bitmaps für die verschiedenen Zustände enthält. Das Steuerelement wird über die StateImageListEigenschaft mit dem ListView-Steuerelement verbunden. Per Default wird bei jedem Listenelement der erste Zustand (also ImageListStatus.Images(0)) angezeigt. Mit jedem Anklicken wird der jeweils nächste Zustand aktiviert.
14.6 Listenfelder
637
Um festzustellen, welche Listeneinträge ausgewählt wurden, können Sie bei jedem Listeneintrag (ListViewItem-Objekt) die Eigenschaften Checked oder StateImageIndex (falls es mehr als zwei Zustände gibt) auswerten. Noch praktischer ist zumeist die Eigenschaft ListView1.CheckedItems, die direkt eine Aufzählung aller ListViewItem-Objekte liefert. •
FullRowSelect gibt an, wo das Listenfeld (nur bei der Detailansicht) angeklickt werden muss, um einen Listeneintrag auszuwählen. Mit FullRowSelect=True gilt der Mausklick für die gesamte Zeilenbreite (was mir persönlich intuitiver erscheint). Die Defaulteinstellung lautet aber False und entspricht damit dem vom Windows-Explorer bekannten Verhalten.
•
Mit GridLines=True erreichen Sie, dass die Listeneinträge durch graue Tabellenlinien getrennt werden (nur bei der Detailansicht).
•
Mit HeaderStyle können Sie angeben, ob bei der Detailansicht Spaltentitel angezeigt werden sollen und ob diese angeklickt werden können (etwa um die Sortierreihenfolge zu verändern). Die Defaulteinstellung lautet Clickable.
•
HoverSelection=True ermöglicht die Auswahl von Listeneinträgen ohne Mausklick. Die
Maus muss dazu nur ein paar Sekunden über dem Listenelement bleiben. In der Praxis ist diese Art der Auswahl aber unüblich und wenig intuitiv. •
LabelWrap gibt an, ob lange Texte zur Beschriftung der Listeneinträge automatisch auf mehrere Zeilen verteilt werden (nur bei der Ansicht LargeIcons). Das ist per Default der Fall.
•
MultiSelect gibt an, ob mehrere Listeneinträge gleichzeitig ausgewählt werden dürfen.
Das ist per Default der Fall. Die Mehrfachauswahl erfolgt entweder durch einen Mausklick zusammen mit Shift oder Strg, oder durch die Markierung der gewünschten Einträge durch ein Auswahlrechteck (Bewegung der Maus bei gedrückter Taste).
Abbildung 14.23: Listendarstellung mit Auswahlkästchen
Optische Gestaltung der einzelnen Listeneinträge Zur Gestaltung der einzelnen Listeneinträge sieht die ListViewItem-Klasse die Eigenschaften Font, Back- und ForeColor vor. Damit können Sie für jedes Listenelement eine eigene Schrift und Farbe verwenden. Wenn Ihnen das noch nicht genug Gestaltungsspielraum gibt, können Sie bei der Detailansicht auch die Schrift und Farben der weiteren Spalten
638
14 Steuerelemente
HINWEIS
indiviuell einstellen. ListViewSubItem sieht dazu ebenfalls die Eigenschaften Font, Back- und ForeColor vor. Diese Eigenschaften werden aber nur dann wirksam, wenn beim zugrunde liegenden ListViewItem die Eigenschaft UseItemStyleForSubItems auf False gestellt wird. Bei der Veränderung der Schriftart sollten Sie allerdings vorsichtig sein. Wenn Sie die Schriftart größer als die Defaultschriftart des Steuerelements einstellen, werden Teile des Texts abgeschnitten. Die Font-Eigenschaft eignet sich daher eher zur Veränderung von Schriftattributen (fett, kursiv etc.).
Sortierung der Spalten Das ListView-Steuerelement sieht eine einfache Möglichkeit vor, die Listeneinträge zu sortieren: Wenn Sie die Eigenschaft Sorting auf Ascending oder Descending setzen, werden die Listeneinträge ihren Texten entsprechend auf- oder absteigend sortiert. (Per Default ist Sorting auf None gestellt. Die Listeneinträge werden dann in der Reihenfolge angezeigt, in der sie eingefügt wurden.) Bei der mehrspaltigen Detailansicht soll es normalerweise möglich sein, die Liste auch nach den Inhalten der weiteren Spalten zu sortieren. Leider bietet das ListView-Steuerelement hierfür keine einfache Möglichkeit. Stattdessen muss an die ListViewItemSorterEigenschaft ein Objekt übergeben werden, die die IComparer-Schnittstelle realisiert. Die Methode Compare dieser Klasse wird dann jedes Mal aufgerufen, wenn das ListView-Steuerelement zwei Listeneinträge miteinander vergleicht. (Auch wenn die Eigenschaft ListViewItemSorter es nahelegt, müssen Sie sich um das Sortieren auch weiterhin nicht kümmern. Sie müssen aber eine Klasse programmieren, die zwei ListViewItem-Objekte vergleicht.) Die folgenden Zeilen zeigen das Schema für eine sehr einfache Sortierklasse, die nach den Texten der zweiten Spalte sortiert. Entscheidend ist, dass die Parameter x und y zwar als Object deklariert werden müssen, dass aber ListViewItem-Objekte übergeben werden. Daher müssen Sie in der Compare-Funktion CType zur Objekttypumwandlung einsetzen. Friend Class CompareBySecondColumn Implements IComparer Private sort_order As SortOrder 'System.Windows.Forms.SortOrder Public Function Compare(ByVal x As Object, ByVal y As Object) _ As Integer Implements IComparer.Compare Return String.Compare(CType(x, ListViewItem).SubItems(1).Text, _ CType(y, ListViewItem).SubItems(1).Text) End Function End Class
Der Code zur Anwendung dieser neuen Klasse sieht folgendermaßen aus: ListView1.ListViewItemSorter = New CompareBySecondColumn()
In der Praxis werden Sie meist etwas komplexere Vergleichsklassen einsetzen, die nicht einfach einen Text, sondern auch die zugrunde liegenden Daten auswerten und die invers
14.6 Listenfelder
639
vergleichen können (damit sie sowohl zum auf- als auch zum absteigenden Sortieren verwendet werden können). Das im nächsten Abschnitt vorgestellte Beispielprogramm enthält hierfür zwei Beispiele. Bei der Anwendung der ListViewItemSorter-Eigenschaft müssen Sie einige Besonderheiten beachten: •
Bei der Veränderung der ListViewItemSorter-Eigenschaft wird die Liste automatisch sortiert. Zu einem späteren Zeitpunkt können Sie die Sortierung manuell mit der SortMethode auslösen.
•
Wenn Sie neue Einträge in das ListView-Steuerelement einfügen, werden diese entsprechend der aktuellen Sortierordnung automatisch korrekt eingeordnet. Allerdings funktioniert das bei der Verwendung einer eigenen Sortierfunktion nicht immer zuverlässig. So wurde etwa bei dem im nächsten Abschnitt vorgestellten Beispielprogramm beim Neuerstellen der Liste (also bei einem Verzeichniswechsel) immer genau ein Eintrag falsch einsortiert! Abhilfe schafft erst das manuelle Ausführen der Sort-Methode am Ende der Einfügeoperation.
•
Darüber hinaus hat sich herausgestellt, dass das Einfügen von Listenelementen bei der Verwendung einer eigenen Sortierfunktion unglaublich langsam erfolgt (und umso langsamer, je mehr Einträge die Liste bereits hat). Das gilt auch dann, wenn vor dem Einfügen der neuen Einträge BeginUpdate und danach EndUpdate ausgeführt wird. So dauert das Einlesen aller Dateien aus dem Verzeichnis Windows\System32 mehrerere Sekunden, während die CPU-Aktivität 100 Prozent beträgt. Abhilfe schafft es, die Sortierfunktion mit ListViewItemSorter = Nothing vorübergehend abzuschalten und erst nach dem Ende der Einfügeoperationen wieder zu aktivieren. Damit wird die Liste erst zum Schluss neu sortiert, was sowohl Zeit spart als auch den gerade beschriebenen Sortierfehler umgeht. Die genaue Vorgehensweise wird im nächsten Abschnitt (ListView-Beispielprogramm) etwas weiter unten genauer erläutert.
•
Wenn Sie von einer individuellen Sortierung zur Defaultsortierung zurückkehren möchten (also Sortierung nach den Namen der Listeneinträge), müssen Sie ListViewItemSorter auf Nothing setzen und anschließend die Sorting-Eigenschaft neu einstellen.
Bleibt als letzte Frage noch, wo die Sortierung ausgelöst werden soll? Der geegnete Ort ist üblicherweise die ColumnClick-Ereignisprozedur des ListView-Steuerelements: An die Prozedur wird die gerade angeklickte Spalte übergeben (e.Column). Nun kann ListViewItemSorter entsprechend eingestellt werden. Beispielcode finden Sie abermals im nächsten Abschnitt.
Pfeile in den Spaltentiteln Aus dem Windows-Explorer sind Sie gewohnt, dass die zur Sortierung berücksichtigte Spalte durch einen kleinen Pfeil gekennzeichnet ist. Das ListView-Steuerelement sieht allerdings keine derartige Formatierung der Spaltentitel vor – in den Titeln kann nur ein einfacher Text angezeigt werden. Das macht aber nichts – .NET ist schließlich Unicode-kompa-
640
14 Steuerelemente
tibel! Was liegt also näher, als einfach die in Unicode vorgesehenen Pfeilsymbole in den Titeln darzustellen? In der Praxis war diese Frage doch nicht so einfach zu beantworten. Hier finden Sie eine Schritt-für-Schritt-Anleitung, um die in Abbildung 14.19 dargestellten Pfeile zu realisieren: •
Um ein Pfeilsymbol in den Programmcode einzufügen (also z.B. in der Form ListView1.Columns(0).Text = "Dateiname ▲") geben Sie das Pfeilzeichen zuerst mit der Hilfe eines Textverarbeitungsprogramms in ein beliebiges Dokument ein (in Microsoft Word z.B. mit EINFÜGEN|SYMBOL, SCHRIFTART Arial, SUBSET Pfeile). Über die Zwischenablage kopieren Sie die Zeichen und fügen sie dann in die VS.NET-Entwicklungsumgebung ein. (Der Entwicklungsumgebung fehlt leider ein Dialog zum Einfügen von Sonderzeichen.) Damit das Unicode-Zeichen im Code auch gespeichert wird, führen Sie in der Entwicklungsumgebung DATEI|SPEICHERN UNTER|SPEICHERN MIT CODIERUNG aus und wählen als Codierung z.B. UNICODE (UTF-8 MIT SIGNATUR). Andernfalls verwendet die Entwicklungsumgebung zum Speichern das ANSI-Format, und das Pfeilzeichen ginge wieder verloren. (Die Entwicklungsumgebung zeigt automatisch eine Warnung an, wenn Sie diesen Schritt vergessen.)
•
Zu guter Letzt müssen Sie noch die Schriftart des ganzen Formulars oder zumindest des ListView-Steuerelements von Microsoft Sans Serif auf Arial umstellen. Zwar sind grundsätzlich beide Schriften Unicode-kompatibel, Arial enthält aber viel mehr Sonderzeichen. Bei Microsoft Sans Serif werden dagegen viele Sonderzeichen durch schwarze Striche dargestellt. HINWEIS
•
Wenn das Programm unter Windows 98/ME ausgeführt wird, kann es sein, dass die Pfeile nicht korrekt dargestellt werden. Das liegt darin, dass das ListView-Steuerelement von einer Betriebssystembibliothek gezeichnet wird. Bei Windows 98/ME ist diese Bibliothek nicht Unicode-kompatibel.
Listeneinträge editieren Die Eigenschaft LabelEdit gibt an, ob der Anwender die Texte der Listeneinträge verändern kann. Wenn die Eigenschaft auf True gesetzt wird, kann der Listeneintrag über die Tastatur verändert werden, nachdem er ein zweites Mal angeklickt wurde. Das allgemein übliche Tastenkürzel F2 funktioniert leider nicht. Wenn Sie F2 aktivieren möchten, fügen Sie einfach die folgende KeyDown-Ereignisprozedur ein. Entscheidend ist die Methode BeginEdit, die das aktuelle Listenelement markiert und einen Cursor dorthin setzt. (Die Not-IsNothing-Bedingung vermeidet, dass F2 einen Fehler auslöst, wenn gerade kein Listeneintrag aktiv ist.)
14.6 Listenfelder
641
Private Sub ListView1_KeyDown(...) Handles ListView1.KeyDown If e.KeyCode = Keys.F2 AndAlso _ (Not IsNothing(ListView1.FocusedItem)) Then ListView1.FocusedItem.BeginEdit() End If End Sub
An sich erfolgt die Veränderung des Listeneintrags automatisch. Im Regelfall sollten sich aber auch die zugrunde liegenden Daten ändern – und das kann natürlich nicht automatisch erfolgen. Im folgenden Beispielprogramm (siehe den nächsten Abschnitt) müsste beispielsweise der Name der Datei bzw. des Verzeichnisses verändert werden. Diese Arbeit müssen Sie selbst erledigen, und zwar in der Ereignisprozedur AfterLabelEdit. (Es gibt übrigens auch ein BeforeLabelEdit-Ereignis, das aber nur selten benötigt wird. Dort können den Editiervorgang gegebenenfalls abbrechen, bevor er noch begonnen hat, wenn beispielsweise ein spezielles Element nicht verändert werden darf.) In der AfterLabelEdit-Ereignisprozedur gibt e.Label den neuen Text an. e.Item enthält die Indexnummer des Listeneintrags, der verändert werden soll. ListView.Items(e.Item).Text liefert weiterhin den ursprünglichen Text.
HINWEIS
Mit dem Verlassen der AfterLabelEdit-Prozedur wird ListView.Items(e.Item).Text automatisch geändert. Bei Bedarf können Sie das mit e.CancelEdit = True verhindern. (Das könnte z.B. im Fehlerbehandlungscode praktisch sein, wenn sich eine Veränderung des Dateinamens als unmöglich herausstellt.) Veränderte Listeneinträge behalten auch dann ihre Position, wenn die Sortierreihenfolge nicht mehr stimmt. Gegebenenfalls müssen Sie eine Neusortierung explizit auslösen.
Auswertung der Listenauswahl Die Auswertung der Listenauswahl erfolgt ganz ähnlich wie bei den bereits vorgestellten anderen Listenfeldern: Bei jeder Veränderung der Auswahl tritt ein SelectedIndexChangedEreignis auf. Die Eigenschaften SelectedIndices bzw. SelectedItems verweisen auf Aufzählungen mit den Indizes bzw. mit den ListViewItem-Elementen der ausgewählten Listeneinträge. FocusedItem verweist auf das gerade aktive Listenelement. (Dieses Element ist allerdings nicht in jedem Fall auch ausgewählt! Wenn ein Element beispielsweise ein zweites Mal mit Strg angeklickt wird, ist es zwar aktiv, aber nicht mehr ausgewählt!) Falls zusammen mit den Listenelementen auch Auswahlkästchen dargestellt werden (CheckBoxes=True), können die so ausgewählten Einträge mit CheckedIndices bzw. CheckedItems ermittelt werden.
642
14 Steuerelemente
14.6.5 ListView-Beispielprogramm Vielleicht haben Sie nach dieser ziemlich umfangreichen Beschreibung den Eindruck gewonnen, dass der Umgang mit dem ListView-Steuerelement sehr kompliziert ist. Das ist nicht der Fall! Erst wenn Sie all die Spezialfälle berücksichtigen, die das Steuerelement vorsieht, wird der Code unübersichtlich. Genau das habe ich im folgenden Beispiel nach Möglichkeit vermieden. Das bereits am Beginn des Abschnitts abgebildete Programm (siehe Abbildung 14.19) zeigt beim Start die Dateien und Verzeichnisse des Laufwerks C: an. Per Doppelklick können Sie in jedes beliebige Verzeichnis wechseln. (Ein Verzeichniswechsel ist allerdings nicht möglich, ebenso wenig wie eine Veränderung von Verzeichnissen oder Dateien oder irgendwelche andere Operationen, etwa das Öffnen oder Starten einer Datei. Das Programm verhält sich also read-only.)
HINWEIS
Die Dateien und Verzeichnisse können nach dem Namen, der Größe oder dem Datum der letzten Veränderung sortiert werden. Dateien und Verzeichnisse bleiben dabei immer in eigenen Gruppen. Komprimierte Verzeichnisse und Dateien werden durch eine blaue Schrift gekennzeichnet. Das Programm ist nicht gegen Zugriffsverletzungen abgesichert. Wenn Sie sich ein Verzeichnis ansehen möchten, zu dem Sie keine Leserechte haben, dann wird das Programm mit einer Fehlermeldung beendet.
Überblick Der Code ist auf zwei Dateien verteilt: Form1.vb enthält die Formulardefinition sowie alle dazugehörenden Ereignisprozeduren. Class1.vb enthält die beiden Klassen CompareBySize und CompareByDate, die zum Sortieren in der Detailansicht nach der Dateigröße bzw. nach dem Datum der letzten Änderung dienen. Form1.vb enthält unter anderem die folgenden Steuerelemente: ImageListSmall ImageListLarge ListView1
ein ImageList-Feld mit drei kleinen Bitmaps (16*16 Pixel) ein ImageList-Feld mit drei großen Bitmaps (32*32 Pixel) das Listenfeld
Die in den beiden ImageList-Feldern enthaltenen Bitmaps finden Sie auch im Unterverzeichnis bitmaps. Bei ListView1 wurden die folgenden Eigenschaften voreingestellt: Columns SmallImageList LargeImageList
enthält die drei Spalten Dateiname (linksbündig) sowie Größe und Datum (beide rechtsbündig) verweist auf das ImageListSmall-Steuerelement verweist auf das ImageListLarge-Steuerelement
14.6 Listenfelder
643
Code Der Programmcode beginnt mit der Deklaration einiger Formularvariablen und -konstanten: ilist_folder, _folder_up und _file enthalten die Indexnummern für die drei Bitmaps in den beiden ImageList-Steuerelementen. showndir gibt das gerade vom Programm angezeigte Verzeichnis an. Und die beiden sort_-Variablen geben an, nach welchen Kriterien die Listeneinträge momentan sortiert sind. (Bei SortOrder handelt es sich übrigens um die Aufzählung System.Windows.Forms.SortOrder, die ich hier für eigene Zwecke eingesetzt habe.) ' Beispiel steuerelemente\listview-test ' Datei Form1.vb Const ilist_folder As Integer = 0 Const ilist_folder_up As Integer = 1 Const ilist_file As Integer = 2 Dim showndir As IO.DirectoryInfo 'aktuell angezeigtes Verzeichnis Dim sort_column As Integer = -1 Dim sort_order As SortOrder = SortOrder.None
In Form_Load wird showndir mit dem Startverzeichnis C: initialisiert. Anschließend wird die Prozedur ReadDirectory aufgerufen, die das ListView-Steuerelement mit den Dateien und Verzeichnissen des durch showndir angegebenen Verzeichnisses füllt. Private Sub Form1_Load(...) Handles MyBase.Load showndir = New IO.DirectoryInfo("C:\") ReadDirectory() End Sub
ListView-Steuerelement mit Listeneinträgen füllen ReadDirectory füllt das Steuerelement mit Daten. Damit das möglichst effizient erfolgt, wird zu Beginn BeginUpdate, am Ende EndUpdate ausgeführt. Außerdem wird eine allfällige Sortierung durch ListViewItemSorter vorübergehend deaktiviert. Die Vergleichsklasse wird in dieser Zeit in der Variablen sort_backup zwischengespeichert (damit am Ende der Prozedur der bisherige Zustand wiederhergestellt werden kann). Eventuell bereits enthaltene Listeneinträge werden durch Items.Clear gelöscht.
Das Einfügen der neuen Daten erfolgt in drei Schritten: Wenn es ein Elternverzeichnis (also das übergeordnete Verzeichnis ..) gibt, wird dieses eingefügt. Anschließend werden alle Unterverzeichnisse und schließlich alle Dateien eingefügt. Bei allen drei Schritten werden in SubItems(...) die Dateigröße und das Datum der letzten Änderung eingetragen. Außerdem wird in Tag das DirectoryInfo-Objekt des Verzeichnisses bzw. das FileInfo-Objekt der Datei gespeichert. (Diese Objekte werden später zum Sortieren benötigen.) Falls die Datei oder das Verzeichnis komprimiert sind, wird durch die ForeColor-Eigenschaft der Listeneintrag auf Blau gestellt. Beachten Sie bitte, dass den Namen der Verzeichnisse jeweils ein Leerzeichen vorangestellt wird. Durch diesen simplen Trick wird erreicht, dass Verzeichnisse immer vor den Dateien sortiert werden.
VERWEIS
644
14 Steuerelemente
Zum Verständnis des Codes ist es erforderlich, dass Sie die Klassen FileInfo, FileSystemInfo und DirectoryInfo aus dem Namensraum System.IO kennen. Dieser Namensraum wird in Abschnitt 10.3 vorgestellt.
Private Sub ReadDirectory() Dim dir As IO.DirectoryInfo Dim file As IO.FileInfo Dim lvitem As ListViewItem Dim sort_backup As IComparer ' Verzeichnisname als Fenstertitel anzeigen Me.Text = showndir.FullName ' Listenfeld erst zum Schluss aktualisieren + sortieren ListView1.BeginUpdate() If Not IsNothing(ListView1.ListViewItemSorter) Then sort_backup = ListView1.ListViewItemSorter ListView1.ListViewItemSorter = Nothing End If ' aktuelle Liste löschen ListView1.Items.Clear() ' Elternverzeichnis If Not IsNothing(showndir.Parent) Then lvitem = ListView1.Items.Add(" ..", ilist_folder_up) lvitem.SubItems.Add("") lvitem.SubItems.Add(showndir.Parent.LastWriteTime.ToString) lvitem.Tag = showndir.Parent End If ' Unterverzeichnisse For Each dir In showndir.GetDirectories() lvitem = ListView1.Items.Add(" " + dir.Name, ilist_folder) lvitem.SubItems.Add("") lvitem.SubItems.Add(dir.LastWriteTime.ToString) lvitem.Tag = dir If (dir.Attributes And IO.FileAttributes.Compressed) = _ IO.FileAttributes.Compressed Then lvitem.ForeColor = Color.Blue End If Next ' Dateien For Each file In showndir.GetFiles() lvitem = ListView1.Items.Add(file.Name, ilist_file) lvitem.SubItems.Add(file.Length.ToString) lvitem.SubItems.Add(file.LastWriteTime.ToString)
14.6 Listenfelder
645
lvitem.Tag = file If (file.Attributes And IO.FileAttributes.Compressed) = _ IO.FileAttributes.Compressed Then lvitem.ForeColor = Color.Blue End If lvitem.UseItemStyleForSubItems = False lvitem.SubItems(1).ForeColor = Color.Red Next ' Liste sortieren und dann anzeigen If Not IsNothing(sort_backup) Then ListView1.ListViewItemSorter = sort_backup End If ListView1.EndUpdate() End Sub
Listendarstellung ändern Button1 variiert die vier möglichen Listendarstellungen. Dazu wird die statische Variable i jedes Mal um eins vergrößert und anschließend auf den Zahlenbereich 0-3 reduziert. Der neue Wert wird dann dazu verwendet, um den Text des Buttons und die View-Eigenschaft einzustellen. Private Sub Button1_Click(...) Handles Button1.Click Static i As Integer = 3 Dim viewStyles As View() = _ {View.LargeIcon, View.SmallIcon, View.List, View.Details} Dim viewStyleText As String() = _ {"LargeIcon", "SmallIcon", "List", "Details"} i = (i + 1) Mod 4 ListView1.View = viewStyles(i) Button1.Text = "View = " + viewStyleText(i) End Sub
Verzeichniswechsel durchführen Ein Doppelklick auf einen Listeneintrag führt zum Aufruf der ListView1_DoubleClick-Ereignisprozedur. Dort wird überprüft, ob der angeklickte Eintrag ein Verzeichnis ist. In diesem Fall wird showndir neu eingestellt und anschließend ReadDirectory aufgerufen. Private Sub ListView1_DoubleClick(...) Handles ListView1.DoubleClick If ListView1.FocusedItem Is Nothing Then Exit Sub If TypeOf ListView1.FocusedItem.Tag Is IO.DirectoryInfo Then showndir = CType(ListView1.FocusedItem.Tag, IO.DirectoryInfo) ReadDirectory() End If End Sub
646
14 Steuerelemente
Sortierreihenfolge ändern Die Ereignisprozedur ListView1_ColumnClick ist nur deswegen so unübersichtlich, weil dort nicht nur die Sortierreihenfolge neu eingestellt wird, sondern auch der Spaltentitel verändert wird. Dem Titel werden die Zeichen "▲" oder "▼" nachgestellt. Damit der Spaltentitel nun nicht bei jeder Änderung der Sortierreihenfolge immer länger wird, müssen die dort zuletzt angefügten Zeichen auch wieder entfernt werden. Dafür sind die ersten Zeilen der Prozedur verantwortlich. Im zweiten Block werden die Variablen sort_order und sort_column neu eingestellt. Wenn eine bereits sortierte Spalte nochmals angeklickt wird, bleibt sort_column gleich, allerdings muss sort_order nun verändert werden. Diese beiden Variablen werden dann im dritten Block ausgewertet, um am Ende des Spaltentitels das Zeichen ▲ oder ▼ einzubauen. Erst am Ende der Prozedur wird die neue Sortierung eingestellt. Dazu wird entweder Sorting oder ListViewItemSorter verändert. (Die Klassen CompareBySize und CompareByDate werden gleich vorgestellt.) Private Sub ListView1_ColumnClick(...) Handles ListView1.ColumnClick ' Kennzeichnung des Spaltentitel entfernen Dim tmp As String If sort_column <> -1 Then tmp = ListView1.Columns(sort_column).Text If tmp.Chars(tmp.Length - 2) = " " Then tmp = Strings.Left(tmp, Len(tmp) -j2) End If ListView1.Columns(sort_column).Text = tmp End If ' falls bereits sortierte Spalte angeklickt wurde: ' Sortierreihenfolge umdrehen If sort_column = e.Column Then If sort_order = SortOrder.Ascending Then sort_order = SortOrder.Descending Else sort_order = SortOrder.Ascending End If Else sort_order = SortOrder.Ascending End If sort_column = e.Column ' Spaltentitel kennzeichnen If sort_order = SortOrder.Ascending Then ListView1.Columns(sort_column).Text += " ▲" Else ListView1.Columns(sort_column).Text += " ▼" End If
14.6 Listenfelder
647
' neu sortieren Select Case sort_column Case 0 'nach Namen sortieren ListView1.ListViewItemSorter = Nothing ListView1.Sorting = sort_order Case 1 'nach Größe sortieren ListView1.ListViewItemSorter = New CompareBySize(sort_order) Case 2 'nach Datum sortieren ListView1.ListViewItemSorter = New CompareByDate(sort_order) End Select End Sub
Nach der Dateigröße sortieren (CompareBySize) Die Methode Compare der Klasse CompareBySize vergleicht die Größe zweier Dateien. An Compare werden zwei ListViewItem-Objekte übergeben. Wenn deren Tag-Eigenschaft auf FileInfo-Objekte verweist, wird mit Length deren Größe ermittelt und in xsize bzw. ysize gespeichert. Bei DirectoryInfo-Objekten bleiben x/xsize auf ihrem Startwert von -1. Das führt dazu, dass Verzeichnisse immer vor Dateien angeordnet werden (oder bei einer umgekehrten Sortierung dahinter). Damit die Vergleichsfunktion sowohl zum auf- als auch zum absteigenden Sortieren verwendet werden kann, muss an den New-Konstruktor die gewünschte Richtung übergeben werden. Der Wert wird in der lokalen Variable sort_order gespeichert und verändert das Vorzeichen des Rückgabewerts. ' Beispiel steuerelemente\listview-test ' Datei Class1.vb Friend Class CompareBySize Implements IComparer Private sort_order As SortOrder 'System.Windows.Forms.SortOrder Public Sub New(ByVal s_order As SortOrder) sort_order = s_order End Sub Public Function Compare(ByVal x As Object, ByVal y As Object) _ As Integer Implements IComparer.Compare Dim xsize Dim ysize If TypeOf xsize = End If If TypeOf ysize = End If
As Long = -1 As Long = -1 CType(x, ListViewItem).Tag Is IO.FileInfo Then CType(CType(x, ListViewItem).Tag, IO.FileInfo).Length CType(y, ListViewItem).Tag Is IO.FileInfo Then CType(CType(y, ListViewItem).Tag, IO.FileInfo).Length
648
14 Steuerelemente
If sort_order = SortOrder.Ascending Then Return xsize.CompareTo(ysize) ElseIf sort_order = SortOrder.Descending Then Return -xsize.CompareTo(ysize) Else Return 0 End If End Function End Class
Nach dem Datum sortieren CompareByDate.Compare vergleicht das Datum von zwei Dateien oder Verzeichnissen, wobei die zugrunde liegenden Daten (ein File- oder DirectoryInfo-Objekt) abermals aus der TagEigenschaft gelesen werden. (Mit CType erfolgt eine Umwandlung in ein FileSystemInfoObjekt. Dabei handelt es sich um die übergeordnete Klasse zu File- oder DirectoryInfo,
weswegen in diesem Fall beide Klassen einheitlich behandelt werden können.) Um auch hier wieder Dateien und Verzeichnisse in eigenen Gruppen behandeln zu können, wird ein kleiner Trick verwendet: Bei Verzeichnissen werden vom Datum 100 Jahre abgezogen. (Diese fiktive Zeit wird nur intern verwendet, nicht aber angezeigt!) Friend Class CompareByDate Implements IComparer Private sort_order As SortOrder
'System.Windows.Forms.SortOrder
Public Sub New(ByVal s_order As SortOrder) sort_order = s_order End Sub Public Function Compare(ByVal x As Object, ByVal y As Object) _ As Integer Implements IComparer.Compare Dim xtag, ytag As Object Dim xdate, ydate As Date Dim cmp_result As Integer ' Objekt ermitteln, auf das via Tag verwiesen wird xtag = CType(x, ListViewItem).Tag ytag = CType(y, ListViewItem).Tag ' falls IO.FileSystemInfo: Datum ermitteln ' falls IO.DirectoryInfo: Datum - 100 Jahre If TypeOf xtag Is IO.FileSystemInfo Then xdate = CType(xtag, IO.FileSystemInfo).LastWriteTime End If
14.6 Listenfelder
If TypeOf xdate = End If If TypeOf ydate = End If If TypeOf ydate = End If
649
xtag Is IO.DirectoryInfo Then xdate.AddYears(-100) ytag Is IO.FileSystemInfo Then CType(ytag, IO.FileSystemInfo).LastWriteTime ytag Is IO.DirectoryInfo Then ydate.AddYears(-100)
' Ergebnis des Vergleichs If sort_order = SortOrder.Ascending Then Return Date.Compare(xdate, ydate) ElseIf sort_order = SortOrder.Descending Then Return -Date.Compare(xdate, ydate) Else Return 0 End If End Function End Class
14.6.6 TreeView Das TreeView-Steuerelement ermöglicht die Darstellung hierarchischer Listen. Das bekannteste Beispiel für eine derartige Liste ist die Verzeichnisstruktur einer Festplatte (siehe Abbildung 14.24), die ja auch im Windows-Explorer in einer hierarchischen Form dargestellt wird. Abgesehen von den Zusatzfunktionen, die sich aus der Verwaltung der Hierarchie ergibt, hat das TreeView-Steuerelement viele Ähnlichkeiten mit dem ListView-Steuerelement. Dieser Abschnitt setzt daher voraus, dass Sie mit dem ListView-Steuerelement bereits vertraut sind. (In sehr vielen Anwendungen werden Sie die beiden Steuerelemente in Kombination einsetzen.) Im Vergleich zum ListView-Steuerelement werden Sie bei TreeView mit erfreulich wenig Klassen konfrontiert, die im folgenden Kasten kurz beschrieben sind. (Es gibt noch einige weitere Klassen, die aber selten benötigt werden bzw. nur zur Parameterübergabe an Ereignisprozeduren dienen.) TreeView-Klassen (System.Windows.Forms-Namensraum) TreeView
ist die Klasse des Steuerelements. Nodes → TreeNodeCollection Nodes(n) → TreeNode SelectedNode → TreeNode TopNode → TreeNode
650
14 Steuerelemente
TreeView-Klassen (System.Windows.Forms-Namensraum) TreeNode
beschreibt einen Listeneintrag. Die Eigenschaften Text, Back- und ForeColor geben an, wie der Text angezeigt werden soll. Die Eigenschaften FirstNode, LastNode, NextNode, PrevNode und Parent verweisen auf andere Listeneinträge in derselben oder in der darüberliegenden Hierarchiestufe. Falls es auch untergeordnete Einträge gibt, sind diese über Nodes zugänglich.
TreeNodeCollection
ermöglicht den Zugriff auf alle Listeneinträge (einer Gruppe) und insbesondere das Einfügen und Entfernen von Einträgen.
Abbildung 14.24: Das TreeView-Steuerelement
Initialisierung des Steuerelements Initialisierung in der Entwicklungsumgebung: Über die Nodes-Eigenschaft im Eigenschaftsfenster gelangen Sie in einen ausgesprochen intiutiven Dialog, in dem Sie mit STAMM HINZUFÜGEN den ersten Eintrag einfügen können, mit UNTERORDNER HINZUFÜGEN untergeordnete Einträge zum gerade ausgewählten Eintrag. Im Label-Feld können Sie den Text des Listeneintrags angeben. (Das Eingabefeld ist wohl irrtümlich mit Label beschriftet worden. Tatsächlich verändern Sie nämlich die Text-Eigenschaft des TreeNode-Objekts.)
14.6 Listenfelder
651
Falls Sie das TreeView- mit einem ImageList-Steuerelement verbunden haben (siehe nächste Überschrift), können Sie mit den beiden Listenfeldern BILD und AUSGEWÄHLTES BILD eine Bitmap auswählen.
Abbildung 14.25: TreeView-Listeneinträge einfügen
Initialisierung per Code: Der TREENODE-EDITOR ist zwar für erste Experimente ganz praktisch, in der Praxis werden Sie die Listeneinträge aber meistens per Code einfügen. Die folgenden Zeilen geben dafür ein Muster. Mit TreeView1.Nodes.Add fügen Sie einen neuen Wurzeleintrag ein (also ein Listenelement, das kein übergeordnetes Element besitzt). Add liefert als Ergebnis das neu erzeugte TreeNode-Objekt zurück. Sie können dieses Objekt verwenden, um nun weitere Eigenschaften des Listeneintrags einzustellen (z.B. die Textfarbe) und um untergeordnete Listeneinträge hinzuzufügen. Dazu bietet auch das TreeNode-Objekt eine Nodes-Eigenschaft.
TIPP
Dim tn1, tn2 As TreeNode TreeView1.Nodes.Clear() tn1 = TreeView1.Nodes.Add("Starteintrag") tn1.Nodes.Add("Subeintrag 1") tn2 = tn1.Nodes.Add("Subeintrag 2") tn2.Nodes.Add("Sub-Subeintrag 1")
Wie beim ListView-Steuerelement sollten Sie umfangreiche Einfügeoperationen durch Begin- bzw. EndUpdate einleiten bzw. abschließen. Darüber hinaus können Sie auf die Methode AddRange zurückgreifen, um zahlreiche Listeneinträge besonders effizient einzufügen.
652
14 Steuerelemente
Bitmaps für die Listeneinträge Vor jedem Listeneintrag kann eine (üblicherweise recht kleine) Bitmap angezeigt werden. Diese Bitmaps werden extern verwaltet, und zwar mit der Hilfe eines ImageList-Steuerelements. Um die Verbindung zwischen den beiden Steuerelementen herzustellen, muss die Eigenschaft ImageList des TreeView-Steuerelements auf das ImageList-Steuerelement verweisen. Die Eigenschaft ImageIndex gibt die Indexnummer des Bilds aus der ImageList an, das bei jedem Listeneintrag angezeigt wird (per Default 0). SelectedImageIndex verweist auf ein weiteres Bild, das automatisch dann angezeigt wird, wenn der Listeneintrag gerade ausgewählt ist. Darüber hinaus kann die gewünschte Bitmap natürlich bei jedem Listeneintrag separat eingestellt werden, und zwar ebenfalls durch die Eigenschaften ImageIndex und SelectedImageIndex (diesmal aber für die TreeNode-Klasse!). Je nach Bitmap-Größe können Sie mit der Eigenschaft ItemHeight des ListView-Steuerelements den Zeilenabstand zwischen den Listeneintragen vergrößern und so vermeiden, dass sich große Bitmaps überlappen.
Verwaltung der Listendaten
VERWEIS
Wie beim ListView-Steuerelement (ListViewItem-Klasse) sind auch beim TreeView-Steuerelement pro Listeneintrag (TreeNode-Klasse) nur die Eigenschaften Text und Tag zur Speicherung von Daten vorgesehen. Wenn Sie außer dem Beschriftungstext zusätzliche Verwaltungsdaten benötigen, können Sie entweder ein beliebiges Objekt in der Tag-Eigenschaft speichern, oder Sie bilden eine neue Klasse, die von der TreeNode-Klasse abgeleitet wird. Beim Einfügen von Listeneinträgen übergeben Sie dann Objekte dieser Klasse an TreeView1.Nodes.Add(...). Die prinzipielle Vorgehensweise zur Programmierung und Verwaltung einer eigenen Klasse wurde bereits in Abschnitt 14.6.4 am Beispiel des ListView-Steuerelements demonstriert. Ein konkretes Beispiel für das TreeView-Steuerelement finden Sie in der Online-Hilfe: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbtsksubclassinglistitemortreenode.htm
Verwaltung der Listenhierarchie Die Listenelemente der TreeView-Steuerelements werden in einer Baumstruktur verwaltet. Sie können zwar mit GetNodeCount(True) die Gesamtzahl aller Listenelemente ermitteln, die Eigenschaft Nodes verweist aber nur auf eine Aufzählung jener Elemente, die sich in der ersten Hierarchieebene befinden. Zu alle untergeordneten Einträgen gelangen Sie nur, indem Sie rekursiv die Nodes-Aufzählungen aller TreeNode-Einträge durchlaufen. Die folgenden Zeilen zeigen, wie Sie alle Listenelemente in der Reihenfolge durchlaufen und
14.6 Listenfelder
653
im Ausgabefenster anzeigen, in der die Elemente auch im Steuerelement dargestellt werden. Private Sub Button1_Click(...) Handles Button1.Click Dim tn As TreeNode For Each tn In TreeView1.Nodes WriteTreeNodeItems(tn) Next End Sub Private Sub WriteTreeNodeItems(ByVal tn As TreeNode) Dim sub_tn As TreeNode Debug.WriteLine(tn.Text) Debug.IndentLevel += 1 For Each sub_tn In tn.Nodes WriteTreeNodeItems(sub_tn) Next Debug.IndentLevel -= 1 End Sub
Ausgehend von einem gegebenen Listeneintrag können Sie mit NextNode und PrevNode das nächste bzw. vorige Element ermitteln. (Die Eigenschaften liefern Nothing, wenn es kein weiteres Element mehr gibt.) FirstNode und LastNode liefern das erste bzw. letzte Element innerhalb der aktuellen Hierarchiegruppe. Parent verweist auf die übergeordnete Ebene (bzw. enthält Nothing, wenn sich das aktuelle Element in der ersten Hierarchieebene befindet). Mit Nodes.Contains(tn) können Sie feststellen, ob sich das TreeNode-Element tn direkt in der angegebenen Aufzählung befindet. (Unterverzeichnisse werden dabei nicht berücksichtigt, d.h., die Suche ist nicht rekursiv.)
Gestaltungs- und Bedienungsdetails Das Aussehen und die Funktion des Steuerelements kann durch eine Reihe von Eigenschaften beeinflusst werden. •
CheckBoxes gibt an, ob zusammen mit jedem Listenelement ein Auswahlkästchen angezeigt werden soll. Veränderungen im Auswahlkästchen lösen die Ereignisse BeforeCheck und AfterCheck aus.
Anders als beim ListView-Steuerelement müssen Sie zur Auswertung alle TreeNode-Elemente durchlaufen und dort die Checked-Eigenschaft auslesen. (Es gibt also keine Aufzählung, die auf alle ausgewählten Listeneinträge verweist.) Das Auswahlkästchen kann leider nur zwei Zustände annehmen (Checked=True oder False). Gerade bei hierarchischen Listen wäre oft ein dritter Zustand, unbestimmt, praktisch. (Das gewöhnliche CheckBox-Steuerelement sieht diesen Zustand vor.) Im Gegensatz zum ListView-Steuerelement besteht auch keine Möglichkeit, das Auswahlkästchen durch eigene Bitmaps zu ersetzen.
654
•
14 Steuerelemente
Indent gibt an, wie stark die Hierarchieebenen eingerückt werden sollen (per Default 19
Pixel je Ebene). •
ItemHeight gibt die Zeilenhöhe an. Die Eigenschaft wird automatisch an die Textgröße angepasst und muss nur dann manuell verändert werden, wenn die Bitmap-Höhe größer als die Texthöhe ist.
•
PathSeparator gibt an, welches Zeichen zur Trennung zwischen den Listeneinträgen
verwendet wird (Default \). Das Zeichen wird nicht angezeigt, sondern ist nur für die FullPath-Eigenschaft von TreeNode-Elementen von Bedeutung: FullPath gibt den kom-
pletten Namen eines Eintrags zurück, der aus allen übergeordneten Einträgen – getrennt eben durch das PathSeparator-Zeichen – zusammengesetzt wird. •
ShowLines gibt an, ob die Listeneinträge durch Linien zur Darstellung der Hierarchiestruktur verbunden werden sollen (per Default True).
•
ShowPlusMinus gibt an, ob vor den Listeneinträgen mit Untereinträgen ein Plus- bzw.
Minuszeichen angezeigt werden soll. Per Default ist das der Fall. Das ist aus zweierlei Gründen praktisch: Erstens ist damit erkenntlich, ob es Untereinträge gibt, und zweitens können diese einfach mit nur einem Mausklick aus- und wieder eingeklappt werden. (Mit ShowPlusMinus=False erfolgt das Ein- und Ausklappen per Doppelklick.) •
ShowRootLines gibt an, ob auch vor den Wurzeleinträgen (also der ersten Hierarchie-
ebene) eine Hierarchielinie und das Plus-/Minuszeichen angezeigt werden soll (per Default True). Wenn es in Ihrer Hierarchie genau ein ausgeprägtes Startobjekt gibt, ist es zumeist plausibler, ShowRootLines auf False zu setzen. •
Sorted gibt an, ob Listeneinträge beim Einfügen automatisch sortiert werden sollen (per Default False). Als Sortierkriterium gilt die Text-Eigenschaft der Listenelemente. Die Sortierung gilt jeweils innerhalb einer Hierarchiegruppe.
Beachten Sie, dass zwar neu eingefügte Einträge automatisch richtig eingeordnet werden, dass sich aber die Position bereits vorhandener Einträge, deren Text-Eigenschaft nachträglich verändert wird, nicht mehr ändert. Wenn Sie das möchten, müssen Sie den Eintrag löschen und neu einfügen (was aber recht kompliziert ist, wenn es auch Untereinträge gibt), oder Sie setzen Sorted auf False und dann wieder auf True (bewirkt eine vollständige Neusortierung). Anders als beim ListView-Steuerelement besteht keine Möglichkeit, eine eigene Vergleichsfunktion für die Sortierung anzugeben. Wenn Sie die Listeneinträge nach einem eigenen Kriterium sortieren möchten, müssen Sie sich beim Einfügen neuer Listeneinträge selbst darum kümmern, diese am richtigen Ort einzufügen. Neben diesen Einstellungen, die für das gesamte Steuerelement gelten, können Sie einzelne Listeneinträge durch die Veränderung der Schrift bzw. der Vor- und Hintergrundfarbe hervorheben. Dazu verändern Sie einfach die NodeFont-, Fore- und BackColor-Eigenschaften der TreeNode-Klasse. (Warum NodeFont nicht einfach Font heißt, wie dies bei fast allen anderen Klassen des .NET-Frameworks üblich ist, weiß allein Microsoft.)
14.6 Listenfelder
655
Auswertung der Listenauswahl, Ereignisse Anders als bei den bisher beschriebenen Listenfeldern fehlt beim TreeView-Steuerelement das SelectedIndexChanged-Ereignis. Stattdessen gibt es die Ereignisse Before- und AfterSelect, die vor bzw. nach der Veränderung des aktiven Listeneintrags eintreffen. In BeforeSelect können Sie den Fokuswechsel durch e.Cancel=True verhindern. Analoge Ereignisse gibt es auch, wenn eine Hierarchiegruppe zusammengeklappt (Before-/AfterCollapse) bzw. auseinandergeklappt werden soll (Before-/AfterExpand). An alle Ereignisprozeduren wird der Parameter e übergeben, deren Eigenschaft Node auf das betroffene Listenelement verweist. Deutlich schwieriger ist es, einen Doppelklick auf ein Listenelement korrekt festzustellen: Das Steuerelement kennt zwar das DoubleClick-Ereignis, dieses wird aber auch dann ausgelöst, wenn der Anwender irgendwo im Listenfeld einen Doppelklick durchführt. An das DoubleClick-Ereignis werden weder das eventuell angeklickte Listenelement noch lokale Mauskoordinaten übergeben. Die folgende DoubleClick-Prozedur liest daher die Mauskoordinaten aus der MousePosition-Eigenschaft, rechnet die absoluten Bildschirmkoordinaten mit PointToClient in das Koordinatensystem des TreeView-Steuerelements um und versucht dann mit GetNodeAt das Listenelement unter der Maus zu ermitteln. Wenn das Ergebnis nicht Nothing lautet, wird dieses Listenelement durch die Veränderung der SelectedNodeEigenschaft ausgewählt. Private Sub TreeView1_DoubleClick(...) Handles TreeView1.DoubleClick Dim tn As TreeNode tn = TreeView1.GetNodeAt(TreeView1.PointToClient( _ TreeView1.MousePosition())) If Not IsNothing(tn) Then TreeView1.SelectedNode = tn ... Reaktion auf den Doppelklick End If End Sub
HINWEIS
In der Praxis sieht die Reaktion auf das DoubleClick-Ereignis meist so aus, dass das Programm zum gerade aktuellen Listeneintrag Subeinträge ermittelt, einfügt und anzeigt. Auf diese Weise ist es möglich, die Listeneinträge erst bei Bedarf zu erzeugen. (Wenn ein Anwender also einen Doppelklick auf den Listeneintrag C: durchführt, sollen die Verzeichnisse in C: angezeigt werden.) Ein konkretes Beispiel für diese Vorgehensweise finden Sie im nächsten Abschnitt bei der Beschreibung des TreeView-Beispielprogramms. Wenn es bereits Untereinträge gibt, dürfen diese kein zweites Mal eingefügt werden! Das kann mit Nodes.Count>0 einfach festgestellt werden. In diesem Fall kann die DoubleClick-Prozedur einfach verlassen werden. Das bei einem Doppelklick ebenfalls übliche Auseinander- und Zusammenklappen erledigt das Steuerelement automatisch.
656
14 Steuerelemente
Listeneinträge editieren Wenn Sie die Eigenschaft LabelEdit auf True setzen, können die Anwender einzelne Listenelemente editieren. (Dazu muss ein bereits markiertes Listenelement ein zweites Mal angeklickt werden.) Die Veränderung der Text-Eigenschaft erfolgt automatisch. Vor dem Beginn des Editierprozesses tritt ein BeginLabelEdit-Ereignis auf, zum Abschluss ein AfterLabelEdit-Ereignis. Einige Details zur AfterLabelEdit-Ereignisprozeduren finden Sie in Abschnitt 14.6.4. Beachten Sie, dass veränderte Listeneinträge ihre Position nicht verändern (egal, wie Sorted eingestellt ist). Wenn Sie möchten, dass das Editieren per F2 gestartet werden kann (was per Default nicht der Fall ist), führen Sie die folgende Ereignisprozedur ein und aktivieren den Editierprozess mit BeginEdit. Private Sub TreeView1_KeyDown(..) Handles TreeView1.KeyDown If e.KeyCode = Keys.F2 AndAlso _ (Not IsNothing(TreeView1.SelectedNode)) Then TreeView1.SelectedNode.BeginEdit() End If End Sub
14.6.7 TreeView-Beispielprogramm Das in Abbildung 14.24 gezeigte Beispielprogramm erzeugt beim Start für jedes Laufwerk am lokalen Rechner einen Starteintrag im TreeView-Steuerelement. Wie beim WindowsExplorer können Sie nun durch Doppelklick die Verzeichnisse in diesem Laufwerk und in der Folge auch alle Unterverzeichnisse öffnen. (Diese Verzeichnisse werden erst bei Bedarf ermittelt.) Mit den drei Auswahlkästchen SHOWLINES, SHOWPLUSMINUS und SHOWROOTLINES können Sie die wichtigsten Gestaltungsvarianten variieren.
HINWEIS
Mit dem Button ALLE ELEMENTE ... AUFLISTEN werden sämtliche Listenelemente durchlaufen und im Ausgabefenster der Entwicklungsumgebung aufgelistet. Mit ALLE UNTERVERZEICHNISSE EINLESEN werden (ausgehend vom gerade markierten Listeneintrag) rekursiv alle Unterverzeichnisse eingelesen und in das Listenfeld eingefügt. Wenn Sie alle Verzeichnisse einer großen Festplatte einlesen, kann das relativ lange dauern! Beispielsweise dauert auf meinem Rechner das erstmalige Einlesen der ca. 4000 Verzeichnisse der Partition C: ca. eineinhalb Minuten. Der Großteil dieser Zeit geht allerdings nicht auf das Konto des Beispielprogramms bzw. auf die Verwaltung der TreeView-Daten, sondern auf das Einlesen der Verzeichnisse von der Festplatte. Wenn das Programm anschließend beendet und neu gestartet wird, gelingt das Einlesen aller Verzeichnisse in nur 15 Sekunden, weil sich nun alle erforderlichen Dateisysteminformationen bereits im RAM befinden.
VERWEIS
14.6 Listenfelder
657
Ein weiteres TreeView-Beispielprogramm finden Sie in Abschnitt 15.6.4. Mit dem Programm können Sie über ein Kontextmenü Listeneinträge hinzufügen, umbenennen und entfernen.
Überblick Form1.vb enthält unter anderem die folgenden Steuerelemente: ImageList1 TreeView1
ein ImageList-Feld mit drei kleinen Bitmaps (16*16 Pixel, siehe Abbildung 14.26) das Listenfeld
Bei TreeView1 wurden die folgenden Eigenschaften voreingestellt: ImageList SelectedImageIndex Sorted
verweist auf das ImageList1-Steuerelement. enthält die Indexnummer 1 (Bitmap open-folder). enthält True (automatische Sortierung der Einträge).
Abbildung 14.26: Die Bitmaps für ImageList1
Code Der Programmcode beginnt mit der Definition einer Aufzählung, die die Indexnummern der drei Bitmaps aus dem ImageList-Steuerelement enthält. In Form_Load werden in das TreeView-Steuerelement die Namen aller verfügbaren Laufwerke eingetragen. Für jeden Listeneintrag werden die Eigenschaften ImageIndex und SelectedImageIndex jeweils auf drive gestellt, so dass bei diesen Listenelementen immer die Laufwerks-Bitmap angezeigt wird. (Bei allen anderen Listeneinträgen werden per Default die Bitmaps 0 und 1 für ein Verzeichnis bzw. für ein geöffnetes Verzeichnis verwendet.)
658
14 Steuerelemente
' Beispiel steuerelemente\treeview-intro Enum ilistIndex folder = 0 open_folder = 1 drive = 2 End Enum Private Sub Form1_Load(..) Handles MyBase.Load Dim s As String Dim drvs As String() = Environment.GetLogicalDrives() Dim tn As TreeNode TreeView1.Nodes.Clear() For Each s In drvs tn = TreeView1.Nodes.Add(s) tn.ImageIndex = ilistIndex.drive tn.SelectedImageIndex = ilistIndex.drive Next End Sub
Bei Doppelklick Unterverzeichnisse einlesen In TreeView1_DoubleClick wird getestet, ob sich unter der Maus ein Listeneintrag befindet. Wenn das der Fall ist, wird dieser ausgewählt. Falls der Eintrag noch keine Untereinträge enthält, wird die Hilfsprozedur ReadDirectories aufgerufen, um diese Einträge einzufügen. ReadDirectories ermittelt mit tn.FullPath den vollständigen Verzeichnisnamen zum ausgewählten Listeneintrag. Anschließend werden mit GetDirectories alle Unterverzeichnisse ermittelt und in das TreeView-Steuerelement eingefügt.
Die Schleife ist zweifach durch Try-Catch-Konstruktionen abgesichert. Die erste Ebene vermeidet Fehlermeldungen, wenn ein ganzes Laufwerk nicht verfügbar ist und daher bereits bei GetDirectories ein Fehler auftritt. (Ein typisches Beispiel: Doppelklick auf A:, obwohl sich keine Diskette im Laufwerk befindet.) Die zweite Ebene vermeidet Fehlermeldungen, wenn – meist wegen mangelnder Zugriffsrechte – einzelne Verzeichnisse nicht zugänglich sind. Vor dem Abschluss der Prozedur wird mit GetNodeCount die Gesamtzahl der Listeneinträge ermittelt und in einem Labelfeld angezeigt. Private Sub TreeView1_DoubleClick(...) Handles TreeView1.DoubleClick Dim tn As TreeNode tn = TreeView1.GetNodeAt(TreeView1.PointToClient( _ TreeView1.MousePosition())) If Not IsNothing(tn) Then TreeView1.SelectedNode = tn If tn.Nodes.Count = 0 Then ReadDirectories(tn) End If End If End Sub
14.6 Listenfelder
659
Private Sub ReadDirectories(ByVal tn As TreeNode) Dim di As New IO.DirectoryInfo(tn.FullPath) Dim subdir As IO.DirectoryInfo Try For Each subdir In di.GetDirectories() Try tn.Nodes.Add(subdir.Name) Catch ' Fehler ignorieren End Try Next tn.Expand() Catch ' Fehler ignorieren End Try ' Gesamtzahl der Einträge anzeigen Label1.Text = "Insgesamt " + _ TreeView1.GetNodeCount(True).ToString + " Listenelemente" End Sub
Alle Unterverzeichnisse einlesen Wenn Button2 angeklickt wird, testet das Programm, ob überhaupt ein Listeneintrag ausgewählt ist. Wenn das der Fall ist, wird die rekursive Prozedur ReadAllDirectories aufgerufen, um beginnend vom Startverzeichnis alle Unterverzeichnisse zu ermitteln und in das Listenfeld einzufügen. Damit das möglichst effizient erfolgt, wird vorher BeginUpdate und anschließend EndUpdate ausgeführt. ReadAllDirectories testet, ob der übergebene Listeneintrag bereits Untereinträge besitzt. Wenn das nicht der Fall ist, werden mit der oben abgedruckten Prozedur ReadDirectories alle Unterverzeichnisse eingelesen. Anschließend wird ReadAllDirectories rekursiv für alle Unterverzeichnisse aufgerufen. Label1.Refresh bewirkt, dass das Labelfeld mit der Anzahl
der Listeneinträge kontinuierlich aktualisiert wird. Damit erhält der Anwender ein stetes Feedback, dass das Programm noch arbeitet und nicht etwa abgestürzt ist. ' gesamte Verzeichnisstruktur einlesen Private Sub Button2_Click(...) Handles Button2.Click Dim tn As TreeNode = TreeView1.SelectedNode If Not IsNothing(tn) Then Me.Cursor = Cursors.WaitCursor TreeView1.BeginUpdate() ReadAllDirectories(tn) TreeView1.EndUpdate() Me.Cursor = Cursors.Default End If End Sub
660
14 Steuerelemente
Private Sub ReadAllDirectories(ByVal tn As TreeNode) Dim subnode As TreeNode If tn.Nodes.Count = 0 Then ReadDirectories(tn) Label1.Refresh() ' ReadAllDirectories für alle Unterverzeichnisse aufrufen For Each subnode In tn.Nodes ReadAllDirectories(subnode) Next End Sub
14.6.8 DataGrid Das DataGrid-Steuerelement zählt zu den komplexesten Steuerelementen, die der Windows.Forms-Namensraum anzubieten hat. Es dient dazu, tabellarische Daten anzuzeigen und zu verändern. Allerdings ist das DataGrid-Steuerelement insbesondere zur Bearbeitung von Daten optimiert, die durch die Klassen DataTable, DataView und DataSet dargestellt werden. Diese Klassen aus dem Namensraum System.Data sind ein Bestandteil von ADO.NET und werden normalerweise in Datenbankanwendungen eingesetzt. Da ich die Themen ADO.NET bzw. Datenbanken aus Platzgründen in einem eigenen Buch behandle, fehlt hier eine Basis für die fundierte Beschreibung des DataGrid-Steuerelements. Das folgende Beispiel (siehe Abbildung 14.27) zeigt daher nur einen Sonderfall für die Anwendung des Steuerelements. Dabei stammen die Daten nicht aus einer Datenbank, sondern aus einem DataTable-Objekt, das per Code mit fünf Spalten und zehn Zeilen initialisiert wird. In die Felder dieser Tabelle werden Zufallszahlen zwischen 0 und 100 eingefügt. Der Anwender kann diese Werte ändern, löschen, Texte eingeben etc. Das Programm beweist, dass sowohl das DataTable-Steuerelement als auch die DataTable-Klasse für einfache Anwendungen unkompliziert zu bedienen ist.
Abbildung 14.27: Der Tabelleninhalt wird durch ein DataTable-Objekt verwaltet
14.6 Listenfelder
661
Form1_Load dient primär zur Initialisierung der DataTable-Variable. Der Code sollte selbst
ohne ADO.NET-Hintergrundwissen auf Anhieb verständlich sein. Um die Tabelle im Steuerelement anzuzeigen, wird die DataTable-Variable einfach der DataSource-Eigenschaft zugewiesen. Button1_Click zeigt den Inhalt der Tabelle in einer MsgBox an. Die Prozedur beweist, dass die Auswertung der Tabelle ebenso unkompliziert ist. ' Beispiel steuerelemente\datagrid-test Dim dt As New DataTable() Private Sub Form1_Load(...) Handles MyBase.Load Dim dr As DataRow Dim i, j As Integer Dim columns As Integer = 5 Dim rows As Integer = 10 ' Spalten der Tabelle erzeugen und beschriften For i = 1 To columns dt.Columns.Add(New DataColumn("Spalte" & i.ToString(), _ GetType(String))) Next i ' Zeilen der Tabelle erzeugen und initialisieren For i = 1 To rows dr = dt.NewRow() For j = 1 To columns dr(j - 1) = CInt(Rnd() * 100) Next dt.Rows.Add(dr) Next i DataGrid1.DataSource = dt End Sub ' den Inhalt der DataTable in einer MsgBox anzeigen Private Sub Button1_Click(...) Handles Button1.Click Dim i As Integer Dim s As String Dim row As Data.DataRow For i = 0 To dt.Rows.Count - 1 row = dt.Rows(i) s += "Zeile " + i.ToString + ": " + _ Join(row.ItemArray, "; ") + vbCrLf Next MsgBox(s) End Sub
662
14.7
14 Steuerelemente
Datums- und Zeiteingabe
Die Steuerelemente MonthCalender und DateTimePicker helfen bei der benutzerfreundlichen Eingabe bzw. Auswahl von Daten und Zeiten. Beide Steuerelemente sind direkt von Control abgeleitet, basieren aber intern offensichtlich dennoch auf einem gemeinsamen Code. So erscheint beim Ausklappen des DateTimePicker diesselbe Datumsansicht wie beim MonthCalender.
Abbildung 14.28: Links der MonthCalender, rechts einige Varianten des DateTimePickers
14.7.1 MonthCalender Das MonthCalender-Steuerelement zeigt einen Kalender für einen oder mehrere Monate an. Mit den Pfeilbuttons können Sie monateweise vor- und zurückblättern (siehe Abbildung 14.28).
Gestaltung Farben: Mit ForeColor, BackColor, TitleForeColor, TitleBackColor und TrailingForeColor können die meisten im Steuerelement sichtbaren Farben eingestellt werden. (TrailingForeColor gibt die
Textfarbe für die Tage vor dem Monatsanfang bzw. nach dem Monatsende an.) CalenderDimension.Width und .Height geben die Größe des Kalenderfelds an. Per Default
sind beide Werte 1, in Abbildung 14.28 wurden beide Werte auf 2 gestellt, um vier Monate gleichzeitig darzustellen.
14.7 Datums- und Zeiteingabe
663
FirstDayOfWeek gibt an, welcher Wochentag bei der Monatsansicht in der ersten Spalte angezeigt wird. Per Default wird hier die Systemeinstellung berücksichtigt, Sie können aber auch explizit einen Wochentag angeben (z.B. Sonntag oder Montag). ShowToday gibt an, ob unterhalb der Monatsansicht das aktuelle Datum angezeigt werden soll. ShowTodayCircle gibt an, ob der heutige Tag im Kalender durch einen roten Kreis hervorgehoben werden soll. ShowWeekNumbers gibt an, ob rechts von jeder Woche auch
die Wochennummer (Kalenderwoche) angezeigt werden soll. Bei der Berechnung der ersten Kalenderwoche wird FirstDayOfWeek berücksichtigt. Mit den Eigenschaften BoldedDates, AnnuallyBoldedDates und MonthlyBoldedDates können Sie angeben, welche Tage im Kalender durch eine fette Schrift hervorgehoben werden sollen. Die folgenden Anweisungen bewirken, dass der 1.1. und der 25.12. jedes Jahr, der 31.3. aber nur im Jahr 2002 fett dargestellt werden. Dim yearly() As Date = {#1/1/2002#, #12/25/2002#} Dim exact() As Date = {#3/31/2002#} MonthCalendar1.AnnuallyBoldedDates = yearly MonthCalendar1.BoldedDates = exact
Mit diversen Add- und Remove-Methoden können Sie die fette Markierung selektiv ergänzen bzw. reduzieren. UpdateBoldedDates aktualisiert anschließend die Darstellung der Monatstage.
Verhalten, Auswertung MinDate und MaxDate grenzen den Bereich ein, aus dem das Datum ausgewählt werden
kann. MaxSelectionCount gibt an, wie viele aufeinander folgende Tage gleichzeitig markiert wer-
den dürfen (per Default 7, also eine Woche). Auf diese Weise können Sie eine ganze Zeitspanne markieren (z.B. einen Urlaub). Den ausgewählten Zeitbereich können Sie den Eigenschaften SelectionStart und -End entnehmen. (Beide Eigenschaften liefern Date-Objekte für das Datum, wobei der Zeitanteil jeweils 00:00 ist.) Wenn Sie die Markierung von Zeitbereichen deaktivieren möchten, setzen Sie MaxSelectionCount auf 1. In diesem Fall kann das markierte Datum aus Value gelesen werden. Während jeder Veränderungen des markierten Datumsbereichs treten DateChanged-Ereignisse auf. Zum Abschluss der Markierung tritt außerdem ein DateSelected-Ereignis auf.
14.7.2 DateTimePicker Das DateTimePicker-Steuerelement erscheint per Default als ausklappbares Listenfeld zur Datumseingabe. Wie Abbildung 14.28 beweist, gibt es daneben eine Menge anderer Gestaltungsvarianten. Im ausgeklappten Zustand erscheint eine Monatsansicht wie bei MonthCalender, wobei allerdings die zahlreichen MonthCalender-spezifischen Eigenschaften zur Gestaltung der Monatsansicht nicht zur Verfügung stehen.
664
14 Steuerelemente
Gestaltung Format gibt an, wie das Datum oder die Uhrzeit formatiert werden sollen. Zur Auswahl stehen Long (Datum ausführlich), Short (Datum kompakt), Time (Uhrzeit) oder Custom. Bei der letzten Variante geben Sie das gewünschte Format durch CustomFormat an. Die Eigen-
schaft erwartet eine Zeichenkette mit den in Abschnitt 8.5.3 beschriebenen .NETFormatcodes für Datum und Zeit. Das vierte DateTimePicker-Beispiel in Abbildung 14.28 verwendet zur Formatierung die Einstellung "MM-dd-yyyy". ShowUpDown = True bewirkt, dass statt des Dropdown-Pfeils zwei Pfeile zum Vergrößern
bzw. Verkleinern des aktuellen Datums angezeigt werden. Das Steuerelement ist dann wie ein Drehfeld (NumericUpDown-Steuerelement) zu bedienen. ShowUpDown sollte unbedingt auf True gesetzt werden, wenn im Steuerelement eine Zeit (also kein Datum) angezeigt wird. Die Monatsansicht ist nämlich zur Zeitangabe ungeeignet. ShowCheckBox bewirkt, dass neben dem Datum ein Auswahlkästchen angezeigt wird. Der Zustand dieses Kästchens kann über Checked gelesen bzw. verändert werden.
Verhalten, Auswertung MinDate und MaxDate grenzen den Bereich ein, aus dem das Datum ausgewählt werden
kann. Nach der Auswahl eines Datums bzw. einer neuen Zeit tritt ein ValueChanged-Ereignis auf. Bevor die Monatsansicht ausgeklappt wird bzw. nachdem sie wieder eingeklappt wurde, treten die Ereignisse DropDown und CloseUp auf. Das ausgewählte Datum bzw. die Zeit kann aus der Value-Eigenschaft gelesen werden. Dabei ist allerdings eine gewisse Vorsicht angebracht. Das Steuerelement fügt zum sichtbaren Datum unsichtbare Zeitanteile hinzu (bzw. umgekehrt): •
Bei der Auswahl eines Datums wird als Zeitanteil die Uhrzeit verwendet, zu der das Programm gestartet wurde. Wenn Sie also beispielsweise das Programm um 9:30:37 starten und das Datum 31.12.2002 auswählen, dann enthält Value den Wert #12/31/2002 9:30:37 AM#. Um das reine Datum zu ermitteln, verwenden Sie Value.Date.
•
Auch wenn im Steuerelement nur eine Uhrzeit angezeigt wird (Format=Time), enthält Value das Startdatum. Wenn Sie nur die Uhrzeit auswerten möchten, müssen Sie Value.TimeOfDay verwenden.
•
Vergleichbare Effekte treten auch bei benutzerdefinierten Formaten auf. Wenn Sie beispielsweise das Format HH:mm zur Einstellung einer Uhrzeit verwenden, dann wird zur ausgewählten Zeit das Startdatum und die Startsekunde hinzugefügt. Wenn das Programm am 1.1.2003 um 8:30:15 gestartet wurde und die Zeit 7:45 eingestellt wurde, enthält Value #1/1/2003 7:45:15 AM#. Die folgende Formel errechnet daraus ein TimeSpanObjekt, das exakt 7:45 enthält: DateTimePicker1.Value.AddSeconds(-DateTimePicker1.Value.Second).TimeOfDay()
14.8 Schiebe- und Zustandsbalken, Drehfelder
14.8
665
Schiebe- und Zustandsbalken, Drehfelder
Dieser Abschnitt beschreibt eine Gruppe von Steuerelementen, die vollkommen unterschiedlich aussehen. Ihr gemeinsames Merkmal besteht darin, dass sie bei der Einstellung oder bei der Anzeige eines Werts helfen. Der folgende Hierarchiekasten gibt an, wo sich die Steuerelemente in der Windows.Forms-Klassenhierarchie befinden. Klassenhierarchie im System.Windows.Forms-Namensraum ...
└─ Control ├─ ProgressBar ├─ ScrollBar │ ├─ HScrollBar │ └─ VScrollBar ├─ ScrollableControl │ └─ ContainerControl │ └─ UpDownBase │ ├─ DomainUpDown │ └─ NumericUpDown └─ TrackBar
Basisklasse für alle Steuerelemente Zustandanzeige Basisklasse für Schiebebalken horizontaler Schiebebalken vertikaler Schiebebalken Basisklasse für automatisches Scrolling Basisklasse zur Verwaltung des Eingabefokus Basisklasse für Drehfelder Drehfeld zur Listenauswahl Drehfeld zur Zahleneingabe Schieberegler
14.8.1 HScrollBar, VScrollBar Die beiden von der ScrollBar-Klasse abgeleiteten Steuerelemente HScrollBar und VScrollBar dienen zur Darstellung eines horizontalen bzw. vertikalen Schiebebalkens. Mit dem Balken kann ein Wert (Eigenschaft Value) zwischen Min und Max eingestellt werden. Durch das Anklicken der Pfeile ändert sich der Wert um SmallChange, beim seitenweisen Scrollen um LargeChange. Während der Schiebebalken mit der Maus bewegt wird, treten MoveEreignisse auf. Wenn die Einstellung abgeschlossen wird, tritt außerdem ein ChangeEreignis auf.
Abbildung 14.29: Farbeinstellung mit drei Schiebebalken
666
14 Steuerelemente
Beispiel Mit dem in Abbildung 14.29 dargestellten Beispielprogramm können Sie mit drei Schiebebalken die Farbe eines PictureBox-Steuerelements einstellen. Bei den drei Schiebebalken gilt Min=0, Max=255, SmallChange=1 und LargeChange=16. Der Programmcode ist denkbar einfach und sollte ohne weitere Erklärungen verständlich sind. ' Beispiel steuerelemente\scrollbars Private Sub VScrollBar1/2/3_Move(...) Handles VScrollBar1/2/3.Move ChangeColor() End Sub Private Sub VScrollBar1/2/3_Scroll(...) Handles VScrollBar1/2/3.Scroll ChangeColor() End Sub Private Sub ChangeColor() Dim r, g, b As Integer r = VScrollBar1.Value g = VScrollBar2.Value b = VScrollBar3.Value PictureBox1.BackColor = Color.FromArgb(r, g, b) End Sub
14.8.2 TrackBar Das TrackBar-Steuerelement ist eine optische Variante zu den H/VScrollBar-Steuerelementen. Value enthält je nach Einstellung des Schiebers einen Wert zwischen Minimum und Maximum. Orientation gibt an, ob das Schieberegler horizontal oder vertikal zu bedienen ist. TickStyle ermöglicht die Auswahl zwischen verschiedenen Gestaltungsformen für den Regler. TickFrequency gibt an, an den wievielten Einheiten ein Skalenstrich angezeigt wird. (Damit ist die Eigenschaft natürlich vollkommen falsch benannt – TickPeriod wäre mathematisch zutreffender.) Während der Bewegung des Reglers treten Move-Ereignisse auf, zum Abschluss ein Scroll-Ereignis.
Abbildung 14.30: TrackBar-Beispielprogramm
14.8 Schiebe- und Zustandsbalken, Drehfelder
667
Beispiel Bei den beiden TrackBar-Steuerelementen des Beispielprogramms gilt jeweils Maximum=100, TickFrequency=5, LargeChange=10 und TickStyle=TopLeft. In den Scroll- und Move-Ereignisprozeduren wird durch Invalidate ein Neuzeichnen des PictureBox-Steuerelements ausgelöst. In dessen Paint-Ereignisprozedur werden einige konzentrische Kreise gezeichnet, wobei sich der Mittelpunkt des Kreises aus den TrackBar-Einstellungen ergibt. Die etwas komplizierte Berechnung für y0 ergibt sich daraus, dass y=0 in der PictureBox oben ist, die Einstellung Value=0 im TrackBar aber unten bedeutet. (Ausführliche Hintergrundinformationen zur Grafikprogrammierung, zur Graphics-Klasse und zur Paint-Ereignisprozedur finden Sie in Kapitel 16.) ' Beispiel steuerelemente\trackbar Private Sub TrackBar1/2_Move(...) Handles TrackBar1.Move PictureBox1.Invalidate() End Sub Private Sub TrackBar1/2_Scroll(...) Handles TrackBar1.Scroll PictureBox1.Invalidate() End Sub Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim x0, y0, r As Integer Dim gr As Graphics = e.Graphics x0 = PictureBox1.ClientSize.Width * TrackBar1.Value \ 100 y0 = CInt(PictureBox1.ClientSize.Width * _ (1.0 - TrackBar2.Value / 100.0)) For r = 5 To 125 Step 4 gr.DrawEllipse(Pens.Black, x0 - r, y0 - r, 2 * r, 2 * r) Next End Sub
14.8.3 ProgressBar Das ProgressBar-Steuerelement dient dazu, den Fortschritt einer länger andauernden Aktion anzuzeigen. Die Größe des Zustandsbalkens wird durch Value eingestellt, wobei der Wert zwischen Minimum und Maximum liegen muss (per Default 0 und 100). Statt Value direkt zu ändern, können Sie auch die Methode PerformStep ausführen. Value wird dann um den Wert Step erhöht (per Default 10).
Abbildung 14.31: Zustandsanzeige
668
14 Steuerelemente
Private Sub Button1_Click(...) Handles Button1.Click Dim i As Integer Me.Cursor = Cursors.WaitCursor For i = 1 To 100 Threading.Thread.Sleep(100) '100 ms warten ProgressBar1.Value = i Next ProgressBar1.Value = 0 Me.Cursor = Cursors.Default End Sub
14.8.4 NumericUpDown Das NumericUpDown-Steuerelement (kurz Drehfeld) ist eine Kombination aus einem Textfeld und einem verkleinerten Schiebebalken (siehe Abbildung 14.32). Es erleichtert die Eingabe einer Zahl zwischen Minimum und Maximum: Die Zahl kann durch Anklicken der beiden Pfeile bzw. mit den Cursortasten um den Wert Increment (per Default 1) vergrößert und verkleinert werden. Per Default ist die Eingabe der Zahl auch über die Tastatur möglich, das kann aber mit ReadOnly = True verhindert werden.
Abbildung 14.32: NumericUpDown- und DomainUpDown-Steuerelement
Die Zahlendarstellung kann optional in hexadezimaler Schreibweise (Hexadecimal= True), mit Tausenderstellen (ThousandsSeparator = True) oder mit einer vorgegebenen Anzahl von Nachkommastellen erfolgen (DecimalPlaces = n). Der ausgewählte Wert kann aus den Eigenschaften Value oder Text gelesen werden. Bei jeder Änderung von Value durch ein Anklicken der Buttons oder durch die Verwendung der Cursortasten tritt ein ValueChanged-Ereignis auf. Bei Veränderungen durch die Tastatur tritt dagegen nur ein TextChanged-Ereignis auf, obwohl sich Value natürlich auch in diesem Fall ändert.
Validierung Bei der Zahleneingabe per Tastatur ist die Eingabe von Buchstaben unmöglich. Allerdings kann der Anwender Zahlen eingeben, die sich außerhalb des durch Minimum und Maximum definierten Zahlenbereichs befinden. Wenn Sie das vermeiden möchten, müssen Sie eine Validating-Ereignisprozedur einfügen.
14.9 Gruppierung von Steuerelementen
669
Private Sub NumericUpDown1_Validating(...) Handles _ NumericUpDown1.Validating With NumericUpDown1 If .Value > .Maximum Then .Value = .Maximum If .Value < .Minimum Then .Value = .Minimum End With End Sub
14.8.5 DomainUpDown Das DomainUpDown-Steuerelement (siehe Abbildung 14.32) ist eine Mischung aus dem NumericUpDown- und dem ComboBox-Steuerelement (siehe Abschnitt 14.6.3). Mit den Pfeilen kann ein Element aus einer Liste ausgewählt werden. Per Tastatur kann ein beliebiger Text eingegeben werden (es sei denn, ReadOnly wird auf True gestellt). Die Listenelemente können im Eigenschaftsfenster über die Items-Eigenschaft eingegeben und mit Sorted sortiert werden. Intern verweist Items auf ein Objekt der Klasse DomainUpDown.DomainUpDownItemCollection, Items(n) auf ein allgemeines Objekt (Klasse Object). SelectedItem verweist auf das zuletzt ausgewählte Element, SelectedIndex gibt dessen Indexnummer an. Bei der Auswahl einen Listenelements tritt ein SelectedItemChanged-Ereignis auf, bei der Texteingabe ein TextChanged-Ereignis. Beim Programmstart wird die Text-Eigenschaft nicht verändert. Wenn Sie möchten, dass dort der Text des ersten Listenelements angezeigt wird, führen Sie die folgende Anweisung aus:
VERWEIS
DomainUpDown1.SelectedIndex = 0
Wenn Sie ein Drehfeld zur Datums- oder Zeiteingabe benötigen, können Sie dazu das DateTimePicker-Steuerelement mit der Einstellung ShowUpDown=True verwenden (siehe Abschnitt 14.7.2).
14.9
Gruppierung von Steuerelementen
Mit den Steuerelementen GroupBox, Panel und TabControl können Sie mehrere Steuerelemente zu einer Gruppe zusammenfassen. Das ist oft aus optischen Gründen sinnvoll, etwa um Dialoge mit vielen Steuerelementen in übersichtliche Teile zu organisieren. GroupBox-Steuerelemente werden aber häufig auch dazu verwendet, um Optionsfelder (RadioButtons) logisch zu gruppieren, so dass in jeder Gruppe unabhängig von den anderen Gruppen eine Option ausgewählt werden kann. Auch wenn die Steuerelemente nicht dieselbe Basis haben (siehe den folgenden Kasten mit der Klassenhierarchie), zeichnen Sie sich durch eine Menge gemeinsamer Eigenschaften aus. Die enthaltenen Steuerelemente können über die Controls-Aufzählung manipuliert
670
14 Steuerelemente
werden. Darüber hinaus erfolgt die Positionierung der enthaltenen Steuerelemente (SizeEigenschaft) in einem lokalen Koordinatensystem, dessen Nullpunkt das linke obere Eck des GroupBox- oder Panel-Steuerelements ist. Klassenhierarchie im System.Windows.Forms-Namensraum ...
└─ Control ├─ GroupBox ├─ ScrollableControl │ │ │ └─ Panel │ └─ TabPage └─ TabControl
Basisklasse für alle Steuerelemente Steuerelemente gruppieren Basisklasse für Steuerelemente, die automatisches Scrolling des Inhalts erlauben Steuerelemente gruppieren eine Seite eines mehrblättrigen Dialogs mehrblättriger Dialog
14.9.1 GroupBox Per Default zeichnet sich das GroupBox-Steuerelement durch einen 3D-Rahmen mit Beschriftung aus. Die Beschriftung kann auch zur Definition einer Tab-Abkürzung verwendet werden, um den Eingabefokus rasch in das erste Steuerelement der Gruppe zu bewegen.
14.9.2 Panel Das Panel-Steuerelement ist per Default unsichtbar. Durch die Veränderung der BorderStyle-Eigenschaft können Sie das Steuerelement mit einem Rand ausstatten. Die eigentliche Besonderheit des Steuerelements besteht darin, dass es durch AutoScroll=True mit ScrollBalken ausgestattet werden kann. Das ist dann sinnvoll, wenn die enthaltenen Steuerelemente größer sind als das Panel-Feld. Mit den Scroll-Balken kann dann der sichtbare Ausschnitt ausgewählt werden.
Abbildung 14.33: Eine Menge Optionsfelder, die in drei Gruppen angeordnet sind
14.9 Gruppierung von Steuerelementen
671
Das Panel-Steuerelement kann nicht beschriftet werden (d.h., es gibt keine Text-Eigenschaft). Sie können aber natürlich ein Panel-Steuerelement in ein GroupBox-Steuerelement einfügen.
Beispiel Um Abbildung 14.33 zu erzeugen, wurden die Optionsfelder RadioButton1 bis -4 manuell während des Programmentwurfs in die beiden GroupBox-Felder eingefügt. Zwanzig weitere Optionsfelder werden beim Programmstart in Form1_Load mit Controls.Add in das Panel-Feld eingefügt. Damit auch bei diesen dynamisch erzeugten Steuerelementen Ereignisse empfangen zu können, wird jedem neuen Steuerelement mit AddHandler die Ereignisprozedur PanelRadioButtons_CheckedChanged zugewiesen. (Grundlagen zum dynamischen Erzeugen von Steuerelementen finden Sie in Abschnitt 14.11.2.) ' Beispiel steuerelemente\groupbox-panel Private Sub Form1_Load(...) Handles MyBase.Load Dim i As Integer Dim rb As RadioButton For i = 0 To 19 rb = New RadioButton() rb.Name = "RadioButton" + (i + 5).ToString rb.Text = "RadioButton" + (i + 5).ToString rb.Location = New Point(10, 10 + i * 30) rb.Size = New Size(200, 20) rb.Visible = True AddHandler rb.CheckedChanged, _ AddressOf PanelRadioButtons_CheckedChanged Panel1.Controls.Add(rb) Next End Sub Private Sub PanelRadioButtons_CheckedChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs)
VERWEIS
Dim rb As RadioButton = CType(sender, RadioButton) If rb.Checked Then MsgBox(rb.Text + " wurde ausgewählt") End If End Sub
In Abschnitt 16.5.8 finden Sie ein Beispiel zur Grafikprogrammierung, in dem die AutoScroll-Eigenschaft eines Panel-Steuerelements dazu verwendet wird, den sichtbaren Ausschnitt eines großen PictureBox-Steuerelements einzustellen.
672
14 Steuerelemente
14.9.3 TabControl (Dialogblätter) Das TabControl-Steuerelement hilft dabei, mehrblättrige Dialoge zu schaffen. Die Verwendung des Steuerelements ist denkbar einfach: Sie fügen das Steuerelement in Ihr Formular ein und ändern im Eigenschaftsfenster die TabPages-Eigenschaft: In einem eigenen Dialog können Sie beliebig viele Dialogblätter einfügen und benennen. Anschließend können Sie in der Entwicklungsumgebung eines der Blätter auswählen und darin Steuerelemente einfügen. Intern werden die einzelnen Dialogblätter als Objekte der TabPage-Klasse verwaltet. Zu den einzelnen Blätter gelangen Sie über die bereits erwähnte TabPages-Eigenschaft. SelectedTab verweist auf das gerade aktive Dialogblatt, SelectedIndex gibt dessen Indexnummer (für TabPages(n)) an, TabCount die Anzahl der Dialogblätter.
Abbildung 14.34: Ein einfacher mehrblättriger Dialog
Gestaltung des gesamten Steuerelements Alignment steuert, wo die Tabellenreiter angezeigt werden. Zur Auswahl steht oben (per Default), unten, links oder rechts. Appearance gibt an, in welcher Form die Reiter dargestellt werden. Zur Auswahl stehen Normal (wie in Abbildung 14.34), Buttons oder FlatButtons. MultiLine bestimmt, ob die Tabellenreiter in mehreren Zeilen angeordnet werden können. Die Aufteilung erfolgt bei Platzmangel automatisch. RowCount gibt dann an, wieviele Zeilen die Beschriftung der Blätter beansprucht. (RowCount kann nur gelesen, nicht verändert werden.) Per Default gilt MultiLine=False. Wenn der Platz nicht ausreicht, um alle Reiter gleichzeitig nebeneinander anzuzeigen, werden automatisch Scroll-Pfeile eingeblendet. SizeMode gibt an, wie die Größe der einzelnen Reiter bestimmt wird: In der Defaulteinstellung Normal sind die Reiter gerade so groß, dass der darin dargestellte Text Platz findet. Fixed bewirkt, dass jeder Reiter exakt gleich groß ist. (Die Größe wird durch ItemSize eingestellt.) Die Einstellung FillToRight ist nur dann relevant, wenn die Reiter über mehrere
14.9 Gruppierung von Steuerelementen
673
Zeilen verteilt sind. In diesem Fall wird die Größe der einzelnen Reiter so gewählt, dass jeweils die gesamte Zeile ausgefüllt wird. Die Werte Padding.X und .Y geben an, wie viele Pixel der Beschriftungstext der Dialogblätter vom Rand der Tabellenreiter eingerückt sein soll.
Gestaltung einzelner Tabellenreiter Für jedes einzelne Dialogblatt können Sie im TABPAGE-Eigenschaftsdialog eine Menge Eigenschaften einstellen. (Die folgenden Eigenschaften beziehen sich also auf die TabPageKlasse!) Die folgenden Eigenschaften betreffen die Gestaltung des Tabellenreiters: •
Text gibt den Text an, der im Tabellenreiter angezeigt wird.
•
ImageIndex gibt an, welche Bitmap aus dem ImageList-Steuerelement links vom Beschriftungstext angezeigt werden soll. (Damit das funktioniert, muss vorher die ImageListEigenschaft des TabControl-Steuerelements eingestellt werden, so dass sie auf ein ImageList-Steuerelement mit Bitmaps verweist.)
•
ToolTipText gibt einen Erklärungstext an, der erscheint, wenn die Maus längere Zeit über dem Tabellenreiter steht. (Damit das funktioniert, muss die ShowToolTips-Eigenschaft des TabControl-Steuerelements auf True gestellt werden.)
Falls Ihnen diese Eigenschaften zu wenig Gestaltungsfreiraum geben, können Sie die Tabellenreiter auch komplett selbst zeichnen. Dazu setzen Sie die Eigenschaft DrawMode des TabControl-Steuerelements auf OwnerDrawFixed und fügen in die DrawItem-Ereignisprozedur den erforderlichen Code zum Zeichnen der Reiter ein. (Die prinzipielle Vorgehensweise ist in Abschnitt 14.6.1 näher erläutert, wo die Einträge eines Listenfelds selbst gezeichnet werden. Dort finden Sie auch ein Beispiel für eine DrawItem-Ereignisprozedur.)
Gestaltung des Innenbereichs der Dialogblätter Eine Reihe weiterer Eigenschaften steuert das Aussehen des Innenbereichs des Dialogblatts: •
ForeColor, BackColor und Font bestimmen Vor- und Hintergrundfarbe und die Schriftart.
•
BorderStyle gibt an, ob und wie der Innenbereich umrandet wird.
•
AutoScroll gibt wie beim Panel-Steuerelement an, ob der Innenbereich automatisch mit
Scroll-Balken ausgestattet werden soll, wenn die darin enthaltenen Steuerelemente sonst keinen Platz mehr finden. Falls die Größe Ihres Dialogfensters nicht variabel ist, sollten Sie diese Eigenschaft auf True setzen: der Platzbedarf der Steuerelemente ist nämlich auch von der DPI-Systemeinstellung abhängig (siehe Abschnitt 15.2.9), und nichts ist lästiger für den Anwender als Dialoge, die wegen der Verwendung augenfreundlicher (also großer) Schriften nur teilweise sichtbar sind.
674
14 Steuerelemente
14.10 Spezielle Steuerelemente Dieser Abschnitt beschreibt einige Steuerelemente, die in keine der anderen Gruppen dieses Kapitels passen: Splitter (Fensterteiler), Timer (Zeitgeber), ToolTip (gelber Hinweistext), HelpProvider (Anzeige kontextspezifischer Hilfe), ErrorProvider (Indikator für Eingabefehler) und NotifyIcon (Anzeige eines Icons in der Taskleiste).
14.10.1 Splitter Das Steuerelement stellt eine per Maus verschiebbare Linie dar. Es wird dazu verwendet, um den Innenbereich eines Fensters in zwei (oder mehr) unterschiedlich große Bereiche zu unterteilen. Das Steuerelement wird häufig zwischen einem hierarchischen Listenfeld und der dazugehörenden Detailansicht angeordnet (z.B. im Windows-Explorer). Auch der EMail-Client Outlook ist ein gutes Beispiel für die Anwendung von Splitter-Steuerelementen: hier sind üblicherweise gleich drei Bereiche zu sehen (links die Verzeichnisse, rechts oben die Liste der E-Mails im ausgewählten Verzeichnis, rechts unten der Text einer E-Mail). Das Splitter-Steuerelement ist leider recht wählerisch, was die Voraussetzungen für seine Anwendung betrifft: •
Der Splitter kann nur dazu verwendet werden, um die Größe von angedockten Steuerelementen zu verändern. (Das liegt daran, dass auch das Splitter-Element selbst immer angedockt sein muss. Die Einstellung Dock=None ist unzulässig.) Im Regelfall sollte das Steuerelement auf der einen Seite des Splitters mit Dock=Left, Right, Bottom oder Up an einem Fensterrand angedockt sein. Das Steuerelement an der anderen Seite sollte mit Dock=Fill den Rest des Fensters füllen. (Wenn Sie nicht das ganze Fenster füllen möchten, fügen Sie einfach beide Steuerelemente sowie den Splitter in ein Panel-Steuerelement ein!)
•
Beim Einfügen der Steuerelemente in das Formular spielt die Reihenfolge eine wichtige Rolle. Der Splitter sollte unmittelbar nach dem Steuerelement eingefügt werden, das an einen Fensterrand gedockt ist. Genau genommen ist nicht die Einfügereihenfolge entscheidend, sondern der so genannte Z-Order-Wert. Dieser Wert gibt an, welches Steuerelement welches andere überdeckt (sofern sich die Steuerelemente überlappen). Damit der Splitter rechts von einem mit Dock=Left angedockten Steuerelement erscheint, muss der Splitter in der ZReihenfolge über dem Steuerelement liegen. Wenn das nicht der Fall ist, erscheint der Splitter links vom Steuerelement, wo er aber wirkungslos ist. Die Lösung besteht nun darin, entweder das Steuerelement mit der rechten Maustaste IN DEN HINTERGRUND zu bewegen oder den Splitter über das Kontextmenü IN DEN VORDERGRUND zu bewegen. Noch einfacher ist es aber, die Steuerelemente gleich in der richtigen Reihenfolge einzufügen – dann stimmt die Z-Reihenfolge automatisch.
14.10 Spezielle Steuerelemente
•
675
TIPP
Das Splitter-Steuerelement ist per Default selbst gedockt, und zwar mit Dock=Left. Wenn Sie mit dem Splitter die Größe eines an den rechten Fensterrand gedockten Steuerelements verändern möchten, müssen Sie Dock=Right einstellen. Um eine horizontale Teilung zwischen zwei Steuerelementen zu erreichen, müssen Sie Dock auf Top oder Bottom stellen (je nachdem, wie das anliegende Steuerelement gedockt ist). Grundsätzlich ist es auch möglich, ein Fenster mit drei Steuerelementen A, B und C durch zwei Splitter zu unterteilen. (Naturgemäß lässt sich die Komplexität des Teilungsschemas mit noch mehr Steuerelementen und Splittern weiter steigern.) Allerdings treten dabei oft Probleme auf, die einerseits mit der bereits erwähnten ZReihenfolge der Steuerelemente und andererseits mit der richtigen Einstellung der Dock-Eigenschaft aller betroffenen Steuerelemente zu tun hat. Bevor Sie Stunden damit vergeuden, diese Eigenschaften richtig einzustellen, ist es viel einfacher, ein Panel-Steuerelement zu Hilfe zu nehmen. Dazu führen Sie zuerst eine Zweiteilung vor (z.B. links das Steuerelement A mit Dock=Left, rechts ein PanelSteuerelement). Anschließend fügen Sie in das Panel die Steuerelemente B mit Dock=Up ein, dann den zweiten Splitter mit Dock=Up und schließlich C mit Dock=Fill.
Aussehen und Verhalten •
Per Default ist der Splitter nur drei Pixel breit. Wenn Sie ein breiteres Steuerelement möchten, verändern Sie die Size-Eigenschaft (am besten direkt im Eigenschaftsfenster).
•
BorderStyle bestimmt das Aussehen des Splitter-Randes. Per Default wird der Rand gar
nicht angezeigt, Sie können aber auch einen zwei- oder dreidimensionalen Rand verwenden. •
MinSize gibt an, wie klein das neben dem Splitter angedockte Steuerelement werden darf (per Default 25 Pixel breit oder hoch).
HINWEIS
MinExtra gibt an, wie klein das auf der anderen Seite des Splitters befindliche Steuerelement (mit Dock=Fill) werden darf (per Default ebenfalls 25 Pixel).
Probleme mit dem Splitter gibt es oft, wenn die Größe des Fensters verändert wird. Die Position des Splitters wird in diesem Fall nicht automatisch verändert! Das kann dazu führen, dass einzelne Bereiche des Fensters ganz unsichtbar werden. Die MinXxx-Eigenschaften werden nicht berücksichtigt. Die einzige Abhilfe besteht darin, die minimale Fenstergröße zu limitieren und die Splitter-Position in der Resize- oder Layout-Ereignisprozedur des Fensters zu überwachen und gegebenenfalls zu verändern.
676
14 Steuerelemente
Ereignisse Während der Splitter mit der Maus bewegt wird, treten zahlreiche SplitterMoving-Ereignisse auf. Die Größe der Steuerelemente wird allerdings zu diesem Zeitpunkt noch nicht verändert. Wenn Sie möchten, dass das Ergebnis der Größenänderung dynamisch sichtbar wird, müssen Sie den erforderlichen Code selbst schreiben. (Dabei müssen Sie selbst darauf achten, dass die MinXxx-Eigenschaften beachtet werden!) Die dynamische Veränderung der Steuerelemente führt allerdings meist dazu, dass die Splitter-Linie zum Schluss nicht mehr korrekt angezeigt wird. Abhilfe schafft die Zeile Me.Refresh in der SplitterMoved-Ereignisprozedur. Wenn der Anwender die Maus loslässt, tritt ein SplitterMoved-Ereignis auf. Anschließend wird die Größe der Steuerelemente auf beiden Seiten des Splitters geändert. In beiden Ereignisprozeduren geben e.X und e.Y die Mausposition sowie e.SplitX und e.SplitY die Position des Splitters an. SplitX und SplitY können bei Bedarf auch verändert werden, beispielsweise um die Teilungsposition einzuschränken.
Beispielprogramm Das Beispielprogramm besteht aus zwei Fenstern, die jeweils einige Label-Felder enthalten, deren Größe durch Splitter eingestellt werden kann (siehe Abbildung 14.35).
Abbildung 14.35: Splitter-Beispielprogramm
Fenster 1: Das Fenster SPLITTER-TEST 1 zeigt die einfachste Anwendung des Splitters: das erste Steuerelement ist an den linken Fensterrand gedockt, das zweite Steuerelement füllt den Rest des Fensters aus. Um die Trennlinie zwischen den beiden Steuerelementen verschiebbar zu machen, gehen Sie wie folgt vor: •
Sie fügen Steuerelement A ein und stellen Dock=Left ein.
•
Sie fügen den Splitter ein.
•
Sie fügen Steuerelement B ein und stellen Dock=Fill ein.
14.10 Spezielle Steuerelemente
677
Damit ist das Beispiel prinzipiell bereits fertig. Der folgende Programmcode ist optional und bewirkt, dass die Steuerelemente während der Veränderung der Splitter-Position dynamisch neu gezeichnet werden und dass das Fensters nur so weit verkleinert wird, dass LabelB sichtbar bleibt. Beachten Sie, dass in Form2_Layout nur die Größe von LabelA verändert werden muss: LabelB sowie Splitter1 werden automatisch mit verschoben. ' Beispiel steuerelemente\splitter-test, Datei Form2.vb Private Sub Splitter1_SplitterMoving(...) Handles _ Splitter1.SplitterMoving If e.SplitX > Splitter1.MinSize And _ Me.ClientSize.Width - e.X > Splitter1.MinExtra Then LabelA.Width = e.X - 1 End If End Sub Private Sub Splitter1_SplitterMoved(...) Handles _ Splitter1.SplitterMoved Me.Refresh() End Sub Private Sub Form2_Layout(...) Handles MyBase.Layout If Me.ClientSize.Width < LabelA.Width + Splitter1.MinExtra Then LabelA.Width = _ Me.ClientSize.Width - Splitter1.MinExtra - Splitter1.Width End If End Sub
Fenster 2: Das Fensterlayout für das zweite Beispiel ist etwas komplexer: Am linken Fensterrand soll sich Steuerelement A befinden, rechts davon die Steuerelemente B bis E. Dabei soll B den oberen Bereich einnehmen; C, D und E sind nebeneinander und unterhalb von B angeordnet (siehe Abbildung 14.35). Auf Code zur Sicherstellung, dass alle Steuerelemente auch in kleinen Fenstern sichtbar bleiben, wurde bei diesem Beispiel verzichtet. (Dieser Code wäre wegen des komplexen Fensterlayouts ziemlich kompliziert ausgefallen.) Die folgende Tabelle gibt an, welche Eigenschaften eingestellt wurden. LabelA Splitter1 Panel1 LabelB in Panel1 Splitter2 in Panel1 Panel2 in Panel1 LabelC in Panel2 Splitter3 in Panel2 LabelD in Panel2 Splitter4 in Panel2 LabelE in Panel2
Dock=Left, BackColor=White, TextAlign=MiddleCenter Size.Width=5 Dock=Fill Dock=Top, sonst wie LabelA Dock=Top, Size.Height=5 Dock=Fill Dock=Left, sonst wie LabelA Size.Width=5 Dock=Left, sonst wie LabelA Size.Width=5 Dock=Fill, sonst wie LabelA
678
14 Steuerelemente
14.10.2 Timer Das Timer-Steuerelement dient dazu, regelmäßig die Tick-Ereignisprozedur auszulösen. Die Anwendung des Steuerelements ist denkbar einfach: Sie fügen das Steuerelement in das Formular ein, stellen mit der Interval-Eigenschaft ein, alle wie viel Millisekunden das Ereignis aufgerufen werden soll, und stellen Enabled auf True. (Vorsicht: Die Defaulteinstellung lautet False. Wenn Sie Enabled vergessen, erzeugt das Steuerelement keine Ereignisse!) Tests haben ergeben, dass das Tick-Ereignis nur bei Vielfachen der Zeitperiode 10 ms auftritt. Interval-Einstellungen kleiner 10 sind also zwecklos, und auch bei etwas größeren Werten wird das Zeitintervall keineswegs exakt eingehalten. Das folgende Minibeispielprogramm zeigt jede Zehntelsekunde die aktuelle Zeit an (siehe Abbildung 14.36). Interval wurde dazu auf 100 gesetzt. ' Beispiel steuerelemente\timer-test Private Sub Timer1_Tick(...) Handles Timer1.Tick Label1.Text = Now.ToString("HH:mm:ss:f0") End Sub
HINWEIS
Abbildung 14.36: Timer-Beispielprogramm
Wenn das Programm gerade beschäftigt ist (z.B. weil gerade eine Ereignisprozedur ausgeführt wird, die längere Zeit dauert), ist die automatische Ausführung der TickEreignisse so lange unterbrochen. Die Ereignisse werden nicht nachgeholt.
Timer-Varianten Neben der Timer-Klasse aus dem Windows.Forms-Namensraum gibt es zwei weitere Timer: •
System.Timers.Timer: Der Timer eignet sich vor allem für Server-Anwendungen ohne Benutzeroberfläche. Der Thread-Aufruf kann in unterschiedlichen worker threads erfolgen.
•
System.Threading.Timer: Der Timer eignet sich vor allem für Anwendungen in Multithreading-Programmen. Der Aufruf der Timer-Prozedur erfolgt in einem eigenen
Thread, der für diesen Zweck automatisch erzeugt wird. Diese beiden Timer-Klassen können zwar nicht so einfach wie das Timer-Steuerelement verwendet werden, die Einrichtung einer Timer-Prozedur, die regelmäßig aufgerufen wird, ist aber nicht schwierig. Beide Timer haben den Vorteil, dass die Intervalldauer wesentlich genauer eingestellt werden kann und dass diese Zeit auch ziemlich exakt eingehalten wird.
14.10 Spezielle Steuerelemente
679
Es kann daher nicht passieren, dass Timer-Ereignisse einfach verloren gehen, nur weil gerade irgendeine zeitaufwendige Ereignisprozedur läuft. Das Problem im Zusammenhang mit Windows.Forms-Anwendungen besteht allerdings darin, dass es laut Dokumentation unzulässig ist, aus fremden Threads auf das Fenster bzw. auf Steuerelemente zuzugreifen und diese zu verändern. Der Grund: Es kann vorkommen, dass die beiden Threads sich gegenseitig stören. (Die Timer-Prozeduren von Timers.Timer bzw. Threading.Timer laufen aber in einem anderen Thread als dem Windows.Forms-Hauptthread!)
VERWEIS
Aus diesem Grund dürfen Veränderungen in Steuerelementen durch externe Threads nur über den Umweg der Invoke-Methode durchgeführt werden. Wenn Sie diese Methode aus dem Timer-Thread heraus aufrufen, verlieren Sie die Vorteile des externen Timers. Durch Invoke wird das durch den Timer ausgelöste Ereignis in die Windows.Forms-Nachrichtenkette eingetragen und erst ausgeführt, nachdem andere bereits laufende Ereignisprozeduren abgeschlossen wurden. Einführende Informationen zum Thema Multithreading finden Sie in Abschnitt 12.6. Ein Beispiel für die Anwendung von Threading.Timer gibt Abschnitt 9.3.5. Einige Tipps zur Anwendung von Multithreading in Windows.Forms-Programmen finden Sie schließlich in Abschnitt 15.2.7. Die Klassenbeschreibung zu Timers.Timer und Threading.Timer enthält jeweils VB.NET-Anwendungsbeispiele. Genaue Informationen zu den Unterschieden zwischen den Timer-Klassen aus Windows.Forms und Timers erhalten Sie, wenn Sie nach Einführung in serverbasierte Zeitgeber suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconServerBasedTimers.htm
14.10.3 ToolTip ToolTips sind die kleinen gelben Informations- oder Hilfetexte, die bei vielen Programmen
auftauchen, wenn die Maus längere Zeit über einem Steuerelement verharrt. Nach einer Weile verschwinden diese Texte wieder.
Abbildung 14.37: Ein einfaches ToolTip-Beispielprogramm
680
14 Steuerelemente
Um ToolTips anzuzeigen, müssen Sie in Ihr Formular ein ToolTip-Steuerelement einfügen. (ToolTip ist genau genommen eine gewöhnliche Klasse, die selbst unsichtbar bleibt.) Anschließend führen Sie (am besten in Form1_Load) für jedes Steuerelement, das mit einem ToolTip ausgestattet werden soll, die Methode SetToolTip aus. Die folgenden Zeilen demonstrieren die Vorgehensweise hinreichend. Wenn Sie den ToolTip-Text über mehrere Zeilen verteilen möchten, fügen Sie einfach mit vbCrLf Codes zur Zeilentrennung in die Zeichenkette ein. Darüber hinaus gibt es zurzeit leider keine Möglichkeit, das Aussehen von ToolTips zu beeinflussen. Die Schriftart, die Form, die Farbe – alles ist vorgegeben. ToolTips in Form von Sprechblasen wird es (wenn überhaupt) erst in zukünftigen .NET-Versionen geben. Private Sub Form1_Load(...) Handles MyBase.Load ToolTip1.SetToolTip(TextBox1, "Tooltip für TextBox1") ToolTip1.SetToolTip(Label1, "Tooltip für Label1") ToolTip1.SetToolTip(RadioButton1, "Tooltip für RadioButton1") ToolTip1.SetToolTip(RadioButton2, "Tooltip für RadioButton2") ToolTip1.SetToolTip(CheckBox1, _ "Tooltip" + vbCrLf + "für CheckBox1") End Sub
HINWEIS
SetToolTip überschreibt einen bereits vorhandenen ToolTip-Text. Sie können diese Methode also auch dazu verwenden, den ToolTip-Text eines Steuerelements zu verändern. RemoveAll löscht alle vom ToolTip-Steuerelement verwalteten ToolTips.
Aus unerfindlichen Gründen wird das Zeichen & in ToolTips nur dann angezeigt, wenn es im ToolTip-Text dreimal (!) angegeben wird. ToolTip1.SetToolTip(myControl, "Drag &&& Drop")
Verhalten Einige Details des ToolTips-Verhaltens können durch Eigenschaften des ToolTip-Steuerelements beeinflusst werden: Active gibt an, ob ToolTips automatisch angezeigt werden sollen (per Default True). InitialDelay gibt an, nach wie vielen Millisekunden der ToolTip erscheint (per Default nach einer halben Sekunde). AutoPopDelay gibt an, wie lange der ToolTip maximal sichtbar bleibt (per Default fünf Sekunden).
Sonderfälle Bei einigen Steuerelementen gibt es spezifische ToolTips für einzelne Details. Dazu sind die Subklassen dieser Steuerelemente mit der Eigenschaft ToolTipText ausgestattet. (Die Darstellung der ToolTips erfolgt also unabhängig vom ToolTip-Steuerelement. Warum diese einfache Vorgehensweise nicht für alle Steuerelementen gilt, weiß freilich nur Microsoft.)
14.10 Spezielle Steuerelemente
StatusBar
kennt eigene ToolTips für jeden Abschnitt (StatusBarPanel-Klasse).
TabControl
kennt eigene ToolTips für jeden Tabellenreiter (TabPage-Klasse).
ToolBar
kennt eigene ToolTips für jeden Button (ToolBarButton-Klasse).
681
ToolTips für einzelne Listeneinträge Bei Listenfeldern wäre es manchmal praktisch, eigene ToolTips für jeden Listeneintrag anzuzeigen. (Wenn der Anwender also länger über einem Listeneintrag verweilt, erscheinen dazu ausführliche Informationen. Sie kennen dieses Verhalten wahrscheinlich aus dem VS.NET-Hilfesystem, wo genau diese Vorgehensweise angewendet wird, wenn der Text eines Listeneintrags zu lang ist.) Leider sieht keines der .NET-Listenfelder diese Möglichkeit vor. Um den gewünschten Effekt dennoch zu erzielen, können Sie in der MouseMove-Ereignisprozedur des Steuerelements zu ermitteln versuchen, über welchem Listeneintrag sich die Maus gerade befindet. Das TreeView-Steuerelement stellt zu diesem Zweck die Methode GetNodeAt zur Verfügung. Wenn Sie das Listenelement erkannt haben, können Sie den ToolTip-Text entsprechend ändern. In der folgenden Beispielprozedur für ein TreeView-Steuerelement ist der Code ein wenig komplizierter, weil versucht wird, ToolTip-Änderungen möglichst nur dann durchzuführen, wenn sich das Listenelement unter der Maus tatsächlich geändert hat. (Deswegen wird in der Variablen oldtn das zuletzt aktive Listenelement gespeichert.) ' Beispiel steuerelemente\tooltip-test Private Sub TreeView1_MouseMove(...) Handles TreeView1.MouseMove Dim tn As TreeNode Static oldtn As TreeNode tn = TreeView1.GetNodeAt(TreeView1.PointToClient( _ TreeView1.MousePosition())) If IsNothing(tn) Then ' die Maus befindet sich über keinem Listeneintrag ' ToolTip-Text für das Steuerelement anzeigen If Not (tn Is oldtn) Then ToolTip1.SetToolTip(TreeView1, "Tooltip für TreeView1") oldtn = tn End If ElseIf Not (tn Is oldtn) Then ' die Maus befindet sich über einem neuen Listeneintrag ' dazu passenden ToolTip-Text anzeigen ToolTip1.SetToolTip( _ TreeView1, "Tooltip für TreeNode " + tn.Text) oldtn = tn End If End Sub
682
14 Steuerelemente
14.10.4 HelpProvider Das HelpProvider-Steuerelement hilft dabei, Online-Hilfe zu Ihrem Programm anzuzeigen. Die Hilfe erscheint normalerweise in einem eigenen Hilfefenster, das automatisch geschlossen wird, wenn das Programm beendet wird. Leider machen sowohl der Funktionsumfang als auch die Dokumentation des HelpProvider-Steuerelements einen ziemlich unausgereiften Eindruck.
Voraussetzungen Der erste Schritt zu einem eigenen Hilfesystem besteht im Regelfall darin, eine HTMLHelp-Hilfedatei zu erzeugen (Dateikennung *.chm). Dazu können Sie den mit VS.NET mitgelieferten HTML-Help-Workshop verwenden (Programme\Visual Studion .NET\Common7\Tools\hcw.exe). Dieses Programm ist allerdings ziemlich alt und ausgesprochen umständlich zu bedienen. Professionelle Hilfedateien für große Projekte werden Sie damit kaum erstellen können. Als Alternative können Sie ein so genanntes Help-Authoring-Programm erwerben. Derartige Programme (z.B. RoboHelp, Doc-to-Help, Help Magician) sind im Angebot mehrerer Software-Anbieter und bieten ungleich mehr Komfort und Funktionen als hcw.exe. Ihr Hauptnachteil ist aber ihr hoher Preis. Eine andere Alternative besteht darin, statt einer Hilfedatei eine Website anzugeben, die Hilfedokumente enthält. Der offensichtliche Nachteil dieser Vorgehensweise besteht darin, dass die Hilfe nur dann verfügbar ist, wenn eine Internet-Verbindung besteht. Dieses Buch geht aus Platzgründen nicht weiter auf das Erstellen von Hilfedateien ein. Es setzt vielmehr voraus, dass eine fertige *.chm-Hilfedatei vorliegt. Beachten Sie bitte, dass HelpProvider mit dem älteren Hilfeformat *.hlp aus der Zeit vor Windows 98/2000 nicht mehr zurechtkommt.
Formular und Hilfedatei miteinander verbinden Die Verbindung zwischen dem Formular und der Hilfedatei stellt das HelpProvider-Steuerelement her. Nach dem Einfügen des Steuerelements müssen Sie lediglich dessen HelpNameSpace-Eigenschaft so einstellen, dass sie den Dateinamen der Hilfedatei enthält. (Diese Einstellung bewirkt noch nicht, dass die Hilfe mit F1 tatsächlich angezeigt wird. Dazu müssen auch die HelpXxx-Eigenschaften eingestellt werden – siehe die übernächste Überschrift!)
Ort der Hilfedatei beim Programmstart einstellen Da sich der Ort der Hilfedatei bei der Auslieferung des Programms meist ändert, ist es eine gute Idee, die Hilfedatei in dasselben Verzeichnis wie das Programm zu installieren. Dann können Sie HelpNameSpace beim Programmstart in Form1_Load an den Installationsort anpassen. (Der Dateiname des laufenden Programms wird dazu mit GetExecutingAssembly.Location ermittelt.)
14.10 Spezielle Steuerelemente
683
Private Sub Form1_Load(...) Handles MyBase.Load Dim fi As New IO.FileInfo( _ System.Reflection.Assembly.GetExecutingAssembly.Location) HelpProvider1.HelpNamespace = _ fi.DirectoryName + "\hilfedatei.chm" End Sub
Hilfetext automatisch durch F1 anzeigen Nun können Sie jedes Steuerelement, das den Eingabefokus hat, mit einem bestimmten Hilfedokument verbinden. Sobald der Anwender in diesem Steuerelement die Taste F1 drückt, wird der Hilfetext automatisch angezeigt. Damit das funktioniert, müssen Sie im Eigenschaftsfenster die Eigenschaft HelpNavigator und eventuell auch HelpKeyword einstellen. Deren Einstellung hängt davon ab, welcher Hilfetext angezeigt werden soll: •
Inhaltsverzeichnis: HelpNavigator=TableOfContents. Im Hilfefenster wird die Startseite und das Inhaltsverzeichnis angezeigt.
•
Index: HelpNavigator=Index. Im Hilfefenster wird die Startseite und das Stichwortverzeichnis angezeigt.
•
Volltextsuche: HelpNavigator=Find, HelpKeyword="xxx". Im Hilfefenster wird die Startseite und das Dialogblatt zur Volltextsuche angezeigt. Die mit HelpKeyword angegebene Zeichenkette wird nicht ausgewertet oder gar als Suchtext übergeben. Es muss allerdings irgendeine Zeichenkette angegeben werden, sonst funktioniert die Einstellung HelpNavigator=Find überhaupt nicht.
•
Eine spezielle Seite: Um eine spezielle Seite anzuzeigen, gibt es zwei Varianten: HelpNavigator=KeywordIndex, HelpKeyword="abc": Bei dieser Einstellung wird im Hilfefenster der Hilfetext zum Stichwort "abc" angezeigt. Das funktioniert natürlich nur, wenn "abc" tatsächlich ein Begriff der Stichwortliste (des Index) ist. HelpNavigator=Topic, HelpKeyword=n: Falls ich die recht vage Dokumentation richtig interpretiere, dann sollte mit dieser Einstellung wohl die Hilfeseite mit der ID-Nummer n erscheinen (entsprechend der VB6-Eigenschaft HelpContextID). Tatsächlich kommt es aber immer nur zum Fehler Seite kann nicht angezeigt werden, ganz egal, ob an HelpKeyword eine gültige ID-Nummer, ein Stichwort, oder der Aliasname eines Hilfetexts übergeben wird.
Intern wird bei dieser Variante wahrscheinlich die Methode Help.ShowHelp(steuerelement, "hilfedatei.chm", HelpNavigator.Topic, n) aufgerufen. Alle Versuche, mit dieser Methode (statt mit HelpProvider) einen spezifischen Hilfetext anzuzeigen, sind gescheitert. Es ist somit schwer zu sagen, ob der Fehler nun bei der Dokumentation oder bei der Implementierung liegt.
684
14 Steuerelemente
HTML-Datei als Hilfetext verwenden Das HelpProvider-Steuerelement kann statt auf eine Hilfedatei auch auf eine lokale HTMLDatei verweisen. Dazu geben Sie als HelpNameSpace einfach die HTTP-Adresse an, z.B.: HelpProvider1.HelpNameSpace="C:\infos\index.html"
Damit diese Seite durch F1 im Standardbrowser des Systems angezeigt wird, verwenden Sie HelpNavigator=TableOfContents.
Hilfetexte aus dem Internet anzeigen Eine weitere Variante besteht darin, dass das HelpProvider-Steuerelement auf eine Website verweist. Dazu geben Sie als HelpNameSpace die HTTP-Adresse an, z.B.: HelpProvider1.HelpNameSpace="http://www.kofler.cc"
Durch die Einstellung HelpNavigator = Topic und HelpKeyword ="index.html" erreichen Sie, dass im Webbrowser die Seite http://www.kofler.cc/index.html angezeigt wird.
Hilfe in einem Popup-Fenster anzeigen Gerade bei einfachen Dialogen ist es oft nicht notwendig, für jedes Steuerelement ein ganzes Hilfedokument zu verfassen. Eine kurze Erklärung zum Steuerelement reicht aus. Unter Windows wird dieser Hilfemechanismus durch einen eigenen Hilfe-Button unterstützt, der im linken oberen Eck des Dialogs angezeigt wird. Wenn der Anwender diesen Button anklickt, verwandelt sich die Maus in ein Fragezeichen. Anschließend kann mit der Maus ein Steuerelement angeklickt werden. Soweit verfügbar erscheint dann ein kurzer Hilfetext in der Art der ToolTip-Texte.
Abbildung 14.38: Popup-Hilfe
Um diesen Hilfemechanismus in VB.NET-Programmen zu nutzen, müssen Sie zuerst drei Eigenschaften des Formulars ändern. (Beachten Sie, dass der Hilfe-Button wirklich nur erscheint, wenn die Buttons zur Minimierung und Maximierung deaktiviert werden!) HelpButton=True MaximizeBox=False MinimizeBox=False
Anschließend müssen Sie die gewünschten Hilfetexte in den HelpString-Eigenschaft angeben. (Die Popup-Hilfetexte werden also nicht aus der Hilfedatei entnommen, sondern müssen direkt angegeben werden!)
HINWEIS
14.10 Spezielle Steuerelemente
685
Grundsätzlich ist es möglich, einen Dialog nur mit Popup-Hilfetexten auszustatten. In diesem Fall benötigen Sie keine eigene Hilfedatei, die Eigenschaft HelpNameSpace des HelpProvider-Steuerelements muss nicht eingestellt werden.
Interna zu den HelpXxx-Eigenschaften Sobald Sie ein HelpProvider-Steuerelement in Ihr Formular einfügen, werden bei jedem Steuerelement vier Help-Eigenschaften angezeigt: HelpNavigator, HelpKeyword, HelpString und ShowHelp. Wenn Sie im Objektbrowser nachsehen, werden Sie aber feststellen, dass weder die Control-Klasse noch die davon abgeleiteten Steuerelemente die Eigenschaften HelpNavigator, HelpKeyword etc. kennen! Tatsächlich sind die Help-Eigenschaften fiktiv. Ihre Einstellung bewirkt, dass im normalerweise ausgeblendeten Codeabschnitt Vom Windows Form Designer generierter Code Code nach dem folgenden Muster eingefügt wird: Me.HelpProvider1.SetHelpKeyword(Me.TextBox1, "abcde") Me.HelpProvider1.SetHelpNavigator(Me.TextBox1, _ System.Windows.Forms.HelpNavigator.KeywordIndex)
Mit anderen Worten: Die Help-Eigenschaften im Eigenschaftsfenster dienen nur zur komfortablen Einstellung der Methoden SetHelpKeyword, SetHelpNavigator etc. des HelpProvider-Steuerelements. (Es wäre übrigens wünschenswert gewesen, wenn Microsoft denselben Mechanismus auch für das ToolTip-Steuerelement vorgesehen hättte. Die Verwaltung der ToolTip-Informationen erfolgt exakt nach demselben Schema wie beim HelpProviderSteuerelement. Allerdings müssen Sie die ToolTips selbst im Programmcode initialisieren.)
Hilfe explizit aufrufen Das HelpProvider-Steuerelement ermöglicht es, dass Hilfetexte automatisch aufgerufen werden, sobald der Anwender F1 drückt. Manchmal ist dieser Automatismus unzureichend – etwa wenn Sie möchten, dass das Hilfefenster als Reaktion auf einen Button-Klick oder auf eine Menüauswahl erscheint. Für derartige Aufgaben verwenden Sie die Methoden der Klasse Windows.Forms.Help. Die wichtigste Methode dieser Klasse ist ShowHelp. Als ersten Parameter müssen Sie ein Steuerelement Ihres Formulars (oder mit Me das Formular selbst) angeben. Aus der Dokumentation geht nicht klar hervor, wozu diese Information eigentlich benötigt wird. Die Bedeutung des zweiten Parameters ist einfacher zu verstehen: Hierbei handelt es sich um den Dateinamen der Hilfedatei (oder um die Webadresse der Hilfe-Website). Help.ShowHelp(Me, "c:\verzeichnis\hilfedatei.chm") Help.ShowHelp(Me, "http://firma.com/support/help.html")
Zur Anwendung von ShowHelp ist es nicht erforderlich, dass das Formular ein HelpProviderSteuerelement enthält. Wenn das aber der Fall ist, können Sie dem Steuerelement den Dateinamen der Hilfedatei entnehmen:
686
14 Steuerelemente
Help.ShowHelp(Me, HelpProvider1.HelpNamespace)
Zu ShowHelp gibt es eine Reihe von Syntaxvarianten, die es ermöglichen, eine ganz spezielle Hilfeseite anzuzeigen. Die dabei übergebenen Parameter entsprechen den oben beschriebenen HelpNavigator- und HelpKeyword-Eigenschaften.
14.10.5 ErrorProvider Das ErrorProvider-Steuerelement ermöglicht es, bei anderen Steuerelementen ein rotes Ausrufezeichen anzuzeigen, wenn dort ein Eingabefehler vorliegt. Auf diese Weise können Sie optisch auf Eingabefehler hinweisen, ohne den Anwender in seinem Arbeitsfluss zu stören. (Genau genommen ist ErrorProvider kein richtiges Steuerelement, sondern eine von Component abgeleitete Klasse. Ich bleibe dennoch beim Begriff Steuerelement.) Zur Anwendung fügen Sie ein ErrorProvider-Steuerelement in Ihr Formular ein. Anschließend fügen Sie in die Change- oder Validating-Ereignisprozeduren der zu überwachenden Steuerelemente einen Code nach dem folgenden Muster ein. If <eingabefehler feststellen> Then ErrorProvider1.SetError(steuerelement, "Fehlermeldung") Else ErrorProvider1.SetError(steuerelement, "") End If
Sobald mit SetError für ein Steuerelement eine Zeichenkette angegeben wird, erscheint neben dem Steuerelement das rote Fehler-Icon. Wenn der Anwender die Maus über das Icon bewegt, erscheint die Fehlermeldung in der Art eines ToolTips (siehe Abbildung 14.39). Wenn bei mehreren Steuerelementen Fehler auftreten, können mehrere Icons gleichzeitig dargestellt werden (mit nur einem ErrorProvider-Steuerelement). Indem Sie an SetError eine leere Zeichenkette übergeben, entfernen Sie das Fehler-Icon wieder.
TIPP
Abbildung 14.39: Ein einfaches Formular mit zwei Eingabefehlern
Das ErrorProvider-Steuerelement kann auch als gebundenes Steuerelement in Datenbankanwendungen eingesetzt werden.
14.10 Spezielle Steuerelemente
687
Gestaltung Per Default erscheint der Fehlerindikator als rotes Ausrufezeichen. Wenn Sie ein anderes Erscheinungsbild wünschen, ändern Sie die Icon-Eigenschaft des Steuerelements. Darüber hinaus können Sie mit BlinkStyle angeben, ob und unter welchen Umständen das Icon zu Beginn blinken soll. (Per Default blinkt es beim Erscheinen bzw. wenn sich der Fehlertext ändert.) BlinkRate gibt schließlich die Periodendauer für das Blinken an (je größer, desto langsamer).
Beispiel Das Beispielprogramm ist eine Variante des Programms textbox-valdiation, das in Abschnitt 14.4.3 vorgestellt wurde. Aufgabe des Programms ist es, das Produkt aus zwei Zahlen zu berechnen. Wenn in einem der beiden Textfelder ein Eingabefehler auftritt, wird der Fehlerindikator angezeigt. Die Validierung erfolgt in der Funktion ValidateTextBox. Diese Funktion wird von den TextChanged- und den Validating-Ereignisprozeduren der Steuerelemente TextBox1 und -2 aufgerufen. (Im Folgenden wurde auf den doppelten Abdruck dieser Prozeduren verzichtet und stattdessen die syntaktisch natürlich nicht korrekte 1/2-Kurzschreibweise verwendet.) ' Beispiel steuerelemente\errorprovider-test Private Sub TextBox1/2_TextChanged(...) Handles TextBox1.TextChanged ValidateTextBox(TextBox1/2) CalcResult() End Sub Private Sub TextBox1/2_Validating(..) Handles TextBox1.Validating If ValidateTextBox(TextBox1/2) = False Then TextBox1/2.SelectAll() End If End Sub Private Function ValidateTextBox(ByVal txtbox As TextBox) As Boolean If IsNumeric(txtbox.Text) = False Then ErrorProvider1.SetError(txtbox, _ "Ihre Eingabe muss eine Zahl sein!") Return False Else ErrorProvider1.SetError(txtbox, "") Return True End If End Function Private Sub CalcResult() [... Ergebnis berechnen und in LabelResult anzeigen ...] End Sub
688
14 Steuerelemente
14.10.6 NotifyIcon Das NotifyIcon-Steuerelement ermöglicht es, ein kleines Icon im Icon-Abschnitt der Taskbar anzuzeigen. Das ist vor allem für solche Programme sinnvoll, die im Normalfall im Hintergrund laufen und unsichtbar sind. Das trifft beispielsweise für Fenster zu, die minimiert sind und nicht in der Taskbar angezeigt werden (ShowInTaskbar=False). NotifiyIcon kennt nur zwei Eigenschaften, die von Bedeutung sind: Icon gibt das an. Visible bestimmt, ob das Icon tatsächlich angezeigt werden soll. Das Icon kann wie alle anderen Steuerelemente mit einem Kontextmenü und mit einem ToolTip-Text (mit der Hilfe des ToolTip-Steuerelements) ausgestattet werden.
Das Steuerelement kann die Ereignisse Click, DoubleClick sowie MouseMove, MouseDown und MouseUp verarbeiten. Die DoubleClick-Ereignisprozedur wird üblicherweise dazu verwendet, die Benutzeroberfläche des Programms sichtbar zu machen.
Beispiel Das folgende Beispielprogramm ist ein einfacher Wecker: In einem DateTimePicker-Steuerelement kann die Weckzeit eingestellt werden (siehe Abbildung 14.40). Mit dem Button WECKER AKTIVIEREN wird das Fenster unsichtbar und ist nur noch im Icon-Bereich der Taskbar sichtbar (siehe Abbildung 14.41). Sobald die eingestellte Zeit erreicht wird, wird das Fenster mit roter Hintergrundfarbe zur Erinnerung wieder sichtbar.
Abbildung 14.40: Einstellung der Weckzeit
Abbildung 14.41: Der Icon-Bereich der Taskbar
Die DoubleClick-Prozedur macht das verkleinerte Programm bei Bedarf sichtbar. ' Beispiel steuerelemente\notifyicon-test Private Sub NotifyIcon1_DoubleClick(...) Handles _ NotifyIcon1.DoubleClick Me.WindowState = FormWindowState.Normal Me.Activate() End Sub
14.10 Spezielle Steuerelemente
689
Intern verwendet das Programm ein Timer-Steuerelement, das einmal pro Sekunde ausgelöst wird. In der Ereignisprozedur wird zuerst der Fensterzustand getestet: Wenn der Wecker aktiv ist, das Fenster also verkleinert ist, wird die aktuelle Zeit mit der Weckzeit verglichen. Gegebenenfalls wird das Fenster mit TopMost = True und WindowState = Normal sichtbar gemacht. Wenn das Fenster dagegen sichtbar ist, wird die aktuelle Zeit im Textfeld angezeigt. Private Sub Timer1_Tick(...) Handles Timer1.Tick If Me.WindowState = FormWindowState.Minimized Then If Now.TimeOfDay.CompareTo( _ DateTimePicker1.Value.TimeOfDay) >= 0 Then Me.WindowState = FormWindowState.Normal Me.Activate() Me.TopMost = True Me.BackColor = Color.Red End If Else TextBox1.Text = Now.ToLongTimeString() End If End Sub
Um den normalen Fensterhintergrund wiederherzustellen, muss der Anwender das Formular anklicken. Damit wird auch TopMost zurückgesetzt. Private Sub Form1_Click(...) Handles MyBase.Click Me.BackColor = SystemColors.Control Me.TopMost = False End Sub
Bemerkenswert ist die ValueChanged-Prozedur zum DateTimePicker-Steuerelement: Hier werden von der gerade eingestellten Zeit die Sekunden abgezogen. Das ist deswegen notwendig, weil das Steuerelement zur Zeiteinstellung 12:30 die Sekundenanzahl hinzufügt, zu der das Programm gestartet wurde. ' Weckzeit einstellen Private Sub DateTimePicker1_ValueChanged(...) Handles _ DateTimePicker1.ValueChanged DateTimePicker1.Value = _ DateTimePicker1.Value.AddSeconds(-DateTimePicker1.Value.Second) End Sub
690
14 Steuerelemente
14.11 Programmiertechniken 14.11.1 Schleife über alle Steuerelemente Die Eigenschaft Controls von Formularen bzw. von Container-Steuerelementen verweist auf ein Control.ControlCollection-Objekt, das wiederum auf alle Steuerelemente im Formular bzw. Container verweist. Dank dieser Aufzählung können Sie mühelos auf alle Steuerelemente zugreifen. Das folgende Beispielprogramm zeigt dafür eine sinnvolle Anwendung: In einem Programm gibt es eine Menge Kontrollkästchen. Per Button-Klick können diese alle zurückgesetzt werden (siehe Abbildung 14.42).
Abbildung 14.42: Der Button deaktiviert alle Kontrollkästchen
Der Programmcode ist einfach zu verstehen: Die For-Each-Schleife greift der Reihe nach auf alle Steuerelemente des Formulars zu. Mit TypeOf wird getestet, ob es sich bei dem Steuerelement um ein Kontrollkästchen handelt. Nur wenn das der Fall ist, wird mit CType eine Typumwandlung durchgeführt, damit anschließend die Checked-Eigenschaft auf False gesetzt werden kann. ' Beispiel steuerelemente\controls-loop Private Sub Button1_Click(...) Handles Button1.Click Dim c As Control, cb As CheckBox For Each c In Controls If TypeOf c Is CheckBox Then cb = CType(c, CheckBox) cb.Checked = False End If Next End Sub
HINWEIS
14.11 Programmiertechniken
691
Beachten Sie bitte, dass Menüeinträge nicht mit Controls angesprochen werden können, weil die MenuItem-Klasse nicht von Control abgeleitet ist und Menüeinträge daher in einer von Controls unabhängigen Aufzählung (MainMenu1.MenuItems) verwaltet werden.
Steuerelemente rekursiv durchlaufen Beachten Sie, dass Controls immer nur auf die unmittelbar enthaltenen Steuerelemente verweist. Steuerelemente können aber ineinander verschachtelt werden. Beispielsweise können die Steuerelemente Panel, GroupBox und TabControl selbst andere Steuerelemente enthalten. Wenn Sie also wirklich alle Steuerelemente eines Formulars durchlaufen möchten, müssen Sie einen rekursiven Ansatz wie im folgenden Beispiel wählen. ' Beispiel steuerelemente\control-array Private Sub Button1_Click(...) Handles Button1.Click ProcessControls(Me.Controls) End Sub Private Sub ProcessControls(ByVal ctrls As Control.ControlCollection) Dim c As Control For Each c In ctrls Debug.WriteLine(c.Name) ProcessControls(c.Controls) Next End Sub
14.11.2 Steuerelemente dynamisch einfügen Normalerweise legen Sie bereits beim Formularentwurf fest, wie viele Steuerelemente es gibt, wo sie sich befinden etc. In manchen Anwendungen kann es sinnvoll sein, einen Teil dieser Steuerelemente erst bei Bedarf zu aktivieren (Enable=True/False) bzw. sichtbar zu machen (Visible=True/False). Noch mehr Flexibilität beim Formularentwurf erzielen Sie, wenn Sie Steuerelemente dynamisch – das heißt im laufenden Programm – einfügen. Grundsätzlich ist das recht einfach zu bewerkstelligen: •
Zuerst erzeugen Sie mit New das neue Steuerelement (z.B. btn = New Button).
•
Anschließend stellen Sie dessen Eigenschaften ein (z.B. btn.Text = "abc"). Besonders wichtig sind natürlich Größe und Position. (Leider steht für Steuerelemente keine Clone-Methode zur Verfügung, um eine exakte Kopie eines Steuerelements zu erstellen. Das würde viel Arbeit bei der Einstellung diverser Eigenschaften ersparen.)
•
Schließlich fügen Sie das Steuerelement mit Add in die Controls-Aufzählung des Formulars oder eines Container-Steuerelements ein (z.B. Form1.Controls.Add(btn)).
692
14 Steuerelemente
Das einzige Problem, das jetzt noch bleibt, ist die Verwaltung der Ereignisse. Da die Steuerelemente erst bei Bedarf erzeugt werden, muss auch die Zuordnung zwischen Ereignissen und der aufzurufenden Prozedur dynamisch erfolgen. VB.NET sieht dazu das Schlüsselwort AddHandler vor. An dieses Kommando übergeben Sie in zwei Parametern den Ereignisnamen und die Adresse der Ereignisprozedur. (Die Adresse ermitteln Sie mit AddressOf.) Um beispielsweise einen dynamisch erzeugten Button mit seiner Click-Ereignisprozedur zu verbinden, führen Sie die folgende Anweisung aus. Dim btn As New Button() ... AddHandler btn.Click, AddressOf ereignisprozedur
Damit dieser Code fehlerfrei kompiliert wird, muss ereignisprozedur korrekt deklariert sein. (Entscheidend ist insbesondere, dass die Parameter im richtigen Typ angegeben sind. Die erforderlichen Informationen finden Sie am einfachsten im Objektbrowser zur Klasse Ihres Steuerelements.) Für das Click-Ereignis eines Buttons sieht die Deklaration folgendermaßen aus:
HINWEIS
Private Sub ereignisprozedur(ByVal sender As System.Object, _ ByVal e As System.EventArgs)
Die Prozedur ereignisprozedur sollte nicht mit Handles ereignisname enden (andernfalls kann es zu einem doppelten Aufruf der Prozedur kommen). Handles ereignisname ist nur erforderlich, wenn AddHandler nicht verwendet wird. Gewöhnliche Ereignisprozeduren (mit Handles ereignisname) und AddHandler-Ereignisprozeduren können parallel existieren. In diesem Fall wird zuerst die gewöhnliche Ereignisprozedur und anschließend die mit AddHandler angegebene Prozedur aufgerufen.
Da Sie üblicherweise dieselbe Ereignisprozedur für mehrere gleichartige Steuerelemente verwenden, muss es innerhalb der Prozedur eine Möglichkeit geben, um festzustellen, für welches Steuerelement das Ereignis ausgelöst wurde. Diese Aufgabe übernimmt der sender-Parameter, der ein Verweis auf das zugrunde liegende Steuerelement enthält. Zur Auswertung müssen Sie diese Variable mit CType in den entsprechenden Objekttyp umwandeln (z.B. CType(sender, Button)). Wenn die Ereignisprozedur für unterschiedliche Steuerelementtypen verwendet wird, müssen Sie natürlich eine Fallunterscheidung für den Objekttyp einfügen (If TypeOf sender Is Button Then...).
Beispiel Das in Abbildung 14.43 dargestellte Beispielprogramm zeigt nach dem Start nur zwei Buttons an. Jedes Mal, wenn Sie NEUER BUTTON anklicken, wird ein neuer Button in das Formular eingefügt. Wenn Sie einen der neuen Buttons anklicken, wird dieser Button wieder entfernt. Mit 1000 NEUE BUTTONS können Sie die Grenzen dieses Verfahrens ausloten. Auf meinem Rechner (Athlon 1,4 GHz) dauerte es weniger als eine Sekunde, um die Buttons einzufü-
14.11 Programmiertechniken
693
gen. Das Programm war anschließend weiterhin problemlos zu bedienen, auch das Scrolling durch das nun ziemlich große Formular (AutoScroll=True) funktionierte noch in einer akzeptablen Geschwindigkeit. Mit anderen Worten: Nichts hindert Sie daran, beinahe beliebig große Dialoge dynamisch erzeugen. (Eine mögliche Anwendung könnte z.B. ein Fragebogen sein, bei dem Sie die Fragen aus einer Datenbank lesen und auf dieser Basis das Formular zusammenstellen. Das ist sicher mit weniger Aufwand verbunden, als Hundert Kontrollkästchen manuell einzufügen.)
Abbildung 14.43: Buttons dynamisch erzeugen und entfernen
In Button1_Click wird mit New Button ein neuer Button erzeugt. Die folgenden Zeilen dienen dazu, dessen Größe, Position und den enthaltenen Text einzustellen. Controls.Add fügt den Button in die Controls-Aufzählung des Formulars ein. AddHandler gibt an, welche Ereignisprozedur bei einem Click-Ereignis aufgerufen werden soll. Die Methoden SuspendLayout und ResumeLayout in Button2_Click weisen .NET an, vorübergehend keine Ereignisse auszulösen, die sich durch das Einfügen der Steuerelemente normalerweise ergeben würden. (Das betrifft insbesondere das Layout-Ereignis.) Die Verwendung der beiden Methoden beschleunigt die Prozedur um ein Mehrfaches! ' Beispiel steuerelemente\dynamic-controls ' Button-Zähler zur Positionierung neuer Buttons Dim dynamic_buttons As Integer = 0 ' einen Button einfügen Private Sub Button1_Click(...) Handles Button1.Click Dim btn As New Button() dynamic_buttons += 1 btn.Size = Button1.Size btn.Left = Button1.Left + _ (Button1.Width + 10) * (dynamic_buttons Mod 5) btn.Top = Button1.Top + _ (Button1.Height + 10) * CInt(Int(dynamic_buttons / 5)) btn.Text = "Button" + (dynamic_buttons + 1).ToString Me.Controls.Add(btn) AddHandler btn.Click, AddressOf btn_Click End Sub
694
14 Steuerelemente
' einen der neuen Buttons entfernen Private Sub btn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Me.Controls.Remove(CType(sender, Button)) End Sub
VERWEIS
' 1000 neue Buttons einfügen Private Sub Button2_Click(...) Handles Button2.Click Dim i As Integer Me.SuspendLayout() For i = 1 To 1000 Button1_Click(Nothing, Nothing) Next Me.ResumeLayout() End Sub
Ein weiteres Beispiel für dynamisch erzeugte Steuerelemente finden Sie in Abschnitt 14.9.2. Dort werden CheckBox-Steuerelemente in ein Panel eingefügt.
14.11.3 Steuerelementfelder In Visual Basic 1 bis 6 gab es ein ausgesprochen praktisches Merkmal, das der .NET-Umstellung leider zum Opfer gefallen ist: Steuerelementfelder. Dank dieses Merkmals konnten mehrere Steuerelemente mit dem selben Namen und – viel wichtiger – der selben Eigenschaftsprozedur ausgestattet werden. Die Unterscheidung der gleichartigen Steuerelemente erfolgte über eine Indexnummer, d.h. die Steuerelemente wurden wie ein Feld angesprochen (CheckBoxXy(3) bezeichnete das vierte Auswahlkästchen). Wenn Sie die beiden vorangegangenen Abschnitte gelesen haben, dann ist Ihnen sicher klar, dass die Funktion von Steuerelementfeldern in VB.NET weiterhin zur Verfügung steht. •
Es ist kein Problem, per Programmcode eine Reihe von Steuerelementen mit einer Ereignisprozedur auszustatten: verwenden Sie einfach AddHandler, um die Steuerelemente mit der Prozedur zu verbinden.
•
Ebenso wenig bereitet es Schwierigkeiten, eine Reihe gleichartiger Steuerelemente in einer Schleife zu bearbeiten: Entweder Sie platzieren die Steuerelemente in ein Panel (dann können Sie die Steuerelemente mit For-Each-Panel.Controls ansprechen), oder Sie verwenden in der For-Each-Me.Controls-Schleife ein Kriterium, um die für Sie relevanten Steuerelemente herauszufiltern.
Der einzige (große) Rückschritt im Vergleich zu VB1-6 besteht darin, dass die Entwicklungsumgebung Steuerelementfelder nicht mehr unterstützt. Dadurch ist die Verwendung von Steuerelementfeldern jetzt weniger intuitiv als früher.
14.11 Programmiertechniken
695
Beispiel Im folgenden Beispiel können Sie mit RadioButton-Steuerelementen die Hintergrundfarbe für die daneben angezeigen PictureBox auswählen (siehe Abbildung 14.44). Die RadioButtons sollten mit einer gemeinsamen Ereignisprozedur ausgestattet werden, um die Verarbeitung der Optionsauswahl zu zentralisieren.
Abbildung 14.44: Die Farbauswahl wird von einer zentralen Ereignisprozedur verarbeitet
Die Steuerelemente wurden dazu in ein GroupBox-Steuerelement eingefügt und mit aussagekräftigen Namen ausgestattet (z.B. RadioButtonRed, RadioButtonGreen). Anstatt nun per Doppelklick auf das Steuerelement für jedes Steuerelement eine eigene Ereignisprozedur in den Formularcode einzufügen, wurde die zentrale Ereignisprozedur RadioButtons_CheckedChanged manuell eingefügt. In Form1_Load wird diese Ereignisprozedur mit allen Steuerelementen verbunden. Private Sub Form1_Load(...) Handles MyBase.Load Dim c As Control, rb As RadioButton For Each c In GroupBox1.Controls If TypeOf c Is RadioButton Then rb = CType(c, RadioButton) AddHandler rb.Click, AddressOf RadioButtons_CheckedChanged End If Next End Sub
In RadioButtons_CheckedChanged wird das Ende des Steuerelementnamens (also z.B. "Red" für das Steuerelement RadioButtonRed) ermittelt und als Grundlage für die Select-Case-Konstruktion verwendet. Private Sub RadioButtons_CheckedChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs) Dim rb As RadioButton Dim rbname As String Dim col As Color
696
14 Steuerelemente
If TypeOf sender Is RadioButton Then rb = CType(sender, RadioButton) If rb.Checked Then ' macht aus "RadioButtonRed" einfach nur "Red" rbname = Mid(rb.Name, Len("RadioButton") + 1) Select Case rbname Case "Red" col = Color.Red Case "Green" col = Color.Green Case "Blue" col = Color.Blue Case "White" col = Color.White Case "Black" col = Color.Black Case Else MsgBox("Der RadioButton " + rb.Name + _ " wird in RadioButtons_CheckedChanged " + _ "nicht berücksichtigt.") End Select PictureBox1.BackColor = col End If End If End Sub
Ob dieser Code nun leichter zu lesen, besser zu warten oder effizienter zu erstellen ist als eine Reihe von Ereignisprozeduren (für jeden RadioButton eine, wobei von dort natürlich auch eine gemeinsame Prozedur aufgerufen werden kann), müssen Sie selbst entscheiden. Sinnvoll wird die hier präsentierte Vorgehensweise wohl nur sein, wenn die Anzahl der gleichartigen Steuerelemente relativ groß ist.
14.12 Neue Steuerelemente programmieren Grundsätzlich gibt es drei Möglichkeiten zur Programmierung neuer Steuerelemente: •
Am einfachsten ist es, ein vorhandenes Steuerelement zu verändern. Dazu starten Sie ein KLASSENBIBLIOTHEK-Projekt, definieren eine neue Klasse durch die Vererbung eines vorhandenen Steuerelements und statten die Klasse mit zusätzlichen Methoden oder Eigenschaften aus.
•
Schon wesentlich aufwendiger ist die zweite Variante, bei der Sie mehrere vorhandene Steuerelemente zu einem neuen Steuerelement kombinieren. Dazu starten Sie ein neues Projekt vom Typ WINDOWS-STEUERELEMENTBIBLIOTHEK. Nun können Sie die vorgegebenen Windows.Forms-Steuerelemente in den Design-Bereich einfügen. (Die Vorgehensweise ist
14.12 Neue Steuerelemente programmieren
697
ganz ähnlich wie die bei der Gestaltung eines Formulares.) Intern wird das neue Steuerelement von der Klasse UserControl abgeleitet. •
Wenn auch die zweite Variante für Sie zu wenig Flexibilität bietet, müssen Sie das neue Steuerelement von Grund auf neu programmieren. In diesem Fall beginnen Sie ähnlich wie bei Variante 1 mit einem KLASSENBIBLIOTHEK-Projekt und verwenden nun als Ausgangsklasse nicht ein bestimmtes Steuerelement, sondern die Klasse Control.
Die beiden folgenden Abschnitte gehen etwas detaillierter auf die beiden ersten Varianten ein. Eine Beschreibung der dritten Varianten ist hier leider aus Platzgründen nicht möglich. Der größte Unterschied im Vergleich zu den beiden ersten Varianten besteht darin, dass Sie nun auch für die Darstellung des Steuerelements selbst verantwortlich sind. Dazu verwenden Sie die Paint-Ereignisprozedur. (Hintergrundinformationen zu diesem Ereignis und zu den zahlreichen .NET-Grafikmethoden finden Sie in Kapitel 16.)
VERWEIS
Über die Programmierung neuer Steuerelemente könnte man ein eigenes kleines Buch schreiben – aber so viel Platz ist hier leider nicht. Dieser Abschnitt gibt daher nur eine erste, beispielorientierte Einführung in das Thema. Weitere Informationen finden Sie in der Online-Hilfe, wenn Sie nach Entwickeln von Windows Forms-Steuerelementen bzw. Grundlagen der Komponentenprogrammierung bzw. .NET-Beispiele Windows Forms: Erstellen von Steuerelementen suchen. ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconcreatingwinformscontrols.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconcomponentprogrammingessentials.htm ms-help://MS.VSCC/MS.MSDNVS.1031/Cpqstart/html/ cpsmpnetsamples-windowsformscontrolauthoring.htm
Der folgende Artikel setzt sich mit der Frage auseinander, welcher Code beim Einfügen eines Steuerelements in ein Formular eingefügt wird (Vom Windows Form Designer generierter Code) und welchen Einfluss Sie dabei auf die Entwicklung eines Steuerelements oder einer anderen Komponente haben. (Der Autor dieses und zahlreicher anderer kompetenter Artikel zu diesem Thema ist Shawn Burke. Wenn Sie also im Internet nach Informationen suchen, steigert dieser Name als zusätzlicher Suchbegriff die Relevanz der Treffer erheblich.) http://msdn.microsoft.com/library/en-us/dndotnet/html/custcodegen.asp
Einen weiteren brauchbaren (ausnahmsweise sogar deutschsprachigen) Artikel finden Sie hier: http://de.gotdotnet.com/quickstart/winforms/doc/WinFormsCreatingControls.aspx
14.12.1 Vererbte Steuerelemente In .NET können Sie dank Vererbung von jeder Klasse eine neue Klasse ableiten. Steuerelemente sind aber ganz gewöhnliche Klassen. Daraus folgt: Sie können vorhandene Steuerelemente mit geringem Aufwand um neue Eigenschaften oder Methoden ergänzen. Wie das folgende Beispiel zeigt, ist dieser Ansatz auch dann sinnvoll, wenn Sie auf bereits
698
14 Steuerelemente
vorhandene Eigenschaften oder Methoden zugreifen möchten, die aufgrund ihrer ProtectedDeklaration unzugänglich sind.
Beispiel – Neues Steuerelement erstellen Das folgende Beispiel ist aus dem Wunsch entstanden, die Methode SetStyle für ein PictureBox-Steuerelement zugänglich zu machen. (Vielleicht fragen Sie sich, wie ich gerade auf SetStyle komme: Diese in Abschnitt 16.5.7 vorgestellte Methode ermöglicht es, bestimmte interne Eigenschaften von Steuerelementen zu ändern. Das ist für manche Anwendungen bei der Grafikprogrammierung wichtig. Beispielsweise erreichen Sie mit SetStyle(ControlStyles.ResizeRedraw, True), dass bei jeder Größenänderung des Steuerelements das gesamte Steuerelement neu gezeichnet wird. Per Default werden nur die Teile neu gezeichnet, die bei einer Größenänderung hinzukommen.) SetStyle ist für die Klasse Control definiert, von der PictureBox abgeleitet ist. Leider ist die Methode mit Protected Sub deklariert, weswegen sie in einem normalen Formularcode nicht verwendet werden kann. PictureBox1.SetStyle(...) liefert daher nur eine Fehlermeldung.
Um diese Einschränkung zu umgehen, starten Sie ein neues Projekt vom Typ KLASSENIm PROJEKTMAPPEN EXPLORER richten Sie einen Verweis auf die Bibliothek System.Windows.Forms ein. Des Weiteren ändern Sie bei den Projekteigenschaften den STAMMNAMENSPACE (das ist der ROOT NAMESPACE) und geben dort den Namen MyControls an. Schließlich geben Sie die folgenden Zeilen als Code für die einzige Klasse dieses Projekts an: BIBLIOTHEK.
' Beispiel steuerelemente\control-inheritance\MyPictureBox1 Imports System.Windows.Forms Public Class MyPictureBox Inherits PictureBox Public Sub MySetStyle( _ ByVal flag As System.Windows.Forms.ControlStyles, _ ByVal value As Boolean) Me.SetStyle(flag, value) End Sub End Class
Fertig! Es steht Ihnen jetzt ein neues Steuerelement mit dem Namen MyControls.MyPictureBox zur Verfügung, das sich vom ursprünglichen Steuerelement PictureBox durch ein winziges Detail unterscheidet – nämlich durch die zusätzliche Methode MySetStyle. Diese Methode macht die geschützte Methode SetStyle der PictureBox global verfügbar. (Noch eleganter wäre es natürlich, die ursprüngliche Methode SetStyle mit Overrides durch eine neue, gleichnamige Methode zu überschreiben. Aber das geht nur, wenn die ursprüngliche Methode mit dem Attribut Overrideable deklariert ist – und das ist bei SetStyle nicht der Fall.) Damit Sie das neue Steuerelement in anderen Anwendungen nutzen können, müssen Sie das Projekt noch kompilieren. Das Ergebnis ist eine neue Bibliothek (name.dll).
14.12 Neue Steuerelemente programmieren
699
Beispiel – Das Steuerelement nutzen Nun beginnen Sie ein neues, gewöhnliches Projekt (WINDOWS-ANWENDUNG). In der TOOLBOX führen Sie per Kontextmenü das Kommando TOOLBOX ANPASSEN aus. Im nun erscheinenden Dialog klicken Sie das Dialogblatt .NET FRAMEWORK KOMPONENTEN und suchen dann mit DURCHSUCHEN die Bibliotheksdatei name.dll Ihres neuen Steuerelements. Damit wird das Steuerelement in der Toolbox sichtbar. Von dort können Sie es in Ihr Formular einfügen – es verhält sich wie ein ganz normales Steuerelement. Indem Sie das Steuerelement in das Formular einfügen, wird dem Projekt automatisch ein Verweis auf die Bibliotheksdatei name.dll hinzugefügt. Darüber hinaus wird die Bibliothek in das lokale Projektverzeichnis kopiert, so dass es später bei der Weitergabe des Programms keine Probleme geben kann (etwa wenn sich das Verzeichnis geändert hat, in dem sich name.dll ursprünglich befand). Das folgende Beispielprogramm hat eine einfach Aufgabe: Im Bildfeld soll eine diagonale Linie gezeichnet werden. Wenn Sie das mit einer normalen PictureBox machen, kommt es bei einer Größenänderung zu Darstellungsfehlern (siehe Abbildung 14.45 rechts). Mit MyPictureBox können diese Probleme dank MyPictureBox1.MySetStyle(ControlStyles.ResizeRedraw, True) vermieden werden. (Die Hintergründe dieses Problems sind in Abschnitt 16.5.4 im Zusammenhang mit Paint- und Resize-Ereignissen beschrieben.)
Abbildung 14.45: Links ein Fenster mit MyPictureBox, rechts ein Fenster mit einer gewöhnlichen PictureBox
Beide Fenster des Beispielsprogramms sind gleich aufgebaut: Es ist jeweils ein Bildfeld (MyPictureBox bzw. PictureBox) enthalten, wobei das Bildfeld mit BorderStyle mit einem Rand ausgestattet ist und an allen vier Seiten verankert ist (so dass es seine Größe automatisch mit der Fenstergröße ändert). Die Paint-Ereignisprozedur sieht jeweils so aus: ' Beispiel steuerelemente\controls-inheritance\useMyPictureBox Private Sub [My]PictureBox1_Paint(..) Handles [My]PictureBox1.Paint e.Graphics.DrawLine(Pens.Black, 0, 0, _ [My]PictureBox1.ClientSize.Width, _ [My]PictureBox1.ClientSize.Height) End Sub
Das unterschiedliche Verhalten ergibt sich daraus, dass für MyPictureBox1 in Form1_Load die folgende Initialisierung durchgeführt wird:
700
14 Steuerelemente
Private Sub Form1_Load(...) Handles MyBase.Load MyPictureBox1.MySetStyle(ControlStyles.ResizeRedraw, True) End Sub
14.12.2 Steuerelemente zusammensetzen Um ein neues Steuerelement aus vorhandenen Steuerelementen zusammenzusetzen, beginnen Sie ein Projekt des Typs WINDOWS-STEUERELEMENTBIBLIOTHEK. In der Entwicklungsumgebung erscheint nun ein Zeichenbereich, der so ähnlich aussieht wie bei einem Formular. Auch die Gestaltung des Zeichenbereichs – also das Einfügen von Steuerelementen, die Einstellung von deren Eigenschaften, die Programmierung von Ereignisprozeduren – erfolgt wie bei einem Formular. Intern basiert die neue Klasse allerdings nicht auf einer Formular (Form-Klasse), sondern auf der UserControl-Klasse. Diese Basisklasse ist speziell für die Programmierung eigener Steuerelemente vorgesehen. Die folgende Hierarchiebox zeigt, dass UserControl selbst von den gleichen Basisklassen abgeleitet ist wie Form und daher ganz ähnliche Eigenschaften hat. Klassenhierarchie im System.Windows.Forms-Namensraum ...
└─ Control └─ ScrollableControl └─ ContainerControl │ ├─ Form └─ UserControl
Basisklasse für alle Steuerelemente automatisches Scrolling des Inhalts Verwaltung des Eingabefokus für die enthaltenen Steuerelemente Klasse für Formlare, Fenster und Dialoge; davon Basisklasse für eigene Steuerelemente
Steuerelement testen Ein Steuerelement kann nicht für sich getestet werden, sondern muss in ein Formular eingefügt werden. Um das neu programmierte Steuerelement zu testen, fügen Sie daher am besten ein zweites Projekt des Typs WINDOWS-ANWENDUNG zur aktuellen Projektmappe hinzu und machen dieses zum STARTPROJEKT. Anschließend fügen Sie das neue Steuerelement in die Toolbox ein (TOOLBOX ANPASSEN) und von dort in das Formular der WindowsAnwendung.
Attribute Welche Eigenschaften eines Steuerelements werden im Eigenschaftsfenster angezeigt und welche nicht? Wie kann die Eigenschaft eingestellt werden (z.B. mit einer Dropdown-Liste, in mehreren Detailfeldern etc.)? Welches ist das Defaultereignis eines Steuerelement, dessen Codeschablone durch einen Doppelklick eingefügt wird? Alle diese Informationen werden durch Attribute gesteuert, die am Beginn der Klasse bzw. bei der Definition einer
14.12 Neue Steuerelemente programmieren
701
Eigenschaft angegeben werden können. Die folgende Aufzählung beschreibt einige elementare Attribute: •
<System.ComponentModel.DefaultEvent("ereignisname")> gibt an, welches das Defaultereig-
nis des Steuerelements ist. Das Attribut muss bei der Klassendefinition angegeben werden (nicht beim betreffenden Ereignis!). •
<System.ComponentModel.Browsable(False)> gibt an, dass die folgende Eigenschaft nicht im Eigenschaftsfenster angezeigt werden soll.
Per Default werden alle öffentlichen Eigenschaften (Public Property xy) angezeigt. Je nach Datentyp der Eigenschaft kann das allerdings zu Problemen bei der Eigenschaftseinstellung oder Speicherung führen. Diese Probleme können durch zusätzlichen Code – z.B. zur Typkonvertierung – gelöst werden. Diese Feinheiten der Programmierung neuer Steuerelemente werden hier aber nicht beschrieben. •
<System.ComponentModel.Category("eigenschaftsgruppe")> gibt an, in welcher Eigenschafts-
gruppe die Eigenschaft in der Entwicklungsumgebung angezeigt werden soll. Zulässige Zeichenketten sind z.B. "Appearance", "Behavior", "Format" etc. •
<System.ComponentModel.Description("kurzbeschreibung")> gibt eine Kurzbeschreibung zur
Eigenschaft bzw. zum Ereignis an.
VERWEIS
Neben diesen Attributen gibt es eine Reihe weiterer, mit denen Sie beispielsweise Datentyp-Konverter, Eigenschaftsdialoge etc. mit einer Eigenschaft verbinden können. Allerdings ist es dabei mit der Angabe eines Attributs nicht mehr getan: Sie müssen auch zusätzlichen Code entwicklen, der die durch die Attribute angegebenen Funktionen ausführt. In manchen Fällen nimmt die die Programmierung dieser Zusatzfunktionen mehr Zeit in Anspruch als die Programmierung des eigentlichen Steuerelements. Aus Platzgründen ist es hier nicht möglich, detailliert auf weitere Attribute einzugehen. Weitere Informationen finden Sie in der Online-Hilfe, wenn Sie nach Entwurfszeitattribute für Komponenten oder nach Erweitern der Entwurfszeitfunktionalität suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpcondesign-timeattributesforcomponents.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconenhancingdesign-timesupport.htm
Eine ganze Menge weiterer Informationen gibt der folgende, sehr praxisnahe Artikel: http://msdn.microsoft.com/library/en-us/dndotnet/html/vsnetpropbrow.asp
14.12.3 UserControl-Beispiel Ziel dieses Beispiels ist die Programmierung der neuen IPTextBox, die eine komfortable Eingabe von Internet-Adressen (IP4-Adressen) ermöglicht. IP-Adressen bestehen aus vier Zahlen in der Form 192.168.0.1. Jede der vier durch einen Punkt getrennten Zahlen muss zwischen 0 und 255 liegen.
702
14 Steuerelemente
Im Eingabefeld können Sie mit Tab, mit den Cursortasten sowie durch die Eingabe eines Punktes zwischen den vier Gruppen hin und her springen. Als Eingabe werden nur maximal dreistellige Zahlen akzeptiert. Eine leere Eingabe wird als 0 interpretiert. Wenn eine Zahl größer 255 eingegeben wird, wird die Eingabe rot markiert; das Textfeld kann dann nicht verlassen werden.
Abbildung 14.46: Test der IPTextBox
Programmierschnittstelle (Methoden, Eigenschaften und Ereignisse) Gegenüber den Standardschlüsselwörtern der UserControl-Klasse kennt IPTextBox die folgenden zusätzlichen Methoden, Eigenschaften und Ereignisse. Text (Eigenschaft)
ermittelt bzw. verändert die im Steuerelement angezeigte IP-Adresse. Die Zeichenkette hat die Form "111.112.113.114". Falls das Steuerelement gerade eine ungültige Eingabe enthält, liefert Text die Adresse "0.0.0.0".
IsValidAddress (Methode) ermittelt, ob das Steuerelement momentan eine gültige Adresse enthält (True/False). GetIPAddress (Methode)
liefert die im Steuerelement angezeigte IP-Adresse als Net.IPAddress-Objekt.
SetIPAddress (Methode)
verändert die im Steuerelement angezeigte Adresse. Als Parameter muss ein Net.IPAddress-Objekt angegeben werden.
Change (Ereignis)
gibt an, dass sich die eingegebene IP-Adresse geändert hat.
Interner Aufbau Obwohl das IPTextBox-Steuerelement wie ein einfaches Textfeld aussieht, ist es in Wirklichkeit aus ziemlich vielen Einzelkomponenten zusammengesetzt: UserControl1
dient als Zeichenfläche und Basisklasse für das IPTextBoxSteuerelement. Als einzige Eigenschaft wurde BackColor=SystemColors.Window verändert.
Panel1
füllt dank Dock=Fill die gesamte UserControl-Zeichenfläche aus. Außerdem wurde BorderStyle=Fixed3D eingestellt, um dem IPTextBox ein 3D-Aussehen wie bei einem gewöhnlichen Textfeld zu geben.
14.12 Neue Steuerelemente programmieren
703
TextBox1-4
dienen zur Eingabe der vier IP-Zahlen. Mit MaxLength=3 ist die Eingabe auf drei Zeichen limitiert. BorderStyle=None vermeidet die Anzeige einer Umrandung für jede TextBox. Mit TextAlign=Center wird der Eingabetext zentriert.
Label1-3
enthalten jeweils das Zeichen ".". Die Größe der Steuerelemente passt sich dank AutoSize=True an die Größe der Schriftart an.
Die Steuerelemente TextBox1, Label1, TextBox2, Label2 etc. sind in dieser Reihenfolge an den linken Rand gedockt. Das bewirkt, dass sich ihre Höhe automatisch an die Panel-Höhe anpasst. Die Breite von Label1-3 ist dank AutoSize vorgegeben. Die Breite der Textfelder wird in IPTextBox_Resize so eingestellt, dass sie den gesamten zur Verfügung stehenden Raum einnimmt.
Programmcode Der gesamte Code für das neue Steuerelement befindet sich in der Datei IPTextBox.vb, die wiederum die Klasse IPTextBox definiert. IPTextBox ist von UserControl abgeleitet. (Die Inherits-Anweisung wird automatisch von der Entwicklungsumgebung eingefügt.) Die Attributinformation DefaultEvent("Changed") gibt an, dass Changed das Defaultereignis des Steuerelements ist. Es folgt dann der Codeblock Vom Windows Form Designer generierter Code, der alle Verwaltungsinformationen zu den Steuerelementen im UserControl enthält. Der restliche Code wurde mit #Region in drei weitere Blöcke gegliedert, die bei den nachfolgenden Überschriften beschrieben werden. ' Beispiel steuerelemente\new-control\iptextbox <System.ComponentModel.DefaultEvent("Changed")> _ Public Class IPTextBox Inherits System.Windows.Forms.UserControl [... Vom Windows Form Designer generierter Code ...] [... Klassenvariablen, Initialisierung ...] [... interne Administration und Ereignisse ...] [... öffentliche Eigenschaften, Methoden und Ereignisse ...] End Class
Klassenvariablen, Initialisierung Die Klassenvariable initialized gibt an, ob bereits die internen Initialisierungsarbeiten durchgeführt wurden. Die Variable wird an unterschiedlichen Stellen im Code ausgewertet, um eine mehrfache Initialisierung zu vermeiden. textcolor enthält die Default-Textfarbe in den drei TextBox-Steuerelementen. Die Variable
wird benötigt, um die Textfarbe wieder herzustellen, nachdem ein durch eine rote Textfarbe signalisierter Eingabefehler behoben wird.
704
14 Steuerelemente
textboxes enthält Verweise auf die vier TextBox-Steuerelemente. Der Zweck dieser Variablen besteht darin, dass die TextBox-Steuerelemente an verschiedenen Orten im Programm kom-
fortabel in einer Schleife angesprochen werden können. Die Prozedur Initialize initialisiert die gerade erwähnten Variablen. Außerdem werden für einige Ereignisse der TextBox-Steuerelemente gemeinsame Ereignisprozeduren eingerichtet. ' Textfarbe für Steuerelemente Private initialized As Boolean = False Private textcolor As Color Private textboxes(3) As TextBox Private Sub UserControl1_Load(...) Handles MyBase.Load If Not initialized Then Initialize() End Sub Private Sub Initialize() Dim tb As TextBox textcolor = TextBox1.ForeColor textboxes(0) = TextBox4 'erster Block (niedrigwertigster) textboxes(1) = TextBox3 textboxes(2) = TextBox2 textboxes(3) = TextBox1 'letzter Block (höchstwertiger) For Each tb In textboxes AddHandler tb.KeyPress, AddressOf TextBoxes_KeyPress AddHandler tb.TextChanged, AddressOf TextBoxes_TextChanged AddHandler tb.KeyDown, AddressOf TextBoxes_KeyDown AddHandler tb.Validating, AddressOf TextBoxes_Validating AddHandler tb.Enter, AddressOf TextBoxes_Enter Next initialized = True End Sub
Interne Administration und Ereignisse TextBoxes_TextChanged wird bei jeder Texteingabe in einer TextBox aufgerufen. Die Prozedur hat zwei Aufgaben: Zum einen erfolgt eine Kontrolle auf Eingabefehler; falls ein Fehler festgestellt wird, wird der Text rot dargestellt. Zum anderen wird das Change-Ereignis für das Steuerelement ausgelöst. Private Sub TextBoxes_TextChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim tb As TextBox = CType(sender, TextBox) If (Not IsNumeric(tb.Text)) OrElse (Val(tb.Text) > 255) Then tb.ForeColor = Color.Red Else tb.ForeColor = textcolor
14.12 Neue Steuerelemente programmieren
705
End If RaiseEvent Change(Me, New EventArgs()) End Sub TextBoxes_KeyPress verhindert die Eingabe von Buchstaben und anderen Sonderzeichen in den TextBox-Steuerelementen. Außer Ziffern wird nur das Zeichen BackSpace akzeptiert. Die Eingabe von . und , führt dazu, dass mit SendKeys.Send ein Fokuswechsel zum nächsten Eingabefeld durchgeführt wird (es sei denn, TextBox4, also das letzte Eingabefeld, hat bereits den Fokus). Private Sub TextBoxes_KeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) 'Backspace korrekt verarbeiten If e.KeyChar = vbBack Then Exit Sub If e.KeyChar = "." Or e.KeyChar = "," Then ' mit . oder , zum nächsten Feld springen e.Handled = True If sender Is TextBox4 Then Exit Sub SendKeys.Send("{TAB}") ElseIf e.KeyChar < "0" Or e.KeyChar > "9" Then ' ansonsten alles außer 0-9 blockieren e.Handled = True End If End Sub
In der KeyDown-Ereignisprozedur wird die Eingabe der Cursor-Tasten festgestellt. Mit SendKeys.Send wird dann ein Fokuswechsel in das vorige bzw. nächste Textfeld ausgelöst. Private Sub TextBoxes_KeyDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyEventArgs) Dim tb As TextBox = CType(sender, TextBox) If e.KeyCode = Keys.Right Then If tb Is TextBox4 Then Exit Sub If tb.SelectionStart + tb.SelectionLength = tb.TextLength Then e.Handled = True SendKeys.Send("{TAB}") End If ElseIf e.KeyCode = Keys.Left Then If tb Is TextBox1 Then Exit Sub If tb.SelectionStart + tb.SelectionLength = 0 Then e.Handled = True SendKeys.Send("+{TAB}") End If End If End Sub
706
14 Steuerelemente
Die Validating-Ereignisprozedur verhindert, dass ein Textfeld mit einem Eingabefehler verlassen werden kann. Wenn gar nichts eingegeben wurde, ist die Prozedur tolerant und fügt das Zeichen 0 in die Textbox ein. Private Sub TextBoxes_Validating(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Dim tb As TextBox = CType(sender, TextBox) If Trim(tb.Text) = "" Then tb.Text = "0" ElseIf (Not IsNumeric(tb.Text)) OrElse (Val(tb.Text) > 255) Then e.Cancel = True End If End Sub
Als Eingabeerleichterung wird bei jedem Fokuswechsel der gesamte Inhalt der Textbox ausgewählt. Das ermöglicht ein bequemes Überschreiben des vorhandenen Texts. Private Sub TextBoxes_Enter(ByVal sender As Object, _ ByVal e As System.EventArgs) CType(sender, TextBox).SelectAll() End Sub IPTextBox_Resize passt die Größe der TextBox-Steuerelemente an die des Gesamtsteuerele-
ments an. ' Textboxes an die Größe des Gesamtsteuerelements anpassen Private Sub IPTextBox_Resize(...) Handles MyBase.Resize Dim newwidth As Integer If Me.ClientSize.Width <> TextBox4.Right Then newwidth = CInt((Me.ClientSize.Width - Label1.Width * 3) / 4) If newwidth > 0 Then TextBox1.Width = newwidth TextBox2.Width = newwidth TextBox3.Width = newwidth TextBox4.Width = newwidth End If End If End Sub
Öffentliche Eigenschaften, Methoden und Ereignisse Die folgenden Zeilen dienen zur formalen Definition des Change-Ereignisses. (Das Ereignis wird in der oben schon beschriebenen Prozedur TextBoxes_TextChanged ausgelöst.) Public Delegate Sub ChangeDelegate(ByVal sender As Object, _ ByVal e As EventArgs) Public Event Change As ChangeDelegate
14.12 Neue Steuerelemente programmieren
707
Die Implementierung der Eigenschaft Text ist relativ aufwendig, weil damit die Inhalte von vier TextBox-Steuerelementen ausgelesen bzw. verändert werden. Der Get-Teil überprüft zuerst, ob eine korrekte Eingabe vorliegt. Wenn das der Fall ist, wird die IP-Adresse zusammengesetzt, andernfalls wird 0.0.0.0 zurückgegeben. Der Set-Teil ist durch eine Try-Catch-Konstruktion gegen allfällige Fehlzuweisungen abgesichert. Die übergebene Zeichenkette wird mit Split in ein Feld zerlegt, die einzelnen Elemente werden dann in einer Schleife an die TextBox-Steuerelemente zugewiesen. Durch das Attribut Browsable(False) wird erreicht, dass Text nicht im Eigenschaftsfenster aufscheint. Der Grund für diese Vorgehensweise besteht darin, dass Text im Eigenschaftsfenster zwar problemlos eingestellt werden könnte, dass diese Einstellung aber nicht gespeichert wird. <System.ComponentModel.Browsable(False)> _ Public Overrides Property Text() As String ' Text-Eigenschaft zusammensetzen Get If IsValidAddress() Then Return TextBox1.Text + "." + _ TextBox2.Text + "." + _ TextBox3.Text + "." + _ TextBox4.Text Else Return "0.0.0.0" End If End Get ' Textboxes neu einstellen Set(ByVal Value As String) Dim i As Integer If Not initialized Then Initialize() Try Dim s() As String s = Split(Value, ".") For i = 0 To 3 If IsNumeric(s(i)) AndAlso Val(s(i)) < 256 Then textboxes(3 - i).Text = s(i) Else textboxes(3 - i).Text = "0" End If Next Catch For i = 0 To 3 textboxes(i).Text = "0" Next End Try
708
14 Steuerelemente
End Set End Property
Mit den Methoden Get- und SetIPAddress kann der Inhalt des Steuerelements als Net.IPAddress-Objekt ausgelesen bzw. mit einem Net.IPAddress-Objekt initialisiert werden. Da GetIPAddress auf Text-Get zurückgreift (siehe oben), liefert die Methode 0.0.0.0, wenn keine gültige Eingabe vorliegt. Public Function GetIPAddress() As Net.IPAddress Return Net.IPAddress.Parse(Me.Text) End Function Public Sub SetIPAddress(ByVal ip As Net.IPAddress) Me.Text = ip.ToString End Sub IsValidAddress testet, ob das Steuerelement momentan eine korrekte IP-Adresse enthält. Public Function IsValidAddress() As Boolean Dim i As Integer Dim tb As TextBox For i = 0 To 3 tb = textboxes(i) If (Not IsNumeric(tb.Text)) OrElse (Val(tb.Text) > 255) Then Return False End If Next Return True End Function
15 Gestaltung von Benutzeroberflächen Nachdem im vorigen Kapitel die einzelnen Steuerelemente vorgestellt wurden, steht in diesem Kapitel deren Zusammenspiel bei der Gestaltung von Benutzeroberflächen im Vordergrund. Das Kapitel beschreibt detailliert den Umgang mit Formularen bzw. Fenstern, wobei auch auf Spezialthemen eingegangen wird. Beispielsweise finden Sie Hintergrundinformationen zu dem von der Entwicklungsumgebung erzeugten Code (Vom Windows Form Designer generierter Code), Tipps zum Anzeigen mehrerer Fenster in eigenen Threads sowie eine Anleitung zur Entwicklung mehrsprachiger Anwendungen. Die weiteren Abschnitte beschreiben Aspekte, die zur Entwicklung typischer Windows-Anwendungen notwendig sind: Verwaltung von Tastatur und Maus, Umgang mit der Zwischenablage, Reaktion auf Drag&Drop-Operationen, Gestaltung von Menüs und Symbolleisten etc. 15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10 15.11 15.12
Formularspezifische Eigenschaften, Methoden und Ereignisse Formularinterna Verwaltung mehrerer Fenster MDI-Anwendungen Standarddialoge Menüs Symbolleiste (ToolBar) Statusleiste (StatusBar) Tastatur Maus Zwischenablage Drag&Drop
710 718 757 768 777 780 795 801 804 809 814 817
710
15.1
15 Gestaltung von Benutzeroberflächen
Formularspezifische Eigenschaften, Methoden und Ereignisse
Vorweg zeigt der folgende Hierarchiekasten – als kurze Wiederholung aus dem einleitenden Windows.Forms-Kapitel – noch einmal die Einordnung der Form-Klasse in der Klassenhierarchie. Klassenhierarchie im System.Windows.Forms-Namensraum ...
└─ Control └─ ScrollableControl │ └─ ContainerControl │ └─ Form
Basisklasse für alle Steuerelemente Basisklasse für Steuerelemente, die automatisches Scrolling des Inhalts erlauben (z.B. Panel) Basisklasse zur Verwaltung des Eingabefokus für die enthaltenen Steuerelemente Klasse für Formulare, Fenster und Dialoge
Aus diesem Hierarchiebaum folgt, dass in Formularen alle Eigenschaften, Methoden und Ereignisse zur Verfügung stehen, die auch das gemeinsame Fundament aller Steuerelemente darstellen (siehe Abschnitt 14.2). Des weiteren können in Formularen fast alle vom Panel-Steuerelement vertrauten Schlüsselwörter verwendet werden (siehe Abschnitt 14.9.2). Aus diesem Grund geht dieser Abschnitt nur auf Eigenschaften, Methoden und Ereignisse ein, die entweder nur für die Form-Klasse definiert sind oder normalerweise nur im Kontext eines Formulares interessant sind und bisher nicht beschrieben wurden.
15.1.1 Aussehen Beschriftung, Grafik Die vertraute Eigenschaft Text gibt an, welcher Text im Fenstertitel angezeigt werden soll. Icon bestimmt, welches Icon im linken oberen Eck bzw. in der Taskleiste angezeigt werden soll. Ob das Fenster in der Taskleiste überhaupt sichtbar sein soll, bestimmt ShowInTaskbar. (Bei modalen Dialogen sollte diese Eigenschaft auf False gesetzt werden.) BackgroundImage verweist auf eine Bitmap, die als Hintergrundgrafik für Fenster verwendet wird. Falls die Bitmap kleiner als das Fenster ist, wird sie periodisch wiederholt, um den gesamten Hintergrund auszufüllen. Alle Grafikeffekte, die sich nicht durch BackgroundImage erzielen lassen, können entweder durch ein PictureBox-Steuerelement oder durch eigenen Code realisiert werden. Dazu tritt immer dann, wenn der Fensterhintergrund neu zu zeichnen ist, ein Paint-Ereignis auf. Ausführliche Informationen zu diesem Ereignis erhalten Sie in Kapitel 16.
15.1 Formularspezifische Eigenschaften, Methoden und Ereignisse
711
Fensterzustand, Sichtbarkeit WindowState gibt den aktuellen Zustand des Fensters (Normal, Minimized oder Maximized) an. Durch die Veränderung der Eigenschaft per Code kann das Fenster beispielsweise in ein Icon verkleinert werden. Visible gibt an, ob das Fenster zurzeit sichtbar ist oder nicht. Die Eigenschaft kann entweder direkt oder mit den Methoden Hide bzw. Show verändert werden. Bei jeder Änderung von Visible tritt das Ereignis VisibleChanged auf. (Dieses Ereignis tritt beispielsweise auch
dann auf, wenn das Fenster zum ersten Mal sichtbar wird.)
Gestaltung des Fensterrahmens FormBorderStyle gibt an, wie die Fensterumrandung gestaltet werden soll. Abbildung 15.1
zeigt die möglichen Einstellungen dieser Eigenschaft. (Beim Fenster in der ersten Spalten unten gilt FormBorderStyle=None.) Die Defaulteinstellung lautet Sizeable. Die vier FixedXxxVarianten zeichnen sich dadurch aus, dass die Fenstergröße nicht verändert werden kann. Die Unterschiede betreffen lediglich die Gestaltung des Fensterrahmens (mit oder ohne Icon, mit oder ohne 3D-Rahmen etc.). Fenster mit FormBorderStyle=None können weder verschoben noch geschlossen werden. Sie eignen sich daher nur für Spezialanwendungen, etwa zur vorübergehenden Anzeige eines so genannten Splash-Fensters während des Programmstarts.
Abbildung 15.1: Verschiedene Fensterrahmen
Die Eigenschaften ControlBox, MaximizeBox, MinimizeBox und HelpButton geben an, ob in der Titelleiste Buttons für das Fenstermenü, zum Maximieren bzw. Minimieren des Fensters sowie zum Aufruf eines Hilfetexts angezeigt werden sollen. Beachten Sie, dass der Hilfe-Button nur angezeigt wird, wenn Maximize- und MinimizeBox auf False gestellt werden. (Weitere Informationen zur Integration einer Hilfefunktion finden Sie in Abschnitt 14.10.4.)
712
15 Gestaltung von Benutzeroberflächen
SizeGripStyle gibt an, ob im rechten unteren Eck eines Fensters einige diagonale Linien angezeigt werden sollen, die andeuten, dass die Fenstergröße hier verändert werden kann. Die Defaulteinstellung lautet Auto und bewirkt seltsamerweise, dass diese Linien beim ersten Fenster einer Anwendung (Form1) nicht angezeigt werden, bei allen weiteren aber schon. Durch die Einstellungen Hide oder Show können Sie die Linien gezielt unterdrücken bzw. anzeigen.
Spezialeffekte Mit Opacity geben Sie durch einen Double-Wert an, wieweit das Fenster deckend dargestellt werden soll. Der Defaultwert 1 (also 100 Prozent) bedeutet, dass das Fenster vollkommen deckend ist. Mit 0 Prozent ist das Fenster inklusive aller seiner Steuerelemente vollkommen unsichtbar. Werte dazwischen bewirken, dass man durch das Fenster durchsehen kann. Mit TransparencyKey können Sie eine Farbe angeben, die als unsichtbar gilt. Jedes Pixel des Fensters bzw. seiner Steuerelemente, das exakt in dieser Farbe dargestellt würde, gilt dann als unsichtbar. So dargestellte Pixel können nicht angeklickt werden. (Auch für den Mausklick gilt die Transparenz, der Klick gilt dann für das dahinter befindliche Fenster.) (Halb-)durchsichtige Fenster werden nicht von allen Windows-Betriebssystemen unterstützt (zurzeit nur von Windows 2000/XP). Ob das der Fall ist, können Sie mit der Methode IsPresent auch per Programmcode feststellen: If OSFeature.Feature.IsPresent(OSFeature.LayeredWindows) Then ...
15.1.2 Position und Größe Die in Abschnitt 14.2.2 beschriebenen Eigenschaften und Methoden gelten auch für Formulare: Location, Left, Right, Top, Bottom, Width und Height beschreiben die Außenmaße bzw. die Position relativ zum Bildschirmpunkt (0,0). ClientSize.Width und .Height geben die Innenmaße an. Form-spezifisch sind dagegen die Eigenschaften DesktopLocation und -Bounds. Sie geben als Point- oder Rectangle-Objekt Position und Außengröße des Fensters an, und zwar relativ zum Desktop-Bereich des Bildschirms. (Der Desktop-Bereich ist der nutzbare Bereich des Bildschirms ohne die Taskleiste.) Wenn Sie das Fenster neu positionieren bzw. seine Größe ändern möchten, können Sie dazu die Methoden SetDesktopLocation oder SetDesktopBounds einsetzen.
Bei jeder Änderung der Fenstergröße treten (in dieser Reihenfolge) die Ereignisse Layout, Resize und SizeChanged auf. Das Layout-Ereignis tritt außerdem auch dann auf, wenn sich die Zahl der Steuerelemente im Fenster ändert. (Unterschiede zwischen Resize und SizeChanged sind nicht dokumentiert.) Die Layout-Ereignisprozedur eignet sich daher besonders für Code, der Steuerelemente in Abhängigkeit von der Fenstergröße und der SteuerElementzahl neu positioniert. Beachten Sie, dass zum Zeitpunkt, zu dem Layout auftritt, die automatische Neupositionierung von durch Anchor oder Dock verankerten Steuerelementen noch nicht erfolgt ist! Wenn Sie
15.1 Formularspezifische Eigenschaften, Methoden und Ereignisse
713
mehrere Steuerelemente einfügen oder entfernen, können Sie wiederholte Layout-Aufrufe vermeiden, indem Sie zuerst die Methode SuspendLayout und später ResumeLayout ausführen. StartPosition bestimmt, wo das Fenster erscheint. Zur Auswahl stehen die folgenden
Werte: •
CenterScreen: Das Fenster wird am Bildschirm zentriert.
•
CenterParent: Das Fenster wird über dem Ausgangsformular zentriert. Diese Einstellung ist z.B. sinnvoll, wenn bereits ein Fenster offen ist und ein zweites Fenster als Dialog angezeigt werden soll.
•
Manual: Hier können Sie die Position durch die Einstellung von Left und Top selbst bestimmen. (Die Maße des Bildschirms in Pixel können Sie aus dem Rectangle-Objekt Screen.Bounds entnehmen.)
•
WindowsDefaultLocation (die Defaulteinstellung): Windows entscheidet (nach eher will-
kürlichen Kriterien), wo das Fenster erscheinen soll. •
WindowsDefaultBounds: Wie WindowsDefaultLocation, aber nun wird auch die Größe durch
Windows bestimmt.
Fensterreihenfolge TopMost gibt an, dass das Fenster über allen anderen Fenstern angezeigt werden soll.
Mit den Methoden BringToFront und SendToBack kann ein Fenster (relativ zu den anderen Fenstern des laufenden Programms) nach vorne bzw. nach hinten bewegt werden. TopMost-Fenster befinden sich immer vor gewöhnlichen Fenstern, BringToFront und SendToBack können aber die Reihung mehrerer TopMost-Fenster verändern.
15.1.3 Verwaltung von Formularen und Steuerelementen Innerhalb der Formularklasse verweist das Schlüsselwort Me auf das Formularobjekt (also auf die aktuelle Instanz der Formularklasse, z.B. Form1). Das Schlüsselwort MyBase verweist auf das Objekt der Basisklasse des Formulars (das ist Form). Controls verweist auf alle direkt im Fenster enthaltenen Steuerelemente. (Beachten Sie aber, dass Steuerelemente – beispielsweise durch die Container GroupBox, TabControl oder Panel – verschachtelt werden können. Controls enthält lediglich eine Aufzählung der Steuerelemente der ersten Ebene.) Mit GetChildAtPoint können Sie feststellen, welches Steuerelement sich an einer bestimmten Position (z.B. unter der Maus) befindet. ActiveForm und -Control verweisen auf das Fenster bzw. auf das Steuerelement mit dem Eingabefokus. ParentForm verweist auf das übergeordnete Formular. Bei normalen Fenstern enthält die Eigenschaft Nothing. Sie ist nur bei MDI-Anwendungen relevant (oder wenn ganze Fenster in Container-Steuerelementen dargestellt werden) – siehe Abschnitt 15.4. (Dort werden
714
15 Gestaltung von Benutzeroberflächen
auch weitere MDI-spezifische Eigenschaften und Methode beschrieben, z.B. IsMdiContainer, MdiChildren und MdiParent.)
Fenster öffnen und schließen Das erste Fenster einer Windows-Anwendung erscheint automatisch. Bei allen weiteren Fenstern muss zuerst mit frm = New Formxxx() ein neues Formularobjekt erzeugt werden. Dabei tritt das Load-Ereignis auf. (Das Load-Ereignis tritt auf, bevor das Fenster sichtbar wird. Es eignet sich daher gut für Initialisierungsarbeiten. Beachten Sie aber, dass einige andere Ereignisse – insbesondere Resize und SizeChanged – bereits vor Load auftreten.) Ein neues Formxxx-Objekt kann dann mit Show oder ShowDialog angezeigt werden. Diese beiden Methoden sowie Tipps zur Verwaltung mehrerer Fenster werden ausführlich in Abschnitt 15.3 beschrieben. ShowDialog liefert als Rückgabewert einen Wert der DialogResult-Aufzählung. Innerhalb des Formularcodes kann dieser Rückgabewert durch eine Veränderung der DialogResult-
Eigenschaft eingestellt werden. Um das Fenster per Code wieder zu schließen, führen Sie Me.Close aus. In Sonderfällen (modale Dialoge, deren Objekt während des Programmablaufs erhalten bleibt) können Sie auch Me.Hide verwenden (siehe abermals Abschnitt 15.3). Bevor ein Fenster geschlossen wird, tritt ein Closing-Ereignis auf. Darin kann das Schließen durch e.Cancel = True verhindert werden. Andernfalls tritt wenig später ein Closed-Ereignis auf, bevor die Steuerelemente und schließlich das Fenster selbst aus dem Speicher entfernt werden (Dispose).
Verwaltung von Tastaturereignissen Fenster können nie den Tastaturfokus erhalten, weil sie selbst keine Tastaturereignisse verarbeiten. Daher treten die von Steuerelementen bekannten Ereignisse Enter und Leave nicht auf. Stattdessen gibt es die Ereignisse Activated und Deactivated, die dann auftreten, wenn ein Fenster aktiviert wird (beim Öffnen oder wenn es später angeklickt wird) bzw. wenn es deaktiviert wird (beim Schließen oder wenn ein anderes Fenster – egal ob vom selben oder von einem anderen Programm – angeklickt wird). Normalerweise verarbeiten Formulare selbst keine Tastaturereignisse, sondern überlassen diese Aufgabe dem Steuerelement, das gerade den Eingabefokus besitzt. Wenn Sie aber die Eigenschaft KeyPreview auf True setzen, dann treten die Ereignissse KeyDown, KeyPress und KeyUp zuerst für das Formularobjekt und erst danach für das Steuerelement auf. (Details zur Verarbeitung von Tastaturereignissen folgen in Abschnitt 15.9.) Die Eigenschaften AcceptButton und CancelButton können auf zwei Buttons verweisen, die dann durch Return bzw. Esc ausgewählt werden können (siehe auch Abschnitt 14.3.1).
15.1 Formularspezifische Eigenschaften, Methoden und Ereignisse
715
15.1.4 Ereignisreihenfolge
TIPP
Die Ereignisreihenfolge hat oft einen wichtigen Einfluss darauf, welcher Code in welcher Ereignisprozedur ausgeführt werden soll (bzw. fehlerfrei ausgeführt werden kann). Dieser Abschnitt gibt einige Informationen darüber, welche Ereignisse bei bestimmten Ereignissen auftreten. Um die Ereignisabfolge zu testen, habe ich das Beispielprogramm benutzeroberflaeche\forms-events verwendet. Sie können das Beispielprogramm gegebenenfalls für eigene Versuche übernehmen.
Beim Erzeugen eines neuen Fensters treten die folgenden Ereignisse auf: Resize, SizeChanged, Move, Load, Layout, VisibleChanged, Activated und Paint Das Fenster wird erst zwischen dem Layout- und dem VisibleChanged-Ereignis sichtbar. Die Prozeduren New und InitializeComponent in dem vom Windows Form Designer generierten Code werden vor allen Ereignissen (also noch vor Resize) ausgeführt. Wenn Sie ein neues Fenster per Code öffnen, sieht die Zuordnung des Ereignisflusses zum Code so aus:
HINWEIS
Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New Form2() 'löst Resize, SizeChanged aus frm.Show() 'löst Move, Load, Layout, VisibleChanged 'und Activated aus [weiterer Code ...] End Sub 'erst jetzt tritt das Paint-Ereignis auf
Eine wichtige Konsequenz aus der Ereignisabfolge besteht darin, dass Initialisierungsarbeiten, die in der Load-Ereignisprozedur durchgeführt werden, in den Resize-, SizeChanged- und Move-Ereignisprozeduren noch nicht vorausgesetzt werden dürfen! Gegebenenfalls müssen Sie Initialisierungscode, der vor Resize- oder Move ausgeführt werden soll, in die Prozeduren New oder InitializeComponent des vom Windows Form Designer generierten Code einfügen. Beachten Sie, dass es am Beginn von New aber noch gar keine Steuerelemente gibt!
Fenstergröße ändern: Layout, Resize, SizeChanged und Paint Wenn sich die Position oder Größe von Steuerelementen durch Anchoring ändert, erfolgt dies erst nach der Layout-Prozedur. Die aktuelle Fenstergröße kann bereits in der LayoutProzedur aus Size oder ClientSize ermittelt werden. Fenster in Icon verkleinern: Move, Layout, Resize, SizeChanged und Deactivate Beachten Sie, dass die Positions- und Größeneigenschaften bei der Darstellung des Fensters als Icon (Taskleisten-Button) recht ungewöhnliche Werte enthalten: Left und Top enthalten je -32000, Width und Height entsprechen der Größe des Buttons in der Taskleiste. Fenster wieder in Normalzustand bringen: Activated, Move, Layout, Resize, SizeChanged und Paint
716
15 Gestaltung von Benutzeroberflächen
Fenster maximieren: Move, Layout, Resize, SizeChanged und Paint Fenster schließen: Closing, Closed, VisibleChanged und Deactivate Nach Deactivate wird die Dispose-Prozedur in dem vom Windows Form Designer generierten Code ausgeführt.
15.1.5 Syntaxzusammenfassung Fensterspezifische Eigenschaften ActiveControl
verweist auf das Steuerelement mit dem Eingabefokus (oder Nothing).
ActiveForm
verweist auf das Fenster mit dem Eingabefokus (oder Nothing).
BackgroundImage
gibt ein Hintergrundbild an, das periodisch wiederholt wird, um den gesamten Fensterhintergrund auszufüllen.
ControlBox
gibt an, ob das Icon mit dem Fenstermenü angezeigt wird.
Controls
verweist auf die direkt im Formular enthaltenen Steuerelemente.
DesktopBounds
gibt die Position und Außengröße des Fensters innerhalb des Desktops an (Bildschirm minus Taskleiste).
DesktopLocation
gibt die Position des Fensters innerhalb des Desktops an (Bildschirm minus Taskleiste).
DialogResult
bestimmt, welcher Rückgabewert an ShowDialog übergeben werden soll, mit der das Fenster als modaler Dialog geöffnet wurde.
FormBorderStyle
bestimmt das Aussehen des Fensterrahmens.
FormParent
verweist auf das übergeordnete Fenster (üblicherweise Nothing).
HelpButton
gibt an, ob ein Hilfe-Button angezeigt wird (nur bei Maximize- und MinimizeBox=False).
Icon
bestimmt das Aussehen des Icons links oben.
IsMdiChild
gibt an, dass das Fenster ein MDI-Subfenster ist.
IsMdiContainer
gibt an, dass das Fenster als MDI-Hauptfenster dient und ein MDI-Subfenster enthalten kann.
KeyPreview
gibt an, ob Tastaturereignisse zuerst für das Formular und erst dann für Steuerelement mit dem Eingabefokus auftreten sollen.
MaximizeBox
gibt an, ob der Button zur Maximierung angezeigt wird.
MdiChildren
verweist auf die im Hauptfenster enthaltenen Subfenster.
15.1 Formularspezifische Eigenschaften, Methoden und Ereignisse
717
Fensterspezifische Eigenschaften MdiParent
verweist von einem MDI-Subfenster auf das übergeordnete Hauptfenster.
MinimizeBox
gibt an, ob der Button zur Minimierung angezeigt wird.
Opacity
gibt an, ob das Fenster deckend (1), vollkommen durchsichtig (0) oder halbdurchsichtig angezeigt werden soll (z.B. 0,5).
ShowInTaskbar
gibt an, ob das Fenster in der Taskleiste angezeigt werden soll.
SizeGripStyle
bestimmt, ob rechts unten diagonale Linien zur Größenveränderung angezeigt werden.
StartPosition
gibt an, wo das Fenster erscheinen soll.
Text
gibt den Fenstertitel an.
TopLevel
gibt an, ob es sich um ein selbstständiges Fenster handelt (True bei gewöhnlichen Fenstern, False bei MDISubfenstern und bei Fenstern, die in Steuerelementen dargestellt werden).
TopMost
gibt an, ob das Fenster über allen anderen Fenstern angezeigt werden soll.
TransparencyKey
gibt eine Farbe an, deren Pixel durchsichtig dargestellt werden sollen.
WindowState
gibt den Fensterzustand an (Normal, Minimized, Maximized).
Fensterspezifische Methoden Close
schließt das Fenster
GetChildAtPoint
ermittelt, welches Steuerelement sich an einer bestimmten Position befindet.
Hide
macht das Fenster unsichtbar (entspricht Visible=False).
LayoutMdi
ordnet die MDI-Subfenster neu an.
ResumeLayout
aktiviert die Layout-Ereignisse wieder (nach SuspendLayout).
SetDesktopBounds
verändert die Position und Außengröße des Fensters relativ zum Desktop-Bereich des Bildschirms.
SetDesktopLocation
verändert die Position des Fensters.
Show
macht ein Form-Objekt als gewöhnliches Fenster sichtbar.
ShowDialog
zeigt ein Form-Objekt als modalen Dialog an, der das restliche Programm bis zum Schließen blockiert.
SuspendLayout
verhindert vorübergehend Layout-Ereignisse (um mehrere Steuerelemente effizient einzufügen bzw. zu löschen).
718
15 Gestaltung von Benutzeroberflächen
Fensterspezifische Ereignisse Activated
tritt auf, wenn das Fenster den Eingabefokus erhält.
Deactivated
tritt auf, wenn das Fenster den Eingabefokus verliert.
Layout
tritt auf, wenn sich die Fenstergröße oder die Zahl der Steuerelemente ändert.
Load
tritt auf, nachdem das Fenster (intern) initialisiert wurde (aber noch bevor es sichtbar wird).
Move
tritt auf, wenn sich die Position des Fensters ändert.
Paint
tritt auf, wenn der Fensterinhalt neu gezeichnet werden muss.
Resize
tritt auf, wenn sich die Fenstergröße ändert.
SizeChanged
tritt auf, wenn sich die Fenstergröße ändert (wie Resize).
VisibleChanged
tritt auf, wenn sich die Sichtbarkeit des Fensters ändert.
15.2
Formularinterna
Dieser Abschnitt gibt einige Informationen, die das Verständnis für die interne Funktionsweise von Formularen vergrößern und einen Anhaltspunkt geben sollen, aus welchen Klassen Sie gegebenenfalls fenster-, desktop- oder betriebssystemspezifische Daten ermitteln können.
Was ist ein Formular bzw. Fenster? Eigentlich erscheint diese Frage hier fehl am Platz – immerhin ist das bereits das dritte Kapitel, das eingehend den Umgang mit Formularen bzw. Fenstern beschreibt. Aber vielleicht haben Sie Formulare bisher nur als das wahrgenommen, als was sie nach außen hin erscheinen. Intern handelt es sich bei jedem selbst erstellten Fenster um ein Objekt der Klasse Form1, Form2 etc. (oder wie auch immer Sie Ihre Formularklasse benannt haben). Diese Klasse ist wiederum direkt von System.Windows.Forms.Form abgeleitet. Der Code, in den Sie beispielsweise eigene Ereignisprozeduren oder die Deklaration eigener Klassenvariablen einfügen, ist der Code, der die Klasse Form1, Form2 etc. beschreibt. Mit anderen Worten, jedes Mal, wenn Sie ein Formular entwerfen, entwerfen Sie eine neue Klasse.
Wo beginnt die Codeausführung? Wenn Sie in der Entwicklungsumgebung bei einer Windows-Anwendung DEBUGGEN|STARTEN ausführen bzw. die kompilierte *.exe-Datei starten, beginnt das Programm zu laufen, und das Fenster erscheint. Aber was passiert dabei wirklich?
15.2 Formularinterna
719
Wenn in der Entwicklungsumgebung bei den Projekteigenschaften als Startobjekt ein Formular angegeben ist (bei Windows-Anwendungen ist das per Default der Fall), dann wird für dieses Formular zum Start der folgende Code ausgeführt: Application.Run(New formname())
Durch New wird das Formular mit all seinen Steuerelementen erzeugt. Der dabei ausgeführte Code befindet sich im Codeabschnitt Vom Windows Form Designer generierter Code und wird etwas weiter unten beschrieben. Außerdem treten die Formularereignisse Resize und SizeChanged auf. Durch die Run-Methode der Application-Klasse wird das als Parameter übergebene Fenster angezeigt. Dazu wird im aktuellen Thread eine Nachrichtenschleife (message loop) eingerichtet, die zum Empfang und zur Verarbeitung aller Benutzereingaben dient – z.B. Tastatureingaben, Mausklicks etc. (Anfänglich treten auf jeden Fall die Ereignisse Move, Load, Layout, VisibleChanged und Activated auf.) Diese Nachrichtenschleife ist also dafür verantwortlich, dass nach dem Anklicken eines Buttons dessen Click-Ereignisprozedur ausgeführt wird. Die folgende Aufzählung beschreibt einige weitere Merkmale von Run: •
Run kann im selben Programm normalerweise nur ein einziges Mal aufgerufen werden
(weil es pro Thread nur eine Nachrichtenschleife geben darf). •
Run kann auch ohne Parameter aufgerufen werden. Allerdings kommt es nun zu kei-
nem automatischen Programmende mehr, wenn das bzw. alle Fenster geschlossen werden. Ein Programmende müssen Sie nun durch Application.Exit() erzwingen! •
VERWEIS
Die Methode Run wird erst abgeschlossen, wenn das an Run übergebene Formular geschlossen wird oder wenn in einer Ereignisprozedur Application.Exit() ausgeführt wird. Anstatt sich auf den Application.Run(New formname())-Automatismus zu verlassen, können Sie die Programmausführung auch in einem Modul in der Prozedur Main starten und dort Application.Run selbst ausführen. Das bietet bei Anwendungen mit mehreren Fenstern mehr Flexibilität. Beispielsweise können Sie mehrere Fenster in eigenen Threads öffnen und jeweils mit ihrer eigenen Nachrichtenschleife ausstatten. Details zu diesem Thema finden Sie in Abschnitt 15.3.2.
Wann endet das Programm? Eine Windows-Anwendung endet automatisch, wenn das an Application.Run übergebene Fenster geschlossen wird. (Bei Programmen mit mehreren Fenstern würden Sie vielleicht erwarten, dass das Programm läuft, bis das letzte Fenster geschlossen ist. Das ist nicht der Fall – entscheidend ist nur das Startfenster. Wie Sie dieses Verhalten ändern können, wird abermals in Abschnitt 15.3.2 beschrieben.) Das Schließen des Fensters wird durch ein explizites Schließen des Fensters (X-Button) oder durch die Methode Me.Close ausgelöst. In der Folge wird die Closing-Ereignisprozedur ausgeführt, in der das Programmende durch e.Cancel=True verhindert werden kann. An-
720
15 Gestaltung von Benutzeroberflächen
HINWEIS
dernfalls wird die Closed-Ereignisprozedur und schließlich die Dispose-Prozedur in dem vom Windows Form Designer generierten Code ausgeführt. Dort werden das Fenster und alle seine Steuerelemente durch Dispose aus dem Speicher entfernt. Wenn Application.Run nicht automatisch ausgeführt wurde, sondern durch ihren eigenen Code, dann wird das Programm nach dem Schließen der Fenster in der Zeile nach Application.Run fortgesetzt.
Eine radikalere Form des Programmendes können Sie durch Application.Exit() erreichen: Dadurch werden alle Nachrichtenschleifen für alle Threads des Programms abgearbeitet. Anschließend werden alle Fenster geschlossen, ohne dass es dabei zu Closing- oder ClosedEreignissen kommt.
15.2.1 Nachrichtenschleife (message loop, DoEvents) Die durch Application.Run initialisierte Nachrichtenschleife empfängt alle Eingaben bzw. Ereignisse, die für die vom Programm angezeigten Fenster relevant sind, und trägt sie in einen Zwischenspeicher ein. Die Ereignisse werden der Reihe nach abgearbeitet und führen zum Aufruf der entsprechenden Ereignisprozedur (natürlich nur, wenn es für ein bestimmtes Ereignis auch eine Prozedur zur Reaktion gibt). Der sequentielle Aufruf von Ereignisprozeduren hat eine wichtige Konsequenz: Wenn die Ausführung einer Ereignisprozedur länger dauert, ist das Programm während dieser Zeit blockiert (d.h., es kann nicht auf andere Ereignisse wie z.B. eine versuchte Menüauswahl reagieren). Bei Anwendungen mit mehreren Fenstern wird das gesamte Programm durch die Ereignisprozedur eines Fensters blockiert.
DoEvents-Methode
HINWEIS
Eine einfache Form der Abhilfe besteht darin, während einer länger andauernden Ereignisprozedur – z.B. während einer Berechnung – regelmäßig Application.DoEvents() auszuführen. Dadurch werden alle mittlerweile in die Nachrichtenschleife eingetragenen Ereignisse verarbeitet, bevor der Code fortgesetzt wird. Die Verwendung von DoEvents führt oft zu unübersichtlichem Code. Durch DoEvents kann es auch dazu kommen, dass die gerade laufende Prozedur ein zweites Mal aufgerufen wird. Das sollte im Regelfall vermieden werden (indem z.B. der Button oder das Menükommando, das die Berechnung startet, vorübergehend deaktiviert wird). Falls die Berechnung durch ein anderes Ereignis abgebrochen werden kann, muss nach DoEvents ein Test erfolgen, ob die Abbruchbedingung eingetroffen ist. DoEvents sollte nicht zu oft ausgeführt werden, weil es das Programm verlangsamt. Eine Alternative zur Anwendung von DoEvents ist die Neukonzeption des Programms als Multithreading-Anwendung (siehe z.B. Abschnitt 16.5.6). Allerdings ist die Multithreading-Programmierung relativ kompliziert.
15.2 Formularinterna
721
DoEvents-Beispiel Das folgende Beispielprogramm füllt ein kleines PictureBox-Steuerelement mit weißen und blauen Punkten (die zufällig ausgewählt werden, siehe Abbildung 15.2). Es findet also keine richtige Berechnung statt, es geht nur darum, eine solche zu simulieren. Nach jedem Punkt wird Threading.Thread.Sleep(3) ausgeführt, um zumindest den Effekt einer langsamen Berechnung zu simulieren. Die Besonderheit des Programms besteht darin, dass die Berechnung jederzeit durch den zweiten Button abgebrochen werden kann (bzw. das Programm durch den X-Button beendet werden kann).
Abbildung 15.2: Die Berechnung der Grafik kann dank DoEvents jederzeit abgebrochen werden
Die Berechnung der Grafik erfolgt in Button1_Click. Um einen rekursiven Aufruf der Prozedur zu vermeiden (das wäre wegen DoEvents möglich), wird der Button durch Enabled=False vorübergehend deaktiviert. Die Variable nextdoevent gibt an, zu welchem Zeitpunkt das nächste Mal DoEvents ausgeführt werden soll. Nach DoEvents wird anhand der Klassenvariable cancelCalculation getestet, ob die Prozedur vorzeitig abgebrochen werden soll. Andernfalls wird der Zeitpunkt für das nächste DoEvents bestimmt (Variable nextdoevent) und die Berechnung fortgesetzt. cancelCalculation wird in Button2_Click und in Form1_Closing auf True gesetzt. (Vergessen Sie nicht Form1_Closing – sonst kann es passieren, dass das Fenster geschlossen wird, Button1_Click aber noch fortgesetzt wird. Dann ist aber die Variable gr, die auf das Graphics-Objekt des PictureBox-Steuerelement verweist, nicht mehr gültig, und es kommt zu einem Fehler.) ' Beispiel benutzeroberflaeche\doevents Dim cancelCalculation As Boolean = False Private Sub Button1_Click(...) Handles Button1.Click Dim x, y As Integer Dim br As Brush Dim gr As Graphics = Graphics.FromHwnd(PictureBox1.Handle) Dim nextdoevent As Date = Now.AddMilliseconds(250) Button1.Enabled = False cancelCalculation = False gr.Clear(SystemColors.Control)
722
15 Gestaltung von Benutzeroberflächen
For x = 0 To PictureBox1.ClientSize.Width - 1 For y = 0 To PictureBox1.ClientSize.Height - 1 [ ... einen Punkt zeichnen ...] Threading.Thread.Sleep(3) '3 ms warten 'DoEvents ausführen If Now > nextdoevent Then Application.DoEvents() If cancelCalculation Then gr.Dispose() Button1.Enabled = True Exit Sub End If nextdoevent = Now.AddMilliseconds(250) End If Next Next gr.Dispose() Button1.Enabled = True End Sub Private Sub Button2_Click(...) Handles Button2.Click cancelCalculation = True End Sub Private Sub Form1_Closing(...) Handles MyBase.Closing cancelCalculation = True End Sub
15.2.2 Windows Form Designer Code Üblicherweise entwerfen Sie das Layout von Fenstern im Windows Form Designer. Das ist die Komponente der Entwicklungsumgebung, mit der Sie Steuerelemente von der Toolbox in ein Fenster einfügen und die Eigenschaften von Steuerelementen und Formular einstellen können. Die Entwicklungsumgebung merkt sich diese Operationen, indem sie Programmcode in einen normalerweise ausgeblendeten Codeabschnitt einfügt (Vom Windows Form Designer generierter Code). Darin wird die Liste aller Steuerelemente samt ihrer Eigenschaften gespeichert. Raffiniert ist die Art der Speicherung: Es handelt sich dabei nämlich um den VB.NET-Code, der beim Erzeugen des Formulars ausgeführt wird, um das Formular und seine Steuerelemente zu initialisieren. Die folgenden Zeilen zeigen beispielshaft, wie dieser Code aussieht. Immer gleich sind die beiden Prozeduren New und Dispose, die den Konstruktur bzw. die Destruktormethode der Form-Klasse überschreiben. In New wird die Prozedur InitializeComponent aufgerufen, um die Steuerelemente zu initialisieren. (Details dazu folgen gleich.) Dispose entfernt zuerst alle Steuerelemente und dann das Formular selbst aus dem Speicher.
HINWEIS
15.2 Formularinterna
723
Zur Wiederholung nochmals der Unterschied zwischen Me und MyBase: Me verweist auf die Instanz der aktuellen Klasse, hier Form1. MyBase verweist dagegen auf das Objekt der Basisklasse, hier Form. Während also Me.Dispose() die Dispose-Prozedur von Form1 aufruft, bewirkt MyBase.Dispose den Aufruf der Dispose-Methode der Form-Klasse.
HINWEIS
Nach dem Code der Prozeduren New und Dispose folgen die Deklarationen der Variablen components zur Verwaltung aller unsichtbaren Steuerelemente (Komponenten wie Timer) sowie der Variablen GroupBox1, TextBox1 etc., die auf die Steuerelemente verweisen. Wenn Sie also in einer Ereignisprozedur mit [Me.]TextBox1.Text den Text eines Steuerelements auslesen, dann ist TextBox1 einfach eine Klassenvariable des Formulars, die auf ein Objekt des Typs TextBox verweist. Umgangssprachlich wird das meist – wie auch in diesem Buch – verkürzt: Da heißt es dann, TextBox1 ist ein Steuerelement.
Die InitializeComponent-Prozedur dient schließlich dazu, die Steuerelementobjekte mit New zu erzeugen, ihre Eigenschaften einzustellen und sie schließlich mit AddRange in das Fenster bzw. in andere Container-Steuerelemente einzufügen. Damit der Code möglichst effizient ausgeführt wird, werden durch SuspendLayout vorübergehend Layout-Ereignisse unterdrückt. Im Code werden Eigenschaften nur dann eingestellt, wenn sie nicht den Defaultwert enthalten. Insofern kann man durch das Lesen der InitializeComponent-Prozedur rasch einen Überblick darüber gewinnen, welche Eigenschaften im Eigenschaftsfenster eingestellt wurden.
VERWEIS
Das DebuggerStepThrough-Attribut bewirkt, dass die Prozedur bei einer zeilenweise Ausführung des Programmcodes – also bei der Fehlersuche – als Block ausgeführt wird (wie eine Zeile). Wenn Sie innerhalb der Prozedur einen Fehler vermuten, können Sie das Attribut entfernen oder einen Haltepunkt setzen. Das Attribut hat keine Auswirkung auf den endgültigen Code, sondern ist nur für den Debugger relevant. Nicht alle Eigenschaften von Steuerelementen können ohne weiteres durch einfache Zuweisungen per Code eingestellt werden. Beispielsweise ist es auf diese Weise nicht möglich, die Image-Eigenschaft eines PictureBox-Steuerelements einzustellen, nachdem Sie hierfür im Eigenschaftsfenster eine Bitmap geladen haben. In solchen Fällen werden die Daten automatisch in einer zum Formular gehörenden Ressourcendatei gespeichert. InitializeComponent enthält dann zusätzlichen Code, um diese Ressourcendatei zu öffnen und die darin enthaltenen Daten auszulesen. Ein Beispiel für derartigen Code finden Sie in Abschnitt 15.2.10, in dem es eigentlich darum geht, mehrsprachige Anwendungen zu entwickeln. In diesem Fall werden auch die Sprachinformationen in Ressourcendateien gespeichert.
724
15 Gestaltung von Benutzeroberflächen
' Beispiel benutzeroberflaeche\key-test Public Class Form1 Inherits System.Windows.Forms.Form #Region "Vom Windows Form Designer generierter Code" Public Sub New() MyBase.New() InitializeComponent() ' ... eigener Code End Sub Protected Overloads Overrides Sub Dispose( _ ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub 'Verwaltung der unsichtbaren Steuerelemente (z.B. Timer) Private components As System.ComponentModel.IContainer 'Verweise auf alle Steuerelemente Friend WithEvents GroupBox1 As System.Windows.Forms.GroupBox Friend WithEvents TextBox1 As System.Windows.Forms.TextBox Friend WithEvents TextBox2 As System.Windows.Forms.TextBox Friend WithEvents Timer1 As System.Windows.Forms.Timer ... <System.Diagnostics.DebuggerStepThrough()> _ Private Sub InitializeComponent() ' Steuerelemente/Komponenten erzeugen Me.components = New System.ComponentModel.Container() Me.GroupBox1 = New System.Windows.Forms.GroupBox() Me.TextBox1 = New System.Windows.Forms.TextBox() Me.TextBox2 = New System.Windows.Forms.TextBox() Me.Timer1 = New System.Windows.Forms.Timer(Me.components) ... ' Eigenschaften möglichst effizient einstellen Me.GroupBox1.SuspendLayout() Me.SuspendLayout()
15.2 Formularinterna
725
'GroupBox1-Eigenschaften Me.GroupBox1.Anchor = System.Windows.Forms.AnchorStyles.Top Or _ System.Windows.Forms.AnchorStyles.Bottom Or _ System.Windows.Forms.AnchorStyles.Left 'Steuerelemente in die GroupBox einfügen Me.GroupBox1.Controls.AddRange( _ New System.Windows.Forms.Control() {Me.TextBox1, ...}) Me.GroupBox1.Location = New System.Drawing.Point(8, 144) Me.GroupBox1.Name = "GroupBox1" Me.GroupBox1 ... 'TextBox1-Eigenschaften Me.TextBox1.Anchor = ... Me.TextBox1.Location = New System.Drawing.Point(16, 184) Me.TextBox1 ... 'Form1-Eigenschaften Me.AutoScaleBaseSize = New System.Drawing.Size(6, 15) Me.ClientSize = New System.Drawing.Size(544, 463) 'Steuerelemente in das Formular einfügen Me.Controls.AddRange(New System.Windows.Forms.Control() _ {Me.TextBox2, Me.GroupBox1, ...}) Me.Name = "Form1" Me.Text = "Fenstertitel" Me.GroupBox1.ResumeLayout(False) Me.ResumeLayout(False) End Sub #End Region [... diverse Ereignisprozeduren] End Class
Modifier-Eigenschaft Im Eigenschaftsfenster finden Sie bei jedem Steuerelement die Modifier-Eigenschaft, wobei Sie zwischen den Einstellungen Protected, Private, Friend oder Public wählen können. In Wirklichkeit ist Modifier gar keine richtige Eigenschaft. Vielmehr gibt die Modifier-Eigenschaft an, wie das Steuerelement in dem vom Windows Form Designer generierten Code deklariert werden soll. Im Regelfall spricht nichts gegen die Defaulteinstellung Friend: Sie bewirkt, dass Sie in allen Teilen Ihres Programms auf die Steuerelemente zugreifen können. Durch Private erreichen Sie, dass das Steuerelement nur innerhalb der Formularklasse zugänglich ist. Umgekehrt erreichen Sie durch Public, dass das Steuerelement auch von externen Programmen angesprochen werden kann. Das ist dann sinnvoll, wenn Sie das Formular als Teil einer Bibliothek weitergeben möchten.
726
15 Gestaltung von Benutzeroberflächen
Code ändern Die Kommentare in dem vom Designer generierten Code empfehlen, den Code nicht zu verändern. Generell ist es sicher eine gute Idee, diesen Ratschlag zu befolgen. Wenn Sie dennoch Änderungen durchführen möchten, sollten Sie wissen, was Sie tun! Der beste Ort für eigene Änderungen sind die New- und Dispose-Prozeduren, in denen Sie Initialisierungs- bzw. Aufräumarbeiten durchführen können. (Nach Möglichkeit sollten Sie das in Form_Load bzw. Form_Closed tun, es gibt aber Anwendungen, wo dies nicht möglich ist.) Wesentlich problematischer sind Änderungen in der InitializeComponent-Prozedur. Änderungen, die Sie im Code durchführen, werden beim Anzeigen des Formulars ausgeführt und so sichtbar. Umgekehrt werden Änderungen im Designmodus bei der Rückkehr in die Codeansicht durchgeführt. Soweit ich das abschätzen kann, wird dabei der gesamte InitializeComponent-Code neu erstellt. Das bedeutet, dass im Code durchgeführte Änderungen, die der Formular-Designer nicht richtig interpretieren kann, bei einem Wechsel in den Designmodus und zurück in den Code kommentarlos aus dem Code entfernt werden.
HINWEIS
Wenn Sie in den InitializeComponent-Code einen Fehler einbauen, kann das Formular im Designmodus möglicherweise nicht mehr angezeigt werden. Stattdessen erscheint eine Fehlermeldung. In solchen Fällen sollten Sie versuchen, den Fehler in der Codeansicht zu beheben.
TIPP
Eine mögliche Anwendung von Codeänderungen in der InitializeComponent-Prozedur besteht darin, dass Sie durch Suchen und Ersetzen umfassende Änderungen oft weit effizienter als im Eigenschaftsfenster durchführen können.
In den dotnet-Newsgruppen war gelegentlich davon zu lesen, dass nach bestimmten Änderungen im Designer (nicht im Code!) plötzlich alle Steuerelemente verschwanden. Die Ursache des Problems bestand offensichtlich darin, dass (aus unerklärlichen Gründen, manchmal im Zusammenhang mit selbst programmierten Steuerelementen oder ActiveX-Steuerelementen) die AddRange-Anweisung innerhalb des InitializeComponent-Codes verloren gegangen ist. Die Abhilfe besteht darin, diese Zeile selbst wieder einzufügen. Bei mir ist dieses Problem allerdings nie aufgetreten, so dass ich dazu nicht viel schreiben kann.
15.2.3 Ereignisprozeduren Die Entwicklungsumgebung bietet zwei Wege, um ein Ereignis mit einer Ereignisprozedur zu verbinden: •
Am bequemsten klappt es mit einem Doppelklick auf das Steuerelement bzw. auf den Formularhintergrund im Formular-Designer: Damit wird eine Schablone für die Ereignisprozedur für das Defaultereignis in den Code eingefügt.
15.2 Formularinterna
•
727
Bei allen anderen Ereignissen ist die Vorgehensweise etwas umständlicher: Im Codefenster wählen Sie zuerst im linken Listenfeld das gewünschte Steuerelement bzw. den Listeneintrag BASISKLASSENEREIGNISSE (für die Ereignisse des Formulars) aus; dann wählen Sie im rechten Listeneintrag das gewünschte Ereignis aus. Abermals wird die Schablone für die Ereignisprozedur in den Code eingefügt.
Die Verbindung zwischen Steuerelement und Prozedur erfolgt durch das Schlüsselwort Handles. Der Name der Prozedur spielt dagegen keine Rolle. Private Sub Form1_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles MyBase.Closing [... hier fügen Sie Ihren Code ein ...] End Sub
Eine Ereignisprozedur für mehrere Ereignisse Syntaktisch ist es auch erlaubt, eine Ereignisprozedur für mehrere Ereignisse oder für mehrere Steuerelemente (oder beides) zu verwenden. Dazu fügen Sie einfach die Namen der Ereignisse an das Handles-Schlüsselwort an. Diese Vorgehensweise ist natürlich nur sinnvoll, wenn mehrere Ereignisse auf die gleiche Art und Weise verarbeitet werden sollen. (Das Steuerelement, das das Ereignis ausgelöst hat, kann anhand des sender-Parameters identifiziert werden.) Private Sub ListBoxes_DragEnter(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListBox1.DragEnter, ListBox2.DragEnter
Ereignisprozeduren dynamisch zuweisen Mit AddHandler können Sie ein Ereignis auch dynamisch mit einer Ereignisprozedur verbinden. Die einzige Voraussetzung besteht darin, dass die mit AddressOf angegebene Prozedur die zum Ereignis passende Parameterliste aufweist. Die dynamische Zuweisung von Ereignisprozeduren eignet sich insbesondere für Steuerelemente, die ebenfalls dynamisch in das Formular eingefügt werden. In den Abschnitten 14.11.2 und 14.11.3 finden Sie dafür eine ganze Reihe von Beispielen und Anwendungen. AddHandler Button1.Click, AddressOf ereignisprozedur Private Sub ereignisprozedur(ByVal sender As System.Object, _ ByVal e As System.EventArgs)
15.2.4 Formular dynamisch erzeugen Üblicherweise entwerfen Sie ein Formular bzw. Fenster in der Entwicklungsumgebung und verwenden die so erzeugte Klasse, um das Fenster dann zu erzeugen (frm = New FormName()) und anzuzeigen (frm.Show()). Das ist zweifellos der einfachste Weg, grundsätzlich ist es aber auch möglich, ein Formular vollständig per Code zu erzeugen. Der dafür
728
15 Gestaltung von Benutzeroberflächen
erforderliche Code sieht so ähnlich aus wie die vom Designer erzeugte InitializeComponentProzedur. Die einzige Besonderheit besteht darin, dass das Formular bzw. dessen Steuerelemente mit AddHandler mit Ereignisprozeduren verbunden werden müssen. Das folgende Programm gibt dafür ein denkbar einfaches Beispiel. Ausgangspunkt ist ein gewöhnliches Fenster (Form1). Wenn Sie dessen einzigen Button anklicken, wird ein neues Fenster mit zufälliger Hintergrundfarbe erzeugt. Das neue Fenster ist ebenfalls mit einem Button ausgestattet, mit dem das Fenster geschlossen werden kann.
Abbildung 15.3: Die vier Fenster rechts wurden dynamisch erzeugt
Button1_Click erzeugt ein neues Fenster (auf der Basis der Form-Klasse) und fügt dort einen
Button ein. Dabei werden die elementarsten Eigenschaften des Fensters eingestellt. Mit AddHandler wird das Click-Ereignis des neuen Buttons mit der Prozedur mybtn_Click verbunden. Diese Prozedur ermittelt über die Parent-Eigenschaft von sender das Formularobjekt und schließt es mit Close. ' Beispiel benutzeroberflaeche\dynamic-window Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New Form() Dim btn As New Button() Dim rand As New Random() frm.Text = "Dynamisch erzeugtes Fenster" frm.Size = New Size(300, 100) frm.BackColor = Color.FromArgb(rand.Next(255), _ rand.Next(255), rand.Next(255)) btn.Text = "Fenster schließen" btn.Bounds = New Rectangle(10, 10, 200, 30) btn.BackColor = SystemColors.Control AddHandler btn.Click, AddressOf mybtn_click frm.Controls.AddRange(New System.Windows.Forms.Control() {btn}) frm.Show() End Sub Private Sub mybtn_click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim btn As Button = CType(sender, Button) CType(btn.Parent, Form).Close() End Sub
15.2 Formularinterna
729
15.2.5 Vererbung bei Formularen Normalerweise sind eigene Formulare von der Basisklasse Form abgeleitet. Sie können aber auch eigene Formulare entwerfen, die von einem anderen Formular abgeleitet sind. Wenn Sie also mehrere Formulare mit teilweise gleichartigen Bedienungselementen, Eigenschaften oder Code erstellen möchten, können Sie zuerst ein Master-Formular entwerfen und dieses dann als Grundlage für den Entwurf der weiteren Formulare verwenden. Wie so oft ist das am einfachsten anhand eines Beispiels zu verstehen.
Beispiel Das folgende Beispielprogramm besteht aus drei Formularen: Form1: Form2: Form3:
das Master-Formular Fenster 1, vom Master-Formular abgeleitet Fenster 2, ebenfalls vom Master-Formular abgeleitet
Form1 wird nur als Master-Formular verwendet und nicht direkt angezeigt. Es besteht aus einem LinkLabel (Dock=Top), der auf meine Website verweist, sowie zwei Buttons, die sich in einem Panel mit Dock=Bottom befinden (siehe Abbildung 15.4). Form2 gilt als Startobjekt des Projekts. In Form2_Load wird auch Form3 angezeigt, so dass
beim Programmstart Fenster 1 und 2 erscheinen (siehe Abbildung 15.5).
Abbildung 15.4: Das Master-Formular Form1 in der Entwicklungsumgebung
Abbildung 15.5: Die beiden Fenster sind von Form1 abgeleitet
730
15 Gestaltung von Benutzeroberflächen
Um Form2 und Form3 zu erzeugen, gibt es zwei Möglichkeiten. Die eine besteht darin, PROJEKT|GEERBTES FORMULAR HINZUFÜGEN auszuführen. Dabei können Sie das Master-Formular auswählen. Die andere Variante besteht darin, das Formular als gewöhnliches Formular zu beginnen und dann in der Codeansicht die Inherits-Zeile (die zweite Zeile der Klasse) zu verändern. ' Beispiel benutzeroberflaeche\form-inheritance Public Class Form3 Inherits Form1 'statt Inherits System.Windows.Forms.Form ...
Einschränkungen Der Umgang mit vererbten Formularen wirkt noch nicht ganz ausgereift: •
Die Eigenschaft Dock bereitet Probleme: Beispielsweise habe ich im Master-Formular das LinkLabel-Steuerelement mit Dock=Top an den oberen Rand angedockt und wollte dann im vererbten Formular für ein TextBox-Steuerelement mit Dock=Fill erreichen, dass es den Rest des Formulars ausfüllt. Das unerfreuliche Ergebnis: die TextBox füllt das gesamte Fenster aus und wird teilweise von den angedockten Steuerelementen des Master-Formulars verdeckt. (In diesem Fall war eine Abhilfe relativ einfach zu finden: Im vererbten Formular wurde Anchor statt Dock verwendet, um die Größe von Steuerelementen einzustellen.)
•
Steuerelemente des Masterformulars können im vererbten Formular normalerweise weder verändert noch mit eigenen Ereignisprozeduren ausgestattet werden. Wenn Sie das möchten, müssen Sie beim Steuerelement im Master-Formular Modifiers=Public einstellen. Tests mit dieser scheinbar einfachen Lösung haben allerdings zu neuen Problemen bei der korrekten Platzierung der Steuerelemente geführt. (Diesmal war es Anchor, das die Probleme verursacht hat.) Außerdem können derartige Steuerelemente im Designer nicht mit der Maus verschoben werden, obwohl die Location-Eigenschaft dies zulassen müsste.
15.2.6 Windows-XP-Optik Per Default haben VB.NET-Programme das Aussehen von Windows-95/98/ME/2000-Programmen, auch dann, wenn sie unter Windows XP ausgeführt werden (siehe Abbildung 15.6 Mitte). Mit Windows XP hat Microsoft die Optik von Fenstern, Buttons etc. aber modernisiert und veränderbar gemacht. Dieses Merkmal von Windows XP wird als themes bezeichnet. Ob am aktuellen Rechner themes zur Verfügung stehen, können Sie per Code feststellen: If OSFeature.Feature.IsPresent(OSFeature.Themes) Then ...
Man kann zwar darüber streiten, ob die neue Optik wirklich einen Fortschritt darstellt oder eher als Modetrend zu bezeichnen ist, aber auf jeden Fall besteht häufig der Wunsch,
15.2 Formularinterna
731
auch VB.NET-Programme mit Windows-XP-Look-and-Feel zu gestalten. Grundsätzlich ist das nur möglich, wenn das Programm unter Windows XP ausgeführt wird. Selbst wenn diese Voraussetzung erfüllt ist, ist die Umstellung erschreckend kompliziert. Von der Einfachheit bei der Programmierung, die .NET angeblich bringen soll, ist da nichts mehr zu spüren. Im Programm müssen zwei Voraussetzungen erfüllt sein: •
Bei allen Steuerelementen, die die Eigenschaft FlatStyle kennen, muss diese auf System gestellt werden. Das gilt insbesondere für die Steuerelemente Button, RadioButton, CheckBox und GroupBox. (Per Default lautet die Einstellung Standard.)
•
Das Programm (die *.exe-Datei) muss mit Version 6 der Bibliothek comctl32.dll verbunden werden. Diese Version steht zurzeit nur unter Windows XP zur Verfügung.
FlatStyle einstellen FlatStyle können Sie für jedes einzelne Steuerelement im Eigenschaftsfenster einstellen. Komfortabler ist es, diese Aktion nur bei Bedarf per Code durchzuführen. ' Beispiel benutzeroberflaeche\xp-look-and-feel Private Sub Form1_Load(...) Handles MyBase.Load If OSFeature.Feature.IsPresent(OSFeature.Themes) Then SetFlatStyle(Me.Controls) End If End Sub
HINWEIS
Private Sub SetFlatStyle(ByVal ctrls As Control.ControlCollection) Dim c As Control For Each c In ctrls If TypeOf c Is ButtonBase Then CType(c, ButtonBase).FlatStyle = FlatStyle.System ElseIf TypeOf c Is GroupBox Then CType(c, GroupBox).FlatStyle = FlatStyle.System End If SetFlatStyle(c.Controls) Next End Sub
Wenn Sie FlatStyle=System verwenden, verlieren Sie einige Einstellmöglichkeiten: Sie können jetzt beispielsweise weder eine eigene Hintergrundfarbe noch eine Hintergrundbitmap angeben. Diese Merkmale werden nun durch die themes vorgegeben. Das ist wohl auch der Grund, warum FlatStyle=Standard als Defaulteinstellung gilt.
Programm mit Version 6 der comctl32.dll-Bibliothek verbinden Damit das Programm die Version 6 der comctl32.dll-Bibliothek verwendet (sofern sie am Rechner verfügbar ist), liefern Sie zusammen mit der *.exe-Datei eine so genannte Mani-
732
15 Gestaltung von Benutzeroberflächen
fest-Datei aus. (Eine Manifest-Datei ist eine XML-Datei, die Abhängigkeiten des Programms von einzelnen Bibliotheken beschreibt.) Diese Datei muss den Namen <programmname>.manifest haben. (Wenn Ihr Programm also hellow.exe heißt, muss die Manifest-Datei hellow.exe.manifest heißen.) Die Manifest-Datei muss den folgenden Inhalt haben. (Sie müssen diese Zeilen nicht abschreiben, sondern finden die Datei auf der beiliegenden CD im Verzeichnis benutzeroberflaeche\xp-look-and-feel\bin. Die Datei stammt aus den unten genannten Quellen. Die große Frage ist natürlich, ob die hier präsentierte Vorgehensweise auch mit künftigen WindowsVersionen kompatibel ist.) <dependency> <dependentAssembly>
Abbildung 15.6: Links das Programm unter Windows 2000, in der Mitte unter Windows XP ohne Manifest-Datei, rechts ebenfalls unter Windows XP, aber mit Manifest-Datei
15.2 Formularinterna
733
VERWEIS
Weitere Informationen zur Umstellung von Windows.Forms-Anwendungen auf die XP-Optik finden Sie hier: http://www.gotdotnet.com/team/windowsforms/Themes.aspx http://msdn.microsoft.com/library/en-us/dv_vstechart/html/ vbtchUsingWindowsXPVisualStylesWithControlsOnWindowsForms.asp http://msdn.microsoft.com/library/en-us/dnwxp/html/xptheming.asp (zum Teil veraltet) ms-help://MS.VSCC/MS.MSDNVS.1031/dnwxp/html/xptheming.htm (zum Teil veraltet) ms-help://MS.VSCC/MS.MSDNVS.1031/sbscs/sidebysideref_03ol.htm (Grundlagen) ms-help://MS.VSCC/MS.MSDNVS.1031/vcresed/html/vcconManifestResources.htm (Grundlagen)
15.2.7 Multithreading Dieser Abschnitt setzt voraus, dass Sie bereits ein grundlegendes Wissen zum Thema Multithreading haben (siehe Abschnitt 12.6). An dieser Stelle geht es nur um die Besonderheiten, die berücksichtigt werden müssen, wenn Sie eine Multithreading-Anwendung in Kombination mit einer Windows.Forms-Benutzeroberfläche realisieren möchten. Das Grundproblem besteht darin, dass ein Form-Objekt – also das Fenster – mit allen darin enthaltenen Steuerelementen in einem STA-Thread (singlethread apartment) läuft. Um mögliche Zugriffskonflikte zu vermeiden, dürfen die Methoden und Eigenschaften dieser Objekte ausschließlich von dem Thread verwendet werden, in dem das Fenster erzeugt wurde. Das bedeutet, dass Sie aus Ihren eigenen Threads keine Eigenschaften oder Methoden von Formularen oder Steuerelementen direkt nutzen dürfen. Der einzig zulässige Weg besteht darin, über Invoke (synchron) bzw. BeginInvoke (asynchron) Prozeduren zu starten, die dann innerhalb des Windows.Forms-Threads ausgeführt werden. (Soweit der Thread Ereignisse ausgelöst, in deren Ereignisprozeduren auf das Fenster oder dessen Steuerelemente zugegriffen werden soll, müssen auch diese Ereignisse mit [Begin]Invoke ausgelöst werden!) Intern kommt bei der Ausführung von [Begin]Invoke der so genannte marshalling-Mechanismus zur Anwendung, der es ermöglicht, Methoden anderer Prozesse oder Threads aufzurufen und dabei alle erforderlichen Daten zwischen den Prozessen oder Threads zu übertragen. Die Verwendung von [Begin]Invoke ist umständlich, langsam (vor allem, wenn sie wiederholt ausgeführt wird) und kann in ungünstigen Fällen zu so genannten deadlocks führen. Das bedeutet, dass sich mehrere Threads gegenseitig blockieren, so dass keiner mehr fortgesetzt werden kann.
734
15 Gestaltung von Benutzeroberflächen
Aus diesem Grund eignet sich Multithreading vor allem für solche Szenarien, in denen ein eigener Thread eine relativ zeitaufwendige Aufgabe vollkommen losgelöst von der Benutzeroberfläche durchführen kann; erst wenn die Aufgabe fertig ist, wird das Ergebnis im Fenster bzw. in einem seiner Steuerelemente angezeigt. Schlecht geeignet sind dagegen Aufgaben, bei denen der Thread ununterbrochen (z.B. in einer Schleife) auf die Eigenschaften oder Methoden von Steuerelementen zugreifen muss. In solchen Fällen ist es meist zielführender, die Aufgabe in herkömmlicher Weise im aktuellen Thread auszuführen und durch regelmäßige DoEvents eine Unterbrechung zuzulassen. Manchmal besteht auch die Möglichkeit, den Code für die skizzierten Anforderungen zu optimieren: Beispielsweise ist es besser, zuerst eine Sammlung neuer Listeneinträge zu bilden und diese dann alle gemeinsam mit AddRange in ein Listenfeld einzufügen als jeden Eintrag extra mit Add einzufügen (was nur durch zahllose Invoke-Aufrufe möglich ist).
VERWEIS
ACHTUNG
ACHTUNG
Einige Steuerelemente – z.B. TreeView – testen, ob ihre Methoden oder Eigenschaften innerhalb des aktuellen Threads aufgerufen werden und lösen einen Fehler auf, wenn das nicht der Fall ist. Diese Steuerelemente zwingen Sie also dazu, [Begin]Invoke zu verwenden. Bei vielen anderen Steuerelementen können Sie scheinbar auf [Begin]Invoke verzichten, was die Multithreading-Programmierung natürlich sehr erleichtern würde. Dieser Versuchung sollten Sie aber widerstehen! Auch wenn nicht sofort ein Fehler auftritt, bedeutet das noch lange nicht, dass Sie so auf Dauer ein stabiles Programm erhalten. Viele Zugriffskonflikte treten nur recht selten auf, was die Sache aber nur noch unangenehmer macht: Sie erhalten ein Programm, das bei Ihren eigenen Tests vielleicht gut funktioniert, das beim Anwender aber hin und wieder mit merkwürdigen Fehlermeldungen abstürzt. Fazit: Verwenden Sie konsequent für jeden Zugriff [Begin]Invoke oder verzichten Sie lieber ganz auf Multithreading in WindowsAnwendungen! SyncLock bietet einen wirksamen Mechanismus, um mehrere Threads zu synchroni-
sieren bzw. um den gleichzeitigen Zugriff von mehreren Threads auf ein Objekt zu vermeiden. Die Online-Hilfe empfiehlt aber explizit, SyncLock nicht auf Steuerelemente oder Formulare anzuwenden, weil daraus relativ leicht deadlocks resultieren können. Auch die Hilfe beschreibt den Zugriff auf Steuerelemente aus eigenen Threads. Suchen Sie nach Bearbeiten von Steuerelementen aus Threads bzw. nach Multithreading bei Formularen und Steuerelementen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbtskManipulatingControlsFromThreads.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconfreethreadingwithformscontrols.htm
Beide Hilfetexte sprechen übrigens immer nur von Methoden, die per Invoke aufgerufen werden müssen. Diese Regel gilt aber auch für Eigenschaften!
15.2 Formularinterna
735
Invoke
HINWEIS
Mit der für das Formular-Objekt sowie für alle Steuerelemente zur Verfügung stehenden Methode Invoke können Sie eine beliebige Prozedur oder Methode aufrufen. Der Code wird innerhalb des Threads des Formulars ausgeführt, d.h., zur Ausführung muss ein Thread-Wechsel durchgeführt werden. Beachten Sie, dass die Ausführung der Methode im Windows-Thread erst dann erfolgt, wenn dieser dazu Zeit hat, also nicht mit der Abarbeitung bereits laufender (Ereignis-)Prozeduren beschäftigt ist! Achtung: Diese durch den Code des Hauptprogramms verursachten Verzögerungen können den MultithreadingCode sehr ineffizient machen und in ungünstigen Fällen zu Deadlocks führen (siehe das entsprechende Beispiel etwas weiter unten). Um es nochmals klarzustellen: Die mit Invoke ausgeführte Prozedur wird nicht im neuen Thread, sondern im Thread des Hauptprogramms (also im Windows.FormsThread ausgeführt). Da der Sinn von Multithreading im Regelfall darin besteht, Code möglichst unabhängig vom Haupt-Thread auszuführen, sollte die durch Invoke ausgeführte Prozedur möglichst wenig Code enthalten und möglichst rasch beendet werden!
An Invoke muss ein Delegate-Objekt übergeben werden. (Eine direkte Adressangabe der Prozedur ist also nicht möglich. Was Delegates sind, wurde in Abschnitt 7.6.2 beschrieben.) Am einfachsten ist der Aufruf, wenn keine Parameter übergeben werden sollen: •
Dazu müssen Sie mit Delegate eine neue Delegate-Klasse für Methoden ohne Parameter definieren. (Diese Definition muss auf Datei-, Modul- oder Klassenebene erfolgen. Delegate kann nicht innerhalb einer Prozedur verwendet werden.)
•
Als Nächstes müssen Sie ein Objekt der Delegate-Klasse mit der Adresse der aufzurufenden Methode bzw. Prozedur initialisieren (mymethod0 im folgenden Codegerüst).
•
Dieses Objekt können Sie nun an Invoke übergeben. Invoke kann auf ein beliebiges Steuerelement oder Formular angewendet werden. Fall sich Invoke im Code der Formularklasse befindet, muss gar kein Objekt angegeben zu werden. (Invoke bezieht sich dann automatisch auf Me.)
•
Die auszuführende Prozedur (do_it0 im Codegerüst) kann beliebigen Code zur Bearbeitung aller Steuerelemente enthalten, die sich im selben Thread befinden wie das Objekt, auf das Invoke angewendet wurde. Wenn Invoke also auf ein Steuerelement oder direkt auf ein Formular angewendet wird, können in der Prozedur alle Steuerelemente des Fensters angesprochen werden – und in der Regel auch alle Steuerelemente aller anderen Fenster des Programms (es sei denn, die Fenster wurden in unterschiedlichen Threads geöffnet).
' Deklaration einer Delegate-Klasse für Methoden ohne Parameter Delegate Sub mydelegate0()
736
15 Gestaltung von Benutzeroberflächen
' Aufruf einer Methode ohne Parameter Dim mymethod0 As New mydelegate0(AddressOf do_it0) mycontrol.Invoke(mymethod0) oder myform.Invoke(mymethod0) ' die aufzurufende Prozedur oder Methode Sub do_it0() ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
Wenn Sie eine Methode mit Rückgabewert aufrufen möchten, müssen Sie die DelegateKlasse als Function deklarieren. Invoke liefert den Rückgabewert der aufgerufenen Funktion als Object zurück, d.h., Sie müssen anschließend in der Regel noch eine Typumwandlung durchführen (CStr, CType etc.). Die folgenden Zeilen geben ein Muster: ' Deklaration einer Delegate-Klasse für Funktionen ohne Parameter Delegate Function mydelegate1() As String ' Aufruf einer Funktion mit String-Rückgabe Dim s As String Dim mymethod1 As New mydelegate0(AddressOf do_it1) s = CStr(myform.Invoke(mymethod1)) ' die aufzurufende Prozedur oder Methode Function do_it1() As String ... Code zur Bearbeitung aller Steuerelemente des Formulars Return "ein Ergebnis" End Sub
Ein bisschen komplizierter wird es, wenn Sie an die Prozedur Parameter übergeben möchten. Die erste Voraussetzung hierfür besteht darin, dass Sie in der Delegate-Klasse die erforderlichen Parameter angeben. An die Invoke-Methode müssen Sie alle Parameter in Form eines Object-Felds übergeben. Invoke übergibt das erste Feldelement an den ersten Parameter der Prozedur, das zweite an den zweiten Parameter etc. Dabei erfolgt eine Typkontrolle, d.h., wenn die Daten im Feld nicht mit den Parametertypen laut Delegate übereinstimmen, kommt es zu einer Fehlermeldung. Außerdem muss die Größe des Felds mit der Anzahl der Parameter übereinstimmen. Im folgenden Codegerüst wird ein Delegate für eine Prozedur definiert, die einen Integerund einen String-Parameter erwartet. Die Übergabe der Parameter erfolgt durch das Feld obj, dessen beide Elemente vor dem Aufruf initialisiert werden. Dieses Feld wird dann als zweiter Parameter an die Invoke-Methode übergeben, die sich darum kümmert, den Inhalt an die aufzurufende Prozedur do_it2 weiterzugeben. ' Deklaration einer Delegate-Klasse für Methoden mit einem ' Integer- und einem String-Parameter Delegate Sub mydelegate2(ByVal n As Integer, ByVal s As String) ' Aufruf einer Prozedur mit zwei Parametern Dim mymethod2 As New mydelegate2(AddressOf do_it2) Dim obj(1) As Object 'Übergabe der Parameter
15.2 Formularinterna
737
obj(0) = 123 obj(1) = "abc" Invoke(mymethod2, obj) ' die aufzurufende Prozedur oder Methode Sub do_it2(ByVal n As Integer, ByVal s As String) ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
BeginInvoke und EndInvoke Invoke wird (wie alle gewöhnlichen Methoden oder Eigenschaften) synchron ausgeführt. Das bedeutet, dass die nächste Anweisung nach Invoke erst nach dem Abschluss der durch Invoke aufgerufenen Prozedur ausgeführt wird. Je nachdem, ob der Haupt-Thread des
Windows-Programms gerade beschäftigt ist, kann es daher eine Weile dauern, bis Ihr eigener Thread fortgesetzt werden kann. Um diese Wartezeit zu vermeiden und eine möglichst hohe Parallelität bei der Ausführung des Codes zu erzielen (was ja oft die Motivation einer Multithreading-Anwendung ist), können Sie die Prozedur mit BeginInvoke auch asynchron starten. BeginInvoke liefert sofort ein Objekt der Schnittstelle IAsyncResult zurück. Über dieses Objekt können Sie feststellen (Eigenschaft IsCompleted), ob die aufgerufene Prozedur schon abgeschlossen ist. Um den Programmfluss wieder zu synchronisieren, führen Sie EndInvoke aus, wobei Sie das IAsyncResult-Objekt übergeben. EndInvoke wartet, bis die Prozedur tatsächlich beendet ist und liefert bei Funktionen den Rückgabewert. Formal erfolgt der Aufruf von BeginInvoke genau gleich wie bei Invoke, d.h., Sie müssen zuerst eine Delegate-Klasse deklarieren und dann ein Objekt dieser Klasse mit der aufzurufenden Prozedur initialisieren. Dieses Objekt (eventuell samt einem Object-Feld für die Parameter) übergeben Sie an BeginInvoke. Die folgenden Zeilen zeigen die Anwendung von Begin- und EndInvoke für eine Prozedur ohne Parameter und ohne Rückgabewert. ' Deklaration einer Delegate-Klasse für Methoden ohne Parameter Delegate Sub mydelegate3() ' Aufruf einer Methode ohne Parameter Dim result As IAsyncResult Dim mymethod3 As New mydelegate0(AddressOf do_it3) result = mycontrol.BeginInvoke(mymethod3) ... 'beliebiger Code, der parallel zu mymethod3 ausgeführt werden kann mycontrol.EndInvoke(result) ' die aufzurufende Prozedur oder Methode Sub do_it3() ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
TIPP
738
15 Gestaltung von Benutzeroberflächen
Ein Beispielprogramm zur Demonstration der unterschiedlichen [Begin]InvokeVarianten finden Sie auf der beiliegenden CD im Verzeichnis benutzeroberflaeche\multithread-invoke. Auf den Abdruck wurde hier aus Platzgründen verzichtet. Das Programm geht im Vergleich zu den vorgestellten Codegerüsten auf keine nennenswerten neuen Aspekte ein.
Programmende Ein Windows-Programm endet automatisch, wenn das Startfenster geschlossen wird. Das bedeutet aber nicht, dass auch ein von diesem Fenster aus gestarteter Thread endet! Dieser läuft im Gegenteil weiter, auch wenn es die Benutzeroberfläche gar nicht mehr gibt. Nur wenn der Thread auf ein Element des Fensters zuzugreifen versucht (über Invoke), tritt ein Fehler auf. Um zu vermeiden, dass einzelne Threads länger laufen als das Hauptprogramm, sollten Sie diese Threads unbedingt explizit in der Closing-Ereignisprozedur des Fensters durch Abort und Join beenden. Private Sub Form1_Closing(...) Handles MyBase.Closing If mythread.IsAlive Then mythread.Abort() 'Thread zum Beenden auffordern mythread.Join() 'warten, bis der Thread wirklich beendet wurde End If End Sub
Die andere Variante besteht darin, den Thread beim Erzeugen als Hintergrund-Thread zu kennzeichnen (mythread.IsBackground = True). Damit wird der Thread beim Programmende automatisch beendet. (Beachten Sie aber, dass es dennoch zu einer Fehlermeldung kommen kann, wenn der Thread gerade in der Phase, in der das Formular aus dem Speicher entfernt wird, der Fenster-Thread aber noch läuft, Invoke ausführt.)
Invoke-Deadlock-Beispiel Normalerweise bezeichnet ein deadlock die Situation, bei der zwei Threads gegenseitig darauf warten, dass der jeweils andere Thread ein (in der Regel durch SyncLock gesperrtes) Objekt wieder freigibt. Die hier beschriebene Situation ist ähnlich, auch wenn es sich genau genommen nicht um einen richtigen deadlock handelt: In Button1_Click wird in einem Thread die Prozedur add_1000_random_nodes aufgerufen. Diese Prozedur ruft per Invoke 1000 Mal add_a_random_node auf, um eine zufällige Zeichenkette in ein TreeView-Steuerelement einzufügen. Während add_1000_random_nodes in einem eigenen Thread läuft, wird im Hauptthread regelmäßig TreeView1.Nodes.Count ausgelesen und in einem Label angezeigt. Die Schleife endet, sobald das TreeView-Steuerelement 1000 Einträge enthält.
15.2 Formularinterna
739
' Beispiel benutzeroberfläche\multithread-deadlock ' Delegate um eine Prozedur/Methode ohne Parameter aufzurufen Delegate Sub mydelegate() ' ruft changelabel Private Sub Button1_Click(...) Handles Button1.Click Dim n As Integer Dim mythread As Threading.Thread mythread = New Threading.Thread(AddressOf add_1000_random_nodes) mythread.Start() Do n = TreeView1.Nodes.Count Threading.Thread.Sleep(1) Label1.Text = n.ToString Loop Until n >= 1000 End Sub ' ruft 1000 Mal per Invoke die Methode add_a_random_node auf Sub add_1000_random_nodes() Dim i As Integer Dim mymethod As New mydelegate(AddressOf add_a_random_node) For i = 1 To 1000 TreeView1.Invoke(mymethod) Next End Sub Sub add_a_random_node() TreeView1.Nodes.Add(Rnd.ToString) End Sub
Leider funktioniert das Programm nicht wie beabsichtigt. Die Do-Loop-Schleife stellt sich als Endlosschleife heraus, die das gesamte Programm blockiert. Warum? Das Problem liegt bei Invoke: die via mymethod aufgerufene Methode add_a_random_node wird erst dann ausgeführt, wenn der Haupt-Thread dazu Zeit hat, also gerade keine anderen Prozeduren ausführt. Button1_Click wartet aber darauf, dass das TreeView-Steuerelement 1000 Elemente enthält ... Das Programm ist also unglücklich formuliert. Abhilfe würde eine Application.DoEvents-Anweisung innerhalb der Do-Loop-Schleife schaffen. Noch besser ist es aber, generell Warteschleifen zu vermeiden, deren Ergebnis von irgendwelchen Threads abhängt! (Natürlich ist es auch unklug, 1000 Mal Invoke aufzurufen. Besser wäre es, zuerst ein TreeNode-Feld zu initialisieren und dieses dann mit AddRange in nur einem einzigen Invoke-Aufruf in das TreeView-Steuerelement einzufügen. Im konkreten Fall hätte das aber auch nichts geholfen, weil der Einfüge-Thread schon beim ersten Invoke-Aufruf hängen bleibt.)
740
15 Gestaltung von Benutzeroberflächen
Beispielanwendung Bei dem in Abbildung 15.7 dargestellten Beispielprogramm können Sie im hierarchischen Listenfeld ein Verzeichnis auswählen. Wenn Sie anschließend den Button EIGENSCHAFTEN anklicken, wird in einem neuen Thread die Anzahl aller Dateien und Unterverzeichnisse sowie der gesamte Platzbedarf ermittelt. Während diese Daten ermittelt werden, wird der Button grau angezeigt (Enabled=False). Sobald die Endergebnisse vorliegen, verschwindet der Button ganz. Das Programm merkt sich die Ergebnisse für das betroffene Verzeichnis, so dass sie bei einem nochmaligen Auswählen des Verzeichnisses sofort zur Verfügung stehen. Da die Verzeichniseigenschaften in einem eigenen Thread ermittelt werden, kann das Programm uneingeschränkt weiterverwendet werden, etwa um ein anderes Verzeichnis im Verzeichnisbaum auszuwählen. Wenn die Eigenschaften eines neuen Verzeichnisses ermittelt werden sollen, bevor die Berechnung der Eigenschaften des vorherigen Verzeichnisses abgeschlossen ist, wird der noch laufende Thread abgebrochen.
Abbildung 15.7: Die Verzeichniseigenschaften werden in einem eigenen Thread ermittelt
Programmcode des Hauptfensters Die Verwaltung des TreeView-Steuerelements erfolgt wie in dem in Abschnitt 14.6.7 bereits vorgestellten Beispielprogramm. (Diese Teile des Codes werden hier nicht mehr abgedruckt.) Zur Ermittlung der Eigenschaften eines Verzeichnisses wird die Klasse dirinfo eingesetzt. An dessen Konstruktor müssen zwei Parameter übergeben werden: ein Verweis auf das Formular sowie das TreeNode-Objekt des Verzeichnisses. Anschließend kann die Ermittlung der Verzeichniseigenschaften mit der Methode GetProperties gestartet werden. Die Klasse kennt zwei Ereignisse: update wird regelmäßig aufgerufen und ermöglicht eine Aktualisierung des Fensters, während das Verzeichnis durchlaufen wird. done gibt an, dass die endgültigen Daten vorliegen.
15.2 Formularinterna
741
Auf Formularebene sind zwei Klassenvariablen definiert: details_tn zeigt auf das TreeNodeObjekt, dessen Eigenschaften momentan im rechten Teil des Fensters angezeigt werden. dinfothread verweist auf den Thread zur Ausführung der dirinfo.GetProperties-Methode. ' Beispiel benutzeroberflaeche\multithread-treeview Dim details_tn As TreeNode Dim dinfothread As Threading.Thread
Die Ereignisprozedur TreeView1_AfterSelect wird immer dann aufgerufen, wenn im Listenfeld durch einen einfachen Mausklick ein Verzeichnis ausgewählt wurde. Die Prozedur speichert dann das ausgewählte TreeNode-Element in details_tn und zeigt die verfügbaren Eigenschaften an. (Bereits zu einem früheren Zeitpunkt ermittelte Eigenschaften können mit tn.Tag aus einem dirinfo-Objekt gelesen werden.) Private Sub TreeView1_AfterSelect(...) Handles TreeView1.AfterSelect Dim tn As TreeNode = TreeView1.SelectedNode ' falls gültige Auswahl If Not IsNothing(tn) Then details_tn = tn lblDirName.Text = "Verzeichnis: " + _ Replace(tn.FullPath, "\\", "\") If IsNothing(tn.Tag) Then lblFiles.Text = "Dateien: ???" lblDirectories.Text = "Verzeichnisse: ???" lblSize.Text = "Platzbedarf: ???" btnProperties.Visible = True btnProperties.Enabled = True Else ShowTreenodeDetails(tn) End If End If End Sub Private Sub ShowTreenodeDetails(ByVal tn As TreeNode) Dim di As dirinfo = CType(tn.Tag, dirinfo) lblFiles.Text = "Dateien: " + di.files.ToString lblDirectories.Text = "Verzeichnisse: " + di.directories.ToString lblSize.Text = "Platzbedarf: " + _ (di.size \ 1024 \ 1024).ToString + " MBytes" btnProperties.Visible = False End Sub
Die Ermittlung der Verzeichniseigenschaften beginnt durch einen Klick auf den EIGENSCHAFTEN-Button. In btnProperties_Click wird zuerst ein eventuell noch laufender Thread beendet. Anschließend wird ein neues dirinfo-Objekt erzeugt. Für die Ereignisse Done und Update werden Ereignisprozeduren eingerichtet. Anschließend wird di.GetProperties in einem neuen Thread gestartet.
742
15 Gestaltung von Benutzeroberflächen
In den update- und done-Ereignisprozeduren wird der rechte Fensterbereich aktualisiert, falls details_tn immer noch auf dasselbe TreeNode-Objekt zeigt, dessen Eigenschaften ermittelt werden. (Vielleicht hat der Anwender in der Zwischenzeit bereits ein anderes Verzeichnis ausgewählt. In diesem Fall läuft der Thread weiter, die Anzeige wird aber nicht aktualisiert.) Sobald das Endergebnis vorliegt (done-Ereignis), wird das dirinfo-Objekt in der Tag-Eigenschaft des TreeNode-Elements gespeichert. ' Eigenschaften zum Verzeichnis ermitteln Private Sub btnProperties_Click(...) Handles btnProperties.Click Dim di As dirinfo If IsNothing(details_tn) Then Exit Sub stop_dirinfo_thread() 'eventuell laufenden Thread abbrechen ' neues dirinfo-Objekt erzeugen und dessen Methode GetProperties ' in neuem Thread starten btnProperties.Enabled = False di = New dirinfo(Me, details_tn) AddHandler di.Done, AddressOf dirinfo_done AddHandler di.Update, AddressOf dirinfo_update dinfothread = New Threading.Thread(AddressOf di.GetProperties) dinfothread.Name = "dinfothread" dinfothread.Start() End Sub ' Ereignisprozedur, wird aufgerufen, ' um die Verzeichnisinfos zu aktualsieren Private Sub dirinfo_update(ByVal di As dirinfo) If di.tn Is details_tn Then lblFiles.Text = "Dateien: " + di.files.ToString lblDirectories.Text = "Verzeichnisse: " + _ di.directories.ToString lblSize.Text = "Platzbedarf: " + _ (di.size \ 1024 \ 1024).ToString + " MBytes" btnProperties.Enabled = False End If End Sub ' Ereignisprozedur, wird aufgerufen, wenn dinfothread fertig ist Private Sub dirinfo_done(ByVal di As dirinfo) di.tn.Tag = di If di.tn Is details_tn Then ShowTreenodeDetails(di.tn) End If End Sub
15.2 Formularinterna
743
' Prozedur, um einen eventuell noch laufenden Thread zu stoppen Private Sub stop_dirinfo_thread() If (Not IsNothing(dinfothread)) AndAlso dinfothread.IsAlive Then dinfothread.Abort() dinfothread.Join() End If End Sub ' bei Programmende eventuell noch laufenden Thread stoppen Private Sub Form1_Closing(...) Handles MyBase.Closing stop_dirinfo_thread() End Sub
Programmcode der dirinfo-Klasse Die dirinfo-Klasse bietet vom Standpunkt der Multithreading-Programmierung her relativ wenig Neuigkeiten. Der Großteil des Codes unterscheidet sich nicht von einer gewöhnlichen Klasse. Bemerkenswert ist nur der Aufruf der Ereignisse Done und Update durch Invoke bzw. BeginInvoke. Das ist deswegen erforderlich, weil in diesen Ereignisprozeduren der Fensterinhalt aktualisiert wird. Deswegen muss sichergestellt sein, dass die Ereignisprozeduren im Thread des Fensters ausgeführt werden. ' Beispiel benutzeroberflaeche\multithread-treeview Class dirinfo Const interval As Integer = 300 'Aktualisierungsinvervall in ms Public files, directories, size, errors As Long Public tn As TreeNode Public path As String Private frm As Form Private nextupdate As Date ' Ereignisse Public Event Done(ByVal di As dirinfo) Public Event Update(ByVal di As dirinfo) ' Delegates Private Delegate Sub sub_nopara() ' Konstruktor Public Sub New(ByVal frm As Form, ByVal tn As TreeNode) Me.frm = frm Me.path = tn.FullPath Me.tn = tn End Sub
744
15 Gestaltung von Benutzeroberflächen
' Methoden Public Sub GetProperties() nextupdate = Now.AddMilliseconds(100) '1. Update nach 100 ms ReadDirectory(path) frm.Invoke(New sub_nopara(AddressOf raiseDoneEvent)) End Sub ' rekursiv die Verzeichniseigenschaften ermitteln Private Sub ReadDirectory(ByVal p As String) Dim dir As New IO.DirectoryInfo(p) Dim subdir As IO.DirectoryInfo Dim file As IO.FileInfo Try For Each file In dir.GetFiles files += 1 Try size += file.Length Catch 'Fehler ignorieren errors += 1 End Try Next For Each subdir In dir.GetDirectories directories += 1 ReadDirectory(subdir.FullName) Next Catch 'Fehler ignorieren errors += 1 End Try ' Update-Event auslösen If Now > nextupdate Then frm.BeginInvoke(New sub_nopara(AddressOf raiseUpdateEvent)) nextupdate = Now.AddMilliseconds(interval) End If End Sub ' werden per Invoke aufgerufen Sub raiseDoneEvent() RaiseEvent Done(Me) End Sub Sub raiseUpdateEvent() RaiseEvent Update(Me) End Sub End Class
15.2 Formularinterna
745
VERWEIS
Weitere Beispiele Tipps, wie Sie mehrere Fenster in eigenen Threads öffnen können, folgen in Abschnitt 15.3.2. Ein weiteres Multithreading-Beispiel finden Sie in Abschnitt 16.5.6: Dort geht es um ein Programm, das im Hintergrund eine Apfelmännchengrafik berechnet, ohne die Benutzeroberfläche zu blockieren. Die Online-Hilfe enthält natürlich ebenfalls Beispiele. Suchen Sie nach Exemplarische Vorgehensweise Multithreading bzw. nach Beispiel für Multithreadsteuerelemente: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconFreeThreadingExample.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/ cpcondevelopingmultithreadedwindowsformscontrol.htm
15.2.8 Betriebssysteminformationen ermitteln Dieser Abschnitt stellt ganz kurz einige Windows.Forms-Klassen vor, die bei der Ermittlung diverser Spezialdaten helfen, die in Windows-Anwendungen manchmal benötigt werden.
SystemInformation-Klasse Die zahlreichen Eigenschaften der SystemInformation-Klasse geben Auskunft über verschiedene Details des Betriebssystems. Die folgende unvollständige Aufzählung beschreibt einige charakteristische Eigenschaften. Eigenschaften von System.Windows.Forms.SystemInformation DoubleClickTime
gibt an, innerhalb welcher Zeitspanne zwei Mausklicks als Doppelklick gewertet werden.
DragFullWindows
gibt an, ob Fenster mit ihrem Inhalt verschoben werden (oder nur als Rahmen).
FrameBorderSize
gibt die Breite des Fensterrands an.
IconSize
gibt die Größe von Icons an.
MenuButtonSize
gibt die Größe von Buttons in Menüs oder Symbolleisten an.
MenuFont
gibt die Schriftart an, die für Menüs verwendet wird.
MonitorCount
gibt die Anzahl der Bildschirme an (normalerweise 1). Weitere Informationen zu den Bildschirmen können Sie mit der Screen-Klasse ermitteln.
MouseWheelScrollLines
gibt an, wie viele Textzeilen bei der Drehung des Mausrads um eine Rastereinheit gescrollt werden sollen.
PenWindows
gibt an, ob eine Windows-Version mit Unterstützung für einen Eingabestift vorliegt.
746
15 Gestaltung von Benutzeroberflächen
Eigenschaften von System.Windows.Forms.SystemInformation VirtualScreen
gibt als Rectangle-Objekt die Gesamtgröße aller Bildschirme an (wenn mehrere Monitore gleichzeitig verwendet werden).
WorkingArea
gibt als Rectangle-Objekt die Größe des Desktop-Bereichs an. (Das ist der Bildschirm abzüglich des Raums, den die Taskleiste einnimmt.)
OSFeature-Klasse Die OSFeature-Klasse gibt Auskunft darüber, welche Fähigkeiten das Betriebssystem hat, unter dem das laufende Programm ausgeführt wird. Die Feature-Eigenschaft dieser Methode liefert ein OSFeature-Objekt. Nun kann mit IsPresent getestet werden, ob eine bestimmte Funktion zur Verfügung steht. GetVersionPresent ermittelt, in welcher Version die Funktion zur Verfügung steht. An IsPresent und GetVersionPresent lassen sich zurzeit nur die Objekte OSFeature.LayeredWindows bzw. OSFeature.Themes übergeben. Damit kann ermittelt werden, ob das Betriebssystem transparente Fenster unterstützt (z.B. Windows 2000/XP) bzw. ob sein Aussehen durch so genannte Themes verändert werden kann (Windows XP). If OSFeature.Feature.IsPresent(OSFeature.LayeredWindows) Then ...
Screen-Klasse Wenn Sie auf einem System mit mehreren Monitoren arbeiten, können Sie mit der ScreenKlasse deren Eigenschaften ermitteln. AllScreens liefert dazu ein Feld von Screen-Objekten, PrimaryScreen verweist auf das Screen-Objekt des Hauptmonitors. Sobald Sie ein Screen-Objekt haben, liefert Bounds ein Rectangle-Objekt mit dem Koordinatenbereich des Monitors. WorkingArea ermittelt den für Fenster nutzbaren Desktop-Bereich (ohne Taskleiste). Primary gibt an, ob es sich um den Hauptmonitor handelt. DeviceName gibt schließlich die interne Bezeichnung des Bildschirms an.
InputLanguage-Klasse Die InputLanguage-Klasse gibt Auskunft über die Sprache, unter der das Programm läuft. Die Eigenschaft CurrentInputLanguage liefert die aktuelle Sprache als InputLanguage-Objekt. DefaultInputLanguage gibt die Defaultsprache am Rechner an, InstalledInputLanguages gibt an, welche Sprachen sonst noch installiert sind. Sobald Sie ein InputLanguage-Objekt haben, können Sie mit LayoutName den Namen der Sprache ermitteln, mit Culture ein dazu passendes Globalization.CultureInfo-Objekt. Wenn Sie die folgenden Zeilen auf einem Rechner im deutschen Sprachraum ausführen, werden Sie im Regelfall das Ergebnis s = "Deutsch" erhalten.
15.2 Formularinterna
747
VERWEIS
Dim s As String s = InputLanguage.CurrentInputLanguage.LayoutName
Die InputLanguage-Klasse bezieht sich nur auf die (durch das Betriebssystem) installierten Sprachen. Sie hat nichts mit einer möglichen Lokalisierung des Programms zu tun, die im nächsten Abschnitt beschrieben wird.
15.2.9 Bildschirmauflösung (DPI) Grundsätzlich können Sie unter Windows angeben, wie viele Punkte pro Zoll (Dots per Inch, DPI) Ihr Monitor darstellen kann. Der Zweck dieser Einstellung besteht darin, die Größe der Schriften so an die Auflösung anzupassen, dass Text immer gleich groß ist (und etwa auch dann noch lesbar ist, wenn Sie vor einem 17-Zoll-Bildschirm sitzen, der mit einer Auflösung von 1600*1200 Punkten betrieben wird). Besonders praktisch ist eine geänderte DPI-Einstellung bei Notebooks, bei denen der Bildschirm häufig klein, die Auflösung aber hoch ist. Aber auch Anwender mit Augenproblemen schätzen es sehr, wenn die Bedienungselemente von Programmen allein durch die Veränderung der DPI-Einstellung besser lesbar werden. Um die DPI-Einstellung zu verändern, klicken Sie den Windows-Desktop-Hintergrund mit der rechten Maustaste an, wählen EIGENSCHAFTEN|EINSTELLUNGEN|ERWEITERT und suchen sich den gewünschten Schriftgrad aus. KLEINE SCHRIFTEN entspricht der Defaulteinstellung von 96 DPI. GROSSE SCHRIFTEN entspricht 120 DPI. Außerdem können Sie mit der Einstellung ANDERE einen beliebigen DPI-Faktor einstellen.
HINWEIS
Die DPI-Einstellung wird nach einem Windows-Neustart von allen Programmen automatisch übernommen. (Bei manchen Programmen klappt es auch ohne Windows-Neustart, bei der VB.NET-Entwicklungsumgebung aber nicht!) Menütexte sollten nun in einer entsprechend kleineren oder größeren Schrift dargestellt werden, die Größe der Symbole in Symbolleisten sollten angepasst werden, ebenso Text in Dialogboxen etc. In der Praxis klappt das oft nur mit Einschränkungen. (Beispielsweise sind die wenigsten Programme in der Lage, Symbolleisten an die DPI-Einstellung anzupassen.) Die meisten Beispielprogramme dieses Buchs sowie die davon abgeleiteten Bildschirmabbildungen wurden mit einer DPI-Einstellung von 120 erzeugt.
DPI-Einstellung bei Windows.Forms Auch das Aussehen von Fenstern und Dialogen, die Sie mit VB.NET erzeugen, ändert sich je nach DPI-Einstellung. Mit zunehmendem DPI-Wert werden die einzelnen Steuerelemente größer und rutschen nach links bzw. nach unten. Diese automatische Größenanpassung erfolgt nur, wenn die Eigenschaft AutoScale des Formulars True enthält. (Das ist die Defaulteinstellung.)
748
15 Gestaltung von Benutzeroberflächen
Die automatische Größenanpassung erfolgt übrigens auch in der Entwicklungsumgebung. Wenn Sie beispielsweise ein Formular bei 96 DPI entwerfen und später (oder auf einem anderen Rechner) bei 120 DPI wieder in die Entwicklungsumgebung laden, dann verändern sich auch dort Größe und Position der Steuerelemente. In Abbildung 15.8 sehen Sie links einen kleinen Dialog bei einer Bildschirmauflösung von 96 DPI. Rechts sehen Sie, wie derselbe Dialog aussieht, wenn das Programm bei 120 DPI ausgeführt wurde. (Der Dialog wurde in der Entwicklungsumgebung bei 96 DPI erstellt.)
Abbildung 15.8: Links ein Dialog bei 96 DPI, rechts bei 120 DPI
Die Position und Größe von Steuerelementen innerhalb eines Formulares wird in Pixeln angegeben. Vor dem Anzeigen des Formulars werden alle derartigen Angaben an die jeweilige DPI-Einstellung angepasst. Das bedeutet, dass die Eigenschaften Size und Location der diversen Steuerelemente je nach DPI-Einstellung unterschiedliche Werte liefern! Beispielsweise befindet sich das linke obere Eck von Button 1 bei 96 DPI an der Position (16, 72), bei 120 DPI aber an der Position (20, 89). Diese Position wird auch im Eigenschaftsfenster angezeigt, wenn Sie das Programm in der Entwicklungsumgebung bei 120 DPI bearbeiten. Im Codeblock Vom Windows Form Designer generierter Code werden Sie dagegen weiterhin die ursprüngliche Position vorfinden.
VERWEIS
Wie aus Abbildung 15.8 hervorgeht, ändern sich nicht nur die Koordinaten der Steuerelemente, sondern auch die Schriftgrößen. (Das ist ja gerade der Sinn der DPI-Anpassung.) Wenn Sie sich aber die Einstellung der Schriftgröße im Eigenschaftsfenster der Entwicklungsumgebung ansehen, werden Sie feststellen, dass diese dort unverändert geblieben ist. Der Grund besteht darin, dass die Schriftgröße nicht in Pixeln, sondern in Punkt (point) angegeben wird. Ein Punkt entspricht 1/72 Inch. Wie groß die Schrift in Pixeln gemessen ist, hängt aber von der DPI-Einstellung ab. Deswegen beansprucht eine Schrift mit der Größe 8 pt je nach DPI-Einstellung unterschiedlich viel Platz (gemessen in Pixel). Die DPI-Einstellung wird auch bei der Erzeugung neuer Font-Objekte berücksichtigt. Mehr zu diesem Thema finden Sie in Abschnitt 16.3, wo der Umgang mit Schriftarten detailliert behandelt wird.
15.2 Formularinterna
749
Interna Als Vergleichsmaßstab für die automatische Größenanpassung dient die AutoScaleBaseSize-Eigenschaft des Formulars. Diese Eigenschaft wird beim Erzeugen des Formulars in der Entwicklungsumgebung initialisiert und kann danach nicht mehr verändert werden (zumindest nicht im Eigenschaftsfenster, wo die Eigenschaft gar nicht angezeigt wird). AutoScaleBaseSize enthält nicht einfach den DPI-Wert, sondern offensichtlich die typische
Größe eines Buchstaben (gemessen in Bildschirmpixel) unter Anwendung der Defaultschriftart des Formulars und der aktuellen DPI-Einstellung. Diese Größe kann auch mit der Methode GetScaleBaseSize ermittelt werden. (Leider sind sowohl AutoScaleBaseSize also auch GetScaleBaseSize äußerst dürftig dokumentiert.) Wenn Sie einen Blick in den Codeblock Vom Windows Form Designer generierter Code werfen, werden Sie entdecken, dass AutoScaleBaseSize dort eingestellt wird, und zwar beispielsweise mit Size(5, 13) bei einem DPI-Wert von 96 bzw. mit Size(6, 15) bei einem DPI-Wert von 120. (Diese Angaben gelten für die Defaultschrift Sans Serif 8pt.) #Region " Vom Windows Form Designer generierter Code" ... Private Sub InitializeComponent() ... Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) ...
Offensichtlich werden erst beim Anzeigen des Formulars (nach der Ausführung von InitalizeComponent) die im Code gespeicherten Pixelkoordinaten der Steuerelemente in die tatsächlichen Koordinaten umgerechnet. Dabei wird offensichtlich getestet, ob der Platzbedarf eines Zeichens in der Formularschriftart von AutoScaleBaseSize abweicht. In diesem Fall werden sämtliche Positions- und Größenangaben korrigiert.
HINWEIS
Eine interessante (und undokumentierte) Konsequenz der automatischen Größenkorrektur besteht darin, dass diese Korrektur auch dann durchgeführt wird, wenn innerhalb von InitializeComponent die Schriftart des Formulars verändert wird. Dazu fügen Sie einfach die folgenden Zeilen am Ende von InitializeComponent ein: Dim fnt As New Font("arial", 15) Me.Font = fnt
Damit wird der Defaultzeichensatz des Formulars und aller darin enthaltenen Steuerelemente auf Arial, 15 Punkt gesetzt. Gleichzeitig werden das Formular und alle enthaltenen Steuerelemente entsprechend vergrößert (siehe Abbildung 15.9). Beachten Sie, dass diese automatische Größenanpassung nicht funktioniert, wenn Sie die Schriftart im Eigenschaftsfenster des Formulars verändern, weil die Entwicklungsumgebung in diesem Fall auch die AutoScaleBaseSize-Einstellung verändert.
750
15 Gestaltung von Benutzeroberflächen
Abbildung 15.9: Nochmals der Dialog aus Abbildung 15.8, diesmal mit der Schriftart Arial, 15 Punkt bei 120 DPI
Probleme durch die automatische DPI-Anpassung Problematisch kann die automatische Größenanpassung dann werden, wenn im Dialog bzw. im Fenster auch Objekte angezeigt werden, deren Größe unveränderlich ist (Bitmaps, Icons etc.) oder wenn Sie mit Graphics-Methoden (siehe Kapitel 16) direkt in das Fenster zeichnen und sich darauf verlassen, dass sich die Steuerelemente an dem Ort befinden, an dem sie sich während des Programmentwurfs befanden. Beispielsweise bereitet das in Abbildung 16.14 (Seite 859) zu sehende Beispielprogramm Probleme: Dort werden die verschiedenen Muster beschriftet. Die Schriftgröße ist DPIabhängig, der Abstand zwischen den Testmustern ist dagegen pixelabhängig. Die Folge: bei einem hohen DPI-Wert ist der Spaltenabstand zu klein, die Texte überlappen einander und werden unleserlich. Eine ganz einfache Lösung bestünde bei diesem Programm darin, die Schriftgröße nicht in Punkt, sondern in Pixel anzugeben. Generell ist es also eine gute Idee, ein Programm mit unterschiedlichen DPI-Einstellungen zu testen. (Ich weiß aus eigener Erfahrung, dass das lästig ist, weil der Rechner neu gestartet werden muss. Aber das ist nicht zu ändern.) Wenn es Probleme gibt, bieten sich unterschiedliche Lösungsansätze an: •
Sie setzen die AutoScale-Eigenschaft des Formulars auf False. Der offensichtliche Nachteil besteht darin, dass gerade Notebook-Besitzer darüber klagen werden, dass die Bedienungselemente ihres Programms unleserlich klein sind.
•
Sie berücksichtigen bei Bildschirmausgaben (Graphics-Methoden) die tatsächliche Position und Größe von Steuerelementen.
•
Sie stellen Bitmaps in PictureBox-Steuerelementen mit SizeMode=StretchImage dar. Das bewirkt, dass die Bitmaps je nach DPI-Einstellung entsprechend verkleinert bzw. vergrößert werden. (Leider ist das auch mit einem gewissen Qualitätsverlust verbunden.)
Die aktuelle DPI-Einstellung können Sie aus den Eigenschaften DpiX und DpiY eines Graphics-Objekt entnehmen, auf das Sie beispielsweise in der Paint-Ereignisprozedur zu jedem Steuerelement zugreifen können:
15.2 Formularinterna
751
Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles MyBase.Paint Dim gr As Graphics = e.Graphics MsgBox("DPI-X: " & gr.DpiX & vbCrLf & _ "DPI-Y: " & gr.DpiY) End Sub
Ein entsprechendes Graphics-Objekt erhalten Sie auch außerhalb einer Paint-Ereignisprozedur, wenn Sie die Handle-Eigenschaft eines Steuerelements oder Formulars nutzen. Beachten Sie aber, dass die Methode FromHwnd aus Sicherheitsgründen per Default nur funktioniert, wenn das Programm von einer lokalen Festplatte (nicht von einem Netzwerklaufwerk) ausgeführt wird! Dim gr As Graphics = Graphics.FromHwnd(Me.Handle)
15.2.10 Lokalisierung von Windows-Anwendungen
VERWEIS
Der Begriff Lokalisierung beschreibt den Prozess, ein Programm an mehrere Sprachen anzupassen. Im Wesentlichen müssen dazu alle Beschriftungstexte (Buttons, Menüs, Fenstertitel etc.) sowie alle Ausgabezeichenketten (für MsgBox etc.) an die jeweilige Landessprache angepasst werden. Weitere Informationen zum Thema Lokalisierung finden Sie in der Hilfe, wenn Sie nach Globalisieren und Lokalisieren suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vboriInternationalization.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vsent7/html/vxoriPlanningGlobalReadyApplications.htm
Eigenschaften von Steuerelementen und Formularen lokalisieren Die Entwicklung mehrsprachiger Windows.Forms-Anwendungen hat sich in VB.NET wesentlich vereinfacht: Die Entwicklung eines mehrsprachigen Programms beginnt damit, dass Sie im Formular-Designer das Fenster anklicken und dann im Eigenschaftsfenster die Eigenschaft Localizable auf True stellen. Anschließend benennen Sie alle Text-Eigenschaften in der Defaultsprache (am besten in Englisch). Um nun eine deutsche, italienische, französische Version etc. zu erstellen, stellen Sie einfach die Language-Eigenschaft ein (die bisher den Wert Default enthalten hat) und geben dann bei allen Steuerelementen neue Beschriftungstexte an. Die Entwicklungsumgebung kümmert sich selbst darum, die Texte für die unterschiedlichen Sprachen in unterschiedlichen Ressourcendateien zu speichern. Beim Kompilieren werden die Sprachinformationen in DLLs umgewandelt, die zusammen mit dem Programm ausgeliefert werden müssen. Beim Programmstart testet das Programm, ob eine Sprach-DLL für die aktuelle Sprache des Benutzers vorliegt. Wenn das der Fall ist, werden
752
15 Gestaltung von Benutzeroberflächen
die darin enthaltenen Texte zur Beschriftung der Steuerelemente verwendet. Andernfalls werden die Beschriftungstexte der Defaultsprache verwendet. Abbildung 15.10 zeigt das Beispielprogramm benutzeroberflaeche\localization-test, mit dem die Lokalisierung ausprobiert wurde. Der Code zur Darstellung der Dialogbox wird etwas weiter unten beschrieben.
Abbildung 15.10: Links das Beispielprogramm in der Defaultsprache (Englisch), rechts in der deutschen Lokalisierung
VORSICHT
Wenn Localizable auf True gestellt wird, dann gelten alle bisherigen Einstellungen für die Sprache Default. Dieses Verhalten geht davon aus, dass Sie Ihr Programm zuerst englischsprachig beschriften. Das ist zwar in den USA eine meist zutreffende Annahme, nicht aber im deutschen Sprachraum. Die Konsequenz dieses Verhaltens: Wenn Sie Ihr Programm zuerst deutschsprachig beschriftet haben und nun Localizable ändern, müssen Sie nicht nur sämtliche TextEigenschaften der Default-Version ins Englische umstellen, sondern anschließend alle Text-Eigenschaften der deutschsprachigen Version neu eingeben! Wenn Sie also vorhaben, ein Programm zuerst in einer deutschen Version zu entwickeln und eine Lokalisierung später durchzuführen, dann sollten Sie unbedingt bereits bei Beginn der Projektentwicklung Localizable = True und Language = German einstellen! Später können Sie dann mühelos die Defaultlokalisierung (üblicherweise Englisch) und eventuell auch eine Lokalisierung für weitere Sprachen durchführen.
Interna Auch wenn ein Blick in das Eigenschaftsfenster das Gegenteil vermuten lässt, sind Localizable und Language gar keine Eigenschaft der Form-Klasse. Vielmehr fügt das Eigenschaftsfenster diese Schlüsselwörter ein, um Ihnen die Lokalisierung so einfach wie möglich zu machen.
15.2 Formularinterna
753
Ressourcendateien: Wenn Sie Localizable auf True stellen, werden unzählige Eigenschaften aller Steuerelemente der zum Formular gehörenden Ressourcendatei gespeichert. Diese Datei im XML-Format speichert normalerweise nur relativ wenige Objekte, die nicht im Textformat ausgedrückt werden können (z.B. Bitmaps, Icons etc.). Bei lokalisierbaren Dateien werden darüber hinaus aber von allen Steuerelementen ca. ein Dutzend Eigenschaftseinstellungen gespeichert, z.B. Text, TextAlign, Font, Image etc. (Der Grund für die relativ große Anzahl von Eigenschaften besteht darin, dass bei der Lokalisierung ja eventuell nicht nur die Text-Eigenschaft eines Buttons geändert werden muss, sondern auch dessen Hintergrundbild etc.)
TIPP
HINWEIS
Der Name der Ressourcendatei ergibt sich aus den Namen des Formulars, d.h., die Ressourcendatei zu Form1.vb hat den Namen Form1.resx. Sobald Sie bei der Einstellung von Language eine bisher noch unbekannte Sprache auswählen, erzeugt die Entwicklungsumgebung eine weiterer Ressourcendatei, in deren Name auch die Sprach- und Landeskennung eingebaut wird. Beispielsweise dient Form1.de.resx zur Speicherung der deutschen Lokalisierung. In den sprachspezifischen Ressourcendateien werden nur die Abweichungen gegenüber der Defaultressourcendatei gespeichert. Aus diesem Grund sind diese Dateien deutlich kleiner. Vorsicht ist aber geboten, wenn Sie Eigenschaften verändern, die für alle Sprachen gelten sollen. Dann müssen Sie unbedingt daran denken, Language vorher auf Default zurückzusetzen – sonst gilt die Änderung nur für die gerade aktuelle Sprache. Das gilt auch für Eigenschaften, die auf den ersten Blick gar nichts mit der Lokalisierung zu tun haben (z.B. Image oder BackgroundImage)!
Wenn Sie sich die Ressourcendateien ansehen möchten, klicken Sie im Projektmappenexplorer den Button ALLE DATEIEN ANZEIGEN an. Anschließend können Sie den Eintrag Form1.vb auseinanderklappen und finden darunter alle dazugehörenden Ressourcendateien (siehe Abbildung 15.11 etwas weiter unten).
Kompilat: Die Einstellungen für die Defaultlokalisierung werden beim Kompilieren direkt in die *.exe-Datei integriert. Für alle anderen Sprachen werden eigene *.dll-Dateien erstellt und in Verzeichnissen gespeichert, deren Name den Sprach- oder Ländercode angibt. Die deutsche Lokalisierungsdatei hat den Namen de\<programmname>.resources.dll. Code: Die Auswertung der Lokalisierungsdateien erfolgt in der Prozedur InitializeComponent des vom Windows Form Designer generierten Codes. Dort wird zuerst ein Objekt der Klasse ResourceManager erzeugt. Anschließend werden die Methoden GetString bzw. GetObject verwendet, um aus den in der *.exe-Datei enthaltenen Ressourcen bzw. aus den Zusatz-DLLs die gewünschten Zeichenketten oder Objekte zu lesen. (Dank der ResourceManager-Klasse wird dabei automatisch die richtige Ressource verwendet!)
754
15 Gestaltung von Benutzeroberflächen
' Beispiel benutzeroberflaeche\localization-test Private Sub InitializeComponent() Dim resources As System.Resources.ResourceManager = _ New System.Resources.ResourceManager(GetType(Form1)) ... Me.Button1.Text = resources.GetString("Button1.Text") Me.Button1.TextAlign = _ CType(resources.GetObject("Button1.TextAlign"), _ System.Drawing.ContentAlignment) ...
Lokalisierung ausprobieren Das Programm verwendet automatisch die Lokalisierung, die zur aktuellen für das Betriebssystem eingestellten Sprache passt. Um auch die anderen Lokalisierungen zu testen, können Sie die Sprache in den Systemeinstellungen ändern. Einfacher ist es aber, die Sprache nur für das laufende Programm zu ändern. Dazu weisen Sie der CurrentUICultureEigenschaft des Threads des Programms (Eigenschaft CurrentThread) eine neues CultureInfo-Objekt zu. Die gewünschte Sprache geben Sie durch eine Zeichenkette an (z.B. "de" für Deutsch, "en" für Englisch etc.) Die Spracheinstellung muss in der Prozedur New des vom Windows Form Designer generierten Codes vor dem Aufruf von InitializeComponent erfolgen. Die folgenden Zeilen stellen die Sprache Schwedisch ein. ' Beispiel benutzeroberflaeche\localization-test Public Sub New() MyBase.New() Threading.Thread.CurrentThread.CurrentUICulture = _ New Globalization.CultureInfo("sv") InitializeComponent() End Sub
Wenn Sie möchten, dass nicht nur die Elemente der Benutzeroberfläche angepasst werden, sondern auch Datums- und Zeitangaben entsprechend den jeweiligen Landeseinstellungen formatiert werden, müssen Sie in New eine zweite Anweisung einfügen, die auch CurrentCulture verändert. Dabei muss das angegebene CultureInfo-Objekt exakter angegeben werden – z.B. "de-DE" für Deutsch in Deutschland, "de-AT" für Deutsch in Österreich oder "sv-SE" für Schwedisch in Schweden.
VERWEIS
Threading.Thread.CurrentThread.CurrentCulture = _ New Globalization.CultureInfo("sv-SE")
Eine Referenz aller Sprach- und Landeszeichenketten zur Erzeugung neuer CultureInfo-Objekte finden Sie in der Hilfe bei der Beschreibung der CultureInfo-Klasse.
HINWEIS
15.2 Formularinterna
755
Eine Änderung der Sprache im bereits laufenden Programm – nachdem also die Steuerelemente in einer bestimmten Sprache angezeigt wurden – ist leider nicht vorgesehen.
Zusätzliche Zeichenketten in den Lokalisierungsdateien speichern Mit der Übersetzung der Texte von Menüs, Buttons etc. ist es nicht getan. Auch im Programmcode befinden sich üblicherweise eine Menge Zeichenketten, die dazu verwendet werden, Dialogtexte, Fehlermeldungen etc. zusammenzusetzen. Die Lokalisierung dieser Texte ist leider erheblich mühsamer als bei den Steuerelementen. Die Grundidee besteht darin, dass Sie selbst für jede Sprache eine weitere Ressourcendatei erstellen müssen, um die Zeichenketten bei Bedarf von dort auszulesen. Auch hier beginnen Sie am besten mit der Ressourcendatei für die Defaultsprache. Diese Datei erzeugen Sie mit PROJEKT|NEUES ELEMENT HINZUFÜGEN|ASSEMBLY-RESSOURCENDATEI. Dabei sollten Sie der Datei gleich einen aussagekräftigen Namen geben, beispielsweise Form1Strings. Die Entwicklungsumgebung fügt automatisch die Endung *.resx hinzu. (Der Name sollte keine Bindestriche enthalten! Verwenden Sie gegebenenfalls einen Unterstrich.)
Abbildung 15.11: Zum Projekt gehören sechs Ressourcendateien
Mit einem Doppelklick im PROJEKTMAPPEN-EXPLORER können Sie die noch leere Datei in einer Tabellenansicht öffnen (siehe Abbildung 15.12). Nun geben Sie für jede Zeichenkette, die Sie in Ihrem Programm benötigen, jeweils deren Namen und den Inhalt an. Damit Sie in ihrem Programm auf diese Zeichenketten zugreifen können, benötigen Sie ein Objekt der Klasse ResourceManager aus dem Namensraum System.Resources. Dieses Objekt erzeugen Sie am besten in der Form_Load-Ereignisprozedur. Bei der New-Methode müssen
756
15 Gestaltung von Benutzeroberflächen
Sie im ersten Parameter den Ressourcennamen angeben. Dieser setzt sich aus dem Stammnamensraum (root namespace, siehe PROJEKTEIGENSCHAFTEN-Dialog) und dem Dateinamen ohne Kennung zusammen. Dim myResources As Resources.ResourceManager Private Sub Form1_Load(...) Handles MyBase.Load myResources = New Resources.ResourceManager( _ "localization_test.Form1Strings", _ GetType(Form1).Assembly) End Sub
Der Zugriff auf die Zeichenketten erfolgt nun ganz einfach mit der Methode GetString. (Achten Sie auf die korrekte Groß- und Kleinschreibung!) Mit den folgenden Zeilen wird eine einfache Dialogbox angezeigt (siehe Abbildung 15.10 etwas weiter oben). Private Sub Button1_Click(...) Handles Button1.Click Dim s1, s2 As String s1 = myResources.GetString("strGreeting") s2 = myResources.GetString("strDate") MsgBox(s1 + vbCrLf + s2 + " " + Now.ToLongDateString) End Sub
Das Schöne an der Sache ist, dass GetString automatisch überprüft, ob es eine lokalisierte Ressourcendatei gibt. In diesem Fall liest die Methode die Zeichenkette aus der passenden Datei. Wenn die Programmentwicklung in der Defaultsprache abgeschlossen ist, erstellen Sie einfach mehrere Kopien der Datei Form1Strings.resx und ändern deren Namen in Form1Strings.de.resx (deutsche Lokalisierung), Form1Strings.sv.resx (schwedische Lokalisierung) etc. Diese Dateien fügen Sie mit PROJEKT|VORHANDENENES ELEMENT HINZUFÜGEN in Ihr Projekt ein, öffnen Sie und übersetzen dann einfach alle Zeichenketten. Wenn Sie bei GetString den Namen einer Zeichenkette angeben, die in der lokalisierten Form fehlt, liefert GetString einfach die Zeichenkette aus der Default-Ressourcendatei. Gibt es den Namen auch dort nicht (z.B. weil Sie sich vertippt und die Zeichenkette "strData" statt "strDate" angefordert haben), liefert GetString ohne Fehlermeldung einfach eine leere Zeichenkette. Das große Problem bei der Programmentwicklung besteht darin, die Ressourcendateien synchron zu halten. Wenn Sie in einer Datei den Namen eines Elements umbenennen, müssen Sie das auch in allen anderen Resourcendateien tun. Das Gleiche gilt, wenn Sie eine neue Zeichenkette einfügen.
15.3 Verwaltung mehrerer Fenster
757
Abbildung 15.12: Oben die Default-Resourcendatei, unten deren deutsche Lokalisierung
15.3
Verwaltung mehrerer Fenster
VERWEIS
Eine spezielle Form zur Verwaltung mehrerer Fenster sind MDI-Anwendungen. Dabei werden mehrere Subfenster in einem Hauptfenster angezeigt. Die Programmierung und Verwaltung von MDI-Anwendungen wird im nächsten Abschnitt beschrieben.
TIPP
Viele Beispiele dieses Kapitels bestehen aus nur einem einzigen Fenster. In der Praxis haben Sie es aber oft mit Anwendungen zu tun, die aus mehreren Fenstern bestehen. Dieser Abschnitt beschreibt einige Techniken im Umgang mit mehreren Fenstern. Insbesondere geht es darum, wie mehrere Fenster per Code geöffnet und geschlossen werden und unter welchen Umständen das gesamte Programm beendet wird.
Die Konzepte bei der Verwaltung mehrerer Fenster sind am einfachsten zu verstehen, wenn Sie es einfach ausprobieren. Sie finden entsprechende Beispielprogramme auf der CD in den Verzeichnissen oberflaeche\multi1, -2 etc.
758
15 Gestaltung von Benutzeroberflächen
15.3.1 Fenster als modale Dialoge anzeigen Der einfachste Fall besteht darin, dass Sie – ausgehend von einem Hauptfenster – weitere Fenster als modale Dialoge anzeigen. Das bedeutet, dass das Hauptfenster erst dann wieder verwendet werden kann, nachdem der Dialog beendet wurde. Zum Aufruf des Dialogs müssen Sie ein neues Objekt der Klasse des Formulars erzeugen. Anschließend verwenden Sie ShowDialog, um den Dialog anzuzeigen. (In den folgenden Beispielen heißen die Formulare Form1, Form2 etc., in realen Anwendungen ist es aber natürlich zielführend, aussagekräftigere Namen zu verwenden.) Dim frm As New Form2 frm.ShowDialog()
Wenn der Code des Dialog-Formulars es vorsieht, liefert ShowDialog den Rückgabewert des Formulars (ein Element der DialogResult-Aufzählung) als Ergebnis. Üblicherweise wird dieser Rückgabewert dazu verwendet, anzugeben, mit welchem Button (z.B. OK, ABBRUCH, JA, NEIN) der Dialog beendet wurde. Dazu kann in den Button-Ereignisprozeduren des Dialogformulars Me.DialogResult=... ausgeführt werden. Wenn die Button-Eigenschaft DialogResult im Eigenschaftsfenster eingestellt wird, können Sie sich diese Codezeile sparen (siehe auch Abschnitt 14.3.1). Die folgenden Zeilen zeigen die Auswertung des Rückgabewerts. Dim result As DialogResult result = frm.ShowDialog() If result = DialogResult.OK Then ...
Gestaltung von Dialogen Bei der Gestaltung bzw. Programmierung von modalen Dialogformularen sollten Sie einige Details beachten: •
In den Ereignisprozeduren zu den Buttons OK und ABBRUCH müssen Sie Me.Close oder Me.Hide ausführen, um das Fenster zu schließen. (Bei modalen Dialogen haben Close und Hide weitgehend dieselbe Wirkung. Der einzige Unterschied besteht darin, dass bei Hide die Closing- und Closed-Ereignisprozeduren des Dialogs nicht aufgerufen werden.)
•
Wenn der Anwender die Fenstergröße nicht ändern soll, stellen Sie FormBorderStyle am besten auf FixedSingle. In diesem Fall sollten Sie auch Maximize- und MinimizeBox=True angeben.
•
Damit der Dialog nicht in der Taskleiste angezeigt wird, stellen Sie ShowInTaskBar auf False.
Dialoge jedes Mal neu erzeugen oder wiederverwenden? Grundsätzlich gibt es zwei mögliche Strategien zur Anzeige von Dialogen. Die eine besteht darin, den Dialog jedes Mal neu zu erzeugen. In diesem Fall sollte der Dialog anschließend mit Dispose wieder aus dem Speicher entfernt werden.
15.3 Verwaltung mehrerer Fenster
759
Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New Form2 frm.ShowDialog() ... Auswertung frm.Dispose() End Sub
Die andere Variante besteht darin, das Formularobjekt einmal zu erzeugen und immer wieder neu anzuzeigen. In diesem Fall muss im Formularcode (also im Form2-Code) Hide verwendet werden, nicht Close! Dim frm As New Form2 Private Sub Button1_Click(...) Handles Button1.Click frm.ShowDialog() ... Auswertung End Sub
Die zweite Variante hat Vor- und Nachteile: Zuerst die Vorteile: Gerade bei aufwendigen Dialogen steht der Dialog sofort zur Verfügung und muss nicht jedes Mal neu erzeugt werden. (Wenn beispielsweise im Dialog eine umfangreiche Liste dargestellt wird, muss diese nur einmal initialisiert werden.) Außerdem sind alle Steuerelemente noch exakt so eingestellt, wie dies beim vorigen Aufruf des Dialogs der Fall war. Wenn Sie also beim ersten Dialogaufruf in einem Textfeld etwas eingegeben haben, steht diese Eingabe beim nächsten Aufruf noch zur Verfügung. (Wenn das Dialogobjekt dagegen jedes Mal neu erzeugt wird, sind die Eingabefelder immer wieder leer bzw. enthalten die bei der Initialisierung vorgesehenen Werte.) Der Nachteil: Das frm-Objekt wird während des Programmstarts des Hauptprogramms erzeugt und verlangsamt somit den Programmstart. Außerdem wird der Speicher für das frm-Objekt bereits beim Programmstart beansprucht und dann nicht mehr freigeben. Welche Strategie nun die bessere ist, hängt vom Anwendungsszenario ab. Den Kopf müssen Sie sich ohnedies nur bei komplexen Dialogen darüber zerbrechen. Bei einfachen Dialogen spielt es keine Rolle – weder ist der Geschwindigkeitsunterschied bemerkbar, noch der Speicherverbrauch spürbar.
Datenauswertung Normalerweise wollen Sie nicht nur wissen, ob der Dialog mit OK oder ABBRUCH (oder einem anderen Button) beendet wurde, sondern auch, welche Daten in dem Dialog eingegeben, welche Listeneinträge ausgewählt und welche Optionen angeklickt wurden. Die einfachste Art der Auswertung besteht darin, einfach die Steuerelemente auszulesen. frm.ShowDialog() If frm.TextBox1.Text = ...
Wenn Sie innerhalb der Dialogklasse (also im Form2-Code) Klassenvariablen mit Friend oder Public deklarieren, können Sie darauf wie auf die Steuerelemente zugreifen. If frm.myIntegerVar > 10 Then ...
760
15 Gestaltung von Benutzeroberflächen
Die eleganteste Methode besteht darin, den Dialog mit zusätzlichen Eigenschaften und Methoden auszustatten, um die eingegebenen Daten zu ermitteln (oder vor dem Aufruf des Dialogs voreinzustellen). Der dafür erforderliche Aufwand lohnt sich allerdings nur bei komplexeren Anwendungen. Die .NET-Standarddialoge geben Beispiele, wie derartige Eigenschaften und Methoden aussehen können (siehe auch Abschnitt 15.5).
Dialog unter der Maus zentrieren Per Default, d.h. mit der Einstellung StartPosition=WindowsDefaultLocation, erscheinen die Dialogfenster im Regelfall irgendwo am Bildschirm, oft weit entfernt vom Ausgangsfenster und von der Maus. (Nach welchen Gesichtspunkten Windows die Position errechnet, weiß ich nicht.) Eine deutliche Verbesserung bewirkt die Einstellung StartPosition = CenterParent. Damit wird der Dialog über dem Ausgangsfenster zentriert. Mit StartPosition=Manual können Sie die Startposition vollkommen selbst bestimmen. Der Code zum Aufruf des Dialogs könnte dann so aussehen: frm.Left = Me.MousePosition.X - frm.Width \ 2 frm.Top = Me.MousePosition.Y - frm.Height \ 2 result = frm.ShowDialog()
Damit wird der Dialog über der aktuellen Mausposition zentriert. Wenn Sie möchten, können Sie die Berechnung der Startkoordinaten natürlich noch ausfeilen, um sicherzustellen, dass der Dialog nicht teilweise über die Bildschirmgrenzen ragt. (Ein RectangleObjekt mit den Bildschirmgrenzen können Sie Screen.Bounds entnehmen.)
Beispiel Das Beispielprogramm oberflaeche\multi1 demonstriert verschiedene Varianten des Dialogaufrufs. Der Code ist ziemlich trivial, so dass aus Platzgründen auf den Abdruck verzichtet wird. (Alle relevanten Codefragmente sind im Rahmen dieses Abschnitts vorgestellt worden.)
Abbildung 15.13: Aufruf von Dialogen
15.3 Verwaltung mehrerer Fenster
761
15.3.2 Mehrere gleichberechtigte Fenster öffnen Die Methode Show ist der einfachste Weg, ein zweites Fenster zu öffnen, so dass dieses parallel zum ersten verwendet werden kann.
HINWEIS
' Beispiel benutzeroberflaeche\multi2 Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New Form1() frm.Show() End Sub
Für die hier beschriebenen Konzepte spielt es keine Rolle, ob die neuen Fenster alle von der gleichen Klasse (Form1) stammen oder ob unterschiedliche Fenster auf der Basis unterschiedlicher Formulare (Form1, Form2, Form3 etc.) erzeugt werden.
Diese Vorgehensweise ist zwar einfach, aber sie hat einige gravierende Nachteile (die Sie mit dem Beispielprogramm multi2 testen können). •
Wenn Sie Fenster 1 (also das Startfenster) schließen, endet das Programm. (Wenn die beiden Fenster wirklich gleichberechtigt wären, sollte das Programm laufen, bis alle Fenster geschlossen werden.)
•
Obwohl die beiden Fenster parallel verwendet werden können, teilen sie doch eine gemeinsame Nachrichtenschleife (message loop). Das hat zur Folge, dass eine länger andauernde Operation in einem Fenster alle anderen Fenster blockiert. Wenn Sie also durch einen Button-Klick eine Berechnung starten, die zehn Sekunden dauert, sind während dieser Zeit alle Fenster blockiert.
•
Es ist nicht ohne weiteres möglich, zwischen den Fenstern Daten auszutauschen. Anders als in VB6 gibt es keine globale Forms-Aufzählung, die auf alle geöffneten Fenster verweist.
VERWEIS
Windows.Form-Programme in einem eigenen Modul starten Vor der Präsentation anderer Lösungsansätze ist ein Grundverständnis erforderlich, was beim Start einer Windows.Forms-Anwendung eigentlich vor sich geht. Im Wesentlichen wird dabei Application.Run(New formname()) ausgeführt. Mehr Details zum Startprozess finden Sie in Abschnitt 15.2.
Statt sich auf den Application.Run-Automatismus beim Start von Windows-Programmen zu verlassen, können Sie diesen Schritt auch selbst durchführen. (Das hat den Vorteil, dass Sie den Startprozess genauer steuern können.) Dazu fügen Sie in Ihr Projekt ein neues Modul ein und programmieren dort die Prozedur Main nach dem unten angegebenen Muster. Außerdem geben Sie bei den Projekteigenschaften an, dass nicht Form1, sondern Module1 als Startobjekt gelten soll. Damit beginnt die Programmausführung mit der Prozedur Module1.Main.
762
15 Gestaltung von Benutzeroberflächen
Module Module1 Sub Main() Application.Run(New Form1()) ' wenn das Fenster geschlossen wird oder Application.Exit() ' ausgeführt wird, wird die Programmausführung an diesem ' Punkt fortgesetzt; wenn es hier keinen Code mehr gibt, ' endet das Programm End Sub End Module
Mehrere gleichberechtigte Fenster öffnen (multi3) Dadurch, dass Application.Run nun in einem Modul statt bisher automatisch ausgeführt wird, ist im Vergleich zu multi2 noch nichts gewonnen. Wenn Sie die Fenster aber selbst öffnen (vergessen Sie Show nicht!) und Application.Run dann ohne Parameter aufrufen, hat das den Vorteil, dass die Programmausführung nicht mehr beim Schließen des ersten Fensters endet. Module Module1 Sub Main() Dim frm1 As New Form1() Dim frm2 As New Form1() Dim frm3 As New Form1() frm1.Show() frm2.Show() frm3.Show() Application.Run() ' Vorsicht: dieser Punkt wird erst erreicht, wenn das ' Programm durch Application.Exit() explizit beendet wird End Sub End Module
Das Programm hat in dieser Form aber auch einen gravierende Nachteil: Es endet überhaupt nicht, auch dann nicht, wenn alle Fenster geschlossen werden! Es muss daher unbedingt sichergestellt werden, dass beim Schließen des letzten Fensters Application.Exit ausgeführt wird! Dazu ist es erforderlich, zumindest die Anzahl der geöffneten Fenster zu verwalten. (Wenn ein nicht behandelter Fehler auftritt, wird das Programm ebenfalls automatisch beendet.) Das im Folgenden vorgestellte Beispielprogramm multi3 basiert auf dem obigen Konzept, macht es aber allgemein anwendbar. Es verbessert multi2 in zwei Aspekten: •
Die Programmausführung endet automatisch, wenn alle geöffneten Fenster geschlossen werden. (Die Reihenfolge, in der die Fenster geschlossen werden, ist gleichgültig.)
•
Über die allen Modulen zugängliche Aufzählung myForms kann jedes Formular auf alle anderen Formulare zugreifen.
15.3 Verwaltung mehrerer Fenster
763
Die Programmausführung beginnt in Module1.Main. Dort wird mit ShowWindow das Startfenster angezeigt. Dazu wird die selbst definierte Methode ShowWindow verwendet, die die Formularvariable in eine ArrayList-Aufzählung einfügt und außerdem frm.Show ausführt. Anschließend wird die Nachrichtenschleife für das Fenster durch Application.Run ohne die Angabe weiterer Parameter gestartet. (Das bedeutet, dass das Programm explizit mit Application.Exit beendet werden muss!)
Abbildung 15.14: Mehrere gleichberechtigte Fenster (Beispiel multi3)
' Beispiel benutzeroberflaeche\multi3 Module Module1 Public myForms As New Collections.ArrayList() Sub Main() ShowWindow(New Form1()) Application.Run() MsgBox("Jetzt endet Main().") End Sub Public Sub ShowWindow(ByVal frm As Form) myForms.Add(frm) frm.Show() End Sub End Module
Im Form1-Code können mit dem Button NEUES FENSTER ANZEIGEN (Prozedur btnShow_Click) beliebig viele neue Fenster geöffnet werden. Daneben sieht das Programm zwei Buttons für ein Programmende vor. Bei der sanften Variante werden in btnClose_Click alle Formulare durchlaufen und durch Close geschlossen. (Damit wird in jedem Fenster die ClosingProzedur ausgeführt, so dass das Programmende dort noch verhindert werden kann.) Die radikale Variante besteht darin, einfach Application.Exit auszuführen, was ein sofortiges Programmende bewirkt.
HINWEIS
764
15 Gestaltung von Benutzeroberflächen
Die Exit-Methode erfordert besondere Ausführungsrechte, die beispielsweise nicht gegeben sind, wenn das .NET-Programm von einem Netzwerkverzeichnis statt von der lokalen Festplatte geladen wird – siehe auch Abschnitt 2.4.
Der Button MUSTER ZEICHNEN führt dazu, dass zehn Sekunden lang zufällige Rechtecke in einem PictureBox-Steuerelement gezeichnet werden. Damit können Sie testen, wie das Programm reagiert, wenn eine Ereignisprozedur längere Zeit dauert. Form1_Closed stellt sicher, dass das Programm nach dem Schließen aller Fenster tatsächlich beendet wird. Dazu wird das aktuelle Formular aus der myForms-Aufzählung entfernt. Wenn myForms keine anderen Elemente mehr enthält, wird Application.Exit ausgeführt. Public Class Form1 [... Vom Windows Form Designer generierter Code] ' neues Fenster öffnen Private Sub btnShow_Click(...) Handles btnShow.Click ShowWindow(New Form1()) End Sub ' alle Fenster durch Close schließen Private Sub btnClose_Click(...) Handles btnClose.Click Dim i As Integer For i = myForms.Count - 1 To 0 Step -1 CType(myForms(i), Form).Close() Next End Sub ' Programm durch Application.Exit gewaltsam beenden Private Sub btnEnd_Click(...) Handles btnEnd.Click Application.Exit() End Sub ' zehn Sekunden lang Zufallsmuster zeichnen Private Sub btnWork_Click(...) Handles btnWork.Click Dim endtime As Date = Now.AddSeconds(10) While Now < endtime [... Rechtecke zeichnen] End While End Sub ' Rückfrage vor dem Schließen des Fensters Private Sub Form1_Closing(...) Handles MyBase.Closing Me.BringToFront() If MsgBox("Soll das Fenster geschlossen werden?", _ MsgBoxStyle.Question Or MsgBoxStyle.YesNo, _ "Fenster schließen?") = MsgBoxResult.No Then e.Cancel = True End If End Sub
15.3 Verwaltung mehrerer Fenster
765
' das Fenster wurde geschlossen, eventuell Programmende Private Sub Form1_Closed(...) Handles MyBase.Closed myForms.Remove(Me) If myForms.Count = 0 Then Application.Exit() End Sub End Class
VORSICHT
Wenn Sie das obige Beispielprogramm als Basis für Ihre eigenen Programme verwenden möchten, müssen Sie drei Dinge beachten: • Die Programmausführung muss mit Module1.Main beginnen. • Alle Fenster müssen mit OpenWindow geöffnet werden. • Alle Fenster müssen die obige Closed-Ereignisprozedur aufweisen, die sicherstellt, dass das Programm beendet wird.
Mehrere gleichberechtigte Fenster in eigenen Threads öffnen (multi4) Ein Problem bleibt auch in multi3 noch bestehen: Die Fenster teilen sich eine gemeinsame Nachrichtenschleife: Während in einem Fenster eine Berechnung ausgeführt wird, sind alle anderen Fenster blockiert.
Abbildung 15.15: Jedes Fenster läuft in einem eigenen Thread (Beispiel multi4)
Die Variante multi4 löst auch dieses Problem. Eine neue Variante von ShowWindow führt nun Application.Run(frm) für jedes Fenster in einem eigenen Thread aus. Damit handelt es sich nun um eine echte Multithreading-Anwendung, bei der jedes Fenster in einem eigenen Thread läuft und eine eigene Nachrichtenschleife besitzt. Während im einen Fenster Rechtecke gezeichnet werden, können alle anderen Fenster uneingeschränkt bedient werden. ShowWindow erzeugt einerseits mit winThread ein neues Thread-Objekt, andererseits wird wie bisher das neue Formular in die myForms-Aufzählung eingefügt. Anschließend wird mit winThread.Start der neue Thread gestartet. Dadurch wird (eventuell einige Millisekunden später) die Prozedur NewWindowThread ausgeführt. Da an diese Prozedur keine Parameter
766
15 Gestaltung von Benutzeroberflächen
übergeben werden können, muss newfrm als Zwischenspeicher für das Formularobjekt dienen. ' Beispiel benutzeroberflaeche\multi4 Module Module1 Public myForms As New Collections.ArrayList() Dim newfrm As Form Sub Main() ShowWindow(New Form1()) End Sub Public Sub ShowWindow(ByVal frm As Form) Dim winThread As New Threading.Thread(AddressOf NewWindowThread) newfrm = frm myForms.Add(frm) winThread.Name = "winthread, started = " + Now.ToLongTimeString winThread.Start() End Sub Private Sub NewWindowThread() Application.Run(newfrm) End Sub End Module
Am Code in Form1 müssen einige kosmetische Veränderungen durchgeführt werden. Eine betrifft die Schleife, um alle Fenster mit Close zu schließen. Da alle Threads gleichzeitig auf myForms zugreifen können, kann es passieren, dass sich der Inhalt von myForms während der Ausführung der Schleife verändert. Deswegen wird der Inhalt in ein lokales Feld kopiert, anschließend werden alle Fenster – abgesichert durch Try-Catch – geschlossen. Public Class Form1 [... Vom Windows Form Designer generierter Code] Private Sub btnClose_Click(...) Handles btnClose.Click Dim i As Integer Dim frmCount As Integer = myForms.Count Dim frms(frmCount - 1) As Object myForms.CopyTo(frms, 0) For i = 0 To frmCount - 1 Try CType(frms(i), Form).Close() Catch End Try Next ' sicher ist sicher Application.Exit() End Sub End Class
15.3 Verwaltung mehrerer Fenster
767
Application.Exit hat trotz des Multithreading-Ansatzes dieselbe Wirkung wie bisher: Es
HINWEIS
beendet das Programm, weil es alle Fenster des laufenden Prozesses schließt. Wenn Sie nur den laufenden Thread beenden möchten, können Sie Application.ExitThread ausführen. (Im Beispielprogramm multi4 können Sie beide Varianten ausprobieren.) Die Methoden Exit und ExitThread erfordern besondere Ausführungsrechte, die beispielsweise nicht gegeben sind, wenn das .NET-Programm von einem Netzwerkverzeichnis statt von der lokalen Festplatte geladen wird – siehe auch Abschnitt 2.4.
Verhalten bei einem nicht behandelten Fehler: Wenn in einem Fenster ein nicht behandelter Fehler auftritt, wird nur der Thread dieses einen Fensters beendet. Die restlichen Fenster laufen unbeeindruckt weiter. Das Problem besteht aber darin, dass myForms nun auf ein Fenster verweist, dass es gar nicht mehr gibt. Das kann in der Folge alle möglichen Probleme auslösen. (btnClose_Click wurde auch im Hinblick auf dieses Szenario so wasserdicht wie möglich formuliert.) Multithreading-Probleme: Wie in Abschnitt 15.2.7 bereits beschrieben wurde, ist die Multithreading-Programmierung von Windows.Forms-Anwendungen eine ziemlich diffizile Angelegenheit. Da nun jedes Fenster in einem eigenen Thread läuft, ist der direkte Zugriff auf die Daten anderer Fenster (also z.B. durch CType(myForms(0), Form1).TextBox1 = "xxx") problematisch. Es könnte sein, dass die Daten durch zwei Threads quasi gleichzeitig verändert werden. Insofern ist die Zeile CType(myForms(0), Form).Close() eigentlich nicht zulässig. Probleme können auch beim Zugriff auf myForms auftreten. Für diese Probleme gibt es zwei Lösungsansätze: •
Synchronisieren Sie die Kommunikation zwischen den Fenstern (beispielsweise durch die Verwendung von Invoke). Der Aufwand hierfür ist aber beträchtlich.
•
Verzichten Sie auf myForms und auf jegliche Kommunikation zwischen den Fenstern. (Das ist allerdings nur bei wenigen Anwendungen möglich. Um noch einmal auf den Internet Explorer zurückzukommen, der ja gewissermaßen als Vorbild für den hier präsentierten Ansatz dient: Dort sind die Fenster zwar an sich unabhängig voneinander, aber wenn Sie in einem Fenster ein Lesezeichen einfügen, erscheint es wenig später auch in allen anderen Fenstern.)
Bevor Sie sich also auf den hier beschriebenen Multithreading-Ansatz einlassen, sollten Sie sich fragen, ob sich der Aufwand wirklich lohnt. In vielen Fällen erreichen Sie durch ein gelegentliches Ausführen von Application.DoEvents in Prozeduren, deren Ausführung längere Zeit beansprucht, einen ähnlichen Effekt, vermeiden aber alle Threading-Probleme. Ein einfaches DoEvents-Beispiel finden Sie in Abschnitt 15.2.1.
Fenster klonen Wenn Sie bei den Beispielprogrammen multi2 bis -4 den Button NEUES FENSTER anklicken, wird tatsächlich ein neues Fenster erzeugt. Alle Steuerelemente befinden sich im Startbzw. Initialisierungszustand.
768
15 Gestaltung von Benutzeroberflächen
Ganz anders verhält sich der Internet Explorer, wenn Sie Strg+N drücken: Im neuen Fenster erscheint dieselbe Seite wie im Ausgangsfenster, und auch alle anderen Layouteinstellungen werden übernommen. Man könnte also sagen, das Fenster wurde geklont. .NET sieht zwar bei manchen Klassen die Methode Clone vor, um die Kopie eines Objekts zu erzeugen, nicht aber für die Form-Klasse. frm = Me.Clone() ist daher unmöglich. Sie können aber natürlich in der Prozedur, in der Sie ein neues Fenster öffnen, möglichst viele Steuerelementeigenschaften vom ursprünglichen Fenster in das neue Fenster übernehmen, wie die folgenden, beispielhaften Zeilen zeigen. Dim frm As New Form1() frm.TextBox1.Text = Me.TextBox1.Text frm.TextBox1.SelectionStart = Me.TextBox1.SelectionStart ... frm.Show()
15.4
MDI-Anwendungen
MDI steht für Multiple Document Interface und bedeutet, dass in einem meist bildschirmfüllenden Hauptfenster mehrere Dokumentfenster angezeigt werden. Diese Art der Benutzeroberfläche ist ziemlich alt und beispielsweise bei allen älteren Versionen von Microsoft Office vorzufinden. (Für das Gegenstück zu MDI gibt es die seltener verwendete Abkürzung SDI für Single Document Interface.)
15.4.1 Programmiertechniken MDI-Anwendungen bestehen zumindest aus einem Hauptfenster und einem oder mehreren Sub- oder Dokumentfenstern (siehe Abbildung 15.16). Das Hauptfenster ist in der Regel mit einem zentralen Menü ausgestattet, dessen Kommandos für alle Subfenster gelten.
Abbildung 15.16: Ein einfaches MDI-Beispielprogramm
15.4 MDI-Anwendungen
769
Hauptfenster Das Hauptfenster ist das Fenster, mit dem das Programm startet. Der einzig wesentliche Unterschied besteht darin, dass die Eigenschaft IsMdiContainer auf True gestellt werden muss. (Damit ändert sich auch die Hintergrundfarbe des Fensters: Als Farbe wird nun SystemColors.AppWorkspace verwendet. Diese Farbe kann nur durch die Systemeinstellung geändert werden.) In das Hauptfenster dürfen zwar Steuerelemente eingefügt werden, diese müssen aber an einem der vier Ränder angedockt werden. Im Regelfall enthält das Hauptfenster nur ein Menü und eventuell Symbol- und Statusleisten. (Nicht angedockte Steuerelemente sind prinzipiell auch zulässig, sie werden aber über allen Subfenstern angezeigt und stören damit erheblich.) Das Hauptfenster sollte zumindest die Möglichkeit bieten, neue Fenster zu erzeugen. Üblicherweise erfolgt das über das Hauptmenü, denkbar wäre aber auch die Verwendung eines Kontextmenüs oder einer Symbolleiste. Beachten Sie, dass das Hauptfenster wegen IsMdiContainer=True keine Mausereignisse (MouseDown, Click etc.) mehr empfängt.
Subfenster Die Basis für das Subfenster ist ein zweites Formular, das mit PROJEKT|WINDOWS FORMS HINZUFÜGEN in das aktuelle Projekt eingefügt wird. Im Regelfall enthält dieses Fenster nur ein einziges Steuerelement (z.B. ein Textfeld oder ein Bildfeld) mit Dock=Fill, so dass es den gesamten Fensterinhalt füllt. Grundsätzlich können Sie das MDI-Subfenster aber vollkommen frei gestalten. Um ein Subfenster in das Hauptfenster einzufügen, führen Sie in der entsprechenden Ereignisprozedur (z.B. zum Menükommando DATEI|NEU) die folgenden Zeilen aus. Damit erzeugen Sie ein neues Objekt der Klasse Form2. (Form2 ist der Name der MDI-Subfensterklasse.) Entscheidend ist die Einstellung von MdiParent, die mit Me auf das Hauptfenster verweist. Dim frm As New Form2() frm.MdiParent = Me frm.Text = "Dokumentfenster " + i.ToString frm.Show()
Wenn eines der Subfenster über den Rand des Hauptfensters hinausreicht, werden darin automatisch Scroll-Balken angezeigt.
Verwaltung der Subfenster Das Form-Objekt des Hauptfensters kennt einige Methoden und Eigenschaften, die bei der Verwaltung der Subfenster helfen:
770
15 Gestaltung von Benutzeroberflächen
Form-Eigenschaften, -Methoden und -Ereignisse ActiveMdiChild
verweist auf das aktive MDI-Subfenster. Die Eigenschaft enthält Nothing, wenn noch kein MDI-Fenster geöffnet wurde.
ActiveMdiChild.ActiveControl
verweist auf das Steuerelement mit dem Eingabefokus innerhalb des Subfensters.
MdiChildren
verweist auf ein Feld mit den Form-Objekten aller Subfenster.
ActivateMdiChild (Methode)
aktiviert das als Parameter angegebene Subfenster.
MdiChildActivate (Ereignis)
tritt auf, wenn sich das aktive Subfenster ändert (z.B. wenn ein anderes Subfenster angeklickt wird oder wenn ein Subfenster geschlossen wird). Das Ereignis tritt auch dann auf, wenn das letzte Subfenster geschlossen wird. ActiveMdiChild enthält dann Nothing.
Als Alternative zur MdiChildren-Aufzählung können Sie natürlich auch eine eigene Aufzählung verwalten, die Sie jedes Mal, wenn ein Subfenster geöffnet oder geschlossen wird, entsprechend ändern. Zugriff auf Subfensterdaten: ActiveMdiChild bzw. MdiChildren liefern Form-Objekte zurück. Wenn Sie diese mit CType in den Datentyp des Subfensters umwandeln, können Sie auf alle Steuerelemente und auf alle Klassenvariablen zugreifen, die mit Friend oder Public deklariert wurden. Die folgenden Zeilen befinden sich im Klassenmodul des Hauptfensters und setzen voraus, dass alle Subfenster Objekte der Klasse Form2 sind und dass Form2 ein Textfeld TextBox1 und eine öffentliche Integer-Variable myvar1 enthält. Dim frm As Form2 If Not IsNothing(Me.ActiveMdiChild) Then frm = CType(Me.ActiveMdiChild, Form2) frm.TextBox1.Text = "abc" frm.myvar1 = 3 End If
Zugriff auf das MDI-Hauptfenster: Code, der in Subfenstern ausgeführt wird, kann mit der bereits erwähnten Eigenschaft MdiParent auf das Hauptfenster zurückverweisen. Über diesen Umweg können Sie von einem Subfenster auf andere Subfenster zugreifen.
Menüverwaltung bei MDI-Anwendungen Der Entwurf und die Verwaltung von Menüs wird in Abschnitt 15.6 ausführlich beschrieben. Dort werden auch die Sonderfälle im Zusammenhang mit MDI-Anwendungen behandelt: •
Bei MDI-Anwendungen werden die Menüs des Haupt- und des gerade aktiven Subfensters kombiniert und im Hauptfenster angezeigt.
15.4 MDI-Anwendungen
•
771
Das so genannte Fenstermenü zur Auswahl des aktiven MDI-Subfensters wird einfach dadurch erzeugt, dass Sie beim Menüeintrag MdiList=True angeben.
Programmende
HINWEIS
Das Programmende wird wie bei gewöhnlichen Programmen dadurch eingeleitet, dass für das Hauptfenster Me.Close ausgeführt wird bzw. der X-Button dieses Fensters angeklickt wird. In der Folge kommt es zuerst zum Aufruf der Closing-Ereignisprozedur für alle Subfenster. Anschließend wird auch die Closing-Prozedur des Hauptfensters ausgeführt. Wenn auch nur in einer einzigen dieser Prozeduren e.Cancel=True ausgeführt wird, stoppt der Ereignisfluss und das Programm wird fortgesetzt. Ein sofortiges Programmende können Sie durch Application.Exit erreichen (nur, wenn Ihr Programm in der höchten Sicherheitsstufe ausgeführt wird, siehe auch Abschnitt 2.4.) Etwas merkwürdig ist die Reaktion auf End. Wenn Sie dieses Kommando in einer Ereignisprozedur eines Subfensters ausführen, wird das Programm sofort beendet, ohne dass eine Closing-Prozedur ausgeführt wird. Wird End dagegen in einer Ereignisprozedur des Hauptfensters ausgeführt, entspricht das Me.Close, d.h., es werden die diversen Closing-Prozeduren ausgeführt, in denen das Programmende eventuell noch verhindert werden kann. Beachten Sie auch, dass End wie Application.Exit nur in der höchsten .NET-Sicherheitsstufe ausgeführt werden kann. Fazit: Vermeiden Sie End!
Beispielprogramm Das in Abbildung 15.16 dargestellte Beispielprogramm erfüllt keine konkrete Aufgabe, sondern zeigt lediglich die Struktur einer einfachen MDI-Anwendung. Mit DATEI|NEU können neue Fenster geöffnet, mit DATEI|SCHLIEßEN wieder geschlossen werden. Das FENSTER-Menü ermöglicht den Fensterwechsel und die Neuanordnung der Fenster. Die Subfenster (Form2) bestehen aus einem Textfeld, das dank Dock=Fill das gesamte Fenster füllt. Der gesamte Code mit befindet sich in den Ereignisprozeduren des Hauptfensters (Form1) und sollte auf Anhieb verständlich sein. ' Beispiel benutzeroberflaeche\mdi-intro ' neues Fenster öffnen Private Sub MenuItemFileNew_Click(...) Handles MenuItemFileNew.Click Dim frm As New Form2() Static i As Integer = 1 frm.MdiParent = Me frm.Text = "Dokumentfenster " + i.ToString frm.Show() i += 1 End Sub
772
15 Gestaltung von Benutzeroberflächen
' Fensterlayout ändern Private Sub MenuItemWindowCascade_Click(...) Handles _ MenuItemWindowCascade.Click Me.LayoutMdi(MdiLayout.Cascade) End Sub Private Sub MenuItemWindowHorizontal_Click(...) Handles ... Me.LayoutMdi(MdiLayout.TileHorizontal) End Sub Private Sub MenuItemWindowVertical_Click(...) Handles... Me.LayoutMdi(MdiLayout.TileVertical) End Sub ' Subfenster schließen Private Sub MenuItemFileClose_Click(...) Handles ... ' wenn kein MDI-Fenster geöffnet ist, dann verweist ' ActiveForm auf das Hauptfenster If Not IsNothing(Me.ActiveMdiChild) Then Me.ActiveMdiChild.Close() End If End Sub
15.4.2 MDI-Fenster andocken In den vergangen Jahren sind MDI-Anwendungen seltener geworden. Bei aktuellen Microsoft-Office-Komponenten wird jedes Dokument in einem eigenen (Haupt-)Fenster geöffnet, und selbst der Webbrowser Opera, dessen MDI-Oberfläche gleichsam ein Markenzeichen war, ist in seiner sechsten Version vom MDI-Konzept abgekommen. Mit der VS.NET-Entwicklungsumgebung könnte MDI allerdings in einer neuen Form wieder modern werden: Bei der VS.NET-Benutzeroberfläche werden alle Fenster innerhalb eines großen Hauptfensters angezeigt. Der wesentliche Unterschied zu klassischen MDIProgrammen besteht aber darin, dass per Default alle Fenster irgendwo angedockt sind. Somit stellt sich die nahe liegende Frage: Können MDI-Fenster auch bei eigenen Programmen angedockt werden? Die Antwort ist ein Ja, aber: Grundsätzlich steht die Dock-Eigenschaft tatsächlich auch für Fenster zur Verfügung, wobei ihre Einstellung aber nur bei MDI-Subfenstern eine sichtbare Wirkung zeigt: Das Fenster wird tatsächlich am jeweiligen Rand platziert und nimmt automatisch die ganze Breite oder Höhe des MDI-Hauptfensters an. Aber was mit einem Subfenster noch recht gut funktioniert, kommt bei mehreren Fenstern rasch aus dem Gleichgewicht: Die Größe von Fenstern ändert sich unvorhergesehen beim Anklicken eines anderen Fensters, es gibt Probleme mit der Verankerung von Steuerelementen innerhalb der MDI-Subfenster etc. Wenn Sie diese einfachste Form von MDI-Docking ausprobieren möchten, starten Sie einfach das Beispielprogramm benutzeroberflaeche\mdi-docking1. Im Hauptfenster gibt es einen Button zum Einfügen neuer Subfenster. Jedes Subfenster ist mit fünf Buttons aus-
15.4 MDI-Anwendungen
773
gestattet, die ein Andocken an einer der vier Seiten bzw. ein Loslösen ermöglichen (siehe Abbildung 15.17). Beim Andocken bzw. Loslösen werden auch die Fenstereigenschaften TopMost und FormBorderStyle verändert.
Abbildung 15.17: Erste MDI-Docking-Versuche
Auf einen vollständigen Abdruck des Codes wird aus Platzgründen verzichtet. Die folgenden zwei Prozeduren sollten klar machen, wie das Andocken funktioniert. ' Beispiel benutzeroberflaeche\mdi-docking1 ' auf einer Seite andocken Private Sub btnTop_Click(...) Handles btnTop.Click Me.Dock = DockStyle.Top Me.FormBorderStyle = FormBorderStyle.SizableToolWindow Me.TopMost = False End Sub 'un-dock Private Sub btnUndock_Click(...) Handles btnUndock.Click Me.Dock = DockStyle.None Me.TopMost = True Me.FormBorderStyle = FormBorderStyle.Sizable End Sub
Fenster in Panel- und TabControl-Steuerelementen anzeigen Wenn Sie das oben vorgestellte Programm ausprobieren, werden Sie rasch zu der Erkenntnis kommen, dass die Dock-Eigenschaft der Subfenster in dieser Form nicht ausreicht, um eine brauchbare Benutzeroberfläche zu gestalten. Mit etwas Experimentierfreude kann man aber feststellen, dass es möglich ist, ein normales Fenster innerhalb eines Panel-Steuer-
774
15 Gestaltung von Benutzeroberflächen
elements anzuzeigen. Das Fenster ist dort sogar verschiebbar. Die einzige Voraussetzung besteht darin, dass die Eigenschaft TopLevel vorher auf False gestellt wird. Abbildung 15.18 zeigt ein Beispielprogramm, bei dem Sie über das TEST-Menü neue Subfenster wahlweise in ein TabControl-Steuerelement oder in den daneben befindlichen Client-Bereich des Hauptfensters einfügen können. (Dabei handelt es sich um ein PanelSteuerelement mit BackColor=SystemColors.AppWorkspace und AutoScroll=True.) Jedes Subfenster enthält einen Button, mit dem Sie das Fenster zwischen dem TabControl und dem ClientBereich hin und her bewegen können. Die Größe des TabControl-Bereichs ist durch ein Splitter-Steuerelement variabel. Auch wenn das Beispielprogramm auf den ersten Blick recht eindrucksvoll aussieht, ist es dennoch eher eine Konzeptstudie denn eine real anwendbare Lösung. Ein Problem besteht z.B. darin, dass das Anklicken der Fensterleiste im Client-Bereich das Fenster zwar in den Vordergrund bringt, den Tastaturfokus aber in dem Fenster belässt, das ihn gerade hat. Ein Wechsel des aktiven Fensters mit Strg+Tab ist ebenfalls nicht möglich.
HINWEIS
Auch wenn dieses Beispielprogramm im MDI-Abschnitt beschrieben wird, handelt es sich hier nur im Erscheinungsbild um eine MDI-Anwendung. Intern gilt im Hauptfenster IsMdiContainer=False! Bei den Subfenstern wird MdiParent nicht verwendet. Das hat natürlich zur Folge, dass diverse MDI-Annehmlichkeiten wie das MDIFenstermenü oder die LayoutMdi-Methode nicht zur Verfügung stehen und gegebenenfalls nachprogrammiert werden müssen. Auch die Optik entspricht nicht ganz dem echter MDI-Anwendungen. Beispielsweise wird die Titelzeile des aktiven Fensters in einer anderen Farbe angezeigt; bei der Maximierung bleibt die Titelzeile sichtbar (statt in die Titelzeile des Hauptfensters eingebettet zu werden) etc. Ich habe natürlich zuerst versucht, ein vergleichbares Programm auf MDI-Basis zu entwickeln, es ist mir aber nicht gelungen, MDI-Fenster in das TabControl und wieder zurück zu bewegen, ohne dabei die interne MDI-Verwaltung durcheinander zu bringen.
Abbildung 15.18: Fenster ähnlich wie in der VS.NET-Entwicklungsumgebung andocken
15.4 MDI-Anwendungen
775
Der Code zu Form1 (dem Hauptformular) enthält zwei Ereignisprozeduren, die über das Menü aufgerufen werden. Beide erzeugen ein neues Fenster, einmal im Panel, einmal im TabControl-Steuerelement. Das Einfügen des neuen Fensters in die Controls-Aufzählung durch Add funktioniert nur, wenn vorher TopLevel=False ausgeführt wird. Einige Fenstereigenschaften werden je nach Container unterschiedlich eingestellt: So gelten bei den Fenstern im TabControl die Einstellungen FormBorderStyle = None und Dock = Fill. ' Beispiel benutzeroberflaeche\mdi-docking2\form1 Dim windowNr As Integer = 1 Private Sub MenuItemNewMDI_Click(...) Handles MenuItemNewMDI.Click Dim frm As New Form2() ' Formulareigenschaften einstellen frm.TopLevel = False frm.Text = "Fenster " + windowNr.ToString frm.TextBox1.Text = frm.Text frm.Button1.Text = "--> TabControl" frm.Left = (windowNr - 1) * 20 frm.Top = (windowNr - 1) * 20 ' Formular in Panel einfügen Panel1.Controls.Add(frm) ' Fenster anzeigen frm.BringToFront() frm.Show() windowNr += 1 End Sub Private Sub MenuItemNewMDITab_Click(...) Handles ... Dim frm As New Form2() Dim tb As New TabPage() ' Formulareigenschaften einstellen frm.TopLevel = False frm.FormBorderStyle = FormBorderStyle.None frm.Text = "Fenster " + windowNr.ToString frm.TextBox1.Text = frm.Text frm.Button1.Text = "--> MDI" frm.Dock = DockStyle.Fill ' Formular in TabPage einfügen tb.Controls.Add(frm) tb.Text = frm.Text ' TabPage in TabControl einfügen TabControl1.TabPages.Add(tb) TabControl1.SelectedTab = tb ' Fenster anzeigen frm.Show() windowNr += 1 End Sub
776
15 Gestaltung von Benutzeroberflächen
Form2 enthält in erster Linie die Ereignisprozedur für den Button, um das Formular
zwischen den beiden Client-Bereichen hin und her zu bewegen. Das geht erstaunlich einfach: Sie brauchen das Formular nur mit Controls.Add(Me) in den neuen Container einzufügen. Der restliche Code dient dazu, diverse Fenstereigenschaften neu einzustellen, ein neues TabPage-Element zu erzeugen bzw. zu löschen, das neue Fenster anschließend in den Vordergrund zu rücken etc. ' Beispiel benutzeroberflaeche\mdi-docking2\form1 Private Sub Button1_Click(...) Handles Button1.Click Dim mainfrm As Form1 Dim tb As TabPage mainfrm = CType(Me.ParentForm, Form1) If (TypeOf Me.Parent Is TabPage) Then ' Fenster vom TabControl in das Panel verschieben tb = CType(Me.Parent, TabPage) ' TabPage entfernen mainfrm.TabControl1.TabPages.Remove(tb) ' Fenstereigenschaften neu einstellen Me.FormBorderStyle = FormBorderStyle.Sizable Me.Dock = DockStyle.None Me.Button1.Text = "--> TabControl" mainfrm.Panel1.Controls.Add(Me) Me.BringToFront() Me.Focus() Else ' Fenster vom Panel in das TabControl bewegen tb = New TabPage() Me.FormBorderStyle = FormBorderStyle.None Me.Dock = DockStyle.Fill Me.Button1.Text = "--> MDI" tb.Controls.Add(Me) tb.Text = Me.Text ' TabPage in TabControl einfügen mainfrm.TabControl1.TabPages.Add(tb) mainfrm.TabControl1.SelectedTab = tb End If End Sub
Die Prozedur Form2_ActivateClickEnter für die Ereignisse Activated, Click und Enter bewirkt, dass das angeklickte Fenster in den Vordergrund kommt und den Tastaturfokus erhält. (Wie bereits erwähnt, funktioniert das nicht, wenn der Fenstertitel oder -rahmen angeklickt wird. Die Ereignisprozedur wird leider nur ausgeführt, wenn das Fensterinnere angeklickt wird.)
15.5 Standarddialoge
777
' Fenster beim Anklicken in den Vordergrund bringen Private Sub Form2_ActivateClickEnter(...) Handles MyBase.Activated, _ MyBase.Click, MyBase.Enter Me.BringToFront() Me.TextBox1.Focus() End Sub
VERWEIS
Auch mit viel Aufwand wird es Ihnen nicht gelingen, aus diesem Prototyp eine Benutzeroberfläche wie die der VS.NET-Entwicklungsumgebung zu programmieren. Es ist aber zu hoffen, dass Microsoft künftige .NET-Versionen in dieser Hinsicht ergänzt. Bis dahin können Sie auf die kostenlose Magic-Bibliothek zurückgreifen, die unter anderem einen Docking-Manager enthält, der weit mehr Funktionen als das hier zweckentfremdete TabControl-Steuerelement enthält. (Die Dokumentation zu dieser Bibliothek geht davon aus, dass Sie C# verwenden, aber natürlich können Sie die Bibliothek ebenso gut in einem VB.NET-Programm einsetzen.) Daneben gibt es auch kommerzielle Docking-Lösungen, beispielsweise die von Actipro Software oder von SyncFusion. http://www.dotnetmagic.com/ http://www.actiprosoftware.com/ http://www.syncfusion.com/
15.5
Standarddialoge
Um Ihnen unnötige Arbeit beim Entwurf immer wieder benötigter Dialoge zu ersparen, gibt es unter Windows einige vordefinierte und standardisierte Dialoge für häufig wiederkehrende Aufgaben – etwas zur Anzeige eines Nachrichtentexts, zur Auswahl einer Datei oder eines Druckers etc. Dieser Abschnitt gibt einen Überblick über die zur Auswahl stehenden Dialoge.
15.5.1 Einfache Dialogboxen (MessageBox, MsgBox) Der .NET-konforme Weg zur Darstellung einer einfachen Dialogbox basiert auf der in Windows.Forms definierten Klasse MessageBox. Der Dialog wird mit Show angezeigt. Die folgenden Zeilen machen die prinzipielle Syntax klar. (Alle Parameter der Show-Methode außer dem ersten sind optional.) Dim result As DialogResult result = MessageBox.Show("Das ist der Nachrichtentext.", _ "Fensterüberschrift", MessageBoxButtons.OKCancel, _ MessageBoxIcon.Question, MessageBoxDefaultButton.Button2)
778
15 Gestaltung von Benutzeroberflächen
•
Der erste Parameter gibt den Nachrichtentext an, der mit vbCrLf in mehrere Zeilen zerlegt werden kann. (Ansonsten wird bei zu langen Zeilen ein automatischer Umbruch durchgeführt.)
•
Der zweite Parameter gibt die Fensterüberschrift des Dialogs an. (Per Default wird kein Titel angezeigt.)
•
Der dritte Parameter gibt an, welche Buttons angezeigt werden (mit der Einstellung MessageBoxButtons.OKCancel z.B. OK und ABBRUCH). Per Default wird nur der OK-Button angezeigt.
•
Der vierte Parameter gibt an, ob links neben dem Nachrichtentext ein Icon (z.B. ein Ausrufezeichen, Fragezeichen etc.) angezeigt werden soll.
•
Der fünfte Parameter gibt an, welcher der (maximal drei) Buttons als Default-Button gelten soll und einfach durch Return ausgewählt werden kann.
Falls mehrere Buttons angezeigt wurden, kann mit dem Rückgabewert ermittelt werden, welcher Button gedrückt worden ist.
Abbildung 15.19: MessageBox, MsgBox und InputBox
MsgBox: Wer schon mit früheren VB-Versionen gearbeitet hat, wird wahrscheinlich aus purer Gewohnheit weiterhin MsgBox statt MessageBox.Show schreiben – schon alleine deswegen, weil der Tippaufwand ein bisschen geringer ist. MsgBox ist in der VB.NET-Runtime-Bibliothek definiert (Namensraum Microsoft.VisualBasic.Interaction). Beachten Sie, dass die Parameter aus Kompatibilitätsgründen eine andere Reihenfolge als bei MessageBox.Show haben. (Beim Kompilieren wird MsgBox übrigens durch MessageBox.Show ersetzt, wobei die
Parameter entsprechend angepasst werden.) Dim result As MsgBoxResult result = MsgBox("Das ist der Nachrichtentext.", _ MsgBoxStyle.AbortRetryIgnore Or MsgBoxStyle.DefaultButton2 Or _ MsgBoxStyle.Question, "Fensterüberschrift")
15.5 Standarddialoge
779
Auch InputBox ist Teil der VB.NET-Runtime-Bibliothek. Diese Methode ermöglicht eine einfache Eingabe von kurzen Texten. Die Methode liefert die eingegebene Zeichenkette zurück (oder "", wenn der ABBRUCH-Button angeklickt wurde). Dim s As String s = InputBox("Geben Sie bitte Ihren Namen ein!", "Fensterüberschrift")
15.5.2 Standarddialoge (OpenFileDialog, ColorDialog) Außer MessageBox enthält der Windows.Forms-Namensraum eine Reihe weiterer Standarddialoge. Die folgende Box hilft bei der Einordnung der Steuerelemente in der Klassenhierarchie und enthält Querverweise auf die Abschnitte, in denen die Dialoge beschrieben sind. (Beispielsweise wird der OpenFileDialog im Kapitel zum Umgang mit Dateien und Verzeichnissen beschrieben.) Klassenhierarchie im System.Windows.Forms-Namensraum ....
diverse andere Klassen Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Standarddialoge Farbe auswählen (siehe Abschnitt 16.2.2) Basisklasse zur Dateiauswahl vorhandene Datei auswählen (siehe Abschnitt 10.4) Datei speichern (siehe Abschnitt 10.4) Seitenlayout einstellen (siehe Abschnitt 17.2.2) Drucker auswählen (siehe Abschnitt 17.2.2)
....
diverse andere Klassen Klasse für Fenster bzw. Formulare Seitenvorschau anzeigen (siehe Abschnitt 17.2.3)
└─ Component │ └─ CommonDialog ├─ ColorDialog ├─ FileDialog │ ├─ OpenFileDialog │ └─ SaveFileDialog ├─ PageSetupDialog └─ PrintDialog └─ Form └─ PrintPreviewDialog
Obwohl alle Dialoge mit der Ausnahme von PrintPreviewDialog von der CommonDialogKlasse abgeleitet sind, ist der gemeinsame Nenner der Dialoge recht klein. Es gibt zahllose Eigenschaften und Methoden, die spezifisch für die jeweilige Anwendung sind. Alle Dialoge werden aber auf die selbe Art und Weise eingesetzt: Zuerst müssen Sie entweder das entsprechende Steuerelement von der Toolbar in das Formular einfügen oder selbst ein neues Objekt der entsprechenden Klasse erzeugen. Vor dem Aufruf können Sie diverse Merkmale des Dialogs durch die Einstellung von AllowXxx- oder ShowXxx-Eigenschaften aktivieren oder deaktivieren. Der Dialog wird durch XxxDialog.ShowDialog() angezeigt. Dass der Dialog mit OK beendet wurde, erkennen Sie am Rückgabewert DialogResult.OK. In diesem Fall werten Sie die diversen Eigenschaften des Dialogobjekts aus (z.B. Color beim ColorDialog oder Font beim FontDialog).
780
15 Gestaltung von Benutzeroberflächen
VERWEIS
Dim myColDialog As New ColorDialog() ... Eigenschaften einstellen If myColDialog.ShowDialog() = DialogResult.OK Then ... Eigenschaften auswerten End If myColDialog.Dispose()
Aus unerfindlichen Gründen gibt es zwar zwei Dialoge zur Auswahl von Dateien, aber anscheinend keinen zur Auswahl eines Verzeichnisses. Abschnitt 10.4.2 zeigt, dass es einen derartigen Dialog (gut versteckt) doch gibt und wie er verwendet werden kann.
15.6
Menüs
15.6.1 Der Menüeditor Um ein Formular mit einem Menü auszustatten, fügen Sie einfach per Doppelklick in der Toolbox das Steuerelement MainMenu in Ihr Formular ein. Das noch leere Menü erscheint nun automatisch am oberen Ende des Formulars (siehe Abbildung 15.20). Nun können Sie einfach per Tastatur die einzelnen Menüelemente benennen. (Wer sich noch an den steinzeitlichen Menüeditor von VB6 erinnert, wird die Verbesserungen richtig zu schätzen wissen.) Bemerkenswert ist, dass die Eingabe direkt im Formular erfolgen darf, während bei allen anderen Steuerelementen die Text-Eigenschaft im Eigenschaftsfenster verändert werden muss. Alt-Tastaturabkürzungen werden wie bei Buttons mit dem Zeichen & definiert (d.h., die Eingabe &Datei führt zum Menüeintrag DATEI). Trennstriche zwischen Menügruppen erreichen Sie durch die Eingabe eines Minuszeichens. Menüeinträge können mit der Maus verschoben und mit Strg kopiert werden. Für komplexere Umbauarbeiten empfiehlt es sich, ganze Menüs mit den Kontextmenükommandos AUSSCHNEIDEN, KOPIEREN und EINFÜGEN zu verschieben. Das Kontextmenü bietet auch die Möglichkeit, zusätzliche Einträge einzufügen. Bevor Sie per Doppelklick Click-Ereignisprozeduren zu den einzelnen Menüeinträgen in den Code einfügen, sollten Sie den einzelnen Menüeinträgen unbedingt aussagekräftige Namen geben. Dazu können Sie wie üblich im Eigenschaftsfenster die Name-Eigenschaft verändern. Viel effizienter ist es aber, den Menüeditor per Kontextmenü in den Modus NAMEN BEARBEITEN umzuschalten: Nun können Sie die internen Namen der einzelnen Menüeinträge bequem im Menüeditor verändern (siehe Abbildung 15.21). Alle anderen Eigenschaften der Menüeinträge müssen im Eigenschaftsfenster eingestellt werden. Wenn Sie bei mehreren Menüeinträgen dieselbe Eigenschaft verändern möchten, können Sie mehrere Einträge einer Hierarchiegruppe gemeinsam markieren (mit Strg oder mit Shift).
15.6 Menüs
781
Abbildung 15.20: Interaktiver Menüentwurf
VORSICHT
Abbildung 15.21: Name-Eigenschaften direkt im Editor verändern
So komfortabel diese Funktion des Menüeditors auf den ersten Blick aussieht, so unzuverlässig ist sie leider (zumindest in der ersten Version von VS.NET): Namensänderungen werden manchmal einfach nicht gespeichert. Vergewissern Sie sich also im Eigenschaftsfenster, ob die von Ihnen durchgeführten Änderungen tatsächlich ausgeführt wurden. Wenn das nicht der Fall ist, müssen Sie die Änderungen im Eigenschaftsfenster ausführen.
782
15 Gestaltung von Benutzeroberflächen
15.6.2 Anwendung und Programmierung Menüklassen Intern wird das Hauptmenü durch ein MainMenu-Objekt und jeder einzelne Menüeintrag durch ein MenuItem-Objekt realisiert. Die Klassen MenuItem und MainMenu sind nicht von Control, sondern direkt von Component abgeleitet. Der folgende Hierarchiebaum hilft bei der Einordnung der Klassen. Da die Menu-Objekte keine richtigen Steuerelemente sind, fehlen viele von Steuerelementen bekannte Eigenschaften. Besonders ärgerlich ist dies bei Name und Tag: einzelne Menüeinträge können in einer gemeinsamen Ereignisprozedur nur durch sender Is xxx identifiziert werden, was nicht in jedem Fall optimal und beispielsweise für Select-Case-Konstrukte vollständig ungeeignet ist. (Natürlich können Sie eine neue Klasse von MenuItem ableiten und mit den entsprechenden Eigenschaften ausstatten. Dann können Sie aber den Menüeditor der Entwicklungsumgebung nicht mehr verwenden ...) Klassenhierarchie im System.Windows.Forms-Namensraum Object └─ MarshalByRefObject └─ Component
│ └─ Menu ├─ ContextMenu ├─ MainMenu └─ MenuItem
.NET-Basisklasse Objekt nur als Referenz weitergeben Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Menüs Verwaltung eines Kontextmenüs Verwaltung eines gewöhnlichen Menübaums ein einzelner Menüeintrag
Verhalten und Aussehen von Menüeinträgen Enabled und Visible geben wie bei Steuerelementen an, ob der Menüeintrag aktiv ist bzw. ob er angezeigt wird. Menüeinträge mit Enabled=False werden in grauer Schrift angezeigt
und können nicht verwendet werden. Mit ShortCut können Sie (zusätzlich zum Alt-Kürzel) eine weitere Tastaturabkürzung definieren. Das ist praktisch, wenn einzelne Menüeinträge mit Funktionstasten, Shift+Einf etc. ausgewählt werden können. Sie brauchen sich um die Auswertung dieser Funktionstasten dann nicht zu kümmern, die entsprechende Menüereignisprozedur wird automatisch aufgerufen. ShowShortCut gibt an, ob die Tastaturabkürzung auch im Menütext angezeigt wird (per Default True). Mit DefaultMenuItem=True können Sie einen Menüeintrag einer Gruppe als Defaulteintrag kennzeichnen. Der Menüeintrag wird dann in fetter Schrift dargestellt und kann durch einen Doppelklick auf den übergeordneten Eintrag ausgewählt werden. (Allerdings ist diese Funktion den wenigsten Benutzern bekannt und wird daher in der Praxis Verwirrung stiften. Insofern sollten Sie diese Eigenschaft eher mit Vorsicht einsetzen.)
15.6 Menüs
783
Durch Checked=True können Sie vor einem Menüeintrag ein Auswahlhäkchen anzeigen. (Checked wird durch eine Menüauswahl nicht automatisch verändert, sondern muss von Ihnen in der Click-Ereignisprozedur gesetzt bzw. gelöscht werden!) Wenn zusätzlich zu Checked auch RadioChecked auf True gesetzt wird, wird statt des Auswahlhäkchens ein gefüllter Optionskreis angezeigt. Diese Variante eignet sich dann, wenn der Anwender per Menü eine von mehreren Varianten auswählen kann. In diesem Fall müssen Sie in der Click-Ereignisprozedur die Checked-Eigenschaft des zuletzt auf diese Weise ausgewählten Eintrags zurücksetzen. Es gibt keine Möglichkeit, die Schriftart zur Darstellung der Menüeinträge zu verändern. Die Schriftart wird durch die Systemeinstellung vorgegeben und kann mit SystemInformation.MenuFont ermittelt werden.
Menüs verwalten Das MainMenu-Objekt verweist mit MenuItems auf alle MenuItem-Objekte des Hauptmenüs. Deren MenuItem-Objekte verweisen wiederum mit MenuItems auf Untermenüeinträge etc. (Ein Beispiel, wie alle Menüeinträge in einer rekursiven Schleife durchlaufen werden können, finden Sie bei der Beschreibung des Click-Ereignisses etwas weiter unten). Zur internen Verwaltung der Menüeinträge einer Menüebene dient die Menu.MenuItemCollection-Klasse. Die Klasse ist vor allem dann praktisch, wenn man Menüs dynamisch während des Programms verändern möchten (mit den Methoden Add und Remove). Dabei besteht aber das Problem, dass es unmöglich ist, mit den neu erzeugten MenuItem-Objekten irgendwelche Kontextinformationen zu speichern. Wie so oft, gibt es mehrere Lösungsansätze. Der einfachste (aber auch fehleranfälligste) besteht darin, die Menüeinträge in der Ereignisprozedur anhand der Text-Eigenschaft zu identifizieren. Eine zweite Variante besteht darin, die neu erzeugten MenuItem-Objekte als Schlüssel in einer Hashtable zu verwenden, um so auf kontextspezifische Daten (im folgenden Beispiel eine Integer-Zahl) zuzugreifen. Eleganter, aber auch aufwendiger ist es, eine neue Klasse zu definieren, die von MenuItem abgeleitet ist und zusätzliche Eigenschaften zur Speicherung von Daten enthält. Im folgende Beispiel erweitert Button1 das TEST-Menü (MenuItemTest) um einen weiteren Eintrag. Jeder Menüeintrag wird in menuhash gespeichert. Bei der Auswahl eines derartigen Eintrags wird menuhash ausgewertet, um die Nummer des Menüeintrags zu ermitteln. ' Beispiel benutzeroberflaeche\menu-test Dim menuhash As New Hashtable() Private Sub Button1_Click(...) Handles Button1.Click Static i As Integer = 1 Dim mi As MenuItem mi = MenuItemTest.MenuItems.Add("neuer Eintrag " + i.ToString) menuhash.Add(mi, i) AddHandler mi.Click, AddressOf NewMenuItems_Click i += 1 End Sub
784
15 Gestaltung von Benutzeroberflächen
Private Sub NewMenuItems_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim mi As MenuItem = CType(sender, MenuItem) MsgBox("Der neue Menüeintrag Nummer " + _ CType(menuhash(mi), Integer).ToString + _ " wurde ausgewählt.") End Sub
Statt mit MenuItems.Add und Remove können Sie Menüs auch mit den ein wenig exotischen Methoden CloneMenu und MergeMenu vervielfältigen und vereinen. Dabei geben die MenuItem-Eigenschaften MergeOrder und MergeType an, in welcher Reihenfolge zwei Menüs zu einem neuen Menü vereint werden.
Ereignisse Das wichtigste MenuItem-Ereignis ist zweifellos Click. Es tritt auf, nachdem ein Menüeintrag angeklickt oder per Tastatur ausgewählt wurde. Es tritt hingegen nicht auf, wenn ein Menüeintrag mit Untermenüs angeklickt wird. In diesem Fall tritt das Popup-Ereignis auf (siehe unten), anschließend wird das Untermenü angezeigt. Die Konzeption der MenuItem-Klasse sieht vor, dass jeder Menüeintrag mit einer eigenen Ereignisprozedur ausgestattet wird. Das führt bei umfangreichen Menüs allerdings zu einer riesigen Anzahl von Ereignisprozeduren. Wenn Sie alle Menüeinträge mit einer einheitlichen Ereignisprozedur ausstatten möchten, können Sie folgendermaßen vorgehen. ' Beispiel benutzeroberflaeche\menu-test Private Sub Form1_Load(...) Handles MyBase.Load AddHandlerForAllMenuItems(MainMenu1.MenuItems) End Sub Private Sub AddHandlerForAllMenuItems( _ ByVal mitems As Menu.MenuItemCollection) Dim mi As MenuItem For Each mi In mitems AddHandler mi.Click, AddressOf MenuItems_Click ' Prozedur rekursiv aufrufen AddHandlerForAllMenuItems(mi.MenuItems) Next End Sub ' Sammelprozedur für das Click-Ereignis aller Menüeinträge Private Sub MenuItems_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim mi As MenuItem = CType(sender, MenuItem) MsgBox("click Menüeintrag " + mi.Text) If mi Is MenuItemFileNew Then ' ...
15.6 Menüs
785
ElseIf mi Is MenuItemFileSave Then ' ... End If End Sub
Das zweite wichtige Ereignis ist Popup. Es tritt für Menüeinträge auf, bevor deren Untermenü erscheint. Das Popup-Ereignis bietet die Möglichkeit, ein Menü vor dem Erscheinen dynamisch zu verändern. Beispielsweise können Sie vor dem Erscheinen des BEARBEITENMenüs testen, ob die Zwischenablage Daten enthält, die Ihr Programm verarbeiten kann. Wenn das der Fall ist, setzen Sie Enabled des Menüeintrags EINFÜGEN auf True, sonst auf False. Private Sub MenuItemEdit_Popup(...) Handles MenuItemEdit.Popup If Clipboard.GetDataObject().GetDataPresent(DataFormats.Text) Then MenuItemEditPaste.Enabled = True Else MenuItemEditPaste.Enabled = False End If End Sub
Client-Bereich des Fensters Durch eine Menüleiste verkleinert sich der Bereich des Fensters, der zum Einfügen von Steuerelementen oder zum Durchführen von Grafikausgaben vorgesehen ist. Der Koordinatenpunkt (0,0) beschreibt daher wie gewohnt den ersten sichtbaren Punkt unterhalb der Menüleiste. Me.ClientSize gibt die Größe des Client-Bereichs ohne Menüleiste an.
VERWEIS
15.6.3 Menüs bei MDI-Anwendungen MDI-Anwendungen sind Programme, bei denen mehrere Subfenster in einem Hauptfenster angezeigt werden. Die Grundlagen derartiger Programme werden in Abschnitt 15.4 beschrieben. Ein MDI-Beispielprogramm inklusive eines einfachen Menüs finden Sie auf der beiliegenden CD im Verzeichnis benutzeroberflaeche\mdiintro.
Fenstermenü: Ein Kennzeichen beinahe aller MDI-Anwendungen ist das so genannte Fenstermenü. Das ist ein Menü, das die Namen aller Subfenster enthält (siehe Abbildung 15.22). Mit dem Menü kann eines der Fenster aktiviert werden. Im Regelfall enthält das Menü auch Kommandos, um die Fensteranordnung zu ändern (also um die Fenster beispielsweise überlappend anzuordnen). Die Umsetzung eines derartigen Menüs ist einfach: Sie fügen im Hauptfenster ein neues Menü FENSTER ein und stellen dessen Eigenschaft MdiList auf True. Damit erreichen Sie, dass die Namen aller MDI-Fenster automatisch als Einträge dieses Menüs angezeigt wer-
786
15 Gestaltung von Benutzeroberflächen
den. Das gerade aktive Fenster wird durch ein Auswahlhäkchen gekennzeichnet. Durch die Auswahl eines dieser Menüeinträge verändert sich das aktive Fenster.
Abbildung 15.22: MDI-Fenstermenü
Darüber hinaus können Sie dem Fenstermenü noch einige eigene Einträge hinzufügen, um das Fensterlayout zu ändern. In den dazugehörenden Ereignisprozeduren führen Sie dann die LayoutMdi-Methode aus, z.B.: Me.LayoutMdi(MdiLayout.Cascade)
Menüeinträge der Subfenster: Nicht nur das Hauptfenster, auch die Subfenster können mit Menüs ausgestattet werden. Diese Menüs werden allerdings im Hauptfenster angezeigt, und zwar nach den Menüeinträgen des Hauptfensters. Wenn das Hauptfenster die Menüs DATEI und FENSTER enthält und das Subfenster die Menüs BEARBEITEN und HILFE, dann gilt im resultierenden Gesamtmenü die eher unübliche Reihenfolge DATEI, FENSTER, BEARBEITEN und HILFE. (Per Code können Sie auf das Gesamtmenü über die Eigenschaft MergedMenu des Hauptfensters zugreifen.) Es besteht leider keine Möglichkeit, die Reihenfolge zu ändern. Wenn Sie damit nicht zufrieden sind, bleibt als Alternative nur die dynamische Veränderung des Hauptmenüs per Programmcode (z.B. immer dann, wenn sich das aktive Subfenster ändert). Dazu stehen zwar einige recht leistungsfähige Methoden zur Verfügung (insbesondere MergeMenu), der Codeaufwand ist aber dennoch erheblich.
15.6.4 Kontextmenüs Kontextmenüs sind kleine Menüs, die an einer beliebigen Stelle im Programm mit der rechten Maustaste aufgerufen werden können. Um ein eigenes Kontextmenü zusammenzustellen, fügen Sie in das Formular ein ContextMenu-Steuerelement ein. Wenn Sie das Steuerelement anklicken, verschwindet vorübergehend das Hauptmenü (wenn Ihr Fenster eines hat), und Sie können den Menüeditor zur Gestaltung des Kontextmenüs verwenden. Auch die Verwaltung der Ereignisprozeduren erfolgt wie bei gewöhnlichen Menüs. Kontextmenüs automatisch anzeigen: Wenn Sie möchten, dass das Kontextmenü durch einen Klick mit der rechten Maustaste automatisch erscheint, stellen Sie die ContextMenuEigenschaft eines Formulars oder Steuerelements so ein, dass sie auf das gewünschte
15.6 Menüs
787
Kontextmenü verweist. Diese Einstellung wird an alle untergeordneten Steuerelemente vererbt. Wenn Sie also die ContextMenu-Eigenschaft für das Fenster einstellen, erscheint das Kontextmenü auch bei allen im Fenster enthaltenen Steuerelementen automatisch. Wenn Sie das nicht möchten, fügen Sie ein zweites, leeres ContextMenu-Steuerelement in Ihr Formular ein und verweisen im Steuerelement mit dessen ContextMenu-Eigenschaft auf das leere Kontextmenü. (Selbstverständlich können Sie auch jedem Steuerelemente ein eigenes Kontextmenü zuweisen.) Kontextmenüs manuell anzeigen: In vielen Anwendungen ist es sinnvoll, beim Drücken der rechten Maustaste zuerst einen Test durchzuführen, ob ein Erscheinen des Kontextmenüs zweckmäßig ist. In diesem Fall verzichten Sie auf die Einstellung der ContextMenuEigenschaft und schreiben stattdessen eine MouseDown-Ereignisprozedur, in der Sie unter anderem die Mauskoordinaten auswerten können. Wenn Sie das Menü anzeigen möchten, führen Sie ContextMenu1.Show aus. An die Methode müssen das Ausgangssteuerelement oder -formular sowie die Koordinaten für die Position des Menüs übergeben werden. Ereignisse: Vor dem Erscheinen des Kontextmenüs tritt für das ContextMenu-Objekt ein Popup-Ereignis auf. Sie können in der Ereignisprozedur die Elemente des Menüs verändern. Sie können allerdings an dieser Stelle nicht mehr verhindern, dass das Menü erscheint. Für die einzelnen Menüeinträge tritt nach deren Auswahl wie gewohnt ein ClickEreignis auf.
Beispiel Das Beispielprogramm oberflaeche\menu-test ist mit zwei Kontextmenüs ausgestattet. Das eine kann im Hintergrundbereich des Fensters verwendet werden. Es erscheint wegen der Einstellung Form1.ContextMenu=ContextMenu1 automatisch und erfüllt ansonsten keine besondere Aufgabe. Das zweite Kontextmenü ist programmtechnisch interessanter: Es kann nur innerhalb des TreeView-Steuerelements verwendet werden und dient dazu, neue Listeneinträge einzu-
fügen bzw. vorhandene Einträge umzubenennen oder zu entfernen (siehe Abbildung 15.23).
Abbildung 15.23: Listeneinträge per Kontextmenü bearbeiten
788
15 Gestaltung von Benutzeroberflächen
Damit durch einen Klick mit der rechten Maustaste im TreeView-Steuerelement nicht automatisch das Kontextmenü des Formulars erscheint, wurde die ContextMenu-Eigenschaft auf das leere ContextMenuEmpty-Steuerelement gerichtet. In TreeView1_MouseDown wird mit GetNodeAt überprüft, ob mit der Maus ein Listeneintrag angeklickt wurde. Dieser Listeneintrag wird in der Variablen tnContext gespeichert, die auch den drei MenuItemContextXxx-Ereignisprozeduren zugänglich ist. Damit wissen die Ereignisprozeduren, welches Element der TreeView-Liste sie bearbeiten sollen. Falls der Starteintrag der Liste ausgewählt wurde, wird der Menüeintrag LÖSCHEN deaktiviert. Damit wird vermieden, dass die gesamte Liste gelöscht werden kann. Anschließend wird das Kontextmenü mit Show angezeigt. ' Beispiel benutzeroberflaeche\menu-test ' Kontextmenü für TreeView-Steuerelement Dim tnContext As TreeNode Private Sub TreeView1_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles TreeView1.MouseDown If e.Button = MouseButtons.Right Then ' TreeNode unter der Maus suchen und auswählen tnContext = TreeView1.GetNodeAt(New Point(e.X, e.Y)) If Not IsNothing(tnContext) Then TreeView1.SelectedNode = tnContext ' Root-Nodes dürfen nicht gelöscht werden If tnContext.Parent Is Nothing Then MenuItemContextTreeRemove.Enabled = False Else MenuItemContextTreeRemove.Enabled = True End If ' Kontextmenü anzeigen ContextMenuTree.Show(CType(sender, TreeView), _ New Point(e.X, e.Y)) End If End If End Sub ' zum ausgewählten Listeneintrag einen untergeordneten ' Eintrag hinzufügen Private Sub MenuItemContextTreeAdd_Click(...) Handles _ MenuItemContextTreeAdd.Click Dim tnNew As TreeNode tnNew = tnContext.Nodes.Add("neuer Listeneintrag") tnNew.EnsureVisible() tnNew.BeginEdit() End Sub
15.6 Menüs
789
' ausgewählten Listeneintrag löschen Private Sub MenuItemContextTreeRemove_Click(...) Handles _ MenuItemContextTreeRemove.Click tnContext.Remove() End Sub ' ausgewählten Listeneintrag umbenennen Private Sub MenuItemContextTreeRename_Click(...) Handles _ MenuItemContextTreeRename.Click tnContext.BeginEdit() End Sub
15.6.5 Menüeinträge selbst zeichnen (owner-drawn menu) Vielleicht haben Sie bei der Beschreibung der MenuItem-Klasse jene Eigenschaften vermisst, mit denen Sie in Menüeinträgen ein kleines Icon darstellen können (wie dies seit Jahren im Office-Paket üblich ist) oder mit denen Sie Menüeinträge mehrfarbig gestalten können (wie in der VS.NET-Entwicklungsumgebung). Leider gibt es diese Eigenschaften nicht! Obwohl unzählige Programmierer seit Jahren diesen Wunsch äußern, bieten die Eigenschaften der MenuItem-Klasse nicht mehr Gestaltungsmöglichkeiten als die Menüeinträge aus VB1. Die einzige Verbesserung besteht darin, dass Sie die Gestaltung von Menüeinträgen durch eigene Zeichenprozeduren nun vollständig selbst übernehmen können. Daraus ergibt sich zwar ein großer Gestaltungsfreiraum, der dafür erforderliche Aufwand ist aber leider mindestens ebenso groß. Wenn Sie einen Menüeintrag selbst zeichnen möchten, müssen Sie OwnerDrawn = True einstellen. Außerdem müssen Sie MeasureItem- und DrawItem-Ereignisprozeduren schreiben. (Es liegt nahe, dass Sie für alle Menüeinträge gemeinsame Ereignisprozeduren verwenden. Um diese Prozeduren mit den Menüeinträgen zu verbinden, führen Sie in Form_Load für alle Menüeinträge AddHandler aus. An dieser Stelle können Sie auch OwnerDraw einstellen.) In der MeasureItem-Ereignisprozedur müssen Sie in den beiden Eigenschaften e.ItemHeight und e.ItemWidth angeben, wie groß der Platzbedarf für den Menüeintrag ist. Die Prozedur wird vor dem Zeichnen eines Menüs für alle Einträge aufgerufen, damit die Menüeinträge mit einer einheitlichen Breite dargestellt werden können. An die DrawItem-Prozedur werden im Parameter e alle zur grafischen Darstellung erforderlichen Daten übergeben: e.ForeColor, e.BackColor, e.Graphics, e.Bounds etc. e.State gibt an, in welchem Zustand sich der Menüeintrag gerade befindet. Die folgende Aufzählung nennt einige Punkte, die Sie bei der Darstellung beachten sollten. •
Wenn im Menütext das Zeichen & enthalten ist, dürfen Sie dieses Zeichen nicht direkt ausgeben, sondern müssen den folgenden Buchstaben unterstreichen. (Den Effekt können Sie einfach durch ein StringFormat-Objekt mit HotkeyPrefix=Show erzielen.)
790
15 Gestaltung von Benutzeroberflächen
•
Vor dem Menütext muss je nach dem Zustand von Checked und RadioChecked ein Auswahlsymbol angezeigt werden.
•
Neben dem Menütext sollte (möglichst rechtsbündig) das durch ShortCut angegebene Tastenkürzel angezeigt werden. Das ist besonders mühsam, weil ShortCut ein Element einer riesigen Aufzählung ist und die ToString-Methode lediglich englische Abkürzungen statt der korrekten deutschsprachigen Beschriftung liefert (also z.B. "CtrS" statt "Strg+S"). Ich habe in der .NET-Bibliothek leider keine Methode gefunden, um Tastenkürzel in die jeweilige Landessprache zu übersetzen. Ich bin mir ziemlich sicher, dass es eine solche Methode gibt – für Menüs, die .NET selbst zeichnet, funktioniert es schließlich auch. Mehrere Experimente endeten aber mit einem out of memory-Fehler. Das wäre noch nicht so schlimm, aber anschließend konnten bis zu einem Neustart des Rechners sämtliche vom Betriebssystem gezeichneten Kontextmenüs (z.B. im Internet Explorer!) nicht mehr verwendet werden! Mit einer gewissen Skepsis darüber, wie ausgereift .NET nun wirklich ist, habe ich die Suche schließlich aufgegeben.
•
Wenn für den Menüeintrag DefaultMenuItem=True gilt, dann sollte der Menüeintrag fett angezeigt werden.
•
Wenn für den Menüeintrag Enabled=False gilt, sollte der Menüeintrag mit grauer Schrift angezeigt werden.
•
Wenn der Menüeintrag nur aus dem Zeichen - besteht, muss er als Trennlinie dargestellt werden.
•
Wenn sich die Maus über einem Hauptmenüeintrag befindet (ohne diesen noch anzuklicken, in e.State ist das Attribut DrawItemState.HotLight gesetzt), sollte der Eintrag hervorgehoben werden. Üblicherweise wird dazu ein dünner Rahmen um den Menüeintrag gezeichnet.
VERWEIS
Den Pfeil, der gegebenenfalls auf Untermenüs verweist, zeichnet das System übrigens trotz OwnerDrawn = True selbst, d.h., darum müssen Sie sich nicht kümmern. Wozu das Rad neu erfinden, wenn es bereits fertige Lösungen gibt? Im Internet finden Sie eine Reihe von (zum Teil kostenlos verfügbaren) Klassen, die Menüs mit einem ansprechenderem Layout ermöglichen als die .NET-Originalklassen. Suchen Sie mit http://www.google.com nach owner-drawn menu vb.net oder c# oder werfen Sie einen Blick auf die folgenden Seiten: http://www.codeproject.com/cs/miscctrl/vsnetmenu.asp http://www.dotnetmagic.com/
Beispiel Das folgende Beispiel zeigt eine einfache Realisierung von Owner-drawn-Menüs (siehe Abbildung 15.24). Der Code ist keineswegs perfekt, sollte aber für eigene Experimente
15.6 Menüs
791
bzw. als Ausgangsbasis für eine Weiterentwicklung ausreichen. Verbesserungsbedarf besteht bei der Darstellung von Tastaturabkürzungen, bei der Konfigurierbarkeit, bei der Anpassungsfähigkeit an unterschiedliche Menütextgrößen etc. Anders als die im Internet angebotenen Lösungen basiert das Beispielprogramm nicht auf einer eigenen Klasse.
VERWEIS
Abbildung 15.24: Optisch ansprechende Owner-drawn-Menüs
Eine ausführliche Beschreibung aller im Code eingesetzten Grafikklassen (z.B. Font, StringFormat etc.) und -methoden (z.B. MeasureString) finden Sie in Kapitel 16.
In Form1_Load wird die rekursive Funktion AddHandlerForAllMenuItems aufgerufen, die alle Menüeinträge mit den Ereignisprozeduren MenuItems_MeasureItem und -_DrawItem verbindet und anschließend OwnerDraw auf True setzt. (Es ist daher nicht notwendig, OwnerDraw für alle Menüeinträge im Eigenschaftsfenster einzustellen.) ' Beispiel benutzeroberflaeche\menu-ownerdrawn Private Sub Form1_Load(...) Handles MyBase.Load AddHandlerForAllMenuItems(MainMenu1.MenuItems) End Sub Private Sub AddHandlerForAllMenuItems( _ ByVal mitems As Menu.MenuItemCollection) Dim mi As MenuItem For Each mi In mitems AddHandler mi.MeasureItem, AddressOf MenuItems_MeasureItem AddHandler mi.DrawItem, AddressOf MenuItems_DrawItem mi.OwnerDraw = True ' Prozedur rekursiv aufrufen AddHandlerForAllMenuItems(mi.MenuItems) Next End Sub MenuItems_MeasureItem berechnet den Platzbedarf für jedes Menüelement. Im Wesentlichen wird mit MeasureString der Platzbedarf (die Breite) für die anzuzeigende Zeichenkette ermittelt. Dabei wird die Schriftart SystemInformation.MenuFont sowie ein spezielles StringFormat-Objekt verwendet, das das &-Zeichen als Markierungszeichen für Alt-Tastenkürzel interpretiert.
792
15 Gestaltung von Benutzeroberflächen
Die Höhe des Menüeintrags wird mit SystemInformation.MenuHeight ermittelt. Wenn es sich bei dem Menüeintrag nur um eine Trennlinie handelt, wird die Höhe mit 10 Pixeln festgelegt. Des Weiteren wird die Breite bei Hauptmenüeinträgen um 10 Pixel reduziert (weil der Abstand zwischen den Menüeinträgen sonst unverhältnismäßig hoch ist), bei allen anderen Menüeinträgen dagegen um SystemInformation.MenuButtonSize vergrößert (um Platz für eine Spalte mit kleinen Icons zu machen). Private Sub MenuItems_MeasureItem(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MeasureItemEventArgs) Dim mi As MenuItem = CType(sender, MenuItem) Dim fnt As Font = SystemInformation.MenuFont Dim sf As New StringFormat() sf.HotkeyPrefix = Drawing.Text.HotkeyPrefix.Show If mi.Text = "-" Then e.ItemHeight = 10 Else e.ItemHeight = SystemInformation.MenuHeight End If e.ItemWidth = CInt(e.Graphics.MeasureString( _ mi.Text + GetShortcutText(mi), fnt, 0, sf).Width) If mi.Parent Is MainMenu1 Then e.ItemWidth -= 10 Else e.ItemWidth += SystemInformation.MenuButtonSize.Width End If End Sub
Die ziemlich lange Prozedur MenuItems_DrawItem ist für die grafische Darstellung des Menüeintrags zuständig. Der Code beginnt damit, dass einige RectangleF-Objekte initialisiert werden. rfAll gibt den gesamten Zeichenbereich an, rfText und rfIcon den Textbereich bzw. die Icon-Spalte (nur bei Menüeinträgen, die sich nicht unmittelbar im Hauptmenü befinden). Private Sub MenuItems_DrawItem(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DrawItemEventArgs) Dim Dim Dim Dim Dim Dim
mi As MenuItem = CType(sender, MenuItem) fnt As Font = SystemInformation.MenuFont brText, brBack, brIcon As Brush sf As New StringFormat() rfAll, rfText, rfIcon As RectangleF gr As Graphics = e.Graphics
15.6 Menüs
793
' Ausgaberechteck für Text und Icons rfAll = RectangleF.op_Implicit(e.Bounds) rfText = rfAll rfIcon = rfAll If mi.Parent Is MainMenu1 Then rfText.X += 2 rfText.Width -= 2 Else rfText.X += SystemInformation.MenuButtonSize.Width + 4 rfText.Width -= SystemInformation.MenuButtonSize.Width + 4 rfIcon.Width = SystemInformation.MenuButtonSize.Width End If
Der nächste Schritt besteht darin, den Hintergrund zu zeichnen. Dazu ist eigentlich die Methode e.DrawBackground vorgesehen. Diese Methode hat sich aber als ungeeignet erwiesen, weil sie aus unerfindlichen Gründen die Farbe SystemColors.Windows (üblicherweise Weiß) statt SystemColors.Control (üblicherweise Grau) verwendet. Außerdem ist der Hintergrund bei diesem Beispiel ja zweigeteilt (außer bei den Hauptmenüeinträgen). Wegen dieser vielen Sonderfälle ist der Code zur Ermittlung der Hintergrundfarben ein wenig unübersichtlich. ' Hintergrund zeichnen If e.BackColor.Equals(SystemColors.Window) Then ' normale Darstellung, per Default weiß If mi.Parent Is MainMenu1 Then brBack = New SolidBrush(SystemColors.Control) Else brBack = New SolidBrush(Color.FromArgb(224, 224, 224)) End If brIcon = New SolidBrush(SystemColors.Control) Else ' inverse Darstellung brBack = New SolidBrush(e.BackColor) brIcon = brBack End If e.Graphics.FillRectangle(brBack, rfAll) ' Hintergrund für die Iconspalte zeichnen If Not (mi.Parent Is MainMenu1) Then gr.FillRectangle(brIcon, rfIcon) End If
Wenn sich die Maus über einem noch nicht angeklickten Menüeintrag befindet, sollte es ein unauffälliges Feedback geben. Im Beispielprogramm wird der Menüeintrag grau umrandet. Eigentlich sollte dazu der Aufruf von e.DrawFocusRectangle ausreichen, allerdings bleibt diese Methode wirkungslos. DrawRectangle ist zum Glück auch nicht viel schwieriger aufzurufen.
794
15 Gestaltung von Benutzeroberflächen
' Rahmen zeichnen If (e.State And DrawItemState.HotLight) = _ DrawItemState.HotLight Then Dim rect As Rectangle rect = e.Bounds rect.Inflate(-1, -1) gr.DrawRectangle(Pens.Gray, rect) End If
Die nächsten Zeilen geben entweder den eigentlichen Menütext aus oder zeichnen eine horizontale Trennlinie. Zur Textausgabe muss je nach mi.Enabled-Zustand eine geeignete Farbe ausgewählt werden. ' Text-/Linienausgabe If mi.Text = "-" Then gr.DrawLine(Pens.Gray, rfText.X + 2, rfText.Y + 4, _ rfText.Right - 2, rfText.Y + 4) Else ' Textfarbe je nach Enabled-Zustand If mi.Enabled Then brText = New SolidBrush(e.ForeColor) Else brText = New SolidBrush(Color.DarkGray) End If sf.HotkeyPrefix = Drawing.Text.HotkeyPrefix.Show e.Graphics.DrawString(mi.Text + GetShortcutText(mi), _ fnt, brText, rfText, sf) End If
Bei Menüeinträgen, die durch Checked=True ausgewählt sind, wird in der Icon-Spalte ein aus zwei kurzen Linien bestehendes Auswahlhäkchen oder ein gefüllter Kreis gezeichnet. Wenn Checked=False gilt, wird durch mehrere Is-Vergleiche ermittelt, welcher Menüeintrag gerade bearbeitet wird. Wenn es dazu im ImageList-Steuerelement ein Icon gibt, wird dieses angezeigt. (Dieser Teil des Codes ist nicht besonders elegant, eine bessere Lösung ist aber nur auf der Basis einer eigenen MenuItem-Klasse möglich.) ' Checkbox / Icon darstellen If mi.Checked Then Dim x0, y0 As Single Dim pn As Pen x0 = rfAll.X + 5 y0 = rfAll.Y + 5 gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.HighQuality If mi.RadioCheck Then gr.FillEllipse(brText, x0, y0, 8, 8) Else pn = New Pen(brText, 3)
15.7 Symbolleiste (ToolBar)
795
gr.DrawLine(pn, x0, y0 + 4, x0 + 4, y0 + 8) gr.DrawLine(pn, x0 + 4, y0 + 8, x0 + 12, y0) End If Else Dim pt As New Point(CInt(rfAll.X + 2), CInt(rfAll.Y + 2)) If mi Is MenuItemFileOpen Then gr.DrawImageUnscaled(ImageList1.Images(0), pt) ElseIf mi Is MenuItemFileSave Then gr.DrawImageUnscaled(ImageList1.Images(1), pt) End If End If ' aufräumen If Not IsNothing(brBack) Then brBack.Dispose() If Not IsNothing(brText) Then brText.Dispose() If Not IsNothing(brIcon) Then brIcon.Dispose() If Not IsNothing(sf) Then sf.Dispose() End Sub
Die Funktion GetShortcutText versucht (leider vergeblich) durch die Angabe eines CultureInfo-Objekts in der ToString-Methode eine Übersetzung des Tastenkürzels zu erreichen. Private Function GetShortcutText(ByVal mi As MenuItem) As String Dim s As String If mi.Shortcut <> Shortcut.None Then s = " (" + mi.Shortcut.ToString("g", _ Globalization.CultureInfo.CurrentCulture()) + ")" End If Return s End Function
15.7
Symbolleiste (ToolBar)
Das ToolBar-Steuerelement ermöglicht es, an einer beliebigen Position im Fenster (üblicherweise am oberen Rand) eine Symbolleiste darzustellen. Die Vorgehensweise zur Erstellung einer eigenen Symbolleiste ist einfach: •
Sie fügen in Ihr Formular ein ImageList- und ein ToolBar-Steuerelement ein.
•
Beim ToolBar-Steuerelement stellen Sie die ImageList-Eigenschaft so ein, dass Sie auf das ImageList-Steuerelement verweist.
•
In das ImageList-Steuerelement fügen Sie die Bilder der Buttons ein, die Sie in der Symbolleiste anzeigen möchten. (Die Bilder sollten in der Regel 16*16 Pixel groß sein. Einige Beispiele finden Sie im Verzeichnis Programme\Microsoft Visual Studio .NET\Common7\Graphics\Bitmaps\OffCtlBr\Small\Color).
796
•
15 Gestaltung von Benutzeroberflächen
Im Dialog zur Einstellung der Buttons-Eigenschaft des ToolBar-Steuerelements (siehe Abbildung 15.25) können Sie schließlich die einzelnen Buttons einfügen, benennen (Name-Feld) und mit Symbolen aus dem ImageList-Steuerelement versehen (ImageIndexEigenschaft). Sie können bei jedem Button dessen Typ angeben (Style-Eigenschaft): Zur Auswahl stehen: PushButton (ein normaler Button) ToggleButton (ein Umschaltbutton) Separator (eine Trennlinie zwischen einer Gruppe von Buttons) DropDownButton (ein Button, der zu einem anderen Eingabeelement führt)
Um die Auswertung der ToolBar_Click-Ereignisse zu erleichtern, können Sie das Steuerelement mit der Tag-Eigenschaft für die interne Identifizierung beschriften.
Abbildung 15.25: Dialog zur Toolbar-Gestaltung
Klassenhierarchie Während die ToolBar-Klasse von Control abgeleitet ist und daher die üblichen Eigenschaften eines Steuerelements hat, ist die ToolBarButton-Klasse zur Verwaltung der einzelnen Buttons von Component abgeleitet. Wie bei dem im vorigen Abschnitt behandelten MenuItem-Objekt fehlt deswegen die Name-Eigenschaft. Immerhin hat man den ToolBarButtons aber eine Tag-Eigenschaft spendiert. Über diesen Umweg können zusammen mit jedem Button Zusatzinformationen gespeichert werden, die in der ButtonClick-Ereignisprozedur ausgewertet werden können.
15.7 Symbolleiste (ToolBar)
797
Klassenhierarchie im System.Windows.Forms-Namensraum Object └─ MarshalByRefObject └─ Component
│ ├─ Control │ └─ ToolBar └─ ToolBarButton
.NET-Basisklasse Objekt nur als Referenz weitergeben Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Steuerelemente Symbolleiste Button einer Symbolleiste
Aussehen und Verhalten Aussehen der Buttons: Je nach Einstellung von Appearance erscheinen die Buttons flach (per Default) oder mit einem 3D-Effekt. Beschriftung der Buttons: Die Buttons der Symbolleiste können im ToolBar-Dialog wahlweise direkt (Eigenschaft Text) oder durch ToolTips (Eigenschaft ToolTipText) beschriftet werden. Je nach Einstellung von TextAlign werden die Beschriftungstexte neben oder unter den Buttons angezeigt. Da beides gleichermaßen hässlich aussieht, werden üblicherweise nur ToolTips zur Beschriftung verwendet.
VERWEIS
Umbruch der Symbolleiste: Per Default werden die Buttons der Symbolleiste automatisch auf mehrere Zeilen verteilt, wenn das Fenster zu schmal ist, um alle Buttons nebeneinander anzuzeigen. Das können Sie durch Wrappable=False vermeiden. Anders als die aus Office-Anwendungen vertrauten Symbolleisten kann das ToolBar-Steuerelement nicht mit der Maus verschoben werden. Sie können dieses Verhalten aber über den Umweg einer Drag&Drop-Operation erreichen, wie das Beispielprogramm in Abschnitt 15.12.2 beweist.
Verwaltung, ButtonClick-Ereignis ToolBar.Buttons verweist auf ein ToolBar.ToolBarButtonCollection-Objekt, das wiederum den Zugang zu allen Buttons der Symbolleiste ermöglicht. Mit Add, Insert und Remove können Sie neue Buttons einfügen bzw. vorhandene Buttons wieder entfernen.
Wenn ein ToolBarButton mit der Maus angeklickt wird, tritt für das ToolBar-Objekt ein ButtonClick-Ereignis auf. (Anders als bei Menüs gibt es also nicht für jeden Button eine eigene Click-Ereignisprozedur, sondern eine gemeinsame Prozedur für alle Buttons!) An die Ereignisprozedur wird mit e.Button ein Verweis auf den angeklickten Button übergeben. Nun stellt sich die Frage: Wie identifizieren Sie den Button? Das ToolBarButton-Objekt hat keine Name-Eigenschaft, und die Text-Eigenschaft ist üblicherweise leer. Eine Variante besteht darin, eine endlose If-Kette mit Is-Objektvergleichen aufzubauen. Der Nachteil dieser
798
15 Gestaltung von Benutzeroberflächen
Vorgehensweise besteht darin, dass sie nicht angewandt werden kann, wenn Buttons dynamisch (per Code) eingefügt wurden. Private Sub ToolBar1_ButtonClick(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.ToolBarButtonClickEventArgs) _ Handles ToolBar1.ButtonClick If e.Button Is ToolBarButtonCopy Then ' ... ElseIf e.Button Is ToolBarButtonCut Then ' ... ElseIf ... End If End Sub
Die anderen Variante besteht darin, dass Sie im ToolBar-Dialog als Tag-Eigenschaft eine Zeichenkette zur Identifizierung angeben. Dann funktioniert beispielsweise der folgende Code: Select Case e.Button.Tag.ToString Case "copy" ' ... Case "cut" '... End Select
Dropdown-Buttons Bei Dropdown-Buttons (tbb.Style=DropDownButton) wird in der Symbolleiste neben dem Button ein Dropdown-Pfeil angezeigt. Wenn dieser Pfeil angeklickt wird, erscheint automatisch das durch die Eigenschaft tbb.DropDownMenu angegebene Kontextmenü. (Die Eigenschaft DropDownMenu muss im Toolbar-Editor so eingestellt werden, dass sie auf ein ContextMenu-Objekt verweist. Die Anzeige eines anderen Steuerelements statt des Menüs ist weder automatisch noch per Code möglich. Microsoft hat keine Eigenschaften vorgesehen, um die Position eines Toolbar-Buttons zu ermitteln.)
TIPP
Beim direkten Anklicken eines Dropdown-Buttons tritt wie bei normalen Buttons ein ButtonClick-Ereignis auf. Nur wenn der daneben angezeigte Dropdown-Pfeil angeklickt wird, tritt ein ButtonDropDown-Ereignis auf. Damit neben den Dropdown-Buttons die kleinen Dropdown-Pfeile angezeigt werden, muss die Eigenschaft DropDownArrow des ToolBar-Steuerelements auf True gesetzt werden.
Für das folgende Beispiel (siehe Abbildung 15.26) wurde ein kleines Kontextmenü mit den drei Einträgen ROT, GRÜN und BLAU mit ToolBarButtonDropDownTest verbunden. Durch die Menüauswahl wird das im Button angezeigte Bild geändert, so dass der Button entspre-
15.7 Symbolleiste (ToolBar)
799
chend der Menüauswahl rot, grün oder blau erscheint. Wenn der Button selbst angeklickt wird, wird der Fensterhintergrund mit der entsprechenden Farbe neu gezeichnet.
Abbildung 15.26: Eine Symbolleiste mit Dropdown-Button und -Menü
VORSICHT
Das eigentliche Problem an dem Beispiel besteht darin, nach der Menüauswahl ein optisches Feedback zu geben: Um dem Toolbar-Button eine neue Farbe zu geben, wird in der Prozedur SetDropDownColor eine neue Bitmap mit 16*16 Pixeln erzeugt und mit der als Parameter übergebenen Farbe gefüllt. Anschließend wird das bisherige Bild für den ToolBarButtonDropDownTest aus der ImageList gelöscht und durch die neue Bitmap ersetzt. Diese Vorgehensweise funktioniert nur dann, wenn das Bild von ToolBarButtonDropDownTest das letzte Bild innerhalb des ImageList-Steuerelements ist. Wenn diese Voraussetzung nicht gegeben ist, verändern sich nämlich die Indexnummern anderer Bilder, und in der Toolbar werden plötzlich falsche Bilder angezeigt. Eine bessere Lösung habe ich aber nicht gefunden, weil das ImageList-Steuerelement weder vorsieht, dass ein Bild geändert oder ersetzt wird, noch, dass ein neues Bild gezielt an einer bestimmten Position in der Liste eingefügt wird, um so die bisherige Reihenfolge wieder herzustellen.
' Beispiel benutzeroberflaeche\toolbar-test Dim dropdownColor As Color Private Sub SetDropDownColor(ByVal tbb As ToolBarButton, _ ByVal c As Color) Dim bm As New Bitmap(16, 16) Dim gr As Graphics = Graphics.FromImage(bm) gr.Clear(c) 'Bitmap mit der Farbe c füllen dropdownColor = c 'Farbe c in dropdownColor speichern ImageList1.Images.RemoveAt(tbb.ImageIndex) tbb.ImageIndex = ImageList1.Images.Add(bm, ToolBar1.BackColor) ToolBar1.Refresh() 'Symbolleiste aktualisieren gr.Dispose() 'Aufräumarbeiten bm.Dispose() End Sub SetDropDownColor wird zum ersten Mal in Form1_Load aufgerufen, wo der Dropdown-
Button der Symbolleiste initialisiert wird.
800
15 Gestaltung von Benutzeroberflächen
Private Sub Form1_Load(...) Handles MyBase.Load SetDropDownColor(ToolBarButtonDropDownTest, Color.Red) End Sub
Das Menü erscheint automatisch beim Anklicken des Dropdown-Pfeils. Als Reaktion auf die Menüauswahl wird die Farbe des Buttons geändert. Private Sub MenuItemRed_Click(...) Handles MenuItemRed.Click SetDropDownColor(ToolBarButtonDropDownTest, Color.Red) End Sub Private Sub MenuItemGreen_Click(...) Handles MenuItemGreen.Click SetDropDownColor(ToolBarButtonDropDownTest, Color.Green) End Sub Private Sub MenuItemBlue_Click(...) Handles MenuItemBlue.Click SetDropDownColor(ToolBarButtonDropDownTest, Color.Blue) End Sub
Wenn der Button selbst angeklickt wird, ändert das Programm die Hintergrundfarbe des Panels, das den sichtbaren Teil des Hauptfensters füllt. Private Sub ToolBar1_ButtonClick(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.ToolBarButtonClickEventArgs) _ Handles ToolBar1.ButtonClick If e.Button Is ToolBarButtonDropDownTest Then Panel1.BackColor = dropdownColor End If End Sub
Client-Bereich des Fensters Anders als die Menüleiste gilt die Symbolleiste als gewöhnliches Steuerelement, das per Default durch Dock=Top am oberen Ende eines Fensters angezeigt wird. Die Symbolleiste füllt damit einen Teil des Client-Bereichs des Fensters. Das bedeutet, dass der Koordinatenpunkt (0,0) sich unterhalb der Symbolleiste befindet. Wenn Sie am Formularhintergrund eine Linie von (0,0) nach (100,100) zeichnen, wird ein Teil der Linie von der Symbolleiste verdeckt. Wie Abbildung 15.27 beweist, kann es noch schlimmer kommen: Wenn Sie das Fenster so schmall machen, dass die Symbolleiste nicht mehr vollständig in einer Zeile Platz findet, wird sie automatisch auf mehrere Zeilen verteilt. Dadurch kommt es unter Umständen zu einem Konflikt mit den darunter befindlichen Steuerelementen. Die Lösung dieses Problems ist zum Glück ist einfach: Fügen Sie im Fenster unterhalb der Symbolleiste ein Panel-Steuerelement mit Dock=Fill ein und verwenden Sie dieses Steuerelement als Container für alle weiteren Steuerelemente oder als Zeichenbereich.
15.8 Statusleiste (StatusBar)
801
Abbildung 15.27: Vorsicht, die Toolbar wird im Client-Bereich des Fensters dargestellt
15.8
Statusleiste (StatusBar)
Im einfachsten Fall dient das StatusBar-Steuerelement dazu, am unteren Fensterrand einen Text mit Statusinformationen zum laufenden Programm anzuzeigen. Wenn Sie gleichzeitig mehrere Informationen in unterteilten Bereichen der Statusleiste anzeigen möchten, können Sie über die Panels-Eigenschaft und den dazugehörenden Dialog mehrere Bereiche (StatusBarPanels) definieren. Damit diese Bereiche angezeigt werden, muss ShowPanels auf True gesetzt werden. (Vorsicht, die Defaulteinstellung lautet False. Damit bleiben die definierten Panels unsichtbar!) Klassenhierarchie im System.Windows.Forms-Namensraum Object └─ MarshalByRefObject └─ Component
│ ├─ Control │ └─ StatusBar └─ StatusBarPanel
.NET-Basisklasse Objekt nur als Referenz weitergeben Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Steuerelemente Statusleiste Element der Statusleiste
Gestaltung der Statusleiste Die Statusleiste selbst ist mit verhältnismäßig wenigen Eigenschaften ausgestattet, die das Aussehen beeinflussen: ShowPanels gibt an, ob Panels oder nur ein einfacher Beschriftungstext angezeigt wird. SizingGrip gibt an, ob am linken Rand des Steuerelements einige diagonale Linien angezeigt werden, die andeuten, dass an dieser Stelle die Fenstergröße verändert werden kann. Alle weiteren Gestaltungsdetails werden im Panels-Dialog als Eigenschaften der StatusBarPanel-Objekte eingestellt. Panel-Größe: Bei jedem Panel kann die Größe auf drei Arten vorgegeben werden: •
Fixe Größe: AutoSize=None, Width=n
•
Größe je nach Inhalt (Text plus Icon): AutoSize=Contents
•
Möglichst groß: AutoSize=Spring.
802
15 Gestaltung von Benutzeroberflächen
Bei der Einstellung AutoSize=Spring füllt das Panel den gesamten zur Verfügung stehenden Platz aus. Wenn es mehrere Spring-Panels gibt, teilen diese sich den Platz. Für die Einstellungen AutoSize=Spring und Contents kann durch MinWidth eine minimale Größe angegeben werden, die nicht unterschritten wird. Panel-Aussehen: Alignment gibt an, ob der Paneltext links-, rechtsbündig oder zentriert dargestellt wird. BorderStyle bestimmt das Aussehen der Panelumrandung. Panel-Icon: Links vom Beschriftungstext kann ein kleines Icon angezeigt werden. Zur Einstellung der Icon-Eigenschaft muss eine Icon-Datei (*.ico) angegeben werden. Eine gewöhnliche Bitmap-Datei ist nicht zulässig. (Bemerkenswert ist der Umstand, dass anders als beim ToolBar-Steuerelement kein Umweg über ein separates ImageList-Steuerelement erforderlich ist. Da stellt sich natürlich die Frage, warum nicht auch bei der ToolBar auf die lästige Verbindung mit ImageList verzichtet wurde.)
Verwaltung, PanelClick-Ereignis StatusBar.Panels verweist auf ein StatusBar.StatusBarPanelCollection-Objekt, das wiederum den Zugang zu allen Panels ermöglicht. Mit Add, Insert und Remove können Sie neue
Panels einfügen bzw. vorhandene Panels entfernen. Das einzige StatusBar-spezifische Ereignis lautet PanelClick. Überraschenderweise gibt es kein DoubleClick-Ereignis. Stattdessen können Sie mit e.Clicks die Anzahl der Mausklicks feststellen (1 oder 2). e.StatusBarPanel verweist auf das angeklickte Panel.
StatusBarPanel selbst zeichnen (Owner-drawn StatusBarPanel) Es besteht leider keine Möglichkeit, andere Steuerelemente innerhalb der Statusleiste anzuzeigen. Besonders praktisch wäre das beim ProgressBar-Steuerelement, mit dem der Fortschritt einer länger andauernden Aktion angezeigt werden könnte. Für solche Fälle sieht die StaturBarPanel-Klasse aber die Einstellung Style = OwnerDraw vor. Damit können Sie das Panel in einer DrawItem-Ereignisprozedur selbst darstellen. (Die Größe des Panels ergibt sich aus der Width-Eigenschaft.)
Abbildung 15.28: Zustandsbalken in einer Statusleiste anzeigen
15.8 Statusleiste (StatusBar)
803
Das folgende Beispiel zeigt, wie während einer längeren Berechnung ein vergleichbarer Zustandsbalken in der Statusbar dargestellt wird (siehe Abbildung 15.28). Dazu wird der aktuelle Zustand in der Variablen panelProgress als Wert zwischen 0 und 100 gespeichert. Die DrawItem-Ereignisprozedur errechnet aus diesem Wert die Größe des Zustandsbalken und zeichnet ihn mit FillRectangle. In der Prozedur Button1_Click, die eine länger andauernde Berechnung simuliert, wird durch Refresh ein regelmäßiges Neuzeichnen der Statusbar erreicht. (Leider gibt es keine Möglichkeit, nur ein Panel neu zu zeichnen.) ' Beispiel benutzeroberflaeche\statusbar-test Dim panelProgress As Integer ' in StatusBarPanel3 (Owner-drawn) Zustandsbalken anzeigen Private Sub StatusBar1_DrawItem(ByVal sender As Object, _ ByVal e As System.Windows.Forms.StatusBarDrawItemEventArgs) _ Handles StatusBar1.DrawItem Dim gr As Graphics = e.Graphics If e.Panel Is StatusBarPanel3 Then gr.FillRectangle( _ Brushes.DarkBlue, e.Bounds.X, e.Bounds.Y, _ CInt(e.Bounds.Width * panelProgress / 100), e.Bounds.Height) End If End Sub ' längere Berechnung simulieren Private Sub Button1_Click(...) Handles Button1.Click Dim i As Integer For i = 1 To 100 Threading.Thread.Sleep(100) panelProgress = i StatusBar1.Refresh() Next panelProgress = 0 StatusBar1.Refresh() End Sub
Client-Bereich des Fensters Wie die Symbolleiste ist auch die Statusleiste ein gewöhnliches Steuerelement, das per Default durch Dock=Bottom am oberen Ende eines Fensters angezeigt wird. Die Statusleiste füllt damit einen Teil des Client-Bereichs des Fensters. Wenn das Probleme bereitet, sollten Sie ein Panel-Steuerelement mit Dock=Fill in das Fenster einfügen und als Container für Steuerelemente und Grafikausgaben verwenden.
804
15 Gestaltung von Benutzeroberflächen
15.9
Tastatur
Alle Steuerelemente, die den Tastaturfokus erhalten können, kennen die folgenden Tastaturereignisse (die in dieser Reihenfolge auftreten): •
KeyDown gibt an, dass eine beliebige Taste gedrückt wurde. Das Ereignis tritt für jede Änderung der gedrückten Tastenkombinationen auf. Wenn Sie beispielsweise Shift+A drücken, um ein großes A einzugeben, treten zwei KeyDown-Ereignisse auf: eines für Shift und wenig später das zweite für A.
Wenn eine Taste längere Zeit gedrückt wird, tritt das KeyDown-Ereignis wiederholt auf (Auto-Repeat). Das gilt auch für Zustandstasten wie Shift. •
KeyPress tritt im Gegensatz zu KeyDown erst auf, wenn eine (eventuell aus mehreren Tasten bestehende) Kombination abgeschlossen ist. Bei Shift+A tritt das KeyPress-Ereignis also erst beim Drücken der Taste A ein. KeyPress tritt weder bei den Funktionstasten (F1-F12, Einfg, Pos1, Ende etc.) noch bei Alt-
Tastenkürzeln auf. KeyPress tritt dagegen sehr wohl bei Strg-Tastenkürzeln auf, die aus einfachen Buchstaben zusammengesetzt sind (z.B. Strg+A, Strg+B etc.). Das Ereignis tritt aber nicht bei Kombinationen mit Ziffern oder Sonderzeichen auf (Strg+1, Strg+Ü etc.)
Die Strg+Alt-Kombinationen entspricht AltGr zur Eingabe von Sonderzeichen wie @, µ oder dem Eurozeichen. In diesem Fall kommt es nur dann zu einem KeyPress-Ereignis, wenn die Tastenkombination einem Zeichen entspricht (z.B. AltGr+Q für @). • VERWEIS
KeyUp ist das äquivalente Ereignis zu KeyDown und tritt beim Loslassen einer Taste auf.
Dieser Abschnitt behandelt nur die Ereignisse zur Verarbeitung von Tastatureingaben, nicht aber jene, die bei einem Fokuswechsel auftreten. Informationen zu Enter und Leave sowie Validating und Validated finden Sie in Abschnitt 14.2.3.
Ereignisparameter An die Ereignisprozeduren zu KeyDown und -Up wird mit e ein Objekt der Klasse KeyEventArgs übergeben. An die KeyPress-Ereignisprozedur wird dagegen nur ein Objekt der Klasse KeyPressEventArgs übergeben, das viel spärlicher mit Informationen ausgestattet ist. KeyEventArgs-Klasse (KeyDown- und KeyUp-Ereignis) e.Alt, e.Control und e.Shift
geben an, ob die Tasten Alt, Strg und Shift gedrückt sind.
e.Modifiers
enthält den gemeinsam Zustand aller Zustandstasten.
15.9 Tastatur
805
KeyEventArgs-Klasse (KeyDown- und KeyUp-Ereignis) e.KeyCode
enthält ein Element der Keys-Aufzählung, das die gedrückte Taste identifiziert, beispielsweise: Keys.A (Taste A) Keys.F2 (Taste F2) Keys.D7 (Taste 7) Keys.NumPad7 (Taste 7 des numerischen Tastenblocks) Keys.Right (Cursortaste nach rechts) Keys.ShiftKey (Shift-Taste)
e.KeyData
identifiziert wie e.KeyCode die gedrückte Taste, enthält aber zusätzlich auch den Zustand von Shift, Alt und Strg. KeyData ist aus einer Kombination von Keys-Elementen zusammengesetzt.
e.KeyValue
enthält den internen Tastaturcode (eine Integer-Zahl) der gedrückten Taste.
KeyPressEventArgs-Klasse (KeyPress-Ereignis)
TIPP
e.KeyChar
enthält als Char-Objekt das eingegebene Unicode-Zeichen. Bei Strg-Kombinationen wie Strg+A, Strg+B etc. enthält KeyChar die Control-Codes 1, 2 etc. Diese Codes können Sie mit AscW(KeyChar) ermitteln.
Den Zustand der Tasten Alt, Strg und Shift können Sie jederzeit auch ohne KeyXxxEreignisse aus der Eigenschaft ModifiersKeys entnehmen, die für alle Steuerelemente zur Verfügung steht.
Verarbeitung von Tastatureingaben unterdrücken Viele Tastatureingaben werden automatisch verarbeitet. Wenn Sie beispielsweise in einem Textfeld die Taste A drücken, wird der Buchstabe a oder A eingefügt (je nachdem, ob auch Shift gedrückt wurde). Wenn Sie Strg+C drücken, wird die aktuelle Markierung in die Zwischenablage kopiert etc. Laut Dokumentation kann sowohl in der KeyDown- als auch in der KeyPress-Ereignisprozedur durch e.Handled=True diese automatische Verarbeitung der Eingabe unterbunden werden. (Mit e.Handled=True geben Sie also an, dass sie sich selbst um diese Eingabe bereits gekümmert haben.) Beispielsweise bewirkt die folgende KeyPress-Prozedur, dass Sie im Textfeld kein kleines a eingeben können.
806
15 Gestaltung von Benutzeroberflächen
Private Sub TextBox1_KeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar = "a" Then e.Handled = True End Sub
Anders als in VB6 können Sie KeyChar innerhalb der Ereignisprozedur nicht verändern, so dass beispielsweise bei der Eingabe von A ein b erscheint. Sie können diesen Effekt aber erreichen, indem Sie die Text-Eigenschaft direkt verändern. Die folgende Ereignisprozedur bewirkt, dass bei der Eingabe eines Kommas ein Punkt erscheint. Private Sub TextBox1_KeyPress(ByVal sender As Object, _ If e.KeyChar = "," Then e.Handled = True TextBox1.SelectedText = "." End If End Sub
Bei KeyDown ist die Angelegenheit komplizierter: e.Handled=True funktioniert nur bei solchen Eingaben, die kein KeyPress-Ereignis verursachen. Beispielsweise können Sie die Taste Pos1 in der KeyDown-Ereignisprozedur deaktivieren (weil Pos1 kein KeyPress-Ereignis verursacht), nicht aber die Taste A. Private Sub TextBox1_KeyDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyEventArgs) _ Handles TextBox1.KeyDown
HINWEIS
If e.KeyCode = Keys.Home Then e.Handled = True End Sub
Das Verhalten auf e.Handled=True in der KeyDown-Prozedur ist so nicht dokumentiert. Die lakonische Information aus der Hilfe zur Handled-Eigenschaft lautet: Gets or sets a value indicating whether the event was handled. Das würde ich so interpretieren, dass Handled für alle und nicht nur für manche Tastaturereignisse gilt. Es ist schwer zu sagen, ob das Handled-Verhalten by design so ist und nur nicht ordentlich beschrieben wurde, oder ob es sich um einen Fehler in .NET handelt, der vielleicht in einem künftigen Service-Pack behoben wird. Sicher ist aber, dass es in den diversen .NET-News-Gruppen zahllose Beiträge gibt, in denen das aktuelle Verhalten kritisiert bzw. als Fehler dargestellt wird.
15.9 Tastatur
807
Tastaturereignisse auf Formularebene Normalerweise erhält nur das Steuerelement die KeyXxx-Ereignisse, das gerade den Tastaturfokus hat. Durch KeyPreview=True erreichen Sie, dass alle Ereignisse zuerst für das Formularobjekt und erst dann für das gerade aktive Steuerelement auftreten. Das ermöglicht es, bestimmte Tastenkombinationen zentral festzustellen und darauf zu reagieren. Die Ereignisabfolge sieht dann so aus: Form_KeyDown → Steuerelement_KeyDown → Form_KeyPress → Steuerelement_KeyPress → Form_KeyUp → Steuerelement_KeyUp
Durch e.Handled=True in der Form_KeyXxx-Ereignisprozedur erreichen Sie, dass das entsprechende Ereignis für das Steuerelement gar nicht mehr auftritt. (Dabei gilt wieder das oben beschriebene Verhalten.)
Sonderfälle •
Wenn sich im Formular ein Menü befindet, verarbeitet dieses Alt-Tastenkombinationen bzw. andere für das Menü definierte Tastenkürzel. Außerdem können Alt-Kombinationen zum Fokuswechsel zwischen unterschiedlichen Steuerelementen eingesetzt werden. Das zuletzt aktive Steuerelement empfängt in diesem Fall nur die KeyDown-, aber keine entsprechenden KeyUp-Ereignisse.
•
Die Taste Tab dient üblicherweise zum Fokuswechsel zwischen den Steuerelementen. In diesem Fall tritt kein Tastaturereignis auf. Es besteht die Möglichkeit, durch die Eigenschaft AcceptsTab für ein Steuerelement anzugeben, dass Tab als normale Texteingabe interpretiert werden soll. (Das ist vor allem für Textfelder praktisch, um Tabulatorzeichen in den Text einzufügen.) In diesem Fall treten die üblichen Ereignisse auf (KeyDown, -Press, -Up). Zum Fokuswechsel muss nun aber die Tastatur verwendet werden.
•
Die Taste Return dient zur Auswahl des Defaultbuttons (wenn dieser durch Form.AcceptButton = ... als solcher definiert ist). In diesem Fall tritt für das aktuelle Ereignis nur das KeyUp-Ereignis auf. Wenn in einem mehrzeiligen Textfeld Return zur Eingabe einer neuen Zeile verwendet werden soll, muss AcceptsReturn auf True gesetzt werden. Damit treten die üblichen Ereignisse auf (KeyDown, -Press, -Up).
•
Die Taste Esc dient zur Auswahl des Abbruchbuttons (Form.CancelButton = ...). Wie bei Return tritt nur das KeyUp-Ereignis auf.
•
Die Taste F1 dient gegebenenfalls zum Aufruf der Hilfe (siehe auch Abschnitt 14.10.4). Die Ereignisse KeyDown und KeyUp treten dennoch auf.
•
Bei den Windows-Tasten treten KeyDown- und KeyUp-Ereignisse auf, bevor das STARTMenü oder ein Kontextmenü erscheint.
808
15 Gestaltung von Benutzeroberflächen
Tastatureingaben simulieren Sie können auf Tastatureingaben nicht nur reagieren, Sie können auch Tasteneingaben simulieren. Dazu übergeben Sie an die Methode SendKeys.Send eine Zeichenkette, die die Tastatureingabe beschreibt. Die simulierten Eingaben gelten für das Programm bzw. für das Steuerelement, das gerade den Eingabefokus hat. Die Syntax für die Tastenzeichenkette sieht folgendermaßen aus: •
Normale Buchstaben und Zahlen werden einfach durch ihre Zeichen angegeben – z.B. "Abc".
•
Für Sondertasten gibt es eigene Schlüsselwörter, die in geschwungenen Klammern angegeben werden müssen, z.B. "{Backspace}", "{Down}", "{Up}", "{Home}". (Eine vollständige Referenz der Sondertasten finden Sie in der Online-Hilfe zu Send.)
•
Die Zustandstasten Shift, Strg und Alt werden durch die drei Zeichen +, ^ und % ausgedrückt. Diese Zeichen müssen unmittelbar vor dem jeweiligen Buchstaben oder Schlüsselwort angegeben werden, z.B. "%D" (Alt+D) oder "^{Home}" (Strg+Pos1).
•
Mehrere Zeichen können einfach hintereinander gestellt werden, z.B. "{Backspace}{Backspace}". Wenn die Zustandstasten für mehrere Zeichen gelten sollen, müssen Klammern verwendet werden, z.B. "%(DF)" (für Alt+D, Alt+F).
Send übergibt die simulierten Eingaben einfach nur an einen Buffer, in dem alle Benutzereingaben bis zu ihrer Verarbeitung zwischengespeichert werden. Es gibt zwei Möglichkeiten, die sofortige Verarbeitung der Eingabe zu erzwingen: SendWait funktioniert wie Send, wartet aber anschließend, bis die Tastatureingaben ausgeführt wurden. Flush ist unabhängig von den SendXxx-Methoden und bewirkt, dass alle Eingaben, die sich im Eingabebuffer befinden, verarbeitet werden.
Im Regelfall helfen die Sendkeys-Methoden dabei, andere Programme fernzusteuern (siehe Abschnitt 15.9). In Sonderfällen kann es aber auch sinnvoll sein, die Eingabe einer Tastenkombination für das laufende Programm zu erzeugen. Beispielsweise zeigt Abschnitt 16.5.10, wie Sie einen Screenshot des laufenden Programms in die Zwischenablage einfügen, von dort in ein Bitmap-Objekt übertragen und schließlich ausdrucken können.
Beispielprogramm Das in Abbildung 15.29 dargestellte Beispielprogramm oberflaeche\key-test hilft dabei, die verschiedenen Tastaturereignisse genauer zu erforschen. Das Programm deckt die meisten Sonderfälle ab: TextBox1 ist mit einem Hilfetext verbunden. OK und ABBRUCH gelten als Default- bzw. Abbruch-Buttons. (Diese Buttons führen nicht zum Programmende, sondern fügen nur eine Zeile Text in das Ausgabefeld ein.) Das Menü enthält sowohl AltKombinationen als auch andere Tastenkürzel (Alt+F8, F6 und Strg+S). Für die Textbox können Sie die Eigenschaften AcceptsTab und AcceptsReturn einstellen und angeben, ob in den KeyDown- bzw. KeyPress-Ereignisprozeduren e.Handled=True ausgeführt werden soll. Wenn Sie möchten, können Sie mit KeyPreview=True auch die Tastaturereignisse auf Formularebene verfolgen und auch dort e.Handled=True ausführen.
15.10 Maus
809
Auf den Abdruck des Codes wurde verzichtet. Das Programm enthält außer zahllosen Ereignisprozeduren zur Auswertung der vielen Ereignisse keinen bemerkenswerten Code.
Abbildung 15.29: Testprogramm für Tastaturereignisse
15.10 Maus Aussehen Das Aussehen der Maus wird durch die Eigenschaft Cursor bestimmt. Wenn diese Eigenschaft für ein Formular bzw. Steuerelement eingestellt wird, gilt die Einstellung auch für alle darin enthaltenen Steuerelemente, soweit dessen Cursor-Eigenschaft nicht ebenfalls anders eingestellt ist. Mit anderen Worten: Mit Me.Cursor=... verändern Sie das Erscheinungsbild der Maus für das gesamte Fenster, mit steuerelement.Cursor=... verändern Sie die Maus nur für dieses spezifische Steuerelement. Cursor erwartet bei der Einstellung ein Objekt der Klasse Cursor. Die Klasse Cursors enthält
eine ganze Reihe derartiger Objekte, die für die meisten Anwendungsfälle ausreichen. (Diese Mausformen stehen auch im Eigenschaftsfenster zur Auswahl.) Besonders häufig werden Sie wahrscheinlich den WaitCursor verwenden, wodurch die Maus als Sanduhr dargestellt wird. Damit können Sie den Anwendern vermitteln, dass nun eine länger dauernde Operation stattfindet. Default stellt das Defaultaussehen wieder her.
810
15 Gestaltung von Benutzeroberflächen
Me.Cursor = Cursors.WaitCursor [... Berechnung] Me.Cursor = Cursors.Default
Wenn Ihnen die vordefinierten Mausformen nicht ausreichen, können Sie an den NewKonstruktor der Cursor-Klasse den Dateinamen einer *.cur-Datei übergeben.
Mausposition ohne Ereignisse feststellen Die Eigenschaft MousePosition enthält als Point-Objekt die Mausposition in absoluten Bildschirmkoordinaten. Diese Eigenschaft steht für alle Steuerelemente zur Verfügung, weil sie wie Cursor für die Klasse Control definiert ist. (Statt aus MousePosition können Sie die absolute Position auch aus Cursor.Position entnehmen.) Da die Koordinaten absolut sind, spielt es aber keine Rolle, ob Sie MousePosition vom Formular oder von einem bestimmten Steuerelement auswerten. Um die Position in das Koordinatensystem eines bestimmten Steuerelements umzurechnen, setzen Sie die Methode PointToClient ein. Dim pt As Point = steuerelement.PointToClient(Me.MousePosition)
In eher seltenen Fällen (etwa zum Aufruf von Grafikmethoden, die absolute Koordinaten erwarten), benötigen Sie die Umkehrmethode PointToScreen. Damit werden relative Koordinaten in absolute umgerechnet. Wenn Sie die Mausposition auswerten, wollen Sie meistens auch den Zustand der Maustasten und der Tasten Shift, Strg und Alt wissen. Diese Informationen können Sie den Eigenschaften MouseButtons und ModifierKeys entnehmen. Bei beiden Eigenschaften müssen Sie mit And arbeiten, um festzustellen, ob eine bestimmte Taste gedrückt ist. (Ein direkter Vergleich ist nicht zielführend, weil mehrere Tasten gleichzeitig gedrückt werden können!) If (Me.MouseButtons And MouseButtons.Right) = MouseButtons.Right Then
Mausposition ändern Sie können die Mausposition per Code auch ändern. Dazu weisen Sie Cursor.Position ein neues Point-Objekt zu, das die neue absolute Position enthält. (Achtung: MousePosition kann nicht verändert werden, Sie müssen die Veränderung via Cursor.Position durchführen.) Me.Cursor.Position = New Point(100, 100)
Mausereignisse An Mausereignissen mangelt es wahrlich nicht! Die folgende Aufzählung gibt eine Kurzbeschreibung der Ereignisse. Die Reihenfolge der Beschreibung orientiert sich an der Reihenfolge, in der die Ereignisse auftreten (falls durch eine bestimmte Mausaktion mehrere Ereignisse gleichzeitig ausgelöst werden).
15.10 Maus
811
Beachten Sie, dass die Mausereignisse im Regelfall nur für das Steuerelement auftreten, über dem sich die Maus gerade befindet. Das Programm muss dazu nicht aktiv sein (d.h., es muss nicht das oberste Fenster sein). •
MouseEnter gibt an, dass die Maus über ein Steuerelement bewegt wurde (also in das Gebiet des Steuerelements eingedrungen ist).
•
MouseMove gibt an, dass die Maus innerhalb des Steuerelements bewegt wurde.
•
MouseHover gibt an, dass sich die Maus eine Weile über dem Steuerelement befunden hat. Das Ereignis tritt anders als MouseEnter nicht sofort auf, sondern erst ca. eine Se-
kunde später. Wenn die Maus innerhalb dieser Zeit bereits in ein anderes Steuerelement weiterbewegt wurde, tritt das Ereignis gar nicht auf. Das Ereignis tritt nur ein einzges Mal auf, bis die Maus das Steuerelement wieder verlässt. •
MouseDown gibt an, dass eine (weitere) Maustaste gedrückt wurde. e.Button gibt die
Maustaste an, die (zusätzlich) gedrückt wurde. Wenn gemeinsam zuerst die linke und dann die rechte Maustaste gedrückt wird, tritt das Ereignis zweimal auf. e.Button enthält beim ersten Ereignis MouseButton.Left und beim zweiten Ereignis MouseButton.Right (und nicht etwa MouseButton.Left Or MouseButton.Right!). Wenn Sie wissen möchten, welche Maustasten insgesamt gedrückt sind, müssen Sie die Steuerelementeigenschaft MouseButtons auswerten. •
Click gibt an, dass eine Maustaste gedrückt wurde. Im Gegensatz zu MouseDown wird an die Ereignisprozedur mit e aber nur ein leeres Objekt übergeben.
•
DoubleClick gibt an, dass eine Maustaste zweimal rasch hintereinander gedrückt wurde. (In diesem Fall kommt es beim ersten Klick zu einem Click- und beim zweiten Klick zu einem DoubleClick-Ereignis.)
•
MouseUp gibt an, dass eine Maustaste losgelassen wurde. Wie bei MouseDown gibt e.Button nur die Taste an, die sich geändert hat.
•
MouseLeave gibt an, dass die Maus den Bereich des Steuerelements verlassen hat.
An die Ereignisprozeduren zu MouseDown, -Up und -Move wird mit e ein Objekt der Klasse MouseEventArgs übergeben. Dieses Objekt enthält die folgenden Informationen: e.X und e.Y: die relativen Mauskoordinaten (im Koordinatensystem des Steuerelements) e.Button: den Zustand der Maustasten e.Clicks: die Anzahl der Mausklicks (1 oder 2) e.Delta: die Bewegung des Mausrads (siehe unten)
Wenn Sie auch Informationen über den Zustand der Tasten Alt, Strg oder Shift benötigen, müssen Sie die oben bereits erwähnte Steuerelementeigenschaft ModifierKeys auswerten.
812
15 Gestaltung von Benutzeroberflächen
HINWEIS
Die Ereignisse MouseMove, MouseHover, MouseEnter und MouseLeave treten auch dann auf, wenn Ihr Formular nicht den Eingabefokus hat (d.h., wenn gerade ein anderes Fenster aktiv ist). MouseMove tritt nur auf, während sich die Maus über einem sichtbaren Teil des Steuerelements bzw. Formulars befindet. MouseMove tritt aber nicht auf, wenn die
Maus bereits mit gedrückter Maustaste in das Steuerelement bewegt wird. In diesem Fall bleiben die MouseMove-Ereignisse bei dem Steuerelement, in dem die Maustaste ursprünglich gedrückt worden ist. Folglich treten auch MouseEnter bzw. MouseLeave nicht auf, wenn die Maus mit gedrückter Maustaste in ein Steuerelement hinein bzw. wieder herausbewegt wird. Die Ereignisse werden aber gleichsam nachgeholt, wenn die Maustaste losgelassen wird und sich die Maus zu diesem Zeitpunkt noch immer innerhalb bzw. außerhalb des Steuerelements befindet.
Wenn Sie ausführlicher erforschen möchten, wann welche Mausereignisse auftreten, sollten Sie das Beispielprogramm benutzeroberflaeche\mouse-test starten (siehe Abbildung 15.30). Alle Mausereignisse, die für die GroupBox2 MAUSEREIGNISSE und die beiden darin enthaltenen PictureBox-Steuerelemente auftreten, werden im daneben liegenden Listenfeld angezeigt. Da dabei die MouseMove-Ereignisse bei weitem dominieren, können diese Ereignisse ausgeblendet werden (was etwa bei Klicktests zu übersichtlicheren Ausgaben führt). Auf einen Abdruck des Programmcodes wird verzichtet – die zahllosen Ereignisprozeduren enthalten nur jeweils ein bis zwei Zeilen Code, um Informationen über das jeweilige Ereignis mit AppendText in das Textfeld zu schreiben.
Abbildung 15.30: Ein Programm zur Analyse der Mausereignisse
VERWEIS
15.10 Maus
813
Eine konkrete Anwendung mehrerer Mausereignisse finden Sie in Abschnitt 16.5.5, wo ein Programm zur so genannten Rubberbox-Auswahl vorgestellt wird. (Eine Rubberbox ist ein Rechteck, das bei gedrückter Maustaste gezeichnet wird, normalerweise, um Objekte auszuwählen.)
Mausrad (MouseWheel-Ereignis) Wenn Sie einen Blick in den Objektbrowser werfen, werden Sie bei der Control-Klasse ein MouseWheel-Ereignis finden. Das ist insofern überraschend, als dieses Ereignis im Codefenster nicht aufscheint und daher nicht per Mausklick eingefügt wird. (Microsoft wollte offensichtlich nicht, dass dieses Ereignis verwendet wird.) Sie können die Ereignisprozedur aber durchaus eintippen: Private Sub ByVal e Handles [... Code End Sub
TextBox1_MouseWheel(ByVal sender As Object, _ As System.Windows.Forms.MouseEventArgs) _ TextBox1.MouseWheel zur Verarbeitung des Ereignisses]
Anders als alle anderen Mausereignisse kann das MouseWheel-Ereignis nur dann empfangen werden, wenn das Steuerelement gerade den Tastaturfokus hat. Dieses Verhalten ist durch die Überlegung bestimmt, dass ein Steuerelement mit Tastaturfokus auch dann per Mausrad bedient werden kann, wenn sich die Maus gerade nicht über dem Steuerelement befindet. (Auch der Windows-2000-Explorer oder der Internet Explorer 6 verhalten sich so.) Dieses Verwaltung der Mausradereignisse hat leider zwei gravierende Nachteile: •
Bei allen Steuerelementen, die den Tastaturfokus nicht besitzen (z.B. Label, PictureBox etc.), können Sie nie ein MouseWheel-Ereignis feststellen!
•
Es ist unmöglich, eine Benutzeroberfläche wie z.B. bei Outlook Express 6 zu erstellen, bei der das Mausrad automatisch für das Steuerelement gilt, über dem sich die Maus gerade befindet. (Ich persönlich finde diesen Ansatz intuitiver.)
In der Praxis bedeutet das, dass Sie das MouseWheel-Ereignis selten benötigen. Die Steuerelemente, die das Ereignis empfangen können, verarbeiten es im Regelfall selbstständig korrekt (z.B. Text- und Listenfelder). Und bei allen anderen Steuerelementen können Sie das Ereignis nicht feststellen ... Der Vollständigkeit halber seien hier dennoch die Informationen zur Verarbeitung des Ereignisses gegeben. An die Ereignisprozedur wird mit e ein Objekt der Klasse MouseEventArgs übergeben. Dessen Eigenschaft Delta gibt an, wieweit das Mausrad gedreht wurde. Bei meiner Maus (Logitech Wheel Mouse) enthält Delta den Wert 120, wenn das Rad um eine Rastereinheit nach oben gedreht wurde bzw. -120 bei einer Drehung nach unten. Laut Dokumentation ist der Wert 120 allerdings keine Konstante, sondern kann je nach Maus bzw. Mauskonfiguration variieren. (Wenn der Sensor für das Mausrad höher auflös-
814
15 Gestaltung von Benutzeroberflächen
end ist, kann es also sein, dass bei der gleichen Drehung mehr MouseWheel-Ereignisse eintreffen, der Delta-Wert aber jedes Mal entsprechend kleiner ist.) Die Dokumentation empfiehlt in solchen Fällen, die Delta-Werte zu summieren und erst dann eine Aktion auszulösen, wenn 120 überschritten wird. Wie stark Sie den Inhalt eines Steuerelements bei einem Delta-Wert von 120 scrollen, hängt natürlich von der Anwendung ab. Einen Anhaltspunkt gibt aber die Eigenschaft SystemInformation.MouseWheelScrollLines. Sie gibt an, um wie viele Zeilen Text ein Textfeld gescrollt werden soll, wenn das Mausrad um 120 Einheiten gedreht wurde. (Die Anzahl der Zeilen kann bei der Mauskonfiguration eingestellt werden. Per Default sind es drei Zeilen – aber manche Anwender wollen pro Mausdrehung weiter scrollen.)
15.11 Zwischenablage
HINWEIS
Eine ganze Reihe von .NET-Steuerelementen kommuniziert selbstständig mit der Zwischenablage. Beispielsweise funktionieren in Textfeldern die Tastenkürzel Strg+C, Strg+X und Strg+V bzw. Shift+Einfg und Shift+Entf automatisch, ohne dass Sie eine Zeile Code schreiben müssen.
TIPP
Der Inhalt der Zwischenablage wird über die Windows.Forms-Klasse Clipboard angesprochen. Die Klasse kennt nur zwei Methoden: GetDataObject, um den Inhalt der Zwischenablage auszulesen, und SetDataObject, um die Zwischenablage zu verändern.
Wenn Sie Programme zur Manipulation der Zwischenablage schreiben, ist das Programm c:\windows\system32\clipbrd.exe ein nützliches Hilfsmittel. Es zeigt die gerade in der Zwischenablage enthaltenen Daten in beliebigen Formaten an.
Zwischenablage lesen GetDataObject liefert ein Objekt der Schnittstelle IDataObject. Dim ido As IDataObject ido = Clipboard.GetDataObject()
Der erste Schritt zur Verarbeitung der Daten besteht darin, ihren Typ festzustellen. Dazu führen Sie ido.GetFormats aus. Diese Methode liefert ein String-Feld mit den Namen aller in der Zwischenablage enthaltenen Formate (bzw. Formate, in die die Daten umgewandelt werden können). In diesem Zusammenhang ist es wichtig zu wissen, dass die Zwischenablage Daten in mehreren Formaten gleichzeitig enthalten kann. Wenn Sie z.B. in Microsoft Word 2000 einen Textabsatz kopieren, dann wird dieser Absatz unter anderem als ASCII-Text, Unicode-Text, RichText (RTF), im HTML-Format sowie als Windows-MetafileGrafikobjekt in die Zwischenablage eingefügt.
15.11 Zwischenablage
815
Oft sind Sie gar nicht an allen Formaten interessiert, sondern wollen nur wissen, ob die Zwischenablage Daten in einem bestimmten Format enthält, das Ihr Programm weiterverarbeiten kann. Dazu verwenden Sie die Methode GetDataPresent, an die Sie wahlweise eine Zeichenkette oder ein System.Type-Objekt zur Beschreibung des Formats übergeben können. Die Klasse DataFormats enthält eine Aufzählung mit den wichtigsten System.TypeObjekten, z.B. Bitmap, CommaSeparatedValue, EnhancedMetafile, Rtf und Text. If ido.GetDataPresent("HTML Format") Then ... If ido.GetDataPresent(DataFormats.Html) Then ...
Beliebige andere Datentypen können Sie angeben, indem Sie auf ein entsprechendes Objekt die Methode GetType anwenden. (GetType funktioniert allerdings nicht, wenn Sie nur den Klassennamen angeben. Bitmap.GetType() liefert daher lediglich eine Fehlermeldung.) Wenn Sie Daten mit externen Programmen austauschen möchten, die Zwischenablage also nicht nur zur Kommunikation innerhalb eines oder mehrerer selbst erstellter Programme einsetzen, sollten Sie nach Möglichkeit die vordefinierten DataFormats-Elemente verwenden. If ido.GetDataPresent(bitmapobj.GetType()) Then ...
Um die Daten in einem bestimmten Format auszulesen, verwenden Sie schließlich die Methode GetData, wobei Sie wie bei GetDataPresent das gewünschte Datenformat angeben müssen. GetData liefert den Datentyp Object, den Sie mit CType in den von Ihnen gewünschten Typ umwandeln müssen. (Diese Umwandlung klappt natürlich nur, wenn eine Umwandlung möglich ist. Sie können also kein Grafikobjekt in den Datentyp String umwandeln.) Die folgende Anweisung fügt Textdaten aus der Zwischenablage in ein Textfeld ein. TextBox1.Text = CType(ido.GetData(DataFormats.Text), String)
Zwischenablage ändern Mit ClipBoard.SetData(obj) fügen Sie einen Verweis auf ein beliebiges Objekt in die Zwischenablage ein. (Wenn sich obj später ändert, ändert sich somit auch der Inhalt der Zwischenablage.) Eventuell in der Zwischenablage befindliche Daten werden dadurch automatisch überschrieben. Grundsätzlich gibt es keine Einschränkungen bezüglich des Objekttyps. Andere Programme werden aber nur dann etwas mit den Daten anfangen können, wenn Sie Ihre Daten in einem der allgemein akzeptierten Formate übergeben. An SetData können Sie im optionalen zweiten Parameter True übergeben: Damit erreichen Sie, dass die Daten in die Zwischenablage kopiert werden. Die Daten stehen dann in der Zwischenablage auch dann noch zur Verfügung, wenn die Daten in Ihrem Programm durch Dispose gelöscht werden oder wenn Ihr Programm endet. In die Zwischenablage eingefügte Daten stehen meist automatisch in mehreren Formaten zur Verfügung. Wenn Sie beispielsweise Text einfügen, liegt dieser in den Formaten "System.String", "Text" und "UnicodeText" vor. Es gibt aber leider keine Möglichkeit, selbst Daten in mehreren Formaten einzufügen.
816
15 Gestaltung von Benutzeroberflächen
Beispielprogramm Das Beispielprogramm (siehe Abbildung 15.31) demonstriert einige einfache Anwendungen der Zwischenablage. Im linken oberen Eck des Beispielprogramms wird ein Listenfeld mit allen in der Zwischenablage enthaltenen Datenformaten angezeigt. Das Listenfeld wird bei jeder Änderung der Zwischenablage aktualisiert. (Die Aktualisierung erfolgt genau genommen in einer Timer-Ereignisprozedur, die vier Mal pro Sekunde aufgerufen wird und überprüft, ob sich die Datenformate in der Zwischenzeit geändert haben.) Mit drei Buttons können Sie Text- oder Bitmap-Daten in die Zwischenablage einfügen bzw. von dort lesen.
Abbildung 15.31: Datenaustausch mit der Zwischenablage
' Beispiel benutzeroberflaeche\clipboard-test Private Sub Timer1_Tick(...) Handles Timer1.Tick Dim ido As IDataObject Dim tmp, frmt As String Static last_formats As String ido = Clipboard.GetDataObject() tmp = Join(ido.GetFormats) If tmp <> last_formats Then last_formats = tmp ListBox1.Items.Clear() For Each frmt In ido.GetFormats() ListBox1.Items.Add(frmt) Next End If Button1.Enabled = ido.GetDataPresent(DataFormats.Text) Button2.Enabled = (TextBox1.SelectionLength > 0) End Sub ' Textinhalt der Zwischenablage in TextBox1 einfügen Private Sub Button1_Click(...) Handles Button1.Click Dim ido As IDataObject ido = Clipboard.GetDataObject() If ido.GetDataPresent(DataFormats.Text) Then
15.12 Drag&Drop
817
TextBox1.SelectedText = _ CType(ido.GetData(DataFormats.Text), String) End If End Sub ' markierten Text aus TextBox1 in die Zwischenablage kopieren Private Sub Button2_Click(...) Handles Button2.Click If TextBox1.SelectionLength > 0 Then Clipboard.SetDataObject(TextBox1.SelectedText) End If End Sub
Beachten Sie, dass es in Button3_Click zwei mögliche Vorgehensweisen gibt: Die eine besteht darin, dass Sie SetDataObject(bm) ausführen – dann dürfen Sie die Bitmap aber nicht mit Dispose aus dem Speicher löschen. Da die Zwischenablage nur einen Verweis auf die Bitmap enthält, ändern sich die über die Zwischenablage zugänglichen Daten, wenn Sie die Bitmap durch weitere Grafikkommandos verändern. Die andere, im Beispielprogramm gewählte Variante besteht darin, SetDataObject(bm, True) zu verwenden und so eine Kopie der Bitmap in die Zwischenablage einzufügen. Damit können Sie mit der Bitmap anschließend tun und lassen, was Sie wollen – die in der Zwischenablage befindlichen Daten werden dadurch nicht beeinträchtigt. ' selbst gezeichnete Bitmap in die Zwischenablage einfügen Private Sub Button3_Click(...) Handles Button3.Click Dim bm As New Bitmap(100, 100) Dim gr As Graphics = Graphics.FromImage(bm) gr.Clear(Color.White) gr.FillEllipse(Brushes.Blue, 0, 0, 100, 100) gr.Dispose() Clipboard.SetDataObject(bm, True) bm.Dispose() End Sub
15.12 Drag&Drop Der Begriff Drag and Drop bezeichnet das Verschieben von Objekten an eine neue Position (im selben oder in einem anderen Fenster, das auch von einem anderen Programm stammen kann). Anders als VB6 unterstützt VB.NET kein automatisches Drag&Drop. Es ist aber nicht besonders schwierig, selbst Drag&Drop-Funktionen zu realisieren. Bevor in diesem Abschnitt die Drag&Drop-Programmierung ausführlich beschrieben wird, vorweg ein kurzer Überblick: Um eine Drag&Drop-Operation in einem VB-Programm auszulösen, führen Sie die Methode DoDragDrop aus (üblicherweise in einer MouseDown- oder MouseMove-Ereignisprozedur).
818
15 Gestaltung von Benutzeroberflächen
Wenn Sie dagegen Drag&Drop-Operation verarbeiten (empfangen) möchten, müssen Sie für das betreffende Steuerelement oder Formular die Eigenschaft AllowDrop auf True stellen und zumindest die zwei Ereignisprozeduren DragEnter und DragDrop programmieren. VB-Programme können sowohl Auslöser als auch Empfänger von Drag&Drop-Ereignissen sein (aber es ist nicht erforderlich, dass ein Programm immer beide Funktionen realisiert).
15.12.1 Programmiertechniken Drag&Drop-Operation initiieren Eine Drag&Drop-Operation beginnt mit der Methode DoDragDrop. Im ersten Parameter übergeben Sie die Daten, die verschoben oder kopiert werden sollen. Im zweiten Parameter geben Sie an, welche Operationen zulässig sind. Zur Auswahl stehen die Elemente der DragDropEffects-Aufzählung (oder eine beliebige Or-Kombination): Copy, Move, Scroll, Link oder All. Bei der Programmierung der Beispiele zu diesem Abschnitt hat sich herausgestellt, dass das Auslösen einer Drag&Drop-Operation viel schwieriger ist als das Empfangen und Verarbeiten solcher Aktionen. Das Problem besteht darin, dass Drag&Drop-Operationen üblicherweise mit der linken Maustaste beginnen. Ein erster Ansatz zum Start einer Verschiebeoperation sieht daher meist folgendermaßen aus: Private Sub steuerelement_MouseDown(...) Handles ToolBar1.MouseDown If e.Button = MouseButtons.Left Then steuerelement.DoDragDrop(daten, DragDropEffects.Move) End If End Sub
Ganz so einfach funktioniert es aber leider selten: Das Drücken der linken Maustaste dient ja oft zur Markierung eines Objekts (oder Listeneintrags). Es muss also zwischen einem gewöhnlichen Mausklick und dem Beginn einer Drag&Drop-Operation unterschieden werden. Viele Steuerelemente bieten dazu leider keine ausreichenden Möglichkeiten, so dass alle möglichen Umwege begangen werden müssen. Zu allem Überfluss kommen sich bei manchen Steuerelementen die interne Logik zur automatischen Verarbeitung von Mausklicks und Ihr eigener Code zum Start einer Drag&Drop-Aktion in die Quere (siehe insbesondere das in Abschnitt 15.12.4 präsentierte Beispiel). Eine positive Ausnahme dieser Misere stellen die beiden Steuerelemente TreeView und ListView dar. Dort tritt nämlich das DragItem-Ereignis auf, wenn der Anwender versucht, zuvor markierte Einträge zu verschieben. In der Ereignisprozedur können Sie dann mühelos DoDragDrop ausführen.
Drag&Drop-Ereignisse zulassen Damit ein Steuerelement bzw. ein Formular als Empfänger für Drag&Drop-Ereignisse dienen und die im Folgenden beschriebenen DragXxx-Ereignisse empfangen kann, muss die AllowDrop-Eigenschaft auf True gestellt werden. Üblicherweise erfolgt das bereits bei
15.12 Drag&Drop
819
der Programmentwicklung im Eigenschaftsfenster. (Achtung, die Defaulteinstellung für diese Eigenschaft lautet False!) Die AllowDrop-Eigenschaft kann grundsätzlich für jedes Steuerelement individuell eingestellt werden. Bei Containern (und insbesondere bei Formularen) vererbt sich die Einstellung AllowDrop=True aber auf alle enthaltenen Steuerelemente, auch wenn für diese AllowDrop=False gilt. In diesem Fall treten die DragXxx-Ereignisse aber nicht für das Steuerelement, sondern für den Container auf. Im Regelfall ist dieses Verhalten praktisch: Wenn beispielsweise das gesamten Fenster als Drag&Drop-Empfänger gelten soll, brauchen Sie lediglich für das Formular AllowDrop auf True zu setzen und DragXxx-Ereignisprozeduren für das Formular zu entwickeln. Um die im Formular enthaltenen Steuerelemente brauchen Sie sich nicht zu kümmern – Drag&Drop-Operationen sind auch dort möglich und werden von den DragXxx-Ereignisprozeduren des Formulares verwaltet. Wenn Sie freilich möchten, dass ein Steuerelement innerhalb eines Drag&Drop-tauglichen Fensters explizit nicht als Empfänger dienen soll, müssen Sie AllowDrop=True einstellen. Das klingt zunächst wahrscheinlich widersinnig. Sie erreichen dadurch aber, dass die DragXxx-Ereignisprozeduren des Steuerelements aktiv werden. Wenn Sie dort keinen Code vorsehen, ist das Steuerelement zwar theoretisch ein Drag&Drop-Empfänger, verweigert tatsächlich aber die Annahme jeglicher Drag&Drop-Objekte. Damit haben Sie erreicht, was Sie möchten.
Drag&Drop-Ereignisse Für den Empfänger einer Drag&Drop-Operation (also für Steuerelemente bzw. das Formular mit AllowDrop=True) treten die folgenden Ereignisse auf: •
DragEnter tritt auf, wenn die Maus mit einem Drag-Objekt über ein Steuerelement bewegt wird. Mit e.Data können die Daten ermittelt werden, die beim Loslassen über-
geben werden. In der Prozedur wird üblicherweise der Objekttyp der Daten getestet. Wenn das Steuerelement mit den Daten zurechtkommt, wird durch e.Effect das Aussehen des Mauscursors verändert. (Per Default sieht die Maus wie die Verkehrstafel Einfahrt verboten aus.) •
DragOver tritt kontinuierlich während der Bewegung der Maus auf. (Das Ereignis entspricht in etwa MoveMove.) Bei manchen Drag&Drop-Anwendungen können Sie hier
eine Art Vorschau realisieren, die anzeigt, was passieren würde, wenn die Maustaste jetzt losgelassen würde. Eine andere Funktion der DragOver-Ereignisprozedur könnte darin bestehen, im TreeView-Steuerelement die hierarchische Liste auseinanderzuklappen (wie dies auch im Windows-Explorer der Fall ist, wenn Sie Dateien in ein anderes Verzeichnis verschieben oder kopieren möchten).
820
15 Gestaltung von Benutzeroberflächen
Je nachdem, ob Zustandstasten wie Shift oder Strg gedrückt sind, können Sie via e.Effect auch hier das Aussehen der Maus verändern, um so optisch zwischen einer Verschiebeund einer Kopieroperation zu unterscheiden. •
DragLeave tritt analog zu DragEnter auf, wenn die Maus das Steuerelement wieder verlässt. Im Regelfall ist es nicht notwendig, eine Prozedur zu diesem Ereignis zu schreiben. Insbesondere brauchen Sie sich nicht um das Aussehen der Maus zu kümmern (Sie können auch gar nicht, weil keine entsprechenden Parameter übergeben werden) – dafür ist nun das Steuerelement oder Programm verantwortlich, über dem sich die Maus jetzt befindet.
•
DragDrop tritt auf, wenn der Anwender die Maustaste loslässt, die Daten also fallen gelassen werden.
Auch für das Steuerelement, das die Drag&Drop-Operation initiiert hat (gewissermaßen der Sender), tritt ein Ereignis auf: •
QueryContinueDrag tritt kontinuierlich während der Drag&Drop-Operation auf. In der Prozedur kann die Operation je nach Tastatureingabe abgebrochen werden (e.Action = DragAction.Cancel) oder vorzeitig abgeschlossen werden (e.Action=DragAction.Drop).
Normalerweise brauchen Sie keine QueryContinueDrag-Ereignisprozedur zu schreiben, weil die Drag&Drop-Operation durch Esc ohnedies automatisch beendet wird und andere Aktionen selten erforderlich sind. An die Ereignisprozeduren zu DragEnter, DragOver und DragLeave wird mit e ein Objekt der Klasse DragEventArgs übergeben. Die folgende Tabelle fasst die Eigenschaften dieser Klasse zusammen. DragEventArgs-Klasse (DragEnter-, DragOver- und DragLeave-Ereignisse) e.AllowedEffect
gibt an, welche Drag&Drop-Operationen zulässig sind. Diese Eigenschaft enthält die DragDropEffects-Kombination, die bei DoDragDrop im zweiten Parameter angegebenen wurde.
e.Data
verweist auf ein IDataObject mit den Daten, die durch die Drag&DropOperation übertragen werden. Die Auswertung der Daten ist im vorigen Abschnitt beschrieben. (Die Zwischenablage überträgt Daten ebenfalls als IDataObject.)
e.Effect
gibt das Aussehen der Maus an. e.Effect muss insbesondere in der DragEnter-Prozedur eingestellt werden, andernfalls wird der Mauscursor als Eintritt verboten dargestellt und es kann kein DragDropEreignis auftreten. Bei der Einstellung von e.Effect sind nur die Werte zulässig, die mit e.AllowedEffect angegeben wurden. Falls je nach den Zustandstasten Strg, Shift oder Alt unterschiedliche Drag&Drop-Operationen möglich sind, sollte in der DragOver-Prozedur e.KeyState ausgewertet werden und e.Effect entsprechend eingestellt werden.
15.12 Drag&Drop
821
DragEventArgs-Klasse (DragEnter-, DragOver- und DragLeave-Ereignisse) e.KeyState
enthält den Zustand der Tasten Strg, Shift oder Alt sowie der Maustasten. Aus unerfindlichen Gründen gilt KeyState als Integer-Wert, d.h., es gibt keine Aufzählung (Enum) mit den zulässigen KeyState-Zuständen. Stattdessen enthält die Online-Hilfe die folgende Tabelle: 1 2 4 8 16 32
linke Maustaste rechte Maustaste Shift-Taste Strg-Taste mittlere Maustaste Alt-Taste
Da beliebige Kombinationen dieser Werte zulässig sind, müssen Sie zur Auswertung And verwenden. Die folgende Zeile stellt fest, ob die mittlere Maustaste gedrückt ist: If (e.KeyState And 16) = 16 Then ...
Einen besser lesbaren Code erhalten Sie freilich, wenn Sie e.KeyState gleich ganz ignorieren und die Informationen über den Zustand der Tasten aus MouseButtons bzw. ModfiersKeys entnehmen. e.X und e.Y
enthält die absoluten Mauskoordinaten, die Ihnen aber wahrscheinlich nicht viel helfen werden. Zur Ermittlung der relativen Mauskoordinaten für das Steuerelement ctrl können Sie die folgende Anweisung verwenden: Dim pt As Point = _ ctrl.PointToClient(Me.MousePosition)
15.12.2 Beispiel – Symbolleiste verschieben Bei dem in Abbildung 15.32 dargestellten Beispielprogramm können Sie die Symbolleiste per Drag&Drop an einen der vier Fensterränder verschieben. Das Fenster besteht aus einem ToolBar-Steuerelement (per Default mit Dock=Top) und einem Panel-Steuerelement mit Dock=Fill, das zwei Buttons enthält. Beim Verschieben der Symbolleiste wird die DockEigenschaft geändert. Die Größe des Panel-Steuerelements passt sich automatisch an (zumindest meistens, siehe unten).
Abbildung 15.32: Die Symbolleiste kann mit der Maus verschoben werden
822
15 Gestaltung von Benutzeroberflächen
Das erste Problem tritt bei der Initialisierung der Drag&Drop-Operation aus. In den MouseDown- bzw. MouseMove-Ereignisprozeduren lässt sich nämlich nicht feststellen, ob die Maus gedrückt wurde, um einen Button der Symbolleiste anzuklicken oder um die Symbolleiste zu verschieben (üblicherweise durch einen Klick außerhalb der Buttons). Die hier gewählte Vorgehensweise ist ein Kompromiss: Indem DoDragDrop in MouseMove erst frühestens 250 ms nach dem erstmaligen Drücken der linken Maustaste ausgeführt wird, sollte die Drag&Drop-Operation gewöhnlichen Button-Klicks nicht in die Quere kommen. Ärgerlich ist aber, dass eine Drag&Drop-Operation auch über einem Button initiiert werden kann. Das sieht für den Anwender so aus, als würde er nur den Button und nicht die ganze Symbolleiste verschieben. Ich habe aber leider keine Lösung für das Problem gefunden. Der Toolbar-Klasse fehlt leider eine Methode, um anhand der Mausposition den darunter befindlichen Button zu ermitteln. (Die von Control vererbte Methode GetChildAtPoint kann nicht angewandt werden, weil ToolBarButtons nicht als Steuerelemente, sondern nur als Komponenten gelten.) ' Beispiel benutzeroberflaeche\drag-and-drop-toolbar Dim mouseDownTime As Date = Now ' Toolbar verschieben (nur wenn kein Button angeklickt wurde) Private Sub ToolBar1_MouseDown(...) Handles ToolBar1.MouseDown If e.Button = MouseButtons.Left Then mouseDownTime = Now End Sub Private Sub ToolBar1_MouseMove(...) Handles ToolBar1.MouseMove If e.Button = MouseButtons.Left And _ (Now > mouseDownTime.AddMilliseconds(250)) Then ToolBar1.DoDragDrop(ToolBar1, DragDropEffects.Move) End If End Sub
In der DragEnter-Ereignisprozedur wird getestet, ob es sich bei den Drag&Drop-Daten um ein ToolBar-Objekt handelt. In diesem Fall wird der Maus-Cursor mit e.Effect=Move auf das Verschiebesymbol eingestellt. Bei den DragOver und DragDrop-Ereignissen wird jeweils derselbe Code ausgeführt: Falls sich die Maus im linken, rechten, oberen oder unteren Viertel des Panels befindet, wird die Toolbar durch eine Veränderung der Dock-Eigenschaft dorthin verschoben. (Die Reaktion auf die Drag&Drop-Operation wird hier also sofort sichtbar, nicht erst beim Loslassen der Maustaste.) Durch die Veränderung von ToolBar1.Dock wird die Größe des Panels (Dock=Fill) normalerweise automatisch angepasst. Wenn die Toolbar von einer Seite nach unten verschoben wird, funktioniert diese automatische Anpassung aus unerklärlichen Gründen aber nicht. (Es handelt sich hier wohl um einen Fehler in der Dock-Logik.) Um den Fehler zu umgehen, wird die Symbolleiste vorrübergehend am oberen Fensterende angedockt.
15.12 Drag&Drop
' visuelles Private Sub ByVal e Handles
823
Feedback Panel1_DragEnter(ByVal sender As Object, _ As System.Windows.Forms.DragEventArgs) _ Panel1.DragEnter
If e.Data.GetDataPresent(ToolBar1.GetType()) Then e.Effect = DragDropEffects.Move End If End Sub ' Drag&Drop-Operation bereits hier ausführen Private Sub Panel1_DragOver(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles Panel1.DragOver Dim pt As Point = Panel1.PointToClient(Me.MousePosition) ' wenn es mehrere ToolBars gibt, muss mit e.Data.GetData(...) ' differentiert werden, welche Toolbar verschoben wird If e.Data.GetDataPresent(ToolBar1.GetType()) Then If pt.Y < Panel1.ClientSize.Height / 4 Then ToolBar1.Dock = DockStyle.Top ElseIf pt.Y > (Panel1.ClientSize.Height * 0.75) Then ' umgeht einen Fehler bei der Logik von Panel.Dock=Fill If ToolBar1.Dock = DockStyle.Left Or _ ToolBar1.Dock = DockStyle.Right Then ToolBar1.Dock = DockStyle.Top End If ToolBar1.Dock = DockStyle.Bottom ElseIf pt.X < Panel1.ClientSize.Width / 4 Then ToolBar1.Dock = DockStyle.Left ElseIf pt.X > (Panel1.ClientSize.Width * 0.75) Then ToolBar1.Dock = DockStyle.Right End If End If End Sub Private Sub Panel1_DragDrop(...) Handles Panel1.DragDrop Panel1_DragOver(sender, e) End Sub
15.12.3 Beispiel – Datei-Drop aus dem Windows-Explorer Im Beispielprogramm können zuvor im Windows-Explorer markierte Dateien per Drag&Drop abgelegt werden. Die Dateinamen werden im Listenfeld im oberen Teil des Fensters angezeigt. Wenn Sie einen Dateinamen anklicken, wird der Inhalt der Datei im unteren Fensterabschnitt angezeigt. (Das funktioniert nur dann zufriedenstellend, wenn es sich um UTF8-Textdateien handelt.)
824
15 Gestaltung von Benutzeroberflächen
Abbildung 15.33: Drag&Drop aus dem Windows-Explorer
Bei Datei-Drag&Drop-Ereignissen enthält e.Data ein Objekt des Typs DataFormats.FileDrop. Wenn derartige Daten in der DragEnter-Ereignisprozedur festgestellt werden, wird e.Effect = Link ausgeführt. (Die Maus wird dann mit einem runden Pfeil dargestellt.) An die DragDrop-Ereignisprozedur wird mit e.Data ein String-Feld übergeben. Die darin enthaltenen Namen werden in das Listenfeld eingetragen. Sobald ein Eintrag des Listenfelds ausgewählt wird, versucht die SelectedIndexChanged-Ereignisprozedur die Datei in das Textfeld zu laden. ' Beispiel benutzeroberflaeche\drag-and-drop-explorer ' Feedback für FileDrop-Ereignisse Private Sub Form1_DragEnter(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles MyBase.DragEnter If e.Data.GetDataPresent(DataFormats.FileDrop) Then e.Effect = DragDropEffects.Link End If End Sub ' Dateinamen des FileDrop-Ereignisses in ListBox einfügen Private Sub Form1_DragDrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles MyBase.DragDrop Dim filenames(), s As String If e.Data.GetDataPresent(DataFormats.FileDrop) Then filenames = CType(e.Data.GetData(DataFormats.FileDrop), _ String()) For Each s In filenames ListBox1.Items.Add(s) Next End If End Sub
15.12 Drag&Drop
825
' die in der Listbox angezeigte Datei im Textformat anzeigen Private Sub ListBox1_SelectedIndexChanged(...) Handles _ ListBox1.SelectedIndexChanged If IsNothing(ListBox1.SelectedItem) Then Exit Sub Dim sr As IO.StreamReader Try sr = New IO.StreamReader(ListBox1.SelectedItem.ToString) TextBox1.Text = sr.ReadToEnd() sr.Close() Catch If Not IsNothing(sr) Then sr.Close() End Try End Sub
15.12.4 Drag&Drop zwischen Listenfeldern Die Idee für dieses Beispiel ist ausgesprochen einfach: Zuvor ausgewählte Listeneinträge sollten von einem Listenfeld in das andere per Drag&Drop verschoben bzw. mit Strg kopiert werden. Die Realisierung dieser Idee hat mich allerdings einen ganzen Arbeitstag gekostet, das Ergebnis ist dennoch nur teilweise zufriedenstellend. Das Problem besteht darin, dass durch das Auslösen einer Drag&Drop-Aktion in der MouseDown-Prozedur beim Drücken der linken Maustaste offensichtlich die interne Verwaltung der ausgewählten Listeneinträge durcheinander kommt. Wenn in der Folge versucht wird, mit SelectedItems oder SelectedIndices die ausgewählten Einträge zu ermitteln, tritt ein interner Fehler auf (Der Index war außerhalb des Arraybereichs).
Abbildung 15.34: Drag&Drop zwischen zwei Listenfeldern
Alle Versuche, das Problem zu umgehen, indem DoDragDrop zeitverzögert in MouseMove ausgeführt wurde, sind gescheitert. In der hier präsentierten Form muss Drag&Drop daher mit der rechten Maustaste ausgeführt werden. Das ist aber nicht intuitiv. Insbesondere
826
15 Gestaltung von Benutzeroberflächen
müssen die Listeneinträge zuerst mit der linken Maustaste angeklickt werden, bevor sie mit der rechten Maustaste verschoben werden können. Damit Sie auch die auftretenden Fehler testen können, bietet das Auswahlkästchen die Möglichkeit, Drag&Drop-Operationen auch mit der linken Maustaste zu veranlassen. Diese Vorgänge werden dann nur sporadisch ausgeführt (wobei aber noch keine Fehlermeldung erscheint). Mit dem Button TEST können sie die ausgewählten Listeneinträge in das darunter befindliche Textfeld kopieren. Dabei tritt dann der oben beschriebene Fehler auf. (Vielleicht wird das Problem in künftigen Service-Packs ja auch behoben ...)
Programmcode Die Klassenvariable dragndropButton gibt an, welche Maustaste für Drag&Drop-Operationen verwendet werden soll. Die Variable wird in CheckBox1_CheckedChanged eingestellt. ' Beispiel benutzeroberflaeche\drag-and-drop-listbox Dim dragndropButton As MouseButtons = MouseButtons.Right ' Drag and Drop mit rechter oder mit linker Maustaste Private Sub CheckBox1_CheckedChanged(...) Handles CheckBox1.CheckedChanged If CheckBox1.Checked Then dragndropButton = MouseButtons.Right Else dragndropButton = MouseButtons.Left End If End Sub
Die Drag&Drop-Operation startet in der MouseDown-Prozedur, sofern die richtige Maustaste gedrückt wird und vorher mindestes ein Listeneintrag ausgewählt wurde. Beachten Sie, dass die Prozedur gleichermaßen für ListBox1 und -2 verwendet wird. (Die Prozedurdefinition wurde für ListBox1 eingefügt. Anschließend wurde im Editor der Name und die Ereignisliste nach Handles verändert.) Private Sub ListBoxes_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles ListBox1.MouseDown, ListBox2.MouseDown Dim lb As ListBox If e.Button = dragndropButton Then lb = CType(sender, ListBox) If lb.SelectedItems.Count > 0 Then lb.DoDragDrop(lb, DragDropEffects.Move Or _ DragDropEffects.Copy) End If End If End Sub
15.12 Drag&Drop
827
Die Ereignisse DragEnter und DragOver für ListBox1 und -2 werden ebenfalls in einer einzigen Prozedur verarbeitet. Je nach Zustand von Strg wird die Maus als Verschiebe- oder Kopiersymbol dargestellt. Das Verschieben ist nur möglich, wenn Empfänger und Sender unterschiedliche ListBox-Steuerelement sind. Private Sub ListBoxes_DragEnterOver(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListBox1.DragEnter, ListBox2.DragEnter, _ ListBox1.DragOver, ListBox2.DragOver If e.Data.GetDataPresent(GetType(ListBox)) Then If (Me.ModifierKeys And Keys.Control) = Keys.Control Then ' Kopieren ist immer erlaubt e.Effect = DragDropEffects.Copy ElseIf Not (sender Is e.Data.GetData(GetType(ListBox))) Then ' Verschieben ist nur sinnvoll, wenn Quell- und Zielbox ' unterschiedlich sind e.Effect = DragDropEffects.Move End If End If End Sub
In der DragDrop-Ereignisprozedur werden nach den üblichen Validitätstests in einer Schleife alle markierten Listeneinträge des Quelllistenfelds durchlaufen. Die Einträge werden in das Ziellistenfeld eingefügt und (nur bei Verschiebeoperationen) im Quelllistenfeld gelöscht. Private Sub ListBoxes_DragDrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListBox1.DragDrop, ListBox2.DragDrop Dim i, n As Integer Dim lbSource, lbDestination As ListBox ' kopieren oder verschieben? Dim copyItems As Boolean = _ (Me.ModifierKeys And Keys.Control) = Keys.Control If e.Data.GetDataPresent(GetType(ListBox)) Then lbDestination = CType(sender, ListBox) lbSource = CType(e.Data.GetData(GetType(ListBox)), ListBox) If lbSource.SelectedIndices.Count > 0 Then For i = lbSource.SelectedIndices.Count - 1 To 0 Step -1 ' Listeneinträge von Source --> Destination kopieren n = lbSource.SelectedIndices(i) lbDestination.Items.Add(lbSource.Items(n)) ' Listeneinträge löschen If Not copyItems Then lbSource.Items.Remove(lbSource.Items(n))
828
15 Gestaltung von Benutzeroberflächen
End If Next End If End If End Sub
Die Prozedur zum Test der SelectedItems- und SelectedIndices-Eigenschaften durchläuft alle ausgewählten Einträge der beiden Listenfelder, bei ListBox1 mit einer For-Each-Schleife und bei ListBox2 mit einer Schleife über alle SelectedIndices(i). Private Sub Button1_Click(...) Handles Button1.Click Dim i, n As Integer Dim o As Object ' Zugriff auf alle ausgewählten Einträge von Liste 1 TextBox1.Clear() For Each o In ListBox1.SelectedItems TextBox1.AppendText(o.ToString + " ") Next ' Zugriff auf alle ausgewählten Einträge von Liste 2 TextBox1.AppendText(vbCrLf + vbCrLf) If ListBox2.SelectedIndices.Count > 0 Then For i = 0 To ListBox2.SelectedIndices.Count - 1 n = ListBox2.SelectedIndices(i) TextBox1.AppendText(ListBox2.Items(n).ToString + " ") Next End If End Sub
Drag and Drop zwischen ListView-Steuerelementen Nachdem sich der Umgang mit ListBox-Feldern also als mühsam herausgestellt hat, habe ich versucht, das Beispiel mit ListView-Feldern durchzuführen. Und siehe da: Jetzt klappt es beinahe ohne Probleme (siehe Abbildung 15.35). Auch der Code ist um einiges übersichtlicher (wobei das Programm auch insofern vereinfacht wurde, als die Einträge jetzt nur noch kopiert, nicht mehr verschoben werden können). Die Drag&Drop-Operation wird in der ItemDrag-Ereignisprozedur gestartet. An DoDragDrop wird ein String-Feld mit den Texten der zu kopierenden Einträge übergeben. In der DragEnter-Prozedur wird getestet, ob die übergebenen Daten tatsächlich das gewünschte Format (also ein String-Feld) aufweisen. In DragDrop werden die Daten dann in das entsprechende Listenfeld eingefügt.
15.12 Drag&Drop
Abbildung 15.35: Drag&Drop zwischen zwei ListView-Steuerelementen
' Beispiel benutzeroberflaeche\drag-and-drop-listview Private Sub ListViews_ItemDrag(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ItemDragEventArgs) _ Handles ListView1.ItemDrag, ListView2.ItemDrag Dim i As Integer Dim lv As ListView = CType(sender, ListView) Dim lvitem As ListViewItem Dim items(lv.SelectedItems.Count - 1) As String For Each lvitem In lv.SelectedItems items(i) = lvitem.Text i += 1 Next lv.DoDragDrop(items, DragDropEffects.Copy) End Sub Private Sub ListViews_DragEnter(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListView1.DragEnter, ListView2.DragEnter Dim items(0) As String If e.Data.GetDataPresent(items.GetType()) Then e.Effect = e.AllowedEffect End If End Sub Private Sub ListViews_Dragdrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListView1.DragDrop, ListView2.DragDrop Dim s, items(0) As String Dim lv As ListView = CType(sender, ListView)
829
830
15 Gestaltung von Benutzeroberflächen
If e.Data.GetDataPresent(items.GetType()) Then items = CType(e.Data.GetData(items.GetType()), String()) For Each s In items lv.Items.Add(s) Next End If End Sub
16 Grafikprogrammierung (GDI+) Dieses ziemlich lange Kapitel führt in die Grafikprogrammierung mit GDI+ ein. GDI+ steht für Graphics Device Interface und ist der Teil der .NET-Bibliothek, der für die Ausgabe von Linien, Mustern etc. in Fenstern, Steuerelementen und am Drucker verantwortlich ist. 16.1 16.2 16.3 16.4 16.5
Einführung Elementare Grafikoperationen Text ausgeben (Font-Klassen) Bitmaps, Icons und Metafiles Interna und spezielle Programmiertechniken
832 840 867 894 915
832
16 Grafikprogrammierung (GDI+)
16.1
Einführung
Die Grafikprogrammierung in VB.NET basiert auf der .NET-Bibliothek System.Drawing (Datei System.Drawing.dll). Diese Bibliothek gibt in .NET den Zugang zum neuen GDI+ System. GDI steht für Graphics Device Interface und umfasst Funktionen zur Ausgabe und Verwaltung zweidimensionaler Grafiken (Punkte, Linien, Rechtecke, Vielecke etc.), zum Umgang mit Text und Schriftarten, zur Manipulation von Bitmaps, zur Erzeugung von Metafiles (Dateien mit Grafikobjekten) etc.
VERWEIS
GDI+ würde genug Stoff geben, um ein eigenes Buch zu schreiben. Da an dieser Stelle aber nur Platz für ein Kapitel ist, beschränkt sich dieses Kapitel auf die Grundbegriffe, auf eine Einführung in die wichtigsten Klassen sowie auf eine Reihe von Beispielprogrammen. Das Kapitel schafft damit ein Fundament, das Ihnen die weitere Erforschung von GDI+ erleichtern soll. Natürlich sind alle Klassen der System.Drawing-Bibliothek sowie all ihre Methoden, Eigenschaften etc. im Rahmen der Online-Hilfe dokumentiert. Darüber hinaus enthält die mitgelieferte Dokumentation aber eine systematische Einführung in GDI+, die durchzublättern schon allein wegen der vielen Abbildungen lohnt! (Die Programmbeispiele sind allerdings in C++, also für VB.NET-Programmierer nur eingeschränkt hilfreich.) MSDN-LIBRARY|GRAFIK UND MULTIMEDIA|GDI+ ms-help://MS.VSCC/MS.MSDNVS.1031/gdicpp/cpp_gdi+start_26ic.htm
Überblick über die System.Drawing-Bibliothek Die System.Drawing-Bibliothek besteht aus mehreren Namensräumen, die in der folgenden Tabelle zusammengefasst sind. Namensräume der Bibliothek System.Drawing System.Drawing
Grundfunktionen zur Grafikprogrammierung
System.Drawing.Design
Elemente für Benutzeroberflächen von Grafikprogrammen (Font-Editor, Bitmap-Editor etc.)
System.Drawing.Drawing2D
Funktionen zum Zusammensetzen von Grafikobjekten, für Matrixtransformationen etc.
System.Drawing.Imaging
Spezialfunktionen zum Lesen und Schreiben von MetafileDateien und zur Bearbeitung von Bitmaps
System.Drawing.Printing
Ausdruck von Grafik und Text
System.Drawing.Text
Funktionen zur Textausgabe und zur Verwaltung von Schriftarten
16.1 Einführung
833
In diesem Kapitel steht der System.Drawing-Namensraum im Mittelpunkt. Klassen der anderen Namensräume werden aus Platzgründen nur ansatzweise beschrieben. Informationen zum Ausdruck von Daten stehen im Mittelpunkt von Kapitel 17. Dort werden Sie sehen, dass es viele Ähnlichkeiten zwischen der Ausgabe am Bildschirm (d.h. in einem Formular oder PictureBox-Steuerelement) und der Ausgabe am Drucker gibt.
Verwendung der System.Drawing-Bibliothek Damit Sie die System.Drawing-Bibliothek in Ihrem Programm benutzen können, muss das Projekt einen Verweis darauf enthalten. Beim Projekttyp VISUAL BASIC PROJEKT|WINDOWSANWENDUNG ist das automatisch der Fall, bei anderen Projekttypen kann es aber sein, dass Sie diesen Verweis selbst einrichten müssen (PROJEKT|VERWEIS HINZUFÜGEN). Bei einer VB.NET-Windows-Anwendung gilt außerdem per Default der Import System.Drawing (siehe PROJEKT|EIGENSCHAFTEN|ALLG. EIGENSCHAFTEN|IMPORTE). Deswegen können Sie im Code alle Klassen von System.Drawing unmittelbar ansprechen, ohne jedes Mal System.Drawing voranzustellen. Der gesamte Code in diesem Kapitel setzt diesen Defaultimport voraus. Wenn das nicht der Fall ist, müssen Sie am Beginn der Codedatei Imports System.Drawing einfügen.
16.1.1 Ein erstes Beispiel Das folgende Programm zeichnet einige einfache geomentrische Formen direkt in ein Formular. Die Programmausführung beginnt in Form1_Load, wo das Point-Feld pts mit einigen Zufallskoordinaten initialisiert wird. (Diese Initialisierung kann nicht in Form1_Paint erfolgen, weil das daraus resultierende Polygon sonst ständig seine Form ändern würde.) Die Ereignisprozedur Form1_Paint wird in der Folge immer dann aufgerufen, wenn der Fensterinhalt neu gezeichnet werden soll. Das ist ziemlich oft der Fall: jedes Mal, wenn verdeckte Teile des Fensters wieder sichtbar werden, wenn die Größe des Fensters verändert wird etc. Die Grundidee von Grafik in VB.NET-Formularen lautet so: Einmal ausgegebene Grafiken werden nicht gespeichert (etwa in einer Bitmap). Stattdessen wird das Programm jedes Mal, wenn Teile der Grafik neu gezeichnet werden müssen, durch eine Ereignisprozedur benachrichtigt. Das Programm muss also zu jedem Zeitpunkt in der Lage sein, den Fensterinhalt neu zu zeichnen. An die Paint-Ereignisprozedur werden zwei Parameter übergeben, von denen hier aber nur e interessant ist. Diese Variable (Klasse PaintEventArgs) enthält Informationen darüber, was eigentlich neu gezeichnet werden soll: •
e.Graphics verweist auf ein System.Drawing.Graphics-Objekt. Alle Grafikmethoden müssen auf dieses Objekt angewandt werden. Um die ständige Wiederholung von e.Graphics zu vermeiden, verwendet das Beispielprogramm die Variable g als Platzhalter.
(Für alle, die sich schon mit dem Vorgänger von GDI+, also mit GDI beschäftigt haben:
834
16 Grafikprogrammierung (GDI+)
Das Graphics-Objekt hat in GDI+ eine vergleichbare Funktion wie der Device Context (DC) in GDI.) •
e.ClipRectangle verweist auf ein System.Drawing.Rectangle-Objekt, das angibt, welche
Bereiche des Fensters neu gezeichnet werden müssen. Diese Information wird oft (wie im Beispielprogramm) ignoriert. Stattdessen wird einfach alles neu gezeichnet. Nähere Informationen zur Bedeutung von ClipRectangle folgen in Abschnitt 16.5.3. Der Rest des Beispielprogramms ist relativ einfach nachzuvollziehen: Alle Grafikmethoden setzen ein System.Drawing.Graphics-Objekt voraus, das durch die Variable gr gegeben ist. Außerdem muss bei den meisten Grafikmethoden eine Zeichenfarbe durch ein PenObjekt oder eine Füllfarbe (oder ein Füllmuster) durch ein Brush-Objekt angegeben werden. Daher werden zwei entsprechende Objekte erzeugt: ein blauer Zeichenstift mit einer Breite von zwei Pixeln sowie ein rotes Füllmuster. Das Koordinatensystem beginnt mit (0,0) im linken oberen Eck des Formulars. Als Einheit werden Pixel verwenden. (Es besteht die Möglichkeit, andere Koordinatensysteme zu definieren – siehe Abschnitt 16.2.5.) An die Methoden DrawRectangle, DrawEllipse und FillRectangle müssen neben dem Pen- oder Brush-Objekt noch die Ausmaße des Objekts in der Form x, y, breite, höhe angegeben werden. An FillPolygon muss stattdessen ein Point-Feld mit den Koordinaten der Eckpunkte übergeben werden. (Beachten Sie, dass es zu all diesen Grafikmethoden zahlreiche Syntaxvarianten gibt, von denen einige im weiteren Verlauf dieses Kapitels vorgestellt werden.)
Abbildung 16.1: Das erste Grafikprogramm
' Beispiel grafik\intro Public Class Form1 Inherits System.Windows.Forms.Form [... "Vom Windows Form Designer generierter Code" ...] ' Punkte für Linienzug definieren Dim pts(4) As Point Private Sub Form1_Load(...) Handles MyBase.Load Dim i As Integer Dim myrand As New Random()
16.1 Einführung
835
For i = 0 To 4 pts(i).X = 150 + myrand.Next(0, 100) pts(i).Y = 10 + myrand.Next(0, 100) Next End Sub Private Sub ByVal ByVal Handles
Form1_Paint( _ sender As Object, _ e As System.Windows.Forms.PaintEventArgs) _ MyBase.Paint
TIPP
' alle Grafikausgaben erfolgen via gr Dim gr As Graphics = e.Graphics 'blauer Zeichenstift, Linienbreite 2 Pixel Dim mypen As New Pen(Color.Blue, 2) 'rote Farbe zum Füllen Dim mybrush As New SolidBrush(Color.Red) ' einige einfache geometrische Objekte gr.DrawRectangle(mypen, 10, 10, 50, 20) gr.DrawEllipse(mypen, 50, 50, 100, 100) gr.FillRectangle(mybrush, 10, 40, 30, 30) gr.FillPolygon(mybrush, pts) End Sub End Class
Die Fehlersuche in Paint-Ereignisprozeduren ist ziemlich schwierig. Wenn Sie beispielsweise einen Haltepunkt innerhalb der Paint-Prozedur setzen, dann lösen Sie durch das Erscheinen der Entwicklungsumgebung ein neues Paint-Ereignis aus. Ein relativ einfaches Hilfsmittel, um dies zu vermeiden, ist die Eigenschaft TopMost des Formulars. Wenn Sie diese Eigenschaft auf True setzen, bleibt das Formular gegenüber der Entwicklungsumgebung immer im Vordergrund. (Ein Paint-Ereignis können Sie dennoch leicht auslösen, z.B. indem Sie das Programmfenster in ein Icon verkleinern und dann wieder vergrößern.)
16.1.2 Grafik-Container (Form, PictureBox) Im obigen Beispielprogramm erfolgten alle Grafikausgaben direkt in ein (ansonsten leeres) Formular. Diese Vorgehensweise werden Sie zwar auch bei den meisten weiteren Beispielen dieses Kapitels bemerken, aber in der Praxis ist sie eher ungewöhnlich: Da besteht ein Formular meist aus mehreren Steuerelementen zur Bedienung des Programms; die Grafikausgaben erfolgen nur in einem abgegrenzten Bereich des Formulars. Im Regelfall wird dazu ein PictureBox-Steuerelement verwendet. Dieses Steuerelement zeichnet sich durch dieselben Paint-Ereignisse aus wie das Formular. e.Graphics verweist nun aber auf ein anderes Graphics-Objekt, das nur den Inhalt des PictureBox-Steuerelements
836
16 Grafikprogrammierung (GDI+)
betrifft. Mit anderen Worten: Der Programmcode sieht exakt gleich aus wie im obigen Beispielprogramm, nur hat die Paint-Ereignisprozedur nun den Namen PictureBox1_Paint (oder wie immer Sie das PictureBox-Steuerelement genannt haben). Bemerkenswert (und neu im Vergleich zu VB6) ist der Umstand, dass auch viele andere Steuerelemente als Grafik-Container verwendet werden können. In Abbildung 16.2 sehen Sie links oben ein PictureBox-Steuerelement, rechts oben einen Button, links unten einen Textlabel und rechts unten ein Textfeld. Alle vier Steuerelemente wurden mit derselben Paint-Ereignisprozedur ausgestattet wie das obige Beispielprogramm. (Den vollständigen Programmcode finden Sie auf der beiliegenden CD, Beispiel grafik\container.) Während die Ereignisprozedur beim Textfeld wirkungslos war, führte sie bei den anderen Steuerelementen dazu, dass die Grafik korrekt angezeigt wurde. Beachten Sie auch, dass die Grafikausgabe am Ende des Steuerelements abgeschnitten wird. Sie brauchen in Ihrem Code also keine Angst haben, dass Sie womöglich über den Rand hinauszeichnen könnten.
Abbildung 16.2: Grafikausgaben in verschiedenen Steuerelementen
16.1.3 Dispose für Grafikobjekte Grundsätzlich ist die Regel einfach (siehe Abschnitt 4.6.2): Wenn Sie ein neu erzeugtes Objekt nicht mehr benötigen und dessen Klasse eine Dispose-Methode vorsieht, dann sollten Sie diese aufrufen. Damit erreichen Sie, dass die Objekte nicht länger als notwendig im Speicher bleiben. Wenn Sie sich die Online-Hilfe zur Schnittstelle IDisposable ansehen, werden Sie feststellen, dass nahezu alle Klassen, die in diesem Kapitel vorgestellt werden, diese Schnittstelle implementieren. Die folgende Tabelle nennt einige Beispiele: Graphics (Klasse zur Durchführung von Grafikoperationen) Brush (Füllmuster; inklusive SolidBrush, Drawing2D.HatchBrush, TextureBrush etc.) Pen (Zeichenstift) Image (Bild speichern; inklusive der davon abgeleiteten Klassen Bitmap und Imaging.Metafile)
16.1 Einführung
837
Icon (kleine Bitmap) GraphicsPath (aus Grafikmethoden zusammengesetztes Grafikobjekt) Region (abgegrenztes Gebiet, z.B. für Clipping-Operationen) Font (Schriftart) FontCollection (Aufzählung von Schriftfamilien) FontFamily (Schriftfamilie) StringFormat (Klasse zur Steuerung des Schriftlayouts) Matrix (zweidimensionales Parameterfeld, z.B. für Transformationen)
Wenn Sie sich konsequent an die oben erwähnte Dispose-Regel halten, dann müsste jede Grafikprozedur mit unzähligen obj.Dispose-Anweisungen enden. Tatsächlich werden Sie aber feststellen, dass sich weder dieses Buch noch die meisten anderen VB.NET-Bücher an diese Regel halten. Auch die Beispiele der Online-Hilfe bieten diesbezüglich kein besseres Vorbild. Sind also Buch- wie Hilfeautoren gleichermaßen schlampig, oder ist Dispose doch nicht so wichtig, wie Abschnitt 4.6.2 vermuten lässt? Um eine seriöse Antwort auf diese Frage geben zu können, habe ich ein paar Tests durchgeführt, die das Speicherverhalten im ungünstigsten Fall simulieren. Die Beispiele sind zwar nicht praxisnah, aber sie stellen eine Art Worst-Case-Szenario dar. In einem ersten Test wurden eine Million Brush-Objekte erzeugt, wobei der schon einigermaßen aufwendige LinearGradientBrush verwendet wird. (Details zu den hier vorgestellten Grafikklassen folgen im weiteren Verlauf dieses Kapitels. Hier geht es nur um die Speicherverwaltung.) Die Schleife wurde probeweise mit und ohne Dispose ausgeführt. Während der Codeausführung habe ich den Speicherbedarf des Programms im Task-Manager verfolgt. Das Ergebnis: Mit br.Dispose wird der Code beinahe doppelt so schnell ausgeführt. Der Speicherbedarf des Programms ist konstant, d.h., über den bereits beim Start des Programms allozierten Speicher wird (fast) kein zusätzlicher Speicher mehr benötigt. Ohne Dispose variiert der zusätzliche Speicherbedarf des Programms um ca. ein MByte. Die zahlreichen Brush-Objekte werden also regelmäßig durch die automatische garbage collection wieder freigegeben, beanspruchen aber vorübergehend doch spürbar Speicher. ' Beispiel grafik\dispose-test Private Sub Button2_Click(...) Handles Button2.Click Dim br As Brush, i As Integer For i = 0 To 1000000 br = New Drawing2D.LinearGradientBrush( _ New Point(0, 0), New Point(100, 100), Color.Red, Color.Blue) ' br.Dispose() Next End Sub
In einem zweiten Test habe ich innerhalb der Schleife ein Graphics-Objekt erzeugt, um damit in den Hintergrund eines Formulars zu zeichnen. Die Ergebnisse (Geschwindigkeitsunterschied, variabler Speicherverbrauch) waren sehr ähnlich, allerdings variierte der
838
16 Grafikprogrammierung (GDI+)
Gesamtspeicherverbrauch nun schon um mehrere MByte. (Die Methode FromHwnd wird übrigens in Abschnitt 16.4.1 vorgestellt.) Dim gr As Graphics ... gr = Graphics.FromHwnd(Me.Handle)
In einem dritten Test habe ich eine Million Bitmap-Objekte erzeugt, die je 100*100 Pixel groß waren. Mit Dispose erledigte VB.NET diese Aufgabe innerhalb weniger Sekunden, der Speicherbedarf des Programms war gering. Ohne Dispose war der Experimentverlauf stark Hardware-abhängig. Auf meinem Notebook (Pentium II 333 MHz, 196 MByte RAM) geriet das Experiment außer Kontrolle: Der Speicherbedarf stieg vorübergehend auf bis zu 500 MByte an; zwar wurde der Speicher immer wieder durch eine automatische garbage collection freigegeben, aber da der Speicherbedarf den physikalisch verfügbaren Speicher überschritt, wurde das Programm extrem langsam und musste schließlich gewaltsam beendet werden. Auf meinem gewöhnlichen Arbeitsrechner (AMD Athlon 1400 MHz, 768 MByte RAM) hatte ich mehr Glück: Der Speicherbedarf des Programms erreichte zwar auch dort ca. 500 MByte, die garbage collection setzte hier aber automatisch ein, bevor das System Arbeitsspeicher auslagern musste. Dramatisch war aber der Geschwindigkeitsvorteil, der sich durch die richtige Anwendung von Dispose ergab: Die Schleife ohne Dispose beanspruchte ca. 2:40 Minuten, die Variante mit Dispose nur 0:17! Dim bm As Bitmap ... bm = New Bitmap(100, 100)
Dispose dürfen Sie nicht verwenden, wenn ... Bevor Sie nun blindlings am Ende jeder Ihrer Grafikprozeduren für alle Objektvariablen Dispose ausführen, noch eine Warnung: Dispose darf nur dann ausgeführt werden, wenn Sie das Grafikobjekt erzeugt haben. Wenn das Grafikobjekt dagegen bereits existiert und Sie darauf in einer Variablen nur verweisen, ist Dispose nicht erlaubt! Die folgenden Beispiele sollen diesen Sachverhalt näher erläutern. Beispiel 1: GDI+ kennt eine ganze Reihe fertiger Objekte, die Sie direkt verwenden können – beispielsweise die Muster Brushes.Black, Brushes.Blue, Brushes.Green etc., die Zeichenstifte Pens.Yellow, Pens.White etc. Wenn Sie diese Objekte zur Initialisierung einer Objektvariablen verwenden, dürfen Sie Dispose nicht ausführen! Im folgenden Beispiel verweist br auf das vorgefertige Objekt Brushes.Black. Die Anweisung br.Dispose löscht das Objekt Brushes.Black! Beim nächsten Versuch, dieses Objekt zu verwenden (also beim zweiten Aufruf von Form1_Paint) erhalten Sie die wenig hilfreiche Fehlermeldung: Eine nicht behandelte Ausnahme des Typs 'System.ArgumentException' ist in system.windows.forms.dll aufgetreten. Zusätzliche Informationen: Ungültiger Parameter verwendet. In der
16.1 Einführung
839
Entwicklungsumgebung markiert die erste Zeile der Klasse des Formulars, was die Eingrenzung des Fehlers zusätzlich erschwert. Public Class Form1 ... Private Sub Form1_Paint(...) Handles MyBase.Paint Dim br As Brush = Brushes.Black e.Graphics.FillRectangle(br, 0, 0, 100, 100) br.Dispose() 'falsch!! End Sub End Class
Beispiel 2: In diesem Beispiel wird die Defaultschriftart des Formulars (Me.Font) verwendet, um einen Text anzuzeigen. fnt zeigt auf diese Schriftart. Natürlich ist Dispose auch hier unzulässig, weil damit die Schriftart des Formulars (die bis zum Programmende benötigt wird) gelöscht wird! Private Sub Form1_Paint(...) Handles MyBase.Paint Dim fnt As Font = Me.Font e.Graphics.DrawString("abc", fnt, Brushes.Black, 0, 0) fnt.Dispose() 'falsch!! End Sub
Beispiel 3: Ein wenig diffiziler ist das letzte Beispiel. In den meisten Paint-Prozeduren in diesem Kapitel finden Sie die folgende Zeile: Dim gr As Graphics = e.Graphics
Darf, soll bzw. muss nun am Ende der Prozedur gr.Dispose() ausgeführt werden? Die Frage ist insofern nicht ganz trivial, weil nicht offensichtlich ist, ob das Graphics-Objekt nur für die Paint-Prozedur erzeugt wurde, oder ob es anschließend auch intern noch benötigt wird. (Dokumentiert ist das natürlich auch nicht.) In solchen Fällen ist es immer gut, das Ganze einfach einmal auszuprobieren. Dabei zeigt sich, dass gr.Dispose() in der Paint-Prozedur eines Formulars anscheinend nicht schadet (keine Fehlermeldung). Wenn Sie gr.Dispose() aber in der Paint-Prozedur eines PictureBoxSteuerelements ausführen, kommt es zur schon bekannten Fehlermeldung. Offensichtlich wollte VB.NET auf das Graphics-Objekt noch zugreifen, nachdem Sie es gelöscht haben. Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics gr.DrawLine(Pens.Black, 0, 0, 100, 100) gr.Dispose() 'falsch!! End Sub
840
16 Grafikprogrammierung (GDI+)
Fazit •
Bei Grafikobjekten, die selbst nur relativ wenig Speicher beanspruchen – z.B. bei Penoder gewöhnlichen Brush-Objekten, lohnt sich Dispose nur, wenn Sie wirklich sehr viele derartige Objekte erzeugen. (Wenn Sie Grafikobjekte in einer Schleife erzeugen, sollten Sie die Anwendung von Dispose immer angewöhnen!)
•
Bei Objekten, die sehr viel Speicher beanspruchen (insbesondere bei großen Image-, Bitmap- und Metafile-Objekten, bei TextureBrush-Objekten auf der Basis einer großen Bitmap etc.), ist die Dispose-Methode ein Muss!
•
Bei den restlichen Objekten liegt die Entscheidung für oder wider Dispose in Ihrem Ermessen. Bei den meisten Anwendungen ist der Unterschied weder im Speicherverbrauch noch in der Geschwindigkeit messbar.
•
Dispose darf auf keinen Fall bei Objekten verwendet werden, die Sie nicht selbst erzeugt
haben bzw. die im Programm (eventuell auch intern) noch benötigt werden. Das gilt auch für die Parameter von Ereignisprozeduren. Generell brauchen Sie keine Angst zu haben, dass Ihr Programm unkontrolliert immer mehr Speicher oder Ressourcen verbraucht, ohne diesen wieder freizugeben. (Das war bei früheren Windows-Versionen bzw. bei früheren Grafikbibliotheken ein akutes Problem – aber diese Zeiten sind vorbei.) Gefährlich ist nur der variable Speicherverbrauch, bis die automatische garbage collection wieder Platz schafft. Wenn dieser Speicherbedarf auch nur vorübergehend so groß wird, dass Arbeitsspeicher auf die Festplatte ausgelagert werden muss, wird das Programm über die Maßen langsam.
16.2
Elementare Grafikoperationen
Dieser Abschnitt gibt einen Überblick über die wichtigsten Methoden der Klasse System.Drawing.Graphics, mit denen Sie Linien, Rechtecke, Vielecke etc. zeichnen können. Außerdem werden die Klassen Color, Pen[s] und XxxBrush eingehend behandelt. Objekte dieser Klassen benötigen Sie, um Farben, Linienformen und Füllmuster für diverse Grafikmethoden einzustellen.
16.2.1 Linien, Rechtecke, Vielecke, Ellipsen, Kurven (Graphics-Klasse) Alle in diesem Abschnitt beschriebenen Methoden werden auf ein Graphics-Objekt angewendet. Wie der vorige Abschnitt bereits gezeigt hat, erhalten Sie ein derartiges Objekt durch den PaintEventArgs-Parameter der Paint-Ereignisprozedur. Um den Code kompakt zu halten, ist es zumeist sinnvoll, am Beginn der Ereignisprozedur eine Referenz auf das Graphics-Objekt in einer Variable mit kurzem Namen zu speichern (in den folgenden Beispielen gr oder g).
16.2 Elementare Grafikoperationen
841
Private Sub Steuerelement_Paint( _ ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles Steuerelement.Paint Dim gr As Graphics = e.Graphics gr.DrawRectangle(...) ... End Sub
Ein gemeinsames Merkmal fast aller Grafikmethoden besteht darin, dass es jeweils unzählige Syntaxvarianten gibt. Zum Zeichnen eines Rechtecks gibt es beispielsweise die folgenden Möglichkeiten: DrawRectangle(pen As Pen, x As Integer, y As Integer, _ width As Integer, height As Integer) DrawRectangle(pen As Pen, x As Single, y As Single, _ width As Single, height As Single) DrawRectangle(pen As Pen, rect As Rectangle)
HINWEIS
Im ersten Fall wird die Größe des Rechtecks durch vier Integer-Werte ausgedrückt. Stattdessen können Sie die Koordinaten aber auch durch Fließkommazahlen (aus Effizienzgründen nur in Single-Genauigkeit) angeben. Schließlich können Sie das Rechteck auch auf der Basis eines System.Drawing.Rectangle-Objekts zeichnen. Alle Beispiele zu diesem Abschnitt sind in einem einzigen Programm vereint, das Sie auf der beiliegenden CD im Verzeichnis grafik\grafikmethoden finden. Das Programm verwendet ein TabPage-Steuerelement, dessen Dialogblätter verschiedene Effekte demonstrieren. Gezeichnet wird jeweils direkt in ein Dialogblatt dieses Steuerelements (Ereignisprozeduren TabPage1_Paint, TabPage2_Paint etc.). Im Folgenden wird aus Platzgründen immer nur diese eine Ereignisprozedur gedruckt.
Elementare Klassen im System.Drawing-Namensraum Bevor Sie sich mit den Methoden DrawRectangle, DrawLine etc. näher auseinandersetzen, sollten Sie einige allgemeine Klassen des System.Drawing-Namensraum kennen lernen. Objekte dieser Klassen werden häufig als Parameter diverser Grafikmethoden eingesetzt. Die folgende Tabelle zählt die wichtigsten derartige Klassen auf. Dabei handelt es sich durchwegs um ValueType-Datenstrukturen (siehe auch Abschnitt 4.1.2). Color Point PointF Rectangle/RectangleF Size/SizeF
Farbe (.R, .G und .B geben den Rot-, Grün- und Blauanteil an) Koordinatenpunkt (Integer-Koordinaten .X und .Y) Koordinatenpunkt (Single-Koordinaten .X und .Y) Rechteck (Integer/Single-Koordinaten) Größe eines rechteckigen Bereichs (.Width und .Height)
842
16 Grafikprogrammierung (GDI+)
Davon abgeleitete Objekte können als Parameter für verschiedene Grafikmethoden verwendet werden. Die folgenden Zeilen zeichnen ein Rechteck auf der Basis eines RectangleObjekts: ' g ist ein Graphics-Objekt Dim rect As New Rectangle(10, 120, 100, 20) Dim mypen As New Pen(Color.Blue, 2) g.DrawRectangle(mypen, rect)
Zur Rectangle-Klasse gehören einige recht praktische Methoden: So können Sie mit IntersectsWith testen, ob sich zwei Rechtecke überlappen. Contains überprüft, ob sich ein Koordinatenpunkt innerhalb des Rechtecks befindet. Konvertierung Integer/Single: Leider ist GDI+ ziemlich inkonsequent, was den Umgang mit den Integer- und Single-Varianten von Rectangle[F], Point[F] und Size[F] betrifft: Manche Eigenschaften bzw. Methoden liefen nur die Integer-, andere liefern nur die Single-Variante als Ergebnis. Andererseits erwarten manche Methoden genau eine der beiden Varianten als Parameter und oft ist es natürlich gerade die Variante, die momentan nicht zur Verfügung steht. Zur Umwandlung zwischen den beiden Typen gibt es unterschiedliche Methoden, die hier am Beispiel von Rectangle[F]-Objekten demonstriert werden. (Analoge Methoden stehen auch für Point[F] und Size[F] zur Verfügung.) rect = Rectangle.Ceiling(rectf) rect = Rectangle.Round(rectf) rect = Rectangle.Truncate(rectf)
'Single 'Single 'Single
--> Integer: aufrunden --> Integer: runden --> Integer: abrunden
Die Operatoren op_Implicit zur Konvertierung von Integer nach Single sind uneinheitlich definiert (für Point und Size, aber für RectangleF): pntf = Point.op_Implicit(pnt) 'Integer --> Single sizf = Size.op_Implicit(pnt) 'Integer --> Single rectf = RectangleF.op_Implicit(rect) 'Integer --> Single
Zur Konvertierung zwischen unterschiedlichen Objekttypen gibt es den Operator op_Explicit: sz = Point.op_Explicit(pnt) pnt = Size.op_Explicit(sz)
'Point 'Size
--> Size --> Point
Rechtecke und Linien Die Syntaxvarianten der Methode DrawRectangle wurde bereits in der Einführung dieses Abschnitts beschrieben. Wenn Sie mehrere Rechtecke mit derselben Farbe auf einmal zeichnen möchten, können Sie stattdessen DrawRectangles verwenden. An diese Methode muss ein Feld von Rectangle-Strukturen übergeben werden, wie das folgende Beispielprogramm demonstriert.
HINWEIS
16.2 Elementare Grafikoperationen
843
DrawRectangle ist nicht in der Lage, Rechtecke mit negativer Breite oder Höhe zu zeichnen. DrawRectangle(mypen, 50,50,-10,-10) zeichnet also nicht ein Rechteck von
(40,40) nach (50,50), wie man vielleicht hätte erwarten können.
Abbildung 16.3: Rechtecke zeichnen
' Beispiel grafik\grafikmethoden Private Sub TabPage1_Paint( _ ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles TabPage1.Paint Dim i As Integer Dim g As Graphics = e.Graphics ' ein rotes Rechteck Dim rect As New Rectangle(10, 10, 100, 20) Dim mypen As New Pen(Color.Red, 3) g.DrawRectangle(mypen, rect) ' ein grünes Rechteck mypen = New Pen(Color.Green, 3) Dim rectF As New RectangleF(10, 60, 123.5, 49.7) g.DrawRectangle(mypen, Rectangle.Ceiling(rectF)) ' einige blaue Rechtecke mypen = New Pen(Color.Blue, 2) Dim rects(9) As Rectangle For i = 0 To 9 rects(i) = New Rectangle(10 + 10 * i, 120 + 7 * i, 50, 20) Next g.DrawRectangles(mypen, rects) End Sub
844
16 Grafikprogrammierung (GDI+)
Zum Zeichnen von Linien verwenden Sie die Methode DrawLine. Die Koordinatenpunkte des Start- und Endpunkts können als Integer-Werte, als Single-Werte oder durch Point[F]Objekte angegeben werden. Wenn Sie einen ganzen Linienzug zeichnen möchten, setzen Sie stattdessen DrawLines ein. Die Methode erwartet als Parameter ein Feld von Point[F]Objekten. Es zeichnet dann eine Linie, die die Punkte der Reihe nach miteinander verbindet. Der Linienzug wird nicht geschlossen. Das Beispielprogramm zeichnet zuerst ein Muster aus verschiedenfarbigen Linien und dann einen Linienzug (siehe Abbildung 16.4). Hintergrundinformationen zum Umgang mit Farben folgen in Abschnitt 16.2.2.
Abbildung 16.4: Einige Linien
' Beispiel grafik\grafikmethoden Private Sub TabPage2_Paint(...) Handles TabPage2.Paint Dim g As Graphics = e.Graphics ' einige Linien zeichnen Dim i As Double Dim mypen As Pen For i = 0 To 1 Step 0.03333 mypen = New Pen( _ Color.FromArgb(CInt(i * 255), 0, CInt(i * 255))) g.DrawLine(mypen, 0, CSng(300 * i), CSng(300 - 300 * i), 0) mypen.Dispose() Next ' Linienzug zeichnen Dim pts(9) As PointF Dim j As Integer For j = 0 To 9 pts(j).X = 100 + 20 * j pts(j).Y = CSng(100 + Math.Sin(j) * 50) Next mypen = New Pen(Color.Blue, 3) g.DrawLines(mypen, pts) End Sub
16.2 Elementare Grafikoperationen
845
Punkte zeichnen Erstaunlicherweise fehlt der Graphics-Klasse eine Methode, um einen einzelnen Punkt zu zeichnen. Wenn Sie eine Grafik aus einzelnen Punkten zusammensetzen möchten, sollten Sie im Regelfall die Bitmap-Klasse anwenden, die mit der Methode SetPixel ausgestattet ist (siehe Abschnitt 16.4). Wenn Sie das nicht möchten, behelfen Sie sich mit FillRectangle und geben als Breite und Höhe jeweils eins an. Beachten Sie aber, dass diese Vorgehensweise verhältnismäßig langsam ist und nicht zum Zeichnen großflächiger Pixelmuster verwendet werden sollte.
VERWEIS
g.FillRectangle(Brushes.Black, 150, 100, 1, 1)
Informationen zu den FillXxx-Methoden finden Sie etwas weiter unten (Überschrift Gefüllte Objekte). Das Beispiel setzt voraus, dass Sie ein Koordinatensystem mit der Längeneinheit Pixel verwenden, wie dies per Default der Fall ist. Wenn Sie ein anderes Koordinatensystem verwenden (siehe Abschnitt 16.2.5), ändert sich auch die Größe eines Rechtecks mit der Breite und Länge eins.
Abbildung 16.5: Ein aus schwarzen und weißen Punkten zusammengesetztes Muster
Private Sub TabPage3_Paint(...) Handles TabPage3.Paint Dim g As Graphics = e.Graphics Dim mybrush As New SolidBrush(Color.Black) Dim x, y As Integer For x = 0 To 199 For y = 0 To 199 If Math.Sin(x * y / 30) > 0 Then g.FillRectangle(mybrush, x, y, 1, 1) End If Next Next End Sub
846
16 Grafikprogrammierung (GDI+)
Hintergrundfarbe einstellen Die Methode Clear füllt die gesamte Zeichenfläche mit einer Hintergrundfarbe: gr.Clear(Color.White)
Ellipsen, Ellipsenbögen und Kreise Mit DrawEllipse zeichnen Sie ganze Ellipsen. Das Ausmaß der Ellipse wird durch einen Eckpunkt sowie Breite und Höhe oder ein entsprechendes Rectangle[F]-Objekt angegeben. Es ist dagegen nicht möglich, den Mittelpunkt und zwei Radien anzugeben. Ebenso ist es nicht möglich, verdrehte Ellipsen zu zeichnen (also mit Hauptachsen, die nicht senkrecht oder waagrecht sind). Es gibt keine eigene Methode zum Zeichnen von Kreisen – verwenden Sie einfach DrawEllipse und geben Sie für Breite und Höhe denselben Wert an. Mit DrawArc können Sie Ellipsen- oder Kreisbögen zeichnen. Die Methode erwartet neben den DrawEllipse-Parametern noch zwei Winkelangaben: den Startwinkel und den zu bedeckenden Winkelbereich. Die Winkel werden in Grad angegeben (0 bis 360). Die Winkelangaben beginnen mit 0 bei der x-Achse und erfolgen im Uhrzeigersinn. DrawPie funktioniert wie DrawArc, zeichnet aber Ellipsen- bzw. Kreissegmente. (Der Ellipsenbogen wird also durch zwei Linien zum Ellipsenmittelpunkt vervollständigt.)
Abbildung 16.6: Ellipsen, Ellipsenbögen und Kreissegmente
' Beispiel grafik\grafikmethoden Private Sub TabPage4_Paint(...) Handles TabPage4.Paint Dim g As Graphics = e.Graphics Dim mypen As New Pen(Color.Blue, 3) ' eine Ellipse g.DrawEllipse(mypen, 10, 10, 200, 50)
16.2 Elementare Grafikoperationen
847
' dünner Kreis, roter Viertelkreis mypen = New Pen(Color.Black, 1) g.DrawEllipse(mypen, 10, 80, 80, 80) mypen = New Pen(Color.Red, 5) g.DrawArc(mypen, 10, 80, 80, 80, 45, 90) ' drei Ellipsensegmente g.DrawPie(mypen, 100, 100, 200, 100, -45, 135) g.DrawPie(mypen, 100, 100, 200, 100, 100, 30) g.DrawPie(mypen, 100, 100, 200, 100, 180, 90) End Sub
Kurvenzug (DrawCurve, DrawClosedCurve) Die Methoden DrawCurve bzw. DrawClosedCurve zeichnen eine geschwungene Kurve, die exakt durch die in einem Point[F]-Feld angegebenen Koordinatenpunkte führt. Bei DrawClosedCurve wird der Linienzug geschlossen (d.h., es wird eine Verbindung zwischen dem ersten und dem letzten Punkt hergestellt). Intern werden so genannte Splines zur Konstruktion der Kurve verwendet. Durch einen optionalen Parameter (tension) können Sie angeben, wie nah die Kurve den Punkten folgen soll. Wenn dieser Wert 0 beträgt, werden die Punkte durch gerade Linien verbunden. Bei 1 schwingt die Kurve relativ stark über die Koordinatenpunkte hinaus. Werte größer als 1 sind zwar zulässig, führen aber selten zu brauchbaren Ergebnissen. Der Defaultwert beträgt 0,5. In Abbildung 16.7 sehen Sie einige zufällige Punkte sowie zwei Linienzüge. Der dicke Linienzug wurde mit dem tension-Defaultwert gezeichnet, der dünnere mit tension=1. Um eine möglichst hohe Bildqualität zu erzielen, wurde Antialiasing aktiviert (siehe Abschnitt 16.5.1). Im Beispielprogramm werden statische Variablen verwendet, um die Koordinatenpunkte des Linienzugs zu speichern. Diese Variablen werden nur beim ersten Aufruf der Ereignisprozedur initialisiert.
Abbildung 16.7: Ein mit DrawCurve gezeichneter Linienzug
848
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\grafikmethoden Private Sub TabPage5_Paint(...) Handles TabPage5.Paint Dim i As Integer Dim g As Graphics = e.Graphics g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias Dim mypen As Pen Dim mybrush As SolidBrush ' Koordinatenpunkte nur einmal initialisieren Static pts(9) As Point Static initialzed As Boolean = False If initialzed = False Then Dim myrand As New Random() For i = 0 To 9 pts(i).X = i * 40 + myrand.Next(0, 30) pts(i).Y = myrand.Next(0, 200) Next initialzed = True End If ' Koordinatenpunkte zeichnen mybrush = New SolidBrush(Color.Red) For i = 0 To 9 g.FillEllipse(mybrush, pts(i).X - 6, pts(i).Y - 6, 12, 12) Next ' Linienzug zeichnen mypen = New Pen(Color.Black, 3) g.DrawCurve(mypen, pts) mypen = New Pen(Color.Green, 1) g.DrawCurve(mypen, pts, 1) End Sub
Bezierkurven Während Sie bei DrawCurve relativ wenig Einfluss auf die genaue Form der Kurve haben, können Sie mit DrawBezier Kurven zeichnen, deren Ausprägung in jedem Segment durch zwei Stützpunkte gesteuert wird. Die Methode erwartet als Parameter vier Koordinatenpunkte. Die Kurve führt vom ersten zum vierten Punkt. Die beiden anderen Punkte dienen als Stützpunkt, die die Lage von Tangenten angeben. Diese Tangenten bestimmen, in welchem Winkel die Kurve vom Start- bzw. zum Endpunkt führt. Je weiter die Stützpunkte vom Start- bzw. Endpunkt entfernt sind, desto stärker nähert sich die Bezierkurve an die Tangenten an. In Abbildung 16.8 ist eine Bezierkurve samt der Stützpunkte und den daraus resultierenden Tangenten zu sehen.
16.2 Elementare Grafikoperationen
849
Wenn Sie einen ganzen Kurvenzug zeichnen möchten, bietet sich DrawBeziers an: Diese Methode erwartet ein Point[F]-Feld mit 3*n+1 Elementen. Die Kurve führt vom ersten Koordinatenpunkt durch den vierten Punkt, dann durch den siebten Punkt etc. Die dazwischenliegenden Punkte sind Stützpunkte.
Abbildung 16.8: Eine Bezierkurve
' Beispiel grafik\grafikmethoden Private Sub TabPage6_Paint(...) Handles TabPage6.Paint Dim i As Integer Dim g As Graphics = e.Graphics Dim pt, pts(3) As Point Dim mypen As Pen Dim mybrush As SolidBrush pts(0) = New Point(10, 10) pts(1) = New Point(30, 30) pts(2) = New Point(140, 80) pts(3) = New Point(150, 10) ' Koordinatenpunkte und Tangenten zeichnen mybrush = New SolidBrush(Color.Red) For Each pt In pts g.FillEllipse(mybrush, pt.X - 6, pt.Y - 6, 12, 12) Next mypen = New Pen(Color.Black, 1) g.DrawLine(mypen, pts(0), pts(1)) g.DrawLine(mypen, pts(2), pts(3)) ' Bezierkurve zeichnen mypen = New Pen(Color.Black, 3) g.DrawBezier(mypen, pts(0), pts(1), pts(2), pts(3)) End Sub
850
16 Grafikprogrammierung (GDI+)
Gefüllte Objekte Die meisten der in diesem Abschnitt besprochenen Methoden DrawXxx stehen auch in der Form FillXxx zur Verfügung. (FillPolygon ist das Gegenstück zu FillLines.) Die Methoden bewirken dann, dass nicht der Rand des Objekts gezeichnet wird, sondern dass das Objekt mit einem Füllmuster ausgefüllt wird. Aus diesem Grund muss als erster Parameter kein Stift (kein Pen-Objekt), sondern ein Füllmuster (ein Brush-Objekt) angegeben werden.
Abbildung 16.9: Mit Farben oder Mustern gefüllte Objekte
' Beispiel grafik\grafikmethoden Private Sub TabPage7_Paint() Handles TabPage7.Paint Dim g As Graphics = e.Graphics Dim mypen As Pen Dim mybrush As Brush mybrush = New SolidBrush(Color.Red) g.FillRectangle(mybrush, 10, 10, 200, 60) mybrush = Brushes.White g.FillEllipse(mybrush, 10, 70, 150, 50) mybrush = New Drawing2D.HatchBrush( _ Drawing2D.HatchStyle.Cross, Color.Red) g.FillPie(mybrush, 10, 120, 100, 100, -10, 340) End Sub
16.2.2 Farben (Color-Klasse) Bei der Erzeugung von Pen- oder Brush-Objekten müssen Sie ein Color-Objekt angeben. In den meisten Beispielen dieses Kapitels wurden dazu einfach die vordefinierten Farben verwendet, von denen die Color-Klasse eine ganze Menge zur Auswahl stellt. Dim mypen As New Pen(Color.Black, 3)
16.2 Elementare Grafikoperationen
851
Wenn Sie eine neue Farbe aus den Rot-, Grün- und Blauanteilen zusammensetzen möchten, verwenden Sie am besten die Color-Methode FromArgb(r, g, b). An diese Methode können Sie die Farbanteile mit Integer-Werten zwischen 0 und 255 angeben. Die Farbe Weiß erhalten Sie also auch mit Color.FromArgb(255, 255, 255).
HINWEIS
Streng genommen ist Color keine Klasse, sondern eine Struktur. Für den praktischen Umgang mit Farben hat das aber wenig Konsequenzen, weswegen dieser Abschnitt bei den Begriffen Klasse (für die abstrakte Definition) und Objekt (für konkrete Instanzen) bleibt.
TIPP
Dim col As Color col = Color.FromArgb(255, 0, 0)
Die Aufzählung Color.farbname enthält gewöhnliche Farbnamen. Wenn Sie dagegen Windows-Systemfarben benötigen, müssen Sie auf die SystemColors-Aufzählung zurückgreifen. Beispielsweise gibt SystemColors.Control die Defaultfarbe für den Hintergrund von Steuerelementen an, SystemColors.ControlText die Farbe für den Text von Steuerelementen und SystemColors.Window die Hintergrundfarbe für Fenster etc.
Alphakanal Die Methode FromArgb kann auch in der Form FromArgb(a, r, g, b) verwendet werden. In diesem Fall gibt a den Wert für den so genannten Alphakanal an. Diese Zusatzinformation steuert, wie durchsichtig die Farbe ist. Wenn die Farbe vollständig decken soll, muss hier der Wert 255 angegeben werden. (Das ist die Defaulteinstellung, wenn Sie nur drei Parameter angeben.) 0 bedeutet, dass die Farbe vollkommen durchsichtig ist. FromArgb funktioniert auch, wenn Sie nur einen einzigen Integer-Wert übergeben. In diesem
VERWEIS
Fall interpretiert das erste Byte den Blauanteil, das zweite Byte den Grünanteil, das dritte Byte den Rotanteil und das vierte Byte den Alphawert. Color.FromArgb(&HFFFF0000) liefert somit die deckende Farbe Rot. Weitere Informationen sowie ein Beispiel für die Anwendung des Alphakanals finden Sie in Abschnitt 16.4.4, in dem es um transparente Bitmaps geht.
Eigenschaften und Methoden von Farben Die drei wichtigsten Eigenschaften eines Color-Objekts lauten R, G und B und liefern den Rot-, Grün- und Blauanteil der Farbe als Byte-Wert; die Eigenschaft A liefert den dazugehörigen Alphawert (255 bei deckenden Farben). Wenn Sie die Farbanteile entsprechend des HSB-Farbmodells ermitteln möchten, müssen Sie dazu die Methoden GetHue, GetSatu-
852
16 Grafikprogrammierung (GDI+)
ration und GetBrightness und verwenden. Die Methode ToArgb liefert die Farbe in dem oben schon beschriebenen Integer-Format.
Beachten Sie bitte, dass Farben nicht geändert werden können. Sie können also nicht bei einem bereits vorhandenen Color-Objekt die Eigenschaft R verändern. Stattdessen müssen Sie – in der Regel mit Color.FromArgb – ein neues Color-Objekt erzeugen.
Farbauswahl mit dem ColorDialog-Steuerelement Wenn Sie in Ihrem Programm eine interaktive Farbauswahl ermöglichen möchten, können Sie dazu auf das ColorDialog-Steuerelement zurückgreifen. Die Anwendung des Steuerelements ist denkbar einfach: Mit ShowDialog zeigen Sie den Dialog an. Sofern eine gültige Auswahl durchgeführt wurde (Rückgabewert auswerten!), können Sie auf die Farbe anschließend mit der Color-Eigenschaft zugreifen. Beim folgenden Beispielprogramm wird vor dem Anzeigen die Eigenschaft FullOpen auf True gesetzt. Damit erreichen Sie, dass gleich der gesamte Dialog (und nicht nur der linke Teil) angezeigt wird. Anschließend wird mit Me.BackColor die Hintergrundfarbe des gesamten Formulars verändert. (Wenn Sie ein Graphics-Objekt mit einer Hintergrundfarbe füllen möchten, verwenden Sie dazu die Methode Clear. Beachten Sie aber, dass diese Einstellung nicht bleibend gespeichert wird. Clear funktioniert im Prinzip wie FillRectangle.) ' Beispiel grafik\farbauswahl Private Sub Button1_Click(...) Handles Button1.Click With ColorDialog1 .FullOpen = True If .ShowDialog() = DialogResult.OK Then Me.BackColor = .Color End If End With End Sub
Abbildung 16.10: Der ColorDialog zur Farbauswahl
16.2 Elementare Grafikoperationen
853
16.2.3 Linienformen (Pen-Klasse) Einfarbige Zeichenstifte Pen-Objekte (also gewissermaßen Zeichenstifte) wurden bereits in zahlreichen Beispielen
dieses Kapitels eingesetzt, ohne dass allzu viele Worte darüber verloren wurden. Tatsächlich ist die Anwendung für einfache Fälle denkbar unkompliziert: Sie deklarieren eine Variable vom Typ Pen und geben im Konstruktor eine Farbe und optional die gewünschte Linienbreite an. (Per Default beträgt die Linienbreite einen Pixel, sofern Sie das Standardkoordinatensystem verwenden.) Damit besitzen Sie einen Zeichenstift, den Sie in der Folge in allen DrawXxx-Methoden anwenden können. Dim mypen As Pen mypen = New Pen(Color.Black, 3) g.DrawXxx(mypen, ...)
Wenn Sie ein Pen-Objekt nur ein einziges Mal benötigen, können Sie sich die Variable auch sparen und das Pen-Objekt innerhalb der Zeichenmethode erzeugen.
HINWEIS
g.DrawLine(New Pen(Color.Red, 3), 0, 0, 100, 100)
Beachten Sie, dass es bei dieser Syntaxvarianten unmöglich ist, das Pen-Objekt anschließend wieder durch Dispose freizugeben. Wie bereits in Abschnitt 16.1.3 erläutert, ist dies im Regelfall kein Problem: Der Speicherverbrauch von Pen-Objekten ist klein, und nach einer Weile wird der von Pen-Objekten beanspruchte Speicher ohnedies wieder automatisch durch eine garbage collection freigegeben. In Schleifen (d.h., wenn Sie ein Pen-Objekt sehr oft einsetzen) sollten Sie aber auf die bequeme Kurzschreibweise verzichten und stattdessen eine Objektvariable (mypen) einsetzen!
Noch komfortabler geht es, wenn Sie einen Zeichenstift mit einer Breite von 1 einsetzen möchten. Dann können Sie auf die in zahlreichen Farben vordefinierten Pens-Objekte zurückgreifen, beispielsweise so: g.DrawLine(Pens.Red, 0, 0, 100, 100)
Zeichenstifte auf der Basis eines Brush-Objekts Pen-Objekte können auch auf der Basis eines Brush-Objekts (siehe nächsten Abschnitt)
erzeugt werden. Damit können Sie erreichen, dass die Linien nicht einfach einfarbig gezeichnet werden, sondern mit einem beliebigen Füllmuster. mypen = New Pen(brushobj, linienbreite)
Linienmuster Linien können unterschiedliche Muster aufweisen (gestrichelt, strichpunktiert etc.). Das Linienmuster wird mit der Eigenschaft DashStyle eingestellt. Zur Auswahl stehen einige
854
16 Grafikprogrammierung (GDI+)
vordefinierte Muster aus der Aufzählung System.Drawing.Drawing2D.DashStyle (siehe Abbildung 16.11). Wenn Sie mit den vordefinierten Mustern nicht zufrieden sind, können Sie der Eigenschaft DashPattern ein eigenes Muster zuweisen. Dazu übergeben Sie ein Single-Feld. Das erste
Element gibt die Länge des ersten Linienstücks an, das zweite Element die Länge des ersten Zwischenraums, das dritte Element die Länge des zweiten Linienstücks etc. Die Längenangaben sind relativ zur Liniendicke, d.h., je dicker die Linie ist, desto länger sind die Teile des Musters. Das folgende Beispielprogramm demonstriert alle vordefinierten Linienmuster sowie ein eigenes Muster mit den Abständen 3-1-5-1.
Abbildung 16.11: Linienmuster
' Beispiel grafik\pen-dashstyle Private Sub Form1_Paint(...) Handles MyBase.Paint Dim i As Integer Dim gr As Graphics = e.Graphics Dim pn As Pen Dim ds As Drawing2D.DashStyle Dim fnt As New Font("Arial", 10) ' alle Linienmuster testen pn = New Pen(Color.Black, 5) For Each ds In System.Enum.GetValues(ds.GetType) i += 1 ' Name der DashStyle-Konstante gr.DrawString(ds.ToString, fnt, Brushes.Black, 10, i * 20) pn.DashStyle = ds ' eigenes Linienmuster definieren If ds = Drawing.Drawing2D.DashStyle.Custom Then Dim pttrn() As Single = {3, 1, 5, 1} pn.DashPattern = pttrn End If gr.DrawLine(pn, 110, 10 + i * 20, 250, 10 + i * 20) Next End Sub
16.2 Elementare Grafikoperationen
855
Linienanfang und -ende Auch zur Gestaltung von Linienanfang und -ende gibt es viele Möglichkeiten. An die Eigenschaften StartCap und EndCap können Sie die Elemente der Aufzählung System.Drawing.Drawing2D.LineCap zuweisen. Abbildung 16.12 zeigt die vordefinierten Muster.
Abbildung 16.12: Linienenden
' Beispiel grafik\pen-caps Private Sub Form1_Paint(...) Handles MyBase.Paint Dim i As Integer Dim gr As Graphics = e.Graphics Dim pn As Pen Dim lc As Drawing2D.LineCap Dim fnt As New Font("Arial", 10) ' höhere Darstellungsqualität gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias ' alle Formen von Linienenden testen (außer Custom) pn = New Pen(Color.Black, 9) For Each lc In System.Enum.GetValues(lc.GetType) If lc <> Drawing.Drawing2D.LineCap.Custom Then i += 1 ' Name der DashStyle-Konstante gr.DrawString(lc.ToString, fnt, Brushes.Black, 10, i * 20) pn.StartCap = lc pn.EndCap = lc gr.DrawLine(pn, 150, 10 + i * 20, 250, 30 + i * 20) End If Next End Sub
Wenn Sie Linienzüge zeichnen, können Sie auch die Form der Ecken einstellen. Dazu weisen Sie der Eigenschaft LineJoin eines der Elemente von System.Drawing.Drawing2D.LineJoin zu. So können Sie beispielsweise abgerundete Ecken erzielen.
856
16 Grafikprogrammierung (GDI+)
16.2.4 Füllmuster (Brush-Klassen) Sämtliche FillXxx-Methoden zum Zeichnen gefüllter Objekte erwarten ein Brush-Objekt, das angibt, mit welcher Farbe bzw. mit welchem Muster das Objekt ausgefüllt werden soll. Genau genommen ist die Brush-Klasse aber nur eine abstrakte Klasse, die nicht unmittelbar verwendet werden kann. Von ihr sind aber alle weiteren Brush-Klassen abgeleitet: •
SolidBrush füllt ein Grafikobjekt einfarbig aus.
•
TextureBrush verwendet eine Bitmap-Datei als Füllmuster.
•
HatchBrush (Drawing.Drawing2D-Namensraum) verwendet eines von zahlreichen
vordefinierten Mustern als Füllmuster (liniert, kariert etc.) •
LinearGradientBrush (Drawing.Drawing2D-Namensraum) verwendet einen Farbverlauf als Füllmuster. Der Farbverlauf wird innerhalb eines rechteckigen Bereichs definiert.
•
PathGradientBrush (Drawing.Drawing2D-Namensraum) verwendet ebenfalls einen Farbverlauf als Füllmuster. Die Klasse bietet aber noch komplexere Möglichkeiten, den Farbverlauf durch ein nichtrechteckiges Objekt (GraphicsPath) zu definieren. Beispielsweise können Sie mit einem PathGradientBrush ein unregelmäßiges Grafikobjekt so füllen, dass sich die Farbe vom Rand zum Inneren hin ändert. Die Anwendung derartiger Füllmuster wird in diesem Buch allerdings nicht behandelt. Weitere Informationen finden Sie in der GDI+-Dokumentation (Online-Hilfe).
Das folgende Beispielprogramm demonstriert vier der fünf obigen Brush-Varianten (siehe Abbildung 16.13). Nähere Informationen zu diesen vier Brush-Varianten folgen im weiteren Verlauf dieses Abschnitts.
Abbildung 16.13: Vier verschiedene Brush-Varianten (Solid-, Texture-, Hatch- und LinearGradientBrush)
Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim br As Brush br = New SolidBrush(Color.Red) gr.FillEllipse(br, 10, 10, 100, 50) br = New Drawing2D.HatchBrush( _ Drawing2D.HatchStyle.DashedDownwardDiagonal, _ Color.Blue, Color.White) gr.FillEllipse(br, 10, 70, 100, 50)
16.2 Elementare Grafikoperationen
857
br = New TextureBrush(New Bitmap("texture2.bmp")) gr.FillEllipse(br, 120, 10, 100, 50) br.Dispose() br = New Drawing2D.LinearGradientBrush( _ New Point(0, 0), New Point(100, 100), Color.White, Color.Black) gr.FillEllipse(br, 120, 70, 100, 50) End Sub
Einfarbige Muster (SolidBrush) Wenn Sie ein einfarbigen Muster benötigen, können Sie sich die Verwendung einer eigenen Objektvariablen oft sparen. Die Klasse Brushes stellt nämlich eine ganze Kollektion vordefinierter Muster zur Verfügung (z.B. Brushes.Red, Brushes.Green) gr.FillEllipse(Brushes.Blue, ...)
Wenn Sie eine Farbe verwenden möchten, die nicht vordefiniert ist, verwenden Sie den Konstruktor der SolidBrush-Klasse: Dim mybrush As New SolidBrush(Color.FromArgb(255, 127, 127)) gr.Fillxxx(mybrush, ...)
Bitmap-Muster (TextureBrush) Wenn Sie eine FillXxx-Methode mit einem Bitmap-Muster ausführen, wird das Grafikobjekt mit der Bitmap gefüllt. Per Default wird der Inhalt der Bitmap automatisch immer wieder wiederholt, so dass ein endloses Muster entsteht. Damit dies überzeugend aussieht, sollten speziell präparierte Bitmaps eingesetzt werden, bei denen sich das Muster nahtlos wiederholt. Bitmap-Muster setzen ein TextureBrush-Objekt voraus. Um ein derartiges Objekt zu erzeugen, müssen Sie im Konstruktor ein Bitmap-Objekt angeben. Im Regelfall werden Sie die erforderliche Bitmap einfach aus einer Datei laden. Die folgenden Zeilen gehen davon aus, dass sich die Bitmap-Datei im Programmverzeichnis befindet. (Weitere Informationen zum Umgang mit Bitmaps finden Sie in Abschnitt 16.4.) Dim br As New TextureBrush(New Bitmap("texture2.bmp")) gr.FillXxx(br, ...)
Noch einige Interna: Das Füllmuster beginnt beim Koordinatenpunkt (0, 0) der Zeichenfläche (also beispielsweise des Formulars oder des PictureBox-Steuerelements) und setzt sich dann in beide Koordinatenrichtungen periodisch fort. Wenn Sie also eine BitmapDatei mit 100*100 Pixeln verwenden und ein Rechteck zwischen den Punkten (50, 50) und (150, 150) zeichnen, dann beginnt das Muster innerhalb des Rechtecks in der Mitte der Bitmap. Abhilfe schafft die Eigenschaft RenderingOrigin, mit der Sie den Koordinatennullpunkt für das Muster verschieben können. RenderingOrigin = New Point(50, 50) bewirkt beispielsweise, dass das Muster an der Position (50, 50) beginnt und sich von dieser Position aus periodisch wiederholt.
858
16 Grafikprogrammierung (GDI+)
Falls Sie möchten, dass das Muster am linken oberen Rand des Rechtecks beginnt (unabhängig von den Koordinaten dieses Eckpunkts), müssen Sie die Eigenschaft WrapMode auf Drawing2D.WrapMode.Clamp stellen. Damit beginnt die Textur am linken oberen Eck des zu zeichnenden Objekts. Allerdings wird die Bitmap nun nicht mehr wiederholt. Teile des zu zeichnenden Objekts, die über die Bitmap hinausreichen, bleiben einfach unsichtbar.
TIPP
Falls Sie die Textur noch besser an das Grafikobjekt anpassen möchten (z.B. wenn Sie die Textur zusammen mit dem Grafikobjekt verdrehen möchten), müssen Sie die TransformMethoden des Brush-Objekts anwenden. Das setzt allerdings ein gutes Verständnis der mathematischen Vorgänge voraus, die bei einer Texturtransformation vor sich gehen. (Ausführliche Informationen finden Sie in jedem guten Buch zur 2D-Grafikprogrammierung.) Bei TextureBrush-Objekten sollten Sie daran denken, diese nach ihrer Verwendung durch Dispose wieder freizugeben. Insbesondere bei großen Bitmaps beanspruchen derartige Objekte relativ viel Speicher. Je früher dieser wieder freigegeben werden kann, desto besser.
Vordefinierte Füllmuster (HatchBrush) GDI+ kennt eine große Anzahl vordefinierter Füllmuster, die über die HatchBrush-Klasse (Namensraum System.Drawing.Drawing2D) zugänglich sind. Bevor Sie ein derartiges Muster verwenden können, müssen Sie im New-Konstruktor drei Parameter angeben: das gewünschte Muster (ein Element der Drawing2D.HatchStyle-Aufzählung) sowie die gewünschte Vorder- und Hintergrundfarbe. Die zur Auswahl stehenden Füllmuster sind in Abbildung 16.14 zusammengefasst. (Den Code des Programms, mit dem diese Abbildung erzeugt wurde, finden Sie auf der beiliegenden CD im Verzeichnis grafik\brush-hatches.) Dim br As New Drawing2D.HatchBrush( _ Drawing2D.HatchStyle.DashedDownwardDiagonal, _ Color.Blue, Color.White) gr.FillXxx(br, ...)
Farbverlaufmuster (LinearGradientBrush) Die Klasse LinearGradientBrush ist ebenfalls im Namensraum Drawing2D definiert. Im einfachsten Fall kann diese Klasse dazu verwendet werden, den linearen Verlauf zwischen zwei Farben als Füllmuster zu verwenden. Das Füllmuster wird dazu durch zwei Koordinatenpunkte und zwei Farben definiert. Ähnlich wie bei Bitmap-Mustern wird das so definierte Muster in beide Richtungen fortgesetzt. Dim br As New Drawing2D.LinearGradientBrush(pt1, pt2, color1, color2) gr.FillXxx(br, ...)
VERWEIS
16.2 Elementare Grafikoperationen
859
Sie können LinearGradientBrush-Objekte aber auch dazu verwenden, um nichtlineare Verläufe zwischen mehreren Farben durchführen – aber dieses Thema übersteigt den Rahmen dieses Buchs. Zu dieser Form der Anwendung finden Sie in den Hilfeseiten weitere Informationen.
Abbildung 16.14: Die vordefinierten HatchBrush-Muster
16.2.5 Koordinatensysteme und -transformationen Per Default beginnt das Koordinatensystem von Formularen und Steuerelementen mit dem Punkt (0,0) im linken oberen Eck. Die x-Koordinatenachse zeigt nach links, die yAchse nach unten. Als Einheit werden Bildschirmpixel verwendet. Wie nicht anders zu erwarten, sind diese Grundannahmen keineswegs unveränderlich. Im Folgenden lernen Sie einige Möglichkeiten kennen, um auf das Koordinatensystem Einfluss zu nehmen. Beachten Sie aber, dass Sie durch eine Änderung des Koordinatensystems nicht nur die Positionen eines Koordinatenpunkts (x,y) ändern, sondern wegen der geänderten Skalie-
860
16 Grafikprogrammierung (GDI+)
rung auch die Einheit für die Linienstärke von Linien oder Kurven. Andererseits werden HatchBrush-Muster generell nie skaliert, sondern bleiben unverändert. Ein Sonderfall ist die Größe von Schriftarten: Ob diese sich mit dem Koordinatensystem ändert oder nicht, hängt von zwei Faktoren ab: In welcher Einheit die Schriftgröße angegeben wird (Punkt, Pixel etc.), und auf welche Weise die Skalierung des Koordinatensystems bewirkt wird: Wenn Sie die Schriftart in Punkt angeben, ändert sich deren Größe weder durch eine geänderte GraphicsUnit-Einstellung noch durch PageScale, wohl aber durch ScaleTransform. (Alle drei Schlüsselwörter werden unten beschrieben.) Generell gilt: Wenn Sie vorhaben, nicht das Defaultkoordinatensystem zu verwenden (oder das Koordinatensystem gar variabel sein soll), sollten Sie Ihr Programm von Anfang an mit diesen Einstellungen testen. Eine spätere Umstellung ist meistens sehr mühsam.
Maßeinheit des Koordinatensystems (PageUnit-Eigenschaft) Die Eigenschaft PageUnit des Graphics-Objekt gibt an, welche Einheit im Koordinatensystem verwendet wird. Zur Auswahl stehen die Elemente der GraphicsUnit-Aufzählung: Pixel Point Display
Document Inch Millimeter World
Bildschirmpixel 1/72 Zoll Einheit abhängig von der Art des Graphics-Objekt: bei Bildschirmausgaben: Pixel beim Ausdruck (siehe Kapitel 17): 1/100 Zoll (Vorsicht, die Online-Hilfe behauptet 1/75 Zoll, das ist falsch!) 1/300 Zoll Zoll (2.54 cm) mm Weltkoordinaten (können nicht für PageUnit verwendet werden!)
Sie können die Einheit durch eine einfache Zuweisung innerhalb der Paint-Prozedur verändern:
VERWEIS
Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics gr.PageUnit = GraphicsUnit.Millimeter ... End Sub
Neben der Einheit des Koordinatensystems wird beim Zeichnen auch die DPI-Einstellung berücksichtigt (also das Maß, wie groß ein Pixel am Bildschirm ist). Das ist insbesondere bei Schriften wichtig, deren Größe normalerweise in Punkt (1/72 Zoll) angegeben wird. Hintergrundinformationen zu diesem Thema finden Sie in den Abschnitten 15.2.9 und 16.3.4.
16.2 Elementare Grafikoperationen
861
Skalierung des Koordinatensystems Mit der Eigenschaft PageScale können Sie das gesamte Koordinatensystem um einen Faktor skalieren. gr.PageScale=2 bewirkt, dass Ausgaben im Graphics-Objekt doppelt so groß werden. PageScale und PageUnit können unabhängig voneinander eingestellt werden, ihre Wirkung multipliziert sich.
Transformationen Noch viel weiter gehende Möglichkeiten zur Beeinflussung des Koordinatensystems bieten die verschiedenen Transformationsmethoden: •
gr.TranslateTransform(dx, dy) verschiebt den Koordinatennullpunkt um dx Einheiten nach rechts und um dy Einheiten nach unten. Oder anders formuliert: bei allen folgenden Grafikausgaben werden die Koordinatenangaben um (dx, dy) verändert, wodurch die Ausgaben weiter rechts und weiter unten erscheinen (bei positiven dx- und dy-Wer-
ten). •
gr.ScaleTransform(fx, fy) skaliert die Koordinatenachsen um die Faktoren fx und fy. ScaleTransform(2, 2) hat somit fast dieselbe Wirkung wie PageScale=2. (Einzige Ausnahme sind Schriften, deren Größe sich durch PageScale nicht beeinflussen lässt, wohl aber durch ScaleTransform!) ScaleTransform und PageScale können gleichzeitig verwendet werden – in
diesem Fall multipliziert sich die Wirkung. Wenn Sie möchten, dass die y-Achse – wie in der Mathematik üblich – nach oben zeigt, können Sie das einfach mit ScaleTransform(1,-1) erreichen. •
gr.RotateTransform(angle) verdreht das Koordinatensystem um den angegebenen
Winkel im Uhrzeigersinn. Als Rotationspunkt gilt immer der Koordinatenursprung! Wenn Sie um einen anderen Punkt drehen möchten, müssen Sie vorher TranslateTransform ausführen. •
Mathematisch versierte Programmierer können mit gr.Transform die Transformationsmatrix (ein Drawing2D.Matrix-Objekt) direkt lesen bzw. verändern.
•
gr.ResetTransform() setzt die Transformation zurück.
Beachten Sie, dass bei der Durchführung von Transformationen die Reihenfolge nicht gleichgültig ist!
Beispiel Das folgende Beispielprogramm demonstriert die Wirkung einfacher Transformationen. Um den Code übersichtlich zu halten, wird das Testrechteck immer mit derselben Prozedur ausgegeben.
862
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\transformation Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics ' möglichst hohe Bildqualität gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias ' Defaultsystem DrawSomething(gr, "default", Color.Black) ' Translation gr.TranslateTransform(200, 10) DrawSomething(gr, "translate only", Color.DarkRed) ' Translation + Skalierung gr.ResetTransform() gr.TranslateTransform(10, 100) gr.ScaleTransform(2, 0.7) DrawSomething(gr, "translate + scale ", Color.DarkBlue) ' Translation + Rotation gr.ResetTransform() gr.TranslateTransform(450, 30) gr.RotateTransform(45) DrawSomething(gr, "translate + rotate", Color.DarkGreen) End Sub Sub DrawSomething(ByVal gr As Graphics, _ ByVal s As String, ByVal c As Color) Dim pn As New Pen(c, 3) Dim br As New SolidBrush(c) Dim fnt As New Font("arial", 10) Dim hb As New Drawing2D.HatchBrush( _ Drawing2D.HatchStyle.DashedDownwardDiagonal, c, Color.White) gr.DrawRectangle(pn, 5, 5, 190, 70) gr.DrawEllipse(pn, 20, 30, 30, 30) gr.FillEllipse(hb, 50, 30, 40, 40) gr.DrawString(s, fnt, br, 10, 10) fnt.Dispose() End Sub
16.2 Elementare Grafikoperationen
863
Abbildung 16.15: Beispiele für mehrere Transformationen
16.2.6 Syntaxzusammenfassung Basisklassen (ValueType-Strukturen) System.Drawing.Point[F]-Struktur X, Y
enthalten die Koordinaten des Punkts.
System.Drawing.Size[F]-Struktur Width, Height
geben die Größe des Objekts in X- und Y-Richtung an.
System.Drawing.Rectangle[F]-Struktur X, Left
enthalten die x-Koordinate des linken oberen Ecks.
Y, Top
enthalten die y-Koordinate des linken oberen Ecks
Width, Height
geben die Größe des Objekts in x- und y-Richtung an.
Right
enthält die x-Koordinate des rechten unteren Ecks.
Bottom
enthält die y-Koordinate des rechten unteren Ecks.
Location
liefert ein Point[F]-Objekt mit dem linken oberen Eckpunkt.
Size
liefert ein Size[F]-Objekt mit der Größe des Rechtecks.
Inflate(x0, y0)
liefert ein neues Rechteck, das an den Rändern um x0 bzw. y0 vergrößert ist.
rect1.Intersect(rect2)
liefert ein neues Rechteck, das sich aus der gemeinsamen Fläche (innere Schnittfläche) der beiden Rechtecke ergibt.
rect1.Union(rect2)
liefert ein neues Rechteck, das beide Rechtecke umschließt.
rect1.IntersectsWith(rect2)
testet, ob die beiden Rechtecke einander überlappen.
rect.Contains(obj)
testet, ob das Rechteck den angegebenen Koordinatenpunkt bzw. das Rechteck enthält.
864
16 Grafikprogrammierung (GDI+)
Gemeinsame Methoden von Point[F], Size[F], Rectangle[F] obj[F] = Offset(x,y)
liefert ein um (x,y) verschobenes Objekt (nicht für Size[F]).
obj = Ceiling(objF)
liefert die Integer-Version des Single-Objekts, wobei alle Maße aufgerundet werden.
obj = Truncate(objF)
liefert die Integer-Version des Single-Objekts, wobei der Nachkommaanteil abgeschnitten wird.
obj = Round(objF)
liefert die Integer-Version des Single-Objekts, wobei gerundet wird.
objF = op_Implicit(obj)
wandelt die Integer-Version in die Single-Version um.
obj1 = op_Explicit(obj2)
wandelt Daten zwischen Size und Point um.
Grafikmethoden Grafikmethoden der Klasse System.Drawing.Graphics Clear
füllt die gesamte Zeichenfläche mit einer (Hintergrund)Farbe.
DrawArc
zeichnet einen Kreis- oder Ellipsenbogen.
DrawBezier[s]
zeichnet eine Bezierkurve durch die angegebenen Punkte.
Draw[Closed]Curve
verbindet mehrere Punkte durch eine (geschlossene) Kurve.
DrawEllipse
zeichnet eine ganze Ellipse bzw. einen Kreis.
DrawIcon
kopiert ein Icon in die Grafik (siehe Abschnitt 16.4.5).
DrawImage[Unscaled]
kopiert eine Bitmap in die Grafik (siehe Abschnitt 16.4.3).
DrawLine[s]
zeichnet eine Linie oder einen Linienzug.
DrawPath
zeichnet ein zusammengesetztes Objekt (ein GraphicsPathObjekt, siehe Abschnitt 16.5.2).
DrawPie
zeichnet ein Ellipsensegment.
DrawRectangle[s]
zeichnet ein oder mehrere Rechtecke.
FillXxx
entspricht den oben aufgezählten Methoden, das Grafikobjekt wird aber mit einer Farbe oder einem Füllmuster gefüllt (Brush-Objekt).
16.2 Elementare Grafikoperationen
865
Farben, Zeichenstifte, Muster und andere Objekte System.Drawing.Color-Struktur erzeugen col = Color.Black, Color.White ... liefert eine vordefinierte Farbe. col = SystemColors.Control ...
liefert eine Standardfarbe für Windows-Komponenten (Steuerelemente, Fensterhintergrund etc.)
col = Color.FromArgb(r, g, b)
liefert eine Farbe entsprechend dem Rot-, Grün- und Blauanteil.
col = Color.FromArgb(a, r, g, b)
wie oben, aber a gibt zusätzlich den Alphakanal an (0 = durchsichtig, 255 = deckend).
col = Color.FromArgb(n)
liefert eine Farbe entsprechend der Integer-Zahl n, deren vier Bytes die Daten für a, r, g und b enthalten.
col = Color.FromString(s)
liefert eine Farbe entsprechend dem angegebenen englischen Namen (z.B. "black").
Methoden und Eigenschaften der Struktur System.Drawing.Color A
liefert den Wert des Alphakanals als Byte-Wert.
R, G, B
liefern den roten, grünen und blauen Farbanteil (Byte).
GetHue, GetSaturation, GetBrightness
liefern den Farbton, die Sättigung und die Helligkeit (HSBFarbmodell).
ToArgb
liefert den Integer-Wert der Farbe.
System.Drawing.Pen-Objekt erzeugen pn = Pens.Red, Pens.Blue
liefert eine der vordefinierten Zeichenstifte mit der Linienstärke 1.
pn = New Pen(col [, n])
liefert ein Pen-Objekt für die angegebene Farbe (mit der Linienstärke n).
pn = New Pen(br [, n])
liefert ein Pen-Objekt auf der Basis des angegebenen BrushObjekts (mit der Linienstärke n).
Eigenschaften der Klasse System.Drawing.Pen ändern DashStyle = ds
verändert das Linienmuster. ds ist ein Element der Aufzählung System.Drawing.Drawing2D.DashStyle.
DashPattern = pttrn
definiert ein eigenes Linienmuster. pttrn ist ein Single-Feld, das die Länge der Linienstücke und der Abstände dazwischen angibt.
866
16 Grafikprogrammierung (GDI+)
Eigenschaften der Klasse System.Drawing.Pen ändern StartCap = lc
verändert das Aussehen des Startpunkts der Linie. lc ist ein Element der Aufzählung System.Drawing.Drawing2D.LineCap.
EndCap = lc
wie oben, aber für den Endpunkt der Linie.
LineJoin = lj
verändert das Aussehen der Verbindungspunkte bei Linienzügen. lj ist ein Element der Aufzählung System.Drawing.Drawing2D.LineJoin.
Verschiedene System.Drawing.Brush-Objekte erzeugen sb = Brushes.Red ...
liefert eines der vordefinierten einfarbigen Muster (SolidBrush-Objekt).
sb = New SolidBrush(col)
liefert ein einfarbiges Muster für die angegebene Farbe.
tb = New TextureBrush(bm)
liefert ein Bitmap-Muster für die angegebene Bitmap.
hb = New _ Drawing2D.HatchBrush( _ hs, fcol, bcol) lgb = New Drawing2D. _ LinearGradientBrush( _ pt1, pt2, col1, col2)
liefert ein vordefiniertes Füllmuster. hs ist ein Element der Aufzählung System.Drawing.Drawing2D.HatchStyle. fcol und bcol geben die Vor- und Hintergrundfarbe an. liefert ein Farbverlaufmuster. pt1 und pt2 geben zwei Koordinatenpunkte an. An diesen Punkten hat das Muster die Farben col1 und col2. Dazwischen wird linear interpoliert.
Manipulation des Koordinatensystems Koordinatentransformation (Graphics-Klasse) PageUnit = gu
bestimmt die Koordinateneinheit. gu ist ein Element der GraphicsUnit-Aufzählung (per Default Pixel).
PageScale = n
bestimmt den Skalierungsfaktor für alle Grafikausgaben (per Default 1).
TranslateTransform(dx, dy)
verschiebt das Koordinatensystem um (dx, dy).
ScaleTransform(fx, fy)
skaliert das Koordinatensystem um die Faktoren fx und fy.
RotateTransform(angle)
rotiert das Koordinatensystem um den Winkel angle (Angabe in Grad).
ResetTransform()
setzt das Koordinatensystem in den Defaultzustand zurück.
Transform [=matrix]
liest bzw. verändert die Transformationsmatrix (ein Drawing2D.Matrix-Objekt).
16.3 Text ausgeben (Font-Klassen)
16.3
867
Text ausgeben (Font-Klassen)
Dieser Abschnitt beschreibt die wichtigsten FontXxx-Klassen in den Namensräumen System.Drawing und System.Drawing.Text. Diese Klassen ermöglichen den Zugriff und die Verwendung verschiedener Schriftarten.
16.3.1 Einführung Grundsätzlich erfolgt die Textausgabe wie das Zeichnen einer Linie oder eines anderen grafischen Objekts. Die Graphics-Klasse sieht dazu die Methode DrawString vor. In der einfachsten Syntaxvariante werden folgende Parameter an die Methode übergeben: gr.DrawString("text", font, brush, x, y)
Damit wird also die Zeichenkette "text" an der Koordinatenposition (x,y) ausgegeben. x und y bezeichnen genau genommen den linken oberen Eckpunkt des Rechtecks, in dem der Text ausgegeben wird. brush ist ein Brush-Objekt und gibt an, welches Muster bzw. welche Farbe (SolidBrush) dabei verwendet werden soll. (Sie können alle in Abschnitt 16.2.4 beschriebenen Brush-Varianten verwenden, um Texte gleichsam anzumalen. Beispielsweise können Sie mit einem LinearGradientBrush-Objekt die Textfarbe kontinuierlich verändern.) font gibt die Schriftart an, in der Sie den Text ausgeben möchten. Zur Erzeugung eines neuen Font-Objekts gibt es wiederum zahlreiche New-Konstruktoren. Im einfachsten Fall geben Sie nur den Namen der Schriftart und die gewünschte Größe an: Dim fnt1 As New Font("Arial", 12)
Damit sieht die Ereignisprozedur, um in einem Formular einen kurzen Text anzuzeigen (siehe Abbildung 16.16), so aus: Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim fnt As New Font("Arial", 15) gr.DrawString("Abcdefg", fnt, Brushes.Black, 10, 10) fnt.Dispose() End Sub
Abbildung 16.16: Eine ganz einfache Textausgabe
Auch wenn dieses erste Beispiel vollkommen problemlos erscheint, ist der Umgang mit Schriften doch ziemlich kompliziert. Die folgenden Abschnitte beschreiben detailliert, wie Sie die Größe von Fonts exakt angeben, wie Sie mehrzeiligen Text ausgeben, wie Sie verschiedene Textattribute (fett, kursiv etc.) erzielen etc.
TIPP
868
16 Grafikprogrammierung (GDI+)
Nach der Verwendung von Schriftarten sollten Sie daran denken, die Font-Objekte mit Dispose wieder freizugeben. Das gilt insbesondere, wenn Sie (z.B. in einer Schleife) sehr viele Font-Objekte erzeugen.
16.3.2 TrueType-, OpenType- und Type-1-Schriftformate Es gibt verschiedene Möglichkeiten, Schriften zu beschreiben. Die folgende Aufzählung nennt die drei wichtigsten Formate: TrueType ist ein Schriftformat, das vor Jahren aus einer gemeinsamen Initiative von Microsoft und Apple entstanden ist. Die meisten Standardschriften, die mit Windows mitgeliefert werden, sind TrueType-Schriften. Bekannte TrueType-Schriften sind Arial, Courier New und Times New Roman. Die Dateikennung lautet *.ttf. (Werfen Sie einen Blick in das Verzeichnis Windows\fonts.)
•
Type-1 ist das (historisch gesehen viel ältere) Schriftformat zur Beschreibung von PostScript-Schriften. Bekannte Schriften sind Helvetica, Courier und Times. Die übliche Dateikennung ist *.pfm.
•
OpenType ist ein Überformat, das gleichermaßen zur internen Beschreibung von TrueType- und Type-1-Schriften geeignet ist. Aktuelle Windows-Versionen unterstützen OpenType per Default. Teilweise liegen die traditionellen TrueType-Schriften dort bereits als OpenType-Schriften zur Verfügung. Beispielsweise ist auf meinem Windows-2000-Rechner die Schriftart Arial als OpenType-Schrift installiert. Intern basiert die Schrift aber weiterhin auf der ursprünglichen TrueType-Schrift und sieht vollkommen unverändert aus.
VORSICHT
•
GDI+ unterstützt leider ausschließlich TrueType- und OpenType-Schriftfamilien. PostScript-Schriftarten (Type-1-Schriftarten), die im Type-1-Format und nicht als OpenType-Schriften installiert sind, können dagegen nicht verwendet werden! Diese Einschränkung gilt beispielsweise, wenn Sie ein Adobe-Schriftpaket installiert haben. Wenn Sie mit New Font("AvantGard", 15) eine Schriftart angeben, die entweder gar nicht oder in einem anderen Format als TrueType oder OpenType installiert ist, wird ohne Fehlermeldung ein Font-Objekt mit der Schrift Microsoft Sans Serif erzeugt. Das passiert auch, wenn Ihnen bei der Angabe des Schriftnamens ein Tippfehler unterläuft!
16.3.3 Schriftarten und -familien GDI+ unterscheidet zwischen konkreten Schriftarten (Font-Klasse) und Schriftfamilien (FontFamily-Klasse). Am einfachsten ist der Unterschied anhand eines Beispiels zu er-
16.3 Text ausgeben (Font-Klassen)
869
kennen: Helvetica ist eine Schriftfamilie. Innerhalb dieser Familie gibt es vier Schriftarten (Fonts), nämlich Helvetica regular,Helvetica italic, Helvetica bold sowie Helvetica bold italic. Beachten Sie, dass Spezialattribute wie narrow oder black in GDI+ eigene Familien gründen. Daher ist Arial Narrow eine eigene Familie (wobei es wiederum die vier Schriftarten gibt, normal, kursiv, fett sowie fett-kursiv). An den bereits bekannten New-Font-Operator kann auch ein FontFamily-Objekt übergeben werden. Im folgenden Beispiel wird zuerst ein Objekt für die FontFamily erzeugt und daraus dann ein spezifisches Font-Objekt abgeleitet. Dim ff As New FontFamily("Arial") Dim fnt As New Font(ff, 15, FontStyle.Italic)
Es ist aber auch der umgekehrte Weg möglich: Sie erzeugen ein Font-Objekt und ermitteln mit der FontFamily-Eigenschaft die dazugehörende Schriftfamilie. fnt = New Font("Arial", 20) ff = fnt.FontFamily
Liste aller Schriftfamilien ermitteln Die Families-Methode der FontFamily-Klasse liefert alle am Rechner installierten TrueTypebzw. OpenType-Schriftfamilien. (Andere Schriftfamilien – z.B. PostScript-Typ-1-Schriften – werden ignoriert!) Die folgenden Zeilen zeigen, wie die Schriftfamilien in einer Schleife ermittelt werden können. Dim ff As FontFamily For Each ff In FontFamily.Families() Debug.WriteLine(ff.Name) Next
Als Alternative zu FontFamily.Families können Sie auch die Familes-Methode der Klassen InstalledFontCollection bzw. PrivateFontCollection auswerten. Beide Klassen sind im Namensraum System.Drawing.Text definiert. Tests auf meinem Rechner führten bei InstalledFontCollection zum selben Ergebnis wie bei der obigen Schleife.
VERWEIS
PrivateFontCollection ermöglicht es, eine eigene Liste von Schriftarten zusammen-
zustellen, wobei dabei sowohl systemweit installierte Schriften als auch Schriftartdateien an einem beliebigen Ort genutzt werden können. Eine Anleitung zur Anwendung von PrivateFontCollection finden Sie in der Dokumentation zu GDI+ im Artikel Creating a Private Font Collection: ms-help://MS.VSCC/MS.MSDNVS.1031/gdicpp/ufontstext_9xut.htm
Bei der Formulierung einer Schleife auf der Basis dieser Klassen sind zwei Besonderheiten zu beachten: Zum einen benötigen Sie jetzt einen New-Operator, weil (anders als bei FontFamily) die Methode Families erst zur Verfügung steht, nachdem das Objekt erzeugt wurde.
870
16 Grafikprogrammierung (GDI+)
Zum anderen muss beim Klassenname Drawing angegeben werden, obwohl System.Drawing ohnehin als Defaultimport gilt. For Each ff In New Drawing.Text.InstalledFontCollection().Families
Beispielprogramm Das folgende Beispielprogramm (siehe Abbildung 16.17) zeigt alle am Rechner verfügbaren Schriftarten in fünf Spalten an. Die Try-Catch-Konstruktion fängt Fälle auf, bei denen eine Schriftart im Stil FontStyle.Regular nicht verfügbar ist. (Auf meinem Rechner gab es z.B. bei der Schriftart Monotype Corsiva Probleme, die nur kursiv zur Verfügung steht.) ' Beispiel grafik\font-families Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim ff As FontFamily Dim fnt As Font Dim rect As RectangleF Dim i, x, y As Integer For Each ff In FontFamily.Families() x = (i Mod 5) * 160 y = CInt(Int(i / 5) * 70)# Try fnt = New Font(ff, 20, FontStyle.Regular, GraphicsUnit.Pixel) rect = New RectangleF(x, y, 150, 65) gr.DrawString(ff.Name, fnt, Brushes.Black, rect) i += 1 fnt.Dispose() Catch ' z.B. wenn eine Schriftart nicht für FontStyle.Regular ' verfügbar ist fnt = New Font("arial", 15, FontStyle.Regular, _ GraphicsUnit.Pixel) rect = New RectangleF(x, y, 150, 65) gr.DrawString("Fehler bei " + ff.Name, fnt, Brushes.Black, rect) i += 1 fnt.Dispose() End Try Next End Sub
16.3 Text ausgeben (Font-Klassen)
871
VERWEIS
Abbildung 16.17: Alle am Rechner des Autors installierten TrueType-Schriftfamilien
Ein weiteres Beispielprogramm, das die Namen aller verfügbaren Schriftarten in einer ListBox anzeigt (und zwar jeden Namen in der richtigen Schriftart), finden Sie in Abschnitt 14.6.1.
16.3.4 Schriftgröße Wenn Sie ein neues Font-Objekt erzeugen, geben Sie die gewünschte EM-Größe per Default in Punkten an. Damit Sie diesen Satz verstehen, sind natürlich ein paar Erläuterungen erforderlich: Die EM-Box ist ein Grundmaß beim Design einer Schrift. (In Zeiten des Bleisatzes konnte ein einzelner Buchstabe nicht größer werden als diese EM-Box. Mittlerweile gilt diese Einschränkung aber nicht mehr.) Ein Punkt ist die traditionelle Maßeinheit für Schriften und entspricht 1/72 Zoll, das sind 1/28 cm bzw. 0,35 mm. Dim fnt As New Font("Arial", 12)
Optional können Sie auch diverse andere Einheiten für die Größe angeben, z.B. Millimeter. Die folgende Anweisung liefert dieselbe Schrift wie oben. Dim fnt As New Font("Arial", CSng(12 * 0.35), FontStyle.Regular, _ GraphicsUnit.Millimeter)
872
16 Grafikprogrammierung (GDI+)
Schriftgröße und Bildschirmauflösung (DPI) Wie bereits in Abschnitt 15.2.9 ausführlich erläutert wurde, kann unter Windows die Bildschirmauflösung (also die Anzahl der Punkte pro Zoll, dots per inch) eingestellt werden. Die Defaulteinstellung beträgt 96 DPI. Bei hoch auflösenden Monitoren verbessert ein höherer Wert die Lesbarkeit von Texten. Im Zusammenhang mit Schriftarten ist der DPI-Wert insofern wichtig, weil die Größe von Schriften in der Regel in Punkten angegeben wird. Wie groß eine Schrift dann tatsächlich am Bildschirm erscheint (gemessen in Pixeln), hängt von der DPI-Einstellung ab. GDI+ berücksichtigt die DPI-Einstellung automatisch. Wenn Sie die Schriftgröße unabhängig von der DPI-Einstellung einstellen möchten, müssen Sie bei der New-Methode als Einheit explizit GraphicsUnit.Pixel angeben: fnt = New Font("Arial", 20, FontStyle.Regular, GraphicsUnit.Pixel)
Beschreibung der Schriftgröße Wie groß ist eine Schrift nun wirklich, gemessen in Pixeln? Zwar können Sie mit der Formel Punktgröße / 72 * DPI die Schriftgröße in Pixel leicht ausrechnen, die Frage bleibt aber weiterhin: Was ist wie viele Pixel groß? Ein normaler Großbuchstaben, z.B. ein A? Oder ein Kleinbuchstabe, z.B. ein c? Was ist mit Überhöhen (z.B. bei À), was ist mit Unterlängen (z.B. bei g)? Wenn Sie sich die Klassen Font und FontFamily näher ansehen, werden Sie eine Menge Eigenschaften bzw. Methoden entdecken, die unterschiedlichste Größenangaben in allen möglichen Einheiten liefern (Punkten, Pixeln, Designeinheiten): Size, SizeInPoint, GetHeight, GetEmHeight etc. Die Dokumentation zu diesen Schlüsselwörtern lässt dann rasch Verzweiflung aufkommen: Die Informationen sind so vage, dass sie die Zusammenhänge zwischen den zahllosen Maßen eher verschleiern.
Schriftdesign (GetXxx-Methoden der FontFamily-Klasse) Ausgangspunkt beim Design einer neuen Schrift ist traditionellerweise eine so genannte EM-Box. Als es noch Bleisatz gab, musste jeder Buchstabe in eine solche Box passen. Mittlerweile gibt es diese Einschränkung nicht mehr, aber die Höhe der EM-Box ist noch immer eine Art Basisgröße für die Schrift. Etwas größer als die Höhe der EM-Box ist der Linespacing-Abstand: Das ist der kleinstmögliche Abstand zwischen zwei Textzeilen, ohne dass es zu Überschneidungen durch Über- oder Unterlängen kommt. Bei vielen Schriftarten ergibt sich der Linespacing-Abstand einfach aus der Summe der Ascent- und Descent-Werte (siehe Abbildung 16.18). Der Ascent-Wert gibt die maximale Buchstabenhöhe oberhalb der so genannten Basislinie an. (Die Basislinie ist die Linie, an der die Buchstaben ausgerichtet werden.) Der Ascent-Wert berücksichtigt auch Überhöhen. Der Descent-Wert gibt die maximale Buchstabenhöhe unterhalb der Basislinie an (für Unterlängen).
16.3 Text ausgeben (Font-Klassen)
873
Abbildung 16.18: Ascent- und Descent-Höhe einer Schriftart
Wie hängen die vier Werte nun zusammen? Bei den meisten Schriftarten ergibt sich der Linespacing-Abstand einfach aus der Summe von Ascent und Descent. Bei manchen Schriftarten wird dazu noch ein kleiner Wert hinzugefügt. Der EM-Wert ist im Regelfall kleiner als die Summe aus Ascent und Descent (möglicherweise deswegen, weil es meistens keine Buchstaben gibt, die zugleich Überhöhe und Unterlänge haben). Fazit: Um zu berechnen, wie hoch Buchstaben werden können, ist ausschließlich die Summe aus Ascent- und Descent-Wert relevant. Aus dem EM-Wert lässt sich kein Rückschluss auf die tatsächliche Buchstabengröße ziehen. Und der Linespacing-Wert ist bei manchen Schriftarten deutlich größer als Ascent + Descent, daher also ebenfalls kein gutes Maß. Alle vier erwähnten Werte können Sie mit den Methoden GetCellAscent, GetCellDescent, GetEmHeight und GetLineSpacing der FontFamily-Klasse ermitteln. Als Parameter müssen Sie dabei die gewünschten Schriftattribute (z.B. kursiv, Elemente der FontStyle-Aufzählung) angeben, aber überraschenderweise keine Schriftgröße. Der Grund besteht darin, dass die vier Methoden ihre Ergebnisse in so genannten Design-Einheiten liefern (je nach Dokumentation auch FUnits oder font units). Diese Einheit wurde beim Design der Schriftart verwendet.
VERWEIS
Das Design der Schrift ist größenunabhängig. Verschiedene Schriftgrößen ergeben sich einfach daraus, wie stark die Buchstaben vergrößert bzw. verkleinert werden. Bei den meisten Schriftarten ist die EM-Höhe 2048 oder eine andere Zweierpotenz, weil dann eine Verkleinerung bzw. Vergrößerung der Schriften besonders effizient durchgeführt werden kann. Eine gute Einführung in die Nomenklatur, die zur Beschreibung von Zeichensätzen (speziell von TrueType-Fonts) verwendet wird, gibt die folgende Seite: http://www.microsoft.com/typography/otspec/TTCH01.htm
Umrechnung der Schriftgröße in Pixel (Font-Klasse) Auch die Font-Klasse bietet eine ganze Reihe von Eigenschaften und Methoden, die in unterschiedlichen Formen die Schriftgröße ausdrücken. Anders als bei der FontFamily-Klas-
874
16 Grafikprogrammierung (GDI+)
se gelten die Resultate nun für eine ganz bestimmte Schriftart, d.h., die Schriftgröße, die Attribute etc. werden automatisch berücksichtigt. •
Die Eigenschaft SizeInPoints gibt die Schriftgröße in Punkten an. (Die Größe bezieht sich genau genommen auf die EM-Box der Schrift.)
•
Size liefert ebenfalls die Größe der EM-Box, diesmal allerdings in der Basiseinheit des Font-Objekts, die durch die Eigenschaft Unit ausgedrückt wird.
Welche Basiseinheit das Font-Objekt hat, hängt davon ab, wie die Schriftgröße beim New-Konstruktor angegeben wurde. Wenn Sie die Schrift beispielsweise mit New Font ("Arial", 20, FontStyle.Regular, GraphicsUnit.Millimeter) erzeugt haben, dann enthält Unit die Einheit GraphicsUnit.Millimeter und Size enthält die Größe der EM-Box in Millimetern. •
Die Eigenschaft Height liefert die Schriftgröße offensichtlich in Pixeln. (Die Dokumentation behauptet, die Angabe erfolgt in current graphics units, aber es ist schleierhaft, wodurch diese current graphics units definiert sind.) Anders als bei den Size-Eigenschaften bezieht sich das Ergebnis von GetHeight auf den Linespacing-Wert der Schrift.
•
Die Methode GetHeight(gr) liefert wie Height den Linespacing-Wert der Schrift. Dabei wird die Maßeinheit des übergebenen Graphics-Objekts verwendet. Wenn gr auf ein Formular oder Steuerelement in seiner Defaulteinstellung verweist, dann gelten ebenfalls Pixel als Einheit. An GetHeight kann statt eines Graphics-Objekts auch ein DPI-Wert übergeben werden – dann wird der Linespacing-Wert in Pixeln entsprechend der DPI-Auflösung ermittelt.
Die Information, nach der Sie eigentlich suchen, nämlich die maximale Buchstabengröße (Ascent + Descent) in Pixeln, liefert Ihnen keine der Font-Eigenschaften und -Methoden. Sie können sich diese Information aber selbst ausrechnen. Dazu müssen Sie die Ergebnisse von GetCellAscent und GetCellDescent in Pixel umrechnen. Der erforderliche Umrechnungsfaktor ergibt sich aus GetHeight (Linespacing in der Graphics-Einheit) und GetLineSpacing (Linespacing in Design-Einheiten). Die folgenden Codezeilen ermitteln die vier Maße ascent, decent, linespacing und emheight in der Einheit des Graphics-Objekts. Des Weiteren wird die maximale Buchstabengröße charheight berechnet. Der Code setzt die Objektvariablen gr (Graphics) und fnt (Font) voraus. Dim ascent, descent, linespacing, emheight As Single Dim charheight As Single Dim factor As Single linespacing = fnt.GetHeight(gr) factor = linespacing / fnt.FontFamily.GetLineSpacing(fnt.Style) ascent = fnt.FontFamily.GetCellAscent(fnt.Style) * factor descent = fnt.FontFamily.GetCellDescent(fnt.Style) * factor emheight = fnt.FontFamily.GetEmHeight(fnt.Style) * factor charheight = ascent + descent
16.3 Text ausgeben (Font-Klassen)
875
Platzbedarf der Textausgabe im Voraus ermitteln (MeasureString) Zwar können Sie mit charheight (siehe die obige Formel) nun den vertikalen Platzbedarf einer Textzeile im Voraus ermitteln. Aber wie viel Platz beansprucht ein Wort oder Satz horizontal? Die Antwort gibt MeasureString: An die Methode werden die Zeichenkette und ein Font-Objekt übergeben. Als Ergebnis erhalten Sie ein SizeF-Objekt, deren Eigenschaften Width und Height den Platzbedarf angeben. In den folgenden Anweisungen wird die Zeichenkette msg im rechten unteren Eck eines PictureBox-Steuerelements ausgegeben. Die Koordinaten für DrawString ergeben sich aus der Breite bzw. Höhe des Steuerelements abzüglich des Platzbedarfs für die Textausgabe. Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics Dim fnt As New Font("Verdana", 20, FontStyle.Italic) Dim msg As String = "Abcdefg" Dim siz As SizeF = gr.MeasureString(msg, fnt) gr.DrawString(msg, fnt, Brushes.Black, _ PictureBox1.Width - siz.Width, PictureBox1.Height - siz.Height) fnt.Dispose() End Sub
Per Default ist das von MeasureString gelieferte SizeF-Objekt etwas größer als der tatsächliche Platzbedarf. Zum einen ist Width etwas breiter als erforderlich: Das soll sicherstellen, dass der Platz auch bei kursiven Buchstaben in jedem Fall ausreichend ist. (In Einzelfällen kann es passieren, dass der Platz für kursive Buchstaben dennoch zu klein ist.) Der Zusatzabstand kann auch als Wortabstand genutzt werden. Auch Height ist ein wenig zu groß und berücksichtig einen Abstand, der mindestens zwischen zwei Textzeilen eingehalten werden sollte. Wenn Sie diese Zusatzabstände vermeiden möchten, müssen Sie an MeasureString die zusätzliche Formatinformation StringFormat.GenericTypographic übergeben. Damit erreichen Sie, dass Width dem tatsächlichen Platzbedarf entspricht (ohne Reserve für Kursivierung) und dass Height exakt der maximalen Buchstabenhöhe (Ascent+Descent) entspricht. siz = gr.MeasureString(msg, fnt, 0, StringFormat.GenericTypographic)
Selbstverständlich sollte StringFormat.GenericTypographic dann auch als zusätzliche Formatinformation bei DrawString angegeben werden, sonst passen der tatsächliche und der von MeasureString ermittelte Platzbedarf nicht zusammen. In Abbildung 16.19 ist zweimal der Buchstabe f in der Schriftart Georgia mit einer Größe von 100 Punkt zu sehen. Die weiße Fläche symbolisiert jeweils den durch MeasureBox berechneten Platzbedarf, der kleine graue Kreis die Koordinatenposition für die Ausgabe des Buchstabens durch DrawString. Links erfolgte die Ausgabe und der MeasureBox-Aufruf normal, rechts unter Angabe der Formatoption GenericTypographic.
876
16 Grafikprogrammierung (GDI+)
VERWEIS
Abbildung 16.19: MeasureString-Resultat mit und ohne die Formatoption GenericTypographic
So wie DrawString Text auch auf mehrere Zeilen verteilen kann, so kann MeasureString den Platzbedarf für mehrzeilige Ausgaben berechnen. Details zur mehrzeiligen Textausgabe sowie zur Angabe von Formaten (wie GenericTypographic) folgen in Abschnitt 16.3.5.
Platzbedarf eines Leerzeichens ermitteln Der erste Versuch, den Platzbedarf eines Leerzeichen zu ermitteln, sieht wahrscheinlich folgendermaßen aus: Dim sizf As SizeF sizf = MeasureString(" ", fnt)
Das so ermittelte Ergebnis ist aber gleich aus zwei Gründen falsch: Zum einen ignorieren MeasureString und DrawString per Default Leerzeichen am Ende einer Zeichenkette. (Das eine Leerzeichen, das hier angegeben wird, gilt ebenfalls als Ende der Zeichenkette!) Zum anderen wird zum ermittelten Platzbedarf (hier 0) noch ein Übermaß hinzugefügt, um eine Platzreserve für kursive Zeichen zu lassen. Die beiden Fehler vermeiden Sie, indem Sie für die Platzberechnung ein StringFormat angeben, das von StringFormat.GenericTypographic abgeleitet ist und bei dem außerdem die Option MeasureTrailingSpaces aktiviert wird. (Hintergrundinformationen zur StringFormatKlasse folgen im nächsten Abschnitt.) Dim sf As StringFormat = StringFormat.GenericTypographic sf.FormatFlags = StringFormatFlags.MeasureTrailingSpaces sizf = gr.MeasureString(s, fnt, 0, sf)
TIPP
16.3 Text ausgeben (Font-Klassen)
877
Wenn Sie möchten, geht es auch ohne StringFormat: Berechnen Sie die Differenz der Breite von "xx" und "x x" (also einmal mit und einmal ohne einem Leerzeichen dazwischen).
Beispiel – Visualisierung von Descent, Ascent und MeasureString Um die oben beschriebenen Schriftgrößen zu erforschen (die Online-Dokumentation war nicht immer besonders aussagekräftig), habe ich ein kleines Programm entwickelt, das die verschiedenen Größen berechnet und visualisiert. Auch Abbildung 16.19 wurde mit diesem Programm erzeugt. In den Eingabeelementen des Programms können Sie den anzuzeigenden Text eingeben, eine Schriftart auswählen, die Schriftattribute fett und kursiv aktivieren und die Formatoption StringFormat.GenericTypographic (für DrawString und MeasureString) auswählen. Der Testtext wird bei jeder Änderung in einer Größe von 100 Punkt angezeigt. Der Koordinatenpunkt (x, y) für die Textausgabe wird durch einen kleinen, grauen Kreis markiert. Der durch MeasureString berechnete Platzbedarf wird durch ein weißes Rechteck dargestellt. Darüber hinaus werden der Ascent-Wert, die Basislinie, der Descent-Wert und der Linespacing-Wert durch vier vertikale Linien markiert. (Bei vielen Schriftarten ergibt die Summe von Ascent und Descent exakt den Linespacing-Wert – dann sind nur drei Linien zu sehen.) Schließlich werden in einem Textfeld alle Zahlenwerte samt Einheit angezeigt.
Abbildung 16.20: Berechnung und Visualisierung verschiedener Parameter der Schriftgröße
878
16 Grafikprogrammierung (GDI+)
Auf den Abdruck des Programms wird hier aus Platzgründen verzichtet. Sie finden den (gut kommentierten) Code auf der beiliegenden CD im Verzeichnis grafik\font-size. Wenn Sie die Zeichensatzfunktionen von GDI+ verstehen möchten, lohnt es sicher, mit dem Programm ein wenig herumzuspielen.
Beispiel – Textausgabe relativ zu einer Grundlinie Vielleicht haben Sie sich schon gefragt, warum ich so ausführlich auf die Berechnung verschiedenster Schriftgrößenparameter eingehe. Die Notwendigkeit erkennen Sie, wenn Sie versuchen, mit DrawString einige Wörter nebeneinander zu stellen, die in unterschiedlichen Schriften oder Schriftgrößen dargestellt werden sollen. Solange Sie zur Positionierung nur das Ergebnis von MeasureString verwenden, ist es unmöglich, die Wörter entlang einer Grundlinie auszurichten. In Abbildung 16.21 sehen Sie, wie ein kurzer Satz auf drei verschiedene Arten dargestellt wird. (Für jedes Wort des Satzes wird eine andere Schriftart oder -größe verwendet.) •
Ganz oben werden die Wörter entlang einer Oberkante ausgerichtet. (Dazu wird bei DrawString einfach immer dieselbe y-Koordinate angegeben.)
•
In der Mitte werden die Wörter entlang der Unterkante der MeasureString-Rechtecke ausgerichtet. Das sieht zwar schon etwas besser aus, aber die Y-Position der einzelnen Wörter variiert immer noch spürbar.
•
Unten wird zur Ausrichtung der Ascent-Wert jedes Wortes berücksichtigt. Die Wörter stehen nun wirklich auf einer Linie.
Abbildung 16.21: Beispielprogramm zur Ausrichtung von Text
Die Grundidee des Programmcodes ist leicht zu verstehen: msg enthält ein Feld mit jedem einzelnen Wort des Satzes. In drei For-Each-Schleife werden die Wörter jeweils einzeln unter Anwendung unterschiedlicher Font-Objekte ausgegeben. Um einen der Schriftgröße entsprechenden Abstand zwischen den Wörtern zu erzielen, wird x jeweils um die exakte Wortlänge (MeasureString mit StringFormat.GenericTypographic) plus ein Drittel der Buchstabenhöhe in Pixel vergrößert.
16.3 Text ausgeben (Font-Klassen)
' Beispiel grafik\font-baseline Private Sub Form1_Paint(...) Handles MyBase.Paint Dim fntnames() As String = _ {"Arial", "Times New Roman", "Comic Sans MS"} Dim msg() As String = Split("Der Umgang mit " + _ "verschiedengroßen Schriftarten ist schwierig.") Dim i As Integer Dim x, y As Single Dim s As String Dim fnt As Font Dim siz As SizeF Dim gr As Graphics = e.Graphics Dim br As Brush = Brushes.Black ' Variante 1: an der Oberkante ausgerichtet i = 0 : x = 0 : y = 10 ' y-Koordinate der Oberkante gr.DrawLine(Pens.White, 0, y, 1000, y) For Each s In msg fnt = New Font(fntnames(i Mod 3), 22 - 2 * i) gr.DrawString(msg(i), fnt, br, x, y) x += gr.MeasureString(msg(i), fnt, 0, _ StringFormat.GenericTypographic).Width x += fnt.SizeInPoints / 72 * gr.DpiX / 3 i += 1 fnt.Dispose() Next ' Variante 2: an der Unterkante ausgerichtet i = 0 : x = 0 : y = 150 'y-Koordinate der Unterkante gr.DrawLine(Pens.White, 0, y, 1000, y) For Each s In msg fnt = New Font(fntnames(i Mod 3), 22 - 2 * i) siz = gr.MeasureString(msg(i), fnt, 0, _ StringFormat.GenericTypographic) gr.DrawString(msg(i), fnt, br, x, y - siz.Height, _ StringFormat.GenericTypographic) x += ... wie oben i += 1 fnt.Dispose() Next ' Variante 3: an der Grundlinie ausgerichtet i = 0 : x = 0 : y = 250 'y-Koordinate der Unterkante gr.DrawLine(Pens.White, 0, y, 1000, y)
879
880
16 Grafikprogrammierung (GDI+)
For Each s In msg fnt = New Font(fntnames(i Mod 3), 22 - 2 * i) DrawBaseString(msg(i), gr, fnt, br, x, y) x += ... wie oben i += 1 fnt.Dispose() Next End Sub
Bei der dritten Variante wird zur Textausgabe die selbst definierte Prozedur DrawBaseString verwendet. Im Unterschied zu DrawString bezeichnet die y-Koordinate hier den Ort der Grundlinie. Innerhalb der Prozedur wird die tatsächliche y-Koordinate berechnet: dazu wird von der y-Basiskoordinate der Ascent-Wert in Pixeln abgezogen. Sub DrawBaseString(ByVal s As String, ByVal gr As Graphics, _ ByVal fnt As Font, ByVal br As Brush, ByVal x As Single, _ ByVal ybase As Single) ' die übergebene y-Koordinate gibt die Grundlinie an Dim siz As SizeF = gr.MeasureString(s, fnt) Dim y As Single y = ybase - fnt.FontFamily.GetCellAscent(fnt.Style) * _ fnt.GetHeight(gr) / fnt.FontFamily.GetLineSpacing(fnt.Style) gr.DrawString(s, fnt, br, x, y) End Sub
16.3.5 Schriftattribute, Textformatierung Schriftattribute (kursiv, fett, durchgestrichen etc.) Wenn Sie Text kursiv, fett, unter- oder durchgestrichen darstellen möchten (oder in einer beliebigen Kombination dieser Attribute), geben Sie bei der Erzeugung des Font-Objekts im optionalen dritten New-Parameter eines oder mehrere Elemente der FontStyle-Aufzählung an. (Wenn Sie Attribute kombinieren möchten, müssen Sie den Operator Or verwenden.) Dim fnt As New Font("Arial", 12, FontStyle.Bold Or FontStyle.Italic)
Layouteffekte mit der StringFormat-Klasse Bei den Methoden DrawString und MeasureString kann als optionaler Parameter ein StringFormat-Objekt angegeben werden. Dieses Objekt steuert diverse Spezialeffekte, die das Aussehen (Layout) des Texts beeinflussen. Es gibt verschiedene Möglichkeiten, StringFormat-Objekte zu erzeugen:
16.3 Text ausgeben (Font-Klassen)
Dim sf1, sf2, sf3 As StringFormat sf1 = StringFormat.GenericDefault sf2 = StringFormat.GenericTypographics sf3 = New StringFormat(formatflags)
881
'Default-Layout 'Layout für tpyographische 'Anwendungen 'Default-Layout plus 'zusätzliche Optionen
Bei der dritten Variante muss formatflags ein Element der StringFormatFlags-Aufzählung sein (oder eine durch Or verknüpfte Kombination dieser Elemente). Die folgende Aufzählung beschreibt ganz kurz einige StringFormatFlags-Elemente. (Eine vollständige und ausführlichere Beschreibung finden Sie in der Online-Hilfe.) •
DirectionVertical: Der Text wird vertikal von oben nach unten ausgegeben. (Eine Ausgabe vertikal von unten nach oben oder andere Textdrehungen können nicht durch ein StringFormat-Objekt erzielt werden. Tipps, wie Text in beliebigen Winkeln verdreht werden kann, folgen aber etwas weiter unten.)
•
LineLimit: Textzeilen werden entweder vertikal vollständig oder gar nicht ausgegeben. (Per Default funktioniert DrawString auch dann, wenn der Platz für die Ausgabe unzu-
reichend ist. In diesem Fall werden die auszugebenden Buchstaben unten abgeschnitten. Mit LineLimit vermeiden Sie abgeschnittene Buchstaben.) MeasureTrailingSpaces: Dieses Element bewirkt, dass MeasureString auch Leerzeichen am Ende einer Zeichenkette berücksichtigt. (Per Default ist das nicht der Fall. Leerzeichen am Beginn einer Zeichenkette werden dagegen immer berücksichtigt.)
•
NoWrap: Dieses Element bewirkt, dass zu lange Zeilen nicht automatisch umbrochen werden, wie dies per Default geschieht (siehe die Überschrift Mehrzeilige Textausgaben).
ANMERKUNG
•
Wenn man sich ein StringFormat.GenericTypographics-Objekt genauer ansieht, so scheint es, als würde es sich dabei um ein einfaches StringFormat-Objekt mit den Formatflags FitBlackBox, LineLimit und NoClip handeln. Wie aus Abbildung 16.19 einige Seiten weiter oben hervorgeht, bewirken diese Formatierungsoptionen, dass DrawString den Text so knapp wie möglich positioniert und dass MeasureString die kleinstmöglichen Größenangaben liefert (ohne Platzreserven für Überstände durch Kursivierung). Bei mehrzeiligem Text wird der Abstand zwischen den Zeilen nicht reduziert, MeasureString berücksichtigt unterhalb der letzten Zeile aber keinen zusätzlichen Zeilenabstand mehr. Merkwürdigerweise ist jeder Versuch gescheitert, mit New StringFormat(flags) ein Objekt zu erzeugen, das denselben Effekt wie ein StringFormat.GenericTypographicsObjekt hat. Bei diesen Experimenten habe ich überhaupt den Eindruck gewonnen, als würden manche StringFormatFlags-Elemente, die bei einem StringFormat.GenericTypographics-Objekt offensichtlich funktionieren, bei einem normalen StringFormatObjekt einfach ignoriert werden. Dieser Umstand und die miserable Dokumentation in der Online-Hilfe haben es leider unmöglich gemacht, StringFormat und insbesondere StringFormatFlags-Elemente klarer zu beschreiben.
882
16 Grafikprogrammierung (GDI+)
Die meisten durch StringFormat erzielbaren Layouteffekte erreichen Sie allerdings nicht durch die StringFormatFlags, sondern indem Sie einzelne Eigenschaften des StringFormat-Objekts verändern. Die folgenden Absätze fassen die wichtigsten Möglichkeiten zusammen: Rechtsbündiger und zentrierter Text: Die Alignment-Eigenschaft bestimmt die horizontale Ausrichtung von Text. Normalerweise wird Text linksbündig ausgerichtet (links von der durch DrawString angegebenen x-Koordinate). sf.Alignment=StringAlignment.Far bewirkt in unserem Sprachraum eine rechtsbündige Formatierung. Die x-Koordinate gibt nun den rechten Begrenzungspunkt für die Ausgabe an. (Bei einigen asiatischen Sprachen, bei denen die Schrift üblicherweise von rechts nach links ausgegeben wird, ist die Wirkung von Far gerade umgekehrt.) Center statt Far bewirkt eine Zentrierung des Texts. Tastenkürzel anzeigen: Steuerelemente und Menüeinträge können üblicherweise auch durch Alt-Tastenkürzel aktiviert bzw. ausgewählt werden. Das Tastenkürzel wird im Beschriftungstext durch ein vorangestelltes &-Zeichen angegeben. Im Steuerelement wird dieses Zeichen aber nicht ausgegeben, vielmehr wird der nachfolgende Buchstabe unterstrichen. Genau diesen Effekt erzielen Sie durch die Einstellung HotkeyPrefix = Drawing.Text.HotkeyPrefix.Show. Text vertikal ausrichten: LineAlignment funktioniert wie Alignment, steuert aber die vertikale Ausrichtung. Durch sf.LineAlignment=StringAlignment.Far erreichen Sie, dass der Text an der durch die y-Koordinate vorgegebenen Unterkante ausgerichtet wird. Center bewirkt eine vertikale Zentrierung der Ausgabe. Text kürzen: Oft stehen Sie vor dem Problem, dass der Platz für die Textausgabe beschränkt ist. Die Lösung besteht dann meist darin, den auszugebenden Text zu kürzen. Diese Arbeit kann auch DrawString übernehmen, wenn Sie beim StringFormat-Objekt die Eigenschaft Trimming richtig einstellen. Trimming wird oft dann eingesetzt, wenn die Textausgabe in nur einer Zeile erfolgen soll. Daher muss das StringFormat-Objekt mit StringFormatFlags.NoWrap erzeugt werden. Besonders praktisch ist die Kürzung von Datei- und Verzeichnisnamen (sf.Trimming = StringTrimming.EllipsisPath): Dabei wird der Dateiname in der Mitte gekürzt, so dass Anfang und Ende lesbar bleiben (siehe Abbildung 16.26). Andere Elemente der StringTrimming-Aufzählung ermöglichen andere Formen der Textkürzung: Beispielsweise entfernt EllipsisWord das Ende eines Satzes an einer Wortgrenze und fügt stattdessen drei Punkte ein. Tabulatoren: Dank StringFormat können Sie bei der Textausgabe sogar einfache Tabulatoren verwenden. Dazu müssen Sie zuerst mit der Methode SetTabStops(n, m()) die Position der Tabulatoren bestimmen. n gibt den Abstand vor dem ersten Tabulator an und ist meistens 0. m ist ein Single-Feld und gibt die Abstände zwischen den Tabulatoren an. Als Maßeinheit wird die Maßeinheit des Grafikobjekts verwendet, auf das DrawString angewendet wird (und nicht Zeichengröße, wie die Hilfe fälschlich behauptet). Bei einer Ausgabe am Bildschirm sind das im Regelfall Pixel. Die folgenden Zeilen definieren zwei Tabulatoren an den Positionen 130 und 260. Dim tabs() As Single = {130, 130} Dim sf As New StringFormat(StringFormatFlags.NoWrap) sf.SetTabStops(0, tabs)
16.3 Text ausgeben (Font-Klassen)
883
Bei der Textausgabe müssen Sie nun in den Text Tabulatorzeichen einfügen (Code 9, Konstante vbTab). StringFormat kennt leider nur linksbündige Tabulatoren.
Beispiel – StringFormat-Anwendungen Das folgende Beispielprogramm (siehe Abbildung 16.22) demonstriert einige Effekte, die Sie mit StringFormat erzielen können. Wenn Sie das Programm ausführen, sollten Sie die Fenstergröße variieren – einige Texte ändern dann dynamisch ihre Position.
Abbildung 16.22: Demonstration verschiedener StringFormat-Effekte
' Beispiel grafik\font-stringformat Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim fnt As Font Dim br As Brush = Brushes.Black Dim rectf As RectangleF Dim sf As StringFormat Dim s As String ' vertikaler Text fnt = New Font("Arial", 25, FontStyle.Regular, GraphicsUnit.Pixel) sf = New StringFormat(StringFormatFlags.DirectionVertical) gr.DrawString("vertikaler Text", fnt, br, 0, 0, sf) sf.Dispose() ' rechtsbündiger Text sf = New StringFormat() sf.Alignment = StringAlignment.Far s = "rechtsbündiger" + vbCrLf + "Text" gr.DrawString(s, fnt, br, Me.ClientSize.Width, 0, sf) sf.Dispose()
884
16 Grafikprogrammierung (GDI+)
' an der Unterkante ausgerichter Text sf = New StringFormat() sf.LineAlignment = StringAlignment.Far s = "an der Unterkante ausgerichteter Text" gr.DrawString(s, fnt, br, 50, Me.ClientSize.Height, sf) sf.Dispose() ' Dateinamen auf vorgegebene Länge kürzen sf = New StringFormat(StringFormatFlags.NoWrap) sf.Trimming = StringTrimming.EllipsisPath rectf = New RectangleF(50, 70, Me.ClientSize.Width - 60, 0) s = "Verzeichnisnamen kürzen:" + vbCrLf + CurDir() gr.DrawString(s, fnt, br, rectf, sf) sf.Dispose() ' mehrspaltiger Text (Tabulatoren) Dim tabs() As Single = {130, 130} sf = New StringFormat(StringFormatFlags.NoWrap) sf.SetTabStops(0, tabs) s = "mehrspaltiger Text mit Tabulatoren" + vbCrLf + _ "Spalte 1" + vbTab + "Spalte 2" + vbTab + "Spalte 3" + vbCrLf + _ "A" + vbTab + "B" + vbTab + "C" + vbCrLf + _ "1" + vbTab + "23" + vbTab + "456" gr.DrawString(s, fnt, br, 50, 160, sf) sf.Dispose() fnt.Dispose() End Sub ' bei Änderung der Fenstergröße alles neu zeichnen Private Sub Form1_Resize() Handles MyBase.Resize Me.Invalidate() End Sub
Mehrzeiliger Text Wie Abbildung 16.22 oben bereits beweist, kommt DrawString auch mit mehrzeiligem Text zurecht. Wenn Sie den Zeilenumbruch selbst vorgeben möchten, müssen Sie die einzelnen Zeilen jeweils durch ein Linefeed-Zeichen (vbLf oder vbCrLf) trennen: gr.DrawString("Abc" + vbLf + "efg", fnt, Brushes.Black, 10, 10) DrawString kann sich allerdings auch selbst um einen Zeilenumbruch zu kümmern (siehe Abbildung 16.23)! Dazu übergeben Sie an DrawString statt einer Koordinatenposition ein RectangleF-Objekt, das das Rechteck angibt, in dem die Ausgabe erfolgen soll.
Wenn Sie vermeiden möchten, dass bei Platzproblemen die letzte sichtbare Zeile abgeschnitten wird (siehe Abbildung 16.23 links), müssen Sie zur Textausgabe ein StringFormat-
16.3 Text ausgeben (Font-Klassen)
885
Objekt mit dem Flag LineLimit verwenden. Im Beispielprogramm wird derselbe Text zweimal angezeigt, einmal linksbündig und einmal zentriert mit der LineLimit-Option. ' beispiele\font-wrapping Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim fnt As New Font("Arial", 20, FontStyle.Regular, _ GraphicsUnit.Pixel) Dim br As Brush = Brushes.Black Dim rectf As RectangleF Dim s As String = "DrawString kümmert sich bei Bedarf " + _ "selbst um einen Zeilenumbruch! " s = s + s + s ' erstes Rechteck: linksbündig rectf = New RectangleF(10, 10, 300, 150) gr.DrawString(s, fnt, br, rectf) gr.DrawRectangle(Pens.Gray, Rectangle.Ceiling(rectf)) ' zweites Rechteck: zentriert, mit LineLimit Dim sf As New StringFormat(StringFormatFlags.LineLimit) sf.Alignment = StringAlignment.Center rectf.Offset(350, 0) gr.DrawString(s, fnt, br, rectf, sf) gr.DrawRectangle(Pens.Gray, Rectangle.Ceiling(rectf) fnt.Dispose() sf.Dispose() End Sub
Abbildung 16.23: Automatischer Zeilenumbruch mit DrawString
Der automatische Zeilenumbruch unterliegt gewissen Einschränkungen: Es ist unmöglich, mehrere Schriftarten zu mischen. Ebenso wird kein Blocksatz unterstützt, es gibt keine Silbentrennung etc. Wenn Sie also vorhaben, Ihr eigenes Textverarbeitungsprogramm zu schreiben, müssen Sie sich selbst um den Zeilenumbruch kümmern. Natürlich kann MeasureString dazu verwendet werden, den Platzbedarf für mehrzeiligen Text zu ermitteln. Die folgende Anweisung ermittelt den Platzbedarf für die erste Textaus-
886
16 Grafikprogrammierung (GDI+)
gabe im obigen Beispielprogramm. sizf.Width enthält die tatsächlich beanspruchte Breite, ist also etwas schmäler als das zugrunde liegende Rechteck. sizf.Height ist in diesem Fall gleich groß wie die Höhe des Rechtecks. Wenn der Text s kürzer wäre und das vorgegebene Rechteck nicht vollständig füllen würde, dann würde sizf.Height ebenfalls nur den tatsächlich beanspruchten Raum angeben. Dim sizf As SizeF = gr.MeasureString(s, fnt, rectf.Size)
Rotierter Text GDI+ sieht keine eigene Methode vor, um Zeichenketten unter Angabe eines bestimmten Winkels auszugeben. Selbstverständlich ist es aber dennoch möglich, rotierten Text in allen erdenklichen Winkeln darzustellen. Sie müssen dazu lediglich das Koordinatensystem des Graphics-Objekt transformieren (siehe auch Abschnitt 16.2.5). Natürlich ist das ein wenig unbequem – eine Transformation des Koordinatensystems betrifft ja alle Grafikausgaben; sie muss daher in der Regel nach der Ausgabe der Zeichenkette wieder rückgängig gemacht werden. Zudem ist es ohne ein wenig Übung bisweilen schwierig, die richtigen Transformationskommandos zu finden. Die folgenden Zeilen demonstrieren die prinzipielle Vorgehensweise. (Vorausgesetzt werden das Graphics-Objekt gr, ein Font-Objekt fnt, die Koordinaten x und y sowie der gewünschte Drehungswinkel angle.) Zuerst wird die aktuelle Transformationsmatrix gespeichert. Anschließend wird eine allfällige bereits vorhandene Transformation gelöscht. Mit TranslateTransform wird der Mittelpunkt des Koordinatensystems an den Punkt verschoben, an dem die Ausgabe erfolgen soll. Mit RotateTransform wird das Koordinatensystem anschließend im Uhrzeigersinn gedreht. Beachten Sie, dass bei der nun folgenden Ausgabe der Koordinatenpunkt (0, 0) verwendet wird! Dim oldtrans As Drawing2D.Matrix = gr.Transform gr.ResetTransform() gr.TranslateTransform(x, y) gr.RotateTransform(angle) gr.DrawString(s, fnt, Brushes.Black, 0, 0) gr.Transform = oldtrans
'alte Transf.matrix
Beispiel 1 – Rotierter Text Das folgende Beispielprogramm gibt eine kurze Zeichenkette unter verschiedenen Winkeln aus (siehe Abbildung 16.24). Der Startpunkt für die Ausgabe jeden Texts sowie das Ausgaberechteck wird durch einen kleinen Kreis bzw. durch ein Rechteck hervorgehoben, um das Verständnis des Codes ein wenig zu erleichtern.
16.3 Text ausgeben (Font-Klassen)
887
Abbildung 16.24: Rotierter Text
' Beispiel grafik\font-rotation Option Strict On Imports System.Math Public Class Form1 Inherits System.Windows.Forms.Form [... Vom Windows Form Designer generierter Code...] ' bei Größenänderung vollständig neu zeichnen Private Sub Form1_Load(...) Handles MyBase.Load Me.ResizeRedraw = True End Sub Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim angle As Single Dim x0, y0, x, y, radius As Single Dim s As String = "Textausgabe mit GDI+" Dim br1 As Brush = Brushes.Black Dim br2 As Brush = Brushes.Gray Dim pn As Pen = Pens.Gray Dim fnt As New Font("Arial", 20, FontStyle.Regular, _ GraphicsUnit.Pixel) Dim sf As StringFormat = StringFormat.GenericTypographic sf.LineAlignment = StringAlignment.Center Dim sizf As SizeF x0 = CSng(Me.ClientSize.Width / 2) y0 = CSng(Me.ClientSize.Height / 2) radius = 70
'Fenstermittelpunkt 'Innenkreisradius
888
16 Grafikprogrammierung (GDI+)
For angle = 0 To 359 Step 30 ' Koordinatenpunkt für die Ausgabe ermitteln x = x0 + radius * CSng(Cos(angle / 180 * PI)) y = y0 + radius * CSng(Sin(angle / 180 * PI)) ' Koordinatentransformation gr.ResetTransform() gr.TranslateTransform(x, y) gr.RotateTransform(angle) ' Text ausgeben gr.DrawString(s, fnt, br1, 0, 0, sf) ' Startkoordinate und Ausgaberechteck darstellen gr.FillEllipse(br2, -5, -5, 10, 10) sizf = gr.MeasureString(s, fnt, 0, sf) gr.DrawRectangle(pn, _ 0, -sizf.Height / 2, sizf.Width, sizf.Height) Next fnt.Dispose() sf.Dispose() End Sub End Class
Beispiel 2 – Rotierter Text Im obigen Beispiel wurden die erforderlichen Koordinatentransformationen direkt im Code ausgeführt. Wenn Sie sich damit nicht plagen möchten, hilft Ihnen vielleicht die Prozedur DrawRotatedString, die im folgenden Beispielprogramm vorgestellt wird. An diese Prozedur übergeben Sie ähnlich wie an die Methode DrawString die Zeichenkette, ein Graphics-Objekt, ein Font-Objekt, ein Brush-Objekt sowie x- und y-Koordinate. Außerdem geben Sie den gewünschten Drehungswinkel an. Besonders praktisch ist der letzte Parameter, der die Position des Rotationspunkt relativ zur Zeichenkette angibt. Zur Auswahl stehen UpperLeft, UpperMiddle, UpperRight, CenterLeft, CenterMiddle, CenterRight, LowerLeft, LowerMiddle und LowerRight. In Abbildung 16.25 ist der Rotationspunkt jeweils durch einen kleinen grauen Kreis hervorgehoben. Die folgenden Zeilen zeigen die Anwendung der Prozedur: ' Beispiel grafik\drawrotatedstring Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim fnt As New Font("Arial", 20) Dim br As Brush = Brushes.Black Dim s As String = "Grafik mit GDI+"
16.3 Text ausgeben (Font-Klassen)
889
DrawRotatedString(s, gr, fnt, br, 10, 10, 30, _ XYPosition.UpperLeft) DrawRotatedString(s, gr, fnt, br, 400, 150, -30, _ XYPosition.LowerRight) DrawRotatedString(s, gr, fnt, br, 600, 100, 215, _ XYPosition.CenterMiddle) fnt.Dispose() End Sub
Abbildung 16.25: Textausgabe mit DrawRotatedString
Der Code für DrawRotatedString ist leicht zu verstehen. Die Transformationsmatrix des Graphics-Objekts wird in oldtrans zwischengespeichert. Anschließend wird ein StringFormatObjekt entsprechend dem pos-Parameter initialisiert. (Hier sind zwei Select-Case-Blöcke erforderlich, weil in einem Block maximal ein Case-Zweig ausgeführt werden kann.) Die weiteren Kommandos zur Koordinatentransformation und Ausgabe sind bereits aus dem vorigen Beispielprogramm vertraut. Enum XYPosition UpperLeft ... LowerRight End Enum Private Sub DrawRotatedString( _ ByVal s As String, ByVal gr As Graphics, _ ByVal fnt As Font, ByVal br As Brush, _ ByVal x As Single, ByVal y As Single, _ ByVal angle As Single, ByVal pos As XYPosition) Dim sf As StringFormat = StringFormat.GenericTypographic Dim oldtrans As Drawing2D.Matrix = gr.Transform
890
16 Grafikprogrammierung (GDI+)
Select Case pos Case XYPosition.LowerLeft, XYPosition.LowerMiddle, _ XYPosition.LowerRight sf.LineAlignment = StringAlignment.Far Case XYPosition.CenterLeft, XYPosition.CenterMiddle, _ XYPosition.CenterRight sf.LineAlignment = StringAlignment.Center End Select Select Case pos Case XYPosition.CenterRight, XYPosition.LowerRight, _ XYPosition.UpperRight sf.Alignment = StringAlignment.Far Case XYPosition.CenterMiddle, XYPosition.LowerMiddle, _ XYPosition.UpperMiddle sf.Alignment = StringAlignment.Center End Select ' Transformation gr.ResetTransform() gr.TranslateTransform(x, y) gr.RotateTransform(angle) ' Text ausgeben gr.DrawString(s, fnt, br, 0, 0, sf) ' Startkoordinate darstellen ' gr.FillEllipse(Brushes.Gray, -5, -5, 10, 10) ' ursprüngliche Transformationsmatrix wieder herstellen gr.Transform = oldtrans ' StringFormat-Objekt freigeben sf.Dispose End Sub
16.3.6 Font-Auswahldialog Zur interaktiven Auswahl einer Schriftart können Sie auf das FontDialog-Steuerelement zurückgreifen, das Sie aus der Toolbox in Ihr Formular einfügen. Die Anwendung des Steuerelements ist dann denkbar einfach: Mit ShowDialog zeigen Sie den Dialog an. Sofern eine gültige Auswahl durchgeführt wurde (Rückgabewert auswerten!), können Sie die Schriftart anschließend der Font-Eigenschaft entnehmen. Das Aussehen des Dialogs kann durch drei Optionen beeinflusst werden: ShowColor gibt an, ob auch die Farbe der Schrift ausgewählt werden darf. ShowEffects gibt an, ob die Zusatzattribute unterstrichen und durchgestrichen zur Auswahl stehen. ShowApply gibt an, ob der Button ÜBERNEHMEN angezeigt wird. Damit kann die Schriftauswahl schon vor dem Dialogende ausprobiert werden. (Damit das funktioniert, muss wie im folgenden Beispielprogramm das Apply-Ereignis des Steuerelements ausgewertet werden.)
16.3 Text ausgeben (Font-Klassen)
891
Wenn Sie (z.B. bei einem Texteditor) nur die Auswahl von solchen Schriften zulassen möchten, bei denen die Buchstabenbreite konstant ist (Courier etc.), müssen Sie die Eigenschaft FixedPitchOnly auf True setzen. Das folgende Miniprogramm demonstriert die Anwendung des FontDialog-Steuerelements (siehe Abbildung 16.26). Nach der Auswahl wird die Schriftart des Label-Steuerelements verändert.
Abbildung 16.26: Schriftartauswahl mit dem FontDialog-Steuerelement
' Beispiel grafik\font-selection Private Sub Button1_Click(...) Handles Button1.Click ' bisherige Einstellungen speichern Dim oldfnt As Font = Label1.Font Dim oldcolor As Color = Label1.ForeColor With FontDialog1 .Color = Label1.ForeColor 'Startfarbe .Font = Label1.Font 'Startfont .ShowApply = True 'Überehmen-Button .ShowColor = True 'Farbauswahl .ShowEffects = True 'unterstreichen, durchstreichen If .ShowDialog() = DialogResult.OK Then ' erfolgreiche Fontauswahl: Eigenschaften von Label1 ändern Label1.Font = .Font Label1.ForeColor = .Color Label1.Text = .Font.Name Else ' Abbruch: bisherige Eigenschaften wiederherstellen Label1.Font = oldfnt
892
16 Grafikprogrammierung (GDI+)
Label1.Text = oldfnt.Name Label1.ForeColor = oldcolor End If End With oldfnt.Dispose() End Sub ' Reaktion auf Übernehmen-Button des Font-Dialogs Private Sub FontDialog1_Apply(...) Handles FontDialog1.Apply Label1.Font = FontDialog1.Font Label1.Text = FontDialog1.Font.Name Label1.ForeColor = FontDialog1.Color End Sub
16.3.7 Syntaxzusammenfassung In dieser Syntaxzusammenfassung ist fnt ein Font-Objekt, ff ein FontFamily-Objekt, sf ein StringFormat-Objekt, fs ein Element der FontStyle-Aufzählung, gr ein Graphics-Objekt und sizf ein SizeF-Objekt. Alle erwähnten Schlüsselwörter sind Teil der System.Drawing-Bibliothek. Basisoperationen mit Fonts Dim fnt As Font
deklariert fnt als Variable für Font-Objekte.
fnt = New Font("name", n)
erzeugt ein neues Font-Objekt für die Schriftart Name in der Größe n. (Genau genommen gibt n die EM-Box in Punkten an.)
fnt = New Font("name", n, fs)
wie oben, aber mit den durch fs angegebenen Attributen (kursiv, fett etc.).
gr.DrawString(s, fnt, br, x, y)
gibt die Zeichenkette s in der Schriftart fnt und in der Farbe (und dem Muster) br an den Koordinaten x und y aus.
gr.DrawString(s, fnt, br, rectf)
wie oben, aber mit automatischem Zeilenumbruch innerhalb des angegebenen Rechtecks.
sizf = MeasureString(s, fnt)
ermittelt den Platzbedarf (sizf.Width und .Height) für die Textausgabe.
Schriftfamilien (FontFamily-Klasse) ff = fnt.FontFamily
liefert die Schriftfamilie (FontFamily-Objekt) zur angegebenen Schriftart.
fnt = New Font(ff, ...)
erzeugt ein neues Font-Objekt zur angegebenen Schriftfamilie.
FontFamily.Families()
liefert ein Feld aller installierten Schriftfamilien.
16.3 Text ausgeben (Font-Klassen)
893
Ermittlung der Schriftgröße fnt.Height
liefert den Minimalabstand zwischen zwei Textzeilen (LineSpacing). Einheit: Pixel.
fnt.GetHeight(gr)
liefert den Minimalabstand zwischen zwei Textzeilen (LineSpacing), wenn die Ausgabe im angegebenen Graphics-Objekt erfolgt. Einheit: gr.PageUnit (per Default Pixel).
fnt.GetHeight(dpi)
wie oben, aber für die Ausgabe in einem Zeichenobjekt, das die angegebene Auflösung hat.
fnt.Size
liefert die EM-Größe der Zeichensatzes. Einheit: fnt.Unit (per Default Punkte).
fnt.SizeInPoints
liefert die EM-Größe des Zeichensatzes. Einheit: Punkte.
ff.GetCellAscent(fs)
liefert die maximale Buchstabenhöhe oberhalb der Grundlinie für das angegebene Schriftattribut (Element der FontStyle-Aufzählung). Einheit: Design-Einheiten (FUnits).
ff.GetCellDescent(fs)
wie oben, aber für den Platzbedarf unterhalb der Grundlinie. Einheit: Design-Einheiten.
ff.GetEmHeight(fs)
liefert die Höhe einer EM-Box. Die EM-Box ist ein Rastermaß beim Design von Zeichensätzen. Einheit: Design-Einheiten.
ff.GetLineSpacing(fs)
liefert den minimalen Vertikalabstand zweier Textzeilen. Einheit: Design-Einheiten.
gr.MeasureString("xxx", fnt)
liefert ein SizeF-Objekt mit dem Platzbedarf für die Textausgabe.
Format- und Layoutoptionen (StringFormat-Klasse) sf = New StringFormat(flags)
erzeugt ein neues StringFormat-Objekt. flags sind Elemente der StringFormatFlags-Aufzählung (z.B. NoWrap, um einen automatischen Zeilenumbruch zu verhindern).
sf = StringFormat. _ GenericTypographics
bewirkt, dass DrawString und MeasureString keine Zusatzabstände für Zeilenabstand, Kursivierung etc. berücksichtigen.
sf.Alignment = sta
steuert die horizontale Ausrichtung. sta ist ein Element der StringAlignment-Aufzählung (z.B. Near, Far oder Center).
sf.LineAlignment = sta
steuert die vertikale Ausrichtung von Text.
sf.Trimming = st
steuert die automatische Verkürzung von zu langem Text. st ist ein Element der StringTrimming-Aufzählung (z.B. EllipsisPath).
894
16 Grafikprogrammierung (GDI+)
Format- und Layoutoptionen (StringFormat-Klasse) sf.SetTabstops(n, m)
16.4
setzt linksbündige Tabulatoren an den Positionen n+m(0), n+m(0)+m(1), n+m(0)+m(1)+m(2) etc. n ist ein Single-Wert (meist 0), m ein Single-Feld mit den Abständen zwischen den Tabulatoren. Als Maßeinheit wird die Einheit des Graphics-Objekts verwendet.
Bitmaps, Icons und Metafiles
Dieser Abschnitt beschreibt Möglichkeiten, Bitmaps, Icons und Metafiles innerhalb von Formularen oder Steuerelementen anzuzeigen und zu manipulieren.
16.4.1 Graphics- versus Image- versus Bitmap-Klasse Bis jetzt hat sich dieses Kapitel relativ wenig um die Grundlagen der Graphics-Klasse gekümmert. Grafikausgaben erfolgten bis jetzt immer in Paint-Ereignisprozeduren. Ein Graphics-Objekt stand als Parameter dieser Ereignisprozedur immer zur Verfügung und wurde dazu verwendet, um diverse Draw- und Fill-Methoden darauf anzuwenden. Um nun aber den Umgang mit Bitmaps richtig verstehen zu können, ist eine etwas eingehendere Beschreibung der drei in der Überschrift genannten Klassen erforderlich. Der folgende Hierarchiebaum zeigt, wie die Klassen voneinander vererbt sind. (In das Hierarchiediagramm wurden auch einige verwandte Klassen miteinbezogen. Die in diesem Abschnitt behandelten Klassen sind fett hervorgehoben.) Klassenhierarchie in der System.Drawing-Bibliothek Object └─ MarshalByRefObject
│ ├─ Graphics ├─ Image │ ├─ Bitmap │ └─ Metafile └─ Icon
.NET-Basisklasse Objekt nur als Referenz an andere Rechner weitergeben Klasse zur Durchführung von Zeichenoperationen abstrakte Basisklasse für Bitmaps und Metafiles Verwaltung einer Bitmap Verwaltung eines Metafiles Verwaltung eines Icons
Ein Graphics-Objekt ist ausschließlich ein Hilfsmittel, um Grafikoperationen durchzuführen (Linien zeichnen, Polygone füllen, Text ausgeben etc.). Es kümmert sich aber nicht um die Speicherung der Daten. (Aus diesem Grund muss der Inhalt eines Fensters bzw. Steuerelements auch immer wieder neu gezeichnet werden, wenn das Fenster zwischenzeitlich verdeckt ist.) Abschließend noch ein Hinweis für GDI-Experten: Das Graphics-Objekt von GDI+ hat eine vergleichbare Funktion wie der Device Context (DC) in GDI.
16.4 Bitmaps, Icons und Metafiles
895
Die Image-Klasse ist eine abstrakte Klasse, von der die Klassen Bitmap (Bitmap-Grafik) und Metafile (Vektorgrafik) abgeleitet sind. Die Klasse stellt also lediglich gemeinsame Methoden und Eigenschaften zur Verfügung. Bitmap-Objekte dienen dazu, Bitmaps zu verwalten (also aus Dateien zu laden, im Arbeitsspeicher zu halten, wieder in Dateien zu speichern etc.). Im Gegensatz zum Graphics-Objekt gibt es aber nur relativ wenig Methoden, um den Bildinhalt zu verändern. (Die wichtigste Methode lautet SetPixel und ermöglicht es, die Farbe eines einzelnen Bildpunkts zu verändern.)
HINWEIS
Sowohl das Graphics- als auch ein Bitmap-Objekt ist an sich unsichtbar! Sichtbar werden derartige Objekte erst, wenn Sie in einem Formular oder Steuerelement dargestellt werden (und dazu gibt es natürlich verschiedene Wege). Falls Ihnen der Begriff Bitmap Probleme bereitet, hier der Versuch einer Definition: Eine Bitmap ist eine Datenmenge, um die Farbe jedes Punktes (jedes Pixels) eines rechteckigen Bereichs zu speichern. Sie können sich eine Bitmap auch als zweidimensionales Feld pixel(x,y) vorstellen, das für jeden Punkt die Farbinformationen enthält. In der Praxis haben Sie ständig mit Bitmaps zu tun: Jedes gescannte Bild, jedes Foto, das mit einer Digitalkamera aufgenommen wurde, jedes mit Photoshop erzeugte Bild ist eine Bitmap. Es gibt zahllose Formate, um Bitmaps in Dateien zu speichern. So ist unter Windows vor allem das BMP-Format verbreitet, während Sie im Internet überwiegend GIF- und JPEG-Dateien vorfinden. Diese Formate unterscheiden sich vor allem durch ihren Speicherbedarf und die Darstellungsqualität. (Bei einigen Formaten werden die Bildinformationen komprimiert, um so kleinere Dateien zu erreichen. Bei den meisten Kompressionsverfahren gehen allerdings – für das menschliche Auge oft fast unmerklich – Bildinformationen verloren.)
Icon-Objekte dienen wenig überraschend dazu, Icons zu verwalten. Zur Bearbeitung von
Icons stehen zwar nur ziemlich wenig Methoden zur Auswahl, aber gegebenenfalls können Sie ein Icon in eine Bitmap umwandeln. Metafile-Objekte helfen laut Dokumentation dabei, Metafile-Dateien zu lesen, anzuzeigen und zu speichern. In der Praxis ist allerdings nur die Ausführung der beiden ersten Aufgaben gelungen. Alle Versuche, selbst eine neue Metafile-Datei zu erstellen, sind gescheitert.
Zusammenhang zwischen Bitmap- und Graphics-Objekten herstellen Sie können ein Bitmap-Objekt nicht in ein Graphics-Objekt umwandeln, weil die beiden Klassen grundverschieden sind. Es gibt aber natürlich Verbindungen zwischen den beiden Klassen. (In der folgenden Aufzählung ist gr ein Graphics-Variable, bm ein Bitmap-Variable.) •
Mit gr.DrawImage(bm, ...) können Sie eine Bitmap in ein Graphics-Objekt ausgeben (dorthin kopieren).
896
•
16 Grafikprogrammierung (GDI+)
VERWEIS
Mit gr = Graphics.FromImage(bm) können Sie ein neues Graphics-Objekt erzeugen, mit dessen Hilfe Sie anschließend alle Draw- und Fill-Methoden direkt auf die Bitmap anwenden können. Das Graphics-Objekt muss nach seiner Verwendung mit Dispose freigegeben werden. Während FromImage die Möglichkeit bietet, ein Graphics-Objekt für eine Bitmap zu erzeugen, ist die umgekehrte Richtung nicht vorgesehen: Sie können also nicht ohne weiteres ein Bitmap-Objekt erzeugen, das den aktuellen Inhalt eines Graphics-Objekts enthält. Tipps, wie es eventuell dennoch geht, gibt Abschnitt 16.5.10.
Wenn Sie einen Zusammenhang zwischen dem Inhalt eines Formulars bzw. Steuerelements und einem Bitmap- bzw. Graphics-Objekt herstellen möchten, helfen die folgenden Methoden. (ctr ist der Name eines Steuerelements oder Formulars.) •
Mit gr = Graphics.FromHwnd(ctr.Handle) erhalten Sie ein Graphics-Objekt, mit dem Sie auch außerhalb einer Paint-Ereignisprozedur im Inneren eines Steuerelements zeichnen dürfen. (Die Eigenschaft Handle liefert eine Windows-interne Identifikationsnummer, die den Zeichenbereich eines Steuerelements angibt. FromHwnd bildet daraus ein Graphics-Objekt.) Das Graphics-Objekt muss nach seiner Verwendung mit Dispose freigegeben werden. Beachten Sie, dass die Methode FromHwnd aus Sicherheitsgründen nur funktioniert, wenn das Programm von einer lokalen Festplatte (nicht von einem Netzwerklaufwerk) ausgeführt wird! Andernfalls kommt es zu einem SecurityException-Fehler.)
•
Die Methode gr = frm.CreateGraphics() liefert dasselbe Ergebnis wie FromHwnd, funktioniert allerdings nur für Form-Objekte (nicht für Steuerelemente). Abermals sollten Sie Dispose nicht vergessen.
•
Mit ctr.[Background]Image = im erreichen Sie, dass die in im gespeicherte Bitmap innerhalb des Steuerelements angezeigt wird. Beachten Sie aber, dass Veränderungen in im erst dann sichtbar werden, wenn der Inhalt des Steuerelements neu gezeichnet wird. Das können Sie gegebenenfalls explizit durch ctr.Invalidate() erreichen.
Diese Aufzählung sollte nur einen ersten Überblick schaffen. Genauere Informationen zu den hier vorgestellten Schlüsselwörtern folgen in den weiteren Abschnitten.
16.4.2 Bitmaps in Formularen darstellen Sie können bereits in der Entwicklungsumgebung eine Grafikdatei in ein Formular oder ein Steuerelement laden. Dazu klicken Sie zuerst das Formular bzw. Steuerelement an, dann im Eigenschaftsfenster die Eigenschaft Image oder BackgroundImage. Es erscheint nun ein Dialog zur Dateiauswahl, wobei unter anderem folgende Bitmap-Dateitypen unterstützt werden: BMP, GIF, JPEG, PNG und TIFF.
VERWEIS
16.4 Bitmaps, Icons und Metafiles
897
Sie können auch Metafile-Dateien mit den Endungen EMF oder WMF laden. Dabei handelt es sich aber nicht um Bitmaps. Mehr Informationen zum Umgang mit Metafile-Dateien finden Sie in Abschnitt 16.4.6.
Die folgenden Punkte stellen die wichtigsten Varianten zur Darstellung einer Bitmap dar: •
Formulare und die BackgroundImage-Eigenschaft: Die Bitmap gilt als Hintergrundgrafik. Über diesen Hintergrund können mit den im vorigen Abschnitt vorgestellten DrawXxx- und FillXxx-Methoden weitere Grafikausgaben durchgeführt werden. Diese Methoden ändern die Bitmap aber nicht, d.h., die Ausgaben müssen weiterhin bei jedem Paint-Ereignis wiederholt werden. Die Hintergrund-Bitmap füllt unabhängig von ihrer Größe die gesamte Formularfläche und wird dazu an ihren Enden einfach periodisch fortgesetzt. Ich habe keine Eigenschaft gefunden, mit der dieses Verhalten geändert werden kann.
•
Steuerelemente (z.B. PictureBox) und die Eigenschaften Image und BackgroundImage: Bei einigen Steuerelementen gibt es gleich zwei Eigenschaften, um eine BitmapGrafik einzubinden. Normalerweise wird Image verwendet. In diesem Fall kann die Position der Bitmap innerhalb des Steuerelements durch SizeMode bestimmt werden. Zur Auswahl stehen Normal (im linken, oberen Eck), CenterImage (zentriert), StretchImage (die Bitmap wird gedehnt oder verzerrt, so dass Sie dieselbe Größe wie das Steuerelement hat), AutoSize (das Steuerelement wird an die Größe der Bitmap angepasst). Wenn Sie die Bitmap dagegen über BackgroundImage einbinden, füllt sie das gesamte Steuerelement aus (und wird dabei gegebenenfalls wie in Formularen periodisch fortgesetzt). Falls Sie sowohl BackgroundImage als auch Image verwenden, überdeckt die Image-Bitmap den BackgroundImage-Hintergrund. Mit den Draw- und Fill-Methoden können Sie über die beiden Bitmaps weitere Grafikausgaben durchführen.
TIPP
In allen Fällen werden die Bitmap-Dateien in die zum Formular gehörende Ressourcendatei form.resx eingefügt. Außerdem wird die Bitmap ein integraler Bestandteil der *.exeDatei, so dass auch deren Platzbedarf bei großen Bitmaps stark ansteigt. Wenn Sie im Eigenschaftsfenster eine Bitmap wieder entfernen möchten, klicken Sie die Eigenschaft mit der rechten Maustaste an und führen RESET aus.
Bitmap dynamisch laden Wenn Sie eine übermäßige Codegröße vermeiden möchten, können Sie die [Background]Image-Eigenschaften auch erst im laufenden Programm initialisieren und die BitmapDateien beim Programmstart selbst laden. Allerdings funktioniert Ihr Programm dann nur, wenn die Bitmap-Dateien auch gefunden werden. (Sichern Sie den Code durch Try/Catch
898
16 Grafikprogrammierung (GDI+)
ab!) Für das folgende Beispiel muss sich river.bmp im selben Verzeichnis wie die *.exeDatei befinden. ' Beispiel grafik\bitmap_intro Private Sub Form1_Load(...) Handles MyBase.Load Try PictureBox1.Image = New Bitmap("river.bmp") Catch MsgBox("Fehler") End Try End Sub
Beispielprogramm In Abbildung 16.27 sehen Sie ein Formular, dessen Hintergrund (BackgroundImage) durch eine relativ kleine Bitmap (160*160 Pixel) gebildet wurde. Der Hintergrund wiederholt sich periodisch. Die Datei wurde bereits in der Entwicklungsumgebung geladen. Im Vordergrund befindet sich ein PictureBox-Steuerelement, dessen Inhalt durch die Datei river.bmp bestimmt wird. Diese Datei wird in der Prozedur Form1_Load geladen (siehe obigen Code). Die Größe des PictureBox-Steuerelements ergibt sich aus der Größe der Bitmap (SizeMode=AutoSize).
Abbildung 16.27: Bitmap-Beispielprogramm
16.4 Bitmaps, Icons und Metafiles
899
Das PictureBox-Steuerelement wird innerhalb des Fensters zentriert dargestellt. Die Bewegung des Bilds über der Hintergrundgrafik sieht recht eindrucksvoll aus, wenn Sie die Fenstergröße verändern. Die erforderliche Ereignisprozedur hat mit der Bitmap-Verwaltung zwar nichts zu tun, ist hier aber der Vollständigkeit halber auch angegeben: ' Beispiel grafik\bitmap_intro Private Sub Form1_Resize(...) Handles MyBase.Resize Dim x, y As Integer x = CInt((Me.ClientSize.Width - PictureBox1.Size.Width) / 2) y = CInt((Me.ClientSize.Height - PictureBox1.Size.Height) / 2) If x < 0 Then x = 0 If y < 0 Then y = 0 PictureBox1.Location = New Point(x, y) End Sub
Weitergabe von Bitmaps (Bitmap-Container) Wenn Sie zusammen mit einem Programm eine Reihe von Bitmaps mitliefern möchten, bestehen drei Möglichkeiten: •
Sie liefern die Bitmaps als Dateien mit, die sich im selben Verzeichnis wie das Programm befinden (oder in einem Unterverzeichnis). Diese Vorgehensweise ist einfach durchzuführen; die Codegröße bleibt, die Wartung ist unkompliziert (etwa der Austausch der Bitmaps durch neue Dateien). Der Hauptnachteil besteht darin, dass der Konfigurationsaufwand für den Setup-Vorgang etwas größer ist und dass generell das Risiko gross ist, dass die Bitmap-Dateien bei der Weitergabe des Programms verschwinden und das Programm in der Folge nicht mehr funktioniert.
•
Das ImageList-Steuerelement ermöglicht es, während des Programmerstellung beliebig viele Bitmaps zu laden. Diese Bitmaps können dann im laufenden Programm mit ImageList1.Images(n) angesprochen werden. Leider ist das Steuerelement nicht als universeller Bitmap-Container geeignet: Beim Laden werden nämlich alle Bitmaps auf eine einheitliche Größe (Eigenschaft ImageSize) und eine einheitliche Farbtiefe (ColorDepth) skaliert. Per Default beträgt die Größe nur 16*16 Pixel und die Farbtiefe 8 Bit (also 256 Farben). Angesichts dieser Einschränkungen ist das Steuerelement also nur dann brauchbar, wenn Sie eine Menge Bitmaps mit einheitlichem Format verwalten möchten (beispielsweise die Bitmaps für eine Symbolleiste).
•
Schließlich können Sie auf Ressourcen-Dateien zurückgreifen. Dabei handelt es sich um XML-Dateien, die bei der Kompilierung direkt in das Programm integriert werden. Die Entwicklungsumgebung verwendet derartige Dateien intern, um in Formularen oder Steuerelementen enthaltene Bitmaps zu speichern. Um eine eigene Ressourcen-Datei zu erzeugen, führen Sie dem Projekt im Projektmappen-Explorer ein NEUES ELEMENT hinzu und wählen als Vorlage eine ASSEMBLY-RESSOURCEN-Datei. Um im Programmcode auf die Elemente der Ressourcen-Datei zugreifen zu können, müssen Sie dann auf die Klassen des System.Resources-Namensraums zurückgreifen.
900
16 Grafikprogrammierung (GDI+)
Diese Vorgehensweise würde der .NET-Philosophie am besten entsprechen, hat aber zwei gravierende Nachteile: Erstens bietet die Entwicklungsumgebung keine Möglichkeit, (Bitmap-)Dateien in Ressourcen-Datei einzufügen, und zweitens ist der Programmieraufwand deutlich höher. Tipps, wie Sie Ressourcen-Dateien per VB-Code selbst erstellen und später wieder auslesen, finden Sie im VB.NET-Buch von Andrew Troelsen (siehe Quellenverzeichnis am Ende des Buchs).
16.4.3 Bitmaps manipulieren Im vorigen Abschnitt stand die Nutzung fertiger Bitmap-Dateien im Vordergrund. Dieser Abschnitt beschäftigt sich damit, wie Sie Bitmaps selbst per Programmcode erzeugen, bearbeiten, speichern und später wieder laden können.
Neue Bitmap erzeugen Ein neues Bitmap-Objekt erstellen Sie am einfachsten mit dem New-Operator, wobei Sie die gewünschte Größe in Pixel angeben. Per Default werden zur Speicherung jedes Pixels 32 Bit vorgesehen (je acht Bit für die drei Farbtöne Rot, Grün und Blau sowie acht Bit für den Alphakanal, der die Transparenz steuert). Durch einen optionalen dritten Parameter können Sie ein anderes Pixelformat angeben (ein Element der Aufzählung Imaging.PixelFormat). Dim bm As New Bitmap(x, y)
HINWEIS
Beachten Sie, dass die neue Bitmap per Default durchsichtig ist. Genau genommen enthalten alle Pixel die Farbe R=0, G=0, B=0 und A=0 (wobei A der Wert des Alphakanals ist). Erst wenn Sie an einer beliebigen Stelle einen Pixel zeichnen, wird die Bitmap hier undurchsichtig. (Details zur Transparenz von Bitmaps folgen am Ende dieses Abschnitts.) Die Imaging.PixelFormat-Aufzählung kennt zwar zahllose interessante Formate, allerdings treten bei vielen davon in der Praxis Probleme auf. Einige (z.B. Format16bppGrayscale) werden zurzeit noch gar nicht unterstützt und befinden sich laut einem Beitrag des Microsoft-Mitarbeiters John Hornick in microsoft.public.dotnet.framework.drawing irrtümlich in der Aufzählung. Eine klare Referenz der Formate, die nun problemlos eingesetzt werden können, habe ich leider nicht gefunden.
Pixel zeichnen Mit der Methode SetPixel können Sie nun die Farbe jedes einzelnen Pixel der Bitmap verändern. (Bei neuen Bitmaps sind alle Pixel weiß.) Die Pixel werden durch Koordinaten angegeben, wobei der Punkt (0,0) der erste Pixel im linken oberen Eck der Bitmap ist. Die folgende Anweisung zeichnet einen roten Punkt an die Koordinatenposition (10,10). bm.SetPixel(10, 10, Color.FromArgb(255,0,0))
16.4 Bitmaps, Icons und Metafiles
901
Umgekehrt können Sie mit der Methode GetPixel die Farbe eines beliebigen Pixels ermitteln. GetPixel liefert als Ergebnis ein Color-Objekt.
Mit einem Graphics-Objekt komplexere Grafikoperationen durchführen SetPixel ist die einzige Grafikmethode, die für Bitmaps vorgesehen ist. Für komplexere Zeichenvorgänge können Sie aber mit der Methode FromImage ein Graphics-Objekt auf der Basis der Bitmap erzeugen. Sie können dann alle Graphics-Zeichenmethoden verwenden, um in der Bitmap Linien, Kreise, Rechtecke etc. zu zeichnen. Dim gr As Graphics = Graphics.FromImage(bm) gr.Clear(Color.White) 'die gesamte Bitmap weiß anmalen gr.DrawEllipse(Pens.Black, 10, 10, 80, 80) 'einen Kreis zeichnen
Bitmap mit DrawImage in ein Graphics-Objekt kopieren Obwohl Sie in der Bitmap nun bereits verschiedene Zeichenoperationen durchgeführt haben, ist die Bitmap noch immer unsichtbar. Es gibt nun zwei Möglichkeiten, die Bitmap sichtbar zu machen: Sie können die Bitmap in der Paint-Ereignisprozedur eines Formulars oder Steuerelements mit DrawImage ausgeben, oder Sie können die Bitmap der ImageEigenschaft eines Steuerelements zuweisen. (Diese zweite Variante wird etwas weiter unten behandelt.) Die Methode DrawImage zeichnet sich durch unzählige Syntaxvarianten aus (insgesamt 30!). In der einfachsten Form wird die gesamte Bitmap ohne Veränderung einfach an eine bestimmte Koordinatenposition im Graphics-Objekt kopiert. Die folgenden Zeilen verwenden zur Ausgabe das Graphics-Objekt, das durch eine Paint-Ereignisprozedur geliefert wird. Mit DrawImage wird die Bitmap an die Koordinatenposition (10, 20) kopiert. Private Sub Form1_Paint( _ ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint Dim gr As Graphics = e.Graphics gr.DrawImage(bm, 10, 20) End Sub
Eine ganze Palette von Syntaxvarianten ermöglicht es, nur Teile der Bitmap zu kopieren und beim Kopieren die Größe zu verändern (die Bitmap zu skalieren). Mit der folgenden Anweisung wird beispielsweise die gesamte Bitmap in das Rechteck zwischen den Koordinatenpunkten (10,10) und (260,160) kopiert. Wenn die Bitmap nicht zufällig 250*150 Punkte groß ist, wird sie entsprechend gedehnt oder zusammengedrückt. gr.DrawImage(bm, New Rectangle(10, 20, 250, 150))
Bei einigen der DrawImage-Methoden können Sie mit einem ImageAttributes-Objekt angeben, wie die Farben der Bitmap beim Kopieren verändert werden sollen. Durch die folgenden Zeilen wird der Bereich zwischen (10,10) und (90,90) aus der Bitmap gelesen und in ein Rechteck zwischen (110, 10) und (230,80) kopiert. Dabei wird eine Gamma-Korrektur mit
902
16 Grafikprogrammierung (GDI+)
einem Gamma-Faktor von 1,5 durchgeführt. (Das ImageAttributes-Objekt bietet zahllose weitere Möglichkeiten zur Farbmanipulation, die aber leider durchwegs schlecht dokumentiert sind.) Dim ga As New Imaging.ImageAttributes() ga.SetGamma(1.5F) gr.DrawImage(bm, New Rectangle(110, 10, 120, 70), _ 10, 10, 80, 80, GraphicsUnit.Pixel, ga) DrawImage führt automatisch eine Interpolation der Bilddaten durch, um auch bei einer
Größenveränderung eine möglichst hohe Qualität zu erzielen. Wenn Sie das nicht möchten, können Sie die Methode DrawImageUnscaled einsetzen, deren Vorteil darin besteht, dass Sie etwas schneller ausgeführt wird.
Bildinformationen von einer Bitmap in eine andere kopieren DrawImage kann auch dazu verwendet werden, wenn Sie einen Teil einer Bitmap bm1 (oder auch die ganze Bitmap bm1) in eine zweite Bitmap bm2 kopieren möchten. Allerdings müssen Sie nun zuerst ein Graphics-Objekt zur zweiten Bitmap erzeugen. Dim gr As Graphics = Graphics.FromImage(bm2) gr.DrawImage(bm1, ...)
'bm2: Ziel-Bitmap 'bm1: Quell-Bitmap
Bitmap in einem Steuerelement anzeigen (Image-Eigenschaft) Wenn Sie die selbst erzeugte Bitmap vollständig und ohne weitere Manipulationen in einem Steuerelement anzeigen möchten, weisen Sie das Bitmap-Objekt einfach der ImageEigenschaft des Steuerelements zu. PictureBox1.Image = bm
Jedes Mal, wenn der Inhalt des Steuerelements neu zu zeichen ist, liest VB.NET automatisch die Bildinformationen aus der Bitmap bm. (Es ist für VB.NET kein Unterschied, ob es eine aus einer Datei geladene Bitmap in einem Steuerelement anzeigt, wie dies in 16.4.2 demonstriert worden ist, oder ob die Bildinformationen aus einer von Ihnen selbst erzeugten Bitmap stammen!) Beachten Sie aber bitte, dass das im Steuerelement angezeigte Bild nicht automatisch aktualisiert wird, wenn Sie die Bitmap durch weitere SetPixel- oder Graphics-Methoden verändern! Diese Bitmap-Veränderungen, die Sie vielleicht als Reaktion auf eine Menüauswahl durchführen, werden erst dann sichtbar, wenn VB.NET die Notwendigkeit erkennt, den Fensterinhalt neu zu zeichnen (also etwa, wenn das Fenster vorübergehend verdeckt war). Wenn Sie möchten, dass das Steuerelement nach einer Veränderung der zugrunde liegenden Bitmap sofort aktualisiert werden soll, müssen Sie die Refresh-Methode ausführen. Beachten Sie aber, dass Refresh bei großen Bitmaps viel Rechenzeit beansprucht. Führen Sie die Methode also nicht nach jedem geänderten Pixel aus, sondern nur, wenn es wirklich notwendig ist!
16.4 Bitmaps, Icons und Metafiles
VERWEIS
bm.SetPixel(x, y, col) PictureBox1.Refresh()
903
'Bitmap verändern 'PictureBox1 aktualisieren
In VB6 gab es die Möglichkeit, bei einem Formular oder einem Bildfeld die so genannte AutoRedraw-Eigenschaft zu aktivieren. Damit wurden alle Grafikausgaben in einer internen Bitmap gespeichert. Daher war keine Paint-Prozedur zur Erneuerung des Bildinhalts erforderlich. VB.NET bietet diese Möglichkeit zwar nicht unmittelbar, aber mit ein paar Zeilen eigenen Code können Sie einen vergleichbaren Effekt erzielen. Details zur Vorgehensweise finden Sie in Abschnitt 16.5.6.
Bitmaps freigeben Bitmap-Objekte können sehr speicheraufwendig sein. Verwenden Sie unbedingt Dispose, um
den Speicher von nicht mehr benötigten Objekten wieder freizugeben! bm.Dispose()
Beispielprogramm Das Beispielprogramm (siehe Abbildung 16.28) enthält fast keinen neuen Code mehr – der gesamte Code wurde auf den vorigen Seiten bereits Zeile für Zeile erklärt. Der Zweck des Beispielprogramms ist es, das Zusammenspiel der verschiedenen Kommandos nochmals zu demonstrieren. Die Programmausführung beginnt in Form1_Load: Dort wird ein neues Bitmap-Objekt erzeugt. Da die Variable auf Klassenebene deklariert wurde, bleibt dieses Objekt so lange gültig, bis das Programm durch das Schließen von Fenster 1 beendet wird. In der Bitmap werden zuerst alle 100*100 Pixel mit Zufallsfarben gefüllt (SetPixel). Anschließend wird ein Graphics-Objekt erzeugt, um in der Bitmap noch einen Kreis zu zeichnen. Bis zu diesem Zeitpunkt ist die Bitmap unsichtbar. Die folgenden Zeilen machen Fenster 2 sichtbar. In diesem Fenster befindet sich ein PictureBox-Steuerelement. Um die Test-Bitmap dort sichtbar zu machen, wird sie einfach der Image-Eigenschaft dieses Steuerelements zugewiesen. (Fenster 2 enthält keinen Programmcode.) Zurück zu Fenster 1: Um die Test-Bitmap auch darin sichtbar zu machen, wird ein anderer Weg gewählt: Jedes Mal, wenn Teile des Fensterinhalts neu zu zeichnen sind, wird die Bitmap in der Paint-Ereignisprozedur auf drei verschiedene Weisen in das Fensterinnere kopiert. Die Prozedur demonstriert so verschiedene Varianten der DrawImage-Methode. Um den Unterschied zwischen Fenster 1 und Fenster 2 nochmals klarzustellen: In Fenster 1 erfolgt die Bitmap-Ausgabe bei Bedarf. Die Ausgabe muss jedes Mal wiederholt werden, wenn Teile des Fensters unsichtbar werden. Die Paint-Ereignisprozedur greift dabei auf das Bitmap-Objekt zurück, das sich (unsichtbar) im Arbeitsspeicher befindet.
904
16 Grafikprogrammierung (GDI+)
In Fenster 2 erfolgt die Bitmap-Ausgabe dagegen automatisch (also ohne Paint-Ereignisprozedur), weil die Test-Bitmap direkt der Image-Eigenschaft des PictureBox-Steuerelements zugewiesen wurde. Diese Vorgehensweise ist also einfacher, bietet aber weniger Gestaltungsmöglichkeiten.
Abbildung 16.28: Ein einfaches Beispielprogramm zur Bitmap-Programmierung
' Beispiel grafik\bitmap-manipulation Public Class Form1 Inherits System.Windows.Forms.Form [... #Region " Vom Windows Form Designer generierter Code" ...] Dim bm As Bitmap Private Sub Form1_Load(...) Handles MyBase.Load Dim x, y As Integer Dim rand As New System.Random() bm = New Bitmap(100, 100) ' Bitmap mit Zufallspixel füllen For x = 0 To 99 For y = 0 To 99 bm.SetPixel(x, y, _ Color.FromArgb(rand.Next(256), rand.Next(256), _ rand.Next(256))) Next Next ' Kreis zeichnen Dim gr As Graphics = Graphics.FromImage(bm) gr.DrawEllipse(Pens.Black, 10, 10, 80, 80) ' Bitmap in PictureBox des zweiten Fensters anzeigen Dim form_2 As New Form2() form_2.PictureBox1.Image = bm form_2.Show() End Sub
16.4 Bitmaps, Icons und Metafiles
905
' Bitmap mit DrawImage im ersten Fenster anzeigen Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics gr.DrawImage(bm, 0, 0) gr.DrawImage(bm, New Rectangle(10, 110, 250, 150)) Dim ga As New Imaging.ImageAttributes() ga.SetGamma(1.5F) gr.DrawImage(bm, New Rectangle(110, 10, 120, 70), _ 10, 10, 80, 80, GraphicsUnit.Pixel, ga) End Sub End Class
Bitmaps laden und speichern Um eine Bitmap aus einer Datei zu laden, verwenden Sie einfach den New-Konstruktur und geben als Parameter den Dateinamen an. Wie bereits erwähnt, kann .NET die meisten Bitmap-Formate lesen. bm = New Bitmap("dateiname.bmp")
Um eine Bitmap zu speichern, verwenden Sie die Methode Save, wobei Sie als zweiten Parameter das gewünschte Grafikformat angeben. Die zur Auswahl stehenden Formate sind in der Aufzählung Imaging.ImageFormat enthalten, wobei beim Speichern aber nur die Formate BMP, GIF, PNG, TIF und JPEG unterstützt werden. Mit dem folgenden Kommando wird eine GIF-Datei erzeugt. bm.Save("dateiname.gif", Imaging.ImageFormat.Gif)
Je nach Format wird das in der Bitmap enthaltene Bild zum Speichern verändert: Beispielsweise wird beim GIF-Format die Anzahl der Farben auf 256 reduziert, wozu eine eigene Palette erzeugt wird. Bei den Formaten JPEG und PNG wird das Bild komprimiert. Mit der oben angegebenen Save-Methode haben Sie allerdings keinen Einfluss darauf, wie diese Veränderungen durchgeführt werden, welche Qualitätsstufe bei der Komprimierung verwendet wird etc. Wenn Sie den Speichervorgang exakter steuern möchten, müssen Sie auf eine andere Syntaxvariante von Save zurückgreifen. Dabei geben Sie statt des Bildformattyps zwei Parameter an, ein Imaging.ImageCodecInfo-Objekt und ein Imaging.EncoderParameters-Objekt. Die Definition dieser Objekte ist allerdings nicht ganz einfach, wie das folgende Beispiel beweist. ' ImageCodecInfo für jpeg ermitteln Dim ici As Imaging.ImageCodecInfo For Each ici In ici.GetImageEncoders() If ici.MimeType = "image/jpeg" Then Exit For Next
906
16 Grafikprogrammierung (GDI+)
' Parameter zur Einstellung der Kompressionsqualität Dim encps As New Imaging.EncoderParameters(1) 'nur ein Parameter ' diesen Parameter einstellen encps.Param(0) = _ New Imaging.EncoderParameter(Imaging.Encoder.Quality, 75) ' Bitmap speichern bm.Save("test.jpg", ici, encps)
Einige Erläuterungen zum obigen Code: Save benötigt zum einen ein ImageCodecInfo-Objekt, das angibt, welches Bildformat beim Speichern verwendet werden soll. Zur Ermittlung eines derartigen Objekts ist eine Schleife erforderlich, weil es keine einfache Möglichkeit gibt, den Encoder für das JPEG-Format zu ermitteln. Stattdessen liefert GetImageEncoders ein Feld mit allen verfügbaren Encodern, aus dem Sie sich dann das richtige Element heraussuchen müssen. Zur Identifizierung verwenden Sie am besten die Eigenschaft MimeType, die den Formatnamen in der Form image/name enthält.
VERWEIS
TIPP
Darüber hinaus benötigt Save ein EncoderParameters-Objekt, das einen oder mehrere Parameter enthält, um die Details des Speichervorgangs zu steuern. Im obigen Beispiel wird nur ein derartiger Parameter verwendet, der den Qualitätslevel beim Speichern angibt (als Wert zwischen 0 und 100, wobei 100 maximale Qualität bedeutet). Andere mögliche Parameter steuern die Farbtiefe (Imaging.Encoder.ColorDepth), die Kompression (-.Compression) etc. Leider ist die .NET-Dokumentation zu diesen Parametern ziemlich dürftig, so dass zum Teil einiges Experimentieren (oder das Studium der C#-Beispielprogramme) erforderlich ist. Sowohl beim Laden als auch beim Speichern darf statt eines Dateinamens auch ein Stream-Objekt angegeben werden (siehe Abschnitt 10.6).
Zum Laden und Speichern von Bitmaps in den unterschiedlichsten Formaten mit und ohne eigener Farbpalette sind in der News-Gruppe microsoft.public.dotnet.framework.drawing bereits endlose Diskussionen geführt worden, nach denen Sie mit groups.google.com suchen können. (Dabei können Sie als zusätzlichen Suchbegriff James DeBroeck angeben – dieser Microsoft-Mitarbeiter kennt diese schlecht dokumentierten Aspekte von GDI+ offensichtlich am besten.) Weitere Informationen über das Laden, Konvertieren und Speichern von Bitmaps in unterschiedlichen Formaten finden Sie hier: ms-help://MS.VSCC/MS.MSDNVS.1031/gdicpp/ucodecs_0dnp.htm (Überblick) http://www.codeproject.com/dotnet/ImageExGDI.asp (animierte GIFs anzeigen) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q318343 (GIFs speichern) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q315780 (GIFs mit eigener Palette) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q319591 (GIFs mit eigener Palette)
16.4 Bitmaps, Icons und Metafiles
907
VERWEIS
Bitmaps direkt im Speicher bearbeiten Wenn Sie Bildverarbeitungsalgorithmen auf sehr große Bitmaps anwenden möchten, werden Sie bald feststellen, dass GetPixel und SetPixel ziemlich langsam sind. Für solche Fälle bietet GDI+ die Möglichkeit, die Bitmap direkt im Speicher zu adressieren. In VB.NET ist das zwar nicht möglich, die Bitmap kann aber in ein Byte-Feld kopiert werden. Auf dessen Elemente können Sie immer noch effizienter als auf die Pixel einer Bitmap zugreifen. Ein Beispiel für diese Art der Programmierung finden Sie in Abschnitt 16.5.11.
16.4.4 Durchsichtige Bitmaps Möglicherweise ist es Ihnen noch gar nicht aufgefallen – neue Bitmaps sind durchsichtig! Wenn Sie also eine Bitmap mit bm = New(100,100) erzeugen, dann enthält die Bitmap 100*100 durchsichtige Pixel. Wenn Sie diese Bitmap nun beispielsweise mit DrawImage in einem Graphics-Objekt ausgeben, bleiben die bisherigen Inhalte unter diesen Pixeln unverändert. Erst wenn Sie einzelne Pixel mit einer deckenden Farbe füllen (mit SetPixel oder mit Graphics-Methoden), werden diese Pixel undurchsichtig.
HINWEIS
Damit das alles funktioniert, werden bei jeder Standard-Bitmap (32 Bits pro Pixel) nicht nur in 24 Bits pro Pixel die Farbinformationen gespeichert, sondern in weiteren 8 Bits der Alphawert. Per Default – also bei neuen Bitmaps – ist dieser Wert 0, d.h., das Pixel ist durchsichtig. Die hier angegebenen Informationen zum Alphakanal gelten natürlich nicht nur für Bitmaps, sondern für alle Grafikoperationen. Wenn Sie beispielsweise FillEllipse ausführen und als Farbe FromArgb(128, Color.Red) angeben, also ein Rot mit einem Alphawert von 128, dann wird die Ellipse in einem durchscheinenden Rot gezeichnet.
Wenn Sie eine Bitmap bereits mit nichttransparenten Mustern gefüllt haben (oder wenn Sie die Bitmap aus einer Datei geladen haben), können Sie mit der Methode MakeTransparent interessante Effekte erzielen. Mit dieser Methode geben Sie eine Farbe an, die unsichtbar werden soll. Die Bitmap wird nun Pixel für Pixel nach dieser Farbe durchsucht. Bei allen Pixeln, die exakt dieser Farbe entsprechen, wird der Alphakanal auf 0 gestellt, d.h., das Pixel wird unsichtbar. Beachten Sie, dass MakeTransparent eine statische Methode ist. Sie betrifft das Aussehen der Bitmap zum aktuellen Zeitpunkt, nicht aber weitere Zeichenoperationen. Sie können also mit MakeTransparent(Color.White) alle weißen Punkte unsichtbar machen. Wenn Sie aber anschließend durch SetPixel oder mit Graphics-Methoden neue weiße Pixel zeichnen, dann sind diese Punkte tatsächlich weiß. Wollen Sie auch diese Pixel unsichtbar machen, müssen Sie MakeTransparent nochmals ausführen.
908
16 Grafikprogrammierung (GDI+)
Beispielprogramm Das folgende Beispielprogramm versucht, den Effekt transparenter Bitmaps zu visualisieren. In Abbildung 16.30 sehen Sie in der oberen Zeile drei Bitmaps: eine undurchsichtige Bitmap mit einem Schwarzweiß-Muster, eine durchsichtige Bitmap (alles, was bisher weiß war, ist nun transparent), und schließlich eine durchscheinende Bitmap: Bei dieser Bitmap wurde der Alphawert aller Pixel von 0 (durchsichtig, links) bis 255 (deckend, rechts) variiert. Diese drei Bitmaps wurden in der zweiten Zeile mit DrawImage über ein einfaches grafisches Muster kopiert. Die verschiedenen Transparenzeffekte sollten offensichtlich sein.
Abbildung 16.29: Beispielprogramm zur Demonstration durchsichtiger Bitmaps
Der Programmcode besteht aus zwei wesentlichen Teilen: In Form1_Load werden die drei Bitmaps erzeugt. Sie werden in den Klassenvariablen bm1 bis bm3 gespeichert und bleiben bis zum Ende des Programms verfügbar. bm1 besteht aus einem weißen Hintergrund (Methode Clear) und einigen zufälligen schwarzen Rechtecken (Methode FillRectangle). bm2 ist einfach eine Kopie von bm1. Allerdings wird nun die Farbe Weiß mit MakeTransparent unsichtbar gemacht.
Auch bm3 wird als Kopie von bm1 erzeugt. Anschließend wird in einer Schleife über alle Pixel der Alphawert von 0 (links) bis 255 (rechts) eingestellt. Diese Schleife ist nicht besonders elegant und bei großen Bitmaps natürlich ineffizient. Ich habe aber leider keine Möglichkeit gefunden, den Alphawert auf eine andere Weise zu verändern, ohne dabei auch die Farbinformationen zu verändern. (Ich hätte an die FillRectangle-Methode unter Anwendung eines LinearGradientBrush gedacht. Allerdings müssten vorher auf irgendeine Weise
16.4 Bitmaps, Icons und Metafiles
909
die Farbinformationen der Bitmap ausmaskiert werden, so dass nur der Alphakanal verändert wird. Falls Sie wissen, wie das geht, senden Sie mir bitte eine E-Mail!) ' Beispiel grafik\bitmap-transparent Public Class Form1 Inherits System.Windows.Forms.Form [... #Region " Vom Windows Form Designer generierter Code" ...] ' drei Bitmaps bm1, bm2, bm3 erzeugen Dim bm1, bm2, bm3 As Bitmap Private Sub Form1_Load(...) Handles MyBase.Load bm1 = New Bitmap(100, 100) Dim gr As Graphics = Graphics.FromImage(bm1) Dim i As Integer Dim rand As New Random() ' bm1 erzeugen gr.Clear(Color.White) For i = 1 To 20 gr.FillRectangle(Brushes.Black, _ rand.Next(100), rand.Next(100), _ rand.Next(50), rand.Next(50)) Next ' bm2 erzeugen, Weiß transparent machen bm2 = New Bitmap(bm1) 'bm2 ist eine Kopie von bm1 bm2.MakeTransparent(Color.White) ' bm3 erzeugen, Alphakanal direkt verändern bm3 = New Bitmap(bm1) 'bm3 ist eine Kopie von bm1 Dim x, y As Integer Dim oldcol, newcol As Color For x = 0 To 99 For y = 0 To 99 oldcol = bm3.GetPixel(x, y) newcol = Color.FromArgb(x / 99 * 255, oldcol) bm3.SetPixel(x, y, newcol) Next Next End Sub
Der zweite Teil des Codes ist einfach zu verstehen. In den Paint-Ereignisprozeduren zu allen sechs PictureBox-Steuerelementen wird die jeweilige Bitmap angezeigt. Bei PictureBox4 bis -6 werden vorher in der Prozedur DrawCircles noch ein paar konzentrische Kreise als Hintergrund gezeichnet. ' Bitmap 1 anzeigen (normal) Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint e.Graphics.DrawImage(bm1, 0, 0) End Sub
910
16 Grafikprogrammierung (GDI+)
' Bitmap 2 anzeigen (durchsichtig) Private Sub PictureBox2_Paint(...) Handles PictureBox2.Paint e.Graphics.DrawImage(bm2, 0, 0) End Sub ' Bitmap 3 anzeigen (durchsichtig) Private Sub PictureBox3_Paint(...) Handles PictureBox3.Paint e.Graphics.DrawImage(bm3, 0, 0) End Sub ' Muster zeichnen, darüber Bitmap 1 kopieren Private Sub PictureBox4_Paint(...) Handles PictureBox4.Paint DrawCircles(e.Graphics) e.Graphics.DrawImage(bm1, 0, 0) End Sub ' Muster zeichnen, darüber Bitmap 2 kopieren Private Sub PictureBox5_Paint(...) Handles PictureBox5.Paint DrawCircles(e.Graphics) e.Graphics.DrawImage(bm2, 0, 0) End Sub ' Muster zeichnen, darüber Bitmap 3 kopieren Private Sub PictureBox6_Paint(...) Handles PictureBox6.Paint DrawCircles(e.Graphics) e.Graphics.DrawImage(bm3, 0, 0) End Sub ' einige konzentrische Kreise um den Punkt (50,50) zeichnen Sub DrawCircles(ByVal gr As Graphics) Dim radius As Integer Dim p As New Pen(Color.Red, 3) gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias For radius = 10 To 150 Step 10 gr.DrawEllipse(p, 50 - radius, 50 - radius, _ radius * 2, radius * 2) Next End Sub End Class
16.4.5 Icons Icons sind kleine Symbole, die unter Windows primär im Explorer und in der Taskleiste sichtbar werden und bei der Identifizierung von Programmen und Dateien helfen. Auch jedes VB.NET-Formular ist mit einem Icon ausgestattet (Eigenschaft Icon). Wenn Sie Ihr Programm mit einem anderen Icon ausstatten möchten, müssen Sie entweder auf ein vorgefertigtes Icon zurückgreifen oder selbst eines erzeugen. Manchmal helfen die wenigen Icons weiter, die in der Klasse SystemIcons enthalten sind (z.B. SystemIcons.Ques-
16.4 Bitmaps, Icons und Metafiles
911
tion). Fertige Icons finden Sie außerdem im Verzeichnis Programme\Microsoft Visual Studio.NET\Common7\Graphics\icons oder im Internet (z.B. http://www.iconfactory.com). Wenn Sie
TIPP
Icons selbst erzeugen möchten, können Sie einen der zahlreichen im Internet verfügbaren Icon-Editoren einsetzen. Auch die Entwicklungsumgebung enthält einen (gut versteckten) Icon-Editor. Das Programm ist nicht gerade intuitiv zu bedienen, für einfache Zwecke aber ausreichend. Um in diesen Editor zu gelangen, fügen Sie Ihrem Projekt eine Icon-Datei hinzu (entweder indem Sie eine vorhandene Icon-Datei öffnen oder indem Sie eine neue SYMBOLDATEI hinzufügen). Sobald der Icon-Editor sichtbar wird, stehen Ihnen über das Kontextmenü bzw. über das Hauptmenü BILD eine Reihe von Manipulationskommandos zur Auswahl. Damit können Sie unter anderem ein Fenster mit einer Farbpalette öffnen. Die Bedienung des Editors erfolgt überwiegend durch die Symbolleiste GRAFIK.
Abbildung 16.30: Der Icon-Editor der Entwicklungsumgebung
Icons im Programmcode verwalten Wenn Sie Icons per Programmcode laden, anzeigen, manipulieren etc. möchten, bietet sich dazu die Icon-Klasse an. Das folgende Minibeispielprogramm lädt eine Icon-Datei (die sich im selben Verzeichnis wie die *.exe-Datei befinden muss) und zeigt diese mit der Methode DrawIcon im Formular an. (Achtung: DrawIcon berücksichtigt das Clipping-Gebiet nicht. Das ist ein bestätigter Fehler im .NET-Framework, der vermutlich in zukünftigen Versionen behoben wird.)
912
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\icon-test Private Sub Form1_Paint(...) Handles MyBase.Paint Dim ic As New Icon("handshak.ico") Dim gr As Graphics = e.Graphics gr.DrawIcon(ic, 10, 10) ic.Dispose() End Sub
Abbildung 16.31: Icon-Beispielprogramm
Die Icon-Klasse bietet relativ wenige Möglichkeiten zur Berarbeitung von Icons (und insbesondere keine einzige Möglichkeit zur Veränderung). Sie können ein Icon aber mit der Methode ToBitmap in ein Bitmap-Objekt umwandeln und dieses dann mit den im vorigen Abschnitt beschriebenen Methoden bearbeiten. Erstaunlicherweise ist das allerdings eine Einbahnstraße: Weder gibt es eine Methode zur Rückverwandlung einer Bitmap in ein Icon, noch funktioniert die Save-Methode der Bitmap-Klasse, wenn als Bildformat Imaging.ImageFormat.Icon angegeben wird. (GDI+ hat zurzeit noch keinen Encoder zum Speichern von Icons – siehe auch den Knowledge-Base-Artikel Q316563.)
16.4.6 Metafile-Dateien Metafile-Dateien sind Dateien, in denen eine Abfolge von Grafikkommandos gespeichert sind. (In der Grafiksprache werden derartige Dateiformate als vektororientiert bezeichnet – im Gegensatz zu pixel- oder rasterorientierten Bitmap-Formaten.) Metafile-Dateien können von vielen Programmen wie andere Grafikdateien geladen und angezeigt werden. Der große Unterschied zu Bitmap-Dateien besteht darin, dass nicht das Endergebnis (also die Farben von einer Menge Pixeln) gespeichert werden, sondern die Kommandos zur Erzeugung der Grafik. Das hat vor allem zwei Vorteile: •
Metafile-Dateien sind meistens viel kleiner (platzsparender) als Bitmap-Dateien.
•
Metafile-Dateien können beliebig skaliert werden. Anders als bei Bitmaps wird dadurch die Qualität nicht beeinträchtigt.
Metafile-Formate Es gibt nicht ein Metafile-Format, sondern gleich vier Formate bzw. Varianten: •
Windows Metafile Format (WMF, seit Windows 3.1), Dateikennung *.wmf.
•
Enhanced Metafile Format (EMF, seit Windows 95), Dateikennung *.emf.
16.4 Bitmaps, Icons und Metafiles
913
•
EMF+ Only: Eine neue, GDI+-konforme Metafile-Variante, die aber inkompatibel zum herkömmlichen GDI ist. (Herkömmliche Windows-Programme können EMF+OnlyDateien daher nicht lesen. Nur .NET-konforme Programme auf der Basis von GDI+ können damit umgehen.) Als Dateikennung wird weiterhin zumeist *.emf verwendet.
•
EMF+Dual: Um das EMF-Kompatiblitätsproblem zwischen GDI und GDI+ zu lösen, gibt es schließlich ein Dual-Format, das die Grafikinformationen gemäß dem alten und und dem neuen EMF-Format enthält. Der Hauptnachteil besteht darin, dass derartige Dateien ca. doppelt so groß sind als eigentlich notwendig wäre.
Laut Dokumentation können Sie mit der Klasse System.Drawing.Imaging.Metafile MetafileDateien lesen bzw. erzeugen. GDI+ kann Metafile-Dateien aller Formate anzeigen, selbst aber nur Dateien in den Formaten EMF, EMF+Only und EMF+Dual erzeugen (nicht WMF).
Metafile-Dateien laden und anzeigen Die folgenden Zeilen zeigen, wie Sie eine Metafile-Datei laden (indem Sie beim New-Konstruktur den Dateinamen angeben) und mit DrawImage beginnend mit der Koordinatenposition (0,0) anzeigen. Zusammen mit Metafile-Dateien ist die Defaultgröße der Grafik gespeichert. Sie können aber ohne weiteres eine andere Größe durch zwei weitere DrawImage-Parameter angeben. Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim mf As New Imaging.Metafile("..\rolodex.wmf") gr.DrawImage(mf, 0, 0) mf.Dispose() End Sub
HINWEIS
Die obige Prozedur ist natürlich ziemlich ineffizient: Jedes Mal, wenn das Fenster neu gezeichnet werden soll, muss die Metafile-Datei neu geöffnet werden. Wenn Sie also eine bestimmte Metafile-Datei immer wieder anzeigen möchten, sollten Sie die Metafile-Variable außerhalb von Form1_Paint deklarieren und nur einmal laden. Syntaktisch ist es möglich, ein Metafile-Objekt der BackgroundImage-Eigenschaft eines Formulars bzw. der Image-Eigenschaft eines Steuerelements zuzuweisen. (Die Metafile-Klasse ist wie die Bitmap-Klasse von der Image-Klasse abgeleitet.) In der Praxis führt dies allerdings nicht zum erwünschten Ergebnis. Im ersten Fall wird ein Teil des Fensters durchsichtig, im zweiten Fall wird die Metafile-Grafik überhaupt nicht angezeigt.
Metafile-Dateien erzeugen Leider sind alle Versuche gescheitert, mit GDI+ eine neue Metafile-Datei zu erstellen. Fehler traten bereits beim Versuch auf, ein neues Metafile-Objekt zu erzeugen, ohne dabei den Namen einer bereits vorhandenen Datei anzugeben. Die Suche nach einem funktionie-
914
16 Grafikprogrammierung (GDI+)
rendem VB.NET- oder C#-Beispiel blieb sowohl in der MSDN-Dokumentation als auch im Internet erfolglos. (Ich will aber nicht ganz ausschließen, dass es vielleicht doch irgendwie geht.)
16.4.7 Syntaxzusammenfassung System.Drawing.Bitmap-Klasse bm = New Bitmap("file.bmp")
lädt eine Bitmap aus einer Datei.
bm = New Bitmap(stream)
lädt eine Bitmap aus einem Stream.
bm = New Bitmap(n, m)
erzeugt eine neue, n*m Pixel große Bitmap (32 Bit pro Pixel).
bm = New Bitmap(n, m, format)
wie oben, aber unter Anwendung des gewünschten Pixelformats (Imaging.PixelFormat-Aufzählung).
bm2 = New Bitmap(bm1)
erzeugt eine Kopie von bm1.
bm.SetPixel(x, y, color)
zeichnet ein Pixel in der Bitmap.
bm.GetPixel(x, y)
ermittelt die Farbe eines Pixels der Bitmap und liefert das Ergebnis als Color-Objekt.
bm.SetTransparent(color)
macht alle Pixel, die exakt die Farbe color aufweisen, unsichtbar.
bm.Save("filename", format)
speichert die Bitmap in einer Datei unter Verwendung des gewünschten Formats (Aufzählung Imaging.ImageFormat).
bm.Save("filename", ici, encps)
speichert die Bitmap in einer Datei unter Verwendung des angegebenen Formats (ImageCodecInfo-Objekt) und unter Berücksichtigung zusätzlicher Parameter (EncoderParameters-Objekt).
bm.Save(stream, ...)
wie oben, aber speichert die Bitmap unter Zuhilfenahme des angegebenen Stream-Objekts.
ctr.Image = bm
zeigt die Bitmap in einem Steuerelement an.
frm.BackGroundImage = bm
zeigt die Bitmap als sich periodisch wiederholenden Hintergrund eines Fensters an.
gr = Graphics.FromImage(bm)
liefert ein Graphics-Objekt, um in einer Bitmap zu zeichnen.
gr.DrawImage(bm, ...)
zeichnet eine Bitmap in ein Graphics-Objekt.
System.Drawing.Icon-Klasse ico = New Icon("file.ico")
lädt eine Icon-Datei und liefert ein Icon-Objekt.
gr.DrawIcon(ico, ...)
zeichnet ein Icon in ein Graphics-Objekt.
bm = ico.ToBitmap()
liefert ein Bitmap-Objekt, das dem Icon entspricht.
16.5 Interna und spezielle Programmiertechniken
915
System.Drawing.Imaging.Metafile-Klasse mf = New Imaging.Metafile( "file.emf")
lädt eine Metafile-Datei und liefert ein Icon-Objekt.
gr.DrawImage(mf, ...)
stellt ein Metafile-Objekt in einem Graphics-Objekt dar.
16.5
Interna und spezielle Programmiertechniken
Dieser Abschnitt demonstriert anhand zumeist kurzer Beispiele diverse fortgeschrittene Techniken, die mit GDI+ möglich sind.
16.5.1 Zeichen- und Textqualität Die Graphics-Klasse stellt einige Eigenschaften zur Verfügung, die die Qualität von Grafikund Textausgaben beeinflussen. Dabei gilt generell eine ganz einfache Regel: Je höher die Qualität, desto langsamer werden die Grafikausgaben! Vorweg eine kurze Erklärung zur prinzipiellen Funktionsweise der hier beschriebenen Einstellungen: Wenn eine Linie oder eine Kurve oder ein Buchstabe gezeichnet wird, liegt ein einzelner Bildschirmpunkt nicht immer exakt innerhalb oder außerhalb des zu zeichnenden Objekts. Vielmehr gibt es auch Pixel, die sich genau auf der Kante befinden. In solchen Grenzfällen ergibt sich die neue Farbe des betroffenen Pixels aus einer Mischung von Hintergrundfarbe und Zeichenfarbe (wobei die Mischung umso stärker zur Zeichenfarbe tendiert, je mehr das Pixel dem Zeichenobjekt zuzurechnen ist). Diese Technik wird bei Text anti-aliasing und bei anderen Grafikoperationen smoothing genannt. Bei der Textausgabe kommt noch hinzu, dass der Abstand zwischen Buchstaben in Bruchteilen von Pixeln variiert werden kann, um eine noch bessere Darstellungsqualität (vor allem bei sehr kleiner Schrift) zu erzielen. GDI+ sieht unzählige Eigenschaften vor, um diese Mechanismen zu steuern. Leider sind die Eigenschaften nur recht knapp dokumentiert, so dass man zum Teil nur raten kann, was sie wirklich bewirken. In der Praxis reicht eine Variation der Eigenschaften CompositingQuality, SmoothingMode und TextRenderingHint meist vollkommen aus, um einen optimalen Kompromiss zwischen Geschwindigkeit und Darstellungsqualität zu erzielen. •
CompositionMode gibt an, ob bei Grafikausgaben der Hintergrund berücksichtigt werden soll oder nicht. Die möglichen Einstellungen lauten SourceOver und CopyOver. SourceOver (die Defaulteinstellung) bewirkt eine Berücksichtigung des Hintergrunds. Alle im Weiteren beschriebenen Maßnahmen zur Erzielung einer hohen Ausgabequalität funktionieren nur in Kombination mit dieser Einstellung. SourceCopy bewirkt, dass Ausgaben einfach in die vorhandene Grafik kopiert werden. Wenn es bereits eine Hintergrundgrafik gibt, sieht das ziemlich hässlich aus.
916
16 Grafikprogrammierung (GDI+)
•
CompositionQuality gibt an, wie die Grafikausgabe auf dem schon vorhandenen Hintergrund erfolgen soll. Die höchste Qualität wird erzielt, wenn die Farbwerte der Pixel aus der Umgebung mitberücksichtigt werden. Mögliche Einstellungen sind unter anderem Default, HighSpeed und HighQuality (Elemente der CompositionQuality-Aufzählung). Die Einstellung betrifft sowohl Grafik- als auch Textausgaben. Wieweit die Einstellung in der Praxis bemerkbar wird, hängt stark vom Hintergrund ab.
•
InterpolationMode beeinflusst ebenfalls, wie die Grafikausgabe und die bereits vorhandene Hintergrundfarbe miteinander kombiniert werden. Mögliche Einstellungen sind Bilinear (per Default), Low und High.
Wie InterpolationMode und CompositionQuality zusammenhängen, ist nicht dokumentiert. Praktische Tests haben ergeben, dass im Regelfall kein Unterschied zwischen der qualitativ besten und der qualitativ schlechtesten Einstellung sichtbar ist. Mit anderen Worten: eine Veränderung von InterpolationMode lohnt sich meistens nicht. •
PixelOffsetMode gibt an, ob Koordinatenangaben in Bildschirmpixeln generell um einen halben Pixel versetzt interpretiert werden sollen. Mögliche Einstellungen sind unter anderem Default, Half, None. Eine Veränderung der Darstellungsqualität im unten angegebenen Testprogramm durch die Variation dieses Parameters konnte allerdings nicht festgestellt werden.
•
SmoothingMode gibt an, ob schräge Linien bzw. Kurven bei der Ausgabe geglättet werden. Mögliche Einstellungen sind unter anderem Default, HighSpeed und HighQuality.
Die Einstellung betrifft nur Grafikausgaben. Sie ist besonders deutlich bei leicht schrägen Linien bzw. flachen Kurven bemerkbar. •
TextRenderingHint gibt an, wie Text geglättet wird (und betrifft daher nur Text-, nicht
aber Grafikausgaben). Mögliche Einstellungen sind unter anderem: SystemDefault (die Defaulteinstellung des Betriebssystems) AntiAliasGridFit (optimale Darstellung auf gewöhnlichen Monitoren) ClearTypeGridFit (optimale Darstellung auf LC-Displays) SingleBitPerPixelGridFit (Komprimiss zwischen Geschwindigkeit und Qualität) SingleBitPerPixel (am schnellsten)
HINWEIS
ClearTypeGridFit steht zurzeit nur unter Windows XP zur Verfügung. Auf älteren Windows-Versionen wird als Ersatz SingleBitPerPixelGridFit verwendet.
Wenn Sie schon mit anderen Grafikbibliotheken gearbeitet haben, ist Ihnen vielleicht aufgefallen, dass in der schier endlosen Liste von Eigenschaften eine fehlt: Es gibt keine Eigenschaft zur Einstellung des Zeichenmodus (z.B. XOR-Modus). Tatsächlich bietet GDI+ diese Möglichkeit nicht! Gerade den XOR-Modus werden alle Programmierer vermissen: Damit kann man rasch eine Linie, ein Rechteck oder eine Figur zeichnen und diesen Zeichenvorgang später einfach durch seine Wiederholung wieder rückgängig machen. In manchen Fällen bieten die Methoden der System.Windows.Forms.ControlPaint-Klasse einen Ausweg, wie das Beispielprogramm in Abschnitt 16.5.5 beweist.
16.5 Interna und spezielle Programmiertechniken
917
Beispielprogramm Das in Abbildung 16.43 dargestellte Beispielprogramm demonstriert den Unterschied zwischen höchstmöglicher Zeichengeschwindigkeit und optimaler Darstellungsqualität. Für die Ausgabe in den beiden PictureBox-Steuerelementen wurde dieselbe Prozedur verwendet. Vor deren Aufruf werden die Eigenschaften CompositingQuality, SmoothingMode und TextRenderingHint unterschiedlich eingestellt.
Abbildung 16.32: Dieselbe Grafikausgabe links in optimaler Geschwindigkeit, rechts in optimaler Qualität (bei der Darstellung auf einem gewöhnlichen Röhrenmonitor)
' Beispiel grafik\quality Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics gr.CompositingQuality = _ Drawing.Drawing2D.CompositingQuality.HighSpeed gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.HighSpeed gr.TextRenderingHint = _ Drawing.Text.TextRenderingHint.SingleBitPerPixel PaintSomething(gr) End Sub Private Sub PictureBox2_Paint(...) Handles PictureBox2.Paint Dim gr As Graphics = e.Graphics gr.CompositingQuality = _ Drawing.Drawing2D.CompositingQuality.HighQuality gr.SmoothingMode = _ Drawing.Drawing2D.SmoothingMode.HighQuality gr.TextRenderingHint = _ Drawing.Text.TextRenderingHint.AntiAliasGridFit PaintSomething(gr) End Sub
918
16 Grafikprogrammierung (GDI+)
Private Sub PaintSomething(ByVal gr As Graphics) ... End Sub
16.5.2 Grafikobjekte zusammensetzen (GraphicsPath) Die Klasse Drawing2D.GraphicsPath ermöglicht es, mehrere grafische Primitive (Linien, Rechtecke, Ellipsen etc.) zu einem neuen Objekt (zu einer Figur) zusammenzufügen. Dazu erzeugen Sie ein neues Objekt dieser Klasse, starten mit der Methode StartFigure ein neues Grafikobjekt und führen dann AddLine-, AddRectangle-, AddEllipse-Methoden etc. aus. Besonders interessant ist die Methode AddString, mit der Sie den Linienzug eines Texts hinzufügen können. Optional können Sie die Figur mit CloseFigure abschließen und anschließend eine weitere Figur beginnen. Zur Darstellung der Figur verwenden Sie die Methode DrawPath oder FillPath der Graphics-Klasse. Im ersten Fall wird der Umriss der Figur mit einem Zeichenstift nachgezeichnet, im zweiten Fall wird die Figur mit einem Füllmuster ausgefüllt. Per Default ergibt sich die Füllfäche aus der Differenzfläche der einzelnen Grafikprimitive (soweit es sich dabei um abgeschlossene Formen handelt). Wenn Sie für das GraphicsPath-Objekt den FillMode mit Winding angeben, wird dagegen die gemeinsame Fläche aller Primitive gefüllt. GraphicsPath-Objekte eignen sich insbesondere dann, wenn gleichartige Grafikprimitive immer wieder gezeichnet werden sollen. (Damit die Figur nicht immer an derselben Position gezeichnet wird, muss vorher eine Koordinatentransformation für das Graphics-Objekt durchgeführt werden.)
Beispielprogramm Das folgende Beispielprogramm zeigt eine einfache Anwendung eines GraphicsPath. Das GraphicsPath-Objekt wird in Form1_Load initialisiert. Es besteht aus einer Ellipse und dem Text abc. Beachten Sie, dass AddString kein Font-, sondern ein FontFamily-Objekt als Parameter erwartet. Die resultierende Figur wird in Form1_Paint zweimal ausgegeben: einmal als Linienzug mit DrawPath und einmal (nach einer einfachen Koordinatentransformation) als Fläche mit FillPath.
Abbildung 16.33: Ein einfaches GraphicsPath-Beispiel
16.5 Interna und spezielle Programmiertechniken
919
' Beispiel grafik\draw-path Private Sub Form1_Load(...) Handles MyBase.Load Dim ff As New FontFamily("arial") gp.StartFigure() gp.AddEllipse(20, 20, 150, 80) gp.AddString("abc", ff, FontStyle.Bold, 50, _ New PointF(30, 30), StringFormat.GenericDefault) End Sub Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics gr.DrawPath(Pens.Black, gp) gr.TranslateTransform(0, 130) gr.RotateTransform(-30) e.Graphics.FillPath(Brushes.Green, gp) End Sub
16.5.3 Umgang mit Regionen und Clipping Mit der Klasse Region können Sie Regionen aus Rechtecken und GraphicsPath-Objekten zusammensetzen. Auf den ersten Blick scheint die Region-Klasse also eine ähnliche Funktion wie die im vorigen Abschnitt behandelte GraphicsPath-Klasse zu haben – aber dieser Eindruck täuscht. Auch wenn Sie mit beiden Klassen ein Grafikobjekt zusammensetzen können, ist die Anwendung dieses Objekts vollkommen unterschiedlich: •
Ein GraphicsPath-Objekt können Sie mit Draw- oder FillPath zeichnen.
•
Im Gegensatz dazu können Sie ein Region-Objekt nicht unmittelbar darstellen. Sie können das Objekt aber zur Definition des Clipping-Bereichs verwenden (siehe die übernächste Überschrift). Außerdem können Sie das Objekt für einfache Tests verwenden, mit denen Sie prüfen, ob ein bestimmter Koordinatenpunkt innerhalb oder außerhalb der Region liegt.
Ein weiterer fundamentaler Unterschied zwischen den beiden Objekten besteht darin, dass beim GraphicsPath-Objekt die Umrisse der Figur durch Linien dargestellt werden. Das Region-Objekt setzt sich dagegen immer aus geschlossenen Flächen zusammen.
Region zusammensetzen Wenn Sie ein Region-Objekt mit New ohne Parameter erzeugen, dann ist darin die gesamte Zeichenfläche erfasst (also der Koordinatenbereich von minus bis plus unendlich). Wenn Sie mit einer leeren Region beginnen möchten, müssen Sie die Methode MakeEmpty ausführen. (Zu MakeEmpty gibt es übrigens das Gegenstück MakeInfinite.) Anschließend können Sie die Region mit den Methoden Complement, Exclude, Intersect, Union und Xor vergrößern bzw. verkleinern. Alle fünf Methoden nehmen als Parameter wahlweise ein Rectangle[F]-, ein GraphicsPath- oder ein anderes Region-Objekt entgegen.
920
16 Grafikprogrammierung (GDI+)
Die folgende Tabelle beschreibt die Wirkung der fünf Methoden. Beachten Sie, dass bei Regionen mit Löchern bzw. Inseln bisweilen unerwartete Ergebnisse entstehen. Methoden der System.Drawing.Region-Klasse reg1.Complement(reg2)
verkleinert reg2 um reg1 (entspricht einer Subtraktion, also reg1 = reg2 - reg1). reg1.Complement(reg2) ist ident mit reg2.Exclude(reg1).
verkleinert reg1 um reg2 (entspricht einer Subtraktion, also reg1 = reg1 - reg2). Wenn reg2 reg1 völlig umschließt, ist die resultierende Region leer.
reg1.Intersect(reg2)
bildet die gemeinsame Schnittmenge beider Regionen.
reg1.Union(reg2)
vereint die beiden Regionen (entspricht einer Addition, also reg1 = reg1 + reg2).
reg1.Xor(reg2)
invertiert reg1 im Bereich von reg2.
HINWEIS
reg1.Exclude(reg2)
Das Region-Objekt implementiert die IDisposable-Schnittstelle. Das bedeutet, dass Sie Region-Objekte mit Dispose löschen sollten, wenn Sie sie nicht mehr benötigen.
Clipping Der Begriff Clipping bedeutet, dass Grafikausgaben innerhalb eines vorgegebenen Bereichs abgeschnitten werden. Wenn Sie also eine Linie von (0,0) bis (100,100) zeichnen, die Clipping-Region aber ein Rechteck von (20,20) bis (60,60) ist, dann wird von der Linie nur der Teil innerhalb des Clipping-Rechtecks gezeichnet – in diesem Fall also von (20,20) bis (60,60). GDI+ verwendet Clipping auch intern: Wenn Sie beispielsweise Grafikausgaben innerhalb eines Steuerelements durchführen, werden diese wie selbstverständlich am Rand des Steuerelements abgeschnitten (so dass es also unmöglich ist, über die Grenzen eines Steuerelements hinauszuzeichnen. Außerdem wird das Clipping-Gebiet bei Paint-Ereignissen automatisch auf den Bereich des Steuerelements verkleinert, der tatsächlich neu gezeichnet werden muss. Das bewirkt in erster Linie eine (bisweilen merkliche) Effizienzsteigerung und reduziert ein Flimmern am Bildschirm. Während viele Grafikbibliotheken nur rechteckige Clipping-Bereiche zulassen, akzeptiert GDI+ jede beliebige Region als Clipping-Gebiet. Das aktuelle Clipping-Gebiet des GraphicsObjekts können Sie über die Eigenschaft Clip lesen und verändern. Clip liefert bzw. erwartet ein Region-Objekt.
16.5 Interna und spezielle Programmiertechniken
921
' gr ist ein Graphics-Objekt Dim myreg As New Region(New Rectangle(20, 20, 40, 40)) gr.Clip = myreg gr.DrawLine(Pens.Black, 0, 0, 100, 100) myreg.Dispose()
Zur Einstellung des Clipping-Bereichs können Sie alternativ auch die Methode SetClip einsetzen. Der Vorteil gegenüber Clip besteht darin, dass Sie mit SetClip das bereits vorhandene Clipping-Gebiet verändern können (z.B. erweitern oder einschränken). Außerdem akzeptieren die Varianten von SetClip auch andere Objekttypen zur Angabe des ClippingBereichs (z.B. ein Rectangle-Objekt), was in einfachen Fällen den Umweg über ein RegionObjekt erspart.
Sichtbarkeitstest Mit reg.IsVisible können Sie testen, ob sich der als Parameter angegebene Koordinatenpunkt innerhalb der Region befindet bzw. ob das angegebene Rechteck sich zumindest teilweise mit der Region überlappt.
Beispielprogramm Das Beispielprogramm bildet eine Region aus dem Text GDI+ und einem Rechteck. Diese Region wird mit einem Quadrat XOr-verknüpft. Die so entstandene Region wird als Clipping-Gebiet für die Grafikausgabe innerhalb des Fensters verwendet. Innerhalb dieses Gebiets wird zuerst alles schwarz gefüllt, anschließend werden konzentrische weiße Kreise darüber gezeichnet. Das Ergebnis ist in Abbildung 16.34 zu sehen. Ein zusätzlicher Effekt ist aus der Abbildung nicht ersichtlich, sondern kann nur ausprobiert werden: Wenn Sie die Maus im Fenster bewegen, ändert sich deren Aussehen, je nachdem, ob sich der Mauszeiger gerade innerhalb oder außerhalb der Grafik befindet.
Abbildung 16.34: Demonstrationsprogramm für Regionen
922
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\region-test Dim reg As New Region() ' Region initialisieren Private Sub Form1_Load(...) Handles MyBase.Load Dim gp As New Drawing2D.GraphicsPath() gp.AddString("GDI+", New FontFamily("arial"), FontStyle.Bold, _ 150, New Point(0, 0), StringFormat.GenericDefault) reg.MakeEmpty() reg.Union(gp) reg.Union(New Rectangle(0, 150, 500, 20)) reg.Xor(New Rectangle(50, 50, 150, 150)) End Sub ' Muster zeichnen Private Sub Form1_Paint(...) Handles MyBase.Paint Dim i As Integer Dim gr As Graphics = e.Graphics Dim pen As New Pen(Color.Black, 3) gr.SetClip(reg, Drawing.Drawing2D.CombineMode.Replace) gr.Clear(Color.Black) For i = 5 To 300 Step 5 gr.DrawEllipse(Pens.White, 200 - i, 100 - i, 2 * i, 2 * i) Next End Sub ' Mauscursor je nach Position ändern Private Sub Form1_MouseMove(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles MyBase.MouseMove If reg.IsVisible(e.X, e.Y) Then Me.Cursor = Cursors.Cross Else Me.Cursor = Cursors.Default End If End Sub
16.5.4 Interna zu den Paint- und Resize-Ereignissen Im Mittelpunkt dieses Abschnitts stehen zwei Ereignisse, mit denen Sie bei jeder Grafikanwendung zu tun haben: Paint und Resize. Der Abschnitt gibt Hintergrundinformationen darüber, wann diese Ereignisse auftreten, welche Parameter dabei übergeben werden, wie auf die Ereignisse reagiert werden soll und welche Möglichkeiten es gibt, den Ablauf der Ereignisse zu beeinflussen bzw. an ihnen gleichsam vorbeizuprogrammieren.
HINWEIS
16.5 Interna und spezielle Programmiertechniken
923
Im Folgenden ist immer von Ereignissen, Methoden und Eigenschaften eines (PictureBox-)Steuerelements die Rede. Ich gehe davon aus, dass Sie zur Grafikausgabe ein derartiges Steuerelement verwenden und dass sich dessen Größe mit der Fenstergröße automatisch ändert (durch eine entsprechende Anchor-Einstellung). Dieselben Ereignisse gibt es aber selbstverständlich auch für Formulare (wenn Sie direkt in ein Formular zeichnen) bzw. für die meisten anderen Steuerelemente, in denen Grafikausgaben möglich sind. Das Formular können Sie im Code mit Me ansprechen. Me.eigenschaft liest also eine Eigenschaft des Form-Objekts des aktuellen Formulars.
Paint-Ereignis
HINWEIS
Zu einem Paint-Ereignis kommt es immer dann, wenn Teile eines Steuerelements erstmals oder neuerlich sichtbar werden. Das ist beim Start des Programms, bei einer Vergrößerung des Steuerelements sowie dann der Fall, wenn das Steuerelement vorübergehend verdeckt war. Der neu zu zeichnende Bereich wird automatisch mit der Hintergrundfarbe des Steuerelements gefüllt. Sie brauchen sich also um dieses Detail in der Paint-Prozedur nicht zu kümmern.
Clipping
VORSICHT
Wenn nur Teile des Steuerelements neu zu zeichnen sind, dann wird an die Paint-Prozedur automatisch ein Graphics-Objekt übergeben, dessen Clipping-Bereich (Eigenschaft Clip) entsprechend verkleinert ist. Das steigert die Effizienz beim Neuzeichnen des Steuerelementinhalts und vermindert ein Flimmern bei Größenänderungen. Das Defaultverhalten von Windows.Forms bei einer Größenänderung hat einen Nachteil: Wenn der Grafikinhalt eines Steuerelemens von seiner Größe abhängig ist, dann muss bei einer Vergrößerung immer der gesamte Inhalt neu gezeichnet werden, nicht nur die neu dazugekommenen Teile. Ebenso muss der gesamte Inhalt auch bei einer Verkleinerung komplett neu gezeichnet werden. (In diesem Fall kommt es per Default nur zu einem Resize-, aber zu keinem Paint-Ereignis.) Aus diesem Grund kommt es zu einer falschen Bildschirmdarstellung, wie in Abbildung 16.35 links zu sehen ist. Abhilfe schafft in diesen Fällen die ResizeRedrawEigenschaft, die etwas weiter unten beschrieben wird. Im Regelfall brauchen Sie sich um die Clipping-Informationen nicht zu kümmern. In Ihrer Paint-Ereignisprozedur zeichnen Sie einfach alles neu. Dank Clipping erfolgt die Ausgabe aber nur da, wo es notwendig ist.
924
16 Grafikprogrammierung (GDI+)
Wenn es Sie aber interessiert, welche Bereiche des Fensters neu zu zeichnen sind, dann können Sie die diversen Clipping-Eigenschaften des Parameters e in der Paint-Ereignisprozedur sowie des Graphics-Objekts auswerten. Clipping-Eigenschaften innerhalb einer Paint-Ereignisprozedur e.ClipRectangle
enthält ein Rechteck (Rectangle-Objekt), das das ClippingGebiet für die Ausgabe umrandet.
e.Graphics.Clip
enthält das exakte Clipping-Gebiet als Region-Objekt. (Wie bereits im vorigen Abschnitt beschrieben wurde, kann ein Region-Objekt aus mehreren Teilgebieten, z.B. aus mehreren Rechtecken zusammengesetzt sein.)
e.Graphics.ClipBounds
enthält ein Rechteck (RectangleF-Objekt), das die äußere Begrenzung des größtmöglichen Clipping-Gebiets für das Steuerelement angibt. Eigenen Tests zufolge hat dieses Rechteck immer dieselbe Größe: X=-4194304, Y=-4194304, Width=8388608, Height=8388608.
Grundsätzlich sollte es möglich sein, das mit reg = e.Graphics.Clip ermittelte Region-Objekt weiter auszuwerten. So sollte reg.GetBounds(gr) die Umrandung des Clipping-Gebiets umgerechnet in die Koordinaten des Graphics-Objekt gr liefern. (Tatsächlich liefert GetBounds aber dasselbe wertlose Ergebnis wie ClipBounds.) Ebenso sollte es mit reg.GetRegionScans möglich sein, rechteckige Teilbereiche der Region zu ermitteln. Allerdings sind sämtliche Versuche gescheitert, mit dieser Methode das Clipping-Gebiet genauer zu analysieren. Die Methode liefert abermals nur ein einziges Rechteck mit beinahe unendlicher Größe.
HINWEIS
Fazit: Wenn Sie wissen möchten, welche Teile Ihres Steuerelements neu gezeichnet werden müssen, bietet einzig e.ClipRectangle eine zuverlässige Information. Allerdings ist das mit dieser Eigenschaft ermittelte Rechteck oft viel größer als der Bereich, der tatsächlich neu gezeichnet werden muss. Unabhängig von Inhalt der Clipping-Eigenschaften werden Grafikausgaben immer am Rand des Steuerelements abgeschnitten. Das ist gewissermaßen eine zusätzliche Clipping-Ebene, auf die Sie keinen Zugriff haben.
Neuzeichnen manuell auslösen Das Paint-Ereignis wird automatisch ausgelöst, wenn Teile des Steuerelements sichtbar werden. Sie können das Ereignis aber auch manuell auslösen, indem Sie die Invalidate-Methode für das Steuerelement bzw. für das Formular ausführen. PictureBox1.Invalidate()
Wenn Sie möchten, dass nur ein bestimmter Bereich des Steuerelements neu gezeichnet werden soll, können Sie als Parameter ein Rectangle- oder Region-Objekt angeben.
16.5 Interna und spezielle Programmiertechniken
925
PictureBox1.Invalidate(New Rectangle(50, 50, 50, 20))
Falls in dem Steuerelement andere Steuerelemente eingebettet sind, können Sie mit einem zweiten optionalen Parameter angeben, ob das Paint-Ereignis auch für diese Steuerelemente ausgelöst werden soll.
Ohne Paint-Ereignis zeichnen Wenn Sie außerhalb einer Paint-Prozedur in ein Steuerelement zeichnen möchten, benötigen Sie dessen Graphics-Objekt. Dazu führen Sie die folgende Anweisung aus: gr = Graphics.FromHwnd(ctr.Handle)
HINWES
Die Eigenschaft Handle liefert eine Windows-interne Identifikationsnummer, die den Zeichenbereich eines Steuerelements angibt. FromHwnd bildet daraus ein Graphics-Objekt. Denken Sie daran, dass Sie dieses Objekt nach Gebrauch durch Dispose wieder freigeben müssen! Beachten Sie, dass die Methode FromHwnd aus Sicherheitsgründen nur funktioniert, wenn das Programm von einer lokalen Festplatte (nicht von einem Netzwerklaufwerk) ausgeführt wird! Andernfalls kommt es zu einem SecurityException-Fehler. Generell ist es vorzuziehen, den gesamten Code zum Neuzeichnen des Fensters in der Paint-Prozedur unterzubringen und bei Bedarf ein Neuzeichnen durch die Invalidate-Methode auszulösen.
Die folgende Ereignisprozedur zeichnet schwarze Punkte im Formular, wenn Sie darin die Maus mit gedrückter linker Maustaste bewegen. (Die Punkte verschwinden aber mit dem nächsten Paint-Ereignis wieder! Wenn Sie die Punkte bleibend speichern wollten, müssten Sie entweder deren Koordinaten in einem Feld speichern, das auch in der Paint-Ereignisprozedur zugänglich ist, oder zum Zeichnen ein Bitmap-Objekt verwenden.) Private Sub Form1_MouseMove(...) Handles MyBase.MouseMove If e.Button = MouseButtons.Left Then Dim gr As Graphics = Graphics.FromHwnd(Me.Handle) gr.FillEllipse(Brushes.Black, e.X - 5, e.Y - 5, 10, 10) gr.Dispose() End If End Sub
Resize-Ereignis Das Resize-Ereignis tritt bei einer Änderung der Steuerelementgröße vor dem Paint-Ereignis auf. Der an die Ereignisprozedur übergebene Parameter e enthält keine Resize-spezifischen Daten. Die neue Innengröße des Steuerelements (also die Größe des Zeichenbereichs) können Sie mit der Eigenschaft ClientSize ermitteln. (Die Eigenschaft liefert ein SizeObjekt, dessen Eigenschaften Width und Height die Innengröße in Pixeln angeben.)
VERWEIS
926
16 Grafikprogrammierung (GDI+)
Neben dem Resize-Ereignis gibt es noch zwei andere Ereignisse, die bei der Änderung der Größe eines Steuerelements oder Fensters auftreten: Layout und SizeChanged (siehe auch Abschnitt 15.1.2): Das Layout-Ereignis tritt vor Resize auf und sollte dazu verwendet werden, Steuerelemente gegebenenfalls neu zu platzieren (soweit dies nicht aufgrund von Anchoroder Dock-Eigenschaften automatisch erfolgt). SizeChanged tritt nach Resize auf. Aus der Dokumentation geht nicht hervor, wodurch es sich von Resize unterscheidet.
ResizeRedraw-Eigenschaft Die Eigenschaft ResizeRedraw bestimmt, ob ein Formular bei jeder Größenänderung vollständig neu gezeichnet soll. Per Default ist das nicht der Fall: bei einer Verkleinerung wird gar nichts neu gezeichnet, bei einer Vergrößerung dank Clipping im Regelfall nur der Teil, der neu dazugekommen ist. ResizeRedraw=True bewirkt, dass immer alles neu gezeichnet wird. Die Einstellung ist dann sinnvoll, wenn sich das Aussehen der dargestellten Grafik mit jeder Änderung der Fenstergröße vollständig ändert. ResizeRedraw ist als Protected-Eigenschaft für die Klasse Control definiert. Das bedeu-
HINWEIS
tet, dass Sie diese Eigenschaft nur innerhalb einer Klasse verändern können, die direkt von einem Steuerelement abgeleitet ist. Im Formularcode ist das nur für das Formular der Fall. (Der Code beginnt ja mit den zwei Zeilen Public Class Form1 und Inherits System.Windows.Forms.Form!) Aus diesem Grund können Sie Form1.ResizeRedraw problemlos verändern, während beispielsweise PictureBox1.ResizeRedraw unzugänglich ist. Das folgende Beispiel verwendet zur Grafikausgabe einfach ein Formular. Wenn Sie für die Grafikausgabe hingegen ein anderes Steuerelement verwenden möchten (z.B. PictureBox), dann haben Sie zwei Alternativen: • Sie verzichten auf die Anwendung der ResizeRedraw-Eigenschaft und führen stattdessen in der Resize-Ereignisprozedur des Steuerelements die Methode Invalidate aus. • Oder Sie definieren ein neues Steuerelement, das von PictureBox abgeleitet ist, das aber eine zusätzliche Eigenschaft kennt, die ResizeRedraw von außen zugänglich macht. Die Vorgehensweise ist in Abschnitt 14.12.3 beschrieben. (Dort wird statt ResizeRedraw die Methode SetStyle verwendet, mit der das ResizeRedraw-Attribut ebenfalls eingestellt werden kann.) ResizeRedraw kann nicht im Eigenschaftsfenster eingestellt werden. Sie können die Anweisung Me.ResizeRedraw = True aber in Form_Load oder in einer anderen Initialisierungsprozedur des Formulars angeben.
16.5 Interna und spezielle Programmiertechniken
927
Dieselbe Wirkung wie durch ResizeRedraw=True erzielen Sie, wenn Sie in der Resize-Ereignisprozedur eine Invalidate-Anweisung für das betroffene Steuerelement angeben. Diese Vorgehensweise ist aber weniger elegant und kann dazu führen, dass das Paint-Ereignis für manche Teile des Steuerelements mehrfach ausgelöst wird. Das folgende Beispielprogramm öffnet zwei gleichartige Fenster, die sich nur durch die ResizeRedraw-Einstellung unterscheiden. Wenn Sie die Fenstergröße ändern, merken Sie sogleich die Wirkung der ResizeRedraw-Eigenschaft.
Abbildung 16.35: Die beiden Fenster demonstrieren die Wirkung der ResizeRedraw-Eigenschaft
Der Programmcode besteht aus zwei Teilen: Das Programm wird in Main in Module1 gestartet. Dort werden die zwei Fenster mit New erzeugt und mit Show angezeigt. Damit das Programm nicht mit dem Ende von Main beendet wird (also sofort nach dem Anzeigen der beiden Fenster), wird Main mit Application.Run abgeschlossen. ' Beispiel grafik\resize-problem ' Datei module1.vb Module Module1 Dim frm1, frm2 As Form1 Sub Main() frm1 = New Form1() frm1.SetResizeRedraw(False) frm1.Text = "ResizeRedraw = False" frm1.Show() frm2 = New Form1() frm2.SetResizeRedraw(True) frm2.Text = "ResizeRedraw = True" frm2.Show() Application.Run() End Sub End Module
Der Formularcode enthält die Methode SetResizeRedraw zur Einstellung der ResizeRedrawEigenschaft. Diese Eigenschaft kann in Main() nicht direkt verändert werden, weil sie als protected gilt und daher außerhalb des Formularcodes unzugänglich ist.
928
16 Grafikprogrammierung (GDI+)
In der Paint-Ereignisprozedur werden einige konzentrische Ellipsen sowie zwei diagonale Linien gezeichnet. x0 und y0 geben den Ellipsenmittelpunkt an. Die unübersichtliche DrawEllipse-Syntax ergibt sich daraus, dass die Eckpunkte der Ellipse angegeben werden müssen. Form1_Close ist dafür schließlich verantwortlich, dass das Programm beendet wird, sobald eines der beiden Fenster geschlossen wird. Application.ExitThread ist das Gegenstück zu Application.Run. ' Beispiel grafik\resize-problem ' Datei form1.vb Friend Sub SetResizeRedraw(ByVal rr As Boolean) Me.ResizeRedraw = rr End Sub Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics Dim i As Single Dim x0, y0 As Single x0 = CSng(Me.ClientSize.Width / 2) y0 = CSng(Me.ClientSize.Height / 2) For i = 0.1 To 1 Step 0.05 gr.DrawEllipse(Pens.Black, _ x0 * (1 - i), y0 * (1 - i), x0 * 2 * i, y0 * 2 * i) Next gr.DrawLine(Pens.Black, 0, 0, x0 * 2, y0 * 2) gr.DrawLine(Pens.Black, 0, y0 * 2, x0 * 2, 0) End Sub Private Sub Form1_Closed(...) Handles MyBase.Closed Application.ExitThread() End Sub
Größe von Steuerelementen oder Formularen per Code verändern Bis jetzt lautet die Frage immer, wie sich Ihr Programm verhält, wenn sich die Größe des Fensters bzw. des Steuerelements ändert (weil der Anwender die Größe manuell verändert hat). Sie können Größe und Position Ihrer Formulare sowie der darin enthaltenen Steuerelemente aber aber auch per Code verändern. Steuerelemente: Bei Steuerelementen ändern Sie zur Positionsänderung die Eigenschaften Left, Top oder Location (erwartet ein Point-Objekt).
Analog können Sie die Größe über die Eigenschaften Right, Bottom, Width, Height und Size (erwartet ein Size-Objekt) verändern. Alle hier angegebenen Eigenschaften beziehen sich auf die Außengröße des Steuerelements. Die Innengröße (der Zeichenbereich) eines Steuerelements ist bisweilen aber deutlich kleiner. Wenn Sie diese Größe verändern möchten, weisen Sie der ClientSize-Eigenschaft
16.5 Interna und spezielle Programmiertechniken
929
ein neues Size-Objekt zu. Die folgende Anweisung bewirkt, dass der Zeichenbereich eines PixtureBox-Steuerelements exakt 100*100 Pixel groß ist. PictureBox1.ClientSize = New Size(100, 100)
Fenster/Formulare: Bei Formularen stehen Ihnen die oben erwähnten Eigenschaften ebenfalls zur Verfügung. Außerdem können Sie mit den Methoden SetDesktopLocation und SetDesktopBounds die Position bzw. Position und Größe einstellen. Dabei gilt das Koordinatensystem des Desktops. Der Punkt (0,0) befindet sich im linken oberen Eck des Bildschirms. Mit SetClientSizeCore können Sie die Innengröße des Fensters verändern.
16.5.5 Rechteck-Auswahl mit der Maus (Rubberbox) Das folgende Beispielprogramm zeigt, wie Sie ein Rechteck mit der Maus auswählen können. Während Sie die Maus innerhalb des PictureBox-Steuerelements gedrückt halten, wird ein gestrichelter Rahmen gezeichnet, der die Ausmaße des Rechtecks angibt (siehe Abbildung 16.36). Dieser Rahmen wird traditionell als rubberbox oder rubberband bezeichnet, obwohl er eigentlich wenig Ähnlichkeit mit einem Gummiband hat. Wenn Sie die Maus loslassen, wird der Rahmen durch ein gefülltes Rechteck in einer zufälligen Farbe ersetzt. Die Auswahl kann durch eine beliebige Taste (z.B. Esc) abgebrochen werden.
Abbildung 16.36: Auswahl eines Rechtecks mit der Maus
Das Beispielprogramm basiert auf der Methode DrawReversibleFrame der Klasse ControlPaint (Namensraum System.Windows.Forms). Mit dieser Methode können Sie ein Rechteck zeichnen. Wenn Sie das Rechteck exakt an derselben Position ein zweites Mal zeichnen, verschwindet es wieder. Wenn Sie diese Methode zum ersten Mal ausprobieren, werden Sie wahrscheinlich wie ich erstaunt feststellen, dass die Koordinatenangaben in Desktop-Koordinaten erfolgen. Und nicht nur das: Mit DrawReversibleFrame zeichnen Sie auch direkt in den Desktop, auch außerhalb der von Ihnen verwalteten Formulare und Steuerelemente! Bei der Anwendung der Methode müssen Sie alle Koordinaten in Desktop-Koordinaten umrechnen. Dabei ist die Methode PointToScreen hilfreich, mit der Sie für alle Steuerelemente eine Umrechnung vom internen Koordinatensystem in das Desktop-Koordinatensystem durchführen können.
HINWEIS
930
16 Grafikprogrammierung (GDI+)
ControlPaint kennt noch zwei verwandte Methoden: DrawReversibleLine zeichnet eine Linie, FillReversibleRectangle zeichnet ein gefülltes Rechteck. Auch bei diesen
Methoden verschwindet das Objekt wieder, wenn der Zeichenvorgang wiederholt wird. Diese Methoden stellen eine Art Ersatz für den in GDI+ nicht existierenden XOR-Zeichenmodus dar.
Beispielprogramm Das Programm beginnt mit der Deklaration einiger Variablen, die in allen Prozeduren zur Verwaltung der Rubberbox zur Verfügung stehen müssen. rubbervisible gibt an, ob das Auswahlrechteck momentan sichtbar ist. rubbercancel gibt an, ob die Auswahl durch eine Tastatureingabe oder durch den Verlust des Eingabefokus abgebrochen wurde. rubberpos gibt den Startpunkt des Rechtecks in Desktop-Koordinaten an. rubberpos_orig enthält ebenfalls die Startposition, diesmal aber im Koordinatensystem der PictureBox. rubbersize enthält schließlich die aktuelle Größe des Rechtecks. ' Beispiel grafik\rubberbox Dim rubbervisible As Boolean = False Dim rubbercancel As Boolean = False Dim rubberpos As Point Dim rubbersize As Size Dim rubberpos_orig As Point
Zum Zeichnen bzw. zum Entfernen des Rechtecks werden die Hilfsprozeduren Draw- bzw. RemoveRubberBox verwendet. Beide Prozeduren verwenden dasselbe Zeichenkommando DrawReversibleFrame und greifen auf die oben beschriebenen Variablen zurück. Der Unterschied besteht darin, wie die Variable rubbervisible ausgewertet und verändert wird, um ein versehentliches mehrfaches Zeichnen zu vermeiden. Private Sub DrawRubberBox() rubbervisible = True ControlPaint.DrawReversibleFrame( _ New Rectangle(rubberpos, rubbersize), _ Color.White, FrameStyle.Dashed) End Sub Private Sub RemoveRubberBox() If rubbervisible = True Then ControlPaint.DrawReversibleFrame( _ New Rectangle(rubberpos, rubbersize), _ Color.White, FrameStyle.Dashed) End If rubbervisible = False End Sub
16.5 Interna und spezielle Programmiertechniken
931
Die eigentliche Verwaltung der Rechteckauswahl erfolgt durch die drei Mausereignisprozeduren MouseDown, MouseMove und MouseUp. Die Auswahl beginnt in der MouseDownProzedur, wo die rubberXxx-Variablen initialisiert werden und ein erstes (noch leeres) Rechteck gezeichnet wird. In der MouseMove-Prozedur wird zuerst getestet, ob überhaupt eine Maustaste gedrückt ist und ob die Auswahl nicht schon abgebrochen wurde. Ist das nicht der Fall, wird das vorhandene Rechteck entfernt und dann die Größe des neuen Rechtecks ermittelt. Das ist deswegen ein wenig umständlich, weil die Größe auf die Größe des Steuerelements limitiert werden muss. (Andernfalls würde DrawReversibleFrame über die Fenstergrenzen hinausreichen.) Die Auswahl wird schließlich in der MouseDown-Prozedur beendet. Das so ausgewählte Rechteck wird nun noch durch ein mit einer Zufallsfarbe gefülltes Rechteck visualisiert. Abermals wirkt die Berechnung der Eckkoordinaten vielleicht ein wenig umständlich. Der Grund besteht diesmal darin, dass DrawRectangle eine positive Rechtecksbreite und -höhe voraussetzt. Wenn das Rechteck ausgehend vom Startpunkt nach links oder nach oben gezogen wurde, müssen daher Start- und Endpunkt vertauscht werden. Private Sub PictureBox1_MouseDown(...) Handles PictureBox1.MouseDown rubbercancel = False rubberpos_orig = New Point(e.X, e.Y) rubberpos = PictureBox1.PointToScreen(rubberpos_orig) rubbersize = New Size(0, 0) ' Startrechteck zeichnen DrawRubberBox() End Sub Private Sub PictureBox1_MouseMove(...) Handles PictureBox1.MouseMove If e.Button <> MouseButtons.None And rubbercancel = False Then Dim x As Integer = e.X Dim y As Integer = e.Y ' bisheriges Rechteck löschen RemoveRubberBox() ' Größe des neuen Rechtecks ermitteln If x < 0 Then x = 0 If x > PictureBox1.ClientSize.Width Then _ x = PictureBox1.ClientSize.Width If y < 0 Then y = 0 If y > PictureBox1.ClientSize.Height Then _ y = PictureBox1.ClientSize.Height ' neues Rechteck zeichnen rubbersize = New _ Size(x - rubberpos_orig.X, y - rubberpos_orig.Y) DrawRubberBox() End If End Sub
932
16 Grafikprogrammierung (GDI+)
Private Sub PictureBox1_MouseUp(...) Handles PictureBox1.MouseUp If rubbercancel Then Exit Sub ' Rechteck löschen RemoveRubberBox() ' als Ergebnis ein gefülltes Rechteck zeichnen Dim x0, y0, x1, y1, tmp As Integer Dim gr As Graphics = Graphics.FromHwnd(PictureBox1.Handle) Dim br As Brush Dim rand As New Random() ' Rechteck von (x0,y0) nach (x1,y1) x0 = rubberpos_orig.X x1 = x0 + rubbersize.Width If x0 > x1 Then tmp = x0 : x0 = x1 : x1 = tmp y0 = rubberpos_orig.Y y1 = y0 + rubbersize.Height If y0 > y1 Then tmp = y0 : y0 = y1 : y1 = tmp br = New SolidBrush(Color.FromArgb( _ rand.Next(256), rand.Next(256), rand.Next(256))) gr.FillRectangle(br, x0, y0, x1 - x0, y1 - y0) br.Dispose() gr.Dispose() End Sub
Die Rechtecksauswahl wird durch das Drücken einer beliebigen Taste oder durch den Fokusverlust abgebrochen (z.B. wenn eine Dialogbox eines anderen Programms erscheint). Private Sub Form1_Leave(...) Handles MyBase.Leave RemoveRubberBox() rubbercancel = True End Sub Private Sub Form1_KeyDown(...) Handles MyBase.KeyDown RemoveRubberBox() rubbercancel = True End Sub
16.5.6 Bitmap-Grafik zwischenspeichern (AutoRedraw) Visual Basic hatte von Version 1 bis 6 eine nette Zusatzfunktion für PictureBox-Steuerelemente: Wenn die Eigenschaft AutoRedraw auf True gesetzt wurde, dann wurde zusammen mit dem Steuerelement eine Bitmap verwaltet, in der alle Grafikausgaben gespeichert wurden. Damit bestand keine Notwendigkeit mehr, Grafikausgaben immer wieder in der Paint-Prozedur zu wiederholen, wenn Teile des Fensters unsichtbar geworden waren. Insbesondere bei Grafiken, deren Berechnung lange dauert, ist diese Vorgehensweise sehr vorteilhaft. (Es ist für den Anwender nicht akzeptabel, jedes Mal sekunden- oder minu-
16.5 Interna und spezielle Programmiertechniken
933
tenlang zu warten, bis ein vorübergehend verdecktes Fenster wieder vollständig sichtbar ist.) In GDI+ gibt es leider weder die AutoRedraw-Eigenschaft für Formulare oder Steuerelemente noch eine äquivalente andere Eigenschaft. Das ist aber nicht so schlimm – mit ein paar Zeilen Extracode zur Verwaltung einer Bitmap mit dem Grafikinhalt lässt sich der Effekt auch selbst erzielen. Dabei gibt es grundsätzlich zwei Vorgehensweisen: •
Variante 1: Die Bitmap kann über die Image-Eigenschaft direkt mit einem Steuerelement verbunden werden. (Dazu erzeugen Sie in Form_Load eine Bitmap in der Größe des Steuerelements und weisen diese der Image-Eigenschaft des Steuerelements zu.) Das hat den Vorteil, dass das Programm nicht mehr auf Paint-Ereignisse reagieren muss. Es reicht vielmehr aus, wenn nach Änderungen an der Bitmap für das Steuerelement die Refresh-Methode ausgeführt wird, damit die Änderungen in der Bitmap auch im Steuerelement sichtbar werden.
•
Variante 2: Die Bitmap kann vom Steuerelement vollkommen getrennt verwaltet werden. Jedes Mal, wenn ein Paint-Ereignis auftritt, wird die Bitmap mit DrawImageUnscaled gezeichnet. Diese Variante ist zwar weniger elegant, entspricht aber wohl eher dem Schema, das bisher in diesem Kapitel präsentiert wurde.
Ein entscheidender Punkt bei beiden Varianten besteht darin, dass die Bitmap mit dem Steuerelementinhalt automatisch an dessen Größe angepasst werden muss. Wenn also das Steuerelement in eine oder in beide Richtungen vergrößert wird, dann muss auch die Bitmap vergrößert werden (natürlich ohne dass ihr Inhalt verloren geht). Da GDI+ dazu keine Möglichkeit bietet, muss eine neue Bitmap erzeugt werden und die alte Bitmap dorthin kopiert werden.
Beispielprogramm zu Variante 1 Das erste Beispiel zeichnet auf Knopfdruck ein aus verschiedenfarbigen Kreisen bestehendes Muster (siehe Abbildung 16.37). Die Grafik wird in einem PictureBox-Steuerelement angezeigt, deren Größe sich automatisch mit dem Fenster ändert. (Die Grafik wird bei einer Größenveränderung allerdings nicht automatisch neu gezeichnet – das dauerte zu lange.) Der Programmcode beginnt damit, dass in Form1_Load eine Bitmap erzeugt wird, die dieselbe Größe wie der Innenbereich der PictureBox1 hat. Diese Bitmap wird durch die Image-Zuweisung als Hintergrundbild für PictureBox verwendet. In PictureBox1_Resize wird getestet, ob die Breite oder Höhe der PictureBox1 mittlerweile größer ist als die bereits vorhandene Bitmap. Wenn das der Fall ist, wird eine neue Bitmap erzeugt. Dabei wird darauf geachtet, dass die Bitmap in keiner Dimension kleiner als bisher wird. (Es könnte ja sein, dass das Fenster zwar breiter, aber gleichzeitig in der Höhe kleiner gemacht wurde.) Die alte Bitmap wird nun mit DrawImageUnscaled in die neue Bitmap kopiert. (Dazu muss ein Graphics-Objekt für die neue Bitmap erzeugt werden, um überhaupt Grafikmethoden ausführen zu können.) Nun kann die neue Bitmap an PictureBox1.Image zugewiesen und die alte Bitmap durch Dispose freigegeben werden.
934
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\autoredraw Dim bm As Bitmap 'für PictureBox1 ' Bitmap erzeugen Private Sub Form1_Load(...) Handles MyBase.Load bm = New Bitmap(PictureBox1.ClientSize.Width, _ PictureBox1.ClientSize.Height) PictureBox1.Image = bm End Sub Private Sub PictureBox1_Resize(...) Handles PictureBox1.Resize ' falls bm noch nicht initialisiert wurde: Exit ' (Resize wird bereits vor Form1_Load das erste ' Mal aufgerufen) If IsNothing(bm) Then Exit Sub ' testen, ob PictureBox jetzt größer ist als die Bitmap If bm.Width < PictureBox1.ClientSize.Width Or _ bm.Height < PictureBox1.ClientSize.Height Then Dim newwidth, newheight As Integer Dim oldbm As Bitmap = bm Dim gr As Graphics ' größere Bitmap erzeugen newwidth = PictureBox1.ClientSize.Width If bm.Width > newwidth Then newwidth = bm.Width newheight = PictureBox1.ClientSize.Height If bm.Height > newheight Then newheight = bm.Height bm = New Bitmap(newwidth, newheight) ' dorthin den Inhalt der alten Bitmap kopieren gr = Graphics.FromImage(bm) gr.DrawImageUnscaled(oldbm, New Point(0, 0)) ' Image-Eigenschaft von PictureBox ändern PictureBox1.Image = bm ' alte Bitmap freigeben oldbm.Dispose() gr.Dispose() End If End Sub
Der Code zur Erstellung der Grafik weist wenig Besonderheiten auf. Es werden einfach ein paar mit unterschiedlichen Farben gefüllte Kreise sowie einige Kreislinien gezeichnet. Entscheidend ist die Methode PictureBox1.Refresh am Ende der Schleife. Sie bewirkt, dass die neue berechnete Grafik tatsächlich sichtbar wird.
16.5 Interna und spezielle Programmiertechniken
935
Abbildung 16.37: AutoRedraw-Beispiel
Wenn die Berechnung der Grafik relativ lange dauert, sollten Sie Refresh während der Berechnung hin und wieder ausführen. Sie erreichen damit, dass die Anwender ein visuelles Feedback bekommen, dass die Berechnung voranschreitet. Führen Sie Refresh aber nicht zu oft aus! Durch Refresh muss die gesamte Bitmap (also der Inhalt von bm) auf den Bildschirm kopiert werden, und das dauert verhältnismäßig lange. Idealerweise führen Sie Refresh alle ein bis zwei Sekunden aus. (Wie eine derartige Zeitintervallberechnung aussehen kann, demonstriert das nächste Beispiel.) Private Sub Button1_Click(...) Handles Button1.Click Dim gr As Graphics = Graphics.FromImage(bm) Dim i As Integer Dim br As SolidBrush = New SolidBrush(Color.Black) ' Maus als Sanduhr darstellen Me.Cursor = Cursors.WaitCursor ' Kantenglättung gr.SmoothingMode = Drawing.Drawing2D.SmoothingMode.HighQuality For i = 511 To 1 Step -1 br.Color = Color.FromArgb( _ 255, CInt(255 - i / 2), CInt(255 - i / 2)) gr.FillEllipse(br, _ CSng(i * 1.5), CSng(i / 2 * (1 + Math.Sin(i / 20))), i, i) If i Mod 4 = 0 Then gr.DrawEllipse(Pens.Black, _ CSng(i * 1.5), CSng(i / 2 * (1 + Math.Sin(i / 20))), i, i) End If Next ' Grafik anzeigen PictureBox1.Refresh() ' Aufräumarbeiten gr.Dispose() br.Dispose() End Sub
936
16 Grafikprogrammierung (GDI+)
Beachten Sie bitte, dass es im Programm keine PictureBox1_Paint-Prozedur gibt! Diese ist nicht erforderlich. Die Grafik ist in der Bitmap bm gespeichert, die wiederum mit PictureBox1.Image verbunden ist. Jedes Mal, wenn Teile des Steuerelements neu zu zeichnen sind, wird die Bitmap automatisch ausgelesen und angezeigt.
Beispielprogramm zu Variante 2 Als zweites Beispiel dient das altbekannte Apfelmännchenprogramm. Das Programm berechnet auf Knopfdruck den in Abbildung 16.38 zu sehenden Ausschnitt aus der Mandelbrotmenge für die gerade aktuelle Fenstergröße. Eine bemerkenswerte Eigenschaft des Programms besteht darin, dass die Berechnung in einem eigenen Thread erfolgt und jederzeit beendet werden kann. (Das Programm ist während der Berechnung also nicht blockiert, sondern kann weiter uneingeschränkt verwendet werden.) Wenn die Berechnung länger als eine Sekunde dauert, wird der Fensterinhalt einmal pro Sekunde aktualisiert.
Abbildung 16.38: Die Apfelmännchengrafik ist einer Bitmap zwischengespeichert und kann sofort wiederhergestellt werden, wenn das Fenster vorübergehend verdeckt war
Überblick Wegen des Multithreading-Ansatzes wirkt der Programmcode vielleicht ein wenig unübersichtlich. Das Problem besteht darin, dass es zu Fehlermeldungen kommt, wenn beide Threads auf dieselben Daten zugreifen müssen. Genau das ist aber erforderlich, um den Fensterinhalt während der Berechnung regelmäßig zu aktualisieren. Um Zugriffskonflikte zu vermeiden, gibt es im Programm drei unterschiedliche Bitmap-Variablen: •
bm (deklariert auf Klassenebene) enthält wie beim Beispielprogramm zu Variante 1 den Grafikinhalt des PictureBox1-Steuerelements. Die Bitmap wird in Form_Load initialisiert, in PictureBox1_Resize bei Bedarf vergrößert und in PictureBox1_Paint dazu verwendet, um
16.5 Interna und spezielle Programmiertechniken
937
den Inhalt des Steuerelements wiederherzustellen. Diese Bitmap ist ausschließlich dem Haupt-Thread zugänglich. •
calcbm (deklariert in der Prozedur calc_mandelbrot) enthält die Grafik während der Be-
rechnung. Diese Bitmap ist ausschließlich dem Berechnungsthread zugänglich. •
In tmpbm (deklariert auf Klassenebene) wird schließlich einmal pro Sekunde der Inhalt von calcbm kopiert. calcbm wird in PictureBox1_Paint verwendet, um den Inhalt auch während der laufenden Berechnung des Steuerelements darzustellen. calcbm wird daher von beiden Threads intensiv genutzt. Um Zugriffsprobleme zwischen beiden Threads zu vermeiden, erfolgt jeder Zugriff auf calcbm in SyncLock-Blöcken. (bm kann für diesen Zweck nicht verwendet werden, weil das darin gespeicherte Bitmap-Objekt bei einer Fenstervergrößerung neu erzeugt wird.)
Der Berechnungs-Thread zur Ausführung der Prozedur calc_mandelbrot wird in der Ereignisprozedur Button1_Click gestartet. Der Thread endet von selbst, wenn die Berechnung abgeschlossen ist. Er kann aber auch vorzeitig durch den Button BERECHNUNG STOPPEN abgebrochen werden. Zur Kommunikation zwischen den beiden Threads werden außer tmpbm die beiden Variablen calcRunning und calcResultWaiting verwendet. calcRunning gibt an, ob momentan ein Berechnungs-Thread läuft. calcResultWaiting gibt an, dass der Berechnungs-Thread abgeschlossen ist und das die Ergebnis-Bitmap aus tmpbm gelesen werden kann.
Verwaltung des PictureBox1-Steuerelements Sehr kurz fällt Form1_Load aus: Hier wird lediglich die Bitmap-Variable bm für die PictureBox1 initialisiert. Anders als bei Variante 1 wird diese Variable aber nicht der Image-Eigenschaft von PictureBox1 zugewiesen. 'Beispiel grafik\autoredraw-mandelbrot 'Bitmap für PictureBox1 Dim bm As Bitmap ' Bitmap für Updates während der Berechnung Dim tmpbm As Bitmap Private Sub Form1_Load(...) Handles MyBase.Load bm = New Bitmap(PictureBox1.ClientSize.Width, _ PictureBox1.ClientSize.Height) End Sub
Der Code in PictureBox1_Resize ist wie bei Variante 1 dafür zuständig, die Bitmap bm zu vergrößern, wenn sich die Größe von PictureBox1 ändert. Der Code ist fast identisch mit dem von Variante 1, weswegen auf einen neuerlichen Abdruck verzichtet wurde. Der einzige Unterschied besteht darin, dass die neue Bitmap nicht an PictureBox1.Image zugewiesen wird.
938
16 Grafikprogrammierung (GDI+)
' Bitmap für PictureBox gegebenenfalls vergrößern Private Sub PictureBox1_Resize(...) Handles PictureBox1.Resize ... wie bei Variante 1 End Sub
Neu ist dagegen PictureBox1_Paint: Wenn das Beispiel keine Multithreading-Anwendung wäre, bestünde die Prozedur aus einer einzigen Zeilen, in der bm im Steuerelement mit DrawImageUnscaled angezeigt würde: ' Bitmap anzeigen Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint gr.DrawImageUnscaled(bm, New Point(0, 0)) End Sub
Im konkreten Fall ist es allerdings ein bisschen komplizierter: In der Paint-Prozedur muss zwischen drei Fällen unterschieden werden: •
Die Berechnung läuft noch: In diesem Fall kann das aktuelle Zwischenergebnis aus tmpbm gelesen und angezeigt werden. Diese Operationen müssen mit SyncLock geblockt werden, um sicherzustellen, dass der gleichzeitig noch aktive Berechnungs-Thread tmpbm während dieser Zeit in Ruhe lässt.
•
Die Berechnung wurde gerade abgeschlossen: In diesem Fall enthält tmpbm das Endergebnis. Die Bitmap muss nun nicht nur angezeigt, sondern außerdem in die Variable bm kopiert werden.
•
Es findet gerade keine Berechnung statt (der Normalfall): bm wird im Steuerelement angezeigt.
Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics If calcRunning Then ' Berechnung läuft noch, Zwischenergebnis aus tmpbm anzeigen SyncLock tmpbm gr.DrawImageUnscaled(tmpbm, New Point(0, 0)) End SyncLock ElseIf calcResultWaiting Then ' Berechnung ist abgeschlossen; tmpbm enthält nun die fertige ' Grafik Dim gr_bm As Graphics = Graphics.FromImage(bm) gr_bm.DrawImageUnscaled(tmpbm, New Point(0, 0)) gr.DrawImageUnscaled(bm, New Point(0, 0)) calcResultWaiting = False tmpbm.Dispose() Else ' Normalfall, keine Berechnung läuft, einfach bm anzeigen gr.DrawImageUnscaled(bm, New Point(0, 0)) End If End Sub
16.5 Interna und spezielle Programmiertechniken
939
Initialisierungsarbeiten für den Berechnungs-Thread Zur Verwaltung des Berechnungs-Threads sowie zur Kommunikation zwischen den beiden Threads werden am Beginn des Programms einige Variablen deklariert. calcThreadDelegate enthält die Adresse der Prozedur, die zur Berechnung der Apfelmännchengrafik in einem eigenen Thread gestartet werden soll. calcThread enthält das dazugehörende Thread-Objekt. Die bereits erwähnten Variablen calcRunning und calcResultWaiting geben schließlich Auskunft über den aktuellen Status des Berechnungs-Threads. ' Variablen zur Verwaltung des Berechnungs-Threads Dim calcThreadDelegate As New Threading.ThreadStart( _ AddressOf calc_mandelbrot) Dim calcThread As Threading.Thread Dim calcRunning As Boolean = False Dim calcResultWaiting As Boolean = False Dim calcResultWaiting As Boolean = False
Innerhalb des Berechnungs-Threads muss zur Aktualisierung des Fensterinhalts die Methode PictureBox1.Invalidate aufgerufen werden. Laut Dokumentation ist ein direkter Aufruf einer Methode aus einem anderen Thread als dem, der das Steuerelement erzeugt hat, aber unzulässig. Daher wird der Umweg über die Hilfsprozedur InvalidatePictureBox1 gewählt. Diese Prozedur wird vom Berechnungs-Thread aus mit PictureBox1.Invoke aufgerufen. Invoke bewirkt, dass der Code tatsächlich erst etwas später in dem Thread aufgerufen wird, in dem das Steuerelement erzeugt worden ist (also im Haupt-Thread dieses Programms). An Invoke muss die Adresse von InvalidatePictureBox1 allerdings in Form eines Delegate-Objekts übergeben werden. Aus diesem Grund wird mit der ersten der vier folgenden Zeilen eine Art Delegate-Klasse für einen Prozeduraufruf ohne Parameter erzeugt. (Diese Klasse wird dann in der Prozedur calc_mandelbrot verwendet. Deren Code folgt etwas weiter unten.) Private Delegate Sub DelegateInvalidatePictureBox1() ' Hilfsprozedur, um PictureBox1.Invalidate() Thread-safe auszuführen Private Sub InvalidatePictureBox1() PictureBox1.Invalidate() End Sub
Berechnungs-Thread starten Der Berechnungs-Thread wird in Button1_Click gestartet. Die Prozedur beginnt mit einem sehr gründlichen Test, ob die Berechnung nicht vielleicht schon läuft. Dabei werden sowohl die beiden calcXxx-Variablen als auch das calcThread-Objekt ausgewertet, um mögliche Synchronisationsprobleme zwischen beiden Threads mit Sicherheit auszuschließen. Zum eigentlichen Start wird ein neues Thread-Objekt erzeugt, benannt (zur einfacheren Fehlersuche) und mit Start gestartet. (Tatsächlich erfolgt der Start dann ein paar ms später, abhängig davon, wann das Betriebssystem den nächsten Task-Wechsel vorsieht.)
940
16 Grafikprogrammierung (GDI+)
' Berechnung in einem eigenen Thread starten Private Sub Button1_Click(...) Handles Button1.Click ' nur dann einen neuen Thread starten, wenn er noch nicht läuft If calcRunning = True Or calcResultWaiting = True Then Exit Sub If IsNothing(calcThread) = False AndAlso _ calcThread.IsAlive = True Then Exit Sub ' bisherige Bitmap löschen Dim gr As Graphics = Graphics.FromImage(bm) gr.Clear(PictureBox1.BackColor) gr.Dispose() PictureBox1.Invalidate() ' neuen Thread starten calcThread = New Threading.Thread(calcThreadDelegate) calcThread.Name = "calculation thread" calcThread.Start() End Sub
Berechnung durchführen Die Berechnung der Grafik erfolgt in calc_mandelbrot. Die Prozedur beginnt mit diversen Initialisierungsarbeiten. Erwähnenswert ist eigentlich nur die Zeile, in der delegateInvalPict als Objekt der Klasse DelegateInvalidatePictureBox1 erzeugt wird. (Diese Klasse wurde am Beginn des Programmcodes mit Delegate deklariert.) Die Variable delegateInvalPict enthält die Adresse der Hilfsprozedur InvalidatePictureBox1 und wird später für den Aufruf dieser Prozedur verwendet. ' Apfelmännchengrafik in der Bitmap calcbm berechnen ' diese Prozedur wird in einem eigenen Thread ausgeführt Sub calc_mandelbrot() ' Bildparameter Const real0 As Double = -0.7476 Const real1 As Double = -0.7452 Const imag0 As Double = 0.0976 Const countermax As Integer = 200 ' Farben für die Grafik Dim col() As Color = {Color.Black, Color.White} ' Bitmap zur Speicherung der Grafik Dim calcbm As Bitmap ' Delegate zum Thread-sicheren Aufruf von PictureBox1.Invalidate Dim delegateInvalPict As DelegateInvalidatePictureBox1 = _ AddressOf InvalidatePictureBox1
16.5 Interna und spezielle Programmiertechniken
941
' Zeit, wie oft das Bild aktualisiert wird Const refresh_interval As Integer = 1 ' in Sekunden Dim nextrefresh As Date ' Variablen zur Berechnung der Grafik Dim x, y, result, tmp As Integer Dim xmax, ymax As Integer Dim real, imag, delta As Double ' Größe der Picturebox ermitteln ' Abbrechen, wenn Größe 0 xmax = PictureBox1.ClientSize.Width ymax = PictureBox1.ClientSize.Height If xmax < 1 Or ymax < 1 Then Exit Sub
Der folgende Code ist durch Try gegen allfällige Fehler abgesichert. Fehler sollten eigentlich keine vorkommen, es kann aber passieren, dass der Thread an einer anderen Stelle im Programm mit Abort beendet wird. In diesem Fall können im Finally-Block Aufräumarbeiten durchgeführt werden. Es wird nun die Bitmap calcbm in der aktuellen Größe des Innenbereichs der PictureBox1 erzeugt. tmpbm bekommt mit Clone eine eigenständige Kopie der Bitmap. Anschließend wird eine Schleife über alle Punkte x und y der Bitmap ausgeführt. Für jeden Punkt wird die Funktion calc_mandelbrot_point ausgeführt. Das Ergebnis dieser Funktion bestimmt die Farbe, die mit SetPixel in die Bitmap gezeichnet wird. Zirca einmal pro Sekunde (je nach Einstellung der Konstanten refresh_interval ) soll der Fensterinhalt erneuert werden. Dazu wird der aktuelle Inhalt von calcbm in tmpbm kopiert. Anschließend wird via Invoke die Prozedur InvalidatePictureBox1 aufgerufen, die dann (etwas später im Haupt-Thread) die Methode PictureBox1.Invalidate ausführt. Nach dem Ende der beiden Schleifen wird das Endergebnis aus calcbm nochmals nach tmpbm kopiert. Mit calcResultWaiting = True wird PictureBox1_Paint signalisiert, dass diese Prozedur als Ergebnis von tmpbm nach bm kopieren soll. Und damit es überhaupt zu einem Aufruf von PictureBox1_Paint kommt, wird nochmals Invoke bemüht. ' Absicherung der gesamten Berechnung (auch gegenüber ' calcThread.Abort) Try ' Initialisierung calcbm = New Bitmap(xmax, ymax) 'Bitmap für die Berechnung If Not IsNothing(tmpbm) Then tmpbm.Dispose() tmpbm = CType(calcbm.Clone, Bitmap) 'Bitmap für Updates calcRunning = True calcResultWaiting = False delta = (real1 - real0) / xmax nextrefresh = Now.AddSeconds(refresh_interval)
942
16 Grafikprogrammierung (GDI+)
' Schleife über alle Punkte For x = 0 To xmax - 1 real = real0 + delta * x For y = 0 To ymax - 1 imag = imag0 + delta * y result = calc_mandelbrot_point(real, imag, countermax) calcbm.SetPixel(x, y, _ col(result Mod (col.GetUpperBound(0) + 1))) Next ' Fenster gelegentlich aktualisieren ' Achtung: zu viele Refreshs machen das Ganze sehr langsam! If Now > nextrefresh Then SyncLock tmpbm ' calcbm in tmpbm kopieren Dim gr As Graphics = Graphics.FromImage(tmpbm) gr.DrawImageUnscaled(calcbm, New Point(0, 0)) tmpbm = CType(calcbm.Clone(), Bitmap) gr.Dispose() End SyncLock ' enstpricht PictureBox1.Invalidate() PictureBox1.Invoke(delegateInvalPict) nextrefresh = Now.AddSeconds(refresh_interval) End If Next ' Endergebnis in bm kopieren SyncLock tmpbm Dim gr As Graphics = Graphics.FromImage(tmpbm) gr.Clear(PictureBox1.BackColor) gr.DrawImageUnscaled(calcbm, New Point(0, 0)) gr.Dispose() End SyncLock ' die neue Bitmap sichtbar machen calcRunning = False calcResultWaiting = True ' entspricht PictureBox1.Invalidate() PictureBox1.Invoke(delegateInvalPict) Finally ' Aufräumarbeiten calcRunning = False If Not IsNothing(calcbm) Then calcbm.Dispose() End Try End Sub
16.5 Interna und spezielle Programmiertechniken
943
Die Berechnung eines einzelnen Punkts erfolgt in der Funktion calc_mandelbrot_point. Dessen Code ist schon beinahe unendlich oft beschrieben worden, weswegen ich hier darauf verzichte. Im Folgenden ist daher nur der Prozedurkopf abgedruckt. ' einen Punkt berechnen (wird später durch C-Funktion ersetzt) Function calc_mandelbrot_point( _ ByVal realstart As Double, ByVal imagstart As Double, _ ByVal countermax As Integer) As Integer ... End Function
Berechnungs-Thread abbrechen, Programmende Mit dem Button BERECHNUNG STOPPEN kann eine laufende Berechnung abgebrochen werden. Dazu wird in Button2_Click die Methode Abort ausgeführt. Anschließend wartet der Haupt-Thread mit Join so lange, bis dies tatsächlich geglückt ist. (Das kann einige ms dauern.) Durch Invalidate wird schließlich ein Neuzeichnen des jetzt wieder leeren PictureBox1-Steuerelements ausgelöst. Private Sub Button2_Click(...) Handles Button2.Click If Not IsNothing(calcThread) Then calcThread.Abort() ' Thread abbrechen calcThread.Join() ' warten, bis das geglückt ist PictureBox1.Invalidate() End If End Sub
Auch beim Programmende muss sichergestellt werden, dass der Berechnungs-Thread beendet wird. Private Sub Form1_Closed(...) Handles MyBase.Closed If Not IsNothing(calcThread) Then calcThread.Abort() calcThread.Join() End If End Sub
Verbesserungsideen Wenn Ihnen das Programm gefällt, sind zahllose Verbesserungsmöglichkeiten denkbar: •
Das Programm berechnet immer den gleichen Ausschnitt aus der Mandelbrotmenge. Interessanter wäre natürlich, wenn es eine Eingabemöglichkeit für den Ausschnitt gäbe (am besten mit einer Zoom-Möglichkeit).
•
Die Schwarz-Weißdarstellung der Mandelbrotmenge eignet sich zwar gut für den Buchdruck, am Bildschirm würde eine Farbdarstellung aber schöner aussehen.
•
Schließlich wäre eine Speichermöglichkeit für die Bilder praktisch.
944
16 Grafikprogrammierung (GDI+)
16.5.7 Flimmerfreie Grafik (Double-Buffer-Technik) Wenn das Neuzeichnen einer Grafik länger dauert als ca. 30 ms, dann tritt ein wahrnehmbares Flimmern auf. Das betrifft z.B. alle Programme, bei denen sich die angezeigte Grafik mit der Fenstergröße ändert: Während die Fenstergröße mit der Maus angepasst wird, zeichnet das Programm immer wieder Teile oder sogar den gesamten Fensterinhalt neu. Dabei wird zuerst der gesamte Hintergrund mit der Hintergrundfarbe gefüllt. Anschließend werden verschiedene Grafikkommandos ausgeführt. Bei anspruchsvollen Grafiken (oder auf langsamen Rechnern) können die Programmanwender gleichsam zusehen, wie die Grafik Schritt für Schritt entsteht. Sie können den Effekt selbst ausprobieren, wenn Sie das Beispielprogramm grafik\font-rotation starten (siehe Abschnitt 16.3.5) und dessen Fenstergröße ändern.
TIPP
Die gängige Abhilfe für dieses Problem ist die so genannte Double-Buffer-Technik. Dabei werden jedes Mal, wenn der Fensterinhalt neu aufgebaut werden muss, alle Grafikoperationen zuerst in einer unsichtbaren Bitmap ausgeführt. Erst wenn die Grafik fertig ist, wird sie im Fenster angezeigt. Durch diese Technik kommt es also zu einer kleinen Verzögerung, die aber im Regelfall als weniger störend empfunden wird als ein Flimmern. Der Begriff double buffering resultiert daraus, dass es nun zwei Speicherpuffer für die Grafik gibt: einen sichtbaren (den Bildschirmspeicher) und einen unsichtbaren (die Bitmap, die zum Zeichnen verwendet wird). Wenn das Neuzeichnen der Grafik mehrere Sekunden oder noch länger dauert, sollten Sie aber ein Feedback geben, dass die Grafik gerade neu gezeichnet wird. Eine Möglichkeit besteht darin, das Maussymbol – die Eigenschaft Me.Cursor – während dieser Zeit zu ändern.)
Double-Buffering in GDI+ Sie müssen die Double-Buffer-Technik nicht selbst implementieren – GDI+ sieht entsprechende Mechanismen bereits vor. Allerdings ist sie nicht immer automatisch aktiv. •
Am einfachsten ist es, wenn Sie für Ihre Grafikausgaben ein PictureBox-Steuerelement verwenden. Dort ist Double-Buffering per Default aktiv.
•
Wenn Sie die Ausgaben direkt in einem Fenster durchführen, aktiviert die folgende SetStyle-Anweisung Double-Buffering: Me.SetStyle(ControlStyles.DoubleBuffer Or _ ControlStyles.UserPaint Or _ ControlStyles.AllPaintingInWmPaint, True)
Sie können diese Anweisung z.B. in der Form_Load-Prozedur ausführen (oder an einer andere Stelle im Code, die für Initialisierungsaufgaben geeignet ist). Falls Sie zusätzlich möchten, dass der Fensterinhalt bei jeder Größenänderung vollständig neu gezeichnet werden soll, sieht die Anweisung so aus:
16.5 Interna und spezielle Programmiertechniken
945
Me.SetStyle(ControlStyles.DoubleBuffer Or _ ControlStyles.UserPaint Or _ ControlStyles.AllPaintingInWmPaint Or _ ControlStyles.ResizeRedraw, True)
Weitere Änderungen im Code sind nicht erforderlich! Alle Graphics-Methoden in Form_Paint werden nun in einer Bitmap zwischengespeichert und erst nach Abschluss der
HINWEIS
Ereignisprozedur am Bildschirm sichtbar. Dass Double-Buffering im PictureBox-Steuerelement automatisch aktiviert wird, ist nicht dokumentiert. Ebenso wenig ist dokumentiert, bei welchen grafikfähigen Steuerelementen Double-Buffering verwendet wird und bei welchen nicht. Eine Veränderung des Defaultverhaltens durch SetStyle ist bei Steuerelementen leider nicht möglich, weil das Schlüsselwort als Protected-Methode definiert ist. Eine mögliche Abhilfe besteht darin, ein neues Steuerelement vom vorhandenen Steuerelement abzuleiten, das mittels einer zusätzlichen Methode die Veränderung von SetStyle erlaubt. Die Vorgehensweise ist in Abschnitt 14.12.3 beschrieben.
SetStyle-Methode Die SetStyle-Methode ist für die Control-Klasse definiert, von der die Form-Klasse zur Darstellung von Fenstern abgeleitet ist. An die Methode geben Sie im ersten Parameter einen oder mehrere durch Or verknüpfte Attributnamen an; im zweiten Parameter geben Sie durch True oder False an, ob Sie diese Attribute aktivieren oder deaktivieren möchten. (Damit ermöglicht es SetStyle, einzelne Attribute nicht nur zu setzen, sondern auch wieder zu löschen.) Die aktuelle Einstellung kann mit GetStyle gelesen werden. Als Attribute kommen alle Elemente der ControlStyles-Aufzählung in Frage. Die meisten Attribute sollten im Regelfall nicht verändert werden, weil sie das Verhalten von Steuerelementen grundlegend ändern und daher die korrekte Funktion des Steuerelements beeinträchtigen können. •
Opaque: Der Hintergrund des Steuerelements wird vor dem Paint-Ereignis nicht auto-
matisch wiederhergestellt. Sie müssen dann den Hintergrund selbst zeichnen. Das ist dann sinnvoll, wenn Sie als Hintergrund nicht eine einfache Farbe, sondern ein Muster, einen Farbübergang oder etwas Vergleichbares haben möchten. In solchen Fällen wäre es sinnlos, wenn das Steuerelement zuerst mit einem Defaulthintergrund und danach ein zweites Mal mit einem individuellen Hintergrund gefüllt würde. •
ResizeRedraw: Das Steuerelement wird bei jeder Größenänderung vollständig neu gezeichnet (entspricht steuerelement.ResizeRedraw=True).
•
DoubleBuffer: Grafikausgaben erfolgen unter Zuhilfenahme eines Zwischenpuffers. (Damit das funktioniert, müssen laut Dokumentation außerdem die Attribute AllPaintingInWmPaint und UserPaint gesetzt sein.)
946
•
16 Grafikprogrammierung (GDI+)
UserPaint: Das Steuerelement kümmert sich selbst darum, sich zu zeichnen. (Wenn
dieses Attribut nicht gesetzt ist, kümmert sich das Betriebssystem um die Darstellung des Steuerelements. Das ist bei den meisten Steuerelementen der Regelfall.) •
AllPaintingInWmPaint: Das Steuerelement ignoriert die Betriebssystemnachricht WM_ERASEBKGND, die normalerweise zum Neuzeichnen des Hintergrunds des Steuerele-
ments auffordert.
Beispiel Das Beispielprogramm basiert auf grafik\font-rotation (siehe Abschnitt 16.3.5 und Abbildung 16.24): In einem Fenster wird ein in 30-Grad-Schritten rotierter Text dargestellt. Das Beispielprogramm wurde hier nur in einem winzigen Detail variiert: In Form1_Load wird für das Fenster Double-Buffering aktiviert. ' Beispiel grafik\font-rotation-double-buffer Private Sub Form1_Load(...) Handles MyBase.Load Me.SetStyle(ControlStyles.ResizeRedraw Or _ ControlStyles.DoubleBuffer Or _ ControlStyles.UserPaint Or _ ControlStyles.AllPaintingInWmPaint, True) End Sub
16.5.8 Scrollbare Grafik Variante 1 mit Form.AutoScroll Der einfachste Weg zu einer Grafik, die größer als das Fenster ist und deren sichtbarer Ausschnitt mit Schiebebalken eingestellt werden kann, führt über die AutoScroll-Eigenschaft des Formulars. Diese Eigenschaft bewirkt, dass ein Fenster automatisch mit Schiebebalken ausgestattet wird, sobald die darin enthaltenen Steuerelemente größer sind als das Fenster in seiner aktuellen Größe.
Abbildung 16.39: Die scrollbare Grafik ist 3000*3000 Pixel groß
16.5 Interna und spezielle Programmiertechniken
947
Das Beispielprogramm (siehe Abbildung 16.39) beruht auf dieser Idee: Im Entwurfsmodus wurde in das Formular ein PictureBox-Steuerelement an der Position (0,0) eingefügt. In Form_Load wird die Größe dieses Steuerelements auf 3000*3000 Punkte vergrößert. PictureBox1_Paint enthält eine einfache Zeichenroutine, die das Steuerelement mit zwei diagonalen Linien und einigen Kreisen füllt. ' Beispiel grafik\kingsize1 ' Voraussetzung: Form1.AutoScroll = True ' (wurde im Eigenschaftsfenster eingestellt) Private Sub Form1_Load(...) Handles MyBase.Load PictureBox1.Width = 3000 PictureBox1.Height = 3000 End Sub Private Sub PictureBox1_Paint(...) Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics Dim x0 As Single = PictureBox1.ClientSize.Width / 2.0F Dim y0 As Single = PictureBox1.ClientSize.Height / 2.0F Dim i As Single gr.DrawLine(Pens.Black, 0, 0, x0 * 2, y0 * 2) gr.DrawLine(Pens.Black, 0, x0 * 2, y0 * 2, 0) For i = 0.1 To 1 Step 0.01 gr.DrawEllipse(Pens.Black, _ x0 - i * x0, y0 - i * y0, 2 * i * x0, 2 * i * y0) Next End Sub
Variante 2 mit Panel.AutoScroll Variante 1 ist nur dann durchführbar, wenn die Grafik das gesamte Fenster füllen soll. Wenn Sie im Fenster aber noch andere Bedienungselemente integrieren möchten, muss das PictureBox-Element für die Grafik in einen Rahmen gegeben werden, der kleiner ist als das aktuelle Fenster. Als Rahmen eignet sich das Panel-Steuerelement, das ebenfalls mit einer AutoScroll-Eigenschaft ausgestattet ist. (Das Panel-Steuerelement dient dazu, andere Steuerelemente aufzunehmen.) Ausgangspunkt für das zweite Beispiel ist daher ein Panel-Steuerelement mit AutoScroll = True und BorderStyle = FixedSingle (damit klar wird, in welchem Bereich die Grafik angezeigt wird). In dieses Steuerelement wird ein PictureBox-Steuerelement eingefügt, dessen Größe in Form_Load (oder auch während des Programmentwurfs im Eigenschaftsfenster) auf 3000*3000 Punkte eingestellt wird. Der gesamte Programmcode ist identisch mit dem von Variante 1, weswegen auf einen Abdruck verzichtet wird. (Sie finden das Programm auf der CD zum Buch im Verzeichnis grafik\kingsize2.)
948
16 Grafikprogrammierung (GDI+)
Abbildung 16.40: Die scrollbare Grafik ist abermals 3000*3000 Pixel groß
16.5.9 Einfache Animationseffekte GDI+ ist eine (weitgehend) vorbildliche Implementierung einer objektorientierten Grafikbibliothek. GDI+ wurde allerdings nicht in Hinblick auf maximale Geschwindigkeit entwickelt. So werden in der aktuellen Version die meisten Grafikoperationen nicht durch die Grafik-Hardware beschleunigt. Mit anderen Worten: GDI+ ist nicht geeignet zur Entwicklung von Spielen, effizienten Animationseffekten oder anspruchsvollen 3D-Grafikprogrammen. (Für solche Zwecke sollten Sie entweder auf die alte GDI-Bibliothek oder auf die DirectX-Bibliotheken zurückgreifen. .NET-Unterstützung gibt es hierfür allerdings noch nicht, und generell stellt sich dann die Frage, ob C++ nicht die besser geeignete Programmiersprache wäre.) Wenn Sie einfache Animationseffekte dennoch mit GDI+ erzielen möchten, hier ein paar Tipps: •
Wenn Sie Geschwindigkeitsprobleme haben, sollten Sie auf eine optimale Darstellungqualität zugunsten einer höheren Zeichengeschwindigkeit verzichten (siehe Abschnitt 16.5.1). Testen Sie Ihr Programm auch auf einem langsamen Rechner!
•
Verwenden Sie Double-Buffering, um ein Flimmern der Grafik zu vermeiden (siehe Abschnitt 16.5.7).
•
Achten Sie darauf, dass Sie nach Möglichkeit nicht immer den gesamten Fensterinhalt neu zeichnen, sondern wirklich nur die Teile, die sich geändert haben. (Das bedeutet z.B., dass Sie an die Invalidate-Methode Rechtecke mit der alten bzw. der neuen Position der Spielfigur übergeben. Diese Technik wird im folgenden Beispiel demonstriert.)
Beispiel Der Platz in diesem Buch reicht nicht dazu aus, hier eine Einführung in verschiedene Programmiertechniken zur Erzielung bewegter Grafiken vorzustellen. Stattdessen muss ein einfaches Beispielprogramm ausreichen, das einen sich bewegenden und drehenden Kreissektor darstellt. Als Hintergrund dient ein Farbverlauf von Weiß nach Grau.
16.5 Interna und spezielle Programmiertechniken
949
Die Figur wird in der Tick-Ereignisprozedur eines Timer-Steuerelements bewegt. Diese Prozedur wird alle 50 ms (also ca. 20 Mal pro Sekunde) automatisch aufgerufen. Dort werden die neuen Koordinaten der Figur berechnet. Anschließend wird ein Rechteck ermittelt, das sowohl die bisherige als auch die neue Figur umfasst. Für diesen Bereich wird der Bildschirm durch Invalidate zum Neuzeichnen freigegeben. (Das führt zu einem Paint-Ereignis mit einem entsprechend kleinen Clipping-Gebiet.)
Abbildung 16.41: Ein einfaches Beispiel für eine bewegte Spielfigur
Der Programmcode beginnt mit der Deklaration der Variablen zur Verwaltung der Spielfigur. backgroundbr enthält ein Brush-Objekt zum Zeichnen des Hintergrunds. Dieses Objekt wird nur bei einer Veränderung der Fenstergröße neu erzeugt (damit der Farbübergang immer die gesamte Fensterbreite umfasst). ' Beispiel grafik\animation Const figureSize As Integer = 100 Dim figureX, figureY As Integer Dim oldX, oldY As Integer Dim deltaX, deltaY As Integer Dim figureAngle As Integer Dim deltaAngle As Integer
'Daten der zu bewegenden Figur 'Position 'alte Position 'Bewegungsvektor 'Drehungswinkel 'Drehgeschwindigkeit
Dim backgroundbr As Brush
'Hintergrund des Fensters
In Form1_Load wird die Startposition der Figur sowie deren Geschwindigkeit mit Zufallswerten initialisiert. Außerdem werden mit SetStyle einige Fenstereigenschaften verändert: Zum Zeichnen soll Double-Buffering verwendet werden. Außerdem soll das gesamte Fenster bei einer Größenänderung neu gezeichnet werden. Um den Fensterhintergrund kümmert sich das Programm selbst, d.h., GDI+ braucht den Hintergrund bei Paint-Ereignissen nicht mit einer Farbe zu füllen. Private Sub Form1_Load(...) Handles MyBase.Load Dim random As New Random() Me.MinimumSize = New Size(figureSize * 2, figureSize * 2) Me.SetStyle(ControlStyles.DoubleBuffer Or _ ControlStyles.UserPaint Or _ ControlStyles.AllPaintingInWmPaint Or _ ControlStyles.ResizeRedraw Or _ ControlStyles.Opaque, True)
950
16 Grafikprogrammierung (GDI+)
deltaX = 1 + random.Next(5) deltaY = 1 + random.Next(5) deltaAngle = 1 + random.Next(5) figureX = _ CInt((Me.ClientSize.Width - figureSize - 1) * random.NextDouble) figureY = _ CInt((Me.ClientSize.Height - figureSize - 1) * random.NextDouble) End Sub
In Form1_Resize wird ein Objekt mit dem Füllmuster für den Hintergrund entsprechend der Fensterbreite neu erzeugt. ' Gradient-Brush für Hintergrund neu erstellen (je nach Fenstergröße) Private Sub Form1_Resize(...) Handles MyBase.Resize If Not IsNothing(backgroundbr) Then backgroundbr.Dispose() backgroundbr = New Drawing2D.LinearGradientBrush( _ New Point(0, 0), _ New Point(Me.ClientSize.Width, 0), _ Color.White, Color.Gray) End Sub
Zum Zeichnen des Fensterinhalts ist wie üblich die Paint-Ereignisprozedur zuständig. Falls backgroundbr noch leer ist, wird Form1_Resize zu dessen Initialisierung aufgerufen. Anschließend werden mit zwei einfachen FillRectangle- bzw. FillPie-Methoden der Hintergrund und die Figur gezeichnet. ' Fensterinhalt zeichnen Private Sub Form1_Paint(...) Handles MyBase.Paint Dim gr As Graphics = e.Graphics ' eventuell Brush-Objekt für Hintergrund initialisieren If IsNothing(backgroundbr) Then Form1_Resize(Nothing, Nothing) ' Hintergrund gr.FillRectangle(backgroundbr, _ 0, 0, Me.ClientSize.Width, Me.ClientSize.Height) ' Figur zeichnen gr.FillPie(Brushes.Red, _ figureX, figureY, figureSize, figureSize, figureAngle, 300) End Sub
Gleichsam der Motor des Programms ist das Timer-Steuerelement, das regelmäßig den Aufruf der Timer1_Tick-Prozedur verursacht. Zur Neuberechnung der Position der Figur wird einfach deltaX zur aktuellen x-Koordinate, deltaY zur aktuellen y-Koordinate addiert. Falls die Figur dadurch über den Fensterrand hinaus bewegt würde, werden die Koordinaten korrigiert und deltaX bzw. deltaY negiert. Damit die Bewegung der Figur im Fenster tatsächlich sichtbar wird, muss mit Invalidate ein Neuzeichnen veranlasst werden. Um den zu zeichnenden Bereich zu minimieren, wird mit Rectangle.Union ein Rechteck zusammengesetzt, das die bisherige und die neue Figur umschließt.
16.5 Interna und spezielle Programmiertechniken
951
Private Sub Timer1_Tick(...) Handles Timer1.Tick Dim rect As Rectangle Dim w, h As Integer ' maximale Koordinaten für die Spielfigur w = Me.ClientSize.Width - figureSize h = Me.ClientSize.Height - figureSize ' neue Position der Spielfigur berechnen figureX += deltaX If figureX < 0 Then figureX = 0 deltaX *= -1 ElseIf figureX > w Then figureX = w deltaX *= -1 End If ... analoger Code für figureY figureAngle = (figureAngle + deltaAngle) Mod 360 ' Neuzeichnen veranlassen rect = Rectangle.Union( _ New Rectangle(figureX - 1, figureY - 1, _ figureSize + 2, figureSize + 2), _ New Rectangle(oldX - 1, oldY - 1, figureSize + 2, figureSize + 2)) Me.Invalidate(rect) oldX = figureX oldY = figureY End Sub
16.5.10 Bitmap mit Fensterinhalt erzeugen und ausdrucken GDI+ bietet leider keine Möglichkeit, eine Bitmap zu erzeugen, die den aktuellen Inhalt eines Fensters oder eines bestimmten Steuerelements enthält. Manchmal wäre es aber (etwa zu Dokumentationszwecken) praktisch, eine 1:1-Abbildung eines Formulars zu speichern oder auszudrucken. In solchen Fällen sind mehrere Vorgehensweisen denkbar. •
Sie können auf API-Funktion BitBlt zurückgreifen, die es in dieser Form in GDI+ nicht mehr gibt. Die Vorgehensweise ist auf der angegebenen Webseite für die Sprache C# beschrieben. Der Vorteil besteht darin, dass dies sowohl für einzelne Steuerelemente als auch für ein ganzes Fenster funktioniert. (Das Fenster muss allerdings sichtbar sein, d.h., es darf nicht von anderen Fenstern überlagert sein.) http://www.c-sharpcorner.com/Graphics/ScreenCaptFormMG.asp
952
16 Grafikprogrammierung (GDI+)
•
Wenn Sie konform zu GDI+ bleiben möchten und nur den Inhalt eines einzelnen Steuerelements (z.B. einer PictureBox) als Bitmap benötigen, müssen Sie diese Bitmap bereits beim Erstellen des Formulars erzeugen und alle Grafikausgaben dort durchführen. Diese Vorgehensweise wurde bereits in Abschnitt 16.5.6 beschrieben.
•
Eine dritte Möglichkeit besteht darin, mit SendKeys die Tastenkombination Alt+Druck zu erzeugen, um auf diese Weise das aktuelle Fenster als Bitmap in die Zwischenablage zu kopieren. Von dort können Sie die Bitmap dann lesen und anschließend verarbeiten.
Das folgende Beispielprogramm demonstriert die dritte Variante: Innerhalb eines Formulars wird eine einfache Grafik gezeichnet (siehe Abbildung 16.42). Mit dem Button SCREENSHOT DURCHFÜHREN wird eine Bitmap des gesamten Fensters erzeugt und am Standarddrucker im Querformat ausgedruckt.
Abbildung 16.42: Dieses Fenster kann per Button-Klick ausgedruckt werden
Programmcode Das Programm verwendet die Variable bm, um darin die Bitmap mit dem Fensterinhalt zwischenzuspeichern. In Button1_Click wird der Screenshot durchgeführt: Sendkeys.Send sendet Alt+Druck in den Tastaturzwischenspeicher, Sendkeys.Flush bewirkt, dass diese virtuelle Tastatureingabe sofort bearbeitet wird (und nicht erst nach dem Ende der Ereignisprozedur). Mit GetData wird die Bitmap aus der Zwischenablage gelesen. Da GetData ein Objekt des Typs Object zurückliefert, muss CType zur Umwandlung in den Typ Bitmap verwendet werden. Schließlich wird mit PrintDocument1.Print ein Ausdruck initiiert. Der eigentliche Ausdruck erfolgt dann in der Ereignisprozedur PrintDocument1_PrintPage, die automatisch aufgerufen wird. Dort wird die Methode DrawImageUnscaled verwendet, um die Bitmap auszugeben. Anschließend wird die Bitmap mit Dispose gelöscht (weil sie nicht mehr länger benötigt wird).
VERWEIS
16.5 Interna und spezielle Programmiertechniken
953
PrintDocument1 ist ein Objekt der Klasse PrintDocument, die in .NET für das Drucken
von Dokumenten verantwortlich ist. Das Thema Drucken wird im Detail in Kapitel 17 behandelt. Dort lernen Sie auch, wie Sie einen Dialog zur Auswahl des Druckers anzeigen können etc.
' Beispiel grafik\screenshot Dim bm As Bitmap ' Screenshot erstellen und in der Variablen bm speichern, ' Ausdruck initiieren Private Sub Button1_Click(...) Handles Button1.Click ' Screenshot durchführen SendKeys.Send("%({PRTSC})") SendKeys.Flush() bm = CType(Clipboard.GetDataObject.GetData("Bitmap", True), Bitmap) ' ausdrucken PrintDocument1.DefaultPageSettings.Landscape = True PrintDocument1.Print() End Sub ' einfaches Muster zeichnen Private Sub Form1_Paint(...) Handles MyBase.Paint Dim i As Integer Dim gr As Graphics = e.Graphics For i = 4 To 400 Step 4 gr.DrawRectangle(Pens.Red, _ i, CSng(100 + Math.Sin(i / 50) * 100), 50, 50) Next End Sub ' Bitmap bm ausdrucken Private Sub PrintDocument1_PrintPage(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles PrintDocument1.PrintPage Dim gr As Graphics = e.Graphics gr.DrawImageUnscaled(bm, e.MarginBounds.X, e.MarginBounds.Y) e.HasMorePages = False bm.Dispose() End Sub
954
16 Grafikprogrammierung (GDI+)
16.5.11 Bitmap über Byte-Feld adressieren Dieser Abschnitt richtet sich an VB.NET-Profis. Er beschreibt eine Technik, mit der Sie den Inhalt einer Bitmap vorübergehend über ein Byte-Feld ansprechen können. Das verkompliziert die Programmierung zwar erheblich, kann aber Bildverarbeitungsalgorithmen deutlich beschleunigen. Ausgangspunkt für das folgende Programm ist die Methode LockBits. Damit kann ein Teil des Bitmaps (oder auch die gesamte Bitmap) in einen Speicherbereich kopiert werden. An die Methode muss die Größe des Bereichs und das gewünschte Pixelformat übergeben werden. Als Ergebis erhalten Sie ein BitmapData-Objekt, das Informationen über den Speicherbereich angibt, in dem sich die Bitmap jetzt befindet. Die wichtigsten Eigenschaften der Bitmap-Klasse sind: Scan0: gibt die Speicheradresse (IntPtr-Objekt) an, an der sich der erste Pixel befindet. Stride: gibt die Anzahl der Byte an, die zur Speicherung einer Pixel-Zeile verwendet wird. Width, Height: geben die Größe der Bitmap bzw. des Bitmap-Bereichs an.
In VB.NET nützen Ihnen diese Daten unmittelbar noch nicht viel, weil anders als in C# keine Möglichkeit besteht, so genannten unsafe code zu erzeugen, also Speicher direkt durch Zeiger (pointer) zu adressieren. Die Klasse Runtime.InteropServices.Marshal stellt aber eine Copy-Methode zur Verfügung, mit der Sie Speicher in ein Byte-Feld und wieder zurück kopieren können. Zur Bearbeitung des Byte-Felds müssen Sie noch wissen, wie die Daten darin organisiert sind: Wenn Sie bei LockBits als Pixelformat Format24bppRgb angeben, dann gelten für das Byte-Feld folgende Regeln: byt(0) byt(1) byt(2) byt(3) byt(4) byt(5) byt(bytes_per_line) byt(bytes_per_line+1) byt(bytes_per_line+2) ...
Blauanteil von Pixel (0,0) Grünanteil von Pixel (0,0) Rotanteil von Pixel (0,0) Blauanteil von Pixel (1,0) Grünanteil von Pixel (1,0) Rotanteil von Pixel (1,0) ... Blauanteil von Pixel (0,1) Grünanteil von Pixel (0,1) Rotanteil von Pixel (0,1)
Wenn Sie mit der Bearbeitung des Byte-Felds fertig sind, verwenden Sie abermals Copy, um die Daten zurück in den Bitmap-Speicher zu kopieren, und anschließend UnlockBits, um die Bitmap wieder für die normale Verwendung freizugeben.
16.5 Interna und spezielle Programmiertechniken
955
Die Idee für das gesamte Verfahren (Umwandlung eines Farbbilds in ein Graustufenbild) stammt von Eric Gunnerson und ist für C# hier dokumentiert: VERWEIS
http://msdn.microsoft.com/library/en-us/dncscol/html/csharp11152001.asp
Jacob Grass hat aus dem dort vorgestellten Programm eine VB.NET-Version entwickelt und am 14.12.2001 in einigen Newsgroups (darunter microsoft.public.dotnet.languages.vb) veröffentlicht. Ich habe die Idee für dieses Buch nochmals aufgegriffen und versucht, den Code auf ein Minimum zu reduzieren, um so ein möglichst einfach nachvollziehbares Muster für andere Anwendungen zu schaffen.
Beispiel Abbildung 16.43 zeigt das Ergebnis einer kleinen Beispielanwendung. Wenn Sie den Button SETPIXEL anklicken, wird eine Bitmap in der Größe der unter den Buttons befindlichen PictureBox erzeugt, mit Pixeln gefüllt und dann der Image-Eigenschaft der PictureBox zugewiesen. Wenn Sie den anderen Button verwenden, wird das Pixelmuster über den Umweg eines Byte-Felds erzeugt. Sie werden feststellen, dass der zweite Weg etwa um den Faktor drei schneller ist. (Vergrößern Sie das Fenster, um sinnvolle Messergebnisse zu erhalten!)
Abbildung 16.43: Beispiel zur Demonstration verschiedener Bitmap-Zeichentechniken
Der Code der traditionellen Variante ist leicht zu verstehen. Die Bitmap wird in einer Schleife mit SetPixel angemalt und anschließend der Image-Eigenschaft von PictureBox1 zugewiesen. Falls sich dort bereits eine Bitmap befindet, wird diese mit Dispose aus dem Speicher entfernt (um Speicher zu sparen).
956
16 Grafikprogrammierung (GDI+)
' Beispiel grafik\bitmap-hardcore Private Sub Button1_Click(...) Handles Button1.Click Dim bm As Bitmap Dim x, xmax, y, ymax As Integer Dim starttime As DateTime = Now ' neue Bitmap in der Größe der PictureBox xmax = PictureBox1.ClientRectangle.Width ymax = PictureBox1.ClientRectangle.Height bm = New Bitmap(xmax, ymax) For x = 0 To xmax - 1 For y = 0 To ymax - 1 bm.SetPixel(x, y, _ Color.FromArgb(_ CInt(x * 255 / xmax), CInt(y * 255 / ymax), 128)) Next Next ' Bitmap in PictureBox1 anzeigen If Not IsNothing(PictureBox1.Image) Then PictureBox1.Image.Dispose() End If PictureBox1.Image = bm Label1.Text = Now.Subtract(starttime).ToString End Sub Button2_Click erfüllt dieselbe Aufgabe wie Button1_Click, beschreitet nun aber den Weg über das Byte-Feld. Das Programm ist sehr ausführlich kommentiert, so dass hier keine weiteren Erklärungen notwendig erscheinen. Private Sub Button2_Click(...) Handles Button2.Click Dim bm As Bitmap Dim x, xmax, y, ymax As Integer Dim bytes_per_line, bytes_total As Integer Dim starttime As DateTime = Now Dim bmdata As Drawing.Imaging.BitmapData Dim startindex, startindex_line As Integer ' neue Bitmap in der Größe der PictureBox xmax = PictureBox1.ClientRectangle.Width ymax = PictureBox1.ClientRectangle.Height bm = New Bitmap(xmax, ymax) ' Speicher der gesamten Bitmap blockieren bmdata = bm.LockBits( _ New Rectangle(0, 0, xmax, ymax), _ Imaging.ImageLockMode.ReadWrite, _ Imaging.PixelFormat.Format24bppRgb)
16.5 Interna und spezielle Programmiertechniken
' Speicher pro Zeile und Gesamtspeicher ausrechnen bytes_per_line = bmdata.Stride bytes_total = bytes_per_line * bmdata.Height ' Byte-Array reservieren, Bitmap-Speicher dorthin kopieren Dim byt(bytes_total - 1) As Byte Runtime.InteropServices.Marshal.Copy(_ bmdata.Scan0, byt, 0, bytes_total) ' Bitmap-Muster erzeugen For y = 0 To ymax - 1 startindex_line = bytes_per_line * y For x = 0 To xmax - 1 startindex = startindex_line + x * 3 byt(startindex) = 128 'Blau byt(startindex + 1) = CByte(y * 255 / ymax) 'Grün byt(startindex + 2) = CByte(x * 255 / xmax) 'Rot Next Next ' Byte-Array zurück in den Bitmap-Speicher kopieren Runtime.InteropServices.Marshal.Copy( _ byt, 0, bmdata.Scan0, bytes_total) ' Bitmap-Speicher wieder freigeben bm.UnlockBits(bmdata) ' Bitmap anzeigen If Not IsNothing(PictureBox1.Image) Then PictureBox1.Image.Dispose() End If PictureBox1.Image = bm Label1.Text = Now.Subtract(starttime).ToString End Sub
957
17 Drucken Das Thema Drucken wurde von Visual Basic 1 bis 6 stiefmütterlich behandelt und trieb so manchen Programmierer zur Verzweiflung (oder zum Kauf teurer Zusatzkomponenten, die das erledigten, wozu Microsoft nicht in der Lage oder willens war). Mit VB.NET zeigt Microsoft nun, wie elegant und unkompliziert das Drucken sein kann: Dank der Steuerelemente PrintDialog und PageSetupDialog können Sie mit wenigen Zeilen Code Dialoge zur Auswahl des Druckers und zur Seiteneinstellung anzeigen. Der eigentliche Ausdruck erfolgt über ein (unsichtbares) PrintDocument-Objekt, wobei der Seitenaufbau auf einem Graphics-Objekt basiert. Es können also alle im vorigen Kapitel ausführlich beschriebenen Grafikmethoden eingesetzt werden. Auch eine Druckvorschau kann problemlos realisiert werden (drei zusätzliche Codezeilen!). Dieses Kapitel stellt die Steuerelemente zum Drucken sowie die wichtigsten Klassen aus dem Namensraum System.Drawing.Printing vor. Anschließend finden Sie eine Reihe konkreter Beispiele, wie Sie den Ausdruck von ein- und mehrseitigen Dokumenten durchführen können. 17.1 17.2 17.3 17.4 17.5
Überblick Grundlagen Beispiel – Mehrseitiger Druck Beispiel – Inhalt eines Textfelds ausdrucken Fortgeschrittene Programmiertechniken
960 961 977 981 992
960
17.1
17 Drucken
Überblick
Vorweg eine erste Beschreibung des Druckvorgangs unter .NET: Das Drucken erfolgt grundsätzlich ereignisgesteuert. Sie können also nicht einfach einen Text mit Print xy an den Drucker senden. Stattdessen benötigen Sie ein PrintDocument-Objekt; wenn Sie dessen Methode Print ausführen, wird die PagePrint-Ereignisprozedur aufgerufen. Dort befindet sich der eigentlich Code zum Drucken. (Die Klasse PrintDocument kann in der VS.NET-Entwicklungsumgebung wie ein Steuerelement verwendet werden und ist deswegen auch in der Toolbox enthalten.)
Klassen Im Namensraum System.Drawing.Printing (Assembly System.Drawing) befinden sich außer PrintDocument unzählige weitere Klassen, mit denen Sie alle erdenklichen Drucker-, Papierund sonstige Einstellungen verwaltet können. Wie so oft in diesem Buch reicht der Platz nicht aus, um alle Klassen, Methoden und Eigenschaften vollständig zu beschreiben. Stattdessen beschränkt sich das Buch auf die wichtigsten Schlüsselwörter. Dafür zeigen einige etwas umfangreichere Beispiele, wie der Ausdruck von Dokumenten in der Praxis funktioniert. Die folgende Tabelle gibt einen Überblick über die wichtigsten Klassen. Wichtige Klassen des System.Drawing.Printing-Namensraum PrintDocument
Administration des Druckvorgangs
PrintPageEventArgs
Parameter, der an die PrintPage-Ereignisprozedur des PrintDocument-Objekts übergeben wird.
PageSettings
Seiteneinstellung (Seitenränder, Hoch-/Querformat etc.
PrinterSettings
Druckereinstellungen (Auswahl des Druckers, Anzahl der Kopien etc.)
Margins
Seitenränder
Steuerelemente Zwar können Sie die Parameter des Druckvorgangs weitestgehend durch die Manipulation der oben aufgezählten Objekte steuern (die alle über das PrintDocument-Objekt zugänglich sind). Ich gehen in diesem Kapitel aber größtenteils davon aus, dass Sie zum Drucken die dafür vorgesehenen Steuerelemente einsetzen. Diese Steuerelemente stellen mehrere fertige Dialoge zur Verfügung und erleichtern die Programmierung erheblich. Falls Sie die Druckdialoge lieber selbst gestalten möchten, finden Sie dazu in Abschnitt 17.5 einige Tipps.
17.2 Grundlagen
961
Steuerelemente zum Drucken aus dem System.Windows.Forms-Namensraum PrintDialog
Standarddialog zum Auswahl des Druckers und zur Angabe des Druckbereichs
PageSetupDialog
Standarddialog zur Einstellung des Seitenlayouts
PrinterPreviewDialog
Standarddialog zur Durchführung einer Druckvorschau
PrintPreviewControl
Steuerelement zur Programmierung einer eigenen Druckvorschau
Limitationen, andere Werkzeuge zum Drucken Zwar hat sich die Programmierung eigener Druckroutinen in VB.NET gegenüber VB6 entscheidend verbessert, dennoch haben auch die in diesem Kapitel vorgestellten Steuerelemente und Klassen ihre Grenzen. •
Es besteht keine Möglichkeit, den Inhalt eines Formulars als Bitmap auszudrucken (d.h. die aus VB6 bekannte Methode PrintForm existiert nicht mehr). Sie können aber den Umweg über Alt+Druck beschreiten, auf diese Weise eine Bitmap des aktiven Fensters ermitteln und diese dann ausdrucken. Ein entsprechendes Beispielprogramm finden Sie in Abschnitt 16.5.10.
•
Es besteht keine Möglichkeit, den Inhalt von Steuerelementen auszudrucken. Das ist bei einigen Steuerelementen (z.B. RichtextBox) eine große Einschränkung, weil die Programmierung eines eigenen Druckcodes extrem aufwendig wäre. Ein möglicher Ausweg besteht darin, die Daten über eine Datei oder die Zwischenablage in ein anderes Programm zu exportieren und dieses (via Automation) zum Ausdruck zu verwenden.
•
Einen Sonderfall stellen schließlich Datenbankanwendungen dar, wo es oft darum geht, seitenlange Zusammenfassungen (reports) unter Berücksichtigung von Abfragen, Bedingungen etc. auszudrucken. In diesem Fall können Sie sich die aufwendige Programmierung eigener Druckroutinen ebenfalls sparen und stattdessen auf das Programm Crystal Reports ausweichen, das mit den meisten VS.NET-Versionen mitgeliefert wird (aber nicht mit der Standardversion).
17.2
Grundlagen
17.2.1 PrintDocument-Steuerelement Das PrintDocument-Steuerelement hilft beim Ausdruck eines Dokuments. Zur Verwendung dieses Steuerelements fügen Sie es einfach von der Toolbox in Ihr Formular ein. Das Steuerelement bleibt im Formular unsichtbar, es stellt aber mehrere Ereignisse zur Verfügung. Der eigentliche Code zum Drucken befindet sich in diesen Ereignisprozeduren.
962
17 Drucken
HINWEISE
Die Anwendung des Steuerelements setzt voraus, dass am Rechner zumindest ein Drucker installiert ist. (Ist das nicht der Fall, führt die Methode Print zum Fehler InvalidPrinterException.) Per Default wird der Windows-Standarddrucker in seinen Standardeinstellungen verwendet. Wenn Sie dem Anwender die Möglichkeit geben möchten, den Drucker auszuwählen oder Druckeinstellungen zu verändern, können Sie dazu das im nächsten Abschnitt beschriebene PrintDialog-Steuerelement einsetzen. Beachten Sie, dass PrintDocument genau genommen kein richtiges Steuerelement ist, sondern eine gewöhnliche Klasse. PrintDocument erscheint nur deswegen als Steuerelement, weil es von der Entwicklungsumgebung als solches behandelt wird. Es kann deswegen über die Toolbox in ein Formular eingefügt werden; die Entwicklungsumgebung kümmert sich bei dieser Gelegenheit auch gleich darum, ein Objekt dieser Klasse automatisch zu erzeugen. Wenn Sie das nicht möchten, können Sie PrintDocument-Objekte auch selbst im Code erzeugen (siehe Abschnitt 17.5.1).
Der Druckvorgang wird durch PrintDocument1.Print eingeleitet (z.B. als Reaktion auf einen Button-Klick oder eine Menüauswahl). In der Folge werden die folgenden Ereignisprozeduren aufgerufen: •
PrintDocument1_BeginPrint() zur Initialisierung des Druckvorgangs.
•
PrintDocument1_QueryPageSettings() zur individuellen Einstellung des Seitenlayouts für die folgende Seite. Diese Ereignisprozedur wird im Regelfall nicht benötigt. Sie ist nur dann erforderlich, wenn die einzelnen Seiten des Ausdrucks mit unterschiedlichem Seitenlayout ausgedruckt werden sollen – z.B. drei Seiten im Hochformat, die vierte Seite im Querformat.
•
PrintDocument1_PrintPage() zum Ausdruck einer Seite. An die Ereignisprozedur wird als Parameter das PagePrintEventArgs-Objekt e übergeben. Dieses Objekt verweist auf mehrere Objekte, die Auskunft über das Seitenlayout geben (Details folgen in Abschnitt 17.2.3), sowie auf ein Graphics-Objekt. Dieses Objekt ist der Schlüssel zur Ausgabe von Text und Grafik: Mit den im vorigen Kapitel vorgestellten Methoden (DrawLine, DrawString etc.) können Sie die aktuelle Seite mit Text und Grafik füllen.
•
PrintDocument1_EndPrint() für allfällige Aufräumarbeiten.
Information über den Druckfortschritt Während des Ausdrucks wird automatisch eine kleine Dialogbox angezeigt, die in Abbildung 17.1 zu sehen ist. Der dort angezeigte Name (hier document) kann über die Eigenschaft DocumentName eingestellt werden. Der Seitenzähler beginnt in jedem Fall mit 1. (Wenn Sie Seite 3-5 ausdrucken, zeigt der Dialog also Seite 1-3 an.) Wenn der Anwender den ABBRUCH-Button anklickt, wird die PrintPage-Ereignisprozedur für die aktuelle Seite zwar bis zum Schluss ausgeführt, die Graphics-Ausgaben werden aber nicht mehr an den Drucker gesandt.
17.2 Grundlagen
963
Abbildung 17.1: Automatische Information über den Druckvorgang
Steuerung des Druckvorgangs Mehrseitiger Ausdruck: Falls ein Ausdruck weitere Seiten umfassen soll, müssen Sie bei allen Seiten außer der letzten die Eigenschaft e.HasMorePages auf True setzen. Solange HasMorePages den Wert True hat, kommt es zu einem weiteren Aufruf der PrintPage-Ereignisprozedur. Allerdings ist die Programmlogik für einen mehrseitigen Ausdruck relativ kompliziert, weil Sie selbst einen Seitenzähler verwalten müssen. Ein Beispiel für einen mehrseitigen Ausdruck finden Sie in Abschnitt 17.3. Ausdruck abbrechen: Wenn Sie innerhalb einer PrintDocument-Ereignisprozedur den Druckvorgang abbrechen möchten, setzen Sie einfach e.Cancel auf False und verlassen die Prozedur. Die bereits begonnene Seite wird nicht gedruckt. (Bereits abgeschlossene Seiten bei einem mehrseitigen Ausdruck sind in der Regel aber bereits im Druckerspooler und unter Umständen auch schon ausgedruckt. Diesen Teil des Ausdrucks können Sie nicht mehr widerrufen.)
Einführungsbeispiel Die folgenden Zeilen zeigen, wie ein einfacher, einseitiger Ausdruck bewerkstelligt wird. Ein Button-Klick löst den Ausdruck aus. Daraufhin wird automatisch PrintDocument1_PrintPage aufgerufen. Dort werden die Eigenschaften e.PageBounds und e.MarginBounds durch Rechtecke und Ellipsen dargestellt. Diese beiden Eigenschaften verweisen auf Rectangle-Objekte, die die Papiergröße und den druckbaren Bereich angeben. (Details zur Bedeutung dieser beiden Rechtecke und dem dabei eingesetzten Koordinatensystem folgen in Abschnitt 17.2.4.) Außerdem wird die Textversion von e.PageSettings mit DrawString angezeigt. Zu den Ereignissen Begin- und EndPrint gibt es keine Ereignisprozeduren. ' Beispiel drucken\intro Private Sub Button1_Click(...) Handles Button1.Click PrintDocument1.Print() End Sub Private Sub PrintDocument1_PrintPage( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles PrintDocument1.PrintPage
964
Dim Dim Dim Dim Dim
17 Drucken
gr As Graphics = e.Graphics ps As Printing.PageSettings = e.PageSettings pagerect As Rectangle = e.PageBounds printrect As Rectangle = e.MarginBounds fnt As New Font("arial", 12)
gr.DrawRectangle(Pens.Black, pagerect) gr.DrawEllipse(Pens.Black, pagerect) gr.DrawRectangle(Pens.Black, printrect) gr.DrawEllipse(Pens.Black, printrect) gr.DrawString(ps.ToString, fnt, Brushes.Black, _ RectangleF.op_Implicit(printrect)) fnt.Dispose() End Sub
Das Ergebnis des Ausdrucks ist in Abbildung 17.2 zu sehen. (Auf dem Testsystem wurde Adobe Distiller als virtueller Standarddrucker eingerichtet. Bei diesem Programm handelt es sich um einen Teil des Adobe-Acrobat-Produktpakets, das wie ein PostScript-Drucker funktioniert, aber PDF-Dateien erzeugt. Das Programm hat sich als ausgesprochen nützlich erwiesen, um die Druckfunktionen zu testen, ohne Unmengen von Papier zu vergeuden. Beachten Sie aber, dass bei einem tatsächlichen Ausdruck Teile der äußeren Ellipse abgeschnitten werden, weil kein Drucker die gesamte Seite bedrucken kann. Mehr Informationen zu diesem Problem folgen in Abschnitt 17.2.4.)
Abbildung 17.2: Ein Beispielausdruck, dargestellt mit Adobe Acrobat
17.2 Grundlagen
965
17.2.2 PrintDialog- und PageSetupDialog-Steuerelement Normalerweise soll der Ausdruck nicht ungefragt an den Standarddrucker gesandt werden. Vielmehr ist es üblich, dass der Anwender unter mehreren Druckern auswählen kann, das Seitenlayout (z.B. Hoch- oder Querformat) verändern kann etc. Zu diesem Zweck sind zwei Standarddialoge vorgesehen: PrintDialog zur Auswahl des Druckers und des Druckbereichs (siehe Abbildung 17.3) sowie PageSetupDialog zur Einstellung des Papierformats (Hoch- oder Querformat, Seitenränder etc., siehe Abbildung 17.4). Im Regelfall (z.B. bei den Microsoft-Office-Programmen, die für viele Anwender ein bekannter Bedienungsmaßstab sind) sieht die Anwendung der Dialoge folgendermaßen aus: Über einen Menüpunkt SEITE EINRICHTEN wird der PageSetupDialog zur Seiteneinstellung angezeigt. Mit OK werden die neuen Einstellungen übernommen. Ein weiterer Menüpunkt DRUCKEN führt zum PrintDialog, wo dann der Drucker und der zu druckende Bereich ausgewählt wird. Hier führt OK dazu, dass das Dokument tatsächlich gedruckt wird. (Eventuell gibt es noch einen dritten Menüpunkt SEITENANSICHT oder DRUCKVORSCHAU – mehr dazu im nächsten Abschnitt.) Ob Sie sich an dieses Schema halten, bleibt natürlich Ihnen überlassen. Die beiden Dialoge bieten auf jeden Fall mehr Flexibilität: Beispielsweise können Sie über den EIGENSCHAFTENButton von PrintDialog ebenfalls das Seitenformat einstellen (nicht aber die Seitenränder). Umgekehrt ermöglicht der DRUCKER-Button von PageSetupDialog auch die Auswahl des Druckers.
Anwendung Die Verwendung beider Steuerelemente ist sehr ähnlich: Sie fügen die Steuerelemente von der Toolbox in Ihr Formular ein. Das Steuerelement bleibt im Formular unsichtbar. Bevor Sie die Dialoge im Programmcode mit ShowDialog anzeigen können, müssen Sie eine der Eigenschaften Document, PrinterSetting oder PageSetting (nur bei PageSetupDialog) einstellen. Am einfachsten ist der Einsatz von PrintDialog bzw. PageSetupDialog in Kombination mit einem PrintDocument-Steuerelement. In diesem Fall reicht die folgende Einstellung aus (die Sie übrigens auch im Eigenschaftsfenster durchführen können!): PrintDialog1.Document = PrintDocument1 PageSetupDialog1.Document = PrintDocument1
Falls Sie die Steuerelemente dagegen losgelöst von PrintDocument-Steuerelement verwenden möchten, müssen Sie selbst ein PrintDocument-, ein PrinterSetting- oder ein PageSettingObjekt erzeugen und dieses an das XxxDialog-Steuerelement zuweisen.
PrintDialog Das PrintDialog-Steuerelement hilft bei der Anzeige des Dialogs zur Auswahl des Druckers sowie zur Einstellung einiger Druckparameter (Seitenbereich, Anzahl der Kopien etc.). Was der Anwender im Dialog alles einstellen darf, hängt von den folgenden Eigenschaften ab:
966
17 Drucken
AllowPrintToFile: ermöglicht den Ausdruck in eine Datei. AllowSelection: ermöglicht es, nur den markierten Bereich eines Dokuments
auszudrucken. AllowSomePages: ermöglicht die Angabe eines Seitenbereichs, der gedruckt werden soll.
Per Default ist AllowPrintToFile=True, die beiden anderen Eigenschaften sind False. Nach der Einstellung dieser Eigenschaften wird der Dialog mit ShowDialog angezeigt (siehe Abbildung 17.3). Wenn diese Methode OK zurückgibt, wird üblicherweise der Druck des Dokuments durch PrintDocument1.Print ausgelöst. Die Auswertung der Druckeinstellungen erfolgt üblicherweise in den PrintDocument-Ereignisprozeduren. (Die zahlreichen Klassen, die Sie dabei berücksichtigen müssen, werden in Abschnitt 17.2.4 vorgestellt. Ein ausführliches Beispiel finden Sie in Abschnitt 17.3. Dort wird ein mehrseitiger Druck demonstriert, wobei der gewünschte Seitenbereich im Druckdialog ausgewählt werden kann.) Private Sub Button1_Click(...) Handles Button1.Click With PrintDialog1 .Document = PrintDocument1 .AllowSomePages = True If .ShowDialog() = DialogResult.OK Then PrintDocument1.Print() End If End With End Sub
Abbildung 17.3: PrintDialog zur Druckerauswahl und zur Einstellung des Druckbereichs
17.2 Grundlagen
967
PageSetupDialog Das PageSetupDialog-Steuerelement hilft bei der Einstellung des Seitenlayouts (Hoch- oder Querformat, Seitenränder, Papierformat, Papiereinzug etc. – siehe Abbildung 17.4). Was der Anwender im Dialog alles einstellen darf, hängt wiederum von einigen AllowXxxEigenschaften ab: AllowMargins: ermöglicht die Einstellung der Seitenränder. AllowOrientation: ermöglicht den Wechsel zwischen Hoch- und Querformat. AllowPaper: ermöglicht die Auswahl der Papiergröße. AllowPrinter: ermöglicht die Druckerauswahl (in einem eigenen Dialog, Button DRUCKER).
Per Default sind alle vier Eigenschaften True. Bei der Einstellung der Seitenränder können Sie über die Eigenschaft MinMargins Minimalwerte vorgeben. Die Eigenschaft verweist auf ein Margins-Objekt, dessen Eigenschaften Left, Right, Top und Bottom die Minimalwerte angeben. Die Eigenschaften haben die Einheit 1/100 Zoll (0,254 mm).
Abbildung 17.4: PageSetupDialog zur Einstellung des Seitenlayouts
Der Dialog wird wie üblich mit ShowDialog angezeigt. Im Programmcode ist üblicherweise keine Auswertung des Ergebnisses erforderlich: wenn der Dialog mit OK beendet wird, werden die neuen Einstellungen in den PrintDocument-Objekten gespeichert, andernfalls bleiben sie unverändert. Die Auswertung erfolgt erst in den PrintDocument-Ereignisprozeduren.
968
17 Drucken
Metrische Probleme im PageSetupDialog Bei der Auswertung der Seitenränder ist Microsoft offensichtlich ein Fehler unterlaufen: Wenn unter Windows das metrische System eingestellt ist (im deutschen Sprachraum per Default, Einstellung mit SYSTEMSTEUERUNG|LÄNDEREINSTELLUNGEN), dann werden die Seitenränder im Dialog in mm angegeben. Das wäre an sich ja toll, nur scheitert der Dialog an der korrekten Umrechnung zwischen dem Druckerkoordinatensystem (Grundeinheit Inch, siehe auch Abschnitt 17.2.4) und den metrischen Maßen. Im Dialog werden um den Faktor 2,54 zu kleine Werte angezeigt. Die Werte werden anschließend aber korrekt zurück in Inch gerechnet. Das bedeutet, dass sich die Seitenränder bei jedem Aufruf des Dialogs um den Faktor 2,54 verkleinern. Die folgende Prozedur korrigiert diese fehlerhafte Berechnung, sofern das Betriebssystem das metrische System verwendet. Um diese Einstellung zu ermitteln, wird die Eigenschaft IsMetric eines Globalization.RegionInfo-Objekts ausgewertet. (Falls der Dialog mit ABBRUCH beendet wird, muss die Korrektur rückgängig gemacht werden.)
VORSICHT
Private Sub Button1_Click(...) Handles Button1.Click ' korrigiert PageSetupDialog-Fehler Dim ri As Globalization.RegionInfo = _ Globalization.RegionInfo.CurrentRegion With PrintDocument1.DefaultPageSettings.Margins If ri.IsMetric Then .Bottom = CInt(.Bottom * 2.54) .Top = CInt(.Top * 2.54) .Left = CInt(.Left * 2.54) .Right = CInt(.Right * 2.54) End If If PageSetupDialog1.ShowDialog() <> DialogResult.OK Then If ri.IsMetric Then .Bottom = CInt(.Bottom / 2.54) .Top = CInt(.Top / 2.54) .Left = CInt(.Left / 2.54) .Right = CInt(.Right / 2.54) End If End If End With End Sub
Die obige Prozedur hat natürlich einen großen Nachteil: Wenn Microsoft den Fehler in PageSetupDialog in einer zukünftigen .NET-Version korrigiert (was zu hoffen ist), dann darf die Korrektur nicht mehr ausgeführt werden, d.h., Sie müssen Ihren Code wieder ändern!
17.2 Grundlagen
969
17.2.3 PrintPreviewDialog-Steuerelement Wenn Sie dem Anwender vor dem Ausdruck die Möglichkeit einer Seitenvorschau (Druckvorschau) bieten möchten, ist das mit drei Zeilen Code erledigt – dem PrintPreviewDialog sei Dank!
VERWEIS
Sie müssen lediglich die Steuerelemente PrintPreviewDialog und PrintDocument über die Eigenschaft Document verbinden (wie bei den oben beschriebenen Steuerelementen PageSetup- und PrintDialog). Das kann wahlweise per Code (siehe unten) oder im Eigenschaftsfenster erfolgen. Verwechseln Sie das PrintPreviewDialog-Steuerelement nicht mit dem PrintPreviewControl-Steuerelement! Letzteres hat zwar einen ganz ähnlichen Namen, bietet aber ganz andere Funktionen. Es handelt sich um ein richtiges Steuerelement, das sichtbar in ein Formular eingebettet wird. Sie können mit diesem Steuerelement einen eigenen Seitenvorschaudialog programmieren. Die Vorgehensweise wird in Abschnitt 17.5.2 am Ende dieses Kapitels kurz erläutert.
Wenn Sie Wert auf eine möglichst hohe Darstellungsqualität legen, müssen Sie die Eigenschaft UseAntiAlias auf True setzen. Große Schriften und Linien erscheinen dann glatter, allerdings ist der Rechenaufwand pro Seite je nach Seiteninhalt zum Teil deutlich höher. (Aus diesem Grund sollten Sie diese Option mit Vorsicht verwenden bzw. dem Anwender Ihres Programm die Möglichkeit geben, die Option nach Bedarf zu verändern.) Anschließend bewirkt ShowDialog, dass ein Seitenvorschaudialog wie in Abbildung 17.5 erscheint. In diesem Dialog können Sie den Zoomfaktor beliebig einstellen sowie ein bis sechs Seiten gleichzeitig ansehen. SCHLIESSEN beendet den Dialog. Wenn Sie den DruckerButton anklicken, wird das Dokument ausgedruckt. (Deswegen muss der Rückgabewert von ShowDialog ausgewertet werden.)
Abbildung 17.5: Seitenvorschau mit dem PrintPreviewDialog-Steuerelement
970
17 Drucken
ANMERKUNG
Private Sub Button2_Click(...) Handles Button2.Click PrintPreviewDialog1.Document = PrintDocument1 If PrintPreviewDialog1.ShowDialog() = DialogResult.OK Then ' das Dokument soll gedruckt werden PrintDocument1.Print() End If End Sub
Der Seitenzähler im Seitenvorschaufenster orientiert sich leider nicht an den tatsächlichen Seitenzahlen. Wenn Sie also die Seiten 4 bis 7 eines Dokuments ausdrucken, dann zeigt der Seitenzähler die Zahl 1 für die Seite 4, 2 für die Seite 5 etc. an. Generell ist die Bedienung des Vorschaudialogs wenig intuitiv: Ein Blättern durch das Dokument per Tastatur gelingt nur manchmal. Und im Steuerfeld für den Seitenzähler müssen Sie den Pfeil nach oben anklicken, um eine Seite nach unten zu blättern! Eine einfache Bildlaufleiste zur Auswahl der aktuellen Seite wäre wohl vernünftiger gewesen – aber ein paar Verbesserungsmöglichkeiten muss es ja auch für VB.NET 2 geben (oder wie immer die zweite VB.NET-Version heißen wird).
Interna Intern funktioniert die Druckvorschau so, dass durch PrintPreviewDialog1.ShowDialog die PrintDocument-Ereignisprozeduren aufgerufen werden, als würde tatsächlich ein Ausdruck durchgeführt. (Deswegen ist dafür kein zusätzlicher Code erforderlich.) Allerdings werden die durchgeführten Ausgaben nicht an den Drucker weitergegeben, sondern gespeichert und im Druckvorschaudialog angezeigt. Für die Vorschau werden die PrintDocument-Ereignisprozeduren für das gesamte Dokument aufgerufen, bevor die erste Seite im Vorschaudialog sichtbar wird. Das hat zwei Konsequenzen: Erstens dauert es bei umfangreichen Dokumenten, die viele Seiten umfassen, ziemlich lange, bis die erste Seite endlich angezeigt wird. (Immerhin können Sie den Ausdruck abbrechen – dann werden nur die ersten Seiten angezeigt.) Und zweitens kann der Speicherbedarf für die Seitenansicht ziemlich groß werden, weil alle zu druckenden Seiten im Arbeitsspeicher gehalten werden, so lange der Dialog sichtbar ist.
17.2.4 Druckereigenschaften und Seitenlayout Egal, ob Sie dem Anwender die Möglichkeit geben, Druckparameter und Seitenlayout einzustellen oder nicht, müssen Sie auf jeden Fall bei der Durchführung des Ausdrucks in den PrintDocument-Ereignisprozeduren die Papiergröße ermitteln (und gegebenenfalls weitere Parameter auswerten). Dieser Abschnitt gibt einen Überblick über die wichtigsten Objekte, die Sie dabei berücksichtigen müssen. Die Syntaxzusammenfassung im nächsten Abschnitt gibt darüber hinaus noch eine Menge Details zu den Eigenschaften dieser Objekte.
HINWEIS
17.2 Grundlagen
971
Dieser Abschnitt geht davon aus, dass Sie zum Drucken ein PrintDocument-Steuerelement verwenden. Zusammen mit diesem Steuerelement stehen in Ihrem Programm dann automatisch auch Objekte der Klassen PrinterSettings und PageSettings zur Verfügung. Sie können auf diese Objekte entweder innerhalb der PrintDocumentEreignisprozeduren, oder direkt via PrintDocument1.PrinterSettings bzw. DefaultPageSettings zugreifen.
PrintPageEventArgs-Klasse (PrintPage-Ereignisprozedur) An die PrintPage-Ereignisprozeduren des PrintDocument-Steuerelements wird ein Objekt e der Klasse System.Drawing.Printing.PrintPageEventArgs übergeben. Über dieses Objekt können Sie auf alle weiteren in diesem Abschnitt beschriebenen Objekte zugreifen.
Koordinatensystem Das Koordinatensystem wird durch das Graphics-Objekt vorgegeben, auf das Sie mit e.Graphics zugreifen. Per Default verwendet es die Maßeinheit Display (siehe auch Abschnitt 16.2.5). Dabei entspricht eine Einheit 1/100 Zoll (0,254 mm). Der Punkt (0,0) bezeichnet das linke obere Eck der Seite. Die x-Achse zeigt nach rechts, die y-Achse nach unten. (Das gilt auch dann, wenn die Seite im Querformat gedruckt wird! In diesem Fall wird das Koordinatensystem automatisch relativ zur Seite gedreht, Sie brauchen sich nicht darum zu kümmern.) Grundsätzlich können Sie natürlich auch ein anderes Koordinatensystem verwenden (z.B. mit gr.PageUnit = GraphicsUnit.Millimeter). Da aber sämtliche Eigenschaften, die Maßangaben für Druckerobjekte enthalten, die Einheit 1/100 Zoll verwenden, handeln Sie sich damit eine Menge Einheitsumrechnungen (und ein entsprechend hohes Fehlerpotenzial) ein.
Seitengröße, Seitenränder e.PageBounds und e.MarginBounds verweisen jeweils auf ein Rectangle-Objekt, das den Koor-
dinatenbereich der gesamten Seite bzw. der um einen Rand verkleinerten Seite angeben. Die Angaben erfolgen im selben Koordinatensystem wie beim Graphics-Objekt (also in 1/100 Zoll). Das PageBounds-Rechteck beginnt mit (0,0) und endet bei (x,y), wobei dieser Punkt exakt das Ende der Seite beschreibt. Allerdings gibt es keinen Drucker, der das gesamte Papier ohne Ränder bedrucken kann. Wie groß die Seitenränder tatsächlich sind, hängt vom jeweiligen Drucker ab. Diese Information kann mit .NET-Methoden aber leider nicht ermittelt werden. Aus diesem Grund ist das MarginBounds-Rechteck per Default ein an allen vier Rändern um 100 Einheiten (2,54 cm) verkleinertes Rechteck. Dieses Rechteck reicht daher unabhängig vom Drucker und unabhängig vom tatsächlich bedruckbaren Raum von (100,100) bis (x-100, y-100). Diese Defaulteinstellung ist ziemlich unbefriedigend, weil unnötig viel Platz
972
17 Drucken
vergeudet wird. Ein realistischer Seitenrand beträgt etwa 0,5 bis 1 cm links und rechts und etwa 1 bis 1,5 cm oben und unten. Grundsätzlich ist MarginBounds nur eine Richtlinie! Wenn Sie möchten, können Sie Ihre Ausgabe auch in den Grenzen des PageBounds-Rechtecks durchführen – aber dann müssen Sie damit rechnen, dass der Ausdruck an den Rändern abgeschnitten wird. Die beste Lösung besteht wohl darin, beim Programmstart realistische Vorgaben für den Seitenrand vorzugeben (siehe oben) und dem Anwender dann die Möglichkeit zu geben, die Seitenränder bei Bedarf selbst zu verändern (mit dem PageSetupDialog-Steuerelement). Zur Veränderung der Defaultseitenränder können Sie sich an dem folgenden Code orientieren. Darin wird das Margins-Objekt des PageSettings-Objekts des PrintDocument-Steuerelements verändert. Auch die Eigenschaften des Margins-Objekt erwarten die Angaben in 1/100 Zoll, weswegen die cm-Angaben durch 0,0254 dividiert werden. Private Sub Form1_Load(...) Handles MyBase.Load ' Default-Seitenränder etwas verkleinern: ' links/rechts: 1 cm ' oben/unten: 1,5 cm With PrintDocument1.DefaultPageSettings.Margins ' Angaben in 1/100 Zoll; hier Umrechnung cm->Zoll .Left = CInt(1 / 0.0254) .Right = CInt(1 / 0.0254) .Top = CInt(1.5 / 0.0254) .Bottom = CInt(1.5 / 0.0254) End With End Sub
Seiteneigenschaften (PageSettings) Das PageSettings-Objekt enthält diverse Informationen, die für den Seitenaufbau relevant sind: Landscape (Papierausrichtung), PrinterResolution (Druckerauflösung), Bounds (Papiergröße in 1/100 Zoll), Margins (Seitenränder in 1/100 Zoll). Eine ausführlichere Beschreibung der Eigenschaften folgt in der Syntaxzusammenfassung im nächsten Abschnitt. Aus Bounds und Margins können dieselben Informationen errechnet werden, die mit den Eigenschaften e.PageBounds und e.MarginBounds an die PrintPage-Ereignisprozedur übergeben werden. Es gibt verschiedene Wege, um auf ein PageSettings-Objekt zuzugreifen: •
In der PrintPage-Ereignisprozedur: e.PageSettings
•
Von einem PrintDocument-Steuerelement: PrintDocument1.DefaultPageSettings
•
Von einem PrinterSettings-Objekt: prs.DefaultPageSettings
VORSICHT
17.2 Grundlagen
973
Vom PrintDocument-Objekt können Sie zwei PageSettings-Objekte ansprechen! Es handelt sich dabei um unterschiedliche Objekte, die nicht denselben Inhalt aufweisen! • printdocument.PrinterSettings.DefaultPageSettings verweist auf die Defaulteinstellungen des Druckers, der durch PrinterSettings momentan ausgewählt ist. • printdocument1.DefaultPageSettings verweist auf die für den Ausdruck relevanten Einstellungen. Wenn Sie Parameter für den Ausdruck verändern möchten, dann müssen Sie die Eigenschaften dieses PageSettings-Objekts verändern!
Druckereinstellung (PrinterSettings) Das PrinterSettings-Objekt beschreibt diverse Druckereigenschaften, z.B. PrinterName (Name des Druckers) und PrinterResolutions (vom Drucker unterstützte Auflösungen). Darüber hinaus enthält das Objekt Informationen über die Angaben, die im Druckdialog vorgenommen wurden, beispielsweise PrintRage (was soll gedruckt werden) sowie From- und ToPage (Seitenbereich). Mit InstalledPrinters können Sie alle am Rechner verfügbaren Drucker ermitteln. (Details zu den PrinterSettings-Eigenschaften finden Sie wieder in der Syntaxzusammenfassung im nächsten Abschnitt.) Abermals gibt es verschiedene Wege, die zum PrinterSettings-Objekt führen: •
In der PrintPage-Ereignisprozedur: e.PageSettings.PrinterSettings
•
Von einem PrintDocument-Steuerelement: PrintDocument1.PrinterSettings
•
Von einem PageSettings-Objekt: pgs.PrinterSettings
Anwendung der PrinterSettings- und PageSettings-Objekte Das übliche Anwendungsszenario sieht so aus, dass Sie in Ihrem Programm zumindest die Steuerelemente PrintDocument und PrintDialog verwenden, optional auch PageSetupDialog und PrintPreviewDialog. In diesem Fall ist eine direkte Änderung der Eigenschaften PrinterSettings- und PageSettings-Objekte nicht erforderlich. Erst wenn tatsächlich ein Dokument ausgedruckt werden soll, werten Sie die Eigenschaften aus (also in den Ereignisprozeduren des PrintDocument-Steuerelements). Diese Vorgehensweise ist in den Beispielen in Abschnitt 17.3 demonstriert. Eine alternative Vorgehensweise besteht darin, dass Sie auf die gerade aufgezählten Steuerelemente verzichten und deren Funktionen selbst nachbilden (etwa durch eigene Dialoge). In diesem Fall müssen Sie die Eigenschaften der Page- und PrinterSettings-Objekte selbst verändern (siehe Abschnitt 17.5).
974
17 Drucken
17.2.5 Syntaxzusammenfassung Steuerelemente System.Drawing.Printing.PrintDocument-Klasse Print()
startet den Ausdruck und führt zu den Ereignissen BeginPrint, PrintPage (einmal für jede Seite) und EndPrint.
DefaultPageSettings
verweist auf ein PageSettings-Objekt.
DocumentName
gibt den Dokumentnamen an.
PrinterSettings
verweist auf ein PrinterSettings-Objekt.
System.Windows.Forms.PrintDialog-Klasse ShowDialog()
zeigt den Dialog zur Druckerauswahl und zur Angabe des Druckbereichs an. Liefert DialogResult.OK, wenn der Dialog mit OK beendet wurde.
Document
verweist auf das dazugehörende PrintDocument-Objekt.
AllowPrintFile
Option anzeigen, mit der der Ausdruck in eine Datei umgeleitet werden kann.
AllowSelection
Option anzeigen, um nur die Markierung zu drucken.
AllowSomePages
Option anzeigen, um einen bestimmten Seitenbereich auszudrucken.
System.Windows.Forms.PageSetupDialog-Klasse ShowDialog()
zeigt den Dialog zur Einstellung des Seitenlayouts an.
Document
verweist auf das dazugehörende PrintDocument-Objekt.
AllowMargins
Textfelder zur Spezifikation der Seitenränder anzeigen.
AllowOrientation
Optionen zur Auswahl der Orientierung (Hoch-/Querformat) anzeigen.
AllowPaper
Listenfeld zur Auswahl der Papiergröße anzeigen.
AllowPrinter
Button zur Auswahl des Druckers anzeigen.
MinMargins
verweist auf ein Margin-Objekt mit den minimalen Werten für die Seitenränder.
System.Windows.Forms.PrintPreviewDialog-Klasse ShowDialog()
zeigt ein Fenster zur Druckvorschau (Seitenansicht) an.
Document
verweist auf das dazugehörende PrintDocument-Objekt.
UseAntiAlias
bewirkt eine geglättete Darstellung der Vorschau.
17.2 Grundlagen
975
Häufig benötigte Klassen des System.Drawing.Printing-Namensraums System.Drawing.Printing.PrintPageEventArgs-Klasse Cancel
bewirkt einen Abbruch des Druckvorgangs, wenn es auf True gesetzt wird.
Graphics
verweist auf ein Graphics-Objekt, das zur Ausgabe der aktuellen Seite verwendet wird.
HasMorePages
bewirkt einen neuerlichen Aufruf der PrintPage-Prozedur zum Ausdruck von weiteren Seiten, wenn es auf True gesetzt wird.
MarginBounds
verweist auf ein Rectangle-Objekt, das den bedruckbaren Bereich der Seite angibt.
PageBounds
verweist auf ein Rectangle-Objekt, das die gesamte Seite angibt.
PageSettings
verweist auf ein PageSetting-Objekt, das detaillierte Informationen über das Seitenlayout gibt.
System.Drawing.Printing.PageSettings-Klasse Bounds
verweist auf ein Rectangle-Objekt, das den Koordinatenbereich der gesamten Seite (ohne Berücksichtigung der Seitenränder) in der Einheit 1/100 Zoll angibt.
Color
gibt an, ob in Farbe gedruckt werden kann (True/False). Diese Eigenschaft enthält allerdings auch bei vielen Schwarzweiß-Druckern True.
Landscape
gibt an, ob der Druck im Querformat erfolgt (True/False).
Margins
verweist auf ein gleichnamiges Objekt, das die aktuelle Einstellung der Seitenränder enthält (Left, Right, Top, Bottom). Alle Werte sind in 1/100 Zoll.
PaperSize
verweist auf ein gleichnamiges Objekt, das Informationen über das Seitenformat (Letter, A4 etc. gibt).
PaperSource
verweist auf ein gleichnamiges Objekt, das angibt, aus welchem Einzug das Papier entnommen wird.
PrinterResolution
verweist auf ein gleichnamiges Objekt, dessen Eigenschaften X und Y die Druckauflösung in DPI (dots per inch) angeben.
PrinterSettings
verweist auf ein PrinterSettings-Objekt (siehe nächste Box).
976
17 Drucken
System.Drawing.Printing.PrinterSettings-Klasse CanDuplex
gibt an, ob der Drucker einen beidseitigen Druck prinzipiell unterstützt (True/False).
Collate
gibt an, in welcher Reihenfolge die Seiten bei einem Mehrfachdruck gedruckt werden soll. True bedeutet, dass das komplette Dokument mehrmals hintereinander ausgedruckt werden soll. False bedeutet, dass jede einzelne Seite mehrfach ausgedruckt werden soll. (Das ist im Regelfall schneller, allerdings muss der Anwender danach die Seiten selbst ordnen.)
Copies
gibt an, wie viele Kopien ausgedruckt werden sollen.
DefaultPageSettings
verweist auf ein PageSettings-Objekt (siehe den vorigen Kasten).
Duplex
gibt an, ob der Ausdruck beidseitig erfolgen soll (True/False).
FromPage, ToPage
gibt an, welcher Seitenbereich ausgedruckt werden soll (nur wenn PrintRange=SomePages).
InstalledPrinters
liefert ein Feld mit allen am System verfügbaren Druckern.
IsDefaultPrinter
gibt an, ob der aktuelle Drucker der Defauldrucker des Systems ist (True/False).
PaperSizes
liefert eine Aufzählung mit allen bekannten Papiergrößen.
PrinterName
gibt den Namen des Druckers an.
PrinterResolutions
liefert eine Aufzählung mit den vom Drucker unterstützten Auflösungen.
PrintRange
gibt an, ob das gesamte Dokument (AllPages), ein angegebener Seitenbereich (SomePages) oder der markierte Bereich des Dokuments ausgedruckt werden soll (Selection). Die möglichen Einstellungen sind Elemente der PrintRange-Aufzählung.
PrintToFile
gibt an, ob der Ausdruck in eine Datei umgeleitet werden soll (True/False).
System.Drawing.Printing.Margins-Klasse Left, Right
linker und rechter Seitenrand (in 1/100 Zoll).
Top, Bottom
oberer und unterer Seitenrand.
17.3 Beispiel – Mehrseitiger Druck
17.3
977
Beispiel – Mehrseitiger Druck
Ziel dieses Beispiels ist es, das Zusammenspiel der verschiedenen Steuerelemente zu demonstrieren. Bei dem auszudruckenden Dokument handelt es sich um zehn Seiten, die ähnlich wie beim Einführungsbeispiel dieses Buchs (siehe Abbildung 17.2) aufgebaut sind: Zwei Ellipsen und Rechtecke veranschaulichen die Größe von PageBounds und MarginBounds. Innerhalb des PageBounds-Rechtecks werden einige Informationen über diverse Druckobjekte angezeigt. Außerdem wird jede Seite mit einer großen Seitennummer ausgestattet. Nun zu den besonderen Merkmalen des Programms: •
Mit dem Button SEITE EINRICHTEN können Sie das Seitenlayout verändern (Hoch-/Querformat, Seitenränder).
•
Mit dem Button DRUCKVORSCHAU können Sie sich den Ausdruck papiersparend im Voraus ansehen.
•
Mit dem Button DRUCKEN können Sie einen Drucker sowie den gewünschten Seitenbereich angeben (z.B. um die Seiten vier bis sieben auszudrucken).
Abbildung 17.6: Vorschau eines mehrseitigen Dokuments
978
17 Drucken
Initialisierung Im Programmcode werden eine Klassenvariable und eine Klassenkonstante definiert: pageNr enthält die aktuelle Seitenzahl, maxPageNr die maximale Seitenanzahl für das gesamte Dokument. (Sie werden auf diese beiden Bezeichner in den PrintDocument1-Ereignisprozeduren stoßen.) ' Beispiel drucken\multipage Dim pageNr As Integer = -1 Const maxPageNr As Integer = 10
In Form1_Load wird die Verbindung zwischen PrintDocument1 und den drei Druckdialogen hergestellt. (Das hätte natürlich auch schon im Eigenschaftsfenster erfolgen können. Die gewählte Vorgehensweise ist aber leichter nachvollziehbar.) Außerdem werden die Defaultseitengrenzen von 2,54 cm auf etwas sinnvollere Maße reduziert (1 bis 1,5 cm). Private Sub Form1_Load(...) Handles MyBase.Load With PrintDocument1.DefaultPageSettings.Margins ' Angaben in 1/100 Zoll; hier Umrechnung cm->Zoll .Left = CInt(1 / 0.0254) .Right = CInt(1 / 0.0254) .Top = CInt(1.5 / 0.0254) .Bottom = CInt(1.5 / 0.0254) End With PageSetupDialog1.Document = PrintDocument1 PrintDialog1.Document = PrintDocument1 PrintPreviewDialog1.Document = PrintDocument1 End Sub
Button-Ereignisprozeduren Die drei Ereignisprozeduren ButtonX_Click enthalten keine Neuerungen gegenüber den Erklärungen im vorigen Abschnitt. ' Seite einrichten Private Sub Button1_Click(...) Handles Button1.Click ... Korrektur der metrischen Umrechnung: ... siehe Code in Abschnitt 17.2.2 PageSetupDialog1.ShowDialog() End Sub ' Vorschau Private Sub Button2_Click(...) Handles Button2.Click If PrintPreviewDialog1.ShowDialog() = DialogResult.OK Then PrintDocument1.Print() End If End Sub
17.3 Beispiel – Mehrseitiger Druck
979
' Drucken Private Sub Button3_Click(...) Handles Button3.Click PrintDialog1.AllowSomePages = True PrintDialog1.AllowSelection = False If PrintDialog1.ShowDialog() = DialogResult.OK Then PrintDocument1.Print() End If End Sub
Ausdruck beginnen Schon deutlich interessanter sind die PrintDocument-Ereignisprozeduren: Sie demonstrieren, wie die Verwaltung der Seitennummer bei einem mehrseitigen Druck erfolgen kann. In der BeginPrint-Prozedur wird die Startseite eingestellt. Wenn das gesamte Dokument gedruckt werden soll, ist das natürlich 1. Andernfalls wird die Startseite der Eigenschaft FromPage des PrinterSettings-Objekt entnommen. ' am Beginn des Druckvorgangs Private Sub PrintDocument1_BeginPrint(...) _ Handles PrintDocument1.BeginPrint With PrintDocument1.PrinterSettings If .PrintRange = Printing.PrintRange.SomePages Then pageNr = .FromPage Else pageNr = 1 End If End With End Sub
Eine Seite drucken Die PrintPage-Ereignisprozedur beginnt mit diversen Grafikkommandos, mit denen die zu druckende Seite aufgebaut wird. Welche Nummer die aktuelle Seite hat, geht aus der Variablen pageNr hervor, die gerade vorhin initialisiert wurde. Am Ende der Prozedur wird die pageNr um eins vergrößert. Falls dabei die letzte Seite erreicht wird, wird e.HasMorePages auf False gesetzt, andernfalls auf True. Das führt etwas später zu einem neuerlichen (automatischen) Aufruf der Prozedur. ' eine Seite drucken Private Sub PrintDocument1_PrintPage( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles PrintDocument1.PrintPage
980
Dim Dim Dim Dim Dim Dim Dim Dim Dim
17 Drucken
gr As Graphics = e.Graphics ps As Printing.PageSettings = e.PageSettings pagerect As Rectangle = e.PageBounds printrect As Rectangle = e.MarginBounds printrectf As RectangleF = RectangleF.op_Implicit(printrect) fnt1 As New Font("arial", 250, FontStyle.Bold) fnt2 As New Font("arial", 12) sf As New StringFormat() s As String
' Seite ausgeben; pagenr enthält die aktuelle Seitennummer sf.Alignment = StringAlignment.Center sf.LineAlignment = StringAlignment.Center gr.DrawRectangle(Pens.Black, pagerect) gr.DrawEllipse(Pens.Gray, pagerect) gr.DrawRectangle(Pens.Black, printrect) gr.DrawEllipse(Pens.Gray, printrect) gr.DrawString(pageNr.ToString, fnt1, Brushes.LightBlue, _ printrectf, sf) s = ps.ToString + vbCrLf + _ pagerect.ToString + vbCrLf + _ printrect.ToString gr.DrawString(s, fnt2, Brushes.Black, printrectf) ' nicht mehr benötigte Grafik-Objekte löschen fnt1.Dispose() fnt2.Dispose() sf.Dispose() ' Seitennummer erhöhen, Ausdruck gegebenenfalls abbrechen pageNr += 1 With ps.PrinterSettings Dim myMaxPageNr As Integer 'maximale Seitennummer myMaxPageNr = maxPageNr If .PrintRange = Printing.PrintRange.SomePages Then If myMaxPageNr > .ToPage Then myMaxPageNr = .ToPage End If End If If pageNr > myMaxPageNr Then e.HasMorePages = False Else e.HasMorePages = True End If End With End Sub
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
981
Ausdruck abschließen Die EndPrint-Prozedur ist eigentlich optional. Die Zuweisung pageNr=-1 bewirkt, dass alle anderen Teile des Programms herausfinden können, ob gerade ein Ausdruck stattfindet. ' Ausdruck abschließen Private Sub PrintDocument1_EndPrint(...) _ Handles PrintDocument1.EndPrint pageNr = -1 End Sub
17.4
Beispiel – Inhalt eines Textfelds ausdrucken
Dieses zweite, deutlich längere Beispielprogramm hat im Prinzip ähnliche Eigenschaften wie das vorige Beispielprogramm: Im Unterschied zum ersten Beispiel ist das Programm aber deutlich praxisnäher: statt einiger Pseudoinhalte kann nun ein beliebiger Text ausgedruckt werden. Die folgenden Punkte fassen die wichtigsten Eigenschaften des Programms zusammen: •
Mit DATEI|DATEI LADEN können Sie eine beliebige ANSI- oder Unicode-Datei (UTF8) in das Textfeld laden.
•
Mit DATEI|SEITE EINRICHTEN können Sie zwischen Hoch- und Querformat wählen und die Seitenränder verändern.
•
DATEI|SEITENANSICHT führt zum Druckvorschaudialog.
•
DATEI|DRUCKEN druckt den Text aus, wobei der Seitenbereich (z.B. Seite 3-5) angegeben
werden kann. •
Mit EXTRAS|SCHRIFTART können Sie die Schriftart und -größe des Textfelds einstellen. Diese Schriftart wird auch für den Ausdruck verwendet.
•
Mit EXTRAS|SEITENANSICHT MIT GEGLÄTTETEN SCHRIFTEN können Sie für die Druckvorschau Anti-Aliasing aktivieren.
•
Beim Ausdruck wird am Beginn jeder Seite die Seitennummer und der Dateiname sowie vor jeder Zeile die Zeilennummer angegeben.
•
Beim Ausdruck werden zu lange Zeilen nicht einfach abgeschnitten, sondern auf mehrere Zeilen verteilt. Dabei wird der Text etwas mehr eingerückt als die erste Zeile. Gerade der Ausdruck von Programmlistings wird dadurch deutlich leserlicher. (Der Effekt ist in Abbildung 17.7 zu sehen.) Wenn die auf mehrere Zeilen verlängerte Zeile auf der aktuellen Seite nicht mehr Platz hat, wird sie vollständig auf der nächsten Seite gedruckt.
•
Der Ausdruck funktioniert auch dann, wenn der Text Tabulatorzeichen enthält.
982
17 Drucken
Abbildung 17.7: Textdatei ausdrucken
17.4.1 Codegerüst Der gesamte Programmcode befindet sich in Form1.vb. Der Code setzt sich aus folgenden Teilen zusammen: •
von der Entwicklungsumgebung generierter Code (Vom Windows Form Designer generierter Code)
•
Definition einiger Variablen auf Formularklassenebene
•
Initialisierungsarbeiten in Form1_Load
•
Diverse Menü-Ereignisprozeduren zum Laden einer Datei und zum Drucken
•
PrintDocument1-Ereignisprozeduren zum Textausdruck
•
Hilfsfunktionen zum Textausdruck: PrintHeadLine druckt die Kopfzeile zu jeder Seite. PrintOnePage druckt eine Seite aus. WrapLine führt den Umbruch für zu lange Zeilen durch. UnTabify ersetzt innerhalb einer Zeile Tabulator- durch Leerzeichen.
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
983
Im Folgenden werden aus Platzgründen nur die für den Textausdruck relevanten Codeteile beschrieben.
Definition einiger Variablen auf Formularklassenebene Die folgenden Variablen werden in den DocumentPrint-Ereignisprozeduren verwendet. initPrint gibt an, ob die Variablen für den Textausdruck bereits initialisiert wurden. (Um Zeit zu sparen, wird diese Initialisierung nur beim Ausdruck der ersten Seite durchgeführt. Anschließend wird initPrint auf True gesetzt.) headLines gibt an, wie viele Textzeilen für den Seitenkopf reserviert sind. filename enthält schließlich den Dateinamen der zuletzt geladenen Datei. ' Beispiel drucken\printtext Dim initPrint As Boolean = False Const headLines As Integer = 2 'Platz für Kopfzeilen Dim filename As String
Form1_Load Diese Prozedur wird während des Programmstarts ausgeführt. Darin wird versucht, die Datei ..\Form1.vb zu laden, also den aktuellen Code. Das ist vor allem zum Testen des Programms praktisch. Wenn die Datei nicht gefunden wird, wird das Programm ohne Fehlermeldung fortgesetzt. Darüber hinaus wird in Form1_Load die Verbindung zwischen den Steuerelementen PrintDocument1 sowie den Dialogen PageSetupDialog1, PrintDialog1 und PrintPreviewDialog1 hergestellt. Außerdem werden die Defaultseitenränder auf 1 bzw. 1,5 cm reduziert.
17.4.2 PrintDocument-Ereignisprozeduren Während beim vorigen Beispielprogramm die BeginPrint-Ereignisprozedur dazu verwendet wurde, diverse Initialisierungsarbeiten durchzuführen, enthält diese Ereignisprozedur diesmal nur die Anweisung initPrint=False. Die eigentliche Initialisierung kann erst in der PrintPage-Ereignisprozedur durchgeführt werden, weil erst dort die erforderlichen Objekte (z.B. das Graphics-Objekt für den Ausdruck) zur Verfügung stehen. Der Rest dieses Abschnitts beschäftigt sich daher mit der PrintPage-Ereignisprozedur.
PrintPage-Ereignisprozedur Diese Prozedur besteht im Wesentlichen aus drei Teilen: •
Der erste Teil ist für die Initialisierung einer ganzen Reihe von Variablen verantwortlich. Dieser Teil wird nur bei der ersten Seite durchgeführt. Die Ergebnisse werden in statischen Variablen gespeichert und stehen daher auch bei den folgenden Aufrufen noch zur Verfügung.
984
17 Drucken
•
Im zweiten Teil wird jeweils eine Seite gedruckt. Da sich der eigentliche Code zum Drucken in den Hilfsprozeduren PrintHeadLine und PrintOnePage befindet, fällt dieser Teil recht kurz aus.
•
Im letzten Teil erfolgt ein Test, ob noch weitere Seiten zu drucken sind. Entsprechend wird e.HasMorePages auf True bzw. auf False gesetzt.
Variablendeklarationen Private Sub PrintDocument1_PrintPage( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles PrintDocument1.PrintPage Dim gr As Graphics = e.Graphics Dim printrectf As RectangleF = _ RectangleF.op_Implicit(e.MarginBounds) Dim fnt As Font = TextBox1.Font Dim s As String Static linesPerPage As Integer Static columnsPerLine As Integer Static pageNr As Integer Static maxPageNr As Integer Static lastPageNr As Integer Static lineNr As Integer Static txtLines() As String
'Zeilen pro Seite 'Spalten pro Zeile 'aktuelle Seitennummer (1 für 'die erste Seite) 'Anzahl der Seiten des gesamten 'Ausdrucks 'letzte zu druckenden Seite 'aktuelle Zeilennummer (0 für 'die erste Zeile) 'enthält zeilenweise den zu 'druckenden Text
Teil 1 – Initialisierung Der folgende Codeblock wird nur ausgeführt, wenn initPrint=False ist. Diese Variable wird am Ende des Blocks auf True gesetzt. Das Ziel des Codeblocks ist es, •
die Textdatei im String-Feld txtLines zu speichern,
•
die Anzahl der Textzeilen pro Seite und der Spalten pro Zeile zu berechnen,
•
die gesamte Seitenanzahl des Dokuments zu ermitteln,
•
die Nummer der Start- und Endseite zu ermitteln (unter Berücksichtigung der maximalen Seitenanzahl und der Benutzerangaben im PrintDialog) sowie
•
die Zeilennummer der ersten Zeile auf der ersten zu druckenden Seite festzustellen.
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
985
Der Code zu den beiden ersten Punkten sollte auf Anhieb verständlich sein. Die LinesEigenschaft des TextBox-Steuerelements liefert ein String-Feld mit den einzelnen Zeilen des Texts. (Wenn Sie eine externe Datei drucken möchten, wäre es natürlich auch kein Problem, die Datei zeilenweise einzulesen.) Zur Berechnung der Spaltenanzahl muss ein StringFormat.GenericTypographics-Objekt verwendet werden. Damit erreichen Sie, dass MeasureString ein möglichst exaktes Ergebnis liefert (und nicht zusätzlichen Spielraum für Kursivierung etc. lässt). Die Hintergründe zur Methode MeasureString und zur Klasse StringFormat finden Sie in Abschnitt 16.3.5. ' Fortsetzung von PrintDocument1_PrintPage If initPrint = False Then ' Textdatei ist via txtLines(n) zeilenweise zugänglich txtLines = TextBox1.Lines ' Anzahl der Textzeilen pro Seite und der Spalten pro Zeile Dim sizf As SizeF sizf = gr.MeasureString("W", fnt) ' Int rundet ab, liefert aber Double; CInt wandelt in Integer um linesPerPage = CInt(Int(printrectf.Height / sizf.Height)) - _ headLines sizf = gr.MeasureString("W", fnt, 0, _ StringFormat.GenericTypographic) columnsPerLine = CInt(Int(printrectf.Width / sizf.Width))
Der dritte Punkt der obigen Aufzählung bereitet die größten Probleme. Der Grund dafür liegt im Zeilenumbruch. Da lange Zeilen beim Ausdruck auf mehrere Zeilen verteilt werden, gilt die einfache Rechnung seitenanzahl = zeilenanzahl / zeilen_pro_seite nicht! Der einzige Weg, die tatsächliche Seitenanzahl auszurechnen, besteht daher darin, den Ausdruck zu simulieren. Aus diesem Grund ist die Hilfsfunktion PrintOnePage so ausgelegt, dass die Seite wahlweise gedruckt oder nur der Platzbedarf ermittelt wird (Parameter reallyPrint = True / False). PrintOnePage führt also einen (simulierten) Ausdruck einer Seite durch. Als Ergebnis liefert
die Funktion die Nummer der Zeile, mit der die nächste Seite beginnt. Diese Zeilennummer wird zur späteren Verwendung in einem ArrayList-Objekt gespeichert. Das eigentliche Ergebnis der folgenden Zeilen ist aber der Wert in maxPageNr. ' Fortsetzung von PrintDocument1_PrintPage ' gesamte Seitenanzahl ermitteln ' gleichzeitig die Nummer der ersten Zeile jeder Seite speichern Dim firstLineOfPage As New Collections.ArrayList() lineNr = 0 pageNr = 0 firstLineOfPage.Insert(0, 0) 'ArrayList besteht darauf, dass es 'zuerst ein 0-Element gibt, bevor 'ein 1-Element eingefügt werden darf
986
17 Drucken
Do pageNr += 1 firstLineOfPage.Insert(pageNr, lineNr) lineNr = PrintOnePage(gr, fnt, printrectf, txtLines, lineNr, _ linesPerPage, columnsPerLine, False) Loop Until lineNr >= txtLines.GetUpperBound(0) maxPageNr = pageNr
In den folgenden Zeilen geht es darum, in pageNr die Nummer der ersten zu druckenden Seite und in lastPageNr die der letzten Seite zu speichern. Wenn das gesamte Dokument gedruckt werden soll, dann werden hierfür die Werte 1 und maxPageNr verwendet. Wenn dagegen im PrintDialog Seitennummern für den auszudruckenden Bereich angegeben worden sind, dann werden diese Werte verwendet (wobei maxPageNr die obere Grenze bestimmt). Bevor nun der Ausdruck beginnen kann, muss das Programm noch wissen, mit welcher Zeile die Startseite beginnt. Diese Information kann einfach aus dem gerade initialisierten ArrayList-Objekt firstLineOfPage ermittelt werden. (Die einzelnen Elemente dieser Aufzählung haben den Datentyp Object, weswegen mit CInt eine Umwandlung in einen IntegerWert erfolgen muss.) ' Fortsetzung von PrintDocument1_PrintPage If e.PageSettings.PrinterSettings.PrintRange = _ Printing.PrintRange.SomePages Then pageNr = e.PageSettings.PrinterSettings.FromPage lastPageNr = e.PageSettings.PrinterSettings.ToPage If lastPageNr > maxPageNr Then lastPageNr = maxPageNr If pageNr > maxPageNr Then pageNr = maxPageNr lineNr = CInt(firstLineOfPage(pageNr)) Else pageNr = 1 lastPageNr = maxPageNr lineNr = 0 End If ' vermeiden, dass dieser Code nochmals ausgeführt wird initPrint = True End If
Teil 2 – Eine Seite drucken Der Code, um die aktuelle Seite auszudrucken, ist kurz. Der Grund besteht darin, dass die eigentliche Arbeit von den Hilfsprozeduren PrintHeadLine und PrintOnePage erledigt wird, die im nächsten Abschnitt beschrieben werden. Beachten Sie, dass der letzte Parameter von PrintOnePage nun True und nicht mehr False lautet – diese Seite soll also wirklich gedruckt werden. Die Funktion liefert die erste Zeilennummer für die nächste Seite zurück. Da lineNr eine statische Variable ist, steht der Wert auch beim nächsten Aufruf von PrintDocument1_PrintPage zur Verfügung.
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
987
' Fortsetzung von PrintDocument1_PrintPage PrintHeadLine(gr, fnt, printrectf, pageNr, maxPageNr) lineNr = PrintOnePage(gr, fnt, printrectf, txtLines, _ lineNr, linesPerPage, columnsPerLine, True)
Teil 3 – Weitere Seiten drucken Abschließend wird pageNr um eins erhöht. Je nachdem, ob damit lastPageNr erreicht wird, wird der Ausdruck nun mit HasMorePages = False abgeschlossen. In diesem Fall wird auch das statische String-Feld txtLines mit dem gesamten Text gelöscht. (Der Text steht ja weiter im Textfeld zur Verfügung.) ' Ende von PrintDocument1_PrintPage pageNr += 1 If pageNr <= lastPageNr Then e.HasMorePages = True Else e.HasMorePages = False Erase txtLines End If End Sub
17.4.3 Hilfsfunktionen Kopfzeile drucken (PrintHeadLine) PrintHeadLine gibt im linken oberen Eck die Seitennummer sowie die Gesamtseitenanzahl aus, im rechten oberen Eck den Dateinamen. Darunter wird eine Linie gezeichnet. Die einzige Besonderheit besteht darin, dass zur Ausgabe ein StringFormat-Objekt mit Trimming = EllipsisPath verwendet wird, um einen sehr langen Dateinamen gegebenenfalls so weit zu verkürzen, dass er in der Kopfzeile Platz hat (siehe auch Abschnitt 16.3.5). Private Sub PrintHeadLine(ByVal gr As Graphics, _ ByVal fnt As Font, ByVal printrectf As RectangleF, _ ByVal pageNr As Integer, ByVal maxPageNr As Integer) ' für die Kopfzeile sind die ersten zwei Zeilen vorgesehen; ' siehe CalcLinesPerPage Dim s As String Dim y As Single Dim sizf As SizeF Dim rectf1 As RectangleF Dim boldfnt As New Font(fnt, FontStyle.Bold) Dim sf As New StringFormat(StringFormatFlags.NoWrap)
988
17 Drucken
' links Seitennummer s = "Seite " & pageNr & " / " & maxPageNr sizf = gr.MeasureString(s, boldfnt) gr.DrawString(s, boldfnt, Brushes.Black, _ printrectf.X, printrectf.Y) ' rechts Dateiname s = filename sf.Alignment = StringAlignment.Far sf.Trimming = StringTrimming.EllipsisPath rectf1 = New RectangleF( _ printrectf.X + sizf.Width * 1.2!, printrectf.Y, _ printrectf.Width - sizf.Width * 1.2!, sizf.Height) gr.DrawString(s, boldfnt, Brushes.Black, rectf1, sf) ' darunter Linie y = printrectf.Y + sizf.Height * 1.2F gr.DrawLine(Pens.Black, printrectf.X, y, printrectf.Right, y) 'Aufräumarbeiten boldfnt.Dispose() sf.Dispose() End Sub
Seite drucken (PrintOnePage) PrintOnePage beginnt damit, dass die X-Position für die Zeilennummer und den Text er-
mittelt wird (die Zeilennummer am linken Rand, der Text so weit eingerückt, wie die Zeilennummer Platz braucht). ypos gibt die Y-Position für die erste Zeile an, wobei hier der Platz für die Kopfzeile berücksichtigt werden muss. Private Function PrintOnePage( _ ByVal gr As Graphics, ByVal fnt As Font, _ ByVal printrectf As RectangleF, ByVal txtLines() As String, _ ByVal startlineNr As Integer, ByVal linesPerPage As Integer, _ ByVal colsPerLine As Integer, ByVal reallyPrint As Boolean) _ As Integer Dim Dim Dim Dim Dim Dim
s As String lineNr As Integer = startlineNr lineNrTxt As String lineheight, xpos1, xpos2, ypos As Single sf As StringFormat = StringFormat.GenericTypographic boldfnt As New Font(fnt, FontStyle.Bold)
' xpos1: X-Position für Zeilennummer ' xpos2: X-Position für den Text ' ypos : Y-Position der nächsten Zeile
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
989
xpos1 = printrectf.X xpos2 = printrectf.X + _ gr.MeasureString("88888", fnt, 0, sf).Width ypos = printrectf.Y + _ gr.MeasureString("W", fnt).Height * headLines
Nun folgt eine Schleife über die Textzeilen, die erst endet, wenn eine Zeile nicht mehr vollständig auf der Seite Platz hat. Jede einzelne Zeile wird zuerst mit UnTabify von Tabulatorzeichen befreit und dann mit WrapLine so umbrochen, dass kein Text am rechten Seitenrand abgeschnitten wird. In lineheight wird der vertikale Platzbedarf dieser Zeile ermittelt. Wenn ausreichend Platz ist, wird sowohl die Zeilennummer als auch die Zeile selbst mit DrawString dargestellt; ypos wird um lineheight vergrößert. Andernfalls wird die Schleife verlassen. Die Schleife wird auch beendet, wenn die letzte Zeile des Texts ausgedruckt wurde. Do ' ' s s
Zeilenumbruch für die nächste Zeile (5 ist die Breite für Zeilennummer) = UnTabify(txtLines(lineNr)) = WrapLine(s, colsPerLine - 5)
' vertikaler Platzbedarf ' "W", damit auch bei leeren Zeilen sinnvolles Ergebnis lineheight = gr.MeasureString("W" + s, fnt).Height ' ist noch genug Platz? If ypos + lineheight < printrectf.Bottom Or _ lineNr = startlineNr Then If reallyPrint Then ' Zeilennummer drucken lineNrTxt = (lineNr + 1).ToString("####") gr.DrawString(lineNrTxt, boldfnt, Brushes.Black, _ xpos1, ypos) ' Zeile drucken gr.DrawString(s, fnt, Brushes.Black, xpos2, ypos, sf) End If ypos += lineheight lineNr += 1 Else ' diese Zeile hat nicht mehr Platz --> neue Seite Exit Do End If Loop Until lineNr >= txtLines.GetUpperBound(0) boldfnt.Dispose() Return lineNr End Function
990
17 Drucken
Tabulatorzeichen durch Leerzeichen ersetzen (UnTabify) UnTabify durchsucht die übergebene Zeichenkette nach Tabulatorzeichen. Diese werden durch so viele Leerzeichen ersetzt, dass sich das nächste Zeichen an einer Position befindet, die ein Vielfaches der Tabulatorgröße beträgt (per Default acht Zeichen). Zur Zusammensetzung der neuen Zeichenkette wird aus Effizienzgründen ein Objekt der StringBuilder-Klasse verwendet (siehe Abschnitt 8.2.6). Private Function UnTabify(ByVal s As String) As String Const tabsize As Integer = 8 Dim pos, realpos, n As Integer Dim tmp As New System.Text.StringBuilder() For pos = 1 To Len(s) If Mid(s, pos, 1) <> vbTab Then tmp.Append(Mid(s, pos, 1)) realpos += 1 Else ' Anzahl der einzufügenden Leerzeichen berechnen n = tabsize - ((pos - 1) Mod tabsize) tmp.Append(Space(n)) realpos += n End If Next Return tmp.ToString End Function
Zeilenumbruch durchführen (WrapLine) WrapLine testet, ob die übergebene Zeichenkette länger ist als die maximale Spaltenanzahl cols. Wenn das der Fall ist, wird zuerst die Einrücktiefe indent ermittelt. Diese ergibt sich aus der Anzahl der Leerzeichen am Beginn der Zeichenkette plus 2. (Im Code steht indent = i + 1. Das liegt daran, dass die VB.NET-Zeichenkettenfunktionen das erste Zeichen mit
dem Index 1 adressieren, nicht mit 0.) Die Einrücktiefe wird auf die halbe Seitenbreite limitiert. In der folgenden Schleife wird die neue Zeichenkette tmp zeilenweise zusammengesetzt. Private Function WrapLine(ByVal s As String, _ ByVal cols As Integer) As String Dim i, indent, pos As Integer Dim tmp As New System.Text.StringBuilder() ' bei kurzen Zeichenketten: kein Umbruch notwendig If Len(s) <= cols Then Return s ' Einrücktiefe ermitteln (max. halbe Seitenbreite) For i = 1 To Len(s) If Mid(s, i, 1) <> " " Then Exit For Next
17.4 Beispiel – Inhalt eines Textfelds ausdrucken
991
indent = i + 1 'um zwei Zeichen einrücken If indent > cols / 2 Then indent = CInt(cols / 2) ' Zeile zerlegen, ab der zweiten Zeile einrücken tmp.Append(Strings.Left(s, cols)) pos = cols While pos < Len(s) tmp.Append(vbCrLf + Space(indent) + _ Mid(s, pos + 1, cols - indent)) pos += cols - indent End While Return tmp.ToString End Function
17.4.4 Verbesserungsideen Wenn Ihnen das Programm gefällt, können Sie noch einige Zeit in seine Optimierung investieren. Hier einige Verbesserungsideen: •
Sehen Sie in PrintDialog die Möglichkeit vor, nur den markierten Teil des Texts auszudrucken. (Diesen Text können Sie aus TextBox1.SelectedText entnehmen. Anschließend verwenden Sie am besten Split, um den Text in einzelne Zeilen zu zerlegen.)
•
Sehen Sie eine Option zur Durchführung eines zweispaltigen Drucks im Querformat vor.
•
Der Zeilenumbruch (Funktion WrapLine) setzt eine konstante Zeichengröße voraus (also die Schriftart Courier oder eine verwandte Schriftart). Damit der Umbruch auch mit Proportionalschriften zufriedenstellend funktioniert, müsste in WrapLine der tatsächliche Platzbedarf ermittelt werden.
•
Generell wäre es wünschenswert, wenn WrapLine den Umbruch an logischen Grenzen (Wortende, Leerzeichen, Satzzeichen etc.) durchführen würde und nicht willkürlich mitten im Wort.
Ausdruck eines formatierten Texts (RichTextBox) Das obige Beispielprogramm basiert darauf, dass unformatierter Text aus einem TextBoxSteuerelement gedruckt werden soll. Viel schwieriger wird es, wenn Sie formatierten Text aus einer RichtextBox drucken möchten. Die in GDI (also in der alten Version ohne +) für einen Ausdruck vorgesehene Methode SelPrint gibt es nicht mehr, ebensowenig einen vergleichbaren Ersatz. Vergeblich werden Sie auch nach einer Möglichkeit suchen, auf den Text in einer strukturierten Form zuzugreifen (z.B. in Blöcken, die gleichermaßen formatiert sind). Ein theoretisch gangbarer Weg besteht darin, die Eigenschaft Rtf auszuwerten: Diese Eigenschaft enthält den gesamten Text inklusive aller Formatierungscodes (z.B. \i, \b, \fsn
992
17 Drucken
VERWEIS
im Textformat). Allerdings ist die Anzahl der Codes beinahe endlos, und der Versuch, daraus einen druckbaren Text zu machen, wäre ein Mammutunternehmen. Praktikabler ist es, den Text einfach zu speichern (im RTF-Format) oder in die Zwischenablage zu kopieren und den Ausdruck dann an ein anderes Programm zu delegieren. Dazu bietet sich jedes Textverarbeitungsprogramm an, das via Automation ferngesteuert werden kann (z.B. Word). Ein konkretes Beispiel finden Sie in Abschnitt 12.5.3.
17.5
Fortgeschrittene Programmiertechniken
Wie es sich für ein VB.NET-Buch gehört, standen in diesem Kapitel bisher die Nutzung der Steuerelemente im Vordergrund. Trotz minimalen Codeaufwands können Sie damit eine gute Benutzerschnittstelle und qualitativ hochwertige Ausdrucke erreichen. In manchen Fällen bieten die Steuerelemente aber zu wenig Flexibilität, um das Programm exakt an eigene Vorstellungen anzupassen. Dieser Abschnitt gibt daher einige Tipps, wie Sie die Objekte aus dem System.Drawing.Printing-Namensraum selbst verwalten können.
17.5.1 Drucken ohne Steuerelemente Um einen Ausdruck durchführen zu können, müssen Sie auf jeden Fall auf die PrintDocument-Klasse zurückgreifen. VB.NET verhält sich so, als wäre diese Klasse ein Steuerelement – aber das ist genau genommen gar nicht der Fall. Indem VB.NET die PrintDocument-Klasse in der Toolbox anbietet, nimmt es Ihnen lediglich die Objekterzeugung ab. (Dazu wird im Codeteil Vom Windows Form Designer generierter Code die Zeile Friend WithEvents PrintDocument1 As System.Drawing.Printing.PrintDocument eingefügt.) Sie können diesen Schritt ohne weiteres selbst durchführen, gewinnen dadurch aber nicht viel. Ein wenig anders sieht es bei den übrigen in diesem Kapitel vorgestellten Steuerelementen aus (PrintDialog etc.). Auf diese Steuerelemente können Sie tatsächlich verzichten. (Das ist dann sinnvoll, wenn Sie zusätzliche Druckoptionen integrieren möchten.) Allerdings müssen Sie dann eine Menge Arbeit selbst erledigen.
PrintDocument-Klasse selbst verwalten Wenn Sie ein Objekt der PrintDocument-Klasse selbst im Code erzeugen, bestehen zwei Möglichkeiten, das Objekt mit seinen Ereignisprozeduren zu verbinden. (Die Grundlagen der Ereignisverwaltung in Windows.Forms-Anwendungen werden in den Abschnitten 14.11.2 und 15.2.3 beschrieben.)
17.5 Fortgeschrittene Programmiertechniken
993
Variante 1 – Mit WithEvents: Komfortabler ist es, das PrintDocument-Objekt mit dem Schlüsselwort WithEvents zu deklarieren. In diesem Fall können Sie die Ereignisprozeduren komfortabel per Mausklick über den Codeeditor einfügen (zuerst im linken Listenfeld den Objektnamen myprintdoc auswählen, dann im rechten Listenfeld das gewünschte Ereignis). Private WithEvents myprintdoc As New Printing.PrintDocument() ' Ereignisprozeduren mit Handles myprintdoc.Eventname Private Sub myprintdoc_BeginPrint(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintEventArgs) _ Handles myprintdoc.BeginPrint ... End Sub
Variante 2 – Ohne WithEvents: Sie können das Ereignis auch mit AddHandler mit der Ereignisprozedur verbinden. In diesem Fall können Sie das PrintDocument-Objekt auch ohne WithEvents deklarieren. Diese Variante ist insbesondere dann interessant, wenn Sie aus irgendeinem Grund mehrere PrintDocument-Objekte verwalten möchten, dafür aber gemeinsame Ereignisprozeduren verwenden möchten. Die Deklarationen der Ereignisprozeduren müssen Sie nun selbst eingeben und dabei darauf achten, dass Sie die korrekten Parameter angeben. Das Schlüsselwort Handles ist jetzt nicht mehr notwendig. Private myprintdoc As New Printing.PrintDocument() ' Verbindung zu Ereignisprozeduren z.B. in Form_Load herstellen AddHandler myprintdoc.BeginPrint, AddressOf myprintdoc_BeginPrint AddHandler myprintdoc.PrintPage, AddressOf myprintdoc_PrintPage AddHandler myprintdoc.EndPrint, AddressOf myprintdoc_EndPrint ' Ereignisprozeduren ohne Handles myprintdoc.Eventname Private Sub myprintdoc_BeginPrint(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintEventArgs) ... End Sub
Liste aller Drucker ermitteln Ausgehend von einem PrintDocument-Objekt können Sie mit einer einfachen Schleife die Namen aller installierten Drucker ermitteln. Die Aufzählung ist über die Eigenschaft InstalledPrinters des PrinterSettings-Objekts zugänglich. Dim prdoc As New Printing.PrintDocument() Dim s As String For Each s In prdoc.PrinterSettings.InstalledPrinters ComboBoxPrinter.Items.Add(s) Next
994
17 Drucken
Standarddrucker ermitteln Den Namen des Standarddruckers (Defaultdruckers) können Sie der PrinterName-Eigenschaft des PrinterSettings-Objekts entnehmen (natürlich nur, wenn Sie diese Eigenschaft noch nicht verändert haben!). s = prdoc.PrinterSettings.PrinterName
Drucker auswählen Der beim Ausdruck verwendete Drucker wird ebenfalls durch PrinterName bestimmt. Wenn Sie nicht den Standarddrucker verwenden möchten, müssen Sie lediglich die PrinterName-Eigenschaft ändern. (Die angegebene Zeichenkette sollte eine aus der InstalledPrintersAufzählung sein.) prdoc.PrinterSettings.PrinterName = "druckername"
Einstellung Hoch-/Querformat Ob der Ausdruck im Hoch- oder Querformat erfolgt, wird durch die Eigenschaft Landscape des PageSettings-Objekts gesteuert. prdoc.DefaultPageSettings.Landscape = True/False
Beachten Sie, dass die Veränderung dieser Eigenschaft die restlichen Seiteneigenschaften (insbesondere PaperSize und Margins) nicht berührt! Die veränderten Seitenmaße für die Graphics-Ausgabe werden erst beim Aufruf der PagePrint-Ereignisprozedur errechnet und im Parameter e übergeben (e.PageBounds bzw. e.MarginBounds).
Unterschiedliches Seitenlayout innerhalb eines Ausdrucks Normalerweise haben alle Seiten eines Ausdrucks dasselbe Layout. Das gemeinsame Seitenlayout wird bereits vor Beginn des Ausdrucks eingestellt (indem Eigenschaften der Page- und PrinterSetting-Objekte verändert werden). In seltenen Fällen kann es aber notwendig sein, für einzelne Seiten (oder für jede Seite) ein besonderes Seitenlayout oder spezielle Druckereinstellungen zu verwenden (etwa um die erste Seite aus einem anderen Papierschacht zu entnehmen etc.). Zur Umsetzung solcher Spezialwünsche ist das QueryPageSettings-Ereignis der PrintDocument-Klasse vorgesehen. Das Ereignis tritt unmittelbar vor dem PrintPage-Ereignis auf, also für jede Seite einmal. Innerhalb der Ereignisprozedur kann e.PageSetting verändert werden. Die hier durchgeführten Änderungen gelten nur für die jeweils nächste Seite. Die folgende Beispielprozedur bewirkt, dass abwechselnd Seiten im Hoch- und im Querformat gedruckt werden. Private Sub myprintdoc_QueryPageSettings(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.QueryPageSettingsEventArgs) _ Handles myprintdoc.QueryPageSettings e.PageSettings.Landscape = Not e.PageSettings.Landscape End Sub
17.5 Fortgeschrittene Programmiertechniken
995
Drucken in eine Datei Wenn Sie den PrintDialog zur Einstellung der Druckerparameter verwenden, können Sie mit dem Dialogfeld IN DATEI DRUCKEN den Ausdruck in eine Datei umleiten. Sobald Sie PrintDocument1.Print() ausführen, erscheint automatisch ein Dialog, in dem Sie den gewünschten Dateinamen angeben können. Leider gibt es momentan keine Möglichkeit, den Druck in eine Datei ohne diesen Dialog durchzuführen. Die Eigenschaft PrintToFile der Klasse PrinterSettings darf gemäß der Hilfe nicht per Programmcode verändert werden, um einen Ausdruck in eine Datei zu erreichen. (Der Versuch, es dennoch zu tun, führt zwar zu keiner Fehlermeldung, bleibt aber auch sonst wirkungslos.) Ebenso fehlt eine Möglichkeit, den gewünschten Dateinamen einzustellen. In einem Beitrag zur News-Gruppe microsoft.public.dotnet.framework.drawing schrieb der Microsoft-Mitarbeiter John Hornick am 9.2.2002, dass die Druckumleitung in eine Datei ohne PrintDialog erst in zukünftigen Versionen der .NET-Klassenbibliothek möglich sein wird.
Drucken ohne Status- bzw. Abbruch-Dialog Sobald Sie PrintDocument.Print ausführen, erscheint automatisch ein kleiner Dialog, der angibt, wie viele Seiten bereits gedruckt wurden und die Möglichkeit zum Abbruch bietet (siehe auch Abbildung 17.1). Der Dialog wird von einem Objekt der Klasse PrintControllerWithStatusDialog erzeugt (Namensraum System.Windows.Forms). Dieses Objekt gilt als Defaulteinstellung für die Eigenschaft PrintController des PrintDocument-Objekts. Der PrintController ist in erster Linie für den Aufruf der diversen Print-Ereignisprozeduren verantwortlich. Wenn Sie möchten, dass kein Statusdialog angezeigt wird, müssen Sie statt des DefaultPrintController einen eigenen verwenden. Dazu fügen Sie in Form_Load (oder an einer ande-
ren Stelle, an der Sie Initialisierungsarbeiten durchführen) die folgende Zeile ein: myprintdoc.PrintController = New Printing.StandardPrintController()
HINWEIS
Damit verwenden Sie statt eines PrintControllerWithStatusDialog nun ein Objekt der Klasse StandardPrintController (Namensraum System.Drawing.Printing), das dieselben Eigenschaften hat, aber eben keinen Dialog anzeigt. Der Statusdialog erscheint wieder, wenn Sie eine Seitenvorschau durchführen. Das liegt daran, dass zur Seitenvorschau automatisch die dritte Variante der PrintController-Klasse zum Einsatz kommt, nämlich ein Objekt der Klasse PreviewPrintController (Namensraum System.Drawing.Printing). Es scheint keine Möglichkeit zu geben, den Statusdiaulog auch bei der Seitenvorschau zu vermeiden.
996
17 Drucken
17.5.2 Eigene Seitenvorschau Seitenvorschau (Drucken)Mit dem PrintPreviewDialog-Steuerelement kann eine Seitenvorschau mit wenigen Zeilen zusätzlichem Code realisiert werden (siehe auch Abschnitt 17.2.3). Der Nachteil dieser Vorgehensweise besteht darin, dass Sie diesen Dialog in keiner Weise verbessern können. (Und gerade was die Bedienung des Seitenvorschaudialogs betrifft, wären viele Verbesserungen vorstellbar.)
Wenn Sie sich also die Arbeit machen möchten, eine eigene Seitenvorschau zu programmieren, dann können Sie als Grundlage dafür das PrintPreviewControl verwenden. Dabei handelt es sich nicht um einen fertigen Dialog, sondern um ein herkömmliches Steuerelement, das nur den eigentlichen Vorschaubereich (Zeichenbereich) der Druckvorschau umfasst. Bevor das Steuerelement angezeigt werden kann, muss die Document-Eigenschaft mit einem PrintDocument-Objekt verbunden werden. Sobald das Steuerelement (als Teil eines Fensters) sichtbar wird, löst es für das zugeordnete PrintDocument-Objekt PrintPage-Ereignisse für alle Seiten des Dokuments aus. Diese Seiten werden intern gespeichert; die erste Seite wird sofort angezeigt. Die Details, welche Seiten wie groß im Steuerelement erscheinen, werden durch eine Reihe von Eigenschaften gesteuert: •
Wenn AutoZoom auf True gestellt wird, dann wird der Zoomfaktor entsprechend der Größe des PrintPreviewControl-Steuerelements automatisch so eingestellt, dass die gesamte Seite abgebildet werden kann.
•
Wenn AutoZoom dagegen False enthält (das ist die Defaulteinstellung), dann steuert Zoom den Vergrößerungsfaktor für die Seite an (per Default 0,3). Wenn Zoom verändert wird, wird AutoZoom automatisch auf False gestellt.
•
Rows und Columns geben an, wie viele Seiten gleichzeitig dargestellt werden sollen (in Zeilen und Spalten gemessen). Wenn Sie also Rows=2 und Cols=3 angeben, dann werden innerhalb des Steuerelements sechs Seiten gleichzeitig angezeigt. Per Default enthalten beide Eigenschaften den Wert 1.
•
StartPage gibt die Nummer der ersten Seite an. Die Nummerierung beginnt mit 0 für
die erste gedruckte Seite. (Die Nummerierung ist unabhängig von den tatsächlichen Seitennummern. Wenn die Seiten drei bis sechs eines Dokuments ausgedruckt wurden, dann bewirkt StartPage=0, dass die dritte Seite sichtbar ist.) Falls aufgrund einer entsprechenden Rows- und Columns-Einstellung mehrere Seite gleichzeitig angezeigt werden, dann bezieht sich StartPage auf die erste sichtbare Seite. Aus unerfindlichen Gründen gibt es keine Eigenschaft, aus der Sie die Gesamtseitenanzahl entnehmen können. Sie können diese Zahl aber mit einem kleinen Trick ermitteln: Weisen Sie StartPage einen riesigen Wert zu (StartPage=100000). Dadurch wird die letzte Seite angezeigt. Die Eigenschaft wird dabei automatisch korrigiert. Bei einem sechsseitigen Dokument enthält StartPage dann also den Wert 5. Leider funktioniert dieser Test erst, nachdem sämtliche Seiten für die Vorschau erzeugt wurden. Zu dem Zeitpunkt, zu dem Form_Load ausgeführt wird, ist das noch nicht der Fall.
17.5 Fortgeschrittene Programmiertechniken
•
997
UseAntiAlias gibt an, ob Texte und Linien im Steuerelement geglättet werden sollen (per Default False). Das bewirkt eine höhere Bildqualität, verursacht aber auch einen
höheren Rechenaufwand.
17.5.3 Beispielprogramm Das folgende Beispielprogramm besteht aus drei Formularen. Das Startformular enthält nur zwei Buttons: Mit TESTSEITE DRUCKEN gelangen Sie in das zweite Formular, wo Sie den Drucker und das Seitenformat auswählen können (siehe Abbildung 17.8). Wenn Sie den Dialog mit OK verlassen, werden auf diesem Drucker drei Testseiten ausgedruckt.
Abbildung 17.8: Beispielprogramm zur eigenen Administration der Druckerobjekte
Der zweite Button führt zu einem einfachen Seitenvorschaudialog, der auf der Basis des PrintPreviewControl-Steuerelements realisiert wurde (siehe Abbildung 17.9). Mit dem Dialog können Sie den Zoomfaktor einstellen und durch ein mehrseitiges Dokument blättern.
Abbildung 17.9: Ein eigener Seitenvorschaudialog
998
17 Drucken
Programmcode Hauptformular Der Programmcode verteilt sich auf drei Formulardateien. In Form1.vb (dem Hauptfenster) wird ein Objekt des Typs PrintDocument erzeugt und in der Variablen myprintdoc gespeichert. Außerdem wird die Variable pageNr als globaler Seitenzähler deklariert. ' Beispiel drucken\ohne-steuerelemente ' Datei form1.vb Private WithEvents myprintdoc As New Printing.PrintDocument() Private pageNr As Integer
Die Button-Ereignisprozeduren dienen dazu, die Formulare 2 und 3 anzuzeigen. ' Dialog zur Einstellung der Druckparameter anzeigen Private Sub Button1_Click(...) Handles Button1.Click Dim frm2 As New Form2() frm2.prdoc = myprintdoc If frm2.ShowDialog() = DialogResult.OK Then myprintdoc.Print() End If frm2.Dispose() End Sub ' Fenster für die Seitenvorschau anzeigen Private Sub Button2_Click(...) Handles Button2.Click Dim frm3 As New Form3() frm3.PrintPreviewControl1.Document = myprintdoc frm3.ShowDialog() frm3.Dispose() End Sub
Auch der Code zur Durchführung des Ausdrucks weist wenig Neuerungen im Vergleich zu den bisherigen Beispielen auf. ' Testseite drucken Private Sub myprintdoc_BeginPrint(...) Handles myprintdoc.BeginPrint pageNr = 1 End Sub Private Sub myprintdoc_PrintPage(...) Handles myprintdoc.PrintPage Dim gr As Graphics = e.Graphics Dim fnt1 As New Font("arial", 15) Dim fnt2 As New Font("arial", 100) Dim s As String gr.DrawEllipse(Pens.Black, e.PageBounds) gr.DrawEllipse(Pens.Red, e.MarginBounds) ' Seitennummer anzeigen gr.DrawString(pageNr.ToString, fnt2, Brushes.Gray, _ RectangleF.op_Implicit(e.MarginBounds))
17.5 Fortgeschrittene Programmiertechniken
999
' Seiteneigenschaften anzeigen s = e.PageSettings.ToString + vbCrLf + _ myprintdoc.PrinterSettings.ToString gr.DrawString(s, fnt1, Brushes.Black, _ RectangleF.op_Implicit(e.MarginBounds)) pageNr += 1 If pageNr > 3 Then e.HasMorePages = False Else e.HasMorePages = True End If fnt1.Dispose() fnt2.Dispose() End Sub
Programmcode Druckdialog Der Code zum Druckdialog besteht im Wesentlichen aus Ereignisprozeduren, die die Veränderungen an den Steuerelementen mit den Einstellungen des PageSetting-Objekts synchronisieren. ' Beispiel drucken\ohne-steuerelemente ' Datei form2.vb Public prdoc As Printing.PrintDocument Private Sub Form2_Load(...) Handles MyBase.Load ' Combobox mit Druckern füllen Dim s As String For Each s In prdoc.PrinterSettings.InstalledPrinters ComboBoxPrinter.Items.Add(s) Next ComboBoxPrinter.Text = prdoc.PrinterSettings.PrinterName ' per Default: Abbruch DialogResult = DialogResult.Cancel End Sub ' Druckerauswahl: PrinterName ändern Private Sub ComboBoxPrinter_SelectedIndexChanged(...) _ Handles ComboBoxPrinter.SelectedIndexChanged prdoc.PrinterSettings.PrinterName = ComboBoxPrinter.Text SyncPrinterSettings() End Sub
1000
17 Drucken
' im Formlar je nach DefaultPageSettings die ' Hoch- oder Querformatoption auswählen Private Sub SyncPrinterSettings() If prdoc.DefaultPageSettings.Landscape = True Then RadioButtonLandscape.Checked = True Else RadioButtonPortrait.Checked = True End If End Sub ' Umschaltung Hoch-/Querformat Private Sub RadioButtonPortrait_CheckedChanged(...) _ Handles RadioButtonPortrait.CheckedChanged If RadioButtonPortrait.Checked Then prdoc.DefaultPageSettings.Landscape = False End If End Sub Private Sub RadioButtonLandscape_CheckedChanged(...) _ Handles RadioButtonLandscape.CheckedChanged If RadioButtonLandscape.Checked Then prdoc.DefaultPageSettings.Landscape = True End If End Sub ' Dialog beenden Private Sub ButtonOK_Click(...) Handles ButtonOK.Click ... Private Sub ButtonCancel_Click(...) Handles ButtonCancel.Click ...
Programmcode Seitenvorschau Überraschend kompakt fällt der Code für den Seitenvorschaudialog aus. Die eigentliche Seitenvorschau wird automatisch durchgeführt, die abgedruckten Button-Ereignisprozeduren helfen nur dabei, den sichtbaren Ausschnitt der Vorschau zu steuern. ' Beispiel drucken\ohne-steuerelemente ' Datei form2.vb ' Zoomfaktor vergrößern Private Sub Button1_Click(...) Handles Button1.Click PrintPreviewControl1.Zoom += 0.1 End Sub
17.5 Fortgeschrittene Programmiertechniken
' Zoomfaktor verkleinern Private Sub Button2_Click(...) Handles Button2.Click If PrintPreviewControl1.Zoom > 0.1 Then PrintPreviewControl1.Zoom -= 0.1 End If End Sub ' Auto-Zoom (ganze Seite anzeigen) Private Sub Button3_Click(...) Handles Button3.Click PrintPreviewControl1.AutoZoom = True End Sub ' vorige Seite anzeigen Private Sub Button4_Click(...) Handles Button4.Click If PrintPreviewControl1.StartPage > 0 Then PrintPreviewControl1.StartPage -= 1 End If End Sub ' nächste Seite anzeigen Private Sub Button5_Click(...) Handles Button5.Click PrintPreviewControl1.StartPage += 1 End Sub
1001
18 Weitergabe von Windows-Programmen (Setup.exe) Zur Installation Ihrer selbst entwickelten Windows-Programme am Rechner des Kunden stellen Sie diesem üblicherweise eine CD zur Verfügung, die unter anderem das Programm setup.exe enthält. Der Kunde führt dieses Programm aus und kann danach das von Ihnen entwickelte Programm nutzen. (setup.exe kann natürlich auch Bestandteil eines ZIP-Archivs sein, dass der Kunde vom Internet lädt.) Dieses Kapitel beschreibt, wie Sie für Ihre Programme ein Installationsprogramm erstellen. Dazu gibt es einen eigenen Projekttyp: SETUP-PROJEKT. Das Ergebnis dieses Projekts ist nicht nur setup.exe, sondern eine *.msi-Datei für eine Windows-Installer-kompatible Installation. 18.1 18.2 18.3 18.4
Einführung Installationsprogramm erstellen (Entwicklersicht) Installation ausführen (Kundensicht) Installationsprogramm für Spezialaufgaben
1004 1005 1008 1011
1004
18.1
18 Weitergabe von Windows-Programmen (Setup.exe)
Einführung
In der Theorie ist alles ganz einfach: Nachdem Sie Ihre VB.NET-Windows-Anwendung zu einer *.exe-Datei kompiliert haben, können Sie diese Datei an andere Personen weitergeben. Diese können das Programm sofort und ohne komplizierte Installation ausführen. Falls Ihr Programm auf irgendwelchen eigenen Bibliotheken (*.dll-Dateien) basiert, müssen diese in dasselbe Verzeichnis wie die *.exe-Datei kopiert werden – auch das ist kein Problem. Microsoft bezeichnet diese aus DOS-Zeiten vertraute Art der Software-Installation nicht ohne Stolz xcopy-Installation. Verallgemeinert bedeutet das: Es müssen einfach nur alle Dateien aus dem bin-Projektverzeichnis in das Zielverzeichnis kopiert werden. Es sind weder Einträge in die Registrierdatenbank noch Veränderungen an den Windows-Systemdateien erforderlich, wie dies früher der Fall war. In dieser Hinsicht bietet VB.NET tatsächlich viele Vorteile gegenüber VB6. Wenn es in der Praxis immer so einfach wäre wie oben beschrieben, gäbe es natürlich kein eigenes Kapitel. Die erste Grundvoraussetzung für die Ausführung jeden .NETProgramms besteht darin, dass am Zielrechner das .NET-Framework installiert ist. Momentan ist diese Voraussetzung leider selten erfüllt. Daher muss der Kunde als Erstes die mehr als 20 MByte große Datei dotnetfx.exe ausführen. (Details zu dotnetfx.exe folgen in Abschnitt 18.3.2.) Darüber hinaus gibt es eine Reihe von Gründen, die gegen eine xcopy-Installation und für ein richtiges Installationsprogramm sprechen: •
Windows-Anwender sind mittlerweile gewohnt, dass sie Programme mit setup.exe installieren und bei Bedarf über die Systemsteuerung komfortabel wieder deinstallieren können. Wohin das Programm installiert wird, interessiert den Kunden weniger als die Tatsache, ob es einen neuen Eintrag im Startmenü und vielleicht ein neues Icon am Desktop gibt.
•
Wenn das VB.NET-Programm auf herkömmliche ActiveX-Bibliotheken oder -Steuerelemente zurückgreift, müssen diese wie bisher in die Registrierdatenbank eingetragen werden.
•
Wenn das eigene Programm ein neues Dateiformat einführt (z.B. *.abc), muss auch dieses Dateiformat registriert werden.
•
Eventuell sollen vor der Installation Lizenzbedingungen angezeigt werden.
•
Eventuell sollen Lokalisierungsdateien für unterschiedliche Sprachen installiert werden.
Die VB.NET-Entwicklungsumgebung unterstützt die Erstellung von Installationsprogrammen durch den eigenen Projekttyp SETUP- UND WEITERGABEPROJEKTE. Die Möglichkeiten dieses Projekttyps stehen im Mittelpunkt der weiteren Abschnitte dieses Kapitels. So viel vorweg: Setup-Projekte bieten ungleich mehr Möglichkeiten als der Weitergabeassistent von VB6 und stellen insofern einen riesigen Fortschritt dar. Leider ist die VS.NET-Benutzeroberfläche zur Erstellung von Setup-Projekten ebenso so unintuitiv wie die dazugehör-
18.2 Installationsprogramm erstellen (Entwicklersicht)
1005
VERWEIS
ende Dokumentation abstrakt und hölzern klingt. In dieser Hinsicht besteht für die nächste Version von VB.NET noch großer Verbesserungsbedarf. Dieses Kapitel beschreibt bei weitem nicht alle Möglichkeiten von SETUP- UND WEITERGABEPROJEKTEN! Insbesondere werden Internet-Installationen (*.cab-Dateien), ASP.NET-Installationen sowie die Weitergabe von Datenbankanwendungen nicht behandelt. In der Online-Hilfe finden Sie ausführliche Informationen zu diesen Themen, wenn Sie nach Weitergeben von Anwendungen und Komponenten suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vsintro7/html/vboriDeploymentInVisualStudio.htm
18.2
Installationsprogramm erstellen (Entwicklersicht)
Dieser Abschnitt beschreibt die Erstellung eines einfachen Installationsprogramms unter Anwendung des SETUPPROJEKT-ASSISTENTEN. Sie finden ein entsprechendes Beispielprojekt auf der beiliegenden CD im Verzeichnis setup\intro\intro-installation. •
Der erste Schritt besteht darin, das VB.NET-Projekt zu laden, eventuell nochmals zu testen und als Release-Version zu kompilieren. Dazu wählen Sie entweder in der Standardsymbolleiste oder im Projekteigenschaftsdialog die Release-Konfiguration aus.
•
Nun fügen Sie mit DATEI|PROJEKT HINZUFÜGEN|NEUES PROJEKT ein Projekt des Typs SETUP-ASSISTENT in Ihre Projektmappe ein. Wenn Sie möchten, dass die Setup-Dateien in einem Unterverzeichnis Ihres VB.NETProjektverzeichnisses gespeichert werden, müssen Sie das Verzeichnis entsprechend einstellen. (Per Default wird das Setup-Verzeichnis im selben Verzeichnis wie das zuletzt erzeugte Projekt erstellt. Das ist nicht immer die beste Lösung und kann Verwirrung stiften.) Denken Sie auch daran, dem Setup-Projekt einen sinnvollen Namen zu geben – per Default wird setup1 verwendet. Dieser Text taucht dann auch in allen Dialogen des Installationsprogramms auf! Die folgenden Absätze gehen davon aus, dass Sie das Projekt setupname nennen.
•
PROJEKTTYP: Im Dialog PROJEKTTYP des Assistenten können zwischen verschiedenen Installationstypen wählen. Für Windows-Programme ist nur die erste Option zweckmäßig: SETUP FÜR EINE WINDOWS-ANWENDUNG ERSTELLEN.
•
PROJEKTAUSGABEGRUPPEN: Recht merkwürdig sind die Übersetzungen der Optionen des
nächsten Dialogs ausgefallen (siehe Abbildung 18.1). Normalerweise müssen Sie nur die erste Option auswählen. PRIMÄRE AUSGABE meint das Programm bzw. die Klassenbibliothek (also die kompilierte *.exe- oder *.dll-Datei).
1006
18 Weitergabe von Windows-Programmen (Setup.exe)
Mit den LOKALISIERTEN RESSOURCEN sind kompilierten Zusatzdateien (*.dll-Dateien) mit Sprachanpassungen gemeint. Derartige Dateien liegen nur vor, wenn das Programm lokalisiert wurde (siehe Abschnitt 15.2.10). Die Defaultlokalisierung ist in jedem Fall enthalten, auch ohne dass diese Option ausgewählt wird. DEBUGSYMBOLE befinden sich in der *.pdb-Datei und liegen nur dann vor, wenn Ihr Programm in der Debug-Konfiguration kompiliert wurde. Bei Programmen, die weitergegeben werden sollen, ist das üblicherweise nicht der Fall. Wenn Ihr Programm noch in Entwicklung ist, können die Debugsymbole aber hilfreich sein, weil damit klarere Fehlermeldungen möglich sind. INHALTSDATEIEN sind Dateien, die dem VB.NET-Projekt während der Entwicklung hinzugefügt wurden (z.B. HTML-Dateien, Grafikdateien, readme.txt etc.), die aber nicht
direkt in das kompilierte Programm einfließen. Diese Dateien werden später in dasselbe Verzeichnis wie das Programm installiert. QUELLDATEIEN sind alle Codedateien.
•
DATEIEN EINBEZIEHEN: In diesem Dialog können Sie weitere Dateien (die nicht Bestandteil
des VB.NET-Projekts sein müssen) zu den Installationsdateien hinzufügen. Per Default werden auch diese Dateien in das Programmverzeichnis installiert.
Abbildung 18.1: Der Setup-Assistent
Basierend auf diesen Angaben erstellt der Assistent nun das neue Setup-Projekt. Sämtliche Einstellungen werden in der Textdatei setupname.vdproj gespeichert. Da die Datei nicht direkt verändert werden sollte (was wegen des unübersichtlichen Formats auch kaum
18.2 Installationsprogramm erstellen (Entwicklersicht)
1007
möglich ist), dominieren in diesem Kapitel die Abbildungen, während es nur wenig Code zu sehen gibt. Vielleicht empfinden Sie das ja als angenehme Abwechslung?
VERWEIS
Falls Ihr VB.NET-Programm ActiveX-Steuerelemente oder -Bibliotheken verwendet, werden automatisch alle erforderlichen *.dll- und *.ocx-Dateien in das Projekt miteinbezogen. Der Assistent zeigt dabei einen Dialog an, in dem er fragt, ob er alle Abhängigkeiten richtig erkannt hat. Zumindest bei den relativ einfachen Tests, die ich durchgeführt habe, gab es dabei keine Probleme. Anders als unter VB6 werden die *.dll- und *.ocx-Dateien übrigens nicht in das Windows-Systemverzeichnis installiert, sondern in das Programmverzeichnis. Das reduziert die Gefahr, dass Konflikte zwischen unterschiedlichen Versionen dieser Dateien entstehen können. Wie Sie in Ihrem Installationsprogramm von den Vorgaben des Assistenten abweichende Sonderwünsche realisieren können, wird in Abschnitt 18.4 beschrieben. Beachten Sie insbesondere, dass der Installationsassistent keinen Eintrag des Programms in das Windows-Startmenü vorsieht. Zumindest in diesem Punkt muss die Konfiguration in der Regel erweitert werden.
Installationsdateien Der Assistent kümmert sich nicht darum, die Installationsdateien tatsächlich zu erzeugen. Dazu müssen Sie ERSTELLEN|SETUPNAME ERSTELLEN ausführen. Als Ergebnis werden in das Verzeichnis setupname\release oder setupname\debug die folgenden Dateien geschrieben. (Die Debug- und Release-Konfiguration gilt also auch für Setup-Projekte!) setup.exe
Programm zum Start der Installation
setup.ini
Konfigurationsdatei zu setup.exe
setupname.msi
die eigentlichen Installationsdaten für das VB.NET-Programm
InstMsiA.exe
Installationsprogramm für den Windows Installer, ANSI-Version (Windows 98/ME)
InstMsiW.exe
Installationsprogramm für den Windows Installer, Unicode-Version (Windows NT/2000/XP)
Windows Installer Die von der Entwicklungsumgebung erstellten Installationsdaten befinden sich ausschließlich in setupname.msi. Die restlichen Dateien sind gewissermaßen nur Beiwerk: setup.exe überprüft am Zielrechner, ob dort der Windows Installer bereits installiert ist. Wenn das nicht der Fall ist, wird (je nach Windows-Version) InstMsiA.exe oder InstMsiW.exe ausgeführt. Anschließend wird setupname.msi installiert. Der Windows Installer ist eine Betriebssystemerweiterung, die Installationen und Deinstallationen besser verwaltet als die Funktionen, die zu diesem Zweck ursprünglich mit Windows mitgeliefert werden. Windows-Installer-kompatible Installationsdateien bestehen
1008
18 Weitergabe von Windows-Programmen (Setup.exe)
TIPP
nur aus einer einzigen Datei *.msi, die alle weiteren Daten und Einstellungen in komprimierter Form enthält. Zu den besonderen Merkmalen des Windows Installers gehört die Reparaturfunktion: Damit werden versehentlich gelöschte Dateien automatisch (z.B. wenn beim Programmstart eine DLL fehlt) oder manuell (über die Systemsteuerung) wiederhergestellt. Auf Rechnern, auf denen der Windows Installer bereits installiert ist, kann die Installation des VB.NET-Programms einfach durch einen Doppelklick auf setupname.msi durchgeführt werden. Innerhalb der VS.NET-Entwicklungsumgebung können Sie eine Probeinstallation auch per Kontextmenü im Projektmappen-Explorer starten. Dort können Sie die Installation durch DEINSTALLIEREN wieder rückgängig machen.
18.3
Installation ausführen (Kundensicht)
18.3.1 Installation eines .NET-Programms Aus Kundensicht sieht die Installation folgendermaßen aus. (Die Abbildungen wurden auf einem Windows-XP-System durchgeführt, auf dem vorher noch kein .NET-Programm installiert wurde.) •
Der Kunde startet das Programm setup.exe. Falls auf seinem Rechner der Windows Installer noch nicht installiert ist, wird diese Betriebssystemerweiterung ohne weitere Rückfrage installiert.
•
Das Installationsprogramm überprüft, ob das .NET-Framework bereits installiert ist. Wenn das nicht der Fall ist, wird der Kunde dazu aufgefordert, dieses zu installieren (siehe Abbildung 18.2). Das Installationsprogramm wird abgebrochen. Da zur Installation des .NET-Frameworks keine weiteren Informationen angegeben werden und diese Installation auch nicht automatisiert werden kann, müssen zu diesem Punkt klare Anweisungen auf der Installations-CD gegeben werden (z.B. in Readme.htm oder Readme.txt).
•
Wenn das .NET-Framework vorhanden ist, wird das eigentliche Installationsprogramm ausgeführt (siehe Abbildung 18.3). Dabei kann der Kunde das Installationsverzeichnis auswählen und angeben, ob das Programm nur für den aktuellen Benutzer installiert werden soll oder so, dass alle Benutzer des Rechner es verwenden können.
18.3 Installation ausführen (Kundensicht)
1009
Abbildung 18.2: Aufforderung zur Installation des .NET-Frameworks
HINWEIS
Abbildung 18.3: Das eigentliche Installationsprogramm
Sowohl das installierte Programm als auch das .NET-Framework können bei Bedarf mit der Systemsteuerung deinstalliert werden. Die zur Deinstallation erforderlichen Informationen werden in *.msi-Dateien im Verzeichnis Windows\Installer gespeichert. Der Windows Installer selbst kann allerdings nicht mehr deinstalliert werden.
1010
18 Weitergabe von Windows-Programmen (Setup.exe)
18.3.2 Installation des .NET-Frameworks Falls das .NET-Framework noch nicht installiert ist, muss der Kunde die mehr als 20 MByte große Datei dotnetfx.exe ausführen. Wenn Sie Ihr Programm per CD verbreiten, sollte sich diese Datei auf der CD befinden; wenn Sie Ihr Programm dagegen per Internet verbreiten, müssen Sie einen Link auf diese Datei anbieten. Sie finden dotnetfx.exe im Verzeichnis WCU\dotnetframework Ihrer VS.NET-CD oder im Internet an der folgenden Adresse. Beachten Sie, dass es das Framework in verschiedenen Sprachversionen gibt! (Die deutsche Version befindet sich unter dem Namen dotnet-de.exe auch auf der beiliegenden CD.) http://msdn.microsoft.com/downloads/default.asp
Sofern die Voraussetzungen zur Installation erfüllt sind (siehe nächste Überschrift), verläuft die Installation vollkommen problemlos. Es müssen keinerlei Einstellungen durchgeführt werden.
TIPP
Abbildung 18.4: Installation des .NET-Frameworks
Nach der Installation aller Dateien zeigt der Zustandsbalken 100 % an, außerdem wird der Text Verbleibende Zeit: 0 Sekunden sichtbar. Anschließend passiert für geraume Zeit scheinbar nichts. (In Wirklichkeit werden irgendwelche Konfigurationsarbeiten durchgeführt.) Drücken Sie zu diesem Zeitpunkt nicht ABBRUCH in der Meinung, es wäre schon alles erledigt! Wenn die Installation wirklich fertig ist, erscheint ein eigener Dialog, der mit OK bestätigt werden muss.
HINWEIS
18.4 Installationsprogramm für Spezialaufgaben
1011
Weisen Sie Ihre Kunden darauf hin, dass eine Konfiguration des .NET-Frameworks mit START|SYSTEMSTEUERUNG|VERWALTUNG|MICROSOFT .NET FRAMEWORK CONFIGURATION durchgeführt werden kann. Auf diese Weise kann der Kunde beispielsweise die .NET-Sicherheitseinstellungen verändern.
Voraussetzungen Die Installation des .NET-Frameworks setzt ein einigermaßen aktuelles Windows-Betriebssystem voraus. (Windows 95 wird explizit nicht unterstützt!) •
Windows 98 und Windows ME
•
Windows NT 4.0 mit Service Pack 6a
•
Windows 2000 (Service Pack 2 wird empfohlen, ist aber nicht Voraussetzung)
•
Windows XP und alle Nachfolgeversionen
Außerdem muss unabhängig vom Betriebssystem zumindest der Internet Explorer 5.01 installiert sein. Bei Datenbankanwendungen werden zusätzlich die Datenbankkomponenten MDAC 2.6 (besser: MDAC 2.7 oder eine neuere Version) vorausgesetzt. Wenn vorauszusehen ist, dass Ihr Kunde diese Voraussetzungen nicht erfüllt, sollten Sie mit Ihrer Installations-CD entsprechende Update-Dateien mitliefern oder auf Ihrer Website Links darauf einrichten. (Sie finden die Update-Dateien im Verzeichnis WCU\dotnetframework Ihrer VS.NET-CD.)
18.4
Installationsprogramm für Spezialaufgaben
Der in Abschnitt 18.2 vorgestellte SETUPPROJEKT-ASSISTENT deckt gerade einmal die ersten fünf Prozent der Variationsmöglichkeiten ab, die in Setup-Projekten möglich sind. Wenn Sie darüber hinausgehende Wünsche haben, gibt dieser Abschnitt einige Tipps für deren Realisierung. Dabei kann das mit dem Assistenten erstellte Setup-Projekt als Ausgangspunkt dienen. Sie können aber auch mit einem neuen (leeren) Setup-Projekt beginnen – die wenigen Schritte des Assistenten sind rasch auch per Hand erledigt und vermitteln ein besseres Verständnis dafür, woraus sich ein Setup-Projekt zusammensetzt.
TIPP
Ein VB.NET-Programm kann durchaus mit mehreren Setup-Projekten verbunden werden, die sich alle in derselben Projektmappe befinden. Die Dateien jedes Setup-Projekts werden in einem eigenen Verzeichnis gespeichert. Per Default befinden sich diese Verzeichnisse auf derselben Ebene wie das VB.NETProjektverzeichnis. Übersichtlicher ist es oft, die Setup-Verzeichnisse als Unterverzeichnisse zum VB.NET-Projektverzeichnis anzulegen. Denken Sie daran, den Speicherort im Dialog PROJEKT HINZUFÜGEN entsprechend einzustellen!
1012
18 Weitergabe von Windows-Programmen (Setup.exe)
18.4.1 Grundeinstellungen eines Setup-Projekts Um ein neues Setup-Projekt ohne den Assistenten zu starten, fügen Sie Ihrem VB.NETProjekt ein neues Projekt des Typs SETUP-PROJEKT hinzu. Vom vorerst noch leeren SetupProjekt wird in der Entwicklungsumgebung nun das Fenster DATEISYSTEM angezeigt (siehe Abbildung 18.5). Dieses Fenster dient zur Steuerung, welche Dateien wohin installiert werden sollen.
VERWEIS
HINWEIS
Neben dem DATEISYSTEM-Fenster gibt es eine Reihe weiterer Fenster, mit denen verschiedene Details des Konfigurationsprozesses gesteuert werden. Sie können diese Fenster durch Buttons im Projektmappen-Explorer oder über das Menü mit ANSICHT|EDITOR|NAME öffnen, solange das Setup-Projekt das aktive Projekt ist. Beachten Sie, dass auch bei SetupProjekten viele Einstellungsdetails im Eigenschaftsfenster durchgeführt werden können! Wenn bei einzelnen Elementen des Setup-Projekts ungültige Einstellungen vorliegen oder wenn die Entwicklungsumgebung Gründe für eine Warnung erkennt, wird das Element durch eine blaue Wellenlinie markiert. Hinweise über die Gründe des Problems finden Sie dann im Fenster AUFGABENLISTE. Manche Warnungen können Sie ignorieren, sie lassen sich nicht vermeiden und behindern das Erstellen des Setup-Projekts nicht. Beispielsweise enthält jedes SetupProjekt die Warnung, dass das .NET-Framework nicht im Projekt enthalten ist. Es ist aber unmöglich, das .NET-Framework in ein Setup-Projekt zu integrieren.
Eine zentrale Referenz aller Elemente und Eigenschaften von Setup-Projekten gibt es leider nicht, und F1 im Eigenschaftsfenster funktioniert leider auch nicht. Ein Startpunkt für die Suche nach einer Beschreibung der Eigenschaften ist die folgende Seite (suchen Sie nach Weitergabeeigenschaften!): ms-help://MS.VSCC/MS.MSDNVS.1031/vsintro7/html/vxconDeploymentProperties.htm
Ein minimales Setup-Projekt Um ein minimales Setup-Projekt zu erstellen, das einem vom Assistenten erstellten Projekt entspricht, müssen Sie lediglich Ihr Programm in den ANWENDUNGSORDNER (das ist das Installationsverzeichnis) einfügen. Dazu führen Sie per Kontextmenü HINZUFÜGEN|PROJEKTAUSGABE aus. Im Einfügedialog können Sie dann das eigentliche Programm (das als PRIMÄRE AUSGABE bezeichnet wird), die Lokalisierungsdateien etc. auswählen. Die Entwicklungsumgebung erkennt automatisch die Abhängigkeit vom .NET-Framework und fügt einen entsprechenden Abhängigkeitseintrag in das Setup-Projekt ein. Dieser Eintrag bewirkt, dass bei der Installation eine Warnung erscheint, wenn das .NET-Framework nicht installiert ist. (Es ist leider keine Möglichkeit vorgesehen, auch das .NET-Framework durch das Installationsprogramm zu installieren.)
18.4 Installationsprogramm für Spezialaufgaben
1013
Abbildung 18.5: Das Dateisystemfenster eines Setup-Projeks
Installationsverzeichnis (Anwendungsordner) Bei der Ausführung der Installation werden alle Objekte des ANWENDUNGSORDNERS per Default in das Verzeichnis Programmme\firmenname\projektname installiert. (Der Kunde kann das Installationsverzeichnis natürlich ändern.) Die Defaulteinstellung für projektname ergibt sich aus dem Namen des Setup-Projekts. Als firmenname verwendet das Setup-Projekt den bei der Windows-Installation angegebenen Firmennamen. Wenn Sie das Verzeichnis anders zusammensetzen möchten, müssen Sie im Projektmappen-Explorer das Projekt anklicken und danach in das Eigenschaftsfenster wechseln. Dort stellen Sie die Eigenschaften ProductName und Manufacturer entsprechend ein.
Installation zusätzlicher Dateien Im Dateisystemfenster können Sie weitere Dateien zur späteren Installation einfügen. Dabei sind Sie nicht auf die drei per Default angezeigten Verzeichnisse beschränkt, sondern können in das Dateisystemfenster per Kontextmenü weitere Verzeichnisse einfügen (z.B. das Verzeichnis für Schriftarten, das Verzeichnis SENDEN AN, das Startmenüverzeichnis des Anwenders etc.). Zu den meisten dieser Verzeichnissen können Sie auch eigene Unterverzeichnisse erstellen (z.B. ein Unterverzeichnis beispiele für den Anwendungsordner, um dort einige Beispieldateien zu installieren.) Bei jedem einzelnen Eintrag in eines dieser Verzeichnisse können Sie im Eigenschaftsfenster die Condition-Eigenschaft einstellen, so dass die Datei nur unter bestimmten Umständen installiert wird. Der Umgang mit Bedingungen wird in Abschnitt 18.4.4 beschrieben.
1014
18 Weitergabe von Windows-Programmen (Setup.exe)
18.4.2 Startmenü, Desktop-Icons Damit das Installationsprogramm im Startmenü des Anwenders einen Link auf das Programm einrichtet bzw. ein entsprechendes Icon in den Desktop gibt, müssen Sie in die Ordner DESKTOP DES BENUTZERS bzw. PROGRAMMMENÜ DES BENUTZERS jeweils eine Verknüpfung auf das Programm einrichten. Dazu gibt es zwei Wege, die beide nicht übermäßig intuitiv sind. •
Klicken Sie im Verzeichnis ANWENDUNGSORDNER den Eintrag PRIMÄRE AUSGABE mit der rechten Maus an und führen Sie VERKNÜPFUNG ERSTELLEN aus. (Zur Wiederholung: PRIMÄRE AUSGABE bezeichnet das Programm!) Diese Verknüpfung verschieben Sie dann mit der Maus in den Ordner DESKTOP DES BENUTZERS bzw. PROGRAMMMENÜ DES BENUTZERS und geben ihr einen neuen Namen.
•
Führen Sie im Ordner DESKTOP DES BENUTZERS bzw. PROGRAMMMENÜ DES BENUTZERS (im rechten Teil des Dateisystemfensters!) das Kontextmenükommando NEUE VERKNÜPFUNG ERSTELLEN aus. Damit gelangen Sie in einen Dialog, in dem Sie das Objekt auswählen können, auf das die Verknüpfung zeigen soll. Wählen Sie PRIMÄRE AUSGABE aus.
Beide Wege führen zum selben Ergebnis – einem Verknüpfungsobjekt im Ordner DESKTOP DES BENUTZERS bzw. PROGRAMMMENÜ DES BENUTZERS mit der Target-Einstellung PRIMÄRE AUSGABE (siehe auch Abbildung 18.6). Verknüpfungsobjekte können nicht kopiert bzw. dupliziert werden; deswegen müssen Sie die hier beschriebene Vorgehensweise wiederholen, wenn Sie sowohl einen Startmenüeintrag als auch ein Desktop-Icon wünschen. Falls Sie möchten, dass die Einträge in das Programmmenü in einem Unterverzeichnis erfolgen (also beispielsweise intro\intro.exe statt einfach intro.exe), fügen Sie einfach in das Verzeichnis PROGRAMMMENÜ ein Unterverzeichnis mit einem beliebigen Namen ein.
Abbildung 18.6: Der Eintrag intro im Programmmenü zeigt auf das auszuführende Programm
Wenn Sie möchten, können Sie nun noch die Icon-Eigenschaft ändern und so das Aussehen des Links optimieren. Beim Icon-Auswahldialog können Sie allerdings nur Dateien auswählen, die Teil des Setup-Projekts sind (die sich also als Einzeldatei in einem der Ver-
18.4 Installationsprogramm für Spezialaufgaben
1015
zeichnisse des Dateisystemfensters befinden). Um zu vermeiden, dass die Icon-Datei selbst in eines der Verzeichnisse am Zielcomputer installiert wird, stellen Sie deren ExcludeEigenschaft auf True. Die Datei wird dann weder installiert, noch wird sie im jeweiligen Verzeichnis im DATEISYSTEMFENSTER angezeigt. Sie kann aber nun zur Einstellung der IconEigenschaft verwendet werden. (Wenn Sie das alles ziemlich unlogisch finden, kann ich Ihnen nur recht geben. So schön die resultierenden Setup-Projekte sind, so wenig intuitiv ist leider der Vorgang zum Erstellen dieser Projekte ...)
18.4.3 Benutzeroberfläche des Installationsprogramms Per Default sieht das Installationsprogramm wie in Abbildung 18.3 aus und ist nur mit einem Minimum an Steuerelementen ausgestattet. Wenn Sie andere Vorstellungen darüber haben, wie das Installationsprogramm aussehen soll bzw. wenn Sie das Installationsprogramm mit zusätzlichen Optionen oder Eingabefeldern ausstatten möchten, müssen Sie die Benutzeroberfläche des Installationsprogramms ändern. Das Aussehen und die Eigenschaften des Installationsprogramms werden über das Fenster BENUTZEROBERFLÄCHE gesteuert (siehe Abbildung 18.7). In diesem Fenster wird die vorge-
HINWEIS
sehene Dialogabfolge in Baumform dargestellt. Die Dialoge sind dabei in drei Gruppen aufgeteilt, STARTEN (vor der eigentlichen Installation), STATUS (Zustandsanzeige während der Installation) und BEENDEN (nach der Installation). Das Benutzeroberflächenfenster unterscheidet zwischen einer gewöhnlichen und einer Administratorinstallation. Eine Administratorinstallation ermöglicht es, die Anwendung so in ein Netzwerkverzeichnis zu installieren, dass später alle Anwender des Netzwerks das Programm von dort installieren können. Dieses Kapitel beschreibt allerdings nur eine gewöhnliche Installation.
Dialogeigenschaften Sie können nun verschiedene Details der Dialoge WILLKOMMEN, INSTALLATIONSORDNER etc. im Eigenschaftsfenster verändern. (Ein direktes Feedback, wie die resultierenden Dialoge dann wirklich aussehen, gibt es leider nicht. Eine Vorstellung über das tatsächliche Aussehen der Dialoge bekommen Sie erst, wenn Sie eine Probeinstallation durchführen.) Die folgenden Punkte beschreiben einige elementare Eigenschaften. •
BannerBitmap bestimmt die Hintergrund-Bitmap, die auf den einzelnen Dialogen ange-
zeigt wird. Laut Dokumentation sollte diese Bitmap 500*70 Punkte groß sein. Je nach DPI-Einstellung des Rechners kann es aber sein, dass die Bitmap größer dargestellt wird. Wenn Sie Wert auf eine hohe Darstellungsqualität legen, sollten Sie eventuell eine Bitmap mit höherer Auflösung verwenden (aber natürlich im gleichen Verhältnis Breite/Höhe). Eine eigene BannerBitmap kann nur aus den Dateien ausgewählt werden, die vorher in das Dateisystemfenster eingefügt wurden. Die Bitmap sollte eher helle Farben verwenden, weil über der Bitmap (links oben) je nach Dialog unterschiedliche Texte in schwar-
1016
18 Weitergabe von Windows-Programmen (Setup.exe)
zer Farbe angezeigt werden (Willkommen beim Setup-Assistenten von [name], Installationsordner wählen etc.). Es scheint keine Möglichkeit zu geben, diese Texte einzustellen. Beachten Sie, dass BannerBitmap für jeden Dialog separat eingestellt werden muss! •
CopyrightWarning enthält einen Text, der vor Copyright-Verletzungen warnt. Der Text
wird im Willkommensdialog angezeigt. •
WelcomeText enthält einen Text, der die Funktion des Installationsprogramms kurz beschreibt. Das ist auch der geeignete Ort für eine kurze Beschreibung des Programms (damit der Kunde weiß, was er jetzt eigentlich installiert).
Abbildung 18.7: Gestaltung der Benutzeroberfläche des Installationsprogramms
Dialogtitel Bei den Eigenschaften für die im Benutzeroberflächenfenster dargestellten Objekte suchen Sie vergeblich nach einer Einstellungmöglichkeit für die Titel der Dialoge. Dieser Text wird stattdessen der Projekteigenschaft ProductName entnommen. Um diese Eigenschaft zu verändern, klicken Sie im Projektmappen-Explorer das Setup-Projekt an und wechseln dann in das Eigenschaftsfenster.
Zusätzliche Dialoge einfügen Neben den in Abbildung 18.7 dargestellten Standarddialogen können Sie per Kontextmenü zusätzliche Dialoge in die drei Gruppen STARTEN, STATUS und BEENDEN einfügen. Sie haben dabei die Wahl zwischen mehreren vorgefertigten Dialogen, deren Details Sie abermals durch die Veränderung von Eigenschaften verändern können. Zur Auswahl stehen: •
Ein Splash-Dialog zur Anzeige einer Begrüßungs-Bitmap, die 480*320 Pixel groß sein sollte. Diese Bitmap wird üblicherweise als erster Dialog des Setup-Prozesses angezeigt.
•
Dialoge mit zwei bis vier Optionen (etwa zur Auswahl verschiedener Installationsvarianten).
18.4 Installationsprogramm für Spezialaufgaben
1017
•
Dialoge mit vier Kontrollkästchen (etwa zur Auswahl, welche optionalen Komponenten installiert werden sollen). Wenn Sie weniger als vier Kontrollkästchen anzeigen möchten, können Sie einzelne Kontrollkästchen unsichtbar machen. Wenn Sie mehr als vier Kontrollkästchen benötigen, können Sie bis zu drei Kontrollkästchendialoge (A, B und C) einsetzen, so dass sich insgesamt maximal zwölf Wahlmöglichkeiten ergeben.
•
Dialoge mit zwei bis vier Texteingabefeldern.
•
Ein Dialog zur Anzeige des Lizenzvertrags bzw. der Nutzungsbedingungen. Die Datei kann im RTF-Format angegeben werden. Die Installation kann nur fortgesetzt werden, wenn der Text akzeptiert wird (ICH STIMME ZU).
•
Ein Dialog zur Darstellung einer Infodatei (abermals RTF-Format). Im Unterschied zum Lizenzdialog braucht dieser Text nicht akzeptiert zu werden.
•
Ein Dialog zur Eingabe der Kundeninformationen (Name, Firma, Seriennummer).
•
Ein Dialog zur Durchführung einer Registrierung. Dieser Dialog enthält einen Button, über den Sie ein externes Programm starten können (das Teil des Setup-Projekts sein muss).
Jeder Dialog kann innerhalb des Projekts nur einmal verwendet werden. Generell bewegen sich alle Veränderung im Installationsprogramm in einem vorgegebenen, engen Rahmen. Setup-Dialoge sind daher nicht mit normalen Windows.Forms-Fenstern zu vergleichen.
ACHTUNG
Soweit die Dialoge Eingabemöglichkeiten vorsehen (Optionen, Kontrollkästchen oder Textfelder), können Sie diese Eingaben beispielsweise in den Weitergabebedingungen auswerten (siehe den nächsten Abschnitt). Eigene Dialoge im Abschnitt STARTEN müssen vor dem Dialog INSTALLATIONSORDNER angegeben werden. Wenn Sie Dialoge dahinter einfügen, erscheint im Fenster AUFGABENLISTE eine Warnung, die vermutlich falsch übersetzt ist und genau das Gegenteil meint (also dass eigene Dialoge nach dem Dialog INSTALLATIONSORDNER angegeben werden müssen).
18.4.4 Start- und Weitergabebedingungen Ob und in welchen Umfang die Installation durchgeführt wird, kann durch Bedingungen gesteuert werden. Setup-Projekte kennen dabei zwei Typen von Bedingungen: Start- und Weitergabebedingungen.
Startbedingungen Startbedingungen werden am Beginn der Installation überprüft; sind sie nicht erfüllt, wird die Installation nach der Anzeige eines Fehlerdialogs abgebrochen. Startbedingungen werden im STARTBEDINGUNGEN-Fenster erstellt. Um eine neue Bedingung zu definieren, führen Sie im Feld ANFORDERUNGEN FÜR DEN ZIELCOMPUTER eines der vorgesehenen Kontextmenü-
1018
18 Weitergabe von Windows-Programmen (Setup.exe)
kommandos aus, z.B. DATEISTARTKONDITION HINZUFÜGEN. Dadurch werden in das STARTBEDINGUNGEN-Fenster zwei Einträge hinzugefügt: •
In den Einträgen der Gruppe ZIELCOMPUTER DURCHSUCHEN formulieren Sie die Bedingung. Abbildung 18.8 zeigt, wie überprüft wird, ob sich im Windows-Systemverzeichnis die Datei abcde.dll befindet. Das Ergebnis dieses Tests ist eine Variable (z.B. FILEEXISTS1). Andere Tests können z.B. bestimmte Einträge in die Registrierdatenbank betreffen.
•
In den Einträgen der Gruppe STARTBEDINGUNGEN formulieren Sie die Reaktion, wenn eine Bedingung aus der obigen Gruppe erfüllt ist. Normalerweise geben Sie dort nur mit der Message-Eigenschaft den Text für die Fehlermeldung an, mit der die Installation abgebrochen werden soll. In manchen Fällen kann es sinnvoll sein, auch die Condition-Eigenschaft der Bedingung zu ändern. Diese enthält normalerweise einfach einen Variablennamen wie FILEEXISTS1. Sie können dort aber auch komplexere Bedingungen formulieren, z.B. FILEEXISTS1 AND (NOT FILEEXISTS2). Weitere Tipps zur Formulierung von Bedingungen folgen in den nächsten Absätzen.
Abbildung 18.8: Vor der Installation wird getestet, ob sich abcef.dll im Windows-Systemverzeichnis befindet
Weitergabebedingungen Weitergabebedingungen entscheiden darüber, welche Teile einer Installation durchgeführt werden können. Zur Formulierung derartiger Bedingungen ist bei manchen Objekten eines Setup-Projekts (insbesondere bei den Verzeichnissen und Dateien, die im Dateisystemfenster angezeigt werden) eine Condition-Eigenschaft vorgesehen. Wenn diese Eigenschaft eingestellt wird, dann wird das betreffende Objekt nur dann installiert bzw. ausgeführt, wenn die Bedingung erfüllt ist. (Per Default sind Condition-Eigenschaften leer. Das bedeutet, dass die Installation auf jeden Fall durchgeführt wird.)
18.4 Installationsprogramm für Spezialaufgaben
1019
Bei der Formulierung einer Bedingung können Sie auf die folgenden Typen von Variablen zugreifen: Ergebnisvariablen von Suchergebnissen (ZIELCOMPUTER DURCHSUCHEN): Das Ergebnis der Suche nach einer Datei, nach einem Eintrag in die Registrierdatenbank etc. kann über eine Variable ausgewertet werden. Der Name ergibt sich durch die Property-Eigenschaft der Suchabfrage (per Default FILEEXISTS1, -2 etc., REGISTRYVALUE1, -2 etc.).
•
Eingabevariablen von Text- oder Optionsfeldern: Wenn Sie die Benutzeroberfläche des Installationsprogramms mit eigenen Dialogen ausgestattet haben, können Sie die dort durchgeführten Eingaben in Bedingungen auswerten. Der Variablenname ergibt sich aus den Property-Eigenschaften des Dialogs. Beispielsweise kennt der Dialog Kontrollkästchen (A) die vier Eigenschaften CheckBox1Property, CheckBox2Property etc. Die dort eingestellte Zeichenkette (per Default CHECKBOXA1, -A2 etc.) bestimmt den Variablennamen.
•
Vordefinierte Variablen: Es gibt eine Reihe vordefinierter Variablen, die vom Windows Installer zur Verfügung gestellt werden. Besonders wichtig sind Version9X, VersionNT, ServicePackLevel und WindowsBuild, mit denen Sie die genaue Betriebssystemversion des Zielrechners feststellen können, um in Abhängigkeit davon bestimmte Komponenten zu installieren oder auch nicht. Mit AdminUser können Sie feststellen, ob die Person, die das Installationsprogramm ausführt, Administratorrechte hat (nur Windows NT/ 2000/XP).
VORSICHT
VERWEIS
•
Zahllose weitere vordefinierte Variablen werden in der Dokumentation zum Windows Installer aufgezählt. Suchen Sie nach Windows Installer property reference: ms-help://MS.VSCC/MS.MSDNVS.1031/msi/pref_3mp1.htm
Beim Erstellen eines Projekts erfolgt kein Test, ob die in Condition-Einstellungen genannten Variablen überhaupt existieren! Wenn Sie sich vertippen, ist die Variable unbekannt und wird wie eine leere Zeichenkette betrachtet. Achten Sie auch auf die korrekte Groß- und Kleinschreibung. Die komplette Syntax für Bedingungen ist hier beschrieben: ms-help://MS.VSCC/MS.MSDNVS.1031/msi/novr_1ol4.htm
Probleme mit Bedingungen Eigene Experimente mit den Weitergabebedingungen lieferten leider nicht immer überzeugende Ergebnisse: •
Beispielsweise hat sich herausgestellt, dass die Condition-Eigenschaft bei Verzeichnissen im Fenster DATEISYSTEM einfach ignoriert wird. Ich wollte erreichen, dass der Eintrag des Programms in das Startmenü bzw. auf den Desktop optional erfolgt. Dazu habe ich einen Dialog mit Kontrollkästchen eingefügt und deren Ergebnisse in den ConditionEigenschaften von PROGRAMMMENÜ DES BENUTZERS und DESKTOP DES BENUTZERS ausge-
1020
18 Weitergabe von Windows-Programmen (Setup.exe)
wertet. Der Eintrag in das Startmenü bzw. auf den Desktop erfolgte aber dennoch in jedem Fall, egal wie die Kontrollkästchen bei der Installation eingestellt wurden bzw. welche Bedingung in der Condition-Eigenschaft angegeben wurde. •
Auch bei selbst erstellten Verzeichnissen scheint Condition ignoriert zu werden.
•
Bei Verknüpfungen zu anderen Dateien oder Verzeichnissen ist gar keine ConditionEigenschaft vorgesehen.
18.4.5 Dateityp registrieren Das Beispielprogramm intro.exe wertet beim Programmstart die Kommandozeile aus. Wenn im ersten Parameter ein gültiger Dateiname angegeben ist, wird die Datei sofort geladen. ' Beispiel setup\intro Private Sub Form1_Load(...) Handles MyBase.Load Dim args() As String = Environment.GetCommandLineArgs() If args.Length > 1 Then Dim enc As System.Text.Encoding = _ System.Text.Encoding.GetEncoding(1252) Dim sr As IO.StreamReader Try sr = New IO.StreamReader(args(1), enc) TextBox1.Text = sr.ReadToEnd() sr.Close() textHasChanged = False Catch MsgBox("Die Datei " + args(0) + _ " konnte nicht geöffnet werden.") If Not IsNothing(sr) Then sr.Close() End Try End If End Sub
Wenn Sie nun zusammen mit dem Programm einen neuen Dateityp registieren, können Sie Dateien mit dieser Kennung im Windows Explorer per Doppelklick öffnen (siehe Abbildung 18.9). Zur Registrierung öffnen Sie im Setup-Projekt das Fenster DATEITYPEN und fügen dort per Kontextmenü einen neuen Dateityp hinzu. Anschließend müssen Sie im Eigenschaftsfenster die Details dieses Dateityps angeben (siehe Abbildung 18.10).
18.4 Installationsprogramm für Spezialaufgaben
1021
Wichtig ist vor allem die Eigenschaft Command, mit der Sie angeben, welches Programm zur Bearbeitung derartiger Dateien gedacht ist: hier geben Sie die Programmdatei (also die primäre Ausgabe des Projekts) an. Bei Extensions können Sie eine oder mehrere Dateierweiterungen angeben (ohne vorangestelltem *). Icon und Description geben an, wie derartige Dateien im Explorer angezeigt werden sollen. (Bei der Icon-Einstellung muss abermals eine Datei ausgewählt werden, die bereits Teil des Setup-Projekts ist.)
Abbildung 18.9: ATXT-Dateien gelten jetzt als ANSI-Textdateien und können per Doppelklick mit intro.exe geöffnet werden
Abbildung 18.10: Definition des ATXT-Dateityps
1022
18 Weitergabe von Windows-Programmen (Setup.exe)
18.4.6 Einträge in der Registrierdatenbank durchführen Das Setup-Projekt kann sich auch darum kümmern, während der Programminstallation Einträge in die Registrierdatenbank vorzunehmen. Dazu öffnen Sie einfach das Fenster REGISTRIERUNG und fügen die gewünschten entsprechenden Einträge ein. Abbildung 18.11 zeigt, wie im Registrierverzeichnis HKEY_CURRENT_USER\SOFTWARE\[FIRMA] die Zeichenkette VERSION mit dem Wert 1.0.0.0 eingefügt wird. Statt [FIRMA] wird der Wert der Eigenschaft Manufacturer aus den Projekteigenschaften verwendet.
Abbildung 18.11: Einträge in der Registrierdatenbank verwalten
Anhang
Anhang A Abkürzungsverzeichnis Diese Tabelle fasst die wichtigsten Abkürzungen aus dem .NET- und VB-Umfeld zusammen. Erklärungen dazu, was die Begriffe bedeuten, folgen im Glossar (Anhang B). ActiveX
ActiveX
Marketing-Acronym, gleichbedeutend mit COM
ADO
ActiveX Data Objects
alte Datenbankbibliothek, basiert auf COM
ADO.NET ADO.NET
neue Datenbankbibliothek, basiert auf .NET
API
Application Programming Interface
Funktionen des Betriebssystems
ASP
Active Server Pages
altes System zur Darstellung dynamischer Webseiten, basiert auf VBScript (einer stark eingeschränkten Version von VB6)
ASP.NET
ASP.NET
neues System zur Darstellung dynamischer Webseiten, basiert auf .NET
BCL
Base Class Library
die .NET-Klassenbibliothek (die auf mehrere Einzelbibliotheken besteht, z.B. mscorlib.dll, System.dll und System.Windows.Forms.dll); BCL und FCL haben dieselbe Bedeutung
C
C
prozedurale Programmiersprache (Entwicklung 1969-1973)
C++
C plus plus
objektorientierte Variante von C
C#
C Sharp
Programmiersprache, die Elemente von C, Java und VB.NET enthält
CLR
Common Language Runtime
Bibliothek mit den .NET-Grundfunktionen
CLS
Common Language Specification
ein Satz von Regeln, die alle .NETProgrammiersprachen unterstützen und die alle .NET-Bibliotheken erfüllen müssen, um als CLS-kompatibel zu gelten
COM
Component Object Model
altes System zur Verwaltung von Objekten (vor .NET)
CTS
Common Type System
formale Beschreibung aller von .NET unterstützten Datentypen
DAO
Data Access Objects
sehr alte Datenbankbibliothek, basiert auf COM, speziell zum Zugriff auf JetDatenbanken konzipiert
EMF
Enhanced Metafile
vektororientiertes Grafikformat (Nachfolger von WMF)
1026
A Abkürzungsverzeichnis
FCL
Framework Class Library
die .NET-Klassenbibliothek (die auf mehrere Einzelbibliotheken besteht, z.B. mscorlib.dll, System.dll und System.Windows.Forms.dll); BCL und FCL haben dieselbe Bedeutung
GAC
Global Assembly Cache
Verzeichnis für die .NET-Basisbibliotheken und andere Assemblies, die allen .NETProgrammen zur Verfügung stehen sollen (Windows\assembly\...)
GDI+
Graphics Device Interface
2D-Grafikfunktionen des .NET-Systems
IL
Intermediate Language
Zwischencode, siehe MSIL
IIS
Internet Information Services
Webserver von Microsoft
MDI
Multiple Document Interface
Benutzeroberfläche mit einem Hauptfenster und mehreren kleineren Dokumentfenstern
MSI
Microsoft Installer
System zur Installation und Deinstallation von Programmen
MSIL
Microsoft Intermediate Language
Zwischencode, in den alle .NET-Programme kompiliert werden; erst bei der tatsächlichen Programmausführung erfolgt durch einen JIT-Compiler die Umwandlung in Maschinencode
.NET
NET
ein reines Marketing-Acronym, dessen Buchstaben aber keine (mir bekannte) Bedeutung haben; .NET bezeichnet je nach Kontext alles, vom Framework (also die Gesamtheit aller Bibliotheken und Tools zur Ausführung von .NET-Programmen) bis hin zur aktuellen Microsoft-Philosophie
OLE
Object Linking and Embedding
System zur Einbettung fremder Objekte in eigene Anwendungen; basiert auf COM
RDO
Remote Data Objects
sehr alte Datenbankbibliothek, basiert auf COM, speziell zum Zugriff auf den Microsoft SQL-Server konzipiert
SDK
Software Development Kit
Software-Paket zur Entwicklung eigener Projekte; das .NET-Framework-SDK enthält beispielsweise alle Dateien, die zur Entwicklung von .NET-Projekten erforderlich sind (allerdings keine Entwicklungsumgebung)
SOAP
Simple Object Access Protocol
Standard zum Austausch von Objekten zwischen Prozessen auf unterschiedlichen Rechnern, die durch ein Netzwerk miteinander verbunden sind
Abkürzungsverzeichnis
1027
VB
Visual Basic
Programmiersprache (1991 von Microsoft entwickelt)
VB6
Visual Basic 6
die letzte und populärste Version von VB
VB.NET
Visual Basic .NET
neue VB-ähnliche Programmiersprache
VS.NET
Visual Studio .NET
gemeinsame Entwicklungsumgebung für VB.NET, C#, Visual C++ und andere .NETProgrammiersprachen
VBA
Visual Basic for Applications
Makroprogrammiersprache für das OfficePaket (Office 95 bis XP), syntaktisch mit VB6 verwandt
WMF
Windows Metafile
vektororientiertes Grafikformat
XML
Extensible Markup Language
Textformat zur Darstellung beliebiger (hierarchischer) Daten
VERWEIS
Anhang B Glossar Dieses Glossar gibt eine Kurzbeschreibung für die wichtigsten neuen Begriffe aus der (VB).NET-Welt. Wenn Sie hier einen Begriff vermissen, sollten Sie einen Blick in die Hilfe werfen, wo Sie ein viel ausführlicheres Glossar finden: ms-help://MS.VSCC/MS.MSDNVS.1031/netstart/html/cpgloa.htm
Assembly: Als Assembly werden mehrere *.dll- und/oder *.exe-Dateien bezeichnet, die gemeinsam weitergegeben werden und die eine Einheit (z.B. eine Klassenbibliothek) bilden. Attribut: Attribute werden dazu verwendet, um Programmkonstrukte (Klassen, Prozeduren, Methoden, Variablen etc.) mit zusätzlichen Eigenschaften auszustatten. Die Besonderheit von Attributen besteht darin, dass diese Informationen losgelöst von der Programmausführung ausgewertet werden können. Attribute geben somit die Möglichkeit, Zusatzinformationen an den Compiler, den Debugger, die Entwicklungsumgebung etc. weiterzugeben. Ausnahme: siehe Exception. Bibliothek: Eine Bibliothek ist eine Sammlung von Klassen, die von anderen .NET-Programmen verwendet werden kann. Bei der VB.NET-Programmierung stehen Ihnen zahlreiche Bibliotheken zur Verfügung, die Teil des .NET-Frameworks sind. Sie können mit VB.NET selbst Bibliotheken entwickeln (z.B. mit dem Projekttyp KLASSENBIBLIOTHEK).
Common Language Runtime (CLR): Die CLR bezeichnet die Gesamtheit aller Tools und Bibliotheken, die zur Ausführung von .NET-Programmen erforderlich sind. Dazu zählen der JIT-Compiler, der MSIL-Code in Maschinencode umwandelt, sowie alle .NET-Basisbibliotheken. Die CLR wird durch das .NET-Framework am Rechner installiert. Common Language Specification (CLS): Die CLS beschreibt eine Teilmenge des CTS (siehe unten). Jede .NET-Programmiersprache muss die CLS vollständig unterstützen. Darüber hinaus steht es jeder .NET-Programmiersprache frei, auch weitere Teile des CTS zu implementieren. Bibliotheken, deren Schnittstellen sich auf den CLS-Teil des CTS beschränken, werden als CLS-kompatibel bezeichnet. Der Vorteil derartiger Bibliotheken besteht darin, dass sie von allen .NET-Programmiersprachen uneingeschränkt verwendet werden können. Die .NETKlassenbibliotheken sind selbst weitgehend CLS-kompatibel; vereinzelte inkompatible Methoden können auch auf eine alternative Weise aufgerufen werden, die CLS-kompatibel ist. Common Type System (CTS): Das CTS ist eine formale Beschreibung aller Datentypen, die von .NET grundsätzlich unterstützt werden. Dazu zählen Wert- und Verweistypen, unterschiedliche Klassentypen, Strukturen, Felder (arrays), Aufzählungen (enums) etc.
1030
B Glossar
Delegates: Vereinfacht ausgedrückt sind Delegates Funktionszeiger (function pointers). Sie ermöglichen den Aufruf einer Prozedur, Funktion oder Methode, wenn nur deren Adresse bekannt ist. Wenn man es etwas genauer betrachtet, sind Delegates besondere Klassen (Typen), die durch die Parameterliste einer Prozedur definiert sind. In einem Objekt einer Delegate-Klasse wird die Adresse der Prozedur gespeichert (bei Methoden auch die Adresse des Objekts, auf das die Methode angewendet werden soll). VB.NET-intern werden Delegates auch für die Verwaltung von Ereignissen und Ereignisprozeduren verwendet. Deserialisierung: siehe Serialisierung. Eigenschaft: Eine Eigenschaft ist eine Sonderform einer Prozedur, die innerhalb einer Klasse definiert ist. Von außen betrachtet (also aus Anwendersicht) werden Eigenschaften wie Klassenvariablen verwendet (z.B. obj.eigenschaft = 123). Eigenschaften dienen dazu, Informationen über die Merkmale eines Objekts zu lesen oder zu verändern. Intern bestehen Eigenschaften aus zwei Codeblöcken, von denen einer für das Lesen, der zweite für das Verändern der Eigenschaft verantwortlich ist. Ereignis: Ereignisse bieten ein Mechanismus zur Kommunikation zwischen Objekten. Wenn im Objekt eine Zustandsänderung festgestellt wird, kann das Objekt ein Ereignis auslösen. Das führt dazu, dass an einer anderen Stelle im Programm (dort, wo das Objekt genutzt wird) eine Ereignisprozedur ausgeführt wird. Ereignisprozedur: Ereignisprozeduren sind Prozeduren, die gleichsam automatisch ausgeführt werden, wenn in einem dazugehörenden Objekt ein bestimmtes Ereignis auftritt. Damit das funktioniert, müssen Ereignisprozeduren mit dem jeweiligen Ereignis verbunden werden. Exception: Das Auftreten eines Programmfehlers (z.B. einer Division durch 0) wird als exception (Ausnahme) bezeichnet. Die Besonderheit einer exception besteht darin, dass der
normale Programmfluss innerhalb einer Prozedur unterbrochen wird und stattdessen (soweit vorhanden) Fehlerbehandlungscode ausgeführt wird; dort können alle fehlerspezifischen Informationen aus einem System.Exception-Objekt ausgelesen werden. Wenn es keinen Fehlerbearbeitungscode gibt, wird die exception an die nächsthöhere Ebene in der Aufrufliste weitergegeben, so dass die Fehlerbehandlung dort erfolgen kann. Framework: Das .NET-Framework bezeichnet alle Komponenten, die zur Ausführung von .NET-Programmen erforderlich sind (die so genannte common language runtime, kurz CLR). Dabei handelt es sich um die .NET-Basisbibliotheken und den JIT-Compiler. Das Framework SDK (Software Development Kit) enthält zusätzlich zur runtime-Umgebung auch noch Tools und Dokumentation zur Entwicklung von .NET-Programmen. Garbage collection: Objekte werden in einem eigenen Speicherbereich, dem so genannten heap, gespeichert. Nicht mehr benötigte Objekte können allerdings nicht explizit aus diesem Bereich entfernt werden. Stattdessen kümmert sich ein im Hintergrund (und in einem eigenen Thread) laufender Prozess darum, dass alle Objekte, die nicht mehr verwendet werden, automatisch aus dem Speicher entfernt werden. Dieser Prozess wird als garbage collection (wörtlich: Müllabfuhr) bezeichnet.
Glossar
1031
Global Assembly Cache (GAC): Der GAC bezeichnet ein Verzeichnis, in dem sich die .NET-Basisbibliotheken und andere für alle .NET-Programme zur Verfügung stehende Assemblies befinden. Bevor eine Bibliothek in den GAC aufgenommen wird, erfolgen spezielle Sicherheitsüberprüfungen. Im Regelfall hat nur der Administrator das Recht, Änderungen am GAC durchzuführen. Graphics Device Interface (GDI+): Das GDI ist ein Teil der .NET-Bibliothek, mit deren Hilfe Sie 2D-Grafikoperationen durchführen können (Linien und andere geometrische Objekte zeichnen, Text ausgeben, Bitmaps bearbeiten etc.). Gültigkeitsbereich: Der Gültigkeitsbereich (engl. scope) gibt an, in welchem Codebereich bzw. in welchem Kontext auf ein Schlüsselwort zugegriffen werden kann. Beispielsweise besteht die Möglichkeit, den Zugriff auf die Mitglieder einer Klasse nur innerhalb der Klasse zu erlauben (Private). Wenn das Mitglied dagegen auch nach außen hin zugänglich sein soll, muss es es als Protected, Friend oder Public deklariert werden, wobei diese drei Varianten den Gültigkeitsbereich zunehmend vergrößern. Import: Die Klassennamen der .NET-Bibliotheken sind zum Teil ziemlich lang (z.B. System.Windows.Forms.Button). Damit nicht jedes Mal der ganze Name angegeben werden muss, können durch so genannte Importe Zeichenketten angegeben werden, die bei der Suche nach Klassennamen berücksichtigt werden. Wenn ein derartiger Import System.Windows.Forms lautet, dann ist die Kurzschreibweise Dim b As Button zulässig.) Instanz: Eine Instanz ist die konkrete Realisierung einer Klasse. Instanz ist ein Synonym für Objekt. Interface: siehe Schnittstelle. Just-in-Time-Compiler (JIT-Compiler): Der JIT-Compiler wird automatisch beim Start jedes .NET-Programms gestartet. Er übersetzt die gerade benötigten Teile des .NET-Programms, die ursprünglich in MSIL-Code vorliegen, in Maschinencode. Der JIT-Compiler ist ein integraler Bestandteil des .NET-Frameworks bzw. der Common Language Runtime. Klasse: Eine Klasse ist die abstrakte Beschreibung (der Bauplan) eines objektorientierten Datentyps, der mit Eigenschaften, Methoden etc. ausgestattet ist. In VB.NET können Sie alle in anderen .NET-Bibliotheken definierten Klassen nutzen. (Dazu erzeugen Sie ein Objekt dieser Klasse – siehe auch die Definition zu Objekt.) Sie können aber auch selbst eigene Klassen definieren. Klassenbibliothek: siehe Bibliothek. Klassenmitglied (member): Jede Klasse definiert Methoden, Eigenschaften und andere Konstrukte, die zur Erzeugung, Bearbeitung und Beseitigung (Löschung) von Objekten dienen. All diese Schlüsselwörter werden in diesem Buch mit dem Überbegriff Klassenmitglied bezeichnet (englisch member). Managed Code: Bei der Ausführung von MSIL-Code kümmert sich die Common Language Runtime um die Speicherverwaltung, stellt Debugging-Möglichkeiten zur Verfügung, stellt die korrekte Verwendung von Datentypen sicher etc. Derartiger Code gilt deswegen als managed. Jedes mit VB.NET erzeugte Programm enthält ausschließlich managed code.
1032
B Glossar
Im Gegensatz dazu steht unmanaged code, der außerhalb der Kontrolle der CLR ausgeführt wird. Dazu kommt es beispielsweise, wenn das Programm direkt Betriebssystemfunktionen (API-Funktionen) aufruft oder auf COM-Komponenten zurückgreift. Dabei gelten entsprechend geringere Schutzmechanismen. Member: siehe Klassenmitglied. Methode: Eine Methode ist eine Prozedur, die innerhalb einer Klasse definiert ist. Methoden dienen primär zur Bearbeitung von Objekten; in diesem Fall muss zum Aufruf der Methode das Objekt vorangestellt werden (also obj.methode(parameter)). Es gibt auch sogenannte shared-Methoden, die unabhängig von einem konkreten Objekt verwendet werden können; diesen Methoden muss der Klassenname vorangestellt werden (z.B. IO.Path.Combine(a, b)). Metafile: Dieser Begriff bezeichnet im Kontext mit der Grafikprogrammierung (GDI+) Dateien, die Anweisungen zur Darstellung grafischer Objekte enthalten. Im Gegensatz zu Bitmaps, die eine unveränderliche Grafik Pixel für Pixel enthalten, ist das Metafile-Format vektororientiert. Das bedeutet, dass die Grafik nachträglich beliebig vergrößert, verkleinert und sogar transformiert (gedreht, skaliert etc.) werden kann. Microsoft Intermediate Language (MSIL): .NET-Programme werden vom Compiler in ein plattformunabhängiges Format umgewandelt. Erst bei der ersten tatsächlichen Ausführung des Programms erfolgt durch einen so genannten Just in Time Compiler (JIT) eine weitere Umwandlung dieses Codes in den Maschinencode des Rechners. Dieses Verfahren hat zwei Vorteile: Zum einen sind .NET-Programme theoretisch plattformunabhängig (auch wenn es die Common Language Runtime sowie den JIT-Compiler zurzeit nur für Windows gibt), zum anderen kann das Kompilat für den jeweiligen Prozessor speziell optimiert werden. Mitglied: siehe Klassenmitglied. Modul: In VB.NET ist ein Modul eine Codeeinheit. Module haben ähnliche Eigenschaften wie Klassen, es gelten aber einige Einschränkungen. Im Kontext der .NET-Technologie bezeichnet ein Modul dagegen eine *.exe- oder *.dll-Datei einer Assembly. Multithreading: Dieser Begriff bezeichnet die quasiparallele Ausführung verschiedener Codeteile eines Programms (siehe auch Thread). Multithreading kommt auch .NET-intern zum Einsatz, etwa für die garbage collection oder bei der Ausführung asynchroner Operationen. Namensraum (namespace): Ein Namensraum fasst mehrere Klassen zu einer Gruppe zusammen. Beispielsweise ist System.IO der Namensraum für alle Klassen, die zur Bearbeitung von Dateien dienen. Einzelne Klassen werden dann in der Form System.IO.name angesprochen. Eine Assembly (eine aus einer oder mehreren Dateien bestehende Klassenbibliothek) kann mehrere Namensräume enthalten, um so Ordnung in eine Klassenbibliothek zu bringen. Auf den ersten Blick vielleicht verwunderlich ist der Umstand, dass derselbe Namensraum auch in unterschiedlichen Assemblies enthalten sein darf. Für den Programmierer spielt
Glossar
1033
dies keine Rolle, d.h., die Klassen aller gleichnamigen Namensräume werden gleich behandelt, egal aus welcher Bibliothek sie stammen. System.IO ist dafür ein gutes Beispiel. Dieser Namensraum ist sowohl in der CLR als auch in der System-Assembly enthalten. (Die System.IO-Klassen der CLR dienen eher für grundlegende Aufgaben, während die System.IO-Klassen der System-Assembly für Spezialanwen-
dungen gedacht sind.) Die einzige Voraussetzung für die Verwendung der Klassen besteht darin, dass die dazugehörende Assembly durch einen Verweis mit dem Projekt verbunden ist. .NET-Framework: siehe Framework. Objekt: Ein Objekt ist eine konkrete Realisierung (eine so genannte Instanz) einer Klasse. Wenn Sie Dim myobject As New klassenname() ausführen, ist myobject eine Variable, die auf ein Objekt der Klasse klassename verweist. Siehe auch Klasse. Prozedur: Eine Prozedur ist ein abgeschlossener Codeblock, der von außen aufgerufen werden kann. Prozeduren werden auch als Unterprogramme, Funktionen (= Unterprogramm mit Rückgabewert) oder als Methoden (=öffentlich zugängliche Prozedur innerhalb einer Klasse) bezeichnet. Innerhalb von Prozeduren können lokale Variablen deklariert werden. Prozess: Programme werden in so genannten Prozessen ausgeführt. Prozesse werden vom Betriebssystem verwaltet. (Bei Windows NT/2000/XP können Sie die Liste der laufenden Prozesse im Task-Manager anzeigen.) Bei manchen Programmen gibt es mehrere Teilprozesse (Threads), die quasi parallel ausgeführt werden (Multithreading). Safe code: Safe code hat keinen direkten Zugriff auf den Speicher (darf also keine Zeiger verwenden). Mit VB.NET erzeugter Code ist immer safe, weil VB.NET generell keine Zeiger unterstützt. Mit C# kann dagegen auch unsafe code erzeugt werden, wenn Zeigeroperationen unbedingt erforderlich sind. Schnittstelle: Eine Schnittstelle ist eine abstrakte Beschreibung von Merkmalen (z.B. Eigenschaften und Methoden). Eine Klasse kann eine Schnittstelle implementieren. Dazu muss es konkreten Code für alle Merkmale der Schnittstelle enthalten. Die Namen von Schnittstellen beginnen üblicherweise mit I. Schnittstellen werden oft dazu verwendet, um auf ähnliche Klassen einen einheitliche Codezugang zu ermöglichen. Beispielsweise beschreibt die Schnittstelle IList Eigenschaften und Methoden, mit denen Aufzählungen bearbeitet werden. Zahlreiche Klassen der .NETBibliothek realisieren diese Schnittstelle – z.B. Felder (System.Array), Aufzählungen (System.Collections.ArrayList) oder Listenfelder (System.Windows.Forms.ListBox.ObjectCollection). Deswegen gibt es einen einheitlichen Zugriff auf die Elemente all dieser Klassen. Scope: siehe Gültigkeitsbereich. Serialisierung (serialization): Serialisierung bedeutet die Umwandlung eines Objekts oder mehrerer miteinander verknüpfter Objekte in einen Datenstrom (wahlweise in einem binären Format oder in XML). Durch eine Deserialisierung können die Objekte aus dem Datenstrom wieder rekonstruiert werden. (De-)Serialisierung ermöglicht es, Objekte in
1034
B Glossar
Dateien zu speichern oder über ein Netzwerk zwischen unterschiedlichen Prozessen auszutauschen. Thread: Ein Thread ist ein Teilprozess eines Programms. Bei gewöhnlichen Programmen gibt es für die Ausführung des von Ihnen entwickelten Codes nur einen Thread. Durch Multithreading können Sie die Ausführung auf mehrere Threads verteilen und so (je nach Anwendung) eine höhere Effizienz oder den Eindruck der Parallelität erzielen. Unmanaged Code: siehe managed code. Unsafe Code: siehe safe code. Variable: Eine Variable ist ein Programmelement zur Speicherung von Zahlen, Zeichenketten oder allgemein Objekten. Vererbung: Der Mechanismus der Vererbung erlaubt es, eine neue Klasse so zu definieren, dass sie auf allen Eigenschaften und Merkmalen einer bereits vorhandenen Klasse aufbaut. Die neue Klasse kann dann um zusätzliche Funktionen erweitert bzw. im Vergleich zur ursprünglichen Klasse auch verändert werden. Verwalteter Code: siehe managed code.
Anhang C Dateikennungen Die folgende Tabelle zählt die für VB.NET-Entwickler wichtigsten Dateikennungen auf (soweit sie für dieses Buch relevant sind). *.asp *.aspx *.bat *.bmp *.chm *.c *.cpp *.cs *.csproj *.cur *.dll *.emf *.gif *.exe *.h *.htm[l] *.hxs, *.hxi *.ico *.il *.jpeg, *.jpg *.licx *.mak *.msi *.pdb *.pfm *.png *.rc *.resx *.xx.resx
*.sln *.suo *.tdl *.ttf *.user
ASP-Datei (Active Server Pages auf VBScript-Basis) ASP.NET-Datei (auf der Basis von .NET) Shell-Script-Datei Bitmap-Datei kompilierte HTML-Hilfe-Datei C-Code C++-Code C#-Code C#-Projektdatei Datei, die das Aussehen eines Mauscursors beschreibt Programmdatei für eine Bibliothek oder Komponente (Binärformat) Enhanced-Metafile-Datei (Vektorgrafik) Bitmap-Datei kompiliertes Programm (Binärformat) C-Header-Datei mit Konstantendefinitionen etc. HTML-Datei Hilfe-Datei (MSDN) Icon-Datei MSIL-Quellcode (Microsoft Intermediate Language) Bitmap-Datei Lizenz-Datei Makefile (Datei, die den Compile-Vorgang von C- oder C++-Projekten steuert) komprimierte Installationsdatei für den Microsoft Windows Installer Debugging-Informationen (Binärformat) Type-1-Schriften (PostScript-Schriften) Bitmap-Datei Ressourcen-Datei Ressourcen-Datei im XML-Format; beispielsweise enthält Form1.resx die Resourcen (Bitmaps etc.) zum Formular Form1.vb zusätzliche landesspezifische Ressourcen; beispielsweise enthält Form1.de.resx Zeichenketten zur Anzeige deutschsprachiger Zeichenketten im Formular Projektmappendatei (enthält bei VB.NET-Projekten einen Verweis auf die *.vbproj-Datei) benutzerspezifische Ergänzungen zur VS.NET-Projektdatei (Binärformat) Richtliniendatei (Template Description Language, siehe ms-help://MS.VSCC/MS.MSDNVS.1031/vsent7/html/vxoriEnterpriseTemplates.htm) TrueType- und OpenType-Schriften benutzerspezifische Ergänzungen zu einer Projektdatei
1036 *.vb *.vbproj *.vdproj *.wmf *.xls *.xml
C Dateikennungen
VB-Code (Module, Klassen etc.) VB-Projektdatei (enthält Projekteinstellungen, Verweise auf Bibliotheken sowie auf die Dateien des Projekts) Einstellungen eines Setup-Projekts Windows-Metafile-Datei (Vektorgrafik) XML Stylesheet XML-Datei
Anhang D Inhalt der CD-ROM Beispielprogramme Auf der beiliegenden CD finden Sie sämtliche Beispielprogramme, die in diesem Buch vorkommen. Die Beispielprogramme sind kapitelweise in Verzeichnisse geordnet. So finden sich alle Beispiele zu Kapitel 7 (Objektorientierte Programmierung) im Verzeichnis beispiele\oo-programmierung.
ACHTUNG
Nachdem Sie den gesamten Verzeichnisbaum beispiele von der CD-ROM auf Ihre Festplatte kopieren haben, sind alle Dateien schreibgeschützt. Führen Sie einfach die kleine ScriptDatei beispiele\readwrite.bat aus. Darin befindet sich das DOS-Kommando attrib /s -r *.*, das den Schreibschutz für alle Dateien aufhebt. Sie sehen, selbst in .NET-Zeiten kann ein kleines DOS-Kommando manchmal ganz praktisch sein ... Sämtliche Beispiele wurden unter Windows 2000 entwickelt und getestet. Beachten Sie, dass viele Programme volle .NET-Zugriffsrechte voraussetzen. Sie müssen die Programme also von der lokalen Festplatte starten und sollten Administrator-Rechte haben. Wenn das nicht der Fall ist, treten bei manchen Programmen SecurityException-Fehler auf. Diese Fehler haben mit dem strengen Sicherheitssystem von .NET zu tun und sind keine Programmfehler! Hintergrundinformationen zum .NET-Sicherheitssystem finden Sie in Abschnitt 2.4.
Links zur Online-Hilfe und in das Internet Dieses Buch enthält zahlreiche VERWEIS-Kästen mit Links in das Hilfesystem und in das Internet. Um Ihnen das fehleranfällige Abschreiben der Links zu ersparen, finden Sie auf der CD die Datei links.html mit allen derartigen Links.
.NET-Framework Damit Sie die Beispiele ausführen können, muss auf Ihrem Rechner zumindest das .NETFramework installiert sein. Das ist automatisch der Fall, wenn Sie bereits VB.NET bzw. VS.NET installiert haben. Andernfalls können Sie für erste Experimente zwischen zwei .NET-Framework-Versionen wählen, die sich auf der CD befinden: framework\dotnet-de.exe
enthält die Runtime-Komponenten der deutschen Version des .NET-Frameworks. Das reicht zum Ausführen von .NET-Programmen. Zur Installation führen Sie dieses Programm einfach aus (siehe Abschnitt 18.3.2).
1038 framework\dotnet-sdk-de.exe
D Inhalt der CD-ROM
enthält die Runtime-Komponenten des Frameworks (deutsche Version) sowie die Entwicklungs-Tools (VB.NET- und C#-Compiler etc.) und die gesamte Dokumentation. Nicht enthalten ist allerdings die VB.NET- bzw. VS.NET-Entwicklungsumgebung – die müssen Sie kaufen (siehe Kapitel 2). Achtung: Die Installation setzt Windows NT 4.0, 2000 oder XP voraus! Der Platzbedarf des SDK (Software Development Kit) auf der Festplatte beträgt ca. 350 MByte.
Service-Packs framework\dotnet-sp1-de.exe
enthält das erste verfügbare Service-Pack zum .NETFramework. Es kann für alle deutschen .NETInstallationen verwendet werden (egal ob Sie nur die Runtime-Komponenten, das SDK oder eine VB.NETbzw. VS.NET-Version installiert haben).
framework\dotnet-sp1-en.exe
wie oben, aber für englische .NET-Installationen.
SharpDevelop sharpdevelop\088bsetup.exe
enthält das Setup-Programm zur kostenlosen Entwicklungsumgebung SharpDevelop (BetaVersion 088b, siehe auch Abschnitt 2.7.2).
sharpdevelop\088bSource.zip
enthält den C#-Quellcode zu SharpDevelop.
sharpdevelop\beispiele
enthält zwei einfache Beispielprojekte für SharpDevelop.
Updates und Errata Mögliche Updates und Errata zum Buch bzw. zu den Beispielprogrammen finden Sie auf meiner Website http://www.kofler.cc.
Anhang E Quellenverzeichnis Meine beiden wichtigsten Informationsquellen für dieses Buch waren das Internet und die Online-Hilfe. Darüber hinaus lagen die folgenden Bücher beinahe ständig auf meinem Schreibtisch. Dan Appleman: Moving to VB.NET – Strategies, Concepts, and Code. apress 2001. Brian Bischof: The .NET Languages: A Quick Translation Guide. apress 2002. Gary Cornell, Jonathan Morrison: Programming VB.NET – A Guide for Experienced Programmers. apress 2001. Eric Gunnerson: A Programmer's Introduction to C#. apress 2001. Michael Kofler: Visual Basic 6 – Programmiertechniken, Datenbanken, Internet. Addison Wesley 1999. Holger Schwichtenberg, Frank Eller: Programmierung mit der .NET-Klassenbibliothek. Addison-Wesley 2002. Andrew Troelsen: Visual Basic .NET and the .NET Platform – An Advanced Guide. apress 2001.
Stichwortverzeichnis Generell werden Schlüsselwörter in ihrer Kurzform angegeben. Daher erscheint die Methode System.IO.FileInfo.Exists() im Stichwortverzeichnis einfach als Exists-Methode. Die einzige Ausnahme von dieser Regel sind Namensräume, die sowohl in ihrer vollen Länge als auch in Kurzform eingeordnet sind. Den System.Windows.Forms-Namensraum finden Sie daher gleich dreimal: als System.Windows.Forms-Namensraum, als Windows.Forms-Namensraum und als Forms-Namensraum.
Referenz aller Operatoren 181 := (benannte Parameter) 179 [] (Namenskonflikte) 117 + (Verkettungsoperator) 300 % (Integer) 127 & (Verkettungsoperator) 300 & (Long) 127 &H (hexadezimale Zahlen) 290 &O (oktale Zahlen) 290 <...> (Attribute) 135, 273 *.emf-Dateien 912 *.gif-Dateien (speichern) 905 *.jpeg-Dateien (speichern) 905 *.manifest-Datei 731 *.msi-Dateien 1007 *.sln-Datei 36 *.suo-Datei 36 *.user-Datei 36 *.vb-Datei 36 *.vb-Dateien 214 *.vbproj-Datei 36 *.wmf-Dateien 912 #-Kommandos 45 #Const 45 #ExternalSource 45 #If 45 #Region 45 3D-Aussehen (Steuerelemente) 584 A A-Eigenschaft 851 Abfragen 162 Abkürzungsverzeichnis 1025 Abort-Methode 534 AboveNormal-Konstante 536 Abs-Methode 293
Absolutbetrag 293 AcceptButton-Eigenschaft 597 AcceptsReturn-Eigenschaft 604, 807 AcceptsTab-Eigenschaft 604, 807 AcceptTabs-Eigenschaft 563 AccessibleDescription-Eigenschaft 593 AccessibleName-Eigenschaft 593 AccessibleRole-Eigenschaft 593 Acos-Methode 293 Activated-Ereignis 714 ActivateMdiChild-Methode 769 Active-Eigenschaft ToolTip-Steuerelement 680 ActiveControl-Eigenschaft 713, 769 ActiveLinkColor-Eigenschaft 602 ActiveMdiChild-Eigenschaft 769 ActiveX 107 ActiveX-Automation 519 ActiveX-Steuerelement Lizenzdateien 579 mit Windows.Forms verwenden 578 Setup-Projekte 1007 Adapter-Methode 374 Add-Methode Columns-Klasse 631 Controls-Aufzählung 691 DateTime-Klasse 325 IList-, IDictionary-Schnittstelle 379 Links-Klasse 602 ListBox-Steuerelement 613 ListViewItem-Klasse 631 ListViewSubItem-Klasse 631 TimeSpan-Klasse 324 AddDays-Methode 325 AddEllipse-Methode 918 AddExtension-Eigenschaft 418
1042 AddHandler 264 Beispiel (dynamische Steuerelemente) 692 Beispiel (Panel-Steuerelement) 671 Beispiel (PrintDocument-Klasse) 993 Steuerelemente 727 AddHours-Methode 325 AddLine-Methode 918 AddMinutes-Methode 325 AddMonths-Methode 325 AddRange-Methode ArrayList-Klasse 374 Control.ControlCollection-Klasse 723 ListView-Steuerelement 631 TreeView-Steuerelement 651 AddRectangle-Methode 918 AddressOf-Operator 264, 268 Beispiel (dynamische Steuerelemente) 692 Beispiel (PrintDocument-Klasse) 993 AddSeconds-Methode 325 AddString-Methode 918 AddValue-Methode 462 AddYears-Methode 325 ADO 103 ADO.NET 103 AfterCheck-Ereignis TreeView-Steuerelement 653 AfterCollapse-Ereignis 655 AfterExpand-Ereignis 655 AfterLabelEdit-Ereignis ListView-Steuerelement 641 TreeView-Steuerelement 656 AfterSelect-Ereignis 655 Aktivierreihenfolge 589 Aktuelles Verzeichnis ändern 402 ermitteln 402 Alias (Declare-Anweisung) 549 Alias (Imports) 196 Alignment-Eigenschaft StatusBarPanel-Klasse 802 StringFormat-Klasse 882 TabControl-Steuerelement 672 AllowColumnReorder-Eigenschaft 636 AllowCustomize-Eigenschaft (VB6) 92 AllowDrop-Eigenschaft 818 AllowedEffect-Eigenschaft DragEventArgs-Klasse 820
Stichwortverzeichnis AllowMargins-Eigenschaft 967 AllowOrientation-Eigenschaft 967 AllowPaper-Eigenschaft 967 AllowPrinter-Eigenschaft 967 AllowPrintToFile-Eigenschaft 965 AllowSelection-Eigenschaft 965 AllowSomePages-Eigenschaft 965 AllPages-Konstante 976 AllPaintingInWmPaint-Konstante 946 AllScreens-Eigenschaft 746 Alphakanal Beispiel 908 Bitmaps 907 Farben 851 Alt-Eigenschaft KeyEventArgs-Klasse 804 Alt-Taste 804, 810 AltDirectorySeparatorChar-Eigenschaft 416 Altersberechnung 327 Anakrino 57 Anchor-Eigenschaft 563, 585 vererbtes Formular 730 And-Operator 181 Beispiel (Enum) 136 AndAlso-Operator 181 Andocken von Steuerelementen 587 Animation-Steuerelement (VB6) 93 AnnuallyBoldedDates-Eigenschaft 663 ANSI 422 Programmcode speichern 129 Textdateien 425 Ansi (Declare-Anweisung) 548 AntiAliasGridFit-Konstante 916 Anti-Aliasing (DrawString-Methode) 915 apartment 528 ApartmentState-Eigenschaft 538 Apfelmännchengrafik 936 API-Funktionen 546 VB6-Kompatibilität 106 API-Viewer 552 AppActivate-Methode 517 Appearance-Eigenschaft 598 TabControl-Steuerelement 672 ToolBar-Steuerelement 797 Append-Konstante 447 Append-Methode 312 AppendText-Methode 412, 427, 605 ApplicationException-Klasse 474 Application-Klasse 719, 927
Stichwortverzeichnis Apply-Ereignis 890 App-Objekt 106 Archive-Konstante 410 Arithmetische Funktionen 293 Array-Steuerelemente 578 ArrayList-Klasse 361, 380 Adapter 374 Beispiel 356 Beispiel (ListBox) 615 sortieren und suchen 366 Ascent (Schriften) 872 ASCII-Code 310 Asc-Methode 310 AscW-Methode 309 Asin-Methode 293 ASP.NET 65 Assembly 59 Name 215 AssemblyInfo.vb-Datei 36, 272 AssemblyQualifiedName-Methode 156 Assert-Methode 499 Assoziative Felder 357 Asymmetrische Felder 144 AsyncCallback-Klasse 270 Asynchrone Callbacks 451 Asynchroner Dateizugriff 449 Asynchroner Methodenaufruf (BeginInvoke) 737 AsyncState-Eigenschaft 452 AsyncWaitHandle-Eigenschaft 451 Atan2-Methode 293 Atan-Methode 293 Attribute 272 angeben 273 auswerten 274 Dateien und Verzeichnisse 391 DllImport 552 eigene Steuerelemente 700 Enum 135 MarshalAs 551 NonSerialized 461 PermissionAttribute 515 selbst definieren 273 Serializable 461 StandardModule 197 StructLayout 401, 551 Attribute-Klasse 272 Attributes-Eigenschaft 390, 391 Aufrufliste 494
1043 Aufzählungen 356 durchsuchen 370 Enum-Aufzählungen 134 Multithreading 376 Ausgabetyp 215 Ausnahmen (exceptions) 472 Debugging-Verhalten 496 Ausrichtung von Text 882 Außenmaße von Steuerelementen 584 Auswahlkästchen 598 Auto 548 AutoCheck-Eigenschaft 599 Auto-Fenster 494 AutoFlush-Eigenschaft 429 Automation 519 AutoPopDelay-Eigenschaft 680 AutoRedraw-Eigenschaft (VB6) 932 AutoScaleBaseSize-Eigenschaft 749 AutoScale-Eigenschaft 747 AutoScroll-Eigenschaft 670, 946 TabPage-Klasse 673 AutoSize-Eigenschaft 585, 600 StatusBarPanel-Klasse 801 AutoSize-Konstante 897 AutoZoom-Eigenschaft 996 B B-Eigenschaft 851 Back-Konstante 300 BackColor-Eigenschaft 583 Formular 852 BackgroundImage-Eigenschaft 710, 897 Metafile-Grafik 913 Steuerelemente 582 Backup-Beispielprogramm 406 BannerBitmap-Eigenschaft 1015 Base Class Library 54 Basislinie (Schriften) 872 BCL 54 Bedingungen 162 Bedingungen (Setup-Projekt) 1017 Befehlsfenster 45 BeforeCheck-Ereignis 653 BeforeCollapse-Ereignis 655 BeforeExpand-Ereignis 655 BeforeLabelEdit-Ereignis ListView-Steuerelement 641 TreeView-Steuerelement 656 BeforeSelect-Ereignis 655
1044 BeginEdit-Methode ListViewItem-Klasse 640 TreeNode-Klasse 656 BeginInvoke-Methode 737 BeginPrint-Ereignis 962 BeginRead-Methode 451 BeginUpdate-Methode ListBox-Steuerelement 614 ListView-Steuerelement 631 TreeView-Steuerelement 651 BeginWrite-Methode 450 BelowNormal-Konstante 536 Benannte Parameter 179 Benutzername 509 Benutzeroberfläche des Installationsprogramms 1015 Berechtigungssätze 62 Besondere Verzeichnisse 402 Betriebssystem Eigenschaften feststellen 745 Funktionen feststellen 746 Version ermitteln 510 Bezierkurve zeichnen 848 Bibliotheken anwenden 185 Überblick 54 Verweise einrichten 193 Bildschirmanzahl 746 Bildschirmauflösung 746 DPI (Textausgabe) 872 DPI (Windows.Forms) 747 bin-Verzeichnis 36 Binärdateien lesen und schreiben 435 Serialisierung 458 BinaryReader-Klasse 444 BinarySearch-Methode 370 ArrayList-Klasse 380 Felder 143, 371 BinaryWriter-Klasse 444 BitArray-Klasse 363 BitBlt-Funktion (GDI) 951 Bitfelder 363 Bitmap 894 als Grafikspeicher für ein Steuerelement 932 Datei laden/speichern 905 Definition 895 drucken 952
Stichwortverzeichnis durchsichtige 907 erzeugen 900 in Formularen darstellen 896 in Steuerelementen darstellen 896 kopieren 901 manipulieren 900 mit dem Programm mitliefern 899 Muster (TextureBrush-Klasse) 857 Qualität beim Speichern 906 über Byte-Feld ansprechen 954 vom Fensterinhalt 951 Bitmap-Klasse 900 Grundlagen 894 über Byte-Feld ansprechen 954 BitmapData-Klasse 954 BitVector32-Klasse 363 BlinkRate-Eigenschaft 687 BlinkStyle-Eigenschaft 687 BoldedDates-Eigenschaft 663 Bold-Konstante 880 BOM (byte order mark, Unicode) 423 Boolean-Klasse 127 BorderStyle-Eigenschaft 583, 600, 603 siehe auch FormBorderStyle 711 StatusBarPanel-Klasse 802 TabPage-Klasse 673 Bottom-Eigenschaft Margins-Klasse 972 Steuerelement 584, 928 Bounds-Eigenschaft Screen-Klasse 746, 760 Steuerelemente 585 boxed value types 147 boxing (ValueType-Klassen) 147 break point 492 Browsable-Attribut 700 Brushes-Klasse 857 Brush-Klasse 856 BufferedStream-Klasse 441 Build-Eigenschaft 510 ButtonBase-Klasse 596 ButtonClick-Ereignis (ToolBarButton) 797 ButtonDropDown-Ereignis 798 Buttons-Eigenschaft (ToolBarButton) 797 Buttons-Konstante 672 Button-Steuerelement 596 ByRef 174 Byte-Klasse 127 ByVal 174
Stichwortverzeichnis C C# versus VB.NET 67 CalenderDimension-Eigenschaft 662 Call 169 Callback-Prozedur 451 CancelButton-Eigenschaft 597 CancelEdit-Eigenschaft 641 Cancel-Eigenschaft Formular 568 PrintPageEventArgs-Klasse 963 CanDuplex-Eigenschaft 976 Capacity-Eigenschaft 313, 380 CapsLock-Zustand 92 Carriage Return 300 Case 162 Casting (CType-Funktion) 335 Casting-Operator 157 Catch 479 Category-Attribut 700 CBool-Funktion 334 CByte-Funktion 295, 334 CChar-Funktion 334 CDate-Funktion 334 CDbl-Funktion 334 CDec-Funktion 334 Ceiling-Methode 294, 842 CenterImage-Konstante 897 Center-Konstante 882 CenterParent-Konstante 713, 760 CenterScreen-Konstante 713 Changed-Ereignis 456 ChangeExtension-Methode 405 CharacterCasing-Eigenschaft 604 Char-Klasse 129, 298 IsXxx-Methoden 317 Konstanten 300 CharSet-Klassenvariable 552 ChDir 402 ChDrive 402 CheckAlign 598 CheckBox-Steuerelement 598 Beispiel (Controls-Schleife) 690 CheckBoxes-Eigenschaft ListView-Steuerelement 636 TreeView-Steuerelement 653 Checked-Eigenschaft 599 DateTimePicker-Steuerelement 664 ListViewItem-Klasse 637 MenuItem-Klasse 783
1045 TreeNode-Klasse 653 CheckedChange-Ereignis 599 CheckedIndexCollection-Klasse 629 CheckedIndices-Eigenschaft 624, 641 CheckedItems-Eigenschaft 624, 637, 641 CheckedListBox-Steuerelement 624 CheckedListViewItemCollection-Klasse 629 CheckFileExists-Eigenschaft 418 CheckOnClick-Eigenschaft 624 CheckPathExists-Eigenschaft 418 CheckState-Eigenschaft 599 Choose-Methode 163 Chr-Methode 310 ChrW-Methode 309 CInt-Funktion 295, 334 Clamp-Konstante 858 Class 218 LinkedList-Beispiel 220 ListViewItem-Beispiel 634 Clear-Methode 365, 846 Columns-Klasse 631 Felder 142 Graphics-Klasse 852 IList-, IDictionary-Schnittstelle 379 ListViewItem-Klasse 631 ClearSelected-Methode 619 ClearTypeGridFit-Konstante 916 Click-Ereignis 597, 811 MDI-Hauptfenster 769 MenuItem-Klasse 784 ClientRectangle-Eigenschaft 585 ClientSize-Eigenschaft 585, 925, 928 Clipboard-Klasse 814 Beispiel 952 ClipBounds-Eigenschaft 924 Clip-Eigenschaft 920, 924 Clipping 920 Paint-Ereignis 923 ClipRectangle-Eigenschaft 834, 924 CLng-Funktion 295, 334 CloneMenu-Methode 784 Clone-Methode 124 Felder 142 Close-Methode 437 Formulare 570 MDI-Anwendungen 771 Stream-Klassen 427 StringWriter-Klasse 433 Closed-Ereignis 714
1046 Fenster 571 CloseMainWindow-Methode 518 CloseUp-Ereignis DateTimePicker-Steuerelement 664 Closing-Ereignis 568, 714 Fenster 571 MDI-Anwendungen 771 CLR 60, 1029 CLS 60, 131, 1029 CObj-Funktion 334 Codedatei 214 Unicode 129 Codeeingabe 34 Codegruppen 62 Coderegion zusammenklappen 45 Codezugriffssicherheit 62 Collate-Eigenschaft 976 Collect-Methode 150 CollectionBase-Klasse 364 Collections.Specialized-Namensraum 356 Collections-Namensraum 356 Color-Aufzählung 850 ColorDepth-Eigenschaft 611, 899 ColorDialog-Steuerelement 852 Color-Eigenschaft (PrintSettings) 975 Color-Klasse 850 SortedList-Beispiel 372 ColumnClick-Ereignisprozedur 639 ColumnHeaderCollection-Klasse 629 ColumnHeader-Klasse 629 Columns-Eigenschaft 630, 996 ColumnWidth-Eigenschaft 612 COM 107 COM-Automation 519 COM-Steuerelement mit Windows.Forms verwenden 578 ComboBox-Steuerelement 625 Auto-Complete-Funktion 627 ComboBoxStyle-Aufzählung 625 CommandButton-Steuerelement (VB6) 88 CommandLine-Eigenschaft 509 Command-Methode 509 Common Language Runtime 60 Common Language Specification 60, 131 Common Type System 60 CommonControls-Steuerelemente (VB6) 90 CommonDialog-Klasse 779 CommonDialog-Steuerelement (VB6) 91 CompareExchange-Methode 540
Stichwortverzeichnis Compare-Methode 306 CompareTo-Methode 366 Compiler 74 #-Kommandos 45 Debug- versus Release-Kompilat 490 Complement-Methode 919 ComponentModel-Namensraum 700 CompositionMode-Eigenschaft 915 CompositionQuality-Eigenschaft 916 Compressed-Konstante 410 Condition-Eigenschaft 1018 Console-Klasse 33, 506 Const 132 ContainsFocus-Eigenschaft 589 ContainsKey-Methode 381 Contains-Methode 842, 863 Hashtable-Klasse 381 IList-, IDictionary-Schnittstelle 379 TreeNodeCollection-Klasse 653 ContextMenu-Eigenschaft 786 ContextMenu-Klasse 786 Control.ControlCollection-Klasse 690 ControlBox-Eigenschaft 711 ControlChars-Klasse 300 Control-Eigenschaft 804 ControlPaint-Klasse 929 Controls-Eigenschaft 592, 713 Formular/Container 690 ControlStyle-Aufzählung 945 Conversion-Klasse 294, 334 Convert-Klasse 131, 336 CoolBar-Steuerelement (VB6) 93, 579 Copies-Eigenschaft 976 Copy-Konstante 818 Copy-Methode 142, 399, 413 Marshal-Klasse 954 TextBox-Steuerelement 605 CopyMenu-Methode 784 CopyrightWarning-Eigenschaft 1015 CopyTo-Methode 399, 413 Aufzählungen 373 Cosh-Methode 293 Cos-Methode 293 Created-Ereignis 456 CreateDirectory-Methode 390, 398, 412 CreateGraphics-Methode 896 CreateInstance-Methode 144 Create-Konstante 447 Create-Methode 412
Stichwortverzeichnis CreateNew-Konstante 447 CreateObject-Methode 522, 526 CreateSubdirectory-Methode 412 CreateText-Methode 427 Cr-Konstante 300 CrLF-Konstante 300 CrossAppDomainDelegate-Klasse 270 CShort-Funktion 295, 334 CSng-Funktion 334 CStr-Funktion 334 CTS 60, 1029 CType-Funktion 157, 335 Beispiel (IComparable-Schnittstelle) 367 Clone-Methode 142 dynamische Steuerelemente 692 Enum 137, 404 IComparer-Schnittstelle 368, 392, 638 ListBox-Steuerelement 616, 621 SortedList-Klasse 372 Tag-Eigenschaft 634 Windows.Forms-Ereignisse 565 Culture-Eigenschaft 746 CultureInfo-Klasse 306, 340 Sprache einstellen 754 CurDir 402 Currency (VB6) 128 CurrentCulture-Eigenschaft 536, 754 CurrentDirectory-Eigenschaft 402 Current-Eigenschaft 378 CurrentEncoding-Eigenschaft 424 CurrentInputLanguage-Eigenschaft 746 CurrentThread-Eigenschaft 754 CurrentUICulture-Eigenschaft 754 Cursor-Eigenschaft Control-Klasse 809 Steuerelemente 584, 591 Cursor[s]-Klasse 584, 809 CustomFormat-Eigenschaft 664 Cut-Methode 605 D DashPattern-Eigenschaft 854 DashStyle-Eigenschaft 853 DataCombo-Steuerelement (VB6) 91 DataFormats-Aufzählung 815 DataGrid-Steuerelement (VB6) 91, 660 DataList-Steuerelement (VB6) 91 DataRepeater-Steuerelement (VB6) 93 DataReport-Steuerelement (VB6) 91
1047 DataSource-Eigenschaft 615 DateAdd-Methode 325 DateAndTime-Klasse 321 DateChanged-Ereignis 663 Date-Datentyp 129, 319, 321 DateDiff-Methode 323 Date-Eigenschaft 322 Datei 384, 389 ANSI-Textdatei öffnen 425 anzeigen (FileListBox) 385 anzeigen per LinkLabel 601 asynchroner Zugriff 449 Attribute 410 Binärdateien 435 Codierung ändern 431 ermitteln (in einem Verzeichnis) 391 erzeugen 398 Größe ermitteln 394 Größe von komprimierten Dateien 555 Kennung 405 kopieren 399 Länge (FileStream) 437 locking 439 löschen 399 Mehrfachzugriff 439 Name kürzen (Textausgabe) 882 schließen 427, 437 Serialisierung 458 sharing 439 sicher löschen 400 sortieren 392 Textdateien 421 Unicode-Textdatei öffnen 423 Veränderungen feststellen 456 verschieben/umbenennen 399 zeilenweise auslesen 425 zu langer Dateiname 469 Zugriffsrechte 402 Dateiname zu lang 469 Dateisystem überwachen 456 Dateisystemfenster (Setup-Projekt) 1012 Dateityp registrieren 1020 Dateizeiger 437 Datenbankprogrammierung 103 Datentypen 127 automatische Konvertierung 331 Kennzeichnung 290 DatePart-Methode 322 DateSelected-Ereignis 663
1048 DateSerial-Methode 320 DateTimeFormatInfo-Klasse 340 DateTime-Klasse 129, 319, 321 DateTimePicker-Steuerelement 689 Datum 319 Date-Datentyp 129 DateTimePicker-Steuerelement 662 formatieren 347, 352 MonthCalender-Steuerelement 662 Day-Methode 322 DayOfYear-Eigenschaft 322 DaysInMonth-Methode 322 DBGrid-Steuerelement (VB5) 93 DDE (Dynamic Data Exchange) 106 Deactivated-Ereignis 714 Deadlock 545 Deadlock-Beispiel 738 Debug-Klasse 497 Debug-Kompilat 490 DebuggerStepThrough-Attribut 723 Debugging 490 Einzelschritt 493 Haltepunkte 492 Paint-Prozedur 835 Variablen überwachen 494 Decimal-Klasse 128 Rundungsfehler 291 DecimalPlaces-Eigenschaft 668 Declare 547 Syntax 548 VB6-Kompatibilität 106 Decrement-Methode 540 Default-Konstante 916 Defaultbutton 597 Defaulteigenschaft 97 DefaultEvent-Attribut 700 DefaultInputLanguage-Eigenschaft 746 DefaultMenuItem-Eigenschaft 782 Defaultnamensraum 215 DefaultPageSettings-Eigenschaft 972 Delegate-Klasse 267 Beispiel 939 Ereignisse deklarieren 270 Multithreading-Beispiel 735 TimerCallback-Klasse 531 Delete-Methode 399, 413 Deleted-Ereignis 456 Delta-Eigenschaft 813 Demand-Methode 515
Stichwortverzeichnis Descent (Schriften) 872 Description-Attribut 700 Deserialisierung 458 Deserialize-Methode 461 Design-Namensraum 419, 832 Desktop-Größe ermitteln 745 Desktop-Icon (Setup-Projekt) 1014 Desktop-Koordinatensystem 929 DesktopBounds-Eigenschaft 712 DesktopLocation-Eigenschaft 712 Details-Konstante 635 Device Context (GDI) 894 Device-Konstante 410 DeviceName-Eigenschaft 746 Dezimalnotation 290 Dezimalpunkt 344 Dezimaltrennzeichen feststellen 346 Diagnostics-Namensraum 150, 516, 537 Dialog 560 bei Mausposition anzeigen 760 mehrblättrig 672 selbst verwalten 758 teilen (Splitter) 674 Dialogblätter 672 DialogResult-Aufzählung 758, 779 DialogResult-Eigenschaft 597 DictionaryBase-Klasse 364 DictionaryEntry-Klasse 357 Dictionary-Klasse (VB6) 83 Dim 116 As New 122 Felder 139 DirectionVertical-Konstante 881 Directory-Klasse 389 Directory-Konstante 410 DirectoryEntry-Klasse 385 DirectoryInfo-Klasse 389 DirectoryNotFoundException-Klasse 469 DirectorySearcher-Klasse 385 DirectorySeparatorChar-Eigenschaft 416 Direktiven für den Compiler 45 DirListBox-Steuerelement 385, 578 Display-Konstante 860 Display-Konstante (Drucken) 971 DisplayMember-Eigenschaft 615 Dispose-Methode 148 Fehlerabsicherung 485 Form-Klasse 722, 758 Grafikobjekte 836
Stichwortverzeichnis selbst implementieren 259 StringWriter-Klasse 433 Division durch 0 (Double/Single-Zahlen) 292 Rest 294 DLL-Funktionen 546 DllImport-Attribut 552 Dock-Eigenschaft 587 MDI-Fenster 772 Splitter-Steuerelement 674 vererbtes Formular 730 DockPadding-Eigenschaft 587 Document-Eigenschaft 965 Document-Konstante 860 DocumentName-Eigenschaft 962 DoDragDrop-Methode 818 DoEvents-Methode 720 Dokument anzeigen 517 LinkLabel-Steuerelement 601 Do-Loop-Schleifen 166 DomainUpDown-Steuerelement 669 DomainUpDownItemCollection-Klasse 669 Doppeldeutigkeiten (Imports) 196 DOS-Codierung (OEM 850) 425 dotnetfx.exe 1010 double buffering (Grafik) 944 Double-Klasse 128 Rundungsfehler 291 DoubleBuffer-Konstante 945 DoubleClick-Ereignis 811 TreeView-Steuerelement 655 DoubleClickTime-Eigenschaft 745 DPI-Einstellung Drucken 975 Schriftarten 872 Windows.Forms 747 DpiX-Eigenschaft 750 DpiY-Eigenschaft 750 Drag&Drop 817 DragDropEffects-Aufzählung 818 DragDrop-Ereignis 819 DragEnter-Ereignis 819 DragEventArgs-Klasse 820 DragFullWindows-Eigenschaft 745 DragItem-Ereignis 818 DragLeave-Ereignis 819 DragOver-Ereignis 819 DrawArc-Methode 846 DrawBackGround-Methode 793
1049 DrawBezier-Methode 848 DrawBeziers-Methode 849 DrawClosedCurve-Methode 847 DrawCurve-Methode 847 DrawEllipse-Methode 846 DrawIcon-Methode 911 DrawImage-Methode 895, 901 Metafile-Grafik 913 DrawImageUnscaled-Methode 902, 952 Drawing.Design-Namensraum 832 Drawing.Drawing2D-Namensraum 832 Drawing.Imaging-Namensraum 832, 913 Drawing.Text-Namensraum 832 Drawing2D-Namensraum 832 Drawing-Namensraum 832 DrawItem-Ereignis 620 MenuItem-Klasse 789 TabControl-Steuerelement 673 DrawItemEventArgs-Klasse 620 DrawMode-Eigenschaft ListBox-Steuerelement 620 TabControl-Steuerelement 673 DrawPath-Methode 918 DrawPie-Methode 846 DrawRectangles-Methode 842 DrawReversibleFrame-Methode 929 DrawReversibleLine-Methode 930 DrawRotatedString-Beispiel 888 DrawString-Methode 867 Ausgabequalität steuern 915 Drehfeld zur Datumsauswahl 664 zur Listenauswahl 669 zur Zahleneingabe 668 DriveListBox-Steuerelement 385, 578 DropDownArrow-Eigenschaft 798 DropDownButton (ToolBar-Steuerelement) 798 DropDown-Eigenschaft 627 DropDown-Ereignis 664 DropDown-Konstante 625 DropDownList-Konstante 625 DropDownMenu-Eigenschaft 798 DropDownStyle-Eigenschaft 625 DropDownWidth-Eigenschaft 626 DroppedDown-Eigenschaft 626 Drucken Ausdruck abbrechen 963 Bitmap 952
1050 Drucker einrichten 973 Druckerauswahl 965 Eigenschaften 975 Hoch-/Querformat 994 in Datei 995 Liste aller Drucker ermitteln 993 mehrseitiger Ausdruck 963 Seite einrichten 967, 972 Seitengröße 971 Seitenvorschau 969 Seitenvorschau, selbst programmieren 996 Statusdialog vermeiden 995 Textbox-Steuerelement 981 Textdokument 981 Druckereinstellungen 976 Druckvorschau 969 DTPicker-Steuerelement (VB6) 92 Duplex-Eigenschaft 976 Durchsichtige Bitmaps 907 Durchsichtige Steuerelemente 583 Durchsichtiges Fenster 572 Dynamic Data Exchange 106 Dynamische Hilfe 44 E E (eulersche Zahl) 293 early binding 521 Effect-Eigenschaft 820 Eigenschaften Eigenschaftsfenster 563 Glossar 186 programmieren 230 Eingabefokus 589 Einzelschritt 493 Ellipse zeichnen 846 EllipsisPath-Konstante 882 EllipsisWord-Konstante 882 Else 162 in Select-Case 162 ElseIf 162 E-Mail versenden (per LinkLabel) 601 E-Mail-Programm starten 517 EM-Box 872 EMF+Dual-Format 912 EMF+Only-Format 912 Enable-Eigenschaft 583 Enabled-Eigenschaft MenuItem-Klasse 782
Stichwortverzeichnis Timer-Steuerelement 678 EnableRaisingEvents-Eigenschaft 456 Encoding-Klasse 425 Encrypted-Konstante 410 End 514 Class 218 Enum 134 Function 168 Get 230 If 162 Interface 257 MDI-Anwendungen 771 Module 239 Namespace 276 Property 230 Select-Case 162 Set 230 Structure 242 Sub 168 SyncLock 539 Try 479 With 189 EndCap-Eigenschaft 855 EndInvoke-Methode 737 Endlosschleife 166 EndPrint-Ereignis 962 EndRead-Methode 451 EndsWith-Methode 304 EndUpdate-Methode ListBox-Steuerelement 614 ListView-Steuerelement 631 TreeView-Steuerelement 651 EndWrite-Methode 451 Enhanced Metafile Format 912 Enter-Ereignis 589, 604 Enter-Methode 540 EntryPoint-Klassenvariable 552 Entwicklungsumgebung 31, 43, 69 Enum 134 Flags-Kombinationen 135 Environment-Klasse 402, 404, 508 Environ-Methode 508 Epsilon-Eigenschaft 292 Eqv-Operator (VB6) 85 Erase 141 Ereignis 262 Glossar 186 message loop (Windows.Forms) 719, 720 mit Delegate deklarieren 270
Stichwortverzeichnis Steuerelemente 565, 581 Windows.Forms 561 Ereignisprozedur dynamisch zuweisen 727 für mehrere Steuerelemente 727 Steuerelemente 565, 726 Windows.Forms 564 Err-Funktion 488 ErrObject-Klasse 488 Error-Ereignis 457 ErrorProvider-Steuerelement 686 Event 263 EventHandler-Klasse 270 EventXxx-Klasse 565 Excel (Automation) 524 Exception-Klasse 474 Debugging-Verhalten 496 vererben 477 ExceptionState-Eigenschaft 534 Exchange-Methode 540 Exclude-Methode 919 Exists-Methode 390, 413 Exit Do 166 For 165 Function 168 Property 230 Sub 168 Exit-Methode Application-Klasse 720 Monitor-Klasse 540 ExitThread-Methode 767, 928 Exp-Methode 293 Exponentialdarstellung 344 Extension-Eigenschaft 390 Externes Programm starten 516 F Fail-Methode 499 Families-Methode 869 Far-Konstante 882 Farben 850 FCL 54 Feature-Eigenschaft 712, 746 Fehler 472 Debugging-Verhalten 496 Fehlerabsicherung 472, 479, 484 asynchrone Dateioperationen 452 Beispiel: Apfelmännchen 941
1051 Beispiel: Font 622 Beispiel: Process.Start 602 Beispiel: Verzeichnisbaum anzeigen 658 Beispiel: zu lange Dateinamen 469 Beispiel: Verzeichnisbaum 395 Multithreading 535 Fehlerindikator (Icon) 686 Fehlersuche siehe auch Debugging 490 Felder 139 assoziative 357 asymmetrische 144 durchsuchen 143, 371 für Bits 363 Initialisierung 139 Parameter von Prozeduren 177 speichern (serialisieren) 463 Speicherverbrauch 152 Typenkonvertierung (CType) 335 Fenster 560 andocken (MDI) 772 anordnen (MDI) 786 durchsichtig 572, 712 dynamisch erzeugen 727 Ereignisreihenfolge 715 Interna 718 klonen 767 MDI-Anwendungen 768 mehrere Fenster verwalten 757 modal anzeigen 758 nichtrechteckige 573 ohne Paint-Ereignis zeichnen 925 ohne Rahmen 572 Paint-Ereignis manuell auslösen 924 Programmstart (Interna) 719 Rahmen 711 Spezialeffekte 572 Tastaturereignisse 807 teilen 674 Titel (Text-Eigenschaft) 710 Toolbox-Rahmen 711 Vererbung 729 Windows-XP-Optik 730 Festkommazahlen 128 Fette Schrift 880 FileAccess-Aufzählung 447 FileAttributes-Aufzählung 391, 410 FileDialog-Klasse 417 FileInfo-Klasse 389 FileIOPermission-Klasse 402, 515
1052 File-Klasse 389 FileListBox-Steuerelement 385, 578 FileLoadException-Klasse 469 FileMode-Aufzählung 447 FileName[s]-Eigenschaft 418 FileNotFoundException-Klasse 469 FileShare-Aufzählung 448 FileStream-Klasse 436 mit BufferedStream optimieren 441 FileSystemInfo-Klasse 389 FileSystem-Klasse 384 FileSystemWatcher-Klasse 456 FillMode-Eigenschaft 918 FillPath-Methode 918 FillReversibleRectangle-Methode 930 FillToRight-Konstante 672 FillXxx-Methoden 850 Filter-Eigenschaft 418, 457 Filter-Methode 303 Finalize-Methode Beispiel 224, 260 selbst implementieren 223 Finally 481 FirstDayOfWeek-Eigenschaft 663 FirstNode-Eigenschaft 653 Fixed3D-Konstante 711 FixedDialog-Konstante 711 Fixed-Konstante 672 FixedPitchOnly-Eigenschaft 891 FixedSingle-Konstante 711 FixedSize-Methode 374 FixedToolWindow-Konstante 711 Fix-Methode 294 Flags-Attribute 135 FlatButtons-Konstante 672 FlatScrollBar-Steuerelement (VB6) 93 FlatStyle-Eigenschaft 584 Windows-XP-Optik 731 FlexGrid-Steuerelement (VB6) 91 Fließkommazahlen 128, 290 formatieren 343, 350 Flimmerfreie Grafik 944 Floor-Methode 294 Flush-Methode 429, 437, 808 SendKeys-Klasse, Beispiel 952 FocusedItem-Eigenschaft 641 Focus-Methode 590 Fokus (Eingabefokus) 589 FolderBrowserDialog-Klasse 420
Stichwortverzeichnis FolderBrowser-Klasse 419 FontDialog-Steuerelement 890 Font-Eigenschaft 581 FontFamily-Eigenschaft 869 FontFamily-Klasse 868 Font-Klasse 867 Größe 871 PostScript-Schriften 868 Fonts 867 FontStyle-Aufzählung 880 ForeColor-Eigenschaft 583 Form Designer 722 verschwundene Steuerelemente 726 Form-Klasse 710 Ereignisreihenfolge 715 siehe auch Formulare 710 FormatCurrency-Methode 350 FormatDateTime-Methode 352 Format-Eigenschaft 664 Format-Methode Datumsformatierungscodes (.NET) 347 Datumsformatierungscodes (VB) 352 String-Klasse (.NET) 341 Strings-Klasse (VB) 349 Zahlenformatierungscodes (.NET) 343 Zahlenformatierungscodes (VB) 351, 352 Formatierung 339 Daten und Zeiten 347, 352 länderspezifisch 340 landesunabhängig 341 Serialisierung 459 Zahlen 343, 350 FormatNumber-Methode 350 FormatPercent-Methode 351 FormBorderStyle-Eigenschaft 711 FormFeed-Konstante 300 Forms.Design-Namensraum 419 Forms-Namensraum 560, 929 Klassenhierarchie 573 Steuerelemente 576 FormStartPosition-Aufzählung 760 Formular 560 dynamisch erzeugen 727 Interna 718 ohne Paint-Ereignis zeichnen 925 Paint-Ereignis manuell auslösen 924 Steuerelemente dynamisch einfügen 691 Tastaturereignisse 807
Stichwortverzeichnis Vererbung 729 Windows-XP-Optik 730 For-Next-Schleife 165 Fortschrittsanzeige 667 FrameBorderSize-Eigenschaft 745 Frame-Steuerelement (VB6) 89 Framework 53, 1030 Architektur 55 Class Library 54 installieren 1010 free threading 528 Friend 277 Modifier-Eigenschaft 725 FromArgb 851 FromDays-Methode 324 FromHours-Methode 324 FromHwnd-Methode 896, 925 FromImage-Methode 895, 901 FromMinutes-Methode 324 FromPage-Eigenschaft 976 FromSeconds-Methode 324 FSO-Bibliothek 384 Füllmuster (Brush-Klasse) 856 Farbverläufe 858 Texturen 857 vordefinierte Muster 858 FullName-Eigenschaft 390 FullName-Methode 156 FullPath-Eigenschaft 654 Function 168 Funktionen 167 arithmetische 293 Methoden 222 Rekursion 172 Zeiger (delegates) 267 G G-Eigenschaft 851 GAC 59 Ganze Zahlen 127 garbage collection 147, 148 Finalize und Dispose 259 Finalize-Methode 223 manuell auslösen 150 GC-Klasse 150 GDI+ 832, 1031 Geschwindigkeit 948 PostScript-Schriften 868 Schriftarten 868
1053 siehe auch Grafik 832 GDI32 554 Geerbte Formulare 729 GenericDefault-Eigenschaft 880 GenericTypographics-Eigenschaft 880 Get (eigene Eigenschaften) 230 Get-Methode (ManagementObjectSearcher) 512 GetBaseException-Methode 475 GetBounds-Methode 924 GetBrightness-Methode 851 GetByIndex-Methode 381 GetBytes-Methode 311 GetCellAscent-Methode 873 GetCellDescent-Methode 873 GetChildAtPoint-Methode 592, 713 GetCommandLineArgs-Methode 1020 GetCurrentProcess-Methode 150, 537 GetCustomAttributes-Klasse 274 GetData-Methode 815, 952 GetDataObject-Methode 814 GetDataPresent-Methode 815 GetDeviceCaps (API-Funktion) 554 GetDirectories-Methode 393 GetDirectoryName-Methode 405 GetEmHeight-Methode 873 GetEncoding-Methode 425 GetEnumerator-Methode 378 GetEnvironmentVariable (API-Funktion) 547 GetEnvironmentVariable[s]-Methode 508 GetException-Methode 488 GetExecutingAssembly-Methode 403 GetExtension-Methode 405 GetFileName-Methode 405 GetFileNameWithoutExtension-Methode 405 GetFiles-Konstante 391 GetFiles-Methode 393 FileIOPermission 514 GetFileSystemEntries-Methode 393 GetFileSystemInfos-Methode 393 GetFolderPath-Methode 404 GetFormats-Methode 814 GetFullPath-Methode 405 GetHashCode-Methode 304 eigene Klassen 361 GetHdc-Methode 555 GetHeight-Methode 874 GetHue-Methode 851 GetImageEncoders-Methode 906
1054 GetInteger-Methode 462 GetKeyList-Methode 381 GetKey-Methode 381 GetLineSpacing-Methode 873 GetLogicalDrives-Methode 405 GetLowerBound-Methode 140, 177 GetNames-Methode 137 GetNodeAt-Methode 655 GetObjectData-Methode 462 GetObject-Methode 522 GetPathRoot-Methode 405 GetRange-Methode 380 GetRegionScans-Methode 924 GetSaturation-Methode 851 GetScaleBaseSize-Methode 749 GetSelected-Methode 618 GetStringBuilder-Methode 432 GetString-Methode 462, 756 GetStyle-Methode 945 GetTempFileName-Methode 403 GetTempPath-Methode 403 GetTotalMemory-Methode 150 GetType-Klasse 274 GetType-Methode 156 Beispiel (Enum) 137 GetType-Operator 157 GetUpperBound-Methode 140, 177 GetValueList-Methode 381 GetValue-Methode 144, 462 GetVersionPresent-Methode 746 GIF-Datei speichern 905 Glättung (von Grafik und Text) 915 global assembly cache 59 Globale Variablen 240 Globalization (Kalender) 321 Globalization-Namensraum 306, 340, 968 Sprache einstellen 754 GMT (Greenwich mean time) 321 GotFocus-Ereignis 589 Grafik 832 Container 835 Flimmern vermeiden 944 Fonts 867 Geschwindigkeit 948 in Steuerelementen 582 Koordinatensystem ändern 859 Methoden 840 scrollen 946 Steuerelemente 610
Stichwortverzeichnis Text 867 Transformationen 859 Graphics-Eigenschaft 833, 971 Graphics-Klasse 834 Ausgabequalität steuern 915 Dispose-Methode 839 Grafikmethoden 840 Grundlagen 894 Punkt zeichnen 845 Zeichenqualität steuern 915 GraphicsPath-Klasse 918 GraphicsUnit-Aufzählung 860 Gregorianischer Kalender 321 GridLines-Eigenschaft 637 Größe (Grafikobjekt) 841 GroupBox-Steuerelement 669 GUID-Klasse 131 Gültigkeitsbereiche (scope) 277 H Hailstorm 66 Haltepunkte 492 Handled-Eigenschaft KeyDown-Ereignis 805 KeyPress-Ereignis 805 Handle-Eigenschaft 896, 925 Handles 264 für mehrere Ereignisse gleichzeitig 727 Windows.Forms-Ereignisprozeduren 565 HasExtension-Eigenschaft 415 Hashtable-Klasse 361, 381 Beispiel 357 hash-Wert (HashTable) 361 hash-Wert (String) 304 HasMorePages-Eigenschaft 963 HatchBrush-Klasse 858 hcw.exe (HTML Help Workshop) 682 HDC (Grafik-Handle) 555 HeaderStyle-Eigenschaft 637 heap-Speicher 147 Hebräischer Kalender 321 Height-Eigenschaft BitmapData-Klasse 954 Font-Objekt 874 Steuerelement 585, 928 Hello-Windows-Beispielprogramm 37 Hello-World-Beispielprogramm 30 HelpButton-Eigenschaft 684, 711 HelpContextID-Eigenschaft (VB6) 683
Stichwortverzeichnis HelpKeyword-Eigenschaft 683, 685 Help-Klasse 685 HelpNavigator-Aufzählung 683 HelpNavigator-Eigenschaft 683, 685 HelpProvider-Steuerelement 682 HelpString-Eigenschaft 684, 685 Hex-Methode 290 Hexadecimal-Eigenschaft 668 Hexadezimale Zahlen 290 Hidden-Konstante 410 Hide-Methode 583, 711 Hierarchisches Listenfeld 649 HighQuality-Konstante 916 HighSpeed-Konstante 916 Hilfe eigene Hilfetexte anzeigen 682 HTML-Datei 684 ID-Nummer 683 Popup-Fenster 684 Website 684 Hintergrundberechnung DoEvents-Methode 720 Multithreading (Apfelmännchen) 936 Hintergrundfarbe einstellen (Graphics) 846 Hintergrundthread 536 Hochformat (Drucken) 994 Hostname 509 HotkeyPrefix-Eigenschaft 882 Hour-Methode 322 HoverSelection-Eigenschaft 637 HSB-Farbmodell 851 HScrollBar-Steuerelement 665 HTML Help 682 HTML Help Workshop 682 HybridDictionary-Klasse 362 I IAsyncResult-Schnittstelle 451, 737 IClonable-Schnittstelle 259 IComparable-Schnittstelle 366 IComparer-Schnittstelle 368 ListView-Beispiel 638 ICompare-Schnittstelle Beispiel 392, 647 Icon-Eigenschaft 910 ErrorProvider-Steuerelement 687 Fenster 710 NotifyIcon-Steuerelement 688 StatusBarPanel-Klasse 802
1055 Icon-Klasse 911 IconSize-Eigenschaft 745 IConvertible-Schnittstelle 259 IDataObject-Schnittstelle 814 IDictionary-Schnittstelle 379 IDisposable-Schnittstelle 148 Fehlerabsicherung 485 selbst implementieren 259 IEEERemainder-Methode 294 IEnumerable-Schnittstelle 378 IEnumerator-Schnittstelle 378 IFormatable-Schnittstelle 259 IFormatProvider-Schnittstelle 340 If-Then-Else-Verzweigung 162 ildasm.exe 56 IList-Schnittstelle 379 Image-Eigenschaft Metafile-Grafik 913 PictureBox-Steuerelement 610 Steuerelement 582, 897, 902 Image-Klasse 894 Image-Steuerelement (VB6) 93 ImageAlign-Eigenschaft 582, 610 ImageCodecInfo-Klasse 906 ImageCombo-Steuerelement (VB6) 93 ImageIndex-Eigenschaft 582 ListViewItem-Klasse 631 TreeNode-Steuerelement 652 TreeView-Steuerelement 652 ImageList-Eigenschaft 582 TreeView-Steuerelement 652 ImageList-Steuerelement 611, 899 Beispiel (Bild ändern) 799 ListView-Anwendung 631 TreeView-Anwendung 652 VB6 91 Images-Eigenschaft 611, 899 ImageSize-Eigenschaft 611, 899 Imaging-Namensraum 832, 913 immutable (String-Klasse) 130 Implements 257 Beispiel (IComparable-Schnittstelle) 366 Beispiel (IComparer-Schnittstelle) 368 Imp-Operator (VB6) 85 Imports 78, 194 Alias 196 Namenskonflikte 196 In-Eigenschaft 506 Inch-Konstante 860
1056 IncludeSubdirectories-Eigenschaft 457 Increment-Eigenschaft 668 Increment-Methode 540 Indent-Eigenschaft 654 Indent-Methode 498 IndentLevel-Eigenschaft 498 IndentSize-Eigenschaft 498 Index-Konstante 683 IndexOfAny-Methode 304 IndexOfValue-Methode 381 Inet-Steuerelement (VB6) 93 Inflate-Methode 863 Information-Klasse 155 Inherits 246 ListViewItem-Beispiel 634 Objektbrowser 206 Schnittstellen vererben 257 vererbte Formulare 730 InitialDelay-Eigenschaft 680 InitialDirectory-Eigenschaft 418 InitializeComponent-Prozedur 723 DPI-Anpassung 749 Lokalisierung 753 Innenmaße von Steuerelementen 585 InnerException-Eigenschaft 475, 486, 487 InputBox-Methode 779 InputLanguage-Klasse 746 Insert-Methode 304, 312 ListBox-Steuerelement 613 Installation eigener Programme 1004 InstalledFontCollection-Klasse 869 InstalledInputLanguages-Eigenschaft 746 InstalledPrinters-Eigenschaft 976, 993 Instance-Klassenmitglieder 201 Instanz 186 InstMsiA/W.exe 1007 InStr-Methode 302 InstrRev-Methode 302 Int16, -32, -64-Klasse 127 Integer-Klasse 127 Multiplikation (Überlauf) 333 IntelliMouse-Ereignisse 813 IntelliSense 34 Interaction-Klasse 163, 508, 516, 522 Interface 257 siehe auch Schnittstellen 256 Interlock-Klasse 540 InternalBufferOverflow-Klasse 457 InternalBufferSize-Eigenschaft 457
Stichwortverzeichnis Internationale Windows-Anwendungen 751 Interop-Bibliothek 520 COM-Steuerelemente 578 InteropServices-Namensraum 401, 551, 954 InterpolationMode-Eigenschaft 916 Intersect-Methode 863, 919 IntersectsWith-Methode 842, 863 Interval-Eigenschaft 678 Int-Methode 294 IntPtr-Klasse 550 API-Beispiel 554 Invalidate-Methode 924 Beispiel (Animation) 950 InvalidCastException-Fehler 247 InvalidOperationException-Fehler 376 InvalidPathChars-Eigenschaft 406, 416 InvalidPrinterException-Fehler 962 InvariantCulture-Eigenschaft 341 Invoke-Methode 268 Beispiel 939 Windows-Programmierung 735 IO-Namensraum 384, 386 IOException-Klasse 469 IPAddress-Klasse 708 IPTextBox-Steuerelement UserControl-Beispiel 701 Is (Select-Case) 162 IsAlive-Eigenschaft 536 IsArray-Methode 155 IsBackground-Eigenschaft 536 IsCompleted-Eigenschaft 451 IsControl-Methode 317 IsDate-Methode 155 IsDefaultPrinter-Eigenschaft 976 IsDefined-Methode 138 IsDigit-Methode 317 ISerializable-Schnittstelle 461 eigene Exception-Klasse 477 IsError-Methode 155 IsFixedSize-Eigenschaft 379 IsInfinity-Methode 292 Islamischer Kalender 321 IsLeapYear-Methode 322 IsLetter-Methode 317 IsLetterOrDigit-Methode 317 IsLower-Methode 317 IsMetric-Eigenschaft 968 IsMissing (VB6) 84
Stichwortverzeichnis IsNegativeInfinity-Methode 292 IsNothing-Methode 155 IsNumber-Methode 317 IsNumeric-Methode 155 ISO-Latin-Codierung 425 IsPositiveInfinity-Methode 292 IsPresent-Methode 712, 746 IsPunctuation-Methode 317 IsReadOnly-Eigenschaft 379 IsReference-Methode 155 IsUpper-Methode 317 IsValueType-Eigenschaft 147, 156 IsVisible-Eigenschaft 921 IsWhiteSpace-Methode 317 Italic-Konstante 880 Item-Eigenschaft AfterLabelEdit-Ereignisprozedur 641 selbst programmieren 234 ItemData-Eigenschaft (VB6) 89 ItemDrag-Ereignis 828 ItemHeight-Eigenschaft 620 MeasureItemEventArgs-Klasse 621 TreeView-Steuerelement 652, 654 Items-Eigenschaft DomainUpDown-Steuerelement 669 ListBox-Steuerelement 613 ListView-Steuerelement 630 ItemSize-Eigenschaft 672 ItemWidth-Eigenschaft 621 J Japanischer Kalender 321 JIT-Compiler 57 Join-Methode 303, 534 JPEG-Datei speichern 905 Just-in-Time-Compiler 57 K Kalender 321 Kantenglättung (Grafikausgaben) 915 KB-Artikel 25 Kennung (von Dateien) 405 Key-Eigenschaft 357 ImageList-Steuerelement (VB6) 91 Toolbar-Steuerelement (VB6) 92 KeyCode-Eigenschaft 804 KeyData-Eigenschaft 804 KeyDown-Ereignis 804 Beispiel 605
1057 Beispiel ListView-Steuerelement 640 Beispiel TreeView-Steuerelement 656 KeyEventArgs-Klasse 804 KeyPress-Ereignis 804 KeyPreview-Eigenschaft 807 Keys-Aufzählung 804 KeyState-Eigenschaft 820 KeyUp-Ereignis 804 KeyValue-Eigenschaft 804 KeywordIndex-Konstante 683 Klassen 119 absichern 483 Definition 217 Eigenschaften 230 Ereignisse 262 Glossar 186 Gültigkeitsbereiche (scope) 277 Hello-World-Beispiel 213 Konstanten 220 Konstruktor 222 Methoden 222 Mitglieder (member) 198 Serialisierung 461 Shared-Mitglieder 236 Variablen 220 Vererbung 245 verschachteln 219 versus Module 240 versus Strukturen 242 Klassenbibliothek 78, 215 anwenden 185 Automation 520 siehe auch Bibliothek 193 Überblick 54 KnowColor-Aufzählung 372 Knowledge-Base-Artikel 25 Kombinationslistenfeld 625 Kommandozeile 509 Beispiel 1020 Kompilat (Debug/Release-Kompilat) 490 Komprimierte Datei 555 Konsolenanwendung 30, 79 absichern 483 Ein- und Ausgabeumleitung 506 Konstanten 132 Aufzählung (Enum) 134 vordefinierte (VB) 132 Zeichenketten 300 Konstruktor (New) 188, 222
1058 Kontextmenü 786 Konvertierung Daten und Zeiten zu Zahlen 338 Fließkommazahlen zu Integerzahlen 338 String zu Char 338 VB6 zu VB.NET 109 zwischen Datentypen 331 Koordinatenpunkt 841 Koordinatensystem Drucker 971 skalieren 861 Koordinatenumrechnung 929 absolut/relativ 810 Kreis zeichnen 846 Kursive Schrift 880 Kurvenzug zeichnen 847 L Label (On Error Goto) 488 Label-Eigenschaft 641 Label-Steuerelement 600 LabelEdit-Eigenschaft ListView-Steuerelement 640 TreeView-Steuerelement 656 LabelWrap-Eigenschaft 637 Landeseinstellungen (Formatierung) 340 Landesunabhängige Formatierung 341 Landscape-Eigenschaft 975, 994 Language-Eigenschaft 751, 752 LargeIcons-Konstante 635 LargeImageList-Eigenschaft 632 LastDLLError-Eigenschaft 547 LastIndex-Methode 380 LastIndexOfAny-Methode 304 LastNode-Eigenschaft 653 LastWriteTime-Eigenschaft 390 late binding 521 Laufwerke 405 anzeigen (DriveListBox) 385 LayeredWindows-Konstante 746 Layout-Ereignis 712 Layouteffekte (Textausgabe) 880 LayoutKind-Aufzählung 551 LayoutMdi-Methode 786 LayoutName-Eigenschaft 746 LBound 140 Leave-Ereignis 589 Leerzeile eingeben 807
Stichwortverzeichnis Left-Eigenschaft Margins-Klasse 972 Namenskonflikt 196 Steuerelement 584, 928 Left-Methode 302 Namenskonflikt 196 Length-Eigenschaft 313, 437 Felder 140 FileInfo-Klasse 390, 394 Len-Methode 302 LF-Konstante 300 Lib (Declare-Anweisung) 549 Like-Operator 307 Line Feed 300 LineAlignment-Eigenschaft 882 LinearGradientBrush-Klasse 858 LineCap-Aufzählung 855 LineJoin-Eigenschaft 855 LineLimit-Konstante 881 Lines-Eigenschaft 604 Linespacing-Abstand (Schriften) 872 Line-Steuerelement (VB6) 93 Linien zeichnen 844 Linienmuster 853 Link-Konstante 818 LinkBehaviour-Eigenschaft 601 LinkColor-Eigenschaft 602 LinkedList-Beispiel 219 Serialisierung 467 LinkLabel-Eigenschaft 601 Links-Klasse 601, 602 LinkVisited-Eigenschaft 602 List-Konstante 635 ListBox-Steuerelement 612 Drag and Drop 825 owner-drawn 620 siehe auch Listenfeld 612 ListBox.ObjectCollection-Klasse 614 ListBox.SelectedIndexCollection-Klasse 618 ListBox.SelectedObjectCollection-Klasse 618 ListDictionary-Klasse 362 Listener-Eigenschaft 498 Listenfeld 612 DomainUpDown-Steuerelement 669 effizient initialisieren 614 hierarchische Listen 649 ToolTips anzeigen 681 ListView-Steuerelement 628 Beispielprogramm 642
Stichwortverzeichnis Drag&Drop-Beispiel 828 DragItem-Ereignis 818 Fehler (SmallIcon-Darstellung) 635 ListView.CheckedIndexCollection-Klasse 629 ListView.CheckedListViewItemCollectionKlasse 629 ListView.ColumnHeaderCollection-Klasse 629 ListView.ListViewItemCollection-Klasse 629 ListView.SelectedIndexCollection-Klasse 629 ListView.SelectedListViewItemCollectionKlasse 629 ListViewItem.ListViewSubItemCollectionKlasse 629 ListViewItem-Klasse 629, 634 ListViewItemSorter-Eigenschaft 638 ListViewSubItem-Klasse 629 Literale 290 Lizenzprobleme (ActiveX-Steuerelemente) 579 Load-Ereignis 571, 714, 715 LoadFile-Methode 609 Localizable-Eigenschaft 751, 752 Location-Eigenschaft 403 Steuerelement 584, 928 LockBits-Methode 954 Locked-Eigenschaft 592 Locking (Dateizugriff) 439 Lock-Methode 441 Log10-Methode 293 Logarithmische Funktionen 293 Login-spezifische Informationen 510 Logische Operatoren 181 Log-Methode 293 Lokal-Fenster 494 Lokale Variablen 170 Lokalisierung von WindowsAnwendungen 751, 754 Long-Klasse 127 Loop 166 LostFocus-Ereignis 589 LSet-Methode 299 LTrim-Methode 302 M MachineName-Eigenschaft 509 MainMenu-Klasse 782 Main-Prozedur 168 Major-Eigenschaft 510
1059 MakeTransparent-Methode 907 managed code 58 Management-Namensraum 511 ManagementObjectCollection-Klasse 512 ManagementObject-Klasse 512 ManagementObjectSearcher-Klasse 512 Mandelbrotmenge 936 Manifest-Datei 731 Manual-Konstante 713 Manufacturer-Eigenschaft 1013 MAPIMessage-Steuerelement (VB6) 93 MAPISession-Steuerelement (VB6) 93 MarginBounds-Eigenschaft 971 Margins-Klasse 967, 972 Markierung (TextBox-Steuerelement) 604 MarshalAs-Attribut 551 Marshal-Klasse (Bitmap kopieren) 954 marshalling 733 MaskedEdit-Steuerelement (VB6) 93, 577 Maßeinheit des Koordinatensystems 860 Math-Klasse 293 Rundungs- und Spezialfunktionen 294 Matrix-Klasse 861 Maus 809 Aussehen 809 Aussehen (Cursor-Eigenschaft) 584 Drag&Drop 817, 819 Ereignisse 810 Mausrad 813 Position ändern 810 Position ermitteln 810 Max-Methode 294 MaxDate-Eigenschaft 663, 664 MaxDropDownItems-Eigenschaft 626 MaximizeBox-Eigenschaft 684, 711 Maximized-Konstante 711 Maximum-Eigenschaft NumericUpDown-Steuerelement 668 ProgressBar-Steuerelement 667 TrackBar-Steuerelement 666 MaxLength-Eigenschaft 604 MaxSelectionCount-Eigenschaft 663 MaxValue-Eigenschaft 292 MDI-Anwendungen 768 Fenster andocken 772 Menü 785 Programmende 771 MdiChildActivate-Ereignis 769 MdiChildren-Eigenschaft 769
1060 MdiList-Eigenschaft 785 MdiParent-Eigenschaft 769 Me 222, 249 Beispiel 226 Fenster 713 Fenster (Debugging) 494 Formularcode 571 MeasureItem-Ereignis ListBox-Steuerelement 620 MenuItem-Klasse 789 MeasureItemEventArgs-Klasse 621 MeasureString-Methode 875 MeasureTrailingSpaces-Konstante 881 Mehrblättrige Dialoge 672 Mehrfachauswahl (Dateinamen) 418 Mehrfachvererbung 247 Mehrfachzugriff auf Dateien 439 Mehrsprachige Windows-Anwendungen 751 Mehrzeiliger Text 884 Member siehe auch Klassen (Mitglieder) 198 MemoryStream-Klasse 443, 467 Menü 780 Kontextmenü 786 MDI-Anwendung 785 selbst gestalten (owner-drawn) 789 Menu.MenuItemCollection-Klasse 783 MenuButtonSize-Eigenschaft 745 MenuFont-Eigenschaft 745, 783 MenuHeight-Eigenschaft 792 MenuItem-Klasse 782 MenuItems-Eigenschaft 783 MergedMenu-Eigenschaft 786 MergeOrder-Eigenschaft 784 MergeType-Eigenschaft 784 message loop 719, 720 Message-Eigenschaft 475, 1018 MessageBox-Klasse 777 Metadaten 272 Metafile 1032 Metafile-Klasse 913 Methoden Glossar 186 New 222 programmieren 222 Shared 238 Zeiger (delegates) 267 Metrisches System (Drucken) 968
Stichwortverzeichnis Microsoft Sans Serif 88, 868 Microsoft Scripting Runtime 384 Microsoft.VisualBasic.dll-Bibliothek 54 Microsoft.VisualBasic-Namensraum 163, 294, 384, 508, 516, 522 Mid-Methode 302 Migrationsassistent 109 Millimeter-Konstante 860 MilliSecond-Eigenschaft 322 MimeType-Eigenschaft 906 Min-Methode 294 MinDate-Eigenschaft 663, 664 MinimizeBox-Eigenschaft 684, 711 Minimized-Konstante 711 Minimum-Eigenschaft NumericUpDown-Steuerelement 668 ProgressBar-Steuerelement 667 TrackBar-Steuerelement 666 MinMargins-Eigenschaft 967 Minor-Eigenschaft 510 Minute-Methode 322 MinValue-Eigenschaft 292 MinWidth-Eigenschaft 802 MMControl-Steuerelement (VB6) 93 Mod-Operator 294 Modaler Dialog 560, 758 Modified-Eigenschaft 605 ModifierKeys-Eigenschaft 591 Controls-Klasse 805, 810 Modifiers-Eigenschaft Entwicklungsumgebung 592 KeyEventArgs-Klasse 804 vererbte Formulare 730 Module 239 Gültigkeitsbereiche (scope) 277 Hello-World-Beispiel 212 Teile einer .NET-Assembly 240 versus Klassen 240 Monat addieren 326 Monatsende ermitteln 326 Monitor (mehrere) 746 MonitorCount-Eigenschaft 745 Monitor-Klasse 540 Mono 67 MonthlyBoldedDates-Eigenschaft 663 Month-Methode 322 MonthView-Steuerelement (VB6) 92 MouseButtons-Eigenschaft 591, 810 MouseDown-Ereignis 811
Stichwortverzeichnis MDI-Hauptfenster 769 Rubberbox-Beispiel 931 MouseEnter-Ereignis 811 MouseEventArgs-Klasse 811 MouseHover-Ereignis 811 MouseLeave-Ereignis 811 MouseMove-Ereignis 811 Rubberbox-Beispiel 931 MousePosition-Eigenschaft 591 Control-Klasse 810 TreeView-Beispiel 655 MouseUp-Ereignis 811 Rubberbox-Beispiel 931 MouseWheel-Ereignis 813 MouseWheelScrollLines-Eigenschaft 745, 814 Move-Ereignis 666 Move-Konstante 818 Move-Methode 399, 413 MoveNext-Methode 378 MoveTo-Methode 399, 413 MS Sans Serif 88 MS[H]FlexGrid-Steuerelement (VB6) 91 MSChart-Steuerelement (VB6) 93, 577 MSComm-Steuerelement (VB6) 93 mscorlib.dll-Bibliothek 54 MsgBox-Methode 778 ms-help-Adressen 24 MSI (Microsoft Windows Installer) 1007 MSIL (Microsoft Intermediate Language) 56, 1032 MTA (Multithreading Apartment Modell) 528 MulticastDelegate-Klasse 270 MultiColumn-Eigenschaft 612 MultiLine-Eigenschaft 603, 672 MultiPage-Steuerelement (VB6) 92 MultiSelect-Eigenschaft 418, 637 MultiSimple-Konstante 617 Multithreading 528 Apartment Modell (MTA) 528 asynchroner Dateizugriff 449 Aufzählungen (Collections) 376 Beispiel (Apfelbrotgrafik) 936 Beispiel (mehrere Fenster) 765 Debugging 496 garbage collection 150 Grundlagen 528 Hintergrund-Thread 536 Modelle (STA, MTA) 528
1061 Programmende 535 Synchronisierung 539 Thread-Liste ermitteln 537 threadsicherer Klassen/Methoden 543 Timer-Varianten 678 Vordergrund-Thread 536 Windows-Anwendungen 733 Windows-Nachrichtenschleife 719 Muster (Brush-Klasse) 856 Mustervergleich 307 MustInherit 247 MustOverride 248 MyBase 249 Fenster 713 MyClass 249 MyServices 66 N Nachrichtenschleife 719, 720 Name-Eigenschaft DirectoryInfo-Klasse 390 fehlt bei MenuItem-Klasse 782 Steuerelement 563, 592 Thread-Klasse 537 Name-Methode Type-Klasse 156 NameEditor.FolderBrowser-Klasse 419 Namenskonflikte []-Schreibweise 117 Namensraum 78, 187, 194, 1032 Defaulteinstellung 215 für eigene Klassen 274 NameObjectCollectionBase-Klasse 364 Namespace 194, 276 siehe auch Namensraum 274 NameValueCollection-Klasse 362 NaN-Eigenschaft 292 Negate-Methode 324 NegativeInfinity-Eigenschaft 292 .NET Architektur 55 Bibliotheken (Überblick) 54 Bibliotheken anwenden 185 Einführung 50 Framework 53 Framework installieren 1010 Klassenbibliothek 78 MyServices 66 Passport 66
1062 Sicherheitsmechanismen 61 Net-Namensraum 708 New 122, 202 Form-Klasse 722 Objekte 119 Overridable 249 selbst programmieren 222 Steuerelemente 691 String 119 Variablen 119 NewLine (Environment) 301 NewLine-Eigenschaft 509 NewLine-Konstante 300 Next (For-Next-Schleife) 165 Next-Methode 297 NextBytes-Methode 297 NextDouble-Methode 297 NextNode-Eigenschaft 653 NodeFont-Eigenschaft 654 Nodes-Eigenschaft TreeNode-Klasse 651 TreeView-Steuerelement 650 None-Konstante 448, 711 NonSerialized-Attribut 461 Normal-Konstante 410, 711, 897 Not-Operator 181 NotContentIndexed-Konstante 410 Nothing versus leere Zeichenkette 425 Zeichenketten 309 NotifyFilters-Eigenschaft 457 NotifyIcon-Steuerelement 688 NotInheritable 247 NotOverridable 248 Now-Eigenschaft 320 NoWrap-Konstante 881 NullChar-Konstante 300 Number-Eigenschaft 488 NumberFormatInfo-Klasse 340 NumericUpDown-Steuerelement 668 O ObjectCollection-Klasse 614 Object-Klasse 130 Objektbrowser 203 Objekte 119 fixieren (With) 189 Glossar 186 in Datei speichern 458
Stichwortverzeichnis kopieren 124 serialisieren 458 vergleichen 366 Objektvariablen 119, 122, 188 obj-Verzeichnis 36 Oct-Methode 290 OEM-Codierung 425 Offline-Konstante 410 Oktale Zahlen 290 OLE 107 OLE-Automation 519 OLE-Drag&Drop (VB6) 99 OLE-Feld (VB6) 93 On Error Goto 488 One-Konstante 617 op_Explicit-Operator 842 op_Implicit-Operator 842 Opacity-Eigenschaft 712 Opaque-Konstante 945 OpenFileDialog-Klasse 416 Open-Konstante 447 OpenOrCreate-Konstante 447 OpenText-Methode 423 OpenType-Schriften 868 OperatingSystem-Klasse 510 Operatoren 181 Casting (CType) 157, 335 Option Compare Text 305 Option Explicit 125 Option Strict 125 Beispiele 126 Defaulteinstellung ändern 47 Optional 178 Optionale Parameter 178 OptionButton-Steuerelement (VB6) 90 Optionsfeld 599 Or-Operator 181 OrElse-Operator 181 OSFeature-Klasse 712, 746 OSVersion-Eigenschaft 510 Out-Eigenschaft 498, 506 Overloads 174, 248 Overridable 248 Overrides 248 Beispiel 228 OwnerDrawFixed-Konstante 620, 673 OwnerDraw-Konstante 802 owner-drawn ListBox 620
Stichwortverzeichnis MenuItem 789 StatusBarPanel 802 TabControl 673 OwnerDrawn-Eigenschaft 789 OwnerDrawVariable-Konstante 620 P Pack (StructLayout-Attribut) 551 Padding-Eigenschaft 673 PageBounds-Eigenschaft 971 PageScale-Eigenschaft 861 PageSettings-Eigenschaft 972 PageSettings-Klasse 972, 975 PageSetupDialog-Steuerelement 967 PageUnit-Eigenschaft 860 Paint-Ereignis 710, 833, 923 Beispiel Button-Steuerelement 597 Fehlersuche 835 PaintEventArgs-Klasse 833 PanelClick-Ereignis 802 Panels-Eigenschaft 801 Panel-Steuerelement 669, 947 als Fenster-Container 773 PaperSizes-Eigenschaft 976 Papierkorb 400 Parameter benannte 179 ByRef versus ByVal 174 Felder 177 optionale 178 von Prozeduren 173 Parent-Eigenschaft 592, 653 ParentForm-Eigenschaft 713 Parse-Methode 337 Enum-Klasse 137 IPAddress-Klasse 708 TimeSpan-Klasse 324 Passport 66 PasswordChar-Eigenschaft 604 Paste-Methode 605 Path-Klasse 405 PathSeparator-Eigenschaft 406, 416 TreeView-Steuerelement 654 PathTooLongException-Klasse 469 PDF-Dokument anzeigen 517 Peek-Methode 426 Pen[s]-Klasse 853 PenWindows-Eigenschaft 745 PerformStep-Methode 667
1063 Permission-Namensraum 402, 515 Pi (Kreisteilungszahl) 293 PIA (Primary Interop Assembly) 521 PictureBoxSizeMode-Aufzählung 897 PictureBox-Steuerelement 610, 835 Double-Buffering 944 Grafik in Bitmap speichern 932 PictureClip-Steuerelement (VB6) 93 Pixel-Konstante 860 PixelOffsetMode-Eigenschaft 916 PlainText-Konstante 609 Platform-Eigenschaft 510 PlatformID 511 Point-Konstante 860 Point[F]-Struktur 841 Konvertierung Integer/Single 842 PointToClient-Methode 591 Control-Klasse 810 TreeView-Beispiel 655 PointToScreen-Methode 591, 810, 929 Popup-Ereignis ContextMenu-Klasse 787 MenuItem-Klasse 785 Popup-Hilfe 684 Popup-Menü 786 Position-Eigenschaft 437 PositiveInfinity-Eigenschaft 292 PostScript-Schriften 868 Pow-Methode 293 Preserve (ReDim) 140 PreviewPrintController-Klasse 995 PrevNode-Eigenschaft 653 Primäre Ausgabe 1012 Primary Interop Assembly (PIA) 521 Primary-Eigenschaft 746 PrimaryScreen-Eigenschaft 746 PrintController-Eigenschaft 995 PrintControllerWithStatusDialog-Klasse 995 PrintDialog-Steuerelement 965 PrintDocument-Klasse 961 Drucken ohne Statusdialog 995 PrinterName-Eigenschaft 976, 994 PrinterResolution-Eigenschaft 975 PrinterSettings-Eigenschaft 973, 975 PrinterSettings-Klasse 973, 976 Print-Methode 962 PrintPage-Ereignis 962 PrintPageEventArgs-Klasse 971 PrintPreviewDialog-Steuerelement 969, 996
1064 PrintRange-Aufzählung 976 PrintRange-Eigenschaft 976 PrintToFile-Eigenschaft 976, 995 Priority-Eigenschaft 536 Private 277 PrivateFontCollection-Klasse 869 Process-Klasse 150, 516, 537 ProcessThread-Klasse 537 ProductName-Eigenschaft 1013, 1016 Programm Elemente 212 starten (externes Programm) 516 starten (Windows.Forms) 719 unterbrechen 491 zeilenweise ausführen 493 Programmcode (Unicode) 129 Programmende MDI-Anwendungen 771 Multithreading-Anwendung 535 Multithreading-Windows-Anwendung 738 Windows-Anwendungen 719, 761 Programmspezifische Informationen 510 ProgressBar-Steuerelement 667 Projektdateien 36 Projekteigenschaften 214 Property 230 PropertyData-Klasse 512 PropertyGrid-Steuerelement 576 Protected 250, 277 Protected Friend 277 Prozeduren 167 Adresse ermitteln 264 Aufruf 169 gleichnamige 173 Main() 168 Methoden 222 Parameter 173 Rekursion 172 Verschachtelung 169 Zeiger (delegates) 267 Prozedurschritt 493 Prozentzahlen 344, 351 Prozess 516 Public 277 Punkt zeichnen 845
Stichwortverzeichnis Q Q000000 (Knowledge-Base-Artikel) 25 Querformat (Drucken) 994 QueryContinueDrag-Ereignis 820 QueryPageSettings-Ereignis 962, 994 Queue-Klasse 363 Quote-Konstante 300 R R-Eigenschaft 851 RadioButton-Steuerelement 599 RadioChecked-Eigenschaft 783 Radmaus-Ereignisse 813 RaiseEvent 263 Ränder einstellen (Drucken) 967 Randomize-Methode 296 Random-Klasse 297 Rank-Eigenschaft 140 Rank-Methode 177 ReadByte-Methode 437 Read-Konstante 447, 448 ReadLine-Methode 33, 425 Read-Methode 426, 437 ReadOnly Eigenschaften 231 Variablen 220 ReadOnly-Eigenschaft DomainUpDown-Steuerelement 669 NumericUpDown-Steuerelement 668 TextBox-Steuerelement 604 ReadOnly-Konstante 410 ReadOnly-Methode (ArrayList) 374 ReadOnlyCollectionBase-Klasse 364 ReadToEnd-Methode 425 ReadWrite-Konstante 447, 448 ReadXxx-Methoden (BinaryReader) 444 Rechengenauigkeit 291 Rechnername 509 Rechnerspezifische Informationen 509 Rechteck 841 Auswahl mit der Maus 929 Test, ob Punkt enthalten ist 842 Überlappung testen 842 zeichnen 842 Rechtsbündiger Text 882 Rectangle[F]-Struktur 841 Konvertierung Integer/Single 842 ReDim 140 Referenztypen 120, 147
Stichwortverzeichnis ByVal 176 Speicherverwaltung 147 Reflection-Namensraum 156, 403 Refresh-Methode Directory- und FileInfo-Klasse 391 PictureBox-Steuerelement, Beispiel 934 StatusBar-Steuerelement 803 Steuerelemente 584 Region-Klasse 919 Regionen (Grafikprogrammierung) 919 RegionInfo-Klasse 968 Registrierdatenbank (Setup.exe) 1022 Rekursion 172 Beispiel (Steuerelemente) 691 Beispiel (Verzeichnisbaum) 394 Release-Kompilat 490 ReleaseHdc-Methode 555 Remove-Methode 304, 312 IList-, IDictionary-Schnittstelle 379 RemoveAll-Methode 680 RemoveAt-Methode 379 RemoveHandler 264 Renamed-Ereignis 456 RenderingOrigin-Eigenschaft 857 ReparsePoint-Konstante 410 Repeat-Methode 380 Replace-Methode 303, 312 Reset-Methode 378 Resize-Ereignis 712, 925 ResizeRedraw-Eigenschaft 926 ResizeRedraw-Konstante 945 ResourceManager-Klasse 755 Resources-Namensraum 755, 899 Ressourcendatei 899 Bitmaps von Formularen 897 Lokalisierung von Formularen 752 Rest einer ganzzahligen Division 294 ResumeLayout-Methode 693, 712 Resume-Methode (Multithreading) 534 Return 168 Return eingeben 807 Reverse-Methode 143, 380 Revision-Eigenschaft 510 RGB-Farbmodell 851 Rich Text Format 608 RichNoOleObjs-Konstante 609 RichText-Konstante 609 RichTextBox-Steuerelement 608 Inhalt ausdrucken 526
1065 Textformatierung 609 RichTextBoxStreamType-Aufzählung 609 Right-Eigenschaft Margins-Klasse 972 Steuerelement 584, 928 Right-Methode 302 RightToLeft-Eigenschaft 592 Rnd-Methode 296 RotateTransform-Methode 861 Rotation (Grafik) 861 Rotierter Text 886 Round-Methode 294 Grafikstrukturen 842 RowCount-Eigenschaft 672 Rows-Eigenschaft 996 RSet-Methode 299 RTF (Rich Text Format) 608 RTF-Eigenschaft 609 RTrim-Methode 302 Rubberbox/-band (Rechteckauswahl) 929 Rückgabeparameter 174 Runden 294 Rundungsfehler 128, 291 Run-Methode 719, 927 Runtime-Umgebung 60, 1029 Running-Konstante 536 Runtime.InteropServices-Namensraum 401, 551, 954 Runtime.Serialization-Namensraum 461 S safe code 58 SafeDelete (API-Beispiel) 400 Sanduhr (Mauscursor) 809 Sans Serif 868 SaveFileDialog-Klasse 416 SaveFile-Methode 609 Save-Methode 905 SByte-Klasse 131 ScaleTransform-Methode 861 Scan0-Eigenschaft 954 Schaltjahr (Test) 322 Schiebebalken 665 für Grafik 946 Schnellüberwachung 494 Schnittstellen 256 IDisposable 259 ISerializable 461 versus Vererbung 259
1066 Schrift ändern (Steuerelemente) 581 Schriftarten 867 Schriftattribute 880 Schriftdesign 872 Schriftfamilien 868 Schriftgröße 871 scope (Gültigkeitsbereiche) 277 Screen-Klasse 746, 760 Screenshot durchführen 951 Scroll-Ereignis 666 Scroll-Konstante 818 Scrollbalken 665 Scrollbare Grafik 946 ScrollBars-Eigenschaft 603 ScrollToCaret-Methode 604 scrrun.dll-Bibliothek 384 SDI (Single Document Interface) 768 SDK (Software Development Kit) 53 Second-Methode 322 Security.Permission-Namensraum 515 SecurityException-Fehler 515 Security-Namensraum 515 Seek-Methode 437 SeekOrigin-Methode 437 Seite einrichten (Drucken) 967 Seitengröße (Drucker) 971 Seitenränder einstellen (Drucken) 967 Seitenvorschau (Drucken) 969 Select-Case-Verzweigung 162 Select-Methode 604 SelectAll-Methode 605 SelectedImageIndex-Eigenschaft 652 SelectedIndexChanged-Ereignis 617, 626, 641 SelectedIndexCollection-Klasse 618, 629 SelectedIndex-Eigenschaft 617, 626, 669, 672 SelectedIndices-Eigenschaft 618, 641, 825 SelectedItemChanged-Ereignis 669 SelectedItem-Eigenschaft 617, 626, 669 SelectedItems-Eigenschaft 618, 641, 825 SelectedListViewItemCollection-Klasse 629 SelectedNode-Eigenschaft 655 SelectedObjectCollection-Klasse 618 SelectedRTF-Eigenschaft 609 SelectedTab-Eigenschaft 672 SelectedTextChanged-Ereignis 626 SelectedText-Eigenschaft 604, 609 Selection-Konstante 976 SelectionAlignment-Eigenschaft 609 SelectionColor-Eigenschaft 609
Stichwortverzeichnis SelectionEnd-Eigenschaft 663 SelectionFont-Eigenschaft 609 SelectionLength-Eigenschaft 604 SelectionMode-Aufzählung 617 SelectionMode-Eigenschaft 617 SelectionStart-Eigenschaft 604, 663 Send-Methode 952, 808 SendKeys-Klasse 808 Beispiel 952 SendWait-Methode 808 Sequential-Konstante 551 Serialisierung 458 eigene Exception-Klasse 477 eigene Klassen 461 Serialization-Namensraum 459, 461 Serialize-Methode 460 Service-Pack 510 Set (eigene Eigenschaften) 230 Set-Operator (VB6) 85 SetBounds-Methode 585 SetClientSizeCore-Methode 929 SetClip-Methode 921 SetCurrentDirectory-Methode 402 SetData-Methode 815 SetDataObject-Methode 814 SetDesktopBounds-Methode 929 SetDesktopLocation-Methode 929 SetError-Methode 686 SetHelpKeyword-Methode 685 SetHelpNavigator-Methode 685 SetIn-Methode 506 SetOut-Methode 506 SetPixel-Methode 900, 901 SetSelected-Methode 619 SetStyle-Methode 944 Beispiel 949 eigene PictureBox-Klasse 698 SetTabStops-Methode 882 SetToolTip-Methode 680 Setup-Projekte 1004 SetValue-Methode 144 Shadows 248 shallow copy 143 Shape-Steuerelement (VB6) 93 Shared 236 Klassenmitglieder 201 Klassenvariablen 237 Klassenvariablen, Beispiel 241 Methoden 238
Stichwortverzeichnis sharing (Dateizugriff) 439 SharpDevelop 71 Shell-Methode 516 SHFileOperation (API-Funktion) 400 Shift-Eigenschaft 804 Shift-Taste 804, 810 ShortCut-Eigenschaft 782 Short-Klasse 127 Show-Methode 583, 711, 761 ContextMenu-Klasse 787 MessageBox-Klasse 777 ShowApply-Eigenschaft 890 ShowCheckBox-Eigenschaft 664 ShowColor-Eigenschaft 890 ShowDialog-Methode 569, 758 ColorDialog-Steuerelement 852 CommonDialog-Klasse 779 FolderBrowserDialog-Klasse 420 FontDialog-Steuerelement 890 Open-/SaveFileDialog 418 PageSetupDialog-Steuerelement 967 PrintDialog-Steuerelement 966 PrintPreviewDialog-Steuerelement 969 ShowEffects-Eigenschaft 890 ShowHelp-Eigenschaft 685 ShowHelp-Methode 685 ShowInTaskbar-Eigenschaft 710 ShowLines-Eigenschaft 654 ShowPanels-Eigenschaft 801 ShowPlusMinus-Eigenschaft 654 ShowRootLines-Eigenschaft 654 ShowShortCut-Eigenschaft 782 ShowTodayCircle-Eigenschaft 663 ShowToday-Eigenschaft 663 ShowUpDown-Eigenschaft 664 ShowWeekNumbers-Eigenschaft 663 Sicherheit 61, 514 Sichtbarkeitstest 921 Sign-Methode 293 Signum-Funktion 293 Simple-Konstante 625 Sin-Methode 293 SingleBitPerPixelGridFit-Konstante 916 SingleBitPerPixel-Konstante 916 Single-Klasse 128 Rundungsfehler 291 Singlethread-Apartment-Modell (STA) 528 Sinh-Methode 293 Sizable-Konstante 711
1067 SizableToolWindow-Konstante 711 Size-Eigenschaft Font-Objekt 874 Steuerelement 585, 928 Size[F]-Struktur 841 Konvertierung Integer/Single 842 SizeChanged-Ereignis 712 SizeGripStyle-Eigenschaft 712 SizeInPoints-Eigenschaft 874 SizeMode-Eigenschaft 672, 897 SizingGrip-Eigenschaft 801 Skalierung (Grafik) 861 Sleep-Methode 533 Slider-Steuerelement (VB6) 92 SmallIcons-Konstante 635 SmallImageList-Eigenschaft 632 SmoothingMode-Eigenschaft 915 SOAP (Serialisierung) 459 Software Development Kit 53 SomePages-Konstante 976 Sort-Methode ArrayList-Klasse 366 Beispiel 392 Felder 143, 371 ListView-Steuerelement 639 Sorted-Eigenschaft ListBox-Steuerelement 614 TreeView-Steuerelement 654 SortedList-Klasse 362, 372, 381 Sortieren ArrayList-Klasse 366 Dateinamen 392 Felder 143, 371 länderspezifische Sortierordnung 306 ListBox-Steuerelement 614 ListView-Beispiel (Dateien) 647 ListView-Steuerelement 638 TreeView-Steuerelement 654 Sorting-Eigenschaft 638 Source-Eigenschaft 475 Space-Methode 299 SparseFile-Konstante 410 SpecialFolder-Aufzählung 415 Specialized-Namensraum 356 Speicherbedarf Felder 152 Programm 510 Variablen 151 Speicherverwaltung 147, 148
1068 Finalize-Methode 223 Spezielle Verzeichnisse 402 Splines zeichnen 847 Split-Methode 303 SplitterMoved-Ereignis 676 SplitterMoving-Ereignis 676 Splitter-Steuerelement 674 SplitX-Eigenschaft 676 SplitY-Eigenschaft 676 Sprache einstellen 754 Sprache ermitteln 746 Spring-Konstante 801 Sprunglabel 488 Sqrt-Methode 293 SSTab-Steuerelement (VB6) 92, 579 STA 528 Stack-Klasse 363 StackOverflowException-Fehler 172 StackTrace-Eigenschaft 475 Stammnamensraum 215, 275 Standardausgabe 506 Standarddialoge Datei öffnen oder speichern 416 Verzeichnis auswählen 419 Standardeingabe 506 StandardModule-Attribut 197 StandardPrintController-Klasse 995 Standardsteuerelement Microsoft.VisualBasic.Compatibility.VB6Namensraum 108, 578 siehe auch Steuerelement 88, 576 Start-Methode 517, 530, 765 Startbedingungen 1017 StartCap-Eigenschaft 855 Startmenü (Setup-Projekt) 1014 Startobjekt 215 StartPage-Eigenschaft 996 StartPosition-Eigenschaft 713, 760 StartsWith-Methode 304 State-Eigenschaft 620 StateImageIndex-Eigenschaft 637 StateImageList-Eigenschaft 636 Static 171 Beispiel 847 Klassenmitglieder 201 static (C#) 237 statische Variablen 171 StatusBar-Steuerelement 801 ToolTips anzeigen 680
Stichwortverzeichnis StatusBarPanel-Klasse 801 StatusBarPanelCollection-Klasse 802 Statusleiste 801 Step (For-Next-Schleife) 165 Step-Eigenschaft 667 Steuerelement 3D-Aussehen 584 andocken 587 Attribute 700 Aussehen 581 durchsichtig 583 dynamisch einfügen 691 Einführung 560 Feld 694 gemeinsame Schlüsselwörter 581 Grafik 610 Größe ändern 928 Größe und Position 584 gruppieren 669 in die Toolbox einfügen 580 in ein Formular einfügen 561 Klassenhierarchie 573 ohne Paint-Ereignis zeichnen 925 Paint-Ereignis manuell auslösen 924 Position ändern 928 Schriftart ändern 581 selbst programmieren 696 Standardfarben 851 Steuerelementfeld 694 Tastaturereignisse 804 Tastaturkürzel 590 Text 600 Überblick 576 Unicode 582 unsichtbar 95 Validierung 590 verankern 585 Vererbung 697 Windows-Standardfarben 851 Windows-XP-Optik 730 Stop 497 Str-Methode 335 StrDup-Methode 299 Stream-Klasse 436 StreamReader-Klasse 423 StreamWriter-Methode 427 StretchImage-Konstante 897 Strg-Taste 804, 810 Stride-Eigenschaft 954
Stichwortverzeichnis StrikeOut-Konstante 880 String-Funktion (VB6) 86 String-Klasse 129, 298 ByVal 176 hash-Wert 304 Interna 308 Konstanten 300 Methoden 304 StringAlignment-Aufzählung 882 StringBuilder-Klasse 312 StringWriter-Klasse 432 StringCollection-Klasse 361 StringDictionary-Klasse 362 StringFormatFlags-Aufzählung 881 StringFormat-Klasse 880 StringReader-Klasse 432 Strings-Klasse (Microsoft.VisualBasicNamensraum) 301 StringTrimming-Aufzählung 882 StringWriter-Klasse 432 StrReverse-Methode 303 StructLayout-Attribut 401, 551 Structure 242 Strukturen 242 Style-Eigenschaft StatusBarPanel-Klasse 802 ToolBarButton-Klasse 796 Sub 168 SubItems-Eigenschaft 630 Subtract-Methode DateTime-Klasse 324, 325 TimeSpan-Klasse 324 Suchen in einem Feld 143, 371 in einer Aufzählung 370 SuspendLayout-Methode 693, 712 Suspend-Methode 534 Symbolleiste 795 mit Drag&Drop verschieben 821 Synchronisierung Threads 539 Verzeichnisse 407 SyncLock 377, 539 SyncRoot-Eigenschaft 377 System-Konstante 410 System-Namenswirrwarr 197 System.Collections.Specialized-Namensraum 356 System.Collections-Namensraum 356
1069 System.ComponentModel-Namensraum 700 System.Data.dll-Bibliothek 54 System.Diagnostics-Namensraum 150, 516, 537 System.dll-Bibliothek 54 System.Drawing.Design-Namensraum 832 System.Drawing.dll-Bibliothek 54 System.Drawing.Drawing2D-Namensraum 832, 853, 855 System.Drawing.Imaging-Namensraum 832, 905, 913 System.Drawing.Text-Namensraum 832, 867 System.Drawing-Namensraum 832 System.Globalization-Namensraum 306, 321, 340, 968 Sprache einstellen 754 System.IO-Namensraum 384, 386 System.Management.dll-Bibliothek 54 System.Management-Namensraum 511 System.Net-Namensraum 708 System.Reflection-Namensraum 156, 403 System.Resources-Namensraum 755, 899 System.Runtime.InteropServicesNamensraum 401, 551, 552, 954 System.Runtime.Serialization-Namensraum 461 System.Security.Permission-Namensraum 402, 515 System.Security-Namensraum 402, 515 System.Text-Namensraum 311, 425 System.Threading-Namensraum 530, 754, 765 System.Timers-Namensraum 678 System.Web.dll-Bibliothek 54 System.Windows.Forms.DesignNamensraum 419 System.Windows.Forms.dll-Bibliothek 54 System.Windows.Forms-Namensraum 560, 929 Klassenhierarchie 573 Steuerelemente 576 System.Xml.dll-Bibliothek 54 System.Xml.Serialization-Namensraum 459 SystemColors-Aufzählung 851 SystemColorsChanged-Ereignis 583 SystemDefault-Konstante 916 SystemDirectory-Eigenschaft 404 SystemException-Klasse 474
1070 Systeminformationen ermitteln 507 SystemInformation-Klasse 745, 783, 814 T TabAppearance-Aufzählung 672 TabControl-Steuerelement 672 als Fenster-Container 773 ToolTips anzeigen 680 TabCount-Eigenschaft 672 Tabellenfeld (DataGrid) 660 TabIndex-Eigenschaft 589 Tab-Konstante 300 TableOfContents-Konstante 683 TabPage-Klasse 672 TabPages-Eigenschaft 672 TabSizeMode-Aufzählung 672 TabStop-Eigenschaft 589 TabStrip-Steuerelement (VB6) 92 Tabulatoren 882 durch Leerzeichen ersetzen 990 Tabulatorreihenfolge 589 Tabulatorzeichen eingeben 807 Tag-Eigenschaft 592 fehlt bei MenuItem-Klasse 782 ListViewItem-Klasse 633 TreeNode-Klasse 652 Tan-Methode 293 Tanh-Methode 293 Target-Eigenschaft 1014 TargetSite-Eigenschaft 475 Tastatur 804 Eingabe simulieren 808 Eingaben unterdrücken 805 Zustandstasten 804, 810 Tastenkürzel Entwicklungsumgebung 43 mit DrawString anzeigen 882 Tausendertrennung 344 Tausendertrennzeichen feststellen 346 Teilbares Fenster 674 Template-Dateien 47 Temporäre Datei 403 Temporäres Verzeichnis ermitteln 403 Temporary-Konstante 410 Text ausgeben (Grafik) 867 drucken 981 rotieren 886 UnicodeEncoding 311
Stichwortverzeichnis TextAlign-Eigenschaft Steuerelemente 581 ToolBar-Steuerelement 797 Textausrichtung 581, 882 TextBox-Steuerelement 603 Beispiel 561 drucken 981 TextChanged-Ereignis 568, 605 ComboBox-Steuerelement 626, 627 DomainUpDown-Steuerelement 669 ListBox-Steuerelement 617 NumericUpDown-Steuerelement 668 Textcodierung ändern 431 Textdatei drucken 981 lesen und schreiben 421 zeilenweise lesen 425 Texteditor-Beispielprogramm 561 Text-Eigenschaft ComboBox-Steuerelement 626 Fenstertitel 710 ListBox-Steuerelement 617 Steuerelemente 581 Steuerelemente (Tastenkürzel) 590 Text-Konstante 815 Texteingabe 603 Textfeld 603 drucken 981 TextLength-Eigenschaft 604 Text-Namensraum 311, 425, 832 Textqualität 915 TextReader-Klasse 387 TextRenderingHint-Eigenschaft 915 TextTextOleObjs-Konstante 609 TextureBrush-Klasse 857 TextWriter-Klasse 387 TextWriterTraceListener-Klasse 498 Themes-Konstante 730, 746 Then 162 ThousandsSeparator-Eigenschaft 668 Thread 528 abbrechen 534 Ende abwarten 534 Liste ermitteln 537 Programmende 535 synchronisieren 539 vorübergehend unterbrechen 533 Thread-Klasse 530, 765 Beispiel 939
Stichwortverzeichnis Sprache einstellen 754 Thread-sicherere Klassen/Methoden 543 ThreadAbortException-Fehler 534 Threading-Namensraum 530, 765 Sprache einstellen 754 ThreadPool-Klasse 528 ThreadPriority-Aufzählung 536 Threads-Eigenschaft 537 Threads-Fenster 496 ThreadState-Eigenschaft 536 ThreeDCheckBoxes-Eigenschaft 624 ThreeState-Eigenschaft 599 Throw 476 Tick-Ereignisprozedur 678 TickCount-Eigenschaft 509 TickFrequency-Eigenschaft 666 Ticks-Eigenschaft 319, 322 TickStyle-Eigenschaft 666 TimeOfDay-Eigenschaft 320, 322, 324 Timer-Klasse Threading-Namensraum 531 Varianten 678 Timer-Steuerelement 678 Beispiel 950 TimerCallback-Klasse 531 Timers-Namensraum 678 TimeSerial-Methode 320 TimeSpan-Klasse 323 Title-Eigenschaft 418 ToArgb-Methode 851 ToArray-Methode 443 ToBitmap-Methode 912 ToBoolean-Methode 336 ToByte-Methode 336 ToChar-Methode 336 ToDateTime-Methode 336 Today-Eigenschaft 320 ToDecimal-Methode 336 ToDouble-Methode 336 ToInt16-, -32-, -64-Methoden 336 ToolBar-Steuerelement 795 mit Drag&Drop verschieben 821 ToolTips anzeigen 680 ToolBar.ToolBarButtonCollection-Klasse 797 Toolbox Bedienung 579 Fensterrahmen 711 Steuerelemente einfügen 580
1071 Steuerelemente in ein Formular einfügen 561 ToolTip-Steuerelement 680 bei Listenfeldern 681 ToolBar-Steuerelement 797 ToolTipText-Eigenschaft 680, 797 Top-Eigenschaft Margins-Klasse 972 Steuerelement 584, 928 ToPage-Eigenschaft 976 Topic-Konstante 683 TopIndex-Eigenschaft 613 TopLevelControl-Eigenschaft 592 TopMost-Eigenschaft 713 als Hilfsmittel zur Fehlersuche 835 Beispiel 689 ToSByte-Methode 336 ToSingle-Methode 336 ToString-Methode 299, 336 Enum-Klasse 137 mit Formatierung 340 selbst programmieren 228 StringWriter-Klasse 432 TotalDays-Eigenschaft 324 TotalHours-Eigenschaft 324 TotalMinutes-Eigenschaft 324 TotalSeconds-Eigenschaft 324 ToUInt16, -32, 64-Methoden 336 TrackBar-Steuerelement 666 TrainlingForeColor-Eigenschaft 662 Transformationen Beispiel (rotierter Text) 886 Grafik 861 Transform-Eigenschaft 861 TranslateTransform-Methode 861 Translation (Grafik) 861 TransparencyKey-Eigenschaft 712 Transparentes Fenster 572 TreeNodeCollection-Klasse 649 TreeNode-Klasse 649 TreeView-Steuerelement 649 Beispiel (Kontextmenü) 787 DragItem-Ereignis 818 ToolTips anzeigen 681 Trigonometrische Funktionen 293 Trim-Methode 302 Trimming-Eigenschaft 882 TrueType-Schriften 868 Truncate-Konstante 447
1072 Truncate-Methode 842 Try 479 Type-1-Schriften 868 Type-Klasse 156 TypeName-Methode 154 TypeOf-Operator 154 U Überladung 174 Überwachungsfenster 494 UBound 140 Ucase-Methode 302 Uhrzeit 319 UInt16-, -32-, -64-Klasse 131 Umgebungsvariablen 508 Umschalt-Button 598 unboxing (ValueType-Klassen) 147 Underline-Konstante 880 Undo-Methode 605 Unicode 129 Declare-Anweisung 548 in Steuerelementen anzeigen 639 Kennzeichnung (BOM) 423 Programmcode speichern 129 Steuerelemente 582 Textdateien 422 Zeichenketten 309 UnicodeEncoding 311 UnicodePlainText-Konstante 609 Unindent-Methode 498 Union-Methode 863 Rectangle-Klasse, Beispiel 950 Region-Klasse 919 Unit-Eigenschaft 874 UnlockBits-Methode 954 Unlock-Methode 441 unmanaged code 58 API-Funktionen 547 unsafe code 58, 954 Unsichtbare Steuerelemente 95 UnTabify-Beispielsfunktion 990 Unterprogramm 167 Until (Do-Loop-Schleifen) 166 Update-Methode 584 UpDown-Steuerelement (VB6) 92, 579 UseAntiAlias-Eigenschaft 969, 997 UseItemStyleForSubItems-Eigenschaft 637 UserControl-Klasse 700, 701 UserDomainName-Eigenschaft 509
Stichwortverzeichnis UserInteractive-Eigenschaft 510 UserName-Eigenschaft 510 UserPaint-Konstante 946 UTC (universal time, coordinated) 321 UtcNow-Eigenschaft 321 UTF-7 310 UTF-8 310 Kennzeichnung 423 Textdateien 422 UTF-16 309 Kennzeichnung 423 UTF-32 310 Kennzeichnung 423
V Val-Methode 335 Validated-Ereignis 590 ValidateNames-Eigenschaft 418 Validating-Ereignis 590 NumericUpDown-Steuerelement 668 TextBox-Steuerelement 606 Validierung von Eingaben (Steuerelemente) 590 Value-Eigenschaft 357 DateTimePicker-Steuerelement 664 NumericUpDown-Steuerelement 668 ProgressBar-Steuerelement 667 TrackBar-Steuerelement 666 ValueChanged-Ereignis DateTimePicker-Steuerelement 664 NumericUpDown-Steuerelement 668 ValueMember-Eigenschaft 615 ValueType-Klasse 120, 147, 151 ByVal 176 Strukturen 242 Variablen 116 Defaultwerte 118 Deklaration 116 Deklaration, Kurzschreibweise 116 Gültigkeitsbereiche (scope) 277 Initialisierung 118, 119 Konvertierung 331 lokale 170 Namen 117 Objektvariablen 119, 122, 188 Shared 237 Shared, Beispiel 241 Speicherbedarf 151
Stichwortverzeichnis statische 171 Typen 127 überwachen (Debugging) 494 Verwaltung (Interna) 147 Zuweisungen 123 VB.NET versus C# 67 VB.NET-Compiler 74 VB6-Migrationsassistent 109 vbc.exe 74 vbCr-Konstante 300 vbCrLf-Konstante 300 vbLf-Konstante 300 VBMath-Klasse 296 vbNewLine-Konstante 300 vbNullChar-Konstante 300 vbTab-Konstante 300 Vektorgrafikformat 912 Verankerung von Steuerelementen 585 Vererbung 245 Beispiel (FolderBrowser) 420 Formulare 729 Me, MyClass und MyBase 249 Objektbrowser 206 Overloads, Overrides und Shadows 248 Schnittstellen 257 Steuerelemente 697 versus Schnittstellen 259 Vergleich Objekte (IComparable) 366 Zeichenketten 305 Verschiebeoperationen siehe auch Drag&Drop 817 Version-Eigenschaft 510 Version-Klasse 510 VerticalTab-Konstante 300 Vertikale Textausrichtung 882 Vertikaler Text 881 Verzeichnis 384, 389 aktuelles 402 anzeigen (DirListBox) 385 Attribute 410 auswählen 419 enthaltene Dateien 391 erzeugen 398 kopieren 399 löschen 399 Name kürzen (Textausgabe) 882 rekursiv durchlaufen 394 spezielles 402
1073 synchronisieren (Beispiel) 407 temporäres Verzeichnis 403 Trennzeichen 390 überwachen 456 Unterverzeichnisse ermitteln 391 verschieben/umbenennen 399 Verzeichnisbaum durchlaufen 394 Windows-Verzeichnis 404 Zugriffsrechte 402 Verzweigungen 162 View-Aufzählung 635 View-Eigenschaft 635 VirtualScreen-Eigenschaft 745 Visible-Eigenschaft 583 Fenster 711 NotifyIcon-Steuerelement 688 VisibleChanged-Ereignis 711 VisitedLinkColor-Eigenschaft 602 VisualBasic-Namensraum 163, 294, 384, 508, 516, 522 VolumeSeparatorChar-Eigenschaft 406, 416 VS.NET 1025 VScrollBar-Steuerelement 665 W WaitCursor-Konstante 809 WaitForPendingFinalizers-Methode 150 WaitHandle-Objekt 451 Webseite anzeigen 517 per LinkLabel 601 Web-Services 66, 105 WeekDay-Methode 322 Weitergabe-Projekte 1004 WelcomeText-Eigenschaft 1015 Wertparameter 174 Werttypen 120 Speicherverwaltung 147 Strukturen 242 When (Catch) 481 While (Do-Loop-Schleifen) 166 white space 302 Width-Eigenschaft BitmapData-Klasse 954 StatusBarPanel-Klasse 801 Steuerelement 585, 928 Win32_LogicalDisk-Tabelle (WMI) 512 win32api.txt 552 Win32NT-Konstante 511 Win32S-Konstante 511
1074 Win32Windows-Konstante 511 Winding-Konstante 918 Windowless-Steuerelemente (VB6) 93, 576 Windows Eigenschaften feststellen 745 installierte Funktionen feststellen 746 Windows Form Designer 722 Code 45, 722 verschwundene Steuerelemente 726 Windows Installer 1007 Windows Management Instrumentation 511 Windows XP Optik 730 Windows-Systemverzeichnis ermitteln 404 Windows-Tasten 807 Windows-Verzeichnis ermitteln 404 Windows.Forms.Design-Namensraum 419 Windows.Forms-Namensraum 560, 929 Klassenhierarchie 573 Steuerelemente 576 WindowsDefaultBounds-Konstante 713 WindowsDefaultLocation-Konstante 760 WindowsDefaultPosition-Konstante 713 WindowState-Eigenschaft 689, 711 WinSock-Steuerelement (VB6) 93 With 189 Beispiel 526 WithEvents 263 Beispiel (PrintDocument-Klasse) 993 WMI 511 Wochentag 322 Word (Automation) 526 WordWrap-Eigenschaft 603 WorkingArea-Eigenschaft 745, 746 WorkingSet-Eigenschaft 510 World-Konstante 860 WrapLine-Beispielsfunktion 990 WrapMode-Eigenschaft 858 Wrappable-Eigenschaft 797 Wrapper-Bibliothek 520 Write-Konstante 447, 448 Write-Methode 428, 437, 444, 498 WriteByte-Methode 437 WriteIf-Methode 498 WriteLineIf-Methode 498 WriteLine-Methode 428 Console-Klasse 33 Debug-Klasse 498 WriteOnly (Eigenschaften) 231
Stichwortverzeichnis WriteTo-Methode 443 X xcopy-Installation 1004 Xml.Serialization-Namensraum 459 XmlSerializer-Klasse 459 Xor-Methode 919 Xor-Operator 181 XOR-Zeichenmodus 916, 930 XP-Optik 730 Y Year-Methode 322 Z Zahlen formatieren 343, 350 Zeichenketten 298 BinaryReader/-Writer-Klassen 444 byteweise bearbeiten 86 Datei- und Verzeichnisnamen 405 effizient zusammensetzen 312 in andere Datentypen umwandeln 337 Initialisierung 299 Interna 308 Kompatibilität mit VB6 86 Konstanten 300 Nothing 309 StringBuilder-Klasse 312 StringReader, -Writer-Klasse 432 Variablentyp 129 Vergleich 305 vordefinierte Konstanten 300 Zeichenmodus 916, 930 Zeichenqualität 915 Zeichensatz ändern (Textdatei) 431 Zeiger (API-Funktionen) 550 Zeiger (pointer) auf Prozeduren 267 Zeilen aus Textdatei lesen 425 Zeit DateTimePicker-Steuerelement 662 messen 327 MonthCalender-Steuerelement 662 Zeitdifferenz in Jahren 327 Zeiten formatieren 347, 352 Zeitspanne 323 Zentrierter Text 882 Zonensicherheit 63 Zoom-Eigenschaft 996 Zufallszahlen 296
Stichwortverzeichnis Zugriffsrechte von Dateien 402 Zusatzsteuerelement siehe auch Steuerelement 88, 576 Zustandsanzeige 667
1075 Zustandstasten 804, 810 Zuweisungen 123 Zwischenablage 814
Copyright Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir lediglich als Einzelplatz-Lizenz zur Verfügung! Jede andere Verwendung dieses eBooks und zugehöriger Materialien und Informationen, einschliesslich der Reproduktion, der Weitergabe, des Weitervertriebs, der Plazierung auf anderen Websites, der Veränderung und der Veröffentlichung bedarf der schriftlichen Genehmigung des Verlags. Bei Fragen zu diesem Thema wenden Sie sich bitte an: mailto:[email protected]
Zusatzdaten Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die Zurverfügungstellung dieser Daten auf der Website ist eine freiwillige Leistung des Verlags. Der Rechtsweg ist ausgeschlossen.
Hinweis Dieses und andere eBooks können Sie rund um die Uhr und legal auf unserer Website
(http://www.informit.de) herunterladen