Bei der Programmierung in Visual C++ und Delphi kann man auf alle Funktionen und Strukturen des Window-APIs zugreifen. Dazu muß man in Visual C++ Programmen die Header-Datei windows.h und in Delphi-Programmen die Unit Windows einbinden. Um zusätzliche, erweiterte APIs nutzen zu können, ist manchmal die Einbindung weiterer Header-Dateien bzw. Units nötig (z.B. commctrl.h / Unit CommCtrl).
Eine andere wichtige Funktionen-Sammlung stellen die Run-Time Libraries (abgekürzt RTL) dar. In beiden Sprachen werden durch diese Bibliotheken oft benötigte Routinen zur Verfügung gestellt. Zu ihnen zählen z.B. Ein-/Ausgabe-Routinen, Routinen zur String-Bearbeitung, arithmetische Routinen, Routinen zur Typkonvertierung und Speicherverwaltung. Um die RTL-Routinen nutzen zu können, müssen ebenfalls die zugehörigen Header-Dateien bzw. Units eingebunden werden. Einzige Ausnahme stellt die Unit System in Object Pascal dar, die automatisch in allen Delphi-Programmen eingebunden wird.
Visual C++ stellt bei Routinen, die Strings als Funktions-Parameter benötigen, jeweils zwei funktional identische Versionen bereit: eine kann String-Parameter im ANSI-Stringformat entgegennehmen, die andere Version (mit einem "w" im Funktionsnamen) akzeptiert String-Parameter nur im Format des erweiterten Win32-Zeichensatzes "UNI-Code". Die meisten RTL-Routinen Delphis unterstützen nur ANSI-String-Parameter.
In der folgenden Tabelle werden einige öfter verwendete RTL-Routinen aufgeführt.
|
Visual C++ |
Delphi |
Ein-/Ausgabe-Routinen: | ||
öffne Datei |
fopen |
Reset, Rewrite |
schließe Datei |
fclose |
CloseFile |
zu bestimmter Position in Datei bewegen |
fseek |
Seek |
prüfe, ob am Ende der Datei |
feof |
Eof |
aktuelle Position in Datei ermitteln |
ftell |
FilePos |
schreibe Variable in Datei |
fwrite |
Write |
Ausgabe auf Standard-Ausgabe |
printf |
Write / Writeln |
Einlesen von Standard-Eingabe |
scanf |
Read / Readln |
Routinen zur Bearbeitung null-terminierter Zeichenketten: | ||
schreibe formatierten String in Variable |
sprintf |
StrFmt |
kopiere String in einen anderen String |
strcpy, strncpy |
StrCopy, StrLCopy |
vergleiche zwei Strings |
strcmp, stricmp, strncmp |
StrComp, StrICmp, StrLComp |
hänge String an das Ende eines anderen an |
strcat, strncat |
StrCat, StrLCat |
ermittle die Anzahl der Zeichen im String |
strlen |
StrLen |
fülle String mit einem Zeichen |
_strset, _strnset |
FillChar |
suche ein Zeichen im String (von links) |
strchr |
StrScan |
suche ein Zeichen im String (von rechts) |
strrchr |
StrRScan |
suche Zeichenkette im String |
strcspn |
StrPos |
arithmetische Routinen: | ||
absoluter Wert (Betrag) |
abs |
Abs |
Sinus, Cosinus, Tangens |
sin, cos, tan |
Sin, Cos, Tan |
Sinus-, Cosinus-, Tangens- Hyperbolicus |
sinh, cosh, tanh |
Sinh, Cosh, Tanh |
Arcus- Sinus, -Cosinus, -Tangens |
asin, acos, atan |
ArcSin, ArcCos, ArcTan |
Exponentialfunktion ex |
exp |
Exp |
Potenz yx |
pow |
Power |
natürlicher und dekadischer Logarithmus |
log, log10 |
Ln, Log10 |
Wurzel |
sqrt |
Sqrt |
ganzzahliger Anteil |
modf |
Int |
Komma-Anteil |
modf |
Frac |
Typkonvertierung: | ||
Real => Integer (durch Abschneiden der Nachkommastellen) |
int() |
Trunc |
Gleitkommawert => String |
_ecvt, _fcvt, _gcvt |
FloatToStr |
Gleitkommawert <= String |
strtod |
StrToFloat |
Integer => String |
_itoa, _ltoa |
IntToStr, Str |
Integer <= String |
strtol, atoi, atol |
StrToInt, Val |
Zeit => String |
ctime |
TimeToStr |
Zeit <= String |
- |
StrToTime |
Datum => String |
strftime |
DateToStr |
Datum <= String |
- |
StrToDate |
UNI-Code String => Ansi-Code String |
wcstombs |
WideCharToString |
UNI-Code String <= Ansi-Code String |
mbstowcs |
StringToWideChar |
Speicherverwaltung: | ||
reserviere Speicher auf dem Heap |
malloc |
AllocMem, GetMem |
ändere Speichergröße |
realloc |
ReallocMem |
Speicher freigeben |
free |
FreeMem |
kopiere Bytes von der Quelle zum Ziel |
memcpy, memmove |
Move |
fülle Speicher mit angegebenem Zeichen |
memset |
FillChar |
Datei- und Verzeichnisverwaltung: | ||
Datei umbenennen |
rename |
RenameFile |
lösche angegebene Datei |
remove |
DeleteFile |
Verzeichnis erstellen |
_mkdir |
CreateDir |
lösche Verzeichnis |
_rmdir |
RemoveDir |
wechsle Arbeits-Verzeichnis |
_chdir |
SetCurrentDir |
Einige Klassen der MFC gehören nicht zum Hierarchiebaum dieser Klassenbibliothek. Sie sind nicht von der Klasse CObject abgeleitet und dienen allgemeinen Zwecken, wie der Serialisierung (Klasse CArchive), der Verwaltung von Strings, Daten und Listen. In Object Pascal werden dafür vielfach neue, elementare Typen eingeführt.
MFC |
Object Pascal / VCL |
Klasse CArchive |
- |
Klasse CString |
Typ String (AnsiString) |
Klasse CTime |
Typ TDateTime |
Klasse CTimeSpan |
Typ TDateTime |
Klasse COleDateTime |
Typ TDateTime |
Klasse COleCurrency |
Typ Currency |
Klasse COleVariant |
Typ Variant |
Naturgemäß werden in den meisten Programmen String-Typen besonders oft verwendet. Die Klasse CString in Visual C++ und der Typ String in Object Pascal stellen eine Alternative zu den umständlich zu nutzenden Char-Arrays und den Standard-Pascal-Strings (Typ ShortString), die maximal 255 Zeichen aufnehmen können, dar.
Alle Methoden der Klasse CString sind nicht-virtuelle Methoden. Die Klasse besitzt nur ein einziges Feld (=Variable), das einen Zeiger auf eine Struktur darstellt. Auch der String in Object Pascal stellt in Wahrheit nur einen Zeiger auf eine ganz ähnliche Struktur dar. Selbst wenn eine Variable vom Typ CString bzw. String statisch deklariert wurde, wird der ihr zugewiesene Text niemals auf dem Stack, sondern immer auf dem Heap abgelegt. Neben dem eigentlichen Text werden auf dem Heap noch einige Zusatzinformationen (wie z.B. die aktuelle Länge des Stringtexts) gespeichert.
CString- bzw. String-Variable besitzen keine deklarierte maximale Länge und können bei Bedarf Text bis zu einer Länge von 2 GByte aufnehmen. SizeOf(String) liefert aber, unabhängig von der Textlänge, immer 4 Byte als Ergebnis.
VC++ |
Object Pascal |
#include <afx.h> CString S = "Das ist ein Text"; printf("%d Byte", sizeof(S)); Ausgabe: 4 Byte |
var S: String; S:= 'Das ist ein Text'; writeln(SizeOf(S), ' Byte'); Ausgabe: 4 Byte |
Beim Auslesen/Zuweisen einer CString- bzw. String-Variablen wird automatisch eine Dereferenzierung des Zeigerwertes vorgenommen. Man braucht nicht zu beachten, daß man es in Wahrheit ständig mit einer Zeiger-Variablen zu tun hat. Auch die Verwaltung des dynamisch reservierten Speichers für CString- bzw. String-Variable erfolgt automatisch und erfordert keinen zusätzlichen, vom Entwickler zu schreibenden Code. Insbesondere kann der gespeicherte Text länger werden, ohne daß man sich um die Bereitstellung zusätzlichen Speichers kümmern muß. Diese Eigenschaft der CString- und String-Variablen vereinfacht die oft benötigte Verkettungs-Operation. Das Verketten ist mit dem Plus-Operator möglich.
VC++ |
Object Pascal |
CString S1, S2, S3; S1 = "Das ist ein zusam"; S2 = "men-gesetzter Text"; S3 = S1 + S2; printf("%s", S3); |
var S1, S2, S3: String; S1:= 'Das ist ein zusam'; S2:= 'men-gesetzter Text'; S3:= S1 + S2; writeln(S3); |
Der Zugriff auf einzelne Zeichen-Elemente kann wie bei Array-Typen erfolgen. Bei CString wird auf das erste Element mit der Index-Nummer 0, bei Object Pascal Strings mit der Index-Nummer 1 zugegriffen. Die Ausgabe des zweiten Zeichens erfolgt somit durch:
VC++ |
Object Pascal |
printf("%c", S3[1]); Ausgabe: a |
writeln(S3[2]); Ausgabe: a |
Der schreibende Zugriff auf einzelne Zeichen muß bei CStrings mit der Funktion SetAt(Index, Zeichen) erfolgen.
Eine direkte Typwandlung in einen Zeigertyp, der auf ein Array von Zeichen zeigt, das mit einem Null-Zeichen endet, ist problemlos möglich. Funktionen, die Zeiger erwarten (wie z.B. viele Windows API-Funktionen), können so auch mit den neuen Stringtypen genutzt werden.
VC++ |
Object Pascal |
MessageBox(0, LPCSTR(S3),"", 0);oder durch MessageBox(0, S3.GetBuffer(0),"", 0); |
MessageBox(0, PChar(S3),'',0); |
Auch umgekehrt ist die direkte Zuweisung einer Zeigertyp-Variablen an eine CString- bzw. String-Variable möglich. Der Stringtext wird in dem Fall kopiert.
VC++ |
Object Pascal |
CString Str; char* Txt; Txt = new char[50]; strcpy(Txt, "Guten Tag"); Str = Txt; delete Txt; printf("%s", Str); |
var Str: String; Txt: PChar; GetMem(Txt, 50); StrCopy(Txt, 'Guten Tag'); Str := Txt; FreeMem(Txt, 50); writeln(Str); |
Eine der Zusatzinformationen, die bei CString und String neben dem eigentlichen Stringtext auf dem Heap gespeichert wird, ist ein Referenzzähler (reference counter). Bei der Zuweisung eines Strings zu einem anderen String, wird der möglicherweise recht lange Stringtext zunächst nicht umkopiert.
VC++ |
Object Pascal |
S1 = "Guten Tag"; S2 = S1; |
S1:= 'Guten Tag'; S2:= S1; |
Das einzige, was kopiert wird, ist der 4 Byte lange Zeiger auf den Stringtext; d.h., im angegebenen Beispiel weist String-Variable S2 nach der Zuweisung auf den selben Stringtext wie S1.
Zusätzlich wird der Wert des Referenzzählers um eins erhöht. Man zählt somit die Anzahl der Verweise; der Referenzzähler hat jetzt den Wert 2. Wenn einer CString- bzw. String-Variablen ein neuer, anderer Wert zugewiesen wird, wird der Referenzzähler des vorherigen Wertes vermindert und der Referenzzähler des neuen Wertes erhöht.
VC++ |
Object Pascal |
Wert des Referenzzählers, auf den S1 zeigt |
Wert des Referenzzählers, auf den S2 zeigt |
CString S1; CString S2; S1 = "Guten Tag"; S2 = S1; S1 = "Hallo Welt"; |
var S1: String; S2: String; S1 := 'Guten Tag'; S2 := S1; S1 := 'Hallo Welt'; |
- - 1 (neu) 2 (=1+1) 1 (neu) |
- - - 2 1 (=2-1) |
Man denke sich, daß beim Object Pascal-Beispiel beide Zuweisungen an die Variable S1 dynamisch zur Laufzeit erfolgen (z.B. durch Einlesen von Text über Readln(S1)). Bei Stringkonstanten und -literalen erzeugt Delphi einen Speicherblock mit derselben Belegung wie bei einem dynamisch reservierten String, setzt den Referenzzähler aber in Wahrheit auf den Wert -1. So können Stringroutinen erkennen, daß Blöcke, die einen Referenzzählerwert -1 besitzen, nicht verändert werden dürfen.
Wenn der Referenzzähler den Wert Null erreicht, wird der dynamisch belegte Speicherplatz, der den Stringtext und die Zusatzinformationen enthält, freigegeben.
Durch das Zählverfahren werden Zuweisungs-Anweisungen sehr schnell und speichersparend abgearbeitet. Erst wenn der Inhalt einer CString / String-Variablen verändert wird und zudem der Referenzzähler einen Wert größer 1 besitzt, muß eine String-Kopie angelegt werden. Im folgenden Beispiel wird eine Änderung am String durch die Änderung eines einzelnen Zeichens im String bewirkt.
VC++ |
Object Pascal |
CString S1, S2; S1 = "Guten Tag"; S2 = S1; S1.SetAt(4, 'R'); // separate Kopie für S1 // wird automat. angelegt printf("%s\n", S1); printf("%s\n", S2); Ausgabe:
|
var S1, S2: String; S1:= 'Guten Tag'; S2:= S1; S1[5]:= 'R'; // separate Kopie für S1 // wird autom. angelegt writeln(S1); writeln(S2); Ausgabe:
|
Durch die angelegte Stringkopie wird sichergestellt, daß durch eine Modifikation nicht auch andere String-Variablen, die auf den selben Stringwert verweisen, verändert werden. Diese Vorgehensweise ist unter dem Begriff der "Copy-on-write-Semantik" bekannt.
Wenn in Object Pascal der Gültigkeitsbereich einer String-Variablen verlassen wird (wie z.B. beim Verlassen einer Funktion mit lokalen String-Variablen), wird ihr Referenzzähler automatisch um eins vermindert. Falls der Zähler dann Null ist, wird wiederum der dynamisch belegte Speicherplatz für den Stringtext freigegeben. String-Variable verhalten sich somit wie ganz gewöhnliche Variable; der dynamische Anhang wird geschickt vor dem Programmierer verborgen.
In Visual C++ werden statisch instanzierte Objekte automatisch beim Verlassen des Gültigkeitsbereichs freigegeben. Der Destruktor der Klasse CString sorgt auch hier für die Freigabe des dynamisch belegten Speichers, wenn nach dem Dekrementieren des Referenzzählers dessen Wert Null ist. CString-Variable können neben gewöhnlichen Strings auch solche im erweiterten UNI-Code Format aufnehmen.
Viele Operatoren und Funktionen sind in der CString-Klasse und in Object Pascal innerhalb der Laufzeitbibliothek in den Units System und SysUtils definiert. Das im Kapitel "2.3.2 Standardtypen" aufgeführte Beispiel zur Stringbearbeitung mit Hilfe von Zeigern auf Zeichenketten könnte mit CString bzw. String so realisiert werden:
VC++ |
Object Pascal |
#include <afx.h> CString KurzText, KurzText2; KurzText = "Hallo Welt"; printf("%s\n", KurzText); KurzText2 = KurzText; KurzText2 = KurzText2.Mid(3); printf("%s\n", KurzText2); KurzText.SetAt(3, 'b'); KurzText.SetAt(4, 'e'); printf("%s\n", KurzText); KurzText = KurzText.Left(8); printf("%s\n", KurzText.Right(5)); Ausgabe:
|
{$LONGSTRINGS ON} //ist Standard var KurzText, KurzText2: String; KurzText:= 'Hallo Welt'; writeln(KurzText); KurzText2:= KurzText; Delete(KurzText2, 1, 3); writeln(KurzText2); KurzText[4]:= 'b'; KurzText[5]:= 'e'; writeln(KurzText); Delete(KurzText, 1, 3); writeln(Copy(KurzText, 1, 5)); Ausgabe:
|
3.2.2 Architektur der Klassenbibliotheken
Die MFC und die VCL sind objektorientierte Klassenbibliotheken, die mit Visual C++ bzw. Delphi ausgeliefert werden. Sie sind in der jeweiligen Zielsprache implementiert worden (MFC in C++ und VCL in Object Pascal) und liegen fast vollständig im Quellcode vor. Ihr Studium bringt dem Leser die jeweilige Sprache nahe und kann zum Verständnis der Bibliotheken beitragen. Die in der MFC und VCL definierten Klassen können als Rahmen bei der Erstellung von eigenen Anwendungen dienen (Application Framework). Durch sie kann einfach ein Programmgerüst gebildet werden, das elementare Funktionalitäten aufweist und das mit anwendungsspezifischem Code zu füllen ist. MFC und VCL sind besonders gut auf die zahlreichen Aufgaben spezialisiert, die bei der Erstellung von Windows-Programmen anfallen und können dem Entwickler einen Großteil der Arbeit abnehmen, die bei herkömmlicher, prozeduraler Entwicklung entsteht. Sie kapseln durch eine Vielzahl von Klassen mehr oder weniger vollständig das Windows-API ab, verbauen dem Programmierer aber trotzdem nicht den direkten Zugriff darauf. Aufgrund des durch sie abgedeckten großen Funktionsumfangs sind beide Bibliotheken sehr komplex und ihre Handhabung entsprechend schwierig zu erlernen. Vollständige, fehlerfreie, aktuelle und sorgfältig aufbereitete Dokumentationen der Klassen-Bibliotheken, zum Beispiel in Form von elektronischer Online-Hilfe, stellen eine wichtige Voraussetzung dar, sie erfolgreich meistern zu können. Visual C++ und Delphi stellen sehr gute und umfangreiche Online-Hilfen bereit, die aber trotzdem noch an vielen Stellen Korrekturen und Verbesserungen vertragen können.
Die Entwicklungsumgebung von Visual C++ wurde mit Hilfe der MFC, die von Delphi mit Hilfe der VCL entwickelt. Beide Programme belegen überzeugend, daß mit den jeweiligen Klassenbibliotheken auch komplexe und anspruchsvolle Windows-Anwendungen erstellt werden können.
Die Namen aller MFC-Klassen beginnen mit dem Buchstaben "C", alle Klassennamen der VCL mit einem "T" (mit Ausnahme der Exception-Klassen, die alle mit "E" beginnen). Die Strukturansichten der Klassenbibliotheken widerspiegeln die teilweise unterschiedlichen Konzepte, die MFC und VCL zugrunde liegen.
zentrale Klassen in der MFC |
zentrale Klassen in der VCL |
![]() |
![]() |
Die Klasse TObject ist die Basisklasse aller Klassen innerhalb der VCL, gehört aber selbst nicht zur VCL, sondern ist Bestandteil der Sprachdefinition von Object Pascal und in der Unit System.pas implementiert. Auf den ersten Blick scheint diese Tatsache ein eher nebensächliches Detail zu sein. Bei genauerer Betrachtung der Klassenmodelle von C++ und Object Pascal erkennt man jedoch, daß durch die in der Basissprache von Object Pascal bereitgestellten Möglichkeiten zum Versenden und Empfangen von Botschaften, die Existenz erweiterter Laufzeit-Typinformationen, Metaklassen und Properties der Grundstein für die elegante Gestaltung einer Windows-Klassenbibliothek in Delphi gelegt wurde.
Dem Objektmodell von C++ fehlen drei der genannten objektorientierten Erweiterungen von Object Pascal. Auf die Nachbildung von Properties verzichtet die Klassenbibliothek von Visual C++. Die Klasse CCmdTarget und sogenannte Botschaftslisten (message maps) gestatten den Botschaftsaustausch zwischen wichtigen Objekten der MFC. Ein erweitertes, nicht ANSI-normiertes Laufzeit-Typinformations-Modell wird mit der Klasse CObject eingeführt.
CObject stellt eine Methode IsKindOf() bereit, die die erweiterten Laufzeit-Typinformationen auswertet. Die Funktion ermittelt, genauso wie TObjects Methode InheritsFrom(), ob ein Objekt mit irgendeiner anderen Klasse verwandt ist. Die Laufzeit-Typinformationen der Klasse CObject werden auch ausgewertet, um zur Laufzeit die Instanzierung beliebiger Klassen zuzulassen, die möglicherweise zur Übersetzungszeit noch nicht genau bekannt oder bestimmt sind.
class CMyClass : public CObject { ... }; CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CMyClass); CObject* pObject = pRuntimeClass->CreateObject();
Im Beispiel wird dynamisch eine Instanz der Klasse CMyClass angelegt und pObject zugewiesen. Das ist möglich, obwohl CMyClass keinen virtuellen Konstruktor besitzt. Die dynamische Erzeugung von Objekten wird in Object Pascal mit Hilfe von Metaklassen und virtuellen Konstruktoren realisiert.
CObject bietet außerdem Unterstützung beim Lesen von Objekten aus dauerhaftem Speicher und beim Schreiben von Objekten in dauerhaften Speicher. Das Lesen und Schreiben wird zusammengefaßt auch "Serialisierung" genannt. Um Objekte dauerhaft, "persistent" abzulegen, werden oftmals Dateien auf beständigen Datenträgern, wie z.B. Festplatten angelegt. Bei der Serialisierung legt ein Objekt seinen Status, der hauptsächlich durch den Inhalt seiner Felder bestimmt wird, auf dem permanenten Datenträger ab. Später kann das Objekt erneut erzeugt werden, indem der Objekt-Status wieder eingelesen wird. Das Objekt ist selbst für das Lesen und Schreiben seiner Daten verantwortlich. Deswegen müssen Klassen, die von CObject abgeleitet sind, die Methode "Serialize" in geeigneter Weise überschreiben.
class CMyClass : public CObject { public: DECLARE_SERIAL(CMyClass) CMyClass(){}; // Standard-Konstruktor muß definiert werden void Serialize(CArchive& archive); ... int Feld1; long Feld2; }; void CMyClass::Serialize(CArchive& archive) { CObject::Serialize(archive); // klassen-spezifische Schreib-/Lese- Aktionen if(archive.IsStoring()) archive << Feld1 << Feld2; else archive >> Feld1 >> Feld2; };
Um die "C++ Spracherweiterungen" der Klasse CObject nutzen zu können, müssen bei der Erstellung neuer Klassen spezielle Makros in der Klassendefinition aufgerufen werden. Im obigen Beispiel wird mit DECLARE_SERIAL(CMyClass) bekannt gemacht, daß die Klasse CMyClass die Funktionen zur Serialisierung benutzen wird. Bei der Implementierung der Klasse (in der CPP-Datei) muß ein weiteres Mal ein Makro aufgerufen werden: IMPLEMENT_SERIAL(CMyClass, CObject). Weitere Makros DECLARE_DYNAMIC / DECLARE_DYNCREATE und zugehörige IMPLEMENT-Makros können angewandt werden, wenn nur ein Teil der "Spracherweiterungen" benutzt werden soll.
In Delphis Klassenbibliothek VCL existiert keine direkte Entsprechung zu den Serialisierungs-Möglichkeiten fast aller MFC-Objekte. Der Klassenname "TPersistent" einer direkt von TObject abgeleiteten Klasse legt die Vermutung nahe, daß hier gleichwertige Methoden zu finden sein könnten; sie bestätigt sich aber nicht. Diese Klasse stellt vielmehr eine abstrakte Basisklasse dar, deren Methoden vom Benutzer zu überschreiben sind. Mit TStream, TMemoryStream, TBlobStream und TFileStream stellt aber auch die VCL Klassen bereit, die das flexible Lesen und Schreiben binärer Daten ermöglichen. Eine bessere, direkt nutzbare Unterstützung für die Serialisierung steht ab der Klasse TComponent zur Verfügung. Intern verwenden die Klasse TComponent und alle von ihr abgeleiteten Klassen ausgiebig Serialisierungs-Methoden, um den Status von Objekten in Ressourcen-Dateien abzulegen. Auf diese Weise werden unter anderem automatisch die Properties aller zur Design-Zeit instanzierten Komponenten in den Formular-Dateien (*.DFM) festgehalten. Methoden, die die explizite Serialisierung zusätzlicher Daten in den DFM-Dateien ermöglichen, stehen ebenfalls zur Verfügung. Die Serialisierung betreffende Themen sind in der Dokumentation Delphis ausgesprochen schlecht dokumentiert.
In der MFC ist die Klasse CCmdTarget ein direkter Nachfahre der Klasse CObject und stellt die "Botschaftslisten-Architektur" allen Nachfahr-Klassen zur Verfügung. Die Methode OnCmdMsg erlaubt das Versenden von Botschaften. Damit eine Klasse Botschaften empfangen kann, muß eine "Botschaftsliste" (message map) angelegt werden. Sie wird mitunter auch "Nachrichtentabelle" oder "Empfängerliste" genannt. Bei der Deklaration einer Klasse wird mit dem Makroaufruf DECLARE_MESSAGE_MAP() eine solche Botschaftsliste angelegt. Bei der Klassen-Implementierung (in der CPP-Datei) wird die Botschaftsliste ausserhalb der Klasse angelegt. In der Botschaftsliste werden für alle Botschaften spezielle Makros eingetragen, auf die in spezieller Weise reagiert werden soll. Für jede Botschaft ist eindeutig bestimmt, welche Methode beim Eintreffen dieser Botschaft aufgerufen werden soll. Die Botschaftsliste wird durch das Makro BEGIN_MESSAGE_MAP eingeleitet und durch das Makro END_MESSAGE_MAP beendet.
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //{{AFX_MSG_MAP(CMainFrame) ON_WM_KEYDOWN() ON_COMMAND(ID_APP_ABOUT, OnAppAbout) ON_COMMAND(ID_FILE_NEW, OnFileNew) //}}AFX_MSG_MAP END_MESSAGE_MAP()
Ähnlich wie bei den Botschaftsbehandlungs-Methoden von Object Pascal, die meist im private-Abschnitt einer Klasse deklariert werden, erfolgt hier innerhalb der Botschaftsliste die Verbindung zwischen der Botschaft, repräsentiert durch eine eindeutige Ganzzahl, und der aufzurufenden "Handler-Methode". Bei jeder empfangenen Botschaft werden die Botschafts-Nummern aller Einträge in der Botschaftsliste mit der eingegangenen Botschaft verglichen. Wird für eine Botschaft keine Botschaftsnummer und zugehörige Behandlungsroutine gefunden, wird automatisch in der Botschaftsliste der Basisklasse nach einer Behandlungsroutine gesucht. Der Name der Basisklasse muß deswegen beim Makroaufruf durch BEGIN_MESSAGE_MAP gesondert angegeben werden. Ebenso wie in Object Pascal wird die Suche solange fortgesetzt, bis entweder eine Behandlungsroutine aufgerufen oder die leere Botschaftsliste der Basisklasse CCmdTarget erreicht worden ist. Bei allen fensterbasierten Klassen (verwandt mit CWnd) wird dann die Fenster-Standardfunktion DefWindowProc der aktuellen Fensterklasse aufgerufen.
Im angegebenen Beispiel wird beim Auftreten der Botschaft ON_WM_KEYDOWN die Methode CMainFrame::OnKeyDown(...) aufgerufen; beim Auftreten der Botschaften ID_APP_ABOUT und ID_FILE_NEW werden die Methoden CMainFrame::OnAppAbout()bzw. CMainFrame::OnFileNew() aufgerufen.
Eine Besonderheit stellen die Kommentare
//{{AFX_MSG_MAP(CMainFrame) und //}}AFX_MSG_MAP
dar. Sie wurden nicht vom Programmierer geschrieben, sondern werden von Visual C++ angelegt und dienen dem "ClassWizard" als Orientierungshilfe beim Analysieren des Quelltextes. Sie werden auch Makro-Kommentare genannt und dürfen vom Programmierer nicht aus dem Quelltext entfernt werden, da der ClassWizard sonst nicht mehr benutzt werden kann.
Delphi verzichtet generell auf das Anlegen derartiger Kommentare und wertet statt dessen sehr schnell ("on the fly") den Quelltext aus.
MFC und VCL verfolgen das Ziel, die Windows-Programmierung zu vereinfachen, so daß es kaum verwundert, daß die Klassen beider Klassenbibliotheken eine Reihe von Gemeinsamkeiten aufweisen. Trotzdem basieren MFC und VCL auf grundlegend unterschiedlichen Konzepten:
Konzept der MFC |
Konzept der VCL |
Document/View-Architektur |
Komponenten-Architektur |
In den weiter vorn gezeigten Klassenbäumen sind die Klassen, denen bei der Umsetzung der jeweiligen Architektur eine besondere Bedeutung zukommt, grau unterlegt.
Die Document/View-Architektur ist dadurch gekennzeichnet, daß das "Document" als Daten-Quelle bzw. Daten-Server fungiert und durch ein "View" zur Ansicht gebracht wird.
Der Anwender erstellt neue Dokumente über den Menüpunkt "Neu" oder lädt bestehende Dokumente über den Menüpunkt "Öffnen". Er arbeitet mit dem Dokument, indem er über das View auf das Dokument einwirkt. Dokumente werden typischerweise in Dateien gespeichert.
Eine anwendungsspezifische Klasse, die von der Klasse CDocument abgeleitet wird, fungiert als Daten-Objekt. Da ein Dokument selbst kein Fenster besitzt, wurde CDocument nicht von der "Fenster"-Klasse CWnd abgeleitet. Die Ableitung von CCmdTarget stellt andererseits sicher, daß auch mit Dokument-Klassen ein Botschafts-Austausch möglich ist. In der View-Klasse - CView - wird festgelegt, wie dem Endanwender das Dokument dargestellt wird und wie er mit ihm arbeiten kann. Verschiedene View-Klassen der MFC können als Basis für die Erstellung anwendungsbezogener Views dienen: CScrollView, CFormView, CEditView, usw. Gemeinsam ist allen Views, daß sie innerhalb von "Dokument-Rahmen-Fenstern" angezeigt werden. Wenn ein Programm in der Gestalt einer SDI-Windows-Anwendung (Single Document Interface) erscheinen soll, dann dient die Klasse CFrameWnd als Rahmen-Fenster. Soll das Programm dagegen in Gestalt einer MDI-Windows-Anwendung (Multiple Document Interface) erscheinen, so dienen die Klassen CMDIFrameWnd und CMDIChildWnd als Rahmen-Fenster. Ein "Application"-Objekt, das von der Klasse CWinApp abstammt, kontrolliert alle zuvor aufgeführten Objekte und ist zudem für die Initialisierung und korrekte Beendigung der Anwendung verantwortlich.
Auch Programme, die mit Delphis VCL erstellt werden, besitzen ein einziges "Application"-Objekt, das hier von der Klasse TApplication abstammt und ebenso wie CWinApp in der MFC als Programm-Kapsel fungiert. Die VCL weist eine Komponenten-Architektur auf, die sehr eng mit der Entwicklungsumgebung Delphis verknüpft ist. Alle Objekte, die man bei der Programmentwicklung öfter benötigt, werden in einer strukturierten "Komponenten-Palette" plaziert.
Die Komponenten sind die Bausteine eines Delphi-Programms, wobei jede Komponente ein einzelnes Anwendungselement, wie z.B. ein Dialogelement, eine Datenbank oder eine Systemfunktion darstellt. Alle Komponenten sind von der Klasse TComponent abgeleitet, die somit das grundlegende Verhalten aller Komponenten bestimmt. Delphis Entwicklungsumgebung und die VCL unterscheiden zwischen einer Designphase und einer Laufzeitphase. Eine Komponente kann prüfen, ob sie sich aktuell in der Designphase (im Formulardesigner) befindet:
if csDesigning in ComponentState then ... // im Designer
Komponenten können durch derartige Prüfungen ganz bewußt ein verändertes Verhalten zur Designzeit aufweisen. Normalerweise aber verhalten sich Komponenten während der Designzeit genauso wie zur Laufzeit des endgültig übersetzten Programms. Die Komponentenpalette kann beliebig erweitert werden, indem neue Klassen in der Entwicklungsumgebung angemeldet werden. Damit sich auch neu hinzugefügte Komponenten zur Designzeit genauso wie zur Laufzeit verhalten, werden alle Komponenten, die sich bei Delphi anmelden, übersetzt und in eine DLL-Bibliothek aufgenommen (Standard-Name Cmplib32.DCL). Während der Designzeit können Komponenten aus der Komponentenpalette ausgewählt werden und auf "Formularen", den Hauptfenstern der zukünftigen Anwendung, plaziert werden. Beim "Plazieren" einer Komponente erzeugt Delphi eine dynamische Objektinstanz von dieser Klasse. Einige Formulare erscheinen im endgültigen Programm allerdings nicht als Fenster. Solche Formulare existieren nur zur Designzeit und dienen z.B. als sogenannte Daten-Module.
Neben sichtbaren Komponenten, wie Edit-Feldern, Schaltern, Listboxen usw. können in der Komponentenpalette auch solche Komponenten aufgenommen werden, die zur Laufzeit eines Programms unsichtbar sind. In der Standard-Version der VCL zählen dazu z.B. Timer oder Datenquell-Objekte für den Zugriff auf Datenbanken. Alle zur Laufzeit sichtbaren Komponenten müssen von der Klasse TControl abgeleitet werden. Sichtbare Komponenten werden in der VCL in jene unterteilt, die mit ihrer Komponente ein eigenes Fenster darstellen (und somit ein Handle von Windows beanspruchen) und in jene, die kein eigenes Window-Handle benötigen. Alle Komponenten, die von der Klasse TGraphicControl abgeleitet werden, verzichten auf den eigenen Fenster-Status und schonen damit die Windows-Ressourcen. Solche Komponenten können zur Laufzeit nicht fokussiert werden und erhalten keine Windows-Botschaften. Sie erhalten somit auch keine WM_PAINT Botschaft und werden deswegen von dem Formular dargestellt, auf dem sie plaziert wurden. Alle Komponenten, die von der Klasse TWinControl abgeleitet werden, besitzen automatisch ein eigenes Fenster-Handle. Die Klasse TWinControl ähnelt der Klasse CWnd in der MFC, bei der ebenfalls erstmals ein Fenster-Handle zur Klassendefinition dazukommt. Eine Entsprechung zur Klasse TGraphicControl existiert in der MFC nicht.
Während der Designphase ist ein Objektinspektor das Bindeglied zwischen dem äußeren Erscheinungsbild der Anwendung und dem Quelltext, der der Anwendung zugrundeliegt. Alle Properties einer Klasse, die im published-Abschnitt aufgeführt wurden, erscheinen als Einträge im Objektinspektor. Mit seiner Hilfe können die Property-Werte aller Komponenten und Formulare einer Anwendung festgelegt und jederzeit verändert werden.
Der Verwaltung von und dem Umgang mit Fenstern fällt bei der Programmentwicklung unter Windows eine zentrale Rolle zu. In der MFC existieren für die verschiedenen grundlegenden Fenster eigene Klassen: CFrameWnd für SDI-Fenster, CMDIFrameWnd für MDI-Fenster und CDialog für Dialogboxen. Mit einem in Visual C++ integrierten Tool, dem AppWizard, kann bei der Neuerstellung von Programmen angegeben werden, zu welcher Kategorie das zukünftige Programm gehören soll. Der AppWizard generiert aus den gesammelten Informationen ein Programm-Grundgerüst, das als Basis für die weitere Programmierung dient.
|
---|
In der VCL existiert dagegen nur eine grundlegende
Hauptfenster-Klasse vom Typ TForm. Ob ein Hauptfenster wie ein SDI-, MDI- oder
Dialogfenster erscheinen soll, wird durch ein Property des Formulars bestimmt,
das mit dem Objektinspektor festgelegt werden kann. Anders als bei dem
Programm-Generator AppWizard von Visual C++ können in Delphi einmal getroffene
Entscheidungen jederzeit auf einfache Weise rückgängig gemacht werden. Wenn man
z.B. ein Programm zunächst im MDI-Stil entwickelt hat, kann man es später mit
sehr wenig Aufwand in ein Programm im SDI-Stil umwandeln, ohne eine Zeile
Programm-Code ändern zu müssen.
Formulare können sich auch wie Dialoge verhalten. Die VCL verzichtet auf die Nutzung von Dialogboxen, wie sie durch das Windows-API zur Verfügung gestellt werden und emuliert sie durch gewöhnliche Fenster. Für einen Endanwender sind keine Unterschiede zu "normalen" Dialogboxen erkennbar. Für den Programmierer ergibt sich aber der Vorteil, daß ein Dialog ohne Aufwand z.B. in ein MDI-Fenster verwandelt werden kann.
Für normale Fenster eines Programms stehen in Visual C++ keine visuellen Editoren zur Verfügung. Nur Dialoge, die auf den "echten" Dialogboxen des Window-APIs basieren, können in Visual C++ mit Hilfe eines Ressourcen-Editors graphisch gestaltet werden. Der Ressourcen-Editor ist in der Entwicklungsumgebung eingebettet. Das Design von mit ihm erstellten Dialogen wird in einer separaten Datei gespeichert (*.RC). Dialoge besitzen zunächst keine Beziehungen zu Klassen. Über ein Tool mit dem Namen ClassWizard kann jedoch für jeden Dialog eine passende Klasse generiert werden. ClassWizard erzeugt für die so generierte Klasse eine CPP- und eine Header-Datei, die wiederum eine Vielzahl von Makro-Kommentaren aufweist. Die auf dem Dialog plazierten Dialog-Elemente repräsentieren, anders als die Komponenten in Delphi, keine Objekte. Sie erscheinen in Visual C++ deswegen logischerweise auch nicht in der Klassen-Definition. Die Dialog-Elemente werden statt dessen über Dialog-ID-Nummern angesprochen. Diese Nummern erhalten symbolische Bezeichner, die in den Header-, CPP- und RC-Dateien erscheinen. Eine Umbenennung eines solchen symbolischen Bezeichners führt, anders als bei Delphi, nicht zu einer automatischen Anpassung in den Quelltext-Dateien. Die Änderungen müssen per Hand durchgeführt werden. Für jedes Dialog-Element werden in der Ressourcen-Datei nur 5 Eigenschaften gespeichert: Windows-Fensterklasse, Fenstertext, Fensterstil, Position und Ausrichtung. Auch relativ einfache Änderungen, wie eine veränderte Farbdarstellung eines Dialog-Elements, können nicht im Ressourcen-Editor vorgenommen werden. Für eine Farbänderung muß z.B. Code geschrieben werden, der in geeigneter Weise auf eine Windows-Botschaft vom Typ WM_GETDLGCOLOR reagiert. Die Änderung anderer Eigenschaften erfordert oftmals ein Subclassing des Dialog-Elements zur Laufzeit. Alle Komponenten in Delphi weisen wesentlich mehr änderbare Eigenschaften (Properties) auf. Erst durch die Nutzung von OCX-Dialog-Elementen können auch im Ressourcen-Editor von Visual C++ weitere Attribute verändert werden. OCX-Dialog-Elemente befinden sich in separaten DLL's. Sie müssen, ähnlich wie Komponenten in Delphi, beim System angemeldet werden, bevor sie im Ressourcen-Editor benutzt werden können. Allerdings müssen bei Programmen, die solche OCX-Komponenten benutzen, die betreffenden DLL's zusammen mit dem endgültigen Programm ausgeliefert werden. Die Komponenten Delphis fügen sich dagegen nahtlos in die VCL ein und werden in die entstandene EXE-Datei mit aufgenommen. Um zusätzliche DLL's braucht man sich nicht zu kümmern.
Auch die Behandlung spezieller Ereignisse von Dialog-Elementen erfordert in Visual C++ ein Subclassing des Elements - eine Ereignis-Handler-Funktion muß manuell geschrieben werden. Alle Komponenten der VCL weisen alle wichtigen Ereignisfunktionen auf; ein Subclassing ist nicht nötig.
Da Visual C++ und die MFC nur Dialog-Elemente unterstützen, die fenster-basiert sind (also ein Window-Handle besitzen), stellen selbst "Static-Text"-Elemente ein Fenster dar. Dies stellt eine Ressourcen-Verschwendung dar, da solche Dialog-Elemente niemals fokussiert werden können und deswegen auch kein Window-Handle benötigen. "Static-Text"-Elemente stellen in der VCL Ableitungen der Klasse TGraphicControl dar und belegen kein Window-Handle.
Da die Dialog-Elemente in Visual C++ keine Objekte darstellen, muß die MFC und der Programmierer einigen Mehraufwand betreiben, um die vom Endanwender eingegebenen Daten zu verifizieren und in das eigentlich objektorientierte Programm zu transferieren. Die MFC führt dazu ein Dialog-Verfifizierungsmodell (DDV = Dialog Data Validation) und ein Dialog-Datenaustauschmodell (DDX = Dialog Data eXchange) ein, das einige Arbeit abnehmen kann. Bei Delphis VCL findet der Datentransfer zwischen Windows-Element und einem Objektfeld innerhalb der Komponenten statt. Der Entwickler kann Validierungen vornehmen, indem er vordefinierte Ereignisse überschreibt und braucht sich um Datentransfers nicht zu kümmern.