This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Programmer’s Choice Von Profis für Profis Folgende Titel sind bereits erschienen: Bjarne Stroustrup Die C++-Programmiersprache 1072 Seiten, ISBN 3-8273-1660-X Elmar Warken Kylix – Delphi für Linux 1018 Seiten, ISBN 3-8273-1686-3 Don Box, Aaron Skonnard, John Lam Essential XML 320 Seiten, ISBN 3-8273-1769-X Elmar Warken Delphi 6 1334 Seiten, ISBN 3-8273-1773-8 Bruno Schienmann Kontinuierliches Anforderungsmanagement 392 Seiten, ISBN 3-8273-1787-8 Damian Conway Objektorientiertes Programmieren mit Perl 632 Seiten, ISBN 3-8273-1812-2 Ken Arnold, James Gosling, David Holmes Die Programmiersprache Java 628 Seiten, ISBN 3-8273-1821-1 Kent Beck, Martin Fowler Extreme Programming planen 152 Seiten, ISBN 3-8273-1832-7 Jens Hartwig PostgreSQL – professionell und praxisnah 456 Seiten, ISBN 3-8273-1860-2 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides Entwurfsmuster 480 Seiten, ISBN 3-8273-1862-9 Heinz-Gerd Raymans MySQL im Einsatz 618 Seiten, ISBN 3-8273-1887-4 Dusan Petkovic, Markus Brüderl Java in Datenbanksystemen 424 Seiten, ISBN 3-8273-1889-0 Joshua Bloch Effektiv Java programmieren 250 Seiten, ISBN 3-8273-1933-1
Frank Budszuhn
Visual C++ Windows-Programmierung mit den MFC
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
Einführung Aufbau der MFC Konventionen Zusammenfassung StockChart, ein Programm zur Anzeige von Aktienkursen Kleine Einführung ins Wertpapiergeschäft Vorstellung des Programms StockChart Die Dokument-Ansicht-Architektur Das Anwendungsgerüst Das Applikationsobjekt Das MVC-Modell Die Klassen des Anwendungsgerüsts Kommunikation zwischen den Klassen des Anwendungsgerüsts Serialisierung Nachrichtenverarbeitung Zusammenfassung Ein Programm mit dem Anwendungs-Assistenten erstellen Der Anwendungs-Assistent Die Applikationsklasse Das Hauptrahmenfenster Die Dokumentenklasse Die Ansichtsklasse Erste Anlaufpunkte im erzeugten Quelltext Zusammenfassung
Inhalt Werkzeugklassen und Dateizugriff Klassen für einfache Datentypen Klassen für Ansammlungen von Datentypen Klassen zum Dateizugriff Klassen zur Ausnahmebehandlung Verwendung der Werkzeugklassen in StockChart Zusammenfassung Grafikausgabe und Drucken GDI-Objekte und Gerätekontexte Abbildungsmodi Grafikausgabe mit der Dokument-Ansicht-Architektur Grafikausgabe in StockChart Tipps zur Vorgehensweise Drucken und Druckvorschau Zusammenfassung Dialogfeldprogrammierung Standarddialogfelder Modale Dialogfelder Nichtmodale Dialogfelder DDX und DDV Datenaustausch ohne DDX Dialogimplementierung im Programm StockChart Tipps zur Vorgehensweise Zusammenfassung Mehr zu Steuerelementen Das Programm WinControl Eigenschaftsdialogfelder Zusammenfassung HTML-basierte Dialogfelder Fehlersuche mit den MFC Makros zur Fehlersuche Tipps zur Vorgehensweise Zusammenfassung MFC und DLLs Vorteile von DLLs Reguläre MFC-DLLs MFC-Erweiterungs-DLLs StockChart mit DLL Tipps zur Vorgehensweise Zusammenfassung Programmierung mit Threads Aufbau und Funktionsweise von Threads Threads in den MFC Das Beispielprogramm Mandelbrot Die Programmierung mit Bitmaps Der Programmcode von Mandelbrot Kapselung des Arbeits-Threads durch die Dokumentenklasse Vorzeitiges Beenden des Arbeits-Threads
2.12.8 Tipps zur Vorgehensweise 2.12.9 Zusammenfassung 2.13 MFC-Zusammenfassung
3
COM, OLE und ActiveX
3.1 3.1.1 3.1.2 3.1.3 3.2 3.2.1 3.2.2
Einführung Motivation Begriffe Übersicht Das Komponentenobjektmodell Anforderungen an eine Softwarekomponente Lassen sich die Anforderungen an Softwarekomponenten mit der Sprache C++ realisieren? Eigenschaften des Komponentenobjektmodells COM Schnittstellen COM-Klassen GUIDs Die COM-Notation Die Schnittstelle IUnknown Klassenfabriken Die COM-Laufzeitbibliothek Verwendung eines COM-Servers Wie man einen COM-Server implementiert Das Beispielprogramm CppServer Registrierung von COM-Servern Bewertung der Implementierung des Programms CppServer Das Beispielprogramm CppServerNested Mehrfachvererbung versus eingebettete Klassen Implementierung eines COM-Servers mit den MFC Die Interface Definition Language Attributierte Programmierung Einbettung und Aggregation Ausführungsmodelle bei COM-Servern COM+ Tipps zur Vorgehensweise COM im Kontext anderer Windows-Technologien Zusammenfassung Automation Ein erstes Beispiel Die Schnittstelle IDispatch Implementierung eines Automationsservers mit den MFC Die Automationsimplementierung der MFC Automation mit dem Windows Scripting Host Implementierung eines Automations-Clients mit den MFC Das Beispielprogramm MFCAutoClient2 Registrierung von Typbibliotheken Duale Schnittstellen Tipps zur Vorgehensweise Zusammenfassung
Vereinheitlichter Datenaustausch Die Schnittstelle IDataObject Datenaustausch über die Zwischenablage Datenaustausch durch Drag&Drop Die Klassen der MFC für den vereinheitlichten Datenaustausch Vereinheitlichter Datenaustausch mit dem Programm StockChart Tipps zur Vorgehensweise Zusammenfassung Object Linking and Embedding Strukturierte Ablage Verbunddateien Die Schnittstelle ILockBytes Persistente COM-Objekte Die OLE-Schnittstellen Die MFC-Klassen zur OLE-Programmierung StockChart als OLE-Server ActiveX-Dokumente MFC-Unterstützung für ActiveX-Dokumente StockChart als ActiveX-Dokument Tipps zur Vorgehensweise Zusammenfassung ActiveX-Steuerelemente Die Anatomie von ActiveX-Steuerelementen Verwendung von ActiveX-Steuerelementen mit den MFC Ein ActiveX-Steuerelement im Programm StockChart Technische Grundlagen von ActiveX-Steuerelementen ActiveX-Steuerelemente mit den MFC erstellen ActiveX-Steuerelemente im Internet Tipps zur Vorgehensweise Zusammenfassung Zusammenfassung COM, OLE und ActiveX
Begriffe und Eigenschaften Datenbankschnittstellen Die Datenbank AKTIEN.MDB ODBC Die ODBC-Architektur Die MFC-Klassen zur ODBC-Programmierung Einrichtung von ODBC Das Beispielprogramm StockODBC Die vordefinierten Funktionen der Klasse CRecordView Das Beispielprogramm ODBCChart Weitere Möglichkeiten Tipps zur Vorgehensweise Zusammenfassung
DAO Die MFC-Klassen zur DAO-Programmierung Das Beispielprogramm StockDAO Das Beispielprogramm DAOChart Tipps zur Vorgehensweise Zusammenfassung OLE DB OLE DB und die MFC Die OLE DB-Nutzer-Template-Klassen OLE DB-Nutzer-Attribute Das Beispielprogramm StockOLEDB Das Beispielprogramm OLEDBChart Tipps zur Vorgehensweise Zusammenfassung Zusammenfassung zur Datenbankprogrammierung
Einführung Bereiche der Internetprogrammierung Internetbrowser im Eigenbau Die Klasse CHtmlView Das Beispielprogramm StockBrowser Tipps zur Vorgehensweise Weitere HTML-Klassen Zusammenfassung Programmierung mit WinInet Protokolle im Internet TCP/IP-Programmierung unter Windows HTTP Das Testen von Internetanwendungen Die MFC-Klassen der WinInet-Bibliothek Ablauf einer WinInet-Sitzung Das Beispielprogramm FileRobot Das Beispielprogramm FileRobot2 Tipps zur Vorgehensweise Zusammenfassung Programmierung von Server-Erweiterungen mit ISAPI MFC-Klassen für die ISAPI-Programmierung Die Server-Erweiterung GuestBook ISAPI-Deployment ISAPI-Filter Tipps zur Vorgehensweise Zusammenfassung Zusammenfassung Internetprogrammierung
Bücher Online-Dokumentation Technische Hinweise Artikel und Aufsätze in der Online-Hilfe Informationsquellen im Internet MFC FAQ Visual C++ developers journal MFC Programmer’s SourceBook Mailinglisten Usenet Gruppen
D
MFC-Programmierung im Schatten von .NET
673
Index
677
669 670 670 670 670 670 670 671 671 671
Vorwort Drei Jahre ist es her, seit Microsoft die letzte Version von Visual C++ herausgebracht hat. In der schnelllebigen Computerindustrie ist das ein sehr langer Zeitraum. Mittlerweile – so scheint es zumindest – hat sich der Fokus der Branche von der C++-Programmierung zu Java und anderen Sprachen verschoben, im Rampenlicht stehen die Application-Server zum Entwurf von Internetlösungen, weniger die Entwicklungsumgebungen zur Erstellung plattformspezifischer GUI-Anwendungen. Dementsprechend heißt die neueste Entwicklungumgebung von Microsoft zur Anwendungsprogrammierung Visual Studio .NET. Visual Studio .NET ist Teil der viel umfassenderen .NET-Initiative des Software-Herstellers, mit der eine neue Programmierplattform geschaffen werden soll, auf der unabhängig vom Betriebssystem Programme auch auf Geräten wie Mobiltelefonen und PDAs laufen können. .NET versteht Software als Dienstleistung, nicht mehr als einzelnes Programm. Natürlich ist .NET inhärent internettauglich. Bei aller Fokussierung auf das Internet sollte man jedoch nicht vergessen, dass große Teile der programmierenden Bevölkerung weiterhin plattformspezifische GUI-Programme entwickeln, meist also Windows-Anwendungen. Aus Gründen der Performance werden diese Programme auch heute gerne mit der Programmiersprache C++ erstellt, statt dafür (teil-)interpretierte Sprachen wie Java oder C# zu verwenden. Die MFC sind nach wie vor die Standardklassenbibliothek, um native, also direkt für den Prozessor übersetzte, Programme für Windows zu erstellen. Entgegen allen Befürchtungen sind die MFC von Microsoft nicht links liegen gelassen worden, sondern seit der letzten Version deutlich weiterentwickelt worden. So wurden die Möglichkeiten zur Nutzung des Internets aus MFC-Anwendungen verbessert, die Möglichkeit geschaffen, MFC-Programme mit einer HTML-Oberfläche zu versehen, sowie die Integration mit der vormals eher rivalisierenden Klassenbibliothek ATL verbessert. So teilen sich MFC und ATL in
12
Vorwort
den neuesten Versionen der Bibliotheken beispielsweise die String-Implementierung. Der Zugriff auf von .NET und WebDienste ist von MFC und ATL aus möglich. Mit der Version 7.0, die die MFC mittlerweile erreicht haben, schreitet die Weiterentwicklung der Klassenbiblitothek langsamer voran, als bei den frühen Versionen. Man sollte dies eher als ein Zeichen von Reife werten, als zu schlussfolgern, dass Klassenbibliotheken wie die MFC mittlerweile überflüssig wären. Auch wenn die MFC jetzt im Schatten von .NET stehen, sind sie weiterhin ein robustes und erprobtes Werkzeug, um Programme direkt für das Windows-API zu entwickeln. Das vorliegende Buch beschreibt die verschiedenen Teilaspekte der Software-Entwicklung mit den Microsoft Foundation Classes. Der Autor steht für Anregungen, Kritik und Fragen per E-Mail gerne zur Verfügung. Frank Budszuhn, [email protected]
1 Einleitung 1.1
Zielgruppe dieses Buchs
Gleich zu Beginn soll gesagt werden, was dieses Buch nicht ist: Dieses Buch ist keine Beschreibung der neuen .NET-Technologie von Microsoft! NET-Applikationen bauen auf einer grundsätzlich neuen Systemarchitektur auf, die es ermöglicht, .NET-Applikationen – zumindest prinzipiell – vom darunter liegenden Betriebssystem zu trennen. .NET-Applikationen sind keine WindowsProgramme im eigentlichen Sinne mehr. Es sind vielmehr Programme, die in einer virtuellen Maschine laufen. Diese virtuelle Maschine kann auch für ein anderes Betriebssystem oder für Geräte wie Handys und Organizer implementiert werden. Für den Programmcode, der für diese Umgebung erstellt wird, hat Microsoft einen Namen gefunden: Managed Code. Dieses Buch behandelt im Gegenzug die Programmierung von Unmanaged Code, die auch weiterhin möglich ist. Unmanaged Code ist Programmcode, der direkt für einen Prozessor und ein Betriebssystem programmiert wird, in unserem Fall für einen Intel-kompatiblen Prozessor und das Betriebssytem Windows. Unmanaged Code ist die Sorte Programmcode, den Windows-Programmierer schon immer geschrieben haben. Eine kurze Gegenüberstellung von den MFC und .NET ist in Anhang D, »MFC-Programmierung im Schatten von .NET« zu finden. Das vorliegende Buch behandelt die wichtigsten Windows-Technologien aus der Sicht eines C++-Programmierers. Die Klassenbibliothek MFC (Microsoft Foundation Classes) wird als Grundlage der Programmentwicklung verwendet. Auf der Basis der besprochenen Technologien wird die Programmierung moderner Softwarekomponenten, Datenbank-, Internet- und Multimediaanwendungen vorgestellt. Da dieses Buch die kompakte Darstellung eines weitreichenden Themenkreises ist, müssen gewisse Vorkenntnisse beim Leser vorausgesetzt werden.
14
1
Einleitung
Welche Voraussetzungen sollte der Leser mitbringen? Der Leser sollte die Programmiersprache C++ flüssig »sprechen«, da alle Beispiele diese Programmiersprache verwenden. Auf die Eigenheiten der Sprache wird nur am Rande eingegangen. Daneben ist Programmiererfahrung unter Windows hilfreich, unabhängig davon, mit welcher Sprache oder Entwicklungsumgebung sie erworben wurde. Wer schon einmal unter Windows programmiert hat, kennt bereits grundlegende Konzepte und Begriffe, wie zum Beispiel Windows-Nachrichten, Ressourcen und Handles. In einigen Kapiteln dieses Buchs sind spezielle Vorkenntnisse von Vorteil. So wird zur Beschreibung von COM in Kapitel 3, »COM, OLE und ActiveX«, eine Reihe von Begriffen der Objektorientierung verwendet. Wenn man die grundlegenden Konzepte der Objektorientierung kennt, ist das Kapitel einfacher zu verstehen. In Kapitel 4, »Datenbankprogrammierung«, werden die Begriffe relationaler Datenbanken in knapper Form erklärt, was jedoch keine Einführung in die Datenbanktheorie ersetzen kann. Kapitel 5, »Internetprogrammierung«, verwendet Beispiele mit HTMLDateien, ohne jedoch eine Einführung in HTML zu geben. Dieses Buch beschreibt nicht den grundlegenden Umgang mit den Werkzeugen der Entwicklungsumgebung von Visual C++, wie Debugger, Editor, Projektverwaltung usw. Der Umgang mit diesen Werkzeugen ist relativ leicht und intuitiv erlernbar. Das Buch konzentriert sich auf die Programmierung und die Besprechung der verwendeten Quelltexte. Im Zusammenhang mit den Beispielen wird auf die Eigenheiten der Entwicklungsumgebung eingegangen. So werden beispielsweise die Assistenten zur Erzeugung und Erweiterung von Programmcode ausführlich besprochen.
1.2
Windows-Programmierung und die MFC
Fast alle Beispiele dieses Buchs sind mit Hilfe der MFC (Microsoft Foundation Classes) entwickelt worden. Es gibt mehrere Gründe für die Verwendung dieser Klassenbibliothek: 왘 Die Verwendung einer Klassenbibliothek wie die MFC führt schnell zu lauffähigen Programmen. Dies ist wichtig bei den Beispielprogrammen eines Buchs, die nur eine beschränkte Größe haben können. Die Grundfunktionalität eines WindowsProgramms wird von den MFC bereitgestellt und liegt für den Programmierer größtenteils unsichtbar innerhalb der Klassenbibliothek versteckt.
Aufbau des Buchs
왘 Die MFC sind eine Standardklassenbibliothek für die Programmierung unter Windows. Die Assistenten der Entwicklungsumgebung bieten weitreichende Unterstützung bei der Anlage und Erweiterung von MFC-Programmen. Erst durch diese Assistenten wird diese Entwicklungsumgebung »visual«. 왘 Die Verwendung der MFC spart Entwicklungszeit. Für viele Probleme gibt es vorgefertigte Lösungen. Man muss das Rad nicht dauernd neu erfinden. 왘 Im Gegensatz zum neuen .NET Framework werden MFC-Programme direkt in Intel-Maschinencode übersetzt und sind damit deutlich performanter als die auf der CLR (Common Language Runtime, vgl. Anhang D) ablaufenden .NET-Programme. Die direkte Programmierung des WIN32-API ist nicht Thema dieses Buchs. Die WIN32-Programmierung kann Geschwindigkeitsvorteile mit sich bringen, ist aber fast immer aufwändiger als die Programmierung mit den MFC. Ebenso ist die Programmierung des .NET Frameworks nicht Gegenstand dieses Buchs.
1.3
Aufbau des Buchs
Das Buch gliedert sich – von dieser Einleitung und vom Anhang abgesehen – in vier Kapitel: 왘 Einstieg in die MFC-Programmierung 왘 COM, OLE und ActiveX 왘 Internetprogrammierung 왘 Datenbankprogrammierung Da nahezu alle in diesem Buch behandelten Beispiele auf den Microsoft Foundation Classes (MFC) basieren, ist ein Verständnis dieser Klassenbibliothek unbedingt notwendig. Das Kapitel 2, »Einstieg in die MFC-Programmierung«, bildet die Grundlage für alle anderen Kapitel des Buchs. Kapitel 2 sollte vom Leser also nur übersprungen werden, wenn er bereits gute Kenntnisse der MFC besitzt. Alle weiteren Kapitel lassen sich unabhängig voneinander durcharbeiten. Wer sich beispielsweise nur mit der Datenbankprogrammierung beschäftigen möchte, der muss vorher nicht das Kapitel zur Internetprogrammierung gelesen haben. Daher ist auch die Reihenfolge, in der man die letzten vier Kapitel durch-
15
16
1
Einleitung
liest, beliebig. Sollte es Querverbindungen zwischen den Kapiteln geben, so wird darauf ausdrücklich hingewiesen. Im Anhang des Buchs befindet sich ein Glossar, welches die wichtigsten Begriffe der in diesem Buch vorgestellten Technologien erklärt. Außerdem wird die Versionsgeschichte der MFC aufgezeigt. Ein Literaturverzeichnis stellt weiterführende Bücher und Quellen aus der Online-Hilfe und dem Internet vor. Anhang D stellt schließlich die neue Microsoft.NET-Architektur den MFC gegenüber.
1.4
Zu den Programmbeispielen
Alle Beispielprogramme in diesem Buch sind auf einer Begleit-CD enthalten. Die Verzeichnisstruktur der CD orientiert sich an den Kapiteln des Buchs. Abbildung 1.1 zeigt die Verzeichnisse der Begleit-CD.
Abbildung 1.1: Die Verzeichnisstruktur der Begleit-CD
Am besten ist es, die Beispielprogramme mit dem auf der CD vorhandenen Installationsprogramm auf die Festplatte zu kopieren. Das Installationsprogramm hebt automatisch den Schreibschutz der Dateien auf, der immer gesetzt ist, wenn man Dateien von einer CDROM kopiert. Das Installationsprogramm heißt SETUP.EXE und befindet sich im Hauptverzeichnis der CD. Möchte man die Beispiele einzeln installieren, so kann man sie mit Hilfe des Windows-Explorer auf die Festplatte kopieren. Die so kopierten Dateien sind schreibgeschützt. Dies kann zu Problemen
Das Beispielprogramm StockChart
17
führen. Daher muss das Schreibschutzattribut aller kopierten Dateien mit dem Windows-Explorer zurückgesetzt werden (unter DATEI | EIGENSCHAFTEN). Das Verzeichnis DATEN enthält Beispieldateien für das in Kapitel 2, »Einstieg in die MFC-Programmierung«, entwickelte Programm StockChart. Der Ordner DB enthält eine Access-Datenbank, die in Kapitel 4, »Datenbankprogrammierung«, verwendet wird. Die Assistenten der Visual C++-Entwicklungsumgebung kommentieren den von ihnen erzeugten Programmcode. Die automatisch generierten Kommentare zeigen, an welchen Stellen Ergänzungen vom Programmierer vorgenommen werden können und welche Aufgabe der generierte Programmcode hat. Die deutsche Version von Visual C++ erzeugt eine bunte Mischung aus deutsch- und englischsprachigen Kommentaren. Die sprachliche Qualität – insbesondere der deutschsprachigen Kommentare – ist manchmal relativ schlecht. Trotzdem wurden diese Kommentare sprachlich unverändert in die Listings dieses Buchs übernommen. Zum einen geben sie trotz ihrer sprachlichen Mängel wertvolle inhaltliche Hinweise, zum anderen treten diese Kommentare später in eigenen Projekten auch auf.
Programmkommentare
Für Klassen-, Variablen- und Konstantennamen werden englische Bezeichner verwendet. Dies ist gängige Praxis in vielen Firmen. Finden sich in einigen Listings trotzdem deutschsprachige Bezeichner, dann wurden diese automatisch von einem der Assistenten erzeugt (beispielsweise aufgrund der deutschsprachigen Tabellenfelder in der Beispieldatenbank in Kapitel 4).
Englische Bezeichner
1.5
Das Beispielprogramm StockChart
Ein Beispielprogramm zieht sich wie ein roter Faden durch das gesamte Buch, das Programm StockChart. Abbildung 1.2 zeigt StockChart bei der Ausführung. StockChart ist ein Programm zum Anzeigen von Aktienkursdiagrammen, so genannten Charts. StockChart speichert diese Kursdiagramme in seinem eigenen Dateiformat und importiert Aktienkurse aus ASCII-Dateien. Natürlich erreicht das Programm nicht den Standard kommerzieller oder auf dem Sharewaremarkt erhältlicher Chartprogramme. Im Zuge der für Beispielprogramme gebotenen Beschränkung auf das Wesentliche sind eine Reihe von Verein-
18
1
Einleitung
fachungen vorgenommen worden, die eine praktische Verwendung des Programms ausschließen. Auf der anderen Seite weist das Programm eine gewisse Komplexität auf, die über das von manchen Beispielprogrammen praktizierte Zeichnen von Vierecken und Kreisen hinausgeht.
Abbildung 1.2: Das Beispielprogramm StockChart
Natürlich lässt sich der in diesem Buch vorgestellte Themenbereich nicht anhand eines Beispiels erklären. Daher finden auch andere Beispielprogramme Verwendung.
2 Einstieg in die MFC-Programmierung Das vorliegende Kapitel führt in die Programmierung mit den Microsoft Foundation Classes (MFC) ein. Die vermittelten Programmierkenntnisse bilden die Grundlage für die weiteren Kapitel dieses Buchs.
2.1
Einführung
Viele Windows-Programme werden heute auf der Grundlage der Microsoft Foundation Classes entwickelt, anstatt das Windows Application Programming Interface (API, die Programmierschnittstelle) direkt zu verwenden. Warum ist das so? In den letzten Jahren wurde die prozedurale Programmiersprache C, auf der das Windows-API basiert, zunehmend durch die neuere, objektorientierte Programmiersprache C++ verdrängt. Dieser Wechsel hängt mit dem Übergang von prozeduralen, strukturierten Programmiertechniken hin zur objektorientierten Softwareentwicklung zusammen. Da C++ eine Obermenge der Sprache C darstellt (Es gibt einige wenige Unterschiede im gemeinsamen Sprachschatz von C und C++. Diese Unterschiede können aber normalerweise vernachlässigt werden), kann auch diese Sprache zur Programmierung des Windows-API verwendet werden. Der C++-Programmierer wird jedoch durch die Strukturen des Windows-API weiterhin zu einer prozeduralen Arbeitsweise gezwungen, die sich deutlich von der objektorientierten Herangehensweise unterscheidet. Während bei der prozeduralen Programmierung ein Programm aus einer Folge von Prozedur- oder Funktionsaufrufen zusammengesetzt ist, besteht das Programm bei der objektorientierten Programmierung aus einer Ansammlung von Objekten, die miteinander durch Funktionsaufrufe kommunizieren (in der Objektorientierung werden Funktionen der Klassen auch als Methoden bezeichnet). Um das Windows-API
Vorteile der MFCProgrammierung
20
2
Einstieg in die MFC-Programmierung
objektorientiert ansprechen zu können, muss man es auf eine Reihe von C++-Klassen abbilden. Ein objektorientiertes Modell des Windows-API muss erstellt werden. Dies ist eine sehr aufwändige und anspruchsvolle Aufgabe. Versionen der MFC
Microsoft hat diesen Umstand frühzeitig erkannt und die Microsoft Foundation Classes entwickelt. Version 1.0 der MFC kam im Jahre 1992 zusammen mit dem MSC-7.0-Compiler auf den Markt. MFC 1.0 führte C++-Klassen für Windows-Objekte wie Fenster, Steuerelemente und Dialoge ein. Außerdem enthielt die erste Version der MFC eine Reihe von Werkzeugklassen, wie beispielsweise Klassen für Strings, Ansammlungen von Objekten (Arrays, Listen und Maps), Dateien und Ausnahmen (Exceptions). Die wichtigste Neuerung der MFC Version 2.0 war die Dokument-Ansicht-Architektur. Damit wurde aus der MFC weit mehr als nur eine Kapselung der Windows-Programmierschnittstelle. Sie stellte jetzt ein Gerüst für eigene Anwendungen bereit, das dem Programmierer eine Struktur vorgab, die an bestimmten Stellen mit Funktionalität zu ergänzen war. Spätere Versionen der MFC führten unter anderem OLE-Unterstützung, Klassen zum Datenbankzugriff und Internetfunktionen ein. Die derzeit aktuelle Version ist MFC 7.0 und wird zusammen mit Visual Studio .NET ausgeliefert.
Anforderungen an die MFC
Microsoft hat, als die MFC entworfen wurden, eine Reihe von Anforderungen an die künftige Klassenbibliothek gestellt: 왘 Mit MFC erstellte Programme sollen weder wesentlich größer noch wesentlich langsamer sein als Programme, die in der Sprache C geschrieben sind. 왘 Die Programmentwicklung soll einfacher sein als mit dem Windows-API. 왘 Neue Windows-Technologien sollen ihre objektorientierte Umsetzung in der Klassenbibliothek finden. 왘 Der Programmierer soll bereits vorhandenes Wissen über die Programmierung des Windows-API weiter nutzen können. Diese Ziele können als erreicht gelten. Zwar sind MFC-Programme etwas größer als ihre C-Pendants, doch fällt der Unterschied im Kontext heutiger Hauptspeicher- und Festplattengrößen nicht mehr ins Gewicht. Dies trifft insbesondere zu, wenn man den Speicherverbrauch einer MFC-Anwendung mit dem Speicherverbrauch einer .NET-Anwendung vergleicht. Hier sieht man
Einführung
21
deutlich, dass es sich bei MFC-Programmen um native (speziell für Intelprozessoren kompilierter Maschinencode) Windows-Programme handelt, während die neue .NET-Plattform prinzipiell prozessor- und betriebssystemunabhängig ist. Zu einem ausführlicheren Vergleich zwischen MFC- und .NET-Programmierung siehe Anhang D, »MFC-Programmierung im Schatten von .NET«. Die Programmierung mit den Microsoft Foundation Classes ist einfacher als die Programmierung des API. Der Programmierer kann dabei selbst zwischen verschiedenen Ebenen der Programmierung wählen. Zunächst hält ihn nichts davon ab, in einem MFC-Programm Funktionen des Windows-API aufzurufen. Dies ist die unterste Ebene der Programmierung. In diesem Fall wird die Funktionalität der MFC nicht genutzt. Bei bestimmten, laufzeitkritischen Anwendungen ist es manchmal sogar wünschenswert, bestimmte Teile der MFC zu umgehen, da deren Laufzeitverhalten sich ungünstig auswirken kann. Als zweite Ebene der Programmierung bieten die MFC eine Reihe von Klassen zur Kapselung von Windows-Objekten, wie Fenstern, Dialogen, Steuerelementen oder grafischen Objekten, an. Beispielsweise dient die Klasse CWnd dazu, Fenster aller Art zu repräsentieren. Viele Details der Windows-Programmierung, wie zum Beispiel der Umgang mit Handles oder die Verwaltung der Nachrichtenschleife, werden dem Programmierer abgenommen. Zwar hält ein Objekt der Klasse CWnd intern ein Handle auf das Fenster, das es repräsentiert, doch muss sich der MFC-Programmierer darum normalerweise nicht kümmern. Windows-Nachrichten werden durch ein sehr leistungsfähiges Nachrichtensystem innerhalb der MFC weitergeleitet und behandelt. Der MFC-Programmierer bekommt weder WinMain noch die Fensterfunktion eines Fensters zu sehen, das er benutzt. Die Namensgebung der Funktionen der MFC-Klassen ist stark an entsprechende Namen aus dem Windows-API angelehnt. So kann sich ein erfahrener Windows-Programmierer viel schneller in die MFC einarbeiten, da er keine neue Namen für bereits vertraute Funktionen lernen muss. Er kann sein Vorwissen innerhalb der MFC Gewinn bringend nutzen. Auf der obersten Ebene der Programmierung bieten die MFC die Dokument-Ansicht-Architektur an. Diese wird im Folgenden als Anwendungsgerüst bezeichnet. Es stellt dem Programmierer ein komplettes Programmgerüst zur Verfügung, das grundlegende Funktionen einer Windows-Anwendung enthält. Zu diesen Funk-
Ebenen bei der MFC-Programmierung
22
2
Einstieg in die MFC-Programmierung
tionen zählen beispielsweise Dokumentenverwaltung, Drucken und Druckvorschau sowie die Trennung zwischen Daten (Dokumenten) und deren Darstellungen (Ansichten). Zusätzlich wird eine leistungsfähige und relativ einfach zu handhabende Unterstützung für Technologien wie OLE, Automation und ActiveX bereitgestellt. Abbildung 2.1 stellt die drei Ebenen der Programmierung mit den MFC dar. Es besteht jederzeit die Möglichkeit, an den MFC-Klassen »vorbeizuprogrammieren«, indem Funktionen des Windows-API aufgerufen werden. In den MFC nicht vorhandene Funktionalität kann durch die Verwendung des Windows-API ergänzt werden.
Anwendungsgerüst MFC API-Kapselung
Windows-API
Abbildung 2.1: Mögliche Ebenen bei der MFC-Programmierung Assistenten
MFC-Programme werden meistens durch den AnwendungsAssistenten erzeugt. Dieser wird automatisch aufgerufen, wenn in Visual Studio .NET ein neues MFC-Projekt angelegt wird. Der Anwendungs-Assitent erzeugt alle notwendigen ProgrammcodeDateien für ein MFC-Programm, das auf der Dokument-AnsichtArchitektur basiert. Dabei können auf mehreren Karteikarten Optionen des zu erstellenden Programmgerüsts bestimmt werden. Der in früheren Versionen ebenfalls vorhandene KlassenAssistent ist mittlerweile durch eine Reihe kleinerer Assistenten ersetzt worden, die automatisch gestartet werden, wenn sie benötigt werden. Solche Assistenten legen beispielsweise COM- oder Datenbankklassen an.
Portabilität
Die Benutzung der MFC gewährleistet zusätzlich eine gewisse Portabilität der Programme. Beim Übergang von der 16-Bit-Version von Windows (WIN16) zur 32-Bit-Version (WIN32) konnten MFC-Programme oft durch einfache Neukompilierung portiert werden. Bei Programmen, die direkt für das Windows-API
Einführung
23
geschrieben worden sind, war dies oft nicht so einfach. Neben diesen MFC-Versionen gibt es eine MFC-Version für »Windows CE«. Von Drittanbietern werden UNIX-Portierungen der MFC angeboten. Diese Portierungen verwenden die Motif-Oberfläche. Microsoft hat stets darauf geachtet, die MFC kompatibel mit früheren Versionen zu halten. Auch wenn sich die Implementierung an manchen Stellen geändert hat, ist das Verhalten der MFC-Klassen über die Versionen hinweg gleich geblieben. Dies ist eine wichtige Voraussetzung dafür, dass die Entwickler die MFC einsetzen. Schließlich möchte kein Programmierer sein Programm ändern müssen, weil sich die zugrunde liegende Klassenbibliothek geändert hat.
2.1.1
Versionskompatibilität
Aufbau der MFC
Die MFC sind eine Klassenbibliothek von respektabler Größe. In der Version 7.0 besteht sie aus mehr als 200 Klassen. Möchte man sich einen Überblick verschaffen, so sollte man zunächst einige wenige Designprinzipien der MFC kennen. Die MFC sind nicht nur eine Klassenbibliothek, seit der Version 2.0 sind sie zudem auch ein Application Framework. Ein Framework ist eine Klassenbibliothek, die ein abstraktes Design für eine Familie verwandter Probleme bereitstellt. Ein Application Framework liefert somit ein vorgefertigtes Design für ein Anwendungsprogramm. Typisch für ein Framework ist die Umkehrung des Kontrollflusses, das heißt, nicht der Programmierer ruft Funktionen des Frameworks auf, sondern er stellt Funktionen bereit, die dann durch das Framework aufgerufen werden! Meist leitet der Programmierer bei der Verwendung eines Frameworks eigene Klassen von abstrakten Klassen des Frameworks ab und überschreibt ausgesuchte Funktionen dieser Klassen. Diese Funktionen werden dann vom Framework aufgerufen. Die Dokument-Ansicht-Architektur der MFC ist ein typisches Framework. Frameworks können sehr viel Arbeit sparen, aber auch zu unflexiblen Anwendungsprogrammen führen, wenn sie das Anwendungsmodell zu sehr festschreiben. Wie viele andere Frameworks leiten die MFC den überwiegenden Teil ihrer Klassen von einer abstrakten Basisklasse ab. In den MFC heißt diese Klasse CObject. Mehrfachvererbung wird in den MFC nicht verwendet. Daraus ergibt sich eine baumartige Struktur für den Aufbau der Klassenbibliothek. Abbildung 2.2 zeigt einen Ausschnitt aus der MFC-Baumstruktur.
Application Framework
24
2
Einstieg in die MFC-Programmierung
CObject
CCmdTarget
CWinThread
CFile
CArray
...
...
CWnd
...
...
...
...
Abbildung 2.2: Baumstruktur der MFC
Microsoft favorisiert allerdings eine etwas andere Darstellungsform für die Klassendiagramme der MFC. Diese Form soll auch in diesem Buch verwendet werden. So zeigt Abbildung 2.3 den gleichen Ausschnitt der MFC wie Abbildung 2.2, verwendet jedoch die Microsoft-Notation. In dieser Notation lassen sich die Klassendiagramme deutlich komprimierter darstellen.
Was sind die Vorteile der von den MFC-Designern gewählten Struktur? Der Verzicht auf Mehrfachvererbung ist eine grundlegende Entscheidung. Sie wirkt sich weitgehend auf die Struktur der resultierenden Klassenbibliothek aus. Die Klassenbibliothek wird übersichtlicher, ist einfacher zu verstehen und die mit ihr erstellten Programme sind teilweise effizienter.
25 Struktur der MFC
Die Baumstruktur der MFC ist eine direkte Folge des Verzichts auf Mehrfachvererbung. Es wurde eine Struktur implementiert, die zum ersten Mal in den Klassenbibliotheken der Sprache Smalltalk entwickelt wurde und sich seitdem in der Praxis immer wieder bewährt hat: CObject ist die Basisklasse für die weiteren Klassen der MFC. Somit ist es möglich, in CObject allgemeine Dienstleistungen zu implementieren, die von den weiteren Klassen der MFC und auch in eigenen, davon abgeleiteten Klassen, verwendet werden können. CObject implementiert beispielsweise die Serialisierung von Objekten (Sichern des Speicherabbildes eines Objekts in einer Datei), dynamische Objekterzeugung und Diagnosefunktionen. In allen von CObject abgeleiteten Klassen stehen diese Dienstleistungen automatisch zur Verfügung. Wer sich die Klassenübersicht der MFC in der Online-Hilfe anschaut, wird allerdings feststellen, dass eine kleinere Anzahl von MFC-Klassen nicht von CObject abgeleitet worden ist. Es gibt zwei Gründe dafür, Klassen nicht von CObject abzuleiten. Entweder werden die Dienstleistungen von CObject schlichtweg nicht benötigt (wie beispielsweise bei den Klassen des Internet-ServerAPI) oder die daraus resultierenden Objekte würden unangemessen groß und damit ineffizient zu handhaben sein (zum Beispiel CPoint oder CString). Speicher- und Laufzeiteffizienz sind wichtige Designmerkmale der MFC.
Klassen der MFC
Nachdem man weiß, welches grundlegende Design den MFC zugrunde liegt, kann man versuchen, logische Teilbereiche ausfindig zu machen. Es gibt hier keine endgültige Einteilung vonseiten Microsofts, auch treten einige Überschneidungen auf. Grob lassen sich jedoch die in Tabelle 2.1 dargestellten logischen Teilbereiche ausmachen:
Teilbereiche der MFC
26
2
Einstieg in die MFC-Programmierung
Bereich
Klassen
Verwendungszweck
Klassen des Anwendungsgerüsts
CCmdTarget-Zweig, Ansichtsklassen
Anwendungsgerüst, Anwendungsassistent
API-Kapselung
CWnd-Zweig (Fenster, Dialoge, Steuerelemente), GDI-Klassen, diverse andere
Es gibt eine Reihe von Klassen, die sich nicht so recht in dieses Schema integrieren lassen. Sie lassen sich eventuell zwei oder mehr Teilbereichen zuordnen. Dieses Kapitel wird sich mit den ersten drei Bereichen der Tabelle 2.1 beschäftigen. Diese Klassen werden zur Erstellung von »normalen« Windows-Anwendungen benötigt. In den weiterführenden Kapiteln dieses Buchs werden immer wieder Klassen aus dem vierten Bereich der Tabelle auftauchen.
2.1.2 Ungarische Notation
Konventionen
Genau wie bei der Windows-API-Programmierung wird auch bei der MFC-Programmierung die ungarische Notation zur Benennung von Variablen verwendet. Selbst wenn man diese nicht in eigenen Programmen verwenden möchte, so sollte man wenigstens wissen, was es damit auf sich hat. Schließlich wird die ungarische Notation durchgängig in den MFC selbst, in MFC-Beispielen und in den von Anwendungsassistenten erzeugten Quelltexten verwendet. Bei der ungarischen Notation wird jeder Variablen ein Buchstabenkürzel vorangestellt, das ihren Typ codiert. So ist nCount durch den Buchstaben n sofort als Integer-Variable erkennbar, lpszName ist ein Zeiger auf eine NULL-terminierte Zeichenkette. Die Tabelle 2.2 zeigt die wichtigsten verwendeten Typkürzel.
Einführung
27
Päfix
Typ
b
Boolesche Variable, entweder TRUE oder FALSE
c
Zeichen vom Typ char
dw
Double Word, 32-Bit-Wert ohne Vorzeichen
h
Handle, identifiziert eine Windows-Ressource
lpsz
Zeiger auf eine NULL-terminierte Zeichenkette (einen C-String)
n
Number, eine Integer-Zahl
p
Pointer, Zeiger
w
Word, 16-Bit-Wert ohne Vorzeichen
Tabelle 2.2: Typkürzel der ungarischen Notation
Die ungarische Notation hat ihren Ursprung in der Zeit, als alle Windows-Programme für WIN16, die 16 Bit breite WindowsSchnittstelle, entwickelt wurden. Damals musste fast an jeder Stelle zwischen 16 Bit breiten und 32 Bit breiten Werten und Zeigern unterschieden werden. Die entsprechenden Kürzel (lp = long pointer, np = near pointer) gibt es aus Gründen der Rückwärtskompatibilität heute noch. Unter WIN32 ist die Unterscheidung jedoch bedeutungslos. Obwohl die Präfixe lp und np auch in 32Bit-Programmen noch verwendet werden können, sind alle Zeiger 32 Bit breit. Zusätzlich zur bereits bei der Windows-API-Programmierung verwendeten ungarischen Notation führen die MFC weitere Konventionen ein: 왘 Klassennamen beginnen mit einem großen C: CObject. 왘 Member-Variablen verwenden das Präfix m_: m_lpszName. 왘 Funktionsnamen beginnen mit einem Großbuchstaben. Außerdem beginnt bei einem Funktionsnamen jedes neue Teilwort mit einem Großbuchstaben. Üblicherweise ist das erste Teilwort ein Verb, die folgenden Teilworte sind oft Substantive: GetWindowHandle.
2.1.3
Zusammenfassung
Die MFC haben viele Aufgaben. Zunächst sind sie eine objektorientierte Schnittstelle zum Windows-API für C++-Programmierer. Darüber hinaus stellen sie einen vollständigen Rahmen für Anwendungsprogramme dar, der als Anwendungsgerüst bezeich-
MFC-Konventionen
28
2
Einstieg in die MFC-Programmierung
net wird. Nebenbei tragen die MFC dazu bei, Programme, zumindest zwischen verschiedenen Windows-Versionen, portabel zu halten und komplexe Windows-Technologien wie ActiveX oder OLE einfach verwenden zu können.
2.2
StockChart, ein Programm zur Anzeige von Aktienkursen
In den folgenden Abschnitten wird das Programm StockChart beschrieben, an dem sich die grundlegende Programmierung mit den MFC zeigen lässt.
2.2.1 Aktienkurven
Kleine Einführung ins Wertpapiergeschäft
Das Beispielprogramm StockChart zeigt den Kursverlauf von Aktien an. Ein Aktienkursdiagramm oder -chart, wie es vom Programm StockChart angezeigt wird, stellt den Kurs einer Aktie in Abhängigkeit von der Zeit dar.
Abbildung 2.4: Typische Aktienkurve
StockChart, ein Programm zur Anzeige von Aktienkursen
29
Die horizontale Achse (x-Achse) repräsentiert die Zeit, während auf der vertikalen (y-Achse) der Wert aufgetragen wird. Die Einheit für die Zeitdarstellung ist vom Verwendungszweck abhängig. Sie kann beispielsweise in Minuten, Tagen oder Monaten angegeben werden. Bei der vertikalen Achse ist sowohl eine lineare als auch eine logarithmische Darstellung möglich. Warum wird die Darstellung von Aktienkursen als Beispiel verwendet? Das Thema Geldanlage mit Aktien ist in Deutschland in letzter Zeit in das Interesse der Öffentlichkeit gerückt. Außerdem ist das Thema leicht verständlich. Aus Sicht einer MFC-Einführung sind insbesondere folgende Punkte interessant: 왘 Eine Aktienkurve ist einfach zu zeichnen. 왘 Die Datenstruktur zur Repräsentation der Aktienkurve ist einfach. Es handelt sich um eine einfache Reihe von Werten. 왘 Das Beispiel wird in den weiterführenden Kapiteln als Grundgerüst für viele Beispiele wiederverwendet. Bevor das Programm StockChart vorgestellt wird, sind noch einige wenige Börsenbegriffe zu klären. Um eine Aktie (oder auch jedes andere handelbare Wertpapier) eindeutig bestimmen zu können, wird ihr in Deutschland eine so genannte Wertpapierkennnummer, abgekürzt WKN, zugeordnet. Die WKN ist eine sechsstellige Zahl, die bei Aktienkäufen oder -verkäufen angegeben werden muss. In Amerika ist es stattdessen üblich, die Aktie durch eine kurze Buchstabenfolge zu identifizieren. Da diese so genannten Tickerkürzel einprägsamer sind als die Wertpapierkennnummern, werden sie auch hierzulande zunehmend verwendet. Tabelle 2.3 zeigt die Wertpapierkennnummern und Tickerkürzel einiger in Deutschland gehandelter Aktien. Firma
WKN
Ticker
Coca Cola
850663
CCC3
Deutsche Bank
514000
DBK
Intel
855681
INL
Microsoft
870747
MSF
SAP
716460
SAP
Siemens
723610
SIE
Tabelle 2.3: Wertpapierkennnummern und Tickerkürzel einiger Aktien
Begriffe
30
2
2.2.2
Einstieg in die MFC-Programmierung
Vorstellung des Programms StockChart
Das Programm StockChart zeichnet alle Kurven in linearer Skalierung. Auf eine Beschriftung der Achsen wird verzichtet, um den Programmcode zur Grafikausgabe einfach und übersichtlich zu halten. Auf eine Speicherung von Zeitpunkten (Datum, Uhrzeit) der Kurswerte wird verzichtet, um eine einfache Datenstruktur für die Kurswerte zu erhalten. Das Programm bietet zwei Dialoge, mit denen der Name der Aktie, die Wertpapierkennnummer, das Tickersymbol, die Farbe der Kurve und die Option, ein Gitternetz zu zeichnen, eingegeben werden können.
Abbildung 2.5: Das Beispielprogramm StockChart MFC-Programmiertechniken
Am Programm StockChart können die folgenden grundlegenden Aspekte der Programmierung des MFC-Anwendungsgerüsts demonstriert werden: 왘 die Dokument-Ansicht-Architektur (Kursdatenreihe und Aktienkurve) 왘 Serialisierung (Speicherung der Datenreihe) 왘 das Arbeiten mit dem Anwendungs-Assistenten 왘 Grafikprogrammierung (Ausgabe der Aktienkurve) 왘 Drucken und Druckvorschau (Druck der Aktienkurve)
StockChart, ein Programm zur Anzeige von Aktienkursen
31
왘 modale und nichtmodale Dialoge, einschließlich Dialogdatenaustausch und Dialogdatenüberprüfung (Eingabe von Charteinstellungen und Aktiendaten) Das Programm StockChart ist als MDI-Applikation (Multiple Document Interface, Gegensatz zu SDI – Single Document Interface) realisiert, das heißt, es können sowohl mehrere Dokumente gleichzeitig geöffnet sein als auch gleichzeitig mehrere Ansichten eines einzelnen Dokuments bestehen. Jedes Ansichtsfenster zeigt den Kursverlauf einer Aktie an, die durch jeweils ein Dokument, das im Fall des Programms StockChart eine Datei ist, repräsentiert wird. Zusätzlich zum Kursverlauf werden die vertikale und die horizontale Achse eingezeichnet. Der Chart kann in drei verschiedenen Farben dargestellt werden und optional kann die Kurve mit einem Gitternetz hinterlegt werden. Innerhalb des Charts werden Name, WKN und Tickersymbol der Aktie angezeigt. An der Grafikausgabe kann sehr gut die Arbeit mit dem Grafiksystem von Windows gezeigt werden.
MDI-Applikationen
Das Programm StockChart besitzt zwei Dialoge, um die Eigenschaften von Aktie und Chart einstellen zu können. Im Menü AKTIE | EIGENSCHAFTEN befindet sich ein modaler Dialog, mit dem man den Namen der Aktie, die Wertpapierkennnummer und das Tickersymbol eingeben kann. Unter AKTIE | CHART EINSTELLUNGEN befindet sich ein nichtmodaler Dialog, mit dem man die Farbe des Charts und die Optionen zum Zeichnen des Gitternetzes und der Durchschnittslinie bestimmen kann. Anhand dieser beiden Dialoge wird im Abschnitt 2.7, »Dialogfeldprogrammierung«, die Dialogfeldprogrammierung vorgestellt sowie auf den Dialogdatenaustausch (DDX) und die Dialogdatenüberprüfung (DDV) eingegangen.
Dialoge
Das Programm StockChart verwendet zur Speicherung der Kursverläufe die Serialisierungsmechanismen der MFC, worauf im Abschnitt 2.4, »Ein Programm mit dem Anwendungs-Assistenten erstellen«, eingegangen wird. Um Kursdaten in das Programm einzulesen, gibt es eine Importfunktion. Diese befindet sich im Menü AKTIE | DATEN importieren. Die Importfunktion liest Kursdaten aus einfachen Textdateien. Dabei wird auf die Dateiklassen und andere, allgemeine Werkzeugklassen der MFC eingegangen, die im Abschnitt 2.5, »Werkzeugklassen und Dateizugriff«, behandelt werden.
Serialisierung
32
2
Einstieg in die MFC-Programmierung
Das StockChart-Projekt befindet sich auf der Begleit-CD im Verzeichnis KAPITEL2\STOCKCHART. Eine Reihe von Beispieldateien ist im Verzeichnis DATEN enthalten. Es gibt einen Ordner mit Dateien zum Import (DATEN\IMPORT) und einen zweiten mit bereits im StockChart-Format vorliegenden Dokumenten (DATEN\STK).
2.3
Die Dokument-Ansicht-Architektur
Im Folgenden wird die Dokument-Ansicht-Architektur beschrieben. Sie ist der Kern des MFC-Anwendungsgerüsts. Das Anwendungsgerüst wiederum stellt die höchste Abstraktionsebene bei der MFC-Programmierung dar, wie bereits in Abbildung 2.1 in Abschnitt 2.1, »Einführung«, gezeigt wurde. Es ist wichtig, die Strukturen und Klassen der Dokument-Ansicht-Architektur zu verstehen, da sie die Grundlage sowohl für die vom Anwendungs-Assistenten erzeugten Programme als auch die Basis für viele andere von den MFC unterstützten Technologien ist, wie beispielsweise ActiveX. Die Struktur und Klassen der DokumentAnsicht-Architektur werden vorgestellt.
2.3.1
Das Anwendungsgerüst
Die MFC werden mit vollständigen Quelltexten ausgeliefert. Die Quelltexte befinden sich im Verzeichnis MICROSOFT VISUAL STUDIO .NET\VC7\ATLMFC\SRC\MFC auf der Installations-CD Visual C++. Je nach gewählter Installationsoption sind die Quelltexte auch auf der Festplatte vorhanden. Wer in den Quelltexten stöbert, wird – vielleicht etwas überrascht – feststellen, dass der Name »MFC« innerhalb dieser Klassenbibliothek gar nicht verwendet wird. Stattdessen wird sehr häufig das Kürzel AFX benutzt. Nicht zufällig wird das Team, das die MFC entwickelt, als AFX-Gruppe bezeichnet. AFX steht für Application Framework. Ein Application Framework ist eine Klassenbibliothek, die dem Anwendungsentwickler einen Rahmen (Frame) vorgibt, in dem er seine Programme entwickeln kann. Manchmal wird dieser Rahmen auch als Gerüst oder Architektur bezeichnet. In diesem Buch wird der Begriff Anwendungsgerüst verwendet, sofern die Klassen der MFC gemeint sind.
Die Dokument-Ansicht-Architektur
33
Was macht ein solches Anwendungsgerüst aus? Die MFC sind erst seit der Version 2.0 ein vollständiges Anwendungsgerüst, vorher waren sie nicht mehr als eine objektorientierte Systemschnittstelle. Der entscheidende Punkt, durch den die MFC zu einem Anwendungsgerüst wurden, war die Einführung der Dokument-AnsichtArchitektur (Document-View-Architecture). Der zentrale Aspekt dieses Modells ist die Trennung zwischen Daten (Dokumenten) und deren Darstellung (Ansichten). Um diesen zentralen Kern baut sich ein Rahmen von Klassen und Funktionen auf, die das Gerüst für ein komplettes Windows-Programm bilden. Gerade dieser Aufbau, der eine Reihe von Regeln, Vorgehensweisen und Strukturen mit sich bringt, macht ein Anwendungsgerüst aus. Ein Anwendungsgerüst ist folglich eine Ansammlung von Klassen, die dem Programmierer eine Struktur für sein Programm vorgeben. Um das Gerüst effizient zu nutzen, muss sich der Programmierer an die vorgegebenen Regeln und Vorgehensweisen halten. Als Gegenleistung für die Einschränkung seiner programmtechnischen Gestaltungsfreiheit erhält der Programmierer einen einfachen Zugang zu Technologien, die außerhalb des Anwendungsgerüsts nur schwer zu handhaben sind.
2.3.2
Das Applikationsobjekt
Wer bisher Programme für das Windows-API entwickelt hat, der wird sich vielleicht wundern, dass die MFC scheinbar keine mainoder WinMain-Funktion haben. Der Startpunkt des Programms erscheint unklar. Allerdings startet ein MFC-Programm, wie jedes andere Windows-Programm auch, mit der Funktion WinMain. Diese ist allerdings innerhalb der MFC versteckt, der MFC-Programmierer bekommt sie nicht zu sehen. MFC-Programmierer starten ihr Programm, indem sie einfach eine globale Instanz eines so genannten Applikationsobjekts anlegen. Dieses Applikationsobjekt wird von der Klasse CWinApp abgeleitet. Die früheste Möglichkeit, eigenen Programmcode auszuführen, bietet sich daher im Konstruktor des Applikationsobjekts. Man sollte sich jedoch darüber im Klaren sein, dass globale Variablen bereits vor dem Aufruf von WinMain erzeugt werden. Damit wird natürlich auch der Konstruktor vor WinMain aufgerufen. Eher mit einem klassischen Hauptprogramm vergleichbar ist die Funktion InitInstance des Applikationsobjekts. In dieser Funktion werden allgemeine Initialisierungen vorgenommen, die das Programm betreffen. Die
Startpunkt eines MFC-Programms
34
2
Einstieg in die MFC-Programmierung
Funktion WinMain ruft die Funktion AfxWinMain auf, die Startfunktion der MFC-Klassenbibliothek. AfxWinMain ruft dann InitInstance des Applikationsobjekts auf. Dies ist in Abbildung 2.6 zu sehen. Das Applikationsobjekt ist gewissermaßen das Zentrum eines MFCProgramms. Als einziges globales Objekt des Anwendungsgerüsts hält es den Rest des Programms zusammen. Es ist der Startpunkt für den Haupt-Thread der Anwendung. Innerhalb der Member-Funktion Run befindet sich die Nachrichtenschleife der MFC-Anwendung. Aus der Funktion Run wird zudem die Funktion OnIdle aufgerufen, wenn keine Nachrichten aus der Nachrichtenschleife zu verteilen sind. OnIdle kann von Programmen überschrieben werden, um Operationen im Hintergrund durchzuführen, ohne einen eigenen Thread dafür verwenden zu müssen. Man sollte dabei darauf achten, die Nachrichtenschleife nicht allzu lange zu blockieren, denn wenn das Programm keine Nachrichten mehr verarbeiten kann, reagiert es auch nicht mehr auf Benutzereingaben. Für den, der bereits Programme für das Windows-API geschrieben hat, mag die Darstellung des Ablaufs einer MFC-Anwendung in Abbildung 2.6 interessant sein.
WinMain AfxWinMain CWinApp:: InitInstance CWinApp:: Run
(Nachrichtenschleife)
CWinApp:: OnIdle
(beliebig oft)
CWinApp:: ExitInstance Abbildung 2.6: Ablauf einer MFC-Anwendung
2.3.3
Das MVC-Modell
Das Anwendungsgerüst der MFC, die oberste Schicht in Abbildung 2.1 (siehe Abschnitt 2.1, »Einführung«), baut im Kern auf der Dokument-Ansicht-Architektur auf. Alle Funktionalität entwickelt sich um dieses zentrale Element, wie Abbildung 2.7 zeigt.
Die Dokument-Ansicht-Architektur
35
Anwendungsgerüst
DokumentAnsichtArchitektur
Abbildung 2.7: Dokument-Ansicht-Architektur als Teil der MFC
Was hat es mit der Dokument-Ansicht-Architektur auf sich? Die Trennung zwischen Dokument und Ansicht ist keine Erfindung von Microsoft, sondern wurde unter der Bezeichnung MVC (Model View Controller) erstmals mit den Klassenbibliotheken der Programmiersprache Smalltalk eingeführt. Smalltalk verfügte auch schon über eine grafische Benutzeroberfläche (GUI, Graphical User Interface), noch bevor der Apple Macintosh diese etwas später populär machte. Die Erfinder von Smalltalk erkannten, dass für eine grafische Benutzeroberfläche entwickelte Programme sinnvollerweise eine andere Struktur haben sollten als Programme, die ihre Ergebnisse auf Terminals ausgeben. Insbesondere sollten die Daten eines Programms (Model in MVC) von deren Darstellung (View) getrennt werden. Die für GUI-Programme typische Ereignissteuerung sollte durch den Controller von Daten und Darstellung getrennt werden. Da Controller und View die sichtbaren Teile eines Programms repräsentieren, liegen sie konzeptionell einander näher als dem Model, das die für den Benutzer nicht sichtbaren Daten repräsentiert. Abbildung 2.8 zeigt diesen Zusammenhang.
Trennung von Daten und Darstellung
Warum bietet sich diese Trennung an? Sobald man größere Programme schreibt, ist man gezwungen, diese nach irgendwelchen Methoden zu organisieren und zu strukturieren, um nicht die Übersicht zu verlieren. Es bietet sich fast immer an, die interne Repräsentation von der externen Darstellung zu trennen. Meistens lassen sich Daten auf verschiedene Weisen darstellen. Trennt man interne Repräsentation und externe Darstellung, so kann man die Darstellung später leicht abändern, ohne die interne Repräsenta-
Vorteile der Trennung
36
2
Einstieg in die MFC-Programmierung
tion zu beeinflussen. Ist die Trennung erst einmal vollzogen, so ist es ein Leichtes, mehrere, unterschiedliche Darstellungen der gleichen internen Repräsentation zu verwenden. Die Praxis, größere Applikationen in Front-End (Darstellung der Daten und Benutzerschnittstelle) und Back-End (Datenbank oder ähnliches zur Datenverwaltung) aufzuteilen, entspricht der Trennung zwischen interner Repräsentation und externer, sichtbarer Darstellung. Meistens besitzen interne Datenmodelle eine wesentlich längere Lebensdauer als ihre externen Darstellungen. Benutzereingaben
Änderung der Darstellung
Bildschirmausgaben
View
Controller
Änderungen in der Benutzerschnittstelle
Zugriffe auf Daten zur Visualisierung
Veränderung von Daten Model (passiv)
Abbildung 2.8: MVC-Modell Begriffe
Im Kontext der MFC werden andere Begriffe verwendet als im MVC-Modell. Das Model wird in den MFC als Dokument bezeichnet, der View in der deutschen Übersetzung als Ansicht. Für den Controller gibt es keine eindeutig zuzuordnende Entsprechung, die Aufgaben des Controllers können von Dokument und Ansicht oder in den MFC auch von Rahmenfenster und Applikationsobjekt übernommen werden.
2.3.4
Die Klassen des Anwendungsgerüsts
Die Klassen des Anwendungsgerüsts befinden sich alle unterhalb der Klasse CCmdTarget. Allerdings gehören nicht alle von CCmdTarget abgeleiteten Klassen zwingend dem Anwendungsgerüst an.
Die Dokument-Ansicht-Architektur
37
So sind auch alle Fenster- und Steuerelementklassen von CCmdTarget abgeleitet. Abbildung 2.9 zeigt die wichtigsten Klassen des Anwendungsgerüsts. CObject CCmdTarget CWinThread CWinApp eigene Anwendungsobjekte CDocTemplate CSingleDocTemplate CMultiDocTemplate CDocument eigene Dokumente CWnd CFrameWnd CMDIChildWnd eigene Kindrahmenfenster CMDIFrameWnd eigene Hauptrahmenfenster eigene Rahmenfenster CView eigene Ansichten Abbildung 2.9: Klassen des Anwendungsgerüsts
Zentrale Klassen des Anwendungsgerüsts sind CDocument, die Basis aller Dokumente, CView, die Grundlage aller Ansichten, CFrameWnd, die Rahmenfensterklasse, sowie CDocTemplate, die Dokumentenvorlage. 왘 CDocument verwaltet die Daten der Anwendung. Vom Begriff Dokument sollte man sich nicht irreführen lassen. Das Dokument muss keine Datei sein. Es kann beispielsweise auch den Zugriff auf eine Datenbank kapseln oder ein Modul zur Messwerterfassung ansteuern. CDocument selbst wird nicht verwendet, man leitet eigene Klassen von CDocument ab. Wenn eine Anwendung mehrere Typen von Daten bearbeiten kann, so wird für jeden Typ eine eigene Dokumentenklasse verwendet.
Zentrale Klassen
38
2
Einstieg in die MFC-Programmierung
왘 CView ist die Basisklasse aller Ansichten. Ansichten haben die Aufgabe, Daten darzustellen. Genau wie CDocument wird CView nicht direkt verwendet, sondern man benutzt von CView abgeleitete Klassen. Zu einer Dokumentenklasse kann es mehrere verschiedene Ansichtsklassen geben, die das Dokument in unterschiedlichen Formen darstellen. 왘 CFrameWnd ist die Basisklasse aller Rahmenfenster. Die Aufgabe dieser Fenster ist es, einen Rahmen für Ansichten zur Verfügung zu stellen. Ansichten sind technisch gesehen Windows-Fenster, werden aber trotzdem zusätzlich immer in Rahmenfenster eingebettet. Auf diese Weise kann Programmcode, der nur den Rahmen betrifft, klar vom Programmcode der Ansicht getrennt werden. Rahmenfensterklassen für SDI-Programme (Programme mit einem Hauptfenster) und MDI-Programme (Programme mit mehreren Kindfenstern) werden von verschiedenen Klassen abgeleitet. SDI-Programme und Programme mit mehreren Hauptfenstern (browserartige Programme) benutzen CFrameWnd direkt als Basisklasse für eigene Rahmenfenster, während MDI-Programme die davon abgeleitete Klasse CMDIChildWnd als Ausgangspunkt haben. Zusätzlich besitzen MDI-Programme eine weitere Fensterklasse, die den Rahmen für die gesamte Anwendung bereitstellt. Das Hauptrahmenfenster beherbergt die Menü-, Symbol- und Statusleisten des Programms. Das Hauptrahmenfenster einer MDIAnwendung schließt im Gegensatz zu den Rahmenfenstern der Kindfenster und zum Rahmenfenster einer SDI-Anwendung jedoch keine Ansicht ein. Die Hauptrahmenfensterklasse wird von CMDIFrameWnd abgeleitet. Abbildung 2.10 zeigt Rahmenfenster und Ansichtsfenster einer MDI-Anwendung. 왘 Dokumentenvorlagen sind der Klebstoff, der alle Teile des Anwendungsgerüsts zusammenhält. Die Basisklasse ist CDocTemplate. Es wird zwischen zwei Arten von Anwendungen unterschieden: solchen, die nur ein aktives Dokument gleichzeitig verwenden, und solchen, die mehrere Dokumente gleichzeitig verwenden. Daher werden von CDocTemplate die beiden Klassen CSingleDocTemplate und CMultiDocTemplate abgeleitet. Beide Klassen sind sich recht ähnlich, allerdings verwaltet CMultiDocTemplate eine Liste von Dokumenten, CSingleDocTemplate jedoch nur eins.
Die Dokument-Ansicht-Architektur
39
Titelleiste Menüleiste
Titelleiste
Rahmenfenster (Kindfenster) Ansichtsfenster
Hauptrahmenfenster
Abbildung 2.10: Rahmen- und Ansichtsfenster einer MDI-Anwendung
Die Dokumentenvorlage wird beim Start eines MFC-Programms innerhalb der Funktion InitInstance der Applikationsklasse erzeugt. Dem Konstruktor der Dokumentenvorlage werden Dokumentenklasse, Rahmenfensterklasse und Ansichtsklasse übergeben. Im Falle einer SDI-Anwendung ergibt sich die in Abbildung 2.11 gezeigte Struktur.
CWinAppObjekt
CSingleDocTemplateObjekt
CDocumentKlasse
CFrameWndKlasse
Abbildung 2.11: Struktur einer SDI-Anwendung
CViewKlasse
SDI-Applikationen
40
2 MDI-Applikationen
Einstieg in die MFC-Programmierung
Das Anwendungsgerüst ist durchaus in der Lage, mit mehr als einem Dokumententyp gleichzeitig umzugehen. Dazu wird für jeden Dokumententyp eine eigene Dokumentenvorlage angelegt und in InitInstance zu der Liste verwendeter Vorlagen hinzugefügt. Mehrere Dokumententypen können von einer MDI-Anwendung gleichzeitig in verschiedenen Ansichten angezeigt werden. Abbildung 2.12 zeigt die Struktur einer MDI-Anwendung. Im Abschnitt 2.4, »Ein Programm mit dem Anwendungs-Assistenten erstellen«, wird der Programmcode des Anwendungsgerüsts anhand des MDI-Beispielprogramms StockChart besprochen.
CWinAppObjekt
CMultiDocTemplateObjekt
CDocumentKlasse
CMDIChildWndKlasse
CMDIFrameWndObjekt
CMultiDocTemplateObjekt
CViewKlasse
...
...
...
Abbildung 2.12: Struktur einer MDI-Anwendung
2.3.5
Kommunikation zwischen den Klassen des Anwendungsgerüsts
Innerhalb des Anwendungsgerüsts haben alle Klassen ihre klar definierten Aufgaben. Jedoch können die Klassen ihre Aufgaben nicht isoliert voneinander erfüllen, sondern sie müssen miteinander kommunizieren. Dies gilt insbesondere für das Dokument und die Ansicht. Die Ansicht kann das zu ihr gehörige Dokument jederzeit über die Member-Funktion GetDocument erfragen und somit auf die Daten des Dokuments zugreifen. Hat sich etwas am Dokument verändert, so muss das Dokumentenobjekt alle zu ihm gehörenden Ansichten von der Änderung informieren. Dazu gibt es die Funk-
Die Dokument-Ansicht-Architektur
41
tion UpdateAllViews der Dokumentenklasse. Wird diese aufgerufen, so ruft sie ihrerseits für jede Ansicht die Funktion OnUpdate der Ansicht auf. OnUpdate ist eine virtuelle Funktion, deren Standardimplementierung die gesamte Ausgabefläche der Ansicht für ungültig erklärt und damit eine Neuzeichnung erzwingt.
Applikationsobjekt
Hauptfenster
AfxGetApp
AfxGetMainWnd
UpdateAllViews Dokument
Ansicht GetDocument
Abbildung 2.13: Kommunikation zwischen den Objekten des Anwendungsgerüsts
Werden Änderungen am Dokument von einer Ansicht ausgelöst, so kann diese UpdateAllViews aufrufen und sich selbst als Parameter übergeben. Die Ansicht wird dann vom Update-Vorgang ausgenommen. Das kann sinnvoll sein, wenn die Ansicht sich bereits vorher selbst aktualisiert hat. Manchmal ist es notwendig, innerhalb eines MFC-Programms auf das Applikationsobjekt zuzugreifen. Es kann jederzeit durch Aufruf der globalen Funktion AfxGetApp ermittelt werden. Möchte man auf das Hauptfenster des Programms zugreifen, so lässt sich dieses durch einen Aufruf der Funktion AfxGetMainWnd ermitteln. Bei SDI-Anwendungen zeigt der von der Funktion AfxGetMainWnd zurückgegebene Zeiger auf das Rahmenfensterobjekt des Programms, bei MDI-Programmen wird ein Zeiger auf das Hauptrahmenfensterobjekt zurückgegeben. Sollte der Aufruf von AfxGetMainWnd aus einem aktiven OLE-Server heraus erfolgen, so liefert die Funktion das Hauptfenster zurück, in das der OLEServer eingebettet ist (OLE-Server werden ausführlich in Kapitel 3, »COM, OLE und ActiveX«, besprochen).
t Globale Funktionen
42
2
2.3.6 Persistente Objekte
Einstieg in die MFC-Programmierung
Serialisierung
Das Konzept der Serialisierung baut auf der Idee persistenter Objekte auf. Persistente Objekte bestehen über die Laufzeit eines Programms hinaus. C++-Objekte und solche anderer objektorientierter Programmiersprachen werden beim Programmstart (globale Objekte) oder während der Laufzeit (funktionslokale Objekte und Objekte auf dem Heap) erzeugt. Die Erzeugung eines C++-Objekts wird durch den Aufruf seines Konstruktors begleitet. Analog dazu werden alle Objekte bereits zur Laufzeit oder beim Beenden des Programms zerstört. Bei C++-Objekten wird dies durch den Aufruf des Destruktors begleitet. Sie können folglich nicht länger leben als das Programm, das sie beherbergt. Sie sind nicht persistent. Serialisierung bezeichnet den Vorgang, normalen Objekten einer objektorientierten Programmiersprache zu Persistenz zu verhelfen. Die Idee dabei ist einfach: Man nimmt einen Speicherabzug des zu serialisierenden Objekts und speichert ihn auf einem dauerhaften Medium, wie beispielsweise in einer Datei oder in einer Datenbank. In umgekehrter Richtung muss aus diesem Speicherabzug wieder ein Objekt der verwendeten Programmiersprache erzeugt werden. Die MFC implementieren diese Technik in der Klasse CObject. Dadurch ist es möglich, alle von dieser Klasse abgeleiteten Klassen zu serialisieren. Auch einige andere, nicht von CObject abgeleitete Klassen, wie beispielsweise CString, implementieren die Serialisierung.
CArchive
Die Serialisierung wird über ein Objekt der Klasse CArchive durchgeführt. Dieses ist gleichsam das Bindeglied zwischen dem zu serialisierenden Objekt und dem Dateiobjekt, in dem der Speicherabzug abgelegt wird. CArchive richtet einen binären, gepufferten Datenstrom zum Dateiobjekt ein.
zu serialisierendes Objekt
CArchive-Objekt
CFile-Objekt
Abbildung 2.14: Objekte bei der Serialisierung
Klassen implementieren zur Serialisierung ihrer Objekte die Funktion Serialize. Dieser Funktion wird ein CArchive-Objekt übergeben. Anhand dieses Objekts kann bestimmt werden, in welcher Richtung serialisiert werden soll, das heißt ob geladen oder gespeichert werden soll.
Die Dokument-Ansicht-Architektur
Im Anwendungsgerüst wird die Serialisierung zum Laden und Speichern von Dokumenten verwendet. Das Anwendungsgerüst nimmt dem Programmierer dabei viele Aufgaben ab. Immer wenn ein Dokument geladen oder gespeichert werden soll, wird die Funktion Serialize des Dokuments aufgerufen. Der gesamte zur Serialisierung notwendige Kontext, wie beispielsweise das CArchive-Objekt, wird vom Anwendungsgerüst erzeugt und bereitgestellt. Der Programmierer muss lediglich die Serialize-Funktion selbst implementieren.
43 Serialisierung im Anwendungsgerüst
Innerhalb der Serialisierungsfunktion können einfache Datentypen (int, char, double usw.) durch Aufruf der Operatoren << und >> serialisiert werden. Diese Operatoren sind auch für einige andere Klassen überladen, beispielsweise können CString-Objekte so serialisiert werden. Komplexe Datentypen werden durch den Aufruf ihrer Serialize-Funktion serialisiert. Möchte man eigene Klassen serialisieren, so sollten diese von CObject abgeleitet werden und die beiden Makros DECLARE_SERIAL und IMPLEMENT_SERIAL sind zu verwenden. Um Typinformationen über Klassen zur Laufzeit zur Verfügung zu stellen, verwenden die MFC drei Gruppen von Makros. Jede Gruppe von Makros besteht aus zwei Teilen. Ein Teil wird bei der Deklaration der Klasse verwendet, der andere bei der Implementierung. Entsprechend beginnen die Makros entweder mit DECLARE oder mit IMPLEMENT. Alle Makros arbeiten vollkommen unabhängig von der vom C++-Compiler bereitgestellten RTTI-Funktionalität. Die Makros DECLARE_DYNAMIC und IMPLEMENT_ DYNAMIC stellen Typinformationen über MFC-Klassen bereit. Damit ist es möglich, Makros wie RUNTIME_CLASS oder Funktionen wie IsKindOf zu verwenden. Die Makros DECLARE_DYNCREATE und IMPLEMENT_ DYNCREATE stellen die gleiche Funktionalität wie DECLARE_ DYNAMIC und IMPLEMENT_DYNAMIC bereit, fügen aber zusätzlich die Möglichkeit hinzu, Objekte aus den erhaltenen Typinformationen zu erzeugen. Dies wird beispielsweise bei der Serialisierung benötigt. Die Makros DECLARE_SERIAL und IMPLEMENT_SERIAL bieten alle Funktionen der bisher genannten Makros, fügen aber zusätzlich noch Versionsnummern für die behandelte Klasse ein.
Typmakros in den MFC
44
2
2.3.7
Einstieg in die MFC-Programmierung
Nachrichtenverarbeitung
Nachrichten werden unter Windows zur Signalisierung von Ereignissen verwendet. Solch ein Ereignis kann beispielsweise die Auswahl eines Menüpunkts oder das Anklicken einer Schaltfläche sein. Eine Windows-Nachricht selbst ist nichts weiter als eine eindeutig definierte Integer-Konstante. Windows definiert eine große Anzahl dieser Nachrichten als Konstanten mit dem Präfix WM_ (die Nachrichten der Steuerelemente haben allerdings teilweise andere Präfixe). Jedes Windows-Programm muss auf eine unterschiedliche Auswahl von Windows-Nachrichten reagieren, um zu funktionieren. Ohne Nachrichtenverarbeitung kommt kein sinnvolles Windows-Programm aus. Jede Windows-Nachricht hat zwei Parameter, WPARAM und LPARAM. Beide Parameter sind Integer-Werte, mit denen der Nachricht ergänzende Informationen mitgegeben werden können. Beispielsweise wird der Nachricht WM_LBUTTONDOWN, die anzeigt, dass die linke Maustaste gedrückt wurde, die aktuelle Mausposition im Parameter LPARAM mitgegeben. Nachrichten werden immer an Fenster gesendet. Bei der Windows-API-Programmierung werden Nachrichten durch CallbackFunktionen, auch Fensterfunktionen genannt, empfangen. Innerhalb der Fensterfunktion muss der Programmierer eine – meist recht große – switch-Anweisung pflegen, die die Nachrichten entweder sofort verarbeitet oder an ihren Zielort weiterleitet, also eine der Nachricht entsprechende Verarbeitungsfunktion aufruft. CCmdTarget
Bei der Nachrichtenverarbeitung verfolgen die MFC einen sehr leistungsfähigen und sich vom Windows-API unterscheidenden Ansatz. Es werden keine Fensterfunktionen verwendet. Die MFC gehen das Problem von der anderen Seite her an. Eine MFC-Klasse kann angeben, welche Nachrichten sie verarbeiten möchte. Für jede Nachricht, die behandelt werden soll, wird eine MemberFunktion der Klasse bestimmt, die diese Nachricht verarbeiten soll. Nicht der Programmierer muss sich darum kümmern, dass die Nachricht die Verarbeitungsfunktion aufruft, sondern das Anwendungsgerüst leitet die Nachricht zu ihrem Ziel. Damit dieser Mechanismus funktioniert, sind zwei Voraussetzungen notwendig. Die Klasse, die Nachrichten empfangen soll, muss zum Nachrichtenempfang eingerichtet sein. Sie ist dies, wenn sie von CCmdTarget abgeleitet worden ist, denn CCmdTarget implementiert
Die Dokument-Ansicht-Architektur
45
die dazu notwendigen Mechanismen. Da alle Klassen des Anwendungsgerüsts von CCmdTarget abgeleitet (siehe Abbildung 2.9 im Abschnitt 2.3.4, »Die Klassen des Anwendungsgerüsts«) sind, eignen sie sich auch alle zum Nachrichtenempfang. Die zweite Voraussetzung zum Nachrichtenempfang ist die Definition von so genannten Message Maps oder Nachrichtenzuordnungstabellen. Diese Tabellen stellen die Beziehung zwischen der Windows-Nachricht und der sie behandelnden Member-Funktion her. Nachrichtenzuordnungstabellen werden mit Hilfe von Makros erstellt. In der Header-Datei der Klasse muss die Tabelle mit dem Makro DECLARE_MESSAGE_MAP deklariert werden. In der Implementierungsdatei werden die Makros BEGIN_MESSAGE_MAP und END_MESSAGE_MAP verwendet. Zwischen diesen beiden Makros befinden sich die Einträge, die die Nachrichtenzuordnung vornehmen. Diese Einträge bestehen wiederum aus Makros. Es gibt eine Reihe verschiedener Makros für unterschiedliche Nachrichtentypen. Beispielsweise ist ON_COMMAND für Nachrichten des Typs WM_COMMAND zuständig, ON_WM_PAINT ordnet eine WM_PAINT-Nachricht zu.
Nachrichtenzuordnungstabellen
Bei der Nachrichtenweiterleitung unterscheidet das Anwendungsgerüst zwischen Kommandonachrichten (Command Messages, Nachrichten des Typs WM_COMMAND) und anderen Nachrichten. Normale Nachrichten, wie beispielsweise WM_SIZE, werden direkt an das zuständige Fensterobjekt gesendet. Bei Kommandonachrichten, also Nachrichten von Menüeinträgen und Steuerelementen, wird ein höherer Aufwand getrieben. Die Nachricht durchläuft nacheinander folgende Stationen:
Nachrichtentypen
1. die aktive Ansicht 2. das zur aktiven Ansicht gehörende Dokument 3. das Hauptrahmenfenster 4. das Applikationsobjekt Wenn eines dieser Objekte die Kommandonachricht verarbeiten kann, wird die weitere Suche abgebrochen. Sinn und Zweck dieses Weiterleitungskonzepts ist es, Kommandonachrichten an der Stelle bearbeiten zu können, an der es am sinnvollsten ist, ohne dass sich der Programmierer dabei Gedanken machen muss, wie die Nachricht zur gewünschten Stelle kommt.
46
2
Einstieg in die MFC-Programmierung
Reflektierte Nachrichten
Reflektierte Nachrichten stellen einen Sonderfall bei der Nachrichtenverarbeitung dar. Sie werden bei Steuerelementen verwendet. Normalerweise senden Steuerelementen ihre Nachrichten an das übergeordnete Fenster (Parent). Dort werden die Nachrichten verarbeitet. Das führt manchmal dazu, dass die Nachrichtenverarbeitung für jede Verwendung des Steuerelements neu implementiert werden muss. Durch reflektierte Nachrichten ist es möglich, diese Nachrichten im Steuerelement selbst zu behandeln. Damit können Steuerelemente lokal agieren (beispielsweise ihre Farben selbst setzen) und sind leichter wiederverwendbar. Reflektierte Nachrichten lassen sich in der Eigenschaftsansicht durch ein vorangestelltes =-Zeichen erkennen. Reflektierte Nachrichten sind eine Eigenschaft der MFC, auf der Ebene der Windows-API-Programmierung gibt es keine Entsprechung für sie. Reflektierte Nachrichten werden ausführlich im technischen Hinweis 62 beschrieben (die technischen Hinweise sind Teil der Online-Hilfe. Siehe hierzu das Literaturverzeichnis im Anhang).
Nachrichtenzuordnungstabellen
Nachrichtenzuordnungstabellen lassen sich einfach in der Eigenschaftsansicht verwalten, wie es Abbildung 2.15 zeigt. Die verfügbaren Nachrichten werden unter »Events« (kleiner gelber Blitz) aufgeführt. Man kann (leere) Nachrichtenbehandlungsfunktionen hinzufügen, die passenden Makros werden dann automatisch ergänzt.
Abbildung 2.15: Nachrichtenzuordnung in der Eigenschaftsansicht
Ein Programm mit dem Anwendungs-Assistenten erstellen
2.3.8
Zusammenfassung
Die Dokument-Ansicht-Architektur ist ein wohldefinierter Rahmen zur Erstellung von Programmen für grafische Benutzeroberflächen. Eine sinnvolle Trennung logisch verschiedener Programmteile wird von ihr vorgegeben. Fester Bestandteil der Dokument-AnsichtArchitektur ist der eingebaute Serialisierungsmechanismus, mit dem sich die Daten eines Programms auf einfache Weise in Dateien speichern lassen. Nicht auf die Dokument-Ansicht-Architektur beschränkt sind die Nachrichtenzuordnungstabellen der MFC. Sie vereinfachen die Behandlung von Windows-Nachrichten. Mit dem Anwendungs-Assistenten lässt sich ein auf der Dokument-AnsichtArchitektur aufbauendes Programm schnell erstellen.
2.4
Ein Programm mit dem AnwendungsAssistenten erstellen
Der Anwendungs-Assistent ist ein Hilfsprogramm innerhalb der Visual Studio Entwicklungsumgebung, das dem Programmierer bei der Erstellung von MFC-basierten Anwendungen hilft. In der englischsprachigen Version von Visual C++ heißt so ein Hilfsprogramm Wizard, also Zauberer. So dient der Anwendungs-Assistent dazu, dem Programmierer den Programmcode einer voll funktionsfähigen Windows-Anwendung mittels weniger Mausklicks herbeizuzaubern.
2.4.1
Der Anwendungs-Assistent
Der Anwendungs-Assistent wird nur einmal, zu Beginn eines Projekts benötigt. Ist ein Projekt erst einmal erzeugt worden, so kann der Anwendungs-Assistent daran keine Änderungen mehr vornehmen. Der Anwendungs-Assistent ist folglich eine Einbahnstraße; einmal gewählte Optionen können nachträglich nicht mehr mit ihm verändert werden. Man sollte sich zu Beginn eines Projekts genau überlegen, welche Optionen man im Anwendungs-Assistenten auswählt. Stellt man gleich zu Beginn eines Projekts fest, dass man bestimmte Optionen falsch gewählt hat, so ist es oft am günstigsten, das Projekt zu löschen und noch einmal von vorn zu beginnen. Natürlich kann ein erfahrener MFC-Programmierer die notwendigen Änderungen manuell im Programmcode vornehmen, nur ist dies ungleich mühsamer. Für Anfänger ist es oft nicht ersichtlich, was geändert werden muss.
47
48
2
Einstieg in die MFC-Programmierung
Der vom Anwendungs-Assistenten erzeugte Programmcode lässt sich nach Beenden des Assistenten sofort in ein lauffähiges Programm übersetzen. Je nach gewählten Optionen unterstützt dieses Programm bereits ein oder mehrere Dokumentenfenster (SDIoder MDI-Programm), eine Symbol- und eine Statusleiste, Menüs zur Dateiverarbeitung und zum Ausschneiden, Kopieren und Einfügen, eine Liste der zuletzt bearbeiteten Dokumente sowie das Windows-Hilfesystem. Auch so fortgeschrittene Funktionen wie Verbunddokumente (Client und Server), Automation und Datenbankschnittstellen kann der Anwendungs-Assistent bereitstellen. Weitere Assistenten
In früheren Versionen von Visual C++ gab es den Klassen-Assistenten, mit dem man Veränderungen und Ergänzungen an einem mit dem Anwendungs-Assistenten erzeugten Programm vornehmen konnte. Der Klassen-Assistent ist mittlerweile durch eine ganze Reihe kleinerer Assistenten ersetzt worden, die automatisch gestartet werden, wenn man beispielsweise neue Klassen zu einem Projekt hinzufügen möchte. Diese Assistenten bestehen zumeist aus nur einem Dialogfeld, das vom Programmierer ausgefüllt werden muss. Anschließend generiert der entsprechende Assistent neuen Programmcode. Weitere Aufgaben des ehemaligen Klassen-Assistenten, wie beispielsweise das Anlegen von Nachrichtenbehandlungsfunktionen oder das Überschreiben von Funktionen, werden in Visual Studio .NET durch das Eigenschaftsfenster übernommen. Das Programm StockChart wurde mit dem Anwendungs-Assistenten erzeugt. Der Anwendungs-Assistent wird automatisch gestartet, wenn man ein neues Projekt vom Typ »MFC-Anwendung« anlegt. Es wurden folgende Optionen angegeben: 왘 Im Menü DATEI | NEU | PROJEKT wurde »MFC-Anwendung« ausgewählt, da eine selbstständig ausführbare WindowsAnwendung entstehen sollte. 왘 Auf der Karteikarte ANWENDUNGSTYP wurde das Programm als MDI-Anwendung definiert. Das Kontrollkästchen zur Verwendung der Dokument-Ansicht-Architektur und die deutschen Ressourcen wurden ausgewählt. Alle Einstellungen entsprechen den Vorgabewerten. Auch die weiteren Einstellungen wurden unverändert übernommen. 왘 Bei UNTERSTÜTZUNG FÜR VERBUNDDOKUMENTE wurde nichts verändert, da StockChart weder ein OLE-Server noch ein OLEContainer ist.
Ein Programm mit dem Anwendungs-Assistenten erstellen
왘 Unter dem Punkt DOKUMENT-VORLAGENZEICHENFOLGEN wurde die Dateinamenerweiterung STK für StockChart eingetragen. Die anderen Einträge wurden dann unverändert übernommen. 왘 Bei DATENBANKUNTERSTÜTZUNG wurde nichts verändert, da StockChart keine Datenbankunterstützung vorsieht. 왘 Auf der Karteikarte BENUTZEROBERFLÄCHEN-FEATURES wurden keine Veränderungen vorgenommen. Das Aussehen von StockChart entspricht somit den Standardvorgaben. 왘 Bei ERWEITERTE FEATURES wurde die Unterstützung für ActiveX-Steuerelemente abgeschaltet, da StockChart keine solchen Steuerelemente enthält. Ansonsten wurden die Vorgaben übernommen. 왘 Die Einstellungen bei ERSTELLTE KLASSEN wurden unverändert übernommen.
Abbildung 2.16: Auswahl des Anwendungs-Assistenten
Zunächst wird der Anwendungs-Assistent durch Auswahl von »MFC-Anwendung« aus den in Abbildung 2.16 gezeigten Projekttypen ausgewählt. Neben dem MFC-Anwendungs-Assistenten gibt es Assistenten, um DLLs, ISAPI-Erweiterungen und ActiveXSteuerelemente auf der Basis der MFC zu erstellen. Der MFCAnwendungs-Assistent für DLLs wird in Abschnitt 2.11, »MFC
49
50
2
Einstieg in die MFC-Programmierung
und DLLs« vorgestellt. Der Assistenten für ActiveX-Steuerelemente findet in Kapitel 3, »COM, OLE und ActiveX«, Verwendung. In Kapitel 5, »Internetprogrammierung«, wird eine ISAPIServer-Erweiterung mit dem Assistenten für ISAPI-Erweiterungen erzeugt. Die restlichen zur Auswahl stehenden Projekttypen bauen nicht auf den MFC auf, sondern dienen unter anderem zur Programmierung von .NET-, WIN32- oder ATL-Anwendungen.
Hat man den Anwendungs-Assistenten ausgewählt und einen Projektnamen vergeben, so legt man unter ANWENDUNGSTYP die grundlegende Struktur der Anwendung fest. Während sich vom Anwendungs-Assistenten erzeugte SDI- und MDI-Anwendungen im Programmcode nicht sehr stark unterscheiden, erzeugt die Option AUF DIALOGFELDERN BASIEREND ein grundlegend anders strukturiertes Programm ohne Verwendung der Dokument-AnsichtArchitektur. Die Unterstützung der Dokument-Ansicht-Architektur für SDI- und MDI-Programme lässt sich abschalten. Es wird dann ein ähnlich strukturiertes Programm ohne Dokument erzeugt. Von den weiteren Optionen des Anwendungs-Assistenten in den anderen Schritten lassen sich in diesem Fall nur noch Teile auswählen. Im Gegensatz zu SDI- und MDI-Programmen bauen dialogfeldbasierte Programme nicht auf der Dokument-Ansicht-Architektur auf. Dialogfeldbasierte Anwendungen eignen sich besonders für
Ein Programm mit dem Anwendungs-Assistenten erstellen
kleine Programme, die keine Dateien oder andere Dokumente verwalten. Übrigens lassen sich nach Auswahl dieser Option einige der Karteikarten im Anwendungs-Assistenten nicht mehr anwählen, denn Optionen wie Datenbankunterstützung oder Verbunddokumente sind bei dialogfeldbasierten Anwendungen nicht möglich. Bei dialogfeldbasierten Programmen bietet der Anwendungs-Assistent die Option HTML-DIALOGFELD VERWENDEN an. Dabei wird das Dialogfeld selbst nicht durch eine Windows Dialogressource, sondern durch HTML definiert. Ein dialogfeldbasiertes Programm wird unter anderem in Kapitel 3, »COM, OLE und ActiveX«, verwendet. In einem Listenfeld lässt sich die Sprache der Anwendung auswählen. Alle Ressourcen des vom Anwendungs-Assistenten erzeugten Programms werden in der ausgewählten Sprache angelegt. In der deutschen Version von Visual Studio .NET stehen die Sprachen Deutsch, Englisch, Spanisch, Französisch und Italienisch zur Auswahl. Auf der rechten Seite kann eine Einstellung zur Erscheinungsform des Programms vorgenommen werden. Es besteht die Möglichkeit, ein Erscheinungsbild auszuwählen, das dem des WindowsExplorer ähnelt. Links wird in einem solchen Programm eine Struktur in Form eines Baums dargestellt, im rechten Teil werden Listen dargestellt und eventuell verändert. Die meisten Programme verwenden jedoch die Einstellung MFC-STANDARD. Schließlich ist es wichtig anzugeben, ob die Klassen der MFC statisch oder dynamisch zu dem zu erzeugenden Programm gelinkt werden sollen. Statisch bedeutet, dass die Klassen der MFC mit in die Programmdatei hineinkopiert werden, während dynamisch heißt, dass auf die MFC-Klassen in einer externen DLL zugegriffen wird. Diese DLL muss dann allerdings auch auf allen Zielsystemen vorhanden sein, auf denen das Programm später laufen soll. Welche DLLs mit dem Programm ausgeliefert werden müssen, wird in den technischen Hinweisen 33 und 56 beschrieben. Welche Option zum Binden der MFC-Bibliothek man wählt, ist oft eine Geschmacksfrage. Dynamisch gebundene Programme sind kleiner, allerdings bestehen sie immer aus mehreren Dateien. Das macht eine Installation eventuell komplizierter, da die MFC DLLs ins Windows-Systemverzeichnis kopiert werden sollten.
51
52
2
Einstieg in die MFC-Programmierung
Abbildung 2.18: Anwendungs-Assistent, Unterstützung für Verbunddokumente Unterstützung für Verbunddokumente
Die Karteikarte UNTERSTÜTZUNG FÜR VERBUNDDOKUMENTE dient der Festlegung von ActiveX-, OLE und Automationsfunktionen. Für »normale« Windows-Anwendungen muss hier nichts ausgewählt werden. Eine detaillierte Beschreibung dieser Optionen befindet sich in Kapitel 3, »COM, OLE und ActiveX«, in dem die Verwendung dieser Technologien besprochen wird.
Abbildung 2.19: Anwendungs-Assistent, Zeichenfolgen für Dokumentvorlagen
Ein Programm mit dem Anwendungs-Assistenten erstellen
Falls das zu erstellende Programm Dateien zur Speicherung seiner Dokumente verwendet, so sollte auf der Registerkarte ZEICHENFOLGEN FÜR DOKUMENT-VORLAGEN bereits die Dateinamenerweiterung angegeben werden. Die anderen Felder werden automatisch ausgefüllt und sollten nur bei Bedarf geändert werden. Auf die Bedeutung der anderen Felder wird in Kapitel 3, »COM, OLE und ActiveX«, eingegangen.
Auf der Karteikarte DATENBANKUNTERSTÜTZUNG werden die Datenbankfunktionen des Programms festgelegt. Hier kann man zwischen zwei Formen der Unterstützung wählen. Es werden die Zugriffstechniken ODBC und OLE DB unterstützt. Wenn eine Datenbankansicht gewählt wird, muss auch gleich die Datenquelle angegeben werden. Die Optionen zur Datenbankunterstützung werden ausführlich in Kapitel 4, »Datenbankprogrammierung«, im Zusammenhang mit der Datenbankprogrammierung besprochen.
Datenbankunterstützung
Auf der Registerkarte BENUTZEROBERFLÄCHENFEATRURES lassen sich verschiedene Einstellungen zu der Erscheinungsform der Fenster des zu erstellenden Anwendungsprogramms vornehmen. Die Option GETEILTES FENSTER stattet das Programm mit der Möglichkeit aus, Fenster zu teilen (Splitter). Stellt man für die Erscheinungsform der Symbolleisten die Option BROWSER-STIL ein, dann verfügt das vom Anwendungs-Assistent generierte Programm
Benutzeroberflächenfeatures
54
2
Einstieg in die MFC-Programmierung
über Symbolleisten, die sich wie die Symbolleisten des Microsoft Internet Explorer verhalten. Anderenfalls besitzt das erzeugte Programm eine einfache Symbolleiste. Dies ist die Voreinstellung. Die weiteren Punkte auf dieser Karteikarte sind selbsterklärend.
Abbildung 2.22: Anwendungs-Assistent, Erweiterte Features
Ein Programm mit dem Anwendungs-Assistenten erstellen
Auf der Karteikarte ERWEITERTE FEATURES werden einige Optionen zur Erscheinungsform eingestellt. Die Option zur Erzeugung von kontextabhängiger Hilfe kann sowohl ein Grundgerüst im alten WinHelp-Format wie auch im neueren HTML-Hilfeformat erzeugen. Man kann die Druckunterstützung abschalten und Optionen zur Automation und zu ActiveX-Steuerelementen auswählen (diese beiden Optionen werden ausführlich in Kapitel 3, »COM, OLE und ActiveX«, beschrieben). Wird die Option MAPI (MESSAGING-API) ausgewählt, so lassen sich Dokumente des Programms per E-Mail verschicken. Die Option WINDOWS-SOCKETS muss ausgewählt werden, wenn das Programm das Internetprotokoll TCP/IP auf der Basis der Windows-Socket-Schnittstelle verwenden soll. Die Option AKTIVE EINGABEHILFEN fügt dem Projekt Unterstützung für die ActiveAccessibility-Schnittstellen von Windows hinzu (Eingabehilfen für Personen mit Behinderungen). Durch Auswahl der Option ALLGEMEINES STEUERELEMENTMANIFEST wird eine XML-Datei generiert, die die neuen allgemeinen Steuerlemente von Windows XP aktiviert. Weiterhin lässt sich die Anzahl der Dateien in der Liste der zuletzt geöffneten Dateien einstellen. Hier lassen sich Werte zwischen 0 und 16 auswählen. Ein Wert von 0 bedeutet, dass keine Liste mit zuletzt geöffneten Dateien angezeigt wird.
Auf der Karteikarte ERSTELLTE KLASSEN können die automatisch generierten Klassen- und Dateinamen verändert werden. Hier kann auch die Basisklasse der Ansichtsklasse ausgewählt werden. Hier hat man die Möglichkeit, statt der vorgegebenen Klasse CView eine der Klassen CEditView, CFormView, CListView, CRichEditView, CScrollView, CTreeView,CHtmlView oder CHtmlEditView als Basisklasse für die eigenen Ansichten auszuwählen. Wählt man eine von CView abweichende Klasse aus, so besitzt die Ansicht bereits eine vorgegebene Funktionalität. So implementiert zum Beispiel die Klasse CEditView einen vollständigen Editor und die Klasse CScrollView die Behandlung von Bildlaufleisten. Auf der Karteikarte ÜBERSICHT zeigt der Anwendungs-Assistent eine grobe Zusammenfassung der ausgewählten Optionen (Abbildung 2.24). Durch Klick auf die Schaltfläche FERTIG STELLEN werden die entsprechenden Klassen und Dateien erzeugt.
Abbildung 2.24: Anwendungs-Assistent, Übersicht
Nachdem alle Dateien erzeugt worden sind, kann das Projekt sogleich übersetzt werden. Zusätzlich zu den erzeugten Quelltextdateien wird auch eine Readme-Datei angelegt, die als erster Anhaltspunkt zur Erkundung des erzeugten Programmcodes dienen kann. Sie enthält eine Aufstellung der vom Anwendungs-Assistenten für das Projekt erzeugten Dateien mit einer Beschreibung des Inhalts der Dateien. Für das Projekt StockChart hat der Anwendungs-Assistent die in Tabelle 2.4 aufgeführten Dateien erstellt.
Ein Programm mit dem Anwendungs-Assistenten erstellen
Datei
Beschreibung
CHILDFRM.CPP
Implementierungsdatei des MDI-Rahmenfensters
CHILDFRM.H
Deklariert die Klasse des MDI-Rahmenfensters
MAINFRM.CPP
Implementierungsdatei des Hauptrahmenfensters
MAINFRM.H
Deklariert die Klasse des Hauptrahmenfensters
README .TXT
Enthält eine Beschreibung der wichtigsten Dateien
RESSOURCE .H
Definiert IDs verwendeter Ressourcen
STDAFX.CPP
Hilfsdatei für vorkompilierte Header-Dateien
STDAFX.H
Wird von den Implementierungsdateien eingebunden, enthält #include-Anweisungen für die MFC-Header-Dateien
STOCKCHART.CPP
Implementierungsdatei der Applikationsklasse
STOCKCHART.H
Deklariert die Applikationsklasse
STOCKCHART.ICO
Enthält das Icon der Anwendung
STOCKCHART.NCB
Enthält Informationen für den Klassenbrowser
STOCKCHART.RC
Ressourcenscript, enthält die Ressourcen der Anwendung oder Verweise darauf
STOCKCHART.RC2
Ressourcenscript, enthält Ressourcen, die nicht vom Developer Studio bearbeitet werden können
STOCKCHART.REG
Eine Datei mit Einträgen für die Registrierungsdatenbank (Registry)
STOCKCHART.SLN
Solution File (Projektdatei)
STOCKCHART.VCPROJ
Projektdatei (XML)
STOCKCHARTDOC .CPP
Implementierungsdatei der Dokumentenklasse
STOCKCHARTDOC .H
Deklariert die Dokumentenklasse
STOCKCHARTDOC .ICO
Enthält das Icon des Dokuments
STOCKCHARTVIEW.CPP
Implementierungsdatei der Ansichtsklasse
STOCKCHARTVIEW.H
Deklariert die Ansichtsklasse
TOOLBAR.BMP
Enthält alle Icons der Symbolleiste in einer Bitmap
57
Tabelle 2.4: Vom Anwendungs-Assistenten erzeugte Dateien
Der Anwendungs-Assistent erzeugt Dateinamen, die mit den Klassennamen übereinstimmen, die in diesen Dateien deklariert oder implementiert werden. Das große »C«, mit dem alle Klassennamen der MFC beginnen, wird im Dateinamen weggelassen.
Dateinamen
58
2
Einstieg in die MFC-Programmierung
Gegen diese Regel verstößt der Dateiname der Klasse des Applikationsobjekts, bei der das »App« des Klassennamens weggelassen wird. So wird die Klasse CStockChartApp in der Datei STOCKCHART.CPP implementiert. Gefallen einem die automatisch generierten Dateinamen nicht, so lassen sie sich auf der Karteikarte ERSTELLTE KLASSEN des Anwendungs-Assistenten ändern. Eine Ausnahme ist hier wieder die Applikationsklasse. Die Namen der Deklarations- und Implementierungsdatei sind fest vorgegeben und lassen sich nicht ändern. Registrierungsdatei
Die vom Anwendungs-Assistenten erzeugte Registrierungsdatei (STOCKCHART.REG in Tabelle 2.4) kann innerhalb eines Installationsprogramms für die erzeugte MFC-Anwendung verwendet werden. Die Registrierungsdatei macht den Dateityp der Anwendung dem System bekannt, so dass Dateien dieses Typs automatisch per Doppelklick oder Drag&Drop geöffnet werden können. Diese Registrierung kann allerdings auch von Programm selbst durch den Aufruf der Funktion RegisterShellFileTypes vorgenommen werden, wie in Abschnitt 2.4.2, »Die Applikationsklasse«, gezeigt wird.
Kommentare
Die vom Anwendungs-Assistenten erzeugten Kommentare sollen dem Programmierer helfen, die richtigen Stellen zu finden, an denen er das generierte Programm um eigene Funktionalität ergänzen kann. Ein typischer Kommentar sagt dem Programmierer, was er an dieser Stelle zu tun hat: // ZU ERLEDIGEN: Hier Code zum Laden einfügen
Natürlich muss bei weitem nicht an jeder Stelle, an der sich ein solcher Kommentar befindet, etwas eingefügt werden. Vielmehr bedeuten die Kommentare, dass an diesen Stellen bevorzugt Programmcode eingefügt werden kann.
2.4.2 CStockChartApp
Die Applikationsklasse
Als guter Startpunkt, um den erzeugten Programmcode zu untersuchen, bietet sich die Klasse des Applikationsobjekts an. Der Anwendungs-Assistent hat diese Klasse CStockChartApp getauft und die beiden Dateien STOCKCHART.H und STOCKCHART.CPP erzeugt. Die Header-Datei ist nicht sehr interessant, daher soll hier gleich die Implementierungsdatei gezeigt werden.
Ein Programm mit dem Anwendungs-Assistenten erstellen BEGIN_MESSAGE_MAP(CStockChartApp, CWinApp) ON_COMMAND(ID_APP_ABOUT, OnAppAbout) // Dateibasierte Standard-Dokumentbefehle ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew) ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen) // Standard-Druckbefehl "Seite einrichten" ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// / // CStockChartApp Konstruktion CStockChartApp::CStockChartApp() { // ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen // Alle wichtigen Initialisierungen in InitInstance platzieren } ////////////////////////////////////////////////////////////// / // Das einzige CStockChartApp-Objekt CStockChartApp theApp; ////////////////////////////////////////////////////////////// / // CStockChartApp Initialisierung BOOL CStockChartApp::InitInstance() { CWinApp::InitInstance(); // Standardinitialisierung // Wenn Sie diese Funktionen nicht nutzen und die Größe Ihrer // fertigen ausführbaren Datei reduzieren wollen, sollten Sie // die nachfolgenden spezifischen Initialisierungsroutinen, // die Sie nicht benötigen, entfernen. // Ändern des Registrierungsschlüssels, unter dem unsere // Einstellungen gespeichert sind. // Sie sollten dieser Zeichenfolge einen geeigneten Inhalt // geben wie z.B. den Namen Ihrer Firma oder Organisation. SetRegistryKey(_T("Addison Wesley Longman")); LoadStdProfileSettings(4); MRU)
// Dokumentvorlagen der Anwendung registrieren. // Dokumentvorlagen dienen als Verbindung zwischen // Dokumenten, Rahmenfenstern und Ansichten. CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_STockChartTYPE, RUNTIME_CLASS(CStockChartDoc), RUNTIME_CLASS(CChildFrame), // Benutzerspezifischer MDI// Child-Rahmen RUNTIME_CLASS(CStockChartView)); AddDocTemplate(pDocTemplate); // Haupt-MDI-Rahmenfenster erzeugen CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; m_pMainWnd = pMainFrame; // Öffnen per Drag&Drop aktivieren m_pMainWnd->DragAcceptFiles(); // DDE-Execute-Open aktivieren EnableShellOpen(); RegisterShellFileTypes(TRUE); // Befehlszeile parsen, um zu prüfen auf Standard// Umgebungsbefehle DDE, Datei offen CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Verteilung der in der Befehlszeile angegebenen Befehle if (!ProcessShellCommand(cmdInfo)) return FALSE; // Das Hauptfenster ist initialisiert und kann jetzt // angezeigt und aktualisiert werden. pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow(); return TRUE; } ////////////////////////////////////////////////////////////// / // CAboutDlg-Dialogfeld für Anwendungsbefehl "Info" class CAboutDlg : public CDialog { public: CAboutDlg();
Ein Programm mit dem Anwendungs-Assistenten erstellen // Dialogfelddaten enum { IDD = IDD_ABOUTBOX }; protected: // DDX/DDV-Unterstützung virtual void DoDataExchange(CDataExchange* pDX); // Implementierung protected: DECLARE_MESSAGE_MAP() }; CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD) { } void CAboutDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); } BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) END_MESSAGE_MAP() // Anwendungsbefehl, um das Dialogfeld aufzurufen void CStockChartApp::OnAppAbout() { CAboutDlg aboutDlg; aboutDlg.DoModal(); } Listing 2.1: Vom Anwendungs-Assistenten erzeugter Quelltext für die Applikationsklasse
Ganz oben im Listing befindet sich die Nachrichtenzuordnungstabelle für die Applikationsklasse. Nach dem leeren Konstruktor wird die globale Instanz des Applikationsobjekts, theApp, angelegt. Das Applikationsobjekt kann man übrigens jederzeit durch Aufruf der globalen Funktion AfxGetApp erhalten. Dann kommt die Implementierung der Funktion InitInstance. InitInstance wird beim Start des Programms aufgerufen. Neben InitInstance gibt es noch die Funktion InitApplication, die aber in 32-Bit-Programmen keine Bedeutung mehr hat, da nicht mehr zwischen verschiedenen Instanzen einer Applikation unterschieden wird. Analog zu InitInstance gibt es die Funktion ExitInstance, die vor dem Beenden des Programms aufgerufen wird. ExitInstance wird allerdings nicht automatisch vom Anwendungs-Assistenten überschrieben, daher taucht sie in der Datei STOCKCHART.CPP nicht auf.
61
62
2 Registrierung
Einstieg in die MFC-Programmierung
Nach dem Aufruf von InitInstance in der übergeordneten Klasse CWinApp, wird die Funktion SetRegistryKey aufgerufen. Diese sorgt dafür, dass alle Einstellungen, die das Programm mittels der Funktionen WriteProfileString und WriteProfileInt schreibt, in der Registrierungsdatenbank (Registry) gespeichert werden. Die Registrierungsdatenbank lässt sich mit dem Programm REGEDIT.EXE ansehen und verändern (Vorsicht: Unsachgemäße Änderungen können die Windows-Installation beschädigen!). Das Programm REGEDIT.EXE befindet sich im Windows-Verzeichnis. Mit den Funktionen GetProfileString und GetProfileInt können Werte aus der Registrierungsdatenbank zurückgelesen werden. Alle vier Funktionen sind Member-Funktionen der Klasse CWinApp. Als Parameter wird SetRegistryKey üblicherweise der Name der Firma übergeben, die die Software erstellt hat oder vertreibt. Der Anwendungs-Assistent trägt hier zunächst »Local AppWizard-Generated Applications« als Namen ein. Man sollte diesen Text durch einen eigenen Namen ersetzen, wie im Listing bereits geschehen. Einstellungen werden in der Registrierungsdatenbank nach folgendem Schema gespeichert: HKEY_CURRENT_USER\Software\\ \<Sektionsname>\<Wertname>
Die Funktionen WriteProfileString, WriteProfileInt, GetProfileString und GetProfileInt sind eigentlich nicht mehr notwendig. Sie werden einerseits aus Gründen der Kompatibilität mit alten MFC-Versionen, andererseits zum einfachen Zugriff auf die Registrierungsdatenbank genutzt. Zum universellen Zugriff auf die Registrierungsdatenbank gibt es eine Reihe von WIN32-Funktionen, die deutlich leistungsfähiger aber auch etwas umständlicher zu verwenden sind als die MFCFunktionen (unter anderem RegOpenKeyEx, RegQueryValueEx und RegCreateKeyEx). Man kann den Aufruf von SetRegistryKey auch löschen. In diesem Fall werden alle Einstellungen in einer INI-Datei im Windows-Verzeichnis gespeichert. Diese Datei trägt den Namen des Programms. Die Eintragungen in einer INI-Datei zu speichern war bis zur Windows-Version 3.11 üblich. Moderne Anwendungen sollten ihre Einstellungen allerdings in der Registrierungsdatenbank speichern.
t
Übrigens sorgt das beim Aufruf von SetRegistryKey verwendete Makro _T dafür, dass der übergebene String Unicode-kompatibel ist.
Ein Programm mit dem Anwendungs-Assistenten erstellen
Nach SetRegistryKey wird durch den Aufruf von LoadStdProfileSettings die Liste der zuletzt bearbeiteten Dokumente geladen. Optional kann man der Funktion die Anzahl der maximalen Einträge dieser Liste übergeben. Lässt man die Parameterliste leer, so werden maximal vier Dokumente in der Liste gespeichert (Abbildung 2.25).
Abbildung 2.25: Zuletzt bearbeitete Dokumente in der Registrierungsdatenbank
Danach wird die Dokumentenvorlage erzeugt und durch den Aufruf der Funktion AddDocTemplate in die Liste der verwendeten Dokumentenvorlagen eingetragen. Da StockChart als MDIAnwendung mehrere Dokumente gleichzeitig bearbeiten kann, wird eine Instanz der Klasse CMultiDocTemplate verwendet. Danach wird das Hauptrahmenfenster erzeugt. Der anschließende Aufruf von DragAcceptFiles erlaubt es dem Programm StockChart, Dokumente durch Hineinziehen des Dokuments mit der Maus und anschließendes Fallenlassen (Drag&Drop) zu öffnen. Der Aufruf von EnableShellOpen erlaubt schließlich auch das Öffnen von Dokumenten durch Doppelklick innerhalb des WindowsExplorers. Dies funktioniert auch, wenn StockChart bereits gestartet wurde. Allerdings muss dazu der Dateityp in der Registrierungsdatenbank eingetragen sein. Man kann dies durch eine Registrierungsdatei (*.REG), ein Installationsprogramm oder aber auch durch das Programm selbst erledigen lassen. Um die Registrierung vom Programm durchführen zu lassen, ist, wie im Beispielprogramm, die Funktion RegisterShellFileTypes aufzurufen. Schließlich wird die dem Programm übergebene Kommandozeile analysiert und darin eventuell vorhandene Kommandos werden ausgeführt. Das Hauptrahmenfenster wird angezeigt, und die Funktion InitInstance kehrt zurück. Der weitere Programmcode behandelt den Info-Dialog der Applikation. Nach der Lektüre des Abschnitts 2.7, »Dialogfeldprogrammierung«, sollte er auch ohne weitere Erklärungen leicht zu verstehen sein.
63
64
2
2.4.3 CMainFrame
Einstieg in die MFC-Programmierung
Das Hauptrahmenfenster
Nun zum Programmcode des Hauptrahmenfensters (MAINFRM.H und MAINFRM.CPP). Listing 2.2 zeigt den Quelltext der Datei MAINFRM.CPP. Hier ist insbesondere die Funktion OnCreate interessant. // MainFrm.cpp : Implementierung der Klasse CMainFrame // #include "stdafx.h" #include "StockChart.h" #include "MainFrm.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// // // CMainFrame IMPLEMENT_DYNAMIC(CMainFrame, CMDIFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd) ON_WM_CREATE() END_MESSAGE_MAP() static UINT indicators[] = { ID_SEPARATOR, // Statusleistenanzeige ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, }; ////////////////////////////////////////////////////////////// // // CMainFrame Nachrichten-Handler CMainFrame::CMainFrame() { // ZU ERLEDIGEN: Hier Code zur // Member-Initialisierung einfügen } CMainFrame::~CMainFrame()
Ein Programm mit dem Anwendungs-Assistenten erstellen { }
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Symbolleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators( indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Statusleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie // nicht wollen, dass die Symbolleiste andockbar ist. m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; } BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if ( !CMDIFrameWnd::PreCreateWindow(cs) ) return FALSE; // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse // oder das Erscheinungsbild, indem Sie // CREATESTRUCT cs modifizieren. return CMDIFrameWnd::PreCreateWindow(cs); }
In der Funktion OnCreate werden Symbol- und Statusleiste des Programms angelegt. Danach kann man das Andockverhalten der Symbolleiste verändern. Durch Löschen der letzten drei Zeilen der Funktion entsteht eine Symbolleiste, die nicht mehr andockbar ist. Man kann aber CBRS_ALIGN_ANY auch durch eine Kombination der Konstanten CBRS_ALIGN_TOP, CBRS_ALIGN_BOTTOM, CBRS_ALIGN_LEFT und CBRS_ALIGN_RIGHT ersetzen, so dass die Symbolleiste nur noch an den angegebenen Kanten des Rahmenfensters andockt. Die Verwendung von CBRS_ALIGN_TOP ist allerdings Pflicht, anderenfalls gibt es eine verletzte Zusicherung beim Erzeugen des Hauptfensters, das seine Symbolleiste beim Programmstart am oberen Ende andocken möchte.
2.4.4 Serialize
Die Dokumentenklasse
Der Programmcode der Dokumentenklasse befindet sich in den Dateien STOCKCHARTDOC.H und STOCKCHARTDOC.CPP. Der Anwendungs-Assistent hat bereits eine Funktion zur Serialisierung erstellt (Listing 2.3). void CStockChartDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // ZU ERLEDIGEN: Hier Code zum Speichern einfügen } else
Ein Programm mit dem Anwendungs-Assistenten erstellen { // ZU ERLEDIGEN: Hier Code zum Laden einfügen } } Listing 2.3: Leere Serialisierungsfunktion
Anhand des übergebenen CArchive-Objekts kann durch den Aufruf von IsStoring bestimmt werden, ob Objekte gelesen oder geschrieben werden sollen. Für das Beispiel StockChart wird hier der Programmcode eingefügt, um die Kursdaten zu laden und zu speichern. Die fertige Version von Serialize ist in Listing 2.4 zu sehen. void CStockChartDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_nID; ar << m_name; ar << m_ticker; ar << m_bGrid; ar << m_bAverage; ar << m_nAverageCnt; ar << m_nColor; } else { ar >> m_nID; ar >> m_name; ar >> m_ticker; ar >> m_bGrid; ar >> m_bAverage; ar >> m_nAverageCnt; ar >> m_nColor; } m_stockData.Serialize (ar); // Serialisierung der Datenliste! } Listing 2.4: Fertig implementierte Serialisierungsfunktion des Programms StockChart
Die Operatoren << und >> werden dazu verwendet, die MemberVariable der Klasse zu speichern oder zu lesen. Die Liste der Kursdaten m_stockData wird durch Aufruf ihrer eigenen Serialisierungsfunktion serialisiert. Die Implementierung dieser Funktion wird in Abschnitt 2.5.5, »Verwendung der Werkzeugklassen in StockChart«, besprochen.
67
68
2
Einstieg in die MFC-Programmierung
Um auf Ereignisse wie das Öffnen, Speichern oder Erstellen eines neuen Dokuments reagieren zu können, existiert innerhalb der Dokumentenklasse eine Reihe von virtuellen Funktionen, die vom Anwendungsgerüst aufgerufen werden (Tabelle 2.5). Funktion
Aufgabe
Standardimplementierung
OnNewDocument
Neuanlage eines Dokuments
Ruft DeleteContents auf und markiert das Dokument als nicht modifiziert.
OnOpenDocument
Öffnen eines Dokuments
Öffnet die Datei, ruft DeleteContents auf, ruft Serialize auf, um die Daten des Dokuments zu laden, und markiert anschließend das Dokument als nicht modifiziert.
OnCloseDocument
Schließen eines Dokuments
Ruft DeleteContents auf und schließt alle Ansichten des Dokuments.
OnSaveDocument
Speichern eines Dokuments
Öffnet die Datei, speichert die Daten des Dokuments mit Serialize und markiert anschließend das Dokument als nicht modifiziert.
Tabelle 2.5: Standardbehandlung von Dokumenten OnNewDocument
Der Anwendungs-Assistent überschreibt allerdings nur die Funktion OnNewDocument (Listing 2.5), die anderen müssen selbst überschrieben werden, falls man sie benötigt. BOOL CStockChartDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen // (SDI-Dokumente verwenden dieses Dokument) return TRUE; } Listing 2.5: Vom Anwendungs-Assistenten überschriebene Funktion OnNewDocument
DeleteContents
Bis auf OnSaveDocument rufen alle diese Funktionen in ihrer Standardimplementierung die virtuelle Funktion DeleteContents auf. Deren Aufgabe ist es, den Inhalt des aktuellen Dokuments zu löschen. Die Standardimplementierung der Funktion ist leer, der Programmierer muss die Funktion selbst überschreiben und implementieren. Die Implementierung dieser Funktion ist insbesondere in SDI-
Ein Programm mit dem Anwendungs-Assistenten erstellen
69
Programmen wichtig, da SDI-Programme die gleiche Instanz der Dokumentenklasse beim Laden oder bei der Neuanlage eines anderen Dokuments weiterbenutzen. Daher werden durch einen Aufruf der Funktion DeleteContents zunächst die alten Daten gelöscht. Für das Programm StockChart ist die Funktion OnOpenDocument überschrieben worden. Hier ist der Aufruf der Funktion CalcAverages eingefügt worden, um die Durchschnittslinie zu berechnen (Listing 2.6). Dies geschieht übrigens unabhängig davon, ob die Linie tatsächlich angezeigt wird. BOOL CStockChartDoc::OnOpenDocument(LPCTSTR lpszPathName) { if (!CDocument::OnOpenDocument(lpszPathName)) return FALSE; CalcAverages (); // Durchschnittslinie berechnen return TRUE; } Listing 2.6: Berechnung der Durchschnittslinie, nachdem das Dokument geladen wurde.
Um zu beschreiben, ob der Zustand eines Dokumentenobjekts mit seinem serialisierten Abbild in einer Datei übereinstimmt, gibt es das Flag m_bModified. Auf dieses Flag wird allerdings nicht direkt, sondern über die Funktionen SetModified und IsModified zugegriffen. Weiterhin gibt es noch die Funktion SaveModified, die aufgerufen wird, bevor das Dokument geschlossen wird. Ist das Dokument modifiziert worden, so fragt die Funktion den Benutzer, ob das veränderte Dokument gespeichert werden soll. Da die Funktion virtuell ist, kann sie durch eine eigene Implementierung ersetzt werden.
2.4.5
Die Ansichtsklasse
Für die Ansichtsklasse hat der Anwendungs-Assistent die Dateien STOCKCHARTVIEW.H und STOCKCHARTVIEW.CPP angelegt. Erster Anlaufpunkt hier ist die Funktion OnDraw, die zum Zeichnen der Ansicht aufgerufen wird. void CStockChartView::OnDraw(CDC* pDC) { CStockChartDoc * pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen // Daten hinzufügen } Listing 2.7: Leere Zeichenfunktion der Ansicht
OnDraw
70
2
Einstieg in die MFC-Programmierung
Damit OnDraw die Ansicht zeichnen kann, muss sie auf die Daten ihres zugehörigen Dokuments zugreifen können. Daher hat der Anwendungs-Assistent bereits den Aufruf der Funktion GetDocument eingebaut, mit der eine Ansicht jederzeit auf ihr Dokument zugreifen kann. Das Makro ASSERT_VALID ruft nur in der DebugVersion des Programms die Funktion AssertValid des Dokuments auf, um dessen Gültigkeit zu prüfen. Der Programmcode der vollständigen OnDraw-Funktion von StockChart wird im Listing 2.20 in Abschnitt 2.6.4, »Grafikausgabe in StockChart«, gezeigt. Die Debug-Möglichkeiten der MFC sind Thema des Abschnitts 2.10, »Fehlersuche mit den MFC«.
2.4.6
Erste Anlaufpunkte im erzeugten Quelltext
Um erste Experimente mit dem vom Anwendungs-Assistenten erzeugten Programm vorzunehmen, eignen sich insbesondere folgende Stellen im Programmcode: 왘 Die erste Anlaufstelle ist die Funktion OnDraw der Ansicht. Diese Funktion wird immer dann aufgerufen, wenn die Ansicht gezeichnet werden muss. OnDraw ist daher der Ausgangspunkt, um den Darstellungscode für die Ansicht zu entwickeln. 왘 Zum Aufbau der eigenen Datenmodelle sollte man beginnen, die Dokumentenklasse um eigene Datenstrukturen zu ergänzen. 왘 Um die eigenen Datenstrukturen im Dokument zu speichern, muss die Serialize-Funktion der Dokumentenklasse implementiert werden. Mit diesen wenigen Schritten lässt sich bereits ein Programm des Anwendungsgerüsts erzeugen, das eine sinnvolle Funktion bereitstellen kann. In der Praxis müssen allerdings noch einige Mittel zur Interaktion mit dem Benutzer, wie Menüeinträge und Dialoge, hinzugenommen werden. Wie StockChart diese Punkte umsetzt, wird im weiteren Verlauf des Kapitels gezeigt.
2.4.7
Zusammenfassung
Der Anwendungs-Assistent ist ein gutes Hilfsmittel, um schnell zu einem funktionsfähigen Programm zu kommen. Dieses generierte Programm ist ein guter Rahmen, um eigene Funktionalität zu imple-
Werkzeugklassen und Dateizugriff
mentieren. Programme, die der Anwendungs-Assistent erstellt, bauen – mit der Ausnahme dialogfeldbasierter Anwendungen – auf der Dokument-Ansicht-Architektur der MFC auf. Es wird kein starres Programmgerüst erzeugt, sondern durch vielfältige Optionen lassen sich die Gestalt und Funktionalität der generierten Anwendung verändern. Die Anwendung kann anschließend um Dinge wie Dialogklassen und Nachrichtenbehandlungsfunktionen erweitert werden. Wer zum ersten Mal den vom Anwendungs-Assistenten generierten Quelltext untersucht, wird sich vielleicht fragen, wo er in all den Dateien anfangen soll, das »eigene Programm« zu schreiben. Hat man jedoch die Strukturen des Anwendungsgerüsts erst einmal verstanden und kennt man die wichtigsten Anlaufstellen in den erzeugten Dateien, so fällt es zunehmend leichter, im Quelltext zu navigieren. Die vom Anwendungs-Assistenten erzeugte Programmstruktur folgt immer dem gleichen Schema. Wenn man Optionen an- oder abschaltet, verändert sich der erzeugte Programmcode lediglich an einigen Stellen entsprechend, es tauchen oft nur ein paar Zeilen Quelltext mehr oder weniger auf. Wer die Dokument-Ansicht-Architektur nicht nutzen möchte, der kann deren Verwendung seit Version 6.0 von Visual C++ abschalten. Die Dokument-Ansicht-Architektur nicht zu verwenden, kann bei sehr speziellen oder sehr großen Programmen sinnvoll sein, beispielsweise wenn eine eigene Architektur zur Datenorganisation verwendet werden soll. Für alle anderen Projekte bildet die Dokument-Ansicht-Architektur eine sinnvolle Grundlage.
2.5
Werkzeugklassen und Dateizugriff
Die MFC bestehen zum überwiegenden Teil aus Klassen, die entweder direkt auf das Windows-API zugreifen oder Teil des Anwendungsgerüsts sind. Neben diesen Klassen gibt es allerdings eine Reihe anderer Klassen, die unabhängig vom oder nur in schwacher Relation zum Betriebssystem häufig benötigte Datenstrukturen implementieren. Diese Hilfs- oder Werkzeugklassen sollen im folgenden Abschnitt vorgestellt werden. Die Werkzeugklassen lassen sich in vier Gruppen einteilen: 왘 Klassen für einfache Datentypen 왘 Klassen für Ansammlungen von Datentypen (Collections)
71
72
2
Einstieg in die MFC-Programmierung
왘 Klassen zum Dateizugriff 왘 Klassen zur Ausnahmebehandlung Jede dieser Gruppen wird im Folgenden vorgestellt.
2.5.1
Klassen für einfache Datentypen
Die Klassen für einfache Datentypen sind nicht von CObject abgeleitet, sie stehen für sich allein.
Die Klasse CPoint repräsentiert einen Punkt, wie er beispielsweise zur Grafikausgabe benötigt wird. CPoint ist die MFC-Entsprechung zu POINT, einer Struktur des Windows-API für Punkte. Tatsächlich ist CPoint sogar von der POINT-Struktur abgeleitet. Dies ist möglich, da unter C++ alle mit struct definierten Strukturen ebenfalls als Klassen behandelt werden. Der Unterschied zu class besteht darin, dass alle Member eines struct automatisch als public gekennzeichnet sind, während class davon ausgeht, dass nicht weiter spezifizierte Member als private definiert sind. CPoint wird in den MFC überall dort verwendet, wo im Windows-API die POINT-Struktur verwendet würde. Analog zu CPoint sind CSize und CRect die MFC-Umsetzungen der Strukturen SIZE und RECT, die zweidimensionale Größen und Rechtecke beschreiben.
CString
Interessanter als die oben genannten Klassen ist die Klasse CString. CString basiert seit der Version 7.0 der MFC auf der TemplateKlasse CStringT. CString ist ein Ersatz für die sonst unter Windows (und unter C) verwendeten NULL-terminierten Strings (char *, LPSTR usw.). CString bietet eine Reihe von Vorteilen gegenüber C Strings:
Werkzeugklassen und Dateizugriff
73
왘 CString implementiert eine dynamische Speicherverwaltung. Der Programmierer muss sich keine Gedanken um die maximale Länge des Strings machen. CString-Objekte können zudem nachträglich wachsen. 왘 CString bietet eine Reihe von komfortablen Operatoren und Funktionen zur Bearbeitung von Strings. Beispielsweise lassen sich CString-Objekte direkt zuweisen und vergleichen. Unhandliche Funktionen der C-Laufzeitbibliothek wie strcpy und strcmp werden nicht mehr gebraucht. 왘 CString besitzt einen Operator, der einen konstanten, NULLterminierten C-String (LPCTSTR) zurückgibt. Damit lassen sich CString-Objekte direkt an Windows-API-Funktionen übergeben, die solche Strings erwarten. Viele Funktionen der MFC akzeptieren CString-Objekte direkt. Listing 2.8 zeigt beispielhaft die Verwendung von CString. // --- CString-Beispiel --CString s1; CString s2("Arthur Dent"); s1 s1 s2 s1
= s2; = "Ford Prefect"; += '!'; += " & " + s2;
// leerer String // aus Stringkonstante
// Zuweisung // Zuweisung einer Konstanten // Anhängen eines Zeichens // Verkettung von Strings // s1 = "Ford Prefect & Arhtur Dent!"
Listing 2.8: Beispiele für die Verwendung der String-Klasse
Seit Version 7.0 der MFC wird CString auf Basis der TemplateKlasse CStringT definiert. Der Klasse CStringT wird als TemplateParameter der Typ der zu verwendenden Zeichen übergeben, die der String zur Zeichendarstellung verwenden soll. Dieser kann beispielsweise CHAR oder TCHAR sein. CString ist als CStringT mit dem vorgegebenen Template-Parameter TCHAR definiert worden. Durch diese Definition bleibt die Rückwärtskompatibilität von CString mit den vorherigen Implementierungen der MFC gewährleistet. CStringT ist unabhängig von den MFC, somit können CString-Objekte auch außerhalb von MFC-Programmen ver-
74
2
Einstieg in die MFC-Programmierung
wendet werden. Die Klasse CStringT ist selbst von CSimpleStringT abgeleitet. Diese Klasse implementiert die grundlegende Funktionalität von Strings. Durch den Template-Parameter von CStringT können Strings auf der Basis von char, wchart_t oder TCHAR gebildet werden. Definiert sind bereits die Klassen CStringA und CStringW zur Behandlung von ANSI- und Unicode-Zeichensätzen. Die Klasse CString kann, wie in vorhergehenden Versionen der MFC, sowohl ANSI- als auch Unicode-Zeichen speichern. Die Klasse CFixedStringT dient zur performanten Behandlung von Strings, die eine vorher definierte Maximallänge nicht überschreiten. Allerdings können auch Objekte der Klasse CFixedStringT dynamisch wachsen, in diesem Fall ist die Performance von Objekten der Klasse CFixedStringT nicht besser als bei Objekten der Klasse CString. CTime, CTimeSpan
Die Klassen CTime und CTimeSpan vereinfachen die Arbeit mit Zeiten und Zeitspannen. CTime basiert auf dem C-Datentyp __time64_t. Mit CTime lassen sich die aktuelle Zeit, das aktuelle Datum und der aktuelle Wochentag bestimmen. CTimeSpan kann intern als Sekunden repräsentierte Zeitspannen in verschiedene Ausgabeformate konvertieren. Der verfügbare Datumsbereich der Klasse CTime reicht vom 1.1.1970 bis zum 31.12.3000, die Auflösung beträgt eine Sekunde. Bei MFC-Versionen vor 7.0 reicht der Datumsbereich nur bis zum 18.1.2038, da mit dem Datentypen time_t gearbeitet wurde.
2.5.2
Klassen für Ansammlungen von Datentypen
In den MFC gibt es drei Gruppen für Ansammlungen von Datentypen (auch Collections genannt): Arrays, Listen und Maps. Im MFC-Sprachgebrauch wird eine solche Gruppe als Architektur oder Shape bezeichnet. Allen drei Gruppen ist gemeinsam, dass sie jeweils eine Ansammlung gleichartiger Objekte speichern. Die Gruppen unterscheiden sich nicht dadurch, was sie für Objekte speichern, sondern dadurch, wie sie diese speichern und wie auf sie zugegriffen werden kann. 왘 Bei Arrays wird auf die darin gespeicherten Elemente über einen Index zugegriffen. Im Gegensatz zu C-Arrays haben MFC-Arrays keine feste Größe, sondern können diese während ihrer Lebensdauer ändern. Um die Speicherverwaltung muss sich der Programmierer nicht kümmern. In der Debug-Version des Programms werden zusätzlich die Array-Grenzen auf Über-
Werkzeugklassen und Dateizugriff
schreitung hin geprüft. In der Release-Version findet diese Überprüfung aus Laufzeitgründen nicht statt.
CObject CArray (Template) CByteArray CDWordArray CObArray CPtrArray CStringArray CUIntArray CWordArray Abbildung 2.27: Array-Klassen in den MFC
왘 Listen sind Datenstrukturen, bei denen die Datenelemente durch Zeiger miteinander verkettet sind. Listen innerhalb der MFC sind doppelt verkettet, das heißt, man kann sowohl vorwärts als auch rückwärts über die Liste iterieren. Die aktuelle Position in der Liste wird durch eine Variable des Typs POSITION repräsentiert.
CObject CList (Template) CPtrList CObList CStringList Abbildung 2.28: Listenklassen in den MFC
왘 Maps (in anderen Klassenbibliotheken auch Dictionaries genannt) sind Ansammlungen aus Verknüpfungen (Associations). Jede Verknüpfung besteht aus einem Schlüssel und aus einem Wert. Der Schlüssel wird dazu benutzt, den Wert innerhalb der Map wiederzufinden. Intern verwenden Maps dazu eine Technik, die als Hashing (Streuspeicherung) bezeichnet wird.
75
76
2
Einstieg in die MFC-Programmierung
CObject CMap (Template) CMapWordToPtr CMapPtrToWord CMapPtrToPtr CMapWordToOb CMapStringToPtr CMapStringToOb CMapStringToString Abbildung 2.29: Map-Klassen in den MFC
Zu jeder der drei Gruppen gibt es eine ganze Reihe von Klassen, die jeweils unterschiedliche Typen von Datenelementen verwenden. Template-basierte Klassen
Für jede der drei Gruppen gibt es eine Template-basierte Klasse. Durch die Verwendung von Templates kann man die Datentypen, die eine Ansammlung haben sollen, selbst bestimmen. Wozu sind dann noch die Klassen für spezielle Datentypen da? In der Tat werden diese Klassen nicht mehr benötigt. Microsoft empfiehlt die Verwendung der Template-basierten Klassen. Alle anderen Klassen stammen aus einer Zeit, als der Microsoft-C++-Compiler noch keine Templates übersetzen konnte; sie sind aus Kompatibilitätsgründen zu alten MFC-Programmen vorhanden. Es steht zwar nicht zu befürchten, dass Microsoft die nicht Template-basierten Klassen aus den MFC entfernt, doch sollte man insbesondere Klassen, die mit typlosen Zeigern (void *, beispielsweise in CPtrArray) arbeiten, nach Möglichkeit nicht benutzen. Die Programmierung mit Templates ist erheblich sicherer als die mit typlosen Zeigern, denn bei typlosen Zeigern wird die Typprüfung des Compilers einfach abgeschaltet – ein Relikt aus den alten Zeiten der C-Programmierung. Für den Fall, dass auf Zeiger nicht verzichtet werden kann, bieten die MFC die Klassen CTypedPtrArray, CTypedPtrList und CTypedPtrMap, die die Verwendung von Ansammlungen mit Zeigern sicherer machen.
Werkzeugklassen und Dateizugriff
77
Listing 2.9 demonstriert die Verwendung von Arrays, Listen und Maps. // --- CArray-Beispiel CArray array; int b, c; array.SetSize (4); array[3] = 17; b = array[3]; array.Add (42); c = array.GetSize ();
// // // // //
das Array hat jetzt 4 Elemente wie SetAt wie GetAt wird ans Ende angehängt c ist 5
// --- CList-Beispiel --CList list; POSITION pos; int m; list.AddHead (1); list.AddHead (2); list.AddTail (3);
// -1// -2-1// -2-1-3-
pos = list.GetHeadPosition (); // Positioniere auf Anfang der Liste m = list.GetNext (pos); // m = 2 m = list.GetNext (pos); // m = 1 list.InsertBefore (pos,4); // -2-1-4-3// --- CMap-Beispiel --CMap map; CString str; CString c1("Mars"),c2("Venus"),c3("Erde"); map.SetAt(1, c1); map[2] = c2; map[3] = c3; map.Lookup (2, str);
// wie SetAt // str = "Venus"
Listing 2.9: Verwendung von CArray, CList und CMap
2.5.3
Klassen zum Dateizugriff
Dateien werden in den MFC durch die Klasse CFile repräsentiert. Von CFile werden eine Reihe weiterer Klassen abgeleitet, die zum Teil sehr spezielle Aufgaben übernehmen. Das folgende Diagramm zeigt alle von CFile abgeleiteten Klassen:
Für den Zugriff auf Dateien im herkömmlichen Sinne, das Lesen und Schreiben der Daten einer Datei, die sich im Dateisystem des Computers befindet, sind lediglich zwei Klassen zuständig: CFile selbst und CStdioFile. Alle anderen Klassen haben Spezialaufgaben und sollen an dieser Stelle nicht besprochen werden. Die Klasse CFile führt ungepufferte Zugriffe auf Dateien aus, während CStdioFile die gepufferte Version darstellt. Im Normalfall wird man daher CStdioFile verwenden. CStdioFile unterstützt zusätzlich zu CFile das Lesen und Schreiben von Textdateien mittels der Funktionen ReadString und WriteString. Die Verwendung von CStdioFile wird in Listing 2.16 anhand des Programms StockChart gezeigt.
2.5.4 Ausnahmen in C++
Klassen zur Ausnahmebehandlung
Ausnahmen sind ein Mechanismus der Sprache C++, mit dem die Behandlung von Fehlern eleganter gelöst werden kann als durch die Rückgabe und Auswertung von Fehlercodes. Die Ausnahmebehandlung erfolgt mittels der drei reservierten Wörter try, catch und throw nach dem im Listing 2.10 gezeigten Schema.
Werkzeugklassen und Dateizugriff
79
... try { // Code, der einen Fehler verursachen könnte ... // falls Fehler vorliegt: throw ("exception!"); ... } catch (char *exception) { // Fehler behandeln oder Fehlermeldung ausgeben // Exception löschen, falls notwendig } Listing 2.10: Ausnahmebehandlung in C++
Der Programmcode, der einen Fehler auslösen könnte, befindet sich im try-Block. Sollte tatsächlich ein Fehler auftreten, so wird mittels des reservierten Worts throw eine Ausnahme ausgelöst. Der Programmfluss wird an dieser Stelle abgebrochen und im catchBlock weitergeführt. Dem catch-Block wird die Ausnahme, die throw »geworfen« hat, als Parameter übergeben. Die Fehlerbehandlung wird im catch-Block durchgeführt. Zwei Dinge sind zum Verständnis wichtig: throw muss nicht direkt im try-Block stehen, sondern kann sich auch in einer vom tryBlock aufgerufenen Funktion befinden. Falls eine Ausnahme ausgelöst wird, »hangelt« das Programm sich so lange durch die Aufrufkette der Funktionen zurück, bis es einen passenden catchBlock findet. Man sollte wissen, dass eine Ausnahme eine ganz normale Variable eines beliebigen Typs ist. Sie muss nicht, wie in Listing 2.10 gezeigt, den Typ char * besitzen. Daraus folgt, dass die Ausnahmevariable im catch-Block freigegeben werden muss, wenn sie zuvor auf dem Heap, also mittels des Operators new, erzeugt worden ist.
Typen einer Ausnahme
Dieser Mechanismus zur Fehlerbehandlung hat zwei Vorteile gegenüber der Verwendung von Fehlerrückgabewerten: Der Programmcode zur Fehlerbehandlung befindet sich nur an einer Stelle. Alle Funktionen, die eine Ausnahme herbeiführen können, lassen sich im gleichen try-Block aufrufen. Es muss nicht nach jeder Funktion eine Fehlerabfrage vorgenommen werden. Außerdem braucht sich der Programmierer keine Gedanken darüber zu machen, wie er einen Fehlerwert in einer Reihe von Funktionsaufrufen zurücktransportiert. Der Programmlauf wird automatisch im passenden catch-Handler weitergeführt.
Vorteile von Ausnahmen
80
2
Einstieg in die MFC-Programmierung
Man sollte darauf achten, dass jede mögliche Ausnahme innerhalb von try/catch-Blöcken abgefangen wird. Zwar kann der Compiler überprüfen, ob es zu einem try-Block auch einen catch-Block gibt, doch kann er nicht bestimmen, ob es für jede throw-Anweisung ein try/catch-Paar gibt. Nicht abgefangene Ausnahmen führen zu einem unsanften Abbruch des Programms, wie in Abbildung 2.31 zu sehen ist.
Abbildung 2.31: Programmabbruch durch nicht abgefangene Ausnahme
Ausnahmen können zu Problemen bei der Freigabe von Speicherbereichen führen. Verlässt man eine Funktion beim Auftreten eines Fehlers durch eine Ausnahme, so wird der Programmcode zur Speicherfreigabe nicht mehr ausgeführt. Um dieses Problem zu lösen, ist eine automatische Freigabe des Speichers wünschenswert. Eine Möglichkeit zur Lösung des Problems bietet sich in Form von automatischen oder »intelligenten« Zeigern an. Diese geben den Speicherbereich, auf den sie zeigen, automatisch frei, sofern der Sichtbarkeitsbereich (Scope) eines Programmbereichs verlassen wird. Das Prinzip von intelligenten Zeigern oder Smart Pointern wird ausführlich im Abschnitt 3.3.7, »Das Beispielprogramm MFCAutoClient2«, erläutert. Ausnahmen in der MFC
Alle Ausnahmen, die in den MFC verwendet werden, sind von der abstrakten Oberklasse CException abgeleitet, wie das Klassendiagramm in Abbildung 2.32 zeigt. Ausnahmen werden in den MFC auf dem Heap angelegt. Das heißt, sie müssen im catch-Block vom Programmierer freigegeben werden. Die Ausnahme darf nicht durch den delete-Operator gelöscht werden, sondern muss mittels CException::Delete freigegeben werden. Um Informationen zum vorliegenden Fehler anzufordern und anzuzeigen, gibt es die beiden Funktionen CException::GetErrorMessage und CException::ReportError. GetErrorMessage liefert die Fehlerbeschreibung in einem C-String zurück, während ReportError
Werkzeugklassen und Dateizugriff
diesen Fehlertext gleich in einer Messagebox anzeigt. Ein Beispiel zur Fehlerbehandlung innerhalb der MFC könnte etwa wie in Listing 2.11 aussehen.
CObject CException CArchiveException CDaoException CDBException CFileException CInternetException COleException COleDispatchException CSimpleException CMemoryException CNotSupportedException CResourceException CUserException Abbildung 2.32: Diagramm der Ausnahmeklassen ... try { ... // Rufe MFC-Funktion auf, die den Fehler verursachen könnte: SomeMFCFunction (); ... } catch (CException *e) { // CException ist durch abgeleitete // Klasse zu ersetzen ... e->ReportError (); e->Delete (); } Listing 2.11: Ausnahmebehandlung in den MFC
81
82
2
Einstieg in die MFC-Programmierung
Als Relikt aus früheren Versionen existiert in den MFC ein Satz von Makros, die einen ähnlichen Ausnahmemechanismus implementiert hatten, noch bevor der Microsoft-C++-Compiler die Ausnahmebehandlung unterstützt hat. Diese Makros verwenden in der aktuellen Version der MFC die Implementierung des Compilers.
2.5.5
Verwendung der Werkzeugklassen in StockChart
Die Werkzeugklassen werden an zwei Stellen innerhalb von StockChart verwendet: Die Reihe der Kurse ist als Liste vom Typ CList implementiert und zum Dateiimport wird die Klasse CStdioFile verwendet. Doch zunächst zur Implementierung der Kursliste: CStockData
In STOCKCHARTDOC.H wird die Klasse CStockData deklariert, die die Liste der Kursdaten enthält (siehe Listing 2.12). class CStockData { public: double min, max; CList<double,double> theData; void Serialize (CArchive& ar); }; Listing 2.12: Die Klasse CStockData
Wie man sieht, werden die Kursdaten in einer Liste aus Werten des Typs double gespeichert. Um die Kurskurve bei der Ausgabe optimal der Zeichenfläche anpassen zu können, werden zusätzlich der minimale und der maximale Kurswert gespeichert. Serialize dient zum Laden und Speichern von CStockData-Objekten. Zum Speichern der Liste selbst wird einfach deren Serialize-MemberFunktion (Listing 2.13) aufgerufen. void CStockData::Serialize (CArchive& ar) { if (ar.IsStoring ()) { ar << min; ar << max; } else { ar >> min; ar >> max; } theData.Serialize (ar); } Listing 2.13: Die Serialisierungsfunktion von CStockData
Werkzeugklassen und Dateizugriff
83
Bei der Ausgabe der Kurve in CStockChartView::OnDraw muss über die Liste iteriert werden, um alle Daten zeichnen zu können (Listing 2.14). ... // Anzahl der Listeneinträge ermitteln int nCnt = pDoc->m_stockData.theData.GetCount (); if (nCnt > 0) { POSITION pos; // hält Position in der Liste double scaleUpFactor; double minValue, value; // An den Anfang der Liste positionieren pos = pDoc->m_stockData.theData.GetHeadPosition (); scaleUpFactor = 10000.0 / (pDoc->m_stockData.max -pDoc >m_stockData.min); minValue = pDoc->m_stockData.min; // Wert holen und ein Element weitergehen value = pDoc->m_stockData.theData.GetNext (pos); [...] // hier Zeichnung beginnen for (int i=1; im_stockData.theData.GetNext (pos); [...] // hier Linien zeichnen } // for } // if ... Listing 2.14: Verwendung des CStockData-Objekts in CStockView::OnDraw
Die aktuelle Position innerhalb der Liste wird in der Variablen pos vom Typ POSITION gespeichert. Die Funktion CList::GetNext inkrementiert diese Variable, nachdem sie das aktuelle Listenelement zurückgegeben hat. Aufgebaut wird die Kursdatenliste – sofern sie nicht per Serialisierung geladen wird – in der Funktion CStockChartDoc::OnFileImport. Der Zweck des Imports ist es, Kursdaten aus einfachen Textdateien in das von StockChart durch die Serialisierung vorgegebene Dateiformat zu übernehmen. OnFileImport erwartet für jeden Kurswert eine eigene Textzeile, in der sich eine Fließkommazahl befindet. Eine Kursdatei könnte beispielsweise wie im Listing 2.15 aussehen (Kursdateien befinden sich auf der Begleit-CD im Verzeichnis DATEN\IMPORT):
Dateiimport
84
2
Einstieg in die MFC-Programmierung
233.850000 238.000000 238.500000 238.700000 241.900000 Listing 2.15: Beispiel einer Kursdatei für StockChart
Die Kurswerte der Textdatei werden von OnFileImport zeilenweise gelesen und in die Kursdatenliste eingetragen (Listing 2.16). void CStockChartDoc::OnFileImport() { // Dateidialog zum Importieren: CFileDialog fileDialog(true, NULL, NULL, NULL, "Textdateien (*.txt)|*.txt | Alle Dateien (*.*)|*.*||"); // wenn der Benutzer den Dialog mit OK verlassen hat: if (IDOK == fileDialog.DoModal ()) { // zunächst alte Daten löschen DeleteContents (); try { CStdioFile file (fileDialog.GetPathName (), CFile::typeText); CString line; double value, minVal, maxVal; file.ReadString (line); minVal = atof (line); maxVal = minVal; while (file.ReadString (line)) { value = atof (line); minVal = min (minVal, value); maxVal = max (maxVal, value); m_stockData.theData.AddTail (value); } m_stockData.min = minVal; m_stockData.max = maxVal; // Durchschnittslinie berechnen CalcAverages (); // Alle Ansichten aktualisieren UpdateAllViews (NULL); // als verändert kennzeichnen SetModifiedFlag (); }
Werkzeugklassen und Dateizugriff
85
catch (CFileException *e) { e->ReportError (); e->Delete (); return; } } } Listing 2.16: Dateiimport mit der Funktion OnFileImport
Nebenbei zeigt OnFileImport, wie man mit den MFC einen Standarddateidialog öffnen kann. Darauf wird genauer im Abschnitt 2.7, »Dialogfeldprogrammierung«, eingegangen. Bevor auf die Kursdatei zugegriffen wird, löscht ein Aufruf der Funktion DeleteContents (Listing 2.17) eine eventuell im Dokument befindliche Kursdatenreihe. void CStockChartDoc::DeleteContents() { // Inhalt der Liste löschen: m_stockData.theData.RemoveAll (); CDocument::DeleteContents(); } Listing 2.17: Implementierung von DeleteContents
Wäre StockChart ein SDI-Programm, dann müsste DeleteContents auch unbedingt implementiert werden, damit beim Laden einer Datei oder der Neuanlage eines Dokuments die alten Kursdaten gelöscht werden. Bei MDI-Programmen wie StockChart ist das unkritisch, da hier stets neue Instanzen der Dokumentenklasse erzeugt werden, anstatt, wie bei SDI-Programmen, Instanzen der Dokumentenklasse wiederzuverwenden. Trotzdem wird DeleteContents auch im Programm StockChart benötigt, nämlich genau dann, wenn Kursdaten in ein Dokument importiert werden, das bereits eine nicht leere Kursdatenreihe enthält. Der gesamte Programmcode zum Zugriff auf die Kursdatei befindet sich innerhalb eines try-Blocks, da Fehler im Dateizugriff jederzeit eine Ausnahme des Typs CFileException auslösen können. Die Kursdatei wird bereits im Konstruktor von CStdioFile als Textdatei geöffnet. Man kann Dateiobjekte auch so anlegen, dass sie erst durch Aufruf der Funktion CFile::Open geöffnet werden. Dazu besitzen CFile und CStdioFile Konstruktoren, die keine Dateinamen erwarten. Mit der Funktion CStdioFile::ReadString wird jeweils eine Zeile der Kursdatei gelesen, in Fließkommawerte umgewandelt
DeleteContents
86
2
Einstieg in die MFC-Programmierung
und durch Aufruf der Funktion CList::AddTail an das Ende der Kursliste angehängt. Zusätzlich werden das Minimum und das Maximum für die eingelesene Kursdatei bestimmt. Nach dem Einlesen der Kursdaten wird zunächst die Durchschnittslinie durch den Aufruf der Funktion CalcAverages berechnet. CalcAverages zeigt noch einmal, wie man über eine Liste iteriert (Listing 2.18). Nach der Berechnung der Durchschnittslinie werden alle Ansichten aufgefordert, sich neu zu zeichnen, und das Dokument wird als verändert gekennzeichnet. void CStockChartDoc::CalcAverages () { int nCnt = m_stockData.theData.GetCount (); POSITION outerPos, innerPos; double value; m_averageData.RemoveAll (); // auf den Anfang der Datenliste setzen outerPos = m_stockData.theData.GetHeadPosition (); for (int i=0; i
2.5.6
Zusammenfassung
Die Werkzeugklassen der MFC bieten eine überschaubare Auswahl an nützlichen Klassen zur Verwendung allgemeiner Datenstrukturen. Häufig anzutreffen ist in MFC-Programmen die Klasse CString (beziehungsweise der Template-Klasse CStringT). Mit ihrer Hilfe kann an vielen Stellen auf die unsicheren und umständlich zu handhabenden C-Strings verzichtet werden. Die kleine, aber sinnvolle Auswahl an Klassen zur Speicherung von Datenansammlungen ist durch ihre Template-Versionen für beliebige Datentypen verwendbar. Die Datenstrukturen können ihre Größe zur Laufzeit ändern, wobei die dazu notwendige Speicherverwaltung dem Programmierer abgenommen wird. Schließlich wird durch die
Grafikausgabe und Drucken
87
Verwendung von Ausnahmeklassen innerhalb der MFC die Fehlerbehandlung eleganter und übersichtlicher realisiert, als dies durch die Verwendung von Fehlerrückgabewerten möglich wäre.
2.6
Grafikausgabe und Drucken
Um grafische Elemente wie Linien, Farbflächen, Text und Bilder (Bitmaps) auszugeben, besitzt Windows das Graphics Device Interface (GDI). Das GDI ist ein Grafiksystem, das unabhängig vom verwendeten Ausgabegerät arbeitet; es abstrahiert von der vorhandenen Hardware. Egal ob man mit dem GDI eine Linie auf dem Bildschirm oder auf einem Drucker ausgeben möchte, der zum Zeichnen der Linie benötigte Programmcode ist genau gleich. Allenfalls bei der Vorbereitung der Zeichenoperation ergeben sich ein paar Unterschiede. Die unterschiedliche Funktionsweise der Ausgabe-Hardware wird vom GDI und den darunter liegenden Hardware-Treibern vollständig verdeckt. Obwohl diese Vorgehensweise heute bei allen grafischen Betriebssystemen Standard ist, war dies zur Zeit der ersten Windows-Versionen keinesfalls so. MSDOS-Programme mussten jede Grafikkarte gesondert behandeln.
2.6.1
GDI-Objekte und Gerätekontexte
Innerhalb des GDI gibt es verschiedene Werkzeuge, um grafische Objekte auszugeben. Diese Werkzeuge werden innerhalb der MFC durch eine Anzahl von Klassen dargestellt. Die Basisklasse dieser Werkzeuge ist CGdiObject. Folgende Zeichenwerkzeuge stehen zur Verfügung (Abbildung 2.33 zeigt den entsprechenden Ausschnitt aus dem MFC-Klassenbaum): CObject CGdiObject CBitmap CBrush CFont CPalette CPen CRgn Abbildung 2.33: Klassen für GDI-Objekte
GDI
88
2
Einstieg in die MFC-Programmierung
왘 Bitmaps. Bitmaps sind zweidimensionale Arrays aus Bildschirmpunkten, die rechteckige Ausschnitte der Zeichenoberfläche eines Ausgabegeräts darstellen. Neben der Darstellung von in Dateien gespeicherten Bildern können Bitmaps für einige andere Zwecke, wie beispielsweise das Zeichnen im Hintergrund (Offscreen) oder zur Verschiebung des Inhalts der Zeichenoberfläche (Scrolling), verwendet werden. Die MFCKlasse für Bitmaps heißt CBitmap. 왘 Pinsel (Brush). Für Pinsel gibt es die MFC-Klasse CBrush. Pinsel dienen zum Ausfüllen der Fläche von geometrischen Figuren. Pinsel besitzen eine Farbe und ein Muster. Das Muster kann aus einer Anzahl vordefinierter Muster ausgewählt oder mittels einer Bitmap definiert werden. 왘 Schriften (Fonts). Text kann mit Hilfe des GDI pixelgenau an beliebigen Stellen der Zeichenfläche ausgegeben werden. Zur Ausgabe können alle im Windows-System installierten Schriftarten verwendet werden. Der Text selbst besitzt keine eigene Repräsentation im GDI, jedoch wird die zur Ausgabe von Text verwendete Schriftart und deren Eigenschaften durch die MFC-Klasse CFont festgelegt. 왘 Paletten. Paletten sind Tabellen von Farbzuordnungseinträgen. Mit Hilfe von Paletten können Geräten, die in der Anzahl von Farben, die sie gleichzeitig ausgeben können, beschränkt sind, genau die Farben zugeordnet werden, die die geringsten Farbverfälschungen hervorrufen. Paletten werden unter Windows zur Ausgabe von Bitmaps – insbesondere mit fotografischem Inhalt – im 256-Farben-Modus verwendet. In höher auflösenden Farbmodi werden keine Paletten benötigt, da dort die Farben direkt ausgewählt werden. Für Paletten gibt es die MFCKlasse CPalette. 왘 Stifte (Pen). Stifte werden durch die MFC-Klasse CPen repräsentiert. Sie dienen zum Zeichnen von Linien, Punkten und Außenlinien von geometrischen Figuren wie Rechtecken und Kreisen. Stifte besitzen eine Stärke, eine Farbe und eine Linienart (durchgehend, gestrichelt, gepunktet). 왘 Regionen. Regionen werden durch die Klasse CRgn repräsentiert und stellen Flächen auf der Ausgabeoberfläche des Ausgabebereichs dar. Sie können aus einem Rechteck, einer Ellipse oder einem oder mehreren Polygonen bestehen. Mehrere Regi-
Grafikausgabe und Drucken
89
onen können zu einer komplexen Region zusammengefasst werden. Regionen werden zumeist zum Clipping verwendet. Alle diese GDI-Objekte können nicht direkt verwendet werden. Sie sind nur innerhalb eines so genannten Gerätekontexts oder Device Context (DC) gültig. Ein Gerätekontext stellt eine Umgebung zur Ausgabe von Grafik bereit. Die Zeichenoberfläche eines Gerätekontexts besteht meist aus dem Client-Bereich eines Fensters, es kann aber auch das gesamte Fenster einschließlich des Menüs und des Rahmens oder aber der Druckbereich eines Druckers sein. Ein Gerätekontext muss von Windows angefordert werden, bevor mit dem Zeichnen begonnen werden kann. Innerhalb der MFC wird ein Gerätekontext an vielen Stellen automatisch bereitgestellt.
Gerätekontexte
In den MFC werden Gerätekontexte durch Objekte der Klasse CDC repräsentiert. Für spezielle Aufgaben gibt es eine Reihe von Klassen, die von CDC abgeleitet sind: CClientDC, CMetaFileDC, CPaintDC und CWindowDC. Da eine Grafikausgabe nur innerhalb eines Gerätekontexts möglich ist, sind sinnvollerweise alle Funktionen zur Grafikausgabe Member-Funktionen von CDC. Abbildung 2.34 zeigt alle in den MFC zur Verfügung stehenden Gerätekontextklassen.
Gerätekontexte sind, wie andere GDI-Objekte auch, eine Ressource, die nicht in unbegrenzter Anzahl zur Verfügung steht. Genauso wie ein Programm keine Speicherlecks haben sollte, darf es auch keine Gerätekontexte oder andere GDI-Objekte verlieren. Der Verlust einer größeren Menge von Gerätekontexten oder GDIObjekten äußert sich meist nach relativ kurzer Zeit durch ein auffälliges Verhalten des Systems. Teile von Fenstern werden nicht mehr richtig gezeichnet, Schriftarten falsch dargestellt und teil-
90
2
Einstieg in die MFC-Programmierung
weise erfolgen Grafikausgaben gar nicht mehr. Um den Verlust von GDI-Objekten und insbesondere von Gerätekontexten zu vermeiden, sollten diese immer auf dem Stack erzeugt werden. Bei Objekten der Klasse CDC wird der Gerätekontext vom Konstruktor bei Windows angefordert, während der Destruktor, der automatisch beim Verlassen der Funktion aufgerufen wird, wenn das CDC-Objekt vom Stack genommen wird, den Gerätekontext wieder freigibt. GDI-Objekte werden analog behandelt. In jedem Gerätekontext ist jeweils genau ein Objekt der oben genannten GDI-Klassen aktiv. Der Gerätekontext enthält, wenn er erzeugt wird, sinnvoll gewählte Vorgaben, wie beispielsweise einen schwarzen Stift und einen weißen Pinsel. Um die vorgegebenen Objekte durch eigene auszutauschen, besitzt der Gerätekontext die Member-Funktion CDC::SelectObject. SelectObject tauscht ein im Gerätekontext vorhandenes Objekt gegen ein der Funktion übergebenes Objekt gleichen Typs aus. Das zuvor im Gerätekontext befindliche Objekt wird von SelectObject zurückgegeben. Im Windows-Sprachgebrauch werden GDI-Objekte in einen Gerätekontext hineinselektiert. Das von SelectObject zurückgegebene Objekt sollte gespeichert und am Ende der Grafikausgabe in den Gerätekontext zurückselektiert werden, damit sich dieser wieder in seinem ursprünglichen Zustand befindet und die von Windows angeforderten GDI-Objekte von deren Destruktoren wieder freigegeben werden können.
2.6.2
Abbildungsmodi
Zu jedem Gerätekontext gehört ein Abbildungsmodus. Der Abbildungsmodus bestimmt, wie die an eine Zeichenfunktion übergebenen Koordinaten (logische Koordinaten) auf die Koordinaten des Ausgabegeräts (Gerätekoordinaten) übertragen werden. Hat der Programmierer keinen Abbildungsmodus ausgewählt, so gilt der Modus MM_TEXT. MM_TEXT gibt an, dass einer logischen Einheit genau ein Bildpunkt auf dem Ausgabegerät entspricht. Während dies bei der Ausgabe von Grafik auf einen Computerbildschirm durchaus sinnvoll sein kann, so ist es beispielsweise problematisch, diesen Abbildungsmodus auf einem Drucker zu verwenden. Die gleiche Grafik, die den ganzen Computerbildschirm ausfüllt, hat auf dem Drucker plötzlich nur noch Briefmarkengröße. Dieser Umstand rührt daher, dass moderne Drucker wesentlich höhere Auflösungen als Computerbildschirme besit-
Grafikausgabe und Drucken
zen. Computerbildschirme stellen meist 72 dpi (dots per inch, Bildpunkte pro Zoll) dar, während moderne Laserdrucker mit 600 bis 1200 dpi, also fast der zehn- bis zwanzigfachen Auflösung, drucken. Um Probleme dieser Art elegant zu umgehen, bietet Windows folgende Abbildungsmodi an: 왘 MM_TEXT. Einer logischen Einheit wird ein Bildpunkt auf dem Ausgabegerät zugeordnet. Dies ist die Voreinstellung eines neu angelegten Gerätekontexts. 왘 MM_HIMETRIC. Einer logischen Einheit entspricht 1/100 mm. 왘 MM_LOMETRIC. Einer logischen Einheit entspricht 1/10 mm. 왘 MM_HIENGLISH. Einer logischen Einheit entspricht 1/1000 Zoll. 왘 MM_LOENGLISH. Einer logischen Einheit entspricht 1/100 Zoll. 왘 MM_TWIPS. Einer logischen Einheit entspricht 1/1440 Zoll. Das ist genau 1/20 eines typografischen Punkts. Dieser Abbildungsmodus eignet sich daher gut zur Ausgabe von Schriften. 왘 MM_ANISOTROPIC. Die Skalierung wird vom Programmierer eingestellt. Horizontale und vertikale Achse werden getrennt betrachtet. 왘 MM_ISOTROPIC. Die Skalierung wird vom Programmierer eingestellt. Horizontale und vertikale Achse werden gleichmäßig skaliert. Bei den Abbildungsmodi MM_HIMETRIC, MM_LOMETRIC, MM_HIENGLISH, MM_LOENGLISH und MM_TWIPS ist zu beachten, dass diese die vertikale Achse anders behandeln als sonst unter Windows üblich. Werte auf der vertikalen Achse nehmen in diesem Abbildungsmodus nach oben hin zu statt nach unten hin. Bei den Abbildungsmodi mit variabler Skalierung, MM_ISOTROPIC und MM_ANISIOTROPIC, ist zu beachten, dass bei Auswahl von MM_ISOTROPIC ein Seitenverhältnis von 1:1 gewahrt wird, während dies bei MM_ANISOTROPIC nicht der Fall ist. Dafür kann dieser Modus die zur Verfügung stehende Zeichenfläche optimal ausnutzen, was allerdings zu deutlichen Verzerrungen der Grafik führen kann. Ein Kreis im Abbildungsmodus MM_ISOTROPIC bleibt immer ein Kreis, im Abbildungsmodus MM_ANISOTROPIC kann er auch zu einer Ellipse werden.
91
92
2
Konvertierung von Koordinaten
Um Werte zwischen logischen Koordinaten und Gerätekoordinaten konvertieren zu können, gibt es die beiden Funktionen CDC::LPtoDP und CDC::DPtoLP. LPtoDP rechnet logische Koordinaten (LP, Logical Points) in Gerätekoordinaten (DP, Device Points) um, DPtoLP nimmt die entgegengesetzte Konvertierung vor. Beide Funktionen benutzen den zum Zeitpunkt des Funktionsaufrufs eingestellten Abbildungsmodus.
2.6.3 OnDraw
Einstieg in die MFC-Programmierung
Grafikausgabe mit der Dokument-AnsichtArchitektur
In Programmen, die auf der Dokument-Ansicht-Architektur basieren, sollten Grafikausgaben grundsätzlich durch die Ansichtsklasse erfolgen. Zweck der Trennung zwischen Dokument und Ansicht ist es schließlich unter anderem, den Programmcode zur Darstellung aller Daten an einer Stelle im Programm – der Ansichtsklasse – zusammenzufassen und ihn nicht über alle Teile eines Programms zu verstreuen. Zur Grafikausgabe stellt die Ansichtsklasse die Funktion OnDraw bereit. Diese wird immer dann aufgerufen, wenn die Ansicht oder ein Teil von ihr neu gezeichnet werden muss. Wenn man mit dem Anwendungs-Assistenten ein neues Projekt anlegt, so enthält OnDraw lediglich zwei Zeilen Programmcode, die das zur Ansicht gehörende Dokument anfordern und es auf Gültigkeit prüfen. Der Programmcode zur Grafikausgabe ist komplett vom Programmierer zu schreiben; hierzu bietet der Anwendungs-Assistent keinerlei Hilfestellung. OnDraw bekommt als Parameter einen Zeiger auf einen Gerätekontext übergeben. Dieser ist nur während der Ausführung von OnDraw gültig und darf nicht zwischengespeichert und an anderer Stelle verwendet werden. OnDraw wird sowohl zur Bildschirmausgabe als auch zum Drucken aufgerufen. Der übergebene Gerätekontext beschreibt also entweder die Ausgabefläche eines Fensters oder aber die Druckfläche eines Druckers. Der Programmierer muss keinen zusätzlichen Programmcode zur Ansteuerung des Druckers schreiben, die MFC-Klassen der Dokument-AnsichtArchitektur nehmen ihm diese Aufgabe ab. Hier ergibt sich eine deutliche Arbeitserleichterung durch die MFC!
Grafikausgabe und Drucken
2.6.4
93
Grafikausgabe in StockChart
Nach der theoretischen Besprechung des GDI soll jetzt der Programmcode vorgestellt werden, den StockChart zur Grafikausgabe verwendet. StockChart macht folgende Vorgaben über das Ausgabekoordinatensystem. Diese Vorgaben halten den Programmcode zum Zeichnen der Aktiendiagramme so einfach wie möglich: 왘 Die Darstellung soll verzerrungsfrei erfolgen. Beispielsweise soll das optional anzuzeigende Gitternetz stets quadratisch sein. 왘 Die zur Verfügung stehende Fläche soll optimal ausgenutzt werden. Wenn das Ausgabefenster vergrößert wird, dann soll auch die Aktienkurve vergrößert werden. 왘 Das Ausgabekoordinatensystem soll unabhängig von der tatsächlichen Größe sein. 왘 Rund um den Grafen soll ein kleiner Rand dargestellt werden, der nicht Teil des Ausgabekoordinatensystems ist. 왘 Innerhalb des Ausgabekoordinatensystems sollen die Werte auf der vertikalen Achse (y-Achse) nach oben hin zunehmen. Diese Anforderungen können nur erfüllt werden, wenn ein Abbildungsmodus mit variabler Skalierung gewählt wird. Da die Darstellung verzerrungsfrei erfolgen soll, verwendet StockChart den Abbildungsmodus MM_ISOTROPIC. Die Zeichenfläche für das Diagramm beträgt 10.000x10.000 logische Einheiten. Dies ist unabhängig von der tatsächlichen Ausgabefläche. An allen Seiten wird ein 300 logische Einheiten breiter Rand verwendet. Da MM_ISOTROPIC beide Achsen gleichmäßig skaliert, um Verzerrungen zu vermeiden, wird der Rand bei einer rechteckigen Form der Ausgabefläche rechts oder unten größer als 300 logische Einheiten sein. Man kann diesen Effekt sehr schön beobachten, wenn man ein Ausgabefenster des Programms StockChart in nur einer Richtung vergrößert oder verkleinert. Es ergibt sich eine Anordnung wie in Abbildung 2.35.
MM_ISOTROPIC
Man kann übrigens MM_ISOTROPIC einfach gegen MM_ANISOTROPIC austauschen (Listing 2.19), um zu sehen, wie sich dieser Abbildungsmodus verhält.
Hinweis
94
2
Einstieg in die MFC-Programmierung
300 Y
300
10000
X 300 300
10000 Ursprung (0,0)
Abbildung 2.35: Abbildungsmodus in StockChart
Der Programmcode, um den Abbildungsmodus für die Ausgabe im Ansichtsfenster einzustellen, befindet sich in der Funktion CStockChartView::OnPrepareDC: void CStockChartView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { CRect clientRect; if (!pDC->IsPrinting ()) { GetClientRect (clientRect); pDC->SetMapMode (MM_ISOTROPIC); pDC->SetWindowExt (10600,-10600); pDC->SetWindowOrg (-300,10300); pDC->SetViewportExt (clientRect.right-clientRect.left, clientRect.bottom-clientRect.top); pDC->SetViewportOrg (0,0); } CView::OnPrepareDC(pDC, pInfo); } Listing 2.19: CStockChartView::OnPrepareDC OnPrepareDC
OnPrepareDC ist eine Funktion des Anwendungsgerüsts, die vor jeder Zeichenoperation aufgerufen wird. Die Einstellung des Gerätekontexts kann auf diese Weise sauber von der eigentlichen Zei-
Grafikausgabe und Drucken
95
chenoperation getrennt werden. OnPrepareDC wird vom Anwendungs-Assistenten nicht erzeugt, kann aber aus der Eigenschaftsansicht überschrieben werden. Zunächst wird mit der Funktion CDC::IsPrinting geprüft, ob der übergebene Gerätekontext zum Drucken verwendet werden soll. Wenn dies der Fall sein sollte, kann der Abbildungsmodus nicht an dieser Stelle eingestellt werden. Wie und wo man den Abbildungsmodus für den Drucker einstellt, wird im Abschnitt 2.6.6, »Drucken und Druckvorschau«, beschrieben. Nachdem MM_ISOTROPIC als Abbildungsmodus gesetzt worden ist, werden die logischen Koordinaten eingestellt. Da der Zeichenbereich 10.000 logische Einheiten hoch und breit sein soll und an beiden Seiten ein Rand von jeweils 300 logischen Einheiten vorgesehen ist, ergibt sich ein Wert von 10.600 logischen Einheiten für Höhe und Breite der Zeichenfläche. Der zweite Wert ist negativ, da ja im Ausgabekoordinatensystem die Werte auf der vertikalen Achse nach oben hin zunehmen sollen. Der Faktor von -1 dreht die von Windows vorgegebene Richtung um. Nachdem mit SetWindowExt die Größe der Zeichenfläche festgelegt worden ist, wird mit SetWindowOrg der Ursprung des Ausgabekoordinatensystems bestimmt. Es ergeben sich die in Listing 2.19 verwendeten Werte. SetWindowExt und SetWindowOrg arbeiten mit logischen Koordinaten. Bei Koordinatensystemen ohne variable Skalierung, wie sie beispielsweise bei den metrischen Abbildungsmodi vorliegen, finden die beiden Funktionen deshalb keine Verwendung.
SetWindowExt, SetWindowOrg
Nachdem die logischen Koordinaten festgelegt worden sind, muss bestimmt werden, auf welche Gerätekoordinaten die Abbildung vorgenommen werden soll. Dazu dienen die Funktionen SetViewportExt und SetViewportOrg. Beide Funktionen arbeiten stets mit Gerätekoordinaten. Im Beispiel erfolgt eine Abbildung auf den gesamten Client-Bereich der Ansicht. Dieser wurde zuvor mit der Funktion CView::GetClientRect ermittelt. Der Ursprung befindet sich in der linken, oberen Ecke. Schließlich muss noch die Funktion OnPrepareDC der Basisklasse aufgerufen werden.
SetViewportExt, SetViewpointOrg
Gezeichnet wird der Aktienchart in der Funktion CStockChartView::OnDraw, die in Listing 2.20 gezeigt ist. void CStockChartView::OnDraw(CDC* pDC) { CStockChartDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc);
96
2
Einstieg in die MFC-Programmierung
CPen plotPen (PS_SOLID, 30, pDoc->m_nColor); CPen axisPen (PS_SOLID, 1, RGB(0,0,0)); CPen gridPen (PS_DOT, 1, RGB(192,192,192)); CPen avgPen (PS_SOLID, 30, RGB (0,255,255)); CPen *pOldPen; CFont font, *pOldFont; double minValue, value; double scaleUpFactor; // alten Stift sichern, neuen in den Gerätekontext selektieren pOldPen = pDC->SelectObject (&axisPen); // Achsen zeichnen pDC->MoveTo (0,0); pDC->LineTo (0,10000); pDC->MoveTo (0,0); pDC->LineTo (10000,0); // falls gewünscht, Gitternetz zeichnen if (pDoc->m_bGrid) { pDC->SelectObject (&gridPen); for (int i=1; i<=10; i++) { pDC->MoveTo (i*1000, 0); pDC->LineTo (i*1000, 10000); pDC->MoveTo (0, i*1000); pDC->LineTo (10000, i*1000); } // for } // Stift zum Zeichnen der Kurve setzen pDC->SelectObject (&plotPen); // Anzahl der Listeneinträge ermitteln int nCnt = pDoc->m_stockData.theData.GetCount (); if (nCnt > 0) { POSITION pos;
// hält Position in der Liste
// an den Anfang der Liste positionieren pos = pDoc->m_stockData.theData.GetHeadPosition (); scaleUpFactor = 10000.0 / (pDoc->m_stockData.max pDoc->m_stockData.min); minValue = pDoc->m_stockData.min; // Wert holen und ein Element weitergehen value = pDoc->m_stockData.theData.GetNext (pos); pDC->MoveTo (0, (int)((value-minValue) * scaleUpFactor));
Grafikausgabe und Drucken for (int i=1; im_stockData.theData.GetNext (pos); pDC->LineTo (i*10000/(nCnt-1), (int)((value-minValue) * scaleUpFactor)); } } // falls gewünscht, Durchschnittslinie zeichnen if (pDoc->m_bAverage) { pDC->SelectObject (&avgPen); int nAvgCnt = pDoc->m_averageData.GetCount (); if (nAvgCnt > 0) { POSITION pos; // hält Position in der Liste // an den Anfang der Liste positionieren pos = pDoc->m_averageData.GetHeadPosition (); value = pDoc->m_averageData.GetNext (pos); pDC->MoveTo (pDoc->m_nAverageCnt/2*10000/(nCnt-1), (int)((value-minValue) * scaleUpFactor)); for (int i=1; im_averageData.GetNext (pos); pDC->LineTo ((i+(pDoc->m_nAverageCnt/2))*10000 /(nCnt-1), (int)((value-minValue) * scaleUpFactor)); } } } // Schriftart erzeugen und setzen font.CreateFont (500, 0,0,0, FW_NORMAL, 0,0,0, ANSI_CHARSET, OUT_DEVICE_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, NULL); pOldFont = pDC->SelectObject (&font); // Beschriftung pDC->TextOut (200, 9800, pDoc->m_name); if (pDoc->m_nID != 0) { TCHAR buffer[16]; // _itot benutzt automatisch Unicode oder ANSI-Zeichen pDC->TextOut (200, 9300, _itot (pDoc->m_nID, buffer, 10)); } pDC->TextOut (200, 8800, pDoc->m_ticker);
97
98
2
Einstieg in die MFC-Programmierung
// alten Stift zurücksetzen pDC->SelectObject (pOldPen); // alte Schriftart zurücksetzen pDC->SelectObject (pOldFont); } Listing 2.20: Funktion OnDraw der Ansichtsklasse Verwendung der Stifte
OnDraw ermittelt zunächst das zur Ansicht gehörende Dokument mit Hilfe der Funktion CView::GetDocument. Das Makro ASSERT_VALID überprüft in der Debug-Version des Programms, ob das ermittelte Dokument gültig ist. Dann werden die zum Zeichnen notwendigen Stifte deklariert: Mit axisPen werden die Achsen, mit gridPen das Gitter und mit plotPen die Kurve gezeichnet. Dem Konstruktor von CPen wird als erstes Argument der Linienstil übergeben. PS_SOLID bedeutet, dass eine durchgehende Linie gezeichnet wird, PS_DOT zeichnet eine gepunktete Linie. Es gibt noch eine Reihe weiterer Linienstile, die in der Online-Hilfe unter CPen beschrieben werden. Der zweite Parameter von CPen gibt die Linienstärke an. Sie wird in logischen Einheiten angegeben. Obwohl plotPen eine 30-mal stärkere Liniendicke als die beiden anderen Stifte besitzt, beträgt die Linienstärke in Gerätekoordinaten bei einem kleineren Ansichtsfenster, genau wie bei den beiden anderen Stiften, scheinbar lediglich einen Bildschirmpunkt. Der Unterschied wird erst deutlich, wenn man die Kurve ausdruckt oder das Ausgabefenster so weit vergrößert, dass die Zeichenfläche ohne Rand mindestens 500 Bildschirmpunkte (10000/30*1,5) hoch und breit ist. Die Kurve erscheint dann dicker als die Achsen und das Gitternetz. Man beachte, dass Linien, die nicht den Stil PS_SOLID besitzen, nur einen Punkt (in Gerätekoordinaten) breit sein dürfen. Sollten sie breiter sein, dann werden sie unabhängig von ihrem Stil durchgängig gezeichnet. Der dritte Parameter von CPen gibt schließlich die Farbe an. Die Farbe von plotPen ist innerhalb des Dokuments in der Variablen m_nColor gespeichert, die Farben der anderen Stifte werden mit Hilfe des RGB-Makros aus ihren Rot-, Grün- und Blauanteilen bestimmt.
Zeichnen von Linien
Nach den Deklarationen wird zunächst der zum Zeichnen der Achsen verwendete Stift in den Gerätekontext hineinselektiert. Der ursprünglich im Gerätekontext enthaltene Stift wird in der Variablen pOldPen zwischengespeichert. Danach werden die Achsen und gegebenenfalls das Gitternetz gezeichnet. Zum Zeichnen
Grafikausgabe und Drucken
99
des Gitternetzes wird der gridPen als neuer Stift in den Gerätekontext selektiert. Gezeichnet wird mit den Funktionen CDC::MoveTo und CDC::LineTo. MoveTo setzt die aktuelle Zeichenposition innerhalb eines Gerätekontexts, LineTo zeichnet eine Linie von der aktuellen Position zu der ihr übergebenen Position. Danach setzt LineTo die aktuelle Zeichenposition auf die ihr übergebenen Koordinaten um. Damit ist es einfach, mehrere Linien miteinander zu verbinden. Das GDI besitzt eine ganze Reihe weiterer Funktionen zur Ausgabe von grafischen Figuren wie Rechtecken, Polygonen und Ellipsen. Diese Funktionen können im Rahmen dieser MFC-Einführung nicht ausführlich besprochen werden. Weitere Informationen dazu findet man in der Online-Hilfe unter CDC. Der Programmcode zum Zeichnen der Achsen und des Gitternetzes ist deshalb so einfach, weil der Abbildungsmodus vorher sorgfältig eingestellt worden ist. Daher enthält der Zeichencode keine Angaben über die Ränder oder die tatsächliche Größe der Zeichenfläche. Nach Ausgabe des Gitternetzes wird die Kurve gezeichnet. Die dafür notwendigen Daten werden aus der Kursliste des Dokuments bezogen. In einer Schleife wird über die Kursdatenliste iteriert und die Kurve wird aus einzelnen Geraden mittels des LineTo-Befehls zusammengesetzt. Die Durchschnittslinie wird anschließend auf die gleiche Art mit den Daten der bereits berechneten Durchschnittswerte gezeichnet, sofern das gewünscht ist. Schließlich wird das Diagramm beschriftet. Dazu muss zunächst eine Schriftart ausgewählt werden. Obwohl die Schriftart selbst bereits im System vorhanden ist, muss sie zur Benutzung bereitgestellt werden – im Windows-Sprachgebrauch: erzeugt werden. Dies geschieht mit der Funktion CFont::CreateFont. Es wird ein Objekt der Klasse CFont angelegt und anschließend die Funktion CreateFont aufgerufen. Die so erzeugte Schriftart wird anschließend in den Gerätekontext hineinselektiert, wobei die ursprünglich dort vorhandene Schriftart in der Variablen pOldFont gesichert wird. CreateFont benötigt die stattliche Menge von 14 Parametern, um die gewünschte Schriftart zu erzeugen. Im Beispiel wird eine Schrift mit einer Höhe von 500 logischen Einheiten (erster Parameter), normaler Dicke (FW_NORMAL) und einem proportiona-
Verwendung von Schriften
100
2
Einstieg in die MFC-Programmierung
len, serifenlosen Aussehen (FF_SWISS) angefordert. Die Wahl der konkreten Schriftart wird Windows überlassen. Möchte man eine ganz bestimmte Schriftart verwenden, so kann man CreateFont im letzten Parameter einen Schriftartennamen übergeben. Auf die weiteren Parameter der Funktion CreateFont und die vielfältigen Möglichkeiten zur Verwendung von Schriftarten unter Windows kann leider im Rahmen dieser Einführung nicht eingegangen werden. Eine ausführlichere Besprechung der Verwendung von Schriftarten unter Windows findet sich beispielsweise in dem im Literaturverzeichnis aufgeführten Buch von Frank Heimann und Nino Turianskyj, GoTo Visual C++ 6. Nach der Ausgabe von Aktienname, Wertpapierkennnummer und Tickersymbol mit der Funktion CDC::TextOut werden zum Schluss der Funktion OnDraw sowohl die ursprüngliche Schriftart als auch der alte Stift in den Gerätekontext zurückselektiert. Dies ist die Standardvorgehensweise bei der Programmierung des GDI und sollte nicht vergessen werden. Verwendung von GDI-Objekten
Abschließend seien noch einige Anmerkungen zur Lebensdauer und Verwendung von GDI-Objekten gemacht. Grundsätzlich sollte der Programmierer für jede Anforderung des Systems, die Ansicht zu zeichnen, folgende Schritte durchführen: 왘 Zuerst den Abbildungsmodus einstellen. 왘 Dann alle benötigten GDI-Objekte anlegen (vorzugsweise auf dem Stack, das heißt als funktionslokale Variable). 왘 Die GDI-Objekte in den bereitgestellten Gerätekontext hineinselektieren, wobei die ursprünglichen Objekte zu sichern sind. 왘 Die Grafikausgabe durchführen. 왘 Den ursprünglichen Zustand des Gerätekontexts wiederherstellen. 왘 Nicht auf dem Stack angelegte GDI-Objekte wieder freigeben. Wer von dieser Vorgehensweise abweicht, sollte das GDI auf APIEbene verstanden haben, und er sollte wissen, dass die MFC von CDC::SelectObject zurückgegebene GDI-Objekte als temporär betrachten und zu einem späteren Zeitpunkt selbsttätig löschen. Man darf diese Objekte also weder selbst löschen noch Zeiger auf diese Objekte zur Benutzung außerhalb von OnDraw verwenden.
Grafikausgabe und Drucken
2.6.5
Tipps zur Vorgehensweise
Um in einem Programm, das auf der Dokument-Ansicht-Architektur basiert, grafische Ausgaben mit dem GDI vorzunehmen, sollte man wie folgt vorgehen: 왘 In OnPrepareDC wird vor Beginn der Zeichenoperation ein geeigneter Abbildungsmodus eingestellt. 왘 Alle grafischen Ausgaben erfolgen zentral in OnDraw oder in von dort aufgerufenen Funktionen. 왘 Zum Zeichnen benötigte GDI-Objekte werden möglichst als lokale Variable der Funktion OnDraw, also auf dem Stack, angelegt. Bei dieser Vorgehensweise sorgt der Konstruktor für die Anforderung des GDI-Objekts von Windows, während der Destruktor es am Ende der Funktion automatisch wieder freigibt. Auf diese Weise können keine GDI-Objekte verloren gehen. 왘 Die ursprünglich im Gerätekontext vorhandenen und von der Funktion SelectObject zurückgegebenen GDI-Objekte müssen gesichert und am Ende der Funktion OnDraw wieder in den Gerätekontext zurückselektiert werden. 왘 Die Grafikausgabe selbst erfolgt mit den Zeichenfunktionen, die der Gerätekontext zur Verfügung stellt. 왘 Alle diese Schritte sind für jeden Aufruf von OnDraw zu wiederholen. Man sollte nicht versuchen, GDI-Objekte für den nächsten Aufruf zwischenzuspeichern, da dies zu einer dauerhaft höheren Ressourcenbelastung des Systems führen würde.
2.6.6
Drucken und Druckvorschau
Ein großer Vorteil bei MFC-Programmen, die unter Verwendung der Dokument-Ansicht-Architektur erstellt werden, ist die weit reichende Druckunterstützung. Im Gegensatz zu Programmen, die das Windows-API direkt benutzen oder die MFC nur als objektorientierte Systemschnittstelle verwenden, muss bei Programmen der Dokument-Ansicht-Architektur nur sehr wenig zusätzlicher Programmcode zum Drucken erstellt werden.
101
102
2
Einstieg in die MFC-Programmierung
Abbildung 2.36: Druckvorschau in StockChart
Sowohl zum Drucken als auch zum Zeichnen der Druckvorschau ruft das MFC-Anwendungsgerüst die bereits vorgestellte Funktion OnDraw auf. Der gleiche Programmcode, der zum Zeichnen der Fensterinhalte verwendet wird, nimmt auch die Ausgabe auf den Drucker vor. Ein Unterschied zur Ausgabe auf den Bildschirm findet sich bei der Einstellung des Abbildungsmodus. Ein skalierbarer Abbildungsmodus kann in OnPrepareDC nicht eingestellt werden, da zum Zeitpunkt des Aufrufs dieser Funktion die Information zur Seitengröße des Druckers noch nicht gültig ist. Das Programm StockChart stellt daher den Abbildungsmodus in der Funktion CStockChartView::OnPrint (Listing 2.21) ein, die zum Drucken jeder Seite aufgerufen wird. Der Programmcode entspricht dem von OnPrepareDC.
Zum Abschluss wird die Funktion CView::OnPrint der Basisklasse aufgerufen. Diese führt den Ausdruck durch und ruft dazu auch CStockChartView::OnDraw auf. Alle nicht variabel skalierbaren Abbildungsmodi sollten wie vorgesehen in OnPrepareDC eingestellt werden, da bei diesen die Größe der Druckseite nicht benötigt wird. Dass die Seitengröße in OnPrepareDC nicht gesetzt wird, kann als Fehler in den MFC angesehen werden. Wer sich den zugehörigen Quelltext ansieht (VIEWPRNT.CPP für den Druck und VIEWPREV.CPP für die Druckvorschau), wird feststellen, dass die Größe der Druckfläche unmittelbar nach (!) dem Aufruf von OnPrepareDC gesetzt wird. Der Fehler dürfte wohl deshalb nicht aufgefallen sein, weil man Druckausgaben meist mit nicht skalierbaren Abbildungsmodi vornimmt. Die Funktion OnPrint wird vom Programm StockChart im Grunde genommen zweckentfremdet, um einen Fehler in den MFC zu umgehen. Normalerweise hat OnPrint die Aufgabe, Elemente auszugeben, die nur beim Drucken vorhanden sein sollen. Dies können beispielsweise zusätzliche Kopf- und Fußzeilen sein, ein Firmenlogo oder der große Schriftzug »Unregistriert« eines Shareware-Programms zur Textverarbeitung. Lässt man den Aufruf der Basisfunktion CView::OnPrint weg, so wird die OnDraw-Funktion nicht mehr aufgerufen, und man kann einen Ausdruck erzeugen, der sich von der Bildschirmausgabe deutlich unterscheidet. Zur Steuerung des Druckvorgangs werden vom Anwendungsgerüst drei weitere Funktionen bereitgestellt. Alle drei Funktionen werden bereits vom Anwendungs-Assistenten in die Ansichtsklasse eingefügt.
OnPrint
104
2
Einstieg in die MFC-Programmierung
왘 OnPreparePrinting wird vor der Anzeige des Dialogfelds DRUCKEN aufgerufen. Mit den Funktionen CPrintInfo::SetMinPage und CPrintInfo::SetMaxPage können die zu druckenden Seiten des Dokuments angegeben werden. Diese werden dann in das Dialogfeld DRUCKEN als Vorgaben übernommen. 왘 OnBeginPrinting wird nach dem Schließen des Dialogfelds DRUCKEN und unmittelbar vor dem Start des Ausdrucks aufgerufen. Hier können zusätzliche GDI-Objekte, die für den Druckauftrag benötigt werden, wie beispielsweise Schriften, angefordert werden. 왘 OnEndPrinting ist das Gegenstück zu OnBeginPrinting. Hier sollten nach Beendigung des Druckauftrags zuvor angeforderte GDI-Objekte wieder freigegeben werden.
2.6.7
Zusammenfassung
Das GDI ist die Standardschnittstelle zur Grafikausgabe unter Windows. Neben dem GDI gibt es alternative Grafikschnittstellen für Spezialzwecke (OpenGL, DirectX). Das GDI ist sehr alt, es geht bis auf die erste Windows-Version zurück. Der heute etwas umständlich anmutende Umgang mit GDI-Objekten ist auf die knappen Ressourcen der ersten Windows-Versionen zurückzuführen. Trotzdem kann die Grafikausgabe mit dem GDI völlig unabhängig von der Ausgabe-Hardware programmiert werden. Durch das Konzept der Abbildungsmodi werden aufwändige Skalierungsaufgaben dem System übertragen.
2.7
Dialogfeldprogrammierung
Dialogfelder, oder kurz Dialoge, sind spezielle Fenster, die zur Kommunikation mit dem Benutzer dienen. Dialoge werden zur Anzeige von Informationen, zur Eingabe von Daten oder auch für beides gleichzeitig verwendet. Innerhalb des Dialogfelds werden Bedien- und Anzeigeelemente, die so genannten Steuerelemente platziert. Aus der Sicht des Betriebssystems sind Steuerelemente – genau wie Dialoge – nichts anderes als spezielle Fenster. Es gibt eine ganze Reihe unterschiedlicher Steuerelemente, wie zum Beispiel Schaltflächen (Buttons), Eingabefelder (Edit-Boxes) und Optionsfelder (Radio-Buttons). Steuerelemente sind nicht an Dialogfelder gebunden, sie können auch in normalen Fenstern ver-
Dialogfeldprogrammierung
105
wendet werden. Dialogfelder bieten allerdings im Gegensatz zu normalen Fenstern einige Mechanismen an, die die Arbeit mit Steuerelementen vereinfachen. So werden Steuerelemente in eine Reihenfolge gebracht (Tabulatorreihenfolge), damit man mit Tastaturbefehlen zwischen ihnen navigieren kann. Es lassen sich Dialogvorlagen definieren, die mit einem Ressourceneditor erstellt werden können und damit einiges an Programmierarbeit sparen. Alternativ dazu können Dialogvorlagen mit HTML erstellt werden. Der einfachste Dialog ist die Windows-Messagebox, die in den MFC durch die Funktion AfxMessageBox aufgerufen wird. Die Messagebox zeigt einen Text zur Benachrichtigung des Benutzers an. Es gibt je nach Typ der Messagebox eine oder mehrere Schaltflächen, um die Messagebox zu schließen und damit eine Aktion des Programms zu bestätigen oder abzubrechen.
Messagebox
Abbildung 2.37: Eine Messagebox ist der einfachste Dialog.
Neben der Messagebox gibt es eine Reihe weiterer, von Windows vordefinierter Dialoge, die der Programmierer sozusagen »von der Stange« verwenden kann. Zu dieser Gruppe der Standarddialoge (Common Dialogs) gehört unter anderem der beim Dateiimport bereits angesprochene DATEI-Dialog.
Standarddialoge
Reicht die Funktionalität der vordefinierten Dialoge nicht aus, so müssen eigene Dialogfelder erstellt werden. Diese basieren auf einer Vorlage, die als Ressource innerhalb der Anwendung gespeichert wird. Die Dialogvorlage wird normalerweise mit dem Ressourceneditor der Entwicklungsumgebung von Visual Studio .NET erstellt, kann allerdings auch mit einem externen Ressourceneditor oder in textueller Form definiert werden. Ein Beispiel für ein durch Ressourcen definiertes Dialogfeld ist das vom Anwendungs-Assistenten erzeugte Info-Dialogfeld, das vom Hilfemenü aus aufgerufen wird.
Dialogvorlagen
106
2
Einstieg in die MFC-Programmierung
Steuerelemente
Die Dialogfeldressource legt die im Dialogfeld verwendeten Steuerelemente sowie deren Anordnung innerhalb des Fensters fest. Steuerelemente sind die eigentlichen Mittel zur Kommunikation innerhalb des Dialogs. Es sind Elemente wie Schaltflächen (Buttons), Texteingabefelder (Edit-Controls) und Optionsfelder (RadioButtons). Windows bietet eine reiche Auswahl an Steuerelementen an.
Modal, nichtmodal
Messageboxen und die meisten Standarddialoge sind modale Dialogfelder, das heißt, sie blockieren die weitere Programmausführung, bis das Dialogfeld geschlossen wird. Daneben gibt es nichtmodale Dialogfelder, die einfach offen gelassen werden können, ohne die weitere Arbeit zu behindern. Beispiele dafür sind die in vielen Textprogrammen verwendeten Suchen/ErsetzenDialoge. Bei selbst erstellten Dialogen kann der Programmierer entscheiden, ob der Dialog modal oder nichtmodal verwendet werden soll. Modale Dialoge sind einfacher zu programmieren, da der Datenaustausch zwischen Programm und Dialog an genau einer Stelle und zu einem genauen Zeitpunkt, nämlich beim Aufruf des Dialogs, erfolgt. Nichtmodale Dialogfelder hingegen sind oft bequemer zu benutzen, denn der Arbeitsfluss mit dem Programm wird nicht unterbrochen.
Dialogklassen
In den MFC werden Dialogfelder – mit Ausnahme der Messagebox – immer durch eine Dialogklasse repräsentiert. Um ein Dialogfeld anzuzeigen, muss eine Instanz der Dialogklasse angelegt und das Dialogfeld dann durch Member-Funktionen der Dialogklasse angezeigt und eventuell auch wieder geschlossen werden. Alle Dialogklassen werden von der Basisklasse CDialog abgeleitet.
DDX, DDV
Um den Datenaustausch zwischen Programm und Dialog zu vereinfachen, verwenden die MFC eine Technik namens DDX. DDX bedeutet Dialog Data Exchange, Dialogdatenaustausch. Dazu werden in der Klasse, die den Dialog repräsentiert, Variablen angelegt, deren Werte mit den Werten der im Dialogfeld vorhandenen Steuerelemente korrespondieren. Die Synchronisation zwischen Variablen und Steuerelementen nehmen die MFC vor. Begleitend zu DDX gibt es DDV, Dialog Data Validation, die Dialogdatenüberprüfung. DDV sorgt dafür, dass vom Programmierer vorgegebene Wertebereiche eingehalten werden.
Dialogfeldprogrammierung
2.7.1
107
Standarddialogfelder
Standarddialogfelder sind ein Hilfsmittel, um dem Benutzer für stets wiederkehrende Aufgaben eine einheitliche Benutzerschnittstelle zu präsentieren. Da der Benutzer die Dialogfelder aus ihm bereits bekannten Programmen wieder erkennt, fällt ihm die Einarbeitung in neue Programme leichter. Das beste Beispiel für diesen Wiedererkennungseffekt sind die Dialogfelder zum Öffnen und Speichern von Dateien. Während in früheren Windows-Versionen jedes Programm seine eigenen Dialoge zum Öffnen und Speichern von Dateien besaß, benutzt heute die Mehrzahl der auf dem Markt befindlichen Programme die Standarddialogfelder. Für den Programmierer ergibt sich der angenehme Nebeneffekt, dass er für oft vorkommende Tätigkeiten wie das Öffnen und Speichern von Dateien, die Auswahl von Farben und Schriftarten sowie für die Auswahl und Einstellung des Druckers keine eigenen Dialogfelder erstellen muss. Alle Standarddialoge sind in den MFC durch jeweils eine eigene Dialogfeldklasse vertreten.
Die Basisklasse aller Standarddialoge ist die Klasse CCommonDialog, die von CDialog, der Basisklasse aller Dialoge, abgeleitet ist. CDialog wiederum ist von CWnd abgeleitet, der Basisklasse aller Fenster. Jeder Dialog ist also letztendlich ein ganz normales Fenster. Nicht in der Abbildung 2.38 vertreten sind eine Reihe von Dialogklassen, die eine Vielzahl wiederkehrender Tätigkeiten in Zusammenhang mit OLE-Dokumenten abdecken. Diese Klassen wurden aus Gründen der Übersichtlichkeit in der Abbildung weggelassen. Es sei jedoch gesagt, dass die OLE-Dialoge analog zu den hier behandelten Standarddialogen anzusprechen sind. Es sollte daher nicht schwer fallen, diese OLE-Dialoge zu verwenden, wenn man bereits andere Standarddialoge eingesetzt hat. Nähere Informationen zu den Standard-OLE-Dialogen findet man in der Online-Hilfe. Die in der Abbildung aufgeführten Dialoge haben im Einzelnen folgende Aufgaben:
Abbildung 2.39: CColorDialog
왘 CColorDialog stellt ein Dialogfeld zur Farbauswahl bereit. Es ist in beinahe jedem Malprogramm vorhanden. 왘 CFileDialog stellt ein Dialogfeld zum Laden oder Speichern von Dateien bereit. Dieses Dialogfeld besitzt in verschiedenen Windows-Versionen ein jeweils unterschiedliches Erscheinungsbild. Die Abbildung zeigt das Standarddialogfeld von Windows 2000.
Dialogfeldprogrammierung
Abbildung 2.40: CFileDialog
Abbildung 2.41: CFindReplaceDialog
왘 CFindReplaceDialog implementiert einen Suchen/Ersetzen-Dialog. Diese Klasse ist die einzige Standarddialogklasse, die nichtmodal verwendet wird. 왘 Mit CFontDialog lässt sich eine Schriftart auswählen. Der Dialog zeigt alle im System installierten Schriftarten an. Der Benutzer kann die Schriftart auswählen und festlegen, in welcher Größe und mit welchen Eigenschaften (kursiv, fett usw.) die Schrift verwendet werden soll. 왘 CPageSetupDialog (Abbildung 2.43) und CPrintDialog (Abbildung 2.44) werden beim Drucken verwendet. Ein mit dem Anwendungs-Assistenten erstelltes Programm kommt ohne diese Klassen aus, da das Anwendungsgerüst intern einen (etwas anderen) Dialog aufruft, mit dem man die Druckereinstellung vornehmen kann.
109
110
2
Abbildung 2.42: CFontDialog
Abbildung 2.43: CPageSetupDialog
Einstieg in die MFC-Programmierung
Dialogfeldprogrammierung
Abbildung 2.44: CPrintDialog
Der Druckdialog von Windows 2000 wird durch die MFC-Klasse CPrintDialogEx angesprochen. Hier werden die Druckeinstellungen auf drei Karteikarten verteilt. Der Druckdialog von Windows 2000 ist in Abbildung 2.45 zu sehen.
Abbildung 2.45: CPrintDialogEx
111
112
2
Verwendung des Dateidialogs
Einstieg in die MFC-Programmierung
Das Programm StockChart verwendet einen Dialog des Typs CFileDialog zur Auswahl einer zu importierenden Kursdatei. Der Dialog wird zu Beginn der bereits im Abschnitt 2.5.3, »Klassen zum Dateizugriff«, vorgestellten Funktion CStockChartDoc::OnFileImport aufgerufen. Listing 2.22 zeigt noch einmal den entsprechenden Quelltextabschnitt. void CStockChartDoc::OnFileImport() { // Dateidialog zum Importieren: CFileDialog fileDialog(true, NULL, NULL, NULL, "Textdateien (*.txt)|*.txt | Alle Dateien (*.*)|*.*||"); // wenn der Benutzer den Dialog mit OK verlassen hat: if (IDOK == fileDialog.DoModal ()) { try { CStdioFile file (fileDialog.GetPathName (), CFile::typeText); ... Listing 2.22: Verwendung des Standarddialogs DATEI | ÖFFNEN im Programm StockChart
Die Parameter des Konstruktors geben die Einstellungen des Dialogfelds vor. Der erste Parameter bestimmt, ob eine Datei geöffnet oder gespeichert werden soll. Mit dem zweiten und dritten Parameter können eine Dateinamenerweiterung und ein Dateiname vorgegeben werden. Mit dem vierten Parameter können diverse Optionen an- und ausgeschaltet werden. Der fünfte Parameter schließlich gibt – etwas umständlich verschlüsselt – die vom Dialogfeld anzuzeigenden Dateitypen vor. Dieser String wird auch als Filter-String bezeichnet. Optional kann dem Konstruktor auch noch ein sechster Parameter übergeben werden, der das Elternfenster angibt. DoModal
Durch den Aufruf der Funktion DoModal wird das Dialogfeld angezeigt. Nachdem der Benutzer den Dialog geschlossen hat, kehrt DoModal zurück. Der Rückgabewert gibt an, wie der Dialog verlassen wurde. Ein Wert von IDOK bedeutet, dass der Benutzer auf OK geklickt oder eine Datei per Doppelklick ausgewählt hat – es wurde also eine gültige Auswahl getroffen, so dass tatsächlich auf diese Datei zugegriffen werden darf. Name und Pfad der ausgewählten Datei werden durch die Funktion CFileDialog::GetPathName zurückgeliefert. Andere Rückgabewerte zeigen an, dass ein Fehler aufgetreten ist oder der Benutzer auf ABBRECHEN geklickt hat. Die Auswahl ist in diesen Fällen nicht gültig.
Dialogfeldprogrammierung
113
Die Standarddialogfelder haben gemeinsam, dass sie für sich wiederholende Aufgaben eingesetzt werden. Dadurch, dass sie anwendungsübergreifend eingesetzt werden können (die Standarddialogfelder können auch außerhalb der MFC direkt durch das Windows-API angesprochen werden), sind sie dem Benutzer bereits vertraut und es fällt keine Einarbeitungszeit an. Es ist nicht anzuraten, für diese Aufgaben eigene Dialogfelder zu kreieren. Bis auf die Ausnahme des Suchen/Ersetzen-Dialogs sind alle Standarddialogfelder modal. Die Standarddialoge lassen sich erweitern, indem man eigene Klassen von den Standarddialogklassen ableitet und eine zusätzliche Ressourcenvorlage liefert, die die zu ergänzenden Steuerelemente enthält. Solche verschachtelten Dialogfelder enthalten den ursprünglichen Standarddialog und gruppieren ihre eigenen Steuerelemente um diesen herum. Für den Benutzer sieht der so zusammengesetzte Dialog allerdings »wie aus einem Stück« aus.
2.7.2
Modale Dialogfelder
Selbst erstellte Dialogfelder, ob modal oder nichtmodal, werden von der Klasse CDialog abgeleitet.
CObject CCmdTarget CWnd CDialog Eigene Dialogklasse Abbildung 2.46: Eigene Dialogklassen in den MFC
Die Erstellung der Dialogfeldklasse gestaltet sich einfach. Zunächst wird eine Dialogvorlage mit dem Ressourceneditor des Developer Studio erstellt. Die gewünschten Steuerelemente werden auf der Dialogfläche platziert. Dabei vergibt das Developer Studio automatisch Ressourcen-IDs für die Steuerelemente. Die für diese IDs automatisch generierten Bezeichner lassen sich im Eigenschaftsdialog des Steuerelements ändern. Über die IDs werden den Steuerelementen später Austauschvariablen für den Dialogdatenaustausch DDX zugeordnet.
Erweiterung der Standarddialoge
114
2
Einstieg in die MFC-Programmierung
Um eine Dialogfeldklasse zur Dialogvorlage zu erstellen, wählt man in der Projektansicht HINZUFÜGEN... | KLASSE HINZUFÜGEN... | MFC-KLASSE. Es erscheint der in Abbildung 2.47 gezeigte Dialog, in dem man die Eigenschaften der zu erstellenden Klasse eingeben kann. Hier muss als Basisklasse CDialog ausgewählt und die Dialogfeld-ID angegeben werden. Hat man diesen Dialog ausgefüllt, so erzeugt der Klassen-Assistent die gewünschte Klasse.
Abbildung 2.47: Dialog zur Erstellung einer Dialogklasse
Angezeigt werden modale Dialoge, genau wie die Standarddialoge, durch den Aufruf der Funktion DoModal des Dialogobjekts. Das Dialogobjekt wird bei modalen Dialogen üblicherweise auf dem Stack angelegt, so kann man nicht vergessen, es zu löschen.
2.7.3
Nichtmodale Dialogfelder
Nichtmodale Dialogfelder werden genauso wie die modalen erstellt. Die Dialogvorlage wird mit dem Ressourceneditor angelegt und die Dialogfeldklasse wird mit dem Klassen-Assistenten erzeugt. Der erste Unterschied ergibt sich bei der Frage, wo die Dialogfeldvariable angelegt werden soll. Bei modalen Dialogfeldern ist es üblich, diese lokal in der Funktion anzulegen, die den Dialog aufruft. Da die Lebensdauer eines modalen Dialogs auf den Aufruf der Funktion DoModal beschränkt ist, ist diese Vor-
Dialogfeldprogrammierung
115
gehensweise sinnvoll. Nichtmodale Dialoge haben, programmtechnisch gesehen, eine wesentlich längere Lebensdauer als modale Dialoge, schließlich läuft das Programm weiter, nachdem der Dialog erstellt worden ist. Es bietet sich folglich an, nichtmodale Dialoge mit dem Operator new zu erzeugen und den erhaltenen Zeiger in einem Objekt zu speichern, das mindestens so lange lebt wie der Dialog. Wird ein nichtmodaler Dialog beispielsweise aus einer Ansichtsklasse heraus geöffnet, so kann man den Zeiger auf den Dialog in dem Ansichtsklassenobjekt speichern. Sollte der Benutzer das Ansichtsfenster schließen, so ist selbstverständlich dafür Sorge zu tragen, dass auch der Dialog ordnungsgemäß beendet und das Dialogfeldobjekt gelöscht wird. Nichtmodale Dialogfelder werden mit der Funktion CDialog::Create erzeugt. Ist in der dazugehörenden Dialogfeldressource die Eigenschaft SICHTBAR ausgewählt, dann wird das Dialogfeld sofort angezeigt. Anderenfalls ist noch die Funktion ShowWindow mit dem Parameter SW_SHOW aufzurufen, um das Dialogfeld anzuzeigen. Create kehrt im Gegensatz zu DoModal sofort nach dem Erzeugen des Dialogfensters zurück. Um den Dialog an späterer Stelle zu schließen, wird die Funktion CDialog::DestroyWindow aufgerufen. Diese schließt allerdings nur das Dialogfenster, die Dialogklasse muss zusätzlich mit dem delete-Operator gelöscht werden.
2.7.4
DDX und DDV
DDX, Dialogdatenaustausch, und DDV, Dialogdatenüberprüfung, vereinfachen den Datenaustausch zwischen der Dialogklasse und den Steuerelementen des Dialogfensters deutlich. Bei der Windows-API-Programmierung ohne DDX werden Werte der Steuerelemente gelesen oder gesetzt, indem ihnen Windows-Nachrichten gesendet werden. Das betreffende Steuerelement wird dazu durch seine Ressourcen-ID identifiziert. Mit DDX erfolgt der Datenaustausch zwischen den Steuerelementen des Dialogs und der Dialogklasse über Member-Variablen des Dialogs. Diese Variablen werden mit einem eigenen Assistenten angelegt und an die Steuerelemente des Dialogfensters gebunden. Dazu wählt man in der Klassenansicht bei der zuvor erzeugten Dialogfeldklasse im Kontextmenü den Menüpunkt HINZUFÜGEN | VARIABLE HINZUFÜGEN... aus.
Create
116
2
Einstieg in die MFC-Programmierung
Abbildung 2.48: DDX-Austauschvariablen anlegen
Für die ausgewählte Dialogklasse zeigt der Assistent die IDs aller zugehörigen Steuerelemente an, sofern das Kontrollkästchen STEUERELEMENTVARIABLE zuvor selektiert wurde. Um eine Variable für ein Steuerelement anzulegen, muss die ID des Steuerelements ausgewählt werden. Der Typ der Variablen, der Typ des Steuerelements und der Variablenname müssen angegeben werden. Dann kann noch zwischen den Einstellungen »Wert« und »Steuerelement« gewählt werden. Die Einstellung »Wert« bedeutet, dass ein einfacher Datentyp wie beispielsweise int oder CString für die Datenaustauschvariable gewählt werden kann. Der Wert der Austauschvariablen bestimmt den Zustand des Steuerelements. Für DDX-Variablen wird normalerweise die Einstellung »Wert« ausgewählt. Für einige Steuerelemente ist die Auswahl von »Wert« jedoch nicht möglich. Austauschvariablen der alternativen Kategorie »Steuerelement« sind nur eingeschränkt zu verwenden. Austauschvariablen dieser Kategorie kapseln das Steuerelement durch eine Instanz der zugehörigen Steuerelementklasse. Leider ist der Zugriff auf die Instanz dieser Steuerelementklasse nur möglich, während das Dialogfeld existiert. Somit ist der Austausch bei modalen Dialogen nicht möglich, da das Dialogfeld weder vor noch nach dem Aufruf von DoModal existiert. Wie man in solchen Fällen vorgeht, wird im Abschnitt 2.7.5, »Datenaustausch ohne DDX«, gezeigt.
Dialogfeldprogrammierung
117
Bei der Zuordnung einer DDX-Variablen zu einer Gruppe von Optionsfeldern (Radio-Buttons) ist etwas Aufmerksamkeit geboten. Beim ersten Optionsfeld der Gruppe muss das Gruppen-Flag gesetzt sein, bei allen weiteren darf es nicht gesetzt sein.
Abbildung 2.49: Das Gruppen-Flag muss beim ersten Optionsfeld gesetzt sein.
Die Optionsfelder müssen zudem innerhalb des Ressourceneditors in eine fortlaufende Tabulatorreihenfolge gebracht werden (Menü FORMAT | AKTIVIERUNGSREIHENFOLGE). Die Optionsfeldgruppe kann dann über den Wert der DDX-Variablen gesetzt und gelesen werden: 0 bedeutet, das erste Optionsfeld ist gesetzt, 1 das zweite usw.
Abbildung 2.50: Die Tabulatorreihenfolge für Optionsfelder muss richtig gesetzt sein.
Tabulatorreihenfolge
118
2
Einstieg in die MFC-Programmierung
DDX-Variablen der Kategorie »Wert« agieren als »Schattenvariablen« der Steuerelemente des Dialogs. Sie reflektieren die Werte der Steuerelemente, enthalten jedoch nicht den eigentlichen Wert des Steuerelements selbst. Werte müssen daher immer zwischen Steuerelement und DDX-Variable übertragen werden. In vielen Fällen geschieht dies automatisch. Datenaustausch modaler Dialog
Der Datenaustausch bei modalen Dialogen gestaltet sich sehr einfach. Vor dem Aufruf der Funktion DoModal werden den Austauschvariablen Werte zugewiesen. Diese Werte werden bei der Anzeige des Dialogfensters als Voreinstellung übernommen. Nachdem der Benutzer den Dialog geschlossen hat und die Funktion DoModal zurückkehrt, befinden sich die vom Benutzer veränderten Werte in den Austauschvariablen.
Datenaustausch nichtmodaler Dialog
Bei nichtmodalen Dialogen gestaltet sich der Datenaustausch unter Umständen etwas schwieriger. Die Initialisierung eines nichtmodalen Dialogs funktioniert wie die Initialisierung eines modalen Dialogs. Die Dialogklasse wird erzeugt, den Austauschvariablen werden ihre anfänglichen Werte zugewiesen und das Dialogfenster wird durch Aufruf der Funktion Create erzeugt. Bei nichtmodalen Dialogen gibt es allerdings den Fall, dass der Benutzer Werte im Dialog verändert und diese in das Programm übernehmen möchte, ohne den Dialog zu schließen. Nichtmodale Dialoge bieten zu diesem Zweck meist eine Schaltfläche ÜBERNEHMEN an, die die Dialogdaten in die Anwendung überträgt. Diese Schaltfläche muss die Übertragung der Dialogdaten per DDX veranlassen und dann die Anwendung davon unterrichten, so dass diese gegebenenfalls ihre Darstellung den veränderten Daten anpassen kann.
UpdateData
Um DDX und DDV manuell auszulösen, genügt es, innerhalb der Dialogklasse die Funktion UpdateData aufzurufen. Der Parameter von UpdateData gibt die Richtung der Datenübertragung an. Dabei bedeutet FALSE, dass die Werte der Austauschvariablen in die Steuerelemente des Dialogs übernommen werden, TRUE hingegen liest die Werte der Steuerelemente in die DDX-Variablen ein. UpdateData ruft die Funktion DoDataExchange auf, die den Datentransport durchführt. DoDataExchange wiederum ruft für jedes Steuerelement des Dialogs eine passende Datenaustauschfunktion auf. Daten eines Texteingabefelds werden beispielsweise durch den Aufruf der Funktion DDX_Text übertragen. Alle Datenaustauschfunktionen beginnen mit dem Kürzel DDX_. Analog dazu gibt es Funktionen zur Dialogdatenüberprüfung. Auch diese wer-
Dialogfeldprogrammierung
den in DoDataExchange aufgerufen. Sie beginnen mit dem Kürzel DDV_, wie beispielsweise die Funktion DDV_MinMaxInt, die den Wertebereich einer Integer-Variablen überprüft. Wichtig ist, dass die DDX-Funktion immer vor der dazugehörigen DDV-Funktion ausgeführt werden muss. Die Überprüfung des Wertebereichs kann immer erst nach dem Datenaustausch erfolgen. Um die Anwendung davon zu unterrichten, dass sich die Dialogdaten verändert haben, gibt es keine standardisierte Methode. Es ist denkbar, dass der Dialog der Anwendung eine selbst definierte Windows-Nachricht sendet (WM_APP) oder dass er bestimmte Funktionen der Anwendung aufruft, um die Veränderung anzuzeigen. In Abschnitt 2.7.6, »Dialogimplementierung im Programm StockChart«, wird beschrieben, wie das Programm StockChart dieses Problem löst.
2.7.5
Datenaustausch ohne DDX
Nicht für alle Steuerelemente ist der einfache Datenaustausch über DDX-Variablen möglich. Beispielsweise lässt das IP-Steuerelement keinen einfachen Datenaustausch per DDX zu. Dennoch bietet es sich auch für Steuerelemente, die DDX nicht verwenden können, an, einen Austausch über Austauschvariablen in der Dialogklasse zu realisieren. Der Austausch sollte analog zu DDX vorgenommen werden, nur ist er weniger automatisch. Die Variablen zum Austausch müssen von Hand angelegt werden. Im Konstruktor der Dialogklasse sollten die Austauschvariablen gegebenenfalls initialisiert werden. Falls die Steuerelemente selbst initialisiert werden müssen, so sollte eine Behandlungsfunktion für die WindowsNachricht WM_INITDIALOG erstellt werden. Beim Aufruf dieser Funktion, normalerweise OnInitDialog genannt, sind die Steuerelemente bereits erstellt und es kann auf sie zugegriffen werden. Der Datenaustausch selbst sollte in der Funktion DoDataExchange zusammen mit dem DDX-Datenaustausch vollzogen werden. Dafür gibt es mehrere Gründe: Der Datenaustausch sollte möglichst analog zum Datenaustausch per DDX durchgeführt werden. Durch die Implementierung in DoDataExchange erfolgt der DDXbasierte und der nicht DDX-basierte Datenaustausch zum gleichen Zeitpunkt und sieht für den Programmierer gleich aus. Bei einer durch DDV festgestellten Wertebereichsüberschreitung werden weder DDX noch der nicht DDX-basierte Datenaustausch vorgenommen. Voraussetzung dafür ist, dass sich der Programmcode
119
120
2
Einstieg in die MFC-Programmierung
zum Datenaustausch dieser Variablen hinter den DDX- und DDVFunktionen befindet. Im Falle einer durch DDV festgestellten Bereichsüberschreitung wird DoDataExchange nämlich vorzeitig durch eine Ausnahme verlassen, so dass der Austauschcode dann nicht mehr ausgeführt wird. Allerdings werden Integer- und String-Werte etwas unterschiedlich behandelt. Beispielsweise wird die Länge von Strings in Eingabefeldern schon bei der Eingabe im Dialog begrenzt, so dass keine Ausnahme ausgelöst werden muss. Das Steuerelement, dessen Daten gelesen oder geschrieben werden sollen, wird über eine Instanz einer Steuerelementklasse angesprochen. Für jeden Typ eines Windows-Steuerelements halten die MFC eine Steuerelementklasse bereit. Die Klassen der Steuerelemente sind von CWnd abgeleitet. Eine Auswahl der gängigsten Steuerelementklassen ist in Abbildung 2.53 in Abschnitt 2.8.1, »Das Programm WinControl«, zu sehen. GetDlgItem
Einen Zeiger auf das betreffende Steuerelementobjekt kann man durch die Funktion CDialog::GetDlgItem erhalten. Da GetDlgItem nur einen Zeiger auf ein Objekt der Klasse CWnd zurückgibt, muss der Zeiger vor der Verwendung durch eine Typumwandlung in den Typ des tatsächlichen Steuerelements konvertiert werden. Anschließend wird der Datenaustausch durchgeführt. Da jede Steuerelementklasse eigene Funktionen zum Lesen und Setzen ihrer Datenelemente besitzt, gibt es zur Programmierung des Datenaustauschs keine allgemein gültige Vorgehensweise. Wird der Datenaustausch mit Hilfe von selbst definierten Datenaustauschvariablen implementiert, so ist er außerhalb des Dialogs nicht vom Datenaustausch per DDX zu unterscheiden. Der Programmierer weist vor dem Aufruf des Dialogs den Austauschvariablen ihre Anfangswerte zu. Nach dem Beenden des Dialogs finden sich die im Dialog eingegebenen Werte in den Austauschvariablen. Wird in einem nichtmodalen Dialog UpdateData aufgerufen, um Daten zu übernehmen, so wird auch dann der Austausch durchgeführt, da UpdateData die Funktion DoDataExchange aufruft.
2.7.6 Modaler Dialog
Dialogimplementierung im Programm StockChart
StockChart besitzt zwei Dialoge, einen modalen und einen nichtmodalen. Im modalen Dialog werden Name, Wertpapierkennnummer und Tickerkürzel der Aktie eingegeben oder geändert. Der Dialog wird durch den Menüeintrag AKTIE | EIGENSCHAFTEN aufgerufen. Abbildung 2.51 zeigt diesen Dialog.
Dialogfeldprogrammierung
Abbildung 2.51: Modaler Dialog in StockChart
Die Dialogklasse dieses Dialogs heißt CStockProperty und wurde mit dem Klassen-Assistenten erstellt. Die Implementierung befindet sich in den Dateien STOCKPROPERTY.H und STOCKPROPERTY.CPP. Zum Datenaustausch mittels DDX wurden drei Member-Variablen angelegt: m_name, m_nID und m_ticker. Der Quelltextausschnitt aus STOCKPROPERTY.H in Listing 2.23 zeigt, wie der Assistent die Variablen in die Definition von CStockProperty eingefügt hat. class CStockProperty : public CDialog { DECLARE_DYNAMIC(CStockProperty) public: CStockProperty(CWnd* pParent = NULL); virtual ~CStockProperty(); // Dialog Data enum { IDD = IDD_STOCKPROPERTY }; protected: // DDX/DDV support virtual void DoDataExchange(CDataExchange* pDX); DECLARE_MESSAGE_MAP() public: // Name der Aktie CString m_name; // Wertpapierkennnummer int m_nID; // Tickersymbol CString m_ticker; }; Listing 2.23: Die Klasse für den modalen Dialog
Interessanterweise legt der Klassen-Assistent eine Enumeration mit dem einzigen Eintrag IDD an. IDD wird auf die Ressourcen-ID der Dialogressource gesetzt. Somit kann IDD immer dann verwendet werden, wenn die Ressourcen-ID des Dialogfelds angege-
121
122
2
Einstieg in die MFC-Programmierung
ben werden muss. Dies ist beispielsweise im Konstruktor von CStockProperty der Fall, wie in Listing 2.24 zu sehen ist. Dort werden auch die Datenaustauschvariablen initialisiert: CStockProperty::CStockProperty(CWnd* pParent /*=NULL*/) : CDialog(CStockProperty::IDD, pParent) m_name(_T("")), m_nID(0), m_ticker(_T("")) { } Listing 2.24: Vom Klassen-Assistenten eingefügte Initialisierungen
Der Datenaustausch wird durch die Funktion DoDataExchange vollzogen, die komplett vom Klassen-Assistenten erstellt worden ist (Listing 2.25). void CStockProperty::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, IDC_EDIT1, m_name); DDX_Text(pDX, IDC_EDIT2, m_nID); DDV_MinMaxInt(pDX, m_nID, 0, 999999); DDX_Text(pDX, IDC_EDIT3, m_ticker); DDV_MaxChars(pDX, m_ticker, 10); } Listing 2.25: Datenaustauschfunktionen in DoDataExchange
Die Funktion DDX_Text führt den Datenaustausch für jedes Steuerelement durch. DDV_MinMaxInt sorgt dafür, dass m_nID innerhalb des Wertebereichs von 0 bis 999999 bleibt, DDV-MaxChars begrenzt die Länge des Ticker-Strings auf zehn Zeichen. Bemerkenswert ist, dass der Programmierer durch die Verwendung von DDX und DDV überhaupt nicht mit den Steuerelementen des Dialogfensters in Kontakt kommt. Einzig und allein während der Definition der Austauschvariablen stellt er die Verbindung zum Steuerelement her. Alles Weitere nehmen ihm die MFC ab. Aufgerufen wird der modale Dialog in CStockChartDoc::OnStockProperty (Listing 2.26). void CStockChartDoc::OnStockProperty() { CStockProperty stockPropertyDialog; stockPropertyDialog.m_name = m_name; stockPropertyDialog.m_ticker = m_ticker; stockPropertyDialog.m_nID = m_nID;
Dialogfeldprogrammierung if (IDOK == m_name m_ticker m_nID
OnStockProperty ist als Ereignisbehandlungsfunktion für den Menüeintrag AKTIE | EIGENSCHAFTEN angelegt worden. Warum ist OnStockProperty in der Dokumentenklasse definiert und nicht in der Ansicht, was einem zunächst intuitiv besser erscheint? Die Eigenschaften, die dieser Dialog verändert, sind Eigenschaften des Dokuments und keine Eigenschaften der Ansicht. Wird beispielsweise die Wertpapierkennnummer geändert, so muss deren Repräsentation im Dokumentenobjekt – im Beispielprogramm ist das die Variable CStockChartDoc::m_nID – auf den neuen Wert gesetzt werden. Danach müssen alle Ansichten, die das betreffende Dokument darstellen, von der Änderung informiert werden, damit die Darstellung in allen Ansichten aktualisiert werden kann. Dass OnStockProperty überhaupt in der Dokumentenklasse definiert sein und trotzdem auf Windows-Kommandonachrichten (Windows-Nachrichten vom Typ WM_COMMAND, wie beispielsweise Nachrichten von Menüeinträgen und Steuerelementen) reagieren kann, liegt an der ausgefeilten Nachrichtenbehandlungsarchitektur des Anwendungsgerüsts. Wenn eine Kommandonachricht von der aktiven Ansicht nicht behandelt wird, dann versucht das Anwendungsgerüst, eine Nachrichtenbehandlungsfunktion im zu dieser Ansicht gehörenden Dokument zu finden (siehe Abschnitt 2.3.7, »Nachrichtenverarbeitung«). Der Programmcode von OnStockProperty ist schnell erklärt: Zunächst wird das Dialogobjekt mit den Werten des Dokuments für Name, WKN und Ticker initialisiert. DoModal öffnet das Dialogfenster und wartet, bis der Dialog vom Benutzer geschlossen wird. Danach werden die vom Benutzer eventuell veränderten Werte in das Dokument zurückkopiert. Der anschließende Aufruf von UpdateAllViews sorgt dafür, dass alle Ansichten, die das Dokument darstellen, neu gezeichnet werden. Statt des Werts NULL kann der Funktion auch ein Zeiger auf die Ansicht übergeben
123
124
2
Einstieg in die MFC-Programmierung
werden, die das Update notwendig gemacht hat. Diese Ansicht wird dann nicht von UpdateAllViews benachrichtigt, sie muss sich vorher selbst neu gezeichnet haben. Für das Programm StockChart ist dies nicht relevant, da die Nachrichtenbehandlung direkt im Dokument stattfindet. Der Aufruf von SetModifiedFlag markiert das Dokument als verändert. Der Benutzer wird beim Verlassen des Programms oder beim Schließen aller Ansichten des Dokuments gefragt, ob er die Änderungen speichern möchte. Nichtmodaler Dialog
Mit dem zweiten, nichtmodalen Dialog werden die Farbe der Kurskurve, die Option zum Zeichnen des Gitternetzes und die Einstellungen der Durchschnittslinie angegeben. Der Dialog befindet sich im Menü AKTIE | CHART EINSTELLUNGEN.
Abbildung 2.52: Nichtmodaler Dialog in StockChart
Da die Farbe und der Zustand von Gitternetz und Durchschnittslinie im Dokument gespeichert werden sollen, wirken sich die Einstellungen, wie schon beim modalen Dialog, auf alle Ansichten aus. Daher wird auch dieser Dialog aus der Dokumentenklasse heraus aufgerufen. Alternativ hätte man auch die Farbe und das Gitternetz für jede Ansicht einzeln einstellen können. Auf diese Weise wäre es möglich gewesen, für jede Ansicht einen solchen Dialog aufzurufen. Der dazu notwendige Programmcode müsste dann von der Ansichtsklasse her aufgerufen werden. Allerdings könnte man die Einstellungen nicht speichern, denn welche Einstellung mehrerer möglicher Ansichten eines Dokuments soll man speichern? Doch nun zur Implementierung des Dialogs. Die Dialogklasse für den nichtmodalen Dialog heißt CChartProperty, wurde mit dem Klassen-Assistenten erstellt und befindet sich in den Dateien CHARTPROPERTY.H und CHARTPROPERTY.CPP. Es wurden vier
Dialogfeldprogrammierung
Member-Variablen und drei Nachrichtenbehandlungsfunktionen in die Klasse eingefügt. Listing 2.27 zeigt die Header-Datei der Klasse CChartProperty. class CStockChartDoc; // Vorwärtsdeklaration // CChartProperty dialog class CChartProperty : public CDialog { DECLARE_DYNAMIC(CChartProperty) public: CChartProperty(CStockChartDoc* pDoc, CWnd* pParent = NULL); // standard constructor virtual ~CChartProperty(); // Dialog Data enum { IDD = IDD_CHARTPROPERTY }; protected: // DDX/DDV support virtual void DoDataExchange(CDataExchange* pDX); DECLARE_MESSAGE_MAP() public: // Gitternetz anzeigen? BOOL m_bGrid; // Farbe int m_nColor; // Durchschnittslinie BOOL m_bAverage; // Durchschnittsregler int m_nAverageCnt; protected: enum { sliderRangeMin = 1, sliderRangeMax = 20 }; CStockChartDoc *pDoc; void OnCancel(void); void OnOK(void); public: void OnApplyNow(void); BOOL Create(); }; Listing 2.27: Deklaration der Klasse für den nichtmodalen Dialog
125
126
2
Einstieg in die MFC-Programmierung
Die Variable m_bGrid zeigt an, ob das Gitternetz gezeichnet werden soll, m_nColor gibt die Farbe der Kurve an, und m_bAverage bestimmt, ob die Durchschnittslinie gezeichnet werden soll. Die Variable m_nAverageCnt gibt den Wert des Schiebereglers an. Alle vier Variablen dienen dem Datenaustausch via DDX und DDV. Die Nachrichtenbehandlungsfunktionen OnApplyNow, OnCancel und OnOK reagieren auf das Betätigen der zu ihnen gehörenden Schaltflächen im Dialog. Es ist zu beachten, dass OnCancel und OnOK virtuelle Funktionen sind, die hier überschrieben werden. Die Funktionen der Basisklasse CDialog dürfen keinesfalls aufgerufen werden, da diese ausschließlich für modale Dialoge vorgesehen sind. In nichtmodalen Dialogen müssen OnCancel und OnOK daher immer überschrieben werden, sofern die zugehörigen Schaltflächen (besser gesagt, die von diesen verwendeten IDs IDOK und IDCANCEL) nicht aus der Dialogvorlage entfernt worden sind. In der Klassendefinition von CChartProperty fällt außerdem auf, dass der Konstruktor um einen zusätzlichen Parameter erweitert worden ist. Dieser zusätzliche Parameter ist manuell eingefügt worden. Er ist ein Zeiger auf das Dokument, das den Dialog aufruft (beim Aufruf aus dem Dokumentenobjekt wird this übergeben). Die Dialogklasse speichert diesen Zeiger in der Variablen pDoc, um später Funktionen des Dokumentenobjekts aufrufen zu können (Listing 2.28). CChartProperty::CChartProperty(CStockChartDoc *pDoc, CWnd* pParent /*=NULL*/) : CDialog(CChartProperty::IDD, pParent), pDoc (pDoc), m_bGrid (FALSE), m_nColor (-1), m_bAverage (FALSE), m_nAverageCnt (5) { } Listing 2.28: Konstruktor der Klasse CChartProperty Kommunikation zwischen Dokument und Dialog
Da sich Dokument und Dialogklasse dadurch gegenseitig kennen, kann eine Kommunikation zwischen ihnen von beiden Seiten her stattfinden. Dies ist bei nichtmodalen Dialogen wichtig, da das Programm den Dialog von Änderungen unterrichten können muss und umgekehrt. Schließt der Benutzer beispielsweise die letzte Ansicht eines Dokuments, so muss das Dokument dafür
Dialogfeldprogrammierung
Sorge tragen, dass der Dialog geschlossen wird. Umgekehrt muss die Dialogklasse das Dokument benachrichtigen, wenn der Benutzer auf ÜBERNEHMEN geklickt hat, damit das Dokument die Änderungen zur Kenntnis nimmt und alle Ansichten benachrichtigt, sich neu zu zeichnen. Genau zu diesem Zweck rufen die Funktionen OnApplyNow, OnCancel und OnOK (Listing 2.29) die Funktion CStockChartDoc::OnPropertyChange auf. Dies ist keine MFC-Funktion, das Programm StockChart implementiert hier einen eigenen Mechanismus, da es in den MFC keinen standardisierten Weg gibt, die Kommunikation zwischen Programmen und nichtmodalen Dialogen zu handhaben. void CChartProperty::OnApplyNow() { //DDX ausführen UpdateData (TRUE); pDoc->OnPropertyChange (TRUE, FALSE); } void CChartProperty::OnCancel() { pDoc->OnPropertyChange (FALSE, TRUE); } void CChartProperty::OnOK() { //DDX ausführen UpdateData (TRUE); pDoc->OnPropertyChange (TRUE, TRUE); } Listing 2.29: OnApplyNow, OnCancel und OnOK
Bevor die Funktion OnPropertyChange aufgerufen wird, um das Dokument zu benachrichtigen, werden die Werte der Steuerelemente durch den Aufruf von UpdateData in die Member-Variablen des Dialogs übertragen. Die Funktion Create (Listing 2.30) der Dialogklasse wurde überschrieben, um den Namen der Aktie in den Titel des Dialogs zu übernehmen. Schließlich ist das Programm StockChart eine MDIAnwendung und für jedes geöffnete Dokument kann ein nichtmodaler Dialog geöffnet sein. Der Aktienname im Titel dient dann zur Unterscheidung der Dialoge. Die Funktion Create wird von der Entwicklungsumgebung mit zwei Parametern für Dialogvorlagen-
127
128
2
Einstieg in die MFC-Programmierung
name und übergeordnetes Fenster erzeugt. Da diese im Beispiel nicht benötigt werden, hat die Funktion hier keine Parameter. Zur Identifikation der Dialogvorlage wird stattdessen die Funktion der Superklasse mit der IDD der Dialogvorlage aufgerufen. BOOL CChartProperty::Create() { // Den Namen der Aktie in den Titel übernehmen if (CDialog::Create(IDD)) { ASSERT_VALID (pDoc); SetWindowText (pDoc->m_name); return true; } else { return false; } } Listing 2.30: CChartProperty::Create Datenaustausch
Der Datenaustausch zwischen den Steuerelementen des Dialogs und den Datenaustauschvariablen findet – wie beim modalen Dialog auch – in DoDataExchange statt. Das ist in Listing 2.31 zu sehen. void CChartProperty::DoDataExchange(CDataExchange* pDX) { DDX_Slider(pDX, IDC_SLIDER_AVERAGE, m_nAverageCnt); DDX_Check(pDX, IDC_CHECK_LINE, m_bAverage); DDX_Radio(pDX, IDC_RADIO_RED, m_nColor); DDX_Check(pDX, IDC_CHECK_GRID, m_bGrid); CDialog::DoDataExchange(pDX); } Listing 2.31: Datenaustausch in DoDataExchange
Die Funktion DoDataExchange führt den Datenaustausch in beiden Richtungen durch: von den Austauschvariablen zu den Steuerelementen und umgekehrt. In welcher Richtung der Datentransport erfolgen soll, lässt sich am übergebenen CDataExchangeObjekt ablesen. CDataExchange besitzt ein Flag namens m_bSave AndValidate. Hat dieses den Wert TRUE, dann werden Daten aus den Steuerelementen in die Austauschvariablen eingelesen. Der Wert FALSE bedeutet, dass die Steuerelemente des Dialogs initialisiert werden. Dialoginitialisierung
Bevor der Dialog angezeigt wird, muss das Schieberegler-Steuerelement zum Einstellen der Durchschnittskurve auf einen Wertebereich eingestellt werden. Das Einstellen der Durchschnittskurve muss zu einem Zeitpunkt geschehen, zu dem das Steuerelement
Dialogfeldprogrammierung
129
bereits erzeugt, der Dialog aber noch nicht sichtbar ist. Genau zu diesem Zweck sendet Windows dem Dialog eine WM_INITDIALOG-Nachricht. Die Klasse CDialog behandelt diese Nachricht in der virtuellen Funktion OnInitDialog. Diese Funktion lässt sich für eigene Dialogklassen überschreiben. Das Programm StockChart benutzt die Funktion OnInitDialog, um darin das Regler-Steuerelement auf einen Bereich zwischen 1 und 20 zu setzen. Dazu wird mit der Funktion GetDlgItem ein Zeiger auf ein Objekt der Klasse CSliderCtrl angefordert. Der Aufruf von CSliderCtrl::SetRange legt den Wertebereich fest (Listing 2.32). BOOL CChartProperty::OnInitDialog() { CDialog::OnInitDialog(); CSliderCtrl *sl = (CSliderCtrl*) GetDlgItem (IDC_SLIDER_AVERAGE); sl->SetRange (sliderRangeMin, sliderRangeMax); return TRUE; } Listing 2.32: Behandlungsfunktion für WM_INITDIALOG
Doch nun zum Aufruf des Dialogfelds aus der Dokumentenklasse: Der nichtmodale Dialog in StockChart wird aus der Funktion CStockChartDoc::OnChartProperty (Listing 2.33) heraus aufgerufen. Diese Funktion wird ihrerseits durch den Menüeintrag AKTIE | CHART EINSTELLUNGEN gestartet. void CStockChartDoc::OnChartProperty() { // Nur erzeugen, wenn es noch keine Instanz gibt: if (NULL == m_pChartProperty) { // Dialogobjekt erzeugen m_pChartProperty = new CChartProperty (this); // Nun die Daten übertragen m_pChartProperty->m_bGrid = m_bGrid; m_pChartProperty->m_bAverage = m_bAverage; if (GetRValue (m_nColor) > 0) m_pChartProperty->m_nColor = 0; else if (GetGValue (m_nColor) > 0) m_pChartProperty->m_nColor = 1; else m_pChartProperty->m_nColor = 2; m_pChartProperty->m_nAverageCnt = m_nAverageCnt;
OnChartProperty benutzt die Member-Variable m_pChartProperty, um auf den nichtmodalen Dialog zuzugreifen. Der Dialog kann so lange existieren, wie es das zu ihm gehörende Dokument gibt. Hat m_pChartProperty den Wert NULL, so bedeutet dies, dass keine Instanz der Dialogklasse existiert. Wird ein Dokument erzeugt, so muss im Konstruktor der Dokumentenklasse die Variable m_pChartProperty auf den Wert NULL gesetzt werden. Sofern noch keine Instanz der Dialogklasse existiert, m_pChartProperty also NULL ist, legt OnChartProperty eine Instanz der Dialogklasse auf dem Heap an. Dabei wird dem Konstruktor der Dialogklasse ein Zeiger auf das Dokument (this) übergeben. Die Instanzvariablen der Dialogklasse werden initialisiert und das Dialogfenster wird durch Aufruf der Funktion Create angezeigt. Jede Betätigung einer Schaltfläche innerhalb des Dialogs löst einen Aufruf der Funktion OnPropertyChange (Listing 2.34) aus. void CStockChartDoc::OnPropertyChange (BOOL bUpdateData, BOOL bCloseWindow) { if (bUpdateData) { m_bGrid = m_pChartProperty->m_bGrid; m_bAverage = m_pChartProperty->m_bAverage; m_nAverageCnt = m_pChartProperty->m_nAverageCnt; switch (m_pChartProperty->m_nColor) { default: case 0: m_nColor = RGB(255,0,0); break; case 1: m_nColor = RGB(0,255,0); break; case 2: m_nColor = RGB(0,0,255); break; } // switch
Die beiden übergebenen Parameter bestimmen, welche Aktionen ausgeführt werden müssen. Wenn bUpdateData den Wert TRUE hat, soll eine Datenübernahme durchgeführt werden. Der Datenaustausch selbst ist beim Aufruf der Funktion OnPropertyChange bereits durchgeführt worden. Falls eine Datenübernahme durchgeführt werden soll, hat das Dialogobjekt bereits die Funktion UpdateData vor der Funktion OnPropertyChange aufgerufen. UpdateData wiederum hat bereits DoDataExchange aufgerufen, so dass in der Funktion OnPropertyChange die Werte in den Austauschvariablen gültig sind und einfach ausgelesen werden können. Die Durchschnittskurve wird anschließend durch den Aufruf der Funktion CalcAverages für den Fall neu berechnet, dass sich der Wert von m_nAverageCnt verändert hat. Wollte man das Programm optimieren, so könnte man sich den vorherigen Wert von m_nAverageCnt merken und vergleichen, ob sich der Wert verändert hat. Die Durchschnittskurve müsste nur dann neu berechnet werden, wenn sich der Wert tatsächlich verändert hat. Um den Dialog zu schließen, wird CDialog::DestroyWindow aufgerufen. Der delete-Operator löscht die Dialogklasse und damit auch den Dialog. Doch was geschieht, wenn das Dokument zerstört wird, weil die letzte Ansicht geschlossen wurde, während der Dialog noch offen war? Dazu wird im Destruktor der Dokumentenklasse das Dialogfenster geschlossen und die Dialogklasse gelöscht. Das ist in Listing 2.35 zu sehen.
131
132
2
Einstieg in die MFC-Programmierung
CStockChartDoc::~CStockChartDoc() { // Wenn Dialog noch offen, dann schließen und löschen if (m_pChartProperty) { m_pChartProperty->DestroyWindow (); delete m_pChartProperty; } } Listing 2.35: Destruktor des Dokuments
Sofern der Dialog bei der Zerstörung des Dokuments noch offen ist, wird er geschlossen!
2.7.7
Tipps zur Vorgehensweise
왘 Standarddialoge können im Allgemeinen direkt durch das Instanziieren der sie repräsentierenden Dialogklasse verwendet werden. Ableitungen von den Klassen der Standarddialoge sind nur notwendig, wenn der Dialog ergänzt oder verändert werden soll. 왘 Selbst erstellte Dialoge, egal ob modal oder nichtmodal, werden von der Klasse CDialog abgeleitet und basieren auf einer mit dem Ressourceneditor erstellten Dialogvorlage. 왘 Modale Dialoge werden durch die Funktion DoModal aufgerufen. Diese Funktion kehrt erst zurück, wenn der Benutzer den Dialog schließt. 왘 Modale Dialogobjekte sollten auf dem Stack angelegt werden. 왘 Zur Verwendung des Dialogdatenaustauschs und der Dialogdatenüberprüfung werden Member-Variablen innerhalb der Dialogklasse mit dem Assistenten erstellt. Vor der Anzeige des Dialogfensters müssen diese Variablen initialisiert werden. Bei modalen Dialogen enthalten diese Variablen nach Rückkehr der Funktion DoModal die vom Benutzer eingegebenen Werte. 왘 Steuerelemente müssen bei Verwendung von DDX und DDV nicht selbst angesprochen werden. 왘 Nichtmodale Dialoge werden durch den Aufruf der Funktion Create angezeigt und durch DestroyWindow wieder geschlossen. Das Dialogobjekt muss auf dem Heap angelegt werden.
Dialogfeldprogrammierung
왘 Zur Kommunikation zwischen Programm und nichtmodalem Dialog muss der Programmierer selbst einen geeigneten Mechanismus implementieren. Dies kann beispielsweise dadurch realisiert werden, dass Dialogklasse und Dokument oder Dialogklasse und Ansicht Zeiger aufeinander halten. 왘 DDX zwischen nichtmodalem Dialog und Programm kann jederzeit durch den Aufruf der Funktion CDialog::UpdateData angestoßen werden. Der Parameter von UpdateData gibt dabei die Richtung an. FALSE bedeutet, dass die Werte der DDXVariablen in die Steuerelemente des Dialogs übernommen werden, TRUE gibt die umgekehrte Richtung an. 왘 Bei einigen Standardsteuerelementen kann der Datenaustausch zwischen Dialog und Programm nicht über DDX abgewickelt werden. Es bietet sich an, selbst einen Mechanismus zu implementieren, der ebenfalls die Funktion DoDataExchange nutzt und sich wie DDX verhält. DDX und DDV können allerdings durch benutzerdefinierte Funktionen erweitert werden. Der technische Hinweis 26 beschreibt die genaue Vorgehensweise.
2.7.8
Zusammenfassung
Dialoge sind ein wichtiges Hilfsmittel in fast jedem Programm, um Informationen auszugeben sowie Daten und Einstellungen einzugeben. Windows und die MFC bieten eine Reihe von Dialogklassen für Standardaufgaben, die einfach zu verwenden sind. Reichen diese nicht aus, so können eigene Dialoge erstellt werden, die auf Ressourcenvorlagen basieren. Dabei bietet Visual Studio .NET umfangreiche Unterstützung bei der Erstellung der Dialogklasse und der Anlage von Variablen zum Dialogdatenaustausch. Der Dialogdatenaustausch vereinfacht den Umgang mit den Steuerelementen eines Dialogs deutlich. Anstatt die Steuerelemente direkt anzusprechen, muss (zumindest bei einfachen Steuerelementen) nur der Wert der DDX-Variablen, die das Steuerelement repräsentiert, gesetzt oder gelesen werden. DDV wacht nebenbei über die Einhaltung vorgegebener Wertebereiche. Daneben bieten die MFC hervorragende Unterstützung für den Einsatz von ActiveX-Steuerelementen in Dialogen. Das wird jedoch erst Thema in Kapitel 3, »COM, OLE und ActiveX«, sein.
133
134
2
2.8
Einstieg in die MFC-Programmierung
Mehr zu Steuerelementen
Nachdem in Abschnitt 2.7, »Dialogfeldprogrammierung«, die grundsätzliche Funktionsweise und der Verwendungszweck von Steuerelementen bereits vorgestellt worden sind, soll nun eine Übersicht über die wichtigsten Steuerelemente von Windows gegeben werden. Steuerelemente sind das Hauptmittel zur Interaktion mit dem Benutzer. Das ist einer der Gründe, warum es eine große Anzahl von ihnen gibt.
2.8.1
Das Programm WinControl
Die meisten Programme verwenden – genau wie das Programm StockChart – meist nur eine kleine Auswahl an Steuerelementen. Alle Steuerelemente von Windows zusammen lassen sich kaum sinnvoll in einem Dialogfeld nutzen. Daher soll an dieser Stelle das Programm WinControl vorgestellt werden, dessen einzige Aufgabe es ist, die wichtigsten Steuerelemente von Windows vorzustellen. Das Programm WinControl befindet sich auf der Begleit-CD im Verzeichnis KAPITEL2\WINCONTROL. Abbildung 2.53 zeigt die Steuerelementklassen der MFC. Das Beispielprogramm WinControl verwendet die meisten dieser Steuerelemente. Das Programm WinControl verwendet drei Dialoge, um die Steuerelemente vorzustellen, da kaum alle Steuerelemente in einem Dialog unterzubringen sind. Ein vierter Dialog im Programm WinControl zeigt, wie man einen Eigenschaftsdialog erstellt. Das Programm WinControl ist mit dem Anwendungs-Assistenten erstellt worden. Im Gegensatz zum Programm StockChart ist es eine SDI-Anwendung, so dass immer genau eine Ansicht aktiv ist. Daher befindet sich der Programmcode, um die drei Dialoge aufzurufen, beim Programm WinControl in der Ansichtsklasse. Auf Status- und Symbolleisten wurde beim Programm WinControl verzichtet. Der Dialog mit dem ersten Teil der Steuerelemente lässt sich durch den Menüeintrag STEUERELEMENTE | TEIL 1 aufrufen. Die einzelnen Steuerelemente des Dialogs sollen nun kurz vorgestellt werden.
In Dialogen lassen sich Icons und Bitmaps als Bilder zur Anzeige grafischer Informationen verwenden. Allerdings werden diese normalerweise nur statisch verwendet, das heißt, sie werden im Dialog nicht verändert. Als Ressourcen-ID wird IDC_STATIC verwendet, ein Zeichen dafür, dass diese Ressource vom Programm nicht angesprochen werden muss. Statt Icons oder Bitmaps können auch einfache Rahmen oder Rechtecke angezeigt werden. Zur Verwendung dieses Steuerelements muss kein Programmcode geschrieben werden, die Zuweisung einer Icon- oder Bitmap-ID geschieht im Ressourceneditor.
Eingabefelder Eingabefelder werden bereits im Programm StockChart verwendet. Dort werden damit Aktienname, WKN und Tickersymbol im modalen Dialog eingegeben. Es gibt sowohl numerische, als auch textbasierte Eingabefelder. Es existieren weitere Optionen für Passwortfelder, mehrzeilige Eingabefelder und für die Ausrichtung des angezeigten Texts (links, rechts, zentriert). Der Datenaustausch über DDX gestaltet sich einfach, es ist sowohl ein Austausch über numerische Werte als auch über CString-Objekte möglich.
Schaltflächen Schaltflächen werden praktisch in jedem Dialog verwendet. Für die häufig verwendeten Schaltflächen OK und ABBRECHEN sind in den MFC bereits die Funktionen CDialog::OnOK und CDia-
Mehr zu Steuerelementen
log::OnCancel vordefiniert. Diese können vom Programmierer überschrieben werden, um angemessen auf die Betätigung dieser Schaltflächen zu reagieren.
Kontrollkästchen Auch Kontrollkästchen wurden bereits im Programm StockChart verwendet. Mit ihnen können die Durchschnittslinie und das Gitternetz aktiviert werden. Kontrollkästchen repräsentieren einen Booleschen Wert.
Optionsfelder Optionsfelder werden immer als Gruppe verwendet. Sie dienen zur Auswahl eines Werts aus einer Gruppe von Werten. Beim Datenaustausch per DDX wird die Gruppe durch nur eine DDXVariable vom Typ int repräsentiert. Diese Variable ist ein Index innerhalb der Gruppe von Optionsfeldern. Beim jeweils ersten Optionsfeld in einer Gruppe muss das Gruppen-Flag gesetzt sein. Bei allen anderen Optionsfeldern darf es nicht gesetzt sein.
Gruppenfelder Gruppenfelder sind passive Steuerelemente, die zur Gruppierung anderer Steuerelemente verwendet werden. Da Gruppenfelder lediglich einfache grafische Elemente darstellen, besitzen sie die ID IDC_STATIC.
Statischer Text Genau wie Bitmaps und Icons wird statischer Text normalerweise nicht verändert. Daher bekommt er auch die Ressourcen-ID IDC_STATIC zugewiesen. Man kann diese ID jedoch trotzdem abändern. Verwendet man eine von IDC_STATIC abweichende ID, dann lassen sich plötzlich auch für statischen Text DDX-Austauschvariablen des Typs CString anlegen. Damit lässt sich der Text vor oder während der Anzeige des Dialogs verändern. Das Programm WinControl demonstriert dies.
Listenfelder Mit Listenfeldern kann eine Reihe von Strings angezeigt werden. Leider lassen sich diese Strings nicht in der Ressource speichern. Daher muss bei der Initialisierung des Dialogs auch eine Initiali-
137
138
2
Einstieg in die MFC-Programmierung
sierung der verwendeten Listenfelder vorgenommen werden. Außerdem funktioniert DDX bei Listenfeldern leider nur in einer Richtung. Listenfelder können über DDX nur ausgelesen werden.
Bildlaufleisten WM_HSCROLL, WM_VSCROLL
Innerhalb von Dialogfeldern werden Bildlaufleisten ähnlich wie andere Steuerelemente verwendet. Damit eine Bildlaufleiste bedient werden kann, muss allerdings immer etwas Programmcode erstellt werden, um die Nachrichten WM_HSCROLL (für horizontale Bildlaufleisten) und WM_VSCROLL (für vertikale Bildlaufleisten) zu behandeln. Werden diese Nachrichten nicht korrekt behandelt, lässt sich eine Bildlaufleiste nicht bedienen. Das Programm WinControl zeigt eine einfache Implementierung. Wer Bildlaufleisten in Programmen des Anwendungsgerüsts verwenden möchte, der kann als Basis für die Ansicht die Klasse CScrollView statt der Klasse CView verwenden. Die Ansteuerung der Bildlaufleisten wird von ihr automatisch erledigt.
Kombinationsfelder Kombinationsfelder ähneln Listenfeldern, bieten allerdings mehr Möglichkeiten. Kombinationsfelder treten in drei Erscheinungsformen auf: als einfaches Kombinationsfeld, als Drop-down-Kombinationsfeld und als Drop-down-Listenfeld. Bei einem einfachen Kombinationsfeld ist genau wie bei einem Listenfeld eine Liste von Auswahlmöglichkeiten vorhanden. In einem extra Eingabefeld oberhalb der Liste lassen sich zusätzliche Eingaben vornehmen, die nicht Teil der Liste sein müssen. Das Kombinationsfeld ist also eine Kombination aus freier Eingabe und einer Auswahlliste. Bei den Drop-down-Varianten des Kombinationsfelds wird die Liste erst angezeigt, wenn man sie herunterklappt. Das Dropdown-Listenfeld ist kein echtes Kombinationsfeld, da die freie Eingabe fehlt. Es verhält sich eher wie das normale Listenfeld, wird aber trotzdem durch das Kombinationsfeld-Steuerelement repräsentiert. Im Beispielprogramm WinControl wird ein Dropdown-Kombinationsfeld verwendet. Im Gegensatz zu den Listenfeldern funktioniert der Datenaustausch per DDX bei Kombinationsfeldern in beiden Richtungen. Auch lassen sich bei Kombinationsfeldern im Gegensatz zu Listenfeldern die Daten der Auswahlliste in der Ressource speichern. Durch die bessere DDX-Unterstützung und die Option, Daten in
Mehr zu Steuerelementen
139
der Steuerelementressource zu speichern, lassen sich Kombinationsfelder einfacher programmieren als Listenfelder. Weitere Steuerelemente sind im zweiten und dritten Dialog des Programms WinControl vertreten. Diese Dialoge lassen sich unter den Menüpunkten STEUERELEMENTE | TEIL 2 und STEUERELEMENTE | TEIL 3 aufrufen.
Abbildung 2.55: Steuerelemente Teil 2
Drehfelder Drehfelder sehen aus wie Bildlaufleisten, deren Mittelteil fehlt. Es gibt sie sowohl in horizontaler als auch in vertikaler Ausrichtung. Sie werden meist in Verbindung mit einem Eingabefeld benutzt und dienen dazu, den im Eingabefeld angezeigten Wert mit der Maus zu erhöhen oder zu vermindern. Das Eingabefeld wird als »Buddy« (Kumpel, Freund) des Drehfelds bezeichnet. Wenn das Eingabefeld nur ganzzahlige numerische Werte entgegennehmen soll, dann lässt sich das Drehfeld sogar völlig ohne Programmierung verwenden.
Statusanzeigen Statusanzeigen sind aus den Kopierdialogen des Windows-Explorers hinlänglich bekannt. Sie implementieren eine einfache Fortschrittsanzeige.
Regler Regler ähneln den Bildlaufleisten. Im Gegensatz zu diesen werden mit Reglern allerdings Werte eingestellt. Die Programmierung von Reglern gestaltet sich einfacher als die von Bildlaufleisten, da
Buddy
140
2
Einstieg in die MFC-Programmierung
keine Windows-Nachrichten behandelt werden müssen. Sie lassen sich horizontal oder vertikal ausrichten, es gibt aber leider nur Schieberegler und keine Drehregler. Die Form des Reglerknopfs kann verändert werden und es ist möglich eine Skala einzublenden. Der Regler verwendet einen Regelbereich, der vom Programmierer gesetzt wird. Verwendet man einen kleinen Regelbereich, so bewegt sich der Regler in Schritten. Bei hinreichend großem Regelbereich läuft der Regler »weich«.
Animationselemente Animationselemente haben – im Gegensatz zu ihrer dynamischen äußeren Erscheinung – einen eher statischen Charakter. Mit Animationselementen können kleine AVI-Filmchen abgespielt werden. Der vom Animationselement abzuspielende Film kann als Datei oder als Ressource vorliegen. Der Film kann gestartet, gestoppt oder in einer Endlosschleife abgespielt werden. Zusätzlich lassen sich einzelne Frames des Films als Standbilder anzeigen. Weitere Kontrollmöglichkeiten gibt es bei Animationselementen nicht.
Listenelemente Listenelemente sehen auf den ersten Blick wie Listenfelder aus, bieten aber ungleich mehr Möglichkeiten. Listenelemente ordnen jedem ihrer Einträge ein Icon zu. Die Liste kann in vier verschiedenen Ansichten betrachtet werden: Minisymbol, Symbol, Liste und Bericht. Diese vier Ansichten stimmen nicht nur zufällig mit den Ansichten des Windows-Explorers überein, es handelt sich um die gleiche Art der Darstellung. In der Ansicht »Bericht« kann ein Listeneintrag noch weitere Texteinträge anzeigen. Zusätzlich kann ein Spaltenkopf angezeigt werden.
Strukturansichten Auch Strukturansichten sind bereits aus dem Windows-Explorer bekannt. Der Explorer verwendet sie, um Verzeichnisbäume darzustellen. Allgemein dient die Strukturansicht der Darstellung hierarchischer Strukturen. Genau wie bei den Listenelementen gibt es auch bei den Strukturansichten eine ganze Reihe von Anzeigeoptionen, die sich in der Ressource einstellen lassen. So lassen sich in der Strukturansicht Linien an- und ausschalten, die Symbole können ausgeblendet werden und es gibt die Möglichkeit, zusätzlich zu den Symbolen kleine Optionsfelder in die Strukturansicht aufzunehmen.
Mehr zu Steuerelementen
Rich-Edit-Eingabefelder Rich-Edit-Eingabefelder können als eine Erweiterung der normalen Eingabefelder aufgefasst werden. Sie ermöglichen es, formatierten Text ein- und auszugeben.
Abbildung 2.56: Steuerelemente Teil 3
Kalendersteuerelemente Kalendersteuerelemente ermöglichen die Auswahl eines Datums aus einer Monatsübersicht. Die Monatsübersicht hat einen Aufbau, den man aus Kalendern kennt: Die Tage sind wochenweise angeordnet. Kalendersteuerelemente bieten Optionen zum Anzeigen des aktuellen Tages und der Kalenderwochen. Es kann durch die Monate und die Jahre navigiert werden.
Steuerelemente zur Datums- und Zeitauswahl Neben den Kalendersteuerelementen gibt es zur Auswahl eines Datums diese einfacheren Steuerelemente. Sie können im Ressourceneditor zwischen der Auswahl von Datum und Uhrzeit umgeschaltet werden. Die Datumsauswahl kann mit einem Drehfeld kombiniert werden oder in Form eines Kombinationsfelds erscheinen.
141
142
2
Einstieg in die MFC-Programmierung
Erweiterte Kombinationsfelder Erweiterte Kombinationsfelder unterscheiden sich von den einfachen Kombinationsfeldern durch die Möglichkeit, Symbole innerhalb der Einträge anzuzeigen. Die Symbole werden wie bei Listenelementen und Strukturansichten durch eine Bilderliste bereitgestellt.
IP Steuerelemente IP Steuerelemente haben einen sehr eingeschränkten Verwendungsbereich, sie lassen sich tatsächlich nur zur Eingabe von Internetadressen verwenden, da in jedem Segment der Steuerelemente nur Zahlen zwischen 0 und 255 eingegeben werden können. Damit IP Steuerelemente verwendet werden können, muss der Microsoft Internet Explorer Version 4.0 oder höher installiert sein.
t
Für die Steuerelemente Eingabefeld, Listenelement, Strukturansicht und Rich-Edit-Eingabefeld gibt es Ansichtsklassen (CEditView, CListView, CTreeView und CRichEditView), mit denen sich diese Steuerelemente direkt als Ansichten in der Dokument-Ansicht-Architektur verwenden lassen. Nach der Vorstellung der allgemeinen Eigenschaften soll nun die Verwendung der Steuerelemente innerhalb des Programms WinControl besprochen werden. Um die drei Dialoge anzusprechen, wurden mit dem Klassen-Assistenten die Dialogklassen CControlDlg1, CControlDlg2 und CControlDlg3 angelegt. Alle Dialoge werden modal verwendet. Der Aufruf erfolgt aus der Ansichtsklasse heraus. CControlDlg1 wird in OnControlsPart1 aufgerufen (Listing 2.36). void CWinControlView::OnControlsPart1() { CControlDlg1 dlg; // DDX-Variablen zuweisen dlg.m_strEdit = "Gute Reise"; // Eingabefeld dlg.m_bCheck = TRUE; // Kontrollkästchen dlg.m_nRadio = 1; // Optionsfeld dlg.m_strCombo = "Deutschland"; // Kombinationsfeld dlg.m_strStatic = "¡Vamos a la playa!"; // statischer Text dlg.DoModal ();
Mehr zu Steuerelementen // // // // // // //
Jetzt ... = ... = ... = ... = ... = ... =
können alle Austauschvariablen gelesen werden. dlg.m_strEdit; dlg.m_bCheck; dlg.m_nRadio; dlg.m_strList; dlg.m_nScroll; dlg.m_nCombo;
} Listing 2.36: Aufruf des ersten Steuerelementdialogs
Der erste Dialog verwendet DDX, um das Dialogfeld zu initialisieren. Es können allerdings nicht alle Steuerelemente DDX in beiden Richtungen verwenden. Bei Listenfeldern und Bildlaufleisten ist das Schreiben nicht möglich, bei statischem Text macht ein Lesen keinen Sinn, da der Text vom Benutzer nicht verändert werden kann. Da das Listenfeld – im Gegensatz zum Kombinationsfeld – keine Daten in der Ressource ablegen kann, muss es bei der Initialisierung des Dialogfelds mit den Listendaten versorgt werden. Dies lässt sich innerhalb der Funktion OnInitDialog vornehmen. Man legt diese Funktion als Behandlungsroutine für die Dialog-Nachricht WM_INITDIALOG an. BOOL CControlDlg1::OnInitDialog() { CDialog::OnInitDialog(); // Initialisierung des Listenfelds: CListBox *lb = (CListBox*) GetDlgItem (IDC_LIST1); lb->InsertString (-1, "Hamburg"); lb->InsertString (-1, "Berlin"); lb->InsertString (-1, "Köln"); lb->InsertString (-1, "München"); lb->SetCurSel (1); // Bildlaufleiste(ScrollBar) initialisieren: CScrollBar *sb = (CScrollBar*) GetDlgItem (IDC_SCROLLBAR1); sb->SetScrollRange (scrollMin, scrollMax, FALSE); sb->SetScrollPos ((scrollMax-scrollMin)/2); return TRUE; } Listing 2.37: CControlDlg1::OnInitDialog: Initialisierung von Listenfeld und Bildlaufleiste
143
144
2
Einstieg in die MFC-Programmierung
OnInitDialog
In OnInitDialog (Listing 2.37) wird zunächst ein Zeiger auf ein Objekt der Klasse CListBox angefordert. CListBox ist die zu Listenfeldern gehörende Steuerelementklasse. Mit der Member-Funktion InsertString der Steuerelementklasse werden vier Einträge in das Listenfeld übernommen. Anschließend wird mit SetCurSel der zweite Eintrag selektiert.
CScrollBar
Auch die Bildlaufleiste muss vor der Benutzung initialisiert werden. Die zu den Bildlaufleisten gehörende Steuerelementklasse ist CScrollBar. Zur Initialisierung wird der Bildlaufleiste – ebenfalls in OnInitDialog – ein Wertebereich (Scrollrange) zugewiesen. Außerdem wird sie in Mittelstellung gebracht. Damit die Bildlaufleiste funktioniert, muss allerdings die Windows-Nachricht WM_HSCROLL (WM_VSCROLL bei vertikalen Bildlaufleisten) korrekt behandelt werden. Dazu wird die Nachrichtenbehandlungsfunktion OnHScroll eingefügt. void CControlDlg1::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { UINT nCurrentPos; nCurrentPos = pScrollBar->GetScrollPos (); switch (nSBCode) { case SB_THUMBPOSITION: pScrollBar->SetScrollPos (nPos); break; case SB_LINELEFT: pScrollBar->SetScrollPos ( max (scrollMin, nCurrentPos-1)); break; case SB_LINERIGHT: pScrollBar->SetScrollPos ( min (scrollMax, nCurrentPos+1)); break; case SB_PAGELEFT: pScrollBar->SetScrollPos ( max (scrollMin, nCurrentPos-scrollPage)); break; case SB_PAGERIGHT: pScrollBar->SetScrollPos ( min (scrollMax, nCurrentPos+scrollPage));
Mehr zu Steuerelementen
145
break; } CDialog::OnHScroll(nSBCode, nPos, pScrollBar); } Listing 2.38: Behandlung der Bildlaufleiste in OnHScroll
OnHScroll (Listing 2.38) behandelt verschiedene Fälle: Der Benutzer klickt auf die kleinen Pfeile der Bildlaufleiste (SB_LINELEFT und SB_LINERIGHT), er blättert, indem er auf den Balken klickt (SB_PAGELEFT und SB_PAGERIGHT), oder er zieht den Anfasser mit der Maus (SB_THUMBPOSITION). Alle anderen Steuerelemente kommen dank DDX ohne weitergehende Initialisierungen oder Behandlungsfunktionen aus. Will man jedoch mehr Kontrolle über diese Steuerelemente bekommen, so kann man auf sie durch eine Instanz einer Steuerelementklasse zugreifen. Für jedes Steuerelement existiert eine Klasse, die es repräsentiert. Mit GetDlgItem erhält man einen Zeiger auf eine Instanz dieser Klasse. Damit lassen sich die – je nach Klasse unterschiedlichen – Member-Funktionen aufrufen. Nicht immer ganz so einfach gestaltet sich die Verwendung der Standardsteuerelemente, von denen die meisten im zweiten Dialog vertreten sind. Auch für den zweiten Dialog wurde eine Dialogklasse mit dem Klassen-Assistenten erstellt: CControlDlg2. Der Dialog wird genau wie der erste aus der Ansicht heraus aufgerufen (Listing 2.39). void CWinControlView::OnControlsPart2() { CControlDlg2 dlg; dlg.m_nProgress = 30; // Statusanzeige dlg.m_nSlider = 10; // Regler dlg.DoModal (); // // // // //
Jetzt ... = ... = ... = ... =
können alle Austauschvariablen gelesen werden. dlg.m_nSlider; dlg.m_strList; dlg.m_strTree; dlg.m_strRichEdit;
Listing 2.39: Aufruf des zweiten Steuerelementdialogs
Standardsteuerelemente
146
2
Einstieg in die MFC-Programmierung
Auch für den zweiten Dialog wird eine Reihe von Austauschvariablen verwendet, allerdings muss teilweise auf DDX verzichtet werden, da nicht alle Standardsteuerelemente DDX unterstützen. In CONTROLDLG2.H (Listing 2.41) werden DDX-Austauschvariablen für den Regler und das Rich-Edit-Eingabefeld deklariert. Daneben werden Austauschvariablen für den manuellen Datenaustausch mit den anderen Steuerelementen angelegt. Eine Enumeration legt drei Wertebereiche für das Drehfeld, die Statusanzeige und den Regler fest. // ControlDlg2.h : Header-Datei // ////////////////////////////////////////////////////////////// / // Dialogfeld CControlDlg2 class CControlDlg2 : public CDialog { // Konstruktion public: CControlDlg2(CWnd* pParent = NULL); Standardkonstruktor
//
// Dialogfelddaten enum { IDD = IDD_DIALOG2 }; int m_nSlider; // DDX-Variablen CString m_strRichEdit; // Definition der (nicht-DDX) Austauschvariablen int m_nProgress; // Statusanzeige CString m_strList; // Listenelement CString m_strTree; // Strukturansicht // Überschreibungen protected: // DDX/DDV-Unterstützung virtual void DoDataExchange(CDataExchange* pDX); // Implementierung protected: // Wertebereiche für das Drehfeld, die Statusanzeige // und den Regler enum { spinMin = 0, spinMax = 100, progressMin = 0, progressMax = 50, progressStep = 1,
Damit ein Dialog, der Rich-Edit-Steuerelemente enthält, erfolgreich aufgerufen werden kann, muss zuvor die Funktion AfxInitRichEdit aufgerufen werden. Im Beispielprogramm wird AfxInitRichEdit daher in der Funktion InitInstance der Applikationsklasse aufgerufen. Genau wie beim ersten Dialog wird auch bei diesem Dialog die Nachrichtenbehandlungsfunktion OnInitDialog eingefügt, um einige der Steuerelemente zu initialisieren (Listing 2.41). BOOL CControlDlg2::OnInitDialog() { CDialog::OnInitDialog(); // === Drehfeld === CSpinButtonCtrl *sb = (CSpinButtonCtrl*) GetDlgItem (IDC_SPIN1); sb->SetRange (spinMin, spinMax); // === Statusanzeige === CProgressCtrl *pc = (CProgressCtrl*) GetDlgItem (IDC_PROGRESS1); pc->SetRange (progressMin, progressMax); // === Regler === CSliderCtrl *sl = (CSliderCtrl*) GetDlgItem (IDC_SLIDER1); sl->SetRange (sliderMin, sliderMax, true); // === Animation === CAnimateCtrl *ani = (CAnimateCtrl*)GetDlgItem (IDC_ANIMATE1); ani->Open (IDR_AVI1); ani->Play (0,-1,-1);
Das Drehfeld-Steuerelement wird durch die Klasse CSpinButtonCtrl repräsentiert. Es ist einfach zu handhaben. In OnInitDialog wird lediglich der Wertebereich durch Aufruf der Funktion SetRange festgelegt. Alles andere lässt sich im Ressourceneditor einstellen. Das Eingabefeld, mit dem das Drehfeld zusammenarbeitet (Buddy), muss in der Tabulatorreihenfolge genau vor dem Drehfeld liegen. Das Eingabefeld muss in der Ressource als numerisches Feld
CSpinButtonCtrl
150
2
Einstieg in die MFC-Programmierung
gekennzeichnet sein. Das Drehfeld muss die Eigenschaften AUTOBUDDY und SETBUDDYINTEGER auf true gesetzt haben. Sind alle diese Voraussetzungen erfüllt, dann funktioniert das Drehfeld ohne weitere Programmierung. Will man hingegen nicht ganzzahlige oder nicht numerische Werte mit dem Drehfeld verarbeiten, so muss man eine Nachrichtenbehandlungsfunktion für die vom Drehfeld ausgesandten WM_VSCROLL- oder WM_HSCROLLNachrichten implementieren. Da ein Drehfeld selbst keine Daten enthält, kann kein Datenaustausch durchgeführt werden. Dieser wird über den Buddy, also das Eingabefeld, abgewickelt. CProgressCtrl
CProgressCtrl ist die MFC-Klasse für Statusanzeigen. Genau wie beim Drehfeld muss auch bei der Statusanzeige der Wertebereich mit der Funktion SetRange vorgegeben werden. Bei der Statusanzeige erfolgt der Datenaustausch nur in einer Richtung, um einen Statuswert anzuzeigen. Dazu wird in DoDataExchange die Funktion CProgressCtrl::SetPos aufgerufen, um die Anzeige auf den Wert der Variablen m_nProgress zu setzen. Statt – wie im Beispiel gezeigt – die Position der Statusanzeige als absoluten Wert zu setzen, lässt sie sich auch relativ zur vorherigen Position setzen (OffsetPos) oder in Schritten hochzählen (StepIt).
CSliderCtrl
Der Regler wird durch die MFC-Klasse CSliderCtrl gekapselt. Auch für ihn ist ein Wertebereich mit der Funktion SetRange vorzugeben. Beim Regler ist ein Datenaustausch in beiden Richtungen möglich. Die Reglerposition wird durch eine DDX-Variable gesetzt und gelesen. CSliderCtrl bietet die Möglichkeit, Positionsteilstriche zu zeichnen, die allerdings im Beispielprogramm nicht genutzt wird.
CAnimateCtrl
Das Animationssteuerelement CAnimateCtrl kann Videos im AVIFormat abspielen. Das AVI-Video kann aus einer Datei oder aus der Ressource des Programms geladen werden. Im Beispielprogramm WinControl wird das Video durch den Aufruf von Open mit einer Ressourcen-ID aus der Ressource geladen. Der Aufruf von Play mit den im Listing 2.41 verwendeten Parametern spielt das Video in einer Endlosschleife ab. Der Ressourceneditor der Entwicklungsumgebung von Visual C++ kennt keine Ressourcen vom Typ AVI. Trotzdem lassen sie sich einfach importieren. Der Ressourceneditor fragt lediglich nach einer Typbezeichnung für diese Ressource. Sinnvollerweise gibt man hier »AVI« an. Die Datei wird dann im Ressourceneditor in hexadezimaler Darstellung angezeigt.
Mehr zu Steuerelementen
Das Rich-Edit-Eingabefeld, CRichEditCtrl, ist ein kleiner leistungsfähiger Editor, dem sämtliche Bedienelemente fehlen. Die Initialisierung des Eingabefelds in OnInitDialog zeigt die grundsätzliche Arbeitsweise mit dem Steuerelement: Zur Formatierung des Texts wird immer mit einem selektierten Bereich gearbeitet. Der erste Schritt besteht also zunächst darin, mit der Funktion SetSel einen Bereich innerhalb des Texts auszuwählen. Durch den Aufruf der Funktion SetSelectionCharFormat wird der markierte Textbereich anschließend formatiert. An SetSelectionCharFormat wird als Parameter eine Referenz auf eine Struktur des Typs CHARFORMAT übergeben. Damit lassen sich unter anderem folgende Eigenschaften des Texts einstellen: Farbe, Schriftart, Höhe und Stilmerkmale (fett, unterstrichen, durchgestrichen, kursiv). Man muss in der Struktur über das Member dwMask angeben, welche Eigenschaft des Texts man setzen möchte, wie beispielsweise Farbe und Schriftart. Das Programm WinControl zeigt in OnInitDialog beispielhaft drei Formatierungsaufrufe: Das erste Wort wird fett, das zweite in der Schriftart Arial und größer, das dritte unverändert (also ohne die vorgegebene Formatierung zu verändern) und das vierte wird rot und unterstrichen dargestellt.
151 CRichEditCtrl
Am Rich-Edit-Steuerelement wird deutlich, dass das Konzept der Datenaustauschvariablen in seiner Wirkungsbreite beschränkt ist. Die Funktionalität komplexer Steuerelemente lässt sich damit nicht ausnutzen. In Listing 2.42 wird die DDX-Variable m_strRichEdit verwendet. Über diese Variable lässt sich der im Steuerelement gezeigte Text setzen und auslesen. Dabei gehen alle Formatierungsinformationen verloren. Die Leistungsfähigkeit des Rich-Edit-Steuerelements wird so nicht ausgenutzt. Um die erweiterte Funktionalität von komplexen Steuerelementen zu verwenden, spricht man das Steuerelement direkt über eine Instanz seiner Steuerelementklasse an und verwendet die zur Verfügung stehenden Funktionen dieser Klasse. Für das Rich-Edit-Steuerelement ist dies anhand der Initialisierung in der Funktion OnInitDialog im Beispielprogramm zu sehen. Dort werden die Formatierungen am Text des Rich-Edit-Steuerelements vorgenommen. Wollte man die Formatierungen des Steuerelements aus dem Dialog zurück in das Programm übernehmen, so müsste man dafür ein eigenes Schema zur Datenübergabe entwerfen und implementieren. Will man Listenelemente und Strukturansichten mit Icons versehen, so müssen diese in einem Objekt der Klasse CImageList gespeichert sein. Das Beispielprogramm WinControl verwendet
CImageList
152
2
Einstieg in die MFC-Programmierung
zu diesem Zweck die Variable m_ImageList in der Klasse CControlDlg2. Die Liste wird in OnInitDialog durch den Aufruf von Create erzeugt und es werden vier kleine Icons mit der Funktion Add hinzugefügt. CListCtrl
Anschließend wird die Bilderliste dem Listenelement, CListCtrl, durch den Aufruf von SetImageList zugewiesen. Der Parameter LVSIL_SMALL gibt an, dass es sich um Icons im Format von 16 x 16 Bildschirmpunkten handelt. Durch den wiederholten Aufruf der Funktion InsertItem werden vier Einträge in das Listenelement vorgenommen. Der erste Parameter von InsertItem gibt die Nummer des Eintrags an, der zweite gibt den Titel an und der dritte Parameter ordnet dem Eintrag ein Icon der Bilderliste zu. Genau wie das Rich-Edit-Steuerelement ist das Listenelement ein Steuerelement mit sehr vielen Möglichkeiten. Im Beispielprogramm soll als einzige Funktion der Text des gewählten Eintrags über die Datenaustauschvariable m_strList zurückgegeben werden. Da bei Listenelementen grundsätzlich eine Mehrfachselektion von Einträgen möglich ist, besitzt die Klasse CListCtrl keine einfache Möglichkeit, den Index eines selektierten Eintrags zu bestimmen. Im Beispielprogramm wurde das Listenelement durch Auswahl der Einstellung EINZELAUSWAHL im Ressourceneditor auf einen aktiven Eintrag beschränkt. Damit man nicht über die Liste aller Einträge iterieren muss, um festzustellen, welcher selektiert ist, wurde beim Listenelement eine andere Vorgehensweise implementiert. Jedes Mal, wenn ein Eintrag des Listenelements selektiert wird, schickt es die Nachricht LVN_ITEMCHANGED. Hierfür wurde mit dem Assistenten die Nachrichtenbehandlungsfunktion OnItemchangedList1 angelegt (Listing 2.43). void CControlDlg2::OnItemchangedList1(NMHDR* pNMHDR, LRESULT* pResult) { NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR; int nSelected = pNMListView->iItem; CListCtrl *lc = (CListCtrl*) GetDlgItem (IDC_LIST1); if (nSelected >= 0) m_strList = lc->GetItemText (nSelected, 0); *pResult = 0; } Listing 2.43: Nachrichtenbehandlungsfunktion für LVN_ITEMCHANGED
Mehr zu Steuerelementen
153
Die Funktion OnItemChangedList1 ermittelt den Text des jeweils selektierten Eintrags mit der Funktion GetItemText und weist ihn der Variablen m_strList zu. Für den Fall, dass OnItemChangedList1 nicht aufgerufen wird, weil der Benutzer das Listenelement gar nicht auswählt, wird m_strList in DoDataExchange mit einem leeren String initialisiert. Damit ist sichergestellt, dass m_strList einen definierten Zustand hat, auch wenn kein Eintrag des Listenelements angeklickt worden ist. Als letztes Steuerelement des Dialogs bleibt jetzt noch die Strukturansicht, CTreeCtrl. Die Strukturansicht verwendet im Beispielprogramm die gleiche Bilderliste wie das Listenelement. Die Bilderliste wird durch Aufruf der Funktion SetImageList zugewiesen. Genau wie beim Listenelement werden Einträge in die Strukturansicht durch Aufruf der Funktion InsertItem vorgenommen. Anders als beim Listenelement muss bei der Strukturansicht zusätzlich angegeben werden, wo der neue Eintrag in den Baum eingefügt werden soll. Es müssen das übergeordnete Element sowie die Position innerhalb der Elemente auf der gleichen Hierarchieebene angebeben werden. Im Beispielprogramm werden zur Identifizierung von Einträgen Handles des Typs HTREEITEM verwendet. Alternativ kann auch mit Strukturen des Typs TV_INSERTSTRUCT gearbeitet werden. Allerdings enthalten diese Strukturen auch nur wieder Handles des Typs HTREEITEM. Sowohl für das übergeordnete Element als auch für die Einfügeposition gibt es Vorgabewerte. Wird kein übergeordneter Eintrag angegeben, so wird TVI_ROOT angenommen, was bedeutet, dass der Eintrag auf der obersten Ebene (Root-Level) der Strukturansicht vorgenommen wird. Wenn die Einfügeposition nicht angegeben wird, dann gilt der Vorgabewert TVI_LAST. Er bedeutet, dass das einzufügende Element an der letzten Stelle der Liste aufgenommen wird. In einer Strukturansicht kann immer nur ein Eintrag selektiert sein. Dieser lässt sich über die Funktion GetSelectedItem ermitteln. In DoDataExchange wird diese Funktion aufgerufen, um den Text des selektierten Eintrags in die Variable m_strTree zu übernehmen. Sollte kein Eintrag selektiert sein, so wird m_strTree auf einen leeren String gesetzt. Der Dialog Steuerelemente Teil 3 verwendet ausschließlich Steuerelemente, die erst mit Version 6.0 in die MFC integriert worden sind. Listing 2.44 zeigt den Aufruf des Dialogs.
können Austauschvariablen gelesen werden dlg.m_DateTimePicker; dlg.m_MonthCal; dlg.m_nComboEx; dlg.m_nIPf1; dlg.m_nIPf2; dlg.m_nIPf3; dlg.m_nIPf4;
} Listing 2.44: Aufruf des dritten Beispieldialogs in OnControlsPart3
Die beiden Steuerelemente zur Ein- und Ausgabe von Datumsund Zeitinformationen können per DDX wahlweise Werte des Typs CTime oder COleDateTime austauschen. Das erweiterte Kombinationsfeld verhält sich beim Datenaustausch wie das einfache Kombinationsfeld. Für das IP Steuerelement gibt es keine Unterstützung durch DDX. Im Beispielprogramm werden daher vier Austauschvariablen des Typs BYTE verwendet, eine für jedes Segment der Internetadresse. Listing 2.45 zeigt die Implementierungsdatei der Klasse CControlDlg3. // ControlDlg3.cpp: Implementierungsdatei // #include "stdafx.h" #include "WinControl.h" #include "ControlDlg3.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE
Mehr zu Steuerelementen static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// / // Dialogfeld CControlDlg3
// dritten Eintrag selektieren cb->SetCurSel (2); return TRUE;
// // // //
return TRUE unless you set the focus to a control EXCEPTION: OCX-Eigenschaftenseiten sollten FALSE zurückgeben
} Listing 2.45: Die Implementierungsdatei der Klasse CControlDlg3
In der Funktion DoDataExchange der Klasse CControlDlg wird der Datenaustausch mit dem IP Steuerelement programmiert. Die Werte der vier Segmente werden durch jeweils einen Aufruf der Funktionen GetAddress und SetAddress gelesen und gesetzt. In der Funktion OnInitDialog ist verhältnismäßig viel Programmcode notwendig, um das erweiterte Kombinationsfeld zu initialisieren. Wie die Steuerelemente Listenelement und Strukturansicht verwendet das erweiterte Kombinationsfeld eine Bilderliste der Klasse CImageList, um die Symbole für das Steuerelement bereitzustellen. Diese Bilderliste wird in OnInitDialog erzeugt und mit der Funktion SetImageList der Instanz des erweiterten Kombinationfelds zugewiesen. Für jeden Eintrag des Kombinationsfelds muss dann eine Struktur des Typs COMBOBOXEXITEM ausgefüllt werden. Mit dieser Struktur gibt man die Nummer des Eintrags (iItem), den Text (pszText), die maximale Länge des Texts (cchTextMax), das Symbol des selektierten Eintrags (iSelectedImage) und des nicht selektierten Eintrags (iImage) aus der Symbolliste an. Die Struktur COMBOBOXEXITEM besitzt noch weitere Einträge, die gesetzt werden können. Welche Einträge gesetzt werden sollen, wird über die Maske (mask) spezifiziert. Der Aufruf der Funktion InsertItem übernimmt einen Eintrag in das erweiterte Kombinationsfeld. Die Struktur des Typs wird dabei als Parameter übergeben.
2.8.2
Eigenschaftsdialogfelder
Eigenschaftsdialogfelder bieten eine bequeme Möglichkeit, eine größere Anzahl von Steuerelementen zu gruppieren und auf kleiner Fläche unterzubringen. Sie nutzen dazu die von vielen Windows-Programmen bekannten Registerkarten. Das Programm WinControl implementiert einen einfachen Eigenschaftsdialog, der im Menü STEUERELEMENTE | EIGENSCHAFTSDIALOG aufgerufen werden kann. Abbildung 2.57 zeigt den Dialog.
Eigenschaftsdialoge bestehen aus zwei Teilen: den Seiten, die durch jeweils eine Registerkarte repräsentiert werden, und dem Blatt (Sheet), auf dem sich die Seiten befinden. Die Seiten werden ganz normal mit dem Ressourceneditor erstellt. Als Titel des Dialogs gibt man an, was man später auf der Registerkarte sehen möchte. Die einzelnen Dialogressourcen für die Seiten sollten möglichst gleich groß sein. Für jede Seite wird dann mit dem Klassen-Assistenten eine Dialogklasse angelegt. Dabei ist darauf zu achten, dass die Klasse von CPropertyPage und nicht von CDialog abgeleitet wird. Auch für das Blatt, auf dem sich die Seiten befinden, wird eine Klasse mit dem Klassen-Assistenten angelegt. Diese wird von CPropertySheet abgeleitet. Für diese Klasse wird keine Dialogvorlage benötigt. Für jede Seite muss in die von CPropertySheet abgeleitete Klasse eine Member-Variable der Klasse aufgenommen werden, die die Seite repräsentiert. Die Seiten werden dann im Konstruktor dem Blatt hinzugefügt. Das Programm WinControl implementiert einen dreiseitigen Eigenschaftsdialog. Die drei Seiten werden durch die drei Klassen CIconPage, CSliderPage und CBitmapPage repräsentiert. Die Klasse für das Dialogblatt heißt CPropertyDialog. Die drei Instanzvariablen m_BitmapPage, m_SliderPage und m_IconPage werden als private in CPropertyDialog deklariert (Listing 2.46). class CPropertyDialog : public CPropertySheet { ... private:
Im Konstruktor von CPropertyDialog werden die Seiten zu dem Blatt hinzugefügt. Es stehen zwei Konstruktoren zur Verfügung. Sie unterscheiden sich voneinander nur dadurch, wie die Überschrift des Eigenschaftsdialogs angegeben wird: durch einen String oder eine Ressourcen-ID. Listing 2.47 zeigt einen der beiden Konstruktoren. CPropertyDialog::CPropertyDialog(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage) :CPropertySheet(pszCaption, pParentWnd, iSelectPage) { m_psh.dwFlags |= PSH_NOAPPLYNOW; AddPage (&m_IconPage); AddPage (&m_SliderPage); AddPage (&m_BitmapPage); } Listing 2.47: Einer der Konstruktoren des Eigenschaftsdialogs
Die Konstruktoren verändern zudem die Einstellungen der Member-Variablen m_psh. Mit m_psh lässt sich der Eigenschaftsdialog in seinem Erscheinungsbild verändern. m_psh ist eine Struktur des Typs PROPSHEETHEADER. Eigenschaftsdialogfelder blenden die Schaltflächen unterhalb der Registerkarten selbst ein, die Schaltflächen werden nicht durch eine Ressource vorgegeben. Vorgegeben sind drei Schaltflächen: OK, ABBRECHEN und ÜBERNEHMEN. Das im Beispielprogramm verwendete Flag PSH_NOAPPLYNOW blendet die Schaltfläche ÜBERNEHMEN aus. Es besteht zudem die Möglichkeit, eine Schaltfläche HILFE einzublenden sowie Symbole in der Titelleiste des Eigenschaftsdialogs anzuzeigen. Eine besonders interessante Möglichkeit ist die Verwendung des Flags PSH_WIZARD. Dieses bewirkt, dass der Eigenschaftsdialog ohne die Registerkarten, dafür aber mit den Schaltflächen ZURÜCK und WEITER angezeigt wird. Der Eigenschaftsdialog sieht damit so aus wie die von vielen Installationsprogrammen verwendeten Dialoge.
PROPSHEETHEADER
160
2
Einstieg in die MFC-Programmierung
Abbildung 2.58: Eigenschaftsdialog im Assistenten-Stil
Aufgerufen wird der Eigenschaftsdialog, wie bei modalen Dialogen üblich, durch die Funktion DoModal. Da der Eigenschaftsdialog selbst jedoch keine Dialogvorlage hat, muss im Konstruktor ein Titel für den Dialog angegeben werden. Listing 2.48 zeigt den Aufruf aus der Ansichtsklasse des Programms WinControl. void CWinControlView::OnControlsProperty() { CPropertyDialog dlg("Eigenschaftsdialog"); dlg.DoModal (); } Listing 2.48: Aufruf des Eigenschaftsdialogs DDX in Eigenschaftsdialogen
Genau wie in einfachen Dialogen so kann auch in Eigenschaftsdialogen der Dialogdatenaustausch DDX verwendet werden. Dabei sind allerdings ein paar Punkte zu beachten. Die Datenübernahme erfolgt bei einem Eigenschaftsdialog immer für alle Seiten gleichzeitig. Sollte der Dialog die Schaltfläche ÜBERNEHMEN verwenden, dann ist dazu die Funktion OnApply zu implementieren. OnApply wird in einer der für die Seiten zuständigen Dialogklassen implementiert. Da der Datenaustausch immer für alle Seiten gleichzeitig erfolgt, muss OnApply nur in einer der Klassen implementiert werden. Um die Schaltfläche ÜBERNEHMEN zu aktivieren, muss eines der für die Seiten zuständigen Objekte die Funktion SetModified(TRUE) aufrufen. Wenn der Benutzer auf ÜBERNEHMEN klickt, wird OnApply aufgerufen und die Schaltfläche bis zum nächsten Aufruf von SetModified(TRUE) deaktiviert.
HTML-basierte Dialogfelder
Es ist wichtig zu wissen, dass auch ein Wechsel zwischen verschiedenen Registerseiten einen Datenaustausch veranlasst. Die Datenübernahme lässt sich danach auch durch das Verlassen des Dialogs mit der Schaltfläche ABBRECHEN nicht mehr rückgängig machen. Eigenschaftsdialoge sind eine einfache Technik, um Dialoge mit Registerkarten zu verwenden. Man muss wenig zusätzlichen Programmcode schreiben, um Eigenschaftsdialoge zu verwenden. Ein Nachteil ist das etwas starre Erscheinungsbild. Mit diesem einfachen Prinzip können keine anderen Steuerelemente neben dem Registerblatt verwendet werden. Wem Eigenschaftsdialoge zu starr sind, der kann das Registerkarten-Steuerelement, CTabCtrl, verwenden. Es wird mit dem Ressourceneditor in eine normale Dialogvorlage eingefügt. Es ist flexibler in der Anwendung, dafür aber aufwändiger zu programmieren.
2.8.3
Zusammenfassung
Steuerelemente sind ein Hauptmittel zur Kommunikation mit dem Benutzer. Windows bietet eine große Anzahl von vordefinierten Steuerelementen. Der größte Teil von ihnen wurde in diesem Kapitel vorgestellt. Viele Möglichkeiten dieser Steuerelemente konnten im Rahmen dieser Einführung jedoch nicht erläutert werden. Darüber hinaus erlaubt Windows, eigene Steuerelemente zu definieren oder die vorhandenen zur erweitern. Auf diese Möglichkeiten konnte hier nicht eingegangen werden. Daneben können Steuerelemente mit Hilfe der ActiveX-Technologie implementiert werden. Dieses Verfahren wird später in Kapitel 3, »OLE, COM und ActiveX«, behandelt werden.
2.9
HTML-basierte Dialogfelder
Nachdem es der Windows-Programmierer seit den ersten Windows-Versionen gewohnt ist, Dialogfelder mittels Ressourcenvorlagen zu definieren und auf den Dialogfeldern Steuerelemente zu platzieren, leitet Microsoft mit Visual Studio .NET hier einen Paradigmenwechsel ein. Dialogfelder lassen sich durch HTML-Vorlagen definieren, die HTML-Elemente selbst können dabei als »Steuerelemente« agieren.
161
t CTabCtrl
162
2 CDHtmlDialog
Einstieg in die MFC-Programmierung
HTML-basierte Dialogfelder werden von der Klasse CDHtmlDialog abgeleitet, die selbst von CDialog abgeleitet ist.
CObject CCmdTarget CWnd CDialog CDHtmlDialog CMultiPageDHtmlDialog Abbildung 2.59: Die DHTML-Dialogklassen der MFC
Dialogfeldbasierte Programme lassen sich durch den Anwendungs-Assistenten mit einer HTML-basierten Dialogfeldklasse ausstatten, wenn man unter »Anwendungstyp« die Option HTMLDIALOGFELD VERWENDEN auswählt. Der Anwendungs-Assistent erstellt dann eine von CDHtmlDialog abgeleitete Dialogklasse und fügt eine HTML-Datei als Dialogvorlage in das Projekt ein. Die HTML-Vorlage kann anschließend mit dem HTML-Editor von Visual Studio .NET bearbeitet werden. Zur Anzeige der HTML-Dialogvorlage wird diese einer »althergebrachten« Dialogvorlage überlagert, die auf einem normalen Dialogfenster positioniert wird. Diese Dialogvorlage sollte normalerweise keine Steuerelemente enthalten, da diese in das HTML-Dialogfeld »durchscheinen«. Im Verzeichnis KAPITEL2\PICTUREVIEWER der Begleit-CD befindet sich das Beispielprogramm PictureViewer, das die Programmierung eines HTML-Dialogfelds demonstriert. Das Beispielprogramm lädt JPEG-Bilder mittels eines Dateiauswahldialogs und zeigt die Bilder an. Abbildung 2.60 zeigt das Programm PictureViewer. Listing 2.49 zeigt den HTML-Quelltext des Dialogfelds des Beispielprogramms. Die Elemente des Programms wurden mittels einer Tabelle angeordnet. Alle Elemente, die vom Programmcode aus angesprochen werden sollen, sind mit einer ID versehen worden. Dies sind die beiden Schaltflächen, das Image-Tag, sowie der Bereich, der den Bildpfad anzeigen soll. Dieser wurde mittels eines Span-Tags realisiert.
HTML-basierte Dialogfelder
Abbildung 2.60: Das Beispielprogramm PictureViewer <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
163
164
2
Einstieg in die MFC-Programmierung
<span id=pathText> Hier steht der Pfad
Listing 2.49: HTML-Dialogvorlage des Beispielprogramms PictureViewer DDX
HTML-Dialogfelder sind derart in die MFC integriert worden, dass ihre Verwendung möglichst analog zu den ressourcenbasierten Dialogfeldern ist. So gibt es DDX-Funktionen zum Austausch von Daten mit HTML-Elementen und analog zu den Nachrichtenzuordnungtabellen gibt es DHTML-Ereigniszuordnungstabellen, die Ereignisse aus HTML-Vorlagen an das Programm weiterreichen. Listing 2.50 zeigt die zur Programmierung von HTMLbasierten Dialogfeldern relevanten Ausschnitte aus dem Programm PictureViewer. // CPictureViewerDlg Dialogfeld BEGIN_DHTML_EVENT_MAP(CPictureViewerDlg) DHTML_EVENT_ONCLICK(_T("ButtonImage"), OnButtonImage) DHTML_EVENT_ONCLICK(_T("ButtonCancel"), OnButtonCancel) END_DHTML_EVENT_MAP()
HRESULT CPictureViewerDlg::OnButtonImage( IHTMLElement* /*pElement*/) { // Dateidialog zum Importieren: CFileDialog fileDialog( true, NULL, NULL, NULL, "JPG (*.jpg)|*.jpg|Alle Dateien (*.*)|*.*||"); // wenn der Benutzer OK geklickt hat: if (IDOK == fileDialog.DoModal ()) { m_pathString = fileDialog.GetPathName (); UpdateData (false); } return S_OK; } HRESULT CPictureViewerDlg::OnButtonCancel(IHTMLElement* / *pElement*/) { OnCancel(); return S_OK; } Listing 2.50: Ausschnitte aus dem Programm PictureViewer
Das Listing zeigt, dass die Zuordnung zwischen den Schaltflächen und den dazugehörigen Nachrichtenbehandlungsfunktionen in der DHTML-Ereigniszuordnungstabelle vorgenommen wird. Beim Klick auf die Schaltfläche »Bild« mit der ID ButtonImage wird entsprechend die zugeordnete Funktion OnButtonImage aufgerufen. Der Funktion OnButtonImage wird als Parameter ein COM-Zeiger vom Typ IHTMLElement übergeben. Über diesen Zeiger lässt sich auf den HTML-Code des Elements zugreifen. Da es sich hier um einen COM-Zeiger handelt, sollte man an dieser Stelle mit der Programmierung des Component Object Model vertraut sein. Dies wird ausführlich in Kapitel 3, »COM, OLE und ActiveX«, dieses Buches beschrieben. In der Funktion OnButtonImage wird zunächst ein Dateidialogfeld geöffnet. Hier kann der Benutzer eine Bilddatei
165
166
2
Einstieg in die MFC-Programmierung
auswählen. Anschließend wird der Pfad der ausgewählten Datei in die Variable m_pathString übernommen. Dieser Dateipfad wird dann durch den Aufruf von UpdateData in die HTML-Elemente des Dialogfelds geschrieben. Der Datenaustausch per DDX findet, wie bei anderen Dialogfeldern auch, in der Funktion DoDataExchange statt. Die Implementierung von DoDataExchange im Programm PictureViewer schreibt den vom Benutzer ausgewählten Dateipfad gleich an zwei Stellen des HTMLDialogfelds. Die DDX-Funktion DDX_DHtml_ElementInnerText schreibt den ihr übergebenen String zwischen öffnendes und schließendes Tag mit der übergebenen ID. Im Beispiel ist dies das Span-Tag mit der ID pathText. Weiterhin schreibt DoDataExchange den gleichen Pfad in das src-Attribut des verwendeten Image-Tag. Hierzu wird die DDX-Funktion DDX_DHtml_Img_Src verwendet. Mit weiteren DDX-Funktionen lassen sich Links verändern, Frames setzen oder Form-Elemente mit Werten bestücken.
2.10 Fehlersuche mit den MFC Wie in der Einleitung bereits beschrieben, soll in diesem Buch nicht der Umgang mit der Entwicklungsumgebung geübt, sondern die MFC- und Windows-Programmierung besprochen werden. Daher wird in diesem Kapitel auch nicht der in die Entwicklungsumgebung integrierte Debugger behandelt. Vielmehr bieten die MFC eine Reihe programmtechnischer Konstrukte, die die Fehlersuche in Programmen vereinfachen können.
2.10.1 Makros zur Fehlersuche TRACE Zu der Zeit, als es noch keine grafischen Benutzeroberflächen gab und Compiler oftmals ohne dazugehörigen Debugger ausgeliefert wurden, haben sich C-Programmierer zur Fehlersuche damit beholfen, dass sie innerhalb ihres Quelltexts printf-Anweisungen eingefügt haben, um den Kontrollfluss zu verfolgen oder Variablenwerte auszugeben. Unter Windows hat jedoch printf außerhalb von Konsolenprogrammen (das sind WIN32-Programme ohne grafische Oberfläche, die in einer DOS-Box laufen) keine Bedeutung mehr, da für Windows-Programme keine Standardausgabe (stdout) definiert ist. Um jedoch genau diese Art von Ausgaben zur Fehlersuche zu ermöglichen, gibt es das TRACE-Makro.
Fehlersuche mit den MFC
167
CString nullNullSieben (_T("Bond")); TRACE (_T("\nMein Name ist %s, James %s\n\n"), (LPCTSTR)nullNullSieben, (LPCTSTR)nullNullSieben); Listing 2.51: Verwendung des TRACE-Makros
TRACE erhält als ersten Parameter einen printf-kompatiblen String und als weitere Parameter die auszugebenden Variablen. Die Ausgaben von TRACE erfolgen in das Debug-Fenster der Entwicklungsumgebung. TRACE-Makros nehmen ihre Ausgaben nur in der Debug-Version des Programms vor. Die Verwendung des Makros ist in Listing 2.51 gezeigt. In diesem Beispiel kommt übrigens der LPCTSTR-Operator von CString zum Einsatz. Dieser Operator lässt ein CString-Objekt wie einen ganz normalen C-String aussehen, indem ein Zeiger auf den internen Puffer des CString-Objekts zurückgegeben wird. Der erhaltene Zeiger darf nur zum Lesen verwendet werden. In Listing 2.51 wird explizit eine Typumwandlung in LPCTSTR durchgeführt. Nur dadurch wird der LPCTSTR-Operator von CString überhaupt aufgerufen. Das TRACE-Makro verwendet nämlich – genau wie printf – eine variable Parameterliste, auch Ellipse genannt. Bei einer Ellipse können prinzipiell keine Typinformationen angegeben werden, daher kann auch der LPCTSTR-Operator nicht in Aktion treten. Lässt man die explizite Typumwandlung weg, wird der LPCTSTR-Operator nicht ausgeführt. Das TRACE-Makro funktioniert – unerwarteterweise – trotzdem! Es ist zu vermuten, dass Microsoft an dieser Stelle speziellen Programmcode in den Laufzeitbibliotheken oder im Compiler verwendet. Bei anderen Compilern oder Portierungen der MFC auf andere Plattformen ist an dieser Stelle mit Problemen zu rechnen. Ellipsen sind ein Teil von C++, der aus der C-Vergangenheit der Sprache stammt und nicht mehr zeitgemäß ist. Da Ellipsen manchmal ungemein praktisch sind, werden sie trotzdem noch verwendet.
Abbildung 2.61: Ausgabe des Beispiels aus Listing 2.51
t
168
2
Einstieg in die MFC-Programmierung
ASSERT Das ASSERT-Makro implementiert ein einfaches Konzept, das aber sehr mächtig sein kann, wenn es durchgehend genutzt wird. Ein ASSERT-Makro überprüft die Zusicherung, dass der ihm übergebene Ausdruck wahr ist. Typischerweise sollten solche Zusicherungen beim Eintritt in eine Funktion und vor dem Verlassen der Funktion gemacht werden. Man spricht dann von Vor- und Nachbedingungen. Die Funktion wird als Black Box betrachtet: Eine Menge von Eingangsvariablen wird der Funktion übergeben und sie liefert dafür eine Anzahl von Ergebnissen in entsprechenden Ausgangsvariablen. (Im mathematischen Sinne hat eine Funktion immer genau einen Ergebniswert, hier werden allerdings C++Funktionen betrachtet, die durchaus mehrere Ausgangswerte haben können.) Sowohl für alle Eingangsvariablen als auch für alle Ausgangsvariablen lassen sich Wertebereiche für gültige Werte angeben. Der Eingangswertebereich muss eingehalten werden, damit die Funktion überhaupt definiert ist. Ist die Funktion korrekt implementiert, so werden die Ausgangswertebereiche natürlich automatisch eingehalten. Eine Überschreitung der Ausgangswertebereiche bei korrekten Eingangswerten bedeutet daher, dass die Funktion fehlerhaft implementiert ist. Das ASSERT-Makro kann dazu verwendet werden, die Gültigkeit der Eingangs- und Ausgangswertebereiche zu überprüfen. double CCalcClass::Sine (double angle) { double result; // Vorbedingung ASSERT(angle >= 0); ASSERT(angle <= 360); // Hier wird jetzt der Sinus errechnet. result = ... // Nachbedingung ASSERT(result <= 1); ASSERT(result >= -1); return result; } Listing 2.52: Zusicherung der Vor- und Nachbedingung einer Funktion
Fehlersuche mit den MFC
Die Funktion in Listing 2.52 soll den Sinus eines in Grad angegeben Winkels berechnen. Für das hypothetische Programm soll der Eingangswertebereich auf Winkel zwischen 0 und 360 Grad beschränkt sein (Vorbedingung). Ist der Algorithmus zur Berechnung des Sinus richtig implementiert, so kann der Ausgangswertebereich nur zwischen -1 und 1 liegen (Nachbedingung). ASSERT-Makros sind nur in der Debug-Version eines Programms wirksam. In der Release-Version werden sie aus Laufzeitgründen abgeschaltet. Tests eines MFC-Programms sollten daher immer zunächst mit der Debug-Version durchgeführt werden. Wurde eine Zusicherung verletzt, wird das Programm an der Stelle, an der die Verletzung auftrat, unterbrochen. Anschließend wird eine Meldung wie in Abbildung 2.62 angezeigt.
Abbildung 2.62: Meldung, dass eine Zusicherung verletzt wurde.
Die Wirksamkeit von Zusicherungen hängt sehr stark von ihrem Einsatz und damit von der Disziplin des Programmierers ab. Wer wenige Zusicherungen verwendet, wird keinen großen Nutzen von ihnen haben. Umgekehrt kann ihr ausgiebiger Einsatz dazu beitragen, die Korrektheit, Robustheit und Qualität eines Programms entscheidend zu verbessern. Zwar führt nicht jeder unentdeckte Fehler gleich zu einem Absturz des Programms; allerdings machen solche unentdeckten Fehler ein Programm bestimmt nicht sicherer. Zusicherungen helfen, Fehler zu finden. Die MFC selbst machen ausgiebigen Gebrauch von ihnen. Der Quelltext der MFC soll über 5000 ASSERT-Makros enthalten.
VERIFY Das Makro VERIFY funktioniert analog zum Makro ASSERT. Im Unterschied zu ASSERT wird ein übergebener Ausdruck in der
169
170
2
Einstieg in die MFC-Programmierung
Release-Version ausgewertet. Genau wie beim Makro ASSERT wird in der Release-Version eines Programms kein Fehlerdialog angezeigt. Der Unterschied beider Makros wird deutlich, wenn man als Argument eine Anweisung verwendet, die eine Zustandsänderung bewirkt, beispielsweise eine Zuweisung. Diese wird in der Release-Version vom VERIFY-Makro ausgeführt, vom ASSERT-Makro aber nicht. Das ASSERT-Makro übernimmt keine Ausdrücke in die Release-Version, weil Zusicherungen lediglich einen beobachtenden Charakter haben. Zusicherungen dienen der Konsistenzüberprüfung eines Programms und dürfen somit keine Zustandsänderungen hervorrufen. Ein Programm muss nach dem Entfernen aller Zusicherungen genauso ablaufen, wie vorher. Dies wäre jedoch nicht der Fall, wenn eine Zusicherung den Zustand des Programms verändern würde. Somit sollte das Makro VERIFY nicht für Zusicherungen verwendet werden. Es kann aber beispielsweise zur Überprüfung von Rückgabewerten bei Funktionsaufrufen verwendet werden, die nur in der Debug-Version geprüft werden sollen. Tabelle 2.6 stellt das ASSERT- und das VERIFYMakro gegenüber. Makro
Debug
Release
ASSERT(Ausdruck)
if (!Ausdruck) Fehler
–
VERIFY(Ausdruck)
if (!Ausdruck) Fehler
Ausdruck
Tabelle 2.6: Verhalten der Makros ASSERT und VERIFY
AssertValid und ASSERT_VALID Die virtuelle Funktion AssertValid wird in CObject deklariert. AssertValid überträgt die Idee des ASSERT-Makros auf den internen Zustand eines Objekts. Durch den Aufruf dieser Funktion möchte man testen, ob sich das zu überprüfende Objekt in einem gültigen oder konsistenten Zustand befindet. Was dabei genau mit gültig oder konsistent gemeint ist, lässt sich nicht allgemein formulieren. Beispielsweise prüft CObject::AssertValid, ob der Objektzeiger this ungleich NULL ist. Die meisten von CObject abgeleiteten Klassen der MFC implementieren AssertValid. Wer AssertValid in eigenen Klassen implementieren möchte, sollte zunächst die AssertValid-Funktion der Basisklasse aufrufen. Die Überprüfung sollte mit Hilfe von ASSERT-Makros durchgeführt werden. AssertValid ist als const definiert, so dass man keine Veränderungen am Zustand des Objekts vornehmen kann.
AssertValid wird nur in der Debug-Version eines Programms implementiert. Da die Konstante _DEBUG nur bei Übersetzungen im Debug-Modus definiert ist, sorgt der Präprozessor dafür, dass AssertValid nicht in die Release-Version aufgenommen wird. Obwohl man AssertValid direkt aufrufen kann, ist es empfehlenswert, stattdessen das Makro ASSERT_VALID zu verwenden. Dieses Makro führt nämlich einige zusätzliche Prüfungen durch: Es überprüft, ob der übergebene Zeiger ungleich NULL ist, ob die Objektadresse gültig ist und ob das Objekt eine gültige vTable hat. Die vTable ist eine Tabelle der virtuellen Funktionen eines Objekts. Sie wird benötigt, um zur Laufzeit die zum Objekt gehörende virtuelle Funktion zu finden und aufzurufen. Dieser Vorgang wird als Late Binding bezeichnet, da erst zur Laufzeit entschieden wird, welche Funktion aufgerufen wird.
ASSERT_VALID
Zustandsprotokollierung mit der Klasse CDumpContext Um den Zustand ganzer Objekte im Debug-Fenster der Entwicklungsumgebung auszugeben, verfügen die MFC über einen Dump-Mechanismus. Dieser Mechanismus besteht aus zwei Teilen: der virtuellen Funktion Dump, die in CObject deklariert wird, und aus einem Objekt der Klasse CDumpContext. Mit dem Operator << der Klasse CDumpContext können, genau wie mit dem Makro TRACE, Ausgaben in das Debug-Fenster der Entwicklungsumgebung gemacht werden. In als Debug-Version übersetzten MFC-Programmen gibt es die globale Variable afxDump. Diese Variable ist eine Instanz der Klasse CDumpContext. Mit ihrer Hilfe erfolgen die Ausgaben in das Debug-Fenster. Da das afxDumpObjekt in Release-Versionen nicht existiert, müssen alle Zugriffe darauf in Präprozessoranweisungen eingebettet werden:
afxDump
172
2
Einstieg in die MFC-Programmierung
#ifdef _DEBUG afxDump << "Hallo Welt!" #endif
Ein Vorteil von afxDump gegenüber TRACE ist, dass der Operator << nicht nur für einfache Datentypen überladen ist, sondern auch für Referenzen und Zeiger auf Instanzen der Klasse CObjekt. Damit lässt sich der Zustand ganzer Objekte im Debug-Fenster ausgeben. Ein weiterer Vorteil ist, dass die Verwendung dieses Objekts eher dem C++-Programmierstil entspricht, da es analog zum Objekt cout aus den Streambibliotheken von C++ zu verwenden ist. Die Implementierung des Operators << ruft die Funktion Dump der Klasse CObject auf, um den Zustand des Objekts an den DumpKontext zu übergeben. Interessanterweise ist Dump als virtuelle Funktion deklariert, so dass sie von abgeleiteten Klassen überschrieben werden kann. Viele Klassen der MFC überschreiben Dump, um ihren gegenüber CObject erweiterten Zustand auszugeben. Diese Technik soll an einer erweiterten Version des Programms StockChart demonstriert werden. Diese Version befindet sich im Verzeichnis KAPITEL2\ STOCKCHARTDEBUG auf der Begleit-CD. Sie besitzt den zusätzlichen Menüeintrag DEBUG | DUMP DES DOKUMENTS, mit dem – in der Debug-Version – das Dokument der gerade aktiven Ansicht über afxDump ausgegeben wird. Außerdem weist diese Version ein Speicherleck auf, das im Folgenden besprochen werden wird. void CStockChartView::OnDumpDocument() { CStockChartDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); #if _DEBUG afxDump << pDoc; #else MessageBox ("Dump ist nur in der Debug-Version möglich."); #endif } Listing 2.54: Dump von pDoc
Der Programmcode zur Ausgabe des aktuellen Dokuments befindet sich in der Funktion CStockChartView::OnDumpDocument. In der Release-Version des Programms wird eine Fehlermeldung ausgegeben (Listing 2.54).
Fehlersuche mit den MFC
Implementiert wird Dump in der Dokumentenklasse CStockChartDoc (Listing 2.55). void CStockChartDoc::Dump(CDumpContext& dc) const { CDocument::Dump(dc); dc dc dc dc dc if
Ein Beispiel für eine Ausgabe von Dump ist in Abbildung 2.63 zu sehen.
Abbildung 2.63: Ausgabe von CStockChartDoc::Dump
Der Wert, den die Ausgaben von Dump für den Fehlersuchenden haben, hängt stark von ihrem Informationsgehalt ab. Wie schon bei den Zusicherungen hängt es auch bei der Verwendung von Dumps von der Disziplin des Programmierers ab, ob diese Technik ein wirksames Mittel bei der Suche nach Fehlern ist.
Speicherlecks C++ ist genau wie C eine Programmiersprache, bei der der Programmierer die Pflicht hat, alle von ihm angeforderten Objekte nach der Benutzung wieder freizugeben. Angefordert werden
173
174
2
Einstieg in die MFC-Programmierung
Objekte üblicherweise durch den Operator new, freigegeben durch den Operator delete. Vergisst der Programmierer die Freigabe von Objekten, so entstehen so genannte Speicherlecks: Das Programm hält Speicher belegt, den es nicht mehr verwendet, aber trotzdem nicht freigibt. Kommen innerhalb des Programms häufig Speicheranforderungen ohne zugehörige Freigaben vor, so ist es nur eine Frage der Zeit, bis dem gesamten System der Speicher ausgeht. Garbage Collector
Speicherlecks sind oft schwer zu finden und auch in kommerziellen Programmen keine Seltenheit. Andere Programmiersprachen, wie beispielsweise Java oder Smalltalk, lösen das Problem sehr elegant, indem sie den Programmierer von der Last der Speicherfreigabe befreien. Ein so genannter Garbage Collector überwacht alle vom Programmierer erzeugten Objekte. Geht die Anzahl der Referenzen auf ein Objekt auf Null zurück, so wird dieses aus dem Speicher entfernt. Von C++ wird dieser Ansatz nicht übernommen, da er sich negativ auf das Laufzeitverhalten auswirkt. Die Überwachung und Verwaltung aller Objektreferenzen kostet schließlich Zeit. Auch weiß man oft nicht genau, wann der Garbage Collector seine Aufräumarbeiten durchführt. In zeitkritischen Anwendungen kann es daher zu Problemen kommen, wenn das Programm durch größere Speicherfreigaben unterbrochen wird.
Speicherüberwachung
Die MFC implementieren einen dem Garbage Collector ähnelnden Speicherüberwachungsmechanismus. Alle Speicheranforderungen- und freigaben im Programm werden protokolliert. Am Programmende werden dann alle Objekte, die nicht freigegeben worden sind, im Debug-Fenster aufgelistet. Bequemerweise werden auch gleich die Programmzeilen angezeigt, in denen die nicht freigegebenen Objekte angefordert worden sind. Aus den bereits erwähnten Laufzeitgründen wird dieser Speicherüberwachungsmechanismus nur in der Debug-Version aktiviert. Um diesen Mechanismus zu demonstrieren, ist im Programm StockChart künstlich ein Speicherleck eingefügt worden. In der Programmversion StockChartDebug wurde die Funktion CStockChartDoc::OnPropertyChange so verändert, dass das Dialogobjekt m_pChartProperty nicht mehr freigegeben wird, wenn der Dialog durch OK oder ABBRECHEN geschlossen wird (Listing 2.56).
Fehlersuche mit den MFC
175
... if (bCloseWindow) { // Dialog schließen und löschen m_pChartProperty->DestroyWindow (); // // // // //
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Die nächste Zeile ist auskommentiert, um ein künstliches Speicherleck einzuführen delete m_pChartProperty; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Unter der Voraussetzung, dass der Dialog während der Programmausführung aufgerufen und geschlossen worden ist, wird nach dem Beenden des Programms der Text aus Abbildung 2.64 im Debug-Fenster angezeigt.
Abbildung 2.64: Ausgabe von Speicherlecks
Dabei wird auf Zeile 125 verwiesen, die Stelle, an der das Dialogobjekt in OnChartProperty erzeugt worden ist. Die Ausgabe von Dateiname und Zeilennummer funktioniert nur, wenn man statt des Operators new das Makro DEBUG_NEW verwendet. Da der Anwendungs-Assistent für alle von ihm erzeugten Implementierungsdateien die Anweisung
DEBUG_NEW
#define new DEBUG_NEW
einfügt, ist dies für alle von ihm erzeugten Programmdateien automatisch der Fall. Man kann den Speicherüberwachungsmechanismus der MFC über das einfache Anzeigen von Speicherlecks beim Programmende hinaus nutzen. Der Zugriff auf den Mechanismus erfolgt durch Objekte der Klasse CMemoryState. Um einen Schnappschuss
CMemoryState
176
2
Einstieg in die MFC-Programmierung
des Zustands der Speicherverwaltung zu machen, wird die Funktion CMemoryState::Checkpoint aufgerufen. Checkpoint merkt sich den Speicherzustand zum Zeitpunkt des Aufrufs der Funktion. Nimmt man ein zweites Objekt der Klasse CMemoryState und ruft man Checkpoint zu einem späteren Zeitpunkt erneut auf, so lässt sich mit der Funktion Difference, der die beiden Objekte übergeben werden, feststellen, ob Unterschiede zwischen den beiden Speicherzuständen bestehen. Difference erzeugt aus den beiden ihr übergebenen Speicherzuständen einen Differenzspeicherzustand. Durch Aufruf der Funktion DumpStatistics erhält man einige Informationen zu einem Speicherzustand in Form von Zahlen. Die Funktion DumpAllObjectsSince gibt alle Objekte aus, die seit dem letzten Checkpoint-Aufruf angefordert und noch nicht wieder freigegeben worden sind. Wird diese Funktion auf ein nicht initialisiertes CMemoryState-Objekt angewandt, so werden alle derzeit im Speicher befindlichen Objekte ausgegeben. Listing 2.57 zeigt eine Beispielanwendung dieser Funktionen. CMemoryState ms1, ms2, msDiff; ms1.Checkpoint (); CString *s1 = new CString("James T. Kirk"); CString *s2 = new CString("USS Enterprise"); ms1.DumpAllObjectsSince (); ms2.Checkpoint (); msDiff.Difference (ms1, ms2); msDiff.DumpStatistics (); Listing 2.57: Verwendung von CMemoryState
Abbildung 2.65: Ausgabe von Listing 2.57
Fehlersuche mit den MFC
Der Programmcode in Listing 2.57 erzeugt die Ausgabe in Abbildung 2.65. Die Programmversion StockChartDebug enthält im Menü DEBUG einige Einträge, mit denen man die Speicherüberwachungsfunktionen ausprobieren kann. Diese Funktionen gibt es natürlich nur in der Debug-Version.
2.10.2 Tipps zur Vorgehensweise 왘 TRACE-Makros sind eine einfache Möglichkeit, den Programmablauf mit Kontrollausgaben zu versehen, die im Debug-Fenster des Developer Studio ausgegeben werden, ohne dass das Programm im Debugger angehalten werden muss. Die Formatierung des Ausgabe-Strings erfolgt analog zu printf. 왘 ASSERT-Makros ermöglichen die Implementierung von Vorund Nachbedingungen. Sie sind nur in der Debug-Version eines Programms aktiv. ASSERT-Makros können auch zum Prüfen von Zeigern verwendet werden, die nur zur Entwicklungszeit getestet werden sollen. 왘 Von CObject abgeleitete Klassen können die virtuelle Funktion AssertValid überschreiben, mit der sich die Konsistenz eines Objekts überprüfen lässt. 왘 Die Speicherüberwachungsmechanismen der MFC sind automatisch in der Debug-Version eines Programms aktiv. Um Speicherlecks zu finden, sollte man nach dem Programmlauf das Ausgabefenster des Developer Studio überprüfen. Nicht freigegebene Objekte werden hier meist mit ihrer Position im Quelltext ausgegeben!
2.10.3 Zusammenfassung Die MFC implementieren einige mächtige, von der Entwicklungsumgebung des Developer Studio unabhängige Hilfsmittel zur Fehlersuche. Das TRACE-Makro und der Dump-Mechanismus erlauben es, Variablenwerte ohne den Debugger auszugeben. Die Technik der Zusicherungen ist sehr mächtig, hängt jedoch stark von der Disziplin des Programmierers ab. Im Gegensatz dazu bekommt man den wirkungsvollen Speicherüberwachungsmechanismus allein durch die Verwendung der MFC praktisch
177
178
2
Einstieg in die MFC-Programmierung
zum Nulltarif. Alle diese Hilfsmittel unterstützen den Programmierer beim Auffinden von Fehlern, teilweise sogar ohne nach diesen suchen zu müssen. Auch im Zeitalter grafischer Debugger sollten solche Hilfsmittel nicht unterschätzt werden. Sie sind gute Werkzeuge, um fehlerfreie Software zu entwickeln.
2.11 MFC und DLLs Wenn man ein Programm übersetzt, das in einer so genannten Hochsprache, wie C oder C++, geschrieben ist, so setzt in einem ersten Schritt der Compiler den Quelltext in einen vom Prozessor ausführbaren Maschinencode um. Der Compiler bearbeitet dabei jede Quelltextdatei einzeln und erzeugt dafür jeweils eine so genannte Objektcodedatei. Die Objektcodedateien werden in einem zweiten Schritt von einem Programm namens Linker zusammengefügt und in ein Format gebracht, welches das Betriebssystem als Programm erkennt und ausführen kann. Schon vor längerer Zeit ist man auf die Idee gekommen, den zweiten Schritt – das Linken – nicht bei der Programmerstellung, sondern bei der Programmausführung vorzunehmen. Dies führte zum Konzept der Shared Libraries, unter Windows besser als Dynamic Link Libraries kurz DLLs, bekannt. Das Linken zur Übersetzungszeit wird auch als statisches Linken, das Linken zur Programmlaufzeit als dynamisches Linken bezeichnet.
2.11.1 Vorteile von DLLs Die Verwendung von DLLs kann – je nach Art des Programms – einige Vorteile haben: 왘 Stimmen bei mehreren Programmen größere Teile des Programmcodes überein, so lassen sich diese Teile in DLLs auslagern. Die Programme können die DLLs gemeinsam nutzen. Dies trägt zur Senkung des Speicherverbrauchs auf der Festplatte bei. Im Falle einer gleichzeitigen Ausführung wird der Speicherverbrauch auch im Hauptspeicher reduziert. 왘 Werden größere Programme durch Aufspaltung in mehrere DLLs modularisiert, so können später einzelne Teile ausgetauscht werden, ohne den Rest des Programms neu übersetzen zu müssen.
MFC und DLLs
179
왘 Bei großen Projekten kann die Verwendung von DLLs eine Zeitersparnis während der Entwicklungsphase bringen, da das zeitaufwändige Linken entfällt oder stark verkürzt wird. 왘 Durch die Verwendung von DLLs lassen sich Plug-In-Konzepte realisieren: Für ein Programm wird eine Schnittstelle spezifiziert, durch die es nachträglich erweitert werden kann. Die Erweiterungen werden in Form von DLLs implementiert. Plug-Ins eignen sich gut, um Programme durch Angebote von Drittherstellern erweitern zu lassen. 왘 DLLs können auf Anweisung des sie verwendenden Programms zur Laufzeit geladen werden. In den MFC gibt es dazu die Funktionen AfxLoadLibrary und AfxFreeLibrary. Wie man an den aufgeführten Punkten sieht, werden DLLs oft verwendet, wenn es um Fragen der Modularisierung geht. Dort liegt die Stärke des DLL-Konzepts. Aufgrund ihrer zahlreichen Vorteile sind DLLs schon seit langer Zeit unter Windows stark verbreitet, große Teile des Systems selbst sind in Form von DLLs implementiert.
Modularisierung
Als das DLL-Konzept entworfen wurde, hat man Programme für Windows noch in der Sprache C geschrieben. Die Sprache C++ wurde damals noch nicht verwendet. Entsprechend konnte niemand die Erfordernisse der Sprache C++ beim Entwurf der DLLArchitektur berücksichtigen. DLLs haben daher normalerweise eine Schnittstelle aus C-Funktionen.
DLL-Schnittstelle
Das Hauptproblem bei der Verwendung von C++ ist, dass der Compiler – anders als bei prozeduralen Sprachen wie C und Pascal – die Namen von Funktionen intern so abändert, dass sie eindeutig werden. In C++ ist die mehrfache Verwendung des gleichen Funktionsnamens möglich. Es ist erlaubt, mehrere Funktionen mit dem gleichen Namen innerhalb einer Klasse zu verwenden, wenn sich diese durch ihre Parameterliste unterscheiden. Damit der Compiler die verschiedenen, aber gleichnamigen Funktionen unterscheiden kann, ergänzt er den Funktionsnamen nach einem eigenen Schema zu einem eindeutigen Namen. Diesen Vorgang nennt man Name Mangling. Leider ist das Name Mangling nicht standardisiert und jeder Compiler-Hersteller verwendet sein eigenes Schema. Das ist das Hauptproblem bei der Verwendung von C++-Schnittstellen in DLLs. Benutzt man nämlich zwei verschiedene Compiler, um die DLL und das Programm, welches
Name Mangling
180
2
Einstieg in die MFC-Programmierung
diese verwendet, zu erstellen, dann kann der eine Compiler nicht die vom anderen Compiler generierten Funktionen aufrufen, da die erweiterten Funktionsnamen nicht zueinander passen. DLL-Modelle
Trotzdem werden C++ und die MFC innerhalb von DLLs verwendet. Bei der Verwendung der MFC in DLLs lassen sich verschiedene Modelle auswählen. Die Modelle haben unterschiedliche Möglichkeiten und Beschränkungen bei der Verwendung der MFC. 왘 WIN32-DLLs sind die konventionellen DLLs, sie verwenden die MFC nicht. Sie werden in C, C++ oder auch in anderen Sprachen, die DLLs erstellen können, programmiert. Die Schnittstelle besteht dabei entweder aus C-Funktionen (oder dazu aufrufkompatiblen Funktionen) oder aus Pascal-Funktionen. C- und Pascal-Funktionen unterscheiden sich dadurch, in welcher Reihenfolge Funktionsparameter auf dem Stack abgelegt werden. Für DLLs, die von verschiedenen Programmiersprachen aus verwendet werden sollen, wird normalerweise die Pascal-Aufrufkonvention verwendet. Diese lässt sich auch unter C und C++ durch das Schlüsselwort __stdcall verwenden. Übrigens ist das Windows-API selbst auf diese Weise definiert. Soll eine WIN32-DLL in C++ implementiert werden, so sind die Schnittstellenfunktionen als extern »C« zu deklarieren und zu definieren, wenn die DLL sprachen- und compilerunabhängig sein soll. Man kann auch C++-Funktionen und -Klassen exportieren, büßt dann aber aufgrund des Name Manglings Sprachen- und Compilerunabhängigkeit ein. 왘 Reguläre MFC-DLLs (auch normale MFC-DLLs genannt) mit statischer Bindung verwenden die Klassen der MFC intern. Allerdings sollte, genau wie bei WIN32-DLLs, die Schnittstelle der DLL nur aus normalen C-Funktionen bestehen. C++-Klassen können zwar exportiert werden, dürfen aber nicht von MFCKlassen abgeleitet worden sein. Da die MFC-Bibliothek statisch zur DLL gelinkt wird, ist eine reguläre MFC-DLL relativ groß.
Anwendung C-Schnittstelle MFC
DLL
Abbildung 2.66: Struktur einer regulären MFC-DLL (statisch)
MFC und DLLs
181
왘 Reguläre MFC-DLLs mit dynamischer Bindung haben die gleichen Eigenschaften wie MFC-DLLs mit statischer Bindung. Dadurch, dass die MFC- Bibliothek dynamisch zur DLL gelinkt wird, ist sie nicht Teil der DLL. Daher ergibt sich für reguläre MFC-DLLs mit dynamischer Bindung eine deutlich geringere Größe als für reguläre MFC-DLLs mit statischer Bindung. Die Abbildung 2.66 zeigt die MFC-Bibliothek bei statischer und Abbildung 2.67 bei dynamischer Bindung.
Anwendung C-Schnittstelle DLL
MFC-DLL
Abbildung 2.67: Struktur einer regulären MFC-DLL (dynamisch)
Anwendung C++/MFC-Schnittstelle DLL
MFC-DLL
Abbildung 2.68: Struktur einer MFC-Erweiterungs-DLL
왘 MFC-Erweiterungs-DLLs sind die einzige Art von DLLs, die MFC-Klassen über ihre Schnittstelle hinaus sichtbar machen können. Im Gegensatz zu normalen DLLs sind die MFC in dieser Art von DLLs nicht eingeschlossen. MFC-Klassen sind von außerhalb der DLL aus zugänglich. Damit Erweiterungs-DLLs funktionieren können, muss das Programm, das die DLL verwendet, die gleiche MFC-Version benutzen wie die DLL. Beide müssen die MFC-Bibliothek dynamisch linken und für beide
182
2
Einstieg in die MFC-Programmierung
muss der gleiche Compiler verwendet werden. Die MFC-Bibliothek wird selbst als Erweiterungs-DLL verwendet, wenn sie dynamisch zu einem Programm gelinkt wird. Abbildung 2.68 zeigt die Struktur von MFC-Erweiterungs-DLLs. C++ erlaubt es, das Name Mangling des Kompilers für Funktionen, die außerhalb von Klassen deklariert werden, abzuschalten. Diese sind als extern »C« zu deklarieren und zu definieren. Hat man mehrere Funktionen, die so definiert und deklariert werden sollen, wird das meist nach folgendem Schema durchgeführt: ... #ifdef __cplusplus extern "C" { #endif ... // Hier Deklarationen bzw. Definitionen vornehmen. ... #ifdef __cplusplus } #endif
Ist man sich sicher, dass die Schnittstellendefinition nur von einem C++Compiler übersetzt werden wird, so kann man die Präprozessorkommandos auch weglassen. Als extern »C« deklarierte C++-Funktionen verhalten sich wie normale C-Funktionen. Anhand einer regulären MFC-DLL mit statischer Bindung sollen zunächst die grundlegenden Aspekte beim Erstellen von DLLs betrachtet werden. Danach wird auf MFC-Erweiterungs-DLLs eingegangen.
2.11.2 Reguläre MFC-DLLs Reguläre MFC-DLLs verwenden die Klassen der MFC nur intern, sie treten nach außen nicht in Erscheinung. Alle Schnittstellen sollten als C-Schnittstellen definiert werden. C++-Schnittstellen können – mit Verlust von Sprachen- und Compilerunabhängigkeit – verwendet werden, dürfen aber keine MFC-Klassen benutzen. Funktionen und Variablen, die von außerhalb der DLL aus angesprochen werden sollen, müssen exportiert werden. Man spricht in diesem Zusammenhang auch von Symbolen, die exportiert werden müssen. Die Symbole stehen für Funktions- und Variablennamen. Möchte auf der anderen Seite ein Programm Funktionen oder Variablen einer DLL verwenden, so muss es deren Symbole
MFC und DLLs
183
importieren. Der Programmierer muss Symbole ausdrücklich exportieren oder importieren, da die Voreinstellung für Funktionen und Variablen festlegt, sie nicht aus einer DLL zu exportieren oder zu importieren. Zum Export und Import wird unter Windows das Schlüsselwort __declspec mit den Attributen dllimport und dllexport verwendet. Das Schlüsselwort muss nur bei der Deklaration angegeben werden.
__delspec
Die Verwendung von regulären MFC-DLLs soll an einem Beispiel demonstriert werden. Um zu zeigen, dass das aufrufende Programm weder die MFC verwenden, noch in C++ geschrieben sein muss, wird ein kleines C-Programm verwendet, das nur das WIN32-API benutzt. Da man WIN32-Programme nicht so ohne weiteres wie eine MFC-Anwendung mit dem Anwendungs-Assistenten erstellen kann, wird im Beispiel ein Konsolenprogramm verwendet. Konsolenprogramme laufen innerhalb der MS-DOSEingabeaufforderung ab und sehen auch wie MS-DOS-Programme aus. Das heißt, sie haben normalerweise keine Fenster. Jedes Konsolenprogramm hat aber vollen Zugriff auf das WIN32API. Konsolenprogramme lassen sich in Visual C++ erstellen, indem man als Projekttyp WIN32-PROJEKT auswählt und im nachfolgenden Dialogfeld unter Anwendungseinstellung die Option KONSOLENANWENDUNG anklickt. Das Beispielprogramm soll einen Text von der Tastatur einlesen, Leerzeichen am Anfang und am Ende des Texts entfernen und den Text in Kleinschreibung umwandeln. Entsprechend heißt das aufrufende Programm LowerAndTrim. Es befindet sich auf der Begleit-CD im KAPITEL2\LOWERANDTRIM. Das Programm LowerAndTrim besteht nur aus einer einzigen Quelltextdatei, die in Listing 2.58 gezeigt wird. // LowerAndTrim.c #include <stdio.h> #include "lat_inc.h" int main (void) { char str[100]; printf ("\nBitte einen String eingeben: "); gets (str);
Programm LowerAndTrim
184
2
Einstieg in die MFC-Programmierung
LowerAndTrim (str); printf ("\nUnd hier ist das Ergebnis: %s", str); return 0; } Listing 2.58: Konsolenprogramm LowerAndTrim
Zur Umwandlung des eingegebenen Texts ruft das Programm die Funktion LowerAndTrim auf. Diese Funktion ist nicht Teil des Programms, sondern ist in einer MFC-DLL enthalten. Wie man sieht, ist die Verwendung der DLL ganz einfach. Es sind nur zwei Dinge zu beachten. Wie sonst unter ANSI-C oder C++ üblich, muss es einen Funktionsprototyp für LowerAndTrim geben. Dieser wird durch die Header-Datei LAT_INC.H eingebunden. Außerdem muss der Linker von der Existenz der Funktion erfahren, da es sonst zu Fehlermeldungen über nicht definierte Symbole kommt. Das wird durch eine Importbibliothek bewerkstelligt. Diese wird beim Erstellen einer DLL automatisch angelegt. Sie hat den gleichen Namen wie die DLL, allerdings mit der Dateiendung LIB. Bei der ClientAnwendung der DLL – die Anwendung, die die DLL verwendet – muss die Importbibliothek entweder in den Projekteinstellungen unter PROJEKT | EIGENSCHAFTEN | LINKER | EINGABE | ZUSÄTZLICHE EINGABE | BEARBEITEN zu den Bibliotheksmodulen hinzugefügt werden oder als Datei mit in das Projekt aufgenommen werden (Im Kontextmenü des Projekts: HINZUFÜGEN | VORHANDENES ELEMENT HINZUFÜGEN...). AfxLoadLibrary
Es gibt alternativ die Möglichkeit, eine DLL auf Anweisung des Programms explizit zu laden. In diesem Fall wird keine Importbibliothek benötigt. Das Programm ruft die Funktion AfxLoadLibrary auf, um die DLL zu laden. Danach muss es für jede Funktion, die aufgerufen werden soll, einen Funktionszeiger mit der Funktion GetProcAddress anfordern. Die Funktionen der DLL können dann über diese Funktionszeiger aufgerufen werden. Wird die DLL nicht mehr benötigt, muss sie durch den Aufruf von AfxFreeLibrary freigegeben werden. Die Verwendung von AfxLoadLibrary ist flexibler als die Verwendung einer Importbibliothek, da das Programm selbst entscheiden kann, welche DLL es laden möchte. Das kann beispielsweise dann sinnvoll sein, wenn eine DLL in mehreren Sprachversionen vorliegt. Die Verwendung einer Importbibliothek ist jedoch bequemer und der Normalfall bei der Arbeit mit DLLs.
MFC und DLLs
Doch nun zur Implementierung der DLL: Die DLL, die vom Programm LowerAndTrim verwendet wird, heißt LAT.DLL. Auf der Begleit-CD befindet sich das DLL-Projekt im Verzeichnis KAPITEL2\LAT. Die DLL ist mit dem MFC-Assistenten für DLLs erstellt worden (unter PROJEKT | NEU | MFC-DLL). Im MFC-DLLAssistenten kann man unter ANWENDUNGSEINSTELLUNG zwischen einer regulären MFC-DLL mit und ohne statische Verwendung der MFC und den MFC-Erweiterungs-DLLs wählen. Außerdem wird Unterstützung für die Automation und Windows-Sockets angeboten. Für LAT.DLL ist die Einstellung »Reguläre DLL, die mit MFC statisch verknüpft ist« ausgewählt worden, die MFCBibliothek ist also statisch zu der DLL gelinkt worden. Die DLL hat die stattliche Größe von 148 Kbyte in der Release-Version. Dafür sind allerdings alle von den MFC benötigten Klassen komplett enthalten, es werden keine weiteren externen Dateien benötigt. Wählt man die Möglichkeit, die MFC dynamisch zu linken, dann schrumpft LAT.DLL auf 8 Kbyte! Der MFC-Assistent für DLLs erzeugt die Quelltextdateien LAT.CPP und LAT.H. Listing 2.59 zeigt den Quelltext von LAT.CPP mit der bereits implementierten Funktion LowerAndTrim. Man sieht, dass auch reguläre MFC-DLLs ein Objekt einer von CWinApp abgeleiteten Klasse verwenden. Es wird in diesem Beispiel allerdings nichts zu CWinApp hinzugefügt oder das Objekt explizit verwendet. ////////////////////////////////////////////////////////////// / // CLATApp BEGIN_MESSAGE_MAP(CLATApp, CWinApp) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// // CLATApp Konstruktion CLATApp::CLATApp() { } ////////////////////////////////////////////////////////////// // Das einzige CLATApp-Objekt CLATApp theApp;
extern "C" void LowerAndTrim (char *str)
185 LAT.DLL
186
2
Einstieg in die MFC-Programmierung
{ // dieser Aufruf ist wichtig, um interne // Variablen innerhalb der DLL in den richtigen // Zustand zu versetzen!!! AFX_MANAGE_STATE(AfxGetStaticModuleState()); CString cStr; cStr = str; cStr.TrimLeft (); cStr.TrimRight (); cStr.MakeLower (); strcpy (str, cStr); } Listing 2.59: LAT.CPP mit der Implementierung von LowerAndTrim
LowerAndTrim implementiert seine Funktionalität durch Verwendung der Klasse CString, also unter Ausnutzung der MFC. Damit die Funktion von beliebigen Sprachen aus aufgerufen werden kann, ist sie als extern »C« definiert. Zu Beginn der Funktion wird das Makro AFX_MANAGE_STATE mit dem Funktionsaufruf AfxGetStaticModuleState als Parameter eingefügt. Dieses Makro muss von jeder aus einer regulären MFC-DLL exportierten Funktion als allererste Anweisung, noch vor der Deklaration von Variablen, aufgerufen werden. Diese Anweisung bewirkt, dass einige interne globale Variablen der DLL in einen korrekten Zustand versetzt werden. Lässt man das Makro weg, dann kann es zu Problemen kommen. Besondere Aufmerksamkeit gilt der Deklaration von LowerAndTrim. Die Deklaration befindet sich nicht in LAT.H, da LAT.H eine C++-Header-Datei ist. Da die DLL aber von einem C-Programm verwendet werden soll, ist es nicht sehr sinnvoll, den Funktionsprototyp in eine C++-Datei zu stecken. Folglich befindet sich die Deklaration von LowerAndTrim in der eigenen Datei LAT_INC.H. Listing 2.60 zeigt diese Datei. // Lat_inc.h #ifndef __lat_inc__ #define __lat_inc__ #ifdef __cplusplus extern "C" { #endif
MFC und DLLs
187
#if _USRDLL void __declspec(dllexport) LowerAndTrim (char *str); #else void __declspec(dllimport) LowerAndTrim (char *str); #endif #ifdef __cplusplus } #endif #endif Listing 2.60: Header-Datei mit dem Funktionsprototyp von LowerAndTrim
Der Präprozessor stellt anhand der Konstanten __cplusplus fest, ob die Datei in einem C- oder C++-Programm verwendet wird. Wird die Header-Datei in einem C++-Programm verwendet, so wird LowerAndTrim automatisch als externe C-Funktion deklariert. Die Präprozessorkonstante _USRDLL ist immer dann definiert, wenn eine reguläre MFC-DLL übersetzt wird. Daher wird – je nachdem, ob die DLL selbst übersetzt oder von einem Programm verwendet wird – die Funktion LowerAndTrim exportiert oder importiert. Allerdings versagt diese Vorgehensweise, wenn LowerAndTrim aus einer anderen MFC-DLL aufgerufen wird, da die Funktion zwar importiert werden soll, aber durch _USRDLL auf Export gesetzt wird. In diesem Fall muss man ein anderes Schema verwenden, um Exporte und Importe zu unterscheiden. Vorschläge dazu finden sich im technischen Hinweis 33.
Verwendung des Präprozessors
Um Importe und Exporte zu kennzeichnen, kann neben __declspec auch eine so genannte Moduldefinitionsdatei (Dateiendung DEF) verwendet werden. Die Verwendung einer Moduldefinitionsdatei ist zwar umständlicher als die Verwendung von __declspec, jedoch können sich bei großen Projekten Geschwindigkeitsvorteile ergeben, da Funktionen nicht über ihre Namen, sondern über Ordinalzahlen exportiert werden. Da dies bei kleineren bis mittleren Projekten jedoch kaum eine Rolle spielt, soll diese Vorgehensweise hier nicht weiter verfolgt werden. Die DLL-Version der MFC verwendet eine solche Moduldefinitionsdatei, die MFC exportieren allerdings auch mehrere tausend Symbole.
DEF-Datei
188
2
Einstieg in die MFC-Programmierung
2.11.3 MFC-Erweiterungs-DLLs Erweiterungs-DLLs verhalten sich in vielen Aspekten völlig anders als reguläre MFC-DLLs. Vereinfacht gesprochen heben Erweiterungs-DLLs die Nachteile einer normalen DLL-Schnittstelle auf. MFC-Konstrukte können frei über die Schnittstelle hin- und herbewegt werden, und die Beschränkung auf eine C-Schnittstelle entfällt. Erkauft wird diese Freiheit mit äußeren Beschränkungen: Erweiterungs-DLLs lassen sich nur von MFC-Programmen verwenden, Programm und DLL müssen die gleiche Version der MFC benutzen, der gleiche Compiler muss für beide verwendet werden und die MFC kann nur dynamisch an Programm und DLL gebunden werden. Der Verwendungsbereich von Erweiterungs-DLLs ist also deutlich kleiner. Dafür eignen sie sich hervorragend für den Zweck, den ihr Name bereits andeutet: Sie können die MFC erweitern. Dialoge oder selbst definierte Steuerelemente lassen sich gut in ihnen unterbringen. Um dies zu demonstrieren, soll der Dialog zur Eingabe von Aktiendaten im Programm StockChart in eine DLL ausgelagert werden.
2.11.4 StockChart mit DLL Die nun besprochene Version des Programms StockChart befindet sich im Verzeichnis KAPITEL2\STOCKCHARTWITHDLL auf der Begleit -CD. Das Programm heißt in dieser Version StockChartWithDll. Aus dem Projekt sind die Klasse CStockProperty und damit die Dateien STOCKPROPERTY.H und STOCKPROPERTY.CPP entfernt worden. Auch die Dialogressource ist nicht mehr im Projekt enthalten. Um den Dialog in einer Erweiterungs-DLL zu implementieren, wurde mit dem MFC-Assistenten für DLLs ein Projekt für eine Erweiterungs-DLL mit dem Namen STOCKPROPERTYDLL angelegt. Im MFC-DLL-Assistenten wurde dazu die dritte Option ERWEITERUNGS-MFC-DLL ausgewählt. Das dazugehörige Projekt befindet sich auf der CD innerhalb des Projektordners STOCKCHARTWITHDLL. Beide Projekte sind zu einem Arbeitsbereich zusammengefasst. Die Dialogklasse und die DDX-Variablen sind in genau der gleichen Weise wie bei der ersten Version des Programms StockChart angelegt worden. Daher soll jetzt nur auf die wenigen Unterschiede eingegangen werden.
MFC und DLLs
Genau wie bei normalen DLLs müssen auch bei ErweiterungsDLLs alle Symbole exportiert werden, die außerhalb der DLL angesprochen werden sollen. Bei Erweiterungs-DLLs lassen sich jedoch auch ganze Klassen exportieren. Die MFC bieten hierfür das Makro AFX_EXT_CLASS an. AFX_EXT_CLASS wird letztendlich auch nur auf __declspec(dllexport) oder __declspec(dllimport) zurückgeführt. Es ist allerdings einfacher zu benutzen, da man nicht selbst unterscheiden muss, wann zu exportieren und wann zu importieren ist. Bei mehreren DLLs, die sich gegenseitig aufrufen, versagt das Makro allerdings. Was man in diesem Falle machen kann, wird im technischen Hinweis 33 beschrieben.
189 AFX_EXT_CLASS
Um die Dialogklasse zu exportieren, wird die – nun in der DLL befindliche – Klasse CStockProperty mit diesem Makro definiert: class AFX_EXT_CLASS CStockProperty : public CDialog
Das reicht aus, um die Klasse außerhalb der DLL verwenden zu können. Prinzipiell ist es auch möglich, Teile von Klassen zu exportieren. Dies ist jedoch komplizierter und kann einige Probleme nach sich ziehen. Zwei wichtige Details bei Erweiterungs-DLLs sind ihr Verhalten in Bezug auf die Speicherverwaltung und auf das Ressourcenmanagement. Im Gegensatz zu allen anderen Arten von DLLs verwenden Erweiterungs-DLLs den gleichen Speicherraum wie die sie aufrufende Anwendung. Das bedeutet, dass Zeiger auf Speicherbereiche frei über die Schnittstelle hinweg ausgetauscht werden dürfen. Die Anwendung kann beispielsweise Speicher anfordern, der in der DLL wieder freigegeben wird. So etwas ist bei anderen DLLs nicht erlaubt, da Programm und DLL Speicher aus zwei unterschiedlichen Quellen (Memory Pools) allozieren (Hierzu lese man den technischen Hinweis 33). Auch bei den Ressourcen ergibt sich ein gemeinsamer Namensraum. Obwohl man die gleichen IDs in Programm und DLL verwenden kann, sollte dies vermieden werden, da man dann unter Umständen die falsche Ressource lädt. Die Ressourcensuche findet innerhalb von Programm, DLL und MFC-DLLs nach vorgegebenen Suchreihenfolgen statt. Die Suchreihenfolge hängt jedoch davon ab, wo die Suche gestartet wurde. Wird eine Ressource aus dem Programm geladen, so ist die Suchreihenfolge: 1. Programmdatei 2. Erweiterungs-DLL 3. MFC-DLLs
Speicherverwaltung in Erweiterungs-DLLs
190
2
Einstieg in die MFC-Programmierung
Im Falle, dass die Ressourcensuche in der Erweiterungs-DLL gestartet wird, ist die Suchreihenfolge: 1. Erweiterungs-DLL 2. MFC-DLLs 3. Programmdatei Wenn sichergestellt ist, dass jeweils die passende Ressource zuerst gefunden wird, können Ressourcen-IDs natürlich auch mehrfach verwendet werden. Zu empfehlen ist das jedoch nicht unbedingt, da es leicht zu Fehlern kommen kann. DllMain
Erweiterungs-DLLs verwenden im Gegensatz zu normalen MFCDLLs kein Objekt einer von CWinApp abgeleiteten Klasse. Die Erweiterungs-DLL arbeitet mit dem entsprechenden Objekt in der sie aufrufenden Anwendung zusammen. Damit eine Erweiterungs-DLL funktioniert, muss allerdings die Funktion DllMain korrekt implementiert sein. DllMain initialisiert die DLL, wenn sie an das sie aufrufende Programm gebunden wird, und führt Aufräumarbeiten durch, wenn die Bindung gelöst wird. Der MFCAssistent für DLLs erzeugt eine passende Version von DllMain, wenn er zur Erzeugung der DLL verwendet wird. Listing 2.61 zeigt die vom Assistenten für die StockChart-DLL erzeugte Funktion. extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { // Entfernen Sie dies, wenn Sie lpReserved verwenden UNREFERENCED_PARAMETER(lpReserved); if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("STOCKPROPERTYDLL.DLL Initializing!\n"); // One-Time-Initialisierung der Erweiterungs-DLL if (!AfxInitExtensionModule (StockPropertyDllDLL, hInstance)) return 0; new CDynLinkLibrary(StockPropertyDllDLL); } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0("STOCKPROPERTYDLL.DLL Terminating!\n");
MFC und DLLs
191
// Bibliothek vor dem Aufruf der Destruktoren schließen AfxTermExtensionModule(StockPropertyDllDLL); } return 1;
// OK
} Listing 2.61: Vom Anwendungs-Assistenten erzeugte Funktion DllMain
Die Standardimplementierung von DllMain muss zumindest die Funktion AfxInitExtensionModule zur Initialisierung aufrufen und ein Objekt der Klasse CDynLinkLibrary muss angelegt werden. AfxInitExtensionModule initialisiert die DLL, das Objekt der Klasse CDynLinkLibrary sorgt unter anderem dafür, dass Ressourcen modulübergreifend gefunden werden können. Eigene Initialisierungen und Aufräumarbeiten können in der Funktion DllMain vorgenommen werden.
2.11.5 Tipps zur Vorgehensweise 왘 Bei der Entwicklung von DLLs mit den MFC muss zunächst entschieden werden, welches Modell verwendet werden soll. Benötigt man die MFC nur innerhalb der DLL, sollte man eine reguläre MFC-DLL erstellen. Wird die DLL dazu benötigt, die MFC oder ein MFC-Programm zu erweitern, dann muss eine MFC-Erweiterungs-DLL verwendet werden. 왘 Bei regulären MFC-DLLs sollten alle Schnittstellenfunktionen als extern »C« definiert werden. 왘 Reguläre MFC-DLLs müssen am Anfang jeder exportierten Funktion das Makro AFX_MANAGE_STATE mit dem Parameter AfxGetStaticModuleState aufrufen. Es ist wichtig, dass diese Anweisung vor allen Anweisungen in der betreffenden Funktion, ja selbst vor den Variablendeklarationen steht. 왘 Exporte und Importe müssen über __declspec(dllexport) und __declspec(dllimport) angegeben werden. Bei ErweiterungsDLLs kann auch AFX_EXT_CLASS verwendet werden, um ganze Klassen zu exportieren oder zu importieren. 왘 Bei Erweiterungs-DLLs muss die DLL-Eintrittsfunktion DllMain korrekt implementiert sein. Der MFC-Assistent für DLLs erzeugt bereits eine Implementierung für diese Funktion. 왘 Zu dem Programm, das die DLL verwendet, muss eine Importbibliothek gelinkt werden. Diese hat den gleichen Namen wie
192
2
Einstieg in die MFC-Programmierung
die DLL, aber die Dateiendung LIB. Alternativ können DLLs explizit durch das Programm geladen werden, was aber aufwändiger ist.
2.11.6 Zusammenfassung Die Verwendung von DLLs ist eine unter Windows intensiv genutzte Möglichkeit, um Programme modular und erweiterbar zu gestalten sowie mehrfach benötigten Programmcode gemeinsam zu nutzen und damit Speicherplatz zu sparen. Die Schnittstelle von DLLs ist für prozedurale Programmiersprachen, das heißt Sprachen wie C oder Pascal, entworfen worden. Bei der Verwendung von C++ treten Probleme auf, die in erster Linie auf die nicht standardisierte Behandlung von Funktionsnamen der Sprache C++ zurückzuführen sind. Die MFC begegnen diesem Problem, indem sie zwei Typen von DLLs bereitstellen: reguläre MFC-DLLs und MFC-Erweiterungs-DLLs. Beide Typen haben ihre spezifischen Vor- und Nachteile. Reguläre MFC-DLLs können die Klassen der MFC nur intern verwenden, sind damit aber von anderen Programmiersprachen aus ansprechbar. MFC-Erweiterungs-DLLs können MFC-Klassen zwar über die DLL-Schnittstelle hinweg verwenden, können aber selbst auch nur aus MFC-Programmen heraus aufgerufen werden. Zudem sind sie auf eine bestimmte Version der MFC beschränkt. Der Programmierer muss den Typ wählen, der sich am besten mit seinen Anforderungen deckt.
2.12 Programmierung mit Threads Prozesse und Threads
Was ist ein Thread? Ein Thread (engl. Faden) ist ein Ausführungspfad für den Programmcode innerhalb eines Prozesses. Was ist ein Prozess? Ein Prozess ist die ausgeführte Instanz eines Programms. Es können sowohl mehrere Prozesse als auch mehrere Threads gleichzeitig ausgeführt werden. Tatsächlich gleichzeitig können allerdings nur so viele Prozesse und Threads ausgeführt werden, wie ein Computer Prozessoren (CPUs) hat. Reichen die zur Verfügung stehenden Prozessoren nicht aus, um alle Threads gleichzeitig auszuführen, was auf heutigen Computersystemen fast immer der Fall ist, dann werden Prozesse und Threads quasi gleichzeitig ausgeführt, das heißt, es wird einfach sehr schnell zwischen ihnen umgeschaltet. Das lässt den Eindruck entstehen, dass die Threads gleichzeitig ausgeführt werden.
Programmierung mit Threads
193
Was unterscheidet nun Prozesse und Threads? Prozesse werden normalerweise vom Betriebssystem angelegt, wenn ein Programm gestartet wird. Ein Prozess besitzt einen eigenen Stack und einen eigenen Speicherbereich, der durch Mechanismen des Betriebssystems gegen den Zugriff von außen geschützt wird. Außerdem besitzt jeder Prozess automatisch einen Thread, seinen HauptThread. Threads sind »die kleinen Brüder« der Prozesse. Auch sie haben ihren eigenen Stack, laufen aber im Speicherbereich eines Prozesses ab. Weil Threads keine besondere Speicherverwaltung benötigen, werden sie auch als »leichtgewichtige Prozesse« bezeichnet. Leichtgewichtig bedeutet hier, dass die Umschaltung zwischen Threads wesentlich effizienter ist als die Umschaltung zwischen Prozessen. Dies liegt darin begründet, dass der Speicherkontext beim Thread-Wechsel nicht umgeschaltet werden muss. Dadurch werden Prozessorzyklen gespart, weswegen Threads deutlich geeigneter sind, um Berechnungsaufgaben zu parallelisieren und damit eventuell auf mehreren Prozessoren gleichzeitig auszuführen. Auch auf Systemen mit nur einem Prozessor kann es durchaus sinnvoll sein, ein Programm in mehrere Threads aufzuteilen. Soll das Programm umfangreiche Berechnungen durchführen, aber gleichzeitig bedienbar bleiben, so ist die einfachste Lösung, die Berechnung in einen zweiten Thread auszulagern. Es gibt auch die Möglichkeit, Berechnungen häppchenweise immer dann auszuführen, wenn das Programm gerade nichts anderes zu tun hat. Diese Vorgehensweise ist jedoch wenig elegant, da der Programmierer selbst seine Berechnung in kleine Stücke zerlegen muss, die in kurzer Zeit abgearbeitet werden können. Die MFC unterstützen allerdings auch diese Vorgehensweise, weil es unter den 16-BitVersionen von Windows noch keine Threads gab. Dazu ist die Funktion CWinApp::OnIdle zu überschreiben und die Berechnung dort in kleinen Intervallen durchzuführen. Bei der Programmierung mit Threads (und auch mit Prozessen) muss sich der Programmierer nicht selbst um die zeitliche Aufteilung seiner Berechnung kümmern. Das Betriebssystem legt den zeitlichen Ablauf anhand von Prioritäten selbst fest. Das Betriebssystem unterbricht den Ablauf von Threads und Prozessen in regelmäßigen Intervallen, um zwischen diesen umzuschalten. Dieser Vorgang wird auch als preemptives (unterbrechendes) Multitasking bezeichnet.
Preemptives Multitasking
194
2
Einstieg in die MFC-Programmierung
2.12.1 Aufbau und Funktionsweise von Threads AfxBeginThread
Wie sieht ein Thread aus Programmierersicht aus? Dem Programmcode sieht man nicht an, von welchem Thread er ausgeführt wird. Da sich alle Threads eines Prozesses den gleichen Speicherbereich teilen, können auch alle Threads alle Funktionen des Prozesses, also des Programms, aufrufen. Auch kann jeder Thread auf alle globalen Variablen des Programms zugreifen. Lediglich funktionslokale Variablen gehören dem einzelnen Thread, da jeder Thread seinen eigenen Stack hat. Gestartet wird ein Thread unter Windows durch den Aufruf einer speziellen Funktion. In den MFC ist das die globale Funktion AfxBeginThread. Dieser Funktion wird – unter anderem – der Zeiger auf eine andere Funktion übergeben. Nach dem Aufruf von AfxBeginThread gibt es zwei Threads. Der neu erzeugte Thread beginnt seine Ausführung in der übergebenen Funktion. Sollte er irgendwann aus dieser Funktion zurückkehren, dann wird er automatisch beendet. Threads haben zwei Eigenschaften, die zusammengenommen sehr gefährlich sein können: Sie können auf alle Daten des Prozesses zugreifen und sie können jederzeit – das heißt auch mitten in einer einzelnen Operation – unterbrochen werden. Moderne Prozessoren können den Ablauf eines Programms sogar mitten in einem Maschinenbefehl unterbrechen. Dadurch wird es potenziell gefährlich, Variablen eines Prozesses von mehr als einem Thread zu verändern. Schließlich kann ein Thread mitten in einer Berechnung unterbrochen worden sein. Benutzt ein zweiter Thread nun diese inkonsistenten Daten für eigene Berechnungen, dann resultieren daraus unvorhersehbare Ergebnisse. Um diesem Dilemma zu entgehen, kommt man nicht umhin, ein weiteres, durch das Betriebssystem unterstütztes Konzept einzuführen. Jeder Bereich, der Variablen verwendet, auf die von mehr als einem Thread aus zugegriffen wird, wird als kritischer Abschnitt bezeichnet. Es kann beliebig viele solcher kritischer Abschnitte in einem Programm geben. Von allen kritischen Abschnitten, die auf die gleichen Variablen zugreifen, darf immer nur einer zu einem Zeitpunkt von einem der Threads ausgeführt werden. Alle anderen Threads müssen warten, falls sie selbst in kritische Abschnitte eintreten wollen. Die Threads können nur im gegenseitigen Ausschluss auf die kritischen Abschnitte zugreifen.
Programmierung mit Threads
Nicht nur der gleichzeitige Zugriff auf gemeinsam genutzte Variablen muss bei der Programmierung mit mehreren Threads geregelt werden, sondern auch der Zugriff auf gemeinsam genutzte Ressourcen aller Art. Gemeinsam genutzte Ressourcen sind unter anderem Ausgabegeräte, wie beispielsweise der Drucker. Nur ein Prozess und innerhalb des Prozesses nur ein Thread dürfen den Drucker zu einem Zeitpunkt ansteuern, wenn ein sinnvoller Ausdruck entstehen soll. Erst wenn ein Druckvorgang abgeschlossen ist, darf der nächste Prozess oder Thread Zugang zum Drucker erhalten. Allgemein sagt man, dass der Zugang zu Ressourcen synchronisiert werden muss. Damit der Programmierer den Zugang zu Ressourcen synchronisieren kann, muss das Betriebssystem Hilfsmittel zur Verfügung stellen, die dies ermöglichen.
195 Synchronisation
Windows stellt eine ganze Reihe solcher Hilfsmittel zur Verfügung. Aufgabe des Programmierers ist es, die richtigen Hilfsmittel auszuwählen und den Zugriff auf kritische Abschnitte und gemeinsam genutzte Ressourcen mit diesen Hilfsmitteln zu schützen.
2.12.2 Threads in den MFC Die MFC stellen zur Erzeugung von Threads die Klasse CWinThread zur Verfügung. Interessanterweise ist CWinThread die Basisklasse von CWinApp. Jede MFC-Applikation ist also automatisch auch eine Instanz von CWinThread (siehe dazu auch Abbildung 2.9, im Abschnitt 2.3.4, »Die Klassen des Anwendungsgerüsts«). Die Klasse CWinThread kapselt zwei verschiedene Arten von Threads: Benutzeroberflächen-Threads und Arbeits-Threads. Benutzeroberflächen-Threads haben eine eigene Nachrichtenschleife und sind zum Zugriff auf die Benutzeroberfläche berechtigt. Der Haupt-Thread einer MFC-Anwendung ist immer ein Benutzeroberflächen-Thread. Üblicherweise verwendet ein Programm einen Benutzeroberflächen-Thread und keinen, einen oder mehrere Arbeits-Threads. Arbeits-Threads werden zur Ausführung von Berechnungen verwendet. Sie dürfen nicht auf Teile der Benutzeroberfläche zugreifen und haben keine eigene Nachrichtenschleife, können also Ereignisse des Systems nicht selbst verarbeiten. Wenn man sehr rechenintensive Aufgaben zu lösen hat, dann ist es oft am sinnvollsten, so viele Arbeits-Threads zu verwenden, wie der benutzte Computer Prozessoren hat. Die Voraussetzung dafür ist, dass sich der verwendete Algorithmus parallelisieren lässt. Die MFC bieten zur Synchronisation zwischen Prozessen eine ganze Reihe von Klassen. Diese bauen auf den von Windows zur
CWinThread
196
2
Einstieg in die MFC-Programmierung
Verfügung gestellten Synchronisationshilfsmitteln auf. Abbildung 2.69 zeigt die von den MFC zur Verfügung gestellten Synchronisationsklassen. CObject CSyncObject CCriticalSection CEvent CMutex CSemaphore Abbildung 2.69: Synchronisationsklassen der MFC CSyncObject
Alle MFC-Synchronisationsklassen sind von der abstrakten Basisklasse CSyncObject abgeleitet. CSyncObject deklariert die virtuellen Funktionen Lock und Unlock, über die der Zugang zu einer Ressource hergestellt wird. Tabelle 2.7 führt die von CSyncObject abgeleiteten Synchronisationsklassen auf und erklärt deren Funktion. Klasse
Aufgabe
CCriticalSection
Diese Klasse dient zum Absichern kritischer Abschnitte. Sie kann nur für Threads, nicht aber für Prozesse verwendet werden. Nur ein Thread kann jeweils in einen durch CCriticalSection geschützten kritischen Abschnitt eintreten. Alle anderen Threads müssen warten oder werden abgewiesen.
CMutex
Diese Klasse ist von der Funktionsweise her mit CCriticalSection identisch. Allerdings lässt sich ein CMutexObjekt auch über Prozessgrenzen hinweg verwenden. Die weitreichendere Verwendungsmöglichkeit von CMutex gegenüber CCriticalSection wird mit einem etwas schlechteren Laufzeitverhalten erkauft.
CSemaphore
Diese Klasse implementiert eine Semaphore. Eine Semaphore ermöglicht einen zählenden, prozessübergreifenden Zugriff auf eine Ressource. Das bedeutet, dass – anders als bei den bereits genannten Klassen – mehr als ein Thread oder Prozess Zugriff auf eine Ressource erhalten kann. Dies kann beispielsweise sinnvoll sein, wenn eine Ressource in mehrfacher Ausfertigung vorhanden ist. Man kann so zum Beispiel über eine Semaphore drei Threads oder Prozesse auf drei zur Verfügung stehende serielle Schnittstellen zugreifen lassen.
Tabelle 2.7: Klassen zur Synchronisation
Programmierung mit Threads
Klasse
Aufgabe
CEvent
Anders als die anderen Klassen schützt CEvent nicht den Zugriff auf einen kritischen Abschnitt oder auf eine Ressource, sondern dient der Signalisierung von Ereignissen. Wartet ein Thread darauf, dass ein anderer Thread eine bestimmte Aufgabe vollendet hat, so kann zur Signalisierung ein CEvent-Objekt verwendet werden.
Tabelle 2.7: Klassen zur Synchronisation (Fortsetzung)
2.12.3 Das Beispielprogramm Mandelbrot Das Programm Mandelbrot zeigt die Programmierung mit Threads. Es befindet sich auf der Begleit-CD im KAPITEL2\MANDELBROT. Mandelbrot ist ein Programm, das die so genannte Mandelbrotmenge berechnet, ein Fraktal, das nach seinem Entdecker benannt wurde. Bekannter ist die Mandelbrotmenge als »Apfelmännchen«.
Abbildung 2.70: Das Beispielprogramm Mandelbrot
197
198 Mathematik des Apfelmännchens
2
Einstieg in die MFC-Programmierung
Mathematisch gesehen ist die Mandelbrotmenge eine Konvergenzüberprüfung einer Iterationsvorschrift für komplexe Zahlen für einen Startbereich auf der komplexen Ebene. Die Iterationsvorschrift lautet z ← z² + c, wobei sowohl z als auch c komplexe Zahlen sind. Je nach Anfangswert von c führt die Iteration ins Unendliche oder konvergiert in einen Bereich der komplexen Ebene. Für praktische Zwecke wird die Anzahl der Iterationen auf 100 begrenzt. Die Iteration läuft ins Unendliche, sobald c größer oder gleich 2 wird. Ist dies auch nach 100 Iterationen nicht der Fall, dann wird angenommen, dass die Iteration innerhalb der komplexen Ebene konvergiert. Als Startwerte werden für z 0 und für c eine Zahl auf der komplexen Ebene gewählt. Jeder Bildpunkt der Mandelbrotmenge entspricht einer Konvergenzüberprüfung. Es wird ein Ausschnitt des komplexen Zahlenbereichs genommen und als Startwerte für c verwendet. Der Wertebereich des klassischen Apfelmännchens, das auch in diesem Beispiel verwendet wird, ist -2,25 bis 0,75 für den Realteil und -1,5 bis 1,5 für den Imaginärteil von c. Jeder Bildpunkt wird entsprechend der Anzahl durchgeführter Iterationen eingefärbt. Im Beispiel wird ein konvergenter Wert schwarz, alle anderen werden entsprechend der Anzahl der durchgeführten Iterationen mit verschiedenen Grauwerten angezeigt. Neben der Programmierung von Threads werden an dem Programm Mandelbrot die Verwendung einer Ansicht mit Bildlaufleisten (CScrollView), die Programmierung mit Bitmaps, die Verwendung benutzerdefinierter Windows-Nachrichten und die Textausgabe in die Statusleiste demonstriert. Das Programm Mandelbrot verwendet zwei Threads: einen Arbeits-Thread, der die Mandelbrotmenge berechnet, und einen Benutzeroberflächen-Thread, den Haupt-Thread des Programms. Der Arbeits-Thread macht seine Ausgaben direkt in eine Bitmap. Eine Kopie dieser Bitmap wird vom Benutzeroberflächen-Thread angezeigt. Somit ist das Ergebnis schon während der Berechnung sichtbar. Das Programm bleibt während der Berechnung voll bedienbar, beispielsweise lässt sich der Info-Dialog öffnen oder die Statusleiste an- und abschalten. Das Programm wurde mit dem Anwendungs-Assistenten als SDIAnwendung erstellt. Auf eine Symbolleiste und Druckerunterstützung wurde verzichtet.
Programmierung mit Threads
199
2.12.4 Die Programmierung mit Bitmaps Die Programmierung mit Bitmaps gehört eigentlich in den Abschnitt über GDI-Programmierung. Da im Beispielprogramm StockChart allerdings kein sinnvoller Einsatz von Bitmaps möglich war, soll die Besprechung im Zuge des Beispielprogramms Mandelbrot nachgeholt werden. Windows verwendet zwei Arten von Bitmaps: geräteabhängige und geräteunabhängige. Die geräteabhängigen Bitmaps sind die ältere Variante. Leider gibt es nur für diese Bitmaps eine Unterstützung durch die MFC. Bei den geräteunabhängigen Bitmaps (DIB, Device Independent Bitmap) ist man auf die direkte Verwendung des Windows-API angewiesen. Oft werden bei der Programmierung noch die geräteabhängigen Bitmaps verwendet. Da nur diese durch die MFC unterstützt werden, sollen sie hier besprochen werden.
Typen von Bitmaps
Die MFC kapseln Bitmaps mit der Klasse CBitmap. Instanzen von CBitmap sind normale GDI-Objekte, wie es auch Pinsel oder Stifte sind. Im Gegensatz zu diesen Werkzeugen lässt sich eine Bitmap allerdings nicht einfach in einen Gerätekontext ausgeben. Eine Bitmap benötigt immer zwei Gerätekontexte, damit sie angezeigt werden kann. Eine Bitmap kann nämlich nur durch eine so genannte Blit-Operation sichtbar gemacht werden. Blit ist eine Abkürzung für Bit Block Transfer und bedeutet, dass ein rechteckiger Ausschnitt, der aus Bits besteht, die Bildschirmpunkte repräsentieren, von einem Gerätekontext in einen anderen Gerätekontext übertragen wird. Unter Windows führen die Funktionen BitBlt und StretchBlt eine solche Operation aus. Diese Operationen benötigen immer einen Quell- und einen Zielgerätekontext. Die übliche Vorgehensweise zum Anzeigen von Bitmaps unter Windows ist daher die folgende:
CBitmap
왘 Man erzeugt einen so genannten Speichergerätekontext. Ein Speichergerätekontext ist ein Gerätekontext, der kein eigenes Ausgabegerät repräsentiert. Alle Ausgaben in einen Speichergerätekontext werden in einen Speicherbereich hineingeschrieben. Obwohl ein Speichergerätekontext kein eigenes Ausgabegerät hat, kann er doch kompatibel zu einem vorgegebenen Ausgabegerät angelegt werden. Dies ist empfehlenswert, da dann bei der Blit-Operation keine zeitaufwändige Konvertierung in das Format des Ausgabegerätekontexts notwendig ist.
200
2
Einstieg in die MFC-Programmierung
왘 Die anzuzeigende Bitmap wird in den Speichergerätekontext hineinselektiert. Dazu wird die Funktion CDC::SelectObject verwendet. 왘 Um die Bitmap anzuzeigen, wird sie aus dem Speichergerätekontext in den Gerätekontext, in dem die Bitmap zu sehen sein soll, hinein- »geblittet«. Die Funktion CDC::StretchBlt erlaubt es im Gegensatz zu CDC::BitBlt, die Bitmap zu stauchen und zu strecken. Offscreen-Grafikausgabe
Die in den Speichergerätekontext hineinselektierte Bitmap ist letztendlich auch die Zeichenoberfläche dieses Kontexts. Damit lässt sich diese Vorgehensweise zur Offscreen-Technik erweitern. Man zeichnet einfach in den Speichergerätekontext hinein. Diese Zeichnung findet auf – oder besser »in« – der Bitmap statt. Ein anschließender Blit-Befehl überträgt dann diese Zeichnung in einen sichtbaren Gerätekontext. Diese Technik wird vom Programm Mandelbrot verwendet. Die Ausgabe des Arbeits-Threads erfolgt in eine Bitmap, die sich in einem Speichergerätekontext befindet. Der Benutzeroberflächen-Thread macht eine Kopie dieser Bitmap und zeigt die Kopie an. Warum man eine Kopie und nicht die Original-Bitmap verwenden muss, wird im Abschnitt 2.12.6, »Kapselung des Arbeits-Threads durch die Dokumentenklasse«, erklärt.
Animationen
Die Technik des Offscreen-Zeichnens wird gern für Animationen verwendet. Da bei heutigen Grafikkarten Blit-Operationen immer Hardware-unterstützt ablaufen, sind sie sehr schnell. Dadurch, dass die Zeichnung nicht Stück für Stück auf dem Bildschirm entsteht, sondern sozusagen schlagartig auf den Bildschirm transferiert wird, entsteht ein besserer Animationseindruck. Die Animation wird nicht kontinuierlich, sondern aus einzelnen Bildschirmen (Frames) bestehend angezeigt.
2.12.5 Der Programmcode von Mandelbrot Das Programm Mandelbrot verwendet die Dokument-AnsichtArchitektur, schließlich ist es mit dem Anwendungs-Assistenten erzeugt worden. Was jedoch ist bei diesem Programm das Dokument? Es werden ja keine Dateien verwendet. Das Dokument repräsentiert die Daten des Programms. Im Fall des Programms Mandelbrot werden die Daten, die das Programm anzeigen soll, erst zur Laufzeit berechnet. Zum Dokument gehört
Programmierung mit Threads
daher das Speichermedium, in das das Bild des Apfelmännchens ausgegeben wird. Im Beispielprogramm ist das eine Bitmap. Doch auch der Thread, der das Bild des Apfelmännchens berechnet, gehört zum Datenteil der Applikation. Der Thread repräsentiert zwar keine Daten, jedoch erzeugt er diese. Daher braucht der Thread außerhalb der Dokumentenklasse nicht bekannt zu sein. Die Dokumentenklasse sollte also die Bitmap und den Thread, der das Apfelmännchen berechnet, kapseln. Es ergibt sich eine einfachere Benutzung der Dokumentenklasse und ein deutlich besseres Design, wenn man von außerhalb sicher (thread-sicher) auf die Klasse zugreifen kann, ohne sich Gedanken über Thread-Synchronisationsprobleme machen zu müssen. Genau dies wird im Beispielprogramm erreicht. Die gesamte Synchronisation wird innerhalb der Klasse durchgeführt. Listing 2.62 zeigt die Implementierung der Dokumentenklasse CMandelbrotDoc. // MandelbrotDoc.cpp : Implementierung der Klasse CMandelbrotDoc // #include "stdafx.h" #include "Mandelbrot.h" #include "MandelbrotDoc.h" #include <math.h> #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif // Wird benötigt, um Calculate aufzurufen struct MandelbrotCall { CMandelbrotDoc *pDoc; HWND hWnd; };
cRe = rMin; cIm = iMin; for (i=0; i
Programmierung mit Threads { // Eigener Stackblock für den Pen CPen pen(PS_SOLID, 1, rgbColor); CPen *oldPen; oldPen = memDC.SelectObject (&pen); memDC.MoveTo (i,j); memDC.LineTo (i,j+1); // Aufräumen memDC.SelectObject (oldPen); } cIm += imStep; // Aufräumen memDC.SelectObject (oldBitmap); } // == Lock wirkt bis hier == cIm = iMin; cRe += reStep; // Nachricht an den View: Update ::PostMessage (hWnd, WM_LINEDONE, i, 0); } // Event setzen, sonst terminert das Programm nie! m_killEvent.SetEvent (); return 0; } CBitmap * CMandelbrotDoc::GetBitmap() { // Zum Kopieren Zugriff auf Bitmap schützen CSingleLock lock (&m_cs, TRUE); CBitmap *oldSrc, *oldDst; CDC memSrcDC, memDstDC; memSrcDC.CreateCompatibleDC (NULL); memDstDC.CreateCompatibleDC (NULL); oldSrc = memSrcDC.SelectObject (m_pBitmap); oldDst = memDstDC.SelectObject (m_pBitmapCopy); memDstDC.BitBlt (0, 0, kViewSize, kViewSize, &memSrcDC, 0, 0, SRCCOPY); memSrcDC.SelectObject (oldSrc); memDstDC.SelectObject (oldDst);
205
206
2
Einstieg in die MFC-Programmierung
return m_pBitmapCopy; }
void CMandelbrotDoc::StartCalculation(HWND hView) { static MandelbrotCall call; // !!! Fallstrick call.pDoc = this; call.hWnd = hView; // Variable für vorzeitigen Abbruch initialisieren m_bWantExit = false; m_pThread = AfxBeginThread (Calculate, &call, THREAD_PRIORITY_LOWEST); // Thread soll sich nicht selbst löschen, // das Handle wird noch für den Exit-Status gebraucht m_pThread->m_bAutoDelete = false; }
void CMandelbrotDoc::OnCloseDocument() { DWORD dwExitCode; // Wenn Berechnung noch läuft, abbrechen: m_bWantExit = true; // Auf Terminierung warten CSingleLock (&m_killEvent, true); // Nun darauf warten, dass der Thread beendet wird // (siehe Text) do { // Bitte zum Thread wechseln Sleep (1); ::GetExitCodeThread (m_pThread->m_hThread, &dwExitCode); } while (dwExitCode == STILL_ACTIVE); // AutoDelete wurde ja abgeschaltet delete m_pThread; CDocument::OnCloseDocument(); } Listing 2.62: Implementierung der Dokumentenklasse von Mandelbrot
Programmierung mit Threads
Im Konstruktor der Dokumentenklasse wird die Ausgabe-Bitmap erzeugt und mit der Farbe Schwarz initialisiert. Weiterhin wird eine zweite Bitmap erzeugt, die als Kopie der Ausgabe-Bitmap dienen kann. Die Kopie wird später zum Anzeigen der Bitmap benötigt. Gestartet werden kann der Thread zur Berechnung der Mandelbrotmenge erst dann, wenn das zum Dokument gehörende Ansichtsfenster erzeugt worden ist. Der Grund dafür ist, dass der Thread – wie weiter unten beschrieben – dem Ansichtsfenster eine Windows-Nachricht sendet, wenn er eine Spalte des Apfelmännchens berechnet hat. Dazu braucht er aber den Fenster-Handle der Ansicht. Der Thread kann nicht im Konstruktor oder in der Funktion OnNewDocument gestartet werden, da zu diesen Zeitpunkten die Ansicht noch gar nicht erzeugt worden ist. Der Thread wird daher von der Ansicht aus durch den Aufruf der Funktion StartCalculation gestartet. Zum Starten eines Threads wird in den MFC die Funktion AfxBeginThread verwendet. AfxBeginThread ist eine globale Funktion, von der es zwei überladene Versionen gibt. Wenn man der Funktion als ersten Parameter einen Zeiger auf ein Objekt einer von CWinThread abgeleiteten Klasse übergibt, dann erzeugt die Funktion einen Benutzeroberflächen-Thread. Verwendet man jedoch die zweite Überladung der Funktion und übergibt einen Funktionszeiger, dann wird ein Arbeits-Thread erstellt. Genau das wird in StartCalculation gemacht; der Funktion AfxBeginThread wird ein Zeiger auf die zuvor definierte Funktion Calculate übergeben. Die übergebene Funktion ist nach einem festen Schema zu deklarieren: Sie muss einen Rückgabewert von Typ UINT haben und als Parameter einen Zeiger auf void – LPVOID – bekommen. Die übergebene Funktion muss eine statische Member-Funktion der Klasse sein, da sie auch ohne Instanz der Klasse aufrufbar sein muss. Neben statischen Member-Funktionen können alternativ Funktionen aus dem globalen Namensraum übergeben werden. Im Beispiel wird die statische Member-Funktion Calculate zum Starten des Threads verwendet. Der zweite Parameter von AfxBeginThread ist der gleiche typlose Zeiger, den die Thread-Funktion beim Aufruf übergeben bekommt. Damit lassen sich Informationen an den neuen Thread übertragen. Mit den weiteren Parametern von AfxBeginThread, für die übrigens Vorgabewerte gesetzt sind, lassen sich verschiedene Eigenschaften des zu erzeugenden Threads ein-
207
208
2
Einstieg in die MFC-Programmierung
stellen. Dazu gehören beispielsweise die Priorität, die Größe des Stacks, der Ausführungszustand (angehalten oder nicht) sowie zusätzliche Sicherheitsattribute des Threads. Im Beispielprogramm wird der Thread auf eine niedrige Priorität gesetzt, damit er nur dann läuft, wenn der Benutzeroberflächen-Thread nichts zu tun hat. Neben der statischen Member-Funktion Calculate besitzt die Klasse CMandelbrotDoc eine zweite, nichtstatische Überladung dieser Funktion. Die statische Funktion ruft die nichtstatische Version auf, um die Ausführung in der Instanz von CMandelbrotDoc weiterzuführen. Damit die statische Version von Calculate allerdings die nichtstatische Version von Calculate aufrufen kann, braucht sie einen Zeiger auf die Instanz des Dokuments. Woher bekommt Calculate diesen Zeiger? Hier bietet sich der an die Thread-Funktion übergebene Zeiger vom Typ void * an. Man könnte dort einen Zeiger auf die Dokumentenklasse übergeben. Dieser Ansatz wird – in erweiterter Form – vom Beispielprogramm verfolgt. Da dem Thread zusätzlich noch der Handle der Ansicht übergeben werden soll, wird eine eigene Struktur definiert, eine Variable davon erzeugt und ein Zeiger auf diese Variable übergeben. Die Struktur heißt MandelbrotCall und wird am Anfang der Datei MANDELBROTDOC.CPP deklariert. Sie enthält einen Zeiger auf ein Objekt der Klasse CMandelbrotDoc und ein Windows-Fenster-Handle. In StartCalculation wird die Variable call des Typs MandelbrotCall erzeugt und mit den passenden Werten gefüllt. Dann wird ein Zeiger an AfxBeginThread übergeben. Die Variable ist als static deklariert, eine absolute Notwendigkeit an dieser Stelle. Hier verbirgt sich nämlich ein klassischer Fallstrick der Programmierung mit mehreren Threads. Es wird ein Zeiger auf Daten an einen anderen Thread weitergegeben. Alle Threads haben zwar denselben globalen Speicherbereich, allerdings hat jeder Thread seinen eigenen Stack. Wäre die Variable call nicht als static deklariert, dann würde sie auf dem Stack des Threads angelegt. AfxBeginThread würde den Zeiger auf Stack-Daten an den zweiten Thread weitergeben. Wenn dieser dann auf diese Daten zugreift, kann es ein böses Erwachen geben. Der entsprechende Stackframe ist unter Umständen gar nicht mehr definiert und es wird auf ungültige Daten zugegriffen.
Programmierung mit Threads
209
Die Funktion CMandelbrotDoc::Calculate berechnet das Apfelmännchen und gibt es in eine Bitmap aus. In zwei for-Schleifen wird über den betrachteten Bereich der komplexen Ebene iteriert, die while-Schleife stellt die Konvergenz und damit die Farbe des betrachteten Bildpunkts fest. Zu Beginn der inneren for-Schleife signalisiert der Thread durch den Aufruf der Funktion Sleep mit dem Wert 0 dem WindowsScheduler (der Scheduler ist der Teil eines Betriebssystems, der bestimmt, welcher Prozess oder Thread den Prozessor zugeteilt bekommt), dass er bereit ist, den Prozessor an andere Threads abzugeben. Eigentlich sollte dies nicht notwendig sein, doch zeigt es sich, dass das Programm in bestimmten Konstellationen sehr zäh reagiert, wenn diese Anweisung fehlt. Obwohl der ArbeitsThread eine niedrigere Priorität als der BenutzeroberflächenThread besitzt, scheint der Arbeits-Thread den Benutzeroberflächen-Thread unter bestimmten Umständen nicht oft genug zum Zuge kommen zu lassen, so dass die Benutzeroberfläche des Programms sehr langsam auf Änderungen reagiert. Beim Autor trat das Problem nur unter Windows NT und nur in der Release-Version auf. Der Aufruf von Sleep mit dem Parameter 0 ist ein Trick, mit dem man dem Betriebssystem vorschlägt, jetzt zu einem anderen Thread zu wechseln. Ruft man Sleep mit einem Wert größer als 0 auf, so legt sich der Thread für die angegebene Anzahl von Millisekunden »schlafen«, das heißt, er wird während der angegebenen Zeitspanne nicht ausgeführt. Ein Thread, der Sleep mit einem Wert größer als 0 aufruft, wird in jeden Fall von der Ausführung suspendiert.
Sleep
Nach dem Aufruf von Sleep beginnt der kritische Bereich für den Zugriff auf die Bitmap, in die das Ergebnis ausgegeben werden soll. Die Ausgabe-Bitmap m_pBitmap wird durch ein Objekt der Klasse CCriticalSection geschützt. Sowohl der Zeiger auf die Ausgabe-Bitmap als auch das Objekt m_cs der Klasse CCriticalSection sind als private Member der Dokumentenklasse definiert. Alle Synchronisationsaspekte sollen innerhalb der Klasse geregelt werden, daher ist ein Zugriff auf das Synchronisationsobjekt von außen verboten.
CCriticalSection
Um in den kritischen Bereich einzutreten, wird nicht das CCriticalSection-Objekt direkt verwendet, sondern es wird ein Hilfsobjekt der Klasse CSingleLock auf dem Stack erzeugt. CSingleLock macht nichts anderes, als die Funktion Lock des Synchronisations-
CSingleLock
210
2
Einstieg in die MFC-Programmierung
objekts aufzurufen. Lock versucht, den Zugriff auf den kritischen Abschnitt zu erlangen. Ist der kritische Abschnitt von keinem anderen Thread belegt, dann läuft Lock einfach weiter. Befindet sich jedoch bereits ein anderer Thread im kritischen Abschnitt, so sind verschiedene Verfahrensweisen möglich: Entweder wartet der Thread so lange, bis der Zugang zum kritischen Abschnitt frei wird, oder man übergibt der Funktion Lock eine maximale Wartezeit. Wird die Wartezeit überschritten, dann kehrt Lock mit dem Rückgabewert FALSE zurück. Dieser muss natürlich abgefragt werden, damit man im Fehlerfall nicht in den kritischen Abschnitt »hineinläuft«. Wenn man keine Wartezeit angibt, dann ist das vorgegebene Verhalten der Funktion Lock, unendlich lange zu warten. Im Beispielprogramm wird unendlich lange gewartet, daher muss kein Fehlerwert abgefragt werden. Da dem Konstruktor von CSingleLock für den Parameter bInitialLock der Wert TRUE übergeben wird, ruft bereits der Konstruktor von CSingleLock die Lock-Funktion des Synchronisationsobjekts auf. Der Destruktor von CSingleLock ruft entsprechend die UnlockFunktion des Synchronisationsobjekts auf. Diese Funktion gibt den Zugriff auf ein Synchronisationsobjekt wieder frei. Warum verwendet man CSingleLock, anstatt direkt die Funktionen CCriticalSection::Lock und CCriticalSection::Unlock aufzurufen? Es spricht scheinbar nichts dagegen, CSingleLock nicht zu verwenden und Lock und Unlock direkt aufzurufen. CSingleLock macht die Verwendung von Synchronisationsobjekten (CSingleLock arbeitet auch mit allen anderen Synchronisationsklassen zusammen) allerdings sicher, wenn mit Ausnahmen gerechnet werden muss. Dadurch, dass man das CSingleLock-Objekt auf dem Stack anlegt, wird Unlock automatisch im Destruktor aufgerufen, wenn der Stackframe zerstört wird, in dem sich das Objekt befindet. Dies ist die einzige Möglichkeit, Deadlocks zu vermeiden, wenn Unlock aufgrund einer Ausnahme nicht selbst aufgerufen werden kann! Dies ist eine elegante C++-Technik, die man auch auf eigene Klassen übertragen kann. Neben CSingleLock gibt es die Klasse CMultiLock, mit der der Zugriff auf mehrere Synchronisationsobjekte gesteuert werden kann.
Programmierung mit Threads
Eine elegante C++-Technik ist es, Start/Ende-Sequenzen mit Hilfe von Konstruktoren und Destruktoren zu kapseln. Bei einer solchen Start/ Ende-Sequenz handelt es sich immer um einen Vorgang, bei dem zuerst eine Ressource oder ein Objekt geöffnet oder angefordert werden muss. Danach findet eine Verarbeitung statt und zum Schluss werden die Ressource oder das Objekt geschlossen beziehungsweise freigegeben. Ein typisches Beispiel dafür ist die Arbeit mit Dateien. Sie müssen vor dem Zugriff geöffnet und danach geschlossen werden. Mit Hilfe von auf dem Stack von Funktionen angelegten C++-Objekten lassen sich solche Start/Ende-Sequenzen sehr schön automatisieren. Der Start-Teil wird im Konstruktor ausgeführt, der Ende-Teil im Destruktor. Das folgende Listing zeigt ein Beispiel für einen automatischen Flaschenöffner: // Deklaration der Klasse class CBottleOpener { public: CBottleOpener (CBottle *bottle); ˜CBottleOpener (); private: CBottle *m_pBottle; }; // Definition des Konstruktors CBottleOpener::CBottleOpener (CBottle *bottle) { m_pBottle = bottle; m_pBottle->Open (); } // Definition des Destruktors CBottleOpener::˜CBottleOpener () { m_pBottle->Close(); } // Anwendung der Klasse void HaveADrink () { CBottle bottle; CBottleOpener (&bottle); // Flasche wird geöffnet bottle.PourIntoGlass(); Drink(); // Flasche wird geschlossen }
211
212
2
Einstieg in die MFC-Programmierung
Um das Öffnen und Schließen der Flasche braucht man sich nicht zu kümmern, sofern man den vollautomatischen Flaschenöffner CBottle Opener verwendet. Der Flaschenöffner funktioniert folgendermaßen: Sobald das CBottleOpener-Objekt beim Funktionsaufruf von HaveADrink erzeugt wird, öffnet der Konstruktor die ihm übergebene Flasche. Beim Verlassen der Funktion HaveADrink wird vom Destruktor die Flasche wieder geschlossen. Innerhalb von HaveADrink ist die Flasche also offen, es kann aus ihr getrunken werden! CSingleLock und CMultiLock sind zwei MFC-Klassen, die genau diese Technik – bezogen auf Synchronisationsklassen – verwenden. Vergleicht man diese Technik mit den von Gamma et al. in ihrem Buch »Entwurfsmuster« (siehe Anhang) aufgeführten Mustern, dann lässt sich diese Technik als Proxy einordnen. Der Zugriff auf ein Objekt erfolgt mit Hilfe eines vorgelagerten Stellvertreterobjekts. Im vorgestellten Beispiel nutzt das vorgelagerte Objekt den Sichtbarkeitsbereich (Scope) von funktionslokalen Variablen aus, um daraus eine automatische Funktionalität in Bezug auf das kontrollierte Objekt abzuleiten. Nachdem das CSingleLock-Objekt auf dem Stack angelegt worden ist, wird die Ausgabe-Bitmap m_pBitmap in den bereits am Anfang der Funktion angelegten Speichergerätekontext memDC hineinselektiert. Damit kann m_pBitmap als Ausgabemedium verwendet werden. Farbschema
Die anschließende while-Schleife bestimmt die Konvergenz des betrachteten Bildpunkts. Der Iterationszähler nCnt ist für die Farbe des betrachteten Bildpunkts maßgebend. Erreicht nCnt den Wert 100, so gilt der betrachtete Bildpunkt als nicht konvergent und wird schwarz dargestellt, da nColor 0 wird. Werte kleiner als 100 ergeben Grauwerte. Das Makro RGB erzeugt aus den drei übergebenen Parametern einen Farbwert. Da für alle drei Werte (rot, grün und blau) die gleiche Variable (nColor) übergeben wird, können nur Grauwerte entstehen. Mit ein wenig Aufwand lässt sich an dieser Stelle auch ein anderes Farbschema implementieren.
Zeichnen des Apfelmännchens
Im folgenden Block wird ein Stift mit der berechneten Farbe erzeugt, in den Speichergerätekontext selektiert und ein Bildpunkt mittels der Funktionen MoveTo und LineTo in den Speichergerätekontext und damit in die darin befindliche Bitmap ausgegeben. Da das GDI keine Funktion zur Ausgabe von Bildpunkten bereitstellt, muss diese Funktionalität durch Ausgabe von Linien der Länge 1 nachgebildet werden. Eine der beiden an LineTo übergebe-
Programmierung mit Threads
213
nen Koordinaten muss sich tatsächlich um 1 von den an MoveTo übergebenen Koordinaten unterscheiden, andernfalls wird kein Bildpunkt ausgegeben. Nachdem klar ist, was der Programmcodeblock macht, ist noch nicht deutlich geworden, warum hier ein eigener Programmcodeblock verwendet wird. Man kann in C++ einen Codeblock innerhalb zweier geschweifter Klammern dazu verwenden, einen eigenen Stackframe zu definieren. Variablen, die lokal innerhalb des Codeblocks deklariert werden, werden am Ende des Codeblocks wieder vom Stack entfernt. Diesen Umstand macht sich das Beispielprogramm zunutze, um die von CPen verwendete StiftRessource möglichst kurzzeitig zu belegen. Diese Programmiertechnik kann sinnvoll sein, wenn man die Ressourcenverwendung möglichst effizient gestalten möchte. Im Beispielprogramm ist sie nicht wirklich notwendig. Natürlich haben auch die anderen Codeblöcke innerhalb der Funktion Calculate ihre eigenen Stackframes. Dies ist auch der Grund, warum der kritische Abschnitt nur bis zur im Quelltext gekennzeichneten Stelle reicht. Die Variable lock ist Teil des Stackframes der inneren for-Schleife. Daher wird das Objekt am Ende der for-Schleife zerstört und damit implizit die Funktion Unlock für den kritischen Abschnitt aufgerufen. Nach der inneren for-Schleife, aber noch innerhalb der äußeren, wird schließlich die Windows-API-Funktion PostMessage aufgerufen. Mit PostMessage wird eine Windows-Nachricht an ein Fenster gesendet. Die Verwendung von PostMessage ermöglicht es dem Arbeits-Thread, den Benutzeroberflächen-Thread zu benachrichtigen und ihm Informationen zukommen zu lassen. Der ArbeitsThread darf keine Funktionen der Benutzeroberfläche aufrufen. Dies würde zu einem Absturz des Programms führen. PostMessage bietet hier einen Ausweg. Die Funktion SendMessage ist hier übrigens nicht geeignet, da diese synchron arbeitet, also wartet, bis die Nachricht verarbeitet wurde.
PostMessage
Um Windows-Nachrichten für programminterne Zwecke nutzen zu können, kann man benutzerdefinierte Windows-Nachrichten verwenden. Windows-Nachrichten sind letztendlich nichts anderes als Integer-Zahlen. Für benutzerdefinierte Nachrichten sind eigene Zahlenbereiche vorgesehen. Private Windows-Nachrichten lassen sich beginnend mit dem Wert der Konstanten WM_USER verwenden: WM_USER+1, WM_USER+2 usw.
Benutzerdefinierte WindowsNachrichten
214
2
Einstieg in die MFC-Programmierung
Es ist davon abzuraten, Konstanten aufbauend auf WM_USER zu benutzen, da die MFC und auch einige Standardsteuerelemente Nachrichten in diesem Bereich definieren. Sicherer ist es, stattdessen die neuere Konstante WM_APP zu verwenden. Das Beispielprogramm Mandelbrot verwendet die Nachricht WM_APP+1, um anzuzeigen, dass eine weitere Spalte des Apfelmännchens berechnet wurde und angezeigt werden kann. Die entsprechende Nachricht ist in MANDELBROT.H definiert (Listing 2.63). // selbst definierte Windows-Nachricht #define WM_LINEDONE WM_APP+1 Listing 2.63: Selbst definierte Windows-Nachricht in Mandelbrot.h
Damit PostMessage die Nachricht versenden kann, braucht es den Handle des Fensters, an das die Nachricht geschickt werden soll. Dieser Handle ist als Teil der Hilfsstruktur MandelbrotCall an die statische Member-Funktion Calculate und von dieser an CMandelbrotDoc::Calculate übergeben worden. Die Funktion PostMessage bietet die Möglichkeit, der Nachricht zwei Integer-Parameter der Typen WPARAM und LPARAM mitzugeben. WPARAM ist als UINT und LPARAM als LONG definiert. Im Beispielprogramm wird WPARAM verwendet, um der Nachricht den aktuellen Spaltenzähler mitzugeben. In der Ansichtsklasse muss eine Nachrichtenbehandlungsfunktion für WM_LINEDONE definiert werden, damit die Nachricht dort verarbeitet werden kann.
2.12.6 Kapselung des Arbeits-Threads durch die Dokumentenklasse Wie bereits erwähnt, ist es ein Zeichen guten Designs, wenn der Arbeits-Thread außerhalb der Klasse, in der er verwendet wird, nicht in Erscheinung tritt. Die Klasse soll thread-sicher verwendbar sein. Im Beispielprogramm Mandelbrot soll die AusgabeBitmap des Apfelmännchens einerseits von dem Arbeits-Thread beschrieben werden, andererseits vom BenutzeroberflächenThread angezeigt werden. Da sich eine Bitmap nicht sicher in zwei Gerätekontexten gleichzeitig befinden kann, auf die von zwei Threads aus zugegriffen wird, ist der Zugriff auf die Bitmap kritisch und muss geschützt werden. Um Ausgaben in die Bitmap vom Arbeits-Thread aus schreiben zu können, muss die Bitmap aus der Dokumentenklasse heraus angesprochen werden. Um die Bitmap anzuzeigen, muss sie aus der Ansichtsklasse heraus ange-
Programmierung mit Threads
sprochen werden. Damit erstreckt sich der Bereich kritischer Abschnitte über mehr als eine Klasse, und das Designziel, die Synchronisation nur auf eine Klasse zu beschränken, ist nicht erreicht. Was kann man tun? Als Abhilfe bietet sich an, eine Kopie der Bitmap zu erstellen und diese zur Anzeige zu verwenden. Die Kopie kann innerhalb der Dokumentenklasse erstellt werden, damit wandert auch der kritische Abschnitt – das Kopieren der Bitmap – in die Dokumentenklasse. Alle kritischen Abschnitte befinden sich innerhalb der Dokumentenklasse, damit ist das Designziel erreicht. Genau dieser Ansatz wird vom Programm Mandelbrot verfolgt. Die Member-Variable m_pBitmapCopy ist ein Zeiger auf diese Kopie. Erkauft wird das bessere Design mit einem geringfügig schlechteren Laufzeitverhalten. Das Kopieren der Bitmap vor dem Zugriff kostet schließlich Zeit. Kopiert wird die Bitmap innerhalb der Funktion CMandelbrotDoc::GetBitmap. Diese Funktion wird von der Ansicht aufgerufen, wenn sie Zugriff auf die Bitmap benötigt. Genau wie in der Berechnungsfunktion wird der kritische Abschnitt in GetBitmap durch ein Objekt der Klasse CSingleLock, das die private Member-Variable m_cs zur Synchronisation verwendet, geschützt. Kopiert wird durch eine einfache Blit-Operation zwischen zwei Speichergerätekontexten.
2.12.7 Vorzeitiges Beenden des Arbeits-Threads Ein etwas problematisches Thema ist das vorzeitige Beenden des Arbeits-Threads. Beendet der Benutzer das Programm, bevor der Arbeits-Thread seine Arbeit getan hat, dann muss natürlich auch der Arbeits-Thread beendet werden. Leider gibt es keine einfache Möglichkeit, den Thread ohne weitere Folgen zu beenden. Man kann das Problem natürlich einfach ignorieren. Wenn das Programm beendet wird, dann wird auch der Prozess und damit alle seine Threads beendet. Meistens geht dabei alles gut. Allerdings nur meistens. Manchmal kann es bei dieser unsanften Methode, den Thread zu terminieren, zu Abstürzen des Programms kommen. Eine andere Möglichkeit, den Thread zu terminieren, ist die Windows-API-Funktion TerminateThread. Allerdings kann diese Funktion unschöne Nebenwirkungen auf andere Prozesse haben. Microsoft bezeichnet die Funktion als gefährlich und rät, sie nur in Extremfällen zu verwenden, also wenn wirklich nichts anderes
215
216
2
Einstieg in die MFC-Programmierung
mehr geht. Daher bleibt im Normalfall nur die dritte Möglichkeit: Der Arbeits-Thread muss sich selbst terminieren. Damit er dies tut, muss es ihm gesagt werden. Autodelete
Im Beispielprogramm Mandelbrot wurden einige Vorkehrungen getroffen, damit der Arbeits-Thread sich angemessen beenden kann. Unmittelbar nachdem der Thread durch Aufruf der Funktion AfxBeginThread erzeugt worden ist, wird beim Thread-Objekt die Autodelete-Funktionalität abgeschaltet. Normalerweise löscht ein MFC-Thread sein CWinThread-Objekt, wenn er sich beendet. Das ist hier nicht gewünscht, da ohne das CWinThread-Objekt der Status des Threads nicht mehr in Erfahrung gebracht werden kann.
CEvent
Die Anforderung, dass der Thread sich beenden möge, wird über die Member-Variable m_bWantExit mitgeteilt. Der Arbeits-Thread fragt diese Variable regelmäßig während seiner Berechnungen ab. Wird sie auf true gesetzt, dann beendet sich der Arbeits-Thread. Da die Variable m_bWantExit vom Arbeits-Thread nur gelesen wird, muss sie nicht durch ein Synchronisationshilfsmittel geschützt werden. Der Arbeits-Thread signalisiert dem HauptThread über das Objekt m_killEvent der Klasse CEvent, dass er die Berechnung abgebrochen hat. Die Signalisierung erfolgt durch den Aufruf der Funktion SetEvent der Klasse CEvent. Danach beendet sich die Berechnungsfunktion, indem sie einfach return aufruft. Der Auslöser für diesen Beendigungsmechanismus liegt in der Funktion CMandelbrotDoc::OnCloseDocument. Sie wird aufgerufen, wenn das Dokument geschlossen wird. Dies ist genau der richtige Zeitpunkt, um den Arbeits-Thread zu beenden. Als Erstes setzt OnCloseDocument die Variable m_bWantExit auf den Wert true, um dem Arbeits-Thread anzuzeigen, dass die Berechnung abgebrochen werden soll. Dann wartet er auf das Eintreffen des Ereignisses m_killEvent. Genau wie bei kritischen Abschnitten kann dazu ein Objekt der Klasse CSingleLock verwendet werden. Der zweite Parameter des Konstruktors gibt an, dass auf das als ersten Parameter übergebene Ereignis gewartet werden soll. Der Thread läuft an dieser Stelle weiter, wenn das Ereignis gesetzt worden ist. Eigentlich sollte dann das Programm beendet werden können. Allerdings ist nicht sichergestellt, dass der Arbeits-Thread zu diesem Zeitpunkt wirklich schon beendet worden ist. Signalisiert der Arbeits-Thread das Ereignis durch den Aufruf von SetEvent, dann
Programmierung mit Threads
kann danach ein Kontextwechsel stattfinden und der HauptThread weiterlaufen. Der Arbeits-Thread ist dann also noch nicht wirklich beendet! Daher muss darauf gewartet werden, dass sich der Arbeits-Thread tatsächlich beendet, bevor das Programm geschlossen wird. Dazu wird die Windows-API-Funktion GetExitCodeThread aufgerufen. Wenn diese STILL_ACTIVE als Wert für den Exitcode des Threads liefert, dann ist der betreffende Thread noch aktiv. Daher wird in einer Schleife darauf gewartet, dass GetExitCodeThread nicht mehr den Wert STILL_ACTIVE liefert. Der Aufruf von Sleep innerhalb der Schleife ist ein Hinweis an den Scheduler, dass er den ArbeitsThread zum Zuge kommen lassen soll. Nachdem der Thread dann tatsächlich beendet worden ist, wird noch das Thread-Objekt gelöscht. Schließlich ist die automatische Löschung nach dem Erzeugen des Threads abgeschaltet worden. Zum Abschluss wird die Funktion der Basisklasse aufgerufen. Die hier vorgestellte Vorgehensweise zum Beenden des Arbeits-Threads verdeutlicht zwar die Verwendung der MFC-Klasse CEvent, allerdings kommt man durch Verzicht auf die Hilfsmittel der MFC in diesem Fall schneller und eleganter zum Ziel. Statt das Beenden des Threads durch ein Ereignis zu signalisieren, das nicht einmal den genauen Zeitpunkt der Beendigung angeben kann, lässt sich auch direkt auf das Ende des Threads warten. Dazu übergibt man der WIN32-Funktion WaitForSingleObjekt einfach den Handle des Arbeits-Threads. Die Funktion wartet dann darauf, dass sich der Thread beendet. Im Beispielprogramm ist dazu folgende Programmzeile zu verwenden: ::WaitForSingleObject (m_pThread->m_hThread, INFINITE);
Die Signalisierung durch das Ereignis und das Abfragen des Exitcodes können dann entfallen. Nicht immer bieten die MFC also die eleganteste Lösung! Damit ist die Dokumentenklasse des Programms Mandelbrot beschrieben. Die Verwendung des Dokuments aus der Ansicht gestaltet sich einfach, da CMandelbrotDoc die gesamte Komplexität der Programmierung mit mehreren Threads kapselt. Listing 2.64 zeigt den Quelltext der Ansichtsklasse.
//////////////////////////////////////////////////////////// // CMandelbrotView Konstruktion/Destruktion CMandelbrotView::CMandelbrotView() { // ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen, } CMandelbrotView::~CMandelbrotView() { } BOOL CMandelbrotView::PreCreateWindow(CREATESTRUCT& cs) { // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das // Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren. return CScrollView::PreCreateWindow(cs); }
Im Gegensatz zur Ansichtsklasse des Programms StockChart ist CMandelbrotView von der Basisklasse CScrollView abgeleitet worden. Die Ansichtsklasse lässt sich im MFC-Anwendungs-Assistenten unter ERSTELLTE KLASSEN auswählen. CScrollView ergänzt die Ansichtsklasse CView um die Fähigkeit, Bildlaufleisten zu verwalten. Die Verwaltung der Bildlaufleisten erfolgt weitgehend automatisch. Beim Erzeugen der Ansicht muss dieser lediglich die Größe ihrer Ausgabefläche mitgeteilt werden. Die Ausgabefläche ist der Bereich, in den die Ansicht ihre Ausgaben schreiben kann. Ist die Ausgabefläche größer als das Ansichtsfenster, dann werden von der Ansicht automatisch Bildlaufleisten zur Verfügung gestellt, mit denen über den gesamten Ausgabebereich gescrollt werden kann. Die Größe der Ausgabefläche wird durch den Aufruf der Funktion SetScrollSizes eingestellt. SetScrollSizes kann jederzeit aufgerufen werden, solange die Ansicht besteht. Die Änderung der Größe der Ausgabefläche zur Laufzeit ist also möglich. Im Beispielprogramm Mandelbrot wird die Größe der Ausgabefläche allerdings zu Beginn festgelegt und dann nicht mehr verändert. Dazu wird SetScrollSizes in der Funktion CMandelbrotView::OnInitialUpdate aufgerufen. Der Funktion wird ein Objekt der Klasse CSize übergeben, das die Ausgabefläche auf kViewSize mal kViewSize Punkte festlegt. kViewSize ist eine Konstante, die in MANDELBROT.H definiert wird und einen Wert von 500 hat. Zusätzlich muss der Funktion SetScrollSizes der Abbildungsmodus der Ansicht übergeben werden. Im Beispielprogramm wird der Abbildungsmodus MM_TEXT verwendet. Ein Punkt im Ausgabekoordinatensystem entspricht also genau einem Bildpunkt auf dem Bildschirm. Die Abbildungsmodi MM_ISOTROPIC und MM_ANISOTROPIC lassen sich nicht zusammen mit SetScrollSizes verwenden. Mehr ist nicht notwendig, um eine Ansicht mit Bildlaufleisten auszustatten. Die Verwaltung der Bildlaufleisten wird von der Ansichtsklasse übernommen.
221 CScrollView
Nach dem Aufruf der Funktion SetScrollSizes in OnInitialUpdate wird die bereits besprochene Funktion StartCalculation des Dokuments aufgerufen. StartCalculation wird der Handle der Ansicht, m_hWnd, übergeben, der dann später vom Arbeits-Thread verwendet wird, um Nachrichten an die Ansichtsklasse zu senden. Um die Nachrichten des Arbeits-Threads zu behandeln, muss in der Ansichtsklasse eine Nachrichtenbehandlungsfunktion definiert sein. Weil es sich bei den Nachrichten, die der Arbeits-Thread sendet, um benutzerdefinierte Nachrichten handelt, lässt sich aus
Behandlung benutzerdefinierter Nachrichten
222
2
Einstieg in die MFC-Programmierung
der Eigenschaftsansicht keine Nachrichtenbehandlungsfunktion erstellen, da die selbst definierte Nachricht dort nicht aufgelistet wird. Daher muss die Nachrichtenbehandlungsfunktion selbst in die Nachrichtenzuordnungstabelle eingetragen werden. WPARAM, LPARAM
Die Nachricht WM_LINEDONE soll durch die Funktion OnLineDone behandelt werden. Dazu ist ein Eintrag in die Nachrichtenzuordnungstabelle der Ansichtsklasse vorzunehmen. Benutzerdefinierte Nachrichten werden mit Hilfe des Makros ON_MESSAGE eingetragen, wie in Listing 2.64 zu sehen ist. Die Nachrichtenbehandlungsfunktion bekommt die beiden Parameter WPARAM und LPARAM übergeben und muss einen Wert des Typs LRESULT zurückgeben. Im Falle von OnLineDone ist WPARAM der aktuelle Spaltenzähler des Arbeits-Threads. OnLineDone macht zwei Dinge: Die Funktion schreibt die gegenwärtig berechnete Spalte in die Statusleiste und fordert die Ansicht auf, sich neu zu zeichnen. Da die Statusleiste eine geschützte Member-Variable (protected) des Hauptrahmenfensters ist, kann die Ansichtsklasse nicht direkt darauf zugreifen. Das Anzeigen der derzeit berechneten Spalte muss also an die Klasse des Hauptrahmenfensters delegiert werden. Ein Zeiger auf das Hauptrahmenfenster lässt sich beim Applikationsobjekt besorgen. Bei der Klasse des Hauptrahmenfensters wird dann die eigens zu diesem Zweck definierte Funktion ShowLine aufgerufen, der der Spaltenzähler übergeben wird.
RedrawWindow
Um das Apfelmännchen neu zu zeichnen, wird die Funktion RedrawWindow aufgerufen. Der neu zu zeichnende Bereich wird auf die Größe der Bitmap begrenzt, indem der Funktion RedrawWindow dieser Bereich in Form eines CRect-Objekts übergeben wird. Man braucht der Funktion RedrawWindow keine Parameter zu übergeben; als Vorgabe wird das ganze Fenster neu gezeichnet, was im Beispielprogramm aber nicht notwendig ist.
WM_ERASEBKGND
Das Zeichnen der Ansicht ist im Beispielprogramm auf zwei Funktionen verteilt worden: Die Standardzeichenfunktion OnDraw zeichnet das Apfelmännchen und OnEraseBkgnd zeichnet den Hintergrund. OnEraseBkgnd ist eine Nachrichtenbehandlungsfunktion für die Windows-Nachricht WM_ERASEBKGND. Diese Nachricht weist ein Fenster an, seinen Hintergrund neu zu zeichnen. Die MFC stellen eine Standardfunktion für diese Nachricht zur Verfügung, die normalerweise nicht überschrieben werden muss. Im Beispielprogramm Mandelbrot wird diese Funktion
Programmierung mit Threads
selbst implementiert, um ein Flackern der Anzeige zu vermeiden. Jedes Mal wenn die Bitmap neu gezeichnet werden soll, wird nämlich zunächst der Hintergrund gelöscht. Da das Ansichtsfenster normalerweise einen hellen Hintergrund hat und die Neuzeichnungen recht schnell hintereinander ausgeführt werden, führt das zu einem unschönen Flackereffekt. CMandelbrotView::OnEraseBkgnd verzichtet darauf, den Bereich hinter der Bitmap neu zu zeichnen, da die Bitmap diesen Bereich sowieso vollständig bedeckt. Allerdings zeichnet OnEraseBkgnd einen schwarzen Rahmen um die Bitmap, der sichtbar wird, wenn das Ansichtsfenster größer als die Bitmap ist. Dieser Bereich wird bei durch den Arbeits-Thread ausgelösten Updates nicht neu gezeichnet, da diese auf den Bereich der Bitmap beschränkt sind (wie bereits beschrieben, bekommt RedrawWindow in OnUpdateLine die Koordinaten der Bitmap als neu zu zeichnende Fläche übergeben). Die schwarzen Flächen werden mit der Funktion CDC::PatBlt gezeichnet. Mit PatBlt lassen sich auf einfache Weise Flächen mit einer Farbe oder einem Muster belegen. Die tatsächliche Ausdehnung des Ansichtsfensters wird zuvor durch Aufruf der Funktion GetClientRect in Erfahrung gebracht. Zum Verständnis der Funktion OnEraseBkgnd ist es wichtig zu wissen, dass die gleichnamige Funktion der Basisklasse ausdrücklich nicht aufgerufen wird. Der Assistent, mit dem die Funktion eingefügt wurde, hat nämlich den Aufruf der Basisklassenfunktion bereits eingebaut, als er OnEraseBkgnd angelegt hat. Dieser Aufruf ist explizit gelöscht worden. In der Funktion der Basisklasse wird normalerweise der Hintergrund des Fensters gezeichnet. Ruft man diese Funktion auf, so tritt das Flackern wieder auf. Es wurde bereits erwähnt, dass die Ausgaben in die Statusleiste von der Klasse des Hauptrahmenfensters aus erfolgen müssen, da der Zeiger auf die Statusleiste als geschützte Member-Variable in dieser Klasse deklariert ist. Daher befindet sich der Programmcode zur Ausgabe der aktuell berechneten Spalte in der Klasse des Hauptrahmenfensters, CMainFrame. Listing 2.65 zeigt die Implementierungsdatei von CMainFrame. // MainFrm.cpp : Implementierung der Klasse CMainFrame // #include "stdafx.h" #include "Mandelbrot.h" #include "MainFrm.h"
223
224
2
Einstieg in die MFC-Programmierung
#ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// / // CMainFrame IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() END_MESSAGE_MAP() static UINT indicators[] = { ID_SEPARATOR, // Statusleistenanzeige ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, }; ////////////////////////////////////////////////////////////// // CMainFrame Nachrichten-Handler CMainFrame::CMainFrame() { // ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen } CMainFrame::~CMainFrame() { } int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // Fehler beim Erzeugen } return 0; }
Programmierung mit Threads
225
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das // Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren. return CFrameWnd::PreCreateWindow(cs); } ////////////////////////////////////////////////////////////// / // CMainFrame Konstruktion/Destruktion #ifdef _DEBUG void CMainFrame::AssertValid() const { CFrameWnd::AssertValid(); } void CMainFrame::Dump(CDumpContext& dc) const { CFrameWnd::Dump(dc); } #endif //_DEBUG ////////////////////////////////////////////////////////////// / // CMainFrame Diagnose void CMainFrame::ShowLine(int nLine) { CString str; str.Format ("Spalte %i von %i", nLine+1, kViewSize); m_wndStatusBar.SetPaneText (0, str); } Listing 2.65: Implementierung der Hauptfensterrahmenklasse
Die derzeit berechnete Spalte wird durch den Aufruf von CMainFrame::ShowLine angezeigt. Auf die Statusleiste kann über die Member-Variable m_wndStatusBar zugegriffen werden. m_wndStatusBar ist eine Instanz der Klasse CStatusBar. Mit der Funktion CStatusBar::SetPaneText wird der Text in der Statusleiste geändert. Der als erster Parameter übergebene Index gibt an, für welches Feld in der Statusleiste der Text gesetzt werden soll. In einer vom AnwendungsAssistenten erzeugten Anwendung ist es das Feld mit dem Index 0, in das Statusausgaben geschrieben werden sollten. Dies ist das linke, große Feld in der Statusleiste. Daneben gibt es drei weitere Felder, die
Ausgabe in die Statusleiste
226
2
Einstieg in die MFC-Programmierung
Indikatoren für den Status der Tasten (Umschalten), (Rollen) und (Num) anzeigen. Natürlich lässt sich die Statusleiste auch um eigene Felder erweitern. Der vom Anwendungs-Assistenten generierte Programmcode zur Erzeugung der Statusleiste befindet sich in der Funktion CMainFrame::OnCreate.
2.12.8 Tipps zur Vorgehensweise 왘 Threads sollten verwendet werden, um eine Berechnung auf mehrere CPUs zu verteilen oder um ein Programm Berechnungen im Hintergrund durchführen zu lassen, ohne die Benutzeroberfläche des Programms zu blockieren. 왘 Threads müssen den Zugriff auf kritische Abschnitte und gemeinsam genutzte Ressourcen synchronisieren. Das effizienteste und einfachste Werkzeug der MFC dazu ist die Klasse CCriticalSection. Objekte der Klasse CCriticalSection können allerdings nur den globalen gegenseitigen Ausschluss auf einen kritischen Abschnitt innerhalb eines Prozesses sicherstellen. Mit den Klassen CMutex und CSemaphore stehen Synchronisationswerkzeuge bereit, die auch über Prozessgrenzen hinaus funktionieren und flexibler eingesetzt werden können. 왘 Mit Objekten der Klasse CEvent wird das Eintreffen von Ereignissen signalisiert oder auf das Eintreffen von Ereignissen gewartet. 왘 Der Wirkungsbereich eines Threads sollte möglichst lokal bleiben. Dazu bietet sich die Kapselung in einer C++-Klasse an. Der Zugriff von außen auf diese Klasse und damit auf den Thread wird dadurch einfacher und sicherer. 왘 Man sollte sich immer klar machen, wo im Speicher Variablen angelegt werden. Variablen, die auf dem Stack angelegt werden, dürfen nicht an einen anderen Thread weitergegeben werden. 왘 Soll ein Thread vorzeitig beendet werden, so ist es am sichersten, einen Mechanismus zu implementieren, bei dem sich der Thread selbst beendet. 왘 Durch den Aufruf von Sleep kann ein Thread-Wechsel ausgelöst, nicht aber erzwungen werden.
MFC-Zusammenfassung
2.12.9 Zusammenfassung Threads können dazu verwendet werden, Rechenaufgaben im Hintergrund ablaufen zu lassen, ohne ein Programm zu blockieren. Auf Computern mit mehr als einer CPU besteht weiterhin die Möglichkeit, Rechenaufgaben mittels Threads zu parallelisieren und gleichzeitig auf mehreren Prozessoren ablaufen zu lassen. Die MFC kennen zwei Typen von Threads. Arbeits-Threads führen Berechnungen im Hintergrund aus, können aber selbst nicht auf die Benutzerschnittstelle zugreifen. Zu diesem Zweck gibt es die Benutzeroberflächen-Threads. Bei der Programmierung mit Threads ist Aufmerksamkeit gefordert. Es gibt nicht mehr nur einen Ablaufpfad durch das Programm, sondern mehrere. Dadurch sind Zugriffe auf Variablen und Ressourcen an mehreren Stellen gleichzeitig möglich. Variablen, auf die von mehreren Threads schreibend zugegriffen werden kann, müssen durch Synchronisationsobjekte, wie kritische Abschnitte oder Semaphoren, geschützt werden. Der Programmierer muss sich mehr als sonst darüber im Klaren sein, wo und wie er seine Daten ablegt (auf dem Stack, auf dem Heap, statisch). Das Konzept der parallelen Verarbeitung durch mehrere Threads ist in der Sprache C++ nicht vorgesehen (beispielsweise im Gegensatz zur Sprache Java). Daher ist ein hohes Maß an Vorsicht geboten, wenn man mit mehreren Threads programmiert. Es bietet sich an, Threads klassenlokal zu verwenden, um die Schwierigkeiten der Thread-Synchronisation vor dem Benutzer der Klasse zu verbergen.
2.13 MFC-Zusammenfassung Die Microsoft Foundation Classes sind sowohl eine objektorientierte Schnittstelle zu Windows als auch ein vollständiges Anwendungsgerüst zur Erstellung von Windows-Programmen. Die Verwendung der MFC ist auf mehreren Ebenen möglich. Auf oberster Ebene gibt die Verwendung des Anwendungsgerüsts dem Programm eine feste Struktur vor. Auf den Ebenen darunter ist ein objektorientierter Zugriff auf das Windows-API über MFCKlassen oder die direkte Verwendung des Windows-API möglich.
227
228
2
Einstieg in die MFC-Programmierung
Je höher man sich in der Abstraktionsebene bewegt, desto einfacher ist es, Programme zu schreiben. Auch die Unterstützung durch die visuellen Werkzeuge der Entwicklungsumgebung ist umfassender. So sind Programme für die Dokument-AnsichtArchitektur sehr einfach mit dem Anwendungs-Assistenten zu erstellen. Weitere Assistenten bieten wertvolle Unterstützung für Bereiche wie Nachrichtenbehandlungsfunktionen und Dialogklassen. Die Einführung in die Programmierung der MFC musste aus Platzgründen in recht knapper Form erfolgen. Hat man jedoch die grundsätzliche Arbeitsweise mit den MFC verstanden, so lassen sich viele Fragen mit der Online-Hilfe klären. Wer sich weiter in die Arbeit mit den MFC vertiefen möchte, für den gibt es mehrere Möglichkeiten. Zu empfehlen ist die Lektüre der technischen Hinweise (Technical Notes), die Teil der Online-Hilfe sind. Des Weiteren gibt es eine ganze Reihe von Büchern, die ausschließlich – und daher ausführlicher – in die MFC-Programmierung einführen. Das Literaturverzeichnis im Anhang führt einige auf. Die MFC sind ohne Zweifel immer noch die Standardklassenbibliothek für Windows, wenn es um die Erstellung von sog. Unmanaged Code geht, also Programmcode der unabhängig von der .NETLaufzeitumgebung läuft. Inwieweit Programmierer in Zukunft die neue .NET-Umgebung und die darauf aufbauende Klassenbibliothek verwenden werden, ist zur Zeit der Abfassung dieses Kapitels noch nicht abzusehen. Allerdings kann man mit Sicherheit sagen, dass MFC-Programme weitaus kompakter und schneller sind als vergleichbare .NET-Programme (siehe hierzu auch Anhang D). Für zeitkritische und performancehungrige Programme wie beispielsweise Spiele oder CD-Brennsoftware kann .NET-keine Alternative zur nativen Programmen sein. Dass die MFC von Microsoft nicht aufgegeben werden, sieht man zum Beispiel daran, dass mit Visual Studio .NET wieder eine neue, erweiterte Version der MFC veröffentlicht worden ist.
3 COM, OLE und ActiveX Das vorliegende Kapitel führt in die Programmierung des Component Object Model (COM) und einiger darauf aufbauender Technologien ein. Zunächst werden die Beweggründe für die Entwicklung von COM beleuchtet, erst dann wird auf die Implementierung eingegangen.
3.1
Einführung
Zunächst ist zu klären, worum es in diesem Kapitel überhaupt geht. Die Begriffe COM, OLE und ActiveX stehen alle für mehr oder weniger abstrakte Technologien und Konzepte. Selbst wenn man eine Vorstellung davon haben sollte, was hinter diesen Begriffen steht, so ist damit noch nicht unmittelbar klar, warum man diese Technologien und Konzepte überhaupt benötigt. Worum also geht es?
3.1.1
Motivation
Alle in diesem Kapitel besprochenen Technologien haben mit dem zentralen Thema der Wiederverwendung zu tun. Wiederverwendung in Softwaresystemen ist ein kompliziertes Thema. Schon seit den sechziger Jahren gibt es das Schlagwort »Softwarekrise«. Software ist sehr aufwändig zu entwickeln, die üblichen Verfahren sind zeitaufwändig, teuer und fehleranfällig. Schon seit langer Zeit sucht man daher nach Techniken, um bestehende Software in anderen Umgebungen weiter- und wiederverwenden zu können. Die Informatik hat bereits eine Reihe solcher Verfahren vorgeschlagen und ist stetig dabei, neue oder verbesserte Verfahren der Softwareentwicklung zu erforschen. Hat man früher Funktionsbibliotheken erstellt, so ist es heute üblich, im Sinne der Objektorientierung Klassenbibliotheken und Frameworks zu entwerfen, um Programmcode wieder zu verwenden.
Wiederverwendung
230 Standardbauteile
3
COM, OLE und ActiveX
Die Zielsetzung ist es, bei der Softwareentwicklung ein ingenieurmäßiges Arbeiten zu ermöglichen, wie dies beispielsweise im Maschinenbau oder in der Architektur möglich ist. Heute ist man noch weit von diesem Ziel entfernt. Sowohl im Maschinenbau als auch in der Architektur gibt es Standardbauteile und Standardverfahren, um eine Konstruktionsaufgabe durchzuführen. Die Verfahren sind den betreffenden Ingenieuren beziehungsweise Architekten wohl bekannt; für Bauteile gibt es Kataloge, in denen diese verzeichnet sind. In der Softwaretechnik gibt es praktisch keine Standardbauteile. Viele Softwareentwickler haben ihre eigenen Bauteile; mit den gängigen Methoden der Wiederverwendung schaffen sich diese Programmierer im Laufe der Zeit ihre eigenen Bauteillager, sie verwenden also Programmcode wieder. Dies ist ganz im Sinne der Wiederverwendung. Allerdings ist der dabei betrachtete Kontext nicht zufrieden stellend: Jeder Programmierer kann nur seine eigenen Bauteile (oder die seines Teams, seiner Firma) wiederverwenden. Wechselt er zu einer anderen Firma, so muss er plötzlich mit völlig neuen Bauteilen umgehen, da er seine eigenen Bauteile aus rechtlichen Gründen nicht mitnehmen darf und die neue Firma wahrscheinlich ihre eigenen Bauteile verwendet. Übertragen auf den Maschinenbau würde dies bedeuten, dass jede Maschinenbaufirma ihren eigenen Typ Schrauben verwenden würde: Schrauben mit dreieckigen Köpfen, Schrauben mit Linksgewinde, Schrauben mit abenteuerlichen Maßen, Schrauben nur für Spezialschraubendreher. Natürlich müsste jede Firma diese Schrauben selbst herstellen, auf dem Markt sind sie schließlich nicht zu kaufen. Der Aufwand für Konstruktion und Herstellung der eigenen Schrauben wäre beträchtlich. Das kann sich keine Maschinenbaufirma leisten; stattdessen bestellt man Schrauben in Standardgrößen aus dem Katalog. Softwarefirmen leisten sich den Luxus der Exklusivität. In vielen Firmen werden eigene Code- und Klassenbibliotheken sowie Frameworks entwickelt. Der Aufwand dafür ist erheblich. Die Entwicklungszeit praktisch einsetzbarer Klassenbibliotheken und Frameworks beträgt zumeist mehrere Mannjahre. Trotzdem sind diese »Schrauben« nur innerhalb des eigenen Entwicklungsteams einsetzbar, außerhalb der Firma möchte keiner diese fremden Bauteile haben; man verwendet lieber die eigenen (im Englischen wird dieses Phänomen als NIH, not invented here, bezeichnet). Nun kommt diese Verhaltensweise der Softwarefirmen und ihrer Entscheidungsträger nicht von ungefähr. Es gab bisher keinen echten Bauteilemarkt für Software. Es gab und gibt zwar Programmcode- und Klassenbibliotheken zu kaufen, doch sind dies nicht
Einführung
231
wirklich Softwarebauteile. Ein Bauteil – wie eine Schraube – ist etwas fertiges. Es existiert, ist eindeutig spezifiziert, trägt eine Bezeichnung und hat oft ein klar umrissenes Einsatzgebiet. Man kann es bestellen, auspacken und einbauen. Kauft man sich jedoch eine Programmcode- oder Klassenbibliothek, so hat man kaum mehr als einen Konstruktionsplan für potenziell sehr viele Bauteile. Aber man muss diesen Konstruktionsplan zunächst verstehen, die richtigen Teile des Plans anwenden, dann das Bauteil selbst bauen (!), es auf Fehler prüfen und diese beseitigen, und erst dann kann man das so entstandene Bauteil verwenden. Oben ist bereits gesagt worden, dass in es in den Ingenieurdisziplinen nicht nur genormte Bauteile gibt, sondern dass dazu auch Standardverfahren bei der Konstruktion verwendet werden. Auch die Softwaretechnik hat bekannte Verfahren, um bei bestimmten Aufgaben zu Problemlösungen zu kommen. Allerdings schneidet die Softwaretechnik beim direkten Vergleich mit den Ingenieurdisziplinen wieder schlecht ab. In der Praxis sind diese Verfahren nämlich oft nicht bekannt, manchmal unausgereift und vage. Verfahren, die sich auf Softwarebauteile stützen, sind praktisch nicht vorhanden. Generell wird dem Aspekt einer globalen Entwicklergemeinde (Schrauben sind weltweit austauschbar!) bisher wenig Beachtung geschenkt. Softwareentwickler denken oft nur systemweit. Damit man auch bei der Softwareentwicklung eine dem Ingenieurwesen gemäße Vorgehensweise einführen kann, müssen Standardbauteile und Vorgehensweisen für deren Einsatz definiert werden. Dabei sind folgende Punkte wichtig: 왘 Verwendbarkeit. Die Akzeptanz des Programmierers muss gewonnen werden. Das Bauteil muss »von der Stange« funktionieren, das heißt, es muss schnell und einfach zu ersehen sein, wie das Bauteil funktioniert. Es muss vollständig dokumentiert und darf nicht zu komplex sein. Je einfacher, desto besser. (Eine Schraube ist sofort verwendbar. Es dauert – normalerweise – nur kurz, bis man verstanden hat, wie der dazugehörende Schraubenzieher funktioniert.) 왘 Verfügbarkeit. Das Bauteil muss fertig sein. Es ist nicht akzeptabel, dass man es erst selbst zusammenbauen muss. Daraus folgt, dass ein Bauteil in binärer Form und nicht als Sourcecode vorliegen sollte. (Eine Schraube ist fertig; man muss sie nicht aus einem Stift selbst drehen.)
Globaler Kontext
232
3
COM, OLE und ActiveX
왘 Standards. Die Teile müssen genormt sein. Der Standard bestimmt das Verhalten und die Beschaffenheit des Bauteils. (Schrauben gibt es in Einheitsgrößen, normale Schrauben haben ein Rechtsgewinde.) 왘 Kontext. Der Kontext der Verwendung muss betrachtet werden. Bauteile müssen bestimmten Verwendungsmustern gerecht werden. Das Verwendungsmuster wird durch die Vorgehensweise bei der Erstellung des Gesamtsystems bestimmt. (Eine Schraube wird nicht einfach [ohne Dübel] in die Wand geschraubt; ein Fahrrad wird nicht zusammengenagelt.) Dieses Kapitel beschreibt die ersten Lösungsvorschläge der Firma Microsoft für das Problem der globalen Wiederverwendung von Software.
3.1.2
Begriffe
Softwarekomponenten
Das, was im vorherigen Abschnitt als »Bauteil« beschrieben wurde, soll von nun an als Softwarekomponente bezeichnet werden. Eine Softwarekomponente ist ein Softwarebauteil, welches in binärer Form vorliegt und von dem es eine vollständige Beschreibung seiner Funktionsweise und seiner Verwendung gibt. Wie die beschriebene Funktion intern realisiert wird, ist dabei nicht von Interesse.
Komponentenobjektmodell
Um Softwarekomponenten zu erstellen und daraus Standardbauteile im beschriebenen Sinne machen zu können, hat Microsoft das Komponentenobjektmodell (COM) eingeführt. COM ist gleichsam die Werkbank zur Softwarekomponentenherstellung, die Basis, auf der genormte Bauteile entstehen können. Alle anderen in diesem Kapitel beschriebenen Technologien sind Anwendungen von COM, sie bauen auf COM auf. Diese Technologien definieren oder sind letztlich Verwendungsmuster von Softwarekomponenten, die auf COM basieren. Sie definieren Kontexte, in denen Softwarekomponenten wiederverwendet werden können. Folgende COMbasierte Technologien werden besprochen:
COM-basierte Technologien
왘 Automation. Bei der Automation (auch Automation) betrachtet man ganze Programme als Objekte. Das Programm stellt Eigenschaften (Variablen) und Methoden (Funktionen) zur Verfügung, mit denen es sich von anderen Programmen oder über eine Scriptsprache steuern lässt.
Einführung
233
왘 Vereinheitlichter Datenaustausch. Der vereinheitlichte Datenaustausch ermöglicht es, Daten zwischen und innerhalb von Applikationen weitgehend unabhängig von Transportmedium und Art der transportierten Daten auszutauschen. Der vereinheitlichte Datenaustausch wird beispielsweise über die Zwischenablage und bei Drag&Drop angewandt. 왘 OLE. Object Linking and Embedding ist eine Technologie, bei der Dokumente verschiedener Programme zusammen in einer Datei gespeichert werden können. Diese Dokumente werden auch als Verbunddokumente bezeichnet. Ein Dokument dient als Container (beispielsweise ein Word-Dokument), in den andere Dokumente eingebettet werden können (beispielsweise ein Excel-Dokument). Das Besondere an OLE ist, dass das eingebettete Dokument innerhalb der Applikation, die das Container-Dokument erstellt hat, bearbeitet werden kann. (Eine Excel-Tabelle kann in Word bearbeitet werden. Die Menü- und Symbolleisten aus Excel werden dazu in Word eingeblendet.) 왘 ActiveX-Steuerelemente. ActiveX-Steuerelemente sind auf der Basis von COM erstellte Steuerelemente. Diese Steuerelemente lassen sich in verschiedenen Programmierumgebungen wie Visual Basic, Delphi oder C++ einsetzen. Auch der Internet Explorer kann in HTML eingebettete ActiveX-Steuerelemente verwenden, damit können diese innerhalb von HTML-Seiten ausgeführt werden. Abbildung 3.1 zeigt den Zusammenhang zwischen den in diesem Kapitel vorgestellten Technologien.
Automation
Vereinheitlichter Datenaustausch
OLE
ActiveXSteuerelemente
COM Abbildung 3.1: Auf COM basierende Technologien
Wie man sehen kann, bildet COM das Fundament für Automation, vereinheitlichten Datenaustausch, OLE und ActiveX-Steuerelemente. Allerdings sind diese vier Anwendungen von COM nicht die einzigen. COM ist beispielsweise auch die Basis für die Multimediatechnologie von Microsoft, DirectX. Außerdem sind die
COM als Basis
234
3
COM, OLE und ActiveX
auf COM aufbauenden Technologien nicht völlig unabhängig voneinander. So wird beispielsweise die Automation innerhalb von ActiveX-Steuerelementen verwendet. Leider ist das Gesamtsystem aus COM und den darauf aufbauenden Technologien nicht so harmonisch und zielgerichtet gewachsen, wie man aufgrund des bisher Gesagten vermuten könnte. Der große Plan einer auf COM basierenden Architektur zur Wiederverwendung von Softwarekomponenten ist erst langsam entstanden. So hat OLE bereits vor COM existiert. Da jedoch die erste Implementierung nicht besonders gelungen war, wurde sie im zweiten Anlauf auf der Basis von COM wiederholt. Damit wurde COM erstmals verwendet. COM hatte aber selbst bei der Einführung von OLE2 (der zweiten und jetzigen Version von OLE) noch keinen eigenen Namen, den bekam COM erst später. Die Vorfahren der ActiveX-Steuerelemente sind ohne große Planung entstanden. Eher nebenbei wurde eine frühe Version von Visual Basic mit einer Architektur zur Verwendung von Steuerelementen von Drittherstellern ausgestattet. Diese VBX-Steuerelemente (Visual Basic Extensions) hatten einen solchen Markterfolg, dass Microsoft die etwas wackelige VBX-Architektur beim fälligen Umstieg von WIN16 auf WIN32 auf die Basis von COM gestellt und fortan OCX genannt hat. Durch den neuen, verbesserten Unterbau konnten OCX-Steuerelemente auch außerhalb von Visual Basic verwendet werden. Heute heißen OCX-Steuerelemente ActiveX-Steuerelemente. Die Namensänderung ist auf technische Verbesserungen zur ursprünglichen Spezifikation und auf Marketinggründe zurückzuführen. Begriffsunklarheiten
Die inkonsistente Verwendung von Begriffen ist auf das nicht ganz gradlinige Wachstum der hier besprochenen Technologien zurückzuführen. Man sollte sich von unklaren Begriffen nicht verwirren lassen. Unter anderem treten folgende Begriffsunklarheiten auf: 왘 COM hatte zunächst keinen eigenen Namen. COM wurde anfangs unter dem Sammelbegriff OLE geführt. 왘 OLE bezeichnete zunächst die Technologie der Verbunddokumente und deren Einbettung und Verknüpfung. Mit Einführung von OLE2 bezeichnete OLE dann alle COM-basierten (damals noch nicht so genannten) Technologien. Heute bezeichnet OLE wieder die Technik der Verbunddokumente.
Einführung
235
왘 Automation hieß früher OLE-Automatisierung oder Automatisierung. 왘 ActiveX-Steuerelemente hießen früher OLE-Steuerelemente oder OCX. Jetzt werden sie teilweise auch als COM-Steuerelemente bezeichnet. Der Begriff ActiveX ist ohne weitere Zusätze leider nicht eindeutig. Meist sind mit ActiveX COM-basierte Technologien von Microsoft gemeint. Allerdings werden auch Internettechnologien aus dem Hause Microsoft zu ActiveX gezählt, obwohl diese mit COM teilweise nichts zu tun haben. Im Zweifelsfall sollte man genau sagen, was man meint (zum Beispiel ActiveX-Steuerelement) und die Schlagworte den Marketingleuten überlassen.
3.1.3
Übersicht
Was soll in diesem Kapitel gezeigt werden? Zunächst ist es wichtig zu verstehen, was das Komponentenobjektmodell COM ist und wie es funktioniert. COM ist so wichtig, weil es die Grundlage aller weiteren in diesem Kapitel besprochenen Technologien ist. COM ist mittlerweile zu einem integralen Bestandteil von Windows geworden und ist die Basis vieler Windows-Technologien. Um zu verdeutlichen, wie COM funktioniert, wird zunächst ohne die Hilfe der MFC eine COM-Komponente erstellt. Bei der Implementierung ohne die Hilfe eines Frameworks oder die Laufzeitumgebung einer interpretierten Sprache wird das interne Wesen von COM deutlich werden. Erst danach wird auf eine Implementierung von COM-Komponenten mit den MFC hingearbeitet.
COM
Mit der Automation wird die erste Anwendung von COM vorgestellt. Das bereits bekannte Programm StockChart wird als Automationsserver neu implementiert. Die Verwendung der Automation mit Visual Basic und mit dem Windows Scripting Host wird besprochen. Schließlich werden zwei Möglichkeiten der Implementierung von Automations-Clients innerhalb der MFC gezeigt.
Automation
Danach wird der vereinheitlichte Datenaustausch besprochen. Drag&Drop sowie der Datenaustausch über die Zwischenablage werden behandelt.
Vereinheitlichter Datenaustausch
Bei der Besprechung von OLE wird zunächst auf die der Technologie zugrunde liegenden Verbunddateien eingegangen. Danach wird das Programm StockChart zunächst als OLE-Server und anschließend als ActiveX-Dokumentserver implementiert.
OLE
236 ActiveX-Steuerelemente
3
Die Verwendung von ActiveX-Steuerelementen innerhalb der MFC wird ebenso beschrieben wie ihre Erstellung mit den MFC. Als Beispiel wird ein Drehknopf-Steuerelement verwendet.
3.2 Weshalb COM?
Das Komponentenobjektmodell
Das »C« in COM sagt es bereits: COM ist eine Technologie, die zur Erstellung von Softwarekomponenten entwickelt worden ist. Diese Komponenten werden vom Programmierer als Objekte angesprochen. Es ist zunächst nicht klar, weshalb man eine Technologie wie COM benötigt. Schließlich sind die meisten heute verwendeten Programmiersprachen mehr oder weniger objektorientiert. Techniken wie Abstraktion und Kapselung gehören zum Grundwissen eines Programmierers. Teile von Programmen lassen sich sehr leicht in DLLs auslagern. Folglich sollte die Erstellung von Softwarekomponenten eigentlich ganz einfach sein: Man nehme beispielsweise eine C++-Klasse, verpacke diese in eine DLL und vermarkte das Ganze als Softwarekomponente. Da es derartige Komponenten auf dem Markt praktisch nicht gibt, muss es wohl einige Probleme mit diesem Ansatz geben. Im Folgenden soll gezeigt werden, welche Probleme bei der Entwicklung von Softwarekomponenten mit C++ auftreten und wie diese von COM gelöst werden.
3.2.1 Implementierung und Schnittstelle
COM, OLE und ActiveX
Anforderungen an eine Softwarekomponente
Eine Softwarekomponente besteht immer aus zwei Teilen. Zunächst soll eine Softwarekomponente eine Aufgabe ausführen, sie soll ein Problem lösen. Allgemein gesprochen nimmt dieser Teil der Komponente einen Satz von Eingangsdaten entgegen und produziert daraus einen Satz von Ausgangsdaten. Das ist der Implementierungsteil oder die Implementierung der Komponente. Um die Funktionalität der Komponente nutzen zu können, muss der Programmierer die Komponente in irgendeiner Form ansprechen können. Dazu stellt die Komponente eine oder mehrere Funktionen bereit, die der Programmierer aufrufen kann. Handelt es sich um eine objektorientierte Komponente, so kann ihre Funktionalität in Form von Objekten bereitgestellt werden, deren Methoden der Programmierer aufruft. Der Teil der Softwarekomponente, der dem Programmierer die Funktionalität der Komponente in Form von Funktions- oder Methodenaufrufen zur Verfügung stellt, wird als Schnittstelle der Komponente bezeichnet. Abbildung 3.2 zeigt eine Softwarekomponente mit Schnittstelle und Implementierung.
Das Komponentenobjektmodell
237 Softwarekomponente
Schnittstelle
Funktion 1
Funktion 2
Implementierung
Funktion 3
Abbildung 3.2: Softwarekomponente mit Schnittstelle und Implementierung
Normalerweise ist es wünschenswert, dass der Zugriff auf eine Softwarekomponente ausschließlich über die Schnittstelle erfolgen kann. Die Implementierung wird gekapselt, ein Zugriff auf sie ist nicht möglich. Auf diese Weise ist es dem Programmierer nicht möglich, Vorteile aus Implementierungsdetails der Komponente zu ziehen. Was auf den allerersten Blick wie eine Einschränkung erscheint, zahlt sich auf lange Sicht immer aus. Dadurch dass der Anwender der Komponente keinen Zugriff auf die Implementierung hat, ist es möglich, die Implementierung in neuen Versionen der Komponente zu ändern, beispielsweise um die Effizienz des verwendeten Algorithmus zu steigern. Idealerweise ist es dem Anwender der Komponente nicht nur unmöglich, auf die Implementierung der Komponente zuzugreifen, sondern er kennt sie überhaupt nicht. Die Komponente tritt als Black Box in Erscheinung, der Zugriff ist nur über die Schnittstelle möglich, das Innere ist unbekannt. Das Verhalten der Komponente wird allein über ihre Schnittstellenbeschreibung definiert. Die Verwendung von Black-Box-Komponenten bei der Softwareentwicklung trägt zur Robustheit von Programmen bei. Kann der Programmierer, der eine Softwarekomponente verwendet, schon durch die Kapselung nicht auf die Implementierung direkt zugreifen, so ist es ihm bei Kenntnis der Implementierung teilweise möglich, Seiteneffekte und damit nicht spezifiziertes Verhalten der Implementierung auch über die Schnittstelle auszunutzen (siehe Kasten). Da solches, nicht spezifiziertes Verhalten sich jedoch bei einer neuen
Kapselung
238
3
COM, OLE und ActiveX
Implementierung ändern kann, trägt dessen Ausnutzung nicht zu einer Robustheit des Programms bei. Daher ist es von Vorteil, wenn der Anwender einer Komponente deren Implementierung nicht kennt und auch nicht in Erfahrung bringen kann. Ausnutzung nicht spezifizierten Verhaltens
Nicht spezifiziertes Verhalten lässt sich üblicherweise auf zwei Weisen auch über Schnittstellen hinweg ausnutzen, wenn man die Implementierung kennt. Zum einen könnte ein Programmierer Parameter an Funktionen übergeben, von denen er weiß, dass die Werte dieser Parameter außerhalb des zulässigen Bereichs liegen. Da er die Implementierung kennt, weiß er, was für Ergebnisse zu erwarten sind. Eventuell findet er die auf diese Weise berechneten Werte nützlich und verwendet sie in seinem Programm. Ein weiterer beliebter Fall von Ausnutzung nicht spezifizierten Verhaltens betrifft das Laufzeitverhalten eines Softwaremoduls oder einer Funktion. Wenn das Laufzeitverhalten einer bestimmten Funktion bekannt ist, kann dieses – beispielsweise als bekannte Verzögerungszeit – vom Programmierer ausgenutzt werden. Es muss wohl kaum gesagt werden, dass beide Vorgehensweisen sehr schlechter Programmierstil sind. Es kann jedoch immer sein, dass unbeabsichtigt Implementierungsdetails ausgenutzt werden. Hat ein Programmierer die Komponente erstellt, die er später in einem Programm verwendet, dann ist es ihm vielleicht gar nicht bewusst, dass er implizit Implementierungsdetails der Komponente in seinem Programm verwendet. Den besten Schutz vor einer solchen unbeabsichtigten Verwendung von Implementierungswissen besteht darin, dass der Anwender einer Komponente die Implementierung derselben tatsächlich nicht kennt. Werden sowohl Softwarekomponenten als auch die Programme, die diese verwenden, in derselben Firma erstellt, so sollte man sie von verschiedenen Teams entwickeln lassen.
Binäre Komponenten
Eine Komponente sollte unabhängig von dem sie verwendenden Programm austauschbar sein. Das bedeutet, dass die Komponente in einem binären Format vorliegen muss, um eine Neukompilierung zu vermeiden. Die Bindung an die Komponente darf erst zur Laufzeit erfolgen, wenn man beim Austausch der Komponente eine Neukompilierung umgehen will. Binäre Softwarekomponen-
Das Komponentenobjektmodell
239
ten helfen – im Gegensatz zu Modulen auf Quellcodeebene – die monolithische Struktur von Anwendungen aufzubrechen. Die Mechanismen, die eine Softwarekomponente verwendet, um Verhalten zu implementieren und nach außen zugänglich zu machen, ähneln durchaus den Mechanismen, die dafür auf Quelltextebene verwendet werden. Die Auswirkungen auf das resultierende Softwaresystem als Ganzes sind aber deutlich andere. Das System ist nicht nur während seiner Konstruktion modular, sondern auch nach seiner Fertigstellung. Idealerweise lässt sich eine Softwarekomponente sowohl in einer beliebigen Programmiersprache erstellen, als auch von einer beliebigen Programmiersprache aus verwenden. Das binäre Format, in dem die Komponente ausgeliefert wird, sollte folglich sprachunabhängig sein. Komponenten, die sprachunabhängig sind, vergrößern vor allen Dingen die Marktchancen einer Komponente und helfen damit, einen Softwarekomponentenmarkt zu etablieren.
Unabhängigkeit von der Programmiersprache
Betrachtet man Softwarekomponenten in einem globalen Kontext und nimmt man einen großen Markt von unabhängigen Komponenten an, dann wird es wichtig, sowohl einzelne Komponenten als auch deren Schnittstellen eindeutig identifizieren zu können. Schließlich wird es für oft auftretende Probleme wahrscheinlich Problemlösungen mehrerer Komponentenhersteller geben. Diese Hersteller verwenden unter Umständen eine problemgerechte und damit teilweise gleiche Namensgebung. Es muss Mittel und Wege geben, solche Komponenten zu unterscheiden und gezielt ansprechen zu können.
Globaler Kontext
Bei Softwarekomponenten muss man die Tatsache berücksichtigen, dass diese sich meist fortlaufend weiterentwickeln. Sofern diese Weiterentwicklung die Schnittstelle der Komponente betrifft, kann es zu Problemen kommen. Alte Programme verlangen die ursprüngliche Version der Schnittstelle, neuere Programme wollen die ganze Funktionalität der Komponente nutzen und verlangen die neue Version der Schnittstelle. Softwarekomponenten müssen dieses Versionsproblem lösen, damit sich Updates von Programm und Komponenten problemlos durchführen lassen.
Versionsproblem
Die folgende Liste fasst alle Anforderungen an Softwarekomponenten zusammen:
Anforderungen an eine Softwarekomponente
왘 Die Funktionalität einer Softwarekomponente darf für den Anwender der Komponente nur über eine Schnittstelle zugänglich sein. Die Implementierung der Komponente wird gekapselt
240
3
COM, OLE und ActiveX
und darf nicht zugänglich sein. Das Verhalten einer Softwarekomponente wird ausschließlich über deren Schnittstelle spezifiziert. 왘 Bei der Verwendung von Softwarekomponenten ist es von Vorteil, sie als Black Box zu betrachten. Kennt der Anwender der Komponente deren Implementierung nicht, so ist er auch nicht versucht, implementierungsspezifische Details auszunutzen. 왘 Eine Softwarekomponente soll unabhängig vom sie verwendenden Programm austauschbar sein. Eine Neukompilierung des Programms darf nicht notwendig werden. 왘 Die Bindung des Programms an die Softwarekomponente soll erst zur Laufzeit erfolgen. Nur so lässt sich eine Komponente nachträglich, ohne Neukompilierung austauschen. Es ist vorteilhaft, wenn der Bindungsvorgang beim Programmstart erfolgt und alle Referenzen überprüft, damit es nicht zu unerwarteten Programmabstürzen aufgrund nicht auflösbarer Referenzen kommt. 왘 Komponenten sollen sprachunabhängig sein, sie sollen sich sowohl in verschiedenen Programmiersprachen implementieren, als auch von verschiedenen Programmiersprachen ausnutzen lassen. 왘 Softwarekomponenten und deren Schnittstellen sollen eindeutig identifizierbar sein. 왘 Verschiedene Versionen von Programm und Softwarekomponente müssen problemlos miteinander arbeiten können. Es stellt sich die Frage, ob die genannten Anforderungen an Softwarekomponenten mit den bereits bekannten Hilfsmitteln von Windows und der Sprache C++ realisiert werden können.
3.2.2
Lassen sich die Anforderungen an Softwarekomponenten mit der Sprache C++ realisieren?
Einer der Hauptgründe für den Einsatz von Softwarekomponenten ist die Möglichkeit der Wiederverwendung in unterschiedlichen Softwareumgebungen. Eine Komponente wird einmal in möglichst allgemein gültiger Form erstellt und kann dann von verschiedenen Programmierern in unterschiedlichen Programmen eingesetzt werden.
Das Komponentenobjektmodell
Der Gedanke der Wiederverwendung ist in der Objektorientierung allgegenwärtig und war sicherlich einer der Beweggründe für die Entwicklung der Sprache C++. Die Sprache C++ versucht, die Wiederverwendung von Programmteilen durch verschiedene Techniken zu erreichen:
241 Wiederverwendung in C++
왘 Polymorphie bedeutet, dass eine Funktion auf mehr als einen Typ angewendet werden kann. Polymorphie kann in Form einfachen Überladens von Funktionsnamen mit unterschiedlichen Parameterlisten innerhalb einer Klasse oder in Form virtueller Funktionen durch mehrfache Verwendung eines Funktionsnamens innerhalb einer Vererbungshierarchie realisiert werden. Zudem tritt Polymorphie in Form von Templates auf. 왘 Kapselung schützt die Implementierung einer Klasse vor dem Zugriff von außen. C++ bietet drei Ebenen der Kapselung an: Durch Verwendung des Schlüsselworts public wird ein freier Zugriff erlaubt, das Schlüsselwort protected erlaubt einen Zugriff innerhalb der Vererbungshierarchie, und nur durch das Schlüsselwort private wird die Implementierung einer Klasse wirklich vor dem Zugriff von außen geschützt. Mit dem Schlüsselwort friend lassen sich zudem Löcher in die Schale der Kapselung bohren. 왘 Durch Vererbung, im Falle von C++ sogar durch Mehrfachvererbung, kann die Implementierung von Klassen wiederverwendet werden. Man spricht auch davon, dass die Implemen tierung vererbt wird, denn es wird tatsächlich Programmcode der Basisklassen in die abgeleiteten Klassen übernommen. Diese von der Sprache C++ verwendeten Techniken eignen sich durchaus zur Wiederverwendung von Programmteilen. Üblicherweise wird gerade die Möglichkeit der Vererbung intensiv genutzt, was zum Entstehen von Klassenbibliotheken und Frameworks führt. Gerade ein solches Framework – die MFC sind ein sehr gutes Beispiel – bietet viele Stellen, an denen sich der Programmierer »einhängen« kann, um die vorhandene Funktionalität zu nutzen. »Einhängen« bedeutet, dass der Programmierer sich eine Basisklasse aussucht, die die von ihm gewünschte Funktionalität implementiert. Der Programmierer leitet von dieser Basisklasse eine eigene Klasse ab und verwendet so die bereits implementierte Funktionalität. Die MFC zeigen, dass auf diese Weise sehr viel Funktionalität wieder verwendet werden kann.
Wiederverwendung in Klassenbibliotheken und Frameworks
242
3
COM, OLE und ActiveX
Vererbung hat keine Schnittstelle
Damit der Programmierer die Funktionalität des Frameworks wiederverwenden kann, muss er diesen allerdings sehr genau kennen. Da das Framework die Implementierung einer Klasse erbt, gibt es keine echte Schnittstelle. Vererbung bedeutet hier vielmehr, dass der Programmierer alles bekommt, was da ist. Zwar können Basisklassen ihre Interna durch das Schlüsselwort private schützen, doch zeigt die Praxis, dass es oft nicht sinnvoll ist, dies zu tun. Vielmehr kann die abgeleitete Klasse meist auf große Teile der Implementierung der Basisklasse zugreifen. Oft muss der Programmierer zumindest Teile der Implementierung der Basisklasse kennen, um deren Funktionalität wiederzuverwenden. Dies widerspricht klar den Anforderungen an eine Softwarekomponente, eine klare Schnittstelle und eine Black-Box-Implementierung zu bieten.
Enge Kopplung durch Vererbung
Doch selbst wenn es den Designern des verwendeten Frameworks gelungen sein sollte, die Implementierung aller Klassen nach außen und innerhalb der Klassenhierarchie vollständig abzuschirmen, ergeben sich weitere Probleme mit dem Ansatz der Wiederverwendung durch die Vererbung der Implementierung. Will man die vorhandene Funktionalität durch eine eigene ergänzen, so wird das oft durch das Überschreiben von virtuellen Funktionen erreicht. Mit der eigenen Version der überschriebenen Funktion ergänzt man bereits vorhandene Funktionalität. Um die vorhandene Funktionalität auszuführen, muss allerdings noch die entsprechende Funktion der Basisklasse aufgerufen werden. Doch muss diese vor oder nach der eigenen Verarbeitung aufgerufen werden? Oder vielleicht in einem bestimmten Fall auch gar nicht? Obwohl die Basisklasse vollständig abgeschirmt ist, besteht immer noch eine enge Kopplung zwischen Basisklasse und abgeleiteter Klasse. Der Programmierer muss immer noch Details der Implementierung der Basisklasse kennen, um zu entscheiden, wann er die Funktionen der Basisklasse aufrufen muss. Eine Black-Box-Implementierung ist selbst bei vollständiger Abschirmung der Basisklasse nicht gegeben.
Vererbungsgraf und Schnittstelle
Ein weiteres Problem betrifft die Schnittstelle einer Softwarekomponente. Die Schnittstelle einer C++-Klasse wird durch ihre Position innerhalb des Vererbungsgrafen mitbestimmt. Selbst wenn zwei C++-Klassen identische Funktionen bereitstellen, sind sie nicht austauschbar, sofern sich die Klassen an unterschiedlichen Positionen des Vererbungsgrafen befinden (vgl. Kasten). Die Kopplung zwischen Vererbungsgraf und Schnittstelle ist für eine Softwarekomponente nicht akzeptabel. Komponenten mit gleicher Schnittstelle müssen untereinander austauschbar sein.
Das Komponentenobjektmodell
Die Schnittstelle einer C++-Klasse wird durch ihre Position im Vererbungsgrafen mitbestimmt. Folgendes Beispiel soll dies verdeutlichen: // Klasse A class A { public: A(int nNum):nNumber(nNum),nFiller(17) {}; int Square() { return nNumber*nNumber; }; protected: int nFiller; // Füllwert (Speicherlayout) int nNumber; }; // Klasse B, abgeleitet von A class B : public A { public: B(int nNum):A(nNum) {}; void DoNothing() {}; }; // Klasse C, nicht Teil der Klassenhierarchie class C { public: C(int nNum):nNumber(nNum) {}; int Square() { return nNumber*nNumber; } void DoNothing() {}; private: int nNumber; };
void B A B C
main (){ b(3); *pA; *pB; *pC;
pA = &b; // ok pB = &b; // ok pC = &b; // *** Compilerfehler: ***
243
244
3
// "'class B *' kann nicht in 'class C // Die Typen, auf die verwiesen wird, // die Konvertierung erfordert einen tor // oder eine Typumwandlung im C- oder
COM, OLE und ActiveX
*' konvertiert werden. sind nicht verwandt; reinterpret_cast-OperaFunktionsformat."
pA->Square (); pB->Square (); pC->Square (); }
Die Klassen B und C haben beide die gleiche Schnittstelle, bestehend aus den Funktionen Square und DoNothing. Die Klasse B ist von der Klasse A abgeleitet, die die Funktion DoNothing nicht besitzt. Das heißt, dass sie eine andere, einfachere Schnittstelle besitzt. Die Klassen B und C haben einen unterschiedlichen internen Aufbau. Die Klasse B besitzt zusätzlich zu C eine geschützte Variable namens nFiller. Eine Variable der Klasse B lässt sich problemlos anderen Variablen der Klassen A und B zuweisen. B und C sind jedoch nicht zuweisungskompatibel. Der Compiler liefert eine Fehlermeldung über einen Typkonflikt. Dies liegt daran, dass C++Typen nicht über deren Schnittstelle, sondern über deren Position im Vererbungsgrafen bestimmt werden. Nimmt man die vom Compiler vorgeschlagene Typumwandlung vor, wird man überrascht sein: Der Aufruf von pC->Square() liefert keineswegs 9, sondern 289! Durch das andere Speicherlayout der Klasse B hat C::Square die Variable nFiller quadriert! Wer jetzt denkt, dass man Square nur als virtuelle Funktion definieren müsste, damit das Beispiel (mit expliziter Typumwandlung) funktioniert, hat Recht. Allerdings ist zu beachten, dass alle Funktionen einer Schnittstelle virtuell und in der gleichen Reihenfolge(!) zu deklarieren sind. Die explizite Typumwandlung sorgt nur dafür, dass sich die Zeiger auf die vtable (Tabelle der virtuellen Funktionszeiger einer Klasse) der beiden Klassen zuweisen lassen. Stimmen die vtables nicht überein, kommt es zu Abstürzen oder unvorhergesehenen Resultaten. Diese Vorgehensweise ist möglich, aber nicht typsicher! C++ und DLLs
C++-Programme werden normalerweise mit Hilfe des Compilers in eine einzige ausführbare Datei übersetzt. Das bedeutet, dass Komponenten, die sich auf Quellcodeebene eindeutig voneinander unterscheiden lassen, mit dem Übersetzungsvorgang zu einem
Das Komponentenobjektmodell
245
großen monolithischen Block verschmolzen werden. C++ kennt keine Komponenten in binärer Form. Unter Windows ist es allerdings möglich, C++-Klassen in DLLs einzubetten. Damit lässt sich die Forderung nach dem Austausch von Komponenten unabhängig vom Programm, das sie verwendet, realisieren. DLLs ermöglichen darüber hinaus die Bindung einer Komponente an ein Programm zur Laufzeit. Allerdings wird durch die Verwendung von DLLs als Container für C++-Komponenten das Problem des Name Manglings nicht gelöst. C++-Kompiler verwenden das Name Mangling, um Funktionsund Variablennamen eindeutig zu machen. Dies wurde bereits in Abschnitt 2.10, »MFC und DLLs«, gezeigt. Wie schon erwähnt, ist dieses Verfahren nicht standardisiert. Jeder Compilerhersteller kann hier sein eigenes Verfahren implementieren. Daher lassen sich keine allgemein verwendbaren Komponenten mit der Sprache C++ erstellen. Dies ist schon bei der Besprechung von MFCErweiterungs-DLLs deutlich geworden. C++-Klassen, die in DLLs eingebettet werden, können nur mit dem Compiler verwendet werden, der diese DLLs erstellt hat. Die für Softwarekomponenten geforderte Sprachunabhängigkeit wird nicht erreicht, es wird nicht einmal Compiler-Unabhängigkeit erreicht.
Name Mangling
C++-Klassen bieten keine Mechanismen, um eindeutig identifizierbare Komponenten zu erzeugen. Das Gleiche gilt für die Schnittstelle einer Komponente. In C++ ist die Schnittstelle Teil der Implementierung und wird nicht explizit abgetrennt. Zwar ist es gängige Praxis, die Deklaration einer Klasse in einer HeaderDatei und die Implementierung in einer Implementierungsdatei vorzunehmen, doch gibt es keinen Mechanismus in der Sprache, der diese Vorgehensweise erzwingt. Oft werden als inline deklarierte Funktionen gleich in der Header-Datei definiert. Dies ist eine klare Vermischung von Schnittstelle und Implementierung. Das Konzept einer expliziten Schnittstelle ist der Sprache C++ nicht bekannt. Die Schnittstelle einer C++-Klasse ist einfach das, was die Klasse als public deklariert: eine schlechte Voraussetzung, wenn man Schnittstellen eindeutig identifizieren möchte.
C++ und Schnittstellen
Schließlich sollen verschiedene Versionen von Programm und Komponente problemlos miteinander arbeiten können. Auch hier kann es bei der Verwendung der Sprache C++ zu Problemen kommen, wie Listing 3.1 zeigt.
Versionsprobleme
246
3
COM, OLE und ActiveX
// Version 1.0 class CMyClass { public: CMyClass (); ˜CMyClass (); long DoWhatIWant (long nInput); private: long m_data; }; // Version 2.0 class CMyClass { public: CMyClass (); ˜CMyClass (); long DoWhatIWant (long nInput); private: long m_data; long m_cache; }; Listing 3.1: Unvollständige Kapselung von C++-Klassen
Version 1.0 der Komponente besitzt eine Schnittstelle, die aus Konstruktor, Destruktor und einer Funktion besteht, der ein Eingabewert übergeben wird und die daraus einen Ausgabewert erzeugt. Die Implementierung benötigt die Variable m_data für interne Berechnungen; m_data ist nicht Teil der Schnittstelle. In der Version 2.0 hat sich die Schnittstelle nicht verändert. Die Effizienz der Implementierung kann jedoch durch die Einführung der Variablen m_cache deutlich gesteigert werden. Beide Versionen der Komponente sollten austauschbar sein, da die Schnittstelle bei beiden gleich ist. Die Vermischung von Schnittstelle und Implementierung zeigt jedoch auch hier ihre Schwächen: Die beiden Komponenten sind nicht austauschbar, da die Objekte beider Klassen verschiedene Größen haben. Ein Objekt der Version 1.0 hat eine Größe von 4 Byte (sizeof(long) == 4), während ein Objekt der Version 2.0 aufgrund der dazugekommenen Variable die doppelte Größe hat. Obwohl die privaten Variablen die Schnittstelle der Komponente nicht verändern, wirken sie sich doch auf die äußeren Eigenschaften der Komponente aus. Die Komponente ist nicht in der Lage, die Größe ihrer internen Variablen zu verstecken. Die Größe der internen Variablen wirkt sich direkt auf die Speichergröße der
Das Komponentenobjektmodell
247
Komponente selbst aus. Damit ist die Kapselung der Implementierung unvollständig. Ein Programm, das eine Komponente der Version 1.0 erwartet, wird genau 4 Byte für diese bereitstellen. Wird in Wirklichkeit aber bereits die Komponente der Version 2.0 verwendet, dann kommt es mit hoher Wahrscheinlichkeit zum Programmabsturz, da die Implementierung der Komponente davon ausgeht, dass ihr 8 Byte Speicherplatz zur Verfügung stehen. Der Speicherplatz für m_cache gehört der Komponente aber gar nicht. Aufgrund der Vermischung von Schnittstelle und Implementierung kann die Verwendung verschiedener Versionen von C++Komponenten sehr gefährlich werden. Man muss sich darüber im Klaren sein, dass eine Veränderung der Implementierung einer Klasse sich auf die Größe der Objekte der Klasse auswirken kann. Für universell verwendbare Softwarekomponenten, die in der Lage sein müssen, in verschiedenen Versionen zusammenarbeiten zu können, ist die direkte Verwendung von C++-Klassen als Softwarekomponenten daher ungeeignet. Zusammenfassend lässt sich sagen, dass die Verwendung von C++Klassen als Softwarekomponenten aus einer ganzen Reihe von Gründen nicht praktikabel ist. Diese Gründe sind insbesondere: 왘 Die Kapselung der Implementierung von C++-Klassen ist unvollständig. Dies gilt besonders wenn C++-Klassen Bestandteil einer Klassenhierarchie sind. Arbeitet man mit der Vererbung von Implementierungen, läuft man Gefahr, Details dieser Implementierung offen zu legen. 왘 C++-Klassen sind meist keine Black-Box-Implementierungen. Dies gilt wiederum besonderes, wenn Vererbung im Spiel ist. 왘 C++-Compiler produzieren normalerweise monolithische, ausführbare Dateien. Die Verwendung von DLLs löst das Problem nicht, da das Name Mangling des Compilers eine Sprach- und Compiler-Unabhängigkeit verhindert. 왘 Die Sprache C++ bietet keine saubere Trennung zwischen Schnittstelle und Implementierung. C++ kennt keine expliziten Schnittstellen, wie sie beispielsweise die Sprache Java kennt. 왘 Die Schnittstelle einer C++-Klasse wird durch ihre Position im Vererbungsgrafen mitbestimmt. Das ist für eine Softwarekomponente inakzeptabel, da sie so nicht frei austauschbar ist.
C++-Klassen sind keine Komponenten
248
3
COM, OLE und ActiveX
Es sei angemerkt, dass auch andere Programmiersprachen viele der Anforderungen an universell einsetzbare Softwarekomponenten nicht erfüllen. Daher bietet es sich an, ein Komponentenmodell einzuführen, das von der Implementierungssprache unabhängig ist. Genau das ist COM.
3.2.3 COM-Eigenschaften
Eigenschaften des Komponentenobjektmodells COM
COM ist ein Modell für die Erstellung programmiersprachenunabhängiger universell einsetzbarer Softwarekomponenten. COM wurde entwickelt, weil sich mit Hilfe normaler Programmiersprachen universell verwendbare Softwarekomponenten nicht oder nur sehr schwer erstellen lassen. Stattdessen wurde mit COM ein Objektmodell entwickelt, das sich an alle gängigen Programmiersprachen anpassen lässt. COM besitzt insbesondere folgende Eigenschaften: 왘 COM ist programmiersprachenunabhängig. COM-Anpassungen existieren für viele gängige Programmiersprachen wie C, C++, Java und Visual Basic. 왘 COM verzichtet auf die Vererbung von Verhalten. Das heißt, dass Vererbung zwar innerhalb einer Komponente durch die Implementierungssprache verwendet werden kann, außerhalb der Komponente kann diese ihre Implementierung jedoch nicht vererben. Die COM-Designer haben die Vererbung als ein Mittel zum Aufbrechen der Kapselung von Komponenten erkannt. Daher ist die Vererbung von Implementierungen in COM nicht erlaubt. 왘 Im Gegensatz zur Sprache C++ sind Schnittstellen in COM von der Implementierung völlig getrennt. In COM besitzen Schnittstellen eigene Bezeichner. Eine Komponente kann mehrere Schnittstellen haben. Ein Zugriff auf die Interna einer Komponente kann ausschließlich über deren Schnittstellen erfolgen. COM-Schnittstellen verfügen ausschließlich über Funktionen; Variablen sind nicht Bestandteil einer Schnittstelle. 왘 Schnittstellen können – oder besser gesagt müssen – in COM vererbt werden. Durch die Vererbung von Schnittstellen wird die Spezifikation von Verhalten vererbt, nicht die Implementierung. Eine Schnittstelle ist schließlich nichts anderes als eine Spezifikation von Verhalten. 왘 Komponenten (COM-Klassen) und Schnittstellen sind bei Benutzung von COM weltweit eindeutig identifizierbar. Zur
Das Komponentenobjektmodell
249
Identifizierung werden 128 Bit lange Integer-Zahlen, die Globally Unique Identifier, kurz GUID, verwendet. 왘 COM besitzt eine eingebaute Referenzzählung. Nicht mehr referenzierte Komponenten löschen sich selbstständig. 왘 COM implementiert ein Client-Server-Modell. COM-Komponenten werden als COM-Server bezeichnet. Die Programme, die sich der COM-Komponenten bedienen, heißen COM-Clients.
3.2.4
Schnittstellen
Nähert man sich COM zum ersten Mal, dann sollte man sich zunächst mit dem Konzept der Schnittstelle auseinander setzen. Auf COM-Komponenten kann in jedem Fall nur über Schnittstellen zugegriffen werden. Es ist wichtig zu verstehen, dass eine Schnittstelle selbst keine Klasse oder Komponente ist. Eine Schnittstelle implementiert keine Funktionalität. Sie ist nur der Zugang zur Implementierung. Jede Schnittstelle besteht aus einer Reihe von Funktionen. Variablen können nicht Teil einer Schnittstelle sein und es kann keine Instanzen von Schnittstellen geben.
Aufbau von COM-Schnittstellen
Jede Schnittstelle muss von einer anderen Schnittstelle abgeleitet werden, von der sie Funktionen – im COM-Sprachgebrauch Methoden – erbt. Die Basisschnittstelle für alle anderen Schnittstellen heißt IUnknown, von ihr müssen alle weiteren Schnittstellen direkt oder indirekt abgeleitet werden. Per Konvention beginnen alle COM-Schnittstellen mit dem großen Buchstaben »I«, der für Interface steht.
COM-Schnittstellen sind per Definition unveränderlich. Eine Schnittstelle darf daher nicht erweitert werden. Sollten Änderungen an einer Schnittstelle notwendig werden, so ist eine neue Schnittstelle zu definieren. Dadurch, dass sich Schnittstellen nicht ändern dürfen, werden Versionsprobleme vermieden. Verwendet ein Programm eine Schnittstelle, so kann es sich darauf verlassen, dass die Schnittstelle immer ihrer ursprünglichen Definition entspricht. In C++ werden COM-Schnittstellen auf C++-Klassen abgebildet, die ausschließlich rein virtuelle Funktionen haben. Listing 3.2 zeigt die Definition einer COM-Schnittstelle mit dem Namen ISquare in C++. #include "unknwn.h" interface ISquare : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetSquare (long nValue, long *pnResult) = 0; }; Listing 3.2: Definition einer COM-Schnittstelle
Für COM-Schnittstellen ist Mehrfachvererbung nicht zugelassen. Eine COM-Klasse kann jedoch mehrere Schnittstellen implementieren, so dass sich aus dem Verzicht auf Mehrfachvererbung bei Schnittstellen keine Nachteile ergeben.
3.2.5 Referenzzählung
COM-Klassen implementieren die Funktionalität von Komponenten. Im Gegensatz zu C++-Klassen können COM-Klassen keine Konstruktoren und Destruktoren über ihre Schnittstellen nach außen zugänglich machen. Instanzen von COM-Klassen werden über einen Referenzzählmechanismus verwaltet. Gibt es keine Referenzen mehr auf eine Instanz einer COM-Klasse, dann löscht sich diese Instanz selbst. Eine COM-Komponente kann mehrere Klassen implementieren.
3.2.6 IIDs und CLSIDs
COM-Klassen
GUIDs
GUIDs sind 128 Bit lange Integer-Zahlen. Wie bereits erwähnt, werden COM-Schnittstellen durch die Zuweisung von GUIDs weltweit eindeutig identifizierbar gemacht (siehe Kasten). Eine zur Identifizierung einer Schnittstelle verwendete GUID wird als Interface Identifier, IID, bezeichnet. Neben Schnittstellen werden auch COM-Klassen durch GUIDs identifiziert. Im Fall einer zur Identifizierung einer
Das Komponentenobjektmodell
COM-Klasse verwendeten GUID spricht man von einem Class Identifier, CLSID. Listing 3.3 zeigt die IID von IUnknown. {00000000-0000-0000-C000-000000000046} Listing 3.3: IID von IUnknown
GUIDs werden durch die Funktion CoCreateGuid der COMLaufzeitbibliothek erzeugt. Diese Funktion ruft ihrerseits die API-Funktion UuidCreate auf, die die eigentliche Arbeit ausführt. Das im Text beschriebene Programm GUIDGEN verwendet intern diese Funktionen, um GUIDs zu erzeugen. Daneben gibt es das Programm UUIDGEN, das in einer DOS-Box läuft und ebenfalls GUIDs erzeugt. Der Algorithmus von UuidCreate erzeugt laut Microsoft »mit einem sehr hohen Grad der Sicherheit« keine doppelten GUIDs. Diese Aussage gilt für alle weltweit erzeugten GUIDs. Wenn man jedoch weiß, welche Grundlage der Algorithmus für seine Berechnungen verwendet, kann man diesen sehr hohen Grad an Sicherheit noch etwas erhöhen. Zum einen stützt sich UuidCreate auf die aktuelle Zeit, um einen eindeutigen Wert zu erhalten. Da jedoch viele Uhren innerhalb von Computern falsch gehen und UuidCreate weltweit sicherlich zur gleichen Zeit mehrfach aufgerufen wird, ist dies keinesfalls ausreichend. Natürlich hilft der sehr große Wertebereich von 128 Bit. Praktisch sollten in diesem Wertebereich keine doppelten Einträge auftreten, aber auch das ist keinesfalls sicher. Falls der Rechner, auf dem die Funktion UuidCreate ausgeführt wird, eine Netzwerkkarte (Ethernet oder Token Ring) besitzt, so wird auch deren Netzwerkadresse als Grundlage zur Berechnung der GUID verwendet. Die Netzwerkadressen solcher Karten werden weltweit eindeutig vergeben, das heißt, jede Netzwerkkarte hat ihre eigene Adresse (diese HardwareAdresse sollte nicht mit der IP-Adresse eines Computers verwechselt werden). Entwickelt man Software, die später an eine größere Zahl von Kunden ausgeliefert werden soll, so empfiehlt es sich, die GUIDs auf einem Computer mit Netzwerkkarte erzeugen zu lassen, da sie dann mit höherer Wahrscheinlichkeit eindeutig sind (die letzte Fehlerquelle liegt jetzt beim Netzwerkkartenhersteller). Das Programm UUIDGEN gibt eine Warnung aus, wenn es keine Netzwerkkarte finden konnte, das Programm GUIDGEN leider nicht.
251
252
3 Das Programm GUIDGEN
COM, OLE und ActiveX
Um eindeutige GUIDs zu erzeugen, gibt es im Windows-API die Funktion CoCreateGuid. Man kann dazu jedoch auch das Hilfsprogramm GUIDGEN aufrufen, das sich im Verzeichnis MICROSOFT VISUAL STUDIO .NET\VC7\BIN befindet. GUIDGEN kann die erzeugte GUID bereits in alle gängigen Darstellungsformate konvertieren (Abbildung 3.4).
Abbildung 3.4: GUID-Erzeugung mit dem Programm GUIDGEN
Da gängige C++-Compiler keinen Datentyp für 128 Bit lange Integer-Zahlen bereitstellen, wird im COM-API dafür der Datentyp GUID definiert. IID und CLSID sind ebenfalls als Typen definiert.
3.2.7
Die COM-Notation
Für COM wurde eine eigene Notation entwickelt, mit der sich Komponenten grafisch darstellen lassen. Abbildung 3.5 zeigt eine Komponente, die die Schnittstellen IUnknown, ISquare und ISquareRoot implementiert. Jede Schnittstelle wird in dieser Notation durch einen eigenen »Anschluss« bezeichnet. Der Anschluss für IUnknown wird üblicherweise nach oben hin ausgeführt, da IUnknown als Basisschnittstelle von allen COM-Komponenten implementiert werden muss. Die Komponente selbst wird als Rechteck mit abgerundeten Ecken dargestellt.
Das Komponentenobjektmodell
253 IUnknown
ISquare ISquareRoot
Abbildung 3.5: Komponente in COM-Notation
3.2.8
Die Schnittstelle IUnknown
IUnknown ist die Basisschnittstelle, von der alle anderen COMSchnittstellen abgeleitet werden. Listing 3.4 zeigt die Definition dieser Schnittstelle. interface IUnknown { public: virtual HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, void **ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0; virtual ULONG STDMETHODCALLTYPE Release(void) = 0; }; Listing 3.4: Definition von IUnknown
Die Methoden AddRef und Release dienen zur Instanzzählung von COM-Objekten. Der Aufruf von AddRef bewirkt, dass im COMObjekt der Referenzzähler um 1 erhöht wird. Release dekrementiert den Referenzzähler. Ist der Referenzzähler bei null angekommen, löscht sich das COM-Objekt selbsttätig.
AddRef und Release
Über die Methode QueryInterface können Zeiger auf andere von der Klasse implementierte Schnittstellen angefordert werden. Damit lassen sich zwar Schnittstellen eines COM-Objekts durch Ausprobieren erfragen, normalerweise sollte der Programmierer aber bereits wissen, welche Schnittstelle er anfordern möchte. Zur Identifizierung der gewünschten Schnittstelle wird QueryInterface eine Referenz auf eine IID übergeben, die die angeforderte Schnittstelle eindeutig beschreibt. Unterstützt die Komponente die angeforderte Schnittstelle, so gibt QueryInterface einen Zeiger auf die Schnittstelle zurück und erhöht selbsttätig den Referenzzähler. Man kann dann sofort Methoden der angeforderten Schnittstelle
QueryInterface
254
3
COM, OLE und ActiveX
aufrufen. Benötigt man die Dienste einer Schnittstelle nicht mehr, dann gibt man sie durch den Aufruf von Release wieder frei.
t
Bei COM wird oft mit Zeigern auf Schnittstellenzeiger gearbeitet. Wie bei der Methode QueryInterface werden dabei Zeiger auf Zeiger auf void verwendet. Die doppelte Verwendung von Zeigern ist notwendig, damit die Methode QueryInterface einen Zeiger auf eine COM-Schnittstelle an seinen Aufrufer zurückliefern kann. Zeiger auf void werden verwendet, damit der Schnittstellenzeiger selbst beliebige Typen annehmen kann. Übergibt man einen Zeiger an eine C++-Funktion, die einen Zeiger auf einen Zeiger auf void erwartet, dann kann der Compiler keine automatische Typkonvertierung vornehmen. Dies ist eine Eigenheit der Sprache C++. Man wird daher bei der COM-Programmierung oft auf explizite Typumwandlungen auf void** stoßen!
3.2.9 IClassFactory
Klassenfabriken
Eine COM-Komponente muss Instanzen ihrer COM-Klassen selbst anlegen können. Obwohl theoretisch jede COM-Klasse ihren eigenen Mechanismus zur Erzeugung von Instanzen implementieren könnte, ist es ganz im Sinne des COM-Gedankens, hierfür eine COM-Schnittstelle zu verwenden, die von allen Komponenten implementiert wird. Diese Schnittstelle heißt IClassFactory (es gibt außerdem eine neuere Version dieser Schnittstelle mit dem Namen IClassFactory2, die lizenzierte Komponenten unterstützt). Die Klasse, die IClassFactory implementiert, wird als Klassenfabrik bezeichnet. Der Name Klassenfabrik ist irreführend und von Microsoft schlecht gewählt, da die Klassenfabrik Objekte fabriziert und keine Klassen. Der Ausdruck Objektfabrik wäre treffender. Die Implementierung der Klassenfabrik muss statisch erfolgen, schließlich ist die Klassenfabrik selbst ein COM-Objekt, und es ist noch niemand da, der eine Instanz der Klassenfabrik erstellen könnte. Da die Klassenfabrik statisch ist, kann es immer nur eine Instanz von ihr geben. Im Sinne der Entwurfsmuster ist die Klassenfabrik ein Singleton (siehe das Buch von Gamma et al. im Literaturverzeichnis). Abbildung 3.6 zeigt eine Komponente mit Klassenfabrik. Die Schnittstelle IClassFactory definiert zwei Methoden, CreateInstance und LockServer. Listing 3.5 zeigt die Definition von IClassFactory.
Das Komponentenobjektmodell
255 IUnknown
IUnknown
Klassenfabrik (statisch)
IClassFactory
COM-Objekt (dynamisch)
ISomeInterface COM-Server Abbildung 3.6: Klassenfabrik und Objektinstanz
Mit der Methode CreateInstance kann eine Instanz eines COMObjekts erstellt werden. Über den Parameter riid wird dabei zugleich eine Schnittstelle angegeben, mit der auf diese Instanz zugegriffen werden kann. Die Methode LockServer kann dazu verwendet werden, den COM-Server zwangsweise im Speicher zu halten. Normalerweise muss diese Methode jedoch nicht aufgerufen werden.
3.2.10 Die COM-Laufzeitbibliothek Um die Arbeit mit COM-Servern unter Windows zu unterstützen, gibt es als Teil des Windows-API die COM-Laufzeitbibliothek. Mit Hilfe der COM-Laufzeitbibliothek können beispielsweise COMServer im System gefunden und gestartet werden. Funktionen der Bibliothek beginnen mit dem Präfix »Co«. Die bereits erwähnte Funktion CoCreateGuid ist eine Funktion der COM-Laufzeitbibliothek. Damit die COM-Laufzeitbibliothek verwendet werden kann,
256
3
COM, OLE und ActiveX
muss sie zunächst initialisiert werden. Die COM-Bibliothek selbst stellt dafür die Funktionen CoInitialize und CoUninitialize zur Verfügung. In MFC-Programmen ruft man jedoch stattdessen zu Beginn des Programms, also normalerweise in der Funktion InitInstance des Applikationsobjekts, die Funktion AfxOleInit auf. Diese initialisiert nicht nur die COM-Laufzeitbibliothek, sondern auch die OLE-DLLs.
3.2.11 Verwendung eines COM-Servers Um die bisher vorgetragene COM-Theorie mit etwas Leben zu erfüllen, soll zunächst die Verwendung eines COM-Servers gezeigt werden. Dieser Server implementiert sowohl eine Klassenfabrik als auch eine über IUnknown hinausgehende Schnittstelle. Diese Schnittstelle heißt ISquare und implementiert genau eine Methode, die das Quadrat einer Integer-Zahl berechnet. Die Definition dieser Schnittstelle wurde bereits in Listing 3.2 aus Abschnitt 3.2.4, »Schnittstellen«, gezeigt. Abbildung 3.7 zeigt den Server in COM-Notation.
IUnknown
ISquare
Abbildung 3.7: ISquare-Server in COM-Notation
Damit die Verwendung eines solchen COM-Servers gezeigt werden kann, wird zunächst eine Implementierung eines solchen Servers benötigt. Ganz im Sinne von COM soll der Server als Black Box betrachtet werden, die Implementierung ist nicht interessant. Das Visual C++-Projekt eines solchen Servers befindet sich auf der Begleit-CD im Verzeichnis KAPITEL3\ATLSERVER. Damit der Server verwendet werden kann, muss das Projekt auf die Festplatte kopiert und übersetzt werden. Der Server wird dabei automatisch registriert. (Alle COM-Server müssen vor der Verwendung registriert werden. Wie man dies bewerkstelligt, wird in Abschnitt 3.2.14, »Registrierung von COM-Servern«, beschrieben.)
Das Komponentenobjektmodell
Im Verzeichnis KAPITEL3\SQUARECLIENT der Begleit-CD befindet sich das Programm SquareClient. Dieses Programm implementiert einen COM-Client, der auf vier verschiedene Implementierungen eines COM-Servers zugreifen kann. Die vier Server implementieren alle die ISquare-Schnittstelle, sind aber unterschiedlich realisiert. Der erste Server ist die bereits erwähnte Black BoxImplementierung. Die drei anderen Server werden im Folgenden besprochen.
257 Beispielprogramm SquareClient
Das Programm SquareClient ist als dialogfeldbasierende Anwendung mit dem Anwendungs-Assistenten erstellt worden. Mit vier Optionsfeldern wird der gewünschte COM-Server ausgewählt. In einem Eingabefeld kann eine Integer-Zahl eingegeben werden. Besteht eine Verbindung mit einem COM-Server, so wird nach einem Klick auf OK das Quadrat der Zahl durch den COM-Server berechnet, an den Client zurückgegeben und dann von diesem ausgegeben. Kann die Verbindung zum gewählten COM-Server nicht aufgebaut werden, so wird eine Fehlermeldung angezeigt. Abbildung 3.8 zeigt das Programm SquareClient.
Abbildung 3.8: Das Programm SquareClient
Bei der Erstellung des Programms SquareClient mit dem Anwendungs-Assistenten sind die Optionen für ActiveX und für Automation nicht ausgewählt worden. Damit man trotzdem Funktionen der Windows-COM-Bibliothek aufrufen kann, ist es notwendig, die Header-Datei AFXOLE.H von Hand einzubinden und in CSquareClientApp::InitInstance die Funktion AfxOleInit aufzurufen. Damit wird die COM-Bibliothek initialisiert. Listing 3.6 zeigt die Implementierungsdatei der Klasse CSquareClientDlg, in der die Verwendung der COM-Server implementiert ist.
Das Komponentenobjektmodell CSquareClientDlg::CSquareClientDlg(CWnd* pParent /*=NULL*/) : CDialog(CSquareClientDlg::IDD, pParent), m_pISquare (NULL) { m_strOut = _T(""); m_nIn = 0; m_strMessage = _T(""); // Beachten Sie, dass LoadIcon unter Win32 keinen // nachfolgenden DestroyIcon-Aufruf benötigt m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } CSquareClientDlg::~CSquareClientDlg () { // Wenn noch eine ISquare-Schnittstelle gehalten wird, // diese freigeben if (m_pISquare) { m_pISquare->Release (); } } void CSquareClientDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, IDC_STATIC_OUT, m_strOut); DDX_Text(pDX, IDC_EDIT1, m_nIn); DDX_Text(pDX, IDC_STATIC_MSG, m_strMessage); } BEGIN_MESSAGE_MAP(CSquareClientDlg, CDialog) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_BN_CLICKED(IDC_RADIO1, OnRadioATL) ON_BN_CLICKED(IDC_RADIO2, OnRadioWIN32) ON_BN_CLICKED(IDC_RADIO3, OnRadioWIN32Nested) ON_BN_CLICKED(IDC_RADIO4, OnRadioMFC) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// / // CSquareClientDlg Nachrichten-Handler BOOL CSquareClientDlg::OnInitDialog() { CDialog::OnInitDialog(); // Hinzufügen des Menübefehls "Info... " zum Systemmenü. // IDM_ABOUTBOX muss sich im Bereich der Systembefehle // befinden.
259
260
3
COM, OLE und ActiveX
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // Symbol für dieses Dialogfeld festlegen. // Wird automatisch erledigt, wenn das Hauptfenster der // Anwendung kein Dialogfeld ist SetIcon(m_hIcon, TRUE); // Großes Symbol verwenden SetIcon(m_hIcon, FALSE); // Kleines Symbol verwenden // Zunächst Eingabefeld abschalten EnableInput (false); return TRUE; } void CSquareClientDlg::OnSysCommand(UINT nID, LPARAM lParam) { if ((nID & 0xFFF0) == IDM_ABOUTBOX) { CAboutDlg dlgAbout; dlgAbout.DoModal(); } else { CDialog::OnSysCommand(nID, lParam); } } // Wollen Sie Ihrem Dialogfeld eine Schaltfläche "Minimieren" // hinzufügen, benötigen Sie den nachstehenden Code, um das // Symbol zu zeichnen. Für MFC-Anwendungen, die das // Dokument/Ansicht-Modell verwenden, wird dies automatisch für // Sie erledigt. void CSquareClientDlg::OnPaint() {
Das Komponentenobjektmodell if (IsIconic()) { CPaintDC dc(this); // Gerätekontext für Zeichnen SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Symbol in Client-Rechteck zentrieren int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; // Symbol zeichnen dc.DrawIcon(x, y, m_hIcon); } else { CDialog::OnPaint(); } } // Die Systemaufrufe fragen die Cursorform ab, die angezeigt // werden soll, während der Benutzer das zum Symbol // verkleinerte Fenster mit der Maus zieht. HCURSOR CSquareClientDlg::OnQueryDragIcon() { return (HCURSOR) m_hIcon; } void CSquareClientDlg::OnRadioATL() { // CLSID des ATL-Servers static const CString strCLSID (_T("{547E8381-F175-11D1-90A2-D0DD2F7ACA49}")); if (AttachToServer (strCLSID)) { m_strMessage = _T("Verbunden mit ATL-Server: ") + strCLSID; EnableInput (true); } else { m_strMessage = _T("Verbindung mit ATL-Server konnte " "nicht hergestellt werden."); EnableInput (false); }
261
262
3
COM, OLE und ActiveX
UpdateData (false); } void CSquareClientDlg::OnRadioWIN32() { // CLSID des WIN32-Servers static const CString strCLSID (_T("{B8EF6063-F1C2-11d1-90A2-D0DD2F7ACA49}")); if (AttachToServer (strCLSID)) { m_strMessage = _T("Verbunden mit WIN32-Server: ") + strCLSID; EnableInput (true); } else { m_strMessage = _T("Verbindung mit WIN32-Server " "konnte nicht hergestellt werden."); EnableInput (false); } UpdateData (false); } void CSquareClientDlg::OnRadioWIN32Nested() { // CLSID des CPP-Servers (nested) static const CString strCLSID (_T("{E3C91780-F24F-11d1-90A2-D0DD2F7ACA49}")); if (AttachToServer (strCLSID)) { m_strMessage = _T("Verbunden mit WIN32-Server (nested): ") + strCLSID; EnableInput (true); } else { m_strMessage = _T("Verbindung mit WIN32-Server (nested) " "konnte nicht hergestellt werden."); EnableInput (false); } UpdateData (false); } void CSquareClientDlg::OnRadioMFC() {
Das Komponentenobjektmodell // CLSID des MFC-Servers static const CString strCLSID (_T("{4831F131-F290-11D1-90A2-D0DD2F7ACA49}")); if (AttachToServer (strCLSID)) { m_strMessage = _T("Verbunden mit MFC-Server: ") + strCLSID; EnableInput (true); } else { m_strMessage = _T("Verbindung mit MFC-Server konnte nicht" "hergestellt werden."); EnableInput (false); } UpdateData (false); } //////////////////////////////////////////////////////////// // AttachToServer // // Diese Funktion baut die Verbindung mit dem COM-Server auf. // BOOL CSquareClientDlg::AttachToServer(const CString & strCLSID) { // IID für ISquare als String: static const CString strIIDSquare (_T("{547E8380-F175-11D1-90A2-D0DD2F7ACA49}")); CLSID clsid; IID iid; HRESULT hRes; IUnknown *pIUnknown; IClassFactory *pIClassFactory; // Wenn noch eine ISquare-Schnittstelle gehalten wird, // diese erst freigeben: if (m_pISquare) { m_pISquare->Release (); m_pISquare = NULL; } // String in eine CLSID umwandeln: if (FAILED(AfxGetClassIDFromString (strCLSID, &clsid))) { MessageBox (_T("CLSID hat ungültiges Format")); return false;
263
264
3
COM, OLE und ActiveX
} // String in eine IID umwandeln: if (FAILED(AfxGetClassIDFromString (strIIDSquare, &iid))) { MessageBox (_T("IID hat ungültiges Format")); return false; } // Zeiger auf IUnknown-Schnittstelle besorgen: hRes = CoGetClassObject (clsid, CLSCTX_ALL, NULL, IID_IUnknown, (void**)&pIUnknown); if (FAILED (hRes)) { ShowCOMError (hRes); return false; } // Fragen, ob das Klassenobjekt // eine Klassenfabrik implementiert: pIUnknown->QueryInterface (IID_IClassFactory, (void**)&pIClassFactory); // IUnknown-Schnittstelle freigeben: pIUnknown->Release (); if (!pIClassFactory) { MessageBox (_T("COM-Server hat keine Klassenfabrik!")); return false; } // Object instanziieren, ISquare-Schnittstelle anfordern: pIClassFactory->CreateInstance (NULL, iid, (void**)&m_pISquare); // IClassFactory-Schnittstelle freigeben: pIClassFactory->Release (); if (!m_pISquare) { MessageBox (_T("Konnte kein Objekt erzeugen")); m_pISquare = NULL; return false; } return true; } void CSquareClientDlg::ShowCOMError(HRESULT hRes) {
Das Komponentenobjektmodell CString strErr; switch (hRes) { case REGDB_E_CLASSNOTREG: strErr = _T("CLSID nicht registriert."); break; case CO_E_DLLNOTFOUND: strErr = _T("COM-Server-DLL nicht gefunden"); break; case CO_E_APPNOTFOUND: strErr = _T("COM-Server-EXE nicht gefunden"); break; case E_ACCESSDENIED: strErr = _T("Zugriffsfehler"); break; case CO_E_ERRORINDLL: strErr = _T("Fehler in COM-Server-Datei"); break; case CO_E_APPDIDNTREG: strErr = _T("Server gestartet, aber hat sich " "nicht registriert!"); break; default: strErr = _T("Unbekannter Fehler"); break; } MessageBox (strErr); } void CSquareClientDlg::OnOK() { // Nur ausführen, wenn eine Verbindung mit // einem COM-Server besteht: if (m_pISquare) { long nResult; UpdateData (true); // Methode des COM-Servers aufrufen: m_pISquare->GetSquare (m_nIn, &nResult); m_strOut.Format ("%li", nResult); UpdateData (false); }
265
266
3
COM, OLE und ActiveX
} void CSquareClientDlg::EnableInput(BOOL bEnable) { if (bEnable) GetDlgItem (IDC_EDIT1)->EnableWindow (true); else GetDlgItem (IDC_EDIT1)->EnableWindow (false); m_nIn = 0; m_strOut = _T(""); UpdateData (false); } Listing 3.6: Implementierungsdatei des Programms SquareClient Verbindung mit dem COM-Server
Die Verbindung zu einem COM-Server wird aufgebaut, indem man auf eins der Optionsfelder klickt. Für jedes Optionsfeld ist eine Nachrichtenbehandlungsfunktion angelegt worden. Von der Struktur her sind diese vier Funktionen – OnRadioATL, OnRadioWIN32, OnRadioWIN32Nested und OnRadioMFC – gleich. Die CLSID des anzusprechenden COM-Servers wird als konstanter String definiert. Durch Aufruf der Funktion CSquareClientDlg::AttachToServer wird versucht, eine Verbindung zum gewählten COM-Server aufzubauen. Je nachdem, ob die Verbindung hergestellt werden konnte oder nicht, wird eine Fehlermeldung oder eine Erfolgsmeldung in die DDX-Variable m_strMessage geschrieben. Diese Meldung wird durch den abschließenden Aufruf von UpdateData in ein statisches Textfeld des Dialogfelds übernommen und damit angezeigt. Die Funktion CSquareClientDlg::EnableInput sperrt das Eingabefeld gegen Eingaben, falls keine Verbindung mit dem COM-Server hergestellt werden konnte. Das Beispielprogramm SquareClient hält immer nur eine Verbindung zu maximal einem COM-Server. Wird eine Verbindung zu einem anderen COM-Server gewünscht, so muss eine eventuell noch bestehende Verbindung vorher abgebaut werden. Als Indikator, ob eine Verbindung zu einem COM-Server besteht, wird die Variable m_pISquare verwendet. Hat m_pISquare den Wert NULL, so bedeutet dies, dass keine COM-Server-Verbindung besteht. Werte ungleich NULL bedeuten, dass m_pISquare ein Zeiger auf eine gültige ISquare-Schnittstelle ist und somit eine Verbindung zu einem COM-Server besteht.
Das Komponentenobjektmodell
267
Die Verbindung mit dem COM-Server wird in der Funktion CSquareClientDlg::AttachToServer aufgebaut. Die IID der ISquareSchnittstelle wird als konstantes String-Objekt definiert. Dann wird geprüft, ob m_pISquare den Wert NULL hat. Falls m_pISquare noch auf eine gültige ISquare-Schnittstelle verweist, wird diese Schnittstelle durch den Aufruf der Funktion Release freigegeben. Anschließend werden die als Strings definierten CLSID und IID durch Aufruf der Funktion AfxGetClassIDFromString aus der String-Darstellung in GUIDs konvertiert. Falls bei der Konvertierung Fehler auftreten, werden diese in Messageboxen angezeigt. Das zur Fehlerüberprüfung verwendete Makro FAILED prüft lediglich, ob ein Wert vom Typ HRESULT negativ ist und damit einen Fehler anzeigt.
Verbindungsaufbau
Der anschließende Aufruf der Funktion CoGetClassObject ist ein zentraler Punkt bei der Arbeit mit COM-Servern. CoGetClassObject ist eine Funktion der COM-Laufzeitbibliothek. Durch den Aufruf dieser Funktion wird die Verbindung zu einem COM-Server hergestellt. CoGetClassObject sucht und lädt den gewünschten COM-Server. Zusätzlich ruft CoGetClassObject bereits einmal die Funktion QueryInterface der Komponente auf, um einen Schnittstellenzeiger anzufordern. Man muss nicht, wie im Beispielprogramm, einen Zeiger auf IUnknown anfordern. Man kann auch gleich eine über IUnknown hinausgehende Schnittstelle anfordern. Im Beispielprogramm wird ein Zeiger auf eine IUnknown-Schnittstelle angefordert, damit danach die Verwendung von QueryInterface gezeigt werden kann. CoGetClassObject bekommt als ersten Parameter die CLSID auf die gewünschte COM-Klasse, die von der Komponente implementiert wird. Über einen Eintrag in der Registrierungsdatenbank (Registry) von Windows kann damit der Pfad des COM-Servers bestimmt werden. Der zweite Parameter gibt an, welcher Typ von COM-Server verwendet werden soll. Hier gibt es die Möglichkeit, einen Server zu verwenden, der im gleichen Prozess läuft wie das aufrufende Programm, einen Server, der als eigener Prozess auf dem gleichen Computer läuft, oder einen Server, der auf einem anderen Computer im Netzwerk läuft. Der im Beispielprogramm übergebene Wert CLSCTX_ALL gibt an, dass jede Art von Server akzeptiert wird. Der dritte Parameter gibt einen Computer im Netzwerk an, falls der COM-Server nicht auf der lokalen Maschine gestartet werden soll. Im Fall von lokalen COM-Servern kann für diesen Parameter NULL übergeben werden. Als vierter Parameter wird die IID auf die gewünschte
CoGetClassObject
268
3
COM, OLE und ActiveX
Schnittstelle übergeben. Der fünfte Parameter ist ein Zeiger auf die angeforderte Schnittstelle. Schlägt der Zugriff auf den COM-Server fehl, dann gibt CoGetClassObject einen Fehler aus einer ganzen Reihe von möglichen Fehlercodes zurück. Im Beispielprogramm wird die Funktion ShowCOMError dazu verwendet, diese Fehlercodes zu entschlüsseln und in einer Messagebox anzuzeigen. QueryInterface
Der Aufruf von CoGetClassObject ist der einzige Aufruf einer APIFunktion aus der COM-Laufzeitbibliothek. Alle weiteren Zugriffe können nun direkt über COM-Schnittstellen erfolgen. Im Beispielprogramm wird zunächst durch den Aufruf von QueryInterface ein Zeiger auf die Klassenfabrik des COM-Servers angefordert. Eine mit QueryInterface angeforderte Schnittstelle ist gültig, sofern der zurückgegebene Schnittstellenzeiger ungleich NULL ist. Da die IUnknown-Schnittstelle nach Anforderung des Zeigers auf IClassFactory nicht mehr benötigt wird, kann sie durch einen Aufruf von Release freigegeben werden.
CreateInstance
Durch Aufruf von IClassFactory::CreateInstance wird dann ein COMObjekt angelegt. CreateInstance gibt einen Zeiger auf eine Schnittstelle des erzeugten Objekts zurück. Diesmal soll der Umweg über IUnknown gespart werden, deshalb wird gleich ein Schnittstellenzeiger auf ISquare angefordert. An dieser Stelle ist es wichtig zu verstehen, dass der jetzt erhaltene Schnittstellenzeiger auf ein von der Klassenfabrik erzeugtes COM-Objekt zeigt. Die vorher verwendeten Schnittstellenzeiger waren hingegen Zeiger auf die Klassenfabrik selbst. Ein von der Funktion CoGetClassObject zurückgegebener Zeiger auf eine IUnknown-Schnittstelle ist nicht mit einem von der Methode CreateInstance zurückgegebenen Zeiger identisch, beide Zeiger zeigen auf verschiedene COM-Objekte. Der erste Zeiger zeigt auf die Klassenfabrik des COM-Servers, der zweite auf ein COM-Objekt, das durch die Klassenfabrik hergestellt worden ist. Nachdem die Instanz des COM-Objekts erzeugt worden ist, kann auch der Zeiger auf die Klassenfabrik durch den Aufruf von Release freigegeben werden. Der Zeiger m_pISquare auf die Schnittstelle ISquare wird jedoch auch nach Verlassen der Funktion AttachToServer gehalten.
Berechnung des Quadrats
Die Berechnung des Quadrats der eingegebenen Zahl wird in CSquareClientDlg::OnOK durchgeführt und gestaltet sich einfach. Wenn der Schnittstellenzeiger m_pISquare gültig ist, dann wird zunächst durch den Aufruf der Funktion UpdateData der Wert aus dem Eingabefeld in die DDX-Variable m_nIn übernommen.
Das Komponentenobjektmodell
Danach wird die Funktion GetSquare der Schnittstelle ISquare aufgerufen. Das COM-Objekt berechnet das Quadrat von m_nIn und reicht es in der Variablen nResult zurück. Das Ergebnis wird dann in einen String konvertiert und per UpdateData in den Dialog zurückgeschrieben.
Zwischenbilanz Wie man im gerade gezeigten Beispiel gesehen hat, ist die Verwendung eines COM-Servers relativ einfach. Man muss den gewünschten Server lediglich über seine CLSID identifizieren und eine erste Schnittstelle mit der Funktion CoGetClassObject anfordern. Über die Schnittstelle IClassFactory werden dann normalerweise Instanzen von Objekten erzeugt. Möchte man nur ein einziges Objekt instanziieren, so kann dies sogar noch einfacher geschehen, indem man die Funktion CoCreateInstance verwendet. Im Fall von CoCreateInstance muss man sich nicht selbst um die Ansprache der Klassenfabrik kümmern; die COM-Laufzeitbibliothek übernimmt diese Aufgabe.
3.2.12 Wie man einen COM-Server implementiert Im Gegensatz zur Verwendung von COM-Servern in eigenen Programmen ist das Schreiben von COM-Servern aufwändiger. Zudem ist es eindeutig von Vorteil, die COM zugrunde liegende Architektur auch auf unterster Ebene zu verstehen, wenn man eigene COM-Server implementieren möchte. Frameworks wie MFC und ATL verdecken die Implementierungsdetails eher, als dass sie erkennen lassen, wie COM auf Implementierungsebene funktioniert. Selbstverständlich helfen Klassenbibliotheken und Frameworks andererseits, die Implementierung von COM-Servern deutlich zu vereinfachen. Es soll daher im weiteren Verlauf dieses Kapitels auf eine Implementierung eines COM-Servers mit der Schnittstelle ISquare mit Hilfe der MFC hingearbeitet werden. Zuvor soll jedoch ein COM-Server mit gleicher Funktionalität ganz »zu Fuß«, also nur mit Hilfe von C++ und ohne die MFC, implementiert werden. Diese Implementierung wird das Wesen von COM wesentlich deutlicher machen, als es eine MFC- oder ATL-Implementierung könnte. Als Übergang zwischen den beiden Servern dient ein dritter Server, der bereits Implementierungstechniken der MFC verwendet, ohne die MFC selbst zu benutzen.
269
270
3
COM, OLE und ActiveX
3.2.13 Das Beispielprogramm CppServer Prozessinterner Server
Das Projekt zum Beispielprogramm CppServer befindet sich auf der Begleit-CD im Verzeichnis KAPITEL3\CPPSERVER. Der Server ist als so genannter prozessinterner Server realisiert, das heißt, er läuft im selben Adressraum wie das aufrufende Programm. Als Folge davon muss der Server in einer DLL implementiert werden. COM bietet auch andere Ausführungsmodelle, die später in diesem Kapitel besprochen werden. Der COM-Server muss mehrere Dinge implementieren: die COMKlasse mit der ISquare-Schnittstelle, eine Klassenfabrik für diese COM-Klasse sowie einige typische, für prozessinterne Server verwendete Funktionen. Das Projekt CppServer ist als WIN32-DLL angelegt worden, die Implementierung des Programms besteht lediglich aus den beiden Dateien CPPSERVER.H und CPPSERVER.CPP. Listing 3.7 zeigt die Header-Datei. #ifndef __cppserver__ #define __cppserver__ #include "unknwn.h" interface ISquare : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetSquare (long value, long *pnResult) = 0; }; class CPPServer : public ISquare { public: // IUnknown-Methoden HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, void **ppvObj); ULONG STDMETHODCALLTYPE AddRef (); ULONG STDMETHODCALLTYPE Release (); // ISquare-Methoden HRESULT STDMETHODCALLTYPE GetSquare (long value, long *pnResult); CPPServer (); private: DWORD m_dwRefCount;
In Listing 3.7 wird zunächst die Schnittstelle ISquare definiert. Der bei COM-Schnittstellen verwendete Modifizierer STDMETHODCALLTYPE wird generell für COM-Methoden verwendet. Unter Windows wird STDMETHODCALLTYPE auf __stdcall zurückgeführt, er gibt also an, wie Parameter auf dem Stack übergeben werden. Statt STDMETHODCALLTYPE kann auch STDMETHODIMP verwendet werden, was den Rückgabewert vom Typ HRESULT gleich mitdefiniert. Nach Definition der Schnittstelle wird die C++-Klasse CPPServer deklariert. Diese Klasse modelliert die COM-Klasse des Servers und implementiert seine ISquare-Schnittstelle. Danach wird mit der Klasse CPPServerFactory die Klassenfabrik des COM-Servers deklariert. Die Klassenfabrik muss die IClassFactory-Schnittstelle implementieren. Beide Klassen müssen IUnknown implementieren und beide Klassen deklarieren eine private Variable namens m_dwRefCount für die Referenzzählung der IUnknown-Schnittstelle. Listing 3.8 zeigt die Implementierung dieser Klassen und einige weitere Funktionen des COM-Servers.
handle to DLL module reason for calling function reserved wird in DllRegisterServer
return TRUE; } /////////////////////////////////////// // Funktionen zum Setzen des DLL-Locks void DllLockServer (void) { nServerLockCount++; } void DllUnlockServer (void) { nServerLockCount--; } Listing 3.8: Implementierungsdatei des Beispielprogramms CppServer
278
3
COM, OLE und ActiveX
Die Implementierungsdatei definiert zunächst die GUIDs der Schnittstelle ISquare und der COM-Serverklasse. Danach werden einige globale Variablen deklariert. Der Instanz-Handle hInstance der DLL wird zur Registrierung des COM-Servers benötigt. Durch die Variable nServerLockCount kann festgestellt werden, ob der Server noch benötigt wird oder ob er aus dem Speicher entfernt werden kann. Die anschließend deklarierten Funktionen DllLockServer und DllUnlockServer dienen zum Inkrementieren beziehungsweise Dekrementieren dieser Variablen. Die Variable serverFactory ist die einzige Instanz der Klassenfabrik des COM-Servers. Als Erstes erfolgt die Implementierung der Klasse CPPServer und damit auch die Implementierung der COM-Klasse. Da COM keine Implementierungsvererbung kennt, müssen alle Methoden des COM-Servers implementiert werden. Implementierung der Referenzzählung
Die Implementierung der Referenzzählung folgt dem in Abschnitt 3.2.8, »Die Schnittstelle IUnknown«, beschriebenen Schema. Im Konstruktor der Klasse CPPServer wird der Referenzzähler auf 0 gesetzt. Bei jedem Aufruf von QueryInterface oder AddRef wird dieser Zähler inkrementiert, bei jedem Aufruf von Release dekrementiert. Erreicht der Referenzzähler wieder den Wert 0, dann löscht sich das Objekt. Im Listing sieht man, dass AddRef und Release genau dieses Verhalten implementieren. Zusätzlich rufen sie die Funktionen DllLockServer beziehungsweise DllUnlockServer auf, falls der Referenzzähler von 0 auf 1 oder von 1 auf 0 wechselt. Durch diese Aufrufe wird sichergestellt, dass der COM-Server nicht aus dem Speicher entfernt werden kann, solange es mindestens eine Instanz eines COM-Objekts gibt.
Implementierung von QueryInterface
Die sich anschließende Implementierung von CPPServer::QueryInterface gestaltet sich recht einfach. Die übergebene IID wird durch eine Reihe von if-else-Anweisungen mit den unterstützten IIDs verglichen. Ergibt sich eine Übereinstimmung, so wird der Zeiger auf die Objektinstanz (this) in den Typ der gewünschten Schnittstelle umgewandelt, AddRef aufgerufen und der Zeiger durch den Parameter ppvObj zurückgegeben. Ergibt sich keine Übereinstimmung, so wird ein Fehlerwert zurückgegeben, der signalisiert, dass das COM-Objekt die gewünschte Schnittstelle nicht unterstützt. Es muss außerdem sichergestellt sein, dass der übergebene Schnittstellenzeiger auf NULL gesetzt wird, wenn die angeforderte Schnittstelle nicht unterstützt wird. Viele Client-Programme von COM-Servern werten nicht den Rückgabewert von QueryInterface aus, sondern prüfen, ob der Schnittstellenzeiger NULL ist.
Das Komponentenobjektmodell
279
Als letzte Funktion der Klasse CPPServer implementiert GetSquare die ISquare-Schnittstelle. GetSquare liefert den Rückgabewert S_OK, den Standardwert bei COM-Schnittstellen, wenn kein Fehler aufgetreten ist. Als Nächstes folgt die Implementierung der Klassenfabrik in der Klasse CPPServerFactory. Der Konstruktor setzt genau wie bei der Klasse CPPServer den Referenzzähler auf 0. AddRef und Release inkrementieren beziehungsweise dekrementieren den Referenzzähler. Da die Klassenfabrik als statische Variable deklariert ist, müssen in Release allerdings keine Objekte freigegeben werden. DllLockServer und DllUnlockServer dienen wieder dazu, die DLL im Speicher zu halten, solange das Client-Programm Zeiger auf Schnittstellen der Klassenfabrik hält. QueryInterface ist ähnlich wie die gleichnamige Funktion in der Klasse CPPServer implementiert, es werden lediglich andere Schnittstellen unterstützt. Als Nächstes implementiert CPPServerFactory die beiden Methoden der Schnittstelle IClassFactory: CreateInstance und LockServer. Mit CreateInstance werden Instanzen der COM-Klasse erzeugt. Da die COM-Klasse durch die C++-Klasse CPPServer implementiert wird, erzeugt die Funktion CreateInstance CPPServer-Objekte. Kann das COM-Objekt angelegt und die angeforderte Schnittstelle unterstützt werden, dann wird durch ppvObj ein Schnittstellenzeiger auf das COM-Objekt zurückgegeben. LockServer ist eine Methode, mit der der COM-Client den Server im Speicher halten kann, selbst wenn er momentan keine Schnittstellenzeiger hält. LockServer verwendet die schon bekannten Funktionen DllLockServer und DllUnlockServer. Die meisten ClientProgramme brauchen die Funktion LockServer nicht aufzurufen. LockServer kann dazu beitragen, die Ausführungszeit zum Anlegen eines COM-Objekts zu verkürzen, da der COM-Server nicht erneut in den Hauptspeicher geladen werden muss.
Implementierung der Klassenfabrik
Damit ist die Implementierung des COM-Servers selbst beendet. Ein prozessinterner COM-Server muss jedoch noch einige Funktionen implementieren, mit denen auf ihn zugegriffen werden kann. Diese Funktionen müssen aus der DLL, die den Server implementiert, exportiert werden. Anders als in Abschnitt 2.10.2, »Reguläre MFC-DLLs«, beschrieben, kann dies bei COM-Servern nicht über dllexport geschehen. Stattdessen müssen die zu exportierenden Funktionen in einer Moduldefinitionsdatei bekannt gemacht und über Ordinalzahlen exportiert werden. Listing 3.9 zeigt die Moduldefinitionsdatei des Projekts CppServer.
DLL-Funktionen
280
3
COM, OLE und ActiveX
; CPPSquareServer.def : Deklariert die Parameter für das Modul. LIBRARY
In der Moduldefinitionsdatei müssen die zu exportierenden Funktionen nach der Anweisung EXPORTS aufgeführt werden. Das Schlüsselwort PRIVATE bewirkt, dass die exportierte Funktion nicht in die Importbibliothek übernommen wird.
DllGetClassObject
Wozu werden die Funktionen verwendet? DllGetClassObject erlaubt den Zugriff auf die Klassenfabrik des COM-Servers. Die Funktionen der COM-Laufzeitbibliothek – beispielsweise CoGetClassObject und CoCreateInstance – benötigen diese Funktion, damit sie einen ersten Zeiger auf die Klassenfabrik des COM-Servers erhalten können. DllGetClassObject ist der Schlüssel zum COM-Server; ohne diese Funktion kommt man nicht an den Server heran.
DllCanUnloadNow
Die Funktion DllCanUnloadNow wertet den bereits beschriebenen Lock-Zähler aus. Gibt diese Funktion S_OK zurück, dann weiß die COM-Laufzeitbibliothek, dass sie die Server-DLL aus dem Speicher entfernen darf.
DllRegisterServer und DllUnregisterServer
Die Funktionen DllMain und DllRegisterServer werden zur Registrierung des COM-Servers verwendet. DllMain wird lediglich gebraucht, damit DllRegisterServer Zugriff auf den Instanz-Handle der DLL hat. DllMain wird aufgerufen, wenn die DLL geladen wird. Die Funktion speichert den Instanz-Handle in der globalen Variablen hInstance. Durch Aufruf der Funktion DllUnregisterServer kann der COM-Server wieder aus der Registrierungsdatenbank entfernt werden.
3.2.14 Registrierung von COM-Servern Damit COM-Server von der COM-Laufzeitbibliothek gefunden werden können, müssen sie in der Registrierungsdatenbank eingetragen werden. Die CLSIDs von COM-Klassen werden unter dem Registrierungsschlüssel
Das Komponentenobjektmodell
281
HKEY_CLASSES_ROOT\CLSID\{CLSID}
eingetragen, wobei {CLSID} die tatsächliche CLSID der COMKlasse darstellt. Abbildung 3.9 zeigt diesen Eintrag in der Registrierungsdatenbank für den COM-Server CppServer.
Abbildung 3.9: CLSIDs in der Registrierungsdatenbank
Neben der Identifizierung von COM-Klassen über ihre CLSID bietet COM die Möglichkeit, COM-Klassen zu benennen. Diese Namen werden ebenfalls in der Registrierungsdatenbank eingetragen. Die Namen von COM-Klassen können mit der Funktion CLSIDFromProgID in Klassen-IDs umgewandelt werden. Die Funktion ProgIDFromCLSID nimmt die Konvertierung in der umgekehrten Richtung vor. Eingetragen werden die Klassennamen ebenfalls unter dem Schlüssel HKEY_CLASSES_ROOT. Jeder Eintrag muss im Unterschlüssel CLSID die Klassen-ID angeben, damit eine Zuordnung zwischen Klassennamen und KlassenID hergestellt werden kann. Abbildung 3.10 zeigt einige Klassennamen in der Registrierungsdatenbank.
Abbildung 3.10: Klassennamen in der Registrierungsdatenbank
Klassennamen
282
3
t
COM, OLE und ActiveX
Die Funktionen CLSIDFromProgID und ProgIDFromCLSID verwenden zur Angabe des Klassennamens den Datentyp OLECHAR*. Dieser Datentyp verwendet für jedes Zeichen zwei Byte, die Zeichen werden im Unicode-Format abgelegt. Grundsätzlich verwenden alle COM-Funktionen diesen Datentyp. Normalerweise muss daher zwischen der UnicodeDarstellung und der unter Windows üblichen ANSI-Darstellung konvertiert werden, es sei denn, man programmiert ausschließlich für Windows NT und verwendet durchgängig Unicode. Der Typ OLECHAR* ist auch als BSTR definiert. BSTRs sind zwar genau wie normale CStrings NULL-terminiert, jedoch besitzen sie zusätzlich einen Zeichenzähler, der vor der Zeichenkette gespeichert wird. Die Klasse CString besitzt die Funktionen AllocSysString und SetSys String, um BSTR-Strings zu erzeugen. Zusätzlich bietet Visual C++ die von den MFC unabhängige Klasse _bstr_t, um mit BSTR-Strings zu arbeiten. Eine String-Konstante kann als BSTR angelegt werden, indem ihr ein großes »L« vorangestellt wird: BSTR command = L"Energie!";
Das Beispielprogramm CppServer registriert lediglich die CLSID der COM-Klasse in der Registrierungsdatenbank. Das bedeutet, dass man auf den COM-Server nur über die CLSID, nicht jedoch über einen Namen zugreifen kann. Prinzipiell muss sich ein COMServer nicht selbst in der Registrierungsdatenbank eintragen können. Man kann diese Aufgabe einem Installationsprogramm überlassen. Allerdings ist es vorteilhaft, wenn der Server sich selbst registrieren kann. Man kann ihn dann auch ohne Installationsprogramm oder fehlerträchtiges manuelles Eintragen in der Registrierungsdatenbank in Betrieb nehmen. Registrierung von COM-Servern
COM-Server, die als prozessinterne Server realisiert sind, sollten die Funktionen DllRegisterServer und DllUnregisterServer implementieren und aus ihrer DLL exportieren. Ein COM-Server, der diese Funktionen implementiert, kann beispielsweise mit dem Hilfsprogramm REGSVR32.EXE registriert werden. REGSVR32.EXE befindet sich im Systemverzeichnis der Windows-Installation. REGSVR32.EXE lädt die ihm übergebene DLL und ruft die Funktion DllRegisterServer auf, um die DLL zu registrieren. Mit dem Programm REGSVR32.EXE lässt sich ein Registrierungseintrag auch wieder aus der Registrierungsdatenbank entfernen. Dazu muss man das Programm mit dem Parameter /U aufrufen.
Das Komponentenobjektmodell
283
Um COM-Server bequem zu registrieren, legt man eine Verknüpfung mit dem Programm REGSVR32.EXE in den Ordner SENDTO des eigenen Benutzers. Nun kann man eine DLL mit dem Befehl »Senden an« aus dem Kontextmenü an das Programm REGSVR32.EXE senden. Damit wird ein dort implementierter COM-Server registriert. Nun zur Implementierung von DllRegisterServer im Beispielprogramm CppServer. Zunächst wird die API-Funktion GetModuleFileName aufgerufen, um zu bestimmen, wo sich die DLL befindet. Das heißt, GetModuleFileName ermittelt den Dateipfad der DLL. Um GetModuleFileName aufzurufen, wird der Instanz-Handle der DLL benötigt. Er wurde bereits zuvor in der Funktion DllMain, die beim Laden der DLL aufgerufen wird, zwischengespeichert. Durch Aufruf der Funktion RegCreateKeyEx wird ein Schlüssel in der Registrierungsdatenbank angelegt und durch den Aufruf von RegSetValueEx wird anschließend der Pfad eingetragen. Der Aufruf von RegCloseKey beendet den Zugriff auf die Registrierungsdatenbank. Der Pfadname wird unter dem Unterschlüssel INPROCSERVER32 eingetragen. INPROCSERVER32 bedeutet, dass CppServer ein prozessinterner Server ist, der auf dem WIN32-API basiert, also ein 32-BitProgramm ist.
Implementierung der Selbstregistrierung
Die Funktion DllUnregisterServer löscht den Eintrag des COM-Servers in der Registrierungsdatenbank. Da unter Windows NT keine Hierarchien von Schlüsseln gelöscht werden können, erfolgt das Löschen des Eintrags in zwei Schritten: Im ersten Schritt wird der Schlüssel INPROCSERVER32 entfernt, im zweiten Schritt die CLSID des COM-Servers. Zum Löschen der Schlüssel wird die WIN32Funktion RegDeleteKey verwendet. Vor dem Löschen muss der Schlüssel mit der Funktion RegOpenKeyEx geöffnet worden sein.
3.2.15 Bewertung der Implementierung des Programms CppServer Das Programm CppServer macht deutlich sichtbar, was innerhalb eines COM-Servers vorgeht. Die gesamte Funktionalität, wie das Verwalten von Referenzzählern und die Suche von Schnittstellen mittels QueryInterface, musste selbst implementiert werden. Es wird deutlich, dass COM weniger eine im Betriebssystem versteckte Technologie ist, sondern vielmehr eine Reihe von Programmierkonventionen darstellt. Da diese Konventionen mit dem Laufzeitmodell von C++ gut harmonieren (die virtuellen Funk-
Konventionen
284
3
COM, OLE und ActiveX
tionstabellen, vTables, von C++ und COM sind gleich), lassen sich COM-Server direkt in C++ schreiben. Die Unterstützung durch die COM-Laufzeitbibliothek ist für prozessinterne COM-Server minimal. Verwendung von Mehrfachvererbung
Das Beispielprogramm CppServer soll nun in einem nächsten Schritt näher an die MFC-Implementierung von COM herangeführt werden. In der vorgestellten Implementierung wird ein Problem schlichtweg ignoriert: Was macht man, wenn ein Server mehrere Schnittstellen implementieren soll, die, anders als im Beispiel, nicht voneinander abgeleitet sind? Die intuitive Lösung zu diesem Problem ist die Verwendung von Mehrfachvererbung. Man lässt die C++-Klasse, die die COM-Klasse implementiert, von mehreren Schnittstellen erben. Als Beispiel stelle man sich vor, die in Abbildung 3.11 gezeigte COM-Klasse solle implementiert werden.
IUnknown
ICar IOwner
Abbildung 3.11: ICar/IOwner-Beispiel in COM-Notation
Jede der beiden Schnittstellen besitzt genau eine Methode: ICar die Methode GetPower, um die Leistung des Motors zu ermitteln, und IOwner die Methode GetAge, um das Alter des Autobesitzers herauszufinden. Implementieren ließe sich der COM-Server, wie in Listing 3.10 gezeigt. interface ICar : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetPower (long *pnPower) = 0; }; interface IOwner : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE
Das Komponentenobjektmodell GetAge (long *pnAge) = 0; }; class CCar : public ICar, public IOwner { public: // IUnknown-Methoden HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, void **ppvObj); ULONG STDMETHODCALLTYPE AddRef (); ULONG STDMETHODCALLTYPE Release (); // ICar-Methoden HRESULT STDMETHODCALLTYPE GetPower (long *pnPower); // IOwner-Methoden HRESULT STDMETHODCALLTYPE GetAge (long *pnAge); // weitere Deklarationen ... }; Listing 3.10: Implementierung mehrerer Schnittstellen durch Mehrfachvererbung
Mehrere Schnittstellen durch Mehrfachvererbung in der gezeigten Weise zu implementieren, ist durchaus praktikabel und machbar. Es ergibt sich allerdings ein Problem, wenn man diese Vorgehensweise schematisieren möchte, um sie beispielsweise in einem Framework wie den MFC zu implementieren. Das Problem tritt auf, wenn ein Methodenname in mehreren Schnittstellen verwendet wird, die Semantik beider Methoden jedoch verschieden ist. Als Beispiel sei angenommen, dass sowohl ICar als auch IOwner jeweils um die Methode GetID erweitert werden sollen. Im Fall von ICar soll GetID die Fahrgestellnummer zurückgeben, bei IOwner ist jedoch die Personalausweisnummer gemeint. Da COMSchnittstellen unveränderlich sind, müssen zu diesem Zweck zwei neue Schnittstellen definiert werden, ICar2 und IOwner2. Die Definition dieser Schnittstellen zeigt Listing 3.11. interface ICar2 : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetPower (long *pnPower) = 0; virtual HRESULT STDMETHODCALLTYPE GetID (long *pnID) = 0; };
Die Methode GetID ist in beiden Schnittstellen vorhanden und hat in beiden Fällen die gleiche Signatur, das heißt, die Parameterlisten beider Methoden sind gleich. Trotzdem ermitteln beide Methoden semantisch verschiedene Werte: Folglich kann die Implementierung der beiden Funktionen nicht zusammengefasst werden, wie es beispielsweise bei den Methoden von IUnknown möglich ist (die Methoden von IUnknown haben die gleiche Semantik und müssen daher nur einmal implementiert werden). Genau hier liegt das Problem bei der Verwendung von Mehrfachvererbung. Soll die COM-Klasse unter Ausnutzung von Mehrfachvererbung implementiert werden, dann erbt die C++-Klasse, die den Server implementiert, die Methode GetID von zwei Schnittstellen. Die C++Klasse kann aber nur eine Implementierung liefern, so dass die Implementierung mittels Mehrfachvererbung auf diese Weise nicht möglich ist. Als Ausweg bietet sich die im Folgenden gezeigte Implementierung mit verschachtelten Klassen an.
3.2.16 Das Beispielprogramm CppServerNested Verschachtelte Klassen
Das Beispielprogramm CppServerNested implementiert die Schnittstelle ISquare in einer eigenen Klasse. Um die COM-Klasse dennoch als eine Entität auch in der Implementierung erscheinen zu lassen, wird eine eingebettete Klasse verwendet. Eingebettete oder verschachtelte Klassen werden ansonsten in C++ relativ selten verwendet, es sind Klassen, die innerhalb von anderen Klassen deklariert werden und nur lokal in diesen Klassen sichtbar sind. Das Beispielprogramm CppServerNested verwendet die Klasse SquareItf, um die Schnittstelle ISquare zu implementieren. Die Klasse SquareItf ist in die Klasse CPPServerNested, die die COMKlasse implementiert, eingebettet. Listing 3.12 zeigt die Deklaration der beiden Klassen.
Das Komponentenobjektmodell #include "unknwn.h" interface ISquare : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetSquare (long nValue, long *pnResult) = 0; }; class CPPServerNested : public IUnknown { public: CPPServerNested (); // IUnknown-Methoden HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, void **ppvObj); ULONG STDMETHODCALLTYPE AddRef (); ULONG STDMETHODCALLTYPE Release (); class SquareItf : public ISquare { public: // IUnknown-Methoden HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, void **ppvObj); ULONG STDMETHODCALLTYPE AddRef (); ULONG STDMETHODCALLTYPE Release (); // ISquare-Methoden HRESULT STDMETHODCALLTYPE GetSquare (long nValue, long *pnResult); // Zeiger auf die einbettende Klasse CPPServerNested *m_pParent; } m_SquareItf; // Gleich ein Objekt der Klasse anlegen! private: DWORD m_dwRefCount; }; class CPPServerNestedFactory : public IClassFactory { public: CPPServerNestedFactory (); // IUnknown-Methoden HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, void **ppvObj); ULONG STDMETHODCALLTYPE AddRef (); ULONG STDMETHODCALLTYPE Release ();
Die Klasse CPPServerNested deklariert als äußere Klasse lediglich die Methoden der Schnittstelle IUnknown. Alle weiteren Schnittstellen, in diesem Fall nur ISquare, werden durch eingebettete Klassen deklariert und später auch implementiert. Da ISquare natürlich auch die Methoden von IUnknown unterstützen muss, sind diese nochmals in der Klassendeklaration von SquareItf vertreten. Man beachte, dass SquareItf nicht nur innerhalb der Klasse CPPServerNested deklariert wird, sondern dass mit m_SquareItf auch gleich eine Instanz von SquareItf angelegt wird, sobald ein Objekt der Klasse CPPServerNested erzeugt wird! Die Klasse SquareItf enthält mit m_pParent einen Zeiger, in dem sie sich die Klasse merkt, in die sie eingebettet ist. Dieser Zeiger ist als public deklariert und kann somit im Konstruktor der Klasse CppServerNested zugewiesen werden. Dies ist in Listing 3.13 zu sehen, das die Implementierungsdatei des COM-Servers zeigt. #include #include "CppServerNested.h" static const GUID IID_ISquare = {0x547E8380,0xF175,0x11D1, 0x90,0xA2,0xD0,0xDD,0x2F,0x7A,0xCA,0x49}; // {E3C91780-F24F-11d1-90A2-D0DD2F7ACA49} static const GUID CLSID_WIN32SquareServerNestedClass = { 0xe3c91780, 0xf24f, 0x11d1, { 0x90, 0xa2, 0xd0, 0xdd, 0x2f, 0x7a, 0xca, 0x49 } }; // Globale Variablen static HINSTANCE hInstance; static long nServerLockCount = 0; static CPPServerNestedFactory serverFactory; // Prototypen zum Festhalten der DLL im Speicher static void DllLockServer (void); static void DllUnlockServer (void); CPPServerNested::CPPServerNested () {
handle to DLL module reason for calling function reserved wird in DllRegisterServer
return TRUE; } /////////////////////////////////////// // Funktionen zum Setzen des DLL-Locks void DllLockServer (void) { nServerLockCount++; } void DllUnlockServer (void) { nServerLockCount--; } Listing 3.13: Implementierungsdatei des Beispielprogramms CppServerNested
Die Implementierung der Schnittstelle IUnknown durch die Klasse CPPServerNested erfolgt genau wie im letzten Beispiel. Interessanter ist da schon die sich daran anschließende Implementierung von IUnknown durch die eingebettete Klasse SquareItf. Wer noch nie mit eingebetteten Klassen gearbeitet hat, dem wird wohl
Das Komponentenobjektmodell
zunächst die doppelte Anwendung des Bereichsoperators :: für die Funktionen der eingebetteten Klasse auffallen. Davon abgesehen ist die Implementierung recht unspektakulär, die Aufrufe werden einfach an die einschließende Klasse delegiert. Somit muss SquareItf nur die Methode GetSquare selbst implementieren. Die sich anschließende Implementierung von Klassenfabrik und DLL-Funktionen gestaltet sich analog zum letzten Beispiel.
3.2.17 Mehrfachvererbung versus eingebettete Klassen Im Beispielprogramm CPPServerNested wird nur eine über IUnknown hinausgehende Schnittstelle implementiert. Wollte man weitere Schnittstellen implementieren, so würde man für jede weitere Schnittstelle eine eigene eingebettete Klasse verwenden. Jede dieser Klassen müsste wieder die Methoden von IUnknown an die einbettende Klasse delegieren. Genau hier liegt der Nachteil, wenn man eingebettete Klassen zur Implementierung mehrerer COM-Schnittstellen verwendet: Man muss mehr Programmcode schreiben. Der Vorteil gegenüber der Mehrfachvererbung ist, dass man das Verfahren leicht schematisieren kann, ohne das Problem der Namenskonflikte (gleichnamige Methoden in mehreren Schnittstellen mit unterschiedlicher Semantik) behandeln zu müssen. Bei der Verwendung von eingebetteten Klassen dürfen Namenskonflikte ruhig auftreten, schließlich besitzt jede Schnittstellenklasse ihren eigenen Namensraum. Weil sich die Implementierung von mehreren COM-Schnittstellen mit eingebetteten Klassen gut schematisieren lässt, wird dieses Verfahren von den MFC verwendet. Ein weiterer Grund ist, dass die MFC es generell vermeiden, Mehrfachvererbung zu verwenden.
3.2.18 Implementierung eines COM-Servers mit den MFC Als nächster und letzter COM-Server soll nun ein Server mit ISquare-Schnittstelle unter Zuhilfenahme der MFC erstellt werden. Leider gibt es für das Erstellen von COM-Servern mit den MFC keinen Assistenten, so dass auf jeden Fall etwas Handarbeit angesagt ist. Mit einem Trick lässt sich allerdings einiges an Arbeit sparen. Das Beispielprogramm heißt MFCServer und befindet sich im Verzeichnis KAPITEL3\MFCSERVER auf der Begleit-CD.
295
296 Ausnutzung der Automationsunterstützung
3
COM, OLE und ActiveX
Der Server wird mit Assistenten für MFC-DLLs angelegt. Dabei wird die Option ausgewählt, eine normale MFC-DLL mit dynamischer Bindung zu erstellen. Der Trick ist, zusätzlich die Option für Automationsunterstützung auszuwählen. Auf diese Weise wird bereits ein großer Teil des für den COM-Server notwendigen Programmcodes erzeugt. Zwar wird darüber hinaus Programmcode zur Automation erzeugt, jedoch kann dieser problemlos gelöscht werden. Abbildung 3.12 zeigt die Einstellungen des Assistenten.
Abbildung 3.12: Anlegen der COM-Server-DLL mit dem Assistenten für DLLs
Durch Auswahl der Option für Automationsunterstützung hat der MFC-Anwendungs-Assistent für DLLs bereits Implementierungen für die von der DLL zu exportierenden Funktionen DllGetClassObject, DllCanUnloadNow und DllRegisterServer erzeugt. Diese Funktionen sind Teil der Datei MFCSERVER.CPP; Listing 3.14 zeigt den entsprechenden Ausschnitt aus dieser Datei. Auch die zum Export der Funktionen notwendige Moduldefinitionsdatei MFCSERVER.DEF ist vom Anwendungs-Assistenten bereits angelegt und dem Projekt hinzugefügt worden. ///////////////////////////////////////////////////////////// // Spezielle, für Inproc-Server benötigte Einsprungpunkte STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
Das Komponentenobjektmodell
297
AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllGetClassObject(rclsid, riid, ppv); } STDAPI DllCanUnloadNow(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllCanUnloadNow(); } // DllRegisterServer - Adds entries to the system registry STDAPI DllRegisterServer(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid)) return SELFREG_E_TYPELIB; if (!COleObjectFactory::UpdateRegistryAll()) return SELFREG_E_CLASS; return S_OK; }
// DllUnregisterServer - Removes entries from the system // registry STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor)) return SELFREG_E_TYPELIB; if (!COleObjectFactory::UpdateRegistryAll(FALSE)) return SELFREG_E_CLASS; return S_OK; } Listing 3.14: Ausschnitt aus MFCSERVER.CPP
Wie man im Listing sehen kann, besitzen die MFC für die vier Funktionen DllGetClassObject, DllCanUnloadNow, DllRegisterServer und DllUnregisterServer bereits vorgefertigte Implementierungen. Die Aufrufe von DllGetClassObject und DllCanUnloadNow werden einfach an die globalen MFC-Funktionen AfxDllGetClassObject und AfxDllCanUnloadNow weitergereicht. Zur Registrie-
COleObjectFactory
298
3
COM, OLE und ActiveX
rung wird dagegen die statische Funktion UpdateRegistryAll der Klasse COleObjectFactory aufgerufen. Wird dabei der Parameterwert FALSE übergeben, so deregistriert sich der Server. Die Klasse COleObjectFactory bietet eine vorgegebene Implementierung von Klassenfabriken innerhalb der MFC, man muss also als MFC-Programmierer die Klassenfabrik eines COM-Servers nicht selbst implementieren (die MFC-Programmierer haben den eher zutreffenden Namen Objektfabrik statt Klassenfabrik verwendet). Zusätzlich kümmert sich COleObjectFactory um die Registrierung von COM-Servern. Abbildung 3.13 zeigt die Klasse COleObjectFactory als Teil des Anwendungsgerüsts.
CObject CCmdTarget COleObjectFactory Abbildung 3.13: Die Klasse COleObjectFactory im Anwendungsgerüst
Um die COM-Klasse zu implementieren, wird in der Klassenansicht eine neue C++-Klasse angelegt. Unter HINZUFÜGEN | KLASSE HINZUFÜGEN... wird dazu MFC-Klasse ausgewählt. Anschließend muss das in Abbildung 3.14 gezeigte Dialogfeld ausgefüllt werden.
Abbildung 3.14: Die Klasse CSquare anlegen
Das Komponentenobjektmodell
299
Es ist wichtig, als Basisklasse CCmdTarget auszuwählen, da CCmdTarget die COM-Funktionalität der MFC implementiert. Jede MFCKlasse, die eine COM-Klasse implementieren soll, muss direkt oder indirekt von CCmdTarget abgeleitet werden. Unter AUTOMATION wird ANHAND VON TYPEN-ID ERSTELLBAR ausgewählt. Die erzeugte Klasse soll zwar eigentlich die Automation unterstützen, kann aber leicht so abgeändert werden, dass sie als COM-Klasse mit selbstdefinierter Schnittstelle dient.
3.2.19 Die Interface Definition Language Im Beispielprogramm MFCServer wird erstmals die Schnittstelle nicht nur in der Sprache C++ definiert, sondern sprachunabhängig mit Hilfe der Interface Definition Language, kurz IDL, formuliert. Möchte man die hier vorgestellten COM-Server in einer Sprache wie Java oder Visual Basic verwenden, so nützt eine in C++ definierte Schnittstellenbeschreibung nicht viel. Erst mit einer von der Implementierungssprache unabhängigen Beschreibung von COMKlassen und deren Schnittstellen kann das Ziel der sprachunabhängigen Verwendung von Softwarekomponenten erreicht werden. IDL wird dazu verwendet, eine sprachunabhängige Beschreibung von COM-Klassen und Schnittstellen zu erstellen. Unter Windows bietet Microsoft den IDL-Compiler MIDL.EXE an, der IDL-Quelltext sowohl in Definitionen für die Sprachen C und C++ als auch in binäre Typbibliotheken übersetzen kann, die von Sprachen wie Visual Basic oder Java gelesen werden können. Auch Visual C++ kann Typbibliotheken einlesen, um daraus automatisch Zugriffsklassen für COM-Server zu generieren.
IDL und Typbibliotheken
IDL ist eine Sprache mit C-ähnlicher Syntax. COM-IDL basiert auf DCE-IDL, einer Sprache, die ursprünglich dafür entwickelt wurde, entfernte Prozeduraufrufe (Remote Procedure Calls) unabhängig von der Implementierungssprache zu formulieren. Listing 3.15 zeigt die Datei MFCSERVER.IDL, in der COM-Klasse und ISquareSchnittstelle des Beispielprogramms MFCServer definiert werden.
IDL-Syntax
// MFCServer.idl : Quellcode der Typbibliothek für MFCServer.dll // Diese Datei wird vom MIDL-Compiler bearbeitet, um die // Typbibliothek zu erzeugen (MFCServer.tlb). [ uuid(77A848A2-FC79-11D1-B50B-006097A8F69A), version(1.0), helpstring ("Square Bibliothek") ]
300
3
COM, OLE und ActiveX
library MFCServer { importlib("stdole32.tlb"); [object, uuid(547E8380-F175-11D1-90A2-D0DD2F7ACA49)] interface ISquare : IUnknown { import "unknwn.idl"; HRESULT GetSquare ([in] long nValue, [out, retval] long* pnResult); } // Class information for CSquare [ uuid(4831F131-F290-11D1-90A2-D0DD2F7ACA49) ] coclass Square { interface ISquare; }; }; Listing 3.15: Definition von COM-Klasse und Schnittstelle in IDL
MFCSERVER.IDL ist bereits vom Anwendungs-Assistenten angelegt worden. Die Informationen zur Klasse CSquare wurden danach automatisch eingetragen. Da jedoch eine COM-Klasse mit Automationsunterstützung erzeugt worden ist, mussten einige Änderungen vorgenommen werden. Die ursprüngliche Definition von ISquare ist gelöscht und durch die im Listing gezeigte Definition ersetzt worden. Die COM-Klasse Square führt ISquare entsprechend als normale COM-Schnittstelle und nicht als Automationsschnittstelle auf. IDL-Attribute und Schlüsselwörter
In IDL werden Klassen- und Schnittstellendefinitionen immer im Rahmen einer Typbibliothek vorgenommen. Daher wird zunächst die Typbibliothek MFCServer definiert. Die eckigen Klammern vor einer Definition sind Attribute, die das definierte Objekt näher beschreiben. Das Attribut uuid bestimmt die GUID der anschließend definierten Typbibliothek, COM-Klasse oder Schnittstelle. Das Attribut version gibt die Versionsnummer der Typbibliothek an, mit helpstring wird eine textuelle Beschreibung der nachfolgenden Definition geliefert. Die Schnittstelle ISquare und die COMKlasse Square werden innerhalb des Gültigkeitsbereichs der Typbibliothek definiert. Die Schnittstellendefinition für eine COMSchnittstelle wird durch das Schlüsselwort interface eingeleitet (im Gegensatz zu dispinterface, welches eine Automationsschnittstelle
Das Komponentenobjektmodell
301
definiert). Das Attribut object ist bei der Definition einer Schnittstelle zwingend notwendig, da sonst nicht deutlich wird, dass es sich um eine COM-Schnittstelle handelt. Das Attribut version ist bei Schnittstellendefinitionen nicht zulässig, da Schnittstellen unveränderlich sind und daher nicht in verschiedenen Versionen auftreten können. Im Beispiel werden die von IUnknown geerbten Methoden einfach durch die Direktive #import "unknwn.idl"
eingebunden. Damit werden die Definitionen für die Methoden AddRef, Release und QueryInterface aus der Definition der Schnittstelle IUnknown übernommen. Man kann Schnittstellendefinitionen auf diese Weise auch mehrfach importieren. Bei der Definition der Methode GetSquare werden die Parameter durch die Attribute in, out und retval näher beschrieben. Das Attribut in kennzeichnet einen Eingabewert, out einen Ausgabewert und retval ermöglicht die Verwendung des so gekennzeichneten Werts als Rückgabewert einer Funktion. Obwohl dies für C++ keine Relevanz hat, lassen sich damit Funktionsaufrufe in Sprachen wie Java und Visual Basic einfacher gestalten, indem die aufgerufene Funktion den durch retval gekennzeichneten Parameter direkt als Funktionswert zurückgibt. Der eigentliche Rückgabewert vom Typ HRESULT wird bei diesen Sprachen innerhalb des Laufzeitsystems der Sprache ausgewertet. Der C++-Programmierer muss HRESULT selbst auswerten, daher ist die Rückgabe von anderen Funktionswerten in der Sprache C++ nicht möglich. Auf der anderen Seite ist dafür die Anbindung von C++ an den COMServer etwas effizienter. Bei der Verwendung von als out gekennzeichneten Parametern ist im Zusammenhang mit der Sprache C++ etwas Vorsicht angebracht. Zwar wird ein solcher Parameter in C++ durch einen Zeiger dargestellt und sollte damit theoretisch auch als Eingabeparameter verwendbar sein, jedoch ist keinesfalls gesagt, dass die Implementierungssprache des COMServers die gleiche Semantik besitzt. Will man Parameter sowohl für Eingaben als auch für Ausgaben verwenden, so muss man beide Attribute, in und out, angeben. Nach der Schnittstelle wird die COM-Klasse Square definiert. Definitionen von COM-Klassen erfolgen durch das Schlüsselwort coclass. Square führt ISquare als Schnittstelle auf.
Ein- und Ausgabewerte
302
3 Ausgabe des IDL-Compilers
COM, OLE und ActiveX
Der IDL-Compiler, der als Teil des Übersetzungsvorgangs aufgerufen wird, erzeugt aus der IDL-Quelldatei eine Typbibliothek, im Beispiel die Datei MFCSERVER.TLB. Normalerweise wird die Typbibliothek jedoch auch gleich in die DLL oder EXE-Datei übernommen, die den COM-Server implementiert, so dass die TLB-Datei selbst nicht gebraucht wird. Sprachen wie Visual Basic oder Java verwenden dann diese Typbibliothek, um daraus die Schnittstellendefinitionen zu entnehmen und somit auf den COM-Server zugreifen zu können. Die neu angelegte Klasse CSquare soll die COM-Klasse des Servers implementieren. Dazu sind einige Änderungen an dem automatisch erzeugten Programmcode notwendig. Listing 3.16 zeigt die Header-Datei der Klasse, Listing 3.17 die Implementierungsdatei. interface ISquare : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetSquare (long value, long *pnResult) = 0; }; ////////////////////////////////////////////////////////////// / // Befehlsziel CSquare class CSquare : public CCmdTarget { DECLARE_DYNCREATE(CSquare) CSquare(); // Attribute public: // Operationen public: // Überschreibungen public: virtual void OnFinalRelease(); // Implementierung protected: virtual ~CSquare(); DECLARE_MESSAGE_MAP() DECLARE_OLECREATE(CSquare)
Der Anwendungs-Assistent hat bereits das Makro DECLARE_ INTERFACE_MAP eingefügt, das eine Schnittstellenzuordnungstabelle definiert. Schnittstellenzuordnungstabellen werden ähnlich wie Nachrichtenzuordnungstabellen verwendet. Im Gegensatz zu diesen bilden Schnittstellenzuordnungstabellen nicht WindowsNachrichten auf C++-Funktionen ab, sondern sie bilden eine Verknüpfung zwischen COM-Methoden und den sie implementierenden C++-Funktionen. Durch die Verwendung der MFC-Makros zur Erstellung von Schnittstellenzuordnungstabellen wird die zur Implementierung von COM-Klassen notwendige Struktur der verschachtelten C++-Klassen aufgebaut. Zwischen den Makros BEGIN_INTERFACE_PART und END_ INTERFACE_PART wird die COM-Schnittstelle spezifiziert. Es müssen hier die Methoden der COM-Schnittstelle aufgeführt werden. Bei den Makros BEGIN_INTERFACE_PART und END_ INTERFACE_PART muss die eingebettete Klasse angegeben werden, die die Schnittstelle implementieren soll. Im Beispiel ist das die Klasse SquareItf. Die Makros zur Definition von Automationstabellen sind aus dem erzeugten Programmcode entfernt worden, die Definition der Schnittstelle ISquare hingegen ist nachträglich zugefügt worden. Bei den gelöschten Teilen handelt es sich um die Verteilertabelle des Automationsservers, zu erkennen an den Makros DECLARE_ DISPATCH_MAP, BEGIN_DISPATCH_MAP und END_DISPATCH_ MAP. Diese Makros werden im Rahmen der Automation in Abschnitt 3.3.4, »Die Automationsimplementierung der MFC«, besprochen. ////////////////////////////////////////////////////////////// // CSquare IMPLEMENT_DYNCREATE(CSquare, CCmdTarget) CSquare::CSquare() {
Schnittstellenzuordnungstabellen
304
3
COM, OLE und ActiveX
EnableAutomation(); // Um die Ausführung der Anwendung fortzusetzen, solange ein // OLE-Automatisierungsobjekt aktiv ist, ruft der Konstruktor // AfxOleLockApp auf. AfxOleLockApp(); } CSquare::~CSquare() { // Um die Anwendung zu beenden, nachdem alle Objekte durch // OLE-Automatisierung erstellt wurden, ruft der Destruktor // AfxOleUnlockApp auf. AfxOleUnlockApp(); } void CSquare::OnFinalRelease() { // Nachdem die letzte Referenz auf ein Automatisierungsobjekt // freigegeben wurde, wird OnFinalRelease aufgerufen. Die // Basisklasse löscht das Objekt automatisch. Fügen Sie // zusätzlichen Bereinigungscode für Ihr Objekt hinzu, bevor // Sie die Basisklasse aufrufen. CCmdTarget::OnFinalRelease(); } BEGIN_MESSAGE_MAP(CSquare, CCmdTarget) END_MESSAGE_MAP() // Hier IID_ISquare ausgetauscht static const GUID IID_ISquare = {0x547E8380,0xF175,0x11D1, 0x90,0xA2,0xD0,0xDD,0x2F,0x7A,0xCA,0x49}; BEGIN_INTERFACE_MAP(CSquare, CCmdTarget) INTERFACE_PART(CSquare, IID_ISquare, SquareItf) END_INTERFACE_MAP() // {4831F131-F290-11D1-90A2-D0DD2F7ACA49} IMPLEMENT_OLECREATE(CSquare, "MFCServer.Square", 0x4831f131, 0xf290, 0x11d1, 0x90, 0xa2, 0xd0, 0xdd, 0x2f, 0x7a, 0xca, 0x49)
In der Implementierungsdatei musste die vom Klassen-Assistenten erzeugte IID für die ISquare-Schnittstelle gegen die bereits in den vorigen Beispielen verwendete IID ausgetauscht werden, der Klassen-Assistent hat nämlich einfach eine neue IID generiert. An die Definition der IID für ISquare schließt sich die endgültige Zuordnung zwischen COM-Schnittstelle, Implementierung und IID an. Diese wird zwischen den Makros BEGIN_INTERFACE_ MAP und END_INTERFACE_MAP platziert. Dem Makro INTERFACE_PART wird als Parameter die implementierende Klasse, die IID der Schnittstelle und die eingebettete Schnittstellenklasse übergeben. INTERFACE_PART muss einmal für jede Schnittstelle aufgerufen werden, die von der Klasse CSquare implementiert wird. Die Schnittstelle IUnknown wird dabei weggelassen. Die Implementierung der COM-Methoden durch C++-Funktionen gestaltet sich jetzt recht einfach. Die Klassenfabrik muss nicht implementiert werden, da die MFC mit der Klasse COleObjectFactory bereits eine vorgegebene Implementierung liefern. Angelegt wird
Implementierung von Klassenfabrik und IUnkown
306
3
COM, OLE und ActiveX
die Klassenfabrik durch die Makros DECLARE_OLECREATE und IMPLEMENT_OLECREATE. Auch für IUnknown besitzen die MFC bereits eine Implementierung, so dass nur noch die Schnittstelle ISquare implementiert werden muss. Die Implementierung von ISquare muss allerdings Aufrufe der Methoden ihrer Basisschnittstelle IUnknown an die einbettende Klasse delegieren, genau wie dies auch schon im Beispiel des Programms CppServerNested praktiziert worden ist. Bei der Definition der Schnittstellenzuordnungstabelle ist der Name der eingebetteten Klasse mit SquareItf angegeben worden. Die MFC-Makros fügen allerdings vor diesem Namen ein großes »X« ein, um die Klasse deutlich als eingebettete Schnittstellenklasse zu kennzeichnen. Dies muss bei der Implementierung der Schnittstellenfunktion bedacht werden. METHOD_ PROLOGUE
Innerhalb der Funktion wird mit Hilfe des Makros METHOD_ PROLOGUE ein Zeiger auf die einbettende Klasse angefordert. Als Parameter werden dem Makro sowohl die einbettende als auch die eingebettete Klasse übergeben. Das Makro legt dann selbsttätig die Variable pThis an und weist ihr den Zeiger zu. Im Listing 3.17 ist zu sehen, dass die Funktionen, die die Methoden von IUnknown implementieren, ihre Aufrufe an die Klasse CSquare delegieren, indem sie die Funktionen ExternalAddRef, ExternalRelease und ExternalQueryInterface aufrufen. Diese drei Funktionen hat die Klasse CSquare von CCmdTarget geerbt. Die Funktionen ExternalAddRef, ExternalRelease und ExternalQueryInterface bieten die Standardimplementierung der MFC für die Schnittstelle IUnknown. Mit der abschließenden Funktion GetSquare ist die Implementierung des COM-Servers bereits beendet. Die MFC haben die Arbeit mit ihren COM-Makros und den bereits vorhandenen Implementierungen von Klassenfabrik und IUnknown deutlich vereinfacht.
3.2.20 Attributierte Programmierung Um die Erstellung von COM-Servern zu vereinfachen, hat Microsoft in der neuesten Version von Visual Studio die attributierte Programmierung eingeführt. Hierbei handelt es sich um eine Erweiterung des C++-Compilers, der nun in eckigen Klammern geschriebene Attribute erkennt. Diese Attribute geben den ihnen folgenden Sprachelementen eine besondere Bedeutung. So macht das Attribut [coclass] aus der nachfolgenden C++-Klasse eine COMKlasse. Attribute können grundsätzlich den meisten C++-Sprachelementen vorangestellt werden, wie beispielsweise Klassen und
Das Komponentenobjektmodell
deren Member-Funktionen und Member-Variablen. Attribute werden durch Attributprovider implementiert. Dies sind separate DLLs, die den Sprachumfang des Compilers dynamisch erweitern. Attribute wurden mit der Absicht eingeführt, die Programmierung von COM-Servern deutlich zu vereinfachen und den vom Programmierer zu erstellenden Programmcode zu minimieren. Außerdem soll die Verteilung von Sourcecode auf verschiedene Dateien minimiert werden. So müssen bei der COM-Programmierung normalerweise separate IDL- und Implementierungsdateien gepflegt werden. Die attributierte Programmierung macht IDL-Dateien weitgehend überflüssig und fügt die in den IDL-Dateien spezifizierte Information direkt in die Implementierungsdateien ein. Die Attribute ähneln stark den bisher in IDL-Dateien verwendeten Konstrukten. Wer bereits IDL beherrscht, wird mit den neu eingeführten Attributen schnell zurechtkommen. Der vom Attributprovider erzeugte Code wird normalerweise nicht sichtbar. Dies trifft allerdings nicht zu, wenn man ein Programm im Debugger ausführt. Hier kann man durch den erzeugten Code hindurchlaufen. Außerdem lässt sich der Providercode durch einen Compiler-Schalter ausgeben. Um einen Eindruck von der attributierten Programmierung zu bekommen, soll das Beispielprogramm ATLServer gezeigt werden. Dies implementiert den bereits in Abschnitt 3.2.11, »Verwendung eines COM-Servers«, beschriebenen COM-Server. Dieser ist dort als Black-Box-Implementierung behandelt worden. Der Server ist jedoch in Wirklichkeit mit attributierter Programmierung erstellt worden. Listing 3.18 zeigt die Header-Datei des Programms ATLServer: #define _ATL_ATTRIBUTES #include #include using namespace ATL;
[coclass, uuid("547E8381-F175-11D1-90A2-D0DD2F7ACA49"), progid("ATLServer.1")] class ATLServer : public ISquare { public: HRESULT GetSquare (long value, long *pnResult); }; Listing 3.18: Die Datei ATLServer.h
Das Programm heißt ATLServer, weil es den ATL-Provider verwendet. Der vom Provider generierte Programmcode basiert auf der ATL-Klassenbibliothek. Neben dem ATL-Provider gibt es andere Provider, wie beispielsweise den OLE DB Consumer Attribut Provider (vergleiche Abschnitt 4.4.3, »OLE DB Nutzer-Attribute«). COM-Server werden bei attributierter Programmierung immer mit der ATL implementiert, einen MFC-Provider gibt es leider nicht. Listing 3.18 zeigt nach der Einbindung der notwendigen ATLHeader-Dateien, wie zunächst durch Angabe des Attributs module die Implementierungsbibliothek des COM-Servers festgelegt wird. Danach wird die COM-Schnittstelle ISquare definiert. Das Schlüsselwort __interface wurde mit Version 7.0 des C++-Compilers neu eingeführt und deklariert alle COM-Methoden automatisch als pure virtual. Durch Verwendung von __interface statt interface sorgt der Compiler dafür, dass die Semantik von COMSchnittstellen eingehalten wird. So ist beispielsweise die Verwendung von Konstruktoren, Destruktoren und statischen Methoden verboten, da diese in COM-Schnittstellen nicht vorkommen dürfen. Eine vollständige Übersicht über die von __interface durchgeführten Compiler-Prüfungen gibt die Online-Hilfe. Nachdem die Schnittstelle definiert worden ist, wird die Klasse ATLServer deklariert. Diese erbt von ISquare und wird durch das Attribut coclass als COM-Klasse ausgezeichnet. Neben der GUID der COM-Klasse wird auch die Prog-ID angegeben, über die man den COM-Server in der Registrierungsdatenbank finden kann. Die Klasse ATLServer deklariert nur die von ISquare geerbte Funktion GetSquare.
Das Komponentenobjektmodell
309
Die Datei ATLSERVER.H ist recht übersichtlich, allerdings wirklich bemerkenswert ist der Umfang der Implementierungsdatei ATLSERVER.CPP. Diese ist in Listing 3.19 zu sehen. #include "ATLServer.h" HRESULT ATLServer::GetSquare (long value, long *pnResult) { *pnResult = value * value; return S_OK; } Listing 3.19: Die Implementierungsdatei ATLServer.cpp
Hier zeigt sich deutlich die Mächtigkeit der attributierten Programmierung. Der vom Programmierer zu schreibende Programmcode beschränkt sich auf die tatsächliche Implementierung der Funktionalität. Um die Implementierung von IUnknown oder der Klassenfabrik muss er sich nicht kümmern. Möchte man sich den vom ATL-Provider generierten Programmcode ansehen, so kann man dies durch den Compiler-Schalter /Fx erreichen. Der Compiler erzeugt dann die Datei ATLSERVER.MRG.H, die den generierten Programmcode enthält. Dieser Programmcode ist im Beispiel 315 Zeilen lang! Die attributierte Programmierung ist eine sehr mächtige Technologie zur Erstellung von COM-Servern. Die Attribute werden von einem Attributprovider in Programmcode umgewandelt, der anschließend vom Compiler übersetzt wird. Da der Attributprovider zur Programmierung von COM-Servern auf der ATL und nicht auf den MFC basiert, soll die attributierte Programmierung hier nicht weiter vertieft werden. Allerdings findet die attributierte Programmierung auch bei der Erstellung von OLE DB Consumern Verwendung (vergleiche Abschnitt 4.4.3, »OLE DB Nutzer-Attribute«).
3.2.21 Einbettung und Aggregation COM bietet zwei Konzepte zur Wiederverwendung von Softwarekomponenten an: Einbettung und Aggregation. Beide Konzepte machen es möglich, bestehende COM-Klassen innerhalb anderer COM-Klassen wiederzuverwenden. Dazu werden mehrere COMKlassen zu einer logischen Einheit zusammengefasst. Die zusammengesetzten Klassen sehen nach außen hin immer wie eine COM-Klasse aus.
Wiederverwendung von COMKomponenten
310
3 Einbettung
COM, OLE und ActiveX
Einbettung ist die einfachere der beiden Wiederverwendungstechniken. Eine äußere Klasse benutzt eine innere COM-Klasse, deren Funktionalität sie verwenden möchte. Die innere COM-Klasse weiß nicht, dass sie als eingebettete Klasse verwendet wird, und muss daher keine besonderen Anforderungen erfüllen. Keine Schnittstellen der inneren Klasse werden nach außen hin sichtbar. Sollen Schnittstellen der inneren Klasse nach außen hin sichtbar werden, so müssten diese explizit von der äußeren Klasse bereitgestellt und dann an die innere Klasse delegiert werden. Bei der Einbettung tritt die äußere Klasse als Client der inneren Klasse auf. Abbildung 3.15 zeigt ein Beispiel für eine eingebettete Klasse. In diesem Beispiel delegiert die äußere COM-Klasse die Methoden der Schnittstelle ISquare an die innere Klasse und implementiert die Schnittstelle ISquareRoot selbst. IUnknown
Die Aggregation von COM-Klassen verfolgt ein etwas weitreichenderes Ziel als die Einbettung. Auch bei der Aggregation gibt es äußere und innere Klassen. Im Gegensatz zur Einbettung sollen allerdings Schnittstellen der inneren Klasse direkt als Schnittstellen der zusammengesetzten Klasse erscheinen. Dazu muss die innere Klasse bestimmte Bedingungen erfüllen, um auf den Fall, dass sie als innere Klasse agieren muss, vorbereitet zu sein. Ob ein Objekt der innere Teil einer Aggregation ist, wird diesem bei seiner Erzeugung mitgeteilt. Der Methode CreateInstance der Schnittstelle IClassFactory wird als erster Parameter der Zeiger pUnkOuter auf eine IUnknown-Schnittstelle übergeben. Wenn dieser Zeiger ungleich NULL ist und somit auf eine gültige Schnittstelle zeigt, dann soll das zu erzeugende Objekt als inneres Objekt an einer Aggregation teilnehmen.
Das Komponentenobjektmodell
Bei Aggregationen gelten besondere Regeln in Bezug auf die IUnknown-Schnittstelle. Es muss zunächst sichergestellt werden, dass das Aggregat (die Aggregation zweier oder mehrerer COMObjekte) als Ganzes eine konsistente Referenzzählung hat. Zusätzlich müssen Aufrufe der Methode QueryInterface so behandelt werden, dass das Aggregat nach außen hin wie eine COM-Klasse erscheint. Dazu werden bei Aggregaten Aufrufe von Methoden der Schnittstelle IUnknown an das innere Objekt geleitet. Dieses hat die Aufgabe, die Methodenaufrufe seiner IUnknown-Schnittstelle an die äußere Klasse zu delegieren. Ob das innere Objekt Teil einer Aggregation ist, weiß es durch den Zeiger pUnkOuter, der bei der Erzeugung des Objekts der Methode CreateInstance übergeben wurde und der auf das Objekt der äußeren Klasse verweist. (Dieser Zeiger wird auch als kontrollierender IUnknown-Zeiger bezeichnet.) Diesen Zeiger muss sich das innere Objekt merken und später dazu verwenden, die Methodenaufrufe an IUnknown an das Objekt der äußeren Klasse zu delegieren. Die äußere Klasse wertet die Schnittstellenanforderung in seiner Implementierung von QueryInterface aus und delegiert sie – falls nötig – wieder an die innere Klasse zurück.
311 Aggregation und IUnknown
Nun kann dieser Mechanismus so noch nicht richtig funktionieren, da die gegenseitige Delegation zwischen innerer und äußerer Klasse in einer Endlosschleife endet. Das Problem wird dadurch gelöst, dass die innere Klasse zwei Implementierungen von IUnknown bereitstellt: eine Implementierung, auf die externe Clients zugreifen, und eine weitere, auf die nur die äußere Klasse Zugriff hat. Die Implementierung für externe Clients delegiert ihre Methodenaufrufe an die äußere Klasse, während die Implementierung für den Zugriff durch die äußere Klasse die normale Semantik der IUnknown-Schnittstelle implementiert, also Referenzzählung betreibt und selbst Schnittstellenzeiger zurückgibt, ohne Methodenaufrufe an andere Objekte zu delegieren. Die MFC implementieren genau diesen Mechanismus in der Klasse CCmdTarget. Dazu verwenden die MFC die in Listing 3.20 gezeigten Funktionen. // Diese Versionen delegieren nicht an m_pOuterUnknown DWORD InternalQueryInterface(const void*, LPVOID* ppvObj); DWORD InternalAddRef(); DWORD InternalRelease();
Aggregation und MFC
312
3
COM, OLE und ActiveX
// Diese Versionen delegieren an m_pOuterUnknown DWORD ExternalQueryInterface(const void*, LPVOID* ppvObj); DWORD ExternalAddRef(); DWORD ExternalRelease(); Listing 3.20: Zwei Implementierungen von IUnknown in CCmdTarget
Die Funktionen ExternalAddRef, ExternalRelease und ExternalQueryInterface delegieren natürlich nur dann an den IUnknown-Zeiger der äußeren Klasse, wenn wirklich eine Aggregation vorliegt. Ist die COM-Klasse, in der diese Funktionen verwendet werden, nicht Teil einer Aggregation, dann delegieren die Funktionen an die Funktionen InternalAddRef, InternalRelease und InternalQueryInterface. Bei der Implementierung von eigenen COM-Schnittstellen sollte daher immer an die externen Versionen delegiert werden. Genau dies wird im Beispielprogramm MFCServer gezeigt. Abbildung 3.16 stellt ein Beispiel für ein Aggregat dar. In diesem Beispiel wird die Schnittstelle ISquare der inneren Klasse direkt als Schnittstelle des Aggregats nach außen hin sichtbar.
IUnknown
kontrollierendes IUnknown
ISquareRoot
ISquare
Abbildung 3.16: Aggregation bei COM-Objekten
3.2.22 Ausführungsmodelle bei COM-Servern Prozessinterne Server
Bei allen in diesem Kapitel vorgestellten COM-Beispielen sind bisher prozessinterne COM-Server verwendet worden. Prozessintern bedeutet, dass der COM-Server im gleichen Prozess und damit auch im gleichen Adressraum wie der Client-Prozess läuft. Prozessinterne Server werden, wie in den Beispielen gezeigt worden ist, in Form von DLLs implementiert. Der Vorteil prozessinterner
Das Komponentenobjektmodell
313
Server ist ihre Geschwindigkeit. Der Aufruf von Methoden eines prozessinternen COM-Servers ist kaum langsamer als der Aufruf virtueller C++-Funktionen. Der größte Nachteil von prozessinternen Servern ist der fehlende Speicherschutz. Da Client und Server im gleichen Adressraum ausgeführt werden, sind sie nicht gegen Speicherzugriffsverletzungen und ähnliche Fehler voreinander geschützt. Stürzt der Client oder der Server ab, so reißt er den jeweils anderen mit sich. Neben prozessinternen Servern gibt es prozessexterne Server. Diese laufen in einem eigenen Prozess ab und haben folglich auch ihren eigenen Adressraum. Durch die verschiedenen Adressräume sind Client und Server voreinander geschützt. Ein Absturz des einen hindert den anderen nicht daran weiterzulaufen. Prozessexterne Server werden in Form von eigenständigen Programmen (EXE) implementiert.
Prozessexterne Server
Die größere Stabilität prozessexterner Server wird durch ein schlechteres Laufzeitverhalten erkauft. Damit die Grenze der Adressräume zwischen Client- und Serverprozess überbrückt werden kann, ist spezieller Programmcode notwendig. Parameter von Methodenaufrufen müssen im Client-Prozess eingepackt, durch ein geeignetes Verfahren zur Interprozesskommunikation an den Serverprozess gesendet und dort wieder ausgepackt werden. Das Ein- und Auspacken der Methodenparameter wird als Marshalling bezeichnet. Das Marshalling wird dem Programmierer normalerweise von der COM-Laufzeitbibliothek abgenommen. Unter bestimmten Umständen kann der Programmierer allerdings auch seine eigenen Marshalling-Funktionen schreiben. Dazu müssen einige dafür vorgesehene COM-Schnittstellen implementiert werden. Mit der Einführung von Distributed COM (DCOM) ist das prozessexterne Ausführungsmodell von COM-Servern so erweitert worden, dass der Serverprozess auch auf einem anderen Computer im Netzwerk residieren kann. Die Kommunikation zwischen Client und Server wird wieder durch Marshalling und die Übertragung der Daten durch ein Netzwerkprotokoll durchgeführt. Für den Programmierer ist die Verwendung von DCOM transparent, er muss sich normalerweise nicht mit den Aspekten der Datenübertragung über das Netzwerk beschäftigen. Natürlich muss man im Auge behalten, dass das Laufzeitverhalten von entfernt ausgeführten Servern meist noch schlechter ist als das von
DCOM
314
3
COM, OLE und ActiveX
prozessexternen Servern, die auf dem gleichen Computer ausgeführt werden. Dafür ist es möglich, Anwendungen auf eine Anzahl von Computern im Netzwerk zu verteilen und parallel ausführen zu lassen.
3.2.23 COM+ Dreischichtenarchitektur
Zusammen mit Windows 2000 hat Microsoft die COM+-Architektur eingeführt. COM+ ist vor allen Dingen für die Erstellung sog. Drei- oder Mehrschichtenarchitekturen interessant. Bei einer solchen Architektur wird eine Anwendung in drei oder mehr Schichten unterteilt. Den einzelnen Schichten werden unterschiedliche Funktionen zugewiesen. Den grundsätzlichen Aufbau einer 3-Schichten-Anwendung zeigt Abbildung 3.17.
Zu unterst befindet sich fast immer eine Datenzugriffsschicht. Diese Schicht kapselt den Zugriff auf Daten in einem Datenspeicher. Meist ist dieser Datenspeicher eine relationale Datenbank. Über der Datenzugriffsschicht liegt die Logikschicht. Bei unternehmensinternen Anwendungen spricht man hier oft auch von der Geschäftsprozesslogik. Ganz oben befindet sich die Darstellungs- oder Präsentationsschicht. Die Präsentationsschicht kann durch ein normales Windows-Programm oder durch eine HTMLbasierte Darstellung realisiert werden. Der Vorteil von Mehrschichtenarchitekturen liegt in ihrer Flexibilität. Einzelne Schichten können zum Beispiel nachträglich neu implementiert werden. Hat man bisher seine Daten in Dateien gespeichert, so kann man die Datenzugriffsschicht so abändern, dass später eine relationale Datenbank verwendet wird. Die höher liegenden Schichten sind davon nicht betroffen. Ebenso sind mehrere parallele Implemen-
Das Komponentenobjektmodell
tierungen einer Schicht denkbar. So kann man einerseits seine Daten in einem speziellen Programm darstellen, andererseits auch als HTML-Darstellung ausgeben. Wichtig bei Mehrschichtenarchitekturen ist die Skalierbarkeit. So können die einzelnen Schichten auf verschiedene Rechner verteilt werden. Vielfach kann auch innerhalb einer Schicht skaliert werden. So ist es durchaus üblich, die Logikschicht auf eine ganze Reihe von Computern zu verteilen, wenn die anfallende Rechenlast entsprechend groß ist. Hier kommt COM+ ins Spiel. COM+ ist eine Ausführungsumgebung für COM-Komponenten, die die einfache Erstellung von Mehrschichtenarchitekturen unterstützt. COM+ bietet dazu eine Reihe von Dienstleistungen, die eine COM-Komponente verwenden kann. Dazu gehören: 왘 Transaktionssicherheit. Alle Datenzugriffe innerhalb einer Komponente oder einer Gruppe von Komponenten kann innerhalb einer Transaktion durchgeführt werden. Hierzu setzt die Komponente einen Startpunkt und führt anschließend die gewünschten Datenmanipulationen durch. Kommt es während dieser Verarbeitungsschritte zu einem Fehler, so werden alle nach dem gesetzten Startpunkt vorgenommenen Änderungen rückgängig gemacht (Rollback). Sollten keine Fehler auftreten, so erreicht die Komponente einen vorher gesetzten Endpunkt und die durchgeführten Änderungen werden festgeschrieben (Commit). 왘 Dynamic Load Balancing. Hierbei werden Zugriffe auf verschiedene Instanzen von Komponenten verteilt, die sich auf verschiedenen Rechnern befinden. Die Verteilung der Zugriffe erfolgt anhand der Reaktionszeit der Komponenteninstanz, die einen Indikator für die tatsächliche Last darstellt. 왘 Object Pooling. Hierbei werden Instanzen einer Komponente bereits vor dem ersten Zugriff sozusagen »auf Vorrat« angelegt. Außerdem werden diese Objekte nicht zerstört, wenn ihr Referenzzähler den Wert 0 erreicht. Stattdessen werden sie für spätere Zugriffe aufbewahrt. Dies kann in bestimmten Situationen die Reaktionszeit der Gesamtanwendung verbessern. 왘 Queued Components und Loosely Coupled Events. Durch diese Mechanismen können Komponenten voneinander entkoppelt werden. Methodenaufrufe erfolgen asynchron und blockieren nicht die Anwendung.
315
316
3
COM, OLE und ActiveX
Möchte man einen oder mehrere der von COM+ zur Verfügung gestellten Dienstleistungen für eine COM-Komponente verwenden, so muss man dazu keinen Programmcode schreiben. Welche Dienstleistungen eine Komponente verwendet, wird allein durch ihre Attribute festgelegt. Eine ausführlichere Beschreibung von COM+ würde leider den Rahmen dieses Buchs sprengen. Daher sei an dieser Stelle auf weiterreichende Literatur verwiesen.
3.2.24 Tipps zur Vorgehensweise 왘 Um COM-Clients mit den MFC zu implementieren, müssen die Optionen AUTOMATION und ACTIVEX-STEUERELEMENTE nicht ausgewählt werden. Es reicht, die COM-Laufzeitbibliothek durch einen Aufruf der Funktion AfxOleInit zu initialisieren. 왘 Zugriff auf einen COM-Server erhält man durch den Aufruf der Funktion CoGetClassObject aus der Laufzeitbibliothek. Diese Funktion liefert einen Zeiger auf eine Schnittstelle der Klassenfabrik. Die Klassenfabrik implementiert per Konvention die Schnittstelle IClassFactory. Üblicherweise werden COM-Objekte durch den Aufruf der Funktion CreateInstance der Schnittstelle IClassFactory instanziiert. Die Funktion CoCreateInstance der COM-Laufzeitbibliothek fasst die beiden Vorgänge in einem Aufruf zusammen und erzeugt so direkt ein COM-Objekt. 왘 Durch den Aufruf von QueryInterface können Schnittstellenzeiger angefordert werden. Durch QueryInterface kann zudem festgestellt werden, ob ein COM-Server eine Schnittstelle implementiert. 왘 Nicht mehr benötigte Schnittstellenzeiger müssen durch einen Aufruf von Release freigegeben werden. 왘 Für die Implementierung von COM-Servern mit den MFC existiert leider kein Assistent. Am schnellsten kommt man zum Ziel, indem man die Option AUTOMATION auswählt und den Quelltext so verändert, dass statt eines Automationsservers ein COM-Server erzeugt wird. 왘 Prozessinterne Server sollten mit dem Anwendungs-Assistenten für DLLs als normale MFC-DLLs angelegt werden. Prozessexterne Server werden ganz normal mit dem AnwendungsAssistenten angelegt.
Das Komponentenobjektmodell
왘 Die vom Anwendungs-Assistenten generierte IDL-Datei muss so abgeändert werden, dass statt eines Automationsservers ein COM-Server implementiert wird. Anstelle von Automationsschnittstellen (Schlüsselwort dispinterface) müssen normale COM-Schnittstellen (Schlüsselwort interface) definiert werden. Die vom Anwendungs-Assistenten erzeugten Verteilertabellen zur Automation (zu erkennen anhand der Makros DECLARE_DISPATCH_MAP, BEGIN_DISPATCH_MAP und END_DISPATCH_MAP) können gelöscht werden. 왘 In der Header-Datei, die die COM-Klasse implementiert, müssen die COM-Schnittstellenmethoden zwischen den Makros BEGIN_INTERFACE_PART und END_INTERFACE_PART angegeben werden. 왘 In der Implementierungsdatei muss mit dem Makro INTERFACE_PART eine Zuordnung zwischen der COMSchnittstelle, der sie implementierenden Klasse und der einbettenden Klasse hergestellt werden. INTERFACE_PART-Makros sind zwischen den Makros BEGIN_INTERFACE_MAP und END_INTERFACE_MAP zu platzieren. 왘 Durch die Makros DECLARE_OLECREATE und IMPLEMENT_ OLECREATE wird eine Klassenfabrik zur Verfügung gestellt. 왘 Schließlich müssen die COM-Methoden innerhalb der eingebetteten Schnittstellenklasse implementiert werden. Es ist zu beachten, dass dem Namen der eingebetteten C++-Schnittstellenklasse automatisch ein »X« vorangestellt wird. Aufrufe der Methoden von IUnknown sind an die MFC-Implementierung zu delegieren (ExternalQueryInterface, ExternalAddRef und ExternalRelease). 왘 Einen Zeiger auf die einbettende Klasse kann man durch das Makro METHOD_PROLOGUE erhalten. 왘 Möchte man die attributierte Programmierung zur Erstellung von COM-Servern verwenden, so sollte man sich bewusst machen, dass hierbei die ATL verwendet wird. So sind die entsprechenden ATL-Header-Dateien einzubinden.
317
318
3
COM, OLE und ActiveX
3.2.25 COM im Kontext anderer Windows-Technologien
...
OLE DB
DirectX
ActiveX Steuerelemente
OLE
Automation
Nach dieser Einführung in die COM-Programmierung soll betrachtet werden, wie COM in der Praxis verwendet wird. COM in seiner unmittelbaren Form, das heißt durch Definition und Implementierung eigener Schnittstellen, wird relativ selten verwendet. Meist verwendet man auf COM basierende Technologien wie OLE, Automation oder DirectX. Die meisten der neueren Windows-Technologien basieren mittlerweile auf COM. Abbildung 3.18 zeigt einige auf COM basierende Windows-Technologien.
COM Abbildung 3.18: Verschiedene COM-Technologien
Was bedeutet »auf COM basierend«? Auf COM basierend bedeutet, dass COM-Schnittstellen definiert werden, die eine Technologie beschreiben. Für die von Microsoft definierten Technologien sind alle Schnittstellen bereits festgelegt und dokumentiert. Bei der Arbeit mit COM-basierten Technologien werden normalerweise keine eigenen Schnittstellen verwendet. Wer eine COMbasierte Technologie verwenden möchte, muss bereits definierte COM-Schnittstellen ansprechen und teilweise auch implementieren. Die Automation beispielsweise beruht zum größten Teil auf einer COM-Schnittstelle namens IDispatch. Wer einen Automationsserver ansprechen möchte, der muss dessen IDispatchSchnittstelle ansprechen. Wer einen Automationsserver programmieren möchte, der muss selbst eine IDispatch-Schnittstelle bereitstellen (wie das geht, wird im Folgenden gezeigt).
Das Komponentenobjektmodell
319
Da auf COM basierende Technologien über ihre Schnittstellen definiert werden, können Implementierungsfragen zunächst völlig vernachlässigt werden. Systeme werden logisch definiert, die Implementierung wird nicht betrachtet. Durch die Eigenschaften von COM ist es möglich, (Teil-)Systeme zu einem späteren Zeitpunkt zu reimplementieren, ohne die Integrität des Gesamtsystems zu gefährden. Systeme können in beliebigen Programmiersprachen implementiert werden, solange eine COM-Bindung für diese Sprachen existiert. Programmiersprachen können beliebig gemischt werden, theoretisch kann jede COM-Klasse in einer anderen Sprache implementiert werden.
Systemdefinition durch Schnittstellen
Die Eigenschaften von COM wirken sich auf die darauf aufbauenden Technologien aus. Weil COM programmiersprachenunabhängig ist, sind auch Technologien wie Automation und OLE automatisch programmiersprachenunabhängig.
Unabhängigkeit von der Programmiersprache
In der Praxis sind die COM-Schnittstellen einiger Windows-Technologien allerdings so komplex (beispielsweise OLE), dass man bereits versucht, Implementierungen in Form von Klassenbibliotheken und Frameworks (ATL, MFC) oder innerhalb der Laufzeitumgebung (Visual Basic) zu liefern.
3.2.26 Zusammenfassung COM ist zunächst ein Objektmodell, weniger eine Technologie, die unabhängig vom Betriebssystem Windows einen Standard für sprachunabhängige, völlig gekapselte und auf Wunsch auch über Netzwerke verteilte Softwarekomponenten definiert. Vieles bei COM basiert nur auf Konventionen. Unterstützung erhält der COM-Programmierer von der COM-Laufzeitbibliothek, die ihm Dinge wie Marshalling und das Suchen von Objekten in der Registrierungsdatenbank abnimmt. Zusätzlich bekommt der Programmierer von vielen Entwicklungssystemen Unterstützung bei der Implementierung von COM-Klassen und -Schnittstellen. Die MFC bieten eine gute Hilfestellung bei der Implementierung von COMServern. Durch Makros werden Schnittstellenklassen angelegt, die Klassenfabrik wird implementiert und die Unterstützung für die COM-Aggregation ist vorhanden. COM dient als Fundament vieler Windows-Technologien.
COM und Windows
Unter dem Strich kommt ein großer Teil der Leistungsfähigkeit von COM durch die Einfachheit der Technologie zustande. COM setzt auf einem niedrigen Niveau an. Es definiert zunächst nur
Formbarkeit durch Einfachheit
320
3
COM, OLE und ActiveX
eine Reihe von Konventionen (Methodenaufruf und Layout im Speicher, Verwendung von Schnittstellen, IUnknown) und bietet wenig Unterstützung für die Implementierung. Eine Objekttechnologie kann sicherlich auch auf höherem Niveau definiert werden, indem man mehr Implementierungsunterstützung bietet. Aber gerade durch seine Einfachheit ist COM sehr flexibel und formbar. Aufbauend auf diesem Fundament ist mit COM+ mittlerweile eine sehr komplexe Laufzeitumgebung verfügbar, mit der COM-Objekte auf einfache Weise Zugriff auf Dienstleistungen wie Transaktionssicherheit, Load Balancing und Object Pooling bekommen können. Damit lassen sich moderne MehrschichtenApplikationen auf recht einfache Weise entwickeln. Inwieweit COM durch Teile der neuen .NET-Architektur abgelöst werden wird, ist noch nicht abzusehen. COM-Komponenten können allerdings aus .NET-Programmen heraus verwendet werden, so dass ein langsamer Übergang zwischen beiden Technologien möglich ist.
3.3
Automation
DDE
Automation, früher auch als OLE-Automatisierung, manchmal auch als Automatisierung bezeichnet, lässt sich aus vielen Blickwinkeln betrachten. Historisch gesehen ist die Automation ein Nachfolger des Dynamischen Datenaustausches (DDE) auf der einen Seite und einer Unzahl von applikationsspezifischen Makrosprachen auf der anderen Seite. DDE ist eine Windows-Technologie, die dem programmübergreifenden Datenaustausch von Windows-Programmen dient. Da DDE jedoch kompliziert zu programmieren und außerdem langsam ist, hat es sich nie in größerem Umfang durchsetzen können. DDE wird heute nur noch sehr eingeschränkt verwendet, beispielsweise um Dateien an ein bereits laufendes Programm zu übergeben.
Makrosprachen
Makrosprachen werden verwendet, um Applikationen dadurch flexibler zu gestalten, dass der Anwender Funktionsabläufe in einer in der Applikation verwendbaren Programmiersprache automatisieren kann. Da es unter Windows bisher keine systemweite Makro- oder Skriptsprache gab (die MS-DOS-Stapelverarbeitung ist dazu unbrauchbar), führte bisher jedes Programm seine eigene Makrosprache ein. Word wurde in Word-Basic programmiert, StarWriter in Star-Basic usw. Dies hatte die fatale
Automation
321
Folge, dass man für jedes Programm eine eigene Sprache lernen musste. Teile von Makro-Programmen ließen sich meist nicht in andere Anwendungen übernehmen. Microsoft hat zwar für seine eigenen Produkte mittlerweile mit Visual Basic for Applications (VBA) eine einheitliche Makrosprache geschaffen, doch lässt sich diese nicht in Konkurrenzprodukten verwenden. Andere Betriebssysteme lösen dieses Problem, indem sie systemweite Skriptsprachen festlegen (wie AppleScript unter MacOS); unter Windows heißt die Lösung dazu Automation. Das Betriebssystem selbst kann man per Automation durch den Windows Scripting Host (WSH) steuern. Die Automation lässt sich am einfachsten als Fernsteuerungstechnologie beschreiben. Ein Programm steuert ein anderes und kann auf dessen Daten zugreifen. Im Gegensatz zu einer systemweiten Skriptsprache ist die Automation jedoch völlig programmiersprachenunabhängig. Sowohl das gesteuerte Programm – der Automationsserver – als auch das steuernde Programm – der AutomationsClient – können in beliebigen Programmiersprachen verfasst werden. Voraussetzung ist allerdings, dass die verwendete Sprache die Erstellung von Automationsservern oder Automations-Clients erlaubt. Die Sprachunabhängigkeit macht die Automation flexibler als eine systemweite Skriptsprache.
Client und Server
Auf logischer Ebene kann die Automation als Ausweitung des objektorientierten Ansatzes (allerdings ohne Vererbung) auf ganze Programme betrachtet werden. Das Programm selbst entspricht einer Klasse, eine in einem Prozess ausgeführte Instanz des Programms entspricht einem Objekt. Über die Automation kann das Programm Methoden bereitstellen, die von anderen Programmen aufgerufen werden können. Außerdem gibt es die Möglichkeit, Variablen – bei der Automation als Eigenschaften bezeichnet – direkt nach außen hin zugänglich zu machen.
Analogie zur Objektorientierung
Technisch gesehen basiert die Automation auf einer einzigen COMSchnittstelle namens IDispatch. Diese Schnittstelle besitzt eine Reihe von Methoden, über die es möglich wird, den Automationsserver, der diese Schnittstelle implementieren muss, durch eine als späte Bindung bezeichnete Technik anzusprechen. Späte Bindung bedeutet, dass der Automations-Client zum Übersetzungszeitpunkt keine Typinformationen des Servers kennen muss. Eine (schwache) Typprüfung findet erst zur Laufzeit statt. Eventuell müssen zur Laufzeit Typkonvertierungen vorgenommen werden. Dagegen
Späte Bindung
322
3
COM, OLE und ActiveX
muss bei der frühen Bindung, wie sie bei normalen COM-Schnittstellen verwendet wird, die gesamte Typinformation bereits zum Übersetzungszeitpunkt zur Verfügung stehen. Abbildung 3.19 zeigt, wie die späte Bindung funktioniert. Automationsschnittstelle
Bei der späten Bindung in der Form, wie sie die Automation verwendet, werden alle Automationsmethodenaufrufe und das Setzen von Automationseigenschaften durch eine COM-Methode geleitet, die Teil der IDispatch-Schnittstelle ist. Diese Methode funktioniert gleichsam wie ein Tunnel, durch den alle Automationsaufrufe hindurchmüssen. Da alle Automationsaufrufe durch diesen Tunnel gehen, muss ein Programm, das die Automation verwenden möchte, nur die Typinformation dieses Tunnels, also der Automationsschnittstelle IDispatch selbst, kennen. Damit wird die späte Bindung erst möglich. Die relativ geringen Anforderungen der IDispatch-Schnittstelle erlauben es, die Unterstützung für Automations-Clients recht einfach in eine Programmiersprache – insbesondere auch eine interpretierte Skriptsprache – zu integrieren.
3.3.1
Ein erstes Beispiel
Um die Automation etwas anschaulicher zu machen, soll zunächst ein einfacher Automations-Client vorgestellt werden. Als Server wird eine um Automationsfunktionen erweiterte Version des Pro-
Automation
323
gramms StockChart aus Kapitel 2, »Einstieg in die MFC-Programmierung«, verwendet. Diese Version von StockChart heißt StockChartAuto und befindet sich im Verzeichnis KAPITEL3\STOCKCHARTAUTO auf der Begleit-CD. Sie wird im Anschluss besprochen. Der Client ist mit Visual Basic .NET erstellt worden. Das Projekt befindet sich im Verzeichnis KAPITEL3\FIRSTAUTOCLIENT auf der Begleit-CD. Abbildung 3.20 zeigt das Programm FirstAutoClient bei der Ausführung.
Client in Visual Basic
Abbildung 3.20: Automations-Client bei der Ausführung
Durch den Start des Programms FirstAutoClient wird automatisch ein neues Dokument im Programm StockChartAuto angelegt. Sollte StockChartAuto noch nicht ausgeführt werden, so wird es automatisch gestartet. (Das Programm StockChartAuto muss bereits in der Registrierungsdatenbank eingetragen sein. Dies lässt sich am einfachsten erreichen, indem man es einmal startet. Das Programm registriert sich dann selbst.) Mit dem Programm FirstAutoClient kann man Daten in das neu angelegte Dokument importieren und Aktienname, WKN und Tickersymbol setzen. Beendet man FirstAutoClient, wird auch der Automationsserver StockChartAuto beendet, sofern keine anderen offenen Dokumente vorhanden sind. Der Automations-Client benutzt einen Teil der von StockChartAuto bereitgestellten Methoden und Eigenschaften. Tabelle 3.1 zeigt diese Methoden und Eigenschaften. Name
Art
Beschreibung
average
Eigenschaft
Durchschnittslinie an/aus
averagecnt
Eigenschaft
Zähler für Durchschnittslinie
grid
Eigenschaft
Gitternetz an/aus
Tabelle 3.1: Methoden und Eigenschaften des Automationsservers StockChartAuto
Parameter
Methoden und Eigenschaften des Servers
324
3
Name
COM, OLE und ActiveX
Art
Beschreibung
Parameter
id
Eigenschaft
Wertpapierkennnummer
name
Eigenschaft
Name der Aktie
statusbar
Eigenschaft
Zugriff auf eine weitere Automationsschnittstelle
statusbar.status
Eigenschaft der zweiten Schnittstelle
Text in der Statusleiste
ticker
Eigenschaft
Tickersymbol
ImportFile
Methode
Importiert Daten in das Dokument
SaveFile
Methode
Speichert das Dokument
Dateipfad
SetColor
Methode
Setzt die Farbe des Charts
Rot-, Grünund Blauanteil
ShowWindow
Methode
Zeigt die Ansicht des Dokuments an
Dateipfad
Tabelle 3.1: Methoden und Eigenschaften des Automationsservers StockChartAuto (Fortsetzung) Server mit zwei Schnittstellen
Das Programm StockChartAuto stellt zwei Automationsschnittstellen bereit. Eine Schnittstelle legt Eigenschaften und Methoden der Dokumentenklasse offen. Die zweite Schnittstelle erlaubt den Zugriff auf die Statusleiste des Programms. Die zweite Schnittstelle ist eine Eigenschaft der ersten und kann nur über diese erreicht werden. Die erste Schnittstelle dagegen ist eine benannte Schnittstelle, das heißt, sie kann über Einträge in der Registrierungsdatenbank von Windows (Registry) gefunden werden. Dabei wird der Registrierungsname der Dokumentenklasse »StockAuto.Document« verwendet. Dim StockChartAuto As Object Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() 'Add any initialization after the InitializeComponent() call StockChartAuto = CreateObject("StockAuto.Document") StockChartAuto.ShowWindow() End Sub
Automation
325
Private Sub ButtonImport_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonImport.Click OpenFileDialogImport.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*" OpenFileDialogImport.ShowDialog() StockChartAuto.ImportFile(OpenFileDialogImport.FileName) End Sub Private Sub ButtonSet_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonSet.Click If (TextBoxWKN.Text.Length > 0) Then StockChartAuto.ID = TextBoxWKN.Text End If StockChartAuto.Name = TextBoxName.Text StockChartAuto.Ticker = TextBoxTicker.Text End Sub Listing 3.21: Ausschnitt aus dem VB-Programm FirstAutoClient
Listing 3.21 zeigt einen Ausschnitt aus dem Beispielprogramm FirstAutoClient. Man kann sehen, dass die Ansprache des Automationsservers recht einfach ist. Der Automationsserver wird als Objekt deklariert und beim Laden des Programms durch den Aufruf von CreateObject gestartet. CreateObject bekommt als Parameter den Klassennamen der Automationsklasse übergeben, im Beispiel StockAuto.Document. Der anschließende Aufruf der Methode ShowWindow des Automationsservers macht das Serverprogramm und die zum Serverdokument gehörende Ansicht sichtbar. Dies geschieht bereits beim Laden des Client-Programms, da die Servermethode ShowWindow aus der Funktion New der Form aufgerufen wird. Generell werden per Automation gestartete Programme zunächst unsichtbar gestartet: Es kann schließlich sein, dass man nur die Funktionalität des Programms ausnutzen möchte, ohne dem Benutzer den Zugriff auf das Programm über die Benutzeroberfläche zu erlauben. Das Serverprogramm muss also immer explizit sichtbar gemacht werden, falls dies gewünscht ist. Ein Klick auf die Schaltfläche SETZE ruft im Beispielprogramm FirstAutoClient die Funktion ButtonSet_Click auf. Innerhalb dieser Funktion werden die Eigenschaften ID, Name und Ticker zugewiesen. Klickt man auf IMPORTIERE, dann wird im Client-Programm die Funktion ButtonImport_Click aufgerufen. Diese demonstriert
Programmcode des Clients
326
3
COM, OLE und ActiveX
den Aufruf der Servermethode ImportFile. ImportFile bekommt einen vollständigen Dateipfad aus dem FileOpenDialog-Element übergeben. Neben Visual Basic .NET lassen sich Automationsserver genauso mit Visual Basic Script oder mit Visual Basic for Applications (VBA) ansteuern. Wie man Automationsserver mit Visual Basic Script (VBS) und Javascript ansteuern kann, wird noch gezeigt.
3.3.2
Die Schnittstelle IDispatch
Der zentrale Punkt der Automation ist die Schnittstelle IDispatch. Listing 3.22 zeigt die Definition der Schnittstelle IDispatch in IDLNotation. interface IDispatch : IUnknown { typedef [unique] IDispatch * LPDISPATCH; HRESULT GetTypeInfoCount( [out] UINT * pctinfo ); HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid, [out] ITypeInfo ** ppTInfo ); HRESULT GetIDsOfNames( [in] REFIID riid, [in, size_is(cNames)] LPOLESTR * rgszNames, [in] UINT cNames, [in] LCID lcid, [out, size_is(cNames)] DISPID * rgDispId ); HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in, out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr ); Listing 3.22: IDispatch in IDL-Notation
Automation
327
IDispatch ist direkt von IUnknown abgeleitet und verfügt über vier eigene Methoden. Zum Verständnis der IDispatch-Schnittstelle sind die beiden Methoden GetTypInfo und GetTypeInfoCount unwichtig. Tatsächlich funktioniert ein Automationsserver ohne eine Implementierung dieser Methoden. Die Methoden dienen dazu, Typinformationen aus einer gegebenenfalls vorhandenen Typbibliothek zugänglich zu machen. Sämtliche Automationsmethodenaufrufe sowie das Lesen und Setzen von Eigenschaften werden durch Aufrufe der COMMethode Invoke durchgeführt. Invoke ist der bereits erwähnte Tunnel, durch den alle Automationsaufrufe hindurchmüssen. Die Methode oder Eigenschaft wird beim Aufruf von Invoke nicht durch ihren Namen, sondern über eine ihr zugewiesene Zahl, die DISPID, identifiziert. DISPIDs können vom Automationsserver beliebig vergeben werden, solange sie positiv sind. Alle negativen DISPIDs und der Wert 0 haben entweder vorgegebene Bedeutungen oder sind für die zukünftige Verwendung durch Microsoft reserviert.
Invoke
Die Verbindung zwischen einer DISPID und einem Methodenoder Eigenschaftsnamen muss vom Automationsserver hergestellt werden. Dies ist Aufgabe der Methode GetIDsOfNames. GetIDsOfNames bekommt den Namen einer Methode oder Eigenschaft sowie – im Falle einer Methode – die Namen der Parameter übergeben und liefert die dazugehörende DISPID zurück. Mit dieser DISPID lässt sich per Invoke die entsprechende Methode aufrufen beziehungsweise die Eigenschaft lesen oder setzen.
GetIDsOfNames
Die große Anzahl von Parametern der Methode Invoke ist notwendig, um sowohl Eigenschaften setzen und lesen zu können, als auch Methodenaufrufe zu tätigen. Im Fall von Methodenaufrufen müssen alle Parameter der Automationsmethode an Invoke übergeben werden. Diese werden als Array übergeben. Invoke besitzt außerdem einen Parameter, um den Rückgabewert von Methoden zu erhalten, sowie weitere Parameter zur Fehlerbehandlung.
Der Datentyp VARIANT Die Automation wird oft im Kontext von Skriptsprachen verwendet. Diese Sprachen kennen entweder keine Datentypen oder haben nur eine schwache Typisierung. Um mit dieser Situation im Kontext einer stark typisierten Sprache wie C++ umzugehen, hat Microsoft den Datentyp VARIANT geschaffen. VARIANT definiert
328
3
COM, OLE und ActiveX
eine ganze Reihe von möglichen Datentypen in Form eines union. Welcher Datentyp tatsächlich in einem VARIANT steckt, wird über die Member-Variable vt angegeben. Der Datenaustausch wird bei der Automation ausschließlich über den Datentyp VARIANT abgewickelt, was den Vorteil hat, dass man sich auf der Ebene der IDispatch-Schnittstelle nicht um die Typen der übertragenen Daten kümmern muss. Allerdings muss der Client seine Daten in ein Array aus VARIANTs einpacken und der Server muss sie wieder auspacken, was einige zusätzliche Arbeit bedeutet. Listing 3.23 zeigt die Definition des Typs VARIANT. typedef struct tagVARIANT { VARTYPE vt; WORD wReserved1; WORD wReserved2; WORD wReserved3; union { LONG lVal; BYTE bVal; SHORT iVal; FLOAT fltVal; DOUBLE dblVal; VARIANT_BOOL boolVal; SCODE scode; CY cyVal; DATE date; BSTR bstrVal; IUnknown * punkVal; IDispatch * pdispVal; SAFEARRAY * parray; BYTE * pbVal; SHORT * piVal; LONG * plVal; FLOAT * pfltVal; DOUBLE * pdblVal; VARIANT_BOOL * pboolVal; SCODE * pscode; CY * pcyVal; DATE * pdate; BSTR * pbstrVal; IUnknown ** ppunkVal; IDispatch ** ppdispVal; SAFEARRAY ** pparray; VARIANT * pvarVal; PVOID byref; CHAR cVal; USHORT uiVal; ULONG ulVal; INT intVal;
Sprachen wie Visual Basic lassen den Datentyp VARIANT für den Programmierer gar nicht in Erscheinung treten. Die Laufzeitbibliothek der Sprache kümmert sich um die notwendigen Konvertierungen. Als C++-Programmierer ohne MFC muss man Konvertierungen zwischen Variablen des Typs VARIANT und Datentypen der Sprache C++ selbst vornehmen. Microsoft stellt dazu eine Reihe von Konvertierungsfunktionen und -makros bereit, die die Arbeit erleichtern. Innerhalb der MFC gibt es die Klasse COleVariant, mit der sich Variablen des Typs VARIANT sehr einfach konvertieren lassen. Allerdings bieten die Assistenten von Visual Studio .NET eine weitgehende Unterstützung für die Automation, so dass sich der MFCProgrammierer um solche Details normalerweise nicht kümmern muss.
3.3.3
Konvertierungen in den Datentyp VARIANT
Implementierung eines Automationsservers mit den MFC
Nun soll die Implementierung des Automationsservers StockChartAuto besprochen werden. Das Projekt StockChartAuto ist mit dem Anwendungs-Assistenten angelegt worden. Zusätzlich zu den in Abschnitt 2.4.1, »Der Anwendungs-Assistent«, besprochenen Optionen ist unter ERWEITERTE FEATURES die Unterstützung der Automation ausgewählt worden. Durch die Auswahl dieser Option wird die Dokumentenklasse des Projekts mit einer Automationsschnittstelle versehen und das Programm als Ganzes wird zum Automationsserver. Mit dem Klassen-Assistenten lassen sich anschließend weitere automationsfähige Klassen einfügen.
Programm StockChartAuto
Im Anwendungs-Assistenten wurde unter DOKUMENT-VORLAGENZEICHENFOLGEN die Dateierweiterung STKA und die Dateityp-ID »StockAuto.Document« angegeben. Das hat folgenden Hintergrund: Über die Dateityp-ID wird die automationsfähige Dokumentenklasse von StockChartAuto identifiziert. Die Dateityp-ID
Dateityp-ID des Servers
330
3
COM, OLE und ActiveX
»StockChart.Document« soll nicht noch einmal verwendet werden, da sich bereits die in Kapitel 2, »Einstieg in die MFC-Programmierung«, vorgestellten Beispielprogramme mit dieser ID in der Registrierungsdatenbank von Windows eingetragen haben. Würde das Beispielprogramm StockChartAuto die gleiche Dateityp-ID wie die anderen Beispiele verwenden, so wäre nicht sichergestellt, dass die Anforderung, ein Objekt des Typs »StockChart.Document« zu erzeugen, auch wirklich an StockChartAuto weitergereicht würde. Je nachdem, welches Programm sich in der Registrierungsdatenbank eingetragen hat, wird entweder eine alte Version von StockChart oder die neue, automationsfähige Version gestartet. Um diese Unsicherheit zu vermeiden, ist die neue Dateityp-ID verwendet worden. Damit kann die automationsfähige Version von StockChart eindeutig über ihre eigene Dateityp-ID angesprochen werden. Die vom Anwendungs-Assistenten erzeugte automationsfähige Dokumentenklasse ruft in ihrem Konstruktor die Funktion EnableAutomation auf. Diese Funktion schaltet die Automationsfunktion der Klasse tatsächlich erst an. Der sich anschließende Aufruf der Funktion AfxOleLockApp sorgt dafür, dass die Applikation im Speicher bleibt, solange sie aktive Automationsobjekte besitzt. Versucht der Benutzer, das Programm unter diesen Umständen zu schließen, dann wird es lediglich versteckt, indem das Programmfenster unsichtbar gemacht wird. Beendet wird das Programm erst durch den entsprechenden Aufruf der Funktion AfxOleUnlockApp. Diese wird im Destruktor der automationsfähigen Klasse aufgerufen. Listing 3.24 zeigt die fertigen Implementierungen von Konstruktor und Destruktor der Dokumentenklasse des Programms StockChartAuto. ////////////////////////////////////////////////////////////// / // CStockChartAutoDoc Konstruktion/Destruktion CStockChartAutoDoc::CStockChartAutoDoc() { m_nID = 0; m_bGrid = false; m_bAverage = false; m_nAverageCnt = 15; m_ticker = ""; m_name = ""; m_nColor = RGB(255,0,0);
Automation
331
m_pChartProperty = NULL; EnableAutomation(); AfxOleLockApp(); } CStockChartAutoDoc::~CStockChartAutoDoc() { // Wenn Dialog noch offen, dann schließen und löschen: if (m_pChartProperty) { m_pChartProperty->DestroyWindow (); delete m_pChartProperty; } AfxOleUnlockApp(); } Listing 3.24: Konstruktor und Destruktor der Dokumentenklasse
Ist das Projekt mit der Option AUTOMATION erstellt worden, so lassen sich in der Klassenansicht Methoden und Eigenschaften zur Automationsschnittstelle hinzufügen. Der dazu notwendige Programmcode wird in die Dokumentenklasse eingefügt. Die Methoden und Eigenschaften von IStockChartAuto sind in Abbildung 3.21 zu sehen.
Abbildung 3.21: Die Schnittstelle IStockChartAuto in der Klassenansicht
Wird eine Eigenschaft zur Klasse hinzugefügt, so gibt es zwei Möglichkeiten, diese zu implementieren. Im in Abbildung 3.22 gezeigten Dialog kann man beim Erstellen einer Eigenschaft zwischen den Optionen MEMBER-VARIABLE und GET/SET-METHODEN auswählen. Die Option MEMBER-VARIABLE funktioniert ähnlich
Implementierung von Eigenschaften
332
3
COM, OLE und ActiveX
wie eine DDX-Austauschvariable. Es wird eine Member-Variable in der Klasse angelegt, die direkt über die Automation gelesen oder gesetzt werden kann. Zusätzlich kann eine Benachrichtigungsfunktion implementiert werden, die aufgerufen wird, wenn auf die Eigenschaft zugegriffen wird. Diese Art des Zugriffs eignet sich gut für Eigenschaften, die keinen Einfluss auf die Benutzeroberfläche des Programms haben. Mit der Option GET/SETMETHODEN zur Implementierung von Eigenschaften wird ein Paar von Get- und Set-Methoden bereitgestellt. Die interne Repräsentation des Eigenschaftswerts muss bei dieser Implementierungsart durch den Programmierer realisiert werden. Die beiden Methoden zeigen lediglich an, wann auf den Eigenschaftswert zugegriffen wird, und sorgen für die Über- beziehungsweise Rückgabe des Eigenschaftswerts. Diese Art der Implementierung eignet sich gut, wenn es bereits eine interne Repräsentation des Eigenschaftswerts gibt und wenn Teile der Benutzeroberfläche des Programms bei einer Änderung des Eigenschaftswerts angepasst werden müssen. Dazu gehören beispielsweise alle Eigenschaften des Programms StockChartAuto. Abbildung 3.22 zeigt den Dialog, mit dem Eigenschaften hinzugefügt werden können.
Abbildung 3.22: Eigenschaft hinzufügen mit dem Property Wizard
Innerhalb der Dokumentenklasse CStockChartAutoDoc sind alle Zugriffe auf Eigenschaften durch Set- und Get-Methoden realisiert worden. Dies ist in Listing 3.25 zu sehen, das alle Automations-
Automation
funktionen der Dokumentenklasse zeigt. Am Ende jeder SetMethode wird UpdateAllViews aufgerufen, um die Ansichten des Dokuments den geänderten Werten anzupassen. Die Eigenschaften selbst sind bereits vorhandene Instanzvariablen des Dokuments, deren Werte durch die Automationsmethoden gesetzt oder zurückgegeben werden. //====================== Automationsfunktionen ================= void CStockChartAutoDoc::ShowWindow(void) { AFX_MANAGE_STATE(AfxGetAppModuleState()); POSITION pos = GetFirstViewPosition(); CView* pView = GetNextView(pos); if (pView != NULL) { CFrameWnd* pFrameWnd = pView->GetParentFrame(); pFrameWnd->ActivateFrame(SW_SHOW); pFrameWnd = pFrameWnd->GetParentFrame(); if (pFrameWnd != NULL) pFrameWnd->ActivateFrame(SW_SHOW); } } long CStockChartAutoDoc::GetId(void) { AFX_MANAGE_STATE(AfxGetAppModuleState()); return m_nID; } void CStockChartAutoDoc::SetId(long nNewValue) { AFX_MANAGE_STATE(AfxGetAppModuleState()); m_nID = nNewValue; UpdateAllViews (NULL); SetModifiedFlag (); } BSTR CStockChartAutoDoc::GetName(void) { AFX_MANAGE_STATE(AfxGetAppModuleState()); return m_name.AllocSysString(); } void CStockChartAutoDoc::SetName(LPCTSTR lpszName) {
333
334
3
COM, OLE und ActiveX
AFX_MANAGE_STATE(AfxGetAppModuleState()); m_name = lpszName; UpdateAllViews (NULL); SetModifiedFlag (); } BSTR CStockChartAutoDoc::GetTicker(void) { AFX_MANAGE_STATE(AfxGetAppModuleState()); return m_ticker.AllocSysString(); } void CStockChartAutoDoc::SetTicker(LPCTSTR lpszTicker) { AFX_MANAGE_STATE(AfxGetAppModuleState()); m_ticker = lpszTicker; m_ticker.MakeUpper (); UpdateAllViews (NULL); SetModifiedFlag (); } void CStockChartAutoDoc::SetColor(short red, short green, short blue) { AFX_MANAGE_STATE(AfxGetAppModuleState()); m_nColor = RGB (red,green,blue); UpdateAllViews (NULL); SetModifiedFlag (); } LPDISPATCH CStockChartAutoDoc::GetStatusbar(void) { AFX_MANAGE_STATE(AfxGetAppModuleState()); CStatusBarInterface *statusBar; LPDISPATCH lpResult; // CStatusBarInterface-Objekt löscht sich selbst, wenn keine // Referenzen mehr auf die Schnittstelle vorhanden sind. statusBar = new CStatusBarInterface (); // Zeiger auf IDispatch-Schnittstelle besorgen: lpResult = statusBar->GetIDispatch (false); return lpResult;
Die Methode ShowWindow ist notwendig, um ein per Automation gestartetes Serverprogramm sichtbar zu machen. Dazu sucht die Implementierung von ShowWindow die erste Ansicht des Dokuments, bestimmt das Rahmenfenster der Ansicht und macht dieses sichtbar. Besitzt das Rahmenfenster selbst ein Rahmenfenster (Hauptrahmenfenster in MDI-Anwendungen), dann wird auch dieses sichtbar gemacht.
Server sichtbar machen
Die Funktionen GetName und GetTicker müssen einen OLE-String vom Typ BSTR zurückgeben. Die Klasse CString unterstützt Konvertierungen in diesen Datentyp durch die beiden Member-Funktionen AllocSysString und SetSysString. AllocSysString legt einen neuen BSTR an, während SetSysString einen bereits vorhandenen BSTR überschreibt. Die von der Klasse CString bereitgestellten Konvertierungsfunktionen vereinfachen den Umgang mit BSTR-Objekten bei der Automation und anderen COM-basierten Technologien deutlich. Bei den Eingabeparametern von Automationsmethoden werden ganz normale LPCTSTR-Zeichenfolgen erwartet. Die IDispatch-Implementierung der MFC nimmt die Konvertierung von LPCTSTR-Zeichenfolgen in BSTR-Zeichenfolgen vor.
BSTR und CString
BSTR-Zeichenfolgen lassen sich CString-Objekten zuweisen. Bei der Zuweisung wird entweder der LPCWSTR- oder der LPCSTR-Operator von CString verwendet, je nachdem, ob das Projekt mit Unicode-Unterstützung übersetzt worden ist. In der umgekehrten Richtung ist eine Zuweisung nicht möglich. Interessant ist die Funktion GetStatusBar. Mit dieser Implementierung einer Get-Methode wird der Zugriff auf die Eigenschaft statusbar ermöglicht. Auf die Implementierung der zugehörigen SetMethode wurde verzichtet, da diese Eigenschaft nicht verändert werden soll. Die Eigenschaft statusbar ist selbst eine Automationsschnittstelle. Implementiert wird diese zweite Automationsschnittstelle des Programms StockChartAuto durch eine eigene Klasse mit Automationsunterstützung. Diese Klasse, CStatusBarInterface, ist mit dem Klassen-Assistenten angelegt worden (siehe Abbildung 3.23). Um die Automation zu unterstützen, musste sie von der Klasse CCmdTarget abgeleitet werden, da CCmdTarget die Automationsunterstützung der MFC implementiert. Unter den Optionen zur Automation ist die Option AUTOMATION ausgewählt worden. Diese Option erzeugt eine Automationsschnittstelle, die vom Programm nicht in der Registrierungsdatenbank von Windows eingetragen wird. Um auf eine solche Automationsschnittstelle zugreifen zu
Zugriff auf die zweite Automationsschnittstelle
338
3
COM, OLE und ActiveX
können, macht man sie – wie im Beispielprogramm gezeigt – meist über eine andere Automationsschnittstelle zugänglich. Möchte man eine automationsfähige Klasse anlegen, die genau wie die Dokumentenklasse eines automationsfähigen MFC-Programms über einen Namen erstellbar ist (»StockAuto.Document« im Beispiel), so muss man im Dialog des Klassen-Assistenten ANHAND VON TYPEN-ID ERSTELLBAR statt AUTOMATION wählen. Diese Typ-ID wird vom Programm dann beim ersten Start in die Registrierungsdatenbank von Windows eingetragen.
Abbildung 3.23: Automationsfähige Klasse mit dem Klassen-Assistenten erstellen
Über die Eigenschaft statusbar können Automations-Clients Zugriff auf die durch CStatusBarInterface implementierte Schnittstelle erhalten. Die Implementierung der Get-Methode, die den Zugriff auf statusbar liefert, GetStatusbar, legt ein Objekt der Klasse CStatusBarInterface auf dem Heap an, besorgt durch Aufruf der Funktion CCmdTarget::GetIDispatch einen Zeiger auf dessen IDispatch-Schnittstelle und gibt diesen Zeiger zurück. Wenn der Automations-Client die Schnittstelle freigibt, löscht sich das Objekt der Klasse CStatusBarInterface selbsttätig, da sein Referenzzähler auf 0 gesetzt wird. Die gezeigte Implementierung von GetStatusbar erzeugt für jeden Aufruf der Funktion ein neues Objekt der Klasse CStatusBarInterface. Alternativ kann man mit einer einzigen Instanz der Klasse
Automation
339
CStatusbar arbeiten und bei jeder Schnittstellenanforderung den Referenzähler der IDispatch-Schnittstelle hochzählen. Dazu muss GetIDispatch mit dem Argument true aufgerufen werden. Die Klasse CStatusBarInterface besitzt nur eine einzelne Eigenschaft und keine Methoden. Mit der Eigenschaft status kann der Text in der Statusleiste des Programms gesetzt werden. Im Gegensatz zur Klasse CStockChartAutoDoc wird die Eigenschaft nicht durch Get/Set-Methoden implementiert, sondern durch die Member-Variable m_status. Wenn sich der Wert der Variablen ändert, wird die Funktion OnStatusChanged aufgerufen. Listing 3.26 zeigt die Implementierung von OnStatusChanged. void CStatusBarInterface::OnStatusChanged() { AFX_MANAGE_STATE(AfxGetAppModuleState()); CMainFrame *pFrame; pFrame = (CMainFrame*)AfxGetApp()->m_pMainWnd; pFrame->ShowStatus (m_status); } Listing 3.26: Benachrichtigungsfunktion zu m_status
OnStatusChanged nimmt den Wert der Automationsvariablen m_status und ruft dann die (selbst implementierte) Funktion ShowStatus des Hauptrahmenfensters auf, die den Text in die Statusleiste des Programms schreibt. Man könnte die Eigenschaft status genauso gut durch eine Set-Methode setzen, allerdings sollte hier die alternative Implementierung mit einer Member-Variablen gezeigt werden.
3.3.4
Die Automationsimplementierung der MFC
Analog zur Nachrichtenbehandlung und zur Implementierung von COM-Schnittstellen verwenden die MFC auch zur Implementierung von Automationsschnittstellen Makros. Diese Makros werden von den Assistenten der Entwicklungsumgebung in den Quelltext eingefügt. Die Zuordnung der Automationsmethoden und -eigenschaften zu den sie implementierenden Funktionen erfolgt durch eine Verteilertabelle (Dispatch Map). Die Verteilertabelle wird durch das Makro DECLARE_DISPATCH_MAP deklariert und durch die Makros BEGIN_DISPATCH_MAP und END_DISPATCH_MAP implementiert. Zwischen letztgenannten
An der Verteilertabelle lässt sich erkennen, wie die Datentypen für den Austausch über Variablen des Typs VARIANT kodiert werden. Beispielsweise wird die Eigenschaft grid durch die VARIANTKodierung VT_BOOL dargestellt. Automation und IDL
Neben der Verwaltung der Verteilertabellen pflegen die Assistenten von Visual Studio .NET die IDL-Beschreibung der Automationsschnittstellen. Für das Projekt StockChartAuto werden die Schnittstellen in der Datei STOCKCHART.IDL definiert. Diese ist in Listing 3.28 zu sehen. // StockChartAuto.idl : Quellcode der Typbibliothek für // StockChartAuto.exe // Diese Datei wird vom MIDL-Compiler bearbeitet, um die // Typbibliothek zu erzeugen (StockChartAuto.tlb). [ uuid(CFDB3165-0122-11D2-B51C-006097A8F69A), version(1.0) ]
Im Gegensatz zu einfachen COM-Schnittstellen werden Automationsschnittstellen durch das IDL-Schlüsselwort dispinterface definiert. Eigenschaften und Methoden werden nach den Schlüsselwörtern properties und methods definiert. Jeder Eigenschaft und jeder Methode wird eine DISPID durch das Attribut id vorangestellt. Von diesen Unterschieden abgesehen, gleicht die Definition von Automationsschnittstellen der von normalen COM-Schnittstellen.
3.3.5
Automation mit dem Windows Scripting Host
Es wurde bereits gesagt, dass Windows früher keine systemweite Skriptsprache besessen hat, wie sie die meisten anderen Betriebssysteme haben. Mit der Einführung von Windows 98 hat sich dies geändert. Zu Windows 98, Windows ME, Windows 2000 und Windows XP gehört der Windows Scripting Host, mit dem sich Skripte auf Betriebssystemebene ausführen lassen. Der Windows Scripting Host lässt sich auch nachträglich auf Windows 95 und Windows NT 4.0 installieren. Die Architektur des Windows Scripting Host schreibt keine feste Skriptsprache vor, es lassen sich unterschiedliche Sprachinterpreter verwenden. Microsoft liefert die Skriptsprachen Visual Basic Script, eine Variante von Visual Basic, die auch zur Steuerung der Entwicklungsumgebung von Visual C++ verwendet werden kann, und JScript, die JavaScriptVersion von Microsoft. Daneben stellen andere Anbieter Interpreter für Sprachen wie Perl und Python bereit. Über die mit den Sprachen und dem Windows Scripting Host gelieferten Bibliotheken kann man auf das Dateisystem, das Netzwerk, die Benutzeroberfläche und die Registrierungsdatenbank von Windows zugreifen. Zusätzlich ermöglicht der Windows Scripting Host die Steuerung von Programmen durch die Automation. Als Beispiel soll die Steuerung des Programms StockChartAuto durch den Windows Scripting Host gezeigt werden. Listing 3.29 zeigt ein Visual-Basic-Script-Programm, das ein Dokumentenobjekt anlegt, Rohdaten importiert, die Eigenschaften von Aktie und Chart setzt und dann das Ergebnis als StockChartAuto-Datei speichert. Das
Automation
343
Skript läuft ab, ohne den Automationsserver sichtbar zu machen: Es läuft im Hintergrund ab. Der Einfachheit halber arbeitet das Programm mit konstanten Dateipfaden. ' create.vbs (Visual Basic Script) ' Erzeugt StockChart-Datei aus Rohdaten ' unter Ausnutzung der Automatisierungsschnittstelle 'Dokument erzeugen Set doc = CreateObject ("StockAuto.Document") 'Daten importieren doc.ImportFile "d:\data\sap.txt" 'Eigenschaften setzen doc.name = "Sap AG" doc.id = 716460 doc.ticker = "sag" doc.grid = true doc.average = true doc.averagecnt = 10 doc.SetColor 255,255,0 ' gelb ! 'Daten speichern doc.SaveFile "d:\data\sap.stka" Listing 3.29: Automation mit Visual Basic Script
Da der Windows Scripting Host sprachunabhängig ist, kann man Skripten in der persönlichen Lieblingssprache formulieren. Voraussetzung ist allerdings, dass bereits eine Anbindung für den Windows Scripting Host in der gewählten Sprache besteht. Listing 3.30 zeigt das gleiche Beispiel in JScript. // create.js (Jscript) // Erzeugt StockChart-Datei aus Rohdaten // unter Ausnutzung der Automatisierungsschnittstelle // Dokument erzeugen doc = WScript.CreateObject ("StockAuto.Document"); // Daten importieren doc.ImportFile ("d:\\data\\sap.txt"); // Eigenschaften setzen doc.name = "Sap AG"; doc.id = 716460; doc.ticker = "sag"; doc.grid = true; doc.average = true; doc.averagecnt = 10; doc.SetColor (255,255,0); // gelb !
Sprachunabhängigkeit des Windows Scripting Host
344
3
COM, OLE und ActiveX
// Daten speichern doc.SaveFile ("d:\\data\\sap.stka"); Listing 3.30: Automation mit JScript
Beide Skripten rufen die Methode ShowWindow nicht auf, daher arbeitet das Programm StockChartAuto im Hintergrund und wird nicht sichtbar gemacht. Nach Abarbeitung des Skripts wird StockChartAuto automatisch beendet. In diesem Fall verhält sich StockChartAuto tatsächlich analog zu einem Objekt, das erzeugt wird, um eine Aufgabe zu erledigen. Nach Fertigstellung dieser Aufgabe wird das Objekt wieder zerstört. Der Programmcharakter von StockChartAuto wird nicht sichtbar, vielmehr wird StockChartAuto im Sinne eines Programmobjekts verwendet. Die Automation ähnelt hierin der objektorientierten Programmierung, nur sind bei der Automation die Objekte ganze Programme! WSCRIPT.EXE und CSCRIPT.EXE
Zur Ausführung von Skripten bietet der Windows Scripting Host die Programme WSCRIPT.EXE und CSCRIPT.EXE an. Die beiden Programme unterscheiden sich durch die Art, wie Ausgaben des Skripts vorgenommen werden. Das Programm WSCRIPT.EXE arbeitet auf der Ebene der Windows Shell; alle Ausgaben erfolgen daher durch Messageboxen. Das Programm CSCRIPT.EXE arbeitet dagegen als Konsolenprogramm; alle Ausgaben gehen an stdout in einer Konsole. Man kann Skripten auch durch einen Doppelklick starten, sie werden dann automatisch mit WSCRIPT.EXE ausgeführt. Der Windows Scripting Host ist eine interessante Möglichkeit, um Aufgaben und Abläufe unter Windows zu automatisieren. Die Möglichkeit, eigene Programme durch den Windows Scripting Host zu steuern, ist ein guter Grund, diese Programme mit Automationsschnittstellen auszustatten.
3.3.6 Programm MFCClient1
Implementierung eines Automations-Clients mit den MFC
Obwohl Visual Basic Programme oft als Automations-Clients verwendet werden, kann man diese natürlich auch in C oder C++ schreiben. Die MFC bieten dabei eine umfangreiche Unterstützung. Das Beispielprogramm MFCClient1, das sich im Verzeichnis KAPITEL3\MFCCLIENT1 auf der Begleit-CD befindet, implementiert einen einfachen Automations-Client mit den MFC. Als Server wird wieder das Programm StockChartAuto verwendet. Das Programm MFCClient1 ist eine einfache Dialoganwendung. Es implementiert zwei Funktionen: Dateien, die auf das Programmfenster
Automation
345
gezogen und fallen gelassen werden (Drag&Drop), werden in das Serverdokument importiert. Zusätzlich gibt MFCClient1 über die zweite Automationsschnittstelle des Programms StockChartAuto eine Laufschrift in die Statusleiste aus. Das Programm MFCClient1 ist mit dem Anwendungs-Assistenten als dialogfeldbasierende Anwendung erstellt worden. Die Option für die Automation wurde angeschaltet. Diese Option erzeugt neben Programmcode, der die Verwendung als AutomationsClient ermöglicht, Programmcode für einen Automationsserver in Form einer Proxyklasse. (Eine Proxyklasse ist eine Stellvertreterklasse, sie implementiert eine Funktionalität stellvertretend für das Programm.) Der Klassen-Assistent kann automatisch eine Klasse zur Ansteuerung von Automationsschnittstellen erzeugen. Dazu wählt man in der Klassenansicht im Kontextmenü KLASSE HINZUFÜGEN | KLASSE AUS DER TYPBIBLIOTHEK HINZUFÜGEN und wählt dort die entsprechende Typbibliothek aus. Im Fall von MFCClient1 ist das die Datei STOCKCHARTAUTO.TLB. Der Klassen-Assistent zeigt dann einen Dialog mit den Schnittstellenklassen, die er für die Typbibliothek generiert. In diesem in Abbildung 3.24 gezeigten Dialog kann man die Namen dieser Klassen und deren Headerund Implementierungsdateien abändern.
Die automatisch erstellten Schnittstellenklassen sind von der Basisklasse COleDispatchDriver abgeleitet. COleDispatchDriver ist eine nicht von CObject abgeleitete Hilfsklasse, mit der die MFC Client-seitige Automationsschnittstellen implementieren. Für das Beispielprogramm MFCClient1 generiert der Klassen-Assistent die beiden von COleDispatchDriver abgeleiteten Schnittstellenklassen CStockChartAuto und CStatusBarInterface. Die vom Klassen-Assistenten generierten Schnittstellenklassen stellen alle Methoden der zugrunde liegenden Automationsschnittstelle in Form von Funktionen bereit. Eigenschaften werden nicht durch Member-Variablen, sondern durch Set- und Get-Funktionen realisiert. Listing 3.31 zeigt die für die Automationsschnittstelle CStockChartAuto generierte Klasse. class CStockChartAuto : public COleDispatchDriver { public: CStockChartAuto(){} // Ruft den COleDispatchDriver// Standardkonstruktor auf CStockChartAuto(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} CStockChartAuto(const CStockChartAuto& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attribute public: long GetId() { long result; GetProperty(0x1, VT_I4, (void*)&result); return result; } void SetId(long propVal) { SetProperty(0x1, VT_I4, propVal); } CString GetName() { CString result; GetProperty(0x2, VT_BSTR, (void*)&result); return result; } void SetName(LPCTSTR propVal) {
Um auf die Automationsschnittstelle zugreifen zu können, muss zunächst eine Instanz des Schnittstellenobjekts angelegt werden. Im Beispielprogramm MFCClient1 wird dazu die Member-Variable itfStockChart in der Klasse CMFCAutoClient1Dlg verwendet. In der Initialisierungsfunktion der Dialogklasse CMFCAutoClient1Dlg::OnInitDialog wird das Dokumentenobjekt erzeugt. Listing 3.32 zeigt einen Ausschnitt aus dieser Funktion.
Der Aufruf der Funktion CreateDispatch legt eine Instanz des Automationsservers an, in diesem Fall also ein Dokument des Programms StockChartAuto. Die Funktion ShowWindow ruft die gleichnamige Methode des Servers auf. Weiterhin ist im Listing zu sehen, wie ein Windows-Timer gestartet wird. Der Aufruf von SetTimer mit den gezeigten Parametern legt einen Timer mit einem Intervall von 150 Millisekunden an. Der erste Parameter gibt eine ID für den Timer an. Diese kann verwendet werden, um mehrere Timer zu unterscheiden. Als dritter Parameter kann optional eine Callback-Funktion übergeben werden. Diese wird dann nach jedem Ablauf des Timer-Intervalls aufgerufen. Falls man wie im Beispiel keine Callback-Funktion übergibt, dann erhält das Programm nach jedem Ablauf der Zeitspanne eine
Windows-Timer
350
3
COM, OLE und ActiveX
Windows-Nachricht vom Typ WM_TIMER. In der Eigenschaftsansicht der Klasse lässt sich die Nachrichtenbehandlungsfunktion OnTimer für diese Nachricht anlegen. Das Beispielprogramm MFCClient1 verwendet diese Funktion, um die Laufschrift zu implementieren. Alle 150 Millisekunden wird die Ausgabe in der Statusleiste des Serverprogramms StockChartAuto um ein Zeichen verschoben. Listing 3.33 zeigt die Implementierung von OnTimer. void CMFCAutoClient1Dlg::OnTimer(UINT nIDEvent) { static int cnt = 0; static const CString text = _T( "*********************************************" " Achtung, das Programm wird ferngesteuert!!! " "*********************************************"); CString str; if (cnt > text.GetLength() - 45) cnt = 0; else cnt++; str = text.Mid (cnt, 45); try { CStatusBarInterface itfStatus (itfStockChart.GetStatusbar ()); itfStatus.SetStatus (str); } catch (COleException *e) { KillTimer (1); // Timer abschalten e->ReportError (); e->Delete (); } CDialog::OnTimer(nIDEvent); } Listing 3.33: Die Timer-Funktion OnTimer Zugriff auf die zweite Automationsschnittstelle
Der erste Teil der Funktion OnTimer ist dafür zuständig, die Laufschrift zusammenzubauen. Der zweite Teil demonstriert dann erstmals, wie auf die zweite Automationsschnittstelle des Serverprogramms StockChartAuto zugegriffen wird. Durch Aufruf der Methode GetStatusbar erhält man einen Zeiger auf die zweite Automationsschnittstelle. Der Assistent hat beim Import der Typbibliothek auch für diese Schnittstelle eine Schnittstellenklasse erzeugt. Um ein Schnittstellenobjekt zu erzeugen, wird diesem per Konstruktoraufruf einfach der von GetStatusbar zurückgegebene
Automation
351
Schnittstellenzeiger übergeben. Damit wird ein gültiges Schnittstellenobjekt erzeugt. Der anschließende Aufruf von SetStatus demonstriert die Verwendung der zweiten Automationsschnittstelle. Es sei an dieser Stelle angemerkt, dass die gezeigte Implementierung recht ineffizient ist. Der Schnittstellenzeiger auf die zweite Automationsschnittstelle wird bei jedem Aufruf von OnTimer neu angefordert. Daraufhin wird auch jedes Mal ein Schnittstellenobjekt angelegt und anschließend wieder zerstört. Alle diese Vorgänge kosten natürlich Zeit und der Aufruf von Automationsschnittstellen ist darüber hinaus recht langsam. In zeitkritischen Anwendungen sollte man einen solchen Schnittstellenzeiger gegebenenfalls zwischenspeichern. In Visual Basic sieht der Programmcode zum Zugriff auf die zweite Automationsschnittstelle übrigens noch einfacher aus (siehe Listing 3.34). 'Zugriff auf zweite Automationsschnittstelle itfStatus = stockChartAuto.statusbar itfStatus.status = "Ausgabe" Listing 3.34: Zugriff auf zweite Automationsschnittstelle in Visual Basic
Beim Aufruf der Methoden des Automationsservers werden im Beispiel Ausnahmen des Typs COleException abgefangen. Diese Ausnahmen werden ausgelöst, wenn Probleme bei der Kommunikation zwischen Automations-Client und -server auftreten. Man sollte darauf gefasst sein, dass die Verbindung zu einem Automationsserver jederzeit unterbrochen werden kann. Beispielsweise kann der Benutzer den Server einfach beenden (gut programmierte Server beenden sich nicht, wenn noch Verbindungen zu Automations-Clients bestehen, sondern machen sich einfach unsichtbar) oder der Server kann abstürzen. Fängt man die auftretenden Ausnahmen nicht selbst ab, so tut dies die Automationsimplementierung der MFC. Es wird dann eine Fehlermeldung mit der aufgetretenen Ausnahme angezeigt.
COleException
Damit das Beispielprogramm MFCClient1 Dateipfade per Drag&Drop entgegennehmen kann, muss es auf die WindowsNachricht WM_DROPFILES reagieren. Die Nachrichtenbehandlungsfunktion für die Nachricht WM_DROPFILES heißt im Beispiel CMFCAutoClient1Dlg::OnDropFiles. Der Funktion wird ein Handle übergeben, mit dem man die an das Programm übergebenen Dateipfade herausfinden kann. Die eigentliche Arbeit muss
WM_DROPFILES
352
3
COM, OLE und ActiveX
durch Aufruf von Windows-API-Funktionen durchgeführt werden, die MFC bietet keine Unterstützung dafür. Listing 3.35 zeigt die Implementierung von OnDropFiles. void CMFCAutoClient1Dlg::OnDropFiles( HDROP hDropInfo ) { char lpszBuffer[_MAX_PATH]; // Nur erste Datei! ::DragQueryFile (hDropInfo, 0, lpszBuffer, _MAX_PATH); try { itfStockChart.ImportFile (lpszBuffer); } catch (COleException *e) { e->ReportError (); e->Delete (); } ::DragFinish (hDropInfo); CDialog::OnDropFiles(hDropInfo); } Listing 3.35: Die Nachrichtenbehandlungsfunktion OnDropFiles
Der Einfachheit halber wertet OnDropFiles nur den ersten an das Programm übergebenen Dateipfad aus. Diesen findet es durch den Aufruf der API-Funktion DragQueryFile. Der so gewonnene Dateipfad wird der Methode ImportFile übergeben, wodurch das Serverprogramm diese Datei in das ferngesteuerte Dokumentenobjekt importiert. Zum Abschluss wird die API-Funktion DragFinish aufgerufen. Diese gibt den intern von Windows für die Drag&Drop-Aktion benötigten Speicherplatz frei. Wie sich zeigt, gestaltet sich die Ansprache von Automationsservern mit den MFC recht einfach. Es stellt sich jedoch die Frage, wie die MFC die Methoden des Servers tatsächlich aufrufen. Die Implementierung war bereits in Listing 3.31 zu sehen. GetProperty, SetProperty und InvokeHelper
Um eine Eigenschaft zu lesen und zu setzen, verwenden die MFC die Funktionen GetProperty und SetProperty. Diese Funktionen bekommen die DISPID der Eigenschaft sowie den Typ der Eigenschaft übergeben. Damit können diese Funktionen die notwendige Konvertierung in den Datentyp VARIANT vornehmen und die Eigenschaft lesen beziehungsweise setzen. Zum Aufruf von Methoden wird die Funktion InvokeHelper verwendet. Diese bekommt eine DISPID sowie ein Array von Parametertypen und
Automation
353
die Parameter selbst übergeben. Damit kann InvokeHelper die Parameter in Variablen des Typs VARIANT konvertieren und die Methode aufrufen.
3.3.7
Das Beispielprogramm MFCAutoClient2
Es soll an dieser Stelle nicht verschwiegen werden, dass es noch eine zweite Möglichkeit gibt, um einen Automations-Client zu erstellen. Diese Möglichkeit baut nicht auf den MFC, sondern auf der Unterstützung des Visual C++-Compilers für COM und die Automation auf. Obwohl die Automation auf diese Weise nicht durch die MFC implementiert wird, lässt sie sich trotzdem einfach in MFC-Programmen verwenden. Das Beispielprogramm MFCAutoClient2 sieht äußerlich genauso aus wie das Programm MFCAutoClient1. Es implementiert auch genau die gleiche Funktionalität wie das Programm MFCAutoClient1. Da die Automation im Beispielprogramm MFCAutoClient2 nicht durch die MFC implementiert wird, muss die Option zur Automationsunterstützung im Anwendungs-Assistenten nicht ausgewählt werden. Dadurch spart man sich in einer dialogfeldbasierten Anwendung wie dem Programm MFCAutoClient2 die Erzeugung der Proxyklasse für die Implementierung eines Automationsservers. Damit man Automationsfunktionen aufrufen kann, muss allerdings in der Funktion InitInstance der Applikationsklasse die Funktion AfxOleInit aufgerufen werden. Dazu ist die Datei AFXDISP.H einzubinden.
Programm MFCAutoClient2
Die gesamte Unterstützung für die Automation wird durch eine einzige Anweisung generiert. In der Datei MFCAUTOCLIENT2DLG.H befindet sich die folgende Anweisung:
Damit wird die Typbibliothek des Automationsservers eingebunden und der notwendige Programmcode für den Zugriff auf diesen Server generiert. Der generierte Zugriffscode befindet sich im Fall des Beispielprogramms in den Dateien STOCKCHARTAUTO.TLH und STOCKCHARTAUTO.TLI. Beide Dateien werden im Debug- beziehungsweise Release-Ordner des Projekts angelegt und dürfen nicht verändert werden. Die Dateien werden nicht in die Projektliste aufgenommen. Beim Import einer Typbibliothek wird für diese normalerweise ein eigener C++-Namensraum (namespace) angelegt. Durch die Verwendung eines eigenen Namensraums werden Namenskonflikte
Namensräume
354
3
COM, OLE und ActiveX
durch den Import ausgeschlossen. Man kann allerdings auch ohne eigenen Namensraum importieren, oder – wie im Beispiel gezeigt – den Namensraum selbst benennen. Ohne die Anweisung rename_namespace würde im Beispiel der Namensraum »StockChartAuto« anstatt »Server« heißen. Zum Zugriff auf den Automationsserver wird in der Datei MFCAUTOCLIENT2DLG.H ein Smart Pointer (siehe Kasten) deklariert. Dieser Zeiger wird durch die Klasse _com_ptr_t implementiert, eine C++-Klasse zur Unterstützung der COM-Programmierung: Server::IStockChartAutoPtr m_itfStockChart;
Der Zugriff auf den Automationsserver erfolgt analog zum vorherigen Beispiel. Zunächst wird in OnInitDialog das Serverdokument angelegt und sichtbar gemacht. Dies ist in Listing 3.36 zu sehen. ... // Hier eigene Initialisierungen SetTimer (1, 150, NULL); try { m_itfStockChart.CreateInstance (__uuidof(Server::Document)); m_itfStockChart->ShowWindow (); } catch (_com_error c) { MessageBox (c.ErrorMessage()); } // Drop von Dateien akzeptieren DragAcceptFiles (); ... Listing 3.36: Anlegen des Serverdokuments
Die Ausnahme vom Typ _com_error ist unbedingt abzufangen. Anders als bei der Nutzung der Automationsimplementierung der MFC gibt es bei der Verwendung der hier gezeigten Implementierung keine vordefinierte Fehlerbehandlung, auf die das Programm zurückfallen kann. Fängt man Ausnahmen nicht ab, so kann es zu Programmabstürzen kommen. Die Klasse _com_error gehört ebenso wie die Klasse _com_ptr_t, die die Smart Pointer implementiert, zu den COM-Klassen von Visual C++. Diese COM-Klassen sind völlig unabhängig von den MFC verwendbar.
Automation
Was ist ein Smart Pointer? Ein Smart Pointer soll sich – im Gegensatz zu einem normalen oder »dummen« Zeiger – »intelligent« verhalten. Intelligentes Verhalten bedeutet, dass der Zeiger neben den normalen Aufgaben eines Zeigers (auf etwas zeigen und Zugriff darauf erlauben) zusätzliche, automatische Funktionalität besitzt. Diese zusätzliche Funktionalität macht den Zeiger »smart«. Smart Pointer werden für Aufgaben wie Referenzzählung, Speicherverwaltung oder Zugriffskontrolle eingesetzt. Ein Smart Pointer ist in Wirklichkeit kein Zeiger, sondern ein Objekt, das sich – größtenteils – wie ein Zeiger verhält und so für den Programmierer wie ein Zeiger aussieht. Das Objekt kapselt einen normalen Zeiger und reicht die Zugriffe an diesen Zeiger weiter. Nebenbei kann das Objekt, das den Smart Pointer repräsentiert, jedoch auch andere Aufgaben ausführen. Der zentrale Punkt bei der Erstellung einer Smart-PointerKlasse ist das Überladen des Zugriffsoperators ->. Als Beispiel soll noch einmal das Flaschenöffnerbeispiel aus Kapitel 2, »Einstieg in die MFC-Programmierung«, herangezogen werden. In diesem Beispiel wurde zwar das Öffnen und Schließen der Flasche durch das Flaschenöffnerobjekt erledigt, allerdings erfolgte der Zugriff auf die Flasche selbst immer noch direkt (PourIntoGlas wurde direkt beim CBottle-Objekt aufgerufen). Dies ist unelegant, weil die Zugriffe auf die Funktionalität von CBottle auf das CBottle-Objekt und das CBottleOpener-Objekt verteilt werden. Es wäre eleganter, wenn der Programmierer alle Funktionen bei einem Objekt aufrufen könnte. Man kann dies dadurch lösen, dass CBottleOpener alle Funktionen von CBottle ebenfalls bereitstellt. Die Implementierungen dieser Funktionen delegieren dann ihre Aufrufe an die entsprechenden Funktionen von CBottle. Im Beispiel wäre diese Vorgehensweise einfach zu implementieren. In der Praxis stellt das Delegieren aller Funktionen der verwaltenden Klasse an die verwaltete Klasse einen unschönen Mehraufwand dar. Man kann den Flaschenöffner allerdings auch ohne Delegation mit einer Smart-Pointer-Klasse implementieren. Da der Flaschenöffner nun alle Zugriffe auf die Flasche regelt, heißt die Flaschenöffnerklasse jetzt CBottleAccess. Das Listing zeigt die Implementierung.
Der Aufruf von PourIntoGlass ist ein Smart-Pointer-Zugriff. Das Programm hat folgende Ausgabe: open log: Zugriff auf den Zeiger pour close
Das Beispiel zeigt lediglich das Prinzip eines Smart Pointers und ist keine vollständige Implementierung. Für eine vollständige Smart-Pointer-Implementierung müssten weitere Operatoren, wie beispielsweise der Zuweisungsoperator, überladen werden. Viele Smart-Pointer-Implementierungen verwenden Templates, um den Typ des verwalteten Zeigers variabel zu halten. Die Implementierung der Funktionen OnTimer und OnDropFiles ähnelt der Implementierung im vorhergehenden Beispiel. Listing 3.37 zeigt die Implementierung dieser Funktionen für das Beispielprogramm MFCAutoClient2. void CMFCAutoClient2Dlg::OnTimer(UINT nIDEvent) { static int cnt = 0; static const CString text = "*********************************************" " Achtung, das Programm wird ferngesteuert!!! " "*********************************************"; CString str; if (cnt > text.GetLength() - 45) cnt = 0; else cnt++; str = text.Mid (cnt, 45); try { Server::IStatusBarInterfacePtr pStatus(m_itfStockChart->Getstatusbar ()); pStatus->Putstatus (str.AllocSysString ()); } catch (_com_error c) { KillTimer (1); MessageBox (c.ErrorMessage()); }
CDialog::OnTimer(nIDEvent); }
357
358
3
COM, OLE und ActiveX
void CMFCAutoClient2Dlg::OnDropFiles( HDROP hDropInfo ) { char lpszBuffer[_MAX_PATH]; // Nur erste Datei! ::DragQueryFile (hDropInfo, 0, lpszBuffer, _MAX_PATH); try { CString str(lpszBuffer); m_itfStockChart->ImportFile (str.AllocSysString()); } catch (_com_error c) { MessageBox (c.ErrorMessage()); } ::DragFinish (hDropInfo); } Listing 3.37: Implementierung der Funktionen OnTimer und OnDropFiles
Wie man sehen kann, unterscheidet sich die Implementierung des Automations-Clients unter Verwendung der Visual C++-COMKlassen kaum von der Implementierung mit den entsprechenden MFC-Klassen. Die automatisch für die Schnittstelle generierten Funktionen unterscheiden sich etwas in ihren Namen (zum Beispiel heißt die Funktion zum Setzen der Eigenschaft status Putstatus anstatt SetStatus). Strings werden durch BSTRs anstatt durch CString-Objekte dargestellt.
3.3.8
Registrierung von Typbibliotheken
Die vom Compiler für einen Automationsserver erzeugte Typbibliothek kann in der Registrierungsdatenbank von Windows eingetragen werden. Ein vom Anwendungs-Assistenten erstelltes Programm nimmt diese Eintragung normalerweise nicht vor, man muss den entsprechenden Programmcode selbst in das Projekt einfügen. Der Hauptgrund für einen Eintrag in der Registrierungsdatenbank dürfte sein, dass sich dort eingetragene Automationsserver mit dem Objektbrowser von Visual Basic untersuchen lassen. Weiterhin nimmt Visual Basic bei Automationsservern mit registrierter Typbibliothek die Zuordnung zwischen Namen und DISPIDs zur Übersetzungszeit und nicht zur Laufzeit vor. Das Laufzeitverhalten wird dadurch (geringfügig) verbessert und die Typsicherheit innerhalb von Visual Basic-Programmen erhöht.
Automation
359
Abbildung 3.25: Objektbrowser von Visual Basic
Abbildung 3.25 zeigt den Objektbrowser von Visual Basic. Der Objektbrowser lässt sich über den Menüeintrag ANSICHT | ANDERE FENSTER | OBJECTBROWSER aufrufen. Bei der Ansprache von Automationsservern mit registrierter Typbibliothek ergibt sich in Visual Basic lediglich ein kleiner Unterschied. Statt Dim StockChartAuto As Object
schreibt man: Dim StockChartAuto As StockChartAuto.Document
IStockChartAuto ist dabei der Name der Automationsschnittstelle, wie er in der ODL-Datei des Projekts angegeben ist. Um die Typbibliothek aus einem MFC-Programm heraus zu registrieren, ruft man die globale MFC-Funktion AfxOleRegisterTypeLib auf. Dieser Funktion wird die GUID der zu registrierenden Typbibliothek übergeben. Damit AfxOleRegisterTypeLib aufgerufen werden kann, muss man die Header-Datei AFXCTL.H einbinden. Am sinnvollsten nimmt man die Registrierung in der Funktion InitInstance des Applikationsobjekts vor. Listing 3.38 zeigt den entsprechenden Ausschnitt aus der Funktion CStockChartAutoApp::InitInstance.
AfxOleRegisterTypeLib
360
3
COM, OLE und ActiveX
// Registrierung der Typbibliothek GUID typeLibGUID; AfxGetClassIDFromString ( _T("{CFDB3165-0122-11D2-B51C-006097A8F69A}"), &typeLibGUID); AfxOleRegisterTypeLib (AfxGetInstanceHandle(), typeLibGUID); Listing 3.38: Registrierung der Typbibliothek
Damit sich eine Typbibliothek in der beschriebenen Form registrieren lässt, muss sich die Typbibliothek als Ressource in der ausführbaren Datei (exe oder dll) befinden. Die Ressource muss von Typ TYPELIB sein und die ID 1 haben. Um die Typbibliothek automatisch beim Übersetzungsvorgang in die Programmdatei aufzunehmen, bietet es sich an, die Typbibliothek per Ressourcen-Include in die Ressourcendatei zu übernehmen. In der Entwicklungsumgebung ruft man dazu unter ANSICHT | RESSOURCEN-INCLUDES den in Abbildung 3.26 gezeigten Dialog auf.
Abbildung 3.26: Typbibliothek den Ressourcen hinzufügen
Bei der Verwendung eines Automationsservers mit den MFC hat die Registrierung der Typbibliothek keinen Vorteil, da der ImportAssistent die Typbibliothek direkt durch Angabe der entsprechenden Datei ausliest. Da Automations-Clients aber zumeist nicht mit den MFC oder in C++ entwickelt werden, ist es durchaus angebracht, eine Typbibliothek zu registrieren.
Automation
3.3.9
361
Duale Schnittstellen
Der größte Nachteil der Automationsschnittstelle IDispatch ist ihr schlechtes Laufzeitverhalten. Dies kommt vor allen Dingen durch das aufwändige Konvertieren der Funktionsparameter beim Aufruf der Methode Invoke zustande. Für Skriptsprachen ist dieses Laufzeitverhalten vertretbar, da diese Sprachen selbst recht langsam ausgeführt werden. Für Programme, die mit kompilierten, stark typisierten Sprachen wie C, C++ oder Delphi entwickelt werden, ist die Automationsschnittstelle allerdings eher ein Hindernis als ein freier Zugang. Das schlechte Laufzeitverhalten von IDispatch bremst die Programme aus; die umständliche Konvertierung aller Parameter in ein Array aus VARIANTs ist zwischen solchen Programmen unnötig. Ein wesentlich besseres Laufzeitverhalten ließe sich erzielen, wenn solche Programme direkt über COMMethoden miteinander kommunizieren würden. Genau dies ist die Idee, die hinter dualen Schnittstellen steckt. Die Automationsschnittstelle wird einmal über IDispatch und die Invoke-Methode bereitgestellt, darüber hinaus stehen alle Automationsmethoden jedoch zusätzlich in Form einfacher COM-Methoden zur Verfügung. Der Client bzw. der Programmierer des Clients kann sich aussuchen, welche Schnittstelle er verwenden möchte. Abbildung 3.27 zeigt das Prinzip einer dualen Schnittstelle. Die gesamte Schnittstelle steht in zweifacher Form zur Verfügung. Der Aufruf der COM-Methoden ist typsicher und schnell. Damit eignet er sich gut für kompilierte Sprachen. Der Aufruf über IDispatch dagegen ist relativ langsam und nicht typsicher. Er eignet sich gut für interpretierte Sprachen wie beispielsweise Skriptsprachen. Leider gibt es keine Unterstützung für duale Schnittstellen durch die Assistenten von Visual Studio .NET. Die Implementierung dieser Schnittstellen ist daher etwas aufwändiger als die Implementierung einfacher Automationsschnittstellen. Der technische Hinweis 65 und das Beispielprogramm ACDUAL, die beide Teil der Visual C++-CD sind, geben einen Überblick über die Implementierung dualer Schnittstellen mit den MFC. Leichter lassen sich duale Schnittstellen mit attributierter Programmierung und der ATL-Klassenbibliothek erzeugen.
Automation und Laufzeitverhalten
362
3
Automationsmethoden
AddRef ...
langsamer Zugriff
Invoke
Methode 1 schneller Zugriff
Mechanismus zur späten Bindung
Duale Schnittstelle abgeleitet von IDispatch
QueryInterface
COM, OLE und ActiveX
Methode 1 Methode 2 Methode 3 ... ... ...
Methode 2 Methode 3 ...
COM-Methoden
... ...
Abbildung 3.27: Duale Schnittstelle
3.3.10 Tipps zur Vorgehensweise 왘 Die Option AUTOMATION muss im Anwendungs-Assistenten sowohl zur Erstellung von Automations-Clients als auch von Automationsservern ausgewählt werden. 왘 Zur Erstellung eines Automations-Clients wird mit Hilfe eines Assistenten die Typbibliothek des Servers eingelesen. Aus dieser Bibliothek erzeugt der Assistent eine von COleDispatchDriver abgeleitete Klasse, die zur einfachen Ansprache des Automationsservers verwendet wird. 왘 Bei Automationsservern, die mit dem Anwendungs-Assistenten erstellt worden sind und die die Dokument-Ansicht-Architektur verwenden, ist die Dokumentenklasse sofort automationsfähig. In der Klassenansicht können Methoden und Eigenschaften eingefügt werden. 왘 Bei einer dialogbasierten Anwendung wird die Serverfunktion durch eine Proxyklasse bereitgestellt.
Vereinheitlichter Datenaustausch
왘 Man kann jederzeit weitere automationsfähige Klassen zu einem Projekt hinzufügen. Diese Klassen treten dann als weitere Automationsschnittstellen in Erscheinung.
3.3.11 Zusammenfassung Die Automation war eine der ersten COM-basierten Technologien, die von Microsoft eingeführt worden sind. Mittlerweile hat sich die Automation als ein sehr nützliches Werkzeug zur Steuerung von Programmen etabliert. Die Automation arbeitet gut mit Skriptsprachen wie Visual Basic und JScript zusammen. Die weitreichende Unterstützung durch die Assistenten von Visual Studio .NET macht die Implementierung der Automation mit den MFC zu einer einfachen Übung.
3.4
Vereinheitlichter Datenaustausch
Es gibt viele Gründe, um Daten zwischen einzelnen WindowsAnwendungen oder auch innerhalb einer Anwendung auszutauschen. Beispielsweise möchte man Texte oder Grafiken mit verschiedenen Programmen bearbeiten. Der klassische Weg ist es, die Grafik oder den Text in einer Datei zu speichern und dann mit einem anderen Programm wieder einzulesen. Moderne grafische Benutzeroberflächen wie Windows verfügen allerdings über einfachere Mittel zum Datenaustausch, wie beispielsweise die Zwischenablage oder den Datenaustausch per Drag&Drop. Aus programmiertechnischer Sicht gibt es eine große Auswahl an Verfahren zum Datenaustausch zwischen Programmen. Daten können prinzipiell über Dateien, globale Speicherbereiche, DLLs, DDE oder das WIN32-API der Zwischenablage ausgetauscht werden. Wünschenswert wäre ein Verfahren, das unabhängig vom Typ der transportierten Daten und von der Art des Transports immer gleich zu verwenden ist. Genau dies hat Microsoft mit dem vereinheitlichten Datenaustausch (Uniform Data Transfer, UDT) geschaffen. Wie bei COM-Technologien üblich, basiert der vereinheitlichte Datenaustausch auf einer COM-Schnittstelle.
3.4.1
Die Schnittstelle IDataObject
Die Schnittstelle IDataObject definiert einen daten- und transportunabhängigen Datenaustauschmechanismus. Verwendet wird IDataObject bei der Ansprache der Zwischenablage und beim
363
364
3
COM, OLE und ActiveX
Datenaustausch per Drag&Drop. Weiterhin wird die Schnittstelle auch bei OLE-Verbunddokumenten und ActiveX-Steuerelementen verwendet. IDataObject ist direkt von IUnknown abgeleitet und besitzt neun Methoden. Die Methoden der Schnittstelle IDataObject verwenden die beiden Datenstrukturen FORMATETC und STGMEDIUM, um die über die Schnittstelle transportierten Daten genauer zu beschreiben. Die FORMATETC-Struktur beschreibt das Format der ausgetauschten Daten, die STGMEDIUM-Struktur gibt an, wo die Daten gespeichert sind. Listing 3.39 zeigt die FORMATETC-Struktur. typedef struct tagFORMATETC { CLIPFORMAT cfFormat; DVTARGETDEVICE *ptd; DWORD dwAspect; LONG lindex; DWORD tymed; } FORMATETC; Listing 3.39: Die Datenstruktur FORMATETC FORMATETC
Das Datenelement cfFormat der FORMATETC-Struktur beschreibt das Format der ausgetauschten Daten in einer Form, die kompatibel zur Windows Zwischenablage ist. Das hier angegebene Format ist allerdings nicht auf die Austauschformate beschränkt, die durch die API-Funktionen zur Ansteuerung der Zwischenablage unterstützt werden. Es werden zusätzlich einige OLE-Formate unterstützt. Eine Applikation kann hier zudem private Werte angeben, die sie vorher definieren muss. Tabelle 3.2 zeigt einige von Windows vordefinierte Werte. Kennung
Bedeutung
CF_TEXT
Text wird ausgetauscht
CF_BITMAP
Handle auf eine Bitmap
CF_PALETTE
Handle auf eine Palette
CF_WAVE
Audiodaten
CF_DIB
Bitmap mit BITMAPINFO-Struktur
Tabelle 3.2: Einige Austauschformate der Zwischenablage
Mit dem Datenelement ptd der FORMATETC-Struktur kann ein Ausgabegerät für die transportierten Daten spezifiziert werden. Dies kann beispielsweise ein Drucker sein.
Vereinheitlichter Datenaustausch
Das Datenelement dwAspect gibt die Darstellungsform an. Damit wird der Detailgrad der Ausgabe beschrieben. So kann beispielsweise eine verkleinerte Darstellung (DVASPECT_THUMBNAIL) oder eine Darstellung als Symbol (DVASPECT_ICON) angefordert werden. Natürlich ist dies nicht für alle Arten von transportierten Daten unbedingt sinnvoll. Tabelle 3.3 zeigt die möglichen Werte. Wert
Bedeutung
DVASPECT_CONTENT
Dies ist der Standardwert. Das behandelte Datenobjekt soll in seiner normalen Darstellung ausgetauscht werden.
DVASPECT_DOCPRINT
Das Objekt soll wie bei einem Ausdruck dargestellt werden.
DVASPECT_THUMBNAIL
Das Objekt soll verkleinert dargestellt werden. Es werden dazu meist 120x120 Pixel und 16 Farben verwendet.
DVASPECT_ICON
Das Objekt soll als Symbol dargestellt werden.
Tabelle 3.3: Kodierung des Detailgrads der betrachteten Daten
Der Wert für das Datenelement lindex der FORMATETC-Struktur wird normalerweise auf -1 gesetzt. lindex beschreibt, was zu tun ist, wenn die Darstellung der betrachteten Daten eine Verteilung auf mehrere Seiten erfordern würde. Der Wert -1 wählt alle Daten aus. Das letzte Datenelement tymed gibt schließlich das Speichermedium an, das zum Datentransport verwendet werden soll. Tabelle 3.4 zeigt die möglichen Werte. Wert
Bedeutung
TYMED_HGLOBAL
Ein globaler Speicher-Handle wird zum Datenaustausch verwendet.
TYMED_FILE
Eine Datei wird zum Datenaustausch verwendet.
TYMED_ISTREAM
Das Datenaustauschmedium wird durch einen Zeiger auf eine IStream-Schnittstelle beschrieben.
TYMED_ISTORAGE
Das Datenaustauschmedium wird durch einen Zeiger auf eine IStorage-Schnittstelle beschrieben.
TYMED_GDI
Ein GDI-Objekt wird ausgetauscht.
Tabelle 3.4: Mögliche Speichermedien für den Datenaustausch
365
366
3
COM, OLE und ActiveX
Wert
Bedeutung
TYMED_MFPICT
Ein Windows-Metafile wird ausgetauscht.
TYMED_ENHMF
Ein Enhanced-Metafile wird ausgetauscht.
TYMED_NULL
Es werden keine Daten ausgetauscht.
Tabelle 3.4: Mögliche Speichermedien für den Datenaustausch (Fortsetzung) STGMEDIUM
Um einen Verweis auf die Daten zu übergeben, wird die STGMEDIUM-Struktur verwendet. Listing 3.40 zeigt diese Datenstruktur. Zunächst wird auch in dieser Struktur ein Datenelement namens tymed verwendet, um das verwendete Speichermedium zu spezifizieren. Danach wird in einem union für jedes mögliche Speichermedium entweder ein Handle, ein Zeiger auf einen Dateinamen oder ein COM-Schnittstellenzeiger deklariert. Je nach Wert der Elementvariablen tymed wird das entsprechende Element von union verwendet. typedef struct tagSTGMEDIUM { DWORD tymed; union { HBITMAP hBitmap; HMETAFILEPICT hMetaFilePict; HENHMETAFILE hEnhMetaFile; HGLOBAL hGlobal; LPOLESTR lpszFileName; IStream *pstm; IStorage *pstg; }; IUnknown *pUnkForRelease; } STGMEDIUM; Listing 3.40: Die Datenstruktur STGMEDIUM
Schließlich wird in STGMEDIUM noch ein Zeiger auf eine IUnknown-Schnittstelle deklariert. Dieser Zeiger ist das Ergebnis eines etwas misslichen Umstands: Es gibt keine einfache Regel, wer nach einem vollzogenen Datenaustausch das verwendete Speichermedium zu löschen hat. Bezogen auf diesen Zeiger gilt jedoch die Regel: Wenn der Zeiger ungleich NULL ist, dann zeigt er auf eine gültige COM-Schnittstelle und muss zur Freigabe des verwendeten Speichermediums benutzt werden. Dazu ist die Release-Methode der Schnittstelle aufzurufen. Ist der COM-Zeiger nicht gültig, so wird für jedes mögliche Speichermedium eine eigene Freigaberegel definiert. Handelt es sich bei den transportierten Daten beispiels-
Vereinheitlichter Datenaustausch
weise um einen globalen Speicherblock, so ist der entnehmende Prozess dafür zuständig, den Speicherblock zu löschen. Wer in welchem Fall das für den Datenaustausch verwendete Speichermedium freigeben muss, ist in der Online-Hilfe definiert. Nach der Beschreibung der zum Verständnis der Schnittstelle IDataObject wichtigen Datenstrukturen sollen nun die Methoden der Schnittstelle kurz vorgestellt werden. Listing 3.41 zeigt die Definition von IDataObject in IDL-Notation. [ object, uuid(0000010e-0000-0000-C000-000000000046), pointer_default(unique) ] interface IDataObject : IUnknown { typedef [unique] IDataObject *LPDATAOBJECT; HRESULT GetData( [in, unique] FORMATETC *pformatetcIn, [out] STGMEDIUM *pmedium); HRESULT GetDataHere( [in, unique] FORMATETC *pformatetc, [in, out] STGMEDIUM *pmedium); HRESULT QueryGetData( [in, unique] FORMATETC *pformatetc); HRESULT GetCanonicalFormatEtc( [in, unique] FORMATETC *pformatectIn, [out] FORMATETC *pformatetcOut); HRESULT SetData( [in, unique] FORMATETC *pformatetc, [in, unique] STGMEDIUM *pmedium, [in] BOOL fRelease); HRESULT EnumFormatEtc( [in] DWORD dwDirection, [out] IEnumFORMATETC **ppenumFormatEtc); HRESULT DAdvise( [in] FORMATETC *pformatetc, [in] DWORD advf, [in, unique] IAdviseSink *pAdvSink, [out] DWORD *pdwConnection);
367
368
3
COM, OLE und ActiveX
HRESULT DUnadvise( [in] DWORD dwConnection); HRESULT EnumDAdvise( [out] IEnumSTATDATA **ppenumAdvise); } Listing 3.41: Definition der Schnittstelle IDataObject in IDL
Die beiden Methoden GetData und GetDataHere sind für das Extrahieren der Daten über die Schnittstelle zuständig. Beide Methoden unterscheiden sich nur dadurch, wer das zum Austausch notwendige Datenmedium stellt. Die Methode GetData kümmert sich um die Bereitstellung des Datenmediums, bei einem Aufruf von GetDataHere muss der Aufrufer selbst für die Bereitstellung sorgen. Mit der Methode QueryGetData kann festgestellt werden, ob Daten in einem bestimmten Format vorliegen. Mit SetData können Daten in der umgekehrten Richtung transportiert werden. Dieses Verfahren wird allerdings selten verwendet. Durch den Aufruf der Methode EnumFormatEtc können die Formate bestimmt werden, mit denen Daten durch den Aufruf von GetData abgeholt werden können. Mit den drei Methoden DAdvise, DUnadvise und EnumDAdvise kann eine Benachrichtigungsverbindung zu einem Datenobjekt aufgebaut werden. Damit kann ein Programm, das ein Datenobjekt verwendet, über Änderungen an den Daten informiert werden.
3.4.2 API-Funktionen zum Datenaustausch
Datenaustausch über die Zwischenablage
In den Zeiten vor UDT sprach man die Zwischenablage durch eine Reihe von Windows-API-Funktionen an. Diese Funktionen stehen auch heute noch zur Verfügung (OpenClipboard, GetClipboardData usw.). Einige wenige dieser Clipboard-Funktionen werden auch von der Klasse CWnd bereitgestellt. Warum sollte man den Zugriff auf die Zwischenablage durch die IDataObject-Schnittstelle implementieren und nicht die dafür bereitgestellten WIN32-API-Funktionen verwenden? Die API-Funktionen stehen vor allen Dingen aus Gründen der Rückwärtskompatibilität weiterhin zur Verfügung. Grundsätzlich sind die Zugriffsverfahren über das Windows-API und mittels der IDataObject-Schnittstelle kompatibel. Schreibt ein Programm mit Hilfe des Windows-API Daten in die Zwischenablage, so können die Daten über die IDataObject-Schnittstelle ausgelesen werden. Auch in
Vereinheitlichter Datenaustausch
der umgekehrten Richtung funktioniert der Datenaustausch. Trotzdem hat die Verwendung der IDataObject-Schnittstelle einige Vorteile. Die alten API-Funktionen zum Datenaustausch über die Zwischenablage können ausschließlich globale Speicherbereiche zum Datenaustausch verwenden. Über das neue Verfahren können dagegen auch andere Medien wie beispielsweise Dateien oder IStorage-Schnittstellenzeiger als Transportmedium fungieren. Zudem ist das Verfahren unter Ausnutzung von IDataObject zukunftssicherer. Sollte Microsoft die Funktionalität der Zwischenablage erweitern, so wird dies sicherlich auf der Basis des IDataObject-Mechanismus geschehen und nicht durch das alte Windows-API. Verwendet man IDataObject, dann hat man im selben Zug bereits die Funktionalität für Drag&Drop implementiert, da auch dieses auf der IDataObjectSchnittstelle basiert. Darüber hinaus ist das neue Verfahren flexibler und birgt einige erweiterte Möglichkeiten, die über das alte API nicht genutzt werden können. Dazu gehört beispielsweise der Austausch von IStorage-Schnittstellenzeigern, die Teile von Verbunddokumenten repräsentieren.
3.4.3
Datenaustausch durch Drag&Drop
Für den Datentransport per Drag&Drop wird genau wie bei der Zwischenablage die Schnittstelle IDataObject verwendet. IDataObject sorgt dafür, dass die bei einer Drag&Drop-Aktion betroffenen Daten von der Quelle zum Ziel transportiert werden können. Über den eigentlichen Datentransport hinausgehend werden für die Durchführung des Drag&Drop-Vorgangs zwei weitere COMSchnittstellen verwendet: IDropSource und IDropTarget. An den Namen lässt sich schon ablesen, dass IDropSource die Quelle der Drag&Drop-Operation repräsentiert, während IDropTarget das Ziel behandelt. Listing 3.42 zeigt die Schnittstelle IDropSource in IDL-Notation. [ local, object, uuid(00000121-0000-0000-C000-000000000046) ] interface IDropSource : IUnknown { typedef [unique] IDropSource *LPDROPSOURCE;
IDropSource ist mit ihren zwei Methoden eine recht übersichtliche Schnittstelle. Die Methode QueryContinueDrag stellt fest, ob eine Drag&Drop-Operation weitergeführt oder abgebrochen wird. Die Methode GiveFeedback setzt den Mauscursor dem übergebenen Effekt entsprechend.
Drop-Effekte
Drag&Drop wird durch verschiedene Effekte während des Ziehens mit der Maus begleitet. Der Mauszeiger zeigt durch seine Form die Art der durchgeführten Operation an. Handelt es sich bei dem gezogenen Objekt um eine Grafik oder etwas Ähnliches, so wird während des Ziehens üblicherweise ein rechteckiger Umriss des Objekts mitgezogen. Tabelle 3.5 zeigt die vordefinierten Drop-Effekte, die an die Methode GiveFeedback übergeben werden können. Drop-Effekt
Beschreibung
DROPEFFECT_NONE
Ein Effekt ist nicht erlaubt oder nicht möglich.
DROPEFFECT_COPY
Zeigt an, dass Daten kopiert werden sollen.
DROPEFFECT_MOVE
Zeigt an, dass Daten verschoben werden sollen.
DROPEFFECT_LINK
Zeigt an, dass eine Verknüpfung erstellt werden soll.
DROPEFFECT_SCROLL
Zeigt Scrollen während des Ziehens an.
Tabelle 3.5: Drop-Effekte
Die verschiedenen Drop-Effekte werden vom Benutzer normalerweise durch zusätzliches Halten der Umschalt-, Steuerungs- und ALT-Taste ausgewählt. Tabelle 3.6 zeigt die Standardbelegung.
Vereinheitlichter Datenaustausch
Tasten
Drop-Effekt
keine Taste
DROPEFFECT_COPY oder DROPEFFECT_MOVE
(Strg)
DROPEFFECT_COPY
(Strg)(Umschalt)
DROPEFFECT_LINK
(Alt)
DROPEFFECT_MOVE
371
Tabelle 3.6: Tastenkombinationen bei Drag&Drop
Wie bereits gesagt, behandelt die Schnittstelle IDropTarget das Ziel eines Drag&Drop-Vorgangs. Das COM-Objekt, das das Ziel einer Drag&Drop-Operation darstellt, muss IDropTarget implementieren. Da aber das Ziel aus Benutzersicht immer ein Windows-Fenster in irgendeiner Form ist, muss, damit Drag&Drop funktionieren kann, das Zielfenster mit der Implementierung von IDropTarget in Verbindung gebracht werden. Dazu gibt es die API-Funktion RegisterDragDrop. Durch den Aufruf von RegisterDragDrop wird ein IDropTarget-Schnittstellenzeiger mit einem Windows-Fenster registriert. Daraufhin werden alle Ereignisse während der Drag&DropOperation durch Aufrufe der Methoden der IDropTarget-Schnittstelle mitgeteilt. Listing 3.43 zeigt die IDropTarget-Schnittstelle in IDLNotation. [ object, uuid(00000122-0000-0000-C000-000000000046), pointer_default(unique) ] interface IDropTarget : IUnknown { typedef [unique] IDropTarget *LPDROPTARGET; HRESULT DragEnter ( [in, unique] IDataObject *pDataObj, [in] DWORD grfKeyState, [in] POINTL pt, [in, out] DWORD *pdwEffect ); HRESULT DragOver ( [in] DWORD grfKeyState, [in] POINTL pt, [in, out] DWORD *pdwEffect );
IDropTarget
372
3
COM, OLE und ActiveX
HRESULT DragLeave ( void ); HRESULT Drop ( [in, unique] IDataObject *pDataObj, [in] DWORD grfKeyState, [in] POINTL pt, [in, out] DWORD *pdwEffect ); } Listing 3.43: Die Schnittstelle IDropTarget in IDL-Notation Funktionen der Ansicht
Innerhalb der MFC werden Aufrufe von Methoden der Schnittstelle IDropTarget in Aufrufe von virtuellen Funktionen der Klasse CView umgesetzt. Diese Funktionen werden zu verschiedenen Zeitpunkten der Drag&Drop-Operation aufgerufen. Tabelle 3.7 zeigt diese Funktionen und gibt einen Überblick darüber, wann sie aufgerufen werden. Funktion
Beschreibung
OnDragEnter
Wird aufgerufen, wenn eine Drag-Operation in den Bereich der Ansicht eintritt.
OnDragLeave
Wird aufgerufen, wenn eine Drag-Operation den Bereich der Ansicht verlässt.
OnDragOver
Wird wiederholt während des Ziehens im Bereich der Ansicht aufgerufen.
OnDrop
Wird beim Fallenlassen innerhalb der Ansicht aufgerufen. Die Operation wird damit beendet.
OnDropEx
Wird beim Fallenlassen innerhalb der Ansicht aufgerufen, wenn die Drag-Operation mit der rechten Maustaste durchgeführt worden ist. Als Reaktion darauf sollte normalerweise ein Kontextmenü angezeigt werden.
Tabelle 3.7: Drag&Drop-Funktionen der Ansicht
3.4.4 COleDataSource und COleDataObject
Die Klassen der MFC für den vereinheitlichten Datenaustausch
Die MFC besitzen zwei Klassen, um den Datenaustausch über die IDataObject-Schnittstelle zu unterstützen. Die Klasse COleDataSource implementiert die Schnittstelle IDataObject. Damit ist es einem MFC-Programm möglich, eine Datenquelle bereitzustellen
Vereinheitlichter Datenaustausch
373
und Daten per Zwischenablage oder per Drag&Drop weiterzugeben. Die Klasse COleDataObject ist dagegen für das andere Ende der Datenübertragung zuständig. Sie kapselt den Zugriff auf einen IDataObject-Zeiger. Damit kann ein MFC-Programm Daten entgegennehmen. Für die Schnittstellen IDropSource und IDropTarget existieren ebenfalls zwei MFC-Klassen, sie heißen COleDropSource und COleDropTarget. Abbildung 3.28 zeigt alle vier Klassen als Teil der MFC-Klassenhierarchie.
CObject CCmdTarget COleDataSource COleDropSource COleDropTarget COleDataObject Abbildung 3.28: MFC-Klassen für den vereinheitlichten Datenaustausch
3.4.5
Vereinheitlichter Datenaustausch mit dem Programm StockChart
Nun soll die bisherige Theorie in die Praxis umgesetzt und das Programm StockChart mit Funktionen zum Datenaustausch über die Zwischenablage und für Drag&Drop ausgestattet werden. Diese Version des Beispielprogramms heißt StockChartUDT. Sie befindet sich auf der Begleit-CD im Verzeichnis KAPITEL3\STOCKCHARTUDT. Die Daten, die das Beispielprogramm StockChartUDT austauschen soll, sind der Name der Aktie, deren Wertpapierkennnummer und das Tickersymbol. Abbildung 3.29 zeigt, wie die über die Zwischenablage kopierten Daten im Windows-Editor aussehen. Der Einfachheit halber wird von StockChartUDT nur das Kopieren in und das Einfügen aus der Zwischenablage unterstützt. In einem reinen Anzeigeprogramm ist die Implementierung des Befehls AUSSCHNEIDEN nicht sinnvoll. Bei Drag&Drop werden die Daten immer kopiert. Die Implementierung des Zugriffs auf die Zwischenablage und von Drag&Drop wird durch die Ansichtsklasse vorgenommen. Listing 3.44 zeigt die entsprechenden Funktionen der Ansichtsklasse.
Programm StockChartUDT
374
3
COM, OLE und ActiveX
Abbildung 3.29: Von StockChart kopierte Daten im Windows-Editor ////////////////////////////////////////////////////////////// / // UDT-Funktionen void CStockChartUDTView::OnUpdateEditCopy(CCmdUI* pCmdUI) { CStockChartUDTDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // Keine leeren Dokumente kopieren: if (pDoc->m_name.GetLength() > 0) pCmdUI->Enable (true); else pCmdUI->Enable (false); } void CStockChartUDTView::OnUpdateEditPaste(CCmdUI* pCmdUI) { COleDataObject dataObject; // Nur einfügen, wenn Text in der Zwischenablage ist: dataObject.AttachClipboard (); if (dataObject.IsDataAvailable (CF_TEXT)) pCmdUI->Enable (true); else pCmdUI->Enable (false); } void CStockChartUDTView::OnEditCopy() { COleDataSource *pData = PutData(); pData->SetClipboard (); }
Vereinheitlichter Datenaustausch void CStockChartUDTView::OnEditPaste() { COleDataObject dataObject; CString text; dataObject.AttachClipboard (); if (ExtractData (&dataObject, text)) { ConvertAndStoreData (text); } } ////////////////////////////////////////////////////////////// // PutData // Verpackt Daten in eine Datenquelle und gibt diese zurück. COleDataSource* CStockChartUDTView::PutData() { CStockChartUDTDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); HGLOBAL hText; COleDataSource* pDataSource = new COleDataSource(); hText = ::GlobalAlloc (GMEM_SHARE, 256); LPTSTR pText = (LPTSTR)::GlobalLock (hText); ASSERT (pText); sprintf (pText, "%s\r\n%li\r\n%s\r\n", pDoc->m_name, pDoc->m_nID, (LPCTSTR)pDoc->m_ticker); ::GlobalUnlock (hText); pDataSource->CacheGlobalData (CF_TEXT, hText); return pDataSource; } ////////////////////////////////////////////////////////////// / // ExtractData // Extrahiert Daten aus einer Datenquelle. bool CStockChartUDTView::ExtractData (COleDataObject *pDataObject, CString & text) { if (pDataObject->IsDataAvailable (CF_TEXT)) { HGLOBAL hText; hText = pDataObject->GetGlobalData (CF_TEXT); if (hText) {
Wie alle MFC-Programme, die intern COM-Schnittstellen ansprechen, muss das Programm StockChartUDT zunächst die COMLaufzeitbibliothek initialisieren. Dies wird durch einen Aufruf der Funktion AfxOleInit innerhalb der Funktion InitInstance des Applikationsobjekts erledigt.
Bearbeiten-Menü
Damit der Datenaustausch über die Zwischenablage möglich wird, wurde dem Programm StockChartUDT als Nächstes wieder ein BEARBEITEN-Menü hinzugefügt. Aus den bisherigen Versionen von StockChart war das BEARBEITEN-Menü gelöscht worden, da es nicht gebraucht wurde. Um die Menüeinträge zu aktivieren und zu deaktivieren sind die Funktionen OnUpdateEditCopy und OnUpdateEditPaste eingefügt worden. Die Funktion OnUpdateEditCopy schaltet den Menüeintrag KOPIEREN nur dann aktiv, wenn ein Name für die Aktie im aktiven Dokument existiert. Es sollen schließlich keine leeren Einträge kopiert werden. Die Funktion OnUpdateEditPaste schaltet den Menüeintrag EINFÜGEN nur dann aktiv, wenn es tatsächlich Daten in der Zwischenablage gibt, die eingefügt werden können. Das Programm StockChartUDT verwendet zum Datenaustausch das Format CF_TEXT. Die Funktion OnUpdateEditPaste darf daher den Menüpunkt EINFÜGEN nur dann aktivieren, wenn auf Daten in der Zwischenablage in diesem Format zugegriffen werden kann. Um auf die Zwischenablage zuzugreifen, legt die Funktion OnUpdateEditPaste zunächst eine Variable der Klasse COleDataObject an. Die Klasse COleDataObject kapselt – wie schon erwähnt – eine IDataObject-Schnittstelle. Durch den Aufruf der Member-Funktion
Vereinheitlichter Datenaustausch
379
AttachClipboard wird diese Schnittstelle mit der Zwischenablage verbunden. Gleichzeitig sperrt der Aufruf von AttachClipboard den Zugriff auf die Zwischenablage, das heißt, andere Programme können die Daten der Zwischenablage nicht verändern. Erst der Aufruf des Destruktors von COleDataObject hebt diese Sperrung wieder auf. Im Sinne der allgemeinen Verfügbarkeit der Zwischenablage sollte man die Zeit der Sperrung auf ein Minimum beschränken. Nachdem die Verbindung mit der Zwischenablage hergestellt ist, wird die Funktion IsDataAvailable mit dem Parameter CF_TEXT aufgerufen, um festzustellen, ob sich Daten im Textformat in der Zwischenablage befinden. Wenn das der Fall ist, wird der Menüpunkt EINFÜGEN aktiviert, anderenfalls wird er deaktiviert. Um die Operationen KOPIEREN und EINFÜGEN selbst zu behandeln, sind die Funktionen OnEditCopy und OnEditPaste zur Ansichtsklasse hinzugefügt worden. Um Daten in die Zwischenablage zu kopieren, wird die Funktion OnEditCopy aufgerufen. Die Funktion OnEditCopy ruft ihrerseits zunächst die Funktion PutData auf, um einen Zeiger auf eine Instanz von COleDataSource zu erhalten. Diese Instanz wird in PutData angelegt und bereits mit den zu kopierenden Daten initialisiert. Die Funktion OnEditCopy muss jetzt nur noch die Daten durch Aufruf der Funktion SetClipboard tatsächlich in die Zwischenablage kopieren. Damit ist das Kopieren in die Zwischenablage abgeschlossen. Interessanterweise wird die Instanz von COleDataSource nicht wieder freigegeben. Die Verantwortung für die Freigabe des Objekts wird an das OLE-Laufzeitsystem übergeben, das das Objekt so lange im Speicher hält, bis die Daten in der Zwischenablage durch andere Daten überschrieben werden. Dann gibt das Laufzeitsystem das Objekt wieder frei. Auf diese Weise bleiben die Daten in der Zwischenablage verfügbar, auch wenn das Programm, das sie dort platziert hat, schon längst beendet worden ist. Das Anlegen der COleDataSource-Variablen und ihre Initialisierung wird nicht direkt in der Funktion OnEditCopy durchgeführt, sondern ist in die Funktion PutData ausgelagert worden, da sich PutData für Drag&Drop wiederverwenden lässt. Drag&Drop verwendet den gleichen auf der COM-Schnittstelle IDataObject aufbauenden Datenaustauschmechanismus wie die Zwischenablage, so dass es sich anbietet, den Kern des Datenaustauschvorgangs unabhängig von der tatsächlichen Austauschmethode zu formulieren. Genau das macht die Funktion PutData und – in umgekehrter Richtung – die Funktion ExtractData.
Freigabe der Daten
380
3 Kopieren der Daten
COM, OLE und ActiveX
Die Funktion PutData legt zunächst ein Objekt der Klasse COleDataSource auf dem Heap an. Danach legt es einen globalen Speicherbereich an und sperrt diesen für den Zugriff. Der Speicher wird mit dem Flag GMEM_SHARE angefordert, um einen Austausch über die Zwischenablage zu ermöglichen. Die Daten werden dann mit der Funktion sprintf in den globalen Speicher kopiert. Zu beachten ist hierbei, dass Textzeilen durch die Kombination \r\n abgeschlossen werden sollten. Dies ist eine Eigenart des Formats CF_TEXT. Der abschließende Aufruf der Funktion CacheGlobalData weist den globalen Speicherbereich dem COleDataSource-Objekt zu. Über die von diesem Objekt implementierte IDataObject-Schnittstelle kann dann später auf die Daten zugegriffen werden. Möchte man statt des globalen Speichers ein anderes Datenaustauschmedium wie beispielsweise eine Datei oder einen IStorageSchnittstellenzeiger verwenden, so muss man statt der Funktion CacheGlobalData die Funktion CacheData der Klasse COleDataSource verwenden. Die Funktion CacheData ist universeller, da der Funktion ein Zeiger auf eine Struktur des Typs FORMATETC sowie ein weiterer Zeiger auf eine Struktur des Typs STGMEDIUM übergeben werden. Damit können alle Austauschmedien des vereinheitlichten Datenaustauschs ausgewählt werden.
Extrahieren der Daten
Die Funktion ExtractData implementiert den Datenaustausch von der anderen Seite: Das heißt, Daten werden aus der Zwischenablage herauskopiert oder ein Drop-Ziel extrahiert Daten. ExtractData bekommt bereits einen Zeiger auf eine Variable der Klasse COleDataObject übergeben. Die Klasse COleDataObject kapselt den Zugriff auf eine IDataObject-Schnittstelle. Der aus der Datenquelle extrahierte Text wird in Form einer CString-Referenzvariablen zurückgereicht. ExtractData gibt den Wert true zurück, wenn Daten erfolgreich extrahiert werden konnten, andernfalls wird der Wert false zurückgegeben. Bevor ExtractData versucht, Daten aus der Datenquelle zu extrahieren, fragt die Funktion durch Aufruf von IsDataAvailable mit dem Parameter CF_TEXT, ob überhaupt Daten im passenden Format vorliegen. Ist das der Fall, dann wird durch Aufruf der Funktion GetGlobalData ein Handle auf einen globalen Speicherbereich angefordert, der die Daten der Datenquelle enthält. Verwendet man keinen globalen Speicherbereich als Datenaustauschmedium, so muss man statt GetGlobalData die Funktion GetData der Klasse COleDataObject verwenden. Diese Funktion ist universeller als GetGlobalData, sie
Vereinheitlichter Datenaustausch
381
kann mit allen Medien umgehen, die zum vereinheitlichten Datenaustausch verwendet werden. GetGlobalData gibt einen globalen Speicher-Handle zurück. Durch Aufruf der Funktion GlobalLock wird ein Zeiger auf den Speicherbereich des Handles angefordert. Durch den Kopieroperator der Klasse CString werden die Daten des Speicherbereichs in die Variable text kopiert. Anschließend wird der globale Speicherbereich freigegeben. Verwendet wird die Funktion ExtractData von der Funktion OnEditPaste der Ansicht, die das Einfügen aus der Zwischenablage implementiert. OnEditPaste legt eine Variable vom Typ COleDataObject auf dem Stack an und ruft dann deren AttachClipboardFunktion auf, um sie mit der Zwischenablage zu verbinden. Der anschließende Aufruf der gerade besprochenen Funktion ExtractData kopiert dann den Inhalt der Zwischenablage in die Variable text. Durch Aufruf der Funktion ConvertAndStoreData wird versucht, den Inhalt der Zwischenablage sinnvoll in die Datenelemente des Dokumentenobjekts zu kopieren. Die Funktion ConvertAndStoreData nimmt die ersten drei Zeilen des erhaltenen Texts, konvertiert diese und ruft die Funktion SetStockData der Dokumentenklasse auf, um die Werte für Aktienname, WKN und Tickersymbol zu setzen. Nun zur Besprechung des Datenaustauschs per Drag&Drop: Damit die Ansichtsfenster des Beispielprogramms StockChartUDT als Drop-Ziel auftreten können, müssen sie sich zunächst als ein solches registrieren. Dazu wird innerhalb der Ansichtsklasse die private Variable m_dropTarget der Klasse COleDropTarget angelegt. In der Funktion OnInitialUpdate der Ansicht wird deren Member-Funktion Register mit dem Zeiger auf die Ansichtsklasse (this) aufgerufen. Damit ist die Ansicht als Ziel von DropOperationen registriert. Als Folge davon werden die Funktionen OnDragEnter, OnDragLeave, OnDragOver, OnDrop und OnDropEx der Ansicht aufgerufen, wenn der Benutzer eine Drag&DropOperation durchführt. Das Programm StockChartUDT implementiert die drei Funktionen OnDragEnter, OnDragOver und OnDrop. Die Funktion OnDropLeave wird nicht benötigt, da StockChartUDT keinen Rahmen für hineingezogene Daten zeichnet, der beim Verlassen der Ansicht mit der Maus zurückgesetzt werden müsste. OnDropEx müsste nur implementiert werden, wenn Unterstützung für die rechte Maustaste gewünscht wäre.
Drag&Drop
382
3
COM, OLE und ActiveX
Drop-Effekt bestimmen
Die Funktionen OnDragEnter und OnDragOver müssen im Beispiel lediglich den passenden Drop-Effekt bestimmen und diesen zurückgeben. Daten einer Ansicht sollen nicht auf die eigene Ansicht oder in andere Ansichten kopiert werden können, die das gleiche Dokument anzeigen. In diesen Fällen muss folglich DROPEFFECT_NONE zurückgegeben werden, ansonsten DROPEFFECT_COPY. Sind die transportierten Daten allerdings nicht vom Typ CF_TEXT, dann können sie vom Programm nicht verwendet werden, und es muss ebenfalls DROPEFFECT_NONE zurückgegeben werden. Um bestimmen zu können, ob gerade Daten in das eigene Dokument kopiert werden, ist der Dokumentenklasse die Variable m_bDrag hinzugefügt worden. Besitzt diese Variable den Wert true, dann wird gerade eine Drag-Operation mit diesem Dokument als Quelle durchgeführt.
Drop ausführen
Die Implementierungen der Funktionen OnDragEnter und OnDragOver prüfen folglich den Wert der Variablen m_bDrag und fragen den Typ der transportierten Daten ab. Wenn m_bDrag den Wert false hat und die transportierten Daten vom Typ CF_TEXT sind, dann wird der Wert DROPEFFECT_COPY zurückgegeben, anderenfalls DROPEFFECT_NONE. Die Funktion OnDrop erklärt sich nun schon fast von selbst. Ist die Variable m_bDrag des Dokuments gesetzt, dann macht OnDrag nichts. Anderenfalls extrahiert sie die Daten genau wie OnEditPaste durch Aufruf der Funktion ExtractData. Die Funktion ConvertAndStoreData konvertiert die Daten anschließend und weist sie dem Dokumentenobjekt zu.
Drag starten
Abschließend bleibt noch die Initiierung einer Drag-Operation zu beschreiben. Diese wird innerhalb der Behandlungsfunktion für die Windows-Nachricht WM_LBUTTONDOWN durchgeführt. WM_LBUTTONDOWN ist die Nachricht, die ein Fenster erhält, wenn die linke Maustaste innerhalb des Fensters gedrückt wird. Die Funktion zur Behandlung dieser Nachricht wird als Nachrichtenbehandlungsfunktion innerhalb der Entwicklungsumgebung angelegt und heißt OnLButtonDown. Die Funktion OnLButtonDown ruft zunächst die bereits beschriebene Funktion PutData auf und legt damit ein Objekt der Klasse COleDataSource an. Dieses Objekt ist nach Aufruf der Funktion PutData bereits mit den zu transportierenden Daten initialisiert. Anschließend muss das Flag m_bDrag des zur Ansicht gehörenden Dokuments auf den Wert true gesetzt werden, damit ein Drop auf Ansichten des eigenen Dokuments verboten wird. Danach muss
Vereinheitlichter Datenaustausch
nur noch die Funktion DoDragDrop der Klasse COleDataSource aufgerufen werden, um den Drag-Vorgang zu starten. DoDragDrop ist eine Funktion, die erst zurückkehrt, wenn die Drag-Aktion entweder durchgeführt oder abgebrochen wurde. Daher kann nach dem Aufruf von DoDragDrop das Flag m_bDrag wieder auf den Wert false gesetzt und das COleDataSource-Objekt gelöscht werden. Man beachte, dass das Objekt im Gegensatz zum Datenaustausch über die Zwischenablage tatsächlich gelöscht wird. Im Falle von Drag&Drop müssen die Daten nicht mehr für spätere Zugriffe aufbewahrt werden.
3.4.6
Tipps zur Vorgehensweise
왘 Die Möglichkeiten zum Datenaustausch über die Zwischenablage und per Drag&Drop sollten immer zusammen betrachtet werden. Da beide Verfahren die COM-Schnittstelle IDataObject verwenden, lässt sich der Datenaustausch unabhängig vom tatsächlich verwendeten Verfahren formulieren. Man kann sich auf diese Weise doppelte Arbeit ersparen. 왘 Die MFC implementieren den vereinheitlichten Datenaustausch mit Hilfe der Klassen COleDataSource und COleDataObject. COleDataSource wird verwendet, um Daten zum Versand einzupacken und abzuschicken; mit der Klasse COleDataObject packt man sie auf der anderen Seite wieder aus. 왘 Damit Drag&Drop funktionieren kann, muss die Ansichtsklasse eine Variable der Klasse COleDropTarget anlegen und deren Register-Funktion aufrufen. Den Aufruf platziert man am besten in der Funktion OnInitialUpdate der Ansichtsklasse.
3.4.7
Zusammenfassung
Der Kern des vereinheitlichten Datenaustauschs ist die COMSchnittstelle IDataObject. Die Verwendung von IDataObject ist keinesfalls nur auf den Datenaustausch über die Zwischenablage und Drag&Drop beschränkt. Obwohl dies die gängigen Anwendungen zur direkten Verwendung dieser Schnittstelle sind, spielt IDataObject auch an anderen Stellen eine wichtige Rolle. So wird IDataObject beispielsweise innerhalb von OLE verwendet. IDataObject macht es möglich, den Austausch von Daten innerhalb eines Programms oder zwischen mehreren Programmen unabhängig von einem Transportmedium zu formulieren. Diese Abstraktion
383
384
3
COM, OLE und ActiveX
vom tatsächlichen Transportmedium führt dazu, dass sich der Datenaustauschmechanismus von IDataObject in verschiedenen Kontexten wiederverwenden lässt. Dies ist genau die Intention, die hinter COM steht. Durch die Definition von Schnittstellen, die abstrakt einen Sachverhalt beschreiben, soll die Wiederverwendbarkeit in verschiedenen Umgebungen erreicht werden. Die Schnittstelle IDataObject liefert diese abstrakte Beschreibung für Belange des Datenaustauschs zwischen und innerhalb von Programmen.
3.5
Object Linking and Embedding
Object Linking and Embedding – kurz OLE – war die erste Technologie, die auf der Basis von COM implementiert wurde. Genau genommen war dies die Version 2.0 von OLE: OLE2. Vor OLE2 gab es bereits eine Version von OLE, die heute rückblickend als OLE1 bezeichnet wird. OLE1 basierte auf DDE statt auf COM, es war langsam und besaß keine besonders gelungene Implementierung. Dokumentenzentriertes Arbeiten
OLE2 war bei seiner Einführung der Star unter den neuen WindowsTechnologien. Durch OLE sollte die Idee des dokumentenzentrierten Arbeitens in Windows verankert werden. Dokumentenzentriert bedeutet, dass sich der Schwerpunkt der Arbeitsweise des Benutzers von den Applikationen (wie Word oder Excel) auf die Dokumente (Texte, Tabellen, Grafiken) verschiebt. Das Dokument tritt in den Vordergrund und damit muss die Applikation in den Hintergrund treten. Diese etwas abstrakte Aussage lässt sich besser verstehen, wenn man unterschiedliche Teile eines Dokuments (wie Texte oder Grafiken) als Objekte begreift. Um diese Objekte zu bearbeiten, benötigt man je nach Typ des Objekts unterschiedliche Werkzeuge. Diese Werkzeuge werden normalerweise durch die Applikation bereitgestellt, die man zum Bearbeiten des Objekts auswählt. Man nimmt beispielsweise Excel, um eine Tabelle zu bearbeiten, oder Corel Draw, um eine Grafik zu verändern. Die Idee des dokumentenzentrierten Arbeitens dreht diese Herangehensweise um: Man sucht sich nicht die Applikation aus, um ein bestimmtes Objekt zu bearbeiten, sondern man fügt einfach ein Objekt eines bestimmten Typs in ein Dokument ein. Will man dieses Objekt dann bearbeiten, so werden die dazu notwendigen Werkzeuge automatisch bereitgestellt! Die Arbeitsumgebung verändert sich automatisch, um den speziellen Anforderungen bei der Bearbeitung eines Objekts innerhalb des Dokuments zu genügen.
Object Linking and Embedding
385
OLE versucht genau diesen Ansatz zu implementieren. Das klassische OLE-Beispiel fügt eine Excel-Tabelle in ein Word-Dokument ein. Die Excel-Tabelle kann dann aus Word heraus bearbeitet werden. Abbildung 3.30 zeigt diesen »Klassiker«.
Abbildung 3.30: Excel-Tabelle in Word eingebettet
Wie viele COM-basierte Technologien ist OLE eine Client-ServerTechnologie. Die mit OLE zu bearbeitenden Objekte werden als OLE-Server bezeichnet, eingefügt werden sie in einen OLE-Container (Client). OLE-Server können durch zwei verschiedene Verfahren eingefügt werden: Durch Einbettung (Embedding) wird das Objekt physisch in den Container eingefügt. Ein eingebettetes Objekt wird im Dokument des Containers gespeichert und innerhalb der Container-Applikation bearbeitet. Als zweite Möglichkeit des Einfügens existiert das Verknüpfen (Linking) von Objekten. Bei verknüpften Objekten wird nur ein Verweis auf das Objekt im Container-Dokument gespeichert. Bearbeitet wird das verknüpfte Objekt außerhalb der Container-Applikation durch sein eigenes Programm. Die Excel-Tabelle in Abbildung 3.30 zeigt den Fall der Einbettung. Wäre die Excel-Tabelle verknüpft, so würde man sie ganz normal innerhalb von Excel bearbeiten. Abbildung 3.31 stellt Einbettung und Verknüpfung gegenüber.
Einbettung und Verknüpfung
386
3
Einbettung
Verknüpfung
Verbunddokument
Verbunddokument
Eingebettetes Dokument
Verknüpfung
COM, OLE und ActiveX
Verknüpftes Dokument
Abbildung 3.31: Einbettung und Verknüpfung bei OLE
In der Praxis wird OLE relativ wenig eingesetzt. Echtes dokumentenzentriertes Arbeiten wird bis heute nicht praktiziert. Die mit OLE konkurrierende Architektur OpenDoc, die ebenfalls dokumentenzentriertes Arbeiten ermöglichen sollte, ist von ihren Entwicklern Apple und IBM schon vor längerer Zeit aufgegeben worden. Ein Grund für die relativ geringe Akzeptanz von OLE und ähnlichen Technologien könnte der Verlust von separaten Dateien für Objekte eines Dokuments sein. Wer beispielsweise Grafiken per OLE in sein Word-Dokument einbettet, wird möglicherweise später Probleme haben, diese Grafiken wieder als separate Grafikdateien abzuspeichern. OLE ist eine COM-Technologie, die auf einer großen Zahl von COM-Schnittstellen aufbaut. Aufgrund der Komplexität von OLE einerseits und seiner relativ geringen Akzeptanz andererseits wird sich dieses Kapitel auf eine Beschreibung der Implementierung von OLE-Servern mit den MFC beschränken. OLE-Container sind von geringerem Interesse, da man meist Objekte in einen bestehenden Container einfügen möchte. In einer idealen, dokumentenzentrierten Welt gäbe es nur einen Standardcontainer, in den alle Server-Objekte eingefügt würden. Bis zu einem gewissen Grad übernehmen die Office-Anwendungen von Microsoft, allen voran Word und Excel, die Rolle solcher Standard-Container.
3.5.1
Strukturierte Ablage
Damit OLE funktionieren kann, bedarf es eines Speichermediums, das unterschiedliche Ablageformate in einer Datei vereinigen kann. Bei der Einbettung werden die Daten der Container-Anwendung
Object Linking and Embedding
387
zusammen mit den Daten aller eingebetteter Objekte gespeichert. Jedes eingebettete Objekt muss seine eigenen Daten in einem ihm angemessenen Format speichern können. Da der Programmierer keine Annahmen darüber treffen kann, welche Art von Daten ein eingebettetes Objekt überhaupt speichert, muss es möglich sein, jede Art von Daten zu speichern. Das eingebettete Objekt muss einen Teil in der Datei des Container-Objekts zugewiesen bekommen, in dem es seine Daten im eigenen Format ablegen kann. Eine solche Speichermöglichkeit wird durch die strukturierte Ablage definiert. Die strukturierte Ablage ist nichts weiter als eine Spezifikation von einigen COM-Schnittstellen, die ein Speichermodell definieren, das dem eines Dateisystems nachempfunden ist. Ein Dateisystem eignet sich schließlich hervorragend dazu, um Daten unterschiedlichster Art in einer Hierarchie von Verzeichnissen übersichtlich abzulegen. Die strukturierte Ablage definiert Storageund Stream-Objekte. Storage-Objekte entsprechen den Verzeichnissen eines normalen Dateisystems, sie bilden das hierarchische Gerüst der strukturierten Ablage. Stream-Objekte entsprechen den Dateien eines Dateisystems, sie enthalten die eigentlichen Daten. Abbildung 3.32 zeigt eine Hierarchie, wie sie in einem Dokument, das die strukturierte Ablage verwendet, aussehen könnte.
Storage
Hauptdokument Stream
Inhaltsverzeichnis
Storage
Bild Stream
Bilddaten
Stream
Bildbeschreibung
Stream
Text
Stream
Impressum
Abbildung 3.32: Beispielstruktur für die strukturierte Ablage
Storage- und Stream-Objekte
388
3 IStream und IStorage
COM, OLE und ActiveX
Sowohl Storage- als auch Stream-Objekte werden durch jeweils eine eigene COM-Schnittstelle definiert. Die Schnittstelle IStorage beschreibt Storage-Objekte, IStream beschreibt Stream-Objekte. Listing 3.45 zeigt beide Schnittstellen in IDL-Notation. Da IStream nicht direkt von IUnknown abgeleitet ist, sondern von ISequentialStream, ist auch diese Schnittstellendefinition im Listing zu sehen. [ object, uuid(0000000b-0000-0000-C000-000000000046), pointer_default(unique) ] interface IStorage : IUnknown { typedef [unique] IStorage * LPSTORAGE; typedef struct tagRemSNB { unsigned long ulCntStr; unsigned long ulCntChar; [size_is(ulCntChar)] OLECHAR rgString[]; } RemSNB; typedef [unique] RemSNB * wireSNB; typedef [wire_marshal(wireSNB)] OLECHAR **SNB; HRESULT CreateStream( [in, string] const OLECHAR *pwcsName, [in] DWORD grfMode, [in] DWORD reserved1, [in] DWORD reserved2, [out] IStream **ppstm); HRESULT OpenStream( [in, string] const OLECHAR *pwcsName, [in, unique] void *reserved1, [in] DWORD grfMode, [in] DWORD reserved2, [out] IStream **ppstm); HRESULT CreateStorage( [in, string] const OLECHAR *pwcsName, [in] DWORD grfMode, [in] DWORD dwStgFmt, [in] DWORD reserved2, [out] IStorage **ppstg);
Die Schnittstelle IStorage besitzt Methoden, um Stream-Objekte anzulegen und zu öffnen (CreateStream und OpenStream), um untergeordnete Storage-Objekte anzulegen und zu öffnen (CreateStorage und OpenStorage), um ganze Storage-Zweige zu kopieren (CopyTo), um Storage-Objekte innerhalb der Hierarchie zu verschieben (MoveElementsTo), um Objekte umzubenennen und zu
391
392
3
COM, OLE und ActiveX
löschen (RenameElement und DestroyElement) und um alle Objekte aufzuzählen (EnumElements). Informationen zu einzelnen Objekten lassen sich durch den Aufruf der Methode Stat ermitteln. Auf eine Besonderheit der Schnittstelle IStorage weisen die Methoden Commit und Revert hin. Änderungen an der Hierarchie der Storage-Objekte laufen im Rahmen einer Transaktion ab. Durch den Aufruf von Commit kann man bisher gemachte Änderungen festschreiben, durch den Aufruf von Revert kann man die Änderungen rückgängig machen. Geschlossen wird ein Storage-Objekt – typisch für COM – durch den Aufruf seiner Release-Methode. Die Schnittstelle IStream besitzt die für Dateien typischen Methoden, um Daten zu lesen und zu schreiben (Read, Write, Seek). Durch die Methode Clone wird ein zweiter Schnittstellenzeiger erzeugt, der allerdings auf die gleichen Daten verweist. Damit können Daten von mehreren Stream-Objekten gemeinsam genutzt werden. Durch den Aufruf von Release wird ein Stream-Objekt geschlossen. Interessant ist, dass die Methoden von IStream den Datentyp ULARGE_INTEGER verwenden. ULARGE_INTEGER ist ein 64 Bit breiter Integer-Datentyp ohne Vorzeichen. Damit lassen sich Dateien mit einer Größe von 16 Exabyte (264 Byte) bearbeiten. Die IStream-Schnittstelle kann also als zukunftssicher gelten.
3.5.2
Verbunddateien
Die Schnittstellen der strukturierten Ablage sind – typisch für COM – lediglich eine Spezifikation. Damit man die strukturierte Ablage auch verwenden kann, müssen diese Schnittstellen erst einmal implementiert werden. Glücklicherweise liefert Microsoft bereits eine Implementierung dieser Schnittstellen. Diese ist Teil des OLE-Systems und bildet die strukturierte Ablage auf normale Dateien ab. Diese Dateien bezeichnet man als Verbunddateien. Um Verbunddateien anzulegen und zu öffnen, existiert eine Reihe von API-Funktionen. Beispielsweise kann man mit der Funktion StgOpenStorage eine Verbunddatei öffnen und mit der Funktion StgCreateDocfile eine ebensolche anlegen. Diese Funktionen liefern IStorage-Schnittstellenzeiger zurück, mit denen dann weitergearbeitet werden kann.
Object Linking and Embedding
Mit im Lieferumfang von Visual C++ befindet sich das kleine Programm DFVIEW.EXE. Es befindet sich im Verzeichnis MICROSOFT VISUAL STUDIO .NET\COMMON7\TOOLS. Mit Hilfe dieses kleinen Programms lässt sich die Struktur von Verbunddateien darstellen. Storage-Objekte werden in Form von Ordnern dargestellt, StreamObjekte besitzen ein Text-Symbol. Abbildung 3.33 zeigt das Programm.
Abbildung 3.33: Ansicht einer Verbunddatei mit DFVIEW.EXE
3.5.3
Die Schnittstelle ILockBytes
Normalerweise spricht nichts dagegen, die von Microsoft gelieferte Implementierung der strukturierten Ablage in Form von Verbunddateien zu verwenden. Allerdings schreibt die Spezifikation der strukturierten Ablage gar nicht vor, dass Daten unbedingt in Dateien gespeichert werden müssen. Man könnte beispielsweise versucht sein, die strukturierte Ablage auf eine Datenbank abzubilden. Dieser Fall ist bei Microsofts Implementierung der strukturierten Ablage bereits vorgesehen. Die Implementierungen von IStorage und IStream greifen nicht direkt auf Dateien zu, sondern abstrahieren ihre Zugriffe über eine weitere COM-Schnittstelle: ILockBytes. Die Schnittstelle ILockBytes hat die Aufgabe, ein beliebiges Speichermedium als ein Array aus Bytes darzustellen. Die Implementierungen von IStorage und IStream greifen auf dieses Byte-Array zu, ILockBytes setzt diese Zugriffe in entsprechende Zugriffe auf das unterliegende Medium um. ILockBytes hat damit eine gewisse Ähnlichkeit mit einem Treiber (Abbildung 3.34).
393 DocFile Viewer
394
3
COM, OLE und ActiveX
Verbunddatei ILockBytes-Implementierung Speichermedium Abbildung 3.34: Schichten: Medium, ILockBytes und Verbunddatei Verbunddateien in globalem Speicherbereich
Microsoft stellt im Rahmen der Verbunddatei-Implementierung bereits zwei Versionen von ILockBytes zur Verfügung. Die Standardvariante bildet alle Zugriffe auf normale Dateien des Dateisystems ab. Diese Version wird implizit benutzt, wenn eine Verbunddatei durch Aufruf der Funktion StgCreateDocfile erzeugt wird. Eine zweite Version von ILockBytes bildet alle Zugriffe auf einen globalen Speicherbereich ab. Dieser Speicherbereich kann beispielsweise mit eigenen Funktionen in eine Datei eingefügt oder in ein BLOBFeld einer Datenbank geschrieben werden. Um ein Verbunddokument in einem globalen Speicherbereich anzulegen, ruft man die Funktion CreateILockBytesOnHGlobal auf. Sie legt den globalen Speicherbereich an und verweist mit einem ILockBytes-Schnittstellenzeiger auf ihn. Dieser Schnittstellenzeiger kann anschließend an die Funktion StgCreateDocfileOnILockBytes übergeben werden, die ein Verbunddokument auf dem durch ILockBytes bereitgestellten Medium erzeugt. ILockBytes ist eine COM-Schnittstelle mit sieben Methoden. Listing 3.46 zeigt die Schnittstelle in IDL-Notation. [ object, uuid(0000000a-0000-0000-C000-000000000046), pointer_default(unique) ] interface ILockBytes : IUnknown { typedef [unique] ILockBytes *LPLOCKBYTES; HRESULT ReadAt( [in] ULARGE_INTEGER ulOffset, [out, size_is(cb), length_is(*pcbRead)] void *pv, [in] ULONG cb, [out] ULONG *pcbRead);
Obwohl Verbunddateien die Grundlage von OLE bilden, lassen sie sich auch hervorragend für eigene Zwecke nutzen. Durch seine interne Struktur ist das Dateiformat sehr flexibel, ja es ist nicht einmal auf Dateien beschränkt. Microsoft selbst verwendet Verbunddateien als grundlegendes Dateiformat für Word, Excel und PowerPoint. Dies lässt sich sehr schön mit DFVIEW.EXE überprüfen (vgl. Abbildung 3.33 in Abschnitt 3.5.2, »Verbunddateien«).
3.5.4
Persistente COM-Objekte
Verbunddateien sind ein erster, wichtiger Schritt zur Realisierung von OLE. Schließlich muss ein OLE-Container jede nur erdenkliche Art von Daten speichern können. Dies wird durch den hierarchischen Aufbau der strukturierten Ablage ermöglicht. Damit OLE funktioniert, ist es notwendig, dass COM-Objekte innerhalb von Verbunddateien gespeichert werden können. Die Eigenschaft von Objekten, über die Laufzeit eines Programms hinaus zu bestehen, wird als Persistenz bezeichnet. In Abschnitt 2.3.6, »Serialisierung«, ist bereits beschrieben worden, wie MFC-Objekte durch
395
396
3
COM, OLE und ActiveX
den Vorgang der Serialisierung persistent gemacht werden können. COM-Objekte erreichen Persistenz dagegen durch Implementierung bestimmter COM-Schnittstellen. Die Schnittstellen IPersistStorage und IPersistStream definieren die Speicherung von COM-Objekten mittels der strukturierten Ablage. Beide Schnittstellen sind von der Schnittstelle IPersist abgeleitet, die lediglich eine über IUnknown hinausgehende Methode deklariert. Die Methode GetClassID liefert die CLSID des COM-Objekts zurück. Listing 3.47 zeigt die Schnittstellen IPersist, IPersistStream und IPersistStorage in IDL-Notation. [ object, uuid(0000010c-0000-0000-C000-000000000046) ] interface IPersist : IUnknown { typedef [unique] IPersist *LPPERSIST; HRESULT GetClassID ( [out] CLSID *pClassID ); } [ object, uuid(00000109-0000-0000-C000-000000000046), pointer_default(unique) ] interface IPersistStream : IPersist { typedef [unique] IPersistStream *LPPERSISTSTREAM; HRESULT IsDirty ( void ); HRESULT Load ( [in, unique] IStream *pStm );
HRESULT HandsOffStorage ( void ); } Listing 3.47: Die COM-Schnittstellen IPersist, IPersistStream und IPersistStorage
Persistente COM-Objekte bilden eine weitere Schicht im Modell der strukturierten Ablage, indem sie auf den Dienstleistungen der Schnittstellen IStream und IStorage aufbauen (Abbildung 3.35).
Die Schnittstelle IPersistStream wird nur verwendet, wenn ein COM-Objekt seine Daten komplett in ein einfaches Stream-Objekt speichern kann. Anderenfalls muss die Schnittstelle IPersistStorage verwendet werden. OLE-Container verwenden ausschließlich IPersistStorage, um eingebettete oder verknüpfte COM-Objekte zu speichern. Persistente Objekte sind nicht nur im Zusammenhang mit OLE interessant, sondern werden auch von ActiveX-Steuerelementen benutzt, um deren Zustand dauerhaft zu sichern.
3.5.5 Direkte Aktivierung
Die OLE-Schnittstellen
Die bisher in diesem Kapitel beschriebenen Schnittstellen haben mit der Darstellung, Einbettung und Verknüpfung von OLEObjekten noch gar nichts zu tun. Es ist nur ein allgemeines Speicherschema beschrieben worden, mit dem sich COM-Objekte und andere Daten zusammen in einer allgemeinen und flexiblen Weise speichern lassen. Das ist der Unterbau, der für OLE benötigt wird, der aber auch an anderer Stelle, beispielsweise für ActiveX-Steuer-
Object Linking and Embedding
399
elemente, verwendet werden kann. Oberhalb dieses Unterbaus definiert OLE ein Protokoll zwischen OLE-Server und OLE-Container, das Vorgänge wie die Ausführung von Serverkommandos, Größenänderungen und die direkte Aktivierung beschreibt. Unter direkter Aktivierung versteht man, dass ein eingebetteter OLEServer innerhalb des Container-Programms aktiviert wird. Dazu müssen Server und Container unter anderem aushandeln, welche Menüs und Symbolleisten aus dem Serverprogramm in das Container-Programm einzublenden sind und wo diese platziert werden müssen. Die in Abbildung 3.30 aus Abschnitt 3.5, »Object Linking and Embedding«, gezeigte Einbettung einer Excel-Tabelle in ein Word-Dokument zeigt diese im Zustand der direkten Aktivierung: Die für Excel typischen Menüs und Symbolleisten sind in Word eingeblendet. Die direkte Aktivierung wird von Microsoft auch als visuelle Bearbeitung bezeichnet. Schnittstelle
Aufgabe
Methoden
IOleClientSite
Durch diese Schnittstelle wird für jedes Serverobjekt, das in einen Container eingebettet ist, ein Andockpunkt bereitgestellt. Der Container stellt für jedes Serverobjekt eine Instanz von IOleClientSite bereit.
6
IAdviseSink
Über diese Schnittstelle empfängt der Container Benachrichtigungen des Servers. Bei diesen Benachrichtigungen kann es sich um Änderungen der Daten oder der Darstellung des Servers handeln. Der Container muss diese Schnittstelle implementieren, um seine Darstellung des Servers eventuellen Änderungen anzupassen.
5
IOleInPlaceSite
Diese Schnittstelle behandelt Aspekte der Benutzerschnittstelle des Containers. Vorgänge wie die Aktivierung und Deaktivierung von eingebetteten Objekten werden durch diese Schnittstelle behandelt.
12
IStorage
Diese Schnittstelle stellt dem Serverobjekt ein Speichermedium zur Verfügung, auf das es sich speichern kann.
15
Tabelle 3.8: Essentielle Schnittstellen eines OLE-Containers
Das Protokoll zwischen OLE-Server und -Container ist recht komplex. Es ist eine ganze Reihe von Schnittstellen daran beteiligt, sowohl auf der Seite des Servers als auch auf der Seite des Contai-
OLE-Protokoll
400
3
COM, OLE und ActiveX
ners. Die OLE-Schnittstellen sollen hier nicht im Detail beschrieben werden. Zur Implementierung von OLE-Servern und OLEContainern mit den MFC ist ein detailliertes Verständnis aller Schnittstellen nicht notwendig. Wer sich trotzdem auf der Ebene der COM-Schnittstellen mit OLE befassen möchte, dem sei das im Anhang aufgeführte Buch von Kraig Brockschmidt empfohlen. Die Tabellen 3.8 und 3.9 zeigen eine Übersicht der wichtigsten OLE-Schnittstellen. Die Liste der in den Tabellen gezeigten Schnittstellen ist allerdings keinesfalls vollständig. Informationen zu den Methoden der Schnittstellen können der Online-Hilfe entnommen werden. Schnittstelle
Aufgabe
Methoden
IOleObject
Dies ist die Hauptschnittstelle eines OLE-Servers. Über diese Schnittstelle findet der Großteil der Kommunikation mit dem Container statt.
21
IDataObject
Diese Schnittstelle dient dazu, Daten des Servers in unterschiedlichen Formaten abzurufen. Sie wurde bereits im Rahmen des vereinheitlichten Datenaustauschs beschrieben.
9
IPersistStorage
Diese Schnittstelle wird vom Container verwen- 7 det, um das Serverobjekt auf das vom Container durch seine IStorage-Schnittstelle bereitgestellte Medium zu speichern.
Tabelle 3.9: Essentzielle Schnittstellen eines OLE-Servers Aufgaben von Server und Container
Zwischen OLE-Servern und OLE-Containern herrscht eine klar definierte Aufgabenteilung, die sich natürlich im Protokoll der OLE-Schnittstellen widerspiegelt. Der Container muss den Speicherplatz für sein eigenes Dokument und alle eingebetteten Server bereitstellen. Der Container muss weiterhin auf Benachrichtigungen des Servers achten und Kommandos der Applikation entgegennehmen, wie beispielsweise den Doppelklick zur Aktivierung des Servers. Der Server hat die Aufgabe, sich auf das vom Container bereitgestellte Speichermedium zu speichern, den Container von Änderungen seiner Darstellung zu unterrichten sowie Menüs und Symbolleisten bereitzustellen, die der Container verwenden kann, wenn der Server direkt aktiviert wird. Der Server muss außerdem eine »eingefrorene« Darstellung seiner Daten bereitstellen, die
Object Linking and Embedding
401
angezeigt wird, wenn der Server nicht aktiviert ist. Aus Gründen der Speicher- und Laufzeiteffizienz zeigt ein Container lediglich eine bildliche Darstellung des Servers an, wenn dieser nicht aktiviert ist. Die Grafikausgabe des Servers wird also nur dann verwendet, wenn dieser auch aktiv ist. Anderenfalls wird auf die bildliche Darstellung zurückgegriffen. Diese eingefrorene, bildliche Darstellung wird üblicherweise in Form einer Windows-Metadatei (siehe Kasten) vom Server bereitgestellt. Windows-Metadateien, kurz WMF, sind Aufzeichnungen von GDI-Befehlen. Diese Aufzeichnungen lassen sich im Speicher oder in einer Datei anlegen. Dazu werden GDI-Funktionen wie bei einer normalen Grafikausgabe aufgerufen. Im Unterschied zur Grafikausgabe auf den Bildschirm oder Drucker wird ein Gerätekontext verwendet, der die Metadatei beschreibt und der zuvor angelegt werden muss. Eine Windows-Metadatei kann nach der Aufzeichnung in einem anderen Gerätekontext abgespielt werden. Die MFC besitzen zur Aufzeichnung von Metadateien die Klasse CMetaFileDC. Das Listing zeigt ein kleines Beispiel. Es wird eine Linie in eine Metadatei ausgegeben, anschließend wird die Metadatei abgespielt. // Metadatei aufzeichnen: CMetaFileDC dc; HMETAFILE hMF dc.Create (); // Metadatei im Speicher anlegen: dc.SetSetMapMode (MM_ANISOTROPIC); dc.SetWindowExt (1000,1000); dc.SetWindowOrg (0,0); dc.MoveTo (0,0); dc.LineTo (1000,1000); hMF = dc.Close (); // Metadatei abspielen: CDC *pDC = GetDC (); pDC->SetMapMode (MM_ANISOTROPIC); pDC->SetWindowExt (1000,1000); pDC->SetWindowOrg (0,0); pDC->SetViewportExt (100,100); pDC->SetViewportOrg (0,0); pDC->PlayMetaFile (hMF); ReleaseDC (pDC); ::DeleteMetaFile (hMF); // Metadatei aus dem Speicher entfernen.
Windows-Metadateien
402
3
COM, OLE und ActiveX
Mini- und Voll-Server
Bei OLE-Servern wird zwischen zwei verschiedenen Typen unterschieden: zwischen Mini-Servern und Voll-Servern. Ein Mini-Server kann nur innerhalb eines Containers ausgeführt werden, allein ist das Programm nicht lauffähig. Ein Voll-Server kann dagegen als eigenständiges Programm oder als OLE-Server fungieren. Zudem kann ein Programm gleichzeitig Server und Container sein. Microsoft Word und Excel sind Beispiele für Voll-Server, die nebenher auch noch OLE-Container sein können. Mini-Server werden dagegen recht selten verwendet.
OLE-Verben
Wenn man einen OLE-Server aktiviert, dann wechselt dieser normalerweise in einen Bearbeitungsmodus. Dies ist jedoch keinesfalls vorgeschrieben. Aktiviert man beispielsweise einen eingebetteten Videoclip, so wird dieser abgespielt. Ein OLE-Server kann dieses Verhalten selbst festlegen. Der Server stellt dem Container eine oder mehrere Aktionen zur Verfügung, die über diesen ausgeführt werden können. Diese Aktionen werden als Verben bezeichnet. Ein Verb, das primäre Verb, gibt die Voreinstellung bei der Aktivierung durch einen Doppelklick an. Sollte der Server neben dem primären Verb noch andere Verben bereitstellen, so lassen sich diese durch ein Menü aufrufen. Die meisten Server stellen nur ein Verb bereit, oft ist dies »Bearbeiten«. Bei eingebetteten Medien (Audio, Video, MIDI) ist es dagegen üblich, das Verb »Wiedergabe« zu implementieren und dieses als primäres Verb zu kennzeichnen. Ein OLE-Server macht seine Verben bekannt, indem er sie in die Registrierungsdatenbank einträgt.
3.5.6
Die MFC-Klassen zur OLE-Programmierung
Die MFC unterstützen die Programmierung von OLE-Servern und OLE-Containern durch eine ganze Reihe von Klassen. Abbildung 3.36 zeigt diese Klassen. Sowohl OLE-Server als auch OLE-Container werden von den MFC in die Dokument-Ansicht-Architektur integriert. Entsprechend gibt es die von CDocument abgeleitete Klasse COleDocument. Diese Klasse dient als Basis für alle OLE-Dokumente der MFC. Dokumente einfacher Container-Anwendungen können direkt durch eine von COleDocument abgeleitete Klasse implementiert werden. Von der Klasse COleDocument leiten sich zwei weitere Klassen ab, COleLinkingDoc und COleServerDoc. Die Klasse COleLinkingDoc muss verwendet werden, wenn eine ContainerAnwendung neben der Einbettung von OLE-Servern auch die Ver-
Object Linking and Embedding
403
knüpfung unterstützen soll. COleServerDoc wird schließlich dazu verwendet, OLE-Server zu implementieren. Möchte man eine Anwendung schreiben, die gleichzeitig Server und Container sein kann, so muss man dafür auch COleServerDoc verwenden. Erstellt man ein Programm mit dem Anwendungs-Assistenten, so wählt dieser automatisch die passende Basisklasse für das Dokument aufgrund der gewählten Angaben aus.
CObject CCmdTarget CDocument COleDocument COleLinkingDoc COleServerDoc CDocItem COleClientItem COleServerItem CWnd CFrameWnd COleIPFrameWnd Abbildung 3.36: OLE-Klassen in den MFC
Die Dokumentenklasse verwaltet eine Liste von OLE-Objekten. Im Falle einer Container-Anwendung sind dies eingebettete oder verknüpfte OLE-Server. Eine Serveranwendung trägt in diese Liste die von ihr bereitgestellten OLE-Server ein. Ist eine Anwendung sowohl Server als auch Container, so trägt sie ihre Server und ihre Clients in die gleiche Liste ein. Als Basisklasse für Einträge in diese Liste der verwalteten OLEObjekte dient die Klasse CDocItem. Von CDocItem werden zwei Klassen abgeleitet, COleClientItem und COleServerItem. Die Klasse COleClientItem repräsentiert in einen Container eingebettete oder verknüpfte Objekte, COleServerItem stellt von einem Server bereitgestellte Server-Objekte dar. Abbildung 3.37 zeigt diverse Server in einem MFC-OLE-Container. Dabei ist die Verwendung der Klassen COleClientItem und COleLinkingDoc zu sehen. Abbildung
Liste der OLE-Objekte
404
3
COM, OLE und ActiveX
3.38 zeigt den umgekehrten Fall eines MFC-OLE-Servers mit Objekten der Klassen COleServerItem und COleServerDoc. In einem MFC-Programm, das gleichzeitig Server und Container ist, würde die Klasse COleServerDoc mit einer Mischung von Objekten der Klassen COleClientItem und COleServerItem verwendet werden.
diverse Server
Excel
COleClientItem
Word
COleClientItem
Media Player
COleClientItem
COleLinkingDoc
MFC-Container
Abbildung 3.37: Diverse Server in einem MFC-OLE-Container
diverse Container
COleSeverDoc
MFC-Server COleServerItem
Word
COleServerItem
Excel
COleServerItem
Power Point
Abbildung 3.38: MFC-OLE-Server in diversen Containern
Object Linking and Embedding
Schließlich ist noch die Klasse COleIPFrameWnd von Interesse. Diese Klasse stellt ein Rahmenfenster zur Verfügung, das statt des normalen Rahmenfensters eines Servers verwendet wird, wenn dieser direkt innerhalb einer Container-Anwendung aktiviert wird. COleIPFrameWnd kümmert sich darum, dass Menüs und Symbolleisten des Servers innerhalb der Container-Anwendung bereitgestellt werden. Ein mit den MFC implementierter OLEVoll-Server verwendet bei der direkten Aktivierung andere Menüund Symbolleisten, als wenn er als eigenständiges Programm abläuft. Die Klasse COleIPFrameWnd verwendet diese eigenen Ressourcen. Dadurch, dass ein Voll-Server zwei Sätze von Ressourcen verwendet, können Menüs und Symbolleisten für den Fall der direkten Aktivierung speziell angepasst werden.
3.5.7
StockChart als OLE-Server
Zur Demonstration eines OLE-Servers dient wieder einmal das Programm StockChart. Diese Version von StockChart heißt StockChartOle und befindet sich im Verzeichnis KAPITEL3\STOCKCHARTOLE auf der Begleit-CD. StockChartOle ist als Voll-Server konzipiert worden. Das Programm lässt sich daher sowohl eigenständig verwenden als auch als OLE-Server benutzen. Bei der Erstellung muss dazu die Option »Voll-Server« unter Verbunddokumentunterstützung ausgewählt werden.
Abbildung 3.39: StockChart-Dokument als Objekt einfügen
405 Rahmenfenster des Servers
406
3
COM, OLE und ActiveX
Nachdem das Projekt StockChartOle übersetzt worden ist und damit die entsprechenden Einträge in die Registrierungsdatenbank vorgenommen worden sind, lassen sich StockChart-Dokumente in einen OLE-Container einbetten. Abbildung 3.39 zeigt den entsprechenden Dialog (Menü EINFÜGEN | OBJEKT) aus Microsoft Word. Wenn StockChartOle direkt aktiviert wird, dann werden das Menü und die Symbolleiste des Programms in die ContainerAnwendung eingeblendet. Abbildung 3.40 zeigt StockChartOle direkt in Microsoft Word aktiviert.
Abbildung 3.40: StockChart in Word, direkt aktiviert
Ist StockChartOle hingegen nicht aktiviert, dann wird nur die Darstellung der Aktienkurve als Windows-Metadatei ausgegeben. Abbildung 3.41 zeigt diesen Zustand. Schaut man sich den vom Anwendungs-Assistenten generierten Programmcode an, so fallen ein paar Punkte sofort auf: 왘 Die Dokumentenklasse ist von COleServerDoc abgeleitet, nicht von CDocument, wie das sonst in MFC-Programmen der Fall ist, die die Dokument-Ansicht-Architektur verwenden. 왘 Der Anwendungs-Assistent hat Ressourcen für Menü und Symbolleiste für den Fall der direkten Aktivierung angelegt.
Object Linking and Embedding
407
Abbildung 3.41: StockChart in Word, nicht aktiviert
왘 Der Anwendungs-Assistent hat die von COleServerItem abgeleitete Klasse CStockChartOleSrvrItem eingefügt. Instanzen dieser Klasse repräsentieren OLE-Serverobjekte. 왘 Der Anwendungs-Assistent hat die Klasse CInPlaceFrame angelegt. Diese von COleIPFrameWnd abgeleitete Klasse stellt das Rahmenfenster bei der direkten Aktivierung zur Verfügung. Innerhalb der Dokumentenklasse haben sich gegenüber der Version des Beispielprogramms StockChart, die in Kapitel 2, »Einstieg in die MFC-Programmierung«, besprochen wird, keine großen Änderungen ergeben. Zwei Punkte sind jedoch zu beachten. Der Anwendungs-Assistent hat zur Dokumentenklasse die Funktion OnGetEmbeddedItem hinzugefügt. Diese Funktion wird aufgerufen, wenn die Container-Anwendung Zugriff auf den eingebetteten OLE-Server erhalten möchte. Der Anwendungs-Assistent liefert bereits eine Implementierung für diese Funktion. OnGetEmbeddedItem erzeugt bei seinem Aufruf ein COleServerItem-Objekt und gibt dieses zurück. Dies ist in Listing 3.48 zu sehen. ////////////////////////////////////////////////////////////// / // CStockChartOleDoc Server-Implementierung COleServerItem* CStockChartOleDoc::OnGetEmbeddedItem() {
Zugriff auf den OLE-Server
408
3
COM, OLE und ActiveX
// OnGetEmbeddedItem wird automatisch aufgerufen, um // COleServerItem zu erhalten, das mit dem Dokument verknüpft // ist. Die Funktion wird nur bei Bedarf aufgerufen. CStockChartOleSrvrItem* pItem = new CStockChartOleSrvrItem(this); ASSERT_VALID(pItem); return pItem; } Listing 3.48: Vom Anwendungs-Assistenten generierte Funktion OnGetEmbeddedItem
Der zweite Punkt betrifft die Funktion DeleteContents der Dokumentenklasse. Diese wird aufgerufen, wenn die Daten des Dokumentenobjekts gelöscht werden sollen, ohne das Dokumentenobjekt selbst zu zerstören. Im Programm StockChart wird in der Funktion DeleteContents die Liste mit den Aktiendaten gelöscht. Anders als in Kapitel 2 gezeigt, darf DeleteContents in der Version StockChartOle keinesfalls die Funktion DeleteContents der Basisklasse COleServerDoc aufrufen. OleServerDoc::DeleteContents löscht nämlich die Instanzen aller eingebetteten Serverobjekte, sofern deren Flag m_bAutoDelete den Wert TRUE hat. Der vom Anwendungs-Assistenten generierte Programmcode erzeugt aber genau solche Serverobjekte mit gesetztem m_bAutoDelete-Flag. Das Löschen des Serverobjekts setzt den OLEServer als Ganzes zwar nicht außer Gefecht, da im Zweifelsfall einfach ein neues Serverobjekt angelegt wird. Allerdings funktionieren dann Änderungen der Darstellung im nicht aktivierten Modus nicht mehr korrekt. Die Serverobjekte werden durch die vom Anwendungs-Assistenten erzeugte Klasse CStockChartOleSrvrItem implementiert. CStockChartOleSrvrItem ist von der Klasse COleServerItem abgeleitet und wird in der Datei SRVRITEM.CPP implementiert. Listing 3.49 zeigt diese Datei. ////////////////////////////////////////////////////////////// / // CStockChartOleSrvrItem Implementierung IMPLEMENT_DYNAMIC(CStockChartOleSrvrItem, COleServerItem) CStockChartOleSrvrItem::CStockChartOleSrvrItem ( CStockChartOleDoc* pContainerDoc) : COleServerItem(pContainerDoc, TRUE) { }
Object Linking and Embedding
409
CStockChartOleSrvrItem::~CStockChartOleSrvrItem() { } void CStockChartOleSrvrItem::Serialize(CArchive& ar) { // CStockChartOleSrvrItem::Serialize wird automatisch // aufgerufen, wenn das Element in die Zwischenablage kopiert // wird. Dies kann automatisch über die OLERückruffunktion // OnGetClipboardData geschehen. Ein Standardwert für das // eingebundene Element dient einfach zur Delegierung der // Serialisierungsfunktion des Dokuments. Wenn Sie Verweise // unterstützen, möchten Sie vielleicht nur einen Teil des // Dokuments serialisieren. if (!IsLinkedItem()) { CStockChartOleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); pDoc->Serialize(ar); } } BOOL CStockChartOleSrvrItem::OnGetExtent( DVASPECT dwDrawAspect, CSize& rSize) { // Die meisten Anwendungen (wie auch diese) unterstützen als // einzigen Aspekt das Zeichnen des Elementinhalts. Wollen // Sie andere Aspekte unterstützen, wie z.B. // DVASPECT_THUMBNAIL (durch Überladen von OnDrawEx), so // sollte diese Implementierung von OnGetExtent dahingehend // modifiziert werden, dass sie zusätzliche Aspekte // verarbeiten kann. if (dwDrawAspect != DVASPECT_CONTENT) return COleServerItem::OnGetExtent(dwDrawAspect, rSize); // // zu // //
CStockChartOleSrvrItem::OnGetExtent wird aufgerufen, um das Extent in HIMETRIC-Einheiten des gesamten Elements ermitteln. Die Standardimplementierung liefert hier einfach eine fest programmierte Einheitenanzahl zurück.
rSize = CSize(5000,5000); Einheiten
// 5000 x 5000 HIMETRIC
410
3
COM, OLE und ActiveX
return TRUE; }
BOOL CStockChartOleSrvrItem::OnDraw(CDC* pDC, CSize& rSize) { // Entfernen Sie dies, wenn Sie rSize verwenden UNREFERENCED_PARAMETER(rSize); CStockChartOleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Setzen Sie Mapping-Modus und Extent // (Das Extent stimmt üblicherweise mit der von OnGetExtent // zurückgelieferten Größe überein) pDC->SetMapMode(MM_ANISOTROPIC); pDC->SetWindowOrg (0,0); pDC->SetWindowExt (5000,5000); ::Draw (pDC, pDoc, 5000); return TRUE; } ////////////////////////////////////////////////////////////// / // CStockChartOleSrvrItem Diagnose #ifdef _DEBUG void CStockChartOleSrvrItem::AssertValid() const { COleServerItem::AssertValid(); } void CStockChartOleSrvrItem::Dump(CDumpContext& dc) const { COleServerItem::Dump(dc); } #endif Listing 3.49: Die Implementierung der Klasse CStockChartOleSrvrItem Serialisierung
An der Funktion CStockChartOleSrvrItem::Serialize kann man sehen, dass die OLE-Unterstützung der MFC sehr gut in die Dokument-Ansicht-Architektur integriert worden ist. Serialize wird aufgerufen, wenn sich der OLE-Server in der Verbunddatei des Containers speichern soll. Dabei bekommt die Funktion ein CArchive-Objekt übergeben, mit dem sich die normalen Serialisie-
Object Linking and Embedding
411
rungsmechanismen der MFC verwenden lassen. Die MFC abstrahieren völlig von Verbunddateien und von den COMSchnittstellen IStorage und IPersistStorage. Für den MFC-Programmierer sieht das Speichern des Serverobjekts in der Verbunddatei des Servers wie eine ganz normale Serialisierungsaktion aus. Die vorgegebene Implementierung des Anwendungs-Assistenten delegiert den Aufruf von Serialize einfach an die Dokumentenklasse. Da ein Voll-Server auf jeden Fall Programmcode zur Serialisierung seines Dokuments implementieren muss, beschränkt sich der zusätzliche Aufwand zum Speichern in der Verbunddatei des Containers auf wenige Zeilen Programmcode. Diese werden außerdem bereits vom Anwendungs-Assistenten bereitgestellt! Die Funktion OnGetExtent wird aufgerufen, wenn der Container die Ausmaße des eingebetteten Serverobjekts ermitteln möchte. Alle Größenangaben werden dabei in Einheiten des Abbildungsmodus MM_HIMETRIC angegeben. Die im Beispiel zurückgegebenen Werte von 5000 x 5000 Einheiten entsprechen daher einer Größe von 5 x 5 cm. Dies kann man nachprüfen, indem man ein StockChart-Dokument in Microsoft Word einbettet und das Objekt mit den Linealen nachmisst.
Ausmaße des Servers
Das Programm StockChartOle verwendet in der Funktion OnGetExtent den einfachsten denkbaren Ansatz: Es gibt einfach eine konstante Größe für den Server an. Dieser Ansatz wird so vom Anwendungs-Assistenten vorgegeben. Ein Nachteil dieses einfachen Ansatzes ist, dass sich StockChartOle als eingebetteter OLEServer im direkt aktivierten Zustand unter bestimmten Umständen nicht vergrößern oder verkleinern lässt. Möchte man die Größe des eingebetteten Serverobjekts dauerhaft verändern, so muss man die Größe des Serverobjekts ändern, wenn es nicht aktiv ist. Größenänderungen bleiben dann bestehen, auch wenn das Objekt danach direkt aktiviert wird. Sollen Größenänderungen auch im direkt aktivierten Zustand möglich sein, so muss man zusätzlich zu der Funktion OnGetExtent die Funktion OnSetExtent implementieren. OnSetExtent wird aufgerufen, wenn der Container dem Server mitteilt, welche Fläche dieser innerhalb des Container-Dokuments zur Verfügung hat. Der Server kann dann die Größe anhand der ihm zur Verfügung stehenden Fläche und aufgrund der Größe seines eigenen Dokuments berechnen und diese beim Aufruf von OnGetExtent zurückgeben.
Größenänderungen
412
3 Aspekte des Servers
COM, OLE und ActiveX
OnGetExtent und OnSetExtent können für verschiedene Aspekte (verschiedene Darstellungsformen) des Servers aufgerufen werden. Der Aspekt wird beim Aufruf als Parameter übergeben. Für einfache Server reicht es hier, den Aspekt DVASPECT_CONTENT zu behandeln, der zur Darstellung in normaler Form (auf dem Bildschirm) dient. Weitere Aspekte beschreiben die Darstellung als Symbol, in gedruckter Form oder als Übersicht (thumbnail). Das Beispiel delegiert Aufrufe für alle Aspekte, die sich von DVASPECT_CONTENT unterscheiden, an die Basisklasse. Als letzte interessante Funktion gibt es in der Klasse CStockChartOleSrvrItem die Funktion OnDraw. Wie bereits angedeutet wurde, besitzt ein OLE-Server zwei Darstellungsarten. Ein direkt aktivierter OLE-Server gibt seine grafischen Ausgaben wie jedes Windows-Programm in ein Fenster aus. Bei mit den MFC erstellten OLE-Servern ist es das Ansichtsfenster. Daher ist im Beispielprogramm StockChartOle die Funktion CStockChartOleView::OnDraw für Grafikausgaben während der direkten Aktivierung zuständig. Grafikausgaben während der direkten Aktivierung funktionieren wie die Ausgabe in ein Ansichtsfenster.
Erstellung der WindowsMetadatei
Wenn ein eingebetteter OLE-Server nicht aktiviert ist, dann wird an seiner Stelle eine »eingefrorene« Darstellung in Form einer Windows-Metadatei angezeigt. Diese Metadatei muss ebenfalls durch den Server bereitgestellt werden. In MFC-Programmen wird die Windows-Metadatei durch den Aufruf der Funktion OnDraw in einer von COleServerItem abgeleiteten Klasse erstellt. Bei der Erstellung der Metadatei sind einige Punkte zu beachten. Der Funktion wird bereits ein Zeiger auf einen Gerätekontext, der die Metadatei bezeichnet, übergeben. Leider ist man in Bezug auf den Abbildungsmodus innerhalb von OnDraw stark eingeschränkt. Damit das Anzeigen der Metadatei im Zusammenspiel mit dem OLE-Container funktioniert, muss man den Abbildungsmodus MM_ANISOTROPIC wählen. Dann wird üblicherweise die Fensterausdehnung durch den Aufruf der Funktion SetWindowsExt auf die gleichen Werte gesetzt, die auch OnGetExtent als Abmessungen angibt. Dadurch zeichnet man im Einheitensystem von MM_HIMETRIC; die resultierende Metadatei kann aber frei skaliert werden. Dass hier überhaupt eine Metadatei verwendet wird, bekommt der MFC-Programmierer im Übrigen gar nicht zu Gesicht.
Object Linking and Embedding
Durch die Einschränkungen beim Abbildungsmodus fällt die Grafikausgabe unter Umständen etwas aufwändiger aus. Zudem wird an zwei Stellen im Programm Grafik ausgegeben, in CStockChartOleSrvrItem::OnDraw und in CStockChartOleView::OnDraw. Es bietet sich daher an, die eigentliche Zeichenfunktion aus der Ansichtsklasse auszulagern und so zu verändern, dass sie auch mit den Erfordernissen der Grafikausgabe in die Metadatei zurechtkommt. Im Beispielprogramm StockChartOle ist die Zeichenfunktion aus der Ansichtsklasse in eine globale Funktion ausgelagert worden. Die Funktion wird in der Datei DRAWFUNC.CPP implementiert, sie heißt Draw. Listing 3.50 zeigt die Funktion Draw. // Ausgelagerte Draw-Funktion void Draw(CDC* pDC, CStockChartOleDoc* pDoc, long nSize) { CPen plotPen (PS_SOLID, 1, pDoc->m_nColor); CPen axisPen (PS_SOLID, 1, RGB(0,0,0)); CPen gridPen (PS_SOLID, 1, RGB(192,192,192)); // solid!!! CPen avgPen (PS_SOLID, 1, RGB (0,255,255)); CPen *pOldPen; CFont font, *pOldFont; double minValue, value; double scaleUpFactor; // delta gibt den Abstand zu den Seiten an, // ext die Ausdehnung int delta = nSize * 3 / 100; int ext = nSize - 2 * delta; // Alten Stift sichern, neuen in den Gerätekontext selektieren: pOldPen = pDC->SelectObject (&axisPen); // Achsen zeichnen: pDC->MoveTo (delta, delta + ext); pDC->LineTo (delta, delta); pDC->MoveTo (delta, delta + ext); pDC->LineTo (delta + ext, delta + ext); // Falls gewünscht, Gitternetz zeichnen: if (pDoc->m_bGrid) { int nPart = ext / 10; pDC->SelectObject (&gridPen); for (int i=1; i<=10; i++) { pDC->MoveTo (i*nPart+delta, delta); pDC->LineTo (i*nPart+delta, ext+delta);
413
414
3
COM, OLE und ActiveX
pDC->MoveTo (delta, (i-1)*nPart+delta); pDC->LineTo (ext+delta, (i-1)*nPart+delta); } // for } // Stift zum Zeichnen der Kurve setzen: pDC->SelectObject (&plotPen); // Anzahl der Listeneinträge ermitteln: int nCnt = pDoc->m_stockData.theData.GetCount (); if (nCnt > 0) { POSITION pos;
// hält Position in der Liste
// Auf Anfang der Liste positionieren: pos = pDoc->m_stockData.theData.GetHeadPosition (); scaleUpFactor = ext / (pDoc->m_stockData.max-pDoc ->m_stockData.min); minValue = pDoc->m_stockData.min; // Wert holen und ein Element weiter gehen value = pDoc->m_stockData.theData.GetNext (pos); pDC->MoveTo (delta, delta + ext (int)((value-minValue) * scaleUpFactor)); for (int i=1; im_stockData.theData.GetNext (pos); pDC->LineTo (delta + i*ext/(nCnt-1), delta + ext - (int)((value-minValue) * scaleUpFactor)); } } // Falls gewünscht, Durchschnittslinie zeichnen: if (pDoc->m_bAverage) { pDC->SelectObject (&avgPen); int nAvgCnt = pDoc->m_averageData.GetCount (); if (nAvgCnt > 0) { POSITION pos; // hält Position in der Liste // Auf Anfang der Liste positionieren: pos = pDoc->m_averageData.GetHeadPosition (); value = pDoc->m_averageData.GetNext (pos); pDC->MoveTo (delta + pDoc->m_nAverageCnt/2*ext/(nCnt-1), delta + ext - (int)((value-minValue) * scaleUpFactor)); for (int i=1; i
Object Linking and Embedding value = pDoc->m_averageData.GetNext (pos); pDC->LineTo (delta + (i+(pDoc->m_nAverageCnt/2)) * ext/(nCnt-1), delta + ext - (int)((value-minValue) * scaleUpFactor)); } } } // Schriftart erzeugen und setzen: font.CreateFont (ext/20, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEVICE_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, NULL); pOldFont = pDC->SelectObject (&font); // Beschriftung pDC->TextOut (3*delta/2, 2*delta, pDoc->m_name); if (pDoc->m_nID != 0) { TCHAR buffer[16]; // _itot benutzt automatisch Unicode- oder ANSI-Zeichen: pDC->TextOut (3*delta/2, 7*delta/2, _itot (pDoc->m_nID, buffer, 10)); } pDC->TextOut (3*delta/2, 5*delta, pDoc->m_ticker); // Alten Stift zurücksetzen: pDC->SelectObject (pOldPen); // Alte Schriftart zurücksetzen: pDC->SelectObject (pOldFont); } Listing 3.50: Ausgelagerte Zeichenfunktion Draw
Durch die Bereitstellung von nur einer Zeichenfunktion wird eine redundante Implementierung vermieden. Im Vergleich zu der ursprünglich im Programm StockChart verwendeten Zeichenfunktion haben sich einige Unterschiede ergeben. Die Funktion Draw kann die Zeichnung nun selbst skalieren, dazu wird der Funktion der Parameter nSize übergeben. Der Parameter nSize gibt die Kan-
415
416
3
COM, OLE und ActiveX
tenlänge des als quadratisch angenommenen Ausgabebereichs an. Aus nSize berechnet die Funktion Draw selbsttätig den Abstand der Achsen zum Rand (Variable delta) und die Ausdehnung des Graphen (Variable ext). Daher kommt die Funktion ohne die Unterstützung von bestimmten Windows-Abbildungsmodi aus. Als problematisch hat sich die Verwendung gestrichelter Linien (Stil PS_DOT) erwiesen. OLE-Server scheinen mit dem Zeichnen solcher Linien innerhalb der Metadatei Probleme zu haben. Daher wird innerhalb der Zeichenfunktion Draw im Gegensatz zu anderen Versionen von StockChart auch für das Gitternetz ein durchgängiger Linientyp verwendet (PS_SOLID). Die sonstige Funktionalität der Funktion Draw entspricht den Fähigkeiten der Zeichenfunktionen in der vorherigen Versionen des Programms StockChart. Rahmenfenster des Servers
Als Letztes bleibt noch das Rahmenfenster für die direkte Aktivierung zu erwähnen. Der Anwendungs-Assistent hat dafür die von COleIPFrameWnd abgeleitete Klasse CInPlaceFrame angelegt. Ein Objekt der Klasse CInPlaceFrame fungiert als Rahmenfenster für die Ansicht, wenn ein OLE-Server direkt aktiviert wird. In dieser Klasse werden beispielsweise die Symbolleisten für die direkte Aktivierung erstellt. Diese werden getrennt von den Symbolleisten behandelt, die das Programm verwendet, wenn es als normale Anwendung gestartet wird. Der Programmcode der Klasse CInPlaceFrame ähnelt stark dem entsprechenden Code der Klasse CMainFrame, die für die Erstellung der »normalen« Symbolleisten zuständig ist. Listing 3.51 zeigt den Programmcode der Klasse CInPlaceFrame. ////////////////////////////////////////////////////////////// / // CInPlaceFrame Konstruktion/Destruktion CInPlaceFrame::CInPlaceFrame() { } CInPlaceFrame::~CInPlaceFrame() { } int CInPlaceFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (COleIPFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // CResizeBar implementiert direkte Größenänderungen. if (!m_wndResizeBar.Create(this))
Object Linking and Embedding { TRACE0("Failed to create resize bar\n"); return -1; // Fehler beim Erzeugen } // Allgemein ist es von Vorteil, ein Drop-Ziel zu // registrieren, das keinerlei Auswirkung auf Ihr // Rahmenfenster hat. Dies verhindert bei Drops // ("Abwürfen") // ein "Durchfallen" zu einem Container, der Drag&Drop // unterstützt. m_dropTarget.Register(this); return 0; } // OnCreateControlBars wird automatisch aufgerufen, um // Steuerleisten im Fenster der Container-Anwendung zu // erstellen. pWndFrame ist das hierarchisch höchste // Rahmenfenster des Containers und ist immer ungleich NULL. // pWndDoc ist das Rahmenfenster auf Dokumentebene // und ist gleich NULL, wenn der Container eine SDI-Anwendung // ist. Eine Server-Anwendung kann MFC-Steuerelementleisten in // jedes Fenster platzieren. BOOL CInPlaceFrame::OnCreateControlBars(CFrameWnd* pWndFrame, CFrameWnd* pWndDoc) { // Entfernen Sie dies, wenn Sie pWndDoc verwenden UNREFERENCED_PARAMETER(pWndDoc); // Dieses Fenster als Owner festlegen, damit Nachrichten an // die richtige Anwendung gesendet werden m_wndToolBar.SetOwner(this); // Symbolleiste im Client-Fenster erzeugen if (!m_wndToolBar.Create(pWndFrame) || !m_wndToolBar.LoadToolBar(IDR_STOCKCTYPE_SRVR_IP)) { TRACE0("Failed to create toolbar\n"); return FALSE; } // ZU ERLEDIGEN: Entfernen, wenn Sie keine QuickInfos oder // variable Symbolleiste wünschen m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC); // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie // nicht wollen, dass die Symbolleiste andockbar ist. m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); pWndFrame->EnableDocking(CBRS_ALIGN_ANY); pWndFrame->DockControlBar(&m_wndToolBar);
417
418
3
COM, OLE und ActiveX
return TRUE; } BOOL CInPlaceFrame::PreCreateWindow(CREATESTRUCT& cs) { // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das // Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren. return COleIPFrameWnd::PreCreateWindow(cs); } ////////////////////////////////////////////////////////////// // CInPlaceFrame Diagnose #ifdef _DEBUG void CInPlaceFrame::AssertValid() const { COleIPFrameWnd::AssertValid(); } void CInPlaceFrame::Dump(CDumpContext& dc) const { COleIPFrameWnd::Dump(dc); } #endif Listing 3.51: Die Rahmenfensterklasse zur direkten Aktivierung
3.5.8
ActiveX-Dokumente
OLE ist eine der ältesten der COM-basierten Technologien. Mit OLE wurde erstmals demonstriert, dass COM als Objektmodell praktikabel einsetzbar ist. Doch die Entwicklung ist nicht bei OLE in seiner ursprünglichen Form stehengeblieben. Eine Weiterentwicklung von OLE wurde zunächst unter dem Namen DocObjects, später dann jedoch als ActiveX-Dokumente vorgestellt. Unterschiede zu OLE-Servern
ActiveX-Dokumente verhalten sich grundsätzlich wie eingebettete OLE-Server, haben aber ein paar Eigenschaften, die sich davon unterscheiden. ActiveX-Dokumente 왘 sind immer direkt aktiviert, das heißt, sie kennen keinen inaktiven Zustand, 왘 verwenden immer die gesamte Fläche des Containers; sie werden also nicht innerhalb eines begrenzten Rechtecks angezeigt, 왘 können sich über mehrere Seiten erstrecken.
Object Linking and Embedding
419
Der Schwerpunkt bei ActiveX-Dokumenten liegt auf dem Dokument als Ganzem und nicht auf der Zusammensetzung verschiedener Dokumententypen. Ein ActiveX-Dokument soll angezeigt und/oder bearbeitet werden können, ohne die gerade verwendete Container-Anwendung verlassen zu müssen. Bestes Beispiel hierfür ist der Microsoft Internet Explorer, der seit Version 3.0 ActiveX-Dokumente anzeigen und bearbeiten kann. Somit ist es beispielsweise möglich, Word- und Excel-Dokumente innerhalb eines Intranets oder auch über das Internet zur Verfügung zu stellen. Sie können dann direkt innerhalb des Browsers angezeigt werden. Ein anderer bekannter Container für ActiveX-Dokumente ist die Office-Sammelmappe. In der Sammelmappe können mehrere ActiveX-Dokumente innerhalb einer Sammelmappendatei gespeichert werden. Die Sammelmappe ist dabei nicht auf die Dokumente der Office-Anwendungen beschränkt.
3.5.9
MFC-Unterstützung für ActiveX-Dokumente
ActiveX-Dokumente werden durch eine Reihe von COM-Schnittstellen definiert, deren Fähigkeiten über das alte OLE-Protokoll hinausgehen. Dazu gehören auf der Seite des Containers die Schnittstelle IOleDocumentSite und auf der Seite des Servers die Schnittstellen IOleDocument und IOleDocumentView. ActiveXDokumente implementieren normalerweise auch alle bereits besprochenen OLE-Schnittstellen, so dass sie auch in der alten Form als OLE-Server einsetzbar sind. Die MFC unterstützen ActiveX-Dokumentserver und ActiveXDokument-Container. Aktiviert wird die Unterstützung für ActiveX-Dokumente, wenn man bei der Erstellung eines OLE-Servers mit dem Anwendungs-Assistenten unter VERBUNDDOKUMENTUNTERSTÜTZUNG die Option »Aktiver Dokumentserver« auswählt. Es wird dann ein OLE-Server erstellt, der auch als ActiveX-Dokumentserver verwendbar ist. Zusätzlich muss im Gegensatz zu normalen OLE-Servern unter DOKUMENT-VORZEICHENFOLGEN unbedingt eine Dateinamenerweiterung für das ActiveX-Dokument angegeben werden. Anderenfalls lässt sich der ActiveX-Dokumentserver nicht erstellen. Zur Unterstützung von ActiveX-Dokumenten gibt es in den MFC vier Klassen: CDocObjectServer, CDocObjectServerItem, COleDocObjectItem und COleDocIPFrameWnd. Abbildung 3.42 zeigt die vier Klassen innerhalb der MFC.
Bei ActiveX-Dokumenten-Containern ersetzt die Klasse COleDocObjectItem die Klasse COleClientItem eines OLE-Containters. Bei ActiveX-Servern implementiert die Klasse CDocObjectServer die für einen ActiveX-Dokumentserver notwendigen zusätzlichen COMSchnittstellen. Die beiden Klassen CDocObjectServerItem und COleDocIPFrameWnd werden bei ActiveX-Dokumentenservern anstelle ihrer Basisklassen verwendet, die lediglich einfache OLE-Server unterstützen. Weitgehende Umbauten sind an einem MFC-Programm, das einen OLE-Server implementiert, nicht notwendig, um es auch als ActiveX-Dokumentserver verwenden zu können. Das folgende Beispiel zeigt die wenigen Unterschiede im Programmcode.
3.5.10 StockChart als ActiveX-Dokument Programm StockChartX
Als Beispiel dient wieder das Programm StockChart. Die Version mit Unterstützung von ActiveX-Dokumenten heißt StockChartX und befindet sich im Verzeichnis KAPITEL3\STOCKCHARTX auf der Begleit-CD.
Object Linking and Embedding
Abbildung 3.43: Einfügen eines StockChartX-Dokuments in die Sammelmappe
Als ActiveX-Dokument lassen sich StockChartX-Dateien in die Office-Sammelmappe einfügen. Wie Abbildung 3.43 zeigt, erscheinen StockChartX-Dokumente automatisch als einzufügende Objekte im entsprechenden Dialog der Sammelmappe.
Abbildung 3.44: StockChartX-Dokument in der Office-Sammelmappe
421
422
3
COM, OLE und ActiveX
Abbildung 3.44 zeigt ein in die Sammelmappe eingefügtes StockChartX-Dokument. Wie man sehen kann, belegt das Dokument die ganze Ausgabefläche des Containers.
Abbildung 3.45: StockChartX-Dokument im Internet Explorer
Als weiterer Container für ActiveX-Dokumente lässt sich der Microsoft Internet Explorer ab der Version 3.0 verwenden. Abbildung 3.45 zeigt ein StockChartX-Dokument im Internet Explorer. Bei der Erstellung des Programms StockChartX mit dem Anwendungs-Assistenten wurde bei VERBUNDDOKUMENTUNTERSTÜTZUNG zusätzlich die Option »Aktiver Dokumentserver« gewählt. Als Dateierweiterung wurde die Endung STKX angegeben.
Object Linking and Embedding
Im Programmcode von StockChartX sind nur sehr wenige Unterschiede zu einem reinen OLE-Server festzustellen. Die Klasse CStockChartXSrvrItem (in SRVRITEM.H) wird erwartungsgemäß von CDocObjectServerItem abgeleitet statt von COleServerItem. Analog dazu wird die Klasse CInPlaceFrame (in IPFRAME.H) von COleDocIPFrameWnd abgeleitet statt von COleIPFrameWnd. In der Dokumentenklasse ist eine Funktion zu den bisher vorhandenen dazugekommen: GetDocObjectServer. Der Anwendungs-Assistent liefert bereits eine Implementierung für diese Funktion. Die Aufgabe der Funktion GetDocObjectServer ist es, ein neues CDocObjectServer-Objekt anzulegen und zurückzugeben. Gibt die Funktion stattdessen den Wert NULL zurück, so bedeutet dies, dass der OLE-Server keine ActiveX-Dokumente unterstützt. Listing 3.52 zeigt die vom Anwendungs-Assistenten generierte Funktion. ////////////////////////////////////////////////////////////// / // CStockChartXDoc Implementierung des ActiveX-DokumentServers CDocObjectServer *CStockChartXDoc::GetDocObjectServer ( LPOLEDOCUMENTSITE pDocSite) { return new CDocObjectServer(this, pDocSite); } Listing 3.52: Die Funktion GetDocObjectServer
Der Aufwand, einen mit den MFC programmierten OLE-Server in einen ActiveX-Dokumentserver umzuwandeln, hält sich – wie man gesehen hat – in erträglichen Grenzen. Dieser ActiveX-Dokumentserver kann natürlich immer noch als ganz normaler OLEServer, beispielsweise in Word oder Excel eingebettet, fungieren, da die MFC auch für ActiveX-Dokumentserver alle für OLE-Server notwendigen Schnittstellen implementieren.
3.5.11 Tipps zur Vorgehensweise 왘 Bei der Programmierung eines OLE-Servers muss die grafische Ausgabe sowohl bei der direkten Aktivierung durch die Ansichtsklasse als auch zur Erzeugung einer Windows-Metadatei in der von COleServerItem abgeleiteten Klasse implementiert werden. Bei komplexeren grafischen Ausgaben bietet es sich an, eine Funktion zu schreiben, die für beide Ausgaben verwendet werden kann. Auf diese Weise wird redundanter
423 Unterschiede in der Implementierung
424
3
COM, OLE und ActiveX
Programmcode vermieden. Allerdings müssen die besonderen Anforderungen an den Abbildungsmodus für das Zeichnen der Metadatei bedacht werden. 왘 Als Abbildungsmodus für die Windows-Metadatei muss normalerweise MM_ANISOTROPIC gewählt werden. Größenangaben erfolgen dabei wie im Abbildungsmodus MM_HIMETRIC. Die eigene Zeichenfunktion sollte gegebenenfalls selbstständig Wertangaben in diesen Abbildungsmodus umrechnen. Dazu kann die von der Klasse CDC bereitgestellte Umrechnungsfunktion LPToHIMETRIC verwendet werden. 왘 OLE-Server können auf die Anforderung zur Angabe ihrer Abmessungen (in den MFC durch die Funktion OnGetExtent in der von COleServerItem abgeleiteten Klasse) eine konstante Größe zurückgeben. Dies entspricht der vom AnwendungsAssistenten vorgegebenen Implementierung. Die Größe solcher OLE-Server lässt sich zumindest im nicht direkt aktivierten Modus problemlos innerhalb des Containers ändern. Die programmtechnisch korrekte Behandlung von Größenänderungen ist recht kniffelig. 왘 Wer sich auf das Abenteuer der korrekten Behandlung von Größenänderungen einlassen möchte, der muss die Funktionen OnSetExtent und OnGetExtent in der von COleServerItem abgeleiteten Klasse korrekt behandeln. Um das Zusammenspiel zwischen Server und Container bei der Größenanpassung zu verstehen, kann es helfen, sowohl einen OLE-Server als auch einen OLE-Container zu implementieren, um das Zusammenspiel zwischen den beiden beobachten zu können. 왘 Aufgrund des geringen zusätzlichen Aufwands lohnt es sich fast immer, aus einem OLE-Server auch einen ActiveX-Dokumentserver zu machen. Dazu ist lediglich beim Erstellen die Option »Aktiver Dokumentserver« auszuwählen. Ein solcher Server kann als OLE-Server und als ActiveX-Dokumentserver fungieren.
3.5.12 Zusammenfassung OLE (genauer gesagt die zweite Version von OLE) und COM sind zusammen entwickelt und eingeführt worden. Dementsprechend war OLE die erste große Implementierung einer Windows-Technologie auf der Basis von COM. In der Praxisrelevanz ist OLE
ActiveX-Steuerelemente
425
mittlerweile von fast allen anderen COM-basierten Technologien überholt worden. Echtes dokumentenzentriertes Arbeiten hat sich bis heute nicht durchgesetzt. Ein Grund dafür mag das Fehlen eigener Dateien für eingebettete Objekte sein. Dadurch ist man automatisch an die Applikation gebunden, die die eingebetteten Daten erstellt hat. Eine Konvertierung der Daten in ein anderes Format ist oft nicht möglich. Microsoft hat diesem Umstand Rechnung getragen: ActiveX-Dokumentserver müssen wieder ihr eigenes Dateiformat mitbringen. Trotzdem lassen sie sich wie normale OLE-Server innerhalb von Container-Anwendungen bearbeiten. Auch wenn OLE relativ selten verwendet wird, so ist es doch der Wegbereiter für COM gewesen. Viele COM-Technologien wie die Automation und der vereinheitlichte Datenaustausch sind in Zusammenhang mit OLE entwickelt worden. Darüber hinaus bilden Teile von OLE die Basis für die im Abschnitt 3.6, »AciveXSteuerelemente«, beschriebenen ActiveX-Steuerelemente. Für einige Anwendungen, wie das Einbetten von Audio- und Videoclips in Dokumente, ist OLE sehr praktisch. Die Unterstützung der MFC für OLE ist sehr weitreichend. Der Anwendungs-Assistent generiert praktisch den gesamten zur Implementierung eines OLE-Servers notwendigen Programmcode. Lediglich bei der grafischen Darstellung des Servers ist zusätzliche Arbeit notwendig, da sowohl der Programmcode für das Zeichnen des direkt aktivierten Servers als auch der Programmcode für das Erzeugen der Windows-Metadatei erstellt werden muss, die als Platzhalter für einen nicht aktivierten OLEServer fungiert. Obwohl hier nur die Programmierung von OLEServern beschrieben worden ist, lassen sich OLE-Container in ähnlich einfacher Weise mit den MFC erstellen. In der Online-Hilfe findet man eine ganze Reihe von Beispielen, die die Erstellung von OLE-Containern beschreiben.
3.6
ActiveX-Steuerelemente
Hinter ActiveX-Steuerelementen (teilweise auch COM-Steuerelemente genannt) steht die Idee des visuellen Programmierens, die Idee Softwarekomponenten mit der Maus zu einem funktionierenden Gesamtsystem »zusammenzustecken«. ActiveX-Steuerelemente sind Softwarekomponenten auf einem relativ hohen
Visuelles Programmieren
426
3
COM, OLE und ActiveX
Abstraktionsniveau, die als Bausteine bevorzugt in visuell orientierten Programmierumgebungen wie Visual Basic oder Delphi verwendet werden. VBX-Steuerelemente
Die erste kommerziell erfolgreiche Architektur für visuell verwendbare Softwarekomponenten waren die Visual Basic-Steuerelemente (VBX). Sie wurden zusammen mit einer der ersten 16-BitVersionen von Visual Basic eingeführt. Die Unterstützung der VBX-Bausteine war von Microsoft nicht von langer Hand vorbereitet worden, denn der Erfolg kam einigermaßen überraschend. Ohne es groß geplant zu haben, hatte Microsoft die Grundlage für recht einfach zu verwendende Softwarebausteine geschaffen. Die Anforderungen an solche Softwarekomponenten haben die VBXSteuerelemente jedenfalls recht gut erfüllt. Es entwickelte sich schnell ein Markt für Drittanbieter, die VBX-Komponenten für eine breite Palette von Aufgaben entwickelten. Diese Komponenten ließen sich innerhalb von Visual Basic sehr einfach zu komplexen Systemen zusammenstellen. In der Folge wurden auch andere Entwicklungsumgebungen (beispielsweise die 16-Bit-Version von Visual C++) mit der Möglichkeit ausgestattet, VBX-Komponenten verwenden zu können. Da die VBX-Architektur ursprünglich nur als ein weiteres »Feature« von Visual Basic entworfen worden war und nicht als die Basis eines stark wachsenden Marksegments für Softwarebausteine, enthielt sie einige Unzulänglichkeiten. Die Implementierung war nicht ideal, außerdem war die Architektur stark an das 16-Bit-Programmiermodell von Windows 3.x angelehnt. Eine Portierung auf die neuen, 32 Bit breiten Versionen von Windows erschien schwierig und nicht sinnvoll.
OCX-Steuerelemente
Als sich die Umstellung der Programmierwerkzeuge von 16 auf 32 Bit abzeichnete, beschloss Microsoft, die VBX-Architektur nicht in die Welt der 32 Bit breiten Programme hinüberzuretten. VBX sollte durch eine neue, solidere Architektur ersetzt werden, die auf einem neuen technischen Konzept basieren sollte. Dabei sollte das Prinzip der einfachen Verwendbarkeit in Programmierumgebungen wie Visual Basic beibehalten, die technische Grundlage aber neu und besser definiert werden. Als Basis für die neuen Steuerelemente wurde COM gewählt. Darüber hinaus wurde eine ganze Reihe von bereits existierenden COM-Technologien, wie die Automation und die direkte Aktivierung, bei dieser neuen Architektur
ActiveX-Steuerelemente
427
wieder verwendet. Zunächst wurden die neuen Steuerelemente als OLE-Steuerelemente bezeichnet, was dann in Anlehnung an die alten VBX-Komponenten als OCX abgekürzt wurde. Da das Internet für die Firmenpolitik immer wichtiger wurde, änderte Microsoft die Spezifikation der OCX-Steuerelemente so, dass diese auch innerhalb eines Internetbrowsers verwendbar wurden. Dabei wurden einige das Laufzeitverhalten optimierende Veränderungen und Ergänzungen an der OCX-Spezifikation vorgenommen. Die Steuerelemente erhielten ebenfalls einen neuen Namen: ActiveX-Steuerelemente.
3.6.1
ActiveX-Steuerelemente
Die Anatomie von ActiveX-Steuerelementen
Da ActiveX-Steuerelemente das visuelle Programmieren ermöglichen sollen, also das Zusammenstellen von Programmen aus Komponenten mit der Maus, ist es wichtig zu wissen, wie sich ActiveX-Steuerelemente dem Programmierer darstellen. Als visuelle Komponenten müssen sich ActiveX-Steuerelemente natürlich durch eine grafische Darstellung repräsentieren können. Diese Darstellung kann üblicherweise in zwei Zuständen erfolgen: im Entwurfsmodus, in dem der Programmierer das Steuerelement in sein Programm einfügt, und im Ausführungsmodus, in dem das Programm ausgeführt wird. Es sei allerdings angemerkt, dass nicht alle Programmierumgebungen zwischen Entwurfs- und Ausführungsmodus unterscheiden. Im Entwurfsmodus ist das Steuerelement inaktiv. Wenn man es in diesem Zustand bearbeitet, dann wird normalerweise ein Rahmen irgendeiner Form um das Steuerelement gezeichnet. Abbildung 3.46 zeigt ein ActiveXSteuerelement im Entwurfsmodus von Visual Basic. Im Ausführungsmodus fehlt der Rahmen dann natürlich.
Abbildung 3.46: ActiveX-Steuerelement im Entwurfsmodus von Visual Basic
Darstellungsmodi
428
3
COM, OLE und ActiveX
Eigenschaften und Methoden
Um auf die Funktionalität des Steuerelements zugreifen zu können, stellt dieses Eigenschaften und Methoden zur Verfügung. Diese Eigenschaften und Methoden funktionieren analog zur bereits beschriebenen Automation. In der Tat werden Eigenschaften und Methoden von Steuerelementen durch Automation implementiert! Bei den Eigenschaften wird allerdings zwischen verschiedenen Formen von Eigenschaften differenziert. So gliedern sich die von ActiveX-Steuerelementen unterstützten Eigenschaften in Standardumgebungseigenschaften, erweiterte Eigenschaften und Steuerelementeigenschaften. Standardeigenschaften sind so allgemein, dass sie in vielen ActiveX-Steuerelementen verwendet werden können (aber nicht müssen!). Sie unterscheiden sich von anderen Eigenschaften eines ActiveX-Steuerelements vor allen Dingen durch Implementierungsdetails. Die erweiterten Eigenschaften werden in Wirklichkeit gar nicht vom ActiveX-Steuerelement selbst implementiert, sondern von dem Container, in den es eingebettet ist. Zu den erweiterten Eigenschaften gehören beispielsweise Größe und Position des Steuerelements. Die Steuerelementeigenschaften sind schließlich die vom Steuerelement selbst frei definierbaren Eigenschaften. Alle Eigenschaften eines ActiveX-Steuerelements sind persistent. Der Container des Steuerelements stellt diesem ein Speichermedium zur Verfügung, auf das es die Werte seiner Eigenschaften speichern kann. Auf diese Weise lassen sich Eigenschaftswerte durch die Entwicklungsumgebung vorbelegen und müssen nicht zur Laufzeit des Steuerelements explizit gesetzt werden.
Ereignisse
Anders als bei der Automation ist es bei ActiveX-Steuerelementen notwendig, dass das Steuerelement den Container benachrichtigen kann, in den es eingebettet ist. Bei normalen Steuerelementen werden solche Benachrichtigungen durch Windows-Nachrichten realisiert. Bei ActiveX-Steuerelementen verwendet man statt dessen einen Methodenaufruf in umgekehrter Richtung. Das ActiveX-Steuerelement ruft eine Methode des Containers auf, um ihn über das Eintreffen eines Ereignisses zu informieren. Diese Aufrufe in umgekehrter Richtung werden als Ereignisse bezeichnet. In der Tat werden Ereignisse durch Aufrufe von Methoden einer IDispatch-Schnittstelle des Containers implementiert. Ein ActiveX-Steuerelement setzt sich aus folgenden (von außen zugänglichen) Teilen zusammen: 왘 Eigenschaften 왘 Methoden
ActiveX-Steuerelemente
429
왘 Ereignisse 왘 visuelle Darstellung Alle diese Aspekte eines ActiveX-Steuerelements müssen in irgendeiner Form auf die Entwicklungsumgebung abgebildet werden, in der ActiveX-Steuerelemente verwendet werden sollen. Besonders gelungen ist die Einbindung von ActiveX-Steuerelementen in Visual Basic und C#. ActiveX-Steuerelemente können hier genauso einfach wie die anderen Komponenten der Entwicklungsumgebung verwendet werden.
Abbildung 3.47: Knob-Steuerelement in C#
3.6.2
Verwendung von ActiveX-Steuerelementen mit den MFC
Die Entwicklungsumgebung von Visual C++ und die MFC bieten eine weitreichende Unterstützung für ActiveX-Steuerelemente. In der Entwicklungsumgebung von Visual C++ lassen sich ActiveXSteuerelemente ähnlich wie normale Steuerelemente verwenden. Damit ein ActiveX-Steuerelement verwendet werden kann, muss es dem Projekt zunächst bekannt gemacht werden. Dazu wählt man im Kontextmenü der Toolbox den Eintrag TOOLBOX ANPASSEN aus. Es öffnet sich das in Abbildung 3.48 gezeigte Dialogfeld. Hier
Steuerelemente hinzufügen
430
3
COM, OLE und ActiveX
wählt man die Registerkarte COM-STEUERELEMENTE aus, die alle in der Registrierungsdatenbank eingetragenen Steuerelemente aufführt. Hier kann man nun ActiveX-Steuerelemente zur Toolbox hinzufügen.
Abbildung 3.48: Auswahl eines ActiveX-Steuerelements für die Toolbox Steuerelementleiste des Ressourceneditors
Nachdem man das Steuerelement ausgewählt und den Dialog bestätigt hat, findet sich das ActiveX-Steuerelement in der Toolbox wieder (siehe Abbildung 3.49). Das eingefügte ActiveX-Steuerelement lässt sich dann wie jedes andere Steuerelement innerhalb von Dialogfeldern verwenden.
Abbildung 3.49: Das ActiveX-Steuerelement wird in die Toolbox aufgenommen.
ActiveX-Steuerelemente
431
Damit ein ActiveX-Steuerelement innerhalb eines MFC-Programms verwendet werden kann, muss das Programm darauf vorbereitet werden. Normalerweise wählt man beim Aufruf des Anwendungs-Assistenten unter ERWEITERTE FEATURES das Kontrollkästchen ACTIVEX-STEUERELEMENTE aus. Ein Visual C++-Projekt lässt sich allerdings auch nachträglich leicht zur Aufnahme von ActiveX-Steuerelementen einrichten. Dazu muss in der Funktion InitInstance der Applikationsklasse der Aufruf der Funktion AfxEnableControlContainer eingefügt werden. Vorher muss dazu die Header-Datei AFXDISP.H eingebunden worden sein.
3.6.3
AfxEnableControlContainer
Ein ActiveX-Steuerelement im Programm StockChart
Als Beispiel soll ein ActiveX-Steuerelement in einen Dialog des Programms StockChart eingebettet werden. Dazu bietet sich der Dialog zur Einstellung der Eigenschaften des Aktiencharts an. Der in diesem Dialog enthaltene Schieberegler zur Einstellung der Durchschnittslinie soll durch einen Drehregler ersetzt werden. Der Drehregler liegt als ActiveX-Steuerelement vor. Im Verzeichnis KAPITEL3\KNOB auf der Begleit-CD befindet sich ein MFC-Projekt für einen ActiveX-Drehregler. Das Projekt muss auf die Festplatte kopiert und übersetzt werden. Damit wird das DrehreglerSteuerelement automatisch registriert. Danach lässt sich der Drehregler – wie bereits beschrieben – durch Aufruf des Eintrags TOOLBOX ANPASSEN des Kontextmenüs in die Toolbox einfügen. Das Steuerelement Knob implementiert die in Tabelle 3.10 gezeigten Eigenschaften, Methoden und Ereignisse. Name
Typ
Aufgabe
BackColor
Eigenschaft
Gibt die Hintergrundfarbe des Steuerelements an.
ForeColor
Eigenschaft
Gibt die Farbe des Drehknopfs an.
Value
Eigenschaft
Repräsentiert die Position des Reglers.
MaxValue
Eigenschaft
Gibt das obere Ende des Regelbereichs an.
AboutBox
Methode
Ruft den Info-Dialog des Steuerelements auf.
OnValueChanged
Ereignis
Wird aufgerufen, wenn der Regler »losgelassen« wird, um die neue Einstellung anzuzeigen.
Tabelle 3.10: Eigenschaften, Methoden und Ereignisse des Steuerelements Knob
Das Steuerelement Knob
432
3
COM, OLE und ActiveX
Der Regelbereich des Reglers startet immer mit dem Wert 0 und endet bei dem durch MaxValue angegebenen Wert. Nachdem das ActiveX-Steuerelement in die Toolbox eingefügt worden ist, lässt sich der Drehknopf mit dem Ressourceneditor der Entwicklungsumgebung einfügen. Der ursprünglich vorhandene Schieberegler ist gelöscht und gegen den Drehregler ausgetauscht worden. Abbildung 3.50 zeigt den so veränderten Dialog.
Abbildung 3.50: Drehknopfsteuerelement im Dialog des Programms StockChart Eigenschaftsdialog des Steuerelements
Die Eigenschaft MaxValue, die den Regelbereich des Drehreglers bestimmt, kann wie bei jedem Steuerelement in der Eigenschaftsansicht der Entwicklungsumgebung eingestellt werden. Daneben besitzt das Steuerelement einen eigenen Eigenschaftsdialog, in dem sich die Farben des Steuerlelements und der Wert für MaxValue festlegen lassen. Abbildung 3.51 zeigt diesen Dialog, der sich über die kleine Schaltfläche EIGENSCHAFTSSEITEN der Eigenschaftsansicht aufrufen lässt. Für das Programm StockChart wird für MaxValue der Wert 29 eingegeben, um wieder den alten Regelbereich zu erhalten (0 bis 29 statt 1 bis 30, da der Drehregler mit 0 beginnend zählt). Im Programmcode des Beispielprogramms StockChart sind nur einige kleinere Änderungen notwendig, um das neue Steuerelement korrekt anzusprechen. Der Programmcode zur Ansprache des Schiebereglers muss natürlich entfernt werden. Das betrifft die Datenaustauschvariable m_nAverageCnt sowie den dazugehörenden Programmcode in den Funktionen DoDataExchange und OnInitDialog.
ActiveX-Steuerelemente
433
Abbildung 3.51: Eigenschaftsdialog des ActiveX-Steuerelements Knob
Abbildung 3.52: ActiveX-Steuerelementeigenschaft als Kategorie im Assistenten für Member-Variablen
Das Steuerelement Knob lässt den Datenaustausch über DDX zu. Im Assistenten zum Hinzufügen von Member-Variablen wird ganz normal eine DDX-Variable hinzugefügt. Zusätzlich – und im Unterschied zu normalen Steuerelementen – werden bei ActiveXSteuerelementen unter »Kategorie« alle Eigenschaften des Steuerelements aufgeführt, die sich für einen Datenaustausch über DDX eignen. Für das Steuerelement Knob lässt sich daher der Wert der Eigenschaft Value durch DDX austauschen. Abbildung 3.52 zeigt
DDX mit dem Steuerelement
434
3
COM, OLE und ActiveX
den Dialog, mit dem die DDX-Variable angelegt worden ist. Dass es sich um die Eigenschaft eines ActiveX-Steuerelements handelt, wird durch den im Dialog angegebenen Steuerelementtypen »OCX« deutlich. Die DDX-Variable für den Drehregler wird, genau wie die Datenaustauschvariable für den vorher vorhandenen Schieberegler, m_nAverageCnt genannt. Ereignisse im KlassenAssistenten
Ereignisse des ActiveX-Steuerelements werden nicht im Beispielprogramm StockChart verwendet. Es soll trotzdem kurz beschrieben werden, wie Ereignisse von ActiveX-Steuerelementen innerhalb der MFC behandelt werden. Ereignisse ersetzen die bei normalen Steuerelementen verwendeten Windows-Nachrichten. Daher werden sie in Visual Studio .NET auch ganz ähnlich verwaltet. Ereignisse eines ActiveX-Steuerelements werden in der Eigenschaftsansicht »Steuerelementereignisse« aufgeführt. Abbildung 3.53 zeigt das einzige Ereignis des ActiveX-Steuerelements Knob – OnValueChanged – in der Eigenschaftsansicht.
Abbildung 3.53: Ereignis des ActiveX-Steuerelements Knob Ereigniszuordnungstabellen
Verwaltet werden Ereignisse innerhalb der MFC durch Ereigniszuordnungstabellen, die durch Makros aufgebaut werden. Deklariert wird eine solche Ereigniszuordnungstabelle durch das Makro DECLARE_EVENTSINK_MAP, implementiert durch die Makros BEGIN_EVENTSINK_MAP, END_EVENTSINK_MAP und ON_ EVENT. Genau wie Nachrichtenzuordnungstabellen werden diese Tabellen durch die Entwicklungsumgebung verwaltet.
ActiveX-Steuerelemente
Wie man sieht, lassen sich ActiveX-Steuerelemente sehr einfach in MFC-Programmen verwenden. ActiveX-Steuerelemente werden sowohl innerhalb der MFC als auch in der Entwicklungsumgebung weitgehend wie normale Steuerelemente behandelt. ActiveX-Steuerelemente lassen sich natürlich nicht nur innerhalb von Dialogfeldern verwenden, sondern auch in beliebigen Fenstern. Auch bei dieser Verwendungsform ähneln ActiveX-Steuerelemente den normalen Steuerelementen. Genau wie diese müssen sie durch einen Aufruf der Funktion Create erzeugt werden, und sie verhalten sich weitgehend wie normale Fenster, da sie wie normale Steuerelemente von der Klasse CWnd abgeleitet werden. Allerdings muss die Ereignisbehandlung in diesem Fall von Hand durch das manuelle Bearbeiten von Ereigniszuordnungstabellen implementiert werden. Die Entwicklungsumgebung bietet bei ActiveX-Steuerelementen, die außerhalb von Dialogfeldern verwendet werden, dafür keine Hilfestellung an. Um aus Visual Studio .NET eine Klasse zu generieren, die das ActiveX-Steuerelement kapselt und mittels der sich das ActiveX-Steuerelement instanziieren lässt, ruft man unter PROJEKT | KLASSE HINZUFÜGEN... den Assistenten mit der Bezeichnung »MFC-Klasse von ActiveX-Steuerelement« auf. Der in Abbildung 3.54 gezeigte Assistent erzeugt dann eine MFC-Klasse, mittels derer sich das ActiveX-Steuerelement ähnlich wie ein normales Steuerelement verwenden lässt.
Abbildung 3.54: Assistent zur Erzeugung von ActiveX-Klassen
435
436
3
3.6.4
COM, OLE und ActiveX
Technische Grundlagen von ActiveX-Steuerelementen
Es wurde bereits gesagt, dass ein ActiveX-Steuerelement sich durch die vier Aspekte Eigenschaften, Methoden, Ereignisse und visuelle Darstellung (siehe Abschnitt 3.6.1, »Die Anatomie von ActiveXSteuerelementen«) dem Komponentenprogrammierer präsentiert. Es soll nun kurz besprochen werden, wie diese Aspekte implementiert werden. Eigenschaften und Methoden werden durch die Automation realisiert. Da die Automation bereits ausführlich besprochen worden ist, muss darauf nicht noch einmal eingegangen werden. Bei Eigenschaften ist allerdings anzumerken, dass diese unterschiedlich implementiert werden. Für Standardumgebungseigenschaften gibt es bereits eine Implementierung in den MFC; erweiterte Eigenschaften werden in Wirklichkeit vom Container implementiert und nur die Steuerelementeigenschaften werden wie normale Automationseigenschaften implementiert. Verbindungspunkte
Ereignisse werden durch eine interessante Erweiterung der COMTechnologie implementiert, durch Verbindungspunkte. Verbindungspunkte sind eine von ActiveX-Steuerelementen unabhängige Erweiterung von COM. Durch Verbindungspunkte wird eine Schwachstelle in COM beseitigt. Normalerweise ruft bei COM ein COM-Client die Methoden eines COM-Servers auf. Die umgekehrte Richtung ist jedoch nicht vorgesehen. Sie wird mit der Einführung von Verbindungspunkten ermöglicht. Verbindungspunkte beschreiben die aus einem COM-Server heraus gerichteten Schnittstellen im Gegensatz zu den normalen, nach innen gerichteten Schnittstellen des COM-Servers. Im Zusammenhang mit Verbindungspunkten spricht man auch von der Quelle und dem Empfänger von Schnittstellenaufrufen. Ein Verbindungspunkt ist eine nach außen gerichtete Schnittstelle der Quelle. Der Empfänger kann über den Verbindungspunkt eine Verbindung zu der Quelle aufbauen und Schnittstellenaufrufe von der Quelle empfangen. Implementiert werden Verbindungspunkte durch die COM-Schnittstellen IConnectionPoint und IConnectionPointContainer; die entsprechende Unterstützung innerhalb der MFC findet durch die Klassen CConnectionPoint und CCmdTarget statt. In der IDL werden ausgehende Schnittstellen durch das Attribut source gekennzeichnet. Bei ActiveX-Steuerelementen wird als ausgehende Schnittstelle eine Automationsschnittstelle von Typ IDispatch verwendet. Ac-
ActiveX-Steuerelemente
437
tiveX-Steuerelemente senden folglich Ereignisse über eine ausgehende Automationsschnittstelle an ihren Container. Ereignisse sind also Aufrufe von Automationsmethoden in umgekehrter Richtung. Die visuelle Darstellung von ActiveX-Steuerelementen wird durch die bereits von OLE bekannte direkte Aktivierung realisiert. Hier gibt es eine ganze Reihe von Ansätzen und Optimierungen, die während der fortschreitenden Entwicklung von ActiveX-Steuerelementen eingeführt worden sind. Die ursprüngliche Spezifikation für OLE-Steuerelemente sah vor, dass diese direkt aktiviert werden, sobald sie sichtbar werden. Neuere Spezifikationen für OCX und ActiveX-Steuerelemente erlauben nun Steuerelemente, die nicht aktiviert werden müssen, wenn sie sichtbar werden (inaktive Steuerelemente), und Steuerelemente ohne eigenes Fenster. Beide Optimierungen können deutliche Geschwindigkeitsvorteile erbringen. Für eine detaillierte Beschreibung dieser Techniken sei auf das Buch von Adam Denning verwiesen, das im Anhang aufgeführt wird.
3.6.5
ActiveX-Steuerelemente mit den MFC erstellen
Ähnlich wie DLLs werden auch ActiveX-Steuerelemente durch einen eigenen Assistenten erzeugt. Der ActiveX-SteuerelementAssistent der MFC erstellt Steuerelemente in drei Schritten. Abbildung 3.55 zeigt die Anwendungseinstellungen.
Abbildung 3.55: Assistent für ActiveX-Steuerelemente, Anwendungseinstellungen
Assistent für ActiveX-Steuerelemente
438
3 Anwendungseinstellungen
COM, OLE und ActiveX
ActiveX-Steuerelemente eröffnen die Möglichkeit zur Lizenzierung. Das bedeutet, dass ein solches Steuerelement nur verwendet werden kann, wenn der Benutzer eine gültige Lizenz für das Steuerelement besitzt. Auf diese Weise sollen Raubkopien verhindert werden. Technisch gesehen basiert die Lizenzierung auf einer erweiterten Spezifikation von Klassenfabriken. Die Schnittstelle IClassFactory2 erlaubt eine Instanziierung des Steuerelements nur, wenn gültige Lizenzierungsinformationen vorliegen. Die MFC bieten Unterstützung bei der Implementierung von lizenzierten Steuerelementen. Möchte man ein lizenziertes Steuerelement erstellen, so kann man die Option LAUFZEITLIZENZ auswählen. Die Option HILFEDATEIEN ERSTELLEN legt ein Grundgerüst für die Online-Hilfe des Steuerelements an.
Abbildung 3.56: Assistent für ActiveX-Steuerelemente, Steuerelementnamen Steuerelementnamen
Auf der Karteikarte STEUERELEMENTNAMEN des Assistenten werden die Namen des zu erzeugenden Steuerelements bearbeitet. Hier können Dateinamen, Klassennamen und andere interne Bezeichner verändert werden. Normalerweise kann man die automatisch erzeugten Namen einfach übernehmen.
Steuerelementeinstellungen
Auf der Karteikarte STEUERELEMENTEINSTELLUNGEN des Assistenten lassen sich Merkmale des Steuerelements einstellen, beispielsweise, ob es eine Aboutbox bekommen soll oder ob man ein unsichtbares Steuerelement erstellen möchte. Hier kann die automatische Aktivierung abgeschaltet werden. Das ist wichtig für Steuerelemente, die nicht direkt aktiviert werden sollen, wenn sie
ActiveX-Steuerelemente
sichtbar werden. Wer zunächst nur ein einfaches Steuerelement entwickeln möchte, kann die vorgegebenen Einstellungen unverändert übernehmen.
Abbildung 3.57: Assistent für ActiveX-Steuerelemente, Steuerelementeinstellungen
Für das Steuerelement Knob wurden alle Voreinstellungen des Assistenten für ActiveX-Steuerelemente unverändert übernommen. MFC-typisch wird das Steuerelement selbst durch ein globales Applikationsobjekt repräsentiert. Bei ActiveX-Steuerelementen wird dieses nicht direkt von der Klasse CWinApp, sondern von der davon abgeleiteten Klasse COleControlModule abgeleitet. Abbildung 3.58 zeigt die MFC-Klassen für ActiveX-Steuerelemente. CObject CCmdTarget CWinThread CWinApp COleControlModule CWnd CDialog COlePropertyPage COleControl Abbildung 3.58: MFC-Klassen für ActiveX-Steuerelemente
439
440
3
COM, OLE und ActiveX
Für das Projekt Knob hat der ActiveX-Steuerelement-Assistent die von COleControlModule abgeleitete Klasse CKnobApp angelegt. Die in Listing 3.53 gezeigte Implementierung dieser Klasse befindet sich in der Datei KNOB.CPP. CKnobApp NEAR theApp; const GUID CDECL BASED_CODE _tlid = { 0x21d56973, 0x2604, 0x11d2, { 0xb5, 0x9c, 0, 0x60, 0x97, 0xa8, 0xf6, 0x9a } }; const WORD _wVerMajor = 1; const WORD _wVerMinor = 0;
if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE)) return ResultFromScode(SELFREG_E_CLASS); return NOERROR; }
////////////////////////////////////////////////////////////// / // DllUnregisterServer - Entfernt Einträge aus der // Systemregistrierung STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE)) return ResultFromScode(SELFREG_E_CLASS); return NOERROR; } Listing 3.53: Applikationsklasse des ActiveX-Steuerelements
Wie in Listing 3.54 zu sehen ist, besitzt die Applikationsklasse des ActiveX-Steuerelements die schon von MFC-Anwendungen bekannten Funktionen InitInstance und ExitInstance, mit denen Initialisierungs- bzw. Aufräumarbeiten, die das gesamte Steuerelement betreffen, durchgeführt werden. In der durch den Assistenten vorgegebenen Implementierung werden hier lediglich die gleichnamigen Funktionen der Basisklasse aufgerufen. Weiterhin befinden sich in der Datei KNOB.CPP die Implementierungen der Funktionen DllRegisterServer und DllUnregisterServer, mit denen sich das Steuerelement selbsttätig in der Windows-Registrierungsdatenbank eintragen und auch wieder löschen kann.
Applikationsklasse
Entfernt verwandt mit den Ansichtsklassen einer MFC-Anwendung, die dem Dokument-Ansicht-Modell folgt, ist die Klasse COleControl. Mit den MFC implementierte ActiveX-Steuerelemente folgen allerdings nicht dem Dokument-Ansicht-Modell und besitzen daher keine Dokumentenklasse. In der von COleControl abgeleiteten Klasse werden daher normalerweise sowohl
COleControl
442
3
COM, OLE und ActiveX
die Darstellung – COleControl ist von CWnd abgeleitet – als auch die Funktionalität implementiert. Die Klasse COleControl implementiert darüber hinaus alle COM-Schnittstellen, die notwendig sind, um das Steuerelement in einen ActiveX-Steuerelement-Container, wie beispielsweise Visual Basic, einzufügen. Im Projekt Knob heißt die von COleControl abgeleitete Klasse CKnobCtrl. Listing 3.54 zeigt zunächst die Header-Datei der Klasse CKnobCtrl. // KnobCtl.h: Deklaration der CKnobCtrl-ActiveX// Steuerelementeklasse. //////////////////////////////////////////////////////////// // CKnobCtrl: Siehe KnobCtl.cpp für Implementierung. class CKnobCtrl : public COleControl { DECLARE_DYNCREATE(CKnobCtrl) // Konstruktor public: CKnobCtrl(); // Überladungen public: virtual void OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid); virtual void DoPropExchange(CPropExchange* pPX); virtual void OnResetState(); // Implementierung protected: ~CKnobCtrl(); DECLARE_OLECREATE_EX(CKnobCtrl) // Klassenerzeugung und GUID DECLARE_OLETYPELIB(CKnobCtrl) // GetTypeInfo DECLARE_PROPPAGEIDS(CKnobCtrl) // Eigenschaftenseiten-IDs DECLARE_OLECTLTYPE(CKnobCtrl) // Typname und versch. // Status // Nachrichtenzuordnungstabellen afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() // Dispatch-Tabellen afx_msg long GetMaxValue();
Der Assistent für ActiveX-Steuerelemente hat, wie in Listing 3.54 zu sehen ist, Überladungen für drei Funktionen eingefügt. Die Funktion OnDraw wird aufgerufen, wenn das Steuerelement sich zeichnen soll. Die Funktion ähnelt der gleichnamigen Funktion der Ansichtsklassen. Der Funktion wird ein gültiger Gerätekontext übergeben, der zum Zeichnen verwendet werden muss. Wichtig ist der Parameter rcBounds. Dieser gibt den Ausgabebereich an, in den das Steuerelement zeichnen darf. Die Funktion DoPropExchange führt den Datenaustausch mit dem Eigenschaftsdialog des Steuerelements durch. Der Eigenschaftsdialog ist der bereits in
443
444
3
COM, OLE und ActiveX
Abbildung 3.51 aus Abschnitt 3.6.3, »Ein ActiveX-Steuerelement im Programm StockChart«, gezeigte Dialog, mit dem sich die Werte für die Eigenschaften des Steuerelements setzen lassen. Es ist die Aufgabe des ActiveX-Steuerelements, diesen Dialog selbst zu implementieren. Die dritte Funktion, OnResetState, wird aufgerufen, wenn die Eigenschaften des Steuerelements auf definierte Werte zurückgesetzt werden sollen. Nach der Deklaration der eben beschriebenen Funktionen sieht man im Listing einige Makros, die unter anderem zur Deklaration der Klassenfabrik und zur Unterstützung von Eigenschaftsdialogseiten dienen. Im Anschluss daran wird die Nachrichtenzuordnungstabelle aufgebaut. Für das Knob-Steuerelement sind Behandlungsfunktionen für die Nachrichten WM_LBUTTONDOWN (linke Maustaste gedrückt), WM_LBUTTONUP (linke Maustaste losgelassen) und WM_MOUSEMOVE (Maus bewegt) eingefügt worden. Eigenschaften
Nach der Nachrichtenzuordnungstabelle wird die Verteilertabelle für Automationseigenschaften angelegt. Diese sind, wie es bereits in Abschnitt 3.3.3, »Implementierung eines Automationsservers mit den MFC«, gezeigt wurde, mit dem Klassen-Assistenten angelegt worden. Das ActiveX-Steuerelement Knob besitzt die in Tabelle 3.10 aus Abschnitt 3.6.3, »Ein ActiveX-Steuerelement im Programm StockChart«, aufgeführten Eigenschaften Value und MaxValue. Die Eigenschaften werden durch die Funktionen SetValue, GetValue, SetMaxValue und GetMaxValue gesetzt und gelesen. Zusätzlich zu diesen beiden Steuerelementeigenschaften besitzt das Steuerelement Knob zwei weitere Eigenschaften: BackColor und ForeColor. Diese beiden Eigenschaften sind Standardumgebungseigenschaften, für die die MFC, wie bereits angedeutet, eine vorgegebene Implementierung besitzen. Weil es die Implementierung für diese Eigenschaften bereits an anderer Stelle innerhalb der MFC gibt, tauchen die Standardumgebungseigenschaften nicht in der Verteilertabelle auf. Angelegt werden sie wie die normalen Steuerelementeigenschaften. Standardumgebungseigenschaften lassen sich beim Hinzufügen der Eigenschaft aus dem Kombinationsfeld »Eigenschaftsname« herunterklappen. Bei der Auswahl einer solchen Eigenschaft wird unter »Implementationstyp« automatisch das Optionsfeld VORDEFINIERT ausgewählt. Dies ist in Abbildung 3.59 zu sehen.
Doch nun weiter mit der Besprechung des Listings! Nach der Verteilertabelle für die Automationseigenschaften kommt die Ereignistabelle des Steuerelements. In der Ereignistabelle werden die Ereignisse aufgeführt, die das Steuerelement liefert. Genau wie andere Tabellen (für Nachrichten, Eigenschaften usw.), wird auch diese mit der Entwicklungsumgebung verwaltet. Ereignisse werden aus der Klassenansicht hinzugefügt. Hat man die das Steuerelement implementierende Klasse selektiert, so lässt sich ein Ereignis durch das Kontextmenü HINZUFÜGEN | EREIGNIS HINZUFÜGEN in die Klasse aufnehmen. Für das Steuerelement Knob ist mit dem Klassen-Assistenten das Ereignis OnValueChanged hinzugefügt worden. Dies soll ausgelöst werden, wenn der Benutzer den Regler loslässt und damit einen neuen Wert eingestellt hat. Abbildung 3.60 zeigt den Assistenten zum Hinzufügen von Ereignissen. Die Implementierung für das neu angelegte Ereignis, die gleich in der Header-Datei vorgenommen wird, ruft die Funktion COleControl::FireEvent auf, um das Ereignis auszulösen. FireEvent ist eine Funktion, die Methoden der ausgehenden Automationsschnittstelle eines ActiveX-Steuerelements aufrufen kann. Die weiteren in KNOBCTL.H deklarierten Funktionen und Variablen sind nicht für ActiveX-Steuerelemente spezifisch, sondern dienen zur Implementierung des Drehreglers.
///////////////////////////////////////////////////////////// // CKnobCtrl::CKnobCtrlFactory::UpdateRegistry // Fügt Einträge der Systemregistrierung für CKnobCtrl hinzu // oder entfernt diese BOOL CKnobCtrl::CKnobCtrlFactory::UpdateRegistry(BOOL bRegister) { // ZU ERLEDIGEN: Prüfen Sie, ob Ihr Steuerelement den Thread// Regeln nach dem "Apartment"-Modell entspricht. // Weitere Informationen finden Sie unter MFC TechNote 64. // Falls Ihr Steuerelement nicht den Regeln nach dem // Apartment-Modell entspricht, so müssen Sie den // nachfolgenden Code ändern, indem Sie den 6. Parameter von // afxRegApartmentThreading auf 0 ändern. if (bRegister) return AfxOleRegisterControlClass( AfxGetInstanceHandle(), m_clsid, m_lpszProgID, IDS_KNOB, IDB_KNOB, afxRegApartmentThreading, _dwKnobOleMisc, _tlid, _wVerMajor, _wVerMinor);
ActiveX-Steuerelemente else return AfxOleUnregisterClass(m_clsid, m_lpszProgID); } ////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////// static const double pi = 3.1415; static const int nKnobSize = 50;
////////////////////////////////////////////////////////////// / // CKnobCtrl::OnResetState - Setzt das Steuerelement in den // Standardzustand zurück void CKnobCtrl::OnResetState() { COleControl::OnResetState(); zurück,
// Setzt die Standards // die in DoPropExchange // gefunden wurden
m_nValue = 0; m_nMaxValue = 100; }
///////////////////////////////////////////////////////////// // CKnobCtrl::AboutBox - Ein Dialogfeld "Info" für den Benutzer // anzeigen void CKnobCtrl::AboutBox() { CDialog dlgAbout(IDD_ABOUTBOX_KNOB); dlgAbout.DoModal(); }
void CKnobCtrl::SetValue(long nNewValue) { m_nValue = nNewValue; Invalidate (); SetModifiedFlag(); } Listing 3.55: Die Implementierungsdatei KNOBCTL.CPP Implementierung der Eigenschaftsdialogseiten
Listing 3.55 zeigt die Implementierungsdatei der Klasse CKnobCtrl. Zunächst werden in dieser Datei die Nachrichtenzuordnungstabelle, die Verteilertabelle für die Automation und die Ereignistabelle definiert. Alle diese Tabellen werden durch die Entwicklungsumgebung verwaltet und müssen daher vom Programmierer normalerweise nicht bearbeitet werden. Dies gilt nicht für die sich anschließende Liste der Eigenschaftsseiten des Eigenschaftsdialogs. Diese Liste muss vom Programmierer von Hand verwaltet werden. Eingeleitet wird die Liste durch das Makro BEGIN_PROPPAGEIDS. Dieses Makro bekommt als zweiten Parameter die Anzahl der
ActiveX-Steuerelemente
Eigenschaftsseiten des Eigenschaftsdialogfelds. Es ist wichtig, dass diese Zahl angepasst wird, wenn Eigenschaftsseiten hinzugefügt oder entfernt werden. Abgeschlossen wird die Liste der Eigenschaftsseiten durch das Makro END_PROPPAGEIDS. Die einzelnen Einträge werden durch das Makro PROPPAGEID definiert. Diesem Makro wird als Parameter die CLSID der Eigenschaftsseite übergeben, die in den Eigenschaftsdialog aufgenommen werden soll. In einem Programm, das mit dem Assistenten für ActiveX-Steuerelemente erzeugt worden ist, befindet sich an dieser Stelle bereits ein Eintrag für eine Eigenschaftsseite, für die der Assistent auch bereits eine Dialogressource angelegt hat. Im Fall des Beispielprogramms Knob ist das der Eintrag mit der CLSID CKnobPropPage::guid. Der zweite Eintrag mit der in Form einer Konstanten angegebenen Klassen-ID CLSID_CColorPropPage ist von Hand hinzugefügt worden. Die durch CLSID_CColorPropPage definierte Eigenschaftsseite ist eine von drei vordefinierten Eigenschaftsseiten für ActiveX-Steuerelemente. Die im Beispiel verwendete Eigenschaftsseite kann zur Auswahl von Farben verwendet werden. Im Fall des Steuerelements Knob können damit die Vorder- und die Hintergrundfarbe ausgewählt werden. Die beiden anderen vordefinierten Eigenschaftsseiten dienen zur Auswahl von Schrift- und Bildeigenschaften (CLSID_ CFontPropPage und CLSID_CPicturePropPage). Abbildung 3.61 zeigt die Eigenschaftsseite zur Farbauswahl des Steuerelements Knob.
Abbildung 3.61: Vordefinierter Farbauswahldialog
455
456
3
COM, OLE und ActiveX
Threading-Modell
Nach der Definition der Eigenschaftsdialogseiten hat der Assistent für ActiveX-Steuerelemente diverse Makros zur Definition von Klassenfabrik, CLSID und IIDs, Typinformation und Typbibliothek in den Quelltext eingefügt. Bei der sich anschließenden Implementierung der Funktion UpdateRegistry, die das Steuerelement in die Windows-Registrierungsdatenbank einträgt, sollte ein Punkt beachtet werden. Wie der vom Assistenten für ActiveXSteuerelemente generierte Kommentar bereits andeutet, wird das Steuerelement als Vorgabe unter dem Threading-Modell Apartment registriert. Das Threading-Modell eines COM-Servers legt fest, wie der COM-Server oder speziell das ActiveX-Steuerelement im Zusammenhang mit mehreren Threads eingesetzt werden kann. Das Modell Apartment sieht vor, dass mehrere Instanzen des COM-Servers bzw. des Steuerelements in jeweils einem eigenen Thread ausgeführt werden können. Vorsicht ist dabei bei globalen oder statischen Daten des Steuerelements geboten. Der Zugriff auf diese Daten muss durch ebenfalls globale oder statische Variablen des Typs CCriticalSection geschützt werden. Wer keine globalen oder statischen Variablen in seinem Steuerelement benutzt, kann das Modell Apartment ohne Probleme verwenden. Wer viele globale oder statische Variablen in seinem Steuerelement einsetzt und den Aufwand scheut, diese gegen konkurrierenden Zugriff zu schützen, der muss beim Aufruf der Funktion AfxOleRegisterControlClass für den Parameter 6 die Konstante afxRegApartmentThreading durch den Wert 0 ersetzen. Das resultierende Steuerelement weist dann allerdings einige Einschränkungen bezüglich der Verwendbarkeit mit mehreren Threads auf. Die genaue Vorgehensweise, wie man das Modell Apartment implementiert, ist im technischen Hinweis 64 beschrieben.
Zeichnen des Knopfs
Nach der Funktion UpdateRegistry der Klassenfabrik schließen sich die Funktionsimplementierungen der Klasse CKnobCtrl an. Der Programmcode im Konstruktor und in den Funktionen OnDraw, Draw und DrawMark soll nicht im Detail beschrieben werden. Der Knopf verwendet die bereits in Abschnitt 2.12.4, »Die Programmierung mit Bitmaps«, im Beispielprogramm Mandelbrot beschriebene Technik des Zeichnens im Hintergrund. Dadurch wird ein Flimmern beim Drehen des Knopfs vermieden. Der Konstruktor der Klasse CKnobCtrl trifft die dazu notwendigen Vorbereitungen. Der Knopf selbst wird immer in einer konstanten Größe dargestellt (nKnobSize = 50).
ActiveX-Steuerelemente
457
Die Funktion OnDraw wird aufgerufen, wenn das Steuerelement sich zeichnen soll. Interessant sind hier die Aufrufe der Funktionen GetBackColor und GetForeColor. Beides sind Funktionen, um Standardeigenschaften des ActiveX-Steuerelements abzufragen. Diese Funktionen werden durch die Klasse COleControl implementiert. Ähnliche vordefinierte Funktionen zum Lesen und Setzen gibt es auch für die anderen Standardeigenschaften eines ActiveX-Steuerelements. Die Funktionen GetBackColor und GetForeColor geben Werte vom Typ OLE_COLOR zurück. Um diesen Datentyp in den vom GDI verwendeten Typ COLORREF zu übersetzen, muss man die Funktion TranslateColor aufrufen. Das eigentliche Zeichnen des Drehknopfs ist in die Funktion Draw ausgelagert worden, damit diese auch unabhängig von OnDraw beim Drehen des Reglers aufgerufen werden kann. Die Funktion Draw zeichnet den Knopf. Dabei werden die Markierungen durch wiederholten Aufruf der Funktion DrawMark gezeichnet. Umrechnungen zwischen Winkeln, Werten und Positionen werden durch die Funktionen ValueFromAngle und AngleFromPos erledigt. Die verwendeten Formeln rechnen mittels trigonometrischer Funktionen Positionen in Winkel um. Es wurde bereits bei der Vorstellung von ActiveX-Steuerelementen gesagt, dass die Eigenschaften eines Steuerelements persistent sind. Damit ein Steuerelement seine Eigenschaften speichern kann, stellt der Container ihm ein Speichermedium zur Verfügung. Dazu existiert mittlerweile eine ganze Reihe von unterschiedlichen Verfahren, die über eine der COM-Schnittstellen IPersistStorage, IPersistStream, IPersistStreamInit oder IPersistPropertyBag abgewickelt werden. Innerhalb der MFC wird die persistente Speicherung durch Objekte der Klasse CPropExchange realisiert. CPropExchange baut einen Kontext für das Speichern der Eigenschaftswerte auf. Das Speichern und Laden der Eigenschaftswerte findet in der Funktion DoPropExchange der von COleControl abgeleiteten Klasse statt. Im Beispiel ist das die Funktion CKnobCtrl::DoPropExchange. Der Datenaustauschkontext wird der Funktion in Form eines Zeigers auf ein CPropExchange-Objekt übergeben. Der Datenaustausch selbst muss – anders als bei dem ähnlich modellierten DDX – von Hand vorgenommen werden. Die MFC stellen zu diesem Zweck eine ganze Reihe von Funktionen zur Verfügung, die mit dem Kürzel PX_ beginnen. Im Bei-
Implementierung der Persistenz
458
3
COM, OLE und ActiveX
spielprogramm wird die Funktion PX_Long verwendet, um die Eigenschaftswerte Value und MaxValue zu laden und zu sichern. Welcher Eigenschaftswert ausgetauscht werden soll, wird der Funktion in Form eines Strings mitgeteilt. Der optionale vierte Parameter der Funktion setzt einen Vorgabewert, der verwendet wird, falls das Laden der Eigenschaftswerte fehlschlägt, beispielsweise weil das Steuerelement zum ersten Mal geladen wird und daher noch keine Eigenschaftswerte gespeichert hat. Standardeigenschaften, wie die im Beispiel verwendeten Eigenschaften ForeColor und BackColor, müssen nicht explizit gesichert werden. Dies geschieht bereits automatisch. Die im Anschluss an DoPropExchange implementierte Funktion OnResetState wird aufgerufen, wenn die Eigenschaftswerte des Steuerelements in einen definierten Zustand gebracht werden sollen, beispielsweise weil noch keine Eigenschaftswerte gesichert worden sind. Behandlung der Maus
Nach OnResetState und der Funktion AboutBox, die den Info-Dialog des Steuerelements anzeigt, werden die drei Nachrichtenbehandlungsfunktionen OnLButtonDown, OnLButtonUp und OnMouseMove definiert. OnLButtonDown wird ausgeführt, wenn der Benutzer die linke Maustaste drückt. Die Aufgabe der Funktion ist es, die Maus »einzufangen«. Einfangen bedeutet, dass sich das Steuerelement alle Mausnachrichten aneignet, auch wenn der Mauszeiger den Bereich des Steuerelements verlässt. Freigegeben werden soll die Maus erst wieder, wenn der Benutzer die Maustaste loslässt, also in der Funktion OnLButtonUp. Zweck dieser Vorgehensweise ist es, den Drehknopf bequem bedienen zu können, auch wenn man den Bereich des Drehreglers verlässt. Ohne das Binden der Mausnachrichten an das Steuerelement würde sich ein unangenehmes Springen des Reglers einstellen, wenn man den Steuerelementbereich verlässt. Um alle Mausnachrichten an das Steuerelement zu binden, wird in der Funktion OnLButtonDown die Funktion CWnd::SetCapture aufgerufen. Von nun an bekommt das Steuerelement alle Mausnachrichten zugestellt, egal wo sich der Mauszeiger befindet. In der Funktion OnLButtonUp wird entsprechend die Funktion ReleaseCapture aufgerufen, die diesen Zustand wieder aufhebt. Zusätzlich wird in OnLButtonDown die Variable bCapture auf den Wert true gesetzt. Damit merkt sich das Steuerelement, ob es Mausnachrichten verarbeiten soll, das heißt, ob der Regler gerade bewegt wird. Diese Variable wird in der
ActiveX-Steuerelemente
459
Funktion OnMouseMoved abgefragt, die immer dann aufgerufen wird, wenn sich die Position der Maus verändert hat. Besitzt bCapture den Wert true, dann wird in OnMouseMoved ein Gerätekontext zum Zeichnen angefordert, aus der Mausposition der Reglerwert bestimmt und dann der Regler neu gezeichnet. Lässt der Benutzer den Regler schließlich los, dann wird nach der Freigabe der Maus die Funktion FireOnValueChanged aufgerufen. Diese löst das Ereignis OnValueChanged aus, indem sie die Funktion COleControl:: FireEvent mit der DISPID für OnValueChanged aufruft. Das Ereignis wird so an die IDispatch-Schnittstelle des Containers geleitet. Die Funktionen SetValue, GetValue, SetMaxValue und GetMaxValue setzen und lesen die Werte der Eigenschaften Value und MaxValue. Die Werte der beiden Eigenschaften werden in den Member-Variablen m_nValue und m_nMaxValue gespeichert. Beim Setzen der beiden Eigenschaften wird der Regler durch den Aufruf der Funktion Invalidate neu gezeichnet und durch Aufruf der Funktion SetModifiedFlag werden die Eigenschaftswerte als verändert gekennzeichnet. Wurde der Regelbereich durch Setzen der Eigenschaft m_nMaxValue verändert, so wird außerdem der Reglerwert neu berechnet, damit der Regler nicht »springt«. Der Regler passt seinen Reglerwert an den neuen Regelbereich an. Die Reglerwertänderung wird durch Auslösen des Ereignisses OnValueChanged an das Programm weitergegeben, welches den Regler verwendet. Nach dieser Besprechung der Datei KNOBCTL.CPP und der Klasse CKnobCtrl bleibt zum Abschluss nur noch die Klasse CKnobPropPage zu erwähnen. Diese implementiert den Eigenschaftsdialog des Steuerelements. Listing 3.56 zeigt die entsprechende Implementierungsdatei KNOBPPG.CPP. IMPLEMENT_DYNCREATE(CKnobPropPage, COlePropertyPage) ////////////////////////////////////////////////////////////// / // Nachrichtenzuordnungstabelle BEGIN_MESSAGE_MAP(CKnobPropPage, COlePropertyPage) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// // // Klassenerzeugung und GUID initialisieren IMPLEMENT_OLECREATE_EX(CKnobPropPage, "KNOB.KnobPropPage.1",
////////////////////////////////////////////////////////////// // // CKnobPropPage::DoDataExchange - Verschiebt Daten zwischen // Dialogfeld+++ und den Variablen+++ void CKnobPropPage::DoDataExchange(CDataExchange* pDX) { DDP_Text(pDX, IDC_EDIT2, m_nMaxValue, _T("MaxValue") ); DDX_Text(pDX, IDC_EDIT2, m_nMaxValue); DDP_PostProcessing(pDX); } Listing 3.56: Die Eigenschaftsdialogklasse CKnobPropPage
ActiveX-Steuerelemente
Die Eigenschaftsdialogklasse CKnobPropPage ist von der MFCKlasse für Eigenschaftsdialoge COlePropertyPage abgeleitet. Der Datenaustausch zwischen Eigenschaftsdialog und Programm findet, wie bei normalen Dialogen, über DDX statt. Durchgeführt wird der Datenaustausch in der Funktion DoDataExchange. Dazu werden die bereits bekannten DDX-Funktionen verwendet. Zusätzlich muss bei Eigenschaftsdialogen von ActiveX-Steuerelementen auch eine Verbindung zwischen der Datenaustauschvariablen und der Eigenschaft selbst hergestellt werden. Dies geschieht durch einen weiteren Satz von Funktionen, den DDPFunktionen. Eine DDP-Funktion besitzt die gleichen Parameter wie die ihr entsprechende DDX-Funktion, bekommt aber zusätzlich als letzten Parameter den Eigenschaftsnamen in Form einer Zeichenkette übergeben. Um den Austausch der Eigenschaftswerte abzuschließen, muss am Ende von DoDataExchange die Funktion DDP_PostProcessing aufgerufen werden.
3.6.6
461 Datenaustausch mit dem Eigenschaftsdialog
ActiveX-Steuerelemente im Internet
Mit dem Siegeszug des Internets und besonders des World Wide Web vor einigen Jahren wurden schnell die Beschränkungen der Beschreibungssprache HTML deutlich, die zur Anzeige von Seiten in WWW-Browsern verwendet wird. Ein Problem dieser Sprache ist die mangelnde Interaktivität der mit ihr erstellten Seiten. Neben dem Versuch, HTML durch allerlei Ergänzungen der Sprache zu verbessern, gab und gibt es verschiedene Ansätze, von HTML völlig unabhängige Technologien in Internetseiten zu integrieren, die mit dieser Beschreibungssprache erstellt werden. Beispiele dafür sind vom Browser verstandene Skriptsprachen wie JavaScript und Visual Basic Script, diverse Plug-Ins, die Multimediatechnologien Shockwave und Flash von Makromedia sowie die Programmiersprache Java von Sun. Ziel aller dieser Ansätze ist es, mehr Interaktivität im Internetbrowser zu ermöglichen, als es mit HTML möglich ist.
Limitierungen von HTML
Natürlich hat sich auch Microsoft mit diesem Problem beschäftigt und versucht eine Lösung zu finden, die möglichst gut mit der bisherigen technologischen Ausrichtung zusammenpasst. Dabei fiel auf, dass die Vorläufer der ActiveX-Steuerelemente, OCX, die Anforderungen an interaktive Komponenten innerhalb einer Webseite bereits recht gut erfüllten. Lediglich die Größe und Ausführungsgeschwindigkeit dieser Komponenten waren für den Einsatz
ActiveX-Steuerelemente im Internet
462
3
COM, OLE und ActiveX
im Internet wenig geeignet. Insbesondere die Größe übertraf vergleichbare Java-Applets deutlich. Mit Blick auf den Einsatz im Internet wurden die Anforderungen an OCX-Steuerelemente zurückgenommen, und es wurden neue Implementierungsverfahren (wie beispielsweise für fensterlose Steuerelemente) entwickelt, die die Geschwindigkeit teilweise deutlich erhöhten und die Code-Größe verringerten. Laufzeit-DLLs der MFC
Auch mit den MFC erstellte ActiveX-Steuerelemente lassen sich im Internet einsetzen. Der Nachteil der mit den MFC erstellten ActiveX-Steuerelemente besteht darin, dass sie grundsätzlich dynamisch gelinkt werden. Dadurch wird das Steuerelement zwar angenehm klein, aber die MFC-Laufzeit-DLLs müssen auf dem Zielsystem vorhanden sein oder zeitaufwändig aus dem Internet geladen werden.
Sicherheitsaspekte
Ein weiterer Punkt, der beim Einsatz von ActiveX-Steuerelementen im Internet zu bedenken ist, sind die Sicherheitsaspekte, die sich aus ihrem Einsatz ergeben. Im Gegensatz zu Java-Applets laufen ActiveX-Steuerelemente nicht in einer geschützten Umgebung ab (»Sandbox«). Ein ActiveX-Steuerelement hat grundsätzlich Zugang zu allen Ressourcen des Rechners, auf dem es ausgeführt wird. Damit kann es leicht zum Überträger von Viren werden oder als bösartiger Eindringling dienen (»Trojanisches Pferd«). Microsoft versucht das Sicherheitsproblem durch eine Zertifizierung der Steuerelemente zu lösen. Mit kryptografischen Verfahren wird das Steuerelement eindeutig und fälschungssicher gekennzeichnet. Nach der Installation des Steuerelements über das Internet wird dem Benutzer ein Sicherheitszertifikat angezeigt, das den Hersteller des Steuerelements eindeutig identifiziert. Der Benutzer kann dann aufgrund dieses Zertifikats entscheiden, ob er das Steuerelement installieren möchte. Weiterhin kann der Benutzer durch die Sicherheitseinstellungen des Browsers festlegen, dass die Ausführung von ActiveX-Steuerelementen generell unterlassen werden soll.
Object-Tag
ActiveX-Steuerelemente werden mit dem Object-Tag in eine HTML-Seite eingebettet. Listing 3.57 zeigt, wie das Steuerelement Knob mit diesem Tag in eine HTML-Seite eingebettet wird. <META NAME="GENERATOR" Content="Microsoft Developer Studio"> <META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1">
ActiveX-Steuerelemente <TITLE>Ein Knopf im Internet!
Ein Knopf im Internet!
Listing 3.57: Einbettung des Knob-Steuerelements in eine HTML-Seite
Bei der Einbettung müssen die Klassen-IDs des Steuerelements sowie dessen Breite und Höhe angegeben werden. Soll das Steuerelement tatsächlich von einem Webserver geladen werden und ist es noch nicht lokal auf dem Rechner installiert, dann ist durch die Angabe von CODEBASE ein Pfad auf dem Server anzugeben. Die Eigenschaften des Steuerelements können durch das Param-Tag gesetzt werden. Abbildung 3.62 zeigt die Datei aus Listing 3.57 im Internet Explorer.
Abbildung 3.62: Das Steuerelement Knob im Internet Explorer
463
464
3
3.6.7
COM, OLE und ActiveX
Tipps zur Vorgehensweise
Die Tipps zur Vorgehensweise bestehen diesmal aus zwei Teilen: den Tipps zur Verwendung und den Tipps zur Erstellung von ActiveX-Steuerelementen. Bei der Verwendung von ActiveX-Steuerelementen in MFC-Programmen gilt: 왘 Damit ActiveX-Steuerelemente innerhalb eines Projekts verwendet werden können, müssen sie zunächst zu dem Projekt hinzugefügt werden. Dazu werden sie einfach in die Toolbox der Entwicklungsumgebung eingefügt. Sie lassen sich dann innerhalb von Dialogfeldern wie normale Steuerelemente verwenden. 왘 Methoden, Eigenschaften und Ereignisse eines verwendeten Steuerelements lassen sich sehr schön mit dem Objektbrowser von Visual Studio .NET untersuchen. 왘 Möchte man ActiveX-Steuerelemente außerhalb von Dialogfeldern verwenden, so ist etwas mehr Handarbeit notwendig. Zunächst muss mit der Entwicklungsumgebung eine Zugriffsklasse für das Steuerelement generiert werden (Proxy-Klasse). Dann muss das Steuerelement durch Aufruf der Create-Funktion der kapselnden Klasse erzeugt werden. Die Ereignisverwaltung muss bei Steuerelementen, die außerhalb von Dialogfeldern verwendet werden, von Hand durchgeführt werden. 왘 ActiveX-Steuerelemente können mit dem Object-Tag in HTMLSeiten eingebettet werden. Bei der Erstellung von ActiveX-Steuerelementen mit den MFC gilt: 왘 Zum Anlegen von ActiveX-Steuerelementen mit den MFC wird der MFC-ActiveX-Steuerelement-Assistent verwendet. 왘 ActiveX-Steuerelemente verwenden nicht die DokumentAnsicht-Architektur. Trotzdem ähnelt die Struktur von ActiveX-Steuerelementen, die mit den MFC erstellt wurden, anderen MFC-Anwendungen. Es gibt ein Applikationsobjekt und die von COleControl abgeleitete Klasse verhält sich wie Dokument und Ansicht einer dokumentenbasierten MFC-Anwendung zusammen.
ActiveX-Steuerelemente
465
왘 Die MFC bieten eine vordefinierte Implementierung für Eigenschaftsseiten von ActiveX-Steuerelementen. Der Austausch von Eigenschaften und den sie repräsentierenden Feldern im Eigenschaftsdialog wird durch einen erweiterten DDX-Mechanismus realisiert. 왘 Steuerelemente, die mit dem Assistenten für ActiveX-Steuerelemente angelegt wurden, werden mit dem Threading-Modell Apartment gekennzeichnet. Man sollte nach der Implementierung des Steuerelements überprüfen, ob es sich tatsächlich an die Regeln dieses Modells hält. Ist dies nicht der Fall, so muss das Steuerelement entweder nachgebessert (Zugriffe auf statische und globale Variablen müssen synchronisiert werden) oder das Modell darf nicht verwendet werden. 왘 Man sollte nicht vergessen, die MFC-Laufzeit-DLLs mit dem Steuerelement auszuliefern. Mit den MFC erstellte ActiveXSteuerelemente lassen sich nicht statisch mit der MFC-Bibliothek linken.
3.6.8
Zusammenfassung
ActiveX-Steuerelemente stellen einen gelungenen Ansatz dar, Programmierern einfach verwendbare, sich visuell darstellende Komponenten zur Verfügung zu stellen. Der Programmierer, der diese Komponenten verwendet, arbeitet auf einem hohen Abstraktionsniveau. Dadurch, dass ActiveX-Steuerelemente auf COM basieren, sind sie völlig sprachunabhängig. In der Tat werden ActiveX-Steuerelemente oft in Sprachen wie Visual Basic oder Visual Basic for Applications (VBA) verwendet. Es gibt mittlerweile eine große Auswahl an ActiveX-Containern. Selbst wenn Microsoft mit .NET auf eine andere Komponentenarchitektur umschwenkt, so wird es noch Jahre dauern, bis ActiveX-Steuerelemente ihre Popularität verlieren. Schließlich lassen sich auch unter .NET die alten ActiveX-Elemente verwenden. Durch die einfache Verwendbarkeit von ActiveX-Steuerelementen hat sich häufig eine arbeitsteilige Herangehensweise beim Erstellen größerer unternehmensinterner Anwendungsprogramme bewährt. Auf der unteren Ebene arbeitet eine Reihe von hoch qualifizierten C++-Programmierern, die Problemlösungen für sich häufig wiederholende und zentrale Aufgaben der unternehmensinternen EDV-Struktur finden. So kann beispielsweise der Zugriff
Aufgabenteilung
466
3
COM, OLE und ActiveX
auf Datenbankserver des Unternehmens über ActiveX-Steuerelemente erfolgen. Eine zweite Gruppe von Programmierern baut aus den Steuerelementen ihrer Kollegen EDV-Lösungen zusammen. Dabei verwenden sie Programmierumgebungen wie Visual Basic oder Delphi, die sich gut als »Komponentenkleber« eignen. Einsatz im Intranet
Durch die Erweiterung des Einsatzbereiches von ActiveX-Steuerelementen auf das Internet ergeben sich interessante neue Verwendungsmöglichkeiten. Durch die bestehenden Sicherheitsprobleme ist die Akzeptanz solcher Steuerelemente im Internet allerdings bisher recht beschränkt geblieben. Anders stellt sich die Situation in unternehmensinternen Intranets dar. Ein Intranet ist ein Netzwerk, das die gleiche Technologie verwendet wie das Internet, dessen Ausdehnung sich aber auf eine Firma beschränkt. Anders als im Internet treten dadurch keine größeren Sicherheitsprobleme auf. Dadurch, dass das unternehmensinterne LAN (Local Area Network, ein Netzwerk, das meist auf Technologien wie Ethernet oder Token Ring basiert) verwendet wird, treten zudem keine Geschwindigkeitsprobleme auf. Mit ActiveX-Steuerelementen lassen sich interessante Anwendungen konzipieren, mit denen Angestellte eines Unternehmens Firmendaten per Internetbrowser abrufen und eventuell auch eingeben können. Allerdings werden sich hier in Zukunft wahrscheinlich Lösungen auf Basis der neuen .NET-Architektur durchsetzen. Die MFC bieten als universelles Framework zur Programmierung von Windows-Anwendungen eine hervorragende Unterstützung für die Entwicklung von ActiveX-Steuerelementen. Mit dem eigenen Assistenten zur Erstellung von ActiveX-Steuerelementen lassen sich diese schnell erzeugen. Der Klassen-Assistent bietet Unterstützung bei der Erstellung von Methoden, Eigenschaften und Ereignissen der Steuerelemente. Zudem lassen sich MFC-Programme auch gut als ActiveX-Container einsetzen. Insbesondere die Integration in den Ressourceneditor ist gut gelungen.
3.7
Zusammenfassung COM, OLE und ActiveX
Das Ziel des vorliegenden Kapitels war es, das Wesen des Komponentenobjektmodells COM zu erläutern. Dazu wurde versucht, COM zunächst sehr nahe an der tatsächlichen Implementierung
Zusammenfassung COM, OLE und ActiveX
zu zeigen. Diese recht technische Sicht soll einem C++-Programmierer verdeutlichen, um was es sich bei COM eigentlich handelt. Erst danach sind die Hilfsmittel der MFC vorgestellt worden, mit denen sich COM-Server und COM-Client deutlich einfacher implementieren lassen. Das grundlegende Verständnis von COM ist notwendig, um die Mächtigkeit des Ansatzes dieser eigentlich recht einfachen Technologie begreifen zu können. Die auf COM aufbauenden Technologien verschleiern nämlich durch die Komplexität, die diese mit sich bringen, leicht den Blick auf COM selbst. COM ist mittlerweile eine der tragenden Säulen von Windows geworden. Mit der netzwerkfähigen Version DCOM bildet es zudem den notwendigen »Klebstoff« für mehrschichtige Unternehmensapplikationen (sog. n-Tier-Applikationen). Solche mehrschichtigen Applikationen teilen die Logik eines Programms beispielsweise in Datenbankschicht, Verarbeitungsschicht und Darstellungsschicht auf. (D)COM fungiert hier als Mittler zwischen den Schichten und wird daher als Middleware bezeichnet. Für den Programmierer ergibt sich bei der Programmierung von DCOM praktisch kein zusätzlicher Aufwand gegenüber der Programmierung von COM. Lediglich der Name des Rechners, auf dem der DCOM-Server ausgeführt werden soll, muss angegeben werden. Den Datentransport über das Netzwerk übernimmt die COM-Laufzeitbibliothek. Die in diesem Kapitel gezeigten auf COM aufbauenden Technologien Automation, OLE und ActiveX-Steuerelemente stellen nur die erste Generation COM-basierter Technologien dar. Mittlerweile benutzen die neuen Spiel- und Multimedia-Schnittstellen – gebündelt unter dem Schlagwort DirectX – die COM-Architektur. Zudem verwenden auch neuere Datenbankschnittstellen, wie OLE DB, COM als Grundlage. OLE DB wird in Kapitel 4, »Datenbankprogrammierung«, beschrieben.
467
4 Datenbankprogrammierung Die Programmierung von Datenbank-Frontends, also von Programmen, die auf Datenbanken zugreifen, ist eine der häufigsten Tätigkeiten bei unternehmensinternen EDV-Projekten. Datenbanken werden nicht selbst programmiert, sondern unter Verwendung von Datenbank-Managementsystemen (DBMS) erstellt. Datenbank-Managementsysteme bieten sich durch ihre Eigenschaften als unternehmensweiter, universeller Datenspeicher an. Man verwendet DBMS immer dann, wenn größere Datenmengen zu speichern sind und eine Speicherung in Dateien nicht mehr praktikabel wäre. Sie bieten gegenüber normalen Dateien viele Vorteile, wie beispielsweise den gleichzeitigen Zugriff durch mehrere Benutzer und die Abschirmung von den Details der Speicherung auf dem Datenträger.
4.1.1
DatenbankManagementsysteme
Begriffe und Eigenschaften
Moderne DBMS folgen entweder dem relationalen oder dem objektorientierten Modell, wobei in der Praxis die relationalen DBMS (RDBMS) deutlich überwiegen. Eine Datenbank, die ein RDBMS verwendet, speichert ihre Daten in einer oder mehreren Tabellen. Die Tabellen einer relationalen Datenbank sind zweidimensional, bestehen also aus Zeilen und Spalten. In einer Spalte werden Daten gleichen Typs gespeichert, man bezeichnet die Spalten als Attribute oder Felder. In den Zeilen werden zusammengehörige Werte gespeichert, man spricht von Tupeln oder Datensätzen.
Relationale Datenbanken
Zur Identifizierung von Datensätzen werden Tabellen mit Schlüsseln versehen. Ein Schlüssel umfasst ein oder mehrere Felder einer Tabelle. Der Primärschlüssel einer Tabelle stellt die Eindeutigkeit von Datensätzen sicher. Er setzt sich aus der minimalen Anzahl von Feldern zusammen, die die Eindeutigkeit der Datensätze garantieren. Ein Fremdschlüssel bezeichnet eine Gruppe von Feldern, die in einer anderen Tabelle die Funktion eines Primärschlüssels einnimmt. Ein Fremdschlüssel stellt Beziehungen zwischen Tabellen her.
Schlüssel
470
4
Datenbankprogrammierung
Normalisierung
Durch geschickte Wahl der Tabellenattribute lassen sich Daten redundanzfrei speichern. Der Prozess, um relationale Datenbanken von Redundanzen zu befreien und Tabellen optimal auszulegen, heißt Normalisierung. Die Tabellen einer Datenbank werden dabei an vorgegebene Normalformen angepasst, die bestimmte Eigenschaften der Datenbank garantieren. Die Normalisierung relationaler Datenbanken ist ein wichtiger Teil des Datenbankdesigns. Sie erfolgt anhand fester Regeln.
Beziehungen
Da die Daten einer Datenbank durch die Verteilung auf mehrere Tabellen nicht mehr zusammenhängend in einer Tabelle gespeichert werden, müssen sie beim Zugriff wieder zusammengeführt werden. Die Verknüpfung von Datenbanktabellen wird durch Beziehungen festgelegt. Die Beziehungen zwischen den Tabellen werden über Schlüssel hergestellt.
Referenzielle Integrität
Die Konsistenz der Daten verschiedener Tabellen einer Datenbank kann durch das Datenbank-Managementsystem überwacht werden. Man spricht in diesem Zusammenhang von der referenziellen Integrität der Datenbank. Die referenzielle Integrität besagt, dass die Fremdschlüsselwerte einer Tabelle als Primärschlüsselwerte in einer anderen Tabelle vorhanden sein müssen. Beim Löschen oder Ändern von Datensätzen kann es zu Verletzungen der referenziellen Integrität kommen. Das DBMS kann entweder das Löschen oder Ändern von Datensätzen verbieten oder die referenzielle Integrität dadurch beibehalten, dass die referenzierten Datensätze gelöscht oder geändert werden (kaskadiertes Löschen oder Löschweitergabe, kaskadiertes Ändern oder Aktualisierungsweitergabe).
Transaktionen
Datenbank-Managementsysteme können mehrere Datenmanipulationen zu einer Transaktion kombinieren. Eine Transaktion ist eine Zusammenfassung von Datenbankanweisungen zu einer unteilbaren oder atomaren Einheit, die entweder ganz oder gar nicht ausgeführt wird. Tritt während der Ausführung der Transaktion ein Fehler auf, so werden die bis dahin durchgeführten Änderungen rückgängig gemacht.
Logische Sichten
Datenbank-Managementsysteme bieten die Möglichkeit, logische Sichten oder Views auf eine oder mehrere Tabellen zu definieren. Dazu werden irrelevante Felder aus Tabellen ausgeblendet und Sichten über mehrere Tabellen angelegt. Eine Sicht stellt sich dem Benutzer als virtuelle Tabelle dar.
471
Relationale Datenbank-Managementsysteme bieten gegenüber der Verwendung von Dateien eine ganze Reihe von Vorteilen. Die folgende Aufzählung führt die wichtigsten Punkte auf: 왘 DBMS eignen sich zur Speicherung großer bis sehr großer Datenmengen. 왘 Der gleichzeitige Zugriff mehrerer Benutzer auf die Daten einer Datenbank ist möglich. 왘 In DBMS gespeicherte Daten können mit einem oder mehreren Indizes versehen werden. Dadurch ist ein schneller Zugriff auf die Daten aufgrund unterschiedlicher Sortierkriterien möglich. 왘 Datenbanken zeigen eine logische Darstellung der in ihnen gespeicherten Daten. Die Darstellung besteht aus den Tabellen der Datenbank, kann aber zusätzlich durch Sichten oder Views ergänzt werden. 왘 Jedes DBMS bietet mindestens eine Sprache zur Abfrage und Manipulation von Daten und Datenbankstrukturen an. Meist wird hierzu eine Variante der Structured Query Language (SQL) verwendet, es existieren aber auch andere Sprachen. 왘 DBMS können die Konsistenz der in ihnen gespeicherten Daten anhand vorgegebener Regeln überwachen. Sie können die referenzielle Integrität der Datenbank aufrechterhalten. 왘 DBMS können Zugriffsrechte detaillierter vergeben, als es in einem Dateisystem möglich ist. 왘 Sperrmechanismen verhindern den gleichzeitigen schreibenden Zugriff auf einen Datensatz. 왘 Zugriffe auf in DBMS gespeicherte Daten können in Form von Transaktionen ablaufen. Transaktionen führen Änderungen am Datenbestand konsistent durch, indem sie eine Reihe von Datenmanipulationen als atomare Einheit durchführen. Im Fehlerfall werden nicht vollständig ausgeführte Transaktionen rückgängig gemacht. Für eine ausführliche Einführung in die Datenbanktheorie sei das Buch von C.J. Date, »An Introduction to Database Systems« (siehe Anhang) empfohlen.
472
4
4.1.2
Datenbankprogrammierung
Datenbankschnittstellen
Bei den meisten Datenbank-Managementsystemen lassen sich die Masken und Programme zum Zugriff auf die in der Datenbank gespeicherten Daten entweder mit den Hilfsmitteln des Datenbank-Managementsystems selbst oder mit einem vom Hersteller gelieferten Entwicklungswerkzeug erzeugen. Oft – besonders im Fall von großen Serverdatenbanken – möchte man jedoch diese Hilfsmittel des Datenbankherstellers nicht nutzen, beispielsweise weil die Benutzeroberfläche damit nicht wie gewünscht generiert werden kann. Um in so einem Fall die gewünschte Benutzeroberfläche zu erhalten, muss man mit eigenen Programmen auf das DBMS zugreifen. Jedes DBMS bietet eine oder mehrere Datenbankschnittstellen, um dies zu ermöglichen. Fast jedes DBMS besitzt eine eigene spezifische Programmschnittstelle. Über diese Schnittstelle können alle Eigenschaften des DBMS ausgenutzt werden. Diese Schnittstelle besitzt zudem das optimale Laufzeitverhalten beim Zugriff auf das DBMS. Neben datenbankspezifischen Schnittstellen gibt es standardisierte Schnittstellen. Diese Standardschnittstellen versuchen, die Gemeinsamkeiten aller Datenbank-Managementsysteme zusammenzufassen und dem Programmierer eine einheitliche Schnittstelle zum Zugriff auf verschiedene DBMS zu geben. Eine solche Standardschnittstelle ist eine Abstraktion des DBMS, auf das zugegriffen werden soll. Die Vorteile liegen auf der Hand: Der Programmierer muss sich mit nur einer Schnittstelle auseinander setzen, auch wenn er verschiedene Datenbank-Managementsysteme verwendet. Dieser Umstand drückt sich in einer deutlichen Zeitersparnis bei der Programmentwicklung aus. Zudem kann ein Programm auf verschiedene DBMS zugreifen, ohne dass der Schnittstellencode geändert werden müsste. Nachteilig sind das teilweise schlechtere Laufzeitverhalten von Standardschnittstellen und die mangelnde Fähigkeit, alle Leistungsmerkmale des DBMS nutzen zu können. Microsoft unterstützt in der Version 7.0 der MFC drei Schnittstellentechnologien: ODBC, DAO und OLE DB. ODBC
ODBC, Open Database Connectivity, war eine der ersten datenbankunabhängigen Schnittstellen überhaupt. ODBC ist heute die wohl am weitesten verbreitete Datenbankschnittstelle. Für praktisch jedes kommerzielle Datenbank-Managementsystem gibt es eine ODBC-Anbindung.
473
DAO, Data Access Objects, ist eine Datenbankschnittstelle, die speziell auf die Jet-Engine, den Kern von MS Access, zugeschnitten ist. Der Jet-Datenbankkern unterstützt eine ganze Reihe von Datenformaten. Da der Jet-Datenbankkern ODBC-Datenquellen öffnen kann, ist es möglich, über DAO auch auf andere DBMS zuzugreifen. Seit Version 7.0 der MFC wird DAO nicht mehr durch die Entwicklungsumgebung unterstützt. Von Neuentwicklungen auf der Basis von DAO wird daher abgeraten. Trotzdem stehen die DAOKlassen weiterhin zur Verfügung.
DAO
OLE DB (Microsoft definiert nicht, was OLE DB unabgekürzt bedeutet) ist eine neue, auf COM basierende datenbankunabhängige Schnittstelle. Sie wird erst seit der Version 6.0 von Visual C++ unterstützt.
OLE DB
Alle drei Datenbankschnittstellen werden in diesem Kapitel anhand einer MS Access-Datenbank vorgestellt.
4.1.3
Die Datenbank AKTIEN.MDB
Alle in diesem Kapitel vorgestellten Beispielprogramme verwenden die gleiche Datenbank. Es handelt sich dabei um eine MS Access-Datenbank, in der Aktien und die dazugehörigen Aktienkurse gespeichert sind. Um auf die Datenbank zuzugreifen und die vorgestellten Beispielprogramme auszuführen, ist MS Access nicht notwendig. MS Access selbst ist nur ein Frontend zu einem Datenbankkern, der auch unabhängig von MS Access verwendet werden kann. Dies ist der bereits erwähnte Jet-Datenbankkern. Durch den Jet-Datenbankkern kann ohne MS Access auf jede MS Access-Datenbank zugegriffen werden. Der Jet-Datenbankkern ist Bestandteil einer Standard-Visual-C++-Installation. Die in diesem Kapitel verwendete Beispieldatenbank heißt AKTIEN.MDB und befindet sich auf der Begleit-CD im Verzeichnis DB. Die Beispieldatenbank hat einen einfachen Aufbau. Abbildung 4.1 zeigt das Datenmodell der Datenbank in Form eines ERMs (Entity Relationship Model).
Aktie
1
besitzt
Abbildung 4.1: Das ERM zur Beispieldatenbank
N
Kurs
ERM
474
4 Datenbanktabellen
Datenbankprogrammierung
Das ERM wird in Form von zwei Tabellen umgesetzt. In der Tabelle Aktie werden alle Eigenschaften einer Aktie gespeichert, die Tabelle Kurs ordnet einer Aktie einen Kurs an einem bestimmten Tag zu, speichert also die Schlusskurse des jeweiligen Börsentages. Listing 4.1 zeigt die Definition der beiden Tabellen. Der Primärschlüssel ist jeweils unterstrichen. Aktie (Aktiennummer, Aktienname, WKN, Tickersymbol) Kurs (Aktiennummer, Datum, Kurs) Listing 4.1: Definition der Tabellen Aktie und Kurs
Der Primärschlüssel der Tabelle Aktie ist ein künstlicher Schlüssel, der automatisch von der Datenbank vergeben wird, wenn ein neuer Datensatz in die Tabelle eingefügt wird. Die Tabellen Kurs und Aktie stehen in einer 1:n-Beziehung zueinander. In der Tabelle Kurs ist das Feld Aktiennummer ein Fremdschlüssel zur Tabelle Aktie. Abbildung 4.2 zeigt das Fenster BEZIEHUNGEN aus MS Access.
Abbildung 4.2: Beziehung der Tabellen Aktie und Kurs
Abbildung 4.3: Referenzielle Integrität der Beispieldatenbank
ODBC
475
Die Einstellungen zur referenziellen Integrität wurden innerhalb von MS Access vorgenommen. Die Überwachung der referenziellen Integrität ist mit kaskadiertem Löschen und Ändern (Löschund Aktualisierungsweitergabe) ausgewählt worden. Abbildung 4.3 zeigt den entsprechenden Dialog aus MS Access (Menü BEZIEHUNGEN | BEZIEHUNGEN BEARBEITEN).
4.2
ODBC
ODBC ist eine Datenbankschnittstelle, die als Teil der Windows Open System Architecture (WOSA) eingeführt wurde. WOSA beruht auf der Idee, von Serverprogrammen bereitgestellte Dienstleistungen über offene, standardisierte Schnittstellen (APIs) mit Client-Programmen zu verbinden. Neben ODBC gehören Schnittstellen wie MAPI (Messaging API), TAPI (Telephone API) und Winsock (Windows Sockets API) zur Familie der WOSASchnittstellen. Das WOSA-Modell wird mittlerweile von Microsoft nicht mehr weiterentwickelt, neu entwickelte Schnittstellen bauen entweder auf dem Component Object Model (COM) auf (siehe Kapitel 3, »COM, OLE und ActiveX«) oder auf der neuen .NETArchitektur. ODBC war eine der ersten datenbankunabhängigen Schnittstellen und ist die älteste der drei hier vorgestellten Datenbanktechnologien. Trotzdem wurde ODBC stetig weiterentwickelt und hat in der Praxis eine breite Anwendung gefunden.
4.2.1
Referenzielle Integrität
Die ODBC-Architektur
ODBC führt die Abstraktion vom Datenbank-Managementsystem in drei Schritten durch. ODBC definiert: 왘 Eine Anzahl von C-Funktionen, Datentypen und Fehlercodes, mit denen auf ein beliebiges DBMS zugegriffen werden kann. Diese Funktionen bilden die Programmierschnittstelle (API) von ODBC. 왘 Eine für alle DBMS einheitliche SQL-Syntax. Sollte das angesprochene DBMS eine andere SQL-Syntax verwenden, so muss diese vom ODBC-Treiber umgesetzt werden. 왘 Eine zweischichtige Treiberarchitektur. Auf der oberen Ebene sorgt der ODBC-Treibermanager für den datenbankunabhängigen Zugriff auf die Daten. Die Anpassung an das jeweilige DBMS wird durch einen datenbankspezifischen Treiber unterhalb des ODBC-Treibermanagers vorgenommen.
WOSA
476
4
Datenbankprogrammierung
Die C-Schnittstelle von ODBC ist nicht Windows-spezifisch und prinzipiell portabel. Es gibt ODBC-Implementierungen sowohl für den Apple Macintosh wie auch für diverse Unix-Systeme. ODBC-SQL
Die von ODBC definierte SQL-Syntax basiert auf einer Spezifikation von X/OPEN und der SQL Access Group. Die Definition der SQL-Syntax für ODBC ist deshalb notwendig, weil fast jeder Datenbankhersteller eine eigene Version der Abfragesprache verwendet. Eine Abstraktion vom Datenbank-Managementsystem kann jedoch nur erfolgen, wenn eine einheitliche Syntax verwendet wird. Die gegebenenfalls notwendige Umsetzung in die datenbankspezifische Variante muss daher vom ODBC-Treiber durchgeführt werden. Die ODBC-Treiberarchitektur ist in Abbildung 4.4 dargestellt.
Applikation
ODBC-Treibermanager
ODBC Oracle Treiber
ODBC Access Treiber
Netzwerk
Netzwerk Oracle Datenbank
ODBC SQL-Server Treiber
Access Datenbank
SQL-Server Datenbank
Abbildung 4.4: Die ODBC-Treiberarchitektur ODBC-Architektur
Die Applikation greift ausschließlich auf den ODBC-Treibermanager zu. Der Treibermanager implementiert die nicht datenbankspezifischen Teile von ODBC. Er verwaltet die unter ihm liegenden datenbankspezifischen ODBC-Treiber und leitet die Aufrufe der Applikation an diese weiter. Es können gleichzeitig mehrere ODBC-Treiber aktiv sein, so dass gleichzeitig auf mehrere Datenbanken zugegriffen werden kann. Die Treiber stellen die Verbindung zu der eigentlichen Datenbank her. Sollte es sich bei der Datenbank um eine Serverdatenbank handeln, die über ein lokales
ODBC
477
Netzwerk angesprochen wird, so übernimmt der ODBC-Treiber die dazu notwendige Kommunikation über das Netzwerk. Natürlich können auch dateiorientierte Datenbanken wie MS Access über ein Netzwerk angesprochen werden. In diesem Fall wird die Netzwerkkommunikation allerdings nicht durch den ODBCTreiber, sondern durch die Dateisystemfunktionen des Betriebssystems durchgeführt. ODBC ist eine Spezifikation, die im Laufe der Zeit verbessert und ergänzt worden ist. Entsprechend gibt es verschiedene Versionen und Levels der Spezifikation. Trotzdem besitzen die Treiber eines Levels nicht immer die gleiche Funktionalität. Man muss sich daher darüber informieren, welche Funktionen ein bestimmter ODBC-Treiber bietet. Wichtige Informationen zum verwendeten ODBC-Treiber lassen sich aus seinem Eintrag in der Registrierungsdatenbank ablesen. Ein ODBCTreiber wird unter dem Schlüssel HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\
in die Registrierungsdatenbank eingetragen. In den Unterschlüsseln APILevel, DriverODBCVer und SQLLevel werden Level des Treibers, Version der
unterstützten ODBC-Spezifikation beziehungsweise unterstützter SQLDialekt des Treibers angegeben (0 = SQL92 Entry, 1 = FIPS127-2 Transitional, 2 = SQL92 Intermediate, 3 = SQL92 Full). Diese Informationen lassen sich auch durch die Funktion SQLGetInfo der ODBC-Schnittstelle erfragen. Die Arbeit mit der ODBC-Schnittstelle läuft normalerweise so ab, dass der Programmierer zunächst eine Datenbankabfrage in SQL formuliert und diese der Datenbank stellt. Daraufhin erhält er von ODBC eine Gruppe von Datensätzen zurückgeliefert, die das Ergebnis der Abfrage darstellen. Innerhalb dieser Satzgruppe kann der Programmierer immer nur auf jeweils einen Datensatz, den aktuellen Datensatz, zugreifen. Der aktuelle Datensatz wird durch den Cursor, einen Zeiger auf den aktuellen Datensatz, angezeigt. Der Cursor ist allerdings kein Zeiger im Sinne der Sprache C oder C++, sondern lediglich eine Positionsanzeige. Mit dem Cursor kann man durch die Datensätze der Satzgruppe navigieren und den jeweils aktuellen Datensatz lesen, ändern oder löschen.
Satzgruppen
Je nach Fähigkeit des verwendeten ODBC-Treibers arbeitet man als Programmierer mit einer von zwei möglichen Arten von Satzgruppen, mit Snapshots oder Dynasets. Snapshots sind Momentauf-
Snapshots und Dynasets
478
4
Datenbankprogrammierung
nahmen der Datenbank. Nach Durchführung einer Abfrage verändern sich die Datensätze der Satzgruppe eines Snapshots nicht mehr. Ergeben sich während der Arbeit mit der Satzgruppe Änderungen an den Daten (beispielsweise durch einen anderen Benutzer der Datenbank, der Datensätze einfügt oder verändert), so werden diese nicht im Snapshot sichtbar. Änderungen an den Daten werden nur sichtbar, wenn die Satzgruppe neu eingelesen wird. Im Gegensatz zu Snapshots lesen Dynasets automatisch veränderte Daten neu ein und reflektieren so immer den aktuellen Datenbestand in der Datenbank. Während Snapshots von den meisten ODBC-Treibern unterstützt werden (oder zumindest durch die Cursorbibliothek emuliert werden), sind Dynasets meist nur durch Level-2-konforme ODBC-Treiber implementiert. Das Level des ODBC-Treibers gibt an, welche Funktionen er unterstützt. Es gibt die Levels 0, 1 und 2, wobei Level 2 am meisten Funktionalität aufweist. Da nicht alle ODBC-Treiber Level-2-konform sind, unterstützen sie oft auch keine Dynasets.
4.2.2
Die MFC-Klassen zur ODBC-Programmierung
Die MFC besitzen vier Klassen, die speziell zur Unterstützung der ODBC-Programmierung vorgesehen sind. Die Klassen CDatabase, CRecordset, CRecordView und CDBException sind in Abbildung 4.5 zu sehen.
CObject CCmdTarget CWnd CView CScrollView CFormView CRecordView CDatabase CRecordset CException CDBException Abbildung 4.5: Die ODBC-Klassen der MFC
ODBC
479
Objekte der Klasse CDatabase repräsentieren Verbindungen zu ODBC-Datenquellen. Sie sind für den Auf- und Abbau von Verbindungen, Timeouts und die Durchführung von Transaktionen zuständig. Auf die Daten einer Datenbank kann man erst zugreifen, wenn durch ein CDatabase-Objekt eine Verbindung zu einer Datenquelle aufgebaut worden ist.
CDatabase
Die Klasse CRecordset dient zum Durchführen von Datenbankabfragen und zur Verwaltung der resultierenden Satzgruppen. Auf die Datensätze einer Satzgruppe kann nur zeilenweise zugegriffen werden. Die aktuelle Datensatzposition (der Cursor) gibt an, auf welche Zeile zugegriffen werden kann. Die aktuelle Datensatzposition lässt sich mit den in Tabelle 4.1 gezeigten Member-Funktionen verändern.
CRecordset
Funktion
Beschreibung
Move
Bewegt die Position des aktuellen Datensatzes entsprechend dem übergebenen Zähler vorwärts oder rückwärts.
MoveFirst
Positioniert den aktuellen Datensatz auf den ersten Eintrag der Satzgruppe.
MoveLast
Positioniert den aktuellen Datensatz auf den letzten Eintrag der Satzgruppe.
MoveNext
Verschiebt den aktuellen Datensatz um eine Position nach hinten.
MovePrev
Verschiebt den aktuellen Datensatz um eine Position nach vorn.
SetAbsolutePosition
Setzt den aktuellen Datensatz auf die als absolute Position übergebene Zahl.
SetBookmark
Setzt den aktuellen Datensatz auf die als Parameter übergebene Position. Positionen können durch Aufruf der Funktion GetBookmark erhalten werden.
Tabelle 4.1: Funktionen, um den aktuellen Datensatz zu verändern
Zu jeder Spalte der Tabelle, auf die ein CRecordset-Objekt zugreift, muss ein Datenelement in der CRecordset-Klasse vorhanden sein. Die Klasse CRecordset kann folglich nicht selbst verwendet werden, sondern es muss eine Klasse von ihr abgeleitet werden, die die notwendigen Datenelemente in Form von Member-Variablen enthält. Diese Member-Variablen enthalten die Werte des aktuellen Datensatzes. Abbildung 4.6 zeigt diesen Zusammenhang. Für
Datenelemente
480
4
Datenbankprogrammierung
die Spalten Spalte1 bis Spalte4 der Datenbank existieren die Member-Variablen m_Spalte1 bis m_Spalte4. Diese Variablen enthalten die Werte des jeweils aktuellen Datensatzes. Satzgruppe Spalte1 Spalte2 Spalte3 Spalte4 ...
Abbildung 4.6: Satzgruppe und Datenelemente der Satzgruppenklasse RFX
Der Datenaustausch zwischen dem aktuellen Datensatz der Satzgruppe und den Member-Variablen der von CRecordset abgeleiteten Klasse erfolgt durch spezielle Datenaustauschfunktionen: die RFX-Funktionen (Record Field Exchange). In ihrer Verwendung haben diese Funktionen durchaus Ähnlichkeit mit den DDXFunktionen zum Austausch von Werten mit Steuerelementen in Dialogfeldern. Aufgerufen werden die RFX-Funktionen in der Funktion DoFieldExchange der von CRecordset abgeleiteten Klasse.
CRecordView
Mit Objekten der Klasse CRecordView lassen sich Datensätze einer ODBC-Quelle in Form von einfachen Masken anzeigen und verändern. Die Klasse CRecordView implementiert eine Formularansicht, bei der Datensätze einer Satzgruppe durch Steuerelemente innerhalb der Ansicht angezeigt und verändert werden können. Die Steuerelemente werden mit dem Ressourceneditor der Entwicklungsumgebung auf einer Dialogfeldvorlage platziert. Die Verbindung zwischen Datensätzen und Steuerelementen kann vollständig durch die Entwicklungsumgebung vorgenommen werden, man muss keine einzige Zeile Programmcode selbst schreiben! Der Datentransfer erfolgt in zwei Schritten: Im ersten Schritt werden Daten zwischen der Datenquelle und einem der Ansicht zugeordneten CRecordset-Objekt per RFX ausgetauscht. In einem zweiten Schritt werden die Daten zwischen diesem Objekt und den Steuerelementen der Ansicht per DDX ausgetauscht. Der Datenaustausch erfolgt in beiden Richtungen, so dass Daten
ODBC
481
innerhalb der Maske verändert werden können. Das in Abschnitt 4.2.4, »Das Beispielprogramm StockODBC«, vorgestellte Beispielprogramm demonstriert die Verwendung der Klasse CRecordView. Treten bei der Verwendung der ODBC-Datenbankklassen Fehler auf, so werden diese in Form von Messageboxen angezeigt. Möchte man selbst eine Fehlerbehandlung implementieren, so muss man Ausnahmen der Klasse CDBException abfangen, die von den ODBC-Klassen der MFC für Fehlermeldungen verwendet werden.
4.2.3
CDBException
Einrichtung von ODBC
Um ODBC zu verwenden, müssen die passenden ODBC-Treiber installiert sein. Bei einer normalen Installation von Visual C++ werden die benötigten Treiber automatisch mitinstalliert. Auch Datenbank-Managementsysteme wie Microsoft Access installieren ODBC-Treiber im System. Ist der ODBC-Treibermanager installiert, so lässt sich in der Systemsteuerung (in Windows 2000 unter Verwaltung) der ODBCDatenquellen-Administrator aufrufen. Dieser ist in Abbildung 4.7 zu sehen.
Abbildung 4.7: Treiber im ODBC-Datenquellen-Administrator
ODBCDatenquellenAdministrator
482
4
Datenbankprogrammierung
Im ODBC-Datenquellen-Administrator kann man sich unter anderem die installierten ODBC-Treiber ansehen – wie in Abbildung 4.7 zu erkennen ist – und Logdateien für Verbindungen zu ODBCDatenbanken anlegen. Die Hauptaufgabe des ODBC-Datenquellen-Administrators ist die Definition von Datenquellen. Datenquellen können einem einzelnen Benutzer oder dem gesamten System zugänglich gemacht werden. Entsprechend gibt es die Registerkarten BENUTZER-DSN und SYSTEM-DSN. DSN steht für Data Source Name (Datenquellenname). Abbildung 4.8 zeigt die Beispieldatenbank AKTIEN.MDB als System-DSN.
Abbildung 4.8: Definition einer System-DSN
Fügt man eine Datenquelle mit dem ODBC-Datenquellen-Administrator hinzu, so ist zunächst der zur Datenbank passende ODBCTreiber auszuwählen. Dies ist in Abbildung 4.9 zu sehen. Anschließend wird die Datenbank selbst gewählt. Dazu muss man den Dialog in Abbildung 4.10 ausfüllen. Im Falle eines dateibasierten, lokalen DBMS wie Microsoft Access ist dazu der vollständige Dateipfad der Datenbank anzugeben. Der Datenquelle muss ein Name gegeben werden und es kann optional eine Beschreibung
ODBC
483
der Datenquelle angegeben werden. Hinter den Schaltflächen ERWEITERT und OPTIONEN befinden sich weitere Einstellmöglichkeiten zur Datenquelle.
Abbildung 4.9: Auswahl des ODBC-Treibers
Abbildung 4.10: Name, Beschreibung und weitere Optionen einer ODBC-Datenquelle
Die ODBC-Einstellungen werden in der Windows-Registrierungsdatenbank gespeichert. Die ODBC-Einstellungen findet man unter dem Schlüssel: HKEY_LOCAL_MACHINE\SOFTWARE\ODBC
Registrierungsdatenbank
484
4
Datenbankprogrammierung
Die mit dem ODBC-Datenquellen-Administrator angelegten Datenquellen befinden sich im untergeordneten Schlüssel ODBC.INI. Die im System installierten ODBC-Treiber findet man dagegen unter dem Schlüssel ODBCINST.INI. Abbildung 4.11 zeigt den Eintrag der Datenquelle Aktien in der Registrierungsdatenbank.
Abbildung 4.11: ODBC-Einträge in der Registrierungsdatenbank
t
Eine Datenquelle sollte definiert werden, bevor man mit dem Anwendungs-Assistenten das zugehörige Visual C++-Projekt erzeugt, damit man die Datenquelle beim Anlegen des Projekts auswählen kann.
4.2.4
Das Beispielprogramm StockODBC
Das Programm StockODBC ist ein Programm, mit dem sich Datensätze der Tabelle Aktien aus der Beispieldatenbank anzeigen, verändern, löschen und hinzufügen lassen. Die Datensätze werden in einer einfachen Eingabemaske angezeigt und bearbeitet. Abbildung 4.12 zeigt das Programm StockODBC. Das Feld »Aktiennummer« wird lediglich angezeigt und kann nicht verändert werden, da es sich um den automatisch generierten Primärschlüssel handelt. Das Beispielprogramm ist mit dem Anwendungs-Assistenten angelegt worden. Unter DATENBANKUNTERSTÜTZUNG wurde im Anwendungs-Assistenten die Option DATENBANKANSICHT OHNE DATEISUPPORT ausgewählt. Diese Option bedeutet, dass die Serialisierungsmechanismen der MFC nicht in die Dokumentenklasse des erzeugten Programms aufgenommen werden. Daher besitzt StockODBC keine Menüs zum Laden und Speichern von Dateien.
ODBC
485
Der Anwendungs-Assistent bietet als Alternative die Möglichkeit, ein Programm zu erzeugen, das sowohl auf eine Datenbank zugreifen, als auch Daten serialisieren kann. In diesem Fall muss man die Option DATENBANKANSICHT MIT DATEISUPPORT auswählen. Als Client-Typ wird ODBC angegeben.
Abbildung 4.12: Das Beispielprogramm StockODBC
Abbildung 4.13: Auswahl der Datenquelle im Anwendungs-Assistenten
Durch einen Klick auf die Schaltfläche DATENQUELLE wird anschließend die Datenbank ausgewählt, mit der das Programm arbeiten soll. Das in Abbildung 4.13 gezeigte Dialogfeld ermög-
Auswahl der Datenbank
486
4
Datenbankprogrammierung
licht die Wahl einer ODBC-Datenquelle. Nachdem man hier eine Datenquelle ausgewählt hat, folgt ein Passwortdialogfeld, das man einfach mit OK bestätigen kann, sofern die ausgewählte Datenquelle keinen Zugangsschutz besitzt. Auswahl der Tabellen
Nach Bestätigung des Passwortdialogfelds wird man in einem weiteren Dialogfeld aufgefordert, die Datenbanktabellen auszuwählen, die im Programm angesprochen werden sollen. Dieses Dialogfeld ist in Abbildung 4.14 zu sehen.
Abbildung 4.14: Dialogfeld zur Auswahl der Datenbanktabellen
Für das Beispielprogramm wurde hier nur die Tabelle Aktie ausgewählt, da auf die Tabelle Kurs in diesem Programm nicht zugegriffen werden soll. Zurück im Anwendungs-Assistenten sollte man anschließend noch die Option ALLE SPALTEN BINDEN auswählen, damit Datenaustauschvariablen für die Spalten der ausgewählten Tabellen erzeugt werden. Lässt man diese Option weg, so legt der Anwendungs-Assistent keine Datenaustauschvariablen an. Außerdem muss zum Schluss noch der gewünschte Satzgruppentyp ausgewählt werden. Hier stehen die bereits beschriebenen Typen Snapshot und Dynaset zur Auswahl. Für das Beispielprogramm StockODBC wurde der Satzgruppentyp Snapshot ausgewählt. Auswahl der Ansichtsklasse
Hat man die Auswahl von Datenquelle und Datenbanktabelle beendet, dann kann man weitere Einstellungen im AnwendungsAssistenten vornehmen. Unter ERSTELLTE KLASSEN ist bereits zu
ODBC
487
sehen, dass der Anwendungs-Assistent als Basisklasse der Ansicht die Klasse CRecordView gewählt hat. Außerdem fügt der Anwendungs-Assistent eine Dialogfeldressource in das Projekt ein, auf der die Steuerelemente der Ansicht platziert werden müssen. Nachdem der Anwendungs-Assistent das Projekt erzeugt hat, sind mit dem Ressourceneditor vier Eingabefelder zu der zur Ansicht gehörenden Dialogfeldressource hinzugefügt worden. Diese vier Eingabefelder sollen jeweils den aktuellen Datensatz der Tabelle Aktie anzeigen: Aktiennummer, Aktienname, Wertpapierkennnummer und Tickersymbol. Die Aktiennummer wird automatisch von der Datenbank vergeben und kann somit nicht verändert werden. Daher wurde das Eingabefeld für Aktiennummer im Ressourceneditor schreibgeschützt. Dadurch wird es automatisch grau hinterlegt. Die Verbindung zwischen den Steuerelementen und den Feldern der Datenbank wird später im Programmcode innerhalb der Funktion DoDataExchange per DDX vorgenommen.
Steuerelemente hinzufügen
Aufgrund der Angaben, die im Anwendungs-Assistenten unter DATENBANKUNTERSTÜTZUNG gemacht worden sind, ist für das Programm StockODBC automatisch die Satzgruppe CStockODBCSet angelegt worden. Die Ansichtsklasse CStockODBCView hält einen Zeiger auf die Satzgruppe in der Member-Variablen m_pSet. Über diesen Zeiger werden die Datenelemente der Satzgruppe angesprochen.
4.2.5
Die vordefinierten Funktionen der Klasse CRecordView
Ein mit dem Anwendungs-Assistenten generiertes Datenbankprogramm, das eine von CRecordView abgeleitete Klasse verwendet, implementiert bereits vier Befehle, um zwischen den Datensätzen einer Satzgruppe zu navigieren. Man kann sich schrittweise durch die Datensätze vor- und zurückbewegen sowie zum ersten und zum letzten Datensatz der Satzgruppe springen. Diese Befehle sind durch Symbole in der Symbolleiste und durch Menüeinträge verfügbar. Implementiert wird diese vorgefertigte Navigation in der Funktion CRecordView::OnMove. Listing 4.2 zeigt die Implementierung der Funktion OnMove aus der Datei DBVIEW.CPP. Die Datei DBVIEW.CPP ist Teil des Sourcecodes der MFC und befindet sich im Verzeichnis MICROSOFT VISUAL STUDIO .NET\VC7\ATLMFC\SRC\MFC. Die Funktion zeigt, wie man sich zwischen den Sätzen einer Satzgruppe bewegt.
OnMove
488
4
Datenbankprogrammierung
BOOL CRecordView::OnMove(UINT nIDMoveCommand) { CRecordset* pSet = OnGetRecordset(); if (pSet->CanUpdate() && !pSet->IsDeleted()) { pSet->Edit(); if (!UpdateData()) return TRUE; pSet->Update(); } switch (nIDMoveCommand) { case ID_RECORD_PREV: pSet->MovePrev(); if (!pSet->IsBOF()) break; case ID_RECORD_FIRST: pSet->MoveFirst(); break; case ID_RECORD_NEXT: pSet->MoveNext(); if (!pSet->IsEOF()) break; if (!pSet->CanScroll()) { // clear out screen since we're sitting on EOF pSet->SetFieldNull(NULL); break; } case ID_RECORD_LAST: pSet->MoveLast(); break; default: // Unexpected case value ASSERT(FALSE); } // Show results of move operation UpdateData(FALSE); return TRUE; } Listing 4.2: Die MFC-Implementierung von CRecordView::OnMove
ODBC
Die Funktion OnMove bekommt den Navigationsbefehl, den sie ausführen soll, als Konstante übergeben. Entsprechend wird die aktuelle Datensatzposition auf den ersten, letzten, nächsten oder vorherigen Satz gesetzt. Zu Beginn der Funktion wird die Satzgruppe bestimmt, auf der gearbeitet werden soll. Eine von CRecordView abgeleitete Klasse bestimmt die ihr zugeordnete Satzgruppe durch einen Aufruf der Funktion OnGetRecordset. Diese Funktion liefert einen Zeiger auf ein Objekt der Klasse CRecordset zurück. Der erste Teil der Funktion OnMove (vor der switch-Anweisung) ist für das Zurückschreiben des alten Datensatzes zuständig, also des Datensatzes, der den aktuellen Datensatz beim Eintritt in die Funktion OnMove darstellt. Programme, die eine von CRecordView abgeleitete Klasse zur Anzeige und Veränderung von Daten verwenden, haben die etwas ungewöhnliche Eigenschaft, dass ein veränderter Datensatz erst dann in die Datenbank zurückgeschrieben wird, wenn man sich von ihm wegbewegt. Daher prüft die Funktion OnMove zunächst durch Aufruf der Funktionen CRecordset::CanUpdate und CRecordset::IsDeleted, ob sich der aktuelle Datensatz in die Datenbank zurückschreiben lässt und ob er nicht als gelöscht gekennzeichnet worden ist. Sind beide Bedingungen erfüllt, dann wird der Datensatz zur Veränderung geöffnet (Aufruf von CRecordset::Edit). Die neuen Daten werden durch den Aufruf von CView::UpdateData per DDX aus der Eingabemaske übernommen und durch den Aufruf von CRecordset::Update in die Datenbank geschrieben. Nach dem Zurückschreiben des alten Datensatzes wird der Cursor auf den neuen Datensatz positioniert. Dazu werden die Funktionen MoveFirst, MoveLast, MoveNext und MovePrev der Klasse CRecordset verwendet. Die Implementierung arbeitet etwas unkonventionell, da das Programm beim Positionieren des Cursors vor den ersten oder hinter den letzten Datensatz in den nächsten case-Teil der switch-Anweisung »durchfällt«, um dann den Cursor auf den ersten oder letzten Datensatz der Satzgruppe zu positionieren. Die dabei verwendete Funktion IsBOF prüft, ob eine Satzgruppe leer ist oder man sich vor dem ersten Datensatz der Satzgruppe befindet. Die Funktion IsEOF testet, ob sich die aktuelle Position hinter dem letzten Datensatz befindet. Der Aufruf von SetFieldNull mit dem Parameter NULL markiert die ganze Satzgruppe als ungültig, für den Fall, dass man sich hinter dem letzten Datensatz befindet und der verwendete ODBC-Treiber
489
490
4
Datenbankprogrammierung
kein Scrolling zulässt, man sich also nicht mehr rückwärts bewegen kann. Der abschließende Aufruf der Funktion CView::UpdateData mit dem Parameter false überträgt die Werte des neuen Datensatzes per DDX in die Steuerelemente der Ansicht. Neben der gerade besprochenen, durch die Klasse CRecordView vordefinierten, Funktionalität sind im Programm StockODBC zwei zusätzliche Funktionen implementiert worden. Durch zwei zusätzliche Menüeinträge im Programm StockODBC lassen sich neue Datensätze anlegen und bestehende Datensätze löschen. Implementiert wird die Funktionalität zum Anlegen und Löschen der Datensätze in der Ansichtsklasse. Der Programmcode der Ansichtsklasse wird in Listing 4.3 gezeigt. ////////////////////////////////////////////////////////////// // CStockODBCView IMPLEMENT_DYNCREATE(CStockODBCView, CRecordView) BEGIN_MESSAGE_MAP(CStockODBCView, CRecordView) ON_COMMAND(ID_RECORD_NEW, OnRecordNew) ON_COMMAND(ID_RECORD_DELETE, OnRecordDelete) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// // CStockODBCView Konstruktion/Destruktion CStockODBCView::CStockODBCView() : CRecordView(CStockODBCView::IDD) { m_pSet = NULL; m_bNewRecord = false; } CStockODBCView::~CStockODBCView() { } void CStockODBCView::DoDataExchange(CDataExchange* pDX) { CRecordView::DoDataExchange(pDX); DDX_FieldText(pDX, IDC_EDIT_STOCKNAME, m_pSet->m_Aktienname, m_pSet); DDX_FieldText(pDX, IDC_EDIT_TICKER, m_pSet->m_Tickersymbol, m_pSet);
////////////////////////////////////////////////////////////// // CStockODBCView Nachrichten-Handler void CStockODBCView::OnRecordNew() { // Satzgruppe besorgen: CStockODBCSet* pSet = (CStockODBCSet*)OnGetRecordset(); // Wenn Änderungen am aktuellen Datensatz gemacht // wurden, diesen erst sichern: if ( pSet->CanUpdate() && // Kann ändern !pSet->IsBOF() && // Ist nicht leer !pSet->IsDeleted()) // Datensatz nicht gelöscht { // Ändern: pSet->Edit(); // Daten aus der Ansicht holen: UpdateData(); // Und zurückschreiben: pSet->Update(); } // Leerer Datensatz: pSet->m_Aktienname pSet->m_Aktiennummer pSet->m_Tickersymbol pSet->m_WKN
= = = =
_T(""); -1; _T(""); 0;
// Neuen Datensatz vormerken: m_bNewRecord = true; // Daten in die Ansicht schreiben: UpdateData (false); } void CStockODBCView::OnRecordDelete() { // Datensatz besorgen CRecordset* pSet = OnGetRecordset(); if (pSet->CanUpdate() && // kann ändern !pSet->IsBOF ()) // Ist nicht leer pSet->Delete(); else
ODBC
493 return; if (pSet->CanRestart()) pSet->Requery(); // neu einlesen
} BOOL CStockODBCView::OnMove(UINT nIDMoveCommand) { if (m_bNewRecord) { // Satzgruppe besorgen: CStockODBCSet* pSet = (CStockODBCSet*)OnGetRecordset(); CString strShareName, strTicker; int nWKN; // Daten aus der Ansicht holen UpdateData(); // Werte sichern strShareName = pSet->m_Aktienname; strTicker = pSet->m_Tickersymbol; nWKN = pSet->m_WKN; // Datensatz erzeugen: pSet->AddNew(); // Daten zuweisen: pSet->m_Aktienname = strShareName; pSet->m_Tickersymbol = strTicker; pSet->m_WKN = nWKN; // Und zurückschreiben: pSet->Update(); if (pSet->CanRestart()) { pSet->Requery(); // Auf (vermutlich) neuen Datensatz positionieren: pSet->MoveLast(); } m_bNewRecord = false; // Mit Ansicht abgleichen: UpdateData (false); } return CRecordView::OnMove(nIDMoveCommand); } Listing 4.3: Die Ansichtsklasse CStockODBCView
494
4
Datenbankprogrammierung
Im Listing der Ansichtsklasse ist zunächst die Definition der Nachrichtenzuordnungstabelle zu sehen, in der sich zwei Einträge für die Funktionen OnRecordNew zum Anlegen eines neuen Datensatzes und OnRecordDelete zum Löschen eines Datensatzes befinden. In der Funktion DoDataExchange sind die Aufrufe der DDX-Funktionen zum Datenaustausch mit den Steuerelementen zu sehen. Die DDXFunktionen führen den Datenaustausch der Steuerelemente direkt mit den Feldern der Satzgruppe durch, so dass an dieser Stelle keine weiteren Variablen benötigt werden! In der anschließenden Funktion OnInitialUpdate ist zu sehen, dass die Satzgruppe – ein Objekt der Klasse CStockODBCSet – nicht in der Ansicht, sondern im Dokument des Programms gespeichert wird. Durch Aufruf der Funktion GetDocument wird auf das Dokument zugegriffen, die Satzgruppe ist darin als öffentliche Variable m_stockODBCSet gespeichert. In der Member-Variablen m_pSet wird die Adresse der Satzgruppe gesichert, um später auf diese zugreifen zu können. Die sich anschließenden Aufrufe der Funktionen CFrameWnd::RecalcLayout und ResizeParentToFit passen die Größe von Ansicht und Rahmenfenster an die Größe der verwendeten Dialogvorlage an. OnGetRecordset
Die virtuelle Funktion OnGetRecordset muss vom Programmierer überschrieben werden. Die Funktion gibt einen Zeiger auf die von der Ansicht verwendete Satzgruppe zurück. Mit dem Anwendungs-Assistenten erzeugte Programme besitzen diese Funktion bereits. Es wird der in OnInitialUpdate gespeicherte Zeiger zurückgegeben.
Datensätze hinzufügen
Mit der Funktion OnRecordNew wird das Einfügen eines neuen Datensatzes in die Datenbank vorbereitet. Es soll dabei das gleiche Schema implementiert werden, wie es auch der vom Anwendungs-Assistenten generierte Programmcode verwendet: Der neue Datensatz wird erst dann in die Datenbank übernommen, wenn der Benutzer sich von diesem wegbewegt. Ob dies für eine Datenbankanwendung sinnvoll ist, sei dahingestellt. Es ist allerdings konform mit dem vom Anwendungs-Assistent generierten Programmcode. Eine andere denkbare Vorgehensweise wäre es, spezielle Ändern- und Einfüge-Schaltflächen vorzusehen. Dazu müsste allerdings die vom Anwendungs-Assistenten vorgegebene Implementierung geändert werden. Entsprechend der vom Anwendungs-Assistenten vorgegebenen Logik fügt die Funktion OnRecordNew selbst keinen Datensatz in die Datenbank ein. Die Funktion löscht lediglich die Eingabe-
ODBC
maske. Der neue Datensatz wird anschließend in der Funktion OnMove eingefügt. Die Implementierung der Funktion OnRecordNew fordert durch Aufruf der Funktion OnGetRecordset zunächst einen Zeiger auf das Satzgruppenobjekt an. Bevor OnRecordNew die Daten des gerade angezeigten Datensatzes aus der Ansicht löscht, werden die Daten in die Datenbank zurückgesichert, denn schließlich kann der Benutzer sie verändert haben. Das Update wird nur ausgeführt, wenn Änderungszugriff auf die Datenbank besteht (CanUpdate), wenn die Satzgruppe nicht leer ist (!IsBOF) und wenn der aktuelle Datensatz nicht als gelöscht gekennzeichnet ist (!IsDeleted). Nicht zurückschreiben lässt sich ein Datensatz beispielsweise, wenn die Datenbank im Nur-Lesemodus (readonly) geöffnet worden ist. Das Update selbst wird durch Aufruf der Funktion Edit eingeleitet. Nun können die Daten des aktuellen Datensatzes verändert werden. Sie werden durch Aufruf der Funktion UpdateData per DDX aus der Ansicht geholt. Der Aufruf von Update schreibt die veränderten Daten in die Datenbank zurück. Nachdem die alten Daten gesichert sind, können die Felder der Eingabemaske gelöscht werden. Dazu werden den DDX-Variablen aus der Satzgruppe leere Strings und der Wert 0 für die Wertpapierkennnummer zugewiesen. Der Aktiennummer wird der Wert -1 zugewiesen, um deutlich zu machen, dass der Datensatz noch nicht in der Datenbank gespeichert ist. Der Aufruf von UpdateData mit dem Parameter false überträgt die Werte in die Ansicht. Es muss nicht befürchtet werden, dass diese Werte in die Datenbank übernommen werden, da ja keine Datenbankfunktionen (wie beispielsweise Update) zum Sichern des Datensatzes aufgerufen werden. Schließlich wird noch die Variable m_bNewRecord auf den Wert true gesetzt, um zu signalisieren, dass sich in der Ansicht ein Datensatz befindet, für den es noch keinen Eintrag in der Datenbank gibt. In die Datenbank übernommen wird der neue Datensatz erst beim nächsten Aufruf der Funktion OnMove. Damit OnMove den neuen Datensatz in die Datenbank übernehmen kann, muss die Funktion in der Ansichtsklasse überschrieben werden. Die überschriebene Version der Funktion OnMove prüft zunächst den Wert der Variablen m_bNewRecord. Ist der Wert auf true gesetzt, dann ist das ein Signal, die Daten der Ansicht als einen neuen Datensatz in die Datenbank zu schreiben. Zunächst werden dazu die Daten aus der Ansicht per DDX durch den Aufruf der Funktion UpdateData
495
496
4
Datenbankprogrammierung
geholt. Diese Werte müssen zwischengespeichert werden, sie können nicht direkt von der Ansicht in die Datenbank geschrieben werden, da es dann zu Problemen mit dem automatisch generierten Primärschlüssel kommen kann (der in der Ansicht im Feld AKTIENNUMMER gespeicherte Wert -1 würde als Primärschlüssel übernommen werden). Um einen neuen Datensatz anzulegen, wird die Funktion AddNew aufgerufen. Diese Funktion legt den Datensatz an. Nun können den Variablen der Satzgruppe die Werte des neuen Datensatzes zugewiesen werden. Erst der Aufruf von Update schreibt diese Werte dann in die Datenbank. Die Datenbank generiert automatisch den Primärschlüssel. Um den neuen Datensatz in die Satzgruppe zu übernehmen, muss diese neu eingelesen werden. Dies geschieht durch den Aufruf der Funktion Requery. Nicht alle Treiber unterstützen diese Funktion. Ob die Funktion ausgeführt werden kann, wird zuvor durch den Aufruf der Funktion CanRestart festgestellt. Danach wird der Cursor durch Aufruf der Funktion MoveLast auf den letzten Datensatz positioniert. Dieser Aufruf dient dazu, um nach Requery wieder einen definierten Datensatz anzuzeigen. Die Tabelle Aktien der Beispieldatenbank verwendet einen Primärschlüssel, der automatisch vergeben wird. Der Schlüssel Aktiennummer besteht aus einer Integerzahl, die für jeden neuen Datensatz um 1 erhöht (inkrementiert) wird. Der Datensatz mit der jeweils höchsten Aktiennummer ist also der zuletzt eingefügte Datensatz. Da beim Öffnen der Satzgruppe keine Angaben zur Sortierung gemacht wurden, sind die Datensätze entsprechend des Primärschlüssels aufsteigend sortiert. Hat in der Zwischenzeit kein anderer Benutzer einen Datensatz eingefügt, so ist der letzte Datensatz auch der neu eingefügte Datensatz. Der abschließende Aufruf der Funktion UpdateData übernimmt die Daten des aktuellen Datensatzes in die Ansicht. Nun erst wird die Funktion OnMove der Basisklasse aufgerufen, die zum angegebenen Datensatz navigiert. Datensätze löschen
Die Funktion OnRecordDelete wird aufgerufen, wenn ein Datensatz gelöscht werden soll. Durch Aufruf der Funktion OnGetRecordset wird zunächst ein Zeiger auf die Satzgruppe angefordert. Durch Aufruf der Funktion CanUpdate wird dann geprüft, ob überhaupt ändernder Zugriff auf die Datenbank erlaubt ist und sich der Datensatz damit löschen lässt. Sollte ein ändernder Zugriff nicht möglich sein, so wird die Funktion verlassen. Der
ODBC
Datensatz wird durch Aufruf der Funktion Delete gelöscht. Im Gegensatz zu den Funktionen Edit und AddNew wird bei Delete die Funktion Update nicht aufgerufen. Nach dem Löschen sorgt der Aufruf von Requery dafür, dass der gelöschte Datensatz aus der Satzgruppe entfernt wird. Verwendet man einen alten ODBC-Treiber, der diese Funktion nicht unterstützt, so bleibt der gelöschte Datensatz in der Satzgruppe, ist aber als gelöscht markiert. Der abschließende Aufruf von CView::UpdateData teilt die Änderung der Ansicht mit. Die Klasse CStockODBCSet verwaltet die in der Ansicht verwendete Satzgruppe. Implementiert wird die Klasse CStockODBCSet in der Datei STOCKODBCSET.CPP, die in Listing 4.4 zu sehen ist. ////////////////////////////////////////////////////////////// // CStockODBCSet Implementierung IMPLEMENT_DYNAMIC(CStockODBCSet, CRecordset) CStockODBCSet::CStockODBCSet(CDatabase* pdb) : CRecordset(pdb) { m_Aktiennummer = 0; m_Aktienname = _T(""); m_WKN = 0; m_Tickersymbol = _T(""); m_nFields = 4; m_nDefaultType = snapshot; } CString CStockODBCSet::GetDefaultConnect() { return _T("ODBC;DSN=Aktien"); } CString CStockODBCSet::GetDefaultSQL() { return _T("[Aktie]"); } void CStockODBCSet::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T("[Aktiennummer]"), m_Aktiennummer); RFX_Text(pFX, _T("[Aktienname]"), m_Aktienname); RFX_Long(pFX, _T("[WKN]"), m_WKN); RFX_Text(pFX, _T("[Tickersymbol]"), m_Tickersymbol); }
Im Konstruktor der Klasse sieht man, wie die vier Datenelemente der Satzgruppe initialisiert werden. Die Member-Variable m_nFields ist ein Zähler und gibt die Anzahl der Datenelemente an. Der Wert dieser Variablen muss korrekt gesetzt sein, damit der Datenaustausch durch RFX funktioniert. Nachdem die Datenelemente initialisiert sind, wird der Typ der Satzgruppe gesetzt, im Beispiel auf Snapshot. Alternativen wären Dynaset für eine dynamische Satzgruppe oder Forwardonly im Fall eines einfachen ODBC-Treibers, der kein freies Bewegen durch die Satzgruppe zulässt. GetDefaultConnect und GetDefaultSQL
An den Konstruktor schließen sich die Definitionen der Funktionen GetDefaultConnect und GetDefaultSQL an. Die beiden Funktionen stellen die Verbindung zur Datenbank und zur verwendeten Datenbanktabelle her. Die Funktion GetDefaultSQL muss implementiert werden, sie gibt die zu verwendende Datenbanktabelle an. Die verwendeten eckigen Klammern sind optional; sie müssen aber verwendet werden, wenn der Name der Datenbanktabelle Leerzeichen enthält. Die Funktion GetDefaultConnect gibt die zu verwendende Datenquelle an. Im Beispielprogramm wird der Name der Datenquelle fest vorgegeben. Legt man ein Programm mit dem Anwendungs-Assistenten an, dann wird GetDefaultConnect auf diese Weise implementiert. Zur Programmentwicklung ist eine fest kodierte Datenquelle durchaus nützlich, im fertig gestellten Programm ist unter Umständen mehr Flexibilität gefordert. Es besteht dann die Möglichkeit, einen eigenen Auswahlmechanismus zu implementieren oder die in den MFC bereits vorgegebene Implementierung zu verwenden. Da
ODBC
499
GetDefaultConnect eine virtuelle Funktion ist, muss man sie nur in der von CRecordset abgeleiteten Klasse löschen, um die von der Klasse CRecordset vorgegebene Implementierung zu verwenden. Diese Implementierung zeigt ein Dialogfeld an, mit dem der Benutzer eine Datenquelle auswählen oder eine neue Datenquelle definieren kann. Im Anschluss wird die Funktion DoFieldExchange definiert. Diese Funktion implementiert den Datenaustausch durch RFX. Der Datenaustausch wird über ein Objekt der Klasse CFieldExchange abgewickelt. Ähnlich wie bei DDX gibt es eine Reihe von RFX-Funktionen, die den Datenaustausch für unterschiedliche Datentypen implementieren. Im Listing 4.4 sind die Funktionen RFX_Long und RFX_Text zu sehen, die den Datenaustausch für Variablen der Typen long und CString durchführen.
4.2.6
Das Beispielprogramm ODBCChart
Das gerade vorgestellte Beispielprogramm StockODBC verwendet die Klasse CRecordView, um Datensätze in einer Datenbankmaske anzuzeigen und Dateneingaben über diese Maske zuzulassen. Datenbankanwendungen, die sich dem Benutzer in Form einer oder mehrerer Eingabemasken präsentieren, können normalerweise mit Werkzeugen wie Microsoft Access oder Visual Basic viel einfacher und schneller erstellt werden, als dies mit Visual C++ und den MFC möglich ist. Als Folge davon wird man für maskenorientierte Programme eher diese Werkzeuge verwenden, statt Datenbankmasken mit Visual C++ zu programmieren. Visual C++ wird dann als Werkzeug verwendet, wenn mit den Daten einer Datenbank komplexe Aufgaben durchgeführt werden sollen, die sich mit den Mitteln eines Datenbank-Managementsystems wie Microsoft Access nicht oder nur schwer realisieren lassen. Als Beispiel für eine Anwendung, die nicht auf Basis der Klasse CRecordView erstellt worden ist, wird das Programm ODBCChart vorgestellt. ODBCChart ähnelt dem Programm StockChart aus Kapitel 2, »Einstieg in die MFC-Programmierung«, allerdings mit dem Unterschied, dass die Daten einer Datenbank als Grundlage für die Erstellung von Aktiencharts verwendet werden. Das Programm ODBCChart ist in Abbildung 4.15 zu sehen.
DoFieldExchange
500
4
Datenbankprogrammierung
Abbildung 4.15: Das Beispielprogramm ODBCChart
Das Programm ODBCChart ist ein Anzeigeprogramm, es können keine Daten verändert werden. Im Dateimenü und in der Symbolleiste sind die Einträge zum Anlegen und Speichern von Dateien entfernt worden. Der Menüeintrag ÖFFNEN öffnet keine Datei, sondern ein Dialogfeld, mit dem sich eine Aktie aus der Datenbank auswählen lässt. Dieses Dialogfeld ist in Abbildung 4.15 zu sehen. Auf der Basis der ausgewählten Aktie wird eine Datenbankabfrage durchgeführt. Die dabei erhaltenen Daten werden entgegengenommen und als Chart angezeigt. Das Beispielprogramm ODBCChart ist mit dem AnwendungsAssistenten erstellt worden. Im Anwendungs-Assistenten wurde bei DATENBANKUNTERSTÜTZUNG die Datenbankoption NUR HEADER-DATEIEN ausgewählt. Damit können die Datenbankklassen der MFC im Projekt verwendet werden. Es wird allerdings keine Ansichtsklasse zur Anzeige von Datensätzen erzeugt. Damit ist der Datenbankzugriff nicht an die Darstellung des Programms gekoppelt. Auch erzeugt der Anwendungs-Assistent in diesem Fall keine Satzgruppenklasse. Diese muss anschließend hinzugefügt werden. Auf der Karteikarte ERWEITERTE FEATURES des Anwendungs-Assistenten ist die Anzahl der Einträge der zuletzt verwendeten Dateien auf 0 gesetzt worden. Dadurch kommt die Liste der zuletzt verwendeten Dateien nicht zum Einsatz.
ODBC
Nachdem der Anwendungs-Assistent das Projekt erzeugt hat, sind in der Klassenansicht zwei Klassen zur Behandlung von Satzgruppen hinzugefügt worden. Zur Erzeugung einer ODBC-Satzgruppenklasse öffnet man durch einen Klick mit der rechten Maustaste das Kontextmenü im Projektmappen-Explorer und wählt dort HINZUFÜGEN | KLASSE HINZUFÜGEN.... Für den Typ der neuen Klasse wählt man dann unter Visual C++ | MFC das Symbol MFC-ODBC-Consumer (Abbildung 4.16) aus. Danach muss der in Abbildung 4.17 gezeigte Dialog ausgefüllt werden.
Abbildung 4.16: MFC Satzgruppenklasse hinzufügen
Abbildung 4.17: MFC Satzgruppenklasse anlegen
501 Satzgruppen hinzufügen
502
4
Datenbankprogrammierung
Der MFC-ODBC-Consumer-Asisstent legt automatisch eine von der Basisklasse CRecordset abgeleitete Satzgruppenklasse an. Die Vorgehensweise ist dabei analog zu der des Anwendungs-Assistenten (siehe 4.2.4, »Das Beispielprogramm StockODBC«). Die Auswahl von Datenquelle und Datenbanktabelle erfolgt mittels der gleichen Dialogfelder (siehe Abbildungen 4.13 und 4.14). Das Beispielprogramm ODBCChart besitzt zwei mit dem MFCODBC-Consumer-Assistenten erzeugte Klassen zur Verwaltung von Satzgruppen. Die Klasse CShares verwaltet den Zugriff auf die Tabelle Aktie. Damit werden alle Aktien im Dialog DATEI | ÖFFNEN angezeigt. Die Klasse CQuotes bildet eine Satzgruppe über die beiden Tabellen Aktie und Kurs. Eine Satzgruppe über mehrere Tabellen lässt sich mit Hilfe des Assistenten nicht direkt anlegen, da man nur eine Tabelle zur Zeit auswählen kann. Man lässt sich daher vom Assistenten eine Satzgruppenklasse für eine der verwendeten Tabellen anlegen und ergänzt die Member-Funktionen, das SQL-Statement und die Datenaustauschfunktion um die fehlenden Attribute von Hand. Die beiden Satzgruppenklassen werden zur Implementierung von zwei Datenbankabfragen verwendet. Die erste Abfrage, sie basiert auf der Klasse CShares, erzeugt die Liste aller Aktien der Beispieldatenbank. Diese Liste wird im Dialogfeld DATEI | ÖFFNEN angezeigt. Entsprechend wird diese Abfrage in der Dialogfeldklasse des Auswahldialogfelds implementiert. Die zweite Abfrage, sie basiert auf der Klasse CQuotes, ermittelt alle Kurswerte einer zuvor vom Benutzer ausgewählten Aktie. Diese Abfrage wird beim Erzeugen eines Dokumentenobjekts des Programms vorgenommen. Die Daten der Abfrage werden im Dokument gespeichert, so dass beim Neuzeichnen der Ansichten eines Aktiencharts keine erneute Datenbankabfrage notwendig wird. Die Implementierung dieser Datenbankabfrage erfolgt in der Dokumentenklasse. Im Folgenden sollen beide Abfragen beschrieben werden, den Anfang macht die Ermittlung aller Aktiennamen im Dialogfeld DATEI | ÖFFNEN. Verwendung von OnNewDocument
Um das Dialogfeld zur Auswahl einer Aktie in das Programm ODBCChart zu integrieren, ist ein kleiner Kniff angewandt worden. Zunächst wurden die Menüpunkte und Symbole zum Speichern und zur Neuanlage von Dateien aus dem Programm entfernt. Der Menüpunkt DATEI | ÖFFNEN wurde so geändert, dass er keine Dateien mehr öffnet, sondern den Dialog zur Aus-
ODBC
wahl einer Aktie anzeigt. Die IDs des Menüeintrags und des entsprechenden Symbols wurden dazu im Ressourceneditor von ID_FILE_OPEN auf ID_FILE_NEW geändert. Ausschlaggebend ist, dass nun die Funktion OnNewDocument der Dokumentenklasse für diesen Menüpunkt überschrieben werden kann. Die Implementierung von OnNewDocument legt ein Paar aus Dokument und Ansicht an, ohne das Dialogfeld zum Öffnen einer Datei anzuzeigen. In OnNewDocument kann daher der Programmcode zum Öffnen des Auswahldialogfelds platziert und anschließend die Implementierung von OnNewDocument der MFC aufgerufen werden, um Dokumenten- und Ansichtobjekt zu erzeugen. Hätte man die ID des Menüpunkts unverändert gelassen, so müsste man den Dialog zur Auswahl einer Aktie in OnOpenDocument implementieren. Für die Ressourcen-ID ID_FILE_OPEN rufen die MFC den Dialog zum Öffnen einer Datei auf, auch wenn man OnOpenDocument überschreibt und die Funktion der Basisklasse nicht aufruft. Daher erscheint die Implementierung des Dialogfelds zur Auswahl einer Aktie in OnNewDocument einfacher. Alternativ könnte man in einer eigenen Funktion selbst Dokument, Ansicht und MDI-Rahmen erzeugen. Diese Aufgabe übernimmt OnNewDocument aber schon. Das Dialogfeld zur Aktienauswahl wird durch die Klasse CStockChooser implementiert. Die innerhalb dieser Klasse durchgeführte Datenbankabfrage verwendet die Klasse CShares, um die Abfrage durchzuführen und die resultierende Satzgruppe darzustellen. Die Klasse CShares ist – wie bereits beschrieben – mit dem Assistenten für ODBC-Consumer erzeugt worden. Listing 4.5 zeigt die Header-Datei dieser Klasse. // Shares.h : Header-Datei // ////////////////////////////////////////////////////////////// / // Satzgruppe CShares class CShares : public CRecordset { public: CShares(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CShares) // Feld-/Parameterdaten long m_Aktiennummer;
503
504
4
Datenbankprogrammierung
CString m_Aktienname; long m_WKN; CString m_Tickersymbol;
Im Listing sind die Datenelemente der Satzgruppe für Aktiennummer, Aktienname, Wertpapierkennnummer und Tickersymbol zu sehen. Entgegen der Konvention dieses Buchs besitzen diese Variablen deutsche Bezeichner, da sie vom ODBC-Consumer-Assistenten automatisch nach den Feldnamen der Tabelle Aktie benannt worden sind. Listing 4.6 zeigt die Implementierungsdatei der Klasse CShares. // Shares.cpp: Implementierungsdatei // #include "stdafx.h" #include "ODBCChart.h" #include "Shares.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////// // CShares IMPLEMENT_DYNAMIC(CShares, CRecordset)
In der Implementierungsdatei der Klasse CShares wird in der Funktion GetDefaultConnect die ODBC-Datenbank Aktien als Datenquelle angegeben, in der Funktion GetDefaultSQL die Tabelle
505
506
4
Datenbankprogrammierung
Aktie für die Satzgruppe bestimmt und in der Funktion DoFieldExchange der Datenaustausch durch RFX-Funktionen festgelegt. Die Klasse CShares wird zur Anzeige aller Aktien im Dialogfeld zur Aktienauswahl und damit in der Klasse CStockChooser verwendet. Die Implementierungsdatei der Klasse CStockChooser ist in Listing 4.7 zu sehen. // StockChooser.cpp: Implementierungsdatei // #include "stdafx.h" #include "ODBCChart.h" #include "StockChooser.h" #include "Shares.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// // Dialogfeld CStockChooser
UpdateData (); // Dialog schließen EndDialog (IDOK); } Listing 4.7: Die Implementierungsdatei der Klasse CStockChooser
CStockChooser ist eine von CDialog abgeleitete Dialogklasse. Der von ihr implementierte Dialog wird als modales Dialogfeld innerhalb des Programms ODBCChart verwendet. In der zu der Klasse gehörenden Dialogfeldressource wird ein Listenfeld definiert, das die Liste aller Aktien anzeigt. Dieses Listenfeld wird bei der Initialisierung des Dialogs in der Funktion OnInitDialog durch eine Datenbankabfrage mit den Namen aller in der Datenbank gespeicherten Aktien gefüllt. Nach dem Aufruf der Funktion OnInitDialog der Basisklasse wird ein Objekt der Klasse CShares angelegt. Durch den Aufruf der Funktion GetDlgItem wird dann ein Zeiger auf das Listenfeld des Dialogs erfragt. An diese Anweisung schließt sich die Datenbankabfrage an. Diese erfolgt in einem try-Block, um im Fehlerfall Ausnahmen der Klasse CDBException abzufangen. Durch Aufruf der Funktion CShare::Open wird die Abfrage gestellt und die Satzgruppe geöffnet. Anschließend wird durch die Funktion IsBOF geprüft, ob die erhaltene Satzgruppe leer ist. Sollte dies der Fall sein, so wird die Satzgruppe geschlossen und die Initialisierung des Dialogfelds beendet. Für den Fall, dass die Satzgruppe nicht leer ist, positioniert die Funktion MoveFirst den Cursor auf den ersten Datensatz der Satzgruppe. In einer Schleife wird über die Datensätze iteriert und der Aktienname jedes Datensatzes durch die Funktion CListBox::AddString in das Listenfeld übernommen. Die Funktion IsEOF bestimmt das Ende der Satzgruppe, die Funktion MoveNext positioniert den Cursor zum nächsten Datensatz. Zuletzt schließt die Funktion Close die Satzgruppe und der Aufruf von CListBox::SetCurSel selektiert den ersten Eintrag im Listenfeld. Die Funktion OnDblclkList1 der Klasse CStockChooser wird aufgerufen, wenn der Benutzer auf einen Eintrag im Listenfeld doppelklickt. Da dies als Auswahl gewertet wird, führt der Aufruf von UpdateData den Datenaustausch per DDX aus. Der Aufruf von EndDialog schließt den Dialog. Der an EndDialog übergebene Parameter mit dem Wert IDOK ist gleichzeitig der Wert, den die Funktion DoModal an ihren Aufrufer zurückgibt. Die Dialogfeldklasse CStockChooser besitzt zum Datenaustausch die Variable m_strShareName, die den im Listenfeld ausgewählten Aktiennamen bezeichnet.
ODBC
509
Aufgerufen wird der durch die Klasse CStockChooser implementierte Dialog in der Dokumentenklasse CODBCChartDoc des Beispielprogramms ODBCChart. Der Aufruf erfolgt aus der Funktion OnNewDocument, um die Aktie zu bestimmen, für die das zu erstellende Dokument angelegt werden soll. In der Funktion OnNewDocument wird dann mit der ausgewählten Aktie als Kriterium eine Abfrage an die Datenbank erstellt, die alle Kurswerte der Aktie in chronologischer Reihenfolge zurückgibt. In SQL formuliert sieht diese Abfrage wie in Listing 4.8 gezeigt aus. SELECT * FROM Aktie, Kurs WHERE Aktie.Aktiennummer = Kurs.Aktiennummer AND Aktienname = <stockChooser.m_strShareName> ORDER BY Datum Listing 4.8: SQL-Abfrage, um alle Kurswerte zu erhalten
Die beiden Tabellen Aktie und Kurs werden über die Aktiennummer verknüpft. Der Aktienname muss mit dem im Auswahldialog gewählten Aktiennamen übereinstimmen. Die Werte werden chronologisch sortiert zurückgegeben. Im Beispiel kann mit dieser SQL-Abfrage sehr gut die Verwendung mehrerer Datenbanktabellen in einer Satzgruppe gezeigt werden. In der Praxis würde man jedoch die Abfrage nicht in dieser Form durchführen. So muss in der Beispielabfrage der Aktienname unnötigerweise eindeutig sein, da über diesen die Aktie ausgewählt wird. Dieser Join ist dagegen nicht notwendig, wenn bei der Aktienwahl im Auswahldialog bereits die Aktiennummer gespeichert wird. Das Abfragen der Kurse beschränkt sich dann auf die Tabelle Kurs mit der zuvor gespeicherten Aktiennummer. Diese Abfrage ist effizienter. Um eine Datenbankabfrage über zwei oder mehrere Datenbanktabellen durchzuführen, reicht es aus, eine Satzgruppe zu definieren, die die beteiligten Tabellen anspricht. Im Beispielprogramm wird die Klasse CQuotes für diese Abfrage verwendet. Listing 4.9 zeigt die Header-Datei der Klasse CQuotes. // Quotes.h : Header-Datei // ////////////////////////////////////////////////////////////// / // Satzgruppe CQuotes class CQuotes : public CRecordset {
t
510
4
Datenbankprogrammierung
public: CQuotes(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CQuotes) // Feld-/Parameterdaten long m_Aktiennummer; CString m_Aktienname; long m_WKN; CString m_Tickersymbol; long m_Aktiennummer2; CTime m_Datum; CString m_Kurs;
Im Listing kann man die Datenelemente der beiden Tabellen Aktie und Kurs sehen. Da das Attribut Aktiennummer in beiden Tabellen vorkommt, müssen auch zwei Datenelemente für dieses Attribut angelegt werden. Das Attribut Aktiennummer der Tabelle Kurs wurde in diesem Fall einfach Aktiennummer2 genannt. Die HeaderDatei der Klasse CQuotes unterscheidet sich abgesehen davon nicht von den bereits besprochenen Satzgruppenklassen. // Quotes.cpp: Implementierungsdatei // #include "stdafx.h" #include "ODBCChart.h" #include "Quotes.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE
Listing 4.10 zeigt die Implementierungsdatei der Klasse CQuotes. In der Funktion GetDefaultSQL ist erkennbar, wie man mehrere Datenbanktabellen für eine Satzgruppe angibt und damit eine Datenbankabfrage über mehrere Tabellen realisiert. Die Tabellennamen werden dazu durch Kommata verbunden. In der Funktion DoFieldExchange ist zu beobachten, dass bei Ansprache der Datenbankspalte Aktiennummer der jeweilige Tabellenname angegeben werden muss, um die Datenbankspalte eindeutig bestimmen zu können. Mit Hilfe der Klasse CQuotes kann die Datenbankabfrage aller Kurse einer Aktie formuliert werden. Das Ergebnis dieser Abfrage wird in einer Liste gespeichert, die von der Dokumentenklasse definiert wird. Diese Liste gleicht der Liste des in Kapitel 2, »Einstieg in die MFC-Programmierung«, vorgestellten Beispielprogramms StockChart. Es fehlt allerdings die Funktion zur Serialisierung der Liste, da diese im Programm ODBCChart nicht benötigt wird. Die Definition der Liste ist in Listing 4.11 zu sehen. Dieses Listing zeigt die Header-Datei der Klasse CODBCChartDoc. #include
// die Template-Werkzeugklassen
class CStockData { public: double min, max; CList<double,double> theData; }; class CODBCChartDoc : public CDocument { protected: // Nur aus Serialisierung erzeugen CODBCChartDoc(); DECLARE_DYNCREATE(CODBCChartDoc) // Attribute
ODBC
513
public: BOOL m_bGrid; // Flag für Gitternetz BOOL m_bAverage; // Flag für Durchschnittslinie int m_nAverageCnt; // Anzahl der Werte für Durchschnitt COLORREF m_nColor; // Farbe der Kurve CString m_name; // Name der Aktie CString m_ticker; // Ticker-Symbol int m_nID; // WKN CStockData m_stockData; // hält die Kursdaten CList<double,double> m_averageData; // hält Daten der // Durchschnittslinie // Operationen public: // Überladungen // Vom Klassenassistenten generierte Überladungen virtueller // Funktionen public: virtual BOOL OnNewDocument(); // Implementierung public: virtual ~CODBCChartDoc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: void CalcAverages (); // Generierte Message-Map-Funktionen protected: DECLARE_MESSAGE_MAP() }; Listing 4.11: Die Header-Datei der Klasse CODBCChartDoc
Im Gegensatz zum Beispielprogramm StockChart aus Kapitel 2, »Einstieg in die MFC-Programmierung«, lassen sich im Programm ODBCChart die Eigenschaften des Charts nicht verändern. Im Konstruktor der Klasse CODBCChartDoc werden die Farbe der Darstellung, das Gitternetz und die Einstellungen der Durchschnittslinie gesetzt. Listing 4.12 zeigt die Implementierungsdatei der Klasse CODBCChartDoc.
517 // auf den Anfang der Datenliste setzen outerPos = m_stockData.theData.GetHeadPosition (); for (int i=0; i
} Listing 4.12: Die Implementierungsdatei der Klasse CODBCChartDoc
Der Dialog zur Aktienauswahl wird in der Funktion OnNewDocument aufgerufen. Anschließend wird in dieser Funktion auch die Abfrage der Aktienkurse durchgeführt. Die Funktion OnNewDocument wird in einer MDI-Applikation aufgerufen, wenn eine neue Instanz der Dokumentenklasse angelegt wird. Zunächst sollte in dieser Funktion die Implementierung der Basisklasse aufgerufen werden. Das ist in Listing 4.12 zu sehen. Anschließend wird ein Objekt der Klasse CStockChooser angelegt, das den Dialog zur Aktienauswahl repräsentiert. Ein Objekt der Klasse CQuotes wird angelegt, um damit die Abfrage der Aktienkurse durchzuführen. Danach wird der Dialog durch Aufruf der Funktion DoModal angezeigt. Sofern der Benutzer im Dialog eine Aktie ausgewählt hat, liefert DoModal den Wert IDOK zurück. Damit kann die Abfrage der Kurse zur ausgewählten Aktie durchgeführt werden. Die Abfrage wird in einem try-Block durchgeführt, um Ausnahmen des Typs CDBException abzufangen. Danach wird mit den Member-Variablen m_strFilter und m_strSort die Abfrage formuliert. Die Variable m_strFilter gibt die WHERE-Anweisung einer SQL-Abfrage an, die Variable m_strSort die Anweisung ORDER BY. Die Abfrage einer Datenbank durch eine von CRecordset abgeleitete Klasse folgt dem Schema im Listing 4.13. SELECT * FROM WHERE <m_strFilter> ORDER BY <m_strSort> Listing 4.13: Schema einer Datenbankabfrage
m_strFilter und m_strSort
518
4
Datenbankprogrammierung
Im FROM-Teil der Abfrage werden implizit die Datenbanktabellen eingetragen, die beim Erzeugen einer von CRecordset abgeleiteten Klasse von der Funktion GetDefaultSQL definiert werden. Im Beispielprogramm sind das die Datenbanktabellen Aktie und Kurs. Nach der Formulierung der Abfrage mit den Strings m_strFilter und m_strSort startet der Aufruf der Funktion Open die Abfrage und öffnet die Satzgruppe. Die Funktion IsBOF prüft anschließend, ob die Satzgruppe leer ist. In diesem Fall wird eine Fehlermeldung ausgegeben und der Wert false zurückgegeben, damit kein neues Dokument erzeugt wird. Ist die Satzgruppe nicht leer, dann positioniert die Funktion MoveFirst den Cursor auf den ersten Datensatz der Satzgruppe. Tickersymbol, Aktienname und Wertpapierkennnummer werden in die entsprechenden MemberVariablen der Dokumentenklasse kopiert. Der Titel des Dokuments wird mit dem Aktiennamen belegt. In einer while-Schleife wird dann über alle Datensätze der Satzgruppe iteriert. Die Datensätze werden in die Datenstruktur m_stockData des Dokuments kopiert. Dabei werden Minimum und Maximum der Kurswerte bestimmt. Abschließend wird die Satzgruppe durch den Aufruf der Funktion Close geschlossen und die Durchschnittslinie durch den Aufruf der Funktion CalcAverages berechnet. Die Funktion CalcAverages entspricht der gleichnamigen Funktion aus dem Beispielprogramm StockChart aus Kapitel 2, »Einstieg in die MFCProgrammierung«. Die Ansichtsklasse CODBCChartView entspricht weitgehend der Ansichtsklasse des Beispielprogramms StockChart. Die Ansichtsklasse verwendet zur Generierung des Aktiencharts die Datenstruktur m_stockData der Dokumentenklasse. Damit ist die Ansichtsklasse von der Datenbank entkoppelt und kann selbst keine Datenbankzugriffe auslösen. Ein Neuzeichnen der Ansicht führt nicht zu einem erneuten Datenbankzugriff, die Dokumentenklasse wirkt als Zwischenspeicher oder Cache für die Daten der Datenbank.
4.2.7 Parametrisierte Datenbankabfragen
Weitere Möglichkeiten
Mit den beiden vorgestellten Beispielprogrammen konnten nicht alle Möglichkeiten der ODBC-Schnittstelle demonstriert werden. Beispielsweise lassen sich Datenbankabfragen parametrisieren. Parametrisierte Datenbankabfragen enthalten innerhalb des FilterStrings m_strFilter ein oder mehrere Fragezeichen, die bei der Aus-
ODBC
519
führung der Datenbankabfrage durch Parameter ersetzt werden. Ein Filter-String für eine parametrisierte Abfrage könnte beispielsweise wie folgt aussehen: pSet->m_strFilter = _T("[Aktienname] = ?");
In die Klasse der Satzgruppe muss eine Member-Variable für den Parameter hinzugefügt werden. Dann wird im Konstruktor der Klasse die Member-Variable m_nParams entsprechend der Anzahl der verwendeten Parameter initialisiert. In der Funktion DoFieldExchange wird der Austausch der Parametervariablen durch RFXFunktionen programmiert. Vor dem eigentlichen Austausch muss dazu der Austauschtyp durch einen Aufruf der Funktion SetFieldType auf den Wert CFieldExchange::param gesetzt werden. Vor der Ausführung der Datenbankabfrage wird der Parametervariablen ein Wert zugewiesen. Parametrisierte Datenbankabfragen sind effizienter als durch Filter realisierte Abfragen, wenn die Abfragen für verschiedene Parameter wiederholt werden müssen. Die ODBC-Technologie unterstützt die Ausführung von Datenbankabfragen durch mehrere Threads. Diese prinzipielle Fähigkeit wird allerdings von vielen ODBC-Treibern nicht unterstützt. Wenn ein ODBC-Treiber kein Multithreading unterstützt, dann müssen alle Datenbankabfragen aus einem Thread heraus erfolgen. Ein Beispiel für einen ODBC-Treiber, der Threads unterstützt, ist der SQL-Server-Treiber. Der MS Access-Treiber unterstützt keine Threads. Bevor man mehrere Threads in einem ODBC-Programm nutzt, sollte man genau prüfen, ob alle verwendeten ODBC-Treiber das Multithreading unterstützen.
Mehrere Threads
Die Klasse CRecordset unterstützt das gesammelte Abrufen von Datensätzen (bulk row fetching). »Gesammeltes Abrufen« bedeutet, dass nicht ein Datensatz nach dem anderen abgerufen wird, sondern dass das Abrufen einer größeren Anzahl von Datensätzen in einem Schritt stattfindet. Die Option zu gesammeltem Abruf wird beim Öffnen der Satzgruppe durch Angabe der Konstanten CRecordset::useMultiRowFetch für den Parameter dwOptions eingerichtet. Das gesammelte Abrufen von Datensätzen kann die Effizienz von Datenbankabfragen erhöhen. Ein gesammeltes Zurückschreiben oder Einfügen von Datensätzen wird von den MFC nicht unterstützt. Dies ist eine Beschränkung der MFC und nicht von ODBC. Durch direkte Verwendung des ODBC-API kann auch das gesammelte Zurückschreiben und Einfügen von Datensätzen realisiert werden.
Gesammeltes Abrufen
520
4 Zugriff ohne Bindung
Datenbankprogrammierung
Statt eine eigene Klasse von CRecordset abzuleiten, kann man Objekte dieser Klasse auch direkt zum Datenbankzugriff verwenden. In diesem Fall findet der Zugriff auf die Felder der Datenbank ohne vorgegebene Bindung statt. »Ohne vorgegebene Bindung« bedeutet, dass den Spalten einer Datenbank keine Variablen in einer Satzgruppe zugeordnet werden. Die Werte von Datenbankfeldern werden über die Funktion GetFieldValue erfragt. Die zurückgelieferten Werte werden in Variablen der Klasse CDBVariant gespeichert. CDBVariant kann Werte aus Datenbankfeldern unabhängig von deren Datentyp speichern. Der ungebundene Zugriff auf Datenbankfelder mit der Funktion GetFieldValue hat den Vorteil, dass man das Layout der Datenbank bei der Erstellung der Datenbankanwendung nicht kennen muss.
4.2.8
Tipps zur Vorgehensweise
왘 Einfache, formularbasierte Datenbankanwendungen lassen sich mit dem Anwendungs-Assistenten auf Basis der Klasse CRecordView erstellen. Diese Anwendungen besitzen allerdings nur eine rudimentäre Benutzerschnittstelle. Flexibler lassen sich Datenbankanwendungen gestalten, wenn man auf die Unterstützung durch den Anwendungs-Assistenten verzichtet und stattdessen mit dem MFC-ODBC-Consumer-Assistenten Klassen zur Ansprache der Datenbank erzeugt. Die Darstellung und Verwaltung der Daten muss dann selbst programmiert werden. 왘 Per DDX lässt sich direkt ein Datenaustausch zwischen Steuerelementen und einer Satzgruppenklasse durchführen. Dadurch werden zusätzliche Variablen in der Ansichtsklasse eingespart. 왘 Die SQL-Anweisungen WHERE und ORDER BY werden in den MFCDatenbankklassen durch die Member-Variablen m_strFilter und m_strSort abgebildet. Weist man diesen beiden Variablen Strings zu, so werden diese Zeichenketten an den entsprechenden Stellen in die SQL-Abfrage eingebaut. 왘 ODBC kann prinzipiell Datenbanken von mehreren Threads aus ansprechen. Leider wird dies jedoch nicht von allen ODBC-Treibern unterstützt. Bevor man mehrere Threads in einem Projekt verwendet, sollte man daher unbedingt überprüfen, ob alle verwendeten ODBC-Treiber Multithreading unterstützen.
DAO
521
왘 Um Datenbankabfragen effizienter zu gestalten, lassen sich wiederholte Abfragen mit abweichenden WHERE-Anweisungen mit Hilfe von Parametern implementieren. 왘 Lesende Zugriffe lassen sich optimieren, indem man das gesammelte Abrufen von Datensätzen implementiert. Mit den Klassen der MFC lassen sich Datensätze allerdings nicht gesammelt zurückschreiben. Dazu ist die direkte Verwendung des ODBC-APIs notwendig.
4.2.9
Zusammenfassung
ODBC war eine der ersten Datenbankschnittstellen, die vollständig von dem verwendeten Datenbank-Managementsystem abstrahiert. Die dadurch ermöglichte datenbankunabhängige Programmierung hat zu einer weiten Verbreitung von ODBC geführt. ODBC ist heute die wohl meistverbreitete Datenbankschnittstelle. Die Klassen der MFC ermöglichen eine einfache Programmierung der ODBC-Schnittstelle. ODBC ist keine neue Technologie mehr, so dass mittlerweile neue Datenbankschnittstellen entstehen, die über die Fähigkeiten von ODBC hinausgehen. ODBC wird in der Praxis gerne verwendet, da es ausgereift ist und es für praktisch jede Datenbank einen ODBC-Treiber gibt.
4.3
DAO
DAO ist eine Programmierschnittstelle zum Microsoft Jet-Datenbankkern, der von Microsoft Access verwendet wird. Der Jet-Datenbankkern kann jedoch völlig unabhängig von Microsoft Access eingesetzt werden. Das Standarddatenformat des Jet-Datenbankkerns ist das von MS Access verwendete MDB-Format. Der JetDatenbankkern ist jedoch nicht auf dieses Format beschränkt. Er kann folgende Formate bearbeiten: 왘 Microsoft Access (MDB). Dies ist das Standardformat des JetDatenbankkerns. Die derzeit aktuelle Version von Jet ist 4.0. Diese Version wird von Microsoft Access 2000 verwendet. Jet kann natürlich auch ältere Formate des MDB-Formats bearbeiten: die Versionen 2.x und 3.0. 왘 Microsoft Excel-Tabellen in den Versionen 3.0, 4.0, 5.0, 97 und 2000. 왘 Lotus WK1- und WK3-Tabellen.
Jet
522
4
Datenbankprogrammierung
왘 Borland Paradox in den Versionen 3.x, 4.x und 5.x. 왘 dBASE in den Versionen dBASE III, dBASE IV und dBASE 5.0. 왘 Textdateien. Textdateien werden oft zum Datenaustausch zwischen inkompatiblen Systemen verwendet. Daten werden dazu zeilenweise in Textdateien geschrieben, wobei Spalten durch festgelegte Zeichen, wie Tabulatoren oder Semikolons, getrennt werden. Jet kann solche Dateien verarbeiten. 왘 HTML-Dateien. Der Jet-Datenbankkern kann HTML-Tabellen lesen und schreiben. 왘 ODBC-Datenquellen. Jet kann über ODBC-Treiber auf alle DBMS zugreifen, für die ein ODBC-Treiber existiert. Damit kann Jet auch zum Zugriff auf Serverdatenbanksysteme wie den Microsoft SQL-Server oder Oracle verwendet werden. Nicht auf alle aufgeführten Formate kann mit dem vollen Funktionsumfang von Jet zugegriffen werden. Bei vielen, insbesondere nicht von Microsoft stammenden Datenbankformaten, muss mit Einschränkungen gerechnet werden. So ist teilweise nur lesender Zugriff möglich. Den vollen Funktionsumfang und die größte Flexibilität bietet das Jet-eigene MDB-Format. Dieses sollte daher bei DAO-Programmen die erste Wahl sein. Mit dem Erscheinen von Visual Studio .NET wird DAO nicht mehr durch die Entwicklungsumgebung unterstützt. Beispielsweise lassen sich DAO-Programme nicht mehr mit dem Anwendungs-Assistenten erzeugen. Alle MFC-Klassen zur DAO-Programmierung sind jedoch weiterhin vorhanden, bestehende Programme können also problemlos weitergepflegt werden. Bei neuen Projekten sollte DAO nicht mehr verwendet werden, da es auf lange Sicht durch andere Zugriffsmethoden (wie ODBC und OLE DB) abgelöst werden wird. DAO ist weiterhin Bestandteil der MFC und wird von bestehenden Programmen verwendet. Daher soll es in diesem Buch neben ODBC und OLE DB beschrieben werden.
4.3.1
Die MFC-Klassen zur DAO-Programmierung
DAO wird durch eine Reihe von COM-Schnittstellen implementiert. Auf diese Schnittstellen kann direkt oder durch eine spezielle DAO-Klassenbibliothek, das DAO-SDK, zugegriffen werden. Eine
DAO
523
Untermenge der DAO-Schnittstelle wird jedoch auch durch Klassen der MFC abgebildet. Abbildung 4.18 zeigt die MFC-Klassen zur DAO-Programmierung.
Die DAO-Klassen der MFC weisen eine große Ähnlichkeit zu den ODBC-Klassen der MFC auf. Für die ODBC-Klassen CRecordView, CDatabase, CRecordset und CDBException gibt es direkte Entsprechungen in Form von DAO-Klassen: CDaoRecordView, CDaoDatabase, CDaoRecordset und CDaoException. Die Ähnlichkeit zwischen ODBC- und DAO-Klassen ist natürlich kein Zufall, sondern Absicht. Durch die Ähnlichkeit beider Klassengruppen muss sich der Programmierer nur mit einem Modell zur Ansprache von Datenbanken vertraut machen. Außerdem lassen sich Programme relativ einfach von ODBC auf DAO umstellen. Für die umgekehrte Richtung gilt das nur, wenn man nicht Teile des deutlich größeren Funktionsumfangs von DAO verwendet.
ODBC und DAO
Für die Klassen CDaoWorkspace, CDaoTableDef und CDaoQueryDef gibt es keine Entsprechungen bei den ODBC-Klassen. Objekte der Klasse CDaoWorkspace verwalten Verbindungen zu Jet-Daten-
CDaoWorkspace
524
4
Datenbankprogrammierung
banken. Normalerweise werden Objekte dieser Klasse vom Programmierer nicht direkt verwendet, sondern implizit erzeugt. Bei DAO werden Transaktionen über diese Objekte gesteuert. Weiterhin gibt es Funktionen, um Datenbanken zu defragmentieren (CompactDatabase) und beschädigte Datenbanken zu reparieren (RepairDatabase). CDaoTableDef
Mit Objekten der Klasse CDaoTableDef kann die Struktur einer Datenbank untersucht und verändert werden. Man kann zum Beispiel Tabellen anlegen, Felder zu bestehenden Tabellen hinzufügen und deren Eigenschaften verändern. Auch das Anlegen und Löschen von Schlüsseln ist möglich.
CDaoQueryDef
Objekte der Klasse CDaoQueryDef repräsentieren Sichten oder Views auf eine Datenbank. In Microsoft Access werden diese Sichten Abfragen genannt. Mit Hilfe der Klasse CDaoQueryDef lassen sich solche Abfragen anlegen und verändern.
t
Im Gegensatz zu ODBC, bei dem es vom ODBC-Treiber abhängt, ob mit mehreren Threads gearbeitet werden kann, erlaubt DAO generell kein Multithreading. Daher müssen alle Zugriffe auf die Datenbank aus einem Thread heraus erfolgen.
4.3.2
Das Beispielprogramm StockDAO
Im Folgenden sollen die in Abschnitt 4.2, »ODBC«, vorgestellten Programme StockODBC und ODBCChart in DAO-Versionen vorgestellt werden. Da die grundlegende Programmstruktur bereits bekannt ist, wird auf die relativ wenigen Unterschiede zwischen ODBC und DAO aus Sicht der MFC eingegangen. Auswahl einer DAO-Datenquelle
Das Programm StockDAO besitzt die gleiche Benutzerschnittstelle und die gleiche Funktionalität wie das Programm StockODBC. Allerdings ist die verwendete Datenquelle eine Access Datenbank, die direkt angesprochen wird. DAO-Datenquellen werden im Unterschied zu ODBC-Datenquellen nicht durch die Systemsteuerung verwaltet. Sie werden einfach durch ihren Dateipfad angegeben.
Tabellensatzgruppen
Im Gegensatz zum Beispielprogramm StockODBC verwendet StockDAO zur Darstellung der Satzgruppe ein Dynaset. Dies ist die Voreinstellung bei DAO-Datenquellen. Zusätzlich zu ODBC gibt es bei DAO-Datenquellen den Typ TABELLE für die Satzgruppen. Tabellensatzgruppen arbeiten direkt auf einer Datenbank-
DAO
tabelle, ohne den Umweg einer Datenbankabfrage zu gehen, wie das bei Snapshots und Dynasets der Fall ist. Bei Tabellensatzgruppen kann man auf einzelne Datensätze zugreifen und es kann sequenziell über die Datensätze einer Tabelle iteriert werden. Tabellensatzgruppen können nicht für DAO-Datenquellen verwendet werden, die über ODBC auf eine Datenbank zugreifen. Die Unterschiede im Programmcode von StockDAO zu StockODBC sind gering. Listing 4.14 zeigt die Implementierungsdatei der Satzgruppenklasse CStockDAOSet. // StockDAOSet.cpp : Implementierung der Klasse CStockDAOSet // #include "stdafx.h" #include "StockDAO.h" #include "StockDAOSet.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// // CStockDAOSet Implementierung IMPLEMENT_DYNAMIC(CStockDAOSet, CDaoRecordset) CStockDAOSet::CStockDAOSet(CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_Aktiennummer = 0; m_Aktienname = _T(""); m_WKN = 0; m_Tickersymbol = _T(""); m_nFields = 4; m_nDefaultType = dbOpenDynaset; } CString CStockDAOSet::GetDefaultDBName() { // Dateidialog zum Abfragen der // Datenbank CFileDialog fileDialog(true, _T("mdb"), _T("Aktien.mdb"), NULL, _T("MDB-Dateien (*.mdb)|*.mdb"));
525
526
4
Datenbankprogrammierung
// Wenn der Benutzer den Dialog mit OK verlassen hat: if (IDOK == fileDialog.DoModal ()) { return fileDialog.GetPathName (); } else { return _T("Aktien.mdb"); } }
CStockDAOSet ist von der Klasse CDaoRecordset abgeleitet worden. Die Datenelemente der Satzgruppe entsprechen den Datenelementen der Klasse CStockODBCSet aus dem Beispielprogramm StockODBC. Die beiden Programme unterscheiden sich durch die Verwendung des Satzgruppentyps. In der Klasse CStockDAOSet wird der Typ Dynaset durch Zuweisung der Konstanten dbOpenDynaset an die Member-Variable m_nDefaultType im Konstruktor verwendet.
DAO
527
Die ODBC- und DAO-Klassen der MFC dürfen nicht vermischt genutzt werden! Man kann beide Klassengruppen zwar in einem Programm verwenden, muss aber beide Gruppen sorgfältig getrennt halten. So darf beispielsweise ein Objekt der Klasse CRecordset nicht zum Zugriff auf DAOSatzgruppen verwendet werden. Dementsprechend gibt es auch getrennte Konstantendefinitionen für beide Klassengruppen. Um bei ODBC den Typ Dynaset auszuwählen, verwendet man die Konstante dynaset, bei DAO muss hingegen dbOpenDynaset angegeben werden. Bei der Auswahl der Datenquelle verwenden ODBC und DAO verschiedene Funktionen. ODBC verwendet die Funktion GetDefaultConnect, DAO die Funktion GetDefaultDBName. Die Funktion GetDefaultDBName muss den Pfad und den Dateinamen der verwendeten DAO-Datenquelle zurückgeben. Während der Entwicklungszeit ist es sehr praktisch, hier einen konstanten Pfad einzutragen. Im fertig gestellten Programm sollte man dem Benutzer jedoch eine Möglichkeit geben, die Datenquelle selbst auszuwählen. Im Beispielprogramm wählt der Benutzer die Datenbank durch einen Dateidialog aus.
GetDefaultDBName
Die Funktion DoFieldExchange übernimmt wie in ODBC-Programmen den Datenaustausch zwischen den Datenelementen der Satzgruppe und der Datenbank. DAO verwendet jedoch keine RFXFunktionen, sondern implementiert einen eigenen Datenaustauschmechanismus: DFX (DAO Record Field Exchange). Aus der Sicht des Programmierers sind RFX und DFX gleich zu benutzen.
DFX
Bei der Verwendung der Satzgruppenfunktionen unterscheiden sich ODBC und DAO fast nicht. Dies wird in der Implementierung der Ansichtsklasse deutlich. Listing 4.15 zeigt die Implementierungsdatei der Ansichtsklasse CStockDAOView. // StockDAOView.cpp : Implementierung der Klasse CStockDAOView // #include "stdafx.h" #include "StockDAO.h" #include "StockDAOSet.h" #include "StockDAODoc.h" #include "StockDAOView.h" #ifdef _DEBUG #define new DEBUG_NEW
////////////////////////////////////////////////////////////// // CStockDAOView Nachrichten-Handler void CStockDAOView::OnRecordNew() { // Satzgruppe besorgen: CStockDAOSet* pSet = (CStockDAOSet*)OnGetRecordset(); // Wenn Änderungen am aktuellen Datensatz gemacht // wurden, diesen erst sichern: if ( pSet->CanUpdate() && // Kann ändern !pSet->IsBOF() && // Ist nicht leer !pSet->IsDeleted()) // Datensatz nicht gelöscht { // Ändern: pSet->Edit(); // Daten aus der Ansicht holen: UpdateData(); // Und zurückschreiben: pSet->Update(); } // Leerer Datensatz: pSet->m_Aktienname = _T(""); pSet->m_Aktiennummer = -1; pSet->m_Tickersymbol = _T(""); pSet->m_WKN = 0;
DAO
531 // Neuen Datensatz vormerken: m_bNewRecord = true; // Daten in die Ansicht schreiben: UpdateData (false);
} void CStockDAOView::OnRecordDelete() { // Datensatz besorgen CDaoRecordset* pSet = OnGetRecordset(); if (pSet->CanUpdate()) // Datensatz löschen: pSet->Delete (); else return; if (pSet->CanRestart()&& // kann ändern !pSet->IsBOF ()) // Ist nicht leer // Neu einlesen: pSet->Requery (); // aktuellen Satz in Ansicht übernehmen UpdateData (false); } BOOL CStockDAOView::OnMove(UINT nIDMoveCommand) { // Datensatz besorgen CStockDAOSet* pSet = (CStockDAOSet*)OnGetRecordset(); if (m_bNewRecord) { CString strShareName, strTicker; int nWKN; // Daten aus der Ansicht holen UpdateData(); // Werte sichern strShareName = pSet->m_Aktienname; strTicker = pSet->m_Tickersymbol; nWKN = pSet->m_WKN; // Datensatz erzeugen: pSet->AddNew (); // Daten zuweisen: pSet->m_Aktienname = strShareName; pSet->m_Tickersymbol = strTicker; pSet->m_WKN = nWKN;
532
4
Datenbankprogrammierung
// und zurückschreiben pSet->Update(); if (pSet->CanRestart()) { pSet->Requery (); // auf (vermutlich) neuen Datensatz positionieren: pSet->MoveLast (); } m_bNewRecord = false; // Mit Ansicht abgleichen: UpdateData (false); } return CDaoRecordView::OnMove(nIDMoveCommand); } Listing 4.15: Die Implementierungsdatei der Klasse CStockDAOView
Die Implementierung der Funktionen OnRecordNew und OnRecordDelete unterscheidet sich praktisch nicht von den entsprechenden Funktionen des Beispielprogramms StockODBC. Daran lässt sich sehen, dass ODBC-Programme, die die MFC-Klassen verwenden, sehr leicht in Programme umgewandelt werden können, die stattdessen DAO verwenden.
4.3.3
Das Beispielprogramm DAOChart
Auch im Programm DAOChart gibt es einige Unterschiede zu der auf ODBC basierenden Variante. So zeigt sich, dass DAO teilweise andere Datentypen für die Datenelemente von Satzgruppen verwendet. Dies ist in der Satzgruppenklasse CQuotes zu sehen. Listing 4.16 zeigt die Header-Datei dieser Klasse. // Quotes.h : Header-Datei ////////////////////////////////////////////////////////////// // DAO Satzgruppe CQuotes class CQuotes : public CDaoRecordset { public: CQuotes(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CQuotes)
DAO
533
// Feld-/Parameterdaten long m_Aktiennummer; CString m_Aktienname; long m_WKN; CString m_Tickersymbol; long m_Aktiennummer2; COleDateTime m_Datum; COleCurrency m_Kurs; // Überschreibungen public: virtual CString GetDefaultDBName(); // StandardDatenbankname virtual CString GetDefaultSQL(); // Standard-SQL für // Satzgruppe // RFX-Unterstützung virtual void DoFieldExchange(CDaoFieldExchange* pFX); // Implementierung #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; Listing 4.16: Die Header-Datei der Klasse CQuotes
Uhrzeit und Datum werden innerhalb der DAO-Klassen durch den Datentyp COleDateTime repräsentiert. Die Klasse COleDateTime kapselt den bei COM verwendeten Datentyp DATE. Objekte der Klasse CTime konnten in MFC-Versionen vor 7.0 nur den Datumsbereich von 1970 bis 2038 darstellen. Diese Beschränkung wurde erst mit Version 7.0 durch neue Funktionen in CTime (64Bit-Darstellung) aufgehoben. Objekte der Klasse COleDateTime konnten dagegen schon immer den Datumsbereich von 100 bis 9999 darstellen. Das Datenelement m_Datum der Klasse CQuotes verwendet den Datentyp COleDateTime.
COleDateTime
Ein weiterer Unterschied zwischen ODBC und DAO ergibt sich bei Datenbankfeldern, die das Format Währung (Currency) besitzen. Die Werte dieser Datenbankfelder werden von den ODBCKlassen der MFC einfach als CString-Objekte dargestellt. Die DAO-Klassen verwenden dagegen den Datentyp COleCurrency. COleCurrency kapselt den Datentyp CURRENCY, der von COM verwendet wird. Im Beispiel verwendet das Datenelement m_Kurs der Satzgruppenklasse CQuotes die Klasse COleCurrency.
COleCurrency
534
4
Datenbankprogrammierung
Bei der Abfrage der Aktienkurse in der Funktion OnNewDocument der Klasse CDAOChartDoc müssen die Aktienkurse, die in Form von COleCurrency-Werten zurückgegeben werden, in Fließkommawerte zur Speicherung in der Datenliste des Dokuments umgewandelt werden. Eine direkte Konvertierungsfunktion gibt es nicht. Daher muss die Konvertierung in zwei Schritten durchgeführt werden: Mit der Funktion Format der Klasse COleCurrency wird ein Währungswert in eine Variable des Typs CString konvertiert. Die Funktion atof der C-Laufzeitbibliothek konvertiert anschließend die String-Variable in eine Variable des Typs double. Listing 4.17 zeigt die Implementierungsdatei der Klasse CDAOChartDoc mit der Funktion OnNewDocument, in der diese Konvertierung vorgenommen wird. // DAOChartDoc.cpp : Implementierung der Klasse CDAOChartDoc // #include "stdafx.h" #include "DAOChart.h" #include "DAOChartDoc.h" #include "StockChooser.h" #include "Quotes.h"
// Titel des Dokuments/MDI-Frames SetTitle (m_name); // Daten kopieren while (!quotes.IsEOF ()) { strQuote = quotes.m_Kurs.Format (0, lcid); value = atof (strQuote); minVal = min (minVal, value); maxVal = max (maxVal, value); m_stockData.theData.AddTail (value); quotes.MoveNext (); } // min und max setzen m_stockData.min = minVal; m_stockData.max = maxVal; // Satzgruppe schließen quotes.Close (); // Durchschnittslinie CalcAverages (); return true; } catch (CDaoException *e) { e->ReportError (); e->Delete (); return false; } } else // auf Abbrechen geklickt! return false; }
DAO
537
///////////////////////////////////////////////////////////// // CDAOChartDoc Diagnose #ifdef _DEBUG void CDAOChartDoc::AssertValid() const { CDocument::AssertValid(); } void CDAOChartDoc::Dump(CDumpContext& dc) const { CDocument::Dump(dc); } #endif //_DEBUG //////////////////////////////////////////////////////////// // CDAOChartDoc Befehle void CDAOChartDoc::CalcAverages () { int nCnt = m_stockData.theData.GetCount (); POSITION outerPos, innerPos; double value; m_averageData.RemoveAll (); // auf den Anfang der Datenliste setzen outerPos = m_stockData.theData.GetHeadPosition (); for (int i=0; i
Die in Objekten der Klasse COleCurrency gespeicherten Werte werden länderspezifisch dargestellt. In Deutschland werden Euro- und Centbeträge durch ein Komma getrennt, in Amerika verwendet man dagegen einen Dezimalpunkt, um Dollar- von Centbeträgen zu trennen. Die länderspezifische Darstellung von Währungsbeträgen muss bei der Konvertierung bedacht werden. Länderspezifika werden unter Windows durch Locales beschrieben. Locales sind Länderkennungen, die sowohl Sprachen als auch spezifische Län-
Länderkennungen
538
4
Datenbankprogrammierung
der und Regionen beschreiben. Eine Länderkennung ist aus zwei Teilen aufgebaut: einer Haupt- und einer Unterkennung. Die Hauptkennung gibt die Sprache an, die Unterkennung einen Dialekt oder eine Region. Beispiele für solche Paare sind Deutsch (Schweiz); Englisch (Australien) und Portugiesisch (Brasilien). MAKELCID und MAKELANGID
Die Member-Funktion Format der Klasse COleCurrency besitzt als zweiten Parameter eine Variable des Typs LCID. Dieser Typ gibt eine Länderkennung an. In Listing 4.17 ist zu sehen, wie eine Länderkennung durch die Makros MAKELCID und MAKELANGID zusammengesetzt wird. Es wird hier die Sprachkennung Englisch (US) verwendet. Diese Länderkennung wird bei der Konvertierung der Kurswerte an die Funktion Format übergeben. Damit wird ein Format erzeugt, das anschließend von der Funktion atof in eine Fließkommazahl umgewandelt werden kann. Ein weiteres Implementierungsdetail des Beispielprogramms DAOChart ergibt sich bei der Auswahl der Datenbank. Die von einer Satzgruppenklasse verwendete Datenbank wird durch den von der Funktion GetDefaultDBName zurückgegebenen Dateipfad spezifiziert. Dies wurde bereits beim Programm StockDAO besprochen. Beim Programmstart sollte dem Benutzer die Möglichkeit gegeben werden, eine Datenbank auszuwählen. Im Programm StockDAO wurde dazu in der Funktion GetDefaultDBName einfach ein Dateidialog geöffnet. Das Programm DAOChart besitzt jedoch zwei Satzgruppenklassen, CQuotes und CShares. Damit gibt es auch zwei Implementierungen der Funktion GetDefaultDBName. Es ist nicht besonders komfortabel für den Benutzer, die Datenbank in jeder der Funktionen einmal auszuwählen. Daher wurde der vorgegebene Ansatz verändert. Die Datenbank wird im Programm DAOChart beim Programmstart innerhalb der Funktion InitInstance der Applikationsklasse ausgewählt. Der Dateipfad wird in einer öffentlichen Variablen der Applikationsklasse gespeichert und kann so von den Klassen CQuotes und CShares durch deren Funktion GetDefaultDBName zurückgegeben werden. Listing 4.18 zeigt die Funktion InitInstance der Klasse CDAOChartApp. ////////////////////////////////////////////////////////////// // CDAOChartApp Initialisierung BOOL CDAOChartApp::InitInstance() { #ifdef _AFXDLL Enable3dControls(); // Diese Funktion bei Verwendung von MFC
DAO
539
// in gemeinsam genutzten DLLs aufrufen #else Enable3dControlsStatic(); // Diese Funktion bei statischen // MFC-Anbindungen aufrufen #endif // Ändern des Registrierungsschlüssels, unter dem unsere // Einstellungen gespeichert sind. // ZU ERLEDIGEN: Sie sollten dieser Zeichenfolge einen // geeigneten Inhalt geben // wie z.B. den Namen Ihrer Firma oder Organisation. SetRegistryKey(_T("Addison Wesley Longman")); LoadStdProfileSettings(0);
// Standard INI-Dateioptionen // laden (einschließlich MRU)
// Dateidialog zum Abfragen der // Datenbank CFileDialog fileDialog(true, _T("mdb"), _T("Aktien.mdb"), NULL, _T("MDB-Dateien (*.mdb)|*.mdb")); // Wenn der Benutzer den Dialog mit OK verlassen hat: if (IDOK == fileDialog.DoModal ()) { strDBName = fileDialog.GetPathName (); } else { strDBName = _T("Aktien.mdb"); } // Dokumentvorlagen der Anwendung registrieren. // Dokumentvorlagen dienen als Verbindung zwischen // Dokumenten, Rahmenfenstern und Ansichten. CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_DAOCHATYPE, RUNTIME_CLASS(CDAOChartDoc), RUNTIME_CLASS(CChildFrame), // Benutzerspezifischer MDI// Child-Rahmen RUNTIME_CLASS(CDAOChartView)); AddDocTemplate(pDocTemplate); // Haupt-MDI-Rahmenfenster erzeugen CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; m_pMainWnd = pMainFrame;
540
4
Datenbankprogrammierung
// Befehlszeile parsen, um zu prüfen auf Standard// Umgebungsbefehle DDE, Datei offen CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Verteilung der in der Befehlszeile angegebenen Befehle if (!ProcessShellCommand(cmdInfo)) return FALSE; // Das Hauptfenster ist initialisiert und kann jetzt // angezeigt und aktualisiert werden. pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow(); return TRUE; } Listing 4.18: Die Funktion InitInstance der Klasse CDAOChartApp
Listing 4.19 zeigt die Implementierung der Funktion GetDefaultDBName der Klasse CQuotes. In der Datei QUOTES.CPP wird die externe Variable theApp angelegt, um damit auf das globale Applikationsobjekt zuzugreifen. In der Funktion GetDefaultDBName wird die zuvor in CDAOChartApp::InitInstance gesetzte Variable zurückgegeben. Die Klasse CShares implementiert die Funktion GetDefaultDBName analog. extern CDAOChartApp theApp; CString CQuotes::GetDefaultDBName() { return theApp.strDBName; } Listing 4.19: Die Funktion GetDefaultDBName der Klasse CQuotes
4.3.4
Tipps zur Vorgehensweise
왘 Die MFC-Klassen der beiden Datenbankschnittstellen DAO und ODBC dürfen nicht vermischt werden. Beide Klassengruppen sind völlig getrennt, es gibt auch keine durch Vererbung bedingten Abhängigkeiten. DAO und ODBC verwenden ihre eigenen Konstanten und Datenaustauschfunktionen. 왘 Programme, die mit den ODBC-Klassen der MFC erstellt wurden, können meist durch einfaches Austauschen der ODBCKlassen durch ihre DAO-Äquivalente in DAO-Programme konvertiert werden. Der umgekehrte Weg ist nur möglich, wenn ausschließlich DAO-Klassen verwendet wurden, für die es ODBC-Gegenstücke gibt.
OLE DB
541
왘 Ist die freie Wahl des Datenbankformats möglich, dann sollte auf jeden Fall das MDB-Format gewählt werden. Es ist das Standardformat des Jet-Datenbankkerns und bietet alle Möglichkeiten der DAO-Schnittstelle. 왘 Die Funktion GetDefaultDBName spezifiziert die Datenbank, die von einer Satzgruppe verwendet wird. Da der Benutzer nicht wie bei ODBC die Möglichkeit hat, Datenbankpfade über die Systemsteuerung zu definieren, sollte an dieser Stelle in DAO-Programmen eine Auswahlmöglichkeit für die Datenbank implementiert werden. Arbeitet ein DAO-Programm mit mehreren Satzgruppen auf der gleichen Datenbank, so ist eine zentrale Auswahl, beispielsweise in der Funktion InitInstance des Applikationsobjekts, sinnvoll. 왘 DAO erlaubt kein Multithreading. Alle Zugriffe auf die Schnittstelle müssen daher aus einem Thread heraus erfolgen. Dies muss nicht der Haupt-Thread der Applikation sein. 왘 DAO sollte nicht mehr für neue Projekte verwendet werden, da Microsoft den Support für diese Schnittstelle ausklingen lässt.
4.3.5
Zusammenfassung
DAO ist die Programmierschnittstelle zum Jet-Datenbankkern. Sie ist speziell auf diesen zugeschnitten und kann dessen Funktionalität besser und weitergehender ausnutzen, als dies über ODBC möglich wäre. Anders als ODBC ist DAO keine datenbankunabhängige Schnittstelle. Zwar kann der Jet-Datenbankkern beliebige ODBC-Quellen öffnen, jedoch können viele Eigenschaften der DAO-Schnittstelle bei ODBC-Datenquellen nicht verwendet werden. Da DAO nicht mehr weiterentwickelt wird, sollte es nicht für neue Projekte verwendet werden.
4.4
OLE DB
Genau wie ODBC ist OLE DB eine datenbankunabhängige Schnittstelle. Als neuere Datenbanktechnologie baut OLE DB auf dem Component Object Model (COM) auf. Der Anspruch universeller Verwendbarkeit von OLE DB geht weit über den von ODBC hinaus. OLE DB ist nicht nur eine Datenbankschnittstelle, sondern
542
4
Datenbankprogrammierung
eine Schnittstelle zu Daten aus unterschiedlichen Quellen. OLE DB ermöglicht den Zugriff auf Daten unabhängig von Typ, Größe oder Ort der Datenquelle. Beispiele für OLE DB-Datenquellen sind: 왘 Serverdatenbanksysteme wie Microsoft SQL-Server oder Oracle 왘 Anwenderdatenbanksysteme wie Microsoft Access 왘 Dateien aus Tabellenkalkulationsprogrammen 왘 Textdateien 왘 E-Mails OLE DB ist prinzipiell nicht auf die erwähnten Beispiele beschränkt. Als universelle Datenschnittstelle sind für OLE DB auch andere Anwendungen denkbar, beispielsweise ein Projektplanungsprogramm mit OLE DB-Schnittstelle. Anders als bei ODBC muss auf Daten einer OLE DB-Datenquelle nicht notwendigerweise über SQL-Abfragen zugegriffen werden. Daten werden von OLE DB in tabellenartiger Form dargestellt, auch wenn die so präsentierten Daten nicht aus einer relationalen Datenbank stammen. Anbieter und Nutzer
OLE DB wird durch über 50 COM-Schnittstellen definiert. Allerdings müssen nicht alle diese Schnittstellen implementiert werden, um OLE DB zu verwenden. Die OLE DB-Architektur teilt sich grundsätzlich in zwei Teile auf: in Anbieter (Provider) und Nutzer (Consumer) von Daten. Ein Anbieter stellt Daten zur Verfügung, wie beispielsweise eine Datenbank. Ein Nutzer dagegen greift auf Daten eines Anbieters zu und verarbeitet diese. Ein Nutzer könnte zum Beispiel ein Buchhaltungsprogramm sein, das Datensätze einer Datenbank verarbeitet (vgl. Abbildung 4.19). Ein Programm kann gleichzeitig Anbieter und Nutzer sein. Denkbar wäre ein Datenbankabfrageprozessor, der die von ihm zusammengestellten Daten verarbeitet und als Anbieter zur Verfügung stellt.
Clientprogramm
Datenbank
OLE DB-Nutzer (Consumer)
OLE DB-Anbieter (Provider)
Abbildung 4.19: OLE DB-Nutzer und Anbieter
OLE DB
543
Bei der Verwendung von OLE DB hat man es folglich immer mit einem Paar aus Anbieter und Nutzer zu tun. Bei klassischen Client-Anwendungen muss man üblicherweise einen Nutzer implementieren. Obwohl OLE DB eine neuere Schnittstellentechnologie ist, werden ältere Datenbankinfrastrukturen nicht aufgegeben. Ein Bestandteil von OLE DB sind Anbieter für ODBC- und Jet-Datenbanken. Man kann damit bestehende Datenbanktreiber nutzen. Für DatenbankManagementsysteme wie Oracle und den Microsoft SQL-Server gibt es OLE DB-Anbieter, die ohne einen ODBC-Treiber auskommen. Abbildung 4.20 zeigt einige OLE DB-Anbieter.
OLE DB-Anbieter für SQL-Server
SQL-Server Datenbank
OLE DB-Anbieter für ODBC
DB2 ODBC-Treiber
...
DB2 Datenbank
...
OLE DB-Anbieter für Jet
Jet-Datenbanken
Access Datenbank
...
Abbildung 4.20: Beispiele für OLE DB-Anbieter
4.4.1
OLE DB und die MFC
Im Gegensatz zu den Datenbankschnittstellen ODBC und DAO besitzen die MFC keinen vollständigen Satz von Klassen zur Programmierung von OLE DB. In den MFC selbst gibt es dazu nur eine einzige Klasse: die Ansichtsklasse COleDBRecordView (vgl. Abbildung 4.21). Mit dieser von CFormView abgeleiteten Klasse lassen sich – analog zu den Klassen CRecordView und CDaoRecordView – einfache formularbasierte Anwendungen auf der Basis von OLE DB entwickeln.
Der Zugriff auf OLE DB erfolgt durch eine Reihe von TemplateKlassen, die nicht Teil der MFC sind. Diese Klassen ähneln von ihrer Konzeption her vielmehr der ATL-Klassenbibliothek, die zur Programmierung von COM-Objekten verwendet wird. Die Template-Klassen zur OLE DB-Programmierung teilen sich gemäß der OLE DB-Architektur in Nutzer-Template-Klassen und AnbieterTemplate-Klassen auf. An dieser Stelle sind lediglich die NutzerTemplate-Klassen von Interesse, da diese für die Entwicklung von Client-Anwendungen für Datenbanken benötigt werden.
4.4.2
Die OLE DB-Nutzer-Template-Klassen
Die Nutzer-Template-Klassen führen kein eigenes Objektmodell ein, sondern basieren auf dem Objektmodell der OLE DB-COM-Schnittstellen zur Programmierung von OLE DB-Nutzern. Um einen OLE DB-Nutzer zu implementieren, sind folgende Objekte notwendig: 왘 Eine Instanz der Klasse CDataSource. Ein Objekt dieser Klasse repräsentiert die Datenquelle. 왘 Ein Objekt der Klasse CSession. Durch ein Objekt dieser Klasse wird eine Verbindung zur Datenquelle hergestellt. Es können gleichzeitig mehrere Verbindungen zu einer Datenquelle bestehen. 왘 Zum Öffnen von Datenquelle und Verbindung werden Objekte der Klasse CDBPropSet benötigt. Diese Objekte legen Eigenschaften von Datenquelle und Verbindung fest. 왘 Ein Zugriffsobjekt der Klassen CAccessor, CManualAccessor, CDynamicAccessor und CDynamicParameterAccessor. Durch ein Zugriffsobjekt wird die Bindung an die Spalten der Datenbank festgelegt. Ein einfache, statische Bindung wird durch ein
OLE DB
545
Objekt der Klasse CAccessor definiert. Dieses bekommt als Typparameter eine Klasse übergeben, die die Anordnung der zu bindenden Datenelemente beschreibt. Die als Parameter übergebene Klasse hat keine Basisklasse. 왘 Objekte der Klassen CTable und CCommand erlauben den Zugriff auf die Datensätze der Datenquelle. Objekte der Klasse CTable erlauben einen einfachen Zugriff auf Datensätze. Objekte der Klasse CCommand greifen auf Datensätze über Abfragen zu, die beispielsweise in SQL formuliert sein können. Von diesen Objekten werden CRowset-Objekte verwaltet, die weitgehend den Satzgruppen aus ODBC und DAO entsprechen. Gibt man bei der Erzeugung eines CTable- oder CCommand-Objekts keine CRowsetKlasse an, so wird implizit ein Objekt der Klasse CRowset verwendet. Abbildung 4.22 zeigt das Objektmodell der OLE DB-Nutzerklassen. Nutzer-Architektur Hilfsklassen CDataSource
CEnumerator
CBookmark
CSession
CDBErrorInfo
SchemaRowset
CDBPropIDSet
T = Bindungsklasse CAccessorBase
CDynamicAccessor
CDBPropSet
T
CManualAccessor
CAccessor CRowset
CDynamicParameterAccessor
CBulkRowset
TAccessor = T oder eine der Accessor-Klassen
CArrayRowset
TRowset = CRowset oder CBulkRowset
TAccessor
TRowset
CAccessorRowset
CTable
CCommand
Abbildung 4.22: Die OLE DB-Nutzerklassen
546
4
4.4.3
Datenbankprogrammierung
OLE DB-Nutzer-Attribute
Mit der Einführung der OLE DB-Nutzer-Attribute in Visual Studio .NET hat sich die Erstellung von OLE DB-Nutzern deutlich vereinfacht. An die Stelle von aufwändigen Template-Konstrukten sind nun einfache Nutzer-Attribute getreten. Allerdings werden intern die gleichen OLE DB-Template-Klassen verwendet, wie zuvor. Daher ist ein Verständnis der Struktur der OLE DB-Template-Klassen weiterhin vorteilhaft. Die Grundlagen der attributierten Programmierung werden in Abschnitt 3.2.20, »Attributierte Programmierung«, im Rahmen der COM-Programmierung beschrieben.
4.4.4
Das Beispielprogramm StockOLEDB
Als erstes Beispiel soll eine einfache, formularbasierte Anwendung auf der Basis von OLE DB implementiert werden. Das Beispielprogramm StockOLEDB entspricht in seiner Funktionalität weitgehend den vorherigen Beispielen StockODBC und StockDAO. OLE DB-Anbieter auswählen
Das Beispielprogramm StockOLEDB ist mit dem AnwendungsAssistenten angelegt worden. Bei DATENBANKUNTERSTÜTZUNG ist die Datenbankoption DATENBANKANSICHT OHNE DATEISUPPORT ausgewählt worden. Um ein Programm mit der Unterstützung für OLE DB auszustatten, wählt man den Client-Typ OLE DB aus. Nach einem Klick auf die Schaltfläche DATENQUELLE wird in einem Auswahldialog (Abbildung 4.23) ein OLE DB-Anbieter selektiert. Für das Beispielprogramm StockOLEDB ist der OLE-DB-Anbieter für Jet ausgewählt worden. Hat man den Anbieter ausgewählt, so gelangt man durch einen Klick auf die Schaltfläche WEITER auf die nächste Registerkarte des Dialogs. Diese ist in Abbildung 4.24 zu sehen. Auf der Registerkarte VERBINDUNG muss man die zu verwendende Datenquelle angeben. Diese Registerkarte sieht je nach gewähltem Anbieter unterschiedlich aus. Im Fall des Jet-Anbieters muss hier der Pfad zu der ausgewählten Datenbank angegeben werden.
OLE DB
Abbildung 4.23: OLE DB-Anbieter auswählen
Abbildung 4.24: Verbindungseigenschaften des Jet-Anbieters
547
548
4
Datenbankprogrammierung
Auf der Registerkarte ERWEITERT können weitere Optionen angegeben werden, wie beispielsweise Zugriffsberechtigungen auf die Datenquelle. Für das Beispielprogramm StockOLEDB wurde hier nichts verändert. Die Registerkarte ALLE fasst alle Optionen in einer tabellarischen Übersicht zusammen. Durch einen Klick auf die Schaltfläche VERBINDUNG TESTEN kann probeweise eine Verbindung zur ausgewählten Datenquelle aufgebaut werden. Dabei werden die Angaben zu Datenbank, Benutzername und Kennwort überprüft. Sollte die Verbindung nicht aufgebaut werden können, dann wird eine Fehlermeldung ausgegeben. Wie bei den Beispielprogrammen StockODBC und StockDAO sind, nachdem das Projekt durch den Anwendungs-Assistenten erzeugt wurde, im Ressourceneditor Eingabefelder für Aktiennummer (schreibgeschützt), Aktienname, Wertpapierkennnummer und Tickersymbol angelegt worden. Der Datentausch zwischen Satzgruppe und Eingabefeldern erfolgt per DDX. Wenn man sich den vom Anwendungs-Assistenten generierten Programmcode ansieht, stellt man fest, dass sich dieser insbesondere bei der Satzgruppenklasse CStockOLEDBSet sehr von den entsprechenden Versionen für ODBC und DAO unterscheidet. Die Klasse CStockOLEDBSet wird ausschließlich inline in der HeaderDatei STOCKOLEDBSET.H definiert. Listing 4.20 zeigt diese Datei. // StockOLEDBSet.h: interface of the CStockOLEDBSet class // #pragma once [ db_source("\ Provider=Microsoft.Jet.OLEDB.4.0; User ID=Admin;\ Data Source=C:\\Visual Studio .NET\\Db\\Aktien.mdb;\ Mode=Share Deny None;\ Extended Properties=\"\";Jet OLEDB:System database=\"\";\ Jet OLEDB:Registry Path=\"\";Jet OLEDB:Database\ Password=\"\";Jet OLEDB:Engine Type=4;Jet OLEDB:\ Database Locking Mode=0;Jet OLEDB:Global Partial Bulk\ Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:\ New Database Password=\"\";Jet OLEDB:Create System\
OLE DB
549 Database=False;Jet OLEDB:Encrypt Database=False;Jet\ OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:\ Compact Without Replica Repair=False;Jet OLEDB:\ SFP=False
"), db_table("Aktie") ] class CStockOLEDBSet { public: // The following wizard-generated data members contain status // values for the corresponding fields. You // can use these values to hold NULL values that the database // returns or to hold error information when the compiler // returns // errors. See "Field Status Data Members in WizardGenerated // Accessors" in the Visual C++ documentation for more // information on using these fields. DBSTATUS DBSTATUS DBSTATUS DBSTATUS
[ db_column( 2, status=m_dwAktiennameStatus, length=m_dwAktiennameLength) ] TCHAR m_Aktienname[51]; // Name der Aktie [ db_column( 1, status=m_dwAktiennummerStatus, length=m_dwAktiennummerLength) ] LONG m_Aktiennummer; // Eindeutige Nummer, die genau die // Aktie einer Firma beschreibt
In Listing 4.20 wird durch Angabe des Attributs db_source zunächst die Datenquelle spezifiziert. Das Attribut db_source bekommt als Parameter einen Verbindungs-String übergeben, der sich aus den zuvor im MFC-Anwendungs-Assistenten per Dialogfeld angegebenen Datenbankeigenschaften zusammensetzt. Teil der Datenbankeigenschaften ist im Fall des Jet-Nutzers unter anderem der absolute Pfad zur Datenbank. Dies ist zwar unschön, wurde aber für dieses Beispiel unverändert übernommen, da sich durch alleinige Verwendung von Attributen hier keine elegantere Lösung finden lässt. Eine andere Methode, dieses Problem anzugehen, wird im nächsten Beispiel OLEDBChart gezeigt werden. In diesem Beispiel ist es jedoch wichtig, darauf zu achten, dass der absolute Pfad mit der tatsächlichen Lage der Acess-Datenbank übereinstimmt. Anderenfalls gibt es beim Versuch, die Datenbank zu öffnen, eine Fehlermeldung.
db_table
Nach Angabe der Datenquelle durch db_source wird durch das Attribut db_table eine Datensatzgruppenklasse zum Zugriff auf die Tabelle Aktie erzeugt. Sowohl Datenzugriff wie Datensatzgruppe
OLE DB
551
werden durch die nachfolgend angegebene C++-Klasse CStockOLEDBSet implementiert. Innerhalb dieser Klasse hat der MFCAnwendungs-Assistent zunächst eine Gruppe von Statusvariablen und dann eine Gruppe von Längenvariablen angelegt. Beide Gruppen sind zur Verwaltung der eigentlichen Satzgruppenvariablen notwendig, brauchen aber nicht näher betrachtet zu werden. Nach den Längenvariablen werden die eigentlichen Satzgruppenvariablen angelegt. Jede Definition wird durch das Attribut db_column eingeleitet. Das Attribut db_column stellt die Verbindung zwischen einer Satzgruppenvariablen und der entsprechenden Spalte der Datenbanktabelle her. Die Zuordnung erfolgt dabei anhand der Position der Spalte in der Datenbanktabelle beginnend mit 1. Alternativ kann man db_column auch den Namen der Datenbankspalte als ersten Parameter übergeben. Hinter dem Attribut db_column, also hinter der schließenden eckigen Klammer, wird jeweils die eigentliche Satzgruppenvariable definiert. Schließlich hat der MFC-Anwendungs-Assistent die Funktion GetRowProperties angelegt. Diese definiert einige Eigenschaften der Satzgruppenklasse. Listing 4.21 zeigt die Implementierungsdatei der Ansichtsklasse CStockOLEDBView des Beispielprogramms StockOLEDB. Hier sind insbesondere die Funktionen OnInitialUpdate, OnGetRowset und OnMove interessant. // StockOLEDBView.cpp : Implementierung der Klasse // CStockOLEDBView #include "stdafx.h" #include "StockOLEDB.h" #include "StockOLEDBSet.h" #include "StockOLEDBDoc.h" #include "StockOLEDBView.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////// // CStockOLEDBView IMPLEMENT_DYNCREATE(CStockOLEDBView, COleDBRecordView)
{ CWaitCursor wait; HRESULT hr = m_pSet->OpenAll(); if (FAILED(hr)) { AfxMessageBox(_T("Record set failed to open."), MB_OK); // Deaktivieren der Datensatzbefehle Nächster und // Vorheriger, da der Versuch, den aktuellen Datensatz // ohne geöffnetes RecordSet zu ändern, einen Absturz // verursacht. m_bOnFirstRecord = TRUE; m_bOnLastRecord = TRUE; } if( hr == DB_S_ENDOFROWSET ) { // the rowset is empty (does not contain any rows) AfxMessageBox(_T("Record set opened but there were no" "rows to return."), MB_OK); // Disable the Next and Previous record commands m_bOnFirstRecord = TRUE; m_bOnLastRecord = TRUE; } } // gehe zum ersten Datensatz m_pSet->MoveFirst(); COleDBRecordView::OnInitialUpdate(); } ///////////////////////////////////////////////////////////// // CStockOLEDBView Drucken BOOL CStockOLEDBView::OnPreparePrinting(CPrintInfo* pInfo) { // Standardvorbereitung return DoPreparePrinting(pInfo); } void CStockOLEDBView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // ZU ERLEDIGEN: Zusätzliche Initialisierung vor dem Drucken // hier einfügen }
////////////////////////////////////////////////////////////// // CStockOLEDBView Nachrichten-Handler BOOL CStockOLEDBView::OnMove(UINT nIDMoveCommand) { // Daten aus der Ansicht holen UpdateData (); // Move durchführen BOOL bMoveResult = COleDBRecordView::OnMove(nIDMoveCommand); if (bMoveResult) { // Daten in die Ansicht übernehmen
OLE DB
555 UpdateData (false);
} return bMoveResult; } Listing 4.21: Die Ansichtsklasse CStockOLEDBView
Die Funktion OnGetRowset ist die Entsprechung der Funktion OnGetRecordset der Klassen CRecordView und CDaoRecordView. Der andere Name dieser Funktion rührt lediglich von der Tatsache her, dass eine Gruppe von Datensätzen bei OLE DB als Rowset und nicht als Recordset bezeichnet wird. Die Arbeitsweise der Funktionen ist ähnlich. Auch OnGetRowset ermöglicht den Zugriff auf ein Satzgruppenobjekt.
OnGetRowset
In der Funktion OnInitialUpdate wird die Satzgruppe durch die Funktion OpenAll geöffnet. Die Funktion OpenAll wird automatisch bei der Expansion der in den Quelltext eingefügten Attribute erstellt. Man kann sich den Quelltext dieser Funktion und den des weiteren Attributcodes ansehen, indem man das Projekt mit dem Compiler-Schalter /Fx übersetzt (vgl. Abbildung 4.28). Schlägt der Aufruf der Funktion OpenAll fehl, so wird eine Fehlermeldung ausgegeben. Der abschließende Programmcode, der die Werte des ersten Datensatzes in die Steuerelemente übernimmt, wurde nicht vom Anwendungs-Assistenten generiert, sondern musste von Hand eingefügt werden. Auch der zum Navigieren durch die Datensätze notwendige Programmcode wird bei einem OLE DB-Programm nicht durch einen der Assistenten erzeugt. Das Navigieren selbst ist zwar bereits durch die Funktion COleDBRecordView::OnMove implementiert, allerdings gibt es keinen Mechanismus, der die Datensätze in die Steuerelemente der Ansicht übernimmt. Um einen solchen Mechanismus zu implementieren, muss die Funktion OnMove überschrieben werden. In Listing 4.21 ist die Implementierung dieser Funktion zu sehen. Zunächst werden durch Aufruf der Funktion UpdateData die Werte der Steuerelemente der Ansicht durch DDX ausgelesen. Durch den Aufruf der Funktion OnMove der Basisklasse werden veränderte Werte in die Datenbank übernommen. Das durch nIDMoveCommand angegebene Kommando wird ausgeführt und der neue Datensatz eingelesen. Ein Aufruf der Funktion Update mit dem Parameter false schreibt die neuen Werte in die Steuerelemente der Ansicht zurück.
OnMove
556
4
Datenbankprogrammierung
Vergleicht man OLE DB mit ODBC, so stellt man fest, dass OLE DB durch die Assistenten der Entwicklungsumgebung schlechter unterstützt wird, was die Einbindung in die MFC anbelangt. Auf der anderen Seite wird durch die OLE DB-Nutzer-Attribute viel Programmcode implizit erzeugt und anschließend vor dem Programmierer versteckt. Sichtbar machen kann man diesen Programmcode durch den Compiler-Schalter /Fx.
4.4.5
Das Beispielprogramm OLEDBChart
Auch das zweite Datenbankbeispiel soll mit der Hilfe von OLE DB realisiert werden. Das Programm wird wie die beiden Beispielprogramme ODBCChart und DAOChart mit dem AnwendungsAssistenten angelegt, ohne dabei die Option DATENBANKUNTERSTÜTZUNG auszuwählen. Die Dialogressource zur Auswahl der Aktie wird in das Projekt übernommen und die Dialogklasse CStockChooser angelegt. Wie in den vorherigen Beispielen wird der Auswahldialog innerhalb der Funktion OnNewDocument der Dokumentenklasse aufgerufen. Um eine Satzgruppenklasse zu einem Projekt hinzuzufügen, wählt man im Kontextmenü des Projekts den Eintrag HINZUFÜGEN | KLASSE HINZUFÜGEN... und dann im Ordner ATL das Icon ATLOLEDB-CONSUMER (Abbildung 4.25).
Abbildung 4.25: OLE DB-Nutzerklasse hinzufügen
OLE DB
Hat man ATL-OLEDB-Consumer ausgewählt, so erscheint das in Abbildung 4.26 gezeigte Dialogfeld. Dieses Dialogfeld ist zu Beginn leer. Zunächst muss durch einen Klick auf die Schaltfläche DATENQUELLE ein OLE DB-Anbieter ausgewählt werden.
Abbildung 4.26: Eigenschaften der Satzgruppenklasse festlegen
Das Festlegen des OLE DB-Anbieters und seiner Einstellungen erfolgt durch die Dialogfelder, die bereits im Rahmen des Beispielprogramms StockOLEDB besprochen wurden (siehe Abbildungen 4.23 und 4.24 in Abschnitt 4.4.3, »Das Beispielprogramm StockOLEDB«). Hat man die Datenquelle bestimmt, so kann man Klassennamen und Dateinamen ändern. Für TYP wird die voreingestellte Option BEFEHL übernommen; diese erzeugt eine auf dem Attribut db_command aufbauende Satzgruppenklasse. Die Option TABELLE arbeitet dagegen mit dem Attribut db_table. Die drei Kontrollkästchen ÄNDERN, EINFÜGEN und LÖSCHEN bestimmen die Zugriffsrechte. Für das Beispielprogramm OLEDBChart wird hier nichts ausgewählt, da nur lesender Zugriff erlaubt sein soll. Das Kontrollkästchen ATTRBUTIERT bestimmt schließlich, ob mit attributierter Programmierung gearbeitet werden soll oder ob die OLE DB-Template-Klassen direkt verwendet werden sollen. Im Normalfall sollte man hier immer ATTRIBUTIERT auswählen, da der erzeugte Programmcode um einiges übersichtlicher ist.
557 Nutzer hinzufügen
558
4
Datenbankprogrammierung
Nach dem Verlassen des Dialogfelds mit OK wird man in einem weiteren Dialogfeld aufgefordert, eine Tabelle der Datenbank auszuwählen. Dieser Dialog ist in Abbildung 4.27 zu sehen.
Abbildung 4.27: Datenbanktabelle auswählen
Leider lässt sich hier nur eine Datenbanktabelle auswählen. Möchte man auf mehrere Tabellen gleichzeitig zugreifen, so muss man den generierten Programmcode später von Hand bearbeiten. Hat man auch diesen Dialog bestätigt, dann generiert der Assistent eine Satzgruppenklasse. Die erzeugte Klasse implementiert eine Funktion OpenAll, um die Datenquelle zu öffnen und eine Abfrage durchzuführen. Diese Funktion ist allerdings im Quelltext nicht zu sehen, da sie durch das Attribut db_command implizit erzeugt wird. Für das Beispielprogramm OLEDBChart wurden mit dem ATLAssistenten die Satzgruppenklassen CShare und CQuotes angelegt. Beide Klassen werden ausschließlich durch ihre Header-Dateien implementiert. Listing 4.22 zeigt die Klasse CShare. // share.h : Declaration of the CShare #pragma once [ db_source(""),
OLE DB
559
db_command(" \ SELECT \ Aktienname, \ Aktiennummer, \ Tickersymbol, \ WKN \ FROM Aktie") ] class CShare { public: // The following wizard-generated data members contain status // values for the corresponding fields. You // can use these values to hold NULL values that the database // returns or to hold error information when the compiler // returns // errors. See "Field Status Data Members in WizardGenerated // Accessors" in the Visual C++ documentation for more // information on using these fields. DBSTATUS DBSTATUS DBSTATUS DBSTATUS
[ db_column( 1, status=m_dwAktiennameStatus, length=m_dwAktiennameLength) ] TCHAR m_Aktienname[51]; // Name der Aktie [ db_column( 2, status=m_dwAktiennummerStatus, length=m_dwAktiennummerLength) ] LONG m_Aktiennummer; // Eindeutige Nummer, die genau die // Aktie einer Firma beschreibt
560
4
Datenbankprogrammierung
[ db_column( 3, status=m_dwTickersymbolStatus, length=m_dwTickersymbolLength) ] TCHAR m_Tickersymbol[7]; // Tickersymbol [ db_column( 4, status=m_dwWKNStatus, length=m_dwWKNLength) ] LONG m_WKN; // Wertpapierkennnummer void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty( DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty( DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); } }; // eigene Klasse ableiten class CMyShare : public CShare { public: HRESULT Open (CString& strUDLFile) { HRESULT hr; hr = m_source.OpenFromFileName( strUDLFile.AllocSysString()); if (FAILED(hr)) return hr; hr = m_session.Open(m_source); if (FAILED(hr)) return hr; return OpenRowset(m_session); } }; Listing 4.22: Die Klasse CShare
OLE DB
561
Die Klasse CShare implementiert den Zugriff auf die Tabelle Aktie der Beispieldatenbank. Um den Zugriff auf die Datenbank flexibler zu gestalten, ist von der Klasse CShare eine weitere Klasse CMyShare abgeleitet worden. In dieser Klasse implementiert die Funktion Open einen flexiblen Zugriff auf die Datenbank, indem sie die Datenbank nicht durch einen absoluten Pfad referenziert, sondern durch die Angabe einer UDL-Datei. Dateien mit der Endung UDL dienen zur Spezifizierung eines Verbindungs-Strings für einen OLE DB-Nutzer. Sie werden auch als Microsoft Data Link-Dateien bezeichnet. Microsoft Data Link-Dateien sind einfache Textdateien, die von ihrer Struktur den altbekannten INI-Dateien ähneln. In der Sektion [ole db] wird der Verbindungs-String definiert, der anderenfalls direkt im Programm stünde. UDL-Dateien lassen sich per Doppelklick im Windows-Explorer öffenen und können dann direkt mit den OLE DB-Nutzer-Dialogfeldern (Abbildungen x und y) bearbeitet werden. Warum ist zur Implementierung der Funktion Open eigens eine neue Klasse abgeleitet worden? Hätte es nicht gereicht, die Klasse CShare einfach um diese Funktion zu ergänzen? Zur Klärung dieser Frage sollte man die Klasse CShare mit dem Compiler-Schalter /Fx übersetzen (siehe Abbildung 4.28). Listing 4.23 zeigt den Inhalt dieser Datei.
Abbildung 4.28: Der Compiler-Schalter /Fx
UDL-Dateien
562
4 // // // // // // // // // // //
Datenbankprogrammierung
Created by Microsoft (R) C/C++ Compiler Version 13.00.9030 c:\beispielprogramme\kapitel4\oledbchart\share.mrg.h compiler-generated file created 03/31/01 at 19:19:15 This C++ source file is intended to be a representation of the source code injected by the compiler. It may not compile or run exactly as the original source file.
// eigene Klasse ableiten class CMyShare : public CShare { public: HRESULT Open (CString& strUDLFile) { HRESULT hr; hr = m_source.OpenFromFileName( strUDLFile.AllocSysString()); if (FAILED(hr)) return hr; hr = m_session.Open(m_source); if (FAILED(hr)) return hr;
OLE DB
567 return OpenRowset(m_session);
} }; Listing 4.23: Die Datei share.mgr.h
In Listing 4.23 ist zu sehen, dass die Klasse CShare aus Listing 4.22 nach der Expandierung der Attribute zu einer Klasse mit dem Namen _CShareAccessor wird. Die resultierende Klasse CShare verwendet _CShareAccessor lediglich als Template-Parameter. Erweitert man _CShareAccessor um eigene Funktionen, so wird man diese in der endgültigen Klasse CShare nicht wiederfinden. Auch die Fehlermeldungen des Compilers sind, ohne einen Blick auf den expandierten Programmcode zu werfen, eher unverständlich. Möchte man automatisch generierte Satzgruppenklassen um eigene Datenelemente oder Funktionen ergänzen, so hat es sich als sinnvoll erwiesen, von der Satzgruppenklasse eine eigene Klasse abzuleiten und in dieser die gewünschte Funktionalität zu implementieren. Auf diese Weise kommt man dem Compiler und seinen Eigenheiten bei der Expansion von Attributen nicht in die Quere. Doch nun zurück zu Listing 4.22. Beim Attribut db_source wurde der Verbindungs-String gelöscht, da das Öffnen der Datenbank durch die eigene Funktion Open der abgeleiteten Klasse CMyShare erfolgen soll. Das Attribut db_command besitzt als Parameter eine einfache Datenbankabfrage in SQL, die der Assitent hier bereits so eingefügt hat. Die SQL-Abfrage selektiert einfach alle Datensätze der zugrunde liegenden Datenbanktabelle Aktie. Innerhalb der Klasse CShare (eigentlich _CShareAccessor) werden – wie bereits im letzten Beispiel – Status- und Längenvariablen sowie die eigentlichen Satzgruppenvariablen deklariert. Anschließend wird von CShare die eigene Satzgruppenklasse CMyShare abgeleitet. Diese öffnet die Satzgruppe durch Angabe einer UDL-Datei anstelle eines festen Verbindungs-Strings. Somit muss das Programm nicht neu übersetzt werden, wenn sich der Speicherort der Datenbank verändert. Der Pfad und Name der UDL-Datei werden beim Programmstart in der Funktion InitInstance durch einen Dateidialog erfragt und in der öffentlichen Variablen strDBName des Applikationsobjekts gespeichert. Diese Variable wird der Funktion Open der Satzgruppenklasse übergeben, um die Datenbank zu spezifizieren.
568
4
Datenbankprogrammierung
Verwendet wird die Klasse CShare innerhalb des Aktienauswahldialogs in der Klasse CStockChooser. Listing 4.24 zeigt die Implementierungsdatei dieser Klasse. // StockChooser.cpp: Implementierungsdatei // #include "stdafx.h" #include "OLEDBChart.h" #include "StockChooser.h" #include "Share.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////// // Dialogfeld CStockChooser
In der Funktion OnInitDialog wird die Liste der in der Datenbank vorhandenen Aktien in das Listenfeld des Dialogs übernommen. Dazu wird eine Variable der Klasse CMyShare angelegt. Die Satzgruppe wird durch Aufruf der Funktion Open geöffnet. Sollte sich die Datenquelle nicht öffnen lassen, so wird eine Fehlermeldung ausgegeben. In einer while-Schleife wird dann über alle Datensätze der Satzgruppe iteriert. Die Funktion MoveNext positioniert dabei den Cursor auf den jeweils nächsten Datensatz. Sobald die Funktion MoveNext einen Wert ungleich S_OK zurückgibt, ist der letzte Datensatz erreicht. Die Funktion AddString der Klasse CListBox übernimmt die Aktiennamen in das Listenfeld. Auch die zweite Satzgruppenklasse des Beispielprogramms OLEDBChart ist mit dem ATL-OLEDB-Nutzer-Assistenten angelegt worden. Im Gegensatz zur Klasse CShare musste die Klasse
570
4
Datenbankprogrammierung
CQuotes von Hand nachbearbeitet werden, um eine Abfrage über zwei Tabellen durchführen zu können. Listing 4.25 zeigt die Klasse CQuotes. // quote.h : Declaration of the CQuote #pragma once
[
db_source (""), db_command("")
] class CQuote { public: // The following wizard-generated data members contain status // values for the corresponding fields. You // can use these values to hold NULL values that the database // returns or to hold error information when the compiler // returns // errors. See "Field Status Data Members in WizardGenerated // Accessors" in the Visual C++ documentation for more // information // on using these fields. DBSTATUS m_dwAktiennummerStatus; DBSTATUS m_dwDatumStatus; DBSTATUS m_dwKursStatus; DBSTATUS m_dwTickersymbolStatus; DBSTATUS m_dwWKNStatus; DBSTATUS m_dwAktiennameStatus; // The following wizard-generated data members contain length // values for the corresponding fields. DBLENGTH m_dwAktiennummerLength; DBLENGTH m_dwDatumLength; DBLENGTH m_dwKursLength; DBLENGTH m_dwTickersymbolLength; DBLENGTH m_dwWKNLength; DBLENGTH m_dwAktiennameLength; [ db_column( 1,
OLE DB
571
status=m_dwAktiennummerStatus, length=m_dwAktiennummerLength) ] LONG m_Aktiennummer; // Eindeutige Nummer, die genau die // Aktie einer Firma beschreibt [ db_column( 2, status=m_dwDatumStatus, length=m_dwDatumLength) ] DATE m_Datum; // Datum des Kurses [ db_column( 3, status=m_dwKursStatus, length=m_dwKursLength) ] CURRENCY m_Kurs; // Schlusskurs für das betreffende Datum [ db_column( 4, status=m_dwAktiennameStatus, length=m_dwAktiennameLength) ] TCHAR m_Aktienname[51]; // Name der Aktie [ db_column( 5, status=m_dwWKNStatus, length=m_dwWKNLength) ] LONG m_WKN; // Wertpapierkennnummer [ db_column( 6, status=m_dwTickersymbolStatus, length=m_dwTickersymbolLength) ] TCHAR m_Tickersymbol[7]; void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty( DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty( DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); } };
572
4
Datenbankprogrammierung
// eigene Klasse ableiten class CMyQuote : public CQuote { public: CStringW GetCommand (CString& strShareName) { CStringW cmd; cmd = _T(" \ SELECT Kurs.Aktiennummer, Datum, Kurs,\ Aktienname, WKN, Tickersymbol \ FROM Kurs, Aktie \ WHERE Kurs.Aktiennummer = Aktie.Aktiennummer \ AND Aktienname = '"); cmd += strShareName; cmd += _T("' ORDER BY Datum "); return cmd; } HRESULT Open (CString& strUDLFile, CString& strShareName) { HRESULT hr; hr = m_source.OpenFromFileName( strUDLFile.AllocSysString()); if (FAILED(hr)) return hr; hr = m_session.Open(m_source); if (FAILED(hr)) return hr; return OpenRowset(m_session, GetCommand (strShareName)); } }; Listing 4.25: Die Klasse CQuotes
Die vom Assistenten generierte Klasse CQuotes ist von Hand um die Status- und Längenvariablen für die Tabelle Aktie ergänzt worden. Ebenso wurden Attribute vom Typ db_column für die Ergebnisse der Datenbankabfrage hinzugefügt. Die Abfrage selbst zeigt Listing 4.26. SELECT Kurs.Aktiennummer, Datum, Kurs, Aktienname, WKN, Tickersymbol FROM Kurs, Aktie
OLE DB WHERE Kurs.Aktiennummer = Aktie.Aktiennummer AND Aktie.Aktienname = <shareName> ORDER BY Datum Listing 4.26: Die von CQuotes verwendete Abfrage
Für <shareName> muss der im Aktienauswahldialog gewählte Aktienname in die Abfrage eingefügt werden. Der SQL-Kommando-String wird innerhalb der Funktion GetCommand konstruiert. Die Funktion GetCommand bekommt den Aktiennamen als Parameter übergeben und setzt diesen in die SQL-Abfrage ein. Das SQL-Kommando definiert so innerhalb der Funktion GetCommand die Reihenfolge der zu erwartenden Ergebnisspalten. Diese wurde bei der Definition der Bindung durch die db_commandAttribute berücksichtigt. Das SQL-Kommando wird der Funktion OpenRowset beim Öffnen der Datenbankabfrage übergeben. Durchgeführt wird die Abfrage der Aktienkurse wie in den Beispielprogrammen ODBCChart und DAOChart in der Funktion OnNewDocument der Dokumentenklasse. Listing 4.27 zeigt die Dokumentenklasse des Programms OLEDBChart. // OLEDBChartDoc.cpp : Implementierung der Klasse COLEDBChartDoc // #include "stdafx.h" #include "OLEDBChart.h" #include "OLEDBChartDoc.h" #include "StockChooser.h" #include "Quotes.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// // COLEDBChartDoc IMPLEMENT_DYNCREATE(COLEDBChartDoc, CDocument) BEGIN_MESSAGE_MAP(COLEDBChartDoc, CDocument) END_MESSAGE_MAP()
void COLEDBChartDoc::CalcAverages () { int nCnt = m_stockData.theData.GetCount (); POSITION outerPos, innerPos; double value; m_averageData.RemoveAll (); // auf den Anfang der Datenliste setzen outerPos = m_stockData.theData.GetHeadPosition (); for (int i=0; i
In der Funktion OnNewDocument wird zunächst ein Objekt der Klasse CMyQuotes angelegt. Dann wird der Aktienauswahldialog angezeigt und damit der Aktienname erhalten. Der Aktienname wird beim Aufruf der Funktion Open der Klasse CMyQuotes als Parameter übergeben. Die Funktion Open führt die Datenbankabfrage aus und öffnet die Satzgruppe. Nachdem der Titel des Dokuments gesetzt und eine Sprach-ID für die Konvertierung von Kurswerten angelegt worden ist, wird durch den Aufruf der Funktion MoveFirst der Cursor der Satzgruppe auf den ersten Datensatz positioniert.
OLE DB
577
Genau wie im Beispielprogramm DAOChart müssen die Kurswerte, die als Währungswerte vorliegen, in Strings konvertiert werden. Die OLE DB-Klassen verwenden den COM-Datentyp CURRENCY direkt. Variablen des Typs CURRENCY lassen sich allerdings Objekten des Typs COleCurrency zuweisen, was eine Konvertierung in Strings vereinfacht. Die Konvertierung von COleCurrency-Werten wird dann, wie schon beim Beispielprogramm DAOChart besprochen, durch die Funktion Format der Klasse COleCurrency vorgenommen. Aus dem ersten Datensatz der Satzgruppe werden der Aktienname, die Wertpapierkennnummer und das Tickersymbol in das Dokument übernommen. Anschließend wird in einer whileSchleife über die restlichen Datensätze der Satzgruppe iteriert. Wenn die Funktion MoveNext der Satzgruppenklasse einen Wert ungleich S_OK zurückgibt, dann ist der letzte Datensatz erreicht. Abschließend werden der minimale und der maximale Kurswert gesetzt und die Durchschnittslinie wird berechnet.
4.4.6
Tipps zur Vorgehensweise
왘 Die OLE DB-Template-Klassen wurden unabhängig von den MFC auf Basis der ATL-Klassenbibliothek realisiert. Dementsprechend können die Template-Klassen nur durch Einbindung der ATL verwendet werden. Dies wird normalerweise automatisch durch die Assistenten der Entwicklungsumgebung vorgenommen. 왘 OLE DB lässt sich sowohl mit attributierter Programmierung wie auch direkt durch die OLE DB-Template-Klasse verwenden. Bei der Programmierung mit Attributen bleibt der Programmcode deutlich übersichtlicher. 왘 Möchte man die vom Assitenten generierten Klassen erweitern, so bietet es sich an, eigene Klassen von diesen abzuleiten. Auf diese Weise kommt man dem Attribut-Expansionsmechanismus des Compilers nicht in den Weg. 왘 Um die Struktur des erzeugten Programmcodes besser zu verstehen, bietet es sich an, bei der Übersetzung den CompilerSchalter /Fx zu verwenden. So kann man die Struktur des Programmcodes besser verstehen und die Verbindung zwischen OLE DB-Nutzer-Attributen und den OLE DB-Template-Klassen erkennen.
578
4
4.4.7
Datenbankprogrammierung
Zusammenfassung
OLE DB hat den Anspruch, eine universelle Schnittstelle für Datenquellen aller Art zu sein. Diese Universalität bringt naturgemäß eine gewisse Komplexität mit sich. Die Programmierung von OLE DB konnte daher an dieser Stelle nur angeschnitten werden. In die MFC ist OLE DB kaum integriert; es ist auch nicht wahrscheinlich, dass es jemals eigene MFC-Klassen zur OLE DB-Programmierung geben wird.
4.5
Zusammenfassung zur Datenbankprogrammierung
In diesem Kapitel sind drei Technologien zum Datenbankzugriff vorgestellt worden. Alle drei haben ihre Stärken und Schwächen. ODBC ist sehr weit verbreitet, DAO ist speziell auf den Jet-Datenbankkern zugeschnitten und OLE DB ist eine universelle Datenschnittstelle, die über Datenbanken hinausgeht. Andererseits ist ODBC vom Funktionsumfang relativ beschränkt, man verwendet oft den kleinsten gemeinsamen Nenner aller Datenbank-Managementsysteme. DAO ist nicht datenbankunabhängig und wird außerdem nicht mehr vollständig unterstützt und OLE DB ist nicht vollständig in die MFC integriert. Tabelle 4.2 vergleicht die drei Technologien. ODBC
DAO
OLE DB
DBMSunabhängig
ja
nein
ja
Integration in die MFC
gut
gut
wenig integriert
Treiberangebot
sehr groß
Zugriff auf andere DBMS nur über Jet
für viele kommerzielle DBMS
COMbasiert
nein
ja
ja
Schwächen
Der volle Funktionsumfang eines DBMS ist über ODBC meist nicht nutzbar.
Wird nicht weiterentwickelt. Für neue Projekte nicht mehr zu empfehlen.
OLE DB ist eine komplexe Technologie, die noch nicht die Verbreitung von ODBC hat.
Stärken
ODBC ist weit verbreitet und ausgereift.
DAO kann die speziel- OLE DB ist flexibel len Fähigkeiten von und nicht auf DBMS MS Access-Datenban- beschränkt. ken ausnutzen.
Tabelle 4.2: Gegenüberstellung von ODBC, DAO und OLE DB
Zusammenfassung zur Datenbankprogrammierung
Welche Schnittstelle man zum Datenbankzugriff auswählt, hängt daher von den spezifischen Anforderungen an das Projekt ab, das man mit der Datenbank realisieren möchte. Sehr vereinfacht kann man sagen, dass ODBC praxiserprobt und verbreitet, DAO gut für MS Access-Datenbanken geeignet und OLE DB die universellste Datenschnittstelle ist. Für sehr große Datenmengen bei der Verwendung von Serverdatenbanken wird man allerdings keine der drei Schnittstellen verwenden, sondern den Datenbankserver über seine eigene (native) Schnittstelle ansprechen. Für neue Projekte sollte man DAO nicht mehr verwenden, da diese Schnittstelle nicht mehr weiterentwickelt wird.
579
5 Internetprogrammierung Dieses Kapitel behandelt drei Teilbereiche der Internetprogrammierung: die Verwendung von HTML in eigenen Programmen, die Programmierung von Internetclient-Programmen mit der WinInet-Bibliothek und die Erstellung von Server-Erweiterungen für die ISAPI-Schnittstelle.
5.1
Einführung
Wer sich bei der Überschrift »Internetprogrammierung« fragt, was denn da programmiert werden soll, der tut das nicht ganz zu unrecht. Der Begriff ist nicht eindeutig, er kann vieles bedeuten. Zum großen Teil liegt das daran, dass auch der Begriff Internet nicht eindeutig definiert ist. Was also ist das Internet? Internet ist ein Sammelbegriff für Kommunikationsprotokolle einerseits und die Anordnung und Verteilung einer großen Anzahl miteinander vernetzter Computer andererseits. Ein Kommunikationsprotokoll legt fest, wie verschiedene Computer, Teile innerhalb eines Computers oder Programme miteinander Daten austauschen können. Protokolle können durch Hardware (wie beispielsweise Ethernet, eine Verbindungstechnik für lokale Netzwerke) oder Software (wie beispielsweise FTP, File Transfer Protocol, ein Protokoll zur Dateiübertragung) implementiert werden. Im Internet wird eine ganze Reihe von Protokollen verwendet, die teilweise aufeinander aufbauen. Auf einige Protokolle des Internets geht der Abschnitt 5.3.1, »Protokolle im Internet«, ein. Auf den Protokollen aufbauend werden Dienste im Internet definiert, für die es jeweils typische Anwendungsprogramme gibt. Jeder Dienst stellt eine Dienstleistung zur Verfügung, die mit einem dazugehörigen Programm wahrgenommen werden kann. Beispiele für Dienste im Internet sind E-Mail, Dateitransfer und das World Wide Web (WWW).
582
5
Internetprogrammierung
Typisch für das Internet ist seine dezentrale Struktur. Es gibt keinen zentralen Punkt im Internet, alle Computer des Internets sind in einer netzartigen Struktur über die gesamte Erde verteilt. Diese netzartige Struktur und die globale Ausdehnung sind ebenfalls im Begriff Internet enthalten. Nutzt man lediglich die Protokolle des Internets in einem kleinen, abgeschlossenen Netz, so spricht man von einem Intranet. Im Gegensatz zum Internet kann ein Intranet auch eine ring- oder sternförmige Topologie (Netzwerkstruktur) haben. Ursprünge des Internets
Die dezentrale Struktur des Internets lässt sich auf die Ursprünge dieses Netzes zurückführen. Die grundlegenden Protokolle des Internets wurden in den 70er Jahren im Auftrag des amerikanischen Verteidigungsministeriums entwickelt. Man wollte ein Computernetzwerk erstellen, das in der Lage sein sollte, selbst einen nuklearen Erstschlag zu überstehen. Als logische Konsequenz dieser Anforderung durfte das Netz keine zentralen Angriffspunkte wie große Zentral- oder Knotenrechner besitzen. Das Netz musste die Fähigkeit haben, defekte Verbindungen zu umgehen und Daten über andere Leitungen ans Ziel zu befördern. Daher suchen sich die Datenpakete des Internets, basierend auf dem damals entwickelten Protokoll TCP/IP, den Weg zu ihrem Ziel selbst, statt von einem zentralen Rechner zum Ziel geleitet zu werden. Neben dem amerikanischen Militär verwendeten die amerikanischen Universitäten die Technik des Internets, die sie mitentwickelt hatten. Die Universitäten vernetzten sich untereinander und bildeten damit den Grundstock des heutigen Internets. Im Laufe der Jahre dehnte sich das so entstandene Netzwerk auch auf Bildungseinrichtungen außerhalb der USA aus. Ein globales Netzwerk entstand.
Internet-Zeitlinie
왘 1957: Die UdSSR startet den Sputnik-Satelliten. Als Reak-
tion darauf wird in den USA unter anderem die Forschungsbehörde ARPA (Advanced Research Projects Agency) gegründet. Die ARPA soll neue Technologien entwickeln, um den technischen Vorsprung vor der Sowjetunion sicherzustellen. 왘 1968: Die Vorarbeiten zu ARPANET, dem ersten paket-
orientierten Computernetzwerk beginnen. Das ARPANET soll in der Lage sein, einen atomaren Erstschlag zu überstehen.
Einführung
583
왘 1969: Das ARPANET wird in Betrieb genommen. Es verbin-
det zunächst vier Computer in amerikanischen Forschungseinrichtungen. 왘 1971: Das ARPANET verbindet mittlerweile 30 Teilnehmer. 왘 1973: Erste internationale Verbindungen zu Computern in
England und Norwegen werden aufgebaut. 왘 1975: Das TCP/IP-Protokoll wird entwickelt. Man verwen-
det es zunächst nur zum Zusammenschluss von Teilnetzen. 왘 1983: Der militärische Teil wird vom ARPANET abgespal-
ten (MILNET). Das ARPANET wird komplett auf TCP/IP umgestellt. Der Übergang vom ARPANET zum Internet vollzieht sich. 왘 1991: Das Internet wird für die kommerzielle Nutzung frei-
gegeben. Am CERN wird das World Wide Web entwickelt. 왘 1993: Der erste grafische Internetbrowser wird entwickelt
(NCSA-Mosaic). 왘 1994: Die Firma Netscape wird gegründet. Der gleichna-
mige Browser der Firma leitet den endgültigen Siegeszug des Internets ein. 왘 2002: In der westlichen Welt haben große Teile der Bevölke-
rung Zugang zum Internet. Das Internet fungiert als elektronischer Einkaufsplatz (eCommerce) und Unterhaltungsmedium. Das Internet wird auf mobilen Geräten nutzbar. Der kommerzielle Durchbruch kam erst einige Jahre später und – wie vieles in der Informationstechnik – eher überraschend und unerwartet. Eine kleine Gruppe von Forschern am Kernforschungszentrum CERN in der Schweiz hatte ein neues Internetprotokoll entwickelt, um Forschungsergebnisse im Internet bequem publizieren zu können. Neben dem Übertragungsprotokoll hatten sie eine Sprache entwickelt, mit der sich die zu publizierenden Dokumente formatieren ließen. Dazu schrieben sie erste einfache Programme, um diese Dokumente anzuzeigen. Das entwickelte Protokoll heißt HTTP (Hypertext Transfer Protocol) und die Sprache HTML (Hypertext Markup Language). Das Anzeigeprogramm war ein Vorläufer der heutigen Internetbrowser. Die neue Technik wurde von der Internetgemeinde schnell aufgenommen und verbreitet. Viele kleine Firmen, allen voran die Firma Netscape, erkannten das Potenzial des neuen Mediums und
HTTP und HTML
584
5
Internetprogrammierung
trieben seine Kommerzialisierung voran. Das damit einsetzende massive Wachstum hat zum Internet in seiner heutigen Form geführt.
5.1.1
Bereiche der Internetprogrammierung
Client-Programme
Die meisten Protokolle im Internet verwenden einen Client-Server-Ansatz, das heißt, auf Computern innerhalb des Internets laufen Serverprogramme, deren Dienstleistungen durch ClientProgramme, die auf anderen Computern im Internet ausgeführt werden, wahrgenommen werden können. Bestes Beispiel für ein Client-Programm ist ein Internetbrowser. Ein heute üblicher Browser implementiert zumeist eine ganze Reihe von Internetprotokollen, zumindest aber HTTP, um Webseiten abzurufen, und FTP, um Dateien übertragen zu können.
Serverprogramme
Als Gegenstück zu den Client-Programmen ist auf der Serverseite entsprechende Server-Software notwendig, die die auf verschiedenen Protokollen basierenden Dienstleistungen implementiert. Auf der Seite des Servers ist der Trend zur Integration verschiedener Dienste innerhalb eines Softwarepakets weniger ausgeprägt als auf der Client-Seite. Man verwendet daher oft für jedes Protokoll eigene Server-Software.
Anforderungen an Internetprogramme
Die Programmierung für das Internet spaltet sich in Client- und Serverprogrammierung auf. Die Anforderungen an beide Teilbereiche der Programmierung sind recht verschieden. Bei der Serverprogrammierung kommt es auf Stabilität, hohe Geschwindigkeit und die Möglichkeit, mehrere Anfragen gleichzeitig zu bearbeiten (Multitasking), an. Üblicherweise schreibt man keine neue Server-Software, um Probleme auf der Seite des Servers zu lösen, sondern entwickelt Programme, die mit bereits bestehender Server-Software zusammenarbeiten oder diese um zusätzliche Funktionen erweitern. Dabei kommt es auf eine einfache und schnelle Verbindung zur Server-Software an. Die Server-Software muss eine Schnittstelle für Erweiterungen besitzen oder die Möglichkeit haben, mit externen Programmen zusammenzuarbeiten.
Benutzerschnittstelle
Bei der Internetprogrammierung auf der Client-Seite hat man es oft mit Aspekten der Benutzerschnittstelle zu tun. Die Frage der Geschwindigkeit spielt eine untergeordnete Rolle, da man es meist nur mit einer einzigen Verbindung zu tun hat. Für viele Dienste haben sich mittlerweile Client-Programme etabliert, die unabhän-
Einführung
585
gig vom Hersteller eine relativ einheitlich zu bedienende Benutzerschnittstelle besitzen. So werden Webseiten mit Browsern verschiedener Hersteller betrachtet, die sich in Bedienung und Funktionsweise stark ähneln. Programme zur Dateiübertragung per FTP werden oft den Dateimanagern für lokale Dateisysteme nachempfunden. Auch die Programme zum Lesen und Versenden von E-Mails werden einander immer ähnlicher. Moderne Client-Programme – insbesondere Internetbrowser – besitzen eine hohe Komplexität. Die Eigenentwicklung solcher Programme ist normalerweise aufgrund der zu erwartenden Entwicklungszeit und -kosten nicht möglich. Es bietet sich daher an, bestehende Programme wie den Microsoft Internet Explorer oder den Netscape Communicator in eigene Programme zu integrieren oder von eigenen Programmen aus zu verwenden. Internetprogrammierung umfasst daher auch die Integration von selbst geschriebenen Programmen in bestehende Client-Anwendungen.
Integration bestehender Anforderungen
Neben Programmen für Standardaufgaben wie E-Mail, Dateitransfer und das WWW gibt es auch Fälle, in denen Client-Programme benötigt werden, die keiner standardisierten Nutzung der Internetdienste entsprechen. Solche Client-Programme müssen oft komplett selbst entwickelt werden. Beispiele für solche Programme sind Suchprogramme, die das Web automatisch nach bestimmten Seiten durchsuchen, und Programme, um den Inhalt ganzer Webserver zu laden.
Teilbereiche der Internetprogrammierung
Zusammenfassend lassen sich drei Teilbereiche der Internetprogrammierung ausmachen: 왘 Die Erweiterung von bereits bestehenden Client-Programmen oder die Einbindung dieser Programme in eigene Anwendungen. Beispiele für Erweiterungen von Client-Programmen sind Plug-Ins für Internetbrowser, die den Browsern zusätzliche Funktionalität verleihen. Als Beispiel für die Einbindung von Client-Programmen in eigene Programme sei die Verwendung des Microsoft Internet Explorers als ActiveX-Komponente genannt. 왘 Die Programmierung kompletter Client-Programme auf der Basis bestehender Protokolle. Oft handelt es sich hierbei um Programme für sehr spezielle Aufgaben, wie die bereits erwähnten Suchprogramme.
586
5
Internetprogrammierung
왘 Die Programmierung von Server-Erweiterungen oder Programmen, die mit existierenden Servern zusammenarbeiten können. Server werden selten komplett neu entwickelt, da bereits sehr gute, teilweise auch kostenlose Server-Software existiert. In diesem Kapitel sollen diese drei Teilbereiche der Internetprogrammierung besprochen werden. Für alle drei gibt es Unterstützung durch die MFC. So wird die Integration des Microsoft Internet Explorers in eigene Programme in Abschnitt 5.2, »Internetbrowser im Eigenbau«, die Programmierung von Client-Programmen mit der WinInet-Bibliothek in Abschnitt 5.3, »Programmierung mit WinInet« und die Programmierschnittstelle ISAPI des Microsoft Information Servers in Abschnitt 5.4, »Programmierung von Server-Erweiterungen mit ISAPI«, besprochen.
5.2
Internetbrowser im Eigenbau
Die Rolle von HTML
Internetbrowser haben sich durch ihre einfache Benutzerschnittstelle und die gelungene Integration mehrerer Internetdienste schnell durchgesetzt. Heutzutage werden Internetbrowser sogar auf Computern installiert, die gar nicht mit dem Internet verbunden sind. Auf solchen Computern dienen die Browser meistens der Anzeige von Dokumenten, die mit der Sprache HTML erstellt worden sind. Bei diesen Dokumenten handelt es sich oft um Dokumentations- oder Hilfedateien. Diese Anwendungen zeigen, dass die Verwendung von HTML durchaus nicht auf das Internet beschränkt ist.
HTML in eigenen Programmen verwenden
Durch die zunehmende Verbreitung von HTML kommt der Wunsch auf, HTML in eigenen Programmen verwenden zu können. War HTML am Anfang seiner Entwicklung noch eine relativ einfache Sprache, so ist sie heute aufgrund der raschen und teilweise recht planlosen Weiterentwicklung schwierig zu handhaben. Zwar wird die Weiterentwicklung von HTML mittlerweile durch das World Wide Web Consortium (W3C) überwacht und spezifiziert, jedoch hielten sich die beiden großen Browserhersteller (Microsoft und Netscape) in der Vergangenheit nicht immer an die Spezifikationen des W3C oder implementieren diese fehlerhaft. Zwar ist in jüngster Zeit wieder der Trend zu standardkonformen Browsern zu beobachten, jedoch sind in der Praxis noch viele alte Versionen in Gebrauch und auch Neuentwicklungen wie Mozilla oder Opera setzen nicht alle Standards vollständig und fehlerfrei
Internetbrowser im Eigenbau
587
um. Grafikdesigner, die HTML-Dokumente erstellen, verwenden meist allerlei Tricks, um bestimmte Effekte zu erzielen. Aufgrund der Komplexität heutiger HTML-Varianten ist die Entwicklung eigener Programme zur Anzeige von HTML-Dokumenten mit vertretbarem Aufwand kaum möglich. Es wird angestrebt, bestehende Lösungen zur Verwendung von HTML in eigene Programme zu integrieren. Um diesem Wunsch nachzukommen, hat Microsoft einen Großteil der Funktionalität des Internet Explorers in Form eines ActiveX-Steuerelements zur Verfügung gestellt. Damit lässt sich die Funktionalität des Internet Explorers in eigene Programme einbauen. Seit der Version 6.0 von Visual C++ wird dieses Steuerelement durch die MFC gekapselt und kann einfach in eigenen Programmen verwendet werden. Einzige Voraussetzung ist, dass der Internet Explorer auf dem Computer installiert ist, auf dem das Steuerelement verwendet werden soll.
5.2.1
Die Klasse CHtmlView
In den MFC wird das Browser-Steuerelement durch die Ansichtsklasse CHtmlView gekapselt. Die Klasse CHtmlView ist von der Klasse CFormView abgeleitet, wie in Abbildung 5.1 zu sehen ist.
Die Objekte der Ansichtsklasse CHtmlView zeigen die Webseiten in einem Layout an, das dem des Microsoft Internet Explorers entspricht, denn intern wird der Internet Explorer verwendet. Auch auf weitere Funktionen des Internet Explorers kann man durch die Klasse CHtmlView zugreifen. Dazu gehören der Verlauf (History) inklusive des Vor- und Zurücknavigierens, der Offline-Modus, der Status des Browsers (busy) und das Navigieren zu der im Internet Explorer voreingestellten Homepage und Suchmaschine. Auf einige Funktionen des Internet Explorers, wie beispielsweise den
Eigenschaften von CHtmlView
588
5
Internetprogrammierung
Vollbildmodus, kann man über die Klasse CHtmlView nicht zugreifen, obwohl die Klasse entsprechende Funktionsdeklarationen (SetFullScreen) enthält. Der Aufruf dieser Funktionen führt zu keinem sichtbaren Ergebnis. Druckfunktionen
Im Unterschied zu den anderen Ansichtsklassen der MFC werden die Druckfunktionen nicht durch die MFC, sondern durch den Internet Explorer bereitgestellt. Der Internet Explorer besitzt zwar mittlerweile eine Seitenvorschau für zu druckende HTML-Seiten, diese wird aber von den MFC nicht unterstützt. Daher gibt es keine Seitenvorschau für HTML-Ansichten. Der Druckvorgang verhält sich anders als bei den anderen Ansichtsklassen. Funktionen wie OnPreparePrinting und OnPrint lassen sich zwar mit der Entwicklungsumgebung anlegen, werden aber beim Drucken nicht aufgerufen.
HTML-Ressourcen
Seit der Version 6.0 unterstützt die Entwicklungsumgebung von Visual C++ den neuen Ressourcentyp HTML. Mit diesem Ressourcentyp lassen sich HTML-Seiten im Ressourcenteil von Dateien ablegen. Diese HTML-Ressourcen können von der Klasse CHtmlView geladen und angezeigt werden. Dafür ist die Funktion LoadFromResource zuständig. Dieser Funktion wird die ID der zu ladenden HTML-Ressource übergeben. Mit HTML-Ressourcen lassen sich Programme mit einer Web-artigen Benutzeroberfläche ausstatten, ohne dass externe HTML-Dateien benötigt werden.
5.2.2
Das Beispielprogramm StockBrowser
Anhand des Beispielprogramms StockBrowser wird die Verwendung der Klasse CHtmlView demonstriert. Das Beispielprogramm befindet sich auf der Begleit-CD im Verzeichnis KAPITEL5\STOCKBROWSER. Das Programm StockBrowser soll eine Anzahl von aktuellen Aktienkursen von der Internetseite http://de.finance.yahoo.com/ abrufen. Von dieser Internetseite kann man eine beliebige Anzahl von Aktienkursen gleichzeitig abrufen. Dazu wird ein speziell zusammengesetzter URL (Uniform Resource Locator) verwendet. URLs
Der URL zum Abruf der Aktienkurse wird nach folgendem Schema zusammenbaut: http://de.finance.yahoo.com/q?s=[wkn1].F+[wkn2].F+...
Dabei sind [wkn1], [wkn2] usw. durch die entsprechenden Wertpapierkennnummern der abzufragenden Aktienwerte zu ersetzen.
Internetbrowser im Eigenbau
URL ist eine Abkürzung für Uniform Resource Locator. Die Aufgabe eines URL ist es, eine Ressource im Internet eindeutig spezifizieren zu können, also anzugeben, wo sich eine Ressource befindet und wie diese angesprochen werden kann. Ein URL setzt sich nach dem folgenden Schema zusammen: Protokoll://Server/Verzeichnis/Datei
Protokoll bezeichnet das zu verwendende Übertragungsprotokoll. Im World Wide Web wird hier http eingetragen. Andere Protokolle sind ftp, telnet, mailto und news. Der Server wird mit seinem Namen (beispielsweise www.addison-wesley.de) oder seiner Internetadresse (194.163.213.76) angegeben. Verzeichnisse müssen nach UNIX-Konvention mit einfachen Schrägstrichen abgetrennt werden. Optional kann für ein Protokoll eine Portnummer hinter dem Servernamen angegeben werden, die durch einen Doppelpunkt abgetrennt werden muss. Der in den URL angegebene Dateiname kann eine HTML- oder Grafikdatei bezeichnen, aber auch ein Programm oder eine Server-Erweiterung, die vom Server ausgeführt werden soll. Nach dem Dateinamen können weitere Informationen an einen URL angehängt werden. Diese müssen durch ein Fragezeichen abgetrennt werden. Diese weiteren Informationen werden beispielsweise verwendet, um Informationen an einen Webserver zu übergeben. Der URL http://de.finance.yahoo.com/q?s=850663.F&s=853194.F
besteht aus folgenden Teilen: Protokoll:
HTTP
Server:
de.finance.yahoo.com
Datei:
q
Weitere Daten: s=850663.F&s=853194.F
URLs ermöglichen es, Ort und Zugriffsweise von Ressourcen flexibel anzugeben. Sie können einfach in HTML-Dokumenten verwendet werden und haben sich zum Standardmittel zur Referenzierung von Informationen im Internet entwickelt. Der mit einem Punkt angehängte Buchstabe »F« kennzeichnet den Börsenplatz Frankfurt. Die Ausgabe der angeforderten Werte erfolgt in Form einer HTML-Tabelle, die alle Werte auf einer einzigen Seite zusammenfasst.
589
t
590
5
Internetprogrammierung
Das Programm StockBrowser ist mit dem Anwendungs-Assistenten angelegt worden. Unter DOKUMENT-VORLAGENZEICHENFOLGEN ist die Dateierweiterung STB vergeben worden und unter ERSTELLTE KLASSEN wurde statt der Ansichtsklasse CView die Klasse CHtmlView ausgewählt. Abbildung 5.2 zeigt das Programm StockBrowser.
Abbildung 5.2: Das Beispielprogramm StockBrowser
Das Programm StockBrowser verwendet die Dokument-AnsichtArchitektur der MFC. Jedes Dokument des Programms speichert eine Liste von Wertpapierkennnummern. Diese Liste wird von der Dokumentenklasse in einen URL umgewandelt, der dann von der Ansichtsklasse angezeigt wird. Die Wertpapierkennnummern eines Dokuments können mit Hilfe des Dialogs eingegeben werden, der in Abbildung 5.3 zu sehen ist.
Abbildung 5.3: Dialog zur Eingabe der Wertpapierkennnummern
Internetbrowser im Eigenbau
591
Hat man eine Liste von Wertpapierkennnummern für ein Dokument erstellt, so wird der entsprechende URL zusammengesetzt und in die HTML-Ansicht geladen. Die Verwaltung der WKNs und deren Zusammensetzung zu einem URL wird durch die Dokumentenklasse des Programms StockBrowser implementiert. Listing 5.1 zeigt die Header-Datei der Dokumentenklasse. #include
// die Template-Werkzeugklassen
// Klasse, um die Liste der Aktien zu halten class CStockList { public: CList m_theList; void EmptyList (); };
// Generierte Message-Map-Funktionen protected: afx_msg void OnEditList(); DECLARE_MESSAGE_MAP() }; Listing 5.1: Header-Datei der Dokumentenklasse des Programms StockBrowser
Zu Beginn der Header-Datei in Listing 5.1 wird die Klasse CStockList deklariert. Ein Objekt dieser Klasse beherbergt die Liste der Wertpapierkennnummern eines Dokuments. Die Variable m_theList speichert die Wertpapierkennnummern in einem Objekt der Klasse CList. Die Listenelemente sind CString-Objekte, so dass man statt Wertpapierkennnummern auch Tickersymbole zur Identifikation in der Liste speichern könnte. Die Funktion EmptyList löscht die Liste. An CStockList schließt sich die Deklaration der Dokumentenklasse CStockBrowserDoc an. Innerhalb dieser Klasse wird eine Variable des Typs CStockList deklariert, die – wie gerade besprochen – die Liste der Wertpapierkennnummern speichert. Daneben sei auf die Deklaration der Funktion GetURL hingewiesen. Diese Funktion wandelt die Liste der Wertpapierkennnummern in einen URL um, der von der Ansichtsklasse angezeigt werden kann. // StockBrowserDoc.cpp : // Implementierung der Klasse CStockBrowserDoc // #include "stdafx.h" #include "StockBrowser.h" #include "StockBrowserDoc.h" #include "StockBrowserView.h" #include "StockListEdit.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif //////////////////////////////////////////////////////////// // CStockBrowserDoc IMPLEMENT_DYNCREATE(CStockBrowserDoc, CDocument) BEGIN_MESSAGE_MAP(CStockBrowserDoc, CDocument) ON_COMMAND(ID_EDIT_LIST, OnEditList) END_MESSAGE_MAP()
Internetbrowser im Eigenbau //////////////////////////////////////////////////////////// // CStockBrowserDoc Konstruktion/Destruktion CStockBrowserDoc::CStockBrowserDoc() { } CStockBrowserDoc::~CStockBrowserDoc() { } BOOL CStockBrowserDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen // (SDI-Dokumente verwenden dieses Dokument) return TRUE; }
/////////////////////////////////////////////////////////// // CStockList Funktionen void CStockList::EmptyList () { m_theList.RemoveAll(); } Listing 5.2: Die Implementierungsdatei der Dokumentenklasse
Internetbrowser im Eigenbau
Listing 5.2 zeigt die Implementierungsdatei der Dokumentenklasse CStockBrowserDoc. Interessant sind hier die Funktionen OnEditList und GetURL. Die Funktion OnEditList wird aufgerufen, wenn man den Menüeintrag BEARBEITEN | LISTE des Programms auswählt. Die Funktion legt zunächst eine Variable der Klasse CStockListEdit an. CStockListEdit ist eine von CDialog abgeleitete Klasse, die den Dialog in Abbildung 5.3 implementiert. An das Objekt der Klasse CStockListEdit wird durch den Aufruf der Funktion CStockListEdit::SetStockList ein Zeiger auf die Liste der Wertpapierkennnummern übergeben. Damit hat die Dialogklasse Zugriff auf die Liste und kann diese auslesen, anzeigen und verändern. Nach Aufruf und Rückkehr aus der Funktion DoModal müssen alle Ansichten des Dokuments benachrichtigt werden, dass sich der URL der Ansicht verändert hat. Alle Ansichten des Dokuments müssen diesen URL neu laden. Da dies nicht durch Neuzeichnen der Ansicht erreicht werden kann, wie sonst bei Veränderungen von Dokumenten unter Ausnutzung der Dokument-Ansicht-Architektur üblich, genügt es nicht, UpdateAllViews aufzurufen. Stattdessen ist es notwendig, einen Mechanismus zu implementieren, der die Ansicht veranlasst, einen URL neu zu laden. Die MFC selbst besitzen keinen solchen Mechanismus. Im Beispielprogramm StockBrowser ist die Funktion NavigateURL zur Ansichtsklasse hinzugefügt worden, um einen Update-Mechanismus zu schaffen. Diese Funktion wird vom Dokument aufgerufen, um der Ansicht mitzuteilen, dass diese ihren URL ändern muss. Die Ansichtsklasse holt sich den URL durch einen Aufruf der Funktion GetURL vom Dokument. Um NavigateURL für alle Ansichten aufzurufen, wird mittels der Funktionen GetFirstViewPosition und GetNextView über alle Ansichten des Dokuments iteriert. Zum Abschluss der Funktion OnEditList wird das Dokument durch Aufruf von SetModifiedFlag als modifiziert gekennzeichnet. Die Funktion GetURL bestimmt den URL aus der Liste der WKNs und gibt diesen zurück. Dabei wird zwischen zwei verschiedenen Fällen unterschieden. Ist die Liste der WKNs leer, dann gibt GetURL den String ABOUT:BLANK zurück. Dieser spezielle URL zeigt im Internet Explorer eine leere Seite an. Ist die Liste nicht leer, dann wird der URL nach dem bereits beschriebenen Schema zusammengebaut.
595
596
5
Internetprogrammierung
#include "stdafx.h" #include "StockBrowser.h" #include "StockBrowserDoc.h" #include "StockBrowserView.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// // // CStockBrowserView IMPLEMENT_DYNCREATE(CStockBrowserView, CHtmlView) BEGIN_MESSAGE_MAP(CStockBrowserView, CHtmlView) ON_COMMAND(ID_EDIT_RELOAD, OnEditReload) // Standard-Druckbefehle ON_COMMAND(ID_FILE_PRINT, CHtmlView::OnFilePrint) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// // // CStockBrowserView Konstruktion/Destruktion CStockBrowserView::CStockBrowserView() { // ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen } CStockBrowserView::~CStockBrowserView() { } BOOL CStockBrowserView::PreCreateWindow(CREATESTRUCT& cs) { // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das // Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren. return CHtmlView::PreCreateWindow(cs); } ////////////////////////////////////////////////////////////// // // CStockBrowserView Zeichnen
Internetbrowser im Eigenbau void CStockBrowserView::OnDraw(CDC* pDC) { CStockBrowserDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Hier Code zum Zeichnen der // ursprünglichen Daten hinzufügen } void CStockBrowserView::OnInitialUpdate() { CStockBrowserDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CHtmlView::OnInitialUpdate(); // URL des Dokuments anzeigen Navigate2 (pDoc->GetURL ()); } ////////////////////////////////////////////////////////////// // CStockBrowserView Drucken
Die Fähigkeit, HTML-Seiten anzuzeigen, wird durch die von CHtmlView abgeleitete Ansichtsklasse CStockBrowserView implementiert. Die Implementierungsdatei dieser Klasse ist in Listing 5.3 zu sehen. Es ist nur wenig Programmcode notwendig, um die Funktionalität eines Internetbrowsers zu realisieren. Die zunächst von der Ansicht angezeigte HTML-Seite wird in der Funktion OnIntialUpdate festgelegt. Der Anwendungs-Assistent trägt als Vorgabe in der Funktion OnInitialUpdate den URL HTTP:// WWW.MSDN.MICROSOFT.COM/VISUALC/ ein, den man durch einen eigenen URL ersetzen muss. Im Beispiel wird die zu ladende Internetseite durch den Aufruf von GetURL beim Dokument abgefragt. Wurde das Dokument neu angelegt, so besitzt es eine leere Liste von Wertpapierkennnummern, und GetURL liefert den speziellen URL ABOUT:BLANK zurück, so dass eine leere Seite angezeigt wird. Navigate und Navigate2
Navigate und Navigate2 sind zwei Funktionen der Klasse CHtmlView mit denen die Ansichtsklasse angewiesen wird, einen URL anzuzeigen. Navigate2 bietet mehr Optionen als die Funktion Navigate. Die Funktion Navigate2 besitzt eine ganze Reihe von Parametern, für die bereits Vorgabewerte gesetzt sind, so dass man für diese Parameter keine Werte übergeben muss. Man kann beispielsweise angeben, ob der URL vom Cache gelesen werden darf, ob er im Verlauf gespeichert werden soll oder ob zusätzliche Daten an den HTTP-Server gesendet werden sollen. Die Ansichtsklasse CStockBrowserView ruft die Funktion Navigate2 innerhalb der Funktionen OnInitialUpdate und NavigateURL auf, um den vom Dokument erhaltenen URL anzuzeigen. Die Funktion NavigateURL wurde schon erwähnt. Sie wird von der Dokumentenklasse aufgerufen, wenn sich der URL des Dokuments verändert hat. Dieser Mechanismus ist nicht Teil der MFC,
Internetbrowser im Eigenbau
599
sondern speziell für das Programm StockBrowser implementiert worden. Die MFC sehen keine Speicherung von URLs in der Dokumentenklasse vor und arbeiten folglich an dieser Stelle nicht unbedingt konform mit der Dokument-Ansicht-Architektur. NavigateURL holt sich den aktuellen URL durch Aufruf der Funktion GetURL beim Dokument ab und setzt den Browser durch Aufruf von Navigate2 auf diesen neuen URL. Die letzte Funktion im Listing ist OnEditReload. Sie wird aufgerufen, um die aktuelle HTML-Seite neu zu laden. Das Programm StockBrowser bietet hierzu den Menüeintrag BEARBEITEN | NEU LADEN sowie ein eigenes Symbol in der Symbolleiste. Die Implementierung von OnEditReload ruft die Funktion Refresh auf, die das Dokument neu lädt. Analog zu der Funktion Navigate gibt es auch von der Funktion Refresh zwei Versionen, Refresh und Refresh2. Refresh2 besitzt einen Parameter, mit dem man angeben kann, auf welche Weise das Dokument neu geladen werden soll. So kann man beispielsweise ein Laden aus dem Cache verhindern. Wie eng die Klasse CHtmlView und der Internet Explorer verbunden sind, kann man feststellen, wenn man nach der Benutzung des Beispielprogramms StockBrowser den Internet Explorer startet und dessen Verlauf (in der deutschsprachigen Version des Internet Explores wird die History-Funktion als Verlauf bezeichnet.) betrachtet. Die mit dem Programm StockBrowser betrachteten Seiten sind sowohl im Verlauf als auch im Cache des Internet Explorers gespeichert (um die Seiten des Caches zu betrachten, muss vorher in den Offline-Modus gewechselt werden).
5.2.3
Tipps zur Vorgehensweise
왘 Damit Programme, die die Klasse CHtmlView verwenden, funktionieren, muss der Microsoft Internet Explorer installiert sein. 왘 Möchte man die URL-Verwaltung in der Dokumentenklasse vornehmen, so muss man einen eigenen Mechanismus zur Benachrichtigung der Ansicht bei Änderungen des URLs des Dokuments implementieren. Die Semantik der Klasse CHtmlView unterscheidet sich von anderen Ansichten, da nicht die Funktion OnDraw für die Darstellung des Inhalts zuständig ist, sondern die Funktion Navigate (oder Navigate2). Die sonst in der Dokument-Ansicht-Architektur übliche Vorgehensweise,
Refresh und Refresh2
600
5
Internetprogrammierung
bei Änderungen des Dokuments die Funktion UpdateAllViews aufzurufen, kann bei HTML-Ansichten folglich nicht angewendet werden. 왘 Die Klasse CHtmlView kann nicht nur HTML-Dateien anzeigen und über das Internet laden, sie kann zusätzlich auch HTMLRessourcen anzeigen. HTML-Ressourcen sind HTML-Dateien, die als Ressource in eine Applikation importiert werden; sie werden mit der Funktion LoadFromResource angezeigt.
5.2.4
Weitere HTML-Klassen
Mit der Version 7.0 der MFC sind weitere Klassen zur HTML-Bearbeitung zu den MFC dazugekommen. Die Klasse CHtmlEditView erlaubt es nun auch, Ansichten zu verwenden, in denen innerhalb der HTML-Darstellung editiert werden kann. Änderungen erfolgen nach dem WYSIWYG-Prinzip moderner Textverarbeitungsprogramme, den HTML-Quelltext bekommt der Anwender demnach nicht zu sehen. Mit Hilfe der von der Klasse CHtmlEditCtrlBase geerbten Funktionen kann ein vollständiger HTML-Editor aufgebaut werden. Außerdem ist die Klasse CHtmlEditCtrl neu zu den MFC hinzugekommen, sie stellt die gleiche Funktionalität wie CHtmlEditView in Form eines Steuerelementes zur Verfügung. Auch CHtmlEditCtrl erbt von der Klasse CHtmlEditCtrlBase.
5.2.5
Zusammenfassung
Die Klasse CHtmlView bietet eine einfache Möglichkeit, eigene Anwendungen mit Internetfunktionen und der Fähigkeit zur Anzeige von HTML-Dokumenten auszustatten. Durch die zunehmende Verbreitung des Internets und von Intranets innerhalb von Firmen entwickelt sich HTML zu einem universellen Dokumentenformat. Dadurch, dass HTML in Form einer Ansichtsklasse in die MFC integriert wurde, ist die Programmierung von Anwendungen, die HTML verwenden, sehr einfach geworden. Die Klasse CHtmlView ermöglicht die Programmierung von Anwendungen mit Internetfähigkeiten auf einem hohen Niveau. Der Programmierer muss sich nicht mit den Implementierungsdetails von HTML und Internetprotokollen zur Übertragung dieser Seiten auseinander setzen.
Programmierung mit WinInet
5.3
601
Programmierung mit WinInet
Manchmal ist die in Abschnitt 5.2, »Internetbrowser im Eigenbau«, beschriebene Integration des Microsoft Internet Explorers bei der Erstellung eigener Internetanwendungen nicht ausreichend. Möglicherweise möchte man eine vom Internetbrowser abweichende Benutzerschnittstelle programmieren oder ein Programm erstellen, das nicht auf HTML basiert. In diesen Fällen muss man tiefer in die Internetprogrammierung einsteigen und sich mit den Protokollen des Internets auseinander setzen. Die Programmierbibliothek WinInet stellt eine Implementierung der wichtigsten Internetprotokolle zur Verfügung. Es existiert eine Reihe von MFC-Klassen, um WinInet in MFC-Programmen einsetzen zu können. Damit wird es einfacher, Anwendungen zu entwickeln, die auf diesen Protokollen aufbauen. Um Internetprotokolle verwenden zu können, muss man sie verstehen. Daher wird in diesem Kapitel zunächst auf die Protokolle des Internets eingegangen. Danach wird die Programmierung mit der WinInet-Bibliothek anhand eines Beispielprogramms demonstriert.
5.3.1
Protokolle im Internet
Computer verwenden Protokolle, um Daten über Netzwerke miteinander auszutauschen. Ein Protokoll legt fest, in welchem Format Daten übertragen werden, wie Fehler erkannt werden können und wie im Fehlerfall verfahren wird. Es gibt eine Vielzahl verschiedener Protokolle, um unterschiedlichen Anforderungen gerecht zu werden.
Protokolle
Ein Protokoll behandelt im Allgemeinen nur wenige Aspekte des gesamten Kommunikationsvorgangs. Es sind vielmehr mehrere Protokolle am Datenaustausch beteiligt. Diese Protokolle bauen üblicherweise aufeinander auf, man sagt, dass bestimmte Protokolle auf der Funktionalität anderer Protokolle »aufsetzen«. Die Protokolle in ihrer Gesamtheit beschreiben den Kommunikationsvorgang. Aufeinander aufbauende Protokolle werden in Schichten eingeteilt. Entsprechend der Schicht, in der sich ein Protokoll befindet, arbeitet es auf einem spezifischen Abstraktionsniveau. Während einige Protokolle sehr Hardware-nah arbeiten oder sogar direkt als Hardware realisiert werden, wie zum Beispiel das
Protokollschichten
602
5
Internetprogrammierung
Ethernet-Protokoll (IEEE 802.3), arbeiten andere Protokolle auf einem hohen Abstraktionsniveau, wie beispielsweise SMTP (Simple Mail Transfer Protocol) zur Übertragung von E-Mail im Internet. Protokolle können nicht nur aufeinander aufbauen, sie können auch Alternativen zueinander darstellen. Protokolle, die Alternativen zueinander darstellen, werden derselben Schicht zugeordnet. Insgesamt ergibt sich eine Anordnung wie in Abbildung 5.4
Protokoll 1
Protokoll 2
Protokoll I
Protokoll3 Protkoll II
Protokoll A Abbildung 5.4: Schichtung von Protokollen Protokollarten
Die Standardisierungsorganisation ISO (International Standards Organization) hat die so genannten OSI-Protokolle (Open Systems Interconnection) definiert, die die Kommunikation in Computernetzwerken in sieben Schichten einteilen. In der Praxis werden allerdings meist weniger als sieben Schichten verwendet. Die unterste Schicht regelt die physikalischen Eigenschaften des Datentransports, also beispielsweise durch welche elektrischen Pegel ein Bit dargestellt wird. Sie kann etwa durch ein EthernetNetzwerk, eine Modemverbindung oder eine Richtfunkstrecke realisiert sein. Die nächsthöheren Schichten implementieren die Netzwerkprotokolle. Netzwerkprotokolle sichern den Datentransport, bauen Verbindungen auf und führen Mechanismen zur Adressierung von Endpunkten ein. Während es in lokalen Netzwerken eine ganze Reihe von Netzwerkprotokollen geben kann, werden im Internet ausschließlich die Protokollpaare TCP/IP (Transmission Control Protocol / Internet Protocol) und UDP/IP (User Datagram Protocol / Internet Protocol) verwendet. Über den Netzwerkprotokollen liegen die anwendungsnahen Schichten. In diesen Schichten werden Protokolle wie HTTP und FTP verwendet. Anwendungsnah bedeutet, dass diese Protokolle direkt von Anwendungsprogrammen benutzt werden. Meist müssen diese Protokolle durch die Anwendung selbst implementiert werden. Abbildung 5.5 zeigt die Protokolle im Internet.
Programmierung mit WinInet
HTTP
FTP
603
SMTP
NNTP
TCP
Gopher
Telnet
UDP IP
Ethernet
Modem / PPP
Token Ring
Abbildung 5.5: Protokolle im Internet
TCP/IP implementiert zwei aufeinander aufbauende Netzwerkschichten. IP sorgt als untere Schicht dafür, dass miteinander kommunizierende Computer kleine Datenpakete, so genannte Datagramme, untereinander austauschen können. Es ist allerdings weder garantiert, dass Datagramme in der gesendeten Reihenfolge beim Empfänger ankommen, noch, dass sie überhaupt dort ankommen. In der IP-Schicht wird die für das Internet typische 32-Bit-Adressierung implementiert. Jedem Computer im Internet ordnet man eine weltweit eindeutige 32-Bit-Zahl, die IP-Adresse, zu. Über diese Adresse wird der Computer identifiziert. Um diese Zahl etwas lesbarer zu machen, wird sie üblicherweise in vier Gruppen zu je 1 Byte aufgeteilt, die durch Punkte getrennt werden. Die Darstellungsart bezeichnet man als Punktnotation. Beispielsweise hatte der Addison-Wesley-Webserver zur Zeit der Abfassung dieses Kapitels die IP-Adresse 194.163.213.76. Die im World Wide Web üblichen lesbaren Namen werden durch das Domain Name System (DNS) in IP-Adressen umgewandelt, damit die Rechner über diese Adressen angesprochen werden können. Das Domain Name System wird von speziellen Servern bereitgestellt.
TCP/IP
TCP sorgt als Schicht oberhalb von IP für die gesicherte Übertragung eines Datenstroms. So werden Datagramme in die richtige Reihenfolge gebracht, fehlende Daten erneut übertragen und Datagramme zu einem kontinuierlichen Datenstrom zusammengefügt. TCP korrigiert Fehler, die in der IP-Schicht auftreten, und sorgt so für eine fehlerfreie, kontinuierliche Übertragung, auf der die höheren, anwendungsnahen Protokolle aufbauen. Neben TCP gibt es UDP. Anders als TCP arbeitet UDP nicht mit einem kontinuierlichen Datenstrom, sondern verwendet Datagramme direkt. Dies kann für Programme sinnvoll sein, die bewusst auf eine Fehlerkorrektur verzichten (zum Beispiel bei der
UDP
604
5
Internetprogrammierung
Übertragung von Echtzeit-Audio) oder die nur wenige Datenpakete zu Steuerzwecken austauschen. Eine UDP-Übertragung ist nicht fehlergesichert und die Datenpakete haben keine garantierte Reihenfolge. Ports
TCP und UDP implementieren das Port-Konzept von TCP/IP. Ein Port ist eine 16-Bit-Zahl, die einer TCP- oder UDP-Übertragung zugeordnet wird. Mit Ports können Protokolle oberhalb der TCP/ UDP-Schicht unterschieden werden. Dazu wird jedem Protokoll ein Standard-Port zugeordnet. Der Standard-Port für HTTP ist beispielsweise 80, der für FTP 21. Die Zuordnung von Protokollen und Ports beruht auf Konventionen. Technisch gesehen kann jedes Protokoll mit jedem Port verwendet werden. Während eine IPAdresse angibt, wo sich ein Server im Netzwerk befindet, gibt ein Port an, welche Dienstleistung des Servers und somit welches Protokoll verwendet werden soll. Die Kombination aus IP-Adresse und Portnummer wird als Socket bezeichnet.
5.3.2
TCP/IP-Programmierung unter Windows
Das TCP/IP-Protokoll ist mit dem UNIX-Betriebssystem entstanden und wird dort als einziges Netzwerkprotokoll eingesetzt. Da TCP/IP zunächst im Berkeley-Zweig von UNIX implementiert wurde, bezeichnet man die Programmierschnittstelle dazu als Berkeley Sockets. Winsock
Nicht-UNIX-Betriebssysteme verwenden in lokalen Netzen oft andere Protokolle als TCP/IP. Durch die Ausbreitung des Internets wurde es jedoch notwendig, TCP/IP in alle gängigen Betriebssysteme zu integrieren. Unter Windows hat Microsoft hierzu die Programmierschnittstelle Winsock geschaffen. Diese bildet die Berkeley Sockets unter Windows nach. Da jedoch Windows 3.x kein Prozessmodell besaß, das dem von UNIX entsprach (Windows 3.x kennt kein preemptives Multitasking), musste in Winsock zusätzlich eine Reihe von speziellen asynchronen Funktionen aufgenommen werden, die sich auch heute noch in der 32-Bit-Version von Winsock finden. Wer mit Winsock Programme entwickeln möchte, der findet dazu folgende unterstützenden Klassen in den MFC: CAsyncSocket, CSocket und CSocketFile. Der Programmierer setzt mit Winsock direkt auf TCP/IP auf, das heißt, er muss Protokolle der Anwendungsschicht, wie FTP oder HTTP, selbst implementieren. Abbildung 5.6 zeigt die SocketKlassen der MFC.
Programmierung mit WinInet
605
CObject CFile CSocketFile CAsyncSocket CSocket Abbildung 5.6: Die Socket-Klassen der MFC-Klassenbibliothek
WinInet ist eine neuere Programmierschnittstelle zur Erstellung von Internetanwendungen für Windows. WinInet ist unabhängig von Winsock implementiert, weshalb die oben genannten MFC-Klassen bei der WinInet-Programmierung keine Verwendung finden. WinInet stellt eine Programmierschnittstelle für Internetanwendungen auf einem hohen Abstraktionsniveau dar. Im Gegensatz zur Winsock-Programmierung muss der Programmierer bei WinInet die Anwendungsprotokolle des Internets nicht selbst implementieren. Stattdessen werden sie ihm durch eine relativ einfache Klassenschnittstelle in Form von Dateizugriffen zur Verfügung gestellt. WinInet implementiert die Protokolle HTTP, FTP und Gopher. Dabei werden Caching und SSL (Secure Sockets Layer, eine durch Verschlüsselung gesicherte Übertragung) unterstützt. WinInet kann nur zur Programmierung von Client-Anwendungen verwendet werden, die Erstellung von Serverprogrammen ist mit WinInet nicht möglich. Hierzu muss entweder Winsock oder die Schnittstelle des Internet Information Servers, ISAPI, verwendet werden. WinInet ist auf die drei erwähnten Protokolle beschränkt. Andere Protokolle, wie beispielsweise SMTP (Mail) oder NNTP (News), müssen auch weiterhin mit Winsock realisiert werden.
5.3.3
HTTP
Von den drei durch WinInet unterstützten Protokollen soll hier beispielhaft HTTP besprochen werden. HTTP ist das einfachste der drei unterstützten Protokolle und wird am häufigsten verwendet. HTTP wird normalerweise zur Übertragung von HTML-Seiten benutzt, kann aber prinzipiell beliebige andere Dateiformate übertragen. HTTP ist ein einfaches, zustandsloses Protokoll, für jede Anfrage wird eine neue Verbindung aufgebaut. Jede HTTP-Anfrage und jede HTTP-Antwort besteht aus einem ASCII-Header, der durch eine Leerzeile von den zu übertragenden Daten getrennt ist. Die Daten
WinInet
606
5
Internetprogrammierung
selbst können ein beliebiges Format haben. Der Typ der Daten wird durch den Content-Type im Header angegeben. Für HTTP sind vom W3C die Versionen 1.0 und 1.1 spezifiziert worden, die bei jeder Anfrage und bei jeder Antwort angegeben werden. Der folgende Kasten zeigt Beispiele für HTTP-Anfragen und -Antworten. HTTP-Beispiele
Anforderung der Hauptseite eines HTTP-Servers. Es wird die Protokollversion 1.0 von HTTP verwendet: GET / HTTP/1.0
Anforderung der Datei INDEX.HTM aus dem Verzeichnis SERVER durch den Internet Explorer Version 5.5 auf Windows 2000 (NT 5.0). Es werden HTML- und GIF-Dateien akzeptiert. Die Bezeichnung Mozilla steht für Netscape und bedeutet, dass der verwendete Browser mit diesem kompatibel sein möchte: GET /Server/Index.htm HTTP/1.0 Accept: text/html Accept: image/gif User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)
Beispiel für eine HTTP-Antwort vom Typ HTML. Der Server hat mit Protokollversion 1.1 geantwortet. Nach der Protokollversion steht der HTTP-Fehlercode, in diesem Fall 200 (kein Fehler). Der verwendete Server ist ein Apache 1.3.12 auf einem Unix-Rechner: HTTP/1.1 200 OK Date: Mon, 19 Feb 2001 16:59:55 GMT Server: Apache/1.3.12 (Unix) Connection: close Content-Type: text/html Hauptseite Meine Homepage
Da HTTP ein zustandsloses Protokoll ist, baut der Client – das ist in den meisten Fällen der Browser des Web-Surfers – für jede Anfrage eine Verbindung mit dem Server auf Port 80 auf (sofern in dem URL kein anderer Port angegeben ist). Wenn die Verbindung zustande gekommen ist, sendet der Client eine HTTP-Anfrage an
Programmierung mit WinInet
607
den Server. Der Server schickt daraufhin eine HTTP-Antwort zurück und baut anschließend die Verbindung ab. Handelt es sich bei dem übertragenen Dokument um eine HTML-Seite, in die Grafiken eingebunden sind, so muss der Client für jede Grafik eine neue HTTP-Anfrage beim Server stellen. Um ein Gefühl für den Ablauf einer HTTP-Übertragung zu bekommen, kann man diese per Telnet simulieren. Dazu ruft man unter Windows das gleichnamige Programm Telnet auf. Man findet das Programm im Windows Systemverzeichnis. Unter Windows 2000 ist Telnet als Konsolenanwendung implementiert. Zunächst sollte mittels des Befehls set local_echo das lokale Echo eingeschaltet werden, damit Eingaben angezeigt werden. Um die Verbindung aufzubauen, gibt man den Befehl open mit der gewünschten Serveradresse als ersten Parameter und der Portnummer 80 als zweiten Parameter jeweils durch ein Leerzeichen abgetrennt ein.
Abbildung 5.7: HTTP-Anfrage mit Telnet simulieren
Gibt man danach GET / HTTP/1.0 ein und drückt dann zweimal Return, dann sendet der HTTP-Server seine Hauptseite als Antwort zurück. Zwei Returns sind notwendig, da der HTTP-Header mit einer Leerzeile abgeschlossen werden muss. Nach der Übertragung baut der Server die Verbindung ab. Dies ist das normale Verhalten eines HTTP-Servers. Der Client muss für jede Anfrage eine neue Verbindung aufbauen. Telnet teilt nach dem Abbau der Verbindung mit, dass die Verbindung zum Host verloren wurde. Die Abbildung 5.7 zeigt den Aufbau einer Verbindung zu einem Webserver mit Telnet. Abbildung 5.8 zeigt das Ergebnis und die Meldung des Verbindungsabbaus.
HTTP simulieren
608
5
Internetprogrammierung
Abbildung 5.8: Ergebnis einer HTTP-Anfrage
Es kommt vor, dass die empfangenen Texte nicht zeilengerecht formatiert sind. Das liegt daran, dass viele Server im Internet unter dem Betriebssystem UNIX laufen. UNIX verwendet im Gegensatz zu Windows und MS-DOS eine andere Art, das Zeilenende in einer Textdatei zu kennzeichnen. Während Windows am Ende einer Zeile ein Carriage Return (CR, Wagenrücklauf, ASCII 13), gefolgt von einem Linefeed (LF, Zeilenvorschub, ASCII 10), verwendet, benutzen alle UNIX-Varianten lediglich ein Linefeed zur Kennzeichnung des Zeilenendes. Bei der Programmentwicklung für das Internet sollte darauf geachtet werden, auch UNIX-Dateien korrekt zu behandeln, das heißt ein Linefeed allein muss auch als Zeilenwechsel erkannt werden.
5.3.4 Server-Software lokal installieren
Das Testen von Internetanwendungen
Bei der Entwicklung von Internetanwendungen ergibt sich oft das Problem, dass diese eine passende Gegenstelle, im Falle von HTTP also einen Web-Server, benötigen. Sollte kein direkter Internetzugang bestehen, so bietet es sich an, die benötigte Server-Software lokal zu installieren. Die Kommunikation über TCP/IP muss nicht notwendigerweise über ein Netzwerk erfolgen. Auch Programme auf einem Computer können über TCP/IP kommunizieren. Die Server-Software lässt sich lokal installieren und verwenden. So können Client-Programme auch dann getestet werden, wenn der Computer, auf dem sie ausgeführt werden, weder einen Internetzugang hat, noch sich in einem lokalen Netzwerk befindet. Die neueren Windows-Versionen bringen Webserver-Software gleich mit. Allerdings muss diese machmal noch installiert werden. Unter Windows 2000 müssen dazu in der Systemsteuerung
Programmierung mit WinInet
609
unter SOFTWARE | WINDOWS KOMPONENTEN HINZUFÜGEN/ENTFERNEN die Internet-Informationsdienste (IIS) installiert werden. Außerdem muss das TCP/IP-Protokoll installiert und konfiguriert sein, was aber heute meist immer der Fall ist. Nach der Installation der Internet-Informationsdienste können diese unter Windows 2000 durch den Internetdienste-Manager konfiguriert werden. Dieser lässt sich in der Systemsteuerung unter VERWALTUNG | INTERNETDIENSTE-MANAGER aufrufen. Die Internetdienste bestehen aus einem HTTP- und einem FTP-Server. Die installierten Dienste erscheinen im Fenster des Internetdienste-Managers, wie in Abbildung 5.9 zu sehen ist. Mit dem Internetdienste-Manager lassen sich die einzelnen Server leicht starten und stoppen sowie die Verzeichnisse und die Startseite des Servers einrichten.
Abbildung 5.9: Der Internetdienste-Manager von Windows 2000
5.3.5
Die MFC-Klassen der WinInet-Bibliothek
Die MFC-Klassen der WinInet-Bibliothek teilen sich in mehrere Kategorien auf. Generell ergibt sich bei WinInet-Anwendungen die in Abbildung 5.10 gezeigte Struktur.
Dateiobjekt
Verbindungsobjekt
Sitzungsobjekt Abbildung 5.10: Struktur einer WinInet-Anwendung
InternetdiensteManager
610
5
CInternetSession
Internetprogrammierung
Als Basis für die Verwendung von WinInet wird ein Sitzungsobjekt benötigt. Das Sitzungsobjekt initialisiert die WinInet-Bibliothek und verwaltet den Status der Kommunikation mit dem Internet. Für jeden Thread einer Anwendung, die die WinInet-Bibliothek verwendet, wird ein Sitzungsobjekt benötigt. Sitzungsobjekte sind Instanzen der Klasse CInternetSession oder davon abgeleiteter Klassen. Abbildung 5.11 zeigt die MFC-Klassen der WinInet-Bibliothek.
Von CInternetSession kann der Programmierer eigene Klassen ableiten und einen Callback-Mechanismus implementieren, der Auskunft über alle Zustandsänderungen während einer Internetverbindung gibt. Verbindungsklassen
Hat man ein CInternetSession-Objekt erzeugt, so können damit HTTP-, FTP- und Gopher-Verbindungen aufgebaut werden. Diese Verbindungen werden durch Verbindungsobjekte repräsentiert.
Programmierung mit WinInet
611
Dafür gibt es die MFC-Klassen CFtpConnection, CGopherConnection und CHttpConnection. Instanzen dieser Objekte erzeugt der Programmierer nicht selbst, sondern sie werden vom Sitzungsobjekt angefordert. Die zu übertragenden Daten werden durch Dateiobjekte verwaltet. Diese Dateiobjekte werden ebenfalls nicht vom Programmierer erzeugt, sondern vom entsprechenden Verbindungsobjekt angefordert. Je nach verwendetem Protokoll werden verschiedene Dateiklassen verwendet. Für die Protokolle Gopher und HTTP gibt es die Dateiklassen CGopherFile und CHttpFile. Bei FTP wird dagegen die Oberklasse CInternetFile verwendet.
Dateiklassen
Die WinInet-Klassen geben Fehlermeldungen über Ausnahmeobjekte der Klasse CInternetException zurück. Fehler können, wie bei MFC-Ausnahmen üblich, über die Member-Funktionen GetErrorMessage und ReportError abgefragt und angezeigt werden.
Ausnahmen
5.3.6
Ablauf einer WinInet-Sitzung
WinInet-Sitzungen laufen, bedingt durch die Struktur der Bibliothek, nach einem grundlegenden Schema ab. Zunächst muss ein Sitzungsobjekt erstellt werden. Von diesem wird ein Verbindungsobjekt angefordert. Vom Verbindungsobjekt wird anschließend ein Dateiobjekt angefordert, mit dessen Hilfe die Daten dann übertragen werden können. Zum Abschluss müssen Verbindungs- und Dateiobjekt vom Programmierer freigegeben werden. Im Folgenden werden diese drei Schritte nochmals aus programmiertechnischer Sicht beschrieben: 왘 Um die WinInet-Bibliothek zu initialisieren und Zugriff auf weitere Funktionen der Bibliothek zu bekommen, muss zunächst ein Objekt der Klasse CInternetSession angelegt werden. Bei Programmen, die während ihrer gesamten Laufzeit auf die WinInet-Bibliothek zugreifen, sollte dieses Objekt beim Start des Programms erzeugt und beim Beenden wieder zerstört werden. Möchte man den Callback-Mechanismus der Klasse CInternetSession nutzen, dann muss man eine eigene Klasse von CInternetSession ableiten und die Funktion OnStatusCallback überschreiben. Im Konstruktor ist die Funktion EnableStatusCallback aufzurufen. Die Funktion OnStatusCallback bekommt Zustandsänderungen in Form von Konstanten übergeben. Diese Konstanten beginnen mit INTERNET_STATUS_ und sind in der Datei WININET.H definiert.
612
5
Internetprogrammierung
왘 Um eine Verbindung zu einem Internetserver aufzubauen, muss vom Sitzungsobjekt ein Verbindungsobjekt angefordert werden. Jedes der drei von WinInet unterstützten Protokolle wird durch eine jeweils eigene Verbindungsklasse repräsentiert: CFtpConnection, CGopherConnection und CHttpConnection. Diese Objekte werden durch den Aufruf der Funktionen GetFtpConnection, GetGopherConnection und GetHttpConnection vom Sitzungsobjekt angefordert. Ist die Instanz eines Verbindungsobjekts erzeugt worden, dann lässt sich mit diesem Verbindungsobjekt die Verbindung zu einem Server steuern. Die drei Verbindungsklassen steuern die Verbindung über unterschiedliche Funktionen, die sich aus den verschiedenen Eigenschaften der Protokolle ergeben. Über das Verbindungsobjekt wird die Verbindung aufgebaut und die Übertragung der Daten veranlasst. Die Daten selbst werden durch Dateiobjekte verwaltet. Verbindungsobjekte müssen nach der Verwendung vom Programmierer gelöscht werden. Sie stellen dem Programmierer den Kontakt zu einem Server als dauerhafte Verbindung dar. Im Falle von HTTP wird jedoch für jede Anfrage eine neue Verbindung aufgebaut, ohne dass dies für den Programmierer sichtbar wird. HTTP besitzt per Definition keine dauerhaften Verbindungen, die WinInet-Bibliothek simuliert jedoch solche permanenten Verbindungen, indem sie für jede neue Anfrage eine neue Verbindung aufbaut. So können wiederholte Anfragen an HTTP-Server einfacher programmiert werden. Bei FTP wird dagegen eine dauerhafte Verbindung zum FTP-Server aufgebaut. 왘 Protokollspezifische Dateiobjekte werden durch die Klassen CHttpFile und CGopherFile repräsentiert. Dateien, die per FTP übertragen werden, verwenden direkt die Oberklasse CInternetFile. Dateiobjekte erzeugt man durch einen Funktionsaufruf beim Verbindungsobjekt. Im Falle von HTTP erhält man einen Zeiger auf ein Objekt der Klasse CHttpFile, indem man die Funktion OpenRequest beim Verbindungsobjekt der Klasse CHttpConnection aufruft. Bei Gopher- und FTP-Verbindungen erhält man ein Dateiobjekt durch den Aufruf der Funktion OpenFile. Über die Dateifunktionen Write, Read, Seek usw. können Daten gelesen und im Fall von FTP auch geschrieben werden (falls man Schreibrechte auf dem FTP-Server besitzt). Dateiobjekte müssen nach der Benutzung vom Programmierer gelöscht werden.
Programmierung mit WinInet
5.3.7
Das Beispielprogramm FileRobot
Das Beispielprogramm FileRobot befindet sich im Verzeichnis KAPITEL5\FILEROBOT auf der Begleit-CD. Das Programm ermöglicht es, eine Liste von Dateien, die über ihren URL angegeben werden, von einem oder mehreren Servern zu laden, also einen Download der Dateien durchzuführen. Verwendet wird HTTP, die Dateien müssen sich also auf einem Webserver befinden. Abbildung 5.12 zeigt das Programm FileRobot.
Abbildung 5.12: Das Beispielprogramm FileRobot
Ein URL wird im Programm aus zwei Teilen zusammengesetzt: Der erste Teil ist der Basis-URL. Er besteht aus dem Servernamen und eventuell einem Verzeichnisnamen auf dem Server. Der zweite Teil beschreibt den Dateinamen der Datei, die geladen werden soll. Beide Teile werden in jeweils eigenen Eingabefeldern eingegeben. Auf diese Weise lassen sich mehrere Dateinamen auf dem gleichen Server schneller in die Liste der zu ladenden Dateien übernehmen. Durch einen Klick auf die Schaltfläche HINZUFÜGEN wird ein URL, der aus den zwei Teilen besteht, in die Dateiliste übernommen. Durch einen Klick auf die Schaltfläche ENTFERNEN lässt sich der gerade selektierte Eintrag aus der Dateiliste löschen. START leitet die Dateiübertragung ein, ENDE beendet das Programm. Die geladenen Dateien werden im gleichen Verzeichnis wie das Programm abgelegt.
613
614
5
Internetprogrammierung
Um die Einfachheit und Übersichtlichkeit zu wahren, wurde beim Beispielprogramm FileRobot auf die Verwendung eines zweiten Threads für das Laden der Dateien verzichtet. Als Folge davon ist die Benutzerschnittstelle blockiert, während Dateien geladen werden. Wie man einen zweiten Thread verwendet, um das Programm auch während des Downloads bedienbar zu halten, wird in Abschnitt 2.11, »Programmierung mit Threads«, beschrieben. Die dort beschriebene Vorgehensweise kann selbstverständlich auf das Programm FileRobot angewandt werden. Das Programm FileRobot ist mit dem Anwendungs-Assistenten als dialogfeldbasierte Anwendung angelegt worden. Besondere Optionen mussten dabei nicht angegeben werden. In der Datei STDAFX.H wurde die Anweisung #include
eingefügt, um die Klassen der WinInet-Bibliothek verwenden zu können. Die gesamte Funktionalität des Programms ist in der Dialogklasse CFileRobotDlg implementiert worden. Listing 5.4 zeigt die Implementierungsdatei dieser Klasse. // FileRobotDlg.cpp : Implementierungsdatei // #include "stdafx.h" #include "FileRobot.h" #include "FileRobotDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////// / // CAboutDlg dialog used for App About class CAboutDlg : public CDialog { public: CAboutDlg(); // Dialog Data enum { IDD = IDD_ABOUTBOX }; // ClassWizard generated virtual function overrides
BEGIN_MESSAGE_MAP(CFileRobotDlg, CDialog) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_BN_CLICKED(IDC_BUTTON_ADDFILE, OnButtonAddFile) ON_BN_CLICKED(IDC_BUTTON_REMOVEFILE, OnButtonRemoveFile) ON_BN_CLICKED(IDC_BUTTON_START, OnButtonStart) END_MESSAGE_MAP() ////////////////////////////////////////////////////////////// // CFileRobotDlg Nachrichten-Handler BOOL CFileRobotDlg::OnInitDialog() { CDialog::OnInitDialog(); // Add "About..." menu item to system menu. // IDM_ABOUTBOX must be in the system command range. ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // Symbol für dieses Dialogfeld festlegen. Wird automatisch // erledigt wenn das Hauptfenster der Anwendung kein // Dialogfeld ist SetIcon(m_hIcon, TRUE); // Großes Symbol verwenden SetIcon(m_hIcon, FALSE); // Kleines Symbol verwenden // *** Eigene Initialisierungen kommen hier *** try { // Sitzungsobjekt anlegen: m_pSession = new CInternetSession (); m_strStatus = _T("Bereit"); } catch (CMemoryException *e)
Programmierung mit WinInet { m_strStatus = _T("InternetSession-Objekt konnte nicht" " erzeugt werden!"); e->Delete (); } // DDX durchführen UpdateData (false); return TRUE; // return TRUE unless you set the focus to a // control } void CFileRobotDlg::OnSysCommand(UINT nID, LPARAM lParam) { if ((nID & 0xFFF0) == IDM_ABOUTBOX) { CAboutDlg dlgAbout; dlgAbout.DoModal(); } else { CDialog::OnSysCommand(nID, lParam); } } // // // // //
Wollen Sie Ihrem Dialogfeld eine Schaltfläche "Minimieren" hinzufügen, benötigen Sie den nachstehenden Code, um das Symbol zu zeichnen. Für MFC-Anwendungen, die das Dokument/Ansicht-Modell verwenden, wird dies automatisch für Sie erledigt.
void CFileRobotDlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // Gerätekontext für Zeichnen SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Symbol in Client-Rechteck zentrieren int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2;
617
618
5
Internetprogrammierung
// Symbol zeichnen dc.DrawIcon(x, y, m_hIcon); } else { CDialog::OnPaint(); } } // Die Systemaufrufe fragen die Cursorform ab, die angezeigt // werden soll, während der Benutzer das zum Symbol // verkleinerte // Fenster mit der Maus zieht. HCURSOR CFileRobotDlg::OnQueryDragIcon() { return (HCURSOR) m_hIcon; } void CFileRobotDlg::OnButtonAddFile() { CListBox *pList = (CListBox*)GetDlgItem (IDC_LIST_FILES); UpdateData (); if (!m_strURL.IsEmpty () && !m_strFileName.IsEmpty ()) { pList->AddString (CreateURL (m_strURL, m_strFileName)); pList->SelectString (-1, CreateURL (m_strURL, m_strFileName)); } } void CFileRobotDlg::OnButtonRemoveFile() { CListBox *pList = (CListBox*)GetDlgItem (IDC_LIST_FILES); pList->DeleteString (pList->GetCurSel ()); pList->SetCurSel (0); } void CFileRobotDlg::OnButtonStart() { CListBox *pList = (CListBox*)GetDlgItem (IDC_LIST_FILES); CString strURL; for (int i=0; iGetCount (); i++) { pList->SetCurSel (i); pList->GetText (i, strURL); GetHTTPFile (strURL);
Das für die Verwendung von WinInet notwendige Sitzungsobjekt der Klasse CInternetSession wird in der Funktion OnInitDialog beim Erzeugen des Dialogs angelegt. Zu Beginn der Funktion OnInitDialog ist eine ganze Reihe von Initialisierungen zu sehen, die vom Anwendungs-Assistenten eingefügt worden sind. Der für das Programm FileRobot ergänzte Teil der Funktion OnInitDialog beginnt nach dem Kommentar Eigene Initialisierungen kommen hier. Nachdem das Sitzungsobjekt erzeugt worden ist, wird der Statustext auf »Bereit« gesetzt und durch den Aufruf der Funktion UpdateData per DDX in das Statusfeld des Dialogs geschrieben. Der Programmstatus wird an einigen anderen Stellen in gleicher Weise ausgegeben. Sollte beim Anlegen des Sitzungsobjekts ein Speicherfehler auftreten, so wird die dabei auftretende Ausnahme vom Typ CMemoryException abgefangen und ein Fehlertext in das Statusfeld übernommen. Die drei Funktionen OnButtonAddFile, OnButtonRemoveFile und OnButtonStart behandeln Klicks auf die drei Schaltflächen HINZUFÜGEN, ENTFERNEN und START. Für die Schaltfläche ENDE wird keine eigene Behandlungsfunktion benötigt, da es sich bei ihr lediglich um die umbenannte Schaltfläche OK handelt, für die bereits eine Behandlungsfunktion in den MFC existiert. Die Funktion OnButtonAddFile fügt einen Eintrag zur Liste der zu ladenden Dateien hinzu. Dazu werden zunächst die Werte der Steuerelemente des Dialogs durch einen Aufruf der Funktion UpdateData per DDX ausgelesen. Der vollständige URL der zu ladenden Datei wird dabei aus den Teilen Basis-URL, repräsentiert durch die DDX-Variable m_strURL, und Dateiname, repräsentiert durch die DDX-Variable m_strFileName, zusammengesetzt. Das Zusammensetzen der beiden Teile übernimmt die Funktion CreateURL, die als private Funktion der Dialogklasse angelegt worden ist. Wurde als Abschluss des Basis-URL kein Schrägstrich (Slash) angegeben, so wird dieser von der Funktion CreateURL automatisch ergänzt. Der so erzeugte vollständige URL wird in das Listenfeld des Dialogs übernommen und selektiert. Die Funktion OnButtonRemoveFile löscht den im Listenfeld ausgewählten URL. Die Funktion OnButtonStart initiiert den Dateitransfer. Dazu wird über das Listenfeld der URL-Einträge iteriert und jeder URL einzeln an die Funktion GetHTTPFile übergeben. Der Dateitransfer selbst wird von der Funktion GetHTTPFile durchgeführt.
Programmierung mit WinInet
In der Funktion GetHTTPFile werden zunächst die notwendigen Variablen deklariert. Dazu gehören unter anderem die Ausgabedatei outFile, eine Zeigervariable auf ein HTTP-Verbindungsobjekt pConnection, eine Zeigervariable auf ein HTTP-Dateiobjekt pHttpFile sowie ein Datenpuffer buffer. Zu Beginn wird der an die Funktion übergebene URL durch einen Aufruf der globalen MFCFunktion AfxParseURL in seine Teile zerlegt. Dann wird geprüft, ob der URL eine HTTP-Anforderung enthält (AFX_INET_ SERVICE_HTTP). Falls das Zerlegen des URLs durch die Funktion AfxParseURL fehlschlägt oder es sich nicht um eine HTTP-Anforderung handelt, wird der URL als fehlerhaft abgewiesen.
623 AfxParseURL
Alle Anweisungen nach dem erfolgreichen Parsen des URLs erfolgen in einem try-Block. Ausnahmen für Dateizugriffe (CFileException) und Internetzugriffe (CInternetException) werden abgefangen. Am Anfang des try-Blocks wird zunächst ein Warte-Cursor (meist eine Sanduhr, das Symbol hängt von den Einstellungen des Benutzers in der Systemsteuerung ab) gesetzt. Dazu muss einfach nur ein Objekt der Klasse CWaitCursor auf dem Stack angelegt werden. Solange das Objekt existiert, ist der Warte-Cursor sichtbar. Anschließend werden das Verbindungs- und das Dateiobjekt angefordert und die HTTP-Anfrage wird durch den Aufruf der Funktion SendRequest an den Server abgeschickt. Durch den Aufruf der Funktion QueryInfoStatusCode beim Dateiobjekt wird anschließend der Statuscode des HTTP-Servers abgefragt. Das hat folgenden Hintergrund: HTTP-Server liefern ihre Fehlermeldungen in Form von HTML-Ausgaben zurück. Eine solche Fehlermeldung zeigt Listing 5.5.
SendRequest
HTTP/1.0 404 Object Not Found
Listing 5.5: Fehlermeldung eines HTTP-Servers
Der Vorteil einer solchen Fehlermeldung ist, dass sie vom Browser direkt angezeigt werden kann. Die in der Fehlermeldung angegebene Zahl stellt den Statuscode der Übertragung dar. Der Statuscode wird nicht nur als Teil der HTML-Fehlermeldung angegeben, sondern wird zusätzlich immer als ein Teil des HTTP-Headers übertragen. Statuscodes werden nicht nur für Fehlermeldungen, sondern auch für erfolgreiche Übertragungen vergeben.
HTML-Fehlermeldungen
624
5
Internetprogrammierung
HTTP-Statuscode ermitteln
Eine Ausnahme des Typs CInternetException wird durch eine HTMLFehlermeldung nicht ausgelöst, denn es hat ja die erfolgreiche(!) Übertragung einer Fehlermeldung stattgefunden! Während solche Fehlermeldungen für Internetbrowser ungemein praktisch sind, taugen sie nicht für Programme, die zwar HTTP, aber nicht HTML verwenden. Daher muss der Statuscode des Servers abgefragt werden. Dieser lässt sich in WinInet durch die Funktion QueryInfoStatusCode der Klasse CHttpFile ermitteln. Diese Funktion liest den Statuscode aus dem HTTP-Header aus. Statuscodes kleiner als 400 zeigen einen Erfolg an, Codes ab 400 einen Fehler. Daher wird im Programm auf Statuscodes kleiner als 400 geprüft. Ließe man den Test des Statuscodes im Programm FileRobot einfach weg, dann würde es die HTTP-Fehlermeldungen als Dateien auf der Festplatte speichern.
Datenübertragung
Die Ausgabedatei wird erst angelegt, wenn keine HTTP-Fehlermeldung aufgetreten ist. Durch den Aufruf der API-Funktion GetModuleFileName werden der Name und der Pfad des Programms FileRobot erfragt. Durch etwas String-Akrobatik wird der Dateiname des Programms abgetrennt und der in dem URL enthaltene Dateiname an den Pfad angefügt. Anschließend wird eine Datei mit dem so erhaltenen vollständigen Dateinamen angelegt und zum Schreiben geöffnet. Daten werden durch den wiederholten Aufruf von CHttpFile::Read vom Server gelesen und in die Ausgabedatei geschrieben. Gleichzeitig wird die übertragene Datenmenge im Statusfeld angezeigt. Die Übertragung ist abgeschlossen, sobald die Funktion Read den Wert 0 für die Anzahl gelesener Byte zurückgibt. Nach dem Abschluss der Übertragung werden die beiden Dateien geschlossen und das Verbindungs- und das Dateiobjekt gelöscht.
5.3.8 OpenURL
Das Beispielprogramm FileRobot2
Neben der mit dem Programm FileRobot beschriebenen protokollspezifischen Datenübertragung gibt es eine zweite Möglichkeit, mit der WinInet-Bibliothek Dateien zu übertragen. Die Funktion CInternetSession::OpenURL kann eine Datei unabhängig vom verwendeten Protokoll öffnen und Daten durch normale Dateizugriffe übertragen. Bei dem Protokoll, das in dem URL angegebenen wird, muss es sich allerdings um eines der drei von der WinInet-Bibliothek unterstützten Protokolle handeln. Die weitere Vorgehensweise ähnelt stark der Datenübertragung durch eine protokollspezifische Verbindung. Die Einflussmöglichkeiten auf die Datenübertragung sind allerdings geringer als bei der Verwendung einer protokollspezifischen Verbindung.
Programmierung mit WinInet
Um die Arbeitsweise der Funktion OpenURL zu demonstrieren, ist das Programm FileRobot so modifiziert worden, dass es mit dieser Funktion arbeitet. Das so modifizierte Beispielprogramm heißt FileRobot2 und befindet sich auf der Begleit-CD im Verzeichnis KAPITEL5\FILEROBOT2. Das Beispielprogramm FileRobot2 unterscheidet sich im Wesentlichen nur durch die Funktion GetURLFile von der ersten Version des Programms. GetURLFile ist die Funktion, die das Laden der Datei übernimmt. Sie ersetzt die Funktion GetHTTPFile des Beispielprogramms FileRobot. Die Funktion ist in Listing 5.6 zu sehen. bool CFileRobotDlg::GetURLFile(const CString &strURL) { DWORD dwServiceType; CString strServer, strFile; INTERNET_PORT nPort; CStdioFile *pInFile; CFile outFile; int nLoadCnt = 0; static const int BUFFERSIZE = 1024; char buffer[BUFFERSIZE]; pInFile = m_pSession->OpenURL ( strURL, 1, INTERNET_FLAG_TRANSFER_BINARY); if (pInFile && AfxParseURL (strURL, dwServiceType, strServer, strFile, nPort)) { CWaitCursor cursor; int nCount; try { // Dateinamen zusammenbauen und Ausgabedatei öffnen ::GetModuleFileName (AfxGetInstanceHandle (), buffer, BUFFERSIZE); CString strOutFile = buffer; strOutFile = strOutFile.Left ( strOutFile.ReverseFind ('\\') + 1) + strFile.Right (strFile.GetLength () - strFile.ReverseFind ('/') - 1); outFile.Open (strOutFile, CFile::modeCreate |
Programmierung mit WinInet else { m_strStatus = _T("Fehler in URL!"); UpdateData (false); return false; } } Listing 5.6: Die Funktion GetURLFile
Im Gegensatz zur Funktion GetHTTPFile aus Listing 5.4 wird der an die Funktion GetURLFile in Listing 5.6 übergebene URL nicht zunächst in seine Teile zerlegt, sondern komplett an die Funktion OpenURL übergeben. Kann diese Funktion mit dem übergebenen URL etwas anfangen, dann gibt sie einen Zeiger auf ein Dateiobjekt der Klasse CStdioFile zurück. Im Beispiel wird der Funktion OpenURL neben dem URL das Flag INTERNET_FLAG_TRANSFER_ BINARY übergeben. Dieses gibt für den Fall von FTP-Verbindungen den Übertragungsmodus binär vor. Sofern die Funktion OpenURL einen gültigen Zeiger zurückgegeben hat, wird anschließend der URL zerlegt, um daraus den Dateinamen für die Ausgabedatei zu gewinnen. Der Name der Ausgabedatei wird zusammengesetzt, die Datei angelegt und zum Schreiben geöffnet. Die Datenübertragung selbst erfolgt analog zum letzten Beispiel, ebenfalls das Schließen und Löschen der beteiligten Objekte. Die hier beschriebene Vorgehensweise unter Ausnutzung der Funktion OpenURL hat den Vorteil, dass neben HTTP auch FTP und Gopher als Übertragungsprotokolle verwendet werden können. Wollte man das Beispielprogramm FileRobot um diese beiden Protokolle ergänzen, so müsste man drei Übertragungsfunktionen vorsehen, eine für HTTP, eine für FTP und eine für Gopher. Nachteilig bei der Verwendung der Funktion OpenURL sind die geringeren Kontrollmöglichkeiten während der Übertragung. So ergibt sich bei auftretenden HTTP-Fehlern der bereits in Abschnitt 5.3.7, »Das Beispielprogramm FileRobot«, beschriebene Effekt, dass die HTTP-Fehlermeldungen als Dateien gespeichert werden! Die Funktion OpenURL unterstützt zudem den zu Anfang erwähnten Status-Callback-Mechanismus der Klasse CInternetSession nicht. Möchte man diese Einschränkungen vermeiden, so darf die Funktion OpenURL nicht verwendet werden. Man muss stattdessen mit Verbindungs- und Dateiobjekten arbeiten und die Übertragung für jedes Protokoll einzeln implementieren.
627
628
5
5.3.9
Internetprogrammierung
Tipps zur Vorgehensweise
왘 Besitzt man keine feste Verbindung zum Internet, so empfiehlt es sich, eigene Server-Software lokal zu installieren. Windows NT und Windows 2000 liefern passende Server-Software mit, die allerdings installiert werden muss. Neben der Server-Software muss das TCP/IP-Protokoll installiert sein. Falls der Rechner noch keine IP-Adresse besitzt, muss ihm eine zugewiesen werden. Es bietet sich hier eine Adresse aus dem Bereich zwischen 192.168.0.1 und 192.168.0.255 an. Diese Adressen sind für die Benutzung zu Testzwecken vorgesehen und werden im Internet nicht verwendet. Konflikte mit eventuell doch bestehenden Internetzugängen gibt es nicht, da Anfragen an diese Adressen nicht ins Internet weitergeleitet werden. 왘 Bei der Verwendung von WinInet muss zunächst ein Sitzungsobjekt angelegt werden. Von diesem wird ein Verbindungsobjekt angefordert. Das Verbindungsobjekt wiederum fordert ein Dateiobjekt an, mit dem die Daten übertragen werden können. Nach der Übertragung müssen Verbindungs- und Dateiobjekt vom Programmierer freigegeben werden. Alternativ kann die Funktion OpenURL verwendet werden. 왘 Zugriffe auf WinInet sollten nach Möglichkeit in einem eigenen Thread erfolgen, um die Benutzerschnittstelle nicht zu blockieren. 왘 Soll der Callback-Mechanismus der Klasse CInternetSession verwendet werden, so muss man von dieser eine eigene Klasse ableiten und die Callbacks durch Aufruf der Funktion EnableStatusCallback aktivieren. Für jeden Callback wird die Funktion OnStatusCallback aufgerufen. Die Funktion OnStatusCallback muss daher in der abgeleiteten Klasse überschrieben werden. 왘 Möchte man ein einfaches Client-Programm für mehrere Protokolle entwickeln, so erreicht man mit der Funktion OpenURL schneller das Ziel. Für professionelle Programme bietet es sich jedoch an, jedes Protokoll separat über die entsprechenden Verbindungs- und Dateiobjekte anzusprechen. Nur so kann man auf die Besonderheiten des Protokolls eingehen und eine individuelle Fehlerbehandlung implementieren.
Programmierung von Server-Erweiterungen mit ISAPI
629
5.3.10 Zusammenfassung Die WinInet-Bibliothek bietet eine recht einfache Möglichkeit der Programmierung von Internetanwendungen auf der Basis der Protokolle HTTP, FTP und Gopher. Da diese Protokolle bereits durch WinInet implementiert werden, findet die Programmierung auf einem deutlich höheren Niveau statt als die alternativ zu verwendende Winsock-Programmierung, die direkt auf dem Protokoll TCP/IP aufsetzt. WinInet lässt Datenübertragungen durch die unterstützten Protokolle wie einfache Dateizugriffe aussehen. Unter Winsock schwer zu implementierende Eigenschaften, wie das Caching der übertragenen Daten und SSL, bekommt man bei WinInet praktisch geschenkt. Der größte Nachteil von WinInet ist die Beschränkung auf die drei genannten Protokolle. Wer ein anderes Protokoll verwenden möchte, muss auch weiterhin auf die Windows-Sockets zurückgreifen.
5.4
Programmierung von ServerErweiterungen mit ISAPI
Es gibt Aufgaben, die HTML alleine nicht leisten kann. Ein Schwachpunkt der Beschreibungssprache ist die mangelnde Interaktivität. Diese mangelnde Interaktivität versucht man durch Integration von Java-Applets, ActiveX-Steuerelementen, Plug-Ins wie Macromedia Flash und Skriptsprachen wie JavaScript auf der Client-Seite in den Griff zu bekommen. Ein zweiter Schwachpunkt von HTML ist, dass die Sprache keine Möglichkeit vorsieht, Daten in eine HTML-Seite einzubetten. Möchte man Daten auf einer HTML-Seite anzeigen, so müssen diese vom Webserver in das HTML-Format konvertiert werden. Da die anzuzeigenden Daten, je nach Anwendung, auf völlig unterschiedliche Weise in HTML konvertiert werden müssen, kann der Server selbst kein Standardschema für alle möglichen Fälle der Konvertierung anbieten. Daher muss der Server entweder externe Programme aufrufen können oder er muss selbst durch Module erweiterbar sein. Um diese abstrakte Beschreibung zu verdeutlichen, stelle man sich ein System zur Annahme von Online-Bestellungen vor. Der Kunde sucht sich mit seinem Browser im Katalog, der aus normalen HTML-Seiten besteht, eine Anzahl von Waren aus, die er anschließend bestellt. Dazu füllt er ein Bestellformular aus, das
Schwachpunkte von HTML
630
5
Internetprogrammierung
sich auch mit den Mitteln von HTML realisieren lässt, und schickt es an den Webserver. Hier kommt nun der Punkt, an dem man mit einfachem HTML und dem Webserver allein nicht mehr weiterkommt. Der Server muss die Daten des Kunden entgegennehmen und an das Bestellsystem des Anbieters weiterleiten. Dann muss, individuell der Bestellung des Kunden entsprechend, eine Bestätigung oder Rechnung in HTML generiert werden, die an den Browser des Kunden geschickt wird. CGI-Schnittstelle
Eine Möglichkeit, die beschriebene Funktionalität zu realisieren, besteht darin, den Server externe Programme aufrufen zu lassen, die die Daten des Kunden speichern und die Rechnung generieren. Externe Programme kann ein Webserver durch das Common Gateway Interface (CGI) aufrufen. Diese plattformübergreifende und portable Schnittstelle wird von fast allen Webservern implementiert. Sie definiert, wie ein Webserver externe Programme aufrufen kann und wie Daten zwischen Programm und Server ausgetauscht werden können. Die externen Programme können in fast jeder beliebigen Programmiersprache geschrieben werden. Oft wird hierzu die Sprache Perl verwendet. Obwohl CGI-Programme vielfach eingesetzt werden, haben sie einige Nachteile. Der wohl größte Nachteil ist, dass die CGI-Schnittstelle relativ langsam ist.
Serverprogramm
Servererweiterung oder externes Programm
Internet
Clientprogramm Abbildung 5.13: Serverprogramm mit Erweiterung ISAPI
Alternativ zu CGI-Programmen besteht die Möglichkeit, Erweiterungsmodule für Webserver zu implementieren. Diese Erweiterungsmodule werden normalerweise in Form von DLLs programmiert. Es gibt mehrere Schnittstellen für WebserverErweiterungen; Netscape, Apache und Microsoft definieren eigene Schnittstellen. Die Schnittstelle von Microsoft wird mit der
Programmierung von Server-Erweiterungen mit ISAPI
631
Abkürzung ISAPI, Internet Server Application Programming Interface, bezeichnet. Neben den Microsoft-Servern unterstützt die Windows-Version des Webservers Apache die ISAPI-Schnittstelle. Auf der Windows-Plattform ist ISAPI mittlerweile die Standardschnittstelle für Webserver-Erweiterungen. Egal, ob man CGI-Programme oder Server-Erweiterungen verwendet, diese Programme werden für den Client nicht direkt sichtbar. Der Client greift nach wie vor auf das Serverprogramm zu. Die Kommunikation mit dem CGI-Programm oder mit der Server-Erweiterung erfolgt über das Serverprogramm. Dies ist in Abbildung 5.13 zu sehen.
5.4.1
MFC-Klassen für die ISAPI-Programmierung
Die ISAPI-Schnittstelle wird durch Klassen der MFC unterstützt. Diese Klassen sind ausschließlich für die Programmierung dieser Schnittstelle gedacht und können außerhalb von ISAPI-Erweiterungen nicht verwendet werden. Die MFC-Klassen zur Programmierung der ISAPI-Schnittstelle zeigt Abbildung 5.14.
Die MFC bilden die Programmierung der ISAPI-Schnittstelle in sechs Klassen ab. Davon dienen die Klassen CHttpArgList, CHtmlStream, CHttpServer und CHttpServerContext der Programmierung von ISAPI-Servererweiterungen und die Klassen CHttpFilter und CHttpFilterContext der Programmierung von ISAPI-Filtern. Die Ausgabe der HTML-Anweisungen an den Client erfolgt durch ein Objekt der Klasse CHtmlStream. CHtmlStream verwaltet einen Ausgabepuffer, in den die HTML-Anweisungen hineingeschrieben werden, bevor sie an den Client gesendet werden.
CHtmlStream
632
5
Internetprogrammierung
CHttpServer
Die Klasse CHttpServer repräsentiert eine Server-Erweiterung. Pro Server-Erweiterung gibt es immer nur eine Instanz dieser Klasse. CHttpServer ist mit der Applikationsklasse einer normalen MFCAnwendung vergleichbar. Eine Instanz dieser Klasse wird erzeugt, sobald ein Browser oder Client-Programm zum ersten Mal auf die Server-DLL zugreift. Die DLL bleibt so lange geladen, bis der Webserver beendet wird. Damit bleibt auch das Objekt der Klasse CHttpServer bis zu diesem Zeitpunkt bestehen.
CHttpServerContext
Für jede Anfrage eines Clients an die Server-DLL wird ein Objekt der Klasse CHttpServerContext erzeugt. Dieses Objekt beschreibt die Verbindung zwischen Client und Server. Da ein Webserver mehrere Client-Zugriffe gleichzeitig bedienen kann, können auch mehrere Objekte der Klasse CHttpServerContext gleichzeitig bestehen. Jeder Client-Zugriff wird in einem eigenen Thread behandelt. Dadurch befinden sich auch die CHttpServerContext-Objekte jeweils in einem dieser Threads. Zugriffe auf globale Variablen oder Variablen des CHttpServer-Objekts müssen daher durch die Synchronisationsmechanismen von Windows Thread-sicher gekapselt werden.
CHttpArgList
Diese Klasse wurde mit der Version 7.0 der MFC neu eingeführt. Mit Objekten der Klasse CHttpArgList lassen sich HTTP-Argumente flexibler handhaben, als dies vorher möglich war. So kann mit Objekten dieser Klasse eine variable Anzahl von HTTP-Argumenten ausgewertet werden.
5.4.2
Die Server-Erweiterung GuestBook
Die Server-Erweiterung GuestBook implementiert ein Gästebuch, in das Besucher eines Webservers Eintragungen machen können. Dieses Beispielprogramm befindet sich auf der Begleit-CD im Verzeichnis KAPITEL5\GUESTBOOK. Im Gästebuch werden der Name des Besuchers und ein kurzer Text gespeichert. Das Gästebuch besteht aus vier Seiten: 왘 Auf der Hauptseite kann der Besucher wählen, ob er einen neuen Eintrag vornehmen oder ob er sich die bestehenden Einträge ansehen möchte (Abbildung 5.15). 왘 Möchte der Besucher die bestehenden Einträge betrachten, so werden ihm diese gesammelt auf einer Seite angezeigt (Abbildung 5.18).
Programmierung von Server-Erweiterungen mit ISAPI
왘 Wenn sich der Besucher ins Gästebuch eintragen möchte, gelangt er zu einer Eingabemaske (in HTML Form genannt), in der er seinen Namen und einen kurzen Text eingeben kann. Diese Daten kann er dann durch einen Klick auf eine Schaltfläche an den Server absenden (Abbildung 5.16). 왘 Nach Eingabe neuer Daten werden dem Besucher die gerade eingetragenen Daten in Form einer Bestätigungsseite angezeigt (Abbildung 5.17).
Abbildung 5.15: Die Hauptseite des Gästebuchs
Die Hauptseite ist eine normale HTML-Seite, die durch die HTML-Datei MAIN.HTML realisiert wird. Diese Datei ist Teil des GuestBook-Projekts. Sie ist in Listing 5.7 zu sehen. <META NAME="GENERATOR" Content="Microsoft Developer Studio"> <META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1"> <TITLE>Gästebuch Hauptseite
Die Gästebuch-DLL besitzt eine Funktion, die aufgerufen wird, wenn die DLL ohne weitere Parameter angesprochen wird. Diese Funktion zeigt dem Besucher alle bereits im Gästebuch bestehenden Einträge an. Der Aufruf der DLL ohne weitere Parameter ist in Listing 5.7 zu sehen.
Abbildung 5.16: Einen Eintrag in das Gästebuch einfügen
Die Eingabemaske zur Eingabe von Name und Text wird ebenfalls durch eine HTML-Datei realisiert. Diese Datei heißt ADD.HTML und ist in Listing 5.8 zu sehen. <META NAME="GENERATOR" Content="Microsoft Developer Studio"> <META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1"> <TITLE>Gästebuch: Eintrag hinzufügen
Die Eingabe wird durch das HTML-Tag FORM realisiert. Durch den Parameter ACTION des Tags wird angegeben, was der Server mit den Daten machen soll, die er vom Browser des Clients erhält. Die Angabe im Listing bedeutet, dass der Browser die Daten an die Server-Erweiterung GUESTBOOK.DLL übergeben soll, wobei die Funktion add dieser DLL aufgerufen wird. Die Daten der Eingabemaske, also die Daten des Eingabefelds mit dem Namen »Name« und des Textfelds mit dem Namen »Text«, werden der Funktion add als Parameter übergeben. Als Übertragungsmethode für die Daten ist im FORM-Tag der Wert POST angegeben. Eine Übertragung per POST bedeutet, dass die zu übertragenden Daten Teil des an den Server gesendeten HTTP-Headers sind. Alternativ kann man die Übertragungsmethode GET wählen. Bei dieser Methode werden Daten an den aufzurufenden URL angehängt (also hinter GUESTBOOK?ADD). GET hat den Nachteil, dass die Menge Daten, die übertragen werden können, beschränkt ist. Je nach verwendetem Browser werden die Daten eventuell abgeschnitten, falls man versucht, größere Datenmengen auf diese Weise an den Server zu senden. Vorteilhaft ist dagegen, dass Daten mit dem URL – beispielsweise als Lesezeichen – gespeichert werden können. Als Bestätigung auf das Absenden eines neuen Eintrags erzeugt die Server-Erweiterung die in Abbildung 5.17 gezeigte Ausgabe. Name und Text des neuen Eintrags werden angezeigt. Da es sich hier um vom Besucher eingegebene Daten handelt, muss diese Seite dynamisch erzeugt werden. Es gibt keine HTML-Datei für diese Seite auf dem Server.
HTML-Formulare
636
5
Internetprogrammierung
Abbildung 5.17: Bestätigung des Eintrags ins Gästebuch
Abbildung 5.18: Einträge im Gästebuch ansehen
Die Seite, die alle Einträge des Gästebuchs anzeigt, muss ebenfalls dynamisch vom Server erzeugt werden.
Programmierung von Server-Erweiterungen mit ISAPI
637
Um eine Server-Erweiterung zu testen, muss diese nicht unbedingt in das Stammverzeichnis des Servers kopiert werden. Einfacher ist es, ein zusätzliches Serververzeichnis zu definieren, das mit dem Ausgabeordner des Compilers übereinstimmt. Dieses Verzeichnis wird im InternetdiensteManager unter dem Menüpunkt VORGANG | NEU | VIRTUELLES VERZEICHNIS zum WWW-Serverdienst hinzugefügt. Abbildung 5.19 zeigt den entsprechenden Dialog des Internetdienst-Managers.
Abbildung 5.19: Virtuelles Verzeichnis gbook im Internetdienste-Manager
Bei der Anlage eines neuen Serververzeichnisses lässt sich ein Alias für dieses angeben. Der Alias ist ein virtuelles Verzeichnis, über das auf die Daten des neuen Serververzeichnisses zugegriffen werden kann. In Abbildung 5.19 wurde GBOOK als Alias für das Verzeichnis der Gästebucherweiterung gewählt. Um die Servererweiterung GuestBook testen zu können, müssen zunächst die beiden Dateien MAIN.HTML und ADD.HTML in das Verzeichnis der Server-DLL kopiert werden. Dies ist beim Testen normalerweise das Debug-Verzeichnis. Das Verzeichnis muss dem WWW-Server bekannt gemacht werden und der WWW-Dienst
Test des Gästebuchs
638
5
Internetprogrammierung
muss gestartet sein. Wählt man den Alias GBOOK für das Verzeichnis der Erweiterung, dann lässt sich das Gästebuch durch Aufruf des URL http://localhost/gbook/main.html
starten. Nimmt man Änderungen an einer Server-Erweiterung vor, so muss man den IIS erneut starten (VORGANG | IIS ERNEUT STARTEN...), bevor man die Änderungen übersetzt. Bei einer bereits im Speicher befindlichen Erweiterungs-DLL kann die Entwicklungsumgebung ansonsten nicht linken. Weitere Tipps zum Testen von ISAPI-Erweiterungen findet man in dem technischen Hinweis 63. Assistent für ISAPI-Erweiterungen
Die Server-Erweiterung GuestBook ist mit dem Assistenten für ISAPI-Erweiterungen erstellt worden. Wählt man den Projekttyp SERVER-ERWEITERUNGSOBJEKT aus, dann wird das Projekt in einem Schritt erstellt. Der Assistent ist in Abbildung 5.20 zu sehen.
Abbildung 5.20: Assistent für ISAPI-Erweiterungen
Im Assistenten werden lediglich der Klassenname und die Beschreibung der Erweiterung sowie die Art angegeben, wie die MFC-Bibliothek zu dem Projekt gelinkt werden soll. Daraufhin wird der Sourcecode für die Erweiterung erzeugt.
Programmierung von Server-Erweiterungen mit ISAPI
Implementiert wird die Erweiterung in den beiden Dateien GUESTBOOK.H und GUESTBOOK.CPP. Listing 5.9 zeigt die Implementierungsdatei GUESTBOOK.CPP // GUESTBOOK.CPP // Implementierungsdatei für Ihren Internet-Server // GuestBook Extension #include "stdafx.h" #include "GuestBook.h" ////////////////////////////////////////////////////////////// / // Das einzige CWinApp-Objekt // HINWEIS: Sie können dieses Objekt entfernen, wenn Sie Ihr // Projekt in ein Nicht-MFC-Projekt ändern und in einer DLL // verwenden. CWinApp theApp; ////////////////////////////////////////////////////////////// / // Tabelle zur Befehlsinterpretation BEGIN_PARSE_MAP(CGuestBookExtension, CHttpServer) // ZU ERLEDIGEN: Fügen Sie Ihr ON_PARSE_COMMAND() ein und // ON_PARSE_COMMAND_PARAMS() hier, um Ihre Befehle // einzubinden. // Beispiel: ON_PARSE_COMMAND(Default, CGuestBookExtension, ITS_EMPTY) ON_PARSE_COMMAND(Add, CGuestBookExtension, ITS_PSTR ITS_PSTR) ON_PARSE_COMMAND_PARAMS("Name text") DEFAULT_PARSE_COMMAND(Default, CGuestBookExtension) END_PARSE_MAP(CGuestBookExtension)
////////////////////////////////////////////////////////////// / // Das einzige CGuestBookExtension-Objekt CGuestBookExtension theExtension;
"); // Link auf Hauptseite *pCtxt << _T("Hauptseite"); EndContent(pCtxt); // Eintrag zusammenbauen CEntry anEntry; anEntry.strName = name; anEntry.strText = message; // Eintrag threadsicher zur Liste hinzufügen m_cs.Lock ();
641
642
5
Internetprogrammierung
m_entryList.AddTail (anEntry); m_cs.Unlock (); }
// Die folgenden Zeilen nicht bearbeiten. // Sie werden vom Klassenassistenten benötigt. #if 0 BEGIN_MESSAGE_MAP(CGuestBookExtension, CHttpServer) END_MESSAGE_MAP() #endif // 0
///////////////////////////////////////////////////////////// // Verwendet Ihre Erweiterung nicht die MFC, benötigen Sie // diesen Code, um sicherzustellen, dass die // Erweiterungsobjekte das Ressourcen-Handle für das Modul // finden können. Wollen Sie Ihre Erweiterung so umwandeln, // dass sie nicht von MFC abhängt, entfernen Sie die // Kommentare um die nachfolgenden AfxGetResourceHandle() // und DllMain()-Funktionen sowie die globale Varaible // g_hInstance. /**** static HINSTANCE g_hInstance; HINSTANCE AFXISAPI AfxGetResourceHandle() { return g_hInstance; } BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason, LPVOID lpReserved) { if (ulReason == DLL_PROCESS_ATTACH) { g_hInstance = hInst; } return TRUE; } ****/ LPCTSTR CGuestBookExtension::GetTitle() const { return _T("Gästebuch"); } Listing 5.9: Die Implementierungsdatei GUESTBOOK.CPP
Programmierung von Server-Erweiterungen mit ISAPI
Die Definition der Parsemap der Server-Erweiterung ist zu Beginn des Listings 5.9 zu sehen. Die Parsemap (in der Online-Hilfe auch als Analysezuordnung bezeichnet) ist eine MFC-typische Zuordnungstabelle, die per HTTP aufgerufene Funktionen auf die MFCFunktionen der Server-Erweiterung abbildet. Die Server-Erweiterung GuestBook stellt zwei per HTTP aufrufbare Funktionen zur Verfügung: die Funktion Add zum Hinzufügen eines Eintrags in das Gästebuch und die Funktion Default zum Anzeigen der Einträge des Gästebuchs. Die Funktion Default ist die Funktion der Server-Erweiterung, die aufgerufen wird, wenn man in dem aufrufenden URL keinen Funktionsnamen angibt. Jede Server-Erweiterung sollte diese Funktion implementieren. Einfache ServerErweiterungen kommen unter Umständen sogar nur mit dieser einen Funktion aus. Wenn man ein Projekt mit dem Assistenten für ISAPI-Erweiterungen anlegt, dann wird diese Funktion unter dem Namen Default angelegt und in die Parsemap eingetragen. Die Definition der Funktion ist leer und muss vom Programmierer ausgefüllt werden. Weitere Funktionen – im Beispiel die Funktion Add – müssen von Hand in die Parsemap eingetragen werden. Das Makro ON_PARSE_COMMAND stellt die Verbindung zwischen dem Kommando- oder Funktionsnamen, der in dem URL angegeben wird, und der implementierenden Funktion her. Der in dem URL verwendete Funktionsname und der im Programm verwendete Funktionsname müssen übereinstimmen. Dieser Funktionsname wird durch den ersten Parameter des Makros ON_PARSE_COMMAND angegeben. Der zweite Parameter des Makros beschreibt die implementierende Klasse. Der dritte Parameter definiert die Argumente der Funktion. Es wird lediglich der Typ der Argumente angegeben. Die Argumente werden durch Konstanten beschrieben, die in der Datei AFXISAPI.H definiert sind. Mehrere Argumente werden durch eine entsprechende Anzahl von Konstanten beschrieben, die jeweils durch ein Leerzeichen getrennt sind. Eine leere Argumentliste muss durch die Konstante ITS_EMPTY angegeben werden. Gibt man in der Parsemap die Konstante ITS_ARGLIST an, so kann man die Argumente einer Funktion flexibel verarbeiten. Man bekommt dann alle Argumente in Form eines Zeigers auf ein CHttpArgList-Objekt übergeben und muss die Dekodierung der Argumente teilweise selbst vornehmen. Allerdings ist man auf diese Weise nicht an starre Argumentübergaben gebunden.
643 Parsemap
644
5 Parameterübergabe
Internetprogrammierung
Die Funktion, die das in der Parsemap beschriebene Kommando implementiert, muss entsprechend der dort beschriebenen Argumentliste aufgebaut sein. So muss passend zu dem Eintrag ON_PARSE_COMMAND(MyFunc, CMyExtension, ITS_I4 ITS_R8 ITS_PSTR)
die Funktion void CMyExtension::MyFunc (CHttpServerContext* pCtxt, long aLong, double aDouble, LPCTSTR aString)
angelegt werden. Die Parameternamen, die innerhalb von HTML verwendet und anschließend an die Server-Erweiterung übergeben werden, müssen durch ein weiteres Makro angegeben werden. Dieses Makro mit dem Namen ON_PARSE_COMMAND_PARAMS muss unmittelbar auf das Makro ON_PARSE_COMMAND folgen. Für das Beispiel könnte es folgendermaßen aussehen: ON_PARSE_COMMAND_PARAMS("MYLONG MYDOUBLE MYSTRING")
In HTML würden die Parameter dann durch die Namen MYLONG, MYDOUBLE und MYSTRING bezeichnet werden. Default-Funktion
Schließlich wird in der Parsemap angegeben, welche Funktion aufgerufen werden soll, wenn sich in dem aufrufenden URL kein Funktions- oder Kommandoname befindet. Diese Funktion wird durch das Makro DEFAULT_PARSE_COMMAND spezifiziert. Dabei muss es sich um eine Funktion handeln, die bereits durch das Makro ON_PARSE_COMMAND beschrieben wurde. Die Parsemap im Beispiel GuestBook beschreibt die beiden bereits erwähnten Funktionen Add und Default. Die Funktion Default hat eine leere Parameterliste, die Funktion Add besitzt zwei StringParameter, nämlich den Namen des Besuchers und den von ihm eingegebenen Text. Die Funktion Default wird als Funktion markiert, die ausgeführt werden soll, wenn in dem URL keine Funktion angegeben wird.
GetExtensionVersion und HttpExtensionProc
Nach der Parsemap wird in Listing 5.9 das Objekt der Klasse CGuestBookExtension angelegt. Dieses wird für alle Anfragen an den Server verwendet. Nach den Definitionen von Konstruktor und Destruktor der Klasse CGuestBookExtension folgt die Funktion GetExtensionVersion. Die im Listing gezeigte Implementierung ist vom Anwendungs-Assistenten erzeugt worden. Diese Funktion
Programmierung von Server-Erweiterungen mit ISAPI
645
muss – neben der Funktion HttpExtensionProc – für jede ServerErweiterung zur Verfügung gestellt werden. Die Funktion HttpExtensionProc wird allerdings bereits durch die MFC implementiert. Beim Beenden der Server-Erweiterung wird die Funktion TerminateExtension aufgerufen. Das ist erst der Fall, wenn der WWWDienst gestoppt wird. In dieser Funktion können Aufräumarbeiten durchgeführt werden.
TerminateExtension
Nach der Funktion TerminateExtension folgt die Implementierung der beiden Funktionen Default und Add. Beiden Funktionen wird als jeweils erster Parameter ein Zeiger auf ein Objekt der Klasse CHttpServerContext übergeben. Dieses Objekt beschreibt die aktuelle Verbindung zu einem Client. Da ein WWW-Server mehrere Verbindungen gleichzeitig bearbeiten können muss, ist es möglich, dass die Funktionen Default und Add zur gleichen Zeit mehrfach aufgerufen und ausgeführt werden müssen. Es ist also auf Thread-Sicherheit beim Zugriff auf gemeinsame Objekte zu achten. Das betrifft im Beispiel die Datenstruktur, in der die Gästebucheinträge abgelegt werden. Der Einfachheit halber verwendet die Server-Erweiterung eine Liste von String-Paaren, die sie im Hauptspeicher hält. Nach dem Stoppen des WWW-Dienstes sind also alle Einträge verschwunden. Im Rahmen dieses Beispiels ist diese Art der Speicherung ausreichend. Listing 5.10 zeigt die Datenstruktur, in der Gästebucheinträge gespeichert werden. // Klasse für Einträge in das Gästebuch class CEntry { public: CString strName; CString strText; }; CList m_entryList; Listing 5.10: Datenstruktur zum Speichern der Gästebucheinträge
Die Variable m_entryList wird als private Member-Variable der Klasse CGuestBookExtension deklariert. Sie dient zur Speicherung aller Gästebucheinträge. Jeder Eintrag wird durch ein Objekt der Klasse CEntry repräsentiert. Zur Synchronisation wird zudem die Variable m_cs der Klasse CCriticalSection deklariert. Die Funktion Default aus Listing 5.9 gibt die Liste aller Gästebucheinträge aus. Zunächst ruft sie die Funktion StartContent auf, die den HTML-Strom »startet«, also normalerweise die Tags
StartContent, WriteTitle und GetTitle
646
5
Internetprogrammierung
und in den HTML-Ausgabestrom schreibt. Der anschließende Aufruf der Funktion WriteTitle schreibt die für den Titel der Seite notwendigen Tags in den Ausgabestrom. Der Titel selbst wird durch den Aufruf der Funktion GetTitle bestimmt und ausgegeben. Im Beispiel gibt die Funktion GetTitle den Text »Gästebuch« zurück. Operator <<
Nach Aufruf der Funktionen StartContent und WriteTitle wird innerhalb der Funktion Default eine Reihe von Ausgaben in den HTMLAusgabestrom geschrieben. Diese Ausgaben erfolgen durch den Operator <<. Dieser Operator schreibt an ihn übergebene Strings und andere Objekte in den HTML-Strom, der durch die Variable m_pStream des CHttpServerContext-Objekts definiert wird.
Kritischer Abschnitt
Zunächst werden die Überschrift und der erste Trennstrich ausgegeben. Anschließend tritt die Funktion durch Aufruf von m_cs.Lock in den kritischen Abschnitt ein. Damit wird die Liste der Gästebucheinträge vor dem gleichzeitigen Zugriff durch mehrere Threads geschützt. In der while-Schleife wird über alle Einträge der Liste iteriert. Sie werden als HTML in den Ausgabestrom geschrieben. Nach der Ausgabe der Liste wird der kritische Abschnitt durch den Aufruf von m_cs.Unlock verlassen und der Link auf die Hauptseite der Gästebuchanwendung ausgegeben. Der Aufruf der Funktion EndContent schließt den HTML-Strom mit den Tags und ab. Die sich anschließende Funktion Add zum Einfügen eines Eintrags in das Gästebuch besitzt einige Ähnlichkeit mit der Funktion Default. Zu Beginn stehen wieder die Funktionen StartContent und WriteTitle. Danach wird die Bestätigungsseite in HTML zusammengebaut, wobei der als Parameter übergebene Name des Besuchers und der ebenfalls als Parameter übergebene Text verwendet werden. Die Zusammenfassungsseite wird durch den Aufruf der Funktion EndContent abgeschlossen. Danach erfolgt der Eintrag von Name und Text in die Liste der Gästebucheinträge. Dies muss wieder Thread-sicher erfolgen und wird daher von der Variablen m_cs geschützt. Ein Objekt der Klasse CEntry wird angelegt, Name und Text werden dem Eintrag zugewiesen, und dieser Eintrag wird dann durch den Aufruf der Funktion AddTail an das Ende der Liste der Einträge angehängt.
5.4.3
ISAPI-Deployment
Der Assistent für ISAPI-Erweiterungen bietet die Möglichkeit, neben einem Projekt zur Erstellung der ISAPI-DLL auch gleich ein Windows Installationsprogramm zur Auslieferung (Deployment)
Programmierung von Server-Erweiterungen mit ISAPI
der Erweiterung zu erstellen. Dazu ist lediglich die Option DEPLOYMENT-SUPPORT auszuwählen. Für das Installationsprogramm wird ein eigenes Projekt angelegt. Das erzeugte Installationsprogramm trägt die Endung MSI und lässt sich per Doppelklick starten. Alle vom Installationsprogramm benötigten Dateien befinden sich in dem gleichen Ordner.
Abbildung 5.21: Das Installationsprogramm bei der Arbeit
Die mit dem Installationprogramm installierten ISAPI-Erweiterungen oder -Filter werden von Windows verwaltet und können somit einfach in der Systemsteuerung unter SOFTWARE | PROGRAMME ÄNDERN ODER ENTFERNEN deinstalliert werden.
Abbildung 5.22: Die Gästebuch-Erweiterung in der Systemsteuerung
647
648
5
5.4.4
Internetprogrammierung
ISAPI-Filter
Neben ISAPI-Server-Erweiterungen gibt es ISAPI-Filter. Ein ISAPIFilter reagiert auf ein oder mehrere Ereignisse, die bei jeder HTTPAnfrage ausgelöst werden. Der Filter hat dann die Möglichkeit, den HTML-Datenstrom und die HTTP-Header zu untersuchen und gegebenenfalls zu verändern. Im Anwendungs-Assistenten für ISAPI-Erweiterungen lassen sich, wenn die Option für ISAPI-Filter ausgewählt wird, in einem zweiten Schritt die Ereignisse angeben, auf die der Filter reagieren soll. Zur Programmierung von ISAPIFiltern gibt es die MFC-Klassen CHttpFilter und CHttpFilterContext.
5.4.5
Tipps zur Vorgehensweise
왘 ISAPI-Server-Erweiterungen müssen Thread-sicher programmiert werden. Der Zugriff auf globale Daten und Datenelemente der Klasse CHttpServer muss daher durch die Synchronisationshilfsmittel von Windows geschützt werden. Es bietet sich die Verwendung der MFC-Klasse CCriticalSection an, um kritische Abschnitte zu schützen. 왘 Die Zuordnung zwischen in dem URL angegebenen Kommandos und den Funktionen der Server-Erweiterung erfolgt durch die Parsemap. Hier müssen auch die Parameter der HTTPKommandos definiert werden. Die Entwicklungsumgebung bietet leider keine Unterstützung bei der Verwaltung der Parsemap, so dass diese von Hand bearbeitet werden muss. Der ISAPI-Assistent trägt lediglich die Funktion mit dem Namen Default in die Parsemap ein. 왘 Jede ISAPI-Server-Erweiterung sollte eine Funktion definieren, die ausgeführt wird, wenn der Client kein Kommando in dem URL angibt. Diese Default-Funktion wird bei Projekten, die mit dem Assistenten für ISAPI-Erweiterungen angelegt werden, automatisch erzeugt. Sie heißt Default. Das Makro DEFAULT_PARSE_COMMAND kennzeichnet eine Funktion als Default-Funktion. Es muss am Ende der Parsemap stehen.
5.4.6
Zusammenfassung
Die ISAPI-Schnittstelle ermöglicht es, Erweiterungen für Windows-basierte Webserver zu erstellen. Die mit Hilfe von ISAPI erstellten Server-Erweiterungen laufen innerhalb des Servers
Zusammenfassung Internetprogrammierung
649
selbst ab und sind damit schneller als externe CGI-Programme. Für den MFC-Programmierer hat das den Vorteil, dass er ISAPIErweiterungen in seiner gewohnten Programmierumgebung erstellen kann und keine neue Programmiersprache, wie beispielsweise Perl, lernen muss. Von den MFC bereitgestellte Funktionalität, wie zum Beispiel die Datenbankschnittstellen zu ODBC und Jet, kann innerhalb von Server-Erweiterungen verwendet werden. Mittlerweile wird ISAPI nicht nur von Microsoft-Server-Software unterstützt, sondern auch von dem meistverwendeten Server des Internets, Apache. Damit kann ISAPI als Standardschnittstelle zur Webserver-Programmierung auf Windows gelten.
5.5
Zusammenfassung Internetprogrammierung
Tabelle 5.1 vergleicht die drei in diesem Kapitel vorgestellten Technologien mit WinSock, der Standardschnittstelle zur Internetprogrammierung. HTMLAnsicht
WinInet
ISAPI
WinSock
Protokolle
alle Protokolle des Microsoft Internet Explorers
FTP, Gopher, HTTP
HTTP
müssen selbst implementiert werden
Client- oder Servertechnologie
Client
Client
Server
Client oder Server
HTML-Darstellung
ja
nein
nein
nein
benötigte Software
Microsoft Internet Explorer
WinInetBibliothek
Webserver mit ISAPISchnittstelle
keine
MFC-Unterstützung
ja
ja
ja
ja
Tabelle 5.1: Gegenüberstellung der Internettechnologien
Die in diesem Kapitel vorgestellten Technologien decken drei verschiedene Aufgabenfelder ab, die sich bei der Internetprogrammierung stellen:
650
5
Internetprogrammierung
왘 HTML-Ansichten erlauben die Verwendung der Seitenbeschreibungssprache HTML in eigenen Programmen. HTMLDokumente können lokal oder über das Internet geladen werden. 왘 WinInet ermöglicht die Programmierung von Client-Programmen auf der Basis der Protokolle FTP, Gopher und HTTP. 왘 ISAPI erlaubt die Implementierung serverseitiger Funktionalität durch Erstellung von Modulen, die ein Server mit ISAPISchnittstelle ausführen kann. Für alle drei Teilbereiche gilt, dass die Programmierschnittstellen gut in die MFC-Klassenbibliothek integriert worden sind. Damit sind sie für den Programmierer leicht zu verwenden und sie können mit anderen durch die MFC-Klassenbibliothek unterstützten Technologien kombiniert werden. Die Programmierung der drei beschriebenen Schnittstellen ist meist einfacher als die Verwendung der WinSock-Schnittstelle.
A Glossar Abbildungsmodus
Der Abbildungsmodus bestimmt, wie an die Zeichenfunktionen des GDI übergebene logische Koordinaten in die physikalischen Koordinaten des Gerätekontexts umgerechnet werden. Der Abbildungsmodus hilft, Programmcode zur Grafikausgabe von umständlichen Umrechnungen und Skalierungen zu befreien.
ActiveX
Sammelbegriff für COM-basierte Technologien und teilweise auch für Internettechnologien.
ActiveX-Dokument Ein ActiveX-Dokument ist ein Dokument, das innerhalb einer fremden Containerapplikation bearbeitet werden kann. Zum Beispiel können Excel-Tabellen im Microsoft Internet Explorer bearbeitet werden. ActiveX-Dokumente sind eine Weiterentwicklung der OLE-Technologie. ActiveX-Steuerelement
Ein Steuerelement, das auf der Basis einer Reihe von COM-Schnittstellen erstellt wird und sich dann programmiersprachenunabhängig in einer großen Anzahl von Entwicklungsumgebungen verwenden lässt.
Aggregation
Eine Wiederverwendungstechnik, bei der mehrere COM-Objekte zu einer Einheit zusammengesetzt werden.
Ansichtsklasse
Ansichtsklassen sind ein Teil der Dokument-AnsichtArchitektur. Sie sind der Teil eines MFC-Programms, der die visuelle Darstellung und die Datenausgabe vornimmt.
AnwendungsAssistent
Ein Hilfsprogramm der Visual C++ Entwicklungsumgebung. Es dient zur Erstellung von voll funktionsfähigen Programmgerüsten für Anwendungen, die die MFC verwenden.
Anwendungsgerüst
Ein Framework, eine Reihe von Klassen oder eine Klassenbibliothek, die eine Struktur für Anwendungsprogramme vorgibt. Durch ein Anwendungsgerüst wird die Programmerstellung wesentlich vereinfacht, der Programmierer muß sich allerdings an die Strukturen des Anwendungsgerüstes halten.
652
A
Glossar
API
Application Programmers Interface, allgemeine Bezeichnung für die Programmierschnittstelle zwischen Anwendungsprogrammen und einem (Sub-)System. Dabei ist das (Sub-) System oft das Betriebssystem, kann aber auch ein anderes Programm oder eine Programmbibliothek sein.
Applikationsobjekt
Ein globales MFC-Objekt, das die Applikation repräsentiert. Es gibt genau ein Applikationsobjekt in jeder MFCAnwendung. Es wird von der Klasse CWinApp abgeleitet.
ATL
Active Template Library. Eine Klassenbibliothek speziell zur COM-Programmierung.
Attribut
Ein Feld einer Datenbanktabelle.
Attributierte Programmierung
Erweiterung des C++-Kompilers um Konstrukte zur vereinfachten COM-Programmierung.
Ausnahme
Ein Mechnismus zur Fehlerbehandlung innerhalb der Sprache C++. Das Auftreten einer Ausnahme unterbricht den Kontrollfluß und setzt ihn in einem speziellen Bereich zur Fehlerbehandlung fort.
Automation
Eine auf COM basierende Technologie, die die Fernsteuerung von Programmen ermöglicht.
Black Box
Eine Betrachtungsweise bei der Implementierung von Modulen. Das Innere eines Moduls ist nicht sichtbar, es ist die »Black Box«.
Blit
Bit Block Transfer. Hardwareunterstütztes Kopieren von rechteckigen Bildausschnitten auf der Grafikkarte, im Hauptspeicher und zwischen Grafikkarte und Hauptspeicher.
C#
Eine neue Programmiersprache, die in Zusammenhang mit der Microsoft .NET-Technologie eingeführt wurde. C# ist die primäre Programmiersprache des .NET Frameworks und besitzt Ähnlichkeiten mit den Sprachen C++ und Java.
CGI
Common Gateway Interface. Eine Konvention, nach der Webserver externe Programme aufrufen und mit diesen Daten austauschen können. Durch CGI lässt sich die Funktionalität des Webservers erweitern.
CLSID
Class Identifier. Eine GUID, die eine COM-Klasse bezeichnet.
COM
Component Object Model. Ein programmiersprachenunabhängiges Modell zur Erstellung binärer Softwarekomponenten.
COM-Klasse
Eine Klasse innerhalb von COM.
COM-Komponente Eine Softwarekomponente, die COM verwendet. Eine COM-Komponente besteht aus einer Klassenfabrik und einer oder mehererer COM-Klassen.
A
Glossar
COM-Laufzeitbibliothek
653
Hilfsfunktionen des Betriebssystems für die Arbeit mit COM-Komponenten.
Common Language Eine einheitliche Laufzeitumgebung, auf der alle ProRuntime (CLR) gramme des Microsoft .NET-Frameworks aufbauen. Die CLR ist genau wie die Java Virtual Machine ein virtueller Prozessor, der seine eigene Maschinensprache, die IL besitzt COM-Schnittstelle
Die Schnittstelle einer COM-Klasse. COM-Schnittstellen sind weltweit eindeutig und werden wiederverwendet. COM-Klassen können mehrere COM-Schnittstellen besitzen.
DAO
Data Access Objects. Eine Programmierschnittstelle zum Jet-Datenbankkern.
Datenbanktabelle
Eine Datenbanktabelle ist eine zweidimensionale Datenstruktur in einer relationalen Datenbank. Die Zeilen enthalten die Datensätze, die Spalten die Attribute.
DBMS
Database Management System. Eine Software zur Erstellung und Verwaltung von Datenbanken.
DCOM
Distributed COM. Die netzwerkfähige, verteilte Version von COM. Auf verschiedenen Computern ablaufende DCOM-Server und Clients können über Rechnernetze miteinander zusammenarbeiten.
Deadlock
Ein Zustand, in dem mindestens zwei Prozesse oder Threads Zugriff auf eine Ressource erhalten wollen, die von dem jeweils anderen Prozess oder Thread gehalten wird. Es kommt zu einer gegenseitigen, nicht auflösbaren Blockade.
DDE
Dynamischer Datenaustausch. Eine ältere Technologie zum Datenaustausch zwischen Windows-Anwendungen. DDE verliert durch den Erfolg von COM an Bedeutung.
DDV
Dialog Data Validation, Dialogdatenüberprüfung. DDV ist ein Mechanismus der MFC, der eine Bereichsüberprüfung von per DDX ausgetauschten Dialogdaten durchführt.
DDX
Dialog Data Exchange, Dialogdatenaustausch. DDX ist ein Mechanismus der MFC zum Austausch der Daten einer Dialogklasse mit den Steuerelementen eines Dialogs.
DFX
DAO Record Field Exchange. Ein Mechanismus der MFC zum Austausch von Daten zwischen den Datensätzen einer DAO-Satzgruppe und Membervariablen innerhalb der Satzgruppenklasse.
Dialogvorlage
Eine Vorlage für ein Dialogfeld, die mit einem Ressourceneditor erstellt wird. Bei der Verwendung von Dialogvorlagen muß das Layout des Dialogs nicht durch Programmierung bestimmt werden.
654
A
Glossar
Direkte Aktivierung Ein OLE-Server wird innerhalb des Containers, in den er eingebettet ist, aktivert. Zum Beispiel lassen sich ExcelTabellen in Word direkt aktivieren. DLL
Dynamic Link Library. Eine DLL ist eine Programmbibliothek, die erst zur Laufzeit an das sie verwendende Programm gebunden wird. Dadurch lassen sich DLLs unabhängig vom Programm austauschen.
DNS
Domain Name System. Serversoftware, die die im Internet verwendeten lesbaren Namen in IP-Adressen umwandelt.
Dokumentenklasse
Dokumentenklassen sind ein Teil der Dokument-AnsichtArchitektur. Sie verwalten die Daten einer MFC-Anwendung.
DokumentAnsicht-Architektur
Der zentrale Teil des MFC-Anwendungsgerüsts, der die Struktur zur Repräsentation und Speicherung von Daten eines MFC-Programms vorgibt. Namensgebender Teil der Dokument-Ansicht-Architektur sind die Dokumentenund Ansichtsklassen der MFC.
DSN
Data Source Name. Eine ODBC-Datenquelle, die über die Systemsteuerung eingerichtet werden kann.
Duale Schnittstelle
Ein COM-Schnittstelle, die durch Automation und durch normale COM-Methoden angesprochen werden kann.
Dynaset
Eine dynamische Satzgruppe, die Änderungen an den Daten der Datenbank automatisch reflektiert.
Eigenschaft
Eine von außen zugängliche Variable eines Automationsservers.
Einbettung
1.) Eine Wiederverwendungstechnik bei COM-Klassen. Eine COM-Klasse wird dabei in eine andere COM-Klasse eingebettet. 2.) Das Einfügen eines OLE-Servers in ein Verbunddokument. Eingebettete OLE-Server speichern ihre Daten in dem Dokument, in das sie eingebettet sind.
Ereignis
Ereignisse werden bei ActiveX-Steuerelementen verwendet. Es sind Automationsausfrufe, die das Steuerelement beim Container vornimmt.
ERM
Entity Relationship Model. Eine Diagrammform, die zur Modellierung von Datenbeziehungen verwendet wird.
Fensterfunktion
Die Fensterfunktion ist eine Callback-Funktion, die von Windows aufgerufen wird, um Nachrichten an ein Fenster zu übergeben. In MFC-Programmen ist die Fensterfunktion für Programmierer nicht sichtbar, Nachrichten werden hier durch Nachrichtenzuordnungstabellen verwaltet.
A
Glossar
655
Framework
Eine Klassenbibliothek, die dem Programmierer eine Programmstruktur vorgibt. Typisch für Frameworks ist die Umkehrung des Kontrollflusses: Nicht der Programmierer ruft Funktionen auf, sondern Funktionen des Programmierers werden vom Framework aufgerufen.
Fremdschlüssel
Eine Kombination einer oder mehrerer Attribute einer Datenbanktabelle, die in einer anderen Datenbanktabelle den Primärschlüssel bildet.
FTP
File Transfer Protocol. Das wichtigste Protokoll zur Dateiübertragung im Internet.
Garbage Collector
Ein Teil der Laufzeitumgebung von Sprachen wie Java und C#, die auf eine explizite Speicherfreigabe verzichten. Der Garbage Collector sammelt nicht mehr genutzte Speicherfragmente ein und ermöglicht deren erneute Verwendung. Die Sprache C++ hat keinen Garbage Collector.
GDI
Graphics Device Interface. Die Programmierschnittstelle zur Ausgabe von Grafik unter Windows.
Gerätekontext
Ein Gerätekontext ist eine Umgebung zur Ausgabe von Grafik mit dem GDI. Alle Zeichenfunktionen können nur innerhalb eines solchen Gerätekontexts verwendet werden.
Gopher
Ein Protokoll, um Internetserver und deren Daten zu Informationshierarchien zu verknüpfen. Mit dem Erfolg des WWW hat dieses Protokoll an Bedeutung verloren.
GUI
Graphical User Interface. Eine grafische Benutzeroberfläche mit Fenstern, Symbolen und Mausbedienung.
GUID
Globally Unique Idenitifier. Eine weltweit eindeutige 128 Bit lange Integerzahl. GUIDs lassen sich durch eine Funktion der COM-Laufzeitbibliothek erzeugen und werden zur Bezeichnung von COM-Klassen und -Schnittstellen verwendet.
Handle
Unter Windows eine Integerzahl, die eindeutig eine Ressource wie ein Fenster, ein Symbol oder einen Speicherbereich beschreibt. Viele Windows-API-Funktionen arbeiten mit Handles. In MFC-Programmen sind Handles normalerweise nicht sichtbar.
Heap
Dies ist der Speicher, der innerhalb eines Programms global zur Verfügung steht. Speicher vom Heap muss angefordert und freigegeben werden. In C++ sind dazu die Operatoren new und delete zuständig.
656
A
Glossar
HTML
Hypertext Markup Language. Eine Seitenbeschreibungssprache, die den Text einer Seite durch eine Folge von Auszeichnungen (Tags) und Auszeichnungspaaren formatiert. Links erlauben das Verweisen auf andere HTML-Seiten.
HTTP
Hypertext Transfer Protocol. Das Übertragungsprotokoll des World Wide Web.
IDL
Interface Definition Language. Eine Sprache mit der COMSchnittstellen unabhängig von einer Implementierungssprache definiert werden können.
IID
Interface Identifier. Eine GUID, die eine COM-Schnittstelle bezeichnet.
Importbibliothek
Eine statische Bibliothek, die die aus einer DLL exportierten Symbole beschreibt. Durch eine Importbibliothek kann eine DLL verwendet werden, ohne explizit Programmcode zum Laden der DLL schreiben zu müssen.
Index
Ein Attribut oder eine Attributskombination einer Datenbanktabelle kann mit einem Index belegt werden. Der Index sorgt für schnelleren Zugriff auf die Daten, da er eine Sortierung gemäß der angegebenen Attribute durchführt.
Intermediate Language (IL)
Die Assembler- oder Maschinensprache der Common Language Runtime des Microsoft .NET-Frameworks. Alle .NET-Programme werden in diese Zwischensprache übersetzt, bevor sie ausgeführt werden.
Internet
Der Zusammenschluss mehrerer Millionen Computer und tausender Teilnetze zu einem globalen Netzwerk. Es ist das weltweit größte Computernetzwerk.
Intranet
Ein lokales Netzwerk (LAN), das die Technologien und Protokolle des Internets verwendet.
IP-Adresse
Internetadresse. Eine weltweit eindeutige 32-Bit-Zahl, die einen Computer im Internet identifiziert.
ISAPI
Internet Server Application Programming Interface. Diese Programmierschnittstelle wird von allen Microsoft-Webservern zur Verfügung gestellt, um darüber serverseitige Funktionalität zu implementieren. Die Schnittstelle stellt eine Alternative zu CGI dar.
Jet-Datenbankkern
Der Kern der MS-Access-Datenbank. Der Jet-Datenbankkern kann allerdings ohne MS-Access verwendet werden, beispielsweise über DAO.
Kapselung
Schutz einer Implementierung vor dem Zugriff von außen.
Klassenfabrik
Der Teil eines COM-Servers, der Instanzen von COMObjekten anlegt. Der Name Klassenfabrik ist irreführend, es handelt sich um eine Objektfabrik.
A
Glossar
657
Kommandonachricht
Eine Windows-Nachricht vom Typ WM_COMMAND.
Komponentenobjektmodell
Der deutsche Name von COM.
kritischer Abschnitt Ein Programmcodeabschnitt, in dem auf Daten von verschiedenen Prozessen oder Threads zugegriffen wird. Es können inkonsistente Daten entstehen, wenn dabei schreibender Zugriff von mehreren Prozessen oder Threads gleichzeitig erfolgt. Managed Code
Programmcode, der innerhalb der .NET-Umgebung abläuft. Managed Code kann in verschiedenen Programmiersprachen erstellt werden, Vererbung funktioniert über Sprachgrenzen hinweg.
Managed Data
Datenelemente, die unter der Verwaltung der .NETUmgebung stehen. Die .NET-Umgebung verwendet automatische Speicherzuteilung und einen Garbage Collector, um Managed Data zu verwalten.
Marshalling
Ein Mechanismus der COM-Laufzeitbibliothek, um Parameter und Rückgabewerte von COM-Methoden zwischen verschiedenen Adreßbereichen zu transportieren. Diese Adreßbereiche können Teil verschiedener Prozesse auf einem Computer, oder im Fall von DCOM, auch mehrerer Computer sein.
MDI-Anwendung
Eine Windows-Anwendung mit einem Haupfenster und einem oder mehreren Kindfenstern. Word und Excel sind Beispiele für MDI-Anwendungen.
Methode
In der Objektorientierung wird die Funktion einer Klasse als Methode bezeichnet. Dies gilt auch für die Funktionen von COM-Schnittstellen. Innerhalb der Sprache C++ spricht man jedoch von Funktionen oder Memberfunktionen.
MFC
Microsoft Foundation Classes. Eine Klassenbibliothek und ein Applikationsgerüst zur Erstellung von WindowsAnwendungen.
modales Dialogfeld
Ein modales Dialogfeld blockiert die Ausführung des Programms, aus dem es aufgerufen wird. Erst nach dem Verlassen des Dialogfelds kann mit dem Programm weiter gearbeitet werden. Daher lässt sich ein modales Dialogfeld einfach durch einen Funktionsaufruf implementieren.
Moduldefinitionsdatei
Eine Datei, die angibt, welche Symbole aus einer DLL exportiert werden sollen. Die Moduldefinitionsdatei besitzt normalerweise die Endung DEF.
658
A
Glossar
MVC
Model View Controller. Ein von den Klassen der Sprache Smalltalk eingeführtes Entwurfsmuster, um Programmcode zur Repräsentation und Darstellung von Daten, sowie der Programmsteuerung zu trennen.
Nachbedingung
Eine Bedingung oder ein Zustand der beim Austritt aus einer Funktion gegeben sein muß. Nachbedingungen lassen sich durch Zusicherungen überprüfen.
Nachricht
Eine Nachricht ist ein Ereignis, das einem Windows-Programm eine Zustandsänderung mitteilt. Nachrichten müssen von jedem Windows-Programm verarbeitet werden. Sie sind asynchron und werden durch die Nachrichtenschleife verteilt.
Nachrichtenschleife Die Nachrichtenschleife ist Bestandteil eines jeden Windows-Programms. Sie nimmt die Nachrichten für ein Programm entgegen und leitet sie zur Verarbeitung weiter. Nachrichtenzuordnungstabelle
In MFC-Programmen werden von der Nachrichtenschleife empfangene Nachrichten durch Nachrichtenzuordnungstabellen an Funktionen weitergeleitet, die die Nachrichten verarbeiten. Nachrichtenzuordnungstabellen werden durch Makros implementiert.
Name Mangling
Das Name Mangling bezeichnet einen in C++-Kompilern verwendeten Mechanismus, der mehrfach verwendete Bezeichner (beispielsweise bei Überladungen) eindeutig macht. Das Name Mangling ist nicht standartisiert.
nichtmodales Dialogfeld
Ein nichtmodales Dialogfeld erlaubt die Weiterarbeit mit dem Programm, aus dem es aufgerufen wird. Ein nichtmodales Dialogfeld ist bequem zu verwenden, da man es offen lassen kann. Nichtmodale Dialogfelder sind komplizierter in der Programmierung als modale Dialogfelder.
.NET
Eine neue Umgebung zur Entwicklung und Ausführung von Programmen. .NET enthält eigene Klassenbibliotheken (.NET Framework), eine eigene Ausführungsumgebung (Common Language Runtime) und die neue Programmiersprache C#. .NET ist allerdings programmiersprachenunabhängig und prinzipiell nicht an das WIN32-API gebunden.
.NET Framework
Das .NET Framework ist eine programmiersprachenübergreifend zur Verfügung stehende Klassenbibliothek, die sozusagen das API von .NET definiert.
NNTP
Network News Transfer Protocol. Das Protokoll zur Übertragung der Internet-Diskussionsforen (News).
A
Glossar
659
Normalform
Eine Normalform garantiert durch die Erfüllung einer Reihe von Regeln bestimmte Eigenschaften einer Datenbank. Es gibt fünf Normalformen, die aufeinander aufbauen.
Normalisierung
Normalisierung beschreibt den Vorgang, mit dem Datenbanktabellen in eine Normalform überführt werden.
OCX
Auch OLE-Steuerelement. Ein Vorläufer der ActiveXSteuerelemente.
ODBC
Open Database Connectivty. Eine datenbankunabhängige Schnittstelle zu relationalen Datenbanken.
ODBC-Treiber
Der datenbankabhängige Teil von ODBC. Für jede Datenbank muß ein eigener ODBC-Treiber erstellt werden.
ODBC-Treibermanager
Der datenbankunabhängige Teil von ODBC, der vom Programmierer angesprochen wird.
OLE
Object Linking and Embedding. OLE bezeichnet die Technologie der Verbunddokumente. Der Begiff wird allerdings teilweise auch als Synonym für COM verwendet, wie beispielsweise in der Bezeichnung OLE DB.
OLE DB
Eine universelle Schnittstelle zu Daten aller Art, die auf COM basiert. Im Gegensatz zu ODBC ist OLE DB nicht auf relationale Datenbanken beschränkt.
OLE DB-Anbieter
Der Teil der OLE DB-Architektur, der Daten anbietet, wie zum Beispiel eine Datenbank.
OLE DB-Nutzer
Der Teil der OLE DB-Architektur, der Daten verarbeitet, wie zum Beispiel ein Buchungsprogramm.
OLE-Container
Ein Programm, das die Einbettung von OLE-Servern erlaubt.
OLE-Server
Ein OLE-Server liefert das Dokument, das sich in einen OLE-Container einbetten lässt. Der OLE-Server bringt seine Menü- und Symbolleisten in den OLE-Container ein, wenn er direkt aktiviert wird.
OLE-Verb
Ein Kommando, das von einem OLE-Conatiner an einen OLE-Server gegeben wird. Es kann Aktionen wie die direkte Aktivierung oder die Wiedergabe einer eingebetten Videodatei auslösen.
Persistenz
Persistenz beschreibt die Eigenschaft eines Objekts, den internen Zustand des Objekts über dessen Existenz im Hauptspeicher zu erhalten.
Polymorphie
Polymorphie beschreibt die Möglichkeit, eine Funktion auf mehr als einen Typ anzuwenden. Polymorphie tritt in Form von Templates, Überschreibungen von virtuellen Funktionen und Überladungen auf.
660
A
Glossar
Port
Eine 16-Bit-Zahl, die die Dienstleistungen eines Servers unterscheidet. Portnummern sind per Konventionen mit Protokollen assoziiert.
Primärschlüssel
Die minimale Kombination von Attributen, die jeden Datensatz in einer Datenbanktabelle eindeutig identifiziert.
Protokoll
Ein Protokoll ist eine Vorschrift zum Datenaustausch. Das Protokoll beschreibt die verwendeten Alogrithmen und Datenstrukturen.
Prozess
Ein Prozess ist eine sich in der Ausführung befindende Instanz eines Programms, die ihren eigenen geschützten Speicherbereich besitzt.
Punktnotation
Schreibweise einer IP-Adresse in Form von vier dezimal angegebenen 8-Bit-Gruppen, die jeweils durch Punkte getrennt sind. Beispiel: 192.168.0.1
Rahmenfenster
Ein Fenster der Dokument-Ansicht-Architektur, das entweder eine Ansicht umschließt oder den Hauptrahem für alle Fenster einer MDI-Anwendung bildet (Hauptrahmenfenster).
RDBMS
Relational Database Management System. Ein DBMS, das dem relationalen Datenbankmodell folgt.
referentielle Integrität
Die referentielle Integrität besagt, daß die Fremdschlüsselwerte einer Tabelle als Primärschlüsselwerte in einer anderen Tabelle vorhanden sein müssen. Sie sorgt für konsistente Daten und kann vom DBMS überwacht werden.
reflektierte Nachricht
Reflektierte Nachrichten sind ein Mechnismus der MFC, der es erlaubt, daß Steuerelemente ihre eigenen Nachrichten behandeln können und dadurch einfacher wieder verwendbar werden.
Registrierungsdatenbank
In der Registrierungsdatenbank speichert Windows die Einstellungen des Systems und seiner Programme. Jedes Programm sollte hier seine eigenen Einstellungen sichern. Die Registrierungsdatenbank löst die früher verwendeten INI-Dateien ab.
relationales Datenbankmodell
Ein Datenbankmodell, das auf mathematischen Relationen aufbaut. Da jede Relation zweidimensional ist, werden Tabellen zur Datenspeicherung verwendet.
RFX
Record Field Exchange. Ein Mechanismus der MFC zum Austausch von Daten zwischen den Datensätzen einer ODBC-Satzgruppe und Membervariablen innerhalb der Satzgruppenklasse.
A
Glossar
661
RTTI
Runtime Type Information. Ein Mechnismus der Sprache C++, mit dem zur Laufzeit Informationen zum Typ eines Objekts abgefragt werden können. In Visual C++ muß RTTI explizit aktiviert werden. MFC-Programme verwenden einen eigenen Mechanismus zur Typabfrage, der nicht auf RTTI basiert.
Sandbox
Eine abgeschirmte Ausführungsumgebung für ein Programm. Ein Programm, das in einer Sandbox abläuft, hat keinen direkten Zugang zum Betriebssystem und kann so keinen Schaden im System anrichten. Java-Applets verwenden eine Sandbox, wenn sie in einem Internet-Browser ausgeführt werden, ActiveX-Steuerelemente dagegen nicht.
Satzgruppe
Eine Gruppe von Datensätzen, die durch eine Datenbankabfrage zurückgeliefert wird.
Scheduler
Der Teil des Betriebssystems, der die zur Verfügung stehende Rechenzeit an die laufenden Prozesse und Threads verteilt.
Schlüssel
Ein Schlüsel umfaßt ein oder mehrere Attribute einer Datenbanktabelle.
Schnittstellenzuordnungstabelle
In MFC-Programmen bilden Schnittstellenzuordnungstabellen eine Verknüpfung zwischen COM-Methoden und den sie implementierenden C++-Funktionen.
SDI-Anwendung
Ein Windows-Programm mit genau einem Fenster, das keine Unterfenster enthält. Beispiele für SDI-Programme sind der Windows Editor und WordPad.
Semaphore
Ein Hilfsmittel zur Prozesssynchronisation. Semaphoren erlauben den zählenden Zugriff auf Ressourcen.
Serialisierung
Ein Vorgang, mit der die Persistenz eines Objekts erreicht werden kann. Ein Speicherauszug des Objekts wird dazu auf einen Datenträger geschrieben.
Smart Pointer
Ein Objekt, das sich wie ein Zeiger verhält, aber neben dem Datenzugriff zusätzliche Aufgaben ausführen kann.
SMTP
Simple Mail Transfer Protocol. Das Protokoll zur Weiterleitung von E-Mail im Internet.
Snapshot
Eine Satzgruppe, die eine Momentaufnahme der Datenbank darstellt und die Änderungen der Daten nicht automatisch reflektiert.
Speichergerätekontext
Ein Gerätekontext, der nur im Speicher existiert und nicht direkt zur Grafikausgabe verwendet werden kann. Er dient normaler weise dazu, Zeichnungen im Hintergrund aufzubauen und danach die Grafik auf einmal in einen sichtbaren Gerätekontext zu kopieren.
662
A
Glossar
SQL
Structured Query Language. Die meistverbreitetste Datenabfrage- und Manipulationssprache bei Datenbanken. Es gibt eine ganze Reihe von SQL-Standards.
SSL
Secure Sockets Layer. Ein Protokoll, über das eine gesichterte Datenübertragung im Internet stattfinden kann. SSL gibt es mit verschiedenen Schlüssellängen, die ein unterschiedliches Maß an Sicherheit garantieren. Für Software, die längere und damit sichere Schlüssel nutzt, existieren teilweise Exportverbote der USA.
Stack
Funktionslokaler Speicher. Die lokalen Variablen einer Funktion werden auf dem Stack gespeichert, sofern sie nicht als static ausgezeichnet sind.
Standarddialogfeld
Ein Dialogfeld, das vom Betriebssystem für häufig wiederkehrende Aufgaben zur Verfügung gestellt wird. Standarddialogfelder gibt es beispielsweise zum Laden und Speichern von Dateien, zur Farb- und Schriftartauswahl und zum Drucken.
Steuerelement
Ein Bedienelement der Windows-GUI zur Dateneinund ausgabe.
Strukturierte Ablage
Ein durch COM-Schnittstellen beschriebenes Modell zur hierarchischen Speicherung von Daten.
Synchronisation
Die Regelung des Zugriffs auf Ressourcen oder kritische Abschnitte, die von mehreren Prozessen oder Threads gemeinsam genutzt werden. Die Synchronisation soll Inkonsistenzen verhindern. Zur Synchronissation werden Hilfsmittel des Betriebssystems wie zum Beispiel Semaphoren verwendet.
Thread
Ein Prozess lässt sich in mehrere Threads aufteilen. Threads besitzen keinen eigenen Speicherbereich aber können genau wie Prozesse parallel ausgeführt werden. Das Umschalten zwischen Threads ist effizienter als das Umschalten zwischen Prozessen.
TCP/IP
Transmission Control Protocol / Internet Protocol. Ein Protokollpaar, das eine gesicherte Verbindung zur Datenübertragung aufbauend auf einzelnen Datenpaketen implementiert.
Topologie
Die Anordnung der Knoten eines Netzwerks zu einer Struktur. Beispiele für Topologien sind der Stern, der Ring und der Bus.
Transaktion
Zusammenfassung mehrerer Datenmanipulationen innerhalb einer Datenbank zu einer atomaren und damit unteilbaren Einheit.
Tupel
Ein Datensatz in einer Datenbank.
A
Glossar
663
Typbibliothek
Eine Bibliothek, die eine COM-Komponente beschreibt.
UDP/IP
User Datagram Protocol / Internet Protocol. Ein Protokollpaar, das eine ungesichterte, paketorientierte Datenübertragung implementiert.
Ungarische Notation
Eine Namenskonvention bei der Programmierung, bei der der Typ einer Variablen durch die Angabe eines Präfix im Bezeichner codiert wird.
Unicode
Unicode ist ein genormter Zeichencode, der 16 Bit für die Darstellung jedes Zeichens verwendet. Dadurch lassen sich fast alle Schriftzeichen der Welt verwenden. Unicode wird von Windows NT und Windows CE unterstützt, nicht aber von Windows 95 und Windows 98.
Unmanaged Code
Unmanaged Code bezeichnet jenen Programmcode, der außerhalb der Microsoft .NET-Umgebung abläuft. Somit bestehen alle Programme, das nicht für die .NET-Umgebung geschrieben wurde, aus Unmanaged Code. Jedes MFC-Programm besteht aus Unmanaged Code. Es gibt Schnittstellen zwischen Managed und Unmanaged Code.
URL
Uniform Resource Locator. Ein allgemeines Schema, um beliebige Ressourcen im Internet zu lokalisieren. In der URL können Servername, Verzeichnis, Dateiname, Protokoll, Port und weitere Informationen angegeben werden.
VARIANT
Ein Datentyp, der bei der Automation verwendet wird. Eine Variable des Typs VARIANT kann eine breite Palette von Datentypen aufnehmen und damit gut mit schwach typisierten Programmiersprachen zusammenarbeiten.
VBX
Visual Basic Extensions. Ein Vorläufer der ActiveX-Steuerelemente, der ursprünglich für die 16-Bit Versionen von Visual Basic entwickelt worden ist.
Verbindungspunkt
Verbindungspunkte implementieren eine Technik, bei der ein COM-Server Methoden des Clients aufrufen kann. Verbindungspunkte erlauben damit eine Umkehrung der Richtung des Kontrollflusses bei COM-Servern.
Verbunddatei
Eine von Windows vorgegebene Implementierung der strukturierten Ablage.
Vereinheitlichter Datenaustausch
Ein auf COM basierendes Verfahren, um Daten unabhängig vom verwendeten Datentransportmechanismus und dem Format der Daten auszutauschen.
Verteilertabelle
In MFC-Programmen bilden Verteilertabellen eine Verknüpfung zwischen Automationmethoden und -eigenschaften und den sie implementierenden C++Funktionen.
664
A
Glossar
View
1.) Eine logische Sicht auf eine Datenbank. Views können eine Verknüpfung mehrer Datenbanktabellen nach außen wie eine Tabelle aussehen lassen. 2.) Der englische Name für Ansicht.
Vorbedingung
Eine Bedingung oder ein Zustand der beim Eintritt in eine Funktion gegeben sein muß. Vorbedingungen lassen sich durch Zusicherungen überprüfen.
vTable
Virtual Function Table. Die Tabelle der virtuellen Funktionen einer Klasse. Über die vTable werden die virtuellen Funktionen einer C++-Klasse aufgerufen. COM verwendet vTables, die zu C++ kompatibel sind.
WIN16
Bezeichnung für die 16 Bit breite Programmierschnittstelle von Windows 3.x. WIN16 steht auch unter Windows 95 und als Subsystem unter Windows NT zur Verfügung.
WIN32
Bezeichnung für die 32 Bit breite Programmierschnittstelle von Windows 95 und Windows NT. Preemtives Multitasking, Threads und lange Dateinamen sind nur mit WIN32 möglich.
Windows Metadatei
Eine Datei, die eine Folge von GDI-Befehlen speichert. Diese GDI-Befehle lassen sich zu einem späteren Zeitpunkt »abspielen«.
WinInet
Eine Programmierschnittstelle, die eine einfache Erstellung von Clientprogrammen für die Protokolle HTTP, FTP und Gopher erlaubt. Die Verwendung anderer Protokolle oder die Erstellung von Serveranwendungen ist nicht möglich.
WinMain
WinMain ist die Startfunktion eines jeden Windows-Programms. Sie ersetzt die Funktion main eines C- oder C++-Programms.
WinSock
Windows Sockets. Die Programmierschnittstelle zur Programmierung von TCP/IP-Anwendungen unter Windows.
WOSA
Windows Open System Architecture. WOSA stellt Windows-Dienstleistungen in Form von C-basierten APIs zur Verfügung. Beispiele für WOSA-Schnittstellen sind ODBC und WinSock.
WSH
Windows Scripting Host. Der Windows Scripting Host erlaubt das Ausführen von Skripten in verschiedenen Sprachen, wie Visual Basic Script, JScript und Perl, auf Betriebssystemebene. Der WSH kann außerdem Automationsserver ansteuern.
A
Glossar
665
WWW
World Wide Web. Der Teil des Internets, der das HTTPProtokoll zur Übertragung und die Sprache HTML zur Darstellung von Internetseiten verwendet. Auf das WWW wird mit Hilfe von Internetbrowsern zugegriffen.
Zusicherung
Ein Programmierhilfsmittel, mit dem der Programmierer eine Bedingung zusichert, die an einem bestimmten Punkt des Programmcodes zutreffen muss, wenn das Programm korrekt arbeitet. Zusicherungen lassen sich in den MFC durch das Makro ASSERT implementieren.
B Versionsgeschichte der MFC Die Version 1.0 der MFC wurde Anfang 1992 zusammen mit dem Microsoft C/C++-Compiler Version 7.0, also noch vor der Einführung von Visual C++, ausgeliefert. Die erste Version war eine reine 16-Bit-Version, die Dokument-Ansicht-Architektur war noch nicht Teil der MFC. Die derzeit aktuelle Version 7.0 gibt es nur noch für 32-Bit-Systeme. Zwischen der ersten und der derzeit aktuellen Version hat es eine ganze Reihe von MFC-Releases gegeben. Die Tabelle gibt einen Überblick über die MFC-Versionen. Sie basiert auf Informationen, die dem MFC-FAQ entnommen worden sind. MFCVersion
Compiler
Plattform
Anmerkungen
1.0
MSC 7.0
WIN16
Erste MFC-Version
2.0
VC++ 1.0
WIN16
Einführung der Dokument-AnsichtArchitektur
2.1
VC++ 1.1
WIN32
Erste 32-Bit-Version der MFC
2.5
VC++ 1.5
WIN16
Letzte große 16-Bit Version, Unterstützung für OLE und ODBC
2.51
VC++ 2.0
WIN16
Fehler beseitigt
2.52
VC++ 2.1
WIN16
Einführung von Eigenschaftsdialogen
2.52b
VC++ 2.2
WIN16
Fehler beseitigt
2.5c
VC++ 4.0
WIN16
Fehler beseitigt
3.0
VC++ 2.0
WIN32
Eigenschaftsdialoge und andockbare Symbolleisten
3.1
VC++ 2.1
WIN32
Unterstützung von Windows Sockets und MAPI
3.2
VC++ 2.2
WIN32
Standardsteuerelemente
4.0
VC++ 4.0
WIN32
Anpassung an Windows 95, Einführung von Thread-Klassen und OCX-Containern
4.1
VC++ 4.1
WIN32
Fehler beseitigt, Einführung der WinInetKlassen
668
B
Versionsgeschichte der MFC
MFCVersion
Compiler
Plattform
Anmerkungen
4.2
VC++ 4.2
WIN32
Fehler beseitigt, Einführung der ISAPIKlassen
WIN32
Fehler beseitigt
4.2b 4.21
VC++ 5.0
WIN32
Unterstützung der IntelliMouse™
6.0
VC++ 6.0
WIN32
HTML-Ansichten, OLE DB-Ansichten, neue Steuerelemente
7.0
VC++ NET WIN32
neue HTML-Klassen, CTimeSpan-Erweiterungen, HTML-Hilfe
Die aktuelle Version der MFC kann der Konstanten _MFC_VER entnommen werden, die immer dann definiert ist, wenn die MFC verwendet wird. Die Konstante ist in AFXVER_.H definiert. Ein Wert von 0x0421 bedeutet Version 4.2.1. Die aktuelle Version 7.0 definiert _MFC_VER als 0x0700.
C Literaturverzeichnis C.1
Bücher
Box, Don: COM: Microsofts Technologie für komponentenbasierte Softwareentwicklung. 1. Auflage. Bonn, Addison-Wesley 1998. Die »Bibel« der COM-Programmierung. Eine umfassende Einführung auf hohem Niveau. Brockschmidt, Kraig: Inside OLE. 2nd Edition. Microsoft Press 1995. Der Klassiker zu den Themenbereichen COM und OLE. Das Buch ist leider nicht mehr im Druck. Date, C.J.: An Introduction to Database Systems. Volume I. 6th Edition. Addison Wesley 1995. Eine ausführliche Einführung in die Theorie relationaler Datenbanksysteme. Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. 1. Auflage. Bonn, Addison-Wesley 1996. Dieses Buch hat die Entwurfsmuster populär gemacht. Es gibt einen detaillierten Überblick über die gängigen Entwurfsmuster. Heimann, Frank; Turianskyj, Nino: GoTo Visual C++ 6. Bonn, Addison-Wesley 1999. Eine ausführliche Einführung in die MFC-Programmierung. Kain, Eugène: The MFC Answer Book. Solutions for Effective Visual C++ Applications. Addison Wesley 1998. Das Buch beantwortet Fragen, die nach den ersten Gehversuchen mit den MFC auftreten, wenn man die MFC in der Praxis verwendet. Shepherd, George; Wingo, Scot: MFC Internals. Inside the Microsoft Foundation Class Architecture. Addison-Wesley 1996. Die Implementierung der MFC wird untersucht. Dabei fallen viele nützliche Programmiertipps und Einsichten ab.
670
C
C.2
Literaturverzeichnis
Online-Dokumentation
C.2.1 Technische Hinweise Die Online-Hilfe von Visual C++ ist eine umfangreiche Quelle für weitere Informationen. Im Rahmen der MFC-Programmierung sind insbesondere die technischen Hinweise (auf Englisch Technical Notes – oder kurz Technotes – genannt) sehr hilfreich. Der Inhalt einiger technischer Hinweise bezieht sich auf alte MFC-Versionen, teilweise sogar auf 16-Bit Versionen der MFC. Man prüfe daher, ob der Inhalt der technischen Hinweise sich auf die verwendete Version der MFC bezieht! Die technischen Hinweise befinden sich im Inhaltsverzeichnis der Online-Hilfe unter VISUAL STUDIO .NET | VISUAL C++ | VISUAL C++ REFERENCE | VISUAL C++ LIBRARIES | MFC REFERENCE | MFC TECHNICAL NOTES. Hier sind die technischen Hinweise nach Kategorien und nach Nummern sortiert.
C.2.2 Artikel und Aufsätze in der Online-Hilfe Die Online-Hilfe enthält eine große Anzahl technischer Hintergrundartikel und Aufsätze, die teilweise aus Zeitschriften und Büchern übernommen worden sind. Zu vielen Themen in der mitgelieferten MSDN Libraray finden sich Technical Articles, Periodicals, Columns und Book Excerpts. Hier lohnt es sich auf jeden Fall zu stöbern!
C.3
Informationsquellen im Internet
Das Internet und insbesondere das World Wide Web bieten einige qualitativ sehr hochwertige Informationsquellen für MFC- und Visual C++-Programmierer an.
C.3.1 MFC FAQ Die MFC Frequently Asked Questions werden von Michael Pickens verwaltet. Unter HTTP://MFCFAQ.STINGSOFT.COM/ kann man ein Installationsprogramm laden, das die jeweils neuste Version der FAQ im Format der neuen HTML-Hilfe installiert.
C.3.2 Visual C++ developers journal Auf HTTP://WWW.VCDJ.COM stehen eine Reihe interessanter und hochwertiger Artikel zu verschiedenen Bereichen der Visual C++-
Informationsquellen im Internet
Programmierung bereit. Leider ist der Zugang zu den meisten Informationen nur noch zahlenden Mitgliedern möglich.
C.3.3 MFC Programmer’s SourceBook Der Internetserver HTTP://WWW.CODEGURU.COM ist wohl die beste und umfangreichste Sammlung von frei verfügbaren MFC-Erweiterungen und Programmcodebeispielen. Daneben gibt es Newsletter und Diskussionsforen.
C.3.4 Mailinglisten Microsoft führt mehrere interessante Mailinglisten auf ihren Servern. Zu den Themen dieses Buchs passen insbesondere folgende zwei Mailinglisten: 왘 MFC: eine moderierte Mailingliste ausschließlich auf die MFC beschränkt. 왘 DCOM: eine unmoderierte Mailingliste zu den Themen COM, DCOM und ActiveX. Die Mailinglisten können über HTTP://WWW.LSOFT.COM/SCRIPTS/ und HTTP://WWW. LSOFT.COM/SCRIPTS/WL.EXE?SL1=DCOM&H=DISCUSS.MICROSOFT.COM erreicht werden. Man kann sich auch direkt beim Listserver anmelden, indem man eine Mail ohne Betreff mit dem Kommando SUBSCRIBE MFC VORNAME NACHNAME oder SUBSCRIBE DCOM VORNAME NACHNAME an [email protected] sendet. WL.EXE?SL1=MFC&H=LISTSERV.MSN.COM
C.3.5 Usenet Gruppen Im Usenet gibt es ein breites Spektrum an Programmiergruppen. Das Niveau ist meist nicht so hoch wie bei den Mailinglisten, aber die Gruppen sind teilweise durchaus lesenswert. Microsoft selbst führt eine eigene, öffentliche Hierarchie im Usenet; die Gruppen sind mit MICROSOFT.PUBLIC bezeichnet. Folgende Gruppen beschäftigen sich mit den MFC: 왘 COMP.OS.MS-WINDOWS.PROGRAMMER.TOOLS.MFC 왘 MICROSOFT.PUBLIC.VC.MFC 왘 MICROSOFT.PUBLIC.VC.MFC.DOCVIEW 왘 MICROSOFT.PUBLIC.VC.MFCOLE
671
D MFC-Programmierung im Schatten von .NET In den Zeiten von .NET finden die MFC kaum noch eine Erwähnung im offiziellen Sprachgebrauch von Microsoft. Wie in diesem Buch beschrieben ist, lässt sich innerhalb von Visual Studio .NET sehr gut mit den MFC programmieren. Die MFC wurden gegenüber der letzten Version von Visual C++ um einige wichtige Klassen, wie beispielsweise CHtmlEditView und CHtmlEditCtrl, erweitert. Trotzdem bestehen teilweise Zweifel an der weiteren Zunkunft dieser Klassenbibliothek. Um die weitere Zukunft der MFC einschätzen zu können, sollen MFC und .NET hier in kurzer Form einander gegenübergestellt werden. Die MFC sind eine Klassenbibliothek bzw. ein Framework, die auf dem WIN32-API aufsetzt. Der C++-Kompiler, der den MFC-Quelltext übersetzt, erzeugt daraus binären Programmcode für Inteloder dazu kompatible Prozessoren. Der Binärcode wird dynamisch gegen das WIN32-API gelinkt. Dieser binäre WIN32-Programmcode wird seit dem Erscheinen von .NET als Unmanaged Code bezeichnet. Das .NET Framework dagegen bringt seine eigene Programmierschnittstelle in Form einer Klassenbibliothek mit. Von hier aus kann zwar noch auf das WIN32-API zugegriffen werden, jedoch funktioniert .NET prinzipiell ohne das WIN32-API und kann damit auf anderen Betriebssystemen verwendet werden, sofern die .NET-Laufzeitumgebung dorthin portiert worden ist. Die .NETLaufzeitumgebung ist sprachunabhängig. Sie baut auf einer prozessorunabhängigen Zwischensprache (IL, Intermediate Language) auf, die von einer virtuellen Maschine ausgeführt wird (CLR, Common Language Runtime). Hier sind deutliche Parallelen zur Programmiersprache Java und der Java Virtual Machine (JVM) zu erkennen. Der innerhalb der .NET-Laufzeitumgebung ausgeführte Programmcode wird als Managed Code bezeichnet. Die neue Ver-
674
D
MFC-Programmierung im Schatten von .NET
sion von Visual Basic erzeugt direkt Managed Code. Auch die mit dem .NET Framework eingeführte Sprache C# (gesprochen ssi scharp, benannt nach der Note Cis) erzeugt Managed Code. Der C++-Kompiler kann wahlweise Managed Code oder Unmanaged Code erzeugen. Wo nun liegen die Chancen für die MFC? Realistisch betrachtet werden die MFC von Micrososoft nicht mehr im großen Stil weiterentwickelt werden. Auf der anderen Seite ist Microsoft auch nicht dafür bekannt, bestehende Systeme sofort einzustampfen. So läuft heute noch fast jedes für Windows 3.0 entwickelte 16-Bit Programm ohne Probleme auf Windows 2000 und Windows XP. Es gibt viele bestehende Programme, die die MFC verwenden und so ist zu erwarten, dass es die MFC auch in kommenden Versionen von Visual Studio geben wird. Der von den MFC erzeugte Unmanaged Code hat zwei Vorteile gegenüber innerhalb von .NET ablaufenden Programmcode: er ist – zumindest bei kleinen Programmen – effizienter in der Speichernutzung und er ist schneller. Managed Code hat die gleichen Probleme wie die Sprache Java: die virtuelle Maschine kann Programmcode nicht so schnell ausführen, wie ein echter Prozessor und sie braucht einen großen Overhead an Hauptspeicher. Um diesen Effekt zu testen, seien zwei kleine Programme vorgestellt, die jeweils einfach nur ein kleines Dialogfeld mit »Hello World« ausgeben (auf der BegleitCD im Verzeichnis AnhangD). Beide Programme sind als ReleaseVersion übersetzt worden. Startet man sie, so lässt sich ein beträchtlicher Unterschied im Hauptspeicherverbrauch feststellen, wie in der Abbildung zu sehen ist. Das .NET-Programm verbraucht mehr als sieben mal soviel Hauptspeicher wie das entsprechende MFC-Programm. Sicherlich wird sich der Mehrverbrauch an Hauptspeicher bei großen Programmen relativieren. An der Tatsache, dass Managed Code langsamer als Unmanaged Code ist, wird sich jedoch nicht viel ändern. Hier ist allenfalls langfristig mit Optimierungen zu rechnen, wie das auch im Fall der Sprache Java über die Jahre hinweg geschehen ist. Die MFC werden – zumindest in den nächsten Jahren – ihren Platz behalten, wenn es um Programme geht, die systemnah und laufzeiteffizient implementiert werden müssen. Hier stehen die MFC – wie auch schon bisher – in Konkurrenz zur direkten Programmierung des WIN32-API, während Managed Code hier selten eine
675
Alternative sein wird. Anwendungen wie das in Kapitel 2 vorgestellte Apfelmännchen, wären als Managed Code verhältnismäßig langsam und sind daher wenig sinnvoll.
Abbildung D.1: Speicherverbrauch von .NET-Anwendungen und MFC-Anwendungen im Vergleich
Die .NET-Architektur hat ihren Fokus, wie der Name andeutet, in der Erstellung von Internetanwendungen. Zwar lassen sich mit .NET auch ganz »normale« Programme schreiben, doch ist das nicht das primäre Ziel dieses Frameworks. Zum Schluss sei angemerkt, dass sich Managed Code auch in MFC-Programme einbinden lässt. Die beiden Welten stehen sich also nicht unüberbrückbar gegenüber. So lassen sich MFC-Programme stückweise nach .NET portieren, falls das denn gewünscht ist. MFC und .NET werden in Zukunft beide Teil von Visual Studio .NET sein und entsprechend ihrer Stärken und Schwächen eingesetzt werden.