Richard Kaiser
C++ mit
Microsoft Visual C++ 2008 Einführung in Standard-C++, C++/CLI und die objektorientierte Windows .NET-Programmierung
ABC
Prof. Richard Kaiser Schwärzlocher Str. 53 72070 Tübingen Deutschland http://www.rkaiser.de/
Xpert.press ISSN 1439-5428 ISBN 978-3-540-23869-0 e-ISBN 978-3-540-68844-0 DOI 10.1007/978-3-540-68844-0 Springer Heidelberg Dordrecht London New York Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. © Springer-Verlag Berlin Heidelberg 2009 Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Übersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Der Springer-Verlag ist nicht Urheber der Daten und Programme. Weder der Springer-Verlag noch der Autor übernehmen Haftung für die CD-ROM und das Buch, einschließlich ihrer Qualität, Handelsoder Anwendungseignung. In keinem Fall übernehmen der Springer-Verlag oder der Autor Haftung für direkte, indirekte, zufällige oder Folgeschäden, die sich aus der Nutzung der CD-ROM oder des Buches ergeben. Einbandentwurf: KuenkelLopka GmbH, Heidelberg Gedruckt auf säurefreiem Papier Springer ist Teil der Fachverlagsgruppe Springer Science+Business Media (www.springer.com)
Für Daniel, Alex und Kathy
Geleitwort
Wenn man heute nach Literatur über Programmiersprachen sucht, so findet man viel Neues über Sprachen wie C# und Java, die von einer virtuellen Maschine ausgeführt werden, aber auch über die dynamischen Sprachen wie Python, Ruby und PHP. Es könnte der Eindruck entstehen, dass Softwareentwicklung im Wesentlichen mit diesen Sprachen stattfindet. Dies ist aber nicht der Fall. Ein nicht zu unterschätzender Teil der professionellen Softwareentwicklung wird auf der Basis der Programmiersprache C++ durchgeführt. In meiner beruflichen Praxis, in der ich Unternehmen betreue, die mit den Softwareentwicklungswerkzeugen von Microsoft arbeiten, begegne ich häufig der Programmiersprache C++. C++ ist auch heute immer noch die erste Wahl, wenn es darum geht, hocheffiziente Software für den technisch-wissenschaftlichen Bereich oder hardwarenahe Aufgaben zu entwickeln. Man darf auch nicht vergessen, dass für C++ umfangreiche, qualitativ hochwertige und plattformunabhängige Klassenbibliotheken für viele Anwendungsdomänen existieren. Ein anderer Vorteil von C++ ist meiner Ansicht nach, dass die Sprache sowohl das prozedurale als auch das objektorientierte Entwicklungsparadigma unterstützt. Für Microsoft spielt C++ weiterhin eine wichtige Rolle bei der Softwareentwicklung im eigenen Haus, aber auch in der Weiterentwicklung der Werkzeuge für die Programmierung mit C++, um diese noch produktiver und sicherer zu machen. Microsoft arbeitet aktiv im ISO Komitee zur Standardisierung von C++ mit und hat mit dem Standard ECMA-372 (C++/CLI) C++ für den Einsatz auf der .NET Laufzeitumgebung erweitert. Außerdem ist die einfache Integration von nativen C++ Klassenbibliotheken mit Programmen, die für Microsoft .NET entwickelt werden bzw. wurden, eine nicht zu unterschätzende Eigenschaft der .NET Laufzeitumgebung, einmal unter dem Gesichtspunkt des Investitionsschutzes, als auch unter dem Aspekt der Laufzeiteffizienz. Das vorliegende Buch von Richard Kaiser, der selbst aktiv im DIN an der Standardisierung von C++ mitarbeitet, gibt eine umfassende Einführung in alle Aspekte von Standard C++, sowie in die Spezialitäten von C++ auf der Microsoft Windows Plattform und C++/CLI für Microsoft .NET. Die Kapitel 1 und 2 bieten eine sehr gute Einführung in das Arbeiten mit Microsoft Visual Studio 2008. Sehr positiv ist noch, dass die prozeduralen und objektorientierten Eigenschaften von C++ explizit dargestellt werden, und dass in Kapitel 3.7 eine Einführung in die Techniken und Möglichkeiten der Programmverifikation im Kontext von C++
iv
Geleitwort
gegeben wird. Diese Techniken werden an Bedeutung gewinnen, um die Qualität und Sicherheit von Software besser zu gewährleisten. Ich wünsche dem Buch viel Erfolg, denn es bietet eine fundierte Einführung in alle Aspekte der Softwareentwicklung mit C++ auf der Microsoft Windows Plattform, einschließlich dem .NET Framework, und es trägt dazu bei, den Einstieg in diese mächtige Programmiersprache zu erleichtern. Klaus Rohe, Platform Strategy Manager, Developer Platform & Strategy Group, Microsoft Deutschland GmbH
Vorwort
Dieses Buch entstand ursprünglich aus dem Wunsch, in meinen Vorlesungen über C++ nicht nur Textfensterprogramme (Konsolenanwendungen), sondern von Anfang an Windows-Programme zu entwickeln. Dafür ist Visual Studio 2008 sehr gut geeignet. Es ist so einfach zu bedienen, dass man es auch in Anfängervorlesungen einsetzen kann, ohne dabei Gefahr zu laufen, dass die Studenten nur noch mit dem Entwicklungssystem kämpfen und gar nicht mehr zum Programmieren kommen. Dieses Buch richtet sich aber nicht nur an Anfänger, sondern ebenso an professionelle Software-Entwickler. In den letzten 10 Jahren habe ich zahlreiche C++-Kurse für Entwickler aus der Industrie gehalten. Dabei wurde ich mit einer Fülle von Anregungen aus ihrer täglichen Arbeit konfrontiert, die dem Buch viele praxisorientierte Impulse gaben. Für diese Leser wird C++ und C++/CLI umfassend dargestellt. Dieses Buch besteht aus drei Teilen: – Teil 1 stellt die Entwicklungsumgebung Visual Studio 2008 vor und zeigt, wie man Windows-Programme mit den wichtigsten Steuerelementen entwickelt. – Teil 2 stellt den gesamten Sprachumfang des C++-Standards umfassend vor. Dazu gehören nicht nur die Sprachelemente von C sowie Klassen, Templates und Exception-Handling, sondern auch die C++-Standardbibliothek. – Teil 3 behandelt den gesamten C++/CLI-Standard und gibt einen Einblick in die .NET Klassenbibliothek. Die C++/CLI-Erweiterungen bieten die Möglichkeit, mit C++ Windows .NET-Programme zu schreiben und die .NET Klassenbibliothek zu nutzen. Insbesondere kann man bestehende C++-Quelltexte in Windows .NET-Anwendungen aufzunehmen und so vorhandenen Code nutzen. Die Programmiersprache C++ wurde als Obermenge der Programmiersprache C entworfen. Dieser Entscheidung verdankt C++ sicher seine weite Verbreitung. Sie hat aber auch dazu geführt, dass oft weiterhin wie in C programmiert wird und lediglich ein C++-Compiler anstelle eines C-Compilers verwendet wird. Dabei werden viele Vorteile von C++ verschenkt. Um nur einige zu nennen:
x
Vorwort
– In C++ werden die fehleranfälligen Zeiger viel seltener als in C benötigt. – Die Stringklassen lassen sich wesentlich einfacher und risikoloser als die nullterminierten Strings von C verwenden. – Die Containerklassen der C++-Standardbibliothek haben viele Vorteile gegenüber Arrays, selbstdefinierten verketteten Listen oder Bäumen. – Exception-Handling bietet eine einfache Möglichkeit, auf Fehler zu reagieren. – Objektorientierte Programmierung ermöglicht übersichtlichere Programme. – Templates sind die Basis für eine außerordentlich vielseitige Standardbibliothek. Ich habe versucht, bei allen Konzepten nicht nur die Sprachelemente und ihre Syntax zu beschreiben, sondern auch Kriterien dafür anzugeben, wann und wie man sie sinnvoll einsetzen kann. Deshalb wurde z.B. mit der objektorientierten Programmierung eine Einführung in die objektorientierte Analyse und das objektorientierte Design verbunden. Ohne die Beachtung von Design-Regeln schreibt man leicht Klassen, die der Compiler zwar übersetzen kann, die aber kaum hilfreich sind. Man hört immer wieder die Meinung, dass C++ viel zu schwierig ist, um es als einführende Programmiersprache einzusetzen. Dieses Buch soll ein in vielen Jahren erprobtes Gegenargument zu dieser Meinung sein. Damit will ich aber die Komplexität von C++ überhaupt nicht abstreiten. Zahlreiche Übungsaufgaben geben dem Leser die Möglichkeit, die Inhalte praktisch anzuwenden und so zu vertiefen. Da man Programmieren nur lernt, indem man es tut, möchte ich ausdrücklich dazu ermuntern, zumindest einen Teil der Aufgaben zu lösen und sich dann selbst neue Aufgaben zu stellen. Der Schwierigkeitsgrad der Aufgaben reicht von einfachen Wiederholungen des Textes bis zu kleinen Projektchen, die ein gewisses Maß an selbständiger Arbeit erfordern. Die Lösungen der meisten Aufgaben findet man auf der beiliegenden CD und auf meiner Internetseite http://www.rkaiser.de. Anregungen, Korrekturhinweise und Verbesserungsvorschläge sind willkommen. Bitte senden Sie diese an die Mail-Adresse auf meiner Internetseite. Bei meinen Schulungskunden und Studenten bedanke ich mich für die zahlreichen Anregungen und anregenden Fragen. Herrn Engesser und seinem Team vom Springer-Verlag danke ich für die Unterstützung und Geduld. Tübingen, im Juni 2009
Richard Kaiser
Inhalt
Teil 1: Windows .NET Programme mit Visual Studio 1 Die Entwicklungsumgebung................................................................ 1 1.1 Visuelle Programmierung: Ein erstes kleines Programm ............................1 1.2 Erste Schritte in C++...................................................................................6 1.3 Der Quelltexteditor .....................................................................................8 1.4 Kontextmenüs und Symbolleisten (Toolbars) ...........................................13 1.5 Projekte, Projektdateien und Projektoptionen...........................................14 1.6 Einige Tipps zur Arbeit mit Projekten ......................................................16 1.7 Die Online-Hilfe (MSDN Dokumentation)...............................................19 1.8 Projektmappen und der Projektmappen-Explorer Ԧ .................................23 1.9 Hilfsmittel zur Gestaltung von Formularen Ԧ ...........................................24 1.10 Win32-, MFC- und Konsolen-Anwendungen Ԧ .......................................25 1.10.1 Win32-Anwendungen Ԧ ...................................................................26 1.10.2 MFC-Anwendungen Ԧ .....................................................................26 1.10.3 Win32 Konsolen-Anwendungen Ԧ...................................................29 1.10.4 CLR Konsolen-Anwendungen Ԧ ......................................................31 1.10.5 Der Start des Compilers von der Kommandozeile Ԧ........................32 1.11 Setup-Projekte für den Windows-Installer Ԧ ............................................32 1.11.1 .NET-Laufzeitbibliotheken: Das Redistributable-Package ...............33 1.11.2 Optionen für das Setup-Programm Ԧ ...............................................34
xii
Inhalt
2 Steuerelemente für die Benutzeroberfläche .................................... 39 2.1 Die Online-Hilfe zu den Steuerelementen.................................................39 2.1.1 Die Online-Hilfe über den Index ......................................................40 2.1.2 Die Online-Hilfe mit F1 ...................................................................43 2.2 Namen.......................................................................................................44 2.3 Labels, Datentypen und Compiler-Fehlermeldungen................................46 2.4 Funktionen, Methoden und die Komponente TextBox ..............................52 2.4.1 Funktionen........................................................................................53 2.4.2 Mehrzeilige TextBoxen ....................................................................57 2.5 Klassen, ListBox und ComboBox.............................................................59 2.6 Buttons und Ereignisse..............................................................................64 2.6.1 Parameter der Ereignisbehandlungsroutinen ....................................65 2.6.2 Der Fokus und die Tabulatorreihenfolge ..........................................67 2.6.3 Einige weitere Eigenschaften von Buttons .......................................68 2.7 CheckBoxen, RadioButtons und einfache if-Anweisungen.......................70 2.8 Container-Komponenten: GroupBox, Panel, TabControl .........................72 2.9 Hauptmenüs und Kontextmenüs................................................................74 2.9.1 Hauptmenüs und der Menüdesigner .................................................74 2.9.2 Kontextmenüs...................................................................................77 2.10 Standarddialoge ........................................................................................78 2.11 Einfache Meldungen mit MessageBox::Show anzeigen............................82 2.12 Eine Vorlage für viele Projekte und Übungsaufgaben ..............................83
Teil 2: Standard-C++ 3 Elementare Datentypen und Anweisungen...................................... 91 3.1 Syntaxregeln .............................................................................................91 3.2 Variablen und Bezeichner.........................................................................95 3.3 Ganzzahldatentypen ..................................................................................98 3.3.1 Die interne Darstellung von Ganzzahlwerten .................................101 3.3.2 Ganzzahlliterale und ihr Datentyp ..................................................104 3.3.3 Zuweisungen und Standardkonversionen bei Ganzzahlausdrücken106 3.3.4 Operatoren und die „üblichen arithmetischen Konversionen“........109 3.3.5 Der Datentyp bool ..........................................................................114 3.3.6 Die Datentypen char und wchar_t (Char) ......................................119 3.3.7 C++/CLI-Ganzzahldatentypen: Int32, Int64 usw............................125 3.4 Kontrollstrukturen und Funktionen .........................................................126 3.4.1 Die if- und die Verbundanweisung .................................................126 3.4.2 Wiederholungsanweisungen ...........................................................131 3.4.3 Funktionen und der Datentyp void..................................................134
Inhalt
xiii
3.4.4 Eine kleine Anleitung zum Erarbeiten der Lösungen .....................138 3.4.5 Werte- und Referenzparameter.......................................................142 3.4.6 Die Verwendung von Bibliotheken und Namensbereichen ............142 3.4.7 Zufallszahlen ..................................................................................144 3.5 Tests und der integrierte Debugger .........................................................146 3.5.1 Systematisches Testen ....................................................................146 3.5.2 Unit-Tests: Testfunktionen für automatisierte Tests.......................152 3.5.3 Unit-Tests in Visual Studio 2008 ...................................................155 3.5.4 Der integrierte Debugger ................................................................158 3.6 Gleitkommadatentypen ...........................................................................162 3.6.1 Die interne Darstellung von Gleitkommawerten.............................163 3.6.2 Der Datentyp von Gleitkommaliteralen..........................................166 3.6.3 Standardkonversionen ....................................................................167 3.6.4 Mathematische Funktionen in Standard-C++ und in .NET.............172 3.6.5 C++/CLI-Gleitkommadatentypen: Single, Double und Decimal ....174 3.6.6 Datentypen für exakte und kaufmännische Rechnungen.................175 3.6.7 Ein Kriterium für annähernd gleiche Gleitkommazahlen ...............183 3.7 Programmverifikation und Programmierlogik ........................................185 3.7.1 Ablaufprotokolle.............................................................................186 3.7.2 Schleifeninvarianten mit Ablaufprotokollen erkennen ...................189 3.7.3 Symbolische Ablaufprotokolle .......................................................193 3.7.4 Schleifeninvarianten und vollständige Induktion Ԧ........................200 3.7.5 Verifikationen, Tests und Bedingungen zur Laufzeit prüfen ..........207 3.7.6 Funktionsaufrufe und Programmierstil für Funktionen...................212 3.7.7 Einfache logische Regeln und Wahrheitstabellen Ԧ .......................219 3.7.8 Bedingungen in und nach if-Anweisungen und Schleifen Ԧ...........221 3.8 Konstanten ..............................................................................................230 3.9 Syntaxregeln für Deklarationen und Initialisierungen Ԧ .........................233 3.10 Arrays und Container ..............................................................................235 3.10.1 Einfache typedef-Deklarationen......................................................236 3.10.2 Eindimensionale Arrays..................................................................236 3.10.3 Die Initialisierung von Arrays bei ihrer Definition.........................243 3.10.4 Arrays als Container .......................................................................245 3.10.5 Mehrdimensionale Arrays...............................................................252 3.10.6 Dynamische Programmierung.........................................................256 3.11 Strukturen und Klassen ...........................................................................257 3.11.1 Mit struct definierte Klassen ..........................................................258 3.11.2 C++/CLI-Erweiterungen: Mit struct definierte Werteklassen.........264 3.11.3 Mit union definierte Klassen Ԧ ......................................................266 3.11.4 Bitfelder Ԧ......................................................................................269 3.12 Zeiger, Strings und dynamisch erzeugte Variablen.................................271 3.12.1 Die Definition von Zeigervariablen ................................................272 3.12.2 Der Adressoperator, Zuweisungen und generische Zeiger .............275 3.12.3 Ablaufprotokolle für Zeigervariable...............................................279 3.12.4 Dynamisch erzeugte Variablen: new und delete .............................280 3.12.5 Garbage Collection mit der Smart Pointer Klasse shared_ptr........290 3.12.6 Dynamische erzeugte eindimensionale Arrays ...............................292
xiv
Inhalt
3.12.7 Arrays, Zeiger und Zeigerarithmetik ..............................................294 3.12.8 Arrays als Funktionsparameter Ԧ ...................................................298 3.12.9 Konstante Zeiger ............................................................................301 3.12.10 Stringliterale, nullterminierte Strings und char*-Zeiger.................303 3.12.11 Konversionen zwischen char* und String ......................................306 3.12.12 Verkettete Listen ............................................................................311 3.12.13 Binärbäume ....................................................................................322 3.12.14 Zeiger als Parameter Ԧ ...................................................................325 3.12.15 C++/CLI-Erweiterungen: Garbage Collection und der GC-Heap...327 3.12.16 C-Bibliotheksfunktionen in string.h für nullterminierte Strings Ԧ..329 3.12.17 Die „Secure Library Functions“ Ԧ..................................................333 3.12.18 Zeiger auf Zeiger auf Zeiger auf ... Ԧ .............................................334 3.13 C++/CLI-Erweiterungen: Die Stringklasse String ..................................335 3.13.1 Die Definition von Variablen eines Klassentyps ............................336 3.13.2 Elementfunktionen der Klasse String .............................................338 3.14 Deklarationen mit typedef und typeid-Ausdrücke ...................................342 3.15 Aufzählungstypen ...................................................................................344 3.15.1 enum Konstanten und Konversionen Ԧ ..........................................346 3.15.2 C++/CLI-Aufzählungstypen Ԧ .......................................................348 3.16 Kommentare und interne Programmdokumentation................................348 3.16.1 Kommentare zur internen Dokumentation......................................349 3.16.2 Dokumentationskommentare ..........................................................352 3.17 Globale, lokale und dynamische Variablen.............................................352 3.17.1 Die Deklarationsanweisung ............................................................352 3.17.2 Die Verbundanweisung und der lokale Gültigkeitsbereich.............353 3.17.3 Statische lokale Variablen ..............................................................355 3.17.4 Lebensdauer von Variablen und Speicherklassenspezifizierer Ԧ ...355 3.18 Referenztypen, Werte- und Referenzparameter ......................................358 3.18.1 Werteparameter ..............................................................................359 3.18.2 Referenzparameter..........................................................................360 3.18.3 Konstante Referenzparameter.........................................................363 3.19 Weitere Anweisungen .............................................................................365 3.19.1 Die Ausdrucksanweisung................................................................366 3.19.2 Exception-Handling: try und throw ................................................368 3.19.3 Die switch-Anweisung Ԧ ................................................................372 3.19.4 Die do-Anweisung Ԧ ......................................................................375 3.19.5 Die for-Anweisung Ԧ .....................................................................376 3.19.6 Die Sprunganweisungen goto, break und continue Ԧ.....................378 3.19.7 Assembler-Anweisungen Ԧ ............................................................381 3.20 Ausdrücke ...............................................................................................382 3.20.1 Primäre Ausdrücke Ԧ .....................................................................383 3.20.2 Postfix-Ausdrücke Ԧ ......................................................................385 3.20.3 Unäre Ausdrücke Ԧ ........................................................................386 3.20.4 Typkonversionen in Typecast-Schreibweise Ԧ...............................389 3.20.5 Zeiger auf Klassenelemente Ԧ ........................................................389 3.20.6 Multiplikative Operatoren Ԧ ..........................................................389 3.20.7 Additive Operatoren Ԧ ...................................................................390
Inhalt
xv
3.20.8 Shift-Operatoren Ԧ .........................................................................390 3.20.9 Vergleichsoperatoren Ԧ..................................................................391 3.20.10 Gleichheitsoperatoren Ԧ .................................................................392 3.20.11 Bitweise Operatoren Ԧ ...................................................................393 3.20.12 Logische Operatoren Ԧ...................................................................394 3.20.13 Der Bedingungsoperator Ԧ.............................................................394 3.20.14 Konstante Ausdrücke Ԧ..................................................................396 3.20.15 Zuweisungsoperatoren....................................................................396 3.20.16 Der Komma-Operator Ԧ.................................................................397 3.20.17 L-Werte und R-Werte Ԧ .................................................................399 3.20.18 Die Priorität und Assoziativität der Operatoren Ԧ..........................399 3.20.19 Alternative Zeichenfolgen Ԧ ..........................................................402 3.20.20 Explizite Typkonversionen Ԧ .........................................................403 3.21 Namensbereiche......................................................................................410 3.21.1 Die Definition von benannten Namensbereichen............................411 3.21.2 Die Verwendung von Namen aus Namensbereichen ......................413 3.21.3 Header-Dateien und Namensbereiche.............................................416 3.21.4 Aliasnamen für Namensbereiche ....................................................418 3.21.5 Unbenannte Namensbereiche .........................................................419 3.22 Präprozessoranweisungen .......................................................................421 3.22.1 Die #include-Anweisung ................................................................422 3.22.2 Makros Ԧ........................................................................................423 3.22.3 Bedingte Kompilation Ԧ.................................................................428 3.22.4 Pragmas Ԧ ......................................................................................434 3.23 Separate Kompilation und statische Bibliotheken...................................437 3.23.1 C++-Dateien, Header-Dateien und Object-Dateien ........................437 3.23.2 Bindung Ԧ ......................................................................................439 3.23.3 Deklarationen und Definitionen Ԧ..................................................441 3.23.4 Die „One Definition Rule“ Ԧ..........................................................443 3.23.5 Die Elemente von Header-Dateien und C++-Dateien Ԧ.................445 3.23.6 Object-Dateien und statische Bibliotheken linken Ԧ ......................447 3.23.7 Der Aufruf von in C geschriebenen Funktionen Ԧ .........................447
4 Die C++-Standardbibliothek ........................................................... 449 4.1 Die Stringklassen string und wstring ......................................................449 4.1.1 Gemeinsamkeiten und Unterschiede der Stringklassen ..................450 4.1.2 Konversionen zwischen string und String ......................................452 4.1.3 Einige Elementfunktionen der Klasse string...................................453 4.1.4 Stringstreams ..................................................................................457 4.2 Sequenzielle Container der Standardbibliothek ......................................463 4.2.1 Die Container-Klasse vector...........................................................463 4.2.2 Iteratoren ........................................................................................466 4.2.3 Die STL/CLR Containerklassen in Visual C++ 2008.....................470 4.2.4 Algorithmen der Standardbibliothek ..............................................473 4.2.5 Die Speicherverwaltung bei Vektoren Ԧ ........................................480
xvi
Inhalt
4.2.6 Mehrdimensionale Vektoren Ԧ.......................................................482 4.2.7 Die Container-Klassen list und deque ............................................484 4.2.8 Gemeinsamkeiten und Unterschiede der sequenziellen Container..485 4.2.9 Die Container-Adapter stack, queue und priority_queue Ԧ ...........487 4.2.10 Container mit Zeigern.....................................................................489 4.2.11 Geprüfte Iteratoren (Checked Iterators)..........................................490 4.2.12 Die Container-Klasse bitset Ԧ ........................................................492 4.3 Dateibearbeitung mit den Stream-Klassen ..............................................494 4.3.1 Stream-Variablen, ihre Verbindung mit Dateien und ihr Zustand ..494 4.3.2 Fehler und der Zustand von Stream-Variablen ...............................499 4.3.3 Lesen und Schreiben von Binärdaten mit read und write...............500 4.3.4 Lesen und Schreiben von Daten mit den Operatoren >....509 4.3.5 Manipulatoren und Funktionen zur Formatierung von Texten Ԧ ...517 4.3.6 Dateibearbeitung im Direktzugriff Ԧ..............................................519 4.3.7 C-Funktionen zur Dateibearbeitung Ԧ............................................523 4.4 Assoziative Container .............................................................................526 4.4.1 Die Container set und multiset........................................................526 4.4.2 Die Container map und multimap...................................................528 4.4.3 Iteratoren der assoziativen Container .............................................530 4.5 Die numerischen Klassen der Standardbibliothek...................................533 4.5.1 Komplexe Zahlen Ԧ........................................................................533 4.5.2 Valarrays Ԧ.....................................................................................536 4.6 TR1-Erweiterungen der Standardbibliothek Ԧ........................................538 4.6.1 Ungeordnete Assoziative Container (Hash-Container)...................539 4.6.2 Reguläre Ausdrücke mit Ԧ ...............................................542 4.6.3 Fixed Size Array Container mit Ԧ.....................................545 4.6.4 Tupel mit Ԧ .......................................................................547 4.6.5 Zufallszahlen mit Ԧ.......................................................548 4.7 Die Boost-Bibliotheken Ԧ.......................................................................550
5 Funktionen ........................................................................................ 551 5.1 Die Verwaltung von Funktionsaufrufen über den Stack..........................552 5.1.1 Aufrufkonventionen Ԧ....................................................................555 5.2 Funktionszeiger und der Datentyp einer Funktion ..................................555 5.2.1 Der Datentyp einer Funktion ..........................................................555 5.2.2 Zeiger auf Funktionen.....................................................................557 5.3 Rekursion ................................................................................................563 5.3.1 Grundlagen .....................................................................................564 5.3.2 Quicksort ........................................................................................569 5.3.3 Ein rekursiv absteigender Parser ....................................................573 5.3.4 Rekursiv definierte Kurven Ԧ.........................................................578 5.3.5 Indirekte Rekursion Ԧ ....................................................................581 5.3.6 Rekursive Datenstrukturen und binäre Suchbäume ........................581 5.3.7 Verzeichnisse rekursiv nach Dateien durchsuchen Ԧ .....................585 5.4 Die Funktionen main und _tmain und ihre Parameter.............................587
Inhalt
xvii
5.5 Default-Argumente .................................................................................588 5.6 Inline-Funktionen....................................................................................590 5.7 Überladene Funktionen ...........................................................................593 5.7.1 Funktionen, die nicht überladen werden können ............................594 5.7.2 Regeln für die Auswahl einer passenden Funktion .........................596 5.8 Überladene Operatoren mit globalen Operatorfunktionen ......................602 5.8.1 Globale Operatorfunktionen ...........................................................604 5.8.2 Die Inkrement- und Dekrementoperatoren .....................................605 5.8.3 Referenzen als Funktionswerte .......................................................607 5.8.4 Die Ein- und Ausgabe von selbst definierten Datentypen ..............609
6 Objektorientierte Programmierung............................................... 613 6.1 Klassen....................................................................................................614 6.1.1 Datenelemente und Elementfunktionen ..........................................614 6.1.2 Der Gültigkeitsbereich von Klassenelementen ...............................622 6.1.3 Datenkapselung: Die Zugriffsrechte private und public .................626 6.1.4 Der Aufruf von Elementfunktionen und der this-Zeiger .................632 6.1.5 Konstruktoren und Destruktoren ....................................................634 6.1.6 OO Analyse und Design: Der Entwurf von Klassen .......................645 6.1.7 Programmierlogik: Klasseninvarianten und Korrektheit ................653 6.1.8 UML-Diagramme und Klassendiagramme in Visual Studio 2008 .661 6.2 Klassen als Datentypen ...........................................................................664 6.2.1 Der Standardkonstruktor.................................................................664 6.2.2 Objekte als Klassenelemente und Elementinitialisierer ..................667 6.2.3 friend-Funktionen und -Klassen .....................................................672 6.2.4 Überladene Operatoren als Elementfunktionen ..............................676 6.2.5 Der Kopierkonstruktor....................................................................684 6.2.6 Der Zuweisungsoperator = für Klassen ..........................................690 6.2.7 Benutzerdefinierte Konversionen Ԧ ...............................................700 6.2.8 Explizite Konstruktoren..................................................................704 6.2.9 Statische Klassenelemente..............................................................705 6.2.10 Konstante Klassenelemente und Objekte........................................708 6.2.11 Klassen und Header-Dateien ..........................................................711 6.3 Vererbung und Komposition...................................................................713 6.3.1 Die Elemente von abgeleiteten Klassen..........................................713 6.3.2 Zugriffsrechte auf die Elemente von Basisklassen..........................715 6.3.3 Die Bedeutung von Elementnamen in einer Klassenhierarchie ......717 6.3.4 using-Deklarationen in abgeleiteten Klassen Ԧ ..............................719 6.3.5 Konstruktoren, Destruktoren und implizit erzeugte Funktionen.....720 6.3.6 Vererbung bei Formularen in Visual Studio ...................................726 6.3.7 OO Design: public Vererbung und „ist ein“-Beziehungen .............727 6.3.8 OO Design: Komposition und „hat ein“-Beziehungen ...................732 6.3.9 Konversionen zwischen public abgeleiteten Klassen......................734 6.3.10 protected und private abgeleitete Klassen Ԧ ..................................739 6.3.11 Mehrfachvererbung und virtuelle Basisklassen ..............................742
xviii
Inhalt
6.4 Virtuelle Funktionen, späte Bindung und Polymorphie ..........................749 6.4.1 Der statische und der dynamische Datentyp ...................................749 6.4.2 Virtuelle Funktionen.......................................................................750 6.4.3 Die Implementierung von virtuellen Funktionen: vptr und vtbl......757 6.4.4 Virtuelle Konstruktoren und Destruktoren .....................................766 6.4.5 Virtuelle Funktionen in Konstruktoren und Destruktoren ..............768 6.4.6 OO-Design: Einsatzbereich und Test von virtuellen Funktionen....769 6.4.7 OO-Design und Erweiterbarkeit .....................................................771 6.4.8 Rein virtuelle Funktionen und abstrakte Basisklassen ....................775 6.4.9 OO-Design: Virtuelle Funktionen und abstrakte Basisklassen .......778 6.4.10 OOAD: Zusammenfassung .............................................................781 6.4.11 Interfaces und Mehrfachvererbung .................................................785 6.4.12 Zeiger auf Klassenelemente Ԧ ........................................................786 6.4.13 UML-Diagramme für Vererbung und Komposition .......................791 6.5 Laufzeit-Typinformationen .....................................................................793 6.5.1 Typinformationen mit dem Operator typeid Ԧ ...............................794 6.5.2 Typkonversionen mit dynamic_cast Ԧ ...........................................797 6.5.3 Anwendungen von Laufzeit-Typinformationen Ԧ ..........................800 6.5.4 static_cast mit Klassen Ԧ ...............................................................802
7 Exception-Handling ......................................................................... 807 7.1 Die try-Anweisung ..................................................................................808 7.2 Exception-Handler und Exceptions der Standardbibliothek ...................813 7.3 Einige vordefinierte C++/CLI und .NET Exceptions..............................817 7.4 throw-Ausdrücke und selbst definierte Exceptions .................................818 7.5 Fehler und Exceptions.............................................................................824 7.6 Die Freigabe von Ressourcen bei Exceptions .........................................827 7.6.1 Ressource Aquisition is Initialization (RAII) .................................828 7.6.2 Die Smart Pointer Klasse shared_ptr .............................................829 7.7 Exceptions in Konstruktoren und Destruktoren ......................................829 7.8 Exception-Spezifikationen Ԧ ..................................................................834 7.9 Die Funktion terminate Ԧ .......................................................................836
8 Templates und die STL.................................................................... 837 8.1 Generische Funktionen: Funktions-Templates ........................................838 8.1.1 Die Deklaration von Funktions-Templates mit Typ-Parametern ....839 8.1.2 Spezialisierungen von Funktions-Templates ..................................840 8.1.3 Funktions-Templates mit Nicht-Typ-Parametern ...........................847 8.1.4 Explizit instanziierte Funktions-Templates Ԧ.................................849 8.1.5 Explizit spezialisierte und überladene Templates...........................850 8.1.6 Rekursive Funktions-Templates Ԧ .................................................853 8.2 Generische Klassen: Klassen-Templates.................................................857 8.2.1 Die Deklaration von Klassen-Templates mit Typ-Parametern .......858
Inhalt
xix
8.2.2 Spezialisierungen von Klassen-Templates......................................858 8.2.3 Templates mit Nicht-Typ-Parametern ............................................866 8.2.4 Explizit instanziierte Klassen-Templates Ԧ....................................868 8.2.5 Partielle und vollständige Spezialisierungen Ԧ ..............................868 8.2.6 TR1-Erweiterungen: Type Traits Ԧ...............................................875 8.2.7 Elemente und friend-Funktionen von Klassen-Templates Ԧ ..........878 8.2.8 Ableitungen von Templates Ԧ ........................................................881 8.3 Funktionsobjekte in der STL...................................................................884 8.3.1 Der Aufrufoperator () .....................................................................885 8.3.2 Prädikate und arithmetische Funktionsobjekte ...............................888 8.3.3 Binder, Funktionsadapter und TR1-Erweiterungen ........................892 8.4 Iteratoren und die STL-Algorithmen.......................................................899 8.4.1 Die verschiedenen Arten von Iteratoren .........................................900 8.4.2 Umkehriteratoren............................................................................902 8.4.3 Einfügefunktionen und Einfügeiteratoren.......................................903 8.4.4 Stream-Iteratoren............................................................................905 8.4.5 Container-Konstruktoren mit Iteratoren .........................................906 8.4.6 STL-Algorithmen für alle Elemente eines Containers ....................907 8.5 Die Algorithmen der STL .......................................................................910 8.5.1 Lineares Suchen..............................................................................911 8.5.2 Zählen.............................................................................................912 8.5.3 Der Vergleich von Bereichen .........................................................913 8.5.4 Suche nach Teilfolgen ....................................................................915 8.5.5 Minimum und Maximum................................................................916 8.5.6 Elemente vertauschen .....................................................................917 8.5.7 Kopieren von Bereichen .................................................................918 8.5.8 Elemente transformieren und ersetzen............................................919 8.5.9 Elementen in einem Bereich Werte zuweisen.................................921 8.5.10 Elemente entfernen .........................................................................922 8.5.11 Die Reihenfolge von Elementen vertauschen .................................923 8.5.12 Permutationen.................................................................................924 8.5.13 Partitionen ......................................................................................925 8.5.14 Bereiche sortieren...........................................................................926 8.5.15 Binäres Suchen in sortierten Bereichen ..........................................927 8.5.16 Mischen von sortierten Bereichen ..................................................929 8.5.17 Mengenoperationen auf sortierten Bereichen .................................930 8.5.18 Heap-Operationen...........................................................................931 8.5.19 Verallgemeinerte numerische Operationen.....................................932
xx
Inhalt
Teil 3: C++/CLI und die .NET Bibliothek 9 C++/CLI ............................................................................................ 937 9.1 Assemblies, CLR-Optionen und DLLs ...................................................939 9.1.1 CLR-Projekte..................................................................................939 9.1.2 CLR-Optionen ................................................................................941 9.1.3 CLR-DLLs......................................................................................942 9.1.4 Assembly-bezogene Zugriffsrechte ................................................944 9.1.5 Win32-DLLs erzeugen Ԧ ...............................................................945 9.1.6 Win32-DLLs und Win32-API Funktionen in CLR-Projekten Ԧ ....946 9.2 Garbage Collection und der GC-Heap ....................................................950 9.3 Die Basisklasse System::Object und ihre Methoden ...............................956 9.4 Die Stringklasse String............................................................................959 9.4.1 Einige Konstruktoren der Klasse String .........................................959 9.4.2 Elementfunktionen der Klasse String .............................................960 9.4.3 Konvertierungs- und Formatierungsfunktionen ..............................966 9.4.4 Die Klasse StringBuilder................................................................970 9.4.5 Die Zeichen eines String: Die Klasse System::Char.......................971 9.5 CLI-Arrays..............................................................................................972 9.6 Verweisklassen........................................................................................975 9.6.1 Elemente von Verweisklassen ........................................................976 9.6.2 Objekte, „heap -“ und „stack semantics“ ........................................978 9.6.3 Der Kopierkonstruktor und Zuweisungsoperator............................980 9.6.4 Statische Elemente..........................................................................983 9.6.5 Konstante Datenelemente mit static const, literal und initonly ......986 9.6.6 Statische Konstruktoren..................................................................988 9.6.7 Statische Operatoren.......................................................................989 9.6.8 Gleichheit bei Verweisklassen: Equals und operator==................990 9.6.9 Typkonversionen mit safe_cast, static_cast und dynamic_cast .....993 9.6.10 explicit bei Konstruktoren und Konversionsfunktionen Ԧ..............994 9.6.11 Destruktoren und Finalisierer .........................................................996 9.6.12 Vererbung bei Verweisklassen .....................................................1001 9.6.13 Konstruktoraufrufe in einer Hierarchie von Verweisklassen ........1002 9.6.14 Virtuelle Funktionen, new und override .......................................1004 9.6.15 Abstrakte Klassen und Elementfunktionen ...................................1006 9.6.16 Versiegelte (sealed) Klassen und Elementfunktionen ..................1006 9.7 Wertetypen und Werteklassen...............................................................1007 9.7.1 Fundamentale Typen ....................................................................1007 9.7.2 C++/CLI-Aufzählungstypen .........................................................1009 9.7.3 Werteklassen ................................................................................1010 9.7.4 Vererbung bei Werteklassen.........................................................1014 9.8 Interface-Klassen ..................................................................................1014 9.8.1 Die .NET Interface-Klasse IComparable .....................................1017
Inhalt
xxi
9.8.2 Collection-Typen, IEnumerable und die for each-Anweisung .....1018 9.8.3 IFormattable und die Darstellung selbstdefinierter Datentypen...1020 9.9 C++/CLI Exception-Handling...............................................................1022 9.9.1 Unterschiede zu Standard-C++.....................................................1022 9.9.2 Die Basisklasse Exception ............................................................1023 9.9.3 Vordefinierte C++/CLI und .NET Exceptions..............................1025 9.9.4 Die Freigabe von Ressourcen mit try-finally ................................1026 9.9.5 Nicht behandelte Exceptions in Windows Forms-Anwendungen .1028 9.9.6 Die Protokollierung von Exceptions in einem EventLog..............1029 9.10 C++/CLI-Erweiterungen für native Klassen..........................................1030 9.11 Parameter-Arrays ..................................................................................1031 9.12 Visuelle Programmierung und Properties (Eigenschaften) ...................1032 9.12.1 Lesen und Schreiben von Eigenschaften ......................................1032 9.12.2 Indizierte Properties .....................................................................1035 9.12.3 Properties und Vererbung.............................................................1037 9.13 Delegat-Typen und Events....................................................................1039 9.13.1 Ausgangspunkt: Funktionszeiger ..................................................1040 9.13.2 Delegat-Typen und -Instanzen......................................................1041 9.13.3 Ereignisse (events)........................................................................1046 9.13.4 Selbst definierte Komponenten und ihre Ereignisse .....................1048 9.13.5 Nichttriviale und Statische Ereignisse ..........................................1053 9.14 Ein kleiner Überblick über die .NET Klassenbibliothek.......................1054 9.14.1 Komponenten und die Klasse Component....................................1054 9.14.2 Steuerelemente und die Klasse Control........................................1055 9.14.3 Verschachtelte Controls: Die Klasse ControlCollection ..............1058 9.14.4 Botschaften für ein Steuerelement ................................................1060 9.14.5 Botschaften für eine Anwendung..................................................1061 9.15 Steuerelementbibliotheken: Die Erweiterung der Toolbox ...................1062 9.15.1 Die Erweiterung der Toolbox um selbstdefinierte Komponenten.1063 9.15.2 Klassen für selbstdefinierte Komponenten ...................................1066 9.15.3 Beispiel: Eine selbstdefinierte Tachometer-Komponente.............1067 9.15.4 Attribute für selbstdefinierte Komponenten .................................1069 9.16 Laufzeit-Typinformationen und Reflektion...........................................1071 9.16.1 Laufzeit-Typinformationen der Klasse Type ................................1071 9.16.2 Reflektion mit der Klasse Assembly .............................................1073 9.16.3 Dynamisch erzeugte Datentypen und Plugins...............................1074 9.17 Attribute ................................................................................................1077 9.17.1 Vordefinierte Attribute .................................................................1079 9.17.2 Selbstdefinierte Laufzeitattribute..................................................1080 9.18 Generische Programmierung.................................................................1084 9.18.1 generic und template: Gemeinsamkeiten und Unterschiede .........1084 9.18.2 Typparameter-Einschränkungen (Constraints ) ............................1088 9.19 Dokumentationskommentare und CHM-Hilfedateien...........................1090 9.19.1 Dokumentationskommentare und XML-Dateien..........................1091 9.19.2 Aus XML-Dateien Hilfedateien im CHM-Format erzeugen.........1095 9.20 Managed C++ und C++/CLI Ԧ .............................................................1096
xxii
Inhalt
10 Einige Elemente der .NET-Klassenbibliothek............................. 1099 10.1 Formatierte Texte mit RichTextBox .....................................................1099 10.2 Steuerelemente zur Eingabe und Prüfung von Daten ............................1102 10.2.1 Fehleranzeigen mit ErrorProvider ...............................................1102 10.2.2 Weitere Formulare und selbstdefinierte Dialoge anzeigen ...........1103 10.2.3 Das Validating-Ereignis ...............................................................1107 10.2.4 Texteingaben mit einer MaskedTextBox filtern ...........................1109 10.2.5 Tastatureingaben filtern mit dem KeyPress-Ereignis....................1112 10.2.6 Hilfe-Informationen mit ToolTip und HelpProvider ....................1113 10.2.7 Auf/Ab-Steuerelemente ................................................................1114 10.2.8 Schieberegler: VScrollBar und HScrollBar..................................1116 10.2.9 Lokalisierung................................................................................1118 10.3 Symbolleisten, Status- und Fortschrittsanzeigen...................................1121 10.3.1 Symbolleisten mit Panels und Buttons..........................................1121 10.3.2 Status- und Fortschrittsanzeigen ...................................................1121 10.3.3 Symbolleisten mit ToolStrip.........................................................1123 10.3.4 ToolStripContainer .......................................................................1124 10.3.5 ToolStripLabel und LinkLabel .....................................................1126 10.3.6 NotifyIcon: Elemente im Infobereich der Taskleiste ....................1127 10.4 Größenänderung von Steuerelementen zur Laufzeit .............................1129 10.4.1 Die Eigenschaften Dock und Anchor............................................1129 10.4.2 SplitContainer: Zur Größenanpassung von zwei Panels ...............1129 10.4.3 TableLayoutPanel: Tabellen mit Steuerelementen Ԧ ...................1130 10.4.4 Automatisch angeordnete Steuerelemente: FlowLayoutPanel Ԧ ..1132 10.5 ImageList, ListView und TreeView......................................................1133 10.5.1 Die Verwaltung von Bildern mit einer ImageList.........................1133 10.5.2 Die Anzeige von Listen mit ListView ..........................................1134 10.5.3 ListView nach Spalten sortieren ...................................................1138 10.5.4 Die Anzeige von Baumstrukturen mit TreeView..........................1140 10.6 Die Erweiterung der Toolbox ...............................................................1146 10.7 MDI-Programme...................................................................................1148 10.8 Uhrzeiten, Kalenderdaten und Timer ....................................................1151 10.8.1 Die Klassen DateTime und TimeSpan ..........................................1151 10.8.2 Steuerelemente zur Eingabe von Kalenderdaten und Zeiten.........1154 10.8.3 Timer und zeitgesteuerte Ereignisse .............................................1155 10.8.4 Hochauflösende Zeitmessung mit der Klasse Stopwatch..............1158 10.8.5 Kulturspezifische Datumsformate und Kalender ..........................1159 10.8.6 Kalenderklassen............................................................................1162 10.9 Asynchrone Programmierung und Threads...........................................1163 10.9.1 Die verschiedenen Thread-Zustände ............................................1165 10.9.2 Multithreading mit der Klasse BackgroundWorker ......................1165 10.9.3 Ereignisbasierte asynchrone Programmierung..............................1169 10.9.4 Die Klasse Thread und der Zugriff auf Steuerelemente ...............1170 10.9.5 IAsyncResult-basierte asynchrone Programmierung.....................1175 10.9.6 Sleep und Threads ........................................................................1178 10.9.7 Kritische Abschnitte und die Synchronisation von Threads .........1179
Inhalt
xxiii
10.9.8 ThreadPool ...................................................................................1185 10.10 Grafiken zeichnen mit PictureBox und Graphics................................1187 10.10.1 Grafiken mit einer PictureBox anzeigen.......................................1187 10.10.2 Grafiken auf einer PictureBox und anderen Controls zeichnen ....1187 10.10.3 Welt- und Bildschirmkoordinaten ................................................1189 10.10.4 Figuren..........................................................................................1193 10.10.5 Farben, Stifte und Pinsel...............................................................1193 10.10.6 Text zeichnen ...............................................................................1195 10.10.7 Drucken mit Graphics ..................................................................1196 10.11 Die Steuerung von MS-Office 2003 Anwendungen ............................1203 10.11.1 Word.............................................................................................1203 10.11.2 Excel.............................................................................................1206 10.12 Collection-Klassen ..............................................................................1208 10.12.1 Generische und nicht generische Collection-Klassen ...................1209 10.12.2 Generische Interface-Klassen: ICollection und IList .....1210 10.12.3 Die generische Collection-Klasse List ..................................1212 10.12.4 DataGridView: Die Anzeige von Daten in einer Tabelle .............1214 10.12.5 Die generische Collection-Klasse Queue ..............................1217 10.12.6 Die generische Collection-Klasse HashSet ...........................1217 10.12.7 Die generische Collection-Klasse LinkedList........................1218 10.12.8 Die generische Collection-Klasse Stack................................1219 10.12.9 Dictionaries und die generische Interface-Klasse IDictionary .....1220 10.12.10 Spezielle Collection-Klassen..................................................1225 10.13 Systeminformationen und –operationen ..............................................1226 10.13.1 Die Klasse Environment ...............................................................1226 10.13.2 Die Klasse Process .......................................................................1227 10.13.3 Die Klasse ClipBoard...................................................................1229 10.14 .NET-Klassen zur Dateibearbeitung....................................................1229 10.14.1 Textdateien bearbeiten: StreamReader und StreamWriter............1230 10.14.2 Die Klasse FileStream ..................................................................1234 10.14.3 BinaryReader/Writer und StreamReader/Writer mit FileStreams1236 10.14.4 Der gleichzeitige Zugriff auf eine Datei und Record-Locking .....1237 10.14.5 XML-Dateien: Die Klassen XmlReader und XmlWriter...............1238 10.14.6 Klassen für Laufwerke, Verzeichnisse, Pfade und Dateien ..........1241 10.14.7 Die Klasse FileSystemWatcher.....................................................1248 10.14.8 Komprimieren und Dekomprimieren von Dateien........................1248 10.15 Serialisierung.......................................................................................1250 10.15.1 Serialisierung mit BinaryFormatter..............................................1252 10.15.2 Serialisierung mit SoapFormatter ................................................1255 10.15.3 Serialisierung mit XmlSerializer...................................................1256 10.16 Datenbanken........................................................................................1259 10.16.1 Eine Datenbank anlegen ...............................................................1260 10.16.2 Die Verbindung zu einer Datenbank herstellen ............................1265 10.16.3 SQL-Anweisungen........................................................................1269 10.16.4 Die Klasse DataTable und die Anzeige in einem DataGridView.1273 10.16.5 Die Klasse DataSet.......................................................................1277 10.16.6 Datenbanken als XML-Dateien lesen und schreiben ....................1278
xxiv
Inhalt
10.16.7 Gekoppelte Datenquellen: Master/Detail DataGridViews............1278 10.16.8 Datenquellen in Visual C++ 2005 und in Visual C# 2008 Ԧ........1280 10.17 Datenbindung ......................................................................................1281 10.17.1 Komplexe Datenbindung ..............................................................1281 10.17.2 BindingSource ..............................................................................1283 10.17.3 Einfache Datenbindung ................................................................1286 10.18 Reguläre Ausdrücke ............................................................................1287 10.19 Internet-Komponenten.........................................................................1294 10.19.1 Die WebBrowser-Komponente der Toolbox................................1294 10.19.2 Up- und Downloads mit der Klasse WebClient ............................1295 10.19.3 E-Mails versenden mit SmtpClient ...............................................1297 10.19.4 Netzwerkinformationenen und die Klasse Ping ............................1298 10.19.5 TCP-Clients und Server mit TcpClient und TcpListener ..............1299
Literaturverzeichnis ............................................................................. 1303 Inhalt Buch-CD ....................................................................................1309 Index ......................................................................................................1311 Ԧ Angesichts des Umfangs dieses Buches habe ich einige Abschnitte mit dem Zeichen Ԧ in der Überschrift als „weniger wichtig“ gekennzeichnet. Damit will ich dem Anfänger eine kleine Orientierung durch die Fülle des Stoffes geben. Diese Kennzeichnung bedeutet aber keineswegs, dass dieser Teil unwichtig ist – vielleicht sind gerade diese Inhalte für Sie besonders relevant.
1 Die Entwicklungsumgebung
Visual Studio besteht aus verschiedenen Werkzeugen (Tools), die einen Programmierer bei der Entwicklung von Software unterstützen. Eine solche Zusammenstellung von Werkzeugen zur Softwareentwicklung bezeichnet man auch als Programmier- oder Entwicklungsumgebung. Einfache Entwicklungsumgebungen bestehen nur aus einem Editor und einem Compiler. Für eine effiziente Entwicklung von komplexeren Anwendungen (dazu gehören viele Windows-Anwendungen) sind aber oft weitere Werkzeuge notwendig. Wenn diese wie in Visual Studio in einem einzigen Programm integriert sind, spricht man auch von einer integrierten Entwicklungsumgebung (engl.: „integrated development environment“, IDE). In diesem Kapitel wird zunächst an einfachen Beispielen gezeigt, wie man mit Visual Studio 2008 Windows-Programme mit einer grafischen Benutzeroberfläche entwickeln kann. Anschließend (ab Abschnitt 1.3) werden dann die wichtigsten Werkzeuge von Visual Studio ausführlicher vorgestellt. Für viele einfache Anwendungen (wie z.B. die Übungsaufgaben) reichen die Abschnitte bis 1.7. Die folgenden Abschnitte sind nur für anspruchsvollere oder spezielle Anwendungen notwendig. Sie sind deshalb mit dem Zeichen Ԧ (siehe Seite xxiv) gekennzeichnet und können übergangen werden. Weitere Elemente der Entwicklungsumgebung werden später beschrieben, wenn sie dann auch eingesetzt werden können.
1.1 Visuelle Programmierung: Ein erstes kleines Programm In Visual Studio 2008 findet man unter Datei|Neu|Projekt Vorlagen für verschiedene Arten von Anwendungen. Eine Windows-Anwendung mit einer graphischen Benutzeroberfläche erhält man mit dem Projekttyp CLR (Common Language Runtime) und der Vorlage Windows Forms-Anwendung. Ein solches Projekt legt man dann an, indem man nach Name einen Namen und nach Speicherort ein Verzeichnis für das Projekt eingibt und dann den OK-Button anklickt:
2
1 Die Entwicklungsumgebung
Anschließend werden einige Tools der IDE angezeigt. Die hier rechts eingeblendete Toolbox wird angezeigt, wenn man mit der Maus über den Toolbox-Rider fährt, oder mit Ansicht|Toolbox:
Das Formular (hier Form1) ist der Ausgangspunkt für alle Windows Forms Anwendungen, die mit Visual Studio 2008 entwickelt werden. Es entspricht dem Fenster, das beim Start des Programms angezeigt wird:
1.1 Visuelle Programmierung: Ein erstes kleines Programm
3
Ein Formular kann mit den in der Toolbox verfügbaren Steuerelementen (Controls) gestaltet werden. Die Toolbox zeigt praktisch alle der unter Windows üblichen Steuerelemente an, wenn das Formular angezeigt wird. Sie sind auf verschiedene Gruppen verteilt (z.B. Allgemeine Steuerelemente, Container usw.), die über die Icons + und – auf- und zugeklappt werden können. Ein Teil dieser Komponenten (wie z.B. ein Button) entspricht Steuerelementen, die im laufenden Programm angezeigt werden. Andere, wie ein Timer aus der Gruppe Komponenten, sind im laufenden Programm nicht sichtbar. Falls Ihnen die Namen und die kleinen Icons nicht allzu viel sagen, lassen Sie einfach den Mauszeiger kurz auf einer Komponente stehen: Dann erscheint ein kleines gelbes Hinweisfenster mit einer kurzen Beschreibung. Um eine Komponente aus der Toolbox auf das Formular zu setzen, zieht man sie einfach von der Toolbox auf das Formular. Oder man klickt mit der Maus zuerst auf die Komponente (sie wird dann als markiert dargestellt) und dann auf die Stelle im Formular, an die ihre linke obere Ecke kommen soll. Beispiel: Nachdem man ein Label (Zeile sieben in Allgemeine Steuerelemente, mit dem großen A), eine TextBox (vierte Zeile von unten, Aufschrift ab) und einen Button (zweite Zeile mit der Aufschrift ab) auf das Formular gesetzt hat, sieht es etwa folgendermaßen aus:
4
1 Die Entwicklungsumgebung
Durch diese Spielereien haben Sie schon ein richtiges Windows-Programm erstellt – zwar kein besonders nützliches, aber immerhin. Sie können es folgendermaßen starten: – mit Debuggen|Debuggen starten von der Menüleiste, oder – mit F5 von einem beliebigen Fenster in Visual Studio oder – durch den Aufruf der vom Compiler erzeugten Exe-Datei. Dieses Programm hat schon viele Eigenschaften, die man von einem WindowsProgramm erwartet: Man kann es mit der Maus verschieben, vergrößern, verkleinern und schließen. Bemerkenswert an diesem Programm ist vor allem der im Vergleich zu einem nichtvisuellen Entwicklungssystem geringe Aufwand, mit dem es erstellt wurde. So braucht Petzold in seinem Klassiker „Programmierung unter Windows“ (Petzold 1992, S. 33) ca. 80 Zeilen nichttriviale C-Anweisungen, um den Text „Hello Windows“ wie in einem Label in ein Fenster zu schreiben. Und in jeder dieser 80 Zeilen kann man einiges falsch machen. Vergessen Sie nicht, Ihr Programm zu beenden, bevor Sie es weiterbearbeiten. Sie können den Compiler nicht erneut starten, solange das Programm noch läuft. Diese Art der Programmierung bezeichnet man als visuelle Programmierung. Während man bei der konventionellen Programmierung ein Programm ausschließlich durch das Schreiben von Anweisungen (Text) in einer Programmiersprache entwickelt, wird es bei der visuellen Programmierung ganz oder teilweise aus vorgefertigten grafischen Komponenten zusammengesetzt. Mit Visual Studio kann die Benutzeroberfläche eines Programms visuell gestaltet werden. Damit sieht man bereits beim Entwurf des Programms, wie es später zur Laufzeit aussehen wird. Die Anweisungen, die als Reaktionen auf Benutzereingaben (Mausklicks usw.) erfolgen sollen, werden dagegen konventionell in einer Programmiersprache (z.B. C++) geschrieben. Wenn die Toolbox oft verwendet wird, ist es am einfachsten, sie in der Entwicklungsumgebung zu fixieren, indem man in ihrem Kontextmenü (rechte Maustaste) „Automatisch im Hintergrund“ deaktiviert.
Die zuletzt auf einem Formular (bzw. im Pull-down-Menü des Eigenschaftenfensters) angeklickte Komponente wird als die aktuell ausgewählte Komponente
1.1 Visuelle Programmierung: Ein erstes kleines Programm
5
bezeichnet. Man erkennt sie an den kleinen Quadraten an ihrem Rand, den sogenannten Ziehquadraten. An ihnen kann man mit der Maus ziehen und so die Größe der Komponente verändern. Ein Formular wird dadurch zur aktuell ausgewählten Komponente, dass man mit der Maus eine freie Stelle im Formular anklickt. Beispiel: Im letzten Beispiel ist button1 die aktuell ausgewählte Komponente. Im Eigenschaftenfenster (Kontextmenü der Komponente auf dem Formular, oder Ansicht|Weitere Fenster|Eigenschaftenfenster, nicht mit Ansicht|Eigenschaften-Manager verwechseln) werden die Eigenschaften (properties) der aktuell ausgewählten Komponente angezeigt. In der linken Spalte stehen die Namen und in der rechten die Werte der Eigenschaften. Mit der Taste F1 erhält man eine Beschreibung der Eigenschaft. Den Wert einer Eigenschaft kann man über die rechte Spalte verändern. Bei manchen Eigenschaften kann man den neuen Wert über die Tastatur eintippen. Bei anderen wird nach dem Anklicken der rechten Spalte ein kleines Dreieck für ein Pull-down-Menü angezeigt, über das ein Wert ausgewählt werden kann. Oder es wird ein Symbol mit drei Punkten „…“ angezeigt, über das man Werte eingeben kann. Beispiel: Bei der Eigenschaft Text kann man mit der Tastatur einen Text eingeben. Bei einem Button ist dieser Text die Aufschrift auf dem Button (z.B. „OK“), und bei einem Formular die Titelzeile (z.B. „Mein erstes C++-Programm“). Bei der Eigenschaft BackColor (z.B. bei einem Button) kann man über ein Pull-down-Menü die Hintergrundfarbe auswählen. Klickt man die rechte Spalte der Eigenschaft Font und danach das Symbol „…“ an, kann man die Schriftart der Eigenschaft Text auswählen. Eine Komponente auf dem Formular wird nicht nur an ihre Eigenschaften im Eigenschaftenfenster angepasst, sondern auch umgekehrt: Wenn man die Größe einer Komponente durch Ziehen an den Ziehquadraten verändert, werden die Werte der entsprechenden Eigenschaften (Location und Size im Abschnitt Layout) im Eigenschaftenfenster automatisch aktualisiert.
6
1 Die Entwicklungsumgebung
1.2 Erste Schritte in C++ Als nächstes soll das Programm aus dem letzten Abschnitt so erweitert werden, dass als Reaktion auf Benutzereingaben (z.B. beim Anklicken eines Buttons) Anweisungen ausgeführt werden. Windows-Programme können Benutzereingaben in Form von Mausklicks oder Tastatureingaben entgegennehmen. Im Unterschied zu einfachen Konsolen-Programmen (z.B. DOS-Programmen) muss man in einem Windows-Programm aber keine speziellen Funktionen (wie z.B. scanf in C) aufrufen, die auf solche Eingaben warten. Stattdessen werden alle Eingaben von Windows zentral entgegengenommen und als sogenannte Botschaften (Messages) an das entsprechende Programm weitergegeben. Dadurch wird in diesem Programm ein sogenanntes Ereignis ausgelöst. Die Ereignisse, die für die aktuell ausgewählte Komponente eintreten können, zeigt das Eigenschaftenfenster an, wenn man das Icon für die Events (Ereignisse) anklickt. Die Abbildung rechts zeigt einige Ereignisse für einen Button. Dabei steht Click für das Ereignis, das beim Anklicken des Buttons eintritt. Offensichtlich kann ein Button nicht nur auf das Anklicken reagieren, sondern auch noch auf zahlreiche andere Ereignisse.
Einem solchen Ereignis kann eine Funktion zugeordnet werden, die dann aufgerufen wird, wenn das Ereignis eintritt. Diese Funktion wird auch als Ereignisbehandlungsroutine (engl. event handler) bezeichnet. Sie wird von Visual Studio durch einen Doppelklick auf die Zeile des Ereignisses erzeugt und im Quelltexteditor angezeigt. Der Cursor steht dann am Anfang der Funktion. Vorläufig soll unser Programm allerdings nur auf das Anklicken eines Buttons reagieren. Die bei diesem Ereignis aufgerufene Funktion erhält man am einfachsten durch einen Doppelklick auf den Button im Formular. Dadurch erzeugt Visual Studio die folgende Funktion:
1.2 Erste Schritte in C++
7
Zwischen die geschweiften Klammern „{“ und „}“ schreibt man dann die Anweisungen, die ausgeführt werden sollen, wenn das Ereignis Click eintritt. Welche Anweisungen hier möglich sind und wie diese aufgebaut werden müssen, ist der Hauptgegenstand dieses Buches und wird ab Kapitel 3 ausführlich beschrieben. Im Rahmen dieses einführenden Kapitels sollen nur einige wenige Anweisungen vorgestellt werden und diese auch nur so weit, wie das zum Grundverständnis von Visual Studio notwendig ist. Falls Ihnen Begriffe wie „Variablen“ usw. neu sind, lesen Sie trotzdem weiter – aus dem Zusammenhang erhalten Sie sicherlich eine intuitive Vorstellung, die zunächst ausreicht. Später werden diese Begriffe dann genauer erklärt. Eine beim Programmieren häufig verwendete Anweisung ist die Zuweisung (mit dem Operator „=“), mit der man einer Variablen einen Wert zuweisen kann. Als Variablen sollen zunächst nur solche Eigenschaften von Komponenten verwendet werden, die auch im Eigenschaftenfenster angezeigt werden. Diesen Variablen können dann die Werte zugewiesen werden, die auch im Eigenschaftenfenster in der rechten Spalte der Eigenschaften vorgesehen sind. In der Abbildung rechts sieht man einige zulässige Werte für die Eigenschaft BackColor. Sie werden nach dem Aufklappen des Pull-down-Menüs angezeigt.
Schreibt man jetzt die Anweisung textBox1->BackColor=Color::Yellow;
zwischen die geschweiften Klammern private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ textBox1->BackColor=Color::Yellow; }
e) {
erhält die Eigenschaft BackColor von textBox1 beim Anklicken von button1 während der Ausführung des Programms den Wert Color::Yellow, der für die Farbe Gelb steht. Wenn Sie das Programm jetzt mit F5 starten und dann button1 anklicken, erhält die TextBox tatsächlich die Hintergrundfarbe Gelb.
8
1 Die Entwicklungsumgebung
Auch wenn dieses Programm noch nicht viel sinnvoller ist als das erste, haben Sie doch gesehen, wie mit Visual Studio Anwendungen für Windows entwickelt werden. Dieser Entwicklungsprozess besteht immer aus den folgenden Aktivitäten: 1. Man gestaltet die Benutzeroberfläche, indem man Komponenten aus der Toolbox auf das Formular setzt (drag and drop) und ihre Eigenschaften im Eigenschaftenfenster oder das Layout mit der Maus anpasst (visuelle Programmierung). 2. Man schreibt in C++ die Anweisungen, die als Reaktion auf Benutzereingaben erfolgen sollen (nichtvisuelle Programmierung). 3. Man startet das Programm und testet, ob es sich auch wirklich so verhält, wie es sich verhalten soll. Der Zeitraum der Programmentwicklung (Aktivitäten 1. und 2.) wird auch als Entwurfszeit bezeichnet. Im Unterschied dazu bezeichnet man die Zeit, während der ein Programm läuft, als Laufzeit eines Programms.
1.3 Der Quelltexteditor Der Quelltexteditor (kurz: Editor) ist das Werkzeug, mit dem die Quelltexte geschrieben werden. Er ist in die Entwicklungsumgebung integriert und kann auf verschiedene Arten aufgerufen werden, wie z.B. . – durch Anklicken seines Registers – über Ansicht|Code oder Strg+Alt+0 von einem Formular aus. – durch einen Doppelklick auf die rechte Spalte eines Ereignisses im Eigenschaftenfenster. Der Cursor befindet sich dann in der Routine, die zu dem angeklickten Ereignis gehört. – durch einen Doppelklick auf eine Komponente in einem Formular. Der Cursor befindet sich dann in einer Funktion, die zu einem bestimmten Ereignis für diese Komponente gehört. Da die letzten beiden Arten den Cursor in eine bestimmte Ereignisbehandlungsroutine platzieren, bieten sie eine einfache Möglichkeit, diese Funktion zu finden, ohne sie im Editor suchen zu müssen. Der Editor enthält über Tastenkombinationen zahlreiche Funktionen, mit denen sich nahezu alle Aufgaben effektiv durchführen lassen, die beim Schreiben von Programmen auftreten. In der ersten der nächsten beiden Tabellen sind einige der Funktionen zusammengestellt, die man auch in vielen anderen Editoren findet.
1.3 Der Quelltexteditor
Tastenkürzel Strg+F F3 Strg+H Strg+S Strg+Umschalt+S Strg+P Strg+Entf Strg+Rücktaste Alt+Rücktaste oder Strg+Z Alt+Umschalt+ Rücktaste oder Strg+Umschalt+Z Pos1 bzw. Ende Strg+Pos1 bzw. Strg+Ende Strg+← bzw. Strg+→ Strg+Bild↑ bzw. Strg+Bild↓ Strg+↑ bzw. Strg+↓ Einfg
9
Aktion oder Befehl. wie Bearbeiten|Suchen und Ersetzen|Schnellsuche wie Strg+F|Weitersuchen wie Bearbeiten|Suchen und Ersetzen|Schnellersetzug wie Datei|Form1.h speichern wie Datei|Alle speichern wie Datei|Drucken löscht das Wort ab der Cursorposition löscht das Wort links vom Cursor wie Bearbeiten|Rückgängig. Damit können EditorAktionen rückgängig gemacht werden wie Bearbeiten|Wiederholen
an den Anfang bzw. das Ende der Zeile springen an den Anfang bzw. das Ende der Datei springen
um ein Wort nach links bzw. rechts springen
an den Anfang bzw. das Ende der Seite springen
Text um eine Zeile nach oben bzw. unten verschieben; die Position des Cursors im Text bleibt gleich schaltet zwischen Einfügen und Überschreiben um
Dazu kommen noch die üblichen Tastenkombinationen unter Windows, wie Markieren eines Textteils mit gedrückter Umschalt-Taste und gleichzeitigem Bewegen des Cursors bzw. der Maus bei gedrückter linker Maustaste. Ein markierter Bereich kann mit Strg+X ausgeschnitten, mit Strg+C in die Zwischenablage kopiert und mit Entf gelöscht werden. Strg+V fügt den Inhalt der Zwischenablage ein. Die nächste Tabelle enthält Funktionen, die vor allem beim Programmieren nützlich sind, und die man in einer allgemeinen Textverarbeitung nur selten findet. Die meisten dieser Optionen werden auch unter Bearbeiten|Erweitert sowie auf der Text Editor Symbolleiste (unter Ansicht|Symbolleisten) angezeigt:
Tastenkürzel
F5 bzw. F7 Umschalt+F5
Aktion oder Befehl kompilieren und starten, wie Debuggen|Debuggen Starten kompilieren, aber nicht starten Laufendes Programm beenden, wie Debuggen|Debuggen beenden. Damit können oft auch Programnicht beendet me beendet werden, die mit
10
1 Die Entwicklungsumgebung
Aktion oder Befehl werden können. Versuchen Sie immer zuerst diese Option wenn Sie meinen, Sie müssten Visual Studio mit dem Windows Task Manager beenden. F1 kontextsensitive Hilfe (siehe Abschnitt 1.7) Rechte Maustaste öffnet in einer Zeile mit einer #include-Anweisung Dokument öffnen die angegebene Datei rückt den als Block markierten Text eine Spalte TAB bzw. nach links bzw. rechts (z.B. zum Aus- und EinUmschalt+TAB bzw. rücken von {}-Blöcken). Bei der Benutzung der TAB-Tasten muss die Zeile ab der ersten Spalte markiert sein. Strg +´ setzt den Cursor vor die zugehörige Klammer, wenn (´ ist das Zeichen er vor einer Klammer (z.B. (), {}, [ ] oder ) steht links von der Rücktaste) Strg+K+C oder einen markierten Block auskommentieren bzw. die Strg+K+U bzw. Auskommentierung entfernen
Tastenkürzel
oder Strg+M+M bzw. unter Bearbeiten|Gliedern Alt|Enter Umschalt+F7 Alt+Maus bewegen bzw. Alt+Umschalt+ Pfeiltaste (←, →, ↑ oder ↓)
Strg+F2
F2 Strg+Umschalt+R Strg+Umschalt+P
ganze Funktionen, Klassen usw. auf- oder zuklappen wie Ansicht|Eigenschaftenfenster wie Ansicht|Designer zum Markieren von Spalten , z.B.
setzt oder löscht ein Lesezeichen (siehe auch Bearbeiten|Lesezeichen) springt zum nächsten Lesezeichen zeichnet ein Tastaturmakro auf spielt ein Tastaturmakro ab
Eine ausführliche Beschreibung der Tastaturbelegung findet man unter Hilfe|Index „Tastenkombinationen“, insbesondere unter dem Untereintrag „Vordefiniert“. Die folgenden Programmierhilfen beruhen auf einer Analyse des aktuellen Programms. Sie werden zusammen mit einigen weiteren (siehe Bearbeiten|IntelliSense) unter dem Oberbegriff IntelliSense zusammengefasst: – Elemente anzeigen und auswählen: Nachdem man den Namen einer Komponente (genauer: eines Klassenobjekts oder eines Zeigers auf ein Klassenobjekt, einen Namensbereich usw.) und einen zugehörigen Operator
1.3 Der Quelltexteditor
11
(z.B. „.“, „->“, „::“ usw.) eingetippt hat, wird eine Liste mit allen Elementen der Klasse oder des Namensbereichs angezeigt. Aus dieser Liste kann man mit der Enter-Taste ein Element auswählen. – Parameter Info: Zeigt nach dem Eintippen eines Funktionsnamens und einer öffnenden Klammer die Parameter der Funktion an – Quick Info: Wenn man mit der Maus über einen Namen für ein zuvor deklariertes Symbol fährt (bzw. mit Strg+K+I), wird die Deklaration angezeigt. Bei einer vordefinierten Funktion wird außerdem eine Beschreibung angezeigt. Beispiel: Wenn das Formular eine TextBox textBox1 enthält, wird nach dem Eintippen von „textBox1->“ eine Liste mit allen Elementen von textBox1 angezeigt:
Tippt man weitere Buchstaben ein, werden nur die Elemente mit diesen Anfangsbuchstaben angezeigt. Falls Sie einen langsamen Rechner und große Programme haben, kann Visual Studio dadurch etwas langsam werden. Dann kann man sie unter Extras|Optionen|Text-Editor|C/C++|Allgemein|Anweisungsvervollständigung abschalten. Unter Extras|Optionen (sowohl unter Umgebung|Schriftarten und Farben|Einstellungen anzeigen für: Text-Editor als auch unter Text-Editor) kann man zahlreiche Einstellungen vornehmen und den Editor individuell anpassen:
12
1 Die Entwicklungsumgebung
Unter Extras|Optionen|Umgebung|Tastatur kann man die Tastenkombinationen für alle Editor-Befehle anpassen. Falls Sie sich z.B. nicht merken können, dass Strg+´ (links von der Backspace-Taste) das Tastenkürzel ist, mit dem man zur passenden Klammer springen kann, können Sie dafür (der Editor-Befehl Bearbeiten.GehezuKlammer) ein intuitiveres Tastenkürzel wählen wie z.B. Strg+“ aus der Liste der Elemente „AppendText“ auswählen kann. Wenn der Cursor dann hinter der Klammer „(“ steht, sollte der Parametertyp String angezeigt werden. Sie brauchen im Moment noch nicht zu verstehen, was das alles bedeutet. Sie benötigen für diese Aufgabe ein Formular mit einer TextBox textBox1 und müssen das alles in einer Funktion wie button1_Click eingeben. e) mit Strg+K+C einen Block auskommentieren und dies Strg+K+U wieder rückgängig machen kann. f) eine Datei „c:\test.txt“ mit der rechten Maustaste öffnen kann, wenn sich der Cursor über der Zeile „#include "c:\test.txt"“ befindet. Dazu müssen Sie vorher (z.B. mit notepad) eine Datei mit diesem Namen anlegen.
1.4 Kontextmenüs und Symbolleisten (Toolbars)
13
1.4 Kontextmenüs und Symbolleisten (Toolbars) Einige der häufiger gebrauchten Menüoptionen stehen auch über Kontextmenüs und Symbolleisten zur Verfügung. Damit kann man diese Optionen etwas schneller auswählen als über ein Menü. Eine Symbolleiste (Toolbar) ist eine Leiste mit grafischen Symbolen (Icons), die unterhalb der Menüleiste angezeigt wird. Diese Symbole stehen für Programmoptionen, die auch über die Menüleiste verfügbar sind. Durch das Anklicken eines Symbols kann man sie mit einem einzigen Mausklick auswählen. Das ist etwas schneller als die Auswahl über ein Menü, die mindestes zwei Mausklicks erfordert. Symbolleisten können außerdem zur Übersichtlichkeit beitragen, da sie Optionen zusammenfassen, die inhaltlich zusammengehören. Visual Studio enthält einige vordefinierte Symbolleisten. Mit Ansicht|Symbolleisten kann man diejenigen auswählen, die man gerade braucht, sowie eigene Symbolleisten konfigurieren. Einige Optionen der Text-Editor Symbolleiste wurden schon im Zusammenhang mit dem Editor vorgestellt. Falls Ihnen die relativ kleinen Symbole nicht viel sagen, lassen Sie den Mauszeiger kurz auf einer Schaltfläche stehen. Dann wird die entsprechende Option in einem kleinen Fenster beschrieben. Eigene Symbolleisten können mit Ansicht|Symbolleisten|Anpassen auf der Seite Symbolleisten mit Neu angelegt werden. Eine neue Symbolleiste sieht zunächst etwas unscheinbar aus (Abbildung rechts). In eine eigene oder vordefinierte Symbolleiste kann man dann von der Seite Ansicht|Symbolleisten|Anpassen|Symbolleisten durch Ziehen mit der Maus Befehle einfügen:
14
1 Die Entwicklungsumgebung
Über die rechte Maustaste erhält man in den meisten Fenstern von Visual Studio ein sogenanntes Kontextmenü (auch die Bezeichnung „lokales Menü“ ist verbreitet), das eine Reihe gebräuchlicher Optionen für dieses Fenster anbietet. Beispiele: In einem Formular erhält man das Kontextmenü links oben, mit dessen erster Option man in den Editor kommt. Im Kontextmenü des Quelltexteditors (Abbildung rechts) kann man mit der ersten Option ins Formular wechseln. Das Kontextmenü in der Titelzeile der meisten Fenster bietet Möglichkeiten zur Gestaltung der Entwicklungsumgebung.
1.5 Projekte, Projektdateien und Projektoptionen Visual Studio erzeugt eine ausführbare Anwendung aus verschiedenen Dateien, die zusammenfassend als Projekt bezeichnet werden. Einige dieser Dateien werden nach Datei|Neu|Projekt erzeugt. Andere werden durch Aktionen wie Debuggen|Debuggen Starten oder durch Erstellen Aktionen erzeugt. Sie befinden sich in dem als Projektmappenname angegebenen Unterverzeichnis von Speicherort, sowie in Unterverzeichnissen davon, die dem Namen des Projekts entsprechen. In der Voreinstellung ist Projektmappenname der Name des Projekts. Zu einem Projekt mit dem Namen MeinProjekt, das in einer Projektmappe mit dem Namen MeineSolution enthalten ist, gehören unter anderem die folgenden Dateien im Verzeichnis MeineSolution\MeinProjekt: MeinProjekt.vcproj enthält Informationen über die verwendete Version von Visual Studio, die gewählte Plattform, die gewählten Projektoptionen usw. Form1.h enthält die Formularklasse, die das Formular darstellt, einschließlich der Funktionen.
1.5 Projekte, Projektdateien und Projektoptionen
15
MeinProjekt.cpp enthält die main-Funktion der Anwendung. Diese Funktion wird beim Start der Anwendung aufgerufen // MeinProjekt.cpp: Hauptprojektdatei. #include "stdafx.h" #include "Form1.h" using namespace MeinProjekt; [STAThreadAttribute] int main(array ^args) { // Aktivieren visueller Effekte von Windows XP, bevor // Steuerelemente erstellt werden Application::EnableVisualStyles(); // Hauptfenster erstellen und ausführen Application::Run(gcnew Form1()); return 0; }
Sie besteht im Wesentlichen aus den beiden Funktionsaufrufen Application::EnableVisualStyles und Application::Run, die in der jeweiligen Zeile davor nach // beschrieben werden. Application::Run bewirkt, dass das Formular angezeigt wird und die Anwendung auf die Ereignisse reagieren kann, für die Ereignisbehandlungsroutinen definiert sind wie z.B. button1_Click. Sie läuft dann so lange, bis sie durch ein Ereignis wie z.B. durch Anklicken des Schließen Buttons oder durch die Tastenkombination Alt-F4 beendet wird. Normalerweise hat ein Programmierer, der Windows Forms-Anwendungen mit Visual Studio entwickelt, allerdings nichts mit der Datei zu tun, die die Funktion main enthält. Diese Datei wird automatisch von Visual Studio erzeugt und sollte normalerweise auch nicht verändert werden. Stattdessen betätigt man sich bei der Entwicklung von solchen Programmen meist in einer Datei wie Form1.h. StdAfx.h, StdAfx.cpp: Diese Dateien werden zur Erzeugung einer vorkompilierter Header-Datei (PCH) verwendet, die der Beschleunigung der Kompilierung dient. Alle Anweisungen bis zu einer #include "stdafx.h"-Anweisung werden vorkompiliert (d.h. nur einmal kompiliert). Fügt man später vor dieser #include-Anweisung noch Anweisungen ein, werden diese nicht kompiliert. In der Symbolleiste Standard kann man auswählen, ob eine Anwendung als Debug- oder Release-Version erzeugt werden soll. Die Debug-Version ist nicht optimiert und enthält Debug-Informationen, mit denen man sie im Debugger (siehe Abschnitt 3.5.5) ausführen kann. Das ist normalerweise die richtige Wahl während man eine Anwendung entwickelt, oder wenn man programmiert, um das Programmieren zu lernen. Die Release-Version ist dagegen optimiert, enthält aber keine Debug-Informationen. Sie ist die richtige Wahl, wenn die
16
1 Die Entwicklungsumgebung
Entwicklung einer Anwendung abgeschlossen ist und sie in Betrieb genommen werden soll. Die ausführbaren Dateien befinden sich in den Unterverzeichnissen „Debug“ oder „Release“ des Projektmappenverzeichnisses. Unter Projekt|Eigenschaften|Konfiguration kann man die Einstellungen für diese und weitere Konfigurationen (die mit dem Konfigurations-Manager angelegt werden können) setzen. In Abhängigkeit von der Debug oder Release Konfiguration erzeugt der Compiler in den Unterverzeichnissen Debug oder Release des Projekts Objektdateien mit der Endung „.obj“. Aus diesen Objektdateien erzeugt der Linker dann in diesen Unterverzeichnissen das ausführbare Programm mit der Endung „.exe“. Der Linker ist wie der Compiler ein in die Entwicklungsumgebung integriertes Programm, das automatisch beim Erzeugen eines Programms (z.B. mit F5) ausgeführt wird. Bei vielen Visual Studio Projekten braucht man allerdings nicht einmal zu wissen, dass der Linker überhaupt existiert. Normalerweise ist es kein Fehler, wenn man sich vorstellt, dass das ausführbare Programm allein vom Compiler erzeugt wird. Die Dateien eines Projekts werden von Visual Studio vor dem Erstellen (z.B. mit F5) automatisch gespeichert, wenn unter Extras|Optionen|Projekte und Projektmappen|Erstellen und Ausführen|Vor Erstellen die Einstellung „Alle Änderungen speichern“ gewählt wurde (Voreinstellung). Man kann sie außerdem mit Datei|Alle speichern speichern. Diese Dateien belegen auch bei kleinen Projekten mehr als 10 MB. Ein Teil dieser Dateien wird bei jeder Kompilation neu erzeugt und kann mit Erstellen|Projektmappe bereinigen gelöscht werden. Aber auch danach belegen die verbleibenden Dateien mehrere MB. Außerdem kann man die Datei Projektname.ncb manuell löschen.
1.6 Einige Tipps zur Arbeit mit Projekten Die Arbeit mit Visual Studio ist meist einfach, wenn man alles richtig macht. Es gibt allerdings einige typische Fehler, über die Anfänger immer wieder stolpern. Die folgenden Tipps sollen helfen, diese Fehler zu vermeiden. 1) Solange man noch nicht weiß, welche Ereignisse es gibt und wann diese eintreten (siehe z.B. Abschnitt 2.6), sollte man nur Ereignisbehandlungsroutinen verwenden, die wie private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { }
1.6 Einige Tipps zur Arbeit mit Projekten
17
durch einen Doppelklick auf einen Button erzeugt wurden. Diese Funktion wird beim Anklicken des Buttons aufgerufen. Durch einen Doppelklick auf eine Textbox erhält man dagegen eine Funktion private: System::Void textBox1_TextChanged( System::Object^ sender, System::EventArgs^ }
e) {
die zur Laufzeit des Programms bei jeder Änderung des Textes in der TextBox aufgerufen. Das ist aber meist nicht beabsichtigt. 2) Um einen von Visual Studio erzeugten Event Handler wieder zu entfernen, reicht es nicht aus, diese Funktion im Editor zu löschen. Er muss außerdem im Eigenschaftenfenster gelöscht werden, indem man den Namen der Funktion in der rechten Spalte löscht. Dadurch entfernt Visual Studio einige interne Verweise auf diese Funktion, die beim nächsten Kompilieren zu einem Fehler führen würden. Falls z.B. versehentlich wie unter 1. durch einen Doppelklick auf eine TextBox der Event Handler private: System::Void textBox1_TextChanged( System::Object^ sender, System::EventArgs^ }
e) {
erzeugt wurde und (nachdem man festgestellt hat, dass er nicht den beabsichtigten Effekt hat) dieser dann manuell aus dem Quelltext gelöscht wird, erhält man eine Fehlermeldung wie error: 'textBox1_TextChanged':Ist kein Element von Form1
Nach dem Löschen dieser Funktion im Eigenschaftenfenster unterbleiben auch die Fehlermeldungen. Der Event Handler kann außerdem dadurch entfernt werden, dass man die Zeile mit der Fehlermeldung in der Funktion InitializeComponent auskommentiert: //this->textBox1->TextChanged+=gcnew System::EventHandler
Diese Funktion soll zwar im Editor nicht verändert werden. Es funktioniert aber und ist etwas einfacher als das Löschen im Eigenschaftenfenster. 3) Achten Sie darauf, dass in der Quelltextdatei zu einem Formular („Form1.h“ in der Voreinstellung) nicht versehentlich eine der letzten beiden schließenden geschweiften Klammern gelöscht wird. Falls nach der schließenden Klammer
18
1 Die Entwicklungsumgebung
zur letzten Funktion nicht noch zwei solche Klammern kommen, hat das Fehlermeldungen zur Folge, die für einen Anfänger meist schwer verständlich sind. 4) Wenn man an einem Projekt weiterarbeiten will, das man zu einem früheren Zeitpunkt begonnen hat, kann man im Windows Explorer die Projektdatei mit der Namensendung *.sln oder *.vcproj öffnen. Da das Projektverzeichnis aber auch noch viele andere Dateien enthält, besteht die Gefahr, dass man eine andere Datei anklickt und dann nicht das Projekt, sondern nur die Datei geöffnet wird. Diese Gefahr besteht mit Datei|zuletzt geöffnete Projekte oder Datei|Öffnen|Projekt nicht. Hier werden nur Projekte angeboten. 5) Es wurde gelegentlich beobachtet, dass der Formular-Designer aus dem Tritt kommt und anstelle des Formulars eine Meldung wie
anzeigt. Dann sollte man zunächst alle Fehler in „Form1.h“ beseitigen, bis das Projekt fehlerfrei übersetzt werden kann. Nach dem Schließen und erneuten Öffnen des Formulars oder einem Neustart von Visual Studio wird dann meist auch wieder das Formular angezeigt. Falls in die Formulardatei „Form1.h“ eine Projektvorlage wie in Abschnitt 2.12 aufgenommen wird, reicht es oft auch aus, die entsprechende #includeAnweisung auszukommentieren // #include "Aufgabe_2_11.h"
und dann das Formular zu schließen und wieder zu öffnen, oder Visual Studio zu beenden und wieder neu zu starten. Danach kann man die Kommentarzeichen „//“ vor der #include-Anweisung wieder entfernen. 6) Wenn ein Projekt auf einem Netzlaufwerk angelegt wird, weigert sich Visual Studio, dieses auszuführen:
1.7 Die Online-Hilfe (MSDN Dokumentation)
19
Dieser Fehler lässt sich dadurch beheben, dass man unter Projekt|Eigenschaften|Konfigurationseigenschaften|Allgemein den Eintrag „$(SolutionDir)$(ConfigurationName)“ durch ein Verzeichnis auf einer lokalen Festplatte ersetzt (z.B. „c:\VsExe“). Die nächsten beiden Fehler wurden nur selten beobachtet. Sie können aber sehr ärgerlich sein, da sie keinen Hinweis darauf geben, wie man sie beheben kann. Dann sucht man ewig im eigenen Programmtext und findet nichts. 7) Das nach dem Start (z.B. mit F5) ausgeführte Programm entspricht nicht dem aktuellen Entwicklungsstand im Editor oder Formulardesigner. Dann hilft Erstellen|Projekt neu erstellen. 8) Der Fehler nach der Meldung
war immer mit einem Neustart von Visual Studio behoben.
1.7 Die Online-Hilfe (MSDN Dokumentation) Da sich kaum jemand die vielen Einzelheiten von Visual Studio, C++, .NET usw. merken kann, ist es für eine effektive Arbeit unerlässlich, die zugehörige Dokumentation nutzen zu können. Diese Dokumentation wird auch als Online-Hilfe oder MSDN Dokumentation bezeichnet und in Abhängigkeit von den Einstellungen unter Extras|Optionen|Umgebung|Hilfe in einem Fenster von Visual Studio (Integrierter Hilfeviewer) oder im Microsoft Document Explorer (Externer Hilfeviewer) angezeigt. Über die Online Einstellungen kann man festlegen, ob Quellen aus dem Internet und/oder lokale Information angezeigt werden. Die Quellen aus dem Internet erhält man
20
1 Die Entwicklungsumgebung
auch mit einem Webbrowser über http://msdn.microsoft.com/de-de/default.aspx (deutsch) oder http://msdn.microsoft.com/en-us/library/default.aspx (englisch) Am einfachsten ist oft die kontextbezogene Hilfe mit F1: In den meisten Fenstern der Entwicklungsumgebung erhält man mit F1 Informationen, wie z.B. – im Editor zum Wort unter dem Cursor, – im Eigenschaftenfenster zur angewählten Eigenschaft, – auf einem Formular oder in der Toolbox zum angeklickten Steuerelement, usw. Falls man das Wort kennt, zu dem man weitere Informationen sucht, kann man mit Hilfe|Index die Online-Hilfe dazu aufrufen. Oft kennt man aber den entsprechenden Indexeintrag nicht. Für diesen Fall bietet das Hilfe Menü zahlreiche Optionen an, die auch über Ansicht|Symbolleisten|Hilfe verfügbar sind:
Über Hilfe|Inhalt kann man in thematisch geordneten Büchern, Anleitungen, Referenzen usw. suchen. Die Online-Hilfe zu den C- und C++-Standardbibliotheken, auf die später öfter verwiesen wird, findet man unter Entwicklungstools und Sprachen|...|Visual C++|Referenz:
Hier erhält man auch ausführliche Informationen zur Entwicklungsumgebung:
1.7 Die Online-Hilfe (MSDN Dokumentation)
21
Unter Inhalt|.NET-Entwicklung|Dokumentation zu .NET Framework|Verweis auf die Klassenbibliothek findet man die Dokumentation zu den .NET Klassen. Diese Informationen werden in Abschnitt 2.1 noch ausführlicher beschrieben. In vielen Fenstern kann man einen Filter auswählen, um die Anzahl der unerwünschten Treffer zu reduzieren:
Hilfe|Dynamische Hilfe zeigt ein Fenster mit Verweisen auf Themen, die in einem Zusammenhang mit den aktuellen Aktivitäten stehen. Das ist manchmal hilfreich, manchmal aber auch störend und ablenkend, wenn sich der Inhalt dieses Fensters mit jeder Bewegung des Cursors im Editor verändert. Hilfe|Suchen (Volltextsuche) zeigt Seiten an, die den Suchbegriff enthalten. Unter Extras|Optionen|Umgebung|Hilfe|Online können Internet-Server ausgewählt werden, die durchsucht werden.
22
1 Die Entwicklungsumgebung
Hilfe|Gewusst wie stellt themenbezogene Artikel zur Verfügung. aus der Symbolleiste des Document Explorers kann man das Mit dem Symbol Inhaltsverzeichnis mit der angezeigten Seite synchronisieren. Mit „Zu Favoriten hinzufügen“ im Kontextmenü einer Hilfeseite kann man Lesezeichen setzen. Die Funktionen der Win32-API sind unter .NET normalerweise nicht notwendig (siehe Abschnitt 3.24.3). Falls man sie aber doch einmal benötigt, findet man sie mit etwas Glück mit dem Suchbegriff „Windows API Reference“ unter MSDN oder über „Windows API“ im Index der lokalen Hilfe. Aufgabe 1.7 Mit diesen Übungen sollen Sie lediglich die Möglichkeiten der Online-Hilfe kennen lernen. Sie brauchen die angezeigten Informationen nicht zu verstehen. a) Rufen Sie mit F1 die Online-Hilfe auf – für das Wort „int“ im Editor, – für eine TextBox auf einem Formular, und – im Eigenschaftenfenster für die Eigenschaft Text einer TextBox. b) Suchen Sie in Inhalt unter Entwicklungstools und Sprachen|Dokumentation zu Visual Studio|Visual C++|Referenz|C/C++ Languages|C++ Language Reference|Basic Concepts|Types nach der Übersicht über „Fundamental Types“.
1.8 Projektmappen und der Projektmappen-Explorer Ԧ
23
1.8 Projektmappen und der Projektmappen-Explorer Ԧ Eine Projektmappe (engl. „Solution“) fasst ein oder mehrere Projekte zusammen und ermöglicht, diese gemeinsam zu bearbeiten, zu verwalten und zu konfigurieren. Falls man ein neues Projekt mit Datei|Neu|Projekt anlegt, ist der Projektmappenname in der Voreinstellung derselbe Name wie der des Projekts. Man kann aber auch einen anderen Namen für die Projektmappe wählen oder ein Projekt zu einer Projektmappe hinzufügen, indem man nach Projektmappe „Hinzufügen“ auswählt. Nachdem man eine Projektmappe angelegt hat, kann man außerdem mit dem Projektmappen-Explorer (Ansicht|Projektmappen-Explorer) nach einem Klick auf die rechte Maustaste ein vorhandenes oder ein neues Projekt hinzufügen. Alle Projekte einer Projektmappe werden durch die Optionen des Erstellen Menüs erzeugt: – Erstellen|Projektmappe erstellen kompiliert und linkt alle Dateien, die seit dem letzten Build verändert wurden. – Erstellen|Projektmappe neu erstellen kompiliert und linkt alle Dateien, unabhängig davon, ob sie geändert wurden oder nicht. – Erstellen|Projektmappe bereinigen entfernt alle Dateien, die beim nächsten Build wieder erzeugt werden. Der Projektmappen-Explorer enthält zahlreiche verschiedene Kontextmenüs, je nachdem, ob man eine Projektmappe, ein Projekt usw. anklickt. Mit ihnen kann man steuern, wie Projekte erzeugt und ausgeführt werden. Insbesondere kann man damit: – mit „Startprojekte festlegen“ das Projekt auswählen, das mit dem nächsten Debuggen|Debuggen Starten (F5) erzeugt und ausgeführt wird (das Startprojekt). – die Reihenfolge festlegen, in der die Projekte oder die Dateien eines Projekts bei einer Build Operation bearbeitet werden. – Projekte zu einer Projektmappe hinzufügen oder aus ihr entfernen. Entsprechende Operationen stehen für die Dateien eines Projekts zur Verfügung. – Abhängigkeiten für die Projekte einer Projektmappe oder die Dateien eines Projekts festlegen. – Projektkonfigurationen festlegen.
24
1 Die Entwicklungsumgebung
– festlegen, dass mehr als seine Anwendung durch Debuggen|Debuggen Starten (F5) ausgeführt werden soll Aufgabe 1.8 Erzeugen Sie eine Projektmappe mit dem Namen „MeineProjektmappe“ und einer CLR Windows Forms-Anwendung mit dem Namen „MeinProjekt1“. Es ist nicht notwendig, irgendwelche Komponenten auf das Formular zu setzen. Damit man die Anwendung später identifizieren kann, soll die Eigenschaft Text des Formulars auf „Projekt 1“ gesetzt werden. Verschaffen Sie sich mit dem Windows-Explorer nach jeder der folgenden Teilaufgaben einen Überblick über die in den verschiedenen Verzeichnissen erzeugten Dateien. a) Führen Sie Debuggen|Debuggen Starten (F5) aus. b) Ändern Sie die Konfiguration von Debug zu Release und führen Sie Debuggen|Debuggen Starten (F5) aus. c) Fügen Sie MeineProjektmappe mit dem Projektmappen-Explorer ein weiteres Projekt MeinProjekt2 hinzu. Setzen Sie die Eigenschaft Text des Formulars auf „Projekt 2“. d) Schalten Sie mit „Startprojekt festlegen“ im Projektmappen-Explorer das Startprojekt auf Projekt2 um und starten Sie Debuggen|Debuggen Starten (F5). Schalten Sie zurück auf Projekt2 e) Führen Sie Erstellen|Projektmappe bereinigen aus. f) Löschen Sie die Datei mit der Erweiterung „.ncb“ manuell.
1.9 Hilfsmittel zur Gestaltung von Formularen Ԧ Für die Gestaltung von Formularen stehen über die Menüoption Format (die mit der Aktivierung der Entwurfsansicht eines Formulars eingeblendet wird) und die Symbolleiste Layout zahlreiche Optionen zur Verfügung.
Die meisten dieser Optionen können auf eine Gruppe von markierten Steuerelementen angewandt werden. Dazu klickt man auf eine freie Stelle im Formular und fasst die Komponenten durch Ziehen mit der gedrückten linken Maustaste zusammen.
1.10 Win32-, MFC- und Konsolen-Anwendungen Ԧ
25
Die Reihenfolge, in der die Steuerelemente des Formulars während der Ausführung des Programms mit der Tab-Taste angesprungen werden (Tab-Ordnung), kann man über die letzte dieser Optionen bzw. Ansicht|Aktivierreihenfolge einstellen. Nach der Auswahl dieser Option wird die Tab-Ordnung jeder Komponente angezeigt und kann durch Anklicken verändert werden:
Aufgabe 1.9 Setzen Sie einige Komponenten (z.B. zwei Buttons und ein Label) in unregelmäßiger Anordnung auf ein Formular. Bringen Sie sie vor jeder neuen Teilaufgabe wieder in eine unregelmäßige Anordnung. a) Ordnen Sie alle Komponenten an einer gemeinsamen linken Linie aus. b) Geben Sie allen Komponenten dieselbe Breite. c) Verändern Sie die Tab-Ordnung.
1.10 Win32-, MFC- und Konsolen-Anwendungen Ԧ Mit Visual Studio kann man nicht nur Windows-Programme für .NET schreiben, sondern auch Windows-Programme auf der Basis der Win32 API oder der MFC Klassenbibliothek sowie Konsolen-Anwendungen. Obwohl auf solche Programme in diesem Buch nicht weiter eingegangen wird, soll hier kurz skizziert werden, wie man solche Anwendungen entwickelt. Da ein Schwerpunkt dieses Buches auf Standard-C++ liegt, können diese Teile des Buches auch mit solchen Anwendungen genutzt werden.
26
1 Die Entwicklungsumgebung
1.10.1 Win32-Anwendungen Ԧ In der Professional Edition von Visual Studio kann man mit Datei|Neu|Projekt|Win32|Win32 Projekt kann man Windows-Anwendungen auf der Basis der Win32 API (Application Programmers Interface) entwickeln. Die erste Version dieser Bibliothek wurde ca. 1992 mit Windows NT veröffentlicht und basiert auf der Programmiersprache C. Wer damit auch heute noch programmieren will, sei auf die alten Ausgaben der Bücher von Charles Petzold „Programming Windows“ verwiesen. Normalerweise gibt es aber heutzutage keinen Grund, neue Projekte auf dieser Basis zu beginnen. Es ist aber häufig notwendig, alte Projekte, die auf dieser Bibliothek basieren, weiterzuentwickeln. Das ist mit Visual Studio 2008 zumindest im Prinzip möglich. Da der C/C++-Compiler von 2008 aber sehr viel schärfere Prüfungen als ältere Compiler durchführt, kann es gut sein, dass Programme, die mit älteren Compilern anstandslos kompiliert wurden, nicht mehr akzeptiert werden. Das liegt dann oft an fragwürdigen Sprachkonstrukten, die zwar nicht unbedingt zu einem Laufzeitfehler führen müssen, aber doch leicht dazu führen können. 1.10.2 MFC-Anwendungen Ԧ Mit Datei|Neu|Projekt|MFC|MFC Anwendung kann man Windows-Anwendungen auf der Basis von MFC (Microsoft Foundation Classes) entwickeln. MFC ist eine ca. 1992 von Microsoft entwickelte Klassenbibliothek, mit der man Windows-Programme einfacher als mit API-Funktionen entwickeln kann. Die MFC wurde inzwischen durch das .NET Framework abgelöst, mit dem man grafische Benutzeroberflächen noch einfacher gestalten kann, und das zahlreiche weitere Vorteile gegenüber MFC hat. Deshalb besteht heute meist keine Veranlassung mehr, neue Anwendungen mit MFC zu entwickeln. Die Unterstützung von MFC ermöglicht vor allem, die Vorteile von Visual Studio 2008 auch bei der Weiterentwicklung von älteren Anwendungen zu nutzen. Da für MFC-Anwendungen im Wesentlichen alle Sprachelemente von StandardC++ zur Verfügung stehen, gelten die meisten Ausführungen von Teil 2 (Kapitel 3 bis 8) auch für MFC-Anwendungen. Im Folgenden soll lediglich an einem einfachen Beispiel (ähnlich wie in Abschnitt 1.1) gezeigt werden, wie man MFC-Anwendungen entwickelt. Für weitere Informationen zu MFC-Anwendungen wird auf die Online-Hilfe verwiesen. 1. In Datei|Neu|Projekt|MFC|MFC Anwendung gibt man nach Name einen Namen für das Projekt ein (in diesem Beispiel wurde MFCDialogBasierteAnw verwendet). Dann klickt man auf Next und wählt anschließend „Dialog based“ Anwendung:
1.10 Win32-, MFC- und Konsolen-Anwendungen Ԧ
27
Die weiteren Auswahloptionen für „Benutzeroberflächenfeatures“, „Erweiterte Features“ und „Generierte Klassen“ werden ohne Änderung übernommen. 2. Anschließend erhält man etwa das folgende Fenster, in dem man Elemente in der Toolbox anklicken und auf das Formular setzen kann:
Sollte dieses Fenster nicht angezeigt werden, kann man es durch einen Doppelklick auf IDD_MFCAPPLDIALOGBASED_DIALOG im Resource View aktivieren. 3. Bei MFC-Anwendungen muss man den Steuerelementen Variable zuordnen. Dazu kann man den „Assistenten zum Hinzufügen von Membervariablen“ ver-
28
1 Die Entwicklungsumgebung
wenden, den man mit der Option „Variable hinzufügen“ im Kontextmenü erhält:
Hier wählt man den Datentyp der Variablen (CEdit für ein Eingabefeld) sowie einen Namen für die Variable (hier: Edit1) aus und schließt den Dialog mit Finish ab:
4. Nach einem Doppelklick auf den Button erzeugt Visual Studio eine Funktion, die aufgerufen wird, wenn der Button angeklickt wird. In dieser Funktion kann man ein Steuerelement mit dem unter 3. vergebenen Namen ansprechen. void CMFCApplDialogBasedDlg::OnBnClickedButton1() { Edit1.SetWindowTextW(L"Hello World"); // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. }
Die nächsten Anweisungen zeigen, wie man Text aus einem Eingabefeld in einen String einlesen und wie man Ausgaben formatieren kann:
1.10 Win32-, MFC- und Konsolen-Anwendungen Ԧ
29
void CMFCApplDialogBasedDlg::OnBnClickedButton1() { // Lese aus dem Eingabefeld: CString t; Edit1.GetWindowText(t); // Formatiere CStrings: int x=17; t.Format(L"x=%d x*x=%d",x,x*x); Edit1.SetWindowTextW(t); }
Offensichtlich leistet dieses Programm auch nicht viel mehr als das von Abschnitt 1.1, das mit wesentlich weniger Aufwand erstellt wurde. Außerdem kann man sich in der Vielzahl der hier zu einem Projekt gehörenden Dateien leicht verlieren. Mit den in diesem Abschnitt beschriebenen Schritten und ein wenig Lektüre in der Online-Hilfe können Sie aber auch den größten Teil der Aufgaben dieses Buches als MFC-Anwendung lösen. 1.10.3 Win32 Konsolen-Anwendungen Ԧ Eine Konsolen-Anwendung verwendet wie ein DOS-Programm ein Textfenster für Ein- und Ausgaben. Im Unterschied zu einem Programm für eine grafische Benutzeroberfläche erfolgen Ein- und Ausgaben vor allem zeichenweise über die Tastatur und den Bildschirm. Solche Programme werden meist von der Kommandozeile aus gestartet. Obwohl eine Konsolen-Anwendung wie ein DOS-Programm aussieht, kann man es nicht unter MS-DOS, sondern nur unter Win32 starten. Ein Projekt für eine solche Anwendung erhält man mit Datei|Neu|Projekt|Win32, Win32 Konsolenanwendung. Dadurch wird eine Datei angelegt, die etwa folgendermaßen aussieht und vor allem eine Funktion mit dem Namen _tmain enthält: #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { return 0; }
Diese Funktion wird beim Start des Konsolen-Programms aufgerufen. Die Anweisungen, die durch dieses Programm ausgeführt werden sollen, fügt man dann in diese Funktion vor return ein. Ein- und Ausgaben erfolgen bei einem solchen Programm vor allem über die in vordefinierten Streams cin cout
// für die Eingabe von der Tastatur // für die Ausgabe am Bildschirm
Für diese Streams sind die Ein- und Ausgabeoperatoren „“ definiert, die man wie in diesem Beispiel verwenden kann:
30
1 Die Entwicklungsumgebung #include "stdafx.h" #include // für cin und cout notwendig using namespace std; int _tmain(int argc, _TCHAR* argv[]) { int x,y; coutx; // den Wert einlesen couty; coutDock=DockStyle::Fill;
Der Datentyp bool hat Ähnlichkeiten mit einem Aufzählungstyp. Er kann die beiden Werte true und false annehmen. Beispielsweise kann man mit der booleschen Eigenschaft Visible die Sichtbarkeit einer visuellen Komponente mit false aus- und mit true anschalten:
50
2 Steuerelemente für die Benutzeroberfläche
Beispiel: Beim Aufruf dieser Funktion wird das Label label1 unsichtbar: private: System::Void button1_Click_1( System::Object^ sender, System::EventArgs^ { label1->Visible=false; }
e)
Bei allen Anweisungen muss man die Sprachregeln von C++ genau einhalten. So muss man z.B. als Begrenzungszeichen für einen String das Zeichen " (Umschalt+2) verwenden und nicht eines der ähnlich aussehenden Akzentzeichen ` oder ´ bzw. das Hochkomma ' (Umschalt+#). Jedes dieser Zeichen führt bei der Übersetzung des Programms zu einer Fehlermeldung des Compilers:
2.3 Labels, Datentypen und Compiler-Fehlermeldungen
51
Ein solcher Fehler bedeutet, dass der Compiler die angezeigte Anweisung nicht verstehen kann, weil sie die Sprachregeln von C++ nicht einhält. Wie dieses Beispiel zeigt, kann ein einziger Fehler eine Reihe von Folgefehlern nach sich ziehen. Mit den Pfeiltasten in der Symbolleiste des Ausgabe-Fensters (Ansicht|Ausgabe) kann man die Stellen im Quelltext, die den Fehler verursacht haben, der sowie mit einem Reihe nach anschauen. Diese Stellen erhält man auch mit Doppelklick auf eine solche Meldung. Wenn man im Ausgabe-Fenster eine solche Fehlermeldung anklickt und dann die Taste F1 drückt, zeigt die Online-Hilfe eine ausführlichere Beschreibung des Fehlers an als nur den Hinweis nach „error“:
Wenn Sie eine solche Fehlermeldung des Compilers erhalten, müssen Sie den Fehler im Quelltext beheben. Das kann vor allem für Anfänger eine mühselige Angelegenheit sein, insbesondere wenn die Fehlermeldung nicht so präzise auf den Fehler hinweist wie in diesem Beispiel. Da Fehler Folgefehler nach sich ziehen können, sollte man immer den Fehler zur ersten Fehlermeldung zuerst beheben. Manchmal sind die Fehlerdiagnosen des Compilers sogar eher irreführend als hilfreich und schlagen eine falsche Therapie vor. Auch wenn Ihnen das kaum nützt: Betrachten Sie es als kleinen Trost, dass die Fehlermeldungen in anderen Programmiersprachen (z.B. in C) oft noch viel irreführender sind und schon so manchen Anfänger völlig zur Verzweiflung gebracht haben. Aufgabe 2.3 Schreiben Sie ein Programm, das nach dem Start dieses Fenster anzeigt:
52
2 Steuerelemente für die Benutzeroberfläche
Für die folgenden Ereignisbehandlungsroutinen müssen Sie sich in der OnlineHilfe über einige Eigenschaften informieren, die bisher noch nicht vorgestellt wurden. Beim Anklicken der Buttons mit der Aufschrift – ausrichten soll der Text im Label mit Hilfe der Eigenschaft TextAlign (siehe Online-Hilfe) links bzw. rechts ausgerichtet werden. Damit das ganze Label sichtbar ist, soll seine Farbe z.B. auf Gelb gesetzt werden. Damit die Größe des Labels nicht der Breite des Textes angepasst wird, soll die Eigenschaft Autosize (siehe Online-Hilfe) im Eigenschaftenfenster auf false gesetzt werden. – sichtbar/unsichtbar soll das Label sichtbar bzw. unsichtbar gemacht werden, – links/rechts soll das Label so verschoben werden, dass sein linker bzw. rechter Rand auf dem linken bzw. rechten Rand des Formulars liegt. Damit der rechte Rand des Labels genau auf den rechten Rand des Formulars gesetzt wird, verwenden Sie die Eigenschaft ClientRectangle (siehe Online-Hilfe) eines Formulars.
2.4 Funktionen, Methoden und die Komponente TextBox Eine TextBox kann wie ein Label einen Text des Datentyps String anzeigen. Der angezeigte Text ist der Wert der Eigenschaft Text, der wie bei einem Label im Eigenschaftenfenster oder im Programm gesetzt werden kann: textBox1->Text = "Hallo";
Im Unterschied zu einem Label kann ein Anwender in eine TextBox auch während der Laufzeit des Programms Text eingeben. Die Eigenschaft Text enthält immer den aktuell angezeigten Text und ändert sich mit jeder Eingabe des Anwenders. Dieser Text kann in einem Programm verwendet werden, indem man die Eigenschaft Text z.B. auf der rechten Seite einer Zuweisung einsetzt: label1->Text = textBox1->Text;
Eine TextBox wird oft als Eingabefeld zur Dateneingabe verwendet. Sie übernimmt in einem Windows-Programm oft Aufgaben, die in einem C++-Konsolenprogramm von cin>> ... und cout angeben muss. Deshalb kann man die statischen Elementfunktionen ToInt32 und ToString folgendermaßen aufrufen: Nach dem Namen der Klasse Convert, dem Operator :: und dem Namen der Funktion ToInt32 wird in Klammern der umzuwandelnde String angegeben. Dieser Ausdruck hat den Datentyp int. Entsprechend wird nach Convert::ToString in Klammern ein int-Ausdruck angegeben, der in einen String umgewandelt werden soll. Dieser Ausdruck hat den Datentyp String^. Beispiel: In einem Formular mit zwei TextBoxen haben die beiden Ausdrücke Convert::ToInt32(textBox1->Text) und Convert::ToInt32(textBox2->Text) den Datentyp int. Mit ihnen kann man im Unterschied zu den Strings textBox1->Text und textBox2->Text auch rechnen: Convert::ToInt32(textBox1->Text) + Convert::ToInt32(textBox2->Text)
ist die Summe der Zahlen in den beiden TextBoxen. Diese Summe kann man nun in einer weiteren TextBox textBox3 ausgeben, wenn man sie in einen String umwandelt. Da man Funktionsaufrufe beliebig verschachteln kann, hat man mit textBox3->Text=Convert::ToString( Convert::ToInt32(textBox1->Text)+ Convert::ToInt32(textBox2->Text));
bereits ein einfaches Programm zur Addition von Zahlen geschrieben, wenn man diese Anweisung beim Anklicken eines Buttons ausführt: private: System::Void button1_Click_1( System::Object^ sender, System::EventArgs^ e) { textBox3->Text=Convert::ToString( Convert::ToInt32(textBox1->Text)+ Convert::ToInt32(textBox2->Text)); }
2.4 Funktionen, Methoden und die Komponente TextBox
55
Wenn man mehrere Werte zusammen mit einem Text ausgeben will, ist die statische Elementfunktion Format der Klasse String oft einfacher als die Funktionen der Klasse Convert: static String^ Format(String^ format, ... array^ args); Dieser Funktion übergibt man als Argument für format einen String mit Formatangaben für die anschließend folgenden Argumente. Die Formatangaben bestehen im einfachsten Fall nur aus der in geschweiften Klammern angegebenen Nummer des Arguments ({0} für das erste Argument, {1} für das zweite usw.). Als Argument für args (ein sogenanntes Parameter Array, siehe Abschnitt 9.11) kann man eine beliebig lange Liste mit Werten nahezu beliebiger Datentypen angeben. Das Ergebnis ist dann der format String, in dem die Formatangaben durch die aus den Argumenten mit Convert::ToString erzeugten Strings ersetzt wurden. Weitere Formatangaben werden in Abschnitt 9.4.2 vorgestellt. Beispiel: Wenn Left und Top die Werte 17 und 18 haben, gibt die folgende Anweisung den Text „Left=17, Top=18“ aus: textBox1->Text=String::Format("Left={0},Top={1}", textBox1->Left,textBox1->Top);
Eine nicht statische Elementfunktion wird mit einem Objekt ihrer Klasse (z.B. einer Komponente) und meist dem Pfeiloperator -> aufgerufen. Beispiel: Die Methode Clear der Klasse TextBox wurde schon in Abschnitt 2.1 vorgestellt. Da ihre Deklaration void Clear();// aus der Online-Hilfe zu TextBoxBase kein static enthält, wird sie über ein Objekt (z.B. textBox1) der Klasse TextBox mit dem Pfeiloperator aufgerufen: textBox1->Clear();
Da die Parameterliste von Clear leer ist, wird diese Funktion ohne Argumente aufgerufen. Wenn eine Funktion Parameter hat, muss bei ihrem Aufruf normalerweise für jeden Parameter ein Argument übergeben werden. Der Datentyp des Arguments ist im einfachsten Fall der des Parameters. Beispiel: Mit der für viele Komponenten definierten Methode void SetBounds(int x,int y,int width,int height); kann man die Eigenschaften Position.x, Position.y, Size.Width und Size.Height der Komponente mit einer einzigen Anweisung setzen. Die
56
2 Steuerelemente für die Benutzeroberfläche
Größe und Position einer TextBox textBox1 kann deshalb so gesetzt werden: textBox1->SetBounds(0,0,100,20);
In C++/CLI gibt es auch für Datentypen wie int, die keine Klassen sind, Funktionen, die wie Elementfunktionen von Klassen aufgerufen werden können. So kann z.B. ein int-Wert mit der Funktion ToString in einen String konvertiert werden, die wie eine Elementfunktion aufgerufen wird. Dabei wird aber der Punkt-Operator „.“und nicht der Pfeil-Operator verwendet. Ein entsprechendes Gegenstück zur Konvertierung eines Strings in eine Zahl gibt es aber nicht. Beispiel: Ein int-Wert kann in einen String konvertiert werden durch textBox1->Text=(17).ToString();
Diese Anweisung hat denselben Effekt wie textBox1->Text=Convert::ToString(17);
Im Beispiel zur Addition von Zahlen kann man das Ergebnis auch so ausgeben: textBox3->Text=(Convert::ToInt32(textBox1->Text) + Convert::ToInt32(textBox2->Text)).ToString();
Manche Funktionen können mit unzulässigen Argumenten aufgerufen werden. So kann man z.B. Convert::ToInt32 mit einem String aufrufen, der wie in Convert::ToInt32("Eins"); // das geht schief
nicht in eine Zahl umgewandelt werden kann. Dann erhält man die Fehlermeldung:
Eine solche Meldung enthält eine Beschreibung der Fehlerursache, hier „Die Eingabezeichenfolge hat das falsche Format.“ Durch Anklicken des Buttons Weiter kann man das Programm fortsetzen.
2.4 Funktionen, Methoden und die Komponente TextBox
57
2.4.2 Mehrzeilige TextBoxen Eine TextBox kann auch mehrzeilige Texte darstellen, wenn man ihre Eigenschaft Multiline auf true setzt (z.B. im Eigenschaftenfenster oder nach einer Markierung der CheckBox, die nach dem Anklicken des kleinen Dreiecks am oberen Rand der TextBox angezeigt wird):
Eine mehrzeilige TextBox hat am oberen und unteren Rand Ziehpunkte, mit denen man ihre Höhe verändern kann. Im Unterschied zu einer einzeiligen TextBox wird Text, der breiter als die TextBox ist, in Abhängigkeit von ihrer Breite und der gewählten Schriftart (die Eigenschaft Font) in Zeilen aufgeteilt. textBox1->Text="Dieser Text ist breiter als die Textbox";
Die TextBox Elementfunktion void AppendText(String^ text); fügt den als Argument übergebenen String am Ende einer TextBox hinzu. Enthält dieser String die Zeichen "\r\n" (genau in dieser Reihenfolge), beginnt der darauf folgende Text in einer neuen Zeile. Diese Zeichen erhält man auch mit Environment::NewLine. Mehrzeilige TextBoxen und die Funktion AppendText werden oft zur Anzeige der Ergebnisse eines Programms verwendet. Dabei kann man verschiedene Strings mit dem Operator „+“ zusammenfügen. Beispiel: Die Funktion private:System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { textBox1->AppendText((1).ToString()+"23"); textBox1->AppendText("abcd\r\n"); textBox1->AppendText("xyz"+Environment::NewLine); textBox1->AppendText(Convert::ToString(7)+"\r\n"); textBox1->AppendText(String::Format( "x={0} y={1}\r\n",17,18)); }
enthält alle bisher vorgestellten Anweisungen zur Ausgabe in einer TextBox. Sie erzeugt die folgende Ausgabe:
58
2 Steuerelemente für die Benutzeroberfläche
Die einzelnen Zeilen einer mehrzeiligen TextBox sind die Elemente des sogenannten CLI-Arrays property array^ Lines Die erste Zeile ist textBox1->Lines[0], die zweite textBox1->Lines[1] usw. Die Anzahl der Zeilen der TextBox ist der Wert der Eigenschaft Length von Lines property int Length Aufgaben 2.4 1. Schreiben Sie ein einfaches Rechenprogramm, mit dem man zwei Ganzzahlen addieren kann. Nach dem Anklicken des Buttons mit der Aufschrift clear sollen sämtliche Eingabefelder gelöscht werden.
Offensichtlich produziert dieses Programm für ganze Zahlen falsche Ergebnisse, wenn die Summe außerhalb des Bereichs –2147483648 .. 2147483647 (–231..231 – 1) liegt:
Die Ursache für diese Fehler werden wir später kennen lernen. 2. Ergänzen Sie das Programm aus Aufgabe 1 um einen Button, mit dem auch Zahlen mit Nachkommastellen wie z.B. 3,1415 addiert werden können. Verwenden Sie dazu die Funktion
2.5 Klassen, ListBox und ComboBox
59
static double ToDouble(String^ value); // weitere Informationen dazu in der Online-Hilfe Der Datentyp double ist einer der Datentypen, die in C++ Zahlen mit Nachkommastellen darstellen können. Solche Datentypen haben einen wesentlich größeren Wertebereich als der Ganzzahldatentyp int. Deshalb treten Bereichsüberschreitungen nicht so schnell auf. 3. Ergänzen Sie das Programm aus Aufgabe 2 um Buttons für die Grundrechenarten +, –, * und /. Die Aufschrift auf den Buttons soll über die Eigenschaft Font auf 14 Punkt und fett (Bold) gesetzt werden. Die jeweils gewählte Rechenart soll in einem Label zwischen den ersten beiden Eingabefeldern angezeigt werden. 4. Ergänzen Sie das Programm aus Aufgabe 3 um eine mehrzeilige Textbox, in die beim Anklicken eines der Buttons +, –, * und / die jeweilige Rechnung geschrieben wird. Verwenden Sie die Funktion Format, um die Operanden, den Operator und das Ergebnis in einer Zeile auszugeben.
5. Geben Sie im laufenden Programm im ersten Eingabefeld einen Wert ein, der nicht in eine Zahl umgewandelt werden kann, und setzen Sie das Programm anschließend fort.
2.5 Klassen, ListBox und ComboBox Eine wichtige Kategorie von Datentypen sind Klassen. Im Unterschied zu elementaren Datentypen wie int können diese Datentypen Eigenschaften, Datenelemente, Methoden und Ereignisse enthalten. Klassen sind die Grundlage der sogenannten objektorientierten Programmierung. Dabei werden Programme aus Bausteinen (Klassen) zusammengesetzt, die wiederum Elemente eines Klassentyps enthalten können. Wir haben Klassen bisher schon als Datentypen der Toolbox-Komponenten kennen gelernt: Alle Komponenten der Toolbox haben einen Datentyp, der eine
60
2 Steuerelemente für die Benutzeroberfläche
Klasse ist. Die Bibliotheken von Visual Studio enthalten zahlreiche weitere Klassen, die oft als Eigenschaften dieser Komponenten verwendet werden. Im Folgenden wird am Beispiel der Klassen ListBox und ComboBox vor allem gezeigt, – wie man die Elemente von Eigenschaften eines Klassentyps anspricht, und – dass Klassen oft viele Gemeinsamkeiten mit anderen Klassen haben.
Eine ListBox zeigt wie eine mehrzeilige TextBox Textzeilen an. Diese können aber im Unterschied zu einer TextBox vom Anwender nicht verändert werden. ListBoxen werden vor allem dazu verwendet, eine Liste von Optionen anzuzeigen, aus denen der Anwender eine auswählen kann. Die angezeigten Zeilen sind die Zeilen der Eigenschaft Items, die den Datentyp ObjectCollection^ hat: property ObjectCollection^ Items Der Datentyp ObjectCollection ist eine Klasse, die unter anderem die folgenden Elemente enthält: int Add(Object^ item);// fügt das Argument für item am Ende ein void Insert(int index,Object^ item); // fügt das Argument für item an der Position index ein Ein Element einer Eigenschaft eines Klassentyps wird meist wie das Element einer Komponente angesprochen: Nach dem Namen der Eigenschaft (z.B. listBox1-> Items) gibt man den Pfeiloperator „–>“ und den Namen des Elements (z.B. der Methode Add) an. Beispiel: Ein Aufruf von Add fügt das Argument am Ende der ListBox als neue Zeile ein: listBox1->Items->Add("Neue Zeile am Ende");
Ein Aufruf von Insert mit dem Argument 0 für Index fügt eine Zeile am Anfang der ListBox ein: listBox1->Items->Insert(0,"Neue Zeile vorne");
Gelegentlich (aber nur selten) gibt es auch Ausnahmen von diesem Schema. Falls das Symbol ^ in der Deklaration einer Eigenschaft nach dem Datentyp nicht angegeben ist, muss man auf die Elemente mit dem Punktoperator „.“ zugreifen. Die Bedeutung dieses Symbols ^ kann erst später (siehe Abschnitt 3.12.15) genauer erklärt werden. Vorläufig reicht es aus wenn man weiß, wie es sich auf die Schreibweise beim Zugriff auf Elemente von Eigenschaften auswirkt.
2.5 Klassen, ListBox und ComboBox
61
Beispiel: Die schon in Abschnitt 2.3 beschriebene Eigenschaft Location (für die Position der Komponente) hat den Klassentyp Point mit den Elementen X und Y: property Point Location; Da diese Deklaration nach dem Datentyp kein Symbol ^ enthält, müssen die Elemente mit dem Punktoperator und nicht mit dem Pfeiloperator angesprochen werden: listBox1->Location.X=17;
Die Einträge in eine Eigenschaft des Typs ObjectCollection (wie z.B. die Eigenschaft Items einer ListBox) können nicht nur zur Laufzeit mit Funktionen wie Add eingegeben werden, sondern auch zur Entwurfszeit nach einem Klick auf des Eigenschaftenfensters: Zeile
in der
Eine ComboBox besteht im Wesentlichen aus einer ListBox und einer TextBox. Die ListBox wird nach dem Anklicken des rechten Dreiecks aufgeklappt. Aus ihr kann ein Eintrag ausgewählt werden, der dann in die TextBox übernommen wird und da editiert werden kann. Die Zeilen der ComboBox sind wie bei einer ListBox der Wert der Eigenschaft Items vom Typ ObjectCollection. Der Text im Eingabefeld der ComboBox wird durch die Eigenschaft Text des Datentyps String dargestellt. Beispiel: Die Beispiele mit listBox1->Items->… lassen sich auf eine ComboBox comboBox1 übertragen, indem man listBox1 durch comboBox1 ersetzt. Den Text comboBox1->Text im Eingabefeld der ComboBox kann man wie die Eigenschaft Text einer TextBox verwenden Da sich die mit einer Eigenschaft zulässigen Operationen allein aus ihrem Datentyp ergeben, kann man zwei verschiedene Eigenschaften desselben Datentyps auf dieselbe Art verwenden. Ist dieser Datentyp eine Klasse, haben beide Eigenschaften dieselben Elemente (Eigenschaften, Methode und Ereignisse), die
62
2 Steuerelemente für die Benutzeroberfläche
ebenfalls auf dieselbe Art verwendet werden können. Wenn eine Eigenschaft, die Sie noch nicht kennen, denselben Datentyp hat wie eine Ihnen schon bekannte Eigenschaft, können Sie die neue Eigenschaft genauso verwenden wie die bereits bekannte, ohne dass Sie irgendetwas Neues lernen müssen. Beispiel: Wenn Sie die Klasse ObjectCollection aus der Arbeit mit der Eigenschaft Items einer ListBox kennen, können Sie mit einer Eigenschaft dieses Typs in jeder anderen Komponente genauso arbeiten. Wenn Sie also z.B. in der Online-Hilfe sehen, dass die Eigenschaft Items einer ToolStripComboBox den Datentyp ObjectCollection hat, können Sie mit dieser Eigenschaft genauso arbeiten. Das gilt nicht nur für die Operationen mit einer Komponente im Programm, sondern auch für die Operationen im Eigenschaftenfenster: Nach dem Anklicken einer Eigenschaft des Typs ObjectCollection im Eigenschaftenfenster wird der Zeichfolgen-Editor geöffnet. Oft stellt eine Klasse Gemeinsamkeiten verschiedener Klassen dar. Dann enthält sie die gemeinsamen Elemente der spezielleren Klassen. In der objektorientierten Programmierung werden solche Gemeinsamkeiten durch Vererbung zum Ausdruck gebracht. Vererbung bedeutet, dass eine abgeleitete Klasse (die Klasse, die erbt) alle Elemente einer Basisklasse übernimmt. Die Online-Hilfe zeigt die Vererbungshierarchie für jede Klasse im Abschnitt „Vererbungshierarchie“ an. Beispiel: Die Klasse ComboBox erbt von der Klasse ListControl:
Sie enthält deshalb alle Elemente von ListControl. Da eine ListBox ebenfalls von ListControl erbt, kann man die gemeinsamen Elemente einer ListBox und einer ComboBox auf dieselbe Art verwenden. ListControl erbt wiederum von Control. Diese Klasse enthält die gemeinsamen Eigenschaften, Methoden und Ereignisse aller Steuerelemente (Controls). Dazu gehören z.B. Eigenschaften wie Visible, die schon in Abschnitt 2.3 vorgestellt wurde.
2.5 Klassen, ListBox und ComboBox
63
Deshalb gilt alles, was für die Eigenschaft Visible in Abschnitt 2.3 gesagt wurde, auch für die Eigenschaft Visible jeder anderen Klasse, die diese Eigenschaft von der Klasse Control erbt. Die Klasse Object ist die Basisklasse aller .NET-Klassen. Eine abgeleitete Klasse enthält meist noch zusätzliche Elemente, die die Unterschiede zur Basisklasse ausmachen. Einige zusätzliche Elemente einer ListBox: Falls ein Benutzer einen Eintrag ausgewählt hat, steht der Index dieses Eintrags unter der int-Eigenschaft SelectedIndex zur Verfügung (0 für den ersten Eintrag). Falls kein Eintrag ausgewählt wurde, hat SelectedIndex den Wert –1. Der ausgewählte Eintrag ist der Wert der Eigenschaft property Object^ SelectedItem Sie kann mit ihrer Elementfunktion ToString als String dargestellt werden: textBox1->AppendText(listBox1->SelectedItem->ToString());
Setzt man die boolesche Eigenschaft Sorted auf true, werden die Einträge alphanumerisch sortiert angezeigt. Dieser Abschnitt sollte insbesondere auch zeigen, dass die zahlreichen Komponenten doch nicht so unüberschaubar viele verschiedene Eigenschaften und Methoden haben, wie man das auf den ersten Blick vielleicht befürchtet. In der objektorientierten Programmierung werden Programme aus Bausteinen (Klassen) zusammengesetzt. Mit einer geschickt konstruierten Klassenbibliothek kann man aus relativ wenigen Klassen Programme für eine Vielzahl von Anwendungen entwickeln. Die Wiederverwendung der Klassen erleichtert den Überblick und den Umgang mit den Komponenten beträchtlich. Aufgabe 2.5 Schreiben Sie ein Programm mit einer ListBox, einer ComboBox, einer einzeiligen TextBox, zwei Buttons und zwei Labels:
64
2 Steuerelemente für die Benutzeroberfläche
a) Beim Anklicken von Button1 soll der Text aus der einzeiligen TextBox der ListBox und der ComboBox hinzugefügt werden. b) Wenn ein Eintrag in der ListBox angeklickt wird, soll er auf Label1 angezeigt werden. c) Beim Anklicken von Button2 soll der in der ComboBox ausgewählte Text auf dem Label2 angezeigt werden.
2.6 Buttons und Ereignisse Ein Button ermöglicht einem Anwender, die Ausführung von Anweisungen zu starten. Durch einen einfachen Mausklick auf den Button werden die für das Ereignis Click definierten Anweisungen ausgeführt. Buttons werden oft in Dialogfenstern (wie z.B. Datei|Öffnen oder Datei|Speichern unter) verwendet, über die ein Programm Informationen mit einem Benutzer austauscht. Solche Fenster enthalten meist einen „Abbrechen“-Button, mit dem das Fensters ohne weitere Aktionen geschlossen werden kann, und einen „OK“-Button, mit dem die Eingaben bestätigt und weitere Anweisungen ausgelöst werden.
2.6 Buttons und Ereignisse
65
Ein Button kann auf die folgenden Ereignisse reagieren:
Ereignis Click
MouseDown MouseUp MouseMove KeyPress
KeyUp KeyDown
Enter Leave
DragDrop DragOver EndDrag
Ereignis tritt ein wenn der Anwender die Komponente mit der Maus anklickt (d.h. die linke Maustaste drückt und wieder loslässt), oder wenn der Button den Fokus (siehe Abschnitt 2.6.2) hat und die Leertaste, Return- oder Enter-Taste gedrückt wird. wenn eine Maustaste gedrückt bzw. wieder losgelassen wird, während der Mauszeiger über der Komponente ist. wenn der Mauszeiger über die Komponente bewegt wird. wenn eine Taste auf der Tastatur gedrückt wird, während die Komponente den Fokus hat. Dieses Ereignis tritt im Unterschied zu den nächsten beiden nicht ein, wenn eine Taste gedrückt wird, die keinem ASCIIZeichen entspricht, wie z.B. eine Funktionstaste (F1 usw.), die Strg-Taste, die Umschalttaste (für Großschreibung) usw. wenn eine beliebige Taste auf der Tastatur gedrückt wird, während die Komponente den Fokus hat. Diese Ereignisse treten auch dann ein, wenn man die Alt-, AltGr-, Shift-, Strgoder Funktionstasten allein oder zusammen mit anderen Tasten drückt. wenn die Komponente den Fokus erhält. wenn die Komponente den Fokus verliert. wenn der Anwender ein gezogenes Objekt – über der Komponente ablegt, – über die Komponente zieht, – über der Komponente ablegt.
Diese Ereignisse werden von der Klasse Control geerbt. Da diese Klasse eine gemeinsame Basisklasse zahlreicher Steuerelemente ist, stehen sie nicht nur für einen Button, sondern auch für zahlreiche andere Komponenten zur Verfügung. 2.6.1 Parameter der Ereignisbehandlungsroutinen Die Ereignisse, die für das auf dem Formular ausgewählte Steuerelement eintreten können, werden im Eigenschaftenfenster nach dem Anklicken des Icons für die Events (Ereignisse) angezeigt.
66
2 Steuerelemente für die Benutzeroberfläche
Mit einem Doppelklick auf die Zeile eines Ereignisses im Eigenschaftenfenster erzeugt Visual Studio die Funktion (Ereignisbehandlungsroutine, engl. „event handler“, in der Online-Hilfe auch „Ereignishandler“), die bei diesem Ereignis aufgerufen wird. Sie wird dann im Quelltexteditor angezeigt, wobei der Cursor am Anfang der Funktion steht.
Für das Ereignis KeyPress von button1 erhält man so diese Ereignisbehandlungsroutine: private: System::Void button1_KeyPress(System::Object^ sender, System::Windows::Forms::KeyPressEventArgs^ {
e)
}
An eine Ereignisbehandlungsroutine werden über die Parameter (zwischen den runden Klammern) Daten übergeben, die in Zusammenhang mit dem Ereignis zur Verfügung stehen. Vor jedem Parameter steht sein Datentyp. In der Funktion button1_KeyPress hat der Parameter sender (den wir allerdings vorläufig nicht verwenden) den Datentyp Object^ und der Parameter e den Datentyp KeyPressEventArgs^. Die Klasse KeyPressEventArgs enthält das Element property wchar_t KeyChar Es enthält das Zeichen der Taste, die auf der Tastatur gedrückt wurde und so zum Aufruf dieser Funktion geführt hat. Beispiel: Diese Ereignisbehandlungsroutine gibt das Zeichen der gedrückten Taste in einer TextBox aus: private: System::Void button1_KeyPress(System::Object^ sender, System::Windows::Forms::KeyPressEventArgs^ e) { textBox1->AppendText((e->KeyChar).ToString()); }
2.6 Buttons und Ereignisse
67
Bei den Ereignissen KeyDown und KeyUp hat e den Datentyp KeyEventArgs: private: System::Void button1_KeyDown(System::Object^ sender, System::Windows::Forms::KeyEventArgs^ { }
e)
Diese Klasse enthält unter anderem Elemente die anzeigen, ob die Umschalt-Taste oder eine der Funktionstasten Alt oder Strg gedrückt wurde: property virtual bool Alt // true, falls die Alt-Taste gedrückt wurde, sonst false property bool Control // true, falls die Strg-Taste gedrückt wurde, sonst false property virtual bool Shift // true, falls die Umschalt-Taste gedrückt wurde Beim Schließen eines Formulars (sowohl nach dem Aufruf der Methode Close als auch nach dem Anklicken des Schließen-Buttons ) wird das Ereignis FormClosing ausgelöst, in dessen Ereignisbehandlungsroutine man z.B. fragen kann, ob man Änderungen speichern will. Um das Schließen des Formulars abzubrechen, setzt man die Eigenschaft Cancel des Arguments e der Ereignisbehandlungsroutine auf true. Das Gegenstück zum Ereignis FormClosing ist der Konstruktor des Formulars. Diese Funktion findet man am Anfang der Formular-Klasse in der Datei „Form1.h“: Form1(void) { InitializeComponent(); // //TODO: Konstruktorcode hier hinzufügen. // }
Um Anweisungen beim Erzeugen eines Formulars auszuführen, kann man diese in den Zeilen nach TODO einfügen. Setzt man hier irgendwelche Eigenschaften, hat das im Wesentlichen denselben Effekt wie wenn man sie im Eigenschaftenfenster setzt. 2.6.2 Der Fokus und die Tabulatorreihenfolge Ereignisse, die durch die Maus ausgelöst werden (z.B. Click oder MouseMove), werden immer dem Steuerelement zugeordnet, über dem sich der Mauszeiger gerade befindet. Bei Tastaturereignissen (wie z.B. KeyPress) ist diese Zuordnung nicht möglich. Sie werden dem Steuerelement zugeordnet, das gerade den Fokus hat. Ein Steuerelement erhält z.B. durch Anklicken oder durch wiederholtes Drücken der Tab-Taste den Fokus. In jedem Formular hat immer nur ein Steuerelement den Fokus. Es wird auch als das gerade aktive Steuerelement bezeichnet. Ein
68
2 Steuerelemente für die Benutzeroberfläche
Button, der den Fokus hat, wird durch einen schwarzen Rand optisch hervorgehoben. Wurde während der Laufzeit eines Programms noch kein Steuerelement als aktives Steuerelement ausgewählt, hat das erste in der Tabulatorreihenfolge den Fokus. Die Tabulatorreihenfolge ist die Reihenfolge, in der die einzelnen Steuerelemente durch Drücken der Tab-Taste den Fokus erhalten. Falls diese Reihenfolge nicht explizit (über die Eigenschaft TabIndex bzw. über Ansicht|Aktivierreihenfolge) gesetzt wurde, entspricht sie der Reihenfolge, in der die Steuerelemente während der Entwurfszeit auf das Formular gesetzt wurden. Von der Aktivierung über die Tab-Taste sind die Steuerelemente ausgenommen, – die deaktiviert sind (die Eigenschaft Enabled hat den Wert false), – die nicht sichtbar sind (die Eigenschaft Visible hat den Wert false), – bei denen die Eigenschaft TabStop den Wert false hat. 2.6.3 Einige weitere Eigenschaften von Buttons Über die Eigenschaft Image kann ein Button mit einem Bild verziert werden:
Die Grafik kann während der Entwurfszeit im Eigenschaftenfenster durch Anklicken der rechten Spalte der Eigenschaft Image ausgewählt oder während der Laufzeit des Programms über die Methode FromFile geladen werden: button1->Image = Image::FromFile("C:\\Programme\\Microsoft Visual Studio 9.0\\Common7\\VS2008ImageLibrary\\ icons\\WinXP\\repair.ico"); button1->ImageAlign = ContentAlignment::MiddleLeft;
Die Visual Studio 2008 Bildbibliothek „VS2008ImageLibrary.zip“ enthält zahlreiche Bilder, von denen viele auch auf Buttons verwendet werden können. Sie wird bei der Installation von Visual Studio standardmäßig in das Verzeichnis „Programme\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary“ kopiert. Um die Bilder zu verwenden, müssen sie extrahiert werden. Bei manchen Formularen soll das Drücken der ESC-Taste bzw. der Return- oder Enter-Taste denselben Effekt wie das Anklicken eines Abbrechen-Buttons oder OK-Buttons haben (wie z.B. bei einem Datei-Öffnen Dialog). Das erreicht man über die Eigenschaften AcceptButton und CancelButton eines Formulars: – Wenn der Eigenschaft AcceptButton ein Button zugewiesen wird (z.B. über das Pulldown-Menü im Eigenschaftenfenster), tritt bei diesem Button das
2.6 Buttons und Ereignisse
69
Ereignis OnClick auf, wenn die Return- oder Enter-Taste gedrückt wird, auch ohne dass der Button den Fokus hat. – Wenn der Eigenschaft CancelButton ein Button zugewiesen wird, tritt bei diesem Button das Ereignis OnClick auf, wenn die ESC-Taste gedrückt wird. Aufgabe 2.6 Schreiben Sie ein Programm, das etwa folgendermaßen aussieht:
a) Wenn für den „Test“-Button eines der Ereignisse Click, Enter usw. eintritt, soll ein Text in die zugehörige TextBox geschrieben werden. Für die Ereignisse, für die hier keine weiteren Anforderungen gestellt werden, reicht ein einfacher Text, der das Ereignis identifiziert (z.B. „Click“). Zur Anzeige von weiteren Parametern kann die Funktion String::Format verwendet werden. – Beim Ereignis KeyPress soll die gedrückte Taste angezeigt werden. – Bei den Ereignissen KeyUp und KeyDown soll der Wert der Eigenschaft KeyValue angezeigt werden. – Beim Ereignis MouseMove sollen die Mauskoordinaten angezeigt werden, die als int-Elemente X und Y des Parameters e zur Verfügung stehen. b) Mit dem Button Clear sollen alle TextBoxen gelöscht werden können. c) Beobachten Sie, welche Ereignisse eintreten, wenn der Test-Button – angeklickt wird – den Fokus hat und eine Taste auf der Tastatur gedrückt wird – den Fokus hat und eine Funktionstaste (z.B. F1, Strg, Alt) gedrückt wird – mit der Tab-Taste den Fokus bekommt. d) Die Tabulatorreihenfolge der TextBoxen soll ihrer Reihenfolge auf dem Formular entsprechen (zuerst links von oben nach unten, dann rechts). e) Der Text der Titelzeile „Events“ des Formulars soll im Konstruktor des Formulars zugewiesen werden.
70
2 Steuerelemente für die Benutzeroberfläche
f) Der „Jump“-Button soll immer an eine andere Position springen (z.B. an die gegenüberliegende Seite des Formulars), wenn er vom Mauszeiger berührt wird. Dazu ist keine if-Anweisung notwendig. Falls er angeklickt wird, soll seine Aufschrift auf „getroffen“ geändert werden.
2.7 CheckBoxen, RadioButtons und einfache if-Anweisungen Eine CheckBox besteht im Wesentlichen aus einer Aufschrift (Eigenschaft Text) und einem Markierungsfeld, dessen Markierung ein Anwender durch einen Mausklick oder mit der Leertaste (wenn sie den Fokus hat) setzen oder aufheben kann. Ihre boolesche Eigenschaft Checked ist true, wenn sie markiert ist, und sonst false. Falls sich auf einem Formular oder in einem Container (siehe Abschnitt 2.8) mehrere CheckBoxen befinden, können diese unabhängig voneinander markiert werden. Ein RadioButton hat viele Gemeinsamkeiten mit einer CheckBox. Er besitzt ebenfalls eine Aufschrift (Eigenschaft Text) und ein Markierungsfeld (Eigenschaft Checked), das mit der Maus markiert werden kann. Der entscheidende Unterschied zu CheckBoxen zeigt sich, sobald ein Formular oder ein Container mehr als einen RadioButton enthält: Wie bei den Sender-Stationstasten eines Radios kann immer nur einer der RadioButtons markiert sein. Markiert man einen anderen, wird bei dem bisher markierten die Markierung aufgehoben. Befindet sich nur ein einziger RadioButton auf einem Formular, kann dessen Markierung nicht zurückgenommen werden. Beispiel: CheckBoxen und RadioButtons unter Extras|Optionen|Umgebung|Allgemein:
CheckBoxen und RadioButtons werden vor allem dazu verwendet, einem Anwender Optionen zur Auswahl anzubieten. Falls mehrere Optionen gleichzeitig ausgewählt werden können, verwendet man eine CheckBox. RadioButtons verwendet man dagegen bei Optionen, die sich gegenseitig ausschließen. Ein einziger RadioButton auf einem Formular macht im Unterschied zu einer einzigen CheckBox wenig Sinn.
2.7 CheckBoxen, RadioButtons und einfache if-Anweisungen
71
Sie werden außerdem zur Anzeige von Daten mit zwei Werten (z.B. „schreibgeschützt ja/nein“) verwendet. Falls die Daten nur angezeigt werden sollen, ohne veränderbar zu sein, setzt man die boolesche Eigenschaft Enabled auf false. Der zugehörige Text wird dann grau dargestellt. Die Auswahl der Optionen soll meist den Programmablauf steuern. Falls eine Option markiert ist, sollen z.B. beim Anklicken eines Buttons bestimmte Anweisungen ausgeführt werden, und andernfalls andere. Anders als bei einem Button werden beim Anklicken einer CheckBox meist keine Anweisungen ausgeführt, obwohl das mit entsprechenden Anweisungen beim Ereignis Click auch durchaus möglich ist. Zur Steuerung des Programmablaufs steht die if-Anweisung zur Verfügung. Bei ihr gibt man nach if in runden Klammern einen booleschen Ausdruck an: if (radioButton1->Checked) label1->Text="Glückwunsch"; else label1->Text="Pech gehabt";
Bei der Ausführung dieser if-Anweisung wird zuerst geprüft, ob radioButton1-> Checked den Wert true hat. Trifft dies zu, wird die folgende Anweisung ausgeführt und andernfalls die auf else folgende. Bei einer if-Anweisung ohne else-Zweig wird nichts gemacht, wenn die Bedingung nicht erfüllt ist. Falls mehrere Anweisungen in Abhängigkeit von einer Bedingung ausgeführt werden sollen, fasst man diese mit geschweiften Klammern { } zusammen. Prüfungen auf Gleichheit erfolgen mit dem Operator „==“ (der nicht mit dem Zuweisungsoperator „=“ verwechselt werden darf) und liefern ebenfalls einen booleschen Wert: if (textBox1->Text == "xyz") { textBox1->AppendText("Na so was! \r\n"); textBox1->AppendText("Sie haben das Passwort erraten."); }
Aufgabe 2.7 Schreiben Sie ein Programm mit drei CheckBoxen, drei RadioButtons und zwei gewöhnlichen Buttons:
72
2 Steuerelemente für die Benutzeroberfläche
Beim Anklicken des Buttons Test sollen in Abhängigkeit von den Markierungen der CheckBoxen und der RadioButtons folgende Aktionen stattfinden: a) Die Markierung der CheckBox enable/disable soll entscheiden, ob bei der zweiten CheckBox, dem ersten RadioButton sowie bei button1 die Eigenschaft Enabled auf true oder false gesetzt wird. b) Die Markierung der CheckBox Aufschrift soll entscheiden, welchen von zwei beliebigen Texten button1 als Aufschrift erhält. c) Die Markierung der CheckBox show/hide soll entscheiden, ob button1 angezeigt wird oder nicht. d) Die Markierung der RadioButtons soll die Hintergrundfarbe der ersten CheckBox festlegen. e) Beim Anklicken des ersten RadioButtons soll die Hintergrundfarbe der ersten CheckBox auf rot gesetzt werden.
2.8 Container-Komponenten: GroupBox, Panel, TabControl Mit einer GroupBox (Toolbox Registerkarte Container) kann man Komponenten auf einem Formular durch einen Rahmen und eine Überschrift (Eigenschaft Text) optisch zu einer Gruppe zusammenfassen. Eine solche Zusammenfassung von Komponenten, die inhaltlich zusammengehören, ermöglicht vor allem die übersichtliche Gestaltung von Formularen. Beispiel: GroupBoxen unter Extras|Optionen|Umgebung|Allgemein:
Die Zugehörigkeit einer Komponente zu einer GroupBox erreicht man, indem man sie mit der Maus auf die GroupBox zieht. Ähnlich wie mit einer GroupBox kann man auch mit einem Panel Komponenten gruppieren. Im Unterschied zu einer GroupBox verfügt ein Panel über keine Beschriftung. Stattdessen kann über die Eigenschaft BorderStyle sein Rand gestaltet werden. Mit dem Wert None, ist das Panel zur Laufzeit optisch nicht erkennbar. Mit einem Panel können Komponenten in einer Gruppe zusammengefasst (und damit gemeinsam verschoben) werden, auch ohne dass die Gruppe als solche erkennbar ist.
2.8 Container-Komponenten: GroupBox, Panel, TabControl
73
Ein TabControl stellt Registerkarten dar, die auch als Seiten bezeichnet werden. Die einzelnen Register sind Komponenten des Datentyps TabPage, dessen Eigenschaft Text die Aufschrift auf der Registerlasche ist. Solche Seiten können beliebige Steuerelemente enthalten. Beispiel: Windows verwendet ein TabControl für die Systemeigenschaften:
Neue Seiten fügt man während der Entwurfszeit über die Option Neue Registerkarte im Kontextmenü des TabControl hinzu. Ein TabControl soll oft das gesamte Formular ausfüllen. Das erreicht man mit dem Wert Fill ihrer Eigenschaft Dock. Zur Anzeige einer Seite im Eigenschaftenfenster klickt man zuerst die Registerlasche und dann die Seite an, oder man klickt die Seite in der Dokumentgliederung (Ansicht|Weitere Fenster) an:
Die Dokumentgliederung zeigt die Komponenten eines Formulars in ihrer hierarchischen Ordnung an. Hier kann man auch verdeckte Elemente für das Eigenschaftenfenster auswählen, verschieben usw. Komponenten, die andere Komponenten enthalten können, nennt man auch Container-Komponenten. Neben den in diesem Abschnitt vorgestellten Komponenten sind auch Formulare Container-Komponenten. Die Zugehörigkeit von Komponenten zu einer Container-Komponenten wirkt sich nicht nur optisch aus: – Verschiebt man eine Container-Komponente, werden alle ihre Komponenten mit verschoben (d.h. ihre Position innerhalb des Containers bleibt unverändert). Das kann die Gestaltung von Formularen zur Entwurfszeit erleichtern. – Bei RadioButtons wirkt sich die gegenseitige Deaktivierung nur auf die RadioButtons in derselben Container-Komponente aus. Das Anklicken eines RadioButtons in einer GroupBox wirkt sich nicht auf die RadioButtons in einer anderen Gruppe aus. So können mehrere Gruppen von sich gegenseitig ausschließenden Auswahloptionen auf einem Formular untergebracht werden.
74
2 Steuerelemente für die Benutzeroberfläche
– Die Eigenschaft Location für die Position einer Komponente bezieht sich immer auf den Container, in dem sie enthalten sind. – Die Eigenschaften Enabled und Visible des Containers wirken sich auf diese Eigenschaften der Elemente aus. Aufgabe 2.8 Ein Formular soll ein TabControl mit drei Registerkarten enthalten, das das ganze Formular ausfüllt. Die Registerkarten sollen den einzelnen Teilaufgaben dieser Aufgabe entsprechen und die Aufschriften „a)“, „b)“ und „c)“ haben.
a) Die Seite „a)“ soll zwei Gruppen von sich gegenseitig ausschließenden Optionen enthalten. Die Buttons OK, Hilfe und Abbruch sollen zu einer Gruppe zusammengefasst werden, die optisch nicht erkennbar ist. Reaktionen auf das Anklicken der Buttons brauchen nicht definiert werden. b) Die Seite „b)“ soll nur einen Button enthalten. c) Die Seite „c)“ soll leer sein. d) Verwenden Sie die Dokumentgliederung, um einen Button von Seite „b)“ auf Seite „c)“ zu verschieben.
2.9 Hauptmenüs und Kontextmenüs Unter Windows werden einem Anwender die verfügbaren Befehle und Optionen oft in Form von Menüs angeboten. Ein Menü wird nach dem Anklicken eines Eintrags in der Menüleiste (unterhalb der Titelzeile des Programms, typische Einträge „Datei“, „Bearbeiten“ usw.) aufgeklappt und enthält Menüeinträge wie z.B. „Neu“, „Öffnen“ usw. Gut gestaltete Menüs sind übersichtlich gegliedert und ermöglichen dem Anwender, eine gewünschte Funktion schnell zu finden. 2.9.1 Hauptmenüs und der Menüdesigner Die Komponente MenuStrip (Toolbox Registerkarte „Menüs & Symbolleisten“) stellt ein Hauptmenü zur Verfügung, das unter der Titelzeile des
2.9 Hauptmenüs und Kontextmenüs
75
Formulars angezeigt wird. Sie ersetzt und erweitert die Funktionalität der MainMenu Komponente aus älteren Versionen von Visual Studio. Ein MenuStrip wird wie jede andere Komponente ausgewählt, d.h. zuerst in der Toolbox angeklickt und dann durch einen Klick auf das Formular gesetzt. Dabei ist die Position auf dem Formular ohne Bedeutung: Zur Laufzeit wird das Menü immer unterhalb der Titelzeile des Formulars und zur Entwurfszeit in einem Bereich unterhalb des Formulars, der als Komponentenfach (engl.: component tray) bezeichnet wird. In diesem Komponentenfach werden alle Komponenten dargestellt, die zur Entwurfszeit keine Benutzeroberfläche haben. Durch einen Klick auf das Menü im Formular oder auf menuStrip1 wird dann der Menüdesigner aufgerufen, mit dem man das Menü gestalten kann:
Dazu trägt man in die mit „Hier eingeben“ gekennzeichneten Felder die Menüeinträge so ein, wie man sie im laufenden Programm haben möchte. Mit den Pfeiltasten oder der Maus kann man die Menüeinträge nachträglich auch noch ändern. Fährt man mit der Maus über ein „Hier eingeben“ Feld, wird rechts kleines schwarzes Dreieck angezeigt, über das man auch eine ComboBox oder eine TextBox in das Menü aufnehmen kann:
Während man diese Einträge macht, kann man im Eigenschaftenfenster sehen, dass jeder Menüeintrag den Datentyp ToolStripMenuItem hat. Der im Menü angezeigte Text ist der Wert ihrer Eigenschaft Text.
76
2 Steuerelemente für die Benutzeroberfläche
Die folgenden Optionen werden in vielen Menüs verwendet: – Durch das Zeichen & („kaufmännisches Und“, Umschalt+6) vor einem Buchstaben der Eigenschaft Text wird dieser Buchstabe zu einer Zugriffstaste. Er wird dann im Menü unterstrichen angezeigt. Die entsprechende Option kann dann zur Laufzeit durch Drücken der Alt-Taste mit diesem Buchstaben aktiviert werden. – Über die Eigenschaft ShortcutKeys kann man im Eigenschaftenfenster Tastenkürzel definieren, die eine Menüoption auch ohne die Alt-Taste aktivieren. – Der Text „-“ wird im Menü als Trennlinie dargestellt. – Verschachtelte Untermenüs erhält man durch Menüeinträge rechts von einem Menüeintrag. Durch einen Doppelklick auf einen Menüeintrag im Menüdesigner (bzw. auf das Ereignis Click des Menüeintrags im Eigenschaftenfenster) erzeugt Visual Studio die Ereignisbehandlungsroutine für das Ereignis Click dieses Menüeintrags. Diese Funktion wird zur Laufzeit beim Anklicken dieses Eintrags aufgerufen: private: System::Void speichernunterToolStripMenuItem_Click (System::Object^ sender, System::EventArgs^ e) { }
Während der Name einer Ereignisbehandlungsroutine bei allen bisherigen Komponenten aus ihrem Namen abgeleitet wurde (z.B. button1_Click), wird er bei einem Menüeintrag aus dem Wert der Eigenschaft Text erzeugt. Da der Text kein zulässiger C++-Name sein muss, werden unzulässige Zeichen (Sonderzeichen, Leerzeichen usw.) entfernt. Deshalb fehlt beim Menüeintrag zu „Speichern unter“ das Leerzeichen. Zwischen die geschweiften Klammern schreibt man dann die Anweisungen, die beim Anklicken des Menüeintrags ausgeführt werden sollen. Das sind oft Aufrufe von Standarddialogen, die im nächsten Abschnitt vorgestellt werden. Zwei weitere Möglichkeiten, die manchmal nützlich sind: – Über das Kontextmenü eines Menüeintrags im Menüdesigner kann man dem Menüeintrag ein Bild zuweisen („Bild festlegen“).
2.9 Hauptmenüs und Kontextmenüs
77
– Über den Eintrag „Standardelemente einfügen“ des Kontextmenüs der Menüleiste bzw. von menuStrip1 kann man die üblichen Menüeinträge (Datei, Bearbeiten usw.) einschließlich Bildern, ShortCuts usw. einfügen. 2.9.2 Kontextmenüs Ein Kontextmenü ist ein Menü, das einem Steuerelement zugeordnet ist und angezeigt wird, wenn man das Steuerelement mit der rechten Maustaste anklickt. Kontextmenüs werden auch als „lokale Menüs“ bezeichnet. Kontextmenüs werden über die Komponente ContextMenuStrip (Toolbox Registerkarte „Menüs & Symbolleisten“) zur Verfügung gestellt. Sie ersetzt und erweitert die Funktionalität der ContextMenu Komponente aus älteren Versionen von Visual Studio. Ein ContextMenuStrip wird wie ein MenuStrip auf ein Formular gesetzt und mit dem Menüdesigner gestaltet. Die Zuordnung eines Kontextmenüs zu der Komponente, bei der es angezeigt werden soll, erfolgt über die Eigenschaft ContextMenuStrip der Komponente. Jede Komponente, der ein Kontextmenü zugeordnet werden kann, hat diese Eigenschaft. Die Zuordnung kann im Eigenschaftenfenster erfolgen: Im PulldownMenü der Eigenschaft ContextMenuStrip kann man alle bisher auf das Formular gesetzten Kontextmenüs auswählen. In der Abbildung rechts wird also der Textbox textBox1 das Kontextmenü contextMenuStrip1 zugeordnet. Die Eigenschaft ContextMenuStrip kann nicht nur während der Entwurfszeit im Eigenschaftenfenster, sondern auch während der Laufzeit des Programms zugewiesen werden: if (checkBox1->Checked) ContextMenuStrip = contextMenuStrip1; else ContextMenuStrip = contextMenuStrip2;
Kontextmenüs bieten oft dieselben Funktionen wie Hauptmenüs an. Dann muss die entsprechende Ereignisbehandlungsroutine kein zweites Mal geschrieben werden, sondern kann im Objektinspektor ausgewählt werden (siehe Abbildung rechts).
78
2 Steuerelemente für die Benutzeroberfläche
2.10 Standarddialoge Die Register Dialogfelder und Drucken der Toolbox enthalten Komponenten für die Standarddialoge unter Windows: („common dialog boxes“).
Diese Dialoge werden von Windows zur Verfügung gestellt, damit häufig wiederkehrende Aufgaben wie die Eingabe eines Dateinamens in verschiedenen Anwendungen auf dieselbe Art erfolgen können. Beispiel: Durch einen OpenFileDialog erhält man das üblicherweise zum Öffnen von Dateien verwendete Dialogfenster:
Ein OpenFileDialog wird im Unterschied zu vielen anderen Steuerelementen (z.B. Buttons) nicht automatisch nach dem Start des Programms angezeigt, sondern erst durch einen Aufruf seiner Methode ShowDialog: DialogResult ShowDialog(); Beispiel: Einen OpenFileDialog ruft man meist in der Ereignisbehandlungsroutine zur Menüoption Datei|Öffnen auf. Die folgenden Anweisungen setzen voraus, dass zuvor ein OpenFileDialog aus der Toolbox auf das Formular gezogen wurde:
2.10 Standarddialoge
79
private: System::Void öffnenToolStripMenuItem_Click (System::Object^ sender, System::EventArgs^ e) { openFileDialog1->ShowDialog(); }
Beim Anklicken des Öffnen Buttons eines OpenFileDialog tritt das Ereignis FileOk ein. Die Ereignisbehandlungsroutine für dieses Ereignis erhält man durch einen Doppelklick auf den Dialog im Formulardesigner. In diese Funktion nimmt man dann die Anweisungen auf, die beim Anklicken des Öffnen Buttons ausgeführt werden sollen. Die Benutzereingaben im OpenFileDialog stehen als Werte von Eigenschaften zur Verfügung. Der ausgewählte Dateiname ist der Wert der Eigenschaft property String^ FileName Beispiel: In der folgenden FileOk-Funktion wird der Eigenschaft Text einer TextBox der Inhalt der Datei (Dateiname openFileDialog1->FileName) zugewiesen, die in dem OpenFileDialog ausgewählt wurde: private: System::Void openFileDialog1_FileOk( System::Object^ sender, System::ComponentModel::CancelEventArgs^ e) { // zur Funktion ReadAllText: siehe Aufgabe 2.10 textBox1->Text=IO::File::ReadAllText( openFileDialog1->FileName); }
Die Standarddialoge können vor ihrem Aufruf über zahlreiche Eigenschaften konfiguriert werden. Bei einem Open- und SaveFileDialog sind das unter anderem: property String^ Filter // Maske für Dateinamen property String^ InitialDirectory // das beim Aufruf angezeigte Verzeichnis Bei der Eigenschaft Filter gibt man keinen, einen oder mehrere Filter an. Jeder Filter besteht aus Text, der im Dialog nach „Dateityp“ angezeigt wird, einem senkrechten Strich „|“ (Alt Gr InitialDirectory= "c:\\Projekt1\\Projekt1"; openFileDialog1->Filter="C++ Dateien|*.CPP;*.H";
Da das Zeichen „\“ in C++ bei Strings im Quelltext eine besondere Bedeutung hat (Escape-Sequenz, siehe Abschnitt 3.3.6), wurde es in diesem Beispiel doppelt angegeben. Anstelle von „\\“ ist bei Pfadangaben in Windows auch „/“ möglich. Beispiel: Die nächsten beiden Pfadangaben sind gleichwertig: openFileDialog1->InitialDirectory = "c:\\Projekt1\\Projekt1"; openFileDialog1->InitialDirectory = "c:/Projekt1/Projekt1";
Die Standarddialoge des Registers „Dialogfelder“ der Toolbox: OpenFileDialog
SaveFileDialog
FolderBrowserDialog
FontDialog
ColorDialog
Zeigt Dateien aus einem Verzeichnis an und ermöglicht, eine auszuwählen oder einzugeben (Eigenschaft FileName), die geöffnet werden soll. Um den Namen auszuwählen oder einzugeben, unter dem eine Datei gespeichert werden soll. Viele gemeinsame Eigenschaften mit OpenFileDialog, z.B. FileName, InitialDirectory, Filter usw. Ähnlich wie ein OpenFileDialog, zur Auswahl eines Verzeichnisses. Das ausgewählte Verzeichnis ist der Wert der Eigenschaft SelectedPath. Zeigt die verfügbaren Schriftarten und ihre Attribute an und ermöglicht, eine auszuwählen (Eigenschaft Font). Zeigt die verfügbaren Farben an. Die hier ausgewählte Farbe ist der Wert der Eigenschaft Color.
Alle diese Dialoge, wie auch die des Registers „Drucken“, werden wie ein OpenFileDialog durch den Aufruf der Methode DialogResult ShowDialog(); angezeigt. Der Rückgabewert ist DialogResult::Cancel, wenn der Dialog abgebrochen wurde (z.B. mit dem „Abbrechen“ Button oder der ESC-Taste), und andernfalls DialogResult::OK (z.B. mit dem Button „Öffnen“ oder der ENTERTaste). Im letzteren Fall findet man die Benutzereingaben aus dem Dialogfenster in entsprechenden Eigenschaften des Dialogs. Beispiel: Bei allen Dialogen (auch bei einem Open- oder SaveFileDialog) kann man durch die Abfrage des Rückgabewerts von ShowDialog prüfen, ob der Dialog mit Öffnen abgeschlossen wurde. Die Benutzereingaben werden nur nach einer Bestätigung des Dialogs verwendet.
2.10 Standarddialoge
81
private: System::Void öffnenToolStripMenuItem_Click (System::Object^ sender, System::EventArgs^ e) { if (openFileDialog1->ShowDialog()== System::Windows::Forms::DialogResult::OK) { // der Dialog wurde bestätigt textBox1->Text=IO::File::ReadAllText( openFileDialog1->FileName); } }
Hier muss leider der relativ umständliche Ausdruck System::Windows::Forms::DialogResult::OK verwendet werden, da der Ausdruck DialogResult::OK mit einem Element der Formularklasse in Konflikt kommt. Die Funktion in diesem Beispiel zeigt ein universell verwendbares Programmschema für Dialog-Aufrufe und die Verwendung der Daten aus dem Dialog: Falls ein Dialog ein FileOk-Ereignis hat, ist die Verwendung dieses Ereignisses aber meist einfacher als eine solche Abfrage. Dann braucht man beim Anklicken einer Menüoption oder eines Symbols auf einer Symbolleiste nur noch ShowDialog aufrufen. Falls ein solcher Dialog an verschiedenen Stellen im Programm aufgerufen werden soll, ist damit sichergestellt, dass jedes Mal dieselben Anweisungen ausgeführt werden. Aufgabe 2.10 Schreiben Sie ein Programm, das einige Funktionen eines Editors bereitstellt. Verwenden Sie dazu eine mehrzeilige TextBox, die den gesamten Client-Bereich des Formulars ausfüllt (Eigenschaft Dock=Fill). Ein Hauptmenü, soll die folgenden Optionen anbieten. Falls die geforderten Anweisungen bisher nicht vorgestellt wurden, informieren Sie sich in der Online-Hilfe darüber (z.B. SelectAll). – Datei|Öffnen: Ein OpenFileDialog soll alle Dateien anzeigen und die ausgewählte Datei in die TextBox einlesen. Sie können dazu die Methode der Klasse IO::File static String^ ReadAllText(String^ path) verwenden und ihren Rückgabewert (der Inhalt der Datei als String) der Eigenschaft Text der TextBox zuweisen wie in textBox1->Text=IO::File::ReadAllText("c:\\Text.txt");
– Datei|Speichern: Der Text der TextBox soll unter dem Namen als Datei gespeichert werden, der in einem SaveFileDialog ausgewählt wurde. Sie können dazu die Methode der Klasse IO::File static void WriteAllText(String^ path, String^ contents) verwenden wie in
82
2 Steuerelemente für die Benutzeroberfläche IO::File::WriteAllText("c:\\Text.txt",textBox1->Text);
– Datei|Drucken. Ein PrintDialog ohne weitere Aktion. Mit den bisher vorgestellten Sprachelementen ist es noch nicht möglich, den Inhalt der TextBox auszudrucken. – Nach einem Trennstrich: Datei|Schließen: Beendet das Programm – – – –
Bearbeiten|Alles Markieren: Ein Aufruf von textBox1->SelectAll. Bearbeiten|Ausschneiden: Ein Aufruf von textBox1->Cut. Bearbeiten|Einfügen: Ein Aufruf von textBox1->Paste. Bearbeiten|Kopieren: Ein Aufruf von textBox1->Copy.
Durch Drücken der rechten Maustaste in der TextBox soll ein Kontextmenü aufgerufen werden, das die Optionen Farben und Schriftart anbietet. – Kontext-Menü|Farben: Die ausgewählte Farbe (Eigenschaft Color des ColorDialogs) soll der Eigenschaft BackColor der TextBox zugewiesen werden. – Kontext-Menü|Schriftart: Die ausgewählte Schriftart (Eigenschaft Font des FontDialogs) soll der Eigenschaft Font der TextBox zugewiesen werden.
2.11 Einfache Meldungen mit MessageBox::Show anzeigen Die Klasse MessageBox enthält über 20 verschiedene Funktionen mit dem Namen Show, die ein Fenster mit einem Text und eventuell noch weiteren Buttons, Icons usw. anzeigen. In der Version mit einem String^-Parameter wird der als Argument übergebene Text angezeigt: static DialogResult Show(String^ text); Beispiel: Das rechts abgebildete Fenster erhält man durch MessageBox::Show("hello, world");
Als zweites Argument kann man auch noch eine Titelzeile übergeben: static DialogResult Show(String^ text,String^ caption); Über MessageBoxButtons static DialogResult Show(String^ text, String^ caption, MessageBoxButtons buttons);
2.12 Eine Vorlage für viele Projekte und Übungsaufgaben
83
kann man die angezeigten Buttons festlegen: AbortRetryIgnore: für die Buttons „Abbrechen“, „Wiederholen“, „Ignorieren“ OK: Ein „OK“ Button OKCancel: für die Buttons „OK“ und „Abbrechen“ RetryCancel: für die Buttons „Wiederholen” und “Abbrechen” YesNo: für die Buttons „Ja” und „Nein“ YesNoCancel: für die Buttons „Ja”, „Nein“ und „Abbrechen“. Für die zahlreichen weiteren Varianten wird auf die Online-Hilfe (siehe Abschnitt 1.7) verwiesen. Alle diese Funktionen geben ein Ergebnis vom Typ DialogResult zurück. Es entspricht dem Button, mit dem der Dialog geschlossen wurde: Abort: für den „Abbrechen“-Button Cancel: für den „Abbrechen“-Button Ignore: für den „Ignorieren“-Button No: für den „Nein“-Button OK: für den „OK“-Button Retry: für den „Wiederholen“-Button None: der Dialog wurde noch nicht geschlossen und wird weiterhin angezeigt.
2.12 Eine Vorlage für viele Projekte und Übungsaufgaben Im Folgenden wird eine einfache Vorlage für eine Windows Forms-Anwendung vorgestellt, die für viele Projekte und insbesondere für die Lösungen aller Übungsaufgaben in diesem Buch verwendet werden kann. Obwohl im Einzelfall oft Vereinfachungen möglich oder Erweiterungen notwendig sind, können grundsätzlich alle Lösungen nach diesem Schema aufgebaut werden. 1. Mit Datei|Neu|Projekt|CLR|Windows Forms-Anwendung ein neues Projekt anlegen. Bei den folgenden Beispielen wird ein Projekt mit dem Namen MeinProjekt angenommen. 2. Das Formular wird dann so gestaltet, dass es alle Steuerelemente enthält, die für die Ein- und Ausgabe von Informationen und den Start von Aktionen notwendig sind. Dazu zieht man entsprechende Steuerelemente aus der Toolbox auf das Formular. Für viele Übungsaufgaben reichen die folgenden Steuerelemente aus: – Eine einzeilige TextBox zur Eingabe von Daten – Eine mehrzeilige TextBox (siehe Abschnitt 2.4.2) zur Anzeige der Ergebnisse. – Ein oder mehrere Buttons (bzw. Menüoptionen usw.) zum Start der Anweisungen
84
2 Steuerelemente für die Benutzeroberfläche
3. Die Funktionen, Deklarationen, Klassen usw. kommen in eine eigene Datei, die dem Projekt mit Projekt|Neues Element hinzufügen|Visual C++|Code als Headerdatei(.h) mit einem passenden Namen (z.B. Aufgabe_2_12.h) hinzugefügt wird. Diese Datei wird dann vor dem namespace des Projekts mit einer #include-Anweisung in die Formulardatei (z.B. Form1.h) aufgenommen: #pragma once #include "Aufgabe_2_12.h" // AppendText("hello world \r\n"); }
Zur Formatierung der ausgegebenen Werte kann String::Format verwendet werden: void MeineLoesung_1(TextBox^ tb) { tb->AppendText("Lösung der Aufgabe 3.17: \r\n"); int x=17; tb->AppendText( String::Format("x={0} y={1}\r\n",x,f(x))); } // f(x) wird in Aufgabe 2.12 beschrieben
2.12 Eine Vorlage für viele Projekte und Übungsaufgaben
85
Für jede solche Funktion wird ein Button auf das Formular gesetzt und mit einem passenden Namen (Eigenschaft Name) und einer passenden Aufschrift (Eigenschaft Text) versehen. In der zugehörigen Ereignisbehandlungsroutine wird dann diese Funktion aufgerufen und die TextBox auf dem Formular (z.B. textBox1) als Argument übergeben: Beispiel: Für ein Formular mit einer TextBox textBox1 kann die Funktion aus dem Beispiel oben folgendermaßen aufgerufen werden: private: System::Void Aufgabe_2_12_a_Click( System::Object^ sender, System::EventArgs^ e) { MeineLoesung_1(textBox1); }
Bei diesem Aufruf werden dann alle Ausgaben in die als Argument übergebene TextBox textBox1 geschrieben. Beachten Sie, dass Sie bei der Definition der Funktion den Namen des Steuerelement-Datentyps verwenden (also z.B. TextBox oder ListBox), und beim Aufruf der Funktion den Namen des Steuerelements auf dem Formular (also z.B. textBox1 oder listBox1). 5. Benutzereingaben erfolgen über einzeilige TextBoxen, deren Eigenschaft Text (Datentyp String) mit einer Convert::-Funktion in den benötigten Datentyp konvertiert wird. Beispiel: Für eine Funktion mit einem int-Parameter void MeineLoesung_2(TextBox^ tb, int x) { tb->AppendText("Hallo Welt: \r\n"); tb->AppendText( String::Format("x={0} f(x)={1} \r\n",x,f(x)) ); }
kann der Text aus einer TextBox folgendermaßen in einen int-Wert konvertiert und an die Funktion übergeben werden: private: System::Void Aufgabe_2_12_b_Click( System::Object^ sender, System::EventArgs^ e) { int i=Convert::ToInt32(textBox2->Text); MeineLoesung_2(textBox1, i); }
Im Prinzip hat die #include-Anweisung (siehe Abschnitt 3.22.1) der Header-Datei von 3. denselben Effekt, wie wenn man die Anweisungen der Header-Datei an der Stelle der #include-Anweisung in das Programm aufnimmt. Das legt die Vereinfachung nahe, auf die extra Header-Datei zu verzichten und ihre Anweisungen
86
2 Steuerelemente für die Benutzeroberfläche
a) an der Stelle der #include-Anweisung ins Programm zu schreiben, oder b) innerhalb des namespace nach „using namespace System::Windows::Forms;“ aufzunehmen und so die Anweisungen using namespace System von 3. überflüssig zu machen, oder c) innerhalb der Formularklasse public ref class Form1 vor der Funktion button_Click ins Programm zu schreiben. Diese Vereinfachungen funktionieren bei vielen Projekten, aber nicht bei allen: a) Da der Windows-Formulardesigner keine anderen Klassen vor der Formularklasse Form1 mag, wird das Formular eventuell nicht mehr richtig angezeigt, wenn man davor eine eigene Klasse definiert. b) Innerhalb des namespace oder der Formularklasse kann man keine Bibliotheken der C++-Standardbibliothek (z.B. mit #include ) aufnehmen. c) Falls innerhalb der Formularklasse Variablen, Funktionen oder Klassen definiert werden, sind das Datenelemente, Elementfunktionen oder verschachtelte Klassen der Formularklasse. Diese haben zwar viele Gemeinsamkeiten mit gewöhnlichen Variablen, Funktionen und Klassen, aber auch einige diffizile Unterschiede. Da die Formularklasse eine Verweisklasse (ref class) ist, gelten weitere Besonderheiten. Damit diese Projektvorlage für möglichst viele Aufgaben funktioniert, wird auf diese Vereinfachungen verzichtet. Natürlich ist die Auslagerung der eigenen Anweisungen in eine extra Datei (wie in 3.) und der Zugriff auf die Steuerelemente über Funktionsparameter etwas umständlich: Sie führt aber zu übersichtlicheren Programmen als wenn alle Anweisungen in der Formulardatei innerhalb der Klasse Form1 stehen. Das erspart viele Programmierfehler, die zu undurchsichtigen Fehlermeldungen führen, und erleichtert die Suche nach Fehlern. Diese Vorlage kann außerdem als Vorlage für die Portierung von Konsolen-Anwendungen auf Formularanwendungen verwendet werden. Falls Sie ein Konsolenprogramm wie in der linken Spalte haben, können Sie es mit relativ wenig Aufwand in eine Formular-Anwendung portieren, indem Sie die I/O-Anweisungen ersetzen und die Funktionen als Reaktion auf einen ButtonClick usw. aufrufen: #include using namespace std; void f(int x) { coutAppendText(String::Format("k={0}\r\n",k)); int i; textBox1->AppendText(String::Format("i={0}\r\n",i)); };
3.3 Ganzzahldatentypen Variablen, deren Datentyp ein Ganzzahldatentyp ist, können ganzzahlige Werte darstellen. Je nach Datentyp können dies ausschließlich positive Werte oder positive und negative Werte sein. Der Bereich der darstellbaren Werte hängt dabei davon ab, wie viele Bytes der Compiler für eine Variable des Datentyps reserviert und wie er diese interpretiert. In C++ gibt es die folgenden Ganzzahldatentypen:
3.3 Ganzzahldatentypen
Datentyp
signed char char (Voreinstellung) unsigned char short int short signed short signed short int unsigned short int unsigned short wchar_t int signed signed int long int long signed long signed long int unsigned int unsigned unsigned long int unsigned long long long (gehört nicht zum C++-Standard 2003) unsigned long long (wie long long) bool
99
Wertebereich in Visual C++ (32 bit) –128 .. 127
Datenformat
0 .. 255 –32768 .. 32767
8 bit ohne Vorzeichen 16 bit mit Vorzeichen
0 .. 65535
16 bit ohne Vorzeichen
8 bit mit Vorzeichen
32 bit mit Vorzeichen –2,147,483,648.. 2,147,483,647
0 .. 4,294,967,295
32 bit ohne Vorzeichen
–9223372036854775808 ..9223372036854775807
64 bit mit Vorzeichen
0 .. 18446744073709551615
64 bit ohne Vorzeichen
true, false
Wie diese Tabelle zeigt, gibt es für die meisten Datenformate verschiedene Namen. Ein fett gedruckter Name steht dabei für denselben Datentyp wie die darauf folgenden nicht fett gedruckten Namen. So sind z.B. char, signed char und unsigned char drei verschiedene Datentypen. Dagegen sind signed und signed int alternative Namen für den Datentyp int. Dass diese Namen unterschiedliche Datentypen sind, ist aber außer in Zusammenhang mit überladenen Funktionen kaum von Bedeutung. Die long long Datentypen gehören nicht zum C++-Standard 2003, werden aber voraussichtlich in die nächste Version des Standards übernommen Der C++-Standard legt explizit nicht fest, welchen Wertebereich ein bestimmter Ganzzahldatentyp darstellen können muss. Es wird lediglich verlangt, dass der Wertebereich eines Datentyps, der in der Liste signed char, signed short, int, long int
100
3 Elementare Datentypen und Anweisungen
rechts von einem anderen steht, nicht kleiner ist als der eines Datentyps links davon. Deswegen können verschiedene Compiler verschiedene Formate für den Datentyp int verwenden: Bei Compilern für 16-bit-Systeme werden häufig 16 Bits für den Datentyp int verwendet und bei Compilern für 32-bit-Systeme 32 Bits. Außerdem gilt: – Wenn T für einen der Datentypen char, int, short oder long steht, dann belegen Variablen der Datentypen T, signed T und unsigned T dieselbe Anzahl von Bytes. – Bei allen Datentypen außer char sind die Datentypen T und signed T gleich. – char, signed char und unsigned char sind drei verschiedene Datentypen, die alle jeweils ein Byte belegen. Der Datentyp char hat entweder das Datenformat von signed char oder das von unsigned char. Welches der beiden Datenformate verwendet wird, kann bei verschiedenen Compilern verschieden sein. In Abschnitt 3.3.6 wird gezeigt, wie diese Voreinstellung geändert werden kann. Die Datentypen signed char, short int, int, long int usw. werden unter dem Oberbegriff Ganzzahldatentyp mit Vorzeichen zusammengefasst. Ein Ganzzahldatentyp ohne Vorzeichen ist einer der Datentypen unsigned char, unsigned short int, unsigned int, unsigned long usw. Die Ganzzahldatentypen mit und ohne Vorzeichen sind zusammen mit den Datentypen bool, char und wchar_t die Ganzzahldatentypen. Der Standard für die Programmiersprache C verlangt, dass die Wertebereiche in einer Datei dokumentiert werden, die man mit „#include “ erhält“. Da der C-Standard auch weitgehend in den C++-Standard übernommen wurde, gilt das auch für C++. Bei dem folgenden Auszug aus „include\limits.h“ wurde das Layout etwas überarbeitet: #define #define #define #define #define #define #define #define #define #define #define #define
CHAR_BIT 8 // number of bits in a char SCHAR_MIN (–128) // minimum signed char value SCHAR_MAX 127 // maximum signed char value UCHAR_MAX 255 // maximum unsigned char value SHRT_MIN (–32767–1)// minimum signed short value SHRT_MAX 32767 // maximum signed short value USHRT_MAX 65535U // maximum unsigned short value LONG_MIN (–2147483647L–1)//minimum signed long .. LONG_MAX 2147483647L // maximum signed long value ULONG_MAX 4294967295UL//maximum unsigned long .. INT_MIN LONG_MIN // minimum signed int value INT_MAX LONG_MAX // maximum signed int value
Beispiel: Die Konstanten aus dieser Datei können nach #include
verwendet werden: textBox1->AppendText(String::Format( "Min={0},Max={1}", INT_MIN,INT_MAX);
3.3 Ganzzahldatentypen
101
In Standard-C++ sind diese und weitere Grenzen außerdem im Klassen-Template numeric_limits definiert. Es steht zur Verfügung nach #include using namespace std;
Auch wenn bisher noch nichts über Klassen und Templates gesagt wurde, soll mit den folgenden Beispielen gezeigt werden, wie man auf die Informationen in diesen Klassen zugreifen kann: int i1 = numeric_limits::min(); //–2147483648 int i2 = numeric_limits::max(); // 2147483647
Natürlich erhält man hier keine anderen Werte als mit den Konstanten aus „limits.h“. Und die etwas längeren Namen wirken zusammen mit der für Anfänger vermutlich ungewohnten Syntax auf den ersten Blick vielleicht etwas abschreckend. Allerdings sind hier alle Namen nach einem durchgängigen Schema aufgebaut, im Unterschied zu den teilweise etwas kryptischen Abkürzungen in „limits.h“. Die minimalen und maximalen Werte für den Datentyp char erhält man, indem man im letzten Beispiel einfach nur int durch char ersetzt: int i3 = numeric_limits::min(); //–128 int i4 = numeric_limits::max(); // 127
Weitere Informationen zu dieser Klasse findet man in der Online-Hilfe. 3.3.1 Die interne Darstellung von Ganzzahlwerten Die meisten Prozessoren verwenden für die interne Darstellung von Werten eines Ganzzahldatentyps ohne Vorzeichen das Binärsystem. Dabei entspricht jedem Wert im Wertebereich ein eindeutiges Bitmuster. Beispiel: Das Bitmuster für Werte des Datentyps unsigned char (8 Bits): Zahl z10 0 1 2 3 ... 254 255
Binärdarstellung mit 8 Bits 0000 0000 0000 0001 0000 0010 0000 0011 ... 1111 1110 1111 1111
Zwischen den einzelnen Bits b7b6b5b4b3b2b1b0 und der durch sie im Dezimalsystem dargestellten Zahl z10 besteht dabei die folgende Beziehung: z10 = b7*27 + b6*26 + b5*25 + b4*24 + b3*23 + b2*22 + b1*21 + b0*20
102
3 Elementare Datentypen und Anweisungen
Beispiel: 2510
= 0*27 + 0*26 + 0*25 + 1*24 + 1*23 + 0*22 + 0*21 + 1*20 = 000110012
Hier ist die jeweilige Basis durch einen tiefer gestellten Index dargestellt: 2510 ist eine Zahl im Dezimalsystem, 000110012 eine im Binärsystem. Bei der Darstellung einer Zahl z durch Ziffern ..z3z2z1z0 im Dezimalsystem wird ebenfalls ein Stellenwertsystem verwendet, nur mit dem Unterschied, dass als Basis die Zahl 10 und nicht die Zahl 2 verwendet wird. Als Ziffern stehen die Zahlen 0 .. 9 zur Verfügung: z = ... z3*103 + z2*102 + z1*101 + z0*100 // zi: 0 .. 9 Beispiel: 2510 = 2*101 + 5*100 Offensichtlich kann eine ganze Zahl in einem beliebigen Zahlensystem zur Basis B mit B Ziffern 0 .. B–1 dargestellt werden: z = ... z3*B3 + z2*B2 + z1*B1 + z0*B0 // zi: 0 .. B–1 Beispiel: 1710 = 1*32 + 2*31 + 2*30 = 1223 1710 = 2*71 + 3*70 = 237 Zur übersichtlicheren Darstellung von Binärzahlen wird oft das Hexadezimalsystem (zur Basis 16) verwendet. Die 16 Ziffern im Hexadezimalsystem werden mit 0, 1, ..., 9, A, B, C, D, E, F bezeichnet: dezimal 0 1 2 3 4 5 6 7
dual 0000 0001 0010 0011 0100 0101 0110 0111
hexadezimal 0 1 2 3 4 5 6 7
dezimal 8 9 10 11 12 13 14 15
dual 1000 1001 1010 1011 1100 1101 1110 1111
hexadezimal 8 9 A B C D E F
Im Hexadezimalsystem können die 8 Bits eines Bytes zu 2 hexadezimalen Ziffern zusammengefasst werden, indem man die vordere und hintere Gruppe von 4 Bits einzeln als Hexadezimalziffer darstellt: Beispiel: 2510 = 0001 10012 = 1916 In C++ wird ein hexadezimaler Wert dadurch gekennzeichnet, dass man vor den hexadezimalen Ziffern die Zeichenfolge „0x“ angibt: int i=0x19; // gleichwertig mit i=25;
3.3 Ganzzahldatentypen
103
Bei Datentypen mit Vorzeichen werden mit n Bits die positiven Zahlen von 0 .. 2n–1–1 ebenfalls im Binärsystem dargestellt. Für negative Zahlen wird dagegen das sogenannte Zweierkomplement verwendet. Das Zweierkomplement beruht darauf, dass man zu einer Zahl, die im Speicher mit n Bits dargestellt wird, die Zahl 10...02 aus n Nullen und einer führenden 1 addieren kann, ohne dass sich diese Addition auf das Ergebnis auswirkt, da die 1 wegen der begrenzten Bitbreite ignoriert wird. Stellt man so die negative Zahl - 000110012
durch (1000000002 - 000110012)
dar, wird die Addition der negativen Zahl (Subtraktion) auf eine Addition des Zweierkomplements zurückgeführt. Das Zweierkomplement erhält man direkt durch die Subtraktion: 1000000002 -000110012 111001112
Einfacher erhält man es aus der Binärdarstellung, indem man jede 1 durch eine 0 und jede 0 durch eine 1 ersetzt (Einerkomplement) und zum Ergebnis 1 addiert. Beispiel:
2510 = Einerkomplement: + 1....... . Zweierkomplement:
000110012 11100110 1 11100111
Damit hat die Zahl –25 die Darstellung 11100111 Im Zweierkomplement zeigt also eine 1 im höchstwertigen Bit an, dass die Zahl negativ ist. Insbesondere wird die Zahl –1 im Zweierkomplement immer durch so viele Einsen dargestellt, wie Bits für die Darstellung der Zahl vorgesehen sind: –1 mit 8 Bits: –1 mit 16 Bits: –1 mit 32 Bits:
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
Berechnet man von einer negativen Zahl, die im Zweierkomplement dargestellt ist, wieder das Zweierkomplement, erhält man die entsprechende positive Zahl. Beispiel: 1. –2510 Einerkomplement: + 1 Zweierkomplement:
= 111001112 00011000 1 00011001
Das ist gerade die Zahl 25 im Binärsystem.
104
3 Elementare Datentypen und Anweisungen
2. Dem maximalen negativen Wert 100 .. 002 entspricht kein positiver Wert. Das Zweierkomplement ist wieder derselbe Wert. Wegen dieser verschiedenen Darstellungsformate kann dasselbe Bitmuster zwei verschiedene Werte darstellen – je nachdem, welches Datenformat verwendet wird. Zum Beispiel stellt das Bitmuster 11100111 für einen 8-bit-Datentyp ohne Vorzeichen den Wert 231 dar, während es für einen 8-bit-Datentyp mit Vorzeichen den Wert –25 darstellt. 3.3.2 Ganzzahlliterale und ihr Datentyp Eine Zeichenfolge, die einen Wert darstellt, bezeichnet man als Konstante oder als Literal. Beispielsweise ist die Zahl „20“ in i = 20;
ein solches Literal. In C++ gibt es die folgenden Ganzzahlliterale: integer-literal: decimal-literal integer-suffix opt octal-literal integer-suffix opt hexadecimal-literal integer-suffix opt
Die ersten Zeichen (von links) eines Literals entscheiden darüber, um welche Art von Literal es sich handelt: – Eine Folge von Dezimalziffern, die mit einer von Null verschiedenen Ziffer beginnt, ist ein Dezimalliteral (zur Basis 10): decimal-literal: nonzero-digit decimal-literal digit nonzero-digit: one of 1 2 3 4 5 6 7 8 9 digit: one of 0 1 2 3 4 5 6 7 8 9
– Eine Folge von Oktalziffern, die mit 0 (Null, nicht der Buchstabe „O“) beginnt, ist ein Oktalliteral (Basis 8). octal-literal: 0 octal-literal octal-digit octal-digit: one of 0 1 2 3 4 5 6 7
3.3 Ganzzahldatentypen
105
– Ein Folge von Hexadezimalziffern, die mit der Zeichenfolge „0x“ oder „0X“ (Null, nicht dem Buchstaben „O“) beginnt, ist ein hexadezimales Literal: hexadecimal-literal: 0x hexadecimal-digit 0X hexadecimal-digit hexadecimal-literal hexadecimal-digit hexadecimal-digit: one of 0 1 2 3 4 5 6 7 8 9 a b c d e f A B C D E F
Beispiele: int i=017; // dezimal 15 int j=0xf; // dezimal 15
Jedes Literal hat einen Datentyp. Dieser ergibt sich nach dem C++-Standard aus seinem Wert, seiner Form und einem optionalen Suffix. Ohne ein Suffix ist der Datentyp – eines Dezimalliterals der erste der Datentypen int oder long int, der den Wert darstellen kann. – eines Oktal- oder Hexadezimalliterals der erste der Datentypen int, unsigned int, long int oder unsigned long int, der den Wert darstellen kann. Falls der Wert nicht in einem dieser Datentypen dargestellt werden kann, ist sein Datentyp nicht durch den C++-Standard definiert. Der Datentyp eines Ganzzahlliterals kann durch ein Suffix beeinflusst werden: integer-suffix: unsigned-suffix long-suffix opt long-suffix unsigned-suffix opt unsigned-suffix: one of u U long-suffix: one of l L long-long-suffix: one of ll LL
Durch das Suffix „u“ oder „U“ erhält das Literal den Datentyp unsigned int oder unsigned long int, durch „l“ oder „L“ den Datentyp long int oder unsigned long int und durch LL long long. Werden diese beiden Suffixe kombiniert (ul, lu, uL, Lu, Ul, lU, UL, oder LU), hat das Literal immer den Datentyp unsigned long int. Beispiel: Da in 32-bit Visual C++ der Wertebereich von int und long int gleich ist, haben Dezimalliterale mit Werten im Bereich INT_MIN .. INT_MAX
106
3 Elementare Datentypen und Anweisungen
den Datentyp int. Die Datentypen der folgenden Literale sind als Kommentar angegeben: 13 013 0x13 17u 17uLL 0xflu
// // // //
Datentyp int, Wert 13 (dezimal) Datentyp int, Wert 11 (dezimal) Datentyp int, Wert 19 (dezimal) Datentyp unsigned int, Wert 17 (dezimal) // Datentyp unsigned long long, Wert 17 // Datentyp unsigned long, Wert 15 (dezimal)
Der Datentyp eines Dezimalliterals außerhalb des Wertebereichs von long int ist im C++Standard nicht definiert. In 32-bit Visual C++ hat 2147483648 (INT_MAX+1) den Datentyp unsigned long. Aufgaben 3.3.2 1. Stellen Sie mit 8 Bits a) die Zahl 37 im Binärsystem dar b) die Zahl -37 im Zweierkomplement dar c) die Zahlen 37 und -37 im Hexadezimalsystem dar. Führen Sie die folgenden Berechnungen im Binärsystem durch und geben Sie das Ergebnis im Dezimalsystem an: d) 37 – 25 // berechnen Sie 37 + (-25) e) 25 – 37 // berechnen Sie 25 + (-37) 2. Welchen Wert stellt das Bitmuster ab16 im Dezimalsystem dar, wenn es a) im Zweierkomplement interpretiert wird? b) im Binärsystem interpretiert wird? 3. Welche Werte werden durch die folgenden Anweisungen ausgegeben: textBox1->AppendText(String::Format("Vorwahl Berlin="+ " {0} b={1} c={2}",030,017+15,0x12+10));
3.3.3 Zuweisungen und Standardkonversionen bei Ganzzahlausdrücken Standardkonversionen sind Konversionen für die vordefinierten Datentypen, die der Compiler implizit (d.h. automatisch) durchführt. Sie sind im C++-Standard definiert sind und werden z.B. in den folgenden Situationen durchgeführt: – Wenn ein Ausdruck als Operand eines Operators verwendet wird (siehe Abschnitt 3.3.4). So wird z.B. bei der Zuweisung v=a der Ausdruck a in den Datentyp von v konvertiert. – Wenn beim Aufruf einer Funktion für einen Parameter eines Datentyps T1 ein Argument eines anderen Datentyps T2 eingesetzt wird.
3.3 Ganzzahldatentypen
107
Insbesondere sind Standardkonversionen für alle Ganzzahldatentypen definiert. Deshalb können in einer Zuweisung v=a beliebige Ganzzahldatentypen von a und v kombiniert werden. Falls dabei die Datentypen von v und a identisch sind, wird durch die Zuweisung einfach das Bitmuster von a an die Adresse von v kopiert, so dass der Wert von v mit dem von a identisch ist. Sind die beiden Datentypen dagegen verschieden, wird der Datentyp der rechten Seite durch eine der folgenden Konversion in den Datentyp der linken Seite konvertiert: 1. Ausdrücke der Datentypen bool, char, signed char, unsigned char, short int oder unsigned short int werden in den Datentyp int konvertiert. 2. Bei der Konversion einer Zahl a in einen n bit breiten Ganzzahldatentyp ohne Vorzeichen besteht das Ergebnis gerade aus den letzten n Bits von a. 3. Bei der Konversion in einen Ganzzahldatentyp mit Vorzeichen wird der Wert nicht verändert, wenn er im Ziel-Datentyp exakt dargestellt werden kann. Andernfalls ist das Ergebnis nicht durch den C++-Standard festgelegt. Die erste Konversion betrifft nur Konversionen „kleinerer“ Datentypen als int in den Datentyp int und wird auch als ganzzahlige Typangleichung (integral promotion) bezeichnet. Die letzten heißen auch ganzzahlige Typumwandlungen (integral conversion). Beispiel: Gemäß der Regeln 2. und 3. werden die folgenden Zuweisungen alle von Visual C++ übersetzt. Obwohl keiner der zugewiesenen Werte im Wertebereich des Datentyps der linken Seite liegt, gibt Visual C++ mit dem voreingestellten Warnlevel keine Warnung aus. int a=–257; char v1=a; // v1=='ÿ' (-1) unsigned int v2=a; // v2==4294967039 unsigned int b=2147483648; // INT_MAX+1 char v3=b; // v3= 0 int v4=b; // v4==–2147483648
Eine Konversion, bei der der Zieldatentyp alle Werte des konvertierten Datentyps darstellen kann, wird als sichere Konversion bezeichnet. Die ganzzahligen Typangleichungen und die folgenden Konversionen sind sichere Konversionen: unsigned char signed char
Æ unsigned short Æ short
Æ unsigned int Æ unsigned long Æ int Æ long
Da bei einem 32-bit-Rechner sizeof(char)=1 < sizeof(short)=2 < sizeof(int)=sizeof(long)=4 gilt, sind hier außerdem noch diese Konversionen sicher:
108
3 Elementare Datentypen und Anweisungen
unsigned char Æ short unsigned short Æ int Falls die beteiligten Datentypen unterschiedlich breit sind wie in int v; // 32 bit breit char a; // 8 bit breit ... v=a; // char wird in int konvertiert
wird das Bitmuster folgendermaßen angepasst: – Bei einem positiven Wert von a werden die überzähligen linken Bits von v mit Nullen aufgefüllt: a=1; // a = 0000 0001 binär v=a; // v = 0000 0000 0000 0000 0000 0000 0000 0001
– Bei einem negativen Wert von a werden die überzähligen linken Bits mit Einsen aufgefüllt. –1 mit 8 Bits: –1 mit 32 Bits:
binär 1111 1111, hexadezimal FF hex.: FFFFFFFF
Diese Anpassung des Bitmusters wird als Vorzeichenerweiterung bezeichnet. Das erweiterte Bitmuster stellt dann im Zweierkomplement denselben Wert dar wie das ursprüngliche. Da Ganzzahlliterale einen Ganzzahldatentyp haben, können sie in einen beliebigen anderen Ganzzahltyp konvertiert werden. Falls der Wert des Literals nicht im Wertebereich des anderen Datentyps liegt, können die impliziten Konversionen zu überraschenden Ergebnissen führen. Beispiel: Die folgenden Zuweisungen werden von Visual C++ ohne Fehlermeldungen übersetzt: // #define INT_MAX 2147483647 int k = 2147483648; // =INT_MAX+1 textBox1->Text=Convert::ToString(k); // –2147483648 int m = 12345678901234567890; // warning: 'Initialisierung': Verkürzung von // '__int64' zu 'int' textBox2->Text=Convert::ToString(m); // m=–350287150
Hier ist das an k und m zugewiesene Literal zu groß für den Datentyp int. Die ausgegebenen Werte entsprechen vermutlich nicht unbedingt den Erwartungen: Offensichtlich können harmlos aussehende Zuweisungen zu Ergebnissen führen, die auf den ersten Blick überraschend sind. Nicht immer wird durch eine Warnung
3.3 Ganzzahldatentypen
109
auf ein solches Risiko hingewiesen. Bei einem umfangreichen Programm mit vielen Warnungen werden diese auch leicht übersehen. Die Verantwortung für die gelegentlich überraschenden Folgen der impliziten Konversionen liegt deshalb letztendlich immer beim Programmierer: Er muss bei der Wahl der Datentypen stets darauf achten, dass sie nur zu sicheren Konversionen führen. Das erreicht man am einfachsten dadurch, dass man als Ganzzahldatentyp immer denselben Datentyp verwendet. Da der Datentyp eines Dezimalliterals meist int ist, liegt es nahe, immer diesen Datentyp zu wählen. Diese Empfehlung steht im Gegensatz zu einer anderen Empfehlung, die man relativ oft findet, nämlich Datentypen minimal zu wählen. Danach sollte man einen Datentyp immer möglichst klein wählen, aber dennoch groß genug, damit er alle erforderlichen Werte darstellen kann. Das führt zu einem minimalen Verbrauch an Hauptspeicher, erfordert allerdings eine gewisse Sorgfalt bei impliziten Konversionen. Beispiel: Für eine Variable, die einen Kalendertag im Bereich 1..31 darstellen soll, ist der Datentyp char oder unsigned char ausreichend. Da beide Datentypen sicher in den Datentyp int konvertiert werden können, spricht auch nichts gegen diese Datentypen. Für eine Variable, die eine ganzzahlige positive Entfernung darstellen soll. ist auf den ersten Blick der Datentyp unsigned int nahe liegend. Da die Konversion dieses Datentyps in int nicht sicher ist, sollte stattdessen int bevorzugt werden. Am einfachsten ist es, wenn man für alle diese Variablen den Datentyp int verwendet. 3.3.4 Operatoren und die „üblichen arithmetischen Konversionen“ Für Ganzzahloperanden sind unter anderem die folgenden binären Operatoren definiert. Sie führen zu einem Ergebnis, das wieder einen Ganzzahldatentyp hat: + – * / %
Addition Subtraktion Multiplikation Division, z.B. 7/4=1 Rest bei der ganzzahligen Division, z.B. 7%4 = 3
Für y ≠ 0 gilt immer: (x / y)*y + (x % y) = x.
110
3 Elementare Datentypen und Anweisungen
Das Ergebnis einer %-Operation mit nicht negativen Operanden ist immer positiv. Falls einer der Operanden negativ ist, ist das Vorzeichen des Ergebnisses im C++Standard nicht festgelegt. Im C++-Standard ist explizit nicht festgelegt, wie sich ein Programm verhalten muss, wenn bei der Auswertung eines Ausdrucks ein Überlauf oder eine unzulässige Operation (wie z.B. eine Division durch 0) stattfindet. Die meisten Compiler (wie auch Visual C++) ignorieren einen Überlauf und rechnen modulo 2n. Bei einer Division durch 0 wird eine Exception (siehe Abschnitt 3.19.2 und Kapitel 7) ausgelöst. Beispiele: Das Vorzeichen von x/y ergibt sich nach den üblichen Regeln: i = 17 / –3; j = –17 / 3; k = –17 / –3;
// i == –5 // j == –5 // k == 5
In Visual C++ hat x%y immer das Vorzeichen von x: i = 17 % –3; j = –17 % 3; k = –17 % –3;
// i == 2 // j == –2 // k == –2
Mit dem %-Operator kann man z.B. feststellen, ob eine Ganzzahl ein Vielfaches einer anderen Ganzzahl ist. if ((i%2)==0)// nur für gerade Werte von i erfüllt
In Zusammenhang mit den binären Operatoren stellt sich die Frage, welchen Datentyp das Ergebnis hat, wenn der Datentyp der beiden Operanden verschieden ist wie z.B. in short s; char c; ... = c + s;
C++ geht dabei in zwei Stufen vor, die auch als die üblichen arithmetischen Konversionen bezeichnet werden. Sie konvertieren die Operanden in einen gemeinsamen Datentyp, der dann auch der Datentyp des Ausdrucks ist. Für Ganzzahldatentypen sind sie folgendermaßen definiert: – In einem ersten Schritt werden alle Ausdrücke der Datentypen char, signed char, unsigned char, short int oder unsigned short int durch eine ganzzahlige Typangleichung (siehe Seite 107) in den Datentyp int konvertiert. – Falls der Ausdruck nach dieser Konvertierung noch verschiedene Datentypen enthält, ist der gemeinsame Datentyp der erste in der Reihe unsigned long int, long int, unsigned int
3.3 Ganzzahldatentypen
111
wenn einer der Operanden diesen Datentyp hat. Der gemeinsame Datentyp von long int und unsigned int ist in 32-bit Visual C++ unsigned long int. Beispiele: 1. Nach den Definitionen char ca=65; // 'A' char cb=0;
wird der Datentyp der Operanden von ca+cb durch ganzzahlige Typangleichungen in int konvertiert. Deshalb hat ca+cb den Datentyp int und nicht etwa char. 2. Für eine Variable u des Datentyps unsigned int wird der zweite Operand des Ausdrucks u–1 ebenfalls in den Datentyp unsigned int konvertiert. Deshalb hat dieser Ausdruck mit u=0 den Wert 4294967295 und nicht den Wert –1. In unsigned int u=0; int i=1/(u–1);
erhält man so den Wert i=0 und nicht etwa den Wert i=–1. Mit int i=u–1;
erhält i dagegen den Wert –1, da der unsigned Wert in int konvertiert wird. Das erste Beispiel zeigt insbesondere, dass sich der Datentyp eines Ausdrucks allein aus dem Datentyp der Operanden ergibt. Falls der Ausdruck einer Variablen zugewiesen wird, beeinflusst der Datentyp, an den die Zuweisung erfolgt, den Datentyp des Ausdrucks nicht. 3. Auch die Operanden der Vergleichs- oder Gleichheitsoperatoren werden mit den üblichen arithmetischen Konversionen in einen gemeinsamen Datentyp konvertiert. Deshalb werden nach der Definition unsigned int ui=1;
die beiden Operanden in der Bedingung (ui > –1) in den gemeinsamen Datentyp unsigned int konvertiert. Dadurch wird das Bitmuster 0xFFFFFFFF des intWerts –1 als unsigned int-Wert interpretiert. Da kein Wert größer als dieser Wert sein kann, wird durch if (ui>-1) textBox1->Text="1 > -1"; else textBox1->Text="1 AppendText("s="+toString(s)+ "\r\n");
In Visual C++ 2008 (aber noch nicht in Visual C++ 2005) ist die Konversion zwischen einem Zeiger auf einen nullterminierten String und einem String auch einfach mit den nach #include using namespace msclr::interop;
verfügbaren Funktionen To_Type marshal_as(From_Type input ); möglich. Bei ihr kann für To_Type der Datentyp String^ angegeben werden. In diesen Datentyp wird dann das Argument für input konvertiert. Einige der zulässigen Argumenttypen für input:
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
To_Type String^
307
Datentyp des Arguments für input char*, const char*, wchar_t*, const wchar_t*
Beispiel: Damit sind die folgenden Konversionen möglich const char* s="abc"; String^ S1 = marshal_as(s); textBox1->AppendText("s="+marshal_as(s));
Mit der Methode marshal_as der Klasse marshal_context kann ein nullterminierter String in einen String konvertiert werden. Der dabei zurückgegebene Zeiger ist aber nur bis zum delete des marshal_context Objekts gültig Beispiel: Die Funktion toCharPtr zeigt, wie man diese Klasse verwenden kann: #include using namespace msclr::interop; void toCharPtr(String^ s) { marshal_context^ context=gcnew marshal_context(); const char* p=context->marshal_as(s); // arbeite mit p delete context; // danach ist p ungültig }
Weitere Funktionen für die Konversion zwischen nullterminierten Strings und der Klasse String stehen im Namensbereich System::Runtime::InteropServices und insbesondere in der Klasse Marshal zur Verfügung. Diese sollen nur am Beispiel der Funktion static String^ PtrToStringAnsi(IntPtr ptr) illustriert werden, die wie die Funktion toString von oben einen als Argument übergebenen Zeiger auf einen nullterminierten String in einen String konvertiert: System::String^ toString(const char* s) { using System::Runtime::InteropServices::Marshal; return Marshal::PtrToStringAnsi((IntPtr)(char *)s); }
Aufgaben 3.12.11 Schreiben Sie die Lösungsfunktionen in eine wie auf Seite 91 beschriebene Header-Datei mit dem Namen „Aufgaben_3_12_11.h“ und rufen Sie diese beim Anklicken eines Buttons auf.
308
3 Elementare Datentypen und Anweisungen
1. a) Schreiben Sie eine Funktion, die die Anzahl der Leerzeichen ' ' in einem als Parameter übergebenen nullterminierten String (char*) zurückgibt. b) Entwerfen Sie systematische Tests für diese Funktion (siehe Abschnitt 3.5.1). c) Schreiben Sie eine Testfunktion (mit hart kodierten Strings, nicht aus einer TextBox) für diese Tests. 2. Auch auf einfache Fragen gibt es oft vielfältig widersprüchliche Antworten. So hat vor einiger Zeit jemand in einer Diskussionsgruppe im Internet gefragt, ob die folgenden Anweisungen char *x; x = "hello";
von erfahrenen Programmierern als korrekt angesehen würden. Er erhielt darauf über 100 Antworten, aus denen die folgenden vier ausgewählt wurden. Diese geben insbesondere auch einen Hinweis auf die Qualität mancher Beiträge in solchen Diskussionsgruppen. Begründen Sie für jede Antwort, ob sie korrekt ist oder nicht. a) Nein. Da durch diese Anweisungen kein Speicher reserviert wird, überschreibt die Zuweisung einen anderen Speicherbereich. b) Diese Anweisungen haben eine Zugriffsverletzung zur Folge. Die folgende Anweisung ist viel besser: char* x="hello";
c) Antwort auf b): Welcher Compiler produziert hier eine Zugriffsverletzung? Die beiden Anweisungen sind völlig gleichwertig. d) Ich würde die Anweisung char x[]="hello";
vorziehen, da diese sizeof(char*) Bytes Speicherplatz reserviert. 3. Welche der folgenden Bedingungen sind nach der Definition const char * s="blablabla";
in den if-Anweisungen zulässig, und welchen Wert haben sie? a) if b) if c) if d) if
(s==" ") ... (*s==" ") ... (*s=='a'+1) ... (s==' ') ...
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
309
Beschreiben Sie das Ergebnis der folgenden Anweisungen: e) textBox1->AppendText(gcnew String(s)+"\r\n"); textBox1->AppendText(gcnew String(s+1)+"\r\n"); textBox1->AppendText(gcnew String(s+20)+"\r\n");
f) char c='A'; char a[100]; strcpy(a,&c);
4. Die Funktion Checksum soll eine einfache Prüfsumme für Namen mit weniger als 10 Zeichen berechnen: int Checksum(const char* name) { char a[10]; // 10 is enough strcpy(a,name); int s=0; for (int i=0; a[i]!=0; i++) s=s+a[i]; return s; }
Beschreiben Sie das Ergebnis dieses Aufrufs: int c= Checksum("Check me, baby");
5. Welche der Zuweisungen in a) bis j) sind nach diesen Definitionen zulässig: const int i=17 ; int* p1; const int* p2; int const* p3; int* const p4=p1;
a) b) c) d) e)
p1=p2; p1=&i; p2=p1; *p3=i; *p4=18;
f) g) h) i) j)
char* q1; const char* q2; char const* q3; char* const q4=q1; q1=q2; q1="abc"; q2=q1; *q3='x'; *q4='y';
6. Für die Suche nach ähnlichen Strings gibt es viele Anwendungen: Um Wörter zu finden, deren genaue Schreibweise man nicht kennt, Korrekturvorschläge bei Schreibfehlern zu machen, Plagiate bei studentischen Arbeiten, Mutationen bei DNA-Sequenzen oder Ähnlichkeiten bei Musikstücken zu entdecken. Diese Aufgabe sowie Aufgabe 4.1, 4. befassen sich mit Verfahren zur Identifikation von ähnlichen Strings. Bei Navarro (2001) findet man eine umfangreiche Zusammenfassung zum Thema „Approximate String Matching“. Die sogenannte Levenshtein-Distanz (auch Levenstein-Distanz oder EditDistanz) ist eines der wichtigsten Maße für die Ähnlichkeit von zwei Strings s1 und s2. Sie ist die minimale Anzahl der Operationen „ein Zeichen ersetzen“,
310
3 Elementare Datentypen und Anweisungen
„ein Zeichen einfügen“ und „ein Zeichen löschen“, die notwendig sind, um den String s1 in s2 umzuwandeln. Für zwei Strings s1 und s2 der Längen l1 und l2 kann dieses Maß in seiner einfachsten Variante mit einer Matrix d mit (l1+1) Zeilen und (l2+1) Spalten folgendermaßen bestimmt werden: a) setze das i-te Element der ersten Zeile auf i b) setze das i-te Element der ersten Spalte auf i c) berechne die Elemente im Inneren der Matrix zeilenweise durch d[i][j] = min3(d[i-1][j-1] + cost, d[i][j-1]+1, d[i-1][j]+1); Dabei ist min3 das Minimum der drei übergebenen Parameter. Der Wert der int-Variablen cost ist 0, falls s1[i-1]==s2[j-1], und sonst 1. Die Levenshtein-Distanz ist dann der Wert des Elements d[l1][l2] Beispiel: s1=receieve, s2=retrieve
r e t r i e v e
0 1 2 3 4 5 6 7 8
r 1 0 1 2 3 4 5 6 7
e 2 1 0 1 2 3 4 5 6
c 3 2 1 1 2 3 4 5 6
e 4 3 2 2 2 3 4 5 6
i 5 4 3 3 3 2 3 4 5
e 6 5 4 4 3 3 2 3 4
v 7 6 5 5 4 4 3 2 3
e 8 7 6 6 5 5 4 3 2
Der Wert rechts unten ist dann die Levenshtein-Distanz ed, also ed=d[8][8]=2 Da zwei identische Strings die Edit-Distanz ed=0 haben und außerdem 0dmax(l1, j l2) gilt, erhält man mit 1-ed/max(l1,l2) ein Maß für die Ähnlichkeit von zwei Strings. Schreiben Sie eine Funktion StringSimilarityEditDistance, die diesen Wert als Funktionswert zurückgibt. Zum Testen können Sie Ihre Ergebnisse mit den folgenden Werten vergleichen. Dabei steht ed für StringSimilarityEditDistance: double n1=sed("receieve","receieve"); // 1-0/8=1 double n2=sed("receieve","receive"); // 1-1/8=0.875 double n3=sed("receieve","receiver"); // 1-2/8=0.75
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
311
double n4=sed("receieve","retrieve"); // 1-2/8=0.75 double n5=sed("receieve","reactive"); // 1-3/8=0,525 7. Welche dieser Funktionen sind const-korrekt. Ändern Sie die nicht constkorrekten Funktionen so ab, dass sie const-korrekt sind. void assign_c(int*& x, int* y) { x=y;} void replace_c(int* x, int* y) { *x=*y; } void clone_c(int*& x, int* y) { x=new int(*y);}
3.12.12 Verkettete Listen Wenn man einen Container als sortiertes Array implementiert, muss man nach dem Einfügen oder Löschen von Elementen alle auf das eingefügte bzw. gelöschte Element folgenden Elemente nach hinten oder vorne verschieben, damit die Sortierung erhalten bleibt. Das ist aber bei manchen Anwendungen zu zeitaufwendig. Diesen Zeitaufwand kann man mit verketteten Listen reduzieren. Eine solche Liste besteht aus sogenannten Knoten, die Daten und einen Zeiger auf den nächsten Knoten enthalten. Die Knoten einer verketteten Liste werden meist durch einen Datentyp wie Listnode dargestellt: typedef int T;// Datentyp der Nutzdaten struct Listnode { T data; Listnode* next; };
// die Nutzdaten // Zeiger auf den nächsten Knoten
Die Verwendung eines Datentyps in seiner Definition ist nur mit Zeigern oder Referenzen möglich. Ein solcher Datentyp wird auch als rekursiver Datentyp bezeichnet. Beispiel: Verwendet man einen Datentyp ohne * oder & in seiner Definition, wird das vom Compiler als Fehler betrachtet: struct Listnode { T data; Listnode next; // error: verwendet gerade }; // definiertes Listnode
Mit einem Datentyp wie Listnode erhält man eine verkettete Liste, indem man mit new Variablen dieses Typs erzeugt und in jedem Knoten dem Zeiger next die Adresse des nächsten Knotens zuweist: data
data
data
next
next
next
Ein Zeiger wie first zeigt auf den ersten Knoten der Liste:
312
3 Elementare Datentypen und Anweisungen ListNode* first; // Zeiger auf den ersten Knoten
Den letzten Knoten der Liste kennzeichnet man durch einen Zeiger next mit dem Wert 0. Grafisch wird dieser Wert oft durch einen schwarzen Punkt dargestellt: • Dann hat die verkettete Liste einen eindeutigen Anfang und ein eindeutiges Ende: first
data
data
data
next
next
next
•
Als nächstes sollen Anweisungen gesucht werden, mit denen man vor einem Knoten, auf den ein Zeiger n0 zeigt, d1 n0
next
einen neuen Knoten einfügen kann, auf den n0 dann zeigt: d1
n0
next d2 next
Das ist mit den Anweisungen unter 1. und 2. möglich: 1. Ein Zeiger tmp soll auf einen neuen Knoten zeigen, der mit new erzeugt wird Listnode* tmp=new Listnode;
// 1.1
und dem die Daten durch eine Anweisung wie tmp->data = d2;
// 1.2
zugewiesen werden. Dadurch ergibt sich: d1 n0
next d2
tmp
next
2. Der neue Knoten *tmp wird dann mit den beiden Anweisungen
3.12 Zeiger, Strings und dynamisch erzeugte Variablen tmp->next = n0; n0 = tmp;
313
// 2.1 // 2.2
in die Liste eingehängt:
n0
d1 2.2
2.1
next
d2 tmp
next
Diese Anweisungen werden mit der Funktion Listnode* newListnode(const T& data, Listnode* next) {// gibt einen Zeiger auf einen neuen Listenknoten // {d0,nx0} zurück, wobei d0 und nx0 die Argumente für // data und next sind. Listnode* tmp=new Listnode; // 1.1 tmp->data = data; // 1.2 tmp->next = next; // 2.2 return tmp; // Nachbedingung: Der Rückgabewert zeigt auf } // einen neuen Knoten {d0,nx0}
durch den Aufruf n0=newListnode(d2,n0);
// 2.1
ausgeführt. n0 zeigt danach auf einen neu erzeugten Knoten, dessen Element next auf den Knoten zeigt, auf den das Argument für next zeigt. Falls das Argument für next den Wert 0 hat, zeigt der Funktionswert auf einen Knoten mit next==0. Das Ergebnis der ersten drei Ausführungen der Anweisung first=newListnode(di,first);
mit den Daten d1, d2 usw., wobei der Wert von first zuerst 0 sein soll, ist in dem folgenden Ablaufprotokoll dargestellt. Dabei sind die Zeiger auf die von newListnode erzeugten Knoten mit n1, n2 usw. bezeichnet, und ein Knoten, auf den ein solcher Zeiger zeigt, mit ->{di,nj}. Der Ausdruck ->{d2,n1} in der Spalte n2 ist also nichts anderes als eine Kurzschreibweise für
d2 n2
n1
Die Werte in den Spalten n1, n2 usw. erhält man einfach durch Einsetzen der Argumente in die Nachbedingung von newListnode:
314
3 Elementare Datentypen und Anweisungen // first n1 n2 n3 first=0 0 ->{d1,0} first=newListnode(d1,first); n1 first=newListnode(d2,first); n2 ->{d2,n1} ->{d3,n2} first=newListnode(d3,first); n3
Dieses Ablaufprotokoll illustriert, wie der erste dieser Aufrufe einen ersten Knoten mit next==0 erzeugt, auf den first dann zeigt, und wie jeder weitere Aufruf einen neuen Knoten am Anfang in die verkettete Liste einhängt, auf die first zeigt (vollständige Induktion, siehe Aufgabe 3). Die Funktionsweise von newListnode beruht insbesondere darauf, dass eine mit new erzeugte Variable bis zum Aufruf von delete existiert. Im Unterschied zu einer gewöhnlichen Variablen wird ihr Speicherplatz nicht mit dem Verlassen des Blocks wieder freigegeben, in dem sie erzeugt wurde. – Deshalb existiert die Variable *tmp, die mit Listnode* tmp=new Listnode;
erzeugt wurde, auch noch nach dem Verlassen der Funktion newListnode. – Der Zeiger tmp ist dagegen eine „gewöhnliche“ lokale Variable, deren Speicherplatz mit dem Verlassen des Blocks wieder freigegeben wird. Da der Wert von tmp dem Element next des Funktionswerts zugewiesen wird, kann man die lokal erzeugte Variable *tmp über den Funktionswert auch außerhalb der Funktion verwenden, in der sie erzeugt wurde. Listen, bei denen neue Elemente am Anfang eingehängt werden, bezeichnet man auch als „Last-in-first-out“-Listen (LIFO), da das zuletzt eingefügte Element am Anfang steht. Eine LIFO-Liste erhält man mit einem Zeiger first, der am Anfang den Wert 0 hat, und wiederholte Aufrufe der Funktion newListnode: Listnode* first=0; void LIFOInsert(const T& data) { first=newListnode(data,first); }
Um alle Elemente einer Liste zu durchlaufen, kann man sich mit einer Hilfsvariablen tmp vom Anfang bis zum Ende durchhangeln: void showList(TextBox^ tb, Listnode* start) { // Gibt alle Daten der Liste ab der Position start aus Listnode* tmp=start;
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
315
while (tmp != 0) { tb->AppendText(tmp->data.ToString()); tmp = tmp->next; } }
Da start als Werteparameter übergeben wird, kann man auch start als Laufvariable verwenden. Wäre start ein Referenzparameter, würde das Argument verändert: while (start != 0) { tb->AppendText(start->data.ToString()); start = start->next; }
Anstelle einer while-Schleife kann man auch eine for-Schleife verwenden: for (Listnode* tmp= start; tmp != 0; tmp = tmp->next) tb->AppendText(tmp->data.ToString());
Durch diese Schleifen werden die Listenelemente in der Reihenfolge ausgegeben, in der sie sich in der Liste befinden. Falls sie durch eine Funktion wie LIFOInsert immer am Anfang eingehängt werden, werden sie in der umgekehrten Reihenfolge ausgegeben, in der sie eingehängt wurden. Einen Zeiger auf den ersten Knoten mit den Daten x erhält man mit der Funktion findLinear. Falls kein solcher Knoten gefunden wird, ist der Funktionswert 0: Listnode* findLinear(Listnode* start, const T& x) { Listnode* found=0; Listnode* tmp= start; while (tmp != 0 && found==0) { if(tmp->data==x) found=tmp; tmp = tmp->next; } return found; }
Oft will man die Knoten einer Liste nicht in der umgekehrten Reihenfolge durchlaufen, in der sie eingefügt wurden, sondern in derselben. Das kann man dadurch erreichen, dass man einen neuen Knoten immer am Ende der Liste einhängt. Damit man sich dann aber nicht bei jedem Einfügen zeitaufwendig bis zum Ende der Liste durchhangeln muss, kann man einen Zeiger last einführen, der immer auf das letzte Element der Liste zeigt:
316
3 Elementare Datentypen und Anweisungen
first
d1
d2
next
next
•
last
Ein neuer Knoten *tmp soll dann der letzte Knoten in der Liste sein:
first
d1
d2
next
next 1.
last
d3
2. tmp
next
•
Das wird dadurch erreicht, dass man sein Element next auf 0 setzt: Listnode* tmp = newListnode(data,0);
In eine nichtleere Liste (last!=0) wird der neue Knoten dann durch last->next = tmp;
// 1.
nach dem bisherigen letzten Element eingehängt. Falls die Liste dagegen leer ist (last==0), ist der neue Knoten der erste in der Liste: first = tmp;
Mit last = tmp;
// 2.
zeigt last dann auf den neuen letzten Knoten. Diese Anweisungen werden durch die folgende Funktion zusammengefasst: Listnode* first = 0; Listnode* last = 0; void insertLastListnode(const T& data) { // Erzeugt einen neuen Listen-Knoten und fügt diesen // nach last ein. last zeigt anschließend auf den // letzten und first auf den ersten Knoten der Liste. Listnode* n=newListnode(data,0); // n->{d0,0} if (last==0) first=n; else last->next=n; // 1. last=n; // 2.
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
317
// Nachbedingung: Bezeichnet man den Wert von last vor // dem Aufruf dieser Funktion mit l0, gilt // Fall I, l0==0: first==n && last==n // Fall II, l0!=0: l0->next==n && last==n }
Beim ersten Aufruf dieser Funktion gilt last==0, was zur Ausführung des thenZweigs der if-Anweisung und zu der als Fall I bezeichneten Nachbedingung führt. Bei jedem weiteren Aufruf gilt last!=0: Dann wird der else-Zweig ausgeführt, und es gilt die als Fall II bezeichnete Nachbedingung. Das Ergebnis der ersten drei Ausführungen der Anweisung insertLastListnode(di);
mit den Daten d1, d2 usw. ist in dem folgenden Ablaufprotokoll dargestellt. Dabei sind die Zeiger auf die in insertLastListnode erzeugten Knoten wieder wie im letzten Ablaufprotokoll mit n1, n2 usw. bezeichnet, und ein Knoten, auf den ein solcher Zeiger zeigt, mit ->{di,nj}. Die Werte in den Spalten n1, n2 usw. erhält man durch Einsetzen der Argumente in die Nachbedingung von insertLastListnode: // first last n1 n2 n3 first=0 0 last=0 0 insertLastLn(d1); n1 n1 ->{d1,0} insertLastLn(d2); n2 ->{d1,n2} ->{d2,0} insertLastLn(d3); n3 ->{d2,n3} ->{d3,0}
Das entspricht nach dem ersten Aufruf der Konstellation first
d1 next
und nach dem zweiten und dritten Aufruf den oben dargestellten Konstellationen. Offensichtlich hängt ein Aufruf von insertLastListnode einen neuen Knoten auch in eine Liste mit n Elementen am Ende ein. Eine Liste, bei der Knoten am Ende eingehängt und am Anfang entnommen werden, bezeichnet man auch als „First-in-first-out“-Liste (FIFO) oder als Queue. FIFO-Listen werden oft zur Simulation von Warteschlangen verwendet. Solche Warteschlangen können sich bilden, wenn Ereignisse in der Reihenfolge ihres Eintreffens bearbeitet werden (Fahrkartenausgabe, Bankschalter, Kasse in einem Supermarkt usw.). Um den Knoten, auf den ein Zeiger pn zeigt, aus einer Liste zu entfernen
318
3 Elementare Datentypen und Anweisungen
data1
pn
next data2 next
hängt man diesen Knoten einfach mit einer Anweisung wie pn = pn->next;
aus der Liste aus: data1
pn
next data2 tmp
next
Den vom ausgehängten Knoten belegten Speicher gibt man wie in eraseListnode mit delete wieder frei. Dazu muss man den Zeiger auf den Knoten vor dem Aushängen speichern: void eraseListnode(Listnode*& pn) { // entfernt *p aus der Liste if (pn!=0)//falls pn==0, nichts machen oder Fehlermeldung { Listnode* tmp = pn; pn = pn->next; delete tmp; } }
Beim Aufruf von eraseListnode muss das Argument der Zeiger im Listenknoten davor sein, damit dieser Zeiger anschließend auf das neue nächste Element zeigt. Diesen Zeiger kann man in einer einfach verketteten Liste nur durch eine Suche vom Anfang aus bestimmen, was relativ aufwendig ist. Deswegen verwendet man eraseListnode am besten nur in einer doppelt verketteten Liste, in der jeder Knoten auch einen Zeiger auf das vorherige Element enthält (siehe Aufgabe 2 h). Alle Knoten einer verketteten Liste können durch eine Funktion wie clearList ausgehängt und gelöscht werden. Falls ein Zeiger last auf das letzte Element zeigen soll, muss last auf 0 gesetzt werden. void clearList() { // löscht alle Knoten der Liste while (first!=0) eraseListnode(first); last=0; }
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
319
Dieser Abschnitt sollte nur einen ersten Einblick in den Aufbau und die Arbeit mit verketteten Listen geben. Dabei hat sich insbesondere gezeigt, dass verkettete Listen eine Alternative zu Arrays sein können, wenn ein Container zur Speicherung von Daten benötigt wird. Vergleichen wir zum Schluss die wichtigsten Vorund Nachteile dieser beiden Alternativen. Diese Vor- und Nachteile gelten dann auch für die in Abschnitt 4.2 vorgestellten Container vector und list der C++-Standardbibliothek, die mit dynamisch erzeugten Arrays und doppelt verketteten Listen implementiert sind: – Die Größe eines gewöhnlichen Arrays muss zum Zeitpunkt der Kompilation festgelegt werden. Wenn man zu diesem Zeitpunkt aber noch nicht weiß, wie viele Daten zur Laufzeit anfallen, reserviert man eventuell zu viel oder zu wenig. – Bei einem dynamisch erzeugten Array kann man mit Funktionen wie ReAllocate (siehe Abschnitt 3.12.6) bei Bedarf auch noch weiteren Speicher reservieren. Wenn man einen Zeiger p auf eine Position in einem dynamischen Array hat und die Speicherbereiche mit einer Funktion wie ReAllocate verschoben werden, ist p anschließend ungültig. Bei einer verketteten Liste werden die Elemente dagegen nie verschoben. Ein Zeiger auf einen Listenknoten wird nur ungültig, wenn der Knoten gelöscht wird. – Für eine mit new erzeugte Variable (wie z.B. ein Knoten einer Liste) ist neben dem Speicherplatz für die eigentlichen „Nutzdaten“ noch Speicherplatz für die Adresse (im Zeiger) notwendig. Speichert man eine Folge von kleinen Datensätzen (z.B. einzelne Zeichen) in einer verketteten Liste, kann das mit einem beträchtlichen Overhead verbunden sein. Die Adresse eines Arrayelements wird dagegen über den Index berechnet und belegt keinen Speicherplatz. – Der Zugriff auf das n-te Element eines Arrays ist einfach über den Index möglich. Da man auf das n-te Element einer verketteten Liste in der Regel keinen direkten Zugriff hat, muss man sich zu diesem meist relativ zeitaufwendig durchhangeln. – Will man in eine sortierte Folge von Daten neue Elemente einfügen bzw. entfernen, ohne die Sortierfolge zu zerstören, muss man in einer verketteten Liste nur die entsprechenden Zeiger umhängen. In einem Array müssen dagegen alle folgenden Elemente verschoben werden. Offensichtlich kann man nicht generell sagen, dass einer dieser Container besser ist als der andere. Vielmehr muss man die Vor- und Nachteile in jedem Einzelfall abwägen. Normalerweise brauchen und sollen Sie (außer in den folgenden Übungsaufgaben) keine eigenen verketteten Listen und dynamischen Arrays schreiben. Die Containerklassen list und vector der C++-Standardbibliothek sind für die allermeisten Anwendungen besser geeignet als selbstgestrickte Listen und Arrays. Da sich die wesentlichen Unterschiede zwischen diesen Containerklassen aus den zu-
320
3 Elementare Datentypen und Anweisungen
grundeliegenden Datenstrukturen ergeben, ist ein Grundverständnis dieser Datenstrukturen wichtig, auch wenn sie nicht selbst geschrieben werden sollen. Aufgabe 3.12.12 Schreiben Sie die Lösungsfunktionen in eine wie auf Seite 91 beschriebene Header-Datei mit dem Namen „Aufgaben_3_12_12.h“ und rufen Sie diese beim Anklicken eines Buttons auf. Falls diese Aufgaben im Rahmen einer Gruppe (z.B. einer Vorlesung oder eines Seminars) bearbeitet werden, können einzelne Teilaufgaben auch auf verschiedene Teilnehmer verteilt werden. Die Lösungen der einzelnen Teilaufgaben sollen dann in einem gemeinsamen Projekt zusammen funktionieren. 1. Ein Programm soll int-Werte aus einer TextBox in eine verkettete Liste einhängen. Die beiden Zeiger first und last sollen bei einer leeren Liste den Wert 0 haben und bei einer nicht leeren Liste immer auf den ersten und letzten Knoten der Liste zeigen. Schreiben Sie die folgenden Funktionen und rufen Sie diese beim Anklicken eines entsprechenden Buttons auf. Sie können sich dazu an den Beispielen im Text orientieren. Schreiben Sie außerdem für jede Funktion Testfunktionen (siehe Abschnitt 3.5.2), die wenigstens eine elementare Funktionalität prüfen. a) pushFront soll einen neuen Knoten mit den als Parameter übergebenen Daten am Anfang in die Liste einhängen. Diese Funktion soll beim Anklicken eines Buttons mit der Aufschrift „Am Anfang einfügen“ aufgerufen werden. Das Argument soll der int-Wert aus der TextBox sein. b) showList soll die Daten der Liste in einer mehrzeiligen TextBox anzeigen und beim Anklicken eines Buttons mit der Aufschrift „Anzeigen“ aufgerufen werden. c) Schreiben Sie ein Ablaufprotokoll für 4 Aufrufe der Funktion pushFront (z.B. mit den Argumenten“10”, “11”, “12” und “13”). Geben Sie eine Beziehung an, die nach jedem Aufruf dieser Funktion gilt. d) findLinear soll ab einer Startposition (ein als Parameter übergebener Zeiger auf einen Knoten) nach dem ersten Knoten der Liste mit den als Parameter übergebenen Daten suchen. Falls ein solcher Knoten existiert, soll ein Zeiger auf ihn zurückgegeben werden und andernfalls der Wert 0. Diese Funktion soll beim Anklicken eines Buttons mit der Aufschrift „Linear suchen“ aufgerufen werden und alle int-Werte der Liste ausgegeben, die gleich dem in der Eingabe-TextBox sind. e) pushBack soll einen neuen Knoten mit den als Parameter übergebenen Daten am Ende der Liste einhängen. Diese Funktion soll beim Anklicken eines Buttons mit der Aufschrift „Am Ende einfügen“ aufgerufen werden. Das Argument soll der int-Wert aus der TextBox sein. f) insertSorted soll einen neuen Knoten mit den Parameter übergebenen Daten so in eine sortierte verkette Liste einhängen, dass die Liste anschließend
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
321
auch noch sortiert ist. Diese Funktion soll beim Anklicken eines Buttons mit der Aufschrift „Sortiert einfügen“ aufgerufen werden. Das Argument soll der int-Wert aus der TextBox sein. Schreiben Sie dazu eine Funktion findBefore, die die Position des Knotens zurückgibt, an der der neue Knoten eingefügt werden soll. g) clearList soll den gesamten von der verketteten Liste belegten Speicherplatz wieder freigeben. Danach sollen first und last wieder eine leere Liste darstellen. Diese Funktion soll beim Anklicken eines Buttons mit der Aufschrift „Liste löschen“ aufgerufen werden h) Bei welcher dieser Funktionen zeigen first und last auch nach dem Aufruf auf den ersten und letzten Knoten der Liste, wenn sie vor dem Aufruf auf diese Knoten gezeigt haben? 2. Eine doppelt verkettete Liste besteht aus Knoten, die nicht nur einen Zeiger next auf den nächste Knoten enthalten, sondern außerdem auch noch einen Zeiger prev auf den Knoten davor. Eine solche Liste kann man sowohl vorwärts als auch rückwärts durchlaufen. Die doppelt verkettete Liste in dieser Aufgabe soll durch die beiden Zeiger firstDll und lastDll dargestellt werden, die immer auf den ersten bzw. letzten Knoten zeigen. Schreiben Sie die folgenden Funktionen und rufen Sie diese beim Anklicken eines entsprechenden Buttons auf. Sie können sich dazu an der letzten Aufgabe orientieren. a) Entwerfen Sie eine Datenstruktur DllListnode, die einen Knoten einer doppelt verketteten Liste darstellt. Eine Funktion newDllListnode soll einen solchen Knoten mit den als Argument übergebenen Zeigern auf die Knoten next und prev sowie den Daten erzeugen und einen Zeiger auf diesen Knoten zurückgeben. b) Schreiben Sie eine Funktion pushFrontDll, die einen Knoten mit den als Argument übergebenen Daten in eine doppelt verkettete Liste am Anfang einhängt. c) showDllForw soll die Daten der doppelt verketteten Liste in einer TextBox anzeigen und dabei mit firstDll beginnen. d) showDllRev soll die Daten der doppelt verketteten Liste in einem TextBox anzeigen und dabei mit lastDll beginnen. e) Stellen Sie das Ergebnis der ersten drei Aufrufe von pushFrontDll in einem Ablaufprotokoll dar. f) pushBackDll soll einen Knoten mit den als Argument übergebenen Daten am Ende in die verkette Liste einhängen. g) Stellen Sie das Ergebnis der ersten drei Aufrufe von pushBackDll in einem Ablaufprotokoll dar h) eraseDllListnode soll den ersten Knoten mit den als Parameter übergebenen Daten löschen, falls ein solcher Knoten existiert. i) clearList soll den gesamten von der verketteten Liste belegten Speicherplatz wieder freigeben. firstDll und lastDll sollen danach eine leere Liste darstellen.
322
3 Elementare Datentypen und Anweisungen
j) Schreiben Sie für Ihre Lösungen Testfunktionen (siehe Abschnitt 3.5.2), die zumindest eine elementare Funktionalität prüfen. Geben Sie die Teile der Aufgabenstellung explizit an, die nicht getestet werden können. 3. Zeigen Sie mit vollständiger Induktion, dass durch wiederholte Aufrufe von a) pushFront (Aufgabe 1 a) eine einfach verkettete Liste aufgebaut wird, bei der ein neuer Knoten am Anfang eingehängt wird. Vor dem ersten Aufruf soll first==last==0 sein. b) pushBack (Aufgabe 1 d) eine einfach verkettete Liste aufgebaut wird, bei der ein neuer Knoten am Ende eingehängt wird. Vor dem ersten Aufruf soll first==last==0 sein. c) pushFrontDll (Aufgabe 2 b) eine doppelt verkettete Liste aufgebaut wird, bei der ein neuer Knoten am Anfang eingehängt wird. Vor dem ersten Aufruf soll firstDLL==0 sein. d) pushBackDll (Aufgabe 2 f) eine doppelt verkettete Liste aufgebaut wird, bei der ein neuer Knoten am Ende eingehängt wird. Vor dem ersten Aufruf soll firstDLL==0 sein. 3.12.13 Binärbäume Baumstrukturen werden aus Knoten aufgebaut, die einen Zeiger auf einen linken und rechten Teilbaum enthalten: 17
root
left
right
41
7
left •
right
left •
right •
13
left •
right •
Ein Baumknoten kann durch den Datentyp Treenode dargestellt werden: typedef int T;// Datentyp der Nutzdaten struct Treenode { T data; Treenode* left; Treenode* right; };
// die Nutzdaten
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
323
Der Zeiger auf den obersten Knoten des Baums wird oft als root bezeichnet: Treenode* root=0;
Die Funktion newTreenode erzeugt einen Baumknoten mit den als Argument übergebenen Daten und Zeigern: Treenode* newTreenode(const T& data, Treenode* left, Treenode* right) { // gibt einen Zeiger auf einen neuen Knoten zurück Treenode* tmp=new Treenode; tmp->data = data; tmp->left = left; tmp->right = right; return tmp; }
Baumstrukturen sollen im Folgenden am Beispiel von binären Suchbäumen illustriert werden. Ein binärer Suchbaum ist eine Baumstruktur, in der – ein Knoten einen Schlüsselwert hat, nach dem die Knoten im Baum angeordnet werden, sowie eventuell weitere Daten, – jeder linke Teilbaum eines Knotens nur Schlüsselwerte enthält, die kleiner sind als der Schlüsselwert im Knoten, und – jeder rechte Teilbaum nur Schlüsselwerte, die größer oder gleich dem Schlüsselwert im Knoten sind. Beispiel: Der Baum von oben ist ein binärer Suchbaum, bei dem data als Schlüsselwert verwendet wird. Hängt man Knoten mit den folgenden Werten an den jeweils angegebenen Positionen ein, ist der Baum auch anschließend noch ein binärer Suchbaum: 5: an der Position left beim Knoten mit dem Wert 7 10: an der Position left beim Knoten mit dem Wert 13 15: an der Position right beim Knoten mit dem Wert 13 In einen binären Suchbaum mit der Wurzel root können Knoten mit der folgenden Funktion eingehängt werden: void insertBinTreenode(const T& x) { if (root==0) root=newTreenode(x,0,0); else { Treenode* i=root; Treenode* p; while(i!=0) { p=i; if (xdata) i=i->left; else i=i->right; }
324
3 Elementare Datentypen und Anweisungen if (xdata) p->left=newTreenode(x,0,0); else p->right=newTreenode(x,0,0); } }
Mit einer Funktion wie searchBinTree kann man einen Knoten mit den als Argument übergebenen Daten finden: Treenode* searchBinTree(const T& x) { Treenode* result=0; if (root!=0) { Treenode* i=root; while(i!=0 && i->data!=x) { if (xdata) i=i->left; else i=i->right; } if (i!=0 && i->data==x) result=i; } return result; }
Falls in einem Baum der linke und rechte Teilbaum eines Knotens jeweils etwa gleich tief ist, reduziert sich der Suchbereich mit jedem Schritt etwa um die Hälfte, so dass man wie beim binären Suchen logarithmische Suchzeiten erhält. Ein solcher Baum wird auch als balancierter Baum bezeichnet. Verbreitete balancierte Bäume sind die sogenannten Rot-Schwarz-Bäume, die oft in der C++-Standardbibliothek verwendet werden, und AVL-Bäume. Bei ihnen werden Knoten immer so eingefügt oder gelöscht, dass der Baum anschließend ausgeglichen ist. Balancierte Binärbäume werden oft mit rekursiven Funktionen bearbeitet, da ihre Rekursionstiefe nicht sehr groß wird (siehe Abschnitt 5.3). Normalerweise brauchen und sollen Sie (außer in den folgenden Übungsaufgaben) keine eigenen Binärbäume schreiben. Die C++-Standardbibliothek enthält die Containerklassen set, map usw. (siehe Abschnitt 4.4), die intern mit balancierten Binärbäumen (meist Rot-Schwarz-Bäume) implementiert sind und die für die allermeisten Anwendungen besser geeignet sind als selbstgestrickte Bäume. Da sich die wesentlichen Eigenschaften dieser Containerklassen aus den zugrundeliegenden Datenstrukturen ergeben, ist ein Grundverständnis dieser Datenstrukturen wichtig, auch wenn sie nicht selbst programmiert werden sollen. Für weitere Informationen zu Baumstrukturen wird auf die umfangreiche Literatur verwiesen (z.B. Cormen, 2001).
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
325
Aufgabe 3.12.13 1. Eine typische Anwendung von balancierten Binärbäumen ist ein Informationssystem, das zu einem eindeutigen Schlüsselbegriff eine zugehörige Information findet, z.B. den Preis zu einer Artikelnummer. Bei den folgenden Aufgaben geht es aber nur um einige elementare Operationen und nicht um Performance. Deshalb muss der Baum nicht balanciert sein. a) Entwerfen Sie eine Datenstruktur Treenode, die einen Knoten eines Baums mit einem Schlüssel key und zugehörigen Daten data (Datentyp z.B. int für beide) darstellt. Eine Funktion newTreenode soll einen solchen Knoten mit den als Argument übergebenen Zeigern auf die Unterbäume left und right sowie den Daten key und data erzeugen. b) Schreiben Sie eine Funktion insertBinTreenode, die einen Knoten mit den als Argument übergebenen Daten in einen Binärbaum einhängt. Rufen Sie diese Funktion beim Anklicken eines Buttons mit Argumenten für key und data auf, die aus zwei TextBoxen übernommen werden. Die Knoten sollen im Baum entsprechend dem Schlüsselbegriff angeordnet werden. c) Schreiben Sie eine Funktion searchBinTree, die einen Zeiger auf einen Knoten mit dem als Argument übergebenen Schlüsselbegriff zurückgibt, wenn ein solcher Knoten gefunden wird, und andernfalls den Wert 0. Schreiben Sie unter Verwendung der Funktion seachBinTree eine Funktion bool ValueToKey(KeyType Key,ValueType& Value)
Ihr Funktionswert soll true sein, wenn zum Argument für Key ein Knoten mit diesem Schlüsselwert gefunden wurde. Die zugehörigen Daten sollen dann als Argument für Value zurückgegeben werden. Falls kein passender Wert gefunden wird, soll der Funktionswert false sein. d) Schreiben Sie Testfunktionen (siehe Abschnitt 3.5.2), die zumindest eine elementare Funktionalität Ihrer Lösungen prüfen. Im Zusammenhang mit den assoziativen Containern der Standardbibliothek wird in Abschnitt 4.4 eine einfachere Lösung dieser Aufgabe vorgestellt, die auf balancierten Binärbäumen beruht. 3.12.14 Zeiger als Parameter Ԧ In der Programmiersprache C gibt es keine Referenzparameter (siehe Abschnitt 3.4.5 und 3.18.2) und auch kein anderes Sprachelement, mit dem man den Wert eines Arguments durch eine Funktion verändern kann. Um in C mit einer Funktion eine als Argument übergebene Variable zu verändern, übergibt man deshalb als Parameter einen Zeiger auf die Variable. Die Variable wird dann in der Funktionsdefinition durch eine Dereferenzierung des Zeigers angesprochen.
326
3 Elementare Datentypen und Anweisungen
Funktionen mit Zeiger-Parametern schreiben bei ihrem Aufruf meist Werte an die als Argument übergebene Adresse. Deshalb muss diese Adresse auf einen reservierten Speicherbereich zeigen. Beispiel: Mit der Funktion vertausche können die Werte von zwei Variablen des Datentyps int vertauscht werden: void vertausche(int* x, int* y) { int h = *x; *x = *y; *y = h; }
Beim Aufruf der Funktion übergibt man dann die Adresse der zu vertauschenden Variablen als Argument: int x=0,y=1; vertausche(&x,&y);
Da viele C++-Programmierer früher in C programmiert haben und C++-Programme oft C-Bibliotheken verwenden, findet man diese Technik auch heute noch in C++-Programmen. Sie bietet dieselben Möglichkeiten wie Referenzparameter. Allerdings sind Referenzparameter aus den folgenden Gründen einfacher: – Die Parameter müssen in der Funktion nicht dereferenziert werden. – Beim Aufruf der Funktion muss der Adressoperator & nicht angegeben werden. Normalerweise besteht in einem C++-Programm keine Notwendigkeit, Parameter eines Zeigertyps zu verwenden. Zu den Ausnahmen gehören Funktionen aus C Bibliotheken, die Parameter eines Zeigertyps haben. Da solche Funktionen in Visual C++ normalerweise nicht notwendig sind, soll ein Beispiel genügen. Beispiel: Die Funktion der C-Standardbibliothek void *memset(void *s, int c, size_t n); // size_t ist unsigned int beschreibt n Bytes ab der Adresse in s mit dem Wert c. Nach double d; double* pd=new double;
kann memset folgendermaßen aufgerufen werden: memset(&d,0,sizeof(d));//übergebe die Adresse von memset(pd,0,sizeof(*pd)); // d
Wie die Darstellung des Datenformats double in Abschnitt 3.6.1 zeigt, ist dieser Aufruf nur eine etwas umständliche Art, eine double-Variable auf 0 zu setzen.
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
327
3.12.15 C++/CLI-Erweiterungen: Garbage Collection und der GC-Heap In Standard-C++ müssen die auf dem Heap mit new angelegten Speicherbereiche manuell freigegeben werden (mit delete). Neben diesem Heap, der auch als „nativer Heap“ oder „nicht verwalteter Heap“ bezeichnet wird, gibt es in Visual C++ bei CLR-Anwendungen (z.B. Windows Forms-Anwendungen) einen Heap, bei dem nicht mehr benötigter Speicher automatisch wieder freigegeben wird. Dieser Heap wird von Zeit zu Zeit von einem sogenannten Garbage Collector (engl. „Müllabfuhr“) überprüft, der den Speicherplatz wieder frei gibt, auf den keine Referenz mehr existiert. Deswegen ist es nicht notwendig, für Objekte auf diesem Heap delete aufzurufen, obwohl das möglich ist. Dieser Heap wird auch als „GCHeap“, „garbage collected Heap“, „CLI-Heap“, „verwalteter Heap“ oder „managed Heap“ bezeichnet. Die zugehörigen Sprachelemente gehören zu den sogenannten C++/CLI-Erweiterungen und werden in Kapitel 9 ausführlich dargestellt. Hier zunächst nur ein kurzer Überblick, der für einfache Operationen mit den .NET-Klassen oder StringObjekten ausreichend ist. – Ein Zeiger auf ein Objekt im GC-Heap wird mit dem Operator ^ definiert. Dieser Operator entspricht dem Operator * für Zeiger auf den nativen Heap. Ein solcher Zeiger wird als gc-Zeiger, Handle oder Objektreferenz bezeichnet. – Objekte auf dem GC-Heap werden mit gcnew erzeugt, während Objekte auf dem nativen Heap mit new erzeugt werden. Beispiel: Die beiden Anweisungen int* p; int^ h;
definieren den Zeiger p auf einen int-Wert auf dem nativen Heap und den Zeiger h auf einen int-Wert auf dem GC-Heap. Objekte auf dem nativen Heap und dem GC-Heap erhält man mit Anweisungen wie: p=new int; // ein Objekt auf dem nativen Heap h=gcnew int; // ein Objekt auf dem GC-Heap
Der für Objekte auf dem GC-Heap reservierte Speicher kann mit delete wieder freigegeben werden. Das ist derselbe Operator wie für Objekte auf dem nativen Heap. Es gibt keinen speziellen delete-Operator für Objekte auf dem GC-Heap, wie etwa gcdelete. Wird delete nicht aufgerufen, kann der Garbage Collector den Speicher wieder freigeben, falls er feststellt, dass kein Handle mehr auf den Speicher verweist. Falls also der Aufruf von delete für ein Objekt auf dem GCHeap vergessen wird, hat das nie ein Speicherleck zur Folge. Beispiel: Nach dem Verlassen der Funktion f kann der Garbage Collector feststellen, dass es keine Referenzen mehr auf das GC-Heap Objekt gibt, und seinen Speicher freigeben:
328
3 Elementare Datentypen und Anweisungen void f() { int^ h=gcnew int(17); }
Gibt man diesen Speicher mit delete wieder frei, wird er nicht unnötig lange reserviert: int^ h=gcnew int(17); delete h;
Einem Zeiger auf den GC-Heap kann man nicht wie einem nativen Zeiger der Wert 0 zuweisen. Wenn man zum Ausdruck bringen will, dass ein gc-Zeiger nicht auf reservierten Speicher zeigt, muss man das Nullzeiger-Literal nullptr verwenden. Beispiel: Einem Zeiger auf den nativen Heap wird üblicherweise der Wert 0 zugewiesen, um auszudrücken, dass er nicht auf reservierten Speicher zeigt: int* p=0; // p zeigt nicht auf einen reservierten // Speicherbereich
Bei einem Handle muss man dafür nullptr verwenden: int^ h=nullptr; // h zeigt auf keinen Wert
Um zu prüfen, ob ein Handle auf reservierten Speicher zeigt, muss man es mit nullptr vergleichen: if (h==nullptr) textBox1->AppendText( "h==nullptr\r\n");
Alle Objekte einer Verweis- oder .NET-Klasse wie z.B. String müssen auf dem GC-Heap erzeugt werden. Beispiel: Variable des Typs String müssen mit ^ definiert werden. Falls sie nicht auf dem GC-Heap angelegt werden, ist das ein Fehler: String^ s1; // ok String s2; // error: String kann ohne "^" nicht // verwendet werden String* s3; //error: '*' für String nicht möglich
Für jedes Steuerelement, das aus der Toolbox auf das Formular gesetzt wird, erzeugt Visual Studio in der Formularklasse (in Form1.h) Anweisungen, die das Steuerelement definieren und mit gcnew erzeugen: private: System::Windows::Forms::Button^ button1; this->button1=gcnew System::Windows::Forms::Button();
Die Steuerelemente werden dann mit Anweisungen wie
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
329
this->Controls->Add(this->button1);
in das Formular aufgenommen. Für die im Eigenschaftenfenster gesetzten Werte werden Anweisungen wie die Folgende erzeugt: this->button1->Text = L"OK ";
3.12.16 C-Bibliotheksfunktionen in string.h für nullterminierte Strings Ԧ Angesichts der großen Bedeutung der Stringbearbeitung gibt es zahlreiche Bibliotheksfunktionen für nullterminierte Strings, die noch aus den Urzeiten von C stammen. Da sie recht bekannt sind, werden sie auch heute noch oft in C++-Programmen verwendet. Das ist allerdings ein Anachronismus, der vermieden werden sollte: C++ enthält Stringklassen, die wesentlich einfacher und sicherer benutzt werden können (siehe Abschnitte 3.13 und 4.1), und die bevorzugt werden sollten. Microsoft Visual C++ 2005/2008 hat diese Funktionen inzwischen als „deprecated“ gebannt. Bei jeder Verwendung einer solchen Funktion gibt der Compiler eine Warnung dieser Art aus: warning C4996: 'sprintf' was declared deprecated Falls Sie also nie mit C-Programmen zu tun haben werden, lassen Sie dieses Kapitel am besten aus und verwenden die hier vorgestellten Funktionen und Bibliotheken nie. Falls Sie jedoch mit älteren Programmen arbeiten müssen, die diese Konzepte verwenden bleibt Ihnen dieses Kapitel nicht erspart. Falls Sie neue Programme entwickeln und nullterminierte Strings benötigen, können die „Secure Libray Functions“ (siehe Abschnitt 3.12.17) eine Alternative sein. Nach #include sind unter anderem die folgenden Funktionen der C Standardbibliothek für nullterminierte Strings verfügbar. Sie hangeln sich alle wie im Beispiel my_strcpy (siehe Abschnitt 3.12.10) von einem als Argument übergebenen Zeiger bis zum nächsten Nullterminator durch. Deshalb dürfen sie nur mit Argumenten aufgerufen werden, bei denen – die Zeiger für eine Quelle auf einen nullterminierten String zeigen, und – die Zeiger für einen Zielbereich auf genügend reservierten Speicher zeigen. Da die Überprüfung dieser Voraussetzungen oft nicht einfach ist oder vergessen werden kann, ist ihre Verwendung nicht ungefährlich. size_t strlen(const char *s); Der Rückgabewert ist die Länge des nullterminierten Strings, auf den s zeigt (ohne den Nullterminator '\0'). Dabei werden ab *s die Zeichen bis zum nächsten Nullterminator gezählt. char *strcpy(char *dest, const char *src);
330
3 Elementare Datentypen und Anweisungen
Kopiert alle Zeichen ab der Adresse in src bis zum nächsten Nullterminator in die Adressen ab dest (wie my_strcpy). char *strcat(char *dest, const char *src); strcat hängt eine Kopie von src an das Ende von dest an. Das Ergebnis hat die Länge strlen(dest) + strlen(src). Der Rückgabewert ist dest. int strcmp(const char *s1, const char *s2); Vergleicht die nullterminierten Strings, auf die s1 und s2 zeigen, zeichenweise als unsigned char. Der Vergleich beginnt mit dem ersten Zeichen und wird so lange fortgesetzt, bis sich die beiden Zeichen unterscheiden oder bis das Ende eines der Strings erreicht ist. Falls s1==s2, ist der Rückgabewert 0. Ist s1 < s2, ist der Rückgabewert < 0 und andernfalls > 0. char *strstr(char *s1, const char *s2); Diese Funktion durchsucht s1 nach dem ersten Auftreten des Teilstrings s2. Falls s2 in s1 vorkommt, ist der Rückgabewert ein Zeiger auf das erste Auftreten von s2 in s1. Andernfalls wird 0 (Null) zurückgegeben. Zur Umwandlung von Strings, die ein Dezimalliteral des entsprechenden Datentyps darstellen, stehen nach #include diese Funktionen zur Verfügung: int atoi(const char *s); // „ascii to int“ long atol(const char *s); // „ascii to long“ double atof(const char *s); // „ascii to float“, aber Ergebnis double Sie geben den Wert des umgewandelten Arguments zurück, falls es konvertiert werden kann: int i=atoi("123"); // i=123 double d=atof("45.67"); // d=45.67
Diese Funktionen brechen die Umwandlung beim ersten Zeichen ab, das nicht zu einem Literal des jeweiligen Datentyps passt: double d=atof("45,67"); // d=45: Komma statt Punkt
Dabei kann man nicht feststellen, ob alle Zeichen des Strings umgewandelt wurden oder nicht. Deshalb sollte man diese Funktionen nicht zur Umwandlung von Benutzereingaben verwenden, da solche Strings nicht immer dem erwarteten Schema entsprechen. Die vielseitigste Funktion zur Umwandlung von Ausdrücken verschiedener Datentypen in einen nullterminierten String ist (nach #include )
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
331
int sprintf(char *buffer, const char *format[, argument, ...]); Sie schreibt einen nullterminierten String in das char Array, dessen Adresse für buffer übergeben wird. Der String ergibt sich aus dem Formatstring (dem Argument für format) und den weiteren Argumenten. Der Formatstring enthält sowohl Zeichen, die unverändert ausgegeben werden, als auch Formatangaben, die festlegen, wie die weiteren Argumente dargestellt werden. Die erste Formatangabe legt das Format für das erste Argument fest, usw. Weitere Funktionen sind ähnlich aufgebaut und schreiben Text in eine Datei (fprintf) oder auf die Konsole (printf). Eine Formatangabe beginnt immer mit dem Zeichen % und ist nach folgendem Schema aufgebaut: % [flags] [width] [.prec] [F|N|h|l|L] type_char Das %-Zeichen wird (immer in dieser Reihenfolge) gefolgt von: optionalen flags (z.B. „–“ für eine linksbündige Formatierung) der optionalen Angabe für die minimale Breite [width] der optionalen Präzisionsangabe [.prec] der optionalen Größenangabe [F|N|h|l|L] dem obligatorischen Typkennzeichen type_char, das festlegt, wie das zugehörige Argument interpretiert wird. Es kann unter anderem einer dieser Werte sein: d x e f p c s
konvertiert einen Ganzzahlwert in das Dezimalformat konvertiert einen Ganzzahlwert in seine Hexadezimaldarstellung stellt einen double-Wert in einem Exponentialformat „ddd...e+dd“ dar stellt einen double-Wert in einem Festkommaformat „-ddd.ddd...“ dar stellt einen Zeiger hexadezimal dar zur Darstellung von Zeichen (Datentyp char) zur Darstellung nullterminierter Strings (Datentyp char*)
Diese Liste ist nicht vollständig. Für weitere Details wird auf die Online-Hilfe verwiesen. Einige Beispiele für die fast unüberschaubare Zahl von Kombinationen: char s[100]; sprintf(s,"%d + %x = %g",17,17,17+17.0); // s="17 + 11 = 34" char const* t="Hallo"; sprintf(s,"%s ihr da dr%cußen: ",t,'a'); // s="Hallo ihr da draußen: " double d=1e5; sprintf(s,"Bitte überweisen Sie %g Euro auf mein Konto",d); // s="Bitte überweisen Sie 100000 Euro auf mein Konto" char const* u="linksbündig"; sprintf(s,"%–20s:",u); // s="linksbündig : "
332
3 Elementare Datentypen und Anweisungen
Die printf Funktionen interpretieren den Speicherbereich an der Adresse eines auszugebenden Arguments nach den zugehörigen Angaben im Formatstring, und zwar unabhängig davon, ob sie zusammenpassen oder nicht. Falls sie nicht zusammenpassen, wird das bei der Kompilation nicht entdeckt und hat falsche Ergebnisse zur Folge. Deshalb ist bei der Verwendung von sprintf Vorsicht geboten. Beispiel: Wenn man sprintf ein int-Argument mit einer double-Formatangabe übergibt, wird das int-Bitmuster als Gleitkommawert interpretiert. Falls das Ergebnis nicht allzu unplausibel aussieht, wird dieser Fehler vom Anwender eventuell nicht einmal bemerkt: int i=17; sprintf(s,"i=%f",i); // s=="i=0.000000"
Falls sprintf 8 Bytes (sizeof(double)) anspricht, obwohl nur 4 Bytes für i reserviert sind, kann das Programm abstürzen. Während der Kompilation erfolgt kein Hinweis auf ein eventuelles Problem. Im Unterschied dazu ist die Verwendung der Stringklassen ohne jedes Risiko: String^ s = i.ToString();
Nullterminierte Strings aus „wide char“-Zeichen werden als Zeiger auf wchar_t definiert. Literale beginnen mit einem „L“ und können Unicode-Texte darstellen: wchar_t* w= L"ΏΏ Ι ε
֘"; // arabische Zeichen
Für solche Strings gibt es im Wesentlichen dieselben Funktionen wie für char*. Ihre Namen beginnen mit „wcs“ (für „wide character string“) anstelle von „str“: size_t wcslen(const wchar_t *s); // wie strlen wchar_t *wcscpy(wchar_t *dest, const wchar_t *src); // wie strcpy wchar_t *wcscat(wchar_t *dest, const wchar_t *src); // wie strcat int wcscmp(const wchar_t *s1, const wchar_t *s2); ... Aufgaben 3.12.16 1. Beschreiben Sie das Ergebnis der folgenden Anweisungen: char c='A'; const char* s="yodel doodle doo"; char* t;
a) b) c) d)
int n1=strlen(&c); int n2=strlen(strstr(s,"doo")); int n3=strlen(strstr("doo",s)); strcpy(t,s);
3.12 Zeiger, Strings und dynamisch erzeugte Variablen
333
2. Schreiben Sie eine Funktion char* cloneString(const char* s), die einen als Parameter übergebenen nullterminierten String in einen dynamisch erzeugten Speicherbereich kopiert und dessen Adresse zurückgibt. a) Beschreiben Sie den Unterschied der beiden Zuweisungen an s1 und s2: char* t="abc"; char* s1=t; char* s2=cloneString(t);
b) Was müssen Sie nach einem Aufruf von cloneString beachten. 3.12.17 Die „Secure Library Functions“ Ԧ Die Arbeit mit nullterminierten Strings (char*) und den zugehörigen Bibliotheksfunktionen ist von Natur aus fehleranfällig. So muss man z.B. beim Kopieren eines solchen Strings mit char *strcpy(char * s1, const char * s2); // immer darauf achten, dass ab der Adresse in s1 mindestens strlen(s2)+1 Zeichen reserviert sind. Die Funktionen, die die Anzahl der kopierten Zeichen begrenzen, scheinen Abhilfe zu schaffen. Sie haben ein zusätzliches „n“ im Namen, wie z.B. char *strncpy(char * s1, const char * s2, size_t n); // strncpy hat denselben Effekt wie strcpy, kopiert allerdings maximal n Zeichen. Deshalb ist mit strncpy und n=“Anzahl der ab s1 reservierten Zeichen“ sichergestellt, dass kein nicht reservierter Speicher überschrieben wird. Damit sind aber nicht alle potentiellen Probleme ausgeschlossen. Das nächste Beispiel (aus n1135.pdf, siehe unten) zeigt, wie ein String ohne den Nullterminator kopiert wird. Wenn anschließend eine Funktion den Speicher ab der Adresse in dst bis zum nächsten Nullterminator anspricht, können nicht reservierte Speicherbereiche angesprochen werden. Beispiel: char* src="Hello"; char dst[5]; // 6 wäre in Ordnung strncpy(dst,src,5); // 6 wäre in Ordnung
An diesem Punkt setzen die „secure library functions“ an, um die der C-Standard für erweitert werden soll (siehe http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1135.pdf) und die in Visual C++ 2008 bereits zur Verfügung stehen. Diese ersetzen viele Bibliotheksfunktionen mit char*-Parametern für nullterminierte Strings (vor allem aus und ) um Funktionen mit im Wesentlichen derselben Funktionalität, die aber sicherer sind. Insbesondere soll die Anfälligkeit von Programmen für „buffer overruns“ vermindert werden. In
334
3 Elementare Datentypen und Anweisungen
C++ sollte man aber immer eine Stringklasse (siehe Abschnitte 3.13 und 4.1) gegenüber char* bevorzugen. Diese Klassen sind ebenfalls sicher und einfacher zu benutzen. Diese Funktionen kontrollieren nicht nur die Anzahl der bearbeiteten Zeichen, sondern außerdem auch noch, ob ein Nullterminator kopiert wird. errno_t strncpy_s(char * s1, size_t s1max, const char * s2, size_t n); Falls das nicht der Fall ist, kopieren sie nichts oder setzen das letzte Zeichen auf den Nullterminator. Zahlreiche weitere Funktionen arbeiten ähnlich. Alle ihre Namen enden mit „_s”. 3.12.18 Zeiger auf Zeiger auf Zeiger auf ... Ԧ Ein Zeiger kann wiederum auf einen Zeiger zeigen. Dabei erhält man Datentypen mit mehreren Sternen, wie z.B.: int** p; int*** q;
Das soll an einigen Beispielen illustriert werden. 1. Nach der Definition int** p=new(int*);//Ein Zeiger auf einen Zeiger auf int
ist *p ein „Zeiger auf int“ und **p ein int: int i=17; *p=&i; // **p==17
2. Doppelstern-Zeiger werden oft von C-Funktionen verwendet, die einen Zeiger zurückgeben, wie z.B. den Funktionen double strtod(const char *s, char **endptr); long strtol(const char *s, char **endptr, int radix); unsigned long strtoul(const char *s, char **endptr, int radix); Sie konvertieren die ersten Zeichen des Stringarguments s in einen Wert des Datentyps double, long oder unsigned long. Das Argument für endptr enthält dann nach dem Aufruf die Adresse des ersten Zeichens, das kein Zeichen des entsprechenden Literals ist. Da diese Funktionen führende whitespace-Zeichen ignorieren, kann man mit sukzessiven Aufrufen mehrere Zahlen aus einem String extrahieren: char* s=" 1.23 4.56xy"; char* q=s; char* p=q; double d1=strtod(p,&q); // d1==1.23 q==" 4.56xy" p=q; double d2=strtod(p,&q); // d2==4.56 q=="xy"
3.13 C++/CLI-Erweiterungen: Die Stringklasse String
335
p=q; double d3=strtod(p,&q); // d3==0 q=="xy"
3. Über einen Zeiger mit n Sternen kann man ein n-dimensionales Array ansprechen: int** p2; // =new ... (siehe die nächsten Anweisungen) p2[1][2]=17; int*** p3; // =new ... p3[1][2][3]=18;
Hier ist a2 ein Zeiger auf Array, dessen Elemente Zeiger auf int sind. Einem so definierten Zeiger kann man dann mit Anweisungen wie int** p2=new int*[Dim1]; // Dim1 Adressen mit den for (int i=0; iSubstring(2,3); int l1=s1->Length; // 3
int IndexOf(String^ value); int IndexOf(String^ value, int startIndex);
// s1="234"
3.13 C++/CLI-Erweiterungen: Die Stringklasse String
339
Ergibt die Position des ersten Zeichens von value im aktuellen String. Ist value nicht in diesem enthalten, ist der Funktionswert -1. Falls ein startIndex angegeben wird, beginnt die Suche ab dieser Position. Unzulässige Argumente: value hat den Wert nullptr. Beispiele: String^ fn="config.sys"; int p=fn->IndexOf("."); // p=6 String^ n=fn->Substring(0,p); // n="config" String^ e=fn->Substring(p+1);//e="sys" String^ f=fn->Substring(p+1,fn->Length-p-1);//e="sys" n="Donck";
Die einzelnen Zeichen eines Strings kann man sowohl mit dem Indexoperator als auch in einer for each Anweisung ansprechen: Beispiel: Diese Anweisungen geben die einzelnen Zeichen des Strings aus: String^ s="abc"; for (int i=0; iLength; i++) tb->AppendText(s[i]+"\r\n"); for each (wchar_t c in s) tb->AppendText(c+"\r\n");
Mit dem Operator + können Strings, ganzzahlige Ausdrücke und von System::Object abgeleitete Ausdrücke zu einem String zusammengefügt (verkettet, „aneinander gehängt“, „zusammengeklebt“) werden. Bei zwei Strings besteht das Ergebnis dann aus dem String, in dem auf die Zeichen des linken Operanden die des rechten folgen. Beispiele: Diese Anweisungen zeigen, wie Strings mit Ausdrücken verschiedener Datentypen und dem +-Operator kombiniert werden können: String^ s1="abc "+1; // "abc 1" String^ s2="abc "+button1; // "abc System.Windows. Forms.Button, Text: button1" String^ s3="abc "+1.23; // "abc 1,23"
In der nächsten Funktion werden 1000 Strings mit je 1000 Zeichen zum String s1M verkettet: String^ LangerString() { String^ s10="0123456789",^s1K,^s1M; // s10 ist ein String mit 10 Zeichen for (int i=1; iText == "Sonntag") Tag = Sonntag;
Für CLI-Aufzählungstypen (siehe Abschnitt 3.15.2) gibt es aber solche Funktionen. 3.15.1 enum Konstanten und Konversionen Ԧ Obwohl der Datentyp eines Enumerators der zugehörige Aufzählungstyp ist, kann er wie eine Ganzzahlkonstante verwendet werden. Ohne eine explizite Initialisierung hat der erste Enumerator in der Liste den Wert 0, und jeder weitere den um 1 erhöhten Wert des Vorgängers. Beispiel: Die Werte der Enumeratoren aus dem Beispiel oben entsprechen den Werten der folgenden Konstanten:
3.15 Aufzählungstypen
347
const int maennlich=0; const int weiblich=1; const int unklar=2;
Ein Enumerator kann bei seiner Definition mit einem ganzzahligen Wert initialisiert werden. So erhalten die Enumeratoren Montag, Dienstag usw. durch enum TT {Montag, Dienstag=17, Mittwoch=17, Donnerstag, Freitag=–2, Samstag, Sonntag=(Dienstag–7)*Mittwoch} Tag;
dieselben Werte wie diese Ganzzahlkonstanten const const const const const const const
int int int int int int int
Montag=0; Dienstag=17; Mittwoch=17; // gleicher Wert wie Dienstag Donnerstag=18; // Mittwoch+1 Freitag=–2; // negative Werte sind möglich Samstag=–1; Sonntag=170;//Ergebnis des konstanten Ausdrucks
Beispiel: In C werden solche Enumeratoren oft für Arraygrenzen verwendet. In C++ besteht dafür aber keine Notwendigkeit: enum {Max=100};//In C++ besser "const int Max=100" int a[Max];
Ein Wert eines Aufzählungstyps wird durch eine ganzzahlige Typangleichung (siehe Abschnitt 3.3.3) in den ersten der Datentypen int, unsigned int, long oder unsigned long konvertiert, der alle bei der Initialisierung verwendeten Werte darstellen kann. Deshalb sind Aufzählungstypen nicht typsicher. Beispiel: Mit den Aufzählungstypen von oben sind diese Ausdrücke zulässig: int i=Montag; // i=0 if (Tag > 0) ... // Vergleich von enum mit int if (Tag > maennlich) ...// Vergleich von // verschiedenen Aufzählungstypen
Eine Konversion in der umgekehrten Richtung ist nicht möglich. Nach dem C++Standard können einer Variablen eines Aufzählungstyps nur Werte desselben Aufzählungstyps zugewiesen werden. Insbesondere können keine Ganzzahlwerte zugewiesen werden, obwohl ein Enumerator damit initialisiert werden kann. Beispiel: Nach den Definitionen von oben werden die folgenden Zuweisungen nicht kompiliert: Tag=1; // error: Konvertierung nicht möglich Tag=unklar; // error: Konvertierung nicht möglich
348
3 Elementare Datentypen und Anweisungen
3.15.2 C++/CLI-Aufzählungstypen Ԧ C++/CLI-Aufzählungstypen sind eine Erweiterung von C++/CLI, die im Unterschied zu den Standard-C++ Aufzählungstypen typsicher sind und in Abschnitt 9.7.2 ausführlich dargestellt werden. Hier nur kurz die wichtigsten Unterschiede: – Ein CLI-Aufzählungstypen wird definiert, indem man bei der Definition nach enum das Wort class angibt: enum class Wochentag {Sonntag, Montag, Dienstag=17} t; enum class WochenendUndSonnenschein {Samstag,Sonntag} w;
CLI-Aufzählungstypen können nur global definiert werden. Lokale Definitionen in einer Funktion sind nicht möglich. – Der Gültigkeitsbereich von solchen Enumeratoren ist auf ihren Aufzählungstyp beschränkt. Die Enumeratoren werden dann mit dem Namen des Aufzählungstyps und dem Bereichsoperator :: angesprochen: So können verschiedene Aufzählungstypen dieselben Enumeratoren verwenden: t=Wochentag::Sonntag; w=WochenendUndSonnenschein::Sonntag;
– Für CLI-Aufzählungstypen ist keine implizite Konversion in einen ganzzahligen Datentyp definiert. Deswegen wird der folgende Vergleich vom Compiler nicht akzeptiert: if (t>0) // error: operator '>': Alle Operanden müssen // den gleichen Enumerationstyp aufweisen
3.16 Kommentare und interne Programmdokumentation Vor allem bei größeren oder komplexeren Programmen besteht gelegentlich das Bedürfnis, Anweisungen oder Deklarationen durch umgangssprachliche Bemerkungen zu erläutern. Deshalb bieten praktisch alle Programmiersprachen die Möglichkeit, Kommentare in ein Programm zu schreiben. Ein Kommentar ist ein Text, der vom Compiler ignoriert wird und keine Auswirkungen auf das ausführbare Programm hat. In C++ wird ein Kommentar entweder durch /* und */ oder durch // und das nächste Zeilenende begrenzt: /* das ist ein Kommentar */ // das ist ein Zeilenendkommentar
Ausnahmen: Wenn diese Zeichen in einem String oder Kommentar enthalten sind:
3.16 Kommentare und interne Programmdokumentation
349
const char* s="/* kein Kommentar */" // die /* ganze Zeile */ ist ein Kommentar /* auch diese Zeile // ist ein Kommentar */
Ein mit /* begonnener Kommentar wird durch das nächste Auftreten von */ beendet. Deshalb können solche Kommentare nicht verschachtelt werden: /* /* dieser Kommentar endet hier */ und vor dem letzten "und" meckert der Compiler. */
Insbesondere können mit den Kommentarbegrenzern /* und */ keine Programmteile auskommentiert werden, die selbst solche Kommentare enthalten. Da man aber oft ganze Programmteile auskommentieren will, ohne die Kommentare zu entfernen, verwendet man für Programmerläuterungen meist Zeilenendkommentare: /* p = 2; // kleinste Primzahl ... p = p0 notwendig sein // ... return n; }
Die Ausführung von throw bewirkt, dass das Programm im nächsten umgebenden Exception-Handler fortgesetzt wird, der zu der Exception passt. Falls es keinen solchen Exception-Handler gibt, wird das Programm beendet. Ein Exception-Handler ist ein zu einer try-Anweisung gehörender Teil, der mit catch beginnt und von einer Verbundanweisung gefolgt wird. Falls bei der Ausführung der auf try folgenden Verbundanweisung eine Exception ausgelöst wird,
3.19 Weitere Anweisungen
369
die zum Exception-Handler passt, wird die zum Exception-Handler gehörende Verbundanweisung ausgeführt und die Exception anschließend gelöscht. Beispiel: Der Aufruf f1(0) löst eine Exception aus, die zum Exception-Handler passt. Deswegen wird als nächste Anweisung nach throw die Ausgabeanweisung nach catch ausgeführt. Anschließend werden die auf catch folgenden weiteren Anweisungen ausgeführt. Die auf f1(0) folgenden Anweisungen werden nicht ausgeführt: try { f1(0); // weitere Anweisungen } catch(exception& e) { textBox1->AppendText(gcnew String(e.what())); } // weitere Anweisungen
Diese Anweisungen geben „Vorbedingung n>0 nicht erfüllt“ aus. Falls bei der Ausführung der auf try folgenden Verbundanweisung keine Exception ausgelöst wird, werden diese Anweisungen der Reihe nach ausgeführt. Danach wird die gesamte try-Anweisung verlassen, ohne die Anweisungen im ExceptionHandler auszuführen. Der Programmablauf ist derselbe wie ohne eine umgebende try-Anweisung. Beispiel: Da der Aufruf von f1(1) keine Exception auslöst, werden durch try { f1(1); // weitere Anweisungen 1. } catch(exception& e) { textBox1->AppendText(gcnew String(e.what())); } // weitere Anweisungen 2.
die folgenden Anweisungen ausgeführt: f1(1); // weitere Anweisungen 1. // weitere Anweisungen 2.
Falls eine Funktion eine Exception auslöst und nicht innerhalb einer try-Anweisung aufgerufen wird, ist ein Programmabbruch die Folge: Beispiel: Wenn der Aufruf von f1(0) nicht innerhalb einer try-Anweisung erfolgt, bewirkt das einen Programmabbruch: f1(0); // löst eine Exception aus
370
3 Elementare Datentypen und Anweisungen
Ob eine Exception zu einem Exception-Handler passt, ergibt sich aus ihrem Datentyp. – In den Beispielen oben wurde darauf hingewiesen, dass eine Exception des Typs logic_error zu einer Exception des Typs exception passt. Die C++Standardbibliothek löst aber auch noch andere Exceptions als logic_error aus. Alle diese Exceptions passen zum Datentyp exception und können deshalb mit dem Exception-Handler von oben abgefangen werden. – Visual C++ löst bei C++/CLI-Anwendungen (dazu gehören auch Windows Forms-Anwendungen) Exceptions aus, die zu Exception passen. Diese Exceptions kann man mit dem folgenden Exception-Handler abfangen: Beispiel: try { f2(x);... } catch(Exception^ e) { textBox1->AppendText(e->Message); }
Solche Exceptions werden z.B. auch bei einer Ganzzahldivision durch Null und bei einer Zugriffsverletzung ausgelöst. Beispiel: Der Aufruf der Funktion f2 mit den Argumenten 0 und 1 löst eine Exception aus, die zu Exception passt: int f2(int n ) { if (n==0) return 1/n; else if (n==1) { int* p=0; *p=1; // Zugriffsverletzung } }
– Der Exception-Handler catch(...) passt zu jeder Exception. In ihm stehen allerdings keine Informationen der Exception (wie z.B. die Meldungen wie e.what() oder e->Message) zur Verfügung. Falls man aber nur feststellen will, ob alles gut ging oder nicht, ist dieser Exception-Handler ausreichend: Beispiel: Nach einer beliebigen Exception wird eine Meldung ausgegeben: try { f1(0); } catch(...) { textBox1->AppendText( "Something's wrong in paradise"); }
3.19 Weitere Anweisungen
371
– Falls man bei Exceptions der Standardbibliothek und Visual C++ ihre jeweils eigenen Meldungen verwenden und außerdem auch noch alle weiteren Exceptions abfangen will, kann man verschiedene Exception-Handler angeben: try { // ... } catch(exception& e) // C++ Standardbibliothek { textBox1->AppendText(gcnew String(e.what())); } catch(Exception^ e) // Visual C++ { textBox1->AppendText(e->Message); } catch(...) // alle weiteren Exceptions { textBox1->AppendText( "Something's wrong in paradise"); }
Die Anweisungen eines Exception-Handlers werden nur ausgeführt, wenn eine Exception mit throw im zugehörigen Block nach try ausgelöst wurde. Es gibt keine andere Möglichkeit, Anweisungen nach catch auszuführen. Wenn man 1. alle Funktionen so schreibt, dass sie bei jedem Fehler eine Exception auslösen, 2. und alle Funktionen in einem try-Block aufruft, dann ist die fehlerfreie Ausführung der Funktionsaufrufe gleichbedeutend damit, dass kein Exception-Handler ausgeführt wird. Exception-Handling bietet also eine einfache Möglichkeit, festzustellen, ob ein Fehler aufgetreten ist oder nicht. In einer Verbundanweisung nach try fasst man meist solche Anweisungen zusammen, die gemeinsam ein bestimmtes Ergebnis erzielen sollen. Falls dann eine dieser Anweisungen ihr Teilergebnis nicht beitragen kann, macht es meist keinen Sinn, die darauf folgenden Anweisungen auszuführen. Dann kann man die Ausführung dieser Anweisungen beenden und in einem Exception-Handler darauf hinweisen, dass etwas schief ging. Aufgaben 3.19.2 1. Lösen Sie in Ihrer Funktion Fibonacci (Aufgabe 3.4.7, 2.) eine Exception der Klasse logic_error aus, wenn sie mit einem Argumenten n>47 aufgerufen wird, bei dem ihre Vorbedingung nicht erfüllt ist (siehe auch Aufgabe 3.7.6 1.). Übergeben Sie dabei eine entsprechende Meldung. Rufen Sie diese Funktionen dann mit Argumenten, die eine Exception auslösen, a) in einer try-Anweisung auf. Geben die die Meldung in einer TextBox aus.
372
3 Elementare Datentypen und Anweisungen
b) außerhalb von einer try-Anweisung auf. 2. Schreiben Sie eine Funktion, die eine Division durch Null und eine Zugriffsverletzung ausführt. Rufen Sie diese Funktion in einer try-Anweisung auf und geben Sie die Message in einer TextBox aus. 3.19.3 Die switch-Anweisung Ԧ Die Auswahl einer aus mehreren Anweisungen ist nicht nur mit einer verschachtelten if-Anweisung möglich, sondern auch mit einer switch-Anweisung. Allerdings müssen die folgenden Voraussetzungen erfüllt sein: 1. Die Bedingung, aufgrund der die Auswahl der Anweisung erfolgt, muss dadurch gebildet werden, dass ein Ausdruck auf Gleichheit mit einer Konstanten geprüft wird. Bedingungen mit den Operatoren = können also nicht verwendet werden, ebenso wenig wie Bedingungen, bei denen ein Ausdruck nicht mit einer Konstanten verglichen wird. 2. Der Datentyp der zum Vergleich herangezogenen Ausdrücke muss ein Ganzzahl- oder ein Aufzählungstyp sein. Gleitkommadatentypen und Strings können nicht verwendet werden. Obwohl diese Voraussetzungen auf den ersten Blick recht einschränkend wirken, sind sie in der Praxis häufig erfüllt: Bei vielen Programmen kann ein Großteil der Auswahlanweisungen mit einer switch-Anweisung formuliert werden. switch ( condition ) statement
Hier muss der Datentyp des Ausdrucks condition ein Ganzzahl- oder ein Aufzählungstyp sein. Die Anweisung nach (condition) ist meist eine Verbundanweisung. In ihr kann man vor jeder Anweisung eine oder mehrere case-Marken angeben: case constant-expression :
Dieser konstante Ausdruck muss einen ganzzahligen Datentyp haben. Die Werte aller Konstanten einer switch-Anweisung müssen verschieden sein. Außerdem kann vor höchstens einer der Anweisungen eine default-Marke stehen: default :
Bei der Ausführung einer switch-Anweisung wird die Anweisung ausgeführt, die auf die case-Marke mit dem Wert von condition folgt. Gibt es keine case-Marke mit diesem Wert, wird die auf default folgende Anweisung ausgeführt oder, wenn sie keine default-Marke besitzt, ohne die Ausführung einer Anweisung verlassen. Nach der Ausführung der Anweisung, die auf eine case- oder eine default-Marke folgt, werden die darauf folgenden Anweisungen ausgeführt, unabhängig davon, ob vor ihnen weitere case- oder default-Marken stehen. Insbesondere wird eine
3.19 Weitere Anweisungen
373
switch-Anweisung nicht mit dem Erreichen der nächsten Marke beendet. Die switch-Anweisung verhält sich in dieser Hinsicht wie eine goto-Anweisung. Wie schon am Anfang dieses Abschnitts bemerkt wurde, wird die switch-Anweisung oft zur Auswahl einer aus mehreren Anweisungsfolgen verwendet. Diese Anweisungsfolgen werden dann durch verschiedene case-Marken begrenzt. Damit die switch-Anweisung nach der Ausführung einer solchen Anweisungsfolge verlassen wird, verwendet man eine break-Anweisung (siehe auch Abschnitt 3.19.6). Beispiel: Die switch-Anweisung in const char* NoteToString(int Note) { switch (Note) { case 1:return "sehr gut!!!"; break; case 2:return "gut"; break; case 3:return "na ja"; break; case 4:return "schwach"; break; case 5: case 6:return "durchgefallen"; break; default: return "Unzulässige Note "; } }
hat dasselbe Ergebnis wie die verschachtelte if-Anweisung: if else else else else
(Note==1) return "sehr gut!!!"; (Note==2) return "gut"; (Note==3) return "na ja"; (Note==4) return "schwach"; ((Note==5) or (Note==6)) return "durchgefallen"; else return "Unzulässige Note "; if if if if
Wie dieses Beispiel zeigt, können für verschiedene Werte von condition (hier die Werte 5 und 6) dieselben Anweisungen ausgeführt werden, indem verschiedene case-Marken ohne weitere Anweisungen (insbesondere ohne ein break) aufeinander folgen. In einer switch-Anweisung wird eine case-Marke angesprungen, auch wenn sie in einer anderen Anweisung enthalten ist. Beispiel: Die folgenden Anweisungen werden ohne Warnung oder Fehlermeldung kompiliert. Sie setzen s auf 7, da nach der Ausführung von s=s+3 auch noch s=s+4 ausgeführt wird.
374
3 Elementare Datentypen und Anweisungen int x=1,s=0; switch (x) // kompletter Schwachsinn { case 3:s=s+1; if (x==2) case 0:s=s+2; else case 1:s=s+3; case 2:s=s+4; break; default: s=-1; }
Wie dieses Beispiel zeigt, unterscheidet sich die switch-Anweisung in ihrem Sprungverhalten nicht von einer goto-Anweisung (siehe Abschnitt 3.19.6). Deshalb sind damit auch dieselben undefinierten Ergebnisse wie mit einer gotoAnweisung möglich. Es muss wohl nicht besonders darauf hingewiesen werden, dass von solchen Konstruktionen nur dringend abgeraten werden kann. Da die switch-Anweisung nicht verlassen wird, wenn die Anweisungen nach einer case-Marke abgearbeitet sind und die nächste erreicht wird, muss man immer darauf achten, dass nicht versehentlich ein break vergessen wird. Ohne break werden alle folgenden Anweisungen der switch-Anweisung ausgeführt, unabhängig davon, ob vor ihnen weitere case- oder default-Marken stehen. Beispiel: Durch diese Anweisungen erhält i den Wert 4: int k=1, i=0; switch (k) { case 1: i=i+1; case 2: i=i+1; case 5: case 6: i=i+1; default: i=i+1; }
// i=1 // i=2 // i=3 // i=4
Die Ausführungen zur logischen Analyse und zum Nachweis der Nachbedingungen von if-Anweisungen von Abschnitt 3.7.8 lassen sich auch auf die switchAnweisung übertragen, da die Bedingungen in switch-Anweisungen Abfragen auf Gleichheit sind. Aufgaben 3.19.3 Lösen Sie die folgenden Aufgaben mit switch- anstelle von if-Anweisungen. Falls eine der Aufgaben nicht lösbar ist, geben Sie den Grund dafür an. 1. Aufgabe 3.4.1, 3 (Material- und Lagergruppe) 2. Aufgabe 3.4.1, 4 (Datumsvergleich) 3. Aufgabe 3.6.6, 8 (Steuerformel)
3.19 Weitere Anweisungen
375
3.19.4 Die do-Anweisung Ԧ Die do-Anweisung ist eine Wiederholungsanweisung do statement while ( expression ) ;
in der expression ein Ausdruck ist, der in den Datentyp bool konvertiert werden kann. Dieser Ausdruck ist die Schleifenbedingung, und die Anweisungen zwischen do und while sind der Schleifenkörper. Bei der Ausführung einer do-Anweisung wird zunächst der Schleifenkörper ausgeführt. Dann wird die Schleifenbedingung ausgewertet. Ergibt sich dabei der Wert false, wird die do-Anweisung verlassen. Andernfalls werden diese Schritte wiederholt, bis die Schleifenbedingung den Wert false hat. Beispiel: Die mit einer while-Anweisung erzielte Ausführung kann auch mit einer do- und einer if-Anweisung erreicht werden (linke Spalte), und die einer do-Anweisung mit einer while-Schleife(rechte Spalte): while (b) S;
do S; while (b);
if (b) do S; while (b);
S; while (b) S;
Offensichtlich wäre bereits eine der beiden Wiederholungsanweisungen ausreichend. Die zweite Formulierung ist jedoch umständlicher, da die Bedingung b oder die Anweisung S zweimal aufgeführt werden müssen. Im Allgemeinen sollte man eine while-Schleife gegenüber einer do-Schleife bevorzugen. Sie hat den Vorteil, dass man bei der Ausführung von S immer die Bedingung b voraussetzen kann. Typische Anwendungen von do-Schleifen sind Konsolenprogramme, die ein Menü anbieten. Beispiel: do { coutAppendText("f1 wird verlassen \r\n"); }
554
5 Funktionen void f2(TextBox^ tb) { tb->AppendText("In f2 angekommen \r\n"); f1(tb); f1(tb); tb->AppendText("f2 wird verlassen \r\n"); } void f3(TextBox^ tb) { tb->AppendText("In f3 angekommen \r\n"); f1(tb); f2(tb); tb->AppendText("f3 wird verlassen \r\n"); }
werden nach einem Aufruf von f3 die Anweisungen folgendermaßen abgearbeitet: f3
"In f3.. " f1
"In f1.. " "f1 verl.. "
f2
"In f2.. " f1 f1
"In f1.. " "f1 verl.." "In f1.. " "f1 verl.."
"f2 verl.. " "f3 verl.. "
Wenn man nur darstellen will, welche Funktionen aufgerufen werden, verwendet man oft ein Strukturdiagramm. Ein solches Strukturdiagramm wird ausgehend vom obersten Knoten durchlaufen. Wenn von einem Knoten mehrere Zweige ausgehen, werden sie von links nach rechts abgearbeitet. Jeder Zweig wird bis zu seinem Endpunkt durchlaufen. Danach geht die Kontrolle an den aufrufenden Knoten zurück. Beispiel: Für die Funktion f3 aus dem letzten Beispiel erhält man das Strukturdiagramm: f3 f1
f2 f1
f1
Mit Debuggen|Fenster|Aufrufliste wird der aktuelle Stack angezeigt:
5.2 Funktionszeiger und der Datentyp einer Funktion
555
Hier steht die zuletzt aufgerufene Funktion (also die, in der man sich gerade befindet) ganz oben. Darunter stehen die Funktionen, deren Adressen sich noch auf dem Stack befinden. So kann man feststellen, über welche Funktionen man in die aktuelle Funktion gekommen ist. 5.1.1 Aufrufkonventionen Ԧ Mit den folgenden Schlüsselwörtern kann man bestimmen, wie die Parameter bei einem Funktionsaufruf auf dem Stack übergeben werden. Normalerweise sollte man die Voreinstellungen des Compilers aber nicht ändern. Siehe auch Abschnitt 3.24.6. _cdecl
bewirkt, dass die Parameter von rechts nach links auf den Stack gelegt werden. Der Stack wird von der aufrufenden Funktion wieder abgeräumt. Diese Art der Parameterübergabe ist z.B. für Funktionen mit einer unspezifizierten Anzahl von Parametern notwendig. _stdcall wie _cdecl. Allerdings ist damit keine unspezifizierte Anzahl von Argumenten möglich. _pascal bewirkt, dass die Parameter von links nach rechts auf den Stack gelegt werden. Der Stack wird von der aufgerufenen Funktion wieder abgeräumt. _fastcall bewirkt, dass die ersten drei Parameter in Registern übergeben werden.
5.2 Funktionszeiger und der Datentyp einer Funktion Mit Funktionszeigern kann man Variablen definieren, denen man Funktionen zuweisen kann. Außerdem kann man mit Funktionszeigern einer Funktion auch Funktionen als Parameter übergeben. Da dafür der Datentyp einer Funktion grundlegend ist, wird zunächst dieser Begriff vorgestellt. 5.2.1 Der Datentyp einer Funktion Formal gehören Funktionen zu den zusammengesetzten Datentypen. Der Datentyp einer Funktion ergibt sich aus dem Datentyp der Parameter und dem Rückgabetyp. Da der Compiler aber bei einem Parameter bestimmte Datentypen in andere konvertiert oder Angaben ignoriert, können Funktionen denselben Datentyp haben, obwohl sie auf den ersten Blick verschieden aussehen. Die wichtigsten Faktoren in diesem Zusammenhang sind:
556
5 Funktionen
1. Jeder Parameter des Datentyps „Array mit Elementen des Datentyps T“ wird in den Datentyp „Zeiger auf T“ konvertiert (siehe auch Abschnitt 3.12.8). Deshalb haben die folgenden Funktionen alle denselben Datentyp „Zeiger auf Funktion mit einem Parameter des Datentyps int* und Rückgabetyp double“ bzw. kürzer „double(*)(int*)“: double double double double
f11(int* a) {} // Datentyp f11: double(*)(int*) f12(int x[]){} f13(int a[10]){} f14(int a[11]){}
Bei mehrdimensionalen Arrays geht dagegen die zweite und jede weitere Dimension in den Datentyp des Parameters ein. Die Datentypen der folgenden vier Funktionen sind deshalb alle verschieden: int int int int
f15(int* x[21]) {return x[3][0];} f16(int x[][20]) {return x[2][0];} f17(int x[17][18]) {return x[0][0];} f18(int x[17][19]) {return x[1][0];}
2. Beim Datentyp eines Parameters werden const- oder volatile-Angaben auf der „äußersten Ebene des Datentyps“ ignoriert. Diese Formulierung aus dem C++Standard bezieht sich auf die Position von const bzw. volatile in der verbalen Beschreibung des Datentyps und nicht auf die Position in der Deklaration. Da der Parameter x in f22 bzw. f23 den Datentyp „const int“ bzw. „volatile int“ hat und die Angaben „const“ bzw. „volatile“ hier außen stehen, wirken sich diese Angaben nicht auf den Funktionstyp aus. Deshalb haben die Funktionen f21, f22 und f23 alle denselben Datentyp „int(*)(int)“: int f21(int x) {return x*x;} int f22(const int x) {return x+1;} int f23(volatile int x) {return x;}
Wenn solche Angaben wie in „Zeiger auf const T“ im Inneren der verbalen Beschreibung des Datentyps des Parameters „enthalten“ sind, werden die Datentypen sehr wohl unterschieden. Insbesondere sind für jeden Datentyp T die Parametertypen „Zeiger auf T“ und „Zeiger auf const T“ sowie „Referenz auf T“ und „Referenz auf const T“ verschieden. Deswegen haben die Funktionen g1 und g2 sowie g3 und g4 einen verschiedenen Datentyp: void void void void
g1(int& i) {} g2(const int& i) {} g3(int* i) {} g4(const int* i) {}
// // // //
Datentyp g1: void(*)(int&) DT g2: void(*)(const int&) DT g3: void(*)(int*) DT g4: void(*)(const int*)
In der verbalen Beschreibung des Parametertyps der Funktionen g5 bzw. g6 steht const dagegen auf der äußersten Ebene: „const Zeiger auf int“ bzw. „const Referenz auf T“. Deshalb hat g5 denselben Datentyp wie g1 und g6 denselben wie g3.
5.2 Funktionszeiger und der Datentyp einer Funktion void g5(int& const i) {} void g6(int* const i) {}
557
// DT g5: void(*)(int&) // DT g6: void(*)(int*)
3. Da ein mit typedef deklarierter Name für einen Datentyp nur ein Synonym für diesen Datentyp ist, sind für den Compiler zwei Datentypen gleich, die sich nur dadurch unterscheiden, dass einer der beiden mit typedef als Synonym für den anderen Datentyp definiert wurde. Deshalb haben die Funktion f31 und f32 denselben Datentyp: typedef int Tint; int f31(int x) {return x*x;} // DT f31: int(*)(int) int f32(Tint x) {return x+1;} // DT f32: int(*)(int)
4. Jeder Parameter des Datentyps „Funktion mit Rückgabetyp T“ wird in den Datentyp „Zeiger auf eine Funktion mit Rückgabetyp T“ konvertiert (siehe auch Abschnitt 5.2.2). Deshalb haben die folgenden beiden Funktionen denselben Datentyp „double (*)(double (*)(double))“: double sum1(double f(double x)) {} double sum2(double (*f)(double x)) {}
5. Default-Argumente und Exception-Spezifikationen (siehe Abschnitt 7.8) gehören nicht zum Datentyp einer Funktion. Deshalb haben die folgenden Funktionen alle denselben Datentyp: double f1(int a) { return 0;} // DT double(*)(int) double f2(int a=0) { return 0;} // DT double(*)(int) double f3(int a) throw() {return 0;}//DT double(*)(int)
6. Die Speicherklassenspezifizierer auto oder register bei einem Parameter wirken sich nicht auf den Datentyp der Funktion aus. Der hier als abgekürzte Schreibweise angegebene Funktionstyp wird auch bei den Fehlermeldungen von Visual C++ oder bei typeid verwendet. Durch // #include ist vorher notwendig tb->AppendText(toString(typeid(f1).name())+"\r\n");
wird der String „double __clrcall(int)“ ausgegeben. 5.2.2 Zeiger auf Funktionen In Zusammenhang mit der Verwaltung von Funktionsaufrufen (siehe Abschnitt 5.1) wurde gezeigt, dass ein Funktionsaufruf zu einem Sprung an die Adresse der ersten Anweisung einer Funktion führt. Diese Adresse wird auch als die Adresse der Funktion bezeichnet. Nach diesem Sprung werden die Anweisungen der Funktionsdefinition ausgeführt. Die Adresse einer Funktion f erhält man in C++ mit dem Ausdruck &f. Der Datentyp dieses Ausdrucks ist ein Zeiger auf den Datentyp dieser Funktion. Diese
558
5 Funktionen
Adresse kann einer Variablen zugewiesen werden, die denselben Datentyp wie der Ausdruck &f hat. Ein Zeiger auf einen Funktionstyp kann z.B. folgendermaßen definiert werden: int (*g)(int);
Hier ist g ein Zeiger auf eine Funktion mit einem Parameter des Datentyps int und dem Rückgabetyp int. Die Klammern um *g sind notwendig, weil der Operator () stärker bindet als * und g sonst als Prototyp für eine Funktion interpretiert wird, die wie die Funktion h einen Zeiger auf int als Funktionswert hat: int* h(int); // ein Prototyp und kein Funktionszeiger
Dem Funktionszeiger g kann die Adresse einer Funktion zugewiesen werden, die denselben Datentyp hat. Der Aufruf von g führt dann zum Aufruf der zugewiesenen Funktion. Außerdem kann der Wert 0 (Null) jedem Zeiger auf eine Funktion zugewiesen werden. Er wird aber meist nur verwendet, um auszudrücken, dass der Zeiger auf keine Funktion zeigt. Beispiel: Nach den Definitionen int f1(int i) { return i; } int f2(int i) { return i*i; } int (*f)(int);
erhält der Funktionszeiger f durch die Zuweisung f=&f1;
den Wert der Adresse von f1. Der Aufruf von f führt dann zum Aufruf von f1: int x=f(2); // x=2
Nach der Zuweisung f=&f2;
führt der Aufruf von f dagegen zum Aufruf der Funktion f2: int y=f(2); // y=4
Verwendet man den Namen einer Funktion auf der rechten Seite einer Zuweisung, wird er durch eine Standardkonversion in einen Zeiger auf die Funktion
5.2 Funktionszeiger und der Datentyp einer Funktion
559
konvertiert. Deshalb muss man im letzten Beispiel den Adressoperator nicht angeben. Die folgenden beiden Zuweisungen sind gleichwertig: f=f1; f=&f1;
Beim Aufruf einer Funktion über einen Funktionszeiger muss dieser nicht dereferenziert werden. Das ist zwar möglich. Man erhält so aber dasselbe Ergebnis, wie wenn man nur den Namen der Funktion verwendet: double x=(*f)(1); double x=f(1);
Funktionstypen lassen sich meist übersichtlicher formulieren, wenn man mit typedef ein Synonym für den Funktionstyp definiert. Dabei wird der Name des Funktionstyps an der Stelle angegeben, an der bei einer Funktionsdeklaration der Name der Funktion steht. Mit typedef double TDoubleFunction(double);
sind die folgenden beiden Definitionen der Funktionszeiger f und g gleichwertig: TDoubleFunction *f; double (*g)(double);
Diesen beiden Funktionszeigern kann dann z.B. die Funktion double id(double a) { return a; }
zugewiesen werden: f=id; g=id;
Mit durch typedef definierten Funktionstypen lassen sich insbesondere Arrays mit Funktionszeigern einfacher definieren als mit explizit angegebenen Funktionstypen. Die folgenden beiden Definitionen sind gleichwertig: TDoubleFunction *fktArr1[10]={sin,cos}; double (*fktArr2[10])(double) = {sin,cos};
Der ersten dieser beiden Definitionen sieht man unmittelbar an, dass hier ein Array mit 10 Funktionszeigern definiert wird, dessen erste beide Elemente mit den Funktionen sin und cos initialisiert werden Die gleichwertige zweite Definition mutet dagegen eher kryptisch an. Wenn man eine solche Definition hinschreiben will, ist nicht immer auf Anhieb klar, in welcher Reihenfolge der Operator *, der Name des Arrays, der Operator [] und die Klammern () aufeinander folgen müssen.
560
5 Funktionen
Mit Funktionszeigern kann man einer Funktion insbesondere auch Funktionen als Parameter übergeben. In der Funktion sum1 ist der Datentyp des Parameters f ein Zeiger auf eine Funktion: double sum1(double (*f)(double x)) { double s=0; for (int i=0; iright,x); }
traverseTree durchläuft alle Knoten von links nach rechts (in sortierter Reihenfolge) und führt mit jedem Knoten die Anweisungen von processTreenode aus: void processTreenode(TextBox^ tb, Treenode* n) { tb->AppendText(n->data.ToString()+"\r\n"); } void traverseTree_rec(TextBox^ tb, Treenode* n) { if (n!=0) { traverseTree_rec(tb,n->left); processTreenode(tb,n); traverseTree_rec(tb,n->right); } }
Die Funktion searchBinTree_rec sucht wie die Funktion searchBinTree einen Knoten mit den als Argument übergebenen Daten. Der Rückgabewert ist ein Zeiger auf diesen Knoten, wenn er gefunden wird, und andernfalls der Wert 0. Da der Binärbaum so aufgebaut ist, dass alle kleineren Schlüsselwerte links und alle größeren rechts vom aktuellen Knoten eingehängt sind, braucht man nach einem kleineren Schlüssel nur links und nach einem größeren nur rechts weitersuchen: Treenode* searchBinTree_rec(Treenode* n, const T& x) { if (n==0) return 0; else if (x==n->data) return n; else if (xdata) return searchBinTree_rec(n->left,x); else return searchBinTree_rec(n->right,x); }
Falls der Baum ausgeglichen ist, halbiert sich der verbleibende Suchbereich mit jedem Rekursionsschritt. Der Knoten mit dem gesuchten Schlüsselwert wird in einem Baum mit n Knoten dann in etwa log2(n) Rekursionsschritten gefunden. Damit ist die Suche in einem binären Suchbaum ähnlich schnell wie die binäre Suche in einem sortierten Array. Wenn zu einem Schlüsselbegriff verschiedene Daten gehören können, kann man in jedem Baumknoten eine verkettete Liste mit diesen Daten aufbauen. Verwaltet
584
5 Funktionen
man diese Liste über zwei Zeiger first und last, die auf den ersten und letzten Knoten zeigen, sieht ein Baumknoten etwa folgendermaßen aus: typedef int dataType ; // Datentyp der Listendaten typedef int keyType ; // Datentyp der Schlüsselwerte struct Listnode { dataType data; Listnode* next; }; struct TreenodeWithList { keyType key; Listnode *first, *last; TreenodeWithList *left, *right; };
In einen Baum mit solchen Knoten kann man dann mit einer Funktion wie insertTreenode einen neuen Knoten einfügen. Die hier aufgerufene Funktion insertLastListnode wurde in Abschnitt 3.12.12 vorgestellt. void insertTreenode(TreenodeWithList*& n, keyType key, dataType data) { if (n==0) { n = new TreenodeWithList; n->key = key; n->left = 0; n->right = 0; n->first = 0; n->last = 0; // Erzeuge einen neuen Listenknoten, auf den n->first // und n->last anschließend zeigen: insertLastListnode(n->first, n->last,data); } else if (key < n->key) insertTreenode(n->left,key,data); else if (key > n->key) insertTreenode(n->right,key,data); else insertLastListnode(n->first, n->last,data); }
Hier bewirken die rekursiven Aufrufe, dass ein neuer Baumknoten gemäß dem Ordnungsbegriff key im linken oder rechten Teilbaum eingehängt wird. Falls ein Baumknoten mit dem Schlüsselwert key bereits existiert, wird ein neuer Listenknoten in die Liste zu diesem Schlüsselwert eingehängt. Im Zweig nach „if (n==0)“ wird ein neuer Baumknoten erzeugt, dessen Zeiger alle auf 0 gesetzt werden. Außerdem wird der Schlüsselwert eingetragen, und ein Listenknoten in die verkettete Liste eingehängt, auf die n->first und n->last zeigen. Die Liste in einem Baumknoten kann man dann mit einer Funktion wie
5.3 Rekursion
585
void printNode(TextBox^ tb, TreenodeWithList* n) { String^ s=n->key+": "; for (Listnode* i=n->first; i!=0; i=i->next) s=s+i->data+" "; tb->AppendText(s+"\r\n"); }
durchlaufen. Durch den Aufruf einer Funktion wie printTree (mit der Wurzel des Baums als Argument) werden alle Baumknoten und zu jedem Baumknoten die zugehörigen Listenknoten durchlaufen. void printTree(TextBox^ tb, TreenodeWithList* n) { // wie traverseTree if (n!=0) { printTree(tb,n->left); printNode(tb,n); printTree(tb,n->right); } }
Wie schon am Ende von Abschnitt 3.12.13 erwähnt wurde, lassen sich Binärbäume oft durch die assoziativen Containerklassen der Standardbibliothek ersetzen. Aufgabe 5.3.6 Ergänzen Sie Ihre Lösung der Aufgabe 3.12.13 um die folgenden rekursiven Funktionen und rufen Sie diese Funktionen beim Anklicken eines Buttons auf: a) Die Funktion insertBinTreenode_rec soll wie insertBinTreenode (Aufgabe 3.12.13, 1. b) einen Knoten mit den als Argument übergebenen Werten für key und data in einen Binärbaum einhängen. b) Die Funktion traverseTree_rec soll für alle Knoten im Baum die Werte für key und data ausgeben. c) Die Funktion searchBinTree_rec soll wie searchBinTree (Aufgabe 3.12.13, 1. c) einen Zeiger auf einen Knoten mit dem als Argument übergebenen Schlüsselbegriff zurückgeben, wenn ein solcher Knoten gefunden wird, und andernfalls den Wert 0. 5.3.7 Verzeichnisse rekursiv nach Dateien durchsuchen Ԧ Die Klasse DirectoryInfo (siehe auch Abschnitt 10.14.6) aus dem Namensbereich System::IO enthält Methoden, mit denen man ein Verzeichnis (Laufwerk, Unterverzeichnis) nach allen Dateien durchsuchen kann. Dazu wird zunächst ein Objekt mit dem Konstruktor DirectoryInfo(String^ path)
586
5 Funktionen
erzeugt, dem der Pfad als Argument übergeben wird. Die Methoden array^ GetFiles() // für alle Dateien zurück array^ GetFiles(String^ searchPattern) array^ GetFiles(String^ searchPattern,SearchOption searchOption) geben dann für alle Dateien, die zu dem als Argument für searchPattern (z.B. „f*“ oder „*.cpp“) übergebenen String passen, ein Element des Typs FileInfo im Rückgabe-Array zurück. Als searchOption kann man einen der Werte TopDirectoryOnly oder AllDirectories des C++/CLI Aufzählungstyps SearchOption angeben. Die Klasse FileInfo enthält unter anderem die folgenden Elemente: property String^ Name // der Name der Datei (z.B. „c.exe“) property String^ FullName // der vollständige Name (z.B. „c:\windows\c.exe“) Die DirectoryInfo-Methoden array^ GetDirectories() // für alle Verzeichnisse zurück array^ GetDirectories(String^ searchPattern) array^ GetDirectories(String^ searchPattern, SearchOption searchOption) geben für alle Verzeichnisse, die zu dem als Argument für searchPattern (z.B. „*.*“ oder „*.cpp“) übergebenen String passen, ein Element des Typs DirectoryInfo im Rückgabe-Array zurück. Die Klasse DirectoryInfo enthält unter anderem die folgenden Elemente: property String^ Name // der Name des Verzeichnisses (z.B. „bin“) property String^ FullName // der vollständige Name (z.B. „c:\bin“) Mit diesen Methoden kann man alle Dateien eines Verzeichnisses nach dem Schema wie in der Funktion traverseSubDirs rekursiv durchsuchen: int traverseSubDirs(String^ path) // gibt die Anzahl der {// Dateien in allen Unterverzeichnissen von path zurück using namespace System::IO; DirectoryInfo^ di=gcnew DirectoryInfo(path); int n=0; // alle Dateien des Verzeichnisses path durchlaufen for each (FileInfo^ f in di->GetFiles() ) n++; for each (DirectoryInfo^ i in di->GetDirectories() ) n+= traverseSubDirs(path+i->Name+"\\"); return n; }
5.4 Die Funktionen main und _tmain und ihre Parameter
587
Hier werden zunächst alle Dateien des als path übergebenen Verzeichnisses durchlaufen. Anschließend wird diese Funktion für alle Unterverzeichnisse rekursiv aufgerufen. Diese Funktion kann man folgendermaßen aufrufen: int n=traverseSubDirs("c:\\");
Da die Methode GetFiles der Klasse DriveInfo (siehe Abschnitt 10.14.6) die Namen der Laufwerke (Eigenschaft Name) mit einem abschließenden „\“ zurückliefert, kann man diese Namen als Argument verwenden. Aufgabe 5.3.7 Schreiben Sie eine Funktion searchFiles, die die Namen aller Dateien in einer TextBox anzeigt, die zu einer als Parameter übergebenen Maske passen. Dabei sollen ausgehend von einem als Parameter übergebenen Laufwerk bzw. Verzeichnis aller Unterverzeichnisse durchsucht werden. Die Anzahl der gefundenen Dateien soll als Funktionswert zurückgegeben werden. Zum Testen können Sie die Anzahl der gefundenen Dateien mit der Anzahl vergleichen, die nach einem entsprechenden dir-Befehl wie z.B. dir \*.cpp /s
ein einer Eingabeaufforderung (Start|Programme|Zubehör) angezeigt wird. Am einfachsten geben Sie den Namen des Verzeichnisses hart kodiert im Quelltext an. Diese Funktion wird in Aufgabe 10.5.4, 2. so überarbeitet, dass alle gefundenen Dateien wie in der Suchen-Funktion von Windows (Start|Suchen|nach Dateien und Ordnern) in einem ListView angezeigt werden. In Abschnitt 10.12.10 wird auf der Basis dieser Funktionen ein Programm entwickelt, das alle doppelten Dateien auf einem Rechner sucht.
5.4 Die Funktionen main und _tmain und ihre Parameter In Standard-C++ enthält ein Programm eine main Funktion mit der folgenden Parameterliste: int main(int argc, char * argv[]) Hier enthält der Parameter argc die Anzahl der beim Start des Programms übergebenen Kommandozeilenparameter. Der Parameter argv ist ein Array von argc Zeigern auf Strings. Sie enthalten:
588
5 Funktionen
– argv [0]: – argv[1]:
den vollständigen Pfadnamen des aktuellen Programms, den ersten Kommandozeilenparameter, der nach dem Namen des Programms angegeben wurde, – argv[2]: den zweiten Kommandozeilenparameter, – argv[argc-1]: den letzten Kommandozeilenparameter, – argv[argc]: NULL. Startet man z.B. die Konsolenanwendung Test.exe int main(int argc, char **argv) { for (int i=0; i= () []
Beispiel: Schon in Zusammenhang mit den Containerklassen der Standardbibliothek wurde erwähnt, dass die Funktion sort zum Sortieren den Operator „AppendText(rk1::toString (e.what())); } catch(Exception^ e) // Visual C++ { textBox1->AppendText(e->Message); } catch(...) // alle weiteren Exceptions { textBox1->AppendText("Was war das?"); }
Auch in einem Exception-Handler kann man wieder eine Exception auslösen. Gibt man dabei nach throw keinen Ausdruck an, wird die aktuelle Exception an den nächsten umgebenden Exception-Handler weitergegeben. Wird dagegen ein Ausdruck angegeben, wird die aktuelle Exception gelöscht und eine neue Exception mit dem angegebenen Ausdruck ausgelöst. Beispiele: Eine von einem logic_error abgeleitete Exception wird weitergegeben: try { /*...*/ } catch (logic_error&) { MessageBox::Show("logisch"); throw; // gibt die Exception weiter }
Eine von einem logic_error abgeleitete Exception wird hier als runtime_error weitergegeben: try { /*...*/ } catch(logic_error&) { throw runtime_error("Ausgetrickst!"); } // gibt runtime_error weiter
Der Programmablauf bei einer Exception ist allerdings weniger strukturiert als bei den üblichen Kontrollstrukturen und deshalb schwerer aus dem Programmtext abzuleiten. Während mit if, while usw. immer ein ganzer Block kontrolliert wird, kann man mit throw in den Exception-Handler einer aufrufenden Funktion springen. Deshalb sollten Exceptions nur dann verwendet werden, wenn mit den üblichen Kontrollstrukturen keine zufriedenstellende Lösung möglich ist. Insbesondere sollte man throw-Ausdrücke nicht für trickreiche Programmabläufe wie ein goto in eine aufrufende Funktion verwenden.
824
7 Exception-Handling
Beispiel: Wenn beim Lesen nach dem Ende einer Datei eine Exception ausgelöst wird, kann man alle Daten einer Datei auch in einer Endlosschleife lesen und die Schleife mit einer Exception verlassen: try { ifstream f("c:\\test\\test.dat"); f.exceptions(ios::eofbit|ios::failbit); char c; while (true) f>>c; } catch (ios_base::failure& e) { // ... };
Die Schleifenbedingung f ist allerdings meist leichter verständlich, da sie direkt zum Ausdruck bringt, dass eine Datei ganz gelesen wird.
7.5 Fehler und Exceptions Bisher wurde immer nur relativ undifferenziert davon gesprochen, dass eine Exception bei einem „Fehler“ ausgelöst wird. Nachdem wir nun gesehen haben, wie man an einer beliebigen Stelle eine Exception auslösen kann, stellt sich die Frage, wann das sinnvoll und was in diesem Zusammenhang überhaupt ein „Fehler“ ist. Der Ausgangspunkt für die folgenden Ausführungen ist, dass jede Anweisung und jede Funktion eine bestimmte Aufgabe (ihre Spezifikation) hat. Wenn sie keine solche Aufgabe hätte, wäre sie auch nicht geschrieben worden. Ein Fehler ist dann dadurch charakterisiert, dass eine Funktion ihre Aufgabe nicht erfüllen kann. Beispiel: Alle bisher betrachteten Exceptions von C++, der .NET und der Standardbibliothek werden nach einem solchen Fehler ausgelöst: – Wenn der mit new angeforderte Speicher nicht zur Verfügung gestellt werden kann, hat die Anweisung mit new ihre Aufgabe nicht erfüllt. – Wenn der Zugriff auf ein Container-Element nicht zulässig ist. – Wenn ein String nicht in ein bestimmtes Format (wie int oder ein Kalenderdatum) umgewandelt werden kann. Nach einem solchen Fehler gibt es zwei Möglichkeiten: 1. Es ist sinnlos, weitere Anweisungen auszuführen, da diese vom Ergebnis des Aufrufs abhängen und nur Folgefehler nach sich ziehen.
7.5 Fehler und Exceptions
825
2. Die folgenden Anweisungen hängen nicht vom Ergebnis dieses Aufrufs ab und können problemlos ausgeführt werden. Wenn man Funktionen so konstruiert, dass sie genau dann eine Exception auslösen, wenn sie ihre Aufgabe nicht erfüllen, kann man diese beiden Fälle folgendermaßen umsetzen: 1. Anweisungen, die von einander abhängig sind und bei denen ein Fehler Folgefehler nach sich zieht, fasst man in einem Block nach try zusammen. 2. Anweisungen, die von einander unabhängig sind, können in getrennten tryBlöcken enthalten sein: Wenn ein Fehler in einem ersten Teil eines Programms keine Auswirkungen auf einen anderen Teil hat, braucht der andere Teil nach einem Fehler im ersten Teil auch nicht abgebrochen werden. Beispiel: Wenn die Funktion f2 nur dann ausgeführt werden soll, wenn der Aufruf von f1 nicht zu einem Fehler geführt hat, aber f3 unabhängig davon, kann man eine Programmstruktur wie die folgende verwenden: try { f1(); f2(); } catch(exception1&) { MessageBox::Show("Fehler bei f1")} catch(exception2&) { MessageBox::Show("Fehler bei f2")} f3();
Weitere typische Anwendungen sind Programme, die verschiedene Optionen in einem Menü anbieten. Wenn eine Option zu einem Fehler führt, kann man nach einem Hinweis auf diesen Fehler oft die weiteren Optionen ausführen. Das gilt auch für die Konstruktoren einer Klasse: Falls ein Konstruktor aus den Argumenten kein Objekt erzeugen kann, das die Klasseninvariante erfüllt (d.h. ein Objekt der Realität, siehe Abschnitt 6.1.7), löst man eine Exception aus und verwendet solche Objekte in einem try-Block. In Programmiersprachen ohne Exception-Handling wird meist folgendermaßen auf Fehler reagiert: – Nach einem schweren Fehler (z.B. einer Division durch Null) wird das Programm meist abgebrochen. Dadurch wird aber auch die Ausführung von Anweisungen unterbunden, die von diesem Fehler überhaupt nicht betroffen sind. – Bei weniger schweren Fehlern wird eine Statusvariable (wie errno in C) gesetzt oder ein spezieller Funktionswert zurückgegeben. Exception-Handling ermöglicht gegenüber diesen konventionellen Techniken eine differenziertere Reaktion und übersichtlichere Programme (siehe Abschnitt 7.1).
826
7 Exception-Handling
Wenn man alle Funktionen in einem Programm so konstruiert, dass sie genau dann eine Exception auslösen, wenn sie ihre Aufgabe nicht erfüllen, und alle solchen Funktionen in einer try-Anweisung aufruft, tritt immer genau dann eine Exception auf, wenn eine Funktion ihre Aufgabe nicht erfüllen kann. Wenn keine Exception auftritt, kann man sicher sein, dass alle Funktionen ihre Aufgabe erfüllt haben. Beispiel: Wenn die Funktionen f1, f2 und f3 bei jedem Fehler eine Exception auslösen, und bei try { f1(); f2(); f3(); } catch(...) // passt zu jeder Exception { MessageBox::Show("da ging was schief"); }
keine Fehlermeldung angezeigt wird, kann man sicher sein, dass kein Fehler aufgetreten ist. Oft kann man für eine Funktion nachweisen, dass sie ihre Aufgabe erfüllt, wenn ihre Argumente bestimmte Bedingungen (die Vorbedingungen) erfüllen. Dann kann man diese prüfen und eine Exception auslösen, wenn sie nicht erfüllt sind. Beispiel: Damit man auch von den Funktionen aus bei einem Fehler eine Exception erhält, ist es oft am einfachsten, sie durch eigene Funktionen zu ersetzen, die bei einem Fehler eine Exception auslösen: double Sqrt(double d) // Ersatz für sqrt { if (dAppendText("range \r\n"); } catch(exception& e) { tb->AppendText("exception \r\n"); } };
Von welcher Klasse wird die Funktion what in b) bis d) aufgerufen? b) void g2(TextBox^ tb, int i) { try { f(i); } catch(logic_error& e) { tb->AppendText(rk1::toString(e.what())); } catch(out_of_range& e) { tb->AppendText(rk1::toString(e.what())); } catch(exception& e) { tb->AppendText(rk1::toString(e.what())); } };
c) void g3(TextBox^ tb, int i) { try { f(i); } catch(exception e) { tb->AppendText(rk1::toString(e.what())); } };
d) void g4(TextBox^ tb, int i) { try { f(i); } catch(exception& e) { tb->AppendText(rk1::toString(e.what())+"\r\n"); } catch(...) { tb->AppendText("irgendeine Exception"); } };
5. Eigene Exceptions können von einer eigenen Basisklasse (z.B. myBaseException) oder einer vordefinierten Exception-Klasse wie Exception (.NETExceptions) oder exception (Standard-C++ Exceptions) abgeleitet werden. a) Geben Sie Gründe für oder gegen jeder dieser Alternativen an. b) Geben Sie für jede der Varianten Exception-Handler an, die auf Exceptions der verschiedenen Klassen differenziert reagieren. 6. Beurteilen Sie die Funktion Sqrt: class ENegative {};
834
7 Exception-Handling double Sqrt(double d) { try { if (d declaration template-parameter-list: template-parameter template-parameter-list , template-parameter template-parameter: type parameter parameter declaration type-parameter class identifieropt class identifieropt = type-id typename identifieropt typename identifieropt = type-id template < template-parameter-list > declaration class identifieropt template < template-parameter-list > declaration class identifieropt = type-id
Mit dem Schlüsselwort export, das aber von Visual C++ nicht unterstützt wird, erhält man ein sogenanntes exportiertes Template.
840
8 Templates und die STL
Ein Typ-Parameter ist ein Template-Parameter, der aus einem der Schlüsselworte typename oder class und einem Bezeichner besteht. Der Bezeichner kann dann in der Funktions-Deklaration des Templates wie ein Datentyp verwendet werden. Dabei sind typename und class gleichbedeutend. In älteren C++Compilern war nur class zulässig. Neuere Compiler akzeptieren auch typename. Da typename explizit zum Ausdruck bringt, dass ein Datentyp gemeint ist, der nicht unbedingt eine Klasse sein muss, wird im Folgenden meist typename verwendet. Beispiel: Die folgenden beiden Templates sind semantisch gleichwertig: template inline void vertausche(T& a, T& b) { T h = a; a = b; b = h; } template inline void vertausche(T& a, T& b) { T h = a; a = b; b = h; }
Alle Algorithmen der STL sind Funktions-Templates, die in einer Datei definiert sind, die mit #include eingebunden wird. Dazu gehört auch das Funktions-Template swap, das genau wie vertausche definiert ist. Bei der Definition von Funktions-Templates ist insbesondere zu beachten: – Spezifizierer wie extern, inline usw. müssen wie im letzten Beispiel nach „template < ... >“ angegeben werden. – Parameter von Funktions-Templates können Templates sein, die wiederum Templates enthalten. Falls bei der Kombination von zwei Templates zwei spitze Klammern aufeinander folgen, wurden diese bei älteren Compilern als Shift-Operator interpretiert. Das führte dann zu. einer Fehlermeldung. Seit Visual C++ 2005 ist das aber zulässig: template void f(vector a) // früher ein Fehler, ab Visual C++ 2005 ok: ^^
8.1.2 Spezialisierungen von Funktions-Templates Ein Funktions-Template kann wie eine gewöhnliche Funktion aufgerufen werden, die kein Template ist.
8.1 Generische Funktionen: Funktions-Templates
841
Beispiel: Das im letzten Beispiel definierte Funktions-Template vertausche kann folgendermaßen aufgerufen werden: int i1=1, i2=2; vertausche(i1,i2); string s1,s2; vertausche(s1,s2);
Der Compiler erzeugt aus einem Funktions-Template dann eine Funktionsdefinition, wenn diese in einem bestimmten Kontext notwendig ist und nicht schon zuvor erzeugt wurde. Ein solcher Kontext ist z.B. der Aufruf eines FunktionsTemplates, da durch den Aufruf des Templates die erzeugte Funktion aufgerufen wird. Wenn der Compiler beim Aufruf eines Funktions-Templates eine Funktionsdefinition erzeugt, bestimmt er den Datentyp der Template-Argumente aus dem Datentyp der Argumente des Funktions-Templates, falls das möglich ist. Die Datentypen der Template-Argumente werden dann in der Funktionsdefinition anstelle der Template-Parameter verwendet. Eine aus einem Funktions-Template erzeugte Funktion wird als Spezialisierung des Templates bezeichnet. Beispiel: Aus dem ersten Aufruf von vertausche im letzten Beispiel erzeugt der Compiler die folgende Spezialisierung und ruft diese auf: inline { int a = b = }
void vertausche(int& a, int& b) h = a; b; h;
Aus dem zweiten Aufruf von vertausche wird eine Spezialisierung mit dem Datentyp string erzeugt: inline void vertausche(string& a,string& b) { string h = a; a = b; b = h; }
Wenn ein Funktions-Template mit Argumenten aufgerufen wird, für die schon eine Spezialisierung erzeugt wurde, wird keine neue erzeugt, sondern die zuvor erzeugte Spezialisierung erneut aufgerufen. Beispiel: Beim zweiten Aufruf von vertausche wird die Spezialisierung aus dem ersten Aufruf aufgerufen: int i1=1, i2=2; vertausche(i1,i2); vertausche(i1,i2); // keine neue Spezialisierung
842
8 Templates und die STL
Funktions-Templates unterscheiden sich von Funktionen insbesondere bei der Konversion von Argumenten: – Ein Parameter einer Funktion hat einen Datentyp, in den ein Argument beim Aufruf der Funktion gegebenenfalls konvertiert wird. – Ein Template-Parameter hat dagegen keinen Datentyp. Deshalb kann ein Argument beim Aufruf eines Funktions-Templates auch nicht in einen solchen Parametertyp konvertiert werden. Der Compiler verwendet den Datentyp des Arguments beim Aufruf eines Funktions-Templates meist unverändert in der erzeugten Funktion. Nur für Argumente, deren Datentyp kein Referenztyp ist, werden die folgenden Konversionen durchgeführt: – Ein Arraytyp wird in einen entsprechenden Zeigertyp konvertiert, – ein Funktionstyp wird in einen entsprechenden Funktionszeigertyp konvertiert, – const- oder volatile-Angaben der obersten Ebene werden ignoriert, Deswegen werden bei Aufrufen eines Funktions-Templates mit verschiedenen Argumenttypen auch verschiedene Spezialisierungen erzeugt. Insbesondere wird ein Argument nicht in den Datentyp des Parameters einer zuvor erzeugten Spezialisierung konvertiert. Im Zusammenhang mit Templates ist es gelegentlich hilfreich, wenn man sich die Datentypen anzeigen lassen kann, die der Compiler in einer Spezialisierung verwendet. Das ist mit einem Funktions-Template wie template string TypeNames(T x, U y) { // returns the template argument typenames return string(typeid(x).name())+","+ typeid(y).name(); }
möglich, das die Namen der Typargumente als String zurückgibt. Sie beruht auf der nach #include
verfügbaren Elementfunktion
const char* name() const; von typeid, die zu einem Typbezeichner oder einem Ausdruck den Namen des Datentyps zurückgibt:
typeid(T).name(); // T: ein Typbezeichner, z.B. typeid(int) typeid(x).name(); // x: ein Ausdruck, z.B. typeid(17) Beispiel: Der erste Aufruf zeigt die Konversion eines Array-Arguments in einen Zeiger:
8.1 Generische Funktionen: Funktions-Templates
843
int a[10]; int* p; Typenames(a,p); // int*,int*
Die nächsten beiden Aufrufe zeigen, dass für int und char-Argumente verschiedene Spezialisierungen erzeugt werden, obwohl char in int konvertiert werden kann: int i;char c; Typenames(i,a); // int,int* Typenames(c,a); // char,int*
Der Compiler kann ein Template-Argument auch aus komplexeren Parametern und Argumenten ableiten. Dazu gehören diese und zahlreiche weitere Formen: const T
T*
T&
T[integer-constant]
Beispiel: Mit template string Typenames2a(vector x, U* y) { // returns the template argument typename return string(typeid(T).name())+","+ typeid(U).name(); }; vector v; double* p;
erhält man den als Kommentar angegebenen String: Typenames2a(v,p); // int,double
Falls mehrere Funktionsparameter eines Funktions-Templates denselben Template-Parameter als Datentyp haben, müssen die beim Aufruf abgeleiteten Datentypen ebenfalls gleich sein. Beispiel: Bei dem Funktions-Template template void f(T x, T y){ }
haben die beiden Funktionsparameter x und y beide den TemplateParameter T als Datentyp. Beim Aufruf f(1.0, 2); // error: template-Parameter "T" ist mehrdeutig
dieses Templates leitet der Compiler für das erste Argument den Datentyp double und für das zweite int ab. Da diese verschieden sind, erzeugt der Compiler eine Fehlermeldung. Da der Compiler den Datentyp eines Template-Arguments aus dem Datentyp der Funktionsargumente ableitet, kann er nur Template-Argumente ableiten, die zu
844
8 Templates und die STL
Parametern gehören. Aus einem Rückgabetyp können keine Argumente abgeleitet werden. Beispiel Beim Aufruf des Templates New template T* New() { return new T; }
erhält man eine Fehlermeldung: int* p=New(); // error: template-Argument für "T" konnte nicht hergeleitet werden
Die Ableitung der Template-Argumente durch den Compiler ist nur eine (die einfachere) von zwei Möglichkeiten, Template-Argumente zu bestimmen. Die andere ist die Angabe von explizit spezifizierten Template-Argumenten in spitzen Klammern nach dem Namen des Templates. Beispiel: Durch das explizit spezifizierte Template-Argument erreicht man mit int* p=New();
dass die Spezialisierung der Funktion New mit dem Datentyp int erzeugt wird. Explizit spezifizierte Template-Argumente werden oft für den Rückgabetyp eines Funktions-Templates verwendet. Template-Argumente werden nur abgeleitet, wenn sie nicht explizit spezialisiert sind. Gibt man mehrere Template-Argumente explizit an, werden sie den Template-Parametern in der aufgeführten Reihenfolge zugeordnet. Beispiel: Aus dem Funktions-Template template void f(T x, T y, U z) { }
werden durch die folgenden Aufrufe Spezialisierungen mit den als Kommentar angegebenen Parametertypen erzeugt: f(1, 2, 3.0); // f(int,int,double); f(1.0, 2, 3.0); // f(char,char,double); f(1.0,2,3.0);// f(double,double,int);
Während der Datentyp von Funktionsargumenten, die denselben Template-Parametertyp haben, bei abgeleiteten Typargumenten gleich sein muss, kann er bei explizit spezifizierten Template-Argumenten auch verschieden sein. Die Funktions-
8.1 Generische Funktionen: Funktions-Templates
845
argumente werden dann wie bei gewöhnlichen Funktionen in den Datentyp des Template-Arguments konvertiert. Beispiel: Bei dem Funktions-Template f aus dem letzten Beispiel haben die ersten beiden Funktionsparameter a und b beide den Template-Parameter T als Datentyp. Beim Aufruf f(1.0, 2, 3.0); // error: template-Parameter "T" ist mehrdeutig
dieses Templates leitet der Compiler aus dem ersten Argument den Datentyp double und beim zweiten int ab. Da diese verschieden sind, erzeugt der Compiler eine Fehlermeldung. Mit explizit spezifizierten Template-Argumenten werden die FunktionsArgumente dagegen in den Typ der Template-Argumente konvertiert. Deshalb ist der folgende Aufruf möglich: f(1.0, 2, 3.0); // f(int,int,double);
Damit der Compiler aus einem Funktions-Template eine Funktion erzeugen kann, muss er seine Definition kennen. Deshalb muss jedes Programm, das ein Template verwendet, den Quelltext der Template-Definition enthalten. Es reicht nicht wie bei gewöhnlichen Funktionen aus, dass der Compiler nur eine Deklaration sieht, deren Definition dann zum Programm gelinkt wird. Der Compiler kann die aus einem Funktions-Template erzeugte Funktion nur dann übersetzen, wenn die Anweisungen der Funktion definiert sind. Andernfalls erhält man eine Fehlermeldung. Beispiel: Aus dem Funktions-Template max (das man auch in der STL findet) template inline T max(const T& x,const T& y) { return ((x>y)?x:y); }
kann mit dem Template-Argument int eine Funktion erzeugt werden, da die Anweisungen dieser Funktion für int-Werte definiert sind: max(1,2);
Dagegen wird beim Aufruf dieses Funktions-Templates mit Argumenten des Typs struct S { int i; };
die Funktion
846
8 Templates und die STL inline S max(const S& x,const S& y) { return ((x>y)?x:y); }
erzeugt, in der der Ausdruck x>y für Operanden des Datentyps S nicht definiert ist. Das führt zu einer mehrzeiligen Fehlermeldung, die etwa so
kein Operator gefunden, der Operanden vom Typ 'S' akzeptiert beginnt und einige Zeilen weiter einen Verweis auf die Zeilennummer des Aufrufs enthält:
Zeilennummer des Aufrufs: Siehe Verweis auf die Instanziierung der gerade kompilierten Funktions-template "T max_(const T &,const T &)". with [ T=S ] „Operator '>': 'S' definiert diesen Operator ... nicht“ Mit einem Klick auf diese Fehlermeldung springt der Cursor im Editor an die Stelle, an der das Template aufgerufen wurde. Das ist meist auch die Stelle, an der der Fehler im Quelltext behoben werden muss. Jede Spezialisierung eines Funktions-Templates enthält ihre eigenen statischen lokalen Variablen. Beispiel: Mit dem Funktions-Template template int f(T j) { static int i=0; return i++; }
erhält man mit den folgenden Funktionsaufrufen die jeweils als Kommentar angegebenen Werte: int i1=f(1); // 0 int i2=f(1); // 1 int i3=f(1.0); // 0
Eine aus einem Funktions-Template erzeugte Funktion unterscheidet sich nicht von einer „von Hand“ geschriebenen Funktion. Deshalb sind Funktions-Templates eine einfache Möglichkeit, Funktionen mit identischen Anweisungen zu definieren, die sich nur im Datentyp von Parametern, lokalen Variablen oder dem des Rückgabewertes unterscheiden.
8.1 Generische Funktionen: Funktions-Templates
847
Die nächste Tabelle enthält die Laufzeiten für eine gewöhnliche Funktion und ein Funktions-Template mit denselben Anweisungen. Obwohl man identische Laufzeiten erwarten könnte, ist das Template etwas schneller:
Visual C++ 2008, Release Build Auswahlsort, n=40000
Funktion 1,56 Sek.
Funktions-Template 1,23 Sek.
Ausdrücke mit static_cast, const_cast usw. sind zwar keine Aufrufe von Funktions-Templates. Sie verwenden aber die Syntax explizit spezifizierter TemplateArgumente, um den Datentyp des Rückgabewerts der Konversion festzulegen: static_cast(3.5); // Datentyp int
8.1.3 Funktions-Templates mit Nicht-Typ-Parametern Wie die letzte Zeile der Syntaxregel template-parameter: type parameter parameter declaration
zeigt, kann ein Template-Parameter nicht nur ein Typ-Parameter sein, sondern auch ein gewöhnlicher Parameter wie bei einer Funktionsdeklaration. Solche Template-Parameter werden auch als Nicht-Typ-Parameter bezeichnet und müssen einen der Datentypen aus der Tabelle von Abschnitt 8.2.3 haben. Vorläufig werden nur die folgenden Parameter und Argumente verwendet.
Datentyp Ganzzahldatentyp Zeiger auf eine Funktion
Argument konstanter Ausdruck eines Ganzzahltyps eine Funktion mit externer Bindung
Im Template sind ganzzahlige Nicht-Typ-Parameter konstante Ausdrücke. Sie können deshalb z.B. zur Definition von Arrays verwendet und nicht verändert werden. In GetValue wird beim ersten Aufruf ein Array initialisiert, dessen Elementanzahl über einen Ganzzahlparameter definiert ist. Bei jedem weiteren Aufruf wird dann nur noch der Wert eines Arrayelements zurückgegeben: template inline T GetValue(int n) { static T a[max]; static bool firstCall=true; if (firstCall) { // berechne die Werte nur beim ersten Aufruf for (int i=0; i': Alle Operanden müssen // den gleichen Enumerationstyp aufweisen
Alle CLI-Aufzählungstypen sind von Basisklasse Enum abgeleitet. Sie haben deshalb alle Elementfunktionen dieser Basisklasse. – Die Methode ToString gibt für einen C++/CLI-Aufzählungstyp den String zurück, der ihrer Zeichenfolge entspricht:
1010
9 C++/CLI textBox1->AppendText(t.ToString()); // Sonntag
– Mit der Methode
static Object^ Parse(Type^ enumType, String^ value) kann ein String in einen CLI-Aufzählungstyp konvertiert werden. Falls der String nicht einem Enumerator entspricht, wird eine Exception ausgelöst: Wochentag^ t=Wochentag::Sonntag; Object^ to=Enum::Parse(Wochentag::typeid,"Sonntag"); t=safe_cast(to);
– Eine Liste aller Werte als Strings erhält man mit
static array^ GetNames(Type^ enumType) Das .NET Framework verwendet C++/CLI Aufzählungstypen häufig. Dazu nur zwei Beispiele. 1. Die Eigenschaft TextAlignment legt fest, wie Text in einer TextBox ausgerichtet wird. Sie hat den Datentyp HorizontalAlignment enum class HorizontalAlignment {Center, Left, Right};
Mit der folgenden Anweisung wird der Text in der TextBox textBox1 zentriert: textBox1->TextAlign=HorizontalAlignment::Center;
2. Der Datentyp FormBorderStyle wird verwendet, um die Rahmenart eines Formulars festzulegen: enum class FormBorderStyle
Das kann einer der folgenden Werte sein:
Fixed3D FixedDialog FixedSingle FixedToolWindow
Ein fester, dreidimensionaler Rahmen. Wie bei Dialogfeldern. Ein fester Rahmen mit einer dünnen Rahmenlinie. Ein Toolfenster, dessen Größe nicht verändert werden kann. None Kein Rahmen. Ein größenveränderlicher Rahmen. Sizable SizableToolWindow Ein größenveränderlicher Toolfensterrahmen.
9.7.3 Werteklassen Eine Werteklasse wird mit
value class oder value struct
9.7 Wertetypen und Werteklassen
1011
definiert. Der Unterschied zwischen einer mit value class und value struct definierten Werteklasse ist derselbe wie der zwischen einer mit class und struct definierten nativen Klasse: Bei value class ist das voreingestellte Zugriffsrecht private, während es bei einer mit value struct definierten Klasse public ist. Werteklassen sind nur für Klassen geeignet, die ihre Werte als Ganzes darstellen und keine Daten über Zeiger oder andere Verweise ansprechen. Sie können einige CLI-Besonderheiten nutzen und ermöglichen vor allem ein schnelles Kopieren von Daten. Dieser Geschwindigkeitsvorteil ist aber nur bei kleinen Klassen (oft wird eine Größe von 16 Bytes genannt) spürbar. Die meisten .NET-Klassen sind Verweisklassen. Es gibt aber auch einige Werteklassen wie die den elementaren Datentypen entsprechenden C++/CLI Datentypen (System::Int32 usw., siehe Abschnitt 9.7.1) und
public value class Decimal //Gleitkommatyp für Geldbeträge (siehe Abschnitt 3.6.6) public value class DateTime // Datentyp für Kalenderdaten und Uhrzeiten public value class Rectangle // in System::Drawing Elemente von Werteklassen können wie bei nativen Klassen Datenelemente, Elementfunktionen und verschachtelte Klassen sein. Die folgenden Elemente sind allerdings nicht zulässig: – Datenelemente, deren Typ eine native Klasse (dazu gehören auch die Klassen der C++ Standardbibliothek, wie z.B. vector oder string), ein natives Array oder ein Bitfeld ist – ein Standardkonstruktor: Werteklassen haben einen vordefinierten Standardkonstruktor, der alle Elemente eines Werteklassentyps mit ihrem Standardkonstruktor initialisiert und alle Elemente eines vordefinierten Typs mit dem Bitmuster 0. Alle Elemente eines Verweisklassentyps erhalten den Wert nullptr. Dieser Standardkonstruktor existiert im Unterschied zu nativen Klassen unabhängig davon, ob die Klasse andere Konstruktoren hat oder nicht. Da eine Werteklasse immer einen solchen Standardkonstruktor hat, kann damit ein Objekt erzeugt werden. Falls ein solches Objekt nicht zulässig ist, sollte man keine Werteklasse verwenden. – Kopierkonstruktor oder Zuweisungsoperator: Für eine Werteklasse sind diese Funktionen vordefiniert. Sie kopieren alle Elemente der Klasse bitweise. Es ist nicht möglich, sie für eine Werteklasse zu definieren. Deshalb sind Werteklassen nur dann angemessen, wenn eine flache Kopie nicht zu Problemen führen kann. – ein Destruktor und ein Finalisierer: Da Werteklassen nie Ressourcen verwalten, sind diese Funktionen auch nicht notwendig. – friend-Deklarationen – verschachtelte native Klassen. Beispiel: Alle Elementfunktionen von V führen zu einer Fehlermeldung:
1012
9 C++/CLI value class V { V(); // error: Werttypen können keine benutzerV(const V& x){} // definierten speziellen ~V() {} // Memberfunktionen enthalten !V() {} // error: Ein Finalizer kann nur ein // Member eines Verweistyps sein. V& operator=(const V& x) {} // Ein Werttyp darf }; // keinen Zuweisungsoperator aufweisen.
Obwohl die Klasse KeyValuePair auf den ersten Blick so aussieht, als ob der Wert nullptr für die Elemente ausgeschlossen würde, sind diese Werte doch über den implizit erzeugten Standardkonstruktor möglich. Dann löst der Aufruf von ToString() eine Exception aus. value class KeyValuePair { String^ key; String^ value; public: KeyValuePair(String^ key, String^ value) { if (key == nullptr || value == nullptr) throw gcnew ArgumentException(); this->key = key; this->value = value; } virtual String^ ToString() override { // beim Zugriff auf nullptr-Werte: Exception return String::Format("Paar({0},{1})", key->ToString(),value->ToString()); } };
Native Klassen können aber für Parameter und Rückgabetypen von Elementfunktionen von Werteklassen verwendet werden. Beispiele: Die Werteklasse V enthält einige zulässige Elemente sowie einige unzulässige (z.B. ein Datenelement der Klasse string) und die zugehörigen Fehlermeldungen: value class V { string s1; // error: "s" kann nicht als Member // von "R1" (verwaltet) definiert werden: String^ s2; // das geht char* s3; // das geht string f(string s) {return s+"/";} class C {}; // error: "C": Ein systemeigener Typ // kann innerhalb des verwalteten Typs "R1" // nicht geschachtelt werden. ref class R{}; // verschachtelte Verweisklasse value class V1{}; // verschachtelte Werteklasse };
9.7 Wertetypen und Werteklassen
1013
Ein Objekt eines Werteklassentyps kann sowohl wie eine gewöhnliche Variable auf dem Stack als auch mit gcnew erzeugt werden. Beispiele: Mit einer Werteklasse V sind die folgenden Definitionen zulässig: V v; V^ gv=gcnew V;
Werteklassen können im Unterschied zu nativen Klassen nicht lokal in einer Funktion definiert werden. Beispiel: Weder in einer Elementfunktion f einer Werteklasse noch in einer globalen Funktion f ist diese Definition einer Werteklasse V möglich: void f() { class C{};// das geht value class V{};//error }
Eine Werteklasse wird als einfache Werteklasse bezeichnet, wenn sie keine Elemente hat, die auf dem GC-Heap angelegt werden. Das sind genau die Werteklassen, bei denen der Datentyp aller Datenelemente ein elementarer Datentyp, ein Aufzählungstyp, ein Zeiger oder eine andere einfache Werteklasse ist. Einfache Werteklasse bieten einige Möglichkeiten, die sonst nur für native Klassen verfügbar sind: – Ein Objekt einer einfachen Werteklasse kann mit new auf dem nativen Heap sowie global angelegt werden. – Eine native Klasse kann Datenelemente haben, deren Datentyp eine einfache Werteklasse ist. Beispiele: Mit der einfachen Werteklasse value class simpleV { int x; };
sind die folgenden Definitionen möglich: simpleV s; // globale Definition simpleV* p=new simpleV; class C { // native Klasse simpleV v; };
Würde man in simpleV ein Element eines Verweisklassetyps (z.B. String^) aufnehmen, wären diese Definitionen nicht möglich.
1014
9 C++/CLI
9.7.4 Vererbung bei Werteklassen Alle Werteklassen werden implizit von der Klasse System::ValueType abgeleitet, auch ohne dass eine Basisklasse angegeben wird. System::ValueType ist wiederum von System::Object abgeleitet und hat im Wesentlichen dieselben Elementfunktionen wie diese Basisklasse. Diese sind allerdings für Werteklassen optimiert. Werteklassen können nur von Interface-Klassen (siehe Abschnitt 9.8) abgeleitet werden. Werteklassen, Verweisklassen oder native Klassen sind nicht als Basisklassen zulässig. Beispiel: Unabhängig davon, ob C eine Werteklasse , Verweisklasse oder native Klasse ist: Mit einer Definition wie value class V:C{}; // error
erhält man die Fehlermeldung Erben von "V" nicht möglich. Werttypen können nur von Schnittstellenklassen erben
Werteklassen sind implizit sealed (siehe Abschnitt 9.6.16) und können nicht als Basisklasse verwendet werden.
Aufgabe 9.7 Die folgenden Werteklassen werden alle erfolgreich kompiliert. Welche sind als Werteklasse geeignet? Falls eine Verweisklasse oder eine native Klasse besser geeignet wäre, geben Sie die Gründe an: value class Date_t { int day, month, year; // ... };
value class Punkt{ int x,y; // ... };
value class Kreis_1 { int r; Punkt^ MP; // ... };
value class Kreis_2 { int r; Punkt MP; // ... };
9.8 Interface-Klassen Eine Interface-Klasse legt Funktionen, Eigenschaften, Ereignisse usw. fest, die in einer abgeleiteten Klasse implementiert werden müssen. Eine solche Klasse hat große Ähnlichkeiten mit einer abstrakten Klasse, deren Elementfunktionen in einer abgeleiteten Klasse überschrieben werden müssen.
9.8 Interface-Klassen
1015
Eine Interface-Klasse wird mit interface class oder interface struct definiert. Beide sind gleichwertig, da alle Elemente einer Interface-Klasse public sind und das auch nicht geändert werden kann. Das Zugriffsrecht public kann angegeben werden, hat aber keine Auswirkung. Andere Zugriffsrechte wie private oder protected sind nicht zulässig. Als Name für eine Interface-Klasse wird oft ein Bezeichner gewählt, der mit dem Buchstaben I beginnt. Viele Interface-Klassen enthalten nur Deklarationen von nicht statischen Elementfunktionen, die keine Definitionen sind. Alle solchen Funktionen sind dann virtuell und abstrakt, auch ohne dass virtual, abstract oder „=0“ angegeben wird. Solche Angaben sind zwar zulässig, haben aber keine Wirkung. Beispiel: Die Interface-Klasse I1 enthält eine Funktionsdeklaration: interface class I1 { bool f(int x); };
Die .NET Interface-Klasse IComparable ist im Wesentlichen folgendermaßen definiert: public interface class IComparable { int CompareTo(Object^ obj); };
Bei einer Verweis- oder Werteklasse (aber nicht bei einer nativen Klasse) können Interface-Klassen als Basisklassen angegeben werden. Dann muss die abgeleitete Klasse alle Elemente der Basisklassen implementieren. Man sagt dann auch, dass die abgeleitete Klasse das Interface implementiert. Falls sie nicht alle Elemente implementiert, gibt der Compiler eine Fehlermeldung aus. Interface-Klassen sind die einzigen C++/CLI-Klassen, mit denen eine Mehrfachvererbung möglich ist. Sie sind außerdem die einzig zulässigen Basisklassen für Werteklassen. Eine Funktion f aus einer Interface-Klasse I wird in einer abgeleiteten Klasse C dadurch implementiert, dass man in C – eine Funktion mit demselben Namen, derselben Signatur und der Angabe virtual (aber ohne override) definiert; oder – mit der benannten Überschreibungssyntax (siehe Abschnitt 9.6.14) C::f direkt angibt. Die Angabe virtual ist dabei obligatorisch. Ohne diese Angabe wird die Funktion aus dem Interface nicht implementiert. Die Angabe override ist (im Unterschied zu Basis-Verweisklassen) ein Fehler, aber abstract ist zulässig. Beispiel: Die Klasse R implementiert das Interface aus dem letzten Beispiel:
1016
9 C++/CLI ref class R:I1 { public: virtual bool f(int x){ return x%2==0;}; };
Das Interface der Klassen I1 und interface class I2 { bool g(int x) ; };
wird durch value class V:I1,I2 { // Mehrfachvererbung public: virtual bool f(int x){ return x%5==0;}; virtual bool g(int x){ return x%7==0;}; };
implementiert. Die den elementaren Datentypen entsprechenden C++/CLI-Datentypen implementieren mehrere Interfaces:
public value class Int32 : IComparable, IFormattable, IConvertible, IComparable, IEquatable Eine Interface-Klasse kann wiederum Basisklasse einer Interface-Klasse sein. Eine Klasse, die ein abgeleitetes Interface implementiert, muss dann alle Elemente der Basisklassen implementieren. Beispiel: Das Interface der Klasse interface class I3:I1 { // I1 von oben bool g(int x) ; };
wird durch die Klasse S implementiert: ref class S:I3 { public: virtual bool h(int x)=I3::g{ return x%5==0;}; virtual bool f(int x){ return x%5==0;}; };
Hier wird die die benannte Überschreibungssyntax nur zur Illustration verwendet. Eine Funktion mit dem Namen g wäre genauso möglich. Interface-Klassen werden in .NET ausgiebig verwendet. Viele Klassen haben solche Basisklassen. Auf diese Weise „zwingt“ .NET einen Anwender, gewisse Funktionen in seiner Klasse zur Verfügung zu stellen, um Funktionen der .NETKlasse nutzen zu können. Die Abschnitte 9.8.1 bis 9.8.3 zeigen einige Beispiele. Weitere zulässige Elemente von Interface-Klassen sind
9.8 Interface-Klassen
1017
– Events und Properties (siehe Abschnitte 9.12.1 und 9.13.3). – statische Elemente (Datenelemente und Methoden) und Literale. Diese werden aber nur selten verwendet. Da statische Methoden nicht virtuell sind, können sie in einer abgeleiteten Klasse auch nicht überschrieben werden. – verschachtelte Klassen, Destruktoren, Operatorfunktionen sowie statische Konstruktoren.
Nicht zulässig sind dagegen Definitionen von Elementfunktionen, nicht-statische Datenelemente, Konstruktoren oder Finalisierer. Beispiel: Die nicht auskommentierten Elemente der Interface-Klasse I4 sind zulässig, und die auskommentierten nicht: interface class I4 { bool f(int x); //int i; // error: kein zulässiges Element //void g(int x){}; // error: Funktionsdefinition //I1(){}; // error: Konstruktor nicht zulässig static int i=17; static int fs() { return 18;} literal int j=19; };
9.8.1 Die .NET Interface-Klasse IComparable Damit die Elemente eines CLI-Arrays mit der Array-Elementfunktion
static void Sort (Array^ array) sortiert werden können, müssen die Elemente des Arrays das Interface von IComparable public interface class IComparable { int CompareTo(Object^ obj); };
implementieren. Dabei muss die Funktion CompareTo die folgenden Anforderungen erfüllen: 0: falls this > obj ist Falls sie diese Anforderungen nicht erfüllt (wie z.B. der Operator Name, cmp->Name); } };
kann dann folgendermaßen sortiert werden: array^ a={a1,a2}; Array::Sort(a);
Ruft man Sort mit einem Array auf, dessen Elementtyp diese Funktion nicht implementiert, wird eine Exception des Typs System::InvalidOperationException mit der Meldung „Failed to compare two elements in the array.“ ausgelöst. Interface-Klassen werden nicht nur als Basisklassen, sondern auch als Parameter verwendet. Der Array::Sort-Methode
static void Sort(Array^ array, IComparer^ comparer) kann man einen gc-Zeiger auf eine Implementation der Interface-Klasse IComparer aus dem Namensbereich System::Collections übergeben. Diese Interface-Klasse enthält eine Methode
int Compare(Object^ x, Object^ y) Wenn man eine solche Klasse (siehe Aufgabe 9.8, 1. c)) ref class CompareDateDescending:IComparer { }
definiert hat, kann man ein Array a mit der nächsten Anweisung sortieren: Array::Sort(a,gcnew CompareDateDescending);
9.8.2 Collection-Typen, IEnumerable und die for each-Anweisung Ein Datentyp wird in C++/CLI als Collection-Typ bezeichnet, wenn er eine der Interface-Klassen
System::Collections::IEnumerable oder System::Collections::Generic::IEnumerable
9.8 Interface-Klassen
1019
implementiert oder ähnliche Anforderungen erfüllt. Diese Interface-Klassen haben die Methoden
IEnumerator^ GetEnumerator() // IEnumerator ist ein Interface-Typ bool MoveNext () und die Eigenschaft
property Object^ Current MoveNext versucht, Current auf das nächste Element der Collection zu setzen. Falls das möglich ist, gibt diese Funktion true zurück, und andernfalls false (wenn das Ende der Collection erreicht wurde). Mit diesen Interfaces wird die for each-Anweisung for each (T^ d in c) statement for each (T* d in c) statement
in der c eine Collection von Elementen des Typs T^ bzw. T* ist, wie die folgenden Anweisungen ausgeführt: ^ e; // bzw. * e; bei try { // Zeigern e = C.GetEnumerator(); while(e->MoveNext()) { T d = safe_cast(e->Current); statement } } finally { delete e; }
Bei einer Collection c mit Elementen eines Wertetyps wird for each (T d in c) statement
wie die folgenden Anweisungen ausgeführt: e = .GetEnumerator(); while(e.MoveNext()) { T d = safe_cast(e.Current); statement }
Obwohl der Aufruf von MoveNext in for each erwarten lässt, dass die for eachAnweisung länger dauert als eine gleichwertige for-Anweisung, wurden bei Laufzeitvergleichen kaum Unterschiede festgestellt.
1020
9 C++/CLI
Der Datentyp System::Array ist ein Collection-Typ, von dem alle CLI-Arrays (siehe Abschnitt 9.5) abgeleitet sind. Außerdem implementieren alle CollectionKlassen (siehe Abschnitt 10.12) das IEnumerable Interface. Deswegen kann die for each-Anweisung mit jedem CLI-Array und jeder Collection-Klasse aufgerufen werden.
9.8.3 IFormattable und die Darstellung selbstdefinierter Datentypen In Abschnitt 9.4.3 wurde gezeigt, wie man mit Formatangaben der Form
{index[,alignment][:formatString]} steuern kann, wie ein Wert mit der String-Methode
static String^ Format(String^ format, array^ args) dargestellt wird. Diese Formatierungsmöglichkeiten stehen außerdem für die Ausgabemethoden von System::IO::TextWriter und System::Console zur Verfügung. Beispiel: Mit double d=12.34; String^ s=String::Format("c={0:c} e={0:e}",d);
wird der Wert von d zuerst im Währungsformat und dann im Exponentialformat formatiert. Solche Formatierungsmöglichkeiten kann man auch für selbstdefinierte Klassentypen definieren, indem man das Interface
public interface class IFormattable implementiert. Diese Interface-Klasse hat als einziges Element die Methode
String^ ToString (String^ format, IFormatProvider^ formatProvider) Dabei entspricht der Parameter format dem formatString einer Formatangabe. Der Parameter zu IFormatProvider ermöglicht weitere (z.B. kulturspezifische) Formatierungsangaben, wird aber im Folgenden nicht berücksichtigt. Beispiel: Mit ref class Punkt :IFormattable { int x,y; public: Punkt(int x_, int y_):x(x_),y(y_){} virtual String^ ToString(String^ format, IFormatProvider^ formatProvider) { if (format==nullptr) // ohne Formatangabe
9.8 Interface-Klassen
1021
return ToString("G", nullptr); else if (format=="G")//allgemeine Formatangabe G return String::Format("({0},{1})", x, y); else if (format=="d") // deutsche Darstellung return String::Format("Punkt({0},{1})",x,y); else if (format=="e") // englische Darstellung return String::Format("Point({0}, {1})",x,y); else // sonst Exception auslösen throw gcnew FormatException(String::Format( "Invalid format string: '{0}'.",format)); } virtual String^ ToString() override {//überschreibt ToString aus Object return ToString("G", nullptr); } };
erhält man mit Punkt^ p=gcnew Punkt2(3,4); String^ s=p->ToString(); // "(3,4)" String^ t=String::Format("{0:d}",p);//"Punkt(3,4)" String^ u=String::Format("{0:e}",p);//"Point(3,4)"
die als Kommentar angegebenen Strings. Eine Implementierung dieser Interface-Klasse sollte wie in diesem Beispiel die Formatangabe „G“ (für „general“ – allgemein) berücksichtigen und ToString aus Object mit dieser allgemeinen Formatierung überschreiben.
Aufgabe 9.8 1. Legen Sie einige Objekte des Typs Date_t (siehe Aufgabe 9.7) in einem CLIArray ab. a) Rufen Sie für dieses Array die Methode Sort auf, ohne dass das Interface IComparable implementiert wird. b) Implementieren Sie das Interface IComparable für Date_t so, dass das Array aufsteigend sortiert wird, und rufen Sie jetzt Sort auf. c) Implementieren Sie das Interface IComparer in einer Klasse CompareDateDescending, so dass der folgende Aufruf das Array a absteigend sortiert: Array::Sort(a,gcnew CompareDateDescending);
d) Erweitern Sie die Klasse Date_t so, dass ein Datum mit der Formatangabe "e" im englischen Format „MM/DD/YYYY“ und mit den Formatangaben "d" und "g" im deutschen Format „TT.MM.YYYY“ ausgegeben wird. 2. Interface-Klassen und abstrakte Klassen haben viele Ähnlichkeiten, und in Sprachen ohne Interface-Klassen werden abstrakte Klassen oft zur Implementa-
1022
9 C++/CLI
tion von Interface-Klassen verwendet. Geben Sie mindestens drei Unterschiede zwischen abstrakten Klassen und Interface-Klassen an.
9.9 C++/CLI Exception-Handling Für Exceptions gelten in C++/CLI im Wesentlichen alle Ausführungen des C++Standards (siehe Kapitel 7). Abweichungen und Erweiterungen betreffen vor allem C++/CLI-Datentypen und werden im Folgenden vorgestellt. Der wichtigste Unterschied zwischen Exception-Handling in Standard-C++ und in C++/CLI sind aber nicht diese sprachlichen Unterschiede. In C++/CLI und der .NET Klassenbibliothek wird bei einem Fehler immer eine Exception ausgelöst, auch von den vordefinierten Funktionen. Deshalb können in jedem C++/CLIProgramm Exceptions auftreten. Ein Programm in Standard-C++ kann dagegen auch ohne Exceptions auskommen.
9.9.1 Unterschiede zu Standard-C++ Obwohl in C++/CLI für Exceptions normalerweise ein von der Klasse Exception (siehe Abschnitt 9.9.2) abgeleiteter Typ verwendet wird, sind weitere Datentypen möglich: – Für eine Verweisklasse kann eine Exception nur mit einem Zeiger auf ein mit gcnew angelegtes Objekt ausgelöst werden. – Obwohl das Exception-Objekt für eine Werteklasse auch mit new angelegt kann werden, sollte man gcnew verwenden, da der Datentyp in einem Exception-Handler immer ein Zeiger auf den GC-Heap sein muss. Da in anderen CLI-Sprachen (z.B. Visual Basic oder C#) aber nur Exceptions der Exception-Hierarchie abgefangen werden können, muss man auf solche Exceptions verzichten, wenn man eine in C++/CLI geschriebene Bibliothek in einer anderen Sprache verwenden will. Beispiel: Für eine native Klasse C kann eine Exception als Wertetyp und mit new ausgelöst werden: C c; throw c; throw new C;
Für eine Verweisklasse kann und für eine Werteklasse sollte eine Exception nur mit gcnew ausgelöst werden: throw gcnew X; // X eine Verweis- oder Werteklasse
9.9 C++/CLI Exception-Handling
1023
In einem Exception-Handler kann eine Verweis- oder Werteklasse nur mit einem Zeiger auf den GC-Heap verwendet werden: catch(X^ e) // X eine Verweis- oder Werteklasse
Die Methoden von Verweis- und Werteklassen können keine Exception-Spezifikationen (siehe Abschnitt 7.8) haben und setzen so die verbreitete Empfehlung um, auf solche Spezifikationen zu verzichten. Eine native Klasse kann ExceptionSpezifikationen haben, deren Datentyp eine native Klasse, Verweis- oder Werteklasse ist. Beispiel: Bei einer nativen Klasse kann man für X den Namen einer nativen Klasse, einer Werte- oder Verweisklasse angeben. Ersetzt man „class N“ durch eine Verweis oder Werteklasse, erhält man eine Fehlermeldung. class N { // zulässig, // aber nicht mit ref oder value class void test() throw (X){ }
9.9.2 Die Basisklasse Exception Die C++/CLI und .NET Exceptions sind wie in der Standardbibliothek in einer Klassenhierarchie definiert. Hier ist die Basisklasse die Klasse Exception, die unter anderem die folgenden Elemente hat:
virtual property String^ Message // Meldung, die die Exception beschreibt virtual property String^ HelpLink virtual property String^ StackTrace // die Aufrufliste als String virtual property String^ Source // Name der Anwendung Die Klassen dieser Hierarchie haben einige Konstruktoren wie z.B.
Exception() Exception(String^ message); Meist übergibt man einen String, der dann als Eigenschaft Message zur Verfügung steht: throw gcnew Exception("Da ging was schief");
Beispiel: Eine Ganzzahldivision durch 0 löst eine von Exception abgeleitete Exception aus. Bei einem Aufruf der Funktion t mit dem Argument 0 für x int f(int i) {return 1/i; }
1024
9 C++/CLI int t(TextBox^ tb, int x) { try { return f(x); } catch(Exception^ e) { tb->AppendText("Msg: "+e->Message+"\r\n"+ "StackTrace:\r\n"+e->StackTrace+"\r\n"); } }
erhält man Meldungen wie die folgenden: Msg: Es wurde versucht, durch 0 (null) zu teilen. StackTrace: bei f(Int32 i) in c:\test.h:Zeile 220. bei t(TextBox tb, Int32 x) in c:\test.h:Zeile 224.
Hier hätte man auch den Exception-Handler catch (DivideByZeroException^ e)
verwenden können. Die Eigenschaft HelpLink kann für eine URL verwendet werden, die dann im Exception-Handler mit einem WebBrowser (Toolbox Registerkarte „Allgemeine Steuerelemente“) angezeigt wird. Beispiel: Die Eigenschaft HelpLink kann man vor dem Auslösen einer Exception Exception^ e=gcnew myException; e->HelpLink="http://www.rkaiser.de"; throw e;
oder in einer abgeleiteten Klasse setzen: ref class myException : Exception { public: myException(String^ URL) { HelpLink = URL; } };
Den HelpLink kann man dann folgendermaßen anzeigen: try {
throw gcnew myException( "file:///C:/test/help.html#Error17"); } catch(myException^ e) { webBrowser1->Navigate(e->HelpLink); }
Ein stark verkürzter Auszug aus der Exception-Klassenhierarchie:
9.9 C++/CLI Exception-Handling
1025
Exception |– ApplicationException |– SystemException |– AccessViolationException |– ArithmeticException |– DivideByZeroException |– NotFiniteNumberException |– OverflowException Viele dieser Exceptions haben keine zusätzlichen Elemente gegenüber Exception. Die meisten sind im Namensbereich System definiert. Die Klasse SystemException ist die Basisklasse aller Exceptions, die von der CLR ausgelöst werden. Eigene Exception-Klassen sollten von ApplicationException oder von Exception abgeleitet werden.
9.9.3 Vordefinierte C++/CLI und .NET Exceptions Die folgenden Beispiele illustrieren einige Exceptions der .NET Bibliothek sowie passende Exception-Handler: 1. Wenn in einer CLR-Anwendung auf ein Objekt einer Verweisklasse zugegriffen wird, das nicht angelegt wurde (z.B. mit gcnew), wird eine NullReferenceException ausgelöst. try { TextBox^ t; // ohne t=gcnew TextBox t->AppendText("das geht schief"); } catch (NullReferenceException^ e) { textBox1->AppendText(e->Message+"\r\n"); }
Diese Exception wird auch beim Zugriff auf ein nicht existierendes String-, Collection- oder Arrayelement sowie bei der Dereferenzierung eines ungültigen Zeigers ausgelöst: try { String^ s; wchar_t c=s[1]; // ungültiges String-Element int a[10]; int x=a[10]; // ungültiges Element eines C-Arrays array^ b; x=b[10];// ungültiges Element eines CLI-Arrays int* i; // ohne new ungültiger Zeiger *i=17; } catch (NullReferenceException^ e) { textBox1->AppendText(e->Message+"\r\n"); }
Bei einer „unmanaged code“-Anwendung (z.B. einer Win32-Anwendung) führen entsprechende Operationen dagegen zu einer AccessViolationException.
1026
9 C++/CLI
2. Falls eine Methode der Klasse Convert ein Argument nicht konvertieren kann, wird eine Exception der Klasse FormatException ausgelöst. Jede der folgenden Konvertierungen löst eine solche Exception aus: try { DateTime t = Convert::ToDateTime("29.2.2009"); int i = Convert::ToInt32("Eins"); } catch (FormatException^ e) { textBox1->AppendText(e->Message+"\r\n"); }
3. Im letzten Abschnitt wurde gezeigt, dass bei einer Ganzzahldivision durch 0 eine Exception des Typs DivideByZeroException ausgelöst wird. Bei einer Gleitkommadivision durch 0 wird aber keine Exception ausgelöst, sondern ein NAN-Ergebnis zurückgegeben. 4. Wenn man auf dem Stack mehr Speicher reservieren will als verfügbar ist, erhält man einen Stack-Overflow (StackOverflowException). Das ist sowohl bei zu großen lokalen Variablen als auch bei rekursiven Funktionen möglich: void zu_gross() { int a[10*1024*1024]; // ... }
void recurse() { recurse(); }
5. Bereichsüberschreitungen lösen bei einem CLI-Array immer eine Exception aus. Bei einem C-Array erhält man nur dann eine Exception, wenn die Bereichsüberschreitung zu einer Zugriffsverletzung führt. 6. Bei einem Overflow wird keine Exception ausgelöst: int i=0x7FFFFFFF; double x=i+i;
9.9.4 Die Freigabe von Ressourcen mit try-finally In Abschnitt 7.6 wurde gezeigt, wie man in Standard-C++ Ressourcen im Destrukor freigeben kann (RAII). Daneben kann man in C++/CLI die Freigabe von Ressourcen auch mit try-finally sicherstellen.
try-block: try compound-statement handler-seq try compound-statement finally-clause try compound-statement handler-seq finally-clause finally-clause: finally compound-statement
9.9 C++/CLI Exception-Handling
1027
Die Anweisungen in dem Block nach finally werden immer ausgeführt, und zwar unabhängig davon, ob in dem Block nach try eine Exception auftritt oder nicht, oder ob dieser Block mit return verlassen wird. Durch eine finally-clause werden keine Exceptions behandelt. Dazu muss man einen Exception-Handler in der zugehörigen oder einer umgebenden try-Anweisung verwenden. Beispiele: – Damit eine Datei auch dann immer wieder geschlossen wird, wenn eine Exception ausgelöst wird, kann man Anweisungen wie StreamWriter^ sw = gcnew StreamWriter(path); try { sw->WriteLine("Schreibe in die Datei"); // Anweisungen, die eine Exception auslösen können } finally { delete sw; }
verwenden. Wenn man eine try-Anweisung wie hier ohne Exception-Handler verwendet, muss man die Exception in einem umgebenden Exception-Handler abfangen. – Durch einen Sanduhr-Cursor zeigt man dem Anwender oft an, dass gerade eine Aktion ausgeführt wird, die etwas länger dauert. Mit einer try-finally Anweisung kann man sicherstellen, dass der Cursor auch dann wieder auf die ursprüngliche Form zurückgesetzt wird, wenn eine Exception ausgelöst wird: private: System::Void button1_Click( System::Object^ sender, System::EventArgs^ e) { System::Windows::Forms::Cursor^ prevCursor=this->Cursor; try { Cursor=System::Windows::Forms ::Cursors::WaitCursor; // hourglass // Anweisungen, die etwas länger dauern und // eventuell eine Exception auslösen } catch(...){ } // hier Exceptions abfangen finally { Cursor=prevCursor; }
Oft ist RAII einfacher. try-finally ist vor allem dann angemessen, wenn Ressourcen von Klassen reserviert werden, deren Quelltext man nicht verändern möchte oder kann (weil z.B. der Quelltext nicht zur Verfügung steht).
1028
9 C++/CLI
9.9.5 Nicht behandelte Exceptions in Windows Forms-Anwendungen Wenn in einer Windows Forms-Anwendung eine nicht behandelte Exception auftritt, wird ein Dialog wie
angezeigt, in dem man auswählen kann, ob die Anwendung fortgeführt oder abgebrochen werden soll. Die vordefinierte Funktion, die diesen Dialog erzeugt, kann man durch eine eigene Funktion ersetzen, die zu dem Delegat-Typ
delegate void ThreadExceptionEventHandler(Object^ sender, ThreadExceptionEventArgs^ e) kompatibel ist (siehe Abschnitt 9.13.2). In dieser Funktion steht über das Element Exception des Parameters e die auslösende Exception mit allen ihren Informationen zur Verfügung. Eine solche Funktion, wie zum Beispiel void myOnThreadExHdlr(Object^,ThreadExceptionEventArgs^ e) { // using namespace System::Threading; System::Windows::Forms::DialogResult result = MessageBox::Show("Exc-Msg: "+e->Exception->Message, "OnThreadExc", MessageBoxButtons::AbortRetryIgnore); if (result == ::DialogResult::Abort ) { // Bricht das Programm ab, falls Abort angeklickt wird Application::Exit(); } }
wird dann dadurch installiert, dass man sie in der main-Funktion der Anwendung vor dem Aufruf von Application::Run dem Ereignis
static event ThreadExceptionEventHandler^ ThreadException// tritt ein, wenn eine unbehandelte Exception auftritt der Application hinzufügt (siehe Abschnitt 9.13.3). Das ist mit den folgenden Anweisungen möglich: int main(array ^args) { // Kommentare entfernt Application::EnableVisualStyles(); Application::ThreadException += gcnew ThreadExceptionEventHandler(myOnThreadExHdlr); Application::SetCompatibleTextRenderingDefault(false); Application::Run(gcnew Form1());
9.9 C++/CLI Exception-Handling
1029
return 0; }
Wenn dann in der Anwendung eine Exception ausgelöst wird, die nicht in einem try/catch-Block abgefangen wird int div(int n) { return 1/n; } private:System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { int x=div(0); }
führt das zum Aufruf der Funktion myOnThreadExHdlr.
9.9.6 Die Protokollierung von Exceptions in einem EventLog Gelegentlich möchte man bei einer Exception nicht nur eine Meldung für den Anwender ausgeben, sondern diese in einer Datei protokollieren. Dazu kann man gewöhnliche Textdateien verwenden. Besser geeignet sind aber oft EventLogs, die mit der Klasse EventLog aus dem Namensbereich System::Diagnostics angelegt werden. Diese Protokolle kann man mit Systemsteuerung|Verwaltung|Ereignisanzeige auch während der Laufzeit des Programms anschauen. Die Klasse EventLog hat zahlreiche Methoden, von denen hier nur einige der wichtigsten vorgestellt werden. Zum Anlegen eines EventLogs kann
static void CreateEventSource(String^ source, String^ logName) verwendet werden, und zur Prüfung, ob ein EventLog schon angelegt wurde
static bool SourceExists(String^ source) Einen Eintrag in den EventLog kann man mit
static void WriteEntry(String^ source, String^ message) schreiben. Beispiel: Einen EventLog kann man in der main-Funktion der Anwendung anlegen: int main(array ^args) { using namespace System::Diagnostics; if (!EventLog::SourceExists("meineAnwendung")) { EventLog::CreateEventSource("meineAnwendung", "myAppLog"); }
1030
9 C++/CLI ..
In einem Exception-Handler kann man mit Anweisungen wie using namespace System::Diagnostics; EventLog::WriteEntry("meineAnwendung","meine Meldung");
Einträge in den EventLog schreiben. Nimmt man solche Anweisungen in einen ThreadExceptionEventHandler (siehe Abschnitt 9.9.5) auf, werden auch nicht abgefangene Exceptions protokolliert.
9.10 C++/CLI-Erweiterungen für native Klassen Einige der C++/CLI-Erweiterungen gelten auch für native Klassen: – Eine nicht verschachtelte native Klasse kann die Assembly-bezogenen Zugriffsrechte private oder public enthalten (siehe Abschnitt 9.1.4) – Eine native Klasse kann abstract und sealed sein (siehe Abschnitt 9.6.15). – Virtuelle Elementfunktionen können abstract und sealed sein (siehe Abschnitt 9.6.16). – Beim Überschreiben von virtuellen Funktionen kann new und override verwendet werden (siehe Abschnitt 9.6.14). – Eine native Klasse kann verschachtelte Verweis-, Werte- und Interface-Klassen enthalten. – Elementfunktionen einer nativen Klasse können ein Parameter-Array enthalten (siehe Abschnitt 9.11). Beispiel: Die native Klasse C enthält einige dieser Erweiterungen: public class C abstract sealed { virtual void f1() abstract{} virtual void f2() sealed{} ref class R {}; };
Entfernt man in C „sealed“, ist die folgende Ableitung möglich: class D:public C{ virtual void f1() override{} };
9.11 Parameter-Arrays
1031
9.11 Parameter-Arrays Die nicht typsicheren Parameterlisten mit „…“ für unspezifizierte Typen und eine unspezifizierte Anzahl von Argumenten in C werden in C++/CLI durch Parameter-Arrays ergänzt. Parameter-Arrays können sowohl bei globalen Funktionen als auch bei Elementfunktionen verwendet werden.
parameter-array: attributesopt ... parameter-declaration Ein Parameter-Array besteht aus dem Ellipsis-Symbol „…“ und einem eindimensionalen CLI-Arraytyp. Beim Aufruf einer Funktion kann für ein Parameter-Array ein entsprechendes Array oder eine beliebige Anzahl von Argumenten eingesetzt werden, die den Datentyp der Array-Elemente haben oder von ihm abgeleitet sind. Beispiel: Der zweite Parameter der Funktion void f(TextBox^ tb, ...array^ p) { for each(Object^ i in p) tb->AppendText(i->GetType()->ToString()+"\r\n"); }
ist ein Parameter-Array. Für dieses Array kann beim Aufruf der Funktion eine beliebige Anzahl von Argumenten eingesetzt werden, die vom Typ der Array-Elemente abgeleitet sind: f(textBox1); f(textBox1, 17, ""); f(textBox1, 17, 3.14, gcnew Form());
Beim Aufruf einer Funktion mit einem Parameter-Array wird als Argument ein Array erzeugt, das mit den Aufruf-Argumenten initialisiert wird. Beispiel: Die Aufrufe im letzten Beispiel führen zum Aufruf der Funktion f mit den folgenden Argumenten: f(textBox1, gcnew array{}); f(textBox1, gcnew array{17,""}); f(textBox1, gcnew array{17, 3.14, gcnew Form()});
Ein Parameter-Array Parameter kann in der Funktion wie ein gewöhnliches CLIArray verwendet werden. Beispiel: Der zweite Parameter der Funktion g ist ein CLI-Array. Diese Funktion kann mit einem Argument aufgerufen werden, das ein Parameter-Array mit den entsprechenden Typen ist:
1032
9 C++/CLI void g(TextBox^ tb, array^ p) { // ... } void f(TextBox^ tb, ...array^ p) { g(tb, p); }
Aufgabe 9.11 Schreiben Sie eine Funktion writeLine, die als ersten Parameter eine TextBox hat. Beim Aufruf dieser Funktion sollen nach dem TextBox-Argument beliebig viele weitere Argumente angegeben werden können. Diese sollen dann in eine Zeile der TextBox geschrieben werden.
9.12 Visuelle Programmierung und Properties (Eigenschaften) Properties (Eigenschaften) sind spezielle Klassenelemente, die in verwalteten Klassen, aber nicht in nativen C++ Klassen zur Verfügung stehen. Wir haben sie bereits bei der ersten Begegnung mit dem Eigenschaftenfenster kennengelernt und wie Variablen bzw. Datenelemente benutzt: Einer property wurde ein Wert zugewiesen, und eine property wurde wie eine Variable in einem Ausdruck verwendet. 9.12.1 Lesen und Schreiben von Eigenschaften Eine property ist allerdings mehr als eine Variable: Mit einer property können Methoden und Datenelemente zum Lesen bzw. Schreiben verbunden sein. Diese Methoden müssen get und set heißen. Die Funktion get ist dann die Lesefunktion und set die Schreibfunktion. Wenn eine property – eine Funktion mit dem Namen get hat, muss das eine Funktion ohne Parameter sein. Ihr Rückgabetyp muss derselbe Datentyp wie der Datentyp der Property sein. Wenn die property in einem Ausdruck verwendet (gelesen) wird, wird diese Funktion aufgerufen. Ihr Rückgabewert ist dann der Wert der property. – eine Funktion mit dem Namen set hat, muss das eine Funktion mit dem Rückgabetyp void und einem einzigen Werte- oder Konstantenparameter sein, der denselben Datentyp hat wie die property. Bei einer Zuweisung an die property wird dann diese Funktion mit dem Argument aufgerufen, das zugewiesen wird. Die get- und set-Funktionen einer property werden bei der Definition der property als accessor-specification angegeben:
9.12 Visuelle Programmierung und Properties (Eigenschaften)
1033
property-definition: attributesopt property-modifiersopt property type-specifier-seq declarator property-indexesopt { accessor-specification } attributesopt property-modifiersopt property type-specifier-seq declarator ; Beispiel: Für die Eigenschaft x des Datentyps T müssen die Lese- und Schreibmethoden die folgenden Funktionstypen haben: typedef int T; ref class C { T fx; public: property T x { T get() { return fx; } void set(T x_) { fx=x_*x_; }; }; };
Die Klasse C kann folgendermaßen verwendet werden: C^ c=gcnew C; c->x=2; int y=c->x; // y==4
Für einen Entwickler sieht eine Property wie ein „gewöhnliches Datenelement“ aus. Sie unterscheidet sich von einem solchen Datenelement aber dadurch, dass der Zugriff auf eine Property (wenn sie gelesen oder beschrieben wird) mit Anweisungen verbunden werden kann. Dieser Unterschied hat insbesondere zur Folge, dass man von einer Eigenschaft nicht mit dem Adressoperator & die Adresse bestimmen kann. Deshalb kann man eine Property auch nicht als Referenz an eine Funktion übergeben. Properties haben Ähnlichkeiten mit einem überladenen Zuweisungsoperator. Ein solcher Operator ist allerdings im Unterschied zu einer Property immer für die ganze Klasse definiert und nicht nur für ein einzelnes Datenelement. Das Konzept der Properties ist eng mit der visuellen Programmierung verbunden. Da mit einer Zuweisung an eine Property Anweisungen ausgeführt werden können, lässt sich mit der Änderung einer Eigenschaft direkt die visuelle Darstellung der Komponente ändern.
1034
9 C++/CLI
Beispiel: Wird die Eigenschaft Top einer visuellen Komponente verändert, ändert sich nicht nur der Wert des zugehörigen Datenelements, sondern außerdem die grafische Darstellung dieser Komponente: Sie wird an der alten Position entfernt und an der neuen Position neu gezeichnet. Eine Eigenschaft mit dem Attribut Browsable (siehe Abschnitt 9.15.4) wird im Eigenschaftenfenster angezeigt, wenn die Klasse in die Toolbox installiert wurde (siehe Abschnitt 9.15). Da auch beim Setzen einer Eigenschaft im Eigenschaftenfenster zur Entwurfszeit die zugehörige Funktion zum Schreiben aufgerufen wird, kann so auch ihre visuelle Darstellung aktualisiert werden. Properties bilden deshalb die Grundlage für die visuelle Gestaltung eines Formulars zur Entwurfszeit. Für Properties gilt außerdem: – Der Datentyp einer Property kann nahezu beliebig sein. Da er aber den Rückgabe- bzw. den Parametertyp der get bzw. set-Funktionen zum Lesen bzw. Schreiben der Eigenschaft bestimmt, ist er auf zulässige Rückgabe- bzw. Parametertypen beschränkt. Deshalb sind z.B. native Arrays nicht zulässig. – Eine Property kann höchstens eine get-Funktion und höchstens eine set-Funktion enthalten. Falls sie nur eine get- bzw. nur eine set-Funktion hat, spricht man auch von einer read-only bzw. write-only Eigenschaft. Eine Property mit beiden Funktionen wird als read-write Eigenschaft bezeichnet. – Die Zugriffsrechte der set- und get-Methoden können verschieden sein. Damit kann man z.B. das Setzen von Eigenschaften auf Elementfunktionen beschränken und das Lesen über ein Objekt ermöglichen. Diese Zugriffsrechte müssen aber geringer sein als das Zugriffsrecht der Property. – Wie die letzte Zeile der Syntaxregel für eine property-definition zeigt, kann man bei der Definition einer Eigenschaft die geschweiften Klammern mit den set- und get-Funktionen auslassen. Dann spricht man von einer trivialen Eigenschaft. Für eine triviale Eigenschaft erzeugt der Compiler ein Datenelement des Property-Typs sowie get- und set-Funktionen, die den Wert dieses Datenelements setzen oder zurückgeben. Triviale Properties bieten also eine einfache Möglichkeit, die set- und get-Funktionen automatisch zu erzeugen. Beispiel: Die Klasse S hat die triviale Eigenschaft p: ref struct S { property int p; };
– Properties können static sein. Dann sind die set- und get-Methoden static und man kann in diesen Methoden nur auf static Datenelemente zugreifen. Beispiel: Eine static Eigenschaft
9.12 Visuelle Programmierung und Properties (Eigenschaften)
1035
ref struct C { static int fx; static property int x { int get() { return fx; } void set(int x_){fx=x_*x_;}; }; };
wird wie ein static Element angesprochen: CSt::x=17;
– Properties können auch in Interface-Klassen definiert werden. Dann dürfen die set- und get-Funktionen nur Deklarationen sein, aber keine Definitionen. Eine triviale Property besteht wie in einer Nicht-Interface-Klasse nur aus einer Deklaration. Beispiel: Durch die Definition interface class InterfaceWithProp { property int ordProp { int get(); void set(int); } property int trivProp; };
wird festgelegt, dass eine von InterfaceWithProp abgeleitete Klasse die Eigenschaften ordProp (eine gewöhnliche Property) und trivProp (eine triviale Property). Interface-Klassen mit Eigenschaften werden in .NET oft verwendet. Beispielsweise hat die Interface-Klasse ICollection hat eine Eigenschaft Count.
9.12.2 Indizierte Properties Mit indizierten Properties („indexed properties“, Indexer) kann man Properties wie ein Array mit einem Index ansprechen. Dazu wird bei der Definition der Eigenschaft nach ihrem Namen in eckigen Klammern eine Liste von Datentypen (property-index-parameter-list) angegeben:
property-definition: // nur ein Auszug attributesopt property-modifiersopt property type-specifier-seq declarator property-indexesopt { accessor-specification } property-indexes: [ property-index-parameter-list ]
1036
9 C++/CLI
property-index-parameter-list: type-id property-index-parameter-list , type-id Bei einer indizierten Eigenschaft müssen die get- und set-Funktionen folgendermaßen aufgebaut sein: – Die Parameter der get-Funktion müssen dieselben Datentypen haben wie die property-index-parameter-list. Der Rückgabetyp muss der Datentyp der Property sein. – Die ersten Parameter der set-Funktion müssen dieselben Datentypen haben wie die property-index-parameter-list. Der letzte Parameter muss den Datentyp der Property haben, und der Rückgabetyp muss void sein. Eine indizierte Property kann dann wie ein ein- oder mehrdimensionales Array mit so vielen Indizes angesprochen werden, wie Datentypen bei der Definition der Property angegeben werden. Die Indizes werden dann als Parameter an die setoder get-Funktionen übergeben. Der Index-Datentyp muss nicht wie bei einem Array ganzzahlig sein (siehe Aufgabe 9.12, 2.). Beispiel: Die Klasse C hat eine indizierte Property ip: public ref class C { array^ a; public: C():a(gcnew array(5)) {} property int ip[int] { double get(int index) { return a[index]; } void set(int index, double value) { a[index] = value; } } }
Die Property ip kann dann wie ein Array angesprochen werden: C^ c = gcnew C(); for (int i = 0 ; i < 5 ; i++) c->ip[i] = i*3.14;
Verwendet man bei einer indizierten Eigenschaft anstelle des Namens einer Property das Wort default, erhält man eine default-indizierte Property. Mit einer solchen Eigenschaft kann man direkt nach dem Namen eines Objekts der Klasse den Indexoperator angeben. Beispiel: Die Klasse C hat eine default-indizierte Property:
9.12 Visuelle Programmierung und Properties (Eigenschaften)
1037
public ref class C { array^ a; public: C():a(gcnew array(5)) {} property int default[int] { double get(int index) { return a[index]; } void set(int index, double value) { a[index] = value; } } }
Damit kann ein Objekt der Klasse wie ein Array angesprochen werden: C^ c = gcnew C(); for (int i = 0 ; i < 5 ; i++) c[i] = i*3.14;
Eine indizierte Property kann nicht trivial sein. Die get- oder set-Funktionen müssen angegeben werden. Einige Beispiele für die Verwendung von indizierten Properties in der .NET Klassenbibliothek: 1. Die Klassen String und StringBuilder haben eine default-indizierte Eigenschaft, die den Zugriff auf die einzelnen Zeichen eines Strings mit dem Indexoperator ermöglicht:
property wchar_t default [int]; 2. Die Interface-Klasse IList definiert eine default-indizierte Eigenschaft
virtual property Object^ default [int] die von allen Collection-Klassen, die dieses Interface implementieren, zur Verfügung gestellt werden muss. Sie ermöglicht den Zugriff auf die Elemente mit dem Index-Operator.
9.12.3 Properties und Vererbung Die get- oder set-Methoden einer Property können virtuell sein und in abgeleiteten Klassen überschrieben werden. Wenn virtual vor property angegeben wird, sind alle Methoden der Property virtuell. So kann die Verwendung einer Eigenschaft in verschiedenen Klassen einer Klassenhierarchie mit verschiedenen Anweisungen verbunden sein.
1038
9 C++/CLI
Beispiel: ref class C { protected: int fx; public: property int x { virtual void set(int x) {fx=x;} virtual int get(){return fx;}; }; }; ref class D: C { public: property int x { virtual void set(int x) override {fx=x*x;} }; };
Properties können außerdem sealed und abstract sein. Die Bedeutung dieser Angaben entspricht dann der bei Funktionen, die nicht zu Properties gehören. Properties von Interface-Klassen sind implizit abstract.
Aufgabe 9.12 1. Überarbeiten Sie die Klassen Punkt und Kreis von Aufgabe 9.6.1 so, dass die Datenelemente durch Properties dargestellt werden. Verfolgen Sie im Debugger schrittweise, zu welchen Aufrufen Zuweisungen von und an diese Eigenschaft führen. 2. Die Klasse ref class AssozContainer { // alles sehr einfach int n; // Anzahl der Elemente im Container ref struct Paar { String^ first; // Schlüsselwert String^ second; // Daten }; array^ a; public: // hier Lösung einfügen AssozContainer(int Max):n(0) { a=gcnew array(Max); }; void showAll(TextBox^ tb) { for (int i=0; iAppendText(a[i]->first+": "+a[i]->second+"\r\n"); } };
9.13 Delegat-Typen und Events
1039
soll Wertepaare des Typs Paar speichern und mit der Methode showAll anzeigen. Ergänzen Sie diese Klasse a) um eine default-indizierte Eigenschaft, mit der ein Paar (i,v) von Strings durch eine Zuweisung wie a[i]=v; in der nächsten freien Position des Arrays der Klasse abgelegt wird. a[i] soll der Wert second des Paars (i,second) aus dem Array sein, falls ein solches Paar im Array enthalten ist. Beispiel: Nach AssozContainer a(10); a["Daniel"]="13.11.79"; a["Alex"]="17.10.81"; a.showAll(textBox1);
soll Daniel: 13.11.79 Alex: 18.10.81 ausgegeben werden, und a["Alex"]
soll den Wert "17.10.81" haben. b) um zwei indizierte Eigenschaften first und second, mit denen man den Wert first bzw. second des i-ten Arrayelements lesen, aber nicht schreiben kann. c) eine Eigenschaft read-only size, die den Wert von n zurückgibt.
9.13 Delegat-Typen und Events Eine weitere Erweiterung von C++/CLI gegenüber Standard-C++ sind DelegatTypen. Ein Delegat (eine Objekt eines Delegat-Typs) fasst Funktionen zusammen, die dann über dieses Objekt aufgerufen werden können. Damit lassen sich Ausdrücke, die in Standard-C++ mit Funktionszeigern (siehe Abschnitt 5.2) und Zeigern auf Elementfunktionen (siehe Abschnitt 6.4.12) gebildet werden, einfacher und einheitlich formulieren. Delegat-Typen und –Objekte werden in .NET oft verwendet:
1040
9 C++/CLI
– Ereignisbehandlungsroutinen (z.B. ButtonClick-Funktionen) werden durch Events (siehe Abschnitt 9.13.3) dargestellt, die auf Delegat-Typen basieren. – die Parameterübergabe von Funktionen (sowohl globale Funktionen als auch Elementfunktionen) ist damit einfacher als mit Funktionszeigern. Die in Abschnitt 8.3.3 vorgestellten Binder und Funktionsadapter (insbesondere bind und mem_fn) bieten ähnliche Möglichkeiten wie Delegat-Typen.
9.13.1 Ausgangspunkt: Funktionszeiger Zur Einführung in die Problematik wird zunächst kurz gezeigt, wie in klassischem Standard-C/C++ Funktionen als Parameter übergeben werden können. Wenn man eine Funktion f_param definieren will, der man eine Funktion wie int f(double x){ return 0;}
als Argument übergeben kann, muss der Datentyp des Parameters von f_param ein Funktionszeiger (siehe Abschnitt 5.2) sein: void f_param(int g(double)) {}//Parameter: Funktionszeiger
Diese Funktion kann man aber nicht mit einem Argument aufrufen, das eine Elementfunktion einer Klasse ist (Datentyp: Zeiger auf eine Elementfunktion, siehe Abschnitt 6.4.12), da einer solchen Elementfunktion immer auch noch ein this-Zeiger auf die Klasse übergeben wird: struct C { int h(double x){} }
Damit man eine Elementfunktion als Argument übergeben kann, muss der Parameter ein Zeiger auf eine Elementfunktion sein: typedef int(C::* PMF)(double);//Zeiger auf Elementfunktion void ef_param(PMF x) { C c; (c.*x)(1.0); }
Die Funktion ef_param kann man mit einer Elementfunktion der Klasse C aufrufen, die denselben Rückgabe- und Parametertyp wie PMF hat: ef_param(&C::h);
Ein Aufruf mit einer Elementfunktion einer anderen Klasse (auch einer von C abgeleiteten, Ausnahme: Kontravarianz, siehe Seite 788) ist aber nicht möglich, auch wenn diese dieselbe Signatur hat:
9.13 Delegat-Typen und Events
1041
struct D:C { int h(double x){return 1;} }; ef_param(&D::h); // error: nicht möglich
Diese Einschränkungen werden in C++/CLI mit Delegat-Typen vermieden, die eine typsichere Übergabe von Funktionen mit derselben Signatur ermöglichen.
9.13.2 Delegat-Typen und -Instanzen Im Prinzip könnte eine Bibliothek für eine graphische Benutzeroberfläche wie .NET auch mit Funktionszeigern geschrieben werden: Jedes Ereignis (z.B. ein ButtonClick) wird dann durch einen Funktionszeiger dargestellt. Wenn einem solchen Zeiger eine Funktion zugeordnet wird, wird diese beim entsprechenden Ereignis aufgerufen. Wenn der Zeiger dagegen den Wert 0 hat, wird das Ereignis ignoriert. Angesichts der zentralen Bedeutung von Ereignissen wäre damit die Entwicklung von Anwendungen aber aufwendig und fehleranfällig. Deswegen wurden andere Sprachelemente wie Delegat-Typen und Events entwickelt. Ein Delegat-Typ ist eine Verweisklasse (also ein Datentyp), die nach dem Schema delegate-specifier: attributesopt top-level-visibilityopt delegate type-specifier-seq declarator ;
global, in einem Namensbereich oder innerhalb einer Verweis- oder Werteklasse definiert wird. In einer solchen Definition müssen die Angaben nach delegate eine Funktionsdeklaration sein. Der Name der Funktion ist dann der Name des Delegat-Typs. Funktionen mit demselben Rückgabetyp und derselben Parameterliste wie in der Funktionsdeklaration werden als kompatibel zum Delegat-Typ bezeichnet, unabhängig davon, ob es globale, statische oder nicht-statische Elementfunktionen sind. Der Rückgabetyp in der Funktionsdeklaration ist der Rückgabetyp des Delegat-Typs. Beispiel: Durch delegate void D1(int, String^);
wird ein Delegat-Typ mit dem Namen D1 definiert. Obwohl diese Definition überhaupt nicht wie die einer Klasse aussieht, sondern eher wie eine Funktionsdeklaration, wird hier eine Klasse D1 definiert. Die Delegat-Typen D2 und D3 werden innerhalb einer Verweis- oder Werteklasse definiert: ref class R { delegate void D2(bool); };
1042
9 C++/CLI value class V { delegate String^ D3(int^, int*); };
In einer nativen Klasse oder in einem Block ist eine solche Definition aber nicht möglich. Zum Delegat-Typ D1 sind die globale Funktion f, die statischen Elementfunktionen rs und vs sowie rf kompatibel: using namespace System::Diagnostics; // für trace void f(int i, String^ s){Trace::WriteLine("f"); } ref class R { public: static void rs(int x, String^ s) { Trace::WriteLine("rs"); } void rf(int x, String^ s) { Trace::WriteLine("rf"); } }; value class V { public: static void vs(int i, String^ s) { Trace::WriteLine("vs"); } };
Ein Objekt eines Delegat-Typs wird als Delegat, „Delegat-Objekt“ oder „DelegatInstanz“ bezeichnet. Ein Delegat kann Funktionen zusammenfassen, die zum Delegat-Typ kompatibel sind. Ein Delegat-Typ hat zwei Konstruktoren. Diesen kann eine Funktion übergeben werden, die dann zum Delegat-Objekt hinzugefügt wird: – Der erste akzeptiert als Argument eine globale Funktion oder eine statische Elementfunktion, die zum Delegat-Typ kompatibel ist. Eine globale Funktion wird dabei einfach mit ihrem Namen übergeben. – Der zweite Konstruktor akzeptiert als erstes Argument a1 ein Objekt einer CLIKlasse, und als zweites Argument eine statische oder nicht statische Elementfunktion des Typs von a1, die zum Delegat-Typ kompatibel ist. Elementfunktionen werden diesen Konstruktoren mit der Syntax eines Zeigers auf eine Elementfunktion übergeben. Für eine Elementfunktion f einer Klasse C ist das der Ausdruck &C::f Delegat-Objekte können mit den Operatoren + und += kombiniert werden. Das Ergebnis ist ein Delegat-Objekt, das die Funktionen der Operanden enthält. Diese Funktionen werden auch als Aufrufliste des Delegat-Objekts bezeichnet. Mit –
9.13 Delegat-Typen und Events
1043
bzw. –= werden die Funktionen der Aufrufliste des rechten Operanden aus der des linken Operanden entfernt. Beispiel: Die folgenden Aufrufe von gcnew D1 erzeugen jeweils ein DelegatObjekt. und fügen dieses zu d1 hinzu: D1^ d1=gcnew D1(f); d1 += gcnew D1(&R::rs); d1 += gcnew D1(&V::vs); R^ r=gcnew R; d1 += gcnew D1(r,&R::rf);
Definiert man einen Delegat-Typ wie D2 innerhalb einer Klasse ref class RD { public: delegate void D2(int, String^); };
muss man bei der Definition eines Delegat-Objekts wie d2 auch die Klasse angeben. d2 können dann dieselben Funktionen wie im letzten Beispiel hinzugefügt werden: RD::D2^ d2=gcnew RD::D2(f); d2 += gcnew RD::D2(&R::rs); d2 += gcnew RD::D2(&V::vs); R^ r=gcnew R; d2 += gcnew RD::D2(r,&R::rf);
Ein Delegat-Objekt kann aufgerufen werden. Dazu gibt man nach dem Namen des Delegat-Objekts wie bei einer Funktion Argumente an, die zu den Funktionen der Aufrufliste passen. Der Aufruf des Delegat-Objekts führt dann zum Aufruf aller Funktionen der Aufrufliste. Bei einer leeren Aufrufliste wird eine Exception des Typs System::NullReferenceException ausgelöst. Beispiel: Die Delegat-Objekte d1 und d2 aus dem letzten Beispiel können folgendermaßen aufgerufen werden und geben die als Kommentar angegebenen Meldungen aus: d1(17,"18"); // Ausgabe "f d2(18,"19"); // Ausgabe "f
rs rs
vs vs
rf" rf"
Das Delegat-Objekt y hat eine leere Aufrufliste. Sein Aufruf löst eine Exception aus: D1^ y=gcnew D1(f); y -= gcnew D1(f); y(19,"20"); // Exception
Falls ein Delegat-Typ einen Rückgabetyp hat, ist der Rückgabewert eines Delegat-Objekts der Rückgabewert der zuletzt aufgerufenen Funktion.
1044
9 C++/CLI
Da ein Delegat-Typ ein Datentyp ist, kann ein Delegat-Typ auch als Datentyp eines Parameters einer Funktion verwendet werden. Beispiel: Die Klasse Thread (siehe Abschnitt 10.9.3) hat einen Konstruktor
Thread(ThreadStart^ start) dessen Parameter ein Delegat-Typ ist:
delegate void ThreadStart() Eine Thread-Variable kann deshalb wie in der Funktion useDelegateParameter initialisiert werden void f(){}; void useDelegateParameter() { using namespace System::Threading; Thread^ t = gcnew Thread(gcnew ThreadStart(f)); }
Algorithmen der STL/CLR (wie z.B. sort, siehe Abschnitt 4.2.3) können nicht nur Funktionen bzw. Funktionszeiger, sondern auch Delegat-Objekte übergeben werden. Beispiel: Nach den Definitionen bool compare(int a, int b) { return aClick=gcnew myEventHandler(this,&myForm1:: myButton1Click); } void myButton1Click(Object^ sender, myEventArgs^ evArgs) { using namespace System::Diagnostics;//für trace Trace::WriteLine("myClickAction: "); } };
Wenn dann im Button b1 die Funktion OnClick aufgerufen wird, führt das zum Aufruf der Funktion myButton1Click.
9.13.3 Ereignisse (events) Events haben große Ähnlichkeiten mit Delegat-Objekten. Ein Event wird im einfachsten Fall nach dem folgenden Schema definiert: event event-type identifier ;
Dabei muss der Datentyp (event-type) des Events ein Delegat-Typ sein. Einem Event kann mit dem Operator += eine Funktion hinzugefügt werden, die zum Delegat-Typ kompatibel ist. Mit –= kann eine solche Funktion entfernt werden. Eine Zuweisung mit = ist aber im Unterschied zu Delegat-Objekten nicht möglich. Die wichtigsten Unterschiede zwischen Events und Delegat-Typen bzw. Objekten: – Elemente eines Delegat-Typs (ein Datentyp) sind in einer Interface-Klasse im Unterschied zu einem event nicht zulässig. – Ein Event kann nur von einer Elementfunktion der eigenen Klasse aufgerufen werden. Ein Delegat-Objekt kann dagegen überall aufgerufen werden, wo ein Zugriffsrecht besteht. – Der Aufruf eines leeren Events löst im Unterschied zum Aufruf eines leeren Delegat-Typs keine Exception aus. – Events haben Zugriffsfunktionen add und remove (siehe Abschnitt 9.13.5). Beispiel: .NET verwendet für seine Ereignisbehandlungsroutinen nicht wie im letzten Beispiel Delegat-Objekte, sondern Ereignisse wie in myButton2. Das Wort event ist der einzige Unterschied zu myButton1: ref struct myButton2 { event myEventHandler^ Click; protected: void OnClick(Object^ sender,myEventArgs^ evArgs) { Click(sender,evArgs); } };
9.13 Delegat-Typen und Events
1047
Nimmt man einen solchen myButton2 myButton2^ b2;
in myForm1 (aus dem letzten Beispiel) auf, kann man diesem wie dem delegate in b1 eine Funktion zuordnen. Allerdings muss dazu der Operator „+=“ verwendet werden (der Operator „=“ wird nicht akzeptiert): myForm1() // Konstruktor { b2=gcnew myButton2; this->b2->Click+=gcnew myEventHandler(this, &myForm1::myButton1Click); b1=gcnew myButton1; b1->Click=gcnew myEventHandler(this, &myForm1::myButton1Click); }
Wenn dann in b2 das Ereignis OnClick ausgelöst wird, führt wie in b1 das zum Aufruf der Funktion myButton1Click. Dieses Beispiel hat bisher noch keinen wesentlichen Unterschied zwischen Delegat-Objekten und Events gezeigt. Events kapseln aber die Funktion stärker in die Klasse und verhindern, dass sie von außerhalb aufgerufen werden kann. Auf diese Weise können die teilweise recht kniffligen internen Abläufe bei den .NETEreignisbehandlungsroutinen sichergestellt werden. Beispiel: Die unterschiedlichen Zugriffsrechte sieht man bei b1 und b2. Die Funktion Click kann über b1 aufgerufen werden, während ihr Aufruf über b2 zu einer Fehlermeldung „Zugriff auf raise nicht möglich“ führt: myForm1^ f=gcnew myForm1; f->b1->Click(nullptr,nullptr); // das geht f->b2->Click(nullptr,nullptr); // error
Die Events in einer Windows Forms-Anwendung sind ähnlich definiert. Alle Steuerelemente (Datentyp von der Klasse Control abgeleitet, siehe Abschnitt 9.14.2) können auf Ereignisse reagieren. Sie erben von Control Events wie
event EventHandler^ Click = {... }; //wenn die Komponente event MouseEventHandler^ MouseClick // angeklickt wird event EventHandler^ DoubleClick = {...}; Dabei sind die EventHandler Delegat-Typen wie
public delegate void EventHandler(Object^ sender, EventArgs^ e) Nach einem Doppelklick auf die rechte Spalte der Seite Ereignisse im Eigenschaftenfenster erzeugt Visual Studio eine Funktion wie
1048
9 C++/CLI
System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { }
und fügt diese in InitializeComponent dem entsprechenden Event hinzu: this->button1->Click
+=
gcnew
System::EventHandler(this, &Form1::button1_Click);
Da der Konstruktor des Formulars beim Start des Programms InitializeComponent aufruft, wird so die Verbindung zwischen dem Ereignis und der aufgerufenen Funktion hergestellt. Durch entsprechende Anweisungen können zur Laufzeit aber auch andere Funktionen festgelegt werden (siehe Aufgaben 1 und 2). Auch die Klasse Application enthält einige Ereignisse, wie z.B.:
static event EventHandler^ ApplicationExit // tritt ein, wenn die Anwendung gerade beendet wird static event ThreadExceptionEventHandler^ ThreadException// tritt ein, wenn eine unbehandelte Exception auftritt Da Application nicht im Eigenschaftenfenster angezeigt wird, kann man die Ereignisbehandlungsroutinen für diese Ereignisse nicht wie bei einem Steuerelement durch ein Anklicken des Ereignisses im Eigenschaftenfenster erzeugen lassen. Wenn man auf diese Ereignisse reagieren will, muss man ihnen mit += Funktionen hinzufügen. Beispiele dazu finden sich in Abschnitt 9.9.5.
9.13.4 Selbst definierte Komponenten und ihre Ereignisse Bei einer Windows Forms-Anwendung gestaltet man die Benutzeroberfläche normalerweise mit den Hilfsmitteln von Visual Studio (Eigenschaftenfenster, Formular- oder Menüdesigner usw.). Aber man kann Formulare und Steuerelemente auch ohne diese Hilfsmittel während der Laufzeit eines Programms erzeugen. Auf diese Weise können Bedingungen berücksichtigt werden, die sich erst während der Laufzeit ergeben und die zur Entwurfszeit noch nicht bekannt sind. Damit ein solches Steuerelement auf einem Formular f angezeigt wird, muss man es mit der Methode
virtual void Add(Control^ value) der Eigenschaft
property Control::ControlCollection^ Controls der Klasse Form dem Formular f hinzufügen.
9.13 Delegat-Typen und Events
1049
Beispiel: Die Funktion makeTextBox erzeugt eine TextBox und fügt diese dem als Argument übergebenen Formular hinzu: TextBox^ makeTextBox(Form^ f, int x, int y, int w, int h) { TextBox^ t = gcnew TextBox(); t->Location = System::Drawing::Point(x, y); t->Size = System::Drawing::Size(w, h); t->Multiline = true; t->Name = L"makeTextBox1"; f->Controls->Add(t); return t; };
Diese Funktion kann dann in einer Elementfunktion eines Formulars so aufgerufen werden: TextBox^ t=makeTextBox(this,10,10,20,50); t->AppendText("bla bla bla");
Da so alle Eigenschaften wie im Eigenschaftenfenster gesetzt werden können, hat man dieselben Gestaltungsmöglichkeiten wie bei der visuellen Programmierung. Eine Komponente kann auch von einer geeigneten Basisklasse abgeleitet und in ihrem Konstruktor initialisiert werden. Beispiel: Die von TextBox abgeleitete Komponente MyTextBox erhält ihre Positionsangaben im Konstruktor: ref class MyTextBox:TextBox { public: MyTextBox(Form^ f, int x, int y, int w, int h): TextBox() { this->Location = System::Drawing::Point(x, y); this->Size = System::Drawing::Size(w, h); this->Multiline = true; this->Name = L"my textBox1"; f->Controls->Add(this); } };
Die folgenden Anweisungen entsprechen denen des letzten Beispiels: MyTextBox^ t1=gcnew MyTextBox(f1,10,10,20,50); t1->Text="MyTextBox";
Man kann solche Klassen aber nicht nur dynamisch während der Laufzeit des Programms erzeugen: In Abschnitt 9.15 wird gezeigt, wie man solche Klassen in die Toolbox installieren und dann wie die vordefinierten Komponenten verwenden kann.
1050
9 C++/CLI
Mit den von der Klasse Control geerbten Events wie
event EventHandler^ Click = {... }; //wenn die Komponente event MouseEventHandler^ MouseClick // angeklickt wird event EventHandler^ DoubleClick = {...}; können auch dynamisch erzeugte Steuerelemente auf Ereignisse reagieren. Dazu muss man einem solchen Event nur wie in Abschnitt 9.13.3 entsprechende EventHandler hinzufügen. Die Funktion wird dann aufgerufen, wenn das Ereignis eintritt. Beispiel: Der Delegat-Typ EventHandler ist folgendermaßen definiert:
delegate void EventHandler(Object^ sender, EventArgs^ e) Fügt man eine Funktion, die wie void h(Object^ sender, EventArgs^ e) { MessageBox::Show("xxx"); }
zu diesem Delegat-Typ kompatibel ist, dem Click-Ereignis einer von Control abgeleiteten Komponente hinzu, wird diese Funktion beim Anklicken des zur Laufzeit erzeugten Steuerelements aufgerufen. Mit den Beispielen von oben erreicht man das mit diesen Anweisungen: TextBox^ t=makeTextBox(f1,10,10,20,500); t->Click+=gcnew EventHandler(h); MyTextBox^ tb1= gcnew MyTextBox(f1,10,10,20,500) tb1->Click+=gcnew EventHandler(h);
Aufgabe 9.13.4 1. Bei vielen Programmen wird beim Auftreten eines bestimmten Ereignisses (z.B. Click) immer die zur Entwurfszeit festgelegte Funktion aufgerufen. Manchmal ist es aber auch notwendig, die aufgerufene Funktion in Abhängigkeit von Laufzeitbedingungen erst zur Laufzeit festzulegen. Das ist möglich, indem man dem event für dieses Ereignis eine andere Funktion zuweist. Schreiben Sie eine Funktion
void changeEventHandler(Control^ c) die dem Click-Ereignis des als Argument übergebenen Steuerelements bei jedem Aufruf eine andere Funktion zuweist (z.B. beim ersten Aufruf eine Funktion eh1, beim zweiten eh2, beim dritten eh3, und dann wieder eh1 usw.). Jede dieser Funktionen soll in einer MessageBox eine Meldung anzeigen, die anzeigt, welche Funktion aufgerufen wurde.
9.13 Delegat-Typen und Events
1051
2. Ein Menüeintrag wird durch ein Objekt der .NET-Klasse ToolStripMenuItem dargestellt und kann durch die folgenden Anweisungen erzeugt werden: ToolStripMenuItem^ mit=gcnew ToolStripMenuItem; mit->Text="MeinMenuItem";// Menütext
Ein solcher Menüeintrag kann sowohl ein Eintrag in der Menüleiste (Datentyp MenuStrip) als auch ein Eintrag in einem Menü (Datentyp ToolStripMenuItem) sein. Im ersten Fall wird er durch die Methode Items->Add eines MenuStrip in die Menüleiste aufgenommen menuStrip1->Items->Add(mit);
und im zweiten Fall durch die Methode Add der Eigenschaft DropDownItems des ToolStripMenuItem dem Menü m hinzugefügt: m->DropDownItems->Add(mit);
a) Schreiben Sie eine Funktion NewMenuBarItem, die einen neuen Menüeintrag erzeugt und in einen als Parameter übergebenen MenuStrip einhängt. Der Menütext soll ebenfalls als Parameter übergeben werden. b) Schreiben Sie eine Funktion NewMenuItem, die einen Menüeintrag erzeugt und einem als Parameter übergebenen ToolStripMenuItem hinzufügt. Der EventHandler, der beim Anklicken des Menüeintrags aufgerufen werden soll, sowie der Menütext sollen ebenfalls als Parameter übergeben werden. c) Schreiben Sie eine Funktion makeMenu, die in die Menüleiste eines als Parameter übergebenen MenuStrip einen Eintrag mit der Aufschrift „Datei“ sowie die beiden Untereinträgen „Neu“ und „Öffnen“ erzeugt. Beim Anklicken eines der Untereinträge soll eine Funktion aufgerufen werden, die den Text „Datei Neu“ bzw. „Datei öffnen“ in einer MessageBox anzeigt. 3. Definieren Sie die unter a) bis c) beschriebenen Komponenten. Damit diese in Aufgabe 9.15 ohne Änderungen in die Toolbox installiert werden können, schreiben Sie ihre Klassendefinition in eine Headerdatei (z.B. mit dem Namen „myComponents.h“) und verwenden diese mit einer #include-Anweisung. Jede dieser Klassen soll einen Konstruktor haben, dem wie in MyTextBox (siehe Abschnitt 9.13.4) ein Formular und Angaben für die Größe und Position übergeben werden können. a) Schreiben Sie eine von TextBox abgeleitete Komponente EnterNextTextBox. Über eine boolesche Eigenschaft EnterNext soll festgelegt werden können, ob das Drücken der Enter-Taste den Fokus auf das nächste Steuerelement setzt oder nicht. Das Drücken der Enter-Taste kann beim Ereignis KeyPress erkannt werden. Dieses Ereignis ruft eine Funktion des Typs
void KeyPressEventHandler(Object^ sender, KeyPressEventArgs^ e)
1052
9 C++/CLI
auf, der im Argument für e das gedrückte Zeichen als Element KeyChar (Datentyp char) übergeben wird. Der folgende EventHandler hat das gewünschte Verhalten zur Folge: void myEnterEventHandler(Object^, KeyPressEventArgs^ e) { if (EnterNext && e->KeyChar == 13) // Die Enter-Taste { // hat den Wert 13 this->Parent->SelectNextControl(this,true,true,true, true); e->Handled = true; // notwendig, damit das Ereignis } // als behandelt betrachtet wird }
b) Schreiben Sie eine von TextBox abgeleitete Komponente FocusColorTextBox. Diese soll automatisch eine auswählbare Hintergrundfarbe erhalten, sobald sie den Fokus erhält (Ereignis Enter). Verliert sie den Fokus (Ereignis Leave), soll sie wieder die ursprüngliche Hintergrundfarbe erhalten. c) Schreiben Sie eine von TextBox abgeleitete Klasse ValueTextBox mit einer double-Eigenschaft Value. Ein dieser Eigenschaft zugewiesener Wert soll mit der Anzahl von Nachkommastellen als Text der TextBox angezeigt werden, die einer Eigenschaft Nachkommastellen entspricht. Zur Formatierung des Wertes können Sie die Funktion ToString mit einem Formatstring der Art "0.00" (für zwei Nachkommastellen) verwenden. 4. Eine Klasse Server soll eine Methode callAllClients haben, die für alle beim Aufruf dieser Methode existierenden Objekte einer Klasse Client deren Methode processMsg aufruft. Diese Aufgabe kann mit einer Klasse Server gelöst werden, die einen statischen EventHandler enthält. Diesem EventHandler wird im Konstruktor der Klasse Client eine Methode mit dem Namen processMsg hinzugefügt, die eine als Parameter übergebene Meldung in eine ebenfalls als Parameter übergebene TextBox ausgibt. Im Destruktor von Client wird diese Methode wieder aus dem EventHandler entfernt. Damit man sieht, für welches Objekt processMsg aufgerufen wurde, soll Client außerdem noch ein String-Element clientName enthalten, das im Konstruktor gesetzt wird. Ein Aufruf der Funktion void test(TextBox^ tb) { Server::callAllClients("Message 1"); Client c1(tb, "A"); Server::callAllClients("Message 2"); { Client c2(tb, "B"); Server::callAllClients("Message 3"); { Client c3(tb, "C");
9.13 Delegat-Typen und Events
1053
Server::callAllClients("Message 4"); } Aufruf des Destruktors von c3 Server::callAllClients("Message 5"); } Server::callAllClients("Message 6"); }
soll dann die folgenden Meldungen ausgeben: A A B A B C A B A
received received received received received received received received received
message message message message message message message message message
'Message 'Message 'Message 'Message 'Message 'Message 'Message 'Message 'Message
2' 3' 3' 4' 4' 4' 5' 5' 6'
9.13.5 Nichttriviale und Statische Ereignisse Wenn ein Ereignis wie im Beispiel von Abschnitt 9.13.3 nach dem Schema event event-type identifier ;
definiert wird, bezeichnet man es als triviales Ereignis. Man kann aber nach dem Namen des Ereignisses auch noch eine accessor-specification angeben: event event-type identifier { accessor-specification} ;
Ein solches Ereignis wird dann als nichttriviales Ereignis bezeichnet. Die accessor-specification muss zwei Funktionen mit den Namen add und remove enthalten, die jeweils einen Parameter des event-Typs und den Rückgabetyp void haben. Außerdem kann noch eine Funktion mit dem Namen raise angegeben werden, die dieselbe Parameterliste und denselben Rückgabetyp wie der Delegat Event-Typ hat. Andere Funktionen sind nicht zulässig. Diese Funktionen werden dann aufgerufen, wenn Handler hinzugefügt (add), entfernt (remove) oder ein Ereignis ausgelöst wird (raise). definiert. Bei einem trivialen Ereignis werden diese Funktionen vom Compiler erzeugt. Beispiel: Die meisten Windows Steuerelemente verwenden wie ein Button nichttriviale Ereignisse: public:event EventHandler^ Click { void add (EventHandler^ value); void remove (EventHandler^ value); }
1054
9 C++/CLI
Die Zugriffsfunktionen können mit den Angaben virtual, sealed, abstract und override definiert werden. Diese Angaben haben die übliche Bedeutung. Ereignisse können mit der Angabe static und virtual definiert werden. Diese Angaben beziehen sich dann auf alle Zugriffsfunktionen.
9.14 Ein kleiner Überblick über die .NET Klassenbibliothek Nachdem in den letzten Abschnitten einige der wichtigsten Sprachelemente von C++/CLI vorgestellt wurden, soll jetzt gezeigt werden, wie diese in der .NETKlassenbibliothek verwendet werden. Damit wird außerdem ein Überblick über die Hierarchie. der .NET-Klassenbibliothek verbunden. Diese Hierarchie ist dann die Grundlage für selbstdefinierte Komponenten und die Erweiterung der Toolbox. Die spezielle Architektur dieser Klassenbibliothek ist die Grundlage für die einfache Bedienbarkeit und die Vielseitigkeit von Windows Forms-Anwendungen in Visual Studio. Da die .NET-Klassenbibliothek in die Entwicklungsumgebung von Visual Studio integriert ist, können Komponenten aus der Toolbox auf ein Formular gesetzt und ohne Schreibarbeit in ein Programm aufgenommen werden. Dabei werden die beim Entwurf eines Formulars gesetzten Eigenschaften in das Programm übernommen. Die Toolbox kann man außerdem um eigene Komponenten ergänzen, die dann ebenfalls in die Entwicklungsumgebung integriert werden. Auch von solchen selbstdefinierten Komponenten können Eigenschaften im Objektinspektor gesetzt werden. .NET ist eine umfangreiche Klassenbibliothek. Angesichts dieses Umfangs soll hier nur der Aufbau skizziert werden. Eine vollständige Beschreibung würde viele Tausend Seiten füllen und ist deshalb auch nicht beabsichtigt. Für weitere Informationen wird auf die Online-Hilfe verwiesen.
9.14.1 Komponenten und die Klasse Component Alle .NET-Klassen sind von der Klasse Object (siehe Abschnitt 9.2) abgeleitet. Dazu gehören insbesondere die folgenden Klassen:
Object |– Exception |– MarshalByRefObject |– String // siehe Abschnitt 3.13 ... // und viele weitere Klassen – Exception ist die Basisklasse für alle CLI-Exceptions (siehe Abschnitt 9.9.2). Daraus lassen sich eigene Exception-Klassen ableiten wie ref class MyException : public Exception{};
9.14 Ein kleiner Überblick über die .NET Klassenbibliothek
1055
ref class MyDivBy0Exc : public DivideByZeroException{};
– MarshalByRefObject ist eine abstrakte Basisklasse für zahlreiche Klassen, die betriebssystem-bezogene Dienste zur Verfügung stellen.
Object |– Exception |– MarshalByRefObjec |– Stream // abstrakte Basisklasse für alle .NET-Streamklassen |– Component |– Graphics // GDI+-Zeichenfläche (siehe Abschnitt 10.10) ... Eine Klasse, die in die Toolbox aufgenommen werden und auf ein Formular gezogen werden kann, wird unter .NET auch als Komponente bezeichnet (siehe Abschnitt 9.15.2). Die Klasse Component ist eine Basisimplementierung einer Komponente, von der man eigene Komponenten ableiten kann:
Object |– Exception |– MarshalByRefObjec |– Stream |– Component |– Control |– Menu |– Timer ... 9.14.2 Steuerelemente und die Klasse Control Control ist die Basisklasse für alle Steuerelemente (Controls). Das sind sichtbare Komponenten, die Informationen anzeigen oder entgegennehmen. Object |– Exception |– MarshalByRefObjec |– Stream |– Component |– Control |– ScrollableControl ... Zusätzliche Eigenschaften und Methoden von Control gegenüber Component sind insbesondere die Folgenden für die Position und Größe:
property int Left property int Top // x/y-Koordinaten linke obere Ecke property int Height property int Width // Höhe und Breite
1056
9 C++/CLI
Alle diese Angaben sind in Pixeln. Top und Left beziehen sich auf den Container, in dem das Steuerelement enthalten ist. Für ein Formular beziehen sie sich auf den Bildschirm. Alle diese Eigenschaften können auch mit der Methode
void SetBounds(int x, int y, int width, int height) gesetzt werden. Die Client-Eigenschaften
property Size ClientSize // Size.Width und Size.Height sind die Größe property Rectangle ClientRectangle // Eigenschaften X, Y, Width und Height, beziehen sich auf den sogenannten Client-Bereich des Steuerelements. Das ist der nutzbare Bereich (ohne Ränder, Schiebebalken, Menüleisten usw.). Er ist für die meisten Steuerelemente (außer Formularen) derselbe wie der durch Top, Left, Width und Height definierte Bereich. Ob das Steuerelement angezeigt wird oder nicht, ergibt sich aus dem Wert der Eigenschaft Visible, die auch durch Show und Hide gesetzt werden kann:
property bool Visible void Show(); void Hide(); Mit Enabled kann man steuern, ob das Steuerelement auf Maus-, Tastatur- oder Timer-Ereignisse reagiert:
property bool Enabled Zahlreiche Eigenschaften sind virtuell und werden in abgeleiteten Klassen überschrieben:
virtual property Color BackColor virtual property Font^ Font virtual property ContextMenuStrip^ ContextMenuStrip virtual property String^ Text Von der Klasse Control abgeleitete Steuerelemente können in einem anderen Steuerelement enthalten sein (z.B. in einem Formular, einer GroupBox oder einem Panel). Das enthaltende Element kann mit der Eigenschaft
property Control^ Parent gesetzt oder gelesen werden. Das oberste übergeordnete Steuerelement (meist das Formular) erhält man mit der Eigenschaft
property Control^ TopLevelControl Beispiel: Die Funktion showParent zeigt den Parent des Arguments für c an:
9.14 Ein kleiner Überblick über die .NET Klassenbibliothek
1057
void showParent(TextBox^ tb, Control^ c) { tb->AppendText("p="+c->Parent->ToString()+"\r\n"); }
Das ist für eine TextBox auf einem Formular das Formular und für einen Button in einer GroupBox die GroupBox. Ein Formular hat keinen Parent, was im Wert nullptr zum Ausdruck kommt. Ändert man die Position eines Steuerelements x, wird auch die aller Steuerelemente verschoben, die k als Parent haben. In Bezug auf k bleiben die Positionen aber gleich. Die Eigenschaft
virtual property IntPtr Handle ist eine interne, eindeutige Nummer eines Fensters unter Windows, die von manchen Funktionen der Win32-API benötigt wird. Mit diesem Handle können solche Funktionen aufgerufen werden. Da die Win32-API Funktionen unter .NET normalerweise nicht notwendig sind, benötigt man diese Eigenschaft meist nicht. Ein Steuerelement kann den Fokus haben. Dann werden ihm alle Tastatureingaben zugeteilt. Die folgenden Methoden hängen direkt damit zusammen:
virtual property bool Focused; // gibt an, ob das Steuerelement den Fokus hat void Select(); // gibt dem Steuerelement den Fokus TabIndex ist der Index des Steuerelements in der Tab-Ordnung (Aktivierreihenfolge). Diese Position gibt an, in welcher Reihenfolge die Komponenten den Fokus erhalten, wenn die Tab-Taste gedrückt wird. Der Wert von TabStop entscheidet, ob das Steuerelement durch das Drücken der Tab-Taste erreicht werden kann. property int TabIndex;
property bool TabStop;
Die Klasse Control enthält außerdem zahlreiche Ereignisse, auf die ein Steuerelement reagieren kann. Ein kleiner Auszug:
event EventHandler^ Click event MouseEventHandler^ MouseClick //Wie Click, aber mehr Informationen event EventHandler^ DoubleClick event MouseEventHandler^ MouseDown event MouseEventHandler^ MouseMove event MouseEventHandler^ MouseUp event EventHandler^ Enter; // wenn die Komponente den Fokus erhält oder event EventHandler^ Leave; // verliert
1058
9 C++/CLI
9.14.3 Verschachtelte Controls: Die Klasse ControlCollection Die Eigenschaft
property ControlCollection^ Controls der Klasse Control enthält alle Steuerelemente, die in dem Steuerelement enthalten sind. Das sind z.B. bei einem Formular oder einer GroupBox alle Steuerelemente, die zum Formular oder zur GroupBox gehören. Falls das Steuerelement keine Elemente enthält, hat Controls den Wert nullptr. Da Klassen wie Menu und Timer nicht von Control, sondern nur von Component abgeleitet sind, sind solche Komponenten nicht enthalten. Beispiel: Die Funktion showControls void showControls(TextBox^ tb, Control^ c) { // Menüs, Timer usw. werden nicht angezeigt for each (Control^ i in c->Controls) tb->AppendText(i->Name+"\r\n"); }
zeigt beim Aufruf showControls(textBox1,this); showControls(textBox1,this->groupBox1);
die Namen aller direkt enthaltenen Elemente des Formulars this bzw. der GroupBox an. Ruft man diese Funktion rekursiv auf, werden auch die indirekt enthaltenen Elemente angezeigt: void showControls_rec(TextBox^ tb, Control^ c) { // Menüs, Timer usw. werden nicht angezeigt for each (Control^ i in c->Controls) { tb->AppendText( i->Name+"\r\n"); if (i!=nullptr) showControls_rec(tb,i); } }
Mit einem dynamic_cast (siehe Abschnitt 6.5.2) kann man prüfen, ob ein Steuerelement von der Klasse TextBox abgeleitet ist. Damit kann man dann z.B. den Text in allen TextBoxen löschen. Beispiel: Die Funktion clearAllTextBoxes löscht die Texte aller TextBoxen in dem als Argument übergebenen Steuerelement (normalerweise ein Formular): void clearAllTextBoxes(Control^ c) { // analog zu Abschnitt 6.5.3 for each (Control^ i in c->Controls) {
9.14 Ein kleiner Überblick über die .NET Klassenbibliothek
1059
TextBox^ t=dynamic_cast(i); if (t) t->Clear(); if (i!=nullptr) clearAllTextBoxes(i); } }
Mit der Eigenschaft
static property FormCollection^ OpenForms der Application kann man alle Formulare einer Anwendung durchlaufen alle TextBoxen in allen Formularen löschen: for each(Form^ f in Application::OpenForms) clearAllTextBoxes(f);
Die Eigenschaft Controls hat den Datentyp ControlCollection. Diese Klasse hat Eigenschaften und Methoden, mit denen man die enthaltenen Elemente verwalten kann:
virtual property int Count // die Anzahl der enthaltenen Elemente property Control^ Owner // der Eigentümer der ControlCollection Mit den Methoden
virtual void Add(Control^ value) virtual void AddRange(array^ controls) virtual void Remove(Control^ value) kann man der Eigenschaft Controls Steuerelemente hinzufügen bzw. Steuerelemente entfernen. Beispiel: Die von Visual Studio erzeugte Funktion InitializeComponent enthält für jedes Steuerelement eine Anweisung wie this->Controls->Add(this->textBox1);
die das Steuerelement dem Formular hinzufügt. Die Methode
array^ Find(String^ key, bool searchAllChildren) gibt alle von Control abgeleiteten Steuerelemente, deren Eigenschaft Name den als Argument für key übergebenen String hat, in einem Array zurück. Damit kann man solche Steuerelemente über ihren Namen ansprechen. Beispiel: Der Rückgabewert von findTextBox ist die TextBox mit dem angegebenen Namen:
1060
9 C++/CLI TextBox^ findTextBox(String^ name, Control^ c) { array^ r=c->Controls->Find(name,true); if (r->Length==1) return safe_cast(r[0]); else return nullptr; }
9.14.4 Botschaften für ein Steuerelement Da die Klasse Control zahlreiche Ereignisse enthält, mit denen sie auf nahezu alle unter Windows üblichen Ereignisse reagieren kann, besteht nur selten die Notwendigkeit, Reaktionen auf weitere Ereignisse zu implementieren. Trotzdem soll jetzt kurz gezeigt werden, wie das in einer von Control abgeleiteten Klasse möglich ist. Windows ist ein ereignisgesteuertes System, das alle Benutzereingaben (Mausklicks, Tastatureingaben usw.) für alle Programme zentral entgegennimmt. Bei jedem solchen Ereignis sendet Windows dann eine Botschaft an das Programm, für das sie bestimmt sind. Die Klasse Control und jede abgeleitete Klasse (und damit jedes Steuerelement von Windows) enthalten eine sogenannte Window-Prozedur, die alle Botschaften von Windows für dieses Steuerelement entgegennimmt:
protected:virtual void WndProc(Message% m) Hier ist Message eine Werteklasse
public value class Message mit Eigenschaften wie
property IntPtr HWnd property int Msg property IntPtr Wparam
property IntPtr Lparam property IntPtr Result
Da WndProc virtuell ist, führt ihr Aufruf zum Aufruf der Funktion, die WndProc in der aktuellen Klasse überschreibt. Das ist z.B. bei einem Button die Funktion WndProc aus Button. Jede Funktion, die WndProc überschreibt, muss auch explizit die entsprechende Funktion der Basisklasse aufrufen, damit die in der aktuellen Klasse nicht behandelten Botschaften behandelt werden. Nach diesem Schema kann man auch in selbstdefinierten Steuerelementen vorgehen und Botschaften abfangen, auf die spezifisch reagiert werden soll. Das folgende Beispiel zeigt das für eine Form-Klasse, die auf einen Doppel-Klick der rechten Maustaste reagieren soll:
9.14 Ein kleiner Überblick über die .NET Klassenbibliothek
1061
virtual void WndProc( Message% m ) override { //wird aufgerufen, wenn die Anwend. eine Message erhält const Int32 WM_RBUTTONDBLCLK=0x0206; // aus "winuser.h" if(WM_RBUTTONDBLCLK ==m.Msg) this->Text="msg="+m.WParam.ToString(); Form::WndProc(m); // rufe WndProc der Basisklasse auf }
9.14.5 Botschaften für eine Anwendung Mit der Methode
static void AddMessageFilter(IMessageFilter^ value) der Klasse Application aus dem Namensbereich System::Windows::Forms kann man einer Anwendung einen Message-Filter hinzufügen. Dieser Message-Filter wird für jede Nachricht an eine Anwendung aufgerufen, bevor die Nachricht an das Formular oder das Steuerelement weitergegeben wird. Ein Message-Filter muss das Interface
public interface class IMessageFilter { bool PreFilterMessage(Message% m) } und deshalb die Elementfunktion PreFilterMessage implementieren. Mit dem Rückgabewert kann man festlegen, ob die Botschaft anschließend an die Window Prozedur WinProc des Steuerelements weitergeleitet wird oder nicht. Mit true wird eine anschließende Behandlung der Botschaft unterbunden. Beispiel: Mit dem Message-Filter ref class MyMessageFilter: public IMessageFilter { TextBox^ tb; public: MyMessageFilter(TextBox^ tb_):tb(tb_){}; virtual bool PreFilterMessage( Message % m ) { tb->AppendText(String::Format("M={0} W={1} L={2} \r\n", m.Msg, m.WParam, m.LParam)); return false; } };
werden alle Botschaften an die Anwendung in einer TextBox angezeigt. Diesen Filter kann man der Anwendung folgendermaßen hinzufügen:
1062
9 C++/CLI private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { Application::AddMessageFilter( gcnew MyMessageFilter(textBox1)); }
Gibt man in PreFilterMessage anstelle von false den Wert true zurück, werden alle Nachrichten nicht mehr an das Formular und seine Steuerelemente weitergegeben, so dass die Anwendung nicht auf Benutzereingaben reagiert. Mit Visual Studio wird das Programm Spy++ ausgeliefert. Damit kann man sich die Meldungen wesentlich „luxuriöser“ als mit ShowMsg anzeigen lassen.
Aufgaben 9.14 1. Verändert man die Größe eines Formulars während der Laufzeit eines Programms, behalten die Komponenten dieses Fensters ihre ursprüngliche Position und Größe. Dann kann ein Button, der beim Entwurf des Programms in der Mitte des Formulars zentriert war, völlig außerhalb der Mitte liegen. Schreiben Sie eine Klasse Resizer mit einer Methode resize(). Beim Anlegen eines Resizer-Objekts sollen die Werte Top, Left, Width und Height aller Komponenten eines als Parameter übergebenen Formulars in einem Array gespeichert werden. Bei jedem Aufruf von resize sollen diese Werte dann für jede Komponente so berechnet und gesetzt werden (z.B. mit SetBounds), dass sie im aktuellen Formular dieselbe relative Position und Größe hat. Diese Funktion kann man dann beim Ereignis Resize eines Formulars aufrufen, das bei jeder Größenänderung des Formulars eintritt. 2. Wenn man den Schließen-Button in der rechten oberen Ecke eines Fensters anklickt oder Alt-F4 drückt, sendet Windows eine WM_CLOSE (das ist der Wert 0x0010) Botschaft an die Anwendung. Wie können Sie verhindern, dass eine Anwendung auf diese Weise geschlossen wird?
9.15 Steuerelementbibliotheken: Die Erweiterung der Toolbox Die Toolbox von Visual Studio kann einfach um eigene Komponenten erweitert werden. Solche selbst definierten Komponenten sind im Prinzip „ganz normale Klassen“. Bei ihrer Definition müssen lediglich einige Besonderheiten berücksichtigt werden. Unter Extras|Partnerproduktkatalog erhält man eine Liste mit zahlreichen Steuerelementbibliotheken. Im Internet kann man weitere finden (teilweise kostenlos).
9.15 Steuerelementbibliotheken: Die Erweiterung der Toolbox
1063
9.15.1 Die Erweiterung der Toolbox um selbstdefinierte Komponenten Im Folgenden werden zunächst die wesentlichen Schritte gezeigt, die beim Erstellen einer selbstdefinierten Komponente anfallen. Die dabei erzeugte Komponente ist ziemlich einfach. Sie zeigt aber, wie man Eingabemasken usw. definieren kann, die in verschiedenen Anwendungen eingesetzt werden können. In Abschnitt 9.15.3 wird dann eine etwas anspruchsvollere Komponente erzeugt. 1. Unter Datei|Neu|Projekt|Visual C++|CLR eine Windows Forms-Steuerelementbibliothek anlegen. Im diesem Beispiel wird für das Projekt der Name ControlLib_1 verwendet. Damit wird eine Komponente mit dem Namen ControlLib_1Control erzeugt und im Entwurfsfenster von Visual Studio angezeigt:
Zu dieser Komponente gehört eine Header-Datei mit einer von System::Windows::Forms::UserControl abgeleiten Klasse: public ref class ControlLib_1Control : public System::Windows::Forms::UserControl { public: ControlLib_1Control(void) { ...
Die Basisklasse UserControl ist vor allem für Steuerelemente geeignet, die andere Steuerelemente enthalten. Die selbstdefinierte Klasse erbt dann von UserControl zahlreiche Methoden, Eigenschaften und Ereignisbehandlungsroutinen, die die Anzeige der Komponente und ihre Reaktion auf Benutzereingaben steuern.
1064
9 C++/CLI
2. Der Klasse ControlLib_1Control kann man wie einem Formular Steuerelemente aus der Toolbox hinzufügen und im Eigenschaftenfenster ihre Eigenschaften setzen. Mit einer TextBox und einem Button sieht das dann wie in der Abbildung rechts aus. Mit einem Doppelklick auf den Button kann man dem Steuerelement eine Ereignisbehandlungsroutine für einen Buttonklick hinzufügen: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { textBox1->AppendText("bla bla bla\r\n"); }
3. Nach dem Erstellen der Steuerelementbibliothek (Menü Erstellen in Visual Studio) kann man das Steuerelement ControlLib_1Control mit – Extras|Toolboxelemente auswählen oder mit – Elemente auswählen im Kontextmenü der Toolbox der Toolbox hinzufügen:
Dazu sucht man im Register .NET Framework-Komponenten mit Durchsuchen im Verzeichnis Debug oder Release des Projektverzeichnisses nach der DLL (hier: ControlLib_1.dll). Nach dem Anklicken des OK-Buttons wird die Komponente in der Toolbox angezeigt:
9.15 Steuerelementbibliotheken: Die Erweiterung der Toolbox
1065
Im Dialogfenster „Toolboxelemente auswählen“ kann man die der Toolbox hinzugefügten Elemente mit der Option „Zurücksetzen“ auch wieder entfernen. Dieses Steuerelement kann man dann wie ein vordefiniertes Steuerelement in einer Windows Forms-Anwendung verwenden, indem man es in der Toolbox anklickt und auf das Formular zieht:
Im Eigenschaftenfenster werden zahlreiche vordefinierte Eigenschaften und Ereignisbehandlungsroutinen der selbstdefinierten Komponente angezeigt. Diese kann man wie bei den vordefinierten Komponenten verwenden. Klickt man nach dem Start der Anwendung den Button an, werden die Anweisungen der Buttonklick-Methode ausgeführt:
1066
9 C++/CLI
9.15.2 Klassen für selbstdefinierte Komponenten Eine wie in Abschnitt 9.15.1 erzeugte Steuerelementbibliothek kann nicht nur Klassen enthalten, die wie die automatisch erzeugte Klasse von UserControl abgeleitet sind. Damit eine Klasse in die Toolbox aufgenommen werden kann, muss sie die folgenden Anforderungen erfüllen: 1. Die Klasse muss das Interface IComponent implementieren. Das erreicht man am einfachsten, indem man sie von der Klasse Component aus dem Namensbereich System::ComponentModel ableitet. Component stellt eine Basisimplementierung von IComponent dar. Eine von Component abgeleitete Klasse wird als Komponente bezeichnet. Diese Anforderung wird insbesondere von allen Klassen erfüllt, die von Control abgeleitet sind, da Control die indirekte Basisklasse Component hat. Generell sollte man immer eine solche Klasse als Basisklasse wählen, die die meiste Funktionalität für das neue Steuerelement schon mitbringt. Dann ist der Aufwand am geringsten. Eine von Control direkt oder indirekt abgeleitete Klasse wird als Steuerelement bezeichnet. Viele (aber nicht alle) Komponenten sind Steuerelemente. Diejenigen Komponenten, die keine Steuerelemente sind, werden vom Formulardesigner im Komponentenfach dargestellt, nachdem sie aus der Toolbox auf ein Formular gezogen wurden. Diejenigen Komponenten, die Steuerelemente sind, werden dagegen auf dem Formular angezeigt. 2. Falls das Setzen einer Eigenschaft die grafische Darstellung des Steuerelements verändert, muss die Darstellung in einer überschriebenen OnPaint-Methode aktualisiert werden. OnPaint muss durch einen Aufruf von Invalidate (siehe Abschnitt 10.10.2) in der set-Funktion der Eigenschaft aufgerufen werden. 3. Die Klasse muss das Assembly-bezogene Zugriffsrecht public haben (siehe Abschnitt 9.1.4). 4. Die Klasse muss einen public Standardkonstruktor haben. Dieser wird aufgerufen, wenn eine in die Toolbox aufgenommene Komponente aus der Toolbox auf das Formular gezogen und wenn eine solche Komponente beim Start der Anwendung erzeugt wird. Eine Klasse ohne einen solchen Konstruktor kann nicht in die Toolbox übernommen werden. Die Klasse kann noch weitere Konstruktoren haben, die man explizit aufrufen kann, um Komponenten zur Laufzeit zu erzeugen. In diesem Standardkonstruktor müssen die Elemente so initialisiert werden, damit die Komponente wie gewünscht dargestellt wird. Falls die Darstellung der Basisklasse reicht, sind hier keine weiteren Initialisierungen notwendig. Eine Steuerelementbibliothek kann nicht nur eine, sondern auch mehrere solcher Klassen enthalten. Falls man die automatisch erzeugte (von UserControl abgeleite-
9.15 Steuerelementbibliotheken: Die Erweiterung der Toolbox
1067
te Klasse) nicht benötigt, kann man diese auch löschen. Die Klassen können alle in demselben oder in verschiedenen Namensbereichen enthalten sein.
9.15.3 Beispiel: Eine selbstdefinierte Tachometer-Komponente Als einfaches Beispiel soll jetzt eine Komponente Tacho entwickelt werden, die wie eine Tachonadel auf einem Tachometer Werte in einem Bereich zwischen Min und Max anzeigt:
Als Basisklasse für Tacho kann die von Control abgeleitete Klasse Label verwendet werden: public ref class Tacho:Label{ // 1.: von Component abgeleitet, da Label von Control erbt // 2.: Assembly-Zugriffsrecht public public: Tacho() { // ... }
// 4. Standardkonstruktor
}
Diese Klasse erfüllt bereits die Anforderungen 1., 2. und 4. von Abschnitt 9.15.2. Sie kann um weitere Konstruktoren, Datenelemente, Eigenschaften, Ereignisse und Methoden ergänzt werden. Mit Attributen (siehe Abschnitt 9.15.4) kann man festlegen, wie die Elemente im Eigenschaftenfenster angezeigt werden. Damit der Blick für das Wesentliche nicht verlorengeht, soll Tacho lediglich drei Eigenschaften erhalten: public ref class Tacho:Label { double Speed_; public: property int Min; // eine triviale Eigenschaft property int Max; // eine triviale Eigenschaft property double Speed { void set(double x) { Speed_=x; Invalidate(); // ruft OnPaint auf }
1068
9 C++/CLI double get() { return Speed_; } }
Damit die Tachonadel bei einer Zuweisung an die Eigenschaft Speed neu gezeichnet wird, ruft die set-Methode Invalidate auf. Dieser Aufruf führt zum Aufruf der OnPaint-Methode von Tacho. In dieser Methode muss immer zuerst die Basisklasse (hier: Label) durch einen Aufruf ihrer OnPaint-Methode gezeichnet werden: protected: virtual void OnPaint(PaintEventArgs^ e) override { Label::OnPaint(e); // Zeichne das Label drawLine(e->Graphics); }
Anschließend müssen dann die für Tacho spezifischen Aktionen durchgeführt werden. Bei diesem einfachen Tacho muss lediglich die Tachonadel (eine einfache Linie) neu gezeichnet werden. Das wird durch die Funktion void drawLine(Graphics^ g) { // zeichne die Linie von unten-Mitte zum Endpunkt Pen pen(Color::Red, 1); Point start = Point(g->ClipBounds.Width/2, g->ClipBounds.Height); Point end = y(Speed_); g->DrawLine(%pen, start,end); } // wegen stack semantics von pen automatisch delete
erledigt. Der Endpunkt der Tachonadel auf dem Kreisbogen wird dabei in Point y(double x0) { // berechne den Endpunkt auf dem Kreisbogen // Min Controls->Add(this); this->AutoSize=false; Min=0; Max=100; }
Nach dem Erstellen der Steuerelementbibliothek kann man ihre Komponenten mit Extras|Toolboxelemente auswählen der Toolbox hinzufügen. Eine solche Komponente kann dann wie eine vordefinierte Komponente durch einfaches Anklicken in ein Formular übernommen werden. Ihre Eigenschaften werden im Eigenschaftenfenster angezeigt. Eine Steuerelementbibliothek ist eine DLL, die wie eine gewöhnliche CLR-DLL (siehe Abschnitt 3.24.1) verwendet werden kann. Nach einer #using-Anweisung wie #using "c:\ControlLib_1\Debug\ControlLib_1.dll"
kann ein Steuerelement wie Tacho wie eine gewöhnliche Klasse aus einer DLL verwendet werden: private:System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { Tacho^ t=gcnew Tacho(this, 20, 20, 70);//falls Tacho einen // Konstruktor wie "Tacho(Form^ f,int x,int y,int w)" hat }
Damit kann man eine selbstdefinierte Komponente oft etwas einfacher testen als wenn man sie in die Toolbox installiert.
9.15.4 Attribute für selbstdefinierte Komponenten Visual Studio steuert mit Attributen (siehe Abschnitt 9.17), ob und wie die Elemente von Komponenten der Toolbox im Eigenschaftenfenster angezeigt werden. Einige der wichtigsten Attribute in diesem Zusammenhang sind:
1070
9 C++/CLI
Attribut BrowsableAttribute
CategoryAttribute
DescriptionAttribute
DefaultValueAttribute DefaultEventAttribute DesignerAttribute
Falls dieses Attribut nicht oder auf den Wert true gesetzt wird, wird die Eigenschaft oder das Ereignis im Eigenschaftenfenster angezeigt. Mit dem Wert false wird es dagegen nicht angezeigt. Kategorie im Eigenschaftenfenster (allgemeiner: in einem PropertyGrid). Text, der im unteren Teil des Eigenschaftenfensters angezeigt wird. Standardwert der Eigenschaft. Standardereignis der Komponente. Damit kann man einen Designer angeben, mit dem die Eigenschaft editiert wird.
Beispiele: Bei der Eigenschaft Speed der Klasse Tacho kann man die folgenden Attribute angeben: [Description("Bestimmt den Winkel der Tachonadel"), Category("Appearance")] // Kategorie "Darstellung" property double Speed { ...
Das Description-Attribut wird dann im Eigenschaftenfenster angezeigt:
Aufgabe 9.15 Übernehmen Sie Ihre Lösungen der Aufgabe 9.13.4, 3. (EnterNextTextBox, FocusColorTextBox und ValueTextBox) in die Toolbox. Dafür sind die folgenden Schritte notwendig: 1. Falls Ihre Klassen die Anforderungen für eine Aufnahme in die Toolbox nicht erfüllen, ergänzen Sie diese. 2. Legen Sie eine neue Windows Forms-Steuerelementbibliothek an und löschen sie in ihr die automatisch erzeugte (von UserControl abgeleitete) Klasse. Nehmen Sie die Datei „myComponents.h“ mit den Lösungen von Aufgabe 9.13.4, 3. mit einer #include-Anweisung in die Steuerelementbibliothek auf.
9.16 Laufzeit-Typinformationen und Reflektion
1071
Verwenden Sie die Steuerelementbibliothek mit einer #using-Anweisung und erzeugen Sie Steuerelemente mit ihren Konstruktoren. 3. Ergänzen Sie die selbstdefinierten Komponenten um einige Attribute, die sich nach der Installation der Komponente in die Toolbox auf die Anzeige im Eigenschaftenfenster auswirken. 4. Erweitern Sie die Toolbox um die Komponenten aus dieser Bibliothek (EnterNextTextBox, FocusColorTextBox und ValueTextBox) 5. Verwenden Sie die in die Toolbox installierten Komponenten in einer Anwendung, in der Sie die von Ihnen definierten und einige der vordefinierten Eigenschaften im Eigenschaftenfenster und im Programm setzen. Diese Anwendung wird am einfachsten in dieselbe Projektmappe wie die Steuerelementbibliothek aufgenommen.
9.16 Laufzeit-Typinformationen und Reflektion C++/CLI stellt in einer Assembly (siehe Abschnitt 9.1) wesentlich umfangreichere Laufzeit-Typinformationen als Standard-C++ zur Verfügung, wo man im Wesentlichen nur den dynamischen Datentyp abfragen kann (siehe Abschnitt 6.5). Für einige weitere Konzepte im Rahmen der Reflektion gibt es in Standard-C++ kein Gegenstück: – In C++/CLI kann man Datentypen dynamisch (d.h. zur Laufzeit) erzeugen (siehe Abschnitt 9.16.3) – In Abschnitt 9.17 wird dann gezeigt, wie man einer Assembly mit Attributen eigene Informationen hinzufügen kann, und wie man auf diese Informationen in einer Anwendung und oder einer CLR-DLL zugreifen kann.
9.16.1 Laufzeit-Typinformationen der Klasse Type C++/CLI-Datentypen enthalten Laufzeit-Typinformationen der Klasse Type (aus dem Namensbereich System), die bereits in der Basisklasse Object von der Funktion GetType zurückgegeben werden:
Type^ GetType() Diese Funktion, und damit auch die Typinformationen, stehen deshalb für jede C++/CLI-Klasse zur Verfügung. Mit ::typeid nach dem Namen eines Datentyps erhält man diese Typinformationen auch zur Compile-Zeit. Beispiel: Mit der Funktion isInt kann man prüfen, ob das Argument auf ein int zeigt:
1072
9 C++/CLI bool isInt(Object^ o) { return o->GetType()==int::typeid; }
Damit erhält man für b true und für c false: bool b=isInt(17); // true bool c=isInt(tb); // mit TextBox^ tb: false
Die Klasse Type enthält zahlreiche Eigenschaften und Methoden, mit denen man prüfen kann, ob der Datentyp zu einer bestimmten Kategorie gehört. Ein kleiner Auszug:
virtual property bool IsClass // true für eine Verweisklasse virtual property bool IsPublic // true für das Assembly-Zugriffsrecht public virtual bool IsInstanceOfType(Object^ o) // true, wenn der Typ des Arguments // für o eine abgeleitete Klasse ist Beispiel: Mit den Klassen public value class V {}; ref class R {}; ref class S:R {};
erhält man die als Kommentar angegebenen Werte: R^ r=gcnew R; V^ v=gcnew V; S^ s=gcnew S; double d; bool br= r->GetType()->IsClass; // true bool bv= v->GetType()->IsClass; // false bool bd= d.GetType()->IsClass; // false bool pr= r->GetType()->IsPublic; // false bool pv= v->GetType()->IsPublic; // true bool bs= s->GetType()->IsInstanceOfType(r);//false bool bt= r->GetType()->IsInstanceOfType(s);//true
Weitere Methoden geben Informationen über die Elemente, Methoden, Eigenschaften usw. in einem Array zurück. Die Elemente der Arrays enthalten dann wiederum zahlreiche Informationen über die Klassenelemente, Methoden usw.
virtual array^ GetMembers() sealed // alle public Elemente // (Eigenschaften, Methoden, Ereignisse usw.) virtual array^ GetMethods() sealed virtual array^ GetProperties() sealed Beispiel: Die Funktion
9.16 Laufzeit-Typinformationen und Reflektion
1073
void showMethods(TextBox^ tb, Type^ t) { using namespace System::Reflection; for each (MethodInfo^ m in t->GetMethods()) tb->AppendText(m->ToString()); }
zeigt alle Methoden des als Argument übergebenen Typs in einer TextBox an: showMethods(textBox1, textBox1->GetType());
Objekte des Datentyps Type können wie andere von Object abgeleitete Objekte verwendet werden. Insbesondere kann man sie Variablen zuweisen oder als Parameter übergeben. Beispiel: Einige Operationen mit Type-Ausdrücken: Type^ TypeVar(Type^ T, Object^ x) { // inhaltlich sinnlos, nur zur Illustration der Type^ t=T; // Syntax t=int::typeid; t=TextBox::typeid; t=x->GetType(); return t; }
Diese Typinformationen mögen auf den ersten Blick exotisch wirken, wenn man sich keine sinnvollen Anwendungen vorstellen kann. Aber nur etwas Geduld: Solche Anwendungen folgen in den Abschnitten 9.16.3 und 9.17.2
9.16.2 Reflektion mit der Klasse Assembly Die Klasse Assembly (Namensbereich System::Reflection) stellt eine Assembly (eine CLR-Anwendung oder eine DLL) dar. Mit einer der Methoden mit „Load“ im Namen, wie z.B.
static Assembly^ LoadFile(String^ path) static Assembly^ ReflectionOnlyLoadFrom(String^ assemblyFile) kann man eine Assembly über ihren Namen oder ihren Dateinamen laden. Die aktuell laufende Assembly erhält man mit
static Assembly^ GetExecutingAssembly() Mit Methoden wie
virtual array^ GetTypes() virtual array^ GetCustomAttributes(bool inherit)
1074
9 C++/CLI
erhält man ein Array mit allen Datentypen und Attributen der Assembly Beispiel: Die folgende Funktion gibt die Namen aller Datentypen der Assembly aus, deren Dateiname als Argument übergeben wurde: void showTypesOfAssembly(TextBox^ tb, String^ fn) { using namespace System::Reflection; Assembly^ a=Assembly::LoadFile(fn); for each (Type^ t in a->GetTypes()) tb->AppendText(t->ToString()+"\r\n"); }
In einer solchen Schleife kann man mit Funktionen wie showMethods aus dem letzten Abschnitt auf alle Datentypen einer Assembly zugreifen.
9.16.3 Dynamisch erzeugte Datentypen und Plugins Die Klasse Type enthält neben der von Object geerbten Methode
Type^ GetType() weitere Methoden mit demselben Namen, denen man einen String übergeben kann:
static Type^ GetType(String^ typeName) Diese Methoden erzeugen Typinformationen, die zu dem Datentyp gehören, der durch das Argument für typeName beschrieben wird. Dabei muss der Datentyp im String in dem Format angegeben werden, das in der Assembly verwendet wird. Um ein Objekt der Klasse System::String anzulegen, darf man nicht die C++Schreibweise „System::String“ verwenden. Stattdessen muss man die C#Schreibweise „System.String“ verwenden. Beispiel: Die Funktion CreateTable (siehe Abschnitt 10.16.4) legt eine Datenbanktabelle zur Laufzeit an. Hier sieht man die Namen, die für die Datentypen int, String, DateTime, char und Decimal angegeben werden müssen: void CreateTable(DataSet^ ds) { DataTable^ t=gcnew DataTable("table"); t->Columns->Add("KontoNr", Type::GetType("System.Int32")); t->Columns->Add("Name", Type::GetType("System.String")); t->Columns->Add("Datum", Type::GetType("System.DateTime")); t->Columns->Add("Bewart", Type::GetType("System.Char")); t->Columns->Add("Betrag", Type::GetType("System.Decimal"));
9.16 Laufzeit-Typinformationen und Reflektion
1075
ds->Tables->Add(t); }
Die Klasse Activator aus dem Namensbereich System enthält einige Methoden, mit denen man aus Typinformationen des Typs Type zur Laufzeit Objekte erzeugen kann. So erzeugt z.B.
static Object^ CreateInstance(Type^ type) ein Objekt mit dem Standardkonstruktor des Typs. Beispiel: Die Funktion createInstanceFromString erzeugt zur Laufzeit ein Objekt des Datentyps, dessen Name als String übergeben wird: Object^ createInstanceFromString(String^ type) { Type^ t=Type::GetType(type); Object^ o=Activator::CreateInstance(t); return o; }
Diese Klassen kann man z.B. dazu verwenden, zur Laufzeit ein Verzeichnis nach allen DLLs zu durchsuchen, die ein bestimmtes Interface implementieren, und dann eine bestimmte Methode der Interface-Klasse aufzurufen. Damit kann man Plugins implementieren, die ein Programm nach seiner Auslieferung erweitern. Im folgenden Beispiel soll diese Technik mit einem Interface illustriert werden, das in einer ersten DLL (mit dem Namen PluginBase.dll) implementiert ist: using namespace System::Windows::Forms; // für TextBox public interface class IPluginBase { void execute(TextBox^ tb); };
Damit in dieser DLL System::Windows::Forms verfügbar ist, muss man diese unter Projekt|Eigenschaften|Allgemeine Eigenschaften|Neuen Verweis hinzufügen hinzufügen. Eine Anwendung kann dann mit einer Funktion wie loadPluginTypes Verzeichnisse nach DLLs durchsuchen und die Type-Informationen aller Datentypen zurückgeben, die dieses Interface implementieren: #using "C:\...\PluginBase.dll" bzw. dem Projekt hinzufügen using namespace System; using namespace System::Windows::Forms; // für TextBox using namespace System::Collections::Generic; // für List using namespace System::Reflection; List^ loadPluginTypes(TextBox^ tb, String^ dir) { // Gibt die Type-Informationen aller Typen aus den DLLs // des Verzeichnisses dir zurück, die das Interface // IPluginBase implemententieren
1076
9 C++/CLI
List^ result = gcnew List; array^ DLLFilenames= System::IO::Directory::GetFiles(dir, "*.dll"); for each(String^ filename in DLLFilenames) { try { Assembly^ as = Assembly::LoadFrom(filename); for each (Type^ t in as->GetTypes()) { String^ s= (IPluginBase::typeid)->FullName; if (t->GetInterface(s) != nullptr) {//Typ gefunden, der IPluginBase implementiert tb->AppendText("Assembly: "+as->FullName+", "+ t->FullName+" implementiert PluginBase\r\n"); result->Add(t); } } } catch(BadImageFormatException^){}// keine zulässige } // Assembly, ignorieren return result; }
Über die so gefundenen Datentypen kann man dann mit einer Funktion wie test die virtuellen Funktionen der Klassen aufrufen, die das Interface implementieren: void test(TextBox^ tb) { String^ d = System::IO::Directory::GetCurrentDirectory(); List^ a=loadPluginTypes(tb, d); for each (Type^ t in a) { Object^ o=Activator::CreateInstance(t); IPluginBase^ ip=safe_cast(o); ip->execute(tb); } }
Wird das Interface dann in einer zweiten DLL implementiert public ref class myPlugin : IPluginBase { public: virtual void execute(TextBox^ tb) { tb->AppendText("mein Plugin"); }; };
und diese in ein Verzeichnis kopiert, das durchsucht wird, wird die Funktion execute aus diesem Plugin aufgerufen.
9.17 Attribute
1077
Aufgabe 9.16 1. Überarbeiten Sie die Funktion makeTextBox von Abschnitt 9.13.4 zu einer Funktion
Control^ makeControl(Form^ f, Type^ type, int x, int y, int w, int h) die ein Steuerelement des als Argument für type übergebenen Typs erzeugt, wenn dieser Typ von Control abgeleitet ist. 2. Die Funktion
void showMembersOfType(TextBox^ tb, Type^ type, System::Reflection::Assembly^ a) soll alle public Elemente des Datentyps type aus der Assembly a in der TextBox tb ausgeben. Sie können dazu die Type-Methode GetMembers verwenden. Rufen Sie diese Funktion z.B. mit dem Datentyp TextBox und der Assembly System.Windows.Forms.Dll auf. Den Pfad dieser Assembly finden Sie unter Projekt|Eigenschaften|Allgemeine Eigenschaften|Framework und Verweise|Vollständiger Pfad.
9.17 Attribute Attribute ermöglichen die Angabe von Informationen bei nahezu allen Arten von Deklarationen: Sie können bei der Deklaration von – C++/CLI-Klassen (Verweis-, Werte und Interfaceklassen, aber nicht bei nativen Klassen), – Elementen von solchen Klassen (Konstruktoren, Daten, Methoden, Properties, Destruktoren und Finalisierer), – Assemblies, – Ereignissen und Delegaten, – Funktionsparametern und Rückgabewerten angegeben werden. Manche Attribute können nur bei bestimmten Arten von Deklarationen (z.B. Klassen) angegeben werden. Andere können bei allen Arten von Deklarationen angegeben werden. In .NET und C++/CLI gibt es zahlreiche vordefinierte Attribute. Diese können sich zur Entwurfszeit in der Entwicklungsumgebung, bei der Kompilation oder während der Laufzeit auswirken. Außerdem können eigene Attribute definiert werden. Aus solchen Attributen können dann zur Laufzeit Objekte erzeugt werden, deren Elemente zur Laufzeit über Reflektion (d.h. Type-Informationen) verfügbar sind.
1078
9 C++/CLI
Attribute werden in eckigen Klammern bei der Deklaration angegeben. Wenn ihr Name wie üblich mit „Attribute“ endet, kann dieser Teil des Namens auch ausgelassen werden. Viele Attribute haben Parameter. Die Argumente für diese Parameter werden nach dem Namen in runden Klammern angegeben. Beispiel: Das vordefinierte Attribut ObsoleteAttribute kann bei der Definition von Klassen, Methoden usw. angegeben werden. Es bewirkt (ähnlich wie das Pragma deprecated, siehe Abschnitt 3.22.4), dass der Compiler eine Warnung erzeugt, wenn die Klasse verwendet bzw. die Methode aufgerufen wird. [ObsoleteAttribute("Do not use C")] ref class C { public: [Obsolete("Do not call f")] void f(){} };
Da der Name dieses Attributs mit „Attribute“ endet, reicht die Angabe Obsolete aus. Eine Angabe eines Attributs ist eine Deklaration, die unabhängig vom Programmablauf ausgewertet werden. Deshalb müssen die Argumente von Attributen Konstanten sein, deren Wert bei der Kompilation bekannt ist. Beispiel: Die Verwendung von Variablen als Attributargumente ist nicht zulässig: ref class C { static String^ s=""; [Obsolete(s)] // Fehler: unzulässiges Attributvoid f() {}; // argument };
Attribute sind Klassen, die sich von gewöhnlichen Klassen nur dadurch unterscheiden, dass sie von der Klasse System::Attribute abgeleitet sind. In der OnlineHilfe erkennt man sie daran, dass ihr Name mit „Attribute“ endet. Beispiel: In dem folgenden Auszug aus der Übersicht zum Namensbereich System sind SerializableAttribute und STAThreadAttribute Attribute, da ihr Name mit „Attribute“ endet:
Im Folgenden wird zunächst am Beispiel von einigen vordefinierten C++/CLI- und .NET-Attributen gezeigt, wie man Attribute verwenden kann. Ab Abschnitt 9.17.2 wird dann gezeigt, wie man eigene Attribute definieren kann.
9.17 Attribute
1079
9.17.1 Vordefinierte Attribute Das oben vorgestellte Attribut ObsoleteAttribute wirkt sich auf die Kompilation aus. Dieser Effekt ist nur mit vordefinierten Attributen möglich. Mit selbstdefinierten Attributen (siehe Abschnitt 9.17.2) kann der Compiler nicht beeinflusst werden. Einige weitere Beispiele für vordefinierte Attribute: – Visual Studio erzeugt die main-Funktion einer Windows Forms-Anwendung mit dem STAThreadAttribute. Das Gegenstück ist MTAThreadAttribute „Multithreaded Appartment“). Diese Attribute wirken sich nur bei COM-InteropAnwendungen aus. – Die Attribute Serializable und NonSerialized werden im Zusammenhang mit Serialisierung verwendet (siehe Abschnitt 10.15). – Das Attribut AttributeUsageAttribute wird bei der Definition von Attributklassen verwendet. Die zulässigen Argumente
attribute-target: assembly delegate field parameter struct
class enum interface property
constructor event method returnvalue
geben an, bei welchen Arten von Deklarationen das Attribut angegeben werden kann. Das attribute-target All bewirkt, dass man das Attribut bei allen Deklarationen verwenden kann. Beispielsweise hat ObsoleteAttribute die folgenden Angaben: [SerializableAttribute] [ComVisibleAttribute(true)] [AttributeUsageAttribute( AttributeTargets::Class|AttributeTargets::Struct| AttributeTargets::Enum|AttributeTargets::Constructor| AttributeTargets::Method|AttributeTargets::Property| AttributeTargets::Field|AttributeTargets::Event| AttributeTargets::Interface|AttributeTargets::Delegate, Inherited=false)] public ref class ObsoleteAttribute sealed : public Attribute
Gibt man ein Attribut bei einer Deklaration an, die nicht zum AttributeUsageAttribute passt, erzeugt der Compiler eine Fehlermeldung. Gibt man bei der Definition eines Attributs kein AttributeUsageAttribute an, entspricht das der Angabe [AttributeUsage(AttributeTargets::All, AllowMultiple=false)]
1080
9 C++/CLI
Weitere vordefinierte Attribute sind die sogenannten Entwurfszeit-Attribute (siehe Abschnitt 9.15.4), die von Visual Studio verwendet werden, um z.B. im Eigenschaftenfenster Informationen anzuzeigen.
9.17.2 Selbstdefinierte Laufzeitattribute Als Nächstes soll gezeigt werden, wie man eigene Attribute definieren und einsetzen kann. Solche Attribute wirken sich wie auch zahlreiche vordefinierte Attribute nur zur Laufzeit aus. Eine Klasse wird dadurch zur Attributklasse, dass sie von der Klasse System::Attribute abgeleitet wird. Beispiel: Die Klasse ref class AuthorAttribute : System::Attribute { // ... };
ist eine Attributklasse, die bei der Definition einer Klasse verwendet werden kann: [Author] ref class C{};
Oft will man Attribute mit Parametern verwenden. Dazu gibt es zwei Möglichkeiten: 1. Objekte einer Attributklasse können wie Objekte gewöhnlicher Klassen mit Konstruktoren initialisiert werden. Diese durch die Konstruktoren des Attributs definierten Parameter werden auch als Positionsparameter bezeichnet. Beispiel: Ergänzt man AuthorAttribute um die Datenelemente name_ und date_ sowie um die beiden Konstruktoren, ref class AuthorAttribute:System::Attribute { String^ name_; String^ date_; public: AuthorAttribute(String^ name):name_(name){} AuthorAttribute(String^ name, String^ date) :name_(name),date_(date){} };
werden diese Elemente durch die folgenden Attribute initialisiert: [Author("ich")] ref class C1 {}; [Author("ich","heute")] ref class C2 {};
9.17 Attribute
1081
2. Jedes public und nicht static Datenelement sowie jede Schreib-/Leseeigenschaft (Property) definiert einen benannten Parameter. Ein benannter Parameter np kann durch die Schreibweise np = Wert initialisiert werden. Bei benannten Parametern spielt die Reihenfolge der Initialisierungen keine Rolle. Beispiel: Ein Objekt der Attributklasse ref class XAttribute:System::Attribute { public: String^ name; String^ date; };
kann folgendermaßen initialisiert werden: [X(date="heute", name="ich")] ref class C {};
Benannte Parameter und Positionsparameter können auch kombiniert werden. Dann müssen bei der Angabe eines Attributs die Argumente für die Konstruktoren zuerst kommen. Als Datentypen für Attributparameter sind nur die folgenden Datentypen aus dem Namensbereich System zulässig sowie die entsprechenden nativen Typen:
Boolean, Byte, SByte, Char, Int16, Int32, Int64, Single, Double, String^, Object^, Type^, ein nativer oder ein C++/CLI-Aufzählungstyp, eindimensionale CLI-Arrays dieser Typen. Beispiel: Der Datentyp DateTime, der bei AuthorAttribute für das Datum der letzten Änderung nützlich sein könnte, kann nicht verwendet werden. Auf die Attribute kann man zur Laufzeit mit einer der Funktionen mit dem Namen GetCustomAttributes zugreifen. Diese Funktionen stehen in der Klasse Type sowie in System::Attribute zur Verfügung, z.B.
virtual array^ GetCustomAttributes(Type^ attributeType, bool inherit) abstract Beispiel: Die Funktion void showAttributes(TextBox^ tb, Type^ t) { tb->AppendText(t->ToString()+" Attribute:\r\n"); for each (Attribute^ a in t->GetCustomAttributes(true)) // oder: Attribute::GetCustomAttributes(t,true))
1082
9 C++/CLI tb->AppendText("
"+a->ToString()+"\r\n");
}
gibt alle Attribute des als Argument übergebenen Datentyps aus. Mit showAttributes(textBox1, C1::typeid); showAttributes(textBox1, C2::typeid); showAttributes(textBox1, C3::typeid);
erhält man mit den Attributen von oben die Ausgabe C1 Attribute: AuthorAttribute C2 Attribute: C3 Attribute: AuthorAttribute
Der Aufruf einer dieser GetCustomAttributes Funktionen bewirkt, dass ein Objekt der Attributklasse angelegt und die entsprechenden Informationen als Funktionswert (im CLI-Array) zurückgegeben werden. Ohne den Aufruf einer solchen Funktion wird kein Objekt erzeugt. Die Angabe eines Attributs hat außer beim Aufruf von GetCustomAttributes keine Auswirkung auf die Laufzeit des Programms. Wenn GetCustomAttributes (wie oft) über die Type-Informationen der Assembly von außerhalb des Programms aufgerufen wird, hat sie keine Auswirkungen auf die Laufzeit. Beispiel: Nach der Angabe eines Attributs wie in [Author("ich", Date="heute")] ref class C{};
hat ein Aufruf einer GetCustomAttributes Funktion im Wesentlichen denselben Effekt wie die Definition eines Objekts AuthorAttribute^ x=gcnew AuthorAttribute("ich"); x->Date="heute";
Normalerweise legt man aber keine Objekte von Attributklassen mit gcnew an, obwohl das möglich ist. Vielmehr verwendet man Attribute vor allem im Zusammenhang mit Reflektion (siehe Abschnitt 9.16.2), um diese zur Laufzeit über Typinformationen abzufragen. Das ist z.B. mit der Methode
virtual array^ GetTypes() der Klasse Assembly aus dem Namensbereich System::Reflection möglich, die alle Datentypen einer Assembly (z.B. eine DLL oder eine ausführbare EXE-Datei) in einem Array zurückgibt. Beispiel: Die Funktion
9.17 Attribute
1083 void showAttributes(TextBox^ tb, System::Reflection::Assembly^ a) { for each (Type^ t in a->GetTypes()) showAttributes(tb, t); }
gibt die Attribute aller Datentypen einer Assembly mit der oben definierten Funktion showAttributes aus. Übergibt man dieser Funktion eine Assembly, die mit LoadFile geladen wird, void showAttributes(TextBox^ tb, String^ FileName) { using namespace System::Reflection; showAttributes(tb, Assembly::LoadFile(FileName)); }
werden die Attribute aller Klassen einer ausführbaren Datei oder einer DLL angezeigt: showAttributes(textBox1, "c:\\Project1.exe");
Die Type-Methode
virtual array^ GetMethods() sealed liefert für alle Methoden einer Klasse Informationen des Typs MethodInfo. Mit einer der MethodInfo-Methoden Invoke kann man die Methode zur Laufzeit aufrufen. Damit kann man Programme schreiben, die alle Methoden aufrufen, die mit einem bestimmten Attribut gekennzeichnet sind und z.B. Unit-Test (siehe Abschnitt 3.5.3) durchführen. Diese Beispiele zeigen, dass Attribute Lösungen ermöglichen können, die mit Standard-C++ nicht oder nur recht umständlich möglich sind. Da die Angabe von Attributen reine Deklarationen sind, bezeichnet man die Programmierung mit Attributen auch als deklarative Programmierung. Dieser Begriff soll vor allem den Unterschied zur sogenannten imperativen Programmierung hervorheben, bei der wie in Standard-C++ Anweisungen im Vordergrund stehen.
Aufgabe 9.17 Definieren Sie in einer Windows Forms-Anwendung eine Attribut-Klasse AuthorAttribute (wie im Text) und geben Sie dieses Attribut bei einigen Klassen und public Methoden an. Schreiben Sie die Funktionen a) void showAuthorAttributes(TextBox^ tb, Type^ t) Diese Funktion soll für den als Parameter übergebenen Typ sowie für alle seine public Methoden den Namen der Klasse bzw. Methode sowie den des Autors
1084
9 C++/CLI
und das Datum in der TextBox ausgeben, wenn bei der Klasse bzw. Methode das AuthorAttribute angegeben ist. Die Methoden des Typs erhalten Sie mit
virtual array^ GetMethods() sealed Ein MethodInfo-Objekt kann wie ein Type-Objekt bearbeitet werden. b) void showAuthorAttributes(TextBox^ tb, System::Reflection::Assembly^ a) Diese Funktion soll für jeden Typ der übergebenen Assembly die Funktion aus a) aufrufen. Testen Sie diese Funktion mit der aktuellen Assembly. c) void showNoAuthorAttributes(TextBox^ tb, Type^ t) Diese Funktion soll den Namen der als Parameter übergebenen Klasse in der TextBox ausgeben, wenn bei ihr das AuthorAttribute nicht angegeben ist.
9.18 Generische Programmierung Generische Programmierung in C++/CLI ermöglicht wie in Standard-C++ (siehe Kapitel 8) die Definition von Klassen und Funktionen, denen Datentypen als Parameter übergeben werden. Diese beiden Varianten der generischen Programmierung haben viele Gemeinsamkeiten, aber auch einige Unterschiede, da die generische Programmierung unter C++/CLI auf Assemblies (siehe Abschnitt 9.1) abgestimmt ist und die Klassen und Funktionen nicht wie in Standard-C++ zur Compilezeit, sondern zur Laufzeit erzeugt werden. In der .NET Klassenbibliothek werden generische Klassen vor allem für Collection-Klassen verwendet. Diese entsprechen den Container-Klassen der C++-Standardbibliothek, die mit Templates implementiert sind. Als Oberbegriff für generische Klassen, Funktionen usw. wird in C++/CLI und C# auch der Begriff „Generika“ verwendet. Auf diesen Begriff wird hier verzichtet.
9.18.1 generic und template: Gemeinsamkeiten und Unterschiede Eine generische Deklaration in C++/CLI muss eine Verweisklasse, Werteklasse, Interface-Klasse, einen Delegat-Typ oder eine Funktion deklarieren bzw. definieren. Sie verwendet das Wort generic anstelle von template in Standard-C++:
generic-declaration: generic < generic-parameter-list > constraint-clause-listopt declaration Wie bei einer Template-Deklaration kann ein Typ-Parameter sowohl nach „class“ als auch nach „typename“ angegeben werden. Semantisch sind beide gleichwertig.
9.18 Generische Programmierung
1085
generic-parameter: attributes opt class identifier attributes opt typename identifier Der Name (identifier) eines Typ-Parameters kann dann wie bei einem Template in der generischen Deklaration als Datentyp verwendet werden. In C++/CLI können nicht nur generische Klassen, Funktionen usw. definiert werden, sondern auch Templates. Beispiel: In C++/CLI kann die generische Funktion swap_g und das FunktionsTemplate swap_t definiert werden: generic void swap_g(T% a,T% b) { T h = a; a = b; b = h; }
template void swap_t(T& a,T& b) { T h = a; a = b; b = h; }
Die generische Klasse G und das Klassen-Template T können ebenfalls beide in C++/CLI definiert werden: generic ref class G { public: T1 a; void f(T1 x, T1 y) { a->g(x,y); } };
template ref class T { public: T1 a; void f(T1 x, T1 y) { a.g(x,y); } };
Im Template T führt der Aufruf von g nur dann zu einer Fehlermeldung des Compilers, wenn das Typargument für T1 keine Funktion g enthält. In der generischen Klasse G führt dieser Aufruf dagegen immer zu einer Fehlermeldung, da ein Typ-Parameter in einer generischen Funktion oder Klasse immer den Datentyp Object hat, wenn nicht eine Typparameter-Einschränkung (siehe Abschnitt 9.18.2) angegeben wird. Die STL/CLR (siehe Abschnitt 4.2.3) ist nicht (wie man eventuell erwarten könnte) mit generischen Klassen und Funktionen, sondern mit Templates implementiert. Die generischen Containerklassen (siehe Abschnitt 10.12) sind dagegen generische Klassen. Mit nativen Klassen kann generic nicht verwendet werden. Dagegen kann template mit C++/CLI-Klassen verwendet werden (wie im letzten Beispiel). Bei einer generischen Deklaration darf im Gegensatz zu einer Template-Deklaration nach einem Typ-Parameter keines der Zeichen *, ^, & oder % angegeben werden. Beispiel: Mit jeder der folgenden generischen Deklarationen (außer g4) erhält man eine Fehlermeldung:
1086
9 C++/CLI generic
class N {}; // error void g1(T^ p){}; // error void g2(T* p){}; // error void g3(T& p){}; // error void g4(T% p){}; // das geht ref class G1 {
Ersetzt man in jeder dieser Definitionen generic durch template, werden alle kompiliert. Bei generischen Funktionen oder Klassen sind im Gegensatz zu Funktions- oder Klassen-Templates nicht möglich: – – – –
Nicht-Typ Parameter (siehe Abschnitte 8.1.3 und 8.2.3) explizite und partielle Spezialisierungen (siehe Abschnitte 8.1.5 und 8.2.5) Default-Template Argumente Typ-Parameter als Basisklasse.
Eine generische Funktion wird wie ein Funktions-Template aufgerufen. Falls die Typ-Argumente nicht explizit wie in swap_g(a,b);
angegeben werden, versucht der Compiler diese aus den Datentypen der Funktionsargumente abzuleiten. Um aus einer generischen Klasse ein Objekt zu erzeugen, müssen die Typ-Argumente immer explizit angegeben werden. Beispiel: Die beiden Funktions-Templates aus dem letzten Beispiel können folgendermaßen aufgerufen werden: int a=1, b=2; swap_g(a,b); swap_t(a,b);
Ein Objekt der generischen Klasse G erhält man durch G^ g=gcnew G; g->f(1,2);
und ein Objekt des Klassen-Templates T durch T t; t.f(1,2);
Zulässige Typ-Argumente für Templates sind im Wesentlichen alle Datentypen von Standard-C++ und C++/CLI. Bei generischen Klassen und Funktionen können
9.18 Generische Programmierung
1087
dagegen nur C++/CLI-Datentypen als Typ-Argumente verwendet werden, aber keine Zeiger, Referenzen, Zeiger auf Werteklassen usw. Beispiel: Mit den Klassen- und Funktions-Templates aus den Beispielen von oben sind die folgenden Aufrufe möglich: C c1, c2; R ^r1, ^r2; int i1,i2; swap_t(r1, r2); swap_t(c1, c2); swap_t(i1, i2); swap_g(r1, r2); swap_g(c1, c2); // error swap_g(i1, i2);
Bei einem Aufruf von swap_g mit Argumenten eines nativen Klassentyps erhält man die Fehlermeldung „Typargument muss ein Werttyp oder ein Handletyp sein“. Aus einem Template erzeugt der Compiler für jedes Typ-Argument, mit dem es verwendet wird, eine Funktion oder Klasse, in der die Typ-Parameter durch die Typ-Argumente ersetzt werden. Diese Funktion oder Klasse wird dann kompiliert. Aus der Deklaration einer generischen Funktion oder Klasse erzeugt der Compiler dagegen Metadaten, und zwar nur eine Metadaten-Definition pro Funktion oder Klasse. Aus den Metadaten werden dann während der Laufzeit mit Type-Methoden wie Activator::CreateInstance (siehe Abschnitt 9.16.3) Klassen oder Funktionen erzeugt. Aus dieser unterschiedlichen Vorgehensweise ergibt sich, dass die Typ-Argumente eines Templates bei der Kompilation bekannt sein müssen, während das für die Argumente von generischen Klassen und Funktionen nicht erforderlich ist. Daraus ergeben sich die folgenden Unterschiede zu Templates: – generic Deklarationen können in C++-Dateien oder DLLs enthalten sein. Sie müssen nicht wie Template-Deklarationen in Header-Dateien enthalten sein. – Der Quelltext ist bei der generic Deklarationen im Gegensatz zu dem für Templates nicht notwendig. Beispiel: Eine in einer DLL definierte generic Klasse oder Funktion kann nach einer using-Direktive verwendet werden. Ersetzt man generic durch template, ist das nicht möglich: // Definition in einer DLL: generic // nach #using verwendbar // template // nicht verwendbar public ref class X { T x; };
1088
9 C++/CLI
9.18.2 Typparameter-Einschränkungen (Constraints ) In C++/CLI können die zulässigen Typ-Argumente für einen Typ-Parameter bei generischen Klassen und Funktionen durch eine oder mehrere TypparameterEinschränkungen (Constraints) eingeschränkt werden.
constraint-clause-list: constraint-clause-listopt constraint-clause constraint-clause: where identifier : constraint-item-list Der identifier nach where muss ein Typ-Parameter sein (ein Bezeichner aus der Liste in generic). Anschließend folgen in einer constraint-item-list die Einschränkungen für diesen Typ-Parameter.
constaint-item-list: constraint-item constraint-item-list , constraint-item constraint-item: type-id refųclass refųstruct valueųclass valueųstruct gcnew ( )
In einem constraint-item steht type-id für einen Datentyp. Falls dieser Datentyp mit E bezeichnet wird und – eine Verweisklasse ist, wird die Einschränkung als Klassen-Constraint bezeichnet. Ein Klassen-Constraint bedeutet, dass das Typ-Argument entweder E oder von E abgeleitet sein muss. Jeder Typ-Parameter kann maximal ein Klassen-Constraint haben. – eine Interface-Klasse ist, wird die Einschränkung als Interface-Constraint bezeichnet. Ein Interface-Constraint bedeutet, dass das Typ-Argument E implementieren muss. Ein Typ-Parameter kann mehrere Interface-Constraints haben. – wie in „where T:U“ ein Typ-Parameter U ist, wird die Einschränkung als „naked type parameter constraint“ bezeichnet. Das Argument für einen solchen Typ-Parameter T muss entweder U sein oder von U abgeleitet sein. Andere Datentypen (wie z.B. Werteklassen oder native Klassen) sind für type-id nicht zulässig. Die übrigen constraint-item (ref class usw.) werden anschließend vorgestellt. In der generischen Klasse oder Funktion können die Elemente des Datentyps der Typparameter-Einschränkung verwendet werden. Falls keine Typparameter-Einschränkung angegeben wird, hat der Typ-Parameter den Datentyp Object. Dann
9.18 Generische Programmierung
1089
stehen in der generischen Klasse oder Funktion auch nur die Elemente von Object zur Verfügung. Ein Element einer Typparameter-Einschränkung wird immer mit dem Operator -> angesprochen, obwohl der Typ-Parameter immer ohne ^ angegeben werden muss. Das gilt auch dann, wenn die generische Klasse oder Funktion mit einer Werteklasse als Typ-Argument verwendet werden kann. Für ein solches Typ-Argument wird der „->“-Operator vom Compiler als „.“-Operator behandelt. Beispiel: Mit interface class I { void f(); };
kann die generische Klasse generic where T:I ref class G2 { T x; public: G2(T a):x(a) { x->f(); // verwende f aus I } };
sowohl mit einer Verweisklasse als auch mit einer Werteklasse als TypArgument verwendet werden, die I implementiert: ref class R:I { public: virtual void f(){}; };
value class V:I { public: virtual void f(){}; };
R^ a=gcnew R; G2^ g2=gcnew G2(a);
V a; G2 g3(a);
Eine Typparameter-Einschränkung wird nicht durch ein Semikolon abgeschlossen. Das gilt auch bei der Kombination von mehreren Typparameter-Einschränkungen: generic where T:E // E muss eine Klasse sein where T:R, I // R oder I muss eine Interface{} // Klasse sein
Die weiteren Typparameter-Einschränkungen ref class, value class, gcnew usw. bedeuten: – Mit gcnew muss das Typ-Argument einen Standardkonstruktor haben. In der generischen Klasse oder Funktion kann man dann mit gcnew ein Objekt
1090
9 C++/CLI
erzeugen. Ein anderer Konstruktor als der Standardkonstruktor kann aber nicht aufgerufen werden. – Mit ref class oder value class kann man erzwingen, dass die Typ-Argumente Verweisklassen oder Werteklassen sind. Damit können Unterschiede zwischen Werte- und Verweisklassen berücksichtigt werden. Beispiel: Die Typparameter-Einschränkung der generischen Funktion createObject ermöglicht, dass hier ein Objekt erzeugt wird. Ohne diese Einschränkung würde diese Anweisung nicht kompiliert: generic where T:gcnew() T createObject(); { return gcnew T(); }
Mit den generischen Klassen generic where T:ref class T GR () {};
generic where T:value class T GV(T x) {}
und value class V {}; ref class R {};
kann man die Objekte g und r erzeugen. Bei g2 und r2 erhält man die als Kommentar angegebenen Fehlermeldungen: GV g; GV h; // error: Typargument muss Werttyp sein GR r; GR s;//error: Typargument muss Referenztyp sein
9.19 Dokumentationskommentare und CHM-Hilfedateien In C++/CLI kann man sogenannte Dokumentationskommentare in den Quelltext aufnehmen, aus denen der Compiler dann beim Erstellen des Projekts eine XMLDatei erzeugt. Aus solchen XML-Dateien kann dann mit weiteren Programmen eine Dokumentation in verschiedenen Formaten wie z.B. CHM erzeugt werden.
9.19 Dokumentationskommentare und CHM-Hilfedateien
1091
9.19.1 Dokumentationskommentare und XML-Dateien Ein Dokumentationskommentar ist entweder ein einzeiliger Kommentar, der mit /// (genau drei '/’-Zeichen) beginnt, oder ein mehrzeiliger Kommentar, der mit /** beginnt: single-line-doc-comment: /// intput-charactersopt delimited-doc-comment: /** delimited-comment-charactersopt */
Mit der Einstellung Projekt|Eigenschaften|Konfigurationseigenschaften|C/C++|Ausgabedateien|XML-Dokumentationsdateien generieren erzeugt der Compiler aus solchen Kommentaren eine XML-Datei. Alle Dokumentationskommentare müssen unmittelbar vor Verweis-, Werte- oder Interfaceklassen bzw. vor Elementen (z.B. Methoden) von solchen Klassen aufgeführt werden. Leider ist es mit solchen Kommentaren nicht möglich, native Klassen zu dokumentieren. Dafür sind andere Tools (wie z.B. doxygen, siehe http://www.doxygen.org)) notwendig. Dokumentationskommentare in einer Header-Datei werden nur dann aufgenommen, wenn die Header-Datei mit der Form #include "…" eingebunden wird. In einem Dokumentationskommentar werden die folgenden XML-Markierungen zur Formatierung des Texts im daraus erzeugten Dokument verwendet. Jede Markierung muss die XML-Regeln einhalten. Falls hier keine Beschreibung der Syntax angegeben ist, wird eine Markierung durch abgeschlossen. Die inhaltliche Beschreibung bezieht sich auf die Textformatierung, die aus solchen Markierungen erzeugt werden soll.
Element
text to be set like code Stellt Textfragmente (z.B. einzelne Wörter) in einer speziellen Schriftart für Quelltexte dar Stellt eine oder mehrere ganze Zeilen in einer speziellen Schriftart für Quelltexte dar Formatiert den Text als Beispiel * description Für Exceptions, die eine Funktion auslösen kann * Ein Verweis auf eine Datei mit Dokumentationskommentaren
1092
9 C++/CLI
Element
*
*
*
*
term description term description Erzeugt eine Liste oder Tabelle Definiert einen Abschnitt in einer summary-, remarks- oder returns-Markierung description Im Kommentar einer Methode zur Beschreibung der Parameter. Wird auch im ObjektBrowser angezeigt. Ergänzende Bemerkungen zu einem Parameter Ergänzende Informationen zu denen der summaryMarkierung. Wird auch im ObjektBrowser angezeigt. Beschreibt den Rückgabewert Ein Verweis auf ein Element, der als Link dargestellt wird. Ein Verweis auf ein Element, der als Link unter „See also“ aufgeführt wird Zusammenfassende Beschreibung, wird auch im ObjektBrowser angezeigt. Zur Beschreibung von Eigenschaften
Falls in einer mit * gekennzeichneten Markierung ein Verweis auf ein nicht vorhandenes Programmelement verwendet wird (z.B. aufgrund einen Schreibfehlers), wird vom Compiler durch eine Warnung darauf hingewiesen. Beispiel: Einige Dokumentationskommentare: using namespace System; namespace N_DocComments { // aus einem Beispiel des C++/CLI-Standards /// /// Class Point models a point in a /// two-dimensional plane. /// public ref class Point { public:
9.19 Dokumentationskommentare und CHM-Hilfedateien
1093
/// /// The Point's x-coordinate. /// property int X; /// /// The Points' y-coordinate. /// property int Y; /// ///This constructor initializes the Point to (0,0) /// Point() { X = 0; Y = 0; } /// /// This function changes the point's location /// to the given coordinates. /// /// /// xord is the new x-coordinate. /// /// /// yord is the new y-coordinate. /// /// /// /// The following code: /// /// Point p(3,5); /// p.Move(-1,3); /// /// results in p's having the value (-1,3). /// void Move(int xord, int yord) { X = xord; Y = yord;
Wird hier in einer param-Markierung ein Name verwendet, der keinen Parameter darstellt, ///
erzeugt der Compiler die Warnung:
warning: XML-Dokumentkommentar: Fehler: "xord1" ist kein Parameter, der in der Deklaration dieser Funktion benannt wird. Der Compiler erstellt im Debug- oder Release-Verzeichnis aus den Dokumentationskommentaren eine XML-Datei, die nur die XML-Markierungen enthält.
1094
9 C++/CLI
Beispiel: Die aus den Dokumentationskommentaren oben erzeugte XML-Datei enthält unter anderem diesen Text: yord is the new y-coordinate. The following code results in p's having the value (-1,3). Point p(3,5); p.Move(-1,3); This constructor initializes the new Point to (0,0).
Diese XML-Datei ist aber auch nicht leichter verständlich als die ursprünglichen Kommentare. Man kann sie aber mit einem XSL Stylesheet und einem XMLViewer etwas übersichtlicher darstellen. Fügt man mit dem unter http://dotnet.jku.at/DocView/ verfügbaren Stylesheet doc.xsl von Anders Hejlsberg die Zeile
als zweite Zeile in das XML-Dokument ein, erhält man mit dem bei Microsoft frei verfügbaren XML Notepad diese Darstellung:
9.19 Dokumentationskommentare und CHM-Hilfedateien
1095
9.19.2 Aus XML-Dateien Hilfedateien im CHM-Format erzeugen Das eigentliche Ziel von Dokumentationskommentaren ist aber nicht die XMLDatei, sondern ein aus ihr erzeugter leicht lesbarer Text (z.B. im HTML- oder pdfFormat, oder eine Hilfedatei im CHM-Format). Das ist mit verschiedenen (teilweise frei verfügbaren) Programmen möglich. Im Folgenden wird das frei verfügbare Sandcastle-Projekt vorgestellt, das Microsoft für die Erstellung der .NETDokumentation verwendet. Notwendige Installationen (siehe http://www.codeplex.com/SHFB): 1. Der frei verfügbare HTML Help Workshop von Microsoft, mit dem man auch unabhängig von Dokumentationskommentaren CHM-Hilfedateien erzeugen kann. 2. Die Sandcastle-Tools (http://www.sandcastledocs.com) Die folgenden Ausführungen orientieren sich an der Version 2.4.10520 vom 12.6.2008. Durch die Installation wird im Installationsverzeichnis (z.B. c:\Programme) ein Verzeichnis Sandcastle angelegt, das insbesondere das Unterverzeichnis Examples\sandcastle enthält. In diesem befindet sich eine bat-Datei mit dem sinnigen Namen build_Sandcastle. Diese muss man von einer Kommandozeile (Start|Programme|Zubehör|Eingabeaufforderung) im Debug- bzw. ReleaseVerzeichnis des Projekts starten, nachdem die XML-Datei erzeugt wurde. Dabei übergibt man als erstes Argument „vs2005“ und als zweites den Namen des Projekts. Unter Windows Vista sind dazu Administrator-Rechte notwendig. Weitere Informationen findet man auf http://blog.benhall.me.uk/2007/07/sandcastle-whatyou-need-to-know.html. Für ein Projekt mit dem Namen Doc1 wird build_Sandcastle mit dieser Kommandozeile gestartet: c:\Programme\Sandcastle\Examples\sandcastle\build_Sandcastle vs2005 Doc1 Dieser Aufruf erzeugt dann im Unterverzeichnis chm von Debug bzw. Release eine CHM-Hilfedatei Doc1.chm, die mit dem Beispiel von oben etwa so aussieht:
1096
9 C++/CLI
Diese CHM-Datei enthält insbesondere auch eine Beschreibung in Visual Basic und C#, obwohl nur ein C++-Projekt angelegt wurde. Die etwas umständliche Bedienung dieses Programms über build_Sandcastle.bat wird noch umständlicher, wenn man das voreingestellte Format der CHM-Datei ändern will. Weitere Programme (z.B. der Sandcastle Help File Builder, http://www.codeplex.com/SHFB) ermöglichen die Einstellung solcher Optionen über eine grafische Benutzeroberfläche.
9.20 Managed C++ und C++/CLI Ԧ C++/CLI ist eine Weiterentwicklung des sogenannten „Managed C++“, das in Visual Studio 2002 und 2003 verwendet wurde. Dieser Dialekt ist inzwischen überholt und sollte nicht mehr für neue Entwicklungen verwendet werden. Er steht aber mit der Befehlszeilenoption /clr:oldSyntax weiterhin zur Verfügung. Falls Sie nicht schon mit Visual C++ 2002/2003 programmiert haben, können Sie diesen Abschnitt auslassen. Die folgenden Beispiele zeigen jeweils gleichwertige Anweisungen in Managed C++ und C++/CLI und illustrieren so einige typische Unterschiede: – String-Literale müssen mit "S" beginnen – Die häufige Verwendung von „__“ (z.B. in __gc, __sealed, __property) hat nicht zur Lesbarkeit der Programme beigetragen:
9.20 Managed C++ und C++/CLI Ԧ __gc class R1 {}; ref class R2 {};
1097 // Verweisklasse in Managed C++ // Verweisklasse in C++/CLI
__value class V1 {}; // Werteklasse in Managed C++ value class V2 {}; // Werteklasse in C++/CLI __interface I1 {}; // Interface-Klasse in Managed C++ interface class I2 {}; // Interface-Klasse in C++/CLI
– Konversionen in elementare Datentypen müssen in Managed C++ mit __box durchgeführt werden: Console::WriteLine(S"{0}", __box(15)); // Managed C++ Console::WriteLine("{0}", 15); // C++/CLI
– Die Syntax von Managed C++ ist teilweise hochgradig nicht-intuitiv: Int32 f() [];// Managed C++ array^ f();// C++/CLI int GetArray() __gc[];// Managed C++ array^ GetArray();// C++/CLI
Zu den syntaktischen Unterschieden kommen noch teilweise diffizile semantische Unterschiede. Eine umfangreichere Gegenüberstellung der Unterschiede von Managed C++ und C++/CLI findet man in dem Artikel „Translation Guide: Moving Your Programs from Managed Extensions for C++ to C++/CLI“ von Stanley Lippman.
10 Einige Elemente der .NET-Klassenbibliothek
In Kapitel 2 wurden Steuerelemente vorgestellt, die für viele einfache WindowsProgramme ausreichen. In diesem Kapitel folgen weitere Steuerelemente, Toolbox-Komponenten und .NET-Klassen zu Themen wie Grafik, Steuerung von Office Anwendungen, Datenbanken, Internet usw. Diese gehören alle zur .NET Klassenbibliothek und machen Visual C++ zu einem Werkzeug, das die Entwicklung anspruchsvoller Windows-Programme mit wenig Aufwand ermöglicht. Angesichts des teilweise beträchtlichen Umfangs dieser Klassen ist keine vollständige Darstellung beabsichtigt. Stattdessen werden nur einige wichtige und typische Aspekte an Beispielen illustriert. Sie sollen dem Leser den Einstieg erleichtern und ihn zu einer weiteren Beschäftigung mit dem jeweiligen Thema anregen. Für weitere Informationen wird auf die Online-Hilfe (siehe Abschnitt 1.7) verwiesen. Anders als in den vorangehenden Kapiteln stehen die Abschnitte dieses Kapitels inhaltlich in keinem Zusammenhang und bauen nicht aufeinander auf. Einige der Themen sind sehr einfach und hätten auch schon in Kapitel 2 behandelt werden können. Andere setzen dagegen gute C++/CLI-Kenntnisse voraus.
10.1 Formatierte Texte mit RichTextBox Eine RichTextBox (Toolbox Registerkarte „Komponenten“) ist wie eine TextBox von der Basisklasse TextBoxBase abgeleitet. Diese Klassen haben deswegen viele gemeinsame Elemente. Im Unterschied zu einer TextBox hat eine RichTextBox aber weitaus mehr Möglichkeiten zur Formatierung von Text. Insbesondere können in einer RichTextBox verschiedene Teile des Textes unterschiedliche Attribute (Schriftgröße, Schriftart usw.) haben. Bei einer TextBox beziehen sich diese Attribute immer auf den gesamten Text. Texte im RTF-Format können mit vielen Textverarbeitungsprogrammen (z.B. Microsoft Word) erzeugt und dann in einer RichTextBox angezeigt werden:
1100
10 Einige Elemente der .NET-Klassenbibliothek
Die Schriftart bzw. die Schriftfarbe im markierten Bereich bzw. der Einfügeposition ist der Wert der Eigenschaft
property Font^ SelectionFont property Color SelectionColor Werte für diese Eigenschaften können in einem Font- oder ColorDialog ausgewählt werden. if (fontDialog1->ShowDialog()== System::Windows::Forms::DialogResult::OK) richTextBox1->SelectionFont=fontDialog1->Font; if (colorDialog1->ShowDialog()== System::Windows::Forms::DialogResult::OK) richTextBox1->SelectionColor=colorDialog1->Color;
Die Eigenschaft Font hat unter anderem die folgenden Elemente:
property String^ Name // Name der Schriftart property bool Bold // Gibt an, ob die Schriftart fett ist property float Size // Schriftgröße in der in Unit festgelegten Maßeinheit Diese können aber nur gelesen und nicht zugewiesen werden. Wenn man diese Eigenschaften setzen will, muss man einen der zahlreichen Konstruktoren der Klasse Font verwenden, wie z.B.: richTextBox1->AppendText("Line\r\n"); richTextBox1->SelectionFont= gcnew System::Drawing::Font("Courier",15); richTextBox1->AppendText("Courier 15\r\n"); richTextBox1->SelectionFont = gcnew System::Drawing::Font( "Verdana", 10, FontStyle::Bold ); richTextBox1->AppendText("Verdana 10 fett \r\n");
Falls dabei eine Schriftart angegeben wird, die es nicht gibt, wird eine StandardSchriftart verwendet: richTextBox1->SelectionFont= gcnew System::Drawing::Font("This is no font name",20);
10.1 Formatierte Texte mit RichTextBox
1101
Mit den nächsten beiden Funktionen kann ein Text im RTF-Format geladen bzw. gespeichert werden (siehe auch Aufgabe 2.10):
void LoadFile(String^ path); void SaveFile(String^ path); Die Funktion LoadFile funktioniert nur mit Dateien im RichText-Format. Falls eine Datei nur die Endung „rtf“, aber nicht dieses Format hat, ist eine Exception die Folge. Um einen Text im Textformat in eine RichTextBox einzulesen, ist eine der folgenden Varianten notwendig. Dabei muss für den zweiten Parameter z.B. der Wert RichTextBoxStreamType::PlainText angegeben werden.
void LoadFile(String^ path, RichTextBoxStreamType fileType); void SaveFile(String^ path, RichTextBoxStreamType fileType); Mit Undo werden die letzten Änderungen im Text rückgängig gemacht und mit Redo die letzten Undo-Operationen (wie Bearbeiten|Wiederholen):
void Undo(); void Redo(); Der gesamte Text kann mit SelectAll markiert werden:
void SelectAll(); Für die Arbeit mit der Zwischenablage stehen diese Methoden zur Verfügung:
void Copy(); // kopiert den markierten Text in die Zwischenablage void Cut(); // schneidet markierten Text aus, Kopie in die Zwischenablage void Paste(); // Text aus der Zwischenablage einfügen Aufgabe 10.1 Erzeugen Sie mit einer RichTextBox ein RTF-Dokument, das aus einer Überschrift (Schriftart Arial, Schriftgröße 15, fett) besteht sowie aus einigen weiteren Zeilen in der Schriftart Courier, Schriftgröße 12, nicht fett. Öffnen Sie diese Datei dann mit Microsoft Word oder einem anderen Editor, der das RTF-Format lesen kann (z.B. WordPad, aber nicht notepad).
1102
10 Einige Elemente der .NET-Klassenbibliothek
10.2 Steuerelemente zur Eingabe und Prüfung von Daten Für den korrekten Ablauf eines Programms sind meist Benutzereingaben mit zulässigen Werten erforderlich. Diese können im Wesentlichen auf zwei Arten erreicht werden: 1. Der Benutzer kann (z.B. in einer TextBox) beliebige Strings eingeben. Das Programm prüft dann, ob die Strings zulässige Werte darstellen. Falls das nicht zutrifft, wird der Anwender auf den Fehler hingewiesen. Dazu gibt es unter anderem die folgenden Möglichkeiten: – Falls der String einen Wert eines C++/CLI-Basisdatentyps darstellen soll, kann man zur Formatprüfung die Methoden Parse oder TryParse des Datentyps oder die Methoden der Klasse Convert verwenden. Diese lösen entweder eine Exception des Typs FormatException aus oder geben einen Wert zurück, der angibt, ob der String das zulässige Format hat oder nicht. – Falls es keine vordefinierten Funktionen gibt, muss man entweder einen regulären Ausdruck (siehe Abschnitt 10.18) finden oder einen eigenen Parser schreiben, der den String prüft. 2. Die Prüfungen unter 1. können recht aufwendig werden. Falls Steuerelemente zur Verfügung stehen, die unzulässige Eingaben unterbinden, ist es meist einfacher, diese zu verwenden. Dazu gehören z.B. – CheckBoxen und RadioButtons für boolesche Werte, – Listboxen für die Auswahl aus einer Liste (z.B. Werte eines Aufzählungstyps) – vordefinierte Dialoge wie OpenFileDialog und FontDialog zur Eingabe von Dateinamen, Farben, Schriftarten usw. – die Komponenten MonthCalendar und DateTime¬Picker (siehe Abschnitt 10.8.2) für Uhrzeiten und Kalenderdaten. – geeignete selbstdefinierte Steuerelemente oder Dialoge. Im Folgenden werden einige Steuerelemente und Konzepte vorgestellt, die vor allem im Zusammenhang mit der Dateneingabe und der Prüfung von Eingabedaten stehen.
10.2.1 Fehleranzeigen mit ErrorProvider Falls nur ein einziger String geprüft werden soll, ist oft eine MessageBox für einen Hinweis auf einen Eingabefehler ausreichend. Bei einem Formular mit mehreren Eingabefeldern kann es aber lästig werden, alle wegzuklicken, wenn mehrere Eingaben falsch sind. Dann ist oft ein ErrorProvider (Toolbox Registerkarte „Komponenten“) angemessen, der ein Fehlersymbol neben einem Steuerelement anzeigt. Ein Errorprovider hat die Methode
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1103
void SetError(Control^ control, String^ value) der man als erstes Argument ein Steuerelement und als zweites Argument einen Text übergeben kann. Nach ihrem Aufruf wird dann neben dem Steuerelement ein blinkendes Symbol angezeigt. Fährt man mit der Maus über das Symbol, wird in einem kleinen Fenster der übergebene String angezeigt. Über Eigenschaften wie
property Icon^ Icon property int BlinkRate kann das dargestellte Symbol, die Blinkfrequenz usw. festgelegt werden. Mit
void Clear() werden alle Einstellungen wieder gelöscht. Beispiel: Falls der Text in textBox1 keinen int-Wert darstellt, wird durch int x; errorProvider1->Clear(); if (!int::TryParse(textBox2->Text,x)) errorProvider1->SetError(textBox1, "int value required");
ein Fehlersymbol neben der TextBox angezeigt:
Fährt man mit der Maus über dieses Symbol, wird der Text „int value required“ angezeigt.
10.2.2 Weitere Formulare und selbstdefinierte Dialoge anzeigen Viele Programme verwenden neben dem Hauptformular und den Standarddialogen von Abschnitt 2.10 weitere Formulare zur Anzeige und Eingabe von Daten. Im Folgenden gezeigt, wie man solche Formulare erzeugen und anzeigen kann. Beispiel: Als Beispiel wird ein Projekt verwendet, das ein Formular Form1 mit der Datei Form1.h enthält. Durch das Anklicken des Buttons wird dann später ein weiteres Formular angezeigt.
1104
10 Einige Elemente der .NET-Klassenbibliothek
Einem Projekt kann man mit Projekt|Neues Element hinzufügen|Visual C++|UI|Windows Form ein weiteres Formular hinzufügen. Dabei muss man unter Name einen Namen eingeben:
Dieses Formular kann man dann wie alle bisherigen Formulare mit Komponenten aus der Toolbox gestalten. Beispiel: In diesem Beispiel soll das Formular Form2 heißen und die zugehörige Header-Datei Form2.h. Diesem Formular wird eine TextBox, ein OKButton und ein Abbrechen-Button hinzugefügt:
Damit man in einer Funktion des einen Formulars (z.B. Form1) auf die Elemente eines anderen Formulars (z.B. Form2) zugreifen kann, muss man die HeaderDatei des verwendeten Formulars (hier Form2.h) mit einer #include-Anweisung in die Header-Datei des aufrufenden Formulars (hier Form1.h) aufnehmen. Diese #include-Anweisung kann man am Anfang von Form1.h vor dem Namensbereich des Projekts manuell eintragen:
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1105
Das Formular kann dann mit gcnew Form2 beim Start des Programms im Konstruktor von Form1 erzeugt werden. Damit man später darauf zugreifen kann, muss man diesen Ausdruck einer Variablen des Typs Form2^ zuweisen (im Beispiel form2):
Nach diesen Vorbereitungen kann ein Formular durch einen Aufruf der Methoden Show und ShowDialog angezeigt werden:
void Show() DialogResult ShowDialog() Diese beiden Methoden unterscheiden sich vor allem durch den Zeitpunkt, zu dem die im Quelltext auf ihren Aufruf folgende Anweisung ausgeführt wird. Bei Show wird sie unmittelbar anschließend ausgeführt, ohne auf das Schließen des Fensters zu warten. Bei ShowDialog wird sie dagegen erst nach dem Schließen des Fensters ausgeführt. Damit ein anderes Fenster der Anwendung aktiviert werden kann, muss zuerst das mit ShowDialog angezeigte Fenster geschlossen werden. Ein mit ShowDialog angezeigtes Formular wird als modales Fenster, modaler Dialog oder als modales Dialogfeld bezeichnet. ShowDialog wird vor allem dann verwendet, wenn man Daten aus dem Fenster in die Anwendung übernehmen will. Falls man nur Daten anzeigen will, kann man auch Show verwenden. Ein modales Fenster wird durch eine Zuweisung eines von Null verschiedenen Wertes an die Eigenschaft DialogResult geschlossen. Dieser Wert ist dann der Rückgabewert von ShowDialog. Über diesen Rückgabewert informiert man den Aufrufer, mit welchem Button ein modales Formular geschlossen wurde. Visual C++ sieht dafür Werte wie DialogResult::Cancel, DialogResult::Ok usw. vor (siehe auch Abschnitt 2.10). Wenn ein modales Fenster mit der SchließenSchaltfläche geschlossen wird, erhält DialogResult den Wert DialogResult::Cancel. Beispiel: Wenn das modale Fenster Form2 zwei Buttons Abbrechen und OK hat, mit denen man es schließen kann, weist man DialogResult beim Anklicken dieser Buttons zwei verschiedene Werte zu, die nicht Null sind.
1106
10 Einige Elemente der .NET-Klassenbibliothek private: System::Void Abbrechen_Click( System::Object^ sender, System::EventArgs^ e) { DialogResult=System::Windows::Forms:: DialogResult::Cancel; } private: System::Void OK_Click( System::Object^ sender, System::EventArgs^ e) { DialogResult=System::Windows::Forms:: DialogResult::OK; }
Im aufrufenden Formular kann man über den Rückgabewert von ShowDialog abfragen, mit welchem Button das Formular geschlossen wurde:
Damit man wie in diesem Beispiel in der Klasse Form1 auf eine Eigenschaft von form2->textBox1 zugreifen kann, muss diese public sein. Das erreicht man sowohl mit einer manuellen Angabe von public als auch über die Eigenschaft Modifiers im Eigenschaftenfenster (Abschnitt Entwurf). Diese Vorgehensweise lässt sich weiter vereinfachen, indem man der Eigenschaft DialogResult (z.B. im Eigenschaftenfenster) der Schließen-Buttons einen Wert zuweist. Dann wird beim Anklicken der Buttons dieser Wert der Eigenschaft DialogResult des Formulars zugewiesen. So kann man sich die Ereignisbehandlungsroutinen Abbrechen_Click und OK_Click sparen. Ein Aufruf von Show bzw. ShowDialog oder das Schließen eines Formulars wirkt sich nur auf die Anzeige aus. Sie hat keinen Einfluss darauf, ob ein Formular und seine Daten geladen sind (und damit Speicherplatz belegen) oder nicht. Deshalb kann man die Daten eines geschlossenen Formulars wie in der Abbildung des letzten Beispiels ansprechen. Aus diesem Grund wurde das Formular auch beim Start des Programms erzeugt (im Konstruktor). Bei Formularen, die als Dialogfelder verwendet werden, setzt man oft auch noch die folgenden Eigenschaften:
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1107
– Der Eigenschaft AcceptButton des Formulars wird der OK-Button zugewiesen (am einfachsten im Eigenschaftenfenster auswählen). Dann hat ein Drücken der Enter-Taste denselben Effekt wie das Anklicken des OK-Buttons. – Der Eigenschaft CancelButton des Formulars wird der Abbrechen-Button zugewiesen (am einfachsten im Eigenschaftenfenster auswählen). Dann hat ein Drücken der ESC-Taste denselben Effekt wie das Anklicken dieses Buttons. – Die Eigenschaft FormBorderStyle wird auf FixedDialog gesetzt. Dann kann man die Größe des Fensters nicht verändern. – Die Eigenschaften ControlBox, MinimizeBox und MaximizeBox werden auf false gesetzt.
Aufgabe 10.2.2 Beim Anklicken eines Buttons mit der Aufschrift Login soll ein modaler Dialog mit einem OK- und einem Abbrechen-Button angezeigt werden, in dem der Anwender aufgefordert wird, seinen Namen in einer TextBox einzugeben. Falls der Dialog mit dem OK-Button oder der Enter-Taste verlassen wird, soll der Name im Hauptprogramm (this->Text) angezeigt werden. Falls er mit dem AbbrechenButton oder der ESC-Taste verlassen wird, sollen keine weiteren Aktionen stattfinden.
10.2.3 Das Validating-Ereignis Bei Formularen mit mehreren Eingabefeldern ist es oft am besten, jede Eingabe nach dem Verlassen des Eingabefeldes zu prüfen. Das ist mit einer Ereignisbehandlungsroutine für das Validating-Ereignis möglich. Dieses Ereignis ist in der Klasse Control definiert und tritt nach dem Ereignis Leave (wenn das Steuerelement den Fokus verliert) ein. Beispiel: Mit den Ereignisbehandlungsroutinen für das Validating-Event der beiden TextBoxen private: System::Void textBox3_Validating( System::Object^ sender, System::ComponentModel::CancelEventArgs^ e) { errorProvider1->Clear(); try { Convert::ToInt32(textBox3->Text); } catch(FormatException^) { errorProvider1->SetError(textBox3,"int required"); } }
1108
10 Einige Elemente der .NET-Klassenbibliothek private: System::Void textBox2_Validating( System::Object^ sender, System::ComponentModel::CancelEventArgs^ e) { int x; errorProvider2->Clear(); if (!int::TryParse(textBox2->Text,x)) errorProvider2->SetError(textBox2,"int required"); }
erhält man nach dem Verlassen der beiden TextBoxen diese Anzeige:
Mit dem Validating-Ereignis kann man insbesondere von der Klasse TextBox abgeleitete Klassen definieren, die nur bestimmte Eingaben (z.B. int-Werte) zulassen (siehe Aufgabe 10.2.5). Im Zusammenhang mit dem Validating-Ereignis sind gelegentlich auch die folgenden Eigenschaften und Methoden von Bedeutung: – Das Validating-Event kann mit dem Wert false der Eigenschaft
property bool CausesValidation auch unterdrückt werden. Setzt man die Eigenschaft
property bool Cancel des CancelEventArgs-Parameters nicht auf false, wird nach dem Ereignis Validating das Ereignis Validated ausgelöst. In der Ereignisbehandlungsroutine zu Validated kann man Anweisungen aufnehmen, die bei einer erfolgreichen Validierung ausgeführt werden sollen. – Mit der Methode
virtual bool ValidateChildren() override eines Formulars kann man für alle Steuerelemente des Formulars das Ereignis Validating auslösen und so die Werte aller solchen Steuerelemente prüfen. Beispiel: Ein Aufruf der folgenden Funktion führt zum Aufruf der ValidatingEreignisbehandlungsroutine für alle Steuerelemente des Formulars:
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1109
private: System::Void button2_Click( System::Object^ sender, System::EventArgs^ e) { this->ValidateChildren(); // this zeigt auf das } // Formular
10.2.4 Texteingaben mit einer MaskedTextBox filtern Am einfachsten werden Eingabefehler vermieden, wenn der Anwender überhaupt keine Möglichkeit für falsche Eingaben hat. Das ist manchmal mit einer MaskedTextBox möglich. Die Klasse MaskedTextBox (Toolbox Registerkarte „Allgemeine Steuerelemente“) ist wie eine TextBox oder eine RichTextBox von der Klasse TextBoxBase abgeleitet und hat viele Gemeinsamkeiten mit diesen Klassen. Über ihre Eigenschaft Text kann man wie mit einer TextBox einen String einlesen. Die Eigenschaft
property String^ Mask stellt eine Eingabemaske dar, mit der man unzulässige Eingaben unterbinden kann. Durch Anklicken des Buttons in der rechten Spalte der Eigenschaft Mask im Eigenschaftenfenster (bzw. der Option „Maske festlegen“ im Kontextmenü) wird ein Maskeneditor aufgerufen, der einige Beispiele für Eingabemasken enthält. Im Eingabefeld „Vorschau“ kann man eine Eingabemaske testen.
Eine Eingabemaske besteht aus – Zeichen für notwendige und optionale Eingaben wie
1110
10 Einige Elemente der .NET-Klassenbibliothek
Kategorie Ziffer Ziffer, +, – Buchstabe alphanumerisches Zeichen beliebiges Zeichen Trennzeichen (in der Landeseinstellung) für Stunden, Minuten und Sekunden Trennzeichen (in der Landeseinstellung) für Tag, Monat und Jahr
notwendig 0 L A & :
optional 9 # ? a C
/
– Zeichen, die die folgenden Zeichen formatieren wie > folgende Zeichen werden in Großschreibung dargestellt < folgende Zeichen werden in Kleinschreibung dargestellt – Zeichen, die unverändert wiedergegeben werden. Dazu gehören alle Zeichen, die nicht für notwendige oder optionale Eingaben und Formatierungen stehen. Für eine umfassende Beschreibung wird auf die Online-Hilfe verwiesen. Die Eigenschaft Text enthält dann die mit der Eingabemaske Mask formatierten eingegeben Zeichen. Über den Wert der Eigenschaft
property bool MaskFull kann man prüfen, ob alle erforderlichen Eingaben gemacht wurden. Beispiel: Mit den Anweisungen maskedTextBox1->Mask="00/00/0000"; maskedTextBox1->Text="1223"; textBox1->AppendText(String::Format( "'{0}' full={1}\r\n" , maskedTextBox1->Text, maskedTextBox1->MaskFull)); maskedTextBox2->Text="12234567"; textBox1->AppendText(String::Format( "'{0}' full={1}\r\n", maskedTextBox1->Text, maskedTextBox1->MaskFull));
erhält man diese Ausgaben: '12.23.' full=False '12.23.4567' full=True
Für zahlreiche Datentypen gibt es Eingaben, die zu einer Eingabemaske passen, aber trotzdem keinen gültigen Wert darstellen. So passt z.B. die Zeichenfolge
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1111
„29.02.2007“ zur Eingabemaske „00/00/0000“, ohne ein zulässiges Datum darzustellen. Falls ein Datentyp eine Parse-Methode mit einer der beiden Signaturen
static Object^ Parse(String^) static Object^ Parse(String^, IFormatProvider^) hat, und dieser Datentyp der MaskedTextBox-Eigenschaft
property Type^ ValidatingType zugewiesen, wird diese Parse-Methode aufgerufen, wenn die MaskedTextBox den Fokus verliert. In der Ereignisbehandlungsroutine zum Ereignis TypeValidationCompleted kann man dann über den Wert von e->IsValidInput prüfen, ob diese Prüfung erfolgreich war. Beispiel: Durch die Markierung von „ValidatingType verwenden“ im „Eingabeformat“-Dialog oder eine Zuweisung wie maskedTextBox1->ValidatingType= System::DateTime::typeid;
wird beim Verlassen der MaskedTextBox die Zulässigkeit des eingegebenen Wertes geprüft. System::Void maskedTextBox1_TypeValidationCompleted( System::Object^ sender, System::Windows::Forms::TypeValidationEventArgs^ e) { if (!e->IsValidInput) MessageBox::Show("Unzulässiger Wert"); }
Für selbstdefinierte Verweisklassen ist für solche Prüfungen nur eine ParseMethode notwendig, die bei einer unzulässigen Eingabe eine Exception auslöst. Diese Exception bewirkt dann, dass in der TypeValidationCompleted-Ereignisbehandlungsroutine IsValidInput den Wert false hat. Beispiel: Wenn eine Verweisklasse myByte nur Werte im Bereich 0..255 darstellen soll, erreicht man mit einer Methode Parse wie in ref class myByte { int x; public: myByte(int x_):x(x_){}; static Object^ Parse(String^ s) { int n=Convert::ToInt16(s); if (0Handled auf true setzt. Beispiel: In einer TextBox mit dieser Ereignisbehandlungsroutine können nur Ziffern, Punkte und Backspace-Zeichen eingegeben werden: private: System::Void textBox1_KeyPress( System::Object^ sender, System::Windows::Forms::KeyPressEventArgs^ e) { if ((e->KeyChar>='0' && e->KeyCharKeyChar=='.') || (e->KeyChar=='\b') ) ; // Setzt man e->Handled nicht auf true, wird else // KeyChar an die TextBox weitergegeben { e->Handled = true; // Die Weitergabe von } // KeyChar an die TextBox unterbinden }
Aufgabe 10.2.5 Schreiben Sie (z.B. analog zu MyTextBox von Abschnitt 9.13.4) eine von der Klasse TextBox abgeleitete Klasse IntInputBox zur Eingabe von int-Werten. Der eingegebene Wert soll in einer int-Eigenschaft value zur Verfügung stehen. a) Im Validating-Ereignis soll geprüft werden, ob die Eigenschaft Text eine Zahl darstellt. Falls das nicht zutrifft, soll in einem ErrorProvider darauf hingewiesen werden. b) Erweitern Sie die Klasse um eine Prüfung im KeyPress-Ereignis, so dass nur Ziffern und ein „+“ oder „–“-Zeichen eingegeben werden kann. c) Definieren Sie diese Klasse in einer Windows Forms Steuerelementbibliothek und nehmen Sie diese in die ToolBox auf (siehe Abschnitt 9.15).
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1113
10.2.6 Hilfe-Informationen mit ToolTip und HelpProvider Im Zusammenhang mit Steuerelementen zur Dateneingabe ist es oft empfehlenswert, dem Anwender die Bedeutung des Steuerelements durch zusätzliche Informationen zu erläutern. Dazu stehen ToolTip und HelpProvider zur Verfügung. Setzt man eine ToolTip-Komponente (Toolbox Registerkarte „Allgemeine Steuerelemente“) auf ein Formular, wird bei jeder Komponente des Formulars im Eigenschaftenfenster eine zusätzliche Kategorie „Sonstiges“ mit der Eigenschaft „ToolTip auf ToolTip1“ eingefügt. Hier kann man einen Text eintragen, der angezeigt wird wenn die Maus über das Steuerelement bewegt wird. Über zahlreiche Eigenschaften kann man die Hintergrundfarbe, die Dauer der Anzeige, die Verzögerung bis zur Anzeige usw. steuern. Mit verschiedenen ToolTips kann man ToolTips mit unterschiedlichen Eigenschaften definieren, wie z.B. zuerst einen kurzen Text und nach fünf Sekunden einen ausführlicheren. Ein HelpProvider (Toolbox Registerkarte „Komponenten“) hat Ähnlichkeiten mit einem ToolTip: Nachdem man eine solche Komponente auf ein Formular gesetzt hat, wird bei den anderen Komponenten auf dem Formular im Eigenschaftenfenster eine zusätzliche Kategorie „Sonstiges“ mit den folgenden Eigenschaften eingeblendet:
Diese Eigenschaften kann man auch durch Anweisungen wie die folgenden setzen. Mit
virtual void SetShowHelp(Control^ ctl, bool value) wird mit dem Argument true für value festgelegt, dass für das Argument für ctl eine Hilfe angezeigt wird. Im einfachsten Fall ist das ein einfacher String, der mit
virtual void SetHelpString(Control^ ctl, String^ helpString) festgelegt wird. Beispiel: Nach der Ausführung der Anweisungen helpProvider1->SetShowHelp(button1,true); helpProvider1->SetHelpString(this->button1, "Da kann ich Ihnen auch nicht helfen");
1114
10 Einige Elemente der .NET-Klassenbibliothek
wird nach dem Drücken der Taste F1 der in SetHelpString festgelegte Text angezeigt, wenn button1 den Fokus hat. Über die Eigenschaft
virtual property String^ HelpNamespace kann der Name einer CHM- oder HTML-Datei festgelegt werden, die beim Drücken der F1-Taste angezeigt wird. Mit
virtual void SetHelpKeyword(Control^ ctl, String^ keyword) virtual void SetHelpNavigator(Control^ ctl, HelpNavigator navigator) kann ein Schlüsselwort festgelegt werden, nach dem in der Datei gesucht wird. Für HelpNavigator kann man einen Wert wie Topic usw. des C++/CLI-Aufzählungstyps HelpNavigator angeben. Dieser Wert legt fest, dass die Online-Hilfe zu dem mit keyword angegebenen Thema angezeigt werden soll. Beispiel: Wenn button1 den Fokus hat und F1 gedrückt wird, wird nach der Ausführung der Anweisungen helpProvider1->HelpNamespace= "c:\\WINDOWS\\help\\Glossary.chm"; helpProvider1->SetHelpKeyword(this->button1, "mk:@MSITStore:c:\\WINDOWS\\help\\Glossary.chm::" "/glossary_pro.htm#gls_dhcp"); helpProvider1->SetHelpNavigator(button1, HelpNavigator::Topic);
der Text aus Glossary.chm angezeigt, der zum in SetHelpKeyword festgelegten Thema gehört. Die Klasse Help bietet ähnlich Möglichkeiten.
10.2.7 Auf/Ab-Steuerelemente Die von der Klasse UpDownBase abgeleiteten Steuerelemente NumericUpDown und DomainUpDown haben Ähnlichkeiten mit einer ListBox oder einer ComboBox. Sie bestehen aus einem Textfeld und zwei kleinen Buttons am Rand, die als Auf/Ab-Buttons bezeichnet werden. Durch das Anklicken der Auf/Ab-Buttons wird im Textfeld der nächste bzw. der vorhergehende Wert aus einem vorgegebenen Bereich von Werten angezeigt. Mit den Werten Left oder Right (Voreinstellung) der Eigenschaft
property LeftRightAlignment UpDownAlign kann man die Auf/Ab-Buttons an den linken oder rechten Rand des Textfeldes setzen. Der Wert der Eigenschaft
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1115
property bool ReadOnly legt fest, ob ein Text auch im Textfeld oder nur über die Auf/Ab-Buttons geändert werden kann. Mit dem NumericUpDown-Steuerelement (Toolbox Registerkarte „Allgemeine Steuerelemente“)
kann man Zahlenwerte des Datentyps Decimal (siehe Abschnitt 3.6.6) im Bereich der Eigenschaften
property Decimal Minimum property Decimal Maximum eingeben. Beim Anklicken des Auf- oder Ab-Buttons wird der angezeigte Wert
property Decimal Value um den Wert der Eigenschaft
property Decimal Increment erhöht oder reduziert. Dieser Typ kann mit Funktionen wie Convert::ToInt32 oder Convert::ToDouble in zahlreiche andere numerische Datentypen konvertiert werden. Die Anzahl der Nachkommastellen kann mit
property int DecimalPlaces festgelegt werden. Mit der Komponente DomainUpDown (Toolbox Registerkarte „Alle Windows Forms“) kann mit den Auf/Ab-Buttons ein Element der Eigenschaft
property DomainUpDownItemCollection^ Items auswählen. Der Datentyp dieser Eigenschaft ist im Wesentlichen eine ArrayList (siehe Abschnitt 10.12.1)
ref class DomainUpDownItemCollection: public ArrayList die Elemente eines beliebigen von Object abgeleiteten Typs enthalten kann. Im Textfeld wird der Wert der ToString-Methode des ausgewählten Objekts angezeigt. Mit ArrayList-Methoden wie
1116
10 Einige Elemente der .NET-Klassenbibliothek
virtual int Add(Object^ item) override virtual void Remove(Object^ item) override kann man Elemente hinzufügen und entfernen. Wenn ein Auf/Ab-Button angeklickt wird, tritt das Ereignis SelectedItemChanged ein. In der zugehörigen Ereignisbehandlungsroutine kann man dann z.B. über die Eigenschaft
property Object^ SelectedItem auf das ausgewählte Element zugreifen.
10.2.8 Schieberegler: VScrollBar und HScrollBar Die Komponenten HScrollBar und VScrollBar (Toolbox Registerkarte „Alle Windows Forms“) stellen horizontale und vertikale Schieberegler dar, deren Schieber (der auch als Bildlauffeld bezeichnet wird) mit der Maus oder mit den Pfeiltasten bewegt werden kann. Seine aktuelle Position ist der Wert der Eigenschaft Value, die Werte im Bereich der Eigenschaften Minimum und Maximum annehmen kann. Alle diese Eigenschaften haben den Datentyp int. Schieber
Scrollbars werden oft als Bildlaufleisten am Rand von Fenstern verwendet, die nicht groß genug sind, um den gesamten Inhalt anzuzeigen. Dann zeigt die Position des Schiebers die aktuelle Position im Dokument an. Eine weitere typische Anwendung ist ein Lautstärkeregler. Ein Schieberegler kann zur Eingabe von ganzzahligen Werten verwendet werden. Mit der Maus an einem Schieber zu ziehen ist oft einfacher als das Eintippen von Ziffern in eine TextBox. Eine solche Eingabekomponente kann völlig ausreichend sein, wenn es nicht auf absolute Genauigkeit ankommt (wie etwa bei einem Lautstärkeregler). Wenn der Schieber verschoben wird, tritt das Ereignis Scroll ein. Die folgende Ereignisbehandlungsroutine zeigt die aktuelle Position des Schiebers in der TextBox textBox1 an: private: System::Void hScrollBar1_Scroll(System::Object^ sender, System::Windows::Forms::ScrollEventArgs^ e) { textBox1->Text=Convert::ToString(hScrollBar1->Value); }
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1117
Aufgabe 10.2.8 In der frühen Steinzeit der Rechenmaschinen (bis ca. 1970) gab es nicht nur Digitalrechner (wie heute nahezu ausschließlich), sondern auch Analogrechner. Die Bezeichnung „analog“ kommt daher, dass mathematische Zusammenhänge durch physikalische Geräte dargestellt wurden, bei denen aus Eingabewerten in Analogie zu den mathematischen Zusammenhängen Ausgabewerte erzeugt werden. Beispiele sind der Rechenschieber oder spezielle elektrische Geräte, bei denen man die Operanden an Drehreglern eingeben und das Ergebnis an Zeigerinstrumenten ablesen konnte. Analogrechner wurden oft für spezielle Aufgaben entwickelt, z.B. um mit den Kirchhoffschen Regeln Gleichungssysteme zu lösen. Sie waren oft wesentlich schneller als die damaligen Digitalrechner. Schreiben Sie ein Programm, mit dem man wie bei einem Analogrechner die Koeffizienten a, b und d des symmetrischen linearen Gleichungssystems ax + by = 1 bx + dy = 1 an Schiebereglern einstellen kann, und das bei jeder Positionsänderung eines Schiebereglers die Lösung x = (b – d)/(b*b – a*d) y = (b – a)/(b*b – a*d) ausgibt:
Wenn einer der Schieberegler bewegt wird (Ereignis Scroll), sollen die Koeffizienten in einer TextBox und die Ergebnisse sowie eine Probe (bei der das Ergebnis in die Gleichung eingesetzt wird) auf einem Label dargestellt werden. Achten Sie darauf dass keine Division durch 0 stattfindet. Dann soll für x bzw. y kein neuer Wert angezeigt werden, sondern stattdessen der Text „Division durch 0“.
1118
10 Einige Elemente der .NET-Klassenbibliothek
10.2.9 Lokalisierung Visual Studio ermöglicht auf einfache Weise, die Texte eines Programms in verschiedenen Sprachen anzubieten. Das können sowohl die Aufschriften auf Steuerelementen (z.B. auf einem Button) als auch Meldungen sein. Damit kann ein Button eine englische Aufschrift haben, wenn das Programm von einem Windows mit englischen Ländereinstellungen gestartet wird, und eine deutsche Aufschrift bei einem deutschen Windows. Damit eine Anwendung lokalisiert wird, muss die Eigenschaft Localizable im Eigenschaftenfenster des Formulars auf true gesetzt werden. Obwohl diese Eigenschaft im Eigenschaftenfenster des Formulars zur Verfügung steht, handelt es sich nicht um ein Element der Klasse Formular: Der Ausdruck this->Localizable; // error
führt zu der Fehlermeldung „'Localizable': Ist kein Element von 'Form1'“. Wenn Localizable auf true gesetzt wurde, führt jede Wahl der FormularEigenschaft Language
dazu, dass für diese Sprache eine Ressourcen-Datei erzeugt wird, die landesspezifische Texte und Einstellungen des Formulars sowie weitere Daten enthält. Der Name dieser Ressourcen-Datei setzt sich aus dem Namen des Formulars, dem Sprachkürzel und der Endung „resx“ zusammen. Bei einem Programm mit den beiden Spracheinstellungen „deutsch“ (Sprachkürzel „de“) und englisch (Sprachkürzel „en“)
10.2 Steuerelemente zur Eingabe und Prüfung von Daten
1119
Wählt man dann im Eigenschaftenfenster des Formulars eine bestimmte Sprache (Eigenschaft Language), kann man mit dem Formulardesigner das Formular für diese Sprache gestalten. Dabei werden nicht nur unterschiedliche Aufschriften, sondern auch unterschiedliche Positions- und Größenangaben in der resx-Datei gespeichert. Beim Start des Programms wird dann die Sprachversion des Formulars angezeigt, die den Ländereinstellungen unter Windows entspricht. Falls für diese Einstellung keine Sprachversion angelegt wurde, wird die für die Spracheinstellung „Standard“ angezeigt. Da man oft für alle die Sprachen, für die keine spezielle Sprachversion angelegt wurde, eine englische Beschriftung will, wird man in diesem Fall für die für die Spracheinstellung „Standard“ eine englische Beschriftung wählen. Zum Testen kann man natürlich die Ländereinstellungen unter Windows entsprechend setzen. Meist ist es aber einfacher, der Eigenschaft CurrentUICulture des aktuellen Thread im Konstruktor vor dem Aufruf von InitializeComponent eine CultureInfo zuzuweisen: Form1(void) { using namespace System::Threading; using namespace System::Globalization; // das muss vor InitializeComponent(); kommen: // Thread::CurrentThread->CurrentUICulture= gcnew CultureInfo("de-DE"); // Vorsicht: CurrentUICulture, nicht CurrentCulture Thread::CurrentThread->CurrentUICulture=gcnew CultureInfo("en"); //Thread::CurrentThread->CurrentUICulture=gcnew CultureInfo("en-US"); InitializeComponent(); }
Weitere Daten (z.B. Meldungen, Bilder, Symbole usw.), die spezifisch für eine länderspezifische Version des Programms sind, können in eigenen RessourcenDateien untergebracht werden. Solche Ressourcen-Dateien können mit Projekt|Neues Element hinzufügen|Visual C++|Ressource|Assemblyressourcendatei (.resx) erzeugt werden. Ihr Name besteht aus einem frei wählbaren ersten Teil, auf den ein Punkt und die Länderkennung sowie die Endung „.resx“ folgen.
1120
10 Einige Elemente der .NET-Klassenbibliothek
Nach einem Doppelklick auf die Ressourcendatei im Projektmappenexplorer kann man in der ersten Spalte den Namen einer Ressource eingeben und in der zweiten Spalte ihren landesspezifischen Wert:
Auf eine solche Ressource kann man dann mit einem ResourceManager-Objekt zugreifen, das mit einem Namen initialisiert wird, der sich aus dem der Anwendung und der Ressource zusammensetzt und weder die Länderkennung noch die Endung „resx“ enthält: using namespace System::Resources; ResourceManager^ r=gcnew ResourceManager( "LocalizedApp.MyRessource",System::Reflection::Assembly: :GetExecutingAssembly());
Über die Methode
virtual String^ GetString(String^ name) dieses Objekts erhält man dann zum Namen einer String-Ressource den String, der zur aktuellen Landeseinstellung gehört. Mit den Ressourcen-Dateien von oben ist das mit MessageBox::Show(rm->GetString("FileOpenError_Message"));
bei einer deutschen Landeseinstellung der String „Fehler beim Öffnen der Datei“ und bei einer englischen „file open error“.
10.3 Symbolleisten, Status- und Fortschrittsanzeigen
1121
10.3 Symbolleisten, Status- und Fortschrittsanzeigen Viele Programme bieten ihre wichtigsten Funktionen über Symbolleisten (Toolbars, Werkzeugleisten) an. Eine Symbolleiste befindet sich meist unterhalb der Menüleiste und enthält Buttons mit Symbolen und andere Steuerelemente. Das Anklicken eines solchen Buttons hat meist denselben Effekt wie die Auswahl einer Menüoption, ist aber einen oder mehrere Mausklicks schneller. Oft können Symbolleisten während der Laufzeit konfiguriert und angeordnet werden.
In Visual Studio stehen für Symbolleisten vor allem die Komponenten ToolStrip und ToolStripContainer zur Verfügung. Die schon in älteren Versionen verfügbare Komponente ToolBar steht aus Kompatibilitätsgründen ebenfalls noch zur Verfügung. Sie wird aber in der Toolbox nicht mehr angeboten und sollte bei neuen Projekten nicht mehr verwendet werden.
10.3.1 Symbolleisten mit Panels und Buttons Auf den ersten Blick könnte man auf die Idee kommen, eine Symbolleiste mit einem Panel zu konstruieren, auf das mehrere Buttons gesetzt werden. Diese Möglichkeit bestand schon in frühen Versionen von Windows. Sie hat aber gegenüber den im Folgenden vorgestellten Komponenten ToolStrip und ToolStripContainer zahlreiche Schwächen und sollte nicht verwendet werden.
10.3.2 Status- und Fortschrittsanzeigen Eine ProgressBar wird vor allem bei längeren Operationen als Fortschrittsanzeige eingesetzt. Der Anwender sieht dann, dass das Programm noch aktiv ist (und nicht hängt) und kann aufgrund der Anzeige abschätzen, wie lange die Operation noch etwa dauert. Zur Fortschrittsanzeige stehen zwei Steuerelemente zur Verfügung, die weitgehend dieselben Eigenschaften und Methoden haben: Die Komponente ProgressBar (Toolbox Registerkarte „Allgemeine Steuerelemente“, Datentyp System::Windows::Forms::ProgressBar), die an jede Position eines Formulars gesetzt werden kann. – Eine meist besser geeignete ToolStripProgressBar, die in einem StatusStrip (siehe unten) oder in einem ToolStrip (Toolbox Registerkarte „Menüs und Symbolleisten“, siehe Abschnitt 10.3.3) enthalten ist. Eine ToolStripProgressBar unterscheidet sich von einer ProgressBar im Wesentlichen nur dadurch, dass sie immer in einem StatusStrip oder einem ToolStrip –
1122
10 Einige Elemente der .NET-Klassenbibliothek
enthalten ist und nicht an eine beliebige Position in einem Formular gesetzt werden kann. Beide haben im Wesentlichen die Eigenschaften
property int Minimum; // untere Grenze der Fortschrittsanzeige property int Maximum; // obere Grenze der Fortschrittsanzeige property int Value; // die aktuelle Position property int Step; // der Wert, um den Value durch PerformStep erhöht wird und Methoden
void Increment(int value); // erhöht die Eigenschaft Value um value void PerformStep(); //erhöht die Eigenschaft Value um Step Der Eigenschaft
property ProgressBarStyle Style kann man einen der Werte Blocks, Continuous und Marquee des C++/CLI-Aufzählungstyps ProgressBarStyle zuweisen. Mit den ersten beiden Werten wird der Fortschritt durch Blöcke oder eine durchlaufende Linie dargestellt. Der Wert Marquee zeigt einen Block, der wie eine Laufschrift durch die Anzeige läuft. Dieser Style wird vor allem dann verwendet, wenn man nicht weiß, wie weit eine Aktion fortgeschritten ist.
Die Komponente StatusStrip (Toolbox Registerkarte „Menüs und Symbolleisten“) ist eine Statusleiste, die normalerweise am unteren Rand eines Fensters Informationen anzeigt. Mit dem Dropdown-Menü kann man Statuslabel für die Anzeige von Text (Eigenschaft Text), Forschrittsanzeigen usw. auf die Statusleiste setzen:
Beispiel: Einem StatusLabel kann man folgendermaßen einen Text zuweisen: toolStripStatusLabel1->Text="Everything’s ok";
Weitere StatusLabel kann man im Eigenschaftenfenster durch einen Doppelklick auf die Eigenschaft Items oder mit den folgenden Anweisungen erzeugen:
10.3 Symbolleisten, Status- und Fortschrittsanzeigen
1123
ToolStripStatusLabel^ tSSLabel2 = gcnew ToolStripStatusLabel; tSSLabel2->Text="another StatusLabel"; this->statusStrip1->Items->Add(tSSLabel2);
10.3.3 Symbolleisten mit ToolStrip Für Symbolleisten steht die Komponente ToolStrip (Toolbox Registerkarte „Menüs & Symbolleisten“) zur Verfügung. Ein ToolStrip wird automatisch unterhalb der Menüleiste ausgerichtet und verfügt über eine integrierte Verwaltung seiner Elemente. Nachdem man einen ToolStrip auf das Formular gesetzt hat, wird unterhalb des Formulars ein Symbol toolStrip1 und unterhalb der Menüzeile des Formulars eine Symbolleiste angezeigt. Über das rechte Pulldown-Menü auf der Symbolleiste werden Buttons (Datentyp ToolStripButton), Label (Datentyp ToolStripLabel) usw. zum Einfügen angeboten:
Nachdem man so einige Komponenten auf die Symbolleiste gesetzt hat, sieht diese zur Laufzeit etwa folgendermaßen aus:
Über das Kontextmenü der Komponenten können diesen auch Bilder zugeordnet (Option „Bild festlegen“) und weitere Komponenten in der Mitte eingefügt werden (Option „Einfügen“):
1124
10 Einige Elemente der .NET-Klassenbibliothek
Oft soll beim Anklicken eines ToolStripButtons eine Ereignisbehandlungsroutine ausgeführt werden, die schon zuvor für eine Menüoption definiert wurde. Diese kann dann im Eigenschaftenfenster nach dem Anklicken des Pulldown-Menüs in der rechten Spalte direkt ausgewählt werden:
10.3.4 ToolStripContainer Ein ToolStripContainer (Toolbox Registerkarte „Menüs & Symbolleisten“) besteht aus einem ToolStripPanel an jeder der vier Seiten des Formulars, und einem ToolStripContentPanel in der Mitte. Auf ein ToolStripPanel kann man einen oder mehrere ToolStrip, MenuStrip oder StatusStrip setzen, und auf ein ToolStripContentPanel beliebige andere Komponenten. Ein ToolStripContainer wird normalerweise als erste Komponente auf das Formular gesetzt. In dem dabei angezeigten Dialog gibt man unter „Bereichsichtbarkeit“ die sichtbaren Bereiche an. Damit man die Bedeutung dieser Optionen später sieht, sollen hier zur Illustration zunächst alle außer „Rechts“ markiert werden. Diese Einstellungen können auch noch später im Eigenschaftenfenster über BottomToolStripPanelVisible, BottomToolStripPanelVisible usw. geändert werden. Dann klickt man auf „Ausfüllformular andocken“:
10.3 Symbolleisten, Status- und Fortschrittsanzeigen
1125
So erhält man diese Darstellung, bei der man die Panels (außer dem rechten) durch Anklicken der Laschen aus- und einklappen kann:
Auf die ToolStripPanel am Rand kann man aus der Toolbox die Komponenten ToolStrip, MenuStrip oder StatusStrip setzen. Nachdem man auf das TopToolStripPanel einen ToolStrip (z.B. mit jeweils zwei ToolStripButtons und zwei ToolStripLabels) und einen MenuStrip gesetzt hat, kann man diese zur Laufzeit mit der Maus beliebig in ihrem ToolStripPanel sowie zwischen all den ToolStripPanels verschieben, deren Bereichsichtbarkeit nicht deaktiviert ist:
Hätte man oben die Bereichsichtbarkeit „Rechts“ ebenfalls aktiviert, könnte man den ToolStrip auch noch am rechten Rand andocken. Da sich die Elemente bei einer solchen Konstellation gegenseitig verdecken (der ToolStripContainer verdeckt das Formular, und der MenuStrip und der ToolStrip
1126
10 Einige Elemente der .NET-Klassenbibliothek
verdecken das obere Panel), kann man die darunter liegenden nicht mit der Maus anklicken, um sie zur Bearbeitung (z.B. im Eigenschaftenfenster) auszuwählen. Dann ist das Dokumentgliederung (Ansicht|Weitere Fenster) nützlich, die die Komponenten eines Formulars in ihrer hierarchischen Ordnung zeigt. Für das in diesem Abschnitt beschriebene Beispiel sieht es folgendermaßen aus:
10.3.5 ToolStripLabel und LinkLabel Ein (über das Kontextmenü eines Toolstrip erzeugtes) ToolStripLabel kann wie ein gewöhnliches Label Text und Bilder anzeigen. Es kann außerdem mit dem Wert true der Eigenschaft
property bool IsLink so eingestellt werden, dass es sich wie ein Hyperlink in einem HTML-Dokument verhält und seine Aufschrift in einer anderen Farbe
property Color VisitedLinkColor dargestellt wird, nachdem es angeklickt wurde. Falls das ToolStripLabel wirklich einen Link darstellen soll, kann man diesen der Eigenschaft
property Object^ Tag zuweisen. Während ein gewöhnliches Label meist nur Text und Bilder anzeigt und beim Anklicken keine Aktionen ausführt, reagiert ein ToolStripLabel oft wie ein Button auf Mausklicks. Ein LinkLabel bietet ähnliche Möglichkeiten. Beispiel: Nach den Initialisierungen toolStripLabel->IsLink=true; toolStripLabel->Tag="www.rkaiser.de";
10.3 Symbolleisten, Status- und Fortschrittsanzeigen
1127
wird beim Anklicken des ToolStripLabel die Funktion System::Void toolStripLabel1_Click(System::Object^ sender, System::EventArgs^ e) { using namespace System::Diagnostics; Process::Start("IEXPLORE.EXE", toolStripLabel1->Tag->ToString()); toolStripLabel1->LinkVisited=true; }
aufgerufen, die den Internet-Explorer startet.
10.3.6 NotifyIcon: Elemente im Infobereich der Taskleiste Die Taskleiste von Windows ist eine Symbolleiste von Windows. Normalerweise besteht keine Veranlassung, dem Start-Bereich, der Schnellstartleiste oder dem Bereich der Programmsymbole Elemente hinzuzufügen, da die Elemente dieser drei Bereiche von Windows verwaltet werden. Im letzten Bereich (dem sogenannten Infobereich, oft auch als „system tray“ bezeichnet) werden Symbole für Programme angezeigt, die typischerweise im Hintergrund laufen und keine eigene Benutzeroberfläche haben. Setzt man ein NotifyIcon (Toolbox Registerkarte „Allgemeine Steuerelemente“) auf ein Formular und seine Eigenschaft Visible auf true, wird das der Eigenschaft Icon zugewiesene Symbol während der Ausführung des Programms im Infobereich angezeigt. Fährt man mit der Maus über dieses Symbol, wird der Wert der Eigenschaft Text angezeigt. Über die Eigenschaft ContextMenuStrip kann ein Kontextmenü zugeordnet werden, das beim Anklicken des Symbols im Infobereich mit der rechten Maustaste angezeigt wird. Beispiel: Weist man der Eigenschaft ContextMenuStrip des NotifyIcons ein Kontextmenü mit den folgenden Menüoptionen zu, kann man die Anwendung über das Kontextmenü minimieren oder in Normalgröße anzeigen: private: System::Void minToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ e){ WindowState=FormWindowState::Minimized; // zeigt } // die Anwendung in einem minimierten Fenster an private: System::Void normToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ e) { WindowState=FormWindowState::Normal; // zeigt die } // Anwendung in einem Fenster in Normalgröße an
Weist man die Eigenschaft BalloonTipText einen Text zu, wird durch einen Aufruf der Methode
void ShowBalloonTip(int timeout)
1128
10 Einige Elemente der .NET-Klassenbibliothek
eine Sprechblase mit diesem Text angezeigt. Die Dauer der Anzeige ergibt sich aus dem Wert des Arguments.
Aufgabe 10.3 Schreiben Sie einen einfachen Editor für Texte im rtf- und Text-Format. Legen Sie dazu ein neues Projekt (z.B. mit dem Namen SimpleEd) an und fügen Sie dem Formular einen ToolStripContainer mit den unten beschriebenen Menüoptionen hinzu. Einige dieser Optionen sollen auch über einen ToolStrip im ToolStripContainer verfügbar sein. Dem ToolStrip werden diese Optionen am einfachsten über „Standardelemente hinzufügen“ hinzugefügt. Setzen Sie auf das ToolStripContentPanel eine RichTextBox (siehe Abschnitt 10.1), die an die Größe des Formulars angepasst wird (mit dem Wert Fill der Eigenschaft Dock). Das Menü soll die folgenden Optionen enthalten: – Datei|Öffnen (rtf-Datei): Ein OpenFileDialog soll Dateien mit der Endung „*.rtf“ anzeigen und die ausgewählte Datei im RTF-Format in die RichTextBox laden. Um diese Funktion zu testen, können Sie mit Word eine Datei im rtfFormat anlegen, indem Sie bei Datei|Speichern unter als Dateityp „Rich Text Format (*.rtf)“ wählen. – Datei|Öffnen: Ein OpenFileDialog soll alle Dateien anzeigen und die ausgewählte Datei im Text-Format in die RichTextBox laden. – Datei|Speichern: Der Inhalt der RichTextBox soll unter dem Namen und in dem Format als Datei gespeichert werden, unter dem sie geöffnet wurde. – Datei|Speichern unter: Der Inhalt der RichTextBox soll unter dem Namen als Datei gespeichert werden, der in einem SaveFileDialog ausgewählt wurde. – Datei|Beenden: Beendet das Programm. Wenn die RichTextBox seit dem letzten Speichern verändert wurde (siehe die Eigenschaft Modified), soll beim Schließen des Formulars (Ereignis FormClosing, siehe Abschnitt 2.6) mit einer MessageBox (siehe Abschnitt 2.11) gefragt werden, ob das Programm ohne Speichern verlassen werden soll. – Bearbeiten|Rückgängig: Macht die letzten Änderungen mit Undo wieder rückgängig. – Bearbeiten|Wiederholen: Macht die letzten Undo-Operationen wieder rückgängig. – Format|Schriftart: In einem FontDialog wird eine Schriftart ausgewählt (Eigenschaft Font) und dem markierten Bereich zugewiesen. – Format|Farbe: In einem ColorDialog wird eine Farbe (Eigenschaft Color) ausgewählt und dem markierten Bereich zugewiesen.
10.4 Größenänderung von Steuerelementen zur Laufzeit
1129
10.4 Größenänderung von Steuerelementen zur Laufzeit Die einfachste Möglichkeit, die Größe eines Steuerelements an die Größe eines umgebenden Containers (z.B. Formulars) anzupassen, besteht über die schon in Abschnitt 2.3 vorgestellte Eigenschaft Dock. Im Folgenden werden einige Eigenschaften und Komponenten vorgestellt, die flexiblere Möglichkeiten bieten.
10.4.1 Die Eigenschaften Dock und Anchor Die Eigenschaft Dock kann Werte des CLI-Aufzählungstyps
enum class DockStyle {None, Top, Bottom, Left, Right, Fill }; annehmen. Setzt man einen von None verschiedenen Wert, wird die Größe bei einer Größenänderung des umgebenden Containers folgendermaßen angepasst:
Top, Bottom: am oberen bzw. unteren Rand, die Höhe wird nicht verändert Left, Right: am linken bzw. rechten Rand, die Breite wird nicht verändert Fill: an allen vier Rändern, die Höhe und Breite werden angepasst. Bei der Verwendung der Eigenschaft Dock haben die jeweiligen Ränder der ausgerichteten Komponente immer den Abstand Null vom Rand der umgebenden Komponente. Falls man einen anderen Abstand vom Rand der umgebenden Komponente haben will, kann man die Eigenschaft Anchor verwenden. Diese Eigenschaft ist eine bitweise Kombination von Werten des CLI-Aufzählungstyps
enum class AnchorStyles{ Bottom, Left, Top, Right, None }; Diese Werte bedeuten, dass der entsprechende Rand des Steuerelements am entsprechenden Rand der umgebenden Komponente verankert wird. Wenn z.B. für einen Button die Werte Left und Right im Eigenschaftenfenster oder mit einer Anweisung wie button1->Anchor=AnchorStyles::Left|AnchorStyles::Right;
gesetzt sind, wird die Größe des Buttons bei einer Größenänderung der umgebenden Komponente so verändert, dass der Abstand zum linken und rechten Rand gleich bleibt.
10.4.2 SplitContainer: Zur Größenanpassung von zwei Panels Während man mit den Eigenschaften Dock und Anchor die Größe eines Steuerelements an die Größe eines Formulars anpassen kann, ermöglicht ein SplitContainer die Aufteilung eines Bereichs auf zwei Teilbereiche. Der Windows Explorer ist ein Beispiel mit einem TreeView links und einem ListView rechts.
1130
10 Einige Elemente der .NET-Klassenbibliothek
Ein SplitContainer besteht aus zwei Panels Panel1 und Panel2 des Typs SplitterPanel, auf die man wie auf ein gewöhnliches Panel andere Komponenten setzen kann. Die beiden Panels sind durch eine Leiste getrennt, an der man mit der Maus ziehen und so die Breite der beiden Panels verändern kann. Frühere Versionen von Visual Studio enthielten eine ähnliche Komponente mit dem Namen Splitter. Normalerweise setzt man die Eigenschaft Dock (siehe Abschnitt 10.4.1) eines SplitContainer auf den Wert Fill, damit er das ganze Formular auch bei einer Größenänderung des Formulars füllt. Außerdem setzt man diese Eigenschaft meist auch bei den auf die Panels gesetzten Komponenten, damit sie das gesamte Panel ausfüllen. Beispiel: Das abgebildete Formular (siehe Aufgabe 10.5.4, 2.) enthält einen SplitContainer mit einem TreeView auf dem linken und einem ListView auf dem rechten Panel, ähnlich wie der Windows Explorer. Die Eigenschaft Dock hat bei allen drei Komponenten den Wert Fill.
10.4.3 TableLayoutPanel: Tabellen mit Steuerelementen Ԧ Ein TableLayoutPanel (Toolbox Registerkarte „Container“) ist eine Tabelle, deren Zellen Steuerelemente enthalten. Die Größe der Steuerelemente wird dann zur Laufzeit an die Größe des Formulars angepasst. Die Einzelheiten der Darstellung können über eine Vielzahl von Eigenschaften gesteuert werden. Das erste Steuerelement, das auf das TableLayoutPanel gesetzt wird, kommt automatisch in die erste Spalte der ersten Zeile, das zweite in die nächste usw. In der Praxis wird diese Komponente aber nicht allzu oft eingesetzt. Durch einen Doppelklick auf die rechte Spalte der TableLayoutPanel-Eigenschaften Columns bzw. Rows im Eigenschaftenfenster wird ein Dialogfenster angezeigt, über das man Zeilen oder Spalten editieren kann.
10.4 Größenänderung von Steuerelementen zur Laufzeit
1131
Beispiel: Das unten abgebildete TableLayoutPanel besteht aus zwei Zeilen und drei Spalten. In jede Zelle der ersten Zeile wurde ein Label gesetzt, und in die Zellen der zweiten Zeile eine mehrzeilige TextBox (links), ein Panel (in der Mitte, mit zwei Buttons) und eine Listbox (rechts). Nach der Ausführung der folgenden Anweisungen tableLayoutPanel1->Dock=DockStyle::Fill; panel1->Dock=DockStyle::Fill; textBox1->Dock=DockStyle::Fill; listBox1->Dock=DockStyle::Fill; tableLayoutPanel1->RowStyles[0]->SizeType= SizeType::Absolute; tableLayoutPanel1->RowStyles[0]->Height=20; tableLayoutPanel1->RowStyles[1]->SizeType= SizeType::Percent; tableLayoutPanel1->RowStyles[1]->Height=100; tableLayoutPanel1->ColumnStyles[0]->SizeType= SizeType::Percent; tableLayoutPanel1->ColumnStyles[0]->Width=50; tableLayoutPanel1->ColumnStyles[1]->SizeType= SizeType::Absolute; tableLayoutPanel1->ColumnStyles[1]->Width=60; tableLayoutPanel1->ColumnStyles[2]->SizeType= SizeType::Percent; tableLayoutPanel1->ColumnStyles[2]->Width=50;
passen sich die Elemente des TableLayoutPanel, deren Eigenschaft SizeType den Wert SizeType::Percent hat, bei einer Größenänderung des Formulars an dessen Größe an:
1132
10 Einige Elemente der .NET-Klassenbibliothek
Mehrere Zellen aus einer Zeile oder Spalte kann man mit den Methoden
void SetColumnSpan(Control^ control, int value) void SetRowSpan(Control^ control, int value) zu einer einzigen zusammenfassen.
10.4.4 Automatisch angeordnete Steuerelemente: FlowLayoutPanel Ԧ Ein FlowLayoutPanel ist eine Container-Komponente, die ihre Elemente zur Laufzeit automatisch anordnet. Bei einer Änderung der Größe des Formulars kann die Position der Elemente neu angeordnet werden. Ein solches Formular hat Ähnlichkeiten mit einem HTML-Formular, bei dem eine Veränderung der Größe zu einer anderen Anordnung der Komponenten führen kann. In der Praxis wird diese Komponente aber nicht allzu oft eingesetzt. Ein FlowLayoutPanel (Toolbox Registerkarte „Container“) hat viele Gemeinsamkeiten mit einem gewöhnlichen Panel (siehe Abschnitt 2.8). Es unterscheidet sich im Wesentlichen nur durch die Position der darauf gesetzten Komponenten. Diese Position kann bei einem gewöhnlichen Panel frei gewählt werden. Bei einem FlowLayoutPanel werden die Komponenten dagegen automatisch angeordnet. Die Art der Anordnung ergibt sich aus dem Werte der Eigenschaft
property FlowDirection FlowDirection die die Werte BottomUp, LeftToRight, RightToLeft und TopDown annehmen kann. Beispiel: Die nächsten beiden Abbildungen zeigen dasselbe Formular mit zwei Buttons und einer TextBox auf einem FlowLayoutPanel. Die Breite des Panels ist über die die Eigenschaft Dock an das Formular gekoppelt, und die Eigenschaft AutoSize auf true gesetzt. Wenn man das Formular schmäler macht, ordnet das Panel die Buttons auf zwei Zeilen an:
10.5 ImageList, ListView und TreeView
1133
10.5 ImageList, ListView und TreeView Die Komponenten ListView und TreeView stellen Listen und Baumstrukturen zusammen mit Symbolen aus einer ImageList dar. Sie bieten die Möglichkeit, umfangreiche Informationen übersichtlich darzustellen. Der Windows Explorer verwendet solche Komponenten zur Darstellung von Verzeichnisbäumen und Dateien. In Programmen für E-Mails werden sie ebenfalls oft verwendet. Die Klassen ListView und TreeView besitzen eine große Anzahl von Elementen, so dass man leicht den Überblick verlieren kann. Die folgenden Ausführungen zeigen aber, dass bereits wenige Elemente für eine Darstellung ausreichen, die im Wesentlichen dem Windows Explorer entspricht.
10.5.1 Die Verwaltung von Bildern mit einer ImageList Eine ImageList (Toolbox Registerkarte „Komponenten“) verwendet man zur Speicherung von Bildern, die von anderen Komponenten (TreeView, ListView, TabControl, Button, CheckBox, RadioButton, Label usw.) angezeigt werden. Sie wird zur Laufzeit nicht auf dem Formular angezeigt. Durch einen Klick auf das Symbol schaftenfenster wird der Editor für
bei der Eigenschaft Images im Eigendie Bilderliste aufgerufen:
Mit dem Button „Hinzufügen“ kann man Bilder in die ImageList laden. Zahlreiche unter Windows gebräuchliche Bilder findet man in „c:\Programme\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary“. Jedes Verzeichnis enthält eine Readme-Datei mit einer Beschreibung der Grafiken.
1134
10 Einige Elemente der .NET-Klassenbibliothek
Die Zuordnung der ImageList zu einer Komponente erfolgt dann über die Eigenschaft ImageList der Komponente (am einfachsten im Pulldown-Menü im Eigenschaftenfenster auswählen). Den einzelnen Elementen werden die Bilder aus der Bilderliste über die Eigenschaft ImageIndex (die Nummer des Bildes) zugeordnet. Diese können ebenfalls im Eigenschaftenfenster über ein Pulldown-Menü ausgewählt werden.
10.5.2 Die Anzeige von Listen mit ListView Ein ListView (Toolbox Registerkarte „Allgemeine Steuerelemente“) zeigt wie die Dateiansicht des Windows-Explorers eine Liste von Strings zusammen mit Icons an. Wie beim Windows-Explorer kann man zwischen verschiedenen Ansichten (Ansicht|Liste, Ansicht|Details usw.) umschalten. Diese Ansicht ergibt sich aus dem Wert der Eigenschaft View, die anschließend beschrieben wird. Die Elemente des ListView sind in der Collection-Klasse
property ListViewItemCollection^ Items enthalten und haben den Datentyp ListViewItem. Diese Collection-Klasse hat zahlreiche Methoden zum Einfügen und Löschen von Einträgen, wie z.B.:
virtual ListViewItem^ Add(String^ text) Der Rückgabewert von Add zeigt auf das erzeugte Element. Über diesen Rückgabewert kann man die Eigenschaften und Methoden eines ListViewItem ansprechen, wie z.B.
property String^ Text // der im ListView angezeigte Text des Eintrags property int ImageIndex // Index der zugehörigen ImageList Beispiel: Mit den folgenden Anweisungen erhält man ein ListView mit den Einträgen „a“, „b“ und „c“: listView1->Items->Add("a"); listView1->Items->Add("b"); ListViewItem^ p=listView1->Items->Add(""); p->Text="c";
Die verschiedenen Ansichten dieses ListView werden später im Zusammenhang mit der Eigenschaft View gezeigt. Die Elemente des ListView kann man auch über die Eigenschaft Items und ihren Index ansprechen (Items[0] , Items[1] usw.). Die Anzahl der Elemente ist der Wert der Eigenschaft Items->Count.
10.5 ImageList, ListView und TreeView
1135
Beispiel: Mit den folgenden Anweisungen erhält man dasselbe ListView wie im letzten Beispiel: listView1->Items->Add(""); // erzeugt Items[0] listView1->Items->Add(""); // erzeugt Items[1] listView1->Items->Add(""); // erzeugt Items[2] listView1->Items[0]->Text="a"; listView1->Items[1]->Text="b"; listView1->Items[2]->Text="c";
Ein ListView verwendet man meist zusammen mit zwei ImageList Komponenten (siehe Abschnitt 10.5.1), die man zunächst auf das Formular setzt und dann im Eigenschaftenfenster den Eigenschaften LargeImageList und SmallImageList zuweist. Die Bilder aus den Bilderlisten werden dann über ihren Index der Eigenschaft ImageIndex des jeweiligen Listeneintrags zugeordnet.
Für die verschiedenen Werte der Eigenschaft View wird ein ListView (mit entsprechenden Bilderlisten und den Einträgen von oben) zur Laufzeit folgendermaßen dargestellt: – Wenn View den Wert View::LargeIcon hat, werden die Bilder aus LargeImageList über dem jeweiligen Listeneintrag angezeigt. Normalerweise wählt man für diese Darstellung größere Icons als in der Abbildung rechts. Im Windows-Explorer entspricht dies der Darstellung mit Ansicht|Miniaturansicht. – Mit dem Wert View::SmallIcon bzw. View::List werden die Bilder aus SmallImageList links vom jeweiligen Listeneintrag angezeigt. Im Windows-Explorer entspricht dies der Darstellung mit Ansicht|Kleine Symbole bzw. Ansicht|Liste. – Setzt man View auf View::Details, werden außer den Listeneinträgen auch noch Untereinträge und Spaltenüberschriften angezeigt. Das sind die Elemente der Collection-Klassen
property ColumnHeaderCollection^ Columns // Liste der Spaltenüberschriften property ListViewSubItemCollection^ SubItems // Liste der Untereinträge denen man mit der Methode Add Einträge hinzufügen kann. Beispiel: Ergänzt man das erste Beispiel von oben um die Anweisungen
1136
10 Einige Elemente der .NET-Klassenbibliothek
listView1->Columns->Add("H1"); listView1->Columns->Add("H2"); listView1->Columns->Add("H3");
erhält man die Spaltenüberschriften „H1“, „H2“ und „H3“. Mit p->SubItems->Add("c1");p->SubItems->Add("c2");
oder listView1->Items[2]->SubItems->Add("c1"); listView1->Items[2]->SubItems->Add("c2");
oder p->SubItems->AddRange(gcnew array {"c1","c2"});
erhält man die Untereinträge „c1“ und „c2“ zum Eintrag „c“. Mit diesen Werten erhält man eine Darstellung wie die rechts abgebildete, die Ansicht|Details im Windows-Explorer entspricht. Die Spaltenüberschriften sind für die Anzeige der Untereinträge notwendig: Ohne Einträge in Column werden auch keine Untereinträge angezeigt. Die Aktualisierung der grafischen Darstellung eines ListView ist mit einem gewissen Zeitaufwand verbunden. Diese Aktualisierung kann mit der ListView-Methode BeginUpdate unterbunden und mit EndUpdate wieder aktiviert werden. Das Einfügen, Verändern, Löschen usw. einer größeren Anzahl von Elementen in einem ListView wird deutlich schneller, wenn man vorher BeginUpdate und danach EndUpdate aufruft. EndUpdate muss genauso oft aufgerufen werden wie BeginUpdate: Wird EndUpdate weniger oft aufgerufen, wird das ListView nicht mehr aktualisiert. Falls zwischen diesen beiden Funktionen eine Exception ausgelöst werden kann, muss mit try-finally sichergestellt werden, dass für jedes BeginUpdate auch EndUpdate aufgerufen wird. Ein kleiner Auszug der zahlreichen weiteren Eigenschaften von ListView:
– MultiSelect (Voreinstellung true) ermöglicht die gleichzeitige Markierung von mehreren Einträgen. – GridLines (Voreinstellung false) steuert die Anzeige von Gitterlinien. – Die Darstellung der Einträge kann frei gestaltet werden, indem man die Eigenschaft OwnerDraw auf true setzt und die Ereignisse DrawItem, DrawSubItem und DrawColumnHeader definiert. – Unter den Windows-Versionen XP und Server 2003 kann ein ListView auch die Werte Tile und
10.5 ImageList, ListView und TreeView
1137
Groups (siehe Abbildung rechts) für View verwenden. Diese werden unter älteren Versionen ignoriert. Meist wird ein ListView wie in den bisherigen Ausführungen zur Laufzeit aufgebaut. Man kann die Einträge aber auch zur Entwurfszeit festlegen. Durch einen bei der Eigenschaft Items im Eigenschaftenfenster wird Klick auf das Symbol der Editor für die Einträge im ListView aufgerufen:
Hier kann man die Eigenschaften der ListView-Einträge eingeben. Der Wert der Eigenschaft Text wird dann im ListView angezeigt. Hier kann man auch die Untereinträge eintragen:
1138
10 Einige Elemente der .NET-Klassenbibliothek
10.5.3 ListView nach Spalten sortieren Ein ListView soll seine Zeilen oft nach dem Anklicken einer Spaltenüberschrift nach den Werten in dieser Spalte zu sortieren. Das erreicht man, indem man nach using namespace System::Collections; // für IComparer
eine Klasse wie die folgende definiert: ref class ListViewItemComparer: public IComparer { private: int col; public: ListViewItemComparer(int column) { col = column; } virtual int Compare(Object^ x, Object^ y) { if (!(0Text; String^ t=(dynamic_cast(y)) ->SubItems[col]->Text; if (col==1) // vergleiche Kalenderdaten in Spalte 2 { DateTime t1=Convert::ToDateTime(s); DateTime t2=Convert::ToDateTime(t); if (t1t2) return -1; else return 0; } else if (col==2) // vergleiche Zahlen in Spalte 2 { int i1=Convert::ToInt32(s); int i2=Convert::ToInt32(t); if (i1i2) return -1; else return 0; } else // alle anderen Spalten als Strings vergleichen { return String::Compare(s,t); } } };
Diese Klasse muss eine Funktion Compare enthalten, die zwei Elemente derselben Spalte eines ListView vergleicht. Diese Funktion wird dann zum Sortieren des ListView verwendet. Ihr Rückgabewert muss
10.5 ImageList, ListView und TreeView
1139
0 sein, falls x bezüglich der Sortierfolge gleich ist wie y >0 sein, falls x bezüglich der Sortierfolge vor y kommt ListViewItemSorter= gcnew ListViewItemComparer(e->Column); }
Hier wird der Index der Spalte, nach der sortiert werden soll, als e->Column übergeben.
Aufgabe 10.5.3 In Abschnitt 9.5 wird gezeigt, dass die Funktion DirectoryInfo ein CLI-Array zurückgibt, das Informationen über alle Dateien des als Argument übergebenen Verzeichnisses enthält: DirectoryInfo^ di=gcnew DirectoryInfo(path);
Dieses Array kann man dann wie in der Schleife for each (FileInfo^ fi in di->GetFiles())
durchlaufen. a) Schreiben Sie eine Funktion
void showFilesOfDirectory(ListView^ lv,String^ path) die alle Dateien des als path übergebenen Verzeichnisses in das als Argument übergebene ListView schreibt. Dieses ListView soll drei Spalten mit der Aufschrift Name, Date und Size haben, in die die FileInfo-Eigenschaften Name, LastWriteTime und Length eingetragen werden. b) Beim Anklicken einer Spaltenüberschrift soll das ListView nach den Werten in dieser Spalte sortiert werden. c) Beim wiederholten Anklicken einer Spaltenüberschrift soll das ListView abwechselnd aufsteigend und absteigend sortiert werden.
1140
10 Einige Elemente der .NET-Klassenbibliothek
d) Schreiben Sie eine Funktion
void findFilesInSubdirs(ListView^ lv, String^ path, String^ mask) die (ähnlich wie die Lösung von Aufgabe 5.3.7, bzw. die Suchen-Funktion von Windows Start|Suchen|Nach Dateien und Ordnern) die Namen aller Dateien, die zu einem für mask übergebenen Muster passen (z.B. *.txt), in einem ListView anzeigt. Dabei sollen ausgehend vom Verzeichnis path alle Unterverzeichnisse durchsucht werden. Testen Sie diese Funktion zunächst mit einem hart kodierten Argument (z.B. „c:\“ oder „c:\test“) für path. e) Schreiben Sie eine Funktion ListViewToClipboard, die die Eigenschaft Text aller Items eines als Parameter übergebenen ListView in die Zwischenablage kopiert. Sie können dazu alle Text-Eigenschaften zu einem String s zusammenfügen und diesen String mit der Methode Clipboard::SetText(s) in die Zwischenablage kopieren.
10.5.4 Die Anzeige von Baumstrukturen mit TreeView Ein TreeView (Toolbox Registerkarte „Allgemeine Steuerelemente“) stellt eine Hierarchie von Knoten als Baumstruktur dar, bei der untergeordnete Baumstrukturen durch Anklicken der Knoten auf- und zugeklappt werden können. Bei einem TreeView kann jeder Knoten mehrere untergeordnete Knoten enthalten. Ein untergeordneter Knoten hat genau einen übergeordneten Knoten. Knoten auf derselben Ebene werden auch als Geschwisterknoten bezeichnet. Alle Knoten eines TreeView haben den Datentyp TreeNode. Dieser Datentyp hat z.B. die Eigenschaften
property String^ Text // der im TreeView angezeigte Text des Knotens property int ImageIndex // Index des mit dem Knoten dargestellten Bildes in der zugehörigen ImageList Die Knoten auf der obersten Ebene des TreeView sind die Elemente der Collection-Klasse
property TreeNodeCollection^ Nodes // eine Eigenschaft von TreeView Diese Knoten werden immer angezeigt, da sie keinen übergeordneten Knoten haben, den man auf- und zuklappen kann. Ein TreeNode hat ebenfalls eine Eigenschaft
property TreeNodeCollection^ Nodes // eine Eigenschaft von TreeNode
10.5 ImageList, ListView und TreeView
1141
mit den unmittelbar untergeordneten Knoten. Die untergeordneten Knoten eines TreeView werden nur dann angezeigt, wenn der übergeordnete Knoten nicht zugeklappt wird. Einer TreeNodeCollection kann mit der Methode
virtual TreeNode^ Add(String^ text); ein neuer Knoten mit dem als Argument übergebenen Text am Ende hinzugefügt werden. Der Rückgabewert zeigt auf den erzeugten Knoten. Über diesen Rückgabewert kann man die Eigenschaften und Methoden des TreeNode ansprechen. Beispiel: Die Funktion fillTV erzeugt den rechts abgebildeten TreeView mit den Knoten „a“, „b“ und „c“ auf der obersten Ebene sowie den dem Knoten „b“ untergeordneten Knoten „b1“, „b2“ und „b21“: void fillTV(TreeView^ tv) { // Erzeuge Knoten auf der obersten TreeView-Ebene: TreeNode^ n0=tv->Nodes->Add("a"); TreeNode^ n1=tv->Nodes->Add("b"); tv->Nodes->Add("c"); // erzeuge untergeordnete Knoten von n1: TreeNode^ n11=n1->Nodes->Add("b1"); TreeNode^ n12=n1->Nodes->Add("b2"); TreeNode^ n121=n12->Nodes->Add("b21"); }
Die Elemente von Nodes des Typs TreeNodeCollection können auch unter Nodes[0] , Nodes[1] usw. angesprochen werden. Beispiel: Ein TreeView treeView1 hat die Knoten (Geschwisterknoten)
treeView1->Nodes[0] , treeView1->Nodes[1] usw. Ein TreeNode N hat die untergeordneten Knoten
N->Nodes[0] , N->Nodes[1] usw. Den aktuell ausgewählten und den obersten Knoten des TreeView erhält man über die TreeView-Eigenschaften
property TreeNode^ SelectedNode property TreeNode^ TopNode Mit den folgenden TreeNode-Eigenschaften kann man benachbarte Knoten finden:
property TreeNode^ FirstNode // den ersten untergeordneten Knoten property TreeNode^ NextNode // der nächste Knoten (Geschwisterknoten)
1142
10 Einige Elemente der .NET-Klassenbibliothek
property TreeNode^ PrevNode // der Geschwisterknoten davor property TreeNode^ Parent // der übergeordnete Knoten Alle diese Eigenschaften haben den Wert nullptr, falls es keinen entsprechenden Knoten gibt. Für die Eigenschaft TopNode gilt das z.B. bei einem leeren TreeView. Beispiel: Ruft man die Funktion void
SearchSubdirs(TreeView^ tv, TreeNode^ String^ path, bool TopLevel=false)
tn,
{ if (tv->TopNode==nullptr||tn==nullptr||TopLevel) tn=tv->Nodes->Add(path); DirectoryInfo^ d=gcnew DirectoryInfo(path); for each (DirectoryInfo^ i in d->GetDirectories()) { TreeNode^ n=tn->Nodes->Add(i->Name); try{//falls kein Zugriffsrecht auf Verzeichnis SearchSubdirs(tv, n, path+i->Name+"\\"); } catch(Exception^ e) { // oder ohne Meldung ignorieren MessageBox::Show(e->Message); } } }
mit Argumenten auf, bei denen die Bedingung der if-Anweisung erfüllt ist, wird das Argument für path auf der obersten Ebene des TreeViewArguments eingetragen. Die Unterverzeichnisse von path werden als untergeordnete Knoten eingetragen (wie im Windows-Explorer): SearchSubdirs(treeView1,treeView1->TopNode,"c:\\", true);
Weist man der Eigenschaft Images des TreeView eine ImageList zu, werden die Bilder aus der Imagelist links von den Einträgen angezeigt. Die Zuordnung der Bilder zu den Einträgen erfolgt auch hier über die Eigenschaft ImageIndex. Wie bei einem ListView kann der Zeitaufwand für das Einfügen von Elementen in ein TreeView reduziert werden, wenn man die Aktualisierung vorher mit den TreeView-Methoden BeginUpdate unterbindet und erst anschließend wieder mit EndUpdate ermöglicht. Ein TreeView wird oft mit einem ListView gekoppelt (wie z.B. im Windows Explorer): Beim Anklicken eines Knotens im TreeView (Ereignis NodeMouseClick) werden dann weitere Daten zu diesem Knoten im ListView angezeigt.
10.5 ImageList, ListView und TreeView
1143
Beim Anklicken eines Knotens tritt das Ereignis NodeMouseClick ein. Der zugehörigen Ereignisbehandlungsroutine
void TreeNodeMouseClickEventHandler ( Object^ sender, TreeNodeMouseClickEventArgs^ e) wird der angeklickte Knoten als Element Node des Parameters e übergeben. Beispiel: Die folgende Ereignisbehandlungsroutine gibt den Text des angeklickten Knotens in einer TextBox aus: private: System::Void treeView1_NodeMouseClick( System::Object^ sender, System::Windows::Forms:: TreeNodeMouseClickEventArgs^ e) { textBox1->AppendText(e->Node->Text)) }
Die TreeNode-Eigenschaft
property String^ FullPath besteht aus dem String, in dem die Text-Eigenschaften aller Knoten vom obersten bis zum aktuellen Knoten aneinandergefügt sind. Dabei werden die einzelnen Strings durch den Wert der Eigenschaft
property String^ PathSeparator getrennt. Diese hat in der Voreinstellung den Wert „\“ ("\\" in C++). Damit entspricht FullPath für einen obersten Knoten mit einem Laufwerksnamen (z.B. „C:\“) und untergeordneten Knoten mit Verzeichnisnamen dem vollständigen Pfad. Beispiel: Wenn die Text-Eigenschaft des obersten Knotens eines TreeView den Wert „c:“ hat, die eines untergeordneten Knotens „Windows“ und die eines dazu untergeordneten Knotens „Help“, hat FullPath den Wert c:\Windows\Help
Manchmal will man in einem TreeNode Knoten zusätzliche Daten speichern, die nicht angezeigt werden sollen. Das ist mit der Eigenschaft
property Object^ Tag möglich, der ein Zeiger auf einen beliebigen von Object abgeleiteten Typ zugewiesen werden. Beim Zugriff auf die Daten muss man diese in den Typ der gespeicherten Daten konvertieren. Beispiel: Weist man der Eigenschaft Tag eines Knotens n einen Zeiger auf einen String zu
1144
10 Einige Elemente der .NET-Klassenbibliothek
n->Tag=path;
kann man diesen beim Zugriff auf die Daten eines Knotens folgendermaßen in einen String konvertieren: String^ s=safe_cast(e->Node->Tag);
Normalerweise wird ein TreeView zur Laufzeit aufgebaut. Die Einträge (Knoten) können aber auch zur Entwurfszeit nach einem Doppelklick auf Eigenschaft Nodes im Eigenschaftenfenster erzeugt werden:
bei der
Aufgabe 10.5.4 1. Schreiben Sie eine Funktion
void makeTreeView(TreeView^ tv, bool TopLevel) die in ein als Argument übergebenes TreeView 3 Knoten mit der Aufschrift „0“, „1“ und „2“ einhängt. Jeder dieser Knoten soll 4 untergeordnete Knoten haben, deren Aufschrift „i-j“ sich aus der Nummer des übergeordneten und der des aktuellen Knotens zusammensetzt. Jeder dieser Knoten soll 2 untergeordnete Knoten mit der entsprechenden Aufschrift „i-j-k“ haben. a) Beim Aufruf dieser Funktion mit dem Argument true für TopLevel sollen die Einträge mit der Aufschrift „0“, „1“ und „2“ wie in der Abbildung oben unterhalb eines obersten Knotens mit der Aufschrift „TopLevel“ eingefügt werden. b) Beim Aufruf dieser Funktion mit dem Argument false für TopLevel sollen die Knoten mit der Aufschrift „0“, „1“ und
10.5 ImageList, ListView und TreeView
1145
„2“ auf der obersten Ebene des TreeView wie in der Abbildung rechts eingefügt werden. 2. Nehmen Sie die folgenden Funktionen in ein Projekt (z.B. mit dem Namen DirTree) auf. Das Formular soll in der obersten Zeile einen ToolStrip (siehe Abschnitt 10.3.3) enthalten. Ein TreeView und ein ListView sollen über einen SplitContainer (siehe Abschnitt 10.4.2) gemeinsam das ganze Formular ausfüllen. Zum rekursiven Durchsuchen der Verzeichnisse eines Laufwerks können Sie sich an den Abschnitten 5.3.7 (Rekursion) und 9.5 (CLI-Arrays) orientieren. a) Schreiben Sie eine Funktion showSubdirsInTreeView, die die Namen aller Verzeichnisse und Unterverzeichnisse eines als Parameter übergebenen Verzeichnisses wie im Windows-Explorer in einen als Parameter übergebenen TreeView einhängt:
Dabei kann wie in der Abbildung auf Symbole usw. verzichtet werden. Falls Sie die Aufgabe 5.3.7 gelöst haben, können Sie die Funktion SearchSubdirs aus der Lösung dieser Aufgabe überarbeiten. Testen Sie diese Funktion mit einigen Verzeichnissen, die Sie hart kodiert in den Quelltext eingeben. b) Erweitern Sie das Formular von Aufgabe a) um ein ListView, in dem beim Anklicken eines Verzeichnisses im TreeView alle Dateien dieses Verzeichnisses angezeigt werden. Sie können dazu die Funktion showFilesOfDirectory von Aufgabe 10.5.3 verwenden:
1146
10 Einige Elemente der .NET-Klassenbibliothek
c) Überarbeiten Sie die Lösung der Aufgabe a) so, dass im TreeView neben dem Namen des Verzeichnisses die Anzahl der in allen Unterverzeichnissen belegten Bytes angezeigt wird:
d) Nehmen Sie einen Aufruf der Funktion findFilesInSubdirs von Aufgabe 10.5.3 in dieses Programm auf, so dass die Namen aller Dateien, die zu einem in eine ToolStripTextBox eingegebenen Muster passen (z.B. *a*.txt), ähnlich wie die Suchen-Funktion von Windows (Start|Suchen|Nach Dateien und Ordnern) in einem ListView angezeigt werden:
e) Erweitern Sie das ListView um ein Kontextmenü mit den Optionen „Löschen“ und „In Zwischenablage kopieren“. Bei der Auswahl dieser Optionen soll das ListView gelöscht bzw. durch den Aufruf der Funktion ListViewToClipboard von Aufgabe 10.5.3 in die Zwischenablage kopiert werden.
10.6 Die Erweiterung der Toolbox Mit Extras|Toolboxelemente auswählen bzw. der Option Elemente auswählen im Kontextmenü der Toolbox kann man der Toolbox Elemente hinzufügen und Elemente aus ihr entfernen. Dazu muss man nur im Dialog „Toolboxelemente auswählen“ die CheckBox in der ersten Spalte markieren bzw. die Markierung aufheben:
10.6 Die Erweiterung der Toolbox
1147
Eine so in die Toolbox aufgenommene Komponente kann dann wie eine vorinstallierte Komponente auf ein Formular gezogen werden. Die Eigenschaften der Komponente können im Eigenschaftenfenster oder im Programm festgelegt werden. Für selbstdefinierte .NET-Komponenten wurde diese Vorgehensweise schon in Abschnitt 9.15 beschrieben. Im Register COM-Steuerelemente werden registrierte ActiveX-Steuerelemente angezeigt. Beim Start des Programms muss dann die zugehörige DLL verfügbar sein. Das erreicht man am einfachsten, indem man das Verzeichnis mit der DLL in den Suchpfad aufnimmt oder die DLL in das Debug- oder Release-Verzeichnis des Projekts kopiert. Beispiele: – Nachdem man eine in die Toolbox übernommene Acrobat-Komponente auf ein Formular gesetzt hat, kann man ihrer Eigenschaft src den Namen einer pdfDatei zuweisen. Dadurch wird diese Datei wie im Acrobat-Reader angezeigt. private: System::Void button1_Click( System::Object^ sender, System::EventArgs^ { axAcroPDF1->src="d:\\pdf\\AufgLsg.pdf"; }
e)
Setzt man dabei die Eigenschaft Dock auf Fill, füllt der Acrobat Reader das gesamte Formular aus. – Das COM-Steuerelement „Microsoft Office Spreadsheet“ stellt eine Tabelle dar. Damit kann man Tabellen im XML, HTML und CSV-Format (z.B. eine in
1148
10 Einige Elemente der .NET-Klassenbibliothek
diesem Format gespeicherte Excel-Datei) laden, anzeigen und bearbeiten. Beim Start des Programms muss die zugehörige DLL (z.B. „AxInterop.OWC11.1.0.dll“) verfügbar sein. Durch eine der folgenden Anweisungen wird eine als XML bzw. HTML-Datei gespeichert Excel-Tabelle angezeigt axSpreadsheet1->XMLURL="c:\\Mappe3.xml"; axSpreadsheet1->HTMLURL="c:\\Mappe3.htm";
– Das COM-Steuerelement „Microsoft Office Chart“ ermöglicht die Darstellung von Werten als Säulen-, Balkendiagramm usw.
Aufgabe 10.6 Nehmen Sie a) die Windows Media Player COM-Komponente und b) die DriveListBox, DirListBox und und FileListBox .NET Komponente in die Toolbox auf und verwenden Sie beide in einem Projekt. Beim Anklicken eines Buttons soll der Eigenschaft URL des Media Players eine Medien-Datei (z.B. mit der Endung .avi) zugewiesen werden, die dann abgespielt wird.
10.7 MDI-Programme Ein MDI-Programm (Multiple Document Interface) besteht aus einem übergeordneten Fenster, das mehrere untergeordnete Fenster enthalten kann. Die untergeordneten Fenster werden während der Laufzeit des Programms als Reaktion auf entsprechende Benutzereingaben (z.B. Datei|Neu) erzeugt. Sie haben keine eigenen Menüs: Das Menü des übergeordneten Fensters gilt auch für die untergeordneten Fenster. MDI-Programme wurden früher oft verwendet, wenn mehrere Dateien gleichzeitig bearbeitet werden sollten (z.B. in älteren Word-Versionen zur Textverarbeitung). Im Unterschied zu MDI-Programmen bezeichnet man Programme, die keine untergeordneten Fenster enthalten, als SDI-Programme (Single Document Interface). Solche Programme erhält man mit einer Windows FormsAnwendung (Datei|Neu|Projekt|Visual C++|CLR) standardmäßig. Eine MDI-Anwendung erhält man mit folgenden Schritten: 1. Zuerst erzeugt man eine Windows Forms-Anwendung (Datei|Neu|Projekt|Visual C++|CLR). Das Formular dieser Anwendung wird dann dadurch zum übergeordneten Fenster, dass man seine Eigenschaft IsMdiContainer im Eigenschaftenfenster auf true setzt. In einem SDI-Formular hat diese Eigenschaft den Wert false (Voreinstellung).
10.7 MDI-Programme
1149
Setzt man auf dieses Formular ein Menü, gilt dieses dann auch für alle untergeordneten Fenster. Für das folgende Beispiel wird ein Menü Datei mit einem Untereintrag Neu vorausgesetzt. 2. Dem Projekt fügt man dann ein neues Formular hinzu (Projekt|Neues Element hinzufügen|Visual C++|UI|Windows Form oder im Projektmappen-Explorer). Dieses Formular wird dann dadurch zum MDI-Child-Formular (untergeordneten Fenster), dass seiner Eigenschaft MdiParent das übergeordnete Formular zugewiesen wird (siehe die Anweisungen im Konstruktor unter 3.). Das MDI-Child-Formular kann nun wie jedes andere Formular gestaltet werden. Für die folgenden Beispiele wird ein MDI-Child-Formular angenommen, das eine mehrzeilige TextBox enthält, die das gesamte Formular ausfüllt (Eigenschaft Dock = Fill). Gibt man diesem Formular den Namen MDIChildForm, erhält man in der Quelltextdatei des Formulars die Klasse public ref class MDIChildForm : public System::Windows::Forms::Form { .. private: System::Windows::Forms::TextBox^ textBox1; .. MDIChildForm(void) // Konstruktor .. };
Speichert man diese Datei unter dem Namen "MDIChildForm.h", steht die Klasse MDIChildForm zur Verfügung nach #include "MDIChildForm.h" // am Anfang von Form1.h
3. Ein solches Formular wird dann wie ein gewöhnliches Formular mit gcnew erzeugt und mit Show angezeigt. Als einziger Unterschied zu gewöhnlichen Formularen muss noch die Eigenschaft MdiParent auf das aktuelle Formular gesetzt werden. Führt man diese Anweisungen als Reaktion auf das Anklicken der Menüoption Datei|Neu aus, wird bei jedem Anklicken ein neues Formular erzeugt: private: System::Void neuToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ e) { MDIChildForm^ f=gcnew MDIChildForm(); f->MdiParent = this; f->Show(); }
Beispiel: Wenn ein Formular wie in 1. bis 3. von einer TextBox ausgefüllt wird, kann man in jeder der TextBoxen wie in einem einfachen Editor einen eigenständigen Text schreiben:
1150
10 Einige Elemente der .NET-Klassenbibliothek
Ein übergeordnetes MDI-Fenster enthält oft ein Menü mit dem Namen Fenster, dessen Untereinträge alle geöffneten Fenster enthalten. Ein solches Menü erhält man, indem man der Eigenschaft MdiWindowListItem des MenuStrip im Eigenschaftenfenster das Menü zuweist. Der im Menü angezeigte Text ist der Text der Titelzeilen (Eigenschaft Text) der untergeordneten MDI-Formulare. Da Windows die MDI-Fenster selbst verwaltet, ist es nicht notwendig, die während der Laufzeit erzeugten Child-Formulare selbst zu verwalten. Sie stehen unter der Eigenschaft
property array^ MdiChildren des MdiContainer-Formulars zur Verfügung und können über MdiChildren[i] angesprochen werden. Mit der folgenden Methode werden alle geschlossen: void closeAllMdiChilds(Form^ F) { for each(Form^ f in F->MdiChildren) f->Close(); }
Das jeweils aktive MDI-Child-Formular ist gegeben durch
property Form^ ActiveMdiChild Mit einer expliziten Typkonversion kann ein MDIChild auf den tatsächlichen Datentyp des Nachfolgers von Form konvertiert werden: void clearAllMdiChilds(Form^ F) { for each(Form^ f in F->MdiChildren) if (dynamic_cast(f)) dynamic_cast(f)->textBox1->Clear(); }
Für die Anordnung von MDI-Child-Formularen steht die Methode
10.8 Uhrzeiten, Kalenderdaten und Timer
1151
void LayoutMdi(MdiLayout value) zur Verfügung, der ein Wert des C++/CLI-Aufzählungstyps MdiLayout übergeben kann:
ArrangeIcons Cascade TileHorizontal TileVertical
// ordnet die Formulare an // ordnet die Formulare überlappend an // ordnet die Formulare untereinander an // ordnet die Formulare nebeneinander an
Aufgabe 10.7 Entwerfen Sie analog zum Beispiel im Text ein MDI-Programm, das auf jedem untergeordneten Fenster mit einem WebBrowser (Toolbox Registerkarte „Allgemeine Steuerelemente“, siehe Abschnitt 10.19.1) eine Internetseite anzeigt. Sie können zur Eingabe der URL (z.B. „www.microsoft.com“) eine ToolStripTextBox oder eine ToolStripComboBox auf dem MenuStrip verwenden und die Seite mit f->webBrowser1->Navigate(toolStripComboBox1->Text);
laden. Die Menüleiste soll lediglich die Option „Datei“ mit dem Unterpunkt „Öffnen“ und die Option „Fenster“ mit den Unterpunkten ArrangeIcons, Cascade, TileHorizontal und TileVertical anbieten, deren Auswahl zum entsprechenden MdiLayout führt. Das Menü „Fenster“ soll die Adressen aller geladenen Internetseiten anzeigen.
10.8 Uhrzeiten, Kalenderdaten und Timer Im Folgenden werden einige der wichtigsten .NET-Klassen vorgestellt, mit denen man Uhrzeiten und Kalenderdaten darstellen und formatieren, Zeiten messen und zyklische Ereignisse auslösen kann.
10.8.1 Die Klassen DateTime und TimeSpan Die Klasse DateTime stellt ein Kalenderdatum und eine Uhrzeit dar. Dazu wird intern ein 62-Bit Wert mit der Anzahl der 100-Nanosekunden Ticks seit dem 1. Januar des Jahres 0 unserer Zeitrechnung verwendet. Der maximal darstellbare Wert entspricht dem 31.12.9999, und der minimale dem 1.1.0001. Diese Klasse hat unter anderem die folgenden Konstruktoren, mit denen man ein Datum zu einem double-Wert, einem String oder vorgegeben Jahres-, Monats- und Stundenzahlen erhält:
1152
10 Einige Elemente der .NET-Klassenbibliothek
DateTime(int year, int month, int day) DateTime(int year, int month, int day, int hour, int minute, int second) DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond) Die Bestandteile eines Datums erhält man mit Eigenschaften wie
property int Month // Monat, zwischen 1 und 12 property int Day // Tag, zwischen 1 und 31 property int DayOfYear // Tag des Jahres, zwischen 1 und 366 property DateTime Date // Datum mit der Uhrzeit 0.00.00 property int Hour // Stunde, zwischen 0 und 23 property int Minute // Minute, zwischen 0 und 59 property int Second // Sekunde, zwischen 0 und 59 property int Millisecond // Millisekunden, zwischen 0 und 999 Aktuelle Werte für Datum und Uhrzeit erhält man mit statischen Eigenschaften:
static property DateTime Now; // aktuelles Datum und Zeit static property DateTime Today; // aktuelles Datum, Zeit=00:00:00 Diese Zeiten sind unter Windows 9x allerdings nur auf etwa 55 Millisekunden und unter Windows NT/2000/XP auf etwa 10 Millisekunden genau. Für die Konvertierung eines Kalenderdatums vom Datentyp DateTime in einen String stehen überladene ToString-Methoden zur Verfügung. Die einfachste Variante (ohne Parameter) verwendet die Ländereinstellung von Windows:
virtual String^ ToString() override Den weiteren Varianten kann man Format-Strings und Kultureinstellungen übergeben, die in Abschnitt 10.8.5 beschrieben werden. Ein String kann mit der statischen Elementfunktion der Klasse Convert
static DateTime ToDateTime(String^ value) sowie mit den folgenden Methoden von DateTime in ein Datum umgewandelt werden:
static DateTime Parse(String^ s) static bool TryParse(String^ s, DateTime% result) Die Funktion Parse versucht, den als Argument übergebenen String in ein Datum zu konvertieren und dabei unpassende Daten zu ignorieren. Falls das nicht möglich ist, wird eine Exception ausgelöst. TryParse unterscheidet sich von Parse nur dadurch, dass nie eine Exception ausgelöst wird und der Rückgabewert angibt, ob die Konversion möglich war.
10.8 Uhrzeiten, Kalenderdaten und Timer
1153
Beispiel: Die folgende Timer-Ereignisbehandlungsroutine (siehe Abschnitt 10.8.3) schreibt das aktuelle Datum und die aktuelle Zeit in eine Statusleiste: private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) { toolStripStatusLabel1->Text = DateTime::Now.ToString(); }
DateTime-Werte können addiert und subtrahiert werden. Außerdem stehen zahlreiche Methoden wie DateTime AddDays(double value) zur Verfügung, mit denen Jahre, Monate, Tage, Stunden, Minuten und Sekunden zu einem Datum addiert werden können. Beispiel: Die einfachste Möglichkeit, die Laufzeit von Anweisungen zu messen, erhält man nach folgendem Schema. Wie oben schon bemerkt wurde, sind diese Ergebnisse aber nicht allzu genau. Ein genaueres Verfahren wird in Abschnitt 10.8.4 vorgestellt. DateTime start=DateTime::Now; double s=0;for (int i=0; iAppendText("t="+(end-start).ToString());
Die Klasse TimeSpan stellt eine Zeitdauer als
property long long Ticks // ein Tick entspricht 100 ns, positiv oder negativ dar. Dieser Datentyp wird z.B. für das Ergebnis der Subtraktion von DateTimeWerten (wie im letzten Beispiel)
static TimeSpan operator-(DateTime d1, DateTime d2) oder von DateTime- und TimeSpan-Werten
static DateTime operator-(DateTime d, TimeSpan t) verwendet. Die Bestandteile eines Zeitraums erhält man mit Eigenschaften wie
property int Milliseconds // Millisekunden, zwischen -999 und 999 property int Seconds // Sekunden, zwischen -59 und 59 property int Days // Tage, positiv oder negativ
1154
10 Einige Elemente der .NET-Klassenbibliothek
(weitere Elemente dieser Art Hours, Minutes). Die Total-Elemente stellen die Anzahl der Ticks in der jeweiligen Einheit dar und sind nicht auf Bereiche wie 999 und 999 oder -59 und 59 beschränkt:
property double TotalMilliseconds property double TotalDays 10.8.2 Steuerelemente zur Eingabe von Kalenderdaten und Zeiten Ein MonthCalendar (Toolbox Registerkarte „Allgemeine Steuerelemente“) stellt einen Monatskalender dar, aus dem man ein Datum auswählen kann. Das ausgewählte Datum ist dann der Wert der Eigenschaft
property DateTime SelectionStart Falls ein Bereich von Kalenderdaten ausgewählt wird, ist der Wert der Eigenschaft SelectionEnd das Datum am Ende des Bereichs. Falls nur ein einziges Datum und kein Bereich ausgewählt wurde, sind die beiden Werte gleich. Beispiel: Diese Anweisung gibt das ausgewählte Datum in einer TextBox aus: textBox1->Text=monthCalendar1->SelectionStart. ToString();
Mit einem DateTimePicker (Toolbox Registerkarte „Allgemeine Steuerelemente“) kann man ein Kalenderdatum bzw. eine Uhrzeit auswählen. Der angezeigte Wert ist dann der Wert der Eigenschaft
property DateTime Value Über die Eigenschaft Format kann man das Darstellungsformat festlegen, z.B. dateTimePicker1->Format=DateTimePickerFormat::Long;
Setzt man die boolesche Eigenschaft ShowUpDown auf true, wird am rechten Rand ein Auf/Ab-Schalter angezeigt, mit dem man jedes einzelne Element der Anzeige erhöhen oder reduzieren kann. Wenn der Wert von ShowUpDown dagegen false ist, wird rechts ein Pulldown-Button angezeigt, über den man einen Monatskalender aufklappen kann. Aus diesem kann man wie bei einem MonthCalendar ein Datum auswählen. Im Unterschied zu einem MonthCalendar kann man hier aber keine Datumsbereiche eingeben.
10.8 Uhrzeiten, Kalenderdaten und Timer
1155
10.8.3 Timer und zeitgesteuerte Ereignisse Die Komponente Timer (Toolbox Registerkarte „Komponenten“, Namensbereich System::Windows::Forms) löst nach jedem Intervall von Interval (einer int-Eigenschaft von Timer) Millisekunden das Ereignis Tick aus. Damit kann man Anweisungen regelmäßig nach dem eingestellten Zeitintervall ausführen. Diese Anweisungen gibt man in der Ereignisbehandlungsroutine für Tick an. Mit der booleschen Eigenschaft Enabled kann der Timer aktiviert bzw. deaktiviert werden. Denselben Effekt erhält man auch durch einen Aufruf der Methoden Start und Stop. Beispiel: Wenn Interval den Wert 1000 hat, schreibt diese Funktion die von der Eigenschaft Now gelieferte aktuelle Zeit jede Sekunde auf ein Label: private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) { label1->Text=DateTime::Now.ToString(); }
Diese Timer-Komponente ist nicht so genau, wie man das von der Intervall-Unterteilung in Millisekunden (ms) erwarten könnte. – Ihre Auflösung liegt bei 55 ms. Deshalb erhält man mit dem Wert 50 für die Eigenschaft Interval etwa genauso viele Ticks wie mit dem Wert 1. Man kann außerdem auch bei großen Intervallen (z.B. 1000 ms) nicht erwarten, dass nach 10 Ticks genau 10 Sekunden vergangen sind. – Weitere Ungenauigkeiten ergeben sich daraus, dass ein Timer-Ereignis gerade ausgeführte Anweisungen nicht unterbricht, sondern lediglich eine Botschaft an die Anwendung sendet. Diese führt erst dann zum Aufruf der Ereignisbehandlungsroutine, wenn die zuvor initiierten Anweisungen abgearbeitet sind oder Application::DoEvents aufgerufen wird. Falls parallel zeitaufwendige Anweisungen ausgeführt werden, können solche Timer-Ereignisse auch verloren gehen (siehe Aufgabe 10.8, 2.). Im Namensbereich System::Timers steht eine weitere Timer-Komponente zur Verfügung, die Ähnlichkeiten mit dem Timer aus dem Namensbereich System::Windows::Forms hat. Dieser Timer (der auch als serverbasierter Timer bezeichnet wird) ist insbesondere etwas genauer als der aus System::Windows::Forms. – Sein Intervall (der Wert der Eigenschaft Interval) kann auch auf kleinere Werte als 55 gesetzt werden. – Ein Timer-Ereignis dieses Timers unterbricht die gerade ausgeführten Anweisungen. Damit beim Zugriff auf Steuerelemente keine Probleme auftreten, müssen solche Zugriffe synchronisiert werden (siehe die Eigenschaft SynchronizingObject weiter unten).
1156
10 Einige Elemente der .NET-Klassenbibliothek
Auch ein solcher Timer kann mit der booleschen Eigenschaft Enabled sowie durch einen Aufruf der Methoden Start und Stop aktiviert bzw. deaktiviert werden. Nach dem Ablauf eines Intervalls werden die Methoden und Funktionen des EventHandlers
event ElapsedEventHandler^ Elapsed aufgerufen. Dabei ist ElapsedEventHandler folgendermaßen definiert:
delegate void ElapsedEventHandler(Object^ sender, ElapsedEventArgs^ e) Die Eigenschaft
property bool AutoReset legt fest, ob das Ereignis nur einmal (false) ausgelöst wird oder mehrfach (true). Beispiel: Mit void myTimerEvent1(Object^ sender, System::Timers::ElapsedEventArgs^ e) { String^ st=Convert::ToString(e->SignalTime); System::Windows::Forms::MessageBox::Show(st); }
wird durch void test(TextBox^ tb) {//Die TextBox wird im nächsten Beispiel verwendet using namespace System; Timers::Timer^ t=gcnew Timers::Timer(); t->Enabled=true; t->Interval=100; t->Elapsed+=gcnew Timers::ElapsedEventHandler( myTimerEvent1); }
nach jeweils 100 Millisekunden eine neue MessageBox mit der aktuellen Zeit angezeigt. Falls eine System-Timer Ereignisbehandlungsroutine ein Steuerelement oder ein Formular verwendet, muss dieses der Eigenschaft
property ISynchronizeInvoke^ SynchronizingObject zugewiesen werden. Ohne eine solche Zuweisung kann eine Exception ausgelöst werden oder das gewünschte Ergebnis nicht eintreten. Bei einem Forms-Timer ist eine solche Zuweisung nicht notwendig. Beispiel: Wenn man bei einem Timer-Ereignis nicht wie im letzten Beispiel ein statisches Objekt (die MessageBox) ansprechen will, sondern ein als
10.8 Uhrzeiten, Kalenderdaten und Timer
1157
Parameter übergebenes Steuerelement, nimmt man dieses in eine Klasse auf und übergibt es im Konstruktor: ref class TextBoxWrapper{ TextBox^ tb; public: TextBoxWrapper(TextBox^ tb_):tb(tb_) {} void myTimerEvent2(Object^ sender, System::Timers::ElapsedEventArgs^ e) { String^ st=(e->SignalTime.TimeOfDay).ToString(); tb->AppendText(st+"\r\n"); } };
Mit dieser Klasse kann man die letzte Anweisung der Funktion test aus dem letzten Beispiel durch die folgenden Anweisungen ersetzen: t->SynchronizingObject=tb;//ohne diese Anw. Fehler TextBoxWrapper^ w=gcnew TextBoxWrapper(tb); t->Elapsed+=gcnew Timers::ElapsedEventHandler(w, &TextBoxWrapper::myTimerEvent2);
Dann wird jeweils nach der in der Eigenschaft Interval festgelegten Anzahl von Millisekunden die aktuelle Zeit in die TextBox geschrieben. Ein dritter Timer steht im Namensbereich System::Threading zur Verfügung. Dieser Timer ist die Grundlage des System::Timers und ist ebenso genau. Er verwendet aber keine Ereignisse. Stattdessen wird einem der Konstruktoren
Timer(TimerCallback^ callback) Timer(TimerCallback^ callback, Object^ state, int dueTime, int period) eine Funktion übergeben, die zu dem Typ
delegate void TimerCallback(Object^ state) passt. Diese wird dann das erste Mal nach dueTime Millisekunden und danach periodisch nach jeweils period Millisekunden aufgerufen. Beispiel: Mit ref class MyState { public: int count; MyState():count(0){} }; void myCallback(Object^ state) { dynamic_cast(state)->count++; }
1158
10 Einige Elemente der .NET-Klassenbibliothek
wird durch die folgenden Anweisungen der Wert 10 ausgegeben: using namespace System::Threading; MyState^ s=gcnew MyState; TimerCallback^ cb=gcnew TimerCallback(myCallback); Timer^ t=gcnew Timer(cb,s,0,100); Thread::Sleep(1000); textBox1->AppendText("count="+s->count+"\r\n");
10.8.4 Hochauflösende Zeitmessung mit der Klasse Stopwatch Die Auflösung von 10 oder 55 Millisekunden eines System::Timer oder eines System::Windows::Forms::Timer ist für manche Zeitmessungen zu gering. Genauere Werte erhält man mit der Klasse Stopwatch aus dem Namensbereich System::Diagnostics. Diese Klasse ermöglicht eine hochauflösende Zeitmessung im Mikrosekunden-Bereich. Falls der Rechner, auf dem diese Funktionen aufgerufen werden, über eine hochauflösende Uhr verfügt (was für alle modernen Rechner zutreffen sollte), hat
static initonly bool IsHighResolution den Wert true. Dann verwendet Stopwatch diesen hochauflösenden Timer, dessen Frequenz (Anzahl der Ticks pro Sekunde) z.B. durch
static initonly long long Frequency// z.B. 3579545 (ca. 300 ns) gegeben ist. Andernfalls verwendet Stopwatch den System-Timer. Mit den Funktionen
void Start() void Stop() kann der Stopwatch-Timer gestartet und angehalten werden. Die dazwischen verstrichene Zeit ist der Wert der Eigenschaften
property TimeSpan Elapsed property long long ElapsedMilliseconds property long long ElapsedTicks // etwas genauer als ElapsedMilliseconds Mit
void Reset() wird der Timer wieder zurückgesetzt. Dabei erhalten die Elapsed-Eigenschaften den Wert Null.
10.8 Uhrzeiten, Kalenderdaten und Timer
1159
Mit diesen Funktionen kann man die Ausführungszeit für eine Anweisung dann z.B. folgendermaßen messen und anzeigen: double Benchmark(TextBox^ tb,int n) { System::Diagnostics::Stopwatch sw; sw.Start(); double s=0; for (int i=0; iAppendText(sw.ElapsedTicks/float(sw.Frequency)+ " Sek. \r\n"); return s; }
Auf diese Weise wurden alle in diesem Buch angegebenen Laufzeiten gemessen. Die Genauigkeit der Ergebnisse ist allerdings nicht so hoch, wie man aufgrund der Auflösung eventuell erwarten könnte. So werden die Ergebnisse dadurch leicht verfälscht, dass Windows anderen gerade laufenden Prozessen Zeit zuteilt. Deswegen erhält man bei verschiedenen Messungen meist verschiedene Ergebnisse. Außerdem sollte man alle Laufzeitvergleiche nur mit Programmen ausführen, die in der Release-Konfiguration kompiliert wurden. Die folgende Tabelle enthält in jeder Zeile die Laufzeit derselben Funktion, die einmal mit einer Debug- und einmal mit einer Release-Konfiguration kompiliert wurde. Offensichtlich können die Laufzeiten sehr unterschiedlich, aber auch gleich sein.
VS 2008 Benchmark 4.1.3, 3 a) mit string Benchmark 4.1.3, 3 a) mit String
Release-Konfig. 0,18 Sek. 0,020 Sek.
Debug-Konfig. 3,18 Sek. 0,020 Sek.
Visual Studio Team System enthält einen Profiler, mit dem Zeitmessungen automatisch durchgeführt werden können.
10.8.5 Kulturspezifische Datumsformate und Kalender Für DateTime-Werte sind zahlreiche Formatangaben verfügbar, die in ToStringMethoden wie
String^ ToString(String^ format) virtual String^ ToString(IFormatProvider^ provider) sealed virtual String^ ToString(String^ format, IFormatProvider^ provider) sealed einzeln oder als Kombination angegeben werden können. Die folgende Tabelle enthält nur die wichtigsten:
1160
Formatangabe d, %d, dd, ddd, dddd M, %M, MM, MMM, MMMM y, %y, yy, yyyy h, %h, hh, H, %H, HH m, %m, mm, s, %s, ss,
10 Einige Elemente der .NET-Klassenbibliothek
Beschreibung Tag: als ein- oder zweistellige Zahl, als kurzer oder langer Name Monat: als ein- oder zweistellige Zahl, als kurzer oder langer Name Jahr: als ein- oder zweistellige Zahl, als kurzer oder langer Name Stunde:als ein- oder zweistellige Zahl in 12oder 24-Stundenschreibweise Minute: als ein- oder zweistellige Zahl, Sekunde: als ein- oder zweistellige Zahl,
Aus diesen Formatangaben kann man Strings mit einer nahezu beliebigen Formatierung aufbauen. Meist ist aber einer der für eine CultureInfo-Klasse vordefinierten Formatierungsstrings ausreichend. Diese Klasse enthält eine Eigenschaft
virtual property DateTimeFormatInfo^ DateTimeFormat mit zahlreichen Eigenschaften, die Formatierungsstrings (Datentyp String) darstellen:
DateTimeFormatInfo Eigenschaft LongDatePattern
ShortDatePattern
FullDateTimePattern
ShortTimePattern
LongTimePattern
SortableDateTimePattern
Beispiele für CultureInfo „de-DE“ und „en-US“ de-DE: dddd, d. MMMM yyyy en-US: dddd, MMMM dd, yyyy de-DE: dd.MM.yyyy en-US: M/d/yyyy de-DE: dddd, d. MMMM yyyy HH:mm:ss en-US: dddd, MMMM dd, yyyy h:mm:ss tt de-DE: HH:mm en-US: h:mm tt de-DE: HH:mm:ss en-US: h:mm:ss tt de-DE: yyyy'-'MM'-'dd'T'HH':'mm':'ss en-US: yyyy'-'MM'-'dd'T'HH':'mm':'ss
Formatzeichen D
d
F
t
T
s
Die CultureInfo-Werte der aktuellen Anwendung erhält man über die Eigenschaft
static property CultureInfo^ CurrentCulture der Application. Mit dem Formatzeichen aus der letzen Spalte der Tabelle erhält man dieselbe Formatierung wie mit dem String aus der ersten Spalte. Dieses Zeichen kann man auch im Formatstring von String::Format verwenden. Beispiel: Die drei AppendText-Anweisungen in
10.8 Uhrzeiten, Kalenderdaten und Timer
1161
String^ f=Application::CurrentCulture-> DateTimeFormat->FullDateTimePattern; DateTime t=DateTime::Now; textBox1->AppendText(t.ToString(f)"); textBox1->AppendText(t.ToString("F")"); textBox1->AppendText(String::Format("{0:F}",t));
erzeugen alle dieselbe Ausgabe, etwa Mittwoch, 21. November 2007 00:54:57 Mit der Klasse CultureInfo aus dem Namensbereich System::Globalization erhält man Formatierungsstrings für zahlreiche Kulturen. Bei der Definition eines Objekts dieser Klasse übergibt man dem Konstruktor einen String im Format "xyUV" nach dem Standard RFC 1766. Dabei sind xy zwei Kleinbuchstaben für einen Sprachcode und UV zwei Großbuchstaben für einen Ländercode. Beispiel: Mit using namespace System::Globalization; CultureInfo^ de=gcnew CultureInfo("de-DE"); CultureInfo^ en=gcnew CultureInfo("en-US"); CultureInfo^ fr=gcnew CultureInfo("fr-FR");
erhält man CultureInfo-Objekte für den deutschen, englisch-amerikanischen und französischen Sprach- und Ländercode. Bei der ToString-Methode von DateTime kann man ein CultureInfo-Objekt angeben. Dann wird das Datum im entsprechenden kulturspezifischen Format dargestellt. Ohne eine solche Angabe werden die Landeseinstellungen von Windows verwendet. Beispiel: Mit den CultureInfo-Objekten de, en und fr aus dem letzten Beispiel erhalten die Strings sde, sen und sfr DateTime dt = DateTime::Now; String^ sde="de-DE: "+dt.ToString("F",de); String^ sen="en-US: "+dt.ToString("F",en); String^ sfr="fr-FR: "+dt.ToString("F",fr);
Werte wie de-DE: Dienstag, 20. November 2007 13:26:35 en-US: Tuesday, November 20, 2007 1:26:35 PM fr-FR: mardi 20 novembre 2007 13:26:35
Bei CultureInfo-Objekten, die nicht wie „de-DE“, „en-US“ und „fr-FR“ einen gregorianischen Kalender verwenden, muss der Eigenschaft DateTimeFormat->Calender ein gregorianischer Kalender zugewiesen werden:
1162
10 Einige Elemente der .NET-Klassenbibliothek
CultureInfo^ ar=gcnew CultureInfo("ar-SA",false); Calendar^ greg = gcnew GregorianCalendar; ar->DateTimeFormat->Calendar = greg; String^ sar="ar-SA: "+dt.ToString("F",ar);
Der String sar erhält dann einen Werte wie ar-SA: 20 ϥϭϑϡΏέ, 2007 01:26:35 ϡ
10.8.6 Kalenderklassen Die Kalenderklassen HebrewCalendar, JapaneseCalendar, JulianCalendar usw. aus dem Namensbereich System::Globalization enthalten zahlreiche Funktion und Eigenschaften, die in verschiedenen Kalendern verschieden sind. Beispiel: Die Anweisungen using namespace System::Globalization; array^ a = gcnew array(5); a[0] = gcnew GregorianCalendar; a[1] = gcnew HebrewCalendar; a[2] = gcnew JapaneseCalendar; a[3] = gcnew JulianCalendar; a[4] = gcnew ThaiBuddhistCalendar; for each(Calendar^ c in a) { DateTime dt = DateTime::Today; int y=c->GetYear(dt); String^ f="Y: {0} Months: {1} Days: {2}\r\n"; s=String::Format(f,y, c->GetMonthsInYear(y), c->GetDaysInYear(y)); tb->AppendText(s); }
erzeugen die folgende Ausgabe: Y: Y: Y: Y: Y:
2007 Months: 12 Days: 365 5768 Months: 13 Days: 383 19 Months: 12 Days: 365 2007 Months: 12 Days: 365 2550 Months: 12 Days: 365
Aufgabe 10.8 1. Schreiben Sie eine Funktion
String^ TimeStr(Stopwatch^ sw, const String^ s) die für die als Argument übergebene Stopwatch die verstrichene Zeit in Sekunden als String zurückgibt. Der Parameter s soll an diesen String angefügt werden und z.B. den Namen der gemessenen Funktion enthalten, um bei mehreren Laufzeitmessungen eine Identifikation des Tests zu ermöglichen
10.9 Asynchrone Programmierung und Threads
1163
Eine solche Funktion ermöglicht bei Laufzeitmessungen eine einheitliche Darstellung der Ergebnisse. Sie soll deshalb in einer eigenen Datei (z.B. mit dem Namen „TimeUtils.h“) verfügbar sein, so dass man die Funktion z.B. mit #include "\CppUtils\BenchUtils.h" // auf der Buch-CD
in ein Programm einbinden kann. Rufen Sie TimeStr in einer Funktion wie Benchmark (siehe Abschnitt 10.8.4) auf und zeigen Sie die Laufzeiten von Benchmark für einige Argumente (z.B. 1000, 10000, 100000) an. 2. Prüfen Sie Genauigkeit von Timer-Ereignissen, indem Sie die Ergebnisse von zwei Timern vergleichen. a) Führen Sie die folgenden Anweisungen mit zwei Timern aus dem Namensbereich System::Windows::Forms und zwei int-Variablen i1 und i2 aus: – Der erste Timer soll ein Intervall von 500 haben und den Wert einer Variablen um 1 erhöhen. – Der zweite soll ein Intervall von 5000 haben und eine zweite Variable um 10 erhöhen. Dieser zweite Timer soll außerdem die Werte der beiden Variablen sowie die seit dem Start des Timers verstrichene Zeit (die mit einer Stopwatch gemessen wird) in einer mehrzeiligen TextBox ausgeben. Im Idealfall müssten die Werte von beiden Variablen gleich sein. b) Führen Sie die Anweisungen aus a) mit zwei Timern aus dem Namensbereich System::Timers: und zwei int-Variablen j1 und j2 aus. c) Starten Sie während der Ausführung der Anweisungen aus a) und b) eine Funktion, die einige Sekunden dauert und beobachten Sie die angezeigten Werte. d) Führen Sie die Anweisungen unter a) und b) mit den Timer-Intervallen 10 und 100 aus.
10.9 Asynchrone Programmierung und Threads Wenn ein Programm mit einer grafischen Benutzeroberfläche eine Funktion aufruft, sind bis zur Beendigung dieser Funktion alle Steuerelemente blockiert, und man kann keine Buttons anklicken, keinen Text in ein Eingabefeld eingeben usw. Bei „schnellen“ Funktionen ist dieser Effekt kaum spürbar. Falls ihre Ausführung aber länger dauert, kann eine solche Blockade lästig und unerwünscht sein. Beispiel: In Abschnitt 5.3 wurde gezeigt, dass die Funktion (siehe Aufgabe 5.3.1) int rec_Fib(int n) { // Fibonacci-Zahlen rekursiv berechnen if (nAppendText("fib(45) ="+ rek_Fib(45)); }
Solche Blockaden kann man mit Multithreading unterbinden, indem man eine zeitaufwendige Funktion als eigenen Thread startet. Damit werden Programme und Threads quasi-parallel ausgeführt. Multithreading kann außerdem zu schnelleren Ergebnissen eines Programms führen: – Das gilt bei einem Einprozessorrechner vor allem dann, wenn auf zeitaufwendige Ein- und Ausgabeoperationen gewartet werden muss. Ein Programm, das nacheinander auf die Daten von 10 Servern wartet, wird meist länger brauchen, als wenn alle 10 Anfragen in einem jeweils eigenen Thread parallel angefordert werden. – Bei einem Mehrprozessorrechner (Mehrkern-CPU) kann das Betriebssystem die verschiedenen Threads auf verschiedene Prozessoren verteilen. – Threads kann man entsprechend ihrer Wichtigkeit eine höhere oder niedrigere Priorität geben. Diese Vorteile bekommt man aber nicht umsonst: – Das Betriebssystem muss die Threads verwalten, was mit einem zusätzlichen Aufwand verbunden ist. Es ist kein Vorteil, Funktionen auf Threads zu verteilen, die ohne Threads genauso schnell sind, weil keine Teilaufgaben parallel abgearbeitet werden. – Die Programmierung von Threads kann fehleranfällig und komplizierter sein. Falls die Threads ihre Zugriffe auf gemeinsame Daten nicht synchronisieren, kann das Fehler nach sich ziehen, die nur schwer zu finden sind, weil sie z.B. erst nach einigen Stunden Laufzeit auftreten. Deswegen sollte man Threads nur verwenden, wenn die Vorteile die Nachteile überwiegen. In diesem Zusammenhang bezeichnet man ein Programm, das in den Hauptspeicher geladen (gestartet) wurde, als Prozess. Zu jedem Prozess gehören ein privater Adressraum, Code, Daten usw. Jedes Programm wird mit einem Thread (dem sogenannten primären Thread) gestartet, kann aber weitere Threads
10.9 Asynchrone Programmierung und Threads
1165
erzeugen. Viele Programme unter Windows bestehen nur aus einem einzigen Thread.
10.9.1 Die verschiedenen Thread-Zustände Ein Thread ist die Basiseinheit von Anweisungen, der das Betriebssystem CPUZeit zuteilen kann. Jeder Thread, der im Zustand Running ist, erhält vom Betriebssystem eine Zeiteinheit (Zeitscheibe, timeslice) zugeteilt. Innerhalb dieser Zeit werden die Anweisungen dieses Threads ausgeführt. Sobald die Zeit abgelaufen ist, entzieht das Betriebssystem diesem Thread die CPU und teilt sie eventuell einem anderen Thread zu. Da jede Zeitscheibe relativ klein ist (Größenordnung 20 Millisekunden), entsteht so auch bei einem Rechner mit nur einem Prozessor der Eindruck, dass mehrere Programme gleichzeitig ablaufen. Auf einem Rechner mit mehreren Prozessoren können verschiedene Threads auch auf mehrere Prozessoren verteilt werden. Der Zustand eines Threads wird durch ein Element des Datentyps
public enum class ThreadState dargestellt. Die wichtigsten Aktionen, die diesen Zustand beeinflussen, sind:
ThreadState vorher
Aktion
ThreadState nachher Unstarted Unstarted den Running
Ein CLR-Thread wird erstellt Ein Thread ruft Start auf Das Betriebssystem startet Thread Running Ein anderer Thread ruft Suspend auf SuspendRequested Das Betriebssystem unterbricht den Thread Suspended Ein anderer Thread ruft Resume auf Running Ein anderer Thread ruft Abort auf AbortRequested Das Betriebssystem unterbricht den Thread
Unstarted Unstarted
SuspendRequested Suspended
Running AbortRequested Stopped
Ein Thread kann außerdem durch den Aufruf einer der Thread-Methoden Sleep, Wait oder Join oder durch einen Synchronisationsmechanismus (z.B. Monitor) blockiert werden. Er befindet sich dann im Zustand WaitSleepJoin. Dieser Zustand wird wieder beendet, wenn die bei Sleep angegebene Zeit abgelaufen ist, die JoinBedingung oder die Synchronisationsbedingung eintritt.
10.9.2 Multithreading mit der Klasse BackgroundWorker Zur Ausführung von Threads,
1166
10 Einige Elemente der .NET-Klassenbibliothek
– in denen Ergebnisse berechnet werden, – die anschließend mit einem Steuerelement angezeigt werden sollen, verwendet man in .NET am einfachsten die Klasse BackgroundWorker aus der Toolbox (Registerkarte „Komponenten“), indem man sie auf das Formular zieht. Sie hat drei Ereignisse, über die die Thread-Funktion festgelegt wird und die Anzeige von Ergebnissen erfolgt:
Diese Funktionen kann man durch einen Doppelklick auf die rechte Spalte des Ereignisses im Eigenschaftenfenster erzeugen. 1. In die Ereignisbehandlungsroutine für das Ereignis DoWork nimmt man die Anweisungen auf, die als Thread ausgeführt werden sollen. Diese Funktion wird dann durch den Aufruf einer der Methoden
void RunWorkerAsync() void RunWorkerAsync(Object^ argument) des BackgroundWorker gestartet. Beispiel: Ruft man die Funktion rec_fib von oben in private: System::Void backgroundWorker1_DoWork( System:: Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) {//über Eigenschaftenfenster|Ereignisse erzeugen rec_Fib(45); }
auf, wird sie durch private: System::Void button2_Click (System:: Object^ sender, System::EventArgs^ e) { backgroundWorker1->RunWorkerAsync(); }
als eigener Thread gestartet.
10.9 Asynchrone Programmierung und Threads
1167
Allerdings darf man in der DoWork-Ereignisbehandlungsroutine keine Komponenten der Benutzeroberfläche verändern. Beispiel: Zeigt man das Ergebnis der Funktion rec_fib wie in private: System::Void backgroundWorker1_DoWork( System:: Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) { int x=Convert::ToInt32(textBox1->Text); int result= rec_Fib(45); textBox2->AppendText("result="+result); }
in einer TextBox an, wird beim Aufruf von AppendText eine Exception ausgelöst. 2. Zur Übergabe von Parametern an die in der DoWork-Ereignisbehandlungsroutine aufgerufenen Funktionen verwendet man die RunWorkerAsyncVariante mit einem Argument:
void RunWorkerAsync(Object^ argument) Das hier übergebene Argument steht in der DoWork-Ereignisbehandlungsroutine über die Eigenschaft
property Object^ Argument des Parameters e zur Verfügung. Über die weitere Eigenschaft
property Object^ Result von e kann ein Ergebnis von der DoWork-Ereignisbehandlungsroutine an die Ereignisbehandlungsroutine
delegate void RunWorkerCompletedEventHandler(Object^ sender, RunWorkerCompletedEventArgs^ e) übergeben werden und steht dann hier unter e->Result zur Verfügung. Dieses Ereignis tritt dann ein, wenn DoWork fertig ist. Das nächste Beispiel zeigt die beiden Ereignisbehandlungsroutinen für die Funktion rec_Fib, die mit einem Parameter aus einer TextBox aufgerufen wird und ihr Ergebnis in einer TextBox anzeigt. Beispiel: Das beim Aufruf von
1168
10 Einige Elemente der .NET-Klassenbibliothek
private: System::Void button1_Click( System::Object^ sender, System::EventArgs^ e) { int x=Convert::ToInt32(textBox1->Text); backgroundWorker1->RunWorkerAsync(x); }
übergebene Argument x wird in private: System::Void backgroundWorker1_DoWork( System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) { // über Eigenschaften|Ereignisse erzeugen int n=safe_cast(e->Argument); e->Result= rec_Fib(n); }
der Funktion rec_fib übergeben. Der e->Result zugewiesene Funktionswert wird nach dem Ende der Thread-Funktion durch private: System::Void backgroundWorker1_RunWorkerCompleted(System::Object^ sender, System::ComponentModel::RunWorkerCompletedEventArgs^ e) { // über Eigenschaften|Ereignisse erzeugen int result=static_cast(e->Result); textBox1->AppendText("result="+result+"\r\n"); }
in der TextBox textBox1 angezeigt. Bei langwierigen Operationen empfiehlt es sich oft, den Anwender darüber informieren, dass das Programm nicht hängt, und wie lange er noch etwa auf das Ergebnis warten muss. Solche Informationen können durch einen Aufruf von
void ReportProgress(int percentProgress) void ReportProgress(int percentProgress, Object^ userState) an die Ereignisbehandlungsroutine
delegate
void
ProgressChangedEventHandler(Object^ sender, ProgressChangedEventArgs^ e)
weitergegeben werden, wenn die Eigenschaft
property bool WorkerReportsProgress auf true gesetzt wurde. Mit der Voreinstellung false löst ein Aufruf von ReportProgress eine Exception aus oder hat keinen Effekt. Die Argumente für percentProgress und userState stehen dann als Eigenschaften
10.9 Asynchrone Programmierung und Threads
1169
property int ProgressPercentage property Object^ UserState des ProgressChangedEventArgs-Arguments in der ProgressChanged-Ereignisbehandlungsroutine zur Verfügung und können zur Aktualisierung einer ProgressBar verwendet werden. Beispiel: Der Aufruf von ReportProgress in System::Void backgroundWorker1_DoWork( System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) { for (int i=0; iReportProgress(i); } }
löst das Ereignis ProgressChanged aus, bei dem hier System::Void backgroundWorker1_ProgressChanged( System::Object^ sender, System::ComponentModel:: ProgressChangedEventArgs^ e) { toolStripProgressBar1->Value= e->ProgressPercentage; }
eine Fortschrittsanzeige aktualisiert wird.
10.9.3 Ereignisbasierte asynchrone Programmierung Einige .NET-Klassen stellen für einige potentiell zeitaufwendige Operationen Methoden zur Verfügung, die „Async“ im Namen enthalten. Diese Methoden führen dieselben Operationen aus wie entsprechende Methoden ohne „Async“ im Namen, allerdings asynchron in einem eigenen Thread. Nach der Fertigstellung der asynchronen Methode wird dann ein Ereignis ausgelöst, das „Completed“ im Namen enthält. Beispiele: Die Klasse WebClient (siehe Abschnitt 10.19.2) enthält eine Methode
void DownloadFileAsync(Uri^ address, String^ fileName) die den Download einer Datei in einem eigenen Thread ausführt. Entsprechende Methoden stehen auch für den Up- und Download von Dateien, Strings usw. zur Verfügung. Nach dem Abschluss wird das Ereignis
event AsyncCompletedEventHandler^ DownloadFileCompleted
1170
10 Einige Elemente der .NET-Klassenbibliothek
ausgelöst. Der EventHandler hat dabei den Datentyp:
delegate void AsyncCompletedEventHandler(Object^ AsyncCompletedEventArgs^ e)
sender,
Die Klasse SmtpClient (siehe Abschnitt 10.19.3) enthält eine Methode SendAsync, die eine Mail in einem eigenen Thread versendet. Die Klasse PictureBox (siehe Abschnitt 10.10.1) enthält eine Methode LoadAsync, die ein Bild in einem eigenen Thread lädt. Mit diesen Methoden ist die Ausführung von Funktionen in einem eigenen Thread sehr einfach, da man sich nicht um die oft recht komplexe Thread-Programmierung kümmern muss. Beispiele: Die Methode Download der Klasse FileDownloader lädt eine Datei im Hintergrund und gibt nach dem Abschluss eine Meldung in einer TextBox aus: ref class FileDownloader { TextBox^ tb; void DownloadCompletedEventHandler(Object^ sender, AsyncCompletedEventArgs^ e) { tb->AppendText("Download completed\r\n"); } public: FileDownloader(TextBox^ tb_):tb(tb_){} void Download(String^ URL, String^ Filename) { using namespace System::Net; WebClient^ client = gcnew WebClient; client->DownloadFileCompleted += gcnew AsyncCompletedEventHandler(this, &FileDownloader::DownloadCompletedEventHandler); Uri ^uri = gcnew Uri(URL); client->DownloadFileAsync(uri, Filename); } };
Solche asynchronen Methoden können auch für eigene Klassen geschrieben werden. Siehe dazu die MSDN-Dokumentation zu „Ereignisbasierte asynchrone Programmierung“ („Event-based Asynchronous Pattern“).
10.9.4 Die Klasse Thread und der Zugriff auf Steuerelemente Die Klasse Thread aus dem Namensbereich System::Threading bietet wesentlich mehr Möglichkeiten als die BackgroundWorker-Klasse. Dafür ist die Arbeit mit ihr aber auch etwas aufwendiger. Die folgenden Punkte sowie Abschnitt 10.9.7 stellen (ausgehend von den einfachsten Fällen) einige der wichtigsten Aspekte vor.
10.9 Asynchrone Programmierung und Threads
1171
Da das Erzeugen und Starten eines Thread (wie in den folgenden Beispielen) einige Millisekunden dauern kann, ist es meist nicht vorteilhaft, wenn man für kurze Aufgaben neue Threads anlegt. Für solche Aufgaben ist der ThreadPool (siehe Abschnitt 10.9.8) oft besser geeignet. 1. Um eine Funktion ohne Parameter und ohne Rückgabewert als Thread auszuführen, wird sie der Klasse Thread als Delegat des Typs
public delegate void ThreadStart() im Konstruktor übergeben und mit der Thread-Methode
void Start() gestartet. Beispiel: Die globale Funktion int result;//global oder static Element void f() { result=rec_Fib(45); }
wird durch die folgenden Anweisungen als Thread gestartet: using namespace System::Threading; ThreadStart^ df= gcnew ThreadStart(f); Thread^ t = gcnew Thread(df); t->Start();
Wenn f in einer Verweisklasse R enthalten ist, kann man die folgende Schreibweise verwenden: R^ r=gcnew R; ThreadStart^ df= gcnew ThreadStart(r,&R::f);
2. Die Vorgehensweise unter 1. hilft bei Funktionen mit Parametern auf den ersten Blick nicht weiter. Um eine solche Funktion als Thread auszuführen, gibt es im Wesentlichen die hier und unter 3. beschriebenen Möglichkeiten. Die Parameter werden in eine Verweisklasse gepackt. Der Klasse Thread wird dann ein Objekt dieser Klasse als Delegat des Typs
public delegate void ParameterizedThreadStart(Object^ obj) im Konstruktor übergeben und mit der Thread-Methode
void Start(Object^ parameter)
1172
10 Einige Elemente der .NET-Klassenbibliothek
gestartet. Das Thread-Objekt übergibt dann das Argument beim Aufruf von Start an das Delegat-Objekt. Da an Start beliebige von Object abgeleitete Objekte übergeben werden können, sollte man in der Thread-Funktion prüfen, ob das Argument auch den richtigen Typ hat. Beispiel: Um die Elementfunktion f der Verweisklasse ref struct R { // struct nur zur Vereinfachung T f(T1 x, T2 y) // T, T1 und T2 beliebige { // Datentypen return 17; }
als Thread zu starten, definiert man eine Klasse für die Argumente ref struct Argumente { // struct nur zur T1 x; // Vereinfachung T2 y; Argumente(T1 x_, T2 y_):x(x_),y(y_){}; };
In einer weiteren Funktion prüft man, ob das Argument den Typ Argumente hat und ruft dann die Funktion f auf: void call_f(Object^ p) { Argumente^ arg=dynamic_cast(p); if (arg) // prüfe den Typ des Arguments T result=f(arg->x,arg->y); }
Diese Funktion kann man dann als Thread folgendermaßen starten: Argumente^ arg=gcnew Argumente("17",18); R r; using namespace System::Threading; ParameterizedThreadStart^ df= gcnew ParameterizedThreadStart(%r,&R::call_f); Thread t(df); t.Start(arg);
3. Allerdings ist es mit der Variante 2. nicht ganz einfach, Steuerelemente anzusprechen. Die folgende Variante geht ähnlich vor und packt die Parameter und Rückgabewerte in eine eigene Klasse. Die Thread-Funktion wird dann von einer Funktion ohne Parameter aufgerufen, die wie unter 1. als Thread gestartet wird. Diese Variante wird unter 4. so erweitert, dass man in einer Thread-Funktion auch auf Steuerelemente zugreifen kann. Beispiel: Die als Thread aufzurufende Funktion wird wie unter 2. in eine Verweisklasse aufgenommen: ref class R {
10.9 Asynchrone Programmierung und Threads
1173
T f(T1 x, T2 y) // T, T1 und T2 beliebige { // Datentypen return ...; }
Dazu kommen noch Datenelemente für die Argumente und den Rückgabewert, ein Konstruktor, der die Argumente setzt, sowie eine Funktion wie call_f: T1 x; // für die Argumente T2 y; public: T result; // Rückgabewert R(T1 x_, T2 y_):x(x_),y(y_){}; // Konstruktor void call_f() // keine Parameter und kein { // Rückgabewert result=f(x,y); } };
Die Funktion call_f ist zu ThreadStart kompatibel und kann wie in R r("17",18); using namespace System::Threading; ThreadStart^ df= gcnew ThreadStart(%r,&R::call_f); Thread t(df); t.Start();
als Thread ausgeführt werden. 4. In einer Funktion kann man nur auf Steuerelemente zugreifen, die von demselben Thread erzeugt wurden, in dem die Funktion ausgeführt wird. Das war in allen bisherigen Beispielen nicht der Fall. Hätte man in diesen Beispielen in der Funktion f oder call_f auf ein Steuerelement zugegriffen, um z.B. das Ergebnis anzuzeigen, hätte man eine InvalidOperationException-Exception mit der Meldung „Ungültiger threadübergreifender Vorgang“ erhalten:
Mit der Control-Eigenschaft
virtual property bool InvokeRequired
1174
10 Einige Elemente der .NET-Klassenbibliothek
kann man feststellen, ob das Steuerelement in demselben Thread erzeugt wurde. Falls das zutrifft, kann das Steuerelement angesprochen werden. Andernfalls muss der Aufruf mit einer der Control-Methoden
Object^ Invoke(Delegate^ method) IAsyncResult^ BeginInvoke(Delegate^ method) im Thread des Steuerelements gestartet werden. Der als Argument übergebene Delegat wird dann von demselben Thread wie das Steuerelement ausgeführt. Beispiel: Die Methode WriteLine der Klasse WriteControl setzt genau das um und kann deshalb von einer Thread-Funktion aufgerufen werden. Sie gibt das Argument in der zuvor mit setTB festgelegten TextBox aus: ref class WriteControl { static TextBox^ tb; delegate void void_String_delegate(String^); public: static void setTB(TextBox ^ tb_){tb=tb_;} static void WriteLine(String^ s) { if (!tb->InvokeRequired) tb->AppendText(s+"\r\n"); else tb->Invoke(gcnew void_String_delegate( WriteControl::WriteLine),s); } };
Ergänzt man die Klasse R aus dem letzten Beispiel um diese WriteControl-Klasse, eine TextBox, in der die Ergebnisse angezeigt werden sollen, sowie um einen Konstruktor, der die TextBox übergibt TextBox^ tb; R(TextBox^ tb_,
T1
x_,
T2
y_):tb(tb_),x(x_), y(y_) {};
und ändert man Funktion call_f folgendermaßen ab void call_f() { result=f(x,y); WriteControl::setTB(tb); WriteControl::WriteLine("f="+result.ToString()); }
wird call_f in einem eigenen Thread ausgeführt. Die Ausgabe des Ergebnisses erfolgt in einem weiteren Thread, der den Thread der TextBox verwendet und diese deshalb ansprechen kann.
10.9 Asynchrone Programmierung und Threads
1175
Invoke ist neben BeginInvoke, EndInvoke und CreateGraphics eine der wenigen Control-Methoden, die in einem Thread aufgerufen werden können. Ein Thread hat zahlreiche Eigenschaften und Methoden, wie z.B.
property ThreadPriority Priority für die Priorität eines Thread. Hier ist ThreadPriority ein C++/CLI-Aufzählungstyp, der unter anderem die Werte AboveNormal, BelowNormal, Highest, Lowest und Normal (Voreinstellung) annehmen kann. Mit den verschiedenen Varianten der Methode
void Join() kann man Threads synchronisieren. Ihr Aufruf bewirkt, dass der aufrufende Thread wartet, bis der Thread, dessen Join-Methode aufgerufen wurde, fertig ist. Ein Thread muss immer alle Exceptions abfangen. Wenn eine Exception in einem Thread nicht abgefangen wird, führt das zum Abbruch des gesamten Prozesses. Es reicht nicht aus, die Exception außerhalb von Thread-Start abzufangen. Beispiel: Im letzten Beispiel muss die Funktion call_f alle Exceptions abfangen. Fängt man die Exceptions außerhalb ab, führt eine Exception zu einem Programmabbruch: try {
// so nicht t.Start();
} catch(...) { // fängt Exceptions aus einem Thread nicht ab // ... }
10.9.5 IAsyncResult-basierte asynchrone Programmierung Eine weitere einfache Form der Thread-Programmierung ist mit den BeginXXX/EndXXX-Methoden möglich, die viele Klassen für eine potentiell zeitaufwendige Funktion XXX zur Verfügung stellen. Diese Methoden sollen am Beispiel der Stream-Klassen aus dem Namensbereich System::IO illustriert werden. Die von der Klasse Stream abgeleiteten Klassen stellen neben den Methoden
virtual int Read(array^ buffer, int offset, int count) virtual void Write(array^ buffer, int offset, int count) die Methoden
1176
10 Einige Elemente der .NET-Klassenbibliothek
virtual IAsyncResult^ BeginRead(array^ buffer, int offset, int count, AsyncCallback^ callback, Object^ state) virtual IAsyncResult^ BeginWrite(array^ buffer, int offset, int count, AsyncCallback^ callback, Object^ state) virtual int EndRead(IAsyncResult^ asyncResult) virtual void EndWrite(IAsyncResult^ asyncResult) Diese Methoden zeigen bereits einige Gemeinsamkeiten mit den BeginXXX- und EndXXX-Methoden, die man auch in anderen Klassen findet: – Die BeginXXX-Methode führt dieselbe Operation wie XXX aus. Ein wesentlicher Unterschied ist aber, dass BeginXXX asynchron und XXX synchron ausgeführt wird. – Die BeginXXX-Methoden haben den Rückgabetyp IAsyncResult. – Die ersten Parameter sind dieselben wie in der synchronen Methode XXX. – Die letzten beiden Parameter haben den Typ AsyncCallback^ und Object. – Die EndXXX-Methoden haben einen Parameter des Typs IAsyncResult. Das Argument muss ein Rückgabewert von BeginXXX sein. – Der Aufruf einer EndXXX-Methode bewirkt, dass Programm wartet, bis die Operationen der BeginXXX-Methode abgeschlossen sind. Vor dem Aufruf von kann man allerdings Anweisungen aufnehmen, die dann asynchron ausgeführt werden. – Zu jedem Aufruf von BeginXXX muss EndXXX genau einmal aufgerufen werden. – Das Argument für callback in BeginXXX kann der Wert nullptr sein. Verwendet man einen Delegaten des Typs
delegate void AsyncCallback(IAsyncResult^ ar) wird dieser beim Ende der Operation aufgerufen. Damit man mit einem Stream asynchrone Lese- oder Schreiboperationen durchführen kann, muss der Stream mit dem Flag FileOptions::Asynchronous öffnen. Schon in Abschnitt 9.13.2 wurde erwähnt, dass ein Delegat-Typ eine Klasse ist. So erzeugt der Compiler z.B. aus dem Delegat-Typ delegate int MeinDelegatTyp(double x,String^ y);
eine Klasse, die in ILDasm folgendermaßen angezeigt wird:
10.9 Asynchrone Programmierung und Threads
1177
Das entspricht etwa der C++-Klasse ref class MeinDelegatTyp sealed : MulticastDelegate { MeinDelegatTyp(Object^, int); IAsyncResult^ BeginInvoke(double, String^, AsyncCallback^, Object^); int EndInvoke(IAsyncResult^); int Invoke(double, String^); }
Die Methode BeginInvoke führt die Delegat-Funktion dann asynchron aus. Zu jedem BeginInvoke muss dann EndInvoke aufgerufen werden. Das Argument muss dabei der Rückgabewert von BeginInvoke sein. Nach der Ausführung der DelegatFunktion wird die AsyncCallback-Funktion aufgerufen. Der Funktionswert wird als Funktionswert von EndInvoke übergeben. Beispiel: Um eine Funktion wie int rec_Fib(int n) { /* wie oben */ }
}
asynchron mit BeginInvoke zu starten, definiert man einen Delegat-Typ, der zu dieser Funktion passt delegate int int_int_Delegate(int n);
sowie eine Funktion, die zu AsyncCallback passt: void toTextBoxCallback(IAsyncResult^ ar) { WriteControl::Write(ar); }
Diese Funktion rec_Fib kann dann folgendermaßen gestartet werden: void int_int_Background(TextBox^ tb) { WriteControl::setTB(tb); int_int_Delegate^ iid=gcnew int_int_Delegate(rec_Fib); iid->BeginInvoke(43,gcnew AsyncCallback(toTextBoxCallback),iid); }
1178
10 Einige Elemente der .NET-Klassenbibliothek
Die Klasse WriteControl ist ähnlich gebaut wie in Abschnitt 10.9.4, 4. Der im Wesentlichen einzige Unterschied besteht darin, dass EndInvoke aufgerufen und dabei der Funktionswert übergeben wird: ref class WriteControl { static TextBox^ tb; public: static void setTB(TextBox ^ tb_){tb=tb_;} static void Write(IAsyncResult^ ar) { if (!tb->InvokeRequired) { int_int_Delegate^ iid= safe_cast(ar->AsyncState); int x=iid->EndInvoke(ar); tb->AppendText("Output="+x.ToString()+"\r\n"); } else tb->Invoke(gcnew AsyncCallback(&WriteControl:: Write),ar); } };
10.9.6 Sleep und Threads Gelegentlich werden Threads mit Endlosschleifen verwendet, die eine Aufgabe zyklisch ausführen und dann die Thread-Methode
static void Sleep(int millisecondsTimeout) aufrufen wie in while (true) { // Aufgabe ausführen System::Threading::Thread::Sleep(60*1000); // warte } // eine Minute
Dieser Aufruf hat für millisecondsTimeout>0 den Effekt, dass der Thread für mindestens die angegebene Anzahl von Millisekunden bei der Zuteilung von Prozessorzeit nicht berücksichtigt wird. Mit dem Argument 0 gibt er die Zeitscheibe auf, und mit dem Wert Timeout::Infinite wird er überhaupt nicht mehr berücksichtigt. Da unklar ist, wann der Thread wieder Prozessorzeit bekommt, sind solche zyklischen Operationen nicht sehr genau. Ein Timer (siehe Abschnitt 10.8.3) ist meist eine bessere und einfachere Alternative. Windows-Formularanwendungen sollten Sleep nie mit einem Argument >0 aufrufen, da sie eine Message-Loop haben, die auch dann Botschaften erhalten kann,
10.9 Asynchrone Programmierung und Threads
1179
wenn die Anwendung keine Prozessorzeit hat. Ein Sleep kann dazu führen, dass Botschaften verloren gehen.
10.9.7 Kritische Abschnitte und die Synchronisation von Threads Den verschiedenen Threads eines Programms wird die CPU vom Betriebssystem zugeteilt und wieder entzogen. Da eine C++-Anweisung vom Compiler in mehrere Maschinenanweisungen übersetzt werden kann, kann es vorkommen, dass einem Thread die CPU während der Ausführung einer C++-Anweisung entzogen wird, obwohl diese erst zum Teil abgearbeitet ist. Beispiel: Während der Ausführung eines Programms im Debugger (Debuggen|Debuggen starten) kann man nach einem Haltepunkt mit Debuggen|Fenster|Disassembly die Maschinenanweisungen anschauen, die der Compiler aus den C++-Anweisung erzeugt hat:
In dieser Abbildung sieht man, wie eine if-Anweisung zu mehreren Maschinenanweisungen führt. Der ++-Operator führt bei einem 32-bit Prozessor mit einem 32-bit Wert zu einer einzigen Maschinenanweisung und mit einem breiteren Wert zu mehreren Maschinenanweisungen. Wenn mehrere Threads auf gemeinsame Daten (globale oder static Datenelemente) zugreifen, kann das dazu führen, dass der eine Thread mit den unvollständigen Werten eines anderen Threads weiterarbeitet. Das kann zu völlig unerwarteten und falschen Ergebnissen führen. Beispiel: Da die Elemente x von Common und c von R static sind, verwenden alle Instanzen von R beim Zugriff auf c->x denselben Speicherbereich: ref struct Common { static int x=-1; // gemeinsame Daten }; ref struct R { static Common^ c=gcnew Common;// static nur für int result; // Initialisierung void noSync() { c->x=-1;
1180
10 Einige Elemente der .NET-Klassenbibliothek
int j=0; for (int i=0; ix++; if (c->x!=0 && c->x!=1) j++; c->x=-1; } result=j; } }
Startet man die Funktion noSync so, dass sie nicht parallel in mehreren Threads ausgeführt wird, erhält man das erwartete Ergebnis 0. Startet man sie dagegen in zwei oder mehr parallelen Threads, erhält man oft ein von 0 verschiedenes Ergebnis: using namespace System::Threading; R^ r1 = gcnew R; ThreadStart^ df1=gcnew ThreadStart(r1,&R::noSync); Thread^ t1 = gcnew Thread(df1); t1->Start(); R^ r2 = gcnew R; ThreadStart^ df2=gcnew ThreadStart(r2,&R::noSync); Thread^ t2 = gcnew Thread(df2); t2->Start();
Für dieses Beispiel wäre es übrigens nicht notwendig gewesen, die gemeinsamen Daten in eine Klasse zu packen. Eine globale int-Variable wäre ausreichend gewesen. Da die gemeinsamen Daten für die nächsten Beispiele aber in einer Verweisklasse enthalten sein müssen, wurde diese bereits jetzt verwendet, damit die Unterschiede zwischen den Beispielen möglichst gering sind. Da viele .NET-Klassen gemeinsame Daten enthalten, die bei parallelen Zugriffen korrumpiert werden können, dürfen viele .NET-Funktionen nicht in einer ThreadFunktion aufgerufen werden. Die Dokumentation zu den .NET-Klassen enthält meist Hinweise, welche Methoden threadsicher sind. Oft sind die statischen Methoden threadsicher und die nicht statischen nicht. Einige Beispiele: – Die Klasse SynchronizedCollection ermöglicht einen thread-sicheren Zugriff auf die Elemente einer List-Collection. – Die nicht generischen Collection-Klassen ArrayList, HashTable, Queue usw. (siehe Abschnitt 10.12) sowie die Klasse Stream enthalten eine Methode wie
static ArrayList^ Synchronized(ArrayList^ list) die eine synchronisierte ArrayList zurückgibt. Über diesen Rückgabewert kann dann von mehreren Threads auf die Collection zugegriffen werden.
10.9 Asynchrone Programmierung und Threads
1181
Beispiel: Nach den folgenden Anweisungen hat a zwei Elemente: ArrayList^ a = gcnew ArrayList(); ArrayList::Synchronized(a)->Add(17); ArrayList^ s = ArrayList::Synchronized(a); s->Add("irgendwas");
Die unerwünschten Effekte beim gemeinsamen Zugriff auf Daten lassen sich dadurch vermeiden, dass man die Threads so synchronisiert, dass immer nur ein Thread auf die gemeinsamen Daten zugreifen kann. Dazu gibt es verschiedene Möglichkeiten zur, von denen die wichtigsten nur kurz skizziert werden sollen: – Die Klasse Monitor (Namensbereich System::Threading) enthält die Elementfunktionen
static void Enter(Object^ obj) static void Exit(Object^ obj); mit denen man sicherstellen kann, dass nur ein einziger Thread Zugriff auf das als Argument übergeben Object hat. Falls ein Thread einen kritischen Bereich mit Enter reserviert hat, muss jeder andere Thread, der Enter mit diesem Object ausführen will, so lange warten, bis dieser kritische Bereich mit Exit wieder freigegeben wird. Falls Enter mehrfach aufgerufen wird, muss Exit genauso oft aufgerufen werden wie Enter, damit der gesperrte Bereich wieder freigegeben wird. Das kann zu Deadlocks führen. Mit
static bool TryEnter(Object^ obj) static bool TryEnter(Object^ obj, int millisecondsTimeout) kann man versuchen, einen kritischen Bereich zu sperren. Falls das nicht möglich ist, wartet diese Funktion aber nicht bzw. nur die als millisecondsTimeout angegeben Zeit und gibt false zurück. Generell sind alle Daten, auf die von mehreren Threads aus zugegriffen werden kann (globale Variable oder static Datenelemente) kritische Bereiche, auf die ein Zugriff aus mehreren parallel ablaufenden Threads verhindert werden muss. Mit der folgenden Variante der Funktion noSync von oben erhält man auch mit mehreren parallel ablaufenden Threads immer das Ergebnis 0: void f_Mon() { using namespace System::Threading; Monitor::Enter(c); c->x=-1; Monitor::Exit(c); int j=0; for (int i=0; ix++; if (c->x!=0&&c->x!=1) j++; c->x=-1; Monitor::Exit(c); } result=j; }
Bei der Definition von kritischen Abschnitten muss immer darauf geachtet werden, dass ein Thread einen reservierten Bereich auch wieder freigibt. Sonst kann der Fall auftreten, dass mehrere Threads darauf warten, dass ein jeweils anderer den reservierten Bereich wieder freigibt (Deadlock). Das gilt insbesondere dann, wenn die Anweisungen im kritischen Bereich eine Exception auslösen können. Dann kann die Freigabe in einem finally-Handler sichergestellt werden: Monitor::Enter(x); try { // x ist gesperrt } finally { Monitor::Exit(x); }
– Die nach #include
im Namensbereich msclr verfügbare Klasse lock erleichtert die Arbeit mit Monitoren, indem sie mit einer Anweisung wie { msclr::lock l(x); // Zugriff auf x } // der Destruktor gibt den lock wieder frei
die im Konstruktor als Argument übergebene Variable mit System::Threading::Monitor::TryEnter
sperrt und im Destruktor wieder freigibt. – Falls mehrere Threads gemeinsame Variable nur lesen, aber nicht verändern, ist ein gemeinsamer Zugriff meist nicht mit Problemen verbunden. Dann ist oft die Klasse ReaderWriterLockSlim angemessen. Sie enthält die Funktionen
void EnterReadLock() bool TryEnterReadLock(int millisecondsTimeout) void ExitReadLock()
10.9 Asynchrone Programmierung und Threads
1183
void EnterWriteLock() bool TryEnterWriteLock(int millisecondsTimeout) void ExitWriterLock(); mit denen kritische Bereiche für Lese- bzw. Schreibzugriffe gesperrt und wieder freigegeben werden können.
ReaderWriterLockSlim steht allerdings nach der Installation von Visual Studio nicht automatisch zur Verfügung, sondern erst nach Projekt|Eigenschaften|Allgemeine Eigenschaften|Neuen Verweis hinzufügen|System Core. In älteren .NET-Versionen war nur die Klasse ReaderWriterLock verfügbar, die ähnlich verwendet werden kann, aber nicht so effizient wie ReaderWriterLockSlim ist. Die Klassen Mutex und Semaphore bieten recht vielseitige Synchronisationsmechanismen, die aber zeitaufwendiger sind als die bisher vorgestellten Klassen. – Mit Objekten der Klasse Mutex kann man einem Thread einen exklusiven Zugriff auf eine Ressource gewähren. Diese Klasse hat Konstruktoren mit und ohne String-Parameter:
Mutex() Mutex(bool initiallyOwned, String^ name) Ohne ein String-Argument erhält man ein lokales Mutex-Objekt, das nur im aktuellen Prozess bekannt ist. Mit einem String erhält man ein globales, das betriebssystemweit bekannt ist und auch zur Synchronisation von Prozessen eingesetzt werden kann. Ein Aufruf einer der WaitOne-Methoden
virtual bool WaitOne() blockiert das Mutex, und ein Aufruf von
void ReleaseMutex() gibt es wieder frei. Beispiel: void f_Mutex() { using namespace System::Threading; Mutex^ mutex=gcnew Mutex; mutex->WaitOne(); c->x=-1; mutex->ReleaseMutex(); int j=0; for (int i=0; iWaitOne(); c->x++; if (c->x!=0&&c->x!=1) j++; c->x=-1;
1184
10 Einige Elemente der .NET-Klassenbibliothek
mutex->ReleaseMutex(); } result=j; }
– Mit der Klasse Semaphore kann die Anzahl der Threads auf eine bestimmte Zahl begrenzt werden, die gemeinsam einen kritischen Bereich betreten können. Diese Anzahl wird im Konstruktor als Argument für maximumCount angegeben:
Semaphore(int initialCount, int maximumCount) Semaphore(int initialCount, int maximumCount, String^ name) Als Argument für initialCount wird normalerweise derselbe Wert wie für maximumCount angegeben. initialCount initialisiert einen internen Zähler, der angibt, wie viele Threads den kritischen Bereich noch betreten können. Kritische Bereiche werden durch Aufrufe der Methoden
virtual bool WaitOne() int Release() begrenzt. Falls der interne Zähler beim Aufruf von WaitOne größer als 0 ist, kann der Bereich betreten werden. Der Zähler wird dann um 1 reduziert. Durch einen Aufruf von Release wird der kritische Bereich wieder verlassen und der Zähler um 1 erhöht. Semaphoren und Mutexe unterscheiden sich insbesondere dadurch, dass ein Mutex nur von dem Thread wieder freigegeben werden kann, der ihn angefordert hat. Ein Semaphor kann dagegen auch von einem anderen Prozess wieder freigegeben werden. Das kann leicht zu unübersichtlichen Programmen führen oder für bösartige Angriffe missbraucht werden, falls die Zugriffsrechte nicht begrenzt werden. Deswegen wird auch oft empfohlen, Semaphoren nach Möglichkeit zu vermeiden. – Wenn mehrere Threads nur einfache Operationen mit gemeinsamen Daten durchführen (z.B. Flags setzen oder Zähler erhöhen), kann der Aufwand für eine Synchronisation mit Monitor usw. unverhältnismäßig groß sein. Dann kann der Zugriff auf die gemeinsamen Daten mit den Methoden der Klasse Interlocked aus dem Namensbereich System::Threading schneller sein. Interlocked stellt die folgenden Methoden als Operationen zur Verfügung, die nicht unterbrochen werden können und die deshalb beim Zugriff auf gemeinsame Daten auch nicht synchronisiert werden müssen.
Methode (die ersten drei auch für long long) static int Add(int% location1, int value) static int Decrement(int% location)
Beschreibung addiert value zu location1 subtrahiert 1
10.9 Asynchrone Programmierung und Threads
Methode (die ersten drei auch für long long) static int Increment(int% location) static long long Read(long long% location)
1185
Beschreibung addiert 1 liest 64-bit Wert
Die nächsten beiden Methoden stehen außer für int und long long auch noch für weitere Datentypen zur Verfügung:
static int Exchange(int% location1, int value) // setzt location1 auf value static int CompareExchange(int% location1, int value, int comparand) // wie: if (location1==comparand) location1=value; Beispiel: Die Funktion void Action() { // i soll eine int-Variable sein System::Threading::Interlocked::Increment(i); }
kann von mehreren Threads aufgerufen werden. Die nächste Tabelle enthält die Laufzeiten für die Beispiele. Sie soll vor allem zeigen, dass die Synchronisation mit einem gewissen Aufwand verbunden ist. Die großen Laufzeiten ergeben sich vor allem daraus, dass zwei Threads sehr oft und nahezu ausschließlich auf dieselben Variablen zugreifen. Das ist in der Praxis aber normalerweise nicht gegeben. Bei vernünftig gestalteten Threads ist der Synchronisationsaufwand oft viel geringer und fällt kaum ins Gewicht.
Beispiel, n=1.000.000 noSync f_Mon f_lock f_RWL_Slim f_Mutex f_Semaphore f_Interlocked
Laufzeit VS 2008, Release 0,009 Sek. 0,06 Sek. 0,2 Sek. 0,2 Sek. 16 Sek. 16 Sek. 0,06 Sek.
10.9.8 ThreadPool Jeder Prozess (unter der CLR) hat einen sogenannten Thread Pool, dessen Threads vom System verwaltet werden. Diesen Threads können Aufgaben zugewiesen werden, ohne dass dazu Threads erzeugt werden müssen (was einige Millisekunden dauern kann und bei kurzen Aufgaben unverhältnismäßig lang sein kann). Nachdem ein Thread aus dem Thread Pool seine Aufgabe beendet hat, wird der Thread nicht zerstört, sondern in den Zustand Suspended gesetzt. Mit der Zuweisung einer neuen Aufgabe wird er wieder aktiviert.
1186
10 Einige Elemente der .NET-Klassenbibliothek
Der Thread Pool ist vor allem für kurze Aufgaben gedacht und auf Geschwindigkeit optimiert. Seine Threads sind einfacher als die der Klasse Thread und bieten nur eingeschränkte Konfigurationsmöglichkeiten. Threads im Thread Pool – kann keine Priorität zugeordnet werden – sind immer Hintergrund-Threads – sind in ihrer Anzahl (Eigenschaft ThreadPool::GetMaxThreads) begrenzt. Weist man ihnen längere Aufgaben zu, kann das eine verzögerte Ausführung anderer Threads des Thread Pools zur Folge haben. Der Thread Pool wird von einigen .NET-Klassen verwendet. Typische Anwendungen sind potentiell zeitaufwendige Funktionen, die als Thread ausgeführt werden, ohne dass man ein Thread-Objekt anlegen und starten muss. Einige Beispiele: – Weist man den Timer-Klassen aus System::Threading und System::Timers (siehe Abschnitt 10.8.3) eine Aufgabe zu, wird diese einem Thread im Thread Pool zugewiesen, der dann zyklisch aktiviert wird. – Auch asynchrone Datei- oder Netzwerk-Operationen wie BeginRead und EndRead können den Thread Pool verwenden. Mit der Klasse ThreadPool und ihren Methoden wie
static bool QueueUserWorkItem(WaitCallback^ callBack) kann man dem ThreadPool eigene Funktionen hinzufügen, die zu
delegate void WaitCallback(Object^ state) passen. Beispiel: Eine Methode wie static void Action1(Object^ o) { System::Threading::Interlocked::Increment(count); }
wird durch ThreadPool::QueueUserWorkItem(gcnew WaitCallback( ThreadTest::Action1));
dem ThreadPool hinzugefügt und dann ausgeführt.
10.10 Grafiken zeichnen mit PictureBox und Graphics
1187
10.10 Grafiken zeichnen mit PictureBox und Graphics In diesem Abschnitt werden einige Klassen vorgestellt, mit denen man Grafiken darstellen und zeichnen kann.
10.10.1 Grafiken mit einer PictureBox anzeigen Mit einer PictureBox (Abschnitt „Common Controls“) kann man Grafiken anzeigen, die in einem der Formate JPG, GIF (den verbreiteten komprimierten Bildformaten), ICO (Icon), WMF (Windows Metafile), BMP (Bitmap) oder PNG als Bilddateien vorliegen. Dazu wird die Grafik entweder zur Entwurfsbei der Eigenschaft Image zeit im Eigenschaftenfenster durch Anklicken von ausgewählt oder zur Laufzeit mit ihrer Methode Load geladen: private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ { pictureBox1->Load("c:\\windows\\feder.bmp"); pictureBox2->Load("file:///c:/windows/feder.bmp"); pictureBox3->Load("http://gcc.gnu.org/gcc.png"); };
e)
Wie dieses Beispiel zeigt, kann der Dateiname auch eine URL (Uniform Resource Locator) sein. Deshalb können mit einer PictureBox nicht nur Bilddateien angezeigt werden, die auf dem Rechner vorhanden sind, sondern auch solche aus dem Internet. Wie in pictureBox2 können auch Dateien auf dem Rechner mit einer URL bezeichnet werden. Dabei wird „/“ anstelle von „\“ verwendet.
10.10.2 Grafiken auf einer PictureBox und anderen Controls zeichnen Die Klasse Graphics aus dem Namensbereich System::Drawing stellt eine GDI+ Zeichenfläche dar. Sie und enthält zahlreiche Elementfunktionen wie z.B.
void DrawLine(Pen^ pen, int x1, int y1, int x2, int y2); // Zeichnet eine Linie // von (x1,y1) nach (x2,y2). mit denen man Linien, Kreise, Rechtecke, Text usw. zeichnen kann. Alle Koordinaten sind in Pixeln angegeben und beziehen sich auf die Zeichenfläche. Dabei ist der Nullpunkt (0,0) links oben und nicht (wie in der Mathematik üblich) links unten. Die Höhe und Breite der Zeichenfläche erhält man als float-Wert über die Methoden Width und Height der Eigenschaft
property RectangleF ClipBounds Der Zeichenstift wird durch ein Objekt der Klasse Pen dargestellt, das durch einen Konstruktor wie
1188
10 Einige Elemente der .NET-Klassenbibliothek
Pen(Color color, float width) mit einer Farbe und einer Strichstärke initialisiert werden kann (siehe auch Abschnitt 10.10.5). Beispiel: Die Funktion zeichneDiagonale zeichnet eine Diagonale von links oben (Koordinaten x=0, y=0) nach rechts unten (Koordinaten x=ClipBounds.Width, y=ClipBounds.Height) auf die als Argument übergebene Zeichenfläche g. void zeichneDiagonale(Graphics^ g) { Pen^ pen=gcnew Pen(Color::Red, 1); Point start = Point(0,0); Point end = Point(g->ClipBounds.Width, g->ClipBounds.Height); g->DrawLine(pen, start,end); delete pen; // nicht notwendig }
Die Zeichenfläche einer PictureBox wird im EventHandler e für das Ereignis Paint übergeben. Diese Funktion wird von Visual Studio nach einem Doppelklick auf das Ereignis Paint im Eigenschaftenfenster erzeugt. Ruft man eine Zeichenfunktion wie DrawLine mit der Zeichenfläche e->Graphics auf, wird die Figur auf dieser Zeichenfläche gezeichnet: private: System::Void pictureBox1_Paint( System::Object^ sender, System::Windows::Forms::PaintEventArgs^ { e->Graphics->Clear(Color::White); if (textBox1->Text=="d") zeichneDiagonale(e->Graphics); else if (textBox1->Text=="f") zeichneFunction(e->Graphics, e->ClipRectangle); }
e)
Dieses Ereignis wird von Windows immer dann automatisch ausgelöst, wenn die PictureBox neu gezeichnet werden muss (z.B. nachdem sie durch ein anderes Fenster verdeckt war). Es kann aber auch vom Anwender durch einen Aufruf der Funktion Invalidate ausgelöst werden: private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ { this->pictureBox1->Invalidate(); }
e)
Dieser Aufruf bewirkt dann, dass die Funktion neu gezeichnet wird. Auf diese Weise kann der Anwender steuern, welche Figuren gezeichnet werden.
10.10 Grafiken zeichnen mit PictureBox und Graphics
1189
Ruft man die Funktion Invalidate ohne Argumente auf, wird das gesamte Steuerelement neu gezeichnet. Falls die Grafik nur ein Teil des Steuerelements ist, kann man Invalidate mit dem Rechteck aufrufen, das neu gezeichnet werden soll, und so etwas Zeit sparen. Als Zeichenfläche steht aber nicht nur eine PictureBox zur Verfügung, sondern jede von der Klasse Control abgeleitete Klasse. Für jedes Objekt einer solchen Klasse kann man mit der von Control geerbten Elementfunktion
Graphics^ CreateGraphics () eine Zeichenfläche erzeugen, auf die man wie bei einer PictureBox im EventHandler zeichnen kann. Die so erzeugte Zeichenfläche muss wieder freigegeben werden, wenn sie nicht mehr benötigt wird. Beispiel: Ruft man die Funktion void zeichneDiagonale(Control^ c, Graphics^ p) { Graphics^ g=c->CreateGraphics(); ZeichneDiagonale(g); delete g; nicht notwendig }
im Paint-EventHandler eines Buttons auf, wird eine Diagonale auf den Button gezeichnet: private: System::Void button1_Paint( System::Object^ sender, System::Windows::Forms::PaintEventArgs^ { zeichneDiagonale(button1,e->Graphics); }
e)
10.10.3 Welt- und Bildschirmkoordinaten Meist ist der Bereich, der gezeichnet werden soll (das sogenannte Weltkoordinatensystem), nicht mit dem Bereich der Bildschirmkoordinaten identisch. Wenn man z.B. die Funktion sin im Bereich von –1 bis 1 zeichnen will, würde man nur relativ wenig sehen, wenn man die Weltkoordinaten nicht transformiert.
1190
10 Einige Elemente der .NET-Klassenbibliothek
Weltkoordinaten
Bildschirmkoordinaten
Y1
0
Y0
H X0
X1
0
W
Durch die folgenden linearen Transformationen werden Weltkoordinaten in Bildschirmkoordinaten abgebildet int x_Bildschirm(double x,double x0, double x1, double W) { // transformiert x aus [x0,x1] in Bildkoordinaten [0,W] return (x–x0)*W/(x1–x0); } int y_Bildschirm(double y,double y0, double y1, double H) { // transformiert y aus [y0,y1] in Bildkoordinaten [H,0] return (y–y1)*H/(y0–y1); }
und umgekehrt: double x_Welt(int px, double x0, double x1, double W) {//transformiert px aus [0,W] in Weltkoordinaten [x0,x1] return x0 + px*(x1–x0)/W; } double y_Welt(int py, double y0, double y1, double H) {//transformiert py aus [0,H] in Weltkoordinaten [y1,y0] return y1 + py*(y0–y1)/H; }
Mit diesen Transformationen kann man eine Funktion y=f(x) folgendermaßen zeichnen: – Zu jedem x-Wert eines Pixels px der Zeichenfläche bestimmt man die Weltkoordinaten x. – Zu diesem x berechnet man den Funktionswert y=f(x). – y transformiert man dann in Bildschirmkoordinaten py. – ab px=1 verbindet den Punkt (px,py) mit einer Geraden mit dem Punkt, den man im vorherigen Schritt (mit px-1) berechnet hat. Nach diesem Verfahren wird in zeichneFunktion die Funktion sin(x*x) im Bereich –4 bis 4 gezeichnet:
10.10 Grafiken zeichnen mit PictureBox und Graphics
1191
void zeichneFunktion(Graphics^ g) { // zeichnet die Funktion y=sin(x*x) im Bereich [–4,4] double x0=-4, y0=-1, x1=4, y1=1;// Weltkoordinaten RectangleF r=g->ClipBounds; Pen^ pen=gcnew Pen(Color::Red,1); int py_alt=0; for (int px=0; px0) g->DrawLine(pen,px-1,py_alt,px,py); py_alt=py; } delete pen; }
Ruft man diese Funktion wie oben im Paint-EventHandler einer PictureBox auf, wird diese Funktion in die PictureBox gezeichnet. Die Gitterlinien der folgenden Abbildung ergeben sich als Lösung der Aufgabe 2:
Solche Transformationen sind auch mit den Graphics-Funktionen möglich, deren Namen „Transform“ enthalten. Durch
public void TranslateTransform( float dx, float dy); wird der Ursprung des Koordinatensystems in den Punkt (dx,dy) gelegt, und durch
public void ScaleTransform( float sx, float sy); die x- bzw. y-Achse um die Faktoren sx bzw. sy skaliert. Nach einer solchen Transformation beziehen sich alle Positionsangaben auf das transformierte Koordinatensystem. Beispiel: Die Funktion
1192
10 Einige Elemente der .NET-Klassenbibliothek
void transformBeispiel(Graphics^ g) { Pen^ blackPen=gcnew Pen(Color::Black,1); g->DrawLine(blackPen,Point(0,0),Point(100,100)); g->TranslateTransform(20,50); g->DrawLine(blackPen,Point(0,0),Point(70,70)); g->ScaleTransform(1,-1); g->DrawLine(blackPen,Point(0,0),Point(40,40)); }
zeichnet drei verschieden lange Geraden ab dem Punkt (0,0). Da vor der zweiten und dritten Linie aber das Koordinatensystem transformiert wurde, erhält man drei Linien, die sich nicht verdecken:
Die nächsten beiden Transformationen setzen den Ursprung des Koordinatensystems wie üblich auf die linke untere Ecke der Zeichenfläche: void transformToUsualKoordinates1(Graphics^ g) { g->TranslateTransform(0,g->ClipBounds.Height); g->ScaleTransform(1,-1); }
Transformationen können über die Eigenschaft
property Matrix^ Transform auch durch Transformationsmatrizen des Typs Matrix dargestellt werden. Eine solche Matrix enthält sechs Werte, die im Konstruktor
Matrix(float m11, float m12, float m21, float m22, float dx, float dy) sowie über zahlreiche Elementfunktionen gesetzt werden können. Die ersten vier dieser Werte sind die Elemente einer Drehmatrix m11=cos ij
m12=–sin ij m21=sin ij
m22= cos ij
und die letzten beiden stellen einen Verschiebungsvektor dar. Die nächste Transformation setzt den Ursprung des Koordinatensystems ebenfalls auf die linke untere Ecke der Zeichenfläche:
10.10 Grafiken zeichnen mit PictureBox und Graphics
1193
void transformToUsualKoordinates2(Graphics^ g) { using namespace System::Drawing::Drawing2D; g->Transform=gcnew Matrix(1,0,0,-1,0,g->ClipBounds.Height); }
10.10.4 Figuren Die Klasse Graphics hat zahlreiche Methoden, mit denen man Figuren zeichnen kann. Die meisten Funktionen, deren Name mit „Draw“ beginnt, haben einen Pen (siehe Abschnitt 10.10.5) als Argument, mit dem die Randlinie der Figur gezeichnet wird. Einige Beispiele:
void DrawRectangle(Pen pen, int x, int y, int width, int height) // Zeichnet ein // Rechteck mit den Koordinaten (x,y) links oben und der angegeben Breite // und Höhe. void DrawEllipse(Pen^ pen, int x, int y, int width, int height) // Zeichnet eine // Ellipse, die von einem Rechteck mit den Koordinaten (x,y) links oben // und der angegeben Breite und Höhe umgeben wird. void DrawLines(Pen^ pen, array^ points) // Zeichnet eine Folge von Linien, die ein Array von Punkten verbinden void DrawString(String^ s, Font^ font, Brush^ brush, PointF point) // Zeichnet den als String übergebenen Text Die meisten Methoden, deren Name mit „Fill“ beginnt, haben einen Brush (siehe Abschnitt 10.10.5) als Argument, mit dem das Innere der Figur gefüllt wird. Einige Beispiele:
void FillRectangle(Brush^ brush, float x, float y, float width, float height); void FillEllipse(Brush^ brush, float x, float y, float width, float height); publicvoid FillPolygon(Brush^ brush, Point points[], FillMode fillMode); 10.10.5 Farben, Stifte und Pinsel In den Klassen und Methoden von System::Drawing wird eine Farbe durch die Klasse Color dargestellt. Viele Farben stehen über vordefinierte Werte wie
Color::Red zur Verfügung. Mit den FromArgb-Methoden von Color kann man eine Farbe als beliebige Kombination der Intensität von Rot-, Grün- und Blauanteilen (jeweils ein Byte, d.h. im Bereich von 0..255 dezimal oder 0..FF hexadezimal) definieren:
static Color FromArgb(int red, int green, int blue);
1194
10 Einige Elemente der .NET-Klassenbibliothek
Diese Farbanteile kann man mit den Eigenschaften
property unsigned char R; // Rot-Anteil property unsigned char G; // Grün-Anteil property unsigned char B; // Blau-Anteil auslesen. Beispiel: Mit den folgenden Farbanteilen erhält man die angegebenen Farben: Color Color Color Color
black=Color::FromArgb(0,0,0); red= Color::FromArgb(255, 0, 0); green=Color::FromArgb(0, 0xFF, 0); blue= Color::FromArgb(0, 0xFF, 0xFF);
Eine Farbe enthält außerdem noch einen alpha-Wert. Dieser Wert im Bereich 0..255 gibt an, wie stark eine mit dieser Farbe gezeichnete Figur den Hintergrund verdeckt: Mit dem alpha-Wert 255 wird der Hintergrund vollständig verdeckt, während er mit kleineren Werten durchscheint. Mit dem alpha-Wert 0 wird der Hintergrund überhaupt nicht verdeckt, die gezeichnete Figur ist unsichtbar. Der alpha-Wert kann z.B. mit der folgenden Variante von
static Color FromArgb(int alpha, int red, int green, int blue); gesetzt werden. Falls er nicht gesetzt wird, ist er 255. Die Graphics-Methoden wie DrawLine verwenden zum Zeichnen einen als Parameter übergebenen Zeichenstift des Datentyps Pen. Die Klasse Pen enthält unter anderem die Eigenschaften:
property Color Color; // Farbe, z.B. Color::Red property DashStyle DashStyle; // Art der Linie: z.B. durchgehend oder aus // Punkten und/oder Strichen zusammengesetzt property float Width; // für die Strichdicke Für DashStyle sind nach using namespace System::Drawing::Drawing2D;
unter anderem die folgenden Werte möglich:
DashStyle::Solid: Eine durchgehende Linie DashStyle::Dash. Eine Linie, die aus Strichen besteht. DashStyle::DashDot: Eine Linie aus Strichen und Punkten. Beispiele: Die Funktion void showDashStyle(Graphics^ g, float p, Drawing2D::DashStyle ds) {
10.10 Grafiken zeichnen mit PictureBox und Graphics
1195
Pen^ pen=gcnew Pen(Color::Red, p/5); pen->DashStyle=ds; g->DrawLine(pen,Point(20,p),Point(200,p)); delete pen; }
zeichnet eine Linie mit dem als Parameter übergebenen DashStyle. Ruft man diese Funktion wie in void showDashStyles(Graphics^ g) { showDashStyle(g, 20, DashStyle::Dash); showDashStyle(g, 30, DashStyle::DashDot); showDashStyle(g, 40, DashStyle::DashDotDot); }
auf, erhält man diese Linien:
Die von der abstrakten Basisklasse Brush abgeleiteten Klassen stellen einen Pinsel dar, mit dem das Innere von Figuren gefüllt werden kann (siehe Abschnitt 10.10.4). Solche Pinsel kann man z.B. mit den folgenden Konstruktoren erzeugen:
SolidBrush(Color color) // einfarbiger Pinsel TextureBrush(Image^ image, Rectangle dstRect) // Bild zum Füllen LinearGradientBrush(Point point1, Point point2, Color color1, Color color2) 10.10.6 Text zeichnen Mit den verschiedenen DrawString-Funktionen der Klasse Graphics kann man einen als String übergeben Text auf der Zeichenfläche ausgegeben. Bei
void DrawString(String^ s, Font^ font, Brush^ brush, float x, float y) sind (x,y) die Koordinaten (in Pixeln) der Position, an der der String ausgegeben wird. Will man mehrere Strings in aufeinander folgende Zeilen schreiben, muss man die y-Koordinate jedes Mal entsprechend erhöhen. Dazu bietet sich die Funktion
float GetHeight() der Klasse Font an, die den Zeilenabstand der Schriftart zurückgibt. Damit schreiben die Anweisungen der Funktion printOnePage die ersten Zeilen eines Text-StreamReaders (siehe Abschnitt 10.14.1) auf eine Graphics-Zeichenfläche:
1196
10 Einige Elemente der .NET-Klassenbibliothek
bool printOnePage(Graphics^ g, StreamReader^ textstream, float leftMargin, float topMargin, float pageHeight, System::Drawing::Font^ font) { int lineNo = 0; // Calculate the number of lines per page: float linesPerPage = pageHeight/font->GetHeight(g); while (lineNo < linesPerPage && !txtstream->EndOfStream) { // print line from file. float y = topMargin + lineNo*font->GetHeight(); String^ line = textstream->ReadLine(); g->DrawString(line,font,Brushes::Black,leftMargin,y); lineNo++; } return !textstream->EndOfStream; }
Die Parameter haben die folgende Bedeutung:
leftMargin: Abstand vom linken Rand topMargin: Abstand vom oberen Rand pageHeight: Seitenhöhe font: Schriftart Diese Funktion kann mit einem StreamReader (siehe Abschnitt 10.14.1) aufgerufen werden, der z.B. folgendermaßen definiert wird: StreamReader^ t=gcnew StreamReader("c:\\Faust.txt");
Sie wird im nächsten Abschnitt verwendet, um eine Textdatei auszudrucken.
10.10.7 Drucken mit Graphics Die unter Windows verfügbaren Drucker können über die Klasse PrintDocument (Toolbox, Registerkarte „Drucken“) angesprochen und zum Drucken verwendet werden. Dazu kann man folgendermaßen vorgehen: Zuerst setzt man ein PrintDocument auf das Formular, und erzeugt dann durch einen Doppelklick auf das Ereignis PrintPage im Eigenschaftenfenster die folgende Ereignisbehandlungsroutine: private: System::Void printDocument1_PrintPage( System::Object^ sender, System::Drawing::Printing::PrintPageEventArgs^ e) { }
In dieser Funktion steht über den Parameter e die Zeichenfläche Graphics des Druckers zur Verfügung, die man mit e->Graphics
10.10 Grafiken zeichnen mit PictureBox und Graphics
1197
ansprechen kann. Diese Zeichenfläche stellt eine Seite (ein Blatt Papier) dar, auf die man mit den Graphics-Funktionen zeichnen kann. Über die PrintPageEventArgs-Eigenschaften kann man die Einstellungen des Druckers abfragen:
MarginBounds: Größe des Graphics-Bereichs innerhalb der Ränder PageBounds: Größe des Graphics-Bereichs einer Druckseite PageSettings: Seiteneinstellungen Durch die PrintPage-Ereignisbehandlungsroutine wird immer nur eine einzige Seite gedruckt. Falls danach noch eine weitere Seite gedruckt werden soll, muss die boolesche Eigenschaft HasMorePages auf true gesetzt werden. Die folgende PrintPage-Ereignisbehandlungsroutine verwendet einen in der Formularklasse definierten StreamReader^ txtstream;
der die auszudruckende Textdatei darstellt, und druckt alle Seiten dieser Datei aus: private: System::Void printDocument1_PrintPage( System::Object^ sender, System::Drawing::Printing::PrintPageEventArgs^ e) { float leftMargin=e->MarginBounds.Left; float topMargin= e->MarginBounds.Top; float pageHeight=e->MarginBounds.Height; System::Drawing::Font^ font= gcnew System::Drawing::Font("Arial",10); e->HasMorePages=printOnePage(e->Graphics, txtstream,leftMargin, topMargin, pageHeight,font); }
Diese Funktion wird dann von der Methode Print des PrintDocument aufgerufen: void printFile(String^ FileName) { txtstream = gcnew StreamReader(FileName); try { printDocument1->Print(); // Message ); } finally { txtstream->Close(); } }
Die Funktion printFile druckt dann die als Parameter übergebene Datei aus. Wenn man den Drucker in einem PrintDialog auswählen will, muss man das PrintDocument nur noch der Eigenschaft Document des PrintDialogs zuweisen:
1198
10 Einige Elemente der .NET-Klassenbibliothek
printDialog1->Document=printDocument1; if (printDialog1->ShowDialog()==::DialogResult::OK) printFile("..\\Text\\Faust.txt");
In Abschnitt 10.11 wird gezeigt, wie man auf einfache Weise Textdateien im Format von Microsoft Word erstellen und ausdrucken kann.
Aufgaben 10.10 1. Schreiben Sie eine Funktion, printOnePage, die ab der aktuellen Position in einer Textdatei so viele Zeilen auf einen Graphics-Parameter ausdruckt, wie auf der aktuellen Seite Platz ist. Verwenden Sie zum Lesen der Datei einen StreamReader, der die Zeilen der Datei als String einliest. Durch den Aufruf der Print-Methode der PrintDocument-Komponente soll eine ganze Textdatei ausgedruckt werden. 2. Schreiben Sie ein Programm, mit dem man mathematische Funktionen der Form y=f(x) zeichnen kann. a) Überarbeiten Sie die Funktion zeichneFunktion von Abschnitt 10.10.3 so, dass sie in Abhängigkeit von einem als Parameter übergebenen Wert eine der folgenden Funktionen zeichnet:
1 2 3 4 5
Funktion y=sin (x*x) y=exp(x) y=x*x y=1/(1+x*x) y=x*sin(x)
y1 dx x0 y0 x1 –4 –1 4 1 1 –1 0 6 100 1 –2 –2 2 4 1 0 0 4 1 1 –2 –6 8 10 1
dy 0.2 10 1 0.2 1
Die Eckpunkte des Weltkoordinatensystems x0, y0, x1, y1 sollen als Parameter übergeben werden. b) Schreiben Sie eine Funktion zeichneGitternetz, die auf einem GraphicsParameter ein graues Gitternetz (Farbe Color::Gray) zeichnet. Dazu kann man ab dem linken Bildrand (in Weltkoordinaten: x0) jeweils im Abstand dx Parallelen zur y-Achse zeichnen. Entsprechend ab y0 im Abstand dy Parallelen zur x-Achse. Die Werte x0, x1, dx, y0, y1 und dy sollen als Parameter übergeben werden. Falls die x- bzw. y-Achse in den vorgegebenen Weltkoordinaten enthalten sind, soll durch den Nullpunkt ein schwarzes Koordinatenkreuz gezeichnet werden. Außerdem sollen die Schnittpunkte der Achsen und der Gitterlinien mit den entsprechenden Werten beschriftet werden.
10.10 Grafiken zeichnen mit PictureBox und Graphics
1199
Falls die x- bzw. y-Achse nicht sichtbar ist, soll diese Beschriftung am Rand erfolgen. c) An zwei überladene Versionen der Funktion zeichneFunktion soll die zu zeichnende Funktion als Funktionszeiger (siehe Abschnitt 5.2) des Typs „double (*) (double)“ und als Delegat-Typ übergeben werden. Diesen Versionen von zeichneFunktion soll die linke und rechte Grenze des Bereichs, in dem sie gezeichnet werden soll, als Parameter übergeben werden. Der minimale und maximale Funktionswert soll in zeichneFunktion berechnet werden. Die zu zeichnende Funktion soll die Graphics-Zeichenfläche von unten bis oben ausfüllen. Auf die Zeichenfläche soll außerdem ein Gitternetz mit z.B. 10 Gitterlinien in x- und y-Richtung gezeichnet werden. Testen Sie diese Funktion mit den Funktionen aus a). 3. Der Binomialkoeffizient bin(n,k) ist der k-te Koeffizient von (a+b)n in der üblichen Reihenfolge beim Ausmultiplizieren: (a+b)1 = 1*a+1*b, d.h. bin(1,0)=1 und bin(1,1)=1 (a+b)2 = 1*a2+ 2*ab+1*b2, d.h. bin(2,0)=1, bin(2,1)=2 und bin(2,2)=1 (a+b)3 = 1*a3+3*a2b+3*ab2+1*b3, d.h. bin(3,0)=1, bin(3,1)=3 usw. Für einen Binomialkoeffizienten gilt die folgende Formel: bin(n,k) = n!/(k!*(n–k)!) a) Schreiben Sie eine Funktion bin(n,k). Zeigen Sie die Werte von bin(n,k) für k=1 bis n und für n=1 bis 30 in einer TextBox an. b) Wenn man als Zufallsexperiment eine Münze wirft und das Ergebnis „Kopf“ mit 0 und „Zahl“ mit 1 bewertet, sind die beiden Ergebnisse 0 und 1 möglich. Wirft man zwei Münzen, sind die Ergebnisse (0,0), (0,1), (1,0) und (1,1) möglich. Damit ist die Anzahl der Ereignisse, die zu den Summen S=0, S=1 und S=2 führen, durch bin(2,0) = 1, bin(2,1) = 2 und bin(2,2) = 1 gegeben. Es lässt sich zeigen, dass diese Beziehung ganz allgemein gilt: Beim n-fachen Werfen einer Münze ist die Anzahl der Ereignisse, die zu der Summe S=k führt, durch bin(n,k) gegeben (Binomialverteilung). Stellen Sie die Binomialverteilung durch Histogramme grafisch dar:
1200
10 Einige Elemente der .NET-Klassenbibliothek
Anscheinend konvergieren diese Rechtecke gegen eine stetige Funktion. Diese Funktion unter dem Namen Gauß’sche Glockenkurve oder Normalverteilung bekannt (nach dem Mathematiker Gauß). 4. Die bekannten Fraktalbilder der Mandelbrot-Menge (nach dem Mathematiker Benoit Mandelbrot) entstehen dadurch, dass man mit den Koordinaten eines Bildpunktes (x,y) nacheinander immer wieder folgende Berechnungen durchführt: x = x2 – y2 + x y = 2xy + y Dabei zählt man mit, wie viele Iterationen notwendig sind, bis entweder x2 + y2 > 4 gilt oder bis eine vorgegebene maximale Anzahl von Iterationen (z.B. 50) erreicht ist. In Abhängigkeit von der Anzahl i dieser Iterationen färbt man dann den Bildpunkt ein, mit dessen Koordinaten man die Iteration begonnen hat: Falls die vorgegebene maximale Anzahl von Iterationen erreicht wird, erhält dieser üblicherweise die Farbe Schwarz (Color::Black). In allen anderen Fällen erhält der Bildpunkt einen von i abhängigen Farbwert. Färbt man so alle Bildpunkte von x0 = –2; y0 = 1.25; (links oben) bis x1 = 0.5; y1 = –1.25; (rechts unten) ein, erhält man das „Apfelmännchen“.
10.10 Grafiken zeichnen mit PictureBox und Graphics
1201
a) Schreiben Sie eine Funktion zeichneFraktal, die ein solches Fraktal auf eine Graphics-Zeichenfläche zeichnet. Sie können dazu folgendermaßen vorgehen: Jeder Bildpunkt (px,py) der Zeichenfläche wird in die Koordinaten des Rechtecks mit den Eckpunkten (x0,y0) {links oben} und (x1,y1) {rechts unten} mit den Funktionen x_Welt und y_Welt (siehe Abschnitt 10.10.3) transformiert: double x = x_Welt(px,x0,x1, g->ClipBounds.Width); double y = y_Welt(py,y1,y0, g->ClipBounds.Height);
Mit jedem so erhaltenen Punkt (x,y) werden die oben beschriebenen Berechnungen durchgeführt. Einen Farbwert zu der so bestimmten Anzahl i von Iterationen kann man z.B. folgendermaßen wählen: i=i%256; // maximal 255 Farben int r30=i%30; // i->[0..29] int t=255-5*r30; // [0..29]->[255..145] if (iSelection->Font->Bold=true; WordApp->Selection->Font->Size=15; WordApp->Selection->TypeText("Fette Schrift, 15 Punkt"); WordApp->Selection->TypeParagraph(); WordApp->Selection->TypeParagraph(); WordApp->Selection->Font->Bold=false; WordApp->Selection->Font->Size=10; WordApp->Selection->TypeText("Nicht fett, 10 Punkt"); WordApp->Selection->TypeParagraph(); WordApp->ChangeFileOpenDirectory("c:\\test"); String^ fn="test1.doc"; WordApp->ActiveDocument->SaveAs(fn,NoArg, NoArg,NoArg, NoArg,NoArg,NoArg,NoArg,NoArg, NoArg,NoArg,NoArg, NoArg,NoArg,NoArg,NoArg); } catch(Exception^ e) { MessageBox::Show(e->Message); } }
Oft will man diese Funktionen aufrufen, ohne für einen Parameter ein Argument zu übergeben. Das ist nach Object^ NoArg = System::Reflection::Missing::Value;
mit dem Wert NoArg möglich. Durch WordApp->Documents->Add(NoArg,NoArg,NoArg,NoArg);
wird die Funktion
1206
10 Einige Elemente der .NET-Klassenbibliothek
Document^ Add(&Object^ Template, &Object^ NewTemplate, &Object^ DocumentType, &Object^ Visible); aufgerufen, ohne dass für die Parameter ein Argument übergeben wird. Ein solcher Aufruf entspricht meist einer Bestätigung eines entsprechenden Dialogs, bei dem alle voreingestellten Werte akzeptiert werden. Falls beim Aufruf einer Funktion von Word ein Fehler auftritt, wird eine Exception ausgelöst. Deshalb sollte man alle solchen Funktionen in einer try-Anweisung aufrufen.
10.11.2 Excel Für Excel muss man dem Projekt in Visual Studio die Excel-Object Library hinzufügen: – Entweder im Projektmappen-Explorer im Kontextmenü zum Projekt unter Verweise über den Button „Neuen Verweis hinzufügen…“., oder – Projekt|Eigenschaften|Allgemeine Eigenschaften|Verweise|Neuen Verweis hinzufügen
Excel wird dann durch den folgenden Aufruf gestartet und angezeigt:
10.11 Die Steuerung von MS-Office 2003 Anwendungen
1207
void testExcel() { using namespace Microsoft::Office::Interop; try { Excel::ApplicationClass^ ExcelApp = gcnew Excel::ApplicationClass(); ExcelApp->Visible=true; Object^ NoArg = System::Reflection::Missing::Value; // hier die Anweisungen von unten einfügen } catch ( Exception^ e) { MessageBox::Show(e->Message); } }
Eine Arbeitsmappe wird durch ein Objekt der Klasse Workbook dargestellt. Durch Excel::Workbook^ wb=ExcelApp->Workbooks->Add(NoArg);
wird eine neue Arbeitsmappe erzeugt. Eine Arbeitsmappe kann man über ihren Index ansprechen wb=ExcelApp->Workbooks[1]; // Index der ersten: 1
und die aktive Arbeitsmappe erhält man mit der ApplicationClass-Eigenschaft
property Workbook^ ActiveWorkbook Eine Arbeitsmappe enthält Arbeitsblätter des Typs WorkSheet, die man ebenfalls über ihren Index bzw. das aktive Arbeitsblatt ansprechen kann: Excel::Worksheet^ worksheet = dynamic_cast(wb->Worksheets[1]); worksheet=dynamic_cast (ExcelApp->ActiveWorkbook->ActiveSheet);
Die Zellen eines Arbeitsblatts werden durch die Eigenschaft
property virtual Range^ Cells dargestellt, die man mit einer Zeilen- und Spaltennummer indizieren kann. Range ist eine Klasse mit vielen Eigenschaften und Methoden, mit denen Werte, Texte, Formatierungen, Formeln usw. in das Arbeitblatt eingetragen werden können:
property Object^ Formula // Formel property Font^ Font // Schriftart in der Zelle property Object^ Style Fügt man die folgenden Anweisungen in die Funktion testExcel ein, wird der Wert 123 in die Zelle der ersten Zeile und Spalte eingetragen:
1208
10 Einige Elemente der .NET-Klassenbibliothek
Excel::Workbook^ wb=ExcelApp->Workbooks->Add(NoArg); Excel::Worksheet^ ws=dynamic_cast (ExcelApp->Workbooks[1]->Worksheets[1]); ws->Cells[1,1]="123"; Excel::Range^ c= dynamic_cast(ws->Cells[1,1]); c->Font->Size=20;
Aufgabe 10.11 Die Funktionen zur Steuerung von Office-Anwendungen ermöglichen, die Ergebnisse eines Programms in ein Word-Dokument oder ein Excel-Arbeitsblatt zu schreiben und dabei die umfangreichen Möglichkeiten zur Formatierung zu nutzen. Schreiben Sie eine Funktion, die a) ein Word-Dokument b) ein Excel-Arbeitsblatt mit 10 Zeilen erzeugt. Jede Zeile soll die Zahlen i (von 1 bis 10) und i*i enthalten.
10.12 Collection-Klassen Die Namensbereiche System::Collections und System::Collections::Generic enthalten einige Collection-Klassen, die ähnliche Funktionen wie die Container-Klassen der Standardbibliothek (siehe Kapitel 4) zur Verfügung stellen. Die Begriffe „Collection“ und „Container“ sind im Wesentlichen gleichwertig: In der C++Standardbibliothek hat sich „Container“ eingebürgert, während im Umfeld von Visual-C++ der Begriff „Collection“ verbreitet ist. Diese Klassen unterscheiden sich von einem CLI-Array (siehe Abschnitt 9.5) insbesondere dadurch, dass die Anzahl der Elemente bei einem CLI-Array fest ist und nicht verändert werden kann. Bei den Collection-Klassen kann sich die Anzahl der Elemente dagegen während der Laufzeit ändern. Führt man die Anweisungen von Abschnitt 4 2.1 mit CLI-Arrays und einigen .NET Collection-Klassen durch, stellt man teilweise deutliche Unterschiede fest:
.NET ArrayList .NET CLI-Array Visual C++ 2008, List Release (Zeiten in Sek.) array Auswahlsort n=10000 0,12 0,86 0,21 sort(v.begin(),v.end()) 0,002 0,10 0,002
Ein Vergleich mit der Tabelle von Abschnitt 4.2.3 zeigt, dass es in beiden Tabellen Datentypen mit ähnlichen Laufzeiten gibt:
10.12 Collection-Klassen
1209
STL/CLR C-Array Visual C++ 2008, STL vector int a[n] Release (Zeiten in Sek.) checked It. unchecked It. vector Auswahlsort n=10000 0,32 0,13 1,28 0,09 sort(v.begin(),v.end()) 0,0016 0,0034 0,011 0,0016
10.12.1 Generische und nicht generische Collection-Klassen Viele der Collection-Klassen gibt es in zwei Varianten, die im Wesentlichen dieselben Methoden und Eigenschaften haben. Eine nicht-generische Variante, die beliebige Elemente enthalten kann, die von Object abgeleitet sind, und eine generische Variante deren Elemente den Datentyp des Typ-Arguments haben oder von ihm abgeleitet müssen. Die nicht generische Klasse ArrayList ist im Wesentlichen so
public ref class ArrayList : IList, ICollection, IEnumerable, ICloneable definiert und kann Zeiger auf beliebige Elemente enthalten, die von Object abgeleitet sind. Ihre generische Variante ist die Klasse List,
generic public ref class List : IList, ICollection, IEnumerable, IList, ICollection, IEnumerable die im Wesentlichen gleichartige Interface-Klassen implementiert und deswegen auch im Wesentlichen dieselben Eigenschaften und Methoden hat. Sie kann aber nur Elemente enthalten, die den Datentyp des Typ-Arguments haben oder von ihm abgeleitet sind. Diesen Containern können z.B. mit den Methoden
virtual int Add(Object^ value) // ArrayList virtual void Add(T item) sealed // List, dabei ist T der Datentyp der Elemente Elemente hinzugefügt werden. Die Anzahl ihrer Elemente ist der Wert der Eigenschaft Count, und der Zugriff auf die Elemente ist sowohl mit dem Indexoperator als auch in einer for each-Anweisung möglich. Weitere Eigenschaften und Methoden werden in Abschnitt 10.12.2 im Zusammenhang mit den implementierten Interfaces beschrieben. Beispiel: Die nächsten beiden Anweisungsblöcke zeigen, wie man ArrayList- und List-Collections definieren, Elemente in ihnen ablegen und auf die Elemente zugreifen kann. Da eine ArrayList-Collection nicht typsicher ist, sollte man bei jedem Zugriff auf die Elemente prüfen (z.B. mit dynamic_cast), ob die Elemente auch tatsächlich den erwarteten Typ haben:
1210
10 Einige Elemente der .NET-Klassenbibliothek
using namespace System::Collections; ArrayList^ a=gcnew ArrayList; a->Add("Daniel"); a->Add("Alex"); for each(Object^ i in a) if (dynamic_cast(i)) tb->AppendText(dynamic_cast(i)); for (int i=0; iCount; i++) if (dynamic_cast(a[i])) tb->AppendText(dynamic_cast(a[i]));
Ein generischer List-Container ist dagegen typsicher. Deshalb ist keine Prüfung des Datentyps der Elemente notwendig: using namespace System::Collections::Generic; List^ gl = gcnew List; gl->Add("one "); gl->Add("two "); gl->Add("three "); for each(String^ i in gl) textBox1->AppendText(i); for (int i=0; iCount; i++) textBox1->AppendText(gl[i]);
Dieses Beispiel zeigt insbesondere, dass die generischen Collection-Klassen einfacher benutzt werden können als die nicht generischen. Wegen der Typsicherheit der generischen und speziellen Collection-Klassen sollte man diese gegenüber den nicht generischen Collection-Klassen bevorzugen. Außerdem sind die generischen Klassen oft schneller als die nicht generischen (siehe die Benchmarks oben). Deswegen werden im Folgenden vor allem die generischen Klassen vorgestellt.
10.12.2 Generische Interface-Klassen: ICollection und IList Viele generische Collection-Klassen implementieren die generischen Interfaces ICollection und IList. Die Elemente dieser Interface-Klassen stehen deshalb in allen diesen Collection-Klassen zur Verfügung. Die Interface-Klasse
generic public interface class ICollection : IEnumerable, IEnumerable hat die Elemente:
10.12 Collection-Klassen
1211
Element property int Count void Add(T item) void Clear() bool Contains(T item)
Anzahl der Elemente fügt das Argument hinzu löscht alle Elemente gibt true zurück, wenn das Argument in der Queue enthalten ist, und andernfalls false void CopyTo(array^ kopiert die Elemente der Collection in ein Array array, int arrayIndex) Löscht den als Argument übergebenen Wert. Der bool Remove(T item) Rückgabewert ist true, falls das Element gelöscht werden konnte.
Da ICollection von IEnumerable abgeleitet ist, kann man die Elemente in einer for each-Anweisung durchlaufen. Beispiel: Die Funktion testICollection kann mit jedem generischen Container mit String-Elementen aufgerufen werden, der das generische Interface ICollection implementiert: using namespace System::Collections::Generic; void testICollection(TextBox^ tb, ICollection^ c) { c->Clear(); c->Add("Daniel"); c->Add("Alex"); c->Add("Kathy"); bool b1=c->Contains("Daniel"); // true bool b2=c->Contains("Alexx"); // false int n=c->Count; // 3 c->Remove("Kathy"); for each(String^ i in c) tb->AppendText(i); array^ ar=gcnew array(3); c->CopyTo(ar,0); } List q; testICollection(textBox1, %q);
Die Interface-Klasse
generic public interface class IList : ICollection, IEnumerable, IEnumerable erweitert ICollection um die folgenden Elemente:
1212
10 Einige Elemente der .NET-Klassenbibliothek
Element property T default[int]
Mit dieser default-indizierten Eigenschaft (siehe Abschnitt 9.12.2) kann man ein Element über seinen Index ansprechen. Bei einem unzulässigen Index wird eine Exception ausgelöst. Den Index des ersten Elements mit dem als Arguint IndexOf(T item) ment übergebenen Wert. Falls das Element nicht gefunden wird, ist der Rückgabewert –1. Fügt den als Argument übergebenen Wert an der void Insert(int index, T item) Position index ein. In Collection-Klassen mit zusammenhängenden Elementen (wie z.B. List) werden die darauf folgenden Elemente nach hinten verschoben. void RemoveAt(int index) Löscht das Element an der Position index. In Collection-Klassen mit zusammenhängenden Elementen (wie z.B. List) werden die darauf folgenden Elemente nach vorne verschoben.
Beispiel: Die Funktion testIList kann mit jedem generischen Container mit StringElementen aufgerufen werden, der das generische Interface IList implementiert (wie z.B. q aus dem letzten Beispiel): using namespace System::Collections::Generic; void testIList(TextBox^ tb, IList^ c) { testICollection(tb, c);// c: Daniel, Alex c->Insert(0,"Kathy"); // c: Kathy, Daniel, Alex c->RemoveAt(0); // c: Daniel, Alex String^ s=c[0]; // Indexoperator int i=c->IndexOf("Alex"); // 1 }
10.12.3 Die generische Collection-Klasse List List gehört zu den Collection-Klassen mit den meisten Methoden. Sie ist oft die richtige Wahl, wenn man keine triftigen Gründe für eine andere CollectionKlasse findet. Die meisten Collection-Klassen haben mehrere Konstruktoren, mit denen man eine leere Collection mit einer vorgegebenen oder selbstdefinierten Anfangskapazität sowie mit den Elementen einer anderen Collection definieren kann. Bei der Klasse
generic public ref class List : IList, ICollection, IEnumerable, IList, ICollection, IEnumerable sind das:
10.12 Collection-Klassen
1213
List() // erzeugt eine leere List-Collection mit vorgegebener Anfangskapazität List(int capacity) // erzeugt eine leere Collection mit der Kapazität capacity List(IEnumerable^ collection) // fügt die Elemente des Arguments ein Bei den zahlreichen Methoden zum Suchen nach Elementen ist der Parameter
generic public delegate bool Predicate(T obj) ein Delegat-Typ, der die Bedingung für die gesuchten Elemente definiert.
Methode virtual int IndexOf(T item) sealed int LastIndexOf(T item) int IndexOf(T item, int index) int LastIndexOf(T item, int index)
gibt den Index des ersten bzw. letzten Elements mit dem Wert des Arguments zurück, bzw.-1, falls es nicht existiert gibt den Index des ersten bzw. letzten Elements ab der Position index mit dem Wert des Arguments zurück, bzw.-1, falls es nicht existiert T Find(Predicate^ match) gibt das erste bzw. letzte Element zurück, das das als Argument übergebene Prädikat T FindLast(Predicate^ match) erfüllt. List^ gibt eine Liste mit allen Elementen zurück, FindAll(Predicate^ match) die das als Argument übergebene Prädikat erfüllen. gibt den Index des ersten Elements zurück int FindIndex( Predicate^ match) gibt den Index des ersten Elements ab der int FindIndex(int startIndex, Predicate^ match) Position startIndex zurück gibt in einer sortierten Liste den Index eines int BinarySearch(T item) Elements mit dem Wert des Arguments zuint BinarySearch(T item, IComparer^ comparer) rück. Falls das Element nicht gefunden wird, ist der Rückgabewert negativ.
Beispiel: bool p(String^ s) { return s->Length==4; } void searchList() { typedef System::Collections::Generic::List SList; SList^ c=gcnew SList(gcnew array(3) {"Daniel","Alex","Kathy"}); int i1=c->IndexOf("Kathy"); // 2 int i2=c->IndexOf("Alexx"); // -1 using System::Predicate;
1214
10 Einige Elemente der .NET-Klassenbibliothek
String^ s=c->Find(gcnew Predicate(p)); int i3=c->FindIndex(gcnew Predicate(p)); } // s=="Alex", i3==1
Zum Sortieren stehen Methoden mit dem Namen Sort zur Verfügung:
Methode void Sort()
void Sort(Comparison^ comparison)
sortiert die Liste nach der Implementation von IComparable im Datentyp der Listenelemente (siehe Abschnitt 9.8.1) sortiert die Liste gemäß dem Argument für
generic public delegate int Comparison(T x, T y) sortiert die Liste gemäß der Funktion void Sort(IComparer^ int Compare(T x, T y) comparer) der Interface-Klasse IComparer sortiert count Elemente ab dem Index index void Sort(int index, int count, IComparer^ comparer) Dabei müssen die Funktionen Comparison und Compare die folgenden Anforderungen erfüllen: x0 Falls sie diese Anforderungen nicht erfüllen (wie z.B. der Operator Add(gcnew Person("ich","hier")); lp->Add(gcnew Person("du","dort")); dataGridView1->DataSource=lp;
erhält man die Anzeige
Eine differenziertere Darstellung ist mit den zahlreichen Eigenschaften und Methoden von DataGridView möglich. Einige der wichtigsten: Die Anzahl der Zeilen und Spalten der Tabelle wird durch die Eigenschaften
property int RowCount property int ColumnCount dargestellt. Weist man diesen einen Wert zu, erhält man eine Tabelle mit der entsprechenden Anzahl von Zeilen und Spalten. Ein DataGridView ist aus Zeilen (Eigenschaft Rows) aufgebaut, die wiederum aus Zellen (Eigenschaft Cells) bestehen. Die Zelle in der Zeile i und in der Spalte j (beide werden ab 0 gezählt) kann man mit
dataGridView1->Rows[i]->Cells[j] // Datentyp DataGridViewCell bzw. über die default-indizierte Eigenschaft
property DataGridViewCell^ Item[int columnIndex, int rowIndex] wie in dataGridView1[j,i]->Value=(i*j).ToString();
ansprechen (Achtung: hier ist j die Spalte und i die Zeile). Von den zahlreichen Elementen von DataGridViewCell soll nur auf zwei hingewiesen werden:
property Object^ Value // der Wert der Zelle
1216
10 Einige Elemente der .NET-Klassenbibliothek
Da Value den Datentyp Object hat, kann man dieser Eigenschaft Werte aller Datentypen zuweisen, die von Object abgeleitet sind. Die Eigenschaft
property DataGridViewCellStyle^ Style enthält zahlreiche Elemente, um die Hintergrundfarbe, Schriftart, Formatierung usw. der Zelle zu gestalten. Beispiel: Die Tabelle rechts wird durch die folgenden Anweisungen erzeugt: dataGridView1-> ColumnCount=5; dataGridView1-> RowCount=4; for (int i=0; iColumns[i]->Name = "Col-"+Convert::ToString(i); dataGridView1->Columns[i]->Width=50; } for (int i=0; iCells[j]->Value=i*j;
Hier kann man die letzte Zeile auch ersetzen durch dataGridView1[j,i]->Value=i*j;//[Spalte,Zeile]
Da man Value den Wert eines beliebigen Datentyps zuweisen kann, der von Object abgeleitet ist, ist auch die nächste Zuweisung zulässig. dataGridView1->Rows[1]->Cells[0]->Value= "abc";
Aufgabe 10.12.4 Eine Klasse Datum soll aus drei int-Werten für den Tag, den Monat und das Jahr bestehen. Eine Klasse Person soll aus einem String^ für den Namen und einem Datum bestehen. Eine Klasse mit dem Namen Geburtstage soll einen ListContainer mit Elementen des Typs Person enthalten. Eine Methode fuelle soll diesen Container mit einigen Werten füllen. a) Schreiben Sie Delegat-Typen und Implementationen der IComparer InterfaceKlasse, die bei Sort als Argument für comparison bzw. comparer übergeben werden können, und die die Collection nach den folgenden Kriterien sortieren: a1) nach dem Namen a2) nach dem Geburtstagsdatum. Falls zwei Personen dasselbe Geburtstagsdatum haben, soll der Name die Reihenfolge festlegen. b) Suchen Sie mit BinarySearch nach einem Element mit einem Namen, der
10.12 Collection-Klassen
1217
b1) in der Liste enthalten ist b2) nicht in der Liste enthalten ist
10.12.5 Die generische Collection-Klasse Queue Die Klasse Queue
genericpublic ref class Queue : IEnumerable, ICollection, IEnumerable speichert die Elemente in einer First-In-First-Out (FIFO) Liste. Da sie das Interface IEnumerable implementiert, können die Elemente in einer for each-Anweisung durchlaufen werden. Sie hat neben den ICollection-Elementen (siehe Abschnitt 10.12.2) unter anderem diese Methoden:
Methode void Enqueue(T item) T Dequeue() T Peek() array^ ToArray()
fügt der Queue am Ende ein neues Element hinzu entfernt das erste Element aus der Queue Gibt das erste Element zurück, ohne es zu entfernen Gibt die Elemente als Array zurück
Beispiel: Die Anweisungen using namespace System::Collections::Generic; Queue q; q.Enqueue(1); q.Enqueue(2); q.Enqueue(3); for each(int i in q) textBox1->AppendText((i).ToString());
geben die folgenden Werte aus: 123
10.12.6 Die generische Collection-Klasse HashSet Die Klasse HashSet
genericpublic ref class HashSet : ICollection, IEnumerable, IEnumerable, ISerializable, IDeserializationCallback steht erst nach dem Hinzufügen von System.Core unter Projekt|Eigenschaften|Allgemeine Eigenschaften|Neuen Verweis hinzufügen zur Verfügung (ab .Net 3.5). Sie stellt eine Menge von Werten dar, die keine Duplikate enthält. Fügt man einem HashSet mit Add einen Wert hinzu, der bereits enthalten ist, ist dieser Wert an-
1218
10 Einige Elemente der .NET-Klassenbibliothek
schließend nur einmal enthalten. Da diese Klasse als Hash-Tabelle implementiert ist, ist die Laufzeit für die Funktion Contains (mit der man prüfen kann, ob ein Wert enthalten ist) von der Anzahl der Elemente unabhängig. Die als Hash-Tabelle implementierten Collection-Klassen (HashSet und Dictionary) haben Konstruktoren, denen eine Implementation der IEqualityComparerKlasse übergeben werden kann. Diese Implementation muss die Methoden Equals und GetHashCode implementieren. Dann wird diese GetHashCode-Methode als Hash-Funktion verwendet. Definiert man die Hash-Tabelle dagegen nicht mit einem solchen Konstruktor, wird die Methode GetHashCode des Typs der Elemente als Hash-Funktion verwendet. Die von der Klasse Object geerbten Methoden sind auch bei selbstdefinierten Datentypen oft ausreichend (siehe Abschnitt 9.3). Normalerweise besteht keine Notwendigkeit, Equals und GetHashCode selbst zu implementieren
10.12.7 Die generische Collection-Klasse LinkedList Die Klasse LinkedList
public ref class LinkedList : ICollection, IEnumerable, ICollection, IEnumerable, ISerializable, IDeserializationCallback speichert die Elemente in einer doppelt verketteten Liste. Da sie das Interface IEnumerable implementiert, können die Elemente in einer for each-Anweisung durchlaufen werden (siehe Abschnitt 9.8.2). Die Knoten haben den Datentyp
generic public ref class LinkedListNode sealed und können mit dem Konstruktor
LinkedListNode(T value) erzeugt werden. Die LinkedListNode-Eigenschaften
property LinkedListNode^ First property LinkedListNode^ Last property LinkedListNode^ Next property LinkedListNode^ Previous zeigen auf den ersten, letzten, nächsten bzw. vorhergehenden Knoten der Liste. Die Daten eines Knotens erhält man mit der Eigenschaft
property T Value
10.12 Collection-Klassen
1219
Eine LinkedList hat insbesondere Methoden, bei denen man angeben kann, vor oder nach welchem Knoten ein Element eingefügt oder gelöscht werden soll:
Methode LinkedListNode^ AddAfter (LinkedListNode^ node,T value) LinkedListNode^ AddBefore (LinkedListNode^ node, T value) LinkedListNode^ AddFirst(T value) LinkedListNode^ AddLast(T value) virtual bool Remove(T value) void Remove(LinkedListNode^ node) void RemoveFirst() void RemoveLast()
fügt value nach node ein
fügt value vor node ein
fügt value am Anfang bzw. Ende ein entfernt das erste Element mit dem Wert value bzw. node entfernt das erste bzw. letzte Element
Beispiel: Einige typische Anweisungen mit einer LinkedList: void testLinkedList(TextBox^ tb) { using namespace System::Collections::Generic; LinkedList^ c=gcnew LinkedList; c->AddFirst("Kathy"); c->AddAfter(c->First,"Daniel"); for each(String^ s in c) tb->AppendText(s+"\r\n"); LinkedListNode^ node=c->Find("Daniel"); tb->AppendText(node->Value+"\r\n"); }
10.12.8 Die generische Collection-Klasse Stack Ein Stack verwaltet seine Elemente in einer Last-In-First-Out (LIFO) Liste.
generic public ref class Stack : IEnumerable, ICollection, IEnumerable Viele Implementierungen eines Stack verfügen nur über Operationen wie
void Push(T item) // Legt einen Wert auf den Stack T Pop() // Gibt das oberste Element zurück und entfernt es Die .NET Stack-Klasse enthält aber mehr Elemente, die sich zum Teil aus dem Interface ICollection ergeben:
virtual property int Count // Anzahl der Elemente T Peek() // gibt das oberste Element zurück, ohne es zu entfernen array^ ToArray() // kopiert den Stack in ein Array
1220
10 Einige Elemente der .NET-Klassenbibliothek
10.12.9 Dictionaries und die generische Interface-Klasse IDictionary Eine Collection, die Paare aus Schlüsselwerten und zugehörigen Daten speichert, wird auch als Dictionary bezeichnet. Dazu gehören unter .NET die folgenden Klassen (TKey ist der Datentyp der Schlüsselwerte und TValue der der Daten):
genericpublic ref class Dictionary : IDictionary, ICollection, IEnumerable, IDictionary,ICollection, IEnumerable, ISerializable, IDeserializationCallback genericpublic ref class SortedList : IDictionary, ICollection, IEnumerable, IDictionary, ICollection, IEnumerable genericpublic ref class SortedDictionary: // dieselben Interfaches wie SortedList Diese Klassen unterscheiden sich vor allem durch ihre interne Implementation und den daraus resultierenden Anforderungen an die Elemente. Aus den Anforderungen an die Elemente ergibt sich bei einer konkreten Aufgabenstellung dann oft auch eine Entscheidung für die Wahl der Dictionary-Klasse.
Dictionary
ist als Hash-Tabelle implementiert. Deshalb ist Zeitaufwand für den Zugriff auf ein Element im Wesentlichen unabhängig von der Anzahl der Elemente im Dictionary (konstante Komplexität). Als Hash-Funktion wird die Funktion GetHashCode des Datentyps der Schlüsselwerte bzw. der im Konstruktor angegebenen IEqualityComparer-Implementation verwendet (siehe Abschnitt 10.12.6). SortedDictionary ist als binäre Struktur implementiert, in der die Elemente nach den Schlüsselwerten sortiert angeordnet sind. Für diese Anordnung muss der Datentyp der Schlüssel das Interface IComparable (siehe Abschnitt 9.8.1) implementieren. SortedList ist als Array von Paaren (Schlüssel und zugehörigen Daten) implementiert, das nach den Schlüsselwerten sortiert ist. Für diesen Vergleich muss der Datentyp der Schlüssel das Interface IComparable (siehe Abschnitt 9.8.1) implementieren. Da die Elemente in SortedDictionary und SortedList sortiert angeordnet sind, kann man auf sie mit einem binären Suchverfahren zugreifen (Zeitaufwand log(n) bei n Elementen). Diese beiden Dictionary-Klassen unterscheiden sich aber beim Zeitaufwand für das Einfügen von Elementen: Da SortedList ein Array ist, müssen beim Einfügen die Elemente verschoben werden, was einen zur Anzahl der
10.12 Collection-Klassen
1221
Elemente proportionalen Zeitaufwand zur Folge hat. Bei einem SortedDictionary hat diese Operation dagegen die Komplexität log(n). Die meisten Elemente der Dictionary-Klassen ergeben sich aus den implementierten Interfaces. Hier werden zunächst nur die Elemente vorgestellt, die sich aus der Interface-Klasse
genericpublic interface class IDictionary: ICollection, IEnumerable, IEnumerable ergeben. Die Schlüsselwerte aller Paare in einem Dictionary müssen eindeutig sein. Fügt man ein Wertepaar mit einem bereits vorhandenen Schlüssel ein, wird eine Exception ausgelöst.
IDictionary Element property TValue default [TKey] property ICollection^ Keys property ICollection ^ Values void Add(TKey key, TValue value) bool ContainsKey(TKey key) bool Remove(TKey key)
Mit dieser default-indizierten Eigenschaft kann man einen Wert über seinen Schlüssel indizieren. Die Collection der Schlüsselwerte im Dictionary
Die Collection der Werte im Dictionary
Fügt dem Dictionary ein Paar mit dem Schlüsselwert key und den Daten value hinzu. Gibt true zurück, falls das Dictionary ein Paar mit dem Argument für key als Schlüssel enthält. Entfernt das Wertepaar mit dem Schlüssel key aus dem Dictionary bool TryGetValue(TKey Falls zum Argument für key ein Wertepaar mit diesem Schlüsselwert existiert, wird der Wert des key, TValue% value) Paars im Argument für value zurückgegeben und der Funktionswert ist true. Andernfalls gibt TryGetValue den Werte false zurück.
Da ein Dictionary auch das Interface ICollection implementiert, kann man damit auch noch die ICollection-Elemente (siehe Abschnitt 10.12.2) verwenden. Beispiel: Die Funktion void testIDict(TextBox^ tb, IDictionary^ d) { d->Add("Daniel","13.11.79"); d["Alex"]="17.10.81"; bool b=d->ContainsKey("Daniel"); // true String^ s; String^ t; if (d->TryGetValue("Daniel",t))
1222
10 Einige Elemente der .NET-Klassenbibliothek
s=t; // s="13.11.79" // hier noch die Anweisungen aus dem nächsten } // Beispiel
kann mit jedem generischen Container mit String-Paaren aufgerufen werden, der das generische Interface IDictionary implementiert: Dictionary d; SortedDictionary ds; SortedList sl; testIDict(textBox1, %d); testIDict(textBox1, %ds); testIDict(textBox1, %sl);
Die Elemente eines Dictionary haben den Datentyp
generic public value class KeyValuePair und können mit dem Konstruktur
KeyValuePair(TKey key, TValue value) erzeugt werden. Die Werte eines KeyValuePair erhält man mit den Eigenschaften:
property TKey Key property TValue Value Die Liste der Schlüsselwerte und die der Daten erhält man mit den Eigenschaften
property KeyCollection^ Keys property ValueCollection^ Values Hier sind KeyCollection und ValueCollection Collections, die für den Typ der Schlüsselwerte bzw. der Werte die Interfaces ICollection, IEnumerable, ICollection und IEnumerable implementieren. Beispiel: Nimmt man in die Funktion testIDict von oben noch die folgenden Anweisungen auf void testIDict(TextBox^ tb, IDictionary^ d) { // nach den Anweisungen aus dem letzten Beispiel for each(KeyValuePair p in d) tb->AppendText(p.Key+": "+p.Value+"\r\n"); for each(String^ s in d->Keys) ; tb->AppendText(s+"\r\n"); for each(String^ s in d->Values) ; tb->AppendText(s+"\r\n"); }
10.12 Collection-Klassen
1223
werden die Elemente der sortierten Dictionaries mit der ersten for eachAnweisung nach dem Schlüsselwert sortiert ausgegeben. Bei einem Dictionary ist die Reihenfolge nicht definiert. Die Dictionary-Klassen von .NET entsprechen den Container-Klassen map und hash_map der C++-Standardbibliothek. Es gibt unter .NET keine vordefinierten Klassen, die multimap und hash_multimap entsprechen. Solche Klassen können aber mit einem sequentiellen Container als Typ der Werte konstruiert werden. Beispiel: Die folgenden Anweisungen zeigen, wie ein Dictionary definiert werden kann, das zu einem String mehrere int-Werte in einer Liste speichert, und wie man auf die Elemente zugreifen kann: using namespace System::Collections::Generic; typedef SortedDictionary MultiDictType; MultiDictType^ md=gcnew MultiDictType; int i=1; String^ s="eins"; if (!md->ContainsKey(s)) md[s]=gcnew(List); // md->Add(s,gcnew(List)); // das geht auch md[s]->Add(i); md[s]->Add(17); // und noch einen Wert ablegen
Aufgaben 10.12.9 1. Ein Informationssystem soll zu einem eindeutigen Schlüsselbegriff eine zugehörige Information finden, z.B. zu einer Artikelnummer des Datentyps int die zugehörige Artikelbezeichnung des Datentyps String. Definieren Sie dazu eine Klasse mit einer Funktion
bool tryGetValue(keyType key, dataType& data) die true zurückgibt, wenn das Argument für key in der Collection enthalten ist und sonst false. Der gefundene Wert soll im Argument für data zurückgegeben werden. Verwenden Sie für die Daten eine geeignete Collection-Klasse. Zum Testen kann man eine solche Collection mit Elementen füllen und dann für einige vorhandene und nicht vorhandene Werte prüfen, ob sie enthalten sind. Dazu legt man am einfachsten Wertepaare in der Collection ab, bei denen man leicht prüfen kann, ob der richtige Wert zu einem Schlüsselwert gefunden wurde, wie 1000 bzw. 100 000 aufeinander folgende Schlüsselwerte des Datentyps int und zugehörige Werte als String. Um welchen Faktor dauert die Suche in einem Container mit 1000 000 Elementen etwa länger als die in einem Container mit 1000 Elementen? 2. Eine Konkordanzliste (Cross-Reference-Liste) ist ein alphabetisch geordnetes Verzeichnis aller Wörter aus einem Text, das zu jedem Wort die Nummer
1224
10 Einige Elemente der .NET-Klassenbibliothek
einer jeden Seite bzw. Zeile enthält, in der es vorkommt (siehe auch Aufgabe 4.4). Für den Text
"Alle meine Entchen" "schwimmen auf dem See," "schwimmen auf dem See," ist z.B.
Alle: 1 auf: 2 3 dem: 2 3 Entchen: 1 meine: 1 schwimmen: 2 3 See: 2 3 eine solche Liste. Eine Konkordanzliste erhält man mit einem sortierten Dictionary, bei dem der Schlüsselbegriff ein String und der Wert zum Schlüsselbegriff eine Liste mit den zugehörigen Zeilennummern ist. Zu jedem Wort aus dem Text trägt man dann jede Zeilennummer in die zugehörige Liste ein und gibt dann alle diese Worte zusammen mit den zugehörigen Nummern aus. Schreiben Sie eine Funktion MakeXRef, die jeden String aus einer Liste mit der Methode Split (siehe Abschnitt 9.4.2) in Worte zerlegt und jedes solche Wort zusammen mit seiner Zeilennummer in ein geeignetes Dictionary ablegt. Eine Funktion PrintXRef soll jedes Wort aus dem mit MakeXRef angelegten Dictionary zusammen mit den zugehörigen Zeilennummern ausgeben. Testen Sie diese Funktionen mit den Strings von oben. 3. Schreiben Sie eine Klasse mit Funktionen, die alle doppelten Dateien auf eine Festplatte finden und anzeigen. Dabei stellt sich die Frage, wann zwei Dateien als gleich betrachtet werden sollen. Der Name ist dafür nicht unbedingt als Kriterium geeignet: Zwei Dateien mit demselben Namen können verschiedene Inhalte haben und zwei Dateien mit verschiedenen Namen denselben. Am besten wäre es, wenn man alle Dateien mit derselben Größe zeichenweise vergleichen würde. Das wäre allerdings sehr zeitaufwendig. Ein einfacher Kompromiss ist ein Schlüssel, bei dem die Dateigröße und der Dateiname zu einem einzigen String zusammengesetzt werden (z.B. „325config.sys“, wenn die Datei „config.sys“ 325 Bytes groß ist). Mit diesem Schlüssel werden nur diejenigen Dateien als gleich betrachtet, die denselben Namen und dieselbe Dateigröße haben. Lösen Sie diese Aufgabe mit einem Dictionary mit einem solchen Schlüssel. Die Daten zu einem solchen Schlüssel sollen eine sequentielle Collectionklasse sein, deren Elemente den Dateinamen mit der vollen Pfadangabe enthalten.
10.12 Collection-Klassen
1225
10.12.10 Spezielle Collection-Klassen Der Namensbereich System::Collections::Specialized und viele Steuerelemente enthalten spezielle Collection-Klassen, deren Namen sich wie bei
public ref class StringCollection : IList, ICollection, IEnumerable aus dem Datentyp der Elemente und „Collection“ zusammensetzt, und die alle die Interfaces IList, ICollection, IEnumerable implementieren. Beispiel: StringCollections bieten eine einfache Möglichkeit zur typsicheren Verwaltung von Strings. Im Gegensatz zu ArrayList sind ihre Elemente keine Object-Handles, sondern Strings, die ohne Typkonversion verwendet werden können: using namespace System::Collections::Specialized; StringCollection sc; sc.Add("1. "); sc.Add("2. "); for each (String^ s in sc) textBox1->AppendText(s+"\r\n");
Zu den Collection-Klassen von Steuerelementen gehört die von der Eigenschaft Items einer ListBox, ComboBox usw. verwendete
public ref class ObjectCollection : IList, ICollection, IEnumerable Auch die folgenden Collection-Klassen implementieren die Interfaces IList, ICollection, IEnumerable: – Die Eigenschaft Images mit den Bildern einer ImageList hat den Datentyp ImageCollection. Die Elemente haben den Datentyp Image. – Die Eigenschaft MenuItems mit den Menüeinträgen eines Menüs hat den Datentyp MenuItemCollection. Die Elemente haben den Datentyp MenuItem. – Die Eigenschaft Items mit den Einträgen in einem ListView hat den Datentyp ListViewItemCollection. Die Elemente haben den Datentyp ListViewItem. – Die Eigenschaft Nodes mit den Knoten eines TreeView hat den Datentyp TreeNodeCollection. Die Elemente haben den Datentyp TreeNode. – zahlreiche weitere wie MatchCollection, IPNodeCollection usw. Zu den Elementen, die sich aus den implementierten Interfaces ergeben, kommen weitere, die für die jeweilige Collection notwendig und hilfreich sind. Beispiel: Eine ImageCollection hat die Methoden
void SetKeyName(int index, String^ name) bool ContainsKey(String^ key)
1226
10 Einige Elemente der .NET-Klassenbibliothek
mit denen man einem Bild einen Namen zuordnen und nach diesem Namen suchen kann.
10.13 Systeminformationen und –operationen Unter .NET stehen viele Systeminformationen zur Verfügung, und meist ist der Zugriff auf diese Informationen auch recht einfach. Das einzige Problem ist oft nur, dass man nicht weiß, wo man diese findet. Die folgenden Ausführungen geben einen kurzen Überblick, wo man einige häufig benötigte Informationen finden kann. Die vorgestellten Elemente sind nur ein Auszug.
10.13.1 Die Klasse Environment Die Klasse Environment aus dem Namensbereich System stellt Informationen über den Rechner und den laufenden Prozess zur Verfügung. Viele Eigenschaften dieser Klasse sind Werte einzelner Umgebungsvariablen (wie sie mit set in einer Eingabeaufforderung angezeigt werden). Die Elemente dieser Klasse, die Verzeichnisse betreffen, sind in Abschnitt 10.14.6 aufgeführt.
Environment: Eigenschaft oder Methode static property String^ die Befehlszeile beim Programmstart CommandLine static array^ ein Array mit den Strings der Befehlszeile, GetCommandLineArgs() wobei das erste Element der Name der ausführbaren Datei ist static String^ GetEnvironment- der Wert der Umgebungsvariablen zum ArVariable(String^ variable) gument für variable gibt ein Dictionary mit allen Namen und static IDictionary^ GetEnvironmentVariables() Werten der Umgebungsvariablen zurück beendet das Programm und übergibt das Arstatic void Exit(int exitCode) gument als Exit-Code an das Betriebssystem static property String^ der NetBIOS-Name des Rechners MachineName static property Operatinggibt den Namen und die Version des BeSystem^ OSVersion triebssystems zurück (mit ToString beide) static property int Anzahl der Prozessoren des Rechners ProcessorCount Anzahl Millisekunden seit Systemstart static property int TickCount
10.13 Systeminformationen und –operationen
1227
Environment: Eigenschaft oder Methode static property String^ Name des angemeldeten Benutzers UserName static property String^ der aktuelle Aufrufstack als String (siehe StackTrace Abschnitt 5.1) Beispiel: Startet man das Programm test.exe mit den Befehlsargumenten (Projekt|Eigenschaften|Konfigurationseigenschaften|Debugging) bzw. von einer Kommandozeile aus mit den Parametern test.exe abc 1 23
erhält man durch die Anweisungen for each (String^ s in Environment:: GetCommandLineArgs()) textBox1->AppendText(s+"\r\n");
die Ausgabe test.exe abc 1 23
10.13.2 Die Klasse Process Mit der Komponente (Toolbox Registerkarte Komponenten) bzw. Klasse (Namensbereich System::Diagnostics) Process kann man Programme starten und beenden. Über Process erhält man außerdem zahlreiche Systeminformationen.
Process: Eigenschaft oder Methode static Process^ Start(String^ filename) static Process^ Start(String^ filename, String^ arguments) static Process^ Start( ProcessStartInfo^ startInfo)
startet eine ausführbare Datei startet eine ausführbare Datei mit den übergebenen Argumenten startInfo enthält zahlreiche Elemente, über die man Argumente, Arbeitsverzeichnisse usw. übergeben kann. gibt Informationen über den aktuelstatic Process^ GetCurrentProcess() len Prozess zurück static array^ gibt Informationen über alle laufenGetProcesses() den Prozesse zurück beendet den Prozess void Kill() property ProcessPriorityClass die Priorität des Prozesses PriorityClass
1228
10 Einige Elemente der .NET-Klassenbibliothek
Process: Eigenschaft oder Methode property ProcessThreadCollection^ Threads property TimeSpan PrivilegedProcessorTime property TimeSpan UserProcessorTime property TimeSpan TotalProcessorTime
property long long WorkingSet64
die Threads des Prozesses
die CPU-Zeit, die der Prozess im Betriebssystemkern verbraucht hat die CPU-Zeit, die der Prozess im Anwendermodus verbraucht hat die CPU-Zeit, die der Prozess verbraucht hat (User- + PrivilegedProcessorTime) für den Prozess reservierter Speicher (in Bytes)
Beispiele: Die Anweisung Process::Start("notepad.exe");
startet das Programm „notepad.exe“. Parameter können für arguments übergeben werden. Die nächste Anweisung startet den Internet Explorer so, dass dieser eine Datei anzeigt: Process::Start("IExplore.exe","C:\\meinTxt.html");
Alle laufenden Prozesse werden (ähnlich wie im Windows TaskManager) mit den nächsten Anweisungen angezeigt: for each(Process^ p in Process::GetProcesses()) tb->AppendText(p->ProcessName+"\r\n");
Aufrufe der Funktion mit den unten angegebenen Argumenten für n int benchSum(int n, TextBox^ tb) { using namespace System::Diagnostics; Stopwatch^ sw=gcnew Stopwatch; Process^ p=Process::GetCurrentProcess(); double t0=p->TotalProcessorTime.TotalMilliseconds; sw->Start(); int sum=0; for (int i=0; iTotalProcessorTime.TotalMilliseconds; sw->Stop(); tb->AppendText("tp: "+((t1-t0)/1000).ToString()); tb->AppendText("sw: "+(sw->ElapsedTicks/ float(sw->Frequency)).ToString()+"\r\n"); return sum; }
ergaben die folgenden Werte:
10.14 .NET-Klassen zur Dateibearbeitung
tp: tp: tp: tp:
0 0,046875 0,328125 3,515625
sw: sw: sw: sw:
0,004643048 0,04215256 0,3513494 3,682464
1229
// // // //
n=1000000 n=10000000 n=100000000 n=1000000000
Diese zeigen, dass die Auflösung der Process CPU-Zeiten nicht so fein ist wie bei einem Stopwatch, und dass bei längeren Laufzeiten die Stopwatch-Werte nicht viel größer sind als die CPU-Zeiten.
10.13.3 Die Klasse ClipBoard Die Klasse ClipBoard aus dem Namensbereich System::Windows::Forms enthält zahlreiche statische Methoden, mit denen man Daten in die Zwischenablage einfügen bzw. aus der Zwischenablage abrufen kann.
ClipBoard: Methode static void Clear() static void SetText(String^ text) static void SetImage(Image^ image)
löscht die Zwischenablage fügt Text in die Zwischenablage ein fügt ein Bild im Bitmap-Format in die Zwischenablage ein fügt Daten im angegebenen Format in static void SetData(String^ format, Object^ data) die Zwischenablage ein der Text der Zwischenablage static String^ GetText() ein Bild der Zwischenablage static Image^ GetImage() true, falls die Zwischenablage ein Bild static bool ContainsImage() enthält
Einige Steuerelemente (z.B. TextBox oder RichTextBox, siehe Abschnitt 10.1) besitzen eigene Funktionen zum Datenaustausch mit der Zwischenablage.
10.14 .NET-Klassen zur Dateibearbeitung Die .NET-Klassen zur Dateibearbeitung bieten im Wesentlichen dieselben Möglichkeiten wie die Streamklassen der C++-Standardbibliothek (siehe Abschnitt 4.3). Da sie aber auf die Besonderheiten von .NET und Windows abgestimmt sind, bieten Sie einige zusätzliche sowie oft auch einfachere Möglichkeiten. Einige Vereinfachungen ergeben sich insbesondere daraus, dass sie für Strings den Datentyp String und nicht string verwenden. Die folgenden Ausführungen sollen nur einen kurzen Überblick geben. Für weitere Informationen wird auf die Online-Hilfe verwiesen.
1230
10 Einige Elemente der .NET-Klassenbibliothek
10.14.1 Textdateien bearbeiten: StreamReader und StreamWriter Mit den Klassen StreamReader und StreamWriter aus dem Namensbereich System::IO kann man Textdateien lesen und schreiben. Sie haben unter anderem die folgenden Konstruktoren
StreamReader(String^ path) StreamWriter(String^ path) die eine Datei mit dem angegebenen Dateinamen öffnen und die Zeichen als Unicode Zeichen (UTF-8) behandeln. Über weitere Konstruktoren können auch andere Zeichensätze verwendet werden. Ein StreamReader hat unter anderem die folgenden Methoden, mit denen Daten aus der Datei gelesen
virtual String^ ReadLine() override // liest die nächste Zeile virtual int Read() override // liest das nächste Zeichen virtual int Read(array^ buffer, int index, int count) override Die Eigenschaft
property bool EndOfStream hat genau dann den Wert true, wenn alle Zeichen gelesen wurden. Beispiel: Die Funktion f1 liest alle Zeilen einer Textdatei und gibt sie in einer TextBox aus: void f1(TextBox^ tb, String^ path) { using namespace System::IO; StreamReader^ r = gcnew StreamReader(path); while (!r->EndOfStream) { tb->AppendText(r->ReadLine()+"\r\n"); } r->Close(); }
Bei FileStreams, die nur lokal in einer Funktion verwendet werden, ist oft die „stack semantics“ Syntax (siehe Abschnitt 9.6.2) einfacher: void f1(TextBox^ tb, String^ path) { using namespace System::IO; StreamReader r(path);
10.14 .NET-Klassen zur Dateibearbeitung
1231
while (!r.EndOfStream) { tb->AppendText(r.ReadLine()+"\r\n"); } // r->Close(); nicht notwendig }
In der nächsten Variante dieser Funktion werden außerdem Exceptions abgefangen. Mit try-finally wird sichergestellt, dass die Datei auch bei einer Exception wieder freigegeben wird: void f2(TextBox^ tb, String^ path) { using namespace System::IO; StreamReader^ r = gcnew StreamReader(path); try { while (!r->EndOfStream) tb->AppendText(r->ReadLine()); } catch (Exception^ e) { tb->AppendText("Fehler: "+ e->Message ); } finally { r->Close(); } }
Die StreamWriter-Klasse enthält zahlreiche überladene Versionen der Funktion
virtual void Write(T value) virtual void WriteLine(T value) die den als Argument übergebenen Wert als Text in eine Datei schreiben. Dabei kann T ein nahezu beliebiger fundamentaler Datentyp oder ein C++/CLI-Datentyp sein. Für einen selbstdefinierten Datentyp wird der von ToString zurückgegebene Wert in die Datei geschrieben. Die WriteLine-Methoden unterscheiden sich von den Write-Methoden nur dadurch, dass sie anschließend noch ein NewLineZeichen schreiben. Setzt man die Eigenschaft
virtual property bool AutoFlush auf true, wird nach jedem Schreiben in die Datei die Funktion
virtual void Flush() override aufgerufen, die die gepufferten Daten in die Datei schreibt. Beispiel: Ein Aufruf der Funktion
1232
10 Einige Elemente der .NET-Klassenbibliothek
void f3(String^ fn) { array^ a=gcnew array(3) {17,3.14,"Hello"}; using namespace System::IO; StreamWriter^ w = gcnew StreamWriter(fn); for each(Object^ i in a) w->WriteLine(i); w->Close(); }
legt eine Textdatei mit den folgenden drei Zeilen an: 17 3,14 Hello
Aufgaben 10.14.1 Verwenden Sie hier nur die .NET-Funktionen zur Dateibearbeitung. 1. Das HTML-Format ist ein Textformat, das unter anderem für Internetseiten verwendet wird. Damit es auf möglichst vielen verschiedenen Rechner- und Betriebssystemen eingesetzt werden kann, verwendet es nur Zeichen des ASCII-Zeichensatzes. Formatangaben werden auch als Markierungen bezeichnet und bestehen aus einem Paar von spitzen Klammern , zwischen denen Schlüsselworte und eventuell noch Parameter stehen. Ein HTML-Dokument beginnt mit der Markierung und endet mit . Wie bei diesem Paar von Markierungen werden Bereiche oft durch Markierungen begrenzt, bei denen die Markierung für das Ende des Bereichs mit dem Zeichen „/“ beginnt, und bei der das Schlüsselwort in der EndeMarkierung gleich oder ähnlich ist wie in der Anfangsmarkierung. Bereiche können verschachtelt werden. So kann ein HTML-Dokument einen durch und begrenzten Bereich mit Angaben enthalten, die das gesamte Dokument betreffen. In einem solchen Bereich kann z.B. zwischen und der Text stehen, der in der Titelzeile des Browsers angezeigt wird. Der im Hauptfenster des Browsers angezeigte Text ist in einem durch und begrenzten Bereich des HTML-Dokuments enthalten. So wird zum Beispiel das HTML-Dokument Mein HTML Dokument
10.14 .NET-Klassen zur Dateibearbeitung
1233
Text in meinem HTML-Dokument Neue Zeile
in einem HTML-Browser folgendermaßen dargestellt:
Die Einrückungen im HTML-Dokument wirken sich nicht auf die Formatierung aus und wurden hier nur zur besseren Übersichtlichkeit aufgenommen. Zeilenvorschübe im Text werden ebenfalls ignoriert und nur durch die Markierung erzeugt. Da die Umlaute nicht zum ASCII-Zeichensatz gehören, werden sie durch spezielle Zeichenkombinationen dargestellt: ä: ä ö: ö ü: ü ß: ß Ä: Ä Ö: Ö Ü: Ü Beispiel: „In München steht ein Hofbräuhaus.“ Schreiben Sie eine Funktion TextToHtml, die aus einer Textdatei ein HTMLDokument erzeugt. Dazu sollen die notwendigen Markierungen erzeugt und der gesamte Text der Textdatei in einen durch und begrenzten Bereich kopiert werden. Die Titelzeile des Browsers soll den Dateinamen anzeigen. Die einzelnen Zeilen der Textdatei sollen im Browser ebenfalls als einzelne Zeilen dargestellt werden. Die Umlaute sollen durch die entsprechenden Zeichenkombinationen ersetzt werden. Testen Sie diese Funktion mit einer Textdatei. Falls diese wie „Faust.txt“ aus dem Verzeichnis Text der Lösungs-CD im Ascii-Format vorliegt, muss sie mit der UTF7-Kodierung geöffnet werden wie in StreamReader r(infn,System::Text::Encoding::UTF7);
2. Überarbeiten Sie Kopien der Funktionen MakeXRef und printXRef von Aufgabe 10.12.9 so, dass ihnen ein Dateiname als Parameter übergeben wird. Wenn dieser Dateiname bei MakeXRef eine Textdatei darstellt, soll aus den
1234
10 Einige Elemente der .NET-Klassenbibliothek
Zeilen dieser Datei eine Konkordanzliste erstellt werden, die zu jedem Wort im Text die Zeilennummern enthält, in denen es vorkommt. printXRef soll die Konkordanzliste in eine Datei schreiben, deren Name als String übergeben wird.
10.14.2 Die Klasse FileStream Die FileStream-Klasse hat zahlreiche Konstruktoren, mit denen man beim Öffnen einer Datei die folgenden Parameter steuern kann, – ob eine Datei nur zum Lesen, nur zum Schreiben oder zum Lesen und Schreiben geöffnet werden soll (FileAccess) – was passieren soll, wenn eine anzulegende Datei bereits existiert oder eine nicht vorhandene Datei geöffnet werden soll – ob andere Anwender die Datei gleichzeitig zum Lesen oder Schreiben öffnen können (FileShare, siehe Abschnitt 10.14.4) – welche Puffergröße verwendet werden soll – ob die Lese- und Schreiboperationen asynchron erfolgen sollen Allerdings stehen zum Lesen und Schreiben nur die folgenden vier Funktionen zur Verfügung, mit denen man ein Byte oder einen Block von Bytes lesen oder schreiben kann:
virtual int Read(array^ array, int offset, int count) override virtual int ReadByte() override virtual void Write(array^ array, int offset, int count) override virtual void WriteByte(unsigned char value) override Die Eigenschaft Length stellt die Länge des Streams (in Bytes) dar:
virtual property long long Length Beispiel: Alle Bytes einer Datei mit dem Namen n werden durch die folgenden Anweisungen in das Array buffer eingelesen: FileStream^ f= gcnew FileStream(n,FileMode::Open); array^buffer=gcnew array(f->Length); f->Read(buffer, 0, buffer->Length ); f->Close();
Diese Funktionen sind zwar im Prinzip ausreichend. Oft ist die Arbeit mit ihnen aber umständlich. In Abschnitt 10.14.3 wird gezeigt, wie man einfacheren Schreibund Leseoperationen von StreamReader, StreamWriter, BinaryReader und BinaryWriter mit den vielseitigen Konfigurationsmöglichkeiten von FileStreams verbinden kann. Der FileMode-Parameter in einem Konstruktor wie
10.14 .NET-Klassen zur Dateibearbeitung
1235
FileStream(String^ path, FileMode mode) ist ein Aufzählungstyp
public enum class FileMode mit den folgenden Elementen:
Create
CreateNew
Open
OpenOrCreate
Append
Truncate
Bedeutung Erstellt eine neue Datei. Falls eine Datei mit diesem Namen bereits existiert, wird sie überschrieben. Erstellt eine neue Datei. Falls eine Datei mit diesem Namen bereits existiert, wird eine IOException ausgelöst. Öffnet eine vorhandene Datei. Falls die Datei nicht existiert, wird eine FileNotFoundException ausgelöst. Öffnet eine vorhandene Datei, wenn sie existiert. Falls die Datei nicht existiert, wird eine neue erstellt. Öffnet eine vorhandene Datei mit dem angegebenen Namen oder legt eine neue Datei an. Der Positionszeiger wird auf das Dateiende und gesetzt. Öffnet eine vorhandene Datei und löscht den gesamten Inhalt.
Die FileStream-Eigenschaft
virtual property long long Length gibt die Länge der Datei an.
virtual property long long Position ist die aktuelle Dateiposition. Mit
virtual long long Seek(long long offset, SeekOrigin origin) override kann man die Dateiposition setzen (Direktzugriff). Dabei ist origin einer der folgenden Werte:
Begin: Der Startpunkt ist der Dateianfang. Current: Der Startpunkt ist die aktuelle Dateiposition. End: Der Startpunkt ist das Dateiende.
1236
10 Einige Elemente der .NET-Klassenbibliothek
10.14.3 BinaryReader/Writer und StreamReader/Writer mit FileStreams Sowohl die in Abschnitt 10.14.1 vorgestellten Klassen StreamReader und StreamWriter als auch BinaryReader und BinaryWriter haben Konstruktoren mit einem Stream-Parameter:
BinaryReader(Stream^ input) BinaryWriter(Stream^ output)
StreamReader(Stream^ stream) StreamWriter(Stream^ stream)
Da die Klasse FileStream von Stream abgeleitet ist, können diese Reader und Writer auch mit einem FileStream initialisiert werden. Über diese Konstruktoren stehen die vielfältigen Konfigurationsmöglichkeiten für FileStreams auch für StreamReader/Writer und BinaryReader/Writer zur Verfügung, die selbst keine Konstruktoren mit entsprechenden Optionen haben. Im Folgenden werden einige der wichtigsten Elemente der Klassen BinaryReader und BinaryWriter vorgestellt. Ein BinaryWriter hat eine für im Wesentlichen alle elementaren Datentypen überladene Funktion
virtual void Write(short value)
virtual void Write(int value)
die das Argument in die Datei schreibt. Ein BinaryReader hat entsprechende Elementfunktionen für diese Datentypen, die wie
virtual short ReadInt16()
virtual int ReadInt32()
einen Wert des entsprechenden Datentyps aus einer Binärdatei lesen. Selbstdefinierte Datentypen (Klassen), deren Elemente alle einen solchen Typ haben, können die Elemente mit diesen Operationen lesen und schreiben. Für Elemente anderer Typen sind oft die Klassen zur Serialisierung (siehe Abschnitt 10.15) besser geeignet. Beispiel: Die folgenden Anweisungen zeigen, wie ein BinaryWriter und ein BinaryReader mit einem FileStream geöffnet wird: using namespace System::IO; FileStream^ fs = gcnew FileStream(fn, FileMode::CreateNew); BinaryWriter^ w = gcnew BinaryWriter(fs); for (int i = 0; i < 11; i++) w->Write(i); w->Close(); fs->Close(); fs = gcnew FileStream(fn, FileMode::Open, FileAccess::Read); BinaryReader^ r = gcnew BinaryReader(fs); for (int i = 0; i < 11; i++) tb->AppendText(r->ReadInt32()+"\r\n"); r->Close(); fs->Close();
10.14 .NET-Klassen zur Dateibearbeitung
1237
10.14.4 Der gleichzeitige Zugriff auf eine Datei und Record-Locking Mit den Argumenten für FileAccess und FileShare kann man in einem Konstruktor wie
FileStream(String^ path, FileMode mode, FileAccess access, FileShare share) steuern, ob eine bereits geöffnete Datei erneut geöffnet werden kann und ob dabei Daten gelesen oder geschrieben werden können. Das ist zwar innerhalb eines Programms nur selten sinnvoll. In einem Netzwerk kann es aber notwendig sein, dass mehrere Programme gleichzeitig auf dieselbe Datei zugreifen können. In diesem Zusammenhang ist es ohne Bedeutung, ob eine Datei von demselben Programm oder von verschiedenen Programmen mehrfach geöffnet wird. Die Elemente des Aufzählungstyps
public enum class FileShare legen fest, ob eine bereits geöffnete Datei gleichzeitig geöffnet werden kann:
Bedeutung Bevor die Datei nicht wieder geschlossen wird, kann sie nicht erneut geöffnet werden. Read Die Datei kann gleichzeitig zum Lesen geöffnet werden. ReadWrite Die Datei kann gleichzeitig zum Lesen und Schreiben geöffnet werden. Write Die Datei kann gleichzeitig zum Schreiben geöffnet werden.
None
Dieser Parameter wird vor allem zusammen mit einem Parameter des Typs
public enum class FileAccess verwendet, dessen Elemente festlegen, ob Daten aus der Datei gelesen oder in die Datei geschrieben werden können:
Bedeutung Read Aus der Datei können Daten gelesen werden. ReadWrite Daten können gelesen oder geschrieben werden. Write In die Datei können Daten geschrieben werden.
Die Zugriffsrechte für die einzelnen Kombinationen von FileShare und FileAccess ergeben sich aus der Tabelle:
1238
10 Einige Elemente der .NET-Klassenbibliothek
\ Zweiter und weitere Konstruktor-Aufrufe Erster\ None Read Write R/W Aufruf \ R W RW R W RW R W RW R W RW - - - - -| - - - - - - - - - - - - - None R | N N N N N N N N N N N N W | N N N N N N N N N N N N RW| N N N N N N N N N N N N Read R | N N N Y N N N N N Y N N W | N N N N N N Y N N Y N N RW| N N N N N N N N N Y N N Write R | N N N N Y N N N N N Y N W | N N N N N N N Y N N Y N RW| N N N N N N N N N N Y N R/W R | N N N Y Y Y N N N Y Y Y W | N N N N N N Y Y Y Y Y Y RW| N N N N N N N N N Y Y Y
Hier bedeutet Y, dass der zweite Konstruktoraufruf erfolgreich ist, und N, dass er nicht erfolgreich ist. Beispiel: Die folgende Anweisung öffnet eine Datei zum Lesen und zum Schreiben, die andere Programme ebenfalls zum Lesen und Schreiben öffnen können: using namespace System::IO; FileStream^ fs1=gcnew FileStream(fn, FileMode::OpenOrCreate, FileAccess::ReadWrite, FileShare::ReadWrite);
Meist spricht nicht viel dagegen, dass mehrere Anwender eine Datei gleichzeitig lesen. Wenn aber mehrere Anwender das Recht haben, in eine Datei zu schreiben, muss sichergestellt werden, dass sie sich nicht gegenseitig Daten überschreiben. Dazu ist es allerdings nicht notwendig, eine gesamte Datei für andere Anwender zu sperren. Mit den FileStream-Methoden
virtual void Lock(long long position, long long length) virtual void Unlock(long long position, long long length) kann auch nur ein Teil einer Datei gesperrt werden (Record-Locking). Wenn dann ein bereits gesperrter Bereich erneut gesperrt wird, wird eine Exception des Typs System::IO::IOException ausgelöst.
10.14.5 XML-Dateien: Die Klassen XmlReader und XmlWriter Der Namensbereich System::Xml enthält zahlreiche Klassen mit zahlreichen Elementen, mit denen man XML-Dateien lesen und schreiben kann (siehe auch die Abschnitte 10.15.3 und 10.16.6). Im Folgenden werden einige Elemente der Klassen
ref class XmlWriter abstract : IDisposable ref class XmlReader abstract : IDisposable
10.14 .NET-Klassen zur Dateibearbeitung
1239
vorgestellt. Da diese Klassen abstrakt sind, kann man mit ihren Konstruktoren keine Objekte anlegen: XmlWriter^ w=gcnew XmlWriter; // geht nicht, da abstract
Obwohl es einige nicht abstrakte Klassen (z.B. XmlTextReader und XmlTextWriter) gibt, die von XmlReader und XmlWriter abgeleitet sind, empfiehlt Microsoft, XmlReader und XmlWriter zu verwenden, da diese umfangreichere Konfigurationsmöglichkeiten als die abgeleiteten Klassen bieten. Anstatt mit dem Konstruktor werden die XmlReader und XmlWriter Objekte mit einer der zahlreichen überladen Create Methoden
static XmlReader^ Create(String^ inputUri, XmlReaderSettings^ settings) static XmlWriter^ Create(String^ outputFileName, XmlWriterSettings^ settings) angelegt. Mit XmlWriter-Methoden wie
void WriteStartElement(String^ localName) void WriteElementString(String^ localName, String^ value) virtual void WriteEndElement() abstract kann man dann ein XML-Dokument aufbauen. Beispiel: Die Anweisungen using namespace System::Xml; XmlWriterSettings^ s=gcnew XmlWriterSettings; s->Indent=true; s->IndentChars=" "; XmlWriter^ w=XmlWriter::Create("c:\\text.xml"); w->WriteStartElement("start-Element"); w->WriteElementString("Preis","17"); w->WriteEndElement(); w->Flush();
erzeugen die Datei  17
Die Klasse XmlReader hat zahlreiche Methoden, mit denen man XML-Dokumente lesen kann. Die Methode
virtual bool Read() abstract liest den nächsten Knoten ein. Wenn die Eigenschaft
virtual property XmlNodeType NodeType
1240
10 Einige Elemente der .NET-Klassenbibliothek
den Wert XmlNodeType::Text hat, erhält man mit
virtual property String^ Value den Textinhalt des aktuellen Knotens. Beispiel: Unter www.w3schools.com findet man unter anderem eine XML-Datei, die etwa folgendermaßen aufgebaut ist: Empire Burlesque Bob Dylan USA Columbia 10.90 1985 ... // weitere CD-Einträge
Die Werte der Knoten kann man mit den folgenden Anweisungen lesen: void testXMLReader(TextBox^ tb) { using namespace System::Xml; XmlReader^ r=XmlReader::Create( "http://www.w3schools.com/XML/cd_catalog.xml"); while (r->Read()) { if (r->NodeType==XmlNodeType::Text) tb->AppendText(r->Value+"\r\n"); } r->Close(); }
Einige weitere Anweisungen, mit denen man einzelne Knoten gezielt ansprechen kann:
XmlReader Methode virtual XmlNodeType MoveToContent() virtual void ReadStartElement(String^ name)
virtual String^ ReadString()
virtual void ReadEndElement()
setzt den Lese-Cursor auf den nächsten Knoten mit einem Inhalt prüft, ob der aktuelle Knoten den Namen name hat und setzt den Lese-Cursor auf den nächsten Knoten gibt den Inhalt des aktuellen Knotens zurück prüft, ob der aktuelle Knoten eine EndeMarkierung ist und setzt den Lese-Cursor auf den nächsten Knoten
10.14 .NET-Klassen zur Dateibearbeitung
XmlReader Methode virtual String^ fasst die letzten ReadElementString(String^ name) zusammen
1241
drei
Anweisungen
Beispiel: Die nächsten Anweisungen zeigen, wie man auf einzelne Knoten zugreifen kann: using namespace System::Xml; XmlReader^ r=XmlReader::Create( "http://www.w3schools.com/XML/cd_catalog.xml"); r->MoveToContent(); r->ReadStartElement("CATALOG"); r->MoveToContent(); while (r->LocalName=="CD") { r->ReadStartElement("CD"); r->ReadStartElement("TITLE"); String^ t=r->ReadString(); r->ReadEndElement(); String^ a=r->ReadElementString("ARTIST"); String^ c=r->ReadElementString("COUNTRY"); String^ com=r->ReadElementString("COMPANY"); String^ p=r->ReadElementString("PRICE"); String^ y=r->ReadElementString("YEAR"); tb->AppendText(t+a+c+p+y+"\r\n"); r->ReadEndElement(); // CD r->MoveToContent(); } r->ReadEndElement(); r->Close();
10.14.6 Klassen für Laufwerke, Verzeichnisse, Pfade und Dateien Der Namensbereich System::IO enthält einige Klassen, die Informationen über Laufwerke, Verzeichnisse, Pfade und Dateien zur Verfügung stellen. Die folgenden Tabellen stellen einige der wichtigsten Elemente dieser Klassen vor. Die Klasse DriveInfo (Namensbereich System::IO) stellt Informationen über die Laufwerke eines Rechners zur Verfügung.
DriveInfo Eigenschaft oder Methode static array^ Gibt für jedes Laufwerk des Rechners GetDrives() Informationen des Typs DriveInfo zurück DriveInfo(String^ driveName) Der Konstruktor erzeugt DriveInfo-Informationen zu dem als String (ein Zeichen "a" … "z" bzw. "A" … "Z") übergebenen Laufwerk Der Name des Laufwerks, z.B. c:\ property String^ Name
1242
10 Einige Elemente der .NET-Klassenbibliothek
DriveInfo Eigenschaft oder Methode property DriveType DriveType Einer der Werte Unknown, NoRootDirectory, Removable, Fixed, Network oder CDRom des Aufzählungstyps DriveType. property DirectoryInfo^ Informationen des Typs DirectoryInfo für RootDirectory das Stammverzeichnis property long long verfügbarer freier Speicher in Bytes AvailableFreeSpace property long long gesamter freier Speicher in Bytes TotalFreeSpace Gesamtgröße des Laufwerks in Bytes property long long TotalSize Beispiel: Die Funktion showAllDrives wurde schon in Abschnitt 9.5 als Beispiel für CLI-Arrays vorgestellt. Sie gibt für jedes bereite Laufwerk des Rechners seinen Namen, seine Kapazität und den freien Platz aus: void showAllDrives(TextBox^ tb) { for each (DriveInfo^ i in DriveInfo::GetDrives()) if (i->IsReady) tb->AppendText(i->Name+" total:"+i->TotalSize+ " free:"+i->TotalFreeSpace+"\r\n"); }
Die Klassen Directory und DirectoryInfo (Namensbereich System::IO) enthalten Methoden und Eigenschaften für Verzeichnisse. In beiden Klassen findet man viele Elemente, die große Ähnlichkeiten haben. Sie unterscheiden sich aber dadurch, dass alle Methoden von Directory statisch sind, und dass sie unterschiedliche Informationen zurückgeben.
Rückgabewert bzw. Operation legt ein Verzeichnis mit dem Namen path an löscht das Verzeichnis mit dem Namen path, falls es leer ist gibt an, ob das Verzeichnis existiert static bool Exists(String^ path) static array^ gibt die Namen aller UnterverzeichGetDirectories(String^ path) nisse zurück gibt die Namen aller Dateien zurück static array^ GetFiles (String^ path) static array^ gibt die Namen der logischen LaufGetLogicalDrives() werke in der Form „c:\“ zurück das aktuelle Arbeitsverzeichnis static String^ GetCurrentDirectory() setzt das aktuelle Arbeitsverzeichnis static void SetCurrentDirectory( String^ path)
Directory Methode static DirectoryInfo^ CreateDirectory(String^ path) static void Delete(String^ path)
10.14 .NET-Klassen zur Dateibearbeitung
Directory Methode static DirectoryInfo^ GetParent(String^ path) static String^ GetDirectoryRoot(String^ path) static void Move( String^ sourceDirName, String^ destDirName)
1243
Rückgabewert bzw. Operation gibt Informationen über das übergeordnete Verzeichnis zurück gibt Volume bzw. Stammverzeichnis oder beides als String zurück verschiebt eine Datei oder ein Verzeichnis
Beispiel: Die nächsten Anweisungen legen das Verzeichnis „c:\test“ an, falls es nicht schon existiert: if (!Directory::Exists("c:\\test")) Directory::CreateDirectory("c:\\test");
Ein DirectoryInfo -Objekt (siehe auch Abschnitt 5.3.7) wird mit dem Konstruktor
DirectoryInfo(String^ path) angelegt. Die Eigenschaften und Methoden (die im Unterschied zur Klasse Directory nicht statisch sind) des Objekts beziehen sich dann auf das Verzeichnis mit dem im Konstruktor angegebenen Namen.
DirectoryInfo: DirectoryInfo(String^ path)
der Konstruktor erzeugt ein DirectoryInfo Objekt zu dem Verzeichnis mit dem als path übergebenen Namen an legt ein Verzeichnis mit dem Namen an, der void Create() im Konstruktor angegeben wurde löscht das Verzeichnis mit dem Namen path virtual void Delete() override gibt an, ob das Verzeichnis mit dem Namen virtual property bool Exists path existiert, der im Konstruktor angegeben wurde array^ gibt DirectoryInfo–Informationen für alle GetDirectories() Unterverzeichnisse zurück array^ GetFiles() gibt FileInfo–Informationen für alle Dateien zurück static array^ gibt die Namen der logischen Laufwerke in GetLogicalDrives() der Form „c:\“ zurück property DirectoryInfo^ Parent gibt Informationen über das übergeordnete Laufwerk zurück gibt Informationen über das Stammproperty DirectoryInfo^ Root verzeichnis zurück
1244
10 Einige Elemente der .NET-Klassenbibliothek
DirectoryInfo: der vollständige Pfad String^ FullPath verschiebt eine Datei oder ein Verzeichnis void MoveTo( String^ destDirName) Beispiel: Die nächsten beiden Anweisungen legen das Verzeichnis „c:\test“ an, falls es nicht schon existiert: DirectoryInfo^ di=gcnew DirectoryInfo("c:\\test"); if (!di->Exists) di->Create();
Die Funktion showFilesOfPath wurde schon in Abschnitt 9.5 als Beispiel für CLI-Arrays vorgestellt. Sie gibt für das als Argument übergebene Verzeichnis alle Dateien, das Datum und die Größe aus: void showFilesOfPath(String^ path, TextBox^ tb) { DirectoryInfo^ di=gcnew DirectoryInfo(path); for each (FileInfo^ i in di->GetFiles()) tb->AppendText(i->Name+" "+i->LastWriteTime+ " "+i->Length+"\r\n"); }
Mit überladenen Versionen der Methoden GetFiles und GetDirectories erhält man nur die Dateien, deren Namen einem bestimmten Muster entsprechen. Diese werden in Abschnitt 5.3.7 verwendet, um Verzeichnisse rekursiv zu durchsuchen. Auch die schon in Abschnitt 10.13.1 vorgestellte Klasse Environment (Namensbereich System) enthält Methoden und Eigenschaften für Verzeichnisse.
Environment Eigenschaft oder Methode static array^ GetLogicalDrives() static String^ GetFolderPath (SpecialFolder folder)
Rückgabewert bzw. Operation
Laufwerke des Rechners als String
eines der speziellen Verzeichnisse, wie z.B. “Eigene Dateien“ (mit dem Argument SpecialFolder::MyDocuments) das aktuelle Arbeitsverzeichnis
static property String^ CurrentDirectory static property String^ Systemverzeichnis als vollständiger Pfad SystemDirectory Beispiel: Mit den Anweisungen
10.14 .NET-Klassen zur Dateibearbeitung
1245
for each (String^ s in Environment:: GetLogicalDrives()) tb->AppendText(s+" "); tb->AppendText(Environment::GetFolderPath( Environment::SpecialFolder::MyDocuments)); tb->AppendText(Environment::SystemDirectory);
erhält man z.B. die folgenden Strings: A:\ C:\ D:\ C:\Dokumente und Einstellungen\..\Eigene Dateien C:\WINDOWS\system32
Die Klasse Application (Namensbereich System::Windows::Forms) enthält Informationen über Verzeichnisse, die zur aktuelle Anwendung gehören. Alle diese Eigenschaften sind static.
Application Eigenschaften static property String^ CommonAppDataPath static property String^ UserAppDataPath static property String^ StartupPath
Verzeichnis für die Anwendungsdaten, das von allen Anwendern genutzt wird Verzeichnis für die Anwendungsdaten des aktuellen Anwenders das Verzeichnis, von dem aus die Anwendung gestartet wurde (ohne den Namen der Anwendung) static property String^ das Verzeichnis, von dem aus die AnExecutablePath wendung gestartet wurde (einschließlich des Namens der Anwendung)
Beispiel: Die Strings CommonAppDataPath und UserAppDataPath sehen z.B. etwa folgendermaßen aus: C:\Dokumente und Einstellungen\All Users\.. C:\Dokumente und Einstellungen\..
Die Klassen File und FileInfo (Namensbereich System::IO) enthalten Methoden und Eigenschaften für Dateien. In beiden Klassen findet man viele Elemente, die Ähnlichkeiten haben. Sie unterscheiden sich aber dadurch, dass alle Methoden von File statisch sind, und dass sie unterschiedliche Informationen zurückgeben.
File: Eigenschaft oder Methode static bool Exists(String^ path)
static void Delete(String^ path)
static void Copy(String^ sourceFileName, String^ destFileName)
gibt an, ob die Datei mit dem als Pfad angegebenen Namen existiert löscht die Datei mit dem als Pfad angegebenen Namen kopiert die Datei
1246
10 Einige Elemente der .NET-Klassenbibliothek
File: Eigenschaft oder Methode static void Move(String^ sourceFileName, verschiebt die Datei String^ destFileName) static FileStream^ OpenRead(String^ path) gibt einen FileStream (siehe Abschnitt 10.14.2) zurück, mit dem man eine Datei lesen kann static StreamReader^ OpenText(String^ gibt einen StreamReader (siehe Abschnitt 10.14.3) zurück, mit path) dem man eine Datei lesen kann Mit weiteren Funktionen kann man – Attribute (z.B. ReadOnly, Hidden usw.) – Zeitinformationen (z.B. des letzten Zugriffs) usw. einer Datei lesen und setzen. Beispiel: Die folgenden Anweisungen verschieben eine Datei, falls sie existiert: String^ path = "c:\\test\\file1.txt"; if (File::Exists(path)) File::Move(path, "c:\\temp\\file1.txt");
Ein FileInfo-Objekt wird mit dem Konstruktor
FileInfo(String^ fileName) angelegt. Die Eigenschaften und Methoden des Objekts beziehen sich dann auf die Datei mit dem im Konstruktor angegebenen Namen.
FileInfo: Eigenschaft oder Methode virtual property bool Exists virtual void Delete() override FileInfo^ CopyTo(String^ destFileName, bool overwrite)
void MoveTo(String^ destFileName) property DirectoryInfo^ Directory
property String^ DirectoryName property long long Length property String^ Extension
gibt an, ob die Datei existiert löscht die Datei kopiert die Datei und überschreibt eine existierende Zieldatei, wenn overwrite true ist verschiebt die Datei das Verzeichnis, das die Datei enthält das Verzeichnis als String die Größe der Datei (in Bytes) die Namensendung der Datei
Beispiel: Die nächsten beiden Anweisungen haben denselben Effekt wie das letzte Beispiel:
10.14 .NET-Klassen zur Dateibearbeitung
1247
FileInfo^ f=gcnew FileInfo("c:\\test\\file1.txt"); if (f->Exists) f->MoveTo("c:\\temp\\file1.txt");
Wie bei der Klasse File kann man mit weiteren Funktionen Attribute, Zeitinformationen usw. einer Datei lesen und setzen. Die Klasse Path (Namensbereich System::IO) enthält Funktionen für Strings, die Pfadnamen darstellen. Sie geben für einen als Parameter path übergebenen Pfad die Namensbestandteile zurück, die dem Verzeichnis, der Namenserweiterung usw. entsprechen.
Path: Eigenschaft oder Methode static String^ GetDirectoryName( String^ path) static String^ GetExtension(String^ path) static String^ ChangeExtension( String^ path, String^ extension) static String^ GetFileNameWithoutExtension(String^ path) static String^ GetFileName(String^ path) static String^ GetPathRoot(String^ path) static String^ GetFullPath(String^ path) static String^ GetTempFileName()
Rückgabewert der Verzeichnisname
die Namenserweiterung ändert die Namenserweiterung
der Dateiname ohne die Namenserweiterung der Dateiname das Stammverzeichnis der vollständige Pfad erzeugt eine Datei der Länge 0 mit einem eindeutigen Namen und gibt ihren Namen zurück gibt den Namen des Temporärstatic String^ GetTempPath() Verzeichnisses zurück setzt Pfadangaben zusammen, static String^ Combine(String^ path1, String^ path2) wobei unnötige Trennzeichen entfernt und notwendige eingefügt werden
Beispiel: Die Ergebnisse der Funktionen sind als Kommentar angegeben: String^ String^ String^ String^ String^ String^ String^ String^
p="C:\\myDir\\fn.ext" dn=Path::GetDirectoryName(p); // C:\myDir ex=Path::GetExtension(p); // .ext fn=Path::GetFileName(p); // fn.ext n=Path::GetFileNameWithoutExtension(p);//fn pr=Path::GetPathRoot(p); // "C:\" cb=Path::Combine(dn,fn); // C:\mydir\fn.ext tp=Path::GetTempPath(); // C:\temp
1248
10 Einige Elemente der .NET-Klassenbibliothek
10.14.7 Die Klasse FileSystemWatcher Die Klasse FileSystemWatcher (siehe auch die entsprechende Komponente in der Toolbox, Registerkarte „Komponenten“) überwacht Meldungen über Änderungen im Dateisystem und löst Ereignisse aus, wenn ein Verzeichnis geändert wurde oder eine Datei in einem Verzeichnis eingefügt, geändert oder gelöscht wurde. Sie hat dazu die Ereignisse
event FileSystemEventHandler^ Changed event FileSystemEventHandler^ Created event FileSystemEventHandler^ Deleted event RenamedEventHandler^ Renamed Diese Event-Handler haben die folgenden Delegat-Typen:
delegate void FileSystemEventHandler(Object^ sender, FileSystemEventArgs^ e) delegate void RenamedEventHandler(Object^ sender, RenamedEventArgs^ e) deren Event-Argumente die folgenden Eigenschaften haben:
property String^ FullPath property WatcherChangeTypes ChangeType // Ein C++/CLI-Aufzählungstyp Das zu überwachende Verzeichnis und das Muster für die Dateinamen (z.B. "*.txt") kann im Konstruktur
FileSystemWatcher(String^ path, String^ filter) oder über die Eigenschaften Path und Filter gesetzt werden, und die Veränderungen, die ein Ereignis auslösen sollen, über einen Wert der Eigenschaft NotifyFilter. Dieser Eigenschaft können Werte des C++/CLI-Aufzählungstyps NotifyFilters
Attributes LastAccess
CreationTime LastWrite
DirectoryName Security
FileName Size
zugewiesen werden. Damit der FileSystemWatcher auch tatsächlich Ereignisse auslöst, muss er mit der Eigenschaft
property bool EnableRaisingEvents aktiviert werden.
10.14.8 Komprimieren und Dekomprimieren von Dateien Im Namensbereich System::IO::Compression steht die Klasse GZipStream zur Verfügung, mit der man eine Datei mit dem gzip-Verfahren komprimieren und dekomprimieren kann. Die von GZipStream verwendeten Verfahren sind mit den
10.14 .NET-Klassen zur Dateibearbeitung
1249
gzip Programmen oder mit WinRar kompatibel, aber mit dem verbreiteten Programm Winzip nur, wenn die mit GZipStream erzeugte Datei die Endung „.z“ hat. GZipStream hat aber den Nachteil, dass der Dateiname nicht gespeichert wird und eine komprimierte Datei nur eine Datei enthalten kann.
GZipStream hat einen Konstruktor, dem man als Argument für stream einen FileStream übergeben kann. GZipStream(Stream^ stream, CompressionMode mode) Für mode übergibt man einen der Werte Compress und Decompress des C++/CLIAufzählungstyps CompressionMode. Sie legen fest, ob komprimiert oder dekomprimiert werden soll. Bei einem zum Komprimieren geöffneten GZipStream werden mit der Methode
virtual void Write(array^ array, int offset, int count) override die Daten aus dem als Argument übergebenen Array komprimiert in den im Konstruktor angegebenen Stream geschrieben. Beispiel: Die Funktion gzCompress komprimiert die Datei mit dem als Argument für src übergebenen Namen in eine Datei mit dem Namen gzfn: void gzCompress(String^ src, String^ gzfn) { using namespace System::IO::Compression; using namespace System::IO; // Lese die gesamte Datei in das Array buffer ein FileStream^ fin = gcnew FileStream(src, FileMode::Open, FileAccess::Read); array^buffer=gcnew array(fin->Length); fin->Read(buffer, 0, buffer->Length ); fin->Close(); FileStream^ fout = gcnew FileStream(gzfn, FileMode::Create, FileAccess::Write); GZipStream ^ gzs = gcnew GZipStream(fout, CompressionMode::Compress,true ); // Schreibe den Puffer komprimiert in die Datei gzs->Write(buffer, 0, buffer->Length); gzs->Close(); fout->Close(); }
Bei einem zum Dekomprimieren geöffneten GZipStream werden mit der Methode
virtual int Read(array^ array, int offset, int count) override die komprimierten Daten aus dem beim Öffnen angegebenen Stream gelesen und in dekomprimierter Form in das als Argument übergebene Array geschrieben.
1250
10 Einige Elemente der .NET-Klassenbibliothek
Beispiel: Die Funktion gzDecompress dekomprimiert die Datei mit dem als Argument für gzfn übergebenen Namen in eine Datei mit dem Namen dst: void gzDecompress(String^ gzfn, String^ dst) { using namespace System::IO::Compression; using namespace System::IO; FileStream^ fin = gcnew FileStream(gzfn, FileMode::Open, FileAccess::Read); array^buffer=gcnew array(fin->Length); FileStream^ fout = gcnew FileStream(dst, FileMode::Create, FileAccess::Write); GZipStream^ gzs = gcnew GZipStream(fin, CompressionMode::Decompress,true ); bool continueLoop=true; while (continueLoop) { int n=gzs->Read(buffer, 0, buffer->Length); if (n>0) fout->Write(buffer,0,n); else continueLoop=false; } gzs->Close(); fout->Close(); fin->Close(); }
Selbstverständlich sollten diese Funktionen auch noch Exceptions abfangen und sicherstellen, dass geöffnete Dateien auch bei einer Exception geschlossen werden.
Aufgaben 10.14.8 1. Eine Funktion MessageLogger soll einen als Parameter übergebenen String immer derselben Datei (einer „zentralen Log-Datei“) am Ende anfügen. 2. Definieren Sie eine Klasse DirLogger, der im Konstruktor ein Verzeichnis und ein Filter (als String) übergeben wird. Diese Klasse soll einen entsprechend konfigurierten FileSystemWatcher enthalten, der mit jeder Änderung im Verzeichnis einer Textdatei (z.B. "c:\\fs.log") eine Meldung mit der Uhrzeit der Änderung und dem Namen der geänderten Datei hinzufügt. Definieren Sie dazu geeignete Ereignisbehandlungsroutinen für den FileSystemWatcher.
10.15 Serialisierung Die Darstellung eines Objekts durch eine Folge von Bytes, aus denen das Objekt dann auch wieder hergestellt werden kann, wird auch als Serialisierung bezeichnet. Die Folge der Bytes kann dann als Binärdatei oder Textdatei (z.B. im XMLFormat) auf einem Datenträger gespeichert und wieder eingelesen werden.
10.15 Serialisierung
1251
Serialisierung hat viele Gemeinsamkeiten mit dem Speichern von Objekten eines Klassentyps in einem Stream. Der Unterschied zwischen einer Speicherung mit den Stream-Klassen (z.B. denen der Standardbibliothek oder aus System::IO) und den .NET-Techniken zur Serialisierung zeigt sich bei Klassen, deren Daten nicht nur durch den Inhalt des Hauptspeichers ab der Anfangsadresse gegeben sind. Wenn eine Klasse Zeiger oder andere Verweise auf Daten enthält, müssen nicht die Zeiger, sondern die zugehörigen Daten (auf die der Zeiger zeigt) gespeichert werden. Bei der Serialisierung werden diese Daten automatisch so gespeichert, dass sie auch wieder gelesen werden können. Serialisierung kann deshalb eine große Vereinfachung gegenüber der Arbeit mit den Streamklassen der Standardbibliothek und von .NET sein. Serialisierung unter .NET ist für C++/CLI-Klassen einfach und erfordert lediglich die beiden Punkte: 1. Damit Objekte eines Klassentyps serialisiert und deserialisiert werden können, muss das Attribut [Serializable] vor der Definition der Klasse angegeben werden. Da dieses Attribut nicht vererbt wird, muss es in einer Klassenhierarchie bei der Basisklasse und den abgeleiteten Klasse angegeben werden. 2. Die eigentliche Serialisierung und Deserialisierung besteht dann lediglich aus dem Aufruf der Methoden
virtual void Serialize(Stream^ serializationStream, Object^ graph) sealed virtual Object^ Deserialize(Stream^ serializationStream) sealed eines sogenannten Formatters (siehe Abschnitte 10.15.1 bis 10.15.3). Diese beiden Methoden sind die einzigen Methoden der Interface-Klasse IFormatter, die ein Formatter implementieren muss. Durch den Aufruf von Serialize bzw. Deserialize werden alle Datenelemente der Klasse serialisiert. Lokale Variablen von Funktionen und statische Klassenelemente werden nicht serialisiert. Falls einzelne Elemente nicht serialisiert werden sollen, können sie mit [NonSerialized] gekennzeichnet werden. Der Aufruf von Serialize oder Deserialize mit einem Objekt, das nicht mit [Serializable] definiert wurde oder solche Elemente enthält, löst eine Exception aus. Zahlreiche .NET-Klassen sind wie die Collection-Klassen
1252
10 Einige Elemente der .NET-Klassenbibliothek
[SerializableAttribute] [ComVisibleAttribute(true)] public ref class Queue : ICollection, IEnumerable, ICloneable und
[SerializableAttribute] generic public ref class List : IList, ICollection, IEnumerable, IList, ICollection, IEnumerable mit dem SerializableAttribute definiert. Deshalb können Objekte solcher Klassen einfach als Datei gespeichert und aus einer solchen Datei wieder eingelesen werden. Die Serialisierung unter .NET ist aber nur für C++/CLI-Klassen möglich. Native Klassen können mit der Boost serialization Bibliothek (siehe Abschnitt 4.7) serialisiert werden.
10.15.1 Serialisierung mit BinaryFormatter Die Klasse
public ref class BinaryFormatter sealed : IRemotingFormatter, IFormatter aus dem Namensbereich System::Runtime::Serialization::Formatters::Binary serialisiert Objekte in einem binären Format. Da BinaryFormatter das Interface IFormatter implementiert, stehen die beiden Methoden
virtual void Serialize(Stream^ serializationStream, Object^ graph) sealed virtual Object^ Deserialize(Stream^ serializationStream) sealed zur Serialisierung und Deserialisierung zur Verfügung. Dabei wird als Argument für den ersten Parameter ein geöffneter Stream aus System::IO übergeben Beispiel: Ein Objekt der Klasse myR [Serializable] ref struct myDate{ int day, month, year; myDate(int d, int m, int y): day(d),month(m),year(y){} virtual String^ ToString() override { return "d="+day+" m="+month+" y="+year; } };
10.15 Serialisierung
1253
[Serializable] ref struct myR{ myR(int i_, String^ s_, myDate^ d_): i(i_),s(s_),d(d_){} int i; String^ s; myDate^ d; virtual String^ ToString() override { return "i="+i+" date="+d+" s="+s; } };
wird mit durch den Aufruf der Funktion void BinSerialize(TextBox^ tb) { myR^ r=gcnew myR(17,"ich", gcnew myDate(17,7,2007)); FileStream^ fs = gcnew FileStream("Bin.dat",FileMode::Create); BinaryFormatter^ bf = gcnew BinaryFormatter; try { bf->Serialize(fs,r); } catch (Exception^ e) { tb->AppendText("Serialize-Fehler: "+e->Message); throw; } finally { fs->Close(); } }
serialisiert und durch void BinDeserialize(TextBox^ tb) { myR^ r = nullptr; FileStream^ fs = gcnew FileStream("Bin.dat",FileMode::Open); try { BinaryFormatter^ bf = gcnew BinaryFormatter; r = dynamic_cast(bf->Deserialize(fs)); } catch (Exception^ e) { tb->AppendText("Deserialize-Fehler: "+ e->Message); throw; } finally { fs->Close(); } tb->AppendText(r+"\r\n"); }
1254
10 Einige Elemente der .NET-Klassenbibliothek
wieder deserialisiert. Speichert man Objekte dieser Klassen mit FileStream-Methoden in einem File-Stream, wird der aktuelle Wert des Zeigers (die Adresse) in der Datei gespeichert. Wenn diese Datei später dann wieder gelesen wird, kann man nicht erwarten, dass an dieser Adresse tatsächlich auch ein String steht. Da die Collection-Klassen mit dem Attribut [Serializable] definiert sind, können ganze Collections einfach in einer Datei gespeichert und wieder aus der Datei eingelesen werden. Mit BinaryFormatter können sowohl generische als auch nicht generische Collections serialisiert werden. Beispiel: Eine ganze Collection kann wie in void SerializeList(TextBox^ tb) { using namespace System::Runtime::Serialization:: Formatters::Binary; using namespace System::Collections::Generic; List^ lst=gcnew List; lst->Add(gcnew myR(17,"ich", gcnew myDate(17,7,2007))); lst->Add(gcnew myR(18,"du", gcnew myDate(18,7,2007))); lst->Add(gcnew myR(17,"er+sie", gcnew myDate(19,7,2007))); FileStream^ fs = gcnew FileStream("List.dat",FileMode::Create); BinaryFormatter^ bf = gcnew BinaryFormatter; try { bf->Serialize(fs,lst); } catch (Exception^ e) { tb->AppendText("Serialize-Fehler: "+e->Message); throw; } finally { fs->Close(); } }
serialisiert und wie in DeserializeList deserialisiert werden: void DeserializeList(TextBox^ tb) { using namespace System::Collections::Generic; using namespace System::Runtime::Serialization:: Formatters::Soap; List^ lst = nullptr; FileStream^ fs = gcnew FileStream("List.soap",FileMode::Open); try { BinaryFormatter^ bf = gcnew BinaryFormatter; lst = dynamic_cast (bf->Deserialize(fs));
10.15 Serialisierung
1255
} catch (Exception^ e) { tb->AppendText("Fehler:"+e->Message); throw; } finally { fs->Close(); } for each (myR^ x in lst) tb->AppendText(x+"\r\n"); }
10.15.2 Serialisierung mit SoapFormatter Die Klasse
public ref class SoapFormatter sealed : IRemotingFormatter, IFormatter aus dem Namensbereich System::Runtime::Serialization::Formatters::Soap serialisiert die Objekte in dem auf XML basierenden SOAP-Format (Simple Object Access Protocol). Damit sie verfügbar ist, muss zuerst #using
angegeben werden. Eine solche Angabe ist für BinaryFormatter nicht notwendig. Ein weiterer Unterschied zu BinaryFormatter ist, dass mit SoapFormatter nur nicht generische Collections serialisiert werden können. Die Serialisierung von generischen Collections ist mit SoapFormatter nicht möglich. Die Anweisungen zur Serialisierung und Deserialisierung sind aber weitgehend identisch mit denen für BinaryFormatter. Beispiel: Ein Objekt der Klasse myR aus dem Beispiel von Abschnitt 10.15.1 wird mit der Funktion void SoapSerialize(TextBox^ tb) { // Der Namensbereich using namespace System::Runtime::Serialization:: Formatters::Soap; // steht nur nach // #using notwendig // zur Verfügung myR^ r=gcnew myR(17,"ich",gcnew myDate(17,7,2007)); FileStream^ fs = gcnew FileStream("Soap.txt",FileMode::Create); SoapFormatter^ sf = gcnew SoapFormatter; try { sf->Serialize(fs,r); }
1256
10 Einige Elemente der .NET-Klassenbibliothek
catch (Exception^ e) { tb->AppendText("Serialize-Fehler: "+e->Message); throw; } finally { fs->Close(); } }
serialisiert. Ein Auszug aus der dabei erzeugten Datei: ... 17 ich 17 7 2007
und kann durch die folgende Funktion wieder deserialisiert werden: void SoapDeserialize(TextBox^ tb) { using namespace System::Runtime::Serialization:: Formatters::Soap; myR^ r = nullptr; FileStream^ fs = gcnew FileStream("Soap.txt",FileMode::Open); try { SoapFormatter^ sf = gcnew SoapFormatter; r = dynamic_cast(sf->Deserialize(fs)); } catch (Exception^ e) { tb->AppendText("Fehler: "+e->Message); throw; } finally { fs->Close(); } tb->AppendText(r+"\r\n"); }
10.15.3 Serialisierung mit XmlSerializer Mit der Klasse XmlSerializer aus dem Namensbereich System::Xml::Serialization können Objekte im XML-Format serialisiert werden. Dabei werden die Daten im XML-Format geschrieben und nicht wie beim SoapFormatter als Soap-Message,
10.15 Serialisierung
1257
die XML-Daten enthält. Damit ist ein Austausch von Daten möglich, die nicht nur unter CLI oder .NET erzeugt wurden.
XmlSerializer unterscheidet sich in einigen Punkten von BinaryFormatter und SoapFormatter: – Bei XmlSerializer muss der Datentyp im Konstruktor angegeben werden:
XmlSerializer(Type^ type) Ein SoapFormatter analysiert dagegen die Daten zur Laufzeit, was ihn deutlich langsamer macht als einen XmlSerializer. – Damit Klassen mit dem XmlSerializer serialisiert und deserialisiert werden können; müssen sie – public sein (siehe Abschnitt 9.1.4) und – einen Standardkonstruktor haben (das schließt Werteklassen aus) – In die Xml-Datei werden dann – public Datenelemente und – public Eigenschaften aufgenommen. Die Angabe [Serializable] ist möglich, hat aber keinen Einfluss darauf, welche Daten aufgenommen werden. Beispiel: Ergänzt man die Klassen aus dem Beispiel von Abschnitt 10.15.1 um die Sichtbarkeitsangabe public und um einen Standardkonstruktor public ref struct myDate{ int day, month, year; myDate():day(1),month(1),year(2000) {} // .Rest wie oben }; public ref struct myR{ myR():i(17),s("18"){} // .Rest wie oben };
können Objekte dieser Klassen folgendermaßen serialisiert werden: void XmlSerialize(TextBox^ tb) { using namespace System::Xml::Serialization; myR^ r=gcnew myR(17,"ich",gcnew myDate(17,7,2007)); FileStream^ fs = gcnew FileStream("ser.xml",FileMode::Create); XmlSerializer^ x = gcnew XmlSerializer(myR::typeid);
1258
10 Einige Elemente der .NET-Klassenbibliothek
try { x->Serialize(fs,r); } catch (Exception^ e) { tb->AppendText("Serialize-Fehler: "+e->Message); throw; } finally { fs->Close(); } }
Die dabei erzeugte Datei sieht etwa folgendermaßen aus: 17 ich 17 7 2007
Eine Deserialisierung kann wie in dieser Funktion erfolgen: void XmlDeserialize(TextBox^ tb) { using namespace System::Xml::Serialization; myR^ r = nullptr; FileStream^ fs = gcnew FileStream("ser.xml",FileMode::Open); XmlSerializer^ x = gcnew XmlSerializer(myR::typeid); try { r = dynamic_cast(x->Deserialize(fs)); } catch (Exception^ e) { tb->AppendText("Fehler: "+e->Message); throw; } finally { fs->Close(); } tb->AppendText(r+"\r\n"); }
Aufgaben 10.15 Legen Sie Daten des Typs
10.16 Datenbanken
1259
ref struct myR{ myR():i(17),s("18"){} myR(int i_, String^ s_, myDate^ d_) :i(i_),s(s_),d(d_){} int i; String^ s; myDate^ d; virtual String^ ToString() override { return "i="+i+" date="+d+" s="+s; } };
in einem generischen List-Container ab. Dabei soll myDate folgendermaßen definiert sein: ref struct myDate{ int day, month, year; myDate():day(1),month(1),year(2000) {} myDate(int d, int m, int y):day(d),month(m),year(y) {} virtual String^ ToString() override { return "d="+day+" m="+month+" y="+year; } };
Serialisieren und deserialisieren Sie den Inhalt dieses Containers a) mit einem BinaryFormatter c) mit einem XMLFormatter
b) mit einem SoapFormatter
10.16 Datenbanken In Zusammenhang mit der dauerhaften Speicherung von Daten auf einem Datenträger treten bestimmte Aufgaben immer wieder in ähnlicher Form auf: – Mehrere Anwender bzw. Programme sollen auf denselben Datenbestand zugreifen können, ohne dass die Gefahr besteht, dass sie sich ihre Daten überschreiben. – Bei großen Datenbeständen ist die sequenzielle Suche nach einem bestimmten Datensatz oft zu langsam. – Die Anweisungen zur Bearbeitung der Daten sollen leicht von einem Betriebssystem auf ein anderes portiert werden können. – Bei einer Gruppe von Anweisungen sollen entweder alle Anweisungen oder keine ausgeführt werden. Diese Anforderung soll insbesondere auch bei einem Programmabsturz oder einem Stromausfall erfüllt werden. Die Lösung solcher Aufgaben ist mit den Stream-Klassen von Standard-C++ bzw. .NET oft relativ aufwendig. Datenbanksysteme ermöglichen oft einfachere Lösun-
1260
10 Einige Elemente der .NET-Klassenbibliothek
gen. Die folgenden Ausführungen sollen kein Lehrbuch über Datenbanken ersetzen, sondern lediglich einen kurzen Einblick geben. Für weitere Informationen wird auf die Online-Hilfe verwiesen. Visual Studio enthält im Register Daten der Toolbox und der .NET Klassenbibliothek zahlreiche Komponenten und Klassen zur Arbeit mit Datenbanken. Für die folgenden Ausführungen ist das kostenlose Datenbanksystem „Microsoft SQL Server Express Edition“ ausreichend. Eine Datenbank besteht aus einer oder mehreren Tabellen, die dauerhaft auf einem Datenträger gespeichert werden. Jede dieser Tabellen ist im Wesentlichen eine Folge von Datensätzen, die man sich mit struct definiert vorstellen kann: struct Datensatz { T1 f1; // ein Feld f1 eines Datentyps T1 T2 f2; // ein Feld f2 eines Datentyps T2 ..., Tn fn; };
Einen Datensatz in einer Tabelle bezeichnet man auch als Zeile der Tabelle und die Gesamtheit der Werte zu einem Datenfeld der Struktur als Spalte. Wir werden zunächst nur mit Datenbanken arbeiten, die aus einer einzigen Tabelle bestehen. Solche Datenbanken entsprechen dann einer Datei mit solchen Datensätzen. Später werden auch Datenbanken behandelt, die aus mehreren Tabellen bestehen.
10.16.1 Eine Datenbank anlegen Unter A) bis E) wird für einige verbreitete Datenbanksysteme kurz skizziert, wie man mit ihnen eine Datenbank anlegen kann. Für andere Datenbanksysteme ist die Vorgehensweise meist ähnlich. Falls Sie auf eine bereits bestehende Datenbank zugreifen wollen, können Sie diesen Abschnitt auch auslassen. Für die Ausführungen in den folgenden Abschnitten ist es weitgehend unerheblich, welches Datenbanksystem verwendet wird. Eine Datenbank kann außerdem auch mit SQL-Anweisungen (siehe Abschnitt 10.16.3) erzeugt werden. Access- und Excel-Datenbanken können mit Access und Excel angelegt werden. Für einige Datenbankverbindungen ist der Name des SQL Servers notwendig. Diesen erhält man mit dem „SQL Server Configuration Manager“, der bei der Installation von „Microsoft SQL Server Express Edition“ mit installiert wird:
10.16 Datenbanken
1261
Hier ist der Name des Servers „SQLEXPRESS2008“.
A) Eine SQL Server-Datenbankdatei mit dem Server Explorer anlegen Mit der Option Verbindung hinzufügen im Kontextmenü zu Datenverbindungen des Server-Explorer (Ansicht|Weitere Fenster|Server Explorer)
kann man eine neue SQL Server Datenbankdatei erstellen, indem als Datenquelle „Microsoft SQL Server-Datenbankdatei (SqlClient)“ gewählt und dann ein neuer Datenbankname eingetragen wird:
1262
10 Einige Elemente der .NET-Klassenbibliothek
Der Server-Explorer ermöglicht außerdem die Verwaltung der Datenbank, wie z.B. das Anlegen von Tabellen:
B) Eine SQL-Server-Datenbank mit dem Server Explorer anlegen Mit dem Server Explorer (Ansicht|Weitere Fenster|Server Explorer) kann man über das Kontextmenü zu Datenverbindungen eine neue Datenbank erstellen. Dazu muss man nur den Server auswählen, anschließend bei einer neuen SQL Server Datenbank eventuell noch „\SQLEXPRESS“ anfügen und den Namen der Datenbank eintragen:
10.16 Datenbanken
1263
Auch solche Datenbanken können im Server-Explorer wie unter A) verwaltet werden.
C) Eine SQLExpress-Datenbank anlegen Nach der Installation der „SQL Server Express Edition“ und „SQL Server Management Studio Express“ steht auch ein Verwaltungsprogramm zur Verfügung, mit dem man Datenbanken anlegen und bearbeiten kann. In der nächsten Abbildung wird eine Datenbank mit dem Namen SQL_db4 angelegt:
In dieser Datenbank kann dann mit der Option Tables|New Table im Kontextmenü von Tables eine Tabelle angelegt werden:
1264
10 Einige Elemente der .NET-Klassenbibliothek
D) Eine MySQL-Datenbank anlegen Nach der Installation des MySQL Servers und Clients, Administrator und Connector (http://www.mysql.org) kann man mit dem Administrator
eine Datenbank (nach dem Anklicken von Catalogs, im Kontextmenü unter Schemata mit Create New Schema) und Tabellen (im Kontextmenü unter Schema Tables) anlegen.
E) Benannte Bereiche in Excel als Datenbank-Tabellen Excel-Tabellen sind ganz bestimmt keine optimalen Datenbanken. Man kann aber benannte Bereiche einer Excel-Tabelle als Datenbank ansprechen und so von einem Programm aus auf ihre Daten zugreifen. Einen benannten Bereich erhält man, indem man einen Bereich in Excel markiert und dann im Namensfeld einen Namen („DbTabelle“ in der Abbildung) einträgt.
10.16 Datenbanken
1265
10.16.2 Die Verbindung zu einer Datenbank herstellen Im Folgenden wird für einige verbreitete Datenbanksysteme kurz skizziert, wie man mit dem Server Explorer (Ansicht|Weitere Fenster|Server Explorer) im Kontextmenü zu Datenverbindungen
eine Verbindung zu einer existierenden Datenbank herstellen kann. Für andere Datenbanksysteme ist die Vorgehensweise meist ähnlich. Als nächstes wird der Dialog „Verbindung hinzufügen“ angezeigt. Nachdem man über diesen Dialog alle notwendigen Einträge gemacht hat, kann man mit dem Button „Testverbindung“ prüfen, ob eine Verbindung möglich ist. Unter Erweitert wird ein Verbindungsstring (ConnectionString) angezeigt, der die Datenbank identifiziert. Diesen String kann man in das Programm kopieren und dann den entsprechenden Funktionen als Argument übergeben.
1266
10 Einige Elemente der .NET-Klassenbibliothek
A) Eine Verbindung zu einer SQL Server Datenbank herstellen Im Dialog Verbindung hinzufügen wählt man zuerst als Datenquelle „Microsoft SQL Server“ aus. Dadurch wird „Microsoft SQL Server (SqlClient)“ als Datenquelle eingetragen. Dann wählt man unter Servername einen der angebotenen Server aus. Falls keiner angeboten wird, muss man ihn manuell eintragen. Dazu muss man dann nur noch den Datenbanknamen auswählen:
B) Eine Verbindung zu einer SQL Datenbankdatei herstellen Im Dialog Verbindung hinzufügen wählt man zuerst als Datenquelle „Microsoft SQL Server-Datenbankdatei (SqlClient)“ aus. Dann muss man nur noch den Namen der Datenbankdatei auswählen.
10.16 Datenbanken
1267
C) Eine Verbindung zu einer MySQL-Datenbank herstellen Im Dialog Verbindung hinzufügen wählt man als Datenquelle „MySQL Database“ oder „“ mit dem „.NET Framework DataProvider for MySQL“.
Falls das nicht funktioniert (Bug: eine Eingabe schließt den Dialog) kann man auch eine „Microsoft ODBC-Datenquelle“ verwenden. Dann klickt man zuerst den RadioButton „Datenquellennamen …“ an und wählt mysql_dsn. Danach markiert man den RadioButton „Verbindungszeichenfolge verwenden“ und klickt den Button Erstellen an. Danach klickt man im Dialog Datenquelle auswählen im Register Computerdatenquelle auf Neu und wählt unter „Neue Datenquelle erstellen“ den MySQL ODBC Driver. OK. Daraufhin meldet sich der Connector, in dem man einige Angaben von der Installation sowie die Datenbank eintragen muss. Der unter Data Source Name eingegebene Name ist dann der Name, der später als Datenquelle angegeben werden muss:
Die Verbindungszeichenfolge kann man danach im Server-Explorer nach „Verbindung ändern“ kopieren.
1268
10 Einige Elemente der .NET-Klassenbibliothek
D) Eine Verbindung zu einer Access-Datenbank herstellen Eine Verbindung zu einer Microsoft Access Datenbank erhält man, indem man als Datenquelle „Microsoft Access-Datenbankdatei“ und anschließend den Namen der Datenbankdatei wählt. Alternativ kann man eine AccessDatenbank auch als ODBC-Datenquelle ansprechen.
E) Excel-Tabellen als Datenbank Einen benannten Bereich einer Excel-Tabelle (siehe Abschnitt 10.16.1) kann man als Datenbanktabelle ansprechen. Die Verbindung zu der Excel-Tabelle kann man im Dialog Verbindung hinzufügen über Microsoft ODBC Datenquelle und „Datenquellennamen …“ Excel-Dateien herstellen. Damit eine Verbindungszeichenfolge erstellt wird, muss man anschließend den RadioButton „Verbindungszeichenfolge verwenden“ und „Erstellen“ anklicken. Dann unter „Computerdatenquelle“ „Excel-Dateien“ auswählen und OK anklicken, und dann den Dateinamen auswählen. Nach dem Anklicken von Erweitert im Dialog Verbindung hinzufügen wird unten der Verbindungsstring angezeigt, den man über die Zwischenablage in das Programm kopieren kann.
10.16 Datenbanken
1269
10.16.3 SQL-Anweisungen SQL („Structured Query Language“ – strukturierte Abfragesprache) ist eine Sprache, die sich für Datenbanken als Standard durchgesetzt hat. SQL-Anweisungen werden auch von Datenbanken unterstützt, die im engeren Sinn keine SQL-Datenbanken sind. Obwohl das „Query“ im Namen nahe legt, dass damit nur Abfragen möglich sind, enthält SQL auch Anweisungen zum Erstellen von Datenbanken. Da SQL eine recht umfangreiche Sprache ist, sollen die folgenden Ausführungen keinen Überblick über SQL geben, sondern lediglich einige der wichtigsten SQLAnweisungen vorstellen, sowie einige Klassen, mit denen man solche Anweisungen ausführen kann. Das sind vor allem die Klassen
SqlCommand OdbcCommand OleDbCommand OracleCommand
// Namensbereich System::Data::SqlClient // Namensbereich System::Data::Odbc // Namensbereich System::Data::OleDb // Namensbereich System::Data::OracleClient
mit denen man SQL-Anweisungen für den jeweiligen Datenbanktyp ausführen kann. Sie sind alle von der abstrakten Basisklasse DbCommand (Namensbereich System::Data::Common) abgeleitet und überschreiben bzw. verdecken die folgenden Methoden von DbCommand:
virtual int ExecuteNonQuery() abstract virtual Object^ ExecuteScalar() abstract DbDataReader^ ExecuteReader() Beim Aufruf dieser Funktionen wird der durch die Eigenschaft
virtual property String^ CommandText dargestellte SQL-Befehl mit der Datenbank ausgeführt, mit der über die Eigenschaft
property DbConnection^ Connection eine Verbindung hergestellt wurde, und die mit Open geöffnet wurde. Die Methode
1270
10 Einige Elemente der .NET-Klassenbibliothek
int ExecuteNonQuery() wird für SQL-Befehle verwendet, die keine Daten zurückgeben. Das sind typischerweise DML-Befehle (Data Manipulation Language) oder DDL-Befehle (Data Definition Language) wie „CREATE TABLE“, INSERT, DELETE, UPDATE und SET. Im nächsten Beispiel wird „CREATE TABLE“ verwendet, um eine Datenbanktabelle mit dem Namen „Kontobew“ anzulegen, die die Spalten „KontoNr“, „Datum“ usw. mit den jeweils angegebenen Datentypen enthält. Beispiel: Die Funktion void createTable(DbCommand^ cmd) { // using namespace System::Data::Common; cmd->CommandText="Create Table Kontobew " "(KontoNr int, Datum DateTime, Bewart char(1), " "Betrag money)"; cmd->Connection->Open(); cmd->ExecuteNonQuery(); }
kann wie in den nächsten drei Funktionen aufgerufen werden: void createSQLTable(String^ ConnString) { using namespace System::Data::SqlClient; SqlCommand^ cmd = gcnew SqlCommand; cmd->Connection=gcnew SqlConnection(ConnString); createTable(cmd); } void createOleDbTable(String^ ConnString) { // z.B. Access using namespace System::Data::OleDb; OleDbCommand^ cmd= gcnew OleDbCommand; cmd->Connection=gcnew OleDbConnection(ConnString); createTable(cmd); } void createOdbcTable(String^ ConnString) { // z.B. Excel using namespace System::Data::Odbc; OdbcCommand^ cmd=gcnew OdbcCommand; cmd->Connection=gcnew OdbcConnection(ConnString); createTable(cmd); }
Jede dieser Funktionen erzeugt dann in der zum ConnectionString ConnString gehörenden Datenbank eine Tabelle mit den in createTable angegebenen Spalten.
10.16 Datenbanken
1271
Diese Beispiele zeigen nicht nur, wie man eine Tabelle in verschiedenen Datenbanksystemen anlegen kann. Sie zeigen außerdem, wie die Architektur dieser Klassen eine weitgehend vom Provider unabhängige Programmierung ermöglicht. Die nächsten Beispiele sind ähnlich aufgebaut und verwenden ebenfalls einen Parameter des Typs DbCommand. Sie können wie createTable mit Argumenten wie im letzten Beispiel aufgerufen werden. – Die Funktion insertData baut den SQL-Befehl zum Einfügen von Daten mit der Methode String::Format auf: void insertData(int KontoNr, Decimal Betrag, DbCommand^ cmd) { cmd->CommandText=String::Format( "Insert into Kontobew (KontoNr, Betrag)" " Values ({0}, {1})", KontoNr,Betrag)"; cmd->Connection->Open(); cmd->ExecuteNonQuery(); cmd->Connection->Close(); }
– Der SQL-Befehl „drop table“ löscht eine Tabelle: void deleteTable(DbCommand^ cmd) { cmd->CommandText="DROP TABLE Kontobew "; cmd->Connection->Open(); cmd->ExecuteNonQuery(); cmd->Connection->Close(); }
Für SQL-Befehle, die einen Wert zurückgeben, verwendet man die Methode
Object^ ExecuteScalar() Der vom SQL-Befehl zurückgegebene Wert ist dann der Rückgabewert dieser Funktion. Er wird durch eine Konversion in den entsprechenden Typ konvertiert. Beispiel: Die Funktion Count gibt die Anzahl der Datensätze zurück int Count(DbCommand^ command ) { cmd->CommandText="Select Count(*) from Kontobew"; cmd->Connection->Open(); return (int)cmd->ExecuteScalar(); }
Die Zeilen einer Tabelle kann man mit einer der von DbDataReader abgeleiteten Klassen
1272
10 Einige Elemente der .NET-Klassenbibliothek
SqlDataReader OdbcDataReader OleDbDataReader OracleDataReader SqlCeDataReader
// Namensbereich System::Data::SqlClient // Namensbereich System::Data::Odbc // Namensbereich System::Data::OleDb // Namensbereich System::Data::OracleClient // Namensbereich System::Data::SqlServerCe
lesen. Objekte dieser Klassen können nicht mit einem Konstruktor, sondern nur durch einen Aufruf der Methoden
DbDataReader^ ExecuteReader() mit einem Objekt einer der von DbCommand abgeleiteten Klassen erzeugt werden. Der CommandText dieses Objekts muss eine SQL-Anweisung sein, die Zeilen zurückgibt (normalerweise eine Select-Anweisung). Zahlreiche syntaktische Varianten ermöglichen die Formulierung von komplexen Bedingungen. Einige einfache Beispiele: – Alle Spalten der Tabelle „Kontobew“ erhält man mit "SELECT * FROM Kontobew "
– Will man nur ausgewählte Spalten anzeigen, gibt man ihren Namen nach „SELECT“ an: "SELECT KontoNr,Betrag FROM Kontobew "
– Nach „WHERE“ kann man eine Bedingung angeben, die die Auswahl der Datensätze einschränkt: "SELECT * FROM Kontobew WHERE KontoNr < 1020 and Betrag < 900"
– Nach "ORDER BY" kann man die Felder angegeben, nach denen die Daten sortiert werden. "SELECT * FROM Kontobew ORDER BY KontoNr "
Die von der Select-Anweisung zurückgegebenen Zeilen kann man dann mit
bool Read() // Rückgabewert true, falls Daten gelesen wurden, sonst false lesen. Die Werte der einzelnen Spalten erhält man mit dem Indexoperator. Beispiel: Die Funktion readDbTable gibt die Werte der ersten beiden Spalten (r[0] und r[1]) der Tabelle Kontobew in einer Textbox aus: void readDbTable(TextBox^ tb, DbCommand^ cmd) { // using namespace System::Data::Common; cmd->CommandText="Select * from Kontobew "; cmd->Connection->Open(); DbDataReader^ r=cmd->ExecuteReader();
10.16 Datenbanken
1273
while (r->Read()) tb->AppendText(String::Format("{0} {1} \r\n", r[0],r[1])); r->Close(); }
Diese Funktion kann dann wie createTable (siehe oben) für verschiedene Datenbanktypen aufgerufen werden: void readSQLTable(TextBox^ tb, String^ ConnString) { using namespace System::Data::SqlClient; SqlCommand^ cmd = gcnew SqlCommand; cmd->Connection=gcnew SqlConnection(ConnString); readDbTable(tb,cmd); }
10.16.4 Die Klasse DataTable und die Anzeige in einem DataGridView Die Klassen DataTable und DataSet gehören zu den wichtigsten .NET-Klassen für Datenbanken. Sie stellen Datenbanktabellen bzw. Datenbanken dar und enthalten Methoden, mit denen man wie mit SQL-Anweisungen Datenbanken und Tabellen anlegen und Daten schreiben bzw. lesen kann. Sie bieten außerdem eine einfache Möglichkeit, diese Daten mit Steuerelementen und .NET-Klassen zu verbinden. Das gilt insbesondere für die Zuordnung von Datentypen, was mit SQLAnweisungen nicht immer einfach ist. Die Verbindung mit einer Datenbank ist mit den folgenden Schritten möglich: 1. Ein von der Klasse DbDataAdapter abgeleiteter SqlDataAdapter, OdbcDataAdapter, OleDbDataAdapter usw. stellt eine Verbindung zwischen einer Datenbank und einem DataSet bzw. einer DataTable dar. Die Datenbank kann man über einen ConnectionString und eine SQL Select-Anweisung im Konstruktor angeben:
SqlDataAdapter(String^ selectCommand, String^ selectConnectionString) Die Methoden
virtual int Fill(DataSet^ dataSet) override int Fill(DataTable^ dataTable) füllen das Argument mit den Daten der zum DataAdapter gehörenden Datenbank. using namespace System::Data; // für DataTable using namespace System::Data::Common;//für DbDataAdapter
1274
10 Einige Elemente der .NET-Klassenbibliothek
DataTable^ getDataTable(DbDataAdapter^ a) { DataTable^ table=gcnew DataTable(); a->Fill(table); return table; }
2. Eine DataTable bzw. ein DataSet erhalten durch Fill eine Kopie der Daten. Ändert man Daten in der DataTable bzw. dem DataSet, werden die geänderten Daten erst durch einen Aufruf der Methoden
virtual int Update(DataSet^ dataSet) override int Update(DataTable^ dataTable) eines DataAdapters wieder in die Datenbank geschrieben. Damit diese Änderungen auch tatsächlich an die Datenbank übertragen werden, müssen zuvor für den DataAdapter mit einem CommandBuilder (SqlCommandBuilder usw.) SQL-Anweisungen erzeugt werden, die diese Änderungen durchführen. DataTable^ getSQLDataTable(SqlDataAdapter^ a) { // analog: getOleDbDataTable, getOdbcDataTable usw. SqlCommandBuilder^ cb = gcnew SqlCommandBuilder(a); return getDataTable(a); }
Unterlässt man den Aufruf von SqlCommandBuilder, bewirkt ein Aufruf von Update keine Aktualisierung der Daten in der Datenbank. Bevor weitere Eigenschaften und Methoden der Klasse DataTable vorgestellt werden, wird zuerst gezeigt, wie man die Daten einer Datenbanktabelle mit wenigen Anweisungen in einem DataGridView (Toolbox Registerkarte „Daten“) anzeigen kann. Dazu muss man nur der DataGridView-Eigenschaft
property Object^ DataSource eine BindingSource (Toolbox Registerkarte „Daten“, siehe Abschnitt 10.17.2) zuweisen, deren DataSource-Eigenschaft die DataTable zugewiesen wurde: SqlDataAdapter^ a=gcnew SqlDataAdapter( "select * from KontoBew", ConnectionString); bindingSource1->DataSource=getSQLDataTable(a); // siehe 2. dataGridView1->DataSource=bindingSource1;
Nach der Ausführung dieser Anweisungen erhält man mit einer wie im letzten Abschnitt erzeugten Tabelle ein DataGridView wie in dieser Abbildung:
10.16 Datenbanken
1275
Setzt man außerdem noch einen BindingNavigator (Toolbox Registerkarte „Daten“) auf das Formular und weist man dessen Eigenschaft BindingSource eine BindingSource (z.B. die von oben) zu bindingNavigator1->BindingSource=bindingSource1;
wird außerdem ein Navigator angezeigt, mit dem man durch den Datenbestand navigieren kann. Eine solche Verbindung von Steuerelementen und Daten wird auch als Datenbindung bezeichnet (siehe Abschnitt 10.17). Die einzelnen Zeilen einer DataTable haben den Typ DataRow und sind in der Eigenschaft Rows enthalten. Die Spalten einer Zeile haben den Typ DataColumn und sind in der Eigenschaft Columns enthalten:
property DataRowCollection^ Rows Beispiel: Die Funktion showTable gibt für alle Zeilen die Werte in den einzelnen Spalten in einer TextBox aus: void showTable(DataTable^ t, TextBox^ tb) { tb->AppendText(t->TableName+"\r\n"); for each(DataRow^ r in t->Rows) { for each(DataColumn^ c in t->Columns) { tb->AppendText(r[c]->ToString()+" "); } tb->AppendText("\r\n"); } }
Die sequenzielle Bearbeitung aller Datensätze einer Tabelle ist wie in der Funktion Sum möglich:
1276
10 Einige Elemente der .NET-Klassenbibliothek
Decimal Sum(DataTable^ t) { Decimal s=0; for each(DataRow^ r in t->Rows) s+=Convert::ToDecimal(r[t->Columns["Betrag"]]); return s; }
Die Zeilen einer DataTable können wie in fillTable gefüllt werden, indem man mit NewRow eine neue Zeile erzeugt und diese mit Add in die Tabelle (am Ende) eingefügt. Dabei können die Spalten über ihren Namen angesprochen werden: void fillTable(DataTable^ dt) { int n=20; for (int i=0; iNewRow(); row["KontoNr"]=1000+i%100; row["Datum"]=System::DateTime::Now.AddDays(i-n); String^ PlusMinus="+-"; row["Bewart"]=PlusMinus[i%2]; row["Betrag"]=1000-i; dt->Rows->Add(row); } }
Nach dem Aufruf dieser Funktion muss die Methode Update des DataAdapters aufgerufen werden, damit die Änderungen in die Datenbank geschrieben werden: SqlDataAdapter^ a=gcnew SqlDataAdapter( "select * from KontoBew",ConnectionString); DataTable^ dt=getSQLDataTable(a); fillTable(dt); a->Update(dt);
Die Funktion DataTable^ createTable(String^ name) { DataTable^ t=gcnew DataTable(name); t->Columns->Add("KontoNr",Type::GetType("System.Int32")); t->Columns->Add("Name",Type::GetType("System.String")); t->Columns->Add("Datum",Type::GetType("System.DateTime")); t->Columns->Add("Bewart",Type::GetType("System.Char")); t->Columns->Add("Betrag",Type::GetType("System.Decimal")); return t; }
zeigt, wie eine neue Tabelle mit dem als Parameter übergebenen Namen angelegt werden kann, wobei die Datentypen und die Namen der Spalten zur Laufzeit festgelegt werden. Auch diese Funktion liefert wie auch schon fillTable eine Ergebnis, das wie in Abschnitt 10.16.3 auch mit SQL-Anweisungen erreichbar wäre. Da eine DataTable aber unabhängig von einer Datenbank erzeugt werden kann, stehen die
10.16 Datenbanken
1277
Funktionen und Klassen für eine DataTable nicht nur für Datenbanken, sondern auch für beliebige, zur Laufzeit erzeugte DataTables zur Verfügung. In Abschnitt 10.16.6 wird gezeigt, wie man eine DataTable und ein DataSet in eine XML-Datei schreiben kann, bzw. zur Laufzeit aus einer XML-Datei aufbauen kann.
10.16.5 Die Klasse DataSet Die Klasse DataSet enthält in der Eigenschaft
property DataTableCollection^ Tables eine Collection von Tabellen des Typs DataTable, die Tabellen einer Datenbank darstellen kann. Weitere Eigenschaften wie
property DataRelationCollection^ Relations // die Relationen einer Datenbank stellen weitere Elemente einer Datenbank dar. Die Verbindung zu einer Datenbank kann wie zu einer DataTable über ein DataAdapter (analog zu den Funktionen von Abschnitt 10.16.4) hergestellt werden: DataSet^ getDataSet(DbDataAdapter^ a) { DataSet^ ds=gcnew DataSet(); a->Fill(ds); return ds; } DataSet^ getSQLDataSet(SqlDataAdapter^ a) { // analog: getOleDbDataSet, getOdbcDataSet usw. SqlCommandBuilder^ cb = gcnew SqlCommandBuilder(a); return getDataSet(a); }
Nach dem Aufruf dieser Funktionen stellt ein DataSet eine Kopie der Datenbank dar. Damit Änderungen im DataSet in die Datenbank geschrieben werden, ist ein Aufruf der Methode Update mit dem zugehörigen DataAdapter notwendig. Die einzelnen Tabellen eines DataSet kann man über Tables[i] bzw. in einer for each Schleife ansprechen. Beispiel: Die Funktion showTables gibt alle Tabellen eines DataSet mit der in Abschnitt 10.16.4 vorgestellten Funktion showTable aus: void showTables(DataSet^ ds, TextBox^ tb) { for each(DataTable^ t in ds->Tables) showTable(t,tb); }
Diese Funktion kann mit einem DataSet aufgerufen werden, der mit einer Datenbank verbunden ist:
1278
10 Einige Elemente der .NET-Klassenbibliothek
SqlDataAdapter^ a=gcnew SqlDataAdapter( "select * from KontoBew", ConnectionString); DataSet^ ds=getSQLDataSet(a); showTables(ds,textBox1);
Mit der Methode Add von DataTableCollection kann man einem DataSet Tabellen hinzufügen. Die Spalten der Tabelle kann man mit der Methode Add der Eigenschaft Columns erzeugen. Beispiel: Eine mit der Funktion createTable von Abschnitt 10.16.4 erzeugte Tabelle kann einem DataSet mit den folgenden Anweisungen hinzugefügt werden: DataTable^ t=createTable("newTable"); DataSet^ ds=gcnew DataSet; ds->Tables->Add(t);
10.16.6 Datenbanken als XML-Dateien lesen und schreiben Die Klassen DataSet und DataTable enthalten Methoden wie
void WriteXml(String^ fileName) // schreibe nur die Daten void WriteXmlSchema(String^ fileName) // schreibe nur das Schema void WriteXml(String^ fileName, XmlWriteMode mode) mit denen man die Daten und das XML-Schema im XML-Format schreiben kann. Gibt man für XmlWriteMode das Argument
XmlWriteMode::WriteSchema an, wird das XML-Schema in dieselbe Datei geschrieben. Mit Methoden wie
XmlReadMode ReadXml(String^ fileName) werden das Schema und die Daten gelesen und eine DataTable bzw. ein DataSet aufgebaut. Beispiel: Mit dem wie im letzen Abschnitt definierten DataSet ds erhält man durch die nächste Anweisung die XML-Datei Kb.xml: ds->WriteXml("c:\\Kb.xml",XmlWriteMode::WriteSchema);
10.16.7 Gekoppelte Datenquellen: Master/Detail DataGridViews Eine Datenbank besteht in vielen Anwendungen aus mehreren Tabellen, die inhaltlich miteinander verknüpft sind. Im Folgenden soll die Verknüpfung der Anzeige von Tabellen am Beispiel einer Tabelle von Kontobewegungen und einer
10.16 Datenbanken
1279
Tabelle von Konten illustriert werden. Dabei wurde die Tabelle mit den Konten mit den nächsten beiden Funktionen angelegt: void createKontenTable(DbCommand^ cmd) { // using namespace System::Data::Common; cmd->CommandText="Create Table Konten " "(KontoNr int, Kontostand money)"; cmd->Connection->Open(); cmd->ExecuteNonQuery(); cmd->Connection->Close(); } void fillKontenTable(DataTable^ dt) { int n=20; for (int i=0; iNewRow(); row["KontoNr"]=1000+i%100; row["Kontostand"]=1000-i; dt->Rows->Add(row); } }
Durch beide Tabellen kann man unabhängig voneinander navigieren, wenn man für jede ein DataGridView, eine BindingSource und einen BindingNavigator auf das Formular setzt und diese wie in Abschnitt 10.16.4 initialisiert: DataSet^ ds = gcnew DataSet(); SqlDataAdapter^ ma=gcnew SqlDataAdapter( "Select * from Konten", ConnString); SqlCommandBuilder^ mcb = gcnew SqlCommandBuilder(ma); masterBindingSource->DataSource = ds; masterGridView->DataSource=masterBindingSource; SqlDataAdapter^ da=gcnew SqlDataAdapter( "Select * from Kontobew", ConnString); SqlCommandBuilder^ dcb = gcnew SqlCommandBuilder(da); detailsBindingSource->DataSource = masterBindingSource; detailsGridView->DataSource=detailsBindingSource;
Der DataSet ds wird nach diesen Initialisierungen mit Daten gefüllt: ma->Fill(ds,"Konten"); da->Fill(ds,"Kontobew");
Die Kopplung zwischen den beiden Tabellen kann man über eine Relation definieren, die dann dem DataSet ds hinzugefügt wird: DataRelation^ relation = gcnew DataRelation("KontoNrRel", ds->Tables["Konten"]->Columns["KontoNr"], ds->Tables["Kontobew"]->Columns["KontoNr"]); ds->Relations->Add(relation);
1280
10 Einige Elemente der .NET-Klassenbibliothek
In dieser Relation werden die beiden Tabellen „Konten“ und „Kontobew“ über die Spalte „KontoNr“ gekoppelt. Weist man den Namen der so definierten Relation der Eigenschaft DataMember der detailsBindingSource zu, und den der Tabelle der masterBindingSource masterBindingSource->DataMember = "Konten"; detailsBindingSource->DataMember = "KontoNrRel";
werden im masterGridView die Daten der Tabelle Konten angezeigt. Blättert man durch die Zeilen dieses DataGridView, werden im detailsGridView die Daten der Tabelle Kontobew mit der entsprechenden Kontonummer angezeigt:
10.16.8 Datenquellen in Visual C++ 2005 und in Visual C# 2008 Ԧ In Visual C++ 2005 und in Visual C# 2008 kann man ein DataSet zu einer Datenbank auch mit Visual Studio erzeugen, indem man nach Daten|Datenquellen hinzufügen im Assistenten zum Konfigurieren von Datenquellen
„Datenbank“ auswählt. Dieses Symbol fehlt in Visual C++ 2008, und das liegt nicht an einer fehlerhaften Installation oder Konfiguration von Visual Studio, sondern an einer strategischen Entscheidung von Microsoft:
10.17 Datenbindung
1281
„We made a strategic decision after shipping VS2005 that we needed to focus our resources on improving the experience for native C++ development. This decision resulted in renewed investment in native libraries (STL, MFC) as well as an upcoming overhaul to our IDE infrastructure.” Dieser Hinweis soll Sie davor bewahren, unnötig Zeit mit der Suche nach diesen Möglichkeiten zu vertun. Falls Sie diese nutzen wollen, empfiehlt Microsoft, C#Projekte zu verwenden.
10.17 Datenbindung In Abschnitt 10.12.4 wurde gezeigt, wie man durch eine einfache Zuweisung einer Collection an die Eigenschaft
property Object^ DataSource eines DataGridView erreichen kann, dass die Daten im DataGridView angezeigt werden. Abschnitt 10.16.4 zeigt entsprechende Anweisungen für eine DataTable. Eine solche Verbindung von Daten mit Steuerelementen wird als Datenbindung bezeichnet und steht nicht nur für ein DataGridView zur Verfügung. Die folgenden Ausführungen sollen einen kurzen Überblick über Datenbindung unter .NET geben. Datenbindung wird üblicherweise in zwei Kategorien gegliedert: Komplexe Datenbindung, bei der wie in den bisherigen Beispielen Klassen mit mehreren Elementen („komplexe“ Datenstrukturen) angezeigt werden. Diese Beispiele haben schon gezeigt, dass sich „komplex“ hier nicht auf den Programmieraufwand für den Anwender bezieht. Wenn dagegen ein Steuerelement (z.B. eine TextBox) mit einer bestimmten Eigenschaft der Daten in einer Liste verbunden wird, spricht man von einfacher Datenbindung (siehe Abschnitt 10.17.3).
10.17.1 Komplexe Datenbindung Die Eigenschaft
property Object^ DataSource steht nicht nur in einem DataGridView zur Verfügung, sondern auch in den Klassen ListControl (und damit in den abgeleiteten Klassen ListBox, ComboBox und CheckedListBox) sowie einigen weiteren. Die folgenden Ausführungen gelten deshalb für alle diese Klassen. Allerdings stellen ein ListControl und ein DataGridView die Daten unterschiedlich dar: – Bei einem ListControl wird in der Voreinstellung der Rückgabewert der Methode ToString() der Collection-Elemente angezeigt. Falls die Collection-
1282
10 Einige Elemente der .NET-Klassenbibliothek
Elemente Eigenschaften haben, kann man den Namen der Eigenschaft, die angezeigt werden soll, mit der Eigenschaft
property String^ DisplayMember festlegen. – Bei einem DataGridView wird jede Eigenschaft in einer Spalte angezeigt. Beispiel: Damit von einer Klasse Person mit den Elementen Name und Address diese Elemente in einer ListBox angezeigt werden, kann man in dieser Klasse eine Methode ToString() definieren: ref class Person { String^ Name; String^ Address; public: Person(String^ name, String^ address):Address(address),Name(name){}; virtual String^ ToString() override { return Name+", "+Address; } };
Damit diese Werte in einem DataGridView angezeigt werden, müssen die Elemente Eigenschaften sein. Dabei sind triviale Eigenschaften ausreichend. Datenelemente, die keine Eigenschaften sind, werden nicht angezeigt: ref class Person { public: Person(String^ name, String^ address) { Address=address; Name=name; } property String^ Name; property String^ Address; };
Auch die Daten, die so angezeigt werden können, sind nicht auf Collection-Klassen und DataTables beschränkt. Für die Anzeige einer Datenquelle ist lediglich erforderlich, dass das zugewiesene Objekt eine der folgenden Schnittstellen implementiert:
IList (z.B. eindimensionale Arrays und Collection-Klassen) IBindingList (z.B. BindingList, IBindingListView oder BindingSource) IListSource (z.B. DataTable oder DataSet) Beispiel: Der Funktion
10.17 Datenbindung
1283
void showList(ListControl^ c, System::Collections::IList^ lst) { c->DataSource=lst; }
kann man als Argument für lst ein CLI-Array oder eine Collection-Klasse übergeben. Sie zeigt dann im Argument für c (z.B. einer ListBox) die ToString()-Werte der Collection-Elemente an. Die Funktion void showList(DataGridView^ c, System::Collections::IList^ lst) { c->DataSource=lst; }
zeigt im Argument für c die Eigenschaften der Collection-Elemente an. Ersetzt man in diesen Funktionen den zweiten Parameter durch IListSource void showList(ListControl^ c, System::ComponentModel::IListSource^ lst) { c->DataSource=lst; }
kann man als Argument ein DataSet oder eine DataTable angeben. Bei einem DataSet kann man mit der Eigenschaft
property String^ DataMember den Namen der Tabelle festlegen, die angezeigt werden soll.
10.17.2 BindingSource Wenn man eine DataSource wie in den letzten Beispielen direkt mit einer Datenquelle verbindet, c->DataSource=lst;
werden nur die Daten von lst zum Zeitpunkt der Zuweisung angezeigt, aber keine später hinzugefügten. Will man erreichen, dass immer die aktuellen Daten angezeigt werden, verwendet man besten eine BindingSource, die man wie in void bind(BindingSource^ bs, ListControl^ c, System::Collections::IList^ lst) { bs->DataSource=lst; c->DataSource=bs; }
1284
10 Einige Elemente der .NET-Klassenbibliothek
mit der Datenquelle und dem Steuerelement verbindet. Nach einer solchen Verbindung kann man die Daten sowohl über die Datenquelle als auch über die BindingSource ansprechen. Beispiel: In void test_bind(ListControl^ c) { using namespace System::Collections::Generic; List^ ds=gcnew List; BindingSource^ bs=gcnew BindingSource; bind(bs, c, ds); bs->Add(gcnew Person("ich","hier")); // a) int i2d=ds->Count; // 1 int i2b=bs->Count; // 1 ds->Add(gcnew Person("du","dort")); // b) int i3d=ds->Count; // 2 int i3b=bs->Count; // 2 }
werden Elemente in a) über die BindingSource hinzugefügt und in b) über die Datenquelle. In beiden Fällen haben bs und ds jeweils dieselben Elemente. Diese Elemente können auch über die BindingSource angezeigt werden, indem man void showList(ListControl^ c, System::ComponentModel::IBindingList^ lst) { c->DataSource=lst; }
mit der BindingSource als Argument für lst aufruft. Eine BindingSource hat zahlreiche weitere Vorteile und sollte deshalb immer zur Verbindung einer Datenquelle mit Steuerelementen verwendet werden. Sie ermöglicht insbesondere auch die Verbindung mit einem BindingNavigator (Toolbox Registerkarte „Daten“), der eine einfache Navigation durch die Daten einer Datenquelle ermöglicht. Dazu muss die BindingSource der Datenquelle nur der Eigenschaft
property BindingSource^ BindingSource des BindingNavigator zugewiesen werden: void bind(BindingSource^ bs, ListControl^ c, System::Collections::IList^ lst, BindingNavigator^ nav) { bs->DataSource=lst; c->DataSource=bs; nav->BindingSource=bs; }
10.17 Datenbindung
1285
Hier wurde die Funktion bind von oben nur durch den Parameter nav und die letzte Anweisung ergänzt. Beispiel: Ergänzt man die Funktion aus dem letzten Beispiel um einen BindingNavigator, dem die BindingSoure einer Datenquelle zugewiesen wird void test_showList(ListControl^ c, BindingNavigator^ nav) { using namespace System::Collections::Generic; List^ ds=gcnew List; ds->Add(gcnew Person("ich","hier")); ds->Add(gcnew Person("du","dort")); ds->Add(gcnew Person("er","woanders")); BindingSource^ bs=gcnew BindingSource; bind(bs,c,ds,nav); bs->Add(gcnew Person("sie","im Schuhgeschäft")); showList(c, bs); }
kann man mit dem Navigator durch den Datenbestand navigieren:
Zur Navigation durch einen Datenbestand ist man aber nicht auf einen BindingNavigator beschränkt. Eine BindingSource hat Methoden wie
void MoveFirst() void MoveLast()
void MoveNext() void MovePrevious()
durch die ein interner Positionszeiger (Eigenschaft Position) auf die erste, nächste usw. Position des Datenbestandes gesetzt wird. Diese Funktionen kann man in beliebigen Steuerelementen aufrufen und so durch die Daten navigieren. Beispiel: Zur Navigation mit einem gewöhnlichen Button: private: System::Void button_next_Click( System::Object^ sender, System::EventArgs^ { bindingSource1->MoveNext(); }
Das aktuelle Element einer BindingSource erhält man mit ihrer Eigenschaft
property Object^ Current
e)
1286
10 Einige Elemente der .NET-Klassenbibliothek
Da jede Move-Funktion (MoveFirst usw.) das Ereignis
event EventHandler^ PositionChanged auslöst, kann man das aktuelle Element im EventHandler für dieses Ereignis beliebig darstellen. Diesen EventHandler erhält man am einfachsten, indem man eine BindingSource aus der Toolbox (Registerkarte „Daten“) auf das Formular setzt und diesen dann durch einen Doppelklick in die rechte Spalte der „Ereignisse“ des Eigenschaftenfensters von Visual Studio erzeugen lässt. Da Current den Datentyp Object^ hat, muss diese Eigenschaft in den Datentyp der Elemente der Datenquelle konvertiert werden. Beispiel: Mit dem folgenden EventHandler werden die Elemente einer Datenquelle in zwei TextBoxen angezeigt: private: System::Void bindingSource1_PositionChanged( System::Object^ sender, System::EventArgs^ e) { Person^ p=safe_cast(bindingSource1-> Current); textBox1->Text=p->Name; textBox2->Text=p->Address; }
10.17.3 Einfache Datenbindung Von einfacher Datenbindung spricht man, wenn ein einzelnes Steuerelement (z.B. eine TextBox) mit einer bestimmten Eigenschaft der Daten in einer Liste verbunden wird. Für diese Form der Datenbindung steht die Eigenschaft
virtual property ControlBindingsCollection^ DataBindings bereits in der Klasse Control und damit in jedem Steuerelement zur Verfügung. Diese Collection stellt Datenbindungen (Datentyp Binding) zwischen einer Eigenschaft des Steuerelements (propertyName), einer Datenquelle (dataSource) und der Eigenschaft eines Elements der Datenquelle dar, die der Collection mit der Control-Methode
Binding^ Add(String^ propertyName, Object^ dataSource, String^ dataMember) hinzugefügt werden können. Hier kann man als Argument für dataSource zwar eine Datenquelle (z.B. eine Collection angeben). Eine BindingSource ist aber normalerweise besser und einfacher. Eine BindingSource kann man mit einem BindingNavigator (Toolbox Registerkarte „Daten“) verbinden und damit durch die Daten navigieren. Über den im letzten Abschnitt vorgestellten EventHandler zum Ereignis PositionChanged kann man die Darstellung der Elemente frei gestalten.
10.18 Reguläre Ausdrücke
1287
Beispiel: Für eine Liste mit Elementen des Typs Person ref class Person { public: Person(String^ name, String^ address) { Address=address; Name=name; } property String^ Name; // triviale Eigenschaft property String^ Address; };
wird durch void simpleBinding(List^ ds, BindingNavigator^ nav, TextBox^ textBox1, TextBox^ textBox2) { BindingSource^ bs=gcnew BindingSource; bs->DataSource=ds; nav->BindingSource=bs; textBox1->DataBindings->Add("Text",bs,"Name"); textBox2->DataBindings->Add("Text",bs,"Address"); } // in Add Argument bs, nicht ds
eine Datenbindung zwischen einer BindingSource und zwei TextBoxen definiert: Die Eigenschaft Text der textBox1 wird an die Eigenschaft Name eines Elements der BindingSource bs gebunden. Entsprechend textBox2 an die Eigenschaft Address. Dann kann man mit dem BindingNavigator durch die Daten navigieren. Dabei werden diese in den TextBoxen angezeigt.
10.18 Reguläre Ausdrücke Mit regulären Ausdrücken kann man Ausdrücke für komplexe Muster von Zeichenketten bilden. Reguläre Ausdrücke stehen in Visual Studio unter der Menüoption Bearbeiten|Suchen (nachdem man die Checkbox Mit markiert hat) sowie in der .NET Klassenbibliothek über die Klasse Regex aus dem Namensbereich System::Text::RegularExpressions zur Verfügung. Die Klasse regex der TR1-Erweiterungen (siehe Abschnitt 4.6) bietet im Wesentlichen dieselben Möglichkeiten.
1288
10 Einige Elemente der .NET-Klassenbibliothek
Im Folgenden werden reguläre Ausdrücke nur für die Suche nach Textmustern mit der statischen Regex-Methode
static bool IsMatch(String^ input, String^ pattern) verwendet. Auf die weiteren Möglichkeiten der Klasse Regex und die weiteren Klassen aus diesem Namensbereich, die verschiedenen Dialekte (ECMA und perl Scripts) und die Möglichkeit, Textmuster zu ersetzen, wird nicht eingegangen. Der Funktion IsMatch übergibt man als zweites Argument einen regulären Ausdruck. Ein regulärer Ausdruck ist ein String, der aus
gewöhnlichen Zeichen (alle Zeichen außer $ ^ { [ ( | ) * + ? \), und sogenannten Metazeichen (Zeichenfolgen, die mit $ ^ { [ ( | ) * + ? \ beginnen) besteht. Der Funktionswert gibt dann an, ob das erste Argument zu dem regulären Ausdruck passt (Rückgabewert true). Wenn man ein Zeichen verwenden will, das auch einem Metazeichen entspricht, muss man es mit einem \ kombinieren. Beispiel: Die Funktion void test_Regex(TextBox^ tb, String^ p, array^ a) { for each (String^ s in a) { if (Regex::IsMatch(s,p)) tb->AppendText(s+" passt zu "+p+" \r\n"); else tb->AppendText(s+" passt nicht zu "+p+"\r\n"); } }
zeigt für jeden String aus dem Array a an, ob er zu dem für p übergebenen regulären Ausdruck passt. Mit einem regulären Ausdruck ohne Metazeichen prüft isMatch, ob die Zeichenfolge des regulären Ausdrucks im Argument für input enthalten ist. Beispiel: Mit den Strings array^ t1={"124","-1234"," 123%","abcd", "321"};
ergibt der Aufruf test_Regex(textBox1, "123", t1);
die folgende Ausgabe: 124 passt nicht zu 123 123% passt zu 123 321 passt nicht zu '123'
-1234 passt zu 123 abcd passt nicht zu 123
10.18 Reguläre Ausdrücke
1289
Die nächsten Tabellen enthalten einige Metazeichen. Das Zeichen „\“ muss in einem C++-String doppelt angegeben werden. Diese Tabellen sind aber nicht vollständig und sollen lediglich einen ersten Eindruck geben:
Bedeutung Entspricht einem beliebigen Zeichen der angegebenen Zeichengruppe. [^ character_group ] Entspricht einem beliebigen Zeichen, das nicht in der angegebenen Zeichengruppe enthalten ist. [firstCharacter – Entspricht einem beliebigen Zeichen im Bereich. Falls lastCharacter ] eine Zeichengruppe ein Minuszeichen enthalten soll, muss es als Escapesequenz „\\-“angegeben werden. \w beliebiges Wortzeichen . (Punkt) entspricht einem beliebigen Zeichen. \s entspricht einem beliebigen Leerzeichen \S entspricht einem beliebigen Nicht-Leerzeichen \d entspricht einer beliebigen Dezimalziffer \D entspricht einer beliebigen Nicht-Dezimalziffer
Zeichenklassen [character_group]
Zeichenklassen und andere Sprachelemente von regulären Ausdrücken können beliebig kombiniert werden. Beispiel: Eine Zeichengruppe mit einem Zeichen entspricht dem Zeichen. Die beiden regulären Ausdrücke "x" und "[x]" sind gleichwertig (der Buchstabe 'x'), ebenso "\*" und "[\*]" (das Zeichen '*' und nicht der später vorgestellte Operator *). Zu dem regulären Ausdruck String^ containsInt_100_999_rx="[1-9][0-9][0-9]";
passt eine Zeichenfolge, die mit einer Ziffer im Bereich 1..9 beginnt und von zwei Ziffern im Bereich 0..9 gefolgt wird (Zahlen im Bereich von 100 bis 999). Zu dem regulären Ausdruck String^ containsDate_rx="\\d\\d/\\d\\d/\\d\\d";
passt eine Datumsangabe im amerikanischen Format MM/DD/YY, bei dem drei Paare von Ziffern durch ein ‚/'-Zeichen getrennt werden. Nach einem Ausdruck kann ein Quantifizierer angegeben werden, der dann für den unmittelbar davor stehenden Ausdruck gilt.
1290
10 Einige Elemente der .NET-Klassenbibliothek
Quantifizierer * + ? {n} {n,} {n,m}
Bedeutung Null oder mehr Übereinstimmungen Eine oder mehr Übereinstimmungen Null oder eine Übereinstimmung genau n Übereinstimmungen mindestens n Übereinstimmungen mindestens n, aber höchstens m Übereinstimmungen
Beispiel: Zu dem regulären Ausdruck String^ AssignToX_rx="x\\s*={1}";
passt eine Zeichenfolge, in der auf das Zeichen 'x' beliebig viele Leerzeichen und genau ein Gleichheitszeichen folgen. Damit kann man alle Zuweisungen an eine Variable x finden. Allerdings werden auch Zuweisungen an Variable gefunden, deren Name mit 'x' endet. Zu dem regulären Ausdruck String^ containsSimpleInt_rx="0*[1-9][0-9]*";
passt eine Zeichenfolge mit oder ohne führende Nullen, in der auf eine Ziffer im Bereich 1..9 beliebig viele Ziffern im Bereich 0..9 folgen. Mit einem Klammerpaar () kann man Teilausdrücke zusammenfassen. Teilausdrücke können zu einer leichteren Verständlichkeit beitragen und ermöglichen die Quantifizierung von Ausdrücken.
Gruppenkonstrukte ( subexpression )
Bedeutung Teilausdruck
Beispiele: Die Funktion String^ AssignTo_rx(String^ VarName) { return "("+VarName+")\\s*={1}"; }
gibt einen regulären Ausdruck zurück, der zu einer Zuweisung an eine Variable mit dem als Parameter übergebenen Namen passt. Mit array^ ta={"x1=17;"," x1 =y ", "y=x1", "x1+y=17","Max1="}; test_Regex(tb, AssignTo_rx("x1"), ta);
erhält man die Ausgabe x1=17; passt zu (x1){1}\s*={1} x1 =y passt zu (x1){1}\s*={1}
10.18 Reguläre Ausdrücke
1291
y=x1 passt nicht zu (x1){1}\s*={1} x1+y=17 passt nicht zu (x1){1}\s*={1} Max1= passt zu (x1){1}\s*={1}
Zu dem regulären Ausdruck String^ NrWith2FractDigits="-?\\d+(\\.\\d{2})?)";
passt eine Zeichenfolge, die – mit einem optionalen Minuszeichen beginnt – von einer Dezimalzahl gefolgt wird und – einem optionalen Teilausdruck, der aus einem Punkt und einer zweistelligen Dezimalzahl besteht. Positionsbezogene Angaben legen eine Position fest, an der ein Muster auftreten muss, damit der Vergleich erfolgreich ist. Damit der gesamte Vergleichsstring einem Muster entspricht (und nicht nur in ihm enthalten ist), verwendet man ^ am Anfang und $ am Ende. So kann man auch prüfen, ob ein von einem Benutzer eingegebener String zu einem bestimmten Muster passt.
positionsbezogen ^ $ \b
Bedeutung Der Vergleich muss am Anfang der Zeichenfolge erfolgen Der Vergleich muss am Ende erfolgen Der Vergleich muss an einer Wortbegrenzung (Übergang zwischen alphanumerischen und nicht alphanumerischen Zeichen) erfolgen
Beispiel: Mit dem regulären Ausdruck (siehe das Beispiel von oben) String^ containsInt_100_999_rx="[1-9][0-9][0-9]";
passt zu den folgenden regulären Ausdrücken String^ s_beg="^"+containsInt_100_999_rx; String^ s_end=containsInt_100_999_rx+"$"; String^ s_tot="^"+containsInt_100_999_rx+"$";
nur ein String, der mit dem Muster beginnt (s_beg), mit dem Muster endet (s_end) oder als Ganzes das Muster darstellt. Mit dem Operator | kann man Ausdrücke kombinieren. Der so entstandene Gesamtausdruck passt dann, wenn einer der Teilausdrücke passt.
Alternierungskonstrukte |
Bedeutung Einer der Operanden muss passen
Beispiel: Zu dem regulären Ausdruck
1292
10 Einige Elemente der .NET-Klassenbibliothek
String^ Class_rx="class|struct";
passt eine Zeichenfolge, die "class" oder "struct" enthält. Mit regulären Ausdrücken wird oft auch die Syntax von Internet- oder EMailadressen und Dateinamen geprüft. Beispiel: Eine EMail-Adresse besteht (etwas vereinfacht) aus den folgenden Bestandteilen: 1. Am Anfang eine Folge aus Buchstaben, Ziffern und den Zeichen '.', '%', '_', '+' oder '-'. 2. Dem Zeichen '@' 3. Eine Folge aus Buchstaben, Ziffern und den Zeichen '.' oder '-'. 4. Ein Punkt 5. Am Schluss zwei bis vier Buchstaben Mit dem regulären Ausdruck String^ EMail_rx="^[A-Za-z0-9%+._\\-]+" "@" "[A-Za-z0-9.\\-]+" "\\." "[A-Za-z]{2,4}$"
// // // // //
1. 2. 3. 4. 5.
erhält man die folgenden Ergebnisse:
zulässig
[email protected] [email protected] [email protected] [email protected] nicht zulässig
[email protected] "sweetie"
[email protected] jane@@abc.def
[email protected]?subject=Hi"
Um alle nach dem offiziellen Standard für EMail-Adressen (RFC 2822) zulässigen Adressen zu prüfen, sind kompliziertere reguläre Ausdrücke notwendig. Auf http://www.regular-expressions.info/email.html wird behauptet, dass (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` {|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x 5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?: [a-z09](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z09])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z09]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\ [\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
den offiziellen Standard für EMail-Adressen vollständig umsetzt (ich habe das nicht nachgeprüft). Die Regex-Funktion
10.18 Reguläre Ausdrücke
1293
static MatchCollection^ Matches(String^ input, String^ pattern) gibt alle Treffer, die im input-String zu dem als Argument für pattern übergebenen regulären Ausdruck passen, in einer MatchCollection zurück. Die Elemente der MatchCollection haben den Datentyp Match, deren Eigenschaft
property String^ Value die gefunden Strings darstellen. Beispiel: Mit String^ containsSimpleInt_rx="0*[1-9][0-9]*"; String^ s=" ab 123 .. x45 6789 ab"; tb->AppendText("\r\n"); for each (Match^ m in Regex::Matches(s, containsSimpleInt_rx)) tb->AppendText("'"+m->Value+"' \r\n");
erhält man die Ausgabe '123' '45' '6789'
Aufgaben 10.18 Schreiben Sie Funktionen, die für einen als Parameter Line übergebenen String prüfen, ob er die Anforderungen erfüllt. Übergeben Sie die variablen Bestandteile der Prüfung als Parameter und bauen Sie mit diesen in den Funktionen reguläre Ausdrücke auf. Bei den Aufgaben a) bis d) sollen zwischen den Sprachelementen wie in C++ beliebig viele Leerzeichen stehen können. Solche Funktionen können dazu verwendet werden, einen Quelltext bzw. eine Textdatei zeilenweise zu lesen und jede Zeile auszugeben, die das entsprechende Kriterium erfüllt. a) In Line folgt auf einen als Parameter übergebenen Namen nach eventuellen Leerzeichen eine Klammer „(“. Damit kann man prüfen, ob Line eine Definition oder einen Aufruf der Funktion mit dem als Parameter übergebenen Namen enthält. b) In Line folgt auf ein Gleichheitszeichen „=“ und eventuellen Leerzeichen ein als Parameter übergebener Name. Damit kann man prüfen, ob Line eine Zuweisung einer Variablen mit dem als Parameter übergebenen Namen enthält. c) In Line folgt auf „class“ oder „struct“ ein Name und dann ein „{„ oder ein „:“). Damit kann man prüfen, ob Line eine Klassendefinition enthält. Klassen-Templates und Template-Spezialisierungen brauchen nicht berücksichtigt werden. d) Line enthält ein Wort, das mit einer als Parameter übergebenen Zeichenfolge beginnt und mit einer weiteren als Parameter übergebenen Zeichenfolge endet.
1294
10 Einige Elemente der .NET-Klassenbibliothek
e) Line ist eine Folge von 4 Zahlen aus einer bis drei Ziffern, wobei die Zahlen wie in „123.4.56.789“ durch einen Punkt getrennt sind. f) Line ist eine EMail-Adresse, die wie im Beispiel oben beginnt, aber mit „com“, „de“ oder „net“ endet. g) Rufen Sie die Funktionen aus a) bis b) in einer Funktion auf, die alle Zeilen aus einer als Parameter übergebenen Textdatei ausgibt, die die jeweiligen Kriterien mit einem als Parameter übergebenen Namen erfüllen.
10.19 Internet-Komponenten Der Namensbereich System::Net enthält zahlreiche Klassen, die eine einfache Lösung von vielen Aufgaben ermöglichen, die im Zusammenhang mit Netzwerken und dem Internet auftreten.
10.19.1 Die WebBrowser-Komponente der Toolbox Mit einem WebBrowser (Toolbox Registerkarte „Allgemeine Steuerelemente“) kann man HTML-Dokumente anzeigen. Das können Dokumente auf dem Rechner oder im Internet sein. Die Methode
void Navigate(String^ urlString) lädt ein HTML-Dokument, dessen Adresse eine sogenannte URL (z.B. „http://msdn.microsoft.com“) ist. Wenn ein solches Dokument einen Link auf ein anderes HTML-Dokument enthält, wird es nach dem Anklicken geladen und angezeigt. private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { webBrowser1->Navigate("http://msdn.microsoft.com"); }
Der Aufruf dieser Funktion zeigt die Startseite von http://msdn.microsoft.com an. Über die Links auf dieser Seite kann man sich weiteren Seiten durchklicken.
10.19 Internet-Komponenten
1295
HTML-Dokumente auf dem Dateisystem des Rechners adressiert man wie in: webBrowser1->Navigate( "file:///C|\\WINDOWS\\Help\\migwiz.htm"); // C:\WINDOWS\Help\migwiz.htm
Zahlreiche weitere Methoden und Eigenschaften ermöglichen alle Operationen, die man auch in einem Internet-Browser findet. Ein Teil dieser Elemente ist in der Tabelle aufgeführt:
Element void Print() bool GoBack() bool GoForward() property Uri^ Url
Druckt die aktuelle Seite Zeigt die vorherige Seite an, falls eine existiert Zeigt die nächste Seite an, falls eine existiert Die aktuelle URL. Eine Zuweisung an diese Eigenschaft ist gleichwertig zum Aufruf der Funktion Navigate.
10.19.2 Up- und Downloads mit der Klasse WebClient Die Klasse WebClient aus dem Namensbereich System::Net enthält zahlreiche Methoden, mit denen man in einem Netzwerk Daten versenden und empfangen kann. Die folgenden Beispiele stellen einige dieser Methoden vor und sollen vor allem zeigen, wie einfach diese Operationen sind. Mit
void DownloadFile (String^ address, String^ fileName) wird die Datei mit dem als Argument für address übergebenen Namen unter dem Namen des Arguments für fileName als lokale Datei gespeichert. Beispiel: Die Funktion void myDownload(String^ srcPath, String^ dstPath, String^ fn) { using namespace System::Net;
1296
10 Einige Elemente der .NET-Klassenbibliothek
WebClient^ myWebClient = gcnew WebClient; myWebClient->DownloadFile(srcPath+fn,dstPath+fn); delete myWebClient; }
lädt eine Datei aus einem Netzverzeichnis auf ein lokales Laufwerk. Sie kann z.B. folgendermaßen aufgerufen werden: String^ s= "http://www.rkaiser.de/rkaiser/VC2008_Buch/"; myDownload(s, "C:\\", "VC2008_AufgLsg.pdf");
Mit
String^ DownloadString(String^ address) wird der Inhalt der als Argument übergebenen Adresse als String zurückgegeben. Beispiel: Die Funktion showLinks weist den Text der Internetseite mit der als Argument für url übergebenen Adresse dem String s zu. Von diesem String werden dann alle Zeichenfolgen, die mit „http://“ oder „https://“ beginnen und durch eine Folge von 2 oder 3 Buchstaben begrenzt werden, in der MatchCollection Regex::Matches (siehe Abschnitt 10.18) zurückgegeben und in einer TextBox angezeigt: void showLinks(TextBox^ tb, String^ url) { using namespace System::Text::RegularExpressions; using namespace System::Net; String^ URL_rx="(http|https)\\://" // http:// "[a-zA-Z0-9\\-\\.]+" // www.rkaiser "\\." // . "[a-zA-Z]{2,3}"; // de WebClient^ myWebClient = gcnew WebClient; String^ s=myWebClient->DownloadString(url); for each (Match^ m in Regex::Matches(s, URL_rx)) tb->AppendText(m->Value+"\r\n"); }
Für das Hochladen stehen Funktionen wie
array^ UploadFile(String^ address, String^ fileName) String^ UploadString(String^ address, String^ data) zur Verfügung. Asynchrone Varianten dieser Funktionen (mit Async im Namen) warten nicht, bis der Up- oder Download abgeschlossen ist, sondern setzen das Programm sofort nach dem Aufruf fort. Der Up- oder Download findet dabei im Hintergrund statt. Die abstrakten Basisklassen WebRequest und WebResponse sowie die abgeleiteten Klassen FileWebRequest, FileWebResponse, FtpWebRequest, FtpWebResponse, HttpWebRequest und HttpWebResponse bieten ähnliche Möglich-
10.19 Internet-Komponenten
1297
keiten. Wie die Namensbestandteile Ftp und Http nahelegen, sind die entsprechenden Klassen auf das Ftp- und Http-Protokoll abgestimmt. Die Klassen mit File im Namen sind für Dateien mit dem Namensschema „file://“.
10.19.3 E-Mails versenden mit SmtpClient Mit der Methode
void Send(MailMessage^ message) der Klasse
public ref class SmtpClient aus dem Namensbereich System::Net::Mail kann man E-Mails versenden. Dabei besteht eine MailMessage im einfachsten Fall aus Strings mit der Absender- und Empfängeradresse, dem Betreff und der Nachricht, die im Konstruktor
MailMessage(String^ from, String^ to, String^ subject, String^ body) gesetzt werden können. Bei den meisten Servern muss man auch noch ein Passwort und einen Benutzernamen als Objekt des Typs NetworkCredential übergeben. Auch diese Eigenschaften können im Konstruktor gesetzt werden:
NetworkCredential(String^ userName, String^ password) Die MailMessage wird dann durch Anweisungen wie SmtpClient c(ServerName); c.Credentials=%cred; // die NetworkCredential c.Send(%msg); // die MailMessage
versendet. Alle notwendigen Operationen sind in der Funktion SendMail zusammengefasst: void SendMail() { using namespace System::Net::Mail; // Mailserver Identifikation: String^ ServerName="...";// z.B. mail.server.com String^ UserName=" "; // aus dem EMail-Konto String^ Password="..."; String^ SenderName="...";// z.B. "
[email protected]" String^ RecName="..."; // z.B. "
[email protected]" String^ Subject="I LOVE YOU"; String^ Body= "Lieber Supermarkt. \n" "Hier ist der Kühlschrank von C. Bukowski. Bier ist " "alle. \n Bringt mal 'ne Flasche vorbei. Aber dalli. \n"
1298
10 Einige Elemente der .NET-Klassenbibliothek
"Mit freundlichen Grüßen \n" "AEG Santo 1718 (Energiesparmodell) \n"; MailMessage msg(SenderName,RecName,Subject,Body); System::Net::NetworkCredential myCredentials(UserName,Password); SmtpClient c(ServerName); c.Credentials=%myCredentials; // required by some servers c.Send(%msg); }
Weitere Eigenschaften von MailMessage ermöglichen die Versendung von Anlagen usw. mit der E-Mail.
10.19.4 Netzwerkinformationenen und die Klasse Ping Der Namensbereich System::Net::NetworkInformation enthält zahlreiche Elemente mit den Netzwerkadressen und statistischen Informationen über die gesendeten und empfangenen Daten. Im Folgenden soll nur die Klasse Ping vorgestellt werden, mit der man feststellen kann, ob ein Rechner im Netzwerk erreichbar ist. Verschiedene überladene Versionen der Methode Send senden Daten mit dem Internet Control Message Protocol (ICMP) an einen Rechner und warten auf eine Antwort. In der einfachsten Version
PingReply^ Send(String^ hostNameOrAddress) gibt man nur den Namen oder die IP-Adresse des Rechners an. Weiteren SendVarianten kann man zusätzliche Optionen, die expliziten Daten und einen TimeOut übergeben. Der Rückgabewert hat insbesondere die Eigenschaft
property IPStatus Status eines C++/CLI-Aufzählungstyps. Wenn dieser Status den Wert IPStatus::Success hat, konnte der Rechner erreicht werden, und die weiteren Eigenschaften enthalten Informationen über
Element property IPAddress^ Address property array^ Buffer property PingOptions^ Options property long long RoundtripTime
Die IP-Adresse Die empfangenen Daten Die verwendeten Optionen Anzahl der Millisekunden bis zum Empfang der Daten
Beispiel: Die Funktion simplePing gibt im Wesentlichen dieselben Informationen wie der Ping-Befehl auf einer Kommandozeile aus:
10.19 Internet-Komponenten
1299
bool simplePing(TextBox^ tb, String^ dest) { using namespace System::Net::NetworkInformation; Ping pingSender; PingReply^ reply = pingSender.Send(dest); if (reply->Status == IPStatus::Success) { String^ s= "Address: "+r->Address->ToString ()+"\r\n"+ "RoundTrip time: "+r->RoundtripTime+"\r\n"+ "Time to live: "+r->Options->Ttl+"\r\n"+ "Buffer size: "+ r->Buffer->Length+"\r\n"; tb->AppendText(s); return true; } else return false; }
10.19.5 TCP-Clients und Server mit TcpClient und TcpListener Alle Internetdienste beruhen letztlich auf dem TCP/IP-Protokoll. Mit den Komponenten TcpClient und TcpListener aus dem Namenbereich System::Net::Sockets ist ein direkter Datenaustausch zwischen verschiedenen Anwendungen möglich, die über dieses Protokoll miteinander verbunden sind. Insbesondere können damit verschiedene Programme auf demselben Rechner oder in einem TCP/IP-Netzwerk Daten austauschen. Ein Programm, das einen TcpListener enthält, kann als Server agieren. Dazu muss lediglich die Eigenschaft Port im Konstruktor
TcpListener(IPAddress^ localaddr, int port) auf einen freien Wert gesetzt werden. Belegte Werte findet man unter „http://www.iana.org/assignments/port-numbers“. Mit einem Aufruf der Methode
void Start() beginnt der TcpListener dann mit der Überwachung der eingehenden Verbindungsanforderungen. Die Methode
TcpClient^ AcceptTcpClient() wartet auf den Verbindungsversuch eines TcpClient und gibt nach einer erfolgreichen Verbindung den Client zurück. Über dessen Methode
NetworkStream^ GetStream() wird ein NetworkStream zurückgegeben, über den mit
1300
10 Einige Elemente der .NET-Klassenbibliothek
virtual int Read(array^ buffer, int offset, int size) override virtual void Write(array^ buffer, int offset, int size) override ein Array von Bytes (unsigned char) an den Client gesendet und von ihm empfangen werden können. Die Funktion myTcpServer fasst diese Anweisungen zusammen. Dabei werden die vom Client empfangenen Byte-Daten mit den Text::Encoding-Funktionen in einen String umgewandelt und der in Großschreibung umgewandelte String wieder als Folge von Bytes zurückgeschickt. Dabei muss eine zulässige IP-Adresse verwendet werden. Für ein lokales Internet (Intranet) sind dafür z.B. die Adressen in den Bereichen 192.168.0.1 bis 192.168.255.255 reserviert. Die IP-Adressen eines Rechners erhält man auch mit dem Kommandozeilen-Programm IPConfig. const int TcpIpPort=5000; void myTcpServer(TextBox^ tb) { // sehr einfach using namespace System::Net; using namespace System::Net::Sockets; try { IPAddress^ adr = IPAddress::Parse("192.168.123.113"); TcpListener^ server = gcnew TcpListener(adr,TcpIpPort); server->Start(); array^ rb = gcnew array(256); // read buffer while (true) // sehr einfach: Endlosschleife { Application::DoEvents(); // Damit die Meldungen // angezeigt werden tb->AppendText("warte auf Verbindung... \r\n"); TcpClient^ client = server->AcceptTcpClient(); tb->AppendText("verbunden!\r\n"); NetworkStream^ stream = client->GetStream(); int i; while (i = stream->Read(rb, 0, rb->Length)) { // Schleife, falls der Client mehr Bytes sendet // als in den Puffer passen String^ s=Text::Encoding::ASCII->GetString(rb,0,i); tb->AppendText("empfangen: "+s+"\r\n" ); s=s->ToUpper();//verändere die empfangenen Daten array^ wb=Text::Encoding::ASCII->GetBytes(s); stream->Write(wb, 0, wb->Length);// senden tb->AppendText("Sent: "+s+"\r\n"); } client->Close(); } }
10.19 Internet-Komponenten
1301
catch (Exception^ e) { tb->AppendText("Exception: "+e->Message); } }
Ein Programm mit einem TcpClient kann als Client agieren und mit einem Server Daten austauschen, der dieselbe Port-Nummer hat. Ein solcher Client kann mit einem Konstruktor wie
TcpClient(String^ hostname, int port) aus einem String mit der IP-Adresse und der Port-Nummer des Servers erzeugt werden. Ein solcher Client kann dann mit dem von GetStream zurückgegebenen NetworkStream (siehe oben) und den Methoden Read und Write mit dem Server Daten austauschen. Die Funktion myTcpClient fasst diese Anweisungen zusammen: void myTcpClient(TextBox^ tb, String^ server, String^ msg) { // sehr einfach using namespace System::Net::Sockets; try { TcpClient^ client = gcnew TcpClient(server,TcpIpPort); NetworkStream^ stream = client->GetStream(); // Konvertiere den String in ein Byte-Array: array^ wb=Text::Encoding::ASCII->GetBytes(msg); // Sende das Array an den Server: stream->Write(wb,0,wb->Length); tb->AppendText("gesendet: "+msg+"\r\n"); // rb ist der Puffer für die empfangenen Daten array^ rb=gcnew array(256); // Lese die Daten vom TcpServer: int bytes = stream->Read(rb, 0, rb->Length); String^ s=Text::Encoding::ASCII->GetString(rb,0,bytes); tb->AppendText("empfangen: "+s+"\r\n"); client->Close(); } catch (Exception^ e ) { tb->AppendText("Exception: "+e->Message); } }
Ein Programm kann sowohl einen TcpClient als auch einen TcpListener enthalten. Durch die Aktivierung einer der beiden wird es dann entweder Client oder Server. Die Beispiele mit den beiden Funktionen myTcpClient und myTcpServer kann man dadurch ausprobieren, dass man dasselbe Programm zweimal startet und bei dem einen den Server- und beim anderen den Client-Button anklickt.
1302
10 Einige Elemente der .NET-Klassenbibliothek
Aufgaben 10.19 1. Beim Anklicken eines Buttons soll eine bestimmte HTML-Seite (z.B. „http://www.yahoo.com“) mit einem WebBrowser angezeigt werden. Mit weiteren Buttons sollen ihre Methoden GoBack und GoForward (siehe dazu die Online-Hilfe) aufgerufen werden, mit denen man zwischen den zuletzt geladenen Seiten navigieren kann. 2. Laden Sie eine Internetseite (z.B. http://www.spiegel.de/) als String und zeigen Sie alle URLs und EMail-Adressen, die in diesem String enthalten sind, in einer TextBox an. Sie können dazu einfache reguläre Ausdrücke wie EMail_rx in Abschnitt 10.18 verwenden. In einer zweiten Variante dieser Funktionen sollen mehrfach enthaltene Treffer nur einmal angezeigt werden. Verwenden Sie dazu eine geeignete CollectionKlasse.
Literaturverzeichnis
Alagic, Suad; Arbib, Michael: The Design of Well-Structured and Correct Programs Springer-Verlag, New York 1978 Austern, Matt: Generic Programming and the STL Addison Wesley, Reading, Mass. 1999 Bauer, Friedrich L.; Wössner, Hans: Algorithmic Language and Program Development Springer-Verlag, Berlin, Heidelberg 1982 Beck, Kent: Extreme Programming Explained: Embrace Change Addison Wesley, Reading, Mass. 1999 Beizer, Boris: Software Testing Techniques van Nostrand Reinhold, New York 1990 Böhm, C.; Jacopini, G.: Flow Diagrams, Turing Machines and Languages with Only Two Formation Rules, CACM, 9,5; May 1966, S. 366–371 Booch, Grady: Object Oriented Analysis and Design with Applications Benjamin Cummings Publishing Company, 2nd ed, Redwood City 1994 C-Standard (C89/C90): ANSI X3.159-1989 identisch mit ISO/IEC 9899:1990 (C90) Published by American National Standards Institute, 11 West 42nd Street, New York, New York 10003, www.ansi.org C-Standard (C99): ISO/IEC 9899:1999 Published by American National Standards Institute, 11 West 42nd Street, New York, New York 10003, www.ansi.org C99-Rationale: Rationale for International Standard Programming Languages C Revision 5.10, April-2003 http://www.open-std.org/jtc1/sc22/wg14/ C++ Standard (C++03): International Standard ISO/IEC 14882:2003 Second Edition, 2003-04-01 C++ Standard (C++98): International Standard ISO/IEC 14882:1998 First Edition, 1998-09-01
1304
Literaturverzeichnis
Published by American National Standards Institute, 11 West 42nd Street, New York, New York 10003, www.ansi.org C++ Draft 4/95: Working Paper for Draft Proposed International Standard for Information Systems Programming Language C++, Doc No: X3J16/96–0225, INFORMATION PROCESSING SYSTEMS WG21/N0687 Date: 2 December 1996, Project: Programming Language C++ http://www.open-std.org/jtc1/sc22/open/n2356/ C++/CLI Standard: C++/CLI Language Specification Standard ECMA 372, 1st edition, December 2005 http://www.ecma-international.org/publications/standards/Ecma-372.htm C++ Library Extensions (TR1): Draft Technical Report on C++ Library Extensions ISO/IEC DTR 19768 Date: 2005-06-24, Doc No: N1836=05-0096 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1836.pdf Coplien, James O..: Advanced C++ Programming Styles and Idioms Addison Wesley, Reading, Mass. 1991 Cormen, Thomas H., C. E. Leiserson, R. L. Rivest, C. Stein: Introduction to Algorithms MIT Press, 2001 Cowlishaw, Mike: General Decimal Arithmetic http://speleotrove.com/decimal/ Crochemore, M., T. Lecroq: Pattern Matching and Text Compression Algorithms in (The Computer Science and Engineering Handbook, A.B. Tucker, Jr, ed., CRC Press, Boca Raton, 1996 Dijkstra, Edsger W.: A Discipline of Programming Prentice Hall, Englewood Cliffs, N. J. 1976 Ghezzi, Carlo; Jazayeri, Mehdi; Mandrioli, Dino: Fundamentals of Software Engineering Prentice Hall, Englewood Cliffs, N. J. 1991 Goldberg, David: What every computer scientist should know about floationg-point arithmetic ACM Computing Surveys, March 1991 http://www.crml.uab.edu/workshop/common-tools/numerical_comp_guide/ goldberg1.ps oder http://docs.sun.com/source/806-3568/ncg_goldberg.html Grogono,Peter;Markku Sakkinen: Copying and Comparing: Problems and Solutions in Elisa Bertino (Ed.): ECOOP 2000, LNCS 1850, pp. 226-250, 2000 Springer-Verlag Berlin Heidelberg 2000 http://www.cs.concordia.ca/~comp746/grogono.pdf Gries, David: The Science of Programming Springer-Verlag, New York 1991
Literaturverzeichnis
1305
Jensen, Kathleen; Wirth, Niklaus: Pascal User Manual and Report 2nd ed., Springer-Verlag, Berlin, Heidelberg, New York 1974 4th ed., ISO Pascal Standard, Springer-Verlag, Berlin, Heidelberg, New York 1991 Josuttis, Nicolai: The C++ Standard Library Addison-Wesley, 1999 Kaner, Cem; Jack, Falk, Hung Quoc Nguyen: Testing Computer Software John Wiley, New York, 1999 Kernighan, Brian; Ritchie, Dennis: The C Programming Language 2nd ed., Prentice Hall, Englewood Cliffs, N. J. 1988 Knuth, Donald: The Art of Computer Programming Vol. 1, Fundamental Algorithms, Addison-Wesley, Reading, Mass. 1973 Vol. 2, Seminumerical Algorithms, 2nd Ed., Addison-Wesley, Reading, Mass. 1981 Vol. 3, Sorting and Searching, Addison-Wesley, Reading, Mass. 1973 Koenig, Andrew; Barbara Moo: Accelerated C++ Addison-Wesley, 2000 Lagarias, Jeff: The 3x+1 problem and its generalizations American Mathematical Monthly Volume 92, 1985, 3 - 23. http://www.cecm.sfu.ca/organics/papers/lagarias/index.html Liggesmeyer, Peter: Software-Qualität Spektrum, Akad.-Verlag, Heidelberg Berlin 2002 van der Linden, Peter: Expert-C-Programmierung Verlag Heinz Heise, 1995 Martin, Robert C.: The Liskov Substitution Principle C++ Report, March 1996 http://www.objectmentor.com/publications/lsp.pdf Martin, Robert C.: Design Principles and Design Patterns http://www.objectmentor.com/publications/Principles%20and%20Patterns.PDF, 2000 Meyer, Bertrand: Object-Oriented Software Construction Prentice Hall, Englewood Cliffs, N. J. 1997 Meyers, Scott: Effective C++ Addison Wesley, Reading, Mass. 1998 Meyers, Scott: Effective STL Addison Wesley, Reading, Mass. 2001 Meyers, Scott: More Effective C++ Addison Wesley, Reading, Mass. 1996 McConnell, Steve: Code Complete 2 Microsoft Press, 2004
1306
Literaturverzeichnis
de Millo, Richard A.; Richard J. Lipton, Alan J. Perlis: Social processes and proofs of theorems and programs Communications of the ACM, Vol. 22, Number 5, May 1979, pp. 271–280. Navarro, Gonzalo: A Guided Tour to Approximate String Matching ACM Computing Surveys, Vol. 33, No. 1, March 2001, pp. 31–88. Park, Stephen K.; K. W. Miller: Random Number Generators: Good ones are hard to find Communications of the ACM, Vol. 31, Number 10, May 1988, pp. 1192–1201. Peitgen, Heinz-Otto; Richter, Peter: The Beauty of Fractals Springer-Verlag, Berlin, Heidelberg, New York 1986 Petzold, Charles: Programmierung unter Windows Microsoft Press Deutschland, Unterschleißheim 1992 Hendrik Post, Carsten Sinz, Alexander Kaiser, and Thomas Gorges: Reducing False Positives by Combining Abstract Interpretation and Bounded Model Checking. In Proc. of the 23rd IEEE/ACM Intl. Conf. on Automated Software Engineering (ASE 2008), L'Aquila, Italy, September 2008. To appear. http://www.carstensinz.de/papers/ASE-2008-Combining.pdf Plauger, P.J.: Frequently Answered Questions: STL C/C++ Users Journal, December 1999, S. 10-17 Rabinowitz, Stanley; Wagon, Stan: A Spigot Algorithm for the Digits of π American Mathematical Monthly, Band 102, Heft 3, 1995, S. 195–203 Riel, Arthur J.: Object-Oriented Design Heuristics Addison-Wesley, Reading, Mass. 1996 Schader, Martin; Kuhlins, Stefan: Programmieren in C++ Springer-Verlag, Berlin, Heidelberg, New York 1998 Segal, Mark; Akeley, Kurt: The OpenGL Graphics System. A Specification (Vers. 1.2.1) 1999, im Internet unter: http://www.opengl.org Stepanov, Alexander; Lee, Meng: The Standard Template Library 31.10.1995, http://www.cs.rpi.edu/~musser/doc.ps Stewart, Ian: Mathematische Unterhaltungen Spektrum der Wissenschaft, 12/1995, S. 10–14 Stroustrup, Bjarne: The C++ Programming Language 2nd ed., Addison-Wesley, Reading, Mass. 1991 3rd ed., Addison-Wesley, Reading, Mass. 1997 Stroustrup, Bjarne: The Design and Evolution of C++ Addison-Wesley, Reading, Mass. 1994 Sutter, Herb: Exceptional C++ Addison-Wesley, 2000
Literaturverzeichnis
1307
Sutter, Herb; Alexandrescu, Andrei: C++ Coding Standards Addison-Wesley, 2005 Sutter, Herb: Exceptional C++ Style Addison-Wesley, 2005 Sutter, Herb: The New C++: Trip Report C/C++ Users Journal, CUJ Online, February 2003 (nur online verfügbar) http://www.cuj.com/documents/s=8246/cujcexp2102sutter/ Taivalsaari, Antero: On the notion of inheritance ACM Comput. Surv. 28, 3 (Sep. 1996), Pages 438 - 479 UML: Unified Modeling Language (UML), version 1.5, 2003 http://www.omg.org/, http://www.uml.org/ Vandevoorde, David; Josuttis, Nicolai: C++ Templates Addison-Wesley, 2003 Veldhuizen, Todd: C++ Templates as Partial Evaluation 1999 ACM SIGPLAN Workshop on Partial Evaluation and Semantics-Based Program Manipulation (PEPM'99) Im Internet unter: http://extreme.indiana.edu/~tveldhui/papers/ Veldhuizen, Todd: Techniques for Scientific C++ Im Internet unter: http://extreme.indiana.edu/~tveldhui/papers/ Wirth, Niklaus: Algorithmen und Datenstrukturen Teubner, Stuttgart 1983
Inhalt Buch-CD
Verzeichnis
Inhalt
Loesungen_VC2008
Die Lösungen der Übungsaufgaben, meist im Rahmen eines vollständigen Visual C++ 2008 Projekts. Alle Lösungen sind in Header-Dateien enthalten, die meist auch unter Visual C++2005 lauffähig sind. Die Namen der Header-Dateien geben meist einen Hinweis auf das Kapitel, zu dem sie gehören. Zum Beispiel in \Loesungen_VC2008\Kap_3__1_10\ Int.h Func.h
Aufgaben 3.3: Ganzzahldatentypen Aufgaben 3.4: Kontrollstrukturen und Funktionen FloatU.h Aufgaben 3.6: Gleitkommadatentypen Arrays.h Aufgaben 3.10: Arrays und Container 10_2.h Aufgaben 3.10.2 Im Unterverzeichnis CppUtils sind einige HeaderDateien, die auch für andere Projekte nützlich sein können.
Buchtext
Der Buchtext und die Lösungen als pdf-Datei.
Index
– – 112, 387 ! Siehe not, logisches != 115 != (ungleich) 392 π Siehe Pi, Siehe Pi #define 423 #elif 428 #else 428 #endif 428 #if 428 #ifdef 431 #ifndef 431 #include 142, 422 #pragma 434 comment(lib, …) 435, 447 Link-Bibliothek 435 once 433 #undef 424 #using 940, 943, 1069 % 390 %= (Zuweisung) 396 & Siehe Adressoperator, Siehe and, bitweise & (Referenztyp) 358 && Siehe and, logisches &= (Zuweisung) 396 * 390 * (Dereferenzierungsoperator) 273, 387 */ 348 *= (Zuweisung) 396 , Siehe Komma-Operator .NET Bibliothek 144
/ 390 /* 348 // 348 /= (Zuweisung) 396 /CLR 941 :: (Bereichsoperator) 384 ? (Bedingungsoperator) 394 ^ 47, 60, Siehe xor, bitweises ^= (Zuweisung) 396 __cplusplus 432 __DATE__ 425 __FILE__ 425 __FUNCSIG__ 425 __FUNCTION__ 425 __LINE__ 425 __TIME__ 425 __try-__except 813 _cdecl 555 _fastcall 555 _MSC_VER 430 _pascal 555 _stdcall 555 _tmain Funktion 588 | Siehe or, bitweises || Siehe or, logisches |= (Zuweisung) 396 ~ Siehe not, bitweises + 390 ++ 112, 387 += (Zuweisung) 396 < 115 386 >= 115 >> 390, 609 >>= (Zuweisung) 396 3n+1-Problem 140
A Ablaufprotokoll 186 für Zeiger 279 symbolisches 193 Ableitung Siehe Vererbung abort 836 abs 593 abstract 1006 abstrakte Basisklasse Siehe Basisklasse abstrakte Klasse Siehe Klasse abstrakter Typ 781 AccessViolationException 1025 accumulate Siehe STL Ackermann-Funktion 568 ActiveX-Komponente 1147 adjacent_difference Siehe STL adjacent_find Siehe STL Adresse 95 Adressoperator & 275, 387 ähnliche Strings Siehe String aktives Steuerelement Siehe Fokus Aktivierreihenfolge Siehe Tabulatorreihenfolge Algorithmus Siehe STL Aliasing Siehe Zeiger Analogrechner (Aufgabe) 1117 Anchor 1129 and 402 & bitweise 112, 393 logisches 115, 394 and_eq 402 ANSI-Zeichensatz 119 Anweisung 365 Apfelmännchen 1200 Application 1061 DoEvents 134 Application Klasse 1048, 1245 argc 587 Argument 54, 135
Default- 588 argv 588 arithmetische Konversion Siehe Konversion arithmetische Operatoren für Ganzzahldatentypen 109 für Gleitkommadatentypen 168 arithmetischer Datentyp 162, Siehe Datentyp array 236 Array 235 Adresse Element 238 als Klassenelement 261 Anzeige in DataGridView 1214 const 245 Container 245 dynamisch erzeugtes 235, 282, 292 fixed size 545 gewöhnliches 235 Initialisierung 243 mehrdim. dynamisch 335 mehrdim. Index 398 mehrdimensionales 252 mit char-Elementen 244 mit range-checks 882 ohne Elementanzahl 244 Parameter 298 Speicherbereich 238 Standardkonstruktor 637 typedef 343 Unterschied zu Struktur 264 Vergleich mit Liste 319 von Objekten 637 Array (Klasse System Array) 973 Arraygrenze enum 347 ArrayList Siehe Collection-Klassen ASCII-Zeichensatz 119 asm 381, 941 Assembler Ausgabe 297 Assembler-Anweisung 381 Assembly 940 Assembly (Klasse) 1073 assert 209, 433, 827 Assert 154 AreEqual 154 assoziative Container 526 Assoziativgesetz 166 asynchrone Programmierung Siehe Thread at 464
Index
1313
atof 458, Siehe nullterminierter String atoi 458, Siehe nullterminierter String Attribute 1077 AttributeTargets 1079 benannte Parameter 1081 Entwurfszeitattribute 1069 GetCustomAttributes 1081 im Eigenschaftenfenster 1069 ObsoleteAttribute 1079 Parameter 1080 selbstdefinierte 1080 STAThread 1079 vordefinierte 1079 Auf/Ab-Steuerelement 1114 Aufgabe EnterNextTextBox 1051, 1071 FocusColorTextBox 1052, 1071 Klasse AssozContainer 682 Klasse Bruch 606, 681 Klasse Grundstueck 642 Klasse Kreis 642 Klasse Quadrat 642 Klasse Rechteck 642 ValueTextBox 1052, 1071 Aufgabenliste 12 Aufrufoperator () Siehe operator Aufruf-Stack 160, 554 Aufzählungstyp 49, Siehe enum Ausdruck 382 additiver 575 auswerten, Parser 573 geklammerter 383 multiplikativer 575 Postfix 385 Präfix 386 primärer 383 Reihenfolge der Auswertung 366 unärer 386 Ausdrucksanweisung 366 ausführbares Programm 16 Ausführungszeit 484 Ausgabeoperator > 511 Einkommensteuer 181 Element einer Klasse 614 Elementfunktion 615 Aufruf 632 Default-Argument 617, 711 inline-Funktion 616 Namensgebung 650 UML-Diagramm 662 Zeiger auf 787 Elementinitialisierer 668, 670 Basisklasse 720 für const Datenelemente 670 else-Zweig 126 E-Mails versenden 1297 end 466 endl 510 Endlosschleife 133, 201, 376 EndUpdate 1136 Entwicklungsprozess 8 Entwicklungsumgebung 1 Entwurfszeit 8 enum 344 Arraygrenze 347 class 348, 1009 CLI Erweiterung 348, 1009 in einer Klasse 709 Konversionen 346 Enum (Basisklasse) 1009 enum class 49 Enumerator 344 Environment Klasse 1226, 1244 eof 503 equal Siehe STL equal_range Siehe STL Equals Siehe Object Equals Methode 1218 erase 467 Eratosthenes, Sieb des 242 Ereignis 6, 64, 65 Click 64, 65 Click 1047 DragOver 65 Enter 65 event 1046 KeyDown 65 KeyPress 65, 1112 KeyUp 65 Leave 65 MessageFilter 1061
1318
MouseDown 65 MouseMove 65 MouseUp 65 Resize 1062 static 1054 triviales 1053 virtual 1054 Ereignisbehandlungsroutine 6, 66 Ereignishandler Siehe Ereignisbehandlungsroutine errno 173, 185, 807 ErrorProvider 1102 Erweiterbarkeit 774 und virtuelle Funktion 771, 780 Verlust mit RTTI 797 Escape-Sequenz 120 event Siehe Ereignis event handler 6, 66 EventHandler 1047 EventLog 1029 Excel Tabelle als Datenbank 1264, 1268 Excel steuern 1206 exception 370, 813 abgeleitete Klasse 821 Klassenhierarchie 816 what 820 Exception 370 AccessViolationException 1025 ApplicationException 1025 auslösen 818 bad_typeid 795 Basisklasse 1054 FormatException 1026 im Konstruktor von C++/CLI-Klassen 1003 in Konstruktor 830 in Windows Forms-Anwendungen 1028 nicht abgefangene 1028 NullReferenceException 818, 1025 SystemException 1025 terminate 834, 836 und Fehler 824 und Ressourcen-Freigabe 827 und Threads 1175 unexpected 834 Exception (.NET-Klasse) 817, 1023 Klassenhierarchie 817, 1023 Konstruktoren 1023 Exception-Handler 808, 810 Exception-Handling 368, 807
Index Aufrufstack 810 stack unwinding 810 exceptions (Funktion) bei Streams 814 failure 814 Exception-Spezifikation 834 C++/CLI 1023 Exit (Programm beenden) 1226 exp 172 explicit 704 explizite Instanziierung 849, 868 explizite Spezialisierung 850, 870 explizite Typkonversion Siehe Konversion expliziter Konstruktor 704 export (Template) 839 extern 441 "C" 448
F fabs 593 Fakultät 179, 568 Farbe (Color) Siehe Graphics fclose 524 Fehler und Exceptions 824 Fehlermeldung des Compilers 50 Fehlerrate, Software 186 Fenster modales 1105 feof 525 ferror 525 Festkommadatentyp FixedP64 176, 698 fflush 524 fgets 526 Fibonacci-Zahlen 139 aus Array 245 dynamische Programmierung 256 iterativ 139 rekursiv 568 FIFO Siehe Liste File Klasse (Dateien) 1245 FileInfo 975, 1244 FileInfo Klasse (Dateien) 586, 1245, 1246 FileOk Ereignis 79 FileSystemWatcher Klasse 1248 fill Siehe STL fill_n Siehe STL Finalisierer 997 Vererbung 999
Index Finalizer 997 find Siehe STL find_end Siehe STL find_first_of Siehe STL find_if 892, Siehe STL fixed size array 545 flache Kopie 687, 692 float 162 FlowLayoutPanel 1132 flush 503 Fokus 68, 1057 FolderBrowserDialog 80 FontDialog 80 fopen 523 for each Anweisung 973 IEnumerable 1019 for_each 885, 908 for-Anweisung 131, 376 Form Show 1105 ShowDialog 1105 Format Siehe String, Siehe String Formatangabe Siehe printf FormatException 818, 1026 FormBorderStyle 1010 Formular 2 alle F. einer Anwendung 1059 Gestaltung 24 Konstruktor 67 Schließen 67 FormWindowState 1127 for-Schleife 238 ForwardIterator 900 fprintf 331, 526 Fraktal 580, 1200 fread 524 free 288 free store 357 freier Speicher 280 friend Funktion 673 Klasse 674 front_inserter 904 frühe Bindung Siehe Bindung fscanf 526 fseek 525 fstream Siehe Stream ftell 525 function-try-block 830 Funktion 53, 134, 551 Adresse 558 als Parameter 560
1319 am besten passende 596 Aufruf 135 aus C 447 Datentyp einer 555 Deklaration 53 exakt passende 596 globale 53 Größe der Parameterliste 215 Header 53 inline 590 Name 213, 217 Parameter 135 Programmierstil 213 Prototyp 53 Rückgabetyp 53 sealed 1006 Spezifikation 213 statische Element- 53 überladene 137, 593, 762 überschreibende 762 verdeckte 750, 762 virtuelle 750 zeichnen 1190, 1198 Funktionsaufruf 552 Nachbedingung 212 Funktionsdefinition 552 Funktionsdeklaration Siehe Funktion Funktionsobjekt 885 arithmetisches 891 Funktions-Template 839 Aufruf 841 explizite Instanziierung 849 explizite Spezialisierung 850 Metaprogrammierung 854 Nicht-Typ-Parameter 847 statische lokale Variablen 846 überladenes 851 Funktionswert 134 Datentyp des 53 Funktionszeiger 558, 600
G Ganzzahldatentyp 98, 100 mit Vorzeichen 100, 103 ohne Vorzeichen 100, 101 ganzzahlige Typangleichung 107 Ganzzahlliteral Siehe Literal garbage collection 291 Garbage Collection 287 Garbage Collector 327, 951 Gauß’sche Glockenkurve 1200
1320 Gauß’sche Osterformel 140 Gauß’sches Eliminationsverfahren 255, 483 gcnew Siehe C++/CLI gc-Zeiger Siehe C++/CLI Geburtstagsproblem von Mises 180, 242 Geldbetrag Siehe Datentyp Generalisierung Siehe Vererbung generate Siehe STL generate_n Siehe STL generic 1084 constraint 1088 Unterschied zu Templates 1086 generische Programmierung 837, 1084 generische Zeiger Siehe Zeiger gestreute Speicherung 181 GetDirectories 975, 1242, 1243 GetDrives 974 GetFiles 975, 1243, 1244 GetHashCode Methode 959, 992, 1218 getline 513 GetLogicalDrives 1242 GetType Siehe Object GetWindowsDirectory 950 ggT Siehe größter gemeins. Teiler Gleitkommadatentyp 162 dezimaler 176, 178 Double 174, 1008 Genauigkeit 162 Reihenfolge der Summation 182 Single 174, 1008 und kaufmännische Rechnungen 175 Wertebereich 162 Gleitkommaformat 163 binäres 163 dezimales 163 Überlauf (overflow) 165 Unterlauf (underflow) 165 Gleitkommaliteral Siehe Literal Gleitkommawerte Gleichheit 171, 183 NearlyEqual 183, 184 globale Variablen Siehe Variablen Goldbach’sche Vermutung 140 goto-Anweisung 378 undefinierte 379 Grafik Siehe Graphics anzeigen 1187 Bildschirmkoordinaten 1190 Weltkoordinaten 1190 zeichnen 1187
Index
Graphics 1187 Brush 1195 Color 1193 DrawEllipse 1193 DrawLine 1187 DrawLine DashStyle 1194 DrawLines 1193 DrawRectangle 1193 DrawString 1193, 1195 Invalidate 1188 Koordinatentransformationen 1191 Paint 1188 Pen 1187 RGB Farbanteile 1194 Text zeichnen 1195 Gregorianischer Kalender 124 größter gemeinsamer Teiler 206, 568 GroupBox 72 Grundstueck 695, 726, 737 GSLGNUscientificlibrary 174 Gültigkeitsbereich 354, 625 Klasse 623 GZipStream Klasse 1249
H Haltepunkt 158 Handle 327 Handle (CLI-Heap) 951 Handle (GC-Heap) 327 Handle (Windows) 1057 Handlungsreisender Siehe Problem des Hash-Container 539 HashSet Siehe Collection-Klassen Hash-Tabelle 181 „hat ein“-Beziehung 733 Hauptmenü Siehe MenuStrip Hauptspeicher 95 Hausdorff-Dimension 580 Header-Datei 438, 444, 711 Elemente 445 Heap 280, 357, 931 CLI-Heap Siehe C++/CLI garbage collected Heap 327, 951 nativer Heap 327, 938, 951 HelpProvider 1113 Hexadezimalliteral Siehe Literal Hexadezimalsystem 102 Hilfe Siehe Online-Hilfe hochauflösende Zeitmessung 1158 HorizontalAlignment 1010 Hornerschema 242, 300
Index
1321
HRESULT E_FAIL 19 HScrollBar 1116 HTML 1294 HTML-Format 514, 1232 Hypothekendarlehen 179
I ICollection Siehe CollectionKlassen IComparable 1015, 1017 IDE 1 identifier 93 IEnumerable 1018 if-Anweisung 71, 126 IFormattable 1020 ifstream Siehe Stream ILDasm 940 IList Siehe Collection-Klassen Image Library Siehe Bildbibliothek ImageList 1133 imperative Programmierung 1083 implizite Typkonversion Siehe Konversion includes Siehe STL Indexer 1035 Indexoperator [] Siehe operator indirekte Basisklasse Siehe Basisklasse indizierte Properties 1035 Induktion Siehe vollständige Induktion information hiding 629 Initialisierung 357, 702 Array 243 dynamisch erzeugte Variable 281 struct 263 von Variablen 235 Zeitpunkt 357 initonly 987 inline Siehe Funktion inline-Funktion 444, 616, 711 inner_product Siehe STL inplace_merge Siehe STL InputIterator 900 insert 467 inserter 904 Instanz Siehe Objekt int 47, 99 Int16 125, 1008 Int32 125, 1008 Int64 125, 1008 integral conversion 107 integral promotion 107
Integration, numerische 561 integrierte Entwicklungsumgebung 1 IntelliSense Siehe Editor Interface-Klasse 785, 1014 als Parameter 1018 Implementation einer 1015 Mehrfachvererbung 1015 zulässige Elemente 1016 Interlocked Siehe Thread Invalidate Siehe Graphics Invariante 195 Klasseninvariante 574, 657 Klasseninvariante bei Vererbung 727 Schleifeninvariante 200 zwischen Funktionsaufrufen 574 iostream 29 IsNaN 173 „ist ähnlich wie ein“-Beziehung 779 „ist ein“-Beziehung 727 C2DPunkt und C3DPunkt 730, 753, 779 Kriterium 730 Quadrat und Rechteck 729 und Umgangssprache 731 als notwendige Bedingung 731 istream_iterator 905 istringstream 457, 611 Items 60 iter_swap Siehe STL Iterator 466, 603, 900 Bereich 902 Bidirektional- 900 Einfüge- 904 Forward- 900 Input- 900 Kategorie 901 Output- 900 RandomAccess- 900 selbst definierter (Aufgabe) 683 Stream- 905 Umkehr- 902 ungültiger 469, 485, 531
J Julianischer Kalender 124
K Kalender 1154 Kalenderdatum 1151 Kalenderklassen 1162
1322
KeyPress-Ereignis 1112 Klasse 59, 258, 614, 646 abgeleitete 713 abstract 1006 abstrakte 776 als Datentyp 664 Arrayelement 261 C++/CLI-Erweiterungen 1030 Diagramm 663 Gleichheit des Datentyps 261, 617 im Debugger 264 Interface 785 Konsistenzbedingung 654 Korrektheit 653 Name Datenelement 649 Name Elementfunktion 649 Namensgebung 649 native 264, 938 polymorphe 752 reales Konzept 646, 730 sealed 1006 static Element 705 UML-Diagramm 661 Vergleichsoperator 262 Vorwärtsdeklaration 626 Werte- 264 Klassenbibliothek (CLR-Projekt) 942 Klassendiagramme 663 Klassenelement 614 Name in einer Hierarchie 718 Klassengültigkeitsbereich Siehe Gültigkeitsbereich Klassenhierarchie 714, 732 systematische Konstruktion 774 Klasseninstanz Siehe Objekt Klasseninvariante Siehe Invariante Klassen-Template 858 abhängige Parameter 865 Array mit range-checks 882 Default Argument 864 Elementfunktionen 878 Element-Templates 878 erzeugte Elemente 859, 868 explizite Instanziierung 868 explizite Spezialisierung 870 Fehler 864 friend 881 Klassen-Elemente 878 Klassen-Elemente 879 mehr spezialisiertes 869 Nicht-Typ-Parameter 866 partielle Spezialisierung 869
Index primäre Deklaration 869 Quelltext 863 template 870 und abgeleitete Klassen 881 virtuelle Elementfunktionen 880 vollständige Spezialisierung 870 Klassifikation 729 kleinste Quadrate 538 Knoten Siehe Liste, verkettete Koch’sche Kurven 579 Kommandozeilen Compiler cl 32 Kommandozeilen-Parameter 1226 Komma-Operator 397 bei for-Initialisierung 377 Kommentar 45, 348 Dokumentationskommentar 1090 verschachtelter 429 Zeilenendkommentar // 348 Kompilation separate 437 komplexe Zahlen 533 Komplexität 484 Komponente 1055, 1066 Komponentenfach 75, 1066 Komposition 732, 741, 793 vs. Vererbung 733 Konkordanzliste 532, 1224 konkreter Typ 781 Konsolen-Anwendung 25, 29 Konstante 104, 230 - Variable 232 symbolische 231 Zeiger- 301 konstante Klassenelemente Siehe const konstante Referenzparameter 363 Konstruktor 337, 634, 960 Aufgabe 637 automatischer Aufruf 634 bei C++/CLI-Klassen 1002 Datenelement einer Klasse 668 Default- Siehe Standardkonstruktor Exception bei C++/CLI-Klassen 1003 Exceptions 830 expliziter 704 function-try-block 830 konvertierender 700, 704 Kopier- Siehe Kopierkonstruktor new 635 Reihenfolge der Aufrufe 669, 721, 745 Standard- Siehe Standardkonstruktor
Index
1323
static 988 und Exceptions 825 und virtuelle Funktionen 761 virtueller 766 Konstruktor-Initialisierer 667 Kontextmenü 77, Siehe Menü, Kontextmenü ContextMenuStrip 77 kontextsensitive Schlüsselworte 938 Kontobewegung (Beispiel) 259 Eingabemaske 478 Kontravarianz 788 Konversion 106 benutzerdefinierte 599, 702 const_cast 404 durch Konstruktor 679, 700 dynamic_cast 797 explizite 403 explizite Typ- 703 implizite Typ- 703 in Funktionsschreibweise 408 in typecast-Schreibweise 408 Parameter 106 reinterpret_cast 407 safe_cast 993 sichere 107 static_cast 405 string 459 String/char* 306 übliche arithmetische 110, 168 von Zeigern 278 zwischen abgeleiteten Klassen 734 Konversionsfunktion 701 Konvertierungsfunktionen 53 Kopierkonstruktor 684 Aufruf 688 bei Verweisklassen 980 der Basisklasse aufrufen 724 implizit erzeugter 686, 723 Korrektheit einer Klasse 655 kovariant 764 Kreis 695
L Label (.NET-Komponente) 47 längste gemeinsame Teilfolge 461 Laufwerke (DriveInfo Klasse) 1241 Laufzeit 8 Laufzeitfehler und Exceptions 809 Laufzeit-Typinformationen 794
Laufzeitvergleiche 171 lcs (longest common subsequence) 461 Lebensdauer 355 automatische 355 dynamische 355, 357 statische 355 und Initialisierung 357 Lebenslauf eines Objekts 655 leere Anweisung 366 letzte überschreibende Funktion 757 Levenshtein-Distanz 310 lexical_cast 856 lexicograhical_compare Siehe STL Lib 447 LIFO Siehe Liste limits.h 100 lineares Gleichungssystem 255, 483 lineares Suchen 246 LinkedList Siehe CollectionKlassen Linker 16, 438, 439, 443 lib im Quelltext angeben 435 Objektdatei linken 447 LinkLabel 1126 list 485, 861 List Siehe Collection-Klassen ListBox 60, 63 Liste, verkettete 311, 582 ausgeben 314 doppelt verkettete 321 FIFO 317 Knoten 311 Knoten einfügen 313 LIFO 314 Speicher freigeben 318 Vergleich mit Array 319 Zeiger auf letztes Element 315 Listendruck 516 ListView 1134 Literal 104 boolesches 114 Dezimal- 104 Ganzzahl-, Datentyp 105 Ganzzahlliteral 104 Gleitkomma- 166 Hexadezimal- 105 Oktal- 104 primärer Ausdruck 383 String- 303 Suffix 105 wchar_t 123 Zeichen- 120
1324
Index
literal Klassenelement 987 lock Siehe Thread log 172 logic_error 370 lokale Variablen Siehe Variablen lokales Menü Siehe Kontextmenü , Siehe Menü, Kontextmenü Lokalisierung 1118 Lokalität 551 long 99 long double 162 long long 99 longest common subsequence 461 Lotka-Volterra-System 1202 lower_bound Siehe STL L-Wert 399
M M_E 173, 423 M_PI 173, 423 main Funktion 15, 587 einer Assembly 939 MainMenu 75 make_heap Siehe STL make_pair 862, 883 Makro 423, 592 TRACE 436 malloc 288 Managed C++ 938, 1096 managed code 940 Manipulator 510, 517 Mantisse 163 map 526, 528, 861 Marshal 307 marshal_as 306 MarshalByRefObject 1055 MaskedTextBox 1109 math.h 143 math.h-Funktionen 172 mathematische Funktionen 172 GNU scientific library 174 Math-Funktionen (.NET) 173 Matrix 253, 482 Mauszeiger 1027 max Siehe STL max_element Siehe STL MDI ActiveMDIChild 1150 Anordnung Formulare 1150 Cascade 1150 Child-Formular 1149
TileMode 1150 MdiChildren 1150 Median 229, 933 Mehrfachvererbung 742 bei C++/CLI-Klassen 1001 bei Interface-Klassen 1015 MeinStack 860 MeinString 631 Append 659 c_str() 644 Destruktor 640 Klasseninvariante 654, 657 Konstruktor 640, 672 Kopierkonstruktor 687 Operator [] 680, 710 Operator + 678, 679 Operator += 679 Operator < 677 Operator = 693 Standardkonstruktor 665 mem_fn 897 mem_fun 896 mem_fun_ref 896 MemberwiseClone Siehe Object memory leak 284 Menü 74 dynamisch erzeugen 1051 Kontextmenü 14 Menüdesigner 75 Menüeintrag 74 Menüleiste 74 MenuStrip 75 merge Siehe STL MessageBox 82 Metadaten 940 Methode 53, 54, 976 Methode der kleinsten Quadrate 538 Metrik 216 MFC-Anwendung 26 min Siehe STL min_element Siehe STL Mischen merge Siehe STL Mises, Geburtstagsproblem 180, 242 mismatch Siehe STL modaler Dialog 1105 modales Fenster 1105 Modulare Programmierung 437 Monatskalender 1154 Monitor Siehe Thread MONO 937 MonthCalendar 1154
Index
1325
MSDN-Dokumentation Siehe OnlineHilfe MSIL (Intermediate Language) 940 MS-Office steuern 1203 MultiDictionary Siehe Collection-Klassen multimap 526, 528 Multiple Document Interface Siehe MDI Multiplikation Stringzahl 341 Multiplikation, Algorithmus 227 multiset 526 Multithreading 1164 mutable 710 Mutex Siehe Thread
N Nachbedingung 195, 827 Name Bezeichner 93 Datenelement 649 einer Komponente 44 einer Komponente ändern 45 Elementfunktion 649 Namensbereich 410 Aliasname 418 benannter 411 globaler 413 rk1 144 unbenannter 419 vs. static Klassenelement 707 namespace Siehe Namensbereich native code 940 native Klasse Siehe Klasse nativer Heap Siehe Heap NDEBUG 433 Negation einfache Bedingungen 219 im else-Zweig 221 NET Quellcode 161 Überblick Klassenhierarchie 1054 NetBIOS Name des Rechners 1226 new 281 placement 283 new[] 282 Newton-Raphson-Verfahren 561 next_permutation Siehe STL N-gram 460 Nicht-Typ-Parameter 847, 866 Normalverteilung 1200
not 402 ~ bitweise 112, 388 logisches 115, 388, 394 not_eq 402 NotifyIcon 1127 npos 454 nth_element Siehe STL NULL 278, 432 null terminierter String wchar_t* 332 Nullanweisung 366 nullptr Siehe C++/CLI NullReferenceException 818, 1025 Nullterminator Siehe Nullzeichen nullterminierter String 303 atof 330 atoi 330 kopieren 304 strcat 330 strcmp 330 strcpy 329, 333, 367 strlen 329 strncpy 333 strncpy_s 334 strstr 330 strtod 334 strtol 334 wcscat 332 wcscpy 332 wcslen 332 Nullzeichen 119, 120, 303 Nullzeiger Siehe Zeiger numeric_limits 101, 871 NumericUpDown 1115 numerische Integration 179, 561 NUnit Siehe Unit-Tests
O Object 63 Clone 981 Equals Methode 958, 991 GetHashCode Methode 959, 992 GetType Methode 957 MemberwiseClone 981 ReferenceEquals 958 Object Constraint Language 196 Objekt 285, 614 Namensgebung 649 reales 646 objektbasierte Programmierung 781 Objektdatei 16
1326 objektorientierte Analyse 645 objektorientierte Programmierung 59, 63, 613 objektorientiertes Design Siehe OODesign OCL 196 ofstream Siehe Stream Oktalliteral Siehe Literal oldSyntax Siehe CLR One definition rule 444 Online-Hilfe 19 .NET 21, 40 C++-Referenz 20 C++-Standardbibliothek 20 C-Standardbibliothek 20 Dynamische Hilfe 21 F1 20 mit der Klasse Help 1114 mit HelpProvider 1113 mit ToolTip 1113 MSDN 20 STL 20 Suchen 21 visuelle Komponenten 40 Win32-API 22 zu Komponente 43 OOAD 645 OO-Design 645 iterativer Prozess 647 Komposition 732 und abstrakte Basisklassen 779 OpenFileDialog 80 operator 602 () -Aufrufoperator 885 [] 680, 710 = 691 Operator = bei Verweisklassen 980 der Basisklasse aufrufen 724 implizit erzeugter 691, 723 Operatoren alternative Zeichenfolgen 402 Assoziativität 400 Priorität 382, 399, 400 Operatorfunktion 602 (++/ – –, C++/CLI) 989 (++/ – –, Präfix- vs. Postfix) 605, 677, 698 > 609 als Elementfunktion 676 binäre Operatoren 678
Index eingängige Symbole 603 globale 604 static (C++/CLI) 989 Typ-Konversion 701 virtuelle 765 or 402 | bitweise 112, 393 logisches 115, 394 or_eq 402 Ostersonntag 140 ostream_iterator 905 ostringstream 457, 611 OutputIterator 900 override 1004
P Paint Siehe Graphics pair 861 Panel 72 Parameter 54, 55, 551 Array 298 Konversion 106, 167 Referenz- 142, 360 Werte- 135, 142, 360 Parameter Array 55 Parameter-Array 1031 Parameterliste 53 Parent 1056 Parse 125, 174, 966, 1008 Parser, rekursiv absteigender 573 partial_sort Siehe STL partial_sort_copy Siehe STL partial_sum Siehe STL partition Siehe STL Pascal-Dreieck 257, 566 path coverage Siehe Test, Bedingungsüberdeckung Path Klasse (Dateien) 1247 Pen Siehe Graphics Permutation 924 Pfadangaben, '_ in 121 Pfadangaben, '\'in 80 Pfadüberdeckung Siehe Test Phasendiagramm 1202 Pi, π 173, 423 numerische Integration 180 Tröpfelverfahren 250 PictureBox 1187 Pixel 47 placeholder 894 Platzhalter 894
Index Pointer Siehe Zeiger Polymorphie 752 Polynom 242, 300 pop 552 pop_heap Siehe STL Position einer Komponente 47, 48 Positionszeiger 498 postcondition Siehe Nachbedingung Postfix-Ausdruck Siehe Ausdruck Potenz Algorithmus 229 pow 172 Prädikat 888 pragma Siehe #pragma Präprozessor 421 precondition Siehe Vorbedingung Predicate 888 prev_permutation Siehe STL Primzahl 139, 242 PrintDocument 1196 printf 52, 331 Formatangabe 331 priority_queue 487, 931 private 627 Ableitung 739 Basisklasse 739 Element 627 Element Basisklasse 716 Problem des Handlungsreisenden 179 Process 1227 Produkt von zwei Zahlen 227 Profiler 1159 Programmgerüst 777 Programm-Parameter 1226 Programmverifikation 194 ergänzende Tests 208 Schleife Siehe Schleifeninvariante Voraussetzungen 207 ProgressBar 1121, 1122 und BackgroundWorker 1169 Projekt Dateien automatisch speichern 16 Dateien speichern 16 Projekt 14 Projektmappe 23 Projektmappen-Explorer 23, 438 Projektvorlage 83 property 5 Property 1032 default-indizierte 1036 im Eigenschaftenfenster 1034 Indexer 1035 indizierte 1035
1327 Lesemethode 1032 Schreibmethode 1032 static 1034 triviale 1034 und visuelle Programmierung 1033 virtuelle Methoden 1037 Zugriffsrecht 1034 protected Ableitung 739 Basisklasse 739 Element 627 Element Basisklasse 716 Protokollklasse 777 Prototyp 442 Prozess 1165 beenden 1227 CPU-Zeit 1228 Speicher 1228 starten 1227 Threads 1228 Pseudo-Destruktor 386 public 627 Ableitung 739 Basisklasse 739 Element 627 Element Basisklasse 716 push 552 push_back 463 push_front 904 push_heap Siehe STL Pythagoräische Zahlentripel 140
Q qsort Siehe Sortieren Quadrat 695, 726, 728, 736 Quelltextdatei 438 Quelltexteditor Siehe Editor queue 487, 865, 866 Queue 317 Queue Siehe Collection-Klassen Quicksort Siehe Sortieren
R RadioButton 70 RAII 828 RAM 95 rand() Siehe Zufallszahlen random access 520 Random Klasse Siehe Zufallszahlen random_shuffle Siehe STL
1328 RandomAccessIterator 900 Räuber-Beute-Modell 1202 rbegin Iterator 902 rdtsc 381 read 503 ReaderWriterLockSlim Siehe Thread realloc 294 ReAllocate 293 Rechenprogramm 58 Rechteck 695, 726, 728, 736 Rechtschreibprüfung 531 Record-Locking 1238 ref class Siehe Verweisklasse ReferenceEquals Siehe Object Referenz Initialisierung 688 Referenzklasse Siehe Verweisklasse Referenzparameter 360, 363, Siehe Parameter Initialisierung 361 konstante 363, 364 Referenzsemantik 762 Referenztyp 358, 360 Rückgabetyp 607 Reflektion 940 Regeln von de Morgan Siehe de Morgan Regex Siehe regulärer Ausdruck register Siehe Speicherklasse Regressionsgerade 538 regulärer Ausdruck .NET 1287 regex 542 Regex 1287 TR1 542 Reihenfolge, Auswertung Teilausdrücke 400 rein virtuelle Funktion 776 reinterpret_cast 407 Rekursion 563 Effizienz 568 Einfachheit 568 indirekte 581 rekursiv absteigender Parser 573 rekursiver Datentyp 581 Release Konfiguration 16 remove Siehe STL remove_if Siehe STL rend Iterator 902 replace 900, Siehe STL replace_copy Siehe STL replace_copy_if Siehe STL
Index
replace_if Siehe STL resource acquisition is initialization 828 Ressourcen-Freigabe 827 return-Anweisung 134, 381 reverse Siehe STL reverse_iterator 902 RichTextBox 1099 rk1 Namensbereich (utilities) 144 Assert 154 textBox 622, 984 tostring 453 toString 306, 453 rotate Siehe STL RoundToInt 181 RTF-Textformat 1099 RTTI Siehe Laufzeit-Typinformationen Rückgabetyp 134, Siehe Funktion Referenztyp 607 Rückgabewert 134 Rundungsfehler 165 R-Wert 399
S safe_cast 993 SaveFileDialog 80 SByte 125, 1008 scanf 52 Schaltjahr 124 Schleifen 131 Schleifenbedingung 131, 375 Schleifeninvariante 190, Siehe Invariante while-Schleife 203 Schleifenkörper 131 Schlüsseltabelle 522 Schlüsselwert 323 Schlüsselwort 94 kontextsensitives 938 Schneeflockenkurve 578 Schnittstelle einer Klasse 629 schrittweise Programmausführung 159 schrittweise Verfeinerung 217 scope Siehe Gültigkeitsbereich scoped_ptr 286, 883 SDI-Programm 1148 sealed 1006 search Siehe STL search_n 915 secure library functions 334 seekp 520
Index selbstdefinierte Komponenten 1062 und Ereignisse 1048 Semaphore Siehe Thread separate Kompilation 437 sequenzielle Container 485 sequenzieller Dateizugriff 494 Serialisierung 1250 BinaryFormatter 1252 SoapFormatter 1255 XmlSerializer 1257 set 526, 861 set_difference Siehe STL set_intersection Siehe STL set_symmetric_difference Siehe STL set_union Siehe STL Setup-Projekt 32 shared_ptr 287, 291, 490, 883 Shift-Operatoren 390 short int 99 Short-Circuit Evaluation 118 Sieb des Eratosthenes 242 signed char 99 signed long 99 Signifikand 163 Simpsonregel 562 sin 243 sin 172 Single Siehe Gleitkommadatentyp Singleton 712 size 464 size_t 113 size_type 454, 464 sizeof 113, 388 Sleep 1178 smart pointer 286, 291 Software, Fehlerrate 186 Software-Metrik 216 sort 572, 926 sort_heap Siehe STL SortedDictionary Siehe Collection-Klassen SortedList Siehe CollectionKlassen Sortieren Array, durch Auswahl 239 Array, mit sort 466 Container, mit sort 466 durch Zerlegen 569 qsort 572 Quicksort 569 späte Bindung Siehe Bindung Speicher
1329 automatischer 357 dynamischer 357 statischer 356 Speicheradresse, schreiben auf 407 Speicherklasse 356 auto 356 register 356 static 356 Speicherleck (memory leak) 284 Speicherplatzbedarf Objekt 626 Spezialisierung Siehe Vererbung eines Funktions-Templates 841 eines Klassen-Templates 859 explizite (Template) 850 vollständige 870 Spezifikation 146 einer Funktion und Exceptions 824 Funktion 213 Split Siehe String SplitContainer 1130 Splitter 1130 Splitting-Verfahren 182 sprintf 331, 458 Spy++ 1062 SQL Siehe Datenbank sqrt 172 srand() Siehe Zufallszahlen sscanf 458 stable_partition Siehe STL stable_sort Siehe STL stack 487, 865 Stack 248, 250, 357, 360, 552, 564, 857 Stack Siehe Collection-Klassen Standard Template Library Siehe STL Standardbibliothek 142, 449 C, C++-Version 143 Standarddialoge 78 Standardkonstruktor 637, 665, 667 implizit erzeugter 666, 670, 723 Teilobjekt 721 Standardkonversion 106, 167, 598 static 54, 440 bei Verweisklassen 983 Klassenelement 705 Klassenelement vs. globale Variable 707 Klassenelement vs. Namensbereich 707 Konstruktor 988 Operatorfunktion 989 Speicherklasse 356 static_cast 405, 802
1330 statischer Datentyp 749, 751 statischer Speicher 356 StatusStrip 1122 Steuerelement 3, 39, 1066 Steuerelementbibliothek 1063 Steuerformel Einkommensteuer 181 STL 449, 837 accumulate 932 adjacent_difference 933 adjacent_find 912 binary_search 476, 928 copy 474, 903, 918 copy_backward 918 count 912 count_if 912 equal 474, 913 equal_range 928 fill 921 fill_n 921 find 474, 911 find_end 915 find_first_of 911 find_if 911 generate 921 generate_n 921 includes 930 inner_product 933 inplace_merge 929 iter_swap 918 lexicographical_compare 914 lower_bound 476, 928 make_heap 931 max 916 max_element 916 merge 929 min 916 min_element 916 mismatch 914 next_permutation 924 nth_element 927 partial_sort 927 partial_sort_copy 927 partial_sum 933 partition 925 pop_heap 931 prev_permutation 925 push_heap 931 random_shuffle 924 remove 922 remove_if 922 replace 920 replace_copy 920
Index
replace_copy_if 920 replace_if 920 reverse 923 rotate 923 search 915 set_difference 930 set_intersection 930 set_symmetric_difference 930 set_union 930 sort_heap 932 stable_partition 925 stable_sort 926 swap 840, 917 swap_ranges 918 transform 919 unique 922 upper_bound 476, 928 STL/CLR 470 STL-Algorithmus 910 für alle Elemente eines Containers 907 Stopwatch 1158 strcat Siehe nullterminierter String strcmp Siehe nullterminierter String strcpy Siehe nullterminierter String Stream 495 .NET-Klassen 1229 > 511 Binärmodus 496, 500 Binärmodus Template 884 close 497 Direktzugriff (.NET) 1235 eof 503 exceptions 500 Fehler 499 FileStream (.NET-Klasse) 1234 flush 503 fstream 497 good 499 ifstream 497 Klassenhierarchie 747 Mehrbenutzerzugriff (.NET) 1237 mode 496 mode app 521 mode ate 521 ofstream 497 open 495 read 503 seekg 520 StreamReader (.NET-Klasse) 1230 StreamWriter (.NET-Klasse) 1230
Index
tellg 521 tellp 521 Textdatei 1230 Textmodus 496, 509 und Threads 1180 write 501 Zustand 499 Stream-Iterator 905 StreamReader 1196 strenge schwache Ordnung 475, 1017, 1214 strict weak ordering 475, 1017, 1214 string 449, 862 c_str 451 Elementfunktionen 450, 453 Index 451 Konstruktoren 450 length 450 String 47, 336, 959 Ähnlichkeit 309, 310, 460 boxing 337, 960 Compare 340, 898, 965 Elementfunktionen 338, 450, 960 Format 55, 967, 1020 Format selbstdef. Datentyp 1020 Format, IFormattable 1020 Index 451 Konstruktoren 337, 450, 960 Konvertierungsfunktionen 966 lcs 461 Length 450 Split 964 umwandeln 459 Vergleichsfunktionen 340, 965 StringBuilder 970 Capacity 294 EnsureCapacity 294 Stringliteral Siehe Datentyp Stringstream 457 strlen Siehe nullterminierter String strncpy_s Siehe nullterminierter String strstr Siehe nullterminierter String strstream 458 strtod Siehe nullterminierter String strtol Siehe nullterminierter String struct 258, 614 Bitfeld 269 Initialisierung 263 Strukturdiagramm 554 strukturierter Datentyp 264 Suchen binäres 246, 476
1331 lineares 246 swap Siehe STL swap_ranges Siehe STL switch-Anweisung 372 symbolische Ausführung Siehe symbolisches Ablaufprotokoll symbolische Konstante Siehe Konstante symbolisches Ablaufprotokoll 193 Beziehungen zwischen Variablen 196 für bedingte Anweisungen 227 Symbolleiste 13, 1121, 1123, 1124 Synchronized Siehe Thread Syntaxfehler 50 Syntaxregel 92 additive-expression 390 Array 234, 236, 252 assignment-expression 396 block declaration 93 case 372 compound statement 127 compound-statement 353 condition 127 cv-qualifier 232 declaration 92 declaration statement 352 delete 284 do 375 equality-expression 392 exception-declaration 809 exception-specification 834 expression 382 expression-statement 366 floating-literal 166 for 376 Funktion 134, 234 identifier 93 if 126 initializer 234 integer literal 104 iteration statement 131 jump-statement 378 labeled statement 378 literal 383 multiplicative-expression 390 new 281 postfix-expression 385 primary expression 383 relational-expression 391 return 381 shift-expression 391 simple declaration 93, 233
1332
Index
simple type specifier 93 statement 365 storage-class-specifier 356 switch 372 throw-expression 818 translation unit 92 try-block 809 type specifier 232 unary-expression 386 Zeiger 234 System IO 974
T TabControl 73 Tabelle (DataGridView) 1214 Tabelle der virtuellen Funktionen 759 TableLayoutPanel 1130 Tabulatorreihenfolge 68 tan 172 Taschenrechner 577 Taskleisten-Symbol 1127 Tastatureingaben filtern 1112 Taylor-Reihe 243 TCP/IP-Protokoll 1299 TcpClient 1299 TcpListener 1299 Teilobjekt 715 Template Siehe Klassen- oder Funktions-Template Argument 859 Funktions- 839 Spezialisierung 859 Template-Argument abgeleitetes 841 explizit spezifiziertes 844 Konversionen 842 template-id 858 Template-Metaprogrammierung 854 Template-Parameter 840 temporäres Objekt 635, 689 Initialisierung durch ein 688 terminate 836 Test 194, Siehe Unit-Tests allgemeine Bedingungen 208 Bedingungsüberdeckung 147 Black-Box Test 146 Dokumentation 152 Funktionen 149 mentale Einstellung 150 Pfadüberdeckung 148
SimpleUnitTests 154 Testfall 147 Test-First-Programming 155 Testfunktionen 152 verallgemeinerter 193 virtuelle Funktion 771 White-Box Test 146 Zweigüberdeckung 147 Testdaten erzeugen 507 Text 47 TextBox 52 mehrzeilige 57 Multiline 57 TextBoxBase 1099, 1109 Textfenster Siehe Konsole Textmodus (stream) 496, 509 then-Zweig 126 this-Zeiger 633 Thread 1164 BackgroundWorker Klasse 1166 ereignisbasiert asynchron 1169 Interlocked 1184 Join 1175 lock 1182 Monitor 1181 Mutex 1183 primärer 1165 Priorität 1164 Priority 1175 ReaderWriterLockSlim 1182 Semaphore 1184 Sleep 1178 Synchronized 1180 Thread Klasse 1170 Thread Pool 1185 Timer und Thread Pool 1186 und Exceptions 1175 und GUI-Anzeigen 1166 Zeitscheibe 1165 Zustand (ThreadState) 1165 ThreadException 1028 throw 368, 818 TileMode 1150 time stamp counter 381 Timer aus System Windows Forms 1155 Timers 1155 Threading 1157 und Thread Pool 1186 TimeSpan 1153
Index
ToBoolean Siehe Convert ToDouble 59, Siehe Convert ToInt32 Siehe Convert tokenize 459, 478 Toolbar Siehe Symbolleiste Toolbox 2, 3, 39 erweitern 1062, 1146 erweitern ActiveX 1147 ToolStrip Siehe Symbolleiste ToolStripContainer Siehe Symbolleiste ToolStripLabel 1126 ToolTip 1113 top level type visibility 944 TopLevelControl 1056 toString 306 ToString 125, 174, 956, 957, 966, 1008, Siehe Convert TR1-Erweiterungen 538 TRACE Makros 436 traits-Klassen 872 transform 891, Siehe STL Trapezregel 179, 562 TreeNode 1140 TreeView 1140 Trigram 460 Tröpfelverfahren 250 try finally 1026 try-Anweisung 369, 808 try-Block 808 und abhängige Anweisungen 825 TryParse 125, 174, 966, 1008 Tupel 547 Typangleichung 598 ganzzahlige 110 Type (Typinformation) 1071 IsClass 1072 IsInstanceOfType 1072 typeid 1071 type_info 344 typecast Siehe Konversion typedef 236, 342 Array 343 Array mit Funktionszeigern 559 Funktionstyp 559 Funktionszeiger 559 in einer Klasse 625 typeid 344, 794, 1071 mit polymorpher Klasse 795 typename 840 Typfeld 772 Typkonversion Siehe Konversion
1333 Typumwandlung, ganzzahlige 107
U überladene Funktion Siehe Funktion Funktions-Templates 851 überprüfbarer typsicherer Code 941 überschreibende Funktion 750 letzte 757 Übersetzungseinheit 437 Überwachte Ausdrücke 159 Uhrzeit 1151 UInt16 125, 1008 UInt32 125, 1008 UInt64 125, 1008 Umgebungsvariable 1226 Umkehriterator 902 UML 661 Umlaute 122 UML-Diagramm Elementfunktion 662 Klasse 661 Komposition 793 Vererbung 715, 791 unary_function 889 unbestimmter Wert einer Variablen 97 Unicode 123 union 266, 614 unique Siehe STL Unit-Tests 153 in VS 2008 155 NUnit 158 SimpleUnitTests 154 unmanaged code 940 unsigned char 99 unsigned int 99 unsigned long long 99 unsigned short int 99 upcast 798 upper_bound Siehe STL UserControl 1063 using-Anweisung 143, 414 using-Deklaration 143, 413, 720, 764
V valarray 536 Validating-Ereignis 1107 value 264 value class Siehe Werteklasse value_type 865
1334 Variablen 95 Beziehungen zwischen 196 globale 96, 354, 356 lokale 97, 127, 135, 354, 357 Name 93, 95, 351 unbestimmter Wert 97 VARIANT 268 vector 235, 463, 485, 861 Konstruktoren 465, 469 mehrdimensionaler 482 Speicherverwaltung 480 vector 482 Verallgemeinerung Siehe Vererbung Verbundanweisung 127, 353 verdeckte Funktion 736, 750 verdecktes Element 718 Vererbung 62, 713, 714, 727 bei C++/CLI-Klassen 1001 bei Formularen 726 C2DPunkt und C3DPunkt 722, 730, 753, 779 Diagramm 715 Generalisierung 727 ohne virtuelle Funktionen 782 private 739 protected 739 public 739 Quadrat und Rechteck 729 Spezialisierung 727 Verallgemeinerung 727 vs. Komposition 733 Verhaltensklasse 777 verifiably type-safe code 941 Verifikation Siehe Programmverifikation vertausche 193, 194, 326 verwalteter/nicht verwalteter Datentyp 949 Verweisklasse 783, 976 Verzeichnis durchsuchen 585 Verzeichnisse Application Klasse 1245 Directory Klasse 1242 DirectoryInfo Klasse 585, 1243 Environment Klasse 1244 virtual 744, 750, 763 virtual function table 759, 789 virtual table pointer 760, 789 virtuelle Basisklasse 744 virtuelle Funktion 749, 750, 769 bei C++/CLI-Klassen 1004 benanntes Überschreiben 1005
Index explizites Überschreiben 1004 implizites Überschreiben 1004 new 1004 rein virtuelle 776 Test 771 und Default-Argumente 764 und Erweiterbarkeit 771 und inline 763 Voraussetzungen 755 virtueller Destruktor Siehe Destruktor Visible 49 Visual C++ 2003 1096 visuelle Programmierung 4 void 53, 136 void* Siehe Zeiger volatile 233 vollständige Induktion 200 verkettete Liste 314, 322 Vorbedingung 195, 827 Vorwärtsdeklaration Klasse 626 Vorzeichenerweiterung 108 vptr Siehe virtual table pointer VScrollBar 1116 vtbl Siehe virtual function table
W wahlfreier Zugriff 494, 520 Wahrheitstabelle 220 Warteschlange 317 wchar_t 99, 123 wchar_t* 332 wcscat Siehe nullterminierter String wcscpy Siehe nullterminierter String wcslen Siehe nullterminierter String WebBrowser-Komponente 1294 WebClient 1295 Werkzeugleiste Siehe Symbolleiste Werteklasse 783, 1010, Siehe Klasse C++/CLI 1007 einfache 1013 Werteparameter Siehe Parameter Wertetabelle 560 Wertetyp (C++/CLI) 1007 while-Anweisung 132 white space 573 Wiederholungsanweisungen 131 Wiederverwendbarkeit 774 Win32 API-Funktionen 946 DLL 945 Handle .NET 1057
Index
1335
Win32-Anwendung 26 Window-Prozedur 1060 Windows-Zeichensatz 119 WinExec 950 WndProc 1060 Word steuern 1203 WordApplication 1205 write 501 wstring 449, 862
X XML-Dateien 1278 und Datenbanken 1278 XmlReader, XmlWriter 1238 XmlSerializer 1257 xor 402 ^ bitweise 112, 393 xor_eq 402
Z Zahlensystem zur Basis B 102 Zeichenliteral Siehe Literal Zeiger 271 ==,!=, = 279 Aliasing 276 als Parameter 326 auf Datenelement 790 auf eine Funktion 558 auf Elementfunktion 787 auf Zeiger 334 explizite Typkonversion 278 Fehlermöglichkeiten 287 konstante 301 Nullzeiger 277 void* 278 Zeigervariable 272
Zuweisung 276 Zeigerarithmetik 295, 468 Zeilenendkommentar // 348 Zeitaufwand 484 Zeitmessung, hochauflösende 1158 Zeitscheibe Siehe Thread Zufallszahlen rand() 144, 178 random (TR1) 548 Random .NET Klasse 145 srand() 145 Zugriffsrecht 627 Assembly-bezogen 944 aus der Basisklasse ändern 720 Basisklasse 715 class 627 Element Basisklasse 716 internal 945 private 627, 945 private protected 945 protected 945 public 627, 945 struct 627 top level visibility 944 Zuweisung 7, 396 Konversion 106 von Arrays 240 Zuweisungsoperator = Siehe operator Zweierkomplement 103 Zweigüberdeckung Siehe Test Zwischenablage (ClipBoard Klasse) 1229 zyklomatische Komplexität 216
Ԧ Ԧ xxiv, 91
C++-Schulungen Workshops – Beratung – Coaching Richard Kaiser führt seit über 20 Jahren Seminare über Programmiersprachen durch. Im Vordergrund stehen dabei Zusammenhänge und Sprachkonzepte. Der Lehrstoff wird durch Übungen ergänzt, in denen die Teilnehmer praxisnahe Programme entwickeln.
ŹMicrosoft Visual C++ 2005/2008 und .NET Fünf aufeinander abgestimmte Seminare: Behandelt wird der gesamte Sprachumfang des C++-Standards, C++/CLI und die .NET Bibliothek. 1. 2. 3. 4. 5.
Einführung in Visual Studio 2005/2008 C/C++ Grundlagen Objektorientierte Programmierung C++/CLI und die .NET-Klassenbibliothek Templates und die C++-Standardbibliothek
ŹC#, C++ und .NET mit Visual Studio 2005/2008 Die nächsten beiden Seminare stellen die Unterschiede von C# und C++ umfassend dar: C# für C/C++-Sprachumsteiger C/C++ für C#-Sprachumsteiger
ŹSystematische Tests und Programmverifikation Eine Einführung in das systematische Testen und die Programmverifikation: Systematische Tests und Unit-Tests mit Visual Studio 2008 Diese Seminare werden vor allem als Firmenseminare (inhouse) durchgeführt. Die Inhalte können an die Wünsche der Teilnehmer angepasst werden.
ŹWeitere Informationen: http://www.rkaiser.de/